TO Be coming

Oct 26, 2026

Verilog code for SPI with AHB-Lite

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.

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

SignalDirectionDescription
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

SignalDirectionDescription
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.

OffsetNameAccessDescription
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

  1. Save the RTL as ahb_lite_spi_master.v.
  2. Save the testbench as tb_ahb_lite_spi_master.v.
  3. Compile both files with a Verilog simulator.
  4. Run the simulation and check for the PASS message.
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.

This example illustrates the main parts of a bus-connected SPI peripheral: an AHB-Lite register interface, a control and status register map, and a serial protocol engine.

No comments:

Post a Comment

SiteMap