Skip to content
VLSI Mentor

UART · Module 13

UART as a Memory-Mapped Peripheral

The register block between a bus and the UART core, and the read and write side effects that make UART registers unlike memory — with a measured demonstration of getting one of them wrong.

Twelve modules have built a UART with wires for an interface: cfg_parity_i is a level, rx_trigger_o is a level, bytes move over valid/ready handshakes. Nothing about that is reachable from software.

A register block turns those wires into addresses. It sounds like clerical work and it contains the single most dangerous property in the whole peripheral: a UART's data register changes state when you read it. Memory does not do that, caches and debuggers assume it does not, and getting the condition wrong loses data silently.

1. What the Register Block Is

It is an adapter, and keeping it one is the discipline that matters. It decodes addresses, holds configuration, presents status, and converts bus transfers into the handshakes the core already speaks. It contains no UART logic whatsoever.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The core, instantiated UNMODIFIED. Every module from 6 through 12 is
// inside it, and the register block adds no protocol behaviour at all.
uart_ip #(.CLK_HZ(CLK_HZ), .BAUD_HZ(BAUD_HZ), ...) u_core (
    .cfg_parity_i(cfg_parity_q),          // a register field drives a wire
    .wr_data_i(wr_data), .wr_valid_i(wr_valid), .wr_ready_o(wr_ready),
    .rd_data_o(rd_data), .rd_valid_o(rd_valid), .rd_ready_i(rd_ready),
    ...);

This is what Chapter 11.1 §5 was protecting when it kept the register map out of the IP. The same core sits behind APB here, behind AXI in another project, and behind a direct hardware interface in a third, with no edit — because the boundary was drawn at wires rather than at addresses.

2. UART Registers Are Not Memory

Four properties, and every one of them breaks an assumption software brings from memory:

MemoryUART registers
reading returns what was writtenyesno — DATA reads the receive queue, writes the transmit queue
reading twice returns the same valueyesno — each read consumes a byte
reading has no effectyesno — reading DATA pops the FIFO
writing then reading returns the valueyesno — the value went out of a serial port

DATA is one address naming two completely different pieces of hardware. A write pushes to the transmit queue; a read pops from the receive queue. They share an address and share nothing else. This is conventional — every 16550-derived UART does it — and it is the first thing that surprises anyone writing a driver from a memory mental model.

3. Qualifying the Side Effect

Given the above, the question is precisely on which cycle does the pop happen? — and it has exactly one right answer: on a single, committed bus transfer.

APB makes this easy to state. A transfer has a SETUP phase (psel asserted, penable low) and an ACCESS phase (psel and penable both asserted), and completes when pready is high during ACCESS.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// A side effect may fire ONLY on a committed access. In APB that is the
// ACCESS phase with pready high: exactly one cycle per transfer, never
// repeated, never speculative.
assign access   = psel_i && penable_i && pready_o;
assign do_write = access &&  pwrite_i;
assign do_read  = access && !pwrite_i;

// THE read side effect. Popping on `do_read` and not on `psel_i` alone is
// the entire point: a bus that asserts select for two cycles, or a debugger
// that reads twice, must not consume two bytes.
assign rd_ready = do_read && (paddr_i == A_DATA);

The tempting error is psel_i && !pwrite_i, which looks like "the bus is reading me" and is wrong because psel is asserted for both phases of every transfer. One read then pops twice.

Measured

Two register blocks, identical except for that one line, each given the same eight bytes through internal loopback and then read back one at a time:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- eight bytes written to each, then read back one at a time
     [bad] read 1 returned 11, expected 10
     [bad] read 2 returned 13, expected 11
     [bad] read 3 returned 15, expected 12
     correctly-qualified block : 8 of 8 bytes delivered, in order=1
     psel-qualified block      : 4 of 8 bytes delivered, in order=0
     -> one bus read consumed 2.0 bytes

Half the data is gone, and the first read is already wrong. 0x10 was popped during the setup phase and discarded before the bus ever sampled a value, so the driver's first byte is the second byte sent. Every subsequent read skips one.

A block diagram of the register block sitting between a bus and the UART core. On the left, an APB bus provides select, enable, write, address and write-data signals. These enter an access qualification stage that identifies exactly one committed transfer cycle, and an address decoder. The decoder routes plain writes to the configuration registers and plain reads to the status and error registers, neither of which has any side effect. It routes a write to the data address into a push on the core's write handshake and a read of the data address into a pop on the core's read handshake, and those two are the only side effects in the block. On the right, the unmodified UART core receives configuration levels and control pulses and returns data with its per-byte status, live conditions and sticky error flags.APBpsel, penable, pwriteaccess qualifyONE committed cycleaddress decodewhich registerconfig + statusNO side effectswrite DATA ->pushside effectread DATA -> popside effectuart_ipunmodifiedtransfercommittedno effectwr_validrd_readyTX queueRX queue12
Figure 1 — the register block as an adapter. The bus side decodes addresses and qualifies side effects; the core side speaks the valid/ready handshakes the IP already had. Only the data register has side effects, and they are tied to a completed transfer. Everything else is a plain read or write.

4. Which Registers Must Be Safe to Read

Deliberately, only DATA has a side effect. Everything else — status, control, error flags, interrupt state — is a plain read.

That is not an accident of convenience; it is a property worth designing for:

A debugger must be able to inspect the peripheral. If reading the status register cleared it, a developer could not look at the UART without changing it, and the act of debugging would destroy the evidence. Every register except DATA can be watched continuously with no effect on behaviour.

A driver must be able to poll. Reading STATUS in a loop to wait for room in the transmit queue is the most common thing any driver does with a UART, and it has to be free.

Verified explicitly, because a design can drift into a side effect by accident:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  pass reading STATUS/IRQ_RAW does NOT consume the byte

A byte was placed in the queue, STATUS was read twice and IRQ_RAW once, and the subsequent DATA read still returned the original byte.

And an empty read must not fabricate data. Reading DATA with nothing queued returns a valid bit of zero rather than a stale byte:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  pass reading an empty RX reports valid=0

That bit is what lets a driver poll DATA directly instead of checking STATUS first — and checking STATUS first is a race anyway, because the queue can change between the two accesses.

5. Status Travels With the Byte

Chapter 10.2 §8 made the RX FIFO wider than the data so each entry carries its own framing and parity status. The register block is where that decision pays off:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A_DATA: begin
    // Status travels WITH the byte — Chapter 10.2 §8. Reading it from a
    // separate register would be a race against the pop.
    prdata_o[7:0] = rd_data;
    prdata_o[8]   = rd_perr;
    prdata_o[9]   = rd_ferr;
    prdata_o[10]  = rd_valid;
end

One read returns the byte and its verdict atomically. The alternative — a separate status register the driver reads after popping — is a race it cannot win: the pop has already advanced the queue, so the status register now describes a different byte, or no byte at all. Putting the per-byte status in the same word as the data removes the race by construction rather than by careful driver ordering.

6. The Port That Was Missing, Again

Chapter 11.4 §3 argued that a hierarchical reference is a missing port, and that blocks acquire their real interfaces from being used. Attaching a bus proved the point immediately.

The receive-idle timeout of Chapter 13.3 needs to know when a character arrives. The core did not publish that, and the first version of this register block reached inside for it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
assign rx_activity = (rd_valid && rd_ready) || (u_core.rx_push);   // WRONG

Same defect, same fix: the core now publishes the event, and the register block reads a port.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// in uart_ip's port list
output logic rx_push_evt_o
...
assign rx_push_evt_o = rx_push;

// in the register block
assign rx_activity = (rd_valid && rd_ready) || rx_push_evt;

This is the expected pattern rather than a failure of the earlier design. A block's interface is discovered by attaching it to things, and each new attachment finds what the previous one did not need. What matters is that the response is a port and not a reach-in — and the regression confirms the addition changed nothing: 86 checks, 0 failures across the Module 11 and 12 suites.

7. Counting the Side Effect

Section 3 established that the side effect must be qualified on the committed cycle, and measured what a looser qualifier costs. Turning that into a testbench needs one idea, and it is the idea the rest of this section is about.

Do not check that the read returned the right byte. Count the pops.

A design that pops twice per read still returns a correct-looking byte on the first access. It returns 0x41 when 0x41 was expected, and a testbench comparing data against a model will report a pass — then report a confusing mismatch several accesses later, by which point the queue and the model have diverged for reasons that no longer point at the cause. Section 3's "one read consuming exactly 2.0 bytes" is only visible as arithmetic: pops issued versus reads requested.

So the receive queue is modelled inside the testbench, every assertion of rx_pop_o is counted, and each claim about the side effect is stated as a sum:

ClaimStated as
a committed read pops oncen_pop increases by exactly 1
seven reads pop seven bytesn_pop increases by exactly 7
the SETUP phase commits nothingn_pop unchanged over six cycles of psel with no penable
an aborted transfer commits nothingn_pop unchanged after psel is withdrawn
the ten other registers are safen_pop and n_push unchanged across ten reads
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  tb_uart_regs — self-checking SystemVerilog testbench
//
//  The property this suite exists for is ONE POP PER READ. Chapter 13.1
//  section 3 measured what a mis-qualified side effect costs -- 4 bytes
//  delivered out of 8, one read consuming 2.0 bytes -- and the only way to
//  see that in a testbench is to COUNT the pops the design generates and
//  compare against the number of reads the stimulus issued.
//
//  So the receive queue is modelled, the pops are counted, and every check
//  about the side effect is stated as an arithmetic claim about those two
//  numbers. Checking that "a read returned the right byte" would pass on a
//  design that popped twice.
//
//  INDEPENDENCE: the expected byte always comes from the queue model the
//  testbench itself loaded, never from reading the DUT's state.
//===========================================================================
`timescale 1ns/1ps

module tb_uart_regs;

    localparam TRIG_W = 4, TMO_W = 16;

    localparam [7:0] A_DATA      = 8'h00;
    localparam [7:0] A_STATUS    = 8'h04;
    localparam [7:0] A_CTRL      = 8'h08;
    localparam [7:0] A_ERR       = 8'h0C;
    localparam [7:0] A_IRQ_EN    = 8'h10;
    localparam [7:0] A_IRQ_STAT  = 8'h14;
    localparam [7:0] A_IRQ_RAW   = 8'h18;
    localparam [7:0] A_FIFO_CTRL = 8'h1C;
    localparam [7:0] A_DMA_CTRL  = 8'h20;
    localparam [7:0] A_TIMEOUT   = 8'h24;
    localparam [7:0] A_TRIGGER   = 8'h28;

    logic pclk = 1'b0;
    always #5 pclk = ~pclk;
    logic presetn = 1'b0;

    logic         psel = 1'b0, penable = 1'b0, pwrite = 1'b0;
    logic  [7:0]  paddr = 8'h00;
    logic  [31:0] pwdata = 32'b0;
    wire [31:0] prdata;
    wire        pready, pslverr;

    // ---- the receive queue, modelled in the testbench --------------------
    logic  [7:0] q_data [0:63];
    logic        q_perr [0:63];
    logic        q_ferr [0:63];
    int    q_head = 0, q_tail = 0;
    wire       q_ne   = (q_head != q_tail);
    wire [7:0] rx_data  = q_ne ? q_data[q_head] : 8'hFF;
    wire       rx_perr  = q_ne ? q_perr[q_head] : 1'b0;
    wire       rx_ferr  = q_ne ? q_ferr[q_head] : 1'b0;
    wire       rx_valid = q_ne;
    wire       rx_pop;

    // ---- pop and push counters: the whole point of the suite -------------
    int n_pop = 0, n_push = 0, pop_when_empty = 0;
    wire [7:0] tx_data;
    wire       tx_push;
    logic  [7:0] tx_seen [0:63];

    always @(posedge pclk) if (presetn) begin
        // SystemVerilog immediate assertions. These restate, at the instant of
        // the event, what the counted checks establish by arithmetic at the
        // end: a side effect may occur ONLY in a committed access phase.
        a_pop_committed: assert (!rx_pop || (psel && penable && pready && !pwrite
                                             && paddr == A_DATA))
            else $error("rx_pop outside a committed DATA read");
        a_push_committed: assert (!tx_push || (psel && penable && pready && pwrite
                                               && paddr == A_DATA))
            else $error("tx_push outside a committed DATA write");
        if (rx_pop) begin
            n_pop++;
            if (!q_ne) pop_when_empty++;
            else       q_head++;
        end
        if (tx_push) begin
            tx_seen[n_push] = tx_data;
            n_push++;
        end
    end

    task q_load;
        input [7:0] d; input p; input f;
        begin q_data[q_tail]=d; q_perr[q_tail]=p; q_ferr[q_tail]=f; q_tail=q_tail+1; end
    endtask

    // ---- status and error stimulus ---------------------------------------
    logic st_tx_empty=1'b1, st_tx_full=1'b0, st_rx_empty=1'b1, st_rx_trig=1'b0;
    logic st_tx_trig=1'b0,  st_busy=1'b0,    st_break=1'b0,    st_rx_tmo=1'b0;
    logic e_frame=1'b0, e_parity=1'b0, e_overrun=1'b0, e_break=1'b0;
    logic [4:0] irq_raw = 5'b0;

    wire [3:0]  err_flags;
    wire [4:0]  irq_en;
    wire [1:0]  cfg_parity;
    wire cfg_hw, cfg_sw, cfg_li, cfg_ll, tx_flush, rx_flush, dma_tx_en, dma_rx_en;
    wire [TMO_W-1:0]  timeout_bits;
    wire [TRIG_W-1:0] rx_trigger, tx_trigger;

    uart_regs #(.ADDR_W(8), .TRIG_W(TRIG_W), .TMO_W(TMO_W)) dut (
        .pclk(pclk), .presetn(presetn), .psel_i(psel), .penable_i(penable),
        .pwrite_i(pwrite), .paddr_i(paddr), .pwdata_i(pwdata),
        .prdata_o(prdata), .pready_o(pready), .pslverr_o(pslverr),
        .rx_data_i(rx_data), .rx_valid_i(rx_valid), .rx_perr_i(rx_perr),
        .rx_ferr_i(rx_ferr), .rx_pop_o(rx_pop),
        .tx_data_o(tx_data), .tx_push_o(tx_push),
        .st_tx_empty_i(st_tx_empty), .st_tx_full_i(st_tx_full),
        .st_rx_empty_i(st_rx_empty), .st_rx_trig_i(st_rx_trig),
        .st_tx_trig_i(st_tx_trig), .st_busy_i(st_busy),
        .st_break_i(st_break), .st_rx_tmo_i(st_rx_tmo),
        .err_frame_set_i(e_frame), .err_parity_set_i(e_parity),
        .err_overrun_set_i(e_overrun), .err_break_set_i(e_break),
        .err_flags_o(err_flags),
        .irq_raw_i(irq_raw), .irq_en_o(irq_en),
        .cfg_parity_o(cfg_parity), .cfg_flow_hw_o(cfg_hw), .cfg_flow_sw_o(cfg_sw),
        .cfg_loop_int_o(cfg_li), .cfg_loop_line_o(cfg_ll),
        .tx_flush_o(tx_flush), .rx_flush_o(rx_flush),
        .dma_tx_en_o(dma_tx_en), .dma_rx_en_o(dma_rx_en),
        .timeout_bits_o(timeout_bits),
        .rx_trigger_o(rx_trigger), .tx_trigger_o(tx_trigger));

    // ---- flush pulses must be exactly one cycle wide ---------------------
    int txf_cycles = 0, rxf_cycles = 0, txf_wide = 0, rxf_wide = 0;
    logic txf_prev = 1'b0, rxf_prev = 1'b0;
    always @(posedge pclk) if (presetn) begin
        if (tx_flush) begin txf_cycles = txf_cycles+1; if (txf_prev) txf_wide=txf_wide+1; end
        if (rx_flush) begin rxf_cycles = rxf_cycles+1; if (rxf_prev) rxf_wide=rxf_wide+1; end
        txf_prev = tx_flush; rxf_prev = rx_flush;
    end

    // ---- APB3 driver -----------------------------------------------------
    logic [31:0] rd;
    task apb_write;
        input [7:0] a; input [31:0] d;
        begin
            @(negedge pclk); psel=1'b1; pwrite=1'b1; paddr=a; pwdata=d; penable=1'b0;
            @(negedge pclk); penable=1'b1;           // ACCESS phase
            @(posedge pclk);
            @(negedge pclk); psel=1'b0; penable=1'b0; pwrite=1'b0;
        end
    endtask

    task apb_read;
        input [7:0] a;
        begin
            @(negedge pclk); psel=1'b1; pwrite=1'b0; paddr=a; penable=1'b0;
            @(negedge pclk); penable=1'b1;           // ACCESS phase
            #1 rd = prdata;   // sampled BEFORE the committing edge, so the
                              // value read is the one the pop is about to remove
            @(posedge pclk);
            @(negedge pclk); psel=1'b0; penable=1'b0;
        end
    endtask

    int checks = 0, failures = 0;
    task automatic check(input logic cond, input string name);
        checks++;
        if (cond) $display("  PASS %0s", name);
        else begin failures++; $display("  FAIL %0s", name); end
    endtask

    int i, base_pop, base_push;

    initial begin
        #500_000;
        $display("  FAIL watchdog: simulation did not finish");
        $display("== %0d checks, %0d failures ==", checks+1, failures+1);
        $display("   RESULT: SYSTEMVERILOG REGS TESTS FAILED (timeout)");
        $finish;
    end

    initial begin
        $display("== uart_regs : self-checking SystemVerilog testbench ==");

        presetn = 1'b0;
        repeat (4) @(negedge pclk);
        presetn = 1'b1;
        repeat (2) @(negedge pclk);

        //=== reset values ==================================================
        apb_read(A_CTRL);
        check(rd == 32'h0, "reset: CTRL is 0 -- 8N1, no flow control, no loopback");
        apb_read(A_IRQ_EN);
        check(rd == 32'h0, "reset: IRQ_EN is 0 -- nothing enabled");
        apb_read(A_DMA_CTRL);
        check(rd == 32'h0, "reset: DMA_CTRL is 0");
        apb_read(A_TIMEOUT);
        check(rd == 32'd40, "reset: TIMEOUT is 40 bit times -- four characters");
        apb_read(A_TRIGGER);
        check(rd[TRIG_W-1:0] == 4'd8 && rd[16 +: TRIG_W] == 4'd4,
              "reset: TRIGGER is rx=8, tx=4");
        check(pready === 1'b1 && pslverr === 1'b0,
              "APB: pready always high, pslverr always low");

        //=== THE side effect: one pop per read =============================
        for (i = 0; i < 8; i++) q_load(8'h40 + i[7:0], 1'b0, 1'b0);
        base_pop = n_pop;
        apb_read(A_DATA);
        check(n_pop - base_pop == 1, "a committed DATA read pops EXACTLY ONCE");
        check(rd[7:0] == 8'h40,      "and returns the byte at the head");
        check(rd[10] === 1'b1,       "valid (bit 10) is set while the queue has data");

        base_pop = n_pop;
        for (i = 0; i < 7; i++) apb_read(A_DATA);
        check(n_pop - base_pop == 7, "seven reads pop exactly seven bytes");
        check(rd[7:0] == 8'h47,      "the last read returns the last byte loaded");

        //=== a read of an EMPTY queue still pops nothing it should not =====
        base_pop = n_pop;
        apb_read(A_DATA);
        check(rd[10] === 1'b0, "valid is CLEAR when the queue is empty");
        check(pop_when_empty == 1,
              "the pop still fires -- the queue, not the register block, guards it");

        //=== the SETUP phase must not commit anything =======================
        // A side effect qualified on psel alone would fire here. This is the
        // defect Chapter 13.1 section 3 measured.
        for (i = 0; i < 4; i++) q_load(8'h90 + i[7:0], 1'b0, 1'b0);
        base_pop = n_pop;
        @(negedge pclk); psel=1'b1; pwrite=1'b0; paddr=A_DATA; penable=1'b0;
        repeat (6) @(negedge pclk);            // sit in SETUP for six cycles
        check(n_pop - base_pop == 0, "six cycles in the SETUP phase pop NOTHING");
        @(negedge pclk); psel=1'b0;            // abort without ever asserting penable
        repeat (2) @(negedge pclk);
        check(n_pop - base_pop == 0, "an aborted transfer pops nothing at all");

        //=== per-byte status travels with the byte ==========================
        q_load(8'hC3, 1'b1, 1'b0);             // parity error on this byte
        q_load(8'hC4, 1'b0, 1'b1);             // framing error on this byte
        apb_read(A_DATA);
        check(rd[7:0] == 8'h90, "the aborted transfer left the queue untouched");
        for (i = 0; i < 3; i++) apb_read(A_DATA);
        apb_read(A_DATA);
        check(rd[7:0] == 8'hC3 && rd[8] === 1'b1 && rd[9] === 1'b0,
              "DATA carries THIS byte's parity error in bit 8");
        apb_read(A_DATA);
        check(rd[7:0] == 8'hC4 && rd[8] === 1'b0 && rd[9] === 1'b1,
              "and THIS byte's framing error in bit 9");

        //=== every other register is safe to read ===========================
        // Chapter 13.1 section 4. A debugger reads these; none may disturb
        // the design.
        for (i = 0; i < 4; i++) q_load(8'h70 + i[7:0], 1'b0, 1'b0);
        base_pop = n_pop; base_push = n_push;
        apb_read(A_STATUS);   apb_read(A_CTRL);      apb_read(A_ERR);
        apb_read(A_IRQ_EN);   apb_read(A_IRQ_STAT);  apb_read(A_IRQ_RAW);
        apb_read(A_FIFO_CTRL);apb_read(A_DMA_CTRL);  apb_read(A_TIMEOUT);
        apb_read(A_TRIGGER);
        check(n_pop - base_pop == 0 && n_push - base_push == 0,
              "reading all ten non-DATA registers has NO side effect");

        //=== writes ==========================================================
        base_push = n_push;
        apb_write(A_DATA, 32'h000000A5);
        check(n_push - base_push == 1, "a committed DATA write pushes EXACTLY ONCE");
        check(tx_seen[n_push-1] == 8'hA5, "and pushes the byte that was written");

        apb_write(A_CTRL, 32'h0000003F);
        apb_read(A_CTRL);
        check(rd[5:0] == 6'h3F, "CTRL writes and reads back");
        check(cfg_parity == 2'b11 && cfg_hw && cfg_sw && cfg_li && cfg_ll,
              "and every CTRL field reaches its output");
        apb_write(A_CTRL, 32'h0);

        apb_write(A_TIMEOUT, 32'd120);
        apb_read(A_TIMEOUT);
        check(rd == 32'd120 && timeout_bits == 16'd120, "TIMEOUT is writable");
        apb_write(A_TRIGGER, (32'd2 << 16) | 32'd12);
        check(rx_trigger == 4'd12 && tx_trigger == 4'd2,
              "TRIGGER carries both levels in one word");

        //=== the flush bits are PULSES ======================================
        base_pop = txf_cycles;
        apb_write(A_FIFO_CTRL, 32'h3);         // both flushes
        repeat (4) @(negedge pclk);
        check(txf_cycles - base_pop == 1, "a flush write produces ONE cycle, not a level");
        check(txf_wide == 0 && rxf_wide == 0, "no flush pulse was ever wider than one cycle");
        apb_read(A_FIFO_CTRL);
        check(rd == 32'h0, "FIFO_CTRL always reads back 0 -- it holds no state");

        //=== ERR: sticky history, write-one-to-clear ========================
        @(negedge pclk) e_frame = 1'b1;
        @(negedge pclk) e_frame = 1'b0;
        repeat (2) @(negedge pclk);
        apb_read(A_ERR);
        check(rd[0] === 1'b1, "ERR bit 0 is STICKY -- a one-cycle pulse is remembered");
        @(negedge pclk) e_parity = 1'b1;
        @(negedge pclk) e_parity = 1'b0;
        repeat (2) @(negedge pclk);
        apb_write(A_ERR, 32'h1);               // clear ONLY bit 0
        apb_read(A_ERR);
        check(rd[0] === 1'b0 && rd[1] === 1'b1,
              "W1C clears only the bits written, and leaves the others alone");
        apb_write(A_ERR, 32'hF);
        apb_read(A_ERR);
        check(rd[3:0] == 4'h0, "writing all ones clears every flag");

        //=== ERR is SET-DOMINANT ============================================
        // If an error arrives in the same cycle the driver clears the flag,
        // the flag must stay set: its only record is this bit.
        @(negedge pclk); psel=1'b1; pwrite=1'b1; paddr=A_ERR; pwdata=32'hF; penable=1'b0;
        @(negedge pclk); penable=1'b1; e_overrun=1'b1;   // set DURING the clear
        @(posedge pclk);
        @(negedge pclk); psel=1'b0; penable=1'b0; pwrite=1'b0; e_overrun=1'b0;
        repeat (2) @(negedge pclk);
        apb_read(A_ERR);
        check(rd[2] === 1'b1,
              "set beats clear in the same cycle -- the error is not lost");
        apb_write(A_ERR, 32'hF);

        //=== interrupts: transparent, not latched ===========================
        @(negedge pclk) irq_raw = 5'b00101;
        apb_write(A_IRQ_EN, 32'b00001);
        apb_read(A_IRQ_RAW);
        check(rd[4:0] == 5'b00101, "IRQ_RAW reports every condition, enabled or not");
        apb_read(A_IRQ_STAT);
        check(rd[4:0] == 5'b00001, "IRQ_STAT reports only what is ENABLED");
        apb_write(A_IRQ_STAT, 32'h1F);         // try to acknowledge it
        apb_read(A_IRQ_STAT);
        check(rd[4:0] == 5'b00001,
              "a write to IRQ_STAT changes NOTHING -- a level has no acknowledgement");
        @(negedge pclk) irq_raw = 5'b00100;    // service the cause instead
        apb_read(A_IRQ_STAT);
        check(rd[4:0] == 5'b00000,
              "removing the cause clears it, with no write at all");

        //=== an unmapped address is inert ===================================
        base_pop = n_pop; base_push = n_push;
        apb_write(8'hF0, 32'hFFFFFFFF);
        apb_read(8'hF0);
        check(rd == 32'h0, "an unmapped address reads as zero");
        check(n_pop - base_pop == 0 && n_push - base_push == 0,
              "and writing one disturbs nothing");
        apb_read(A_CTRL);
        check(rd == 32'h0, "CTRL was not corrupted by the unmapped write");

        //=== whole-run accounting ===========================================
        check(pop_when_empty == 1,
              "across the whole run, exactly one pop was issued on an empty queue");

        $display("== %0d checks, %0d failures ==", checks, failures);
        if (failures == 0) $display("   RESULT: ALL SYSTEMVERILOG REGS TESTS PASSED");
        else               $display("   RESULT: SYSTEMVERILOG REGS TESTS FAILED");
        $finish;
    end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  tb_uart_regs_v — self-checking Verilog-2001 testbench
//
//  The property this suite exists for is ONE POP PER READ. Chapter 13.1
//  section 3 measured what a mis-qualified side effect costs -- 4 bytes
//  delivered out of 8, one read consuming 2.0 bytes -- and the only way to
//  see that in a testbench is to COUNT the pops the design generates and
//  compare against the number of reads the stimulus issued.
//
//  So the receive queue is modelled, the pops are counted, and every check
//  about the side effect is stated as an arithmetic claim about those two
//  numbers. Checking that "a read returned the right byte" would pass on a
//  design that popped twice.
//
//  INDEPENDENCE: the expected byte always comes from the queue model the
//  testbench itself loaded, never from reading the DUT's state.
//===========================================================================
`timescale 1ns/1ps

module tb_uart_regs_v;

    localparam TRIG_W = 4, TMO_W = 16;

    localparam [7:0] A_DATA      = 8'h00;
    localparam [7:0] A_STATUS    = 8'h04;
    localparam [7:0] A_CTRL      = 8'h08;
    localparam [7:0] A_ERR       = 8'h0C;
    localparam [7:0] A_IRQ_EN    = 8'h10;
    localparam [7:0] A_IRQ_STAT  = 8'h14;
    localparam [7:0] A_IRQ_RAW   = 8'h18;
    localparam [7:0] A_FIFO_CTRL = 8'h1C;
    localparam [7:0] A_DMA_CTRL  = 8'h20;
    localparam [7:0] A_TIMEOUT   = 8'h24;
    localparam [7:0] A_TRIGGER   = 8'h28;

    reg pclk = 1'b0;
    always #5 pclk = ~pclk;
    reg presetn = 1'b0;

    reg         psel = 1'b0, penable = 1'b0, pwrite = 1'b0;
    reg  [7:0]  paddr = 8'h00;
    reg  [31:0] pwdata = 32'b0;
    wire [31:0] prdata;
    wire        pready, pslverr;

    // ---- the receive queue, modelled in the testbench --------------------
    reg  [7:0] q_data [0:63];
    reg        q_perr [0:63];
    reg        q_ferr [0:63];
    integer    q_head = 0, q_tail = 0;
    wire       q_ne   = (q_head != q_tail);
    wire [7:0] rx_data  = q_ne ? q_data[q_head] : 8'hFF;
    wire       rx_perr  = q_ne ? q_perr[q_head] : 1'b0;
    wire       rx_ferr  = q_ne ? q_ferr[q_head] : 1'b0;
    wire       rx_valid = q_ne;
    wire       rx_pop;

    // ---- pop and push counters: the whole point of the suite -------------
    integer n_pop = 0, n_push = 0, pop_when_empty = 0;
    wire [7:0] tx_data;
    wire       tx_push;
    reg  [7:0] tx_seen [0:63];

    always @(posedge pclk) if (presetn) begin
        if (rx_pop) begin
            n_pop = n_pop + 1;
            if (!q_ne) pop_when_empty = pop_when_empty + 1;
            else       q_head = q_head + 1;
        end
        if (tx_push) begin
            tx_seen[n_push] = tx_data;
            n_push = n_push + 1;
        end
    end

    task q_load;
        input [7:0] d; input p; input f;
        begin q_data[q_tail]=d; q_perr[q_tail]=p; q_ferr[q_tail]=f; q_tail=q_tail+1; end
    endtask

    // ---- status and error stimulus ---------------------------------------
    reg st_tx_empty=1'b1, st_tx_full=1'b0, st_rx_empty=1'b1, st_rx_trig=1'b0;
    reg st_tx_trig=1'b0,  st_busy=1'b0,    st_break=1'b0,    st_rx_tmo=1'b0;
    reg e_frame=1'b0, e_parity=1'b0, e_overrun=1'b0, e_break=1'b0;
    reg [4:0] irq_raw = 5'b0;

    wire [3:0]  err_flags;
    wire [4:0]  irq_en;
    wire [1:0]  cfg_parity;
    wire cfg_hw, cfg_sw, cfg_li, cfg_ll, tx_flush, rx_flush, dma_tx_en, dma_rx_en;
    wire [TMO_W-1:0]  timeout_bits;
    wire [TRIG_W-1:0] rx_trigger, tx_trigger;

    uart_regs_v #(.ADDR_W(8), .TRIG_W(TRIG_W), .TMO_W(TMO_W)) dut (
        .pclk(pclk), .presetn(presetn), .psel_i(psel), .penable_i(penable),
        .pwrite_i(pwrite), .paddr_i(paddr), .pwdata_i(pwdata),
        .prdata_o(prdata), .pready_o(pready), .pslverr_o(pslverr),
        .rx_data_i(rx_data), .rx_valid_i(rx_valid), .rx_perr_i(rx_perr),
        .rx_ferr_i(rx_ferr), .rx_pop_o(rx_pop),
        .tx_data_o(tx_data), .tx_push_o(tx_push),
        .st_tx_empty_i(st_tx_empty), .st_tx_full_i(st_tx_full),
        .st_rx_empty_i(st_rx_empty), .st_rx_trig_i(st_rx_trig),
        .st_tx_trig_i(st_tx_trig), .st_busy_i(st_busy),
        .st_break_i(st_break), .st_rx_tmo_i(st_rx_tmo),
        .err_frame_set_i(e_frame), .err_parity_set_i(e_parity),
        .err_overrun_set_i(e_overrun), .err_break_set_i(e_break),
        .err_flags_o(err_flags),
        .irq_raw_i(irq_raw), .irq_en_o(irq_en),
        .cfg_parity_o(cfg_parity), .cfg_flow_hw_o(cfg_hw), .cfg_flow_sw_o(cfg_sw),
        .cfg_loop_int_o(cfg_li), .cfg_loop_line_o(cfg_ll),
        .tx_flush_o(tx_flush), .rx_flush_o(rx_flush),
        .dma_tx_en_o(dma_tx_en), .dma_rx_en_o(dma_rx_en),
        .timeout_bits_o(timeout_bits),
        .rx_trigger_o(rx_trigger), .tx_trigger_o(tx_trigger));

    // ---- flush pulses must be exactly one cycle wide ---------------------
    integer txf_cycles = 0, rxf_cycles = 0, txf_wide = 0, rxf_wide = 0;
    reg txf_prev = 1'b0, rxf_prev = 1'b0;
    always @(posedge pclk) if (presetn) begin
        if (tx_flush) begin txf_cycles = txf_cycles+1; if (txf_prev) txf_wide=txf_wide+1; end
        if (rx_flush) begin rxf_cycles = rxf_cycles+1; if (rxf_prev) rxf_wide=rxf_wide+1; end
        txf_prev = tx_flush; rxf_prev = rx_flush;
    end

    // ---- APB3 driver -----------------------------------------------------
    reg [31:0] rd;
    task apb_write;
        input [7:0] a; input [31:0] d;
        begin
            @(negedge pclk); psel=1'b1; pwrite=1'b1; paddr=a; pwdata=d; penable=1'b0;
            @(negedge pclk); penable=1'b1;           // ACCESS phase
            @(posedge pclk);
            @(negedge pclk); psel=1'b0; penable=1'b0; pwrite=1'b0;
        end
    endtask

    task apb_read;
        input [7:0] a;
        begin
            @(negedge pclk); psel=1'b1; pwrite=1'b0; paddr=a; penable=1'b0;
            @(negedge pclk); penable=1'b1;           // ACCESS phase
            #1 rd = prdata;   // sampled BEFORE the committing edge, so the
                              // value read is the one the pop is about to remove
            @(posedge pclk);
            @(negedge pclk); psel=1'b0; penable=1'b0;
        end
    endtask

    integer checks = 0, failures = 0;
    task check;
        input cond;
        input [8*80-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

    integer i, base_pop, base_push;

    initial begin
        #500_000;
        $display("  FAIL watchdog: simulation did not finish");
        $display("== %0d checks, %0d failures ==", checks+1, failures+1);
        $display("   RESULT: VERILOG REGS TESTS FAILED (timeout)");
        $finish;
    end

    initial begin
        $display("== uart_regs_v : self-checking Verilog testbench ==");

        presetn = 1'b0;
        repeat (4) @(negedge pclk);
        presetn = 1'b1;
        repeat (2) @(negedge pclk);

        //=== reset values ==================================================
        apb_read(A_CTRL);
        check(rd == 32'h0, "reset: CTRL is 0 -- 8N1, no flow control, no loopback");
        apb_read(A_IRQ_EN);
        check(rd == 32'h0, "reset: IRQ_EN is 0 -- nothing enabled");
        apb_read(A_DMA_CTRL);
        check(rd == 32'h0, "reset: DMA_CTRL is 0");
        apb_read(A_TIMEOUT);
        check(rd == 32'd40, "reset: TIMEOUT is 40 bit times -- four characters");
        apb_read(A_TRIGGER);
        check(rd[TRIG_W-1:0] == 4'd8 && rd[16 +: TRIG_W] == 4'd4,
              "reset: TRIGGER is rx=8, tx=4");
        check(pready === 1'b1 && pslverr === 1'b0,
              "APB: pready always high, pslverr always low");

        //=== THE side effect: one pop per read =============================
        for (i = 0; i < 8; i = i + 1) q_load(8'h40 + i[7:0], 1'b0, 1'b0);
        base_pop = n_pop;
        apb_read(A_DATA);
        check(n_pop - base_pop == 1, "a committed DATA read pops EXACTLY ONCE");
        check(rd[7:0] == 8'h40,      "and returns the byte at the head");
        check(rd[10] === 1'b1,       "valid (bit 10) is set while the queue has data");

        base_pop = n_pop;
        for (i = 0; i < 7; i = i + 1) apb_read(A_DATA);
        check(n_pop - base_pop == 7, "seven reads pop exactly seven bytes");
        check(rd[7:0] == 8'h47,      "the last read returns the last byte loaded");

        //=== a read of an EMPTY queue still pops nothing it should not =====
        base_pop = n_pop;
        apb_read(A_DATA);
        check(rd[10] === 1'b0, "valid is CLEAR when the queue is empty");
        check(pop_when_empty == 1,
              "the pop still fires -- the queue, not the register block, guards it");

        //=== the SETUP phase must not commit anything =======================
        // A side effect qualified on psel alone would fire here. This is the
        // defect Chapter 13.1 section 3 measured.
        for (i = 0; i < 4; i = i + 1) q_load(8'h90 + i[7:0], 1'b0, 1'b0);
        base_pop = n_pop;
        @(negedge pclk); psel=1'b1; pwrite=1'b0; paddr=A_DATA; penable=1'b0;
        repeat (6) @(negedge pclk);            // sit in SETUP for six cycles
        check(n_pop - base_pop == 0, "six cycles in the SETUP phase pop NOTHING");
        @(negedge pclk); psel=1'b0;            // abort without ever asserting penable
        repeat (2) @(negedge pclk);
        check(n_pop - base_pop == 0, "an aborted transfer pops nothing at all");

        //=== per-byte status travels with the byte ==========================
        q_load(8'hC3, 1'b1, 1'b0);             // parity error on this byte
        q_load(8'hC4, 1'b0, 1'b1);             // framing error on this byte
        apb_read(A_DATA);
        check(rd[7:0] == 8'h90, "the aborted transfer left the queue untouched");
        for (i = 0; i < 3; i = i + 1) apb_read(A_DATA);
        apb_read(A_DATA);
        check(rd[7:0] == 8'hC3 && rd[8] === 1'b1 && rd[9] === 1'b0,
              "DATA carries THIS byte's parity error in bit 8");
        apb_read(A_DATA);
        check(rd[7:0] == 8'hC4 && rd[8] === 1'b0 && rd[9] === 1'b1,
              "and THIS byte's framing error in bit 9");

        //=== every other register is safe to read ===========================
        // Chapter 13.1 section 4. A debugger reads these; none may disturb
        // the design.
        for (i = 0; i < 4; i = i + 1) q_load(8'h70 + i[7:0], 1'b0, 1'b0);
        base_pop = n_pop; base_push = n_push;
        apb_read(A_STATUS);   apb_read(A_CTRL);      apb_read(A_ERR);
        apb_read(A_IRQ_EN);   apb_read(A_IRQ_STAT);  apb_read(A_IRQ_RAW);
        apb_read(A_FIFO_CTRL);apb_read(A_DMA_CTRL);  apb_read(A_TIMEOUT);
        apb_read(A_TRIGGER);
        check(n_pop - base_pop == 0 && n_push - base_push == 0,
              "reading all ten non-DATA registers has NO side effect");

        //=== writes ==========================================================
        base_push = n_push;
        apb_write(A_DATA, 32'h000000A5);
        check(n_push - base_push == 1, "a committed DATA write pushes EXACTLY ONCE");
        check(tx_seen[n_push-1] == 8'hA5, "and pushes the byte that was written");

        apb_write(A_CTRL, 32'h0000003F);
        apb_read(A_CTRL);
        check(rd[5:0] == 6'h3F, "CTRL writes and reads back");
        check(cfg_parity == 2'b11 && cfg_hw && cfg_sw && cfg_li && cfg_ll,
              "and every CTRL field reaches its output");
        apb_write(A_CTRL, 32'h0);

        apb_write(A_TIMEOUT, 32'd120);
        apb_read(A_TIMEOUT);
        check(rd == 32'd120 && timeout_bits == 16'd120, "TIMEOUT is writable");
        apb_write(A_TRIGGER, (32'd2 << 16) | 32'd12);
        check(rx_trigger == 4'd12 && tx_trigger == 4'd2,
              "TRIGGER carries both levels in one word");

        //=== the flush bits are PULSES ======================================
        base_pop = txf_cycles;
        apb_write(A_FIFO_CTRL, 32'h3);         // both flushes
        repeat (4) @(negedge pclk);
        check(txf_cycles - base_pop == 1, "a flush write produces ONE cycle, not a level");
        check(txf_wide == 0 && rxf_wide == 0, "no flush pulse was ever wider than one cycle");
        apb_read(A_FIFO_CTRL);
        check(rd == 32'h0, "FIFO_CTRL always reads back 0 -- it holds no state");

        //=== ERR: sticky history, write-one-to-clear ========================
        @(negedge pclk) e_frame = 1'b1;
        @(negedge pclk) e_frame = 1'b0;
        repeat (2) @(negedge pclk);
        apb_read(A_ERR);
        check(rd[0] === 1'b1, "ERR bit 0 is STICKY -- a one-cycle pulse is remembered");
        @(negedge pclk) e_parity = 1'b1;
        @(negedge pclk) e_parity = 1'b0;
        repeat (2) @(negedge pclk);
        apb_write(A_ERR, 32'h1);               // clear ONLY bit 0
        apb_read(A_ERR);
        check(rd[0] === 1'b0 && rd[1] === 1'b1,
              "W1C clears only the bits written, and leaves the others alone");
        apb_write(A_ERR, 32'hF);
        apb_read(A_ERR);
        check(rd[3:0] == 4'h0, "writing all ones clears every flag");

        //=== ERR is SET-DOMINANT ============================================
        // If an error arrives in the same cycle the driver clears the flag,
        // the flag must stay set: its only record is this bit.
        @(negedge pclk); psel=1'b1; pwrite=1'b1; paddr=A_ERR; pwdata=32'hF; penable=1'b0;
        @(negedge pclk); penable=1'b1; e_overrun=1'b1;   // set DURING the clear
        @(posedge pclk);
        @(negedge pclk); psel=1'b0; penable=1'b0; pwrite=1'b0; e_overrun=1'b0;
        repeat (2) @(negedge pclk);
        apb_read(A_ERR);
        check(rd[2] === 1'b1,
              "set beats clear in the same cycle -- the error is not lost");
        apb_write(A_ERR, 32'hF);

        //=== interrupts: transparent, not latched ===========================
        @(negedge pclk) irq_raw = 5'b00101;
        apb_write(A_IRQ_EN, 32'b00001);
        apb_read(A_IRQ_RAW);
        check(rd[4:0] == 5'b00101, "IRQ_RAW reports every condition, enabled or not");
        apb_read(A_IRQ_STAT);
        check(rd[4:0] == 5'b00001, "IRQ_STAT reports only what is ENABLED");
        apb_write(A_IRQ_STAT, 32'h1F);         // try to acknowledge it
        apb_read(A_IRQ_STAT);
        check(rd[4:0] == 5'b00001,
              "a write to IRQ_STAT changes NOTHING -- a level has no acknowledgement");
        @(negedge pclk) irq_raw = 5'b00100;    // service the cause instead
        apb_read(A_IRQ_STAT);
        check(rd[4:0] == 5'b00000,
              "removing the cause clears it, with no write at all");

        //=== an unmapped address is inert ===================================
        base_pop = n_pop; base_push = n_push;
        apb_write(8'hF0, 32'hFFFFFFFF);
        apb_read(8'hF0);
        check(rd == 32'h0, "an unmapped address reads as zero");
        check(n_pop - base_pop == 0 && n_push - base_push == 0,
              "and writing one disturbs nothing");
        apb_read(A_CTRL);
        check(rd == 32'h0, "CTRL was not corrupted by the unmapped write");

        //=== whole-run accounting ===========================================
        check(pop_when_empty == 1,
              "across the whole run, exactly one pop was issued on an empty queue");

        $display("== %0d checks, %0d failures ==", checks, failures);
        if (failures == 0) $display("   RESULT: ALL VERILOG REGS TESTS PASSED");
        else               $display("   RESULT: VERILOG REGS TESTS FAILED");
        $finish;
    end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
--===========================================================================
--  tb_uart_regs — self-checking VHDL-2008 testbench
--
--  The property this suite exists for is ONE POP PER READ. Chapter 13.1
--  section 3 measured what a mis-qualified side effect costs -- 4 bytes
--  delivered out of 8, one read consuming 2.0 bytes -- and the only way to
--  see that in a testbench is to COUNT the pops the design generates and
--  compare against the number of reads the stimulus issued.
--
--  So the receive queue is modelled, the pops are counted, and every check
--  about the side effect is stated as an arithmetic claim about those two
--  numbers. Checking that "a read returned the right byte" would pass on a
--  design that popped twice.
--
--  Same 40 counted checks as the Verilog and SystemVerilog twins.
--
--  VHDL note: an unresolved type may have only ONE driver, so the queue's
--  head pointer and the counters are driven by the observer process while
--  its tail pointer and contents are driven by the stimulus process. That
--  split is forced by the language and it is a good discipline anyway --
--  producer and consumer state cannot be accidentally written from both
--  ends.
--===========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity tb_uart_regs is
end entity tb_uart_regs;

architecture sim of tb_uart_regs is

    constant TCLK   : time     := 10 ns;
    constant TRIG_W : positive := 4;
    constant TMO_W  : positive := 16;

    constant A_DATA      : std_logic_vector(7 downto 0) := x"00";
    constant A_STATUS    : std_logic_vector(7 downto 0) := x"04";
    constant A_CTRL      : std_logic_vector(7 downto 0) := x"08";
    constant A_ERR       : std_logic_vector(7 downto 0) := x"0C";
    constant A_IRQ_EN    : std_logic_vector(7 downto 0) := x"10";
    constant A_IRQ_STAT  : std_logic_vector(7 downto 0) := x"14";
    constant A_IRQ_RAW   : std_logic_vector(7 downto 0) := x"18";
    constant A_FIFO_CTRL : std_logic_vector(7 downto 0) := x"1C";
    constant A_DMA_CTRL  : std_logic_vector(7 downto 0) := x"20";
    constant A_TIMEOUT   : std_logic_vector(7 downto 0) := x"24";
    constant A_TRIGGER   : std_logic_vector(7 downto 0) := x"28";

    signal pclk    : std_logic := '0';
    signal presetn : std_logic := '0';
    signal done    : boolean   := false;

    signal psel, penable, pwrite : std_logic := '0';
    signal paddr  : std_logic_vector(7 downto 0)  := x"00";
    signal pwdata : std_logic_vector(31 downto 0) := (others => '0');
    signal prdata : std_logic_vector(31 downto 0);
    signal pready, pslverr : std_logic;

    -- the receive queue, modelled in the testbench
    type byte_arr is array (0 to 63) of std_logic_vector(7 downto 0);
    type bit_arr  is array (0 to 63) of std_logic;
    signal q_data : byte_arr := (others => (others => '0'));
    signal q_perr : bit_arr  := (others => '0');
    signal q_ferr : bit_arr  := (others => '0');
    signal q_head : natural  := 0;          -- driven by the observer
    signal q_tail : natural  := 0;          -- driven by the stimulus
    signal q_ne   : std_logic;

    signal rx_data  : std_logic_vector(7 downto 0);
    signal rx_perr, rx_ferr, rx_valid, rx_pop : std_logic;

    signal n_pop, n_push, pop_when_empty : natural := 0;
    signal tx_data  : std_logic_vector(7 downto 0);
    signal tx_push  : std_logic;
    signal tx_seen  : byte_arr := (others => (others => '0'));

    signal st_tx_empty : std_logic := '1';
    signal st_tx_full  : std_logic := '0';
    signal st_rx_empty : std_logic := '1';
    signal st_rx_trig  : std_logic := '0';
    signal st_tx_trig  : std_logic := '0';
    signal st_busy     : std_logic := '0';
    signal st_break    : std_logic := '0';
    signal st_rx_tmo   : std_logic := '0';

    signal e_frame, e_parity, e_overrun, e_break : std_logic := '0';
    signal irq_raw : std_logic_vector(4 downto 0) := (others => '0');

    signal err_flags : std_logic_vector(3 downto 0);
    signal irq_en    : std_logic_vector(4 downto 0);
    signal cfg_parity : std_logic_vector(1 downto 0);
    signal cfg_hw, cfg_sw, cfg_li, cfg_ll : std_logic;
    signal tx_flush, rx_flush, dma_tx_en, dma_rx_en : std_logic;
    signal timeout_bits : std_logic_vector(TMO_W-1 downto 0);
    signal rx_trigger, tx_trigger : std_logic_vector(TRIG_W-1 downto 0);

    signal txf_cycles, rxf_cycles, txf_wide, rxf_wide : natural := 0;

begin

    pclk <= not pclk after TCLK/2 when not done else '0';

    q_ne     <= '1' when q_head /= q_tail else '0';
    rx_data  <= q_data(q_head) when q_ne = '1' else x"FF";
    rx_perr  <= q_perr(q_head) when q_ne = '1' else '0';
    rx_ferr  <= q_ferr(q_head) when q_ne = '1' else '0';
    rx_valid <= q_ne;

    dut : entity work.uart_regs
        generic map (ADDR_W => 8, TRIG_W => TRIG_W, TMO_W => TMO_W)
        port map (
            pclk => pclk, presetn => presetn, psel_i => psel,
            penable_i => penable, pwrite_i => pwrite, paddr_i => paddr,
            pwdata_i => pwdata, prdata_o => prdata,
            pready_o => pready, pslverr_o => pslverr,
            rx_data_i => rx_data, rx_valid_i => rx_valid,
            rx_perr_i => rx_perr, rx_ferr_i => rx_ferr, rx_pop_o => rx_pop,
            tx_data_o => tx_data, tx_push_o => tx_push,
            st_tx_empty_i => st_tx_empty, st_tx_full_i => st_tx_full,
            st_rx_empty_i => st_rx_empty, st_rx_trig_i => st_rx_trig,
            st_tx_trig_i => st_tx_trig, st_busy_i => st_busy,
            st_break_i => st_break, st_rx_tmo_i => st_rx_tmo,
            err_frame_set_i => e_frame, err_parity_set_i => e_parity,
            err_overrun_set_i => e_overrun, err_break_set_i => e_break,
            err_flags_o => err_flags,
            irq_raw_i => irq_raw, irq_en_o => irq_en,
            cfg_parity_o => cfg_parity, cfg_flow_hw_o => cfg_hw,
            cfg_flow_sw_o => cfg_sw, cfg_loop_int_o => cfg_li,
            cfg_loop_line_o => cfg_ll,
            tx_flush_o => tx_flush, rx_flush_o => rx_flush,
            dma_tx_en_o => dma_tx_en, dma_rx_en_o => dma_rx_en,
            timeout_bits_o => timeout_bits,
            rx_trigger_o => rx_trigger, tx_trigger_o => tx_trigger);

    -- pop and push counters: the whole point of the suite
    observer : process (pclk)
    begin
        if rising_edge(pclk) and presetn = '1' then
            if rx_pop = '1' then
                n_pop <= n_pop + 1;
                if q_ne = '0' then
                    pop_when_empty <= pop_when_empty + 1;
                else
                    q_head <= q_head + 1;
                end if;
            end if;
            if tx_push = '1' then
                tx_seen(n_push) <= tx_data;
                n_push          <= n_push + 1;
            end if;
        end if;
    end process observer;

    -- flush pulses must be exactly one cycle wide
    flush_obs : process (pclk)
        variable txf_prev, rxf_prev : std_logic := '0';
    begin
        if rising_edge(pclk) and presetn = '1' then
            if tx_flush = '1' then
                txf_cycles <= txf_cycles + 1;
                if txf_prev = '1' then txf_wide <= txf_wide + 1; end if;
            end if;
            if rx_flush = '1' then
                rxf_cycles <= rxf_cycles + 1;
                if rxf_prev = '1' then rxf_wide <= rxf_wide + 1; end if;
            end if;
            txf_prev := tx_flush; rxf_prev := rx_flush;
        end if;
    end process flush_obs;

    watchdog : process
    begin
        wait for 500 us;
        report "watchdog: simulation did not finish" severity failure;
    end process watchdog;

    stim : process
        variable checks, failures : natural := 0;
        variable rd               : std_logic_vector(31 downto 0);
        variable base_pop, base_push, base_cyc : natural := 0;

        procedure check(cond : boolean; name : string) is
        begin
            checks := checks + 1;
            if cond then
                report "  PASS " & name severity note;
            else
                failures := failures + 1;
                report "  FAIL " & name severity error;
            end if;
        end procedure check;

        procedure q_load(d : std_logic_vector(7 downto 0); p, f : std_logic) is
        begin
            q_data(q_tail) <= d;
            q_perr(q_tail) <= p;
            q_ferr(q_tail) <= f;
            q_tail         <= q_tail + 1;
            wait for 0 ns;                 -- let the update take effect
        end procedure q_load;

        procedure apb_write(a : std_logic_vector(7 downto 0);
                            d : std_logic_vector(31 downto 0)) is
        begin
            wait until falling_edge(pclk);
            psel <= '1'; pwrite <= '1'; paddr <= a; pwdata <= d; penable <= '0';
            wait until falling_edge(pclk);
            penable <= '1';                 -- ACCESS phase
            wait until rising_edge(pclk);
            wait until falling_edge(pclk);
            psel <= '0'; penable <= '0'; pwrite <= '0';
        end procedure apb_write;

        procedure apb_read(a : std_logic_vector(7 downto 0)) is
        begin
            wait until falling_edge(pclk);
            psel <= '1'; pwrite <= '0'; paddr <= a; penable <= '0';
            wait until falling_edge(pclk);
            penable <= '1';                 -- ACCESS phase
            wait for 1 ns;                  -- sample BEFORE the committing edge
            rd := prdata;
            wait until rising_edge(pclk);
            wait until falling_edge(pclk);
            psel <= '0'; penable <= '0';
        end procedure apb_read;
    begin
        report "== uart_regs : self-checking VHDL testbench ==" severity note;

        presetn <= '0';
        for i in 1 to 4 loop wait until falling_edge(pclk); end loop;
        presetn <= '1';
        for i in 1 to 2 loop wait until falling_edge(pclk); end loop;

        --=== reset values ==================================================
        apb_read(A_CTRL);
        check(rd = x"00000000", "reset: CTRL is 0 -- 8N1, no flow control, no loopback");
        apb_read(A_IRQ_EN);
        check(rd = x"00000000", "reset: IRQ_EN is 0 -- nothing enabled");
        apb_read(A_DMA_CTRL);
        check(rd = x"00000000", "reset: DMA_CTRL is 0");
        apb_read(A_TIMEOUT);
        check(unsigned(rd) = 40, "reset: TIMEOUT is 40 bit times -- four characters");
        apb_read(A_TRIGGER);
        check(unsigned(rd(TRIG_W-1 downto 0)) = 8
              and unsigned(rd(16+TRIG_W-1 downto 16)) = 4,
              "reset: TRIGGER is rx=8, tx=4");
        check(pready = '1' and pslverr = '0',
              "APB: pready always high, pslverr always low");

        --=== THE side effect: one pop per read =============================
        for i in 0 to 7 loop
            q_load(std_logic_vector(to_unsigned(16#40# + i, 8)), '0', '0');
        end loop;
        base_pop := n_pop;
        apb_read(A_DATA);
        check(n_pop - base_pop = 1, "a committed DATA read pops EXACTLY ONCE");
        check(rd(7 downto 0) = x"40", "and returns the byte at the head");
        check(rd(10) = '1', "valid (bit 10) is set while the queue has data");

        base_pop := n_pop;
        for i in 0 to 6 loop apb_read(A_DATA); end loop;
        check(n_pop - base_pop = 7, "seven reads pop exactly seven bytes");
        check(rd(7 downto 0) = x"47", "the last read returns the last byte loaded");

        --=== a read of an EMPTY queue ======================================
        base_pop := n_pop;
        apb_read(A_DATA);
        check(rd(10) = '0', "valid is CLEAR when the queue is empty");
        check(pop_when_empty = 1,
              "the pop still fires -- the queue, not the register block, guards it");

        --=== the SETUP phase must not commit anything =======================
        for i in 0 to 3 loop
            q_load(std_logic_vector(to_unsigned(16#90# + i, 8)), '0', '0');
        end loop;
        base_pop := n_pop;
        wait until falling_edge(pclk);
        psel <= '1'; pwrite <= '0'; paddr <= A_DATA; penable <= '0';
        for i in 1 to 6 loop wait until falling_edge(pclk); end loop;
        check(n_pop - base_pop = 0, "six cycles in the SETUP phase pop NOTHING");
        wait until falling_edge(pclk);
        psel <= '0';
        for i in 1 to 2 loop wait until falling_edge(pclk); end loop;
        check(n_pop - base_pop = 0, "an aborted transfer pops nothing at all");

        --=== per-byte status travels with the byte ==========================
        q_load(x"C3", '1', '0');
        q_load(x"C4", '0', '1');
        apb_read(A_DATA);
        check(rd(7 downto 0) = x"90", "the aborted transfer left the queue untouched");
        for i in 0 to 2 loop apb_read(A_DATA); end loop;
        apb_read(A_DATA);
        check(rd(7 downto 0) = x"C3" and rd(8) = '1' and rd(9) = '0',
              "DATA carries THIS byte's parity error in bit 8");
        apb_read(A_DATA);
        check(rd(7 downto 0) = x"C4" and rd(8) = '0' and rd(9) = '1',
              "and THIS byte's framing error in bit 9");

        --=== every other register is safe to read ===========================
        for i in 0 to 3 loop
            q_load(std_logic_vector(to_unsigned(16#70# + i, 8)), '0', '0');
        end loop;
        base_pop := n_pop; base_push := n_push;
        apb_read(A_STATUS);    apb_read(A_CTRL);      apb_read(A_ERR);
        apb_read(A_IRQ_EN);    apb_read(A_IRQ_STAT);  apb_read(A_IRQ_RAW);
        apb_read(A_FIFO_CTRL); apb_read(A_DMA_CTRL);  apb_read(A_TIMEOUT);
        apb_read(A_TRIGGER);
        check(n_pop - base_pop = 0 and n_push - base_push = 0,
              "reading all ten non-DATA registers has NO side effect");

        --=== writes ==========================================================
        base_push := n_push;
        apb_write(A_DATA, x"000000A5");
        check(n_push - base_push = 1, "a committed DATA write pushes EXACTLY ONCE");
        check(tx_seen(n_push-1) = x"A5", "and pushes the byte that was written");

        apb_write(A_CTRL, x"0000003F");
        apb_read(A_CTRL);
        check(rd(5 downto 0) = "111111", "CTRL writes and reads back");
        check(cfg_parity = "11" and cfg_hw = '1' and cfg_sw = '1'
              and cfg_li = '1' and cfg_ll = '1',
              "and every CTRL field reaches its output");
        apb_write(A_CTRL, x"00000000");

        apb_write(A_TIMEOUT, std_logic_vector(to_unsigned(120, 32)));
        apb_read(A_TIMEOUT);
        check(unsigned(rd) = 120 and unsigned(timeout_bits) = 120,
              "TIMEOUT is writable");
        apb_write(A_TRIGGER, std_logic_vector(to_unsigned(2, 16))
                           & std_logic_vector(to_unsigned(12, 16)));
        check(unsigned(rx_trigger) = 12 and unsigned(tx_trigger) = 2,
              "TRIGGER carries both levels in one word");

        --=== the flush bits are PULSES ======================================
        base_cyc := txf_cycles;
        apb_write(A_FIFO_CTRL, x"00000003");
        for i in 1 to 4 loop wait until falling_edge(pclk); end loop;
        check(txf_cycles - base_cyc = 1,
              "a flush write produces ONE cycle, not a level");
        check(txf_wide = 0 and rxf_wide = 0,
              "no flush pulse was ever wider than one cycle");
        apb_read(A_FIFO_CTRL);
        check(rd = x"00000000", "FIFO_CTRL always reads back 0 -- it holds no state");

        --=== ERR: sticky history, write-one-to-clear ========================
        wait until falling_edge(pclk); e_frame <= '1';
        wait until falling_edge(pclk); e_frame <= '0';
        for i in 1 to 2 loop wait until falling_edge(pclk); end loop;
        apb_read(A_ERR);
        check(rd(0) = '1', "ERR bit 0 is STICKY -- a one-cycle pulse is remembered");
        wait until falling_edge(pclk); e_parity <= '1';
        wait until falling_edge(pclk); e_parity <= '0';
        for i in 1 to 2 loop wait until falling_edge(pclk); end loop;
        apb_write(A_ERR, x"00000001");
        apb_read(A_ERR);
        check(rd(0) = '0' and rd(1) = '1',
              "W1C clears only the bits written, and leaves the others alone");
        apb_write(A_ERR, x"0000000F");
        apb_read(A_ERR);
        check(rd(3 downto 0) = x"0", "writing all ones clears every flag");

        --=== ERR is SET-DOMINANT ============================================
        wait until falling_edge(pclk);
        psel <= '1'; pwrite <= '1'; paddr <= A_ERR;
        pwdata <= x"0000000F"; penable <= '0';
        wait until falling_edge(pclk);
        penable <= '1'; e_overrun <= '1';      -- set DURING the clear
        wait until rising_edge(pclk);
        wait until falling_edge(pclk);
        psel <= '0'; penable <= '0'; pwrite <= '0'; e_overrun <= '0';
        for i in 1 to 2 loop wait until falling_edge(pclk); end loop;
        apb_read(A_ERR);
        check(rd(2) = '1',
              "set beats clear in the same cycle -- the error is not lost");
        apb_write(A_ERR, x"0000000F");

        --=== interrupts: transparent, not latched ===========================
        wait until falling_edge(pclk); irq_raw <= "00101";
        apb_write(A_IRQ_EN, x"00000001");
        apb_read(A_IRQ_RAW);
        check(rd(4 downto 0) = "00101",
              "IRQ_RAW reports every condition, enabled or not");
        apb_read(A_IRQ_STAT);
        check(rd(4 downto 0) = "00001", "IRQ_STAT reports only what is ENABLED");
        apb_write(A_IRQ_STAT, x"0000001F");    -- try to acknowledge it
        apb_read(A_IRQ_STAT);
        check(rd(4 downto 0) = "00001",
              "a write to IRQ_STAT changes NOTHING -- a level has no acknowledgement");
        wait until falling_edge(pclk); irq_raw <= "00100";
        apb_read(A_IRQ_STAT);
        check(rd(4 downto 0) = "00000",
              "removing the cause clears it, with no write at all");

        --=== an unmapped address is inert ===================================
        base_pop := n_pop; base_push := n_push;
        apb_write(x"F0", x"FFFFFFFF");
        apb_read(x"F0");
        check(rd = x"00000000", "an unmapped address reads as zero");
        check(n_pop - base_pop = 0 and n_push - base_push = 0,
              "and writing one disturbs nothing");
        apb_read(A_CTRL);
        check(rd = x"00000000", "CTRL was not corrupted by the unmapped write");

        --=== whole-run accounting ===========================================
        check(pop_when_empty = 1,
              "across the whole run, exactly one pop was issued on an empty queue");

        report "== " & integer'image(checks) & " checks, "
                     & integer'image(failures) & " failures ==" severity note;
        if failures = 0 then
            report "   RESULT: ALL VHDL REGS TESTS PASSED" severity note;
        else
            report "   RESULT: VHDL REGS TESTS FAILED" severity error;
        end if;
        done <= true;
        wait;
    end process stim;

end architecture sim;

All three run the same 40 checks and agree:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
== uart_regs : self-checking SystemVerilog testbench ==
  PASS reset: CTRL is 0 -- 8N1, no flow control, no loopback
  PASS reset: TIMEOUT is 40 bit times -- four characters
  PASS a committed DATA read pops EXACTLY ONCE
  PASS seven reads pop exactly seven bytes
  PASS six cycles in the SETUP phase pop NOTHING
  PASS an aborted transfer pops nothing at all
  PASS the aborted transfer left the queue untouched
  PASS DATA carries THIS byte's parity error in bit 8
  PASS reading all ten non-DATA registers has NO side effect
  PASS a flush write produces ONE cycle, not a level
  PASS FIFO_CTRL always reads back 0 -- it holds no state
  PASS ERR bit 0 is STICKY -- a one-cycle pulse is remembered
  PASS W1C clears only the bits written, and leaves the others alone
  PASS set beats clear in the same cycle -- the error is not lost
  PASS a write to IRQ_STAT changes NOTHING -- a level has no acknowledgement
  PASS removing the cause clears it, with no write at all
== 40 checks, 0 failures ==

Verilog-2001    : 40 checks, 0 failures
SystemVerilog   : 40 checks, 0 failures
VHDL-2008       : 40 checks, 0 failures

8. Verification

Test the side effect's boundaries, not just its function. That a read returns the right byte is the easy half; the half that matters is that it consumes exactly one byte per transfer.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Assertion — the pop is exactly as wide as a committed transfer. This is
// the property the psel-qualified version violates, and it is checkable
// without knowing anything about UART behaviour.
property p_pop_only_on_access;
    @(posedge clk) disable iff (!rst_n)
        rd_ready |-> (psel_i && penable_i && pready_o && !pwrite_i);
endproperty

// Assertion — no register except DATA disturbs anything.
property p_only_data_has_side_effects;
    @(posedge clk) disable iff (!rst_n)
        (do_read && paddr_i != A_DATA) |-> !rd_ready;
endproperty

Read every register twice and compare. A register that returns different values on two consecutive reads, with nothing else happening, either has a side effect or reflects a live condition — and the design must say which on purpose. This is a three-line test that catches an entire class of accidental side effect.

Drive the bus correctly, and then incorrectly. The comparison in §3 needed a second register block, because a correct master will never exercise the wrong qualification. Testing an adapter with only a well-behaved master verifies less than it appears to.

Count bytes end to end. The §3 defect is invisible to every UART-level check and obvious to a test that writes eight bytes and demands eight back. Whole-transaction accounting is what catches adapter bugs, because adapters fail by losing things rather than by corrupting them.

9. Debugging

10. What This Means on an FPGA

The register block is small and almost entirely flops. The configuration fields, the interrupt state and the decode are a few dozen registers and a comparator on the address — negligible against the core.

One wait state is usually unnecessary. Every register here is a flop or a combinational mux of flops, so pready is tied high and every access completes in the APB minimum of two cycles. Adding wait states because it seems safer costs every driver access and buys nothing.

Keep the address map sparse and word-aligned. Four-byte spacing for every register wastes nothing real and means a driver never needs a sub-word access, which is where byte-enable handling becomes an unnecessary complication.

Do not let the register block accumulate logic. The pressure to "just handle this in the registers" is constant and it is how a reusable core turns into a bus-specific one. If a behaviour belongs to the UART, it belongs in the core where Chapter 13.5's different bus will also get it.

11. Understanding Check

12. Summary

The register block is an adapter and must stay one. It decodes, holds configuration, presents status and converts transfers into handshakes — and contains no UART logic, which is what lets the same core sit behind a different bus unchanged.

UART registers are not memory in four separate ways, all at DATA: one address naming two different pieces of hardware, a read that consumes, a read that is not repeatable, and a write that cannot be read back.

A side effect fires on a committed transfer, never on "select is asserted". Qualifying on select instead delivered 4 of 8 bytes, with the first read already returning the second byte — one bus read consuming exactly 2.0 bytes.

That defect is invisible to every UART-level diagnostic and presents as a serial-domain problem. Byte accounting end to end is what finds it.

Only DATA has side effects, so a debugger can watch the peripheral and a driver can poll without consequence — and an empty read reports valid = 0 rather than fabricating a byte.

Per-byte status is returned in the same word as the data, because reading it separately is a race against the pop.

And attaching a bus found another missing port — the receive-arrival event — which is Chapter 11.4's rule recurring exactly as predicted: a hierarchical reference is a missing port, and interfaces are discovered by attaching things.

13. What Comes Next

The mechanism is established; the contents are not. Chapter 13.2 lays out the actual register map — which fields exist, which register each lives in, and why.

That includes the decisions that look arbitrary and are not: why the trigger levels are writable, why the error flags are write-one-to-clear rather than clear-on-read, and why some status bits appear in two places on purpose.

Browse the full path on the UART tutorials index. For the per-byte status this chapter returns atomically, read back to Chapter 10.2.

Continue learning

Where this fits

Part of the UART curriculum.