
简介面向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不要一起清零把另一边模式冲掉。更常见的坑是第一次写TMOD0x51把T0设成计数模式、T1设成定时模式看起来没问题但后面重新初始化任何一个定时器时如果写成TMOD 0x0F或TMOD | 0x10会把原来的配置还原掉。所以初始化最好一次性把TMOD写完整。void Timer_Init(void) { // 方式1T0计数模式T1定时模式 // TMOD 高4位给T1低4位给T0 TMOD 0x51; // 0101 0001 // T1定时50ms65536-50000155360x3CB0 TH1 0x3C; TL1 0xB0; // T0计数初值清零 TH0 0x00; TL0 0x00; // 中断使能 ET0 1; // T0溢出中断用于扩展计数位 ET1 1; // T1定时中断用于闸门 EA 1; // 同时启动两个定时器 TR1 1; TR0 1; }TMOD0x51的具体值低4位为0001表示T0工作在方式1、C/T1也就是外部计数高4位为0101其中C/T0表示T1是定时器GATE0让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才是频率。问题在于低频比如输入只有20Hz0.1秒内平均只能数到2个脉冲误差最大可能到2±1×10显示在200Hz和100Hz之间跳。对于低频测量更合理的做法是单独做周期测量法或者用更长的闸门时间。下面是我常用的选型参考前提是直接测频、不采用倍频闸门时间最小可测频率(约)典型分辨率刷新周期适用场景1s30Hz以上±1Hz1s50Hz电网、转速显示0.1s300Hz以上±10Hz0.1s风机转速、流水线计数0.01s3kHz以上±100Hz0.01s高频振荡器快速校验10s5Hz以上±0.1Hz10s精密晶振频率校准还要注意计数寄存器的最高位。16位计数器最大65535如果1秒内计数超过65535TH0/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 0000a至f为0g为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。注意P00xFF必须先于位选切换顺序反了照样有鬼影。驱动共阳数码管的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: 0Hz,1kHz,2MHz 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(v50)/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晶振下机器周期1usT2从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位是不是0x01T0有没有进入计数模式。另一个常见错误是外部脉冲幅度不够P3.4是TTL电平输入外部信号如果是集电极开路输出必须接上拉电阻否则高电平可能停在1.2V以下计数器数不到边沿。最后外接信号线超过20cm时现场如果有电机或变频器建议P3.4对地并联一个100pF电容、串联100欧电阻必要时加施密特触发器整形否则数码管上偶尔跳出来的毛刺不是算法问题而是输入抖动被当成脉冲了。整个系统标定到误差±1Hz以内这套数码管显示脉冲频率的方案就可以交付了。本文还有配套的精品资源点击获取