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 inputBytes read intactLostGarbledSTATUS showed an errorStart bits lost
rxd straight in862129 times4
rxd through two flops100000 times0

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.

The UART: two paths through one register blockArrows show bytes and bits. The top row sends: DATA writes fill the TX FIFO, the transmitter drains it onto txd. The bottom row receives: rxd crosses the synchronizer, the receiver assembles bytes, the RX FIFO holds them for DATA reads.
The UART: two paths through one register blockArrows show bytes and bits. The top row sends: DATA writes fill the TX FIFO, the transmitter drains it onto txd. The bottom row receives: rxd crosses the synchronizer, the receiver assembles bytes, the RX FIFO holds them for DATA reads. component APB registers DATA, STATUS,CTRL; irq queue TX FIFO 4 bytes component Transmitter R3.6: start, 8 data,parity, stop queue RX FIFO 4 bytes; full meansOVR component Receiver finds the fall, samplesbit centres cdc sync2 two flops, reset to 1 DATA writebyterx_linebyte + errorsDATA read
  1. 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
    1. data byte 0x48: APB registers → TX FIFO
    2. data byte 0x48: TX FIFO → Transmitter
  2. Cross

    The line from the far end, rxd, changes whenever the far clock says. Two flops in sync2 turn it into rx_line, a signal that changes only at this clock's edges.

  3. 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
    1. data byte 0x41: Receiver → RX FIFO
    2. 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.

OffsetRegisterAccessFields
0x00DATAwrite: push TX FIFO; read: pop RX FIFObyte in [7:0]; PSLVERR when the TX FIFO is full (write) or the RX FIFO is empty (read)
0x04STATUSbits 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
0x08CTRLread/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.

The receiver finds a start bit (8 cycles per bit)rxd falls 1 ns after edge 3. Each column is the value 1 ns after edges 3 to 18. line is the synchronizer's output, the receiver's input; prev is the line one cycle ago.
0123456789101112131415clkrxds1lineprevbusybaud0123456701234samplebitn012
  1. Cycle 1

    Edge 4 is the first edge after the fall. The synchronizer's first flop, s1, takes the 0.

  2. Cycle 2

    Edge 5: the second flop passes it on, so line is 0. prev still holds 1.

  3. Cycle 3

    Edge 6: prev is 1 and line is 0, a fall. busy rises and the baud counter starts from 0.

  4. Cycle 6

    The counter reads 3, the middle cycle of an 8-cycle bit, so sample is 1. Edge 10 samples the start bit: still 0, so the frame is real.

  5. Cycle 14

    Eight cycles later sample is 1 again. Edge 18 samples data bit 0 of 0x41, a 1, and bitn moves 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

Design brief
PortDirectionWidthMeaning
pclkinput1APB clock
presetninput1APB reset, asynchronous, active-low
pselinput1APB select
penableinput1APB access phase
pwriteinput1APB write
paddrinput[31:0]APB address
pwdatainput[31:0]APB write data
pstrbinput[3:0]APB write strobes
pprotinput[2:0]APB protection
preadyoutput1APB ready
prdataoutput[31:0]APB read data
pslverroutput1APB error
rxdinput1the serial line in: asynchronous to pclk
txdoutput1the serial line out
irqoutput1interrupt
Clock
PCLK 10 ns; rxd comes from an unrelated clock and may change at any time
Reset
PRESETn asynchronous, active-low; the synchronizer and txd reset 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_BIT cycles 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
  1. 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.

  2. 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.

  3. 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.

RTLIcarus Verilog · in your browser · Lint: Verilator service · Synthesize: Yosys service
Edit the design, then Run, Lint or Synthesize
Expected: 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 notes

Original 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

Predict the outputThe far end sends five bytes back to back while nobody reads. The RX FIFO holds four. Then the driver reads STATUS, keeps its OVR bit, and drains the FIFO. What does the bench print?
// uart_apb: a UART on APB. Bytes written to DATA wait in a TX FIFO and leave on txd; bytes that
// arrive on rxd are checked, then wait in an RX FIFO until a read of DATA takes them.
// Frame: start bit 0, eight data bits (least significant first), even parity, one stop bit 1.
// Every bit lasts CLKS_PER_BIT cycles of pclk. The register map:
//   0x00 DATA    write: push a byte into the TX FIFO (PSLVERR when it is full)
//                read:  pop a byte from the RX FIFO (PSLVERR when it is empty)
//   0x04 STATUS  [0] RXNE  read-only: the RX FIFO holds a byte
//                [1] TXF   read-only: the TX FIFO is full
//                [2] TXE   read-only: the TX FIFO is empty and the transmitter is idle
//                [4] OVR   write-1-to-clear: a byte arrived while the RX FIFO was full and was lost
//                [5] FERR  write-1-to-clear: a byte arrived with its stop bit at 0
//                [6] PERR  write-1-to-clear: a byte arrived with the wrong parity
//   0x08 CTRL    [0] RXIE: irq while RXNE   [1] ERRIE: irq while OVR, FERR or PERR
// Any other address ends with PSLVERR. No wait states.
module uart_apb #(parameter int CLKS_PER_BIT = 8) (
  input  logic        pclk,
  input  logic        presetn,     // active-low, asynchronous
  input  logic        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,
  output logic        pready,
  output logic [31:0] prdata,
  output logic        pslverr,
  input  logic        rxd,         // the serial line in: asynchronous to pclk
  output logic        txd,
  output logic        irq
);
  localparam logic [7:0] A_DATA = 8'h00, A_STATUS = 8'h04, A_CTRL = 8'h08;
  localparam int RXNE = 0, TXF = 1, TXE = 2, OVR = 4, FERR = 5, PERR = 6, RXIE = 0, ERRIE = 1;

  // ---- The receive side: line, receiver, RX FIFO ----
  logic rx_sync, rx_line;
  sync2 u_sync (.clk(pclk), .rst_n(presetn), .d(rxd), .q(rx_sync));
  assign rx_line = rx_sync;                      // the line the receiver samples

  logic       rx_valid, rx_ferr, rx_perr, rx_full, rx_empty, pop_rx;
  logic [7:0] rx_data, rx_head;
  uart_rx #(.CLKS_PER_BIT(CLKS_PER_BIT)) u_rx (.clk(pclk), .rst_n(presetn), .line(rx_line),
    .out_valid(rx_valid), .out_data(rx_data), .frame_err(rx_ferr), .parity_err(rx_perr));
  sync_fifo #(.DEPTH(4), .WIDTH(8)) u_rxf (.clk(pclk), .rst_n(presetn), .write_en(rx_valid),
    .write_data(rx_data), .read_en(pop_rx), .head(rx_head), .full(rx_full), .empty(rx_empty));

  // ---- The transmit side: TX FIFO, transmitter ----
  logic       push_tx, tx_full, tx_empty, tx_ready;
  logic [7:0] tx_head;
  sync_fifo #(.DEPTH(4), .WIDTH(8)) u_txf (.clk(pclk), .rst_n(presetn), .write_en(push_tx),
    .write_data(pwdata[7:0]), .read_en(tx_ready), .head(tx_head), .full(tx_full), .empty(tx_empty));
  uart_tx #(.CLKS_PER_BIT(CLKS_PER_BIT)) u_tx (.clk(pclk), .rst_n(presetn), .in_valid(!tx_empty),
    .in_data(tx_head), .in_ready(tx_ready), .txd(txd));

  // ---- The registers (R8.1): no wait states, act at the completion edge ----
  logic [6:4] err_q;                             // OVR, FERR, PERR
  logic [1:0] ctrl_q;
  logic [7:0] status;
  always_comb begin
    status = 8'd0;
    status[RXNE] = !rx_empty;
    status[TXF]  = tx_full;
    status[TXE]  = tx_empty && tx_ready;
    status[OVR]  = err_q[OVR];
    status[FERR] = err_q[FERR];
    status[PERR] = err_q[PERR];
  end
  wire [7:0] a    = paddr[7:0];
  wire       done = psel && penable;
  assign pready  = 1'b1;
  wire bad = !(a == A_DATA || a == A_STATUS || a == A_CTRL)
          || (a == A_DATA && pwrite && tx_full) || (a == A_DATA && !pwrite && rx_empty);
  assign pslverr = done && bad;
  wire wr  = done && !bad && pwrite && pstrb[0];
  assign push_tx = wr && a == A_DATA;
  assign pop_rx  = done && !bad && !pwrite && a == A_DATA;
  always_comb begin
    prdata = 32'd0;
    case (a)
      A_DATA:   prdata[7:0] = rx_head;
      A_STATUS: prdata[7:0] = status;
      A_CTRL:   prdata[1:0] = ctrl_q;
      default:  prdata = 32'd0;
    endcase
  end

  logic [6:4] err_d;
  always_comb begin                              // write 1 to clear; a new error wins
    err_d = err_q;
    if (wr && a == A_STATUS) err_d = err_d & ~pwdata[6:4];
    if (rx_valid && rx_full) err_d[OVR]  = 1'b1;
    if (rx_valid && rx_ferr) err_d[FERR] = 1'b1;
    if (rx_valid && rx_perr) err_d[PERR] = 1'b1;
  end
  always_ff @(posedge pclk or negedge presetn)
    if (!presetn) begin
      err_q <= '0; ctrl_q <= '0; irq <= 1'b0;
    end else begin
      err_q <= err_d;
      if (wr && a == A_CTRL) ctrl_q <= pwdata[1:0];
      irq <= (ctrl_q[RXIE] && !rx_empty) || (ctrl_q[ERRIE] && |err_q);
    end
endmodule

// sync2: the two-flop synchronizer (R6.2). It resets to 1, the idle level of the line (R6.5).
module sync2 (input logic clk, input logic rst_n, input logic d, output logic q);
  logic s1;
  always_ff @(posedge clk or negedge rst_n)
    if (!rst_n) begin s1 <= 1'b1; q <= 1'b1; end
    else        begin s1 <= d;    q <= s1;   end
endmodule

// uart_rx: the serial decoder of R3.6's bench, as hardware. A fall of the line starts a frame; the
// receiver waits half a bit-time to the middle of the start bit, checks it is still 0, then
// samples every CLKS_PER_BIT cycles: eight data bits, the parity bit, the stop bit.
module uart_rx #(parameter int CLKS_PER_BIT = 8) (
  input  logic       clk,
  input  logic       rst_n,
  input  logic       line,
  output logic       out_valid,      // one cycle per frame, with the byte and its two error flags
  output logic [7:0] out_data,
  output logic       frame_err,
  output logic       parity_err
);
  localparam int BW = $clog2(CLKS_PER_BIT);
  localparam logic [BW-1:0] MID  = BW'(CLKS_PER_BIT / 2 - 1);   // the sample cycle within a bit
  localparam logic [BW-1:0] LAST = BW'(CLKS_PER_BIT - 1);
  logic          prev_q, busy_q, par_q;
  logic [BW-1:0] baud_q;
  logic [3:0]    bitn_q;              // 0 start, 1-8 data, 9 parity, 10 stop
  logic [7:0]    sh_q;
  wire fall   = prev_q && !line;
  wire sample = busy_q && (baud_q == MID);
  always_ff @(posedge clk or negedge rst_n)
    if (!rst_n) begin
      prev_q <= 1'b1; busy_q <= 1'b0; baud_q <= '0; bitn_q <= '0; sh_q <= '0; par_q <= 1'b0;
      out_valid <= 1'b0; out_data <= '0; frame_err <= 1'b0; parity_err <= 1'b0;
    end else begin
      prev_q    <= line;
      out_valid <= 1'b0;
      if (!busy_q) begin
        baud_q <= '0; bitn_q <= '0;
        if (fall) busy_q <= 1'b1;                       // a start bit begins
      end else begin
        baud_q <= (baud_q == LAST) ? '0 : baud_q + 1'b1;
        if (sample) begin
          bitn_q <= bitn_q + 4'd1;
          if (bitn_q == 4'd0 && line) busy_q <= 1'b0;   // the start bit is gone: a glitch
          else if (bitn_q >= 4'd1 && bitn_q <= 4'd8) sh_q <= {line, sh_q[7:1]};
          else if (bitn_q == 4'd9) par_q <= line;
          else if (bitn_q == 4'd10) begin               // the stop bit: deliver the byte
            busy_q     <= 1'b0;
            out_valid  <= 1'b1;
            out_data   <= sh_q;
            frame_err  <= !line;
            parity_err <= (^sh_q) != par_q;
          end
        end
      end
    end
endmodule

// sync_fifo: the synchronous FIFO of R5.2 (pointers with the extra wrap bit), with its read port
// showing the oldest word on head at all times. A write is accepted when write_en is 1 and the
// FIFO is not full, a read when read_en is 1 and it is not empty.
module sync_fifo #(parameter int DEPTH = 4, parameter int WIDTH = 8) (
  input  logic             clk,
  input  logic             rst_n,
  input  logic             write_en,
  input  logic [WIDTH-1:0] write_data,
  input  logic             read_en,
  output logic [WIDTH-1:0] head,
  output logic             full,
  output logic             empty
);
  localparam int AW = $clog2(DEPTH);
  logic [WIDTH-1:0] mem [DEPTH];
  logic [AW:0]      wptr_q, rptr_q, used;
  assign used  = wptr_q - rptr_q;
  assign empty = (used == 0);
  assign full  = (used == (AW+1)'(DEPTH));
  assign head  = mem[rptr_q[AW-1:0]];
  wire take_w = write_en && !full;
  wire take_r = read_en && !empty;
  always_ff @(posedge clk)
    if (take_w) mem[wptr_q[AW-1:0]] <= write_data;
  always_ff @(posedge clk or negedge rst_n)
    if (!rst_n) begin wptr_q <= '0; rptr_q <= '0; end
    else begin
      if (take_w) wptr_q <= wptr_q + 1'b1;
      if (take_r) rptr_q <= rptr_q + 1'b1;
    end
endmodule

// uart_tx: the transmitter of R3.6 with its bit counter fixed.
module uart_tx #(parameter int CLKS_PER_BIT = 8) (
  input  logic       clk,
  input  logic       rst_n,
  input  logic       in_valid,
  input  logic [7:0] in_data,
  output logic       in_ready,
  output logic       txd
);
  typedef enum logic [2:0] {IDLE, START, DATA, PARITY, STOP} state_e;
  state_e st_q, st_d;
  localparam int BW = $clog2(CLKS_PER_BIT);
  localparam logic [BW-1:0] LAST = BW'(CLKS_PER_BIT - 1);
  logic [BW-1:0] baud_q;
  logic [2:0]    bit_q;
  logic [7:0]    sh_q;
  logic          par_q, line;
  wire tick   = (baud_q == LAST);
  assign in_ready = (st_q == IDLE);
  wire accept = in_valid && in_ready;
  always_comb begin
    st_d = st_q;
    case (st_q)
      IDLE:    if (accept) st_d = START;
      START:   if (tick) st_d = DATA;
      DATA:    if (tick && bit_q == 3'd7) st_d = PARITY;
      PARITY:  if (tick) st_d = STOP;
      STOP:    if (tick) st_d = IDLE;
      default: st_d = IDLE;
    endcase
  end
  always_comb begin
    case (st_q)
      START:   line = 1'b0;
      DATA:    line = sh_q[0];
      PARITY:  line = par_q;
      default: line = 1'b1;
    endcase
  end
  always_ff @(posedge clk or negedge rst_n)
    if (!rst_n) begin
      st_q <= IDLE; baud_q <= '0; bit_q <= '0; sh_q <= '0; par_q <= 1'b0; txd <= 1'b1;
    end else begin
      st_q   <= st_d;
      baud_q <= (st_q == IDLE || tick) ? '0 : baud_q + 1'b1;
      if (accept) begin
        sh_q <= in_data; par_q <= ^in_data; bit_q <= '0;
      end else if (tick && st_q == DATA) begin
        bit_q <= bit_q + 3'd1;
        sh_q  <= sh_q >> 1;
      end
      txd <= line;
    end
endmodule
`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;
  `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

  // 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
    quiet = 1;
    for (int i = 0; i < 5; i++) remote_send(8'h30 + i, 2);
    idle(20);
    apb_read(32'h04);
    begin
      logic ovr;
      ovr = prdata_q[4];
      drain();
      $display("read %0d bytes, OVR=%b", got.size(), ovr);
    end
    $finish(0);                             // quiet: no "$finish called" line
  end
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
Spot the bugEach option is the receiver's sample cycle MID within a bit (8 cycles per bit). The far end runs slow: 82.2 ns bits against the receiver's 80. The bench sends 12 bytes and expects all of them intact. Which one fails?
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
ConceptWhy does the receiver wait half a bit-time after the fall before it takes its first sample?
ConceptThe shipped receiver reads rxd directly in two flops, prev_q and busy_q. What goes wrong when rxd falls just before an edge?
Read the waveformIn the measured trace, at which edge does the receiver take its sample of the start bit?
The start of a frame
0123456789101112131415rxdlinebusybaud0123456701234sample

Values 1 ns after edges 3 to 18, left to right. rxd fell 1 ns after edge 3.

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.