Freitag, 6. Januar 2017

VDHL #1: ghdl setup and rotary encoder simulation

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:
  • 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.


%.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;



Keine Kommentare:

Kommentar veröffentlichen