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

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

Модель поведения задаётся просто:
Перейдём непосредственно к интерпретатору.

Как видно, нужен доступ к тумблерам SW, кнопкам KEY, светодиодам LED и семисегментникам HEX. Сигнал синхронизации будет вырабатывать внутренний генератор 50Mhz.
RUN — тот самый переключатель режима работы SW9, RESET — кнопка KEY3.
Так как на индикаторах требуется показывать не только вывод программы, но и содержимое каждой конкретной ячейки памяти, использовано два вектора: out_result содержит вывод программы, а final_out_result подключён к дешифраторам семисегментных индикаторов.
По сигналу сброса (кнопки в Cyclone II инверсные, поэтому и условие инверсное) обнуляем значения ячеек памяти и выходной вектор, а если при этом ещё и идёт запись программы, заполняем соответствующую ячейку памяти команд.
В любом случае при выходе из режима выполнения необходимо «забывать» о предыдущих результатах, чтобы каждый следующий запуск происходил «с нуля».
Выбр вывода: на дешифраторы подаётся либо выход программы, либо значение из текущей ячейки. Выбор осуществляется с помощью тумблера SW5.
Программа на Brainfuck представляется как автомат: есть набор фиксированных состояний, перемещение между которыми осуществляется (за исключением циклов) линейно. Такая модель на VHDL (да и не только) реализуется switch-case конструкцией.
Как уже говорилось, u — счётчик открытых скобок. Команды выполняются только при (u = 0), в остальных случаях происходит поиск парной скобки. В нормальном режиме и при поиске закрывающей скобки указатель команд движется вперёд, иначе — назад. Здесь ясно видно, что если бы u была сигналом, при первой реакции на закрывающую скобку счётчик команд увеличился бы, только на следующем такте указатель пошёл бы назад, наткнулся на закрывающую скобку второй раз (u = -2), а такого количества парных открывающих скобок нет — программа никогда бы не выполнилась.
Условие (u /= 0) сделано для реализации вложенных циклов.

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


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

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

- Память команд — 64 команды, ячейки памяти — 32 ячейки по 8 бит каждая;
- Устройство должно поддерживать два режима: занесения программы и выполнения; смена режима должна осуществляться при помощи переключателя SW9;
- В режиме занесения программы переключатели SW8 – SW3 определяют адрес в памяти программ, SW2 – SW0 — код команды; запись в память осуществлятся при нажатии кнопки KEY3; содержимое текущей ячейки памяти отображается на свтодиодах LEDR2 – LEDR0;
- В режиме выполнения программы значения ячеек памяти должны отображаться на семисегментных индикаторах HEX1 – HEX0; адрес отображаемой ячейки должен задаваться при помощи переключателей SW4 – SW0;
- В любом режиме работы по нажатию кнопки 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 (с обозначением всех портов на координатной сетке)