51单片机课设 | 基于STC89C52的简易电子日历(12864 LCD显示+按键调时)
最近刚做完单片机实训的电子日历项目,趁热乎记录一下整个过程,顺便分享给有需要的同学。
先放个原理图:

Proteus仿真注意 :单片机晶振频率要设置成 11.0592MHz,不是12MHz,不然时序可能会有问题。
项目简介
做的是一个简易电子日历,功能不复杂:
- 显示年月日、星期、时分秒
- 三个按键可以调节时间
- 编辑时对应项会闪烁提示
- 5秒不操作自动退出编辑
硬件用的是 STC89C52 + 12864 LCD(KS0108控制器),Proteus仿真用的AT89C51。
💡 兼容性说明:代码兼容整个51系列,AT89C51、AT89C52、STC89C52等都能直接用,不用改。
硬件部分
元器件清单
| 元器件 | 型号/规格 | 数量 |
|---|---|---|
| 单片机 | STC89C52 / AT89C51 | 1 |
| LCD显示屏 | 12864(KS0108控制器) | 1 |
| 晶振 | 12MHz | 1 |
| 瓷片电容 | 30pF | 2 |
| 电解电容 | 10μF | 1 |
| 电阻 | 10KΩ | 4 |
| 排阻 | 10KΩ×8 | 1 |
| 电位器 | 10KΩ | 1 |
| 轻触按键 | - | 3 |
接线说明
P0 → LCD数据口 DB0-DB7(必须接10K上拉排阻!)
P2.0 → E 使能
P2.1 → RW 读写选择
P2.2 → RS 命令/数据选择
P2.3 → CS2 右半屏片选
P2.4 → CS1 左半屏片选
P2.5 → K1 选择键
P2.6 → K2 加键
P2.7 → K3 减键
踩坑记录:P0口上拉电阻
刚开始LCD死活不显示,查了半天发现是P0口没接上拉电阻。
51单片机的P0口比较特殊,是开漏输出,内部没有上拉电阻。不接外部上拉的话,输出高电平时其实是高阻态,根本驱动不了LCD。
解决办法:P0口接一个10KΩ的排阻上拉到VCC。
踩坑记录:LCD对比度
LCD能亮但是啥都看不见?大概率是对比度没调好。
V0引脚接个10K电位器,中间抽头接V0,两端接VCC和GND,慢慢调就能看到了。Proteus仿真的话一般不用管这个。
软件部分
整体架构
程序结构其实挺简单的:
main()
│
├── lcd_init() // LCD初始化
├── timer_init() // 定时器初始化
│
└── while(1) // 主循环
├── key_scan() // 按键扫描
├── display() // 显示更新
└── delay(20) // 控制刷新率
计时靠的是定时器中断,50ms中断一次,累计20次就是1秒。主循环只负责扫按键和刷新显示。
几个关键算法
1. 闰年判断
c
uchar is_leap(uint y)
{
if((y % 4 == 0 && y % 100 != 0) || (y % 400 == 0))
return 1;
return 0;
}
这个应该都知道:能被4整除但不能被100整除,或者能被400整除,就是闰年。
2. 蔡勒公式算星期
这个算法挺有意思的,可以算出任意日期是星期几:
c
uchar get_week(uint y, uchar m, uchar d)
{
int c, yy, w;
if(m < 3) { // 1、2月当作上一年的13、14月
m += 12;
y--;
}
c = y / 100; // 世纪数
yy = y % 100; // 年份后两位
w = yy + yy/4 + c/4 - 2*c + 26*(m+1)/10 + d - 1;
return ((w % 7) + 7) % 7; // 0=周日,1-6=周一到周六
}
为什么1、2月要特殊处理?因为闰年多出来的那天在2月底,把1、2月算到上一年去,就不用单独处理闰年对星期的影响了。
最后那个 ((w % 7) + 7) % 7 是为了处理负数的情况,C语言的取模对负数的处理不太一样。
3. 获取月份天数
c
uchar code days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
uchar get_days(uint y, uchar m)
{
if(m == 2 && is_leap(y))
return 29;
return days[m];
}
用查表法,2月特殊处理一下闰年。数组用 code 关键字放到ROM里,省RAM。
LCD12864驱动
12864屏幕结构
12864 LCD的分辨率是128×64像素,但它其实是由**两片独立的64×64控制器(KS0108)**拼起来的:
128列
←─────────────────────→
┌───────────┬───────────┐
│ │ │ ↑
│ 左半屏 │ 右半屏 │ 64行
│ (CS1=0) │ (CS2=0) │ ↓
│ 列0~63 │ 列0~63 │
└───────────┴───────────┘
所以写程序的时候要分别控制左右两边,超过63列就要切换到右半屏。
显示存储器组织(页/列)
这个是理解12864驱动的关键。屏幕的64行被分成8页,每页8行:
页0 ─── 第 0~ 7 行
页1 ─── 第 8~15 行
页2 ─── 第16~23 行
页3 ─── 第24~31 行
页4 ─── 第32~39 行
页5 ─── 第40~47 行
页6 ─── 第48~55 行
页7 ─── 第56~63 行
每次写入一个字节,就是在当前页的当前列写入8个像素点(垂直方向)。字节的每一位对应一个像素:
写入数据 0x81 (二进制: 10000001)
列n
│
页m ─→ ■ ← bit0 (最上面,值=1,点亮)
□ ← bit1 (值=0,不亮)
□ ← bit2
□ ← bit3
□ ← bit4
□ ← bit5
□ ← bit6
■ ← bit7 (最下面,值=1,点亮)
所以一个16×16的汉字需要占用2页×16列 = 32字节的数据。
显示坐标系统
要在屏幕上显示内容,需要先设置好"页地址"和"列地址":
设置页地址:write_cmd(0xB8 + 页号); // 页号 0~7
设置列地址:write_cmd(0x40 + 列号); // 列号 0~63
比如要在左半屏的第2页、第10列开始写数据:
c
sel_left(); // 选择左半屏
write_cmd(0xB8 + 2); // 设置页地址 = 2
write_cmd(0x40 + 10); // 设置列地址 = 10
write_data(0xFF); // 写入数据,这一列8个点全亮
写完一个字节后,列地址会自动加1,所以连续写多个字节就能画出一行。
字模数据格式
以数字"0"的8×16字模为例:
c
{0x00,0xE0,0x10,0x08,0x08,0x10,0xE0,0x00, // 上半部分(页n)
0x00,0x0F,0x10,0x20,0x20,0x10,0x0F,0x00} // 下半部分(页n+1)
前8个字节是上半部分(8×8),后8个字节是下半部分(8×8),拼起来就是8×16的数字。
显示的时候要分两次写:
c
// 写上半部分
write_cmd(0xB8 + p); // 设置页
write_cmd(0x40 + c); // 设置列
for(i = 0; i < 8; i++)
write_data(num[n][i]);
// 写下半部分
write_cmd(0xB8 + p + 1); // 下一页
write_cmd(0x40 + c); // 列地址要重新设置
for(i = 0; i < 8; i++)
write_data(num[n][i + 8]);
显示原理小结
简单来说就是:
- 屏幕分左右两半,用CS1/CS2选择
- 每半屏分8页,每页8行像素
- 写一个字节 = 画一列8个点
- 先设置页和列地址,再写数据
- 16×16的字需要写2页,8×16的数字也是2页
基本操作函数
c
// 等待LCD空闲
void check()
{
uchar tmp;
DATA = 0xFF;
RS = 0; RW = 1;
do {
E = 1;
_nop_(); _nop_();
tmp = DATA;
E = 0;
_nop_();
} while(tmp & 0x80); // D7=1表示忙
RW = 0;
}
// 写命令
void write_cmd(uchar cmd)
{
check();
RS = 0; RW = 0;
DATA = cmd;
E = 1; _nop_(); _nop_(); E = 0;
}
// 写数据
void write_data(uchar dat)
{
check();
RS = 1; RW = 0;
DATA = dat;
E = 1; _nop_(); _nop_(); E = 0;
}
RS=0是命令,RS=1是数据。每次操作前都要检测忙标志,不然LCD还没处理完上一条命令就发新的,会出问题。
常用命令
| 命令 | 代码 | 说明 |
|---|---|---|
| 开显示 | 0x3F | |
| 设置页 | 0xB8+n | n=0~7 |
| 设置列 | 0x40+n | n=0~63 |
| 设置起始行 | 0xC0+n | 用于滚屏 |
按键处理
传统消抖的问题
一开始用的是传统的延时消抖:
c
if(K1 == 0) {
delay(10);
if(K1 == 0) {
// 处理
while(K1 == 0); // 等松手
}
}
问题是这样会阻塞,按住按键的时候整个程序都卡住了,时钟也不走了。
改用边沿检测
后来改成了边沿检测,用静态变量记住上次的按键状态:
c
void key_scan()
{
static uchar k1_old = 1;
uchar k1_now = K1;
// 检测下降沿(1→0)
if(k1_old == 1 && k1_now == 0) {
// 按键按下,处理
}
k1_old = k1_now;
}
这样就不会阻塞了,响应也快。主循环20ms跑一次,相当于自带消抖。
编辑模式
用一个 mode 变量控制当前状态:
- mode=0:正常走时
- mode=1~6:分别编辑年、月、日、时、分、秒
K1切换编辑项,K2加,K3减。编辑的时候对应数字会闪烁,5秒不操作自动退出。
有个细节要注意:改完年份或月份后,要检查日期是否还合法。比如从3月31日切到2月,日期要自动变成28或29。
c
if(ri > get_days(nian, yue))
ri = get_days(nian, yue);
定时器中断
c
void timer_init()
{
TMOD &= 0xF0;
TMOD |= 0x01; // T0模式1(16位)
TH0 = 0x4C; // 50ms初值(12MHz晶振)
TL0 = 0x00;
ET0 = 1; // 允许T0中断
EA = 1; // 开总中断
TR0 = 1; // 启动T0
}
void timer0() interrupt 1
{
TH0 = 0x4C; // 重装初值
TL0 = 0x00;
cnt++;
if(cnt >= 20) { // 20×50ms = 1秒
cnt = 0;
miao++;
if(miao >= 60) {
miao = 0;
fen++;
// ... 依次进位
}
}
}
50ms中断一次,计数20次就是1秒。进位链:秒→分→时→日→月→年。
显示布局
最终显示效果是这样的:
┌────────────────────────────────────┐
│ 第0行:2025年12月22日 │
│ 第2行:星期一 │
│ 第4行:12:30:45 │
│ 第6行:国泰民安 │
└────────────────────────────────────┘
12864一共8页,每页16像素高,正好放4行16×16的汉字/数字。
显示函数里根据 mode 和 flash 变量控制闪烁:
c
if(mode == 1 && flash) {
// 清除年份显示区域(闪烁效果)
clear_num_L(0, 8);
// ...
} else {
// 正常显示年份
show_num_L(0, 8, nian / 1000);
// ...
}
完整代码
代码比较长,直接贴在下面。字模数据是用取模软件生成的,16×16的汉字和8×16的数字。
c
/*******************************************************************************
* 简易电子日历
* 单片机:STC89C52
* 显示屏:12864 LCD (KS0108控制器)
*
* 功能:显示年月日、星期、时分秒,按键可调节时间
*
* 接线说明:
* P0 - LCD数据口(需要接10K上拉电阻)
* P2.0 - E 使能
* P2.1 - RW 读写
* P2.2 - RS 命令/数据
* P2.3 - CS2 右半屏
* P2.4 - CS1 左半屏
* P2.5 - 选择键
* P2.6 - 加键
* P2.7 - 减键
******************************************************************************/
#include <reg52.h>
#include <intrins.h>
typedef unsigned char uchar;
typedef unsigned int uint;
/* LCD引脚 */
#define DATA P0
sbit E = P2^0;
sbit RW = P2^1;
sbit RS = P2^2;
sbit CS2 = P2^3;
sbit CS1 = P2^4;
/* 按键引脚 */
sbit K1 = P2^5; /* 选择键 */
sbit K2 = P2^6; /* 加键 */
sbit K3 = P2^7; /* 减键 */
/* 时间变量 */
uint nian = 2025; /* 年 */
uchar yue = 12; /* 月 */
uchar ri = 22; /* 日 */
uchar shi = 0; /* 时 */
uchar fen = 0; /* 分 */
uchar miao = 0; /* 秒 */
/* 其他变量 */
uint cnt = 0; /* 定时器计数 */
uchar mode = 0; /* 编辑模式:0正常 1年 2月 3日 4时 5分 6秒 */
uchar flash = 0; /* 闪烁标志 */
uchar fcnt = 0; /* 闪烁计数 */
uchar wait = 0; /* 等待计数,用于自动退出编辑 */
/* 每月天数 */
uchar code days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
/*******************************************************************************
* 数字字模 0-9 (8x16)
******************************************************************************/
uchar code num[][16] = {
{0x00,0xE0,0x10,0x08,0x08,0x10,0xE0,0x00,0x00,0x0F,0x10,0x20,0x20,0x10,0x0F,0x00}, /* 0 */
{0x00,0x10,0x10,0xF8,0x00,0x00,0x00,0x00,0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00}, /* 1 */
{0x00,0x70,0x08,0x08,0x08,0x88,0x70,0x00,0x00,0x30,0x28,0x24,0x22,0x21,0x30,0x00}, /* 2 */
{0x00,0x30,0x08,0x88,0x88,0x48,0x30,0x00,0x00,0x18,0x20,0x20,0x20,0x11,0x0E,0x00}, /* 3 */
{0x00,0x00,0xC0,0x20,0x10,0xF8,0x00,0x00,0x00,0x07,0x04,0x24,0x24,0x3F,0x24,0x00}, /* 4 */
{0x00,0xF8,0x08,0x88,0x88,0x08,0x08,0x00,0x00,0x19,0x21,0x20,0x20,0x11,0x0E,0x00}, /* 5 */
{0x00,0xE0,0x10,0x88,0x88,0x18,0x00,0x00,0x00,0x0F,0x11,0x20,0x20,0x11,0x0E,0x00}, /* 6 */
{0x00,0x38,0x08,0x08,0xC8,0x38,0x08,0x00,0x00,0x00,0x00,0x3F,0x00,0x00,0x00,0x00}, /* 7 */
{0x00,0x70,0x88,0x08,0x08,0x88,0x70,0x00,0x00,0x1C,0x22,0x21,0x21,0x22,0x1C,0x00}, /* 8 */
{0x00,0xE0,0x10,0x08,0x08,0x10,0xE0,0x00,0x00,0x00,0x31,0x22,0x22,0x11,0x0F,0x00} /* 9 */
};
/* 冒号字模 (8x16) */
uchar code maohao[] = {
0x00,0x00,0x00,0xC0,0xC0,0x00,0x00,0x00,
0x00,0x00,0x00,0x30,0x30,0x00,0x00,0x00
};
/*******************************************************************************
* 汉字字模 (16x16)
******************************************************************************/
/* 国 */
uchar code guo[] = {
0x00,0xff,0x01,0x05,0x45,0x45,0x45,0xfd,0x45,0x45,0x45,0x05,0x01,0xff,0x00,0x00,
0x00,0x3f,0x10,0x14,0x14,0x14,0x14,0x17,0x14,0x15,0x16,0x14,0x10,0x3f,0x00,0x00
};
/* 泰 */
uchar code tai[] = {
0x20,0x22,0x2a,0xaa,0x6a,0x3a,0xaf,0x2a,0x6a,0xaa,0x2a,0x22,0x20,0x00,0x00,0x00,
0x04,0x12,0x11,0x09,0x16,0x24,0x1f,0x02,0x06,0x09,0x11,0x12,0x04,0x04,0x00,0x00
};
/* 民 */
uchar code min[] = {
0x00,0xfe,0x92,0x92,0x92,0x92,0xf2,0x92,0x92,0x92,0x9e,0x80,0x80,0x00,0x00,0x00,
0x00,0x3f,0x10,0x08,0x08,0x00,0x01,0x06,0x08,0x10,0x20,0x3c,0x00,0x00,0x00,0x00
};
/* 安 */
uchar code an[] = {
0x40,0x50,0x4c,0x44,0x44,0xc4,0x75,0x46,0x44,0xc4,0x44,0x54,0x4c,0x40,0x00,0x00,
0x00,0x20,0x20,0x20,0x13,0x12,0x0c,0x04,0x06,0x09,0x08,0x10,0x30,0x00,0x00,0x00
};
/* 年 */
uchar code hz_nian[] = {
0x00,0x10,0x08,0xe7,0x24,0x24,0x24,0xfc,0x24,0x24,0x24,0x24,0x04,0x00,0x00,0x00,
0x02,0x02,0x02,0x03,0x02,0x02,0x02,0x3f,0x02,0x02,0x02,0x02,0x02,0x02,0x00,0x00
};
/* 月 */
uchar code hz_yue[] = {
0x00,0x00,0x00,0x00,0xfe,0x12,0x12,0x12,0x12,0x12,0xfe,0x00,0x00,0x00,0x00,0x00,
0x00,0x20,0x10,0x0c,0x03,0x01,0x01,0x01,0x11,0x21,0x1f,0x00,0x00,0x00,0x00,0x00
};
/* 日 */
uchar code hz_ri[] = {
0x00,0x00,0x00,0xfe,0x42,0x42,0x42,0x42,0x42,0x42,0x42,0xfe,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x1f,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x1f,0x00,0x00,0x00,0x00
};
/* 星 */
uchar code xing[] = {
0x00,0x00,0xbe,0x2a,0x2a,0x2a,0xea,0x2a,0x2a,0x2a,0x3e,0x00,0x00,0x00,0x00,0x00,
0x24,0x22,0x25,0x25,0x25,0x25,0x3f,0x25,0x25,0x25,0x25,0x21,0x20,0x00,0x00,0x00
};
/* 期 */
uchar code qi[] = {
0x04,0x04,0xff,0x54,0x54,0xff,0x04,0x00,0xfe,0x12,0x12,0x12,0xfe,0x00,0x00,0x00,
0x22,0x12,0x0b,0x02,0x02,0x0b,0x12,0x30,0x0f,0x01,0x11,0x21,0x1f,0x00,0x00,0x00
};
/* 一 */
uchar code yi[] = {
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x10,0xff,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x3f,0x00,0x00,0x00,0x00
};
/* 二 */
uchar code er[] = {
0x00,0x00,0x00,0x00,0xfc,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x0f,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x3f,0x00,0x00,0x00
};
/* 三 */
uchar code san[] = {
0x00,0x00,0x00,0x00,0xfe,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xfc,0x00,0xff,0x00,
0x00,0x00,0x00,0x00,0x1f,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0f,0x00,0x3f,0x00
};
/* 四 */
uchar code si[] = {
0x00,0x00,0xfe,0x22,0x22,0x22,0x22,0x12,0x12,0x0a,0x06,0x02,0xfe,0x02,0x00,0x00,
0x00,0x00,0x1f,0x11,0x11,0x11,0x11,0x11,0x11,0x1e,0x10,0x10,0x1f,0x10,0x00,0x00
};
/* 五 */
uchar code wu[] = {
0x00,0x00,0xfe,0x40,0x40,0x40,0xfc,0x20,0x20,0x20,0x10,0x10,0xff,0x00,0x00,0x00,
0x00,0x00,0x1f,0x00,0x00,0x00,0x07,0x04,0x04,0x04,0x04,0x04,0x3f,0x00,0x00,0x00
};
/* 六 */
uchar code liu[] = {
0x20,0x40,0x40,0x00,0xff,0x00,0x00,0x10,0x10,0x08,0x08,0x04,0x02,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x1f,0x00,0x00,0x01,0x02,0x04,0x04,0x08,0x08,0x00,0x00,0x00
};
/*******************************************************************************
* 延时函数
******************************************************************************/
void delay(uint ms)
{
uint i, j;
for(i = ms; i > 0; i--)
for(j = 120; j > 0; j--);
}
/*******************************************************************************
* LCD基本操作函数
******************************************************************************/
/* 等待LCD空闲 */
void check()
{
uchar tmp;
DATA = 0xFF;
RS = 0;
RW = 1;
do {
E = 1;
_nop_();
_nop_();
tmp = DATA;
E = 0;
_nop_();
} while(tmp & 0x80);
RW = 0;
}
/* 写命令 */
void write_cmd(uchar cmd)
{
check();
RS = 0;
RW = 0;
DATA = cmd;
E = 1;
_nop_();
_nop_();
E = 0;
}
/* 写数据 */
void write_data(uchar dat)
{
check();
RS = 1;
RW = 0;
DATA = dat;
E = 1;
_nop_();
_nop_();
E = 0;
}
/* 选左半屏 */
void sel_left() { CS1 = 0; CS2 = 1; }
/* 选右半屏 */
void sel_right() { CS1 = 1; CS2 = 0; }
/* LCD初始化 */
void lcd_init()
{
sel_left();
write_cmd(0x3F); /* 开显示 */
write_cmd(0xC0); /* 起始行0 */
sel_right();
write_cmd(0x3F);
write_cmd(0xC0);
}
/* 清屏 */
void lcd_clear()
{
uchar p, c;
for(p = 0; p < 8; p++) {
sel_left();
write_cmd(0xB8 + p);
write_cmd(0x40);
for(c = 0; c < 64; c++)
write_data(0x00);
sel_right();
write_cmd(0xB8 + p);
write_cmd(0x40);
for(c = 0; c < 64; c++)
write_data(0x00);
}
}
/*******************************************************************************
* 显示函数
******************************************************************************/
/* 左屏显示数字 */
void show_num_L(uchar p, uchar c, uchar n)
{
uchar i;
sel_left();
write_cmd(0xB8 + p);
write_cmd(0x40 + c);
for(i = 0; i < 8; i++) write_data(num[n][i]);
write_cmd(0xB8 + p + 1);
write_cmd(0x40 + c);
for(i = 0; i < 8; i++) write_data(num[n][i + 8]);
}
/* 右屏显示数字 */
void show_num_R(uchar p, uchar c, uchar n)
{
uchar i;
sel_right();
write_cmd(0xB8 + p);
write_cmd(0x40 + c);
for(i = 0; i < 8; i++) write_data(num[n][i]);
write_cmd(0xB8 + p + 1);
write_cmd(0x40 + c);
for(i = 0; i < 8; i++) write_data(num[n][i + 8]);
}
/* 左屏清除数字区域 */
void clear_num_L(uchar p, uchar c)
{
uchar i;
sel_left();
write_cmd(0xB8 + p);
write_cmd(0x40 + c);
for(i = 0; i < 8; i++) write_data(0x00);
write_cmd(0xB8 + p + 1);
write_cmd(0x40 + c);
for(i = 0; i < 8; i++) write_data(0x00);
}
/* 右屏清除数字区域 */
void clear_num_R(uchar p, uchar c)
{
uchar i;
sel_right();
write_cmd(0xB8 + p);
write_cmd(0x40 + c);
for(i = 0; i < 8; i++) write_data(0x00);
write_cmd(0xB8 + p + 1);
write_cmd(0x40 + c);
for(i = 0; i < 8; i++) write_data(0x00);
}
/* 左屏显示冒号 */
void show_maohao_L(uchar p, uchar c)
{
uchar i;
sel_left();
write_cmd(0xB8 + p);
write_cmd(0x40 + c);
for(i = 0; i < 8; i++) write_data(maohao[i]);
write_cmd(0xB8 + p + 1);
write_cmd(0x40 + c);
for(i = 0; i < 8; i++) write_data(maohao[i + 8]);
}
/* 右屏显示冒号 */
void show_maohao_R(uchar p, uchar c)
{
uchar i;
sel_right();
write_cmd(0xB8 + p);
write_cmd(0x40 + c);
for(i = 0; i < 8; i++) write_data(maohao[i]);
write_cmd(0xB8 + p + 1);
write_cmd(0x40 + c);
for(i = 0; i < 8; i++) write_data(maohao[i + 8]);
}
/* 左屏显示汉字 */
void show_hz_L(uchar p, uchar c, uchar *hz)
{
uchar i;
sel_left();
write_cmd(0xB8 + p);
write_cmd(0x40 + c);
for(i = 0; i < 16; i++) write_data(hz[i]);
write_cmd(0xB8 + p + 1);
write_cmd(0x40 + c);
for(i = 0; i < 16; i++) write_data(hz[i + 16]);
}
/* 右屏显示汉字 */
void show_hz_R(uchar p, uchar c, uchar *hz)
{
uchar i;
sel_right();
write_cmd(0xB8 + p);
write_cmd(0x40 + c);
for(i = 0; i < 16; i++) write_data(hz[i]);
write_cmd(0xB8 + p + 1);
write_cmd(0x40 + c);
for(i = 0; i < 16; i++) write_data(hz[i + 16]);
}
/*******************************************************************************
* 日期计算函数
******************************************************************************/
/* 判断闰年 */
uchar is_leap(uint y)
{
if((y % 4 == 0 && y % 100 != 0) || (y % 400 == 0))
return 1;
return 0;
}
/* 获取某月天数 */
uchar get_days(uint y, uchar m)
{
if(m == 2 && is_leap(y))
return 29;
return days[m];
}
/* 计算星期(蔡勒公式) */
uchar get_week(uint y, uchar m, uchar d)
{
int c, yy, w;
if(m < 3) { m += 12; y--; }
c = y / 100;
yy = y % 100;
w = yy + yy/4 + c/4 - 2*c + 26*(m+1)/10 + d - 1;
return ((w % 7) + 7) % 7;
}
/*******************************************************************************
* 按键扫描(边沿检测,无消抖延时)
******************************************************************************/
void key_scan()
{
static uchar k1_old = 1, k2_old = 1, k3_old = 1;
uchar pressed = 0;
uchar k1_now = K1, k2_now = K2, k3_now = K3;
/* K1选择键 - 下降沿触发 */
if(k1_old == 1 && k1_now == 0) {
pressed = 1;
if(mode == 0) mode = 1;
else { mode++; if(mode > 6) mode = 1; }
}
k1_old = k1_now;
/* K2加键 - 下降沿触发 */
if(k2_old == 1 && k2_now == 0) {
if(mode > 0) {
pressed = 1;
if(mode == 1) { nian++; if(nian > 2030) nian = 2024; }
else if(mode == 2) { yue++; if(yue > 12) yue = 1; }
else if(mode == 3) { ri++; if(ri > get_days(nian, yue)) ri = 1; }
else if(mode == 4) { shi++; if(shi > 23) shi = 0; }
else if(mode == 5) { fen++; if(fen > 59) fen = 0; }
else if(mode == 6) { miao++; if(miao > 59) miao = 0; }
if(ri > get_days(nian, yue)) ri = get_days(nian, yue);
}
}
k2_old = k2_now;
/* K3减键 - 下降沿触发 */
if(k3_old == 1 && k3_now == 0) {
if(mode > 0) {
pressed = 1;
if(mode == 1) { if(nian > 2024) nian--; else nian = 2030; }
else if(mode == 2) { if(yue > 1) yue--; else yue = 12; }
else if(mode == 3) { if(ri > 1) ri--; else ri = get_days(nian, yue); }
else if(mode == 4) { if(shi > 0) shi--; else shi = 23; }
else if(mode == 5) { if(fen > 0) fen--; else fen = 59; }
else if(mode == 6) { if(miao > 0) miao--; else miao = 59; }
if(ri > get_days(nian, yue)) ri = get_days(nian, yue);
}
}
k3_old = k3_now;
if(pressed) wait = 0;
}
/*******************************************************************************
* 定时器初始化与中断
******************************************************************************/
void timer_init()
{
TMOD &= 0xF0;
TMOD |= 0x01; /* T0模式1 */
TH0 = 0x4C; /* 50ms */
TL0 = 0x00;
ET0 = 1;
EA = 1;
TR0 = 1;
}
void timer0() interrupt 1
{
TH0 = 0x4C;
TL0 = 0x00;
cnt++;
if(cnt >= 20) { /* 1秒 */
cnt = 0;
miao++;
if(miao >= 60) {
miao = 0;
fen++;
if(fen >= 60) {
fen = 0;
shi++;
if(shi >= 24) {
shi = 0;
ri++;
if(ri > get_days(nian, yue)) {
ri = 1;
yue++;
if(yue > 12) {
yue = 1;
nian++;
}
}
}
}
}
}
}
/*******************************************************************************
* 显示更新
******************************************************************************/
void display()
{
uchar week;
/* 闪烁控制 */
fcnt++;
if(fcnt >= 10) {
fcnt = 0;
flash = !flash;
/* 编辑模式下计时,5秒无操作自动退出 */
if(mode > 0) {
wait++;
if(wait >= 10) { mode = 0; wait = 0; }
}
}
/*=== 第0行:年月日 ===*/
if(mode == 1 && flash) {
clear_num_L(0, 8); clear_num_L(0, 16);
clear_num_L(0, 24); clear_num_L(0, 32);
} else {
show_num_L(0, 8, nian / 1000);
show_num_L(0, 16, (nian / 100) % 10);
show_num_L(0, 24, (nian / 10) % 10);
show_num_L(0, 32, nian % 10);
}
show_hz_L(0, 40, hz_nian);
if(mode == 2 && flash) {
clear_num_L(0, 56); clear_num_R(0, 0);
} else {
show_num_L(0, 56, yue / 10);
show_num_R(0, 0, yue % 10);
}
show_hz_R(0, 8, hz_yue);
if(mode == 3 && flash) {
clear_num_R(0, 24); clear_num_R(0, 32);
} else {
show_num_R(0, 24, ri / 10);
show_num_R(0, 32, ri % 10);
}
show_hz_R(0, 40, hz_ri);
/*=== 第2行:星期 ===*/
show_hz_L(2, 24, xing);
show_hz_L(2, 40, qi);
week = get_week(nian, yue, ri);
if(week == 0) show_hz_L(2, 56, hz_ri);
else show_num_R(2, 0, week);
/*=== 第4行:时分秒 ===*/
if(mode == 4 && flash) {
clear_num_L(4, 28); clear_num_L(4, 36);
} else {
show_num_L(4, 28, shi / 10);
show_num_L(4, 36, shi % 10);
}
show_maohao_L(4, 44);
if(mode == 5 && flash) {
clear_num_L(4, 52); clear_num_R(4, 0);
} else {
show_num_L(4, 52, fen / 10);
show_num_R(4, 0, fen % 10);
}
show_maohao_R(4, 8);
if(mode == 6 && flash) {
clear_num_R(4, 16); clear_num_R(4, 24);
} else {
show_num_R(4, 16, miao / 10);
show_num_R(4, 24, miao % 10);
}
/*=== 第6行:国泰民安 ===*/
show_hz_L(6, 32, guo);
show_hz_L(6, 48, tai);
show_hz_R(6, 0, min);
show_hz_R(6, 16, an);
}
/*******************************************************************************
* 主函数
******************************************************************************/
void main()
{
P0 = 0xFF;
P2 = 0xFF;
delay(100);
lcd_init();
lcd_clear();
timer_init();
while(1) {
key_scan();
display();
delay(20);
}
}
一些小问题和解决办法
1. 时钟走着走着就不准了
这个问题挺常见的。定时器初值计算要准确,12MHz晶振下50ms的初值是:
6553= 15536 = 0x3CB0
但我代码里用的是 0x4C00,其实有点偏差。如果对精度要求高,可以用 0x3CB0,或者干脆用DS1302时钟芯片。
不过对于课设来说,误差不大,能接受。
2. 按键按一下触发好几次
这就是没做好消抖。用边沿检测的话基本不会有这个问题,因为只在状态变化的那一瞬间触发一次。
如果还是有问题,可以在主循环的delay里多加点时间,比如从20ms改成50m
3. 显示乱码
检查几个地方:
- P0口有没有接上拉电阻
- 字模数据有没有问题(可以用取模软件重新生成)
- LCD的CS1、CS2有没有接反
4. 改完月份日期不对
比如从3月31日改到2月,日期还是31,但2月没有31号。
解决办法是每次改完年份或月份后,检查一下日期是否超出当月最大天数:
c
if(ri > get_days(nian, yue))
ri = get_days(nian, yue);
可以改进的地方
- 加个闹钟功能:再加几个变量存闹钟时间,到点了蜂鸣器响
- 用DS1302 :掉电不丢时. 加温度显示:接个DS18B20,显示当前温度
- 农历显示:这个算法比较复杂,有兴趣可以研究一下
总结
这个项目虽然不复杂,但涉及的知识点还挺全的:定时器、中断、LCD驱动、按键处理、日期算法。做完之后对51单片机的理解会深很多。
完整源码和Proteus仿真工程已经打包上传了,免费下载,文章顶部就能看到。有问题评论区交流~
如果觉得有帮助,点个赞再走呗~ 👍