简介:这份资源面向嵌入式开发学习者与信号处理方向的工程师,聚焦如何借助STM32内置ADC与定时器完成周期性信号的频率检测与相位跟踪,适合作为实时控制与信号采集场景下的实践参考。压缩包共316个文件,约512KB,以80个h头文件与74个c源文件构成核心代码,辅以42个o目标文件、42个d依赖文件及40个crf等编译中间产物,另含hex、map、axf等烧录与映射文件以及uvproj、uvopt工程配置,便于直接导入Keil环境复现实验。已有1392人学习下载,说明其在同类项目中具备一定参考价值。资源内含定时器捕获配置、ADC采样与中断处理、相位差计算及动态参数调整等实现思路,读者可据此理解频率公式推导、多通道相位捕获与DMA连续采样的组织方式,并对照原理图与配置文档完成从代码到硬件的验证,对初学者是完整的练手项目,对专业人士也可作为方案比对与排错参考。
1. 频率测量与相位跟踪:一个 STM32 工程包能省掉多少试错
做电力参数采集、电机控制或者音频锁相的同行,大概都经历过这样的阶段:示波器上波形明明很干净,可自己写的频率计算一上电就跳字,相位差更是飘得没法看。问题往往不在算法本身,而在采样时序、定时器配置和滤波窗口这三者的配合上。这个名为「基于 STM32 频率测量与相位跟踪」的 7z 压缩包,就是围绕这条链路打包的一套工程源码,核心解决两件事:一是把输入信号的频率测准,二是在频率变化时让参考相位跟得住。它适合已经能点亮 STM32、写过定时器和 ADC 的嵌入式开发者,也适合正在做课程设计或样机验证、需要一份可跑通的频率检测与相位追踪骨架的人。压缩包内是完整的工程目录,不是零散片段,拿到后能直接对照自己的硬件改引脚和时钟。
2. 拆开工程看结构:频率检测与相位追踪的代码骨架
2.1 工程目录与模块划分
拿到压缩包先别急着编译,解压后按目录过一遍,能少走很多弯路。这类工程通常按「底层驱动 + 算法模块 + 应用层」三层组织,常见结构如下表。不同版本命名会有差异,但职责划分基本一致,对照着找即可。
| 目录/文件 | 职责 | 上手时要确认的点 |
|---|---|---|
| Core / Src / Inc | 主循环、外设初始化、中断入口 | 系统时钟频率是否与晶振匹配 |
| Drivers | 标准外设库或 HAL 库 | 库版本与芯片系列是否对应 |
| Algorithm / App | 频率计算、相位跟踪、滤波 | 采样率常量是否与定时器一致 |
| 工程文件(.uvprojx 等) | 编译配置、下载配置 | 器件型号、调试器类型 |
先确认工程文件里的芯片型号和你手上的板子一致,再确认系统时钟配置。这两步不做,后面所有测量结果都没有参考价值。我一般会先把主频、定时器预分频、ADC 采样周期这三个数抄在纸上,因为频率测量的精度几乎完全由它们决定。
2.2 频率测量的两条实现路线
工程里频率检测通常走两条路,选哪条取决于信号频率范围和精度要求。第一条是「定时器输入捕获法」:把信号接到定时器的捕获通道,记录相邻两次上升沿之间的计数值,用计数差反推周期。优点是硬件直接测,CPU 占用低,适合中高频方波或经过整形的信号。第二条是「ADC 采样 + 过零检测/FFT」:连续采样后找过零点或用频谱峰值定位频率,适合正弦波、需要同时拿到幅值和相位的场景。
输入捕获的关键代码逻辑大致是这样,捕获中断里保存两次计数值并做溢出处理:
/* 输入捕获中断:记录上升沿时刻并计算周期 */ volatile uint32_t cap_last = 0; volatile uint32_t cap_period = 0; volatile uint8_t cap_overflow = 0; void TIM_Capture_IRQHandler(void) { if (捕获中断标志置位) { uint32_t now = 读取当前捕获寄存器值; /* 两次捕获之间定时器可能溢出,需累加溢出次数 */ uint32_t delta = (now + cap_overflow * 65536u) - cap_last; cap_last = now; cap_overflow = 0; if (delta > 最小有效周期) { /* 滤掉毛刺 */ cap_period = delta; } 清除中断标志; } if (溢出中断标志置位) { cap_overflow++; /* 记录溢出次数,供下次计算补偿 */ 清除溢出标志; } }这段代码里有两个参数必须和硬件对上:一是定时器的计数位宽(16 位还是 32 位),决定溢出补偿的基数;二是「最小有效周期」阈值,设小了毛刺会混进来,设大了低频信号会被误判为无效。实际频率等于定时器时钟除以预分频再除以cap_period,所以预分频值一变,换算公式里的分母也要跟着改,这是新手最容易漏的一处。
2.3 相位跟踪的闭环思路
相位追踪不是单独算一个相位差就完事,它需要一个参考相位和一个反馈量构成闭环。工程里常见的做法是:用测得的频率驱动一个本地参考信号(软件生成或 PWM 输出),把输入信号与参考信号的相位差作为误差,经过一个比例积分环节去微调参考信号的相位步进,使误差收敛到零附近。这样即使输入频率缓慢漂移,参考相位也能跟住,而不是每来一个周期就重新对齐一次。
实现时要注意相位差的归一化。两个信号过零点的时间差除以当前周期,得到的是 0 到 1 之间的相位比例,再乘以 360 才是角度。如果直接用时间差做误差输入,频率一变,同样的时间差对应的角度就变了,闭环增益会跟着漂,表现为跟踪时快时慢。我一般会把误差统一换算成角度或弧度再进 PI,参数调起来才有可比性。PI 的两个系数初始值可以给得很小,先看误差是否单调收敛,再逐步加大比例项,积分项留到最后压稳态误差。
3. 从采样到出数:把频率和相位跑通的实操步骤
3.1 采样率与定时器的参数对齐
频率测量和相位跟踪共用同一套采样时基,所以第一步是把定时器触发 ADC 的节奏定死。假设目标信号在 45 到 65 赫兹之间,按每周期采样 128 点算,采样率大约在 6 千赫兹上下,留一点余量取 8 千赫兹比较稳妥。定时器时钟若为 72 兆赫兹,预分频取 9、自动重装值取 999,触发频率就是 72M / 9 / 1000 = 8 千赫兹。这三个数一旦确定,后面所有换算都以它为准。
/* 定时器触发 ADC 采样,参数需与采样率换算一致 */ void Sample_Timer_Init(void) { 使能定时器时钟; 时基结构体.预分频 = 9 - 1; /* 72MHz / 9 = 8MHz 计数时钟 */ 时基结构体.自动重装 = 1000 - 1; /* 8MHz / 1000 = 8kHz 触发频率 */ 时基结构体.计数模式 = 向上计数; 初始化定时器; 配置主模式输出触发信号给 ADC; 启动定时器; }预分频和自动重装都写了减一,这是寄存器实际写入值比理论分频数小一的缘故,漏掉这个减一会让实际采样率偏离设计值,频率计算结果整体偏一个固定比例,而且这个偏差很隐蔽,因为波形看起来仍然正常。采样率定好后,把每周期点数、采样率、定时器时钟这三个常量集中定义在一个头文件里,算法模块只引用常量,不写死数字,换硬件时改一处即可。
3.2 过零检测与频率解算
采样数据进来后,先做过零检测拿到周期。正弦信号过零点的判断不能只看符号翻转,因为噪声会让信号在零点附近反复穿越。常见做法是加一个滞回区间:信号从低于负阈值上升到高于正阈值才算一次有效上升沿,阈值取信号幅值的百分之五到百分之十。这样毛刺被挡在外面,代价是过零点位置有轻微偏移,对频率影响很小,对相位则需要在两个通道上用同样的阈值以保持一致性。
/* 滞回过零检测:返回相邻有效上升沿之间的采样点数 */ #define HYST_RATIO 0.08f /* 滞回阈值占幅值比例 */ static float amp_est = 0.0f; /* 幅值估计,可由滑动峰值更新 */ int ZeroCross_Period(const float *buf, int len) { float hyst = amp_est * HYST_RATIO; int last = -1, period = 0; for (int i = 1; i < len; i++) { /* 由负到正且越过正阈值,记为一次有效上升沿 */ if (buf[i-1] < -hyst && buf[i] >= hyst) { if (last >= 0) period = i - last; last = i; } } return period; /* 单位:采样点数 */ }拿到周期点数后,频率等于采样率除以周期点数。这里有个精度细节:整数点数会带来量化误差,周期越长相对误差越小,所以低频信号反而更容易测准,高频信号要么提高采样率,要么用插值把过零点位置细化到小数。工程里如果只做几十赫兹的工频类信号,整数点数足够;若信号频率上百赫兹,建议在过零点两侧做线性插值,把周期算成浮点。
3.3 相位差的归一化与跟踪输出
相位差的计算依赖两路信号的过零点时刻。单路信号做频率检测时只需要周期,做相位跟踪时则需要同时记录输入通道和参考通道的过零时刻,两者之差除以周期得到相位比例。参考通道如果是软件生成的,它的过零时刻可以直接由相位累加器给出,不必再采样,这样能省一路 ADC 也避免引入额外噪声。
/* 相位误差归一化后送入 PI,输出参考相位步进修正量 */ float phase_err_norm(int in_cross, int ref_cross, int period) { int diff = in_cross - ref_cross; /* 把差值折到半个周期内,避免 360 度跳变 */ if (diff > period / 2) diff -= period; if (diff < -period / 2) diff += period; return (float)diff / (float)period; /* 结果范围约 -0.5 ~ 0.5 */ } float pi_update(float err, float *integ) { const float kp = 0.02f, ki = 0.0005f; /* 初始值宜小,逐步加 */ *integ += ki * err; if (*integ > 0.1f) *integ = 0.1f; /* 积分限幅,防饱和 */ if (*integ < -0.1f) *integ = -0.1f; return kp * err + *integ; }归一化那一步的折半处理是关键,不做的话相位差在正负半个周期交界处会从接近 1 跳到接近负 1,PI 输出跟着剧烈抖动,现象是参考信号每隔一段时间猛甩一下。积分限幅同样不能省,输入信号短暂丢失时误差会持续累积,没有限幅的话恢复后要很久才能拉回来。比例和积分系数没有通用值,和采样率、信号信噪比都有关,先给小的看收敛趋势,再往上加。
4. 避坑与排查:频率跳字、相位漂移的五个真实来源
4.1 频率读数周期性跳变
现象是频率显示每隔几秒跳一次,幅度不大但很规律。原因多半是定时器溢出补偿没做对,捕获中断里只记了当前计数值,没累加溢出次数,导致跨溢出边界的那个周期被算成一个极大或极小的值。解决方法是像 2.2 节那样维护一个溢出计数,在计算周期差时把溢出次数乘上计数位宽加进去,并在每次有效捕获后清零。排查时可以临时把溢出计数打印出来,看它是否在跳变时刻恰好加一。
4.2 相位差缓慢单向漂移
现象是相位误差不收敛,一直朝一个方向慢慢偏。原因通常是参考信号的频率步进和实际采样时基不同源,一个是软件累加器按标称频率走,一个是硬件定时器实际触发,两者存在微小频差,闭环只能压住瞬时误差,压不住累积漂移。解决办法是让参考相位累加器也由同一个采样中断驱动,用实测频率而不是标称频率去更新步进,保证两者同源。这个坑很隐蔽,因为短时间内看不出来,跑几分钟才显形。
4.3 低幅值信号下过零检测失效
现象是小信号输入时频率直接不出数或者乱跳。原因是滞回阈值按固定幅值设定,信号变小后阈值相对过高,有效过零点被漏掉。解决方法是让阈值跟随幅值自适应,用滑动窗口内的峰值或有效值估计幅值,再按比例算阈值,同时给阈值设一个下限防止噪声被当成信号。工程里如果信号幅值范围固定,也可以标定一次后写死,但换传感器就得重标。
4.4 中断里做浮点运算导致采样丢点
现象是频率偶尔算出一个明显偏大的值,对应周期偏短。原因是在捕获或采样中断里做了除法、开方这类耗时运算,中断服务时间超过采样间隔,下一次触发被错过。解决方法是中断里只做数据搬运和计数,把浮点解算挪到主循环或低优先级任务里,用标志位通知。判断依据可以看中断进出时间,或者统计采样缓冲区里是否有重复或缺失的序号。
4.5 编译优化等级改变后结果变化
现象是同一份代码在调试配置下正常,切到发布配置后频率或相位结果变了。原因是某些中间变量没加volatile,被编译器优化掉或重排,尤其在中断和主循环共享的变量上。解决办法是把跨上下文访问的计数、标志、缓冲区索引都声明为volatile,并检查优化等级切换后是否有变量被寄存器缓存。这类问题没有固定报错,只能靠代码审查和对比测试发现。
5. 进阶验证:用已知信号反推测量链路是否可信
工程跑起来不等于测准了,得有一套验证手段。我习惯用信号发生器给一个已知频率和已知相位差的双通道信号,从低频到高频扫几个点,把测量值和设定值列成表对比。下面这张表是验证时该记录的最小字段集,误差列按相对误差算,相位按角度差算。
| 设定频率 | 设定相位差 | 测得频率 | 频率相对误差 | 测得相位差 | 相位绝对误差 |
|---|---|---|---|---|---|
| 50 Hz | 0 度 | 待填 | 待填 | 待填 | 待填 |
| 50 Hz | 90 度 | 待填 | 待填 | 待填 | 待填 |
| 55 Hz | 45 度 | 待填 | 待填 | 待填 | 待填 |
| 60 Hz | 180 度 | 待填 | 待填 | 待填 | 待填 |
扫频时重点看两个边界:一是频率接近采样率整数分之一的那些点,容易和采样混叠;二是相位差接近正负 180 度的点,归一化折半处理如果写错,这里误差会突然放大。如果条件允许,再用一个独立的高精度仪表做交叉比对,比只看自己的显示值可靠得多。
另一个容易被忽略的验证是动态响应。让信号频率按斜坡缓慢变化,观察跟踪输出是否平滑跟随,有没有滞后或过冲。静态准不代表动态跟得住,PI 参数在静态下看着收敛,斜坡输入时可能明显滞后。我一般会把频率变化速率控制在每秒一两赫兹,看相位误差的峰值是否在可接受范围内,超了就回头调比例项。
还有个习惯值得养成:每次改完采样率或定时器参数,先把原始采样缓冲区导出来看波形,确认没有丢点、没有削顶、没有直流偏置漂移,再去信频率和相位的数字。数字不对的时候,九成问题能在原始波形里找到线索,直接盯着结果调参数往往是白费功夫。从那以后我每次换硬件平台,都强制先把采样链路单独验证一遍再上算法,这个顺序省下的时间比调参本身多得多。希望帮到你。
本文还有配套的精品资源,点击获取