Framing, Bit Order and Endianness

Name the three ways a serial link marks where a frame starts and ends, say which bit and which byte goes first on each link, and spot the reading that was assembled backwards.

Lesson
P5.5
Time
25 min
Runs on
Your browser
Check
5 questions

A battery monitor reported 29710 for a cell its sensor measured as 3700. The sensor sent its 16-bit reading high byte first. The driver, copied from a part that sent the low byte first, put the first byte in the low half. Every byte arrived intact; the word did not.

Two bytes of one reading: 0x0e, then 0x74
startdata 0data 1data 2data 3data 4data 5data 6data 7stopnext startnext data 0next data 1next data 2next data 3next data 4next data 5next data 6next data 7next stop 0123456789101112131415161718192021222324252627282930313233343536373839404142434445rxsamplerx sampled at 4.6875rx sampled at 4.8125rx sampled at 4.9375rx sampled at 6.6875rx sampled at 6.8125rx sampled at 6.9375rx sampled at 8.6875rx sampled at 8.8125rx sampled at 8.9375rx sampled at 10.6875rx sampled at 10.8125rx sampled at 10.9375rx sampled at 12.6875rx sampled at 12.8125rx sampled at 12.9375rx sampled at 14.6875rx sampled at 14.8125rx sampled at 14.9375rx sampled at 16.6875rx sampled at 16.8125rx sampled at 16.9375rx sampled at 18.6875rx sampled at 18.8125rx sampled at 18.9375rx sampled at 20.6875rx sampled at 20.8125rx sampled at 20.9375rx sampled at 24.6875rx sampled at 24.8125rx sampled at 24.9375rx sampled at 26.6875rx sampled at 26.8125rx sampled at 26.9375rx sampled at 28.6875rx sampled at 28.8125rx sampled at 28.9375rx sampled at 30.6875rx sampled at 30.8125rx sampled at 30.9375rx sampled at 32.6875rx sampled at 32.8125rx sampled at 32.9375rx sampled at 34.6875rx sampled at 34.8125rx sampled at 34.9375rx sampled at 36.6875rx sampled at 36.8125rx sampled at 36.9375rx sampled at 38.6875rx sampled at 38.8125rx sampled at 38.9375rx sampled at 40.6875rx sampled at 40.8125rx sampled at 40.9375
Read phases and sample points (transmitter half bit)
  • start: columns 2 through 3.
  • data 0: columns 4 through 5.
  • data 1: columns 6 through 7.
  • data 2: columns 8 through 9.
  • data 3: columns 10 through 11.
  • data 4: columns 12 through 13.
  • data 5: columns 14 through 15.
  • data 6: columns 16 through 17.
  • data 7: columns 18 through 19.
  • stop: columns 20 through 21.
  • next start: columns 22 through 23.
  • next data 0: columns 24 through 25.
  • next data 1: columns 26 through 27.
  • next data 2: columns 28 through 29.
  • next data 3: columns 30 through 31.
  • next data 4: columns 32 through 33.
  • next data 5: columns 34 through 35.
  • next data 6: columns 36 through 37.
  • next data 7: columns 38 through 39.
  • next stop: columns 40 through 41.
  • rx sampled at 4.688, 4.813, 4.938, 6.688, 6.813, 6.938, 8.688, 8.813, 8.938, 10.688, 10.813, 10.938, 12.688, 12.813, 12.938, 14.688, 14.813, 14.938, 16.688, 16.813, 16.938, 18.688, 18.813, 18.938, 20.688, 20.813, 20.938, 24.688, 24.813, 24.938, 26.688, 26.813, 26.938, 28.688, 28.813, 28.938, 30.688, 30.813, 30.938, 32.688, 32.813, 32.938, 34.688, 34.813, 34.938, 36.688, 36.813, 36.938, 38.688, 38.813, 38.938, 40.688, 40.813, 40.938.

Generated and decoded back: the two frames on the wire, 0x0e then 0x74. Nothing in either frame says which one is the high byte. The bench run below measures the misread: reading 3700 sent big-endian: bytes 0e 74, little-endian driver reads 29710.

Three Ways to Mark a Frame

How a Receiver Finds the FrameArrows show which marker each link uses. The marker must be something a data bit can never look like.
How a receiver finds the frameArrows show which marker each link uses. The marker must be something a data bit can never look like. component Transmitter one byte to send component Start and stopbits on the data line itself component A select line a wire of its own component START and STOP an edge data nevermakes component Receiver knows bit 0 of theframe UARTSPI, QSPII2C, I3C
  1. Start and stop bits

    UART has no clock. The line idles at 1, a 0 start bit opens the frame, and a 1 stop bit guarantees the next start is a falling edge.

  2. A select line

    SPI spends a pin: chip select low is the frame. Data bits can be anything, because the frame marker is on another wire.

  3. START and STOP conditions

    I2C and I3C change SDA only while SCL is low. An SDA edge while SCL is high is reserved: falling is START, rising is STOP.

Read the diagram as text
  • Transmitter: one byte to send
  • Start and stop bits: on the data line itself
  • A select line: a wire of its own
  • START and STOP: an edge data never makes
  • Receiver: knows bit 0 of the frame
  • Transmitter → Start and stop bits: UART
  • Transmitter → A select line: SPI, QSPI
  • Transmitter → START and STOP: I2C, I3C
  • Start and stop bits → Receiver
  • A select line → Receiver
  • START and STOP → Receiver

Three answers to one question: where does bit 0 of the frame begin?

0x41 framed by start and stop bits
startdata 0data 1data 2data 3data 4data 5data 6data 7stop 012345678910111213141516171819202122232425rxsamplerx sampled at 4.6875rx sampled at 4.8125rx sampled at 4.9375rx sampled at 6.6875rx sampled at 6.8125rx sampled at 6.9375rx sampled at 8.6875rx sampled at 8.8125rx sampled at 8.9375rx sampled at 10.6875rx sampled at 10.8125rx sampled at 10.9375rx sampled at 12.6875rx sampled at 12.8125rx sampled at 12.9375rx sampled at 14.6875rx sampled at 14.8125rx sampled at 14.9375rx sampled at 16.6875rx sampled at 16.8125rx sampled at 16.9375rx sampled at 18.6875rx sampled at 18.8125rx sampled at 18.9375rx sampled at 20.6875rx sampled at 20.8125rx sampled at 20.9375
Read phases and sample points (transmitter half bit)
  • start: columns 2 through 3.
  • data 0: columns 4 through 5.
  • data 1: columns 6 through 7.
  • data 2: columns 8 through 9.
  • data 3: columns 10 through 11.
  • data 4: columns 12 through 13.
  • data 5: columns 14 through 15.
  • data 6: columns 16 through 17.
  • data 7: columns 18 through 19.
  • stop: columns 20 through 21.
  • rx sampled at 4.688, 4.813, 4.938, 6.688, 6.813, 6.938, 8.688, 8.813, 8.938, 10.688, 10.813, 10.938, 12.688, 12.813, 12.938, 14.688, 14.813, 14.938, 16.688, 16.813, 16.938, 18.688, 18.813, 18.938, 20.688, 20.813, 20.938.

Generated and decoded back. Start, eight data bits, stop: 10 bit-times. The receiver only knows the frame because the start bit is a falling edge from idle (lesson P6.2).

0x41 framed by chip select
mode 0 012345678910111213141516171819CS_nCLKMOSIMISOMOSI sampled at 3MOSI sampled at 5MOSI sampled at 7MOSI sampled at 9MOSI sampled at 11MOSI sampled at 13MOSI sampled at 15MOSI sampled at 17MISO sampled at 3MISO sampled at 5MISO sampled at 7MISO sampled at 9MISO sampled at 11MISO sampled at 13MISO sampled at 15MISO sampled at 17
Read phases and sample points (half clock)
  • mode 0: columns 1 through 18.
  • MOSI sampled at 3, 5, 7, 9, 11, 13, 15, 17.
  • MISO sampled at 3, 5, 7, 9, 11, 13, 15, 17.

Chip select falls, 8 clocks move the byte, chip select rises. The data lines carry nothing but data (lesson P7.2).

0x41 framed by START and STOP
START0xa0 + ACK0x41 + ACKSTOP 012345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879SCLSDActrl_lowtgt_lowSAAPSDA sampled at 6SDA sampled at 10SDA sampled at 14SDA sampled at 18SDA sampled at 22SDA sampled at 26SDA sampled at 30SDA sampled at 34SDA sampled at 38SDA sampled at 42SDA sampled at 46SDA sampled at 50SDA sampled at 54SDA sampled at 58SDA sampled at 62SDA sampled at 66SDA sampled at 70SDA sampled at 74
Read phases and sample points (quarter bit)
  • START: columns 2 through 3.
  • 0xa0 + ACK: columns 4 through 39.
  • 0x41 + ACK: columns 40 through 75.
  • STOP: columns 76 through 79.
  • SDA sampled at 6, 10, 14, 18, 22, 26, 30, 34, 38, 42, 46, 50, 54, 58, 62, 66, 70, 74.

START is SDA falling while SCL is high, STOP is SDA rising while SCL is high. Every data bit changes SDA only while SCL is low (lesson P8.2).

Each marker can be broken. The library benches below each break one, and the receiver's view of the frame goes with it.

LinkThe marker, brokenMeasured
UARTnormal stop bits, then a stop bit cut to a quarter with the next frame right behindRX 41 ok, RX 42 ok; then RX 41 FE, RX e8 ok
SPIchip select rises one bit early while writing 0x5A to register 2, which holds 0x41register 2 reads 41: the write was dropped
I2Cthe controller also moves SDA while SCL is high, in one write of two bytesone write: 1 START, 1 STOP on the wire before; one write: 9 START, 7 STOP on the wire after

RX lines are the UART receiver; the I2C counts are the bus monitor's. A broken marker never looks like a broken byte: the receiver frames a different message. The flash in module P7 has the same SPI rule: a write that ends off a byte boundary is ignored (W25Q128JV §8).

Bit Order Inside a Byte

Once the frame is found, the receiver must know which bit comes first. SPI, QSPI flash, I2C and I3C send the most significant bit first. UART sends bit 0 first. Mixing the two mirrors the byte and raises no error.

Both ends most significant bit first

write register 2 = 8'h41;
read register 2;

Measured: MSB first both ends: register holds 41, controller reads 41.

p.lsb_first = 1: the peripheral sends bit 0 first

p.lsb_first = 1;
write register 2 = 8'h41;
read register 2;

Measured: peripheral LSB first: register holds 41, controller reads 82. Its ID byte still passes: peripheral LSB first: ID register 3c, controller reads 3c.

The register holds the right byte; only the shift order is wrong. 0x3C reads the same in both orders, so a test that only reads an ID register passes. Use a byte that differs from its mirror, such as 0x41, which arrives as 0x82.

Word Order Across Bytes

A wider value crosses a byte link one byte at a time. Big-endian sends the most significant byte first; little-endian sends the least significant byte first. The wire does not say which one the sender used.

One Word, Two ReadingsArrows show the bytes travelling from the sender to two receivers that assemble them differently.
One word, two readingsArrows show the bytes travelling from the sender to two receivers that assemble them differently. component Word 12345678 sent big-endian component Wire: 12 34 5678 four UART frames component Big-endian:12345678 first byte is the top component Little-endian:78563412 first byte is thebottom top byte firstmatchesreversed
Read the diagram as text
  • Word 12345678: sent big-endian
  • Wire: 12 34 56 78: four UART frames
  • Big-endian: 12345678: first byte is the top
  • Little-endian: 78563412: first byte is the bottom
  • Word 12345678 → Wire: 12 34 56 78: top byte first
  • Wire: 12 34 56 78 → Big-endian: 12345678: matches
  • Wire: 12 34 56 78 → Little-endian: 78563412: reversed

Values from the bench run: the same four bytes read as 12345678 or 78563412.

Each device's document states its order, and the driver must follow it. A flash read sends its 24-bit address top byte first (W25Q128JV §8.1.2, Table 1). I3C sends its 16-bit maximum write length top byte first (MIPI I3C Basic §5.1.9.3.5), and a target's 48-bit provisional ID most significant bit first (§5.1.4.2).

On-chip buses place bytes little-endian across lanes, a separate question covered in lesson P1.8. A serial link adds time order on top: which byte goes first.

Where This Shows Up

IdeaWhere it returns
Start and stop bits, bit 0 firstP6.2 the UART frame; P6.3 finding the bits from the start edge
Chip select as the frame, most significant bit firstP7.2 SPI modes; P7.3 the whole-byte rule for writes; P7.6 flash commands and addresses
START, STOP and the high-SCL ruleP8.2 START and STOP; P8.3 bytes and the ninth clock
The same conditions with push-pull dataP9.2 I3C SDR frames
Bytes placed in lanes on a parallel busP1.8 byte lanes and alignment

Each later module answers the same three questions for one link: the marker, the bit order and the byte order.

Try It

send_word puts a word on the UART bench one byte at a time, top byte first when big_endian is 1. The receiver hands each byte over, and the program assembles the same four bytes both ways.

Change the last argument of the first send_word to 0, so the sender goes little-endian. Now the little-endian assembly is right and the big-endian one is reversed.

RUNIcarus Verilog · in your browser
Edit the code, then run it
Expected: sent 12345678 big-endian, bytes on the wire: 12 34 56 78 assembled big-endian: 12345678 assembled little-endian: 78563412 reading 3700 sent big-endian: bytes 0e 74, little-endian driver reads 29710
Read included file: uart_tx.sv
`timescale 1ns/1ps
module uart_tx(output reg tx=1);
  real error=0.0; int bits=8, parity=0; real stop_bits=1.0;
  bit lsb_first=1, parity_includes_start=0, parity_includes_stop=0, parity_wrong_sense=0, short_stop=0, no_idle_between=0;
  task automatic bit_time(input bit v); tx=v; #(16.0*(1.0+error)); endtask
  task automatic send_byte(input byte unsigned value);
    bit p; p=(parity==2);
    bit_time(0);
    for(int b=0;b<bits;b++) begin bit_time(value[lsb_first?b:bits-1-b]); p=p^value[b]; end
    if(parity!=0) bit_time(p ^ parity_includes_stop ^ parity_wrong_sense);
    tx=1; #(16.0*(1.0+error)*(short_stop?0.25:stop_bits));
    // no_idle_between is a legal back-to-back frame control, not a fault.
    if(!no_idle_between) #(16.0*(1.0+error));
  endtask
  task automatic send_break(); tx=0; #(200*16); tx=1; #32; endtask
  task automatic set_error(input real e); error=e; endtask
endmodule
Read included file: uart_rx.sv
`timescale 1ns/1ps
module uart_rx(input wire clk, rst_n, rx, take,
  output reg [7:0] data=0, output reg valid=0, frame_err=0, parity_err=0, overrun=0, break_seen=0, frame_done=0);
  int bits=8, parity=0; real stop_bits=1.0;
  bit trace=0;
  bit parity_includes_start=0; // XOR of the zero-valued START is an equivalent control.
  bit sample_at_edge=0, qualify_1_tick=0, no_vote=0, no_resync=0, rearm_on_low=0, stop_as_data=0, no_overrun_flag=0, parity_includes_stop=0;
  int state=0, low_run=0, left_ticks=0, index=0, votes=0, frames=0, ticks=0, next_data_tick=0;
  bit armed=1, sampled=0, p=0, pe=0, pending_zero_fe=0; reg [7:0] shift=0;
  always @(posedge clk) begin
    frame_done=0;
    if(!rst_n) begin state=0;low_run=0;left_ticks=0;index=0;votes=0;valid=0;data=0;frame_err=0;parity_err=0;overrun=0;break_seen=0;armed=1;frames=0;ticks=0;next_data_tick=0;pending_zero_fe=0; end
    else begin
      ticks++;
      if(take)valid=0;
      low_run=rx?0:low_run+1;
      if(rx)break_seen=0;
      if(low_run>=176&&!rearm_on_low)begin break_seen=1;state=4;pending_zero_fe=0;end
      if(state==4)begin if(rx)begin
        if(pending_zero_fe)begin
          if(valid&&!no_overrun_flag)overrun=1;
          data=0;valid=1;frame_err=1;parity_err=pe;frames++;frame_done=1;pending_zero_fe=0;
        end
        state=0;armed=1;
      end end
      else if(state==0)begin
        if(rx)begin armed=1;end
        else if(armed&&low_run>=(qualify_1_tick?1:8))begin
          state=1;left_ticks=sample_at_edge?8:16;index=0;shift=0;p=(parity==2)^parity_includes_stop;votes=0;pe=0;
          // Fault: later frames keep the first frame's free-running schedule.
          // At nominal back-to-back pacing it can work; a new START must normally
          // reset this timing so baud error cannot accumulate across frames.
          if(no_resync&&frames>0)begin
            while(next_data_tick<=ticks)next_data_tick+=16*(1+bits+(parity!=0))+$rtoi(16*stop_bits);
            left_ticks=next_data_tick-ticks;
          end else next_data_tick=ticks+left_ticks;
          next_data_tick+=16*(1+bits+(parity!=0))+$rtoi(16*stop_bits);
        end
      end else begin
        if(left_ticks<=3)votes=votes+(rx?1:0);
        left_ticks=left_ticks-1;
        if(left_ticks==0)begin
          sampled=no_vote?rx:(votes>=2);votes=0;left_ticks=16;
          if(trace)$display("UART SAMPLE t=%0t state=%0d bit=%0d value=%0d",$time,state,index,sampled);
          if(state==1)begin shift[index]=sampled;p=p^sampled;index=index+1;if(index==bits)state=parity==0?3:2;end
          else if(state==2)begin pe=(sampled!=p);state=3;end
          else begin
            if(shift==0&&!sampled&&!rearm_on_low)pending_zero_fe=1;
            else begin
              if(valid&&!no_overrun_flag)overrun=1;
              data=stop_as_data?{sampled,shift[7:1]}:shift;
              frame_err=!sampled;parity_err=pe;valid=1;frames=frames+1;frame_done=1;
            end
            state=(sampled||rearm_on_low)?0:4;armed=1;
          end
        end
      end
    end
  end
endmodule
Read included file: uart_flow.sv
`timescale 1ns/1ps
module uart_flow(input wire clk,rst_n,push,pop,cts_n,input wire [7:0] din,
 output wire rts_n,output wire send,output wire [7:0] dout,output reg overrun=0);
  reg [7:0] fifo[0:15]; int head=0,count=0,trigger=12;
  bit cts_ignored=0,trigger_off_by_one=0;
  assign rts_n=count>=(trigger+(trigger_off_by_one?1:0));
  assign send=count>0&&(!cts_n||cts_ignored);
  assign dout=count>0?fifo[head]:8'h00;
  always @(posedge clk)begin
    if(!rst_n)begin head=0;count=0;overrun=0;end
    else begin
      if(pop&&send)begin head=(head+1)%16;count=count-1;end
      if(push)begin if(count==16)overrun=1;else begin fifo[(head+count)%16]=din;count=count+1;end end
    end
  end
endmodule
Read included file: uart_checker.sv
`timescale 1ns/1ps
module uart_checker(input wire clk,rx,valid,frame_err,parity_err,input wire [7:0] data,input wire frame_done);
  int broken=0,received=0;
  task automatic expect_byte(input byte unsigned wanted,input bit fe=0,pe=0);
    if(!valid||data!==wanted||frame_err!==fe||parity_err!==pe)begin broken++;$display("UART CHECK got=%02h valid=%b FE=%b PE=%b expected=%02h FE=%b PE=%b",data,valid,frame_err,parity_err,wanted,fe,pe);end
  endtask
  always @(negedge clk)if(frame_done)received++;
endmodule
Read included file: uart_mon.sv
`timescale 1ns/1ps
// Independent wire decoder: completion is reported after the stop sample, never at START.
module uart_mon(input wire rx);
 int bits=8,parity=0; real bit_period=16.0; int frames=0; bit trace=0;
 reg [7:0] value; bit p,pe,fe;realtime began;
 initial forever begin
  @(negedge rx);began=$realtime; #(bit_period/2); if(rx==0)begin
   value=0;p=(parity==2);pe=0;
   for(int i=0;i<bits;i++)begin #(bit_period);value[i]=rx;p=p^rx;end
   if(parity!=0)begin #(bit_period);pe=(rx!=p);end
   #(bit_period);fe=!rx;
   if(value==0&&fe)begin
    while(rx==0&&$realtime-began<11*bit_period)#(bit_period/16);
    if(rx==0)begin wait(rx==1);if(trace)$display("UART -- BREAK");end
    else begin frames++;if(trace)$display("UART 00 FE");end
   end
   else begin frames++;if(trace)$display("UART %02h %s",value,fe?"FE":pe?"PE":"ok");end
  end
 end
endmodule
Read included file: uart_tasks.svh
task automatic send_byte(input byte unsigned b); tx0.send_byte(b); endtask
task automatic send_break(); tx0.send_break(); endtask
task automatic set_error(input real e); tx0.set_error(e); endtask
task automatic consume(); @(negedge clk);take=1;@(negedge clk);take=0;endtask
Read included file: p6_lab.svh
// p6_lab.svh: lesson helpers. The wire monitor follows the transmitter's format and rate, so its
// UART line shows what was sent; the RX line shows what the receiver under test delivered.
bit rx_log = 1, sample_log = 0;
int ticks = 0, edge_tick = 0, last_left = 0, last_state = 0, last_index = 0;
always @(posedge clk) ticks++;
always @(negedge rx) if (rx0.state == 0) edge_tick = ticks;
always @(tx0.parity) mon.parity = tx0.parity;
always @(tx0.bits) mon.bits = tx0.bits;
always @(tx0.error) mon.bit_period = 16.0 * (1.0 + tx0.error);
always @(negedge clk) begin
  if (sample_log && last_left == 1 && rx0.left_ticks == 16) begin
    if (last_state == 1) $display("tick %0d: data %0d sample reads %b", ticks - edge_tick, last_index, rx0.sampled);
    else if (last_state == 2) $display("tick %0d: parity sample reads %b", ticks - edge_tick, rx0.sampled);
    else $display("tick %0d: stop sample reads %b", ticks - edge_tick, rx0.sampled);
  end
  last_left = rx0.left_ticks; last_state = rx0.state; last_index = rx0.index;
end
// The RX line is printed one bit-time after the receiver's stop sample, after the monitor's line.
always @(rx0.frames) if (rx_log) begin
  #16;
  if (overrun) $display("RX %02h %s overrun", data, frame_err ? "FE" : parity_err ? "PE" : "ok");
  else $display("RX %02h %s", data, frame_err ? "FE" : parity_err ? "PE" : "ok");
end
always @(posedge break_seen) if (rx_log) begin wait (rx == 1); #16 $display("RX break"); end
// A low pulse of the given number of ticks on the idle line.
task automatic glitch(input int width); tx0.tx = 0; #(width); tx0.tx = 1; endtask
// Send a byte and pull the line low for `width` ticks ending at receiver tick `at` after its start edge
// (tick n is the n-th clock edge after the edge), then give the line back its level.
int hit_at = 0, hit_width = 0; reg hit_level = 1; event hit_go;
always @(hit_go) begin
  #(hit_at - 0.5 - hit_width + 0.25); hit_level = tx0.tx; tx0.tx = 0; #(hit_width); tx0.tx = hit_level;
end
task automatic send_hit(input byte unsigned b, input int at, input int width);
  hit_at = at; hit_width = width; -> hit_go; send_byte(b);
endtask

Check Yourself

Predict the outputThe SPI peripheral sends bit 0 first (p.lsb_first = 1). The controller writes 0x0D to register 2 and reads it back, most significant bit first as usual. What does it print?
`timescale 1ns/1ps
module tb;reg cs_n=1,sclk=0,mosi=0;tri miso;wire oe;reg [7:0] received;
`include "spi_ctrl_tasks.svh"
spi_periph #(.CPOL(0),.CPHA(0)) p(cs_n,sclk,mosi,miso,oe);
spi_checker ck(cs_n,sclk,mosi,miso,oe,cpol[0],cpha[0]);spi_mon mon(cs_n,sclk,mosi,miso,cpol[0],cpha[0]);
initial begin cpol=0;cpha=0; #10;p.lsb_first = 1;                 // the peripheral sends bit 0 first
cs_lo(); xfer(8'h82, received); xfer(8'h0d, received); cs_hi();
cs_lo(); xfer(8'h02, received); xfer(8'h00, received); cs_hi();
$display("controller reads %h", received); $finish;end
initial begin #100000;$fatal(1,"SPI watchdog");end
endmodule
Read included file: spi_periph.sv
`timescale 1ns/1ps
// A teaching register device, not the Winbond register map. Writes commit at CS
// rising only when the entire command/data frame ended on a byte boundary.
module spi_periph #(parameter CPOL=0,CPHA=0)(input wire cs_n,sclk,mosi,inout wire miso,output wire oe);
 int cpol=CPOL,cpha=CPHA,wrong_mode=0;
 bit drive_when_deselected=0,late_miso=0,lsb_first=0,loopback=0,chain=0;
 realtime t_clqv=1.0;
 reg [7:0] tx_word=8'h3c,last_rx=0,ctrl=0,data_reg=0,shift=0,response=8'h3c,pending=0;
 reg out=0;int count=0,words=0,frames=0;reg [7:0] command=0;
 wire p=cpol[0]^((wrong_mode&2)!=0),h=cpha[0]^((wrong_mode&1)!=0);
 assign oe=!cs_n||drive_when_deselected;assign miso=oe?out:1'bz;
 function automatic bit txbit(input int i);txbit=chain?shift[7]:response[lsb_first?i:7-i];endfunction
 function automatic [7:0] reg_value(input reg [6:0] addr);
  case(addr)0:reg_value=8'h3c;1:reg_value=ctrl;2:reg_value=data_reg;3:reg_value=frames[7:0];default:reg_value=0;endcase
 endfunction
 always @(negedge cs_n)begin
  count=0;shift=chain?tx_word:0;words=0;command=0;response=tx_word;
  // Preload bit 7 without advancing count. A same-edge 0/3 (or 2/1)
  // pair may sample before the peripheral sees its first launch edge.
  // CPHA=1's leading launch repeats this bit; only sampling advances count.
  out<=#(t_clqv)txbit(0);
 end
 always @(posedge cs_n)begin
  if(words>0)begin
   if(!chain&&!loopback&&count==0&&words>=2&&command[7])begin
    case(command[6:0])1:ctrl=pending;2:data_reg=pending;default:;endcase
   end
   frames++;if(chain)tx_word=shift;
  end
 end
 always @(sclk)if(!cs_n)begin
  if(sclk==(h?p:!p))begin
   shift={shift[6:0],mosi};count++;
   if(count==8)begin
    last_rx=shift;count=0;
    if(!chain)begin
     if(loopback)response=shift;
     else if(words==0)begin command=shift;response=shift[7]?0:reg_value(shift[6:0]);end
     else pending=shift;
    end
    words++;
   end
   if(late_miso)out<=#(t_clqv)txbit(count);
  end else if(!late_miso)out<=#(t_clqv)txbit(count);
 end
endmodule
Read included file: spi_ctrl_tasks.svh
int cpol=0,cpha=0;realtime half_period=5.0,t_launch=1.0;bit cs_too_early=0,sample_late=0,cs_between_bytes=0;int clocks=0;
task automatic set_mode(input int p,input int h);cpol=p;cpha=h;sclk=p;endtask
task automatic set_period(input real ns);if(ns<=0)$fatal(1,"SPI period must be positive");half_period=ns/2;endtask
task automatic cs_lo(input int id=0);sclk=cpol;#(half_period);cs_n=0;#(half_period);endtask
task automatic cs_hi(input int id=0);#(half_period);cs_n=1;#(half_period);endtask
task automatic xfer(input reg [7:0] value,output reg [7:0] received);
 received=0;
 for(int b=7;b>=0;b--)begin
  if(t_launch>=half_period)$fatal(1,"launch delay must fit half-period");
  if(!cpha)begin
   #(t_launch);mosi=value[b];#(half_period-t_launch);
   if(!sample_late)received[b]=miso;sclk=!cpol;clocks++;
   #(half_period);if(sample_late)received[b]=miso;sclk=cpol;
  end else begin
   #(half_period);sclk=!cpol;clocks++;#(t_launch);mosi=value[b];#(half_period-t_launch);
   if(!sample_late)received[b]=miso;sclk=cpol;
   if(sample_late)begin #(half_period);received[b]=miso;end
  end
  if(cs_too_early&&b==1)cs_n=1;
 end
 if(cs_between_bytes)begin cs_hi();cs_lo();end
endtask
Read included file: spi_checker.sv
`timescale 1ns/1ps
// Mode-generic teaching checker. tCLQV margin uses the configured receiver setup;
// positive margin is a bound, not a guarantee that a particular byte is correct.
module spi_checker(input wire cs_n,sclk,mosi,miso,oe,input wire cpol,cpha);
 int broken=0,samples=0;realtime changed=0,mosi_changed=0,last_sample=-100,last_launch=-100,last_edge=-100,selected=-100,deselected=-100;
 bit enabled=0,sample_late=0;realtime tdvch=1.0,tchdx=2.0,tclqv=1.0,receiver_setup=0.0;
 realtime tslch=3.0,tchsh=3.0,tshch=3.0,tchsl=3.0,tshsl=10.0;
 task automatic bad(input string rule,input real margin);broken++;$display("SPI CHECK %s margin %0.3f ns",rule,margin);endtask
 // Late sampling consumes the old bit just before the next launch. Changes
 // after that launch are legal; its setup bound is checked by tclqv below.
 always @(miso)begin changed=$realtime;
  if(enabled&&!sample_late&&!cs_n&&samples>0&&sclk==(cpha?cpol:!cpol))bad("same_edge",$realtime-last_sample);
 end
 always @(mosi)begin
  mosi_changed=$realtime;
  if(enabled&&!cs_n&&$realtime-last_sample<tchdx)bad("tchdx",$realtime-last_sample-tchdx);
 end
 task automatic timing_margin(input real half_period,input real tclqv_max,input real setup_ns);
  real margin; margin=(sample_late?2:1)*half_period-tclqv_max-setup_ns;
  $display("SPI MARGIN half=%0.3f tCLQV=%0.3f setup=%0.3f margin=%0.3f ns",half_period,tclqv_max,setup_ns,margin);
  if(margin<0)bad("tclqv",margin);
 endtask
 always @(negedge cs_n)begin
  if(sclk!==cpol)bad("cs_clk_idle",0);
  if(enabled&&$realtime-deselected<tshsl)bad("tshsl",$realtime-deselected-tshsl);
  if(enabled&&$realtime-last_edge<tchsl)bad("tchsl",$realtime-last_edge-tchsl);
  selected=$realtime;samples=0;enabled=1;
 end
 always @(sclk)begin
  if(enabled)begin
   if(!cs_n&&$realtime-selected<tslch)bad("tslch",$realtime-selected-tslch);
   if(cs_n&&$realtime-deselected<tshch)bad("tshch",$realtime-deselected-tshch);
  end
  last_edge=$realtime;
  if(!cs_n)begin
   if(sclk==(cpha?cpol:!cpol))begin
    samples++;last_sample=$realtime;
    if($realtime-mosi_changed<tdvch)bad("tdvch",$realtime-mosi_changed-tdvch);
    if(!sample_late&&last_launch>=selected&&$realtime-last_launch<tclqv+receiver_setup)bad("tclqv",$realtime-last_launch-tclqv-receiver_setup);
   end else begin
    if(sample_late&&last_launch>=selected&&$realtime-last_launch<tclqv+receiver_setup)bad("tclqv",$realtime-last_launch-tclqv-receiver_setup);
    last_launch=$realtime;
   end
  end
 end
 always @(posedge cs_n)if(enabled)begin
  if(sclk!==cpol)bad("cs_clk_idle",0);
  if($realtime-last_edge<tchsh)bad("tchsh",$realtime-last_edge-tchsh);
  if(samples%8!=0)bad("byte_boundary",0);
  deselected=$realtime;#1;if(oe)bad("miso_released",0);
 end
endmodule
Read included file: spi_mon.sv
`timescale 1ns/1ps
module spi_mon(input wire cs_n,sclk,mosi,miso,input wire cpol,cpha);
 reg [7:0] a=0,b=0;int count=0,frames=0;bit trace=0,sample_wrong_edge=0;
 always @(negedge cs_n)count=0;
 always @(sclk)if(!cs_n&&sclk==((cpha^sample_wrong_edge)?cpol:!cpol))begin
  a={a[6:0],mosi};b={b[6:0],miso};count++;
  if(count==8)begin frames++;if(trace)$display("SPI m%0d %02h/%02h",2*cpol+cpha,a,b);count=0;end
 end
endmodule
Spot the bugA 32-bit word arrives least significant byte first. Each option runs once per byte b, with i counting 0 to 3 and word starting at 0. Which one assembles the word wrongly?
ConceptWhy can an SPI data byte never be mistaken for the end of a frame, while a UART receiver can lose its place after a bad stop bit?
Read the waveformOne SPI byte in mode 0 (generated and decoded back). Which byte does MOSI carry?
SPI on the wires
mode 0 012345678910111213141516171819CS_nCLKMOSIMISOMOSI sampled at 3MOSI sampled at 5MOSI sampled at 7MOSI sampled at 9MOSI sampled at 11MOSI sampled at 13MOSI sampled at 15MOSI sampled at 17MISO sampled at 3MISO sampled at 5MISO sampled at 7MISO sampled at 9MISO sampled at 11MISO sampled at 13MISO sampled at 15MISO sampled at 17
Read phases and sample points (half clock)
  • mode 0: columns 1 through 18.
  • MOSI sampled at 3, 5, 7, 9, 11, 13, 15, 17.
  • MISO sampled at 3, 5, 7, 9, 11, 13, 15, 17.

Generated and decoded back: one SPI byte, mode 0, one column per half clock.

ConceptA flash read (03h) of address 0x001014 sends the command and then three address bytes. Which address byte goes on the wire first?

Remember

  • A frame needs a marker that data can never imitate: start and stop bits (UART), a select line (SPI), or START and STOP conditions (I2C, I3C).
  • A broken marker frames a different message: a misread next byte, a dropped write, or extra STARTs and STOPs.
  • SPI, flash, I2C and I3C send the most significant bit first; UART sends bit 0 first. Test bit order with a byte that differs from its mirror.
  • Word order is a convention of each device, written in its document: the same bytes assemble into different words big-endian and little-endian.