本文前半部分直接给出实现(注意进位问题是秒->分->小时,用 if 嵌套即可实现),后半部分讲解定时器和中断系统。
效果展示:

LCD1602电路图:

项目结构:

代码实现:
main.c
cpp
#include <REGX52.H> //包含51单片机寄存器定义头文件
#include "LCD1602.h" // 包含LCD1602液晶驱动头文件
#include "Timer0.h" // 包含定时器T0驱动头文件
// 定义时间变量并初始化(23:59:57)
unsigned int Hour = 23;
unsigned int Min = 59;
unsigned int Sec = 57;
void main() {
LCD_Init(); // 初始化LCD1602液晶显示屏
Timer0Init(); // 初始化定时器T0
// 在LCD上显示静态文本
LCD_ShowString(1, 1, "Clock:"); // 第1行第1列显示"Clock:"
LCD_ShowString(2, 3, ":"); // 第2行第3列显示冒号(分隔小时和分钟)
LCD_ShowString(2, 6, ":"); // 第2行第6列显示冒号(分隔分钟和秒钟)
while(1) {
// 持续更新时间显示(动态刷新)
LCD_ShowNum(2, 1, Hour, 2); // 第2行第1列显示2位小时数
LCD_ShowNum(2, 4, Min, 2); // 第2行第4列显示2位分钟数
LCD_ShowNum(2, 7, Sec, 2); // 第2行第7列显示2位秒钟数
}
}
// 定时器T0中断服务函数(中断号1)
void Timer0_Routine() interrupt 1 {
static unsigned int T0Count; // 静态变量,用于计数中断次数
// 重新装载定时器初值(配置为1ms中断一次,实际需根据晶振频率计算)
TL0 = 0x66; // 定时器低位初值
TH0 = 0xFC; // 定时器高位初值
T0Count++; // 中断次数计数器自增
// 当计数达到1000次(约1秒)时更新时间
if(T0Count >= 1000) {
T0Count = 0; // 重置计数器
Sec++; // 秒钟加1
// 时间进位处理
if(Sec >= 60) { // 超过59秒
Sec = 0; // 秒钟归零
Min++; // 分钟加1
if(Min >= 60) { // 超过59分钟
Min = 0; // 分钟归零
Hour++; // 小时加1
if(Hour >= 24) { // 超过23小时
Hour = 0; // 小时归零
}
}
}
}
}
LCD1602.h
cpp
#ifndef __LCD1602_H__
#define __LCD1602_H__
//用户调用函数:
void LCD_Init();
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char);
void LCD_ShowString(unsigned char Line,unsigned char Column,char *String);
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length);
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
#endif
LCD1602.c
cpp
#include <REGX52.H>
//引脚配置:
sbit LCD_RS=P2^6;
sbit LCD_RW=P2^5;
sbit LCD_EN=P2^7;
#define LCD_DataPort P0
//函数定义:
/**
* @brief LCD1602延时函数,12MHz调用可延时1ms
* @param 无
* @retval 无
*/
void LCD_Delay()
{
unsigned char i, j;
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
}
/**
* @brief LCD1602写命令
* @param Command 要写入的命令
* @retval 无
*/
void LCD_WriteCommand(unsigned char Command)
{
LCD_RS=0;
LCD_RW=0;
LCD_DataPort=Command;
LCD_EN=1;
LCD_Delay();
LCD_EN=0;
LCD_Delay();
}
/**
* @brief LCD1602写数据
* @param Data 要写入的数据
* @retval 无
*/
void LCD_WriteData(unsigned char Data)
{
LCD_RS=1;
LCD_RW=0;
LCD_DataPort=Data;
LCD_EN=1;
LCD_Delay();
LCD_EN=0;
LCD_Delay();
}
/**
* @brief LCD1602设置光标位置
* @param Line 行位置,范围:1~2
* @param Column 列位置,范围:1~16
* @retval 无
*/
void LCD_SetCursor(unsigned char Line,unsigned char Column)
{
if(Line==1)
{
LCD_WriteCommand(0x80|(Column-1));
}
else if(Line==2)
{
LCD_WriteCommand(0x80|(Column-1+0x40));
}
}
/**
* @brief LCD1602初始化函数
* @param 无
* @retval 无
*/
void LCD_Init()
{
LCD_WriteCommand(0x38);//八位数据接口,两行显示,5*7点阵
LCD_WriteCommand(0x0c);//显示开,光标关,闪烁关
LCD_WriteCommand(0x06);//数据读写操作后,光标自动加一,画面不动
LCD_WriteCommand(0x01);//光标复位,清屏
}
/**
* @brief 在LCD1602指定位置上显示一个字符
* @param Line 行位置,范围:1~2
* @param Column 列位置,范围:1~16
* @param Char 要显示的字符
* @retval 无
*/
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char)
{
LCD_SetCursor(Line,Column);
LCD_WriteData(Char);
}
/**
* @brief 在LCD1602指定位置开始显示所给字符串
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param String 要显示的字符串
* @retval 无
*/
void LCD_ShowString(unsigned char Line,unsigned char Column,char *String)
{
unsigned char i;
LCD_SetCursor(Line,Column);
for(i=0;String[i]!='\0';i++)
{
LCD_WriteData(String[i]);
}
}
/**
* @brief 返回值=X的Y次方
*/
int LCD_Pow(int X,int Y)
{
unsigned char i;
int Result=1;
for(i=0;i<Y;i++)
{
Result*=X;
}
return Result;
}
/**
* @brief 在LCD1602指定位置开始显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~65535
* @param Length 要显示数字的长度,范围:1~5
* @retval 无
*/
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
unsigned char i;
LCD_SetCursor(Line,Column);
for(i=Length;i>0;i--)
{
LCD_WriteData(Number/LCD_Pow(10,i-1)%10+'0');
}
}
/**
* @brief 在LCD1602指定位置开始以有符号十进制显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:-32768~32767
* @param Length 要显示数字的长度,范围:1~5
* @retval 无
*/
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length)
{
unsigned char i;
unsigned int Number1;
LCD_SetCursor(Line,Column);
if(Number>=0)
{
LCD_WriteData('+');
Number1=Number;
}
else
{
LCD_WriteData('-');
Number1=-Number;
}
for(i=Length;i>0;i--)
{
LCD_WriteData(Number1/LCD_Pow(10,i-1)%10+'0');
}
}
/**
* @brief 在LCD1602指定位置开始以十六进制显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~0xFFFF
* @param Length 要显示数字的长度,范围:1~4
* @retval 无
*/
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
unsigned char i,SingleNumber;
LCD_SetCursor(Line,Column);
for(i=Length;i>0;i--)
{
SingleNumber=Number/LCD_Pow(16,i-1)%16;
if(SingleNumber<10)
{
LCD_WriteData(SingleNumber+'0');
}
else
{
LCD_WriteData(SingleNumber-10+'A');
}
}
}
/**
* @brief 在LCD1602指定位置开始以二进制显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~1111 1111 1111 1111
* @param Length 要显示数字的长度,范围:1~16
* @retval 无
*/
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
unsigned char i;
LCD_SetCursor(Line,Column);
for(i=Length;i>0;i--)
{
LCD_WriteData(Number/LCD_Pow(2,i-1)%2+'0');
}
}
Timer0.h
cpp
#ifndef __TIMER0_H__
#ifndef __TIMER0_H__
void Timer0Init(void);
#endif
Timer0.c
cpp
#include <REGX52.H>
/**
* @brief 定时器0初始化,1毫秒@11.0592MHZ
* @param 无
* @retval 无
*/
void Timer0Init(void) //1毫秒@11.0592MHz
{
TMOD &= 0xF0; //清零低4位(对应定时器0的配置),保留高4位(定时器1的配置)不变
TMOD |= 0x01; //设置定时器0为模式1(二进制0000 0001中的低4位),即16位定时器模式
TL0 = 0x66; //设置定时初值
TH0 = 0xFC; //设置定时初值
TF0 = 0; //清除定时器0溢出标志位,防止初始化后立即触发中断
TR0 = 1; //启动定时器0,开始计数
ET0=1; //开启定时器0中断(ET0)和全局中断(EA),允许中断触发
EA=1;
PT0=0; //设置定时器0中断优先级为低优先级
}
//定时器通用模板
//void Timer0_Routine() interrupt 1{
// static unsigned int T0Count;
// TL0 = 0x66; //设置定时初值
// TH0 = 0xFC; //设置定时初值
// T0Count++;
// if(T0Count>=1000)
// {
// T0Count=0;
// }
//}
定时器和中断系统:
1.定时器
**定时器介绍:**51单片机定时器属于单片机的内部资源,其电连接和运转均在单片机内部完成。
定时器的作用:
- 基于时钟信号计数,提供精准的时间基准(如微秒/毫秒级延时)
- 替代长时间的Delay,提高CPU的运行效率和处理速度
- 当计数值达到预设阈值时,触发中断,执行特定任务(如周期任务调度)
- 通过调节占空比和频率,输出脉宽调制信号(PWM)
STC89C52定时器资源:
- 定时器个数:3个(T0、T1、T2),T0和T1与传统的51单片机兼容,T2是此型号单片机增加的资源
- 注意:定时器的资源和单片机的型号是关联在一起的,不同的型号可能会有不同的定时器个数和操作方式,但一般来说,T0和T1的操作方式是所有51单片机所共有的
定时器工作原理框图:
- 定时器在单片机的内部就像一个小闹钟一样,根据时钟的输出信号,每隔"一秒",计数单元的数值就增加一,当计数单元数值增加到"设定的闹钟提醒时间"时,计数单元就会向中断系统发出中断申请,产生"闹钟提醒",使程序跳到中断服务函数中执行。

定时器工作模式:
- STC89C52的T0和T1均有四种工作模式:
模式0:13位定时器/计数器
模式1:16位定时器/计数器(常用)
模式2:8位自动重装模式
模式3:两个8位计数器
定时器/计数器除模式3外,其他工作模式与定时器/计数器0相同,T1在模式3时无效,停止计数。
- 下面以模式1(16位定时器/计数器)为例:

一、时钟
- SYSclk:系统时钟(System Clock),即晶振周期,单片机核心工作频率,决定指令执行速度。本开发板上晶振为11.0592MHz。
- ÷12/÷6:分频器
12T模式:每12个时钟周期构成1个机器周期(速度较慢,兼容传统设计)。
6T模式:每6个时钟周期构成1个机器周期(速度更快,适合高性能场景)。
- C/
:模式选择位(Counter/Timer Select Bit)
C/=0:定时器模式,对分频后的系统时钟脉冲计数(用于计时)。
C/=1:计数器模式,对T0 Pin(外部引脚)的脉冲信号计数(用于统计外部事件)。
二、计数单元

- TR0:定时器0运行控制位(Timer 0 Run Control)。
TR0=1:启动定时器/计数器;TR0=0:停止。
- GATE:门控位。
GATE=1:定时器启动需要满足TR0=1 且=1(外部中断0引脚高电平)。
GATE=0:仅需TR0=1即可启动。
:外部中断0引脚,用于门控模式下协同控制。
- 16位寄存器:
TL0(8 Bits):低8位计数器,存储计数值低字节。
TH0(8 Bits):高8位计数器,存储计数值高字节。
三、中断
- TF0:定时器0溢出标志(Timer 0 Overflow Flag)
当计数器从65535加1变为0时,TF0自动置1,触发中断请求。
- Interrupt:中断信号,通知CPU处理定时器溢出事件(例如更新显示、执行任务)。
2.中断系统
当中央处理机CPU正在处理某件事的时候外界发生了紧急事件请求,要求CPU暂停当前
的工作,转而去处理这个紧急事件,处理完以后,再回到原来被中断的地方,继续原来的工
作,这样的过程称为中断。实现这种功能的部件称为中断系统,请示CPU中断的请求源称为中
断源。
举个煮面例子说明:

关键对应关系:
|----------|------------|---------------------|
| 厨房场景 | 中断系统 | 电路图对应(TF0中断) |
| 煮面流程 | 主程序执行 | 定时器正常计数 (TL0/TH0累加) |
| 电话响铃 | 中断源触发 | 定时器溢出导致TF0=1,触发中断 |
| 关火并记住进度 | 保存CPU现场 | 硬件自动保存程序计数器(PC) |
| 接电话 | 执行中断服务程序 | 跳转到中断向量表执行定时器中断代码 |
| 重新开火继续煮面 | 恢复现场并继续主程序 | RETI指令返回,恢复PC继续主流程 |
下图以STC89C51RC/RD+系列讲解中断系统结构示意图:

一、中断源与触发机制
图中左侧标注了 8个中断源,按触发类型分为三类:
- 外部中断(4个)
INT0(外部中断0):
触发方式:由 TCON.0/IT0
位设置(0=低电平触发,1=下降沿触发)。
标志位:IE0
(TCON.1),触发后自动置1。
INT1/INT2/INT3(外部中断1/2/3):类似INT0,分别由 TCON.2/IT1
、XICON.0/IT2
、XICON.2/IT3
控制触发方式,标志位为 IE1/IE2/IE3
。
- 定时器中断(3个)
Timer0/TF0:定时器0溢出时 TF0
(TCON.5)置1,允许位 ET0
(IE.1)。
Timer1/TF1:定时器1溢出时 TF1
(TCON.7)置1,允许位 ET1
(IE.3)。
Timer2/TF2/EXF2:溢出触发 TF2
(T2CON.7),外部触发 EXF2
(T2CON.6),允许位 ET2
(IE.5)。
- 串口中断(UART)
接收中断:RI
(SCON.0)置1时触发。
发送中断:TI
(SCON.1)置1时触发。
统一允许位 ES
(IE.4)。
二、中断允许控制寄存器
- IE寄存器(Interrupt Enable)
全局控制:EA=1
(IE.7)时总中断允许生效。
分项控制:
EX0=1
(IE.0):允许外部中断0
ET0=1
(IE.1):允许定时器0中断
EX1=1
(IE.2):允许外部中断1
ET1=1
(IE.3):允许定时器1中断
ES=1
(IE.4):允许串口中断
ET2=1
(IE.5):允许定时器2中断
- XICON寄存器(扩展中断控制)
扩展允许位:EX2=1
(XICON.4)、EX3=1
(XICON.5)分别允许外部中断2/3。
触发方式:IT2
(XICON.0)、IT3
(XICON.2)设置边沿/电平触发。
三、中断优先级控制
- IP/IPH寄存器(Interrupt Priority)
标准优先级(IP):每个中断源对应1位(0=低优先级,1=高优先级)。
例如:PX0=1
(IP.0)设置外部中断0为高优先级。
扩展优先级(IPH):与IP组合实现 4级优先级(STC特有功能):
PX0H:PX0
(IPH.0:IP.0)= 00(0级)→11(3级),数值越大优先级越高。
- 中断查询次序,图中右侧竖列标注了中断响应顺序(优先级相同时的默认查询次序):
INT0->Timer0->INT1->Timer1->UART->Timer2(优先级 高->低)
3.配置定时器和中断示例
cpp
#include <REGX52.H>
/**
* @brief 定时器0初始化,1毫秒@11.0592MHZ
* @param 无
* @retval 无
*/
void Timer0Init(void) //1毫秒@11.0592MHz
{
TMOD &= 0xF0; //清零低4位(对应定时器0的配置),保留高4位(定时器1的配置)不变
TMOD |= 0x01; //设置定时器0为模式1(二进制0000 0001中的低4位),即16位定时器模式
TL0 = 0x66; //设置定时初值
TH0 = 0xFC; //设置定时初值
TF0 = 0; //清除定时器0溢出标志位,防止初始化后立即触发中断
TR0 = 1; //启动定时器0,开始计数
ET0=1; //开启定时器0中断(ET0)和全局中断(EA),允许中断触发
EA=1;
PT0=0; //设置定时器0中断优先级为低优先级
}
//定时器通用模板
//void Timer0_Routine() interrupt 1{
// static unsigned int T0Count;
// TL0 = 0x66; //设置定时初值
// TH0 = 0xFC; //设置定时初值
// T0Count++;
// if(T0Count>=1000)
// {
// T0Count=0;
// }
//}