Привет, Хабр! Хочу внести свою посильную лепту в продвижение ПЛИС. В этой статье я постараюсь объяснить, как на языке VHDL описать устройство, управляющее семисегментным дисплеем. Но перед тем как начать, хочу кратко рассказать о том как я пришел к ПЛИС и почему я выбрал язык VHDL.
Где-то пол года назад решил попробывать свои силы в программировании ПЛИС. До этого со схемотехникой никогда не сталкивался. Был небольшой опыт использования микроконтроллеров (Atmega328p, STM32). Сразу после решения освоиться с ПЛИС, встал вопрос выбора языка, который я буду использовать. Выбор пал на VHDL из-за его строгой типизации. Мне, как новичку, хотелось как можно больше возможных проблем отловить на этапе синтеза, а не на рабочем устройстве.
Почему именно семисегментный дисплей? Мигать светодиодом уже надоело, да и логика мигания им не представляет из себя ничего интересного. Логика управления дисплеем с одной стороны сложнее, чем мигание светодиодом (т. е. писать ее интереснее), а с другой достаточно простая в реализации.
Что я использовал в процессе создания устройства:
Создать устройство, которое будет в цикле показывать числа 0 — 9. Раз в секунду отображаемое на дисплее значение должно увеличиваться на 1.
Реализовать данную логику можно по-разному. Я разделю данное устройство на модули, каждый из которых будет выполнять какое-то действие и результат этого действия будет передаваться следующему модулю.
Главное устройство будет управлять корректной передачей сигналов между модулями, а также генерировать сигнал 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 описывается логика работы устройства.
Через поле generic мы можем задать устройству нужную задержку. А в поле ports описываем входящие и исходящие сигналы устройства.
Код внутри секции process исполняется последовательно, любой другой код исполняется параллельно. В скобках, после ключевого слова process указываются сигналы, по изменению которых данный процесс будет запускаться (sensivity list).
Устройство bcd_counter в плане логики выполнения идентично устройству delay. Поэтому на нем я подробно останавливаться не буду.
Вся логика данного устройства выполняется параллельно. О том как получить формулы для данного устройства я рассказывал в одном из видео на своем канале. Кому интересно, вот ссылка на видео.
В сигнал sclk я перенаправляю значение входящего в передатчик сигнала clk, но только в том случае, если устройство в данный момент выполняет передачу данных (сигнал ready = false). В противном случае значение сигнала sclk будет равно 0. В начале передачи данных (сигнал enable = true), я объединяю данные из двух входящих в устройство 8-и битных векторов (digit_pos и digit) в 16-и битный вектор (data_v) и передаю данные из этого вектора по одному биту за такт, устанавливая значение передаваемого бита в исходящий сигнал dio. Из интересного в этом устройстве хочу отметить то, что данные в dio устанавливаются на задний фронт сигнала clk, а в сдвиговый регистр дисплея данные с пина dio будут записаны по приходу переднего фронта сигнала sclk. По завершению передачи, установкой сигнала ready <= true сигнализирую другим устройствам, что передача завершилась.
Это устройство управляет другими устройствами. Здесь, перед объявлением вспомогательных сигналов, я объявляю компоненты которые буду использовать. В самой архитектуре (после ключевого слова begin) я создаю экземпляры устройств:
После экземпляров компонентов объявляется процесс. Этот процесс решает несколько задач:
Сначала передаются данные “10011111“. Затем передается позиция числа на дисплее “00010000“ (этот параметр приходит в передатчик, как константа X”10”). В обоих случаях первым передается крайний правый бит (lsb).
Весь код можно посмотреть на github. Файлы с припиской *_tb.vhd — это отладочные файлы для соответствующих компонентов (например transmitter_tb.vhd — отладочный файл для передатчика). Их я на всякий случай тоже залил на github. Данный код был загружен и работал на реальной плате. Кому интересно, иллюстрацию работы кода можно посмотреть вот тут (начиная с 15:30). Спасибо за внимание.
Где-то пол года назад решил попробывать свои силы в программировании ПЛИС. До этого со схемотехникой никогда не сталкивался. Был небольшой опыт использования микроконтроллеров (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.
После экземпляров компонентов объявляется процесс. Этот процесс решает несколько задач:
- Если передатчик не занят, то процесс инициализирует начало передачи данных
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 ...
- Если передатчик занят (tr_ready_s = false), то процесс устанавливает значение сигнала disp_refresh_s <= true (этот сигнал обозначает, что по завершении передачи нужно обновить данные на дисплее). Также устанавливается значение сигнала tr_enable_s <= false, если этого не сделать до завершения передачи, то загруженные в передатчик данные будут переданы повторно
- Устанавливает и сбрасывает сигнал 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 на первую позицию дисплея

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