ARTICLE DETAIL

资讯详情

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

51单片机频率计设计:闸门计数、测周法与数码管显示实现

51单片机频率计设计:闸门计数、测周法与数码管显示实现

简介:一份面向单片机初学者的频率计-8实现工程,基于8位单片机测量周期信号频率,涵盖计数器与定时器中断配置、脉冲计数、频率换算等核心环节,适合电子技术及嵌入式入门者动手实践。压缩包共9个文件,以C源程序、HEX烧录文件为主,附带M51内存分配、LST列表、OBJ目标代码等编译中间文件,以及BPF、DDK、EDK工程配置文件,整体仅20KB,内容紧凑便于逐项对照,可完整还原Keil工程的开发与调试流程。已有218人学习下载,说明该套频率计实现方案具有一定的典型性和参考价值。通过这份资料,读者可以完整了解从定时闸门控制到脉冲计数的频率计设计思路,理解中断服务程序与主循环的配合方式,同时获得可编译、可烧录的实物代码,为后续扩展频率量程或精度优化打下基础。

1. 频率计-8 是什么:一块能数出 8 位频率的显示盒子

做小车测速要数轮子上的霍尔脉冲,调信号发生器要确认输出频率对不对,验证晶振标称值又希望得到一个比万用表更直接的答案,频率计-8 就是为这些场景做的一个单片机实现频率计数器:把外部方波脉冲接到定时器引脚上,让硬件在一段已知闸门时间内自动数脉冲个数,再由软件换算,把频率值送到 8 位数码管显示。它介于课程设计作业和桌面仪器之间,适合正在做 51 单片机毕设课题或蓝桥杯同类题目的学生,也适合想花几十块钱搭一台小仪表的工程师。整台机器的难点不在数码管驱动,而在闸门怎么开、低频段怎么切换测量方法。

2. 测量原理先立住:闸门计数、测周法和等精度方案怎么选

2.1 固定 1 秒闸门为什么测不了低频信号

先看最直觉的做法:开一个 1 秒的窗口,数输入脉冲的上升沿个数,个数就是频率值。窗口由定时器产生,外部脉冲接在计数器引脚上由硬件自动计,程序只在窗口结束时把计数值读出来。脉冲计数不占用 CPU,主频再低也不会漏数,这个特性决定了用单片机做频率计数器是可行的。

误差出在窗口的边沿。计数值 N 是整数,但闸门开启和关闭的时刻几乎不可能和被测信号的边沿对齐,一开一关最多各差一个脉冲,也就是最大 ±1 个数的误差。把这个误差换成相对误差就是 1/N。1 秒闸门测 1MHz,N 是一百万,误差很小;测 100Hz,N=100,误差 1%,读数在 99 到 101 之间跳,这就没法用了。低频问题的本质是窗口内能数到的脉冲太少。

改善有两个方向:拉长窗口到 10 秒,但刷新率低得没法当仪器用;在低频段放弃数脉冲,改成量周期。这就是测频法和测周法分工的由来。另外要提前说清一个前提:测频法默认输入是规整方波。如果信号占空比严重不对称,只要边沿清晰,计数结果依然正确,因为数的是边沿数不是高电平时间;真正怕的是噪声在阈值附近产生一串伪边沿,那会让读数大几十倍,这不是算法问题,而是前端整形电路的问题。

2.2 测周法:低频段的通行做法

低频信号周期长,50Hz 信号一个周期是 20ms,如果用一个 10us 的时基去量一个周期,能数到约 2000 个 tick,量化误差只有 0.05%,比 1 秒闸门测频法的 2% 好了两个数量级。测周法的核心是让定时器跑在一个可信的时基下,用外部中断去捕捉被测信号边沿,记录两次边沿之间的 tick 数,再换算成频率。

实现上我一般会保留 T0 做高频计数,另用 T1 产生 10us 中断维护 tick 计数器,被测信号接到 INT0,下降沿中断里读出 tick 差值。这个方案的坑在于时基中断和外部中断的优先级:tick 计数如果被其他中断打断或丢失,周期会整段跳变,所以关键变量都要 volatile 修饰,中断服务程序里只做最小操作。

切换点没有唯一答案,取决于闸门时间和 tick 分辨率。以 1 秒闸门配 10us 时基为例,两种方法的误差曲线在 1kHz 附近相交:1kHz 用测频法 N=1000,相对误差 0.1%;用测周法只能数到 100 个 tick,误差反而到 1%。所以低于 500Hz 时切测周法更可靠,高频段继续用测频法。这个阈值我习惯写在主循环里,用 if 判断频率值后切换,而不是让用户手动拨开关。

2.3 再说等精度测频:为什么多数 DIY 方案不碰它

等精度测量是为了消除“±1 个被测脉冲”误差而设计的:让闸门的开启和关闭都由被测信号自己的边沿触发,这样开、关时刻天然和脉冲边沿对齐,被测脉冲计数不再有 ±1 误差,误差转移到参考时钟计数上,于是测量精度与被测频率无关,全频段一致。听起来完美,但代价是需要一套闸门同步电路,在 51 上通常要外接 D 触发器和门电路,软件还要同时读两个计数器。

常见做法是用 CPLD 或 FPGA 实现等精度频率计,因为并行计数和同步闸门可以做成纯硬件逻辑。用普通单片机硬做软等精度,要么牺牲同步精度,要么代码复杂到不如直接加预分频器,所以我一般不动它。真正值得先做好的,是测频加测周的组合方案,它已经能覆盖 10Hz 到数 MHz 的绝大多数工程场景。等精度不是不能用,而是放到下一级硬件平台上去解决更划算。

2.4 选型:STC89C52 还是 STC8 系列

频率计对主频要求不高,对资源的要求是至少两个定时器/计数器:一个做外部计数,一个做闸门时基。这个门槛 STC89C52 和 STC8G 都满足。STC89C52 的优势是资料量大,照着江科大 51 单片机笔记把定时器配置练过一遍,TMOD、TH0/TL0 这些寄存器都熟,移植成本低;STC8 系列的优势是主频更高、外设更丰富,但寄存器布局和经典 51 不一样,照着老代码抄容易踩坑。

我自己的做法是:手头有开发板就用板上的芯片,不必为了频率计额外买芯片。以 STC89C52RC 为例,被测信号接 P3.4,T1 做闸门,P0 加两片 74HC573 驱动 8 位数码管,12MHz 晶振就能跑。如果你的板子是 STC8G1K17 这类新器件,思路完全一样,只是 TMOD 和定时器初值按头文件来写。选型时看一眼引脚复用:P3.4 如果被板上的按键或 LED 占用,测频时会互相干扰,最好飞线从排针引出。T0 对外部计数通常是下降沿触发,这一点在判断读数异常时有对照意义,先记下来。

3. 硬件搭建:整形电路、引脚分配与 74HC573 锁存显示

3.1 输入整形:为什么计数器前级必须放一片 74HC14

被测信号直接进单片机引脚是最常见的翻车点。单片机 IO 输入阈值不是一条绝对干净的分界线,信号上升沿稍慢,或者叠了纹波,阈值附近就会反复跳变,一次真实沿可能被数成几次,读数瞬间变成天文数字。给小车测速时霍尔传感器输出是带负载的慢信号,这个现象格外明显。

常见做法是在输入端加一片 74HC14 施密特触发器反向器。施密特触发器的正向阈值和负向阈值不重合,中间有一段回差,信号只有越过整个回差区间才会翻转状态,噪声造成的反复穿越被天然滤掉。两路反相器串联,输出和输入同相,同时完成整形和缓冲。74HC14 用 5V 供电,阈值大约在 1.6V 和 3.4V 附近,对 3.3V 逻辑信号能正确识别,但低电平必须比 1V 更低才安全,这与普通 74HC 门电路要求一致。

输入保护也要一起做。被测信号幅度不确定时,先在输入端串一只 1k 电阻再进 74HC14,防止超过 VCC+0.5V 造成芯片闩锁;如果信号高于 5V,另用 10k 与 10k 电阻分压后再送整形。现场有电机或继电器时,信号线用屏蔽线、屏蔽层单端接地,能去掉大部分共模干扰,这比在软件里加任何滤波都有效。

3.2 引脚分配与最小接线表

整机最小系统包括 STC89C52RC、12MHz 晶振、复位电路、两片 74HC573、8 位共阳数码管、8550 三极管阵列和 74HC14。下面是我习惯的引脚分配,照这个接线基本不会出现两个外设抢同一个 IO 的问题:

信号接到哪说明
被测方波输入74HC14 第 1 路输入先整形再进单片机
整形后脉冲P3.4(T0)硬件计数,下降沿计数
段码数据P0.0 ~ P0.7两片 573 的 1D~8D 并接
段码锁存P2.674HC573 的 LE
位码锁存P2.7另一片 573 的 LE
数码管位选8550 集电极573 输出经 10k 电阻接 8550 基极
下载串口P3.0(RXD)、P3.1(TXD)接 CH340C 的 TTL 电平

段码和位码共用 P0,靠的就是 573 的锁存功能:先让 P0 输出段码,拉一下段码锁存,段码被保存在 573 输出端;再让 P0 输出位码,拉一下位码锁存,位选也被锁存好。P0 随后可以空出来做别的事,数码管内容保持不变。P0 是开漏输出,必须在 P0 到 VCC 之间接 8 只 4.7k 上拉电阻,否则高电平顶不上去。

数码管限流电阻接在 573 输出和段引脚之间。共阳接法下段点亮时 573 输出低电平灌电流,每段串 220Ω 电阻,把段电流限制到 5~8mA,8 段全亮总电流约 50mA,USB 供电足够。位选用 8550 PNP 管,发射极接 5V,基极经 10k 电阻由位码锁存驱动,集电极接数码管公共阳极端,位码为低电平时三极管导通。参考手头 51 核心板原理图确认一下 P3.4 没有接其他外设,再飞线。

3.3 数码管动态扫描的程序配合

8 位共阳数码管一位一位直接点需要占 16 个 IO,锁存方案下程序要做的事变成四步:P0 写段码、段码锁存、P0 写位码、位码锁存,然后延时 1~2ms 换下一位。8 位轮流扫完一轮约 16ms,刷新率约 60Hz,人眼看不到闪烁。

我一般把扫描写成状态机而不是多个延时嵌套。主循环维护一个 scan_pos 变量,每次循环刷新当前位后自增,到 8 归零。这样主循环既能做显示扫描,又不会阻塞闸门标志的判断。如果中断里也去碰 P0 或锁存引脚,就会出现虚亮和鬼影,所以显示数据要放独立缓冲区,中断只置标志,主循环统一刷新。

段码表按共阳数码管给一份备查:0 到 9 分别是 0xC0、0xF9、0xA4、0xB0、0x99、0x92、0x82、0xF8、0x80、0x90。位码表按 PNP 低电平导通给:第 1 位到第 8 位是 0xFE、0xFD、0xFB、0xF7、0xEF、0xDF、0xBF、0x7F。如果换用共阴数码管,段码取反,位选三极管换 NPN,代码里只改这两张表。

4. C 代码跑通测频:T0 计数门控、临界区处理与下载链

4.1 初始化 T0 做外部计数器,T1 做 1 秒闸门

TMOD 寄存器一次配置两个定时器。低四位给 T0,高四位给 T1。T0 要对外部脉冲计数,C/T 位写 1,选 16 位模式;T1 要产生 50ms 中断,C/T 写 0,也选 16 位模式。12MHz 晶振下机器周期是 1us,T1 计数 50000 次就是 50ms,初值取 65536-50000,十六进制是 0x3CB0。写在中断服务程序里的初值重装和计数器清零逻辑如下:

#include <STC89C5x.H> // 显示缓冲区,从高到低存 8 个数字 unsigned char disp_buf[8]; // 闸门计数,T1 中断里累加,20 次就是 20 * 50ms = 1s volatile unsigned char gate_tick = 0; volatile unsigned char gate_flag = 0; // T0 溢出次数,用来和 TH0/TL0 拼成完整计数值 volatile unsigned int t0_overflow = 0; void timer_init(void) { // TMOD: T1 定时模式1, T0 计数模式1 -> 0x15 TMOD = 0x15; TH0 = 0x00; TL0 = 0x00; // T0 从 0 开始数脉冲 TH1 = 0x3C; TL1 = 0xB0; // T1 初值 15536,16 位定时 50ms @12MHz ET0 = 1; // 允许 T0 溢出中断,累计溢出次数 ET1 = 1; // 允许 T1 溢出中断,产生闸门时基 EA = 1; TR0 = 1; // 启动 T0 计数 TR1 = 1; // 启动 T1 定时 } void timer1_isr(void) interrupt 3 { TH1 = 0x3C; TL1 = 0xB0; // 重装初值,保证闸门稳定在 50ms if (++gate_tick >= 20) { gate_tick = 0; gate_flag = 1; // 1 秒闸门到 } } void timer0_isr(void) interrupt 1 { t0_overflow++; // 计数超过 65535 时溢出,必须累计 }

TMOD=0x15 是整套方案的关键:高四位 0x10 表示 T1 定时模式 1,低四位 0x05 表示 T0 计数模式 1。如果误写成 0x11,T0 也变成定时器,P3.4 上的脉冲不再参与计数,读数会变成完全无关的数据,这是频率计数器最常见的低级错误。

T1 中断里手动重装初值而不是用自动重装,是为了让闸门更接近严格的 50ms。定时器中断响应本身有延迟,软件重装可以尽量压缩误差,但要记住:任何中断嵌套都会让重装晚几个机器周期,所以中断服务里除了重装和计数,不要做任何显示或计算类的工作。

4.2 读计数与换算:临界区处理和 1 秒公式

闸门标志置位后,主循环要把 T0 的计数值取出来。问题在于 T0 还在继续跑,读取 TH0、TL0、t0_overflow 这三个量不是原子操作,取到一半溢出中断进来,三个量就不配套了。解决办法是关中断再读,读完立刻恢复,换算公式按 1 秒闸门直接取计数值即可:

unsigned long count_all; unsigned long freq; unsigned char i; void display_update(unsigned long f) { // 拆成 8 位十进制数字,存进显示缓冲区 for (i = 0; i < 8; i++) { disp_buf[i] = f % 10; f /= 10; } } void main(void) { unsigned char scan_pos = 0; timer_init(); while (1) { if (gate_flag) { gate_flag = 0; EA = 0; // 关中断,防止读数被撕裂 count_all = (unsigned long)t0_overflow << 16 | (unsigned int)TH0 << 8 | TL0; TH0 = 0; TL0 = 0; // 读数后立刻清零,开始下一个闸门 t0_overflow = 0; EA = 1; // 恢复中断 freq = count_all; // 闸门 1 秒,计数值就是频率值 display_update(freq); } // 8 位动态扫描,每次循环刷一位 // 段码和位码经 P0 分时输出,由 573 锁存保持 P0 = seg_tab[disp_buf[scan_pos]]; SEG_LATCH = 1; SEG_LATCH = 0; P0 = bit_tab[scan_pos]; BIT_LATCH = 1; BIT_LATCH = 0; if (++scan_pos >= 8) scan_pos = 0; } }

主循环里的扫描延时由整个 while 循环的自然耗时构成,每位刷新间隔并不严格相同,但锁存输出后显示内容由 573 保持,肉眼看不到跳动。如果想让每位刷新时间可控,可以在每位之间加 1ms 延时函数,代价是主循环对 gate_flag 的响应最多慢 8ms,不影响闸门精度。

count_all 是 32 位无符号数,最大能表达 4.29G。但 T0 外部计数频率受内部采样限制,STC89C52 在 12MHz 晶振下被测信号一般不要超过 3~4MHz,具体极限值以数据手册为准。超过这个范围,读数会偏小且没有规律,这时要在硬件上加 74HC390 之类的前置预分频器。

4.3 低频段切换测周法的补丁

主循环里的 freq 计算在闸门为 1 秒时等价于频率。如果检测到 freq 低于 500Hz,切换条件就该成立。测周模式下被测信号改接到 INT0,T1 继续跑 10us 中断维护 tick 计数器:

volatile unsigned int tick_10us = 0; volatile unsigned int last_tick = 0; volatile unsigned int period_ticks = 0; void timer1_isr(void) interrupt 3 { TH1 = (65536 - 92) >> 8; // 11.0592MHz 下约 10us,按实际晶振计算 TL1 = (65536 - 92) & 0xFF; tick_10us++; } void int0_isr(void) interrupt 0 { // 下降沿触发,两次沿之间的 tick 差值就是一个周期 period_ticks = tick_10us - last_tick; last_tick = tick_10us; }

这段补丁给的是测周思路。晶振若不是 11.0592MHz,初值要按机器周期重算,换算公式是 1000000us 除以 period_ticks,例如 50Hz 信号周期 20ms,period_ticks 约 2000,算出来就是 50Hz。切回测频模式时要把 T0 清零并恢复 TMOD,同时关掉 INT0 外部中断,避免残留的边沿中断继续干扰主流程。

这里有个实测中容易忽略的点:测周法只关心边沿间隔,对占空比极端的信号比测频法更稳当;但如果输入信号本身边沿在抖动,周期读数还是会跳。此时在整形输出后加一个 RC 低通,截止频率取信号频率的 5~10 倍,能把高频毛刺压掉,又不明显影响真实沿的到达时刻。

4.4 用 STC-ISP 下载固件时避免失败

Keil5 里新建工程时选 Atmel AT89C52,在 Output 页勾选 Create HEX File,编译生成 hex。下载用 STC 单片机官网的 STC-ISP 工具,芯片型号选 STC89C52RC,串口选 CH340 对应的 COM 口,波特率建议先设 2400,慢速下载成功率更高。STC 的下载方式是冷启动:先点“下载/编程”,再给板子上电,顺序反了就会一直等待检测。

如果提示“正在检测目标单片机”后超时,先看 P3.0、P3.1 是否被外设占用,再查 CH340 驱动是否正常。Proteus 仿真 51 单片机时串口行为是理想模型,仿真通过不代表实机没问题,这一节说的冷启动时序和电平问题在仿真里根本不会出现,实机调试时还要从电源和共地查起。

提示:下载前先把 RX/TX 两根杜邦线交叉确认一遍,很多“下载失败”其实是线插反了。

5. 频率计避坑记录:5 个让读数翻车的常见问题

频率计调试时容易踩的坑集中在五个方向:共享变量、定时器模式、低频误差、输入干扰和下载时序。下面按我实际遇到的频率排序,每条写清楚现象、原因和排查顺序。

5.1 显示乱跳:中断与主循环共享变量没保护

现象:数码管大部分时间正常,每隔一秒在闸门切换瞬间跳几个数,或者某一位偶尔虚亮。原因:主循环读 count_all 时,T0 溢出中断刚好插入,溢出次数和计数值不是同时刻的快照;显示缓冲区被中断改动,扫描到一半换了段码。解决:跨中断变量一律加 volatile,取计数值时用 EA=0 关中断完成快照,显示数据只在主循环写入,中断只置标志。排查顺序是先确认乱跳是否固定发生在闸门刚结束的时刻,是就优先怀疑临界区。

5.2 频率折算完翻倍或停滞:TMOD 写成定时器模式

现象:信号源给 1000Hz,显示 2000 或完全乱数,换输入频率读数不跟随。原因:TMOD 低四位被误写成定时器模式,T0 不再数外部脉冲而是按内部机器周期计数,读数变成由晶振决定的无关值;另一种情况是整形后信号抖动,边沿被数了两次。解决:先核对 TMOD=0x15 并读回验证,再用示波器看 P3.4 波形是否只有一个干净下降沿。排查顺序是先软件后硬件,寄存器写错比硬件问题常见得多。

5.3 低频读数来回飘:固定闸门在低频段误差本来就大

现象:1 秒闸门下测 50Hz,读数在 49 到 51 之间不停跳,加软件滤波也压不住。原因:这是测频法的固有量化误差。50Hz 在 1 秒闸门内只有 50 个沿,±1 计数误差就是 2%,每次采样本身就差一个数,平均滤波消不掉。解决:把频率低于 500Hz 的切换分支写进主循环,切到测周模式用 10us 时基测周期,误差立刻降到 0.5% 以内。排查顺序是先算一下理论误差,如果读数跳动范围和理论误差吻合,就不要在滤波上浪费时间。

5.4 电机一开读数疯涨:输入整形没做全

现象:信号发生器输入时读数正常,电机或继电器一工作,数码管数字飙升到离谱值。原因:电磁干扰耦合进输入线,在阈值附近反复穿越,一次真实沿被数成几十次,计数器只认边沿不认真假。解决:输入端加 74HC14 施密特整形,信号线换屏蔽线,输入串联 1k 电阻后对地并联 100pF 电容构成低通;共地必须做扎实,实测很多“干扰”其实是地线压差造成的误触发。排查顺序是先看输入波形,再用镊子碰输入看读数反应,最后查地线。

5.5 STC-ISP 下载失败:冷启动时序和端口占用

现象:点“下载/编程”后工具一直停在“正在检测目标单片机”,直到超时。原因:多数是下载时序问题,STC 必须先把工具切到下载状态再给目标板上电;也有源程序把 P3.0/P3.1 当普通 IO 使用,把串口占住了。解决:按“先点下载、再上电”的固定顺序操作,P3.0/P3.1 只留给串口,波特率降到 2400,换一根短 USB 线再试。排查顺序是先确认冷启动时序,再用示波器看 RXD 引脚在点击下载瞬间有没有波形,没有波形就换 CH340 驱动或检查数据线。

6. 把频率计做准的关键一步:先校准时基再相信计数器

6.1 基准校准与整机验证

频率计真正的基准是晶振,不是计数逻辑。12MHz 晶振常温下偏几十 ppm 很常见,1 秒闸门显示 10MHz 时就会偏出几百赫兹,想当仪器用必须先校准。常见做法是拿一台精度更高的信号源输出 10.000000MHz,接入 P3.4,记录读数 f_read,软件里引入一个校准系数,用浮点算出修正后的频率:

// 校准系数:用已知参考频率除以当前读数得到 float cal_factor = 10000000.0f / f_read; // 之后每次显示频率前乘一次该系数 freq_display = (float)freq * cal_factor;

校准系数不能在整数运算里算,否则 10MHz 级别的小偏差会被整除抹掉。参考信号源越准,校准结果越接近真实值;如果手头没有高精度信号源,至少用温补晶振模块做参考,不要把开发板自带的普通晶振当成参照。

校准完成后做整机验证:分别输入 1kHz、100kHz、1MHz、5MHz,记录误差。如果误差随频率等比例变化,说明问题在晶振基准,继续调校准系数;如果误差没有规律且频率越高越乱,回去查临界区和 TMOD 配置,多半是计数链路有丢数。我的习惯是验证数据记在代码文件头部注释里,下次再动硬件能直接对比。

我第一次做频率计时,开发板上默认晶振是 11.0592MHz,实测 10MHz 信号偏了 700Hz,查了两天中断代码,最后用另一块板子测晶振频率才发现是基准不准。那次之后我养成了一个习惯:做任何测频率的项目,第一行注释先写基准晶体实测值,调试顺序永远是先校准时基,再查计数链路。希望帮到你。

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

返回列表