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

в 18:01, , рубрики: fpga, vhdl

Привет! Хочу внести свою посильную лепту в продвижение ПЛИС. В этой статье я постараюсь объяснить, как на языке 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 <= true сигнализирую другим устройствам, что передача завершилась.

Вот как выглядит 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;
                    

Временная диаграмма

Вот как выглядит временная диаграмма передачи числа 1 на первую позицию дисплея

timing diagram

Сначала передаются данные “10011111“. Затем передается позиция числа на дисплее “00010000“ (этот параметр приходит в передатчик, как константа X”10”). В обоих случаях первым передается крайний правый бит (lsb).

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

Автор: LLDevLab

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js