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
| Offset | Name | Access | Purpose |
|---|---|---|---|
0x00 | DATA | RW | read pops the RX queue, write pushes the TX queue |
0x04 | STATUS | RO | live conditions |
0x08 | CTRL | RW | configuration |
0x0C | ERR | W1C | sticky error flags |
0x10 | IRQ_EN | RW | interrupt enable mask |
0x14 | IRQ_STAT | RO | pending and enabled — see 13.3 |
0x18 | IRQ_RAW | RO | pending before masking |
0x1C | FIFO_CTRL | RW | flush bits, self-clearing |
0x20 | DMA_CTRL | RW | DMA enables |
0x24 | TIMEOUT | RW | receive-idle timeout, in bit times |
0x28 | TRIGGER | RW | FIFO 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
| Bits | Read | Write |
|---|---|---|
[7:0] | received byte | byte 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
| Bit | Meaning |
|---|---|
| 0 | TX queue empty |
| 1 | TX queue full |
| 2 | RX queue empty |
| 3 | RX trigger reached |
| 4 | TX trigger reached |
| 5 | busy — work in progress (Chapter 11.1 §6) |
| 6 | break active now |
| 7 | receive-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
| Bits | Field |
|---|---|
[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.
// 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];
endA 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.
// 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:
-- 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 conditionThe 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.
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.
// ===========================================================================
// 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
endmoduleThe 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.
//===========================================================================
// 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
endmoduleVHDL 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.
--===========================================================================
-- 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.
// ---------------------------------------------------------------------------
// 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
endclass7. 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.
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
Related tutorials
- Related topic
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.
- Related topic
Interrupts: Sources, Enables and Clear Semantics
Which conditions deserve to be interrupt sources, why a receive-idle timeout is not optional, and the clear-semantics decision a driver lives with for the life of the chip — with a real defect found and fixed.
- Related topic
DMA Interaction and Bulk Transfer
Request and acknowledge handshaking with a DMA engine, where the residual bytes at the end of a transfer go, and a real defect that only a DMA-style reader could expose.
- Related topic
Bus Attachment and Integration Concerns
What attaching the register block to an APB- or AXI-class bus requires of the UART, what the UART must require of the integration, and the clock question — answered by measurement.
Where this fits
Part of the UART curriculum.
