STM32 ADC 多通道 DMA 采集与六种数字滤波算法实战:从 1MHz 采样率计算到卡尔曼滤波全解析

文章目录

    • 一、前言
      • [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_Initmain 中已调用
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_UPDATEMasterSlaveMode 配置正确
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 官方文档

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/云计算全覆盖。

相关推荐
致Great15 小时前
科研人的 AI,不该只回答问题:我用字节TraeWork 跑了一遍真实研究任务
算法
zlinear数据采集卡15 小时前
USB HS 480M高速传输与SRAM记录仪:DABM-D223数据存储系统全解析
arm开发·嵌入式硬件·fpga开发·开源
纵有疾風起15 小时前
线性表的定义与基本操作 — 从逻辑结构到 ADT 接口
数据结构·算法·408·线性表·adt
wuyk55516 小时前
1.栈:后进先出的线性数据结构
c语言·数据结构·stm32·单片机
测试_AI_一辰16 小时前
AI Agent 评测最隐蔽的坑-记忆
人工智能·算法·ai·自动化·ai编程
lucas_AI16 小时前
给YOLO检测器插 LoRA,'插对地方'比'插什么'更要命
人工智能·算法
Crkylin16 小时前
Cortex-M SysTick系统滴答定时器详解
单片机·嵌入式硬件
LuminousCPP16 小时前
数据结构-时间与复杂度|时间/空间复杂度 + 两道力扣练习 + 二分查找复盘
c语言·数据结构·经验分享·算法·leetcode
Nil20816 小时前
leetcode 三数之和
数据结构·算法·leetcode
Hello_Damon_Nikola16 小时前
AC7911从入门到精通
开发语言·嵌入式硬件·物联网·php