Skip to content

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=1DLL (RW, divisor low).
  • 0x4DLAB=0IER (RW, interrupt enable); DLAB=1DLM (RW, divisor high).
  • 0x8 — read = IIR (RO), write = FCR (WO).
  • 0xCLCR (RW), contains the DLAB bit.
  • 0x14LSR (RO+volatile, line status).
16550 UART register map: RBR/THR/DLL aliased at 0x0, IER/DLM at 0x4 by DLAB, IIR/FCR at 0x8, LCR with DLAB, LSR volatile0x0 RBR/THR/DLL0x0 RBR/THR/DLLread=RBR(RO) write=THR(WO); DLAB=1 -> DLL(RW)read=RBR(RO) write=THR(WO); DLAB=1 -> DLL(RW)0x4 IER/DLM0x4 IER/DLMDLAB=0 -> IER(RW); DLAB=1 -> DLM(RW)DLAB=0 -> IER(RW); DLAB=1 -> DLM(RW)0x8 IIR/FCR0x8 IIR/FCRread=IIR(RO) write=FCR(WO)read=IIR(RO) write=FCR(WO)0xC LCR0xC LCRRW · contains the DLAB bank-select bitRW · contains the DLAB bank-select bit0x14 LSR0x14 LSRRO + VOLATILE · line statusRO + VOLATILE · line statusuart_reg_blockuart_reg_block16550-style UART16550-style UART
Figure 1 — the 16550 UART register map, defined by ALIASING and BANKING. At 0x0, read/write alias two registers (RBR read, THR write); with DLAB=1 the same 0x0 becomes DLL. At 0x4, DLAB selects IER (0) or DLM (1). The DIVISOR latches (DLL/DLM), which set the baud rate, are BANKED behind the data/IER registers and reachable ONLY when DLAB=1 in LCR — a stateful banking protocol: set DLAB, then access the divisor (like indirect regs, 12.5). LSR is RO + volatile (hardware-updated 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):

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endclass

4. 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):

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endtask

5. A Transmit Sequence

After configuring baud and format, transmit a byte through THR and observe LSR:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endclass

6. Debugging Walkthrough — the baud rate that went to the wrong register

1

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
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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.
Symptom

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.

Root Cause

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.

Fix

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/DLM without setting DLAB. The write routes to THR/IER (the DLAB=0 view); set DLAB=1 first, then restore it (12.5).
  • Modelling RBR/THR as one register. They are two registers aliased on one address by read/write direction — model both, not a single RW.
  • Forgetting to clear DLAB after setting the divisor. Leaving DLAB=1 breaks subsequent RBR/THR/IER accesses — restore the bank.
  • LSR not modelled volatile. Line status is hardware-updated; without volatile its 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 DLAB banking protocol explicitly. Set DLAB, access DLL/DLM, clear DLAB — as a coupled sequence (set_baud).
  • Model read/write aliasing faithfully. RBR/THR and IIR/FCR are 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_baud task that manages DLAB prevents forgetting the bank step.
  • Diagnose 'divisor not sticking' as a missing bank. Writes routed to THR/IER mean DLAB was 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 the DLAB bank-select bit in LCR.
  • The divisor latches (DLL/DLM), which set the baud rate, are banked behind the data/IER registers — reachable only when DLAB=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 the DLAB=0 view (THR/IER), so the baud is silently misconfigured and the aliased register is corrupted — the dual symptom that fingerprints it.
  • Model RBR/THR as two distinct registers aliased by direction (not one RW), and mark LSR volatile (hardware-updated line status, 0.1).
  • Encapsulate banked access in a set_baud helper (set DLAB, write divisor, clear DLAB) so the bank step cannot be forgotten — 'divisor not sticking' plus a corrupted neighbour means a missing bank-select (12.5).