Управление семисегментным дисплеем с помощью ПЛИС

Привет, Хабр! Хочу внести свою посильную лепту в продвижение ПЛИС. В этой статье я постараюсь объяснить, как на языке VHDL описать устройство, управляющее семисегментным дисплеем. Но перед тем как начать, хочу кратко рассказать о том как я пришел к ПЛИС и почему я выбрал язык VHDL.

Где-то пол года назад решил попробывать свои силы в программировании ПЛИС. До этого со схемотехникой никогда не сталкивался. Был небольшой опыт использования микроконтроллеров (Atmega328p, STM32). Сразу после решения освоиться с ПЛИС, встал вопрос выбора языка, который я буду использовать. Выбор пал на VHDL из-за его строгой типизации. Мне, как новичку, хотелось как можно больше возможных проблем отловить на этапе синтеза, а не на рабочем устройстве.

Почему именно семисегментный дисплей? Мигать светодиодом уже надоело, да и логика мигания им не представляет из себя ничего интересного. Логика управления дисплеем с одной стороны сложнее, чем мигание светодиодом (т. е. писать ее интереснее), а с другой достаточно простая в реализации.
Что я использовал в процессе создания устройства:

  • ПЛИС Altera Cyclone II (знаю, что он безнадежно устарел, зато у китайцев его можно купить за копейки)
  • Quartus II версии 13.0.0 (на сколько я знаю это последняя версия поддерживающая Cyclone II)
  • Симулятор ModelSim
  • Семисегментный дисплей со сдвиговым регистром


Задача


Создать устройство, которое будет в цикле показывать числа 0 — 9. Раз в секунду отображаемое на дисплее значение должно увеличиваться на 1.

Реализовать данную логику можно по-разному. Я разделю данное устройство на модули, каждый из которых будет выполнять какое-то действие и результат этого действия будет передаваться следующему модулю.

Модули


  • Данное устройство должно уметь отсчитывать время. Для подсчета времени я создал модуль «delay». Этот модуль имеет 1 входящий и 1 исходящий сигнал. Модуль принимает частотный сигнал ПЛИС и, через указанное количество периодов входящего сигнала, меняет значение исходящего сигнала на противоположное.
  • Устройство должно считать от 0 до 9. Для этого будет использоваться модуль bcd_counter.
  • Для того, чтобы зажечь сегмент на дисплее, нужно выставить в сдвиговом регистре дисплея соответствующий сегменту бит в 0, а для того, чтобы погасить сегмент в бит нужно записать 1 (мой дисплей имеет инвертированную логику). Установкой и сбросом нужных битов будет заниматься декодер bcd_2_7seg.
  • За передачу данных будет отвечать модуль transmitter.


Главное устройство будет управлять корректной передачей сигналов между модулями, а также генерировать сигнал rclk по завершению передачи данных.

Для наглядности, привожу схему данного устройства
схема


Как видно из схемы устройство имеет 1 входящий сигнал (clk) и 3 исходящих сигнала (sclk, dio, rclk). Сигнал clk приходит в 2 делителя сигнала (sec_delay и transfer_delay). Из устройства sec_delay выходит исходящий сигнал с периодом 1с. По переднему фронту этого сигнала счетчик (bcd_counter1) начинает генерировать следующее число для отображения на дисплее. После того, как число сгенерировано, декодер (bcd_2_7seg1) преобразует двоичное представление числа в горящие и не горящие сегменты на дисплее. Которые, с помощью передатчика (transmitter1), передаются на дисплей. Тактирование передатчика осуществляется с помощью устройства transfer_delay.

Код


Для создания устройства в VHDL используется конструкция из двух составляющих entity и architecture. В entity декларируется интерфейс для работы с устройством. В architecture описывается логика работы устройства.

Вот как выглядит entity устройства delay
entity delay is
    -- При объявлении entity, поле generic не является обязательным
    generic (delay_cnt: integer);
    -- Описываем входные и выходные сигналы устройства
    port(clk: in std_logic; out_s: out std_logic := '0');       
end entity delay;



Через поле generic мы можем задать устройству нужную задержку. А в поле ports описываем входящие и исходящие сигналы устройства.

Архитектура устройства delay выглядит следующим образом
-- В секции architecture описывается то, как устройство будет работать
-- С одной entity может быть связано 0 или более архитектур
architecture delay_arch of delay is
begin
    delay_proc: process(clk)
        variable clk_cnt: integer range 0 to delay_cnt := 0;
        variable out_v: std_logic := '0';
    begin
        -- Если имеем дело с передним фронтом сигнала
        if(rising_edge(clk)) then
            clk_cnt := clk_cnt + 1;                                     
                                
            if(clk_cnt >= delay_cnt) then
                -- switch/case в языке VHDL
                case out_v is
                    when '0' => 
                        out_v := '1';
                    when others =>
                        out_v := '0';
                end case;
                                
                clk_cnt := 0;
                -- Устанавливаем в сигнал out_s значение переменной out_v
                out_s <= out_v;
            end if;
        end if;
    end process delay_proc;
end delay_arch;



Код внутри секции process исполняется последовательно, любой другой код исполняется параллельно. В скобках, после ключевого слова process указываются сигналы, по изменению которых данный процесс будет запускаться (sensivity list).

Устройство bcd_counter в плане логики выполнения идентично устройству delay. Поэтому на нем я подробно останавливаться не буду.

Вот как выглядит entity и architecture декодера
entity bcd_to_7seg is
    port(bcd: in std_logic_vector(3 downto 0) := X"0";
           disp_out: out std_logic_vector(7 downto 0) := X"00");
end entity bcd_to_7seg;

architecture bcd_to_7seg_arch of bcd_to_7seg is
    signal not_bcd_s: std_logic_vector(3 downto 0) := X"0";
begin
    not_bcd_s <= not bcd;

    disp_out(7) <= (bcd(2) and not_bcd_s(1) and not_bcd_s(0)) or 
                           (not_bcd_s(3) and not_bcd_s(2) and not_bcd_s(1) 
                            and bcd(0));
                                        
    disp_out(6) <= (bcd(2) and not_bcd_s(1) and bcd(0)) or
                           (bcd(2) and bcd(1) and not_bcd_s(0));
                                        
    disp_out(5) <= not_bcd_s(2) and bcd(1) and not_bcd_s(0);
        
    disp_out(4) <= (not_bcd_s(3) and not_bcd_s(2) and not_bcd_s(1) 
                             and bcd(0)) or
                           (bcd(2) and not_bcd_s(1) and not_bcd_s(0)) or
                           (bcd(2) and bcd(1) and bcd(0));
                                        
    disp_out(3) <= (bcd(2) and not_bcd_s(1)) or bcd(0);
        
    disp_out(2) <= (not_bcd_s(3) and not_bcd_s(2) and bcd(0)) or
                           (not_bcd_s(3) and not_bcd_s(2) and bcd(1)) or
                           (bcd(1) and bcd(0));
                                        
    disp_out(1) <= (not_bcd_s(3) and not_bcd_s(2) and not_bcd_s(1)) or
                           (bcd(2) and bcd(1) and bcd(0));
        
    disp_out(0) <= '1';
end bcd_to_7seg_arch;



Вся логика данного устройства выполняется параллельно. О том как получить формулы для данного устройства я рассказывал в одном из видео на своем канале. Кому интересно, вот ссылка на видео.

В устройстве transmitter я комбинирую последовательную и параллельную логику
entity transmitter is
    port(enable: in boolean; 
           clk: in std_logic; 
           digit_pos: in std_logic_vector(7 downto 0) := X"00"; 
           digit: in std_logic_vector(7 downto 0) := X"00"; 
           sclk, dio: out std_logic := '0'; 
           ready: buffer boolean := true);
end entity transmitter;

architecture transmitter_arch of transmitter is
    constant max_int: integer := 16;
begin
    sclk <= clk when not ready else '0';             

    send_proc: process(clk, enable, ready)
        variable dio_cnt_v: integer range 0 to max_int := 0;
        variable data_v: std_logic_vector((max_int - 1) downto 0);
    begin
        -- Установка сигнала dio происходит по заднему фронту сигнала clk
        if(falling_edge(clk) and (enable or not ready)) then
            if(dio_cnt_v = 0) then
                -- Прежде всего передаем данные, потом позицию на дисплее
                -- Нулевой бит данных идет в нулевой бит объединенного вектора
                data_v := digit_pos & digit;
                ready <= false;
            end if;                     
                        
            if(dio_cnt_v = max_int) then                                
                dio_cnt_v := 0;
                ready <= true;
                dio <= '0';
            else        
                dio <= data_v(dio_cnt_v);
                dio_cnt_v := dio_cnt_v + 1;
            end if;
        end if;
    end process send_proc;
end transmitter_arch;



В сигнал sclk я перенаправляю значение входящего в передатчик сигнала clk, но только в том случае, если устройство в данный момент выполняет передачу данных (сигнал ready = false). В противном случае значение сигнала sclk будет равно 0. В начале передачи данных (сигнал enable = true), я объединяю данные из двух входящих в устройство 8-и битных векторов (digit_pos и digit) в 16-и битный вектор (data_v) и передаю данные из этого вектора по одному биту за такт, устанавливая значение передаваемого бита в исходящий сигнал dio. Из интересного в этом устройстве хочу отметить то, что данные в dio устанавливаются на задний фронт сигнала clk, а в сдвиговый регистр дисплея данные с пина dio будут записаны по приходу переднего фронта сигнала sclk. По завершению передачи, установкой сигнала ready

Вот как выглядит entity и architecture устройства display
entity display is
    port(clk: in std_logic; sclk, rclk, dio: out std_logic := '0');
end entity display;

architecture display_arch of display is
    component delay is
        generic (delay_cnt: integer); 
        port(clk: in std_logic; out_s: out std_logic := '0');
    end component;
        
    component bcd_counter is
        port(clk: in std_logic; bcd: out std_logic_vector(3 downto 0));
    end component;
        
    component bcd_to_7seg is
        port(bcd: in std_logic_vector(3 downto 0); 
               disp_out: out std_logic_vector(7 downto 0));
    end component;
        
    component transmitter is
        port(enable: in boolean; 
               clk: in std_logic; 
               digit_pos: in std_logic_vector(7 downto 0); 
               digit: in std_logic_vector(7 downto 0); 
               sclk, dio: out std_logic; 
               ready: buffer boolean);
    end component;
        
    signal sec_s: std_logic := '0';
    signal bcd_counter_s: std_logic_vector(3 downto 0) := X"0";
    signal disp_out_s: std_logic_vector(7 downto 0) := X"00";
        
    signal tr_enable_s: boolean;
    signal tr_ready_s: boolean;
    signal tr_data_s: std_logic_vector(7 downto 0) := X"00";
        
    -- Этот флаг, совместно с tr_ready_s контролирует 
    -- установку и сброс rclk сигнала 
    signal disp_refresh_s: boolean;
        
    signal transfer_clk: std_logic := '0';
begin
    sec_delay: delay generic map(25_000_000)    
                                port map(clk, sec_s);
                                
    transfer_delay: delay generic map(10)
                                port map(clk, transfer_clk);
                                
    bcd_counter1: bcd_counter port map(sec_s, bcd_counter_s);
        
    bcd_to_7seg1: bcd_to_7seg port map(bcd_counter_s, disp_out_s);
        
    transmitter1: transmitter port map(tr_enable_s, 
                                                    transfer_clk, 
                                                    X"10", 
                                                    tr_data_s, 
                                                    sclk, 
                                                    dio,
                                                    tr_ready_s);
        
    tr_proc: process(transfer_clk)
        variable prev_disp: std_logic_vector(7 downto 0);
        variable rclk_v: std_logic := '0';
    begin
        if(rising_edge(transfer_clk)) then
            -- Если передатчик готов к передаче следующей порции данных
            if(tr_ready_s) then 
                -- Если передаваемые данные не были только что переданы
                if(not (prev_disp = disp_out_s)) then            
                    prev_disp := disp_out_s;
                    -- Помещаем передаваемые данные в шину данных передатчика
                    tr_data_s <= disp_out_s; 
                    -- Запускаем передачу данных                        
                    tr_enable_s <= true;
                end if;
            else
                disp_refresh_s <= true;
                                
                -- Флаг запуска передачи данных нужно снять 
                -- до завершения передачи,
                -- поэтому снимаю его по приходу следующего частотного сигнала
                tr_enable_s <= false;
            end if;
                        
            if(rclk_v = '1') then
                disp_refresh_s <= false;
            end if;
                        
            if(tr_ready_s and disp_refresh_s) then                       
                rclk_v := '1';
            else
                rclk_v := '0';
            end if;
                        
            rclk <= rclk_v;
        end if;         
    end process tr_proc;
end display_arch;



Это устройство управляет другими устройствами. Здесь, перед объявлением вспомогательных сигналов, я объявляю компоненты которые буду использовать. В самой архитектуре (после ключевого слова begin) я создаю экземпляры устройств:

  • sec_delay — экземпляр компонента delay. Исходящий сигнал направляется в сигнал sec_s.
  • transfer_delay — экземпляр компонента delay. Исходящий сигнал направляется в сигнал transfer_clk.
  • bcd_counter1 — экземпляр компонента bcd_counter. Исходящий сигнал направляется в сигнал bcd_counter_s.
  • bcd_to_7seg1 — экземпляр компонента bcd_to_7seg. Исходящий сигнал направляется в сигнал disp_out_s.
  • transmitter1 — экземпляр компонента transmitter. Исходящие сигналы направляются в сигналы sclk, dio, tr_ready_s.


После экземпляров компонентов объявляется процесс. Этот процесс решает несколько задач:

  1. Если передатчик не занят, то процесс инициализирует начало передачи данных
                if(tr_ready_s) then
                    if(not (prev_disp = disp_out_s)) then
                        prev_disp := disp_out_s;
                        -- Помещаем передаваемые данные в 
                        -- шину данных передатчика
                        tr_data_s <= disp_out_s;
                        -- Запускаем передачу данных
                        tr_enable_s <= true;     
                    end if;
                else
                    ...
                
    


  2. Если передатчик занят (tr_ready_s = false), то процесс устанавливает значение сигнала disp_refresh_s <= true (этот сигнал обозначает, что по завершении передачи нужно обновить данные на дисплее). Также устанавливается значение сигнала tr_enable_s <= false, если этого не сделать до завершения передачи, то загруженные в передатчик данные будут переданы повторно
  3. Устанавливает и сбрасывает сигнал rclk после завершения передачи данных
                    if(rclk_v = '1') then
                        disp_refresh_s <= false;
                    end if;
                            
                    if(tr_ready_s and disp_refresh_s) then                   
                        rclk_v := '1';
                    else
                        rclk_v := '0';
                    end if;
                            
                    rclk <= rclk_v;
                    
    



Весь код можно посмотреть на github. Файлы с припиской *_tb.vhd — это отладочные файлы для соответствующих компонентов (например transmitter_tb.vhd — отладочный файл для передатчика). Их я на всякий случай тоже залил на github. Данный код был загружен и работал на реальной плате. Кому интересно, иллюстрацию работы кода можно посмотреть вот тут (начиная с 15:30). Спасибо за внимание.

© Habrahabr.ru