蓝桥杯单片机备赛指南- 第十一讲:DS1302实时时钟
一、 DS1302 硬件原理基础
DS1302 是蓝桥杯板载的一款低功耗实时时钟芯片,它能提供秒、分、时、日、月、周、年等信息,并能自动调整闰年。
1. 核心引脚与通信协议
DS1302 使用非标准的三线串行接口(SPI-like)进行通信:
- CE (RST):复位/片选脚。读写数据时必须保持高电平,操作结束拉低。
- SCLK (SCK):串行时钟脚。
- 写入数据:在SCLK 的上升沿数据被写入芯片。
- 读取数据:在SCLK 的下降沿数据从芯片输出。
- I/O (SDA):双向数据线,用于传输指令和数据。
2. 寄存器与BCD 码(重中之重)
DS1302 内部的时间数据是以**BCD 码(Binary-Coded Decimal)**存储的,而不是我们常用的十进制。
- **什么是BCD 码?**用4 位二进制数表示1 位十进制数。
- 例如:59 秒。
- 十进制:
59 - 十六进制(Hex):
0x3B - BCD 码:
0x59(二进制0101 1001)
- 编程启示:
- 读取时:从DS1302 读出的
0x59,如果要分离出十位5和个位9,直接用位运算:Data/16和Data%16。 - 写入时:如果要写入
23,不能直接写整数23,要写入0x23(即2*16 + 3)。
- 读取时:从DS1302 读出的
3. 寄存器地址分布
- 写保护寄存器(0x8E):在写入时间前,必须向0x8E 写入0x00 (关闭写保护);写完后,写入0x80 (打开写保护)。
- 时间寄存器:
- 秒:0x80 (写) / 0x81 (读)
- 分:0x82 (写) / 0x83 (读)
- 时:0x84 (写) / 0x85 (读)
- 日:0x86 (写) / 0x87 (读)
- 月:0x88 (写) / 0x89 (读)
- 年:0x8C (写) / 0x8D (读)
二、 底层驱动详解( ds1302.c& ds1302.h)
以下代码完整取自您提供的ds1302.c,并逐段进行详细讲解。
1. 引脚定义与基础读写
这部分是官方提供的基础时序代码,通常不需要修改,但需要理解。
C
#include <reg52.h>
#include <intrins.h>
sbit SCK=P1^7; // 时钟线
sbit SDA=P2^3; // 数据线
sbit RST=P1^3; // 复位/片选线
// 单字节写入函数 (底层时序)
static void Write_Ds1302(unsigned char temp)
{
unsigned char i;
for (i=0;i<8;i++)
{
SCK = 0;
SDA = temp&0x01; // 先发低位
temp>>=1;
SCK=1; // 上升沿写入
}
}
// 向指定地址写入数据
static void Write_Ds1302_Byte( unsigned char address,unsigned char dat )
{
RST=0; _nop_();
SCK=0; _nop_();
RST=1; _nop_(); // 1. 拉高 RST 启动通信
Write_Ds1302(address); // 2. 写入目标地址
Write_Ds1302(dat); // 3. 写入数据
RST=0; // 4. 拉低 RST 结束通信
}
// 从指定地址读取数据
static unsigned char Read_Ds1302_Byte ( unsigned char address )
{
unsigned char i,temp=0x00;
RST=0; _nop_();
SCK=0; _nop_();
RST=1; _nop_();
Write_Ds1302(address); // 1. 写入读指令
for (i=0;i<8;i++)
{
SCK=0; // 下降沿读取
temp>>=1;
if(SDA)
temp|=0x80;
SCK=1;
}
RST=0; _nop_();
SCK=0; _nop_();
SCK=1; _nop_();
SDA=0; _nop_();
SDA=1; _nop_(); // 释放总线
return (temp);
}
2. 应用层:设置时间( Set_Rtc)
这是您编写的封装函数,用于一次性写入时、分、秒。
C
void Set_Rtc(unsigned char*Rtc){
unsigned char i=0; //循环
Write_Ds1302_Byte(0x8E,0); // 【关键】必须先关闭写保护
for(i=0;i<3;i++){
// 地址计算技巧:
// i=0 -> 写入 Rtc[0] (时) -> 地址 0x84
// i=1 -> 写入 Rtc[1] (分) -> 地址 0x82
// i=2 -> 写入 Rtc[2] (秒) -> 地址 0x80
Write_Ds1302_Byte(0x84-2*i,Rtc[i]);
}
Write_Ds1302_Byte(0x8E,1); // 【关键】写完后开启写保护
}
3. 应用层:读取时间( Read_Rtc)
用于一次性读出时、分、秒。
C
void Read_Rtc(unsigned char*Rtc){
unsigned char i=0; //循环
for(i=0;i<3;i++){
// 地址计算技巧:
// i=0 -> 读取 0x85 (时) -> 存入 Rtc[0]
// i=1 -> 读取 0x83 (分) -> 存入 Rtc[1]
// i=2 -> 读取 0x81 (秒) -> 存入 Rtc[2]
Rtc[i]=Read_Ds1302_Byte(0x85-2*i);
}
}
4. 应用层:日期操作( Set_Date/ Read_Date)
单独处理年、月、日。
C
void Set_Date(unsigned char*Date){
Write_Ds1302_Byte(0x8E,0); // 关写保护
Write_Ds1302_Byte(0x8C,Date[0]); // 年 0x8C
Write_Ds1302_Byte(0x88,Date[1]); // 月 0x88
Write_Ds1302_Byte(0x86,Date[2]); // 日 0x86
Write_Ds1302_Byte(0x8E,1); // 开写保护
}
void Read_Date(unsigned char*Date){
Date[0]=Read_Ds1302_Byte(0x8D); // 年 0x8D
Date[1]=Read_Ds1302_Byte(0x89); // 月 0x89
Date[2]=Read_Ds1302_Byte(0x87); // 日 0x87
}
三、 程序设计要求(PDF还原)
本节内容完全还原您提供的PDF 文档要求。
1. 数码管模块
1) 时钟显示界面
显示当前运行时钟时间。
| 小时 | 间隔符 | 分钟 | 间隔符 | 秒钟 | ||||
|---|---|---|---|---|---|---|---|---|
| 2 | 3 | - | 5 | 9 | - | 5 5 |
2) 时钟设置界面
显示当前设置时钟时间。选中的设置单元以0.5秒/次的速度闪烁。
| 小时 | 间隔符 | 分钟 | 间隔符 | 秒钟 | ||||
|---|---|---|---|---|---|---|---|---|
| 2 | 3 | - | 5 | 9 | - | 5 5 |
3) 闹钟查看界面
显示当前所处闹钟时间。
| 小时 | 间隔符 | 分钟 | 间隔符 | 秒钟 | ||||
|---|---|---|---|---|---|---|---|---|
| 0 | 0 | - | 0 | 0 | - | 0 0 |
4) 闹钟设置界面
显示当前设置闹钟时间。选中的设置单元以0.5秒/次的速度闪烁。
| 小时 | 间隔符 | 分钟 | 间隔符 | 秒钟 | ||||
|---|---|---|---|---|---|---|---|---|
| 0 | 0 | - | 0 | 0 | - | 0 0 |
5) 日期显示界面
显示当前运行日期数据。
| 年 | 间隔符 | 月 | 间隔符 | 日 | ||||
|---|---|---|---|---|---|---|---|---|
| 2 | 2 | - | 1 | 2 | - | 1 5 |
6) 日期设置界面
显示当前设置日期数据。选中的设置单元以0.5秒/次的速度闪烁。
2. 按键模块(矩阵键盘)
- S4-S13 : 0-9 键盘输入矩阵。
- S14 (界面切换) : 在非参数设置界面有效,按下后按照“时钟-> 闹钟-> 日期”顺序循环切换显示模式。
- S15 (参数设置/确认) :
- 在显示界面按下:跳转到当前参数的设置模式。
- 在设置界面按下:若数据合理,保存并跳转回显示界面;若不合理,则重新开始输入。
- S16 (设置退出) : 仅在参数设置界面有效,不保存数据,直接跳转回当前参数显示模式。
- S17 (闹钟切换) : 仅在闹钟相关界面有效,切换当前选中的闹钟编号(1->2->3)。
- S18 (闹钟功能开关) : 仅在非参数设置界面有效,关闭/开启闹钟功能。
- S19 (闹钟删除) : 仅在闹钟设置界面有效,删除当前设置的闹钟(全部显示为
17即灭)。
3. LED 模块
- L1 (闹钟提醒) : 闹钟使能时触发,以0.2秒为间隔闪烁,持续5秒,直到任意按键按下。
- L2-L4 (闹钟指示) : 指示当前选中的闹钟。 L2 代表闹钟1,L3 代表闹钟2,L4 代表闹钟3。
- L8 (有效性指示) : 指示当前选中的闹钟是否有效。如果被删除则灭,有效则亮。
4. 初始状态
- 默认时间:23:59:55
- 闹钟1:00:00:00;闹钟2:00:01:00;闹钟3:未启用。
- 默认日期:22-12-12。
- 默认闹钟处于开启状态。
四、 程序设计与代码全解( main.c)
本程序逻辑复杂,涉及多层界面跳转、数据校验(闰年)、闹钟触发等。我们将通过完整的main.c代码来剖析。
(Triggered: Helps visualize the flow of Time_Judge and Key_Proc)
1. 头文件与全局变量定义
程序采用了**“显示数据”与“设置缓存”**分离的策略。
Rtc[]: 存储从DS1302 读来的BCD 码。Rtc_Set[]: 存储用户正在输入的十进制位(数组大小为6,分别对应时十位、时个位…)。这样设计方便修改每一位数字。
C
//头文件
#include <STC15F2K60S2.H>
#include "Init.h"
#include "Key.h"
#include "LED.h"
#include "Seg.h"
#include "ds1302.h"
//变量声明
typedef unsigned char u8;
typedef unsigned int u16;
u8 Key_Slow_Down=0; //键盘减速
u16 Seg_Slow_Down=0; //数码管减速
u8 Seg_Pos=0; //数码管扫描指针
u8 Seg_Buf[8]={10,10,10,10,10,10,10,10}; //数码管扫描数组 (10代表熄灭)
u8 Seg_Point[8]={0,0,0,0,0,0,0,0}; //小数点数组
// LED相关
u8 LED_Pos=0;
u8 LED_Buf[8]={0,0,0,0,0,0,0,0};
// 按键相关
u8 Key_Val=0,Key_Old=0,Key_Down=0,Key_Up=0;
// 时间与日期数据 (BCD格式)
u8 Rtc[3]={0x23,0x59,0x55};
u8 Date[3]={0x22,0x12,0x12};
// 设置缓存 (十进制拆解格式,长度6)
u8 Rtc_Set[6]={0};
u8 Rtc_Set_Index=0; // 光标位置
u8 Date_Set[6]={0};
u8 Date_Set_Index=0;
// 界面模式: 1时钟 2闹钟 3日期 4时钟设置 5闹钟设置 6日期设置
u8 Seg_Mode=1;
// 闹钟相关
u8 Alarm_Flag[3]={1,1,0}; // 3个闹钟的独立开关 (1开0关)
u8 Alarm_Mode=0; // 当前选中的闹钟索引 (0,1,2)
u8 Alarm[3][3]={ // 3个闹钟的时间 (BCD格式)
0x00,0x00,0x00,
0x00,0x01,0x00,
0x00,0x00,0x00
};
u8 Alarm_Set[3][6]={0}; // 闹钟设置缓存
u8 Alarm_Set_Index=0;
// 闪烁与计时标志
bit Flash_Flag=0; // 0亮1不亮 (500ms翻转)
bit All_Alarm_Flag=1; // 闹钟总开关
u8 Seg_Flash=0;
u16 Time_500ms=0;
bit Alarm_Start=0; // 闹钟触发标志
bit LED1_Flash=0; // L1闪烁标志
u8 Time_200ms=0;
u8 Time_5s_Count=0;
2. 辅助函数:闰年判断与合法性校验
这是本题的逻辑核心。用户输入的时间必须合法才能保存。
Mode=0:校验时间(23:59:59)。Mode=1:校验日期(大小月、闰年2月)。
C
//闰年判断
bit leap_year(u16 year) {
if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
return 1;
else
return 0;
}
//时间合理性判断
// Time: 传入的设置数组(长度6的十进制位)
// Mode: 0-校验时间, 1-校验日期
bit Time_Judge(u8*Time, bit Mode){
u8 i=0;
u8 Time_Temp[3]; // 临时存合成后的 时/分/秒 或 年/月/日
u16 Year_Temp;
// 1. 将两位拆解数组合成十进制数
for(i=0;i<3;i++){
Time_Temp[i]=Time[2*i]*10+Time[2*i+1];
}
if(Mode){ // === 日期校验 ===
// 检查月份(1-12)和日期(>0)
if(Time_Temp[1]<=12 && Time_Temp[1]>0 && Time_Temp[2]>0){
Year_Temp=Time_Temp[0]+2000; // 补充年份为20xx
// 4,6,9,11月 -> 30天
if(Time_Temp[1]==4||Time_Temp[1]==6||Time_Temp[1]==9||Time_Temp[1]==11){
return !(Time_Temp[2]>30); // 如果>30返回0(非法)
}
// 2月 -> 闰年29, 平年28
else if(Time_Temp[1]==2){
if(leap_year(Year_Temp)){
return !(Time_Temp[2]>29);
}else{
return !(Time_Temp[2]>28);
}
}else{
// 其他月份 -> 31天
return !(Time_Temp[2]>31);
}
} else { return 0; } // 月份非法
}else{ // === 时间校验 ===
if(Time_Temp[0]<=23 && Time_Temp[1]<=59 && Time_Temp[2]<=59){
return 1; // 合法
}else{
return 0; // 非法
}
}
}
3. 按键处理进程( Key_Proc)
处理极其复杂的界面跳转和数据录入。
C
void Key_Proc(){
u8 i=0,j=0;
if(Key_Slow_Down) return; Key_Slow_Down=1; // 10ms消抖
Key_Val=Key_Read();
Key_Down=Key_Val&(Key_Val^Key_Old);
Key_Up=~Key_Val&(Key_Val^Key_Old);
Key_Old=Key_Val;
// 任意按键按下,停止闹钟响铃
if(Key_Down){
Alarm_Start=0;
LED1_Flash=0;
}
// S4-S13 (数字键) 输入处理
// 仅在 Mode>=4 (设置模式) 有效
if(Key_Down>=4 && Key_Down<=13 && Seg_Mode>=4){
switch(Seg_Mode){
case 4: // 时钟设置
Rtc_Set[Rtc_Set_Index]=Key_Down-4; // 填入数字
if(++Rtc_Set_Index==6) Rtc_Set_Index=0; // 光标循环
break;
case 5: // 闹钟设置
Alarm_Set[Alarm_Mode][Alarm_Set_Index]=Key_Down-4;
if(++Alarm_Set_Index==6) Alarm_Set_Index=0;
break;
case 6: // 日期设置
Date_Set[Date_Set_Index]=Key_Down-4;
if(++Date_Set_Index==6) Date_Set_Index=0;
break;
}
}
switch(Key_Down){
case 14: // S14 界面切换 (非设置模式)
if(Seg_Mode<=3){
if(++Seg_Mode==4) Seg_Mode=1; // 1->2->3->1
}
break;
case 15: // S15 进入设置 / 确认保存
if(Seg_Mode<=3){
// === 进入设置 ===
Seg_Mode+=3; // 1->4, 2->5, 3->6
switch(Seg_Mode){
case 4: // 将当前 BCD 时间拆解为十进制数组供编辑
for(i=0;i<3;i++){
Rtc_Set[2*i]=Rtc[i]/16;
Rtc_Set[2*i+1]=Rtc[i]%16;
}
Rtc_Set_Index=0;
break;
case 5: // 载入当前闹钟
for(i=0;i<3;i++){
for(j=0;j<3;j++){
Alarm_Set[i][2*j]=Alarm[i][j]/16;
Alarm_Set[i][2*j+1]=Alarm[i][j]%16;
}
}
Alarm_Set_Index=0;
break;
case 6: // 载入当前日期
for(i=0;i<3;i++){
Date_Set[2*i]=Date[i]/16;
Date_Set[2*i+1]=Date[i]%16;
}
Date_Set_Index=0;
break;
}
}else{
// === 确认保存 ===
switch(Seg_Mode){
case 4: // 保存时间
if(Time_Judge(Rtc_Set,0)){ // 校验合法性
for(i=0;i<3;i++){
Rtc[i]=Rtc_Set[2*i]*16+Rtc_Set[2*i+1]; // 合成BCD
}
Seg_Mode=1; // 返回显示
Set_Rtc(Rtc); // 写入芯片
}else{
// 不合法:重置数据,光标归零,不退出
for(i=0;i<3;i++){
Rtc_Set[2*i]=Rtc[i]/16;
Rtc_Set[2*i+1]=Rtc[i]%16;
}
Rtc_Set_Index=0;
}
break;
case 5: // 保存闹钟 (类似逻辑)
if(Time_Judge(Alarm_Set[0],0)&&Time_Judge(Alarm_Set[1],0)&&Time_Judge(Alarm_Set[2],0)){
for(i=0;i<3;i++){
for(j=0;j<3;j++){
Alarm[i][j]=Alarm_Set[i][2*j]*16+Alarm_Set[i][2*j+1];
}
}
Seg_Mode=2;
}else{
// ... 重置逻辑 ...
Alarm_Set_Index=0;
}
break;
case 6: // 保存日期
if(Time_Judge(Date_Set,1)){
for(i=0;i<3;i++){
Date[i]=Date_Set[2*i]*16+Date_Set[2*i+1];
}
Seg_Mode=3;
Set_Date(Date);
}else{
// ... 重置逻辑 ...
Date_Set_Index=0;
}
break;
}
}
break;
case 16: // S16 取消 (不保存直接返回)
if(Seg_Mode>3)
Seg_Mode-=3;
break;
case 17: // S17 切换闹钟 ID (1->2->3)
if(Seg_Mode==2||Seg_Mode==5){
if(++Alarm_Mode==3) Alarm_Mode=0;
}
break;
case 18: // S18 闹钟总开关
if(Seg_Mode<=3){
All_Alarm_Flag^=1;
if(All_Alarm_Flag == 0){ // 关闭时立即停止响铃
Alarm_Start = 0;
LED1_Flash = 0;
Time_5s_Count = 0;
}
}
break;
case 19: // S19 删除闹钟 (仅在设置模式)
if(Seg_Mode==5){
Alarm_Flag[Alarm_Mode]=1-Alarm_Flag[Alarm_Mode]; // 翻转有效标志
}
break;
}
}
4. 数码管显示进程( Seg_Proc)
处理6 种模式的显示,特别是设置模式下的闪烁。
C
void Seg_Proc(){
u8 i=0;
if(Seg_Slow_Down) return; Seg_Slow_Down=1;
// 实时读取时间用于显示
Read_Rtc(Rtc);
Read_Date(Date);
Seg_Buf[2]=Seg_Buf[5]=17; // 设置间隔符 (-, 在Seg.c中17对应-)
switch(Seg_Mode){
case 1: // 时钟显示 (直接显示 Rtc 数组的高低位)
for(i=0;i<3;i++){
Seg_Buf[3*i]=Rtc[i]/16;
Seg_Buf[3*i+1]=Rtc[i]%16;
}
break;
case 2: // 闹钟显示
if(Alarm_Flag[Alarm_Mode]){ // 如果闹钟有效
for(i=0;i<3;i++){
Seg_Buf[3*i]=Alarm[Alarm_Mode][i]/16;
Seg_Buf[3*i+1]=Alarm[Alarm_Mode][i]%16;
}
}else{ // 如果闹钟被删除/无效,显示全熄灭(或---)
for(i=0;i<8;i++){
Seg_Buf[i]=17; // 17是横杠
}
}
break;
case 3: // 日期显示
for(i=0;i<3;i++){
Seg_Buf[3*i]=Date[i]/16;
Seg_Buf[3*i+1]=Date[i]%16;
}
break;
case 4: // 时钟设置 (含闪烁逻辑)
for(i=0;i<3;i++){
// 核心闪烁逻辑:(Flash_Flag && 当前位是光标位) ? 熄灭 : 显示
Seg_Buf[3*i]=(Flash_Flag&&Rtc_Set_Index==2*i)?10:Rtc_Set[2*i];
Seg_Buf[3*i+1]=(Flash_Flag&&Rtc_Set_Index==2*i+1)?10:Rtc_Set[2*i+1];
}
break;
// case 5, 6 逻辑同 case 4
case 5:
if(Alarm_Flag[Alarm_Mode]){
for(i=0;i<3;i++){
Seg_Buf[3*i]=(Flash_Flag&&Alarm_Set_Index==2*i)?10:Alarm_Set[Alarm_Mode][2*i];
Seg_Buf[3*i+1]=(Flash_Flag&&Alarm_Set_Index==2*i+1)?10:Alarm_Set[Alarm_Mode][2*i+1];
}
}else{
for(i=0;i<8;i++) Seg_Buf[i]=17;
}
break;
case 6:
for(i=0;i<3;i++){
Seg_Buf[3*i]=(Flash_Flag&&Date_Set_Index==2*i)?10:Date_Set[2*i];
Seg_Buf[3*i+1]=(Flash_Flag&&Date_Set_Index==2*i+1)?10:Date_Set[2*i+1];
}
break;
}
}
5. LED 与中断处理
- LED_Proc : 检测闹钟触发条件(RTC == Alarm),并控制L2-L4。
- Timer0_Isr : 提供闪烁时基,扫描数码管。
C
void LED_Proc(){
u8 i=0;
// 1. 闹钟触发检测
if(All_Alarm_Flag){
for(i=0;i<3;i++){
// 必须:闹钟有效 && 时分秒完全相等
if(Alarm_Flag[i]&&Alarm[i][0]==Rtc[0]&&Alarm[i][1]==Rtc[1]&&Alarm[i][2]==Rtc[2]){
Alarm_Start=1;
}
}
}
LED_Buf[0]=(LED1_Flash)?1:0; // L1 随闪烁标志亮灭
// 2. 闹钟 ID 指示 (L2-L4)
for(i=1;i<4;i++){
// 只有在闹钟界面(2或5)才显示
LED_Buf[i]=(Alarm_Mode+1==i)*(Seg_Mode==2||Seg_Mode==5);
}
// 3. 闹钟有效性指示 (L8)
LED_Buf[7]=(Seg_Mode==2||Seg_Mode==5)*(Alarm_Flag[Alarm_Mode]);
}
void Timer0_Isr(void) interrupt 1
{
// ... 减速代码 ...
// ... 扫描 Seg_Disp, LED_Disp ...
// 闪烁时基 (500ms) - 用于数码管设置位闪烁
if(Seg_Mode>3){
if(++Time_500ms==500){
Time_500ms=0;
Flash_Flag^=1;
}
}
// 闹钟 L1 闪烁控制 (200ms)
if(Alarm_Start&&All_Alarm_Flag){
if(++Time_200ms==200){
Time_200ms=0;
LED1_Flash^=1;
// 5秒倒计时 (25 * 200ms = 5000ms)
if(++Time_5s_Count==25){
Time_5s_Count=0;
Alarm_Start=0; // 停止闹钟
LED1_Flash=0;
}
}
}
}
void main(){
System_Init();
Timer0_Init();
Set_Rtc(Rtc); // 写入初始时间
Set_Date(Date); // 写入初始日期
while(1){
Key_Proc();
Seg_Proc();
LED_Proc();
}
}
我们完成了基础功能的实现。但在面对像DS1302 电子钟这样涉及“时钟、闹钟、日期”三个极其相似界面的题目时,如果采用传统的“switch-case 堆砌法”(即分别为时钟写一套逻辑、闹钟写一套、日期写一套),代码量会爆炸且极难调试。
之后引导您利用指针数组和内存拷贝( memcpy)技术,将数百行冗余代码压缩为通用的几十行,实现真正的工程化代码架构。
五、基于指针与内存操作的代码优化
一、 优化核心思想:抽象与复用
观察本题的三个功能模块(时钟、闹钟、日期),它们的逻辑高度相似:
- 数据结构相似:都是3 个字节(时分秒/ 年月日)。
- 操作逻辑相似:进入设置-> 拷贝数据到缓存-> 修改缓存-> 校验合法性-> 保存回原变量。
- 显示逻辑相似:都是显示3 组数据。
优化目标:不再编写三套代码,而是编写一套通用逻辑,通过“指针”指向不同的数据源。
二、 优化手段I:指针数组统一数据源
传统写法需要通过switch(Mode)来判断操作哪个数组( Rtc, Alarm, Date)。优化写法定义一个指针数组,将这三个数组的地址存起来,通过索引直接访问。
1. 代码实现(main.c变量声明区)
C
// 原始数据数组
unsigned char ucRtc[3] = {0x23,0x59,0x55}; // 时钟
unsigned char ucAlarm[9] = {0x00,0x00,0x00, ...}; // 闹钟 (3组)
unsigned char ucDate[3] = {0x22,0x12,0x12}; // 日期
// 【核心优化】指针数组
// Set_Flag[0] 指向 ucRtc
// Set_Flag[1] 指向 ucAlarm
// Set_Flag[2] 指向 ucDate
unsigned char* Set_Flag[3] = {ucRtc,ucAlarm,ucDate};
// 【核心优化】通用设置缓存数组
// 不再定义 Rtc_Set, Alarm_Set, Date_Set,而是公用一个 Set_Dat
// 大小定义为9,足以容纳最大的数据块(闹钟有3组共9字节)
unsigned char Set_Dat[9];
2. 优势分析
- 以前:需要写
if(mode==1) Rtc[i]... else if(mode==2) Alarm[i]... - 现在:直接写
Set_Flag[mode][i],一行代码搞定所有模式的数据读取。
三、 优化手段II:利用memcpy简化数据加载
在进入设置界面时,需要将“当前数据”备份到“设置缓存”。传统写法需要for循环逐个赋值。优化写法使用C 标准库<string.h>中的memcpy。
1. 代码实现( Key_Proc → case 15)
C
#include <string.h> // 必须包含头文件
case 15: // 参数设置按键
if(Seg_Disp_Mode < 3) // 在显示界面按下 -> 进入设置
{
Seg_Disp_Mode += 3; // 切换到对应的设置界面 (0->3, 1->4, 2->5)
Alarm_Dat_Index = 0; // 闹钟指针复位
// 【核心优化代码】
// 一行代码代替巨大的 switch-case
// 逻辑:将 Set_Flag[i] 指向的源数据,拷贝 9 个字节到 Set_Dat 缓存
// 解释:Seg_Disp_Mode-3 正好对应指针数组的下标 0,1,2
memcpy(Set_Dat, Set_Flag[Seg_Disp_Mode-3], 9);
}
2. 优势分析
这行memcpy完美替代了以下冗余代码:
C
// 被优化掉的垃圾代码:
switch(Seg_Disp_Mode) {
case 3: for(i=0;i<3;i++) Set_Dat[i] = ucRtc[i]; break;
case 4: for(i=0;i<9;i++) Set_Dat[i] = ucAlarm[i]; break;
case 5: for(i=0;i<3;i++) Set_Dat[i] = ucDate[i]; break;
}
四、 优化手段III:通用的键盘输入逻辑
在设置参数时,无论是在改时间、改闹钟还是改日期,本质上都是修改缓存数组Set_Dat中的某一位。
1. 代码实现(Key_Proc输入区域)
C
/* 键盘输入区域 */
// 只要是数字键 (4-13) 且光标未溢出
if(Key_Down >= 4 && Key_Down <= 13 && Input_Index < 3)
{
if(Seg_Disp_Mode >= 3) // 处于设置状态
{
// 计算目标索引:Input_Index (当前位) + 3*Alarm_Dat_Index (闹钟偏移)
// 对于时钟和日期,Alarm_Dat_Index 始终为 0,不影响逻辑
unsigned char target_index = Input_Index + 3*Alarm_Dat_Index;
if(Input_Flag == 0) // 输入高位 (BCD码的十位)
Set_Dat[target_index] = ((Key_Down - 4) << 4) | (Set_Dat[target_index] & 0x0f);
else // 输入低位 (BCD码的个位)
Set_Dat[target_index] = (Set_Dat[target_index] & 0xf0) | (Key_Down - 4);
Input_Flag ^= 1; // 切换高低位
if(Input_Flag == 0) // 输完一组(两位),光标后移
Input_Index++;
}
}
2. 优势分析
这段代码完全没有出现 if(mode==时钟)或if(mode==闹钟)的判断。
- 如果是时钟/日期设置:
Alarm_Dat_Index为0,操作Set_Dat[0]~[2]。 - 如果是闹钟设置:通过
Alarm_Dat_Index(0,1,2) 自动偏移,操作Set_Dat[0]~[8]。 - 结果:一套逻辑通吃所有数据的键盘录入。
五、 优化手段IV:通用的显示处理
显示函数通常是代码量的“重灾区”。通过指针数组,我们可以将显示逻辑压缩到极致。
1. 代码实现( Seg_Proc)
C
void Seg_Proc()
{
// ... (读取时间代码略) ...
if(Seg_Disp_Mode < 3) // === 处于显示界面 (只读) ===
{
for(i=0; i<3; i++)
{
// 【核心优化】直接从 Set_Flag 指向的源数组读取数据
// Set_Flag[Seg_Disp_Mode] 自动指向 ucRtc / ucAlarm / ucDate
// i + 3*Alarm_Dat_Index 自动处理闹钟的偏移
Seg_Buf[3*i] = Set_Flag[Seg_Disp_Mode][i+3*Alarm_Dat_Index] / 16;
Seg_Buf[3*i+1] = Set_Flag[Seg_Disp_Mode][i+3*Alarm_Dat_Index] % 16;
}
}
else // === 处于设置界面 (读缓存 Set_Dat) ===
{
// 1. 常规显示:显示 Set_Dat 中的数据
for(i=0; i<3; i++)
{
Seg_Buf[3*i] = Set_Dat[i+3*Alarm_Dat_Index] / 16;
Seg_Buf[3*i+1] = Set_Dat[i+3*Alarm_Dat_Index] % 16;
}
// 2. 闪烁处理:仅覆盖当前光标位置 Input_Index
// 如果 Seg_Star_Flag (闪烁标志) 为真,则显示数据;否则显示 10 (熄灭)
// 这里的逻辑稍微反了一下:Seg_Star_Flag ? 数据 : 熄灭
Seg_Buf[3*Input_Index] = Seg_Star_Flag ? (Set_Dat[Input_Index+3*Alarm_Dat_Index]/16) : 10;
Seg_Buf[3*Input_Index+1] = Seg_Star_Flag ? (Set_Dat[Input_Index+3*Alarm_Dat_Index]%16) : 10;
}
}
2. 优势分析
- 如果不优化,你需要写6 个
case,每个case里写一个for循环。 - 优化后,只需要区分“显示态”和“设置态”两种情况。无论未来增加多少个功能(比如秒表、倒计时),只要数据结构一样,这段显示代码甚至不需要改动一行!
六、 总结:为什么要这样写?
在蓝桥杯省赛/国赛中,时间非常紧迫。
- 减少代码量:从几百行压缩到几十行,意味着出错的概率成倍降低。
- 便于记忆:只需记住
unsigned char* Ptr[3] = {A, B, C};这种定义方式,配合memcpy,即可通杀所有多界面题目。 - 极易扩展:如果题目突然加一个“第二时钟”,你只需要修改
Set_Flag数组和ucSecondClock变量,逻辑代码自动适配。
给读者的建议:请务必在电脑上亲自敲一遍main.c中的Key_Proc和Seg_Proc,体会指针数组带来的逻辑美感。这是从“写出代码”到“写好代码”的关键一步。