基于单片机的多功能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温度采集模块与按键模块构成,各模块功能清晰、接口简单、易于实现与扩展。软件部分采用模块化思想与定时任务机制,实现了稳定可靠的显示刷新、按键消抖、万年历进位与合法性判断、温度读取与格式化显示、设置模式状态机等关键功能。

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

相关推荐
编程武士4 小时前
UUID 升级:四种现代化方案深度对比
系统架构·课程设计
tianyue1005 小时前
STM32G431 ADC 多个channel 采集
stm32·单片机·嵌入式硬件
longson.6 小时前
怎样避免空间碎片而且高效的分配空间
嵌入式硬件·缓存
清风6666666 小时前
基于单片机的水泵效率温差法测量与报警系统设计
单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
luoluoal6 小时前
基于python的语音和背景音乐分离算法及系统(源码+文档)
python·mysql·django·毕业设计·源码
焦糖码奇朵、7 小时前
课设:基于Arduino的无线LED开关控制系统
嵌入式硬件·物联网·arduino·信息与通信·信号处理
z20348315208 小时前
定时器练习报告
单片机·嵌入式硬件
zk008 小时前
内容分类目录
单片机·嵌入式硬件
安生生申8 小时前
STM32 ESP8266连接ONENET
c语言·stm32·单片机·嵌入式硬件·esp8266
广药门徒8 小时前
电子器件烧毁的底层逻辑与避坑指南
单片机·嵌入式硬件