STC89C52电子日历:12864 LCD+按键调时【附源码+Proteus仿真,免费】

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]);

显示原理小结

简单来说就是:

  1. 屏幕分左右两半,用CS1/CS2选择
  2. 每半屏分8页,每页8行像素
  3. 写一个字节 = 画一列8个点
  4. 先设置页和列地址,再写数据
  5. 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的汉字/数字。

显示函数里根据 modeflash 变量控制闪烁:

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. 显示乱码

检查几个地方:

  1. P0口有没有接上拉电阻
  2. 字模数据有没有问题(可以用取模软件重新生成)
  3. LCD的CS1、CS2有没有接反

4. 改完月份日期不对

比如从3月31日改到2月,日期还是31,但2月没有31号。

解决办法是每次改完年份或月份后,检查一下日期是否超出当月最大天数:

c 复制代码
if(ri > get_days(nian, yue)) 
    ri = get_days(nian, yue);

可以改进的地方

  1. 加个闹钟功能:再加几个变量存闹钟时间,到点了蜂鸣器响
  2. 用DS1302 :掉电不丢时. 加温度显示:接个DS18B20,显示当前温度
  3. 农历显示:这个算法比较复杂,有兴趣可以研究一下

总结

这个项目虽然不复杂,但涉及的知识点还挺全的:定时器、中断、LCD驱动、按键处理、日期算法。做完之后对51单片机的理解会深很多。

完整源码和Proteus仿真工程已经打包上传了,免费下载,文章顶部就能看到。有问题评论区交流~


如果觉得有帮助,点个赞再走呗~ 👍

相关推荐
恒锐丰小吕2 小时前
屹晶微 EG2113D 高压 600V 半桥 MOS 管驱动芯片技术解析
嵌入式硬件·硬件工程
一路往蓝-Anbo12 小时前
【第13期】中断机制详解 :从向量表到ISR
c语言·开发语言·stm32·单片机·嵌入式硬件
ArrebolJiuZhou12 小时前
00 arm开发环境的搭建
linux·arm开发·单片机·嵌入式硬件
易水寒陈12 小时前
使用J-Link RTT Viewer
stm32·单片机
少一倍的优雅13 小时前
hi3863(ws63)智能小车 (三)PWM驱动马达
单片机·嵌入式硬件·hi3863
xingzhemengyou113 小时前
STM32 内存空间中的选项字节
stm32·单片机
v先v关v住v获v取14 小时前
椰子采摘机械臂设计cad9张+三维图+设计说明书
科技·单片机·51单片机
就是蠢啊15 小时前
51单片机——TFTLCD显示器(一)
嵌入式硬件·计算机外设·51单片机
qq_4480111615 小时前
嵌入式中IO、GPIO、专用IO
单片机·嵌入式硬件