Dev Log
Engineering Ashley Golland

The 0x04 Trap in Embedded Bring-Up

Manual driver work often fails through offset mistakes, misplaced bitfields, and access-mode errors. See how generated drivers prevent them.

Diagram of a register addressing error caused by confusing byte and word offsets

A register listed at word offset 1 may actually sit at byte address 0x04. If a developer takes the table's "word offset" value literally and calculates base + 1, the driver accesses the wrong location without an obvious warning. This is a common error in hand-written code, including code from experienced engineers, because datasheets switch between byte and word addressing and do not always label their columns consistently.

Confusing Byte and Word Offsets

A 32-bit peripheral bus does not usually mean that its register map uses word offsets. On most 32-bit MCUs, register position N corresponds to byte offset N * 4 from the peripheral base, rather than byte offset N. Datasheets vary: one may use byte offsets everywhere, another may use word offsets in one section and byte offsets in another, while a table labelled only "Offset" leaves the unit to be inferred from context.

Take the NXP LPC55S69 FLEXCOMM peripheral. Its FIFO status register, FIFOSTAT, is at byte offset 0xE24 from the FLEXCOMM base. If a developer interprets a word-indexed table and calculates FLEXCOMM0_BASE + 0x391, the resulting address is entirely different. That access may succeed silently by reaching another register or reserved space, or it may fault during bus decoding. In either case, finding the cause is difficult without a register-level debugger session.

Embedd's parser supports both address formats. If the table heading does not establish the unit, it compares the listed addresses with known peripheral bases and the expected register spacing. Generated structs then use byte offsets throughout, following CMSIS conventions.

Wrong Peripheral Instance

When an MCU provides several instances of one peripheral type, every instance has a different base address. On an STM32L4, USART1 is at 0x40013800, while USART2 is at 0x40004400. Using the wrong base still reaches valid registers, but in the wrong peripheral. A disabled instance may ignore the writes; an instance used elsewhere may interfere in ways that appear unrelated to the intended configuration.

This bug appeared in early-access pilot code. USART1 served the debug serial connection, while USART2 served a sensor. The driver targeted USART1 but configured USART2. Debug output kept working because the bootloader's prior USART1 settings remained, while the sensor was quietly configured incorrectly.

Embedd creates an individual driver for each peripheral instance, rather than one driver accepting a base-address parameter. The instance appears in both the function name and header guard: embx_stm32l4_usart1_init and embx_stm32l4_usart2_init are distinct functions with distinct base address constants. Calling the wrong function therefore becomes a compile-time error instead of a silent runtime mistake.

Bitfield Misalignment Caused by Datasheet Errors

Datasheets do sometimes contain mistakes, and those mistakes matter during bring-up. A register table may assign a field the wrong bit position. STM32 errata sheets and silicon errata documents have corrected register descriptions where the original reference manual gave incorrect bitfield boundaries.

Often the error is a one-bit shift in the field's starting or ending position. A field documented as [7:4] but implemented as [7:3] leads drivers using that description to write and read incorrect values. The result may be partial operation: an incorrect lower boundary lets a neighboring field enter the configured value, or lets the configured field affect its neighbor. Such behavior appears only for certain configurations and is hard to diagnose without the relevant silicon errata.

Embedd handles this in two ways. Its correction database records known errors for indexed chips, allowing generated output to use the corrected bit positions. For chips whose errata have not yet been identified, generated unit tests perform boundary checks that can expose one-bit errors when run on hardware. Writing 0xF to a 4-bit field and reading back another value is a direct signal of a boundary problem.

Access Mode Violations

Peripheral registers and fields carry access annotations: Read-Write (RW), Read-Only (RO), Write-Only (WO), and Write-1-to-Clear (W1C). The datasheet defines the mode for each register and bitfield. Using a mode incorrectly can cause behavior that is difficult to trace back to the driver.

A write-only register, such as a UART transmit data register on some peripherals, may return an undefined value or the last value written, depending on the implementation. Writing a read-only status bit does nothing. However, read-modify-write code intended to preserve an RO field writes back whatever the read produced, not necessarily the hardware's current state. If that value is stale cached data, the register can be corrupted.

Write-1-to-Clear fields cause many access-mode errors. They commonly appear in interrupt flag registers, where writing 1 to a flag bit clears it. A read-modify-write operation intended to clear one flag also clears every other active flag: the OR operation places 1s in all positions that were active when the register was read. The proper method is to write the selected mask directly, without reading first:

/* WRONG: read-modify-write clears all currently-set flags */
USART1->ICR |= EMBX_USART1_ICR_ORECF_MSK;

/* CORRECT: direct write clears only the specified flag */
USART1->ICR = EMBX_USART1_ICR_ORECF_MSK;

Embedd generates accessors that match each field's access mode. RO fields expose read accessors only, WO fields expose write accessors only, and W1C clear functions write directly instead of using read-modify-write. Generated header comments also state the mode for every register, explaining why a direct write is used instead of the more familiar read-modify-write approach.

Byte and Word Access Width Requirements

Some peripheral registers demand 32-bit word accesses. Byte or halfword accesses may fault or return incorrect data. STM32 FMC (Flexible Memory Controller) configuration registers are one example: the bus decoder expects aligned 32-bit accesses, so compiler-generated byte instructions for a struct field can cause a bus error on some implementations.

The issue can arise with C struct overlays, where a compiler may load a 16-bit member with a halfword instruction. CMSIS headers therefore declare every register field as uint32_t, including fields that hold 16-bit or smaller values, preventing sub-word instructions. Embedd uses the same approach: register structs contain uint32_t fields, and software applies bit masks. A field that is architecturally 16-bit occupies the low 16 bits of a 32-bit register word.

How Generated Drivers Prevent These Errors

These errors are not unusual edge cases. They follow from a manual workflow in which someone reads a table, copies a value, converts a unit, or overlooks a footnote. The problem is structural: datasheets have defined formats, and register maps have defined encodings. A parser that correctly interprets those formats removes the transcription errors.

Generated drivers are not guaranteed to be error-free. Embedd carries forward mistakes present in the datasheet, including factual errors that need manual correction. They do remove the errors introduced between the datasheet and the source code, such as copying mistakes, unit conversions, and misread access modes. In our pilot teams, this group made up most of the bring-up debugging time for new chip families.

Generate your first driver

Upload a chip datasheet for C99 peripheral drivers in under two minutes.

Get started free