第二周:移动平均与连续 DMA(Day4–Day5)
第一周已经得到 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 点半块覆盖:
这 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
操作步骤:
- 复制已验证的 Day3 STM32F407 工程目录为 Day4。
- 打开
.ioc,逐项核对上表,不改变采样链路。 - 生成代码并先编译一次,确认复制后的基础工程仍正常。
- 在
USER CODE区域增加filtered_buf[]、滤波函数和统计字段。 - 再编译、下载和观察数组。
如果一开始就同时改采样率、DMA 模式和滤波算法,出现错误时无法判断是哪一层造成的。
Day5:把 DMA 从 Normal 改为 Circular
在 Day5 的 CubeMX 中:
- 打开 ADC1 → DMA Settings。
- 选中 ADC1 对应的 DMA 行。
- 把 Mode 从
Normal改为Circular。 - 保持下列项目不变:
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
- 在 Connectivity → USART1 中选择 Asynchronous。
- 在 Pinout 中确认
PA9 = USART1_TX。 - 本练习只发送数据,不接收命令;使用 TX 即可。
- 在 Parameter Settings 中设置:
Baud Rate = 115200
Word Length = 8 Bits
Parity = None
Stop Bits = 1
Hardware Flow Control = None
Mode = Transmit
Oversampling = 16
- 生成代码后确认初始化顺序中出现:
MX_DMA_Init();
MX_ADC1_Init();
MX_TIM2_Init();
MX_USART1_UART_Init();
USART1 只负责把已经算好的统计量送到电脑,它不参与采样触发。即使串口接线错误,ADC/DMA 仍可能正常;所以排错时要把“采样失败”和“看不到输出”分开。
当前 Day5 .ioc 与源码不一致
当前 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 可能覆盖现有配置。可靠做法是:
- 先备份或提交当前工程;
- 在 CubeMX 中按上面步骤补上 Circular DMA 和 USART1;
- 再生成代码;
- 对照生成后的初始化函数;
- 最后把算法代码保留在
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 上电后的正确观察顺序
- 先不接串口,确认
half_dma_callbacks、full_dma_callbacks持续增长。 - 确认
half_blocks_processed、full_blocks_processed跟得上回调。 - 确认
dma_event_overruns == 0。 - 再接 USB-TTL,检查串口表头和数据行。
- 最后才比较 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 点移动平均
对内部样点:
实际代码使用 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
峰峰值为:
它能快速反映摆幅范围,但可能被单个异常点主导。因此不能把“滤波后 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 用来发现回调数量已经明显领先于处理数量,即主循环可能没有及时消费半块。
当前 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 只看一块中的最大值和最小值:
如果只有一个尖峰,Vpp 就可能明显变大。RMS 使用全部样点,更适合观察一块数据的整体波动:
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 连续观察
第一周解决“数据能不能正确到达并变成有符号信号”,第二周解决“处理结果是否更平滑、能否连续运行、如何定量证明”。