-
Notifications
You must be signed in to change notification settings - Fork 254
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ADC Calibration #326
Comments
I don't believe we do any ADC calibration so that would be another thing to fix in esp-hal. |
So, I double checked some things Having 13-bit on ESP32-S3 is wrong since it only supports 12-bit - but setting the bit-width is a no-op here - so it's not the cause of the problems. I compared the readings to esp-idf and certainly the esp-idf readings are correct while esp-hal shows the described behavior. But if I change So, the real problem is actually the uncalibrated reads |
So I tried to understand this whole calibration thing but at this point I give up. I found these files: The code seems to:
Is any of this documented somewhere or is there someone knowledgeable about it I could ask? |
I also had a look and it's unfortunately under-documented in the TRM Another source to look at would be the NuttX implementation e.g. https://github.com/apache/nuttx/blob/master/arch/risc-v/src/esp32c3/esp32c3_adc.c for ESP32-C3. It might help a bit to see what is really needed and what not |
Thanks for the link, it does indeed help! I started writing some code to test things: // constants taken from https://github.com/espressif/esp-idf/blob/045163a2ec99eb3cb7cc69e2763afd145156c4cf/components/soc/esp32s3/include/soc/regi2c_saradc.h
const I2C_SAR_ADC: u32 = 0x69;
const I2C_SAR_ADC_HOSTID: u32 = 1;
const ADC_SAR1_INITIAL_CODE_HIGH_ADDR: u32 = 0x1;
const ADC_SAR1_INITIAL_CODE_HIGH_ADDR_MSB: u32 = 0x3;
const ADC_SAR1_INITIAL_CODE_HIGH_ADDR_LSB: u32 = 0x0;
const ADC_SAR1_INITIAL_CODE_LOW_ADDR: u32 = 0x0;
const ADC_SAR1_INITIAL_CODE_LOW_ADDR_MSB: u32 = 0x7;
const ADC_SAR1_INITIAL_CODE_LOW_ADDR_LSB: u32 = 0x0;
pub fn adc1_set_calibration(val: u16) {
let [val_h, val_l] = val.to_be_bytes();
unsafe {
regi2c_write_mask!(I2C_SAR_ADC, ADC_SAR1_INITIAL_CODE_HIGH_ADDR, val_h as u32);
regi2c_write_mask!(I2C_SAR_ADC, ADC_SAR1_INITIAL_CODE_LOW_ADDR, val_l as u32);
}
} Calling this function with different values does indeed change the ADC voltage range. Could you maybe help me understand some things here? I can see that the macro expands to a function call of a function that resides somewhere in ROM. But I can't find anything about what the function does and what kind of value it expects. |
It's unfortunately undocumented as far as I can see I think there is some I2C bus and various things are controlled by it (obviously something about the ADC and also other things https://github.com/cnlohr/esp32-c3-cntest#process-for-figuring-out-i2c-and-apll ) For ESP32-C6 those functions are not used from ROM but they are in esp-idf, eg. here https://github.com/espressif/esp-idf/blob/045163a2ec99eb3cb7cc69e2763afd145156c4cf/components/esp_rom/patches/esp_rom_regi2c_esp32c6.c#L172-L195 While that is for ESP32-C6 it would look similar for other chips, I guess. At least it gives an impression of what the ROM function does |
Interesting info. Here's what I found out today: From trying out different values I found that you want to call Supposedly you can recover this value from some Efuse bits or you can perform your own measurements using some internal GND connection of the ADC. The value differs between ADC1 and ADC2 and different Attenuations. The IDF calls I wrote some helper functions over at: The efuse data seems to be stored in a very ... unusual... format. The offsets I read from the Efuses don't match up with good offsets which I determined experimentally. Maybe I messed up translating the code somewhere or I'm still missing some info. I'm not sure I'll continue my investigation at this point. |
That's already awesome! Thanks for sharing your findings. I definitely understand you might not feel like continuing this journey but thankfully you shared your findings in a very accessible way so even if you don't want to continue here someone can continue from here Thanks again! |
Do ADC calibrations need to be made on each boot? |
I'll try to look into the configuration process, hopefully we can make some progress here 😅 |
I haven't tried reading efuse yet, but on an esp32c3 I've had some success imitating the "Calibrate based on GND voltage" approach in the NuttX implementation. @noonien as @dimpolo pointed out above, you need to reset the calibration whenever the attenuation changes. Since the attenuation is only known in |
Just a quick update, I'm able to get raw values in the range of 0 - 4095 and not just ~2200-4095 on C3 (tested with joystick). I still have to implement the approximation functions from raw values to voltage and right now, I'm working with hardcoded addresses/values etc. But I hope we are very close to the correct calibration on C3 at least. |
Regarding the conversion to voltages: since I need to scale the settings differently anyway, it would be nice to also have access to the raw values. |
You could probably use a type, e.g. I'm still closely following this issue for the @AeroRust workshop @JurajSadel and I'm really happy that you did such a progress on it. |
Hello, |
I think @JurajSadel made some good progress in getting it to work for ESP32-C3 (which is IIRC what you urgently need). Maybe there is a branch you can already test |
Great! If there's a branch I would like to try it out. Edit: Found it https://github.com/JurajSadel/esp-hal/tree/feature/adc_calibration |
Currently ESP-IDF implements two-step raw to voltage conversion with curve fitting (at least for ESP32-C3 and ESP32-S3) The curve coefficients hardcoded in source for each attenuation and differs for C3 and S3 (see esp32c3/curve_fitting_coefficients.c and esp32s3/curve_fitting_coefficients.c). Original implementation far from ideal. First, it uses decimal fixed-point arithmetic in hot code path (for raw value to voltage convarsion) which requires decimal division by My quick solution for coefficients table looks like so: macro_rules! curve_fitting_coeff_tables {
($($name:ident: $mul:path [ $([ $($val:literal,)* ],)* ];)*) => {
$(
const $name: [[i64; TERM_MAX]; COEFF_GROUP_NUM] = [
$([
$(($val as f64 * $mul as f64) as i64,)*
],)*
];
)*
};
}
const COEFF_GROUP_NUM: usize = 4;
const TERM_MAX: usize = 5;
const ERROR_COEF_MUL: u64 = 1 << 60;
curve_fitting_coeff_tables! {
C3_ERROR_COEF_ATTEN: ERROR_COEF_MUL [
// atten0
[
-0.2259664705000430,
-0.0007265418501948,
0.0000109410402681,
0, // unused
0, // unused
],
// atten1
[
0.4229623392600516,
-0.0000731527490903,
0.0000088166562521,
0, // unused
0, // unused
],
// atten2
[
-1.0178592392364350,
-0.0097159265299153,
0.0000149794028038,
0, // unused
0, // unused
],
// atten3
[
-1.4912262772850453,
-0.0228549975564099,
0.0000356391935717,
-0.0000000179964582,
0.0000000000042046,
],
];
} |
This is my attempt: adc_calibration (PoC and WiP). I tested this code with potentiometers and thermistors. Seems it works pretty fine. |
As everyone seems to be working on either C3 specific code, or generic (like the curve fitting), I've started poking at the S3. This thing really isn't documented at all. (Maybe there's hope for a newer TRM, as the last revision is March of 2023?) My work on S3 calibration is here. I'm using the Efuse maps from @dimpolo as they are a lot more convenient than me looking up bits from here and there, pieces of the C3 PR, and the curve fitting by @katyo looks like a good finishing touch. As it turns out, my S3 came with the offsets calibrated, and the self-calibration code came from the C3 impl, and I think only the efuse reading part is working. I'll need to circle back to this. What's there: before every conversion, the software loads calibration params from global memory. This is missing in the PR from @JurajSadel which assumes a single calibration value. I'm confident we'll get to the end of this eventually 😂 Edit: as a fun side note, it looks like my ADC reading code is really sensitive (as in, different amounts of error in the returned voltage) to the time between reads. I suspect something is not correctly stopped in the driver. |
Looks like curve fitting from IDF have a problem with zero term. /**
* For atten0 ~ 2:
* error = (K0 * X^0) + (K1 * X^1) + (K2 * X^2);
*
* For atten3:
* error = (K0 * X^0) + (K1 * X^1) + (K2 * X^2) + (K3 * X^3) + (K4 * X^4);
*/
variable[0] = 1;
coeff = (*param->coeff)[atten][0][0];
term[0] = variable[0] * coeff / (*param->coeff)[atten][0][1];
error = (int32_t)term[0] * (*param->sign)[atten][0]; adc_cali_curve_fitting.c#L207-L217 For example (esp32c3):
I'm not sure if this was done on purpose. If represent raw ADC samples as a binary fixed-point values (i.e. when raw range 0..2^12 corresponds to range 0.0..1.0) then I planned some experiments to find coefficients myself and compare it with original implementation. |
@katyo if I am interpreting the C code and examples correctly, curve fitting is not actually used by the ADC driver, rather it's an optional feature for users. Is this correct? I don't actaully see any mention of it inside the conversion functions, and the examples seem to use the curve-fitting calibration externally. Because, if this is the case, I'm wondering whether curve fitting should be included in the HAL or provided separately. Alternatively, whether the HAL should provide one read function each for raw and curve-fitted conversions. |
Actually curve fitting consist of two steps: linear fitting and correction using error curve. First (linear fitting) step uses reference point which represented as a mean ADC sample for some voltage in a middle of range. That ADC sample value stores in efuse registers as a 10-bit signed int (actual_value = 2000 + stored_value), the voltage for reference point is hardcoded constant. For example (esp32c3):
Second (curve fitting) step uses hardcoded coefficients (specific for chip model) to do error correction for measured value. Because each step above is specific for device model I think it should be a part of HAL. My implementation is flexible as to which calibration steps will be applied for each input. You can combine it using tuples:
Zero bias correction uses initial values stored in efuse registers for each attenuation but in case when storead values is not a valid this values can be measured by connecting input to ground internally. NOTE: The implementation from IDF provides values in millivolts. I would like fix it to get calibrated raw ADC values in range 0..2^12-1 instead. |
Implemented calibration for ESP32-H2 today. Doesn't appear to be working correctly unfortunately, so will debug it next week and open a PR when it's ready. |
I tried following @jessebraham work on H2 calibration but my H2 revision does not support adc calibration. Will try to get a new devkit in two weeks. Edit1: WIP ADC cal for H2 branch: https://github.com/SergioGasquez/esp-hal/tree/feat/adc-cal-h2 |
On ESP32-S3 with an attenuation of 11dB, the readings saturate at about 1.5V. I have a correcttly working project for the same board using the IDF with C. In this project, the resolution is set to 12-bit whereas the setting in
esp-hal
is fixed at 13-bit. It would be nice to also have a configurable setting for the resolution.Edit by @jessebraham
Status of ADC calibration is as follows:
The text was updated successfully, but these errors were encountered: