Skip to content

soc_esp32::rtc

The RTC_CNTL register block.

RTC_CNTL is a peripheral several unrelated things need: the CPU-frequency measurement wants its counter, the second core is stalled through it, the reset reason is read from it, one watchdog lives in it, and the radio asks it for the crystal frequency. Every one of those had its own copy of the base address and its own offsets, and two of them had their own copy of the latch-and-read sequence below.

Chip infrastructure that more than one driver wants is exactly what a soc/* crate is for, so it lives here once.

RTC_CNTL runs on the RTC slow clock — nominally 150 kHz from an internal RC oscillator, against a CPU at 80 or 240 MHz. A write does not take effect when the store retires. That is not a detail: appcpu::stall returned immediately after requesting a stall, the caller disabled the other core’s cache in the microseconds before the request landed, and the core executed rubbish. Anything here that changes state has to say how the caller knows it happened.

Register facts from esp-idf components/soc/esp32/register/soc/ rtc_cntl_reg.h, cross-checked against NuttX where the encoding is not obvious.

unsafe fn counter(max_polls: u32) -> Option<u64>

Sample the 48-bit RTC counter.

The counter is latched rather than read live: ask with TIME_UPDATE, wait for TIME_VALID, then read the halves. Reading them without the latch can straddle a carry and produce a time that never happened.

Returns None if the sample has not landed after max_polls reads, so a stopped or missing RTC block fails rather than hanging the caller. Both callers care: one runs during boot, the other inside PHY initialisation, and a hang in either points nowhere near the clock that stopped.

esp-idf’s rtc_time_get follows this with a write to RTC_CNTL_INT_CLR_REG to drop a stale “time valid” interrupt flag. FlintOS never enables that interrupt, so there is nothing to clear.

Reads and writes RTC_CNTL registers. No side effects beyond the sample request, which is what the register is for.

fn round_to_plausible(raw_hz: u64) -> Option<u32>

Snap a raw frequency measurement to the nearest plausible ESP32 CPU clock, or None if it is more than 25% from any of them.

Rounding rather than reporting the raw number is the point: the RTC reference is an RC oscillator, so the raw figure is never exactly right, and a tick period computed from it would drift. Refusing a measurement that lands nowhere near a real clock is what keeps a stopped or misconfigured reference from being believed.

unsafe fn xtal_freq_mhz() -> Option<u32>

The crystal frequency in MHz as the bootloader recorded it, or None if the register does not hold a credible one.

The encoding is not obvious and is not guessed. NuttX documents it:

Values of RTC_XTAL_FREQ_REG and RTC_APB_FREQ_REG are stored as two copies in lower and upper 16-bit halves.

and esp-idf’s rtc_clk_xtal_freq_get reads the same register at the same offset. Two sources agreeing is what makes this safe to write.

The duplicated halves are the validity check: a register nothing has written reads as something whose halves differ. esp-idf also keeps a “ROM log disabled” flag in bit 16 — bit 0 of the upper half — and compensates by setting bit 0 of the value, so the halves still match and the low bit may be set spuriously. Hence the mask.

Returning None rather than a default is deliberate. A wrong crystal mis-calibrates the radio, and the symptom is poor range rather than anything pointing here, so the caller is made to decide what to do about not knowing.

Reads one RTC_CNTL register. No side effects.

const DEFAULT_CPU_HZ: u32;

What to assume when the measurement fails, in Hz.

80 MHz, because that is what the stock esp-idf second-stage bootloader – the one espflash writes – configures before handing off. It expects the application to raise the clock during its own init, which FlintOS does not do. 240 MHz is the figure an esp-idf application sees only after calling esp_clk_init().

A caller falling back to this should say so out loud: every timeout in the system is scaled by it, and using a wrong clock silently is what caused issue #6 in the first place.

const OPTIONS0: u32;

RTC_CNTL_OPTIONS0_REG. Holds SW_STALL_APPCPU_C0 in bits [1:0].

const PLAUSIBLE_CPU_HZ: [u32; 3];

CPU frequencies the second-stage bootloader could plausibly have left the part running at.

const RESET_STATE: u32;

RTC_CNTL_RESET_STATE_REG — why each core last reset.

const SLOW_HZ_NOMINAL: u64;

Nominal RTC slow-clock rate, used as the reference when measuring the CPU frequency.

The ESP32 defaults RTC_SLOW_CLK to an internal 150 kHz RC oscillator – untrimmed, commonly 5-10% off – unless something has switched it to the external 32 kHz crystal or the 8 MHz/256 divider. FlintOS’s boot path does neither. The imprecision is fine for the only question being asked: “80, 160 or 240 MHz?”, which a clock good to 10% resolves easily.

const SW_CPU_STALL: u32;

RTC_CNTL_SW_CPU_STALL_REG. Holds SW_STALL_APPCPU_C1 in bits [25:20].

const TIME0: u32;

RTC_CNTL_TIME0_REG — the low 32 bits of the counter.

const TIME1: u32;

RTC_CNTL_TIME1_REG — the high bits of the counter.

const TIME_UPDATE: u32;

RTC_CNTL_TIME_UPDATE_REG. Bit 31 requests a sample, bit 30 says it landed.

const WDTCONFIG0: u32;

RTC_CNTL_WDTCONFIG0_REG — the RTC watchdog’s control register.

const WDTCONFIG1: u32;

RTC_CNTL_WDTCONFIG1_REG — stage 0 timeout, in RTC slow-clock ticks.

const WDTFEED: u32;

RTC_CNTL_WDTFEED_REG.

const WDTWPROTECT: u32;

RTC_CNTL_WDTWPROTECT_REG — the write-protect key register.

const XTAL_26_MHZ: u32;
const XTAL_40_MHZ: u32;

Crystals the ESP32 supports, in MHz.

const XTAL_FREQ: u32;

RTC_XTAL_FREQ_REG, which is RTC_CNTL_STORE4_REG.

A retention register the bootloader writes, not a hardware one — nothing in silicon measures the crystal and puts it here.