蓝桥杯第八届省赛单片机设计完全入门(零基础保姆级教程)

基于IAP15F2K61S2的电子钟项目实战


教程说明

本教程面向零基础想要学习IAP15F2K61S2单片机、准备参加蓝桥杯单片机比赛的同学。我们将通过第八届蓝桥杯省赛真题——电子钟设计,带你一步步理解单片机开发的核心知识。

学习目标:通过本教程,你将学会如何用CT107D竞赛板完成一个功能完整的电子钟系统,掌握数码管显示、按键检测、实时时钟、温度采集等核心技能。


目录


一、赛题要求解读

题目:基于单片机的电子钟程序设计与调试(70分)

1.1 基本要求
要求项具体内容
开发板使用CT107D单片机竞赛板
MCU型号IAP15F2K61S2(51内核)
开发环境Keil uVision(C51编译器)
工程命名以准考证号命名
1.2 功能要求
初始化功能
✓ 关闭蜂鸣器、继电器等无关外设
✓ 初始时钟:235950秒
✓ 初始闹钟:000000
显示功能
时间显示格式:12-00-02
()()()
温度显示格式:     24C
(熄灭)(温度值)(单位)
按键功能
按键功能说明
S7时钟设置循环选择时/分/秒,第4次按下保存退出
S6闹钟设置循环选择时/分/秒,第4次按下保存退出
S5当前选中单元+1(循环)
S4当前选中单元-1(循环)
在时间显示页按住显示温度
闹钟提示功能
✓ 时间匹配时触发闹钟
✓ L1 LED以0.2秒间隔闪烁
✓ 持续5秒后自动关闭
✓ 按任意键可提前关闭

二、硬件系统介绍

核心硬件模块

2.1 主控芯片:IAP15F2K61S2
┌─────────────────────────────────────┐
│   IAP15F2K61S2 技术参数              │
├─────────────────────────────────────┤
│ • 内核:51单片机兼容                 │
│ • 主频:最高35MHz(本项目12MHz)     │
│ • ROM:61KB Flash                    │
│ • RAM:2KB                           │
│ • 定时器:4个(使用Timer1)          │
│ • I/O口:36个                        │
│ • 工作电压:3.3V-5.5V                │
└─────────────────────────────────────┘
2.2 输出设备

8位LED数码管

  • 型号:共阳数码管
  • 显示方式:动态扫描
  • 刷新频率:约125Hz(每位1ms)
  • 用途:显示时间、温度

8个LED指示灯

  • L1:闹钟提示
  • L2-L8:保留(可扩展)
2.3 输入设备

独立按键

  • S4-S7:4个独立按键
  • 接地方式:按下接地,松开高电平
  • 需要软件消抖处理
2.4 外围芯片

⏰ DS1302实时时钟芯片

功能:提供准确的时间基准
特点:
• 内置晶振(32.768kHz)
• 掉电保持时间(配合纽扣电池)
• SPI通信接口
• 日历功能(年月日时分秒星期)

️ DS18B20温度传感器

功能:测量环境温度
特点:
• 单总线通信
• 测量范围:-55~+125℃
• 精度:±0.5℃
• 转换时间:最多750ms

三、核心知识点预习

必备C语言基础

3.1 数组的使用
// 数组定义
unsigned char ucRtc[3] = {23, 59, 50};
// 数组访问
ucRtc[0] = 12;  // 修改"时"
ucRtc[1] = 30;  // 修改"分"
ucRtc[2] = 45;  // 修改"秒"
// 实际应用:存储时分秒
// ucRtc[0] → 时
// ucRtc[1] → 分
// ucRtc[2] → 秒

为什么用数组?

  • 相关数据统一管理
  • 便于循环操作
  • 方便作为参数传递
3.2 取模运算(%)
// 取模运算:获取余数
23 / 10 = 2  // 商(十位)
23 % 10 = 3  // 余数(个位)
// 应用1:拆分数字
int hour = 23;
int ten = hour / 10;   // 2
int one = hour % 10;   // 3
// 数码管显示:2 3
// 应用2:循环计数
int count = 0;
count = (++count) % 24;  // 0→1→2→...→23→0
3.3 三元运算符
// 格式:条件 ? 真值 : 假值
int a = 10;
int b = (a > 5) ? 1 : 0;  // b = 1
// 应用:闪烁控制
Seg_Buf[0] = (Light_Mode) ? value : 10;
// 如果Light_Mode为1,显示value
// 如果Light_Mode为0,显示10(熄灭)
3.4 位运算
// 异或(^):相同为0,不同为1
0 ^ 1 = 1
1 ^ 1 = 0
// 应用:状态翻转
led_state = 0;
led_state ^= 1;  // 变成1
led_state ^= 1;  // 变成0
// 按位与(&):都为1才为1
0xBF = 1011 1111
AUXR &= 0xBF;  // 清零第6位
3.5 结构体与函数指针
// 结构体:打包相关数据
typedef struct {
void (*task_func)(void);  // 函数指针
unsigned long rate_ms;     // 执行周期
unsigned long last_ms;     // 上次执行时间
} task_t;
// 函数指针:指向函数的指针
void Led_Proc(void) {
// LED处理代码
}
task_t task = {Led_Proc, 10, 0};
task.task_func();  // 等价于 Led_Proc();

四、代码详解:初始化篇

题目要求 3.1:初始化

1)关闭蜂鸣器、继电器等无关外设;
2)设备初始化时钟为 23 时 59 分 50 秒,闹钟提醒时间 0 时 0 分 0 秒。


4.1 全局变量声明

/* ========== 系统变量 ========== */
idata unsigned long int uwTick = 0;  // 系统计时器(毫秒)
/* ========== 按键变量 ========== */
idata unsigned char Key_Val, Key_Old, Key_Down, Key_Up;
/* ========== LED变量 ========== */
pdata unsigned char ucLed[8] = {0, 0, 0, 0, 0, 0, 0, 0};
/* ========== 数码管变量 ========== */
idata unsigned char Seg_Pos;
pdata unsigned char Seg_Buf[8] = {10, 10, 10, 10, 10, 10, 10, 10};
/* ========== 时钟相关变量 ========== */
idata unsigned char ucRtc[3] = {23, 59, 50};      // 当前时间
idata unsigned char ucAlarm[3] = {0, 0, 0};        // 闹钟时间
idata unsigned char ucRtc_ctrl[3] = {0, 0, 0};     // 时间设置缓冲
idata unsigned char ucAlarm_ctrl[3] = {0, 0, 0};   // 闹钟设置缓冲
/* ========== 状态控制变量 ========== */
idata unsigned char Seg_Show_Mode = 0;     // 显示页面
idata unsigned char Change_Time_Mode = 0;  // 时间调整模式
idata unsigned char Change_Alarm_Mode = 0; // 闹钟调整模式
idata unsigned char temperature_value = 0; // 温度值
/* ========== 定时计数变量 ========== */
idata unsigned int time_500ms = 0;   // 500ms计数
idata unsigned char time_200ms = 0;  // 200ms计数
idata unsigned int time_5s = 0;      // 5s计数
/* ========== 标志位 ========== */
idata bit Setting_Mode = 0;      // 设置模式标志
idata bit Alarming = 0;          // 闹钟触发标志
idata bit Light_Seg_Mode = 0;    // 数码管闪烁标志
idata bit Light_Led_Mode = 0;    // LED闪烁标志

变量说明表

类别变量名数据类型作用
系统uwTickunsigned long系统运行时间(ms)
按键Key_Downunsigned char按键按下标志
Key_Upunsigned char按键松开标志
时间ucRtc[3]unsigned char数组当前时间[时,分,秒]
ucAlarm[3]unsigned char数组闹钟时间[时,分,秒]
ucRtc_ctrl[3]unsigned char数组时间设置临时变量
显示Seg_Show_Modeunsigned char0=时间 1=设时间 2=设闹钟 3=温度
Seg_Buf[8]unsigned char数组数码管显示缓冲区
标志Alarmingbit闹钟是否正在响
Setting_Modebit是否处于设置状态

4.2 主函数初始化流程

void main()
{
System_Init();       // 第1步:系统初始化
rd_temperature();    // 第2步:启动温度传感器
Delay750ms();        // 第3步:等待传感器稳定
Set_Rtc(ucRtc);      // 第4步:设置初始时间
Scheduler_Init();    // 第5步:初始化任务调度器
Timer1Init();        // 第6步:启动定时器
while (1)            // 第7步:进入主循环
{
Scheduler_Run(); // 执行任务调度
}
}

初始化流程图

开机上电
↓
System_Init()
关闭蜂鸣器/继电器
↓
rd_temperature()
启动DS18B20传感器
↓
Delay750ms()
等待传感器稳定
↓
Set_Rtc(ucRtc)
写入初始时间23:59:50Scheduler_Init()
计算任务数量
↓
Timer1Init()
启动1ms定时器
↓
while(1) 主循环
不断执行Scheduler_Run()

关键代码解析

① System_Init() - 系统初始化
void System_Init()
{
P0 = 0xFF;  // 关闭所有P0口设备
P2 = 0xFF;  // 关闭所有P2口设备
// 这样蜂鸣器、继电器等无关外设都被关闭
}
② rd_temperature() - 温度传感器启动
unsigned char rd_temperature()
{
// DS18B20的读取需要先发送转换命令
// 然后等待750ms转换完成
// 这里只是启动转换
}

为什么要延时750ms?

  • DS18B20完成一次温度转换需要最多750毫秒
  • 如果不等待,第一次读取的温度可能不准确
③ Set_Rtc(ucRtc) - 写入初始时间
void Set_Rtc(unsigned char *p)
{
// 将ucRtc数组的值写入DS1302芯片
// p[0] = 23 → 写入"时"寄存器
// p[1] = 59 → 写入"分"寄存器
// p[2] = 50 → 写入"秒"寄存器
}

五、代码详解:显示功能

题目要求 3.2:显示功能

  1. 时间显示格式:12-00-02
  2. 温度显示格式: 21C

5.1 数码管显示原理

动态扫描技术
8位数码管 → 共用段码线 → 独立位选线
原理:快速轮流点亮每一位
速度:每位1ms,扫描一轮8ms
效果:视觉暂留,看起来同时亮
位01234567
┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐
│2│ │3│ │-│ │4│ │5│ │-│ │3│ │0│
└─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘
↑
当前点亮
数码管缓冲区
pdata unsigned char Seg_Buf[8] = {10, 10, 10, 10, 10, 10, 10, 10};
// Seg_Buf存储要显示的内容
// 数值  →  显示字符
// 0-9   →  数字0-9
// 10    →  熄灭(不显示)
// 11    →  显示"-"
// 12    →  显示"C"

5.2 时间显示处理

/* 数码管处理函数 */
void Seg_Proc()
{
switch (Seg_Show_Mode)
{
case 0:  // ===== 时间显示页面 =====
Seg_Buf[0] = ucRtc[0] / 10;  // 时的十位
Seg_Buf[1] = ucRtc[0] % 10;  // 时的个位
Seg_Buf[2] = 11;              // 分隔符"-"
Seg_Buf[3] = ucRtc[1] / 10;  // 分的十位
Seg_Buf[4] = ucRtc[1] % 10;  // 分的个位
Seg_Buf[5] = 11;              // 分隔符"-"
Seg_Buf[6] = ucRtc[2] / 10;  // 秒的十位
Seg_Buf[7] = ucRtc[2] % 10;  // 秒的个位
break;
// ... 其他模式
}
}

实战案例:显示 23:45:30

// 假设当前时间
ucRtc[0] = 23;  // 时
ucRtc[1] = 45;  // 分
ucRtc[2] = 30;  // 秒
// 处理过程
Seg_Buf[0] = 23 / 10 = 2;   // 显示"2"
Seg_Buf[1] = 23 % 10 = 3;   // 显示"3"
Seg_Buf[2] = 11;            // 显示"-"
Seg_Buf[3] = 45 / 10 = 4;   // 显示"4"
Seg_Buf[4] = 45 % 10 = 5;   // 显示"5"
Seg_Buf[5] = 11;            // 显示"-"
Seg_Buf[6] = 30 / 10 = 3;   // 显示"3"
Seg_Buf[7] = 30 % 10 = 0;   // 显示"0"
// 最终数码管显示:23-45-30

数学知识:整数除法与取模

// 除法运算(/)
23 / 10 = 2    // 舍去小数部分
45 / 10 = 4
30 / 10 = 3
// 取模运算(%)
23 % 10 = 3    // 得到余数
45 % 10 = 5
30 % 10 = 0
// 应用:拆分任意两位数
十位 = 数字 / 10
个位 = 数字 % 10

5.3 温度显示处理

case 3:  // ===== 温度显示页面 =====
Seg_Buf[0] = 10;                              // 熄灭
Seg_Buf[1] = 10;                              // 熄灭
Seg_Buf[2] = 10;                              // 熄灭
Seg_Buf[3] = 10;                              // 熄灭
Seg_Buf[4] = 10;                              // 熄灭
Seg_Buf[5] = temperature_value / 10 % 10;     // 温度十位
Seg_Buf[6] = temperature_value % 10;          // 温度个位
Seg_Buf[7] = 12;                              // 显示"C"
break;

实战案例:显示温度 24℃

// 假设温度传感器读取值
temperature_value = 24;
// 处理过程
Seg_Buf[0] = 10;            // 熄灭
Seg_Buf[1] = 10;            // 熄灭
Seg_Buf[2] = 10;            // 熄灭
Seg_Buf[3] = 10;            // 熄灭
Seg_Buf[4] = 10;            // 熄灭
Seg_Buf[5] = 24 / 10 = 2;   // 显示"2"
Seg_Buf[6] = 24 % 10 = 4;   // 显示"4"
Seg_Buf[7] = 12;            // 显示"C"
// 最终数码管显示:           24    C
//                (5个空位)(温度)(单位)

为什么温度要右对齐显示?

错误显示: 24C      (左对齐,不美观)
正确显示:      24C (右对齐,符合习惯)

5.4 闪烁效果实现

case 1:  // ===== 时间设置页面 =====
if (Change_Time_Mode == 0)  // 调整"时"
{
// 三元运算符:条件 ? 真值 : 假值
Seg_Buf[0] = (Light_Seg_Mode) ? ucRtc_ctrl[0] / 10 : 10;
Seg_Buf[1] = (Light_Seg_Mode) ? ucRtc_ctrl[0] % 10 : 10;
Seg_Buf[2] = 11;              // "-"始终显示
Seg_Buf[3] = ucRtc_ctrl[1] / 10;  // "分"始终显示
Seg_Buf[4] = ucRtc_ctrl[1] % 10;
Seg_Buf[5] = 11;
Seg_Buf[6] = ucRtc_ctrl[2] / 10;  // "秒"始终显示
Seg_Buf[7] = ucRtc_ctrl[2] % 10;
}

闪烁原理动画

Light_Seg_Mode = 1  →  显示数字
0.5秒后
Light_Seg_Mode = 0  →  显示10(熄灭)
0.5秒后
Light_Seg_Mode = 1  →  显示数字
...循环
用户看到效果:
12-34-56  (0.5)
-34-56  (0.5,时位熄灭)
12-34-56  (0.5)
-34-56  (0.5)

六、代码详解:按键处理

题目要求 3.3:按键功能

S7=时钟设置 | S6=闹钟设置 | S5=加 | S4=减/温度


6.1 按键检测原理

按键消抖技术
/* 按键处理函数(每10ms调用一次) */
void Key_Proc()
{
Key_Val = Key_Read();                      // 读取当前按键状态
Key_Down = Key_Val & (Key_Val ^ Key_Old);  // 检测按下瞬间
Key_Up = ~Key_Val & (Key_Val ^ Key_Old);   // 检测松开瞬间
Key_Old = Key_Val;                         // 保存本次状态
// ... 后续处理
}

按键消抖原理

机械按键的抖动问题:
理想状态:  ┌────────
│
─────────┘
实际状态:  ┌─┐┌──┐┌───
│ ││  ││
─────────┘ └┘  └┘
↑抖动区域
解决方案:每10ms检测一次,忽略中间抖动

为什么需要 Key_Old?

// 第1次检测:按键未按下
Key_Old = 0;
Key_Val = 0;
Key_Down = 0;  // 没有变化
// 第2次检测:按键按下
Key_Old = 0;  // 上次的值
Key_Val = 7;  // 当前的值
Key_Down = 7; // 检测到按下!
// 第3次检测:按键保持按下
Key_Old = 7;
Key_Val = 7;
Key_Down = 0;  // 没有变化,不重复触发

6.2 S7按键:时钟设置

case 0:  // ===== 时间显示页面 =====
switch (Key_Down)
{
case 7:  // 按下S7键
Seg_Show_Mode = 1;           // 切换到时间设置页面
ucRtc_ctrl[0] = ucRtc[0];    // 复制当前时间到缓冲区
ucRtc_ctrl[1] = ucRtc[1];    // (作为设置的初始值)
ucRtc_ctrl[2] = ucRtc[2];
break;
}
break;
case 1:  // ===== 时间设置页面 =====
switch (Key_Down)
{
case 7:  // 再次按下S7键
if (Change_Time_Mode == 2)  // 如果已经调整到"秒"
{
Seg_Show_Mode = 0;        // 返回时间显示
Change_Time_Mode = 0;     // 重置调整模式
Set_Rtc(ucRtc_ctrl);      // 将设置值写入DS1302
}
else
Change_Time_Mode++;       // 切换到下一项(时→分→秒)
break;
}

S7按键操作流程

【时间显示页面】23-45-30
↓ 按S7
【时间设置页面】23-45-30 (时位闪烁,Change_Time_Mode=0)
↓ 按S5/S4调整"时"
【时间设置页面】12-45-30 (调整为12)
↓ 按S7
【时间设置页面】12-45-30 (分位闪烁,Change_Time_Mode=1)
↓ 按S5/S4调整"分"
【时间设置页面】12-30-30 (调整为30)
↓ 按S7
【时间设置页面】12-30-30 (秒位闪烁,Change_Time_Mode=2)
↓ 按S5/S4调整"秒"
【时间设置页面】12-30-00 (调整为00)
↓ 按S7 (4,保存)
Set_Rtc(ucRtc_ctrl) → 写入DS1302芯片
↓
【时间显示页面】12-30-00 (新时间开始走)

为什么要用缓冲区 ucRtc_ctrl?

// 不使用缓冲区的问题:
ucRtc[0] = 23;  // 当前显示的时间
// 用户按S5,时间变成24(错误!)
// 用户想取消,但时间已经被修改了!
// 使用缓冲区的好处:
ucRtc[0] = 23;         // 真实时间,继续运行
ucRtc_ctrl[0] = 23;    // 缓冲区,随便修改
// 用户调整缓冲区的值,不影响真实时间
// 只有按下"确认",才将缓冲区写入真实时间

6.3 S5按键:加(增加数值)

case 1:  // ===== 时间设置页面 =====
switch (Key_Down)
{
case 5:  // 按下S5键
if (Change_Time_Mode == 0)
// 调整小时:0→1→2→...→23→0(循环)
ucRtc_ctrl[Change_Time_Mode] = (++ucRtc_ctrl[Change_Time_Mode]) % 24;
else
// 调整分秒:0→1→2→...→59→0(循环)
ucRtc_ctrl[Change_Time_Mode] = (++ucRtc_ctrl[Change_Time_Mode]) % 60;
break;
}

循环加法实现原理

// 小时循环(0-23)
ucRtc_ctrl[0] = (++ucRtc_ctrl[0]) % 24;
实例:
ucRtc_ctrl[0] = 22
↓ 按一次S5
++ucRtc_ctrl[0] = 23
23 % 24 = 23  ✓
ucRtc_ctrl[0] = 23
↓ 再按一次S5
++ucRtc_ctrl[0] = 24
24 % 24 = 0(循环到0)

取模运算对照表

当前值+1后%24结果说明
011正常递增
101111正常递增
222323正常递增
23240循环到0

分钟/秒钟同理(0-59循环)

// 分秒循环(0-59)
ucRtc_ctrl[1] = (++ucRtc_ctrl[1]) % 60;
实例:
ucRtc_ctrl[1] = 58
↓ 按一次S5
++ucRtc_ctrl[1] = 59
59 % 60 = 59  ✓
ucRtc_ctrl[1] = 59
↓ 再按一次S5
++ucRtc_ctrl[1] = 60
60 % 60 = 0(循环到0)

6.4 S4按键:减(减少数值)

case 1:  // ===== 时间设置页面 =====
switch (Key_Down)
{
case 4:  // 按下S4键
if (Change_Time_Mode == 0)
// 小时:23→22→...→1→0→23(循环)
ucRtc_ctrl[Change_Time_Mode] =
(ucRtc_ctrl[Change_Time_Mode] == 0) ? 23 : --ucRtc_ctrl[Change_Time_Mode];
else
// 分秒:59→58→...→1→0→59(循环)
ucRtc_ctrl[Change_Time_Mode] =
(ucRtc_ctrl[Change_Time_Mode] == 0) ? 59 : --ucRtc_ctrl[Change_Time_Mode];
break;
}

❓ 为什么减法不能用取模?

// 错误的写法:
ucRtc_ctrl[0] = (--ucRtc_ctrl[0]) % 24;
问题分析:
ucRtc_ctrl[0] = 0  (unsigned char类型)
↓ 执行 --ucRtc_ctrl[0]
--ucRtc_ctrl[0] = 255  (下溢,变成最大值)
255 % 24 = 15(我们想要23)

✅ 正确的写法:三元运算符

ucRtc_ctrl[0] = (ucRtc_ctrl[0] == 0) ? 23 : --ucRtc_ctrl[0];
分解理解:
if (ucRtc_ctrl[0] == 0)
ucRtc_ctrl[0] = 23;  // 如果是0,直接赋值23
else
ucRtc_ctrl[0]--;     // 否则减1
实例:
ucRtc_ctrl[0] = 1
↓ 按一次S4
不等于0,执行减1
ucRtc_ctrl[0] = 0  ✓
ucRtc_ctrl[0] = 0
↓ 再按一次S4
等于0,赋值23
ucRtc_ctrl[0] = 23

6.5 S4按键:显示温度(特殊功能)

case 0:  // ===== 时间显示页面 =====
switch (Key_Down)
{
case 4:  // 按下S4键
Seg_Show_Mode = 3;  // 切换到温度显示页面
break;
}
break;
case 3:  // ===== 温度显示页面 =====
if (Key_Up == 4)        // 松开S4键
Seg_Show_Mode = 0;  // 返回时间显示页面
break;

S4按键操作演示

【时间显示】23-45-30
↓ 按住S4 (Key_Down=4)
Seg_Show_Mode = 3
【温度显示】     82C
↓ 保持按住S4
【温度显示】     82C (继续显示)
↓ 松开S4 (Key_Up=4)
Seg_Show_Mode = 0
【时间显示】23-45-30

为什么用 Key_Up 而不是 Key_Down?

// 如果用 Key_Down:
case 3:
if (Key_Down == 4)  // ✗ 错误
Seg_Show_Mode = 0;
问题:
按下S4 → Seg_Show_Mode=3 (进入温度页)
Key_Down=4 立即触发 → Seg_Show_Mode=0 (马上退出)
结果:看不到温度,一闪而过!
// 正确用 Key_Up:
case 3:
if (Key_Up == 4)  // ✓ 正确
Seg_Show_Mode = 0;
效果:
按下S4 → 进入温度页
保持按住 → 持续显示温度
松开S4 → 返回时间页

6.6 S6按键:闹钟设置

case 0:  // ===== 时间显示页面 =====
switch (Key_Down)
{
case 6:  // 按下S6键
Seg_Show_Mode = 2;              // 切换到闹钟设置页面
ucAlarm_ctrl[0] = ucAlarm[0];   // 复制当前闹钟到缓冲区
ucAlarm_ctrl[1] = ucAlarm[1];
ucAlarm_ctrl[2] = ucAlarm[2];
break;
}
break;
case 2:  // ===== 闹钟设置页面 =====
switch (Key_Down)
{
case 6:  // 再次按下S6键
if (Change_Alarm_Mode == 2)  // 如果已经调整到"秒"
{
Seg_Show_Mode = 0;         // 返回时间显示
Change_Alarm_Mode = 0;     // 重置调整模式
ucAlarm[0] = ucAlarm_ctrl[0];  // 保存闹钟设置
ucAlarm[1] = ucAlarm_ctrl[1];  // (注意:不写入DS1302)
ucAlarm[2] = ucAlarm_ctrl[2];
}
else
Change_Alarm_Mode++;       // 切换到下一项
break;
case 5:  // S5加
if (Change_Alarm_Mode == 0)
ucAlarm_ctrl[Change_Alarm_Mode] = (++ucAlarm_ctrl[Change_Alarm_Mode]) % 24;
else
ucAlarm_ctrl[Change_Alarm_Mode] = (++ucAlarm_ctrl[Change_Alarm_Mode]) % 60;
break;
case 4:  // S4减
if (Change_Alarm_Mode == 0)
ucAlarm_ctrl[Change_Alarm_Mode] =
(ucAlarm_ctrl[Change_Alarm_Mode] == 0) ? 23 : --ucAlarm_ctrl[Change_Alarm_Mode];
else
ucAlarm_ctrl[Change_Alarm_Mode] =
(ucAlarm_ctrl[Change_Alarm_Mode] == 0) ? 59 : --ucAlarm_ctrl[Change_Alarm_Mode];
break;
}

闹钟设置 vs 时间设置 对比

项目时间设置闹钟设置
按键S7S6
页面Seg_Show_Mode=1Seg_Show_Mode=2
模式Change_Time_ModeChange_Alarm_Mode
缓冲ucRtc_ctrlucAlarm_ctrl
保存Set_Rtc()写入DS1302直接赋值给ucAlarm
原因时间需要芯片计时闹钟只需内存比较

6.7 按键关闭闹钟

if (Alarming)  // 如果闹钟正在响
{
if (Key_Down != 0)  // 按下任意键
Alarming = 0;   // 关闭闹钟
}

判断"任意键"的技巧

Key_Down != 0  // 不等于0,说明有键按下
为什么?
Key_Down = 0  → 没有按键
Key_Down = 4  → 按下S4
Key_Down = 5  → 按下S5
Key_Down = 6  → 按下S6
Key_Down = 7  → 按下S7
只要 Key_Down != 0,说明某个键被按下

七、代码详解:闹钟功能

题目要求 3.4:闹钟提示功能

1)指示灯L1以0.2秒为间隔闪烁,持续5秒钟;
2)闹钟提示状态下,按下任意按键,关闭闪烁提示功能。


7.1 时间获取与闹钟判断

/* 时间获取函数(每200ms调用一次) */
void Get_Time()
{
Read_Rtc(ucRtc);  // 从DS1302读取当前时间
// 判断是否到达闹钟时间
if ((ucRtc[0] == ucAlarm[0]) &&   // 时相同
(ucRtc[1] == ucAlarm[1]) &&   // 分相同
(ucRtc[2] == ucAlarm[2]))     // 秒相同
{
Alarming = 1;  // 触发闹钟
}
}

闹钟触发示例

设置闹钟:06:30:00
ucAlarm[0] = 6;
ucAlarm[1] = 30;
ucAlarm[2] = 0;
时间推移:
┌──────────┬──────────┬──────────┬────────┐
│ 当前时间 │ 时匹配? │ 分匹配? │ 秒匹配?│ 结果
├──────────┼──────────┼──────────┼────────┤
│ 06:29:58 │    ✓     │    ✗     │   ✗    │ 不触发
│ 06:29:59 │    ✓     │    ✗     │   ✗    │ 不触发
│ 06:30:00 │    ✓     │    ✓     │   ✓    │ ✅触发!
│ 06:30:01 │    ✓     │    ✓     │   ✗    │ 不触发
└──────────┴──────────┴──────────┴────────┘
说明:闹钟只在精确匹配的那一秒触发

为什么要每200ms读取一次时间?

如果每秒读取:
可能错过闹钟触发时刻
因为程序可能正在执行其他任务
如果每1ms读取:
太频繁,浪费CPU资源
DS1302通信速度有限
200ms是经验值:
✓ 足够快,不会错过秒级变化
✓ 不会过度占用CPU
✓ 给其他任务留出时间

7.2 LED闪烁控制

/* LED处理函数(每1ms调用一次) */
void Led_Proc()
{
Setting_Mode = (Seg_Show_Mode == 1) || (Seg_Show_Mode == 2);
ucLed[0] = Light_Led_Mode;  // L1由闪烁标志控制
Led_Disp(ucLed);            // 更新LED硬件显示
}

LED数组说明

pdata unsigned char ucLed[8] = {0, 0, 0, 0, 0, 0, 0, 0};
//                              L1 L2 L3 L4 L5 L6 L7 L8
// 0 = 熄灭
// 1 = 点亮
ucLed[0] = 1;  // 点亮L1
ucLed[7] = 1;  // 点亮L8

7.3 定时器中断:闪烁核心逻辑

/* 定时器中断(每1ms执行一次) */
void Timer1Isr(void) interrupt 3
{
uwTick++;  // 系统计时+1ms
// ... 数码管扫描代码 ...
if (Alarming)  // 如果闹钟触发
{
// ===== 200ms计时器 =====
if (++time_200ms == 200)  // 每200ms执行
{
Light_Led_Mode ^= 1;  // LED状态翻转
time_200ms = 0;       // 重置计时器
}
// ===== 5秒计时器 =====
if (++time_5s == 5000)    // 累计5000ms
{
Light_Led_Mode = 0;   // 关闭LED
time_200ms = 0;       // 清零所有计时器
time_5s = 0;
Alarming = 0;         // 关闭闹钟标志
}
}
else  // 闹钟未触发
{
Light_Led_Mode = 0;  // 确保LED熄灭
time_200ms = 0;      // 清零计时器
time_5s = 0;
}
// ... 数码管闪烁代码 ...
}

闪烁时序图

闹钟触发(Alarming=1)
↓
time_200ms开始计数
↓
0ms:   time_200ms=0,   Light_Led_Mode=0  [L1灭]
100ms: time_200ms=100, Light_Led_Mode=0  [L1灭]
200ms: time_200ms=200, 执行翻转
Light_Led_Mode^=1 → Light_Led_Mode=1  [L1亮] ★
time_200ms=0 (重置)
300ms: time_200ms=100, Light_Led_Mode=1  [L1亮]
400ms: time_200ms=200, 执行翻转
Light_Led_Mode^=1 → Light_Led_Mode=0  [L1灭] ★
time_200ms=0
600ms: time_200ms=200, Light_Led_Mode=1  [L1亮]800ms: time_200ms=200, Light_Led_Mode=0  [L1灭]...
5000ms: time_5s=5000, 自动关闭闹钟

异或翻转原理

Light_Led_Mode ^= 1;  // 异或赋值
真值表:
原值 ^ 1 = 结果
0  ^ 1 = 1
1  ^ 1 = 0
效果:每次执行都会翻转状态
等价写法:
if (Light_Led_Mode == 0)
Light_Led_Mode = 1;
else
Light_Led_Mode = 0;^= 1 更简洁高效

5秒持续时间验证

闪烁周期:200ms亮 + 200ms灭 = 400ms
闪烁次数:5000ms ÷ 400ms = 12.5次
实际效果:闪烁约12-13次后自动停止 ✓

7.4 数码管闪烁(设置时间时)

if (Setting_Mode)  // 如果处于设置状态
{
if (++time_500ms == 500)  // 每500ms执行
{
Light_Seg_Mode ^= 1;  // 数码管闪烁标志翻转
time_500ms = 0;
}
}
else  // 不在设置状态
{
time_500ms = 0;
Light_Seg_Mode = 0;  // 确保不闪烁
}

LED闪烁 vs 数码管闪烁

项目LED闪烁数码管闪烁
触发条件闹钟响设置时间/闹钟
闪烁周期200ms500ms
持续时间5秒或按键直到退出设置
标志位Light_Led_ModeLight_Seg_Mode
控制变量AlarmingSetting_Mode

八、代码详解:任务调度器

为什么需要任务调度器?

传统方法的问题
// ❌ 不推荐的写法
void main()
{
while(1)
{
Led_Proc();
delay_ms(1);
Key_Proc();
delay_ms(10);
Get_Time();
delay_ms(200);
// 问题:
// 1. 延时会卡住程序
// 2. 按键响应延迟
// 3. 修改周期很麻烦
}
}
任务调度器的优势
✓ 多任务并发执行
✓ 各任务独立设置周期
✓ 不会卡住程序
✓ 易于扩展和维护
✓ 响应速度快

8.1 任务结构体定义

/* 任务结构体 */
typedef struct
{
void (*task_func)(void);    // 任务函数指针
unsigned long int rate_ms;  // 执行周期(毫秒)
unsigned long int last_ms;  // 上次执行时间
} task_t;

结构体成员详解

成员类型作用示例
task_func函数指针指向要执行的函数Led_Proc
rate_msunsigned long任务执行周期10(每10ms执行)
last_msunsigned long上次执行的时间点1000(在1000ms时执行的)

函数指针入门

// 普通函数
void Led_Proc(void)
{
// LED处理代码
}
// 函数指针定义
void (*task_func)(void);  // 定义一个函数指针
// 函数指针赋值
task_func = Led_Proc;     // 指向Led_Proc函数
// 通过函数指针调用函数
task_func();              // 等价于 Led_Proc();

8.2 任务数组定义

/* 任务列表 */
idata task_t Scheduler_Task[] = {
{Led_Proc,        1,   0},  // 任务0:LED处理,每1ms
{Key_Proc,       10,   0},  // 任务1:按键处理,每10ms
{Get_Time,      200,   0},  // 任务2:读取时间,每200ms
{Get_Temputure, 300,   0},  // 任务3:读取温度,每300ms
{Seg_Proc,      300,   0}   // 任务4:数码管处理,每300ms
};
idata unsigned char task_num;  // 任务总数

任务列表说明

┌────┬───────────────┬────────┬──────────────┬───────────────┐
│编号│ 任务名称      │ 周期   │ 功能说明     │ 为什么这样设置│
├────┼───────────────┼────────┼──────────────┼───────────────┤
│ 0  │ Led_Proc      │ 1ms    │ 更新LED显示  │ 快速响应闪烁  │
│ 1  │ Key_Proc      │ 10ms   │ 扫描按键     │ 消抖需要10ms  │
│ 2  │ Get_Time      │ 200ms  │ 读取时间     │ 时钟秒级变化  │
│ 3  │ Get_Temputure │ 300ms  │ 读取温度     │ 温度变化缓慢  │
│ 4  │ Seg_Proc      │ 300ms  │ 更新数码管   │ 内容不常变化  │
└────┴───────────────┴────────┴──────────────┴───────────────┘

为什么任务周期不同?

LED (1ms)- 闹钟闪烁需要精确控制
- 快速响应状态变化
按键 (10ms)- 软件消抖需要10ms延时
- 平衡响应速度和稳定性
时间 (200ms)- DS1302每秒更新
- 200ms读取5次,足够精确
温度 (300ms)- 环境温度变化很慢
- 节省CPU资源

8.3 调度器初始化

void Scheduler_Init()
{
task_num = sizeof(Scheduler_Task) / sizeof(task_t);
}

自动计算任务数量

sizeof(Scheduler_Task)  // 整个数组的字节数
sizeof(task_t)          // 单个结构体的字节数
task_num = 总字节数 ÷ 单个字节数 = 元素个数
假设:
task_t结构体大小 = 8字节
Scheduler_Task有5个元素
sizeof(Scheduler_Task) = 8 × 5 = 40字节
sizeof(task_t) = 8字节
task_num = 40 ÷ 8 = 5

✨ 这样做的好处

添加新任务:
只需在数组中加一行,task_num自动更新
{New_Task, 100, 0}  // 新任务
无需手动修改 task_num = 6;

8.4 调度器运行(核心算法)

void Scheduler_Run()
{
unsigned char i;
for (i = 0; i < task_num; i++)  // 遍历所有任务
{
unsigned long int now_time = uwTick;  // 获取当前时间
// 判断任务是否该执行
if (now_time >= Scheduler_Task[i].last_ms + Scheduler_Task[i].rate_ms)
{
Scheduler_Task[i].last_ms = now_time;  // 更新时间
Scheduler_Task[i].task_func();         // 执行任务
}
}
}

调度器工作流程动画

系统启动,uwTick=0
↓
━━━━━ 第1次循环 (uwTick=1) ━━━━━
检查任务0 (Led_Proc):
now_time(1) >= last_ms(0) + rate_ms(1) ?
1 >= 0 + 1 ? ✓ 执行!
last_ms = 1
检查任务1 (Key_Proc):
now_time(1) >= last_ms(0) + rate_ms(10) ?
1 >= 0 + 10 ? ✗ 跳过
... 其他任务同理 ...
━━━━━ 第10次循环 (uwTick=10) ━━━━━
检查任务0 (Led_Proc):
10 >= 1 + 1 ? ✓ 执行!(上次是1ms)
last_ms = 10
检查任务1 (Key_Proc):
10 >= 0 + 10 ? ✓ 执行!(第一次)
last_ms = 10
━━━━━ 第11次循环 (uwTick=11) ━━━━━
检查任务0:
11 >= 10 + 1 ? ✓ 执行!
检查任务1:
11 >= 10 + 10 ? ✗ 跳过(还没到20ms)

任务执行时间线

时间轴(ms)0    10   20   30   40   50  ...  200  300
│    │    │    │    │    │        │    │
Led_Proc:    ●────●────●────●────●────●── ... ●────●──
1ms间隔,每次都执行
Key_Proc:    ●────────●────────●────── ... ●────●──
10ms间隔
Get_Time:    ●────────────────────────────●───────
200ms间隔
Seg_Proc:    ●──────────────────────────────────●──
300ms间隔

8.5 调度精度分析

问题:误差累积
// 假设Led_Proc执行需要0.5ms
uwTick=1000: 执行Led_Proc,耗时0.5ms
uwTick=1001: 应该在1001ms执行,但实际1000.5ms就执行了
累积误差:0.5ms
// 但我们的算法:
last_ms = now_time;  // 记录的是1000,不是1000.5
下次判断:1001 >= 1000 + 1 ? ✓
实际执行时间:1001ms ✓ 准确!

✨ 调度器的智能之处

调度器不依赖实际执行时间,而是依赖系统时钟uwTick
即使某个任务执行慢了,也不会影响其他任务的周期
举例:
Get_Time本应在200ms执行
但由于某种原因,在205ms才执行完
下次执行时间:
错误算法:205 + 200 = 405ms (累积误差)
正确算法:200 + 200 = 400ms (自动修正)

九、代码详解:定时器中断

⏱️ 定时器基础知识

9.1 什么是定时器?
定时器 = 单片机内部的"电子秒表"
工作原理:
1. 设置一个初始值
2. 每个时钟周期+1
3. 计数溢出时触发中断
4. 自动重新加载初始值

IAP15F2K61S2定时器资源

┌────────────────────────────────────┐
│ 定时器资源(本项目使用Timer1)     │
├────────────────────────────────────┤
│ Timer0:16位定时器(保留)         │
│ Timer1:16位定时器 ← 本项目使用    │
│ Timer2:16位定时器(保留)         │
│ Timer3:16位定时器(保留)         │
└────────────────────────────────────┘

9.2 定时器初始化

/* 定时器1初始化 - 1毫秒@12MHz */
void Timer1Init(void)
{
AUXR &= 0xBF;  // 定时器时钟12T模式
TMOD &= 0x0F;  // 设置定时器模式
TL1 = 0x18;    // 设置定时初值(低8位)
TH1 = 0xFC;    // 设置定时初值(高8位)
TF1 = 0;       // 清除溢出标志
TR1 = 1;       // 启动定时器
ET1 = 1;       // 使能定时器1中断
EA = 1;        // 使能总中断
}
寄存器详解

① AUXR寄存器:辅助寄存器

AUXR &= 0xBF;  // 0xBF = 1011 1111
作用:设置定时器时钟模式
0 = 12T模式(12个机器周期计数一次)
1 = 1T模式(1个机器周期计数一次)
本项目使用12T模式:
晶振频率:12MHz
定时器频率:12MHz ÷ 12 = 1MHz
每计数一次:1 ÷ 1,000,000 = 1微秒

② TMOD寄存器:定时器模式

TMOD &= 0x0F;  // 0x0F = 0000 1111
TMOD寄存器结构:
┌───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┐
│ GATE1 │ C/T1  │ M11   │ M10   │ GATE0 │ C/T0  │ M01   │ M00   │
├───────┴───────┴───────┴───────┼───────┴───────┴───────┴───────┤
│     Timer1控制位              │     Timer0控制位              │
└───────────────────────────────┴───────────────────────────────┘
&= 0x0F 操作效果:
保留低4位(Timer0设置)
清零高4位(Timer1设置为模式0

③ TH1和TL1:定时初值

TH1 = 0xFC;  // 高8位
TL1 = 0x18;  // 低8位
// 组合成16位值:0xFC18 = 64536
计算过程:
目标:定时1毫秒
定时器频率:1MHz(每微秒计数1次)
需要计数:1000次(1000微秒 = 1毫秒)
16位定时器最大值:65536
初值 = 65536 - 1000 = 64536 = 0xFC18
工作过程:
0xFC18 (64536) → 计数1次 → 64537
→ 计数2次 → 64538...
→ 计数1000次 → 65536 (溢出!) → 触发中断

定时时间计算公式

定时时间 = (65536 - 初值) × 计数周期
本项目:
初值 = 0xFC18 = 64536
计数周期 = 1微秒
定时时间 = (65536 - 64536) × 1μs
= 1000μs
= 1ms ✓

④ TR1:定时器运行控制

TR1 = 1;  // 启动定时器
TR1 = 0;  // 停止定时器(不使用)
TR1 = 1;  // 启动定时器

⑤ ET1和EA:中断使能

ET1 = 1;  // 使能定时器1中断
EA = 1;   // 使能总中断
中断系统结构:
EA (总中断开关)
↓
┌────┴────┐
ET0   ET1   ES   ...
↓     ↓     ↓
Timer0 Timer1 串口
必须同时满足:
✓ EA = 1 (总开关打开)
✓ ET1 = 1 (Timer1开关打开)
→ 定时器1中断才能工作

9.3 定时器中断函数

/* 定时器1中断服务函数(每1ms自动执行) */
void Timer1Isr(void) interrupt 3
{
// ===== 系统计时 =====
uwTick++;  // 全局时间计数器+1
// ===== 数码管动态扫描 =====
Seg_Pos = (++Seg_Pos) % 8;  // 位置循环:0→1→...→7→0
if (Seg_Buf[Seg_Pos] > 20)
Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos] - ',', 1);
else
Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos], 0);
// ===== 闹钟闪烁控制 =====
if (Alarming)  // 闹钟触发
{
if (++time_200ms == 200)  // 200ms翻转
{
Light_Led_Mode ^= 1;
time_200ms = 0;
}
if (++time_5s == 5000)    // 5秒后自动关闭
{
Light_Led_Mode = 0;
time_200ms = 0;
time_5s = 0;
Alarming = 0;
}
}
else
{
Light_Led_Mode = 0;
time_200ms = 0;
time_5s = 0;
}
// ===== 数码管闪烁控制 =====
if (Setting_Mode)  // 设置状态
{
if (++time_500ms == 500)  // 500ms翻转
{
Light_Seg_Mode ^= 1;
time_500ms = 0;
}
}
else
{
time_500ms = 0;
Light_Seg_Mode = 0;
}
}

interrupt 3 是什么意思?

void Timer1Isr(void) interrupt 3
interrupt:中断关键字
3:中断向量号
中断向量表:
┌───────────────────┬───────┐
│ 中断源            │ 向量号│
├───────────────────┼───────┤
│ 外部中断0 (INT0)0   │
│ 定时器0 (Timer0)1   │
│ 外部中断1 (INT1)2   │
│ 定时器1 (Timer1)3   │ ← 我们用的
│ 串口 (UART)4   │
└───────────────────┴───────┘

⚠️ 中断函数注意事项

1. 中断函数会自动调用,不需要手动调用
2. 中断函数应该尽量简短
3. 中断函数不能有返回值
4. 中断函数不能被其他函数调用
5. 中断函数中不能使用delay延时

9.4 数码管动态扫描详解

// 每1ms执行一次
Seg_Pos = (++Seg_Pos) % 8;  // 位置:0→1→2→...→7→0
Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos], 0);

扫描过程可视化

时间:0ms
点亮位0,显示Seg_Buf[0]
┌━┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐
│2│ │ │ │ │ │ │ │ │ │ │ │ │ │ │
└─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘
↑ 点亮
时间:1ms
点亮位1,显示Seg_Buf[1]
┌─┐ ┌━┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐
│ │ │3│ │ │ │ │ │ │ │ │ │ │ │ │
└─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘
↑ 点亮
...1ms移动一位 ...
时间:8ms
回到位0,新一轮扫描
┌━┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐
│2│ │ │ │ │ │ │ │ │ │ │ │ │ │ │
└─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘
↑ 再次点亮
人眼视觉暂留:
扫描频率 = 1000ms ÷ 8 = 125Hz
人眼极限约50Hz
所以看起来8位同时点亮!

为什么 if (Seg_Buf[Seg_Pos] > 20)

if (Seg_Buf[Seg_Pos] > 20)
Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos] - ',', 1);  // 带小数点
else
Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos], 0);        // 不带小数点
解释:
Seg_Buf存储的数字大于20时,表示需要显示小数点
例如:
Seg_Buf[5] = 45;  // 45 = ',' + 1
// ',' 的ASCII码是44
显示:带小数点的数字1
这是一种编码技巧,用一个字节同时表示数字和小数点

十、完整流程梳理

程序运行全景图

┌─────────────────────────────────────────────────────────────┐
│                     程序启动流程                            │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────┐
│   单片机上电复位        │
└─────────────────────────┘
↓
┌─────────────────────────┐
│   执行main()函数        │
└─────────────────────────┘
↓
┌───────────────────┴───────────────────┐
↓                                       ↓
┌───────────────┐                     ┌─────────────────┐
│ 硬件初始化    │                     │ 软件初始化      │
├───────────────┤                     ├─────────────────┤
│System_Init()  │                     │Scheduler_Init() │
│关闭蜂鸣器继电器│                     │计算任务数量     │
├───────────────┤                     ├─────────────────┤
│rd_temperature()│                     │Timer1Init()     │
│启动温度传感器 │                     │启动1ms定时器    │
├───────────────┤                     └─────────────────┘
│Delay750ms()   │
│等待传感器稳定 │
├───────────────┤
│Set_Rtc()      │
│写入初始时间   │
└───────────────┘
↓
┌─────────────────────────┐
│  while(1) 主循环        │
│  Scheduler_Run()        │
└─────────────────────────┘

10.1 主循环与中断的协作

┌─────────────────────┐        ┌─────────────────────┐
│     主循环          │        │     中断服务        │
│                     │        │                     │
│ while(1) {          │        │ void Timer1Isr()    │
│   Scheduler_Run();  │◄───────┤   interrupt 3       │
│   ├─ Led_Proc()     │ 被打断 │ {                   │
│   ├─ Key_Proc()     │        │   uwTick++;         │
│   ├─ Get_Time()     │        │   扫描数码管        │
│   ├─ Get_Temputure()│        │   闪烁控制          │
│   └─ Seg_Proc()     │        │ }                   │
│ }                   │        │                     │
└─────────────────────┘        └─────────────────────┘
↑                               ↓
└───────────────────────────────┘
每1ms触发一次

为什么主循环不会被打断?

其实会被打断,但打断时间很短:
主循环执行中... (假设执行Key_Proc)1ms到了,触发中断
↓
暂停主循环,保存现场
↓
执行Timer1Isr() (约几十微秒)
↓
恢复现场,返回主循环
↓
继续执行Key_Proc...
用户感觉:无缝执行,没有延迟

10.2 典型用户操作流程

场景1:查看温度
用户操作:按住S4键
程序响应:
┌────────────┐
│ 1. Key_Proc检测到Key_Down=4 (10ms检测)
│    ↓
│ 2. Seg_Show_Mode = 3 (切换到温度页)
│    ↓
│ 3. Seg_Proc更新数码管缓冲 (300ms)
│    Seg_Buf = [10,10,10,10,10,8,2,12]
│    ↓
│ 4. 中断扫描数码管 (1ms)
│    显示:     82C
│    ↓
│ 5. 用户松开S4
│    ↓
│ 6. Key_Proc检测到Key_Up=4
│    ↓
│ 7. Seg_Show_Mode = 0 (返回时间页)
│    ↓
│ 8. Seg_Proc更新数码管缓冲
│    Seg_Buf = [2,3,11,4,5,11,3,0]
│    ↓
│ 9. 显示:23-45-30
└────────────┘
场景2:设置闹钟
用户操作:设置闹钟为06:30:00
程序响应:
┌────────────┐
│ 第1步:按S6进入闹钟设置
│    Key_Down=6 → Seg_Show_Mode=2
│    ucAlarm_ctrl = ucAlarm (复制当前值)
│    ↓
│ 第2步:调整"时"
│    数码管显示:00-00-00 (时位闪烁)
│    按S5 6次:0001...06
│    ↓
│ 第3步:按S6切换到"分"
│    Change_Alarm_Mode++1
│    数码管显示:06-00-00 (分位闪烁)
│    按S5 30次:0001...30
│    ↓
│ 第4步:按S6切换到"秒"
│    Change_Alarm_Mode++2
│    数码管显示:06-30-00 (秒位闪烁)(无需调整)
│    ↓
│ 第5步:按S6保存退出
│    Change_Alarm_Mode==2 → 保存
│    ucAlarm = ucAlarm_ctrl
│    Seg_Show_Mode = 0 (返回时间页)
└────────────┘
场景3:闹钟触发
系统自动响应:
时间线:
06:29:58  Get_Time()读取时间,不匹配
06:29:59  Get_Time()读取时间,不匹配
06:30:00  Get_Time()读取时间,✅匹配!
↓
Alarming = 1
↓
定时器中断开始控制LED闪烁
↓
0ms:      Light_Led_Mode=0, L1灭
200ms:    Light_Led_Mode^=11, L1亮
400ms:    Light_Led_Mode^=10, L1灭
600ms:    Light_Led_Mode^=11, L1亮
...
5000ms:   time_5s==5000 → 自动关闭
或
用户按键:  Key_Down!=0 → Alarming=0 → 提前关闭

10.3 时间同步机制

DS1302芯片 ←─────→ 单片机
(硬件时钟)        (软件显示)
↓                ↓
持续计时        每200ms读取
│                │
│  Read_Rtc()    │
└───────►────────┘
↓
ucRtc[3]更新
↓
Seg_Proc()处理
↓
Seg_Buf[]更新
↓
中断扫描显示
↓
数码管显示时间
设置时间时:
用户调整 → ucRtc_ctrl[]
↓
按S7第4次保存
↓
Set_Rtc(ucRtc_ctrl)
↓
写入DS1302芯片
↓
新时间开始走

十一、常见问题FAQ

编程相关

Q1:为什么有些变量前面有 idatapdata
idata unsigned char ucRtc[3];  // 内部RAM
pdata unsigned char ucLed[8];  // 外部RAM
A:这是51单片机的内存类型关键字
┌──────────────────────────────────┐
│ 51单片机内存结构                  │
├──────────────────────────────────┤
│ data  : 内部RAM低128字节(最快)    │
│ idata : 内部RAM全部256字节        │
│ xdata : 外部RAM(最大64KB)         │
│ pdata : 外部RAM单页(256字节)      │
│ code  : 程序存储区(ROM)           │
└──────────────────────────────────┘
建议:
频繁访问的变量 → data/idata (速度快)
大数组 → xdata/pdata (空间大)
Q2:为什么有些地方用 bit 类型?
idata bit Alarming = 0;  // 只占1位
A:节省内存,提高效率
普通变量:
unsigned char flag = 0;  // 占用8位
位变量:
bit flag = 0;            // 只占1位
8个bit变量 = 1char变量的空间
适用场景:
✓ 开关状态(开/关)
✓ 标志位(是/否)
✗ 需要存储数值(如计数器)
Q3:++ 在前和在后有什么区别?
// 前缀++:先加后用
int a = 5;
int b = ++a;  // a=6, b=6
// 后缀++:先用后加
int a = 5;
int b = a++;  // b=5, a=6
在代码中:
ucRtc_ctrl[0] = (++ucRtc_ctrl[0]) % 24;
// 必须用++前缀,因为要先加1再取模

硬件相关

Q4:DS1302和DS18B20有什么区别?
┌─────────────────────────────────────────────┐
│            DS1302 vs DS18B20                │
├──────────────┬──────────────────────────────┤
│ DS1302       │ 实时时钟芯片                 │
│              │ 功能:计时(时分秒)         │
│              │ 通信:SPI(3线)             │
│              │ 精度:秒级                   │
│              │ 掉电保持:是(配纽扣电池)   │
├──────────────┼──────────────────────────────┤
│ DS18B20      │ 温度传感器                   │
│              │ 功能:测温(-55~125℃)     │
│              │ 通信:单总线(1线)          │
│              │ 精度:±0.5℃                 │
│              │ 转换时间:750ms              │
└──────────────┴──────────────────────────────┘
Q5:为什么数码管要动态扫描?
静态显示:
8位数码管 × 8根段码线 = 64根线
引脚不够用!
动态扫描:
8根段码线(共用)+ 8根位选线 = 16根线
节省引脚,成本低
原理:
利用人眼视觉暂留(约50Hz)
扫描频率125Hz >> 50Hz
看起来是同时亮的

调试相关

Q6:如何调试单片机程序?
方法1:LED指示法
void debug_point_1() {
P2 = 0xFE;  // 点亮L1
}
方法2:数码管显示法
Seg_Buf[0] = 1;  // 显示调试代码
方法3:逻辑分析仪
监控引脚电平变化
方法4:仿真器
Keil + 仿真器可以单步调试、查看变量
Q7:按键没反应怎么办?
检查清单:
□ 硬件:按键是否接触良好
□ 代码:Key_Read()函数是否正确
□ 周期:Key_Proc()是否在运行
□ 消抖:延时是否合适(10ms)
□ 逻辑:switch-case是否有break
调试方法:
在Key_Proc()中添加LED指示:
if (Key_Down != 0) {
P2 = ~(1 << 0);  // 点亮L1
}

十二、学习资源

推荐学习路线

1阶段:C语言基础(1-2周)
✓ 变量、数组、函数
✓ 指针、结构体
✓ 位运算
第2阶段:51单片机入门(2-3周)
✓ GPIO输出:点亮LED
✓ GPIO输入:按键检测
✓ 定时器:定时1ms
✓ 中断:理解中断机制
第3阶段:外设驱动(3-4周)
✓ 数码管:静态显示、动态扫描
✓ DS1302:读写时间
✓ DS18B20:读取温度
第4阶段:综合项目(2-3周)
✓ 电子钟:本教程项目
✓ 拓展:增加日期、闹铃音乐等

知识点速查表

知识点说明代码示例
数组存储多个相同类型数据int a[3] = {1,2,3};
取模获取余数,实现循环value % 24
位运算操作二进制位LED ^= 1;
三元运算符简化if-elsea = (b>0) ? 1 : 0;
函数指针指向函数的指针void (*p)(void);
结构体打包相关数据struct {...} task;
中断硬件事件触发函数void ISR() interrupt 3

学习资料来源

声明:
以上的代码均由米醋电子工作室提供的模板和相关的考点资料学习而完成的。

如果对单片机感兴趣,想跟更多大佬一起学习嵌入式的话:

  • B站搜索:Alice_西风
  • 抖音搜索:米醋电子工作室

致谢与寄语

恭喜你完成了这个完整的教程!

你已经掌握了:
✅ 单片机基本开发流程
✅ 任务调度器设计思想
✅ 定时器与中断机制
✅ 外设驱动开发方法
✅ 项目调试技巧

接下来的建议:

  1. 动手实践:在实验板上运行代码
  2. 修改参数:观察现象,加深理解
  3. 功能拓展:添加日期显示、贪吃蛇游戏等
  4. 参加比赛:蓝桥杯是很好的练习平台

记住:
理论 + 实践 = 真正的掌握
多写代码,多调试,多思考

每一个大佬都是从小白成长起来的
坚持学习,你也可以成为嵌入式高手!


附录A:完整代码索引

A.1 main.c 主程序

#include <STC15F2K60S2.H>
  #include "ds1302.h"
  #include "key.h"
  #include "led.h"
  #include "onewire.h"
  #include "seg.h"
  #include "init.h"
  #include "intrins.h"
  /*===============================
  *         变量声明区
  *===============================*/
  // ---- 计时器/调度器 ----
  idata unsigned long int uwTick = 0; // 系统毫秒计数(由定时器中断每1ms自增),供简易调度器使用
  // ---- 按键 ----
  idata unsigned char Key_Val, Key_Old, Key_Down, Key_Up;
  // Key_Val  :当前按键值(外部Key_Read()提供)
  // Key_Old  :上一次扫描到的按键值
  // Key_Down :按键按下沿(本次为1且与上次不同)
  // Key_Up   :按键释放沿(本次为0且与上次不同)
  // ---- LED ----
  pdata unsigned char ucLed[8] = {0, 0, 0, 0, 0, 0, 0, 0};
  // 每位代表1个LED的目标状态:0灭/1亮(实际驱动在 Led_Proc -> Led_Disp 中完成)
  // 注:具体电气为共阳,最终会在驱动层取反输出
  // ---- 数码管 ----
  idata unsigned char Seg_Pos;                 // 当前要刷新的位(0~7,定时器中断里循环)
  pdata unsigned char Seg_Buf[8] = {10, 10, 10, 10, 10, 10, 10, 10};
  // 显存缓冲:0~9数字;10=灭位/空白;11='-';12='C'
  // Timer1Isr中按Seg_Pos逐位扫描显示
  /* ---- 功能变量 ---- */
  idata unsigned char ucRtc[3] = {23, 59, 50};     // 当前时间:时、分、秒(从DS1302读/写)
  idata unsigned char ucAlarm[3] = {0, 0, 0};      // 闹钟时间:时、分、秒(生效值)
  idata unsigned char ucRtc_ctrl[3] = {0, 0, 0};   // 设置界面下的时间缓冲(未确认前写这里)
  idata unsigned char ucAlarm_ctrl[3] = {0, 0, 0}; // 设置界面下的闹钟缓冲(未确认前写这里)
  idata unsigned char Seg_Show_Mode = 0;  // 显示页面选择:0时间显示 1时钟设置 2闹钟设置 3温度显示
  idata unsigned char Change_Time_Mode = 0;  // 时间设置:0调小时 1调分钟 2调秒
  idata unsigned char Change_Alarm_Mode = 0; // 闹钟设置:0调小时 1调分钟 2调秒
  idata unsigned char temperature_value = 0; // 温度整数值(rd_temperature()读回)
  // ---- 闪烁/节拍用计数器 ----
  idata unsigned int  time_500ms = 0; // 500ms计时(控制设置界面闪烁)
  idata unsigned char time_200ms = 0; // 200ms计时(控制闹钟LED闪烁频率)
  idata unsigned int  time_5s   = 0;  // 5s计时(闹钟自动停止)
  // ---- 状态标志 ----
  idata bit Setting_Mode   = 0; // 是否处于设置界面(1设置中/0非设置)
  idata bit Alarming       = 0; // 闹钟是否正在响(1响/0未响)
  idata bit Light_Seg_Mode = 0; // 数码管闪烁标志(设置界面下用于闪烁当前正在修改的位)
  idata bit Light_Led_Mode = 0; // LED闪烁标志(闹钟响时闪烁LED)
  /*===============================
  *            按键任务
  *===============================*/
  void Key_Proc()
  {
  // ---- 生成按键沿信号 ----
  Key_Val  = Key_Read();                   // 读取当前按键编码(具体编码规则见key.c)
  Key_Down = Key_Val & (Key_Val ^ Key_Old); // 本次为1且上次不同 -> 按下沿
  Key_Up   = ~Key_Val & (Key_Val ^ Key_Old);// 本次为0且上次不同 -> 释放沿
  Key_Old  = Key_Val;
  // ---- 根据当前显示页面处理按键 ----
  switch (Seg_Show_Mode)
  {
  case 0: // ====== 时间显示页面 ======
  switch (Key_Down)
  {
  case 7: // 进入“时钟设置”
  Seg_Show_Mode = 1;
  ucRtc_ctrl[0] = ucRtc[0];
  ucRtc_ctrl[1] = ucRtc[1];
  ucRtc_ctrl[2] = ucRtc[2];
  break;
  case 6: // 进入“闹钟设置”
  Seg_Show_Mode = 2;
  ucAlarm_ctrl[0] = ucAlarm[0];
  ucAlarm_ctrl[1] = ucAlarm[1];
  ucAlarm_ctrl[2] = ucAlarm[2];
  break;
  case 4: // 进入“温度显示”
  Seg_Show_Mode = 3;
  break;
  }
  break;
  case 1: // ====== 时钟设置页面 ======
  switch (Key_Down)
  {
  case 7: // 确认/切换设置位:小时->分钟->秒->完成返回
  if (Change_Time_Mode == 2)
  {
  Seg_Show_Mode = 0;           // 退出到时间显示
  Change_Time_Mode = 0;        // 重置设置位置
  Set_Rtc(ucRtc_ctrl);         // 将设置写入DS1302
  }
  else
  {
  Change_Time_Mode++;          // 切到下一个字段
  }
  break;
  case 5: // 加一(当前设置位加1,循环)
  if (Change_Time_Mode == 0)
  ucRtc_ctrl[0] = (++ucRtc_ctrl[0]) % 24; // 小时:0~23
  else
  ucRtc_ctrl[Change_Time_Mode] = (++ucRtc_ctrl[Change_Time_Mode]) % 60; // 分/秒:0~59
  break;
  case 4: // 减一(当前设置位减1,循环)
  if (Change_Time_Mode == 0)
  ucRtc_ctrl[0] = (ucRtc_ctrl[0] == 0) ? 23 : --ucRtc_ctrl[0]; // 小时:0<-23
  else
  ucRtc_ctrl[Change_Time_Mode] =
  (ucRtc_ctrl[Change_Time_Mode] == 0) ? 59 : --ucRtc_ctrl[Change_Time_Mode]; // 分/秒:0<-59
  break;
  }
  break;
  case 2: // ====== 闹钟设置页面 ======
  switch (Key_Down)
  {
  case 6: // 确认/切换设置位:小时->分钟->秒->完成返回
  if (Change_Alarm_Mode == 2)
  {
  Seg_Show_Mode = 0;              // 退出到时间显示
  Change_Alarm_Mode = 0;          // 重置设置位置
  ucAlarm[0] = ucAlarm_ctrl[0];   // 生效:拷回正式的闹钟时间
  ucAlarm[1] = ucAlarm_ctrl[1];
  ucAlarm[2] = ucAlarm_ctrl[2];
  }
  else
  {
  Change_Alarm_Mode++;            // 切到下一个字段
  }
  break;
  case 5: // 加一(循环)
  if (Change_Alarm_Mode == 0)
  ucAlarm_ctrl[0] = (++ucAlarm_ctrl[0]) % 24; // 小时
  else
  ucAlarm_ctrl[Change_Alarm_Mode] = (++ucAlarm_ctrl[Change_Alarm_Mode]) % 60; // 分/秒
  break;
  case 4: // 减一(循环)
  if (Change_Alarm_Mode == 0)
  ucAlarm_ctrl[0] = (ucAlarm_ctrl[0] == 0) ? 23 : --ucAlarm_ctrl[0]; // 小时
  else
  ucAlarm_ctrl[Change_Alarm_Mode] =
  (ucAlarm_ctrl[Change_Alarm_Mode] == 0) ? 59 : --ucAlarm_ctrl[Change_Alarm_Mode]; // 分/秒
  break;
  }
  break;
  case 3: // ====== 温度显示页面 ======
  if (Key_Up == 4) // 松开键4返回时间页面
  Seg_Show_Mode = 0;
  break;
  }
  // 闹钟响时,任意按键按下(Key_Down!=0)都可停止闹钟
  if (Alarming)
  {
  if (Key_Down != 0)
  Alarming = 0;
  }
  }
  /*===============================
  *         数码管显示任务
  *===============================*/
  void Seg_Proc()
  {
  switch (Seg_Show_Mode)
  {
  case 0: // ====== 时间显示 hh-mm-ss ======
  Seg_Buf[0] = ucRtc[0] / 10;
  Seg_Buf[1] = ucRtc[0] % 10;
  Seg_Buf[2] = 11; // '-'
  Seg_Buf[3] = ucRtc[1] / 10;
  Seg_Buf[4] = ucRtc[1] % 10;
  Seg_Buf[5] = 11; // '-'
  Seg_Buf[6] = ucRtc[2] / 10;
  Seg_Buf[7] = ucRtc[2] % 10;
  break;
  case 1: // ====== 时间设置界面(当前设置位闪烁) ======
  if (Change_Time_Mode == 0) // 正在改“小时”
  {
  Seg_Buf[0] = (Light_Seg_Mode) ? ucRtc_ctrl[0] / 10 : 10;
  Seg_Buf[1] = (Light_Seg_Mode) ? ucRtc_ctrl[0] % 10 : 10;
  Seg_Buf[2] = 11;
  Seg_Buf[3] = ucRtc_ctrl[1] / 10;
  Seg_Buf[4] = ucRtc_ctrl[1] % 10;
  Seg_Buf[5] = 11;
  Seg_Buf[6] = ucRtc_ctrl[2] / 10;
  Seg_Buf[7] = ucRtc_ctrl[2] % 10;
  }
  else if (Change_Time_Mode == 1) // 正在改“分钟”
  {
  Seg_Buf[0] = ucRtc_ctrl[0] / 10;
  Seg_Buf[1] = ucRtc_ctrl[0] % 10;
  Seg_Buf[2] = 11;
  Seg_Buf[3] = (Light_Seg_Mode) ? ucRtc_ctrl[1] / 10 : 10;
  Seg_Buf[4] = (Light_Seg_Mode) ? ucRtc_ctrl[1] % 10 : 10;
  Seg_Buf[5] = 11;
  Seg_Buf[6] = ucRtc_ctrl[2] / 10;
  Seg_Buf[7] = ucRtc_ctrl[2] % 10;
  }
  else // Change_Time_Mode == 2,正在改“秒”
  {
  Seg_Buf[0] = ucRtc_ctrl[0] / 10;
  Seg_Buf[1] = ucRtc_ctrl[0] % 10;
  Seg_Buf[2] = 11;
  Seg_Buf[3] = ucRtc_ctrl[1] / 10;
  Seg_Buf[4] = ucRtc_ctrl[1] % 10;
  Seg_Buf[5] = 11;
  Seg_Buf[6] = (Light_Seg_Mode) ? ucRtc_ctrl[2] / 10 : 10;
  Seg_Buf[7] = (Light_Seg_Mode) ? ucRtc_ctrl[2] % 10 : 10;
  }
  break;
  case 2: // ====== 闹钟设置界面(当前设置位闪烁) ======
  if (Change_Alarm_Mode == 0) // 正在改“小时”
  {
  Seg_Buf[0] = (Light_Seg_Mode) ? ucAlarm_ctrl[0] / 10 : 10;
  Seg_Buf[1] = (Light_Seg_Mode) ? ucAlarm_ctrl[0] % 10 : 10;
  Seg_Buf[2] = 11;
  Seg_Buf[3] = ucAlarm_ctrl[1] / 10;
  Seg_Buf[4] = ucAlarm_ctrl[1] % 10;
  Seg_Buf[5] = 11;
  Seg_Buf[6] = ucAlarm_ctrl[2] / 10;
  Seg_Buf[7] = ucAlarm_ctrl[2] % 10;
  }
  else if (Change_Alarm_Mode == 1) // 正在改“分钟”
  {
  Seg_Buf[0] = ucAlarm_ctrl[0] / 10;
  Seg_Buf[1] = ucAlarm_ctrl[0] % 10;
  Seg_Buf[2] = 11;
  Seg_Buf[3] = (Light_Seg_Mode) ? ucAlarm_ctrl[1] / 10 : 10;
  Seg_Buf[4] = (Light_Seg_Mode) ? ucAlarm_ctrl[1] % 10 : 10;
  Seg_Buf[5] = 11;
  Seg_Buf[6] = ucAlarm_ctrl[2] / 10;
  Seg_Buf[7] = ucAlarm_ctrl[2] % 10;
  }
  else // Change_Alarm_Mode == 2,正在改“秒”
  {
  Seg_Buf[0] = ucAlarm_ctrl[0] / 10;
  Seg_Buf[1] = ucAlarm_ctrl[0] % 10;
  Seg_Buf[2] = 11;
  Seg_Buf[3] = ucAlarm_ctrl[1] / 10;
  Seg_Buf[4] = ucAlarm_ctrl[1] % 10;
  Seg_Buf[5] = 11;
  Seg_Buf[6] = (Light_Seg_Mode) ? ucAlarm_ctrl[2] / 10 : 10;
  Seg_Buf[7] = (Light_Seg_Mode) ? ucAlarm_ctrl[2] % 10 : 10;
  }
  break;
  case 3: // ====== 温度显示(末两位显示温度,最后一位显示'C') ======
  Seg_Buf[0] = 10;
  Seg_Buf[1] = 10;
  Seg_Buf[2] = 10;
  Seg_Buf[3] = 10;
  Seg_Buf[4] = 10;
  Seg_Buf[5] = temperature_value / 10 % 10;
  Seg_Buf[6] = temperature_value % 10;
  Seg_Buf[7] = 12; // 'C'
  break;
  }
  }
  /*===============================
  *            LED任务
  *===============================*/
  void Led_Proc()
  {
  // 设置页面时置位Setting_Mode,用于触发数码管闪烁节拍
  Setting_Mode = (Seg_Show_Mode == 1) || (Seg_Show_Mode == 2);
  // LED效果:把第0号LED绑定到闹钟闪烁标志(Light_Led_Mode)
  ucLed[0] = Light_Led_Mode;
  // 刷新LED硬件(底层 led.c 中的 Led_Disp 负责把ucLed[]打包并输出,注意共阳需反相)
  Led_Disp(ucLed);
  }
  /*===============================
  *          读取温度任务
  *===============================*/
  void Get_Temputure()
  {
  temperature_value = rd_temperature(); // 通过单总线传感器读温度(返回整数部分)
  }
  /*===============================
  *          读取时间任务
  *===============================*/
  void Get_Time()
  {
  Read_Rtc(ucRtc); // 从DS1302读取当前时间到 ucRtc[3]
  // 到达设定的闹钟时间则置位闹钟触发
  if ((ucRtc[0] == ucAlarm[0]) &&
  (ucRtc[1] == ucAlarm[1]) &&
  (ucRtc[2] == ucAlarm[2]))
  {
  Alarming = 1;
  }
  }
  /*===============================
  *        定时器1初始化(1ms)
  *===============================*/
  void Timer1Init(void) // 1毫秒@12.000MHz
  {
  AUXR &= 0xBF; // 定时器时钟选择为12T模式
  TMOD &= 0x0F; // 清除T1控制位;保留T0设置
  TL1  = 0x18;  // 装载计数初值(低字节)
  TH1  = 0xFC;  // 装载计数初值(高字节) -> 1ms溢出
  TF1  = 0;     // 清溢出标志
  TR1  = 1;     // 启动T1
  ET1  = 1;     // 允许T1中断
  EA   = 1;     // 全局中断打开
  }
  /*===============================
  *        定时器1中断服务
  *===============================*/
  void Timer1Isr(void) interrupt 3
  {
  // ---- 基本节拍 ----
  uwTick++;                          // 系统毫秒计数+1(给调度器用)
  Seg_Pos = (++Seg_Pos) % 8;         // 位选循环:0~7
  // ---- 数码管扫描显示 ----
  if (Seg_Buf[Seg_Pos] > 20)
  // 当缓存大于20时,约定:显示 Seg_Buf[Seg_Pos] - ',' 并点亮小数点(具体实现见 Seg_Disp)
  Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos] - ',', 1);
  else
  // 正常显示,无小数点
  Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos], 0);
  // ---- 闹钟响与闪烁管理 ----
  if (Alarming)
  {
  // 每200ms翻转一次 LED 闪烁标志
  if (++time_200ms == 200)
  {
  Light_Led_Mode ^= 1; // 取反实现闪烁
  time_200ms = 0;
  }
  // 5秒后自动停止闹钟
  if (++time_5s == 5000)
  {
  Light_Led_Mode = 0;
  time_200ms = 0;
  time_5s = 0;
  Alarming = 0;
  }
  }
  else
  {
  // 非闹钟状态:所有与闹钟相关的闪烁与计数复位
  Light_Led_Mode = 0;
  time_200ms = 0;
  time_5s = 0;
  }
  // ---- 设置界面下数码管闪烁(500ms翻转) ----
  if (Setting_Mode)
  {
  if (++time_500ms == 500)
  {
  Light_Seg_Mode ^= 1; // 设置界面的当前位闪烁
  time_500ms = 0;
  }
  }
  else
  {
  // 非设置状态:不闪烁
  time_500ms = 0;
  Light_Seg_Mode = 0;
  }
  }
  /*===============================
  *            简易调度器
  *===============================*/
  // 任务描述:函数指针+周期+上次执行时间
  typedef struct
  {
  void (*task_func)(void);         // 任务函数
  unsigned long int rate_ms;       // 任务周期(ms)
  unsigned long int last_ms;       // 上次执行时刻(ms)
  } task_t;
  // 任务表:按优先级(顺序)调度
  idata task_t Scheduler_Task[] = {
  {Led_Proc,        1,   0}, // LED任务:1ms检查(配合中断闪烁标志)
  {Key_Proc,        10,  0}, // 按键扫描:10ms
  {Get_Time,        200, 0}, // 读时钟:200ms
  {Get_Temputure,   300, 0}, // 读温度:300ms
  {Seg_Proc,        300, 0}  // 组装数码管显示缓冲:300ms
  };
  idata unsigned char task_num; // 任务数量
  // 调度器初始化:计算任务数
  void Scheduler_Init()
  {
  task_num = sizeof(Scheduler_Task) / sizeof(task_t);
  }
  // 调度器运行:轮询任务表,按周期执行
  void Scheduler_Run()
  {
  unsigned char i;
  for (i = 0; i < task_num; i++)
  {
  unsigned long int now_time = uwTick; // 取当前节拍
  if (now_time >= Scheduler_Task[i].last_ms + Scheduler_Task[i].rate_ms)
  {
  Scheduler_Task[i].last_ms = now_time; // 更新时间戳
  Scheduler_Task[i].task_func();        // 调用任务函数
  }
  }
  }
  /*===============================
  *          延时函数(约750ms)
  *===============================*/
  void Delay750ms() //@12.000MHz
  {
  unsigned char i, j, k;
  _nop_(); _nop_();
  i = 35;
  j = 51;
  k = 182;
  do
  {
  do
  {
  while (--k);
  } while (--j);
  } while (--i);
  }
  /*===============================
  *              主函数
  *===============================*/
  void main()
  {
  System_Init();        // 系统初始化(GPIO/DS1302/显示/按键等外设初始化,见init.c)
  rd_temperature();     // 先读取一次温度(有些传感器首次读取需丢弃或预热)
  Delay750ms();         // 按原设计做一次上电延时
  Set_Rtc(ucRtc);       // 将默认时间写入DS1302(上电同步一次)
  Scheduler_Init();     // 初始化任务表
  Timer1Init();         // 配置1ms定时器中断(提供系统节拍&数码管扫描)
  while (1)
  {
  Scheduler_Run();  // 循环执行简易调度器(轮询各任务是否到时)
  }
  }

A.2 任务调度器

/* 任务结构体定义 */
typedef struct
{
void (*task_func)(void);
unsigned long int rate_ms;
unsigned long int last_ms;
} task_t;
/* 任务列表 */
idata task_t Scheduler_Task[] = {
{Led_Proc, 1, 0},
{Key_Proc, 10, 0},
{Get_Time, 200, 0},
{Get_Temputure, 300, 0},
{Seg_Proc, 300, 0}
};
idata unsigned char task_num;
/* 调度器初始化 */
void Scheduler_Init()
{
task_num = sizeof(Scheduler_Task) / sizeof(task_t);
}
/* 调度器运行 */
void Scheduler_Run()
{
unsigned char i;
for (i = 0; i < task_num; i++)
{
unsigned long int now_time = uwTick;
if (now_time >= Scheduler_Task[i].last_ms + Scheduler_Task[i].rate_ms)
{
Scheduler_Task[i].last_ms = now_time;
Scheduler_Task[i].task_func();
}
}
}

A.3 中断服务函数

/* 定时器1初始化 */
void Timer1Init(void)
{
AUXR &= 0xBF;
TMOD &= 0x0F;
TL1 = 0x18;
TH1 = 0xFC;
TF1 = 0;
TR1 = 1;
ET1 = 1;
EA = 1;
}
/* 定时器1中断 */
void Timer1Isr(void) interrupt 3
{
uwTick++;
Seg_Pos = (++Seg_Pos) % 8;
if (Seg_Buf[Seg_Pos] > 20)
Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos] - ',', 1);
else
Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos], 0);
// 闹钟闪烁控制
if (Alarming)
{
if (++time_200ms == 200)
{
Light_Led_Mode ^= 1;
time_200ms = 0;
}
if (++time_5s == 5000)
{
Light_Led_Mode = 0;
time_200ms = 0;
time_5s = 0;
Alarming = 0;
}
}
else
{
Light_Led_Mode = 0;
time_200ms = 0;
time_5s = 0;
}
// 数码管闪烁控制
if (Setting_Mode)
{
if (++time_500ms == 500)
{
Light_Seg_Mode ^= 1;
time_500ms = 0;
}
}
else
{
time_500ms = 0;
Light_Seg_Mode = 0;
}
}

附录B:关键算法解析

B.1 循环加法算法

/* 问题:如何实现0-23的循环? */
// ✅ 正确方法:取模运算
value = (++value) % 24;
// 算法证明:
// 当value=22: (22+1) % 24 = 23 ✓
// 当value=23: (23+1) % 24 = 0  ✓ 循环到0
// 当value=0:  (0+1)  % 24 = 1  ✓
/* 扩展到任意范围 */
// 0~59循环(分秒)
value = (++value) % 60;
// 1~12循环(月份)
value = value % 12 + 1;

B.2 循环减法算法

/* 问题:如何实现23-0的倒序循环? */
// ❌ 错误方法:
value = (--value) % 24;  // value=0时会下溢到255
// ✅ 正确方法:三元运算符
value = (value == 0) ? 23 : --value;
// 算法证明:
// 当value=1: value不是0,执行--value=0 ✓
// 当value=0: value是0,赋值23 ✓
// 当value=23: value不是0,执行--value=22 ✓
/* 通用公式 */
// 循环范围:MAX ~ 0
value = (value == 0) ? MAX : --value;

B.3 数字拆分算法

/* 问题:如何把两位数拆成十位和个位? */
// 方法:除法和取模
unsigned char num = 45;
unsigned char ten = num / 10;  // 十位 = 4
unsigned char one = num % 10;  // 个位 = 5
// 数学原理:
// 45 = 4×10 + 5
// 45 / 10 = 4 (整数除法,舍去余数)
// 45 % 10 = 5 (取模运算,得到余数)
/* 扩展到三位数 */
unsigned int num = 123;
unsigned char hundred = num / 100;      // 百位 = 1
unsigned char ten = (num / 10) % 10;    // 十位 = 2
unsigned char one = num % 10;           // 个位 = 3

B.4 状态翻转算法

/* 问题:如何让LED每隔一段时间亮灭? */
// ✅ 方法1:异或运算
led_state ^= 1;  // 0变1,1变0
// 等价于:
if (led_state == 0)
led_state = 1;
else
led_state = 0;
// ✅ 方法2:取反运算
led_state = !led_state;
// ✅ 方法3:逻辑运算
led_state = 1 - led_state;
/* 推荐异或运算,效率最高 */

附录C:错误排查指南

C.1 编译错误

错误1:undefined symbol
Error: undefined symbol 'Key_Read'
原因:函数未声明或未定义
解决:
1. 检查是否包含对应头文件
#include "key.h"
2. 检查函数名是否拼写正确
Key_Read()key_read()(大小写错误)
错误2:missing ‘;’
Error: missing ';' before '}'
原因:语句末尾缺少分号
解决:仔细检查每条语句
int a = 10;int b = 20(缺少分号)
错误3:redefinition
Error: redefinition of 'uwTick'
原因:变量重复定义
解决:
1. 全局变量只在一个.c文件中定义
2. 其他文件用extern声明
// main.c
unsigned long uwTick;
// other.c
extern unsigned long uwTick;

C.2 运行错误

问题1:数码管不显示
排查步骤:
1. 检查硬件连接
□ 数码管是否插好
□ 电源是否正常
2. 检查初始化
□ System_Init()是否调用
□ Seg_Buf[]是否有有效值
3. 检查中断
□ Timer1Init()是否调用
□ EA和ET1是否使能
4. 检查扫描
□ Seg_Disp()函数是否正确
□ Seg_Pos是否在0-7范围
问题2:按键无响应
排查步骤:
1. 硬件检查
□ 按键是否正常
□ 上拉电阻是否存在
2. 代码检查
□ Key_Proc()是否在任务列表中
□ Key_Read()返回值是否正确
3. 调试方法
在Key_Proc()开头添加:
if (Key_Down != 0) {
ucLed[0] = 1;  // 按键按下时点亮L1
Led_Disp(ucLed);
}
问题3:时间不走
排查步骤:
1. DS1302检查
□ 芯片是否焊接良好
□ 晶振是否起振
□ 电池是否有电
2. 通信检查
□ Set_Rtc()是否成功写入
□ Read_Rtc()是否正确读取
3. 调试方法
在Get_Time()中添加:
Seg_Buf[0] = ucRtc[2] / 10;  // 显示秒
Seg_Buf[1] = ucRtc[2] % 10;
// 观察秒是否跳动
问题4:闹钟不响
排查步骤:
1. 设置检查
□ 闹钟时间是否设置正确
□ ucAlarm[]数组值是否正确
2. 触发检查
□ Get_Time()是否正常运行
□ 时间比较逻辑是否正确
3. 闪烁检查
□ Alarming标志是否置1
□ 定时器中断是否工作
4. 调试方法
强制触发闹钟:
Alarming = 1;  // 在main()中临时添加

附录D:扩展功能建议

D.1 功能扩展思路

扩展1:添加日期显示
/* 数据结构 */
idata unsigned char ucDate[3] = {1, 1, 1};  // 年月日
/* 按键切换 */
case 0:  // 时间显示页面
if (Key_Down == 4)
Seg_Show_Mode = 4;  // 进入日期显示
break;
/* 显示处理 */
case 4:  // 日期显示
Seg_Buf[0] = ucDate[0] / 10;  // 年
Seg_Buf[1] = ucDate[0] % 10;
Seg_Buf[2] = 11;  // "-"
Seg_Buf[3] = ucDate[1] / 10;  // 月
Seg_Buf[4] = ucDate[1] % 10;
Seg_Buf[2] = 11;  // "-"
Seg_Buf[6] = ucDate[2] / 10;  // 日
Seg_Buf[7] = ucDate[2] % 10;
break;
扩展2:多组闹钟
/* 数据结构 */
idata unsigned char ucAlarm[3][3];  // 3组闹钟
idata unsigned char alarm_index = 0;  // 当前编辑的闹钟
/* 判断逻辑 */
void Get_Time()
{
Read_Rtc(ucRtc);
// 检查每组闹钟
for (alarm_index = 0; alarm_index < 3; alarm_index++)
{
if ((ucRtc[0] == ucAlarm[alarm_index][0]) &&
(ucRtc[1] == ucAlarm[alarm_index][1]) &&
(ucRtc[2] == ucAlarm[alarm_index][2]))
{
Alarming = 1;
break;
}
}
}
扩展3:闹钟音乐
/* 蜂鸣器发声 */
void Alarm_Music()
{
unsigned int freq[] = {262, 294, 330, 349, 392};  // 音符频率
unsigned char i;
for (i = 0; i < 5; i++)
{
Buzzer_Sound(freq[i], 200);  // 发声200ms
Delay_ms(50);                // 间隔50ms
}
}
/* 在闹钟触发时调用 */
if (Alarming)
{
Alarm_Music();
}
扩展4:整点报时
/* 判断是否整点 */
void Get_Time()
{
Read_Rtc(ucRtc);
// 整点报时(分秒都是00)
if ((ucRtc[1] == 0) && (ucRtc[2] == 0))
{
Hourly_Chime();
}
}
/* 报时函数 */
void Hourly_Chime()
{
unsigned char i;
for (i = 0; i < ucRtc[0]; i++)  // 报时次数=当前小时数
{
Buzzer_Sound(500, 100);
Delay_ms(200);
}
}
扩展5:秒表计时
/* 数据结构 */
idata unsigned int stopwatch_ms = 0;  // 毫秒计数
idata bit stopwatch_running = 0;     // 运行标志
/* 按键控制 */
case 5:  // S5启动/停止
stopwatch_running = !stopwatch_running;
break;
case 6:  // S6清零
stopwatch_ms = 0;
break;
/* 中断计时 */
void Timer1Isr(void) interrupt 3
{
if (stopwatch_running)
{
stopwatch_ms++;
if (stopwatch_ms >= 60000)  // 60秒归零
stopwatch_ms = 0;
}
}
/* 显示处理 */
unsigned char sec = stopwatch_ms / 1000;
unsigned char ms = (stopwatch_ms % 1000) / 10;
Seg_Buf[0] = sec / 10;
Seg_Buf[1] = sec % 10;
Seg_Buf[2] = 11;  // "-"
Seg_Buf[3] = ms / 10;
Seg_Buf[4] = ms % 10;

D.2 代码优化建议

优化1:宏定义提高可读性
/* 定义按键宏 */
#define KEY_S4  4
#define KEY_S5  5
#define KEY_S6  6
#define KEY_S7  7
/* 定义显示模式宏 */
#define MODE_TIME      0
#define MODE_SET_TIME  1
#define MODE_SET_ALARM 2
#define MODE_TEMP      3
/* 使用宏 */
switch (Key_Down)
{
case KEY_S7:
Seg_Show_Mode = MODE_SET_TIME;
break;
}
优化2:函数封装
/* 封装时间设置逻辑 */
void Time_Adjust(unsigned char mode, unsigned char *time_buf)
{
if (mode == 0)
time_buf[mode] = (++time_buf[mode]) % 24;
else
time_buf[mode] = (++time_buf[mode]) % 60;
}
/* 使用 */
case KEY_S5:
Time_Adjust(Change_Time_Mode, ucRtc_ctrl);
break;
优化3:减少全局变量
/* 改用结构体封装 */
typedef struct {
unsigned char mode;
unsigned char value[3];
unsigned char ctrl[3];
unsigned char change_pos;
} time_config_t;
idata time_config_t rtc_config = {0, {23,59,50}, {0,0,0}, 0};
idata time_config_t alarm_config = {0, {0,0,0}, {0,0,0}, 0};

附录E:比赛技巧

E.1 时间分配建议

总时间:4小时
┌────────────────────────────────────┐
│ 任务分配(70分题目)               │
├────────────────────────────────────┤
│ 0:00-0:15  读题、理解需求     15min│
│ 0:15-0:30  设计程序结构       15min│
│ 0:30-2:00  编写核心代码       90min│
│ 2:00-3:00  调试、测试功能     60min│
│ 3:00-3:30  完善、优化代码     30min│
│ 3:30-4:00  检查、备份工程     30min│
└────────────────────────────────────┘
重点:
✓ 先完成基础功能(50分)
✓ 再完成扩展功能(20分)
✓ 留足调试时间
✓ 定期保存代码

E.2 答题策略

策略1:分步实现
第1步:显示功能(最基础)
第2步:按键功能
第3步:时钟功能
第4步:闹钟功能
策略2:模块测试
每完成一个模块立即测试
不要等全部完成再测试
策略3:备份代码
每完成一个功能就备份
避免后面改错导致全部重来
策略4:注释清晰
关键代码添加注释
方便后续调试和检查

E.3 常见失分点

❌ 失分点1:初始化不完整
   解决:严格按照题目初始化所有外设
❌ 失分点2:边界条件处理
   解决:23→0,59→0的循环要测试
❌ 失分点3:闪烁效果不准确
   解决:用定时器精确控制时间
❌ 失分点4:按键响应不及时
   解决:10ms周期扫描,软件消抖
❌ 失分点5:代码可读性差
   解决:添加注释,变量命名规范
❌ 失分点6:忘记关闭无关外设
   解决:System_Init()中全部处理

结语:从零基础到蓝桥杯

你已经学会了什么

✅ 单片机开发完整流程
   └─ 需求分析 → 程序设计 → 编码实现 → 调试优化
✅ 核心编程技能
   └─ 数组、指针、结构体、函数指针
   └─ 状态机、任务调度、中断处理
✅ 硬件驱动能力
   └─ 数码管、LED、按键、DS1302、DS18B20
✅ 调试能力
   └─ 问题定位、错误排查、功能测试
✅ 项目经验
   └─ 完整的电子钟系统开发

接下来的学习路径

第1阶段:巩固基础 ⭐⭐⭐⭐⭐
  □ 把本项目完整实现一遍
  □ 尝试修改参数观察效果
  □ 添加简单的扩展功能
第2阶段:专项训练 ⭐⭐⭐⭐
  □ 历年蓝桥杯真题练习
  □ 重点练习常见外设驱动
  □ 提高代码编写速度
第3阶段:综合提升 ⭐⭐⭐
  □ 学习模拟电路基础
  □ 了解常用通信协议
  □ 掌握Keil调试技巧
第4阶段:参赛冲刺 ⭐⭐
  □ 模拟考试环境练习
  □ 总结常用代码模板
  □ 熟练使用开发板

给初学者的忠告

 心态篇
  • 不要害怕出错,错误是最好的老师
  • 不要追求完美,先跑通再优化
  • 不要死记硬背,理解原理最重要
 学习篇
  • 多动手实践,看10遍不如写1遍
  • 遇到问题先自己思考3分钟
  • 善用搜索引擎,学会找答案
 比赛篇
  • 时间管理很重要,合理分配
  • 基础分一定要拿稳
  • 代码要规范,注释要清晰
 心理篇
  • 遇到困难很正常,不要放弃
  • 每天进步一点点,日积月累
  • 相信自己,你一定可以!

最后的话

┌─────────────────────────────────────────────┐
│                                             │
│  "The best way to predict the future       │
│   is to create it."                        │
│                                             │
│  预测未来的最好方法就是创造它。             │
│                                             │
│  - Abraham Lincoln                          │
│                                             │
│  现在你已经掌握了创造的工具,               │
│  剩下的就是动手实践了!                     │
│                                             │
│  加油,未来的嵌入式工程师!                 │
│                                             │
└─────────────────────────────────────────────┘

特别感谢:
• 米醋电子工作室提供的学习资源
• 所有支持和鼓励的老师和同学们
• 正在阅读这份教程的你

祝你在蓝桥杯比赛中取得优异成绩!
祝你在嵌入式学习之路上越走越远!


版本信息

  • 教程版本:v1.0
  • 适用对象:IAP15F2K61S2单片机初学者
  • 对应赛题:第八届蓝桥杯单片机省赛(电子钟)

版权声明

本教程中的代码和资料来源于米醋电子工作室提供的底层模板。

本教程仅供学习交流使用,未经许可不得用于商业用途。

如需转载或引用,请注明出处。


恭喜你完成了整个教程的学习!现在就开始你的单片机之旅吧!