ZYNQ: SPI Transmitter Using an AXI Stream Interface

AXI Interfaces are awesome because you can connect wires to them.

AXI interfaces are widely used within the Xilinx and ARM ecosystem. I found it’s well worth the time to write your own code using these standard interfaces because it allows you to connect to existing infrastructure. It is, for example, very easy to map an IP core with an AXI interface into the address space of the ARM microcontroller.

There are two main AXI interfaces: Standard and Stream. The Standard interface is a parallel interface with lots of address and data signals (like in the olden days of microprocessors). The Stream interface does not use addresses. It is a glorified parallel data bus with some clock and handshake. I believe the AXI Stream interface is the simplest AXI interface. You can easily connect it to existing IP cores which can map it into memory, add FIFOs or DMA capability.

In this tutorial we will learn:

  • How to use a wizard to create a custom IP core with an AXI Stream interface.
  • How to implement a simple SPI transmitter (SCLK, MOSI, SS).
  • How to simulate the behaviour using a test bench.

Let’s Create a Throw-Away Project

Create an IP Core With an AXI Stream Interface

  • The IP Core will end up at a fairly random location. Just go with it, we will copy it later to the desired location.

Implement the SPI Interface

  • The wizard creates two Verilog files: A wrapper (mySPI_Tx_AXIS_v1_0.v) and the actual IP core (mySPI_Tx_AXIS_v1_0_S00_AXIS.v).
  • The wizard also adds a default implementation with a FIFO. In this example we are not going to use that.
  • Edit the two files and replace the contents with the two source codes below.

mySPI_Tx_AXIS_v1_0.v

`timescale 1 ns / 1 ps

	module mySPI_Tx_AXIS_v1_0 #
	(
		// Users to add parameters here
        parameter integer width	= 8,
        parameter integer clkdiv= 4,

		// User parameters ends
		// Do not modify the parameters beyond this line


		// Parameters of Axi Slave Bus Interface S00_AXIS
		parameter integer C_S00_AXIS_TDATA_WIDTH	= 32
	)
	(
		// Users to add ports here
        output wire sclk,
        output wire mosi,
        output wire ss,
        
		// User ports ends
		// Do not modify the ports beyond this line


		// Ports of Axi Slave Bus Interface S00_AXIS
		input wire  s00_axis_aclk,
		input wire  s00_axis_aresetn,
		output wire  s00_axis_tready,
		input wire [C_S00_AXIS_TDATA_WIDTH-1 : 0] s00_axis_tdata,
		input wire [(C_S00_AXIS_TDATA_WIDTH/8)-1 : 0] s00_axis_tstrb,
		input wire  s00_axis_tlast,
		input wire  s00_axis_tvalid
	);

    // Instantiation of Axi Bus Interface S00_AXIS
	mySPI_Tx_AXIS_v1_0_S00_AXIS # (
        .width(width),
        .clkdiv(clkdiv),
		.C_S_AXIS_TDATA_WIDTH(C_S00_AXIS_TDATA_WIDTH)
	) mySPI_Tx_AXIS_v1_0_S00_AXIS_inst (
        .sclk(sclk),
        .mosi(mosi),
        .ss(ss),
		.S_AXIS_ACLK(s00_axis_aclk),
		.S_AXIS_ARESETN(s00_axis_aresetn),
		.S_AXIS_TREADY(s00_axis_tready),
		.S_AXIS_TDATA(s00_axis_tdata),
		.S_AXIS_TSTRB(s00_axis_tstrb),
		.S_AXIS_TLAST(s00_axis_tlast),
		.S_AXIS_TVALID(s00_axis_tvalid)
	);

	// Add user logic here

	// User logic ends

	endmodule

mySPI_Tx_AXIS_v1_0_S00_AXIS.v

`timescale 1 ns / 1 ps

	module mySPI_Tx_AXIS_v1_0_S00_AXIS #
	(
		// Users to add parameters here
        parameter integer width	= 8,
        parameter integer clkdiv= 4,

		// User parameters ends
		// Do not modify the parameters beyond this line

		// AXI4Stream sink: Data Width
		parameter integer C_S_AXIS_TDATA_WIDTH	= 32
	)
	(
		// Users to add ports here
        output wire sclk,
        output reg mosi = 0,
        output wire ss,

		// User ports ends
		// Do not modify the ports beyond this line

		// AXI4Stream sink: Clock
		input wire  S_AXIS_ACLK,
		// AXI4Stream sink: Reset
		input wire  S_AXIS_ARESETN,
		// Ready to accept data in
		output wire  S_AXIS_TREADY,
		// Data in
		input wire [C_S_AXIS_TDATA_WIDTH-1 : 0] S_AXIS_TDATA,
		// Byte qualifier
		input wire [(C_S_AXIS_TDATA_WIDTH/8)-1 : 0] S_AXIS_TSTRB,
		// Indicates boundary of last packet
		input wire  S_AXIS_TLAST,
		// Data is in valid
		input wire  S_AXIS_TVALID
	);

	// This holds the shift register
	reg [width-1 : 0] buffer = 0;
	reg buffer_full = 0;

    // Counts the bits
	reg [5:0] bitcounter = 0;
	
	// Makes things slower
	reg [clkdiv-1:0] prescaler = 0;

    // State machine states
    localparam IDLE = 0;
    localparam S1 = 1;
    localparam S2 = 2;
    localparam S3 = 3;

    // Default state is IDLE
    reg [1:0] state = IDLE;

    // Signals we are ready to receive
	assign S_AXIS_TREADY = !buffer_full;
	
	// SPI Clock (data is valid during Low/High transition)
	assign sclk = state==S2 || state==S3;
	
	// SPI Slave Select
	assign ss = state!=IDLE;
	
	// This is the main state machine
    always @(posedge S_AXIS_ACLK) begin
        // There is only one important rule for an AXI Stream interface:
        // If during the rising clock, S_AXIS_TVALID==1 and S_AXIS_TREADY==1, then we have to accept the data.  
        if (S_AXIS_TVALID==1 && S_AXIS_TREADY==1) begin
            buffer <= S_AXIS_TDATA[width-1 : 0];
            buffer_full = 1;
        end else if (state==S3 && prescaler==1) begin
            buffer_full = 0;
        end
    
        prescaler <= prescaler+1;
        if (prescaler==0) begin // The state transitions are synchronized to the SPI bit clock
            case(state)
                IDLE:   begin // ss=0, sclk=0, mosi=0
                            mosi <= 0;
                            if (buffer_full==1) begin
                                mosi <= buffer[width-1];
                                bitcounter <= 1;
                                state <= S1;
                            end
                        end
                S1:     begin // ss=1, sclk=0
                            if ( bitcounter==width ) begin
                                state <= S3;
                            end else begin
                                state <= S2;
                                buffer <= buffer<<1;
                            end
                        end
                S2:     begin // ss=1, sclk=1
                            state <= S1;
                            mosi <= buffer[width-1];
                            bitcounter <= bitcounter+1;
                        end
                S3:     begin // ss=1, sclk=1 (last bit)
                            if (buffer_full==1) begin
                                mosi <= buffer[width-1];
                                bitcounter <= 1;
                                state <= S1;
                            end else begin
                                state <= IDLE;
                            end
                        end
                default:begin
                            state <= IDLE;
                        end
            endcase
        end
    end

	endmodule

AXI Stream Interface (Oversimplified)

Here is a very simple view on the AXI Stream interface.

It consists of the following signals:

  • ACLK
  • Master-to-Slave: TDATA (e.g. 32 bits wide)
  • Master-to-Slave: TVALID
  • Slave-to-Master: TREADY

There are two simple rules:

  • All transitions happen on the rising edge of ACLK.
  • The slave (e.g. the SPI transmitter) will accept data only when this is true: TVALID=1 and TREADY=1 during a high-transition of the clock. Then (and only then) the data at TDATA is accepted by the slave.

The master observes this condition as well and thus knows when the data has been accepted by the slave. A transfer would look like this:

  • The master applies the data to TDATA and asserts TVALID
  • Then waits for TVALID=1 and TREADY=1. Once that condition occurred, the master can either de-assert TVALID (otherwise the data would be written twice) or apply the next data to TDATA and keep TVALID asserted.
  • The slave accepts data in case TVALID=1 and TREADY=1. If the slave will not be ready to accept data at the next positive clock, it is important to de-assert TREADY. Otherwise the master could send the next data at the next clock edge.

Add a Test Bench

Add the Test Bench Code

  • Don’t forget to make the test bench file the top-level module

tb_mySPI_AXIS.v

`timescale 1ns / 1ps

module tb_mySPI_AXIS();
    wire sclk;
    wire mosi;
    wire ss;
    
    reg s00_axis_aclk = 0;
    reg s00_axis_aresetn = 0;
    wire s00_axis_tready;
    reg [31:0]s00_axis_tdata=0;
    reg s00_axis_tstrb=0;
    reg s00_axis_tlast=0;
    reg s00_axis_tvalid=0; 

	mySPI_Tx_AXIS_v1_0_S00_AXIS # (
        .width(8),
        .clkdiv(2),
		.C_S_AXIS_TDATA_WIDTH(32)
	) mySPI_Tx_AXIS_v1_0_S00_AXIS_inst (
        .sclk(sclk),
        .mosi(mosi),
        .ss(ss),
		.S_AXIS_ACLK(s00_axis_aclk),
		.S_AXIS_ARESETN(s00_axis_aresetn),
		.S_AXIS_TREADY(s00_axis_tready),
		.S_AXIS_TDATA(s00_axis_tdata),
		.S_AXIS_TSTRB(s00_axis_tstrb),
		.S_AXIS_TLAST(s00_axis_tlast),
		.S_AXIS_TVALID(s00_axis_tvalid)
	);

    always begin
        #1 s00_axis_aclk=~s00_axis_aclk;
    end   

    initial begin
        #20;
        @ (posedge s00_axis_aclk) s00_axis_tdata = 16'hAAAA;s00_axis_tvalid = 1;
        while (s00_axis_tready==0) begin
            @ (posedge s00_axis_aclk) ;
        end
        @ (posedge s00_axis_aclk) s00_axis_tdata = 16'h5555;s00_axis_tvalid = 1;
        while (s00_axis_tready==0) begin
            @ (posedge s00_axis_aclk) ;
        end
        @ (posedge s00_axis_aclk) s00_axis_tdata = 16'h0000;s00_axis_tvalid = 0;
    end
endmodule 

Simulate the SPI Transmitter

  • Note that you can drag signals into the diagram. You need to reset the simulation and restart it (using the play-buttons on top of the screen). But you don’t need to run the behavioural simulation again unless you change the Veriolg code.
  • To get more space it is useful to undock the diagram window.
  • If you close the simulation you may be asked if you want to save the waveform configuration (a *.wcfg file). Make sure to save it to the project directory. You will be asked to add it to the project as well.

Package the IP Core

 

Next: Use the SPI Transmitter in a Block Diagram

  • The IP Core should now be in c:/VivadoProjects/ip_repo/mySPI_Tx_AXIS_1.0
  • You can close the Throw-Away project now and delete it.
  • Click here to learn how  to use your new IP core.

Leave a comment

Your email address will not be published. Required fields are marked *