基于单片机的多功能LCD万年历时钟设计与温度显示系统

1、基于单片机的多功能LCD万年历时钟设计与温度显示系统

点击链接下载prrotues仿真设计资料:https://download.csdn.net/download/m0_51061483/92081498

1.1、系统概述

本设计以单片机为控制核心,构建一套集"万年历时钟显示、时间校准与设置、温度采集与显示、人机按键交互"为一体的多功能LCD万年历时钟系统。系统能够在LCD屏幕上实时显示完整的年月日时分秒信息,并支持闰年判断、月份天数自动切换等万年历功能。同时,系统通过DS18B20数字温度传感器采集环境温度,实现温度的实时显示。为了满足实际使用需求,系统配备按键操作,可用于时间的修改、参数设置与校准,确保时钟在长期运行中仍能保持准确性和良好的可用性。

在硬件结构上,系统主要由单片机最小系统模块、LCD显示模块、时钟计时模块(可采用DS1302/DS3231或软件计时)、DS18B20温度采集模块、按键输入模块、电源模块、蜂鸣器或提示模块(可扩展)等构成。

在软件结构上,系统采用模块化设计思想,将显示刷新、按键扫描、时间计时、万年历计算、温度读取、设置界面逻辑等分别封装为独立模块,并通过定时中断产生系统节拍,实现稳定、实时、低延迟的运行效果。

系统适用于桌面电子钟、学习型嵌入式作品、智能家居显示终端等场景。通过本设计,能够掌握单片机基础外设驱动、LCD显示、1-Wire总线通信、时间与日期算法、按键消抖与交互状态机设计等关键知识点,具有较强的综合实践意义。


2、系统功能设计

2.1、LCD显示完整时间信息(年月日时分秒)

系统能够在LCD屏幕上显示当前时间的完整信息,包括:

  • 年:YYYY
  • 月:MM
  • 日:DD
  • 时:HH(24小时制)
  • 分:MM
  • 秒:SS

典型显示形式可为两行显示:

  • 第一行:2026-01-05
  • 第二行:12:34:56 25.3C

其中第一行主要显示日期信息,第二行显示时间与温度信息。通过合理布局,用户能够在同一屏幕中获得日期、时间与温度的综合信息。

为了保证显示观感良好,系统在显示时应考虑:

  • 数字补零(如 01 月、09 分)
  • 固定位置刷新(避免屏幕闪烁)
  • 采用缓存字符串刷新方式(先拼接字符串再输出)
  • 时间变化频率为1秒刷新,温度可每500ms或1s刷新一次

2.2、支持时间校准功能,确保时间准确

系统提供时间校准功能,通过按键操作进入设置模式,对时间进行手动调整。校准对象包括:

时间校准不仅用于初次上电设定,也用于长期运行后因晶振误差导致的时间偏差修正。对于使用软件计时的方案,系统可以采用定时器中断每秒累加时间;对于使用硬件RTC(如DS1302/DS3231)的方案,则可通过串行接口读写时钟芯片寄存器实现更高精度的计时。

在本设计中,为保证功能完整并便于扩展,推荐采用"外接RTC + 单片机显示与管理"结构,能够有效减轻单片机计时误差问题,同时在掉电后保持时间不丢失。

2.3、DS18B20采集温度并实时显示

系统通过DS18B20温度传感器读取环境温度,并将温度值转换成可读格式显示在LCD上。DS18B20具备如下优点:

  • 数字输出,抗干扰能力强
  • 采用1-Wire单总线,节省IO口
  • 分辨率可配置(9~12位)
  • 温度范围广(-55℃到+125℃)
  • 精度较高(典型±0.5℃)

系统读取温度的基本流程为:

1)初始化1-Wire总线

2)发送温度转换命令(Convert T)

3)等待转换完成(典型750ms@12位)

4)读取温度寄存器(Scratchpad)

5)转换成摄氏温度并显示

为了提升体验,系统可采用"温度数据缓存 + 周期更新"的策略:

  • 每秒触发一次温度读取任务
  • 将温度值保存为整数或带小数形式
  • LCD刷新时直接读取缓存温度值
    这样可以避免频繁读取导致的延迟或影响主循环实时性。

2.4、按键操作实现时间修改与设置

按键交互是本系统的重要组成部分,它决定了系统的可操作性和用户体验。按键功能通常包括:

  • 模式键(MODE):进入/退出设置模式
  • 切换键(NEXT):在年/月/日/时/分/秒之间切换
  • 增加键(UP):数值加1或快速增加
  • 减少键(DOWN):数值减1或快速减少
  • 确认键(OK):保存并退出(可选)

系统在按键逻辑中需要重点解决的问题:

  • 软件消抖(避免一次按键产生多次触发)
  • 长按加速(提高设置效率)
  • 参数边界与合法性检查(如月份1~12、日期依据月份变化)
  • 设置过程的显示提示(闪烁当前字段、显示"SET"标志等)

良好的按键交互应做到:

  • 进入设置后能清晰知道当前调整的是哪个字段
  • 调整后的时间合法且不出现错误日期
  • 保存退出时立刻生效并继续计时

3、电路设计

3.1、硬件总体结构说明

系统硬件模块可划分为:

1)单片机最小系统模块

2)电源模块

3)LCD显示模块(LCD1602或LCD12864等)

4)时钟计时模块(RTC:DS1302/DS3231 或软件计时)

5)温度采集模块(DS18B20)

6)按键输入模块

7)蜂鸣器/提示模块(可选扩展)

本设计可选择LCD1602作为显示器件,以字符显示形式实现日期、时间和温度显示。单片机可使用STC89C52/AT89S52等51内核芯片,也可使用AVR或STM8等,本文以经典51单片机为设计基准进行描述,便于教学与实现。


3.2、单片机最小系统模块

单片机最小系统包含单片机、晶振电路、复位电路以及必要的电源去耦电容。其作用是为整个系统提供稳定运行的控制核心。

3.2.1、时钟晶振电路

晶振为单片机提供系统时钟,常用频率包括:

  • 11.0592MHz:利于串口通信波特率整除
  • 12MHz:传统经典频率,便于定时计算

晶振两端一般连接两个负载电容(通常20pF~33pF),形成稳定振荡。晶振布局要尽量靠近单片机时钟引脚,走线短且远离强干扰源,以保证振荡稳定。

3.2.2、复位电路

复位电路用于上电复位与按键复位,常用RC上电复位方式:

  • 上电瞬间电容充电形成复位高电平
  • 延迟一定时间后复位信号释放
    同时可增加手动复位按键,在系统运行异常时可人为触发复位。

3.2.3、电源去耦与稳定性设计

单片机VCC与GND之间应并联0.1uF去耦电容,并尽量靠近芯片电源引脚,以滤除高频噪声。对时钟芯片、LCD、温度传感器等也应配置适当滤波电容,形成整体稳定的电源网络。


3.3、电源模块

本系统通常采用5V供电。电源来源可为:

  • USB供电(5V)
  • 9V~12V适配器输入后稳压到5V
  • 电池供电(需升压/稳压)

稳压方案:

  • 线性稳压7805:电路简单但发热较大
  • DC-DC降压模块:效率高,适合长期运行

电源模块设计应考虑:

  • 输入保护(防反接、保险丝)
  • 输出滤波(电解电容 + 陶瓷电容)
  • LCD背光电流较大,供电线需足够
  • RTC与DS18B20对电源纹波敏感,建议靠近器件加小电容滤波

如果采用RTC芯片,通常还需要纽扣电池备份电源,以实现掉电保持时间功能。例如DS1302常用CR2032供电作为后备电源。


3.4、LCD显示模块

LCD作为主要人机界面,本系统可选用LCD1602或LCD12864。

  • LCD1602:字符型显示,成本低,驱动简单
  • LCD12864:图形点阵,可显示更多内容但驱动复杂

本设计以LCD1602为例,采用4位数据模式节省IO口。LCD1602常用引脚包括:

  • RS:数据/命令选择
  • RW:读写选择(通常接地仅写)
  • EN:使能脉冲
  • D4~D7:数据线(4位模式)
  • V0:对比度调节(接电位器)
  • LED+/LED-:背光供电

显示布局设计:

  • 第一行显示日期:YYYY-MM-DD
  • 第二行显示时间与温度:HH:MM:SS XX.XC

为了避免闪烁,建议每秒刷新一次整个屏幕,而在秒变化时只更新秒字段亦可进一步优化显示效果。


3.5、时钟计时模块(RTC或软件计时)

万年历时钟系统最关键的模块是计时模块,它决定了时间显示是否准确、掉电是否保持。常用方案有两种:

3.5.1、方案一:外接RTC芯片(推荐)

常用RTC芯片:

  • DS1302:三线接口,成本低,使用广泛
  • DS3231:I2C接口,内置温补晶振,精度高

采用RTC芯片的优点:

  • 计时精度高,误差小
  • 掉电后可通过纽扣电池继续运行
  • 单片机只需读取寄存器即可获得准确时间
  • 软件实现更稳定,抗干扰更好

3.5.2、方案二:单片机软件计时

如果不使用RTC,可通过定时器中断实现秒计时:

  • 定时器每1ms中断一次
  • 计数到1000次为1秒
  • 每秒更新时分秒,并进行日期进位与闰年判断

缺点是:

  • 受晶振误差影响,长期偏差明显
  • 掉电时间丢失
  • 软件逻辑复杂,对中断精度要求高

本系统建议采用RTC作为计时核心,同时保留软件万年历算法用于日期合法性校验与设置时校正边界。


3.6、DS18B20温度采集模块

DS18B20采用1-Wire总线通信,仅需一根数据线即可实现数据交换。硬件连接要点:

  • 数据线接单片机IO口
  • 数据线上必须加上拉电阻(一般4.7kΩ)到VCC
  • 传感器供电可采用外部供电模式(VDD接5V)
  • 布线尽量短,避免与高干扰信号并行

DS18B20读取温度时序严格,需要软件精确延时支持。设计中要保证:

  • 初始化复位脉冲时序准确
  • 写0/写1、读位时序符合规范
  • 读取数据后进行CRC校验(可选)提高可靠性

3.7、按键输入模块

按键模块是人机交互核心。常见硬件实现方式:

  • 独立按键:每个按键占用一个IO口
  • 矩阵键盘:节省IO口,但扫描复杂

对于时间设置功能,通常使用4个独立按键即可满足需求:MODE、NEXT、UP、DOWN。按键设计需要:

  • 使用上拉电阻或利用单片机内部上拉
  • 按键按下输出低电平或高电平,保证逻辑一致
  • 软件消抖是必须的,硬件可加小电容辅助消抖(可选)

3.8、蜂鸣器或提示模块(可选扩展)

虽然题目未强制要求蜂鸣器,但为了增强交互体验,可扩展蜂鸣器作为:

  • 按键提示音
  • 时间设置完成提示
  • 整点报时提示(扩展功能)

蜂鸣器驱动可使用有源蜂鸣器(IO直接控制)或无源蜂鸣器(PWM驱动),并通过三极管放大电流。


4、程序设计

4.1、软件总体架构与运行机制

系统软件推荐采用"主循环 + 定时中断"的结构:

  • 定时器产生1ms系统节拍
  • 10ms执行一次按键扫描任务
  • 500ms或1s执行一次温度读取任务
  • 1s执行一次时间刷新任务
  • LCD周期性刷新显示内容
  • 设置模式下,显示当前字段闪烁或加标识

软件模块化结构如下:

1)系统初始化模块

2)LCD驱动模块

3)RTC驱动模块(若使用DS1302/DS3231)

4)万年历算法模块(闰年判断、天数计算、日期合法性)

5)DS18B20驱动模块

6)按键扫描与设置状态机模块

7)显示格式化与刷新模块

8)可选蜂鸣器提示模块

通过这种结构,系统具有较好的扩展性和可维护性,后续可加入整点报时、闹钟、星期显示等功能而不破坏原有框架。


4.2、系统初始化模块

系统初始化主要任务:

  • IO口方向配置
  • 定时器初始化(1ms中断)
  • LCD初始化并清屏
  • RTC初始化(若存在)
  • DS18B20初始化(检测传感器是否存在)
  • 时间变量与温度变量默认值设置
  • 进入正常显示模式

初始化应尽量避免过长阻塞,尤其是LCD初始化与温度初始化需要适当延时,但延时应在可控范围内。


4.3、LCD显示模块设计

LCD显示模块负责:

  • 写命令、写数据
  • 指定位置显示字符串
  • 数值转换为字符串(补零、插入符号)
  • 刷新显示缓存

为了实现稳定显示,建议使用"显示缓冲区"方式:

1)先将日期、时间、温度拼接到两个字符串缓冲区

2)再一次性写入LCD对应行

这样可避免刷新过程中出现字符残留或显示错位。

显示内容格式建议:

  • 第一行:YYYY-MM-DD
  • 第二行:HH:MM:SS XX.XC

其中温度可显示一位小数,若需要更精细可以显示两位小数,但LCD长度有限,需合理布局。


4.4、万年历算法模块设计

万年历算法的核心是日期进位和合法性判断,关键点包括:

  • 闰年判断
  • 每月天数计算
  • 日期进位(秒→分→时→日→月→年)
  • 设置时确保日期合法(例如2月不能设置30日)

4.4.1、闰年判断

闰年规则:

  • 能被4整除且不能被100整除,为闰年
  • 或能被400整除,为闰年

4.4.2、每月天数计算

  • 1,3,5,7,8,10,12月为31天
  • 4,6,9,11月为30天
  • 2月闰年29天,平年28天

4.4.3、日期进位逻辑

当秒达到60,秒归0,分加1;

当分达到60,分归0,时加1;

当时达到24,时归0,日加1;

当日超过本月最大天数,日归1,月加1;

当月超过12,月归1,年加1。

该算法不仅用于正常计时,也用于设置模式下对用户输入进行约束和修正,避免无效日期导致显示错误。


4.5、DS18B20温度采集模块设计

DS18B20驱动是本系统重点模块之一,涉及严格时序控制。软件流程如下:

1)总线复位 → 检测存在脉冲

2)发送Skip ROM(0xCC)跳过ROM

3)发送Convert T(0x44)开始温度转换

4)等待转换完成(可延时或读取状态位)

5)再次复位

6)发送Skip ROM(0xCC)

7)发送Read Scratchpad(0xBE)

8)读取2字节温度值

9)按分辨率换算为摄氏温度

温度数据一般为16位有符号数,低4位为小数部分(默认12位分辨率)。换算可采用整数运算避免浮点。

例如:

  • raw = temp_high<<8 | temp_low
  • 实际温度(0.0625℃/LSB)
  • 若要显示一位小数,可计算:temp_x10 = raw * 10 / 16

读取温度应避免过于频繁,因为转换时间较长。通常1秒读取一次即可满足"实时显示"的需求,同时避免阻塞主循环。


4.6、按键扫描与设置模块设计

按键扫描建议每10ms执行一次。消抖策略:

  • 连续多次检测到同一按键按下才认为有效
  • 检测到按键释放后才产生一次按键事件

设置模式逻辑可设计为状态机:

  • 正常模式:显示时间与温度,按MODE进入设置

  • 设置模式:

    • MODE:进入/退出设置
    • NEXT:选择要修改的字段(年→月→日→时→分→秒)
    • UP/DOWN:修改当前字段数值
    • 退出时保存到RTC或系统时间变量

为了增强提示性,设置模式下可让当前字段闪烁:

  • 每500ms切换一次显示/空白,实现闪烁效果
  • 或在字段前加"^"或"*"标识(LCD1602不支持直接移动光标闪烁时,可用空格覆盖实现)

数值修改时必须保证合法:

  • 月范围1~12
  • 日范围1~本月最大天数
  • 时范围0~23
  • 分秒范围0~59
    如果用户把月份改为2月,则应自动限制日期最大为28/29。

4.7、时间校准与写入模块设计

若系统使用RTC芯片,则时间校准需要写入RTC寄存器。以DS1302为例:

  • 写入前需关闭写保护
  • 写入秒、分、时、日、月、星期、年等寄存器
  • 写入完成后再开启写保护

如果采用软件计时,则需要在设置完成后直接更新系统时间变量,并从下一秒开始继续递增。

为了避免设置过程中计时跳动,通常在进入设置模式后暂停计时累加,退出设置后再恢复计时。


4.8、温度显示与单位处理模块设计

温度显示可选择:

  • 摄氏度:XX.XC
  • 也可扩展华氏度显示

由于LCD1602字符限制,常用显示方式为:

  • 25.3C
  • 若为负温度显示:-3.2C

温度显示涉及符号处理与数值分离:

  • 若温度为负,显示负号并取绝对值
  • 整数部分与小数部分分别计算并拼接
  • 统一保留一位小数即可满足绝大多数显示需求

5、核心程序示例(模块化代码)

5.1、基础数据结构与全局变量

c 复制代码
#include <reg52.h>
#include <string.h>

typedef unsigned char u8;
typedef unsigned int  u16;
typedef unsigned long u32;

// LCD1602 4位模式示例
sbit LCD_RS = P2^6;
sbit LCD_RW = P2^5;
sbit LCD_EN = P2^7;
#define LCD_PORT P0

// 按键(低电平按下)
sbit KEY_MODE = P3^2;
sbit KEY_NEXT = P3^3;
sbit KEY_UP   = P3^4;
sbit KEY_DN   = P3^5;

// DS18B20数据线
sbit DQ = P1^0;

// 系统时间结构体
typedef struct {
    u16 year;
    u8  month;
    u8  day;
    u8  hour;
    u8  min;
    u8  sec;
} datetime_t;

datetime_t now;

// 温度(x10,表示一位小数)
int temp_x10 = 253; // 25.3℃

// 设置模式相关
bit set_mode = 0;
u8 set_field = 0; // 0年 1月 2日 3时 4分 5秒

// 定时标志
volatile u16 ms_tick = 0;
volatile bit flag_10ms = 0;
volatile bit flag_500ms = 0;
volatile bit flag_1000ms = 0;
volatile bit blink = 0;

5.2、定时器初始化与中断节拍模块

c 复制代码
void Timer0_Init_1ms(void)
{
    TMOD &= 0xF0;
    TMOD |= 0x01;     // 定时器0模式1
    TH0 = (65536 - 1000) / 256;  // 12MHz时约1ms(需按实际晶振修正)
    TL0 = (65536 - 1000) % 256;
    ET0 = 1;
    TR0 = 1;
    EA  = 1;
}

void Timer0_ISR(void) interrupt 1
{
    TH0 = (65536 - 1000) / 256;
    TL0 = (65536 - 1000) % 256;

    ms_tick++;

    if(ms_tick % 10 == 0)   flag_10ms = 1;
    if(ms_tick % 500 == 0)  { flag_500ms = 1; blink = !blink; }
    if(ms_tick % 1000 == 0) flag_1000ms = 1;
}

5.3、LCD1602驱动模块(4位模式)

c 复制代码
void LCD_Delay(u16 t)
{
    while(t--);
}

void LCD_Write4(u8 dat)
{
    LCD_PORT = (LCD_PORT & 0x0F) | (dat & 0xF0);
    LCD_EN = 1;
    LCD_Delay(50);
    LCD_EN = 0;
}

void LCD_Cmd(u8 cmd)
{
    LCD_RS = 0;
    LCD_RW = 0;
    LCD_Write4(cmd);
    LCD_Write4(cmd << 4);
    LCD_Delay(2000);
}

void LCD_Dat(u8 dat)
{
    LCD_RS = 1;
    LCD_RW = 0;
    LCD_Write4(dat);
    LCD_Write4(dat << 4);
    LCD_Delay(2000);
}

void LCD_SetPos(u8 row, u8 col)
{
    u8 addr = (row == 0) ? (0x80 + col) : (0xC0 + col);
    LCD_Cmd(addr);
}

void LCD_Str(u8 row, u8 col, char *s)
{
    LCD_SetPos(row, col);
    while(*s) LCD_Dat(*s++);
}

void LCD_Init(void)
{
    LCD_RS = 0;
    LCD_RW = 0;
    LCD_EN = 0;

    LCD_Delay(20000);

    LCD_Write4(0x30);
    LCD_Delay(5000);
    LCD_Write4(0x30);
    LCD_Delay(200);
    LCD_Write4(0x30);
    LCD_Delay(200);

    LCD_Write4(0x20);  // 4位模式
    LCD_Cmd(0x28);     // 2行 5x7
    LCD_Cmd(0x0C);     // 开显示
    LCD_Cmd(0x06);     // 光标右移
    LCD_Cmd(0x01);     // 清屏
}

5.4、万年历算法模块(闰年与天数)

c 复制代码
bit IsLeapYear(u16 y)
{
    if((y % 400) == 0) return 1;
    if((y % 100) == 0) return 0;
    if((y % 4) == 0)   return 1;
    return 0;
}

u8 DaysInMonth(u16 y, u8 m)
{
    switch(m)
    {
        case 1: case 3: case 5: case 7: case 8: case 10: case 12:
            return 31;
        case 4: case 6: case 9: case 11:
            return 30;
        case 2:
            return IsLeapYear(y) ? 29 : 28;
        default:
            return 30;
    }
}

void Time_Tick1s(datetime_t *t)
{
    if(++t->sec >= 60) { t->sec = 0; if(++t->min >= 60) { t->min = 0; if(++t->hour >= 24) {
        t->hour = 0;
        if(++t->day > DaysInMonth(t->year, t->month))
        {
            t->day = 1;
            if(++t->month > 12)
            {
                t->month = 1;
                t->year++;
            }
        }
    }}}}
}

5.5、DS18B20驱动模块(1-Wire时序)

c 复制代码
void DelayUs(u16 us)
{
    while(us--);
}

bit DS18B20_Reset(void)
{
    bit presence;
    DQ = 1;
    DelayUs(5);

    DQ = 0;            // 拉低480us
    DelayUs(500);
    DQ = 1;            // 释放
    DelayUs(70);

    presence = DQ;     // 读取存在脉冲(0表示存在)
    DelayUs(410);
    return (presence == 0);
}

void DS18B20_WriteBit(bit b)
{
    DQ = 0;
    DelayUs(b ? 5 : 60);
    DQ = 1;
    DelayUs(b ? 60 : 5);
}

bit DS18B20_ReadBit(void)
{
    bit b;
    DQ = 0;
    DelayUs(5);
    DQ = 1;
    DelayUs(5);
    b = DQ;
    DelayUs(60);
    return b;
}

void DS18B20_WriteByte(u8 dat)
{
    u8 i;
    for(i=0; i<8; i++)
    {
        DS18B20_WriteBit(dat & 0x01);
        dat >>= 1;
    }
}

u8 DS18B20_ReadByte(void)
{
    u8 i, dat = 0;
    for(i=0; i<8; i++)
    {
        dat >>= 1;
        if(DS18B20_ReadBit()) dat |= 0x80;
    }
    return dat;
}

int DS18B20_ReadTemp_x10(void)
{
    u8 tl, th;
    int raw;
    int t10;

    if(!DS18B20_Reset()) return 9999; // 传感器异常
    DS18B20_WriteByte(0xCC); // Skip ROM
    DS18B20_WriteByte(0x44); // Convert T

    // 等待转换(12位750ms),这里简单延时
    // 实际可根据分辨率优化或用读取状态方式减少等待
    DelayUs(750000);

    if(!DS18B20_Reset()) return 9999;
    DS18B20_WriteByte(0xCC);
    DS18B20_WriteByte(0xBE); // Read Scratchpad

    tl = DS18B20_ReadByte();
    th = DS18B20_ReadByte();

    raw = (th << 8) | tl; // 16位有符号
    // raw单位为1/16℃
    // 转为x10:raw * 10 / 16
    t10 = (raw * 10) / 16;
    return t10;
}

5.6、按键扫描与设置状态机模块

c 复制代码
u8 Key_ReadRaw(void)
{
    if(KEY_MODE == 0) return 1;
    if(KEY_NEXT == 0) return 2;
    if(KEY_UP   == 0) return 3;
    if(KEY_DN   == 0) return 4;
    return 0;
}

u8 Key_Scan(void)
{
    static u8 last = 0;
    static u8 cnt = 0;
    u8 now = Key_ReadRaw();

    if(now == last)
    {
        if(cnt < 3) cnt++;
    }
    else
    {
        cnt = 0;
    }
    last = now;

    if(cnt == 3 && now != 0)
    {
        while(Key_ReadRaw() == now); // 等待松手
        return now;
    }
    return 0;
}

void FixDateIfNeeded(datetime_t *t)
{
    u8 maxd = DaysInMonth(t->year, t->month);
    if(t->day > maxd) t->day = maxd;
    if(t->day < 1) t->day = 1;
}

void Set_ProcessKey(u8 k)
{
    switch(k)
    {
        case 1: // MODE:退出设置
            set_mode = 0;
            set_field = 0;
            break;

        case 2: // NEXT:切换字段
            set_field++;
            if(set_field > 5) set_field = 0;
            break;

        case 3: // UP:增加
            if(set_field == 0) now.year++;
            else if(set_field == 1) { if(++now.month > 12) now.month = 1; FixDateIfNeeded(&now); }
            else if(set_field == 2) { if(++now.day > DaysInMonth(now.year, now.month)) now.day = 1; }
            else if(set_field == 3) { if(++now.hour > 23) now.hour = 0; }
            else if(set_field == 4) { if(++now.min > 59) now.min = 0; }
            else if(set_field == 5) { if(++now.sec > 59) now.sec = 0; }
            break;

        case 4: // DOWN:减少
            if(set_field == 0) { if(now.year > 2000) now.year--; }
            else if(set_field == 1) { if(now.month <= 1) now.month = 12; else now.month--; FixDateIfNeeded(&now); }
            else if(set_field == 2) { if(now.day <= 1) now.day = DaysInMonth(now.year, now.month); else now.day--; }
            else if(set_field == 3) { if(now.hour == 0) now.hour = 23; else now.hour--; }
            else if(set_field == 4) { if(now.min == 0) now.min = 59; else now.min--; }
            else if(set_field == 5) { if(now.sec == 0) now.sec = 59; else now.sec--; }
            break;
    }
}

void Key_Process(void)
{
    u8 k = Key_Scan();
    if(k == 0) return;

    if(!set_mode)
    {
        if(k == 1) { set_mode = 1; set_field = 0; }
    }
    else
    {
        Set_ProcessKey(k);
    }
}

5.7、显示格式化与刷新模块

c 复制代码
void TwoDigit(u8 v, char *buf)
{
    buf[0] = (v / 10) + '0';
    buf[1] = (v % 10) + '0';
}

void FourDigit(u16 v, char *buf)
{
    buf[0] = (v / 1000) % 10 + '0';
    buf[1] = (v / 100)  % 10 + '0';
    buf[2] = (v / 10)   % 10 + '0';
    buf[3] = (v % 10) + '0';
}

void TempToStr(int t10, char *buf)
{
    // 格式:-XX.XC 或 XX.XC
    int abs = t10;
    u8 neg = 0;

    if(t10 < 0) { neg = 1; abs = -t10; }

    buf[0] = neg ? '-' : ' ';
    buf[1] = (abs / 100) % 10 + '0';
    buf[2] = (abs / 10) % 10 + '0';
    buf[3] = '.';
    buf[4] = (abs % 10) + '0';
    buf[5] = 'C';
    buf[6] = '\0';
}

void LCD_Refresh(void)
{
    char line1[17] = "0000-00-00      ";
    char line2[17] = "00:00:00  00.0C ";

    char buf[8];

    // 日期行
    FourDigit(now.year, buf);
    memcpy(&line1[0], buf, 4);
    TwoDigit(now.month, buf);
    memcpy(&line1[5], buf, 2);
    TwoDigit(now.day, buf);
    memcpy(&line1[8], buf, 2);

    // 时间行
    TwoDigit(now.hour, buf);
    memcpy(&line2[0], buf, 2);
    TwoDigit(now.min, buf);
    memcpy(&line2[3], buf, 2);
    TwoDigit(now.sec, buf);
    memcpy(&line2[6], buf, 2);

    // 温度
    TempToStr(temp_x10, buf);
    memcpy(&line2[9], buf, 6);

    // 设置模式闪烁当前字段(简单示例:500ms隐藏字段)
    if(set_mode && blink)
    {
        switch(set_field)
        {
            case 0: memcpy(&line1[0], "    ", 4); break; // 年
            case 1: memcpy(&line1[5], "  ", 2); break;   // 月
            case 2: memcpy(&line1[8], "  ", 2); break;   // 日
            case 3: memcpy(&line2[0], "  ", 2); break;   // 时
            case 4: memcpy(&line2[3], "  ", 2); break;   // 分
            case 5: memcpy(&line2[6], "  ", 2); break;   // 秒
        }
    }

    LCD_Str(0, 0, line1);
    LCD_Str(1, 0, line2);
}

5.8、主程序框架

c 复制代码
void System_Init(void)
{
    // 默认时间(可在此从RTC读取)
    now.year  = 2026;
    now.month = 1;
    now.day   = 5;
    now.hour  = 12;
    now.min   = 0;
    now.sec   = 0;

    LCD_Init();
    Timer0_Init_1ms();
}

void main(void)
{
    System_Init();

    while(1)
    {
        if(flag_10ms)
        {
            flag_10ms = 0;
            Key_Process();
        }

        if(flag_1000ms)
        {
            flag_1000ms = 0;

            // 非设置模式下正常走时
            if(!set_mode)
                Time_Tick1s(&now);

            // 每秒读取一次温度(可优化为2秒一次)
            temp_x10 = DS18B20_ReadTemp_x10();
        }

        if(flag_500ms)
        {
            flag_500ms = 0;
            LCD_Refresh();
        }
    }
}

6、系统可靠性与优化建议

6.1、提升时间准确性与掉电保持

若系统使用软件计时,长期运行会产生误差。推荐使用RTC芯片,尤其是DS3231,它采用温补晶振,日误差极小,适合长期高精度计时应用。配合纽扣电池,可实现断电时间不丢失。

6.2、温度采样的非阻塞优化

DS18B20在12位分辨率下温度转换时间最长可达750ms,若采用阻塞延时会降低系统响应。可优化为:

  • 在任务调度中先发起Convert T命令
  • 后续周期再读取Scratchpad
  • 采用状态机实现非阻塞采集
    这样可以使按键响应更灵敏,显示刷新更流畅。

6.3、按键长按加速与体验提升

在时间设置时,逐次按键加减效率较低。可加入长按加速:

  • 长按500ms后开始连续加减
  • 连续加减速度逐渐加快(例如每100ms变化一次)
    提高时间设置效率,使系统更接近实际电子钟体验。

6.4、显示布局扩展与信息丰富

在LCD1602限制下仍可扩展显示:

  • 星期显示(Mon/Tue或数字1~7)
  • 设置模式显示"SET"标识
  • 温度异常显示"Err"提示
    若采用LCD12864,可进一步显示更丰富界面,如图标、曲线、菜单等。

7、总结

基于单片机的多功能LCD万年历时钟设计与温度显示系统,将时间计时、万年历算法、LCD显示、DS18B20温度采集与按键交互等功能有机结合,实现了"年月日时分秒完整显示、时间校准设置、实时温度显示、按键修改参数"等核心需求。硬件部分由单片机最小系统、电源模块、LCD显示模块、时钟计时模块、DS18B20温度采集模块与按键模块构成,各模块功能清晰、接口简单、易于实现与扩展。软件部分采用模块化思想与定时任务机制,实现了稳定可靠的显示刷新、按键消抖、万年历进位与合法性判断、温度读取与格式化显示、设置模式状态机等关键功能。

该系统不仅能够满足日常电子时钟与环境温度显示需求,同时也具备良好的扩展空间。后续可进一步增加闹钟、整点报时、温度报警、串口上位机显示、自动校时等功能,使其成为更完善的智能时间与环境信息显示终端。

相关推荐
菜鸟的学习日记、29 分钟前
GPIO的几种模式——以STM32为例
stm32·单片机·嵌入式硬件·gpio
辰哥单片机设计1 小时前
STM32智能睡眠检测系统
stm32·单片机·嵌入式硬件
隔窗听雨眠3 小时前
在STM32上跑通TinyML:从模型训练到推理优化的完整实战指南
stm32·单片机·嵌入式硬件
ryanuo75 小时前
Mac(M芯片)上进行嵌入式开发遇到的问题
嵌入式硬件·macos·开发板
机器视觉知识推荐、就业指导5 小时前
为什么同一个引脚不能同时做按键和串口
stm32·单片机·嵌入式硬件
崇山峻岭之间6 小时前
单片机基本定时器实验
单片机·嵌入式硬件
DS小龙哥6 小时前
基于ESP32设计的智能养蜂监测系统
stm32·单片机·嵌入式硬件·物联网·华为云
夜月yeyue6 小时前
STM32 DMA 双缓冲采样
linux·stm32·单片机·嵌入式硬件·系统架构
羊羊小栈7 小时前
Uplift营销供应链协同决策系统(基于Uplift因果推断与运筹优化算法)
前端·人工智能·算法·毕业设计·大作业
西城微科方案开发7 小时前
SIC8P370D2L-PLP16 8位OTP单片机 低功耗多功能MCU详解
单片机·嵌入式硬件