第二周:移动平均与连续 DMA(Day4–Day5)

info
与第一周的关系

第一周已经得到 centered_buf[],证明 TIM、ADC、DMA、HT/TC 和块均值去直流能够工作。第二周不重新搭采样链路,而是继续回答两个问题:怎样让信号更平滑,以及怎样证明处理确实有效。

本周主线

第一周输出:centered_buf[]
        ↓
Day4:3 点移动平均 → filtered_buf[]
        ↓
Day5:Circular DMA 连续交付半块
        ↓
mean / Vpp / RMS → USART1 CSV

两天的分工:

Day4:先在一帧数据上看懂“邻域平均”
Day5:再把一帧实验改成连续流,并用指标量化

延续不变的采样条件

项目 Day4 Day5
MCU STM32F407ZG STM32F407ZG
采样触发 TIM2 TRGO TIM2 TRGO
采样率 10 kHz 10 kHz
ADC ADC1_IN0 / PA0,12 位 ADC1_IN0 / PA0,12 位
Buffer 256 点,分成两个 128 点半块 256 点,分成两个 128 点半块
DMA Normal Circular
处理链 去直流 → 3 点平均 去直流 → 3/9 点平均 → 统计 → CSV

每个 128 点半块覆盖:

$$ T_{half}=\frac{128}{10000}=12.8\,\text{ms} $$

这 12.8 ms 不只是时间换算。Day5 进入连续模式后,它也是 CPU 处理一个安全半块的大致时间预算。

第二周的 CubeMX 配置逻辑

第二周不应该重新点击一遍第一周所有选项。正确做法是复制上一天已验证的工程,只修改当天真正变化的部分:

Day3 工程
  ↓ 复制为 Day4
保持 TIM2 / ADC1 / DMA Normal 不变,只加入滤波代码
  ↓ 复制为 Day5
DMA 改 Circular,再加入 USART1 TX 和量化输出代码

这样做的逻辑是控制变量:Day4 出错时优先怀疑滤波代码,Day5 出错时优先检查 Circular DMA、处理速度或串口,而不是把所有外设重新排查一遍。

Day4:CubeMX 不需要新增滤波外设

移动平均是 CPU 对数组做运算,不是 STM32 的某个外设。因此 Day4 在 CubeMX 中应保持:

TIM2:PSC=15,ARR=99,TRGO=Update Event
ADC1:PA0 / Channel 0,TIM2 TRGO Rising Edge,12 bit
DMA2 Stream0 Channel0:Peripheral to Memory,Half Word
DMA Mode:Normal
DMA2 Stream0 global interrupt:Enabled

操作步骤:

  1. 复制已验证的 Day3 STM32F407 工程目录为 Day4。
  2. 打开 .ioc,逐项核对上表,不改变采样链路。
  3. 生成代码并先编译一次,确认复制后的基础工程仍正常。
  4. 在 USER CODE 区域增加 filtered_buf[]、滤波函数和统计字段。
  5. 再编译、下载和观察数组。

如果一开始就同时改采样率、DMA 模式和滤波算法,出现错误时无法判断是哪一层造成的。

Day5:把 DMA 从 Normal 改为 Circular

在 Day5 的 CubeMX 中:

  1. 打开 ADC1 → DMA Settings。
  2. 选中 ADC1 对应的 DMA 行。
  3. 把 Mode 从 Normal 改为 Circular。
  4. 保持下列项目不变:
Stream                = DMA2 Stream0
Channel               = Channel 0
Direction             = Peripheral to Memory
Peripheral Increment  = Disable
Memory Increment      = Enable
Peripheral Width      = Half Word
Memory Width          = Half Word
Priority              = Medium
FIFO                   = Disable

为什么只改 Mode?Normal 在 256 点后停止;Circular 在 TC 后把目标位置回到 adc_buf[0],从而继续下一帧。地址方向、位宽和递增规则没有改变。

Day5:配置 USART1 只发送 CSV

  1. 在 Connectivity → USART1 中选择 Asynchronous。
  2. 在 Pinout 中确认 PA9 = USART1_TX。
  3. 本练习只发送数据,不接收命令;使用 TX 即可。
  4. 在 Parameter Settings 中设置:
Baud Rate       = 115200
Word Length     = 8 Bits
Parity          = None
Stop Bits       = 1
Hardware Flow Control = None
Mode            = Transmit
Oversampling    = 16
  1. 生成代码后确认初始化顺序中出现:
MX_DMA_Init();
MX_ADC1_Init();
MX_TIM2_Init();
MX_USART1_UART_Init();

USART1 只负责把已经算好的统计量送到电脑,它不参与采样触发。即使串口接线错误,ADC/DMA 仍可能正常;所以排错时要把“采样失败”和“看不到输出”分开。

当前 Day5 .ioc 与源码不一致

warning
重新生成前必须注意

当前 E:\embedded\TIM-ADC-DMA\Day5\Day3.ioc 仍记录 DMA_NORMAL,也没有记录 USART1;但当前 main.c 与 stm32f4xx_hal_msp.c 已经手工实现 DMA_CIRCULAR、USART1 TX=PA9。也就是说,当前 .ioc 不能完整复现当前 Day5 源码。

如果直接用这个 .ioc 重新生成,CubeMX 可能覆盖现有配置。可靠做法是:

  1. 先备份或提交当前工程;
  2. 在 CubeMX 中按上面步骤补上 Circular DMA 和 USART1;
  3. 再生成代码;
  4. 对照生成后的初始化函数;
  5. 最后把算法代码保留在 USER CODE 区域。

生成后至少核对:

stm32f4xx_hal_msp.c
  hdma_adc1.Init.Mode = DMA_CIRCULAR
  PA9 Alternate Function = GPIO_AF7_USART1

main.c
  MX_USART1_UART_Init()
  BaudRate = 115200
  Mode = UART_MODE_TX

Day5 上电后的正确观察顺序

  1. 先不接串口,确认 half_dma_callbacks、full_dma_callbacks 持续增长。
  2. 确认 half_blocks_processed、full_blocks_processed 跟得上回调。
  3. 确认 dma_event_overruns == 0。
  4. 再接 USB-TTL,检查串口表头和数据行。
  5. 最后才比较 3 点、9 点窗口的 RMS。

顺序的逻辑是:

先证明连续采样
→ 再证明 CPU 能及时处理
→ 再证明串口能输出
→ 最后讨论滤波效果

第二周常见问题怎么定位

现象 优先检查 原因判断
Day5 仍只处理一次 DMA Mode 是否真为 Circular 代码或 .ioc 仍可能是 Normal
回调增长但 processed 落后 阻塞串口、算法时间、断点 CPU 没在 12.8 ms 内消费半块
dma_event_overruns 增长 长断点、UART 超时、处理过慢 新事件到来时旧事件尚未处理
串口完全没有文字 PA9、共地、COM 口、115200 输出链路与 ADC 链路是两件事
只有乱码 波特率或串口格式 两端必须都是 115200、8-N-1
数组正常但滤波没区别 输入过于稳定或观察尺度不合适 常量经过平均仍是常量
半块边缘出现异常 滤波循环范围 可能访问了 i-1 或 i+1 的越界位置
重新生成后 Circular/USART 消失 .ioc 未同步 手工源码与 CubeMX 配置不一致

Day4:第一次做邻域滤波

工程:E:\embedded\TIM-ADC-DMA\Day4

Day4 从 STM32F407 版 Day3 复制而来,保留原来的 TIM2、ADC1、DMA 和去直流,只增加第三个 Buffer:

uint16_t adc_buf[256];
int16_t  centered_buf[256];
int16_t  filtered_buf[256];

数据关系是:

adc_buf[]
  ↓ 块均值去直流
centered_buf[]
  ↓ 3 点移动平均
filtered_buf[]

3 点移动平均

对内部样点:

$$ y[i]=\frac{x[i-1]+x[i]+x[i+1]}{3} $$

实际代码使用 int32_t 保存中间和:

int32_t neighbor_sum = (int32_t)centered_dst[i - 1U]
                     + (int32_t)centered_dst[i]
                     + (int32_t)centered_dst[i + 1U];
filtered_dst[i] = (int16_t)(neighbor_sum / 3);

移动平均的直观作用是让三个相邻样点共同决定当前输出:

  • 单点快速抖动更容易被摊薄;
  • 输出通常比原始零中心数据平滑;
  • 窗口也会把真实的快速变化和边沿变钝。

因此“更平滑”不是没有代价,平滑程度和动态响应必须一起看。

半块边界怎么处理

Day4 对两个 128 点半块分别处理,首尾样点没有完整的左右邻居。当前代码采用复制边界:

filtered_dst[0] = centered_dst[0];
filtered_dst[length - 1U] = centered_dst[length - 1U];

只对 1..126 做 3 点平均。这样不会访问半块外数据,也不会在 HT 时误读仍被 DMA 写入的后半块。

代价是两个半块的交界处没有跨块连续滤波:

前半块:0 ........ 127
后半块:128 ...... 255
         ↑ 各自处理,边界不互借样点

这是简单、安全的第一版边界策略。以后追求严格连续滤波时,再保存前一块尾部状态或使用流式 FIR。

Day4 统计量

BlockStats 同时保存去直流和滤波后的结果:

raw_mean
centered_mean / centered_min / centered_max / centered_peak_to_peak
filtered_mean / filtered_min / filtered_max / filtered_peak_to_peak

峰峰值为:

$$ V_{pp}=x_{max}-x_{min} $$

它能快速反映摆幅范围,但可能被单个异常点主导。因此不能把“滤波后 Vpp 每一块都必须下降”当成验收标准。

Day4 仍然是单帧实验

hdma_adc1.Init.Mode = DMA_NORMAL;

Day4 的 HT 处理前 128 点,TC 处理后 128 点;采满 256 点后 DMA 停止。它先让滤波算法和数组边界变得可观察,不负责长期连续采样。

Day5:从单帧处理变成连续数据流

工程:E:\embedded\TIM-ADC-DMA\Day5

Day5 最关键的系统变化不是多算了一个指标,而是:

hdma_adc1.Init.Mode = DMA_CIRCULAR;

此时 256 点 Buffer 会循环写入:

DMA 写 0..127
  → HT:低半块安全,DMA 转去写高半块
DMA 写 128..255
  → TC:高半块安全,DMA 回到低半块

CPU 必须在 DMA 再次覆盖同一半块前完成处理。于是 HT/TC 从“单帧内的两个通知”变成持续交替的数据交付节拍。

回调只置标志并统计事件

void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc)
{
    if (hadc->Instance == ADC1) {
        half_dma_callbacks++;
        if (half_dma_callbacks > (half_blocks_processed + 1U)) {
            dma_event_overruns++;
        }
        adc_half_done = 1U;
    }
}

TC 回调对高半块做同样的事。dma_event_overruns 用来发现回调数量已经明显领先于处理数量,即主循环可能没有及时消费半块。

warning
布尔标志的边界

当前 adc_half_done / adc_done 只是布尔标志。如果 CPU 连续错过两个同类事件,多个事件可能合并成一次 true。dma_event_overruns 能提示风险,但更严格的长期系统应使用事件计数、队列、多缓冲或 RTOS 同步机制。

Day5:通用移动平均窗口

Day5 把固定 3 点写法推广为可配置奇数窗口:

#define FILTER_WINDOW  3U
#define FILTER_RADIUS  ((FILTER_WINDOW - 1U) / 2U)

核心循环:

for (uint16_t i = FILTER_RADIUS;
     i < (length - FILTER_RADIUS);
     ++i) {
    int32_t window_sum = 0;
    for (uint16_t k = 0U; k < FILTER_WINDOW; ++k) {
        window_sum += centered_dst[i + k - FILTER_RADIUS];
    }
    filtered_dst[i] =
        (int16_t)(window_sum / (int32_t)FILTER_WINDOW);
}

窗口必须是奇数,才能以当前样点为中心对称取邻域。边界处仍然复制输入值,不访问半块之外。

3 点与 9 点怎么比较

3 点:平滑较弱,响应较快
9 点:平滑通常更强,响应更慢,边沿更钝

对比时必须保持相同输入、采样率和块长度。否则 RMS 或 Vpp 的变化不能只归因于窗口大小。

为什么还要增加 RMS

Vpp 只看一块中的最大值和最小值:

$$ V_{pp}=x_{max}-x_{min} $$

如果只有一个尖峰,Vpp 就可能明显变大。RMS 使用全部样点,更适合观察一块数据的整体波动:

$$ x_{rms}=\sqrt{\frac{1}{N}\sum_{i=0}^{N-1}(x_i-\bar{x})^2} $$

Day5 会分别减去 centered_mean 和 filtered_mean 后再平方,因此这里的 RMS 关注交流/噪声变化,而不是直流偏移。

数值范围

平方和使用 int64_t:

int64_t centered_square_sum = 0;
int64_t filtered_square_sum = 0;

随后使用整数平方根,输出 0.1 ADC count 精度:

centered_rms_x10 = 123  →  12.3 counts

这样既避免平方累加溢出,也不依赖串口浮点格式化。

UART CSV 输出

Day5 使用 USART1 TX:

PA9 / USART1_TX
115200 baud
8 data bits, no parity, 1 stop bit
无硬件流控

输出表头:

seq,half,raw_mean,centered_mean,filtered_mean,centered_vpp,filtered_vpp,centered_rms,filtered_rms
  • seq:输出序号;
  • half=0:HT 交付的低半块;
  • half=1:TC 交付的高半块;
  • raw_mean:输入直流中心;
  • centered_mean / filtered_mean:处理后均值;
  • 两组 Vpp:观察范围;
  • 两组 RMS:比较整体波动强度。

串口发送当前使用阻塞式 HAL_UART_Transmit()。调试时不要长期停在断点上,否则 DMA 仍在运行而 CPU 停止消费,dma_event_overruns 会增加。

接线

STM32 PA9 / TX → USB-TTL RX
STM32 GND      → USB-TTL GND
电平           → 3.3 V TTL

不要把 RS-232 电平直接接入 MCU。设备管理器中出现 COM 口,也不代表该串口已经物理连接到 PA9;仍需根据开发板原理图确认。

第二周验收清单

Day4:算法正确性

adc_error_code == 0
HT / TC 均完成一次
half_blocks_processed / full_blocks_processed 均增加
centered_mean 接近 0
filtered_mean 接近 0
filtered_buf[] 比 centered_buf[] 更平滑
边界没有越界访问

Day5:连续性与定量结果

adc_start_status == HAL_OK
tim2_start_status == HAL_OK
uart_tx_status == HAL_OK
half_dma_callbacks / full_dma_callbacks 持续增长
half_blocks_processed / full_blocks_processed 持续增长
dma_event_overruns 保持 0
串口 CSV 连续输出
稳定输入下 filtered_rms 通常低于 centered_rms

不要要求每一块的 filtered_vpp 都下降;Vpp 对单个异常点敏感。应在相同输入下结合多行 RMS、Vpp 和均值一起判断。

当前已验证到哪里

本地构建日志显示:

Day4:0 Error(s), 0 Warning(s)
Day5:0 Error(s), 0 Warning(s)
Day5 的 9 点窗口对比版本:0 Error(s), 0 Warning(s)

这证明工程能够编译链接,也证明 FILTER_WINDOW=9 的代码路径能通过构建;它还不能替代板上验收。以下内容仍需实物验证:

  • USART1 PA9 的实际接线与 CSV 输出;
  • Circular DMA 长时间运行时是否丢块;
  • dma_event_overruns 是否始终为 0;
  • 相同输入下 3 点、9 点窗口的 RMS 与动态响应差异。

第二周复盘

天数 核心问题 新增理解
Day4 如何把零中心 ADC 数据变得更平滑? 3 点移动平均、半块边界、filtered Buffer、滤波前后 mean/Vpp
Day5 如何连续处理并证明滤波有效? Circular DMA、处理时限、3/9 点窗口、RMS、UART CSV、overrun 监测

本周主线可以压缩成:

ADC 原始数据
→ 块均值去直流
→ centered_buf[]
→ 奇数窗口移动平均
→ filtered_buf[]
→ mean / Vpp / RMS
→ UART CSV 连续观察

第一周解决“数据能不能正确到达并变成有符号信号”,第二周解决“处理结果是否更平滑、能否连续运行、如何定量证明”。