As you know, VHDL is a high-level language for describing hardware (if this is doubtful, you can look
here and
here ). Of all the variety of tasks, I liked the
brainfuck because of the ease of implementation on the one hand and the magic of creating a programmable (albeit rather limited) calculator on the other.
In this article, I will not delve into the jungle of syntax and environment settings, concentrating on the implementation of a specific task.
The test bench will be the
Altera Cyclone II Starter Kit (EP2C20F484C7)

Fans of flashing light bulbs please under cat.
Technical task

- Command memory - 64 commands, memory cells - 32 cells, 8 bits each;
- The device must support two modes: program enrollment and execution; mode change must be carried out using the switch SW9;
- In the program entry mode, the switches SW8 - SW3 determine the address in the program memory, SW2 - SW0 - the command code; memory recording is performed when the KEY3 button is pressed; the contents of the current memory cell is displayed on LEDR2 LEDs - LEDR0;
- In the program execution mode, the values ​​of memory cells should be displayed on seven-segment indicators HEX1 - HEX0; the address of the displayed cell must be set using switches SW4 - SW0;
- In any mode of operation by pressing the KEY3 button, the values ​​of all memory cells should be reset.
Marking the front of work
The project in Quartus II was created, it's time to decide on the entity set. I decided not to stand out and, even if it is not very beautiful, to realize everything in one essence. To output to seven-segment indicators, a special decoder will be needed, which we will separate into a separate entity.
Implementation
The decoder can be implemented immediately, "in the forehead." It performs the simplest conversion, which is given in tabular form, so there are only two ports:
')
entity dc7x is port( i: in std_logic_vector(3 downto 0); z: out std_logic_vector(6 downto 0) ); end dc7x;
The behavior model is set simply:
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;
We proceed directly to the interpreter.
Ports
Involved external input and output devices are shown in the figure:

As you can see, you need access to SW toggle switches, KEY buttons, LEDs and HEX seven-segment devices. The synchronization signal will produce an internal 50Mhz generator.
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 - the same mode switch SW9, RESET - KEY3 button.
Architecture
We will need several internal elements: arrays of memory commands and data, as well as pointers to specific cells in them.
Since the indicators need to show not only the output of the program, but the contents of each specific memory cell, two vectors are used:
out_result contains the output of the program, and
final_out_result is connected to the decoders of the seven-segment indicators.
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)
Finally, they got to the most important thing - the interpreter's behavior patterns. To begin, let's declare a variable-counter of open brackets.
variable u: integer := 0;
For normal work with cycles, it should be just a variable, not a signal. The main difference between the first and the second is that the value is written to the signal at the end of the process, and to the variable - immediately at the moment of assignment.
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
By the reset signal (the buttons in Cyclone II are inverse, and therefore the inverse condition), the values ​​of the memory cells and the output vector are zeroed, and if the program is also being recorded, we fill the corresponding command memory.
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");
In any case, when exiting the execution mode, it is necessary to “forget” about the previous results so that each subsequent launch will occur from scratch.
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;
Output selection: either the output of the program or the value from the current cell is sent to the decoders. The selection is made using the SW5 toggle switch.
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;
The program on Brainfuck is represented as an automaton: there is a set of fixed states, the movement between which is carried out (with the exception of cycles) is linear. Such a model on VHDL (and not only) is implemented by a switch-case construction.
As already mentioned, u is the count of open brackets. Commands are executed only when (u = 0), in other cases the search for matching brackets occurs. In normal mode and when searching for the closing parenthesis, the instruction pointer moves forward, otherwise, backward. It is clearly seen here that if
u were a signal, the first reaction to the closing bracket would increase the command counter, only on the next clock cycle the pointer would go back, came across the closing bracket a second time (u = -2), and such a number of paired opening brackets no - the program would never have run.
The condition (u / = 0) is made to implement nested loops.
Testbench
The code is ready and compiled,

but before flashing the device, it is necessary to test the algorithm for adequacy. I will not give the text of the testbench, it is in the attached files. I will only note that a stupid sequential run of all values ​​does not work here, therefore the correctness of the execution of a specific program is checked. I used the addition of two numbers:
+++>++<[->+<]>.x
ModelSim-Altera was used as a modeling environment.
PCB layout
The last stage before the firmware is the task of matching the signals of the model to the real ports of the board. The coordinates of the findings are in the application "Documentation Cyclone II", but for whom laziness is the finished pinout:


Conclusion
Well, that's all, it remains only to open the Programmer, flash the board, and ... sit and drive in all the commands and addresses manually :) I didn’t give all the code, omitting the standard parts like the
use section. Promised:
- Completely ready for firmware (compiled and diluted on the board) project
- Altera Cyclone II documentation (with all ports on the grid indicated)
PS
The above code can, of course, be used for implementation on a different hardware, for this you need to create a project for a specific board and attach source files to it.