简介面向学习嵌入式信号处理的开发者这是一套基于STM32F103的波形分辨工程代码通过ADC采集外部模拟信号利用均方根RMS算法识别三角波、方波、正弦波等标准波形适合入门ADC采样和数字信号处理的人员参考。工程压缩包共234个文件大小约12.18MB文件类型以C源码、H头文件为主同时包含Keil工程配置、编译生成的axf/hex文件以及HARDWARE、DSP、SYSTEM、USER等分层目录便于从底层驱动到应用层逐级阅读。该资源已有6876人学习项目基于STM32标准固件库编写模块划分清晰完整演示了ADC通道配置、中断/轮询采样、RMS计算和波形特征匹配的流程同时提供了不同标准波形理论值与实测结果的对比思路。通过学习可以掌握STM32外设操作、波形定量分析、嵌入式代码组织方法为后续开展仪器仪表、信号检测或数据采集类项目打下直接工程基础。1. STM32分辨标准波形本质是采样之后的特征分类问题经常有做信号采集、仪器仪表或者自动化测试的同学问我STM32怎么分辨三角波、方波、正弦波这类标准波形第一次听到这个问题时我也以为要上神经网络后来发现常规做法并不玄学——用ADC把波形采进来去掉直流做一阶差分算几个统计特征量阈值判断就能拿下来。整件事的核心不是「识别算法」而是先搞清楚不同波形在时域特征上差在哪、采样参数怎么配合、边界情况怎么兜底。这篇文章就拿一套可直接运行的C代码为主线讲清楚STM32分辨标准波形的完整落地路径。适合正在做波形检测、信号分类或者想给设备加「波形识别」功能的开发者照着做大概一个下午能跑通。2. 三类波形的可分性三个与频率无关的时域特征决定方案边界拿到一段波形数据后第一反应可能是做FFT看频谱。但实际做下来时域差分特征往往更快、更稳也更好调试。这一章先把「为什么能分」讲透后面代码才有底气。2.1 为什么主力用差分特征而不是FFT开销与实时性权衡FFT确实能把三类波形分开正弦波只有基波方波有衰减较慢的奇次谐波三角波谐波衰减更快。但FFT有两个现实问题一是计算量1024点复数FFT在F1系列上大约要几毫秒如果还要加窗、取模、找谱峰实时性很紧张二是窗口截断问题采样窗口如果不是整数倍周期频谱泄漏会直接把谐波比例算错噪声一大就翻车。所以我的惯用方案是「时域差分特征为主、FFT为辅」。差分特征只需要减法、乘加和开方全程几十条指令主频72MHz的芯片也能在微秒级跑完。它不依赖绝对幅值也不依赖精确频率只关心波形在采样点之间的变化形态天然抗直流偏置。FFT只用在边界模糊时做二次确认放到最后一章讲。时域差分特征的选取也不是拍脑袋。理想的判别特征要满足两个条件对幅值不敏感、对频率不敏感。幅值不敏感靠归一化解决频率不敏感靠特征本身的量纲设计解决。下面这两个核心特征都是在差分序列上构造的天然满足这两点。2.2 核心特征R差分峰值与差分RMS之比三角波约1、正弦波约1.414定义一个长度为N的单周期采样序列先减掉直流分量得到 y[i]再算一阶差分d[i] y[i1] - y[i]i 0, 1, ..., N-2然后统计两个量差分绝对值的峰值 diff_peak以及差分序列的均方根值 diff_rms。两者之比记为RR diff_peak / diff_rms这个R值在理想波形下有非常清晰的指向。正弦波每个采样点之间的差分近似等于原信号的余弦形式diff_rms等于diff_peak除以√2所以R约等于1.414。对称三角波不一样它的斜率恒定不变差分序列绝对值几乎处处相等只在转折点附近跳变因此diff_rms约等于diff_peakR约等于1.0。方波则完全不同——绝大部分采样点落在平坦区差分接近零只有跳变沿附近几个点产生很大的差分值于是diff_rms被大量零点拉低R值往往能到5以上甚至随每周期采样点数增加而更大。这三个取值区间拉得很开构成了分类的主干判据。更关键的是这个比值不受幅值影响波形放大两倍diff_peak和diff_rms一起放大两倍R不变。它也不随频率恶化正弦波差分近似正比于频率但峰值和RMS同步增长比值保持稳定。所以R是一个天然与幅值、频率解耦的特征量这是它作为主判据的根基。需要指出的是R的推导隐含一个前提每个周期至少有几十个采样点差分才能近似反映真实斜率。如果采样率过低每周期只有十几个点差分值不再平滑R会偏离理论值甚至出现误判。工程上这条下限我一般卡在每周期50个点低于这个值就提高采样率或者判为未知类型。2.3 辅助特征active_ratio与跳变幅度把方波单独拎出来只靠R值区分正弦波和三角波还算勉强可行但要同时处理方波就得再加一个维度。方波的差分序列是「稀疏尖峰」形态而正弦波和三角波的差分序列处处有值这个结构性差异用active_ratio就能量化active_ratio 差分绝对值大于某相对阈值的采样点个数 / 总差分点数阈值我一般取 diff_peak 的5%也就是只有差分值足够显著的采样点才被记为「活跃」。方波在一个周期内只有上升沿和下降沿两个区域是活跃的假设每周期200个点active_ratio大约只有2%三角波几乎所有点都有恒定差分active_ratio接近100%正弦波在过零点附近差分大、峰值附近差分小但超过5%阈值的点仍然占八成以上。再加一个跳变幅度条件方波的识别就更稳了。方波跳变沿一个采样间隔的电压变化量接近整个峰峰值Vpp而正弦波和三角波在同样采样率下相邻点变化量通常只有Vpp的百分之几。所以可以用「diff_peak 0.3 × Vpp」作为方波的硬性门槛。两个条件合在一起方波基本不可能漏判。到这里三类波形的可分性已经立住了。只要采样参数正常三个特征量在图上各自有清晰的分布区间。提示上面说的1.0和1.414都是理论值实际ADC量化噪声会把R值往下拉一点因为噪声给diff_rms加了不少分量却不一定能抬升diff_peak。阈值边界一定要留余量。3. 先配采样再谈识别定时器触发ADC与DMA搬运的关键参数代码写得再漂亮采样参数不合理一样识别不出来。这一章聊的是整个方案的底座也是新手最容易忽略的部分。很多「波形识别不准」的报错最后查出来不是算法问题而是ADC配置有问题。3.1 采样率怎么定每周期100~200个采样点起步兼顾频率自适应采样率决定了差分特征的可信度。点太少三角波和正弦波的R值都往一起靠点太多缓冲区长度吃紧、中断频率太高。工程上我一般按信号频率的100~200倍来设采样率。比如信号是1kHz采样率设在100kHz到200kHz之间一个周期刚好100到200个点差分特征既平滑又有区分度。定时器触发ADC是标准做法。用STM32的通用定时器产生更新事件触发ADC转换每个转换完成由DMA搬到内存。定时器分频和重载值的关系很简单采样率 定时器计数时钟 / (预分频值1) / (重载值1)。假设定时器输入时钟84MHz想要100kHz采样率可以先预分频到1MHz计数时钟再设重载值为9即1MHz/10100kHz。// 假设定时器时钟 84MHz目标采样率 100kHz htim2.Init.Prescaler 84 - 1; // 84MHz / 84 1MHz 计数时钟 htim2.Init.Period 10 - 1; // 1MHz / 10 100kHz 采样率 htim2.Init.CounterMode TIM_COUNTERMODE_UP; HAL_TIM_Base_Init(htim2); HAL_TIM_Base_Start(htim2);预分频值决定计数时钟的粗细重载值决定采样周期。改采样率时优先动预分频让计数时钟保持在1MHz到10MHz之间采样间隔抖动更小。如果信号频率未知我一般这样处理先固定一个较高采样率采一段数据靠过零检测算出实测频率再反推需要的预分频和重载值最后重新初始化定时器。这样无论信号源输出200Hz还是5kHz都能保证每周期采样点数落在合理区间。3.2 交流信号进ADC前的电平抬升与阻抗匹配STM32的ADC输入范围是0到参考电压通常3.3V而标准波形发生器输出的往往是双极性信号比如±1V的正弦波。直接接进ADC引脚负半周会被钳位到地波形削底后面所有特征全部失真。很多第一次做波形识别的工程师在这踩坑以为算法有问题实际是前端电路不对。常见做法是用运放做电平抬升把信号整体搬到1.65V中心附近。一个同相放大加偏置电路就能搞定信号经过RC低通滤波后送入运放同相端反相端通过电阻分压接参考电压把增益和偏置调到目标范围。运放选型也有一点讲究需要轨到轨输入输出、低失调的型号否则信号接近0V或3.3V时会被削波。阻抗匹配同样不能忽视。ADC采样电容充电需要时间如果信号源输出阻抗太高采样保持时间不够采出来的值会偏小看起来就像波形被「压扁」了。我一般确保ADC引脚前面的等效阻抗低于10kΩ软件里设置采样时间为12个ADC时钟周期以上有条件的话在ADC引脚并一个100nF电容效果非常明显。3.3 DMA双缓冲和优先级采集连续性比代码调优更重要波形分类需要拿到一段完整连续的采样序列中途不能丢失点。如果不用DMA而在ADC中断里一个一个读数据那么中断服务程序的执行时间会限制采样率上限而且CPU稍微忙一下就会丢转换结果。所以DMA搬运是必选项。我常用的配置是ADC工作在DMA循环模式缓冲区开2048个uint16_t使能DMA半传输和全传输中断。每次中断进来处理一半数据另外一半仍在被DMA填充这样CPU处理时间被藏住了采样不会断档。缓冲区分成两半天然就是双缓冲结构不需要自己动手切换。#define ADC_BUF_LEN 2048 static uint16_t adc_buf[ADC_BUF_LEN]; static volatile uint8_t half_ready 0; void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) half_ready 1; } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) half_ready 2; }half_ready等于1表示前一半缓冲区满等于2表示后一半满。主循环里轮询这个标志拿到数据后先做波形分类再做显示或动作。处理函数必须保证在下一半数据到达之前返回这个时间预算由采样率决定100kHz采样率下一半缓冲区1024个点大约10ms时间非常充裕只要分类函数里不做繁琐的浮点打印就没有压力。DMA优先级也要提一下。如果系统里还有其他外设DMA请求ADC的DMA流优先级应该设成最高或次高否则偶尔一次总线抢占就可能丢掉一个采样点。丢点对波形分类的影响比想象中大每次丢点都会在差分序列里产生一个虚假尖峰把diff_peak抬高R值直接失真。4. 可复现的分类核心代码周期截取、差分统计与三档判决采样配置搞定后终于到核心代码。这一章给出一套能直接抄进工程的结构先是数据结构和主流程然后是分类函数本体最后把三个阈值参数掰开讲清楚。4.1 数据结构、主流程与HAL回调为了方便说明把波形类型定义成枚举特征量封装成一个结构体。实际工程里可以直接挂在现有任务上不需要单独建工程。枚举里除了三类标准波形还留了一个UNKNOWN这是刻意的后面会解释它的价值。typedef enum { WAVE_UNKNOWN 0, WAVE_SINE, // 正弦波 WAVE_TRIANGLE, // 三角波 WAVE_SQUARE // 方波/矩形波 } WaveType; typedef struct { float freq_hz; // 实测频率 float vpp; // 峰峰值单位V float vmean; // 直流分量单位V float ratio_r; // 差分峰值 / 差分RMS float active_ratio; // 活跃差分占比 float confidence; // 分类置信度 0~1 } WaveFeature;主流程放在主循环里轮询DMA标志。拿到半缓冲区数据后先转成电压值12位ADC对应0~3.3V再调用分类函数。分类函数的输入是实际的ADC原始值数组内部自己做转电压、去直流、过零检测、周期截取和特征计算。这样上层调用方不需要关心处理细节。WaveType type WAVE_UNKNOWN; WaveFeature feat; while (1) { if (half_ready ! 0) { uint8_t half half_ready; half_ready 0; uint16_t *buf (half 1) ? adc_buf : adc_buf ADC_BUF_LEN / 2; // 半缓冲区长度单位采样点数 type classify_waveform(buf, ADC_BUF_LEN / 2, 3.3f, 4095, 100000.0f, feat); // 根据type和feat做后续显示或控制 } }classify_waveform函数的几个参数分别是ADC原始数据指针、数据长度、参考电压、ADC满量程值、实际采样率。参考电压和满量程用于把原始值换算成电压采样率用于过零检测后计算频率。这些参数全部由调用方传入函数内部不做任何全局依赖方便单元测试。4.2 差分特征计算与波形判定函数分类函数分四个阶段去直流与归一化、过零检测与周期截取、差分统计、阈值判定。前三步都是为第四步准备特征量。完整实现如下建议直接对照注释看。WaveType classify_waveform(const uint16_t *raw, uint32_t len, float vref, uint32_t full_scale, float sample_rate, WaveFeature *feat) { if (len 64 || raw NULL || feat NULL) return WAVE_UNKNOWN; float *y (float *)malloc(len * sizeof(float)); if (y NULL) return WAVE_UNKNOWN; /* 第1步转电压找峰峰值 */ float vmin 1e9f, vmax -1e9f; for (uint32_t i 0; i len; i) { y[i] (float)raw[i] * vref / (float)full_scale; if (y[i] vmin) vmin y[i]; if (y[i] vmax) vmax y[i]; } float vpp vmax - vmin; feat-vpp vpp; feat-vmean 0.5f * (vmin vmax); /* 幅度太小直接判未知 */ if (vpp 0.05f * vref) { free(y); return WAVE_UNKNOWN; } /* 第2步去直流即减中心电平 */ for (uint32_t i 0; i len; i) y[i] - feat-vmean; /* 第3步过零检测找相邻同向过零点得到一个完整周期 */ int32_t zc1 -1, zc2 -1; for (uint32_t i 1; i len; i) { if (y[i-1] 0.0f y[i] 0.0f) { if (zc1 0) zc1 (int32_t)i; else { zc2 (int32_t)i; break; } } } if (zc1 0 || zc2 0) { free(y); return WAVE_UNKNOWN; } /* 第4步在单个完整周期内计算差分特征 */ uint32_t n (uint32_t)(zc2 - zc1); if (n 50) { /* 每周期点数太少特征不可信 */ free(y); return WAVE_UNKNOWN; } float *seg y zc1; /* 截取从过零点开始的一个周期 */ float diff_peak 0.0f; float diff_rms 0.0f; for (uint32_t i 0; i n - 1; i) { float d seg[i1] - seg[i]; float a fabsf(d); if (a diff_peak) diff_peak a; diff_rms a * a; } diff_rms sqrtf(diff_rms / (float)(n - 1)); /* 活跃差分占比阈值取差分峰值的5% */ uint32_t active_cnt 0; for (uint32_t i 0; i n - 1; i) { if (fabsf(seg[i1] - seg[i]) 0.05f * diff_peak) active_cnt; } float active_ratio (float)active_cnt / (float)(n - 1); float r diff_peak / (diff_rms 1e-9f); /* 第5步阈值判定 */ WaveType type WAVE_UNKNOWN; float confidence 0.0f; if (active_ratio 0.3f diff_peak 0.3f * vpp) { type WAVE_SQUARE; confidence 1.0f - active_ratio / 0.3f; } else if (r 1.18f) { type WAVE_TRIANGLE; confidence (1.18f - r) / 0.18f; } else if (r 1.32f) { type WAVE_SINE; confidence (r - 1.32f) / 0.12f; } feat-freq_hz sample_rate / (float)n; feat-ratio_r r; feat-active_ratio active_ratio; if (confidence 0.0f) confidence 0.0f; if (confidence 1.0f) confidence 1.0f; feat-confidence confidence; free(y); return type; }代码里几个细节说一下。过零检测只找上升沿过零点找到两个就截出一个完整周期这样信号起始相位不会影响结果FFT方案里最头疼的整周期截断问题在这里被天然避开。每周期点数少于50就直接判WAVE_UNKNOWN这是对差分特征有效性的硬保障。diff_rms加1e-9f是为了防止全零数组导致除零让分类函数更健壮。置信度的计算方式是「特征离阈值边界越远越可信」。比如三角波区间是r 1.18当r1.0时置信度约等于1.0r1.15时置信度只有0.17接近边界时输出结果本身就不可靠。这个设计在后面避坑章节会继续讲。4.3 三个判决阈值的物理意义与调参顺序代码里一共有三个阈值在起作用方波判据里的active_ratio0.3和diff_peak0.3×vpp以及三角波与正弦波分界的r值阈值。理解它们的物理含义比背参数更有用。active_ratio0.3的含义是「一个周期内差分显著的采样点不足三成」。纯方波只有跳变沿附近才产生大差分这个值通常在0.02左右如果方波沿不够陡跳变占了好几个采样点active_ratio会上升但只要不超过0.3仍然能正确识别。两个方波条件必须同时满足防止窄脉冲等病态波形混入。r1.18判三角波、r1.32判正弦波这组阈值是理论值的加减余量。对称三角波R约1.0标准正弦波R约1.414取中间地带作为过渡区落在1.18到1.32之间的判为UNKNOWN而不是硬猜一个结果。实际信号源存在谐波失真和噪声非对称三角波的R值可能偏高削顶正弦波的R值可能偏低过渡区就是给这些边界场景预留的缓冲。调参顺序我建议从采样率开始。先确认每周期至少100个点用示波器或者串口打印原始波形确认没有削底、没有饱和。然后打印r和active_ratio的实际分布你就能看到三类波形在特征空间里的位置阈值完全可以按实测分布微调。最后再动置信度参数因为它只影响输出形式不影响分类正确率。5. 波形识别避坑从误判到系统级故障的4条踩坑记录这章整理的是我在实际调试中经常遇到的四类问题每一条都按「现象、原因、解决」的顺序写。很多问题单看代码完全发现不了必须结合硬件和信号特征一起排查。5.1 方波被识别成三角波采样率太低导致沿被拉斜现象信号发生器输出1kHz方波算法却返回三角波。串口打印active_ratio发现高达0.6远超0.3的方波阈值。原因采样率只有2kHz每个周期只采到2个点。方波的上升沿在采样间隔内被「平均」成了一段斜坡差分序列看起来就像三角波的固定斜率三个特征全部失真。这是采样定理层面的问题再聪明的算法也救不回来。解决提高采样率让每周期点数至少达到50个。我当时把采样率从2kHz改到100kHz方波的active_ratio直接掉到0.02识别立刻恢复。排查时先打印每周期点数这个值小于50优先怀疑采样配置不要先去调分类阈值。5.2 正弦波与三角波在R值上打架非对称波形让理论边界失效现象信号源的三角波输出被设为上升慢、下降快的不对称模式。算法反复在SINE和TRIANGLE之间跳变置信度都很低。原因非对称三角波的差分序列不再是两段等幅常数上升段斜率小、下降段斜率大。推导一下就能发现它的R值不再是1.0而是趋向1.25甚至更高逼近正弦波的阈值边界。解决方案分两步。第一步如果业务允许要求信号源输出标准对称三角波第二步如果必须支持非对称波形就把1.18和1.32两个阈值向外扩同时引入过零点附近的斜率方差做辅助判断——正弦波过零处斜率连续变化三角波过零处斜率是折线突变这个差异在时域上可以量化。5.3 信号幅度太小被误判为噪声先看偏置与有效位现象输入信号峰峰值只有200mV分类结果几乎随机有时UNKNOWN有时SINEconfidence始终不高。原因STM32的12位ADC在3.3V参考下1个LSB约0.8mV200mV信号有效位数只有8位左右。更糟的是如果偏置电路没有把信号抬到1.65V中心而是贴在0.5V附近负半周已经削底波形根本不再是对称的。解决先把信号源输出调大比如2Vpp确认分类稳定后再压缩幅度。如果应用要求必须识别小信号前端加一级放大或者改用更高分辨率的ADC不要指望算法去补偿硬件动态范围。检查偏置也很简单打印vmean如果偏离1.65V太多优先查运放偏置电路。5.4 分类结果随机抖动采样窗口没有对齐整数周期现象同一波形重复识别结果大部分时间正确偶尔跳成UNKNOWN间隔没有明显规律。原因DMA半缓冲区是固定长度2048个点不一定刚好覆盖整数个信号周期尤其信号频率不是采样率的约数时缓冲区末尾会残留半截波形。如果分类算法直接在整个缓冲区上算特征这半截波形的差分尖峰就会污染diff_peak。解决代码里的过零检测正是为了处理这个问题——截取相邻两个同向过零点之间的完整周期再算特征残片被排除在计算范围外。如果你的实现没有这一步务必加上如果加上了还在抖检查过零检测是否被噪声干扰可以给y数组先做一次三点滑动平均再找过零点。6. 进阶FFT谐波比做二次判决把边界区波形也兜住时域差分方案在绝大多数场景够用但总有一些边界波形让人头疼失真略大的三角波、带振铃的方波、削顶的正弦波。它们的主要特征会落在UNKNOWN区间此时再用FFT做一次谐波分析能给出非常明确的补救判据。常见做法是先把时域判决落在边界区的波形重采样到1024点加汉宁窗再调用芯片厂商官方DSP库里的实数FFT函数。arm_rfft_fast_instance_f32 fft_inst; arm_rfft_fast_init_f32(fft_inst, 1024); // windowed_buf为加窗后的波形fft_out长度为1024实数虚数交替排列 arm_rfft_fast_f32(fft_inst, windowed_buf, fft_out, 0); float base_amp sqrtf(fft_out[2]*fft_out[2] fft_out[3]*fft_out[3]) * 2.0f / 1024.0f; float third_amp sqrtf(fft_out[6]*fft_out[6] fft_out[7]*fft_out[7]) * 2.0f / 1024.0f; float ratio3 third_amp / (base_amp 1e-9f);谐波比判据的理论值很干净标准方波的三次谐波幅值约为基波的1/3三角波约为1/9正弦波几乎没有三次谐波。实际算出来方波ratio3在0.3左右、三角波在0.1左右、正弦波接近0.01三个区间完全分得开。而且即使波形带了一点噪声谐波比依然稳定因为噪声能量是分散在全频段的不会集中堆在三次谐波这个单频点上。不过FFT方案有个绕不开的前提窗口内必须是整数个周期否则频谱泄漏会严重污染谐波幅值。所以我会先用过零检测确定周期再精确截取整数周期数据而不是直接拿缓冲区硬算。加汉宁窗能进一步压制泄漏代价是基波和三次谐波的幅值都需要乘以约2倍的窗系数修正具体补偿系数取决于窗函数类型建议用已知波形标定一次。我自己现在的新项目都是「时域差分R谐波比」双通道时域结果置信度高就直接用落在UNKNOWN区间再调FFT做二次确认。这个组合把误判率压到了很低的水平代价只是分类耗时加了一两毫秒在显示和控制类应用里完全可以接受。波形识别的坑磨多了会发现真正的工程能力不是把算法调得多花哨而是知道每个判据什么时候不可信、以及用什么备选方案去接住它。希望这篇能帮你少走几段弯路。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?