Introduction
There are some challenges for an "ordinary" programmer (like me) to start with VHDL development. This post is not intended to be a VHDL tutorial. In order to learn a language, I will always recommend to read a good book.Instead, I would like to share some pieces of knowlege that I found important (like "milestones") during my VHDL learning experience. I write this for my past self and present information that I would have found uesful when I started.
In this first VHDL post I want to show a simple, yet not trivial example of VHDL code, presenting the structure of a simple project. I will discuss various aspects of the code in later posts.
Hardware description and simulation
The "HDL" in VHDL means "Hardware Description Language". So it is all about hardware development, which (at least for digital hardware) happens nowadays by creating configuration files for FPGAs (Field Programmable Gate Array) that are mounted on a circuit board. FPGAs are configurable logic ICs, used to create functionality for a special purpose for which no specialized ICs are available. The process of creating a configuration file from VHDL code is called "synthesis".
However, no serious FPGA development is possible without simulating the behavior of the described circuit. The first important point is to recognize that VHDL is a language for hardware description, as well as for the simulation of the described hardware. Only a subset of the VHDL language is useful for hardware synthesis. Many language features are only there to help writing simulations. Typically, a small VHDL project will consist of a synthesizable VHDL "module", and a VHDL "test bench" for this module. The test bench will simulate the environment where the module will be used in once it is sythesized.
Rotary encoder in VHDL
As a simple example, I'll show my implementation of a rotary encoder driver in VHDL. A rotary encoder is a digital input device (a knob) that can be turned left or right in steps. On each step, the two outputs will emit a characteristic wave form (encoder[0] and encoder[1] in the picture below). Both wave forms are similar, but one is ahead in time of the other one. Which one is which depends on the direction of the rotation (left/right). Each rotational step can be detected by driving a state machine with these two signals as input. The states with all transitions are shown in the following picture (high signal level is 1, low signal level is 0).
The special states (R00_emit and L00_emit) are only there to emit a pulse that indicates one step left or right of the encoder. This can be used for example in a counter track the position of the encoder knob. These states are left in the next clock cycle of the state machine (the state machine has a clock; it is a "synchronous" state machine).
GHDL
GHDL is a free VHDL simulator that allows to create hardware designs and simulate the behavior. It does not support hardware synthesis, because FPGA manufacturers usually don't release information to allow the open source community to develop free synthesizer tools. Almost any VHDL project will start as a pure simulation, so this restriction is no problem for now. I will not describe how to install GHDL because it depends on the system you are working on.
The project code consists of three files:
The project code consists of three files:
- moduel.vhd synthesizable module code
- module_tb.vhd test bench code
- makefile
The makefile is useful to describe the build process of the project: first, the VHDL code is compiled into an executable file. This can be executed and runs a the simulation. The output of the simulation is a file that can be opened with a wave form wiever such as gtkwave.
I have a target "make view" that creates the simulation result and starts gtkwave. The default target creates the simulation result (if the sources were updated, of course) and tells a running instance of gtkwave to reload the wave form file.
Here is a screen shot of the project in gtkwave.
I have a target "make view" that creates the simulation result and starts gtkwave. The default target creates the simulation result (if the sources were updated, of course) and tells a running instance of gtkwave to reload the wave form file.
%.o: %.vhd ghdl -a --ieee=synopsys $< %.o: %.vhdl ghdl -a --ieee=synopsys $< all: module_tb.ghw view: module_tb.ghw gtkwave module_tb.ghw & module_tb.ghw: module_tb makefile ./module_tb --stop-time=20000ns --wave=module_tb.ghw gconftool-2 --type string --set /com.geda.gtkwave/0/reload 0 module_tb.o: module.o module_tb: module.o module_tb.o ghdl -e --ieee=synopsys module_tb
Here is a screen shot of the project in gtkwave.
The module code
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity module is
port (
clk_i : in std_logic;
rst_i : in std_logic;
-- encoder input
encoder_i : in std_logic_vector(1 downto 0);
-- encoder output
left_o : out std_logic;
right_o : out std_logic
);
end entity;
architecture rtl of module is
type state_t is (s11,s10,r00,r00_emit,l00,l00_emit,s01,s_unknown);
signal state, next_state : state_t;
signal sync_encoder : std_logic_vector(1 downto 0);
begin
-- sync. process for state transition, input synchronization
p_state_switch: process (clk_i)
begin
if rising_edge(clk_i) then
-- synchronous reset
if rst_i = '1' then
state <= s_unknown;
end if;
-- update state
state <= next_state;
end if;
end process;
p_sync: process (clk_i)
begin
if rising_edge(clk_i) then
-- synchronize input
sync_encoder <= encoder_i;
end if;
end process;
-- conditonal assignment for output generation
right_o <= '1' when state = r00_emit else '0';
left_o <= '1' when state = l00_emit else '0';
-- async. process for update of state
p_state_analysis: process (state, sync_encoder)
begin
case state is
when s_unknown =>
if sync_encoder = "11" then next_state <= s11;
elsif sync_encoder = "10" then next_state <= s10;
elsif sync_encoder = "00" then next_state <= r00;
elsif sync_encoder = "01" then next_state <= s01;
else next_state <= s_unknown;
end if;
when s11 =>
if sync_encoder = "10" then next_state <= s10;
elsif sync_encoder = "01" then next_state <= s01;
elsif sync_encoder = "11" then next_state <= s11;
else next_state <= s_unknown;
end if;
when s10 =>
if sync_encoder = "00" then next_state <= r00;
elsif sync_encoder = "11" then next_state <= s11;
elsif sync_encoder = "10" then next_state <= s10;
else next_state <= s_unknown;
end if;
when r00 =>
if sync_encoder = "01" then next_state <= r00_emit;
elsif sync_encoder = "10" then next_state <= s10;
elsif sync_encoder = "00" then next_state <= r00;
else next_state <= s_unknown;
end if;
when r00_emit => next_state <= s01;
when l00 =>
if sync_encoder = "10" then next_state <= l00_emit;
elsif sync_encoder = "01" then next_state <= s01;
elsif sync_encoder = "00" then next_state <= l00;
else next_state <= s_unknown;
end if;
when l00_emit => next_state <= s10;
when s01 =>
if sync_encoder = "11" then next_state <= s11;
elsif sync_encoder = "00" then next_state <= l00;
elsif sync_encoder = "01" then next_state <= s01;
else next_state <= s_unknown;
end if;
when others => next_state <= s_unknown;
end case;
end process;
end architecture;
There is one process "p_state_switch" that updates the current state of the state machine for each clock cycle. It also does a synchronous reset.
The most important practical thing I learned here, was the synchronization of the input. The determinism of almost all FPGA configurations depends on all signals being synchronized to the clock of the flip flops in the device. That is why the first thing to do with the input signals "encoder_i[1:0]" is to sample it inside the process "p_sync" and create a synchronized version of it "sync_encoder[1:0]" it was very striking when I synthesized the project into an FPGA without that process (directly using "encoder_i" for the state machine) and see how the system misses steps occasionally. Synchronization is not only needed for external inputs, but also for internal signals that run with a different clock speed.
The reset of the code is for output generation and the state machine transition logic.
The test bench code
The test bench creates all input signals for the module: a clock signal and the two signals from the rotary encoder. The edges of these input signals are not synchronized to the clock, just like in the real world. It also has a counter process that tracks the number of right and left steps the knob was turned.
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; entity module_tb is end entity; architecture simulation of module_tb is signal clk, rst : std_logic; constant clk_period : time := 20 ns; signal a, b : std_logic; -- simulating the input form quadrature rotary encoder signal left, right : std_logic; signal counter : unsigned(7 downto 0) := (others => '0'); begin p_clock: process begin clk <= '0'; wait for clk_period / 2; clk <= '1'; wait for clk_period / 2; end process; p_reset: process begin rst <= '1'; wait for clk_period * 20; rst <= '0'; wait; end process; p_rot_encoder: process begin A <= '1'; B <= '1'; -- counting up for i in 1 to 4 loop wait for clk_period * 50; A <= '0'; wait for clk_period * 14.4; B <= '0'; wait for clk_period * 11.4; A <= '1'; wait for clk_period * 7.4; B <= '1'; wait for clk_period * 50; end loop; -- counting down for i in 1 to 4 loop wait for clk_period * 50; B <= '0'; wait for clk_period * 6.4; A <= '0'; wait for clk_period * 8.4; B <= '1'; wait for clk_period * 13.4; A <= '1'; wait for clk_period * 50; end loop; end process; -- instantiate module encoder : entity work.module port map ( clk_i => clk, rst_i => rst, encoder_i(0) => a, encoder_i(1) => b, left_o => left, right_o => right ); p_count: process (clk) begin if rising_edge(clk) then if right = '1' then counter <= counter + 1; end if; if left = '1' then counter <= counter - 1; end if; end if; end process; end architecture;