I am posting regularly about VHDL verification lately. I loved using UVVM library and verify modules having AXI4-Full, AXI4-Lite and UART interfaces with UVVM BFM packages. Now it is time to verify one of my designs which is a driver of ADXL362 Accelerometer IC having an SPI interface with UVVM’s spi_bfm_pkg VHDL package.
ADXL362 is an accelerometer, sensing 3-Axes acceleration data and give an output through SPI. This IC is well known as it is on Digilent’s famous NEXYS4 DDR board. Last year I recorded a YouTube video showing how to read ADXL362 with VHDL in my YouTube channel (in Turkish Language). You can access the VHDL codes of the design from my github page:
https://github.com/mbaykenar/apis_anatolia/tree/main/VHDL_ile_FPGA_PROGRAMLAMA/ders26
I verified this design by using some processes inside a testbench and also verified in the hardware. But I want to use UVVM spi_bfm_pkg package for verification now, so that in the future I can use easily UVVM for verifying modules having SPI.
For this post, I am not giving details of the design part. There are two VHDL files: ADXL362.vhd and spi_master.vhd. spi_master.vhd handles SPI master protocol, generic and port definitions are:
entity spi_master is
generic (
c_clkfreq : integer := 100_000_000;
c_sclkfreq : integer := 1_000_000;
c_cpol : std_logic := '0';
c_cpha : std_logic := '0'
);
Port (
clk_i : in STD_LOGIC;
en_i : in STD_LOGIC;
mosi_data_i : in STD_LOGIC_VECTOR (7 downto 0);
miso_data_o : out STD_LOGIC_VECTOR (7 downto 0);
data_ready_o : out STD_LOGIC;
cs_o : out STD_LOGIC;
sclk_o : out STD_LOGIC;
mosi_o : out STD_LOGIC;
miso_i : in STD_LOGIC
);
end spi_master;
ADXL362.vhd module instantiates spi_master. The generic and port definitions are:
entity ADXL362 is
generic (
c_clkfreq : integer := 100_000_000;
c_sclkfreq : integer := 1_000_000;
c_readfreq : integer := 100;
c_cpol : std_logic := '0';
c_cpha : std_logic := '0'
);
Port (
clk_i : in STD_LOGIC;
miso_i : in STD_LOGIC;
mosi_o : out STD_LOGIC;
sclk_o : out STD_LOGIC;
cs_o : out STD_LOGIC;
ax_o : out STD_LOGIC_VECTOR (15 downto 0);
ay_o : out STD_LOGIC_VECTOR (15 downto 0);
az_o : out STD_LOGIC_VECTOR (15 downto 0);
ready_o : out STD_LOGIC
);
end ADXL362;
This module simply reads 3-Axes acceleration data in each “readfreq” frequency, so by default the frequency is 100 Hz. “ready_o” signal is set for 1 clock cycle when read operation is done at 100 Hz. To verify this module, the simulator must behave like ADXL362 IC.
The driver first writes to the POWER_CTL register (address 0x2D) 0x02, which enables measurement mode. This write operation only done once at the beginning. In order to write to a register of the slave IC first 0x0A is sent, then the register address and the data to be written:
Then after configuration, in measurement mode, the master continuously read acceleration registers in 100 Hz. First 0x0B is sent to the slave to inform it a read operation is gonna happen, then the register address is given, then the slave sent data as long as master continue clocking the slave:
Now let’s analyze the testbench code for ADXL362 module. As usual, we need to add UVVM utility library and SPI BFM package:
library uvvm_util;
context uvvm_util.uvvm_util_context;
use uvvm_util.spi_bfm_pkg.all;
SPI signals are defined as a record type in spi_bfm_pkg VHDL package as t_spi_if:
type t_spi_if is record
ss_n : std_logic; -- master to slave
sclk : std_logic; -- master to slave
mosi : std_logic; -- master to slave
miso : std_logic; -- slave to master
end record;
There is also t_spi_bfm_config record type for configuring SPI:
-- Configuration record to be assigned in the test harness.
type t_spi_bfm_config is
record
CPOL : std_logic; -- sclk polarity, i.e. the base value of the clock.
-- If CPOL is '0', the clock will be set to '0' when inactive, i.e., ordinary positive polarity.
CPHA : std_logic; -- sclk phase, i.e. when data is sampled and transmitted w.r.t. sclk.
-- If '0', sampling occurs on the first sclk edge and data is transmitted on the sclk active to idle state.
-- If '1', data is sampled on the second sclk edge and transmitted on sclk idle to active state.
spi_bit_time : time; -- Used in master for dictating sclk period
ss_n_to_sclk : time; -- Time from SS active until SCLK active
sclk_to_ss_n : time; -- Last SCLK until SS off
inter_word_delay : time; -- Minimum time between words, from ss_n inactive to ss_n active
match_strictness : t_match_strictness; -- Matching strictness for std_logic values in check procedures.
id_for_bfm : t_msg_id; -- The message ID used as a general message ID in the SPI BFM
id_for_bfm_wait : t_msg_id; -- The message ID used for logging waits in the SPI BFM
id_for_bfm_poll : t_msg_id; -- The message ID used for logging polling in the SPI BFM
end record;
I created a signal of t_spi_if type to connect SPI signals in the port definition and a signal of t_spi_bfm_config to configure SPI:
-----------------------------------------------------------------------------
-- SPI_BFM Signals
signal spi_if : t_spi_if;
signal spi_bfm_config : t_spi_bfm_config := C_SPI_BFM_CONFIG_DEFAULT;
The instantiation of DUT (design under test) module:
-- Instantiate the Unit Under Test (UUT)
DUT : entity work.ADXL362
generic map(
c_clkfreq => c_clkfreq ,
c_sclkfreq => c_sclkfreq ,
c_readfreq => c_readfreq ,
c_cpol => c_cpol ,
c_cpha => c_cpha
)
port map(
clk_i => clk ,
miso_i => spi_if.miso ,
mosi_o => spi_if.mosi ,
sclk_o => spi_if.sclk ,
cs_o => spi_if.ss_n ,
ax_o => ax_o ,
ay_o => ay_o ,
az_o => az_o ,
ready_o => ready_o
);
Inside the main stimuli process I created varaibles:
------------------------------------------------
-- PROCESS: p_main
------------------------------------------------
p_main: process
constant C_SCOPE : string := C_TB_SCOPE_DEFAULT;
variable v_time_stamp : time := 0 ns;
variable v_acc_data : t_slv_array(0 to 5)(8-1 downto 0);
variable v_adxl_config : t_slv_array(0 to 2)(8-1 downto 0);
variable v_acc_x : std_logic_vector(2*8-1 downto 0);
variable v_acc_y : std_logic_vector(2*8-1 downto 0);
variable v_acc_z : std_logic_vector(2*8-1 downto 0);
variable v_rx_data : std_logic_vector(8-1 downto 0);
UVVM library suggests declaring overloaded local procedures for bfm procedures to ease of use. I created 2 overloaded local procedures: “spi_slave_transmit_and_receive” and “spi_slave_check”:
-----------------------------------------------------------------------------
-- SPI Internal Overload Procedures
procedure spi_slave_transmit_and_receive (
constant tx_data : in std_logic_vector;
variable rx_data : out std_logic_vector;
constant when_to_start_transfer : in t_when_to_start_transfer;
constant msg : in string) is
begin
spi_slave_transmit_and_receive(
tx_data => tx_data,
rx_data => rx_data,
msg => msg,
spi_if => spi_if,
when_to_start_transfer => when_to_start_transfer,
scope => C_SCOPE,
msg_id_panel => shared_msg_id_panel,
config => spi_bfm_config,
ext_proc_call => ""
);
end;
-----------------------------------------------------------------------------
procedure spi_slave_check (
constant data_exp : in std_logic_vector;
constant when_to_start_transfer : in t_when_to_start_transfer;
constant msg : in string) is
begin
spi_slave_check(
data_exp => data_exp,
msg => msg,
spi_if => spi_if,
alert_level => error,
when_to_start_transfer => when_to_start_transfer,
scope => C_SCOPE,
msg_id_panel => shared_msg_id_panel,
config => spi_bfm_config
);
end;
Then I started process by initializing SPI:
-----------------------------------------------------------------------------
-- BEGIN STIMULI
-----------------------------------------------------------------------------
begin
-- spi initializations
spi_bfm_config.spi_bit_time <= 1 us;
spi_bfm_config.ss_n_to_sclk <= 500 ns;
spi_bfm_config.sclk_to_ss_n <= 500 ns;
wait for 1 ps;
spi_if <= init_spi_if_signals(
config => spi_bfm_config,
master_mode => false
);
wait for 1 ps;
I chosed master_mode as false, since we simulate ADXL362 IC and it is an SPI slave device. I will use 1 MHz SPI clock frequency and so defined the time values according to it.
I know (as I designed the module) ADXL362 module starts with sending 0x0A2D02 to the slave device for configuring the IC in measurement mode like seen in the code:
when S_CONFIGURE =>
if (timer_rd_tick = '1') then
beginread <= '1';
end if;
if (beginread = '1') then
if (cntr = 0) then
en <= '1';
mosi_data <= x"0A"; -- write command to ADXL362
if (data_ready = '1') then
mosi_data <= x"2D"; -- POWER_CTL register address
cntr <= cntr + 1;
end if;
elsif (cntr = 1) then
if (data_ready = '1') then
mosi_data <= x"02"; -- enable measurmenet mode
cntr <= cntr + 1;
end if;
elsif (cntr = 2) then
if (data_ready = '1') then
cntr <= 0;
en <= '0';
state <= S_MEASURE;
beginread <= '0';
end if;
end if;
end if;
Therefore, I need to verify if the SPI slave device really getting these values from the SPI master. In order to test this condition I utilized spi_slave_check procedure of spi_bfm_pkg package:
-----------------------------------------------------------------------------
-- read config info and check
-- "when_to_start_transfer" parameter must be "START_TRANSFER_ON_NEXT_SS" for first and "START_TRANSFER_IMMEDIATE" for others
v_adxl_config(0) := x"0A";
v_adxl_config(1) := x"2D";
v_adxl_config(2) := x"02";
spi_slave_check(v_adxl_config(0),START_TRANSFER_ON_NEXT_SS,"");
spi_slave_check(v_adxl_config(1),START_TRANSFER_IMMEDIATE,"");
spi_slave_check(v_adxl_config(2),START_TRANSFER_IMMEDIATE,"");
The trick here is defining “when_to_start_transfer” parameter of the procedure. In the first call of the procedure it is “START_TRANSFER_ON_NEXT_SS” and in the other calls it is “START_TRANSFER_IMMEDIATE”. This is due to the transaction of the first byte starts with slave select signal going down and other two bytes just follow while slave select is still at low.
Then in the MEASURE state the ADXL362 module first sent 0x0B0E and then read 6 bytes which are the values of 3-Axes acceleration as seen in the design module:
if (timer_rd_tick = '1') then
beginread <= '1';
end if;
if (beginread = '1') then
if (cntr = 0) then
en <= '1';
mosi_data <= x"0B"; -- read command to ADXL362
if (data_ready = '1') then
mosi_data <= x"0E"; -- XDATA_L register address
cntr <= cntr + 1;
end if;
elsif (cntr = 1) then
if (data_ready = '1') then
mosi_data <= x"00"; -- in continious read mode, only first address is enough
cntr <= cntr + 1;
end if;
elsif (cntr = 2) then
if (data_ready = '1') then
-- mosi_data_i <= x"00"; -- in continious read mode, only first address is enough
cntr <= cntr + 1;
ax_o(7 downto 0) <= miso_data;
end if;
elsif (cntr = 3) then
if (data_ready = '1') then
-- mosi_data_i <= x"00"; -- in continious read mode, only first address is enough
cntr <= cntr + 1;
ax_o(15 downto 8) <= miso_data;
end if;
elsif (cntr = 4) then
if (data_ready = '1') then
-- mosi_data_i <= x"00"; -- in continious read mode, only first address is enough
cntr <= cntr + 1;
ay_o(7 downto 0) <= miso_data;
end if;
elsif (cntr = 5) then
if (data_ready = '1') then
-- mosi_data_i <= x"00"; -- in continious read mode, only first address is enough
cntr <= cntr + 1;
ay_o(15 downto 8) <= miso_data;
end if;
elsif (cntr = 6) then
if (data_ready = '1') then
-- mosi_data_i <= x"00"; -- in continious read mode, only first address is enough
cntr <= cntr + 1;
az_o(7 downto 0) <= miso_data;
end if;
elsif (cntr = 7) then
if (data_ready = '1') then
-- mosi_data_i <= x"00"; -- in continious read mode, only first address is enough
cntr <= 0;
az_o(15 downto 8) <= miso_data;
ready_o <= '1';
en <= '0';
beginread <= '0';
end if;
end if;
end if;
In order test this, I first checked if the SPI master really sents 0x0B0E again using spi_slave_check procedure:
-- now the module is in measure mode
-- we need to sent acceleration data to the master
-- first check if master sents 0x0B 0x0E
v_adxl_config(0) := x"0B";
v_adxl_config(1) := x"0E";
spi_slave_check(v_adxl_config(0),START_TRANSFER_ON_NEXT_SS,"");
spi_slave_check(v_adxl_config(1),START_TRANSFER_IMMEDIATE,"");
The next step is transmitting data from SPI slave device to the SPI master device. I utilized spi_slave_transmit_and_receive procedure of the spi_bfm_pkg package:
-- then sent accelerometer data
-- ACC_X
v_acc_data(0) := x"1A";
v_acc_data(1) := x"03";
-- ACC_Y
v_acc_data(2) := x"2B";
v_acc_data(3) := x"04";
-- ACC_X
v_acc_data(4) := x"3C";
v_acc_data(5) := x"05";
spi_slave_transmit_and_receive(v_acc_data(0),v_rx_data,START_TRANSFER_IMMEDIATE,"");
spi_slave_transmit_and_receive(v_acc_data(1),v_rx_data,START_TRANSFER_IMMEDIATE,"");
spi_slave_transmit_and_receive(v_acc_data(2),v_rx_data,START_TRANSFER_IMMEDIATE,"");
spi_slave_transmit_and_receive(v_acc_data(3),v_rx_data,START_TRANSFER_IMMEDIATE,"");
spi_slave_transmit_and_receive(v_acc_data(4),v_rx_data,START_TRANSFER_IMMEDIATE,"");
spi_slave_transmit_and_receive(v_acc_data(5),v_rx_data,START_TRANSFER_IMMEDIATE,"");
Again I used START_TRANSFER_IMMEDIATE parameter for when_to_start_transfer input of the procedure as the slave device sents data after getting 0x0B0E from the master.
I checked if the transmission is correct and the ADXL362 module gives correct value on its output ports by check_value function, which is defined in methods_pkg package of the UVVM:
-- check if module output signals and simulation acc data are the same
check_value(v_acc_data(1) & v_acc_data(0), ax_o, ERROR, "");
check_value(v_acc_data(3) & v_acc_data(2), ay_o, ERROR, "");
check_value(v_acc_data(5) & v_acc_data(4), az_o, ERROR, "");
Then I gave different acceleration values for the next SPI transaction and check if the outputs are correct:
-----------------------------------------------------------------------------
-- check next samples with different acc values
v_adxl_config(0) := x"0B";
v_adxl_config(1) := x"0E";
spi_slave_check(v_adxl_config(0),START_TRANSFER_ON_NEXT_SS,"");
spi_slave_check(v_adxl_config(1),START_TRANSFER_IMMEDIATE,"");
-- then sent accelerometer data
-- ACC_X
v_acc_data(0) := x"23";
v_acc_data(1) := x"01";
-- ACC_Y
v_acc_data(2) := x"34";
v_acc_data(3) := x"02";
-- ACC_X
v_acc_data(4) := x"45";
v_acc_data(5) := x"03";
spi_slave_transmit_and_receive(v_acc_data(0),v_rx_data,START_TRANSFER_IMMEDIATE,"");
spi_slave_transmit_and_receive(v_acc_data(1),v_rx_data,START_TRANSFER_IMMEDIATE,"");
spi_slave_transmit_and_receive(v_acc_data(2),v_rx_data,START_TRANSFER_IMMEDIATE,"");
spi_slave_transmit_and_receive(v_acc_data(3),v_rx_data,START_TRANSFER_IMMEDIATE,"");
spi_slave_transmit_and_receive(v_acc_data(4),v_rx_data,START_TRANSFER_IMMEDIATE,"");
spi_slave_transmit_and_receive(v_acc_data(5),v_rx_data,START_TRANSFER_IMMEDIATE,"");
-- check if module output signals and simulation acc data are the same
check_value(v_acc_data(1) & v_acc_data(0), ax_o, ERROR, "");
check_value(v_acc_data(3) & v_acc_data(2), ay_o, ERROR, "");
check_value(v_acc_data(5) & v_acc_data(4), az_o, ERROR, "");
Here is the waveform view for the configuration of the ADXL362 IC:
Waveform of the first read of the acc values from the IC:
And waveform of the second read of the acc values:
You can find the full testbench file and design files of ADXL362 accelerometer from my github page:
https://github.com/mbaykenar/apis_anatolia/tree/main/website/spi_uvvm
In this post, I showed how to verify a module having an SPI master interface by using spi_bfm_pkg of UVVM library. I have chosen ADXL362 3-Axes accelerometer IC from the NEXYS4 DDR board. I want to verify another IC which has I2C interface in my post.
Regards,
Mehmet Burak AYKENAR
You can connect me via LinledIn: Just sent me an invitation