Detecting Errors

Say what parity, a checksum and a CRC each catch, read the measured misses for single, double and burst errors, and tell an acknowledge from a check.

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

A motor drive accepted a corrupted speed command. Its link carried one parity bit per byte, and a burst of noise flipped two bits in the same byte. Parity counts the 1s, and two flips leave the count even.

What arrived: 0xa0 with a correct parity bit
startdata 0data 1data 2data 3data 4data 5data 6data 7even paritystop 0123456789101112131415161718192021222324252627rxsamplerx 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 22.6875rx sampled at 22.8125rx sampled at 22.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.
  • even parity: columns 20 through 21.
  • stop: columns 22 through 23.
  • 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, 22.688, 22.813, 22.938.

Generated and decoded back. The sender sent 0xA5; two bits were pulled low on the way. The receiver on the bench logged RX a0 ok: a well-formed frame with the wrong byte in it.

Four Checks and a Fault Injector

One Payload, One Noisy Wire, a Check at Each EndArrows show the frame moving left to right and the receiver's answer coming back.
One payload, one noisy wire, a check at each endArrows show the frame moving left to right and the receiver's answer coming back. component Payload four random bytes component Sender adds acheck parity, checksum orCRC-8 component Fault injector single, double orburst flips component Receiverrecomputes match: accept; elsereject component ACK or NACK NACK asks for theframe again frameframe, maybe flippedretry on NACK
  1. Add a check

    Parity adds one bit per byte so each byte holds an even number of 1s. The checksum adds the four bytes and keeps the low 8 bits. CRC-8 divides the payload by x^8 + x^2 + x + 1 (0x07) and sends the remainder.

  2. Hit it

    The injector flips bits anywhere in the frame, check bits included: one bit, two bits, or a burst whose first and last bits flip.

  3. Check it

    The receiver computes the check again. A frame that still matches is accepted, even when it carries an error.

  4. Answer it

    With ACK and NACK the receiver can ask for a rejected frame again. The answer is only as good as the check behind it.

Read the diagram as text
  • Payload: four random bytes
  • Sender adds a check: parity, checksum or CRC-8
  • Fault injector: single, double or burst flips
  • Receiver recomputes: match: accept; else reject
  • ACK or NACK: NACK asks for the frame again
  • Payload → Sender adds a check
  • Sender adds a check → Fault injector: frame
  • Fault injector → Receiver recomputes: frame, maybe flipped
  • Receiver recomputes → ACK or NACK
  • ACK or NACK → Sender adds a check: retry on NACK

Every scheme saw the same kinds of hit, 2000 frames per kind, from one fixed seed.

A check is extra bits computed from the payload. A flip that changes the payload but leaves the check consistent is a miss. The fault injector counts exactly that: frames hit once and still accepted.

What Each Check Missed

SchemeSingle flipDouble flipBurst of 3 to 8Burst of 9 to 16
Even parity per byte0434682385
8-bit checksum012507
CRC-8 (0x07)0007
ACK with no check2000200020002000

Frames accepted with an error in them, out of 2000 per cell. Measured by the example program below.

Parity misses every even number of flips inside one byte: 434 of the double flips landed in one byte. The checksum misses flips that cancel in the sum, such as one bit rising in one byte while the same bit falls in another.

CRC-8 caught every single flip, every double flip and every burst of 3 to 8 bits. Longer bursts can slip through: 7 of 2000 did. A wider CRC lowers that fraction; no check makes it zero.

The Flips Parity Cannot See

0xA5 sent, even parityBits pulled low on the wireThe receiver logs
cleannoneRX a5 ok
one flipbit 0RX a4 PE
two flipsbits 0 and 2RX a0 ok

Measured on the UART bench (P6), forcing the line low over the receiver's vote ticks of each chosen bit. PE is the parity error flag.

One flip is caught. Two flips give 0xa0, which still has an even count of 1s with the parity bit, so the frame is accepted. The same rule covers any even number of flips in one byte.

An ACK Is a Receipt

An I2C acknowledge says the receiver took the byte (UM10204 §3.1.6). It checks nothing. On the bench, noise lifts SDA during one data bit of a write.

Clean write of 0x3C to register 10

start; address7(7'h50, W);
write_byte(8'h10); write_byte(8'h3c);
stop;

Measured: I2C S 50 W A 10 A 3c A P, last ACK 1, register 10 holds 3c.

Noise lifts SDA during data bit 7

lift = 1;          // force SDA high for one bit
start; address7(7'h50, W);
write_byte(8'h10); write_byte(8'h3c);
stop;

Measured: I2C S 50 W A 10 A bc A P, last ACK 1, register 10 holds bc.

The target acknowledged bc and stored it. I3C adds an odd parity T-bit after each controller-written byte (MIPI I3C Basic §5.1.2.3.3), a check like UART parity.

ACK and NACK become error control only with a check behind them. CRC-8 plus retry delivered 9 wrong frames of 2000 after long bursts: the CRC's misses remain. The resends cost bus time: 1970, 2058, 2134, 1880 across the four columns.

Where This Shows Up

IdeaWhere it returns
Even and odd parity on a UART frameP6.2 the frame; P6.5 which flag means which fault
ACK and NACK on every byteP8.3 the ninth clock belongs to the receiver
A parity T-bit on controller writesP9.2 I3C SDR frames
Detecting and leaving an error stateP9.7 HDR exit and error recovery

SPI itself defines no check. The W25Q128JV flash used in module P7 lists no check field in any of its instruction tables (§8.1), so a read is trusted as clocked.

Try It

Each row is one scheme, each column one kind of hit, and each count is the frames accepted with an error. The last two rows are CRC-8 with NACK and retry: frames delivered wrong, then how many resends it took.

Change seed and run again: the zeros stay zeros, the other counts move a little. Then change the polynomial in crc8 from 8'h07 to 8'h01 and watch which columns stop being zero.

RUNIcarus Verilog · in your browser
Edit the code, then run it
Expected: parity | single 0 | double 434 | burst 3-8 682 | burst 9-16 385 | missed of 2000 checksum | single 0 | double 125 | burst 3-8 0 | burst 9-16 7 | missed of 2000 crc-8 | single 0 | double 0 | burst 3-8 0 | burst 9-16 7 | missed of 2000 ack, no check | single 2000 | double 2000 | burst 3-8 2000 | burst 9-16 2000 | missed of 2000 crc-8 + retry | single 0 | double 0 | burst 3-8 0 | burst 9-16 9 | delivered wrong of 2000 its resends | single 1970 | double 2058 | burst 3-8 2134 | burst 9-16 1880 | NACKs and resends

Check Yourself

Predict the outputUART bench, even parity at both ends. The sender sends 0xA5 and noise pulls data bits 0, 2 and 5 low. What does the receiver log?
`timescale 1ns/1ps
module tb;
  reg clk = 0, rst_n = 0, take = 0;
  always #0.5 clk = ~clk;                       // one tick = one receiver clock; a bit is 16 ticks
  wire rx, valid, frame_err, parity_err, overrun, break_seen, frame_done;
  wire [7:0] data;
  uart_tx  tx0(rx);
  uart_rx  rx0(clk, rst_n, rx, take, data, valid, frame_err, parity_err, overrun, break_seen, frame_done);
  uart_mon mon(rx);
  // Send one frame and pull the line low over the middle of every data bit set in \`mask\`.
  task automatic send_flipped(input [7:0] b, input [7:0] mask);
    fork
      send_byte(b);
      begin
        #16;                                         // the start bit
        for (int i = 0; i < 8; i++) begin
          #2; if (mask[i]) force rx = 0;             // over the receiver's vote ticks of data bit i
          #12; release rx;
          #2;
        end
      end
    join
  endtask
  `include "uart_tasks.svh"
  `include "p6_lab.svh"
  initial begin
    mon.trace = 1;
    #4 rst_n = 1; #4;
    take = 1; mon.trace = 0;
    tx0.parity = 1; rx0.parity = 1;                   // even parity at both ends
    send_flipped(8'ha5, 8'b0010_0101);                // bits 0, 2 and 5 pulled low
    #64; $finish;
  end
endmodule
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
Spot the bugEach option is the per-byte update of a CRC-8 with polynomial 0x07, most significant bit first. The bench checks that every single and double flip in a 40-bit frame is caught. Which update is wrong?
ConceptWhich error does an 8-bit checksum (the bytes added, carries dropped) let through, where CRC-8 catches it?
ConceptAn I2C target acknowledged every byte of a write, yet the register holds the wrong value. Which reading of the ACK explains it?
ConceptA link adds NACK and retry on top of CRC-8. Which statement about the measured result is right?

Remember

  • A check is extra bits computed from the payload; a miss is an error that leaves the check consistent.
  • Parity catches any odd number of flips in a byte and misses any even number.
  • An 8-bit checksum misses flips that cancel in the sum; CRC-8 (0x07) caught every single, double and short burst error in the run.
  • An ACK reports receipt and checks nothing; NACK and retry recover only what a check rejects.