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

51单片机课设 | 基于STC89C52的简易电子日历(12864 LCD显示+按键调时)

最近刚做完单片机实训的电子日历项目,趁热乎记录一下整个过程,顺便分享给有需要的同学。

先放个原理图:

Proteus仿真原理图

Proteus仿真注意:单片机晶振频率要设置成 11.0592MHz,不是12MHz,不然时序可能会有问题。


项目简介

做的是一个简易电子日历,功能不复杂:

  • 显示年月日、星期、时分秒
  • 三个按键可以调节时间
  • 编辑时对应项会闪烁提示
  • 5秒不操作自动退出编辑

硬件用的是 STC89C52 + 12864 LCD(KS0108控制器),Proteus仿真用的AT89C51。

💡 兼容性说明:代码兼容整个51系列,AT89C51、AT89C52、STC89C52等都能直接用,不用改。


硬件部分

元器件清单

元器件型号/规格数量
单片机STC89C52 / AT89C511
LCD显示屏12864(KS0108控制器)1
晶振12MHz1
瓷片电容30pF2
电解电容10μF1
电阻10KΩ4
排阻10KΩ×81
电位器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. 闰年判断
uchar is_leap(uint y)
{
    if((y % 4 == 0 && y % 100 != 0) || (y % 400 == 0))
        return 1;
    return 0;
}

这个应该都知道:能被4整除但不能被100整除,或者能被400整除,就是闰年。

2. 蔡勒公式算星期

这个算法挺有意思的,可以算出任意日期是星期几:

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. 获取月份天数
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列开始写数据:

sel_left();           // 选择左半屏
write_cmd(0xB8 + 2);  // 设置页地址 = 2
write_cmd(0x40 + 10); // 设置列地址 = 10
write_data(0xFF);     // 写入数据,这一列8个点全亮

写完一个字节后,列地址会自动加1,所以连续写多个字节就能画出一行。

字模数据格式

以数字"0"的8×16字模为例:

{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的数字。

显示的时候要分两次写:

// 写上半部分
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页

基本操作函数

// 等待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+nn=0~7
设置列0x40+nn=0~63
设置起始行0xC0+n用于滚屏

按键处理

传统消抖的问题

一开始用的是传统的延时消抖:

if(K1 == 0) {
    delay(10);
    if(K1 == 0) {
        // 处理
        while(K1 == 0);  // 等松手
    }
}

问题是这样会阻塞,按住按键的时候整个程序都卡住了,时钟也不走了。

改用边沿检测

后来改成了边沿检测,用静态变量记住上次的按键状态:

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。

if(ri > get_days(nian, yue)) 
    ri = get_days(nian, yue);

定时器中断

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 变量控制闪烁:

if(mode == 1 && flash) {
    // 清除年份显示区域(闪烁效果)
    clear_num_L(0, 8);
    // ...
} else {
    // 正常显示年份
    show_num_L(0, 8, nian / 1000);
    // ...
}

完整代码

代码比较长,直接贴在下面。字模数据是用取模软件生成的,16×16的汉字和8×16的数字。

/*******************************************************************************
 * 简易电子日历
 * 单片机: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号。

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

if(ri > get_days(nian, yue)) 
    ri = get_days(nian, yue);

可以改进的地方

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

总结

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

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


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

单片机C语言程序设计实训100基于8051+Proteus仿真源码: 第 01 篇 基础部分 01 闪烁的LED 02 从左到右的流水灯 03 左右来回的流水灯 04 花样流水灯 05 LED模拟交通灯 06 单只数码管循环显示0-9 07 8只数码管滚动显示单个数字 08 8只数码管显示多个不同字符 09 8只数码管闪烁显示 10 8只数码管滚动显示数字串 11 K1-K4 控制LED移位 12 K1-K4 键状态显示 13 K1-K4 分组控制LED 14 K1-K4 控制数码管移位显示 15 K1-K4 控制数码管加减演示 16 4×4键盘矩阵控制条形LED显示 17 数码管显示4×4键盘矩阵按键 18 开关控制LED 19 继电器控制照明设备 20 数码管显示拨码开关编码 21 开关控制报警器 22 按键发音 23 播放一段音乐 24 INT0中断计数 25 INT0中断控制LED 26 INT0及INT1中断计数 27 TIMER0控制单只LED闪烁 28 TIMER0控制流水灯 29 TIMER0控制四只LED滚动闪烁 30 TIMER0控制LED二进制计数 31 TIMER0与TIMER1控制条形LED 32 10秒的秒表 33 用计数器中断实现100以内的按键计数 34 100000秒以内的计程序 35 定器控制数码动态显示 35 定器控制数码管动管显示 36 8×8LED点阵屏显示数字 37 按键控制8×8LED点阵屏显示图形 38 用定器设计的门铃 39 演奏一段音阶 40 按键控制定器选播多段音乐 41 定器控制交通指示灯 42 报警器与旋转灯 43 串行数据转换为并行数据 44 并行数据转换为串行数据 45 甲机通过串口控制乙机LED闪烁 46 单片机之间双向通信 47 单片机向主机发送字符串 48 单片机与PC机串口通讯仿真 第 02 篇 硬件应用 01 74LS138译码器应用 02 74HC154译码器应用 03 74HC595串入并出芯片应用 04 74LS148扩展中断 05 IIC-24C04与蜂鸣器 06 IIC-24C04与数码管 07 6264扩展内存 08 用8255实现接口扩展 09 555的应用 10 BCD译码数码管显示数字 11 MAX7221控制数码管动态显示 12 1602字符液晶滚动演示程序 13 1602液晶显示的DS1302实钟 14 12864LCD图形滚动演示 15 160128LCD图文演示 16 2×20串行字符液晶演示 17 开关控制12864LCD串行模式显示 18 ADC0832模数转换与显示 19 ADC0808 PWM实验 20 ADC0809模数转换与显示 21 用DAC0832生成锯齿波 22 用DAC0808实现数字压 23 PCF8591模数与数模转换实验 24 DS1621温度传感器实验 25 DS18B20温度传感器实验 26 正反转可控的直流电机 27 正反转可控的步进电机 28 键控看门狗 第 03 篇 综合设计 01 可以控的走马灯 02 按键选播电子音乐 03 可演奏的电子琴 04 1602LCD显示仿手机键盘按键字符 05 1602LCD显示电话拨号键盘按键实验 06 12864LCD显示计算器键盘按键实验 07 数码管随机模拟显示乘法口诀 08 1602LCD随机模拟显示乘法口诀 09 用数码管设计的可电子钟 10 用1602LCD设计的可电子钟 11 用DS1302与数码管设计的可电子表 12 用DS1302与1602LCD设计的可电子日历钟 13 用DS1302与12864LCD设计的可式中文电子日历 14 用PG12864LCD设计的指针式电子钟 15 高仿真数码管电子钟 16 1602LCD显示的秒表 17 数码管显示的频率计 18 字符液晶显示的频率计 19 用ADC0832节频率输出 20 用ADC0832设计的两路电压表 21 用数码管与DS18B20设计温度报警器 22 用1602LCD与DS18B20设计的温度报警器 23 数码管显示的温控电机 24 温度控制直流电机转速 25 用ADC0808设计的温报警器 26 160128LCD中文显示温度与间 27 用DAC0808设计的直流电机速器 28 160128液晶中文显示ADC0832两路模数转换结果 29 160128液晶曲线显示ADC0832两路模数转换结果 30 串口发送数据到2片8×8点阵屏滚动显示 31 用74HC595与74LS154设计的16×16点阵屏 32 用8255与74LS154设计的16×16点阵屏 33 8×8LED点阵屏仿电梯数字滚动显示 34 用24C04与1602LCD
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值