Skip to content

soc_esp32::dport

DPORT: peripheral clock gating, reset, and safe access to the block.

Most ESP32 peripherals come out of reset clock-gated off and held in reset. Every register access to such a peripheral reads as zero and writes nowhere, with no fault — so a driver that forgets this looks like a driver with a wrong register map, and behaves identically to one.

Bit positions confirmed against esp-idf soc/dport_reg.h.

Two hazards, and they are not the same one

Section titled “Two hazards, and they are not the same one”

DPORT is shared by both cores, which makes it unsafe in two independent ways. Fixing either alone leaves a real failure in place.

1. A silicon erratum on reads. When one CPU reads a DPORT register while the other accesses APB, the DPORT read can return the APB value. Nothing faults; the caller simply gets the wrong number. Espressif’s workaround is not a lock — it is to read any APB register immediately before the DPORT read, with the two loads adjacent and interrupts masked. The APB pre-read synchronises the two CPUs’ view of the bus.

Verified against esp-idf soc/esp32/dport_access.c (v5.1) and esp_hw_support/port/esp32/dport_access.c (v4.4), which agree instruction for instruction. Note what the same header says about writes:

Write value to DPORT register (does not require protecting)

So write is a plain store. Only reads need the dance.

2. An ordinary read-modify-write race. Two cores each setting a different bit in PERIP_CLK_EN can lose one of them — the second read happens before the first write lands. The erratum workaround does nothing about this, because each read is individually correct. [modify] and the clock-gate helpers take a lock across the whole sequence.

Before both cores ran, neither hazard was reachable and the second was documented as a caller’s problem. Both are live now.

struct ClockBit(/* private */);

Clock-enable / reset bit for a peripheral, in DPORT_PERIP_CLK_EN_REG and DPORT_PERIP_RST_EN_REG. The same bit position serves both registers.

Implements: Clone, Copy, Debug, Eq, PartialEq, StructuralPartialEq

struct CryptoClockBit(/* private */);

A clock-enable / reset bit in DPORT_PERI_CLK_EN_REG / DPORT_PERI_RST_EN_REG.

Implements: Clone, Copy, Debug, Eq, PartialEq, StructuralPartialEq

fn clock_bit(base: u32) -> Option<ClockBit>

The clock/reset bit for a peripheral base address.

References: ClockBit

unsafe fn disable(bit: ClockBit)

Gate a peripheral’s clock off and hold it in reset.

Same as [enable].

References: ClockBit

unsafe fn disable_crypto(bit: CryptoClockBit)

Gate a crypto block’s clock off and hold it in reset.

Same as [enable_crypto].

References: CryptoClockBit

unsafe fn enable(bit: ClockBit)

Enable a peripheral’s clock and release it from reset.

Call this before touching any of the peripheral’s registers.

Both registers are updated under one lock acquisition. Taking the lock twice would let the other core see a peripheral clocked but still held in reset, which is the one intermediate state a caller must never observe.

Touches shared DPORT state. Safe against the other core and against interrupts; still requires that the peripheral is yours to gate.

References: ClockBit

unsafe fn enable_crypto(bit: CryptoClockBit)

Enable a crypto block’s clock and release it from reset.

The reset polarity here is the reverse of [enable]: a set bit in PERI_RST_EN holds the block in reset, so releasing it clears the bit. Same one-lock invariant as [enable] — clock and reset move together so no core sees the block clocked but still reset.

Touches shared DPORT state. Safe against the other core and interrupts; still requires that the block is yours to gate.

References: CryptoClockBit

unsafe fn modify(reg: u32, clear: u32, set: u32)

Read-modify-write one DPORT register, atomically against the other core.

reg must be a valid DPORT register address. Must not be called from inside another modify/[enable]/[disable].

unsafe fn radio_clock_disable(mask: u32)

Turn off the radio clocks in mask.

Not the inverse of [radio_clock_enable] in practice. The common bits are shared, so clearing them while the other radio is up stops it dead. Nothing here tracks that; the caller owns the decision, and today the only caller refcounts it.

Writes DPORT. mask must be a combination of the RADIO_CLK_* constants.

unsafe fn radio_clock_enable(mask: u32)

Turn on the radio clocks in mask.

Read-modify-write through [read]/write, so it inherits the DPORT erratum workaround and the lock: the PHY is brought up from a task while the other core may be doing anything at all, and a lost bit here is a radio block that never gets a clock.

Writes DPORT. mask must be a combination of the RADIO_CLK_* constants.

unsafe fn read(reg: u32) -> u32

Read a DPORT register, with the erratum workaround.

Use this for every DPORT read. A plain read_volatile on this block is correct almost always, which is what makes the bug so unpleasant: it fails only when the other core happens to touch APB in the same few cycles, so it is rare, load-dependent, and reproduces on nobody’s desk.

reg must be a valid DPORT register address.

unsafe fn wifi_mac_reset()

Pulse the Wi-Fi MAC’s reset.

esp-idf’s wifi_reset_mac_wrapper, which is a set followed immediately by a clear — the reset is edge-driven and leaving the bit asserted would hold the MAC down rather than restart it.

Deliberately not periph_module_reset(PERIPH_WIFI_MODULE), which is what NuttX calls: on the ESP32 that lands in periph_ll_get_rst_en_mask’s default arm and returns a mask of zero, so it sets and clears nothing. The two references disagree, and this follows esp-idf, whose version is the one the blob was built and tested against.

Writes DPORT and resets a peripheral. The Wi-Fi MAC must not be mid-frame, which is the caller’s business — the blob calls this while its own driver is stopped.

unsafe fn write(reg: u32, value: u32)

Write a DPORT register.

Plain store: esp-idf’s DPORT_REG_WRITE is _DPORT_REG_WRITE, documented as not requiring protection. This exists so that call sites read symmetrically with [read], and so the one place that could change is one place.

reg must be a valid DPORT register address, and value meaningful for it.

const PERIP_CLK_EN: u32;

DPORT_PERIP_CLK_EN_REG. Public so the on-target self-tests have a real DPORT register to read — the erratum workaround can only be checked against hardware, and a test that invents an address checks nothing.

const PERIP_RST_EN: u32;

DPORT_PERIP_RST_EN_REG.

const PERI_CLK_EN: u32;

DPORT_PERI_CLK_EN_REG. The crypto blocks — AES, SHA, RSA — are not gated by PERIP_CLK_EN like every other peripheral; they share this separate register (and PERI_RST_EN below). Confirmed against esp-idf soc/esp32/include/soc/dport_reg.h and hal/esp32/clk_gate_ll.h.

const PERI_RST_EN: u32;

DPORT_PERI_RST_EN_REG.

const RADIO_CLK_BT: u32;

Clocks only Bluetooth needs. Bits 11, 16 and 17.

const RADIO_CLK_COMMON: u32;

Clocks both radios need. Enabling either radio needs these on, and only the last one out may turn them off – which is the caller’s business, since this crate has no idea who else is using the radio.

const RADIO_CLK_WIFI: u32;

Clocks only Wi-Fi needs. Bits 1, 2 and 10.