把工程里的串口、时钟、GPIO都跑通之后这次正好轮到 ADC 电压采集。手头这块 STM32C542R 是 Cortex-M33 内核的新一代片子ADC 模块跟老 F1 相比强了不少硬件过采样、多通道扫描、DMA 搬运都是标配。这篇文章就把我在 STM32C542R 上做 ADC 电压采集的完整过程记录下来从采样原理、参数计算到 CubeMX 配置、实际代码再到数据滤波和避坑一条龙讲清楚。ADC 电压采集能做的事太多了电池电压检测、电位器旋钮、模拟传感器光敏、热敏、力矩、电流采样等本质上都是先把物理量变成电压再用 ADC 量化成数字。只要把这套 ADC 采集流程吃透后面换传感器、加通道都是套模板的事。整篇文章偏实操但也把该讲的“为什么”都讲了新手照着做能跑通老手也能在滤波和布局思路上有些收获。1. 先把ADC这回事想明白方案选择与参数计算1.1 STM32C542R的ADC到底能干什么ADC 全称 Analog-to-Digital Converter就是把连续变化的模拟电压转成离散的数字值。STM32C542R 内部集成的 ADC 是逐次逼近型SAR结构工作原理可以类比成用天平称重量先放一个半量程的砝码比较电压是高了还是低了再决定下一步加砝码还是减砝码如此反复一步步逼近真实值。12 位分辨率意味着有 2 的 12 次方也就是 4096 个量化台阶如果参考电压是 3.3V每一个数字量对应约 0.806mV 的电压变化。Cortex-M33 内核的主频比老一代 F1 高出一大截ADC 的转换速度也跟着上来了。更重要的是这个 ADC 支持扫描模式可以把多个通道排成一队自动轮流转换配合 DMA 直接搬运到内存CPU 只管最后取数这对多路传感器采集是刚需。硬件过采样功能也值得提一句把多次转换结果累加移位等效提升分辨率有时候比软件滤波更省心。这套 ADC 能覆盖的场景非常广。工业上常见的 4-20mA 电流环采样先经过采样电阻转成电压再进 ADC消费类产品里的电池电量检测用两个电阻分压后送到 ADC 引脚电机控制里母线电压检测、相电流采样也是 ADC 的高频应用场景。可以说只要 MCU 需要“感知”外部模拟世界第一步基本都是过 ADC。1.2 电压采集方案的几个关键取舍拿到一个电压采集需求先别急着写代码第一步是确定量程和参考。STM32C542R 的 ADC 输入范围是 0 到 VDDA通常就是 3.3V超过这个范围就必须先做信号调理。如果采集的是 12V 电池电压直接怼到引脚上芯片当场就冒烟了正确做法是加电阻分压比如 10k 和 3.3k 串联把 12V 分到 3V 左右再进 ADC。第二个取舍是用内部 ADC 还是外部 ADC。内部 ADC 的优势是零额外成本、布线简单、读取速度够快适合大多数嵌入式场景外部 ADC比如 ADS1115、ADS1256的优势是分辨率更高、精度更好、参考源更干净适合称重、温度采集等对精度要求苛刻的场合。我做 STM32C542R 的电压采集直接就用片内 ADC理由很简单12 位分辨率配合硬件过采样对于常规电压监控和传感器采样已经够用没必要把系统搞复杂。第三个取舍是单次转换还是连续转换。单次转换适合“按需读取”的场景比如用户按一下按键才读一次连续转换配合 DMA 适合需要周期性监控的场合比如实时电压监测、音频采样。实际项目里连续转换加 DMA 是更常用的组合因为 CPU 参与得越少系统越稳定。第四个容易被忽略的点是采样保持时间。ADC 内部有一个采样电容接通外部信号后需要一定时间让这个电容充到和输入电压一致。如果信号源内阻很大采样时间不够电容充不满采集出来的值就会偏低。这个参数在 CubeMX 里是可以配置的选短了速度快但精度差选长了精度好但吞吐率低需要根据信号源阻抗来平衡。1.3 采样周期与电压换算的数学基础采样周期和数据换算这两个概念搞明白了 ADC 就算入门一半。采样周期在 STM32 里由两部分组成采样阶段耗时加上逐次逼近转换耗时。假设配置了 19.5 个 ADC 时钟周期的采样时间12 位分辨率下转换阶段固定需要 12.5 个周期SAR 每一位比较一次那么单次转换总共消耗 19.5 12.5 32 个 ADC 时钟周期。举个例子如果 ADC 时钟是 36MHz实际最高频率以数据手册为准那么一次转换耗时就是 32 / 36MHz ≈ 0.89 微秒。如果换成 601.5 周期的超长采样时间一次转换大约 17 微秒适合信号源内阻很大的场景。电压换算公式非常简单电压 ADC值 / 4096 * 参考电压比如参考电压 VREF 3.3VADC 读出 2048电压就是 2048 / 4096 * 3.3 1.65V。如果读出来是 1000电压就是 1000 * 3.3 / 4096 ≈ 0.8057V。理论上满量程对应 4095也就是 3.3V 左右实际由于参考电压的精度、ADC 的增益误差和偏移误差读到的值往往有些偏差这就是后面第 4 章要讲的校准问题。还有一个公式是分压电路的反推如果前端是 10k 和 3.3k 分压那么实际输入电压 测得电压 * (10k 3.3k) / 3.3k。做电池电压检测时这个反推几乎每次都要用建议封装成一个函数。2. 硬件设计与PCB布局精度从电路板开始2.1 输入前端电路分压、RC滤波、保护ADC 的精度硬件电路至少占一半。采样前端处理不好软件怎么写都救不回来。首先是分压电路。电阻分压看似简单实际有几个讲究一是分压电阻的精度普通的 ±5% 贴片电阻会引入至少 5% 的误差如果做精度要求高的采集要选 ±1% 甚至更高精度的电阻二是分压后等效源阻抗变大10k 和 3.3k 并联等效源阻抗大约是 2.49k这个值会直接影响采样保持时间的选择后面要配套拉长采样周期。其次是 RC 滤波。ADC 输入引脚上串联一个电阻、对地接一个电容组成一阶低通滤波把高频噪声滤掉。典型取值是 R 1kΩC 100nF截止频率约为 1 / (2πRC) ≈ 1.6kHz。如果采集的是缓慢变化的直流信号这个截止频率绰绰有余如果采集的是几十 kHz 的交流信号RC 参数要相应调整避免把有用信号滤掉。RC 滤波还有个隐形好处是抑制采样瞬间的电荷注入。ADC 采样电容从外部汲取电荷时如果串联电阻太大会形成电压降落导致读数偏低并联的电容就像一个“蓄水池”在采样瞬间提供电荷减小电压跌落。第三是输入保护。如果 ADC 引脚有可能接到外部接口、航空插头或者长线缆建议在输入端并联 TVS 管或稳压管防止静电或者浪涌打坏芯片 ADC 内部结构。PCB 上还要注意走线尽量短别把 ADC 输入信号线和 PWM、串口这类高频数字信号并行走太远否则串扰会直接体现在采样噪声上。2.2 规避时钟抖动与电源噪声的3个PCB布局要点关于 ADC 外围电路网上讨论最多的是三个 PCB 布局要点我自己踩过坑之后觉得确实重要在这里展开讲一下。要点一参考电压 VREF 和模拟电源 VDDA 的去耦电容必须紧贴引脚回流路径要短。ADC 转换过程中内部比较器需要参考电压保持稳定如果 VREF 引脚上的去耦电容离得太远走线电感会让参考电压在高频下波动这相当于在基准上叠加了噪声直接污染转换结果。正确的做法是 1uF 和 100nF 两个电容并联放在 VREF 引脚 3mm 以内电容地端通过过孔直接下到模拟地平面不要绕路。要点二ADC 输入信号线要短、直、远离数字信号线。STM32 芯片上本身有数字开关噪声当 GPIO 翻转或者时钟翻转时会产生快速的电流变化这些噪声通过空间辐射和地弹耦合到输入线上。如果 ADC 输入走线在 PCB 上和串口 TX 线平行走了三四厘米测出来的波动会让你怀疑是不是代码写错了。布局时宁可绕一点也要让输入线单独走两侧尽量铺模拟地做屏蔽。要点三模拟地和数字地要单点连接不要让数字回流电流穿过模拟区域。很多人把板上所有地都直接连在一起实际上数字电路开关时的大电流会在地平面产生噪声ADC 模拟部分如果共用这段噪声地参考电压和输入信号都会受影响。常规做法是把模拟部分和数字部分的地分开铺在 ADC 芯片附近或者电源入口用磁珠或 0 欧电阻单点连接。这个点连好了采样数据的稳定性会明显上一个台阶。2.3 实物接线清单与参考电压处理我在这次实验中用的引脚是 PC0 作为 ADC 输入通道具体通道号以所用型号的引脚定义为准3.3V 供电板载基准电压。接线是这样的器件接法电位器/传感器信号串联 1kΩ 电阻接 PC0PC0 对地并联 100nF 电容VREF1uF 100nF 去耦到地VDDA磁珠连接 VDD 3.3V并 1uF 100nF 去耦模拟地/数字地单点连接实测下来这个小配置跑起来很稳采集到的数据原始值波动范围在 ±3 LSB 以内。如果你的板子没有单独的 VREF 引脚说明 VREF 已经在芯片内部和 VDDA 绑定了这时候 VDDA 的去耦就更重要必须单独走线、单独去耦不要偷懒和数字电源混在一起。3. CubeMX配置与代码实现一条龙跑通ADC3.1 时钟树配置先把ADC的时钟喂对ADC 要想工作第一步是保证 ADC 时钟正确。在 CubeMX 的 Clock Configuration 页面里找到 ADC 相关的时钟源配置项确认 ADC 时钟没有超过芯片数据手册规定的最大频率。ADC 时钟从系统时钟经过分频得到分频系数可选 1、2、4、6、8 等具体可选值以 CubeMX 下拉菜单为准。通常把 ADC 时钟降到一个比较安全的频段比如 30-40MHz 左右既能保证高速采样又不会因为时钟太快导致精度下降。这里有一个常见误区ADC 时钟不是越快越好。SAR ADC 的逐次逼近过程需要内部电容阵列有足够的时间稳定时钟过快会导致电容没有完全建立就进入下一次比较精度反而下降。如果对精度敏感宁可选低一点的 ADC 时钟配合合适的采样时间效果比盲目拉高时钟好得多。3.2 CubeMX里ADC的参数设置在 CubeMX 的 Pinout Configuration 页面找到 ADC1按下面的思路配置分辨率选 12 位这是这个系列 ADC 的常规满分辨率选项。数据对齐方式选右对齐因为左对齐一般用于 DMA 传输低位裁剪的场合右对齐处理起来更直观。扫描模式下如果只用一个通道直接选 Disabled 即可多通道采集时再开启扫描。连续转换模式看需求单次触发选 Disabled想要一轮接一轮地自动转换就选 Enabled。我用 DMA 做连续采集所以开了连续转换模式。采样时间这里重点说一下。CubeMX 会提供好几个选项比如 1.5、2.5、7.5、19.5、39.5、92.5、247.5、601.5 个周期等等。选择的原则是信号源阻抗越大采样时间要越长。如果信号就是 3.3V 直接分压过来的源阻抗通常几 kΩ 以内选 19.5 或 39.5 周期足够如果信号来自运算放大器输出低阻抗选短一点的采样时间也没问题如果信号源是纯高阻输出比如某些 pH 传感器、光电二极管前端那就必须选 601.5 周期宁可慢不能错。DMA 设置里ADC1 的 DMA Request 选择 Enabled模式选 Circular 循环模式这样 ADC 每一轮转换完会自动触发 DMA 搬运CPU 完全不用管数据搬移。数据宽度按半字Half Word设置因为 12 位 ADC 结果占 16 位。最后打开 ADC 的校准功能。CubeMX 里如果能看到校准选项直接启用如果没看到代码里手动调用校准函数后面还会提到。3.3 代码实现链路单通道与DMA多通道CubeMX 生成工程之后逻辑代码主要在 user 代码区里补。最简单的单通道非中断读取代码如下#include main.h ADC_HandleTypeDef hadc1; UART_HandleTypeDef huart2; uint16_t adc_raw; float adc_voltage; void Read_ADC_Voltage(void) { HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 10); adc_raw (uint16_t)HAL_ADC_GetValue(hadc1); adc_voltage (float)adc_raw * 3.3f / 4096.0f; HAL_ADC_Stop(hadc1); }这种“启动-等待-读取-停止”的方式代码简单但每次读取都要把时序走到位效率一般适合低频读取场景。做连续监控、多通道采集我更推荐 DMA 方式。DMA 方式的核心思路是定义一块缓冲区ADC 采集的数据自动存入缓冲区主循环只负责从缓冲区取值做处理和换算。代码结构如下#define ADC_CHANNEL_COUNT 2 // 采集通道数 #define ADC_SAMPLE_DEPTH 16 // 每个通道累加采样次数用于软件滤波 uint16_t adc_buf[ADC_CHANNEL_COUNT * ADC_SAMPLE_DEPTH]; void MX_ADC1_Init_DMA(void) { // CubeMX已生成底层配置这里只启动 HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf, ADC_CHANNEL_COUNT * ADC_SAMPLE_DEPTH); } float Get_Channel_Voltage(uint32_t ch) { uint32_t sum 0; for (uint32_t i 0; i ADC_SAMPLE_DEPTH; i) { sum adc_buf[ch i * ADC_CHANNEL_COUNT]; } float avg (float)sum / ADC_SAMPLE_DEPTH; return avg * 3.3f / 4096.0f; }这里有几个细节必须注意。第一缓冲区数据类型是 uint16_tDMA 配置为半字传输两者必须匹配如果缓冲区定义成 uint32_t 而 DMA 配置半字数据会错位。第二DMA 循环模式下缓冲区会被 ADC 不断覆盖所以主循环读数据时要么加临界保护要么设计成双缓冲交替处理避免读到一半被覆盖。第三DMA 中断里不要做浮点运算浮点运算耗时较长会拖累后续数据搬运。实际项目里我一般把 Get_Channel_Voltage 放到定时器周期中断里调用每 10ms 刷新一次电压值主逻辑要用数据时直接访问这个全局变量互不干扰。3.4 校准是必须的吗STM32 的 ADC 内部有校准机制启动时会对内部电容阵列的偏差做一次自校准生成一个校准因子写入寄存器。这块芯片上电后如果不做校准转换结果可能会存在几个 LSB 到十几个 LSB 的偏移误差这在电压精度要求高的场合是不能接受的。CubeMX 生成的外设初始化代码里一般会自动调用校准函数。如果发现你的工程里没调用可以在启动 ADC 之前手动补上HAL_ADCEx_Calibration_Start(hadc1);校准函数要在 ADC 时钟稳定之后、开始转换之前调用。如果换了采样时间配置一般建议重新校准一次。另外需要注意的是每次 MCU 上电温度不同校准因子也会变所以校准应该在运行时执行而不是只在出厂时校准一次。4. 数据不稳定的解法软件滤波与矫正4.1 采集值跳变的噪声来源ADC 原始值跳来跳去是最常见的现象也是新手最容易慌的问题。先别急着改代码把噪声来源理一遍。最大的一类来源是电源和参考电压噪声。MCU 的数字部分每时每刻都在翻转工作电流随之波动如果模拟电源没处理好ADC 参考电压就会跟着抖动转换结果自然不稳定。这类问题最有效的解决手段在前面硬件章节已经讲了去耦电容、模拟地和数字地单点连接、参考电压独立走线。第二类是空间电磁干扰。电机、继电器、开关电源这些强干扰源周围ADC 输入线上会感应出噪声。解决办法是输入端加 RC 滤波、屏蔽线、尽量缩短走线。第三类是数字信号耦合。同一条排线里PWM 信号和 ADC 信号挨着走PWM 翻转时通过线间电容耦合到 ADC 线上采样值就会周期性波动。这个在 PCB 上就要注意布线距离实在不行就在软件上加滤波。第四类是采样本身带来的量化噪声和抖动。量化噪声是 ADC 原理决定的12 位分辨率下理论量化噪声幅度为 1 个 LSB还有触发采样的时刻不稳定导致信号高频分量折叠到低频看起来就像随机跳动。搞清楚了这些来源再选择对应的滤波手段就能有的放矢。4.2 三个经典滤波函数的C语言实现软件滤波是 ADC 采集的常规操作几乎所有做过 ADC 采集的人都会写几个滤波函数。我这里分享三个最常用、最有效的实现。第一个是滑动平均滤波。思路是把最近 N 次数据存进环形缓冲区每次取平均。它对随机白噪声抑制效果好响应速度快适合大多数缓慢变化的信号。#define FILTER_WINDOW_SIZE 8 uint16_t moving_average_filter(uint16_t new_value) { static uint16_t buffer[FILTER_WINDOW_SIZE] {0}; static uint8_t index 0; static uint32_t sum 0; uint32_t avg 0; sum - buffer[index]; buffer[index] new_value; sum new_value; index (index 1) % FILTER_WINDOW_SIZE; avg sum / FILTER_WINDOW_SIZE; return (uint16_t)avg; }第二个是中位值平均滤波。先取 N 个数据排序后去掉最大最小值再对剩余数据取平均。这个滤波对脉冲干扰、偶然尖峰特别有效比如电火花干扰、接触不良导致的偶发跳变。缺点是需要排序代码稍多一点而且适合中等采样频率的场景。#define MEDIAN_WINDOW_SIZE 7 uint16_t median_average_filter(uint16_t new_value) { static uint16_t buffer[MEDIAN_WINDOW_SIZE] {0}; static uint8_t index 0; uint16_t tmp[MEDIAN_WINDOW_SIZE]; uint16_t sum 0; uint8_t i, j; buffer[index] new_value; index (index 1) % MEDIAN_WINDOW_SIZE; for (i 0; i MEDIAN_WINDOW_SIZE; i) { tmp[i] buffer[i]; } // 简单冒泡排序 for (i 0; i MEDIAN_WINDOW_SIZE - 1; i) { for (j 0; j MEDIAN_WINDOW_SIZE - 1 - i; j) { if (tmp[j] tmp[j 1]) { uint16_t t tmp[j]; tmp[j] tmp[j 1]; tmp[j 1] t; } } } // 去掉最小和最大取中间值平均 for (i 1; i MEDIAN_WINDOW_SIZE - 1; i) { sum tmp[i]; } sum / (MEDIAN_WINDOW_SIZE - 2); return (uint16_t)sum; }第三个是一阶低通滤波也叫惯性滤波。公式是 y alpha * x (1 - alpha) * y_prev其中 alpha 是滤波系数范围 0 到 1。alpha 越大响应越快但滤波效果弱alpha 越小曲线越平滑但滞后严重。适合信号本来就是低频变化、需要平滑曲线的场景。float first_order_lowpass_filter(float new_value, float alpha) { static float filtered_value 0; filtered_value alpha * new_value (1.0f - alpha) * filtered_value; return filtered_value; }三个滤波器怎么选我的建议是普通电压监控用滑动平均现场环境有脉冲干扰用中位值平均信号变化缓慢、对时延不敏感用一阶低通。也可以组合使用比如先用中位值平均去掉尖峰再用一阶低通平滑效果往往不错。滤波方式抗随机噪声抗脉冲干扰实时性代码复杂度滑动平均好一般快低中位值平均好强中中一阶低通中弱中低4.3 进阶参考电压校准与两点标定软件滤波只能解决随机噪声系统性的偏差还得靠校准。ADC 的系统偏差主要来自两个地方偏移误差输入为 0 时输出不为 0和增益误差输入满量程时输出不到 4095 或者超出。最实用的校准方法是两点标定。找两个已知电压比如用高精度电源分别给 0.5V 和 3.0V记录 ADC 读数值然后拟合一条直线。这样可以同时补偿偏移和增益误差实际精度提升非常明显。举个例子假设 0.5V 时 ADC 读出 6203.0V 时读出 3724那么实际电压和 ADC 码值的关系就是一条直线电压 (ADC值 - 620) * (3.0 - 0.5) / (3724 - 620) 0.5。把这个公式写进代码替代原来的线性换算误差就能从几十 mV 压到几 mV 以内。如果不想做两点标定还有一个更省事的方法用芯片内部的参考电压通道 VREFINT。这个通道输出一个相对稳定的基准电压虽然不同芯片之间有离散性但同一颗芯片上电后比较稳定。通过读取 VREFINT 通道的值可以反推出当前实际的 VDDA 电压然后用这个实际电压替代默认的 3.3V 去做换算能把因 VDDA 波动引起的误差消掉一部分。这个思路特别适合电池供电的设备电池电压会随电量下降VDDA 不再是准确 3.3V但 VREFINT 是内部基准不随 VDDA 变化读它就能实时估算 VDDA 到底是多少从而提高采集精度。5. 实测问题清单与排查思路5.1 高频问题速查表做 ADC 采集这些年踩过的坑和帮别人解决的典型问题基本可以归纳成一张速查表。每一条后面附上排查思路方便大家现场对照。现象可能原因处理建议采集值上下跳变几十个 LSB电源噪声、参考电压去耦不足检查 VDDA/VREF 去耦开启软件滤波采集值整体偏低 5% 以上分压电阻精度差、源阻抗太大、采样时间不足换 1% 精度电阻加大采样时间多通道采集时数值错位DMA 缓冲区处理不正确、扫描顺序和代码取数不一致核对通道顺序和缓冲区索引映射DMA 不触发或数据全 0DMA 配置错误、缓冲区地址对齐问题确认 DMA 半字/字配置与缓冲区类型匹配单次读取结果稳定但连续读取波动参考电压噪声被持续采样放大加强滤波检查参考电压的稳定性采样值在某个值附近反复震荡信号源叠加了高频噪声输入端并联电容调整 RC 滤波第一个问题最常见几乎每次采集电压我都先确认供电和去耦再谈软件。第二个问题经常出现在新手改电阻分压的场景三个 10k 电阻排成一串精度和阻抗一起来采样时间又没改读数自然偏低。第三个问题我详细说一下。CubeMX 配置了多通道扫描之后ADC 按照 Rank 顺序依次转换通道DMA 缓冲区里的排列也是按照这个顺序。比如 Rank1 对应通道 3Rank2 对应通道 4那么 DMA 缓冲区里偶数位是通道 3 的数据奇数位是通道 4 的数据。有时候为了吞吐率把缓冲区设成 uint32_t 数组DMA 传输宽度也改成了字传输错位方式就会更隐蔽排查时要先确认这一层。5.2 一套实用的排查顺序遇到 ADC 数据不对劲最忌讳的是瞎改。按下面这个顺序排查大部分问题十分钟内能定位。先把万用表打在直流电压档直接量 ADC 引脚的电压和预期值对比。如果引脚电压就是偏的那就是硬件电路的问题查分压、查接触、查供电和软件无关。这一步能直接切分硬件和软件问题。如果引脚电压正常但 ADC 读出来的数值不对再用串口把原始 ADC 值打出来。注意是原始值不是换算后的电压值因为换算公式一旦写错会把本身没错的数据变成错的。观察原始值随输入电压的变化趋势如果趋势对但整体偏移就往参考电压和校准方向查如果趋势乱跳就往电源和去耦方向查。排查完信号通路还要检查代码逻辑。启动顺序对不对、DMA 有没有启动、缓冲区数组是不是被其他代码踩了这些问题用调试器查看内存就能确认。还有一个老掉牙但好用的技巧把 ADC 引脚接地和接 3.3V 各测一次。接地应该接近 0接 3.3V 应该接近 4095这两个极端点验证通过ADC 本体基本没问题剩下的都是外围的事。最后一个压箱底的建议在工程最开始阶段先别急着上复杂的滤波算法先把原始值通过串口完整打印出来观察它本身的稳定程度和噪声形态。做过这一步之后你对自己硬件底子就有了直观认识后面加滤波是锦上添花而不是给硬件缺陷打补丁。