UART · Module 11
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.
Chapter 11.1 drew the diagram and listed six decisions that exist only at the top level. This chapter makes four of them, and they share a character worth naming up front:
Each is invisible from inside any single block, and each has a wrong answer that looks reasonable.
busy_o was the example Chapter 11.1 §6 gave, and it deadlocked simulation. The three here are the same shape: enable fan-out, flush behaviour, and status aggregation — all of them connections rather than logic, and all of them places where a careless answer produces a defect no unit test can see.
1. One Generator, Fanned Out
Chapter 8.4 established the result and the top level is where it is applied: one fractional generator at OVERSAMPLE × BAUD_HZ, with the receiver consuming the oversample enable directly and the transmitter taking every sixteenth of them.
// Synthesizable SystemVerilog — one instance, two enables.
logic os_tick, baud_tick;
uart_timing_gen #(.CLK_HZ(CLK_HZ), .BAUD_HZ(BAUD_HZ), .OVERSAMPLE(OVERSAMPLE))
u_timing (.clk(clk), .rst_n(rst_n), .os_tick_o(os_tick), .baud_tick_o(baud_tick));Two independent generators would have been the obvious structure and it is the worse one. Chapter 8.4 §3 quantified why: a shared fractional base is 58× more accurate than independent integer dividers, and a shared integer base would have been 73× worse — the choice of generator matters more than the choice to share.
Sharing also makes the two enables structurally coherent. Measured over 200,000 fabric clocks in the integrated IP:
os_tick = 31,999
baud_tick = 1,999
ratio = 16, exactly — and it cannot drift, because one is a subsample
of the other rather than an independently generated signalThe fan-out is unremarkable as a load. os_tick reaches the receiver's phase counter, its FSM and the break detector; baud_tick reaches the transmitter alone. A few dozen endpoints at under 2 MHz is nothing a tool will comment on.
2. The TX Datapath Connection
Three blocks in series, and the interesting part is the launch condition:
// Synthesizable SystemVerilog — TX path assembly.
assign tx_push = wr_valid_i && !tx_fifo_full && !cfg_tx_flush_i;
assign wr_ready_o = !tx_fifo_full;
// Both flow-control mechanisms are permissions. EITHER can withhold;
// neither overrides the other — Chapters 10.5 and 10.6.
assign tx_offer = launch_ok && !tx_paused;
assign tx_pop = tx_offer && tx_ready;tx_pop is the conjunction of an offer and an acceptance, which is Chapter 10.2's contract meeting Chapter 7.4's. The FIFO's show-ahead head is the transmitter's tx_data_i, !empty combined with permission is its tx_valid_i, and the transmitter's tx_ready_o is the FIFO's pop_i. The two handshakes compose without an adapter because both were built to the same shape — which was not an accident but is easy to take for granted.
tx_busy_i is tied low at the gate, deliberately. Chapter 10.5 §7 established that permission gates the launch and never truncates a frame in flight; wiring the transmitter's busy into the gate would have re-introduced exactly the behaviour that chapter rejected.
3. The RX Datapath Connection
The receive side has one extra concern — the status must travel with the byte:
// Synthesizable SystemVerilog — RX path assembly.
localparam int unsigned RX_ENTRY_W = DATA_W + 2;
assign rx_push_entry = {rx_perr, rx_ferr, rx_char}; // status ALONGSIDE data
assign rx_push = rx_char_valid && !cfg_rx_flush_i;
assign rx_char_ready = !rx_fifo_full; // backpressure the engine
assign {rd_parity_err_o, rd_frame_err_o, rd_data_o} = rx_pop_entry;
assign rd_valid_o = !rx_fifo_empty;rx_char_ready = !rx_fifo_full is the receiver's holding register being backpressured by the queue, which is Chapter 10.1 §6's connection. It works completely — and reaches no further than the receiver, which is Chapter 10.4 §5's limit and the reason flow control exists.
The pack and unpack are a single concatenation each, with no intermediate registers. Widening the entry costs two bits per entry and preserves the association Chapter 6.4 built the publish discipline to establish.
4. Flush
A flush discards buffered data on command. The question is what "discard" means when something is already moving.
| TX flush | RX flush | |
|---|---|---|
| Discards | queued bytes not yet launched | queued bytes not yet read |
| Does not discard | the frame currently being transmitted | the character currently being received |
| Why not | freezing mid-frame corrupts it — 10.5 §7 | the receiver has no abort; it completes and is then dropped |
// Synthesizable SystemVerilog — flush as a FIFO reset, not an engine reset.
uart_sync_fifo #(.WIDTH(DATA_W), .DEPTH(TX_DEPTH)) u_tx_fifo (
.clk(clk), .rst_n(rst_n && !cfg_tx_flush_i), // flush resets the QUEUE
.push_i(tx_push), ...
);5. Status Aggregation
The IP has several distinct points at which a byte can be lost, and they are not the same event:
| Loss point | Signal | Built in |
|---|---|---|
| receiver's holding register full | rx_overrun_o | 6.5 |
| RX FIFO full, push rejected | overflow_evt_o | 10.2 |
// Synthesizable SystemVerilog — both loss points feed ONE overrun flag.
uart_err_status u_status (
.clk(clk), .rst_n(rst_n),
.frame_error_evt_i (rx_char_valid && rx_char_ready && rx_ferr),
.parity_error_evt_i(rx_char_valid && rx_char_ready && rx_perr),
.overrun_evt_i (rx_ovr | rx_ovf_evt), // BOTH points
.break_evt_i (break_evt),
.clear_i(cfg_err_clear_i),
...
);Both must feed the flag, and reporting only one is a real defect. In a buffered receiver the FIFO fills first and the holding register overruns second, so a design that wires only overflow_evt_o misses nothing in the common case — and a design that wires only rx_overrun_o misses the case where the consumer is fast enough to keep the holding register moving but not fast enough to drain the queue. From software's point of view a byte was lost either way, and the aggregate flag answers that question rather than saying where.
The framing and parity events are qualified by acceptance, rx_char_valid && rx_char_ready. That is deliberate: a character the FIFO refused is not stored, so its status is not stored either, and counting it in the sticky flags would report an error against a byte the consumer will never see. The loss itself is reported — by the overrun term — which is the honest attribution.
6. The Assembled RTL
The listing below is the file that was compiled and simulated, and it already carries the three corrections Chapter 11.4 makes: the break threshold derived from DATA_W rather than hardcoded, a width-safe comparison for the software-flow codes, and rx_active taken from a published port on the receiver instead of a hierarchical reference into its state register — each found by asking what would happen at a different parameter set — and the second loopback Chapter 11.5 adds.
// ===========================================================================
// Module 11 — the complete UART IP.
//
// Every sub-block below was built and verified independently in Modules 6-10
// and is instantiated here UNMODIFIED. What this file adds is the integration:
// enable fan-out, configuration distribution, status aggregation, flush,
// loopback, and the decisions that only exist once the blocks meet.
//
// Deliberately NOT here: the bus-facing register map, interrupt controller
// and DMA hooks (Module 13); CDC and reset methodology (Module 12).
// ===========================================================================
module uart_ip
import uart_parity_pkg::*;
#(
parameter int unsigned CLK_HZ = 100_000_000,
parameter int unsigned BAUD_HZ = 115_200,
parameter int unsigned DATA_W = 8,
parameter int unsigned OVERSAMPLE = 16,
parameter int unsigned TX_DEPTH = 16,
parameter int unsigned RX_DEPTH = 16,
parameter int unsigned RX_TRIGGER = 12, // service when level >= this
parameter int unsigned TX_TRIGGER = 4, // refill when level <= this
parameter int unsigned FC_STOP = 12, // withdraw permission at/above
parameter int unsigned FC_RESUME = 6 // restore permission at/below
) (
input logic clk,
input logic rst_n,
// ── serial pins ──────────────────────────────────────────────────────
input logic rx_i,
output logic tx_o,
input logic cts_n_i,
output logic rts_n_o,
// ── configuration (Module 13 drives these from registers) ────────────
input parity_mode_t cfg_parity_i,
input logic cfg_sw_flow_en_i,
input logic cfg_hw_flow_en_i,
input logic cfg_loopback_i, // INTERNAL: tx engine -> rx engine
input logic cfg_line_loop_i, // LINE: rx pin -> tx pin, for a remote tester
input logic cfg_tx_flush_i, // one cycle
input logic cfg_rx_flush_i, // one cycle
input logic cfg_err_clear_i, // one cycle
// ── write side: producer pushes bytes to transmit ────────────────────
input logic [DATA_W-1:0] wr_data_i,
input logic wr_valid_i,
output logic wr_ready_o,
// ── read side: consumer pops received bytes + their status ───────────
output logic [DATA_W-1:0] rd_data_o,
output logic rd_parity_err_o,
output logic rd_frame_err_o,
output logic rd_valid_o,
input logic rd_ready_i,
// ── aggregated status (Module 13 exposes these) ──────────────────────
output logic tx_trigger_o,
output logic rx_trigger_o,
output logic tx_empty_o,
output logic rx_empty_o,
output logic busy_o,
output logic err_frame_o,
output logic err_parity_o,
output logic err_overrun_o,
output logic err_break_o,
output logic break_active_o,
output logic any_error_o
);
// RX entries carry their per-character status — Chapter 10.2 §8.
localparam int unsigned RX_ENTRY_W = DATA_W + 2;
// The break threshold is a property of the CONFIGURED frame, not of this
// file — Chapter 11.4. 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
// Software flow control compares 8-bit control codes. Narrower characters
// zero-extend; wider ones compare on the low byte, where the codes live.
localparam int unsigned SW_CMP_W = (DATA_W < 8) ? DATA_W : 8;
// Parameter legality, checked at ELABORATION — Chapter 11.4. An `initial`
// block would only complain once somebody ran a simulation; these stop the
// build, so an illegal set 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");
end
// ── ONE timing generator, fanned out to both halves — Chapter 8.4 ────
logic os_tick, baud_tick;
uart_timing_gen #(.CLK_HZ(CLK_HZ), .BAUD_HZ(BAUD_HZ), .OVERSAMPLE(OVERSAMPLE))
u_timing (.clk(clk), .rst_n(rst_n), .os_tick_o(os_tick), .baud_tick_o(baud_tick));
// ── configuration capture: frozen while either half is active ────────
// Chapter 11.3. A change is held until the link is quiet, so a frame in
// flight is never reformatted underneath itself.
parity_mode_t cfg_parity_q;
logic cfg_pending_q;
parity_mode_t cfg_parity_shadow_q;
logic link_quiet;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cfg_parity_q <= PARITY_NONE;
cfg_parity_shadow_q <= PARITY_NONE;
cfg_pending_q <= 1'b0;
end else begin
if (cfg_parity_i != cfg_parity_shadow_q) begin
cfg_parity_shadow_q <= cfg_parity_i;
cfg_pending_q <= 1'b1;
end
if (cfg_pending_q && link_quiet) begin
cfg_parity_q <= cfg_parity_shadow_q;
cfg_pending_q <= 1'b0;
end
end
end
// ── serial routing, including internal loopback — Chapter 11.5 ───────
// ── the two loopbacks — Chapter 11.5 ─────────────────────────────────
// They face opposite directions and are independent: internal loopback
// lets the IP test itself without the board; line loopback lets the far
// end test the board without trusting this IP's engines. Both set is
// legal and not a loop, because rx_i is an input.
logic tx_line, rx_line;
assign tx_o = cfg_line_loop_i ? rx_i : tx_line;
assign rx_line = cfg_loopback_i ? tx_line : rx_i;
// ── TX FIFO ──────────────────────────────────────────────────────────
logic [DATA_W-1:0] tx_fifo_head;
logic tx_fifo_empty, tx_fifo_full, tx_pop;
logic [$clog2(TX_DEPTH+1)-1:0] tx_level;
logic tx_push;
assign tx_push = wr_valid_i && !tx_fifo_full && !cfg_tx_flush_i;
assign wr_ready_o = !tx_fifo_full;
uart_sync_fifo #(.WIDTH(DATA_W), .DEPTH(TX_DEPTH)) u_tx_fifo (
.clk(clk), .rst_n(rst_n && !cfg_tx_flush_i),
.push_i(tx_push), .push_data_i(wr_data_i),
.pop_i(tx_pop), .pop_data_o(tx_fifo_head),
.empty_o(tx_fifo_empty), .full_o(tx_fifo_full), .level_o(tx_level),
.overflow_evt_o(), .underflow_evt_o());
// ── flow-control permission: hardware AND software — Ch 10.5 / 10.6 ──
logic tx_paused, xon_consume, launch_ok;
uart_tx_gate u_gate (
.clk(clk), .rst_n(rst_n),
.cts_n_i(cfg_hw_flow_en_i ? cts_n_i : 1'b0), // disabled = permitted
.tx_busy_i(1'b0), // NOT in the decision
.tx_fifo_empty_i(tx_fifo_empty),
.launch_allowed_o(launch_ok));
// ── transmitter ──────────────────────────────────────────────────────
logic tx_ready, tx_busy;
logic tx_offer;
// Offer a byte only when the FIFO has one AND both flow-control
// mechanisms permit it. Either can withhold; neither overrides.
assign tx_offer = launch_ok && !tx_paused;
assign tx_pop = tx_offer && tx_ready;
uart_tx #(.DATA_W(DATA_W)) u_tx (
.clk(clk), .rst_n(rst_n), .baud_tick_i(baud_tick),
.tx_data_i(tx_fifo_head), .parity_mode_i(cfg_parity_q),
.tx_valid_i(tx_offer), .tx_ready_o(tx_ready),
.tx_o(tx_line), .tx_busy_o(tx_busy));
// ── receiver ─────────────────────────────────────────────────────────
logic [DATA_W-1:0] rx_char;
logic rx_char_valid, rx_char_ready, rx_perr, rx_ferr, rx_ovr;
logic rx_active;
uart_rx #(.DATA_W(DATA_W), .OVERSAMPLE(OVERSAMPLE)) u_rx (
.clk(clk), .rst_n(rst_n), .rx_i(rx_line), .os_tick_i(os_tick),
.parity_mode_i(cfg_parity_q),
.rx_data_o(rx_char), .rx_valid_o(rx_char_valid), .rx_ready_i(rx_char_ready),
.rx_parity_err_o(rx_perr), .rx_frame_err_o(rx_ferr), .rx_overrun_o(rx_ovr),
.rx_active_o(rx_active));
// ── RX FIFO, entries carrying status ─────────────────────────────────
logic [RX_ENTRY_W-1:0] rx_push_entry, rx_pop_entry;
logic rx_fifo_empty, rx_fifo_full, rx_push, rx_ovf_evt;
logic [$clog2(RX_DEPTH+1)-1:0] rx_level;
assign rx_push_entry = {rx_perr, rx_ferr, rx_char};
assign rx_push = rx_char_valid && !cfg_rx_flush_i;
assign rx_char_ready = !rx_fifo_full; // backpressure the receiver
uart_sync_fifo #(.WIDTH(RX_ENTRY_W), .DEPTH(RX_DEPTH)) u_rx_fifo (
.clk(clk), .rst_n(rst_n && !cfg_rx_flush_i),
.push_i(rx_push), .push_data_i(rx_push_entry),
.pop_i(rd_ready_i && rd_valid_o), .pop_data_o(rx_pop_entry),
.empty_o(rx_fifo_empty), .full_o(rx_fifo_full), .level_o(rx_level),
.overflow_evt_o(rx_ovf_evt), .underflow_evt_o());
assign {rd_parity_err_o, rd_frame_err_o, rd_data_o} = rx_pop_entry;
assign rd_valid_o = !rx_fifo_empty;
// ── watermarks ───────────────────────────────────────────────────────
uart_fifo_watermark #(.DEPTH(RX_DEPTH), .RX_TRIGGER(RX_TRIGGER), .TX_TRIGGER(RX_DEPTH))
u_rx_wm (.level_i(rx_level), .rx_trigger_o(rx_trigger_o), .tx_trigger_o());
uart_fifo_watermark #(.DEPTH(TX_DEPTH), .RX_TRIGGER(TX_DEPTH), .TX_TRIGGER(TX_TRIGGER))
u_tx_wm (.level_i(tx_level), .rx_trigger_o(), .tx_trigger_o(tx_trigger_o));
// ── hardware flow control from RX occupancy ──────────────────────────
logic rts_n_int, allow_remote;
uart_flow_ctrl #(.DEPTH(RX_DEPTH), .STOP_LEVEL(FC_STOP), .RESUME_LEVEL(FC_RESUME))
u_fc (.clk(clk), .rst_n(rst_n), .level_i(rx_level),
.rts_n_o(rts_n_int), .allow_remote_o(allow_remote));
// Disabled hardware flow control drives the permitted level, not the pin's
// idle state — an unconfigured link must not appear to be backpressuring.
assign rts_n_o = cfg_hw_flow_en_i ? rts_n_int : 1'b0;
// ── software flow control watches the RECEIVED character stream ──────
uart_xon_xoff u_xon (
.clk(clk), .rst_n(rst_n),
.rx_char_valid_i(rx_char_valid && rx_char_ready),
.rx_char_data_i(8'(rx_char[SW_CMP_W-1:0])),
.sw_flow_en_i(cfg_sw_flow_en_i),
.tx_paused_o(tx_paused), .consume_char_o(xon_consume));
// ── break detection, as an observer — Chapter 9.4 ────────────────────
logic break_evt;
uart_break_detect #(.OVERSAMPLE(OVERSAMPLE), .FRAME_BITS(FRAME_BITS_MAX)) u_break (
.clk(clk), .rst_n(rst_n), .os_tick_i(os_tick), .rx_sync_i(rx_line),
.break_active_o(break_active_o), .break_evt_o(break_evt),
.break_released_o());
// ── status aggregation — Chapter 11.2 §5 ─────────────────────────────
uart_err_status u_status (
.clk(clk), .rst_n(rst_n),
.frame_error_evt_i (rx_char_valid && rx_char_ready && rx_ferr),
.parity_error_evt_i(rx_char_valid && rx_char_ready && rx_perr),
.overrun_evt_i (rx_ovr | rx_ovf_evt), // BOTH loss points
.break_evt_i (break_evt),
.clear_i(cfg_err_clear_i),
.frame_error_sticky_o (err_frame_o),
.parity_error_sticky_o(err_parity_o),
.overrun_sticky_o (err_overrun_o),
.break_sticky_o (err_break_o),
.any_error_o (any_error_o));
assign tx_empty_o = tx_fifo_empty;
assign rx_empty_o = rx_fifo_empty;
// busy_o means WORK IN PROGRESS, not "data exists somewhere".
//
// Unread bytes sitting in the RX FIFO are NOT busy — they are reported by
// rx_empty_o and rx_trigger_o, which is what a consumer polls. Including
// RX occupancy here makes busy_o impossible to clear until software has
// read everything, so a driver that waits for !busy before sleeping never
// sleeps. Chapter 11.2 §5 develops this; it was a real defect in the first
// version of this file and simulation deadlocked on it.
assign busy_o = tx_busy | !tx_fifo_empty | rx_active;
// link_quiet is a STRICTER condition used only to commit configuration:
// nothing transmitting, nothing queued to transmit, no frame being
// received. It also excludes RX occupancy, for the same reason.
assign link_quiet = !tx_busy && tx_fifo_empty && !rx_active;
endmodule7. The Review Questions for an Integration
A top level is reviewed differently from a block. The block questions — widths, reset, off-by-one — were answered where each block was built. What remains is about connections, and this list is the one worth running:
| Question | Here |
|---|---|
| Is any sub-block edited rather than instantiated? | No — every one is used as published |
| Does every crossing signal fit one of the four kinds? | Yes — enable, valid/ready, event, level |
| Is any enable used as a clock? | No — one clk throughout |
| Are both asynchronous inputs synchronised at their boundary? | Yes — rx_i in the receiver, cts_n_i in the gate |
| Does any status signal aggregate things that are not alike? | Checked — §5; both overrun points are alike, busy_o was not |
| Does flush reach anything other than the queues? | No — §4 |
| Can two permissions conflict? | No — they are ANDed; either may withhold |
| Does a disabled feature drive a safe value? | Checked — 11.3 §4 |
| Is any block's contract stretched by how it is connected? | No — tx_busy_i tied low is the closest, and it is deliberate |
The fifth row is where busy_o failed. Chapter 11.1 §6 described the defect; the review question that would have caught it is does this signal aggregate things that are alike? — and receive occupancy is not alike to work in progress.
8. Verification
Integration testing answers a different question from unit testing, and both suites are kept.
Loopback is the strongest single test. Routing tx_o internally to the receiver makes the whole datapath self-checking: a byte written must emerge unchanged and in order, having passed through the TX FIFO, the launch gate, the transmitter, the wire, the receiver, and the RX FIFO. Chapter 11.5 builds it properly.
From the integration suite:
| Scenario | Result |
|---|---|
| reset: line idle, both FIFOs empty, not busy, no errors | pass |
| enable ratio over 200,000 cycles | 31,999 : 1,999 = exactly 16 |
| loopback 8N1, eight standard patterns | 8/8 in order, no errors |
| loopback 8E1, 8O1 | pass |
RX FIFO past FC_STOP | rx_trigger and rts_n assert |
drain below FC_RESUME | rts_n released |
| TX flush | queue empties, engine undisturbed |
| RX flush | queue empties |
| error clear | aggregate clears |
busy_o with data unread | clears |
TOTAL CHECKS: 53 FAILURES: 0 (27 bytes looped)// Assertion — the two enables keep an exact ratio, because one is a
// subsample of the other. This fails if someone "decouples" them into
// independent generators, which is the most likely restructuring error.
property p_baud_implies_os;
@(posedge clk) disable iff (!rst_n) baud_tick |-> os_tick;
endproperty
assert property (p_baud_implies_os);
// Assertion — flush never disturbs a frame in flight.
property p_flush_spares_the_engine;
@(posedge clk) disable iff (!rst_n)
cfg_tx_flush_i |=> $stable(tx_o) || $past(baud_tick);
endproperty
assert property (p_flush_spares_the_engine);
// Assertion — a byte accepted by the write port is never silently discarded
// except by an explicit flush.
property p_accepted_write_is_queued;
@(posedge clk) disable iff (!rst_n)
(wr_valid_i && wr_ready_o && !cfg_tx_flush_i) |=> (dut_tx_level > 0);
endproperty
// Assertion — busy_o does not depend on receive occupancy. This is the
// §5 / Chapter 11.1 §6 defect, stated so a later edit cannot reintroduce it.
property p_busy_excludes_rx_fifo;
@(posedge clk) disable iff (!rst_n)
(!tx_busy && tx_fifo_empty && rx_idle) |-> !busy_o;
endproperty
assert property (p_busy_excludes_rx_fifo);The last is worth writing even though the code is now correct. It encodes a decision that was made wrongly once, and the reasonable-looking alternative is one line away.
9. Debugging
10. What This Means on an FPGA
Integration adds almost no logic. The top level is concatenations, a handful of AND terms and two comparators' worth of tie-offs. Essentially all the area is in the blocks it instantiates.
The enable fan-out is the only broadcast, and at UART scale it is unremarkable — a few dozen endpoints toggling at under 2 MHz.
Probe at the block boundaries. os_tick, baud_tick, both FIFO levels, rx_valid/rx_ready, tx_ready, and the status set localise a failure to one block in a single capture. That is the practical dividend of having built and verified them separately.
Keep the flush pulses one cycle. A held flush holds its FIFO in reset, which presents as a queue that is permanently empty and a write port that is permanently ready while nothing is transmitted — an easy defect to create in a register decode and a confusing one to read.
11. Understanding Check
12. Summary
One generator, fanned out. The receiver takes the oversample enable and the transmitter every sixteenth of them — 31,999 : 1,999 measured, exactly 16 — and the ratio cannot drift because one is a subsample of the other.
The TX and RX handshakes compose without adapters, because the FIFO contract and the engine contracts were built to the same valid/ready shape. tx_busy_i is tied low at the gate deliberately: permission gates the launch and never truncates a frame.
The RX entry packs status with data in one concatenation each way, preserving the association through the buffer.
Flush resets the queues and not the engines. Resetting an engine mid-character corrupts the link rather than cleaning it, and the documented consequence is that one more character may appear on the wire after a TX flush.
Two distinct loss points feed one overrun flag, because from software's view they are the same event — while framing and parity events are qualified by acceptance, so no error is reported against a byte the consumer will never see.
Top-level review is about connections: no block edited, every crossing fitting one of four disciplines, no enable used as a clock, both asynchronous inputs synchronised, and no status aggregating things that are not alike — which is where busy_o failed.
Verified: 53 checks, 0 failures, 27 bytes round-tripped in three parity modes.
13. What Comes Next
Configuration has been treated as a signal that arrives and is used. Chapter 11.3 asks the harder question: what happens when it changes.
A parity mode altered while a frame is in flight would reformat that frame underneath itself — Chapter 7.5 §3 solved that for the transmitter alone, and at the top level the same change must reach both halves consistently, or the two ends of the same IP disagree about the frame they are exchanging. That chapter builds the deferral, and confronts what a disabled feature should drive.
Browse the full path on the UART tutorials index. For the blocks this chapter connects, read back to Chapter 10.2 and Chapter 8.4.
Continue learning
Related tutorials
- 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
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
Parity Check, Stop-Bit Validation and Error Status
The receiver decides whether a frame was good, then faces the harder question: which frame does each status flag describe? Getting that wrong lets an incoming frame rewrite the status of a byte the consumer has not yet read.
- Related topic
Error Status, Sticky Flags and Receiver Re-Synchronisation
Transient events become persistent state, and the design decision is what happens when a clear and a new event land on the same clock edge. Detection and recovery are different problems.
Where this fits
Part of the UART curriculum.
