Creating Your own AXI4-Lite Logic

Handling axi4-lite transactions

12156

The Xilinix IP Wizard automatically creates logic that handles AXI4-lite reads and writes to a set of generated registers. To gain a deeper understanding of the axi4-lite interface, we will go over writing our own axi4-lite slave logic.

Write transactions

A write transaction has 3 phases: the write data phase, the write address phase, and the write response phase. The write data and address phases can occur independently of one another, but the write response phase only occurs after the data and address phases.

In a write transaction the slave device can wait for both data and address handshakes to occur and then send a response. Since write data can be provided before the address is provided, the data and address should be latched. After both handshakes have occurred the slave knows both the address and data have been latched and can (proceed to use the data).

Defining channel logic

Now we will write our own logic to respond to a axi4-lite write transaction initiated by a bus master

Recall the Axi handshake procedure:

  1. channel handshake is initiated by placing the payload on the bus and asserting “valid”
  2. the valid signal is detected and when ready to receive the data asserts “ready”
  3. When both “ready” and “valid” are both asserted, the next positive clock edge completes the handshake. Both “ready” and “valid” de-assert before the next rising edge

From the slaves perpective this sequence of events occurs, assume

  1. Slave waits for valid signal from the master
  2. slave receives valid signal, asserts its ready signal to acknowledge
  3. both valid and ready are asserted, slave de-asserts ready

The slave device’s can be in two states: Waiting for data, and acknowledging data We can then make a simple state machine that covers the behavior of the slave; Writing this up in verilog can look something like this:

module axi_hs(
	input clk,rst,
	input valid,
	output reg ready
);

	always @(posedge clk)
		if( rst | (ready & valid))
			ready<=0;
		else if( ~ready & valid)
			ready <=1;


endmodule


Extending this to the write address and write data transactions we can write:


module axi_write_logic(
	input axi_clk,
	input rstn, //axi is reset low


	input [3:0] write_addr,
	input write_addr_valid,
	output reg write_addr_ready,

	input [31:0] write_data,
	input write_data_valid,
	output reg write_data_valid

);



	//write address handshake
	always @(posedge axi_clk)
	begin
		if(~rstn | (write_addr_valid & write_addr_ready) )
			write_addr_ready<=0;
		else if(~write_addr_ready & write_addr_valid)
			write_addr_ready<=1;
	end

	//write data handshake
	always @(posedge axi_clk)
	begin
		if(~rstn | (write_data_valid & write_data_ready) )
			write_data_ready<=0;
		else if(~write_data_ready & write_data_valid)
			write_data_ready<=1;
	end

endmodule

This code only handles the axi handshake logic and does nothing with the two ‘payloads’ for the data and address channels: the write data and the write address. As stated earlier, these should be latched after their respective handshakes, so if one handshake precedes the other the data or address will be saved after the handshake is complete. Note that the address latch doesn’t save the two lowest bits of the address. This is because the ARM uses a byte-addressing system. For our IP we are only interested in word-aligned addresses, thus, we can discard the lower two bits of the address.

Using the axi signals, the data can be latched as follows

	//flip flops for latching data
	reg [31:0] data_latch;
	reg [1:0] addr_latch;


	//latching logic
	always @(posedge axi_clk)
	begin
		if(rstn==0)
		begin
			data_latch<=32'd0;
			addr_latch<=2'd0;
		end
		else
		begin
			if(write_data_valid & write_data_ready) //look for data handshake
				data_latch<=write_data;
			
			if(write_addr_valid & write_addr_ready)
				addr_latch<=write_addr;
		end
	end

Write response and extra logic

For every axi4-lite write transaction, after both the address and data phases complete, the slave must send a write response. The above verilog defines a system that handles both the address and data phases of an axi transaction and handles latching the associated payload. However, the system has no mechanism to detect when both handshakes have completed, thus, it has no way of knowing when to send a write response.

The following code segment implements two registers and logic keep track of which handshakes have been made and reset after both have been made, so the device can handle subsequent transactions.


//flip flops for latching data

	reg addr_done;	//indicates the address handshake has occured
	reg data_done;	//indicates the data handshake has occured


	//keep track of which handshakes completed
	always @(posedge axi_clk)
	begin
		if(rstn==0 || (addr_done & data_done) ) //reset or both phases done
		begin
			addr_done<=0;
			data_done<=0;
		end
		else
		begin	

			if(write_addr_valid & write_addr_ready) //look for addr handshake
				addr_done<=1;
		
			if(write_data_valid & write_data_ready) //look for data handshake
				data_done<=1;	
		end
	end

Now that we have a mechanism to tell when both phases have completed, we can implement the write response logic, note that since the slave is the data provider, it controls the valid signal and waits for the ready signal:


//new signal definitions
input write_resp_ready,
output reg write_resp_valid
output [1:0] write_resp

	assign write_resp =2’d0; //always indicate OKAY status for writes
	
	//write response logic
	always @(posedge axi_clk)
	begin	
		if( rstn==0 | (write_resp_valid & write_resp_ready) )
			write_resp_valid<=0;
		else if(~write_resp_valid & (data_done & addr_done) )
			write_resp_valid<=1;	
	end	
end

Passing data onwards

All the logic preceding serves to handle an axi4-lite write transaction. After both handshakes, the two registers, data_latch and addr_latch, will contain the write data and address from the master. After both of these values are latched, they can be used by your IP’s logic. An additional signal can be used to indicate that this data is valid. Recall the data_done and addr_done indicate that the respective latches have valid data and when both data_done and addr_done are asserted at the same time, they will stay high for one clock cycle. The logical AND of both can then be used as a data valid signal.

output [31:0] data_out, //data output to external logic
output [1:0] addr_out, //address output to external logic
output data_valid		//signal indicating output data and address are valid

assign data_out = data_latch;
assign addr_out = addr_latch;

assign data_valid = data_done & addr_done;

Putting it all together

We can combine all of our code into a module.

module axi_write_logic(
	input axi_clk,
	input rstn, //axi is reset low


	//write address channel
	input [1:0] write_addr,
	input write_addr_valid,
	output reg write_addr_ready,


	//write data channel
	input [31:0] write_data ,
	input write_data_valid,
	output reg write_data_ready,

	//write response channel	
	output [1:0] write_resp,
	input write_resp_ready,
	output reg write_resp_valid,


	
	output [31:0] data_out, //data output to external logic
	output [1:0] addr_out, //address output to external logic
	output data_valid		//signal indicating output data and address are valid
);


	reg addr_done;
	reg data_done;


	//flip flops for latching data
	reg [31:0] data_latch;
	reg [1:0] addr_latch;


	assign data_out = data_latch;
	assign addr_out = addr_latch;

	assign data_valid = data_done & addr_done;

	assign write_resp = 2'd0; //always indicate OKAY status for writes

	//write address handshake
	always @(posedge axi_clk)
	begin
		if(~rstn | (write_addr_valid & write_addr_ready) )
			write_addr_ready<=0;
		else if(~write_addr_ready & write_addr_valid)
			write_addr_ready<=1;
	end

	//write data handshake
	always @(posedge axi_clk)
	begin
		if(~rstn | (write_data_valid & write_data_ready) )
			write_data_ready<=0;
		else if(~write_data_ready & write_data_valid)
			write_data_ready<=1;
	end


	//keep track of which handshakes completed
	always @(posedge axi_clk)
	begin
		if(rstn==0 || (addr_done & data_done) ) //reset or both phases done
		begin
			addr_done<=0;
			data_done<=0;
		end
		else
		begin	

			if(write_addr_valid & write_addr_ready) //look for addr handshake
				addr_done<=1;
		
			if(write_data_valid & write_data_ready) //look for data handshake
				data_done<=1;	
		end
	end

	//latching logic
	always @(posedge axi_clk)
	begin
		if(rstn==0)
		begin
			data_latch<=32'd0;
			addr_latch<=2'd0;
		end
		else
		begin
			if(write_data_valid & write_data_ready) //look for data handshake
				data_latch<=write_data;
			
			if(write_addr_valid & write_addr_ready)
				addr_latch<=write_addr;
		end
	end


	//write response logic
	always @(posedge axi_clk)
	begin	
		if( rstn==0 | (write_resp_valid & write_resp_ready) )
			write_resp_valid<=0;
		else if(~write_resp_valid & (data_done & addr_done) )
			write_resp_valid<=1;	
	end



endmodule

Using the external signals.

The data output, address output, and valid signals can now then be used by other logic to implement your axi peripheral. One use case, illustrated below, might be writing to a register file. This, however, is not necessarily the case; The data can be passed to a FIFO, serial port, etc.

module axi_mapped_registers(

input axi_clk,
input axi_rstn,

input [31:0] axi_write_data,
input write_data_valid,
output write_data_ready,

input [1:0] axi_write_addr,
input write_addr_valid,
output write_addr_ready,

output [1:0] axi_write_resp,
input write_resp_ready,
output write_resp_valid



);


	//four registers
	reg [31:0] rfile[3:0];
	wire [1:0] wraddr;
	wire [31:0] rdata_in;
	wire write_en;



	//instatiate axi slave logic controller
	axi_write_logic wrinst(

		.axi_clk(axi_clk),
		.rstn(axi_rstn),

		.write_addr(axi_write_addr),
		.write_addr_valid(write_addr_valid),
		.write_addr_ready(write_addr_ready),


		.write_data(axi_write_data),
		.write_data_valid(write_data_valid),
		.write_data_ready(write_data_ready),

		.write_resp(axi_write_resp),
		.write_resp_ready(write_resp_ready),
		.write_resp_valid(write_resp_valid),


		.data_out(rdata_in),
		.addr_out(wraddr),
		.data_valid(write_en)

	);

	integer i;


	//write logic for register file
	always @(posedge axi_clk)
	begin
		if(axi_rstn==0)
		begin
			for(i=0;i<4;i=i+1)
				rfile[i]<=0;
		end
		else if(write_en)
			rfile[wraddr] <= rdata_in;
	end



endmodule

Note: The registers defined in the module above are not connected externally

Next Steps

Now that you have a module that can handle write transactions, and save the data to registers, modify the module to handle read transactions as well.