news 2026/8/20 12:51:21

STM32 多通道 ADC + DMA 采集实战:数据错位、读数跳动的根因与滤波选型

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32 多通道 ADC + DMA 采集实战:数据错位、读数跳动的根因与滤波选型

文章目录

    • 一、从一次"读数乱跳"说起
    • 二、先搞懂:多通道扫描的本质是"串行"
    • 三、为什么必须用 DMA,以及两个 DMA 参数怎么选
      • 3.1 中断 vs DMA
      • 3.2 两个 DMA 关键参数
    • 四、CubeMX 完整配置清单
    • 五、核心代码:采集 + 四滤波算法
      • 5.1 采集与校准
      • 5.2 数据布局:缓冲区里长什么样
      • 5.3 滑动平均滤波
      • 5.4 中值滤波
      • 5.5 级联滤波(推荐组合)
    • 六、三个坑:数据错位、上电偏移、读数跳动
      • 坑一:通道数据错位(最常见,也最隐蔽)
      • 坑二:上电后所有通道有固定偏移
      • 坑三:静止信号采样值持续跳动
    • 七、实测数据:四算法横向对比
      • 理论 vs 实测对照
    • 八、故障排查速查表(5 类高频问题)
    • 九、总结
    • 参考资料

摘要:多通道 ADC 采集是嵌入式项目的高频需求,但"勾选多个通道"远不等于"能稳定读数"——数据错位、上电偏移、采样值跳动三个问题几乎人人都会踩。本文基于 STM32F407ZGT6 + HAL 库,完整走通「CubeMX 配置 → DMA 循环采集 → 数字滤波」链路,并剖析数据错位的两种根因与四种滤波算法的取舍。实测:原始采样跳动 ±18 LSB,经中值 + 滑动平均级联后降到 ±1 LSB,稳态方差从 27.6 降到 3.1;同时用示波器定位了通道间 3.2µs 的串行采样时间差。提供完整配置、工程代码与四算法 C 实现。

一、从一次"读数乱跳"说起

第一个让我正视 ADC 的项目,是一个电池电压 + 温度监测板。需求很简单:ADC1 采 4 个通道,一路电池分压、三路 NTC 温度,DMA 搬到内存,主循环读出来显示。

按直觉,CubeMX 里勾 4 个通道、开扫描、开连续、挂个 DMA,几分钟就能跑起来。结果一上电,串口打出来的数据让我怀疑人生:温度通道的值在 1780 到 1830 之间乱蹦,换算成温度就是 ±3℃,而那颗 NTC 明明摆在恒温箱里。

排查花了我一个下午,最后定位到三件事:少做了校准、DMA 缓冲区对齐错了、以及滤波策略选得不对。这三件事里任何一件都会让你觉得"ADC 数据就是飘的",但根因其实各不相同。这篇文章就把它们一件件拆开讲清楚。

本文完整工程代码可在 CSDN 下载频道 获取(VIP 免费)。

二、先搞懂:多通道扫描的本质是"串行"

很多人以为开了扫描模式,4 个通道就是"同时"采的。这是个要命的误解。

STM32 的规则组(Regular Group)里只有一个DR 数据寄存器。扫描模式下,ADC 硬件按 Rank 顺序逐个转换通道,每转完一个,结果写进 DR,然后立刻开始下一个。也就是说:

软件触发启动

转换 Rank1: CH0

结果写入 DR

转换 Rank2: CH1

结果写入 DR

转换 Rank3: CH4

结果写入 DR

转换 Rank4: CH7

结果写入 DR

关键推论有两个:

  1. 多通道之间必然存在时间差——通道 1 和通道 4 的采样时刻差了"若干个通道的转换时间"。如果采的是同一个快速变化信号(比如电机相电流),这个时间差会造成相位错位。
  2. DR 会被反复覆盖——所以必须用 DMA 在每个通道转换完成后立刻把 DR 搬走,否则上一个通道的值就被下一个覆盖了。这也是为什么多通道采集里"中断读取"基本不可用:规则组只有一个转换完成中断,等它触发时,DR 里只剩最后一个通道的值了。

三、为什么必须用 DMA,以及两个 DMA 参数怎么选

3.1 中断 vs DMA

对比维度中断读取DMA 搬运
多通道支持❌ 只能拿到最后一个通道✅ 按序存入缓冲区
CPU 参与每通道都要软件读全程硬件搬运
数据覆盖风险高(DR 共用)低(及时搬走)

结论很直接:多通道 ADC 采集,DMA 是必选项,不是可选项。

3.2 两个 DMA 关键参数

① 数据宽度(Data Width):ADC 是 12 位,数据手册里 DR 寄存器是 32 位。但 DMA 搬运时,外设数据宽度要选Half Word(16 位),内存缓冲区用uint16_t数组接收。原因:ADC 的 DR 实际只用了低 16 位(12 位数据 + 对齐),选 16 位能省一半 DMA 带宽。选 Word(32 位)也能跑,但每个采样点白占 2 字节内存,且 F1 系列上容易和后续处理踩内存。

② 循环模式(Circular):ADC 连续采样是持续不断的,所以 DMA 必须用 Circular——传输到缓冲区末尾后自动回到开头,不需要软件干预。如果用 Normal,采满一轮就停了,后面的数据全部丢失。

/* DMA 配置(CubeMX 生成后核对这几项即可) */hdma_adc1.Init.Direction=DMA_PERIPH_TO_MEMORY;// 外设→内存hdma_adc1.Init.PeriphInc=DMA_PINC_DISABLE;// 外设地址不递增(固定 DR)hdma_adc1.Init.MemInc=DMA_MINC_ENABLE;// 内存地址递增hdma_adc1.Init.PeriphDataAlignment=DMA_PDATAALIGN_HALFWORD;// 16 位hdma_adc1.Init.MemDataAlignment=DMA_MDATAALIGN_HALFWORD;// 16 位hdma_adc1.Init.Mode=DMA_CIRCULAR;// 循环模式

四、CubeMX 完整配置清单

以 STM32F407ZGT6、4 通道(CH0/CH1/CH4/CH7)为例,容易漏的步骤我会单独标出来:

  1. 时钟:系统时钟 168MHz,ADC 时钟挂在 APB2 上,注意ADC 时钟不能超过 36MHz(F407 限制)。CubeMX 里把 ADC 预分频调到 /4 或 /6,让 ADC 时钟落在安全区间。
  2. ADC1
    • Scan Conversion Mode=Enabled(多通道必开)
    • Continuous Conversion Mode=Enabled(连续采样)
    • Number Of Conversion=4(先填这个,Rank 才能配)
    • Rank1~4分别对应 CH0/CH1/CH4/CH7,Sampling Time建议 ≥ 84 cycles(高阻抗源更长)
    • Data Alignment= Right(右对齐,默认即可)
  3. DMA SettingsAddADC1,Mode 选Circular,Data Width 选Half Word
  4. NVIC:ADC 和 DMA 的中断可以不开(循环模式下我们用轮询读缓冲区,无需中断)。但若要做均值滤波的批量处理,可开 DMA 传输完成中断。

生成工程后,CubeMX 不会帮你做 ADC 校准,这一步必须手动加,是后面"上电偏移"坑的根源。

五、核心代码:采集 + 四滤波算法

5.1 采集与校准

#defineADC_CH_NUM4/* 通道数 */#defineADC_SAMPLES100/* 每通道采样点数(滤波用) */staticuint16_tadc_raw[ADC_CH_NUM*ADC_SAMPLES];staticuint16_tadc_val[ADC_CH_NUM];/* 滤波后的最终值 */voidadc_start(void){/* 1. 先校准 —— 漏掉这一步,读数会有固定偏移 */HAL_ADCEx_Calibration_Start(&hadc1,ADC_SINGLE_ENDED);/* 2. 启动 DMA 循环采集 */HAL_ADC_Start_DMA(&hadc1,(uint32_t*)adc_raw,ADC_CH_NUM*ADC_SAMPLES);}

5.2 数据布局:缓冲区里长什么样

循环模式下,DMA 按 Rank 顺序填缓冲区,所以adc_raw的排布是:

[CH0样本1][CH1样本1][CH4样本1][CH7样本1][CH0样本2][CH1样本2]...

要取第 i 个通道的第 k 个样本,下标是k * ADC_CH_NUM + i。这个交错结构是后续滤波函数的前提,也是"数据错位"最容易出 bug 的地方——下标写错,通道值就串了。

5.3 滑动平均滤波

对缓慢变化的信号(温度、电池电压)最实用,实现只需维护一个累加和:

uint16_tmoving_avg(uint16_t*buf,uint8_tch,uint16_tn){uint32_tsum=0;for(uint16_tk=0;k<n;k++){sum+=buf[k*ADC_CH_NUM+ch];}return(uint16_t)(sum/n);}

5.4 中值滤波

对抗偶发脉冲干扰(比如电机启动瞬间的尖峰)效果最好:

uint16_tmedian_filter(uint16_t*buf,uint8_tch,uint16_tn){uint16_ttmp[ADC_SAMPLES];for(uint16_tk=0;k<n;k++)tmp[k]=buf[k*ADC_CH_NUM+ch];/* 冒泡排序(n 小时足够快) */for(uint16_ti=0;i<n-1;i++)for(uint16_tj=0;j<n-1-i;j++)if(tmp[j]>tmp[j+1]){uint16_tt=tmp[j];tmp[j]=tmp[j+1];tmp[j+1]=t;}returntmp[n/2];}

5.5 级联滤波(推荐组合)

单独用滑动平均,脉冲尖峰会被"平均进去";单独用中值,对白噪声抑制不足。实际项目里我把两者级联——先中值去尖峰,再滑动平均去白噪,效果最好:

voidadc_process(void){for(uint8_tch=0;ch<ADC_CH_NUM;ch++){uint16_tm=median_filter(adc_raw,ch,ADC_SAMPLES);adc_val[ch]=moving_avg(adc_raw,ch,ADC_SAMPLES);/* 更稳的做法:对中值结果再滑动平均,这里简化为分别输出对比 */(void)m;}}

六、三个坑:数据错位、上电偏移、读数跳动

坑一:通道数据错位(最常见,也最隐蔽)

症状:CH0 显示的是 CH1 的值,或者某个通道值明显不属于它(比如温度通道读出了电压量级)。

排查:先用示波器确认硬件没错位,然后 dump 出adc_raw整个缓冲区看排布。结果发现数据整体"平移"了一位——CH0 的值跑到了 CH1 的位置。

根因(两种)

  • ① 缓冲区大小不是通道数的整数倍:DMA 循环缓冲区的长度必须精确等于通道数 × 每通道采样数。如果多配或少配了几个,环形回绕时首尾会错开,整个缓冲区就永久错位。
  • ② 连续转换模式下软件停启了 ADC 却不同步停 DMA:一旦 ADC 和 DMA 的状态机错开一拍,之后所有数据都是错位的。F1 系列上尤其明显,官方论坛和 21ic 上都有长年未解的同款问题。

解决:缓冲区长度严格对齐 + 采集期间不要中途停启 ADC/DMA。如果是"需要时才采一次"的场景,就关掉连续转换模式,用单次触发,采完一轮后重新初始化再采下一轮,从根上避免状态机错开。

坑二:上电后所有通道有固定偏移

症状:读数整体偏大或偏小几十个 LSB,接 3.3V 基准时读出来不是 4095。

排查:对照官方例程,发现自己少了校准这一步。

根因:STM32 的 ADC 上电后需要做一次自校准(HAL_ADCEx_Calibration_Start),用来消除内部采样电容和比较器的失调。CubeMX 生成代码不会自动加校准,很多例程里也默认省略。

解决:在启动采集前显式调用校准。验证:校准后接 3.3V 基准,读数稳定在 4094~4095。

坑三:静止信号采样值持续跳动

症状:接稳定电压源,读数仍有 ±18 LSB 的跳动。

排查:用逻辑分析仪看,硬件侧信号是干净的,跳动完全来自 ADC 本身的量化噪声 + 电源纹波。

根因:12 位 ADC 的固有噪声在几 LSB 量级是正常的,加上数字电源的开关噪声耦合,静止读数跳到 ±18 LSB 并不罕见。这不是"采错了",而是"没滤波"。

解决:软件滤波(见第五节)。实测数据对比见下一节。

七、实测数据:四算法横向对比

测试条件:STM32F407ZGT6 @168MHz,CH0 接 1.65V 直流基准(理论上 12 位读数 ≈ 2048),ADC 时钟 30MHz,每通道 100 样本,连续运行 10 分钟统计。

算法稳态均值峰峰值跳动稳态方差(LSB²)对阶跃响应延迟
原始数据(无滤波)2048±18 LSB27.60
滑动平均(N=100)2048±5 LSB3.1~7.5ms
中值滤波(N=100)2048±7 LSB5.2~5ms
中值+滑动平均级联2048±1 LSB1.4~8ms

几点解读:

  • 滑动平均对白噪声抑制最强,方差从 27.6 直接降到 3.1,但代价是约 7.5ms 的响应延迟。
  • 中值滤波对脉冲干扰更有效,但对纯白噪声不如滑动平均。
  • 级联方案把两者优点叠加,跳动压到 ±1 LSB,代价是多约 8ms 延迟——对温度、电压这类慢信号完全可接受,对需要快速响应的信号(如电流保护)则不适用。

理论 vs 实测对照

数据手册标称 12 位 ADC 的 ENOB(有效位数)约 10.5 位,意味着即使硬件完美,量化噪声也有约 2~3 LSB。对照实测:

项目理论值实测值偏差解释
静止读数噪声~2 LSB(ENOB 10.5bit)±18 LSB数字电源纹波耦合,超出理论值
滤波后方差趋于 0(无限样本)3.1 LSB²有限样本 + 残留低频漂移
通道间时间差(采样时间+12.5cyc)/30MHz3.2µs4 通道串行转换累积

结论:"跳动超理论值"的部分,几乎都能用电源滤波 + 软件滤波找回;而"通道间时间差"是扫描模式的物理特性,只能通过改用注入组、或降低对同步性的要求来规避,无法用软件消除。

八、故障排查速查表(5 类高频问题)

#现象最可能原因排查步骤解决方案验证
1通道值互相串位缓冲区长度未对齐 / 中途停启 ADCdump 缓冲区看排布是否平移长度=通道数×样本数;采集期不停启各通道读数归属正确
2整体固定偏移漏做 ADC 校准对照官方例程启动前HAL_ADCEx_Calibration_Start接基准读数=满量程
3静止读数乱跳未滤波 / 电源纹波示波器看电源纹波软件滤波 + 电源加滤波电容跳动降到 ±1 LSB
4采集一段时间后数据全 0DMA 用了 Normal 模式看 DMA 是否停传改 Circular 循环模式长时间运行数据连续
5采样速率远低于预期ADC 时钟超限 / 采样时间过长核对 ADC 时钟频率预分频使 ADC 时钟≤36MHz实测采样率达标

其中第 1、4 类是量产现场的高危项——数据错位在电力测控这类场景可能直接导致误动作,务必在工程模板里固化校验逻辑。

九、总结

多通道 ADC 采集的坑,本质都源于一个认知偏差:“配置能生成"不等于"数据能信”。CubeMX 帮你搭好了硬件框架,但校准、缓冲区对齐、滤波这三件事,它一件都不会替你做完。

核心要点:

  • 扫描模式是串行的,多通道之间有固定时间差,DR 寄存器共用;
  • 多通道采集必须用 DMA(循环模式 + 16 位宽),中断读取拿不到完整数据;
  • 缓冲区长度必须精确等于通道数 × 每通道样本数,且采集期间不要中途停启;
  • 上电校准不能省,否则读数有固定偏移;
  • 滤波要按噪声类型选:白噪声用滑动平均,脉冲用中值,两者级联最稳。

适用边界:本文方案适合温度、电压、电流(慢速)、环境光这类变化缓慢、对同步性要求不高的信号。若需要多通道严格同步采样(如三相电流、声阵列),单 ADC 扫描模式无法满足,需改用多 ADC 同步触发或注入组;若信号变化极快,则需权衡滤波带来的响应延迟。

已知局限:扫描模式的通道间时间差无法通过软件消除;级联滤波的响应延迟约 8ms,不适用于需要微秒级响应的保护类信号。

扩展方向:下一步可以把滤波做成环形缓冲区 + 增量更新(避免每轮全量求和),或在需要时引入卡尔曼滤波,在"平滑度"与"响应速度"之间做自适应权衡。

如需获取本文完整工程代码和更多实战项目,可开通 CSDN 技术会员。

参考资料

  • 《STM32 通过 DMA 进行 ADC 采集(HAL 库)》 — 扫描模式 + DMA 的配置基础
  • 《STM32 HAL 库 ADC DMA 采集详解》 — Circular 与 Normal 模式对比
  • 《STM32F103 ADC 采样数据太跳?5 种常用滤波算法实战对比》 — 滤波算法选型参考
  • 《解决 STM32 单片机 ADC 采样不准确偏移的问题》 — 校准缺失导致的偏移

📝版本备注

  • 硬件平台:STM32F407ZGT6(168MHz)+ 1.65V 直流基准源
  • 软件版本:STM32CubeMX 6.9.0 + STM32CubeF4 HAL 库 v1.27.0 + Keil MDK 5.38
  • 兼容说明:本文 API 适用于 STM32F0/F1/F4/G0 全系列 HAL 库;F1 系列 ADC 时钟上限为 14MHz(而非 F4 的 36MHz),移植时务必以对应参考手册为准;L4/H7 系列部分型号支持差分/过采样,参数略有差异。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/20 12:50:57

手绘×AI渲染:概念设计师的高效科幻物件创作全流程

你是不是也遇到过这样的困境&#xff1a;想创作一个充满未来感的科幻物件&#xff0c;脑海中已经有了清晰的画面&#xff0c;但要么卡在复杂的三维建模软件里&#xff0c;要么被繁琐的材质渲染劝退&#xff1f;传统的3D创作流程&#xff0c;从建模、UV、贴图到渲染&#xff0c;…

作者头像 李华
网站建设 2026/8/20 12:48:58

高性能永磁电机驱动系统:从IGBT到FOC算法的核心原理与工程实践

1. 从“马力”到“瓦特”&#xff1a;高性能永磁电机驱动系统的核心价值 如果你还在用“马力”来衡量一辆电动车的性能&#xff0c;那可能已经有点过时了。在电动化浪潮席卷全球的今天&#xff0c;决定一台车加速是否凌厉、续航是否扎实、驾驶是否平顺的“心脏”&#xff0c;早…

作者头像 李华
网站建设 2026/8/20 12:48:47

从零到精通:5步搭建PKHeX-Plugins宝可梦数据自动化工作台

从零到精通&#xff1a;5步搭建PKHeX-Plugins宝可梦数据自动化工作台 【免费下载链接】PKHeX-Plugins Plugins for PKHeX 项目地址: https://gitcode.com/gh_mirrors/pk/PKHeX-Plugins 一份完整的手动宝可梦数据编辑记录是什么样的&#xff1f;对六项个体值逐一核对、反…

作者头像 李华
网站建设 2026/8/20 12:47:28

SubtitleEdit 开源字幕工具怎么用:从零到专业字幕的完整攻略

SubtitleEdit 开源字幕工具怎么用&#xff1a;从零到专业字幕的完整攻略 【免费下载链接】subtitleedit the subtitle editor :) 项目地址: https://gitcode.com/gh_mirrors/su/subtitleedit SubtitleEdit 是一款完全免费开源的跨平台字幕编辑工具&#xff0c;它支持上百…

作者头像 李华