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

摘要:多通道 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 采集与校准

#define ADC_CH_NUM    4          /* 通道数 */
#define ADC_SAMPLES   100        /* 每通道采样点数(滤波用) */

static uint16_t adc_raw[ADC_CH_NUM * ADC_SAMPLES];
static uint16_t adc_val[ADC_CH_NUM];   /* 滤波后的最终值 */

void adc_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_t moving_avg(uint16_t *buf, uint8_t ch, uint16_t n)
{
    uint32_t sum = 0;
    for (uint16_t k = 0; k < n; k++)
    {
        sum += buf[k * ADC_CH_NUM + ch];
    }
    return (uint16_t)(sum / n);
}

5.4 中值滤波

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

uint16_t median_filter(uint16_t *buf, uint8_t ch, uint16_t n)
{
    uint16_t tmp[ADC_SAMPLES];
    for (uint16_t k = 0; k < n; k++)
        tmp[k] = buf[k * ADC_CH_NUM + ch];

    /* 冒泡排序(n 小时足够快) */
    for (uint16_t i = 0; i < n - 1; i++)
        for (uint16_t j = 0; j < n - 1 - i; j++)
            if (tmp[j] > tmp[j + 1]) {
                uint16_t t = tmp[j];
                tmp[j] = tmp[j + 1];
                tmp[j + 1] = t;
            }
    return tmp[n / 2];
}

5.5 级联滤波(推荐组合)

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

void adc_process(void)
{
    for (uint8_t ch = 0; ch < ADC_CH_NUM; ch++)
    {
        uint16_t m = 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 技术会员

参考资料


📝 版本备注

  • 硬件平台: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 系列部分型号支持差分/过采样,参数略有差异。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LCG元

你的鼓励将是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值