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.
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
- 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.
- 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.
- 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?
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).
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).
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.
| Link | The marker, broken | Measured |
|---|---|---|
| UART | normal stop bits, then a stop bit cut to a quarter with the next frame right behind | RX 41 ok, RX 42 ok; then RX 41 FE, RX e8 ok |
| SPI | chip select rises one bit early while writing 0x5A to register 2, which holds 0x41 | register 2 reads 41: the write was dropped |
| I2C | the controller also moves SDA while SCL is high, in one write of two bytes | one 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.
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
| Idea | Where it returns |
|---|---|
| Start and stop bits, bit 0 first | P6.2 the UART frame; P6.3 finding the bits from the start edge |
| Chip select as the frame, most significant bit first | P7.2 SPI modes; P7.3 the whole-byte rule for writes; P7.6 flash commands and addresses |
| START, STOP and the high-SCL rule | P8.2 START and STOP; P8.3 bytes and the ninth clock |
| The same conditions with push-pull data | P9.2 I3C SDR frames |
| Bytes placed in lanes on a parallel bus | P1.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.
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 29710Read 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
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.