Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add MD5 functions from ESP ROM (#618)
- Add embassy async `read` support for `uart` (#620)
- Add bare-bones support to run code on ULP-RISCV / LP core (#631)
- Add ADC calibration implementation for a riscv chips (#555)

### Changed

Expand Down
40 changes: 40 additions & 0 deletions esp-hal-common/src/analog/adc/cal_basic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use core::marker::PhantomData;

use crate::adc::{AdcCalEfuse, AdcCalScheme, AdcCalSource, AdcConfig, Attenuation, RegisterAccess};

/// Basic ADC calibration scheme
///
/// Basic calibration is related to setting some initial bias value in ADC.
/// Such values usually is stored in efuse bit fields but also can be measured
/// in runtime by connecting ADC input to ground internally a fallback when
/// it is not available.
#[derive(Clone, Copy)]
pub struct AdcCalBasic<ADCI> {
/// Calibration value to set to ADC unit
cal_val: u16,

_phantom: PhantomData<ADCI>,
}

impl<ADCI> AdcCalScheme<ADCI> for AdcCalBasic<ADCI>
where
ADCI: AdcCalEfuse + RegisterAccess,
{
fn new_cal(atten: Attenuation) -> Self {
// Try to get init code (Dout0) from efuse
// Dout0 means mean raw ADC value when zero voltage applied to input.
let cal_val = ADCI::get_init_code(atten).unwrap_or_else(|| {
// As a fallback try to calibrate via connecting input to ground internally.
AdcConfig::<ADCI>::adc_calibrate(atten, AdcCalSource::Gnd)
});

Self {
cal_val,
_phantom: PhantomData,
}
}

fn adc_cal(&self) -> u16 {
self.cal_val
}
}
240 changes: 240 additions & 0 deletions esp-hal-common/src/analog/adc/cal_curve.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
use core::marker::PhantomData;

use crate::adc::{
AdcCalEfuse,
AdcCalLine,
AdcCalScheme,
AdcHasLineCal,
Attenuation,
RegisterAccess,
};

const COEFF_MUL: i64 = 1 << 52;

type CurveCoeff = i64;

/// Polynomial coefficients for specified attenuation.
pub struct CurveCoeffs {
/// Attenuation
atten: Attenuation,
/// Polynomial coefficients
coeff: &'static [CurveCoeff],
}

type CurvesCoeffs = &'static [CurveCoeffs];

/// Marker trait for ADC which support curve futting
///
/// See also [`AdcCalCurve`].
pub trait AdcHasCurveCal {
/// Coefficients for calculating the reading voltage error.
///
/// A sets of coefficients for each attenuation.
const CURVES_COEFFS: CurvesCoeffs;
}

/// Curve fitting ADC calibration scheme
///
/// This scheme implements final polynomial error correction using predefined
/// coefficient sets for each attenuation.
///
/// This scheme also includes basic calibration ([`AdcCalBasic`]) and line
/// fitting ([`AdcCalLine`]).
#[derive(Clone, Copy)]
pub struct AdcCalCurve<ADCI> {
line: AdcCalLine<ADCI>,

/// Coefficients for each term (3..=5)
coeff: &'static [CurveCoeff],

_phantom: PhantomData<ADCI>,
}

impl<ADCI> AdcCalScheme<ADCI> for AdcCalCurve<ADCI>
where
ADCI: AdcCalEfuse + AdcHasLineCal + AdcHasCurveCal + RegisterAccess,
{
fn new_cal(atten: Attenuation) -> Self {
let line = AdcCalLine::<ADCI>::new_cal(atten);

let coeff = ADCI::CURVES_COEFFS
.iter()
.find(|item| item.atten == atten)
.expect("No curve coefficients for given attenuation")
.coeff;

Self {
line,
coeff,
_phantom: PhantomData,
}
}

fn adc_cal(&self) -> u16 {
self.line.adc_cal()
}

fn adc_val(&self, val: u16) -> u16 {
let val = self.line.adc_val(val);

let err = if val == 0 {
0
} else {
// err = coeff[0] + coeff[1] * val + coeff[2] * val^2 + ... + coeff[n] * val^n
let mut var = 1i64;
let mut err = (var * self.coeff[0] as i64 / COEFF_MUL) as i32;

for coeff in &self.coeff[1..] {
var = var * val as i64;
err += (var * *coeff as i64 / COEFF_MUL) as i32;
}

err
};

(val as i32 - err) as u16
}
}

macro_rules! coeff_tables {
($($(#[$($meta:meta)*])* $name:ident [ $($att:ident => [ $($val:literal,)* ],)* ];)*) => {
$(
$(#[$($meta)*])*
const $name: CurvesCoeffs = &[
$(CurveCoeffs {
atten: Attenuation::$att,
coeff: &[
$(($val as f64 * COEFF_MUL as f64 * 4096f64 / Attenuation::$att.ref_mv() as f64) as CurveCoeff,)*
],
},)*
];
)*
};
}

#[cfg(any(esp32c3, esp32c6, esp32s3))]
mod impls {
use super::*;

impl AdcHasCurveCal for crate::adc::ADC1 {
const CURVES_COEFFS: CurvesCoeffs = CURVES_COEFFS1;
}

#[cfg(esp32c3)]
impl AdcHasCurveCal for crate::adc::ADC2 {
const CURVES_COEFFS: CurvesCoeffs = CURVES_COEFFS1;
}

#[cfg(esp32s3)]
impl AdcHasCurveCal for crate::adc::ADC2 {
const CURVES_COEFFS: CurvesCoeffs = CURVES_COEFFS2;
}

coeff_tables! {
/// Error curve coefficients derived from https://github.com/espressif/esp-idf/blob/903af13e8/components/esp_adc/esp32c3/curve_fitting_coefficients.c
#[cfg(esp32c3)]
CURVES_COEFFS1 [
Attenuation0dB => [
-0.2259664705000430,
-0.0007265418501948,
0.0000109410402681,
],
Attenuation2p5dB => [
0.4229623392600516,
-0.0000731527490903,
0.0000088166562521,
],
Attenuation6dB => [
-1.0178592392364350,
-0.0097159265299153,
0.0000149794028038,
],
Attenuation11dB => [
-1.4912262772850453,
-0.0228549975564099,
0.0000356391935717,
-0.0000000179964582,
0.0000000000042046,
],
];

/// Error curve coefficients derived from https://github.com/espressif/esp-idf/blob/903af13e8/components/esp_adc/esp32c6/curve_fitting_coefficients.c
#[cfg(esp32c6)]
CURVES_COEFFS1 [
Attenuation0dB => [
-0.0487166399931449,
0.0006436483033201,
0.0000030410131806,
],
Attenuation2p5dB => [
-0.8665498165817785,
0.0015239070452946,
0.0000013818878844,
],
Attenuation6dB => [
-1.2277821756674387,
0.0022275554717885,
0.0000005924302667,
],
Attenuation11dB => [
-0.3801417550380255,
-0.0006020352420772,
0.0000012442478488,
],
];

/// Error curve coefficients derived from https://github.com/espressif/esp-idf/blob/903af13e8/components/esp_adc/esp32s3/curve_fitting_coefficients.c
#[cfg(esp32s3)]
CURVES_COEFFS1 [
Attenuation0dB => [
-2.7856531419538344,
-0.0050871540569528,
0.0000097982495890,
],
Attenuation2p5dB => [
-2.9831022915028695,
-0.0049393185868806,
0.0000101379430548,
],
Attenuation6dB => [
-2.3285545746296417,
-0.0147640181047414,
0.0000208385525314,
],
Attenuation11dB => [
-0.6444034182694780,
-0.0644334888647536,
0.0001297891447611,
-0.0000000707697180,
0.0000000000135150,
],
];

/// Error curve coefficients derived from https://github.com/espressif/esp-idf/blob/903af13e8/components/esp_adc/esp32s3/curve_fitting_coefficients.c
#[cfg(esp32s3)]
CURVES_COEFFS2 [
Attenuation0dB => [
-2.5668651654328927,
0.0001353548869615,
0.0000036615265189,
],
Attenuation2p5dB => [
-2.3690184690298404,
-0.0066319894226185,
0.0000118964995959,
],
Attenuation6dB => [
-0.9452499397020617,
-0.0200996773954387,
0.00000259011467956,
],
Attenuation11dB => [
1.2247719764336924,
-0.0755717904943462,
0.0001478791187119,
-0.0000000796725280,
0.0000000000150380,
],
];
}
}
100 changes: 100 additions & 0 deletions esp-hal-common/src/analog/adc/cal_line.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
use core::marker::PhantomData;

use crate::adc::{
AdcCalBasic,
AdcCalEfuse,
AdcCalScheme,
AdcCalSource,
AdcConfig,
Attenuation,
RegisterAccess,
};

/// Marker trait for ADC units which support line fitting
///
/// Usually it means that reference points are stored in efuse.
/// See also [`AdcCalLine`].
pub trait AdcHasLineCal {}

/// Coefficients is actually a fixed-point numbers.
/// It is scaled to put them into integer.
const GAIN_SCALE: u32 = 1 << 16;

/// Line fitting ADC calibration scheme
///
/// This scheme implements gain correction based on reference points.
///
/// A reference point is a pair of a reference voltage and the corresponding
/// mean raw digital ADC value. Such values are usually stored in efuse bit
/// fields for each supported attenuation.
///
/// Also it can be measured in runtime by connecting ADC to reference voltage
/// internally but this method is not so good because actual reference voltage
/// may varies in range 1.0..=1.2 V. Currently this method is used as a fallback
/// (with 1.1 V by default) when calibration data is missing.
///
/// This scheme also includes basic calibration ([`AdcCalBasic`]).
#[derive(Clone, Copy)]
pub struct AdcCalLine<ADCI> {
basic: AdcCalBasic<ADCI>,

/// Gain of ADC-value
gain: u32,

_phantom: PhantomData<ADCI>,
}

impl<ADCI> AdcCalScheme<ADCI> for AdcCalLine<ADCI>
where
ADCI: AdcCalEfuse + AdcHasLineCal + RegisterAccess,
{
fn new_cal(atten: Attenuation) -> Self {
let basic = AdcCalBasic::<ADCI>::new_cal(atten);

// Try get the reference point (Dout, Vin) from efuse
// Dout means mean raw ADC value when specified Vin applied to input.
let (code, mv) = ADCI::get_cal_code(atten)
.map(|code| (code, ADCI::get_cal_mv(atten)))
.unwrap_or_else(|| {
// As a fallback try to calibrate using reference voltage source.
// This methos is no to good because actual reference voltage may varies
// in range 1000..=1200 mV and this value currently cannot be given from efuse.
(
AdcConfig::<ADCI>::adc_calibrate(atten, AdcCalSource::Ref),
1100, // use 1100 mV as a middle of typical reference voltage range
)
});

// Estimate the (assumed) linear relationship between the measured raw value and
// the voltage with the previously done measurement when the chip was
// manufactured.
//
// Rounding formula: R = (OP(A * 2) + 1) / 2
// where R - result, A - argument, O - operation
let gain =
((mv as u32 * GAIN_SCALE * 2 / code as u32 + 1) * 4096 / atten.ref_mv() as u32 + 1) / 2;

Self {
basic,
gain,
_phantom: PhantomData,
}
}

fn adc_cal(&self) -> u16 {
self.basic.adc_cal()
}

fn adc_val(&self, val: u16) -> u16 {
let val = self.basic.adc_val(val);

// pointers are checked in the upper layer
(val as u32 * self.gain / GAIN_SCALE) as u16
}
}

#[cfg(any(esp32c2, esp32c3, esp32c6))]
impl AdcHasLineCal for crate::adc::ADC1 {}

#[cfg(esp32c3)]
impl AdcHasLineCal for crate::adc::ADC2 {}
Loading