蓝桥杯第五周之第二讲

蓝桥杯单片机备赛指南- 第十一讲: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/16Data%16
    • 写入时:如果要写入23,不能直接写整数23,要写入0x23(即2*16 + 3)。

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)技术,将数百行冗余代码压缩为通用的几十行,实现真正的工程化代码架构。


五、基于指针与内存操作的代码优化

一、 优化核心思想:抽象与复用

观察本题的三个功能模块(时钟、闹钟、日期),它们的逻辑高度相似:

  1. 数据结构相似:都是3 个字节(时分秒/ 年月日)。
  2. 操作逻辑相似:进入设置-> 拷贝数据到缓存-> 修改缓存-> 校验合法性-> 保存回原变量。
  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循环。
  • 优化后,只需要区分“显示态”和“设置态”两种情况。无论未来增加多少个功能(比如秒表、倒计时),只要数据结构一样,这段显示代码甚至不需要改动一行!

六、 总结:为什么要这样写?

在蓝桥杯省赛/国赛中,时间非常紧迫。

  1. 减少代码量:从几百行压缩到几十行,意味着出错的概率成倍降低。
  2. 便于记忆:只需记住unsigned char* Ptr[3] = {A, B, C};这种定义方式,配合memcpy,即可通杀所有多界面题目。
  3. 极易扩展:如果题目突然加一个“第二时钟”,你只需要修改Set_Flag数组和ucSecondClock变量,逻辑代码自动适配。

给读者的建议:请务必在电脑上亲自敲一遍main.c中的Key_ProcSeg_Proc,体会指针数组带来的逻辑美感。这是从“写出代码”到“写好代码”的关键一步。