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 offset0x18) 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 ofUARTICR(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) isUARTRIS & UARTIMSC— the raw events gated by the per-source maskUARTIMSC(0x38). The singleUARTINTRline to the interrupt controller is the OR-reduction ofUARTMIS. 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. ButUARTFRreturns live FIFO-derived flags, andUARTDRreturns 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
UARTRISarrive on the baud/serial timeline, asynchronous toPCLK, so the interrupt block is a small synchroniser-and-latch problem behind the register, not just a flop. Configuration registers likeUARTIBRD/UARTFBRD/UARTLCR_H/UARTCRare 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 enableFEN, parity, stop bits),UARTCR(UARTENenable,TXEtransmit-enable,RXEreceive-enable),UARTIFLS(the FIFO trigger levels), andUARTIMSC(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).
UARTFRis 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 ofUARTFRis a snapshot of the device right now.UARTDRis 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.UARTIMSCmasks it.UARTMIS = UARTRIS & UARTIMSCis what the SoC sees, OR-reduced into oneUARTINTR. Firmware clears a serviced event by writing a 1 to that bit ofUARTICR— reading 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,
UARTFRcannot 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.
UARTIFLSselects 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 intoUARTRIS, 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.
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.
| Offset | Register | Access | Reset | Purpose |
|---|---|---|---|---|
0x00 | UARTDR | RW (port) | 0x000 | Data: write pushes TX FIFO, read pops RX FIFO (with per-char error flags in [11:8]) |
0x04 | UARTRSR/UARTECR | RW (W=clear) | 0x0 | Receive status / error clear: read RX errors (FE/PE/BE/OE); any write clears them |
0x18 | UARTFR | RO (live) | 0x090 | Flags: BUSY, RXFE, TXFF, TXFE, RXFF — combinational from FIFO levels and shifter |
0x24 | UARTIBRD | RW | 0x0000 | Baud-rate divisor, integer part (16-bit) |
0x28 | UARTFBRD | RW | 0x00 | Baud-rate divisor, fractional part (6-bit) |
0x2C | UARTLCR_H | RW | 0x00 | Line control: WLEN word length, FEN FIFO enable, STP2/EPS/PEN |
0x30 | UARTCR | RW | 0x300 | Control: UARTEN enable, TXE transmit-enable, RXE receive-enable |
0x34 | UARTIFLS | RW | 0x12 | FIFO level select: TX and RX interrupt trigger thresholds |
0x38 | UARTIMSC | RW | 0x000 | Interrupt mask set/clear: 1 = enable that interrupt source |
0x3C | UARTRIS | RO (latched) | 0x00X | Raw interrupt status: event bits, set-and-held independent of the mask |
0x40 | UARTMIS | RO | 0x00X | Masked interrupt status: UARTRIS & UARTIMSC — what drives UARTINTR |
0x44 | UARTICR | W1C | 0x000 | Interrupt 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:
// ============================================================
// 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.
| Decision | Option A | Option B | When to choose which |
|---|---|---|---|
| Status source | UARTFR live-combinational from FIFO pointers | Registered snapshot updated each PCLK | Live by default — polling must see true FIFO state; a registered snapshot lags by a cycle and lies about fullness |
| Interrupt clear | W1C via UARTICR (read does not clear) | Read-to-clear on UARTRIS | W1C — lets firmware observe then acknowledge selectively; read-to-clear drops events a shared read did not mean to handle |
| FIFO trigger level | Programmable via UARTIFLS (1/8…7/8) | Fixed half-full | Programmable — high level cuts interrupt rate but raises RX-overrun risk; low level is safe but chatty. Expose the knob |
| Set-vs-clear priority | Event SET wins over UARTICR clear same cycle | Clear wins | SET wins — never lose an event that coincides with an acknowledge; clear-wins silently drops interrupts under back-to-back load |
UARTDR read side effect | Read pops the RX FIFO | Read is non-destructive; separate pop | Pop-on-read matches the PL011 contract and software expectation; a non-destructive read needs an extra register and breaks driver portability |
| Unmapped offset | Read-as-0, write-ignored, PSLVERR=0 | Assert PSLVERR | Read-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
UARTINTRline never deasserts. The ISR runs, reads the data, returns — and is immediately re-entered.topshows 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 readingUARTMISandUARTDRon every entry, but never writingUARTICR. - Waveform/bus clue: trace the IRQ.
UARTRIS[RX]goes high when the RX FIFO crosses itsUARTIFLSlevel and stays high. The ISR readsUARTMIS(sees the RX bit), drainsUARTDR, and returns.UARTMIS = UARTRIS & UARTIMSCis still1becauseUARTRIS[RX]was never cleared — soUARTINTRre-asserts the instant the ISR returns. The bus shows reads of0x40and0x00but no write to0x44. - 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
UARTICRclearsUARTRIS. With noUARTICRwrite, the latched raw bit holds, the masked status holds, and the IRQ storms. - Correct firmware/RTL contract: the ISR must write
UARTICRwith 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 MentorSnippet// 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
UARTICRwrite actually clears the masked interrupt when no fresh event coincides — i.e. the acknowledge path works:Azvya Education Pvt. Ltd.VLSI MentorSnippet// 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
UARTICRis written with the serviced bits, and that the RTL's set-vs-clear priority does not let the clear swallow a coincident event.
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
UARTFRreturns the live flag state. A directed test that readsUARTFRafter 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 MentorSnippet// 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
UARTICRwrite of 1 (with no coincident event) clears the corresponding masked bit next cycle, and a read ofUARTRIS/UARTMISleaves the raw status unchanged — the storm bug from §7 cannot hide:Azvya Education Pvt. Ltd.VLSI MentorSnippet// 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
UARTFRis exercised at its boundaries), eachUARTIFLStrigger level crossing that raises an interrupt, and the set-vs-clear collision (an event coincident with aUARTICRwrite). 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
- Bucket the map. Sort
UARTDR,UARTFR,UARTIBRD,UARTCR,UARTIMSC,UARTRIS,UARTMIS, andUARTICRinto plain-RW-config, live-status, and interrupt-funnel, and state for each whether a read or write has a side effect. - Trace the IRQ. Given
UARTRIS = 0x010,UARTIMSC = 0x050, computeUARTMISand the state ofUARTINTR, then state exactly what a firmware write toUARTICRmust contain to clear the active interrupt. - Catch the storm. Write the SVA that proves a
UARTICRwrite of 1 (with no coincident event) drops the matchingUARTMISbit next cycle, and explain which bug it catches and which it does not. - Reason about live status. Explain why
UARTFRmust 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. - Diagnose the stuck TX. Given a UART where firmware wrote a byte to
UARTDRbutTXDnever toggles andUARTFR.TXFFis stuck high, list theUARTCR/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, andUARTDRas a dual-direction FIFO port), and a latching interrupt funnel (UARTRIS/UARTMIS/UARTICR). Knowing which bucket each offset is is the integration skill. UARTFRmust 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 derivationUARTRIS & UARTIMSC, and the IRQ is its OR. Clearing is W1C viaUARTICR; reading never clears — the absence of that write is the classic interrupt storm. UARTDRis 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
UARTICRclear, or interrupts are silently dropped under load; and the event terms must be synchronised from the serial domain intoPCLKbefore they setUARTRIS. - Verify the two stateful behaviours, not just the bus: one property that a read of
UARTFRreturns the live flags, and one that aUARTICRwrite clears the masked interrupt while a read does not — the bus protocol itself is just a zero-wait CSR withPREADYtied high. (Spec basis: Arm PrimeCell UART (PL011) Technical Reference Manual; AMBA APB Protocol Specification, IHI 0024C §2.1.)