Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions esp-hal/src/analog/adc/calibration/curve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,11 +198,11 @@ mod impls {
];


/// Error curve coefficients derived from <https://github.com/espressif/esp-idf/blob/903af13e8/components/esp_adc/esp32c6/curve_fitting_coefficients.c>
/// Error curve coefficients derived from <https://github.com/espressif/esp-idf/blob/master/components/esp_adc/esp32c6/curve_fitting_coefficients.c>
#[cfg(esp32c6)]
CURVES_COEFFS1 [
_0dB => [
-0.0487166399931449,
-0.487166399931449,
0.0006436483033201,
0.0000030410131806,
],
Expand Down
17 changes: 13 additions & 4 deletions esp-hal/src/analog/adc/esp32.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use core::{

use super::{AdcConfig, Attenuation};
use crate::{
peripherals::{ADC1, ADC2, SENS},
peripherals::{ADC1, ADC2, APB_CTRL, SENS},
private,
};

Expand Down Expand Up @@ -175,9 +175,18 @@ impl RegisterAccess for ADC2<'_> {
}

fn clear_dig_force() {
SENS::regs()
.sar_read_ctrl2()
.modify(|_, w| w.sar2_dig_force().clear_bit());
// Switch ADC2 to RTC-controller mode. ESP-IDF's `adc_ll_set_controller` for
// ADC2 RTC also clears `sar2_pwdet_force` and sets `SYSCON.saradc_ctrl.sar2_mux`
// — without these, ADC2 stays muxed to the power-detect path (especially after
// Wi-Fi/BT initialization) and every conversion returns the PWDET output
// instead of the requested channel.
SENS::regs().sar_read_ctrl2().modify(|_, w| {
w.sar2_dig_force().clear_bit();
w.sar2_pwdet_force().clear_bit()
});
APB_CTRL::regs()
.apb_saradc_ctrl()
.modify(|_, w| w.saradc_sar2_mux().set_bit());
}

fn set_start_force() {
Expand Down
96 changes: 80 additions & 16 deletions esp-hal/src/analog/adc/riscv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,32 @@ mod calibration;
// https://github.com/espressif/esp-idf/blob/903af13e8/components/soc/esp32h2/include/soc/regi2c_saradc.h
cfg_if::cfg_if! {
if #[cfg(adc_adc1)] {
/// Mask for the 12-bit conversion result returned by the ADC.
const ADC_VAL_MASK: u16 = 0xfff;
/// Number of samples averaged during runtime self-calibration.
const ADC_CAL_CNT_MAX: u16 = 32;
/// Sentinel passed to [`CalibrationAccess::ADC_CAL_CHANNEL`] from the shared
/// driver. The RISC-V hardware doesn't use this as a physical channel number;
/// instead, calibration writes [`ONETIME_CAL_CHANNEL`] to the `onetime_channel`
/// field and the calibration attenuation to channel 0's atten slot.
const ADC_CAL_CHANNEL: u16 = 15;
}
}

// Encoding of the `APB_SARADC.onetime_sample.onetime_channel` field (4 bits):
// bit 3 – ADC unit selector (clear = ADC1, set = ADC2)
// bits 2:0 – channel index for the selected unit (or 0b111 for the cal mux)
// See `adc_oneshot_ll_set_channel` / `adc_oneshot_ll_disable_channel` in
// `components/hal/<chip>/include/hal/adc_ll.h` — both encode `(adc_n << 3) | x`.
const ONETIME_CHANNEL_MASK: u8 = 0b0111;
#[cfg(adc_adc2)]
const ONETIME_UNIT2_BIT: u8 = 0b1000;
/// Value written to `onetime_channel` when starting a calibration conversion.
/// Matches ESP-IDF's `adc_oneshot_ll_disable_channel`, which writes
/// `(adc_n << 3) | 0xF` — the OR collapses to `0xF` regardless of unit because
/// `0xF` already has bit 3 set. This selects the on-chip calibration mux.
const ONETIME_CAL_CHANNEL: u8 = 0x0F;

// The number of analog IO pins, and in turn the number of attentuations,
// depends on which chip is being used
cfg_if::cfg_if! {
Expand Down Expand Up @@ -74,8 +94,10 @@ where

ADCX::enable_vdef(true);

// Start sampling
ADCX::config_onetime_sample(ADC_CAL_CHANNEL as u8, atten as u8);
// Route the SAR to the on-chip calibration mux instead of an external pad.
// The cal mux internally connects to either GND or Vref depending on the
// value programmed by `connect_cal` below.
ADCX::config_cal_sample(atten as u8);

// Connect calibration source
ADCX::connect_cal(source, true);
Expand Down Expand Up @@ -113,6 +135,12 @@ pub trait RegisterAccess {
/// Configure onetime sampling parameters
fn config_onetime_sample(channel: u8, attenuation: u8);

/// Configure onetime sampling to read from the on-chip calibration mux
/// instead of an external pad. Equivalent to ESP-IDF's
/// `adc_oneshot_ll_disable_channel` + `adc_oneshot_ll_set_atten(unit, 0, atten)`
/// pair in `cal_setup`.
fn config_cal_sample(attenuation: u8);

/// Start onetime sampling
fn start_onetime_sample();

Expand All @@ -136,14 +164,39 @@ pub trait RegisterAccess {
impl RegisterAccess for crate::peripherals::ADC1<'_> {
fn config_onetime_sample(channel: u8, attenuation: u8) {
APB_SARADC::regs().onetime_sample().modify(|_, w| unsafe {
// Make sure only ADC1's onetime-sample bit is set; alternating ADC1/ADC2
// reads must not leave both units enabled.
#[cfg(adc_adc2)]
w.saradc2_onetime_sample().clear_bit();
w.saradc1_onetime_sample().set_bit();
w.onetime_channel()
.bits(channel & ONETIME_CHANNEL_MASK);
w.onetime_atten().bits(attenuation)
});
}

fn config_cal_sample(attenuation: u8) {
APB_SARADC::regs().onetime_sample().modify(|_, w| unsafe {
#[cfg(adc_adc2)]
w.saradc2_onetime_sample().clear_bit();
w.saradc1_onetime_sample().set_bit();
w.onetime_channel().bits(channel);
w.onetime_channel().bits(ONETIME_CAL_CHANNEL);
w.onetime_atten().bits(attenuation)
});
}

fn start_onetime_sample() {
APB_SARADC::regs()
// The hardware requires a 0->1 transition of onetime_start that the ADC
// digital controller can capture on its (slower) clock. ESP-IDF holds the
// bit low for >= 3 ADC controller cycles before the rising edge — see
// `adc_hal_onetime_start` in adc_oneshot_hal.c.
let saradc = APB_SARADC::regs();
saradc
.onetime_sample()
.modify(|_, w| w.onetime_start().clear_bit());
#[cfg(esp32c6)]
crate::rom::ets_delay_us(5);
saradc
.onetime_sample()
.modify(|_, w| w.onetime_start().set_bit());
}
Expand All @@ -158,7 +211,7 @@ impl RegisterAccess for crate::peripherals::ADC1<'_> {
.read()
.saradc1_data()
.bits() as u16
& 0xfff
& ADC_VAL_MASK
}

fn reset() {
Expand Down Expand Up @@ -231,14 +284,33 @@ impl super::CalibrationAccess for crate::peripherals::ADC1<'_> {
impl RegisterAccess for crate::peripherals::ADC2<'_> {
fn config_onetime_sample(channel: u8, attenuation: u8) {
APB_SARADC::regs().onetime_sample().modify(|_, w| unsafe {
// Make sure only ADC2's onetime-sample bit is set; alternating ADC1/ADC2
// reads must not leave both units enabled.
w.saradc1_onetime_sample().clear_bit();
w.saradc2_onetime_sample().set_bit();
w.onetime_channel().bits(channel);
w.onetime_channel()
.bits(ONETIME_UNIT2_BIT | (channel & ONETIME_CHANNEL_MASK));
w.onetime_atten().bits(attenuation)
});
}

fn config_cal_sample(attenuation: u8) {
APB_SARADC::regs().onetime_sample().modify(|_, w| unsafe {
w.saradc1_onetime_sample().clear_bit();
w.saradc2_onetime_sample().set_bit();
w.onetime_channel().bits(ONETIME_CAL_CHANNEL);
w.onetime_atten().bits(attenuation)
});
}

fn start_onetime_sample() {
APB_SARADC::regs()
let saradc = APB_SARADC::regs();
saradc
.onetime_sample()
.modify(|_, w| w.onetime_start().clear_bit());
#[cfg(esp32c6)]
crate::rom::ets_delay_us(5);
saradc
.onetime_sample()
.modify(|_, w| w.onetime_start().set_bit());
}
Expand All @@ -253,7 +325,7 @@ impl RegisterAccess for crate::peripherals::ADC2<'_> {
.read()
.saradc2_data()
.bits() as u16
& 0xfff
& ADC_VAL_MASK
}

fn reset() {
Expand Down Expand Up @@ -399,14 +471,6 @@ where
let attenuation = self.attenuations[channel as usize].unwrap() as u8;
ADCX::config_onetime_sample(channel, attenuation);
ADCX::start_onetime_sample();

// see https://github.com/espressif/esp-idf/blob/b4268c874a4cf8fcf7c0c4153cffb76ad2ddda4e/components/hal/adc_oneshot_hal.c#L105-L107
// the delay might be a bit generous but longer delay seem to not cause problems
#[cfg(esp32c6)]
{
crate::rom::ets_delay_us(40);
ADCX::start_onetime_sample();
}
}

// Wait for ADC to finish conversion
Expand Down
73 changes: 67 additions & 6 deletions esp-hal/src/analog/adc/xtensa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,39 @@ pub(super) const NUM_ATTENS: usize = 10;

cfg_if::cfg_if! {
if #[cfg(esp32s3)] {
const ADC_VAL_MASK: u16 = 0xfff;
const ADC_CAL_CNT_MAX: u16 = 32;
const ADC_CAL_CHANNEL: u16 = 15;
/// Mask covering the 12-bit conversion result on ESP32-S3.
///
/// `sar_meas{1,2}_ctrl2.meas_data_sar` is a 16-bit field that also
/// contains arbiter / validity flag bits in the high bits; reads must
/// be masked to the actual converter width to avoid garbage readings
/// (especially on ADC2, where the arbiter sets flags when Wi-Fi takes
/// over).
const ADC_VAL_MASK: u16 = 0x0fff;
} else if #[cfg(esp32s2)] {
/// Mask covering the 13-bit conversion result on ESP32-S2.
const ADC_VAL_MASK: u16 = 0x1fff;
}
}

#[cfg(esp32s3)]
const ADC_CAL_CNT_MAX: u16 = 32;

/// Sentinel value for [`CalibrationAccess::ADC_CAL_CHANNEL`]. The xtensa
/// calibration routine never uses this as a channel index — instead it calls
/// [`RegisterAccess::disable_channels`] (matching ESP-IDF's
/// `adc_oneshot_ll_disable_channel`) and writes the calibration attenuation
/// to channel 0's atten slot, since the RTC controller auto-selects channel 0's
/// attenuation when all channels are disabled.
#[cfg(esp32s3)]
const ADC_CAL_CHANNEL: u16 = 15;

/// Attenuation slot the RTC controller falls back to when no channels are
/// enabled. ESP-IDF's `cal_setup` writes the calibration attenuation here.
/// Referenced from the generic `adc_calibrate` body, so it has to be in
/// scope for any `RegisterAccess` impl — not just chips that actually
/// implement `CalibrationAccess`.
const CAL_ATTEN_CHANNEL: usize = 0;

impl<ADCX> AdcConfig<ADCX>
where
ADCX: RegisterAccess,
Expand All @@ -38,9 +65,17 @@ where

ADCX::enable_vdef(true);

// Start sampling
ADCX::set_en_pad(ADCX::ADC_CAL_CHANNEL as u8);
ADCX::set_attenuation(ADCX::ADC_CAL_CHANNEL as usize, atten as u8);
// Match ESP-IDF's `cal_setup` in components/hal/adc_hal_common.c:
//
// adc_oneshot_ll_disable_channel(adc_n); // sar_en_pad = 0
// adc_oneshot_ll_set_atten(adc_n, 0, atten); // channel 0's atten
//
// The IDF comment notes: "When controlled by RTC controller, when all
// channels are disabled, HW auto selects channel0 atten param." So the
// literal `0` here is a hardware-defined fallback slot, not an arbitrary
// channel choice.
ADCX::disable_channels();
ADCX::set_attenuation(CAL_ATTEN_CHANNEL, atten as u8);

// Connect calibration source
ADCX::connect_cal(source, true);
Expand Down Expand Up @@ -86,6 +121,12 @@ pub trait RegisterAccess {

fn set_en_pad(channel: u8);

/// Disable all channels on this ADC unit (write `sar_en_pad = 0`).
///
/// Used by the calibration setup to detach the SAR from any external pad.
/// Equivalent to ESP-IDF's `adc_oneshot_ll_disable_channel`.
fn disable_channels();

fn clear_start_sample();

fn start_sample();
Expand Down Expand Up @@ -140,6 +181,12 @@ impl RegisterAccess for crate::peripherals::ADC1<'_> {
.modify(|_, w| unsafe { w.sar1_en_pad().bits(1 << channel) });
}

fn disable_channels() {
SENS::regs()
.sar_meas1_ctrl2()
.modify(|_, w| unsafe { w.sar1_en_pad().bits(0) });
}

fn clear_start_sample() {
SENS::regs()
.sar_meas1_ctrl2()
Expand All @@ -161,11 +208,15 @@ impl RegisterAccess for crate::peripherals::ADC1<'_> {
}

fn read_data() -> u16 {
// The data field is 16 bits wide but only `ADC_VAL_MASK` of it carries
// the conversion result; the upper bits are arbiter / validity flags
// (see comment on `ADC_VAL_MASK`).
SENS::regs()
.sar_meas1_ctrl2()
.read()
.meas1_data_sar()
.bits()
& ADC_VAL_MASK
}

#[cfg(any(esp32s2, esp32s3))]
Expand Down Expand Up @@ -250,6 +301,12 @@ impl RegisterAccess for crate::peripherals::ADC2<'_> {
.modify(|_, w| unsafe { w.sar2_en_pad().bits(1 << channel) });
}

fn disable_channels() {
SENS::regs()
.sar_meas2_ctrl2()
.modify(|_, w| unsafe { w.sar2_en_pad().bits(0) });
}

fn clear_start_sample() {
SENS::regs()
.sar_meas2_ctrl2()
Expand All @@ -271,11 +328,15 @@ impl RegisterAccess for crate::peripherals::ADC2<'_> {
}

fn read_data() -> u16 {
// ADC2 specifically can return non-zero flag bits in the upper bits
// when the arbiter takes over for Wi-Fi; an unmasked read produces
// huge bogus values. See comment on `ADC_VAL_MASK`.
SENS::regs()
.sar_meas2_ctrl2()
.read()
.meas2_data_sar()
.bits()
& ADC_VAL_MASK
}

#[cfg(any(esp32s2, esp32s3))]
Expand Down
8 changes: 5 additions & 3 deletions esp-hal/src/efuse/esp32c2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,13 @@ pub fn rtc_calib_cal_code(_unit: AdcCalibUnit, atten: Attenuation) -> Option<u16
}

// see <https://github.com/espressif/esp-idf/blob/903af13e8/components/efuse/esp32c2/esp_efuse_table.csv#L97>
// ESP-IDF: signed-extended diff_code11 (sign bit 5); out_digi = code0 - signed - 123
// (so bit 5 set means add the magnitude; bit 5 clear means subtract).
let diff_code11: u16 = super::read_field_le(ADC1_CAL_VOL_ATTEN3);
let code11 = if diff_code0 & (1 << 5) != 0 {
code0 - (diff_code11 & 0x1f)
let code11 = if diff_code11 & (1 << 5) != 0 {
code0 + (diff_code11 & 0x1f)
} else {
code0 + diff_code11
code0 - diff_code11
} - 123;

Some(code11)
Expand Down
Loading
Loading