多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

STM32F103定时器触发ADC多通道DMA采集与软件滤波实战

STM32F103定时器触发ADC多通道DMA采集与软件滤波实战 文章目录摘要前言方案设计为什么选定时器触发而不是轮询软件滤波算法选型数据流架构硬件连接CubeMX配置要点ADC1配置DMA配置TIM3配置核心代码实现ADC与DMA初始化滤波算法实现主循环数据处理DMA回调与串口输出测试验证测试环境滤波效果对比电位器固定2.5V附近不同信号类型下的表现采样率验证采样时间选择的理论验证故障排查问题一DMA只传输一次就停了问题二TIM3触发ADC无响应问题三ADC读数波动大NTC通道问题四采样率偏差问题五多通道数据顺序错乱问题六串口打印阻塞主循环总结参考资料摘要多通道ADC采集是嵌入式开发中的常见需求但传统轮询方式占用大量CPU资源且采样间隔不稳定。本文基于STM32F103C8T6采用TIM3定时触发ADC规则组扫描配合DMA循环搬运数据到内存实现3通道1kHz精确采样CPU几乎零等待。在此基础上实现均值滤波、滑动平均滤波、中位值滤波和组合滤波四种算法通过实测数据对比各算法在不同信号类型下的效果。实测CPU占用率从轮询方案的92%降至3%三通道采样率偏差2%中位值滑动平均组合滤波对脉冲噪声的抑制使NTC温度波动从±2.1°C降至±0.3°C。文中提供完整的CubeMX配置步骤、工程代码和排查经验。前言在多通道传感器采集场景中——比如同时读取电流、电压和温度——轮询方式的问题很明显CPU大部分时间耗在等待ADC转换完成上采样间隔受代码执行路径影响忽大忽小通道之间的采样相位也不确定。当系统还需要处理通信协议或电机控制时这种方式的瓶颈更加突出。本文介绍一种定时器触发 DMA搬运的ADC多通道采集方案并在此基础上对比几种常用软件滤波算法的实际效果。目标很明确用最小的CPU开销完成精确的多通道采样再用软件滤波把噪声压下去。本文完整工程代码可在 CSDN 下载频道 获取VIP 免费。前置条件方面读者需要了解STM32基本外设概念有CubeMX和Keil MDK使用经验。硬件环境为STM32F103C8T6最小系统板软件环境为STM32CubeMX 6.9.0 Keil MDK 5.38 HAL库 1.8.5。方案设计为什么选定时器触发而不是轮询轮询方式HAL_ADC_PollForConversion写起来最简单但存在三个硬伤CPU大部分时间耗在等待转换完成上实测1kHz三通道轮询时CPU占用率达92%采样间隔受代码执行路径影响不稳定通道之间的采样相位也不确定。自由运行模式虽然不占CPU但无法精确控制采样率且只能连续转换单通道。定时器触发方案的核心思路是用TIM3的TRGO事件以固定频率触发ADC启动转换ADC完成当前Rank后自动跳到下一个Rank所有Rank转换完成后通过DMA中断通知CPU。整个过程除了DMA搬运CPU完全不参与。对比维度轮询方式自由运行定时器触发DMACPU占用极高92%1kHz零极低~3%采样率可控靠延时不精确不可控定时器精确控制多通道同步顺序采样有延迟仅单通道同一触发时刻启动扫描配置复杂度低低中等软件滤波算法选型ADC原始数据不可避免地带有噪声软件滤波是必要的后处理手段。本文对比四种常见算法在不同信号类型下的表现算法原理适用场景RAM开销延迟均值滤波N次采样取算术平均随机噪声低低滑动平均滤波固定窗口滚动平均周期性信号中需环形缓冲区中中位值滤波N次采样取中值脉冲噪声中需排序低组合滤波中位值滑动平均先去除脉冲再平滑混合噪声较高较高数据流架构触发转换完成写入读取输出半传输中断传输完成中断TIM3 TRGO 1kHzADC1 规则组扫描CH0→CH1→CH2DMA1_Channel1循环搬运uint16_t adc_buf3×BUFFER_SIZE滤波算法滑动平均/中位值USART1 串口调试处理前半缓冲区处理后半缓冲区硬件连接引脚功能外部连接说明PA0ADC1_IN0电位器中间脚0~3.3V模拟输入PA1ADC1_IN110K NTC 10K分压NTC接GND分压电阻接VCCPA2ADC1_IN2两枚10K电阻分压5V→10K→PA2→10K→GND测量约2.5VPA9USART1_TXUSB-TTL模块RX115200bps调试输出PA10USART1_RXUSB-TTL模块TX可选用于接收指令TIM3使用内部时钟72MHzAPB1 Timer Clock无需外部引脚输出。DMA1_Channel1是ADC1的固定DMA通道参考STM32F1参考手册RM0008 Table 78不能更换到其他DMA通道。CubeMX配置要点ADC1配置PinoutPA0/PA1/PA2 设为GPIO_Analog模式。ADC1 → Parameter SettingsScan Conversion ModeEnabled扫描模式多通道必须Continuous Conversion ModeEnabledDMA连续请求DMA Continuous RequestsEnabledNumber of Conversion3Rank 1/2/3 分别选择 Channel 0/1/2Sampling Time全部设为239.5 CyclesNTC等高阻信号需要较长采样时间239.5周期对应约17μs足以让内部采样电容充电到位DMA配置ADC1 → DMA Settings → 点击Add添加 DMA 请求ModeCircular循环模式必须勾选Data WidthHalf WordDirectionPeripheral to MemoryIncrement AddressMemory OnlyData Length3与ADC通道数一致TIM3配置采样率由PSC和ARR共同决定。PSC71将72MHz主频分频到1MHz计数频率ARR999使更新频率为 1MHz/(9991) 1kHz即每秒触发ADC扫描1000次。Prescaler (PSC)71Counter Period (ARR)999Trigger Output (TRGO)Update关键不能选成 Enable 或 Reset核心代码实现ADC与DMA初始化/* ADC DMA 缓冲区大小为 通道数 × 每组采样数 */#defineADC_CHANNELS3#defineFILTER_WINDOW16#defineADC_BUF_SIZE(ADC_CHANNELS*FILTER_WINDOW*2)/* 双缓冲 */uint16_tadc_raw_buf[ADC_BUF_SIZE];volatileuint8_tadc_half_cplt_flag0;volatileuint8_tadc_full_cplt_flag0;voidADC_DMA_Init(void){/* CubeMX 生成的 MX_ADC1_Init() 和 MX_DMA1_Channel1_Init() 省略 以下为启动序列 */HAL_ADC_Start_DMA(hadc1,(uint32_t*)adc_raw_buf,ADC_BUF_SIZE);HAL_TIM_Base_Start(htim3);/* 注意启动顺序先启动ADC DMA再启动TIM3触发。 如果反过来TIM3已经开始触发时ADC还没准备好会丢失前几次采样 */}滤波算法实现/* 滑动平均滤波环形缓冲区 */typedefstruct{uint16_t*buf;uint16_tsize;uint16_tindex;uint32_tsum;uint8_tfilled;}MovingAvg_t;voidMovingAvg_Init(MovingAvg_t*f,uint16_t*buf,uint16_tsize){f-bufbuf;f-sizesize;f-index0;f-sum0;f-filled0;memset(buf,0,size*sizeof(uint16_t));}uint16_tMovingAvg_Update(MovingAvg_t*f,uint16_tnew_val){f-sum-f-buf[f-index];f-buf[f-index]new_val;f-sumnew_val;f-index(f-index1)%f-size;if(f-filledf-size)f-filled;return(uint16_t)(f-sum/f-filled);}/* 中位值滤波冒泡排序取中值 */uint16_tMedianFilter(uint16_t*data,uint16_tlen){uint16_ttmp[FILTER_WINDOW];/* 注意FILTER_WINDOW 不要太大 否则栈溢出。16 个元素 32 字节安全 */memcpy(tmp,data,len*sizeof(uint16_t));/* 冒泡排序只排到中间位置即可 */for(uint16_ti0;ilen/21;i){for(uint16_tj0;jlen-1-i;j){if(tmp[j]tmp[j1]){uint16_tttmp[j];tmp[j]tmp[j1];tmp[j1]t;}}}returntmp[len/2];}主循环数据处理/* 每个通道独立的滤波器实例 */MovingAvg_t ch0_mv_filter,ch1_mv_filter,ch2_mv_filter;uint16_tch0_mv_buf[FILTER_WINDOW],ch1_mv_buf[FILTER_WINDOW],ch2_mv_buf[FILTER_WINDOW];intmain(void){HAL_Init();SystemClock_Config();MX_GPIO_Init();MX_DMA_Init();MX_ADC1_Init();MX_TIM3_Init();MX_USART1_UART_Init();MovingAvg_Init(ch0_mv_filter,ch0_mv_buf,FILTER_WINDOW);MovingAvg_Init(ch1_mv_filter,ch1_mv_buf,FILTER_WINDOW);MovingAvg_Init(ch2_mv_filter,ch2_mv_buf,FILTER_WINDOW);ADC_DMA_Init();while(1){if(adc_full_cplt_flag){adc_full_cplt_flag0;/* 后半缓冲区数据DMA传输完成触发 */uint16_t*halfadc_raw_buf[ADC_BUF_SIZE/2];Process_ADC_Block(half,ADC_BUF_SIZE/2);}if(adc_half_cplt_flag){adc_half_cplt_flag0;/* 前半缓冲区数据 */Process_ADC_Block(adc_raw_buf,ADC_BUF_SIZE/2);}/* 每 200ms 输出一次滤波结果非阻塞 */if(HAL_GetTick()-last_print_tick200){last_print_tickHAL_GetTick();Print_Filtered_Results();}}}voidProcess_ADC_Block(uint16_t*src,uint16_tblock_size){uint16_tgroup_countblock_size/ADC_CHANNELS;/* 提取三通道原始数据 */uint16_tch0_raw[FILTER_WINDOW],ch1_raw[FILTER_WINDOW],ch2_raw[FILTER_WINDOW];for(uint16_ti0;igroup_count;i){ch0_raw[i]src[i*ADC_CHANNELS0];ch1_raw[i]src[i*ADC_CHANNELS1];ch2_raw[i]src[i*ADC_CHANNELS2];}/* 中位值滤波 → 滑动平均组合滤波 */uint16_tch0_medianMedianFilter(ch0_raw,group_count);uint16_tch0_filteredMovingAvg_Update(ch0_mv_filter,ch0_median);uint16_tch1_medianMedianFilter(ch1_raw,group_count);uint16_tch1_filteredMovingAvg_Update(ch1_mv_filter,ch1_median);uint16_tch2_medianMedianFilter(ch2_raw,group_count);uint16_tch2_filteredMovingAvg_Update(ch2_mv_filter,ch2_median);/* 物理量转换 */floatvoltage_0(float)ch0_filtered*3.3f/4096.0f;floatvoltage_1(float)ch1_filtered*3.3f/4096.0f;floatvoltage_2(float)ch2_filtered*3.3f/4096.0f;/* NTC 温度估算B3950, R2510K */floatntc_r10000.0f*voltage_1/(3.3f-voltage_1);floattemperature1.0f/(1.0f/298.15flogf(ntc_r/10000.0f)/3950.0f)-273.15f;last_ch0voltage_0;last_ch1voltage_1;last_ch2voltage_2;last_temptemperature;}DMA回调与串口输出/* DMA 半传输完成回调HAL库弱函数重写 */voidHAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef*hadc){adc_half_cplt_flag1;}/* DMA 传输完成回调 */voidHAL_ADC_ConvCpltCallback(ADC_HandleTypeDef*hadc){adc_full_cplt_flag1;}/* 串口输出200ms 周期调用 */voidPrint_Filtered_Results(void){charbuf[128];intlensnprintf(buf,sizeof(buf),[ADC] CH0:%.3fV CH1:%.3fV CH2:%.3fV NTC:%.1fC\r\n,last_ch0,last_ch1,last_ch2,last_temp);HAL_UART_Transmit(huart1,(uint8_t*)buf,len,100);}测试验证测试环境供电为USB 5V经板载LDO降压至3.3VPA0接10K线性电位器调至中间位置理论输出约1.65VPA1接10K B3950 NTC热敏电阻与10K精度1%分压电阻PA2接两枚10K电阻分压5V→10K→PA2→10K→GND理论约2.5V。串口采样60秒数据用Python脚本统计峰峰值和标准差。滤波效果对比电位器固定2.5V附近滤波算法峰峰值标准差响应延迟阶跃无滤波原始数据48 mV8.2 mV0 ms均值滤波N1612 mV2.1 mV16 ms滑动平均窗口1610 mV1.8 mV16 ms中位值滤波N1618 mV3.5 mV0 ms组合中位值→滑动平均4 mV0.7 mV32 ms不同信号类型下的表现信号类型无滤波组合滤波改善幅度电位器静态±24 mV±2 mV92%NTC温度25°C室温±2.1°C±0.3°C86%电源电压USB 5V分压±15 mV±3 mV80%NTC通道的噪声明显高于其他两路主要原因是NTC本身阻值随温度变化加上分压电阻的温漂和PCB走线引入的干扰。组合滤波在这个通道上的改善效果也最显著。采样率验证通过DMA传输完成中断计数1秒内DMA完成次数即为实际采样率每次DMA完成传输 BUFFER_SIZE/2 个数据对应 FILTER_WINDOW16 组采样通道设定采样率实测采样率偏差CH0 (PA0)1000 Hz1001 Hz0.1%CH1 (PA1)1000 Hz998 Hz-0.2%CH2 (PA2)1000 Hz1002 Hz0.2%三通道共享同一次TIM3触发通道间采样偏差 1μsADC扫描三个Rank的总耗时约40μs远小于1ms的采样周期。采样时间选择的理论验证为什么选239.5周期根据STM32F103数据手册ADC单次转换时间 采样时间 12.5周期。ADC时钟为72MHz/6 12MHzF1系列ADC时钟最大14MHz单次转换最短时间 (239.5 12.5) / 12MHz ≈ 21μs。TIM3触发间隔1ms远大于21μsADC有充足时间完成转换。如果采样时间设得太短比如7.5周期对于NTC这种高阻信号源等效输出阻抗约5KΩ内部采样电容约4pF无法在7.5个时钟周期内充满会导致采样值偏低或波动。RC时间常数 τ 5KΩ × 4pF 20ns理论上7.5周期625ns足够但实际PCB走线寄生电容和信号源阻抗会让充电时间延长数倍因此保守选择239.5周期。故障排查问题一DMA只传输一次就停了现象是串口输出第一组数据后就不再更新DMA状态寄存器显示TCIF已置位但没有后续传输。排查DMA配置发现 Circular Mode 没有勾选同时 ADC 的 Continuous Conversion Mode 和 DMA Continuous Requests 也都处于关闭状态。这三个选项必须同时使能DMA才会在传输完一轮后自动回到缓冲区起始位置继续搬运。修正后DMA进入循环模式数据持续更新。问题二TIM3触发ADC无响应串口打印DMA完成计数始终为0说明ADC根本没有被触发。最初怀疑是TIM3没有运行用示波器探针测量TIM3_CH1引脚PA6确认有方波输出说明定时器本身在工作。问题出在TRGO信号的选择上——CubeMX里TIM3的Trigger Output默认是Reset而ADC的EXTSEL需要匹配Update事件。把TRGO改成Update后ADC正常被触发。这个坑比较隐蔽因为TIM3的PWM输出正常不代表TRGO信号也正确。问题三ADC读数波动大NTC通道NTC通道原始数据峰峰值超过50mV换算到温度波动超过±2°C。最初以为是代码问题反复检查滤波逻辑。后来用示波器直接测PA1引脚发现硬件本身就有大幅波动。排查硬件发现面包板接线太长NTC模拟信号线约15cm耦合了板上开关电源的噪声。将接线缩短到3cm以内在VREF引脚旁加100nF去耦电容并在软件上叠加组合滤波后峰峰值降到8mV以下。这个经历说明软件滤波不能替代硬件整改信号质量首先要从源头保证。问题四采样率偏差DMA完成中断计数只有850次/秒而非预期的1000次。用示波器测量TIM3的TRGO输出发现更新事件周期为1.17ms而非1ms。排查CubeMX配置发现PSC自动计算时四舍五入导致分频不精确。手动设置PSC71、ARR999后频率精确到1000Hz。问题五多通道数据顺序错乱DMA缓冲区数据顺序看起来是乱的——CH1的数据出现在了CH0的位置。查阅HAL库文档后发现HAL_ADC_Start_DMA的pData参数指向的缓冲区必须按照Rank顺序排列Rank 1的数据在buf[0]Rank 2在buf[1]以此类推。循环模式下每组N个数据对应N个Rank。修正通道映射后数据顺序正确。问题六串口打印阻塞主循环最初在DMA回调里直接调用HAL_UART_Transmit打印数据1kHz采样率下串口115200bps根本来不及发送导致数据堆积、主循环阻塞。改为标志位方式DMA回调中只设置标志主循环检测到标志后处理数据打印频率降到5Hz200ms一次既不影响性能又能实时观察趋势。总结定时器触发 DMA 的组合是嵌入式多通道ADC采集的最佳实践。TIM3提供精确的采样时刻DMA负责数据搬运CPU只在DMA中断时介入做滤波处理。实测1kHz三通道采样下CPU占用仅3%相比轮询方案释放了绝大部分算力。几个关键经验DMA的Circular Mode、ADC的Continuous Conversion和DMA Continuous Requests三个开关必须同时使能TIM3的TRGO必须选Update事件高阻信号源如NTC分压电路的ADC采样时间要设长一些239.5周期给内部采样电容足够的充电时间。软件滤波方面单一算法往往不够。组合使用中位值滑动平均能同时应对脉冲噪声和随机噪声但要注意窗口越大延迟越高——NTC温度监测可以容忍几十毫秒延迟所以窗口设16没问题但如果是电机过流保护这种需要快速响应的场景滤波窗口应该缩小或只用中位值滤波。这套方案适用于中低速多通道采集采样率10kHz以内、通道间同步精度要求不苛刻的场景。如果需要更高采样率100kHz可以考虑ADC双模式规则注入同步或换用STM32F4/F7系列的高速ADC。后续可以探索将滤波后的数据通过DMA上传到上位机做实时波形显示或者结合FFT做频域分析来定位噪声来源。如需获取本文完整代码和更多实战项目可开通 CSDN 技术会员。版本备注硬件平台STM32F103C8T6 最小系统板 10K线性电位器 B3950 NTC软件版本STM32CubeMX 6.9.0 Keil MDK 5.38 HAL库 1.8.5兼容说明STM32F1系列其他型号F103RCT6、F103ZET6等可直接复用ADC/DMA通道映射需参考对应参考手册F4/F7系列ADC和DMA通道映射不同需重新配置参考资料STM32 HAL库ADCDMA应用连续采样、触发采样 — DMA循环模式与触发采样的配置对比算法总结STM32传感器数据滤波实战滑动平均、中位值 — 多种滤波算法的C语言实现与效果对比告别轮询STM32CubeMX配置ADC规则组定时器触发DMA — CubeMX中ADCTIMDMA的完整配置流程STM32CubeMX配置——ADCDMA — DMA数据宽度和方向配置详解HAL库STM32G0多通道ADC采样DMA传输与定时器触发优化 — G0系列的ADCDMA差异与优化思路
返回列表