51单片机定时器计数器测脉冲频率+数码管显示完整方案
2026/9/16 2:14:52 网站建设 项目流程

简介:面向51单片机学习者的脉冲频率测量与数码管显示工程资源,适合电子工程专业学生、单片机初学者及嵌入式开发爱好者用于练习定时器计数、外部中断和动态扫描显示等核心技能。压缩包为rar格式,整体大小约3.92MB,共含18个文件,包括main.c源码、Keil工程文件(uvproj/uvopt)、启动文件STARTUP.A51、可烧录的hex文件、编译生成的obj/lst/m51等中间文件,以及一张数码管接线示意图,结构清晰,便于对照学习和直接实验。已有597人学习下载。通过该工程,读者可以完整看到脉冲捕获、定时器溢出更新计数值、频率换算和数码管刷新显示的代码实现,同时结合接线图快速搭建硬件环境;源码和工程配置均保留完整,既能直接编译烧录观察效果,也可在此基础上修改参数或扩展功能,是一份兼具教学与实用价值的单片机综合训练资料。

1. 数码管显示脉冲频率这个需求,51单片机最顺手的一版方案

先说清楚这是一件什么事:有一颗传感器或者仪表在你面前输出一串脉冲,比如光电转速计、霍尔流量计、电网频率检测模块,你想在三位或六位数码管上实时看到频率数值。拆开来看工作只有两样:把脉冲数按时间窗口统计出来,再把统计结果刷到LED上。51单片机的定时器/计数器恰好把第一件事做成了硬件:T0在计数模式下可以直接对P3.4引脚的下降沿或上升沿计数,完全不占CPU,而你只需要管理好“闸门时间”这个窗口。这也是为什么很多低成本频率计、转速表、简易PLC的前端都用这一套方案。

我搭的版本是STC89C52、六位共阳数码管、74HC573锁存,脉冲从P3.4接入,显示能做到100Hz到50kHz稳定不跳。下面按测频原理、数码管驱动、完整工程到校准避坑展开,新手可以直接照抄,老手跳着看参数就够了。

2. 用 51 单片机的定时器计数器测脉冲频率:闸门时间怎么选

2.1 计数模式下 T0 当计数器,T1 当定时器,这一句话把分工说死了

频率的数学定义很简单:F = N / T。N 是这段时间里到达的脉冲个数,T是你设定的测量窗口。51单片机里T0可以工作在方式1,也就是16位计数器模式,此时引脚P3.4作为外部计数输入,每检测到一个由1到0的跳变,TH0和TL0组成的16位寄存器就自增1。外部脉冲计数这件事完全由硬件完成,不需要中断配合,只有计满65536溢出时才需要软件介入。这就是为什么测频要用计数器模式而不是定时器模式,定时器计的是机器周期,不是外部事件。

配合T1去产生精确的时间窗口。以12MHz晶振为例,我需要T1每50ms中断一次,累计20次形成1秒闸门。T1初始化时要注意TMOD寄存器的高4位属于T1、低4位属于T0,不要一起清零把另一边模式冲掉。更常见的坑是:第一次写TMOD=0x51,把T0设成计数模式、T1设成定时模式,看起来没问题,但后面重新初始化任何一个定时器时如果写成TMOD &= 0x0F或TMOD |= 0x10,会把原来的配置还原掉。所以初始化最好一次性把TMOD写完整。

void Timer_Init(void) { // 方式1:T0计数模式,T1定时模式 // TMOD 高4位给T1,低4位给T0 TMOD = 0x51; // 0101 0001 // T1定时50ms:65536-50000=15536=0x3CB0 TH1 = 0x3C; TL1 = 0xB0; // T0计数初值清零 TH0 = 0x00; TL0 = 0x00; // 中断使能 ET0 = 1; // T0溢出中断,用于扩展计数位 ET1 = 1; // T1定时中断,用于闸门 EA = 1; // 同时启动两个定时器 TR1 = 1; TR0 = 1; }

TMOD=0x51的具体值:低4位为0001,表示T0工作在方式1、C/T=1,也就是外部计数;高4位为0101,其中C/T=0表示T1是定时器,GATE=0让TR1直接控制启动,方式是1。初值0x3CB0对应12MHz晶振下50ms定时,因为一个机器周期是12个时钟周期,50ms需要的计数值是50000。这个初值有一点取舍:从写入TH1到真正运行还存在一条指令的误差,所以用软件累计20次得到的不是严格1.000000秒,而是1秒加20条指令时间。对数码管显示三位有效数字来说通常可以忽略,但做高精度频率计就要用外部晶振加更精细的时间基准去修正。

2.2 闸门时间 1 秒、0.1 秒还是 0.01 秒,先看你要几位有效数字

闸门时间直接决定分辨率。1秒闸门下,计数寄存器的值就是频率值,读出来不用换算,这是最直观的做法。但代价是刷新率只有1Hz,眼睛能看出来数码管一秒跳一次,而且100Hz以下的低频信号在1秒内只能数到几十或几百,末位跳动很大。

0.1秒闸门可以让显示每秒刷新10次,视觉效果“活”很多,但读到的N乘以10才是频率。问题在于低频:比如输入只有20Hz,0.1秒内平均只能数到2个脉冲,误差最大可能到(2±1)×10,显示在200Hz和100Hz之间跳。对于低频测量,更合理的做法是单独做周期测量法,或者用更长的闸门时间。下面是我常用的选型参考,前提是直接测频、不采用倍频:

闸门时间最小可测频率(约)典型分辨率刷新周期适用场景
1s30Hz以上±1Hz1s50Hz电网、转速显示
0.1s300Hz以上±10Hz0.1s风机转速、流水线计数
0.01s3kHz以上±100Hz0.01s高频振荡器快速校验
10s5Hz以上±0.1Hz10s精密晶振频率校准

还要注意计数寄存器的最高位。16位计数器最大65535,如果1秒内计数超过65535,TH0/TL0会溢出。这时不能只读TH0、TL0,而要在T0溢出中断里维护一个unsigned int变量,每溢出一次加1,最终频率值等于(溢出次数×65536 + 当前TH0/TL0)。很多单片机教程在这个地方直接写死“频率不超过65535”,设计上是有隐患的。后面完整工程里我会把32位计数的读取过程写出来,读取顺序、临时锁存、重新清零缺一不可。

3. 数码管显示脉冲频率:从段码表到动态扫描的细节

3.1 共阳数码管和共阴数码管的段码表正好是取反关系

驱动方案先说清。最省事的接法是P0口通过锁存器(74HC573)接段选,P1口或P2口经三极管接位选,位选的作用是让六位数码管分时共用P0这8根段线。使用锁存器的一个额外好处是:P0口在段码写入后由锁存器保持住,主循环不需要一直拽着P0不放,动态扫描的时序更好写。

共阳数码管的公共端接+5V,当某个段导通则需要对应引脚输出低电平。数一下0到9的标准段码,通常按从高位到低位顺序:a,b,c,d,e,f,g,dp,共阳时数字0的段码是0xC0,也就是1100 0000,a至f为0,g为1;数字1是0xF9。如果买到的是共阴数码管,段码就是取反,0x3F对应0。段码表不要凭记忆背,最好用笔在纸上把g段位置核对一遍,我见过的“显示8字缺一横”大多不是程序问题,而是段序没对齐。

// 共阳数码管段码,下标对应0~15,最后两位是A、b code unsigned char seg_tab[] = { 0xC0, 0xF9, 0xA4, 0xB0, 0x99, // 0 1 2 3 4 0x92, 0x82, 0xF8, 0x80, 0x90, // 5 6 7 8 9 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E // A b C d E F };

这里用code关键字把数组放到程序存储区,不会占用data。如果显示缓冲区里存的是BCD码或数字,显示时直接查表即可。注意段码表是按“P0.0接a段、P0.1接b段”这样的顺序定的,如果你的PCB把P0.0接到了dp段,整个表格就要重排。所有网上找的例程默认段序是一致的,换了板子必须先确认这一点再怀疑程序。

3.2 动态扫描的刷新率不是越高越好,消影要放在位选之前

动态扫描的原理一句话:同一时刻只有一位数码管被点亮,六位轮流点亮,利用人眼视觉暂留看起来是同时显示的。关键在于每一位的刷新频率。如果整轮扫描周期做到5ms以内,每个数码管每秒被点亮200次以上,看起来就是连续亮度。超过10ms你就会看到明显的闪烁,尤其是靠近数码管边缘的位置。更隐蔽的问题是“重影”:位选控制信号和段码信号到达时间有先后时,上一位的残影会漏到下一位上。

整轮扫描周期每位点亮时间视觉效果
1ms约167us很亮,但中断占用偏高
5ms约833us正常,推荐
10ms约1.67ms轻微可感知闪烁
20ms约3.3ms明显闪烁,不能接受

我一般的做法是在切换位选前先把段码输出全部关闭,也就是让锁存器输出全1,再更新位选,最后输出新位段码。写成代码就是:

// 六位数码管显示缓冲区 unsigned char disp_buf[6]; void Display_Scan(void) { static unsigned char pos = 0; // 1. 先关闭所有段,避免位选切换瞬间残留上一段的亮灯 P0 = 0xFF; // 2. 选中当前位,P2低4位做位选,高4位不用 P2 &= 0xF0; P2 |= (1 << pos); // 3. 输出当前位的段码,如果这一位要显示小数点,再清掉dp unsigned char seg = seg_tab[disp_buf[pos]]; if (pos == 1) { seg &= 0x7F; // 清除dp位,共阳低电平有效 } P0 = seg; // 4. 指向下一位 pos++; if (pos >= 6) { pos = 0; } }

这个函数放在定时器中断里每2ms调用一次,还是放在主循环里持续调用,取决于工程的实时性要求。我倾向于利用已有的50ms闸门中断太慢,不能用来扫描,最好另开一个1ms定时器中断来刷新显示;如果51芯片没有第二组定时器,就在主循环while(1)里不断调用,确保整个循环里没有任何delay持续超过1ms。注意P0=0xFF必须先于位选切换,顺序反了照样有鬼影。驱动共阳数码管的P0口,实际低电平才能点亮,所以“关闭段”是置1。

4. 把测频和显示合并成完整工程:脉冲频率从输入到数码管的整条链路

4.1 完整测频框架:T0计数、T1定时,读计数时要锁存初值

下面给出一个可以编译跑起来的骨架。核心逻辑是:T1每50ms产生一次中断,软件累计20次得到1s闸门,闸门到达时把T0的计数读写出来,再清0重新开始下一轮计数。读写TH0/TL0有个经典的风险:TH0和TL0是两个独立8位寄存器,分两次读取时另一个正在计数,可能读到错位组合。比如先读低字节后读高字节,如果恰好进位发生,结果会偏差256。解决方法是按特定顺序读:先读高字节,再读低字节,再一次读高字节,两次高字节相等才算有效;或者先停掉TR0再读,读完清零重开。停表法更简单,但要评估闸门关闭再开启的指令时间损失。

unsigned int count_hi = 0; // T0溢出次数 unsigned long freq_val = 0; // 当前测得频率,单位Hz unsigned char tick_50ms = 0; // 闸门累计 void Timer0_ISR(void) __interrupt 1 { count_hi++; // T0已经溢出,高位加1 } void Timer1_ISR(void) __interrupt 3 { tick_50ms++; // 累计20个50ms,也就是1秒闸门到 if (tick_50ms >= 20) { tick_50ms = 0; // 停表读取T0计数器值 TR0 = 0; unsigned int n_lo = ((unsigned int)TH0 << 8) | TL0; unsigned long total = (unsigned long)count_hi * 65536UL + n_lo; count_hi = 0; // 1秒窗口内直接等于频率 freq_val = total; // 清空T0重新开始下一轮 TH0 = 0; TL0 = 0; TR0 = 1; } }

参数说明:count_hi是16位计数器的高位扩展,因为T0是16位,配合count_hi相当于32位计数。total是门控时间内总脉冲数,当闸门恰好1秒时其数值就等于频率值。如果你把闸门改成0.1秒,就要在最后freq_val = total * 10;改成0.01秒时乘以100。这里有个取舍:读取时先停TR0再清0,会让下一轮闸门起点变晚,除第一条外的累积误差约几十个机器周期,对数码管三位显示足够;如果你追求更高精度,就不要停表,用三次读取法避免错位,那会多十几条代码,换来的是闸门连续性。

主循环里只需要不断把freq_val拆成六位十进制数字放进disp_buf,再周期调用Display_Scan。拆数字用整除和取余即可,注意用long类型运算在51上没有硬件除法,一秒才一次,不影响。这里不建议把拆数字放在中断里,因为Keil C51下的除法和取模会占用几百个机器周期,在定时中断里做会破坏时序。主循环读取freq_val前也可以临时关闭中断,防止读到半分更新的long值,读完后重新开中断。

4.2 频率值怎么上数码管:拆位、消隐和自动量程切换

得到freq_val后,要放到disp_buf[0..5]中。假如六位显示屏,最大显示999999,直接拆成每位数字即可。而实际测量中高频如123456Hz,低位会不停跳动。有两种常见处理:一是固定显示全部六位,低频时前面几位为0,这样视觉上会看到“000123”这种不美观但不影响读数;二是在前面加半字节消隐,也就是最高非零位之前的所有位不亮。消隐的代价是显示位数不固定,看惯了也没问题,但如果你要固定小数点位,就必须保留某些0。

另外一个值得做的功能是用按键在量程之间切换:比如按下按键后,显示数值从Hz切换为kHz,并把小数点固定在第三位;再按一次切换为MHz。这个功能在信号发生器上很常见。切换量程本质上是在拆位前对freq_val做一次缩放:

void Update_Display(void) { unsigned char i; unsigned long v = freq_val; // range: 0=Hz,1=kHz,2=MHz switch (range) { case 1: v = (v + 50) / 100; // 四舍五入到0.01kHz break; case 2: v = (v + 500) / 1000; // 保留千位 break; } // 拆成六位 disp_buf[0] = v / 100000 % 10; disp_buf[1] = v / 10000 % 10; disp_buf[2] = v / 1000 % 10; disp_buf[3] = v / 100 % 10; disp_buf[4] = v / 10 % 10; disp_buf[5] = v % 10; }

按键读入不能放在定时中断里做全量延时消抖,我一般用10ms扫描一次键盘,连读两次相同电平才确认按下。这样量程切换逻辑就独立于测频函数,只改一个range变量。注意这里用v=(v+50)/100做四舍五入,而不是直接除,避免kHz显示时误差被截断。实际生产中,这个量程切换还会配合Hz/kHz的单位LED灯,但作为最小方案,只靠小数点位也可以区分。

5. 用第二路定时器自检脉冲频率显示,顺手把三个常见坑填上

有信号发生器当然好,没有的时候,可以直接在这块增强型51上开一路T2定时器,让它在P1.0上输出方波,再用杜邦线把P1.0和P3.4接到一起。这样测频、闸门、显示全链路都在内部闭环,排除外部信号源干扰,是验证代码逻辑最快的方法。STC89C52这类增强型芯片都有T2;如果手头只有标准AT89C51,没有T2,可以用主循环翻转IO,频率没那么准,但同样能跑通链路。

// 用T2产生约1kHz方波,从P1.0输出 sbit Test_Out = P1^0; void Timer2_Init_1k(void) { T2CON = 0x00; // 定时器,16位自动重载 RCAP2H = 0xFE; // 500个机器周期溢出 RCAP2L = 0x0C; // 正好500us TH2 = RCAP2H; TL2 = RCAP2L; ET2 = 1; EA = 1; TR2 = 1; } void Timer2_ISR(void) __interrupt 5 { Test_Out = !Test_Out; // 翻转一次,周期1ms }

参数说明:12MHz晶振下机器周期1us,T2从0xFE0C计数到0xFFFF需要500个计数,500us溢出一次,进入中断后翻转P1.0,所以P1.0输出周期1ms的方波,也就是1kHz。如果想把重载值改成0xFF06,溢出时间缩短到10us,输出频率约为50kHz,可以快速扫一遍测频范围。注意T2中断里只做翻转,千万不要在这里调用显示刷新,否则方波频率会乱。

把P1.0接到P3.4后,数码管应该在整数秒后显示1000Hz左右。如果还是0,先看P3.4有没有和GND短接,再看TMOD的低4位是不是0x01,T0有没有进入计数模式。另一个常见错误是外部脉冲幅度不够:P3.4是TTL电平输入,外部信号如果是集电极开路输出,必须接上拉电阻,否则高电平可能停在1.2V以下,计数器数不到边沿。最后,外接信号线超过20cm时,现场如果有电机或变频器,建议P3.4对地并联一个100pF电容、串联100欧电阻,必要时加施密特触发器整形,否则数码管上偶尔跳出来的毛刺不是算法问题,而是输入抖动被当成脉冲了。整个系统标定到误差±1Hz以内,这套数码管显示脉冲频率的方案就可以交付了。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询