VHDL для начинающих. Brainfuck

Как известно, VHDL – высокоуровневый язык описания аппаратуры (если это вызывает сомнения, можно посмотреть здесь и здесь). Из всего разнообразия задач мне приглянулся именно brainfuck благодаря лёгкости в реализации с одной стороны и волшебству создания программируемого (пусть и весьма ограниченно) вычислителя с другой.
В рамках данной статьи я не буду углубляться в дебри синтаксиса и настройки среды, сконцентрировавшись на реализации конкретной задачи.
Испытательным стендом будет Altera Cyclone II Starter Kit (EP2C20F484C7)


Любителей мигающих лампочек прошу под кат.

Техническое задание


  1. Память команд — 64 команды, ячейки памяти — 32 ячейки по 8 бит каждая;
  2. Устройство должно поддерживать два режима: занесения программы и выполнения; смена режима должна осуществляться при помощи переключателя SW9;
  3. В режиме занесения программы переключатели SW8 – SW3 определяют адрес в памяти программ, SW2 – SW0 — код команды; запись в память осуществлятся при нажатии кнопки KEY3; содержимое текущей ячейки памяти отображается на свтодиодах LEDR2 – LEDR0;
  4. В режиме выполнения программы значения ячеек памяти должны отображаться на семисегментных индикаторах HEX1 – HEX0; адрес отображаемой ячейки должен задаваться при помощи переключателей SW4 – SW0;
  5. В любом режиме работы по нажатию кнопки KEY3 значения всех ячеек памяти должны обнуляться.

Разметка фронта работ

Проект в Quartus II создан, самое время определиться с набором entity. Я решил не выделяться и, пусть это и не очень красиво, реализовать всё в одной сущности. Для вывода на семисегментные индикаторы понадобится специальный дешифратор, который выделим в отдельный entity.

Реализация

Дешифратор можно реализовать сразу, «в лоб». Он выполняет простейшее преобразование, задаваемое таблично, поэтому портов всего два:

entity dc7x is
	port(
		i: in std_logic_vector(3 downto 0);
		z: out std_logic_vector(6 downto 0)
		);
end dc7x;


Модель поведения задаётся просто:
with i select
		z <= "1000000" when "0000", --0
			  "1111001" when "0001", --1
			  "0100100" when "0010", --2
			  "0110000" when "0011", --3
			  "0011001" when "0100", 
			   ********
			  "0001110" when "1111", --F
			  "0111111" when others;


Перейдём непосредственно к интерпретатору.

Порты
Задействованные внешние устройства ввода и вывода показаны на рисунке:

Как видно, нужен доступ к тумблерам SW, кнопкам KEY, светодиодам LED и семисегментникам HEX. Сигнал синхронизации будет вырабатывать внутренний генератор 50Mhz.
entity brainfuck is
	port(
		RUN: in std_logic;
		SW: 	in std_logic_vector(8 downto 0);
		LED:	out std_logic_vector(2 downto 0);
		HEX1:	out std_logic_vector(6 downto 0);
		HEX2:	out std_logic_vector(6 downto 0);
		clk: in std_logic;
		RESET: in boolean
		);
end brainfuck;

RUN — тот самый переключатель режима работы SW9, RESET — кнопка KEY3.

Архитектура
Нам понадобится несколько внутренних элементов: массивы памяти команд и данных, а также указатели на конкретные ячейки в них.
Так как на индикаторах требуется показывать не только вывод программы, но и содержимое каждой конкретной ячейки памяти, использовано два вектора: out_result содержит вывод программы, а final_out_result подключён к дешифраторам семисегментных индикаторов.
type 		t_memory is array (31 downto 0) of std_logic_vector (7 downto 0); -- command memory
signal 	cell_memory: t_memory := (others => x"00");

type		d_memory is array (63 downto 0) of std_logic_vector (2 downto 0); -- cells memory
signal 	comm_memory: d_memory := (others => "000");

signal comm_number: std_logic_vector(6 downto 0) := (others => '0');
signal cell_number: std_logic_vector(5 downto 0) := (others => '0');

signal out_result: std_logic_vector(7 downto 0) := (others => '0');
signal final_out_result: std_logic_vector(7 downto 0) := (others => '0');


Process (clk, RESET)
Наконец подобрались к самому главному — модели поведения интерпретатора. Для начала объявим переменную-счётчик открытых скобок.
variable 	u: integer := 0; 
Для нормальной работы с циклами это должна быть именно переменная, а не сигнал. Главное отличие первого от второго в том, что значение в сигнал записывается по окончании выполнения процесса, а в переменную — непосредственно в момент присваивания.
begin
		if rising_edge(clk) then
		if (not RESET) then
			cell_memory <= (others => x"00");
			out_result <= (others => '0');
			final_out_result <= cell_memory(conv_integer(unsigned(cell_number)));
			if (RUN = '0') then -- writing a programm
			     comm_memory(conv_integer(unsigned(SW(8 downto 3)))) <= SW(2 downto 0);
			end if;

По сигналу сброса (кнопки в Cyclone II инверсные, поэтому и условие инверсное) обнуляем значения ячеек памяти и выходной вектор, а если при этом ещё и идёт запись программы, заполняем соответствующую ячейку памяти команд.
else
			if (RUN = '0') then
				running_led <= false;
				LED <= comm_memory(conv_integer(unsigned(SW(8 downto 3))));	
				comm_number <= (others => '0');
				cell_number <= (others => '0');
				cell_memory <= (others => x"00");

В любом случае при выходе из режима выполнения необходимо «забывать» о предыдущих результатах, чтобы каждый следующий запуск происходил «с нуля».
else -- executing
			running_led <= true;
			LED <= (others => '0');
				if (SW(5) = '1') then final_out_result <= cell_memory(conv_integer(unsigned(SW(4 downto 0)))); -- out: user's or programm's cell
					else final_out_result <= out_result;
				end if;
				

Выбр вывода: на дешифраторы подаётся либо выход программы, либо значение из текущей ячейки. Выбор осуществляется с помощью тумблера SW5.
			case comm_memory(conv_integer(unsigned(comm_number))) is 
				when "000" => 			-- next
					if (u = 0) then cell_number <= cell_number + 1;
						end if;
					if (u < 0 )then comm_number <= comm_number - 1;
						else comm_number <= comm_number + 1;
					end if;
				
				****************
				
				when "100" => 			-- [
					if ((cell_memory(conv_integer(unsigned(cell_number))) = x"00") or (u /= 0)) then
						u := u + 1;
					end if;
					if (u < 0 )then comm_number <= comm_number - 1;
						else comm_number <= comm_number + 1;
					end if;
				
				when "101" =>			 -- ]
					if ((cell_memory(conv_integer(unsigned(cell_number))) /= x"00") or (u /= 0)) then
						u := u - 1;
					end if;
					if (u < 0 )then comm_number <= comm_number - 1;
						else comm_number <= comm_number + 1;
					end if;
			
			when others =>			-- stop
				if (u = 0) then
					null;
				end if;

Программа на Brainfuck представляется как автомат: есть набор фиксированных состояний, перемещение между которыми осуществляется (за исключением циклов) линейно. Такая модель на VHDL (да и не только) реализуется switch-case конструкцией.
Как уже говорилось, u — счётчик открытых скобок. Команды выполняются только при (u = 0), в остальных случаях происходит поиск парной скобки. В нормальном режиме и при поиске закрывающей скобки указатель команд движется вперёд, иначе — назад. Здесь ясно видно, что если бы u была сигналом, при первой реакции на закрывающую скобку счётчик команд увеличился бы, только на следующем такте указатель пошёл бы назад, наткнулся на закрывающую скобку второй раз (u = -2), а такого количества парных открывающих скобок нет — программа никогда бы не выполнилась.
Условие (u /= 0) сделано для реализации вложенных циклов.

Testbench

Код готов и компилируется,

но перед прошивкой устройства надо протестировать алгоритм на адекватность. Текст тестбенча приводить не буду, он есть в прикреплённых файлах. Отмечу лишь, что тупой последовательный прогон всех значений здесь не подойдёт, поэтому проверяется корректность выполнения конкретной программы. Я использовал сложение двух чисел:
+++>++<[->+<]>.x
В качестве среды моделирования использовалась ModelSim-Altera.

Разводка платы

Последний этап перед прошивкой — задание соответствий сигналов модели реальным портам платы. Координаты выводов есть в приложении «Документация Cyclone II», ну а кому лень — вот готовая распиновка:



Заключение

Ну вот и всё, осталось только открыть Programmer, прошить плату, и… сидеть вбивать все команды и адреса вручную :) Я привёл не весь код, опустив стандартные части вроде секции use. Обещанное:
  • Полностью готовый к прошивке (скомпилированный и разведённый по плате) проект
  • Документация к Altera Cyclone II (с обозначением всех портов на координатной сетке)


P.S.

Приведённый код можно, конечно, использовать и для реализации на другом железе, для этого надо создать проект под конкретную плату и прикрепить к нему файлы исходников.
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 29

    +2
    отличная статья! реквестирую больше статьей по HDL на хабре.

    только стоит наверное указать, что все такие это за плата.
      0
      Спасибо, добавил ссылку
        0
        Собираюсь организовать цикл статей по AHDL, «с нуля» так сказать, в черновиках уже лежит одна, думал опубликовать, но кармы нет
          +1
          Было бы просто здорово.
            0
            присоединяюсь.
            –1
            Не стоит эксгумировать труп.
              0
              не стоит на нем пытаться делать что-нибудь стоящее, а вот начать втыкать в *hdl, вообщем-то стоит, при наличии материала.
                0
                Зря вы так. На нем все еще пишут. Кроме того, он как Basic в HDL-языках — самое то для начинающих.
                  0
                  Расскажите в теме программистов про «бэйсик для начинающих». Только тут еще хуже — бэйсик поддерживается микрософтом до сих пор, а AHDL не рекомендуется альтерой для новых дизайнов (уже лет 10 как). А больше и вовсе никем не поддерживается.
            +1
            хех, помню писал как-то свой простенький процессор, на ПЛИСе.
            жаль до железа не довёл, а закончил на эмуляторе.
              +2
              Забавная штука, но для получения зачета я бы отправил вас доделать защиту от дребезга и метастабильности по входам.
                +2
                От какого дребезга? Контактов? Там же триггеры стоят
                  +5
                  Вот из мануала:
                  A Schmitt Trigger circuit on each switch debounces the signal
                    0
                    Ок, тогда только от метастабильности ;)
                      +1
                      Так тригер же :)
                        0
                        Работа триггера Шмидта асинхронна к вашему внутреннему тактовому сигналу и потому совершенно не спасает от метастабильности. Момент переключения входного сигнала может попасть непосредственно на фронт тактового сигнала, что приведет к печальным последствиям — значения со входов используются непосредственно в управляющей логике из-за чего проблемам будет подвержен весь проект.
                        Для учебного проекта, который должен отработать один раз, вероятность наступления такого события невилика (но все равно можно облажаться на зачете), для промышленного — недопустимо.
                        Для начала можно почитать например
                            0
                            Откуда асинхронность, если значение с триггера принимается в расчёт только по приходу фронта сигнала синхронизации? Насколько я понимаю, фронт — моментное состояние, и одновременно с ним существует одно конкретное значение сигнала RESET. А так как кнопку нажимает человек, при частоте 50MHz (её при желании можно поднять до 60) хотя бы на один фронт CLK с большой вероятностью попадёт «чистое» значение с триггера, а не его фронт/спад.
                              0
                              Почитайте по ссылке выше, чтобы ознакомится с понятием «метастабильность триггера». Достаточно одного неудачного попадания, чтобы вывести ваш проект из строя на несколько минут-секунд, а то и вплоть до выключения питания.
                                +1
                                Ну, мгновенных событий не бывает, и изменение сигнала на выходе триггера может совпасть с изменением сигнала clock, что и повлечёт за собой все радости мультивибрирования.
                                  0
                                  Действительно, что-то я поторопился с выводами.
                                  Вариант решения: благодаря долгому (относительно большой частоты) нажатию кнопки сравнивать предыдущее и текущее значения триггера, и при их совпадении считать значение установившимся со всеми вытекающими. Контролировать срабатывание легко по изменению индикаторов LED.
                                0
                                Да я в курсе. Но, в этой же как раз (отличной, надо сказать) статье сказано, что фига с два Вы поборете метастабильность совсем, можно лишь уменьшать её вероятность. В данной задаче… Ну… Всё сильно зависит, конечно, от свойств данного конкретного триггера…

                                Но вообще да, убедили… Всяко может быть и надо предохраняться.
                      0
                      Черт возьми. Как я мог пропустить такое :) Побежал изучать VHDL
                        +2
                        Несколько вопросов-замечаний.
                        1.Список чувствительности для дешифратора покажите пожалуйста.
                        2. Список чувствительноести в процессе «модели поведения интерпретатора» Process (clk,RESET) неверный. RESET там абсолютно лишний потому что выполняется синхронно под изменением сигнала clk.
                          +2
                          Ага. код для дешифратора из приложенных файлов строго говоря тоже не совсем хорош. Выбранный вариант реализации без процесса породит адову конструкцию логических функций. Лучше бы с процессом — Quartus упаковал бы это более компактно, учитывая доступные аппаратные особенности Циклона.

                          Ну и чисто побрюзжать. Плату вы не разводили. Все что вы сделали это назначили пины проекта на ножки микросхемы. В design flow Альтеры этот этап так и называется — «назначение пинов» (Pin assignement).
                            0
                            насчет последнего — было бы интересно увидеть пояснения, как правильно переносить проект с девборды на свою борду, или хотя бы вот на такую. я думаю там есть достаточно аспектов для освещения.
                              0
                              И сильная экономия получится? Привык не заниматься ненужной оптимизацией, а в моём случае логических элементов и так использовано немного.

                              Второе — да, для красного словца написал :)
                                0
                                Совершенно неважно, описывать конструкцию с явно заданным процессом или без — логика будет одна и та же. Если писать синхронный процесс, то у квартуса появится шанс упаковать логику в память, но для столь малого объема функции это неэффективно.
                                0
                                Спасибо, действительно, рудимент остался

                              Only users with full accounts can post comments. Log in, please.