单片机第三周

:one: 代码整体结构

模块 作用 关键文件/函数
头文件 包含 8051 MCU 寄存器、按键、数码管驱动 REGX52.HKey.hSeg.h
全局变量 保存按键状态、计时器、时钟/闹钟时间、显示状态等 Key_ValTimer_1000msAlarm_Set
主循环 while(1) { Key_Proc(); Seg_Proc(); Led_Proc(); } 调用 3 个子程序完成输入、显示、LED 控制
中断服务 1 ms 定时器、按键/数码管更新,计时器 & 计数器 Timer0Server()
键盘处理 根据按键值切换模式、修改时钟/闹钟 Key_Proc()
显示处理 根据 Seg_Disp_Mode 产生 Seg_BufSeg_Proc() 显示 Seg_Proc()
LED 与闹钟 只要闹钟时间等于实时时间就点亮 LED Led_Proc()

:two: 全局变量与其意义

变量 含义 备注
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/闪烁、秒钟计数

:three: 关键逻辑

3.1 键盘处理 Key_Proc()

按键 作用 说明
1 复制 Clock_DispClock_Set,切换到 时钟设置 进入编辑模式
2 复制 AlarmAlarm_Set,切换到 闹钟设置 进入闹钟编辑
3 进一 Clock_Set_Index(或 Alarm_Set_Index 切换聚焦的时间段 (0‑2)
4 Alarm_Flag ^= 1 开/关闹钟功能
5 按值+1 Clock_SetAlarm_Set 做递增,超过上限回 0
6 按值-1 Clock_SetAlarm_Set 做递减,回 255 时恢复最大值
7 提交 把当前 Clock_Set / Alarm_Set 写回 Clock_Disp / Alarm 并回到时钟显示
8 退出 仅回到时钟显示(不保存任何改动)

:warning: 错误源:按 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_DownSeg_Slow_DownSeg_Pos

  • Timer_1000ms 负责秒钟计数,依次递增秒、分、时。

  • Timer_500ms 用来闪烁 LED 或数码管。

  • Led_Proc() 根据 Alarm_FlagAlarm_Enable_Flag 控制 P1 与 P2_3,点亮不同风格 LED。


:four: 常见错误与对应解决方法

错误 / 问题 可能原因 解决方案
main.c(80): error C202: 'Time_Count': undefined identifier 变量未声明、未定义或文件未加入项目 在一个 .cvolatile 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 8for(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 之类的问题。


:five: 推荐改进点

  1. 拆分为模块

    • timer.c / timer.h:计时器 ISR、Timer_1000ms / Timer_500ms

    • display.c / display.h:数码管扫描、显示缓冲。

    • alarm.c / alarm.h:闹钟处理(保存/提交/取消)。

  2. 使用结构体封装时间

    typedef struct { unsigned char h,m,s; } time_t;
    time_t Clock_Disp, Clock_Set, Alarm, Alarm_Set;
    

    可直接读写

    h/m/s
    

    ,简化指针/下标操作。

  3. 统一键盘扫描Key_Read() 包装成返回枚举值 enum key{KEY_NONE, KEY_1, …}

  4. 逻辑函数化if (Seg_Disp_Mode == 1) 等重复判断提炼为函数 bool in_clock_setting(void)

  5. 异常处理Key_Proc() 最后加 default:/* ignore */,避免未知按键导致意外跳转。


:six: 结论与下一步

  • 通过将 = 改为 ==、补齐 break、为未定义符号加定义以及明确闹钟保存/取消逻辑,你的程序即可编译通过且行为符合预期。

  • 进一步的提升可以参考 :five: 的模块化建议,尤其是将各功能拆成 .c/.h 份后,代码清晰、易维护,也方便加入单元测试。

:graduation_cap: 代码整体梳理与知识点概述


:one: 项目模块架构

模块 作用 关键文件/函数
头文件 包含 MCU 寄存器、按键、数码管驱动 REGX52.HKey.hSeg.h
全局变量 保存按键状态、计时器、时钟/闹钟时间等 Key_ValTimer_1000msAlarm_Set
主循环 while(1) { Key_Proc(); Seg_Proc(); Led_Proc(); } 触发键盘扫描、数码管刷新、LED 控制
中断服务 1 ms 定时器、键盘/数码管更新、秒钟计数 Timer0Server()
键盘处理 根据按键值切换模式、修改时间 Key_Proc()
显示处理 根据 Seg_Disp_Mode 产生 Seg_BufSeg_Proc() 显示 Seg_Proc()
LED & 闹钟 当闹钟时间等于实时时间点亮 LED Led_Proc()

:two: 全局变量 & 含义

/* 按键相关 */
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_1000msKey_Slow_Down),避免编译器优化导致逻辑错误。


:three: 关键业务流程

3.1 Key_Proc()(键盘扫描 & 模式切换)

键值 功能 说明
1 复制 Clock_DispClock_Set,切换 时钟设置 进入编辑模式
2 复制 AlarmAlarm_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 退出 仅切换回显示模式 不保存修改(默认行为)

:warning: 潜在 bug

  • case 8 时并未把 Alarm_Set 还原或写回 Alarm,导致下次按 2 进入闹钟设置时仍显示上一次未保存的 Alarm_Set

  • 解决方案:在 case 8 里恢复 Alarm_Set,或把“保存”逻辑单独放于 case 7,让 case 8 只负责退出。

3.2 Seg_Proc()(数码管显示生成)

  1. 时钟显示Seg_Disp_Mode == 0) 把 Clock_Disp 拆分为 6 位十进制数存入 Seg_Buf

  2. 时钟设置Seg_Disp_Mode == 1) 把 Clock_Set 拆分后显示,同时通过 Seg_Flag 做闪烁。

  3. 闹钟设置Seg_Disp_Mode == 2) 类似时钟设置,使用 Alarm_SetSeg_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


:four: 常见错误 & 解决方案

错误 诊断 解决
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 8for(i) Alarm_Set[i] = Alarm[i];,或在 case 7 单独处理保存
缺少 break; case 1 末尾无 break 给每个 casebreak;,避免跳至下一个 case
未定义 P2_3 没有位定义 在头文件中 sbit P2_3 = P2^3;
计时器变量未 volatile ISR & 主程序共享 Timer_1000ms, Timer_500ms, Key_Slow_Down, Seg_Slow_Downvolatile
数组未初始化 (Alarm[], Alarm_Set[]) 仅声明无大小 Alarm[]Alarm_Set[] 分配大小并初始化,例如 unsigned char Alarm[3]={0,0,0};

调试技巧:把编译器警告等级提升到 “Verbose”,以便尽早发现类似 C276 的逻辑错误。


:five: 可视化笔记

:alarm_clock: 计时器 + 闹钟项目笔记


:one: 项目结构简介

模块 作用 关键文件/函数
头文件 包含 MCU 寄存器、按键/数码管驱动 REGX52.HKey.hSeg.h
全局变量 保存按键状态、计时器、时钟/闹钟时间等 Key_ValTimer_1000msAlarm_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


:two: 关键全局变量

/* 按键相关 */
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


:three: 业务流程(代码剖析)


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


:four: 常见错误与解决方案

错误 诊断/原因 解决方案
Time_Count 未定义 变量未声明/未加入项目 在某个 .cvolatile 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 给每个 casebreak
未定义 P2_3 未在头文件中 sbit P2_3 = P2^3; 添加定义
计时器变量未 volatile ISR 与主程序共享 在声明处加 volatile
数组未初始化 (Alarm[]/Alarm_Set[]) 仅声明无大小 给明确大小并初始化
可读性/维护 大段 if/else 手风琴式嵌套 把逻辑拆成小函数/模块

调试技巧

  1. 把编译器警告等级调到 -Wall -Wextra,并把 C276 设为 -Werror=C276 以防遗漏。

  2. 在 Keil/IAR 里打开 “All Warnings” 以快速定位常量条件。


:five: 改进建议

方案 优点 示例(代码片段)
模块化 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 ^= 逻辑。