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
  • Interconnect and Bus Fabrics26
  • Clock and Reset Architecture24
  • Power Architecture21
  • Interrupts, DMA and Peripherals15
    • Interrupt Controller Integration
    • Interrupt Routing and Latency
    • DMA Integration
    • Peripheral Subsystem
      • UART Integration
      • SPI and GPIO Integration
      • Bridges, Clocks and Bring-Up Order
    • Pad Muxing and GPIO
  • 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

←Peripheral Subsystem
  1. Topics
  2. SoC Design
  3. Interrupts, DMA and Peripherals
  4. Peripheral Subsystem
  5. UART Integration
On this page

    Part 7 of 14, lesson 10 of 15Part 7 of 14/Interrupts, DMA and Peripherals/Lesson 10 of 15All levels

    UART Integration

    Baud generation, the one-byte holding registers, the interrupt enables, and the overrun budget.

    Why it matters

    This lesson has one job: integrate the UART as a block the boot ROM can rely on for its first output. The UART is the simplest peripheral on the chip and the easiest to misconfigure: one divider sets the baud, two enables gate the interrupts, and two status bits report registers exactly one byte deep. At 115,384 baud a byte arrives every 86.7 us, so the integration question is never throughput but correctness at the edges: the divider rounding, the overrun when software is late, and the interrupt that must fire neither early nor twice.

    The core idea

    The register block is four words. DATA transmits on write and receives on read. STATUS reports TXE, transmit holding empty, and RXNE, receive holding full, both read-only. BAUDDIV divides the 24 MHz peripheral clock by 16 times the divider: with the reset value 13 the baud is 24 MHz divided by 208, which is 115,384.6, about 0.16 percent above the nominal 115,200. CTRL holds the peripheral enable and the two interrupt enables, TXIE and RXIE, all reset to 0 so the block is silent until software opts in. The interrupt is the OR of transmit-empty-when-wanted and receive-full-when-wanted: irq_uart equals TXIE AND TXE, OR-ed with RXIE AND RXNE, held while the condition lasts. Reading DATA drops the receive term; writing the next byte drops the transmit term, and with nothing left to send the handler must clear TXIE, or a transmit-empty interrupt never stops.

    One byte of holding on each side sets the whole timing story. Transmit (a NanoSoC choice, not a universal rule): software writes a byte, TXE clears until the shifter finishes all 10 wire bits, then TXE sets and the interrupt, if enabled, fires for the next byte, so TXE here means transmitter idle; a UART with a separate holding register sets TXE as soon as the byte moves to the shifter. Receive: a complete byte sets RXNE and raises the interrupt; if a second byte finishes before software reads the first, the first is lost to overrun (also a NanoSoC teaching choice). The 86.67 us deadline from the latency topic is the budget this hardware gives software, generous against a 2.03 us worst-case path, which is why the UART never overruns unless its interrupt is masked or its handler is stuck behind something unbounded.

    Worked example: baud, error and byte time

    The script below derives the baud from the reset divider, checks the error against nominal 115,200, prices the boot banner of 16 characters at 10 bits each, and confirms the overrun deadline leaves a margin of better than forty to one over the worst-case interrupt path.

    example.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    clk_per = 24e6
    bauddiv = 0x000D  # reset value 13
    baud = clk_per / (16 * bauddiv)
    print(f"baud: {baud:.1f}")
    error = (baud - 115200) / 115200
    print(f"error vs 115200: {error * 100:+.2f}%")
    assert abs(error) < 0.005
    banner_chars = 16  # "NanoSoC boot ok" plus newline
    banner_time = banner_chars * 10 / baud
    print(f"16-char banner: {banner_time * 1e6:.1f} us")
    assert abs(banner_time - 1386.667e-6) < 1e-6
    byte_time = 10 / baud
    T_CPU = 1 / 192e6
    worst_path = 182.3e-9 + 281.25e-9 + (10 + 40 + 200 + 50) * T_CPU  # same budget as latency lesson
    print(f"byte period (overrun deadline): {byte_time * 1e6:.2f} us")
    print(f"worst-case path:               {worst_path * 1e6:.3f} us")
    assert 42 < byte_time / worst_path < 44
    print("PASS: 115384.6 baud, banner 1386.7 us, deadline margin 43x")

    What the review looks for

    • BAUDDIV is programmed for the board's crystal, not copied from an example; the error against nominal is computed and under half a percent.

    • RXIE is enabled only after the receive handler is installed, and the handler reads DATA before returning so the level drops.

    • The overrun behaviour is documented: which byte survives, which status bit reports it, and what software must do.

    Verify and debug

    Loop the transmit pin to the receive pin on the bench and send the banner: every character must echo with RXNE setting once per byte. If the baud is off, the framing breaks on specific bit patterns first, long runs of alternating bits, which is the signature of a divider error rather than noise. If characters arrive doubled, the handler reads DATA twice or the interrupt enable was set before the handler existed; if they never arrive, BAUDDIV is wrong or the peripheral clock is gated.

    Register spec out of sync with the RTL: the driver header names a field offset or reset value the block does not implement, so every access hits the wrong bit. Caught by a register-model-versus-RTL check and a reset-value test; fixed by generating the RTL, the model, the header and the docs from one machine-readable register description.

    If asked for the baud, write 24 MHz over 16 times 13 without pausing, then give the error and say why half a percent is fine: the receiver samples near the bit centre, so a fraction of a percent of drift still samples correctly across 10 bits. The overrun deadline is the follow-up; answer 86.7 us with the one-byte reason.

    Check yourself Software enables RXIE but the interrupt fires continuously with no cable attached. What is floating, and what drops the level?

    The receive input is floating and noise frames phantom bytes, each setting RXNE. Reading DATA drops the level for the current byte, but the cure is at the pad: P1 carries uart_rx once UART.CTRL.EN is 1, its idle level is high, so the pad has a pull-up (a pull-down would read as a continuous break), which is the pad topic's lesson.

    Related topics

    • Next: SPI and GPIO Integration

    • Protocol First Principles: UART

    • Protocols: Embedded Peripherals

    Previous

    145 / 289

    Next: SPI and GPIO Integration

    Up next

    Up nextSPI and GPIO Integration

    Sign in to save progress

    Practice this lessonPrevious lessonReport an error

    See also

    • Bridges, Clocks and Bring-Up Order