AHB-Lite SPI Master in Verilog: RTL, Registers and Testbench
This tutorial explains how to connect a simple SPI master to an AHB-Lite bus using Verilog. It covers the system block diagram, AHB-Lite and SPI signals, memory-mapped registers, RTL, and a simulation testbench.
The example transfers one byte at a time, sends the most significant bit first, and supports the four standard SPI clock modes through CPOL and CPHA settings. It is an educational starting point, not a complete production peripheral.
AHB-Lite SPI master block diagram
+-------------------+ AHB-Lite +--------------------------+
| | HADDR, HTRANS | |
| AHB-Lite master | HWRITE, HWDATA ----> | AHB-Lite register block |
| (CPU / testbench)| <---- HRDATA | |
| | <---- HREADYOUT | CTRL / TX / RX / STATUS |
+-------------------+ +-------------+------------+
|
| start, data,
| clock settings
v
+--------------------------+
| SPI bit engine |
+-----+------+------+-----+
| | |
SCLK MOSI MISO
| | |
+------|------+
|
+----v----+
| SPI |
| slave |
+---------+
The SPI chip-select output (spi_cs_n) is also connected to the SPI slave.
The AHB-Lite register block stores configuration and transmit data. When software writes the command register, the SPI bit engine performs the serial transfer and makes the received byte available through the receive-data register.
AHB-Lite and SPI interface signals
AHB-Lite signals
| Signal | Direction | Description |
|---|---|---|
HCLK |
Input | AHB-Lite system clock. |
HRESETn |
Input | Active-low reset. |
HSEL |
Input | Slave select from the AHB-Lite interconnect. |
HADDR[31:0] |
Input | Byte address for the current AHB transfer. |
HTRANS[1:0] |
Input | Transfer type. HTRANS[1] is high for NONSEQ and SEQ transfers. |
HWRITE |
Input | High for a write transfer; low for a read transfer. |
HWDATA[31:0] |
Input | Write data for the AHB data phase. |
HREADY |
Input | Bus-wide ready signal. The slave advances its AHB pipeline when this is high. |
HRDATA[31:0] |
Output | Read data from the selected register. |
HREADYOUT |
Output | Slave ready signal. This example is always ready. |
HRESP |
Output | Transfer response. This example always returns OKAY. |
SPI signals
| Signal | Direction | Description |
|---|---|---|
spi_sclk |
Output | SPI serial clock. |
spi_mosi |
Output | Master-out, slave-in data. Transmitted MSB first. |
spi_miso |
Input | Master-in, slave-out data. |
spi_cs_n |
Output | Active-low slave select. Low while a transfer is in progress. |
Memory-mapped register map
All registers are 32 bits wide and word-aligned.
| Offset | Name | Access | Description |
|---|---|---|---|
0x00 |
CTRL |
Read/write | Bit 0: CPOL; bit 1: CPHA; bits 15:8: SPI half-period divider. A divider of 0 produces one system-clock cycle per SPI half-period. |
0x04 |
TXDATA |
Read/write | Transmit byte is stored in bits 7:0. |
0x08 |
RXDATA |
Read-only | Most recently received byte is returned in bits 7:0. |
0x0C |
STATUS |
Read-only | Bit 0: busy; bit 1: done; bit 2: receive data valid. |
0x10 |
COMMAND |
Write-only | Write bit 0 as 1 to start a transfer. A start request while busy is ignored. |
The done and rx_valid status bits are cleared
when the next transfer is accepted. The transmit data and control
settings are captured at the start of a transfer.
Verilog RTL for the SPI master
The AHB-Lite write channel is pipelined: the address and control phase
is captured first, and the associated HWDATA is applied in
the following data phase. The read address is registered for the
register read mux.
module ahb_lite_spi_master (
input wire HCLK,
input wire HRESETn,
input wire HSEL,
input wire [31:0] HADDR,
input wire [1:0] HTRANS,
input wire HWRITE,
input wire [31:0] HWDATA,
input wire HREADY,
output reg [31:0] HRDATA,
output wire HREADYOUT,
output wire HRESP,
output reg spi_sclk,
output reg spi_mosi,
input wire spi_miso,
output reg spi_cs_n
);
assign HREADYOUT = 1'b1;
assign HRESP = 1'b0;
localparam [7:0] ADDR_CTRL = 8'h00;
localparam [7:0] ADDR_TXDATA = 8'h04;
localparam [7:0] ADDR_RXDATA = 8'h08;
localparam [7:0] ADDR_STATUS = 8'h0C;
localparam [7:0] ADDR_COMMAND = 8'h10;
reg [31:0] ctrl_reg;
reg [7:0] tx_reg;
reg [7:0] rx_reg;
reg busy;
reg done;
reg rx_valid;
reg wr_pending;
reg [7:0] wr_addr;
reg [7:0] rd_addr;
reg start_pulse;
reg [7:0] tx_shift;
reg [7:0] rx_shift;
reg [3:0] bit_count;
reg [7:0] div_count;
reg active_cpol;
reg active_cpha;
reg [7:0] active_div;
// AHB-Lite register interface.
always @(posedge HCLK or negedge HRESETn) begin
if (!HRESETn) begin
ctrl_reg <= 32'h0000_0000;
tx_reg <= 8'h00;
wr_pending <= 1'b0;
wr_addr <= 8'h00;
rd_addr <= 8'h00;
start_pulse <= 1'b0;
end else begin
start_pulse <= 1'b0;
if (HREADY) begin
if (wr_pending) begin
case (wr_addr)
ADDR_CTRL: begin
ctrl_reg <= HWDATA;
end
ADDR_TXDATA: begin
tx_reg <= HWDATA[7:0];
end
ADDR_COMMAND: begin
if (HWDATA[0])
start_pulse <= 1'b1;
end
default: begin
// RXDATA and STATUS are read-only.
end
endcase
end
wr_pending <= HSEL && HTRANS[1] && HWRITE;
if (HSEL && HTRANS[1] && HWRITE)
wr_addr <= HADDR[7:0];
if (HSEL && HTRANS[1] && !HWRITE)
rd_addr <= HADDR[7:0];
end
end
end
// Combinational read mux.
always @* begin
case (rd_addr)
ADDR_CTRL:
HRDATA = ctrl_reg;
ADDR_TXDATA:
HRDATA = {24'h0, tx_reg};
ADDR_RXDATA:
HRDATA = {24'h0, rx_reg};
ADDR_STATUS:
HRDATA = {29'h0, rx_valid, done, busy};
default:
HRDATA = 32'h0000_0000;
endcase
end
// SPI master engine.
always @(posedge HCLK or negedge HRESETn) begin
if (!HRESETn) begin
spi_sclk <= 1'b0;
spi_mosi <= 1'b0;
spi_cs_n <= 1'b1;
busy <= 1'b0;
done <= 1'b0;
rx_valid <= 1'b0;
rx_reg <= 8'h00;
tx_shift <= 8'h00;
rx_shift <= 8'h00;
bit_count <= 4'd0;
div_count <= 8'd0;
active_cpol <= 1'b0;
active_cpha <= 1'b0;
active_div <= 8'd0;
end else begin
if (start_pulse && !busy) begin
busy <= 1'b1;
done <= 1'b0;
rx_valid <= 1'b0;
active_cpol <= ctrl_reg[0];
active_cpha <= ctrl_reg[1];
active_div <= ctrl_reg[15:8];
tx_shift <= tx_reg;
rx_shift <= 8'h00;
bit_count <= 4'd0;
div_count <= 8'd0;
spi_sclk <= ctrl_reg[0];
spi_cs_n <= 1'b0;
if (ctrl_reg[1] == 1'b0)
spi_mosi <= tx_reg[7];
else
spi_mosi <= 1'b0;
end else if (busy) begin
if (div_count == active_div) begin
div_count <= 8'd0;
spi_sclk <= ~spi_sclk;
// Before the toggle, SCLK equals CPOL on a leading edge.
if (spi_sclk == active_cpol) begin
if (active_cpha) begin
// CPHA=1: change MOSI on the leading edge.
spi_mosi <= tx_shift[7];
tx_shift <= {tx_shift[6:0], 1'b0};
end else begin
// CPHA=0: sample MISO on the leading edge.
rx_shift <= {rx_shift[6:0], spi_miso};
bit_count <= bit_count + 1'b1;
end
end else begin
if (active_cpha) begin
// CPHA=1: sample MISO on the trailing edge.
rx_shift <= {rx_shift[6:0], spi_miso};
if (bit_count == 4'd7) begin
rx_reg <= {rx_shift[6:0], spi_miso};
busy <= 1'b0;
done <= 1'b1;
rx_valid <= 1'b1;
spi_cs_n <= 1'b1;
end else begin
bit_count <= bit_count + 1'b1;
end
end else begin
// CPHA=0: change MOSI on the trailing edge.
tx_shift <= {tx_shift[6:0], 1'b0};
spi_mosi <= tx_shift[6];
if (bit_count == 4'd8) begin
rx_reg <= rx_shift;
busy <= 1'b0;
done <= 1'b1;
rx_valid <= 1'b1;
spi_cs_n <= 1'b1;
end
end
end
end else begin
div_count <= div_count + 1'b1;
end
end
end
end
endmodule
Simulation testbench
This testbench writes a transmit byte, starts an SPI transfer, and
models a slave that returns 8'hA5. It demonstrates SPI
mode 0, where CPOL=0 and CPHA=0.
`timescale 1ns/1ps
module tb_ahb_lite_spi_master;
reg HCLK;
reg HRESETn;
reg HSEL;
reg [31:0] HADDR;
reg [1:0] HTRANS;
reg HWRITE;
reg [31:0] HWDATA;
reg HREADY;
wire [31:0] HRDATA;
wire HREADYOUT;
wire HRESP;
wire spi_sclk;
wire spi_mosi;
reg spi_miso;
wire spi_cs_n;
reg [31:0] read_value;
reg [31:0] status_value;
integer rx_bit_index;
integer timeout_count;
localparam [7:0] SLAVE_RESPONSE = 8'hA5;
ahb_lite_spi_master dut (
.HCLK (HCLK),
.HRESETn (HRESETn),
.HSEL (HSEL),
.HADDR (HADDR),
.HTRANS (HTRANS),
.HWRITE (HWRITE),
.HWDATA (HWDATA),
.HREADY (HREADY),
.HRDATA (HRDATA),
.HREADYOUT (HREADYOUT),
.HRESP (HRESP),
.spi_sclk (spi_sclk),
.spi_mosi (spi_mosi),
.spi_miso (spi_miso),
.spi_cs_n (spi_cs_n)
);
// 100 MHz system clock.
initial begin
HCLK = 1'b0;
forever #5 HCLK = ~HCLK;
end
// Provide the first response bit before the first rising SCLK edge.
initial begin
spi_miso = SLAVE_RESPONSE[7];
rx_bit_index = 0;
end
// Mode-0 slave model: advance response data on falling SCLK edges.
always @(negedge spi_sclk) begin
if (!spi_cs_n) begin
rx_bit_index = rx_bit_index + 1;
if (rx_bit_index < 8)
spi_miso = SLAVE_RESPONSE[7-rx_bit_index];
else
spi_miso = 1'b0;
end
end
// AHB-Lite write task for this zero-wait-state example.
task ahb_write;
input [7:0] address;
input [31:0] data;
begin
@(negedge HCLK);
HSEL = 1'b1;
HTRANS = 2'b10;
HWRITE = 1'b1;
HADDR = {24'h000000, address};
@(negedge HCLK);
HSEL = 1'b0;
HTRANS = 2'b00;
HWRITE = 1'b0;
HWDATA = data;
@(negedge HCLK);
HWDATA = 32'h0000_0000;
end
endtask
task ahb_read;
input [7:0] address;
output [31:0] data;
begin
@(negedge HCLK);
HSEL = 1'b1;
HTRANS = 2'b10;
HWRITE = 1'b0;
HADDR = {24'h000000, address};
@(posedge HCLK);
#1;
data = HRDATA;
@(negedge HCLK);
HSEL = 1'b0;
HTRANS = 2'b00;
HADDR = 32'h0000_0000;
end
endtask
initial begin
HRESETn = 1'b0;
HSEL = 1'b0;
HADDR = 32'h0000_0000;
HTRANS = 2'b00;
HWRITE = 1'b0;
HWDATA = 32'h0000_0000;
HREADY = 1'b1;
repeat (4) @(negedge HCLK);
HRESETn = 1'b1;
// Divider=1, CPHA=0, CPOL=0: SPI mode 0.
ahb_write(8'h00, 32'h0000_0100);
// Load the transmit byte.
ahb_write(8'h04, 32'h0000_003C);
// Start the transfer.
ahb_write(8'h10, 32'h0000_0001);
// Poll STATUS until BUSY clears.
timeout_count = 0;
status_value = 32'h0000_0000;
while (status_value[0] == 1'b1 || timeout_count == 0) begin
ahb_read(8'h0C, status_value);
timeout_count = timeout_count + 1;
if (timeout_count > 2000) begin
$display("ERROR: timeout waiting for SPI transfer");
$finish;
end
end
// Read and check the received byte.
ahb_read(8'h08, read_value);
if (read_value[7:0] !== SLAVE_RESPONSE) begin
$display("ERROR: RXDATA expected %02h, got %02h",
SLAVE_RESPONSE, read_value[7:0]);
$finish;
end
if (status_value[1] !== 1'b1 || status_value[2] !== 1'b1) begin
$display("ERROR: DONE or RX_VALID status bit was not set");
$finish;
end
$display("PASS: transmitted 0x3C and received 0x%02h",
read_value[7:0]);
$finish;
end
endmodule
Running the simulation
- Save the RTL as
ahb_lite_spi_master.v. - Save the testbench as
tb_ahb_lite_spi_master.v. - Compile both files with a Verilog simulator.
- Run the simulation and check for the
PASSmessage.
iverilog -o spi_sim ahb_lite_spi_master.v tb_ahb_lite_spi_master.v
vvp spi_sim
Design limitations and possible extensions
This example transfers one byte per command, has one chip-select
output, and does not insert AHB-Lite wait states. The approximate SPI
clock frequency is
HCLK / (2 × (divider + 1)).
A larger peripheral might add transmit and receive FIFOs, programmable transfer length, multiple chip-select outputs, interrupts, and additional error reporting. Before using the design in a real system, verify the AHB-Lite behavior and test each SPI mode, divider setting, reset condition, and bus transfer sequence.
The design uses a single clock domain. If control logic or SPI signals are moved to another clock domain, appropriate clock-domain crossing measures may be required.
No comments:
Post a Comment