Capstone: a UART with FIFOs and CDC
Assemble a complete UART on APB: the transmitter, a receiver that finds each start bit on a synchronized line and samples the bit centres, two FIFOs and a register block.
- Lesson
- R8.2
- Time
- 45 min
- Runs on
- Your browser
- Check
- 5 questions
A UART receiver read its serial line straight into its start-bit detector. Two flops sampled that line: one kept its last value, the other decided that a frame had begun. When the line fell just before a clock edge, the two could settle differently, and the start bit vanished. The receiver then framed a byte on a falling edge inside the data, and the bytes after it came out garbled.
| Receiver input | Bytes read intact | Lost | Garbled | STATUS showed an error | Start bits lost |
|---|---|---|---|---|---|
rxd straight in | 86 | 2 | 12 | 9 times | 4 |
rxd through two flops | 100 | 0 | 0 | 0 times | 0 |
The lab: 100 bytes from a transmitter on an unrelated clock, with the bench's model of flops that sample a changing line. Four lost start bits damaged 14 bytes.
The whole UART
A UART sends and receives bytes over one wire each way, with no clock beside them (R3.6). This one has five parts. The transmitter of R3.6 sends. A receiver, built like R3.6's serial decoder, listens. Two FIFOs of four bytes (R5.2) let software write and read in bursts. A register block on APB (R8.1) gives software DATA, STATUS and CTRL.
- Send
A write to DATA pushes 0x48 into the TX FIFO at its completion edge. The transmitter takes it when idle and sends it on
txd.Follow the calls, values and waits
- data byte 0x48: APB registers → TX FIFO
- data byte 0x48: TX FIFO → Transmitter
- Cross
The line from the far end,
rxd, changes whenever the far clock says. Two flops insync2turn it intorx_line, a signal that changes only at this clock's edges. - Receive
The receiver assembles 0x41 and pushes it, with its frame and parity flags, into the RX FIFO. A read of DATA pops it at its completion edge.
Follow the calls, values and waits
- data byte 0x41: Receiver → RX FIFO
- data byte 0x41: RX FIFO → APB registers
Read the diagram as text
- APB registers: DATA, STATUS, CTRL; irq
- TX FIFO: 4 bytes
- Transmitter: R3.6: start, 8 data, parity, stop
- RX FIFO: 4 bytes; full means OVR
- Receiver: finds the fall, samples bit centres
- sync2: two flops, reset to 1
- APB registers → TX FIFO: DATA write
- TX FIFO → Transmitter: byte
- sync2 → Receiver: rx_line
- Receiver → RX FIFO: byte + errors
- RX FIFO → APB registers: DATA read
Only one signal crosses clock domains: rxd. Everything else runs on PCLK.
| Offset | Register | Access | Fields |
|---|---|---|---|
| 0x00 | DATA | write: push TX FIFO; read: pop RX FIFO | byte in [7:0]; PSLVERR when the TX FIFO is full (write) or the RX FIFO is empty (read) |
| 0x04 | STATUS | bits 2-0 read-only; bits 6-4 write-1-to-clear | [0] RXNE byte waiting, [1] TXF TX full, [2] TXE all sent; [4] OVR byte lost, [5] FERR stop bit 0, [6] PERR parity |
| 0x08 | CTRL | read/write | [0] RXIE irq while RXNE, [1] ERRIE irq while any error bit is 1 |
The register map, written once as localparams in the design. A read of DATA pops the FIFO, so it acts at the completion edge only (R3.5).
The receiver: find the fall, wait half a bit, sample
The line idles high. A frame starts with a fall. The receiver waits half a bit-time to reach the middle of the start bit and checks it is still 0. Then it samples once per bit-time: eight data bits, parity, stop. Each sample sits near a bit centre, which leaves the most room for a far clock that runs fast or slow.
line is the synchronizer's output, the receiver's input; prev is the line one cycle ago.- Cycle 1
Edge 4 is the first edge after the fall. The synchronizer's first flop,
s1, takes the 0. - Cycle 2
Edge 5: the second flop passes it on, so
lineis 0.prevstill holds 1. - Cycle 3
Edge 6:
previs 1 andlineis 0, a fall.busyrises and the baud counter starts from 0. - Cycle 6
The counter reads 3, the middle cycle of an 8-cycle bit, so
sampleis 1. Edge 10 samples the start bit: still 0, so the frame is real. - Cycle 14
Eight cycles later
sampleis 1 again. Edge 18 samples data bit 0 of 0x41, a 1, andbitnmoves to 2.
Measured with a probe of the lab's design. The value edge 10 reads left rxd at edge 8, 49 ns into the 80 ns start bit.
The synchronizer delays the line by two cycles. It delays the fall and every sample alike, so the samples still land in the same place. Without it, each step happens two edges sooner and reads the same moment of the line.
Centre sampling is what tolerates clock mismatch. In the lab the far end sends 78 ns bits against the receiver's 80, so each sample lands 2 ns later in its bit than the one before. Measured on the bench, the stop bit is read 60 to 70 ns into its 78 ns. A receiver that skips the half-bit wait, against a slow far end, reads data bits from the bit before.
Why the line needs a synchronizer
In the shipped design rx_line is rxd itself. Two receiver flops read it at the edge where a start bit begins: prev_q stores the line, and busy_q decides a frame has started. A flop that samples a changing input can settle late, to either value (R6.1). When prev_q settles to 0 and busy_q saw no fall, the next edge finds prev 0 and line 0. No fall: the start bit is gone.
Line straight in
assign rx_line = rxd;
// prev_q and busy_q both sample
// a line from another clock
Two flops can settle differently at one edge. 4 start bits lost in 100 bytes.
Line through sync2
sync2 u_sync (.clk(pclk), .rst_n(presetn),
.d(rxd), .q(rx_sync));
assign rx_line = rx_sync;
Only s1 samples rxd, and it has a whole cycle to settle. 100 bytes intact.
Lint sees part of this. On the shipped design Verilator reports rx_sync as unused: a synchronizer was built and nothing reads it. The synchronizer resets to 1, the idle level of the line, so leaving reset never looks like a start bit (R6.5).
The lab: one line to change
| Port | Direction | Width | Meaning |
|---|---|---|---|
| pclk | input | 1 | APB clock |
| presetn | input | 1 | APB reset, asynchronous, active-low |
| psel | input | 1 | APB select |
| penable | input | 1 | APB access phase |
| pwrite | input | 1 | APB write |
| paddr | input | [31:0] | APB address |
| pwdata | input | [31:0] | APB write data |
| pstrb | input | [3:0] | APB write strobes |
| pprot | input | [2:0] | APB protection |
| pready | output | 1 | APB ready |
| prdata | output | [31:0] | APB read data |
| pslverr | output | 1 | APB error |
| rxd | input | 1 | the serial line in: asynchronous to pclk |
| txd | output | 1 | the serial line out |
| irq | output | 1 | interrupt |
- Clock
- PCLK 10 ns;
rxdcomes from an unrelated clock and may change at any time - Reset
- PRESETn asynchronous, active-low; the synchronizer and
txdreset to 1 - Latency
- no wait states; every effect at the completion edge
- Parameters
- Frame: start 0, eight data bits least significant first, even parity, stop 1;
CLKS_PER_BITcycles per bit, 8 in the lab. Registers: DATA, STATUS, CTRL as in the map. FIFOs: four bytes each way; a byte that arrives with the RX FIFO full is dropped and sets OVR - Corner cases
- a fall just before an edge; a far clock 2.5% fast; back-to-back frames; a burst into a full RX FIFO
The bench writes "Hi" to DATA, and the serial decoder of R3.6 reads it off txd. Meanwhile a transmitter on a 13 ns clock sends 100 bytes, and a driver polls STATUS and reads DATA. Run the shipped design: 86 bytes intact. The bench's model reads rx_line and writes into the flops u_sync.s1, u_rx.prev_q and u_rx.busy_q, so keep those names. Change assign rx_line = rxd; to assign rx_line = rx_sync; and run again: 100 intact, no start bit lost.
Synthesize both. The shipped design is 493 cells and 132 flops: the unused synchronizer is removed. With it the UART is 474 cells, 134 flops and 17 levels of logic on this flow. Two more flops and fewer cells is no paradox. The synchronized line gives the optimizer a cleaner function, so the gates around it shrink by more than the two flops add. The FIFO storage is 64 of those flops.
Asked in interviews
How does a UART receiver find the bit centre?
It watches for the fall that starts the start bit, waits half a bit-time and checks the line is still 0. From there it samples once per bit-time, so each sample sits near a centre with room for clock mismatch either way.
Why is a UART a clock domain crossing?
The far end's clock is unrelated to yours, so the receive line can change at any moment, including just before your edge. It needs a two-flop synchronizer before any logic reads it, and only one flop may sample it.
What are the FIFOs for?
They decouple the serial rate from software. The TX FIFO lets a driver write a burst and leave; the RX FIFO holds bytes until the driver comes back. Size the RX FIFO for the longest time the driver may be away.
Try it
The first four lines are the transmit path: two DATA writes, then the decoder reading both bytes off txd. The fifth line compares what the driver read with what the far end sent. In 67 edges rxd had changed less than 1 ns before; in four of them the receiver lost a start bit. The checker counts the DATA reads, the STATUS polls and the clears: the bus itself broke no rule.
With assign rx_line = rx_sync; the last three lines become remote sent 100 bytes: 100 read intact, 0 lost, 0 garbled; STATUS showed an error 0 times, model: 67 edges saw rxd change, 0 start bits lost and checker: 2 writes, 1712 reads, 0 wait edges, 0 rules broken, 0 notes.
Synthesize the design to measure its cells and inspect the mapped gates.
e3-e4 WRITE 0x00000000 data=0x00000048 strb=1111 waits=0 OKAY e5-e6 WRITE 0x00000000 data=0x00000069 strb=1111 waits=0 OKAY e6 rx 0x48 ok e95 rx 0x69 ok remote sent 100 bytes: 86 read intact, 2 lost, 12 garbled; STATUS showed an error 9 times model: 67 edges saw rxd change, 4 start bits lost checker: 11 writes, 1706 reads, 0 wait edges, 0 rules broken, 0 notesOriginal design · 493 cells · 132 flip-flops · 0 latches · 17 logic levels. Synthesis uses Yosys; counts depend on the toolchain.
Read included file: tb.sv
`timescale 1ns/1ps
module tb;
logic clk = 0;
always #5 clk = ~clk; // the sampling clock: rising edges at 5, 15, 25, ... ns
logic clk_b = 0; // the second clock: rising edges at 2.6, 15.6, ... ns
initial begin
#2.6;
forever begin clk_b = 1; #6.5; clk_b = 0; #6.5; end
end
logic rst_n = 1; // falls at 1 ns: a real edge, so asynchronous resets act before any clock
int edge_n = -1; // the number of the last rising edge of clk
always @(posedge clk) edge_n = edge_n + 1;
localparam int CPB = 8; // the UART: 8 cycles of clk per bit, 80 ns
localparam int CPB_B = 6; // the remote transmitter: 6 cycles of clk_b per bit
wire pclk = clk, presetn = rst_n; // the APB names of the UART's clock and reset
logic psel = 0, penable = 0, pwrite = 0;
logic [31:0] paddr = 0, pwdata = 0;
logic [3:0] pstrb = 0;
logic [2:0] pprot = 0;
logic pready, pslverr;
logic [31:0] prdata;
logic rxd = 1, txd, irq;
apb_checker chk (.pclk(pclk), .presetn(presetn), .psel(psel), .penable(penable), .pwrite(pwrite), .paddr(paddr),
.pwdata(pwdata), .pstrb(pstrb), .pprot(pprot), .pready(pready), .pslverr(pslverr));
serial_decoder #(.CLKS_PER_BIT(CPB)) dec (.clk(clk), .line(txd)); // reads what the UART sends
`include "rtl_apb_tasks.svh"
// The remote transmitter, in the clk_b domain: one frame per call, then gap idle cycles of clk_b.
int sent = 0;
task automatic remote_send(input logic [7:0] b, input int gap);
logic [10:0] f;
f = {1'b1, ^b, b, 1'b0}; // stop, even parity, data, start: sent from bit 0
for (int i = 0; i < 11; i++) begin
@(posedge clk_b); rxd = f[i];
repeat (CPB_B - 1) @(posedge clk_b);
end
repeat (gap) @(posedge clk_b);
sent++;
endtask
// ---- A model of flops that sample a changing line (R6.1). An RTL flop is never metastable. ----
// When the line a flop samples changed less than WINDOW before an edge of clk, the flop settles to
// the old or the new value at random, and each flop settles on its own. The flops modelled: the
// synchronizer's first flop u_sync.s1 (it reads rxd), and the two receiver flops that read rx_line
// when a start bit begins, u_rx.prev_q and u_rx.busy_q. Fixed seed: every run prints the same lines.
localparam realtime WINDOW = 1.0;
int unsigned seed = 5;
function automatic bit coin();
seed = seed * 32'd1664525 + 32'd1013904223;
return seed[31];
endfunction
realtime t_raw = -100.0, t_line = -100.0;
logic raw_now = 1, raw_old = 1, line_now = 1, line_old = 1;
always @(rxd) begin raw_old = raw_now; raw_now = rxd; t_raw = $realtime; end
always @(dut.rx_line) begin line_old = line_now; line_now = dut.rx_line; t_line = $realtime; end
int near_edges = 0, misses = 0;
always @(posedge clk) if (rst_n) begin : model
bit raw_near, line_near, busy_was;
logic r_old, l_old;
raw_near = ($realtime - t_raw < WINDOW); line_near = ($realtime - t_line < WINDOW);
r_old = raw_old; l_old = line_old; busy_was = dut.u_rx.busy_q;
#0.01;
if (raw_near) begin near_edges++; if (coin()) dut.u_sync.s1 = r_old; end
if (line_near) begin
if (coin()) dut.u_rx.prev_q = l_old; // prev_q kept the old line
if (coin() && !busy_was && dut.u_rx.busy_q) dut.u_rx.busy_q = 1'b0; // busy_q saw no fall
if (!busy_was && !dut.u_rx.busy_q && !dut.u_rx.prev_q) misses++; // the fall is gone
end
end
// The driver: polls STATUS, reads DATA while RXNE is 1, counts and clears the error bits.
logic [7:0] got [$], exp_q [$];
int errs = 0;
task automatic drain();
apb_read(32'h04);
while (prdata_q[0]) begin
apb_read(32'h00); got.push_back(prdata_q[7:0]);
apb_read(32'h04);
end
if (prdata_q[6:4] != 0) begin errs++; apb_write(32'h04, {25'd0, prdata_q[6:4], 4'd0}); end
endtask
uart_apb #(.CLKS_PER_BIT(CPB)) dut (.pclk(pclk), .presetn(presetn), .psel(psel), .penable(penable), .pwrite(pwrite),
.paddr(paddr), .pwdata(pwdata), .pstrb(pstrb), .pprot(pprot), .pready(pready), .prdata(prdata), .pslverr(pslverr),
.rxd(rxd), .txd(txd), .irq(irq));
// Advance to the next rising edge of clk, wait for the design to settle, then print.
task automatic tick();
@(posedge clk); #1;
endtask
initial begin
#1 rst_n = 0; tick(); tick(); rst_n = 1; // asserted at 1 ns, clk edges 0 and 1 in reset, released 1 ns after edge 1
tick();
apb_write(32'h00, 32'h48); // DATA: send "Hi" ...
apb_write(32'h00, 32'h69);
quiet = 1;
fork
begin // ... while the remote end sends 100 bytes
logic [7:0] b;
b = 8'h41;
for (int i = 0; i < 100; i++) begin exp_q.push_back(b); remote_send(b, (i * 7) % 11); b = b * 8'd5 + 8'd3; end
end
while (sent < 100) begin drain(); idle(4); end
join
repeat (40) begin drain(); idle(4); end
begin // match what was read against what was sent, in order
int j, ok, lost, garbled;
j = 0; ok = 0; lost = 0; garbled = 0;
foreach (got[i]) begin
int k;
k = j;
while (k < j + 4 && k < exp_q.size() && exp_q[k] != got[i]) k++;
if (k < exp_q.size() && k < j + 4 && exp_q[k] == got[i]) begin ok++; lost += k - j; j = k + 1; end
else begin garbled++; j++; end // a garbled byte stands in for one that was sent
end
if (j < exp_q.size()) lost += exp_q.size() - j;
$display("remote sent %0d bytes: %0d read intact, %0d lost, %0d garbled; STATUS showed an error %0d times", sent, ok, lost, garbled, errs);
$display("model: %0d edges saw rxd change, %0d start bits lost", near_edges, misses);
end
$finish(0); // quiet: no "$finish called" line
end
endmodule
Read included file: apb_checker.sv
`timescale 1ns/1ns
// apb_checker.sv: the rules of an APB interface, checked at every rising edge (read-only).
// reset PSEL is 1 while PRESETn is low;
// known PSEL is x, or a control signal, a strobed write byte or PREADY in access is x;
// setup PENABLE is 1 without a setup cycle (PSEL 1, PENABLE 0) before it;
// setup_len a setup cycle not followed by an access cycle;
// stable PSEL, PADDR, PWRITE, PPROT, PSTRB or the write data changed inside a transfer,
// or PSEL or PENABLE dropped before PREADY;
// enable_drop PENABLE still 1 in the cycle after a completed transfer;
// strb_read PSTRB not zero during a read;
// onehot more than one PSELx at once;
// timeout no PREADY after TIMEOUT wait cycles (a liveness watchdog, not a protocol rule).
// A NOTE (not a broken rule): PSLVERR is 1 outside the completion cycle, which the spec allows
// and recommends against (IHI 0024E 3.4).
module apb_checker #(parameter int NSEL = 1, parameter int TIMEOUT = 16) (
input logic pclk,
input logic presetn,
input logic [NSEL-1:0] psel,
input logic penable,
input logic pwrite,
input logic [31:0] paddr,
input logic [31:0] pwdata,
input logic [3:0] pstrb,
input logic [2:0] pprot,
input logic pready,
input logic pslverr);
int broken = 0, notes = 0, writes = 0, reads = 0, waits = 0, edge_n = -1, stall = 0;
logic p_any = 0, p_en = 0, p_rdy = 0, p_wr = 0;
logic [NSEL-1:0] p_sel = 0;
logic [31:0] p_addr = 0, p_wdata = 0;
logic [3:0] p_strb = 0;
logic [2:0] p_prot = 0;
task automatic report(input string rule, input string msg);
broken++;
$display("RULE %0s broken at edge %0d: %0s", rule, edge_n, msg);
endtask
task automatic note(input string rule, input string msg);
notes++;
$display("NOTE %0s at edge %0d: %0s", rule, edge_n, msg);
endtask
always @(posedge pclk) begin
logic any, en, rdy, bytes_x;
edge_n = edge_n + 1;
any = (|psel) === 1'b1; en = (penable === 1'b1); rdy = (pready === 1'b1);
if (presetn !== 1'b1) begin
if (any) report("reset", "PSEL is 1 while PRESETn is low");
any = 0; en = 0;
end else begin
bytes_x = 0;
for (int l = 0; l < 4; l++) if (pstrb[l] === 1'b1 && (^pwdata[8*l +: 8]) === 1'bx) bytes_x = 1;
if ((^psel) === 1'bx) report("known", "PSEL is x");
else if (any && (^{paddr, pwrite, penable, pstrb, pprot}) === 1'bx) report("known", "a control signal is x while PSEL is 1");
else if (any && pwrite === 1'b1 && bytes_x) report("known", "a strobed byte of PWDATA is x");
else if (any && en && pready === 1'bx) report("known", "PREADY is x in an access cycle");
if (any && en && !(p_any && (!p_en || !p_rdy)))
report("setup", "PENABLE is 1 without a setup cycle (PSEL 1, PENABLE 0) before it");
if (p_any && !p_en && !(any && en))
report("setup_len", "the setup cycle was not followed by an access cycle");
if (p_any && (!p_en || !p_rdy) && any) begin
if (psel !== p_sel) report("stable", $sformatf("PSEL changed from %b to %b inside a transfer", p_sel, psel));
if (paddr !== p_addr) report("stable", $sformatf("PADDR changed from 0x%0h to 0x%0h inside a transfer", p_addr, paddr));
if (pwrite !== p_wr) report("stable", "PWRITE changed inside a transfer");
if (pprot !== p_prot) report("stable", "PPROT changed inside a transfer");
if (pstrb !== p_strb) report("stable", "PSTRB changed inside a transfer");
if (p_wr && pwdata !== p_wdata) report("stable", $sformatf("PWDATA changed from 0x%0h to 0x%0h inside a transfer", p_wdata, pwdata));
if (p_en && !en) report("stable", "PENABLE dropped before PREADY");
end
if (p_any && p_en && !p_rdy && !any) report("stable", "PSEL dropped before PREADY");
if (p_any && p_en && p_rdy && en) report("enable_drop", "PENABLE is still 1 in the cycle after a completed transfer");
if (any && pwrite === 1'b0 && pstrb !== 4'b0000) report("strb_read", $sformatf("PSTRB is %b during a read", pstrb));
if ($countones(psel) > 1) report("onehot", $sformatf("PSELx is %b: two completers selected", psel));
if (any && en && rdy) begin
if (pwrite === 1'b1) writes++; else reads++;
stall = 0;
end else if (any && en) begin
waits++; stall++;
if (stall == TIMEOUT) report("timeout", $sformatf("no PREADY after %0d wait cycles", TIMEOUT));
end
if (pslverr === 1'b1 && !(any && en && rdy))
note("pslverr", "PSLVERR is 1 outside the completion cycle (ignored by the requester; recommended LOW)");
end
p_any = any; p_en = en; p_rdy = rdy; p_sel = psel; p_addr = paddr; p_wr = (pwrite === 1'b1);
p_wdata = pwdata; p_strb = pstrb; p_prot = pprot;
end
final $display("checker: %0d writes, %0d reads, %0d wait edges, %0d rules broken, %0d notes", writes, reads, waits, broken, notes);
endmodule
Read included file: rtl_apb_tasks.svh
// rtl_apb_tasks.svh: the requester side of the bench (read-only).
// apb_write(addr, data, strb) and apb_read(addr) run one transfer: SETUP for one cycle, ACCESS until
// PREADY, then the bus goes idle. Each transfer prints one line at its completion edge:
// e3-e4 WRITE 0x00000000 data=0x0000005a strb=1111 waits=0 OKAY
// (setup edge - completion edge). A read leaves its data in prdata_q and its error in pslverr_q.
// quiet = 1 turns the log off; watch = 1 prints the bus at every edge.
// cycle is the number of the last rising edge; it updates after the edge's active events, so at an
// edge every reader (the tasks, the watch line) sees the same old value and names the edge cycle + 1.
int cycle = -1;
always @(posedge pclk) cycle <= cycle + 1;
bit quiet = 0, watch = 0;
logic [31:0] prdata_q = 0;
logic pslverr_q = 0;
int waits_q = 0, give_up = 24;
always @(posedge pclk) if (watch)
$display("wave e%0d: psel=%b penable=%b pwrite=%b paddr=%h pwdata=%h pstrb=%b pready=%b prdata=%h pslverr=%b",
cycle + 1, psel, penable, pwrite, paddr, pwdata, pstrb, pready, prdata, pslverr);
task automatic apb_xfer(input bit wr, input logic [31:0] addr, input logic [31:0] data, input logic [3:0] strb);
int s_edge, w;
paddr = addr; pwrite = wr; pwdata = wr ? data : 32'h0; pstrb = wr ? strb : 4'h0; pprot = 3'b000;
psel = 1; penable = 0; // SETUP
s_edge = cycle + 1;
@(posedge pclk); #1 penable = 1; // ACCESS
w = 0;
@(posedge pclk);
while (pready !== 1'b1 && w < give_up) begin w++; @(posedge pclk); end
prdata_q = prdata; pslverr_q = pslverr; waits_q = w;
if (!quiet) begin
if (wr && pslverr) $display("e%0d-e%0d WRITE 0x%08h data=0x%08h strb=%b waits=%0d ERROR", s_edge, cycle + 1, addr, data, strb, w);
else if (wr) $display("e%0d-e%0d WRITE 0x%08h data=0x%08h strb=%b waits=%0d OKAY", s_edge, cycle + 1, addr, data, strb, w);
else if (pslverr) $display("e%0d-e%0d READ 0x%08h data=0x%08h waits=%0d ERROR", s_edge, cycle + 1, addr, prdata, w);
else $display("e%0d-e%0d READ 0x%08h data=0x%08h waits=%0d OKAY", s_edge, cycle + 1, addr, prdata, w);
end
#1 psel = 0; penable = 0; pwrite = 0; pstrb = 0; pwdata = 0; // idle
endtask
task automatic apb_write(input logic [31:0] addr, input logic [31:0] data, input logic [3:0] strb = 4'hf);
apb_xfer(1, addr, data, strb);
endtask
task automatic apb_read(input logic [31:0] addr);
apb_xfer(0, addr, 32'h0, 4'h0);
endtask
task automatic idle(input int n);
repeat (n) @(posedge pclk); #1;
endtask
Read included file: serial_decoder.sv
// serial_decoder.sv: the receiving end of the bench (read-only).
`timescale 1ns/1ps
// It samples the line once per cycle, 1 ns after each rising edge. When the line falls, the start
// bit has begun; the decoder waits half a bit-time to reach its middle, then samples every
// CLKS_PER_BIT cycles: eight data bits (least significant first), the parity bit, the stop bit.
// It prints one line per frame: the edge where the start bit began, the byte, and a verdict:
// e3 rx 0x5a ok e3 rx 0x5a PARITY ERROR e3 rx 0x5a FRAME ERROR (stop bit 0)
module serial_decoder #(parameter int CLKS_PER_BIT = 4) (input logic clk, input logic line);
int e = -1; // the number of the last rising edge
always @(posedge clk) e = e + 1;
int frames = 0, errors = 0;
task automatic skip(input int n); // move n samples on
repeat (n) begin @(posedge clk); #1; end
endtask
initial begin : rx
logic prev, stop, par;
logic [7:0] b;
int s;
prev = 1'b1;
forever begin
skip(1);
if (prev === 1'b1 && line === 1'b0) begin // the first low sample of a start bit
s = e;
skip(CLKS_PER_BIT / 2); // the middle of the start bit
if (line === 1'b0) begin
for (int k = 0; k < 8; k++) begin skip(CLKS_PER_BIT); b[k] = line; end
skip(CLKS_PER_BIT); par = line;
skip(CLKS_PER_BIT); stop = line;
frames++;
if (stop !== 1'b1) begin errors++; $display("e%0d rx 0x%h FRAME ERROR", s, b); end
else if ((^b ^ par) !== 1'b0) begin errors++; $display("e%0d rx 0x%h PARITY ERROR", s, b); end
else $display("e%0d rx 0x%h ok", s, b);
end
end
prev = line;
end
end
endmodule
Check yourself
Remember
- A UART's receive line comes from an unrelated clock: it crosses a two-flop synchronizer before any logic reads it, and only one flop samples it.
- The receiver finds the fall of the start bit, waits half a bit-time, checks the line is still 0, then samples once per bit-time near each centre.
- The synchronizer delays the fall and the samples alike, so the samples stay centred; centre sampling is what tolerates a far clock that is fast or slow.
- FIFOs decouple the serial rate from software; a byte that arrives with the RX FIFO full is lost, and a write-1-to-clear OVR bit says so.