STM32C5A3R ADC电压采集教程:采样周期、DMA与精度校准全解析
2026/9/12 6:58:08 网站建设 项目流程

电源设计做完,转台搭起来,第七篇终于轮到模拟量采集这关。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^N

12bit、参考 3.3V 时,1 LSB 约 0.806mV。量化误差理论上在正负半个 LSB 之间,也就是 ±0.4mV 左右。这听起来很小,但注意这是只考虑量化误差的理想情况。实际项目里,电源纹波、参考电压漂移、输入阻抗引起的充电误差、PCB 漏电流带来的误差可能远远大于这个值。所以我从来不跟别人说“12bit 分辨率就一定能测出 0.8mV 精度”,分辨率只代表最小步进,不代表精度。

2.4 采样时间和转换时间的完整计算

确定采样时间之前,要先知道 STM32C5A3R 的 ADC 时钟从哪里来。通常 ADC 时钟是由 APB 时钟经过分频器得到的,CubeMX 的时钟树里能直接看到。ADC 总转换时间按下面的关系计算:

Tconv = (采样周期 + 逐次逼近周期) / fADC

12bit 模式下,逐次逼近部分一般占 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 ModeIndependent先用独立模式,不要碰双重模式
Clock Prescaler根据时钟树确定保证 fADC 不超过上限
Resolution12 bit默认最高分辨率
Data AlignmentRight alignment右对齐,左对齐主要用于 DSP 某些场景
Scan Conversion ModeDisabled单通道不需要扫描
Continuous Conversion ModeDisabled先用单次转换,手动启动
End Of Conversion SelectionEOC flag at end of single conversion方便轮询读取
Number Of Conversion1只配置一个规则通道
Sampling Time12.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 内跳动,说明噪声耦合已经比较明显。此时按优先级从高到低排查:

  1. 供电电源是否来自开关电源,纹波是否过大;
  2. 板子的模拟地是否和数字地彻底分隔,是否存在大面积地线环路;
  3. VREF+ 引脚旁边的去耦电容是否靠近引脚放置;
  4. ADC 采样周期是否过短;
  5. 是否有大电流数字外设与模拟通道相邻走线。

电源噪声和时钟抖动是 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.500V0.5012V
1.650V1.6489V
3.050V3.0478V

假设换算关系是线性修正:

实际电压 = k × ADC换算电压 + b

k 和 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 采集这东西,胆子要大,心要细,把这篇文章里的配置步骤和排查链路走一遍,电压采集基本就不会再拖后腿了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询