ADC Acquisition Service
Overview
The ADC Acquisition Service provides high-performance, timer-triggered ADC sampling with DMA transfer, digital filtering, and a publish-subscribe mechanism for distributing ADC data to multiple consumers. The service is fully configured via Kconfig and devicetree and operates in a dedicated thread.
Features
Timer-Triggered Acquisition: Configurable sampling rate using hardware timer triggers
DMA-Based Transfer: Efficient zero-copy data acquisition using Direct Memory Access
3rd-Order Cascaded RC Filtering: Digital low-pass filtering with configurable cutoff frequency
Publish-Subscribe Pattern: Memory pool-based asynchronous data distribution to multiple subscribers
VREFINT Calibration: Automatic VDD voltage measurement using internal voltage reference
Thread-Safe: Dedicated service thread with synchronization primitives
Devicetree-Driven: ADC channels and trigger timer configured via devicetree
Shell Commands: Runtime debugging and inspection via Zephyr shell
Architecture
Thread Model
The service runs a dedicated thread that loops at the notification rate interval:
Polls the control queue with a
notificationRatetimeout (handles stop/suspend requests)Processes filtered ADC data and calculates voltage values using VREFINT calibration
Notifies active subscribers by allocating a memory pool block and calling each callback
ADC sampling happens independently and asynchronously: a hardware timer fires at the sampling rate, triggers an async ADC conversion via DMA, and the completion callback pushes raw samples into the digital filter buffers. The service thread reads the filter output on each notification interval.
Data Flow
flowchart LR
A[Timer Trigger] --> B[ADC + DMA]
B --> C[Raw Buffer]
C --> D[Digital Filter]
D --> E[Voltage Conversion]
E --> F[Subscribers]
Memory Pool Pattern
The service uses CMSIS-RTOS v2 memory pools to pass ADC data to subscribers:
Pool is created with
2 × maxSubCountblocksEach block contains:
sizeof(SrvMsgPayload_t) + (channelCount × sizeof(float))Data includes the embedded pool ID (
poolId) for proper cleanupSubscribers must free memory after processing:
osMemoryPoolFree(data->poolId, data)
Configuration
Kconfig Options
Enable the service in prj.conf:
CONFIG_ENYA_ADC_ACQUISITION=y
CONFIG_ENYA_ADC_ACQUISITION_LOG_LEVEL=3
CONFIG_ENYA_ADC_ACQUISITION_STACK_SIZE=1024
CONFIG_ENYA_ADC_ACQUISITION_SAMPLING_RATE_US=500
CONFIG_ENYA_ADC_ACQUISITION_FILTER_TAU=31
CONFIG_ENYA_ADC_ACQUISITION_MAX_SUB_COUNT=4
CONFIG_ENYA_ADC_ACQUISITION_NOTIFICATION_RATE_MS=10
Required dependencies:
# CMSIS-RTOS v2 for memory pools
CONFIG_CMSIS_RTOS_V2=y
CONFIG_CMSIS_V2_MEM_SLAB_MAX_DYNAMIC_SIZE=1024
# Heap for dynamic allocation
CONFIG_HEAP_MEM_POOL_SIZE=8192
Devicetree Configuration
Define ADC channels in the zephyr,user node and create an alias for the trigger timer:
/ {
aliases {
adc-trigger = &adc_trigger_counter;
};
zephyr,user {
io-channels = <&adc1 0>, <&adc1 1>, <&adc1 4>, <&adc1 13>;
vref-channel-index = <3>; /* Index of VREFINT channel */
};
};
&timers6 {
status = "okay";
st,prescaler = <31999>; /* 64MHz / 32000 = 2kHz timer clock */
adc_trigger_counter: counter {
status = "okay";
};
};
&adc1 {
status = "okay";
pinctrl-0 = <&adc1_in0_pa0>, <&adc1_in1_pa1>, <&adc1_in4_pa4>;
pinctrl-names = "default";
dmas = <&dmamux1 1 5 (STM32_DMA_PERIPH_TO_MEMORY | STM32_DMA_MEM_INC |
STM32_DMA_PERIPH_16BITS | STM32_DMA_MEM_16BITS)>;
dma-names = "adc";
channel0: channel@0 {
reg = <0>;
zephyr,gain = "ADC_GAIN_1";
zephyr,reference = "ADC_REF_INTERNAL";
zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_TICKS, 40)>;
zephyr,oversampling = <4>;
zephyr,resolution = <12>;
};
/* Additional channels... */
};
Digital Filtering
The service implements a 3rd-order cascaded RC low-pass filter using integer mathematics for efficiency.
Filter Equation
Each stage uses: y[n] = y[n-1] + α × (x[n] - y[n-1])
where α = tau / 512 (FILTER_PRESCALE = 9)
Calculating Filter Tau
To calculate the tau value for a desired 3rd-order cutoff frequency (fc_3rd):
Calculate the required 1st-order cutoff:
fc_1st = fc_3rd / 0.5098
Calculate alpha:
α = 1 - exp(-2π × fc_1st / fs)
where
fsis the sampling frequency (1/samplingRate)Calculate tau:
tau = α × 512
Round to nearest integer (valid range: 1 to 511)
Example Calculation
For fs = 2000 Hz (samplingRate = 500 μs) and desired fc_3rd = 10 Hz:
fc_1st = 10 / 0.5098 ≈ 19.6 Hz
α = 1 - exp(-2π × 19.6 / 2000) ≈ 0.0614
tau = 0.0614 × 512 ≈ 31
Note
Each RC stage has cutoff fc_1st, but cascading three stages results in:
fc_3rd = fc_1st × 0.5098
API Usage
Initialization
The service is fully configured via Kconfig — no runtime parameters are required:
#include "adcAcquisition.h"
int err = adcAcqInit();
if (err < 0) {
LOG_ERR("ADC init failed: %d", err);
return err;
}
Subscribing to ADC Data
The callback receives a SrvMsgPayload_t containing voltage values for all channels as floats.
The subscriber must free the payload back to the pool before returning.
int adcCallback(SrvMsgPayload_t *data)
{
size_t chanCount = data->dataLen / sizeof(float);
float *voltages = (float *)data->data;
/* Process voltage values */
for (size_t i = 0; i < chanCount; i++) {
LOG_INF("ch%d: %.3f V", i, (double)voltages[i]);
}
/* CRITICAL: always free memory back to pool */
osMemoryPoolFree(data->poolId, data);
return 0;
}
/* Subscribe to ADC updates */
err = adcAcqSubscribe(adcCallback);
if (err < 0) {
LOG_ERR("Subscribe failed: %d", err);
}
Managing Subscriptions
/* Pause a subscription (stop receiving callbacks) */
err = adcAcqPauseSubscription(adcCallback);
/* Resume a paused subscription */
err = adcAcqUnpauseSubscription(adcCallback);
/* Unsubscribe completely */
err = adcAcqUnsubscribe(adcCallback);
Shell Commands
The service provides shell commands for runtime debugging:
# Get the number of configured ADC channels
uart:~$ adc_acq get_chan_count
SUCCESS: channel count: 4
# Get raw ADC value for channel 0
uart:~$ adc_acq get_raw 0
SUCCESS: channel 0 raw value: 8192
# Get voltage value for channel 0
uart:~$ adc_acq get_volt 0
SUCCESS: channel 0 volt value: 1.650 V
Memory Pool Sizing
The service automatically calculates memory pool size based on configuration:
Block Size:
sizeof(SrvMsgPayload_t) + (channelCount × sizeof(float))Block Count:
2 × CONFIG_ENYA_ADC_ACQUISITION_MAX_SUB_COUNT
Ensure CONFIG_CMSIS_V2_MEM_SLAB_MAX_DYNAMIC_SIZE is larger than the total pool memory
(blockSize × blockCount).
Best Practices
Always Free Memory: Subscribers must call
osMemoryPoolFree(data->poolId, data)after processingKeep Callbacks Fast: Callbacks run in the service thread context; avoid blocking operations
Use Message Queues: For heavy processing, copy data to a message queue and free immediately:
int callback(SrvMsgPayload_t *data) { float voltages[MY_CHAN_COUNT]; memcpy(voltages, data->data, data->dataLen); osMemoryPoolFree(data->poolId, data); return k_msgq_put(&myQueue, voltages, K_NO_WAIT); }
Configure Adequate Heap: Ensure heap size accommodates memory pool requirements
Match Sampling and Notification Rates: Set notification rate as a multiple of sampling period
Troubleshooting
Pool Allocation Failures
Symptom: ERROR: pool allocation failed for subscription X
Causes:
All pool blocks in use (subscribers not freeing memory)
Callbacks taking too long (blocking pool returns)
Solutions:
Verify all callbacks call
osMemoryPoolFree(data->poolId, data)Increase
CONFIG_ENYA_ADC_ACQUISITION_MAX_SUB_COUNT(increases pool size)Increase
CONFIG_ENYA_ADC_ACQUISITION_NOTIFICATION_RATE_MS(slower callback frequency)
Memory Pool Creation Failure
Symptom: ERROR -12: unable to create subscription data pool
Causes:
CONFIG_HEAP_MEM_POOL_SIZEtoo smallCONFIG_CMSIS_V2_MEM_SLAB_MAX_DYNAMIC_SIZEtoo small
Solutions:
Increase heap:
CONFIG_HEAP_MEM_POOL_SIZE=8192Increase CMSIS limit:
CONFIG_CMSIS_V2_MEM_SLAB_MAX_DYNAMIC_SIZE=1024
Implementation Notes
Service uses VREFINT (internal voltage reference) for VDD measurement and voltage calibration
Timer trigger prevents ADC overruns with a busy flag
Digital filter state is maintained across conversions for each channel
Subscription array uses dynamic allocation with Kconfig-defined limits
Thread priority is set via
CONFIG_ENYA_ADC_ACQUISITION_THREAD_PRIORITY
API Reference
- group ADC Acquisition Service
High-performance, timer-triggered ADC sampling with DMA transfer and digital filtering.
The ADC Acquisition Service provides timer-triggered ADC sampling with DMA transfer, a 3rd-order cascaded RC digital filter, and a publish-subscribe mechanism for distributing ADC data to multiple consumers. The service is fully configured via devicetree and operates in a dedicated thread.
Typedefs
-
typedef int (*AdcSubCallback_t)(SrvMsgPayload_t *data)
The ADC subscription callback type.
float*).Called with @p data pointing to the ADC data buffer (must be freed by the subscriber). The buffer contains float values (cast to
- Return:
0 if successful, the error code otherwise.
Functions
-
int adcAcqInit(void)
Initialize the ADC acquisition service.
Configures the ADC and subscriptions from Kconfig, creates and names the service thread, and registers it with the service manager. All configuration is driven by Kconfig symbols.
- Returns:
0 if successful, the error code otherwise.
-
int adcAcqSubscribe(AdcSubCallback_t callback)
Subscribe to the ADC service.
- Parameters:
callback – [in] The subscription callback.
- Returns:
0 if successful, -ENOSPC if the maximum subscription count is reached.
-
int adcAcqUnsubscribe(AdcSubCallback_t callback)
Unsubscribe from the ADC service.
- Parameters:
callback – [in] The subscription callback.
- Returns:
0 if successful, -ESRCH if subscription not found.
-
int adcAcqPauseSubscription(AdcSubCallback_t callback)
Pause a subscription.
- Parameters:
callback – [in] The subscription callback.
- Returns:
0 if successful, -ESRCH if the subscription is not found.
-
int adcAcqUnpauseSubscription(AdcSubCallback_t callback)
Unpause a subscription.
- Parameters:
callback – [in] The subscription callback.
- Returns:
0 if successful, -ESRCH if the subscription is not found.
-
struct AdcConfig_t
- #include <adcAcquisition.h>
The ADC configuration structure.
property. The trigger timer is obtained from the adc-trigger devicetree alias. The sampling rate is in microseconds and will be used to set the timer period. The timer will trigger the ADC conversion.The ADC device and channels are obtained from devicetree io-channels
integer mathematics. It uses the digital RC filter equation: y[n] = y[n-1] + α × (x[n] - y[n-1]) Where α = tau / 512 (FILTER_PRESCALE = 9)Filter Description: The filter is a 3rd-order cascaded RC low-pass filter implemented in
(fc_3rd):Filter Tau Calculation: To calculate the tau value for a desired 3rd-order cutoff frequency
Calculate the required 1st-order cutoff: fc_1st = fc_3rd / 0.5098
Calculate alpha: α = 1 - exp(-2π × fc_1st / fs) where fs is the sampling frequency (1/samplingRate)
Calculate tau: tau = α × 512
Round to nearest integer (valid range: 1 to 511)
Example: For fs = 2000 Hz (samplingRate = 500 μs) and desired fc_3rd = 10 Hz: fc_1st = 10 / 0.5098 ≈ 19.6 Hz α = 1 - exp(-2π × 19.6 / 2000) ≈ 0.0614 tau = 0.0614 × 512 ≈ 31
results in: fc_3rd = fc_1st × 0.5098Note: Each RC stage has cutoff fc_1st, but cascading three stages
-
struct AdcSubConfig_t
- #include <adcAcquisition.h>
The ADC subscriptions configuration structure.
-
typedef int (*AdcSubCallback_t)(SrvMsgPayload_t *data)