When we move data from an FPGA to off-chip components such as DDR memory or vice versa, we would want to verify whether the received data matches the sent data. Traffic generators come in handy for such situations. The output of a traffic generator can either be a random data stream generated based on a particular algorithm or it can be a deterministic pattern specified by the user. I am going to show the steps of creating the latter one in Verilog. I have also included a testbench that you can use to verify the DUT using behavioral simulation. All the source files can be found in the Github repository I created for this project.
Traffic Generator IPThe traffic generator IP we are going to create consists of a top-level module that instantiates a register module and a stream master module. An AXI-Lite slave interface connected to the register module is used to configure the core. We have two parameters to control the functionality of the traffic generator; (i) enable, and (ii) the number of words. The enable signal is used to turn on/off the master module. The output of the master module is a traffic pattern determined by the number of words. For example, when the number of words is set to 16 and the enable signal is asserted, the stream output goes from 1 to 16, then rolls back to 1, and the process repeats. We will talk about driving this peripheral using the Zynq processor of the Zybo Z7-10 board in the future, For the scope of this exercise, we will focus on creating the IP and verify its functionality using a testbench created in Vivado.
Open up Vivado and create a new RTL project.
Do not specify sources at this time. Click Next.
Select the device or the evaluation board that this IP is going to be implemented. Pick Zybo Z7-10 and click Next.
Click FInish to create the project.
Select Tools->Create and Package New IP.
We are going to create an AXI-4 peripheral. Select this option and click Next.
Enter the details for the peripheral and click Next.
We will add two interfaces for the peripheral. By default, you will see an AXI slave interface on the list of interfaces. Keep it as is.
Add a stream master interface. This is the interface on which traffic generated in the IP move toward other peripherals on your FPGA design.
After creating interfaces, we need to customize the modules that our IP consists of. To do this, select the option Edit IP and click Finish.
A new project opens up in Vivado which contains all the modules that we need. You will see:
- trafficgen_v1_0.v - Top-level module
- trafficgen_v1_0_S00_AXI.v - Register module
- trafficgen_v1_0_M00_AXIS.v - Stream master module
When generating these modules in Vivado, the tool will automatically add sample code which I find really helpful in most cases. If you take a look at the source code of the top-level module, you'll notice that the register module and the stream master module have been instantiated.
What we need to do now is to add the logic corresponding to enable
and num_of_words
parameters. These outputs of the register module should be connected to the inputs of the stream master module. We do this by using the wires enable_w
and num_of_words_w
. I have created a parameter for the bit width of the number of words.
parameter integer NUM_OF_WORDS_WIDTH = 10,
Depending on your requirement, you may change the bit width.
wire enable_w;
wire [NUM_OF_WORDS_WIDTH-1:0] num_of_words_w;
Then we connect these wires to the respective ports of the register and master module instances.
.enable(enable_w),
.num_of_words(num_of_words_w),
Once the top-level module is done, let's take a look at the register module. We need to add two output ports for the two parameters we are going to use.
output wire enable,
output wire [NUM_OF_WORDS_WIDTH-1:0] num_of_words,
By default, we can see there are four slave registers in this module from slv_reg0
to slv_reg3
. But we only need two. So we are going to do some changes in the code such that:
//-- Number of Slave Registers 2
reg [C_S_AXI_DATA_WIDTH-1:0] enable_reg;
reg [C_S_AXI_DATA_WIDTH-1:0] num_of_words_reg;
We also need to replace other occurrences of slv_reg0
to slv_reg3
with enable_reg
and num_of_words_reg
. I am not going to explain all these in detail, but if you look at the source code of the register module, you will see how this has been done.
The next step is to modify the stream master module to generate the traffic pattern we need. Luckily, most of the hard work has been done by Vivado in the auto-generated file, so we only need to do some minor changes in the code. First, we need to add the input ports.
input wire enable,
input wire [NUM_OF_WORDS_WIDTH-1:0] num_of_words,
When we take a look at the code, we see that the tool has created a local parameter with the name NUMBER_OF_OUTPUT_WORDS
. We don't need this parameter because we are going to use the value of the input num_of_words
when creating the output stream. So, go ahead and delete NUMBER_OF_OUTPUT_WORDS
.
There is a read pointer that is incremented on each clock until it reaches num_of_words
at which point it rolls back to 1. This process is repeated indefinitely unless tx_en
is false.
always@(posedge M_AXIS_ACLK)
begin
if(!M_AXIS_ARESETN)
begin
read_pointer <= 0;
end
else
if (read_pointer <= num_of_words - 1)
begin
if (tx_en)
begin
read_pointer <= read_pointer + 1;
end
end
else if (read_pointer == num_of_words)
begin
read_pointer <= 1;
end
end
The stream transmission is valid only when the state machine state is SEND_STREAM
and enable
is true.
assign axis_tvalid = (mst_exec_state == SEND_STREAM) && enable;
The output stream of the master interface is based on the value of the read pointer.
always @( posedge M_AXIS_ACLK )
begin
if(!M_AXIS_ARESETN)
begin
stream_data_out <= 32'b1;
end
else if (tx_en)// && M_AXIS_TSTRB[byte_index]
begin
if (read_pointer == num_of_words)
begin
stream_data_out <= 32'b1;
end
else
begin
stream_data_out <= read_pointer + 32'b1;
end
end
end
Note that tlast
signal for each frame is asserted when the read pointer reaches num_of_words-1
.
assign axis_tlast = (read_pointer == num_of_words-1);
Feel free to do any customizations, for example, create different traffic patterns etc., depending on your requirements.
TestbenchA testbench has also been created to validate the behavior of the core that we created. The DUT has two clocks s00_axi_aclk
and m00_axis_aclk
. However, since we don't need two separate clocks we can just use the same clock signal to drive these ports.
To create the testbench, right-click Simulation Sources and then click Add Sources. Then select Add or create simulation sources.
If you want to create a source file for simulation from scratch, type a file name. and click OK. If you want to use the testbench in the repository or any other existing testbench, click Add Files to select the file.
The code I wrote for the testbench will
- Create clock signals and resets
- Set up the write channel
- Set up the read channel (this is done in order to verify if the write operation was successful)
- Use the
tready
signal of the master interface to enable, disable and re-enable the output stream - Disable and then enable the output stream by writing to the slave register (
enable
)
Before you launch the simulation make sure to select a library for all source files. If not, the simulation will fail.
To launch the simulation, click Run Behavioral Simulation
which will launch the waveform viewer. Observe the changes in various signals and make sure the simulation waveforms are accurate.
After verifying, we need to re-package the IP.
The IP is now ready to be used in another Vivado project.
What's next?Now that we have a working IP, the next step is to use it in a Vivado design to do some interesting stuff. We will see how this IP is used along with the Zynq processor in Zybo Z7-10 to generate and send data to DDR memory of the board using DMA transactions. Stay tuned!
Comments