
文章目录摘要一、前言为什么多通道采集这么容易翻车二、原理SAR ADC 的一次转换到底经历了什么2.1 逐次逼近与转换时间构成2.2 采样时间不是越大越好2.3 多通道扫描的完整数据流三、方案对比轮询、中断、DMA 该怎么选四、CubeMX 配置一步一步来4.1 工程基本设置4.2 ADC1 参数配置4.3 DMA 配置4.4 串口与时钟配置五、代码实现从启动到校准5.1 变量定义与 ADC 启动5.2 电压换算VREFINT 动态校准5.3 主循环采集与打印六、测试验证理论 vs 实测6.1 转换时间验证6.2 VREFINT 校准精度验证6.3 CPU 占用对比七、故障排查我踩过的 6 个坑7.1 第一通道读数正常后面的通道全错7.2 读数跳变严重±40 个 LSB7.3 不调用校准函数误差高达 10%7.4 DMA 只搬一轮就停了7.5 低功耗模式唤醒后 ADC 读数异常7.6 VREFINT 读数异常偏低八、总结8.1 核心要点回顾8.2 适用边界与已知问题8.3 扩展方向参考资料摘要电池供电产品里ADC 多通道采集看似简单实际坑不少轮询方式在采样频率高时拖垮 CPU固定按 3.3V 换算电压在电池电压跌落时误差能到 5% 以上。本文基于 STM32L412CBT6 STM32CubeMX 6.12 HAL 库 1.13实现 ADC1 四通道扫描 DMA 循环搬运并用内部基准电压 VREFINT 反推真实 VDDA 做动态校准。实测四通道 1kHz 采样时 CPU 占用从轮询的 38% 降到 1.2%VREFINT 校准时电压测量误差从固定换算的 4.7% 压到 0.6% 以内单次转换耗时 187.5ns理论值与示波器实测 192ns 吻合。提供完整 CubeMX 配置、工程级代码和 6 类常见故障排查。一、前言为什么多通道采集这么容易翻车做低功耗产品的人对 ADC 都不陌生电量检测、温湿度传感器、光敏采样、电流采样一路信号对应一个通道。单通道轮询谁都会麻烦从多通道 连续采集开始。我最初在 NUCLEO-L412KB 上做三路传感器采集用的是最朴素的轮询HAL_ADC_Start→HAL_ADC_PollForConversion→HAL_ADC_GetValue三路轮一遍再进HAL_Delay。功能上没问题但一上逻辑分析仪就露馅了100Hz 采样时 CPU 空转等待转换完成的时间占比高达 38%而且三路信号在时间上错开了——第 1 路和第 3 路之间差了整整一个转换周期对需要同步采样的场景比如同时刻的电压电流这就是系统性误差。另一个高频翻车点是电压换算。很多教程直接写adc_val * 3.3f / 4095这个 3.3V 是理想值。电池供电时 VDDA 会随电量跌落实际可能只有 3.1V这时所有通道的电压读数整体偏大约 6%。更隐蔽的是STM32L4 的 ADC 参考电压就是 VDDAVDDA 变了同一个 ADC 原始值代表的实际电压也变了固定换算等于把错误当真理。本文要解决的正是这两个问题用 DMA 把 CPU 从转换搬运里解放出来用 VREFINT 把电压换算从拍脑袋 3.3V改成实测 VDDA。读完你可以直接套用到自己的四通道以内采集工程。前置条件会 STM32CubeMX 生成工程、能看懂 HAL 库代码、手头有 STM32L4 系列开发板L412/L431/L496 均可配置略有差异我会标注。本文完整工程代码可在 CSDN 下载频道 获取VIP 免费。二、原理SAR ADC 的一次转换到底经历了什么2.1 逐次逼近与转换时间构成STM32L4 的 ADC 是 12 位逐次逼近型SAR。一次完整转换分两段采样阶段采样电容充电到输入电压和转换阶段逐次比较逼近12 位需要 12.5 个 ADC 时钟周期。总转换时间TCONV Tsampling 12.5 × TADC_CLK其中 Tsampling 可编程L4 系列提供 8 档2.5 / 6.5 / 12.5 / 24.5 / 47.5 / 92.5 / 247.5 / 640.5 个 ADC 时钟周期。以 fADC_CLK 80MHz、采样时间 2.5 周期为例TCONV (2.5 12.5) × (1/80MHz) 15 × 12.5ns 187.5ns也就是约 5.33 Msps——这是 L4 在 12 位下的理论极限。实际工程里极少跑满原因后面故障排查部分会讲。2.2 采样时间不是越大越好采样时间本质是给内部采样电容充电的时间窗口。信号源内阻越大需要的充电时间越长。L4 参考手册给过源阻抗与最小采样时间的关系源阻抗 50Ω 以下用 2.5 周期就够几 kΩ 的传感器输出建议 47.5 周期起步。但采样时间拉长会直接拖慢转换速率。四通道如果全用 640.5 周期一轮扫描要4 × (640.5 12.5) / 80MHz ≈ 32.7μs1kHz 刷新绰绰有余但如果要 10kHz 同步采样就顶不住了。正确的做法是给每个 Rank 单独配采样时间低阻抗通道运放输出、分压网络用短采样高阻抗通道光敏电阻、NTC 分压用长采样。2.3 多通道扫描的完整数据流DMA 请求每通道转换完成软件触发 / 定时器触发ADC 规则序列扫描Rank1: IN0 - Rank2: IN1 - ...SAR 转换核心采样 12.5 周期逐次逼近ADC_DR 数据寄存器DMA 控制器Circular 循环搬运内存数组 adc_buf[]按 Rank 顺序填充CPU 零参与主循环直接读数组EOC 标志 / 可选中断DMA 在这里的角色是搬运工ADC 每转换完一个通道DMA 就自动把 ADC_DR 的值搬到内存数组对应下标一轮扫描结束后回到数组头继续周而复始。CPU 只在启动时调用一次HAL_ADC_Start_DMA之后完全不参与。相关阅读《STM32 ADC DMA 采集模拟量让 CPU 彻底不碰采样》 — 对 Rank 顺序与数组下标的对应关系有非常直白的解释三、方案对比轮询、中断、DMA 该怎么选先说结论采集频率低、通道少、对 CPU 占用不敏感 → 轮询单通道高频但不想写 DMA → 中断多通道连续采集 → DMA没有之一。对比维度轮询模式中断模式DMA 模式CPU 占用四通道 1kHz约 38%实测约 12%每次进中断约 1.2%实测通道间时间错位明显逐通道串行等待较小无硬件连续扫描代码复杂度最低中等中等一次配置永久受益数据搬运CPU 亲自读CPU 进中断读DMA 硬件搬运适用场景单次采样、低频轮询单通道中频多通道连续、传感器同步采集低功耗适配采样完即停灵活中断唤醒配定时器触发 自动关闭模式最佳我自己的决策过程很简单产品要同时采电池电压、光敏、NTC 温度三路还要跑 LCD 刷新和按键扫描轮询的 38% CPU 占用直接不可接受中断模式每路都进中断中断里做数据处理容易把主流程打乱而且三路信号依旧做不到严格同步。DMA 循环搬运 主循环定时读取是最省心且可扩展的方案——以后加一路传感器改一下数组大小和 Rank 配置就行。相关阅读《ADC 学习笔记从原理到 STM32 实战一篇讲透》 — 三种模式的 CubeMX 配置差异对比得很清楚四、CubeMX 配置一步一步来4.1 工程基本设置我用的环境STM32CubeMX 6.12.0、STM32CubeL4 固件包 1.13.0、IDE 选择 MDK-ARM。芯片选 STM32L412CBT6NUCLEO-L412KB 板载Flash 128KB、RAM 40KB跑低功耗项目足够。时钟树按默认MSI 4MHz 起振SYSCLK 80MHzAHB 不分频。ADC 时钟这里有个坑L4 的 ADC 有两路时钟源可选——AHB 分频adc_ker_ck和独立异步时钟adc_ker_ck_presCubeMX 里在 ADC 的 Clock Prescaler 下拉选择。异步时钟的好处是不受总线时钟抖动影响我用的 80MHz 异步时钟。4.2 ADC1 参数配置在 Pinout Configuration → Analog → ADC1 中配置项值说明IN0 (PA0)使能电池电压采样分压后IN1 (PA1)使能光敏电阻分压IN2 (PA2)使能NTC 温度采样IN17 (VREFINT)使能内部基准电压通道用于 VDDA 反推Resolution12 bits默认 12 位够用Scan Conversion ModeEnabled多通道扫描必须开Continuous Conversion ModeEnabled连续转换DMA 循环搬运的前提End of Conversion SelectionEOC after each conversion每通道转换完置 EOCNumber Of Conversion44 个 RankRank1 ~ Rank4 采样时间见下方说明按通道阻抗单独配采样时间分配这是我自己踩过坑后定的方案Rank通道采样时间理由1IN0 电池分压47.5 周期分压网络源阻抗约 10kΩ需要较长充电时间2IN1 光敏分压92.5 周期光敏电阻内阻大短采样读数跳得厉害3IN2 NTC 分压92.5 周期同上NTC 在低温段阻值可达几十 kΩ4IN17 VREFINT47.5 周期内部基准官方建议采样时间别太短第一次做这个项目时我图省事四路全设成 2.5 周期结果光敏那路读数在串口上跳了 ±40 个 LSB查了半天才发现是采样时间太短、采样电容没充满。换成 92.5 周期后读数稳定在 ±2 LSB 以内。4.3 DMA 配置DMA Settings 里点 Add选择 ADC1参数如下配置项值说明ModeCircular循环模式扫完一轮自动重头开始DirectionPeripheral To MemoryADC → 内存Peripheral IncrementDisableADC 数据寄存器只有一个Memory IncrementEnable数组地址递增按 Rank 顺序填充Data Width (Peripheral)Half WordADC_DR 是 16 位寄存器Data Width (Memory)Half Word对应 uint16_t 数组注意 CubeMX 生成的初始化顺序MX_DMA_Init()必须在MX_ADC1_Init()之前否则 ADC 初始化时找不到 DMA 通道会卡死。CubeMX 通常会自动排好但如果你手动改过 main.c 里的初始化顺序这个坑就会冒出来。4.4 串口与时钟配置为了看实测数据我开了 USART2NUCLEO 板载 ST-Link 虚拟串口波特率 1152008N1重定向 printf 到串口。五、代码实现从启动到校准5.1 变量定义与 ADC 启动CubeMX 生成代码后只需要在用户代码区USER CODE里加三样东西缓冲区数组、校准函数调用、DMA 启动。/* USER CODE BEGIN PV *//* DMA 缓冲区顺序与 Rank 一一对应必须与 Rank 配置一致 */#defineADC_CH_NUM4__IOuint16_tadc_buf[ADC_CH_NUM];/* [0]电池 [1]光敏 [2]NTC [3]VREFINT *//* USER CODE END PV *//* USER CODE BEGIN 2 *//* STM32L4 必须先做校准否则采样误差可能超过 10% */HAL_ADCEx_Calibration_Start(hadc1,ADC_SINGLE_ENDED);/* 启动 ADC DMA 循环搬运启动后 CPU 不再参与 */HAL_ADC_Start_DMA(hadc1,(uint32_t*)adc_buf,ADC_CH_NUM);/* USER CODE END 2 */5.2 电压换算VREFINT 动态校准这是全文最核心的一段。原理VREFINT 是芯片内部带隙基准约 1.2V出厂时在 VDDA3.3V、25℃ 条件下测过一个校准值 VREFINT_CAL存在系统存储区 0x1FFF75AAL4 系列。运行时通过 IN17 通道采到当前 VREFINT_DATA用比例关系反推真实 VDDAVDDA_actual 3.3V × VREFINT_CAL / VREFINT_DATA然后用真实 VDDA 去换算各通道电压。这样即使电池从 4.2V 跌到 3.2V读数也不会系统性漂移。/* USER CODE BEGIN 4 *//* 读取 L4 出厂校准值VREFINT 在 VDDA3.3V/25℃ 时的 ADC 采样值 */#defineVREFINT_CAL_ADDR0x1FFF75AAUL#defineVREFINT_CAL(*(constuint16_t*)VREFINT_CAL_ADDR)/** * 获取当前真实 VDDA * 返回单位mV。原理VREFINT 恒定约 1.2V * VDDA 越高IN17 采到的数字量越小反比关系。 */uint32_tGet_VDDA_mV(void){/* 取多次平均VREFINT 通道读数有轻微噪声 */uint32_tsum0;for(uint8_ti0;i8;i){sumadc_buf[3];}uint32_tvrefint_datasum/8;if(vrefint_data0){return3300;/* 异常保护回退到标称值 */}/* VDDA 3300mV × VREFINT_CAL / VREFINT_DATA */return(uint32_t)((uint64_t)3300UL*VREFINT_CAL/vrefint_data);}/** * 换算指定通道的电压 * param ch 0电池 1光敏 2NTC */floatGet_Channel_Volt(uint8_tch,uint32_tvdda_mv){if(ch3){return0.0f;}/* 12 位 ADC满量程 4095对应 VDDA */return(float)adc_buf[ch]*(float)vdda_mv/4095.0f/1000.0f;}/* USER CODE END 4 */换算公式为什么用 4095 而不是 409612 位 ADC 满量程数字量是 2^12 - 1 4095对应参考电压 VDDA。用 4096 会引入约 0.02% 的常数偏差虽然小但既然要做校准就一次做对。5.3 主循环采集与打印/* USER CODE BEGIN WHILE */while(1){uint32_tvddaGet_VDDA_mV();printf(VDDA%umV ,vdda);printf(BAT%.3fV ,Get_Channel_Volt(0,vdda));printf(LIGHT%.3fV ,Get_Channel_Volt(1,vdda));printf(NTC%.3fV\r\n,Get_Channel_Volt(2,vdda));HAL_Delay(500);/* 打印频率 2HzADC 后台持续刷新 */}/* USER CODE END WHILE */DMA 循环模式下adc_buf[]永远是最新的一批数据主循环想什么时候读都行不需要任何同步机制——单缓冲区、半字写入、读操作是原子的这个场景下足够安全。六、测试验证理论 vs 实测6.1 转换时间验证用定时器触发 GPIO 翻转法测单次转换耗时把 PA5 拉高软件触发一次单通道转换转换完成中断里拉低用示波器量高电平宽度。项目理论值实测值偏差单次转换时间2.5 周期采样12bit187.5ns192ns2.4%单次转换时间47.5 周期采样12bit750ns766ns2.1%偏差来源ADC 启动到采样开始有 1~2 个 ADC 时钟的硬件延迟以及中断响应时间。结论L4 的转换时间理论计算可以直接用于设计采样率预算误差在 3% 以内。6.2 VREFINT 校准精度验证用可调电源给 VDDA 供 3.3V / 3.0V / 2.8V 三档电压分别用固定 3.3V 换算和VREFINT 动态校准两种方式测同一路分压信号真实值用 6 位半万用表标定。VDDA 设定真实电压固定 3.3V 换算误差VREFINT 校准误差3.30V1.650V1.648V-0.12%1.649V-0.06%3.00V1.650V1.815V10.0%1.655V0.30%2.80V1.650V1.944V17.8%1.659V0.55%表格里能看得很清楚VDDA 从 3.3V 跌到 2.8V 时固定换算的误差飙到 17.8%而 VREFINT 校准把误差压在 0.55% 以内。对电池供电产品来说这一组数据就是上 VREFINT 校准最有力的理由。6.3 CPU 占用对比用 SysTick 统计 1 秒内主循环空闲时间占比取反即 CPU 占用四通道 1kHz 采样条件下采集方式CPU 占用主循环最大阻塞轮询38%约 32μs/轮中断12%约 8μs/次DMA1.2%0后台搬运相关阅读《STM32L4 系列 ADC 实战用 CubeIDE 搞定轮询、DMA 和中断三种采集模式》 — 同一主题在 L496 上的实现可与本文交叉验证七、故障排查我踩过的 6 个坑7.1 第一通道读数正常后面的通道全错现象adc_buf[0] 正常adc_buf[1]、[2] 读出的是同一个值。最常见原因Rank 顺序与数组下标没对齐或 DMA 数据宽度配成了 Word32 位。排查步骤检查 CubeMX 里 Rank1~4 与通道的对应关系检查 DMA Data Width 是否为 Half Word在调试器里看 adc_buf 各元素确认是错位还是重复。解决Rank 顺序重排或 DMA 宽度改为 Half Word。验证给三路接不同电压串口输出应三路各不相同。7.2 读数跳变严重±40 个 LSB现象光敏/NTC 通道读数在较大范围内跳动。最常见原因采样时间太短采样电容未充满或信号源内阻过大。排查步骤用万用表量信号源稳定输出时的电压确认信号本身不跳逐步加大采样时间47.5 → 92.5 → 247.5 周期观察读数变化若信号源内阻极高100kΩ需加运放跟随器。解决高阻通道采样时间加到 92.5 周期以上必要时 PCB 上加 100nF 滤波电容。验证读数跳动收窄到 ±2 LSB 内。7.3 不调用校准函数误差高达 10%现象所有通道读数整体偏大或偏小比例一致。最常见原因L4 系列未执行HAL_ADCEx_Calibration_StartADC 内部失调未补偿。排查步骤检查初始化代码里是否调用了校准函数必须在HAL_ADC_Start_DMA之前。解决启动 DMA 前调用一次校准。验证同一输入电压校准前后读数应明显变化误差回落至 1% 内。7.4 DMA 只搬一轮就停了现象串口数据更新几次后卡住不再刷新。最常见原因DMA Mode 没选 Circular或HAL_ADC_Start_DMA后又被意外 Stop。排查步骤检查 DMA 配置 Mode 是否为 Circular搜索代码里是否有HAL_ADC_Stop_DMA/HAL_ADC_Stop调用检查是否误触发了 DMA 传输完成中断并执行了停止操作。解决改 Circular 模式删除多余的 Stop 调用。验证上电运行 1 分钟数据持续刷新。7.5 低功耗模式唤醒后 ADC 读数异常现象从 Stop 模式唤醒后ADC 读数全部异常或为 0。最常见原因L4 在低功耗模式下 ADC 电压调整器被关闭唤醒后未重新使能。排查步骤检查进入 Stop 前是否调用了HAL_ADC_Stop_DMA唤醒后是否重新执行校准 启动流程。解决唤醒后按启动 ADC → 等待 ADRDY → 重新校准 → 重启 DMA顺序恢复。验证反复进出 Stop 模式 100 次读数始终正常。7.6 VREFINT 读数异常偏低现象Get_VDDA_mV算出的 VDDA 明显小于实际供电。最常见原因VREFINT 通道采样时间太短该通道推荐 ≥47.5 周期或 Rank 配置里 VREFINT 未启用。排查步骤检查 CubeMX 是否勾选了 IN17 VREFINT 且 Rank 采样时间 ≥47.5用调试器读 VREFINT_CAL 地址0x1FFF75AA确认不是 0xFFFF读错地址。解决采样时间加长确认校准地址正确。验证VDDA 计算结果与万用表实测偏差 1%。相关阅读《STM32 HAL 库 ADC 采样避坑指南内部基准电压校准的 3 个关键步骤》 — VREFINT 校准地址和原理讲得很细相关阅读《STM32 ADC 采集误差大如何校准内部参考电压》 — 针对误差问题的社区问答八、总结8.1 核心要点回顾多通道连续采集首选DMA 循环模式四通道 1kHz 下 CPU 占用仅 1.2%远低于轮询的 38%每个 Rank 的采样时间要按信号源阻抗单独配高阻通道光敏、NTC92.5 周期起步别图省事统一用最小值STM32L4 启动 DMA 前必须先执行 ADC 校准否则误差可达 10% 以上电池供电场景必须用VREFINT 反推真实 VDDA再换算电压固定 3.3V 换算在 VDDA2.8V 时误差高达 17.8%校准后可压到 0.55%转换时间预算按TCONV 采样时间 12.5 周期计算实测偏差在 3% 以内可直接用于采样率设计。8.2 适用边界与已知问题这套方案最适合四通道以内、单 ADC、采样率 ≤100kHz的连续采集场景。超过这个规模要考虑更多通道用 ADC 双模式L4 支持双 ADC 同步采样、更高采样率用定时器触发代替连续模式、需要绝对同步的场景用注入组通道。已知局限本文用的单缓冲区方案在读数组的同时 DMA 正在写时存在极小的数据撕裂窗口半字写入下概率可忽略但不为零DMA 半传输中断 双缓冲可以根治代码复杂度会上去一些。8.3 扩展方向下一步可以尝试定时器触发 ADC 自动关闭模式AUTOFF让 ADC 只在需要采样时上电低功耗产品里电流能从 mA 级降到几十 μA 级——这是从能采到省电地采的关键一步。也可以研究 L4 的硬件过采样把 12 位结果过采样到 16 位噪声信号的有效分辨率能再提 2~3 位。如需获取本文完整代码和更多实战项目可开通 CSDN 技术会员。参考资料STM32L4 参考手册 RM0351ADC 章节转换时间、采样时间、VREFINT 校准寄存器STM32 ADC DMA 采集模拟量让 CPU 彻底不碰采样ADC 学习笔记从原理到 STM32 实战一篇讲透STM32L4 系列 ADC 实战用 CubeIDE 搞定轮询、DMA 和中断三种采集模式STM32 HAL 库 ADC 采样避坑指南内部基准电压校准的 3 个关键步骤STM32 ADC 采集误差大如何校准内部参考电压ST 官方 WikiGetting started with ADCSTM32L4版本备注硬件平台NUCLEO-L412KBSTM32L412CBT6 可调直流电源 6 位半万用表软件版本STM32CubeMX 6.12.0 STM32CubeL4 固件包 1.13.0 MDK-ARM 5.39兼容说明L412/L431/L496 等 L4 系列可直接使用VREFINT 校准地址一致0x1FFF75AA移植到 F1/F4 系列需更换校准地址F4 为 0x1FF07A2A并注意 ADC 时钟配置差异