STC89C52RC | 电子万年历 | 普中A2板载(已解决DS1302与DS18B20冲突)
开机时,LCD1602默认显示初始化日历时间和温度值,当按下K1键第1次,进入日期和时间设定模式,此时光标会在要调整的时间位置闪烁,可通过K3键进行数据加1,如需切换所要调整的时间位置,可按K2键切换。当按下K1键第2次,进入闹钟设置模式(时-分),此时光标同样在所要调整的时间位置闪烁,可通过K3键进行数据加1,如需切换所要调整的时间位置,可按K2键切换,要让闹钟开启,除了设定对应的时间外,还需要
目录
1、功能要求
开机时,LCD1602默认显示初始化日历时间和温度值,当按下K1键第1次,进入日期和时间设定模式,此时光标会在要调整的时间位置闪烁,可通过K3键进行数据加1,如需切换所要调整的时间位置,可按K2键切换。当按下K1键第2次,进入闹钟设置模式(时-分),此时光标同样在所要调整的时间位置闪烁,可通过K3键进行数据加1,如需切换所要调整的时间位置,可按K2键切换,要让闹钟开启,除了设定对应的时间外,还需要设置闹钟开关为“ON”。当按下K1键第3次,又会回到刚才日期和时间设定模式,如此循环。当设定完成后,可按下K4键保存。时间会按照预定设定进行,如果开启闹钟,当前时间(时-分)如果与闹铃时间(时-分)相等,则蜂鸣器发出声音,一分钟以后则停止。
2、实现
2.1、主要器件
STC89C52RC 8位单片机,运行主程序
LCD1602 显示时间、温度等信息
DS1302 实时时钟芯片,提供时间数据
DS18B20 温度传感器,测量当前环境温度
独立按键 设定时间、闹钟等
蜂鸣器 作为闹钟提醒
定时器 0 1s 刷新一次 LCD1602

2.2、主要功能设计
2.2.1、时钟模块
DS1302.c:
#include <regx52.h>
typedef unsigned char u8;
typedef unsigned int u16;
// 寄存器写入地址/指令定义
#define DS1302_SECOND 0x80
#define DS1302_MINUTE 0x82
#define DS1302_HOUR 0x84
#define DS1302_DATE 0x86
#define DS1302_MONTH 0x88
#define DS1302_DAY 0x8A
#define DS1302_YEAR 0x8C
#define DS1302_WP 0x8E // 写保护
// DS1302 引脚定义
sbit DS1302_SCLK = P3^6; // 时钟
sbit DS1302_IO = P3^4; // 数据线
sbit DS1302_CE = P3^5; // 复位引脚
// 时间数组,索引0~6分别为年、月、日、星期、时、分、秒
extern u8 DS1302_Time[7] = {25, 2, 28, 5, 11, 0, 0}; // 默认初始时间:19年1月1日 星期一 00:00:00
// 初始化 DS1302
void DS1302_Init(void) {
DS1302_CE = 0; // 片选拉低
DS1302_SCLK = 0; // 时钟初始为低
// 关闭写保护
// DS1302_WriteRegister(DS1302_WP, 0x00);
}
/*
// SPI 写入一个字节(时序)
void DS1302_WriteByte(u8 dat) {
u8 i;
// DS1302_SCLK = 0; // 一开始默认0,准备=1的上升沿,但在init中定义
for (i = 0; i < 8; i++) {
DS1302_IO = dat & 0x01; // 发送最低位
dat >>= 1; // 数据右移
DS1302_SCLK = 1; // 时钟上升沿,传一位数据
DS1302_SCLK = 0; // 时钟下降沿,准备下一次传输
}
}
*/
/*
// SPI 读取一个字节(时序)
u8 DS1302_ReadByte(void) {
u8 i, dat = 0; // dat接收数据用
//
for (i = 0; i < 8; i++) {
dat >>= 1; // 数据右移
if (DS1302_IO) {
dat |= 0x80; // 如果数据线为高,写入最高位
}
DS1302_SCLK = 1; // 时钟上升沿
DS1302_SCLK = 0; // 时钟下降沿
}
return dat;
}
*/
// 写入一个字节到 DS1302
void DS1302_WriteByte(u8 addr, u8 dat) {
u8 i;
DS1302_CE = 1; // CE拉高
// 发送命令字节
for (i = 0; i < 8; i++) {
DS1302_IO = addr & (0x01 << i);
DS1302_SCLK = 1;
DS1302_SCLK = 0;
}
// 发送数据字节
for (i = 0; i < 8; i++) {
DS1302_IO = dat & (0x01 << i);
DS1302_SCLK = 1;
DS1302_SCLK = 0;
}
DS1302_CE = 0; // CE拉低,结束
DS1302_CE = 1; // 可删,仅此处使用,为防止CS(3.5)置0使DOUT(3.7)置0,最后再置回1(DS1302与DS18B20冲突)
}
// 从 DS1302 读取一个字节
u8 DS1302_ReadByte(u8 addr) {
u8 i, dat = 0x00;
addr |= 0x01; // 读指令(最低位设置为1)(最低位0读1写),改成0
DS1302_CE = 1;
// 发送命令字节
for (i = 0; i < 8; i++) {
DS1302_IO = addr & (0x01 << i);
DS1302_SCLK = 0;
DS1302_SCLK = 1;
}
// 读取数据字节
for (i = 0; i < 8; i++) {
DS1302_SCLK = 1;
DS1302_SCLK = 0;
if (DS1302_IO) {
dat |= (0x01 << i);
}
}
DS1302_CE = 0;
DS1302_CE = 1; // 可删,仅此处使用,为防止CS(3.5)置0使DOUT(3.7)置0,最后再置回1(DS1302与DS18B20冲突)
DS1302_IO = 0; // 最后必须拉0
return dat;
}
// 接下来两个操作对DS1302_Time数组内容改变
// 设置(写)时间
void DS1302_SetTime(void) {
DS1302_WriteByte(DS1302_WP, 0x00); // 关闭写保护
// 接下来写入各时间,但数组里的十进制数转BCD码
/*
DS1302_Time[0] / 10 * 16:
将十位转换为 BCD 码中的高 4 位。
在 BCD 码中,每个数字占 4 位(1 个半字节),
十位需要左移一位到高 4 位,因此乘以 16。
*/
DS1302_WriteByte(DS1302_YEAR, (DS1302_Time[0] / 10 * 16) + (DS1302_Time[0] % 10));
DS1302_WriteByte(DS1302_MONTH, (DS1302_Time[1] / 10 * 16) + (DS1302_Time[1] % 10));
DS1302_WriteByte(DS1302_DATE, (DS1302_Time[2] / 10 * 16) + (DS1302_Time[2] % 10));
DS1302_WriteByte(DS1302_DAY, (DS1302_Time[3] / 10 * 16) + (DS1302_Time[3] % 10));
DS1302_WriteByte(DS1302_HOUR, (DS1302_Time[4] / 10 * 16) + (DS1302_Time[4] % 10));
DS1302_WriteByte(DS1302_MINUTE, (DS1302_Time[5] / 10 * 16) + (DS1302_Time[5] % 10));
DS1302_WriteByte(DS1302_SECOND, (DS1302_Time[6] / 10 * 16) + (DS1302_Time[6] % 10));
// 完事写保护
DS1302_WriteByte(DS1302_WP, 0x80); // 打开写保护
}
// 获取(读)时间(基本同理)
void DS1302_ReadTime(void) {
u8 tempTime; // 省事
// 读不用保护
tempTime = DS1302_ReadByte(DS1302_YEAR);
DS1302_Time[0] = (tempTime / 16 * 10) + (tempTime % 16);
tempTime = DS1302_ReadByte(DS1302_MONTH);
DS1302_Time[1] = (tempTime / 16 * 10) + (tempTime % 16);
tempTime = DS1302_ReadByte(DS1302_DATE);
DS1302_Time[2] = (tempTime / 16 * 10) + (tempTime % 16);
tempTime = DS1302_ReadByte(DS1302_DAY);
DS1302_Time[3] = (tempTime / 16 * 10) + (tempTime % 16);
tempTime = DS1302_ReadByte(DS1302_HOUR);
DS1302_Time[4] = (tempTime / 16 * 10) + (tempTime % 16);
tempTime = DS1302_ReadByte(DS1302_MINUTE);
DS1302_Time[5] = (tempTime / 16 * 10) + (tempTime % 16);
tempTime = DS1302_ReadByte(DS1302_SECOND);
DS1302_Time[6] = (tempTime / 16 * 10) + (tempTime % 16);
}
2.2.2、温度传感器模块
oneWire.c:
/*
1-wire 协议(1-Wire Protocol)是一种由 Dallas Semiconductor
(现在的 Maxim Integrated)推出的通信协议,
它使用单根数据线(或叫做“1-wire”)进行数据传输和设备识别。
这个协议的特点是通信只需要两根线:一根电源线(Vcc)和一根数据线(DQ),
使得它特别适合一些低功耗、低成本的应用。
*/
/*
基本的 1-wire 通信操作:
复位和初始化: 在每次与 1-wire 设备通信前,主设备(通常是微控制器)必须发送一个复位脉冲,
设备通过返回一个响应信号来确认其存在。
数据传输: 数据是通过电压的变化来传输的,低电平表示 0,高电平表示 1。
每个数据位的传输都需要特定的时间延时,通常是几十微秒。
设备选择和通信:
每个 1-wire 设备都有一个 64 位的唯一地址,
设备通信时需要先通过地址选择来指定与哪个设备进行通信。
通过地址选择,主设备可以与多个设备进行单独通信,而无需通过多个数据线。
命令和数据读取: 一旦设备被选择,主设备可以向其发送命令(如读取温度、配置设置等),
然后设备响应数据。比如,在 DS18B20 温度传感器的例子中,主设备会发送“读取温度”的命令,
然后等待从设备返回的温度数据。
读取和写入数据:
1-wire 设备支持“读取”或“写入”操作。
每次传输的数据位都是一个时钟脉冲的变化过程,
主设备控制时钟信号,设备根据时钟信号的变化来读写数据。
数据传输采用逐位传输的方式,即每次发送或接收一个 bit。
*/
/*
1-Wire 协议的数据传输流程:
复位脉冲:
主设备发送一个低电平脉冲(通常是480微秒),然后再拉高数据线,表示复位信号。
所有的设备会响应主设备,并返回一个低电平脉冲(60-240微秒),表示它们处于活动状态。
设备选择:
主设备通过广播或单独地址来选择目标设备,选择时会使用目标设备的 64 位唯一 ID。
读写操作:
写数据: 主设备通过控制时钟线(SCL)和数据线(SDA)上的电平变化来发送数据。
主设备控制时钟信号的高低,设备读取数据线的电平来获取数据。
读数据: 主设备通过向设备发送读命令,设备在时钟的控制下将数据位传送回主设备。
数据确认:
在数据传输过程中,设备通过确认信号(通常是 ACK/NACK)来表示是否正确接收到数据。
*/
/*
DS18B20 传感器的 1-Wire 通信:
以 DS18B20 温度传感器为例,它采用了 1-wire 协议来读取温度。
DS18B20 内部有一个独特的 64 位地址,通过这个地址,主设备可以选择它并读取温度。
主要的通信步骤包括:
向 DS18B20 发送一个复位信号。
通过设备的唯一地址选择 DS18B20 进行通信。
发送启动温度转换命令,启动温度转换过程。
等待传感器完成温度转换(约 750ms)。
读取温度数据,温度数据包含高低字节,主设备可以将其解析为实际的温度值。
关闭通信。
*/
#include <regx52.h>
#include "delay.h"
typedef unsigned char u8;
typedef unsigned int u16;
sbit ONEWIRE_DQ = P3^7;
// #define ONEWIRE_DQ P3_7 // 1-wire数据线,也是(连接到)温度传感器的DQ数据线
// 不能写P3^7,这个只能用在sbit重定义某一位时
// 或者写 sbit ONEWIRE_DQ = P3^7;
// P3_7:这是 Keil C51 默认的位寄存器定义方式,用于直接操作端口位。
// 仅在 regx52.h 中有定义
// P3^7:是一种稍老的或另类的定义风格,在现代 Keil 编译器中可能未启用,或者需要额外设置
// 在 reg52.h 和 regx52.h 中均又定义
// 发送1-Wire总线的起始信号(初始化时序)
u8 oneWireInit() {
unsigned char i;
u8 AckBit;
ONEWIRE_DQ = 1;
// delay10Us(1); // 等待总线稳定
ONEWIRE_DQ = 0; // 拉低电平
//delay10Us(50); // 拉低持续480us
i = 247;while (--i); //Delay 500us
ONEWIRE_DQ = 1; // 释放总线
//delay10Us(7); // 释放15-60us
i = 32;while (--i); //Delay 70us
AckBit = ONEWIRE_DQ;
//delay10Us(50);
i = 247;while (--i); //Delay 500us
return AckBit;
}
// 向1-Wire总线写入一个位(发送时序)
void oneWireWriteBit(u8 bitvalue) {
unsigned char i;
ONEWIRE_DQ = 0;
//delay10Us(1); //
i = 4;while (--i); //Delay 10us
ONEWIRE_DQ = bitvalue; // 根据bitValue决定总线的电平(0,写0,1,写1)
//delay10Us(5); // 保证数据稳定
i = 24;while (--i); //Delay 50us
ONEWIRE_DQ = 1; // 释放总线(电阻上拉)
// delayUs(2); // 释放2us
}
// 读取1-Wire总线上的一个位(读取时序)
u8 oneWireReadBit() {
unsigned char i;
u8 bitvalue;
ONEWIRE_DQ = 0;
//delayUs(5);
i = 2;while (--i); //Delay 5us
ONEWIRE_DQ = 1; // 拉高开始读取
//delayUs(5);
i = 2;while (--i); //Delay 5us
bitvalue = ONEWIRE_DQ;
//delay10Us(5);
i = 24;while (--i); //Delay 50us
return bitvalue;
}
// 发送1-Wire总线的数据字节
void oneWireWriteByte(u8 byte) {
u8 i;
for (i = 0; i < 8; i++) {
oneWireWriteBit(byte & (0x01 << i)); // 发送数据的最低位
// byte >>= 1;
}
}
// 读取1-Wire总线的数据字节
u8 oneWireReadByte() {
u8 i;
u8 byte = 0x00;
for (i = 0; i < 8; i++) {
// byte >>= 1;
if (oneWireReadBit()) {
byte |= (0x01 << i); // 读到最高位,bitvalue=1,则1,=0,则原0不变
}
}
return byte;
}
DS18B20.c:
#include <regx52.h>
#include "delay.h"
#include "oneWire.h"
typedef unsigned char u8;
typedef unsigned int u16;
#define DS18B20_ADDR 0x28 // DS18B20的设备地址(8位地址)
#define DS18B20_PRECISION 0.0625 // 默认12位精度0.0625
#define TEMPERATURE_CONVERSION_TIME 750 // 12位温度转换时间750ms
// 写0xcc 是跳过rom命令
// 写0x44 是转换命令
// 写0xbe 是读存储器命令
// 启动温度转换
void DS18B20_StartConversion() {
oneWireInit(); // 初始化1-wire总线
oneWireWriteByte(0xcc); // 跳过ROM命令,直接控制设备
oneWireWriteByte(0x44); // 正式启动温度转换
}
// 读取DS18B20温度值
float DS18B20_ReadTemperature() {
u8 MS_Byte, LS_Byte; // 温度寄存是两个字节16位,前5位符号位,后4为小数,其余整数,MS高字节,LS低字节
int rawTemperature; // 未处理的温度值(十六进制)
float temperature; // 最后返回用的温度值
oneWireInit();
oneWireWriteByte(0xcc);
oneWireWriteByte(0xbe); // 读取温度寄存器
LS_Byte = oneWireReadByte(); // 读取的第一个字节是低字节
MS_Byte = oneWireReadByte(); // 读取的第一个字节是高字节
rawTemperature = (MS_Byte << 8) | LS_Byte; // 高低字节合并,未处理温度值
// temperature = rawTemperature / 16.0;
if (rawTemperature & 0x8000) { // 高位是1,则负温度
// 先算补码,取反+1,补码再转浮点数温度值,再乘精度0.0625
int temp = (~rawTemperature + 1);
temperature = (float)temp * DS18B20_PRECISION;
}
else { // 否则是0,则是温度
// 正值补码等于自己
temperature = (float)rawTemperature * DS18B20_PRECISION;
}
return temperature; // 返回温度值,单位:°C
}
// 初始化DS18B20,并等待温度转换完成
void DS18B20_Init() {
DS18B20_StartConversion(); // 初始化即要启动温度转换
delay1ms(TEMPERATURE_CONVERSION_TIME); // 等待转换750ms
}
2.2.3、定时器
timer0.c:
#include <REGX52.H>
typedef unsigned char u8;
typedef unsigned int u16;
// 定时器0
void Timer0_Init() {
TMOD &= 0xF0; // 设置定时器模式
TMOD |= 0x01; // 设置定时器模式
TL0 = 0x18; // 设置定时初值(低位)
TH0 = 0xFC; // 设置定时初值(高位)
TF0 = 0; // 清除TF0标志
TR0 = 1; // 定时器0开始计时
ET0 = 1; // 开启定时器0中断
EA = 1; // 开启总中断
PT0 = 0; // 中断优先级设置
}
2.2.4、LCD显示屏
取鉴自B站UP主江协科技。
LCD1602.c:
#include <REGX52.H>
#include "delay.h"
typedef unsigned char u8;
typedef unsigned int u16;
// 引脚配置:
sbit LCD_RS = P2^6;
/*
(Register Select) 引脚,用于区分 LCD 接收的是指令还是数据:
RS = 0:写入命令(比如清屏、设置光标等)。
RS = 1:写入数据(即要显示的字符)。
*/
sbit LCD_RW = P2^5;
/*
(Read/Write) 引脚,决定 LCD 是读还是写:
RW = 0:写入数据/命令。
RW = 1:读取 LCD 状态(比如忙标志位)。
*/
sbit LCD_EN = P2^7;
/*
(Enable) 使能引脚,用于触发 LCD 执行指令或数据传输:
先置 EN = 1,再 EN = 0,LCD 在下降沿时执行命令或数据操作。
*/
#define LCD_DataPort P0
/*
LCD1602 的数据端口,通常连接单片机的 8 位数据总线(这里是 P0 口)。
LCD1602 采用 8 位或 4 位并行通信,8 位模式下,所有数据由 P0 端口传输。
*/
// 函数定义:
// LCD1602延时函数,12MHz调用可延时1ms
void LCD_Delay() {
u8 i, j;
i = 2;
j = 239;
do {
while (--j);
} while (--i);
}
// LCD1602写命令
void LCD_WriteCommand(u8 Command) {
LCD_RS = 0; // 选择命令寄存器 (RS = 0)
LCD_RW = 0; // 选择写模式 (RW = 0)
LCD_DataPort = Command; // 发送命令数据到 LCD 数据端口 (P0)
LCD_EN = 1; // 使能信号上升沿,LCD 开始接收命令
LCD_Delay(); // 短暂延时,确保命令被稳定接收
LCD_EN = 0; // 使能信号下降沿,执行命令
LCD_Delay(); // 延时等待命令执行完成
}
// LCD1602写数据
void LCD_WriteData(u8 Data) {
LCD_RS = 1; // 选择数据寄存器 (RS = 1)(与命令模式不同)
LCD_RW = 0; // 选择写模式 (RW = 0)(向 LCD 写入数据)
LCD_DataPort = Data; // 将要显示的数据写入 LCD 数据端口 (P0)
LCD_EN = 1; // 产生上升沿,LCD 开始接收数据
LCD_Delay(); // 短暂延时,确保数据被稳定接收
LCD_EN = 0; // 产生下降沿,LCD 执行数据存储
LCD_Delay(); // 再次延时,确保 LCD 处理完成
}
/**
* @brief LCD1602设置光标位置
* @param Line 行位置,范围:1~2
LCD1602 共有两行,每行 16 列:
第一行的起始地址:0x80 | 0x00 = 0x80。
第二行的起始地址:0x80 | 0x40 = 0xC0。
* @param Column 列位置,范围:1~16
* @retval 无
*/
void LCD_SetCursor(u8 Line, u8 Column) {
if(Line == 1) {
LCD_WriteCommand(0x80 | (Column - 1)); // Column-1 是为了让列号从 1 开始符合人的直觉(而不是从 0 开始)。例如 LCD_SetCursor(1, 5); → LCD_WriteCommand(0x80 | (5-1)) = LCD_WriteCommand(0x84);,即光标移动到 第一行第 5 列。
}
else if(Line == 2) {
LCD_WriteCommand(0x80 | (Column - 1 + 0x40));
}
}
/**
* @brief LCD1602初始化函数
* @param 无
* @retval 无
*/
void LCD_Init() {
LCD_WriteCommand(0x38); // 0x38 8位数据接口,两行显示,5×7点阵字符
LCD_WriteCommand(0x0C); // 0x0C 开启显示,光标关闭,光标不闪烁
LCD_WriteCommand(0x06); // 0x06 数据读写后,光标自动右移,画面不动
LCD_WriteCommand(0x01); // 0x01 光标复位,清屏
// 0x18 移动屏幕
}
/**
* @brief 在LCD1602指定位置上显示一个字符
* @param Line 行位置,范围:1~2
* @param Column 列位置,范围:1~16
* @param Char 要显示的字符
* @retval 无
*/
void LCD_ShowChar(u8 Line, u8 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(u8 Line, u8 Column, char* String) {
u8 i;
LCD_SetCursor(Line, Column);
for(i = 0; String[i] != '\0'; i++) { // 遍历字符串,直到遇到 '\0'
LCD_WriteData(String[i]); // 逐个字符写入 LCD
}
}
/**
* @brief 返回值=X的Y次方
*/
int LCD_Pow(int X, int Y) {
u8 i;
int Result = 1;
for(i = 0; i < Y; i++) {
Result *= X;
}
return Result;
}
/**
* @brief 在LCD1602指定位置开始显示所给数字(0~65535)
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~65535
* @param Length 要显示数字的长度,范围:1~5
* @retval 无
*/
void LCD_ShowNum(u8 Line, u8 Column, u16 Number, u8 Length) {
u8 i;
LCD_SetCursor(Line, Column);
for(i = Length; i > 0; i--) {
LCD_WriteData(Number / LCD_Pow(10, i-1) % 10 + '0');
/*
通过 LCD_Pow(10, i-1) 计算 当前位的权重(即 10 的幂次)。
Number / LCD_Pow(10, i-1) 提取 高位数(整数除法)。
% 10 取出 当前位。
+ '0' 转换为 ASCII 码(字符显示)。
*/
}
}
/*
LCD_ShowNum(1, 1, 45, 5);
i LCD_Pow(10, i-1) Number / LCD_Pow(10, i-1) % 10 显示字符
5 10000 45 / 10000 = 0 0 '0'
4 1000 45 / 1000 = 0 0 '0'
3 100 45 / 100 = 0 0 '0'
2 10 45 / 10 = 4 4 '4'
1 1 45 / 1 = 45 5 '5'
*/
/**
* @brief 在LCD1602指定位置开始以有符号十进制显示所给数字(-32768~32767)
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:-32768~32767
* @param Length 要显示数字的长度,范围:1~5
* @retval 无
*/
void LCD_ShowSignedNum(u8 Line, u8 Column, int Number, u8 Length) {
u8 i;
u16 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(u8 Line, u8 Column, u16 Number, u8 Length) {
u8 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'); // 0~9 显示为 '0'~'9'
}
else {
LCD_WriteData(SingleNumber - 10 + 'A'); // 10~15 显示为 'A'~'F'
}
}
}
/*
LCD_ShowHexNum(1, 1, 0x07D, 3);
i LCD_Pow(16, i-1) Number / LCD_Pow(16, i-1) % 16 显示字符
3 256 0x07D / 256 = 0 0 '0'
2 16 0x07D / 16 = 7 7 '7'
1 1 0x07D / 1 = 125 D 'D'
*/
/**
* @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(u8 Line, u8 Column, u16 Number, u8 Length) {
u8 i;
LCD_SetCursor(Line, Column);
for(i = Length; i > 0; i--) {
LCD_WriteData(Number / LCD_Pow(2, i-1) % 2 + '0'); // 逐位取值并转换为 '0' 或 '1'
}
}
/*
LCD_ShowBinNum(1, 1, 0b01110101, 8);
i LCD_Pow(2, i-1) Number / LCD_Pow(2, i-1) % 2 显示字符
8 128 0b01110101 / 128 = 0 0 '0'
7 64 0b01110101 / 64 = 1 1 '1'
6 32 0b01110101 / 32 = 3 1 '1'
5 16 0b01110101 / 16 = 7 1 '1'
4 8 0b01110101 / 8 = 14 0 '0'
3 4 0b01110101 / 4 = 29 1 '1'
2 2 0b01110101 / 2 = 58 0 '0'
1 1 0b01110101 / 1 = 117 1 '1'
*/
// LCD1602显示计时器
void LCD_ShowTime(u8* timeArray) {
LCD_SetCursor(2, 1);
// 显示分钟
LCD_WriteData(timeArray[0] / 10 % 10 + '0'); // 分钟十位
LCD_WriteData(timeArray[0] % 10 + '0'); // 分钟个位
LCD_WriteData('-'); // 分隔符 '-'
// 显示秒
LCD_WriteData(timeArray[1] / 10 % 10 + '0'); // 秒十位
LCD_WriteData(timeArray[1] % 10 + '0'); // 秒个位
LCD_WriteData('-'); // 分隔符 '-'
// 显示10ms
LCD_WriteData(timeArray[2] / 10 % 10 + '0'); // 10毫秒的十位
LCD_WriteData(timeArray[2] % 10 + '0'); // 10毫秒的个位
}
// LCD滚动显示字符串
void LCD_ScrollString(u8 Line, char* str, u8 speed) {
u8 len = 0;
u8 i = 0, j;
u8 index;
while(str[len] != '\0') len++; // 计算字符串长度
while(1) {
for(j = 0; j < 16; j++) { // LCD1602 屏幕宽度 = 16
index = (i + j) % (len + 16); // 让字符串与空格形成循环 // 0 ~ len+16-1 即滚动索引 i 变化范围
if(index < len) LCD_ShowChar(Line, j + 1, str[index]);
else LCD_ShowChar(Line, j + 1, ' '); // 清空超出的部分
}
i = (i + 1) % (len + 16); // 让字符串和空格部分形成循环
delay1ms(speed);
}
}
2.2.5、主函数
main.c:
#include <REGX52.H>
#include "beep.h"
#include "key.h"
#include "LCD1602.h"
#include "DS18B20.h"
#include "oneWire.h"
#include "DS1302.h"
#include "delay.h"
#include "timer0.h"
typedef unsigned char u8;
typedef unsigned int u16;
// 蜂鸣器
sbit BEEP = P2^5;
bit beepFlag = 0;
// 时间全局变量(外部定义)
extern u8 DS1302_Time[7]; // {年, 月, 日, 星期, 时, 分, 秒}
// 按键变量
u8 keyNum = 0;
u8 mode = 0; // 0:正常模式,1:时间设置,2:闹钟设置
u8 pos = 0; // 光标调整位置
u8 alarmTime[2] = {0, 0}; // 设定闹钟小时和分钟
bit alarmOn = 0; // 闹钟开关
// DS1302读取的时间数组
u8 timeData[7]; // {年, 月, 日, 星期, 时, 分, 秒}
// DS18B20温度变量
float temperature = 0.0;
// 新增:是否正在修改数据(修改数据时光标不覆盖修改值)
bit isEditing = 0;
// 新增:标记是否需要清理残留的 "Set"
bit needClearSet = 0;
void main() {
// 初始化各模块
Timer0_Init();
LCD_Init();
DS1302_Init();
DS18B20_Init();
// 设置并读取初始时间
DS1302_SetTime();
DS1302_ReadTime();
// LCD 初始显示
LCD_ShowString(1, 1, " . C D - - "); // 第一行:Wxx.xDYY-MM-DD
LCD_ShowString(2, 1, " T : : "); // 第二行: THH:MM:SS
while(1) {
// 读取时间
// 只有在“非设定状态”时,才读取 DS1302
if(!isEditing) DS1302_ReadTime();
delay1ms(10);
// 读取温度
DS18B20_StartConversion();
temperature = DS18B20_ReadTemperature();
// 模式处理
if(mode == 0) { // 正常模式
// 如果刚从设置模式切换回来,清理左侧的 "Set" 字样
if(needClearSet) {
LCD_ShowString(1, 1, " . C"); // 覆盖 "Set"
LCD_ShowString(2, 1, " "); // 覆盖 "Time" 或 "Alarm"
needClearSet = 0;
}
// 更新 LCD 显示
// 第一行:Wxx.xDYY-MM-DD
if(temperature < 0) {
LCD_ShowChar(1, 1, '-');
temperature = -temperature;
}
else LCD_ShowChar(1, 1, '+');
LCD_ShowNum(1, 2, temperature, 2); // 温度整数部分
LCD_ShowNum(1, 5, (u16)(temperature * 10000) % 10000, 1); // 小数部分
LCD_ShowNum(1, 9, DS1302_Time[0] % 100, 2); // 年份后两位
LCD_ShowNum(1, 12, DS1302_Time[1], 2); // 月
LCD_ShowNum(1, 15, DS1302_Time[2], 2); // 日
// 第二行: THH:MM:SS
LCD_ShowNum(2, 9, DS1302_Time[4], 2); // 时
LCD_ShowNum(2, 12, DS1302_Time[5], 2); // 分
LCD_ShowNum(2, 15, DS1302_Time[6], 2); // 秒
if(alarmOn) LCD_ShowString(2, 1, "AL:ON");
else LCD_ShowString(2, 1, "AL:OFF");
}
else { // mode == 1 或 mode == 2
needClearSet = 1; // 标记需要清理残留
if(mode == 1) {
LCD_ShowString(1, 1, "Set ");
LCD_ShowString(2, 1, "D & T ");
LCD_ShowNum(1, 9, DS1302_Time[0] % 100, 2); // 年份
LCD_ShowNum(1, 12, DS1302_Time[1], 2); // 月
LCD_ShowNum(1, 15, DS1302_Time[2], 2); // 日
LCD_ShowNum(2, 9, DS1302_Time[4], 2); // 时
LCD_ShowNum(2, 12, DS1302_Time[5], 2); // 分
LCD_ShowNum(2, 15, DS1302_Time[6], 2); // 秒
}
else if(mode == 2) {
LCD_ShowString(1, 1, "Set ");
LCD_ShowString(2, 1, "Alarm ");
LCD_ShowNum(1, 9, DS1302_Time[0] % 100, 2); // 年份
LCD_ShowNum(1, 12, DS1302_Time[1], 2); // 月
LCD_ShowNum(1, 15, DS1302_Time[2], 2); // 日
LCD_ShowNum(2, 9, alarmTime[0], 2); // 时
LCD_ShowNum(2, 12, alarmTime[1], 2); // 分
LCD_ShowNum(2, 15, DS1302_Time[6], 2); // 秒
}
delay1ms(500);
// 光标闪烁
switch(pos) {
case 0: LCD_ShowString(1, 9, " "); break; // 年
case 1: LCD_ShowString(1, 12, " "); break; // 月
case 2: LCD_ShowString(1, 15, " "); break; // 日
case 3: LCD_ShowString(2, 9, " "); break; // 时
case 4: LCD_ShowString(2, 12, " "); break; // 分
case 5: LCD_ShowString(2, 15, " "); break; // 秒
}
delay1ms(500);
}
// 检测是否触发闹钟
if(alarmOn && DS1302_Time[4] == alarmTime[0] && DS1302_Time[5] == alarmTime[1]) {
beepTime(60000);
alarmOn = 0;
}
}
}
void Timer0_Routine() interrupt 1 {
// static u16 timerCount = 0;
TL1 = 0x18;
TH1 = 0xFC;
// P2_1 = 0; // 检测中断是否进入
// timerCount++;
// if(timerCount >= 4) {
// timerCount = 0;
// 读取键值
keyNum = keyScan2(0);
if(keyNum) {
switch(keyNum) {
case 1: { // K1: 切换模式
mode = (mode + 1) % 3; // 切换模式(0: 正常, 1: 时间设置, 2: 闹钟设置)
if(mode == 1) { // 时间设定模式:从“年”开始
pos = 0;
isEditing = 1; // 进入设定模式
}
else if(mode ==2) { // 闹钟设定模式:从“小时”开始
alarmTime[0] = DS1302_Time[4]; // 设定初始闹钟小时为当前小时
alarmTime[1] = DS1302_Time[5]; // 设定初始闹钟分钟为当前分钟
pos = 3;
isEditing = 1; // 进入设定模式
}
else isEditing = 0; // 退回mode0,普通模式
break;
}
case 2: { // K2: 切换调整位置
if(mode == 1) {
pos = (pos + 1) % 6; // 时间设定模式:在 0~5 之间循环
}
else if(mode == 2) {
pos = (pos == 3) ? 4 : 3; // 闹钟设定模式:在 3 和 4 之间切换
}
break;
}
case 3: { // K3: 数据 +1
if (mode == 1) { // 时间设定模式
switch(pos) {
case 0: DS1302_Time[0] = (DS1302_Time[0] + 1) % 100; break; // 年
case 1: DS1302_Time[1] = (DS1302_Time[1] % 12) + 1; break; // 月
case 2: DS1302_Time[2] = (DS1302_Time[2] % 31) + 1; break; // 日
case 3: DS1302_Time[4] = (DS1302_Time[4] + 1) % 24; break; // 时
case 4: DS1302_Time[5] = (DS1302_Time[5] + 1) % 60; break; // 分
case 5: DS1302_Time[6] = (DS1302_Time[6] + 1) % 60; break; // 秒
}
}
else if (mode == 2) { // 闹钟设定模式
switch(pos) {
case 3: alarmTime[0] = (alarmTime[0] + 1) % 24; break; // 闹钟小时
case 4: alarmTime[1] = (alarmTime[1] + 1) % 60; break; // 闹钟分钟
}
}
isEditing = 1; // 标记为正在手动修改
break;
}
case 4: { // K4: 保存设定
if (mode == 1) {
DS1302_SetTime(); // 保存时间
DS1302_WriteByte(0x8E, 0x00); // 清除写保护
DS1302_WriteByte(0x80, DS1302_ReadByte(0x80) & 0x7F); // 开启时钟
}
else if (mode == 2) {
alarmOn = !alarmOn;
}
isEditing = 0; // 保存后,退出设定模式
mode = 0;
beepTime(100); // 短鸣 100ms,增强交互反馈
break;
}
}
}
// }
}
3、展示
mode0为常态(观看)模式,包括温度,日期,时间,闹钟开关,程序烧入后如下:

按下K1进入mode1,时间设置模式,此时可通过K2调整设置光标,K3调整光标为数据+1,K4保存并切回mode0。

mode1下再次按下K1进入mode2,闹钟设置模式,此时可通过K2调整设置光标,K3调整光标为数据+1,K4决定闹钟开关,并切回mode0。

闹钟设置完毕后显示AL:ON,当屏幕时间与闹钟时间相符时蜂鸣器发出一分钟的响声。

4、关于普中A2板载的DS1302与DS18B20冲突
由于 ET2046 的 CS(片选) 信号和 DOUT(数据输出) 都连接到了 P3.5 和 P3.7,而 DS1302 和 DS18B20 也共用了这些引脚,因此在片选低电平(CS=0)时,ET2046 会拉低 DOUT,导致 P3.7 变成低电平,从而影响 DS1302 和 DS18B20 的正常通信。
解决方案:在 DS1302 操作后,不要直接把 CE 置 0,或者在 DS18B20 操作前重新置 1。本质是在 DS18B20 进行通信前,确保 DS1302 的 CE(P3.5)为高电平,以让 ET2046 进入高阻态,从而防止 P3.7 被强行拉低。

更多推荐



所有评论(0)