ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

8位单片机频率计的测频/测周/等精度方案与整形避坑

8位单片机频率计的测频/测周/等精度方案与整形避坑

简介:面向电子技术入门者与嵌入式开发学习者,这套频率计-8项目以单片机为核心实现频率计数器,覆盖从信号输入、脉冲计数到定时中断读取频率的完整流程,也涉及输入信号检测与显示更新等环节。压缩包共9个文件、约20KB,包括C源码、可直接烧录的hex固件,以及obj、lst、m51、omf等编译过程文件,便于对照代码理解程序结构,也可用于排查构建细节。资源包中的C程序体现了计数器与定时器的配合关系,通过中断服务完成频率读取、计数清零与结果更新,对学习定时器初值计算和中断编程很有帮助。已有218人学习下载,适合课程设计、电子实训或单片机外设应用自学,也可作为单片机测频项目的快速参考。借助源码和工程文件,能够掌握从初始化到主循环的整体框架,并弄清频率计实现中的关键细节。

1. 频率计数器不必先买台式仪器:8位单片机方案的适用边界先弄清楚

做频率计数器(频率计)这件事,很多人的第一反应是买一台台式频率计,可真正落到单片机小车测速、电机转速检测、信号发生器输出校验这类场景时,一块几块钱的8位单片机加一个LCD或数码管就够了。关键在于先回答三个问题:要测的频率范围是多少、需要多高的分辨率、信号源是什么波形。回答完这三个问题,测频法、测周法、等精度测量三条路线里自然就选出了一条。网上大多数例程只给“1秒闸门数脉冲”这一种写法,低频几十赫兹直接乱跳,高频几十千赫兹又溢出翻车。这篇文章我会从测量原理、完整代码、前级整形到标定流程一步步讲,让你照着做能拿到稳定读数,遇到问题也不用靠玄学猜。

2. 测频法与测周法怎么选:误差公式先决定量程边界,别急着写代码

2.1 测频法:固定闸门里数脉冲,频率越低误差越难看

测频法是最直观的思路:在固定时间Tg里对输入脉冲计数N,频率就是F = N / Tg。公式看着简单,误差分析才是选型关键。计数器的固有误差是±1个脉冲,所以相对误差约等于1 / (Tg × F)。Tg取1秒时测1kHz信号,N=1000,误差0.1%;测100Hz信号,N=100,误差直接到1%。频率越低,相对误差越大,这就是测频法“吃高频”的原因。

闸门时间也不是越长越好。1秒闸门分辨率是1Hz,10秒闸门分辨率能到0.1Hz,但刷新一次要等10秒,调试时人能等疯掉。我一般做产品默认给1秒闸门,调试模式单独加一个0.1秒快速闸门用于观察信号有没有进来,等确认信号稳定了再切回1秒。这两档位用按键切换,代码里只是改T1中断的累计次数而已。

闸门时间分辨率1kHz时相对误差适用场景
1秒1Hz0.1%常规测量
0.1秒10Hz1%快速判断有无信号
10秒0.1Hz0.01%低频精细测量

2.2 测周法:低频信号的周期长,数时钟个数反而更准

测周法的思路反过来:不再数1秒内来了多少个脉冲,而是测一个脉冲周期占了多少个机器时钟周期。还是±1计数误差,但这时相对误差约等于1 / M,M是周期内的时钟计数个数。

举例更直观:12MHz晶振下STC89C52机器周期为1µs,测50Hz工频信号,单个周期20ms,M=20000,相对误差0.005%,比测频法1秒闸门的0.02%好了一个数量级。而测100kHz信号时,单个周期只有10个时钟周期,M=10,相对误差10%,完全没法看。所以测周法适合低频,频率一高就崩。

工程里常见的分界线是:信号低于500Hz优先用测周法,信号高于1kHz优先用测频法,中间500Hz到1kHz两种方法误差接近,看你的晶振稳定度决定。如果频率范围横跨几十Hz到几百kHz,那就不能只用其中一种,这正是下面等精度测量想解决的问题。

2.3 等精度测量为什么两头都准:两个计数器同起同停才是核心

等精度测量的公式是 fx = Nx / Nref × fref,其中Nx是待测信号计数,Nref是参考时钟计数,fref是参考时钟频率。它不依赖固定的1秒闸门,而是同步启动两个计数器,经过一段时间后同时停止,再取比值。因为两个计数的起止时刻对齐到同一信号边沿,±1误差只会出现在参考计数上,相对误差约等于1 / (Nref)。

这意味着只要参考计数足够大,无论待测信号是10Hz还是1MHz,误差都保持同一量级。8位单片机做等精度,真正的难点在于“同时停止”两个计数器。软件顺序读取TH0和TH1必然存在几个机器周期的间隔,信号频率越高这个间隔引入的误差越明显。所以等精度方案要么加外部硬件闸门锁存,要么用STC8系列自带的硬件捕获通道,纯靠老51软件模拟只能算“准等精度”。

2.4 选型前先回答三个问题,再决定用哪个型号

我每次接到频率计需求,会让对方先回答三个问题。最高测量频率是多少?超过200kHz建议先加74HC4040分频,不然16位计数器轻松溢出。最低测量频率是多少?低于100Hz就别用测频法,直接上测周法。待测信号是什么波形?正弦波、三角波必须先整形,方波还要看电平幅度是否符合TTL标准。

型号选择上,STC89C52适合学习和做固定量程的产品,因为资料多、Keil工程模板遍地都是,Proteus仿真51单片机也很方便。STC8系列适合需要更高精度的场景,1T模式下定时器步进更细,还多了硬件捕获。至于4位OTP单片机,定时器资源少,做简单计数可以,做频率计尤其是等精度方案就很吃力了,不建议在这个方向上省成本。

3. 用测频法在STC89C52上做通第一版频率计:1秒闸门Keil代码与仿真步骤

3.1 引脚分配与寄存器配置思路

我建议第一版用LCD1602显示,比数码管省事得多。数码管要用到锁存器、段码片选和动态扫描,中断里稍有不慎就花屏,LCD1602只需往数据口按时序丢字节。P3.4作为T0计数器外部脉冲输入,这是8051内核固定引脚,没法改。P0接LCD数据口,P2.6和P2.7分别接RS和EN。

寄存器配置的核心是TMOD。T0做16位外部计数器,对应低四位为0x05;T1做16位定时器,对应高四位为0x10。两个拼起来TMOD=0x51。这里有个容易看晕的点:STC89C52和AT89C52的TMOD定义完全一致,网上那些“STC要多配AUXR寄存器”的说法是针对STC12和STC8的,老51内核不需要。

外部脉冲接P3.4后,T0在每个下降沿自动加1,完全靠硬件完成,不占CPU时间。T1则工作在定时器模式,每50ms产生一次中断,累计20次就是1秒。主循环只需要处理LCD显示和按键,测量逻辑全部在中断里,这是频率计代码能保持稳定的基本架构。

3.2 第一版完整代码:定时器配置与1秒锁存

// STC89C52 测频法频率计,晶振12MHz // T0做16位外部计数器,接P3.4;T1做50ms定时,20次累计为1秒 #include <reg52.h> #define LCD_DB P0 sbit LCD_RS = P2^6; sbit LCD_EN = P2^7; unsigned char t1_cnt = 0; unsigned char t0_ovf = 0; // T0溢出次数,扩展量程用 unsigned long freq = 0; void delay_ms(unsigned int ms) { unsigned int i, j; for (i = 0; i < ms; i++) for (j = 0; j < 123; j++); } void lcd_cmd(unsigned char cmd) { LCD_RS = 0; LCD_DB = cmd; LCD_EN = 1; delay_ms(2); LCD_EN = 0; } void lcd_data(unsigned char dat) { LCD_RS = 1; LCD_DB = dat; LCD_EN = 1; delay_ms(2); LCD_EN = 0; } void lcd_init(void) { lcd_cmd(0x38); // 8位数据,2行显示 lcd_cmd(0x0C); // 显示开,光标关 lcd_cmd(0x06); // 写入后光标右移 lcd_cmd(0x01); // 清屏 } void timer_init(void) { TMOD = 0x51; // T1定时器方式1,T0计数器方式1 TH1 = 0x3C; // 12MHz下机器周期1us,50ms初值 TL1 = 0xB0; // 65536 - 50000 = 0x3CB0 TH0 = 0; TL0 = 0; ET0 = 1; ET1 = 1; TR0 = 1; TR1 = 1; EA = 1; } void timer0_isr(void) interrupt 1 { t0_ovf++; // T0溢出一次,代表高16位加65536 } void timer1_isr(void) interrupt 3 { TH1 = 0x3C; TL1 = 0xB0; if (++t1_cnt >= 20) { t1_cnt = 0; ET0 = 0; // 暂时屏蔽T0中断,防止读取期间溢出计数错位 if (TF0) { // 若T0溢出已发生但还没进入中断,这里补一次 TF0 = 0; t0_ovf++; } freq = (unsigned long)t0_ovf * 65536UL + ((unsigned int)TH0 << 8) + TL0; TH0 = 0; TL0 = 0; t0_ovf = 0; ET0 = 1; // 恢复T0中断 } } void main(void) { unsigned char buf[16]; lcd_init(); timer_init(); while (1) { lcd_cmd(0x80); sprintf(buf, "Freq:%ld Hz", freq); // 实际使用中sprintf较慢,建议改为手动拆分数字,这里为可读性简化 } }

这段代码里最关键的是T1中断中的锁存操作。先关ET0再读取TH0/TL0,防止T0溢出中断在读取过程中插入,导致t0_ovf和TH0/TL0的组合错位。if (TF0)这一段补的是“溢出已经发生但还没来得及响应中断”的边界情况,很多例程漏了这一句,频率稍微抬高一点就会偶尔跳变。

频率计算结果中,t0_ovf是高16位,TH0和TL0合起来是低16位,组合成32位数。由于闸门是1秒,这个数字在数值上就等于频率值。12MHz晶振下最大可测频率约65.5kHz,超过这个值t0_ovf开始工作,理论上到不了4G上限,实际受限于T0计数输入的最高翻转频率。

3.3 避免清零竞争:用差值法替代清零操作

第一版代码在第1秒到第2秒的切换瞬间有一个隐患:清零TH0/TL0时如果有外部脉冲进来,这个脉冲就丢了。频率低时丢一个脉冲无所谓,到50kHz以上丢一个就是万分之二的误差,而且这种误差是随机跳变的。

更稳的写法是“只读不清零”。每次1秒到点后,读出一个累计总数,与上一秒的累计总数相减,差值就是这一秒的脉冲数。这样就不存在清零窗口漏脉冲的问题,unsigned long回绕也不会出错,因为差值计算本身对回绕是免疫的。

unsigned long total_now, total_last; void timer1_isr(void) interrupt 3 { TH1 = 0x3C; TL1 = 0xB0; if (++t1_cnt >= 20) { t1_cnt = 0; ET0 = 0; if (TF0) { TF0 = 0; t0_ovf++; } total_now = (unsigned long)t0_ovf * 65536UL + ((unsigned int)TH0 << 8) + TL0; freq = total_now - total_last; total_last = total_now; ET0 = 1; } }

差值法的唯一代价是t0_ovf必须一直累计,不能清零,这意味着你想重新开始测量时要手动复位total_last。实际使用中,main循环里加一个按键清零功能:按一下按键就执行total_last = total_now,读数归零。这个逻辑比第一版更接近实战。

3.4 Proteus仿真验证步骤与常见坑

用Proteus仿真51单片机做验证时,从元件库拖出AT89C52或STC89C52,把晶振频率改成12MHz。Virtual Terminal接P3.4作为信号源,这里要特别注意:必须选DIGITAL类型的方波发生器,如果直接拖一个正弦源进去,Proteus的I/O模型会在阈值附近产生不可预期的逻辑翻转,仿真结果会忽大忽小。

推荐用DCLOCK作为信号源,频率设置成你要测的值。运行后LCD上读到相同数值,说明定时器计数链路正确。如果你手头有示波器,可以顺便看看DCLOCK输出的上升沿是否干脆,这能帮你理解后面为什么实物测量要加整形。

仿真中的另一个坑是STC89C52在Proteus里默认没有启动内部复位延时,直接点运行往往能跑,但换到真实芯片后如果复位电路太弱,会出现下载后第一次上电不工作的现象。所以仿真通过不等于实物能直接交付,这一步要心里有数。

4. 前级波形整形与PCB布局:为什么Proteus能过、实物一接正弦波就乱跳

4.1 I/O计数器看的是边沿,不是电平——正弦波直连必翻车

单片机计数器引脚虽然写着“下降沿计数”,但内部识别的是逻辑电平跳变,而不是数学意义上的“边沿”。正弦波经过零点时变化缓慢,叠加一点噪声就会在阈值附近来回穿越,计数器可能把一次过零算成好几次。实测中50Hz正弦波直连P3.4,读到的频率可能从200Hz跳到几千Hz,完全不能用。

另一个容易被忽略的问题是高电平幅度。51单片机传统TTL电平要求高电平大于2.0V,低电平小于0.8V,但STC系列按CMOS工艺设计,输入高电平建议不低于3.3V。如果你拿3.3V单片机和5V单片机对接,中间没有电平转换,计数器很可能把中间区域的缓慢上升误判成一串脉冲。

4.2 两个实用整形电路:74HC14施密特反相器和LM393比较器

最省事的整形方案是74HC14,一片里有6个施密特触发的反相器,回差电压约0.4V到0.9V。接线很简单:输入信号经过1k电阻接到74HC14的1A脚,1Y输出经过第二个反相器转回同相后接P3.4。输入端的1k电阻主要起限流作用,防止外部信号电压过高损坏芯片。

如果信号幅度特别小,比如只有几百毫伏,74HC14的阈值在1.6V左右根本触发不了,这时要用LM393比较器。反向输入端用两个10k电阻分压得到2.5V参考电压,同相输入端经过10k电阻接被测信号,输出端接一个4.7k上拉电阻到VCC,再从输出端通过510k电阻反馈到同相输入端形成几十毫伏的回差。

方案适用信号回差输出电平注意点
74HC14TTL/CMOS方波0.4-0.9V0到VCC幅度要够
LM393小信号正弦/三角波可调30-100mVOC输出需上拉参考电压要稳
比较器加放大级微伏到毫伏级可调0到VCC前级加运放

用LM393时注意它是开集电极输出,必须接上拉电阻才能输出高电平。有人忘记接上拉,输出只有低电平和悬空,P3.4在悬空时读到的全是噪声,频率计显示几千几万都是正常的。

4.3 PCB布局里的五个反直觉细节

第一个细节:整形电路的地和单片机的地必须是同一个地,但整形输入的地不能直接接到LCD的背光地。LCD背光电流变化在公共地上产生压降,最小也有几十毫伏,对比较器来说足以改变触发点。我一般把模拟整形部分单独拉一条地线到电源端子,数字地和模拟地在电源入口处单点汇合。

第二个细节:P3.4走线不要和晶振引脚并行走线。51单片机核心板原理图上P3.4往往离晶振不远,晶振的振荡信号会通过空间耦合到计数器引脚上,导致频率值小幅漂移。走线时让P3.4尽量短,不在顶层绕弯,必要时在引脚旁加一个10pF到地的滤波电容。

第三个细节:测试夹或杜邦线过长会引入电感,快速沿在长线上会产生振铃。如果测试环境被迫用长线,我一般在一端串一个22Ω电阻,配合引脚对地电容形成一个低通滤波,能有效压制边沿过冲。

第四个细节:被测信号的地和频率计电源地之间可能有压差,直接用示波器探头地线夹住P3.4的地是不对的。这时候要优先用隔离探头或保证共地。频率计测量的是相对自己参考地的电位,共地不稳,读数也就没有意义。

第五个细节:不要把被测信号直接接到MCU引脚,中间一定要经过整形级。MCU引脚本身有施密特输入,但这个施密特是数字逻辑专用,不是给模拟信号准备的,在阈值附近仍然可能抖动。外置整形电路不仅保护MCU引脚,还能通过回差电压实打实吃掉噪声毛刺。

5. 避坑:低频乱跳、高频偏低、下载失败——频率计实测中的5个典型问题

5.1 低频读数乱跳,50Hz显示在48到52之间摆动

现象:用测频法测50Hz工频信号,LCD读数稳定不下来,47Hz、51Hz、49Hz乱跳,完全没有规律。原因有两层:一是测频法1秒闸门在50Hz时N=50,±1脉冲误差就是2%,这是算法层面的硬伤;二是信号过零点噪声叠加,实际计数可能偶尔多计几个脉冲。解决:低频段切换为测周法,中断里记录相邻两个下降沿之间的定时器差值。测周法在50Hz时的误差只有0.005%,比测频法高两个数量级。

// 测周法核心代码:INT0下降沿触发,T0测量周期 void int0_isr(void) interrupt 0 { if (period_flag == 0) { // 第一个下降沿,启动计时 TH0 = 0; TL0 = 0; period_flag = 1; } else { // 第二个下降沿,锁存周期 period_cnt = (TH0 << 8) + TL0; freq = 1000000UL / period_cnt; // 12MHz晶振下1us为1个计数 period_flag = 0; } }

这段代码里period_cnt的单位是机器周期,由于12MHz晶振下机器周期正好是1µs,周期计数值除以1000000再取倒数就是频率。如果换用11.0592MHz晶振,机器周期是1.085µs,就不能直接用1000000作为分子,这是换晶振后最常见的翻车点。

5.2 高频段读数偏低,100kHz测出来只有95kHz

现象:用信号发生器输出100kHz方波,频率计显示在95kHz徘徊,而且数值带有慢漂移。原因是第一版代码“清零窗口漏脉冲”加上T0溢出中断与T1中断竞争。实测中100kHz时1秒闸门内T0会溢出一次,如果溢出发生在T1读取并清零的窗口里,溢出计数值就会丢失,读数偏低。解决:改为3.3节差值法,同时把TF0边沿补计数逻辑写进去。我实测改为差值法后,100kHz测量误差从5%降到0.1%以内。

5.3 手靠近引脚读数猛增,表笔一碰更是跳上几万

现象:还没接信号,LCD上就显示几千Hz,手指靠近P3.4引脚时读数大幅上升。原因是引脚悬空时处于高阻态,人体感应的50Hz干扰和PCB板上的数字噪声都被计数器捕捉。内部上拉虽然存在,但计数器引脚的灵敏度依然很高。解决:用10k电阻把P3.4上拉到VCC,并在引脚对地接一个100pF电容。这个组合形成低通滤波,能滤掉大部分空间耦合的高频干扰。注意上拉电阻不能太小,否则被测信号的低电平可能被抬到阈值以上导致无法计数。

5.4 STC单片机下载失败,一直提示“正在检测目标单片机”

现象:Keil编译通过,STC-ISP点下载后卡在检测界面,重试多次依旧失败。这个问题在STC89C52上很常见,原因往往是冷启动时序没满足。STC系列要求先点击下载,再给单片机上电,顺序反了就会检测失败。解决:下载电路不要在电源端并联大电容,47µF已经足够,太大的话掉电慢,单片机来不及进入ISP状态电源还没放完。P3.0和P3.1如果有外接电路,确认没有对地的大电容。另外STC8系列等待时间更短,用USB转串口模块时要选带RTS/DTR控制的,手动断电再上电成功率更高。

5.5 数码管刷新导致读数卡顿和闪烁

现象:用共阴数码管动态扫描显示频率,1秒刷新一次的读数变得不均匀,数码管本身也有闪烁感。原因是把数码管段码扫描写到了定时器中断里,占用了中断执行时间,干扰了T1的秒闸门累积。解决:频率计的中断里只放测量逻辑和FREQ_READY标志,数码管刷新放到主循环中做。如果你对数码管动态扫描不熟悉,建议先用74HC595做静态锁存,段码数据一次锁存输出,MCU不需要反复刷新,逻辑简单得多。

6. 进阶:双定时器联动与多周期平均,把8位机精度再往上顶一截

6.1 双定时器联动:用T0数待测信号,用T1数参考时钟,然后取比值

真正等精度需要硬件锁存同步,但8位机可以用一种近似的双定时器联动方案:闸门开启时同时清零T0和T1,闸门结束时读取两个计数器的值。T0数的是外部待测信号,T1数的是内部机器周期,最终频率等于两者比值乘以机器频率。这样消除了一部分闸门时间本身的误差,因为闸门长度不再依赖晶振绝对精度,而是依赖两个计数器的相对关系。

// 准等精度方案:T0外部计数,T1内部计数,1秒闸门 void timer1_isr(void) interrupt 3 { TH1 = 0x3C; TL1 = 0xB0; if (++t1_cnt >= 20) { t1_cnt = 0; ET0 = 0; ET1 = 0; sig_cnt = (unsigned long)t0_ovf * 65536UL + (TH0 << 8) + TL0; ref_cnt = (unsigned long)t1_ovf * 65536UL + (TH1 << 8) + TL1; // 实际频率 = sig_cnt / ref_cnt * 1000000 freq = (unsigned long)sig_cnt * 1000000UL / ref_cnt; ET0 = 1; ET1 = 1; } }

这段代码的问题在于T1本身是产生秒闸门的定时器,读取ref_cnt时它其实已经又向前跑了几个计数,这不是真正的同步。要做得更严谨,可以用T2作为独立的参考计数器,或者外扩一个D触发器来做硬件闸门。但对于绝大多数应用,这个准等精度方案已经能把误差控制在0.1%以内,比纯测频法在低频段好很多。

6.2 多周期平均测周:低频信号的精度倍增器

测周法测单个周期,±1个机器周期的误差在低频时虽然不大,但依然存在。更稳的方式是连续测量N个周期,累加后除以N得到平均周期。50Hz信号测20个周期,对应400ms刷新一次,相对误差缩小到原来的二十分之一。

// 多周期平均测周:连续测量20个周期后计算平均频率 void int0_isr(void) interrupt 0 { static unsigned char edge_cnt = 0; if (edge_cnt == 0) { TH0 = 0; TL0 = 0; TR0 = 1; edge_cnt = 1; } else { period_sum += (TH0 << 8) + TL0; if (++edge_cnt >= 20) { freq = 1000000UL / (period_sum / 20); edge_cnt = 0; period_sum = 0; } else { TH0 = 0; TL0 = 0; } } }

这里有个需要注意的细节:每次测量完一个周期后要重新清零T0,否则period_sum累加的是“上次清零以来的时间”,而不是单个周期。我见过不少人把累加值和清零时机对不上,导致平均结果完全错误。清零操作应该在中断服务程序里完成,而不是在主循环里。

6.3 标定验证:信号发生器逐点比对才是最后一道关

代码写完之后,拿一台信号发生器和一块频率计做逐点比对。我一般选五个频点:100Hz、1kHz、10kHz、100kHz、1MHz,前四个直接读,1MHz用74HC4040分频后读。每个频点记录“标准值”和“测量值”,算出偏差百分比。如果偏差随频率线性变化,多半是晶振频率偏移;如果只在某个频段偏高,要查整形电路的回差电压和占空比影响。

测量设备不标定就是黑匣子。我接过一个测速工装项目,对方说误差很大,结果发现是参考晶振偏差达到200ppm,在100kHz频段就是20Hz误差。后来我在代码里加了一个校准系数,把标准频率计测得的偏差值折算成一个乘数,烧进STC内部EEPROM,每次上电读出来修正。校准系数不会随温度变得完全精准,但对于常温下使用的频率计,这是成本最低、效果最直接的精度补偿手段。

用8位单片机做频率计这件事,最大的收获不是省下几百元仪器钱,而是搞清楚了一个道理:测量方案必须先定边界,再写代码。晶振精度、信号波形、闸门时间、中断竞争,每一个因素都能让看似正确的代码跑出离谱的数据。希望这篇笔记能帮你少走我走过的弯路,一次就把频率计做到稳定可用。

本文还有配套的精品资源,点击获取

返回列表