STM32周期波形识别与参数测量:从输入捕获到ADC+DMA采样
简介面向嵌入式开发者的STM32周期波形信号识别与参数测量完整工程覆盖ADC采样、DMA传输、定时器中断、数字滤波及串口输出等关键环节。压缩包共207个文件约747KB以100个.h头文件与96个.c源文件为主体并包含Keil工程文件、编译生成的hex固件、辅助脚本及相关配置文件可直接打开工程编译调试。工程实现了频率、周期、幅度、占空比等参数的实时测量通过定时器捕获与ADCDMA协同工作结合滑动平均、中值滤波等算法提升抗干扰能力代码结构清晰注释完整。已有1554人学习使用适合嵌入式入门及中级开发者学习波形分析思路也可直接移植至工业控制、自动化设备等场景进行二次开发。1. STM32周期波形识别不只是测频率先分清测量原理同样是测一路 50Hz 方波用定时器输入捕获拿到 50.012Hz而用 ADC 采样加过零检测算出来却是 50.000Hz——差距不在硬件在测量原理。STM32周期波形信号识别和参数测量解决的不是读个频率寄存器那么简单你得先判断信号是不是周期波、波形类型是什么、一个周期从哪里开始再谈频率、脉宽、占空比和幅值。识别两个字决定了这条路以采样加算法为主不单靠外设。电机测速、电源纹波分析、传感器校准这类场景里都能直接套用。适合手里有一块开发板、想自己写信号处理代码的人也适合做产品原型时需要把波形参数量化出来的工程师。时域算法做周期测量比 FFT 的频率分辨率更高也更直观本文就按采集—识别—测参—精调这条线展开。2. STM32波形识别的两条路线定时器输入捕获与ADCDMA采样2.1 先搞清识别和测量的硬件边界定时器输入捕获本质上是一种事件计数信号上升沿来了硬件把当前计数器的值锁进捕获寄存器两次捕获值相减就是周期。它只能告诉你沿什么时候到完全看不到波形中间长什么样。所以 stm32定时器捕获测频率 这类经典教程做得再好也解决不了这是正弦波还是方波这种识别问题。ADC 采样才是形态识别的正确出发点。把波形按固定间隔搬进内存后周期、脉宽、幅值、RMS、波形类型全都变成数组上的数学运算。代价是采样率和存储空间要吃资源。STM32 的 ADCDMA 组合正是为了把采样这个动作从 CPU 里解放出来让算法只面对一块连续的内存。2.2 两条路线的原理与误差特性输入捕获测频率的公式很简单f TIM_CLK / (CCR_n − CCR_(n−1))。误差来源有三个时基时钟本身的 ppm、信号噪声引起的边沿抖动、以及用中断读 CCR 时的延迟。频率越高计数器计数值越小同样 ±1 个计数的抖动在误差里占比越大。所以测高频脉冲的正确姿势是测 N 个周期再除以 N而不是卡在单周期上。ADCDMA 路线的核心是采样率。采样定理要求 fs 2f实际做参数测量我一般取信号最高频率的 10 到 20 倍。比如 1kHz 正弦波采样率至少 10kSPS想看清上升沿细节就要更高。时间基准由定时器触发保证采样点间隔严格对齐之后所有参数都建立在这个时间轴上。对比项定时器输入捕获ADCDMA 采样频率测量快且准CPU 开销极低受采样率限制但算法可补偿占空比测量双沿捕获精度高需阈值判断依赖基线波形类型识别不能只看到边沿能靠形状判断抗噪声边沿抖动直接进结果滞后比较插值可抑制适用信号方波、脉冲、电机转速正弦波、方波、三角波、任意周期波2.3 怎么选什么时候该放弃输入捕获只测频率和转速输入捕获是性价比最高的方案一个定时器搞定连 DMA 都省了。但只要需求里出现幅值、RMS、波形类型、占空比同时要这些字眼输入捕获就无能为力了。我一般这样判断信号是纯方波脉冲走捕获信号有可能是正弦波、三角波、畸变波形或者要求同时输出频率和幅度走 ADCDMA。标题里的信号识别决定了本文以 ADC 方案为主线输入捕获只作为对拍验证的手段。两边同时测还能交叉检验算法有没有算错周期。3. STM32波形采集链路定时器触发ADCDMA双缓冲3.1 为什么是定时器触发ADC而不是ADC连续采样ADC 自由运行模式下转换间隔由 ADC 时钟和采样时间决定软件很难精确控制每个点隔多久。频率测量一旦建立在不确定的时间间隔上结果就不可信。定时器触发把采样时刻锁到硬件上TIMx 的更新事件通过 TRGO 引脚直接触发 ADC 转换转换完成再由 DMA 搬进内存。CPU 全程不参与采样时刻的决定这是参数测量里时间戳准确的关键。CubeMX 里勾选 ADC 的External Trigger Conversion Source为 Timer 2 Trigger Out同时把 DMA 设为 Circular 模式硬件链路就通了。用 HAL 库写寄存器细节被封装掉但要注意 ADC 的触发边沿和定时器 TRGO 的映射关系。工程从一款 MCU 换到另一款时这部分通常在重新生成时钟树后要重新确认。3.2 最小采集配置TIM2触发ADC1DMA双缓冲// 假设 ADC1 挂在 APB2TIM2 时钟 84MHz预分频后 1MHz #define ADC_BUF_LEN 1024 static uint16_t adc_buf[ADC_BUF_LEN]; void Waveform_ADC1_Init(void) { // ADC112 位分辨率单通道扫描关闭外部上升沿触发 hadc1.Instance ADC1; hadc1.Init.ClockPrescaler ADC_CLOCK_SYNC_PCLK_DIV4; hadc1.Init.Resolution ADC_RESOLUTION_12B; hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; hadc1.Init.ScanConvMode DISABLE; hadc1.Init.EOCSelection ADC_EOC_SINGLE_CONV; hadc1.Init.ContinuousConvMode DISABLE; // 关键不自由运行 hadc1.Init.ExternalTrigConvEdge ADC_EXTERNALTRIGCONVEDGE_RISING; hadc1.Init.ExternalTrigConv ADC_EXTERNALTRIGCONV_T2_TRGO; HAL_ADC_Init(hadc1); // DMA外设到内存半字对齐循环模式 hdma_adc1.Init.Direction DMA_PERIPH_TO_MEMORY; hdma_adc1.Init.MemInc DMA_MINC_ENABLE; hdma_adc1.Init.Mode DMA_CIRCULAR; hdma_adc1.Init.PeriphDataAlignment DMA_PDATAALIGN_HALFWORD; hdma_adc1.Init.MemDataAlignment DMA_MDATAALIGN_HALFWORD; HAL_DMA_Init(hdma_adc1); __HAL_LINKDMA(hadc1, DMA_Handle, hdma_adc1); HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf, ADC_BUF_LEN); } void Waveform_TIM2_Trigger_Init(uint32_t sample_interval_us) { // 预分频 84-1 得 1MHzPeriod 决定触发间隔 htim2.Instance TIM2; htim2.Init.Prescaler 84 - 1; htim2.Init.Period sample_interval_us - 1; HAL_TIM_Base_Init(htim2); TIM_MasterConfigTypeDef sMasterConfig {0}; sMasterConfig.MasterOutputTrigger TIM_TRGO_UPDATE; HAL_TIMEx_MasterConfigSynchronization(htim2, sMasterConfig); HAL_TIM_Base_Start(htim2); }触发间隔由Prescaler和Period共同决定预分频把 84MHz 降到 1MHzPeriod 填 999 时每隔 1000 个计数即 1ms 触发一次 ADC。1ms 对应采样率只有 1kSPS适合 50Hz 工频信号测 kHz 级波形时把 Period 调到 9采样率就变成 100kSPS。采样时间参数在HAL_ADC_ConfigChannel的SamplingTime里信号源内阻大时选 28.5 周期以上高速信号选最短档避免采样电容没充满就断开了。双缓冲的两种中断标志分别代表前半段和后半段数据就绪处理主循环只需要看标志位volatile uint8_t adc_half_done 0, adc_full_done 0; void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { adc_half_done 1; // adc_buf[0 .. 511] 已就绪 } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { adc_full_done 1; // adc_buf[512 .. 1023] 已就绪 } while (1) { if (adc_half_done) { adc_half_done 0; Process_Waveform(adc_buf, ADC_BUF_LEN / 2); } if (adc_full_done) { adc_full_done 0; Process_Waveform(adc_buf ADC_BUF_LEN / 2, ADC_BUF_LEN / 2); } }这两个回调跑在中断上下文里面只置标志位不要在回调里做浮点运算或调用 HAL_Delay。DMA 还在持续往内存写数据处理函数消耗的时间一旦超过半个缓冲的时间窗口新数据就会覆盖正在算的旧数据波形会撕裂。这是 stm32 dmaadc hal 方案里最常见的坑。3.3 验证采集链路的三板斧第一用示波器看一个空闲 GPIO 的翻转频率确认定时器实际触发间隔和设定值一致这一步排除时钟配置错误。第二把 ADC 输入短接到 GND 和 VREF分别读回 0x000 和 0xFFF 附近确认量程正确。第三输入一路已知 50Hz 正弦波先用串口把 adc_buf 发出来在 PC 上画图肉眼看波形轮廓正常再往下写算法。链路没验证就调算法出了问题很难分清楚是采样坏了还是计算坏了。4. 周期识别与参数测量算法阈值、过零与RMS计算4.1 先估算基线、幅值范围和信号质量拿到一块缓冲数据后第一件事不是找周期而是算基线。对纯交流信号直接求平均值就是直流分量对叠加了偏置的信号平均值就是那个偏置。最大最小值和平均值一起决定了后面所有阈值的放法。uint16_t vmin 0xFFFF, vmax 0; uint32_t sum 0; for (int i 0; i ADC_BUF_LEN; i) { if (adc_buf[i] vmin) vmin adc_buf[i]; if (adc_buf[i] vmax) vmax adc_buf[i]; sum adc_buf[i]; } uint16_t vmid (vmin vmax) / 2; // 粗略基线 uint16_t vspan vmax - vmin; // 峰峰值 float vscale 3.3f / 4096.0f * (1.0f / gain); // 分压系数修正vmid是用最大最小值的中心估的基线速度快但受噪声尖峰影响。如果信号本身很干净这个值够用如果现场干扰大改成先排掉前后 5% 的极值再取中心更稳。vspan太小说明信号没进来或者幅度太低识别无从谈起我一般设一个门槛vspan 50时直接报无有效信号。这里的 50 是 12 位 ADC 的码值对应约 40mV具体值按自己系统噪声底调。4.2 带滞回的过零检测找到所有上升沿用单一阈值判断电平会死在噪声上信号在阈值附近抖动几次软件就数出好几个假周期。带滞回的比较器能解决这个问题思路是离开中心线一段距离才算状态改变。#define HYST_RATIO 0.05f // 滞回宽度取幅值 5% float low_th vmid - vspan * HYST_RATIO; float high_th vmid vspan * HYST_RATIO; int rising_edges[16]; int edge_count 0; int state_prev (adc_buf[0] low_th) ? 0 : (adc_buf[0] high_th ? 1 : 0); for (int i 1; i ADC_BUF_LEN edge_count 16; i) { int state_now (adc_buf[i] low_th) ? 0 : (adc_buf[i] high_th) ? 1 : state_prev; if (state_prev 0 state_now 1) { rising_edges[edge_count] i; // 找到一个上升沿 } state_prev state_now; }滞回宽度取幅值的 5%是折中值太窄抗不住噪声太宽会把小幅度的真实波动也滤掉。状态机的意思是信号必须从低于low_th一路涨到高于high_th中途回来不算数这样在过零区域来回抖动的样本就被挡掉了。数组只存下标之后算周期要用容量 16 足够覆盖 1024 点缓冲里的低频信号如果高频信号边沿多要相应加大数组。4.3 用线性插值把过零点算准识别周期稳定性采样点下标是整数直接拿两个边沿下标相减周期精度最多到 1 个采样间隔。1kSPS 下测 50Hz一个点误差就有 0.1% 的量级。线性插值把过零点细化到分数位置不需要提高采样率就能把分辨率提上去。// 已知 i-1 点低于 vmidi 点高于 vmid求穿过 vmid 的分数下标 float t_cross (float)(i - 1) (float)(vmid - adc_buf[i - 1]) / (float)(adc_buf[i] - adc_buf[i - 1]);插值的前提是两点之间信号近似线性。过零附近正弦波本来就接近直线所以这个假设成立方波沿很陡两点间样本差异大插值依然稳定。得到所有上升沿的t_cross后相邻差值乘以采样间隔就是一个周期的时间更稳的做法是取(最后一个沿 - 第一个沿) / (边数 - 1)把每一个周期的随机抖动平均掉。识别周期波形里周期二字应该落在稳定性上。连续几个周期长度偏差超过 5%说明信号不是稳定的周期波也许是变频器输出、调制的脉冲或者随机噪声。这时候频率值即使算出来也没有意义算法应该输出周期不稳定的提示而不是硬给一个数。4.4 频率、占空比、峰峰值与 RMS 计算频率和占空比建立在过零序列上。占空比方波测量简单统计一个周期内高于vmid的样本数除以总样本数再用端点插值修正。正弦波和三角波的占空比概念不强一般报告 50%真正有意义的是 RMS。float period_us (cross[edge_count - 1] - cross[0]) / (edge_count - 1) * sample_interval_us; float freq_hz 1000000.0f / period_us; float sum_sq 0; for (int i 0; i ADC_BUF_LEN; i) { float v (adc_buf[i] - vmid) * vscale; sum_sq v * v; } float rms sqrtf(sum_sq / ADC_BUF_LEN);RMS 计算里先减掉基线vmid再平方否则直流偏置会被算进功率里。得到的 RMS 是交流分量有效值对 220V 市电采样也是这个思路。峰峰值就是vspan * vscale记得乘分压系数和参考电压的缩放。占空比如果由脉宽和周期两个量分别算出来再相除误差会叠加直接在同一个过零区间里数点数误差更小。4.5 顺带识别波形类型RMS/峰值比和最大斜率波形类型识别不需要上深度学习一个比值就够了。对偶极性周期信号RMS 除以峰值幅度峰峰值的一半有固定特征波形RMS / 峰值识别阈值参考正弦波0.7070.62 ~ 0.78三角波0.5770.50 ~ 0.62方波1.000 0.85比值算出来后再补一个斜率校验对数组做一阶差分找最大斜率。方波沿上相邻点差值接近满量程正弦波最大斜率是 2πfA三角波斜率恒定且较小。这两个特征组合起来正弦、方波、三角波基本不会认错。噪声大的环境里多采几帧取平均比值的方差会明显降下来。5. 提高参数精度的三个技巧过采样、周期平均与误差标定5.1 用硬件过采样把 12 位 ADC变成接近 16 位STM32 系列部分型号的 ADC 外设自带硬件过采样把连续多次转换结果累加再右移等效提高分辨率。4 次过采样加 2 位移位就能提升 1 位有效位16 次加 4 位移位提升 2 位。代价是有效采样率下降过采样率 16 时实际能吃进的信号带宽只有原来的 1/16。所以这个技巧只适合慢信号比如 50Hz 工频或者传感器缓变波形。hadc1.Init.OversamplingMode ENABLE; hadc1.Init.Oversampling.Ratio ADC_OVERSAMPLING_RATIO_16; hadc1.Init.Oversampling.RightBitShift ADC_RIGHTBITSHIFT_4;配完这段ADC 转换时间会明显变长定时器触发间隔要相应放大否则触发追不上转换DMA 会漏数据。软件平均是另一种等价手段对同一触发点多次采样求均值能压掉随机噪声。硬件过采样不占 CPU软件平均更灵活二者取舍看 CPU 余量。5.2 多点周期平均消除沿抖动单周期测出的频率总是带随机抖动。把连续 N 个上升沿之间的总时间除以 N抖动按平方根关系摊薄。我在 4.3 里已经用了这个思路工程上更讲究的做法是对所有相邻边沿间隔做一次标准差统计float mean_interval (cross[edge_count - 1] - cross[0]) / (edge_count - 1); float variance 0; for (int i 1; i edge_count; i) { float d (cross[i] - cross[i - 1]) - mean_interval; variance d * d; } variance / (edge_count - 1);标准差如果超过平均周期的 1%说明信号本身有抖动或噪声污染测出来的频率可信度要打个问号。这个统计量还能用来判断周期稳定性比单看频率数字更全面。软件锁相是另一个方向用上一次测出的频率去预测下一个过零点位置然后在预测点附近小窗口里搜真实过零对强噪声信号特别有效。5.3 用信号发生器做整机标定算法写的再漂亮没标定都是自嗨。把信号发生器接到 ADC 输入分别输出 100Hz、1kHz、10kHz 正弦波和方波记录单片机算出的频率和幅值。频率误差应该主要来自晶振偏差幅值误差几乎都来自参考电压VREF和分压电阻的精度。VREF 不准时用芯片内部的VREFINT通道做比例校准这是成本最低的补偿手段。标定的另一个内容是确认 ADC 采样保持时间。信号源内阻 50Ω 一般没问题但如果是高阻传感器不配缓冲器直接接 ADC采样时间不够会看到高频幅度明显衰减。对比信号发生器面板读数和 STM32 算出的峰峰值高频段衰减超过预期就先查这个别急着改算法。5.4 4 个容易踩的坑第一DMA 半传输标志写进回调后主循环处理时间太长导致覆盖用示波器量处理函数的耗时保证小于半缓冲周期。第二定时器触发选错了 TRGO 事件TIM2 用 PWM 输出模式时OCxREF和UPDATE两个触发源别混配了 PWM 输出就不要再期望更新事件触发的频率和占空比设置一致。第三调试器占用的 JTAG 引脚和 ADC 通道冲突PB3、PB4 这类引脚做模拟输入时要先禁用对应调试端口否则读数直接不对。第四回调里用HAL_Delay或任何阻塞调用中断优先级处理不好直接死锁DMA 停在那采样链路整个卡死。验证精度最快的方法同一路信号同时接示波器和 STM32连续读 20 次数值系统误差看平均值差随机误差看标准差。两者都稳定后再固化标定系数这套测量链路基本就算收工了。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →