把工程里的串口、时钟、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.3V,ADC 读出 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Ω 电阻,接 PC0 |
| PC0 对地 | 并联 100nF 电容 |
| VREF+ | 1uF + 100nF 去耦到地 |
| VDDA | 磁珠连接 VDD 3.3V,并 1uF + 100nF 去耦 |
| 模拟地/数字地 | 单点连接 |
实测下来这个小配置跑起来很稳,采集到的数据原始值波动范围在 ±3 LSB 以内。如果你的板子没有单独的 VREF+ 引脚,说明 VREF+ 已经在芯片内部和 VDDA 绑定了,这时候 VDDA 的去耦就更重要,必须单独走线、单独去耦,不要偷懒和数字电源混在一起。
3. CubeMX配置与代码实现:一条龙跑通ADC
3.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_t,DMA 配置为半字传输,两者必须匹配,如果缓冲区定义成 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 读出 620,3.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 不触发或数据全 0 | DMA 配置错误、缓冲区地址对齐问题 | 确认 DMA 半字/字配置与缓冲区类型匹配 |
| 单次读取结果稳定但连续读取波动 | 参考电压噪声被持续采样放大 | 加强滤波,检查参考电压的稳定性 |
| 采样值在某个值附近反复震荡 | 信号源叠加了高频噪声 | 输入端并联电容,调整 RC 滤波 |
第一个问题最常见,几乎每次采集电压我都先确认供电和去耦,再谈软件。第二个问题经常出现在新手改电阻分压的场景,三个 10k 电阻排成一串,精度和阻抗一起来,采样时间又没改,读数自然偏低。
第三个问题我详细说一下。CubeMX 配置了多通道扫描之后,ADC 按照 Rank 顺序依次转换通道,DMA 缓冲区里的排列也是按照这个顺序。比如 Rank1 对应通道 3,Rank2 对应通道 4,那么 DMA 缓冲区里偶数位是通道 3 的数据,奇数位是通道 4 的数据。有时候为了吞吐率把缓冲区设成 uint32_t 数组,DMA 传输宽度也改成了字传输,错位方式就会更隐蔽,排查时要先确认这一层。
5.2 一套实用的排查顺序
遇到 ADC 数据不对劲,最忌讳的是瞎改。按下面这个顺序排查,大部分问题十分钟内能定位。
先把万用表打在直流电压档,直接量 ADC 引脚的电压,和预期值对比。如果引脚电压就是偏的,那就是硬件电路的问题,查分压、查接触、查供电,和软件无关。这一步能直接切分硬件和软件问题。
如果引脚电压正常,但 ADC 读出来的数值不对,再用串口把原始 ADC 值打出来。注意是原始值不是换算后的电压值,因为换算公式一旦写错,会把本身没错的数据变成错的。观察原始值随输入电压的变化趋势,如果趋势对但整体偏移,就往参考电压和校准方向查;如果趋势乱跳,就往电源和去耦方向查。
排查完信号通路,还要检查代码逻辑。启动顺序对不对、DMA 有没有启动、缓冲区数组是不是被其他代码踩了,这些问题用调试器查看内存就能确认。还有一个老掉牙但好用的技巧:把 ADC 引脚接地和接 3.3V 各测一次。接地应该接近 0,接 3.3V 应该接近 4095,这两个极端点验证通过,ADC 本体基本没问题,剩下的都是外围的事。
最后一个压箱底的建议:在工程最开始阶段,先别急着上复杂的滤波算法,先把原始值通过串口完整打印出来,观察它本身的稳定程度和噪声形态。做过这一步之后你对自己硬件底子就有了直观认识,后面加滤波是锦上添花,而不是给硬件缺陷打补丁。