UART · Module 13
UART as a Memory-Mapped Peripheral
The register block between a bus and the UART core, and the read and write side effects that make UART registers unlike memory — with a measured demonstration of getting one of them wrong.
Twelve modules have built a UART with wires for an interface: cfg_parity_i is a level, rx_trigger_o is a level, bytes move over valid/ready handshakes. Nothing about that is reachable from software.
A register block turns those wires into addresses. It sounds like clerical work and it contains the single most dangerous property in the whole peripheral: a UART's data register changes state when you read it. Memory does not do that, caches and debuggers assume it does not, and getting the condition wrong loses data silently.
1. What the Register Block Is
It is an adapter, and keeping it one is the discipline that matters. It decodes addresses, holds configuration, presents status, and converts bus transfers into the handshakes the core already speaks. It contains no UART logic whatsoever.
// The core, instantiated UNMODIFIED. Every module from 6 through 12 is
// inside it, and the register block adds no protocol behaviour at all.
uart_ip #(.CLK_HZ(CLK_HZ), .BAUD_HZ(BAUD_HZ), ...) u_core (
.cfg_parity_i(cfg_parity_q), // a register field drives a wire
.wr_data_i(wr_data), .wr_valid_i(wr_valid), .wr_ready_o(wr_ready),
.rd_data_o(rd_data), .rd_valid_o(rd_valid), .rd_ready_i(rd_ready),
...);This is what Chapter 11.1 §5 was protecting when it kept the register map out of the IP. The same core sits behind APB here, behind AXI in another project, and behind a direct hardware interface in a third, with no edit — because the boundary was drawn at wires rather than at addresses.
2. UART Registers Are Not Memory
Four properties, and every one of them breaks an assumption software brings from memory:
| Memory | UART registers | |
|---|---|---|
| reading returns what was written | yes | no — DATA reads the receive queue, writes the transmit queue |
| reading twice returns the same value | yes | no — each read consumes a byte |
| reading has no effect | yes | no — reading DATA pops the FIFO |
| writing then reading returns the value | yes | no — the value went out of a serial port |
DATA is one address naming two completely different pieces of hardware. A write pushes to the transmit queue; a read pops from the receive queue. They share an address and share nothing else. This is conventional — every 16550-derived UART does it — and it is the first thing that surprises anyone writing a driver from a memory mental model.
3. Qualifying the Side Effect
Given the above, the question is precisely on which cycle does the pop happen? — and it has exactly one right answer: on a single, committed bus transfer.
APB makes this easy to state. A transfer has a SETUP phase (psel asserted, penable low) and an ACCESS phase (psel and penable both asserted), and completes when pready is high during ACCESS.
// A side effect may fire ONLY on a committed access. In APB that is the
// ACCESS phase with pready high: exactly one cycle per transfer, never
// repeated, never speculative.
assign access = psel_i && penable_i && pready_o;
assign do_write = access && pwrite_i;
assign do_read = access && !pwrite_i;
// THE read side effect. Popping on `do_read` and not on `psel_i` alone is
// the entire point: a bus that asserts select for two cycles, or a debugger
// that reads twice, must not consume two bytes.
assign rd_ready = do_read && (paddr_i == A_DATA);The tempting error is psel_i && !pwrite_i, which looks like "the bus is reading me" and is wrong because psel is asserted for both phases of every transfer. One read then pops twice.
Measured
Two register blocks, identical except for that one line, each given the same eight bytes through internal loopback and then read back one at a time:
-- eight bytes written to each, then read back one at a time
[bad] read 1 returned 11, expected 10
[bad] read 2 returned 13, expected 11
[bad] read 3 returned 15, expected 12
correctly-qualified block : 8 of 8 bytes delivered, in order=1
psel-qualified block : 4 of 8 bytes delivered, in order=0
-> one bus read consumed 2.0 bytesHalf the data is gone, and the first read is already wrong. 0x10 was popped during the setup phase and discarded before the bus ever sampled a value, so the driver's first byte is the second byte sent. Every subsequent read skips one.
4. Which Registers Must Be Safe to Read
Deliberately, only DATA has a side effect. Everything else — status, control, error flags, interrupt state — is a plain read.
That is not an accident of convenience; it is a property worth designing for:
A debugger must be able to inspect the peripheral. If reading the status register cleared it, a developer could not look at the UART without changing it, and the act of debugging would destroy the evidence. Every register except DATA can be watched continuously with no effect on behaviour.
A driver must be able to poll. Reading STATUS in a loop to wait for room in the transmit queue is the most common thing any driver does with a UART, and it has to be free.
Verified explicitly, because a design can drift into a side effect by accident:
pass reading STATUS/IRQ_RAW does NOT consume the byteA byte was placed in the queue, STATUS was read twice and IRQ_RAW once, and the subsequent DATA read still returned the original byte.
And an empty read must not fabricate data. Reading DATA with nothing queued returns a valid bit of zero rather than a stale byte:
pass reading an empty RX reports valid=0That bit is what lets a driver poll DATA directly instead of checking STATUS first — and checking STATUS first is a race anyway, because the queue can change between the two accesses.
5. Status Travels With the Byte
Chapter 10.2 §8 made the RX FIFO wider than the data so each entry carries its own framing and parity status. The register block is where that decision pays off:
A_DATA: begin
// Status travels WITH the byte — Chapter 10.2 §8. Reading it from a
// separate register would be a race against the pop.
prdata_o[7:0] = rd_data;
prdata_o[8] = rd_perr;
prdata_o[9] = rd_ferr;
prdata_o[10] = rd_valid;
endOne read returns the byte and its verdict atomically. The alternative — a separate status register the driver reads after popping — is a race it cannot win: the pop has already advanced the queue, so the status register now describes a different byte, or no byte at all. Putting the per-byte status in the same word as the data removes the race by construction rather than by careful driver ordering.
6. The Port That Was Missing, Again
Chapter 11.4 §3 argued that a hierarchical reference is a missing port, and that blocks acquire their real interfaces from being used. Attaching a bus proved the point immediately.
The receive-idle timeout of Chapter 13.3 needs to know when a character arrives. The core did not publish that, and the first version of this register block reached inside for it:
assign rx_activity = (rd_valid && rd_ready) || (u_core.rx_push); // WRONGSame defect, same fix: the core now publishes the event, and the register block reads a port.
// in uart_ip's port list
output logic rx_push_evt_o
...
assign rx_push_evt_o = rx_push;
// in the register block
assign rx_activity = (rd_valid && rd_ready) || rx_push_evt;This is the expected pattern rather than a failure of the earlier design. A block's interface is discovered by attaching it to things, and each new attachment finds what the previous one did not need. What matters is that the response is a port and not a reach-in — and the regression confirms the addition changed nothing: 86 checks, 0 failures across the Module 11 and 12 suites.
7. Counting the Side Effect
Section 3 established that the side effect must be qualified on the committed cycle, and measured what a looser qualifier costs. Turning that into a testbench needs one idea, and it is the idea the rest of this section is about.
Do not check that the read returned the right byte. Count the pops.
A design that pops twice per read still returns a correct-looking byte on the
first access. It returns 0x41 when 0x41 was expected, and a testbench
comparing data against a model will report a pass — then report a confusing
mismatch several accesses later, by which point the queue and the model have
diverged for reasons that no longer point at the cause. Section 3's "one read
consuming exactly 2.0 bytes" is only visible as arithmetic: pops issued
versus reads requested.
So the receive queue is modelled inside the testbench, every assertion of
rx_pop_o is counted, and each claim about the side effect is stated as a
sum:
| Claim | Stated as |
|---|---|
| a committed read pops once | n_pop increases by exactly 1 |
| seven reads pop seven bytes | n_pop increases by exactly 7 |
| the SETUP phase commits nothing | n_pop unchanged over six cycles of psel with no penable |
| an aborted transfer commits nothing | n_pop unchanged after psel is withdrawn |
| the ten other registers are safe | n_pop and n_push unchanged across ten reads |
//===========================================================================
// tb_uart_regs — self-checking SystemVerilog testbench
//
// The property this suite exists for is ONE POP PER READ. Chapter 13.1
// section 3 measured what a mis-qualified side effect costs -- 4 bytes
// delivered out of 8, one read consuming 2.0 bytes -- and the only way to
// see that in a testbench is to COUNT the pops the design generates and
// compare against the number of reads the stimulus issued.
//
// So the receive queue is modelled, the pops are counted, and every check
// about the side effect is stated as an arithmetic claim about those two
// numbers. Checking that "a read returned the right byte" would pass on a
// design that popped twice.
//
// INDEPENDENCE: the expected byte always comes from the queue model the
// testbench itself loaded, never from reading the DUT's state.
//===========================================================================
`timescale 1ns/1ps
module tb_uart_regs;
localparam TRIG_W = 4, TMO_W = 16;
localparam [7:0] A_DATA = 8'h00;
localparam [7:0] A_STATUS = 8'h04;
localparam [7:0] A_CTRL = 8'h08;
localparam [7:0] A_ERR = 8'h0C;
localparam [7:0] A_IRQ_EN = 8'h10;
localparam [7:0] A_IRQ_STAT = 8'h14;
localparam [7:0] A_IRQ_RAW = 8'h18;
localparam [7:0] A_FIFO_CTRL = 8'h1C;
localparam [7:0] A_DMA_CTRL = 8'h20;
localparam [7:0] A_TIMEOUT = 8'h24;
localparam [7:0] A_TRIGGER = 8'h28;
logic pclk = 1'b0;
always #5 pclk = ~pclk;
logic presetn = 1'b0;
logic psel = 1'b0, penable = 1'b0, pwrite = 1'b0;
logic [7:0] paddr = 8'h00;
logic [31:0] pwdata = 32'b0;
wire [31:0] prdata;
wire pready, pslverr;
// ---- the receive queue, modelled in the testbench --------------------
logic [7:0] q_data [0:63];
logic q_perr [0:63];
logic q_ferr [0:63];
int q_head = 0, q_tail = 0;
wire q_ne = (q_head != q_tail);
wire [7:0] rx_data = q_ne ? q_data[q_head] : 8'hFF;
wire rx_perr = q_ne ? q_perr[q_head] : 1'b0;
wire rx_ferr = q_ne ? q_ferr[q_head] : 1'b0;
wire rx_valid = q_ne;
wire rx_pop;
// ---- pop and push counters: the whole point of the suite -------------
int n_pop = 0, n_push = 0, pop_when_empty = 0;
wire [7:0] tx_data;
wire tx_push;
logic [7:0] tx_seen [0:63];
always @(posedge pclk) if (presetn) begin
// SystemVerilog immediate assertions. These restate, at the instant of
// the event, what the counted checks establish by arithmetic at the
// end: a side effect may occur ONLY in a committed access phase.
a_pop_committed: assert (!rx_pop || (psel && penable && pready && !pwrite
&& paddr == A_DATA))
else $error("rx_pop outside a committed DATA read");
a_push_committed: assert (!tx_push || (psel && penable && pready && pwrite
&& paddr == A_DATA))
else $error("tx_push outside a committed DATA write");
if (rx_pop) begin
n_pop++;
if (!q_ne) pop_when_empty++;
else q_head++;
end
if (tx_push) begin
tx_seen[n_push] = tx_data;
n_push++;
end
end
task q_load;
input [7:0] d; input p; input f;
begin q_data[q_tail]=d; q_perr[q_tail]=p; q_ferr[q_tail]=f; q_tail=q_tail+1; end
endtask
// ---- status and error stimulus ---------------------------------------
logic st_tx_empty=1'b1, st_tx_full=1'b0, st_rx_empty=1'b1, st_rx_trig=1'b0;
logic st_tx_trig=1'b0, st_busy=1'b0, st_break=1'b0, st_rx_tmo=1'b0;
logic e_frame=1'b0, e_parity=1'b0, e_overrun=1'b0, e_break=1'b0;
logic [4:0] irq_raw = 5'b0;
wire [3:0] err_flags;
wire [4:0] irq_en;
wire [1:0] cfg_parity;
wire cfg_hw, cfg_sw, cfg_li, cfg_ll, tx_flush, rx_flush, dma_tx_en, dma_rx_en;
wire [TMO_W-1:0] timeout_bits;
wire [TRIG_W-1:0] rx_trigger, tx_trigger;
uart_regs #(.ADDR_W(8), .TRIG_W(TRIG_W), .TMO_W(TMO_W)) dut (
.pclk(pclk), .presetn(presetn), .psel_i(psel), .penable_i(penable),
.pwrite_i(pwrite), .paddr_i(paddr), .pwdata_i(pwdata),
.prdata_o(prdata), .pready_o(pready), .pslverr_o(pslverr),
.rx_data_i(rx_data), .rx_valid_i(rx_valid), .rx_perr_i(rx_perr),
.rx_ferr_i(rx_ferr), .rx_pop_o(rx_pop),
.tx_data_o(tx_data), .tx_push_o(tx_push),
.st_tx_empty_i(st_tx_empty), .st_tx_full_i(st_tx_full),
.st_rx_empty_i(st_rx_empty), .st_rx_trig_i(st_rx_trig),
.st_tx_trig_i(st_tx_trig), .st_busy_i(st_busy),
.st_break_i(st_break), .st_rx_tmo_i(st_rx_tmo),
.err_frame_set_i(e_frame), .err_parity_set_i(e_parity),
.err_overrun_set_i(e_overrun), .err_break_set_i(e_break),
.err_flags_o(err_flags),
.irq_raw_i(irq_raw), .irq_en_o(irq_en),
.cfg_parity_o(cfg_parity), .cfg_flow_hw_o(cfg_hw), .cfg_flow_sw_o(cfg_sw),
.cfg_loop_int_o(cfg_li), .cfg_loop_line_o(cfg_ll),
.tx_flush_o(tx_flush), .rx_flush_o(rx_flush),
.dma_tx_en_o(dma_tx_en), .dma_rx_en_o(dma_rx_en),
.timeout_bits_o(timeout_bits),
.rx_trigger_o(rx_trigger), .tx_trigger_o(tx_trigger));
// ---- flush pulses must be exactly one cycle wide ---------------------
int txf_cycles = 0, rxf_cycles = 0, txf_wide = 0, rxf_wide = 0;
logic txf_prev = 1'b0, rxf_prev = 1'b0;
always @(posedge pclk) if (presetn) begin
if (tx_flush) begin txf_cycles = txf_cycles+1; if (txf_prev) txf_wide=txf_wide+1; end
if (rx_flush) begin rxf_cycles = rxf_cycles+1; if (rxf_prev) rxf_wide=rxf_wide+1; end
txf_prev = tx_flush; rxf_prev = rx_flush;
end
// ---- APB3 driver -----------------------------------------------------
logic [31:0] rd;
task apb_write;
input [7:0] a; input [31:0] d;
begin
@(negedge pclk); psel=1'b1; pwrite=1'b1; paddr=a; pwdata=d; penable=1'b0;
@(negedge pclk); penable=1'b1; // ACCESS phase
@(posedge pclk);
@(negedge pclk); psel=1'b0; penable=1'b0; pwrite=1'b0;
end
endtask
task apb_read;
input [7:0] a;
begin
@(negedge pclk); psel=1'b1; pwrite=1'b0; paddr=a; penable=1'b0;
@(negedge pclk); penable=1'b1; // ACCESS phase
#1 rd = prdata; // sampled BEFORE the committing edge, so the
// value read is the one the pop is about to remove
@(posedge pclk);
@(negedge pclk); psel=1'b0; penable=1'b0;
end
endtask
int checks = 0, failures = 0;
task automatic check(input logic cond, input string name);
checks++;
if (cond) $display(" PASS %0s", name);
else begin failures++; $display(" FAIL %0s", name); end
endtask
int i, base_pop, base_push;
initial begin
#500_000;
$display(" FAIL watchdog: simulation did not finish");
$display("== %0d checks, %0d failures ==", checks+1, failures+1);
$display(" RESULT: SYSTEMVERILOG REGS TESTS FAILED (timeout)");
$finish;
end
initial begin
$display("== uart_regs : self-checking SystemVerilog testbench ==");
presetn = 1'b0;
repeat (4) @(negedge pclk);
presetn = 1'b1;
repeat (2) @(negedge pclk);
//=== reset values ==================================================
apb_read(A_CTRL);
check(rd == 32'h0, "reset: CTRL is 0 -- 8N1, no flow control, no loopback");
apb_read(A_IRQ_EN);
check(rd == 32'h0, "reset: IRQ_EN is 0 -- nothing enabled");
apb_read(A_DMA_CTRL);
check(rd == 32'h0, "reset: DMA_CTRL is 0");
apb_read(A_TIMEOUT);
check(rd == 32'd40, "reset: TIMEOUT is 40 bit times -- four characters");
apb_read(A_TRIGGER);
check(rd[TRIG_W-1:0] == 4'd8 && rd[16 +: TRIG_W] == 4'd4,
"reset: TRIGGER is rx=8, tx=4");
check(pready === 1'b1 && pslverr === 1'b0,
"APB: pready always high, pslverr always low");
//=== THE side effect: one pop per read =============================
for (i = 0; i < 8; i++) q_load(8'h40 + i[7:0], 1'b0, 1'b0);
base_pop = n_pop;
apb_read(A_DATA);
check(n_pop - base_pop == 1, "a committed DATA read pops EXACTLY ONCE");
check(rd[7:0] == 8'h40, "and returns the byte at the head");
check(rd[10] === 1'b1, "valid (bit 10) is set while the queue has data");
base_pop = n_pop;
for (i = 0; i < 7; i++) apb_read(A_DATA);
check(n_pop - base_pop == 7, "seven reads pop exactly seven bytes");
check(rd[7:0] == 8'h47, "the last read returns the last byte loaded");
//=== a read of an EMPTY queue still pops nothing it should not =====
base_pop = n_pop;
apb_read(A_DATA);
check(rd[10] === 1'b0, "valid is CLEAR when the queue is empty");
check(pop_when_empty == 1,
"the pop still fires -- the queue, not the register block, guards it");
//=== the SETUP phase must not commit anything =======================
// A side effect qualified on psel alone would fire here. This is the
// defect Chapter 13.1 section 3 measured.
for (i = 0; i < 4; i++) q_load(8'h90 + i[7:0], 1'b0, 1'b0);
base_pop = n_pop;
@(negedge pclk); psel=1'b1; pwrite=1'b0; paddr=A_DATA; penable=1'b0;
repeat (6) @(negedge pclk); // sit in SETUP for six cycles
check(n_pop - base_pop == 0, "six cycles in the SETUP phase pop NOTHING");
@(negedge pclk); psel=1'b0; // abort without ever asserting penable
repeat (2) @(negedge pclk);
check(n_pop - base_pop == 0, "an aborted transfer pops nothing at all");
//=== per-byte status travels with the byte ==========================
q_load(8'hC3, 1'b1, 1'b0); // parity error on this byte
q_load(8'hC4, 1'b0, 1'b1); // framing error on this byte
apb_read(A_DATA);
check(rd[7:0] == 8'h90, "the aborted transfer left the queue untouched");
for (i = 0; i < 3; i++) apb_read(A_DATA);
apb_read(A_DATA);
check(rd[7:0] == 8'hC3 && rd[8] === 1'b1 && rd[9] === 1'b0,
"DATA carries THIS byte's parity error in bit 8");
apb_read(A_DATA);
check(rd[7:0] == 8'hC4 && rd[8] === 1'b0 && rd[9] === 1'b1,
"and THIS byte's framing error in bit 9");
//=== every other register is safe to read ===========================
// Chapter 13.1 section 4. A debugger reads these; none may disturb
// the design.
for (i = 0; i < 4; i++) q_load(8'h70 + i[7:0], 1'b0, 1'b0);
base_pop = n_pop; base_push = n_push;
apb_read(A_STATUS); apb_read(A_CTRL); apb_read(A_ERR);
apb_read(A_IRQ_EN); apb_read(A_IRQ_STAT); apb_read(A_IRQ_RAW);
apb_read(A_FIFO_CTRL);apb_read(A_DMA_CTRL); apb_read(A_TIMEOUT);
apb_read(A_TRIGGER);
check(n_pop - base_pop == 0 && n_push - base_push == 0,
"reading all ten non-DATA registers has NO side effect");
//=== writes ==========================================================
base_push = n_push;
apb_write(A_DATA, 32'h000000A5);
check(n_push - base_push == 1, "a committed DATA write pushes EXACTLY ONCE");
check(tx_seen[n_push-1] == 8'hA5, "and pushes the byte that was written");
apb_write(A_CTRL, 32'h0000003F);
apb_read(A_CTRL);
check(rd[5:0] == 6'h3F, "CTRL writes and reads back");
check(cfg_parity == 2'b11 && cfg_hw && cfg_sw && cfg_li && cfg_ll,
"and every CTRL field reaches its output");
apb_write(A_CTRL, 32'h0);
apb_write(A_TIMEOUT, 32'd120);
apb_read(A_TIMEOUT);
check(rd == 32'd120 && timeout_bits == 16'd120, "TIMEOUT is writable");
apb_write(A_TRIGGER, (32'd2 << 16) | 32'd12);
check(rx_trigger == 4'd12 && tx_trigger == 4'd2,
"TRIGGER carries both levels in one word");
//=== the flush bits are PULSES ======================================
base_pop = txf_cycles;
apb_write(A_FIFO_CTRL, 32'h3); // both flushes
repeat (4) @(negedge pclk);
check(txf_cycles - base_pop == 1, "a flush write produces ONE cycle, not a level");
check(txf_wide == 0 && rxf_wide == 0, "no flush pulse was ever wider than one cycle");
apb_read(A_FIFO_CTRL);
check(rd == 32'h0, "FIFO_CTRL always reads back 0 -- it holds no state");
//=== ERR: sticky history, write-one-to-clear ========================
@(negedge pclk) e_frame = 1'b1;
@(negedge pclk) e_frame = 1'b0;
repeat (2) @(negedge pclk);
apb_read(A_ERR);
check(rd[0] === 1'b1, "ERR bit 0 is STICKY -- a one-cycle pulse is remembered");
@(negedge pclk) e_parity = 1'b1;
@(negedge pclk) e_parity = 1'b0;
repeat (2) @(negedge pclk);
apb_write(A_ERR, 32'h1); // clear ONLY bit 0
apb_read(A_ERR);
check(rd[0] === 1'b0 && rd[1] === 1'b1,
"W1C clears only the bits written, and leaves the others alone");
apb_write(A_ERR, 32'hF);
apb_read(A_ERR);
check(rd[3:0] == 4'h0, "writing all ones clears every flag");
//=== ERR is SET-DOMINANT ============================================
// If an error arrives in the same cycle the driver clears the flag,
// the flag must stay set: its only record is this bit.
@(negedge pclk); psel=1'b1; pwrite=1'b1; paddr=A_ERR; pwdata=32'hF; penable=1'b0;
@(negedge pclk); penable=1'b1; e_overrun=1'b1; // set DURING the clear
@(posedge pclk);
@(negedge pclk); psel=1'b0; penable=1'b0; pwrite=1'b0; e_overrun=1'b0;
repeat (2) @(negedge pclk);
apb_read(A_ERR);
check(rd[2] === 1'b1,
"set beats clear in the same cycle -- the error is not lost");
apb_write(A_ERR, 32'hF);
//=== interrupts: transparent, not latched ===========================
@(negedge pclk) irq_raw = 5'b00101;
apb_write(A_IRQ_EN, 32'b00001);
apb_read(A_IRQ_RAW);
check(rd[4:0] == 5'b00101, "IRQ_RAW reports every condition, enabled or not");
apb_read(A_IRQ_STAT);
check(rd[4:0] == 5'b00001, "IRQ_STAT reports only what is ENABLED");
apb_write(A_IRQ_STAT, 32'h1F); // try to acknowledge it
apb_read(A_IRQ_STAT);
check(rd[4:0] == 5'b00001,
"a write to IRQ_STAT changes NOTHING -- a level has no acknowledgement");
@(negedge pclk) irq_raw = 5'b00100; // service the cause instead
apb_read(A_IRQ_STAT);
check(rd[4:0] == 5'b00000,
"removing the cause clears it, with no write at all");
//=== an unmapped address is inert ===================================
base_pop = n_pop; base_push = n_push;
apb_write(8'hF0, 32'hFFFFFFFF);
apb_read(8'hF0);
check(rd == 32'h0, "an unmapped address reads as zero");
check(n_pop - base_pop == 0 && n_push - base_push == 0,
"and writing one disturbs nothing");
apb_read(A_CTRL);
check(rd == 32'h0, "CTRL was not corrupted by the unmapped write");
//=== whole-run accounting ===========================================
check(pop_when_empty == 1,
"across the whole run, exactly one pop was issued on an empty queue");
$display("== %0d checks, %0d failures ==", checks, failures);
if (failures == 0) $display(" RESULT: ALL SYSTEMVERILOG REGS TESTS PASSED");
else $display(" RESULT: SYSTEMVERILOG REGS TESTS FAILED");
$finish;
end
endmodule//===========================================================================
// tb_uart_regs_v — self-checking Verilog-2001 testbench
//
// The property this suite exists for is ONE POP PER READ. Chapter 13.1
// section 3 measured what a mis-qualified side effect costs -- 4 bytes
// delivered out of 8, one read consuming 2.0 bytes -- and the only way to
// see that in a testbench is to COUNT the pops the design generates and
// compare against the number of reads the stimulus issued.
//
// So the receive queue is modelled, the pops are counted, and every check
// about the side effect is stated as an arithmetic claim about those two
// numbers. Checking that "a read returned the right byte" would pass on a
// design that popped twice.
//
// INDEPENDENCE: the expected byte always comes from the queue model the
// testbench itself loaded, never from reading the DUT's state.
//===========================================================================
`timescale 1ns/1ps
module tb_uart_regs_v;
localparam TRIG_W = 4, TMO_W = 16;
localparam [7:0] A_DATA = 8'h00;
localparam [7:0] A_STATUS = 8'h04;
localparam [7:0] A_CTRL = 8'h08;
localparam [7:0] A_ERR = 8'h0C;
localparam [7:0] A_IRQ_EN = 8'h10;
localparam [7:0] A_IRQ_STAT = 8'h14;
localparam [7:0] A_IRQ_RAW = 8'h18;
localparam [7:0] A_FIFO_CTRL = 8'h1C;
localparam [7:0] A_DMA_CTRL = 8'h20;
localparam [7:0] A_TIMEOUT = 8'h24;
localparam [7:0] A_TRIGGER = 8'h28;
reg pclk = 1'b0;
always #5 pclk = ~pclk;
reg presetn = 1'b0;
reg psel = 1'b0, penable = 1'b0, pwrite = 1'b0;
reg [7:0] paddr = 8'h00;
reg [31:0] pwdata = 32'b0;
wire [31:0] prdata;
wire pready, pslverr;
// ---- the receive queue, modelled in the testbench --------------------
reg [7:0] q_data [0:63];
reg q_perr [0:63];
reg q_ferr [0:63];
integer q_head = 0, q_tail = 0;
wire q_ne = (q_head != q_tail);
wire [7:0] rx_data = q_ne ? q_data[q_head] : 8'hFF;
wire rx_perr = q_ne ? q_perr[q_head] : 1'b0;
wire rx_ferr = q_ne ? q_ferr[q_head] : 1'b0;
wire rx_valid = q_ne;
wire rx_pop;
// ---- pop and push counters: the whole point of the suite -------------
integer n_pop = 0, n_push = 0, pop_when_empty = 0;
wire [7:0] tx_data;
wire tx_push;
reg [7:0] tx_seen [0:63];
always @(posedge pclk) if (presetn) begin
if (rx_pop) begin
n_pop = n_pop + 1;
if (!q_ne) pop_when_empty = pop_when_empty + 1;
else q_head = q_head + 1;
end
if (tx_push) begin
tx_seen[n_push] = tx_data;
n_push = n_push + 1;
end
end
task q_load;
input [7:0] d; input p; input f;
begin q_data[q_tail]=d; q_perr[q_tail]=p; q_ferr[q_tail]=f; q_tail=q_tail+1; end
endtask
// ---- status and error stimulus ---------------------------------------
reg st_tx_empty=1'b1, st_tx_full=1'b0, st_rx_empty=1'b1, st_rx_trig=1'b0;
reg st_tx_trig=1'b0, st_busy=1'b0, st_break=1'b0, st_rx_tmo=1'b0;
reg e_frame=1'b0, e_parity=1'b0, e_overrun=1'b0, e_break=1'b0;
reg [4:0] irq_raw = 5'b0;
wire [3:0] err_flags;
wire [4:0] irq_en;
wire [1:0] cfg_parity;
wire cfg_hw, cfg_sw, cfg_li, cfg_ll, tx_flush, rx_flush, dma_tx_en, dma_rx_en;
wire [TMO_W-1:0] timeout_bits;
wire [TRIG_W-1:0] rx_trigger, tx_trigger;
uart_regs_v #(.ADDR_W(8), .TRIG_W(TRIG_W), .TMO_W(TMO_W)) dut (
.pclk(pclk), .presetn(presetn), .psel_i(psel), .penable_i(penable),
.pwrite_i(pwrite), .paddr_i(paddr), .pwdata_i(pwdata),
.prdata_o(prdata), .pready_o(pready), .pslverr_o(pslverr),
.rx_data_i(rx_data), .rx_valid_i(rx_valid), .rx_perr_i(rx_perr),
.rx_ferr_i(rx_ferr), .rx_pop_o(rx_pop),
.tx_data_o(tx_data), .tx_push_o(tx_push),
.st_tx_empty_i(st_tx_empty), .st_tx_full_i(st_tx_full),
.st_rx_empty_i(st_rx_empty), .st_rx_trig_i(st_rx_trig),
.st_tx_trig_i(st_tx_trig), .st_busy_i(st_busy),
.st_break_i(st_break), .st_rx_tmo_i(st_rx_tmo),
.err_frame_set_i(e_frame), .err_parity_set_i(e_parity),
.err_overrun_set_i(e_overrun), .err_break_set_i(e_break),
.err_flags_o(err_flags),
.irq_raw_i(irq_raw), .irq_en_o(irq_en),
.cfg_parity_o(cfg_parity), .cfg_flow_hw_o(cfg_hw), .cfg_flow_sw_o(cfg_sw),
.cfg_loop_int_o(cfg_li), .cfg_loop_line_o(cfg_ll),
.tx_flush_o(tx_flush), .rx_flush_o(rx_flush),
.dma_tx_en_o(dma_tx_en), .dma_rx_en_o(dma_rx_en),
.timeout_bits_o(timeout_bits),
.rx_trigger_o(rx_trigger), .tx_trigger_o(tx_trigger));
// ---- flush pulses must be exactly one cycle wide ---------------------
integer txf_cycles = 0, rxf_cycles = 0, txf_wide = 0, rxf_wide = 0;
reg txf_prev = 1'b0, rxf_prev = 1'b0;
always @(posedge pclk) if (presetn) begin
if (tx_flush) begin txf_cycles = txf_cycles+1; if (txf_prev) txf_wide=txf_wide+1; end
if (rx_flush) begin rxf_cycles = rxf_cycles+1; if (rxf_prev) rxf_wide=rxf_wide+1; end
txf_prev = tx_flush; rxf_prev = rx_flush;
end
// ---- APB3 driver -----------------------------------------------------
reg [31:0] rd;
task apb_write;
input [7:0] a; input [31:0] d;
begin
@(negedge pclk); psel=1'b1; pwrite=1'b1; paddr=a; pwdata=d; penable=1'b0;
@(negedge pclk); penable=1'b1; // ACCESS phase
@(posedge pclk);
@(negedge pclk); psel=1'b0; penable=1'b0; pwrite=1'b0;
end
endtask
task apb_read;
input [7:0] a;
begin
@(negedge pclk); psel=1'b1; pwrite=1'b0; paddr=a; penable=1'b0;
@(negedge pclk); penable=1'b1; // ACCESS phase
#1 rd = prdata; // sampled BEFORE the committing edge, so the
// value read is the one the pop is about to remove
@(posedge pclk);
@(negedge pclk); psel=1'b0; penable=1'b0;
end
endtask
integer checks = 0, failures = 0;
task check;
input cond;
input [8*80-1:0] name;
begin
checks = checks + 1;
if (cond) $display(" PASS %0s", name);
else begin failures = failures + 1; $display(" FAIL %0s", name); end
end
endtask
integer i, base_pop, base_push;
initial begin
#500_000;
$display(" FAIL watchdog: simulation did not finish");
$display("== %0d checks, %0d failures ==", checks+1, failures+1);
$display(" RESULT: VERILOG REGS TESTS FAILED (timeout)");
$finish;
end
initial begin
$display("== uart_regs_v : self-checking Verilog testbench ==");
presetn = 1'b0;
repeat (4) @(negedge pclk);
presetn = 1'b1;
repeat (2) @(negedge pclk);
//=== reset values ==================================================
apb_read(A_CTRL);
check(rd == 32'h0, "reset: CTRL is 0 -- 8N1, no flow control, no loopback");
apb_read(A_IRQ_EN);
check(rd == 32'h0, "reset: IRQ_EN is 0 -- nothing enabled");
apb_read(A_DMA_CTRL);
check(rd == 32'h0, "reset: DMA_CTRL is 0");
apb_read(A_TIMEOUT);
check(rd == 32'd40, "reset: TIMEOUT is 40 bit times -- four characters");
apb_read(A_TRIGGER);
check(rd[TRIG_W-1:0] == 4'd8 && rd[16 +: TRIG_W] == 4'd4,
"reset: TRIGGER is rx=8, tx=4");
check(pready === 1'b1 && pslverr === 1'b0,
"APB: pready always high, pslverr always low");
//=== THE side effect: one pop per read =============================
for (i = 0; i < 8; i = i + 1) q_load(8'h40 + i[7:0], 1'b0, 1'b0);
base_pop = n_pop;
apb_read(A_DATA);
check(n_pop - base_pop == 1, "a committed DATA read pops EXACTLY ONCE");
check(rd[7:0] == 8'h40, "and returns the byte at the head");
check(rd[10] === 1'b1, "valid (bit 10) is set while the queue has data");
base_pop = n_pop;
for (i = 0; i < 7; i = i + 1) apb_read(A_DATA);
check(n_pop - base_pop == 7, "seven reads pop exactly seven bytes");
check(rd[7:0] == 8'h47, "the last read returns the last byte loaded");
//=== a read of an EMPTY queue still pops nothing it should not =====
base_pop = n_pop;
apb_read(A_DATA);
check(rd[10] === 1'b0, "valid is CLEAR when the queue is empty");
check(pop_when_empty == 1,
"the pop still fires -- the queue, not the register block, guards it");
//=== the SETUP phase must not commit anything =======================
// A side effect qualified on psel alone would fire here. This is the
// defect Chapter 13.1 section 3 measured.
for (i = 0; i < 4; i = i + 1) q_load(8'h90 + i[7:0], 1'b0, 1'b0);
base_pop = n_pop;
@(negedge pclk); psel=1'b1; pwrite=1'b0; paddr=A_DATA; penable=1'b0;
repeat (6) @(negedge pclk); // sit in SETUP for six cycles
check(n_pop - base_pop == 0, "six cycles in the SETUP phase pop NOTHING");
@(negedge pclk); psel=1'b0; // abort without ever asserting penable
repeat (2) @(negedge pclk);
check(n_pop - base_pop == 0, "an aborted transfer pops nothing at all");
//=== per-byte status travels with the byte ==========================
q_load(8'hC3, 1'b1, 1'b0); // parity error on this byte
q_load(8'hC4, 1'b0, 1'b1); // framing error on this byte
apb_read(A_DATA);
check(rd[7:0] == 8'h90, "the aborted transfer left the queue untouched");
for (i = 0; i < 3; i = i + 1) apb_read(A_DATA);
apb_read(A_DATA);
check(rd[7:0] == 8'hC3 && rd[8] === 1'b1 && rd[9] === 1'b0,
"DATA carries THIS byte's parity error in bit 8");
apb_read(A_DATA);
check(rd[7:0] == 8'hC4 && rd[8] === 1'b0 && rd[9] === 1'b1,
"and THIS byte's framing error in bit 9");
//=== every other register is safe to read ===========================
// Chapter 13.1 section 4. A debugger reads these; none may disturb
// the design.
for (i = 0; i < 4; i = i + 1) q_load(8'h70 + i[7:0], 1'b0, 1'b0);
base_pop = n_pop; base_push = n_push;
apb_read(A_STATUS); apb_read(A_CTRL); apb_read(A_ERR);
apb_read(A_IRQ_EN); apb_read(A_IRQ_STAT); apb_read(A_IRQ_RAW);
apb_read(A_FIFO_CTRL);apb_read(A_DMA_CTRL); apb_read(A_TIMEOUT);
apb_read(A_TRIGGER);
check(n_pop - base_pop == 0 && n_push - base_push == 0,
"reading all ten non-DATA registers has NO side effect");
//=== writes ==========================================================
base_push = n_push;
apb_write(A_DATA, 32'h000000A5);
check(n_push - base_push == 1, "a committed DATA write pushes EXACTLY ONCE");
check(tx_seen[n_push-1] == 8'hA5, "and pushes the byte that was written");
apb_write(A_CTRL, 32'h0000003F);
apb_read(A_CTRL);
check(rd[5:0] == 6'h3F, "CTRL writes and reads back");
check(cfg_parity == 2'b11 && cfg_hw && cfg_sw && cfg_li && cfg_ll,
"and every CTRL field reaches its output");
apb_write(A_CTRL, 32'h0);
apb_write(A_TIMEOUT, 32'd120);
apb_read(A_TIMEOUT);
check(rd == 32'd120 && timeout_bits == 16'd120, "TIMEOUT is writable");
apb_write(A_TRIGGER, (32'd2 << 16) | 32'd12);
check(rx_trigger == 4'd12 && tx_trigger == 4'd2,
"TRIGGER carries both levels in one word");
//=== the flush bits are PULSES ======================================
base_pop = txf_cycles;
apb_write(A_FIFO_CTRL, 32'h3); // both flushes
repeat (4) @(negedge pclk);
check(txf_cycles - base_pop == 1, "a flush write produces ONE cycle, not a level");
check(txf_wide == 0 && rxf_wide == 0, "no flush pulse was ever wider than one cycle");
apb_read(A_FIFO_CTRL);
check(rd == 32'h0, "FIFO_CTRL always reads back 0 -- it holds no state");
//=== ERR: sticky history, write-one-to-clear ========================
@(negedge pclk) e_frame = 1'b1;
@(negedge pclk) e_frame = 1'b0;
repeat (2) @(negedge pclk);
apb_read(A_ERR);
check(rd[0] === 1'b1, "ERR bit 0 is STICKY -- a one-cycle pulse is remembered");
@(negedge pclk) e_parity = 1'b1;
@(negedge pclk) e_parity = 1'b0;
repeat (2) @(negedge pclk);
apb_write(A_ERR, 32'h1); // clear ONLY bit 0
apb_read(A_ERR);
check(rd[0] === 1'b0 && rd[1] === 1'b1,
"W1C clears only the bits written, and leaves the others alone");
apb_write(A_ERR, 32'hF);
apb_read(A_ERR);
check(rd[3:0] == 4'h0, "writing all ones clears every flag");
//=== ERR is SET-DOMINANT ============================================
// If an error arrives in the same cycle the driver clears the flag,
// the flag must stay set: its only record is this bit.
@(negedge pclk); psel=1'b1; pwrite=1'b1; paddr=A_ERR; pwdata=32'hF; penable=1'b0;
@(negedge pclk); penable=1'b1; e_overrun=1'b1; // set DURING the clear
@(posedge pclk);
@(negedge pclk); psel=1'b0; penable=1'b0; pwrite=1'b0; e_overrun=1'b0;
repeat (2) @(negedge pclk);
apb_read(A_ERR);
check(rd[2] === 1'b1,
"set beats clear in the same cycle -- the error is not lost");
apb_write(A_ERR, 32'hF);
//=== interrupts: transparent, not latched ===========================
@(negedge pclk) irq_raw = 5'b00101;
apb_write(A_IRQ_EN, 32'b00001);
apb_read(A_IRQ_RAW);
check(rd[4:0] == 5'b00101, "IRQ_RAW reports every condition, enabled or not");
apb_read(A_IRQ_STAT);
check(rd[4:0] == 5'b00001, "IRQ_STAT reports only what is ENABLED");
apb_write(A_IRQ_STAT, 32'h1F); // try to acknowledge it
apb_read(A_IRQ_STAT);
check(rd[4:0] == 5'b00001,
"a write to IRQ_STAT changes NOTHING -- a level has no acknowledgement");
@(negedge pclk) irq_raw = 5'b00100; // service the cause instead
apb_read(A_IRQ_STAT);
check(rd[4:0] == 5'b00000,
"removing the cause clears it, with no write at all");
//=== an unmapped address is inert ===================================
base_pop = n_pop; base_push = n_push;
apb_write(8'hF0, 32'hFFFFFFFF);
apb_read(8'hF0);
check(rd == 32'h0, "an unmapped address reads as zero");
check(n_pop - base_pop == 0 && n_push - base_push == 0,
"and writing one disturbs nothing");
apb_read(A_CTRL);
check(rd == 32'h0, "CTRL was not corrupted by the unmapped write");
//=== whole-run accounting ===========================================
check(pop_when_empty == 1,
"across the whole run, exactly one pop was issued on an empty queue");
$display("== %0d checks, %0d failures ==", checks, failures);
if (failures == 0) $display(" RESULT: ALL VERILOG REGS TESTS PASSED");
else $display(" RESULT: VERILOG REGS TESTS FAILED");
$finish;
end
endmodule--===========================================================================
-- tb_uart_regs — self-checking VHDL-2008 testbench
--
-- The property this suite exists for is ONE POP PER READ. Chapter 13.1
-- section 3 measured what a mis-qualified side effect costs -- 4 bytes
-- delivered out of 8, one read consuming 2.0 bytes -- and the only way to
-- see that in a testbench is to COUNT the pops the design generates and
-- compare against the number of reads the stimulus issued.
--
-- So the receive queue is modelled, the pops are counted, and every check
-- about the side effect is stated as an arithmetic claim about those two
-- numbers. Checking that "a read returned the right byte" would pass on a
-- design that popped twice.
--
-- Same 40 counted checks as the Verilog and SystemVerilog twins.
--
-- VHDL note: an unresolved type may have only ONE driver, so the queue's
-- head pointer and the counters are driven by the observer process while
-- its tail pointer and contents are driven by the stimulus process. That
-- split is forced by the language and it is a good discipline anyway --
-- producer and consumer state cannot be accidentally written from both
-- ends.
--===========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity tb_uart_regs is
end entity tb_uart_regs;
architecture sim of tb_uart_regs is
constant TCLK : time := 10 ns;
constant TRIG_W : positive := 4;
constant TMO_W : positive := 16;
constant A_DATA : std_logic_vector(7 downto 0) := x"00";
constant A_STATUS : std_logic_vector(7 downto 0) := x"04";
constant A_CTRL : std_logic_vector(7 downto 0) := x"08";
constant A_ERR : std_logic_vector(7 downto 0) := x"0C";
constant A_IRQ_EN : std_logic_vector(7 downto 0) := x"10";
constant A_IRQ_STAT : std_logic_vector(7 downto 0) := x"14";
constant A_IRQ_RAW : std_logic_vector(7 downto 0) := x"18";
constant A_FIFO_CTRL : std_logic_vector(7 downto 0) := x"1C";
constant A_DMA_CTRL : std_logic_vector(7 downto 0) := x"20";
constant A_TIMEOUT : std_logic_vector(7 downto 0) := x"24";
constant A_TRIGGER : std_logic_vector(7 downto 0) := x"28";
signal pclk : std_logic := '0';
signal presetn : std_logic := '0';
signal done : boolean := false;
signal psel, penable, pwrite : std_logic := '0';
signal paddr : std_logic_vector(7 downto 0) := x"00";
signal pwdata : std_logic_vector(31 downto 0) := (others => '0');
signal prdata : std_logic_vector(31 downto 0);
signal pready, pslverr : std_logic;
-- the receive queue, modelled in the testbench
type byte_arr is array (0 to 63) of std_logic_vector(7 downto 0);
type bit_arr is array (0 to 63) of std_logic;
signal q_data : byte_arr := (others => (others => '0'));
signal q_perr : bit_arr := (others => '0');
signal q_ferr : bit_arr := (others => '0');
signal q_head : natural := 0; -- driven by the observer
signal q_tail : natural := 0; -- driven by the stimulus
signal q_ne : std_logic;
signal rx_data : std_logic_vector(7 downto 0);
signal rx_perr, rx_ferr, rx_valid, rx_pop : std_logic;
signal n_pop, n_push, pop_when_empty : natural := 0;
signal tx_data : std_logic_vector(7 downto 0);
signal tx_push : std_logic;
signal tx_seen : byte_arr := (others => (others => '0'));
signal st_tx_empty : std_logic := '1';
signal st_tx_full : std_logic := '0';
signal st_rx_empty : std_logic := '1';
signal st_rx_trig : std_logic := '0';
signal st_tx_trig : std_logic := '0';
signal st_busy : std_logic := '0';
signal st_break : std_logic := '0';
signal st_rx_tmo : std_logic := '0';
signal e_frame, e_parity, e_overrun, e_break : std_logic := '0';
signal irq_raw : std_logic_vector(4 downto 0) := (others => '0');
signal err_flags : std_logic_vector(3 downto 0);
signal irq_en : std_logic_vector(4 downto 0);
signal cfg_parity : std_logic_vector(1 downto 0);
signal cfg_hw, cfg_sw, cfg_li, cfg_ll : std_logic;
signal tx_flush, rx_flush, dma_tx_en, dma_rx_en : std_logic;
signal timeout_bits : std_logic_vector(TMO_W-1 downto 0);
signal rx_trigger, tx_trigger : std_logic_vector(TRIG_W-1 downto 0);
signal txf_cycles, rxf_cycles, txf_wide, rxf_wide : natural := 0;
begin
pclk <= not pclk after TCLK/2 when not done else '0';
q_ne <= '1' when q_head /= q_tail else '0';
rx_data <= q_data(q_head) when q_ne = '1' else x"FF";
rx_perr <= q_perr(q_head) when q_ne = '1' else '0';
rx_ferr <= q_ferr(q_head) when q_ne = '1' else '0';
rx_valid <= q_ne;
dut : entity work.uart_regs
generic map (ADDR_W => 8, TRIG_W => TRIG_W, TMO_W => TMO_W)
port map (
pclk => pclk, presetn => presetn, psel_i => psel,
penable_i => penable, pwrite_i => pwrite, paddr_i => paddr,
pwdata_i => pwdata, prdata_o => prdata,
pready_o => pready, pslverr_o => pslverr,
rx_data_i => rx_data, rx_valid_i => rx_valid,
rx_perr_i => rx_perr, rx_ferr_i => rx_ferr, rx_pop_o => rx_pop,
tx_data_o => tx_data, tx_push_o => tx_push,
st_tx_empty_i => st_tx_empty, st_tx_full_i => st_tx_full,
st_rx_empty_i => st_rx_empty, st_rx_trig_i => st_rx_trig,
st_tx_trig_i => st_tx_trig, st_busy_i => st_busy,
st_break_i => st_break, st_rx_tmo_i => st_rx_tmo,
err_frame_set_i => e_frame, err_parity_set_i => e_parity,
err_overrun_set_i => e_overrun, err_break_set_i => e_break,
err_flags_o => err_flags,
irq_raw_i => irq_raw, irq_en_o => irq_en,
cfg_parity_o => cfg_parity, cfg_flow_hw_o => cfg_hw,
cfg_flow_sw_o => cfg_sw, cfg_loop_int_o => cfg_li,
cfg_loop_line_o => cfg_ll,
tx_flush_o => tx_flush, rx_flush_o => rx_flush,
dma_tx_en_o => dma_tx_en, dma_rx_en_o => dma_rx_en,
timeout_bits_o => timeout_bits,
rx_trigger_o => rx_trigger, tx_trigger_o => tx_trigger);
-- pop and push counters: the whole point of the suite
observer : process (pclk)
begin
if rising_edge(pclk) and presetn = '1' then
if rx_pop = '1' then
n_pop <= n_pop + 1;
if q_ne = '0' then
pop_when_empty <= pop_when_empty + 1;
else
q_head <= q_head + 1;
end if;
end if;
if tx_push = '1' then
tx_seen(n_push) <= tx_data;
n_push <= n_push + 1;
end if;
end if;
end process observer;
-- flush pulses must be exactly one cycle wide
flush_obs : process (pclk)
variable txf_prev, rxf_prev : std_logic := '0';
begin
if rising_edge(pclk) and presetn = '1' then
if tx_flush = '1' then
txf_cycles <= txf_cycles + 1;
if txf_prev = '1' then txf_wide <= txf_wide + 1; end if;
end if;
if rx_flush = '1' then
rxf_cycles <= rxf_cycles + 1;
if rxf_prev = '1' then rxf_wide <= rxf_wide + 1; end if;
end if;
txf_prev := tx_flush; rxf_prev := rx_flush;
end if;
end process flush_obs;
watchdog : process
begin
wait for 500 us;
report "watchdog: simulation did not finish" severity failure;
end process watchdog;
stim : process
variable checks, failures : natural := 0;
variable rd : std_logic_vector(31 downto 0);
variable base_pop, base_push, base_cyc : natural := 0;
procedure check(cond : boolean; name : string) is
begin
checks := checks + 1;
if cond then
report " PASS " & name severity note;
else
failures := failures + 1;
report " FAIL " & name severity error;
end if;
end procedure check;
procedure q_load(d : std_logic_vector(7 downto 0); p, f : std_logic) is
begin
q_data(q_tail) <= d;
q_perr(q_tail) <= p;
q_ferr(q_tail) <= f;
q_tail <= q_tail + 1;
wait for 0 ns; -- let the update take effect
end procedure q_load;
procedure apb_write(a : std_logic_vector(7 downto 0);
d : std_logic_vector(31 downto 0)) is
begin
wait until falling_edge(pclk);
psel <= '1'; pwrite <= '1'; paddr <= a; pwdata <= d; penable <= '0';
wait until falling_edge(pclk);
penable <= '1'; -- ACCESS phase
wait until rising_edge(pclk);
wait until falling_edge(pclk);
psel <= '0'; penable <= '0'; pwrite <= '0';
end procedure apb_write;
procedure apb_read(a : std_logic_vector(7 downto 0)) is
begin
wait until falling_edge(pclk);
psel <= '1'; pwrite <= '0'; paddr <= a; penable <= '0';
wait until falling_edge(pclk);
penable <= '1'; -- ACCESS phase
wait for 1 ns; -- sample BEFORE the committing edge
rd := prdata;
wait until rising_edge(pclk);
wait until falling_edge(pclk);
psel <= '0'; penable <= '0';
end procedure apb_read;
begin
report "== uart_regs : self-checking VHDL testbench ==" severity note;
presetn <= '0';
for i in 1 to 4 loop wait until falling_edge(pclk); end loop;
presetn <= '1';
for i in 1 to 2 loop wait until falling_edge(pclk); end loop;
--=== reset values ==================================================
apb_read(A_CTRL);
check(rd = x"00000000", "reset: CTRL is 0 -- 8N1, no flow control, no loopback");
apb_read(A_IRQ_EN);
check(rd = x"00000000", "reset: IRQ_EN is 0 -- nothing enabled");
apb_read(A_DMA_CTRL);
check(rd = x"00000000", "reset: DMA_CTRL is 0");
apb_read(A_TIMEOUT);
check(unsigned(rd) = 40, "reset: TIMEOUT is 40 bit times -- four characters");
apb_read(A_TRIGGER);
check(unsigned(rd(TRIG_W-1 downto 0)) = 8
and unsigned(rd(16+TRIG_W-1 downto 16)) = 4,
"reset: TRIGGER is rx=8, tx=4");
check(pready = '1' and pslverr = '0',
"APB: pready always high, pslverr always low");
--=== THE side effect: one pop per read =============================
for i in 0 to 7 loop
q_load(std_logic_vector(to_unsigned(16#40# + i, 8)), '0', '0');
end loop;
base_pop := n_pop;
apb_read(A_DATA);
check(n_pop - base_pop = 1, "a committed DATA read pops EXACTLY ONCE");
check(rd(7 downto 0) = x"40", "and returns the byte at the head");
check(rd(10) = '1', "valid (bit 10) is set while the queue has data");
base_pop := n_pop;
for i in 0 to 6 loop apb_read(A_DATA); end loop;
check(n_pop - base_pop = 7, "seven reads pop exactly seven bytes");
check(rd(7 downto 0) = x"47", "the last read returns the last byte loaded");
--=== a read of an EMPTY queue ======================================
base_pop := n_pop;
apb_read(A_DATA);
check(rd(10) = '0', "valid is CLEAR when the queue is empty");
check(pop_when_empty = 1,
"the pop still fires -- the queue, not the register block, guards it");
--=== the SETUP phase must not commit anything =======================
for i in 0 to 3 loop
q_load(std_logic_vector(to_unsigned(16#90# + i, 8)), '0', '0');
end loop;
base_pop := n_pop;
wait until falling_edge(pclk);
psel <= '1'; pwrite <= '0'; paddr <= A_DATA; penable <= '0';
for i in 1 to 6 loop wait until falling_edge(pclk); end loop;
check(n_pop - base_pop = 0, "six cycles in the SETUP phase pop NOTHING");
wait until falling_edge(pclk);
psel <= '0';
for i in 1 to 2 loop wait until falling_edge(pclk); end loop;
check(n_pop - base_pop = 0, "an aborted transfer pops nothing at all");
--=== per-byte status travels with the byte ==========================
q_load(x"C3", '1', '0');
q_load(x"C4", '0', '1');
apb_read(A_DATA);
check(rd(7 downto 0) = x"90", "the aborted transfer left the queue untouched");
for i in 0 to 2 loop apb_read(A_DATA); end loop;
apb_read(A_DATA);
check(rd(7 downto 0) = x"C3" and rd(8) = '1' and rd(9) = '0',
"DATA carries THIS byte's parity error in bit 8");
apb_read(A_DATA);
check(rd(7 downto 0) = x"C4" and rd(8) = '0' and rd(9) = '1',
"and THIS byte's framing error in bit 9");
--=== every other register is safe to read ===========================
for i in 0 to 3 loop
q_load(std_logic_vector(to_unsigned(16#70# + i, 8)), '0', '0');
end loop;
base_pop := n_pop; base_push := n_push;
apb_read(A_STATUS); apb_read(A_CTRL); apb_read(A_ERR);
apb_read(A_IRQ_EN); apb_read(A_IRQ_STAT); apb_read(A_IRQ_RAW);
apb_read(A_FIFO_CTRL); apb_read(A_DMA_CTRL); apb_read(A_TIMEOUT);
apb_read(A_TRIGGER);
check(n_pop - base_pop = 0 and n_push - base_push = 0,
"reading all ten non-DATA registers has NO side effect");
--=== writes ==========================================================
base_push := n_push;
apb_write(A_DATA, x"000000A5");
check(n_push - base_push = 1, "a committed DATA write pushes EXACTLY ONCE");
check(tx_seen(n_push-1) = x"A5", "and pushes the byte that was written");
apb_write(A_CTRL, x"0000003F");
apb_read(A_CTRL);
check(rd(5 downto 0) = "111111", "CTRL writes and reads back");
check(cfg_parity = "11" and cfg_hw = '1' and cfg_sw = '1'
and cfg_li = '1' and cfg_ll = '1',
"and every CTRL field reaches its output");
apb_write(A_CTRL, x"00000000");
apb_write(A_TIMEOUT, std_logic_vector(to_unsigned(120, 32)));
apb_read(A_TIMEOUT);
check(unsigned(rd) = 120 and unsigned(timeout_bits) = 120,
"TIMEOUT is writable");
apb_write(A_TRIGGER, std_logic_vector(to_unsigned(2, 16))
& std_logic_vector(to_unsigned(12, 16)));
check(unsigned(rx_trigger) = 12 and unsigned(tx_trigger) = 2,
"TRIGGER carries both levels in one word");
--=== the flush bits are PULSES ======================================
base_cyc := txf_cycles;
apb_write(A_FIFO_CTRL, x"00000003");
for i in 1 to 4 loop wait until falling_edge(pclk); end loop;
check(txf_cycles - base_cyc = 1,
"a flush write produces ONE cycle, not a level");
check(txf_wide = 0 and rxf_wide = 0,
"no flush pulse was ever wider than one cycle");
apb_read(A_FIFO_CTRL);
check(rd = x"00000000", "FIFO_CTRL always reads back 0 -- it holds no state");
--=== ERR: sticky history, write-one-to-clear ========================
wait until falling_edge(pclk); e_frame <= '1';
wait until falling_edge(pclk); e_frame <= '0';
for i in 1 to 2 loop wait until falling_edge(pclk); end loop;
apb_read(A_ERR);
check(rd(0) = '1', "ERR bit 0 is STICKY -- a one-cycle pulse is remembered");
wait until falling_edge(pclk); e_parity <= '1';
wait until falling_edge(pclk); e_parity <= '0';
for i in 1 to 2 loop wait until falling_edge(pclk); end loop;
apb_write(A_ERR, x"00000001");
apb_read(A_ERR);
check(rd(0) = '0' and rd(1) = '1',
"W1C clears only the bits written, and leaves the others alone");
apb_write(A_ERR, x"0000000F");
apb_read(A_ERR);
check(rd(3 downto 0) = x"0", "writing all ones clears every flag");
--=== ERR is SET-DOMINANT ============================================
wait until falling_edge(pclk);
psel <= '1'; pwrite <= '1'; paddr <= A_ERR;
pwdata <= x"0000000F"; penable <= '0';
wait until falling_edge(pclk);
penable <= '1'; e_overrun <= '1'; -- set DURING the clear
wait until rising_edge(pclk);
wait until falling_edge(pclk);
psel <= '0'; penable <= '0'; pwrite <= '0'; e_overrun <= '0';
for i in 1 to 2 loop wait until falling_edge(pclk); end loop;
apb_read(A_ERR);
check(rd(2) = '1',
"set beats clear in the same cycle -- the error is not lost");
apb_write(A_ERR, x"0000000F");
--=== interrupts: transparent, not latched ===========================
wait until falling_edge(pclk); irq_raw <= "00101";
apb_write(A_IRQ_EN, x"00000001");
apb_read(A_IRQ_RAW);
check(rd(4 downto 0) = "00101",
"IRQ_RAW reports every condition, enabled or not");
apb_read(A_IRQ_STAT);
check(rd(4 downto 0) = "00001", "IRQ_STAT reports only what is ENABLED");
apb_write(A_IRQ_STAT, x"0000001F"); -- try to acknowledge it
apb_read(A_IRQ_STAT);
check(rd(4 downto 0) = "00001",
"a write to IRQ_STAT changes NOTHING -- a level has no acknowledgement");
wait until falling_edge(pclk); irq_raw <= "00100";
apb_read(A_IRQ_STAT);
check(rd(4 downto 0) = "00000",
"removing the cause clears it, with no write at all");
--=== an unmapped address is inert ===================================
base_pop := n_pop; base_push := n_push;
apb_write(x"F0", x"FFFFFFFF");
apb_read(x"F0");
check(rd = x"00000000", "an unmapped address reads as zero");
check(n_pop - base_pop = 0 and n_push - base_push = 0,
"and writing one disturbs nothing");
apb_read(A_CTRL);
check(rd = x"00000000", "CTRL was not corrupted by the unmapped write");
--=== whole-run accounting ===========================================
check(pop_when_empty = 1,
"across the whole run, exactly one pop was issued on an empty queue");
report "== " & integer'image(checks) & " checks, "
& integer'image(failures) & " failures ==" severity note;
if failures = 0 then
report " RESULT: ALL VHDL REGS TESTS PASSED" severity note;
else
report " RESULT: VHDL REGS TESTS FAILED" severity error;
end if;
done <= true;
wait;
end process stim;
end architecture sim;All three run the same 40 checks and agree:
== uart_regs : self-checking SystemVerilog testbench ==
PASS reset: CTRL is 0 -- 8N1, no flow control, no loopback
PASS reset: TIMEOUT is 40 bit times -- four characters
PASS a committed DATA read pops EXACTLY ONCE
PASS seven reads pop exactly seven bytes
PASS six cycles in the SETUP phase pop NOTHING
PASS an aborted transfer pops nothing at all
PASS the aborted transfer left the queue untouched
PASS DATA carries THIS byte's parity error in bit 8
PASS reading all ten non-DATA registers has NO side effect
PASS a flush write produces ONE cycle, not a level
PASS FIFO_CTRL always reads back 0 -- it holds no state
PASS ERR bit 0 is STICKY -- a one-cycle pulse is remembered
PASS W1C clears only the bits written, and leaves the others alone
PASS set beats clear in the same cycle -- the error is not lost
PASS a write to IRQ_STAT changes NOTHING -- a level has no acknowledgement
PASS removing the cause clears it, with no write at all
== 40 checks, 0 failures ==
Verilog-2001 : 40 checks, 0 failures
SystemVerilog : 40 checks, 0 failures
VHDL-2008 : 40 checks, 0 failures8. Verification
Test the side effect's boundaries, not just its function. That a read returns the right byte is the easy half; the half that matters is that it consumes exactly one byte per transfer.
// Assertion — the pop is exactly as wide as a committed transfer. This is
// the property the psel-qualified version violates, and it is checkable
// without knowing anything about UART behaviour.
property p_pop_only_on_access;
@(posedge clk) disable iff (!rst_n)
rd_ready |-> (psel_i && penable_i && pready_o && !pwrite_i);
endproperty
// Assertion — no register except DATA disturbs anything.
property p_only_data_has_side_effects;
@(posedge clk) disable iff (!rst_n)
(do_read && paddr_i != A_DATA) |-> !rd_ready;
endpropertyRead every register twice and compare. A register that returns different values on two consecutive reads, with nothing else happening, either has a side effect or reflects a live condition — and the design must say which on purpose. This is a three-line test that catches an entire class of accidental side effect.
Drive the bus correctly, and then incorrectly. The comparison in §3 needed a second register block, because a correct master will never exercise the wrong qualification. Testing an adapter with only a well-behaved master verifies less than it appears to.
Count bytes end to end. The §3 defect is invisible to every UART-level check and obvious to a test that writes eight bytes and demands eight back. Whole-transaction accounting is what catches adapter bugs, because adapters fail by losing things rather than by corrupting them.
9. Debugging
10. What This Means on an FPGA
The register block is small and almost entirely flops. The configuration fields, the interrupt state and the decode are a few dozen registers and a comparator on the address — negligible against the core.
One wait state is usually unnecessary. Every register here is a flop or a combinational mux of flops, so pready is tied high and every access completes in the APB minimum of two cycles. Adding wait states because it seems safer costs every driver access and buys nothing.
Keep the address map sparse and word-aligned. Four-byte spacing for every register wastes nothing real and means a driver never needs a sub-word access, which is where byte-enable handling becomes an unnecessary complication.
Do not let the register block accumulate logic. The pressure to "just handle this in the registers" is constant and it is how a reusable core turns into a bus-specific one. If a behaviour belongs to the UART, it belongs in the core where Chapter 13.5's different bus will also get it.
11. Understanding Check
12. Summary
The register block is an adapter and must stay one. It decodes, holds configuration, presents status and converts transfers into handshakes — and contains no UART logic, which is what lets the same core sit behind a different bus unchanged.
UART registers are not memory in four separate ways, all at DATA: one address naming two different pieces of hardware, a read that consumes, a read that is not repeatable, and a write that cannot be read back.
A side effect fires on a committed transfer, never on "select is asserted". Qualifying on select instead delivered 4 of 8 bytes, with the first read already returning the second byte — one bus read consuming exactly 2.0 bytes.
That defect is invisible to every UART-level diagnostic and presents as a serial-domain problem. Byte accounting end to end is what finds it.
Only DATA has side effects, so a debugger can watch the peripheral and a driver can poll without consequence — and an empty read reports valid = 0 rather than fabricating a byte.
Per-byte status is returned in the same word as the data, because reading it separately is a race against the pop.
And attaching a bus found another missing port — the receive-arrival event — which is Chapter 11.4's rule recurring exactly as predicted: a hierarchical reference is a missing port, and interfaces are discovered by attaching things.
13. What Comes Next
The mechanism is established; the contents are not. Chapter 13.2 lays out the actual register map — which fields exist, which register each lives in, and why.
That includes the decisions that look arbitrary and are not: why the trigger levels are writable, why the error flags are write-one-to-clear rather than clear-on-read, and why some status bits appear in two places on purpose.
Browse the full path on the UART tutorials index. For the per-byte status this chapter returns atomically, read back to Chapter 10.2.
Continue learning
Related tutorials
- Related topic
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.
- 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.
