Skip to content

Conversation

@jonathanhogg
Copy link
Contributor

@jonathanhogg jonathanhogg commented Sep 14, 2021

Rework the ADC implementation to use the #4213 API (mostly) adding support for calibrated voltage readings and the ADC2 block. Resolves #6219. Part-resolves #3943.

@jonathanhogg jonathanhogg force-pushed the esp32_adc_cal branch 3 times, most recently from dc526c2 to 93583fb Compare September 14, 2021 18:10
@jonathanhogg
Copy link
Contributor Author

jonathanhogg commented Sep 14, 2021

@dpgeorge: this is a version of the ADCBlock API for ESP32. I've not done docs yet as I've diverged a bit from #4213 and wanted to check some notes with you first.

I've got:

  • block = ADCBlock(unit_id, *, bits)
  • block.init(*, bits)
  • adc = block.connect(pin, *, …) or .connect(channel, *, …) or .connect(channel, pin, *, …) – keyword arguments passed through to adc.init()
  • adc = ADC(pin, *, atten)
  • adc.init(*, atten)
  • raw = adc.read()
  • raw_u16 = adc.read_u16()
  • microvolts = adc.read_uv() – using calibration API (only actually mV resolution on ESP32)

and for compatibility:

  • adc.width(bits) – now an instance method that updates the linked block
  • adc.atten(atten) – same as adc.init(atten=atten)

This seemed like the base functionality to match what we have now.

I'm minded to add:

  • block = adc.block() (done)

I'm not sure:

  • Should the ESP32 port accept a sample_ns keyword argument for ADC.init() and ignore it?
  • How did you mean the gain_mult and gain_div keyword arguments to work? Are these just storing values that should be mixed into the read_uv() result to adjust it? I don't see an easy way to map these to the ESP32 attenuations so I've put an atten keyword argument in for that.

For good measure, I've finally included support for the ADC2 unit.

@jonathanhogg
Copy link
Contributor Author

jonathanhogg commented Dec 8, 2021

@dpgeorge: I've added some notes on this API to the ESP32 quick ref and am marking this as ready for review.

It seems to me that the new calibrated read_uv() method and support for ADC block 2 are really worth having now and I've made sure that the old API is still supported while adding your new ADCBlock() API(*). Not doing any changes to the main docs while this is the only port with ADCBlock() support.

(*) I've not done anything about the sample_ns keyword (not supported on ESP32) or the gain_mult and gain_div keywords as I just don't understand how you meant them to work. I'm happy to make changes later as you see fit.

@jonathanhogg jonathanhogg marked this pull request as ready for review December 8, 2021 14:14
@jonathanhogg
Copy link
Contributor Author

For reference:

I'm using this new calibrated voltage API in the field now with 20 microcontrollers running a pair of large-scale light artworks, sampling supply voltages and case temperatures. Consistency across the different packages is very good – way better than I've ever seen just dumbly adjusting the raw values with a fixed multiplier.

@dpgeorge
Copy link
Member

dpgeorge commented Dec 9, 2021

Thanks for this, it looks great!

Do you think it's worth having an explicit calibration method, like ADCBlock.calibrate(...)? Calibration may be done automatically in some circumstances (eg for ease of use) but there may be situations where the user wants to explicitly recalibrate? I know on stm32 parts this would be useful.

@jonathanhogg
Copy link
Contributor Author

Do you think it's worth having an explicit calibration method, like ADCBlock.calibrate(...)?

I guess the question is what does it do?

If it's non-port-specific then we'd have to bake-in a simple, common algorithm for converting ADC raw values to voltages and this method would get/set the parameters for that. Then presumably that would take precedence over any built-in calibration API/parameters.

If it's to be port-specific then on the ESP32 port it could get/set the 5 calibration parameters stored in eFuse, but I'm not sure that you'd want to do that unless you really knew what you were doing and then you could just as easily use a fuse tool to set these. Presumably you'd have to go to the effort of setting up a calibration rig and code to come up with better versions of these values?

@jonathanhogg
Copy link
Contributor Author

Wait! Scratch that. The eFuse on ESP32 is, of course, one time programmable, so you'll not be updating it…

Do you think it's useful to have a common ADC calibration API in MicroPython?

@dpgeorge
Copy link
Member

dpgeorge commented Dec 9, 2021

I guess the question is what does it do?

Maybe esp32 does not need it, it seems the calibration is predefined?

But on stm32 there are two types of calibration:

  1. hardware auto-calibration, where you set a bit in the ADC peripheral and wait until the bit is cleared, and during that time the peripheral will auto-calibrate (I think to determine DC offset)
  2. calculation of conversion factor from raw ADC reading to voltage value; this requires making an ADC reading of the internal 1.2v reference diode, reading the internal calibration value for this reference diode, and computing a correction factor, which is then applied to subsequent readings

In the second case there, the calibration takes time and depending on the application you may want to do it before every reading, or every 10 seconds, or only once at the start, etc.

For these two cases I would propose that a) the calibration is done automatically on creation of the first ADC/ADCBlock instance; and b) the calibration can be redone by the user by a call to ADCBlock.calibrate().

@jonathanhogg
Copy link
Contributor Author

Maybe esp32 does not need it, it seems the calibration is predefined?

Yes, the packages are calibrated during manufacture and the parameters (interval Vref voltage and raw ADC values at 150mV and 850mV for each block) are burned into the eFuse.

For these two cases I would propose that a) the calibration is done automatically on creation of the first ADC/ADCBlock instance; and b) the calibration can be redone by the user by a call to ADCBlock.calibrate().

Sounds port-specific to me then. I don't think there's anything comparable that a calibrate() method on ESP32 could do.

@dpgeorge
Copy link
Member

I've not done anything about the sample_ns keyword (not supported on ESP32) or the gain_mult and gain_div keywords as I just don't understand how you meant them to work. I'm happy to make changes later as you see fit.

For sample_ns, this is definitely needed at least on stm32, because many times I've needed to edit the C code to change the default sampling time. But, let's add that later on, to keep this PR focused.

For gain_mult and gain_div: I think my idea with these was that the overall gain would be gain_mult/gain_div, and these would be integer values to prevent heap allocation of floats. The alternative would be to specify gain in (integer) dB, so that positive/negative numbers can be used for gain/attenuation. The variable atten would be the negative of gain, and IMO it's cleaner to specify it as gain rather than the logically-inverted atten.

Ideally one would simplify specify gain as a constant, eg -6. But that won't support fractional values if we want to stick with integers only (which IMO is a good goal). So then maybe we use units of milli-dB (or milli-B...) and -6dB would be a value of gain_mdb=-6000. That seems rather confusing though. And since ports like esp32 only allow a limited set of gain/attenuation values it does seem better to just use constants. Eg gain=ADC.NEG2DB5 for an attenuation of 2.5dB.

@jonathanhogg jonathanhogg marked this pull request as draft December 21, 2021 16:25
@jonathanhogg
Copy link
Contributor Author

Marking this as a draft again temporarily as I've made the latest changes without any hardware to test them on. I'm away until after xmas now, so I'll test properly on my return before marking it as ready for review again.

@jonathanhogg jonathanhogg force-pushed the esp32_adc_cal branch 2 times, most recently from 04d7d59 to 76c57bb Compare December 21, 2021 16:34
@jonathanhogg
Copy link
Contributor Author

Have re-tested on actual hardware and I'm happy that this works still.

@jonathanhogg jonathanhogg marked this pull request as ready for review January 6, 2022 14:03
Document read_u16(), read_uv() and ADCBlock(). Mark old read(), atten() 
and width() methods as legacy.
Rework the ADC implementation to use the micropython#4213 API (mostly) adding
support for calibrated voltage readings and the ADC2 block. Resolves
Add new machine_adcblock.c file containing the ADCBlock class 
implementation, with new helper functions to connect with ADCBlock.
@dpgeorge
Copy link
Member

Looks good, and I'm happy with the API (and I updated #4213 to match).

I did some basic testing and merged in 3300d6d and 63438a3

Thanks for the work on this! Now that machine.ADCBlock is in the main docs, you may want to tweak the esp32 ADC quickref.

@jonathanhogg
Copy link
Contributor Author

Ace news! One fewer patch for me to maintain 😊

Yes, I'll take a look at the docs again and check it all still makes sense.

@jonathanhogg jonathanhogg deleted the esp32_adc_cal branch January 21, 2022 12:40
tannewt pushed a commit to tannewt/circuitpython that referenced this pull request Apr 7, 2023
fetch-submodules fallback; add remove-submodules
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ESP32] Read from ADC2 Pins RFC: refine and standardise machine.ADC class

2 participants