Skip to content

fix(adc): calibration, efuse, and register-sequence bugs across all chips#5589

Draft
MabezDev wants to merge 2 commits into
esp-rs:mainfrom
MabezDev:adc-fixes
Draft

fix(adc): calibration, efuse, and register-sequence bugs across all chips#5589
MabezDev wants to merge 2 commits into
esp-rs:mainfrom
MabezDev:adc-fixes

Conversation

@MabezDev
Copy link
Copy Markdown
Member

Side-by-side diff of esp-hal's ADC against ESP-IDF (components/esp_adc, components/hal, components/efuse). Each item is independently contributing to wrong/garbage readings; none of them depend on each other.

Calibration

  • C6 0 dB curve K0 coefficient was 10x too small (stale IDF revision)
  • S3 calibration wrote the cal attenuation to a non-existent atten slot (channel 15). The RTC controller falls back to channel 0's atten when all channels are disabled, so write there
  • RISC-V calibration sent the channel-15 sentinel through config_onetime_sample, which on ADC1 wrote 0x7 instead of the required 0xF cal-mux value. Split into a dedicated config_cal_sample

Efuse parsing

  • C2 atten-11dB cal voltage: sign bit read from the wrong variable and branches inverted
  • H2 cal_code never sign-extended at bit 9 (~500 digi-counts of gain error when set); also missing version checks on cal_code and init_code

Register sequencing

  • C3 ADC2 wrote channel to onetime_channel instead of (adc_n << 3) | channel — every ADC2 read sampled an ADC1 pad. Also clears the other unit's onetime bit
  • start_onetime_sample had no rising edge; replaced with clear -> (C6 delay) -> set, drops the old broken double-start workaround
  • S2/S3 read_data returned the 16-bit field unmasked, exposing arbiter/validity flag bits
  • ESP32 ADC2's switch to RTC controller was missing sar2_pwdet_force = 0 and SYSCON.saradc_ctrl.sar2_mux = 1; without these, after Wi-Fi/BT init ADC2 stayed muxed to the power-detect path

HIL test

DAC→ADC internal-loopback test on esp32 / esp32s2 (the only chips with both peripherals). Each DAC channel shares its pad with an ADC2 channel, so no wiring needed. Verifies zero/max/midpoint/sweep/cross-channel independence. Thresholds expressed as fractions of ADC_MAX so the same test scales between ESP32's 12-bit and S2's 13-bit output.

Future work

Deliberately out of scope here; tracked for follow-ups:

  • Runtime self-calibration averages 32 raw samples; ESP-IDF does a binary search per iteration to find the init code that drives GND to zero. Current code is approximately correct but noisier.
  • ADC2 arbiter is never configured on C3 or S3 — works in isolation, but Wi-Fi can yank ADC2 mid-sample.
  • No meas_status busy-wait before SAR1 start on ESP32 / S2 / S3 — race that can return stale data on tight read loops.
  • C6 / H2 V2 calibration silicon is misread as V1 (different efuse offsets and coefficient tables).
  • ESP32 has no line-fit (Vref / Two-Point) calibration support; values come back raw.
  • ESP32-S2 has no efuse calibration support at all.

MabezDev added 2 commits May 20, 2026 17:39
Found by comparing the esp-hal ADC driver against ESP-IDF's
components/esp_adc + components/hal + components/efuse. All independently
contribute to wrong/garbage readings; none of them needed each other.

Calibration:
- C6 0 dB curve K0 coefficient was 10x too small (-0.0487... vs
  -0.487166...). Stale IDF revision; the upstream divisor changed to 1e15
  for that one term.
- S3 calibration was writing the cal attenuation to a non-existent atten
  slot (channel 15). The RTC controller falls back to channel 0's atten
  when all channels are disabled, so write there instead — matches
  ESP-IDF's `cal_setup`.
- RISC-V calibration was passing the channel-15 sentinel through
  `config_onetime_sample`, which on ADC1 wrote `0x7` instead of the
  required `0xF` cal-mux value. Split into a dedicated
  `config_cal_sample`.

Efuse parsing:
- C2 atten-11dB cal voltage read the sign bit from the wrong variable and
  had inverted branches.
- H2 cal_code was never sign-extended at bit 9 (~500 digi-counts of gain
  error when that bit was set) and skipped its version check.
- H2 init_code's version check was effectively a no-op (`version > 4`
  with version only ever 0/1).

Register sequencing:
- C3 ADC2 wrote `channel` to `onetime_channel` instead of
  `(adc_n << 3) | channel`, so every ADC2 read sampled an ADC1 pad. Now
  also clears the other unit's onetime bit so alternating ADC1/ADC2 reads
  don't leave both units enabled.
- `start_onetime_sample` had no rising edge — it called `set_bit` on a
  bit that was usually already set. Replaced with a proper
  clear -> (C6 delay) -> set, dropping the old broken double-start
  workaround in `read_oneshot`.
- S2/S3 `read_data` returned the 16-bit field unmasked, exposing the
  ADC2 arbiter/validity flag bits as part of the result.
- ESP32 ADC2's switch to RTC controller was missing
  `sar2_pwdet_force = 0` and `SYSCON.saradc_ctrl.sar2_mux = 1`; without
  these, after any Wi-Fi/BT init ADC2 stayed muxed to the power-detect
  path.

Also renamed several inline bitmasks to named constants
(`ADC_VAL_MASK`, `ONETIME_CHANNEL_MASK`, `ONETIME_UNIT2_BIT`,
`ONETIME_CAL_CHANNEL`, `CAL_ATTEN_CHANNEL`) and added a `disable_channels`
helper on xtensa to replace the `1 << 15`-mask-truncation trick.
Each DAC channel on ESP32 and ESP32-S2 lives on the same pad as one of
the ADC2 channels (ESP32: GPIO25/26 -> ADC2_CH8/9; ESP32-S2: GPIO17/18
-> ADC2_CH6/7). This test uses that internal wiring as a no-rig signal
source: write known DAC codes, sample the same pad with the ADC, and
assert that the reading is in the right ballpark.

Cases:
- DAC=0 reads in the bottom 10% of full scale
- DAC=255 reads in the top 20%
- Midpoint (DAC=128) lands in the middle half
- Full sweep is monotonic (modulo ~1.2% noise band) and spans >70% FS
- Cross-channel independence: driving one DAC must not affect the
  other pad. Pre-fix on RISC-V the ADC2 channel encoding bug would
  have failed this; covers the same class of bug on Xtensa.

Thresholds are all fractions of ADC_MAX because the resolution differs
per chip: ESP32 returns 12-bit (default of its configurable Resolution),
ESP32-S2 returns 13-bit unconditionally.

Drive-by: ungate CAL_ATTEN_CHANNEL in esp-hal/src/analog/adc/xtensa.rs
so the generic adc_calibrate body still name-resolves on ESP32-S2 (it
has no CalibrationAccess impl, but the body is still typechecked).
@MabezDev
Copy link
Copy Markdown
Member Author

Need more setup for the HIL tests to work, will iterate locally in the meantime.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant