Skip to content
VLSI Mentor

UART · Module 13

Control, Configuration, Data and Status Registers

A complete, simulated UART register map — and the reasoning behind each placement, including a parameter that turned out to belong in a register and the elaboration check that became a runtime clamp.

Chapter 13.1 built the mechanism. This chapter fills it in, and the interesting content is not the table — it is the reasoning behind each placement, because almost every one of them is a decision that could have gone the other way.

One of them did go the other way in this design, and applying Chapter 11.4's own test found it.

1. The Map

OffsetNameAccessPurpose
0x00DATARWread pops the RX queue, write pushes the TX queue
0x04STATUSROlive conditions
0x08CTRLRWconfiguration
0x0CERRW1Csticky error flags
0x10IRQ_ENRWinterrupt enable mask
0x14IRQ_STATROpending and enabled — see 13.3
0x18IRQ_RAWROpending before masking
0x1CFIFO_CTRLRWflush bits, self-clearing
0x20DMA_CTRLRWDMA enables
0x24TIMEOUTRWreceive-idle timeout, in bit times
0x28TRIGGERRWFIFO trigger levels

Eleven registers, word-aligned, and only one of them has a side effect. That ratio is deliberate — Chapter 13.1 §4.

DATA — 0x00

BitsReadWrite
[7:0]received bytebyte to transmit
[8]parity error for this byte—
[9]framing error for this byte—
[10]valid — a byte was actually present—

The per-byte status shares the word with the data so one access gets both atomically, which Chapter 13.1 §5 showed is the only way to avoid racing the pop.

Bit 10 is what makes polling DATA directly legal. Without it a driver must read STATUS first, and the queue can change between the two accesses — so the "safe" sequence is the racy one.

STATUS — 0x04, read-only, no side effects

BitMeaning
0TX queue empty
1TX queue full
2RX queue empty
3RX trigger reached
4TX trigger reached
5busy — work in progress (Chapter 11.1 §6)
6break active now
7receive-idle timeout

Bits 3, 4, 6 and 7 also appear as interrupt sources, and that duplication is intentional. A driver that uses interrupts reads IRQ_STAT; a driver that polls reads STATUS; a developer debugging either reads STATUS and gets the truth regardless of the enable mask. Forcing one path through the interrupt registers would mean a polling driver has to enable interrupts it does not want.

CTRL — 0x08

BitsField
[1:0]parity mode — none / even / odd / mark
[2]hardware flow control enable
[3]software flow control enable
[4]internal loopback
[5]line loopback

All five are levels, and all five reset to zero — which is 8N1, no flow control, no loopback. Chapter 11.5 §5 required that: a test mode whose reset value is "on" eventually ships that way.

A parity change written here does not take effect immediately. Chapter 11.3 defers it until the link is quiet, so both halves switch on one edge. That is invisible in this register and very visible to a driver that writes CTRL and immediately expects the new format.

ERR — 0x0C, write-one-to-clear

Sticky flags for framing, parity, overrun and break — Chapter 9.6. Separate from the per-byte bits in DATA because they answer a different question: has anything gone wrong since I last looked, versus was this byte good.

FIFO_CTRL — 0x1C

Bit 0 flushes the transmit queue, bit 1 the receive queue. Both are self-clearing: writing 1 produces a one-cycle pulse and the bit reads back as 0.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Control PULSES are one cycle by construction — Chapter 11.3 §1.
tx_flush_pulse  <= 1'b0;
rx_flush_pulse  <= 1'b0;
...
A_FIFO_CTRL: begin
    tx_flush_pulse <= pwdata_i[0];
    rx_flush_pulse <= pwdata_i[1];
end

A flush bit that software must clear is a defect waiting to happen — a driver that sets it and returns leaves the queue permanently in reset, presenting as a queue that is always empty while the write port stays ready. Generating the pulse in hardware removes the possibility.

2. A Parameter That Belonged in a Register

Chapter 11.4 §1 gave the test: does the value change the gates, or does it select among behaviours the gates already implement? Applying it honestly to this design found a violation.

The FIFO trigger levels were parameters. But the comparison already exists in silicon either way — level >= threshold is one comparator regardless of the threshold's value. Nothing is built differently. By the test, they belong in a register, and having them as parameters meant a driver could not tune service latency without a re-synthesis.

That is not hypothetical. Chapter 10.3 computed the trade-off: a lower trigger means more frequent servicing and lower latency, a higher one means fewer interrupts and more headroom. The right value depends on the software, not on the hardware — which is the definition of something that belongs to software.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Chapter 13.2 — a parameter that sets a REGISTER'S RESET VALUE, which is
// Chapter 11.4's recommended shape for a value with a sensible default that
// a driver must still be able to tune without a re-synthesis.
logic [$clog2(RX_DEPTH+1)-1:0] rx_trigger_q;
logic [$clog2(TX_DEPTH+1)-1:0] tx_trigger_q;
...
rx_trigger_q <= RX_TRIGGER[$clog2(RX_DEPTH+1)-1:0];   // reset value
tx_trigger_q <= TX_TRIGGER[$clog2(TX_DEPTH+1)-1:0];

This is exactly the shape Chapter 11.4 §1 recommended and deferred to this module: a parameter that sets a register's reset value. The integrator gets a sensible default chosen at elaboration; the driver gets to change it.

Measured — the same four bytes, with only a register write between the two observations:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- 8. FIFO trigger level, changed at run time
  pass trigger register resets to the parameter default (RX 12, TX 4)
  pass 4 bytes do not reach the default trigger of 12
  pass SAME 4 bytes now reach a trigger of 2 — tuned at run time
  pass trigger register reads back what was written
  pass raising the trigger withdraws the condition

The change reached down to a Module 10 block. uart_fifo_watermark now takes the thresholds as inputs rather than parameters, which is the third time in three modules that attaching something new revealed a block's interface was incomplete — Chapter 11.4 §3 and Chapter 13.1 §6 being the other two. That is the normal way a block's interface is discovered, and the discipline is to change the interface rather than work around it.

A block diagram mapping four driver questions onto four registers. The question was this particular byte good is answered by the data register, which returns the byte together with its own parity and framing bits and a valid bit, because reading that status separately would race the pop. The question what is happening right now is answered by the status register, which is read-only and has no side effects so it can be polled freely and watched by a debugger. The question has anything gone wrong since I last looked is answered by the sticky error register, which is write-one-to-clear because a register that needs clearing cannot also be safe to read repeatedly. The question what should I service is answered by the interrupt status register, which shows only sources that are both pending and enabled and is read-only because every source is a level cleared by servicing its cause, with a separate raw register showing pending conditions before masking so a polling driver never has to enable an interrupt to see the hardware.was THIS byte good?per-byte, one shotwhat is happeningnow?live conditionanything wrongsince?sticky historywhat should Iservice?pending and enabledDATA 0x00byte + parity + frame +validSTATUS 0x04RO, no side effectsERR 0x0CW1CIRQ_STAT 0x14RO, maskedatomic with the popseparate read would racesafe to poll andwatchdebugger cannot disturbitneeds clearingso it cannot live inSTATUSIRQ_RAW 0x18 besideitunmasked, for pollersansweransweransweranswerbecausebecausebecauseand12
Figure 1 — where each kind of information lives, and why. The four questions a driver asks are answered by four different registers with four different lifetimes, and the placement follows from the lifetime rather than from convenience.

3. Where Fields Do Not Go

Placement decisions are easier to justify by what was rejected:

Per-byte status does not go in STATUS. It would race the pop — Chapter 13.1 §5. It goes in DATA.

Sticky errors do not go in STATUS. STATUS is read-only with no side effects so a debugger can watch it; sticky flags need clearing, and a register that needs clearing cannot also be safe to read repeatedly. They get their own W1C register.

The interrupt enable does not gate STATUS or IRQ_RAW. A condition is true whether or not anyone asked to be interrupted by it, and a polling driver must not have to enable interrupts to see the state of the hardware.

Baud rate is not in this map at all, which is this design's remaining gap. It is still a parameter, so changing it requires a re-synthesis — and by §2's test it fails for the same reason the trigger levels did: Chapter 8.3's fractional divider takes an increment value, not a different structure, so nothing is built differently. A production UART puts the divisor in a register, with the parameter supplying its reset value, exactly as §2 did for the triggers. It is left as a parameter here because the whole curriculum's timing analysis is written against a fixed rate, and that is a pedagogical reason rather than an engineering one — worth stating plainly rather than leaving as an implied endorsement.

4. What a Driver Actually Does

The map is only good if the common sequences are short.

Initialisation: write CTRL, write TRIGGER, write TIMEOUT, write IRQ_EN. Four writes, no read-modify-write, no ordering constraints among them.

Transmit a byte: read STATUS, check bit 1, write DATA. Or with DMA, nothing at all.

Receive a byte: read DATA, check bit 10, use bits 9:8 for the verdict. One access. A map that needed a status read before every data read would double the bus traffic on the hot path.

Service an interrupt: read IRQ_STAT, handle the cause. No write at all — every source here is a level, and the status register reports the cause rather than latching it, so servicing the cause is what clears the interrupt. Chapter 13.3 is about why that is the right choice and when it is not.

5. The Register Block in Three Languages

The map above is a specification. This is the thing that implements it — one module, eleven registers, and three rules that between them account for every line of it.

A side effect is qualified on the committed cycle. On APB that is psel && penable && pready, and nothing else. Chapter 13.1 §3 measured what a looser qualifier costs, and the number is not subtle: four bytes delivered out of eight, one read consuming exactly 2.0 bytes.

Status is transparent; history is sticky. IRQ_STAT reports a live condition and is therefore read-only. ERR remembers, and is therefore write-one-to-clear. The two are opposite by design and Chapter 13.3 §2 is the argument.

A control pulse is generated in hardware. The flush bits default low every cycle, so a write produces exactly one cycle and the bit reads back as zero.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ===========================================================================
//  uart_regs — Synthesizable SystemVerilog
//
//  The APB3 register block of Chapters 13.1, 13.2 and 13.5: eleven registers,
//  and exactly ONE of them has a side effect.
//
//  Three rules carry the design, and each is a chapter:
//
//    1. A SIDE EFFECT IS QUALIFIED ON THE COMMITTED CYCLE, never on select
//       alone. On APB that is psel && penable && pready, and nothing else.
//       Chapter 13.1 section 3 measures what a looser qualifier costs:
//       4 bytes delivered out of 8, one read consuming 2.0 bytes.
//
//    2. STATUS IS TRANSPARENT; HISTORY IS STICKY. IRQ_STAT reports a live
//       condition and is read-only. ERR remembers, so ERR is write-one-to-
//       clear. Chapter 13.3 section 2 is the whole argument.
//
//    3. A CONTROL PULSE IS GENERATED IN HARDWARE. The FIFO flush bits are
//       one cycle wide by construction and read back as zero, so a driver
//       cannot leave a queue held in reset.
// ===========================================================================
module uart_regs #(
    parameter int unsigned ADDR_W = 8,
    parameter int unsigned TRIG_W = 4,      // width of each FIFO trigger level
    parameter int unsigned TMO_W  = 16      // receive-idle timeout, in bit times
) (
    // ---- APB3 slave ------------------------------------------------------
    input  logic              pclk,
    input  logic              presetn,
    input  logic              psel_i,
    input  logic              penable_i,
    input  logic              pwrite_i,
    input  logic [ADDR_W-1:0] paddr_i,
    input  logic [31:0]       pwdata_i,
    output logic [31:0]       prdata_o,
    output logic              pready_o,
    output logic              pslverr_o,

    // ---- receive queue ---------------------------------------------------
    input  logic [7:0]        rx_data_i,      // head of the queue
    input  logic              rx_valid_i,     // a byte is present
    input  logic              rx_perr_i,      // parity error FOR THIS BYTE
    input  logic              rx_ferr_i,      // framing error FOR THIS BYTE
    output logic              rx_pop_o,       // ONE cycle, on a committed read

    // ---- transmit queue --------------------------------------------------
    output logic [7:0]        tx_data_o,
    output logic              tx_push_o,      // ONE cycle, on a committed write

    // ---- live status (Chapter 13.2, STATUS at 0x04) ----------------------
    input  logic              st_tx_empty_i,
    input  logic              st_tx_full_i,
    input  logic              st_rx_empty_i,
    input  logic              st_rx_trig_i,
    input  logic              st_tx_trig_i,
    input  logic              st_busy_i,
    input  logic              st_break_i,
    input  logic              st_rx_tmo_i,

    // ---- sticky error history (Chapter 9.6) ------------------------------
    input  logic              err_frame_set_i,   // one-cycle set pulses
    input  logic              err_parity_set_i,
    input  logic              err_overrun_set_i,
    input  logic              err_break_set_i,
    output logic [3:0]        err_flags_o,       // {break,overrun,parity,frame}

    // ---- interrupts (Chapter 13.3) ---------------------------------------
    input  logic [4:0]        irq_raw_i,         // LEVELS, computed elsewhere
    output logic [4:0]        irq_en_o,

    // ---- configuration out -----------------------------------------------
    output logic [1:0]        cfg_parity_o,
    output logic              cfg_flow_hw_o,
    output logic              cfg_flow_sw_o,
    output logic              cfg_loop_int_o,
    output logic              cfg_loop_line_o,
    output logic              tx_flush_o,        // ONE cycle, self-clearing
    output logic              rx_flush_o,        // ONE cycle, self-clearing
    output logic              dma_tx_en_o,
    output logic              dma_rx_en_o,
    output logic [TMO_W-1:0]  timeout_bits_o,
    output logic [TRIG_W-1:0] rx_trigger_o,
    output logic [TRIG_W-1:0] tx_trigger_o
);

    // ---- the map, from Chapter 13.2 --------------------------------------
    localparam logic [7:0] A_DATA      = 8'h00;   // RW  — the ONE side effect
    localparam logic [7:0] A_STATUS    = 8'h04;   // RO
    localparam logic [7:0] A_CTRL      = 8'h08;   // RW
    localparam logic [7:0] A_ERR       = 8'h0C;   // W1C
    localparam logic [7:0] A_IRQ_EN    = 8'h10;   // RW
    localparam logic [7:0] A_IRQ_STAT  = 8'h14;   // RO — raw & enable
    localparam logic [7:0] A_IRQ_RAW   = 8'h18;   // RO — before masking
    localparam logic [7:0] A_FIFO_CTRL = 8'h1C;   // RW — self-clearing
    localparam logic [7:0] A_DMA_CTRL  = 8'h20;   // RW
    localparam logic [7:0] A_TIMEOUT   = 8'h24;   // RW
    localparam logic [7:0] A_TRIGGER   = 8'h28;   // RW

    // =======================================================================
    //  RULE 1 — the committed cycle
    //
    //  On APB a transfer is committed in the ACCESS phase: psel and penable
    //  both high, with pready. It is unambiguous, it happens exactly once
    //  per transfer, and it is the only correct qualifier for a side effect.
    //  Writing `psel_i && !pwrite_i` instead would fire in the SETUP phase
    //  too, and pop twice per read.
    // =======================================================================
    wire access   = psel_i && penable_i && pready_o;
    wire do_read  = access && !pwrite_i;
    wire do_write = access &&  pwrite_i;

    assign pready_o  = 1'b1;      // zero wait states: nothing here is slow
    assign pslverr_o = 1'b0;      // every decoded address is legal

    assign rx_pop_o  = do_read  && (paddr_i == A_DATA);
    assign tx_push_o = do_write && (paddr_i == A_DATA);
    assign tx_data_o = pwdata_i[7:0];

    // ---- configuration registers -----------------------------------------
    logic [5:0]        ctrl_q;
    logic [4:0]        irq_en_q;
    logic [1:0]        dma_q;
    logic [TMO_W-1:0]  timeout_q;
    logic [TRIG_W-1:0] rx_trig_q, tx_trig_q;
    logic              tx_flush_q, rx_flush_q;

    assign cfg_parity_o    = ctrl_q[1:0];
    assign cfg_flow_hw_o   = ctrl_q[2];
    assign cfg_flow_sw_o   = ctrl_q[3];
    assign cfg_loop_int_o  = ctrl_q[4];
    assign cfg_loop_line_o = ctrl_q[5];
    assign irq_en_o        = irq_en_q;
    assign dma_tx_en_o     = dma_q[0];
    assign dma_rx_en_o     = dma_q[1];
    assign timeout_bits_o  = timeout_q;
    assign rx_trigger_o    = rx_trig_q;
    assign tx_trigger_o    = tx_trig_q;
    assign tx_flush_o      = tx_flush_q;
    assign rx_flush_o      = rx_flush_q;

    always_ff @(posedge pclk or negedge presetn) begin
        if (!presetn) begin
            // Chapter 11.5 section 5: every test mode resets OFF. A loopback
            // whose reset value is "on" eventually ships that way.
            ctrl_q     <= 6'b0;                 // 8N1, no flow control, no loop
            irq_en_q   <= 5'b0;
            dma_q      <= 2'b0;
            timeout_q  <= TMO_W'(40);           // 40 bit times = 4 characters
            rx_trig_q  <= TRIG_W'(8);
            tx_trig_q  <= TRIG_W'(4);
            tx_flush_q <= 1'b0;
            rx_flush_q <= 1'b0;
        end else begin
            // RULE 3 — the flush bits are pulses. Default low every cycle, so
            // a write produces exactly one cycle and the bit reads back as 0.
            tx_flush_q <= 1'b0;
            rx_flush_q <= 1'b0;
            if (do_write) begin
                case (paddr_i)
                    A_CTRL      : ctrl_q    <= pwdata_i[5:0];
                    A_IRQ_EN    : irq_en_q  <= pwdata_i[4:0];
                    A_DMA_CTRL  : dma_q     <= pwdata_i[1:0];
                    A_TIMEOUT   : timeout_q <= pwdata_i[TMO_W-1:0];
                    A_TRIGGER   : begin
                        rx_trig_q <= pwdata_i[TRIG_W-1:0];
                        tx_trig_q <= pwdata_i[16 +: TRIG_W];
                    end
                    A_FIFO_CTRL : begin
                        tx_flush_q <= pwdata_i[0];
                        rx_flush_q <= pwdata_i[1];
                    end
                    default     : ;             // RO and W1C handled elsewhere
                endcase
            end
        end
    end

    // =======================================================================
    //  RULE 2 — history is sticky, and the set wins
    //
    //  Set-dominant: if an error arrives in the same cycle the driver clears
    //  the flag, the flag stays set. The alternative loses an error whose
    //  only record is this bit. Chapter 9.6 establishes the discipline.
    // =======================================================================
    logic [3:0] err_q;
    wire  [3:0] err_set = {err_break_set_i, err_overrun_set_i,
                           err_parity_set_i, err_frame_set_i};
    wire  [3:0] err_w1c = (do_write && (paddr_i == A_ERR)) ? pwdata_i[3:0] : 4'b0;

    always_ff @(posedge pclk or negedge presetn) begin
        if (!presetn) err_q <= 4'b0;
        else          err_q <= (err_q & ~err_w1c) | err_set;   // set-dominant
    end
    assign err_flags_o = err_q;

    // ---- the read mux ------------------------------------------------------
    // IRQ_STAT is `raw & enable` with no state behind it (Chapter 13.3
    // section 4). There is nothing to write because nothing is remembered.
    wire [4:0] irq_stat = irq_raw_i & irq_en_q;
    wire [7:0] status   = {st_rx_tmo_i, st_break_i, st_busy_i, st_tx_trig_i,
                           st_rx_trig_i, st_rx_empty_i, st_tx_full_i, st_tx_empty_i};

    always_comb begin
        prdata_o = 32'b0;
        case (paddr_i)
            // Bit 10 is what makes polling DATA directly legal: without it a
            // driver must read STATUS first, and the queue can change between
            // the two accesses — so the "safe" sequence is the racy one.
            A_DATA      : prdata_o = {21'b0, rx_valid_i, rx_ferr_i,
                                      rx_perr_i, rx_data_i};
            A_STATUS    : prdata_o = {24'b0, status};
            A_CTRL      : prdata_o = {26'b0, ctrl_q};
            A_ERR       : prdata_o = {28'b0, err_q};
            A_IRQ_EN    : prdata_o = {27'b0, irq_en_q};
            A_IRQ_STAT  : prdata_o = {27'b0, irq_stat};
            A_IRQ_RAW   : prdata_o = {27'b0, irq_raw_i};
            A_FIFO_CTRL : prdata_o = 32'b0;     // pulses: always read back 0
            A_DMA_CTRL  : prdata_o = {30'b0, dma_q};
            A_TIMEOUT   : prdata_o = {{(32-TMO_W){1'b0}}, timeout_q};
            A_TRIGGER   : prdata_o = {{(16-TRIG_W){1'b0}}, tx_trig_q,
                                      {(16-TRIG_W){1'b0}}, rx_trig_q};
            default     : prdata_o = 32'b0;
        endcase
    end
endmodule

The Verilog-2001 translation is the same design with the conveniences removed: no logic, no always_ff, no sized casts, so the reset values are written with explicit widths. The indexed part-select pwdata_i[16 +: TRIG_W] is Verilog-2001 and needs no change.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  uart_regs_v — Synthesizable Verilog-2001
//
//  The APB3 register block of Chapters 13.1, 13.2 and 13.5: eleven
//  registers, and exactly ONE of them has a side effect.
//
//  Three rules carry the design, and each is a chapter:
//
//    1. A SIDE EFFECT IS QUALIFIED ON THE COMMITTED CYCLE, never on select
//       alone. On APB that is psel && penable && pready, and nothing else.
//       Chapter 13.1 section 3 measures what a looser qualifier costs:
//       4 bytes delivered out of 8, one read consuming 2.0 bytes.
//
//    2. STATUS IS TRANSPARENT; HISTORY IS STICKY. IRQ_STAT reports a live
//       condition and is read-only. ERR remembers, so ERR is write-one-to-
//       clear, and the set wins over the clear.
//
//    3. A CONTROL PULSE IS GENERATED IN HARDWARE. The FIFO flush bits are
//       one cycle wide by construction and read back as zero.
//
//  Verilog-2001 note: no `logic`, no always_ff/always_comb, and no sized
//  casts, so reset values are written with explicit widths. The structure
//  is otherwise identical to the SystemVerilog listing.
//===========================================================================
module uart_regs_v #(
    parameter ADDR_W = 8,
    parameter TRIG_W = 4,
    parameter TMO_W  = 16
) (
    // ---- APB3 slave ------------------------------------------------------
    input  wire              pclk,
    input  wire              presetn,
    input  wire              psel_i,
    input  wire              penable_i,
    input  wire              pwrite_i,
    input  wire [ADDR_W-1:0] paddr_i,
    input  wire [31:0]       pwdata_i,
    output reg  [31:0]       prdata_o,
    output wire              pready_o,
    output wire              pslverr_o,

    // ---- receive queue ---------------------------------------------------
    input  wire [7:0]        rx_data_i,
    input  wire              rx_valid_i,
    input  wire              rx_perr_i,
    input  wire              rx_ferr_i,
    output wire              rx_pop_o,

    // ---- transmit queue --------------------------------------------------
    output wire [7:0]        tx_data_o,
    output wire              tx_push_o,

    // ---- live status -----------------------------------------------------
    input  wire              st_tx_empty_i,
    input  wire              st_tx_full_i,
    input  wire              st_rx_empty_i,
    input  wire              st_rx_trig_i,
    input  wire              st_tx_trig_i,
    input  wire              st_busy_i,
    input  wire              st_break_i,
    input  wire              st_rx_tmo_i,

    // ---- sticky error history --------------------------------------------
    input  wire              err_frame_set_i,
    input  wire              err_parity_set_i,
    input  wire              err_overrun_set_i,
    input  wire              err_break_set_i,
    output wire [3:0]        err_flags_o,

    // ---- interrupts ------------------------------------------------------
    input  wire [4:0]        irq_raw_i,
    output wire [4:0]        irq_en_o,

    // ---- configuration out -----------------------------------------------
    output wire [1:0]        cfg_parity_o,
    output wire              cfg_flow_hw_o,
    output wire              cfg_flow_sw_o,
    output wire              cfg_loop_int_o,
    output wire              cfg_loop_line_o,
    output wire              tx_flush_o,
    output wire              rx_flush_o,
    output wire              dma_tx_en_o,
    output wire              dma_rx_en_o,
    output wire [TMO_W-1:0]  timeout_bits_o,
    output wire [TRIG_W-1:0] rx_trigger_o,
    output wire [TRIG_W-1:0] tx_trigger_o
);

    // ---- the map, from Chapter 13.2 --------------------------------------
    localparam [7:0] A_DATA      = 8'h00;   // RW  -- the ONE side effect
    localparam [7:0] A_STATUS    = 8'h04;   // RO
    localparam [7:0] A_CTRL      = 8'h08;   // RW
    localparam [7:0] A_ERR       = 8'h0C;   // W1C
    localparam [7:0] A_IRQ_EN    = 8'h10;   // RW
    localparam [7:0] A_IRQ_STAT  = 8'h14;   // RO -- raw & enable
    localparam [7:0] A_IRQ_RAW   = 8'h18;   // RO -- before masking
    localparam [7:0] A_FIFO_CTRL = 8'h1C;   // RW -- self-clearing
    localparam [7:0] A_DMA_CTRL  = 8'h20;   // RW
    localparam [7:0] A_TIMEOUT   = 8'h24;   // RW
    localparam [7:0] A_TRIGGER   = 8'h28;   // RW

    //=======================================================================
    //  RULE 1 -- the committed cycle
    //=======================================================================
    wire access   = psel_i && penable_i && pready_o;
    wire do_read  = access && !pwrite_i;
    wire do_write = access &&  pwrite_i;

    assign pready_o  = 1'b1;
    assign pslverr_o = 1'b0;

    assign rx_pop_o  = do_read  && (paddr_i == A_DATA);
    assign tx_push_o = do_write && (paddr_i == A_DATA);
    assign tx_data_o = pwdata_i[7:0];

    reg [5:0]        ctrl_q;
    reg [4:0]        irq_en_q;
    reg [1:0]        dma_q;
    reg [TMO_W-1:0]  timeout_q;
    reg [TRIG_W-1:0] rx_trig_q, tx_trig_q;
    reg              tx_flush_q, rx_flush_q;

    assign cfg_parity_o    = ctrl_q[1:0];
    assign cfg_flow_hw_o   = ctrl_q[2];
    assign cfg_flow_sw_o   = ctrl_q[3];
    assign cfg_loop_int_o  = ctrl_q[4];
    assign cfg_loop_line_o = ctrl_q[5];
    assign irq_en_o        = irq_en_q;
    assign dma_tx_en_o     = dma_q[0];
    assign dma_rx_en_o     = dma_q[1];
    assign timeout_bits_o  = timeout_q;
    assign rx_trigger_o    = rx_trig_q;
    assign tx_trigger_o    = tx_trig_q;
    assign tx_flush_o      = tx_flush_q;
    assign rx_flush_o      = rx_flush_q;

    always @(posedge pclk or negedge presetn) begin
        if (!presetn) begin
            // Chapter 11.5 section 5: every test mode resets OFF.
            ctrl_q     <= 6'b0;
            irq_en_q   <= 5'b0;
            dma_q      <= 2'b0;
            timeout_q  <= 40;                   // 40 bit times = 4 characters
            rx_trig_q  <= 8;
            tx_trig_q  <= 4;
            tx_flush_q <= 1'b0;
            rx_flush_q <= 1'b0;
        end else begin
            // RULE 3 -- the flush bits are pulses: default low every cycle.
            tx_flush_q <= 1'b0;
            rx_flush_q <= 1'b0;
            if (do_write) begin
                case (paddr_i)
                    A_CTRL      : ctrl_q    <= pwdata_i[5:0];
                    A_IRQ_EN    : irq_en_q  <= pwdata_i[4:0];
                    A_DMA_CTRL  : dma_q     <= pwdata_i[1:0];
                    A_TIMEOUT   : timeout_q <= pwdata_i[TMO_W-1:0];
                    A_TRIGGER   : begin
                        rx_trig_q <= pwdata_i[TRIG_W-1:0];
                        tx_trig_q <= pwdata_i[16 +: TRIG_W];
                    end
                    A_FIFO_CTRL : begin
                        tx_flush_q <= pwdata_i[0];
                        rx_flush_q <= pwdata_i[1];
                    end
                    default     : ;
                endcase
            end
        end
    end

    //=======================================================================
    //  RULE 2 -- history is sticky, and the SET WINS
    //=======================================================================
    reg  [3:0] err_q;
    wire [3:0] err_set = {err_break_set_i, err_overrun_set_i,
                          err_parity_set_i, err_frame_set_i};
    wire [3:0] err_w1c = (do_write && (paddr_i == A_ERR)) ? pwdata_i[3:0] : 4'b0;

    always @(posedge pclk or negedge presetn) begin
        if (!presetn) err_q <= 4'b0;
        else          err_q <= (err_q & ~err_w1c) | err_set;   // set-dominant
    end
    assign err_flags_o = err_q;

    // ---- the read mux ------------------------------------------------------
    wire [4:0] irq_stat = irq_raw_i & irq_en_q;
    wire [7:0] status   = {st_rx_tmo_i, st_break_i, st_busy_i, st_tx_trig_i,
                           st_rx_trig_i, st_rx_empty_i, st_tx_full_i, st_tx_empty_i};

    always @* begin
        prdata_o = 32'b0;
        case (paddr_i)
            // Bit 10 (`valid`) is what makes polling DATA directly legal.
            A_DATA      : prdata_o = {21'b0, rx_valid_i, rx_ferr_i,
                                      rx_perr_i, rx_data_i};
            A_STATUS    : prdata_o = {24'b0, status};
            A_CTRL      : prdata_o = {26'b0, ctrl_q};
            A_ERR       : prdata_o = {28'b0, err_q};
            A_IRQ_EN    : prdata_o = {27'b0, irq_en_q};
            A_IRQ_STAT  : prdata_o = {27'b0, irq_stat};
            A_IRQ_RAW   : prdata_o = {27'b0, irq_raw_i};
            A_FIFO_CTRL : prdata_o = 32'b0;     // pulses: always read back 0
            A_DMA_CTRL  : prdata_o = {30'b0, dma_q};
            A_TIMEOUT   : prdata_o = {{(32-TMO_W){1'b0}}, timeout_q};
            A_TRIGGER   : prdata_o = {{(16-TRIG_W){1'b0}}, tx_trig_q,
                                      {(16-TRIG_W){1'b0}}, rx_trig_q};
            default     : prdata_o = 32'b0;
        endcase
    end
endmodule

VHDL argues back in two places, and both are worth knowing.

It cannot read its own out ports. pready_o is an output, and the committed-cycle expression needs its value — so the design states it as a constant and drives the port from that, rather than reading the port back.

Its case statement wants a clean default. The read mux is built in a variable, initialised to zero and then overwritten per address. That makes the "default zero, then override" pattern explicit instead of relying on signal sub-element driver rules, which are correct but easy to misread.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
--===========================================================================
--  uart_regs — Synthesizable VHDL-2008
--
--  The APB3 register block of Chapters 13.1, 13.2 and 13.5: eleven
--  registers, and exactly ONE of them has a side effect.
--
--  Three rules carry the design, and each is a chapter:
--
--    1. A SIDE EFFECT IS QUALIFIED ON THE COMMITTED CYCLE, never on select
--       alone. On APB that is psel and penable with pready, and nothing
--       else. Chapter 13.1 section 3 measures what a looser qualifier
--       costs: 4 bytes delivered out of 8, one read consuming 2.0 bytes.
--
--    2. STATUS IS TRANSPARENT; HISTORY IS STICKY. IRQ_STAT reports a live
--       condition and is read-only. ERR remembers, so ERR is write-one-to-
--       clear, and the set wins over the clear.
--
--    3. A CONTROL PULSE IS GENERATED IN HARDWARE. The FIFO flush bits are
--       one cycle wide by construction and read back as zero.
--
--  VHDL note: the architecture cannot read its own `out` ports, so the read
--  data and every output consumed internally are kept as internal signals
--  and driven onto the ports concurrently. The read mux is built in a
--  VARIABLE and assigned once, which makes the "default zero, then override"
--  pattern unambiguous rather than relying on sub-element driver rules.
--===========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity uart_regs is
    generic (
        ADDR_W : positive := 8;
        TRIG_W : positive := 4;
        TMO_W  : positive := 16
    );
    port (
        -- APB3 slave
        pclk      : in  std_logic;
        presetn   : in  std_logic;
        psel_i    : in  std_logic;
        penable_i : in  std_logic;
        pwrite_i  : in  std_logic;
        paddr_i   : in  std_logic_vector(ADDR_W-1 downto 0);
        pwdata_i  : in  std_logic_vector(31 downto 0);
        prdata_o  : out std_logic_vector(31 downto 0);
        pready_o  : out std_logic;
        pslverr_o : out std_logic;

        -- receive queue
        rx_data_i  : in  std_logic_vector(7 downto 0);
        rx_valid_i : in  std_logic;
        rx_perr_i  : in  std_logic;
        rx_ferr_i  : in  std_logic;
        rx_pop_o   : out std_logic;

        -- transmit queue
        tx_data_o  : out std_logic_vector(7 downto 0);
        tx_push_o  : out std_logic;

        -- live status
        st_tx_empty_i : in std_logic;
        st_tx_full_i  : in std_logic;
        st_rx_empty_i : in std_logic;
        st_rx_trig_i  : in std_logic;
        st_tx_trig_i  : in std_logic;
        st_busy_i     : in std_logic;
        st_break_i    : in std_logic;
        st_rx_tmo_i   : in std_logic;

        -- sticky error history
        err_frame_set_i   : in  std_logic;
        err_parity_set_i  : in  std_logic;
        err_overrun_set_i : in  std_logic;
        err_break_set_i   : in  std_logic;
        err_flags_o       : out std_logic_vector(3 downto 0);

        -- interrupts
        irq_raw_i : in  std_logic_vector(4 downto 0);
        irq_en_o  : out std_logic_vector(4 downto 0);

        -- configuration out
        cfg_parity_o    : out std_logic_vector(1 downto 0);
        cfg_flow_hw_o   : out std_logic;
        cfg_flow_sw_o   : out std_logic;
        cfg_loop_int_o  : out std_logic;
        cfg_loop_line_o : out std_logic;
        tx_flush_o      : out std_logic;
        rx_flush_o      : out std_logic;
        dma_tx_en_o     : out std_logic;
        dma_rx_en_o     : out std_logic;
        timeout_bits_o  : out std_logic_vector(TMO_W-1 downto 0);
        rx_trigger_o    : out std_logic_vector(TRIG_W-1 downto 0);
        tx_trigger_o    : out std_logic_vector(TRIG_W-1 downto 0)
    );
end entity uart_regs;

architecture rtl of uart_regs is

    -- the map, from Chapter 13.2
    constant A_DATA      : std_logic_vector(7 downto 0) := x"00";  -- RW, side effect
    constant A_STATUS    : std_logic_vector(7 downto 0) := x"04";  -- RO
    constant A_CTRL      : std_logic_vector(7 downto 0) := x"08";  -- RW
    constant A_ERR       : std_logic_vector(7 downto 0) := x"0C";  -- W1C
    constant A_IRQ_EN    : std_logic_vector(7 downto 0) := x"10";  -- RW
    constant A_IRQ_STAT  : std_logic_vector(7 downto 0) := x"14";  -- RO
    constant A_IRQ_RAW   : std_logic_vector(7 downto 0) := x"18";  -- RO
    constant A_FIFO_CTRL : std_logic_vector(7 downto 0) := x"1C";  -- RW, pulses
    constant A_DMA_CTRL  : std_logic_vector(7 downto 0) := x"20";  -- RW
    constant A_TIMEOUT   : std_logic_vector(7 downto 0) := x"24";  -- RW
    constant A_TRIGGER   : std_logic_vector(7 downto 0) := x"28";  -- RW

    signal addr : std_logic_vector(7 downto 0);

    -- RULE 1: the committed cycle. pready is a constant here, so it is
    -- written as one rather than read back off the port.
    constant PREADY : std_logic := '1';
    signal access_s, do_read, do_write : std_logic;

    signal ctrl_q      : std_logic_vector(5 downto 0);
    signal irq_en_q    : std_logic_vector(4 downto 0);
    signal dma_q       : std_logic_vector(1 downto 0);
    signal timeout_q   : unsigned(TMO_W-1 downto 0);
    signal rx_trig_q   : unsigned(TRIG_W-1 downto 0);
    signal tx_trig_q   : unsigned(TRIG_W-1 downto 0);
    signal tx_flush_q  : std_logic;
    signal rx_flush_q  : std_logic;

    signal err_q   : std_logic_vector(3 downto 0);
    signal err_set : std_logic_vector(3 downto 0);
    signal err_w1c : std_logic_vector(3 downto 0);

    signal irq_stat : std_logic_vector(4 downto 0);
    signal status   : std_logic_vector(7 downto 0);

begin

    addr <= paddr_i(7 downto 0);

    pready_o  <= PREADY;
    pslverr_o <= '0';        -- every decoded address is legal

    access_s <= psel_i and penable_i and PREADY;
    do_read  <= access_s and (not pwrite_i);
    do_write <= access_s and pwrite_i;

    rx_pop_o  <= do_read  when addr = A_DATA else '0';
    tx_push_o <= do_write when addr = A_DATA else '0';
    tx_data_o <= pwdata_i(7 downto 0);

    cfg_parity_o    <= ctrl_q(1 downto 0);
    cfg_flow_hw_o   <= ctrl_q(2);
    cfg_flow_sw_o   <= ctrl_q(3);
    cfg_loop_int_o  <= ctrl_q(4);
    cfg_loop_line_o <= ctrl_q(5);
    irq_en_o        <= irq_en_q;
    dma_tx_en_o     <= dma_q(0);
    dma_rx_en_o     <= dma_q(1);
    timeout_bits_o  <= std_logic_vector(timeout_q);
    rx_trigger_o    <= std_logic_vector(rx_trig_q);
    tx_trigger_o    <= std_logic_vector(tx_trig_q);
    tx_flush_o      <= tx_flush_q;
    rx_flush_o      <= rx_flush_q;

    cfg_proc : process (pclk, presetn)
    begin
        if presetn = '0' then
            -- Chapter 11.5 section 5: every test mode resets OFF.
            ctrl_q     <= (others => '0');
            irq_en_q   <= (others => '0');
            dma_q      <= (others => '0');
            timeout_q  <= to_unsigned(40, TMO_W);   -- 40 bit times
            rx_trig_q  <= to_unsigned(8, TRIG_W);
            tx_trig_q  <= to_unsigned(4, TRIG_W);
            tx_flush_q <= '0';
            rx_flush_q <= '0';
        elsif rising_edge(pclk) then
            -- RULE 3: the flush bits are pulses -- default low every cycle.
            tx_flush_q <= '0';
            rx_flush_q <= '0';
            if do_write = '1' then
                case addr is
                    when A_CTRL     => ctrl_q   <= pwdata_i(5 downto 0);
                    when A_IRQ_EN   => irq_en_q <= pwdata_i(4 downto 0);
                    when A_DMA_CTRL => dma_q    <= pwdata_i(1 downto 0);
                    when A_TIMEOUT  =>
                        timeout_q <= unsigned(pwdata_i(TMO_W-1 downto 0));
                    when A_TRIGGER  =>
                        rx_trig_q <= unsigned(pwdata_i(TRIG_W-1 downto 0));
                        tx_trig_q <= unsigned(pwdata_i(16+TRIG_W-1 downto 16));
                    when A_FIFO_CTRL =>
                        tx_flush_q <= pwdata_i(0);
                        rx_flush_q <= pwdata_i(1);
                    when others => null;
                end case;
            end if;
        end if;
    end process cfg_proc;

    --=======================================================================
    --  RULE 2 -- history is sticky, and the SET WINS
    --
    --  If an error arrives in the same cycle the driver clears the flag, the
    --  flag stays set. The alternative loses an error whose only record is
    --  this bit. Chapter 9.6 establishes the discipline.
    --=======================================================================
    err_set <= err_break_set_i & err_overrun_set_i
             & err_parity_set_i & err_frame_set_i;
    err_w1c <= pwdata_i(3 downto 0)
               when (do_write = '1' and addr = A_ERR) else (others => '0');

    err_proc : process (pclk, presetn)
    begin
        if presetn = '0' then
            err_q <= (others => '0');
        elsif rising_edge(pclk) then
            err_q <= (err_q and (not err_w1c)) or err_set;   -- set-dominant
        end if;
    end process err_proc;

    err_flags_o <= err_q;

    -- IRQ_STAT is `raw and enable` with no state behind it. There is nothing
    -- to write because nothing is remembered. Chapter 13.3 section 4.
    irq_stat <= irq_raw_i and irq_en_q;
    status   <= st_rx_tmo_i & st_break_i & st_busy_i & st_tx_trig_i
              & st_rx_trig_i & st_rx_empty_i & st_tx_full_i & st_tx_empty_i;

    read_mux : process (all)
        variable v : std_logic_vector(31 downto 0);
    begin
        v := (others => '0');
        case addr is
            -- Bit 10 (`valid`) is what makes polling DATA directly legal:
            -- without it a driver must read STATUS first, and the queue can
            -- change between the two accesses.
            when A_DATA =>
                v(7 downto 0) := rx_data_i;
                v(8)          := rx_perr_i;
                v(9)          := rx_ferr_i;
                v(10)         := rx_valid_i;
            when A_STATUS    => v(7 downto 0) := status;
            when A_CTRL      => v(5 downto 0) := ctrl_q;
            when A_ERR       => v(3 downto 0) := err_q;
            when A_IRQ_EN    => v(4 downto 0) := irq_en_q;
            when A_IRQ_STAT  => v(4 downto 0) := irq_stat;
            when A_IRQ_RAW   => v(4 downto 0) := irq_raw_i;
            when A_FIFO_CTRL => null;          -- pulses: always read back 0
            when A_DMA_CTRL  => v(1 downto 0) := dma_q;
            when A_TIMEOUT   => v(TMO_W-1 downto 0) := std_logic_vector(timeout_q);
            when A_TRIGGER   =>
                v(TRIG_W-1 downto 0)      := std_logic_vector(rx_trig_q);
                v(16+TRIG_W-1 downto 16)  := std_logic_vector(tx_trig_q);
            when others      => null;
        end case;
        prdata_o <= v;
    end process read_mux;

end architecture rtl;

6. The Same Map as a UVM Register Model

A register map is the one part of a design that verification can describe declaratively, and UVM's register layer exists for exactly this. The value is not that it saves typing — it is that the map stops being duplicated.

Written by hand, the map exists three times: in the RTL, in the driver's header file, and in the testbench's address constants. Three copies drift, and the drift is silent until something reads the wrong offset. A register model makes the testbench's copy generated rather than typed, and gives every test uvm_reg's built-in sequences for free.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------------
//  uart_reg_block — the map of section 1, as a UVM register model
//
//  Note what each field's ACCESS POLICY says. These are not decoration: the
//  built-in sequences read them, and a policy that disagrees with the RTL
//  produces a test failure that is really a model bug. Getting them right is
//  the whole job.
// ---------------------------------------------------------------------------
class uart_reg_data extends uvm_reg;
    `uvm_object_utils(uart_reg_data)
    rand uvm_reg_field data;     // RW  — but see the callout below
         uvm_reg_field perr;     // RO  — status for THIS byte
         uvm_reg_field ferr;     // RO
         uvm_reg_field valid;    // RO

    function new(string name = "uart_reg_data");
        super.new(name, 32, UVM_NO_COVERAGE);
    endfunction

    virtual function void build();
        data  = uvm_reg_field::type_id::create("data");
        perr  = uvm_reg_field::type_id::create("perr");
        ferr  = uvm_reg_field::type_id::create("ferr");
        valid = uvm_reg_field::type_id::create("valid");
        //           parent lsb size access volatile reset has_reset is_rand indiv
        data .configure(this, 8,  0, "RW", 1, 8'h00, 0, 1, 1);
        perr .configure(this, 1,  8, "RO", 1, 1'b0,  0, 0, 1);
        ferr .configure(this, 1,  9, "RO", 1, 1'b0,  0, 0, 1);
        valid.configure(this, 1, 10, "RO", 1, 1'b0,  0, 0, 1);
    endfunction
endclass

class uart_reg_err extends uvm_reg;          // the one W1C register
    `uvm_object_utils(uart_reg_err)
    rand uvm_reg_field frame, parity, overrun, brk;

    function new(string name = "uart_reg_err");
        super.new(name, 32, UVM_NO_COVERAGE);
    endfunction

    virtual function void build();
        frame   = uvm_reg_field::type_id::create("frame");
        parity  = uvm_reg_field::type_id::create("parity");
        overrun = uvm_reg_field::type_id::create("overrun");
        brk     = uvm_reg_field::type_id::create("brk");
        // "W1C" is what makes uvm_reg_bit_bash and the built-in W1C
        // sequences do the right thing instead of reporting a mismatch.
        frame  .configure(this, 1, 0, "W1C", 1, 1'b0, 1, 1, 1);
        parity .configure(this, 1, 1, "W1C", 1, 1'b0, 1, 1, 1);
        overrun.configure(this, 1, 2, "W1C", 1, 1'b0, 1, 1, 1);
        brk    .configure(this, 1, 3, "W1C", 1, 1'b0, 1, 1, 1);
    endfunction
endclass

class uart_reg_block extends uvm_reg_block;
    `uvm_object_utils(uart_reg_block)

    rand uart_reg_data      DATA;
    rand uart_reg_status    STATUS;
    rand uart_reg_ctrl      CTRL;
    rand uart_reg_err       ERR;
    rand uart_reg_irq_en    IRQ_EN;
    rand uart_reg_irq_stat  IRQ_STAT;
    rand uart_reg_irq_raw   IRQ_RAW;
    rand uart_reg_fifo_ctrl FIFO_CTRL;
    rand uart_reg_dma_ctrl  DMA_CTRL;
    rand uart_reg_timeout   TIMEOUT;
    rand uart_reg_trigger   TRIGGER;

    uvm_reg_map apb_map;

    virtual function void build();
        apb_map = create_map("apb_map", 'h0, 4, UVM_LITTLE_ENDIAN);

        DATA = uart_reg_data::type_id::create("DATA");
        DATA.configure(this); DATA.build();
        apb_map.add_reg(DATA, 'h00, "RW");

        STATUS = uart_reg_status::type_id::create("STATUS");
        STATUS.configure(this); STATUS.build();
        apb_map.add_reg(STATUS, 'h04, "RO");

        // ... CTRL 'h08, ERR 'h0C, IRQ_EN 'h10, IRQ_STAT 'h14,
        //     IRQ_RAW 'h18, FIFO_CTRL 'h1C, DMA_CTRL 'h20,
        //     TIMEOUT 'h24, TRIGGER 'h28 — same shape
        lock_model();
    endfunction
endclass

7. Verification

Check reset values for every field. It is the cheapest test in the module and it catches a whole class of integration surprise — a loopback bit that resets high is a link that is dead on arrival.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  pass CTRL resets to 0 (8N1, no flow, no loopback)
  pass all interrupts disabled at reset
  pass no sticky errors at reset
  pass TX empty, RX empty at reset
  pass timeout defaults to 40 bit times (4 chars)
  pass trigger register resets to the parameter default (RX 12, TX 4)

Write and read back every writable field. It catches decode errors, width mismatches and fields wired to the wrong bit — and it is mechanical enough to generate from the map.

Test the reserved and unimplemented space. Reading an undecoded address returns zero here rather than an error, which is a choice: it makes a driver's probing harmless and it means a typo'd address silently does nothing. Returning a bus error instead surfaces the typo and complicates every debugger that scans the region. Either is defensible; the map's documentation must say which, because a driver will eventually depend on the answer.

Generate the test from the map, not from the RTL. A test written by reading the implementation verifies that the implementation is what it is. The map in §1 was extracted programmatically from the RTL for this chapter's accuracy — but a register test should come from the specification the driver team was given.

8. Debugging

9. What This Means on an FPGA

Word-align everything and leave gaps. Four-byte spacing costs no real resources — the decode is a comparison on a few address bits — and it means no driver ever needs a sub-word access.

Reserved space is free; renumbering is not. Leaving offsets unused costs nothing, and a register map that has shipped cannot be renumbered without breaking every driver built against it. Allocate generously the first time.

Read multiplexing is the only thing that grows. Each register adds a leg to the read mux; at eleven registers this is trivial, and at a hundred it is worth a registered read with one wait state.

The trigger registers are narrow. $clog2(DEPTH+1) bits — five for a 16-deep FIFO — and placing them at bits [4:0] and [20:16] of one word leaves both fields extendable if the depth grows.

10. Understanding Check

11. Summary

Eleven registers, word-aligned, and exactly one with a side effect — the ratio that lets a debugger inspect the peripheral and a driver poll it for free.

DATA carries the byte and its verdict in one word, because reading them separately races the pop, and its valid bit makes polling DATA directly the correct sequence rather than the risky one.

STATUS duplicates the interrupt conditions on purpose, so a polling driver never has to enable interrupts to see the hardware.

Flush bits self-clear in hardware, because a flush held asserted is a queue permanently in reset.

The trigger levels were parameters and failed Chapter 11.4's own test. Nothing is built differently for a different threshold, and the right value depends on the software — so they are now registers whose reset values come from the parameters. Verified: the same four bytes crossed the threshold after a single register write.

An elaboration check becomes a runtime clamp when its parameter becomes a register. A trigger of zero was a build error and is now clamped in hardware — and the review question that generalises is where did this parameter's legality check go?

And the gap stated plainly: baud rate is still a parameter and by the same test should not be. It is left that way for the curriculum's sake, not because it is right.

12. What Comes Next

The map has an interrupt enable, a raw status and a masked status, and this chapter has treated them as ordinary registers. They are not.

Chapter 13.3 takes interrupts seriously: which conditions deserve to be sources, why the receive-idle timeout exists at all, and the clear-semantics decision — clear-on-read versus write-one-to-clear — that a driver has to live with for the life of the chip. It also confronts the race that makes set-dominant clearing non-negotiable.

Browse the full path on the UART tutorials index. For the service-latency analysis that sets the trigger levels this chapter made writable, read back to Chapter 10.3.

Continue learning

Where this fits

Part of the UART curriculum.