Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
observed p  ==  expected(mode, assembled d)     ->  relation holds

An equivalent and more revealing form is to reduce the whole protected set at once:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
even parity:   ^{d, p}  ==  0        the protected set has even weight
odd  parity:   ^{d, p}  ==  1        the protected set has odd weight

Parity 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------------
//  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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
--==========================================================================
--  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;
PropertyVerilog-2001SystemVerilogVHDL-2008Agree?
reduction^data_i^data_ixor data_i (2008 unary)yes
even mode expects^datasamesameyes
odd mode expects~^datasamesameyes
verdict cleared byframe_start_isamesameyes
verdict latched atcheck_en_isamesameyes
event widthone clockone clockone clockyes
width sourceparameterparameter int unsignedgeneric positiveyes

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
--==========================================================================
--  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;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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 PASSED

45 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

MutationWhat it breaksFailures
polarity reversed (even/odd swapped)§1's relation42 / 45
mode input ignored entirelyconfigurability17 / 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 weightcasesdetectedundetected
1 bit990
2 bits36036
3 bits84840
4 bits1260126

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 clean

0xA6 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:

transmittedflipped bitsreceivedparity relation
0xA60 and 10xA5holds — undetected
0xA60 and 70x27holds — undetected
0xA63 and 50x8Eholds — 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 cycles
A comparison across eleven bit intervals of three received frames. The first shows the transmitted reference carrying A6 hexadecimal with an even parity bit of zero. The second shows the same frame with a single data bit flipped, which changes the exclusive-or reduction of the data and therefore violates the parity relation, so a parity error is raised. The third shows the same frame with two data bits flipped, producing the byte A5 hexadecimal; because two flips leave the reduction unchanged, the parity relation still holds and no parity error is raised even though the delivered byte is wrong.parity interval IDENTICAL in all threeparity interval IDENTICALin all three1 flip -> detected1 flip -> detected2nd flip -> now invisible2nd flip -> now invisiblefieldSTARTd0d1d2d3d4d5d6d7PARSTOPsent 0xA61 flip2 flipsparity_error_ot0t1t2t3t4t5t6t7t8t9t10
Figure 1 — the same frame under three corruptions. Columns are BIT INTERVALS. The first row is the transmitted reference; the second flips one data bit and is detected; the third flips two and is not. Note that the third row's payload differs from the reference in two positions and the receiver's parity check reports no error at all — the parity interval itself is identical in all three cases.

7. What a Passing Check Licenses

This is the distinction that matters most in practice.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

StimulusExpectedResult
clean frame, even parityno errorpass
clean frame, odd parityno errorpass
one data bit flippeddetectedpass
parity bit itself flippeddetectedpass
two data bits flipped, 0xA6 → 0xA5NOT detectedpass
three data bits flippeddetectedpass

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

Where this fits

Part of the UART curriculum.