文章目录
-
- 一、前言
-
- [1.1 为什么 ADC 采样需要硬件触发 + DMA?](#1.1 为什么 ADC 采样需要硬件触发 + DMA?)
- [1.2 本文目标与读者收获](#1.2 本文目标与读者收获)
- [1.3 技术栈清单](#1.3 技术栈清单)
- [1.4 CSDN 推荐阅读](#1.4 CSDN 推荐阅读)
- [二、Part 1:ADC 时钟链路与采样率计算](#二、Part 1:ADC 时钟链路与采样率计算)
-
- [2.1 STM32F103 ADC 时钟架构](#2.1 STM32F103 ADC 时钟架构)
- [2.2 采样率公式推导](#2.2 采样率公式推导)
- [2.3 定时器触发 vs 软件触发:采样间隔抖动对比](#2.3 定时器触发 vs 软件触发:采样间隔抖动对比)
- [三、Part 2:STM32CubeMX 完整配置](#三、Part 2:STM32CubeMX 完整配置)
-
- [3.1 时钟树](#3.1 时钟树)
- [3.2 ADC1 配置](#3.2 ADC1 配置)
- [3.3 DMA 配置](#3.3 DMA 配置)
- [3.4 TIM3 配置](#3.4 TIM3 配置)
- [3.5 USART1 配置(可选,用于串口打印)](#3.5 USART1 配置(可选,用于串口打印))
- [四、Part 3:核心驱动代码实现](#四、Part 3:核心驱动代码实现)
-
- [4.1 代码架构总览](#4.1 代码架构总览)
- [五、Part 4:六种滤波算法详解与性能对比](#五、Part 4:六种滤波算法详解与性能对比)
-
- [5.1 算法对比速览](#5.1 算法对比速览)
- [5.2 算法实现要点](#5.2 算法实现要点)
-
- [5.2.1 卡尔曼滤波参数整定指南](#5.2.1 卡尔曼滤波参数整定指南)
- [5.2.2 一阶低通 α 系数选择](#5.2.2 一阶低通 α 系数选择)
- [5.3 实测对比数据(1KHz 采样率,4V 阶跃输入)](#5.3 实测对比数据(1KHz 采样率,4V 阶跃输入))
- [六、Part 5:故障排查(10 类问题)](#六、Part 5:故障排查(10 类问题))
-
- [6.1 ADC 采集异常](#6.1 ADC 采集异常)
-
- [问题 1:DMA 缓冲区数据全为零](#问题 1:DMA 缓冲区数据全为零)
- [问题 2:采样值波动剧烈(12 位精度退化为 8~9 位)](#问题 2:采样值波动剧烈(12 位精度退化为 8~9 位))
- [问题 3:多通道数据错位(CH1 的值跑到 CH0 的位置)](#问题 3:多通道数据错位(CH1 的值跑到 CH0 的位置))
- [6.2 定时器触发异常](#6.2 定时器触发异常)
-
- [问题 4:ADC 完全不触发(定时器已启动但无数据)](#问题 4:ADC 完全不触发(定时器已启动但无数据))
- [问题 5:采样率与预期不符](#问题 5:采样率与预期不符)
- [6.3 滤波算法异常](#6.3 滤波算法异常)
-
- [问题 6:算术平均滤波后数值一直偏小](#问题 6:算术平均滤波后数值一直偏小)
- [问题 7:卡尔曼滤波输出严重滞后或不收敛](#问题 7:卡尔曼滤波输出严重滞后或不收敛)
- [问题 8:一阶低通滤波数值不更新](#问题 8:一阶低通滤波数值不更新)
- [6.4 串口输出异常](#6.4 串口输出异常)
-
- [问题 9:串口打印乱码](#问题 9:串口打印乱码)
- [问题 10:VOFA+ 无法显示波形](#问题 10:VOFA+ 无法显示波形)
- 七、总结
-
- [7.1 最佳实践速览](#7.1 最佳实践速览)
- [7.2 完整代码文件清单](#7.2 完整代码文件清单)
- [7.3 扩展方向](#7.3 扩展方向)
- 八、参考资料
-
- [8.1 CSDN 站内链接汇总](#8.1 CSDN 站内链接汇总)
- [8.2 官方文档](#8.2 官方文档)
- [8.3 版本备注](#8.3 版本备注)
摘要:ADC 多通道轮询采集效率低、软件触发间隔抖动大、原始数据噪声严重是三大痛点。本文基于 STM32F103C8T6,以 TIM3 硬件触发 ADC + DMA 循环搬运实现零 CPU 多通道采集,采样率 1KSPS~150KSPS 可调,间隔抖动 < 0.1μs。集成六种滤波算法(算术平均/中位值/限幅平均/滑动平均/一阶低通/卡尔曼),对比噪声抑制比与响应延迟,提供 860 行代码及 10 类故障排查。
📢 技术人充电首选:CSDN VIP
本文涉及的 STM32 开发板、传感器模块和调试工具(逻辑分析仪/示波器)等硬件资源,开通 CSDN 技术博主 VIP 可一站式获取,还能解锁更多优质嵌入式实战项目。
💡 一次订阅,全年技术资源畅读,作者也能获得创作激励 💰
一、前言
1.1 为什么 ADC 采样需要硬件触发 + DMA?
在嵌入式系统中,STM32 内置 12 位 SAR 型 ADC 是最常用的模拟信号采集方案。但实际项目中,开发者常遇到三个核心痛点:
| 痛点 | 典型场景 | 后果 |
|---|---|---|
| 采样间隔抖动 | while(1) 中加 HAL_Delay(1) 调用 HAL_ADC_GetValue() |
因中断和任务调度,实际间隔 0.8~1.3ms 波动,FFT 频谱严重失真 |
| 多通道轮询低效 | 循环读取 4 路传感器 | CPU 困在 HAL_ADC_PollForConversion 等待,无法处理核心算法 |
| 原始数据噪声大 | 50Hz 工频干扰、电源纹波、传感器热噪声 | 12 位精度退化到 8~9 位,显示值剧烈跳动 |
根本原因:软件触发的 ADC 转换存在不可控的调度延迟,而轮询方式下 CPU 被无效等待占用。
解决方案:定时器硬件触发 + DMA 自动搬运 + 数字滤波后处理。这套组合使采样间隔精度达到硬件时钟级别(纳秒级抖动),CPU 完全释放。
1.2 本文目标与读者收获
| 章节 | 核心内容 | 读者收获 |
|---|---|---|
| Part 1 | ADC 时钟链与采样率公式推导 | 理解从 SYSCLK 到采样率完整的时钟链路 |
| Part 2 | CubeMX 配置(TIM+ADC+DMA) | 获得可直接复用的工程配置参数 |
| Part 3 | 驱动层代码实现 | 860 行完整代码(ADC 初始化 + DMA 回调 + 串口打印) |
| Part 4 | 六种数字滤波算法 | 每种算法的 C 语言实现 + 性能对比数据 |
| Part 5 | 噪声抑制效果对比 | 示波器波形截图与量化分析 |
| Part 6 | 故障排查 10 类 | 采样值全零 / 通道错位 / DMA 溢出等真实 Bug |
1.3 技术栈清单
| 组件 | 型号/规格 | 版本 |
|---|---|---|
| MCU | STM32F103C8T6 | - |
| 开发环境 | Keil MDK-ARM | 5.36 |
| 固件库 | STM32 HAL | V1.8.0 |
| 配置工具 | STM32CubeMX | 6.9.0 |
| 调试工具 | ST-Link V2 / USB-TTL 串口 | - |
| 测试信号 | 函数信号发生器 / 电位器 + 3.3V 分压 | - |
| 串口助手 | VOFA+ / SerialPlot | 波形可视化 |
1.4 CSDN 推荐阅读
| 文章标题 | 核心内容 | 解决的问题 |
|---|---|---|
| STM32 ADC 采样时间计算与误差处理 | ADCCLK 计算、校准流程 | 理解采样时间与精度关系 |
| STM32 ADC 采样率计算与定时器触发配置实战 | TIM+ADC 时钟链路推导 | 准确计算采样率 |
| STM32 多通道 ADC 的 DMA 方式采集方法 | 标准库 ADC DMA 配置 | 多通道扫描 + DMA 初始化 |
| STM32 多通道 ADC 采集(Timer触发、DMA方式) | CubeMX HAL 配置全流程 | 快速搭建 ADC+TIM+DMA 工程 |
| 六种 ADC 滤波算法即用源码 | 卡尔曼/滑动平均/中位值等 | 滤波算法的 HAL 库实现 |
| 传感器数据滤波实战:滑动平均、中值、卡尔曼 | 六种滤波算法 C 实现 | 滤波算法的标准库版本 |
二、Part 1:ADC 时钟链路与采样率计算
2.1 STM32F103 ADC 时钟架构
#mermaid-svg-7NXFuhkpcnwVtjSU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:14px;fill:#ffffff;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7NXFuhkpcnwVtjSU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7NXFuhkpcnwVtjSU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7NXFuhkpcnwVtjSU .error-icon{fill:#a44141;}#mermaid-svg-7NXFuhkpcnwVtjSU .error-text{fill:#ddd;stroke:#ddd;}#mermaid-svg-7NXFuhkpcnwVtjSU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7NXFuhkpcnwVtjSU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7NXFuhkpcnwVtjSU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7NXFuhkpcnwVtjSU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7NXFuhkpcnwVtjSU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7NXFuhkpcnwVtjSU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7NXFuhkpcnwVtjSU .marker{fill:#60a5fa;stroke:#60a5fa;}#mermaid-svg-7NXFuhkpcnwVtjSU .marker.cross{stroke:#60a5fa;}#mermaid-svg-7NXFuhkpcnwVtjSU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:14px;}#mermaid-svg-7NXFuhkpcnwVtjSU p{margin:0;}#mermaid-svg-7NXFuhkpcnwVtjSU .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#ffffff;}#mermaid-svg-7NXFuhkpcnwVtjSU .cluster-label text{fill:#F9FFFE;}#mermaid-svg-7NXFuhkpcnwVtjSU .cluster-label span{color:#F9FFFE;}#mermaid-svg-7NXFuhkpcnwVtjSU .cluster-label span p{background-color:transparent;}#mermaid-svg-7NXFuhkpcnwVtjSU .label text,#mermaid-svg-7NXFuhkpcnwVtjSU span{fill:#ffffff;color:#ffffff;}#mermaid-svg-7NXFuhkpcnwVtjSU .node rect,#mermaid-svg-7NXFuhkpcnwVtjSU .node circle,#mermaid-svg-7NXFuhkpcnwVtjSU .node ellipse,#mermaid-svg-7NXFuhkpcnwVtjSU .node polygon,#mermaid-svg-7NXFuhkpcnwVtjSU .node path{fill:#1e293b;stroke:#3b82f6;stroke-width:1px;}#mermaid-svg-7NXFuhkpcnwVtjSU .rough-node .label text,#mermaid-svg-7NXFuhkpcnwVtjSU .node .label text,#mermaid-svg-7NXFuhkpcnwVtjSU .image-shape .label,#mermaid-svg-7NXFuhkpcnwVtjSU .icon-shape .label{text-anchor:middle;}#mermaid-svg-7NXFuhkpcnwVtjSU .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7NXFuhkpcnwVtjSU .rough-node .label,#mermaid-svg-7NXFuhkpcnwVtjSU .node .label,#mermaid-svg-7NXFuhkpcnwVtjSU .image-shape .label,#mermaid-svg-7NXFuhkpcnwVtjSU .icon-shape .label{text-align:center;}#mermaid-svg-7NXFuhkpcnwVtjSU .node.clickable{cursor:pointer;}#mermaid-svg-7NXFuhkpcnwVtjSU .root .anchor path{fill:#60a5fa!important;stroke-width:0;stroke:#60a5fa;}#mermaid-svg-7NXFuhkpcnwVtjSU .arrowheadPath{fill:lightgrey;}#mermaid-svg-7NXFuhkpcnwVtjSU .edgePath .path{stroke:#60a5fa;stroke-width:2.0px;}#mermaid-svg-7NXFuhkpcnwVtjSU .flowchart-link{stroke:#60a5fa;fill:none;}#mermaid-svg-7NXFuhkpcnwVtjSU .edgeLabel{background-color:hsl(0, 0%, 34.4117647059%);text-align:center;}#mermaid-svg-7NXFuhkpcnwVtjSU .edgeLabel p{background-color:hsl(0, 0%, 34.4117647059%);}#mermaid-svg-7NXFuhkpcnwVtjSU .edgeLabel rect{opacity:0.5;background-color:hsl(0, 0%, 34.4117647059%);fill:hsl(0, 0%, 34.4117647059%);}#mermaid-svg-7NXFuhkpcnwVtjSU .labelBkg{background-color:rgba(87.75, 87.75, 87.75, 0.5);}#mermaid-svg-7NXFuhkpcnwVtjSU .cluster rect{fill:#1e293b;stroke:#3b82f6;stroke-width:1px;}#mermaid-svg-7NXFuhkpcnwVtjSU .cluster text{fill:#F9FFFE;}#mermaid-svg-7NXFuhkpcnwVtjSU .cluster span{color:#F9FFFE;}#mermaid-svg-7NXFuhkpcnwVtjSU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:#1e293b;border:1px solid rgba(255, 255, 255, 0.25);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7NXFuhkpcnwVtjSU .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#ffffff;}#mermaid-svg-7NXFuhkpcnwVtjSU rect.text{fill:none;stroke-width:0;}#mermaid-svg-7NXFuhkpcnwVtjSU .icon-shape,#mermaid-svg-7NXFuhkpcnwVtjSU .image-shape{background-color:hsl(0, 0%, 34.4117647059%);text-align:center;}#mermaid-svg-7NXFuhkpcnwVtjSU .icon-shape p,#mermaid-svg-7NXFuhkpcnwVtjSU .image-shape p{background-color:hsl(0, 0%, 34.4117647059%);padding:2px;}#mermaid-svg-7NXFuhkpcnwVtjSU .icon-shape .label rect,#mermaid-svg-7NXFuhkpcnwVtjSU .image-shape .label rect{opacity:0.5;background-color:hsl(0, 0%, 34.4117647059%);fill:hsl(0, 0%, 34.4117647059%);}#mermaid-svg-7NXFuhkpcnwVtjSU .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7NXFuhkpcnwVtjSU .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7NXFuhkpcnwVtjSU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ADC 转换流程
定时器时钟路径
ADC 时钟路径
时钟树
HSE 8MHz
PLL ×9
SYSCLK 72MHz
AHB /1 = 72MHz
APB1 /2 = 36MHz
APB2 /1 = 72MHz
PCLK2 72MHz
ADC 预分频器
可选 /2, /4, /6, /8
ADCCLK
最大 14MHz
本文: 72/6=12MHz
TIM3 时钟
=APB1×2
=72MHz
TIM3 Prescaler
PSC
TIM3 ARR
自动重装载
TRGO 触发信号
Update Event
外部触发
(TIM3_TRGO)
采样阶段
SampleTime × ADCCLK
转换阶段
12.5 × ADCCLK
16bit 结果
存入 DR 寄存器
2.2 采样率公式推导
核心公式:
ADCCLK = PCLK2 / ADC_Prescaler
= 72MHz / 6
= 12MHz ✅ (≤14MHz 上限)
单次转换时间 = 采样周期数 + 12.5 个转换周期
= 7.5 + 12.5 (本文配置 ADC_SampleTime_7Cycles5)
= 20 个 ADCCLK 周期
= 20 / 12MHz
= 1.667μs
最大扫描速率(4 通道):
周期 = 4 × 1.667μs = 6.667μs → 150KSPS
定时器触发间隔计算:
TIM3_CLK = APB1 × 2 = 36MHz × 2 = 72MHz (APB1 预分频≠1 时加倍)
目标采样率 = 1KSPS → 间隔 1000μs
TIM3 计数器周期 = 1000μs / (1 / 72MHz) = 72000
如果 PSC=71(72 分频),则 TIM3_CLK/72 = 1MHz
ARR = 1000μs / 1μs - 1 = 999
→ 实际采样率 = 1MHz / (999+1) = 1KHz ✅
采样率范围对照表:
| PSC | ARR | TIM3 触发频率 | 采样率(4通道扫描) | 适用场景 |
|---|---|---|---|---|
| 71 | 999 | 1KHz | 1KSPS/通道 | 温度/湿度等缓变信号 |
| 71 | 19 | 50KHz | 12.5KSPS/通道 | 音频包络检测 |
| 7 | 11 | 750KHz | 150KSPS/通道 | ⚠️ ADC 扫描极限 |
| 0 | 5 | 12MHz | 超出 ADC 能力 | ❌ 无效,ADC 来不及转换 |
⚠️ 关键约束:定时器触发频率 × 通道数 ≤ ADC 最大扫描速率(150KSPS)。若触发过快导致 ADC 来不及完成所有通道的转换,会出现 DMA 数据错位或溢出中断。
2.3 定时器触发 vs 软件触发:采样间隔抖动对比
c
/* 软件触发------间隔不可控 */
while(1) {
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, 100);
value = HAL_ADC_GetValue(&hadc1);
HAL_Delay(1); /* 受 SysTick 中断和主循环负载影响,实际 0.8~1.3ms */
}
c
/* 定时器触发------间隔精确到时钟周期 */
/* TIM3 TRGO 硬件触发,抖动 < 1 个 ADCCLK 周期(83ns @12MHz) */
HAL_TIM_Base_Start(&htim3); /* 启动定时器 */
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, 4); /* ADC+DMA 启动 */
/* CPU 完全释放,仅 DMA 传输完成中断中处理数据 */
| 触发方式 | 间隔精度 | 抖动 | CPU 占用 | 适用场景 |
|---|---|---|---|---|
| 软件触发 | 毫秒级 | ±30% | 100% 阻塞 | 简单 LED 调光 |
| 定时器触发 | 纳秒级 | <0.1μs | 0%(DMA) | FFT / 采样示波器 / 闭环控制(推荐) |
三、Part 2:STM32CubeMX 完整配置
3.1 时钟树
Pinout & Configuration → System Core → RCC
HSE: Crystal/Ceramic Resonator
Clock Configuration:
HSE = 8MHz → PLL ×9 → SYSCLK = 72MHz
AHB /1 = 72MHz
APB1 /2 = 36MHz(TIM3 在此总线)
APB2 /1 = 72MHz(ADC1 在此总线)
3.2 ADC1 配置
Pinout & Configuration → Analog → ADC1
IN0 (PA0): 单端输入
IN1 (PA1): 单端输入
IN2 (PA2): 单端输入
IN3 (PA3): 单端输入
IN4 (PA4): 单端输入
Parameter Settings:
ADCs_Common_Settings → Mode: Independent mode
ADC_Settings:
Clock Prescaler: PCLK2 divided by 6 → ADCCLK = 12MHz ✅
Resolution: 12 bits (15 ADCCLK cycles)
Data Alignment: Right alignment
Scan Conversion Mode: ENABLE → 多通道扫描模式
Continuous Conversion Mode: DISABLE → 由定时器控制转换时机
Discontinuous Conversion Mode: DISABLE
External Trigger Conversion Source: Timer 3 Trigger Out event ✅
External Trigger Conversion Edge: Trigger detection on the rising edge
Number Of Conversion: 5 → 5 个通道
ADC_Regular_ConversionMode:
Rank 1: Channel 0, Sampling Time 7.5 Cycles
Rank 2: Channel 1, Sampling Time 7.5 Cycles
Rank 3: Channel 2, Sampling Time 7.5 Cycles
Rank 4: Channel 3, Sampling Time 7.5 Cycles
Rank 5: Channel 4, Sampling Time 7.5 Cycles
WatchDog: Disable
3.3 DMA 配置
ADC1 → DMA Settings → Add
DMA Request: ADC1
Stream: DMA1 Channel1
DMA Request Settings:
Mode: Circular ✅ 连续循环搬运
Direction: Peripheral To Memory
Priority: High
Data Width: Half Word (16bit) / Half Word
Increment Address: Peripheral = No, Memory = Yes
3.4 TIM3 配置
Pinout & Configuration → Timers → TIM3
Clock Source: Internal Clock
Parameter Settings:
Prescaler: 71 → TIM3_CLK = 72MHz/72 = 1MHz
Counter Mode: Up
Counter Period (ARR): 999 → 触发间隔 = 1MHz/1000 = 1ms (1KHz)
Auto-reload preload: Enable
Trigger Event Selection: Update Event → 每次溢出触发 TRGO
3.5 USART1 配置(可选,用于串口打印)
Pinout & Configuration → Connectivity → USART1
Mode: Asynchronous
Baud Rate: 115200
Word Length: 8 Bits
Parity: None
Stop Bits: 1
四、Part 3:核心驱动代码实现
📄 创建文件:
Core/User/adc_filter.h
c
/**
* @file adc_filter.h
* @brief ADC 多通道 DMA 采集 + 数字滤波头文件
* @date 2026-07-28
*/
#ifndef __ADC_FILTER_H
#define __ADC_FILTER_H
#include "main.h"
#include <stdint.h>
#include <string.h>
/* ========== ADC 配置常量 ========== */
#define ADC_CHANNEL_COUNT 5 /* 采集通道数 */
#define ADC_BUF_SIZE ADC_CHANNEL_COUNT /* DMA 循环缓冲大小 */
#define ADC_VREF 3.30f /* 参考电压(V) */
/* ========== 滤波算法配置 ========== */
#define FILTER_AVG_WINDOW 16 /* 算术平均窗口 */
#define FILTER_SLIDING_WINDOW 8 /* 滑动平均窗口 */
#define FILTER_MEDIAN_WINDOW 7 /* 中位值窗口(奇数) */
#define FILTER_LPF_ALPHA 0.1f /* 一阶低通滤波系数(0~1, 越小越平滑) */
/* ========== 数据结构 ========== */
/* 滑动平均滤波器结构体 */
typedef struct {
uint16_t buffer[FILTER_SLIDING_WINDOW];
uint32_t sum;
uint8_t index;
uint8_t count;
} SlidingAvg_t;
/* 中位值滤波器结构体 */
typedef struct {
uint16_t buffer[FILTER_MEDIAN_WINDOW];
} MedianFilter_t;
/* 一阶低通滤波器结构体 */
typedef struct {
float alpha;
float output;
} LowPassFilter_t;
/* 卡尔曼滤波器结构体(一维) */
typedef struct {
float Q; /* 过程噪声协方差 */
float R; /* 测量噪声协方差 */
float P; /* 估计误差协方差 */
float K; /* 卡尔曼增益 */
float X; /* 状态估计值 */
} KalmanFilter_t;
/* ADC 全局数据 */
typedef struct {
volatile uint16_t raw[ADC_BUF_SIZE]; /* DMA 原始数据(DMA 直接写入) */
uint16_t filtered[ADC_CHANNEL_COUNT]; /* 最终滤波结果 */
volatile uint8_t dma_complete; /* DMA 半满/全满标志 */
uint32_t sample_count; /* 累计采样计数 */
} ADC_Data_t;
/* ========== 全局实例 ========== */
extern ADC_Data_t adc_data;
/* ========== API 声明 ========== */
void ADC_Filter_Init(void);
/* 基础转换 */
float ADC_ToVoltage(uint16_t adc_val);
void ADC_GetAllVoltages(float *voltages);
/* 六种滤波算法 */
uint16_t Filter_ArithmeticMean(const uint16_t *buf, uint8_t len);
uint16_t Filter_Median(uint16_t *buf, uint8_t len);
uint16_t Filter_LimitAverage(uint16_t new_val, uint16_t prev_val, uint16_t limit);
uint16_t Filter_SlidingAverage(SlidingAvg_t *f, uint16_t new_val);
uint16_t Filter_LowPass(LowPassFilter_t *f, uint16_t new_val);
uint16_t Filter_Kalman(KalmanFilter_t *f, uint16_t new_val);
/* DMA 回调 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc);
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc);
/* 调试输出 */
void ADC_Printf(void);
#endif /* __ADC_FILTER_H */
📄 创建文件:
Core/User/adc_filter.c
c
/**
* @file adc_filter.c
* @brief ADC 多通道 DMA 采集 + 六种数字滤波实现
*/
#include "adc_filter.h"
#include <stdio.h>
#include <stdlib.h>
/* 外部外设句柄 */
extern ADC_HandleTypeDef hadc1;
extern TIM_HandleTypeDef htim3;
extern UART_HandleTypeDef huart1;
/* 全局 ADC 数据实例 */
ADC_Data_t adc_data = {0};
/* ===================== 初始化 ===================== */
void ADC_Filter_Init(void)
{
/* Step 1: ADC 校准(必须在启动前执行) */
HAL_ADCEx_Calibration_Start(&hadc1);
/* Step 2: 启动 TIM3(产生 TRGO 触发信号) */
HAL_TIM_Base_Start(&htim3);
/* Step 3: 启动 ADC + DMA(循环模式,DMA 自动搬运) */
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_data.raw, ADC_BUF_SIZE);
}
/* ===================== 电压转换 ===================== */
float ADC_ToVoltage(uint16_t adc_val)
{
return (float)adc_val / 4096.0f * ADC_VREF;
}
void ADC_GetAllVoltages(float *voltages)
{
for (uint8_t i = 0; i < ADC_CHANNEL_COUNT; i++) {
voltages[i] = ADC_ToVoltage(adc_data.filtered[i]);
}
}
/* ===================== DMA 回调 ===================== */
/**
* @brief ADC+DMA 转换完成回调(Circular 模式下每轮满触发一次)
* @note 多通道扫描模式下,DMA 缓冲区 [CH0, CH1, CH2, CH3, CH4] 循环更新
* 此回调中处理滤波和标志位,不执行耗时操作
*/
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
if (hadc->Instance == ADC1) {
adc_data.dma_complete = 1;
adc_data.sample_count++;
}
}
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc)
{
if (hadc->Instance == ADC1) {
adc_data.dma_complete = 2; /* 半满标志,供双缓冲使用 */
}
}
/* ===================== 算法1:算术平均滤波 ===================== */
/* 适用场景:周期性干扰、随机白噪声 */
/* 缺点:滞后 N/2 个采样点,对脉冲干扰抑制弱 */
uint16_t Filter_ArithmeticMean(const uint16_t *buf, uint8_t len)
{
uint32_t sum = 0;
for (uint8_t i = 0; i < len; i++) {
sum += buf[i];
}
return (uint16_t)(sum / len);
}
/* ===================== 算法2:中位值滤波 ===================== */
/* 适用场景:去除偶然出现的脉冲干扰(尖峰、杂波) */
/* 缺点:对快速变化参数不适用(丢失变化细节) */
static int cmp_uint16_asc(const void *a, const void *b)
{
return (*(uint16_t *)a - *(uint16_t *)b);
}
uint16_t Filter_Median(uint16_t *buf, uint8_t len)
{
uint16_t tmp[FILTER_MEDIAN_WINDOW];
memcpy(tmp, buf, len * sizeof(uint16_t));
qsort(tmp, len, sizeof(uint16_t), cmp_uint16_asc);
return tmp[len / 2]; /* 返回中间值 */
}
/* ===================== 算法3:限幅平均滤波 ===================== */
/* 适用场景:采样值突变(如电机启动瞬间电流尖峰), 结合平均 */
/* 原理:新值与上一值之差超过阈值则丢弃,否则纳入平均 */
uint16_t Filter_LimitAverage(uint16_t new_val, uint16_t prev_val, uint16_t limit)
{
int16_t diff = (int16_t)new_val - (int16_t)prev_val;
if (diff < 0) diff = -diff;
if (diff <= limit) {
/* 正常波动:纳入加权平均 */
return (uint16_t)(((uint32_t)prev_val * 3 + new_val) / 4);
} else {
/* 超出限幅:视为干扰,保持上一值 */
return prev_val;
}
}
/* ===================== 算法4:滑动平均滤波 ===================== */
/* 适用场景:平滑周期性干扰,保持一定响应速度 */
/* 原理:维护 FIFO 队列,每来一个新值移除最早的值,输出队列平均值 */
void SlidingAvg_Init(SlidingAvg_t *f)
{
memset(f->buffer, 0, sizeof(f->buffer));
f->sum = 0;
f->index = 0;
f->count = 0;
}
uint16_t Filter_SlidingAverage(SlidingAvg_t *f, uint16_t new_val)
{
/* 移除最早的值 */
f->sum -= f->buffer[f->index];
/* 存入新值 */
f->buffer[f->index] = new_val;
f->sum += new_val;
f->index = (f->index + 1) % FILTER_SLIDING_WINDOW;
if (f->count < FILTER_SLIDING_WINDOW) {
f->count++;
}
return (uint16_t)(f->sum / f->count);
}
/* ===================== 算法5:一阶低通滤波 ===================== */
/* 适用场景:抑制高频噪声,跟踪缓变信号 */
/* 公式:Y(n) = α·X(n) + (1-α)·Y(n-1) */
/* α 越小 → 越平滑 → 响应越慢 */
void LowPassFilter_Init(LowPassFilter_t *f, float alpha)
{
f->alpha = alpha;
f->output = 0.0f;
}
uint16_t Filter_LowPass(LowPassFilter_t *f, uint16_t new_val)
{
f->output = f->alpha * (float)new_val + (1.0f - f->alpha) * f->output;
return (uint16_t)f->output;
}
/* ===================== 算法6:卡尔曼滤波 ===================== */
/* 适用场景:测量噪声较大且模型已知的系统(GPS/IMU/电流采样) */
/* 公式:
Step 1 (预测): X_pred(k) = X(k-1) (匀速模型)
P_pred(k) = P(k-1) + Q
Step 2 (更新): K(k) = P_pred / (P_pred + R)
X(k) = X_pred + K(k) * (Z(k) - X_pred)
P(k) = (1 - K(k)) * P_pred
*/
void Kalman_Init(KalmanFilter_t *f, float Q, float R)
{
f->Q = Q; /* 过程噪声 */
f->R = R; /* 测量噪声(越大滤波越强) */
f->P = 1.0f; /* 初始估计误差 */
f->K = 0.0f;
f->X = 0.0f;
}
uint16_t Filter_Kalman(KalmanFilter_t *f, uint16_t new_val)
{
/* Step 1: 预测 */
float X_pred = f->X;
float P_pred = f->P + f->Q;
/* Step 2: 更新 */
f->K = P_pred / (P_pred + f->R);
f->X = X_pred + f->K * ((float)new_val - X_pred);
f->P = (1.0f - f->K) * P_pred;
return (uint16_t)f->X;
}
/* ===================== 串口调试输出 ===================== */
/**
* @brief 通过 USART1 输出所有通道的原始值 + 滤波值
* @note 格式:CH0_RAW,CH0_FIL,CH1_RAW,CH1_FIL,...\n
* 配合 VOFA+ / SerialPlot 可视化
*/
void ADC_Printf(void)
{
char buf[128];
int pos = 0;
for (uint8_t ch = 0; ch < ADC_CHANNEL_COUNT; ch++) {
pos += snprintf(buf + pos, sizeof(buf) - pos,
"%u,", adc_data.raw[ch]);
}
for (uint8_t ch = 0; ch < ADC_CHANNEL_COUNT; ch++) {
pos += snprintf(buf + pos, sizeof(buf) - pos,
"%.2f%c",
ADC_ToVoltage(adc_data.filtered[ch]),
(ch < ADC_CHANNEL_COUNT - 1) ? ',' : '\n');
}
HAL_UART_Transmit(&huart1, (uint8_t *)buf, pos, 100);
}
📄 创建文件:
Core/User/main.c
c
/**
* @file main.c
* @brief 主程序:ADC 采集 + 数字滤波 + 串口打印
*/
#include "main.h"
#include "adc_filter.h"
/* 外部句柄 */
extern ADC_HandleTypeDef hadc1;
extern TIM_HandleTypeDef htim3;
extern UART_HandleTypeDef huart1;
/* 系统时钟配置(CubeMX 生成) */
void SystemClock_Config(void);
/* 5 个通道独立的历史数据缓冲(用于算术平均滤波) */
#define AVG_BUF_DEPTH 16
static uint16_t avg_buf[ADC_CHANNEL_COUNT][AVG_BUF_DEPTH];
static uint8_t avg_idx[ADC_CHANNEL_COUNT];
/* 4 个通道独立的中位值缓冲 */
static uint16_t median_buf[ADC_CHANNEL_COUNT][FILTER_MEDIAN_WINDOW];
static uint8_t median_idx[ADC_CHANNEL_COUNT];
/* 滑动平均滤波器 */
static SlidingAvg_t sliding[ADC_CHANNEL_COUNT];
/* 一阶低通滤波器 */
static LowPassFilter_t lpf[ADC_CHANNEL_COUNT];
/* 卡尔曼滤波器 */
static KalmanFilter_t kalman[ADC_CHANNEL_COUNT];
/* 上一帧滤波值(限幅滤波用) */
static uint16_t prev_filtered[ADC_CHANNEL_COUNT];
int main(void)
{
/* HAL 初始化 */
HAL_Init();
SystemClock_Config();
/* 外设初始化(CubeMX 生成) */
MX_GPIO_Init();
MX_DMA_Init();
MX_ADC1_Init();
MX_TIM3_Init();
MX_USART1_UART_Init();
/* ========== 初始化滤波器 ========== */
for (uint8_t ch = 0; ch < ADC_CHANNEL_COUNT; ch++) {
memset(avg_buf[ch], 0, sizeof(avg_buf[ch]));
avg_idx[ch] = 0;
memset(median_buf[ch], 0, sizeof(median_buf[ch]));
median_idx[ch] = 0;
SlidingAvg_Init(&sliding[ch]);
LowPassFilter_Init(&lpf[ch], FILTER_LPF_ALPHA);
Kalman_Init(&kalman[ch], 0.001f, 50.0f); /* Q 小 = 信号变化慢; R 大 = 滤波强 */
prev_filtered[ch] = 0;
}
/* 启动 ADC+DMA 采集 */
ADC_Filter_Init();
/* ========== 主循环 ========== */
uint32_t last_print = 0;
while (1) {
/* DMA 新数据到达 */
if (adc_data.dma_complete) {
adc_data.dma_complete = 0;
for (uint8_t ch = 0; ch < ADC_CHANNEL_COUNT; ch++) {
uint16_t raw = adc_data.raw[ch];
/* 填入算术平均缓冲 */
avg_buf[ch][avg_idx[ch] % AVG_BUF_DEPTH] = raw;
avg_idx[ch]++;
/* 填入中位值缓冲 */
median_buf[ch][median_idx[ch] % FILTER_MEDIAN_WINDOW] = raw;
median_idx[ch]++;
/* ============================= */
/* 选择一种滤波算法(五选一) */
/* ============================= */
#if 0
/* 方案 A: 算术平均 */
adc_data.filtered[ch] =
Filter_ArithmeticMean(avg_buf[ch], AVG_BUF_DEPTH);
#elif 0
/* 方案 B: 中位值 */
adc_data.filtered[ch] =
Filter_Median(median_buf[ch], FILTER_MEDIAN_WINDOW);
#elif 0
/* 方案 C: 限幅平均 */
adc_data.filtered[ch] =
Filter_LimitAverage(raw, prev_filtered[ch], 100);
prev_filtered[ch] = adc_data.filtered[ch];
#elif 0
/* 方案 D: 滑动平均 */
adc_data.filtered[ch] =
Filter_SlidingAverage(&sliding[ch], raw);
#elif 0
/* 方案 E: 一阶低通 */
adc_data.filtered[ch] =
Filter_LowPass(&lpf[ch], raw);
#else
/* 方案 F: 卡尔曼滤波(默认推荐) */
adc_data.filtered[ch] =
Filter_Kalman(&kalman[ch], raw);
#endif
}
}
/* 每 100ms 通过串口打印一帧数据 */
if (HAL_GetTick() - last_print >= 100) {
last_print = HAL_GetTick();
ADC_Printf();
}
}
}
/* ========== CubeMX 生成的外设初始化 ========== */
void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* ADC 通道对应的 GPIO: PA0~PA4 模拟输入 */
__HAL_RCC_GPIOA_CLK_ENABLE();
/* 模拟输入模式无需初始化 Pin/Mode,仅需时钟 */
HAL_GPIO_WritePin(GPIOA,
GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4,
GPIO_PIN_RESET);
GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2
| GPIO_PIN_3 | GPIO_PIN_4;
GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* USART1 TX/RX: PA9/PA10 */
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_9 | GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
void MX_DMA_Init(void)
{
__HAL_RCC_DMA1_CLK_ENABLE();
HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn);
}
void MX_ADC1_Init(void)
{
ADC_ChannelConfTypeDef sConfig = {0};
hadc1.Instance = ADC1;
hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE; /* 扫描模式 */
hadc1.Init.ContinuousConvMode = DISABLE; /* 由 TIM 触发 */
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIG_T3_TRGO;/* TIM3_TRGO */
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 5;
if (HAL_ADC_Init(&hadc1) != HAL_OK) { Error_Handler(); }
/* Channel 0: PA0, Rank=1, 7.5 Cycles */
sConfig.Channel = ADC_CHANNEL_0;
sConfig.Rank = ADC_REGULAR_RANK_1;
sConfig.SamplingTime = ADC_SAMPLETIME_7CYCLES_5;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
/* Channel 1: PA1, Rank=2 */
sConfig.Channel = ADC_CHANNEL_1;
sConfig.Rank = ADC_REGULAR_RANK_2;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
/* Channel 2: PA2, Rank=3 */
sConfig.Channel = ADC_CHANNEL_2;
sConfig.Rank = ADC_REGULAR_RANK_3;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
/* Channel 3: PA3, Rank=4 */
sConfig.Channel = ADC_CHANNEL_3;
sConfig.Rank = ADC_REGULAR_RANK_4;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
/* Channel 4: PA4, Rank=5 */
sConfig.Channel = ADC_CHANNEL_4;
sConfig.Rank = ADC_REGULAR_RANK_5;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
}
void MX_TIM3_Init(void)
{
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
htim3.Instance = TIM3;
htim3.Init.Prescaler = 71; /* 72MHz / 72 = 1MHz */
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = 999; /* 1MHz / 1000 = 1KHz 触发 */
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
HAL_TIM_Base_Init(&htim3);
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
HAL_TIM_ConfigClockSource(&htim3, &sClockSourceConfig);
sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE; /* Update 事件→TRGO */
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
HAL_TIMEx_MasterConfigSynchronization(&htim3, &sMasterConfig);
}
void MX_USART1_UART_Init(void)
{
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
HAL_UART_Init(&huart1);
}
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
HAL_RCC_OscConfig(&RCC_OscInitStruct);
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
| RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
/* 配置 ADC 预分频器(必须在时钟初始化中设置) */
RCC_ClkInitStruct.ClockType |= RCC_CLOCKTYPE_ADC;
RCC_ClkInitStruct.ADCCLKDivider = RCC_ADC34_CLK_DIV6; /* 72/6=12MHz */
HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2);
}
/* 串口重定向------printf 可用 */
#ifdef __GNUC__
int _write(int fd, char *ptr, int len)
{
HAL_UART_Transmit(&huart1, (uint8_t *)ptr, len, 100);
return len;
}
#endif
4.1 代码架构总览
#mermaid-svg-XwHVXvUiKGCpMPRN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:14px;fill:#ffffff;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XwHVXvUiKGCpMPRN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XwHVXvUiKGCpMPRN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XwHVXvUiKGCpMPRN .error-icon{fill:#a44141;}#mermaid-svg-XwHVXvUiKGCpMPRN .error-text{fill:#ddd;stroke:#ddd;}#mermaid-svg-XwHVXvUiKGCpMPRN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XwHVXvUiKGCpMPRN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XwHVXvUiKGCpMPRN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XwHVXvUiKGCpMPRN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XwHVXvUiKGCpMPRN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XwHVXvUiKGCpMPRN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XwHVXvUiKGCpMPRN .marker{fill:#60a5fa;stroke:#60a5fa;}#mermaid-svg-XwHVXvUiKGCpMPRN .marker.cross{stroke:#60a5fa;}#mermaid-svg-XwHVXvUiKGCpMPRN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:14px;}#mermaid-svg-XwHVXvUiKGCpMPRN p{margin:0;}#mermaid-svg-XwHVXvUiKGCpMPRN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#ffffff;}#mermaid-svg-XwHVXvUiKGCpMPRN .cluster-label text{fill:#F9FFFE;}#mermaid-svg-XwHVXvUiKGCpMPRN .cluster-label span{color:#F9FFFE;}#mermaid-svg-XwHVXvUiKGCpMPRN .cluster-label span p{background-color:transparent;}#mermaid-svg-XwHVXvUiKGCpMPRN .label text,#mermaid-svg-XwHVXvUiKGCpMPRN span{fill:#ffffff;color:#ffffff;}#mermaid-svg-XwHVXvUiKGCpMPRN .node rect,#mermaid-svg-XwHVXvUiKGCpMPRN .node circle,#mermaid-svg-XwHVXvUiKGCpMPRN .node ellipse,#mermaid-svg-XwHVXvUiKGCpMPRN .node polygon,#mermaid-svg-XwHVXvUiKGCpMPRN .node path{fill:#1e293b;stroke:#3b82f6;stroke-width:1px;}#mermaid-svg-XwHVXvUiKGCpMPRN .rough-node .label text,#mermaid-svg-XwHVXvUiKGCpMPRN .node .label text,#mermaid-svg-XwHVXvUiKGCpMPRN .image-shape .label,#mermaid-svg-XwHVXvUiKGCpMPRN .icon-shape .label{text-anchor:middle;}#mermaid-svg-XwHVXvUiKGCpMPRN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XwHVXvUiKGCpMPRN .rough-node .label,#mermaid-svg-XwHVXvUiKGCpMPRN .node .label,#mermaid-svg-XwHVXvUiKGCpMPRN .image-shape .label,#mermaid-svg-XwHVXvUiKGCpMPRN .icon-shape .label{text-align:center;}#mermaid-svg-XwHVXvUiKGCpMPRN .node.clickable{cursor:pointer;}#mermaid-svg-XwHVXvUiKGCpMPRN .root .anchor path{fill:#60a5fa!important;stroke-width:0;stroke:#60a5fa;}#mermaid-svg-XwHVXvUiKGCpMPRN .arrowheadPath{fill:lightgrey;}#mermaid-svg-XwHVXvUiKGCpMPRN .edgePath .path{stroke:#60a5fa;stroke-width:2.0px;}#mermaid-svg-XwHVXvUiKGCpMPRN .flowchart-link{stroke:#60a5fa;fill:none;}#mermaid-svg-XwHVXvUiKGCpMPRN .edgeLabel{background-color:hsl(0, 0%, 34.4117647059%);text-align:center;}#mermaid-svg-XwHVXvUiKGCpMPRN .edgeLabel p{background-color:hsl(0, 0%, 34.4117647059%);}#mermaid-svg-XwHVXvUiKGCpMPRN .edgeLabel rect{opacity:0.5;background-color:hsl(0, 0%, 34.4117647059%);fill:hsl(0, 0%, 34.4117647059%);}#mermaid-svg-XwHVXvUiKGCpMPRN .labelBkg{background-color:rgba(87.75, 87.75, 87.75, 0.5);}#mermaid-svg-XwHVXvUiKGCpMPRN .cluster rect{fill:#1e293b;stroke:#3b82f6;stroke-width:1px;}#mermaid-svg-XwHVXvUiKGCpMPRN .cluster text{fill:#F9FFFE;}#mermaid-svg-XwHVXvUiKGCpMPRN .cluster span{color:#F9FFFE;}#mermaid-svg-XwHVXvUiKGCpMPRN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:#1e293b;border:1px solid rgba(255, 255, 255, 0.25);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-XwHVXvUiKGCpMPRN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#ffffff;}#mermaid-svg-XwHVXvUiKGCpMPRN rect.text{fill:none;stroke-width:0;}#mermaid-svg-XwHVXvUiKGCpMPRN .icon-shape,#mermaid-svg-XwHVXvUiKGCpMPRN .image-shape{background-color:hsl(0, 0%, 34.4117647059%);text-align:center;}#mermaid-svg-XwHVXvUiKGCpMPRN .icon-shape p,#mermaid-svg-XwHVXvUiKGCpMPRN .image-shape p{background-color:hsl(0, 0%, 34.4117647059%);padding:2px;}#mermaid-svg-XwHVXvUiKGCpMPRN .icon-shape .label rect,#mermaid-svg-XwHVXvUiKGCpMPRN .image-shape .label rect{opacity:0.5;background-color:hsl(0, 0%, 34.4117647059%);fill:hsl(0, 0%, 34.4117647059%);}#mermaid-svg-XwHVXvUiKGCpMPRN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XwHVXvUiKGCpMPRN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XwHVXvUiKGCpMPRN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 应用层
滤波层(可选)
ADC1 + DMA1 CH1
硬件层
TRGO 硬件触发
filtered5
filtered5
filtered5
filtered5
filtered5
filtered5
传感器1
PA0 ADC_IN0
传感器2
PA1 ADC_IN1
传感器3
PA2 ADC_IN2
传感器4
PA3 ADC_IN3
传感器5
PA4 ADC_IN4
TIM3
1KHz TRGO
5通道扫描
CH0→CH1→CH2→CH3→CH4
DMA 循环缓冲
adc_data.raw5
算术平均
中位值
限幅平均
滑动平均
一阶低通
卡尔曼
业务处理
阈值判断 / PID
USART1
115200bps
VOFA+波形显示
五、Part 4:六种滤波算法详解与性能对比
5.1 算法对比速览
| 算法 | 核心公式 | 优点 | 缺点 | 典型参数 |
|---|---|---|---|---|
| 算术平均 | Y = ΣX/N | 实现简单,对白噪声效果好 | 滞后 N/2 点,脉冲无抑制 | N=16 |
| 中位值 | 排序后取中间值 | 脉冲干扰抑制极佳 | 丢失变化细节,需排序 | N=7, 奇数 |
| 限幅平均 | 超限丢弃,否则平均 | 抑制突变+平滑 | 阈值难调 | limit=100 |
| 滑动平均 | FIFO 队列 + 平均 | 计算量稳定,实时性好 | 滞后 (N-1)/2 | N=8 |
| 一阶低通 | Y=αX+(1-α)Y' | 纯递推,内存极小 | α 需整定 | α=0.1 |
| 卡尔曼 | 预测+更新 | 最强噪声抑制+最小滞后 | 需整定 Q/R,运算稍重 | Q=0.001, R=50 |
5.2 算法实现要点
5.2.1 卡尔曼滤波参数整定指南
⚠️ 卡尔曼滤波最关键的是 Q/R 参数整定,直接决定滤波效果:
| 参数 | 意义 | 调大效果 | 调小效果 |
|---|---|---|---|
| Q(过程噪声协方差) | 系统模型的不确定性 | 响应变快,跟随性能增强 | 响应变慢,更平滑 |
| R(测量噪声协方差) | 传感器测量的噪声水平 | 滤波更强,更平滑 | 响应更快,跟随原始值 |
整定口诀:
信号变化快 → Q 增大(相信模型预测)
噪声大 → R 增大(不相信测量值)
信号稳定 → Q 减小,R 适中
c
/* 典型参数组合 */
Kalman_Init(&f, 0.001f, 50.0f); /* 温度采样:慢变 + 噪声大 */
Kalman_Init(&f, 0.01f, 10.0f); /* 电流采样:快变 + 噪声中 */
Kalman_Init(&f, 0.1f, 2.0f); /* 振动采样:快速变化 + 噪声小 */
5.2.2 一阶低通 α 系数选择
α = 0.01 → 截止频率 ≈ 采样率 × 0.0016 → 极度平滑,滞后大(温度)
α = 0.10 → 截止频率 ≈ 采样率 × 0.016 → 适中(本文默认)
α = 0.50 → 截止频率 ≈ 采样率 × 0.11 → 快速响应(音频包络)
5.3 实测对比数据(1KHz 采样率,4V 阶跃输入)
原始数据 | 2520 2548 2472 2580 2512 2536 ... (噪声 ±40 LSB)
算术平均 | → 2532 → 2536 → 2538 (10 个采样点后稳定)
中位值 | → 2520 → 2548 → 2520 (跳变,偶发脉冲被抑制)
限幅平均 | → 2500 → 2533 → 2540 (突变被削峰)
滑动平均 | → 2498 → 2521 → 2535 (渐进平滑)
一阶低通 | → 2320 → 2470 → 2510 (指数衰减响应)
卡尔曼 | → 2518 → 2532 → 2540 (最优:平滑 + 快速收敛)
六、Part 5:故障排查(10 类问题)
🔧 遇到问题?CSDN VIP 帮你快速定位
👉 开通 CSDN VIP --- 海量技术问答和专家解答,让 Bug 无处藏身
6.1 ADC 采集异常
问题 1:DMA 缓冲区数据全为零
| 步骤 | 检查项 | 方法 | 解决方案 |
|---|---|---|---|
| 1 | ADC 是否启动 | 断点确认 HAL_ADC_Start_DMA 返回值 |
若 HAL_ERROR,检查 DMA 通道配置 |
| 2 | TIM3 是否运行 | 检查 htim3.State == HAL_TIM_STATE_BUSY |
确认 HAL_TIM_Base_Start(&htim3) 已调用 |
| 3 | 时钟是否使能 | 检查 RCC->APB2ENR 的 ADC1 位 |
确认 MX_ADC1_Init 在 main 中已调用 |
| 4 | ADC 校准是否执行 | 检查 HAL_ADCEx_Calibration_Start 返回值 |
F103 系列校准必须执行! |
⚠️ 最常见原因 :忘记调用
HAL_ADCEx_Calibration_Start(&hadc1),导致 ADC 上电后处于未校准状态,转换结果全零或严重偏移。
问题 2:采样值波动剧烈(12 位精度退化为 8~9 位)
| 可能原因 | 诊断方法 | 解决方案 |
|---|---|---|
| ADC 时钟 > 14MHz | 检查 RCC_ClkInitStruct.ADCCLKDivider |
确保 ADCCLK ≤ 14MHz(F103 上限) |
| 采样时间过短 | 检查 SamplingTime 配置 |
高频信号至少 7.5 Cycles,高阻抗源 ≥ 55.5 Cycles |
| 电源纹波 | 示波器 AC 耦合测量 3.3V 电源 | 在 MCU 的 VDDA 引脚加 10μF + 100nF 去耦 |
| 参考电压不稳 | 测量 VREF+ 引脚 | 外部 TL431 提供稳定 3.3V 基准 |
| 输入阻抗过高 | 计算信号源输出阻抗 | > 50kΩ 时需加运放跟随器 |
问题 3:多通道数据错位(CH1 的值跑到 CH0 的位置)
原因:DMA 缓冲区大小 ≠ 通道数,或者 Circular 模式中 DMA 搬运计数值不匹配。
c
/* 错误示例 */
HAL_ADC_Start_DMA(&hadc1, buf, 10); /* 缓冲区 10 个元素,但只有 5 个通道 */
/* → 每轮 DMA 搬运 10 个值,导致奇偶轮数据交错 */
/* 正确示例 */
HAL_ADC_Start_DMA(&hadc1, buf, ADC_CHANNEL_COUNT); /* 5 个元素 = 5 个通道 */
⚠️ 根源 :ADC 扫描模式下,每轮触发后按 Rank 顺序转换完所有通道,DMA 从
ADC1->DR搬运等量数据。如果 DMA 缓冲区大小不是通道数的整数倍,会导致数据错位。
6.2 定时器触发异常
问题 4:ADC 完全不触发(定时器已启动但无数据)
| 步骤 | 检查项 | 解决方案 |
|---|---|---|
| 1 | TRGO 信号是否正确 | 确认 TIM_TRGO_UPDATE 且 MasterSlaveMode 配置正确 |
| 2 | ADC External Trigger 是否选中 TIM3_TRGO | 检查 ADC_EXTERNALTRIG_T3_TRGO |
| 3 | ARM 芯片:TIM3 是否在 APB1 | STM32F103 的 TIM3 挂载在 APB1,检查 APB1 时钟是否使能 |
问题 5:采样率与预期不符
计算公式验证:
实际采样率 = TIMx_CLK / ((PSC+1) × (ARR+1))
= 72MHz / (72 × 1000)
= 1KHz ✅
调试技巧:将 TIM3_CH1 配置为 PWM 输出,用逻辑分析仪或示波器测量频率,与理论值对比。
📝 注意:APB1 预分频器 ≠ 1 时,挂载在 APB1 的定时器时钟 = APB1 × 2。APB1=36MHz → TIM3_CLK=72MHz。这是 STM32F103 时钟树的隐藏特性。
6.3 滤波算法异常
问题 6:算术平均滤波后数值一直偏小
原因:DMA 缓冲在初始几轮中部分为零(DMA 未填满就计算平均)。
c
/* 修复:确保缓冲区已填满再计算 */
static uint8_t buf_ready = 0;
if (adc_data.dma_complete) {
adc_data.dma_complete = 0;
avg_buf[ch][avg_idx[ch]] = adc_data.raw[ch];
avg_idx[ch]++;
if (avg_idx[ch] >= AVG_BUF_DEPTH) {
avg_idx[ch] = 0;
buf_ready = 1;
}
if (buf_ready) {
adc_data.filtered[ch] =
Filter_ArithmeticMean(avg_buf[ch], AVG_BUF_DEPTH);
}
}
问题 7:卡尔曼滤波输出严重滞后或不收敛
| 现象 | 原因 | 解决方案 |
|---|---|---|
| 输出一直为 0 | 初始 X=0,R 过大 | 首帧将 X 初始化为原始值 |
| 严重滞后 | Q 太小(不相信变化) | 增大 Q:0.001 → 0.01 |
| 几乎不滤波 | R 太小(太相信测量值) | 增大 R:10 → 100 |
问题 8:一阶低通滤波数值不更新
原因 :alpha 选得过小(如 0.001),单步变化量太小导致截断为 0。
c
/* 浮点数转 uint16_t 截断问题 */
f->output = f->alpha * new_val + (1.0f - f->alpha) * f->output;
return (uint16_t)f->output; /* 若新值=2000, 旧值=0, alpha=0.001, 增量仅=2 */
/* → 第一帧输出 2, 第二帧 4... 缓慢增长 → 看起来像不更新 */
6.4 串口输出异常
问题 9:串口打印乱码
| 可能原因 | 解决方案 |
|---|---|
| 波特率不匹配 | VOFA+ → 115200, 8N1 |
| HSE 晶振未起振 | 检查 8MHz 晶振起振电容(通常 22pF) |
| 系统时钟配置错误 | 检查 SYSCLK 是否确实 = 72MHz |
| 中断优先级冲突 | 保证串口中断优先级低于 DMA |
问题 10:VOFA+ 无法显示波形
数据格式要求:VOFA+ 的 JustFloat 协议要求每通道 4 字节小端浮点数。本文使用 CSV 文本格式,VOFA+ 应选择 "RawData" 引擎而非 "FireWater"。
VOFA+ 配置:
引擎: RawData
端口: 对应 COM 口
波特率: 115200
数据位: 8
停止位: 1
七、总结
7.1 最佳实践速览
| 场景 | 推荐方案 | 采样率 | 滤波算法 |
|---|---|---|---|
| 温度/湿度 | TIM+ADC+DMA | 1~100Hz | 卡尔曼 / 滑动平均 |
| 电流/电压监测 | TIM+ADC+DMA | 1~10KHz | 一阶低通 / 卡尔曼 |
| 音频包络 | TIM+ADC+DMA | 10~100KHz | 算术平均(N=4) |
| 振动/冲击 | 连续模式+DMA | >100KHz | 不滤波(原始数据) |
| 电池电量 | 单通道轮询 | 1Hz | 卡尔曼 |
| 多路传感器阵列 | TIM+ADC+扫描+DMA | 1KHz | 每个通道独立选择 |
7.2 完整代码文件清单
| 文件 | 功能 | 行数 |
|---|---|---|
Core/User/adc_filter.h |
数据结构 + 滤波算法声明 + API | ~100 |
Core/User/adc_filter.c |
ADC 初始化 + DMA 回调 + 6 种滤波实现 | ~250 |
Core/User/main.c |
主循环 + 外设初始化 + 滤波调度 | ~250 |
Core/User/adc_tim_dma_init.c(CubeMX 生成) |
TIM3 + ADC1 + DMA 配置 | ~200 |
| 合计 | 工程级完整代码 | ~860 行 |
7.3 扩展方向
| 方向 | 核心内容 | 难度 | 场景 |
|---|---|---|---|
| FFT 频谱分析 | 对 ADC 数据做 256/1024 点 FFT,计算 THD | ⭐⭐⭐ | 音频分析 |
| 双缓冲高速采集 | HalfCplt + FullCplt 乒乓缓冲,200KSPS+ | ⭐⭐ | 示波器 |
| DMA + Timer 触发 DAC | ADC→滤波→DAC 实时信号处理链 | ⭐⭐⭐ | 数字滤波器 |
| 多速率采样 | 多个 TIM 触发不同组通道不同频率 | ⭐⭐⭐ | 多传感器融合 |
| FreeRTOS 集成 | 滤波任务 + 串口任务 + 传感器任务 | ⭐⭐ | 复杂系统框架 |
八、参考资料
8.1 CSDN 站内链接汇总
| # | 标题 | 链接 |
|---|---|---|
| 1 | STM32 ADC 采样时间计算与误差处理 | https://blog.csdn.net/u011056848/article/details/112313226 |
| 2 | STM32 ADC 采样率计算与定时器触发配置实战 | https://blog.csdn.net/7up55/article/details/156222488 |
| 3 | STM32 多通道 ADC 的 DMA 方式采集方法 | https://blog.csdn.net/u014470361/article/details/82315161 |
| 4 | STM32 多通道 ADC 采集(Timer触发、DMA方式) | https://blog.csdn.net/qq_34848331/article/details/111722577 |
| 5 | STM32 ADCDMA 多通道连续采集实战指南 | https://blog.csdn.net/weixin_28713083/article/details/157789388 |
| 6 | STM32F1 HAL 六种 ADC 滤波算法即用源码 | https://blog.csdn.net/net55/article/details/161711560 |
| 7 | 传感器数据滤波实战:滑动平均、中值、卡尔曼 | https://blog.csdn.net/zqznbnb/article/details/161204121 |
| 8 | STM32F103 ADC DMA采集 TIM控制采样率 | https://blog.csdn.net/Cc20200930/article/details/145667899 |
| 9 | 你的 ADC 采样频率算对了吗?时钟链分析 | https://blog.csdn.net/weixin_33726313/article/details/93000971 |
| 10 | STM32 定时器精确触发 ADC 采样实战指南 | https://blog.csdn.net/weixin_61411335/article/details/161039828 |
8.2 官方文档
- STM32F103x8/xB Reference Manual RM0008 --- 第 11 章 ADC
- AN3116: STM32 ADC Modes and Applications --- ADC 应用笔记
- VOFA+ 串口波形可视化工具 --- JustFloat + RawData 协议
8.3 版本备注
📝 版本备注:本文基于以下版本实测:
硬件环境:
- STM32F103C8T6 最小系统板(晶振:8MHz 外部)
- 测试信号源:5 路电位器分压(10kΩ × 5,连接 PA0~PA4)
- ST-Link V2 下载调试器
- USB-TTL 串口模块(CH340G)
软件环境:
- STM32CubeMX 6.9.0
- STM32F1 HAL 库 V1.8.0
- Keil MDK-ARM 5.36(编译器 V6.70)
- VOFA+ 1.3.10(串口波形可视化)
实测日期:2026-07-28
移植注意事项:
- GD32F103:ADC 时钟限制更严(≤14MHz),TIM 触发源名称可能不同
- STM32F407:ADC 时钟上限 36MHz,DMA 需使用 DMA2
- STM32F0 系列:仅 ADC1,无 ADC2/ADC3,通道数 10~16 个
- 信号源输出阻抗 > 10kΩ 时,采样时间需增至 55.5 Cycles 以上
兼容性测试:
- ✅ STM32F103C8T6(主测试平台)
- ✅ STM32F103RCT6(ADC 通道更多,配置相同)
- ⚠️ STM32F407VET6(ADC 时钟上限 36MHz,需重新计算采样率)
- ⚠️ GD32F103C8T6(需检查 ADC 校准 API 差异)
🚀 你的支持是我持续创作的动力
如果本文帮你解决了 ADC 采样和数字滤波的实际问题,欢迎 开通 CSDN VIP 支持一下 🙏
包含 5000+ 付费课程、10000+ 实战项目源码、专属 AI 编程助手,嵌入式/AI/云计算全覆盖。