江科大 51 单片机课程笔记:DS1302 实时时钟 + LCD1602 可调电子钟(小白逐行讲解)

1. 写在前面:这篇笔记适合谁

如果你正在跟江科大(江协科技)的 51 单片机课程,学到 DS1302 实时时钟 + LCD1602 显示 + 按键调时 这一节,可能会被一堆 0x80、BCD、interrupt 1 搞得有点懵。

这篇笔记就是给「代码小白」准备的:不跳步、不假设你已经懂了,把整份工程拆成 6 个文件,一行一行讲清楚「这行代码在干嘛、为什么要这么写」。

说明:本文代码来自课程配套工程,笔记中的注释和讲解为个人理解整理,方便复习时对照。

2. 先看全局:这个电子钟到底在做什么

一句话概括:DS1302 芯片负责「记住时间」,LCD1602 负责「显示时间」,按键负责「调时间」,定时器负责「让选中的数字闪烁」。
#mermaid-svg-kBiRTQMhn39EgtQf{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kBiRTQMhn39EgtQf .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kBiRTQMhn39EgtQf .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kBiRTQMhn39EgtQf .error-icon{fill:#552222;}#mermaid-svg-kBiRTQMhn39EgtQf .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kBiRTQMhn39EgtQf .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kBiRTQMhn39EgtQf .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kBiRTQMhn39EgtQf .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kBiRTQMhn39EgtQf .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kBiRTQMhn39EgtQf .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kBiRTQMhn39EgtQf .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kBiRTQMhn39EgtQf .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kBiRTQMhn39EgtQf .marker.cross{stroke:#333333;}#mermaid-svg-kBiRTQMhn39EgtQf svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kBiRTQMhn39EgtQf p{margin:0;}#mermaid-svg-kBiRTQMhn39EgtQf .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-kBiRTQMhn39EgtQf .cluster-label text{fill:#333;}#mermaid-svg-kBiRTQMhn39EgtQf .cluster-label span{color:#333;}#mermaid-svg-kBiRTQMhn39EgtQf .cluster-label span p{background-color:transparent;}#mermaid-svg-kBiRTQMhn39EgtQf .label text,#mermaid-svg-kBiRTQMhn39EgtQf span{fill:#333;color:#333;}#mermaid-svg-kBiRTQMhn39EgtQf .node rect,#mermaid-svg-kBiRTQMhn39EgtQf .node circle,#mermaid-svg-kBiRTQMhn39EgtQf .node ellipse,#mermaid-svg-kBiRTQMhn39EgtQf .node polygon,#mermaid-svg-kBiRTQMhn39EgtQf .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kBiRTQMhn39EgtQf .rough-node .label text,#mermaid-svg-kBiRTQMhn39EgtQf .node .label text,#mermaid-svg-kBiRTQMhn39EgtQf .image-shape .label,#mermaid-svg-kBiRTQMhn39EgtQf .icon-shape .label{text-anchor:middle;}#mermaid-svg-kBiRTQMhn39EgtQf .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-kBiRTQMhn39EgtQf .rough-node .label,#mermaid-svg-kBiRTQMhn39EgtQf .node .label,#mermaid-svg-kBiRTQMhn39EgtQf .image-shape .label,#mermaid-svg-kBiRTQMhn39EgtQf .icon-shape .label{text-align:center;}#mermaid-svg-kBiRTQMhn39EgtQf .node.clickable{cursor:pointer;}#mermaid-svg-kBiRTQMhn39EgtQf .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-kBiRTQMhn39EgtQf .arrowheadPath{fill:#333333;}#mermaid-svg-kBiRTQMhn39EgtQf .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-kBiRTQMhn39EgtQf .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-kBiRTQMhn39EgtQf .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kBiRTQMhn39EgtQf .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-kBiRTQMhn39EgtQf .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kBiRTQMhn39EgtQf .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-kBiRTQMhn39EgtQf .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-kBiRTQMhn39EgtQf .cluster text{fill:#333;}#mermaid-svg-kBiRTQMhn39EgtQf .cluster span{color:#333;}#mermaid-svg-kBiRTQMhn39EgtQf div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-kBiRTQMhn39EgtQf .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-kBiRTQMhn39EgtQf rect.text{fill:none;stroke-width:0;}#mermaid-svg-kBiRTQMhn39EgtQf .icon-shape,#mermaid-svg-kBiRTQMhn39EgtQf .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kBiRTQMhn39EgtQf .icon-shape p,#mermaid-svg-kBiRTQMhn39EgtQf .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-kBiRTQMhn39EgtQf .icon-shape .label rect,#mermaid-svg-kBiRTQMhn39EgtQf .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kBiRTQMhn39EgtQf .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-kBiRTQMhn39EgtQf .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-kBiRTQMhn39EgtQf :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 读时间 ReadTime
键码 1/2/3/4
写时间 SetTime
LCD_ShowNum 显示
翻转闪烁标志
DS1302 时钟芯片
主程序 main
独立按键 Key
LCD1602 屏幕
定时器 T0 中断

整个工程一共 6 个文件:

文件 作用 小白理解
main.c 主逻辑:显示、调时、模式切换 大脑
ds1302.c / ds1302.h 时钟芯片驱动 记忆时间的本子
lcd1602.c / lcd1602.h 屏幕驱动 显示器
key.c / key.h 按键扫描 遥控器
timer0.c / timer0.h 定时器中断 秒表/节拍器
delay.c / delay.h 软件延时 等待工具

3. 核心概念先扫盲(小白必读)

3.1 BCD 码:为什么时间要「除以 16 乘 10」

DS1302 内部存时间用的是 BCD 码,不是普通二进制。

  • 普通二进制:0x25 表示十进制的 37
  • BCD 码:0x25 表示十进制的 25(高 4 位是十位,低 4 位是个位)

所以从芯片读出来要转成十进制:

c 复制代码
十进制 = BCD / 16 * 10 + BCD % 16;

反过来写进芯片要转成 BCD:

c 复制代码
BCD = 十进制 / 10 * 16 + 十进制 % 10;

记住这两行,整个 DS1302 就通了一半。

3.2 三个引脚:CE、SCLK、IO

DS1302 是「串行」通信,只用 3 根线:

  • CE(也叫 RST):片选,拉高表示「我要开始通信了」
  • SCLK:时钟线,每来一个上升沿/下降沿就传一位
  • IO:数据线,一位一位地传
c 复制代码
sbit DS1302_SCLK = P3^6;
sbit DS1302_IO   = P3^4;
sbit DS1302_CE   = P3^5;

3.3 命令字:0x80 是「写秒」,0x81 是「读秒」

DS1302 的每个寄存器都有一个地址,最低位是「读/写」标志:

  • 最低位 = 0 → 写
  • 最低位 = 1 → 读

所以 0x80 写秒,0x81 读秒;0x8E 是写保护寄存器。

c 复制代码
#define DS1302_SECOND 0x80
#define DS1302_MINUTE 0x82
#define DS1302_HOUR   0x84
#define DS1302_DATE   0x86
#define DS1302_MONTH  0x88
#define DS1302_DAY    0x8A
#define DS1302_YEAR   0x8C
#define DS1302_WP     0x8E

4. DS1302 驱动逐行讲解

4.1 初始化:把两根线拉低

c 复制代码
void DS1302_Init(void)
{
	DS1302_CE = 0;
	DS1302_SCLK = 0;
}

上电后先把片选和时钟拉低,保证通信从「干净」的状态开始。

4.2 写一个字节:先发命令,再发数据

c 复制代码
void DS1302_WriteByte(unsigned char Command, unsigned char Data)
{
	unsigned char i;
	DS1302_CE = 1;                 // 开始通信
	for(i = 0; i < 8; i++)         // 先发 8 位命令
	{
		DS1302_IO = Command & (0x01 << i);  // 取出第 i 位
		DS1302_SCLK = 1;           // 上升沿,芯片采样
		DS1302_SCLK = 0;
	}
	for(i = 0; i < 8; i++)         // 再发 8 位数据
	{
		DS1302_IO = Data & (0x01 << i);
		DS1302_SCLK = 1;
		DS1302_SCLK = 0;
	}
	DS1302_CE = 0;                 // 结束通信
}

小白理解 :0x01 << i 就是「把 1 左移 i 位」,配合 & 就能把第 i 位单独抠出来。循环 8 次,一个字节就发完了。

4.3 读一个字节:命令最低位置 1

c 复制代码
unsigned char DS1302_ReadByte(unsigned char Command)
{
	unsigned char i, Data = 0x00;
	DS1302_CE = 1;
	Command |= 0x01;               // 最低位置 1,表示「读」
	for(i = 0; i < 8; i++)         // 发命令
	{
		DS1302_IO = Command & (0x01 << i);
		DS1302_SCLK = 0;
		DS1302_SCLK = 1;
	}
	for(i = 0; i < 8; i++)         // 收数据
	{
		DS1302_SCLK = 1;
		DS1302_SCLK = 0;
		if(DS1302_IO) { Data |= (0x01 << i); }  // 读到 1 就把对应位置 1
	}
	DS1302_IO = 0;
	DS1302_CE = 0;
	return Data;
}

关键点 :Command |= 0x01 这一句,把「写地址」变成「读地址」,比如 0x80 变 0x81。

4.4 写入时间:先关写保护

c 复制代码
void DS1302_SetTime(void)
{
	DS1302_WriteByte(DS1302_WP, 0x00);   // 关闭写保护
	DS1302_WriteByte(DS1302_YEAR,   DS1302_Time[0]/10*16 + DS1302_Time[0]%10);
	DS1302_WriteByte(DS1302_MONTH,  DS1302_Time[1]/10*16 + DS1302_Time[1]%10);
	DS1302_WriteByte(DS1302_DATE,   DS1302_Time[2]/10*16 + DS1302_Time[2]%10);
	DS1302_WriteByte(DS1302_HOUR,   DS1302_Time[3]/10*16 + DS1302_Time[3]%10);
	DS1302_WriteByte(DS1302_MINUTE, DS1302_Time[4]/10*16 + DS1302_Time[4]%10);
	DS1302_WriteByte(DS1302_SECOND, DS1302_Time[5]/10*16 + DS1302_Time[5]%10);
	DS1302_WriteByte(DS1302_DAY,    DS1302_Time[6]/10*16 + DS1302_Time[6]%10);
	DS1302_WriteByte(DS1302_WP, 0x80);   // 重新打开写保护
}

踩坑提醒:如果忘了关写保护,写进去的时间不会生效,读出来永远是老值。

4.5 读取时间:BCD 转十进制

c 复制代码
void DS1302_ReadTime(void)
{
	unsigned char Temp;
	Temp = DS1302_ReadByte(DS1302_YEAR);
	DS1302_Time[0] = Temp/16*10 + Temp%16;
	// 后面月、日、时、分、秒、周同理
}

5. LCD1602 驱动:只讲你会用到的

LCD1602 的驱动代码比较长,小白不需要全部背下来,只要会用下面几个函数:

c 复制代码
LCD_Init();                                  // 初始化,必须最先调用
LCD_ShowString(1, 9, "  -  -  ");            // 第 1 行第 9 列开始显示字符串
LCD_ShowNum(1, 9, DS1302_Time[0], 2);        // 第 1 行第 9 列显示 2 位数字

参数含义:(行, 列, 内容, 位数),行是 1~2,列是 1~16。

几个底层函数的作用:

  • LCD_WriteCommand():写命令(比如清屏、设置光标)
  • LCD_WriteData():写数据(真正显示的内容)
  • LCD_SetCursor():设置光标位置
  • LCD_Pow():自己实现的乘方,因为 51 没有现成的

LCD_ShowNum 的核心就一句:

c 复制代码
LCD_WriteData(Number / LCD_Pow(10, i-1) % 10 + '0');

小白理解 :先除再取余,把某一位数字抠出来,+'0' 是把它变成字符 '0'~'9'。

6. 按键与定时器:调时和闪烁的秘密

6.1 按键扫描:返回 1~4

c 复制代码
unsigned char Key()
{
	unsigned char Keynum = 0;
	if(P3_1==0){Delay(20);while(P3_1==0);Delay(20);Keynum=1;}
	if(P3_0==0){Delay(20);while(P3_0==0);Delay(20);Keynum=2;}
	if(P3_2==0){Delay(20);while(P3_2==0);Delay(20);Keynum=3;}
	if(P3_3==0){Delay(20);while(P3_3==0);Delay(20);Keynum=4;}
	return Keynum;
}

Delay(20) 是消抖 ,while(P3_1==0) 是等待松手,这样按一次只算一次。

6.2 定时器:每 500ms 翻转一次闪烁标志

c 复制代码
void Timer0Routine() interrupt 1
{
	static unsigned int T0Count;
	TL0 = 0x18;
	TH0 = 0xFC;
	T0Count++;
	if(T0Count >= 500)
	{
		T0Count = 0;
		TimeSetFlashFlag = !TimeSetFlashFlag;
	}
}

定时器每 1ms 进一次中断,500 次就是 500ms,! 取反让标志位在 0/1 之间跳,从而实现「选中的数字一闪一闪」。

7. 主程序:把上面所有零件拼起来

7.1 显示时间

c 复制代码
void Timeshow(void)
{
	DS1302_ReadTime();
	LCD_ShowNum(1, 9,  DS1302_Time[0], 2);   // 年
	LCD_ShowNum(1, 12, DS1302_Time[1], 2);   // 月
	LCD_ShowNum(1, 15, DS1302_Time[2], 2);   // 日
	LCD_ShowNum(2, 9,  DS1302_Time[3], 2);   // 时
	LCD_ShowNum(2, 12, DS1302_Time[4], 2);   // 分
	LCD_ShowNum(2, 15, DS1302_Time[5], 2);   // 秒
}

7.2 调时间:加/减都要判断边界

c 复制代码
if(Keynum == 2)              // 切换要调的位
{
	TimeSetSelect++;
	TimeSetSelect %= 6;      // 0~5 循环,等价于 if(>5) =0
}
if(Keynum == 3)              // 加
{
	DS1302_Time[TimeSetSelect]++;
	if(DS1302_Time[0] > 99) { DS1302_Time[0] = 0; }
	if(DS1302_Time[1] > 12) { DS1302_Time[1] = 1; }
	// 日要按大小月和闰年分别判断......
}
if(Keynum == 4)              // 减
{
	DS1302_Time[TimeSetSelect]--;
	if(DS1302_Time[0] < 0) { DS1302_Time[0] = 99; }
	if(DS1302_Time[1] < 1) { DS1302_Time[1] = 12; }
	// 同理......
}

小白最容易懵的地方:为什么「加」只判断上界,「减」要判断上下界?

因为加的时候数值只会变大,超过上界就回绕;减的时候数值只会变小,低于下界就回绕。但日比较特殊,因为月份变化会影响日的上限,所以减的时候还要额外判断「日是否超过当前月的最大天数」。

7.3 闪烁显示

c 复制代码
if(TimeSetFlashFlag == 1 && TimeSetSelect == 0)
	LCD_ShowString(1, 9, "  ");      // 闪烁时显示空格
else
	LCD_ShowNum(1, 9, DS1302_Time[0], 2);

6 个位置各写一遍,就是「当前选中的那一位在闪」。

7.4 main 函数

c 复制代码
void main()
{
	LCD_Init();
	DS1302_Init();
	Timer0Init();
	LCD_ShowString(1, 9, "  -  -  ");
	LCD_ShowString(2, 9, "  :  :  ");
	DS1302_SetTime();

	while(1)
	{
		Keynum = Key();
		if(Keynum == 1)              // 切换模式
		{
			if(mode == 0) { mode = 1; }
			else if(mode == 1) { mode = 0; DS1302_SetTime(); }  // 调完写回芯片
		}
		switch(mode)
		{
			case 0: Timeshow(); break;
			case 1: TimeSet();  break;
		}
	}
}

重点 :从「调时模式」切回「显示模式」时,一定要调用 DS1302_SetTime() 把改好的时间写回芯片,否则断电就丢了。

8. 小白常见踩坑清单

  1. 读出来的时间乱跳:忘了关写保护,或者 BCD 没转换。
  2. 时间不走 :DS1302_Init() 没调用,或者 CE 一直是高电平。
  3. 按键按一次跳好几下 :消抖的 Delay(20) 漏了。
  4. 闪烁不生效 :定时器中断没开 EA=1、ET0=1。
  5. 调完时间又变回去 :切回显示模式时没调用 DS1302_SetTime()。
  6. LCD 显示乱码 :LCD_Init() 没在 DS1302_Init() 之前调用,或者数据口接错。

9. 总结

这一节其实是 51 单片机课程的「综合大作业」:把 GPIO、串行通信、定时器中断、按键、LCD 显示 全部串起来了。

复习时建议按这个顺序看:

  1. 先搞懂 BCD 码转换(两行公式)
  2. 再看 DS1302 的读写时序(CE → 命令 → 数据)
  3. 然后看 LCD 的显示函数怎么用
  4. 最后看 main 里怎么把模式切换和调时逻辑拼起来

把这份笔记对着代码过一遍,再自己动手改改「闪烁频率」「调时范围」,基本就吃透了。

相关推荐
女神下凡16 分钟前
芯参谋(107):杰理AC_中科蓝讯_开发指南
linux·服务器·网络·单片机·嵌入式硬件
岑梓铭1 小时前
《考研408数据结构》第七章(B树 和 B+树)复习笔记
数据结构·笔记·b树·考研·408
shimly1234562 小时前
(done) 嵌入式硬件电路图 (11) 用 AI 看电路图与总结
嵌入式硬件
shimly1234562 小时前
(done) 嵌入式硬件电路图 (12) 学习总结与工作建议
嵌入式硬件
Go Work6 小时前
docker 占满根分区时先分清谁在吃盘
经验分享·笔记
3095566667 小时前
股票接口返回 404 与空数组的排查笔记:证书状态与级别参数
java·笔记·python·ios·iphone·股票数据api·股票数据接口
蒸蒸yyyyzwd7 小时前
从邓明学习的后端笔记
笔记·学习
岑梓铭7 小时前
《考研408数据结构》第八章(排序)复习笔记
数据结构·笔记
Rabitebla19 小时前
【Linux】进程控制(十二) 自定义 Shell(二):内建命令与进程工作目录管理
linux·c++·笔记·学习·bash