1. 项目概述:从模拟世界到数字世界的桥梁
在嵌入式开发,尤其是基于STM32这类MCU的项目里,ADC(模数转换器)绝对是一个绕不开的核心外设。无论你是想做一个读取电位器旋钮位置的简单控制器,还是一个需要监测电池电压、采集传感器信号(比如温度、光照、压力)的复杂物联网节点,ADC都是连接现实模拟世界与MCU数字世界的唯一通道。我刚开始接触STM32时,对ADC的理解也仅仅停留在“读取电压值”的层面,直到在实际项目中踩过几次坑——比如采样值跳动剧烈导致控制不稳,或者采样时机不对错过关键信号——才真正明白,用好ADC远不是调用一个HAL库函数
HAL_ADC_GetValue()
那么简单。
这篇笔记,我就结合自己这些年做过的几个典型项目,从最基础的原理到实际开发中那些容易忽略的细节,系统地梳理一下STM32中ADC的使用。我们会聊到如何根据需求选择合适的ADC工作模式(单次、连续、扫描、间断),如何利用DMA来解放CPU,如何处理采样数据(滤波算法),以及如何通过定时器来精准控制采样节奏。这些内容不仅仅是寄存器配置,更多的是在工程实践中积累下来的“手感”和“避坑指南”。无论你是刚入门的新手,还是想深化理解的开发者,希望这些实实在在的经验能帮你把ADC这个工具用得更加得心应手。
2. ADC核心概念与STM32的ADC外设解析
2.1 ADC到底是什么?为什么它如此关键?
简单来说,ADC就是一个“翻译官”。我们身处的物理世界,绝大多数信号都是连续变化的模拟量,比如温度从20度缓慢升到25度,音量大小连续可调。但微控制器(MCU)如STM32,其内核是数字的,只能处理“0”和“1”这样的离散数字量。ADC的职责,就是将连续的模拟电压信号(例如0V到3.3V),按照一定的规则,转换成MCU可以理解和处理的数字值(例如0到4095)。
这个过程的核心参数有三个: 分辨率 、 采样率 和 参考电压 。
- 分辨率 :决定了ADC有多“细腻”。常见的12位ADC,意味着它能把参考电压范围分成 2^12 = 4096 个等级。对于3.3V的参考电压,每个数字等级(1LSB)代表的电压是 3.3V / 4096 ≈ 0.8mV。分辨率越高,对电压变化的感知就越灵敏。
- 采样率 :指ADC每秒钟能完成多少次“翻译”(转换)。它决定了系统能捕获多快变化的信号。根据奈奎斯特采样定理,要无失真地还原一个信号,采样率至少需要是信号最高频率的两倍。对于音频信号(20kHz),采样率通常需要大于40kHz。
- 参考电压(Vref+) :这是ADC转换的“标尺”。ADC输出的数字值,表示的是输入电压相对于参考电压的比例。STM32的Vref+通常可以接外部精密基准源,或者直接使用VDDA(模拟电源)。 这里有一个非常重要的注意事项 :VDDA的稳定性直接决定了ADC的精度。如果VDDA随着负载波动,那么你的采样值也会飘。在要求高的场合,务必为VDDA提供干净、稳定的电源,并确保Vref+连接正确。
STM32的ADC外设功能非常强大且灵活。以STM32F1和F4系列常见的12位逐次逼近型ADC为例,它支持多通道(16个外部通道+2个内部通道),支持单次、连续、扫描、间断等多种转换模式,并且可以与DMA、定时器无缝协作,几乎能满足大部分中低速信号采集的需求。
2.2 STM32 ADC的几种关键工作模式剖析
理解并选对工作模式,是高效使用ADC的第一步。很多人一开始只用单次转换,遇到多通道或连续采样需求时就抓瞎了。
-
单次转换模式 :这是最简单的模式。启动一次,转换一个指定的通道(或一组通道)一次,然后ADC就停止并等待下一次启动。它适用于不频繁的、手动触发的采样,比如按键按下时才读取一次电位器值。优点是省电,逻辑简单。
-
连续转换模式 :一旦启动,ADC就会马不停蹄地对一个通道进行连续转换。每次转换结束后立即开始下一次,直到被停止。这种模式适合需要对单一信号进行持续监控的场景。但要注意,它只针对一个通道,要切换通道需要重新配置或使用扫描模式。
-
扫描模式 :这是处理多通道采集的利器。在扫描模式下,ADC会按照预先配置好的通道序列(比如通道1, 5, 8, 11),自动地一个接一个进行转换。 扫描模式通常需要与DMA或中断结合使用 ,因为ADC转换完序列里的所有通道后,会产生一个“序列转换完成”事件,你需要在这个事件里及时把数据从ADC数据寄存器(DR)里搬走,否则会被下一次转换覆盖。
-
间断模式 :这个模式比较特殊,它允许你将一个长的通道序列分成若干个子组。每次由一个外部事件(如定时器触发)来启动一个子组的转换。这在需要将多个通道的采样与某个特定事件严格同步时非常有用,比如与PWM输出同步采集电流。
实操心得 :对于大多数多通道数据采集应用,我的首选组合是 “扫描模式 + 连续转换模式 + DMA” 。这样配置后,ADC会自动、循环地采集所有你关心的通道,并通过DMA自动将数据搬运到你指定的内存数组中。CPU完全不用干预,只需要去数组里读取最新的数据即可,效率极高,实时性也好。在CubeMX里配置好这个组合,你的主循环会清爽很多。
3. 基于CubeMX的ADC配置与初始化详解
现在,我们以STM32F407为例,使用STM32CubeMX图形化工具,一步步配置一个“三通道扫描连续采集+DMA传输”的典型场景。假设我们要采集通道1(PA1)、通道2(PA2)、通道5(PA5)上的电压。
3.1 CubeMX工程基础配置
首先,新建工程,选择你的芯片型号。在
Pinout & Configuration
视图下:
-
在左侧边栏找到
Analog下的ADC1。 -
在
ADC1的设置中,进入Parameter Settings标签页。-
Resolution:选择12-bit。根据需求也可选10位、8位或6位,位数越低转换越快。 -
Scan Conversion Mode:选择Enabled。这是启用扫描模式的关键。 -
Continuous Conversion Mode:选择Enabled。启用连续转换。 -
DMA Continuous Requests:选择Enabled。这保证了DMA请求是连续的,与ADC的连续转换模式匹配。 -
End Of Conversion Selection:选择EOC after each conversion(每次转换后产生EOC)或EOC after sequence(序列转换后产生EOC)。对于DMA搬运,通常选EOC after each conversion,这样每转换完一个通道就触发一次DMA请求,搬运一次数据。
-
-
进入
DMA Settings标签页,点击Add添加一个DMA请求。Mode通常选择Circular(循环模式),这样DMA会周而复始地搬运数据,填满缓冲区后自动回到开头,形成一个自动更新的数据池。Increment Address根据你的存储数组来选择,如果数据存到一个数组里,Destination的地址需要自增。 -
进入
NVIC Settings标签页,如果你还需要在转换完成时做额外处理(比如数据就绪标志),可以开启ADC全局中断。对于纯DMA方案,中断不是必须的。 -
最后,在图形化引脚图上,找到PA1, PA2, PA5,将它们分别设置为
ADC1_IN1,ADC1_IN2,ADC1_IN5。
3.2 通道配置与采样时间设定
在
ADC1
的
Configuration
界面,点击
Channel Settings
,会看到我们使能的三个通道。这里需要配置两个关键参数:
Rank
(次序)和
Sampling Time
(采样时间)。
- Rank :这就是扫描的先后顺序。你可以通过上下箭头调整,比如设置Channel1为Rank1, Channel2为Rank2, Channel5为Rank3。ADC就会按照1->2->5的顺序循环采样。
-
Sampling Time(采样时间)
:这是最容易出问题的地方之一。采样时间太短,ADC内部的采样电容来不及充到外部信号的电压,会导致转换结果不准确;时间太长,又会限制整体的采样率。采样时间以ADC时钟周期为单位。STM32F4的ADC时钟最高可达36MHz(取决于APB2分频)。对于信号源阻抗较高的情况(比如通过一个很大的电阻连接),需要增加采样时间。一个经验值是,如果信号源阻抗在10kΩ以下,使用
15 Cycles或28 Cycles通常就够了。如果不确定,可以先设长一点(如84 Cycles或112 Cycles)确保精度,再根据实际波形和需求调整。
注意事项 :ADC的输入引脚阻抗是有限的。如果外部信号源的内阻很大,与ADC内部的采样电容会形成一个RC充电电路。如果采样时间不足,电容电压还没稳定到信号电压,转换就开始了,结果必然偏低。这就是为什么直接读取电位器中间抽头(电位器本身阻抗大)时,如果采样时间设置过短,值会跳得很厉害的原因。解决办法一是增加采样时间,二是在ADC输入前加一个电压跟随器(运算放大器)进行缓冲,降低输出阻抗。
生成代码后,CubeMX会帮我们初始化好ADC和DMA。我们需要在用户代码区做一点补充。
4. 数据采集、DMA传输与软件滤波实战
4.1 启动ADC与DMA,并管理数据缓冲区
在
main.c
的
/* USER CODE BEGIN PV */
区域,我们定义存储数据的数组和相关的变量。
/* Private variables ---------------------------------------------------------*/
#define ADC_CHANNEL_NUM 3 // 我们使用了3个通道
uint16_t adc_raw_buffer[ADC_CHANNEL_NUM]; // 用于存放DMA搬运的原始数据
uint32_t adc_filtered_values[ADC_CHANNEL_NUM] = {0}; // 用于存放滤波后的值
// 可以再定义一个数组用于存放计算后的电压值
float adc_voltage[ADC_CHANNEL_NUM];
在
/* USER CODE BEGIN 2 */
区域,启动ADC和DMA。
/* USER CODE BEGIN 2 */
// 启动DMA传输,将ADC转换结果自动搬运到adc_raw_buffer数组
// 参数:ADC句柄,目标地址,数据长度(注意是字长,对于12位ADC,数据寄存器是16位的)
if (HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_raw_buffer, ADC_CHANNEL_NUM) != HAL_OK)
{
Error_Handler();
}
/* USER CODE END 2 */
这样,ADC就会自动开始工作,并且每转换完一个通道(Rank),DMA就会把数据从
ADC1->DR
寄存器搬到
adc_raw_buffer
数组的对应位置。由于DMA是循环模式,它会不断覆盖这个数组,因此
adc_raw_buffer
里永远是最新的三个通道的原始数据。
4.2 必须掌握的软件滤波算法
直接从ADC读出来的原始值(
adc_raw_buffer
)通常是带有噪声的,直接使用可能会导致控制抖动或显示数字乱跳。因此,软件滤波是必不可少的一步。这里介绍几种最常用且有效的算法。
-
算术平均滤波 :最简单,连续采样N次取平均值。能有效抑制周期性干扰,但对突发性干扰(尖峰)的抑制能力弱。
#define FILTER_N 10 uint32_t adc_sum[ADC_CHANNEL_NUM] = {0}; uint8_t filter_index = 0; uint16_t adc_history[ADC_CHANNEL_NUM][FILTER_N]; // 历史数据队列 // 在定时中断或主循环中调用 void ADC_Filter_Average(void) { for(int ch=0; ch<ADC_CHANNEL_NUM; ch++) { // 1. 减去最旧的数据 adc_sum[ch] -= adc_history[ch][filter_index]; // 2. 存入最新的数据 adc_history[ch][filter_index] = adc_raw_buffer[ch]; // 3. 加上最新的数据 adc_sum[ch] += adc_raw_buffer[ch]; // 4. 计算平均值 adc_filtered_values[ch] = adc_sum[ch] / FILTER_N; } filter_index = (filter_index + 1) % FILTER_N; // 移动索引 } -
中值滤波 :连续采样N次(N为奇数),将这N个值排序,取中间值作为结果。 对脉冲性干扰(比如开关造成的毛刺)有非常好的滤除效果 ,是硬件滤波的软件补充。我常在读取按键或存在瞬间干扰的传感器时使用。
#define MEDIAN_N 5 uint16_t get_median_value(uint16_t* arr, uint8_t n) { uint16_t temp[MEDIAN_N]; memcpy(temp, arr, sizeof(uint16_t)*n); // 简单的冒泡排序找中值 for(int i=0; i<n-1; i++) { for(int j=0; j<n-i-1; j++) { if(temp[j] > temp[j+1]) { uint16_t t = temp[j]; temp[j] = temp[j+1]; temp[j+1] = t; } } } return temp[n/2]; } // 使用时,需要维护一个长度为MEDIAN_N的历史数组,每次调用此函数求中值。 -
一阶滞后滤波(低通滤波) :适用于变化缓慢的参数,如温度。公式:
本次滤波结果 = α * 本次采样值 + (1-α) * 上次滤波结果。α是滤波系数(0<α<1),α越小,滤波越平滑,但滞后越严重。float alpha = 0.2; // 滤波系数,可调 for(int ch=0; ch<ADC_CHANNEL_NUM; ch++) { adc_filtered_values[ch] = alpha * adc_raw_buffer[ch] + (1-alpha) * adc_filtered_values[ch]; }
实操心得 :不要追求“最完美”的滤波算法,而是选择“最合适”的。 我的常用组合是:中值滤波 + 算术平均滤波 。先用中值滤波(比如采样5次取中值)干掉可能出现的尖峰脉冲,然后再对这个中值结果进行多次平均,让曲线更平滑。这个组合在大多数工业测量场景下都非常稳健。滤波次数N的选择也需要权衡:N越大越平滑,但响应速度越慢。你需要根据信号的实际变化频率来调整。
4.3 将数字量转换为实际物理量
得到稳定的滤波值后,我们需要将其转换为有意义的电压或物理量。转换公式很简单:
电压值 = (ADC滤波值 / 满量程数字) * 参考电压
对于12位ADC,满量程数字是4095(2^12 - 1)。假设Vref+接的是3.3V。
#define VREF 3.3f
#define ADC_MAX 4095.0f
for(int ch=0; ch<ADC_CHANNEL_NUM; ch++) {
adc_voltage[ch] = (adc_filtered_values[ch] / ADC_MAX) * VREF;
}
如果你的传感器是线性变化的,比如某压力传感器输出0.5V~2.5V对应0~100kPa,那么再通过一次线性映射即可得到压力值:
float voltage_min = 0.5f;
float voltage_max = 2.5f;
float pressure_min = 0.0f;
float pressure_max = 100.0f;
float pressure;
if (adc_voltage[ch] < voltage_min) pressure = pressure_min;
else if (adc_voltage[ch] > voltage_max) pressure = pressure_max;
else {
pressure = pressure_min + (adc_voltage[ch] - voltage_min) * (pressure_max - pressure_min) / (voltage_max - voltage_min);
}
5. 高级应用:定时器触发与注入通道
5.1 使用定时器精确控制采样率
在“扫描+连续+DMA”模式下,采样率是由ADC的转换时间决定的。转换时间 = 采样时间 + 12.5个时钟周期(12位转换时间)。这个速率是固定的。但有时我们需要ADC的采样动作与外部事件严格同步,比如:
- 每1毫秒准时采集一次。
- 在电机驱动中,需要在PWM波形的特定时刻(如上桥臂打开的中点)采样电流,以计算相电流。
这时就需要用到 外部触发转换 功能。STM32的ADC可以由多种内部定时器(TIMx的TRGO事件)来触发。配置步骤如下:
-
在CubeMX的ADC配置中,将
External Trigger Conversion Source设置为某个定时器事件,例如Timer 2 Trigger Out event。 -
将
External Trigger Conversion Edge设置为上升沿、下降沿或双边沿触发。 -
关键:将
Continuous Conversion Mode改为Disabled。因为我们现在希望由定时器来控制每次转换的启动。 - 配置好对应的定时器(如TIM2),设置其更新频率(ARR和PSC值)为你期望的采样频率,并使能其TRGO输出。
这样,定时器就会按照设定的周期,定时产生一个触发信号给ADC,ADC收到触发信号后,启动一次完整的扫描序列(或多个通道)。这种方式实现了 等间隔采样 ,对于后续进行数字信号处理(如FFT)至关重要。
5.2 注入通道:应对高优先级信号的插队机制
STM32的ADC除了常规的规则通道序列(我们前面一直在用的),还有一个叫做“注入通道”的机制。你可以把它理解为一个高优先级的“VIP通道”。
规则通道序列就像排队办理业务,按顺序来。而注入通道则像紧急事件,可以打断当前的规则序列转换。当注入通道被触发(可以由另一个定时器或外部引脚触发)时,ADC会立即暂停当前的规则序列转换,转而去转换配置好的注入通道序列,转换完成后,再自动回到被打断的规则序列继续。
典型应用场景 :在电机FOC控制中,规则通道用于常规的电压、温度采样,而注入通道则专门用于由PWM定时器触发的、在特定时刻对三相电流进行同步采样。这保证了电流采样的时刻精度,不受规则通道转换进度的影响。
配置注入通道也在CubeMX的ADC配置界面完成,有独立的
Injected Conversions
配置栏,可以设置注入通道的数目、序列、采样时间以及触发源。
6. 常见问题、调试技巧与精度提升
6.1 ADC采样值不稳定、跳动大
这是最常见的问题。除了前面提到的增加采样时间、降低信号源阻抗、添加硬件滤波(在ADC输入引脚对地加一个10nF~100nF的电容)外,还需要检查以下几点:
- 电源噪声 :用示波器测量VDDA和VSSA(模拟地)的波形,看是否有明显的毛刺或纹波。确保模拟电源部分使用了磁珠或电感与数字电源隔离,并搭配了足够的去耦电容(如10uF钽电容 + 100nF陶瓷电容)。
- 地线问题 :确保模拟地(AGND)和数字地(DGND)是单点连接的。PCB布局时,模拟部分的地回路要尽量短、粗,避免数字电流在模拟地路径上产生压降。
- 参考电压噪声 :如果使用了外部参考电压芯片,确保其输出干净稳定。
- 软件滤波不足 :尝试增加滤波算法的窗口大小或调整滤波系数。
6.2 DMA数据错位或溢出
在扫描多通道时,有时会发现DMA缓冲区里的数据顺序不对,或者数据混乱。
-
检查DMA配置
:在CubeMX中,确认DMA的
Data Width(数据宽度)与ADC的数据宽度匹配(都是Half Word,16位)。确认Memory Increment Address(内存地址自增)已开启,且Peripheral Increment Address(外设地址自增)已关闭(因为ADC数据寄存器地址固定)。 -
检查缓冲区大小
:DMA的
Data Length(数据长度)应设置为 通道数 ,而不是字节数。例如3个通道,就设为3。DMA会搬运3次(每次16位)后完成一个循环。 - 检查中断冲突 :如果同时开启了ADC的DMA和ADC自身的转换完成中断,要小心处理中断服务程序,避免在DMA搬运过程中访问数据缓冲区造成冲突。对于纯DMA方案,可以关闭ADC中断。
6.3 如何校准ADC以提高精度
STM32的ADC内置了校准功能,可以修正其内部的电容误差。 上电后,在第一次使用ADC前,执行一次校准是很好的习惯 。
HAL_ADCEx_Calibration_Start(&hadc1); // 对于单端输入
// 或者对于差分输入
// HAL_ADCEx_Calibration_Start(&hadc1, ADC_SINGLE_ENDED);
校准过程会消耗一些时间(几十到几百微秒),校准系数会被自动存入ADC的内部寄存器。注意,校准应在ADC完成初始化、且稳定供电后进行。
6.4 使用过采样提升有效分辨率
如果你的信号变化很慢,但需要比12位更高的分辨率(比如用于高精度电子秤),可以使用过采样技术。原理是通过以高于奈奎斯特频率的速率进行多次采样并求和平均,可以将分辨率提高几位。例如,4倍过采样可以将有效分辨率提高1位,16倍过采样可提高2位。STM32的ADC硬件直接支持过采样功能,可以在CubeMX中配置过采样系数和右移位数,硬件会自动完成累加和平均,输出一个更高位数的结果,这比软件过采样效率高得多。
调试ADC时,最有力的工具就是示波器和逻辑分析仪。用示波器看输入信号的波形和噪声,用逻辑分析仪抓取定时器的触发信号和ADC的转换完成信号(EOC),可以清晰地看到采样是否按时发生,时序是否正确。当遇到奇怪的问题时,回归到硬件信号层面去观察,往往能快速定位问题根源。ADC的配置选项繁多,第一次接触可能会觉得复杂,但一旦理解了每种模式背后的设计意图,并亲手调试通过一两个完整的多通道采集项目,它就会成为你嵌入式工具箱里最得心应手的工具之一。
589



被折叠的 条评论
为什么被折叠?



