Skip to content

BSP Sensor Analog Module

The bsp_sensor_analog module is a lightweight, generic analog-sensor driver for the ESP32/ESP32-S3 platform. It provides a clean abstraction over ESP-IDF 5.x’s adc_oneshot API and is designed for telemetry / monitoring use cases — measuring physical quantities such as voltage, temperature (via thermistor), or any other parameter that can be expressed as an analog voltage.

Key design properties:

PropertyValue
Locationhardware/common/drivers/sensor_analog/
ScopeSingle shared instance (singleton)
ADC APIESP-IDF adc_oneshot (oneshot mode)
Build flagSENSOR_ANALOG=ON → ESP3D_SENSOR_ANALOG_FEATURE=1
FilteringNone (telemetry use; filtering belongs at the application layer)
Thread safetyNot re-entrant — caller must serialize access

Not to be confused with the bsp_physical_inputs potentiometer driver (phy_potentiometer). Both use adc_oneshot internally, but they serve entirely different roles. See § Relationship with phy_potentiometer.


Physical Hardware

ESP-IDF 5.x

Hardware Abstraction Layer

Application Layer

sensor_analog_read()

sensor_analog_get_unit()

converter fn + unit string

adc_oneshot_read()

ESP3DSensor

(main/modules/sensors/esp3d_sensor.h/.cpp)

sensor_analog

(hardware/common/drivers/sensor_analog/)

Customization Hook

(customizations/sensor_analog/customizations.h)

adc_oneshot API

(esp_adc/adc_oneshot.h)

ADC GPIO Pin

(voltage divider, thermistor, etc.)

bsp_sensor_analog (hardware/common/drivers/sensor_analog/)

sensor_analog_config.h

─────────────────

sensor_analog_config_t

sensor_analog_data_t

sensor_analog_converter_fn_t

sensor_analog.h

─────────────────

sensor_analog_configure()

sensor_analog_read()

sensor_analog_get_unit()

sensor_analog_deinit()

sensor_analog.c

─────────────────

_config (static)

_adc_handle (static)

_is_initialized (static)


A function pointer that transforms a raw 12-bit ADC value into a physical value.

typedef float (*sensor_analog_converter_fn_t)(int raw_value);

raw_value ranges from 0 to 4095 for a 12-bit ADC. The returned float is in whatever physical unit the sensor requires. Passing NULL causes sensor_analog_read() to use the raw integer value cast to float.


Result structure populated by sensor_analog_read().

typedef struct {
bool valid; // true if the ADC read succeeded
int raw; // Raw ADC integer (0-4095 at 12-bit)
float value; // Physical value after conversion (e.g. 3.14 V)
} sensor_analog_data_t;
FieldDescription
validGuards against stale data. Always check this before using value.
rawUnprocessed ADC count. Useful for calibration or debugging.
valueConverter output (or raw cast to float when no converter is set).

Configuration passed to sensor_analog_configure() once at startup.

typedef struct {
adc_unit_t adc_unit; // ADC_UNIT_1 or ADC_UNIT_2
adc_channel_t adc_channel; // ESP-IDF channel constant
adc_atten_t adc_atten; // ADC_ATTEN_DB_0/2_5/6/12
adc_bitwidth_t adc_bitwidth; // ADC_BITWIDTH_12 (typical)
sensor_analog_converter_fn_t converter; // NULL = identity (raw as float)
const char *unit; // e.g. "V", "C", "%", "mV"
} sensor_analog_config_t;
FieldGuidance
adc_unitADC_UNIT_1 recommended. ADC_UNIT_2 is not usable with Wi-Fi active on ESP32.
adc_attenADC_ATTEN_DB_12 gives a 0–3.9 V input range on ESP32; adjust for the voltage your sensor drives.
adc_bitwidthADC_BITWIDTH_12 is the standard 12-bit resolution (0–4095).
converterSet to NULL to expose raw counts, or point to a board-specific function (see § Customization).
unitString stored by pointer — must remain valid for the lifetime of the driver (use a string literal or a customizations.h macro).

esp_err_t sensor_analog_configure(const sensor_analog_config_t *config);

Initializes the ADC oneshot unit and configures the selected channel. Safe to call again if already initialized — returns ESP_OK immediately without re-initializing.

ReturnMeaning
ESP_OKDriver ready
ESP_ERR_INVALID_ARGconfig == NULL
ESP-IDF ADC errorUnit or channel setup failed; _adc_handle is freed and cleared

esp_err_t sensor_analog_read(sensor_analog_data_t *data);

Performs a single ADC oneshot read and, if a converter is configured, transforms the raw value. On success, data->valid is set to true.

ReturnMeaning
ESP_OKdata populated, data->valid == true
ESP_ERR_INVALID_ARGdata == NULL
ESP_ERR_INVALID_STATEDriver not initialized
ESP-IDF ADC errorHardware read failure; data->valid remains false

const char *sensor_analog_get_unit(void);

Returns the unit string from the stored configuration (e.g., "V", "C"). Returns "" if not initialized or no unit was set.


void sensor_analog_deinit(void);

Releases the ADC oneshot unit handle and resets the internal state. Safe to call on an already-deinitialized driver (no-op).


User CallbackESP3DSensorESP-IDF adc_oneshotsensor_analog driverboard_init (BSP)User CallbackESP3DSensorESP-IDF adc_oneshotsensor_analog driverboard_init (BSP)loop[Every _interval ms (ESP3DSensor::handle)]getDataString() callssensor_analog_get_unit()sensor_analog_configure(&config)adc_oneshot_new_unit()adc_oneshot_config_channel()handle stored in _adc_handlesensor_analog_read(&data)adc_oneshot_read(_adc_handle, channel, &raw)raw integer valuedata.value = config.converter(raw)ESP_OK, data.valid=truecallback(data, user_ctx)

module loaded

sensor_analog_configure() returns ESP_OK

sensor_analog_read() called

read complete (success or error)

sensor_analog_deinit()

sensor_analog_deinit() [no-op]

sensor_analog_configure() [already init, returns ESP_OK immediately]

Uninitialized

Initialized

Reading

Internal static variables:

VariableTypeDescription
_configsensor_analog_config_tCopy of the configuration passed at init
_is_initializedboolGuards re-initialization and read calls
_adc_handleadc_oneshot_unit_handle_tESP-IDF ADC unit handle

CMakeLists.txt

OPTION(SENSOR_ANALOG ON)

cmake/features.cmake

add_compile_options(-DESP3D_SENSOR_ANALOG_FEATURE=1)

component 'sensor_analog' included in build

cmake/sanity_check.cmake

Validates exactly one of

SENSOR_DHT / SENSOR_BMP280 / SENSOR_ANALOG is ON

esp3d_sensor.cpp

#if ESP3D_SENSOR_ANALOG_FEATURE

#include 'sensor_analog.h'

ESP3DSensor::doRead()

calls sensor_analog_read()

ESP3DSensor::getDataString()

calls sensor_analog_get_unit()

cmake/sanity_check.cmake enforces that exactly one sensor type is active per build:

CMake OptionDriverMeasures
SENSOR_DHT=ONsensor_dhtTemperature + Humidity (DHT11 / DHT22, 1-wire)
SENSOR_BMP280=ONsensor_bmp280Temperature + Pressure ± Humidity (I²C)
SENSOR_ANALOG=ONsensor_analogAny analog voltage (ADC)

If no sensor is required, all three must be OFF and ESP3D_SENSOR_FEATURE must also be OFF.


Board- or product-specific conversion logic lives exclusively in:

customizations/sensor_analog/customizations.h

This file is the only file that should be edited to adapt sensor_analog to a new sensor. The default ships with a linear voltage-divider example:

// Unit label displayed after the value (e.g. in [ESP210] response)
#define SENSOR_ANALOG_UNIT_STR "V"
// Converter: 12-bit ADC, ADC_ATTEN_DB_12 → 0–3.9 V linear range on ESP32
static inline float sensor_analog_converter(int raw_value)
{
return (float)raw_value * 3.9f / 4095.0f;
}
#define SENSOR_ANALOG_CONVERTER_FN sensor_analog_converter
Sensor typeUnit stringFormula
Direct voltage (11 dB atten, ESP32)"V"raw * 3.9f / 4095.0f
Direct voltage (11 dB atten, ESP32-S3)"V"raw * 3.1f / 4095.0f
NTC thermistor"C"Steinhart-Hart equation applied to the derived voltage
Light sensor (percentage)"%"(float)raw * 100.0f / 4095.0f
Raw pass-through"raw"NULL (no converter; raw int cast to float)

Memory note: sensor_analog_converter is static inline in the header. It adds zero runtime overhead and requires no additional RAM after init.


The ESP3DSensor class (main/modules/sensors/esp3d_sensor.h) is the sole application-layer consumer of sensor_analog. It:

  1. Does not call sensor_analog_configure() — that responsibility belongs to board_init().
  2. Calls sensor_analog_read() inside ESP3DSensor::doRead() at each polling interval.
  3. Maps sensor_analog_data_t fields into ESP3DSensorData (analog_value, analog_raw, analog_valid).
  4. Calls sensor_analog_get_unit() inside getDataString() to produce formatted output (e.g., "3.3[V]") compatible with the [ESP210] command and WebSocket dispatch.

sensor_analog_read()

sensor_analog_get_unit()

ESP3DSensor::doRead()

ESP3DSensor::getDataString()

esp3d_sensor_cb_t callback

(UI update, WebSocket push, ...)

ESP210 command response

e.g. '3.3[V]'

For full details on ESP3DSensor, its polling mechanism, and [ESP210] integration, see the Core Platform & Application Services module documentation.


Both drivers use adc_oneshot internally, but they serve completely different purposes and must not be confused:

Aspectsensor_analogphy_potentiometer
RoleTelemetry sensor (monitoring, reporting)UI input device (pendant speed / feed selector)
Locationhardware/common/drivers/sensor_analog/hardware/common/drivers/phy_potentiometer/
Parent modulebsp_sensor_analog (this module)bsp_physical_inputs
FilteringNone (single oneshot read)5-sample moving average, adaptive threshold
CalibrationNoneADC line-fitting or curve-fitting scheme
Output consumerESP3DSensor → [ESP210] / WebSocketpotentiometer_read_cb → LVGL LV_EVENT_POTENTIOMETER_CHANGED
Build flagSENSOR_ANALOG=ONESP3D_HARDWARE_POTENTIOMETER_FEATURE=1
Can coexistYes, on separate ADC channelsYes, on separate ADC channels

Minimal board-level setup (pattern used in board_init.c):

#include "sensor_analog.h"
#include "customizations/sensor_analog/customizations.h"
static const sensor_analog_config_t analog_sensor_cfg = {
.adc_unit = ADC_UNIT_1,
.adc_channel = ADC_CHANNEL_0, // GPIO1 on ESP32-S3
.adc_atten = ADC_ATTEN_DB_12, // 0–3.9 V input range (ESP32)
.adc_bitwidth = ADC_BITWIDTH_12, // 12-bit resolution (0–4095)
.converter = SENSOR_ANALOG_CONVERTER_FN,
.unit = SENSOR_ANALOG_UNIT_STR,
};
// Called from board_init(), guarded by #if ESP3D_SENSOR_ANALOG_FEATURE
esp_err_t ret = sensor_analog_configure(&analog_sensor_cfg);
if (ret != ESP_OK) {
esp3d_log_e("Analog sensor init failed: %d", ret);
return ret;
}
// Direct read (normally done automatically by ESP3DSensor::handle()):
sensor_analog_data_t data;
ret = sensor_analog_read(&data);
if (ret == ESP_OK && data.valid) {
esp3d_log("Sensor: raw=%d, value=%.2f %s",
data.raw, data.value, sensor_analog_get_unit());
}

ConstraintDetail
SingletonOnly one ADC channel per build. A second call to sensor_analog_configure() is silently accepted (returns ESP_OK) without re-initializing — the first configuration wins.
No filteringSingle oneshot read per sensor_analog_read() call. Noisy signals should be averaged inside the converter function or at the application layer.
ADC_UNIT_2 + Wi-FiADC_UNIT_2 is unavailable while Wi-Fi is active on ESP32 (hardware silicon limitation). Use ADC_UNIT_1 in all network-enabled builds.
Not ISR-safeadc_oneshot_read() must not be called from an interrupt context. Call sensor_analog_read() only from task context.
Memory footprintThree static variables (_config ≈ 28 bytes, _is_initialized, _adc_handle); no heap allocations after sensor_analog_configure().
Sensor type exclusivityThe build system enforces exactly one sensor type (SENSOR_DHT, SENSOR_BMP280, or SENSOR_ANALOG) per firmware image.

ModuleRelationship
bsp_physical_inputsSibling BSP driver; phy_potentiometer uses the same ADC peripheral for UI input rather than telemetry.
bsp_bsp_board_initializationboard_init() calls sensor_analog_configure() when ESP3D_SENSOR_ANALOG_FEATURE=1.
bspParent BSP module grouping all hardware driver sub-modules.