Continuous ADC
The ContinuousAdc provides a mechanism for high-frequency, continuous, deterministic sampling of analog voltages for multiple channels (potentially across multiple ADC units, depending on the ESP32 chip used). It does this be enabling the continuous ADC DMA mode and then running its own task which retrieves the data and filters it. When the user calls get_mv(adc_channel_t), it simply returns the most recent filtered value for that channel, if it was configured.
Conversion Modes (multiple units)
On chips whose digital controller supports both ADC units in DMA mode (e.g. ESP32-S2 / ESP32-P4), channels from both units can be sampled by one ContinuousAdc. Two conversion modes control how the hardware schedules them:
ADC_CONV_ALTER_UNIT(the derived default when the configured channels span both units): the controller performs one conversion per trigger, alternating between the units. Every channel is sampled evenly at approximatelysample_rate_hzregardless of how the channels are split across the units. Use this for general multi-channel monitoring - e.g. several independent sensors that happen to be wired to pins on different units:// joystick axes on ADC1 + battery divider on ADC2, each sampled ~1 kHz espp::ContinuousAdc adc({.sample_rate_hz = 1000, .channels = {joy_x, joy_y, vbat}});
ADC_CONV_BOTH_UNIT: both units convert simultaneously (in lockstep) on every trigger. Choose it explicitly when the relative timing of two signals matters - e.g. sampling a voltage (on ADC1) and a current (on ADC2) at the same instant to compute instantaneous power, or capturing phase-matched sensor pairs:espp::ContinuousAdc adc({.sample_rate_hz = 1000, .channels = {v_sense, i_sense}, .convert_mode = ADC_CONV_BOTH_UNIT});
Note that with
ADC_CONV_BOTH_UNITeach unit converts on every trigger, so the effective per-channel rate is higher thansample_rate_hz(twice, for a one-channel-per-unit configuration);get_rate()reports the actual rate.
API Reference
Header File
Macros
-
ESP_IDF_VERSION_VAL(major, minor, patch)
-
ESP_IDF_VERSION
Classes
-
class ContinuousAdc : public espp::BaseComponent
ContinuousAdc provides a wrapper around the ESP-IDF continuous adc subsystem, enabling high-frequency, filtered measurements of analog values. The
get_mv()function will always return the most up to date value, without needing to perform additional reads (therefore it is non-blocking).Continuous ADC Example
// NOTE: the unit must support continuous (DMA) mode; on ESP32 / C3 / S3 // that is only unit 1 std::vector<espp::AdcConfig> channels{{.unit = EXAMPLE_ADC_UNIT, .channel = EXAMPLE_ADC_CHANNEL_1, .attenuation = ADC_ATTEN_DB_12}, {.unit = EXAMPLE_ADC_UNIT, .channel = EXAMPLE_ADC_CHANNEL_2, .attenuation = ADC_ATTEN_DB_12}}; // this initializes the DMA and filter task for the continuous adc. The // conversion mode is derived automatically from the channel units; you // can also set it explicitly with e.g. .convert_mode = // ADC_CONV_SINGLE_UNIT_1 (chip-dependent: BOTH_UNIT, ALTER_UNIT, // SINGLE_UNIT_1, SINGLE_UNIT_2) espp::ContinuousAdc adc({.sample_rate_hz = CONFIG_EXAMPLE_ADC_SAMPLE_RATE_HZ, .channels = channels, .window_size_bytes = CONFIG_EXAMPLE_ADC_WINDOW_SIZE_BYTES, .log_level = espp::Logger::Verbosity::WARN}); adc.start(); auto task_fn = [&adc, &channels](std::mutex &m, std::condition_variable &cv) { for (auto &conf : channels) { auto maybe_mv = adc.get_mv(conf); if (maybe_mv.has_value()) { fmt::print("{}: {} mV\n", conf, maybe_mv.value()); } else { fmt::print("{}: no value!\n", conf); } auto maybe_rate = adc.get_rate(conf); if (maybe_rate.has_value()) { fmt::print("{}: {} Hz\n", conf, maybe_rate.value()); } else { fmt::print("{}: no rate!\n", conf); } } // NOTE: sleeping in this way allows the sleep to exit early when the // task is being stopped / destroyed { std::unique_lock<std::mutex> lk(m); cv.wait_for(lk, 500ms); } // don't want to stop the task return false; }; auto task = espp::Task({.callback = task_fn, .task_config = {.name = "Read ADC"}, .log_level = espp::Logger::Verbosity::INFO}); task.start(); // test stopping and starting the adc std::this_thread::sleep_for(3s); logger.info("Stopping ADC"); adc.stop(); std::this_thread::sleep_for(3s); logger.info("Starting ADC"); adc.start();
Note
The available conversion modes, sample frequency range, and throughput depend on the chip. The aggregate sample frequency (sample_rate_hz * number of channels) must be within [SOC_ADC_SAMPLE_FREQ_THRES_LOW, SOC_ADC_SAMPLE_FREQ_THRES_HIGH] for the chip (e.g. 20 kHz - 2 MHz on ESP32; 611 Hz - 83.3 kHz on ESP32-S3 / C3 / C6 / P4 and other newer chips). Some chips (ESP32, ESP32-C3, ESP32-S3) only support continuous (DMA) mode on ADC unit 1, and on ESP32-P4 ADC2 continuous conversions currently produce all-zero samples with esp-idf (oneshot on ADC2 works fine), so prefer ADC1 there as well.
Note
If a channel could not be calibrated (e.g. the calibration eFuse is not burnt),
get_mv()returns the filtered raw value for that channel instead of millivolts (matching OneshotAdc’s behavior).Note
This class is thread-safe: start(), stop(), get_mv(), and get_rate() may be called concurrently from multiple tasks (though, as with any object, destruction must not race other calls).
Note
When the configured channels span both ADC units (on chips whose digital controller supports both units in DMA mode, e.g. ESP32-S2 / ESP32-P4), the conversion mode - if not explicitly provided - is derived as ADC_CONV_ALTER_UNIT: the controller performs one conversion per trigger, alternating between the units. This keeps every channel sampled evenly at approximately
sample_rate_hzregardless of how the channels are split across the units, and is the right choice for general multi-channel monitoring - e.g. several independent sensors that happen to be wired to pins on different units:The alternative, ADC_CONV_BOTH_UNIT, makes both units convert simultaneously (in lockstep) on every trigger. Choose it explicitly when the relative timing of two signals matters - e.g. sampling a voltage (on ADC1) and a current (on ADC2) at the same instant to compute instantaneous power, or capturing phase-matched sensor pairs:// joystick axes on ADC1 + battery divider on ADC2, each ~1 kHz espp::ContinuousAdc adc({.sample_rate_hz = 1000, .channels = {joy_x, joy_y, vbat}});
With BOTH_UNIT each unit converts on every trigger, so the effective per-channel rate is higher thanespp::ContinuousAdc adc({.sample_rate_hz = 1000, .channels = {v_sense, i_sense}, .convert_mode = ADC_CONV_BOTH_UNIT});
sample_rate_hz(twice, for a one-channel-per-unit configuration); get_rate() reports the actual rate.Warning
On ESP32-P4 (verified on hardware with esp-idf v6.0.1), initializing and then deleting the oneshot ADC driver (e.g. a destructed espp::OneshotAdc) before starting continuous mode causes all continuous conversions to produce all-zero samples, on either unit. If you need both drivers on the P4, create the ContinuousAdc first, or keep the OneshotAdc alive.
Public Functions
-
inline explicit ContinuousAdc(const Config &config)
Initialize and start the continuous adc reader.
- Parameters:
config – Config used to initialize the reader.
-
inline ~ContinuousAdc()
Stop, deinit, and destroy the adc reader.
-
inline void start()
Start the continuous adc reader.
-
inline void stop()
Stop the continuous adc reader.
-
inline std::optional<float> get_mv(const AdcConfig &config)
Get the most up to date filtered voltage (in mV) from the provided
channel.Note
If the channel could not be calibrated, the filtered raw value is returned instead.
- Parameters:
config – The config used to initialize the channel (includes unit and channel)
- Returns:
std::optional<float> voltage in mV for the provided channel (if it was configured and the adc is running).
-
inline std::optional<float> get_rate(const AdcConfig &config)
Get the most up to date sampling rate (in Hz) from the provided
channel.- Parameters:
config – The config used to initialize the channel (includes unit and channel)
- Returns:
std::optional<float> Rate in Hz for the provided channel (if it was configured and the adc is running).
-
inline const std::string &get_name() const
Get the name of the component
Note
This is the tag of the logger
- Returns:
A const reference to the name of the component
-
inline void set_log_tag(const std::string_view &tag)
Set the tag for the logger
- Parameters:
tag – The tag to use for the logger
-
inline espp::Logger::Verbosity get_log_level() const
Get the log level for the logger
See also
See also
- Returns:
The verbosity level of the logger
-
inline void set_log_level(espp::Logger::Verbosity level)
Set the log level for the logger
See also
See also
- Parameters:
level – The verbosity level to use for the logger
-
inline void set_log_verbosity(espp::Logger::Verbosity level)
Set the log verbosity for the logger
See also
See also
See also
Note
This is a convenience method that calls set_log_level
- Parameters:
level – The verbosity level to use for the logger
-
inline espp::Logger::Verbosity get_log_verbosity() const
Get the log verbosity for the logger
See also
See also
See also
Note
This is a convenience method that calls get_log_level
- Returns:
The verbosity level of the logger
-
inline void set_log_rate_limit(std::chrono::duration<float> rate_limit)
Set the rate limit for the logger
See also
Note
Only calls to the logger that have _rate_limit suffix will be rate limited
- Parameters:
rate_limit – The rate limit to use for the logger
-
struct Config
Configure the sample rate (globally applied to each channel), select the number of channels, and optionally the conversion mode.
Public Members
-
size_t sample_rate_hz
Samples per second to read from each channel.
-
std::vector<AdcConfig> channels
Channels to read from, with associated attenuations.
-
adc_digi_convert_mode_t convert_mode{static_cast<adc_digi_convert_mode_t>(0)}
Conversion mode (unit 1, unit 2, alternating, or both). Support depends on the chip. Leave at the default (0) to derive it automatically from the units used by
channels.
-
size_t task_priority = {5}
Priority to run the adc data reading / filtering task.
-
size_t window_size_bytes = {256}
Amount of bytes to allocate for the DMA buffer when reading results. Larger values lead to more filtering.
-
size_t sample_rate_hz
-
inline explicit ContinuousAdc(const Config &config)