STM32 ADC Basics
Introduction
The Analog-to-Digital Converter (ADC) is one of the most important peripherals in microcontrollers, including STM32 devices. It allows your microcontroller to interface with the analog world by converting continuously varying analog signals (like temperature, light, or sound) into digital values that can be processed by the microcontroller.
In this tutorial, we'll explore the fundamental concepts of using ADCs on STM32 microcontrollers, from basic theory to practical implementation. By the end, you'll be able to configure and use the ADC peripheral to read analog inputs in your own projects.
Understanding ADC Basics
What is an ADC?
An Analog-to-Digital Converter (ADC) translates analog voltage signals into digital values that your microcontroller can understand and process.
Key ADC Specifications
Before diving into STM32-specific implementations, let's understand the key specifications of ADCs:
-
Resolution: The number of bits used to represent the analog value. STM32 ADCs typically offer 12-bit resolution, but some models support 10, 14, or 16-bit modes.
-
Sampling Rate: The number of conversions performed per second, measured in samples per second (SPS).
-
Reference Voltage: The voltage against which analog inputs are measured. In STM32, this is typically connected to the VDDA pin.
-
Input Channels: STM32 microcontrollers offer multiple input channels, allowing you to read several analog signals through a single ADC.
ADC Resolution Example
With a 12-bit ADC (common in STM32), you get 2^12 = 4096 different levels. If your reference voltage is 3.3V, the resolution is:
Resolution = 3.3V / 4096 = 0.0008V = 0.8mV
This means the smallest voltage change the ADC can detect is 0.8mV.
STM32 ADC Features
STM32 microcontrollers come with powerful ADC peripherals that include:
- Multiple ADC units (up to 3 in some devices)
- 12-bit resolution (up to 16-bit in some models)
- Multiple input channels
- Different conversion modes
- DMA support for high-speed operation
- Programmable sampling time
- Various triggering options
Configuring STM32 ADC
Basic Configuration Using STM32CubeMX
The easiest way to configure the ADC is using STM32CubeMX:
- Select your microcontroller
- Configure a pin as an analog input in the Pinout view
- Configure the ADC parameters in the ADC configuration tab
- Generate code
Manual Configuration Steps
If you prefer configuring the ADC manually, here are the basic steps:
- Enable the ADC clock
- Configure the ADC parameters (resolution, alignment, etc.)
- Configure the desired channel
- Calibrate the ADC (recommended)
- Enable the ADC
Here's an example code snippet for basic ADC configuration:
// Enable ADC clock
RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
// Configure ADC
ADC1->CR1 = 0; // Reset CR1 register
ADC1->CR2 = 0; // Reset CR2 register
// Set 12-bit resolution and right alignment
ADC1->CR1 &= ~ADC_CR1_RES; // RES = 00: 12-bit resolution
// Configure channel 0 (PA0)
ADC1->SQR1 &= ~ADC_SQR1_L; // 1 conversion in regular sequence
ADC1->SQR3 = 0; // Channel 0 is first in sequence
// Set sampling time for channel 0
ADC1->SMPR2 = (7 << (3 * 0)); // Set channel 0 sampling time to 480 cycles
// Enable ADC
ADC1->CR2 |= ADC_CR2_ADON;
Using HAL Library
Most developers prefer using the STM32 HAL library which simplifies ADC configuration. Here's a typical example:
ADC_HandleTypeDef hadc1;
void MX_ADC1_Init(void)
{
ADC_ChannelConfTypeDef sConfig = {0};
// ADC1 configuration
hadc1.Instance = ADC1;
hadc1.Init.Resolution = ADC_RESOLUTION_12B;
hadc1.Init.ScanConvMode = DISABLE;
hadc1.Init.ContinuousConvMode = DISABLE;
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 1;
hadc1.Init.DMAContinuousRequests = DISABLE;
hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
if (HAL_ADC_Init(&hadc1) != HAL_OK)
{
Error_Handler();
}
// Configure Channel
sConfig.Channel = ADC_CHANNEL_0;
sConfig.Rank = 1;
sConfig.SamplingTime = ADC_SAMPLETIME_56CYCLES;
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
{
Error_Handler();
}
}
Reading ADC Values
Polling Method
The simplest method to read ADC values is polling:
uint16_t readADC(void)
{
HAL_ADC_Start(&hadc1); // Start ADC conversion
// Wait for conversion to complete
if (HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK)
{
return HAL_ADC_GetValue(&hadc1); // Return the conversion result
}
return 0; // Return 0 if conversion failed
}
Interrupt Method
For more efficient operation, you can use interrupts:
// In initialization:
HAL_ADC_Start_IT(&hadc1);
// Interrupt callback:
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
if (hadc->Instance == ADC1)
{
uint16_t adcValue = HAL_ADC_GetValue(&hadc1);
// Process the ADC value
// Restart conversion if needed
HAL_ADC_Start_IT(&hadc1);
}
}
DMA Method
For high-speed applications or when reading multiple channels, DMA is the best option:
uint16_t adcValues[4]; // Buffer for 4 channels
// In initialization:
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adcValues, 4);
// DMA callback:
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
if (hadc->Instance == ADC1)
{
// adcValues array now contains the latest readings
// Process the values here
}
}
ADC Conversion Modes
STM32 ADCs support several conversion modes:
Single Conversion Mode
In this mode, one conversion is performed when requested, and the ADC returns to idle state afterward.
// Start a single conversion
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, 100);
uint16_t value = HAL_ADC_GetValue(&hadc1);
Continuous Conversion Mode
In this mode, the ADC continuously performs conversions without the need to retrigger:
// Enable continuous mode in configuration
hadc1.Init.ContinuousConvMode = ENABLE;
// Start continuous conversion
HAL_ADC_Start(&hadc1);
// Later, get the latest value anytime
uint16_t value = HAL_ADC_GetValue(&hadc1);
Scan Mode
Scan mode allows you to convert multiple channels in sequence:
// Enable scan mode in configuration
hadc1.Init.ScanConvMode = ENABLE;
hadc1.Init.NbrOfConversion = 4; // Number of channels
// Configure each channel
sConfig.Channel = ADC_CHANNEL_0;
sConfig.Rank = 1;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
sConfig.Channel = ADC_CHANNEL_1;
sConfig.Rank = 2;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
// And so on for additional channels...
Practical Example: Reading a Potentiometer
Let's look at a complete example of reading a potentiometer connected to PA0:
#include "main.h"
ADC_HandleTypeDef hadc1;
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_ADC1_Init(void);
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_ADC1_Init();
uint16_t raw_value;
float voltage;
while (1)
{
// Start ADC conversion
HAL_ADC_Start(&hadc1);
// Wait for conversion to complete
if (HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK)
{
// Get the conversion result
raw_value = HAL_ADC_GetValue(&hadc1);
// Convert to voltage (assuming 3.3V reference)
voltage = (float)raw_value * (3.3 / 4096.0);
// Do something with the value (e.g., print it via UART)
// ...
}
HAL_Delay(100); // Read every 100ms
}
}
// ADC initialization function
static void MX_ADC1_Init(void)
{
ADC_ChannelConfTypeDef sConfig = {0};
hadc1.Instance = ADC1;
hadc1.Init.Resolution = ADC_RESOLUTION_12B;
hadc1.Init.ScanConvMode = DISABLE;
hadc1.Init.ContinuousConvMode = DISABLE;
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 1;
hadc1.Init.DMAContinuousRequests = DISABLE;
hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
if (HAL_ADC_Init(&hadc1) != HAL_OK)
{
Error_Handler();
}
sConfig.Channel = ADC_CHANNEL_0; // PA0
sConfig.Rank = 1;
sConfig.SamplingTime = ADC_SAMPLETIME_56CYCLES;
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
{
Error_Handler();
}
}
Real-World Application: Temperature Sensor
STM32 microcontrollers often include an internal temperature sensor that can be read via ADC. Here's how to read it:
void readInternalTemp(void)
{
ADC_ChannelConfTypeDef sConfig = {0};
float temperature;
uint16_t raw_value;
// Configure ADC for internal temperature sensor
sConfig.Channel = ADC_CHANNEL_TEMPSENSOR;
sConfig.Rank = 1;
sConfig.SamplingTime = ADC_SAMPLETIME_144CYCLES;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
// Enable temperature sensor
ADC->CCR |= ADC_CCR_TSVREFE;
// Start ADC conversion
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, 100);
// Get the raw value
raw_value = HAL_ADC_GetValue(&hadc1);
// Convert to temperature (formula depends on specific STM32 family)
// For STM32F4:
temperature = ((float)raw_value * 3.3 / 4096.0 - 0.76) / 0.0025 + 25.0;
// For other families, check the reference manual for the conversion formula
}
Common ADC Sampling Techniques
Oversampling
To improve accuracy, you can oversample (take multiple readings and average them):
uint16_t oversampledADC(uint8_t samples)
{
uint32_t sum = 0;
for (uint8_t i = 0; i < samples; i++)
{
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, 100);
sum += HAL_ADC_GetValue(&hadc1);
HAL_Delay(1); // Small delay between samples
}
return (uint16_t)(sum / samples);
}
Low-Pass Filtering
For noisy signals, a simple digital low-pass filter can help:
uint16_t filteredValue = 0;
const float ALPHA = 0.1; // Filter coefficient (0-1)
uint16_t lowPassFilter(uint16_t newValue)
{
filteredValue = (uint16_t)(ALPHA * newValue + (1 - ALPHA) * filteredValue);
return filteredValue;
}
ADC Calibration
For maximum accuracy, some STM32 families support ADC calibration:
void calibrateADC(void)
{
// For STM32F3/L4/G4 families
HAL_ADCEx_Calibration_Start(&hadc1, ADC_SINGLE_ENDED);
// For other families that support calibration, check reference manual
}
Best Practices
- Match Sampling Time to Signal: Use longer sampling times for high-impedance sources.
- Use Oversampling: For critical measurements, oversample to reduce noise.
- Check Reference Voltage: Make sure your reference voltage is stable.
- Filter Noisy Signals: Apply digital or analog filtering when needed.
- Calibrate When Possible: Always calibrate the ADC if your MCU supports it.
- Consider DMA: For high-speed applications, use DMA to free the CPU.
Summary
In this tutorial, we've covered the basics of using STM32 ADCs:
- Understanding ADC concepts and specifications
- Configuring the ADC using different methods
- Reading ADC values through polling, interrupts, and DMA
- Working with different conversion modes
- Implementing practical examples like a potentiometer reader
- Advanced techniques like filtering and calibration
With this knowledge, you should be able to integrate analog sensors into your STM32 projects and read values from the physical world.
Exercises
- Connect a potentiometer to an analog pin and display its value on an LED (brightness control).
- Read the internal temperature sensor and trigger an alarm when it exceeds a threshold.
- Implement a multi-channel ADC reading using DMA.
- Create a digital oscilloscope using the ADC in high-speed mode.
- Design a data logger that samples an analog sensor and stores readings in memory.
Further Learning
To deepen your understanding of STM32 ADCs, consider exploring:
- ADC with timer triggering
- Injected channels
- Dual ADC modes (available on some STM32 models)
- Hardware oversampling (available on newer STM32 families)
- Analog watchdog functionality
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)