Project 3, Part 3: The Mode-Configurable Master, Verified
The last two articles built the shift register and chip-select framing. This one builds the controller where CPOL and CPHA are honoured, generating SCLK with the right idle polarity and triggering the shift register on the right edge.
Generating SCLK according to CPOL
logic sclk_reg;
assign sclk = sclk_reg;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sclk_reg <= cpol; // idle at whatever polarity CPOL specifies: Part 1's definition
end else if (spi_state == SPI_XFER && sclk_toggle_tick) begin
sclk_reg <= ~sclk_reg;
end else if (spi_state != SPI_XFER) begin
sclk_reg <= cpol; // return to idle polarity between transactions
end
end
That one line, sclk_reg <= cpol, on reset and between transactions, is the whole CPOL implementation. The clock idles at whatever the configuration input says.
Triggering shift_en according to CPHA
CPHA decides which SCLK transition samples a bit, and because CPOL decides which physical transition is “leading”, the two bits interact:
logic leading_edge, trailing_edge;
// The leading edge is the FIRST transition away from idle; which physical
// edge that is depends on CPOL, exactly Part 1's mode table.
assign leading_edge = cpol ? (sclk_falling) : (sclk_rising);
assign trailing_edge = cpol ? (sclk_rising) : (sclk_falling);
// CPHA=0 samples on the leading edge; CPHA=1 samples on the trailing edge
assign shift_en = cpha ? trailing_edge : leading_edge;
This is Part 1’s table as live logic. Feed it CPOL=0, CPHA=0 and shift_en fires on sclk_rising (mode 0). Feed it CPOL=1, CPHA=0 and it fires on sclk_falling (mode 2), because with the clock idling high the leading edge is a falling edge, the reasoning from Part 1’s exercise. For CPHA=1, the data line must be set up on the leading edge so it is stable at the trailing edge; the shift register’s load handles the first bit and each subsequent shift places the next bit on MOSI half a clock before it is sampled.
The controller FSM
typedef enum logic [1:0] {SPI_IDLE, SPI_START, SPI_XFER, SPI_DONE} spi_state_t;
spi_state_t spi_state, spi_next_state;
logic [3:0] bit_count;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) spi_state <= SPI_IDLE;
else spi_state <= spi_next_state;
end
always_comb begin
spi_next_state = spi_state;
unique case (spi_state)
SPI_IDLE: if (start_xfer) spi_next_state = SPI_START;
SPI_START: if (cs_setup_done) spi_next_state = SPI_XFER;
SPI_XFER: if (bit_count == 4'd8 && sclk_toggle_tick) spi_next_state = SPI_DONE;
SPI_DONE: spi_next_state = SPI_IDLE;
default: spi_next_state = SPI_IDLE;
endcase
end
This is deliberately the same shape as every controller in this module. SPI_START plays the “settle before real activity” role that RX_START’s centre confirmation played for UART, and SPI_XFER’s bit-count exit is the same as the TX_DATA and RX_DATA counting from Project 2, counting clock edges instead of oversampled ticks.
Verifying all four modes, not one
A testbench that verifies this master only in mode 0 has verified a quarter of the claim “this master supports all four modes”. Each mode needs its own loopback check:
task automatic test_mode(bit cpol_val, bit cpha_val, bit [7:0] tx_byte);
cpol = cpol_val; cpha = cpha_val;
tx_data = tx_byte;
start_xfer = 1'b1;
@(posedge clk); start_xfer = 1'b0;
wait (spi_state == SPI_DONE);
@(posedge clk);
if (rx_data == tx_byte)
$display("PASS: Mode CPOL=%b CPHA=%b, sent %h, looped back %h", cpol_val, cpha_val, tx_byte, rx_data);
else
$display("FAIL: Mode CPOL=%b CPHA=%b, sent %h, looped back %h", cpol_val, cpha_val, tx_byte, rx_data);
wait (spi_state == SPI_IDLE);
endtask
initial begin
rst_n = 0; start_xfer = 0;
repeat (3) @(posedge clk); rst_n = 1;
// MISO is looped back to MOSI in the testbench, so whatever the master
// sends it also receives: the same idea as Project 2's serial_line,
// applied to SPI's full-duplex pair
test_mode(1'b0, 1'b0, 8'hA5); // Mode 0
test_mode(1'b0, 1'b1, 8'hA5); // Mode 1
test_mode(1'b1, 1'b0, 8'hA5); // Mode 2
test_mode(1'b1, 1'b1, 8'hA5); // Mode 3
$finish;
end
assign miso = mosi; // the loopback itself: MISO mirrors whatever MOSI drives
assign miso = mosi; does the job Project 2’s serial_line did, turning the master’s output into its own input so one self-contained testbench verifies full-duplex behaviour with no slave. Running all four modes with the same byte is deliberate: it isolates the mode handling as the thing being varied, rather than mixing a data-value bug with a mode bug in one run.
Testing every declared configuration independently is what a real verification plan requires before a configurable peripheral is considered verified. A master that works in mode 0 and was never tested in modes 1 to 3 has an unverified, and in practice frequently buggy, three-quarters of its stated functionality.
Practice Exercise
Explain why assign miso = mosi; is sufficient to test full-duplex behaviour, even though a real slave computes its own response rather than echoing what it received.
Solution
The goal is not to verify slave behaviour. It is to verify that the master’s shift register performs simultaneous shift-out and shift-in correctly (Part 2) and that the controller sequences the shifting according to the configured mode. Looping MISO to MOSI means whatever byte the master sends is, bit for bit and on the right edges, the byte it should receive back, so “does rx_data equal tx_data” is a sufficient check of the master’s own correctness, independent of any slave’s application-level response. Verifying against a real slave’s response is a separate test for when the master is integrated with a specific peripheral, not needed for the master’s protocol-level correctness in isolation.
The controller generates SCLK idling at the polarity CPOL specifies and triggers sampling on the edge CPHA and CPOL together designate, implementing Part 1’s table as configurable logic. Verifying that honestly means all four modes independently, using the loopback technique from Project 2 on SPI’s full-duplex pair. That closes Project 3. In the next article, Project 4 moves from a serial protocol to a parallel memory interface, an SRAM controller, where the discipline that matters shifts from clock edges and modes to address-to-data timing.