-
Notifications
You must be signed in to change notification settings - Fork 2k
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
drivers/adc_ng: Added common ADC API #13247
base: master
Are you sure you want to change the base?
Conversation
@bergzand: Could you verify that the issue of using the ADC API from MicroPython without knowing the resolution supported by the hardware in use in advance is possible with this API? E.g. I expect Update: Rephrased the poor choice of words in the last sentence. |
@gschorcht: You're interested in provided access to both external and internal ADCs using a common API. Could you have a look if this API would fit your goals? |
@benpicco: As you're currently working on the ADC driver of the LPC2387, maybe you might be interested in this as well? |
Do you plan to replace the legacy API in future or will it coexist forever?
I could offer to modify PR #10619 for the new API as proof for external ADCs. |
At least until the proposed API here has matured and proven, it is nice to have stable API as alternative to point users to. When this point it reached, a well informed discussion of the pros and cons of keeping the existing API can be done. There is at least one advantage of the old API: By exposing fewer features and not having a |
Sounds good, thanks! I suggest to wait a bit first, so that I can address the first round of feedback first. It would be a pity if you have to constantly rebase and adapt to whatever changes are made to this API based on the reviews. |
drivers/adc_ng/adc_ng.c
Outdated
|
||
#include "adc_ng.h" | ||
|
||
#if (ADC_NG_NUMOF == 1) && defined(MODULE_PERIPH_ADC_NG) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#if (ADC_NG_NUMOF == 1) && defined(MODULE_PERIPH_ADC_NG) | |
#if (ADC_NG_NUMOF == 1) && MODULE_PERIPH_ADC_NG |
could be used instead since an undefined macro yields 0 in expressions by definition. It's just a comment, no change request.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure? It wasn't like this a month ago. There was a heated discussion for adding IS_USED(MODULE_FOO)
to get 0
in case either MODULE_FOO
is 0
or not defined.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure? It wasn't like this a month ago. There was a heated discussion for adding
IS_USED(MODULE_FOO)
to get0
in case eitherMODULE_FOO
is0
or not defined.
yeah, I figured that out just during the mentioned discussion. An undefined macro resolves to "0" only in preprocessor expressions. With IS_USED()
it can be used in C expressions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An undefined macro resolves to "0" only in preprocessor expressions.
Exactly. So if there are no design rules against it (???), #if MODULE_FOO
could be used especially in long expressions to keep them shorter and avoid cascades of #if defined() && !defined() && defined()
.
Did you forget to commit the application? |
No, the commit is there: 9500294 |
Ah sorry, I didn't realize the different PR 😎 |
Thanks for picking this up. First thing I noticed is that you're using
Taking a quick look, I think the glue will be fairly simple. Only issue I see is that MicroPython expects a single number as ADC line (so without the adc dev). An initial implementation might assume that Micropython will always use the internal ADC lines or maybe a conversion function is possible to convert a single number into a combination of device and ADC line. |
Well, actually this is the reason for this: Those are plain numbers and should be treated like that. Unlike e.g. |
I just did some major changes to the proposed API:
As this major changes invalidated any reviews anyway, I squashed and rebased to fix a merge conflict. |
And updated some leftovers in the doc. (I stop squashing now ;-)) |
I hope I can allocate some time for this this week. But I guess the realistic path forward is to mark this API as unstable and get it upstream with only one implementation (the ATmega one already implemented). Likely, the API will have to be adapted to meet the requirements of various ADCs well. But I guess when marked as unstable, there is not that much harm in letting in mature upstream. Note that @gschorcht has ported an external ADC driver to this new API, though. Once that is rebased, there would be two implementations of this API :) |
This API aims to provided an alternative to the periph ADC API with the following improvements: - Support for multiple ADC devices (both external and peripheral ADCs) - Support for selecting the reference voltage - Convenience functions to convert into meaningful physical units It is written to allow coexistence with the existing periph ADC API.
Implemented the periph/adc API on top of the adc_ng API. This allows application using the periph ADC API to also use external ADCs.
Otherwise waiting for IRQs during periph_init() results in a deadlock.
This received some love:
The requested feature to change the ADC MUX config without re-initializing the ADC is not yet provided. At least in the case of the ATmega implementation, there would be really few CPU cycles be gained by this, especially since the sample directly after changing the MUX config should be thrown away anyway due to high noise. I guess it might make sense to add this later on, combined with a PoC implementation that shows that there is actually something to gain. At least of the ATmega backend it is likely not worth the ROM. |
@kfessel convinced me that adding a way to change the mux settings while the ADC is up an running is a sensible thing to do. I'll extend the API accordingly. |
Nice one! I'm still missing burst conversions - but as you mentioned - that could be a topic for a follow-up. Some points that make me wonder:
Some pseudo code: adc_ng_req_t request = ADC_NG_SAMPLE_SINGLE(0); /* 0 is the ADC backend */
adc_ng_req_channel(&request, 1);
adc_ng_req_resolution(&request, 16); /* 16 bit, might return an errno if the requested resolution can't be acchieved */
adc_ng_req_reference_mV(&request, 2500); /* tries to find the best reference */
adc_ng_req_reference_VDD(&request); /* compares to (A)VDD, might call adc_ng_req_reference_mV(&request, 0) internally? */
int sample_raw;
adc_ng_sample(&request, &sample_raw); /* working with pointers allows to hand over arrays for burst conversions */
int sample_mV;
adc_ng_convert_mV(&request, &sample_raw, &sample_mV);
/* `request` can be used for further adc_ng_sample calls - it might be const */ For conversions Sorry for the bike shedding this late in the review process 🙈 |
* @brief Signature of @ref adc_ng_driver_t::single | ||
* | ||
* @param handle Handle of the ADC | ||
* @param[out] dest The sample will be stored here |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What resolution will the sample have?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function is intended to be lightweight and does not support any configuration of the ADC. It depends on a call to the init()
function to be issued first, which configures and enables the ADC. A call to off()
is needed after one is done with sampling the ADC.
It would be e.g. totally valid to implement the ADC to just constantly sample and atomically write the sample into a buffer (e.g. via DMA). And this would atomically read form this buffer to fetch the most recent sample. To allow this level of optimization for speed, we cannot allow switching the resolution anymore here.
Note that many ADCs have something along the line of "if you change the resolution, reference,e etc. make sure to throw away the first three samples". So any chance in configuration should ideally result in three ADC samples to be taken and tossed away first. So this really is not only about shaving of CPU overhead, but also ADC stabilization overhead.
For ATmega, this is mutliple orders of magnitude faster than the old ADC API but not having to wait for the ADC to get only and stable before the next sample is taken when sampling as fast as possible.
I eventually hope to flash an STM32F1 based DSO (I think this one to be similar to the one I bought years ago) with RIOT. But for that I need fast samples :)
Note that the architecture here is fundamentally different to the other periph drivers. The drivers consists of a single upper part, and the multiple lower part implementations. The lower part needs to be written for every different hardware ADC and is a low-level hardware abstraction only, which is not intended to be used by the "typical" application developer. The upper part does the impedance matching between an API that adds little to what the bare hardware provides (hence, it is easy to implement for a driver developer) and an API that is easy to use for an end user. All the interesting NTC stuff is implemented in a single place ( There is one exception, though: The NTC parameters of an ADC internal NTC need to be provided by the the ADC driver as a struct with two parameters. As many hardware ADC implementations just toss in an NTC anyway, it is convenient to have it here: We only need to provide the NTC parameters for each different ADC hardware and all boards using the same ADC hardware will have them readily available. Also note: The NTC utility function works fine with a board or application defined |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's get the ball rolling on this again!
* The reference voltage to use is given with @p ref in millivolt. The driver | ||
* will pick a reference voltage that is as close to @p ref as possible, but | ||
* doesn't reduce the range. (The absolute value of the chosen reference is not | ||
* smaller than the absolute value of the requested reference and it has the | ||
* same sign as the requested reference.) The actually chosen reference voltage | ||
* is stored in @p ref. | ||
* | ||
* E.g. if a device supports reference voltages -2.56V, -1.1V, 1.1V, and 2.56V | ||
* and the user requests a reference voltage of 1.2V (`*ref == 1200`), the | ||
* reference 2.56V is chosen even though 1.1V is a closer match. Otherwise | ||
* input voltages above 1.1V would no longer be distinguishable. Similar if | ||
* -1.3V is requested -2.56V is chosen over -1.1V. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really don't like this auto-magic.
Usually the hardware designer has thought of a specific supported reference voltage that is to be used with an ADC.
In the case of external voltage reference, there is no way the driver could know that value.
IMHO this should be part of periph_conf.h
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you provide more details on how this would look? Especially for the use case of two internal ADCs with a different set of reference voltages and one external with a third, distinct set of reference voltages.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would imaging the reference voltage to be a property of the ADC, so you'd have an ADC config struct in periph_conf.h
static const adc_conf_t adc_conf[] = {
{
.dev = ADC0,
.gclk = SAM0_GCLK_8MHZ,
.ref = ADC_REFCTRL_REFSEL_INTVCC1,
},
};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
adc_common:
typedef struct adc_ref {
int32_t voltage_mv;
union{uint32 refconfv, void* refconfs} internal;
} adc_ref_t;
MCU_adc:
const struct MCU_adc_dev {
adc_ref_t references[NUMBER_OF_REFS];
} mcu_adc;
int adc_ng_init(uint8_t adc, uint8_t chan, uint8_t res, const adc_ref_t *ref)
Initialisation will will be made with a pointer to the ref you want to use
but i would actually go with
adc_init( *adc_dev, *adc_config)
the adc_config can be const and device specific and be prepared for the board / application.
and adc_mux could just get the same config.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can have just an "index" to identity a reference voltage. But we still need to add something to allow users to find what is the most suitable index. Sometimes looking it up for a specific ADC driver would be enough, but if the app is intended to compile and work on multiple boards whe need some abstraction.
Also, just exposing an array of the reference voltage values per ADC isn't sufficient. E.g. for the supply voltage as reference this may depend on the battary charge left ans the temperature, or the Seeduino UNI can run from 5V or 3.3V depending on a sliding button. The driver mostly can figure out the reference voltage at run time (e.g. by selecting the reference voltage of unkown value as reference and a stable internal reference voltage as input).
I don't mind having the fancy reference selection split out. But I really want to be able to write a "multimeter app" that works on any board with an ADC and a display supported by RIOT.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But that's the thing, the reference voltage is usually not something you'd chose at run-time. ADC pins are usually hardwired to a certain functionality, so we can as well specify their voltage reference at the board level and don't have to make it part of the API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the multimeter or the DSO use cases the run-time selection is crucial.
It can also be helpful for reading analog sensor. By swichting to a lower reference voltage, it is possible to pick up smaller changes. But going for a larger reference voltage, high sensor outputs won't oversample.
* @brief Perform a single conversion using the specified ADC channel | ||
* | ||
* @param[in] adc ADC device to use | ||
* @param[out] dest The result is stored here |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What format is result
?
e.g. with a 12 bit ADC, will the max value be 4095 or 4294967295?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
anything but left alignment wouldn't make sense :)
so max value == int32 max and min value == int32 min, if not the value would depend on the chose resolution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but why is the return value int wont this get us into trouble for avr (int == int16)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This intents to also work for external ADCs, e.g. 24 bit ADCs on 8 bit MCUs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oops i thought return is the returned value, but the return value is just an error code
* @brief Initialize an ADC channel, perform a single conversion with | ||
* maximum resolution and range, and power it off again |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does not sound quick
but simple or easy to me
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
indeed, those names are better. Any preference?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not really (just a little in favor of simple) or all_in_one
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i am also in favor of return values being return values and not error codes
Contribution description
This API aims to provided an alternative to the periph ADC API with the following improvements:
It is written to allow coexistence with the existing periph ADC API.
Design decisions
adc_sample()
init
function (e.g. for differential mode to measure the difference of two inputs) would be easy to add, and the existing API could still be used to trigger a conversion and retrieve the resultadc_ng_driver_t
struct containing function pointersTesting procedure
#13248 provides a proof of concept implementation of the ATmega ADC as well as a test application.
Proof that the API is feasible
Issues/PRs references