ADC and Analog Sensor Reading: Converting the Real World to Numbers
How the Analog-to-Digital Converter Works
The real world is analog. Temperature, pressure, current, and vibration are all continuous signals — but a microcontroller can only process digital numbers. The Analog-to-Digital Converter (ADC) bridges this gap by sampling an analog voltage and converting it into a discrete integer value.
The ADC reads the voltage on an input pin and compares it to a reference voltage (typically 3.3V on STM32). The result is a number proportional to the ratio between the input voltage and the reference:
Digital Value = (Vin / Vref) * (2^resolution - 1)
For a 12-bit ADC with 3.3V reference reading 1.65V: (1.65 / 3.3) * 4095 = 2047. Most industrial MCUs have multiple ADC units with several input channels for simultaneous sensor reading.
Resolution and Sampling: 10-bit vs 12-bit
Resolution determines how finely the ADC distinguishes between voltage levels. A 10-bit ADC divides the reference range into 1,024 steps, while a 12-bit ADC provides 4,096 steps.
| Parameter | 10-bit ADC | 12-bit ADC |
|---|---|---|
| Total steps | 1,024 | 4,096 |
| Voltage per step (3.3V ref) | 3.22 mV | 0.806 mV |
| Typical MCU | Arduino (ATmega328) | STM32, ESP32 |
For industrial sensors outputting 0-10V (through a voltage divider) or 4-20 mA (through a shunt resistor), 12-bit resolution is the minimum standard. Measuring 0-500 degrees Celsius with 12 bits gives approximately 0.12 degrees per step.
Sampling rate is how many conversions the ADC performs per second. Industrial sensor readings rarely need more than 100-1000 samples per second; excess bandwidth is used for oversampling and averaging to reduce noise.
Reading an NTC Thermistor Temperature Sensor
An NTC thermistor is one of the most common temperature sensors in industrial equipment. Its resistance decreases as temperature rises. The thermistor forms a voltage divider with a fixed resistor, and firmware calculates temperature using the Beta equation.
#include "stm32f4xx_hal.h"
#include <math.h>
#define ADC_MAX 4095.0f
#define R_FIXED 10000.0f // 10K fixed resistor
#define BETA 3950.0f
#define T_NOMINAL 298.15f // 25C in Kelvin
#define R_NOMINAL 10000.0f // Resistance at 25C
float read_temperature(ADC_HandleTypeDef *hadc) {
HAL_ADC_Start(hadc);
HAL_ADC_PollForConversion(hadc, 100);
uint32_t raw = HAL_ADC_GetValue(hadc);
float resistance = R_FIXED * ((ADC_MAX / (float)raw) - 1.0f);
float steinhart = logf(resistance / R_NOMINAL) / BETA;
steinhart += 1.0f / T_NOMINAL;
return (1.0f / steinhart) - 273.15f;
}
Reading an ACS712 Current Sensor
The ACS712 is a Hall-effect current sensor used in industrial panels to monitor motor current and detect overloads. It outputs a voltage centered at VCC/2 (2.5V), varying linearly with current. The 20A variant has 100 mV/A sensitivity.
#define ACS712_SENSITIVITY 0.100f // 100mV/A for 20A variant
#define ACS712_OFFSET 2.500f
#define DIVIDER_RATIO 0.66f // 3.3V/5V voltage divider
float read_current(ADC_HandleTypeDef *hadc) {
HAL_ADC_Start(hadc);
HAL_ADC_PollForConversion(hadc, 100);
uint32_t raw = HAL_ADC_GetValue(hadc);
float voltage = ((float)raw / 4095.0f) * 3.3f;
float actual = voltage / DIVIDER_RATIO;
return (actual - ACS712_OFFSET) / ACS712_SENSITIVITY;
}
In production firmware, this reading triggers overcurrent protection that disconnects the motor if current exceeds a safe threshold for a configurable duration.
Noise and Filtering: Moving Average
Industrial environments are electrically noisy. Motors and inverters generate interference that corrupts ADC readings. The moving average filter smooths noisy data by maintaining a buffer of the last N samples and outputting their mean.
#define FILTER_SIZE 16
typedef struct {
uint32_t buffer[FILTER_SIZE];
uint8_t index;
uint32_t sum;
bool filled;
} MovingAverage;
void ma_init(MovingAverage *ma) {
memset(ma, 0, sizeof(MovingAverage));
}
uint32_t ma_update(MovingAverage *ma, uint32_t sample) {
ma->sum -= ma->buffer[ma->index];
ma->buffer[ma->index] = sample;
ma->sum += sample;
ma->index = (ma->index + 1) % FILTER_SIZE;
if (!ma->filled && ma->index == 0) ma->filled = true;
uint8_t count = ma->filled ? FILTER_SIZE : ma->index;
return (count > 0) ? (ma->sum / count) : 0;
}
A 16-sample average reduces random noise by a factor of 4. At 100 Hz sampling, this adds 160 ms latency — acceptable for process monitoring but too slow for fast motor control.
Practical Example: Reading 4 Analog Sensors and Displaying Values
This example reads four sensors via DMA, filters them, and sends values over UART.
#define NUM_CHANNELS 4
static uint32_t adc_values[NUM_CHANNELS];
typedef struct {
const char *name;
float scale, offset;
MovingAverage filter;
} SensorChannel;
SensorChannel sensors[NUM_CHANNELS] = {
{"Temperature", 0.0806f, -20.0f, {0}},
{"Current", 0.0122f, -25.0f, {0}},
{"Pressure", 0.0024f, 0.0f, {0}},
{"Humidity", 0.0244f, 0.0f, {0}},
};
void read_all_sensors(ADC_HandleTypeDef *hadc) {
HAL_ADC_Start_DMA(hadc, adc_values, NUM_CHANNELS);
HAL_Delay(5);
char buf[80];
for (int i = 0; i < NUM_CHANNELS; i++) {
uint32_t filtered = ma_update(&sensors[i].filter, adc_values[i]);
float val = (float)filtered * sensors[i].scale + sensors[i].offset;
snprintf(buf, sizeof(buf), "%s: %.2f\r\n", sensors[i].name, val);
HAL_UART_Transmit(&huart2, (uint8_t*)buf, strlen(buf), 100);
}
}
Summary
The ADC converts real-world analog signals into digital values that firmware can process. Resolution (10-bit vs 12-bit) determines measurement precision, while sampling rate controls conversion frequency. Industrial sensors like NTC thermistors and ACS712 current sensors output analog voltages requiring calibration math to convert into engineering units. Electrical noise in factory environments demands filtering — a moving average is the simplest effective approach. DMA-based multi-channel scanning frees the CPU for control logic. The next lesson explores serial communication protocols (UART, SPI, I2C) for connecting the MCU to digital sensors and other controllers.