
简介面向51单片机入门学习者的电子时钟完整实现方案基于4只LED数码管显示“时时分分”以发光二极管闪动指示秒计数并通过独立按键完成时间与闹铃设置、闹铃查看以及蜂鸣器停止等交互同时支持百分之一秒精度的秒表功能适合课程设计、Proteus仿真实训或单片机原理巩固。压缩包共19个文件整体约22.22MB包含Keil工程源码与目标文件、Proteus仿真电路、课程设计说明文档和讲解分析录屏其中uvproj/hex/a51为程序工程pdsprj/pdsbak为仿真电路docx为设计报告wmv为操作视频分类明确便于对照学习与二次开发。资源覆盖数码管动态扫描、按键消抖与功能切换、定时器中断、蜂鸣器驱动等典型单片机模块读者既可打开源码理解时序逻辑也可在仿真中直接观察走时、闹铃和秒表状态再结合录屏讲解与文档梳理解题思路形成从需求分析、硬件设计到软件调试的完整经验。目前已有1452人学习下载对需要快速搭建综合项目或完成电子时钟类作业的学习者具有较强的参考价值。1. 4位数码管、1ms定时中断与C51电子时钟的骨架晚上试了下这个工程按住设置键连续调时数码管没闪断闹钟响的时候按下停止键蜂鸣器立刻安静秒表计数到99.99自动回零。很多同学自己写的时候做不到这种手感问题往往不是点不亮数码管而是把显示扫描、按键检测、时间累计挤在一个while循环里互相抢时间。这个C51电子时钟用4位LED数码管显示“时时分分”LED闪动做秒指示带闹铃和百分之一秒秒表整套逻辑压在一个AT89C52里。核心思路是让1ms定时器中断承担时间基准和扫描节奏主循环只处理按键与状态切换。我从T0重装值开始拆到Proteus调试覆盖整个工程能跑起来的关键细节适合做课程设计也适合想理解中断驱动架构的工程师回看。2. T0定时器重装值计算与数码管动态扫描刷新时序2.1 T0方式1选型与初值计算公式AT89C52的T0是16位计数器这个工程选方式1TMOD0x01做系统时钟。方式0是13位计数器最大计数8192在12MHz晶振下最多定时8.192ms想精确计时1ms还得分段溢出判断方式1直接用65536减计数个数一行代码就能装下初值。初值计算落到寄存器上就是下面两行TH0 (65536 - 1000) / 256; // 12MHz机器周期1us数1000个就是1ms TL0 (65536 - 1000) % 256;算出来的TH00xFC、TL00x18。这里用除法而不是直接写死0xFC18是为了换晶振频率时容易改。如果是11.0592MHz晶振机器周期约1.085us1000个机器周期就不是1ms了初值要按实际机器周期重新数。实际keil编译后interrupt 1入口会自动保存ACC、PSW等寄存器重装指令也要耗时几个机器周期。所以很多工程把重装值放在中断第一句执行而不是依赖硬件自动重装就是为了让这些额外开销变得可估算。2.2 共阴数码管段码表与位选极性工程用的是共阴数码管P0口输出段码P2口低4位做位选。共阴数码管公共端接地段码高电平点亮字段0到9的段码如下表。数字P0输出段码00x3F10x0620x5B30x4F40x6650x6D60x7D70x0780x7F90x6F如果你手里的是共阳数码管把每个段码按位取反再输出其他逻辑不用动。P0口在51里是开漏输出做实物板必须在P0到数码管段脚之间串270到470欧姆限流电阻Proteus仿真里不串也能显示但做板子不串数码管亮度偏高且长时间跑会拉伤IO口。2.3 1ms中断扫描一位的驱动代码动态扫描的原理是一个时间片只点亮一位按位轮询刷新。如果扫描周期超过10ms人眼会明显看到闪烁这个工程在1ms中断里切换一位4ms完成一轮刷新刷新率250Hz肉眼基本看不到闪烁亮度也比4位同时用IO口直驱均匀得多。void Timer0_Init(void) { TMOD 0x01; // T0方式116位定时器 TH0 0xFC; // 12MHz下1ms重装值高8位 TL0 0x18; // 1ms重装值低8位 ET0 1; // 开T0中断 EA 1; // 开总中断 TR0 1; // 启动T0 } static unsigned char scan_pos 0; static unsigned char smg_buf[4] {0, 0, 0, 0}; static unsigned int ms_cnt 0; void Timer0_ISR(void) interrupt 1 { TH0 0xFC; // 重装中断里的其他指令会占用几个机器周期 TL0 0x18; P2 0x00; // 消隐先关掉所有位选 P0 SEG_TAB[smg_buf[scan_pos]]; // 输出当前位的段码 P2 (unsigned char)(1 scan_pos); // 点亮当前位 scan_pos (scan_pos 1) 0x03; // 0~3循环 ms_cnt; // 进一次中断就是1ms if (ms_cnt 1000) { ms_cnt 0; sec_flag 1; // 置位后交给主循环处理 } }最容易翻车的是消隐顺序。段码输出前必须先把P2位选全部清掉否则上一位的位选还没撤走P0已经切到下一位的段码数码管会看到很短的混叠拖影。另一个细节是scan_pos只取0到3中断里不该对smg_buf下标做复杂运算所以这个工程在秒表模式下只是改smg_buf填充内容扫描逻辑完全不动。中断里不直接改hour、minute只置sec_flag是因为hour、minute可能同时被主循环里的按键设置修改。如果中断和主循环同时写入这两个8位变量会引入中间态。中断负责“产生1秒事件”主循环负责“消费这个事件”这个划分是整个工程的骨架之一。2.4 LED秒闪烁与秒事件的消费秒显示用P1.0上的一颗LED每秒翻转一次。翻转动作放在主循环消费sec_flag时做不在中断里做就算主循环因为按键处理多跑了几毫秒LED翻转节奏始终跟着sec_flag走不会越积越乱。if (sec_flag) { sec_flag 0; LED_SEC ~LED_SEC; // 一秒翻转一次 }LED驱动不需要PWM开关量就够了。如果你想在Proteus里后续用示波器验证时间精度建议在P1.0到地之间加一个330欧电阻然后直接用虚拟示波器卡方波周期。3. 按键状态机与时间闹铃设置消抖、回绕与设置流程3.1 为什么不能只用delay做按键消抖最朴素的按键读取是主循环发现引脚为低delay十几毫秒再确认一次。这个工程里这么写会有三个问题一是用户按住按键时delay期间按键状态会被重复判成多次按下二是主循环被delay阻塞sec_flag不能及时清零LED秒闪节奏会被拉长三是长按和短按无法区分而秒表模式正好依赖长按设置键切换。工程里用的是连续采样累加的方式主循环每轮都读一次按键电平连续多次读到低才确认同时累计低电平持续的时间超过阈值再触发长按。这样既过滤了机械抖动也让短按与长按的区别只体现在累计值上。void Key_Scan(void) { static unsigned char key_buf 0xFF; static unsigned int press_ms 0; static bit long_press_sent 0; key_buf (key_buf 1) | (KEY_SET ? 1 : 0); if ((key_buf 0x07) 0x00) { // 连续3次采样都是低 press_ms; if (press_ms 800 long_press_sent 0) { long_press_sent 1; // 长按设置键进入秒表模式 stopwatch_mode ~stopwatch_mode; } } else { if (press_ms 3 !long_press_sent) { key_short_evt 1; // 短按事件主循环消费 } press_ms 0; long_press_sent 0; } }key_buf就是一个移位寄存器每次把KEY_SET电平左移进来低3位全0才认为按键稳定。这比delay消抖好的地方在于整个消抖窗口不需要阻塞主循环三次采样在1ms内就能完成。press_ms统计的是稳定低电平持续了多少次主循环800对应长按阈值。这个值在Proteus里可能会偏灵敏因为仿真器里主循环跑得和真实芯片不一样。如果发现短按设置键直接跳进了秒表就把800调大到1200或者把key_buf的判断位数从3改成4延长确认时间。3.2 设置状态迁移设置键短按在整个时间设置流程里是循环状态机状态列表如下。状态含义数码管显示内容MODE_RUN正常走时时时分分MODE_SET_H设置小时时位闪烁MODE_SET_M设置分钟分位闪烁MODE_SET_AH设置闹钟小时显示闹钟小时MODE_SET_AM设置闹钟分钟显示闹钟分钟正常走时按一下设置键进入MODE_SET_H再按进MODE_SET_M之后是闹钟小时、闹钟分钟最后再按设置键保存退出。秒表模式不进这条链路它由长按设置键从任意状态切入。秒表运行时短按设置键做启动/停止另一颗按键做清零。闪烁提示没有额外开定时器而是复用sec_flag在设置小时时每次秒事件翻转就插入一次清空显示的操作让对应位看起来以秒周期闪动。这只影响显示缓冲不影响内部计时变量。3.3 加减调整与数字回绕设置项加减的边界处理不能简单“超过就丢弃”小时到23再加应回0分钟到59再加应回0。这样连续调整更顺手也不会出现用户停在23无法往上加的情况。if (key_short_evt) { switch (state) { case MODE_SET_H: if (KEY_UP 0) { hour; if (hour 23) hour 0; } else if (KEY_DOWN 0) { if (hour 0) hour 23; else hour--; } break; case MODE_SET_M: if (KEY_UP 0) { minute; if (minute 59) minute 0; } else if (KEY_DOWN 0) { if (minute 0) minute 59; else minute--; } break; default: break; } key_short_evt 0; }这里有一个容易被忽略的点KEY_UP和KEY_DOWN用if和else if串联如果两个按键同时按下一次扫描只执行加一下次扫描才执行减一。严格说这不是期望行为但课设里不会有人同时按两个按键。如果要做防冲突可以在进入函数时先读键值再判断只有某一路键值为零时才允许对应分支执行。3.4 闹钟时间的存储与显示闹钟小时和闹钟分钟分开存和hour、minute独立。正常走时时即使闹钟时间到了数码管也始终显示实时时间蜂鸣器响但屏不切换这样用户看到的就是当前时刻不会被切走。设置闹钟时smg_buf填充的是alarm_hour和alarm_minute填充函数和显示实时时间的函数保持一致只是数据源不同。退出设置后smg_buf自动切回hour和minute。闹钟使能用一个bit变量维护建议把它放在设置流程的最后一个状态按加键切换开和关开启时用小数点常亮作为指示避免用户忘记自己有没有开闹钟。4. 蜂鸣器闹钟触发条件与百分之一秒秒表计时4.1 闹钟匹配的时间窗口时钟走到1秒更新后主循环检查当前时间和闹钟是否匹配。最直接的写法是hour等于alarm_hour并且minute等于alarm_minute但这样会导致闹钟持续响60秒。这个工程要求按停止键停所以触发条件应该收窄到秒从59跳到0的那一瞬间即只在second等于0时判断。if (alarm_en sec_flag) { if (hour alarm_hour minute alarm_minute second 0) { alarm_ring 1; // 进入响铃状态 } }这里依赖sec_flag中断每1秒置位一次主循环在消费sec_flag时检查second。当second为0的那一次事件被消费后后面即使hour和minute仍然匹配second已经不是0不会重复触发。按停止键清掉alarm_ring闹钟就停了等下次整分时才能再触发。4.2 有源蜂鸣器与无源蜂鸣器的驱动差异Proteus里蜂鸣器模型分有源和无源。有源蜂鸣器内部有振荡源高电平就响无源蜂鸣器需要方波才能发声。P1.1作为控制脚有源版本直接写BEEP1无源版本要按频率翻转。蜂鸣器类型控制方式停止方式有源输出高电平输出低电平无源输出1k~3kHz方波停止翻转并输出低无源版本可以在1ms中断里翻转BEEP分频后的频率落在听觉舒适区。一个简单实现是把中断里的beep_cnt每次累加达到阈值就翻转一次BEEP。每2ms翻转一次输出大约250Hz方波声音偏低沉要更清脆就把阈值改成1得到500Hz。实际做板子时蜂鸣器两端一定要并联续流二极管。Proteus里不接也能仿真但真机不接蜂鸣器关断瞬间的反向电动势可能击穿P1.1口。停止闹钟时只清alarm_ring不清时间变量这样同一分钟只响一次。4.3 百分之一秒秒表的计时基准秒表要显示百分之一秒直接复用T0的1ms中断。中断里维护一个10ms累加计数器每10次就让百分位加1。显示格式是秒十位、秒个位、小数点、百分位十位、百分位个位比如45.67表示45秒67百分秒。static unsigned char sw_cent 0; // 百分之一秒0~99 static unsigned char sw_sec 0; // 秒0~99 static unsigned int sw_1ms_cnt 0; if (stopwatch_running) { sw_1ms_cnt; if (sw_1ms_cnt 10) { sw_1ms_cnt 0; sw_cent; if (sw_cent 100) { sw_cent 0; sw_sec; if (sw_sec 100) { sw_sec 0; // 最多99.99秒回零重新计 } } } }填入显示缓冲时要拆成单个数字与动态扫描的smg_buf对应。小数点在共阴段码里就是段码最高位置1扫描到小数点所在位时把P0的当前输出或上0x80不需要单独建一张带小数点的段码表。void Fill_Stopwatch(void) { smg_buf[0] sw_sec / 10; smg_buf[1] sw_sec % 10; smg_buf[2] sw_cent / 10; // 扫描到这一位时给P0或上0x80 smg_buf[3] sw_cent % 10; }百分秒的精度受中断本身指令开销影响。1ms中断里除了重装还要执行扫描和计数实际周期会略大于1ms秒表走出来的百分秒会有几十毫秒级误差。课设验收用手机秒表对照5秒误差在几格之内就被认可。想更准就把数码管扫描挪到主循环中断里只维护时间累加变量。4.4 秒表启动停止与复位秒表模式由长按设置键进入进入后LED_SEC常亮指示。短按设置键在启动和暂停之间切换启动时秒表开始累加暂停时变量保留方便记录中途时间。停止状态短按清零按键会清掉sw_sec和sw_cent。清零只在暂停状态生效避免计到一半误触发。这里在Proteus里有一个仿真特有的现象暂停后再次启动第一次百分位跳变可能在10ms之后而不是立刻因为主循环采样按键和中断计时各自独立。这不是程序逻辑错误是仿真环境运行节奏决定的不影响功能验收。5. Proteus与Keil联合调试从hex加载到定时误差修正5.1 单片机属性与Keil工程晶振一致加载hex前先双击Proteus里的单片机在Clock Frequency字段把仿真晶振改成12MHz。很多工程时间跑偏不是因为代码而是这里还停在默认值。Keil Target里的Xtal只影响软件调试不决定最终hex在仿真器里的运行频率。两个频率不一致时T0重装值按12MHz算仿真里机器周期却是另一个频率时间自然对不上。5.2 用虚拟示波器验证1ms中断与初值补偿把P1.0的秒闪LED先用短接跳线接到P1.1在定时器中断里翻转P1.1用Proteus虚拟示波器观察方波周期。如果方波周期比2ms偏大说明定时偏慢原因是中断现场保护和重装指令占用了机器周期。补偿方式是在初值上微调现象判断初值调整方向方波周期偏大定时偏慢TL0加1方波周期偏小定时偏快TL0减1调整后重新编译生成hex再在Proteus里替换加载观察波形变化。这个补偿步骤可以重复迭代直到示波器测出的周期落在2.00ms附近。记住只改定时器初值不要改中断里的核心逻辑否则又要重新校准。5.3 串口虚拟终端辅助观察状态变量如果想快速确认秒表百分位在累加、或者按键状态是不是按预期迁移加一个虚拟终端是最省事的办法。在Proteus里放置Virtual Terminal把单片机TXD和RXD接上然后用printf输出关键变量。需要重映射putcharvoid putchar(unsigned char ch) { SBUF ch; while (!TI); TI 0; }在调试位置调用printf(sw_cent%d state%d\r\n, (int)sw_cent, (int)state);就能在终端实时看变量变化。注意printf比较大如果单片机内存不足改成逐字节发送也有同样效果。串口发送不要放在定时器中断里中断里阻塞等TI会把1ms周期拉长发送动作放到主循环处理。这套思路和按键事件一致中断只负责标记主循环负责消费。本文还有配套的精品资源点击获取