Simulating the APB bus

emulating an APB bus master to test APB-connected peripherals

2894

Simulating an APB bus transaction.

Compared to the multi-channel approach of AXI, the APB’s bus transactions are relatively simple. As with designing the hardware, this also reduces the complexity of simulating APB hardware.

The APB Bus Cycle

Each APB bus transaction lasts a minimum of two clock cycles. The first cycle is the setup phase, where the bus master makes a request to read or write to a slave. All subsequent cycles are in the access phase, where the slave and master exchange data (depending on if the transaction is a read or a write). The slave can extend the transaction by indicating it is not ready in the access phase. When no slave is selected, the bus is unused (idle).

Quiescent Bus state

When the bus is idle, no data is being read or written to the bus. This can be determined from the psel signal, if it is low for all slaves, the bus is idle.

The first cycle, setup phase

On the first cycle of an APB transfer, the bus master drives the select signal for the slave it is accessing high. The master also places an address on the bus, indicating to the slave what address is being accessed for a read or write.

For a read, the pwrite signal is driven low, indicating that the slave needs to drive the read data bus: prdata.

For a write, the master drives pwrite high, as well as placing the data to be written on the write data bus: pwdata.

The setup phase is always one cycle, thus the access phase always begins at the next positive clock edge.

The second cycle, access phase

The access phase, unlike the setup phase, can take multiple clock cycles. This phase is indicated by penable being driven high. It will stay high until the transfer completes. When penable is high, the master samples the pready signal. If pready and penable are both high, the master completes the bus transfer, and it can start another in the next clock cycle.

In a read transaction, the slave drives the read data bus. In this case, the pready signal can be thought of as a ‘valid’ signal for rdata.

For a write, pready serves as a write acknowledgement to the bus master.

In both cases, pready can be held low to extend a transfer (sometimes referred to as “inserting wait states”). For example, this can be used if the memory being accessed is slower than the bus speed. In such a case the slave might use a counter to delay a transaction for a specified amount of time, or it might rely on handshaking signals from external logic/memory. If a slave can always perform read/writes in a single cycle, it can drive pready high always, simplifying the logic needed.

Slave Error Signal

There is also a slave error signal, pslverr. In most cases this isn’t utilized and can always be driven low. However it can be used to indicate an address being accessed is invalid or other errors during transactions. The signal is sampled by the bus master when psel, penable, and pready are all high (end of the transaction).

Simulating an APB read transaction.

The APB Read and write transactions are very similar. The only difference being the direction data is being moved, which is determined by how the master drives the pwrite signal at the start of the transaction. If it is driven low, the master is making a read at the requested address and will take up the data driven on prdata. For a write, pwrite is driven high and the master drives write data on the pwdata bus through the entire transaction.

Provided is a simple APB-connected device which will read constant values depending on the address.

//simple read-only apb slave
//returns constant values based on the address
//ignores but accepts all writes
module simple_apb(
	input rst,

	input clk,

	input [31:0] paddr,
	input psel,
	input penable,
	input pwrite,

	output pready,
	output pslverr,

	input [31:0] pwdata,
	output [31:0] prdata

);

	reg [31:0] read_data;

	assign prdata = read_data;
	assign pready = 1'b1;
	assign pslverr = 1'b0;


	//drive read data bus
	//with constant data, based on requested address
	always @(paddr)
	begin
		//use 2-bit (4-byte) addresses: 4 addresses
		case(paddr[3:2])
			0:
				read_data= 32'hAAAA0000;
			1:
				read_data= 32'hBBBB1111;
			2:
				read_data = 32'hCCCC2222;
			3:
				read_data = 32'hDDDD3333;
		endcase
	end


endmodule

This example module will respond to read transactions with different data based on what address it is given. The bits 3 and 2 are used to decode which value is muxed onto the data bus for a read cycle. It is common to ignore the lowest 2 bits are they’re used for byte addressing. As the Zynq doesn’t allow unaligned accesses through the FPGA AXI port, bits 1 and 0 of the address will always be zero. It is also common to decode the upper bits of the address external to the peripheral itself; In the case of a Zynq axi system it would be connected to an AXI-APB bridge. The AXI-APB bridge can be configured to assign base address and address ranges at which it will assert psel for each peripheral. Thus the AXI-APB bridge will do the upper address space decoding and we only need to define the address space inside the module (In this case, only four discrete addresses, so we only need 2 bits of the address bus).

The given module will always indicate ready, so both read and write transactions will complete in the 2nd cycle of the transaction. As there is no logic to process the write data from the bus master, writes will have no effect.

Simulating the APB bus phases

Setup Phase

The first clock cycle of the transaction, the bus master drives the corresponding psel for the slave being addressed, it also drives the 32 bit ‘paddr’ which the slave will use for decoding. At the same time the master indicates it is making a read by driving pwrite low.


//start a transaction,
//read from 32'd4, (since pwrite is low)
psel<=1;
paddr<=32'd4;
pwrite<=0;

@(posedge clk); //wait for next clock edge (to start the access phase)
Access Phase

The second clock cycle starts the access phase, so penable must be driven high. An actual bus master would have to sample pready during this phase to determine if it needs to extend the transaction. However we can make this simulation with the assumption that our simulated slave can be accessed in a single cycle. You should consider modifying the example code to correctly poll pready, in case you need to design a slave with wait states, or wish to simulate IP that you didn’t design.

//indicate in the access phase
penable<=1;

@(posedge clk); //end the access phase (assume pready is 1)
Returning the bus to idle state

At this point the bus either returns idle (psel<=0, penable<=0), or the next transaction’s access phase can begin (psel<=1, penable<=0). For our case, we do not need to simulate back-to-back transfers, thus we can place the bus back in a known state at the end of this task, this way tasks being invoked after this one will always start with an idle bus.

penable<=0;
psel<=0;
Simulating the bus


module apb_sim();

	reg rst;

	reg clk;

	//apb signals to slave
	reg [31:0] paddr;
	reg psel;
	reg penable;
	reg pwrite;
	reg [31:0] pwdata;

	//apb signals from slave
	wire pready;
	wire pslverr;
	wire [31:0] prdata;


	//instantiate the 
	simple_apb abp_uut(
		.rst(rst),

		.clk(clk),

		.paddr(paddr),
		.psel(psel),
		.penable(penable),
		.pwrite(pwrite),
		.pready(pready),
		.pslverr(pslverr),

		.pwdata(pwdata),
		.prdata(prdata)

	);

	//simulate clock
	always
		#5 clk <=~clk;


	integer addr;

	//sequence signals 
	initial
	begin
		clk<=0;
		rst<=1;

		//initial values for bus signals
		psel<=0;
		penable<=0;
		pwrite<=0;
		paddr<=0;
		pwdata<=0;

		//make sure rst is asserted for 1 or more cycles
		@(posedge clk);
		@(posedge clk);

		//deassert reset
		rst<=0;
		@(posedge clk);
		@(posedge clk);


		//now we can simulate multiple reads
		//increment address by 4 bytes each iteration
		for(addr=0;addr<32'd16;addr=addr+4)
		begin
			@(posedge clk);//wait for next positive edge

			//setup phase, making a read from address given
			psel<=1;
			paddr<=addr;
			pwrite<=0;

			@(posedge clk); //move to access phase

			//indicate in access phase
			//slave will drive read data
			penable<=1;

			@(posedge clk); //end of access phase, thus transaction ends

			//place bus into idle state
			psel<=0;
			penable<=0;

		end
		$finish;
	end

endmodule

Combining sequencing into a task

The sequence for the APB read transaction can be condensed into a verilog simulation task. This allows the invocation of the task rather than needing to copy the sequence of procedural statements multiple times. Tasks fall within the scope of a verilog simulation module (so they need to be placed prior to the endmodule statement, and signals used in the task need to be defined in the module.



//NOTE: This was designed with the assumption the slave will not hold pready low
//As this signal is ignored, this may not work with any slave that requests wait states,
//and it must be modified to implement this functionality.
//Due to the posedge clk at the start of the task, back to back transfers are not possible.
//Task assumes bus is in idle state (psel and penable are low) when it is invoked.
task apb_read;
input [31:0] addr;
begin
	@(posedge clk); //start this at the next positive edge

	//first clock cycle of transaction (setup phase):
	//Drive peripheral select high, indicate to slave it is being addressed
	//Put address being accessed on the bus
	//Since this is a read, prwite is zero. Write data is don't care
	psel<=1;
	paddr<=addr;
	pwrite<=0;
	@(posedge clk); //go to next cycle


	//Second cycle (access phase):
	//penable is driven high,
	//slave drives the read data bus
	//slave can drive pready low to insert wait states (not implemented here) 
	penable<=1;
	@(posedge clk); //This transaction is over

	//return bus to quiescent values
	psel<=0;
	penable<=0;

end
endtask


Now we can just call the task, with an argument, to simulate a read on the apb bus.

The initial block above can be modified to this:



	initial
	begin
		clk<=0;
		rst<=1;

		//initial values for bus signals
		psel<=0;
		penable<=0;
		pwrite<=0;
		paddr<=0;
		pwdata<=0;

		//make sure rst is asserted for 1 or more cycles
		@(posedge clk);
		@(posedge clk);

		//deassert reset
		rst<=0;
		@(posedge clk);
		@(posedge clk);

		//using task
		//increment address by 4 bytes each iteration
		for(addr=0;addr<32'd16;addr=addr+4)
		begin
			//call read task with addr as the argument
			apb_read(addr);
			@(posedge clk);
		end
		$finish;
	end

APB Write transaction

The APB Write transaction is largely the same, however with pwrite asserted and the bus master placing valid write data on the bus (to be written to the given address).