Skip to content

sensor_analog — Analog Sensor Driver (ADC Oneshot)

sensor_analog — Analog Sensor Driver (ADC Oneshot)

Section titled “sensor_analog — Analog Sensor Driver (ADC Oneshot)”

Low-level hardware driver that wraps the ESP-IDF ADC oneshot API for generic analog sensor input.
Located at: hardware/common/drivers/sensor_analog/

Build status: The SENSOR_ANALOG flag is compiled and link-tested but is not enabled in any current board CMakeLists.txt. It is reserved groundwork for future ESP3D-X sensor hardware.
For the higher-level facade that selects between sensor_analog, sensor_dht, and sensor_bmp280 at compile time, see sensors.md.


  1. Purpose and Scope
  2. Module Architecture
  3. File Structure
  4. Data Structures
  5. Public API
  6. Data Flow
  7. Component Interaction
  8. Integration: ESP3DSensor Facade
  9. sensor_analog vs phy_potentiometer
  10. Build Wiring and Feature Flags
  11. ESP32 Constraints
  12. Usage Example
  13. Key Files
  14. Related Documentation

sensor_analog is a minimal, generic ADC oneshot driver for reading a single analog channel and converting the raw ADC integer into a physically-meaningful float value.

Design goals:

  • Thin wrapper around esp_adc/adc_oneshot.h — no filtering, no calibration, no internal task.
  • Board-agnostic — ADC unit, channel, attenuation, and bit-width are all runtime parameters.
  • Converter injection — a user-supplied sensor_analog_converter_fn_t translates the raw ADC code into the sensor’s unit (°C, V, %, etc.) without coupling the driver to any specific sensor hardware.
  • Memory-safe — uses only a single static instance; no heap allocation.

It is one of three sensor drivers that can be compiled into an ESP3D-X build; the other two are sensor_dht (one-wire, DHT11/DHT22) and sensor_bmp280 (I2C, BMP280/BME280). Exactly one is selected per build via CMake.


Board Init / BSP

ESP-IDF ADC Driver

sensor_analog Driver

Application Layer

sensor_analog_configure(config)

sensor_analog_read(&data)

sensor_analog_get_unit()

ESP3DSensor facade

main/modules/sensors/

sensor_analog_configure()

sensor_analog_read()

sensor_analog_deinit()

sensor_analog_get_unit()

Static state

_config · _adc_handle · _is_initialized

adc_oneshot_new_unit

adc_oneshot_config_channel

adc_oneshot_read

adc_oneshot_del_unit

Board-specific config

sensor_analog_config_t

converter fn

(user-supplied float(*)(int))


hardware/common/drivers/sensor_analog/
├── CMakeLists.txt # IDF component: requires esp3d_log, esp_adc
├── sensor_analog_config.h # sensor_analog_config_t, sensor_analog_data_t, converter fn type
├── sensor_analog.h # Public API declarations
└── sensor_analog.c # Implementation (static single-instance)
idf_component_register(
SRCS "sensor_analog.c"
INCLUDE_DIRS .
REQUIRES esp3d_log esp_adc
)

The component is excluded from the build unless SENSOR_ANALOG is ON (cmake/features.cmake, lines 242–256).


typedef float (*sensor_analog_converter_fn_t)(int raw_value);

User-supplied callback that converts the raw ADC integer (0–4095 for 12-bit) to a physical value in the sensor’s natural unit. Pass NULL to use the raw ADC code cast to float unchanged.

Examples:

// 12-bit ADC → voltage (0–3.3 V with ADC_ATTEN_DB_12)
float adc_to_volts(int raw) {
return raw * (3.3f / 4095.0f);
}
// NTC thermistor: simplified linear approximation
float adc_to_celsius(int raw) {
return raw * 0.0806f - 10.0f;
}
// 0–100 % scale (e.g. light level)
float adc_to_percent(int raw) {
return (raw / 4095.0f) * 100.0f;
}

Configuration passed once to sensor_analog_configure(). Copied into static storage — the caller does not need to retain the struct after the call returns.

FieldTypeDescription
adc_unitadc_unit_tADC hardware unit (ADC_UNIT_1 or ADC_UNIT_2)
adc_channeladc_channel_tADC channel mapped to the sensor GPIO pin
adc_attenadc_atten_tInput attenuation (ADC_ATTEN_DB_0 … ADC_ATTEN_DB_12)
adc_bitwidthadc_bitwidth_tADC resolution (ADC_BITWIDTH_12 → range 0–4095)
convertersensor_analog_converter_fn_tConversion callback, or NULL for raw float output
unitconst char *Display unit string (e.g. "C", "V", "%")

Attenuation vs. usable input voltage range (ESP32, ADC1):

adc_attenUsable input range
ADC_ATTEN_DB_00 – ~950 mV
ADC_ATTEN_DB_2_50 – ~1250 mV
ADC_ATTEN_DB_60 – ~1750 mV
ADC_ATTEN_DB_120 – ~3100 mV (most common; covers 0–3.3 V sensors)

Output structure populated by sensor_analog_read().

FieldTypeDescription
validbooltrue only after a successful ADC read and conversion
rawintRaw ADC integer (0–4095 for 12-bit)
valuefloatconverter(raw) if a converter is set; (float)raw otherwise

All symbols are declared in sensor_analog.h with extern "C" guards, making the driver callable from both C and C++.


esp_err_t sensor_analog_configure(const sensor_analog_config_t *config);

Initializes the ESP-IDF ADC oneshot unit and configures the selected channel.

Behavior:

  • Idempotent. If already initialized, logs a message and returns ESP_OK — no re-initialization.
  • Copies *config into static storage (_config).
  • Creates an adc_oneshot_unit_handle_t via adc_oneshot_new_unit().
  • Configures the channel via adc_oneshot_config_channel(). On failure, releases the unit handle and returns the error.
ParameterDirectionNotes
configinPointer to a filled sensor_analog_config_t. Must not be NULL.

Returns: ESP_OK, ESP_ERR_INVALID_ARG (null config), or an ESP-IDF ADC error code.


esp_err_t sensor_analog_read(sensor_analog_data_t *data);

Performs a single ADC oneshot read and populates *data.

Behavior:

  • Resets data->valid = false, data->raw = 0, data->value = 0.0f at entry.
  • Returns ESP_ERR_INVALID_STATE if called before sensor_analog_configure().
  • Calls adc_oneshot_read() with the stored channel handle.
  • If a converter was provided, stores converter(raw) in data->value; otherwise stores (float)raw.
  • Sets data->valid = true only on full success.
ParameterDirectionNotes
dataoutPointer to sensor_analog_data_t to fill. Must not be NULL.

Returns: ESP_OK, ESP_ERR_INVALID_ARG (null data), ESP_ERR_INVALID_STATE (not initialized), or an ADC read error code.


const char *sensor_analog_get_unit(void);

Returns the unit string stored in the configuration, or "" if the driver is not initialized or config.unit was NULL.


void sensor_analog_deinit(void);

Releases the ADC oneshot unit handle and clears all static state. Safe to call when not initialized (no-op).


ESP3DSensor FacadeESP-IDF ADC oneshotsensor_analogBoard BSP / InitESP3DSensor FacadeESP-IDF ADC oneshotsensor_analogBoard BSP / Initloop[Poll every N ms (from NVS esp3d_sensor_interval, default 30 000 ms)]sensor_analog_configure(&config)adc_oneshot_new_unit(unit_id)adc_handleadc_oneshot_config_channel(channel, atten, bitwidth)ESP_OKESP_OK (_is_initialized = true)sensor_analog_read(&data)adc_oneshot_read(adc_handle, channel)raw_value (int, 0-4095)data.value = converter(raw) OR (float)rawESP_OK · data.valid=true · data.raw · data.valueNormalize into ESP3DSensorDataInvoke registered callback (esp3d_sensor_cb_t)sensor_analog_deinit()adc_oneshot_del_unit(adc_handle)_is_initialized = false · _adc_handle = NULL

Application Layer (C++)

Driver Layer (C)

Build System

#if ESP3D_SENSOR_ANALOG_FEATURE

cmake/features.cmake

SENSOR_ANALOG ON

→ ESP3D_SENSOR_ANALOG_FEATURE

cmake/sanity_check.cmake

Exactly one sensor type

sensor_analog

hardware/common/drivers/sensor_analog/

esp3d_log

components/esp3d_log/

esp_adc

(ESP-IDF built-in)

ESP3DSensor

main/modules/sensors/esp3d_sensor.cpp

[ESP210] command

main/core/commands/esp210.cpp

NVS: esp3d_sensor_interval

via ESP3DSettings

sensor_analog has no dependency on LVGL, FreeRTOS tasks, network services, or the ESP3DValues observable system. It is a pure C driver with static storage, callable from any context without risking LVGL concurrency constraints.


sensor_analog is consumed exclusively through the ESP3DSensor C++ facade in main/modules/sensors/. For the complete facade architecture — polling model, ESP3DValues integration status, [ESP210]/[ESP420] commands, and mDNS sensor TXT record — see sensors.md.

The relevant compile-time branch inside esp3d_sensor.cpp:

// Simplified from esp3d_sensor.cpp — doRead()
#if defined(ESP3D_SENSOR_ANALOG_FEATURE)
sensor_analog_data_t d;
esp_err_t ret = sensor_analog_read(&d);
if (ret == ESP_OK && d.valid) {
_data.analog = d.value;
_data.analog_valid = true;
}
return ret;

The facade normalizes the result into a unified ESP3DSensorData struct. getDataString() then formats it into strings such as "12.3[V]" for [ESP210] and [ESP420] responses.

No ESP3DValues integration. Unlike WiFi status or position data, sensor readings are not pushed into the observable values system. No LVGL screen currently subscribes to sensor data. Adding a UI tile would require a new ESP3DValuesIndex entry — that wiring does not exist today.


Both drivers use esp_adc/adc_oneshot.h to read an analog GPIO pin. They are independent drivers serving different purposes and different consumers.

phy_potentiometer - UI input device

1-3 samples per poll

5-sample moving average filter

ADC calibration (line-fit / curve-fit per chip)

Adaptive change threshold: 8-15 ADC units

Inactivity detection: 2 s window

Consumer: LVGL potentiometer_read_cb

Emits LV_EVENT_POTENTIOMETER_CHANGED

sensor_analog - environmental sensor

Single oneshot read

Optional converter fn

No filtering

No ADC calibration

Unit string for display

Consumer: ESP3DSensor / ESP210

Concernsensor_analogphy_potentiometer
Primary useGeneric sensors: temperature, voltage, lightPhysical feed-rate / speed override knob
FilteringNoneMoving average (5 samples) + adaptive threshold
ADC calibrationNoneadc_cali_scheme — line-fit (ESP32) or curve-fit (ESP32-S3)
Output typefloat physical value + raw intRaw filtered ADC integer (0–4095)
ConsumerESP3DSensor facadeLVGL lv_indev_t (potentiometer_read_cb)
BoardAny (when SENSOR_ANALOG ON)pibot_pendant_v1_0
Feature guardSENSOR_ANALOG (currently OFF everywhere)Always ON for pibot_pendant_v1_0 BSP
Locationhardware/common/drivers/sensor_analog/hardware/common/drivers/phy_potentiometer/

Important: potentiometer_read_cb in boards/pibot_pendant_v1_0/components/bsp/board_init.c calls phy_potentiometer_read(), not sensor_analog_read(). The two drivers do not share state.


cmake configure

SENSOR_SERVICE ON + SENSOR_ANALOG ON

SENSOR_ANALOG OFF

Board CMakeLists.txt

OPTION(SENSOR_SERVICE OFF)

OPTION(SENSOR_ANALOG OFF)

cmake/features.cmake

#define ESP3D_SENSOR_FEATURE

#define ESP3D_SENSOR_ANALOG_FEATURE

Component excluded from build

(features.cmake lines 242-256)

cmake/sanity_check.cmake

Exactly ONE of SENSOR_DHT /

SENSOR_BMP280 / SENSOR_ANALOG must be ON

main/CMakeLists.txt

modules/sensors/ included

esp3d_sensor.cpp

#if ESP3D_SENSOR_ANALOG_FEATURE

sensor_analog.c

CMake flagEffect
SENSOR_SERVICE OFF (default for all boards)Entire sensor subsystem excluded; sensor_analog not compiled
SENSOR_SERVICE ON + SENSOR_ANALOG ONsensor_analog component included; ESP3D_SENSOR_ANALOG_FEATURE defined
Multiple sensor type flags ON simultaneouslyBuild error raised by cmake/sanity_check.cmake lines 186–201

NVS polling interval: esp3d_sensor_interval (default 30 000 ms, defined at main/core/includes/esp3d_settings_defs.inc lines 717–722). Adjustable live via [ESP401] without reflashing.


sensor_analog.c uses three module-level statics:

static sensor_analog_config_t _config;
static bool _is_initialized = false;
static adc_oneshot_unit_handle_t _adc_handle = NULL;

Only one analog sensor channel can be active per binary. This is reinforced by the build system (exactly one sensor type per build) and by the idempotent initialization guard.

ESP32’s ADC2 is shared with the WiFi RF hardware path and cannot be used reliably while WiFi is active. Always select ADC_UNIT_1 in sensor_analog_config_t when WiFi is enabled.

sensor_analog_configure() uses only stack-local ESP-IDF config structs. The one persistent resource (_adc_handle) is an opaque pointer into ESP-IDF’s internal ADC driver pool — no malloc() is called in the driver itself. This satisfies the project’s requirement to avoid dynamic allocation in hardware drivers.

There is no mutex protecting _is_initialized, _adc_handle, or _config. The driver assumes:

  1. sensor_analog_configure() is called once at board init, before any FreeRTOS tasks that call sensor_analog_read() start.
  2. sensor_analog_read() is called from a single context (the network-tick loop that drives ESP3DSensor::handle()).
  3. sensor_analog_deinit() is called only during a controlled shutdown sequence, never concurrently with sensor_analog_read().

If concurrent access becomes necessary, add a portMUX_TYPE spinlock at the call sites — do not modify the driver itself.


#include "sensor_analog.h"
// ── Converter function ────────────────────────────────────────────────────────
// 12-bit ADC (0–4095), ADC_ATTEN_DB_12 (0–3.1 V), 3.3 V supply
// NTC voltage divider, simplified linear fit:
static float ntc_to_celsius(int raw) {
float voltage = raw * (3.3f / 4095.0f);
return (voltage - 0.5f) * 100.0f; // Example only — replace with real curve
}
// ── Board init (called once, before tasks start) ──────────────────────────────
void board_init_temperature_sensor(void) {
sensor_analog_config_t cfg = {
.adc_unit = ADC_UNIT_1,
.adc_channel = ADC_CHANNEL_6, // GPIO34 on ESP32
.adc_atten = ADC_ATTEN_DB_12, // Covers 0–3.1 V
.adc_bitwidth = ADC_BITWIDTH_12, // 0–4095 range
.converter = ntc_to_celsius,
.unit = "C",
};
esp_err_t ret = sensor_analog_configure(&cfg);
if (ret != ESP_OK) {
esp3d_log_e("Temperature sensor init failed: %d", ret);
}
}
// ── Polling read (called from ESP3DSensor::doRead() on the polling interval) ──
void read_temperature_sensor(void) {
sensor_analog_data_t data;
esp_err_t ret = sensor_analog_read(&data);
if (ret == ESP_OK && data.valid) {
esp3d_log("Temperature: %.1f [%s] raw=%d",
data.value,
sensor_analog_get_unit(),
data.raw);
} else {
esp3d_log_e("Temperature read error: %d", ret);
}
}

FileRole
hardware/common/drivers/sensor_analog/sensor_analog_config.hsensor_analog_config_t, sensor_analog_data_t, sensor_analog_converter_fn_t
hardware/common/drivers/sensor_analog/sensor_analog.hPublic API (configure, read, get_unit, deinit)
hardware/common/drivers/sensor_analog/sensor_analog.cImplementation — static single instance, ADC oneshot
hardware/common/drivers/sensor_analog/CMakeLists.txtIDF component; requires esp3d_log, esp_adc
main/modules/sensors/esp3d_sensor.h / .cppESP3DSensor facade; calls sensor_analog_configure() and sensor_analog_read()
cmake/features.cmakeMaps SENSOR_ANALOG flag → ESP3D_SENSOR_ANALOG_FEATURE; excludes component when OFF
cmake/sanity_check.cmakeEnforces exactly one sensor driver per build (lines 186–201)
main/core/commands/esp210.cpp[ESP210] — get/set sensor polling interval, get current reading
main/core/includes/esp3d_settings_defs.incesp3d_sensor_interval NVS definition (lines 717–722, default 30 000 ms)
hardware/common/drivers/phy_potentiometer/phy_potentiometer.cSeparate knob driver — not related to sensor_analog

  • sensors.md — ESP3DSensor facade, all three sensor driver types, polling model, [ESP210]/[ESP420] commands, mDNS sensor TXT record, and ESP3DValues integration status.
  • esp3d_log_guide.md — esp3d_log / esp3d_log_e macro levels and backend configuration used throughout this driver.
  • esp32_memory_constraints.md — Heap fragmentation constraints relevant to all ESP32 hardware drivers.
  • Input_system.md — LVGL virtual keyboard and input handling (consumer of phy_potentiometer, not sensor_analog).
  • display_drivers.md — SPI / I80 / RGB panel driver architecture for context on the broader BSP driver layer.