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

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

Комментарии (44)


  1. Dmitriy0111
    28.07.2019 17:56

    Остался самый нудный этап, хотя и полностью графический — назначить сигналам конкретные пины.

    Данный этап можно также сделать в удобном для Вас текстовом редакторе, если правильно описать .qsf файл. Да и в целом можно вообще не запускать графическое представление Quartus. Например, как здесь github.com/Dmitriy0111/wrapper_for_8bitworkshop через makefile.


  1. Sdima1357
    28.07.2019 18:23
    +1

    1 Нет такой платы за 200, начинаются с 300 без индикаторов и кнопок. (MAX ii EPM240)
    2 чем Вас таймер от stm32f103 не устраивал, за 100Р?


    1. oam2oam Автор
      28.07.2019 18:28
      +1

      1. Ну вот так мне повезло… К тому же сами чипы очень дешевы — рублей по 80 стоят, а то вдруг придется что-то на них делать…
      2. Так тем и не устроила, что не FPGA! На ней же не освоишь программирование на VHDL…


      1. Sdima1357
        28.07.2019 18:32
        -1

        К тому же сами чипы очень дешевы

        У них всего 100 циклов программирования, если я не путаю.

        На ней же не освоишь программирование на VHDL

        Ну можно поспорить :)


        1. oam2oam Автор
          28.07.2019 18:35
          +1

          Ну да, 100 — но это же целых 100! У меня пока всего один истрачен…


        1. AlexanderS
          28.07.2019 19:47
          +1

          У них всего 100 циклов программирования, если я не путаю.

          Путаете. Вернее не путаете, но путаете. То есть что-то путает производитель. Мдя… В общем, всё запутано так, что лучше почитать вот здесь. Если кратно — 10'000 циклов она точно выдержит)


        1. nckma
          29.07.2019 11:56

          Убить макс: marsohod.org/11-blog/91-killmax


          1. oam2oam Автор
            29.07.2019 12:00

            так я и не понял — все же в доке пропустили три нолика? или что? и на сколько циклов перезаписи ориентироваться, кто-нибудь подскажет?


  1. AlexanderS
    28.07.2019 19:52
    +1

    Это где ж такие платки по 200р можно прикупить? Мне кажется, что вам кажется, что они столько стоят)

    Модуль генерации корректно описан? В процессе tact по переднему фронту при старте тикнет счётчик cntX и вроде бы больше тикать никогда не будет.


    1. DanilinS
      28.07.2019 20:05

      Автор похоже закупался тогда, когда доллар был по 6 рублей.
      Сейчас аналогичная плата «EPM240 Training Board Altera MAX II CPLD» стоит в районе 1100 руб.


      1. Sdima1357
        28.07.2019 21:29

        За эти деньги можно прикупить «Xilinx XC6SLX16 Spartan 6 FPGA development board 32Mb SDRAM» ~$18- $19
        Вполне серьезная плата


        1. oam2oam Автор
          29.07.2019 06:19

          Вот, кстати, если думать о будущем, хотелось бы перейти на что-то серьезное — но так, чтобы это стоило потом применять в проектах…
          Однако выбор довольно большой — что же делать?


          1. AlexanderS
            29.07.2019 15:33

            Тут надо понимать, что ПЛИС — это просто матрица соединений и всё. Да, во «взрослых» ПЛИСах есть всякие штуки типа PLL, или DSS. Но там нет ЦАП и АЦП, энергонезависимой памяти, готовых интерфейсов типа UART/SPI/I2C (на самом деле через IP Core всё добывается, но ядра денег стоят) и прочего. Если вы захотите реагировать на уровень чего-то, то вам придётся ставить микросхему АЦП и реализовать интерфейс с ней. Если понадобиться формировать уровень — то же самое с ЦАП. Хранить калибровочные параметры или какие-то данные? Ставьте свою ПРОМ. Получается куча мороки вместо того, чтобы сразу использовать какой-нибудь микроконтроллер. ПЛИС хорош для быстрой обработки сигналов, но, как практика показывает, их сфера применения «в быту» очень узка. Иногда, бывает нужна надежность. Чтобы контроллер не завис, не сбойнул и не сформировал какую-нибудь бяку из-за «поехавшей» в памяти программы. В этом плане ПЛИС неплох, т.к. по своей сути представляет цифровую схему из набора регистров — тут логике сложно сломаться.

            Я, уже не помню для чего, как-то использовал Digilent 410-328. Это плата в форм-факторе DIP-48 на основе Xilinx Artix-7. Плата уже имеет на борту конфигурационную память, распаянный программатор и немного SDRAM. Помню, что туда без проблем влезли все интерфейсные модули (два протокола UART + SPI) и места ещё осталось достаточно. 10 тыс. лутов, конечно, гораздо лучше, чем 240 ячеек в MAX II, но и цена платы сильно побольше будет)


            1. NetNazgul
              29.07.2019 16:02
              +1

              Мы тут как-то по работе недавно покупали Zynq-модули у Trenz, на выходе получается ARM-процессор + ПЛИС довольно большой ёмкости (это в одном кристалле), + SPI Flash + пара модулей на плате, выводы и отдельный Xilinx-совместимый программатор на CPLD от Lattice, и всё это чуть меньше $100.


              1. AlexanderS
                29.07.2019 16:23

                С этой серией в живую я напрямую не сталкивался. Философия этой платформы вроде как раз в том, чтобы в ПЛИСе реализовать то, что в МК не сделать (какую-нибудь специфичный высокоскоростной интерфейс, например, или БПФ), а всё остальное — отдать обычным программистам, благо на ARM ядро хоть Debian ставь. По логике это должно увеличивать сроки разработки.


                1. NetNazgul
                  30.07.2019 08:45

                  Ну там никто не мешает и как просто ПЛИС без процессора использовать на самом деле.

                  Но вообще — позволяет выполнить и интегрировать realtime-периферию гораздо ближе к ядру процессора, чем при отдельных кристаллах, не ограничиваясь при этом softcore-процессором внутри самой ПЛИС.


            1. mpa4b
              29.07.2019 16:40

              Забавно, что именно в max II как раз таки есть энергонезависимая память, в виде юзерского кусочка флеш.


              1. AlexanderS
                29.07.2019 19:55

                А еще для неё не нужно внешней конфигурационной памяти)


                1. oam2oam Автор
                  29.07.2019 19:58

                  А вот, кстати, как осуществляется защита IP в fpga с внешней конфигурационной памятью? В MAX II это просто…


                  1. AlexanderS
                    29.07.2019 21:09

                    Во флешке лежит bitstream же, который получен с помощью САПР. Можно взять эту прошивку и залить в другой такой же ПЛИС — работать будет. Вытащить прошивку можно, но это надо чтобы вот прям очень нужно было реверсить железо для неё. Да и сам разработчик заранее может озаботиться шифрованием обмена памяти с ПЛИСом. Ну а как ведётся сам по себе учёт IP — это уже вопросы ценовой политики. IP стоят ооочень дорого)


                    1. Andy_Big
                      30.07.2019 18:29

                      Да и сам разработчик заранее может озаботиться шифрованием обмена памяти с ПЛИСом

                      Протокол загрузки конфигурации в ПЛИС строго регламентирован и не допускает никаких изменений со стороны разработчика. Так что да — посадив сниффер на линии загрузки ПЛИС можно легко и безболезненно получить «прошивку» ПЛИС.
                      Есть ПЛИС, поддерживающие шифрование конфигурации, но они более дорогие и для них нужны более дорогие загрузочные флэшки, которые так же поддерживают шифрование.


                      1. AlexanderS
                        30.07.2019 20:09

                        Да, именно так. Мало того, частичку ПЛИСа ещё нужно и постоянным питанием обеспечивать чтобы не утратился ключ в энергозависимой памяти. Ставятся батарейка или аккумулятор. Но тут нюанс в том, что нужно заранее продумать схемотехнику замены этого аккумулятора — ставят большие кондёры или ионистор, чтобы при техобслуживании через N-лет можно было быстренько заменить источник питания. Конечно, так заморачиваются только когда реально интеллектуальная собственность в прошивке очень дорогая.


                        1. Andy_Big
                          30.07.2019 20:40

                          частичку ПЛИСа ещё нужно и постоянным питанием обеспечивать чтобы не утратился ключ в энергозависимой памяти

                          Этого я не знал :) Думал, что ключ прошивается в какую-нибудь OTP-область :)


                        1. nerudo
                          31.07.2019 08:59

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


                          1. AlexanderS
                            31.07.2019 14:19

                            Я сам лично этот вопрос не изучал — коллеги по работе в каком-то проекте заморачивались, знаю как у них реализовывалось.

                            Однократно программируемый ключ можно же вытащить? Область наверняка заранее определена: электронный микроскоп + знание топологии ПЛИСа…


                            1. nerudo
                              31.07.2019 14:28

                              Вероятно можно, у нас тут специалист даже был BarsMonster но это переводит задачу на совсем другой уровень…


            1. hhba
              31.07.2019 08:56

              Иногда, бывает нужна надежность. Чтобы контроллер не завис, не сбойнул и не сформировал какую-нибудь бяку из-за «поехавшей» в памяти программы. В этом плане ПЛИС неплох, т.к. по своей сути представляет цифровую схему из набора регистров

              А эта цифровая схема из набора регистров в случае fpga чем конфигурируется, как вы думаете?


              1. AlexanderS
                31.07.2019 14:33

                Конфигурируется-то она из ПРОМки, но если будет косяк в данных, то процедура инициализации не завершится (CRC не сойдётся) и ПЛИС не загрузится.


                1. hhba
                  31.07.2019 15:19

                  Ненене, я не про то, из чего она конфигурируется, а про то, где лежат параметры конфигурации в загруженной fpga.


                  1. AlexanderS
                    31.07.2019 23:02

                    Ааа, вы про конфигурирование макроячеек и матрицы соединений? В принципе там такие же транзисторы как и везде)) Технологическая разница в изготовлении может как-то сказываться? Ну… это вопрос интересный. Надо бы на досуге поизучать.


                    1. hhba
                      31.07.2019 23:26

                      Да, я про макроячейки, но не про технологическую разницу, а про SRAM-based FPGA (то есть примерно все коммерческие СБИС, исключая CPLD-мелочь и космические антифузы). Там самые натуральные ячейки памяти, которые собственно и конфигурируют макроячейки. И если в ячейку памяти что-то этакое попадет… Уж явно в этой ситуации у ПЛИС нет никакого преимущества перед такой же ячейкой памяти в микроконтроллере.


  1. nerudo
    28.07.2019 21:05

    > хотя и пишу все на С, но люблю все же Ada

    Смахнул слезу — есть же еще люди!


  1. Pyhesty
    29.07.2019 01:32

    Я извиняюсь, не пишу на vhdl, но зная архитектуру ПЛИС предположу, что у вас должны наблюдаться гличи на сигнале генератора, так как выходной сингал не простробирован регистром…
    Единственный раз, когда пришлось работать с vhdl, намучился с переменными (когда частота подросла) и все перевел на сигналы, сигналы это физически регистр.
    А в целом отлично, что есть доступные для изучения комплекты, спасибо за статью


  1. azudem
    29.07.2019 04:59

    Во-первых, эстетически больно смотреть на отступы вашем коде, не говоря о неожиданных сменах регистра и именах.

    Во-вторых, считывать кнопки по фронту не совсем хорошая идея из-за дребезга, особенно в случае дешёвых китайских кнопок. Обычно просто сэмплят с малой частотой, 50-100 Гц.


    1. oam2oam Автор
      29.07.2019 06:26

      Я тоже хотел сначала устранить дребезг и добавить автоизменение (ну для этого надо просто процесс с частотой 2-5 герц) — но тогда не входит по размеру, а как оптимизировать не придумал, к сожалению.


    1. VioletGiraffe
      29.07.2019 10:07

      А по-моему, очень даже неплохое и удобочитаемое форматирование. Сам бы я делал чуть по-другому, но вариант автора мне нравится и более чем имеет право на жизнь, ИМХО.


  1. VioletGiraffe
    29.07.2019 10:12

    Спасибо за заметку! Смахнул скупую слезу, вспомнив, как в универе писал на VHDL железки для отладочной платы Spartan-3E.

    А посоветуйте, пожалуйста, какую отладочную плату FPGA / CPLD взять за не очень дорого, скажем, до $70, чтобы восстановить навык, ну и делать простые штуки, подобные описанной в статье? Лучше, наверное, FPGA? Есть идея подключить быстрый АЦП и попытаться сделать простой осциллограф.


    1. AlexanderS
      29.07.2019 15:43

      Можно на e-bay взять плату с Virtex4. XC4VLX100 вам не только для осциллографа хватит, поверьте) Но готовьтесь к тому, что нужно будет купить не особо дешёвый JTAG программатор. Либо же взять какую-нибудь отладочную плату на Spartan 6, выше название где-то приводили. Я в своё время маялся вот такой ерундой на марсоходе)


  1. NetNazgul
    29.07.2019 13:42
    +2

    Добро пожаловать в мир HDL! Несколько придирок, на которые хотелось бы обратить внимание. :)

    1. Строго говоря, код на HDL не является «программой». На это же напрямую намекает само название hardware description language — «язык описания аппаратуры». HDL скорее ближе к схемотехнике, чем к программированию. Хотя конечно ПЛИС дают некоторое впечатление программирования, т.к. результат можно увидеть буквально через пару минут, а не через три месяца как в случае со СБИС.
    2. Первый процесс даст вам деление на 6, а не на 5 (отсчёты с 0 до 5, 0-1-2-3-4-5).
    3. Никогда раньше не сталкивался с

    shared variable
    , в любом случае даже просто переменные использовать в VHDL лучше по минимуму и для промежуточных значений в процессах; основу, а тем более 100% выходов синхронных элементов, должны составлять сигналы
    signal
    . Если возникают сложности с использованием значений сигналов внутри процессов — следует подробнее ознакомиться к параллельными и последовательными блоками в VHDL (в Verilog логика похожа) и присвоением значений сигналам и переменным.
    4. С т.з. пользователя управление с требованием зажатия нескольких кнопок выглядит не очень приятным. Я бы предложил, например, отвести одну кнопку под выбор режима (редактирование одного или другого параметра), две под инкремент/декремент и одну под включение/выключение как есть.
    5. Если уж мы выполняем проверку «длина импульса должна быть меньше периода» при изменении импульса, то стоило бы проверять и обратный случай при изменении периода.
    6. В целом получается, что duty/period и dig/di — это живущие своей независимой жизнью одинаковые величины, отличающиеся лишь в представлении. В общем случае более логично было бы выполнять двоично-десятичное преобразование от исходных значений. Но в данном примере подозреваю, что стоял вопрос жёсткого ограничения по площади ПЛИС.
    7.
    if cnt > period then

    elsif cnt = duty then

    Здесь опять «ошибка +1» — счётчик работает на диапазоне от 0 до периода, что приводит к заданию числом period реального значения в period+1 тактов.
    8.
     variable cntX : integer range 0 to 1000 := 0;
     variable cnt : integer range 0 to 1000 := 0;

    Тут опять максимальное значение 1000, что даст нам 1001 отсчётов, а еще — возможно данное описание корректно работает с обработкой переполнения счётчика (переход из 1000 в 0), но описано несколько страшновато. В самом первом процессе счётчик более корректен — там данная проверка описания переполнения выполняется явно:
    	t := t + 1;
      	if t = 5 then
    	  t := 0;
    	  tact <= not tact;
    	end if;

    … и тут я понял, что пункт №2 как раз правильный, а у меня нет, всё дело в переменных, где сначала выполняется t := t + 1, получаем 5, а потом сразу меняем на 0. Но такое использование переменных может привести к избыточной логике. При использовании сигналов, новое значение устанавливается в сигнале только по выходу из процесса, т.о. если процесс сихронный, то результат t <= t + 1 мы «увидим» в этом процессе только на следующем такте (так же это работает и в реальности для триггеров). Совсем хорошо было бы так:
    
    signal t: integer;
    process(clk)
    begin
      if rising_edge(clk) then
        if t = 4 then
          t <= 0;
        else
          t <= t + 1;
        end if;
      end if;
    end process;
    

    Возвращаясь к счётчикам — для cntX переход через 0 не проверяется, а для cnt, как уже выше было сказано, посчитается как +1.


    1. oam2oam Автор
      29.07.2019 13:55

      Спасибо за конструктивную критику! Буду думать… Правда, не совсем понимаю, почему нельзя использовать shared variable — они же есть в языке, их использование оговорено, вроде я по уставу их использовал. Ну а с кнопками — как уж вышло, я уже выше отвечал, что хотелось бы большего, но LUTы не позволяют…


      1. NetNazgul
        29.07.2019 15:58
        +1

        Variable, используемые как де-факто регистры, — это один из верных способов выстрелить себе в ногу в VHDL. Их поведение сложнее контролировать в асинхронных (описывающих логику, а не триггер) процессах, в целом есть довольно высокий шанс получить на выходе т.н. защёлки (latch), которых не должно быть в проекте, если только не ставишь их туда вручную с конкретной целью.

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


        1. oam2oam Автор
          29.07.2019 16:32

          так у меня, как я думал, все синхронное; видимо, поэтому не ощутил проблем. А так, как подумаешь — как эти разработчики под ПЛИС думают — что-то очень уж сложное выходит.


          1. AlexanderS
            29.07.2019 20:55
            +2

            Просто когда пишется код на HDL надо понимать, что каждая строчка кода отрабатывает каждый такт. Всё работает параллельно и результаты этой работы будут доступны ровно через такт — обновляются сигналы одновременно во всём коде. Те, кто приходят из Си мыслят в цикле последовательном исполнении команд и переключиться сразу на параллельное мышление нет так просто как кажется на первый взгляд. Поэтому новички обязательно вкорячивают переменные в код, где они просто даром не нужны.

            Это не значит, что переменные вообще использовать не надо. В процедурах и функциях — там можно использовать их «как обычно». Но надо чётко понимать к чему это приводит на схемотехническом уровне в реальной схеме.

            Например надо подсчитывать чётность передаваемого байта в потоке данных. Мы ограничены во времени — на всё отводится один такт (потоковая передача). Можно сделать это так:

            process(CLK)
            begin
            if (CLK'event) and (CLK = '1') then
            
              Do <= Di;
              PAR <= Di(7) xor Di(6) xor Di(5) xor Di(4) xor Di(3) xor Di(2) xor Di(1) xor Di(0);
            
            end process;
            


            А можно сделать так:

            process(CLK)
              variable parity : std_logic := '0';
            begin
            if (CLK'event) and (CLK = '1') then
            
              Do <= Di;
            
              parity := '0';
              for i in 0 to 7 loop parity := parity xor Di(i); end loop;
              PAR <= parity;
            
            end process;
            


            Схемотехнически результат этих двух примеров кода будет один и тот же — логика XOR соответствующих бит между выходами регистра Di и входом регистра PAR. Верхняя запись смотрится лаконичнее и красивее, чем нижняя, но это только для байта. А если на входе будет 64-разрядное слово? Уже целесообразнее нижняя форма записи чисто из-за экономии времени и нежелания делать тупую работу, расписывая все 64 бита)

            В целях обучения я бы на вашем месте не использовал переменные в синтезируемом коде. Кроме того, я бы сделал полностью синхронную схему, работающую по единому тактовому сигналу без асинхронных элементов и межтактовых переходов. К сожалению, сложно объяснить всю опасность «ездящей» времянки из-за асинхронных вещей — это хорошо доходит до людей только после этак недельки интенсивной отладки большого проекта, когда то работает, то нет) Опять же надо понимать, что и асинхронные схемы вполне можно использовать, но разработчики со стажем прибегают к этому обычно по веским причинам и когда деваться некуда.


            1. Andy_Big
              30.07.2019 18:34

              Те, кто приходят из Си мыслят в цикле последовательном исполнении команд и переключиться сразу на параллельное мышление нет так просто как кажется на первый взгляд.

              Вот это да, самое большое затруднение для программистов при вхождении в ПЛИС. Сам с этим помучился пока не переключился :)