UART · Module 11
Parameterisation and Reusable RTL Organisation
What belongs to a parameter, what belongs to a register, and how to organise the RTL so the IP survives its second project — with three real defects the first project could never have exposed.
The IP works. It passes 53 integration checks, loops bytes back through every parity mode, defers configuration changes correctly and reports errors honestly. Every one of those results was obtained at one parameter set.
This chapter asks the only question that separates an IP from a block that happens to work: what happens at the second parameter set? The answer, found by actually elaborating the same source three ways, was three real defects — one of which reports a break condition on perfectly legal data, and none of which any test in Modules 6 through 11.3 could have caught.
1. Parameter or Register?
The test is not "might someone want to change it". Almost everything qualifies under that. The test is when it changes:
| Parameter | Register | |
|---|---|---|
| Fixed at | elaboration | runtime |
| Changing it costs | a re-synthesis | a bus write |
| Affects | the gates that exist | how existing gates behave |
| Wrong value found | at build time, if checked | at run time, by debugging |
| In this IP | DATA_W, OVERSAMPLE, TX_DEPTH, RX_DEPTH, CLK_HZ | parity mode, flow-control enables, loopback |
The deciding question is whether the value changes the hardware. RX_DEPTH determines how many flip-flops exist; it cannot be a register, because the storage has to be built. Parity mode selects between behaviours that both already exist in the gates; making it a parameter would force a re-synthesis to talk to a different peer, which is absurd for a link that negotiates its format with a human typing into a terminal program.
BAUD_HZ is the interesting one, and this IP gets it half right. It is a parameter here, which means changing baud rate requires re-synthesis — acceptable for an embedded link fixed at 115,200, and unacceptable for anything user-configurable. Chapter 8.3 built the fractional divider whose increment is a value, not a structure, precisely so that this can be a register later. The right shape is a parameter that sets the reset value of a register, which is the pattern Module 13 will apply.
2. The Defect That Needed a Second Configuration
FRAME_BITS sets the break detector's threshold: BREAK_TICKS = FRAME_BITS × OVERSAMPLE, the number of consecutive low sample ticks that declare a break (Chapter 9.4 §2). At FRAME_BITS = 10, OVERSAMPLE = 16 that is 160 ticks — exactly ten bit intervals.
Now count the longest legal low run the IP can produce. Under even parity, the byte 0x00:
start 0 1 interval
data 0 0 0 0 0 0 0 0 (LSB first) 8 intervals
parity 0 (even parity of 0x00 = 0) 1 interval
stop 1 -- run ends here
---------------
low run = 10 intervals = 160 ticksThe longest legal frame and the break threshold are the same number. The detector reaches 160 on the final tick of the parity bit and declares a break — on a byte that is entirely valid, on a link that is working perfectly.
Measured, against the IP as it stood at the end of Chapter 11.2:
BREAK_TICKS in dut = 160 (FRAME_BITS=10 x OS=16)
pass 8N1 0x00 : no break declared
pass 8N1 0x00 : byte delivered 0x00
-- sending 0x00 under EVEN parity (10 consecutive low intervals)
dut err_break_o (FRAME_BITS=10) = 1 <-- break declared
ref break_active (FRAME_BITS=11) = 0
pass 8E1 0x00 : dut DECLARES break on legal data
pass 8E1 0x00 : derived-threshold detector stays quiet
pass 8E1 0x00 : the byte itself still arrives intact
pass 8O1 0x00 : no break (parity bit is 1, breaks the run)
pass 8E1 0x01 : no break (a set data bit breaks the run)Read the last three lines, because they explain why this survived so long. The byte still arrives, intact and without a parity error — the break detector is an observer (Chapter 9.4 §5), so it corrupts nothing. It merely raises a sticky error flag that is not true. And the trigger is narrow: it needs even parity and the byte 0x00 specifically. Odd parity puts a 1 in the parity bit and ends the run at nine; any set data bit ends it earlier still.
One byte value, in one of three parity modes, producing a false error flag and no data corruption. That is a defect that ships.
The fix derives the threshold from the frame the IP can be configured to send, not the frame it happens to be sending:
// AFTER — the threshold is a property of the configured frame, not of the file.
// The longest LEGAL low run is start + DATA_W zero data bits + a zero parity
// bit; the threshold must sit one interval past it, which is exactly the
// longest frame this IP can be told to send.
localparam int unsigned FRAME_BITS_MAX = DATA_W + 3; // start + data + parity + stop
uart_break_detect #(.OVERSAMPLE(OVERSAMPLE), .FRAME_BITS(FRAME_BITS_MAX)) u_break (...);The threshold uses the maximum, not the current configuration. It could track cfg_parity_q and tighten to ten intervals in 8N1, detecting a break one bit-time sooner. It deliberately does not: the detector is an observer that stays independent of configuration, and one extra bit-time — 8.68 µs at 115,200 baud — is not worth coupling it to the deferral logic of Chapter 11.3. Taking the worst case is the honest parameter-time answer.
After the fix, with a regression proving real breaks are still caught:
pass 8E1 0x00 : NO break on legal data (FRAME_BITS derived)
pass 8E1 0x00 : the byte itself still arrives intact
pass idle line : no break
pass real break (13 bit times low) : declared
pass line released : break_active clears
pass sticky err_break_o survives the release
== 11 checks, 0 failures ==3. The Other Two
The same question — what breaks at a different parameter set? — found two more in the same file.
A width-fragile bit select. Software flow control compares against the 8-bit codes 0x11 and 0x13:
.rx_char_data_i(rx_char[7:0]), // BEFOREWith DATA_W = 8 this is exact. With DATA_W = 7 it selects a bit that does not exist. The fix states the intent rather than assuming the width:
// AFTER — narrower characters zero-extend; wider ones compare on the low
// byte, where the control codes live.
localparam int unsigned SW_CMP_W = (DATA_W < 8) ? DATA_W : 8;
...
.rx_char_data_i(8'(rx_char[SW_CMP_W-1:0])),A hierarchical reference, which is the worst of the three. The top level needed to know whether the receiver was mid-frame, and the receiver did not publish it — so the integration reached inside:
assign busy_o = tx_busy | !tx_fifo_empty | (u_rx.state_q != 3'd0); // BEFORE
assign link_quiet = !tx_busy && tx_fifo_empty && (u_rx.state_q == 3'd0); // BEFORE4. Legality, Checked at Elaboration
Parameters have relationships, and an integrator overriding one can violate a relationship with another that they never touched. The IP checks for that — the question is when.
// BEFORE — runtime. Produces a working build; complains only if simulated.
initial begin
if (RX_TRIGGER > RX_DEPTH) $fatal(1, "uart_ip: RX_TRIGGER exceeds RX_DEPTH");
...
end// AFTER — elaboration time. An illegal set cannot produce a build at all,
// so it can never reach synthesis.
if (RX_TRIGGER > RX_DEPTH) begin : g_bad_rx_trigger
$error("uart_ip: RX_TRIGGER exceeds RX_DEPTH");
end
if (TX_TRIGGER > TX_DEPTH) begin : g_bad_tx_trigger
$error("uart_ip: TX_TRIGGER exceeds TX_DEPTH");
end
if (FC_RESUME >= FC_STOP) begin : g_bad_hysteresis
$error("uart_ip: FC_RESUME must be below FC_STOP");
end
if (FC_STOP > RX_DEPTH) begin : g_bad_fc_stop
$error("uart_ip: FC_STOP exceeds RX_DEPTH");
end
if (DATA_W < 5 || DATA_W > 9) begin : g_bad_data_w
$error("uart_ip: DATA_W outside the 5..9 range a UART frame supports");
end
if (OVERSAMPLE < 4) begin : g_bad_oversample
$error("uart_ip: OVERSAMPLE below 4 leaves no usable sampling window");
end
if (CLK_HZ < BAUD_HZ * OVERSAMPLE) begin : g_bad_clock
$error("uart_ip: CLK_HZ cannot produce BAUD_HZ at this OVERSAMPLE");
endThe difference is not stylistic. A runtime check produces a simulation binary, and reports the problem only to whoever runs it. An elaboration check stops the build. Compiling a top level containing five deliberately-illegal instances:
ERROR: uart_ip.sv:86: uart_ip: RX_TRIGGER exceeds RX_DEPTH
ERROR: uart_ip.sv:95: uart_ip: FC_STOP exceeds RX_DEPTH
ERROR: uart_ip.sv:92: uart_ip: FC_RESUME must be below FC_STOP
ERROR: uart_ip.sv:98: uart_ip: DATA_W outside the 5..9 range a UART frame supports
ERROR: uart_ip.sv:101: uart_ip: OVERSAMPLE below 4 leaves no usable sampling window
ERROR: uart_ip.sv:104: uart_ip: CLK_HZ cannot produce BAUD_HZ at this OVERSAMPLE
6 error(s) during elaboration.
binary produced? NO — build stoppedSix errors from five instances, and the extra one is the lesson. The first instance overrode only RX_DEPTH to 8 and RX_TRIGGER to 24 — and also tripped FC_STOP, whose default of 12 was left untouched and is illegal against a depth of 8.
One tool note, verified rather than assumed. Icarus Verilog 13.0 supports elaboration tasks but rejects format arguments:
sorry: Elaboration tasks currently only support a single string argument.So $error("... TRIG=%0d ...", TRIG) fails to compile for the wrong reason — it looks like the check firing when it is the tool declining the call. Write the message as a plain string; it costs the offending value in the text and keeps the check portable.
5. Elaborating the Same Source Three Ways
The claim "it is parameterised" is worth exactly what has been elaborated. Three instances of the same unmodified file, in one simulation:
| A — default | B — fast | C — narrow | |
|---|---|---|---|
CLK_HZ | 100 MHz | 50 MHz | 24 MHz |
BAUD_HZ | 115,200 | 1,000,000 | 115,200 |
DATA_W | 8 | 8 | 7 |
OVERSAMPLE | 16 | 16 | 8 |
TX_DEPTH / RX_DEPTH | 16 / 16 | 8 / 32 | 4 / 4 |
| parity | even | none | odd |
| clocks per bit | 868.06 | 50 | 208.33 |
FRAME_BITS_MAX | 11 | 11 | 10 |
| FIFO storage | 288 bits | 384 bits | 64 bits |
The storage row is TX_DEPTH × DATA_W + RX_DEPTH × (DATA_W + 2) — the RX side is wider because each entry carries its own framing and parity status (Chapter 10.2 §8). Set B holds 33% more bits than the default while having a smaller transmit queue, which is what an asymmetric link costs.
-- B_fast: 50 MHz / 1000000 baud / 8b / OS16 / TXD8 RXD32
pass B_fast : all 4 bytes looped back
pass B_fast : no error flags raised
pass B_fast : no spurious break
-- A_default: 100 MHz / 115200 baud / 8b / OS16 / TXD16 RXD16
pass A_default : all 4 bytes looped back
pass A_default : no error flags raised
pass A_default : no spurious break
-- C_narrow: 24 MHz / 115200 baud / 7b / OS8 / TXD4 RXD4
pass C_narrow : all 4 bytes looped back
pass C_narrow : no error flags raised
pass C_narrow : no spurious break
== 9 checks, 0 failures ==
RESULT: one source, three parameter sets, all passSet A is the one carrying the weight. It is even parity with 0x00 in its pattern list — the exact combination that produced the false break in §2 — so A_default : no spurious break is the fix holding at the configuration that exposed the defect.
Set C is the one that would have broken the old code twice over: DATA_W = 7 walks into the rx_char[7:0] select, and OVERSAMPLE = 8 halves every threshold derived from it.
6. Organising the File
Two structural rules, both of which this IP follows and neither of which is obvious until the second project:
Sub-blocks take parameters; they do not read globals. Every block receives DATA_W, OVERSAMPLE and its depth through its own parameter list. A block reading a package constant instead cannot be instantiated twice with different values in one design — which is exactly what a two-channel UART needs.
The top level owns every derivation. FRAME_BITS_MAX, SW_CMP_W, RX_ENTRY_W are localparams in uart_ip, computed once from the parameters and passed down. A block computing DATA_W + 2 for itself makes the same arithmetic exist twice, and the second copy is where the two drift apart.
The package holds types, not configuration. uart_parity_pkg carries parity_mode_t and nothing else. A type is shared by definition — two instances with different parity modes still agree about what PARITY_EVEN means. Putting DATA_W in a package would make it global, which is the mistake the first rule exists to prevent.
7. Verification
A parameterisation test is a sweep, not a case. The structure that matters is instantiating several configurations in one simulation, so a change has to survive all of them before it can be committed — rather than a script that runs one and is easy to skip.
// Assertion — the break threshold always exceeds the longest legal frame.
// Checks the DERIVATION rather than its current value, so it survives any
// change to DATA_W and fails on a re-hardcoded literal.
if (FRAME_BITS_MAX <= DATA_W + 2) begin : g_break_threshold_too_low
$error("uart_ip: break threshold does not exceed the longest legal frame");
end
// Assertion — no false break during ordinary traffic. This is the check the
// integration suite was missing; it is what turns the §2 defect into a failure.
property p_no_break_while_framing;
@(posedge clk) disable iff (!rst_n)
rx_active |-> !break_evt;
endproperty
assert property (p_no_break_while_framing);The first was run, and it works. Rebuilding with the derivation deliberately weakened to DATA_W + 2 — the value that makes the threshold equal the longest legal frame — stops the build:
3 error(s) during elaboration.
binary? NO — guard caught the broken derivationThe second assertion is the lesson from §2 written down. The integration suite checked delivered bytes and per-byte status, and both were right; nothing watched whether a sticky flag was being raised during traffic that had no errors in it. Add a check that error flags stay clear on a clean run, not only that data arrives — an IP that delivers every byte correctly while asserting an error is still broken, and it is broken in the way that costs a field debug.
8. What This Means on an FPGA
Derived parameters cost nothing. FRAME_BITS_MAX and SW_CMP_W resolve at elaboration; the synthesised netlist is identical to one with the literals written out. The fix in §2 changes one constant in a comparator, not the comparator.
Depths dominate the area, and they are not linear in the way people expect. Going from RX_DEPTH = 16 to 32 doubles storage, adds one bit to the pointers and one to the level counter — and on most FPGAs a deep-enough FIFO changes kind, moving from distributed LUT RAM into a block RAM. Check what the depth actually inferred rather than assuming the trend continued.
OVERSAMPLE is nearly free and DATA_W is not. Oversampling changes counter widths by a bit or two. DATA_W widens the shift registers, both FIFOs and every datapath between them.
The elaboration checks are the highest-value lines in the file per character typed. Seven if statements, zero gates, and they convert an entire class of integration mistake from a bring-up mystery into a compiler error with the reason in the message.
9. Understanding Check
10. Summary
Parameter or register is decided by whether the value changes the gates, not by whether someone might want to change it. Depths and widths build hardware; parity and flow-control enables select among behaviours that already exist.
A parameter that can be set inconsistently with another parameter is a defect waiting for its second user, and the fix is usually to derive rather than to default.
A hardcoded FRAME_BITS(10) made the IP declare a break on the byte 0x00 under even parity — a legal frame whose low run is exactly ten intervals. The byte arrived intact and a sticky error flag was raised that was not true. One value, one parity mode, no data corruption: a defect that ships.
A hierarchical reference is a missing port. Depending on a sub-block's private state encoding means a legitimate re-encoding silently breaks the top level — including the link-quiet condition that gates configuration commits.
Legality checks belong at elaboration, not in an initial block. Runtime checks produce a working build and complain only to whoever simulates it; elaboration checks stop the build, so an illegal set never reaches synthesis. Five illegal instances produced six errors, and the extra one came from an untouched default.
Three adversarially-chosen parameter sets, elaborated from one unmodified source, all pass — including the set that exposed the false break and the set that would have broken two defects at once.
And the check the integration suite was missing: assert that error flags stay clear on a clean run. An IP that delivers every byte correctly while raising an error is still broken.
11. What Comes Next
Every defect in this chapter was found by reasoning about a configuration that had never been run. Chapter 11.5 is about the opposite instinct: features built into the IP so that the board in front of you can answer questions by itself.
Internal loopback has already been used throughout this module as a verification convenience. That chapter treats it as what it actually is — a bring-up instrument that partitions a dead link into a dead IP and a dead board, in about four lines of RTL — along with the other test hooks worth their gates.
Browse the full path on the UART tutorials index. For the break threshold this chapter re-derived, read back to Chapter 9.4.
Continue learning
Related tutorials
- Related topic
Top-Level UART IP Architecture
Ten independently verified blocks and the interfaces that join them. The top level adds no new function; what it adds is fan-out, configuration distribution, status aggregation and the decisions that only exist once blocks meet.
- Related topic
Integrating TX, RX, Baud Generation and FIFOs
The details that only appear at the top level: one generator feeding both halves, a flush that must not corrupt a frame in flight, and several distinct loss points that must feed one honest overrun flag.
- Related topic
Configuration, Control and Status Plumbing
Routing configuration to both halves and status back, with defined behaviour when a change arrives mid-traffic — because a format applied to one half and not the other is worse than not applying it at all.
- Related topic
Loopback, Test Modes and Built-In Self-Check
Internal and line loopback, what each one proves, and — measured — the faults they provably cannot see. The cheapest RTL in the IP and the most decisive during bring-up.
Where this fits
Part of the UART curriculum.
