news 2026/9/10 7:53:07

STM32 ADC电压采集全解析:从SAR原理到DMA多通道实战与滤波校准

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32 ADC电压采集全解析:从SAR原理到DMA多通道实战与滤波校准

STM32C552开发(7)----ADC电压采集

ADC电压采集这个主题,我在这个开发系列的开篇就答应过要单独拿出来细聊,这次兑现。做嵌入式开发只要跟模拟量打交道,ADC就是绕不开的外设。很多人觉得STM32的ADC简单,无非就是初始化、启动转换、读寄存器,但真实项目里遇到的坑远不止这些:读数跳得像心电图的、电压明明很稳采出来却偏着几百毫伏的、换了个信号源数据直接报废的、多通道采集时各通道互相串扰的——这些问题我在C552上全部碰到过一轮。

这篇我把STM32C552的ADC从原理到落地完整走一遍,重点放在三块:第一是C552这颗芯片的ADC硬件特性跟老一代F1/F4有哪些差异,第二是采样时间、校准、时钟这些决定精度的关键参数怎么选,第三是实测过程中遇到的典型问题和排查思路。文章里的代码和配置全是我在C552板子上跑过的,可以直接参考。

1. 搞明白C552的ADC,先说原理和硬件底子

1.1 SAR ADC是怎么工作的

STM32C552内置的ADC是逐次逼近型(SAR ADC)。它内部有一个比较器、一个数模转换器(DAC)和一个逐次逼近寄存器。转换过程可以类比成用天平秤重物:先放一个最大的砝码(比如满量程的一半),如果重物比砝码重就保留这个砝码,否则换更轻的;然后依次尝试更小的砝码,每一轮都根据比较器的结果决定当前位是1还是0。

拿12位ADC来说,一次完整的转换需要12次这样的比较。从最高位开始,逐位逼近输入电压,最终得到12位数字值。这个原理听起来不复杂,但它决定了SAR ADC的若干特性:一是转换速度跟比较器建立时间有关系,二是输入信号在采样保持期间必须足够稳定,否则会比较出错误结果,三是精度受内部DAC线性度和比较器噪声的直接影响。

在C552上,软件配置层面对应的影响是:

  • 采样时间要足够长,让内部采样电容充分充电到输入电压;
  • 参考电压要干净稳定,因为整个转换过程都是拿输入电压跟参考电压做比例比较;
  • 触发转换后,转换期间不要随意切换通道或改配置。

1.2 C552的ADC资源与特性

C552的ADC是12位SAR架构,硬件上带有过采样单元,可以把分辨率往上再叠加。这跟F1时代的ADC比有一处很关键的区别——它支持内置的校准流程,代码里能显式触发偏移校准和线性校准。F103的ADC虽然也有校准位,但普遍被忽略,而C552如果跳过校准步骤,初始偏移可能会明显拉低测量精度。

在引脚资源方面,C552的ADC通道分布在多个GPIO端口上,且与数字功能复用。我刚开始用的时候就在引脚映射上吃过亏,配置了个PA口引脚当成普通输入用,结果IO配置没切成模拟模式,采出来的数据不是0就是满量程。后面第4章讲排查时我会细说这个问题的表现和解决方法。

时钟配置上,ADC模块有独立的时钟分频器,必须工作在允许的频率范围内。C552的ADC时钟上限比F103高不少,但这不代表你可以直接往极限拉——ADC时钟越高,转换时间越短,但内部采样保持电路的建立余量也越小,对信号源内阻的容忍度就越差。合理的做法是先用低速把功能调通,确认数据稳定后再逐步提高时钟。

1.3 内部参考电压VREFINT的正确用法

C552内部集成了高精度参考电压VREFINT,这是它跟老款芯片拉开差距的地方之一。F103时代如果要用精确的ADC基准,通常得在VREF引脚外接一个高精度基准芯片,增加成本不说,PCB上还要专门布局。C552可以直接使用内部参考,省掉外部基准器件。

但有一个细节必须注意:VREFINT不是所有场景下都自动启用的。不同的电源配置模式下,参考电压的来源和特性可能不同。我在调试中遇到的情况是,用内部参考时采集结果的满量程对应电压不是3.3V,而是芯片手册里标注的VREFINT实际值,这个值在不同芯片之间有微小差异。如果你的系统对电压绝对精度有要求,建议做一次单点标定,把芯片实际的满量程系数记录下来,再去做后续的电压换算。

内部参考电压还好用来监测VDDA电源轨是否正常。比如测量一个固定的VREFINT通道,如果读到的值跟预期偏差过大,说明电源供电可能出了问题。这个技巧在设备现场排查时特别有用。

2. 配置阶段别凭惯性,三个参数决定成败

2.1 ADC时钟:不是越快越好

ADC的时钟树在C552上看起来简单,就是一条分频链:APB时钟通过ADC分频器给到ADC内核。但这里藏着一个常见配置误区——很多人直接把分频系数设得很小,让ADC跑在最高频率,理由是转换速度快。实际上ADC模块存在“采样时间最小限制”和“转换周期固定开销”两层约束,单纯提高时钟会导致可配置的采样时间档位变少,反而牺牲了灵活性。

以12位转换为例,一次完整转换需要固定的若干个ADC时钟周期开销量,这部分无法压缩。如果你把ADC时钟拉满,留给采样保持的周期数选择就只剩最小几档,对于高阻抗信号源来说,采样电容来不及充满,读出的数据就会偏低甚至剧烈跳动。反过来,如果你把ADC时钟降一半,可用采样时间档位多了一倍,很多原本难搞的信号源反而能稳定测量了。

我的习惯是:先估算信号源阻抗,高阻抗场合(10k以上)优先保证采样时间,用中等分频即可;只有低阻抗输出(比如运放缓冲后的信号)才考虑拉高ADC时钟追求吞吐率。

2.2 采样时间:精度和速度的平衡

采样时间是ADC配置里对精度影响最大的单个参数。它的物理含义是采样开关闭合期间,内部采样电容对输入电压充电的时间。信号源内阻越大,完成充电需要的时间越长。如果采样时间不够,电容上的电压还没充到输入信号的实际值,采样开关就断开了,后续转换拿到的自然是一个偏低的值。

实际选择采样时间时,我通常会做一个简单估算。STM32系列的ADC输入等效电路中,采样电容典型值在几pF量级,加上引脚和内部走线的寄生电容,总电容通常在10pF上下。充电时间常数跟信号源内阻直接相关:

时间常数约等于信号源内阻乘以采样电容。假设信号源内阻是10kΩ,采样电容按10pF算,时间常数就是0.1微秒。要让采样电容充到0.1%误差以内,需要大概7倍时间常数,那就是0.7微秒。对照不同的ADC时钟频率,一个周期的时长不同,你可以直接算出至少需要多少个采样周期。

我在C552上用的经验值是:普通低阻抗信号源用1.5到7.5个时钟周期就能工作,但为了数据好看,我一般直接给到13.5以上;接高阻抗分压网络或者未经缓冲的传感器输出时,至少给到28.5个周期起。采样时间拉长一倍,读数稳定性提升非常明显,代价仅仅是最大采样率下降,很多应用根本感受不到区别。

2.3 校准:偏移校准和外部三点校准

C552支持芯片内部的自动偏移校准。操作上就是在ADC初始化完成后,调用相应的校准接口,芯片内部会自动完成偏移量的测量和补偿。这个步骤我强烈建议每次上电都执行,成本极低,能消除大部分温漂和芯片个体差异引起的偏移误差。

但内部校准只能解决偏移问题,增益误差(传输曲线斜率偏差)和非线性误差它管不了。如果你要求电压测量的绝对精度比较高,比如万用表对比误差控制在0.5%以内,就需要做外部校准,业内常用的是两点校准或三点校准。

三点校准的做法是:给ADC输入三个已知的电压点,分别覆盖量程的低段、中段和高段,记录对应的ADC读数。然后利用这些数据做线性拟合,得到每个点附近的增益和偏移修正系数。由于ADC的非线性通常在量程两端比较明显,三点校准比两点校准能更好地修正两端的误差。实际处理时,可以简单地对每个区间做分段线性修正,也可以用最小二乘法拟合一条整体直线来求增益和偏移系数。

校准后的电压计算思路是:

整理后的电压 =(ADC读数 - 偏移修正值)× 增益修正系数。

有了这一组系数,后续每次读到的ADC原始值,先带上修正系数计算,再乘以参考电压并除以满量程值,得到的就是比裸读数准得多的电压。

需要提醒的是,外部校准依赖的是标准电压源的准确度,校准前先把万用表跟标准源比对一下,别拿一块没校准的万用表去标定ADC,那就变成拿“不标准”去修正“不标准”了。

3. CubeMX配置与实际代码

3.1 CubeMX图形化配置要点

这一节我把C552上ADC采集的配置步骤走一遍,工具是STM32CubeMX,生成HAL库工程。

第一步,在Pinout视图里把要用于ADC的引脚设置成ADC通道。C552的ADC通道引脚可选范围挺大,选谁你说了算,但要注意别跟其他需要用到的外设引脚冲突。比如我接的是一个分压后的电池电压检测点,用的是ADC1的通道0,对应的引脚在CubeMX上会被标记为ADC1_IN0。

第二步,在Analog选项卡里选择对应的ADC使能,然后进入参数配置页。常规工程的配置参数如下:

  • 分辨率:12位
  • 数据对齐:右对齐
  • 扫描模式:单通道时关闭
  • 连续转换模式:按需求,轮询读取时通常关闭,DMA+定时器触发时按需开启
  • 转换数量:1(单通道)
  • 外部触发:软件触发(最简单)
  • 采样时间:优先生成配置里选择较大值,比如ADC_SAMPLINGTIME_COMMON_1的较高档位
  • 过采样:先关掉,后面需要再加

ADC时钟分频这里,我一般结合工程里的APB时钟设置推算出实际ADC时钟,确认在数据手册允许范围内。CubeMX的时钟树页面也能直接看到计算结果,留意一下就行。

第三步,如果需要用内部参考VREFINT,需要在ADC配置里使能相关选项。CubeMX会在生成的初始化代码里处理好参考电压的使能流程。

第四步,生成代码。CubeMX生成的main函数已经包含MX_ADC1_Init()的调用,初始化顺序在时钟、GPIO之后自动排好。注意几个关键宏定义:ADC1的句柄结构体、ADC_HandleTypeDef、ADC_ChannelConfTypeDef都会在adc.h里声明好。

3.2 单通道电压采集代码

生成代码后,正常的单次转换流程就是“启动转换、等待转换完成、读数值、停止转换”。我在C552上实测可以直接跑通的代码如下:

uint16_t read_adc_single(ADC_HandleTypeDef *hadc) { uint16_t value = 0; HAL_ADC_Start(hadc); if (HAL_ADC_PollForConversion(hadc, 100) == HAL_OK) { value = HAL_ADC_GetValue(hadc); } HAL_ADC_Stop(hadc); return value; }

读取完成后,电压换算这一步要根据实际参考源来定。如果使用3.3V外部参考(VDDA供电作为基准),12位ADC的换算公式是:

电压值 = ADC读数 × 3.3 / 4095.0

如果使用了内部参考VREFINT,满量程值不一定是3.3,要以芯片手册给出的VREFINT标称值为准。我这边测下来,芯片实测满量程约为3.34V左右,换算时用这个值比直接用3.3更接近万用表读数。

在项目的main函数里循环打印采集结果的写法,我测试过可以直接用:

uint16_t adc_val = 0; float voltage = 0.0f; while (1) { adc_val = read_adc_single(&hadc1); voltage = (float)adc_val * 3.3f / 4095.0f; printf("ADC Value: %d, Voltage: %.3f V\r\n", adc_val, voltage); HAL_Delay(500); }

用串口把结果打出来调试是最好使的方式。你注意到没有,我每次读取都调用一次HAL_ADC_Start和HAL_ADC_Stop,这保证了每次转换都是干净的软件触发单次转换,不会因为上次残留的状态影响下一次读数。这种写法牺牲了一点吞吐率,但换来的是代码逻辑清晰稳定,特别适合初学阶段和调试阶段。

3.3 一个稳定好使的软件滤波方案

ADC读数跳变是每个做模拟采集的人都绕不开的问题。硬件上加RC低通滤波自然是治本的办法,但在硬件定型之后,软件滤波是立竿见影的补救手段。

我经常用的是去极值平均滤波:连续采样N次,去掉最大值和最小值,剩下的取平均。中值能剔除偶发的脉冲干扰,平均能压低随机噪声。操作如下:

#define ADC_FILTER_N 11 uint16_t read_adc_filtered(ADC_HandleTypeDef *hadc) { uint16_t buf[ADC_FILTER_N]; uint16_t temp; uint32_t sum = 0; for (int i = 0; i < ADC_FILTER_N; i++) { buf[i] = read_adc_single(hadc); } // 用简单冒泡做一次排序 for (int i = 0; i < ADC_FILTER_N - 1; i++) { for (int j = 0; j < ADC_FILTER_N - 1 - i; j++) { if (buf[j] > buf[j + 1]) { temp = buf[j]; buf[j] = buf[j + 1]; buf[j + 1] = temp; } } } for (int i = 1; i < ADC_FILTER_N - 1; i++) { sum += buf[i]; } return (uint16_t)(sum / (ADC_FILTER_N - 2)); }

N取多少合适?我试过5、7、11、21几档。N越大滤波效果越好,但响应速度也越慢。对电池电压这类缓变信号,11完全够用;对需要快速响应的电流采样,N取5或7更合适。还有一个细节:如果采样频率很低(比如每秒50次),N取21会导致输出滞后超过0.4秒,这在某些控制回路里是不可接受的。

除了去极值平均,滑动平均滤波(EMA)也是ADC后处理的好选择,特别是内存紧张的场景。EMA只需要两个变量就能实现,但调滤波系数比较考验经验,容易在“太滞后”和“滤不干净”之间摇摆。我的建议是,普通产品开发优先用去极值平均,简单、直观、可控。

4. 多通道ADC加DMA的实战方案

4.1 多通道扫描模式的配置

很多场景需要同时采集多路模拟量,比如三相电流、多节电池电压、多个电位器状态。STM32C552的ADC支持扫描模式 + 多通道排序,可以一次性把多个通道依次转换完。

CubeMX里的配置变化不大:

  • 扫描模式:使能
  • 转换数量:按实际通道数设置,比如4
  • 依次配置每个通道的Rank(转换顺序)和采样时间
  • 连续转换模式:DMA配合时通常使能

多通道扫描模式最容易出的问题是转换结果和通道对应关系错乱。ADC按照Rank顺序依次转换,数据在结果寄存器里是一份份排好的,当你用DMA搬运时,数据顺序就是Rank顺序,不是引脚编号顺序。比如你把Rank1配成ADC1_IN3、Rank2配成ADC1_IN0,那DMA搬出来的第一个数据就是IN3的,第二个才是IN0的,这个顺序很多人第一次用都会踩。

多通道扫描还有一个隐含问题:如果各通道的信号源阻抗差异很大,较高的采样时间设置会拖慢整体转换速度,较低采样时间档位又可能导致高阻抗通道数据不准。我的处理办法是:给高阻抗通道单独配置较长采样时间,低阻抗通道用较短采样时间,宁可总耗时高一点,也要保证每一路都准确。

4.2 DMA循环传输与数据解析

多通道单次转换配合DMA,是嵌入式里最常用的ADC采集架构。DMA可以在每次ADC转换完成后自动把结果搬到内存缓冲区,完全不用CPU介入,CPU拿来做什么都行,读取数据只是翻翻内存而已。

在CubeMX里,开启ADC的DMA请求,选择循环模式,数据宽度设为半字(16位,对应12位ADC读数正好)。生成的代码里多出MX_DMA_Init()和HAL_ADC_Start_DMA(&hadc1, buffer, length)调用。

我实际用的DMA缓冲方案是这样:

#define ADC_CHANNEL_NUM 4 uint16_t adc_dma_buf[ADC_CHANNEL_NUM]; HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_dma_buf, ADC_CHANNEL_NUM);

DMA开启后,ADC会在每次扫描完成后,自动把四个通道的转换结果按顺序填入adc_dma_buf数组,整体循环更新。你只需要在应用代码里读取这个数组即可,不用关心ADC转换的时序细节。

这里有个坑:读取数组时要注意数据可能被DMA正在更新。如果在DMA搬运的瞬间读数据,缓冲区可能出现“上一次的数据和本次的数据混在一起”的半新半旧状态。解决办法是:开启DMA传输完成中断,在中断回调里给一个标志位,主循环发现标志位置位后再读取数据;或者用双缓冲,DMA交替写入两个缓冲区,保证读数据的时候缓冲区内容是一致的。

我自己的习惯是用标志位方案,简单可靠:

volatile uint8_t adc_data_ready = 0; void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { adc_data_ready = 1; } } // 主循环中使用 if (adc_data_ready) { adc_data_ready = 0; for (int i = 0; i < ADC_CHANNEL_NUM; i++) { printf("CH%d: %d\r\n", i, adc_dma_buf[i]); } }

DMA + 中断这套组合,比轮询方式在CPU占用率上低了一个量级,长期运行时数据一致性也更好,项目到了后面基本都会切到这种架构上。

5. 实测数据和问题排查记录

5.1 一组实测数据:空载、分压、滤波前后对比

为了写这篇,我在C552板子上做了一组对照测试,用同一个通道、同一个外部信号源,只改变配置参数,观察采集结果的变化。

第一组是不同采样时间下的表现。信号源是一个10kΩ电位器分压电路,输出约1.65V,不滤波直接读原始值:

采样时间档位读到的ADC值换算电压波动范围
1.5周期2010~20501.62~1.65V波动较大
13.5周期2028~20421.635~1.646V波动减小
55.5周期2034~20381.640~1.643V稳定

表格里能看出,高阻抗信号源下采样时间短了数据明显偏低且跳得厉害,拉长采样时间后数据靠近真实值且稳定下来。这个情景在真实项目中太常见了——电位器、NTC热敏电阻分压、光敏电阻分压,全都有高内阻问题。

第二组是滤波前后对比。采样时间固定在13.5周期,信号是锂电池经电阻分压后约3.0V,连续读30次取统计:

  • 未滤波:最小2850、最大2885、方差约120
  • 去极值平均(N=11):最小2862、最大2874、方差约20

滤波之后波动范围直接缩小了一半以上,满足显示类应用完全没压力。当然滤波带来的延迟也要接受,但电池电压检测这类本身就不需要毫秒级响应的场景,这点延迟无感。

5.2 排查实录:读数跳变、0xFFF、通道串扰

问题一:读数全是0xFFF。有一次我换了一个引脚接模拟信号,配置似乎都对,但读回来全是4095。排查了半天发现GPIO模式没有配置成模拟输入。ADC引脚必须设置成模拟模式,HAL库的GPIO_InitStruct里PinMode要写GPIO_MODE_ANALOG,否则引脚保持数字输入状态,采样结果不是0就是满量程。这个问题CubeMX正常配置不会发生,但手动改代码时很容易漏。

问题二:读数跳变幅度大,滤波也没用。有一回我测一个温度传感器信号,去极值平均之后数据还是晃。后来用示波器看引脚波形,发现传感器输出本身叠加了高频噪声,万用表测不出来,但ADC采得到。解决方案是在软件上把采样时间拉到最大、加大滤波深度的同时,硬件上在引脚对地加一个0.1uF到1uF的电容。软件滤波是辅助,本质上要去抑制噪声源头。

问题三:多通道采集,通道之间数据串了。之前提过的Rank顺序问题我实际踩过一次。当时配了四个通道,Rank1是IN0、Rank2是IN1,在主循环里想当然地认为buffer[0]对应IN0、buffer[1]对应IN1,结果发现数据对不上。后来打印每个通道的原始值才发现,DMA buffer的顺序是和Rank一一对应的。排查这种问题最直接的办法是给不同通道接不同电压,然后看打印结果里哪一路对应哪个电压,一目了然。

问题四:用内部参考电压后,满量程值跟3.3对不上。这个是正常现象,前面提过,VREFINT的实际电压不等于VDDA的3.3V。解决办法是在量程的高点做一个标定,获得实际满量程系数,再代入换算公式。我在项目里就是这样处理的,标定之后电压读数跟万用表误差控制在0.3%以内。

5.3 给新手的一点建议

如果你在C552上第一次调ADC,我建议按这个顺序来:先用3.3V供电作为参考、软件触发、单通道、轮询方式读取,把整条链路跑通,确认能读到随外部电压变化的数值;然后再逐步引入校准、多通道、DMA和滤波。一次只加一个变量,出了问题也容易定位。

等我说的这套东西跑熟悉了,再考虑拿ADC做PWM采样、交流信号采集、高速连续转换这些进阶功能。ADC这块的知识体系是层层递进的,底子打牢固了,后面越学越顺。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 7:52:56

基于SpringBoot的游戏集成网站:从源码拆解到部署避坑指南

你知道大学里最常被问的一句话是什么吗&#xff1f;——“你毕设做的啥&#xff1f;”如果你打开过 CSDN、GitHub 或者各种毕设源码平台&#xff0c;大概率见过“基于SpringBootWeb的小游戏集成网站”这样的题目。听起来挺唬人&#xff0c;拆开看其实就是一个简化版的小游戏平台…

作者头像 李华
网站建设 2026/9/10 7:52:52

Zephyr RTOS 离线开发环境完整搭建指南:内网隔离也能编译烧录

Zephyr RTOS 离线开发环境完整搭建指南&#xff1a;内网隔离也能编译烧录 【免费下载链接】zephyr Primary Git Repository for the Zephyr Project. Zephyr is a new generation, scalable, optimized, secure RTOS for multiple hardware architectures. 项目地址: https:/…

作者头像 李华
网站建设 2026/9/10 7:50:51

风光储微电网Simulink仿真建模与控制策略实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 7:49:15

CANN/ge 引擎特性分析

Engine 特性分析 1 特性背景 1.1 问题域 昇腾 AI 处理器是一种异构计算架构&#xff0c;其芯片内部集成了多种不同类型的计算单元——AI Core 负责密集矩阵运算&#xff08;如卷积、MatMul&#xff09;&#xff0c;Vector Core 负责向量运算&#xff08;如 ElementWise&#xf…

作者头像 李华
网站建设 2026/9/10 7:48:19

SpringBoot3整合SpringSecurity6+JWT实现前后端分离认证授权实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华