UVM RAL · Chapter 14 · Industry Case Studies
Case Study: UART Register Model
This case study models a 16550-style UART end to end, whose defining challenge is banked and aliased addressing. The same offsets serve different registers depending on whether the access is a read or a write and on the state of the DLAB bank-select bit in the line-control register. At the first offset a read returns receive data while a write goes to transmit data, and with DLAB set that same offset becomes the divisor latch low byte. The divisor latches that set the baud rate sit banked behind the data and interrupt-enable registers, reachable only when DLAB is set, so it behaves like a stateful indirect access: set the bank, then reach the register. The lesson ends with the signature bug, where a test writes the divisor without first setting DLAB, hits the wrong register, and silently misconfigures the baud rate.
Foundation14 min readUVM RALcase studyUARTDLABbanked registers
Chapter 14 · Section 14.3 · Industry Case Studies · Capstone
1. The Peripheral and Its Register Map
A 16550 UART packs many registers into few addresses via read/write aliasing and DLAB banking — the defining modelling challenge.
0x0— read =RBR(RO, RX data), write =THR(WO, TX data);DLAB=1→DLL(RW, divisor low).0x4—DLAB=0→IER(RW, interrupt enable);DLAB=1→DLM(RW, divisor high).0x8— read =IIR(RO), write =FCR(WO).0xC—LCR(RW), contains theDLABbit.0x14—LSR(RO+volatile, line status).
2. Industry Context — why the UART is the aliasing/banking case
The 16550 UART is a de facto standard, and its register model is the canonical example of packing many registers into few addresses — read/write aliasing (RBR/THR, IIR/FCR) and DLAB banking (DLL/DLM behind the data/IER registers). This is not a quirk; it is a widely-copied pattern, so learning to model it — and to drive the banking protocol correctly — transfers to any register set that overlays registers behind a mode/bank bit. And the bug it surfaces — accessing a banked register without setting the bank first — is the same class as an indirect-register stale-index (12.5): a stateful access protocol collapsed by forgetting the state-setting step.
3. The Register Model — aliasing and banking
Model the aliased and banked registers, with DLL/DLM reachable via the DLAB bank (RAL supports read/write-aliased and banked registers; the essential point is that the divisor is a distinct register selected by DLAB):
// UART registers: RBR/THR aliased at 0x0 by read/write; DLL/DLM banked by DLAB; LSR RO+volatile.
class uart_reg_block extends uvm_reg_block;
`uvm_object_utils(uart_reg_block)
rand uvm_reg RBR, THR, IER, IIR, FCR, LCR, LSR, DLL, DLM;
// (fields omitted for brevity; each configured with its policy)
function new(string name="uart_reg_block"); super.new(name, UVM_NO_COVERAGE); endfunction
virtual function void build();
default_map = create_map("default_map", 0, 4, UVM_LITTLE_ENDIAN);
// Read/write aliasing at 0x0: RBR (RO) on read, THR (WO) on write.
RBR = uvm_reg::type_id::create("RBR"); RBR.configure(this); RBR.build(); // RO field
THR = uvm_reg::type_id::create("THR"); THR.configure(this); THR.build(); // WO field
default_map.add_reg(RBR, 'h0); // read path
default_map.add_reg(THR, 'h0); // write path (aliased address; direction selects)
// DLAB-banked divisor latches: DLL at 0x0 / DLM at 0x4 WHEN DLAB=1.
DLL = uvm_reg::type_id::create("DLL"); DLL.configure(this); DLL.build(); // RW divisor low
DLM = uvm_reg::type_id::create("DLM"); DLM.configure(this); DLM.build(); // RW divisor high
// (DLL/DLM are the BANKED view of 0x0/0x4 when DLAB=1 — access requires DLAB set)
IER = uvm_reg::type_id::create("IER"); IER.configure(this); IER.build();
default_map.add_reg(IER, 'h4); // DLAB=0 view of 0x4
LCR = uvm_reg::type_id::create("LCR"); LCR.configure(this); LCR.build(); // contains DLAB
default_map.add_reg(LCR, 'hC);
LSR = uvm_reg::type_id::create("LSR"); LSR.configure(this); LSR.build(); // RO + volatile
default_map.add_reg(LSR, 'h14);
lock_model();
endfunction
endclass4. Driving the Banking Protocol — set DLAB, access divisor, clear DLAB
Program the baud rate by entering the divisor bank (DLAB=1), writing DLL/DLM, then leaving it (DLAB=0):
// Set the baud divisor: SET DLAB, write DLL/DLM (banked), then CLEAR DLAB — a stateful banking protocol.
task set_baud(uart_reg_block regs, byte lo, byte hi);
uvm_status_e s;
regs.LCR.write(s, 32'h0000_0080); // SET DLAB (bit 7) -> 0x0/0x4 now expose DLL/DLM
regs.DLL.write(s, lo); // divisor low (only lands correctly with DLAB=1)
regs.DLM.write(s, hi); // divisor high (only lands correctly with DLAB=1)
regs.LCR.write(s, 32'h0000_0003); // CLEAR DLAB (+ set 8N1) -> 0x0/0x4 back to RBR/THR / IER
endtask5. A Transmit Sequence
After configuring baud and format, transmit a byte through THR and observe LSR:
// Configure then transmit: set baud (banked), then write THR and watch LSR (line status, volatile).
class uart_tx_seq extends uvm_reg_sequence#(uvm_sequence#(uvm_reg_item));
uart_reg_block regs;
virtual task body();
uvm_status_e s; uvm_reg_data_t lsr;
set_baud(regs, 8'h68, 8'h00); // baud via the DLAB bank
regs.IER.write(s, 32'h0000_0002); // enable TX interrupt (DLAB=0 view of 0x4)
regs.THR.write(s, "A"); // transmit a byte (write alias of 0x0)
regs.LSR.read(s, lsr); // read line status (RO + volatile)
endtask
endclass6. Debugging Walkthrough — the baud rate that went to the wrong register
Writing the divisor latches without first setting DLAB sends the write to the data/IER register instead of the divisor, so the baud rate is silently misconfigured
SET DLAB BEFORE ACCESSING THE DIVISOR// The divisor is written WITHOUT first setting DLAB (LCR bit 7):
// (missing) regs.LCR.write(s, 32'h80); // SET DLAB first!
regs.DLL.write(s, 8'h68); // BUG: with DLAB=0, address 0x0 is THR/RBR, NOT DLL -> write hits THR
regs.DLM.write(s, 8'h00); // BUG: with DLAB=0, address 0x4 is IER, NOT DLM -> write hits IER
// The divisor is never programmed; instead a byte was pushed to THR and IER was overwritten.
// Baud rate stays at its default (wrong), and IER is corrupted -> confusing downstream failures.The UART's baud rate is wrong even though the test 'set' the divisor, and interrupt behaviour is also off (because the intended DLM write landed on IER). The divisor writes succeeded on the bus — no error — but they went to the wrong registers (THR and IER), so the divisor latches were never touched and the baud stays at its default. It looks like the divisor is 'not sticking,' but the writes are being routed to the data/interrupt-enable registers because the bank was not selected.
The UART divisor latches (DLL/DLM) are banked: they are only accessible at 0x0/0x4 when DLAB=1 in LCR; with DLAB=0 those addresses are THR/RBR and IER. The test wrote DLL/DLM without first setting DLAB=1, so the hardware routed the writes by the current bank (DLAB=0): the DLL write hit THR (pushing a byte into the transmit register) and the DLM write hit IER (corrupting interrupt enables). The divisor was never programmed. This is a stateful banking-protocol bug — the two-step protocol (set the bank, then access the banked register) collapsed to one step (access without setting the bank) — exactly analogous to an indirect register's stale-index bug (12.5): a data access using a leftover/wrong bank state lands on the wrong register. It is not a bus, adapter, or divisor-register bug; the writes are faithfully routed by DLAB, which was simply not set. The baud is wrong because the divisor bank was never entered.
Honor the DLAB banking protocol: set DLAB=1 (write LCR bit 7), then write DLL/DLM, then clear DLAB to restore the RBR/THR/IER view — the set_baud sequence of Section 4. The lesson the UART case teaches: UART divisor latches are banked behind the data/interrupt-enable registers via DLAB, so accessing them is a stateful banking protocol — set the bank (DLAB), access the banked register, restore the bank; accessing the divisor without setting DLAB routes the write to whatever register the current bank exposes (THR/IER), silently misconfiguring the baud (and corrupting the aliased register). It is the UART form of the indirect-register stale-state bug (12.5): the bank-select is the state you must set before every banked access.
7. Common Mistakes
- Accessing
DLL/DLMwithout settingDLAB. The write routes toTHR/IER(theDLAB=0view); setDLAB=1first, then restore it (12.5). - Modelling
RBR/THRas one register. They are two registers aliased on one address by read/write direction — model both, not a singleRW. - Forgetting to clear
DLABafter setting the divisor. LeavingDLAB=1breaks subsequentRBR/THR/IERaccesses — restore the bank. LSRnot modelled volatile. Line status is hardware-updated; withoutvolatileits mirror falls behind (0.1, 11.1).- Treating banked access as ordinary. It is stateful (like an indirect register) — the bank is state you must set before each banked access (12.5).
8. Industry Best Practices
- Drive the
DLABbanking protocol explicitly. SetDLAB, accessDLL/DLM, clearDLAB— as a coupled sequence (set_baud). - Model read/write aliasing faithfully.
RBR/THRandIIR/FCRare distinct registers on one address, selected by direction. - Mark
LSR(and other status) volatile. Hardware-updated line status must not go stale (0.1). - Encapsulate banked access in a helper. A
set_baudtask that managesDLABprevents forgetting the bank step. - Diagnose 'divisor not sticking' as a missing bank. Writes routed to
THR/IERmeanDLABwas not set (12.5).
9. Interview / Review Questions
10. Key Takeaways
- The 16550 UART's defining RAL challenge is banked and aliased addressing: the same offsets serve different registers by read/write direction (
RBR/THR,IIR/FCR) and by theDLABbank-select bit inLCR. - The divisor latches (
DLL/DLM), which set the baud rate, are banked behind the data/IERregisters — reachable only whenDLAB=1— a stateful banking protocol: set the bank, access the banked register, restore the bank (like indirect registers, 12.5). - The signature bug is accessing the divisor without setting
DLAB: the write routes to theDLAB=0view (THR/IER), so the baud is silently misconfigured and the aliased register is corrupted — the dual symptom that fingerprints it. - Model
RBR/THRas two distinct registers aliased by direction (not oneRW), and markLSRvolatile (hardware-updated line status, 0.1). - Encapsulate banked access in a
set_baudhelper (setDLAB, write divisor, clearDLAB) so the bank step cannot be forgotten — 'divisor not sticking' plus a corrupted neighbour means a missing bank-select (12.5).