拓冰建站拓冰建站
首页 / 资讯中心 / 正文

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去极值平均N11最小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这块的知识体系是层层递进的底子打牢固了后面越学越顺。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门