Skip to content

A UART is the canonical proof that a peripheral is more than its register bank: behind a flat APB read/write port sit two FIFOs, a baud divisor, and a latching interrupt funnel whose status and clear semantics are nothing like the plain RW flops you have been building. Everything in this module taught you how an APB subordinate decodes an address, muxes read data, and honours per-field access types. This chapter spends that knowledge on a real device — the Arm PrimeCell PL011 UART — and shows where the simple model strains: UARTFR is a live status word that must reflect the FIFO levels at the read edge, not a flop you wrote; UARTDR is a single address that pushes the TX FIFO on write and pops the RX FIFO on read; and the interrupt path is a three-stage UARTRIS → UARTIMSC → UARTMIS chain whose raw bits latch and clear only when firmware writes UARTICR. The single idea to carry: the hard part of a peripheral integration is not the bus — it is the status and interrupt-clear semantics the bus has to expose, and a UART makes every one of them concrete with real register names and offsets.

1. Problem statement

The problem is mapping a stateful serial device — FIFOs, a baud generator, and a latching interrupt block — onto a stateless APB read/write port without breaking the register map's contract.

APB gives you exactly one primitive: an addressed 32-bit read or write that completes in two phases. A UART is nothing like that. It has a transmit FIFO that fills on writes and drains at the baud rate; a receive FIFO that fills from the wire and drains on reads; a divisor that sets the bit clock; and an interrupt block where events happen asynchronously to the bus and must be remembered until firmware acknowledges them. Bridging the two forces three decisions that the plain bank chapters never had to make:

  • Some registers are not storage — they are windows onto live hardware. UARTFR (the flag register at offset 0x18) is not a flop firmware wrote; it is a combinational view of the FIFO fill levels and the shifter's busy state, sampled at the read edge. UARTDR (0x00) is a port, not a cell — a write pushes the TX FIFO, a read pops the RX FIFO, and the same address does two different things in two directions.
  • Interrupt status must latch, and clearing it is a write, not a read. When the RX FIFO crosses its threshold, that event sets a bit in UARTRIS (raw interrupt status, 0x3C) that stays set even after the FIFO is drained, until firmware explicitly writes a 1 to the matching bit of UARTICR (0x44). Reading the status does not clear it. This is the write-1-clear (W1C) contract, and getting it wrong is the difference between a clean interrupt and a storm.
  • The interrupt the SoC sees is masked status, not raw. UARTMIS (0x40) is UARTRIS & UARTIMSC — the raw events gated by the per-source mask UARTIMSC (0x38). The single UARTINTR line to the interrupt controller is the OR-reduction of UARTMIS. So three registers — raw, mask, masked — describe one IRQ, and a driver must read and clear the right one.

So the job is to author the APB-side decode that realises this specific contract: a live-status read mux, a dual-direction FIFO port, and a raw→mask→masked→clear interrupt chain — all over a bus that only knows "read this word, write that word."

2. Why previous knowledge is insufficient

This module built every primitive a UART slave needs — but it built them on registers that are just storage, and a UART's most important registers are not.

  • Register-bank design and the read-data mux assumed the read returns a flop. They taught you to mux a stored value onto PRDATA. But UARTFR returns live FIFO-derived flags, and UARTDR returns the next RX FIFO entry while side-effecting the FIFO pointer. The read path here has to source from combinational status and from a FIFO with a pop, not from a register file — a generalisation the bank chapter did not make.
  • Status registers and read-only registers taught RO, but not latched-and-W1C. A plain RO status bit tracks hardware and needs no clear. A UART interrupt bit is different: it is set by a transient event, held until acknowledged, and cleared by a write. That is a distinct access type — closer to write-1-clear semantics layered on a status word — and it is the heart of every interrupt-driven driver.
  • CSR design fundamentals gave the contract view, but not the cross-domain reality. The CSR chapter framed the map as a software contract. A UART adds a wrinkle that pure CSR theory glosses over: the events that set UARTRIS arrive on the baud/serial timeline, asynchronous to PCLK, so the interrupt block is a small synchroniser-and-latch problem behind the register, not just a flop. Configuration registers like UARTIBRD/UARTFBRD/UARTLCR_H/UARTCR are plain RW — but the status and interrupt registers are not, and that asymmetry is the lesson.

The gap, then, is the jump from "a register is a flop you read and write" to "a register can be a live window, a FIFO port, or a latched event you clear by writing" — and a real UART is where you cannot avoid making that jump. Carrying these patterns into other peripherals (a GPIO's edge-latched interrupts, a timer's compare flags) reuses exactly this structure.

3. Mental model

The model: a UART behind APB is a flat address window laid over three different kinds of hardware — plain configuration flops, live status windows, and a latching interrupt funnel — and the integration skill is knowing which kind each offset is.

Sort every PL011 register into one of three buckets and the whole device becomes legible:

  • Configuration flops (plain RW). UARTIBRD/UARTFBRD (the integer and fractional baud divisors), UARTLCR_H (word length, FIFO enable FEN, parity, stop bits), UARTCR (UARTEN enable, TXE transmit-enable, RXE receive-enable), UARTIFLS (the FIFO trigger levels), and UARTIMSC (the interrupt mask) are exactly the RW registers from the bank chapter. Firmware writes them, reads them back, and the hardware obeys. Nothing exotic.
  • Live status windows (combinational RO). UARTFR is the prime example: its bits — BUSY (shifter still sending), RXFE (RX FIFO empty), TXFF (TX FIFO full), TXFE (TX FIFO empty), RXFF (RX FIFO full) — are computed from the FIFO pointers at the read edge, not stored. A read of UARTFR is a snapshot of the device right now. UARTDR is the dual-direction port: write side pushes the TX FIFO, read side pops the RX FIFO and also exposes the per-character error flags.
  • The latching interrupt funnel (raw → mask → masked → clear). An event (RX timeout, TX FIFO below trigger, RX FIFO above trigger, error) sets and holds a bit in UARTRIS. UARTIMSC masks it. UARTMIS = UARTRIS & UARTIMSC is what the SoC sees, OR-reduced into one UARTINTR. Firmware clears a serviced event by writing a 1 to that bit of UARTICRreading never clears. This funnel is the one piece of stateful, asynchronous logic the bus must expose, and it is where integration bugs cluster.

Three refinements sharpen the model:

  • The FIFO is the reason status is live. Because the TX FIFO drains and the RX FIFO fills between bus accesses, UARTFR cannot be a flop firmware maintains — it must reflect the FIFO state at the instant of the read, or polling loops read stale fullness and either overflow the TX FIFO or spin forever on a drained RX FIFO.
  • The interrupt level is FIFO-driven, not edge-driven. UARTIFLS selects how full the FIFOs must be to raise the TX/RX FIFO interrupts (e.g. RX ≥ 1/2 full). The interrupt is a function of a FIFO level crossing the programmed threshold — a continuous condition latched into UARTRIS, not a one-shot pulse — which is why a poorly chosen level either interrupts too often or risks RX overrun.
  • Read and clear are deliberately separate operations. Splitting "observe the event" (UARTMIS/UARTRIS) from "acknowledge the event" (UARTICR) lets firmware read which interrupts fired, decide what to service, and clear exactly those — without a read accidentally dropping an event it had not handled yet. The separation is a feature, and the W1C clear is its mechanism.
Block diagram: the APB bus on the left enters a CSR register-decode block listing the PL011 register map, which fans out to a baud generator, a TX FIFO and shift register driving the TXD pin, an RX shift register and FIFO from the RXD pin, and interrupt logic that ANDs UARTRIS with UARTIMSC to form UARTMIS and ORs it into a single UARTINTR line, with UARTICR clearing the raw bits.
Figure 1 — the PL011 UART exposed over APB, with the CSR/register-decode block fanning out to the four functional units. On the left the APB bus (pclk, presetn, psel, penable, pwrite, paddr, pwdata → prdata, pready, pslverr) enters the register-decode block holding the real PL011 map: the baud divisors UARTIBRD (0x24) and UARTFBRD (0x28), line control UARTLCR_H (0x2C), enable UARTCR (0x30), FIFO levels UARTIFLS (0x34), the interrupt mask UARTIMSC (0x38), raw status UARTRIS (0x3C), masked status UARTMIS (0x40), the W1C clear UARTICR (0x44), the live flag word UARTFR (0x18), and the dual-direction data port UARTDR (0x00). The block fans out to the baud generator (PCLK divided by the IBRD.FBRD divisor produces the bit clock that drives both shifters), the TX path (UARTDR writes push a TX FIFO drained by a shift register to the TXD pin, with UARTFR.TXFF/TXFE/BUSY reflecting its level), the RX path (a shift register samples RXD into an RX FIFO read out through UARTDR, with UARTFR.RXFE/RXFF reflecting its level), and the interrupt logic (UARTRIS ANDed with UARTIMSC gives UARTMIS, OR-reduced to a single UARTINTR to the SoC interrupt controller, with UARTICR writes clearing the latched raw bits). The figure makes concrete that a flat 32-bit read/write port decodes into FIFO-driven live status and a raw→mask→masked→clear interrupt funnel.

4. Real SoC implementation

The PL011's APB-visible register map is the contract. Below is the subset this chapter teaches, with the real Arm offsets, access types, and reset values — note how cleanly it splits into the three buckets of the mental model.

OffsetRegisterAccessResetPurpose
0x00UARTDRRW (port)0x000Data: write pushes TX FIFO, read pops RX FIFO (with per-char error flags in [11:8])
0x04UARTRSR/UARTECRRW (W=clear)0x0Receive status / error clear: read RX errors (FE/PE/BE/OE); any write clears them
0x18UARTFRRO (live)0x090Flags: BUSY, RXFE, TXFF, TXFE, RXFF — combinational from FIFO levels and shifter
0x24UARTIBRDRW0x0000Baud-rate divisor, integer part (16-bit)
0x28UARTFBRDRW0x00Baud-rate divisor, fractional part (6-bit)
0x2CUARTLCR_HRW0x00Line control: WLEN word length, FEN FIFO enable, STP2/EPS/PEN
0x30UARTCRRW0x300Control: UARTEN enable, TXE transmit-enable, RXE receive-enable
0x34UARTIFLSRW0x12FIFO level select: TX and RX interrupt trigger thresholds
0x38UARTIMSCRW0x000Interrupt mask set/clear: 1 = enable that interrupt source
0x3CUARTRISRO (latched)0x00XRaw interrupt status: event bits, set-and-held independent of the mask
0x40UARTMISRO0x00XMasked interrupt status: UARTRIS & UARTIMSC — what drives UARTINTR
0x44UARTICRW1C0x000Interrupt clear: write 1 to a bit to clear the matching UARTRIS event

The APB side decodes this map with a read mux, a write decode, and — crucially — a W1C clear for the interrupt status. Real APB signals throughout:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ============================================================
// PL011 UART — APB-side CSR decode (read mux + write + W1C clear)
// Real APB signals: pclk presetn psel penable pwrite paddr
//                   pwdata prdata pready pslverr
// ============================================================
localparam UARTDR   = 8'h00, UARTFR   = 8'h18, UARTIBRD = 8'h24,
           UARTFBRD = 8'h28, UARTLCR_H= 8'h2C, UARTCR   = 8'h30,
           UARTIFLS = 8'h34, UARTIMSC = 8'h38, UARTRIS  = 8'h3C,
           UARTMIS  = 8'h40, UARTICR  = 8'h44;
 
// Access phase: an APB access is live when selected AND in the ENABLE cycle.
wire access  = psel && penable;
wire wr_en   = access &&  pwrite;     // qualified write strobe
wire rd_en   = access && !pwrite;     // qualified read strobe
 
// ---- Live status (combinational windows, NOT flops) -------------------
// UARTFR is computed from FIFO pointers at the read edge — never stored.
wire [15:0] uartfr = {7'h0,
                      rx_fifo_full,    // RXFF [6]
                      tx_fifo_empty,   // TXFE [7]  (bit positions per PL011 TRM)
                      rx_fifo_empty,   // RXFE [4]
                      tx_fifo_full,    // TXFF [5]
                      3'h0,
                      tx_busy};        // BUSY [3]  — shifter still draining
// (packed here for illustration; real PL011 bit map: BUSY[3] RXFE[4] TXFF[5] TXFE[6] RXFF[7])
 
// ---- Masked status is purely derived: RIS gated by the mask -----------
wire [10:0] uartmis = uartris & uartimsc;          // what the SoC sees
assign      uart_intr = |uartmis;                  // OR-reduce -> single IRQ line
 
// ---- READ MUX: source from flops, FIFO pop, and live status -----------
// UARTDR read POPS the RX FIFO (side effect) — a port, not a cell.
assign rx_fifo_pop = rd_en && (paddr == UARTDR);
always_comb begin
  prdata = 32'h0;                                  // default + reserved read-as-0
  unique case (paddr)
    UARTDR   : prdata = {20'h0, rx_err[3:0], rx_fifo_dout[7:0]}; // pop value + errors
    UARTFR   : prdata = {16'h0, uartfr};           // LIVE flags, sampled this edge
    UARTIBRD : prdata = {16'h0, ibrd_q};
    UARTFBRD : prdata = {26'h0, fbrd_q};
    UARTLCR_H: prdata = {24'h0, lcr_h_q};
    UARTCR   : prdata = {16'h0, cr_q};
    UARTIFLS : prdata = {26'h0, ifls_q};
    UARTIMSC : prdata = {21'h0, uartimsc};
    UARTRIS  : prdata = {21'h0, uartris};          // raw, mask-independent
    UARTMIS  : prdata = {21'h0, uartmis};          // raw & mask
    default  : prdata = 32'h0;                      // unmapped -> read 0 (or pslverr)
  endcase
end
 
// ---- WRITE DECODE: plain RW config registers --------------------------
wire tx_fifo_push = wr_en && (paddr == UARTDR);    // UARTDR write PUSHES TX FIFO
always_ff @(posedge pclk or negedge presetn) begin
  if (!presetn) begin
    ibrd_q  <= 16'h0;  fbrd_q  <= 6'h0;  lcr_h_q <= 8'h0;
    cr_q    <= 16'h300;                            // reset: RXE,TXE set, UARTEN off
    ifls_q  <= 6'h12;   uartimsc <= 11'h0;         // all interrupts masked at reset
  end else if (wr_en) unique case (paddr)
    UARTIBRD : ibrd_q   <= pwdata[15:0];
    UARTFBRD : fbrd_q   <= pwdata[5:0];
    UARTLCR_H: lcr_h_q  <= pwdata[7:0];
    UARTCR   : cr_q     <= pwdata[15:0];           // UARTEN[0] TXE[8] RXE[9]
    UARTIFLS : ifls_q   <= pwdata[5:0];
    UARTIMSC : uartimsc <= pwdata[10:0];
    default  : ;                                   // UARTDR handled by FIFO push
  endcase
end
 
// ---- W1C INTERRUPT CLEAR: the non-trivial part ------------------------
// UARTRIS bits are SET by hardware events and HELD; a 1 written to the
// matching UARTICR bit clears them. Reading UARTRIS/UARTMIS does NOT clear.
wire uarticr_wr = wr_en && (paddr == UARTICR);
always_ff @(posedge pclk or negedge presetn) begin
  if (!presetn)
    uartris <= 11'h0;
  else begin
    // set on the asynchronous event (synchronised into PCLK upstream),
    // clear on a 1 written to UARTICR. SET wins on a same-cycle collision.
    uartris <= (uartris & ~(uarticr_wr ? pwdata[10:0] : 11'h0)) | int_event_set;
  end
end
 
// ---- Single-cycle APB completion: zero-wait slave ---------------------
assign pready  = 1'b1;                              // CSR access completes in ACCESS
assign pslverr = 1'b0;                              // (assert on unmapped offset if desired)

Two facts make this an integration problem, not a bank exercise. First, UARTFR and the UARTDR read are not register reads — they are a live window and a FIFO pop. Sourcing UARTFR from the FIFO pointers at the read edge is what lets a polling driver see true fullness; reading UARTDR must advance the RX FIFO read pointer as a side effect, which means the read mux has a write-like consequence on one address. The plain read-data mux has no side effects; this one does, and that asymmetry must be deliberate. Second, the interrupt latch is the W1C heart of the device. UARTRIS is set by an event term (int_event_set, itself a FIFO-level crossing or an error, synchronised from the serial domain) and held until firmware writes UARTICR; the read path for UARTRIS/UARTMIS is pure observation. The collision rule — set wins over clear in the same cycle — guarantees that an event arriving on the exact cycle firmware clears the previous instance is not silently lost. Get that priority backwards and you drop interrupts under load.

5. Engineering tradeoffs

Integrating a UART over APB is a sequence of choices about how much of the device's statefulness to surface, and how. Each trades software simplicity, area, and robustness.

DecisionOption AOption BWhen to choose which
Status sourceUARTFR live-combinational from FIFO pointersRegistered snapshot updated each PCLKLive by default — polling must see true FIFO state; a registered snapshot lags by a cycle and lies about fullness
Interrupt clearW1C via UARTICR (read does not clear)Read-to-clear on UARTRISW1C — lets firmware observe then acknowledge selectively; read-to-clear drops events a shared read did not mean to handle
FIFO trigger levelProgrammable via UARTIFLS (1/8…7/8)Fixed half-fullProgrammable — high level cuts interrupt rate but raises RX-overrun risk; low level is safe but chatty. Expose the knob
Set-vs-clear priorityEvent SET wins over UARTICR clear same cycleClear winsSET wins — never lose an event that coincides with an acknowledge; clear-wins silently drops interrupts under back-to-back load
UARTDR read side effectRead pops the RX FIFORead is non-destructive; separate popPop-on-read matches the PL011 contract and software expectation; a non-destructive read needs an extra register and breaks driver portability
Unmapped offsetRead-as-0, write-ignored, PSLVERR=0Assert PSLVERRRead-as-0 is the lenient norm; assert PSLVERR only if the integration policy wants errored access to faulted offsets (pslverr behaviour)

The throughline: every choice is made for the driver that polls and services this device under real load. Live status keeps polling honest; W1C keeps acknowledgement selective; a programmable trigger level lets firmware tune the interrupt/latency tradeoff; and "set wins over clear" keeps the interrupt count exact. The bus is trivial — PREADY is just tied high for a single-cycle CSR — and all the engineering lives in the status and interrupt semantics.

6. Common RTL mistakes

7. Debugging scenario

Pick the interrupt storm, because it is the signature integration failure of any latching-interrupt peripheral and a UART makes it vivid: the CPU is pinned at 100% in the UART ISR and the system makes no progress, yet every directed register test passed.

  • Observed symptom: after the first received character, the UARTINTR line never deasserts. The ISR runs, reads the data, returns — and is immediately re-entered. top shows the core spending all its time in the UART interrupt handler; throughput collapses to nothing. On a logic-analyser capture of the APB bus, the ISR is seen reading UARTMIS and UARTDR on every entry, but never writing UARTICR.
  • Waveform/bus clue: trace the IRQ. UARTRIS[RX] goes high when the RX FIFO crosses its UARTIFLS level and stays high. The ISR reads UARTMIS (sees the RX bit), drains UARTDR, and returns. UARTMIS = UARTRIS & UARTIMSC is still 1 because UARTRIS[RX] was never cleared — so UARTINTR re-asserts the instant the ISR returns. The bus shows reads of 0x40 and 0x00 but no write to 0x44.
  • Root cause: the driver assumed that reading the status (or draining the data) acknowledges the interrupt, as it would on a read-to-clear peripheral. On the PL011 the raw status is W1C — only a write of 1 to UARTICR clears UARTRIS. With no UARTICR write, the latched raw bit holds, the masked status holds, and the IRQ storms.
  • Correct firmware/RTL contract: the ISR must write UARTICR with a 1 in each serviced bit after handling the event. The RTL must honour that W1C clear and — critically — let a new event set the bit even on the same cycle as the clear, so a character arriving during the ISR is not lost:
    Azvya Education Pvt. Ltd.VLSI Mentor
    Snippet
    // RTL: clear on UARTICR write-1, but a fresh event still SETS (set wins).
    always_ff @(posedge pclk or negedge presetn)
      if (!presetn) uartris <= '0;
      else uartris <= (uartris & ~(uarticr_wr ? pwdata[10:0] : '0)) | int_event_set;
  • Verification assertion: prove that a UARTICR write actually clears the masked interrupt when no fresh event coincides — i.e. the acknowledge path works:
    Azvya Education Pvt. Ltd.VLSI Mentor
    Snippet
    // ack-clears-masked: writing 1 to a UARTICR bit (no concurrent event)
    // drops the corresponding UARTMIS bit on the next cycle.
    assert property (@(posedge pclk) disable iff (!presetn)
      (uarticr_wr && pwdata[RX_BIT] && !int_event_set[RX_BIT])
        |=> !uartmis[RX_BIT]);
  • Debug habit: when an interrupt-driven peripheral storms, do not start in the data path — capture the APB bus and check whether the ISR ever writes the clear register. For any W1C interrupt block (UART, GPIO, timer), a storm is almost always "status read but never acknowledged." Confirm UARTICR is written with the serviced bits, and that the RTL's set-vs-clear priority does not let the clear swallow a coincident event.
APB timing waveform over six cycles showing pclk, psel, penable, pwrite, paddr, pwdata, prdata, pready. An UARTDR write at address 0x00 with data 0x41 completes on a marked edge pushing the TX FIFO, then a UARTFR read at 0x18 returns live flags TXFE=1 TXFF=0 RXFE=1 BUSY=0 sampled on the second marked completing edge.
Figure 2 — an APB write to UARTDR followed by a read of UARTFR, over roughly six PCLK cycles, with the completing edges marked. The first access is a write: in the SETUP cycle PSEL rises with PWRITE high, PADDR holds 0x00 (UARTDR) and PWDATA holds the TX byte 0x41; in the ACCESS cycle PENABLE and PREADY are high, and on the rising edge where PSEL, PENABLE and PREADY are all high — the marked completing edge — the byte is pushed into the TX FIFO. The second access is a read: PSEL rises with PWRITE low and PADDR holds 0x18 (UARTFR); in the ACCESS cycle PRDATA presents the live flag state (TXFE=1, TXFF=0, RXFE=1, BUSY=0) which the manager samples on the second marked completing edge. The figure makes two points concrete: a write commits to the FIFO only on the completing edge (not when PWDATA first appears), and a read of UARTFR returns the flags as they stand at that edge — a combinational view of the FIFO levels, not a previously latched snapshot — which is exactly why the status is honest for polling drivers.

8. Verification perspective

A UART slave is verified against its register-map contract plus its two stateful behaviours — live status and latched-W1C interrupts — and the highest-value properties are exactly the ones that catch the live-window and W1C-clear mistakes a per-register read/write test sails past.

  • Prove the read of UARTFR returns the live flag state. A directed test that reads UARTFR after a known FIFO operation is not enough; assert that the value sampled on the read edge equals the combinational flag vector at that edge, so a sneaky registered snapshot is caught:
    Azvya Education Pvt. Ltd.VLSI Mentor
    Snippet
    // uartfr-live: a read of UARTFR returns the flags as they stand at the
    // access edge — not a stale latched copy.
    assert property (@(posedge pclk) disable iff (!presetn)
      (psel && penable && !pwrite && paddr == UARTFR)
        |-> prdata[15:0] == uartfr);
  • Prove the W1C clear and that read never clears. Two paired properties pin the interrupt contract: a UARTICR write of 1 (with no coincident event) clears the corresponding masked bit next cycle, and a read of UARTRIS/UARTMIS leaves the raw status unchanged — the storm bug from §7 cannot hide:
    Azvya Education Pvt. Ltd.VLSI Mentor
    Snippet
    // ack-clears: write-1 to UARTICR drops the masked bit (no concurrent event).
    assert property (@(posedge pclk) disable iff (!presetn)
      (uarticr_wr && pwdata[RX_BIT] && !int_event_set[RX_BIT]) |=> !uartmis[RX_BIT]);
    // read-does-not-clear: observing status must not change the raw latch.
    assert property (@(posedge pclk) disable iff (!presetn)
      (psel && penable && !pwrite && (paddr==UARTRIS || paddr==UARTMIS) && !int_event_set)
        |=> $stable(uartris));
  • Cover the FIFO corners and the masked-status identity. Functional coverage must hit TX FIFO empty/full and RX FIFO empty/full transitions (so UARTFR is exercised at its boundaries), each UARTIFLS trigger level crossing that raises an interrupt, and the set-vs-clear collision (an event coincident with a UARTICR write). Bind a permanent check that the masked status is always the derived value — UARTMIS == UARTRIS & UARTIMSC — so the three interrupt registers can never silently disagree. A UVM register model (built from the same map) gives the per-field access-type checks for free; these properties add the stateful behaviour the model alone does not capture, and APB assertions and monitors supply the bus-protocol layer beneath them.

The point: a UART's verification is "the bus is correct" (handled by the generic APB checkers) plus two device-specific safety properties — status is live, and acknowledge-not-read clears the interrupt — that are the entire reason a UART integration is harder than a register bank.

9. Interview discussion

"Walk me through how you'd hook a UART up to APB" is a strong integration question because a weak answer describes a register bank and stops, while a strong one immediately separates the plain registers from the stateful ones and explains the interrupt-clear contract — the part that actually bites in silicon.

Frame it as three buckets behind one flat address window. The configuration registers — UARTIBRD/UARTFBRD (baud divisor), UARTLCR_H (word length, FEN), UARTCR (UARTEN/TXE/RXE), UARTIFLS (FIFO levels), UARTIMSC (mask) — are plain RW flops, nothing special. The live status — UARTFR (BUSY/RXFE/TXFF/TXFE/RXFF) — must be combinational from the FIFO pointers so polling sees true fullness, and UARTDR is a dual-direction port that pushes the TX FIFO on write and pops the RX FIFO on read. Then deliver the depth that signals real silicon work: the interrupt path is UARTRIS → UARTIMSC → UARTMIS, where raw status latches and holds, masked status is the pure derivation UARTRIS & UARTIMSC, and the single UARTINTR is its OR-reduction — and clearing is W1C via UARTICR; reading never clears. Land two war stories: the stuck TX (firmware set UARTEN but not TXE, so the FIFO fills and BUSY never asserts) and the interrupt storm (the ISR drained the data but never wrote UARTICR, so the latched raw bit held and the IRQ re-fired forever). Closing with "and the RTL must let a fresh event win over a coincident clear, or you drop interrupts under load" shows you have actually debugged one, not just read the PL011 TRM.

10. Practice

  1. Bucket the map. Sort UARTDR, UARTFR, UARTIBRD, UARTCR, UARTIMSC, UARTRIS, UARTMIS, and UARTICR into plain-RW-config, live-status, and interrupt-funnel, and state for each whether a read or write has a side effect.
  2. Trace the IRQ. Given UARTRIS = 0x010, UARTIMSC = 0x050, compute UARTMIS and the state of UARTINTR, then state exactly what a firmware write to UARTICR must contain to clear the active interrupt.
  3. Catch the storm. Write the SVA that proves a UARTICR write of 1 (with no coincident event) drops the matching UARTMIS bit next cycle, and explain which bug it catches and which it does not.
  4. Reason about live status. Explain why UARTFR must be combinational from the FIFO pointers rather than a registered snapshot, and describe the polling-driver failure each direction (TX and RX) suffers if it is latched one cycle stale.
  5. Diagnose the stuck TX. Given a UART where firmware wrote a byte to UARTDR but TXD never toggles and UARTFR.TXFF is stuck high, list the UARTCR/UARTLCR_H/baud-divisor conditions you would check and the order you would check them.

11. Q&A

12. Key takeaways

  • A UART behind APB is three kinds of hardware under one flat address window: plain RW configuration (UARTIBRD/UARTFBRD/UARTLCR_H/UARTCR/UARTIFLS/UARTIMSC), live status windows (UARTFR, and UARTDR as a dual-direction FIFO port), and a latching interrupt funnel (UARTRIS/UARTMIS/UARTICR). Knowing which bucket each offset is is the integration skill.
  • UARTFR must be combinational from the FIFO pointers, sampled at the read edge — never a latched snapshot — so a polling driver sees true TX/RX fullness instead of a stale lie.
  • The interrupt path is UARTRIS → UARTIMSC → UARTMIS → UARTINTR: raw status latches and holds, masked status is the pure derivation UARTRIS & UARTIMSC, and the IRQ is its OR. Clearing is W1C via UARTICR; reading never clears — the absence of that write is the classic interrupt storm.
  • UARTDR is a port, not a cell: a write pushes the TX FIFO, a read pops the RX FIFO as a side effect — so the read mux has a write-like consequence on one address, and a speculative double read consumes a character.
  • In the interrupt latch, a fresh event must win over a coincident UARTICR clear, or interrupts are silently dropped under load; and the event terms must be synchronised from the serial domain into PCLK before they set UARTRIS.
  • Verify the two stateful behaviours, not just the bus: one property that a read of UARTFR returns the live flags, and one that a UARTICR write clears the masked interrupt while a read does not — the bus protocol itself is just a zero-wait CSR with PREADY tied high. (Spec basis: Arm PrimeCell UART (PL011) Technical Reference Manual; AMBA APB Protocol Specification, IHI 0024C §2.1.)