ESP32-C6: undocumented radio registers

What this project knows about the ESP32-C6 registers that lib/esp_sdr relies on, or that a later feature could use, and that Espressif does not document, or documents without the behaviour that matters: where each one is, its fields, what it does, the values seen, and how sure we are. The ESP32-S3 has its own page, undocumented-registers-esp32s3.md. How the library copes with the radio’s quirks is in hardware-quirks.md.

How to read this page

  • The register part follows Espressif’s technical reference manual: a register summary, then each register with a bit-field table and its fields’ descriptions. The table’s Init row, where present, is the value after the PHY’s initialization (read on the bench after it), not the reset value. “(unknown)” marks bits nobody has tied to a function yet, “(documented)” bits that Espressif’s SVD or headers document in a register this page only adds to.

  • Names are spelled as in Espressif’s headers (REGISTER_REG, FIELD): Espressif’s own in the peripherals its SVD has (HP_SYS, IO_MUX, MODEM_SYSCON, MODEM_LPCON), this project’s elsewhere.

  • Access: R/W read and write, RO read only (set by the hardware), WO written only (reads unknown). It is inferred from the vendor code and this project’s use.

  • Analog I2C registers are 8 bits wide and addressed block:reg with a decimal register number; 0x62:12[3:2] is bits 3:2 of register 12 in analog block 0x62 (upstream esp-sdr uses the same form). Their blocks are not memory-mapped (see “Analog I2C access”).

  • The register part is generated from registers/esp32c6.svd, a CMSIS-SVD file of this project’s additions to Espressif’s SVD (espressif/svd at be20aa1), by doc/registers/svd2md.py: edit the SVD, then run the script.

  • Evidence paths are relative to this repository (lib/, doc/, …) or, for files outside it, to the west workspace: modules/lib/esp-sdr/ is upstream esp-sdr (commit 1fe5535), modules/hal/espressif/ the HAL. git show <commit>:<path> cites a file at an older upstream commit: run it in modules/lib/esp-sdr/ (34bb79b and e605684 are in the history of 1fe5535). “Build” with a date is the link map of an app of this repository built that day. “Bench” with a date is a measurement with scratch tools that are not in the repository: 2026-10-07 to 09 and 2026-10-11 on one XIAO ESP32C6 next to one XIAO ESP32S3 (2026-10-11: the C6 transmitting into the S3 over coax, the S3’s receive gain fixed at index 0).

  • Entries marked decompile come from objdump of the C6 blobs (modules/hal/espressif/zephyr/blobs/lib/esp32c6/libphy.a, zephyr/blobs/lib/esp32c6/librftest.a and, where an entry names them, libcoexist.a and libble_app.a from the same directory as libphy.a) and of the C6 ROM. ROM addresses refer to esp32c6_rev0_rom.elf of esp-rom-elfs 20230320. An entry citing an S3 object, as in “S3 phy_rfpll.o”, means a member of modules/hal/espressif/zephyr/blobs/lib/esp32s3/libphy.a. ESP32-C5 and ESP32-C61 entries marked decompile come from objdump of those chips’ libphy.a, libcoexist.a and libble_app.a in modules/hal/espressif/zephyr/blobs/lib/esp32c5/ and esp32c61/, their librftest.a from esp-phy-lib e37ecdc9, and their ROMs from esp-rom-elfs 20260528.

Confidence

Meaning

measured

Seen on the bench boards, on air between them, or in the link map of a build.

working

The library depends on it and works; the meaning is not proven beyond that.

upstream

Stated by upstream esp-sdr (code, comments, docs); not re-checked here.

decompile

Read from vendor code (decompile or disassembly); not confirmed on hardware.

speculative

A guess. Treat the field as unknown.

Analog I2C access

The library reaches the analog blocks through two ROM functions, rom_chip_i2c_readReg(block, host, reg) and rom_chip_i2c_writeReg(block, host, reg, value), on 8-bit registers. The Zephyr link maps them to the ROM at 0x40003dc0 and 0x400040b4.

Fact

Evidence

The host argument is ignored. The I2C master is picked from the block id: 0x62, 0x63, 0x64, 0x67 and 0x6b go to master 1, every other block to master 0 (bit mask 0x227 over block - 0x62).

decompile: C6 ROM rom_chip_i2c_readReg and rom_get_i2c_hostid (0x40003d4a, andi 551). measured: host 0 and 1 read identically in every register of blocks 0x60 to 0x6f (bench 2026-10-09).

Host values in code are therefore cosmetic: the library passes 1 for BBTOP, the RF PLL and the LO selector, and upstream’s docs say host 1 for BBTOP on every chip. Across the two chips they are inconsistent (see the ESP32-S3 page).

lib/esp_sdr/src/esp_sdr_priv.h:106; modules/lib/esp-sdr/docs/rx-controls.md:43

The ROM functions take no lock (shown on the ESP32-S3, whose ROM lock functions are empty; see its page). libphy’s ram_chip_i2c_* call phy_i2c_enter_critical(), which the HAL maps to regi2c_enter_critical(). The Wi-Fi PHY may run a sequence at any time, so the library takes regi2c_enter_critical() around every rom_chip_i2c_* sequence.

modules/hal/espressif/components/esp_phy/src/phy_override.c:50-58; working: lib/esp_sdr/src/esp_sdr_priv.h:175-180, hardware-quirks.md

The I2C masters themselves are memory-mapped (I2C_ANA_MST below).

Block map

Register counts are the loop bounds of the vendor’s debug dump phy_i2c_check(), names the HAL’s comment. The HAL defines fields only for 0x61, 0x66, 0x69, 0x6a and 0x6d.

Block

HAL name and registers

Used by esp_sdr

0x61

ULP, 11

no (stale-state symptom)

0x62

PLL, 19 (0 to 18)

VCO status, LO divider

0x63

SDM, 7

no (read on the bench)

0x64

absent

0x65

absent

0x66

BBPLL, 11

no

0x67

BB, 57

RX filter codes

0x69

SAR, 9

no

0x6a

BIAS, 4

no

0x6b

TXRF, 1 to 15

no

0x6d

PMU, 15

no

Evidence: decompile C6 libphy.a phy_debug.o (phy_i2c_check); modules/hal/espressif/components/esp_hal_regi2c/esp32c6/regi2c_impl.c:9-19. measured: blocks 0x60, 0x64, 0x65, 0x68, 0x6c, 0x6e and 0x6f read 0xff in every register (bench 2026-10-09).

Register summary

Name

Description

Address

Access

Dump and DAC engine

DUMP_CTRL_REG

Receive side: run, trigger and count of a capture

0x600a9004

varies

DUMP_MODE_REG

Dump source; the live write index in upstream’s ring

0x600a9008

varies

DUMP_DAC_TRIG_REG

Transmit side: run, loop, rate and count of a playback

0x600a900c

varies

DUMP_CONFIG_REG

Lane selectors: the sources packed into a dump word

0x600a9014

R/W

DUMP_DIAG_SEL0_REG, DUMP_DIAG_SEL1_REG

Modem diagnostic bus selectors

0x600a9018, 0x600a901c

R/W

SRAM bank ownership

HP_SYSTEM_SRAM_USAGE_CONF_REG

Hands an SRAM block to the dump engine

0x60095004

R/W

Modem clocks and reset

MODEM_SYSCON_CLK_CONF_REG

Modem clock enables, forced on for the dump engine

0x600a9804

R/W

MODEM_SYSCON_CLK_CONF_POWER_ST_REG

Forced to 0xffffffff for the dump engine

0x600a980c

R/W

MODEM_SYSCON_MODEM_RST_CONF_REG

Modem resets, pulsed at boot against stale modem state

0x600a9810

R/W

MODEM_SYSCON_CLK_CONF1_REG

Bits 18:0 forced on for the dump engine

0x600a9814

R/W

MODEM_SYSCON_WIFI_BB_CFG_REG

Wi-Fi baseband configuration; the 802.11p clock mode in bits 4:2

0x600a981c

R/W

Modem LP reset

MODEM_LPCON_RST_CONF_REG

Modem LP resets, pulsed at boot against stale modem state

0x600af024

WO

Modem diagnostic lines

IO_MUX_MODEM_DIAG_EN_REG

Routes modem diagnostic lines into the GPIO matrix

0x600900bc

R/W

BTBB page (0x600a2000)

BTBB_RB4_REG

Dump gate in bit 0

0x600a20b4

R/W

BTBB_R8A0_REG

Unknown; bits 31:30 written by the BT branch of force_txon_mode()

0x600a28a0

R/W

Wi-Fi MAC page (0x600a4000)

WIFIMAC_EVENT_SEL_REG

Wi-Fi MAC event selector, used by the dump’s adctrig() mode 7

0x600a4e30

R/W

AGC force

AGC_FORCE_REG

Forced receive gain index, force bit and largest calibrated index

0x600a702c

R/W

AGC_R30_REG

Unknown; bit 5 cleared in the 802.11p clock mode

0x600a7030

R/W

AGC_RB8_REG

Unknown; bits 2:0 written by set_dump_mode()

0x600a70b8

R/W

AGC_RDC_REG

Unknown; bit 16 cleared by force_txon()

0x600a70dc

R/W

Front end

FE_RC0_REG

Hardware frequency engine stop

0x600a00c0

R/W

FE_PWR_INDEX_REG

Power index force

0x600a0410

R/W

FE_DAC_SRC_REG

DAC source select

0x600a0414

R/W

FE_TXDC_CAL_REG

TX DC calibration comparator

0x600a0418

varies

FE_TONE0_REG, FE_TONE1_REG

Tones 0 and 1: enable, attenuation, frequency

0x600a0420, 0x600a0424

R/W

FE_TONE_FREQ_LO_REG

Fine tone frequency: the low two bits of each tone’s frequency

0x600a042c

R/W

FE_TXDC_REG

TX DC value

0x600a0444

R/W

FE_R44C_REG

Unknown; init sets bits 27 and 25 and clears bit 22

0x600a044c

R/W

FE_FORCE_REG

Receive, transmit and BT mode force pairs; receive and transmit clocks

0x600a0910

R/W

FE_R958_REG

Unknown; set_dump_mode() clears bits 1:0

0x600a0958

R/W

Analog I2C master

I2C_ANA_MST_I2C0_CTRL_REG, I2C_ANA_MST_I2C1_CTRL_REG

Command word of master 0 and master 1

0x600af800, 0x600af804

varies

I2C_ANA_MST_ANA_CONF1_REG

Read mask

0x600af81c

R/W

I2C_ANA_MST_ANA_CONF2_REG

Master select

0x600af820

R/W

Analog I2C block 0x61 (ULP)

ULP_R4

HAL I2C_ULP_OCODE, function undocumented

0x61:4

R/W

ULP_R9

Unknown

0x61:9

R/W

Analog I2C block 0x62 (RF PLL)

RFPLL_R0

VCO calibration start

0x62:0

varies

RFPLL_R1

Manual capacitor code, bits 7:0

0x62:1

R/W

RFPLL_R2

Manual code bit 8 and a 4-bit code override

0x62:2

R/W

RFPLL_R4

Unknown

0x62:4

R/W

RFPLL_R5

Capacitor code readback, bits 7:0

0x62:5

RO

RFPLL_R6

Calibrated 4-bit code

0x62:6

RO

RFPLL_R7

Capacitor code bit 8 and calibration done

0x62:7

RO

RFPLL_R11

Capacitor code source

0x62:11

R/W

RFPLL_R12

VCO window comparator and two unknown status bits

0x62:12

RO

RFPLL_R14

Charge pump calibration done and result

0x62:14

RO

RFPLL_R15

Charge pump calibration

0x62:15

varies

RFPLL_R16

LO 5/6 divider select

0x62:16

R/W

RFPLL_R17

Unknown

0x62:17

R/W

RFPLL_R18

Unknown

0x62:18

R/W

Analog I2C block 0x63 (RF PLL fractional-N divider)

RFPLL_SDM_R0

Bit 3 cleared and set around a divider write; other bits unknown

0x63:0

R/W

RFPLL_SDM_R1

Unknown

0x63:1

R/W

RFPLL_SDM_R3

Divider integer part

0x63:3

R/W

RFPLL_SDM_R4

Divider fraction, high byte

0x63:4

R/W

RFPLL_SDM_R5

Divider fraction, middle byte

0x63:5

R/W

RFPLL_SDM_R6

Divider fraction, low 3 bits

0x63:6

R/W

Analog I2C block 0x66 (BBPLL)

Analog I2C block 0x67 (BBTOP, analog baseband)

BBTOP_R4, BBTOP_R5

Receive low-pass capacitor code, I (BBTOP_R4) and Q (BBTOP_R5)

0x67:4, 0x67:5

R/W

BBTOP_R12, BBTOP_R13

Probably the transmit low-pass pair A, as on the S3

0x67:12, 0x67:13

R/W

BBTOP_R14, BBTOP_R15

Probably the transmit low-pass pair B, as on the S3

0x67:14, 0x67:15

R/W

Analog I2C block 0x6a (bias)

Registers

Dump and DAC engine

Same RUN, TRIGGER, DONE and COUNT bits as the S3 at another address (upstream modules/lib/esp-sdr/main/families/c5_c6_c61/receiver.c, modules/lib/esp-sdr/main/targets/esp32c6/chip.h). Receive runs at 80 MS/s only; the transmit side is a DAC engine at 0x600a900c (DUMP_DAC_TRIG_REG, below), which no vendor code programs: the C6 dactrig() only writes a ramp into 0x40840000 (decompile C6 librftest.a mac_common.o). Base 0x600a9000 has no name in the HAL. Each capture and each playback needs the clocks in MODEM_SYSCON forced on and an SRAM block handed to the engine in HP_SYSTEM_SRAM_USAGE_CONF_REG.

Receive rate. The C6 adctrig() starts only mode 0 by software; the other modes wait for a hardware event. It writes no part of DUMP_MODE_REG (0x600a9008) but the source field, DUMP_SRC, and never reads the argument that the C5 and C61 versions turn into a rate (decompile C6 librftest.a mac_common.o):

adctrig() mode

DUMP_SRC

Also

0

15

software start

1, 3

1

2

0

4

2

5

3

tries to send a frame each poll, at most one per tx_frame_delay_us (800 by default)

6

4

as mode 5

7

5

Wi-Fi MAC event selector WIFIMAC_EVENT_SEL_REG (0x600a4e30): bit 8 set, bits 7:0 the event

8

0

BTBB_DUMP_GATE (0x600a20b4 bit 0) set, lanes 0x0e to 0x11, BLE receiver started

Upstream found no other source that gives a uniform stream, and neither adc_rate_set() nor candidate divider bits changed the timing (upstream git show 34bb79b:docs/rx-controls.md, “C6 sample-rate investigation”). Upstream’s C6 ring is gapless at 80 MS/s, rotating SRAM blocks 1 and 2 under a circular DUMP_CTRL_REG (DUMP_CIRCULAR, bit 17, count 16384), and consumes it as FFT windows (upstream modules/lib/esp-sdr/main/common/ring_capture.c:96-131, 1485-1507): the 160 MHz CPU has 2 cycles per sample, too few to decimate the whole stream.

Leads for a continuous C6 receive below 80 MS/s, none tried:

  • The modem diagnostic bus into PARL_IO. A third-party project, not re-checked here, reports for the ESP32-C5 that the dump’s I/Q MSBs appear on MODEM_DIAG (Q[9:6] on lines 9:6, I[9:6] on 19:16), and that routed through the GPIO matrix into PARL_IO, a cyclic 40 MS/s capture takes every second sample of the 80 MS/s bus as 4+4-bit I/Q, bit-perfect in a bounded capture (it claims no gapless running across ring wraps; Twotoz/C5VRX). The C6 selector registers are DUMP_DIAG_SEL0_REG and DUMP_DIAG_SEL1_REG (0x600a9018 and 0x600a901c), and the diag lines are enabled in IO_MUX_MODEM_DIAG_EN_REG (0x600900bc); both below. Which selector value puts receive I/Q on the C6 bus is unknown. Sampling below 80 MHz decimates without filtering, so the analog low-pass has to be narrowed first.

  • The 802.11p clock mode: phy_11p_set(1, m) writes MODEM_SYSCON_WIFI_BB_CFG_REG (0x600a981c) bits 4:2 = 4 for m = 0, else 5, presumably for 10 and 5 MHz channels, and clears AGC_R30_REG (0x600a7030) bit 5 (decompile C6 libphy.a phy_feature.o). Whether the dump follows the slower clock is unknown.

  • The BLE dump (mode 8 above): rate unknown; it runs only while the BLE receiver does.

Register DUMP_CTRL_REG (0x600a9004)

Receive side: run, trigger and count of a capture

Bits

31

30:20

19

18

17

16:14

13:0

Field

DUMP_RUN

(unknown)

DUMP_TRIGGER

DUMP_DONE

DUMP_CIRCULAR

(unknown)

DUMP_COUNT

DUMP_RUN Enables the engine, as on the S3. (R/W)

DUMP_TRIGGER As on the S3, write 1, then 0, to start a one-shot capture. (WO)

DUMP_DONE As on the S3, reads 1 once DUMP_COUNT words are written. Write DUMP_COUNT with DUMP_DONE first to clear a stale DUMP_DONE, as the stock sequence does. (R/W)

DUMP_CIRCULAR Circular ring, in upstream’s C6 ring code. Not used here. (R/W)

DUMP_COUNT Words to capture, 256 to 16380; 16380 samples take about 205 us. (R/W)

No rate field, unlike the S3’s: receive runs at 80 MS/s only.

Evidence:

  • working lib/esp_sdr/src/esp_sdr_priv.h:77-81, lib/esp_sdr/src/esp_sdr_rx.c:302-310; upstream modules/lib/esp-sdr/main/targets/esp32c6/chip.h:43-46 (DUMP_RUN, DUMP_TRIGGER, DUMP_DONE, DUMP_COUNT)

  • DUMP_CIRCULAR: upstream modules/lib/esp-sdr/main/common/ring_capture.c:104, 1499-1505

Register DUMP_MODE_REG (0x600a9008)

Dump source; the live write index in upstream’s ring

Bits

31:24

23:21

20:19

18:15

14:0

Field

(unknown)

DUMP_B23_21

(unknown)

DUMP_SRC

(unknown)

DUMP_B23_21 Not a rate field, unlike on the C5 and C61, where these bits take a rate code: written bits do not stay, and captures and replays keep their timing. (RO)

DUMP_SRC Dump source; 15 = raw receive I/Q, as the stock adctrig() (see “Receive rate” above for the other sources). The library writes 15. (R/W)

Upstream’s ring code reads the live write index from the low bits of this register.

Evidence:

  • upstream modules/lib/esp-sdr/main/common/ring_capture.c:96, 1507 (write index)

  • DUMP_B23_21: measured bench 2026-10-11; C5 and C61: upstream modules/lib/esp-sdr/main/targets/esp32c5/chip.h:11, modules/lib/esp-sdr/main/targets/esp32c61/chip.h:20-21

  • DUMP_SRC: working lib/esp_sdr/src/esp_sdr_priv.h:82-84; upstream modules/lib/esp-sdr/main/targets/esp32c6/chip.h:6, 39

Register DUMP_DAC_TRIG_REG (0x600a900c)

Transmit side: run, loop, rate and count of a playback

Bits

31

30:28

27:20

19

18

17:16

15

14

13:0

Field

DUMP_DAC_RUN

(unknown)

DUMP_B27_20

DUMP_DAC_LOOP

DUMP_DAC_DONE

(unknown)

DUMP_DAC_RATE_80M

(unknown)

DUMP_DAC_COUNT

DUMP_DAC_RUN Written with DUMP_DAC_COUNT and with DUMP_DAC_LOOP clear: one pass, with no trigger pulse as on the S3. (R/W)

DUMP_B27_20 Read back as written; effect untested. The S3 dactrig() writes the count’s low byte here (see the ESP32-S3 page); upstream’s C5 replay uses it as a repeat count, 0 meaning endless with bit 19 set. (R/W)

DUMP_DAC_LOOP Set with DUMP_DAC_RUN (DUMP_B27_20 at 0): the engine replays the first DUMP_DAC_COUNT words back to back until DUMP_DAC_RUN is cleared; DUMP_DAC_DONE stays clear. (R/W)

DUMP_DAC_DONE Sets after one pass. (RO)

DUMP_DAC_RATE_80M 1 = 80 MS/s, 0 = 40 MS/s: a pass takes 4 cycles of the 160 MHz CPU per word at 40 MS/s (DUMP_DAC_COUNT 1000 to 16000), 2 at 80 MS/s (DUMP_DAC_COUNT 16000). The rate is this bit, as on the S3, not bit 17 as in upstream’s C5 replay. (R/W)

DUMP_DAC_COUNT Words to play from the start of the owned block, up to 16383. (R/W)

8 bytes above DUMP_CTRL_REG, as on the S3. Bits 14, 16 and 17 do not read back.

  • One word per sample, I in bits 9:0 and Q in 19:10, as for the S3’s DAC (see “Sample word formats”): a complex tone of k cycles in n words plays at +k/n of the rate (200 cycles in 16000 words: 0.5 MHz at 40 MS/s, 1 MHz at 80 MS/s), its image 37 to 40 dB down. All-zero words play nothing, a constant word a carrier.

  • What playback needs: the dump clocks forced on as for a capture (without them DUMP_DAC_DONE still sets on time but nothing reaches the antenna; see MODEM_SYSCON_CLK_CONF_REG), SRAM block 2 handed to the engine (HP_SYSTEM_SRAM_USAGE = 4, that is 0x60095004 bits 11:8, samples from 0x40840000), and the transmit chain on without its tone: the vendor CW (esp_phy_wifi_tx_tone(1, 6, 80)), then bit 18 (FE_TONE_EN) of 0x600a0420 and 0x600a0424 cleared. That leaves the DAC source select, FE_DAC_SRC (0x600a0414 bits 1:0), at 0, the tone generator’s setting (see “Front end”).

  • While the engine owns the block, CPU stores are dropped and loads return other data, as during a capture.

No vendor blob or ROM of the C6, C5 or C61 writes this register. Only upstream code does: the C5 transmitter of its first upload (emit_dma(), removed in ab76232), and the C61 capture and ring code, which writes 0 to it before each capture. Upstream’s S31 code clears bit 31 of its counterpart, 0x2010900c, when it sets up the dump.

Evidence:

  • measured bench 2026-10-11 (read-back, sample format, what playback needs, CPU access)

  • decompile (no vendor or ROM writer)

  • git show e605684:main/c5_main.c, emit_dma() (C5 transmitter)

  • upstream modules/lib/esp-sdr/main/targets/esp32c61/chip.h:12, 17, modules/lib/esp-sdr/main/common/ring_capture.c:1493 (C61)

  • upstream modules/lib/esp-sdr/main/targets/esp32s31/burst/receiver.c:37, 123 (S31)

  • DUMP_DAC_RUN: measured bench 2026-10-11

  • DUMP_B27_20: measured bench 2026-10-11 (read-back only); upstream git show e605684:main/c5_main.c, comment above emit_dma()

  • DUMP_DAC_LOOP: measured bench 2026-10-11

  • DUMP_DAC_DONE: measured bench 2026-10-11

  • DUMP_DAC_RATE_80M: measured bench 2026-10-11

  • DUMP_DAC_COUNT: measured bench 2026-10-11

Register DUMP_CONFIG_REG (0x600a9014)

Lane selectors: the sources packed into a dump word

Bits

31:25

24:0

Field

(unknown)

DUMP_LANES

DUMP_LANES Lane selectors as the S3’s DUMP_CONFIG_REG (0x60033d90, see the ESP32-S3 page): four 6-bit lane selectors, lane n in bits 6n+5:6n. The library clears the field, then sets it to 0x000c2040. adctrig() mode 8, the BLE dump, uses lanes 0x0e to 0x11 (see “Receive rate” above). (R/W)

Evidence:

  • DUMP_LANES: working lib/esp_sdr/src/esp_sdr_rx.c:258; upstream modules/lib/esp-sdr/main/targets/esp32c6/chip.h:38

  • DUMP_LANES: decompile C6 librftest.a mac_common.o (mode 8)

Register DUMP_DIAG_SEL0_REG, DUMP_DIAG_SEL1_REG (0x600a9018, 0x600a901c)

Modem diagnostic bus selectors

Bits

31:0

Field

(unknown)

gpio_diag_init_ble() writes 1 to DUMP_DIAG_SEL0_REG and coex_hw_debug_matrix_config() 2 to DUMP_DIAG_SEL1_REG, on the C6 as on the C5, which has the pair at 0x600a9404 and 0x600a9408. On the C6 each function then enables diag lines in IO_MUX_MODEM_DIAG_EN_REG: 0xffff0000 and 0x0000000f. Which selector value puts receive I/Q on the C6 bus is unknown (see the leads above).

Evidence:

  • decompile C6 and C5 libble_app.a gpio_diag.c.o, libcoexist.a coexist_hw.o

SRAM bank ownership

Register HP_SYSTEM_SRAM_USAGE_CONF_REG (0x60095004)

Hands an SRAM block to the dump engine

Bits

31:17

16

15:12

11:8

7:1

0

Field

(unknown)

HP_SYSTEM_MAC_DUMP_ALLOC

(unknown)

HP_SYSTEM_SRAM_USAGE

(unknown)

(documented)

HP_SYSTEM_MAC_DUMP_ALLOC Adds a 64 KiB offset (HAL). The library leaves it 0; upstream’s ring clears it. (R/W)

HP_SYSTEM_SRAM_USAGE One-hot: field bit n (register bit 8 + n) hands block n, 0x40800000 + n x 128 KiB, to the engine, for capture and playback alike; 0 gives every block back to the CPUs. n = 2 (0x40840000, the stock adctrig() bank) is the capture bank. The HAL says “0: cpu use hp-memory, 1: mac-dump accessing hp-memory”. (R/W)

The library saves the register and restores it after each use. Undocumented behaviour:

  • While the engine owns a block the CPUs cannot reach any of it, so nothing else may live there; the linker snippet reserves the block and lowers _heap_sentry. The engine owns the whole 128 KiB block during a capture, although a capture uses at most 64 KiB, so the library’s other buffers live in block 3.

  • On the RISC-V core the switch must land before the engine starts: a fence rw, rw and a read-back follow each write.

  • Only block 2 can be the engine’s. Block 3 ends in the ROM’s .bss and .data (0x4087e610 to 0x40880000), which ROM functions use at any time, and a Zephyr image with Wi-Fi fills blocks 0 and 1 (the C6 build of apps/rfprobe has _end at 0x408366b0 and its libc heap above that up to block 2), so upstream’s rotation over blocks 1 and 2 does not fit.

Evidence:

  • lib/esp_sdr/src/esp_sdr_rx.c:246-278, lib/esp_sdr/src/esp_sdr.c:123-133 (save and restore)

  • measured bench 2026-10-08, lib/esp_sdr/esp_sdr_bank_c6.ld:18-20 (whole block)

  • working lib/esp_sdr/src/esp_sdr_rx.c:262-265, 274-277 (fence)

  • measured build 2026-10-11 (only block 2); modules/zephyr/soc/espressif/esp32c6/memory.h:29-40 (ROM .bss and .data); lib/esp_sdr/esp_sdr_bank_c6.ld:29-30 (_heap_sentry, rfprobe heap)

  • HP_SYSTEM_MAC_DUMP_ALLOC: modules/hal/espressif/components/soc/esp32c6/register/soc/hp_system_reg.h:63-68

  • HP_SYSTEM_SRAM_USAGE: modules/hal/espressif/components/soc/esp32c6/register/soc/hp_system_reg.h:46-62

Modem clocks and reset

Documented registers; the entries give the behaviour the HAL does not document.

Register MODEM_SYSCON_CLK_CONF_REG (0x600a9804)

Modem clock enables, forced on for the dump engine

Bits

31

30:22

21

20:0

Field

MODEM_SYSCON_CLK_DATA_DUMP_EN

(documented)

MODEM_SYSCON_CLK_DATA_DUMP_MUX

(unknown)

MODEM_SYSCON_CLK_DATA_DUMP_EN Written 1 with the rest of the register (0xffffffff). (R/W)

MODEM_SYSCON_CLK_DATA_DUMP_MUX Written 1 with the rest of the register (0xffffffff). (R/W)

The dump engine needs this register, MODEM_SYSCON_CLK_CONF1_REG and MODEM_SYSCON_CLK_CONF_POWER_ST_REG forced on before each capture and each playback, as the vendor’s rftest_init()/phy_set_clk_conf() does. The library writes 0xffffffff here, then 0x7ffff to MODEM_SYSCON_CLK_CONF1_REG, then 0xffffffff to MODEM_SYSCON_CLK_CONF_POWER_ST_REG. What playback needs besides: see DUMP_DAC_TRIG_REG.

Evidence:

  • working lib/esp_sdr/src/esp_sdr_rx.c:255-257; measured bench 2026-10-11 (playback); upstream modules/lib/esp-sdr/main/targets/esp32c6/chip.h:35-36; modules/hal/espressif/components/soc/esp32c6/include/modem/modem_syscon_reg.h:21-27, 82-87

Register MODEM_SYSCON_CLK_CONF_POWER_ST_REG (0x600a980c)

Forced to 0xffffffff for the dump engine

Bits

31:8

7:0

Field

(documented)

(unknown)

Written 0xffffffff before each capture and each playback, the last of the three clock writes (see MODEM_SYSCON_CLK_CONF_REG).

Evidence:

  • working lib/esp_sdr/src/esp_sdr_rx.c:255-257; measured bench 2026-10-11 (playback); upstream modules/lib/esp-sdr/main/targets/esp32c6/chip.h:35-36; modules/hal/espressif/components/soc/esp32c6/include/modem/modem_syscon_reg.h:151

Register MODEM_SYSCON_MODEM_RST_CONF_REG (0x600a9810)

Modem resets, pulsed at boot against stale modem state

Bits

31:29

28

27:24

23

22

21:19

18:14

13:11

10

9

8

7:0

Field

(documented)

(unknown)

(documented)

(unknown)

(documented)

(unknown)

(documented)

(unknown)

(documented)

(unknown)

(documented)

(unknown)

Undocumented: the modem subsystem keeps the previous firmware’s state across a chip reset (USB-Serial-JTAG, esptool). The library writes 0xffffffff, then 0, here and 0xf, then 0, to MODEM_LPCON_RST_CONF_REG at PRE_KERNEL_1, before the Wi-Fi driver starts, through modem_syscon_ll_reset_all() and modem_lpcon_ll_reset_all(), as ESP-IDF’s esp_system_reset_modules_on_exit() does on a restart.

Evidence:

  • measured bench 2026-10-09 (symptoms under “Stale modem state”); working lib/esp_sdr/src/esp_sdr.c:29-48; modules/hal/espressif/components/hal/esp32c6/include/hal/modem_syscon_ll.h:325-329, modules/hal/espressif/components/esp_system/port/soc/esp32c6/system_internal.c:45-46

Register MODEM_SYSCON_CLK_CONF1_REG (0x600a9814)

Bits 18:0 forced on for the dump engine

Bits

31:24

23:0

Field

(unknown)

(documented)

Bits 18:0 written 1 (0x7ffff) before each capture and each playback, the second of the three clock writes (see MODEM_SYSCON_CLK_CONF_REG).

Evidence:

  • working lib/esp_sdr/src/esp_sdr_rx.c:255-257; measured bench 2026-10-11 (playback); upstream modules/lib/esp-sdr/main/targets/esp32c6/chip.h:35-36; modules/hal/espressif/components/soc/esp32c6/include/modem/modem_syscon_reg.h:281

Register MODEM_SYSCON_WIFI_BB_CFG_REG (0x600a981c)

Wi-Fi baseband configuration; the 802.11p clock mode in bits 4:2

Bits

31:0

Field

MODEM_SYSCON_WIFI_BB_CFG

MODEM_SYSCON_WIFI_BB_CFG Bits 4:2: phy_11p_set(1, m) writes 4 for m = 0, else 5, presumably for 10 and 5 MHz channels, and clears AGC_R30_REG bit 5. Whether the dump follows the slower clock is unknown (a lead under “Dump and DAC engine”). (R/W)

Evidence:

  • MODEM_SYSCON_WIFI_BB_CFG: decompile C6 libphy.a phy_feature.o

Modem LP reset

Documented register; the behaviour the HAL does not document is under MODEM_SYSCON_MODEM_RST_CONF_REG.

Register MODEM_LPCON_RST_CONF_REG (0x600af024)

Modem LP resets, pulsed at boot against stale modem state

Bits

31:4

3

2

1

0

Field

(unknown)

MODEM_LPCON_RST_LP_TIMER

MODEM_LPCON_RST_I2C_MST

MODEM_LPCON_RST_COEX

MODEM_LPCON_RST_WIFIPWR

MODEM_LPCON_RST_LP_TIMER Set, then cleared, at boot (0xf, then 0). (WO)

MODEM_LPCON_RST_I2C_MST Set, then cleared, at boot (0xf, then 0). (WO)

MODEM_LPCON_RST_COEX Set, then cleared, at boot (0xf, then 0). (WO)

MODEM_LPCON_RST_WIFIPWR Set, then cleared, at boot (0xf, then 0). (WO)

The library writes 0xf, then 0, at PRE_KERNEL_1 through modem_lpcon_ll_reset_all(), with MODEM_SYSCON_MODEM_RST_CONF_REG (see there for why).

Evidence:

  • measured bench 2026-10-09 (symptoms under “Stale modem state”); working lib/esp_sdr/src/esp_sdr.c:29-48; modules/hal/espressif/components/hal/esp32c6/include/hal/modem_lpcon_ll.h:266-270, modules/hal/espressif/components/esp_system/port/soc/esp32c6/system_internal.c:45-46

Modem diagnostic lines

Register IO_MUX_MODEM_DIAG_EN_REG (0x600900bc)

Routes modem diagnostic lines into the GPIO matrix

Bits

31:0

Field

IO_MUX_MODEM_DIAG_EN

IO_MUX_MODEM_DIAG_EN Bit i routes modem_diag[i] into the GPIO matrix. After writing their selectors (DUMP_DIAG_SEL0_REG, DUMP_DIAG_SEL1_REG), gpio_diag_init_ble() enables 0xffff0000 here and coex_hw_debug_matrix_config() 0x0000000f. A lead for a continuous receive below 80 MS/s (see “Dump and DAC engine”). (R/W)

Evidence:

  • IO_MUX_MODEM_DIAG_EN: modules/hal/espressif/components/soc/esp32c6/register/soc/io_mux_reg.h:325-336; decompile C6 libble_app.a gpio_diag.c.o, libcoexist.a coexist_hw.o

BTBB page (0x600a2000)

Register BTBB_RB4_REG (0x600a20b4)

Dump gate in bit 0

Bits

31:1

0

Field

(unknown)

BTBB_DUMP_GATE

BTBB_DUMP_GATE Cleared before every capture, as the stock sequence. The C6 adctrig() clears it in every mode but 8, the BLE baseband dump, which sets it (see “Receive rate” under “Dump and DAC engine”); upstream’s ESP32-H2 notes call it the trigger source. (R/W)

Evidence:

  • BTBB_DUMP_GATE: working lib/esp_sdr/src/esp_sdr_rx.c:261; decompile C6 librftest.a mac_common.o; upstream modules/lib/esp-sdr/main/targets/esp32c6/chip.h:40, modules/lib/esp-sdr/docs/esp32h2.md:20

Register BTBB_R8A0_REG (0x600a28a0)

Unknown; bits 31:30 written by the BT branch of force_txon_mode()

Bits

31:30

29:0

Field

BTBB_B31_30

(unknown)

BTBB_B31_30 Only with the BT flag (its second argument), force_txon_mode(on, bt, idx) stops the hardware frequency engine (FE_HW_FREQ_STOP, 0x600a00c0 bit 26), writes 0x600af4b4 bit 0, FE_BT_FORCE (0x600a0910 bits 13:12) and these bits (all ones with on, 0 without), and restarts the engine if it ran before (see “Front end”). (R/W)

Wi-Fi MAC page (0x600a4000)

Register WIFIMAC_EVENT_SEL_REG (0x600a4e30)

Wi-Fi MAC event selector, used by the dump’s adctrig() mode 7

Bits

31:9

8

7:0

Field

(unknown)

WIFIMAC_B8

WIFIMAC_EVENT

WIFIMAC_B8 Set by adctrig() mode 7. (R/W)

WIFIMAC_EVENT The event, written by adctrig() mode 7 (DUMP_SRC 5; see “Receive rate” under “Dump and DAC engine”). (R/W)

Evidence:

  • decompile C6 librftest.a mac_common.o

AGC force

Register AGC_FORCE_REG (0x600a702c)

Forced receive gain index, force bit and largest calibrated index

Bits

31:24

23

22:15

14:8

7:0

Field

AGC_FORCE_INDEX

AGC_FORCE_EN

(unknown)

AGC_GAIN_MAX

(unknown)

AGC_FORCE_INDEX Forced receive gain index. (R/W)

AGC_FORCE_EN Force: 1 holds the index, 0 hands the gain to the AGC. (R/W)

AGC_GAIN_MAX Largest calibrated gain index, as on the S3: below 80 (library cap 79). Larger reads point at uncalibrated slots; the library treats them as 0. (R/W)

Same layout as the S3’s 0x6001c02c; upstream calls the register AGCPWR_CTRL7. On the C6, force_rx_gain(force, index, 0) writes only this register. A new forced index alone can leave stale receive state, so a gain change reruns the front-end setup.

Evidence:

  • working lib/esp_sdr/src/esp_sdr_priv.h:110-112; decompile C6 librftest.a phy_test.o (force_rx_gain); upstream modules/lib/esp-sdr/main/common/burst_gain.h:11-12, 36

  • lib/esp_sdr/src/esp_sdr_rx.c:164-166 (front-end rerun)

Register AGC_R30_REG (0x600a7030)

Unknown; bit 5 cleared in the 802.11p clock mode

Bits

31:6

5

4:0

Field

(unknown)

AGC_B5

(unknown)

AGC_B5 phy_11p_set(1, m) writes MODEM_SYSCON_WIFI_BB_CFG_REG bits 4:2 and clears this bit (the 802.11p clock mode, a lead under “Dump and DAC engine”). (R/W)

Evidence:

  • AGC_B5: decompile C6 libphy.a phy_feature.o

Register AGC_RB8_REG (0x600a70b8)

Unknown; bits 2:0 written by set_dump_mode()

Bits

31:3

2:0

Field

(unknown)

AGC_B2_0

AGC_B2_0 set_dump_mode(x) clears FE_R958_REG (0x600a0958) bits 1:0 and writes 1 here when x is 0, else 0. (R/W)

Evidence:

  • AGC_B2_0: C6 librftest.a phy_test.o

Register AGC_RDC_REG (0x600a70dc)

Unknown; bit 16 cleared by force_txon()

Bits

31:17

16

15:0

Field

(unknown)

AGC_B16

(unknown)

AGC_B16 force_txon(on) clears it and sets FE_RX_FORCE (0x600a0910 bits 9:8) to 2, then FE_TX_FORCE (bits 11:10) to 3; off goes in reverse. force_txon_mode() calls force_txon(0), then force_txon(on) (see “Front end”). (R/W)

Front end

Unused by the library (it has no C6 transmit path). Evidence marked decompile in this section comes from C6 libphy.a phy_reg.o, phy_pbus.o, phy_init.o and phy_tx_cal.o, librftest.a rf_test.o, esp_phy_api.o, phy_test.o and wifi.o, and the C6 ROM.

How the vendor functions drive the front end:

  • esp_phy_wifi_tx_tone(start, chan, backoff) (wifiscwout()) tunes with chip_v7_set_chan(chan, 0) (above 2000 the argument is MHz, truncated to a channel: 2437 to 2441 all give channel 6), forces the analog TX chain on through pbus debug mode (ram_txcal_debuge_mode(): pbus_force_mode(1), ram_pbus_xpd_tx_on(), the calibrated DC offset, the TX clock), and starts tone 0 at frequency 0, attenuated by the calibrated value + backoff + 12 in quarter dB. It never calls force_txon(). Stopping runs ROM txcal_work_mode() (0x400047d6): tone off, TX clock off, pbus back to receive and to normal mode. On the bench the carrier arrives at the channel centre (measured bench 2026-10-11).

  • ram_stop_tx_tone() stops the tones and selects the data path, leaving the pbus forcing and the TX clock on: the C6 counterpart of the C5’s phy_stop_tx_tone(1). The DAC engine played with the source left at 0 (see “Dump and DAC engine”); with 3 it is untested.

  • force_txon(on) clears 0x600a70dc bit 16 and sets FE_RX_FORCE to 2, then FE_TX_FORCE to 3; off goes in reverse.

  • In force_txon_mode(on, bt, idx) the second argument, which the ESP32-S3 page calls mode, acts as a BT flag: the function always writes the power index force (FE_PWR_INDEX_FORCE = on, index in FE_PWR_INDEX), then calls force_txon(0) and force_txon(on); only with the flag it first adds 32 to the index, stops the hardware frequency engine (FE_HW_FREQ_STOP), writes 0x600af4b4 bit 0, FE_BT_FORCE and 0x600a28a0 bits 31:30 (all ones with on, 0 without), and restarts the engine if it ran before.

  • set_dump_mode(x) clears FE_R958_REG bits 1:0 and writes 0x600a70b8 bits 2:0 = 1 when x is 0, else 0 (C6 librftest.a phy_test.o).

Register FE_RC0_REG (0x600a00c0)

Hardware frequency engine stop

Bits

31:27

26

25:0

Field

(unknown)

FE_HW_FREQ_STOP

(unknown)

FE_HW_FREQ_STOP The hardware frequency engine stop: the BT branch of force_txon_mode() stops the engine (this bit) and restarts it afterwards if it ran before. (R/W)

Register FE_PWR_INDEX_REG (0x600a0410)

Power index force

Bits

31:24

23

22

21:16

15:0

Field

(unknown)

FE_PWR_INDEX_FORCE

(unknown)

FE_PWR_INDEX

(unknown)

FE_PWR_INDEX_FORCE Forces the power index FE_PWR_INDEX. force_txon_mode() sets it to its on argument, so force_txon_mode(0, ...) releases the force. (R/W)

FE_PWR_INDEX The forced power index: force_txon_mode(on, bt, idx) writes idx here, plus 32 with the BT flag. (R/W)

Evidence:

  • FE_PWR_INDEX_FORCE: decompile C6 libphy.a phy_reg.o (force_txon_mode() passes on != 0), C6 ROM phy_force_pwr_index (0x400057e2, bit 23 = its first argument)

Register FE_DAC_SRC_REG (0x600a0414)

DAC source select

Bits

31:2

1:0

Field

(unknown)

FE_DAC_SRC

FE_DAC_SRC DAC source select as the vendor uses it: ram_start_tx_tone_step() writes 0 with a tone and 3 without, ram_stop_tx_tone() writes 3; the ROM tone functions leave it alone. The DAC engine plays with 0 (see “Dump and DAC engine”); with 3 it is untested. (R/W)

Evidence:

  • FE_DAC_SRC: decompile; measured bench 2026-10-11

Register FE_TXDC_CAL_REG (0x600a0418)

TX DC calibration comparator

Bits

31:30

29

28

27:23

22

21:2

1

0

Field

(unknown)

FE_TXDC_CMP_ROW2

FE_TXDC_CMP_ROW3

(unknown)

FE_TXDC_CAL_DONE

(unknown)

FE_TXDC_CAL_EN

FE_TXDC_CAL_START

FE_TXDC_CMP_ROW2 Comparator output for pbus row 2 (presumably I). (RO)

FE_TXDC_CMP_ROW3 Comparator output for pbus row 3 (presumably Q). (RO)

FE_TXDC_CAL_DONE Calibration done. (RO)

FE_TXDC_CAL_EN Calibration enable. (R/W)

FE_TXDC_CAL_START Calibration start. (R/W)

Layout as libphy txdc_cal_new() drives it. The result goes into pbus rows 2 and 3 through rom_pbus_force_test(). The ROM txdc_cal() (0x4000761e) uses another layout on the same register and no C6 library calls it. Shell writes sometimes made a carrier, not reproducibly.

Evidence:

  • decompile libphy.a phy_tx_cal.o, C6 ROM; measured bench 2026-10-11

Register FE_TONE0_REG, FE_TONE1_REG (0x600a0420, 0x600a0424)

Tones 0 and 1: enable, attenuation, frequency

Bits

31:19

18

17:10

9:0

Field

(unknown)

FE_TONE_EN

FE_TONE_ATTEN

FE_TONE_FREQ

FE_TONE_EN Tone enable. (R/W)

FE_TONE_ATTEN The attenuation, negated. (R/W)

FE_TONE_FREQ The frequency >> 2. libphy ram_start_tx_tone_step() puts the frequency’s low two bits in FE_TONE_FREQ_LO_REG; the ROM version splits the frequency so only while FE_TONE0_REG bit 29 is set, else it writes it unshifted and leaves FE_TONE_FREQ_LO_REG alone. (R/W)

The vendor keeps bits 31:28 and writes 0 to bits 27:19. The vendor CW leaves tone 0 at 0x00068400 or 0x00068c00: frequency 0, a carrier at the LO.

Evidence:

  • decompile; measured bench 2026-10-11

Register FE_TONE_FREQ_LO_REG (0x600a042c)

Fine tone frequency: the low two bits of each tone’s frequency

Bits

31:4

3:2

1:0

Field

(unknown)

FE_TONE1_FREQ_LO

FE_TONE0_FREQ_LO

FE_TONE1_FREQ_LO Low two bits of tone 1’s frequency. (R/W)

FE_TONE0_FREQ_LO Low two bits of tone 0’s frequency. (R/W)

libphy ram_start_tx_tone_step() puts the frequency’s low two bits here. The ROM version does so only while FE_TONE0_REG bit 29 is set, else it writes the frequency unshifted and leaves this register alone.

Evidence:

  • decompile

Register FE_TXDC_REG (0x600a0444)

TX DC value

Bits

31:20

19:10

9:0

Field

(unknown)

FE_TXDC_Q

FE_TXDC_I

FE_TXDC_Q TX DC value, Q. (R/W)

FE_TXDC_I TX DC value, I. (R/W)

The C6 set_txdc_dig(i, q), the only code that writes the register, writes only FE_TXDC_I and FE_TXDC_Q; the identical C61 function writes 0x600a0c08, the register upstream’s first C5 code drove CPU-paced I/Q through (TXDC_REG, emit_iq()). Untested on the C6.

Evidence:

  • decompile C6 and C61 librftest.a phy_test.o; upstream git show e605684:main/c5_main.c:32, 211-238

Register FE_R44C_REG (0x600a044c)

Unknown; init sets bits 27 and 25 and clears bit 22

Bits

31:28

27

26

25

24:23

22

21:0

Field

(unknown)

FE_B27

(unknown)

FE_B25

(unknown)

FE_B22

(unknown)

FE_B27 Set at init. (R/W)

FE_B25 Set at init. (R/W)

FE_B22 Cleared at init. (R/W)

ROM fe_reg_init() and libphy reg_set_new() set bits 27 and 25 and clear bit 22 at init, at the step where the C61’s phy_fe_reg_init() sets bits 25 (IQ swap) and 26 (inject) of its TX DC register 0x600a0c08. That 25 and 27 are the C6’s IQ swap and inject bits is a guess.

Evidence:

  • decompile (the writes; C61 libphy.a phy_reg.o); speculative (the meaning)

Register FE_FORCE_REG (0x600a0910)

Receive, transmit and BT mode force pairs; receive and transmit clocks

Bits

31:18

17:16

15:14

13:12

11:10

9:8

7:0

Field

(unknown)

FE_TX_CLK

FE_RX_CLK

FE_BT_FORCE

FE_TX_FORCE

FE_RX_FORCE

(unknown)

FE_TX_CLK Transmit clock, 3 on, 0 off, as ROM set_txclk_en() (0x40005268) writes it. (R/W)

FE_RX_CLK Receive clock, 3 on, 0 off, as ROM set_rxclk_en() (0x4000528c) writes it. (R/W)

FE_BT_FORCE BT mode force pair: 3 from the BT branch of force_txon_mode(), txon_set(1) and bt_rx_force(). (R/W)

FE_TX_FORCE Transmit force pair: 3 from force_txon(1) and txon_set(1); 2 from force_txrx_off(1) and ROM phy_close_pa(1). (R/W)

FE_RX_FORCE Receive force pair: 2 from force_txon(1), txon_set(1) and force_txrx_off(1); 3 from bt_rx_force() and ROM fe_adc_on(). (R/W)

FE_RX_FORCE, FE_TX_FORCE and FE_BT_FORCE are force pairs, high bit force, low bit value: 0 released, 2 forced off, 3 forced on (inferred from all writers).

Evidence:

  • decompile

Register FE_R958_REG (0x600a0958)

Unknown; set_dump_mode() clears bits 1:0

Bits

31:2

1:0

Field

(unknown)

FE_B1_0

FE_B1_0 Cleared by set_dump_mode(x). (R/W)

Evidence:

  • FE_B1_0: C6 librftest.a phy_test.o

Analog I2C master

I2C_ANA_MST at 0x600af800 is in the HAL; the register names are the HAL’s.

Register I2C_ANA_MST_I2C0_CTRL_REG, I2C_ANA_MST_I2C1_CTRL_REG (0x600af800, 0x600af804)

Command word of master 0 and master 1

Bits

31:27

26

25

24

23:16

15:8

7:0

Field

(unknown)

I2C_ANA_MST_B26

I2C_ANA_MST_BUSY

I2C_ANA_MST_WRITE

I2C_ANA_MST_DATA

I2C_ANA_MST_REG_ADDR

I2C_ANA_MST_BLOCK

I2C_ANA_MST_B26 Unknown. The ROM sets it in every command word, as on the ESP32-S3; the HAL’s own access code does not set it. (R/W)

I2C_ANA_MST_BUSY Busy; polled after each command word. (RO)

I2C_ANA_MST_WRITE Set for a write, clear for a read. (R/W)

I2C_ANA_MST_DATA Data to write; after a read, the result. (R/W)

I2C_ANA_MST_REG_ADDR Register number in the block. (R/W)

I2C_ANA_MST_BLOCK Analog block id. (R/W)

The ROM uses the same command word as the ESP32-S3: one command word per access, then poll I2C_ANA_MST_BUSY; block, register and I2C_ANA_MST_B26 for a read (0x04000000 base), plus data and I2C_ANA_MST_WRITE for a write (0x05000000 base). The result is in I2C_ANA_MST_DATA.

Evidence:

  • modules/hal/espressif/components/soc/esp32c6/include/modem/i2c_ana_mst_reg.h:15-45; decompile C6 ROM 0x40003d88

  • I2C_ANA_MST_B26: modules/hal/espressif/components/esp_hal_regi2c/esp32c6/regi2c_impl.c:112-180

Register I2C_ANA_MST_ANA_CONF1_REG (0x600af81c)

Read mask

Bits

31:24

23:0

Field

(documented)

I2C_ANA_MST_ANA_CONF1

I2C_ANA_MST_ANA_CONF1 The read mask. (R/W)

Evidence:

  • modules/hal/espressif/components/soc/esp32c6/include/modem/i2c_ana_mst_reg.h:105-150

Register I2C_ANA_MST_ANA_CONF2_REG (0x600af820)

Master select

Bits

31:24

23:0

Field

(documented)

I2C_ANA_MST_ANA_CONF2

I2C_ANA_MST_ANA_CONF2 The master select. The ROM’s get_i2c_hostid() keeps the register’s bits outside 17:4 and ORs in 0x1fe00. (R/W)

Evidence:

  • modules/hal/espressif/components/soc/esp32c6/include/modem/i2c_ana_mst_reg.h:105-150

  • I2C_ANA_MST_ANA_CONF2: decompile C6 ROM 0x40003d4a

Analog I2C block 0x61 (ULP)

Registers 0 to 10 at 2412 MHz: 1f 02 08 49 53 40 51 2f 33 59 20 (bench 2026-10-09). The Init rows below come from this dump.

Register ULP_R4 (0x61:4)

HAL I2C_ULP_OCODE, function undocumented

Bits

7:0

Field

ULP_OCODE

Init

0x53

ULP_OCODE HAL name I2C_ULP_OCODE; the function is undocumented. Reads 0x4f instead of 0x53 after the native 802.15.4 radio ran in the previous firmware (see “Stale modem state”). (R/W)

Evidence:

  • ULP_OCODE: measured bench 2026-10-09; modules/hal/espressif/components/soc/esp32c6/include/soc/regi2c_lp_bias.h:45

Register ULP_R9 (0x61:9)

Unknown

Bits

7:0

Field

(unknown)

Init

0x59

Reads 0x5a in the same stale state as ULP_OCODE (see “Stale modem state”).

Evidence:

  • measured bench 2026-10-09

Analog I2C block 0x62 (RF PLL)

The RF PLL’s VCO and its calibration (the S3 has the same block). The library reads RFPLL_VCO_WINDOW after every tune and drives the LO 5/6 divider through RFPLL_LO_DIV. The capacitor code is 0x62:5 | 0x62:7[2] << 8 (9 bits; the vendor clamps to 0x1ff); a higher code means a lower VCO frequency.

Some field meanings come from the S3 vendor code, as the fields and their evidence say.

Upstream esp-sdr does a manual 512-code search on the ESP32-S2 through the same fields (RFPLL_MANUAL_CODE, RFPLL_MANUAL_CODE_B8, RFPLL_CODE_SRC, 0x62:0[7], and RFPLL_VCO_WINDOW as the window indication); it does not apply it to the S3 or the C6 (upstream modules/lib/esp-sdr/main/targets/esp32s2/pll.h:8-36).

Registers 0 to 18 at 2412 MHz: 68 bb 8d 61 28 bb 0d 03 80 f0 a0 44 00 00 14 74 07 ff 60 (bench 2026-10-09). The Init rows below come from this dump.

Register RFPLL_R0 (0x62:0)

VCO calibration start

Bits

7

6

5

4:0

Field

(unknown)

RFPLL_R0_B6

RFPLL_CAL_START

(unknown)

Init

0

1

1

0x8

RFPLL_R0_B6 Unknown; toggled around the calibration start (see RFPLL_CAL_START). (R/W)

RFPLL_CAL_START restart_cal() starts the VCO calibration with a rising edge on this bit while RFPLL_R0_B6 is toggled: the C6 clears RFPLL_R0_B6, pulses this bit and sets RFPLL_R0_B6; the S3 sets it first and clears it after. (WO)

Evidence:

  • decompile C6 ROM restart_cal (0x4000587e), S3 phy_rfpll.o; measured C6 dump

Register RFPLL_R1 (0x62:1)

Manual capacitor code, bits 7:0

Bits

7:0

Field

RFPLL_MANUAL_CODE

Init

0xbb

RFPLL_MANUAL_CODE Bits 7:0 of a manual capacitor code. The meaning comes from the S3 vendor code, where write_pll_cap() writes the code here. After a tune it holds the low byte of the code in use (on the C6 it reads as RFPLL_R5). (R/W)

Init: 0xbb, the same as RFPLL_R5 (see also “Stale modem state”)

Evidence:

  • RFPLL_MANUAL_CODE: decompile S3 phy_rfpll.o; measured C6 dump

Register RFPLL_R2 (0x62:2)

Manual code bit 8 and a 4-bit code override

Bits

7

6:5

4

3:0

Field

RFPLL_CODE4_OVR

(unknown)

RFPLL_MANUAL_CODE_B8

RFPLL_CODE4_VAL

Init

1

0x0

0

0xd

RFPLL_CODE4_OVR Cleared before and set after the vendor copies the calibrated 4-bit code into RFPLL_CODE4_VAL (see below). (R/W)

RFPLL_MANUAL_CODE_B8 Bit 8 of the manual capacitor code, written with RFPLL_MANUAL_CODE. The meaning comes from the S3 vendor code. (R/W)

RFPLL_CODE4_VAL The 4-bit code of the override, copied from RFPLL_CODE4_CAL (see below). (R/W)

Init: 0x8d, read with RFPLL_R6 = 0x0d

The 4-bit code override: the S3’s get_rf_freq_init() clears RFPLL_CODE4_OVR, calibrates at 2437 MHz, copies RFPLL_CODE4_CAL into RFPLL_CODE4_VAL and sets RFPLL_CODE4_OVR. What the 4-bit code controls is unknown.

Evidence:

  • decompile S3 phy_hw_freq.o (override sequence); measured C6 dump

  • RFPLL_MANUAL_CODE_B8: decompile S3 phy_rfpll.o

Register RFPLL_R4 (0x62:4)

Unknown

Bits

7:0

Field

(unknown)

Init

0x28

Init: 0x28 (40) at every PLL frequency read, 2280 to 2438 MHz

Evidence:

  • measured bench 2026-10-09

Register RFPLL_R5 (0x62:5)

Capacitor code readback, bits 7:0

Bits

7:0

Field

RFPLL_CAP_CODE

Init

0xbb

RFPLL_CAP_CODE Bits 7:0 of the capacitor code; bit 8 is RFPLL_CAP_CODE_B8. Values: see “Measured PLL behaviour”. (RO)

Evidence:

  • RFPLL_CAP_CODE: decompile S3 phy_rfpll.o; measured

Register RFPLL_R6 (0x62:6)

Calibrated 4-bit code

Bits

7:4

3:0

Field

(unknown)

RFPLL_CODE4_CAL

Init

0x0

0xd

RFPLL_CODE4_CAL The calibrated 4-bit code. The vendor copies it into RFPLL_CODE4_VAL (see RFPLL_R2). (RO)

Evidence:

  • RFPLL_CODE4_CAL: decompile as RFPLL_R2

Register RFPLL_R7 (0x62:7)

Capacitor code bit 8 and calibration done

Bits

7:3

2

1

0

Field

(unknown)

RFPLL_CAP_CODE_B8

RFPLL_CAL_DONE

(unknown)

Init

0x0

0

1

1

RFPLL_CAP_CODE_B8 Bit 8 of the capacitor code readback (see RFPLL_CAP_CODE). (RO)

RFPLL_CAL_DONE Calibration done; 1 after every tune. The S3’s wait_rfpll_cal_end() polls it every 20 us, 100 times, then prints error: pll_cal exceeds 2ms!!!. (RO)

Evidence:

  • RFPLL_CAP_CODE_B8: decompile S3 phy_rfpll.o; measured

  • RFPLL_CAL_DONE: decompile S3 phy_rfpll.o; measured

Register RFPLL_R11 (0x62:11)

Capacitor code source

Bits

7

6

5:0

Field

(unknown)

RFPLL_CODE_SRC

(unknown)

Init

0

1

0x4

RFPLL_CODE_SRC Cleared before each software calibration (set_rfpll_freq()), set by rfpll_cap_init_cal() before it tries manual codes. Inferred: 0 = code from the calibration engine, 1 = code from RFPLL_MANUAL_CODE and RFPLL_MANUAL_CODE_B8. (R/W)

Evidence:

  • RFPLL_CODE_SRC: decompile C6 ROM set_rfpll_freq and rfpll_cap_init_cal, S3 phy_rfpll.o

Register RFPLL_R12 (0x62:12)

VCO window comparator and two unknown status bits

Bits

7:4

3:2

1

0

Field

(unknown)

RFPLL_VCO_WINDOW

RFPLL_R12_B1

RFPLL_R12_B0

Init

0x0

0x0

0

0

RFPLL_VCO_WINDOW After calibration: 0 = the VCO inside its tuning window; 1 = target below the window (code pinned at its maximum); 2 = target above it (code 0). Live for the current code: the S3 vendor’s rfpll_cap_correct() moves the code in steps of 2, up while it reads 1 and down while it reads 2. esp_sdr_set_freq() and esp_sdr_set_freq_offset() return -ERANGE when it is not 0. Window (PLL): 2140 to 2884 MHz. (RO)

RFPLL_R12_B1 Unknown; set from about 2700 MHz (PLL) up, on both chips. (RO)

RFPLL_R12_B0 Unknown; set below about 2412 MHz. (RO)

Evidence:

  • RFPLL_VCO_WINDOW: measured bench 2026-10-09; working lib/esp_sdr/src/esp_sdr.c:78-97, 194-196, 213-215; decompile S3 phy_rfpll.o

  • RFPLL_R12_B1: measured bench 2026-10-09

  • RFPLL_R12_B0: measured bench 2026-10-09

Register RFPLL_R14 (0x62:14)

Charge pump calibration done and result

Bits

7

6:5

4:0

Field

RFPLL_CHGP_CAL_DONE

(unknown)

RFPLL_CHGP_CAL_RESULT

Init

0

0x0

0x14

RFPLL_CHGP_CAL_DONE Charge pump calibration done (see RFPLL_R15). (RO)

RFPLL_CHGP_CAL_RESULT Charge pump calibration result (see RFPLL_R15). (RO)

Evidence:

  • decompile C6 libphy.a phy_rfpll.o (rfpll_chgp_cal)

Register RFPLL_R15 (0x62:15)

Charge pump calibration

Bits

7

6

5

4:0

Field

(unknown)

RFPLL_R15_B6

RFPLL_R15_B5

RFPLL_CHGP_CODE

Init

0

1

1

0x14

RFPLL_R15_B6 Cleared before the charge pump calibration and set after it (see below). (R/W)

RFPLL_R15_B5 Pulsed before RFPLL_CHGP_CAL_DONE is polled (see below). (WO)

RFPLL_CHGP_CODE Written from the calibration result, min(31, 7r / 6 + 9) (see below). (R/W)

rfpll_chgp_cal() clears RFPLL_R15_B6, pulses RFPLL_R15_B5, polls RFPLL_CHGP_CAL_DONE (20 us, 100 tries), reads r = RFPLL_CHGP_CAL_RESULT, sets RFPLL_R15_B6 and writes RFPLL_CHGP_CODE = min(31, 7r / 6 + 9). The C6 read RFPLL_R14 = 0x14 and RFPLL_R15 = 0x74 at 2412 MHz, which this formula does not produce, so the reading is incomplete or RFPLL_R14 changes later.

Evidence:

  • decompile C6 libphy.a phy_rfpll.o (rfpll_chgp_cal); measured C6 dump

Register RFPLL_R16 (0x62:16)

LO 5/6 divider select

Bits

7:4

3

2:0

Field

(unknown)

RFPLL_LO_DIV

(unknown)

Init

0x0

0

0x7

RFPLL_LO_DIV 0: the LO is the PLL. 1: LO = PLL / 1.2 (5/6 conversion). One selector feeds both the receive and the transmit mixer. Mask 0x08; the S3 has the same selector in block 0x65, which the C6 does not have. (R/W)

  • Change only the bit (read-modify-write). Clear it before the vendor tuning call, which calibrates in normal mode; set it after the receive front-end setup.

  • The library uses the divider for every frequency below 2210 MHz (PLL at 1.2 times the LO), upstream only from 1842 MHz.

  • A scan of the C6 blob objects found no access to 0x62:16.

  • Upstream qualifies the selector on the ESP32, S2, S3, C2, C3 and C6 and warns that the C5, C61, H2 and S31 lay out their analog blocks differently.

Evidence:

  • working lib/esp_sdr/src/esp_sdr.c:106-108, lib/esp_sdr/src/esp_sdr_rx.c:92-95 (when to clear and set)

  • lib/esp_sdr/src/esp_sdr.c:65-76, modules/lib/esp-sdr/main/common/rx_lo.h:15-19 (divider band)

  • decompile, objdump (no access in the C6 blobs)

  • modules/lib/esp-sdr/docs/rx-controls.md:102-107 (qualified chips)

  • RFPLL_LO_DIV: upstream modules/lib/esp-sdr/main/common/rx_lo.h:47-62, external-tone tests modules/lib/esp-sdr/docs/rx-controls.md:76-100; measured on air for receive and transmit (bench 2026-10-09)

Register RFPLL_R17 (0x62:17)

Unknown

Bits

7:0

Field

(unknown)

Init

0xff

Init: 0xff at 2412 MHz; 0x7e at 2413 MHz

Evidence:

  • measured C6 dump

Register RFPLL_R18 (0x62:18)

Unknown

Bits

7:0

Field

(unknown)

Init

0x60

Evidence:

  • measured C6 dump

Analog I2C block 0x63 (RF PLL fractional-N divider)

Registers 0 and 3 to 6:

f_PLL = 1.5 * f_xtal * (32 + r3 + (r4 * 2^11 + r5 * 2^3 + r6[2:0]) / 2^19)

That is 60 MHz per integer step with the 40 MHz crystal. The vendor computes a word W = r3 << 19 | r4 << 11 | r5 << 3 | r6 with f_PLL = 3 * f_xtal * (16 + W / 2^20); W bit 27 is computed but not written. write_rfpll_sdm() clears RFPLL_SDM_R0_B3 (0x63:0[3]), writes registers 3 to 6 and sets the bit again.

PLL MHz

reg 3

reg 4

reg 5

reg 6

Note

2412

0x08

0x33

0x33

0x01

2413

0x08

0x37

0x77

0x03

2417

0x08

0x48

0x88

0x04

2437

0x08

0x9d

0xdd

0x06

2438

0x08

0xa2

0x22

0x01

2280

0x06

0x00

0x00

0x00

LO 1900 MHz through the divider

2404

0x08

0x11

0x11

0x00

asked for 2412 MHz in the stale modem state

Evidence: measured bench 2026-10-09; decompile C6 ROM write_rfpll_sdm (0x400058ee), C6 libphy.a phy_rfpll.o (ram_rfpll_set_freq).

The bench reading 60 MHz * (N + F / 2^20) with N = 32 + reg 3 and F = r4 << 12 | r5 << 4 | r6 is the same formula except that the vendor code weights reg 6 twice as much (it holds W bits 2:0), a difference below 0.2 kHz. The bench reading also took reg 3 as the low 4 bits of N; the vendor code puts 8 bits there (N - 32), which agrees for every value seen.

The crystal selector of the vendor calls picks the divisor, as on the S3: 1 = 26 MHz, 2 = 32 MHz, anything else = 40 MHz (decompile C6 ram_rfpll_set_freq).

The Init rows below are the values at 2412 MHz.

Register RFPLL_SDM_R0 (0x63:0)

Bit 3 cleared and set around a divider write; other bits unknown

Bits

7:4

3

2:0

Field

(unknown)

RFPLL_SDM_R0_B3

(unknown)

Init

0x5

1

0x3

RFPLL_SDM_R0_B3 write_rfpll_sdm() clears it, writes RFPLL_SDM_R3 to RFPLL_SDM_R6 and sets it again. (R/W)

Evidence:

  • measured bench 2026-10-09

  • RFPLL_SDM_R0_B3: decompile C6 ROM write_rfpll_sdm (0x400058ee)

Register RFPLL_SDM_R1 (0x63:1)

Unknown

Bits

7:0

Field

(unknown)

Init

0xab

Evidence:

  • measured bench 2026-10-09

Register RFPLL_SDM_R3 (0x63:3)

Divider integer part

Bits

7:0

Field

RFPLL_SDM_INTEGER

Init

0x8

RFPLL_SDM_INTEGER r3 in the formula: N - 32. The vendor code puts 8 bits here. (R/W)

Register RFPLL_SDM_R4 (0x63:4)

Divider fraction, high byte

Bits

7:0

Field

RFPLL_SDM_FRAC_H

Init

0x33

RFPLL_SDM_FRAC_H r4 in the formula. (R/W)

Register RFPLL_SDM_R5 (0x63:5)

Divider fraction, middle byte

Bits

7:0

Field

RFPLL_SDM_FRAC_M

Init

0x33

RFPLL_SDM_FRAC_M r5 in the formula. (R/W)

Register RFPLL_SDM_R6 (0x63:6)

Divider fraction, low 3 bits

Bits

7:3

2:0

Field

(unknown)

RFPLL_SDM_FRAC_L

Init

0x0

0x1

RFPLL_SDM_FRAC_L r6[2:0] in the formula: W bits 2:0. (R/W)

Analog I2C block 0x66 (BBPLL)

Documented by the HAL; esp_sdr does not touch the block. Upstream looked for other ADC rates (on the S3 through this block); for the C6 it reports that no reliable lower-rate path was found (upstream modules/lib/esp-sdr/docs/rx-controls.md:59-61).

Registers 0 to 10 at 2412 MHz: 18 25 50 08 6b 80 73 00 c2 96 03 (bench 2026-10-09).

Analog I2C block 0x67 (BBTOP, analog baseband)

The C6 libphy groups the filter codes as the S3 does (filter_dcap_set(): registers 4, 5, 12, 13 from one PHY parameter, 6, 7, 14, 15 from the next), so the transmit pairs are probably at the same place as on the S3 (decompile C6 libphy.a phy_i2c.o). esp_sdr_tx_set_lpf() returns -ENOTSUP on the C6. esp_sdr_bbtop_read() and esp_sdr_bbtop_write() give unchecked access (lib/esp_sdr/src/esp_sdr_rx.c:648-667).

Register BBTOP_R4, BBTOP_R5 (0x67:4, 0x67:5)

Receive low-pass capacitor code, I (BBTOP_R4) and Q (BBTOP_R5)

Bits

7:6

5:0

Field

(unknown)

BBTOP_RX_LPF

BBTOP_RX_LPF Values 0 to 63; larger is narrower, 54 to 12 MHz on the C6. 0 is the widest setting, not a bypass. (R/W)

Bits 7:6 are kept.

Evidence:

  • BBTOP_RX_LPF: working lib/esp_sdr/src/esp_sdr_rx.c:126-148; upstream curves modules/lib/esp-sdr/main/common/rx_bandwidth.h:79-84

Register BBTOP_R12, BBTOP_R13 (0x67:12, 0x67:13)

Probably the transmit low-pass pair A, as on the S3

Bits

7:0

Field

(unknown)

filter_dcap_set() writes them from the same PHY parameter as BBTOP_R4 and BBTOP_R5 (see the introduction).

Evidence:

  • decompile C6 libphy.a phy_i2c.o

Register BBTOP_R14, BBTOP_R15 (0x67:14, 0x67:15)

Probably the transmit low-pass pair B, as on the S3

Bits

7:0

Field

(unknown)

filter_dcap_set() writes them from the same PHY parameter as registers 6 and 7 (see the introduction).

Evidence:

  • decompile C6 libphy.a phy_i2c.o

Analog I2C block 0x6a (bias)

The HAL names the block (I2C_BIAS). Registers 0 to 3 at 2412 MHz read cf 7c 1a 07 (bench 2026-10-09).

Sample word formats

Word

Bits 9:0

Bits 19:10

Bits 31:20

Evidence

receive

Q (assumed)

I (assumed)

the same helpers as on the S3 (include/esp_sdr/esp_sdr_rx.h:209-225); not checked against a tone

transmit, played by the DAC engine

I

Q

0

measured bench 2026-10-11 (tone side and image)

  • Each field is 10-bit two’s complement, -512 to 511.

  • Packed forms: IQ8 is bits 19:12 then 9:2 (I then Q); IQ10 packs bits 19:0 of two words into 5 bytes (lib/esp_sdr/src/esp_sdr_rx.c:548-576).

Vendor library functions as used

Function

Library

Called as

What it does

Evidence

chip_v7_set_chan(mhz, mode)

libphy

(channel ? mhz : 2412, cbw)

Channel setup; converts MHz through mhz2ieee and loses off-grid requests, hence the calibration channel plus phy_set_freq().

working lib/esp_sdr/src/esp_sdr.c:114-120; upstream modules/lib/esp-sdr/main/targets/esp32c6/chip.h:12-19

phy_set_freq(mhz, khz)

libphy

(PLL MHz, kHz + offset)

Stores khz at phy_param + 0x20 and calls set_rf_freq_offset(phy_param[81], mhz, khz) (crystal selector from the PHY).

decompile C6 libphy.a phy_rfpll.o

ram_stop_tx_tone, pbus_workmode, rom_pbus_xpd_tx_off, ram_pbus_xpd_rx_on, set_rxclk_en

libphy, ROM

RX: stop_tx_tone(1), workmode(), xpd_tx_off(), xpd_rx_on(1), set_rxclk_en(1)

Forces the analog chain and the receive clock on with the MAC idle, so the engine sees live ADC data (sequence from upstream prepare_rx()). Settle 3 ms after a retune.

working lib/esp_sdr/src/esp_sdr_rx.c:78-98; upstream modules/lib/esp-sdr/main/targets/esp32s3/receiver.c:92-106; measured hardware-quirks.md

force_rx_gain(force, index, bt)

librftest

(manual, index, 0); index 40 while the AGC is in charge

See AGC_FORCE_REG.

lib/esp_sdr/src/esp_sdr_rx.c:41-54

esp_wifi_set_ps(), esp_wifi_set_promiscuous(), esp_wifi_set_channel()

Wi-Fi driver

WIFI_PS_NONE; true; (1, WIFI_SECOND_CHAN_NONE)

The engine and the PHY calls need the Wi-Fi driver up. NULL mode accepts a channel only in promiscuous mode.

working lib/esp_sdr/src/esp_sdr.c:153-166

For reading the decompile: the C6 libphy calls ROM helpers through the function table g_phyFuns, slots 0x50 i2c_readReg(block, host, reg), 0x58 i2c_writeReg(block, host, reg, data), 0x5c i2c_readReg_Mask(block, host, reg, msb, lsb) and 0x60 i2c_writeReg_Mask(block, host, reg, msb, lsb, data). Evidence: decompile C6 libphy.a phy_rfpll.o.

Measured PLL behaviour

Bench 2026-10-09, frequencies are the PLL’s (the LO is PLL / 1.2 behind the divider). Code is the capacitor code (0x62:5 plus 256 times 0x62:7[2]), reg 12 the raw 0x62:12.

PLL MHz

Code

reg 12

2110 to 2138

351

0x05 (below the window)

2140

351

0x01

2412

189 (187 in another run)

0x00

2413

187

2437

177

2600

102

0x00

2800

29

0x02

2884

3

0x02

2888 to 2900

erratic: 324, 430, 337, 321

0x09

2904

0

0x02

2908 and up

0

0x0a (above the window)

VCO window, 0x62:12[3:2] = 0 (PLL)

2140 to 2884 MHz

PLL locks on air

2128 to at least 2856 MHz (limit of the S3 transmitter)

LO without -ERANGE

1784 to 2884 MHz

LO on air

1774 to at least 2856 MHz

  • Below the window the calibration pins the code at 351, above it at 0; 430, seen in the erratic band, shows that all 9 bits exist. Repeated calibrations at one frequency land a few codes apart.

  • The window sits about 12 MHz inside the lock limits, so -ERANGE errs on the safe side (include/esp_sdr/esp_sdr.h:31-38, 110-125, README.rst, “The library”).

  • The limits are the same with and without the divider, so they are VCO limits, per chip and untested over temperature. Below 2210 MHz the library always uses the divider, which reaches the VCO’s lower limit / 1.2 (lib/esp_sdr/src/esp_sdr.c:65-76; tests/regression/lo_tuning guards it, doc/testing.md, “lo_tuning”).

  • 0x62:12[1] is set from about 2700 MHz up, 0x62:12[0] below about 2412 MHz; their meaning is unknown.

Stale modem state

After the native 802.15.4 radio ran (apps/wpan) and the chip was only reset, not power cycled:

Item

Clean

Stale

PLL for 2412 MHz through the vendor channel path

2412 MHz, 0x63 regs 3 to 6 08 33 33 01

about 2404 MHz, 08 11 11 00

0x62 regs 1 and 5 (code) at 2412 MHz

0xbb (187)

0xbf (191)

0x61:4 (I2C_ULP_OCODE)

0x53

0x4f

0x61:9

0x59

0x5a

A power cycle or a deep sleep clears it, and so does the modem reset at PRE_KERNEL_1 (see MODEM_SYSCON_MODEM_RST_CONF_REG). Evidence: measured bench 2026-10-09; the behaviour is in hardware-quirks.md.