AHB-Lite SPI Master in Verilog: RTL, Registers and Testbench
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.
Scope: This example implements a single-byte, MSB-first
SPI master with one active-low chip-select output. It uses zero AHB
wait states and returns an OKAY response. Review and verify the design
against your own integration requirements before using it in a system.
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 PASS message.
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.
From RTL to Silicon. One Tech Hub for Digital Design Engineers.
Deep, practical guides on RTL design, Verilog, CDC, static timing analysis, synthesis,
bus protocols and verification - built for ASIC and FPGA engineers.