前言
ADC(模数转换器)是嵌入式开发中测量模拟信号的核心外设,从简单的电压读取到复杂的传感器数据采集都离不开它。STM32F103 内置 12 位逐次逼近型 ADC,最多支持 18 个通道,在 72MHz 主频下最高采样率达 1Msps,性能非常实用。
在实际项目中,我们最常接触两种采集方式:
- 普通查询模式:简单直接,适合低频率、单通道的采集。
- DMA 传输模式:配合 DMA 进行多通道连续采集,CPU 零负担,是工程应用的主流选择。
本文使用 标准外设库(SPL) ,以 STM32F103C8T6 为核心,先讲清 ADC 时基与规则通道原理,再分别给出普通模式和 DMA 模式的 完整可运行代码,所有配置均经过验证,复制到你的工程里即可点亮功能。
一、STM32 ADC 基础原理
1.1 逐次逼近型 ADC 如何工作
STM32 的 ADC 采用逐次逼近架构,内部通过二分法比较输入电压与 DAC 输出,经过 12 个时钟周期(12 位分辨率)逼近真实值,转换结果范围为 0 ~ 4095。
1.2 规则通道与注入通道
- 规则通道组 :可安排 16 个通道的转换序列,按顺序依次转换,结果只存到一个 共用的 16 位数据寄存器
ADC_DR中。如果 CPU 来不及读取,新的结果会覆盖旧值,这就是多通道采集必须依赖 DMA 的根本原因。 - 注入通道组 :最多 4 个通道,拥有 独立的数据寄存器,可打断规则通道的转换序列,常用于紧急采样(如电流环控制)。
本文将专注于使用最广泛的 规则通道组。
1.3 时钟与采样时间
ADC 挂载在 APB2 总线上,时钟通过分频器供给,最大不超过 14MHz 。
标准配置:72MHz ÷ 6 = 12MHz。
单次转换所需时间 = 采样时间 + 12.5 个 ADC 时钟周期。例如采样时间选 55.5 周期,则总时间 ≈ 68 / 12M ≈ 5.67μs,采样率约 176kHz。
1.4 关键配置参数一览
| 参数 | 含义 | 普通单通道 | DMA 多通道 |
|---|---|---|---|
ADC_Mode |
工作模式 | ADC_Mode_Independent |
ADC_Mode_Independent |
ADC_ScanConvMode |
扫描模式 | DISABLE |
ENABLE |
ADC_ContinuousConvMode |
连续转换 | 按需 | ENABLE |
ADC_DataAlign |
数据对齐 | ADC_DataAlign_Right |
ADC_DataAlign_Right |
ADC_NbrOfChannel |
规则通道数量 | 1 | 实际通道数 |
二、普通模式(查询法)
特点:CPU 主动查询 EOC 标志,手动读取 ADC_DR。代码清晰,适用于低频单通道场景。
2.1 单通道单次转换(PA0)
c
/* adc_single.c */
#include "adc_single.h"
/**
* @brief ADC1 单通道初始化(PA0, 通道0)
*/
void ADC1_Single_Init(void)
{
ADC_InitTypeDef ADC_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
/* 1. 开启时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6); // ADC 时钟 = 72M/6 = 12MHz
/* 2. PA0 模拟输入 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* 3. ADC 基础配置 */
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
ADC_InitStructure.ADC_ScanConvMode = DISABLE; // 单通道不扫描
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; // 单次转换
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 软件触发
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_NbrOfChannel = 1; // 通道数
ADC_Init(ADC1, &ADC_InitStructure);
/* 4. 配置规则通道:通道0,排序第1,采样时间 55.5 周期 */
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
/* 5. 使能 ADC,并校准(必须先使能再校准) */
ADC_Cmd(ADC1, ENABLE);
ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1));
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1));
}
/**
* @brief 触发一次软件转换,返回 ADC 值
*/
uint16_t ADC1_GetValue(void)
{
ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 启动转换
while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); // 等待转换完成
return ADC_GetConversionValue(ADC1); // 读取结果(同时清EOC)
}
c
/* adc_single.h */
#ifndef __ADC_SINGLE_H__
#define __ADC_SINGLE_H__
#include "stm32f10x.h"
void ADC1_Single_Init(void);
uint16_t ADC1_GetValue(void);
#endif
使用示例(main.c):
c
int main(void)
{
uint16_t adc_val;
float vol;
ADC1_Single_Init();
while(1) {
adc_val = ADC1_GetValue();
vol = adc_val * 3.3f / 4095; // 参考电压 3.3V
// 可通过串口打印 vol
}
}
2.2 多通道连续扫描(仅演示,不推荐用于实际项目)
开启扫描 + 连续转换后,ADC 会按顺序循环转换各个通道,但 读取时无法确认当前数据属于哪个通道 ,极易覆盖。此模式仅供学习了解流程,正式项目请直接跳到 DMA 模式。
c
/* 不推荐!仅作演示 */
void ADC1_Multi_NoDMA_Init(void)
{
// ... 时钟、GPIO 同前 ...
ADC_InitStructure.ADC_ScanConvMode = ENABLE; // 扫描模式
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 连续转换
ADC_InitStructure.ADC_NbrOfChannel = 2;
// ... 其余相同 ...
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);
// 启动一次连续转换
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}
警告:这种方式读到的数据顺序不可靠,请不要在项目中采用。
三、DMA 模式(多通道自动采集)
3.1 DMA 如何解决多通道采集难题
DMA(直接存储器访问)可以不经过 CPU,直接将 ADC_DR 中的结果搬运到内存数组里。配合 扫描模式 + 连续转换 + DMA 循环模式,可实现:
- ADC 自动扫描所有通道 → 转换结果由 DMA 按序存入数组 → 数组满后自动从头覆盖。
- CPU 完全被解放,只需在需要时读取数组即可。
3.2 双通道 DMA 循环传输(PA0、PA1)
我们以最常见的双通道电压采集为例,数据存放于 ADC_ConvertedValue[2] 中,索引 0 对应通道 0(PA0),索引 1 对应通道 1(PA1)。
c
/* adc_dma.c */
#include "adc_dma.h"
#define ADC_CH_NUM 2
uint16_t ADC_ConvertedValue[ADC_CH_NUM] = {0};
/**
* @brief ADC1 + DMA 初始化(PA0/通道0, PA1/通道1)
*/
void ADC1_DMA_Init(void)
{
ADC_InitTypeDef ADC_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
DMA_InitTypeDef DMA_InitStructure;
/*========== 1. 使能时钟 ==========*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // DMA1 时钟
RCC_ADCCLKConfig(RCC_PCLK2_Div6); // ADC 时钟 12MHz
/*========== 2. 配置 PA0, PA1 为模拟输入 ==========*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/*========== 3. 配置 DMA1 通道1 ==========*/
DMA_DeInit(DMA1_Channel1); // 复位 DMA 通道
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; // 外设地址
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)ADC_ConvertedValue; // 内存地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 外设到内存
DMA_InitStructure.DMA_BufferSize = ADC_CH_NUM; // 传输个数(两个通道)
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址固定
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址递增
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // 半字(16位)
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 循环模式
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel1, &DMA_InitStructure);
DMA_Cmd(DMA1_Channel1, ENABLE); // 使能 DMA 通道
/*========== 4. 配置 ADC1 ==========*/
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
ADC_InitStructure.ADC_ScanConvMode = ENABLE; // 多通道必须扫描
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 连续转换
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_NbrOfChannel = ADC_CH_NUM; // 通道总数
ADC_Init(ADC1, &ADC_InitStructure);
/* 配置规则通道:序列1 - 通道0, 序列2 - 通道1 */
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);
/* 使能 ADC 的 DMA 请求(极易遗漏!) */
ADC_DMACmd(ADC1, ENABLE);
/* 使能 ADC 并校准 */
ADC_Cmd(ADC1, ENABLE);
ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1));
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1));
/* 启动连续转换(只需触发一次,之后自动循环) */
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}
c
/* adc_dma.h */
#ifndef __ADC_DMA_H__
#define __ADC_DMA_H__
#include "stm32f10x.h"
extern uint16_t ADC_ConvertedValue[2];
void ADC1_DMA_Init(void);
#endif
主程序使用示例(main.c):
c
#include "adc_dma.h"
int main(void)
{
float vol_ch0, vol_ch1;
ADC1_DMA_Init(); // 启动后 DMA 自动循环搬运
while(1) {
/* ADC_ConvertedValue[0] 始终存放通道0(PA0)的最新值 */
/* ADC_ConvertedValue[1] 始终存放通道1(PA1)的最新值 */
vol_ch0 = ADC_ConvertedValue[0] * 3.3f / 4095;
vol_ch1 = ADC_ConvertedValue[1] * 3.3f / 4095;
// 延时或处理数据......
}
}
3.3 代码要点批注
DMA_Mode_Circular:搬运完两个通道后自动跳回数组开头,与 ADC 连续转换完美配合,永不停止。DMA_MemoryInc = Enable:每搬运一次地址递增,保证[0]存通道 0,[1]存通道 1。ADC_DMACmd(ADC1, ENABLE):老手也偶尔忘记这行,没有它 DMA 不会收到任何请求。- 校准顺序 :必须 先
ADC_Cmd(ENABLE)再校准,否则校准无效,结果会有偏差且很难排查。
四、常见避坑指南
-
校准顺序错误
ADC_Cmd一定要在校准函数之前调用,这是标准库用户最容易踩的坑。 -
忘记设置 ADC 时钟分频
不调用
RCC_ADCCLKConfig(RCC_PCLK2_Div6)的话,ADC 时钟默认为 72MHz,远超 14MHz 上限,会导致 ADC 工作异常。 -
DMA 通道对应错误
ADC1 固定使用
DMA1_Channel1,其他外设不同通道,切勿混淆。 -
数据错位(通道对应不上)
检查
ADC_RegularChannelConfig的排序参数:第 1 个通道对应数组[0],第 2 个对应[1],以此类推。 -
查询模式下忘记等待 EOC
若不等转换完成直接读
ADC_DR,得到的是旧值或无效值。使用ADC_GetConversionValue会同时清除 EOC 标志。
五、总结
| 模式 | 适用场景 | 优缺点 |
|---|---|---|
| 普通单次查询 | 低频单通道(如温度巡检) | 简单,但阻塞 CPU |
| 多通道查询 | 不建议实际使用 | 数据极易覆盖,仅作学习 |
| DMA 循环 | 多通道连续采集(推荐) | CPU 零负担,数据准确可靠 |
掌握了普通模式和 DMA 模式的标准库写法后,应对绝大多数 STM32 模拟信号采集需求都将游刃有余。建议先在普通模式下调通单通道,确认硬件没问题,再切换到 DMA 模式,这样可以排除很多基础配置错误。
文中代码均可直接复制到 KEIL 标准库工程中编译运行,若遇到问题,欢迎在评论区讨论交流。
参考资料
- STM32F103x8/xB 数据手册
- RM0008 Reference Manual
- STM32F10x Standard Peripherals Library 使用手册