电源设计做完,转台搭起来,第七篇终于轮到模拟量采集这关。STM32C5A3R 的 ADC 电压采集功能和之前玩过的 F1/G4 在 CubeMX 下长得差不多,真正卡人的地方不是“会不会配置”,而是采样周期怎么选、DMA 缓冲区为什么乱、参考电压怎么影响精度,还有那根始终绕不开的电源噪声和地线回路。这篇我把自己从零开始配置到调通的完整过程写出来,适合正在用 STM32C5 系列做电机控制、电池监测、模拟量采集,或者从 F1 系列转过来对 HAL 库不熟的开发者。看完你可以直接抄走轮询版和 DMA 版两套写法,顺便避掉我在实际调试里踩过的几个坑。
1. 这颗芯片的ADC资源,以及我为什么要单独开一篇写电压采集
1.1 先对上芯片型号和外设概况
STM32C5A3R 属于 ST 的 C 系列,核心是 Cortex-M33,主频能跑到 250MHz,放在需要同时跑控制算法和通信协议栈的场景里非常合适。手头这块板子以它为主控,主要做电机控制和系统状态监测,ADC 承担的任务包括母线电压采样、电流采样、电位器给定信号读取等。标题里写的“ADC电压采集”是其中最基础、也最关键的一类应用。
这颗芯片内部集成多个 ADC 模块,不同封装条件下可用的通道数和引脚会变,但共同点是支持 12bit 分辨率,带注入通道、扫描模式、外部触发、DMA 和过采样。相比 STM32F103 那代老产品,C5 系列的 ADC 在外设设计上更贴近 G4/H5 的思路,时钟树、校准寄存器、低功耗模式下的行为都有明显变化。如果你之前只用过 F1,直接套老经验会吃亏。
1.2 电压采集在项目里的作用
采集电压不单是为了显示一个数字。在电机驱动板上,母线电压是过压欠压保护的关键判据;在电池供电设备里,ADC 用来估算电量;在传感器接口中,ADC 是模拟量进数字世界的唯一入口。我的习惯是第一步先把 ADC 轮询采集跑通,输出原始值和换算后的电压值,确认硬件链路没问题,然后再考虑 DMA 多通道、定时器触发这些进阶功能。
这个顺序很重要。很多人在 CubeMX 里一次就把多通道 DMA、扫描模式、外部触发全勾上,结果数据乱套了都不知道问题出在软件还是硬件。ADC 外设本身不复杂,但它和外部的模拟电路、参考电压、电源完整性强相关,分层验证是避免玄学问题的最好方式。
1.3 和 F103 时代的 ADC 相比,配置上多了什么讲究
F103 的 ADC 时钟最高约 14MHz,采样时间选项少,没有校准寄存器,用起来比较直白。STM32C5A3R 的 ADC 时钟上限更高,带来的直接问题是“采样周期如果选得太短,内部采样电容来不及建立稳定电压”,尤其是输入端串了大电阻或者信号源输出阻抗高的时候,读到的值会肉眼可见地偏低。
此外,C5 系列在复位后默认并不保证 ADC 偏移误差在校准状态,需要软件触发校准,很多从 F1 转过来的人会漏掉这一步。后面我会专门用一节讲校准,这里先记住一个结论:校准不是你工程里可有可无的加分项,它直接影响低电压段读数的准确性。
2. 决定采集精度的从来不是单看“分辨率”:采样周期与建立时间怎么算
2.1 SAR ADC 的工作原理,用天平来理解最舒服
STM32C5A3R 内置的 ADC 属于逐次逼近型,也就是 SAR ADC。它的工作方式像一台自动天平:先放一个最大的砝码试试,太重就拿掉,换小一号的继续试,从高位到低位一路试出最接近输入电压的二进制码。这个“逐次比较”的过程,每一位都要花一个 ADC 时钟周期。
它和容易弄混的 Σ-Δ ADC 不同,SAR 不需要过采样和数字滤波,转换速度快,但精度受比较器噪声、参考电压噪声、采样开关导通电阻、输入源阻抗的影响更大。所以测直流电压时如果数值一直在跳,不要第一时间怀疑 ADC 坏了,多半是外部噪声耦合或参考电压不稳。
2.2 已知输入 1V,ADC 读取值到底是多少
很多人问:如果单片机 ADC 输入口电压为 1V,采样得到的值是多少?这不能直接给一个数字,得看参考电压和分辨率。通用公式是:
ADC原始值 ≈ Vin / VREF × 2^N以 12bit 分辨率为例,N=12,2^N=4096。参考电压 VREF 是 3.3V 时:
1.0V / 3.3V × 4096 ≈ 1241参考电压改成 5V 时:
1.0V / 5.0V × 4096 ≈ 819有的资料会写成乘以 4095,这里有一个很容易绕晕的小坑。理想 ADC 的满量程对应 4095,但工程上很多 ST 的 HAL 库和参考实现直接用 4096 做分母,最终 0.024% 的差异对绝大多数项目没有影响。你只需要保持一个口径:显示电压时用raw × VREF / 4096.0f,如果转过来的结果显示满量程差一个 LSB,不必纠结,这是量化边界问题。
2.3 量化噪声与分辨率的关系
理想的 N 位 ADC 量化噪声其实是可以算出来的。每个 LSB 对应的电压是:
1 LSB = VREF / 2^N12bit、参考 3.3V 时,1 LSB 约 0.806mV。量化误差理论上在正负半个 LSB 之间,也就是 ±0.4mV 左右。这听起来很小,但注意这是只考虑量化误差的理想情况。实际项目里,电源纹波、参考电压漂移、输入阻抗引起的充电误差、PCB 漏电流带来的误差可能远远大于这个值。所以我从来不跟别人说“12bit 分辨率就一定能测出 0.8mV 精度”,分辨率只代表最小步进,不代表精度。
2.4 采样时间和转换时间的完整计算
确定采样时间之前,要先知道 STM32C5A3R 的 ADC 时钟从哪里来。通常 ADC 时钟是由 APB 时钟经过分频器得到的,CubeMX 的时钟树里能直接看到。ADC 总转换时间按下面的关系计算:
Tconv = (采样周期 + 逐次逼近周期) / fADC12bit 模式下,逐次逼近部分一般占 12.5 个 ADC 时钟周期。假设 fADC = 40MHz,采样周期选择 6.5 个 ADC 时钟:
Tconv = (6.5 + 12.5) / 40MHz = 19 / 40 = 0.475us也就是说,单次转换只需要不到 0.5us。算这个数字的意义在于:如果你用定时器触发 ADC,触发频率必须低于转换完成频率,否则会出现在上一次转换还没完成时,新的触发就来了,导致结果混乱或读到中间值。
2.5 采样周期太长太短都不行
采样周期太短,内部采样电容还没充到输入电压的电平,测出来偏小;采样周期太长,转换速率下降,且对高频噪声的积分时间变长,数值反而容易抖。对纯直流电压采集,如果信号源内阻不大,比如经过一个 10kΩ 分压电阻直接进 MCU,我通常选 12.5 到 47.5 个周期之间。如果输入端走了长走线或有大电容,应适当增加采样周期。
判断依据很简单:把采样周期从最短往大调,观察 ADC 值变化。如果数值一直往上走并最后稳定,说明一开始采样时间不够;如果数值稳定了,继续加大采样时间只会降低更新率。这个方法比抓数据手册里的 RC 曲线更直接,也更适合快节奏排查。
3. CubeMX 配置单通道电压采集的完整步骤
3.1 基础时钟设置是 ADC 配置的地基
用 CubeMX 新建工程后,第一步不是打开 ADC 页面,而是先校准时钟树。我把系统主频配置到 250MHz,确保 APB 时钟满足 ADC 工作条件。ADC 时钟不建议直接放在最高频率,我常用的是让 fADC 保持在数据手册允许的范围内,再留一点余量,因为此时电源噪声的影响最小。
在 Clock Configuration 页面里可以看到 AHB、APB1、APB2 的分频关系,ADC 时钟源从这里出来。选好后检查一下 CubeMX 生成的代码,确认 ADC 时钟源被正确初始化。这个步骤很多人会跳过,但实测下来,ADC 时钟设置不对会直接导致采样时间选项与预期不符。
3.2 配置 ADC1 单通道轮询模式
进入 Analog > ADC1 配置界面,建议先做如下设置:
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| ADC Mode | Independent | 先用独立模式,不要碰双重模式 |
| Clock Prescaler | 根据时钟树确定 | 保证 fADC 不超过上限 |
| Resolution | 12 bit | 默认最高分辨率 |
| Data Alignment | Right alignment | 右对齐,左对齐主要用于 DSP 某些场景 |
| Scan Conversion Mode | Disabled | 单通道不需要扫描 |
| Continuous Conversion Mode | Disabled | 先用单次转换,手动启动 |
| End Of Conversion Selection | EOC flag at end of single conversion | 方便轮询读取 |
| Number Of Conversion | 1 | 只配置一个规则通道 |
| Sampling Time | 12.5 Cycles 起步 | 若信号源内阻大再提高 |
然后给 ADC1 选择一个输入通道,比如 ADC1_IN1,使能这个通道。参考电压方面,如果板子直接用 VREF+ 引脚接 3.3V,那么不需要额外配置;如果芯片支持内部 VREFBUF,需要明确是否启用内部参考电压缓冲。我的板子使用外部参考,所以不勾选 VREFBUF,避免参考电压来源混乱。
3.3 生成工程后先检查代码顺序
生成工程后打开 main.c,检查 MCU 初始化顺序。ADC 初始化函数MX_ADC1_Init()必须在时钟初始化之后调用,HAL 库会自动处理这种依赖关系,但如果你手动在 GPIO 初始化之前调用 ADC 相关函数,可能出现引脚功能不对。
我自己每次都额外检查 DMA 页面是否为 ADC 分配了正确的 DMA 通道。如果要跑 DMA,就应在 DMA Settings 里添加 ADC1 的请求,并设置为 Circular 循环模式,缓冲区大小和后续代码要匹配。我一般先把 DMA 配置放在后面单独做,第一版先跑通轮询,确认原始值正常后再引入 DMA。
4. 代码落地的三层递进:轮询、DMA 与滤波
4.1 第一版:最简单的单次轮询读取
CubeMX 生成的 ADC 初始化函数会自动完成配置,但不会自动开始转换,还需要手动调用启动函数。第一版代码我写成下面这样:
uint16_t adc_raw = 0; float adc_voltage = 0.0f; // 启动 ADC 并进行校准 HAL_ADCEx_Calibration_Start(&hadc1); // 启动一次软件触发的转换 HAL_ADC_Start(&hadc1); // 等待转换完成,超时设 100ms if (HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK) { adc_raw = HAL_ADC_GetValue(&hadc1); adc_voltage = (float)adc_raw * 3.3f / 4096.0f; } printf("raw = %d, voltage = %.3f V\r\n", adc_raw, adc_voltage);注意校准函数放在启动转换之前,且只在 ADC 初始化完成后执行一次即可。如果你用的 HAL 版本较新,校准函数签名可能带更多参数,以当前安装的固件包实际生成为准。把读写日志打印放在主循环外,避免持续占用 CPU。
4.2 第二版:多通道 DMA 扫描采集
当需要同时采集多个通道时,轮询模式就会变得很浪费 CPU。这时把 ADC 设置为扫描模式,并用 DMA 把转换结果自动搬到内存。CubeMX 里需要在 ADC 配置中打开 Scan Conversion Mode,同时把 Continuous Conversion Mode 设为 Enabled,再在 DMA Settings 里添加 ADC 的 DMA 请求,模式选 Circular。
代码侧最关键的是缓冲区:
#define ADC_CHANNEL_NUM 4 uint16_t adc_buf[ADC_CHANNEL_NUM] = {0}; // 启动 DMA 采集,共 ADC_CHANNEL_NUM 次转换 HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, ADC_CHANNEL_NUM);这里有一个非常容易踩的坑:HAL 库在很多系列上以 32 位字为单位去配置 DMA 传输数量,而 ADC 结果寄存器只有 12bit 有效数据,缓冲区却习惯声明成 16bit。如果你的 DMA 配置和数据宽度不匹配,数组索引和实际转换结果会错位,出现通道像“鬼影”一样错位重叠。稳妥的办法是确认 DMA 初始化里外设端和数据端的数据宽度,并让缓冲区类型与 DMA 配置保持一致。
DMA 转换完成后,HAL 会回调用户的完成函数:
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { // 此时 adc_buf 内已经是最新一轮数据 for (uint8_t i = 0; i < ADC_CHANNEL_NUM; i++) { // 按固定周期打印或做控制逻辑 adc_processed[i] = (uint16_t)adc_buf[i]; } } }不要在回调函数里直接做打印或耗时的浮点运算,尤其是打印串口这种慢速外设。正确的做法是回调里只拷贝数据或置标志位,主循环里统一处理。
4.3 校准函数到底做了什么
STM32C5A3R 这类芯片的 ADC 具备偏移校准功能。校准过程由 ADC 内部自动完成,对零输入时的偏移误差进行测量和修正。为什么我强调要做?因为不做校准的情况下,输入端接地读到的值不一定是 0,可能是几十个 LSB,也可能出现负偏移,这会直接影响低电压测量段的准确性。
校准也分时机。上电校准一次基本够用,但如果芯片在高温或低温环境下工作,我建议在系统运行一段时间后重新校准,或者至少在做高精度采样前校准。同样的电压,校准前后的差值在某些芯片上可以相差 1 到 3 个 LSB。
4.4 C 语言 ADC 值滤波函数的三种朴素写法
滤波不是把所有原始值平均一下那么简单。根据信号特点选择合适的滤波方法,比堆数量更有效。
第一种是去极值平均滤波。我采集一组数据,去掉最大最小值,再对剩下的值取平均,适合抑制随机尖峰干扰。
#define FILTER_N 7 uint16_t adc_filter_average(uint16_t *buf, uint8_t len) { uint16_t min_val = buf[0]; uint16_t max_val = buf[0]; uint32_t sum = 0; for (uint8_t i = 0; i < len; i++) { if (buf[i] < min_val) min_val = buf[i]; if (buf[i] > max_val) max_val = buf[i]; sum += buf[i]; } sum -= min_val; sum -= max_val; return (uint16_t)(sum / (len - 2)); }第二种是滑动平均,适合周期性采样场景,例如基于 PWM 中断每 100us 采一次电流,只要维护一个环形队列,平均队列里最近 N 次的值即可。它比去极值平均更平滑,但对脉冲干扰不敏感。
第三种是一阶低通滤波,不适合采样式读数,但适合需要快速响应的控制环路:
float adc_lowpass(float current, float previous, float alpha) { return (1.0f - alpha) * previous + alpha * current; }alpha 越小越平滑,响应越慢。注意滤波永远是在原始值和电压换算之间的取舍,不要为了追求显示稳定,把真实电压变化也滤掉了。
5. 采样结果“看着对但不准”的六步排查链路
5.1 现象一:读数一直是 0 或一直 4095
如果输入接了确定的中间电压,读 0 或满量程,不用先怀疑软件。第一步用万用表量 ADC 引脚对 GND 电压,确认引脚处实际电压是否正确。引脚电压正常,检查 CubeMX 里通道号是否和物理引脚对应,尤其多引脚封装非常容易选错。
排除引脚映射后,再查参考电压是否给上。VREF+ 没有接对,或者参考电压引脚悬空,会得到极端的读数值。还有一种常见情况是 ADC 未真正启动,只做了初始化但没有调用HAL_ADC_Start(),这时候HAL_ADC_GetValue()读到的是旧值甚至 0。
5.2 现象二:数值毫无规律地跳动
先确认这不是正常噪声。把 ADC 输入端直接短接到 GND,如果读到的值在几十 LSB 内跳动,说明噪声耦合已经比较明显。此时按优先级从高到低排查:
- 供电电源是否来自开关电源,纹波是否过大;
- 板子的模拟地是否和数字地彻底分隔,是否存在大面积地线环路;
- VREF+ 引脚旁边的去耦电容是否靠近引脚放置;
- ADC 采样周期是否过短;
- 是否有大电流数字外设与模拟通道相邻走线。
电源噪声和时钟抖动是 ADC 采样精度的大敌,而且它们的影响在高分辨率场景下会被放大。即使是直流电压采集,只要模拟输入走线敏感,也能在结果里看到明显的 50Hz 或开关频率分量。
5.3 现象三:输出值有固定的偏大或偏小
固定偏差首先检查输入电阻分压网络,看看电阻精度和实际分压比是否和你计算的一致。网络本身没问题的话,再做一次 ADC 硬件校准。若校准后仍然有偏差,可能是采样时间不足,特别是输入源阻抗较大时,采样电容充不满,结果系统性偏小。
如果输入信号经过运放缓冲,那么运放的输入偏置电压和输出摆幅限制同样会造成固定偏差。用高精度万用表对照 ADC 读数是排查固定偏差最直接的方法。
5.4 现象四:多通道 DMA 数据“错位”或“串道”
这个问题踩过的人非常多,尤其从别的芯片平台移植代码时特别容易爆发。数据错位的本质原因通常不是 ADC,而是 DMA 的数据宽度与缓冲区类型不匹配。ADC 结果是 12bit,寄存器是 32bit,DMA 搬运时如果按 32 位搬,缓冲区却声明成 16 位,数组里每个元素只吃了高 16 位或低 16 位的一部分,表现出来就是通道数据乱跳。
遇到错位后的排查顺序:先确认 DMA 初始化代码中 PeriphDataAlignment 和 MemDataAlignment 与缓冲区类型匹配;然后把 DMA 一次传输的数据个数改成 1 个通道做测试,看看单个通道是否正常;最后再增加通道数。这样能快速定位是 ADC 问题还是 DMA 问题。
5.5 现象五:注入通道的结果什么时候才能读
STM32 的 ADC 有规则通道和注入通道之分。规则通道是常规轮询或 DMA 的采集对象,注入通道可以打断规则通道的转换序列,适合紧急情况下插入关键采样。如果你配置了注入通道,却还是在规则通道转换完成回调里读注入结果,那一定读到的是旧值或错误值。
注入通道的转换结果存放在独立的注入数据寄存器中。启动注入通道转换并使用中断或轮询等待注入通道转换结束标志,然后在注入通道转换完成回调里读取对应的注入数据寄存器。注入通道的数据不会自动进入规则通道的 DMA 缓冲区,这也是一个常见误解。
5.6 现象六:读数跟随 PWM 工作状态变化而变化
如果系统里 PWM 一开,ADC 数值就跳动或者飘移,你的问题大概率不是 ADC 配置,而是采样时刻选得不对。PWM 开关切换瞬间会产生很大的 di/dt 和 dv/dt,通过地弹和空间耦合进入模拟信号链路。解决办法是不要在任何随机时刻采样,而是用定时器触发 ADC,把采样点放在 PWM 的开关死区内或者电流稳定区。
中心对齐模式下,PWM 在计数顶点和底点通常会翻转或处于死区,这两个时刻是适合触发 ADC 的窗口。CubeMX 里可以让定时器的某个事件作为 ADC 外部触发源,并据此设置采样时刻。这个方案对电机电流采样几乎是必须的,对普通电压采集则可以不用这么严格。
6. 校准与布局经验:三点校准和三个 PCB 要点
6.1 外部三点校准:把系统的总误差压下来
芯片内部的偏移校准只能修正 ADC 自身的偏移,没法修正输入分压电阻精度、参考电压误差和走线压降带来的系统级误差。对电压采集精度要求更高的场景,建议做外部校准。
所谓三点校准,就是给 ADC 输入三个已知的准确电压,记录下来 ADC 读到的换算电压值,然后用简单的直线关系做修正。两点校准可以修正增益和偏移,三点校准则能顺带验证线性度。实际操作中,我会用一台四位半或六位半万用表和高精度可调直流源,在量程的低、中、高三个点上记录:
| 标准电压 | ADC换算电压 |
|---|---|
| 0.500V | 0.5012V |
| 1.650V | 1.6489V |
| 3.050V | 3.0478V |
假设换算关系是线性修正:
实际电压 = k × ADC换算电压 + bk 和 b 可以通过两个点解出来,再用第三个点验证。多点数据也可以用最小二乘法拟合,误差会更均衡。修正公式放到代码里最后一级处理。这个做法的意义是:软件修正优于换电阻,换电阻优于改布局,改布局优于干瞪眼。
需要提醒的是,外部校准使用的信号源必须稳定且输出阻抗低。如果信号源本身带载能力差,接上 ADC 后电压被拉低,校出来的 k 和 b 就是错的,得不偿失。
6.2 PCB 布局第一点:模拟电源和参考电压的去耦位置
ADC 的转换精度本质上取决于参考电压的稳定性。VREF+ 引脚旁必须放置高频去耦电容,建议 100nF 陶瓷电容紧贴引脚,旁边再放一个 1uF 到 10uF 电容。如果 VDDA 和数字电源 VDD 共用一个电源,中间最好用磁珠或小电阻做隔离,把数字平面上的开关噪声挡在模拟域之外。
电源噪声进 ADC 的路径有两个:一是参考电压引脚,二是模拟电源引脚。很多人只给 VREF+ 加电容,VDDA 却直接从数字电拉过来,结果 VREF 稳了,比较器供电还是脏的,一样会体现在结果里。
6.3 PCB 布局第二点:ADC 输入通道的 RC 低通与采样电容匹配
ADC 输入引脚内部有采样电容,每次采样都会从外部信号源取电荷。如果外部走线接了大电阻,采样瞬间的充电电流会造成电压跌落,采样时间不够时读到的就偏低。这也是刚才反复强调采样周期要匹配信号源阻抗的原因。
常规做法是在 ADC 输入引脚前加一个 RC 低通滤波器,R 放在靠近信号源那一侧,C 放在靠近 MCU 引脚那一侧。R 和 C 的值要匹配采样周期。R 太大会造成建立时间不足,C 太小则滤波效果差。对带宽要求不高的直流电压采集,我给一个参考值:R 在 100Ω 到 1kΩ 之间,C 在 1nF 到 100nF 之间。实际值需要根据采样周期和允许的误差倒推,不能无脑照抄。
6.4 PCB 布局第三点:数字信号和模拟走线的空间隔离
晶振引脚的时钟信号、PWM 输出、SPI/I2C 通信线,这些陡变的数字边沿会产生高频谐波,通过空间辐射或串扰进入模拟走线。低频直流电压采集对这类干扰往往不敏感,但采样保持电路本质上是一个宽带采样器,一旦模拟输入在高频干扰下发生振荡,低通之后依然会混入低频分量,这就是什么玄学问题都查不出来时的最常见解释。
布局原则不复杂:模拟输入走线远离数字走线,不要和时钟线、PWM 输出线平行长距离共走;模拟地和数字地单点连通;如果条件允许,在 ADC 输入引脚下方保持完整的地平面,不要被数字走线穿破。时钟抖动对 ADC 的影响也来自这个层面,采样时钟沿一旦被数字噪声扰动,采样点的时刻就会抖动,对高频输入信号来说造成的幅值误差会被放大。
6.5 后续还能往哪扩展
这篇文章只写了最基本的 ADC 电压采集闭环,实际工程里基于这套流程可以继续扩展:定时器触发采样做电机相电流采集、DMA 双缓冲配合高级定时器更新事件做中心对齐点采样、过采样提高有效分辨率、ADC 数据与上位机联动做波形显示。STM32C5A3R 这颗料的 ADC 外设并不算复杂,真正决定系统精度的反而是你在原理图阶段有没有把去耦、参考、地分割、采样时间这四件事想清楚。
最后分享一个我个人的习惯:无论项目多急,第一版一定是轮询模式读出原始值,确认硬件链路没有问题,再写 DMA 和滤波。很多人跳过了这个步骤,最后查来查去发现只是某个引脚虚焊。ADC 采集这东西,胆子要大,心要细,把这篇文章里的配置步骤和排查链路走一遍,电压采集基本就不会再拖后腿了。