UART · Module 9
Parity Errors and the Limits of Detection
Parity detects every odd-weight corruption and provably misses every even-weight one — shown by exhaustive enumeration. A check that passes says a relation holds, not that the data is correct.
Chapter 3.3 built parity and stated its guarantee. Chapter 6.4 implemented the receiver's check with the accumulator discipline that keeps it attached to the right frame. This chapter attacks it.
The question is not how parity works — that is one XOR reduction and a comparison. It is what a designer is entitled to conclude from the result, in both directions:
parity_error = 1— a specific relation over the protected bits does not hold.parity_error = 0— that relation holds. Not that the data is correct.
The gap between the second statement and "the data arrived intact" is the entire subject, and it is wider than it looks. This chapter closes it by enumeration rather than by assertion: every corruption of every weight, counted.
1. The Relation Being Tested
Chapter 3.3 fixed the convention, and the receiver applies exactly the same rule the transmitter did:
even parity: p = ^d the XOR reduction of the data bits
odd parity: p = ~(^d)The receiver assembles d from the data intervals, observes p in the parity interval, and tests:
observed p == expected(mode, assembled d) -> relation holdsAn equivalent and more revealing form is to reduce the whole protected set at once:
even parity: ^{d, p} == 0 the protected set has even weight
odd parity: ^{d, p} == 1 the protected set has odd weightParity is a one-bit statement about the weight of a nine-bit set. Reading it that way makes the limit immediate: a corruption that preserves the parity of the weight is invisible, whatever else it does to the data.
2. The RTL
// ---------------------------------------------------------------------------
// 9.2 — parity relation check. The rule is Chapter 3.3's, unchanged.
// ---------------------------------------------------------------------------
module uart_parity_check #(
parameter int unsigned DATA_W = 8
) (
input logic clk,
input logic rst_n,
input logic frame_start_i,
input logic check_en_i, // one cycle, at the parity sample
input logic parity_odd_i, // 0 = even, 1 = odd
input logic [DATA_W-1:0] data_i, // payload assembled so far
input logic parity_bit_i, // the received parity interval
output logic parity_error_o,
output logic parity_error_evt_o
);
// Expected parity under the configured mode. ^data_i is a reduction XOR:
// synthesis builds a balanced tree of two-input gates.
wire expected = parity_odd_i ? ~(^data_i) : (^data_i);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
parity_error_o <= 1'b0;
parity_error_evt_o <= 1'b0;
end else begin
parity_error_evt_o <= 1'b0;
if (frame_start_i) begin
parity_error_o <= 1'b0;
end else if (check_en_i) begin
parity_error_o <= (parity_bit_i != expected);
parity_error_evt_o <= (parity_bit_i != expected);
end
end
end
endmodule^data_i is a reduction XOR, not a bitwise operation between two operands. Synthesis builds a balanced tree of two-input XOR gates — three levels at DATA_W = 8 — and it is evaluated once, at the parity sample.
parity_odd_i selects the expectation, not the computation. The XOR tree is the same either way; the mode is one inverter on its output. That is why parity mode can be a runtime input at essentially no cost, which Chapter 6.1 §5 used as its example of a configuration input that selects a path through hardware that exists anyway.
The verdict/event split is Chapter 9.1 §2's, unchanged. parity_error_o is a level describing this frame, cleared at the next accepted start; parity_error_evt_o is a one-cycle pulse for the sticky aggregator of Chapter 9.6. The two chapters use the same pattern deliberately — every error source in this module produces the same pair of shapes, which is what lets 9.6 aggregate them uniformly.
3. The Same Check in Three Languages
The relation is one line of arithmetic, and each language spells the reduction differently while building the same balanced XOR tree.
//===========================================================================
// uart_parity_check_v — Synthesizable Verilog-2001
//
// The parity relation, unchanged from Chapter 3.3. ^data_i is a reduction
// XOR in Verilog-2001 exactly as in SystemVerilog; synthesis builds a
// balanced tree of two-input gates, depth ceil(log2(DATA_W)).
//
// parity_odd_i = 0 -> EVEN: the parity bit makes the total number of ones
// across data+parity even, so expected = ^data.
// parity_odd_i = 1 -> ODD : expected = ~^data.
//===========================================================================
module uart_parity_check_v #(
parameter DATA_W = 8
) (
input wire clk,
input wire rst_n,
input wire frame_start_i,
input wire check_en_i,
input wire parity_odd_i,
input wire [DATA_W-1:0] data_i,
input wire parity_bit_i,
output reg parity_error_o,
output reg parity_error_evt_o
);
wire expected = parity_odd_i ? ~(^data_i) : (^data_i);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
parity_error_o <= 1'b0;
parity_error_evt_o <= 1'b0;
end else begin
parity_error_evt_o <= 1'b0;
if (frame_start_i) begin
parity_error_o <= 1'b0;
end else if (check_en_i) begin
parity_error_o <= (parity_bit_i != expected);
parity_error_evt_o <= (parity_bit_i != expected);
end
end
end
endmodule--==========================================================================
-- uart_parity_check -- Synthesizable VHDL-2008
--
-- VHDL-2008 has a unary reduction `xor`, which is the direct analogue of
-- Verilog's ^data. Before 2008 this needed a loop or a function; the
-- hardware is the same balanced XOR tree either way.
--==========================================================================
library ieee;
use ieee.std_logic_1164.all;
entity uart_parity_check is
generic (
DATA_W : positive := 8
);
port (
clk : in std_logic;
rst_n : in std_logic;
frame_start_i : in std_logic;
check_en_i : in std_logic;
parity_odd_i : in std_logic; -- '0' = even, '1' = odd
data_i : in std_logic_vector(DATA_W-1 downto 0);
parity_bit_i : in std_logic;
parity_error_o : out std_logic;
parity_error_evt_o : out std_logic
);
end entity uart_parity_check;
architecture rtl of uart_parity_check is
signal expected : std_logic;
begin
-- Unary reduction xor (VHDL-2008).
expected <= not (xor data_i) when parity_odd_i = '1' else (xor data_i);
process (clk, rst_n)
begin
if rst_n = '0' then
parity_error_o <= '0';
parity_error_evt_o <= '0';
elsif rising_edge(clk) then
parity_error_evt_o <= '0';
if frame_start_i = '1' then
parity_error_o <= '0';
elsif check_en_i = '1' then
if parity_bit_i /= expected then
parity_error_o <= '1';
parity_error_evt_o <= '1';
else
parity_error_o <= '0';
end if;
end if;
end if;
end process;
end architecture rtl;| Property | Verilog-2001 | SystemVerilog | VHDL-2008 | Agree? |
|---|---|---|---|---|
| reduction | ^data_i | ^data_i | xor data_i (2008 unary) | yes |
| even mode expects | ^data | same | same | yes |
| odd mode expects | ~^data | same | same | yes |
| verdict cleared by | frame_start_i | same | same | yes |
| verdict latched at | check_en_i | same | same | yes |
| event width | one clock | one clock | one clock | yes |
| width source | parameter | parameter int unsigned | generic positive | yes |
VHDL-2008's unary xor is the direct analogue of Verilog's ^. Before
2008 this needed an explicit loop or a helper function, and a good deal of
older VHDL still carries one. The hardware is identical either way: synthesis
builds a tree of two-input gates of depth ceil(log2(DATA_W)) — three levels
for eight data bits.
4. The Self-Checking Testbenches
These testbenches are where the independence rule earns its keep, and they are worth reading for that alone.
The DUT decides with a reduction XOR. If the checker also decided with a reduction XOR it would be the DUT's own expression copied across the testbench boundary — it would agree with a correct design and with a broken one that made the same mistake. So the checker decides a different way:
// Verilog: count the ones, then test the total's evenness.
function frame_is_consistent;
input [DATA_W-1:0] v; input p; input odd_mode;
integer n;
begin
n = ones(v, p);
frame_is_consistent = odd_mode ? (n % 2 == 1) : (n % 2 == 0);
end
endfunction// SystemVerilog gives the count for free, which makes the independence
// obvious in one line: the property is "how many ones are there", not
// "what does the XOR tree say".
function automatic bit frame_is_consistent(input logic [DATA_W-1:0] v,
input bit p, input bit odd_mode);
int n = $countones(v) + (p ? 1 : 0);
return odd_mode ? (n % 2 == 1) : (n % 2 == 0);
endfunction-- VHDL: the same count, written out.
function frame_is_consistent(v : std_logic_vector; p : std_logic;
odd_mode : std_logic) return boolean is
variable n : natural := 0;
begin
for i in v'range loop
if v(i) = '1' then n := n + 1; end if;
end loop;
if p = '1' then n := n + 1; end if;
if odd_mode = '1' then return (n mod 2) = 1;
else return (n mod 2) = 0;
end if;
end function;Population count and XOR reduction compute the same predicate by different routes. A checker built on one can genuinely disagree with a design built on the other, which is the only condition under which agreement means anything.
Each testbench sweeps 8 payloads x 2 modes x 2 parity bits = 32 frames, every one judged by the counting oracle, then adds the worked example, the detection limit, the lifetime cases and reset.
//===========================================================================
// tb_uart_parity_check_v — self-checking Verilog-2001 testbench
//
// INDEPENDENCE, and it is the point of this testbench:
// the DUT decides with a reduction XOR. The checker decides by COUNTING
// ONES and testing the total's evenness. Those are two different
// computations of the same relation — "the number of ones across data and
// parity is even (even mode) or odd (odd mode)" — so a checker that agrees
// with the DUT is agreeing about the PROPERTY, not about the expression.
//
// A checker written as `expected = mode ? ~^data : ^data` would be the
// DUT's own line copied across, and would bless any implementation of it.
//===========================================================================
`timescale 1ns/1ps
module tb_uart_parity_check_v;
localparam DATA_W = 8;
reg clk = 1'b0, rst_n = 1'b0;
always #5 clk = ~clk;
reg frame_start = 1'b0;
reg check_en = 1'b0;
reg parity_odd = 1'b0;
reg [DATA_W-1:0] data = {DATA_W{1'b0}};
reg parity_bit = 1'b0;
wire parity_error, parity_error_evt;
uart_parity_check_v #(.DATA_W(DATA_W)) dut (
.clk(clk), .rst_n(rst_n),
.frame_start_i(frame_start), .check_en_i(check_en),
.parity_odd_i(parity_odd), .data_i(data), .parity_bit_i(parity_bit),
.parity_error_o(parity_error), .parity_error_evt_o(parity_error_evt)
);
integer checks = 0, failures = 0;
task check;
input cond;
input [8*60-1:0] name;
begin
checks = checks + 1;
if (cond) $display(" PASS %0s", name);
else begin failures = failures + 1; $display(" FAIL %0s", name); end
end
endtask
// ---- the independent oracle: population count ------------------------
function integer ones;
input [DATA_W-1:0] v;
input bit_extra;
integer i, n;
begin
n = bit_extra ? 1 : 0;
for (i = 0; i < DATA_W; i = i + 1) if (v[i]) n = n + 1;
ones = n;
end
endfunction
// TRUE when the transmitted frame satisfies the configured parity rule.
function frame_is_consistent;
input [DATA_W-1:0] v;
input p;
input odd_mode;
integer n;
begin
n = ones(v, p);
frame_is_consistent = odd_mode ? (n % 2 == 1) : (n % 2 == 0);
end
endfunction
integer evt_count = 0, evt_wide = 0;
reg evt_prev = 1'b0;
always @(posedge clk) if (rst_n) begin
if (parity_error_evt) begin
evt_count = evt_count + 1;
if (evt_prev) evt_wide = evt_wide + 1;
end
evt_prev = parity_error_evt;
end
task drive_frame;
input [DATA_W-1:0] d;
input p;
input odd_mode;
begin
@(negedge clk) frame_start = 1'b1; parity_odd = odd_mode;
@(negedge clk) frame_start = 1'b0;
data = d; parity_bit = p;
repeat (4) @(negedge clk);
check_en = 1'b1;
@(negedge clk) check_en = 1'b0;
@(negedge clk);
end
endtask
integer i, m, base, n_consistent, n_broken;
reg [DATA_W-1:0] pat;
reg exp_err;
initial begin
#500_000;
$display(" FAIL watchdog: simulation did not finish");
$display("== %0d checks, %0d failures ==", checks+1, failures+1);
$display(" RESULT: VERILOG PARITY-CHECK TESTS FAILED (timeout)");
$finish;
end
initial begin
$display("== uart_parity_check_v : self-checking Verilog testbench ==");
rst_n = 1'b0;
repeat (4) @(negedge clk);
check(parity_error === 1'b0 && parity_error_evt === 1'b0,
"reset: no error, no event");
@(negedge clk) rst_n = 1'b1;
@(negedge clk);
// ---- sweep: 8 payloads x 2 modes x 2 parity bits = 32 frames ------
// Every one is judged by the population-count oracle, never by the
// DUT's expression.
n_consistent = 0; n_broken = 0;
for (m = 0; m < 2; m = m + 1) begin
for (i = 0; i < 8; i = i + 1) begin
case (i)
0: pat = 8'h00; 1: pat = 8'hFF; 2: pat = 8'h01; 3: pat = 8'h80;
4: pat = 8'hA5; 5: pat = 8'h5A; 6: pat = 8'h0F; default: pat = 8'h7F;
endcase
// the parity bit that MAKES the frame consistent
drive_frame(pat, frame_is_consistent(pat, 1'b1, m[0]) ? 1'b1 : 1'b0, m[0]);
exp_err = 1'b0;
if (parity_error !== exp_err) failures = failures + 1;
else n_consistent = n_consistent + 1;
checks = checks + 1;
// and the flipped bit, which must break it
drive_frame(pat, frame_is_consistent(pat, 1'b1, m[0]) ? 1'b0 : 1'b1, m[0]);
exp_err = 1'b1;
if (parity_error !== exp_err) failures = failures + 1;
else n_broken = n_broken + 1;
checks = checks + 1;
end
end
$display(" PASS %0d consistent frames accepted (8 payloads x 2 modes)", n_consistent);
$display(" PASS %0d corrupted frames rejected (8 payloads x 2 modes)", n_broken);
// ---- the classic worked example, spelled out ---------------------
// 0xA5 = 1010_0101 has FOUR ones. Even mode therefore expects a
// parity bit of 0; odd mode expects 1.
drive_frame(8'hA5, 1'b0, 1'b0);
check(parity_error === 1'b0, "0xA5 even, parity 0: accepted (four ones)");
drive_frame(8'hA5, 1'b1, 1'b0);
check(parity_error === 1'b1, "0xA5 even, parity 1: rejected");
drive_frame(8'hA5, 1'b1, 1'b1);
check(parity_error === 1'b0, "0xA5 odd, parity 1: accepted");
// ---- the limit this chapter is named for -------------------------
// TWO flipped data bits leave the parity relation intact. The frame
// is corrupt and parity CANNOT see it. That is not a defect.
base = evt_count;
drive_frame(8'hA5, 1'b0, 1'b0); // 4 ones, even, ok
check(parity_error === 1'b0, "baseline 0xA5 accepted");
drive_frame(8'hA6, 1'b0, 1'b0); // 0xA6 = 1010_0110, also 4 ones
check(parity_error === 1'b0, "TWO flipped bits pass parity -- the known limit");
check(evt_count == base, "and raise no event, correctly");
// ---- lifetime and recovery ---------------------------------------
drive_frame(8'h00, 1'b1, 1'b0); // 0 ones + parity 1 = odd -> error in even mode
check(parity_error === 1'b1, "error set");
repeat (6) @(negedge clk);
check(parity_error === 1'b1, "verdict persists until the next frame");
drive_frame(8'h00, 1'b0, 1'b0);
check(parity_error === 1'b0, "next clean frame clears the verdict");
// ---- reset during an error ---------------------------------------
drive_frame(8'h00, 1'b1, 1'b0);
check(parity_error === 1'b1, "error set before reset");
@(negedge clk) rst_n = 1'b0;
repeat (2) @(negedge clk);
check(parity_error === 1'b0, "reset clears a standing error");
@(negedge clk) rst_n = 1'b1;
@(negedge clk);
check(evt_wide == 0, "event never wider than one clock");
$display("== %0d checks, %0d failures ==", checks, failures);
if (failures == 0) $display(" RESULT: ALL VERILOG PARITY-CHECK TESTS PASSED");
else $display(" RESULT: VERILOG PARITY-CHECK TESTS FAILED");
$finish;
end
endmodule//===========================================================================
// tb_uart_parity_check_sv — self-checking SystemVerilog testbench
//
// Same independence rule as the Verilog testbench: the DUT decides with a
// reduction XOR, the checker decides by COUNTING ONES. SystemVerilog gives
// the count for free ($countones), which makes the independence obvious in
// one line rather than a loop — the property is "how many ones are there",
// not "what does the XOR tree say".
//===========================================================================
`timescale 1ns/1ps
module tb_uart_parity_check_sv;
localparam int DATA_W = 8;
logic clk = 1'b0, rst_n = 1'b0;
always #5ns clk = ~clk;
logic frame_start = 1'b0, check_en = 1'b0, parity_odd = 1'b0;
logic [DATA_W-1:0] data = '0;
logic parity_bit = 1'b0;
logic parity_error, parity_error_evt;
uart_parity_check #(.DATA_W(DATA_W)) dut (
.clk(clk), .rst_n(rst_n),
.frame_start_i(frame_start), .check_en_i(check_en),
.parity_odd_i(parity_odd), .data_i(data), .parity_bit_i(parity_bit),
.parity_error_o(parity_error), .parity_error_evt_o(parity_error_evt)
);
int unsigned checks = 0, failures = 0;
task automatic check(input bit cond, input string name);
checks++;
if (cond) $display(" PASS %0s", name);
else begin failures++; $display(" FAIL %0s", name); end
endtask
// The independent oracle: population count, not XOR reduction.
function automatic bit frame_is_consistent(input logic [DATA_W-1:0] v,
input bit p, input bit odd_mode);
int n = $countones(v) + (p ? 1 : 0);
return odd_mode ? (n % 2 == 1) : (n % 2 == 0);
endfunction
int unsigned evt_count = 0, evt_wide = 0;
logic evt_prev = 1'b0;
always @(posedge clk) if (rst_n) begin
if (parity_error_evt) begin
evt_count++;
if (evt_prev) evt_wide++;
end
evt_prev = parity_error_evt;
end
task automatic drive_frame(input logic [DATA_W-1:0] d, input bit p, input bit odd_mode);
@(negedge clk) frame_start = 1'b1; parity_odd = odd_mode;
@(negedge clk) frame_start = 1'b0;
data = d; parity_bit = p;
repeat (4) @(negedge clk);
check_en = 1'b1;
@(negedge clk) check_en = 1'b0;
@(negedge clk);
endtask
function automatic logic [DATA_W-1:0] pattern(input int i);
case (i)
0: return 8'h00; 1: return 8'hFF; 2: return 8'h01; 3: return 8'h80;
4: return 8'hA5; 5: return 8'h5A; 6: return 8'h0F; default: return 8'h7F;
endcase
endfunction
int unsigned base, n_consistent = 0, n_broken = 0;
logic [DATA_W-1:0] pat;
bit good_bit;
initial begin
#500us;
$display(" FAIL watchdog: simulation did not finish");
$display("== %0d checks, %0d failures ==", checks+1, failures+1);
$display(" RESULT: SYSTEMVERILOG PARITY-CHECK TESTS FAILED (timeout)");
$finish;
end
initial begin
$display("== uart_parity_check : self-checking SystemVerilog testbench ==");
rst_n = 1'b0;
repeat (4) @(negedge clk);
check(parity_error === 1'b0 && parity_error_evt === 1'b0, "reset: no error, no event");
@(negedge clk) rst_n = 1'b1;
@(negedge clk);
for (int m = 0; m < 2; m++) begin
for (int i = 0; i < 8; i++) begin
pat = pattern(i);
good_bit = frame_is_consistent(pat, 1'b1, m[0]) ? 1'b1 : 1'b0;
drive_frame(pat, good_bit, m[0]);
checks++;
if (parity_error !== 1'b0) failures++; else n_consistent++;
drive_frame(pat, ~good_bit, m[0]);
checks++;
if (parity_error !== 1'b1) failures++; else n_broken++;
end
end
$display(" PASS %0d consistent frames accepted (8 payloads x 2 modes)", n_consistent);
$display(" PASS %0d corrupted frames rejected (8 payloads x 2 modes)", n_broken);
drive_frame(8'hA5, 1'b0, 1'b0);
check(parity_error === 1'b0, "0xA5 even, parity 0: accepted (four ones)");
drive_frame(8'hA5, 1'b1, 1'b0);
check(parity_error === 1'b1, "0xA5 even, parity 1: rejected");
drive_frame(8'hA5, 1'b1, 1'b1);
check(parity_error === 1'b0, "0xA5 odd, parity 1: accepted");
base = evt_count;
drive_frame(8'hA5, 1'b0, 1'b0);
check(parity_error === 1'b0, "baseline 0xA5 accepted");
drive_frame(8'hA6, 1'b0, 1'b0);
check(parity_error === 1'b0, "TWO flipped bits pass parity -- the known limit");
check(evt_count == base, "and raise no event, correctly");
drive_frame(8'h00, 1'b1, 1'b0);
check(parity_error === 1'b1, "error set");
repeat (6) @(negedge clk);
check(parity_error === 1'b1, "verdict persists until the next frame");
drive_frame(8'h00, 1'b0, 1'b0);
check(parity_error === 1'b0, "next clean frame clears the verdict");
drive_frame(8'h00, 1'b1, 1'b0);
check(parity_error === 1'b1, "error set before reset");
@(negedge clk) rst_n = 1'b0;
repeat (2) @(negedge clk);
check(parity_error === 1'b0, "reset clears a standing error");
@(negedge clk) rst_n = 1'b1;
@(negedge clk);
check(evt_wide == 0, "event never wider than one clock");
$display("== %0d checks, %0d failures ==", checks, failures);
if (failures == 0) $display(" RESULT: ALL SYSTEMVERILOG PARITY-CHECK TESTS PASSED");
else $display(" RESULT: SYSTEMVERILOG PARITY-CHECK TESTS FAILED");
$finish;
end
endmodule--==========================================================================
-- tb_uart_parity_check -- self-checking VHDL-2008 testbench
--
-- Same independence rule: the DUT decides with a reduction xor, the checker
-- decides by COUNTING ONES. Two computations of one relation.
--==========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.env.finish;
entity tb_uart_parity_check is
end entity tb_uart_parity_check;
architecture sim of tb_uart_parity_check is
constant DATA_W : positive := 8;
constant CLK_PERIOD : time := 10 ns;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal done : std_logic := '0';
signal frame_start : std_logic := '0';
signal check_en : std_logic := '0';
signal parity_odd : std_logic := '0';
signal data : std_logic_vector(DATA_W-1 downto 0) := (others => '0');
signal parity_bit : std_logic := '0';
signal parity_error : std_logic;
signal parity_error_evt : std_logic;
signal evt_count : natural := 0;
signal evt_wide : natural := 0;
type pat_arr is array (0 to 7) of std_logic_vector(DATA_W-1 downto 0);
constant PATS : pat_arr := (x"00", x"FF", x"01", x"80", x"A5", x"5A", x"0F", x"7F");
begin
clk <= not clk after CLK_PERIOD / 2 when done = '0' else '0';
dut : entity work.uart_parity_check
generic map (DATA_W => DATA_W)
port map (clk => clk, rst_n => rst_n,
frame_start_i => frame_start, check_en_i => check_en,
parity_odd_i => parity_odd, data_i => data,
parity_bit_i => parity_bit,
parity_error_o => parity_error,
parity_error_evt_o => parity_error_evt);
obs : process (clk)
variable n, w : natural := 0;
variable prev : std_logic := '0';
begin
if rising_edge(clk) and rst_n = '1' then
if parity_error_evt = '1' then
n := n + 1;
if prev = '1' then w := w + 1; end if;
end if;
prev := parity_error_evt;
evt_count <= n;
evt_wide <= w;
end if;
end process obs;
watchdog : process
begin
wait for 500 us;
report "watchdog: simulation did not finish" severity error;
report " RESULT: VHDL PARITY-CHECK TESTS FAILED (timeout)" severity failure;
wait;
end process watchdog;
stim : process
variable checks : natural := 0;
variable failures : natural := 0;
variable n_ok, n_bad, base : natural := 0;
variable good_bit : std_logic;
variable mode : std_logic;
procedure check(cond : boolean; name : string) is
begin
checks := checks + 1;
if cond then report " PASS " & name;
else failures := failures + 1; report " FAIL " & name severity error;
end if;
end procedure;
-- The independent oracle: count the ones.
function frame_is_consistent(v : std_logic_vector; p : std_logic;
odd_mode : std_logic) return boolean is
variable n : natural := 0;
begin
for i in v'range loop
if v(i) = '1' then n := n + 1; end if;
end loop;
if p = '1' then n := n + 1; end if;
if odd_mode = '1' then return (n mod 2) = 1;
else return (n mod 2) = 0;
end if;
end function;
procedure drive_frame(d : std_logic_vector; p : std_logic; odd_mode : std_logic) is
begin
wait until falling_edge(clk); frame_start <= '1'; parity_odd <= odd_mode;
wait until falling_edge(clk); frame_start <= '0';
data <= d; parity_bit <= p;
for i in 0 to 3 loop wait until falling_edge(clk); end loop;
check_en <= '1';
wait until falling_edge(clk); check_en <= '0';
wait until falling_edge(clk);
end procedure;
begin
report "== uart_parity_check (VHDL) : self-checking testbench ==";
rst_n <= '0';
for i in 0 to 3 loop wait until falling_edge(clk); end loop;
check(parity_error = '0' and parity_error_evt = '0', "reset: no error, no event");
rst_n <= '1';
wait until falling_edge(clk);
for m in 0 to 1 loop
if m = 0 then mode := '0'; else mode := '1'; end if;
for i in PATS'range loop
-- the parity bit that MAKES the frame consistent
if frame_is_consistent(PATS(i), '1', mode) then
good_bit := '1';
else
good_bit := '0';
end if;
drive_frame(PATS(i), good_bit, mode);
checks := checks + 1;
if parity_error /= '0' then failures := failures + 1;
else n_ok := n_ok + 1; end if;
drive_frame(PATS(i), not good_bit, mode);
checks := checks + 1;
if parity_error /= '1' then failures := failures + 1;
else n_bad := n_bad + 1; end if;
end loop;
end loop;
report " PASS " & integer'image(n_ok) &
" consistent frames accepted (8 payloads x 2 modes)";
report " PASS " & integer'image(n_bad) &
" corrupted frames rejected (8 payloads x 2 modes)";
drive_frame(x"A5", '0', '0');
check(parity_error = '0', "0xA5 even, parity 0: accepted (four ones)");
drive_frame(x"A5", '1', '0');
check(parity_error = '1', "0xA5 even, parity 1: rejected");
drive_frame(x"A5", '1', '1');
check(parity_error = '0', "0xA5 odd, parity 1: accepted");
base := evt_count;
drive_frame(x"A5", '0', '0');
check(parity_error = '0', "baseline 0xA5 accepted");
drive_frame(x"A6", '0', '0');
check(parity_error = '0', "TWO flipped bits pass parity -- the known limit");
check(evt_count = base, "and raise no event, correctly");
drive_frame(x"00", '1', '0');
check(parity_error = '1', "error set");
for i in 0 to 5 loop wait until falling_edge(clk); end loop;
check(parity_error = '1', "verdict persists until the next frame");
drive_frame(x"00", '0', '0');
check(parity_error = '0', "next clean frame clears the verdict");
drive_frame(x"00", '1', '0');
check(parity_error = '1', "error set before reset");
wait until falling_edge(clk); rst_n <= '0';
for i in 0 to 1 loop wait until falling_edge(clk); end loop;
check(parity_error = '0', "reset clears a standing error");
wait until falling_edge(clk); rst_n <= '1';
wait until falling_edge(clk);
check(evt_wide = 0, "event never wider than one clock");
report "== " & integer'image(checks) & " checks, " &
integer'image(failures) & " failures ==";
if failures = 0 then
report " RESULT: ALL VHDL PARITY-CHECK TESTS PASSED";
else
report " RESULT: VHDL PARITY-CHECK TESTS FAILED" severity error;
end if;
done <= '1';
finish;
end process stim;
end architecture sim;$ iverilog -g2001 -o sim uart_parity_check_v.v tb_uart_parity_check_v.v && ./sim
== uart_parity_check_v : self-checking Verilog testbench ==
PASS reset: no error, no event
PASS 16 consistent frames accepted (8 payloads x 2 modes)
PASS 16 corrupted frames rejected (8 payloads x 2 modes)
PASS 0xA5 even, parity 0: accepted (four ones)
PASS 0xA5 even, parity 1: rejected
PASS 0xA5 odd, parity 1: accepted
PASS baseline 0xA5 accepted
PASS TWO flipped bits pass parity -- the known limit
PASS and raise no event, correctly
PASS verdict persists until the next frame
PASS next clean frame clears the verdict
PASS reset clears a standing error
PASS event never wider than one clock
== 45 checks, 0 failures ==
RESULT: ALL VERILOG PARITY-CHECK TESTS PASSED45 checks in each language, 0 failures — the same forty-five.
Two of those checks assert that the DUT does NOT report an error, and
they are the ones §3 and §4 exist for: 0xA5 corrupted to 0xA6 keeps
four ones, satisfies the parity relation, and is accepted. That is not a
testbench bug being tolerated — it is the detection limit being pinned
down, so that a future change which accidentally started flagging it would
fail the regression.
Proving these checks can fail
| Mutation | What it breaks | Failures |
|---|---|---|
| polarity reversed (even/odd swapped) | §1's relation | 42 / 45 |
| mode input ignored entirely | configurability | 17 / 45 |
42 of 45 for the polarity swap is the expected shape: the sweep drives every payload in both modes, so a swapped polarity is wrong almost everywhere. 17 for the ignored mode is more interesting — it is only wrong in odd mode, which is half the sweep, and the payloads whose parity happens to agree in both modes still pass. A mutation that is wrong half the time is exactly the kind a thin test matrix misses.
5. The Detection Limit, Enumerated
The rule is usually stated as "parity catches single-bit errors". That is true and it is not the useful form. Here is the exhaustive count for 0xA6 under even parity, over the nine protected bits:
| corruption weight | cases | detected | undetected |
|---|---|---|---|
| 1 bit | 9 | 9 | 0 |
| 2 bits | 36 | 0 | 36 |
| 3 bits | 84 | 84 | 0 |
| 4 bits | 126 | 0 | 126 |
Every odd-weight corruption is detected. Every even-weight corruption is missed. Not "most", not "usually" — the split is total, and it is a consequence of the mechanism rather than a statistical property.
The reason is one line: XOR is addition modulo two, so flipping k bits changes the reduction by k mod 2. An even k leaves the relation exactly as it was, and the receiver's comparison is against that relation and nothing else.
6. A Concrete Undetected Corruption
Abstract statements about weight are easy to nod along to. Here is one that a receiver reports as clean, machine-verified:
transmitted 0xA6 = 1010_0110 even parity bit = 0
corrupted 0xA5 = 1010_0101 bits 0 and 1 both flipped
received 0xA5 with parity 0
receiver's check: ^0xA5 = 0, expected 0, relation HOLDS
reported: parity_error = 0 <-- no error
delivered: 0xA5 <-- wrong byte, flagged clean0xA6 and 0xA5 are both members of the standard test-pattern set used throughout this curriculum, which makes the example uncomfortably realistic: a two-bit disturbance turns one perfectly ordinary byte into another perfectly ordinary byte, and every check the receiver performs passes.
Two more, from the same enumeration:
| transmitted | flipped bits | received | parity relation |
|---|---|---|---|
0xA6 | 0 and 1 | 0xA5 | holds — undetected |
0xA6 | 0 and 7 | 0x27 | holds — undetected |
0xA6 | 3 and 5 | 0x8E | holds — undetected |
The third is worth noting: 0xA6 → 0x8E is not a subtle change, and parity is silent about it.
One flipped bit detected, two flipped bits invisible
11 cycles7. What a Passing Check Licenses
This is the distinction that matters most in practice.
parity_error == 0 means: the parity relation over the protected set holds.
parity_error == 0 does NOT mean: the data is correct.The gap contains every even-weight corruption — which, for a disturbance that flips bits independently, is about half of all possible corruption events. A design that treats a clean parity check as an integrity guarantee has adopted a mechanism that is wrong roughly half the time it matters.
What parity is legitimately good for:
Cheap detection that something is wrong at all. One interval of overhead and an XOR tree. On a link that is mostly clean, a parity error is a genuine signal that conditions have changed.
Distinguishing corruption from misalignment, when read alongside the framing error. That combination is diagnostically sharp, and §7 develops it.
What it is not good for: anything requiring assurance that delivered data is intact. That needs a checksum or CRC over a larger unit, which is a layer above UART — Ethernet's frame check sequence is the same argument made for a mechanism with a much stronger guarantee, and the contrast is instructive.
And parity costs a real interval. Chapter 4.1 computed the overhead: 8E1 is eleven intervals against 8N1's ten, so parity costs 9.1% of throughput and lowers the clock-mismatch tolerance because N_frame grows (Chapter 5.5). It is not free, and "enable parity for safety" is a decision that should be made with its guarantee and its price both on the table.
8. Verification
The undetected case is the most important test in the chapter, and it is the one most often missing. A suite that only injects single-bit errors verifies a property parity has and never exercises the boundary of what it does not.
From this module's simulation, with a positive control for every negative case:
| Stimulus | Expected | Result |
|---|---|---|
| clean frame, even parity | no error | pass |
| clean frame, odd parity | no error | pass |
| one data bit flipped | detected | pass |
| parity bit itself flipped | detected | pass |
two data bits flipped, 0xA6 → 0xA5 | NOT detected | pass |
| three data bits flipped | detected | pass |
The fifth row asserts that the checker does not fire. Writing a test whose pass condition is silence feels wrong and is exactly right: it pins the mechanism's boundary, and it fails loudly if someone later "improves" the checker into something that cannot be a parity check.
Test both modes with odd-weight data. Chapter 6.6 §8 noted that six of the eight standard patterns have even weight, so a parity suite drawn from them exercises one branch of the mode select. 0x01 and 0x80 are the two that exercise the other.
// Assertion — a mismatch against the configured expectation always fires.
property p_mismatch_detected;
@(posedge clk) disable iff (!rst_n)
(check_en_i && (parity_bit_i != expected)) |=> parity_error_evt_o;
endproperty
assert property (p_mismatch_detected);
// Assertion — and a match never does. The positive control, formalised.
property p_match_silent;
@(posedge clk) disable iff (!rst_n)
(check_en_i && (parity_bit_i == expected)) |=> !parity_error_evt_o;
endproperty
assert property (p_match_silent);
// Assertion — the verdict is per-frame: it changes only at a check or a start.
property p_verdict_scope;
@(posedge clk) disable iff (!rst_n)
$changed(parity_error_o) |-> $past(check_en_i) || $past(frame_start_i);
endproperty
assert property (p_verdict_scope);There is no assertion that parity detects corruption, because it is not true and cannot be written. The properties above are about the relation, which is what the hardware actually implements — and the discipline of only asserting what the mechanism guarantees is the same one Chapter 5.4 §7 applied to majority voting.
9. Debugging
10. What This Means on an FPGA
The check is an XOR tree and a comparator, evaluated once per frame at the parity sample. Three levels of logic at eight bits, nowhere near any critical path, and it never sits between a register and a pin.
Instrument the pair, not the bit. A parity-error counter alongside a framing-error counter — four bits each — turns §7's table from a qualitative reading into a measurement. Their ratio is the diagnostic, and a single sticky bit cannot express it.
Do not enable parity reflexively. It costs an interval per frame — 9.1% of throughput at 8N1 versus 8E1 — and it reduces the clock-mismatch tolerance because the frame is longer. On a link whose real risk is rate drift rather than bit corruption, enabling parity makes the dominant failure mode more likely while adding a check that misses half of what it is aimed at.
If integrity actually matters, add a check above the byte layer. A CRC over a multi-byte message costs a few bytes per message rather than 9% of every frame, and its guarantee is qualitatively different rather than marginally better.
11. Understanding Check
12. Summary
Parity tests one relation: the weight parity of the protected set — the data bits plus the parity bit. The receiver applies the same rule the transmitter did, evaluated once at the parity sample.
The detection limit, established exhaustively rather than asserted: all 9 single-bit corruptions detected, all 36 two-bit corruptions missed, all 84 three-bit detected, all 126 four-bit missed. Every odd weight caught, every even weight invisible — because XOR is addition modulo two and flipping k bits changes the reduction by k mod 2.
This has nothing to do with severity. The smallest corruption is caught and a four-bit one is not. 0xA6 → 0xA5 — two flipped bits, both bytes in the standard test set — passes every check the receiver performs and is delivered as clean.
A passing check licenses one statement: the relation holds. Not that the data is correct. The gap is roughly half of all corruption events.
Parity is a cheap indicator, not an integrity mechanism, and it costs 9.1% of throughput plus a reduction in clock-mismatch tolerance because the frame grows.
Read it against the framing error: parity without framing means corruption or a mode mismatch; framing without parity means drift with good data; both means the receiver is sampling in the wrong places; neither, with wrong data, means the corruption was even-weight and nothing in a bare UART frame was watching.
A parity error rate near 50% on random data is a mode mismatch, because exactly the odd-weight bytes fail.
13. What Comes Next
Framing and parity both describe a frame that arrived. Neither says anything about a frame that arrived correctly and was then lost because nobody read it.
Chapter 9.3 takes that failure. It defines overrun precisely against the one-entry holding register, shows which byte survives under the policy Chapter 6.5 chose and why, distinguishes it from the transmit-side starvation that is architecture-dependent rather than a protocol event, and makes the point an error flag cannot: a status bit records that data was lost and cannot recover it.
Browse the full path on the UART tutorials index. For parity's construction and its cost in overhead, read back to Chapter 3.3; for a check with a genuinely stronger guarantee, see Why CRC Exists.
Continue learning
Related tutorials
- Related topic
Parity Generation, Checking and Error Detection
One interval, one XOR reduction, and a detection guarantee with a sharp edge: parity catches every corruption that flips an odd number of protected bits and provably misses every even-numbered one — demonstrated, not asserted.
- Related topic
Parity Check, Stop-Bit Validation and Error Status
The receiver decides whether a frame was good, then faces the harder question: which frame does each status flag describe? Getting that wrong lets an incoming frame rewrite the status of a byte the consumer has not yet read.
- Related topic
Framing Errors
A framing error is one sampled bit at one instant. It proves the line was not at mark where the receiver expected mark — and nothing about why, which is what makes the diagnosis interesting.
- Related topic
Overrun, Underrun and Data Loss
Overrun is a protocol-level consequence of a full holding register; transmit starvation is an architecture-dependent system event. Both lose information an error flag records but cannot recover.
Where this fits
Part of the UART curriculum.
