When developing FPGAs there is a critical triad which needs to be achieved for the successful delivery of a project. First we need to develop the Register Transfer Level code which implements the desired functionality, following that we need to verify the design to ensure the functionality implemented aligns with the requirements. The final element of course is the implementation of the design in the target hardware and achieving timing closure.
We have looked in depth at the timing closure techniques recently, including two webinars.
However we have not talked a lot about how to go about doing verification using an approach which is provides for a scalable test solution. While there are several frameworks which can be used such as UVVM, UVM, OSVVM etc in this blog we are going to look at how we might structure a test bench if w were to write It all from scratch. Of course our structure and approach will be the same as for these frameworks however, doing it from scratch allows us to understand a little more about the process involved.
When it comes to verification the main objective is to demonstrate the requirements have been achieved this could be at the device level or module level. Of course there should exist FPGA requirements which define the overall device requirements along with derived sub system requirement specifications for each of the modules.
No matter if we are verifying the FPGA or a module, each of these requirements must have a associated test case which exercises the unit under test to demonstrate the requirement is achieved. This test case should be descriptive and outline the approach which is to be taken, the stimulus to be applied and of course the expected results. It goes without saying the test bench should be self checking, that is it checks the expected results occur when expected and of course, it reports unexpected results.
This is where organisation of the test bench becomes critical, we may have several test cases what we need to apply to demonstrate compliance against the requirements. However, we also have to consider readability, and maintainability. We may also be faced with other challenges which vary depending upon the end application such as the need to demonstrate code coverage or functional coverage.
Within Vivado we can use the Vivado simulator to verify our designs, the architecture of my test benches is as follows.
Test Cases – These are the test cases which apply stimulus and record and log results from the test case.
BFM Packages – This defines a number of procedures and functions to interact with the IO connected to the UUT.
Test Bench – This is the structural element of the verification element in maps in the unit under test, connects bus functional models to the IO of the UUT and provides clocking and resets.
Unit Under Test – This is the module which we are trying to verify.
Using an approach like this allows us to modularise the test bench, separating structural elements of the test bench along with providing the ability for test cases to be architected such that they are a series of function and procedure calls to interact with the BFMs connected to the UUT. The use of several Test Cases ensure flexibility as test cases are developed as the project progresses.
Lets take a look at a simple example which uses a UART, as the UUT. To provide the stimulus to the UUT I define a BFM package which will facilities transmission of data using the UART.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use ieee.math_real.all;
entity uart is generic (
reset_level : std_logic := '0'; -- reset level which causes a reset
clk_freq : natural := 100000000; -- oscillator frequency
baud_rate : natural := 115200 -- baud rate
);
port (
--System Inputs
clk : in std_logic;
reset : in std_logic;
--External Interfaces
rx : in std_logic;
tx : out std_logic;
--Internal Interfaces
data_tx : in std_logic_vector(7 downto 0);
data_tx_val : in std_logic;
data_rx : out std_logic_vector(7 downto 0);
data_rx_val : out std_logic;
error : out std_logic;
busy : out std_logic);
end entity;
architecture rtl of uart is
function vector_size(clk_freq, baud_rate : real) return integer is
variable div : real;
variable res : real;
begin
div := (clk_freq/baud_rate);
res := CEIL(LOG(div)/LOG(2.0));
return integer(res - 1.0);
end;
function parity (a : std_logic_vector) return std_logic is
variable y : std_logic := '0';
begin
for i in a'range loop
y := y xor a(i);
end loop;
return y;
end parity;
constant fe_det : std_logic_vector(1 downto 0) := "10";
constant bit_period : integer := (clk_freq/baud_rate) - 1;
constant start_bit : std_logic := '0';
constant stop_bit : std_logic_vector := "11";
type cntrl_fsm is (idle, wait_start, wait_tx);
type rx_fsm is (idle, start, sample, check);
signal current_state : cntrl_fsm; --:= idle;
signal rx_state : rx_fsm;-- := idle;
signal baud_counter : unsigned(vector_size(real(clk_freq), real(baud_rate)) downto 0) := (others => '0'); --timer for outgoing signals
signal baud_en : std_logic := '0';
signal meta_reg : std_logic_vector(3 downto 0) := (others => '0'); -- fe detection too
signal capture : std_logic_vector(8 downto 0) := (others => '0'); -- data and parity
signal bit_count : integer range 0 to 63 := 0;
signal pos_count : integer range 0 to 15 := 0;
signal load_tx : std_logic := '0';
signal complete : std_logic := '0';
signal tx_reg : std_logic_vector(11 downto 0) := (others => '0');
signal tmr_reg : std_logic_vector(11 downto 0) := (others => '0');
signal payload : std_logic_vector(7 downto 0) := (others => '0');
begin
process (reset, clk)
begin
if reset = reset_level then
current_state <= idle;
payload <= (others => '0');
load_tx <= '0';
elsif rising_edge(clk) then
load_tx <= '0';
case current_state is
when idle =>
if data_tx_val = '1' then
current_state <= wait_start;
load_tx <= '1';
payload <= data_tx;
end if;
when wait_start =>
current_state <= wait_tx;
when wait_tx =>
if complete = '1' then
current_state <= idle;
end if;
when others =>
current_state <= idle;
end case;
end if;
end process;
busy <= '0' when (current_state = idle) else '1';
process (reset, clk)
begin
if reset = reset_level then
baud_counter <= (others => '0');
baud_en <= '0';
elsif rising_edge(clk) then
baud_en <= '0';
if (load_tx = '1') then
baud_counter <= (others => '0');
elsif (baud_counter = bit_period) then
baud_en <= '1';
baud_counter <= (others => '0');
else
baud_counter <= baud_counter + 1;
end if;
end if;
end process;
process (reset, clk)
--metastability protection rx signal
begin
if reset = reset_level then
meta_reg <= (others => '1');
elsif rising_edge(clk) then
meta_reg <= meta_reg(meta_reg'high - 1 downto meta_reg'low) & rx;
end if;
end process;
process (reset, clk)
begin
if reset = reset_level then
pos_count <= 0;
bit_count <= 0;
capture <= (others => '0');
rx_state <= idle;
data_rx_val <= '0';
data_rx <= (others => '0');
error <= '0';
elsif rising_edge(clk) then
data_rx_val <= '0';
error <= '0';
case rx_state is
when idle =>
if meta_reg(meta_reg'high downto meta_reg'high - 1) = fe_det then -- FE
pos_count <= 0;
bit_count <= 0;
capture <= (others => '0');
rx_state <= start;
end if;
when start =>
if bit_count = bit_period then
bit_count <= 0;
rx_state <= sample;
else
bit_count <= bit_count + 1;
end if;
when sample =>
bit_count <= bit_count + 1;
rx_state <= sample;
if bit_count = (bit_period/2) and (pos_count < 9) then
capture <= meta_reg(meta_reg'high) & capture(capture'high downto capture'low + 1);
elsif bit_count = bit_period then
if pos_count = 9 then
rx_state <= check;
else
pos_count <= pos_count + 1;
bit_count <= 0;
end if;
end if;
when check =>
if parity(capture) = '1' then
data_rx_val <= '1';
data_rx <= capture(7 downto 0);
error <= '0';
rx_state <= idle;
else
data_rx_val <= '1';
data_rx <= capture(7 downto 0);
error <= '1';
rx_state <= idle;
end if;
end case;
end if;
end process;
op_uart : process (reset, clk)
begin
if reset = reset_level then
tx_reg <= (others => '1');
tmr_reg <= (others => '0');
elsif rising_edge(clk) then
if load_tx = '1' then
tx_reg <= stop_bit & not(parity(payload)) & payload & start_bit ;
tmr_reg <= (others => '1');
elsif baud_en = '1' then
tx_reg <= '1' & tx_reg(tx_reg'high downto tx_reg'low + 1);
tmr_reg <= tmr_reg(tmr_reg'high - 1 downto tmr_reg'low) & '0';
end if;
end if;
end process;
tx <= tx_reg(tx_reg'low);
complete <= '1' when (tmr_reg = "000000000000" and current_state = wait_tx) else '0';
end architecture;
The test harness, provides the mapping of the UUT into its test structure
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
package uart_tb_bfm is new work.uart_bfm_pkg
generic map(
G_PERIOD => 10 ns,
G_BIT_PERIOD => 1 us --hint
);
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity uart_test is
end uart_test;
architecture tb of uart_test is
signal s_clk : std_logic ;
signal s_reset : std_logic ;
signal s_tx : work.uart_tb_bfm.r_tx ;
signal s_tx_cntrl : work.uart_tb_bfm.r_tx_cntrl;
signal s_rx : work.uart_tb_bfm.r_rx ;
signal s_rx_cntrl : work.uart_tb_bfm.r_rx_cntrl;
signal s_busy : std_logic;
begin
s_rx.rx <= s_tx.tx;
uut: entity work.uart generic map (
reset_level => '0', -- reset level which causes a reset
clk_freq => 100000000, -- oscillator frequency
baud_rate => 115200 -- baud rate
)
port map (
--System Inputs
clk => s_clk,
reset => s_reset,
--External Interfaces
rx => s_rx.rx,
tx => s_tx.tx,
--Internal Interfaces
data_tx => s_tx_cntrl.tx_data,
data_tx_val => s_tx_cntrl.tx_val,
data_rx => s_rx_cntrl.rx_data,
data_rx_val => s_rx_cntrl.rx_val,
error => s_rx_cntrl.error,
busy => s_busy);
end architecture;
Finally we have the test case, which commands the BFM to undertake specific actions to stimulate the UART.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity test_case_01 is
end entity;
architecture tc01 of test_case_01 is
constant clk_period : time := 10 ns;
alias s_uart_tx is << signal .test_case_01.tb.s_tx : work.uart_tb_bfm.r_tx >>;
alias s_uart_cntrl is << signal .test_case_01.tb.s_tx_cntrl : work.uart_tb_bfm.r_tx_cntrl >>;
alias s_clk is << signal .test_case_01.tb.s_clk : std_logic >>;
alias s_reset is << signal .test_case_01.tb.s_reset : std_logic >>;
alias s_busy is << signal .test_case_01.tb.s_busy : std_logic >>;
begin
tb : entity work.uart_test;
clk_gen:process
begin
loop
s_clk <= '0';
wait for clk_period/2;
s_clk <= '1';
wait for clk_period/2;
end loop;
end process;
s_reset <= '0', '1' after 1 us;
stim:process
begin
wait for 2 us;
work.uart_tb_bfm.tx_data(x"55",s_uart_cntrl);
wait until (s_busy = '0');
work.uart_tb_bfm.tx_data(x"AA",s_uart_cntrl);
wait until (s_busy = '0');
report "simulation complete" severity failure;
end process;
end architecture;
While this is a simple example it outlines the approach which can be undertaken to verify a module from simple to more complex. I would encourage you to think carefully about verification and as your projects grow in size and complexity examine the frameworks used above which can reduce the development time needed for verification.
To interact with signals within the test harness I recommend the use of signal alias and hierarchical access.
I have uploaded to project to my github if you want to take a look at it, there are also several extensions which can be made to this test bench if you so desire.
Workshops and Webinars
If you enjoyed the blog why not take a look at the free webinars, workshops and training courses we have created over the years. Highlights include
Upcoming Webinars Timing, RTL Creation, FPGA Math and Mixed Signal
Professional PYNQ Learn how to use PYNQ in your developments
Introduction to Vivado learn how to use AMD Vivado
Ultra96, MiniZed & ZU1 three day course looking at HW, SW and PetaLinux
Arty Z7-20 Class looking at HW, SW and PetaLinux
Mastering MicroBlaze learn how to create MicroBlaze solutions
HLS Hero Workshop learn how to create High Level Synthesis based solutions
Perfecting Petalinux learn how to create and work with PetaLinux OS
Boards
Get an Adiuvo development board
Adiuvo Spartan 7 / RPi 2040 Embedded System Development Board
Adiuvo Spartan 7 Tile - Low Risk way to add a FPGA to your design.
Embedded System Book
Do you want to know more about designing embedded systems from scratch? Check out our book on creating embedded systems. This book will walk you through all the stages of requirements, architecture, component selection, schematics, layout, and FPGA / software design. We designed and manufactured the board at the heart of the book! The schematics and layout are available in Altium here Learn more about the board (see previous blogs on Bring up, DDR validation, USB, Sensors) and view the schematics here.