Сложно ли написать свою первую программу на VHDL? Трудно сказать, но главное тут — мотивация…
Может, мне и удалось бы оттянуть этот момент, но сосед попросил сделать генератор прямоугольных импульсов, чтобы наглядно отображались и можно было бы управлять частотой и длительностью импульса.
И с точностью в 0.1 микросекунды…
И мой взгляд упал на платку с CPLD (рублей за 200, вроде) на которой были и индикаторы и кнопки. Когда-то стоит начинать работать с такой штукой, подумал я и
Выбор на чем писать VHDL или Verilog не стоял — хотя и пишу все на С, но люблю все же Ada — так что VHDL однозначно. К тому же, почитав введение в FPGA, понял, что ничего сложного и не будет (ну по крайней мере для такой простой задачи).
Итак, вначале было слово сделаем себе генератор. Частота родного клока 50 Мгц, то есть низведем его до 10, так что переключение линии тактирования будет в середине и конце. Вот что получилось.
-- 100 ns signal generator
process(clk)
variable t:integer range 0 to 5 := 0;
begin
if rising_edge(clk) then
t := t + 1;
if t = 5 then
t := 0;
tact <= not tact;
end if;
if t = 2 then
tact <= not tact;
end if;
end if;
end process;
Затем нужно как-то отображать и управлять. У нас две величины — длина периода и длина импульса, так что на длину периода отведем 3 знакоместа (с учетом десятых), и на длину периода — 3.
shared variable period : integer range 0 to 1000 := 500;
shared variable duty : integer range 0 to 1000 := 250;
shared variable dig1:std_logic_vector(3 downto 0):="0000";
shared variable dig2:std_logic_vector(3 downto 0):="0101";
shared variable dig3:std_logic_vector(3 downto 0):="0010";
shared variable di1:std_logic_vector(3 downto 0):="0000";
shared variable di2:std_logic_vector(3 downto 0):="0000";
shared variable di3:std_logic_vector(3 downto 0):="0101";
Ну для управления подойдут сигналы от кнопок, что видны внизу платы — их всего 4,
так что пусть две управляют изменением периода и импульса соответственно, одна задает знак изменения, а еще одна включает и отключает вывод генератора…
process(key1)
begin
if rising_edge(key1) then
ready <= not ready;
end if;
end process;
process(key3)
begin
if rising_edge(key3) then
if key4 = '1' then
inc_duty;
else
dec_duty;
end if;
end if;
end process;
process(key2)
begin
if rising_edge(key2) then
if key4 = '1' then
inc_period;
else
dec_period;
end if;
end if;
end process;
В управлении есть процедуры inc/dec, вот они
procedure inc_duty is
begin
if duty < period then
duty := duty + 1;
if dig1 = "1001" then
dig1 := "0000";
if dig2 = "1001" then
dig2 := "0000";
if dig3 = "1001" then
dig3 := "0000";
else
dig3 := dig3 + 1;
end if;
else
dig2 := dig2 + 1;
end if;
else
dig1 := dig1 + 1;
end if;
end if;
end procedure;
procedure dec_duty is
begin
if duty > 1 then
duty := duty - 1;
if dig1 = "0000" then
dig1 := "1001";
if dig2 = "0000" then
dig2 := "1001";
dig3 := dig3 - 1;
else
dig2 := dig2 - 1;
end if;
else
dig1 := dig1 - 1;
end if;
end if;
end procedure;
procedure inc_period is
begin
if period < 1000 then
period := period + 1;
if di1 = "1001" then
di1 := "0000";
if di2 = "1001" then
di2 := "0000";
if di3 = "1001" then
di3 := "0000";
else
di3 := di3 + 1;
end if;
else
di2 := di2 + 1;
end if;
else
di1 := di1 + 1;
end if;
end if;
end procedure;
procedure dec_period is
begin
if period > 1 then
period := period - 1;
if di1 = "0000" then
di1 := "1001";
if di2 = "0000" then
di2 := "1001";
if di3 = "0000" then
di3 := "1001";
else
di3 := di3 - 1;
end if;
else
di2 := di2 - 1;
end if;
else
di1 := di1 - 1;
end if;
end if;
end procedure;
Немного длинно и сложно (потому и свернуто), но вполне понятно.
Ну и надо как-то отображать — у нас семисегментный индикатор, и их 6 штук (вообще-то 8). Отображать будем время и, чтобы не мучится с точкой, в десятых микросекунды.
Пусть они по циклу переключаются и отображается текущая цифра:
process(tactX)
begin
case tactX is
when"000"=> en_xhdl<="11111110";
when"001"=> en_xhdl<="11111101";
when"010"=> en_xhdl<="11111011";
when"011"=> en_xhdl<="11110111";
when"100"=> en_xhdl<="11101111";
when"101"=> en_xhdl<="11011111";
when"110"=> en_xhdl<="10111111";
when"111"=> en_xhdl<="01111111";
when others => en_xhdl<="01111111";
end case;
end process;
process(en_xhdl)
begin
case en_xhdl is
when "11111110"=> data4<=dig1;
when "11111101"=> data4<=dig2;
when "11111011"=> data4<=dig3;
when "11110111"=> data4<="1111";
when "11101111"=> data4<=di1;
when "11011111"=> data4<=di2;
when "10111111"=> data4<=di3;
when "01111111"=> data4<="0000";
when others => data4<="1111";
end case;
end process;
process(data4)
begin
case data4 is
WHEN "0000" =>
dataout_xhdl1 <= "11000000";
WHEN "0001" =>
dataout_xhdl1 <= "11111001";
WHEN "0010" =>
dataout_xhdl1 <= "10100100";
WHEN "0011" =>
dataout_xhdl1 <= "10110000";
WHEN "0100" =>
dataout_xhdl1 <= "10011001";
WHEN "0101" =>
dataout_xhdl1 <= "10010010";
WHEN "0110" =>
dataout_xhdl1 <= "10000010";
WHEN "0111" =>
dataout_xhdl1 <= "11111000";
WHEN "1000" =>
dataout_xhdl1 <= "10000000";
WHEN "1001" =>
dataout_xhdl1 <= "10010000";
WHEN OTHERS =>
dataout_xhdl1 <= "11111111";
END CASE;
END PROCESS;
Признаюсь честно часть кода утащил от исходников, что шли с платкой — уж очень здорово и понятно написано! en_xhdl — этот сигнал будет управлять какой индикатор включен в такте переключения, dataout_xhdl1 — этот сигнал включает светодиоды, ну а data4 временный регистр и хранит цифру.
Осталось написать сердце, которое все и считает — собственно генератор. Тут tactX — генератор отображения, а cnt — счетчик положения в импульсе. Ну и lin — сигнал собственно генератора.
process(tact)
variable cntX : integer range 0 to 1000 := 0;
variable cnt : integer range 0 to 1000 := 0;
begin
if rising_edge(tact) then
if cntX = 0 then
tactX <= tactX + 1;
end if;
cntX := cntX + 1;
if cnt > period then
cnt := 0;
else
cnt := cnt + 1;
end if;
if cnt = 0 then
lin <= '0';
elsif cnt = duty then
lin <= '1';
end if;
end if;
end process;
Ну вот, осталось вывести данные — это делается постоянно, так что должно располагатся в блоке параллельного выполнения.
cat_led <= dataout_xhdl1;
en_led <= en_xhdl;
led1 <= not ready;
out1 <= lin when ready = '1' else '0';
out2 <= not lin when ready = '1' else '0';
В конце слепить все процессы вместе — полученный файл Quarus Prime благосклонно принял, скомпилил, сообщил, что
Top-level Entity Name v12
Family MAX II
Device EPM240T100C5
Timing Models Final
Total logic elements 229 / 240 ( 95 % )
Total pins 29 / 80 ( 36 % )
Total virtual pins 0
UFM blocks 0 / 1 ( 0 % )
Остался самый нудный этап, хотя и полностью графический — назначить сигналам конкретные пины. И все — осталось залить все в устройство и проверить! Что интересно, удалось все же уложиться в 229 ячеек, так что осталось еще аж 11 штук — но в реальности почти все сожрал интерфейс — кнопки и отображение. Собственно генератор может быть уложен в несколько ячеек — у интела есть документ, где они описывают, как уложить в 1 LUT — ну конечно, без управления…
Так что отвечая на вопрос заголовка — нет, не сложно, если знаешь С или Ада и понимаешь, как работает цифровая электроника, и да, сложно, если нет представления о базовых вещах… По крайней мере, у меня процесс написания занял день, и я получил массу удовольствия как от процесса разработки, так и от функционирующего устройства! И сосед доволен :)
Автор: oam2oam