代码整体结构
| 模块 | 作用 | 关键文件/函数 |
|---|---|---|
| 头文件 | 包含 8051 MCU 寄存器、按键、数码管驱动 | REGX52.H、Key.h、Seg.h |
| 全局变量 | 保存按键状态、计时器、时钟/闹钟时间、显示状态等 | Key_Val、Timer_1000ms、Alarm_Set 等 |
| 主循环 | while(1) { Key_Proc(); Seg_Proc(); Led_Proc(); } |
调用 3 个子程序完成输入、显示、LED 控制 |
| 中断服务 | 1 ms 定时器、按键/数码管更新,计时器 & 计数器 | Timer0Server() |
| 键盘处理 | 根据按键值切换模式、修改时钟/闹钟 | Key_Proc() |
| 显示处理 | 根据 Seg_Disp_Mode 产生 Seg_Buf 供 Seg_Proc() 显示 |
Seg_Proc() |
| LED 与闹钟 | 只要闹钟时间等于实时时间就点亮 LED | Led_Proc() |
全局变量与其意义
| 变量 | 含义 | 备注 |
|---|---|---|
Key_Val / Key_Down / Key_Old |
当前、落下沿、延迟扫描 | Key_Read() 用于返回 8‑bit 键码 |
Key_Slow_Down / Seg_Slow_Down |
降速计数器 | 在 ISR 内每 1 ms 更新,达到阈值后清零,防止连续快速按键 |
Seg_Buf[6] |
当前需要显示的 6 位十进制数 | 用于数码管扫描 |
Seg_Disp_Mode |
0‑显示时钟、1‑设置时钟、2‑设置闹钟 | Key_Proc() 与 Seg_Proc() 根据此变量切换 |
| `Clock_Disp[3] | HH:mm:ss` | 实时时钟 |
Clock_Set[3] |
与 Clock_Disp 同格式,但用于“设置时钟”模式 |
|
Alarm[3] |
设定闹钟时间 | |
Alarm_Set[3] |
临时闹钟缓存,在“设置闹钟”模式中做编辑 | |
Alarm_Flag |
控制闹钟是否启用 | |
Alarm_Enable_Flag |
当闹钟时间等于实时时间时置 1,触发 LED 开/关 | |
Led |
通过 Led_Proc() 控制 P1 总线亮度(分高低 4 位) |
|
Seg_Slow_Down, Timer_1000ms, Timer_500ms |
计时器 (1 ms / 1000 ms / 500 ms) | 用于更新 LED/闪烁、秒钟计数 |
关键逻辑
3.1 键盘处理 Key_Proc()
| 按键 | 作用 | 说明 |
|---|---|---|
| 1 | 复制 Clock_Disp 到 Clock_Set,切换到 时钟设置 |
进入编辑模式 |
| 2 | 复制 Alarm 到 Alarm_Set,切换到 闹钟设置 |
进入闹钟编辑 |
| 3 | 进一 Clock_Set_Index(或 Alarm_Set_Index) |
切换聚焦的时间段 (0‑2) |
| 4 | Alarm_Flag ^= 1 |
开/关闹钟功能 |
| 5 | 按值+1 | 对 Clock_Set 或 Alarm_Set 做递增,超过上限回 0 |
| 6 | 按值-1 | 对 Clock_Set 或 Alarm_Set 做递减,回 255 时恢复最大值 |
| 7 | 提交 | 把当前 Clock_Set / Alarm_Set 写回 Clock_Disp / Alarm 并回到时钟显示 |
| 8 | 退出 | 仅回到时钟显示(不保存任何改动) |
错误源:按 8 时并未把
Alarm_Set还原/写回,导致下次进入 2 时仍然看到上一次未保存的Alarm_Set。 解决方案见 4. 闹钟设置保存/取消。
3.2 显示处理 Seg_Proc()
-
通过
Seg_Disp_Mode判断显示哪种数据。 -
对
Clock_Disp/Clock_Set/Alarm_Set进行拆分为 10‑进制,仅显示非 “闪烁” 位时才呈现数字 (Seg_Flag用于闪烁实现)。
3.3 计时器 / Led_Proc()
-
计时器 ISR 每 1 ms 更新
Key_Slow_Down、Seg_Slow_Down、Seg_Pos。 -
Timer_1000ms负责秒钟计数,依次递增秒、分、时。 -
Timer_500ms用来闪烁 LED 或数码管。 -
Led_Proc()根据Alarm_Flag、Alarm_Enable_Flag控制 P1 与 P2_3,点亮不同风格 LED。
常见错误与对应解决方法
| 错误 / 问题 | 可能原因 | 解决方案 |
|---|---|---|
main.c(80): error C202: 'Time_Count': undefined identifier |
变量未声明、未定义或文件未加入项目 | 在一个 .c 中 volatile unsigned int Time_Count; 并在需要文件 extern volatile unsigned int Time_Count; |
warning C276: constant in condition expression |
赋值语句写成 =,而不是比较 == |
找到对应 if (Seg_Disp_Mode = 1) 并改成 if (Seg_Disp_Mode == 1) |
未解决外部符号 ALARM_SET |
只声明数组,却没有实际定义 | 在某个 .c 文件中写 unsigned char Alarm_Set[3] = {0,0,0}; 或使用 extern + 头文件 |
| 闹钟未保存 按 2 → 5/6 → 8 后仍保持修改 | 退出 8 时不把 Alarm_Set 复制回 Alarm |
(A) 在 case 8 里 for(i) Alarm_Set[i]=Alarm[i]; ;(B) 只在 case 7 提交更改,case 8 单纯退出 |
| 程序闪烁/死循环 | case 1 中缺 break,导致继续执行 case 2 |
给每个 case 添加 break; 并保证跳转逻辑正确 |
未定义 P2_3 |
未在头文件中定义位 | 在 REGX52.H 或自定义头文件中 sbit P2_3 = P2^3; |
计时器变量未 volatile |
ISR 和主程序共用变量 | 使用 volatile 防止编译器优化错误 |
数组未初始化 (Alarm[]、Alarm_Set[]) |
仅声明 unsigned char Alarm[]; 而无大小 |
在某处 unsigned char Alarm[3]={0,0,0}; 并在其它文件使用 extern |
小贴士:在 Keil / IAR 里将 Warnings 切到 “Verbose” 级别,可以快速定位
C276之类的问题。
推荐改进点
-
拆分为模块
-
timer.c / timer.h:计时器 ISR、Timer_1000ms / Timer_500ms。 -
display.c / display.h:数码管扫描、显示缓冲。 -
alarm.c / alarm.h:闹钟处理(保存/提交/取消)。
-
-
使用结构体封装时间
typedef struct { unsigned char h,m,s; } time_t; time_t Clock_Disp, Clock_Set, Alarm, Alarm_Set;可直接读写
h/m/s,简化指针/下标操作。
-
统一键盘扫描 把
Key_Read()包装成返回枚举值enum key{KEY_NONE, KEY_1, …}。 -
逻辑函数化 把
if (Seg_Disp_Mode == 1)等重复判断提炼为函数bool in_clock_setting(void)。 -
异常处理 在
Key_Proc()最后加default:里/* ignore */,避免未知按键导致意外跳转。
结论与下一步
-
通过将
=改为==、补齐break、为未定义符号加定义以及明确闹钟保存/取消逻辑,你的程序即可编译通过且行为符合预期。 -
进一步的提升可以参考
的模块化建议,尤其是将各功能拆成 .c/.h份后,代码清晰、易维护,也方便加入单元测试。
代码整体梳理与知识点概述
项目模块架构
| 模块 | 作用 | 关键文件/函数 |
|---|---|---|
| 头文件 | 包含 MCU 寄存器、按键、数码管驱动 | REGX52.H、Key.h、Seg.h |
| 全局变量 | 保存按键状态、计时器、时钟/闹钟时间等 | Key_Val、Timer_1000ms、Alarm_Set 等 |
| 主循环 | while(1) { Key_Proc(); Seg_Proc(); Led_Proc(); } |
触发键盘扫描、数码管刷新、LED 控制 |
| 中断服务 | 1 ms 定时器、键盘/数码管更新、秒钟计数 | Timer0Server() |
| 键盘处理 | 根据按键值切换模式、修改时间 | Key_Proc() |
| 显示处理 | 根据 Seg_Disp_Mode 产生 Seg_Buf 供 Seg_Proc() 显示 |
Seg_Proc() |
| LED & 闹钟 | 当闹钟时间等于实时时间点亮 LED | Led_Proc() |
全局变量 & 含义
/* 按键相关 */
unsigned char Key_Val, Key_Down, Key_Old;
unsigned char Key_Slow_Down; // 按键减速计数
/* 数码管显示缓冲 */
unsigned char Seg_Buf[6] = {10,10,10,10,10,10};
unsigned char Seg_Pos; // 当前扫描位
unsigned int Seg_Slow_Down; // 数码管减速计数
bit Seg_Flag; // 用于闪烁控制
unsigned char Seg_Point[6] = {0,1,0,1,0,1};
/* 时钟 / 闹钟 */
unsigned char Clock_Disp[3] = {23,59,55}; // HH:MM:SS
unsigned char Clock_Set[3]; // 设定时钟临时变量
unsigned char Clock_Set_Index; // 0-小时 1-分钟 2-秒
unsigned char Alarm[3] = {0,0,0}; // 已保存闹钟
unsigned char Alarm_Set[3]; // 设定闹钟临时变量
bit Alarm_Flag = 1; // 开启/关闭闹钟
bit Alarm_Enable_Flag; // 时间匹配时置 1
/* 计时器 */
unsigned int Timer_1000ms; // 1 秒计数
unsigned int Timer_500ms; // 0.5 秒计数
/* LED 控制 */
unsigned char Led; // P1 高低 4 位混合亮度
提示:使用
volatile修饰 ISR 与主程序共享的计数器(如Timer_1000ms、Key_Slow_Down),避免编译器优化导致逻辑错误。
关键业务流程
3.1 Key_Proc()(键盘扫描 & 模式切换)
| 键值 | 功能 | 说明 |
|---|---|---|
| 1 | 复制 Clock_Disp → Clock_Set,切换 时钟设置 |
进入编辑模式 |
| 2 | 复制 Alarm → Alarm_Set,切换 闹钟设置 |
进入闹钟编辑 |
| 3 | Clock_Set_Index++(0‑1‑2 循环) 仅在 Seg_Disp_Mode == 1 时生效 |
选中要修改的时间段 |
| 4 | Alarm_Flag ^= 1 |
开/关闹钟功能 |
| 5 | 上调 Clock_Set[Clock_Set_Index]++ 或 Alarm_Set[Clock_Set_Index]++ |
递增对应值 |
| 6 | 下调 Clock_Set[Clock_Set_Index]-- 或 Alarm_Set[Clock_Set_Index]-- |
递减对应值 |
| 7 | 提交 把当前编辑的时钟/闹钟复制回主变量并回到显示模式 | 保存修改 |
| 8 | 退出 仅切换回显示模式 | 不保存修改(默认行为) |
潜在 bug:
在
case 8时并未把Alarm_Set还原或写回Alarm,导致下次按 2 进入闹钟设置时仍显示上一次未保存的Alarm_Set。解决方案:在
case 8里恢复Alarm_Set,或把“保存”逻辑单独放于case 7,让case 8只负责退出。
3.2 Seg_Proc()(数码管显示生成)
-
时钟显示(
Seg_Disp_Mode == 0) 把Clock_Disp拆分为 6 位十进制数存入Seg_Buf。 -
时钟设置(
Seg_Disp_Mode == 1) 把Clock_Set拆分后显示,同时通过Seg_Flag做闪烁。 -
闹钟设置(
Seg_Disp_Mode == 2) 类似时钟设置,使用Alarm_Set。Seg_Flag同样控制闪烁,使被选中位闪烁。
闪烁实现:
Timer_500ms每 500 ms 改变Seg_Flag,刷新时根据Seg_Flag决定是否显示数字。
3.3 Led_Proc()(LED 与闹钟触发)
if (Alarm_Flag == 1) {
if (Clock_Disp matches Alarm) Alarm_Enable_Flag = 1;
if (Alarm_Enable_Flag) {
P2_3 = 0; // LED 开启
P1 = Led; // 控制亮度
} else {
P2_3 = 1;
P1 = 0xff; // LED 关
}
} else {
P2_3 = 1;
P1 = 0xff;
}
Led通过中断里Led ^= 0xf0/0x0f以 12 h/24 h 切换不同亮度模式。
3.4 Timer0Server()(1 ms 定时器 ISR)
-
更新
Key_Slow_Down,Seg_Slow_Down,Seg_Pos。 -
计数
Timer_1000ms→ 秒钟计数。 -
计数
Timer_500ms→ 用于闪烁与 LED 切换。
关键点:ISR 里所有可变全局变量 应为
volatile。
常见错误 & 解决方案
| 错误 | 诊断 | 解决 |
|---|---|---|
undefined identifier 'Time_Count' |
变量未定义或未加入项目 | 在一个 .c 定义 volatile unsigned int Time_Count; 并在需要文件 extern 声明 |
| C276: constant in condition expression | if (Seg_Disp_Mode = 1) |
改为 if (Seg_Disp_Mode == 1) |
未解决外部符号 ALARM_SET |
只声明数组,未定义 | 在某个 .c 里定义 unsigned char Alarm_Set[3] = {0,0,0}; 或使用 extern 与头文件分离 |
| 闹钟未保存 (按 2 → 5/6 → 8 后仍保留修改) | case 8 未恢复 Alarm_Set 或写回 Alarm |
在 case 8 里 for(i) Alarm_Set[i] = Alarm[i];,或在 case 7 单独处理保存 |
缺少 break; |
case 1 末尾无 break |
给每个 case 加 break;,避免跳至下一个 case |
未定义 P2_3 |
没有位定义 | 在头文件中 sbit P2_3 = P2^3; |
计时器变量未 volatile |
ISR & 主程序共享 | 为 Timer_1000ms, Timer_500ms, Key_Slow_Down, Seg_Slow_Down 加 volatile |
数组未初始化 (Alarm[], Alarm_Set[]) |
仅声明无大小 | 给 Alarm[]、Alarm_Set[] 分配大小并初始化,例如 unsigned char Alarm[3]={0,0,0}; |
调试技巧:把编译器警告等级提升到 “Verbose”,以便尽早发现类似 C276 的逻辑错误。
可视化笔记
计时器 + 闹钟项目笔记
项目结构简介
| 模块 | 作用 | 关键文件/函数 |
|---|---|---|
| 头文件 | 包含 MCU 寄存器、按键/数码管驱动 | REGX52.H、Key.h、Seg.h |
| 全局变量 | 保存按键状态、计时器、时钟/闹钟时间等 | Key_Val、Timer_1000ms、Alarm_Set… |
| 主循环 | while(1){Key_Proc();Seg_Proc();Led_Proc();} |
触发键盘扫描、数码管刷新、LED 控制 |
| 中断服务 | 1 ms 定时器 ISR,更新计数/闪烁/LED | Timer0Server() |
| 键盘处理 | 根据键码切换模式、修改时间 | Key_Proc() |
| 显示处理 | 根据 Seg_Disp_Mode 产生 Seg_Buf |
Seg_Proc() |
| LED & 闹钟 | 等钟时间匹配点亮 LED | Led_Proc() |
Tip: 只要 ISR 与主程序共享全局变量,一律在声明处添加
volatile。
关键全局变量
/* 按键相关 */
unsigned char Key_Val, Key_Down, Key_Old;
unsigned char Key_Slow_Down; /* 键盘减速计数 */
/* 数码管显示缓冲 */
unsigned char Seg_Buf[6] = {10,10,10,10,10,10};
unsigned char Seg_Pos; /* 当前扫描位 */
unsigned int Seg_Slow_Down; /* 数码管减速计数 */
bit Seg_Flag; /* 闪烁标志 */
unsigned char Seg_Point[6] = {0,1,0,1,0,1};
/* 时钟 / 闹钟 */
unsigned char Clock_Disp[3] = {23,59,55}; /* HH:MM:SS */
unsigned char Clock_Set[3];
unsigned char Clock_Set_Index; /* 0-小时 1-分钟 2-秒 */
unsigned char Alarm[3] = {0,0,0};
unsigned char Alarm_Set[3];
bit Alarm_Flag = 1; /* 开/关闹钟 */
bit Alarm_Enable_Flag; /* 时间匹配 → 1 */
/* 计时器 */
unsigned int Timer_1000ms; /* 1 s 计数 */
unsigned int Timer_500ms; /* 0.5 s 计数 */
/* LED 控制 */
unsigned char Led; /* P1 的低 4/高 4 位混合亮度 */
Reminder:
Timer_1000ms/Timer_500ms/Key_Slow_Down/Seg_Slow_Down必须声明为volatile。
业务流程(代码剖析)
3.1 Key_Proc() — 键盘扫描 & 模式切换
| 键值 | 功能 | 说明 |
|---|---|---|
1 |
复制 Clock_Disp → Clock_Set,切换为时钟设定模式 |
进入编辑 |
2 |
复制 Alarm → Alarm_Set,切换为闹钟设定模式 |
进入编辑 |
3 |
仅 Seg_Disp_Mode==1 时:Clock_Set_Index++ (0‑1‑2 循环) |
选中要修改的时间段 |
4 |
Alarm_Flag ^= 1 |
开/关闹钟 |
5 |
Clock_Set[Clock_Set_Index]++ 或 Alarm_Set[Clock_Set_Index]++ |
递增对应值 |
6 |
Clock_Set[Clock_Set_Index]-- 或 Alarm_Set[Clock_Set_Index]-- |
递减对应值 |
7 |
提交:把编辑结果写回主变量,返回显示模式 | 保存修改 |
8 |
退出:仅切回显示模式(不保存) | 默认不保存 |
Bug & Fix
case 8里 没有把Alarm_Set还原,导致下一次2进入时仍看到上一次未保存的值。 解决方案:case 8: for (int i = 0; i < 3; ++i) Alarm_Set[i] = Alarm[i]; /* 取消修改 */ Seg_Disp_Mode = 0; break;或将“保存”逻辑专门放在
case 7。
3.2 Seg_Proc() — 数码管显示生成
switch (Seg_Disp_Mode) {
case 0: /* 时钟显示 */
Seg_Buf[0] = Clock_Disp[0]/10 % 10;
Seg_Buf[1] = Clock_Disp[0] % 10;
...
break;
case 1: /* 时钟设置 */
/* 同 Clock_Set */
Seg_Buf[0+Clock_Set_Index*2] = Seg_Flag ? Clock_Set[Clock_Set_Index]/10 % 10 : 10;
Seg_Buf[1+Clock_Set_Index*2] = Seg_Flag ? Clock_Set[Clock_Set_Index] % 10 : 10;
break;
case 2: /* 闹钟设置 */
/* 同 Alarm_Set */
}
Seg_Flag 用于亮/灭(闪烁)。 闪烁实现:Timer_500ms 每 500 ms 取反 Seg_Flag,刷新时根据 Seg_Flag 决定是否显示数字。
3.3 Led_Proc() — LED 与闹钟触发
if (Alarm_Flag == 1) {
if (Clock_Disp[0]==Alarm[0] &&
Clock_Disp[1]==Alarm[1] &&
Clock_Disp[2]==Alarm[2])
Alarm_Enable_Flag = 1; /* 匹配 → 亮 */
if (Alarm_Enable_Flag) {
P2_3 = 0; /* 开灯 */
P1 = Led; /* 高低 4 位混合亮度 */
} else {
P2_3 = 1; /* 关灯 */
P1 = 0xff;
}
} else {
P2_3 = 1; /* 按闹钟关闭状态 */
P1 = 0xff;
}
LED 切换(12 h/24 h):
Timer0Server()里Led ^= 0xf0/Led ^= 0x0f。
3.4 Timer0Server() — 1 ms 定时器 ISR
/* 每 1 ms */
TL0 = 0x18; TH0 = 0xFC;
++Key_Slow_Down; if (Key_Slow_Down==10) Key_Slow_Down = 0;
++Seg_Slow_Down; if (Seg_Slow_Down==500) Seg_Slow_Down = 0;
++Seg_Pos; if (Seg_Pos==6) Seg_Pos = 0;
Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos], Seg_Point[Seg_Pos]);
/* 1 s / 秒计数 */
static unsigned int timer1k = 0;
if (++timer1k == 1000) {
timer1k = 0; Timer_1000ms++;
/* 计时钟搬位 */
}
/* 0.5 s / 闪烁/LED */
static unsigned int timer500 = 0;
if (++timer500 == 500) {
timer500 = 0; Seg_Flag ^= 1;
if (Clock_Disp[0] >= 12) Led ^= 0xf0; else Led ^= 0x0f;
}
强调:上述所有共享计数器/标志必须用
volatile。
常见错误与解决方案
| 错误 | 诊断/原因 | 解决方案 |
|---|---|---|
Time_Count 未定义 |
变量未声明/未加入项目 | 在某个 .c 里 volatile unsigned int Time_Count; 并在需要文件 extern |
| C276:constant in condition expression | 赋值写成比较 (if (Seg_Disp_Mode = 1)) |
改为 if (Seg_Disp_Mode == 1) |
| 未解决外部符号 ALARM_SET | 只声明数组(unsigned char Alarm_Set[];) |
给大小或在 .c 定义 unsigned char Alarm_Set[3] = {0,0,0}; |
闹钟未保存 (2→5/6→8) |
case 8 处未写回/恢复 Alarm_Set |
在 case 8 中恢复,或把“保存”逻辑放在 case 7 |
缺少 break; |
case 1 末尾无 break |
给每个 case 加 break |
未定义 P2_3 |
未在头文件中 sbit P2_3 = P2^3; |
添加定义 |
计时器变量未 volatile |
ISR 与主程序共享 | 在声明处加 volatile |
数组未初始化 (Alarm[]/Alarm_Set[]) |
仅声明无大小 | 给明确大小并初始化 |
| 可读性/维护 | 大段 if/else 手风琴式嵌套 |
把逻辑拆成小函数/模块 |
调试技巧
把编译器警告等级调到
-Wall -Wextra,并把 C276 设为-Werror=C276以防遗漏。在 Keil/IAR 里打开 “All Warnings” 以快速定位常量条件。
改进建议
| 方案 | 优点 | 示例(代码片段) |
|---|---|---|
| 模块化 | clock.c/h, alarm.c/h, display.c/h, timer.c/h |
把 Key_Proc() 与 Seg_Proc() 拆到独立文件 |
| 结构体包装 | typedef struct{unsigned char h,m,s;} time_t; |
对 Clock_Disp, Alarm, Alarm_Set 使用 time_t |
| 统一接口 | 把键盘扫描返回枚举,如 enum key{KEY_NONE,KEY_1,…} |
使 switch 语句更安全、可读 |
| 错误提升 | 把所有警告视为错误 (-Werror) |
编译时能立刻捕获所有潜在 bug |
| 注释 & 日志 | 在 Timer0Server() 里加 // ISR 标示 |
维护者更易定位 |
| 单元测试 | 对 Clock_Set / Alarm_Set 区块写测试 |
保证重构不破坏功能 |
更进一步
把
Seg_Flag通过传递参数给Seg_Proc(),避免全局状态把 LED 亮度与闹钟模式打包成结构体,减少
Led ^=逻辑。