文章目录
摘要:多通道 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,然后立刻开始下一个。也就是说:
关键推论有两个:
- 多通道之间必然存在时间差——通道 1 和通道 4 的采样时刻差了"若干个通道的转换时间"。如果采的是同一个快速变化信号(比如电机相电流),这个时间差会造成相位错位。
- 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)为例,容易漏的步骤我会单独标出来:
- 时钟:系统时钟 168MHz,ADC 时钟挂在 APB2 上,注意 ADC 时钟不能超过 36MHz(F407 限制)。CubeMX 里把 ADC 预分频调到 /4 或 /6,让 ADC 时钟落在安全区间。
- 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(右对齐,默认即可)
- DMA Settings:
Add→ADC1,Mode 选 Circular,Data Width 选 Half Word。 - 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 LSB | 27.6 | 0 |
| 滑动平均(N=100) | 2048 | ±5 LSB | 3.1 | ~7.5ms |
| 中值滤波(N=100) | 2048 | ±7 LSB | 5.2 | ~5ms |
| 中值+滑动平均级联 | 2048 | ±1 LSB | 1.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)/30MHz | 3.2µs | 4 通道串行转换累积 |
结论:"跳动超理论值"的部分,几乎都能用电源滤波 + 软件滤波找回;而"通道间时间差"是扫描模式的物理特性,只能通过改用注入组、或降低对同步性的要求来规避,无法用软件消除。
八、故障排查速查表(5 类高频问题)
| # | 现象 | 最可能原因 | 排查步骤 | 解决方案 | 验证 |
|---|---|---|---|---|---|
| 1 | 通道值互相串位 | 缓冲区长度未对齐 / 中途停启 ADC | dump 缓冲区看排布是否平移 | 长度=通道数×样本数;采集期不停启 | 各通道读数归属正确 |
| 2 | 整体固定偏移 | 漏做 ADC 校准 | 对照官方例程 | 启动前 HAL_ADCEx_Calibration_Start | 接基准读数=满量程 |
| 3 | 静止读数乱跳 | 未滤波 / 电源纹波 | 示波器看电源纹波 | 软件滤波 + 电源加滤波电容 | 跳动降到 ±1 LSB |
| 4 | 采集一段时间后数据全 0 | DMA 用了 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 系列部分型号支持差分/过采样,参数略有差异。

405

被折叠的 条评论
为什么被折叠?



