Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutRoadmapCareersInternshipsFAQContactPrivacyTerms
Skip to lesson

Course contents

  • How to Use This Course
  • The NanoSoC Tour
  • Reading Path
  • SoC Glossary10
  • SoC Metrics Reference
  • SoC Cheatsheet
  • What SoC Integration Is12
  • Architecture and Budgets21
  • Memory Maps and Address Decode26
    • Designing the Address Map
    • Implementing Address Decode
    • Decode Errors and Default Slaves
    • Register Maps and Banks
      • Designing a Register Block
      • Checking the Register Specification
      • A Working Register Block and Its Bugs
    • Security and Privilege Regions
    • The Software View and the Hardware-Software Contract
    • IP-XACT and Automated Assembly
    • Memory-Map Verification
  • Interconnect and Bus Fabrics26
  • Clock and Reset Architecture24
  • Power Architecture21
  • Interrupts, DMA and Peripherals15
  • Memories, Boot and Security21
  • IP Integration and Handoff21
  • Top-Level Verification and Bring-up21
  • Package, Pads and I/O18
  • Chip Signoff and Tapeout18
  • SoC Interview Prep16
  • Capstone: Integrate NanoSoC14
Loading lesson

←Register Maps and Banks
  1. Topics
  2. SoC Design
  3. Memory Maps and Address Decode
  4. Register Maps and Banks
  5. A Working Register Block and Its Bugs
On this page

    Part 3 of 14, lesson 14 of 26Part 3 of 14/Memory Maps and Address Decode/Lesson 14 of 26All levels

    A Working Register Block and Its Bugs

    A small compiling DMA-style register block in SystemVerilog, plus W1C mix-ups, the IRQ_EN trap and busy-ignored writes.

    Why it matters

    Register blocks look trivial and fail in production anyway, because their bugs are semantic: the write that cleared an interrupt enable the driver still needed, the status clear that lost a race with hardware, the configuration write that vanished because the engine was busy. The NanoSoC DMA, whose behaviour comes from real RTL, exhibits all three by design, which makes it the perfect specimen: build the pattern once, correctly, and the traps become recognisable everywhere else.

    The core idea

    The block below implements the DMA's five registers with their real register semantics, simplified in timing and in the job engine: reads are combinational with pready tied high (the real block takes three clocks per access with one wait state), and BUSY, DONE-set, ERR-set and COUNT arrive as inputs from the engine rather than being generated inside. What is modelled faithfully is the register behaviour software sees: CTRL stores IRQ_EN from bit 1 on every write and pulses GO only when bit 0 is one and the engine is idle, an accepted GO clears DONE (the real engine also raises BUSY and clears COUNT, which live on the engine side here), and ERR is never cleared by GO. STATUS holds DONE and ERR as sticky bits that hardware sets and software clears by writing one; a hardware set wins a same-cycle clear, so a completion can never be erased unseen. LEN and KEY accept writes only while the engine is idle. COUNT is read-only. Anything else completes with pslverr.

    nano_dma_regs.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    // NanoSoC teaching values: DMA-style APB register block (simplified timing:
    // pready tied high; the real block inserts one wait state, three clocks/access).
    module nano_dma_regs (
      input  logic        clk,
      input  logic        rst_n,
      input  logic [7:0]  paddr,
      input  logic        psel,
      input  logic        penable,
      input  logic        pwrite,
      input  logic [31:0] pwdata,
      output logic [31:0] prdata,
      output logic        pready,
      output logic        pslverr,
      input  logic        busy_in,
      input  logic        done_set,
      input  logic        err_set,
      input  logic [15:0] count_in,
      output logic        go_pulse,
      output logic        irq_en_o,
      output logic [15:0] len_o,
      output logic [31:0] key_o
    );
      logic        irq_en, done_r, err_r;
      logic        mapped;
      logic        wr, is_ctrl, is_status, is_len, is_key;
    
      assign wr       = psel & penable & pwrite;
      assign is_ctrl  = (paddr == 8'h00);
      assign is_status = (paddr == 8'h04);
      assign is_len   = (paddr == 8'h08);
      assign is_key   = (paddr == 8'h0C);
      assign mapped   = is_ctrl | is_status | is_len | is_key | (paddr == 8'h10);
      assign pready   = 1'b1;
      assign pslverr  = psel & penable & ~mapped;
    
      always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
          irq_en <= 1'b0; done_r <= 1'b0; err_r <= 1'b0;
          len_o <= 16'h0; key_o <= 32'h0; go_pulse <= 1'b0;
        end else begin
          go_pulse <= 1'b0;
          if (wr && is_ctrl) begin
            irq_en <= pwdata[1]; // every CTRL write copies bit 1 (see lesson text)
            if (pwdata[0] && !busy_in) begin
              go_pulse <= 1'b1;
              done_r <= 1'b0; // an accepted GO clears DONE (ERR is not cleared)
            end
          end
          if (wr && is_status) begin
            if (pwdata[1]) done_r <= 1'b0;
            if (pwdata[2]) err_r <= 1'b0;
          end
          if (done_set) done_r <= 1'b1; // hardware set wins over a same-cycle clear
          if (err_set) err_r <= 1'b1;
          if (wr && is_len && !busy_in) len_o <= pwdata[15:0]; // ignored while BUSY
          if (wr && is_key && !busy_in) key_o <= pwdata;       // ignored while BUSY
        end
      end
      assign irq_en_o = irq_en;
    
      always_comb begin
        case (paddr)
          8'h00:   prdata = {30'h0, irq_en, 1'b0}; // GO reads back 0
          8'h04:   prdata = {29'h0, err_r, done_r, busy_in};
          8'h08:   prdata = {16'h0, len_o};
          8'h0C:   prdata = key_o;
          8'h10:   prdata = {16'h0, count_in};
          default: prdata = 32'h0;
        endcase
      end
    endmodule

    The three classic bugs

    First, the access-type mix-up: implementing STATUS as plain read-write lets software clear DONE by writing zero to the whole register, which also overwrites bits it did not mean to touch, while implementing it as write-1-to-clear at the wrong priority loses completions to a same-cycle clear. The block above takes the safe line on both: per-field types, set wins. Second, the control-write trap, which is real DMA behaviour and not a defect: every CTRL write copies bit 1 into IRQ_EN, so a driver that writes 0x1 to start a job silently disables its own interrupt and then waits forever for a completion that already happened. The driver rule is to write IRQ_EN = 1 on every CTRL write when interrupts are used, 0x3 to start a job with the interrupt armed. Third, the silently ignored write: LEN and KEY writes during BUSY complete normally, with no error, and change nothing, so a driver that reprograms a running engine gets the old transfer with the old key and no signal anywhere. The driver rule is to poll BUSY to zero first. All three rules are hardware-software contract terms; the software topic writes them into the errata discipline.

    What the review looks for

    • Status clear priority is stated in the specification (set wins) and matches the RTL line by line.

    • Control registers with read-modify side effects, like CTRL copying IRQ_EN, are flagged in the table and in the driver guide.

    • Busy-ignored writes are documented as ignored, with the driver's polling obligation stated beside them.

    Verify and debug

    Exercise the block the way the verification topic will: reset readback against the table, write 0x1 to CTRL and observe IRQ_EN clear (the trap made visible), write LEN during a busy job and read back the old value, and set DONE in the same cycle as a clear and confirm it survives. Each behaviour is one directed test; together they pin the semantics the driver depends on. If a test surprises you, the specification is ambiguous and the fix belongs in the table first.

    The clear-versus-set race: software clears a status bit in the same cycle hardware sets it for a new event, and the event is lost. Set-wins priority in hardware, or an explicit race test if the block chose otherwise, is the standing fix.

    If asked for a register bug story, tell the CTRL one: writing 0x1 starts the job and disables the interrupt in the same write, because bit 1 is stored every time. The fix is one line in the driver (write 0x3, keeping IRQ_EN set, whenever interrupts are used), and noticing it requires reading the RTL semantics, not the register name.

    Check yourself A driver writes KEY while BUSY is high, receives no error, and the job output is wrong. What happened and what is the rule?

    The write was silently ignored, as specified: the engine kept the old key. The rule is to wait for BUSY to read zero before touching LEN or KEY, and to confirm by readback when it matters.

    Related topics

    • Checking the Register Specification

    • Hierarchical Decode and Slot Decode

    • UVM: Why a Register Model

    Previous

    52 / 289

    Next: Security and Privilege Regions

    Up next

    Up nextSecurity and Privilege Regions

    Sign in to save progress

    Practice this lessonPrevious lessonReport an error

    See also

    • Designing a Register Block