Управление семисегментным дисплеем с помощью ПЛИС
Привет, Хабр! Хочу внести свою посильную лепту в продвижение ПЛИС. В этой статье я постараюсь объяснить, как на языке 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 is
-- При объявлении entity, поле generic не является обязательным
generic (delay_cnt: integer);
-- Описываем входные и выходные сигналы устройства
port(clk: in std_logic; out_s: out std_logic := '0');
end entity delay;
Через поле generic мы можем задать устройству нужную задержку. А в поле ports описываем входящие и исходящие сигналы устройства.
-- В секции 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 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;
Вся логика данного устройства выполняется параллельно. О том как получить формулы для данного устройства я рассказывал в одном из видео на своем канале. Кому интересно, вот ссылка на видео.
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 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;
Весь код можно посмотреть на github. Файлы с припиской *_tb.vhd — это отладочные файлы для соответствующих компонентов (например transmitter_tb.vhd — отладочный файл для передатчика). Их я на всякий случай тоже залил на github. Данный код был загружен и работал на реальной плате. Кому интересно, иллюстрацию работы кода можно посмотреть вот тут (начиная с 15:30). Спасибо за внимание.