Сложно ли написать свою первую программу на VHDL?

Сложно ли написать свою первую программу на VHDL? Трудно сказать, но главное тут — мотивация…

z2bvhnb6ur4cnx35lh3rh-fj-ra.png

Может, мне и удалось бы оттянуть этот момент, но сосед попросил сделать генератор прямоугольных импульсов, чтобы наглядно отображались и можно было бы управлять частотой и длительностью импульса.

И с точностью в 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 — ну конечно, без управления…

Так что отвечая на вопрос заголовка — нет, не сложно, если знаешь С или Ада и понимаешь, как работает цифровая электроника, и да, сложно, если нет представления о базовых вещах… По крайней мере, у меня процесс написания занял день, и я получил массу удовольствия как от процесса разработки, так и от функционирующего устройства! И сосед доволен :)

© Habrahabr.ru