目录

1、功能要求

2、实现

2.1、主要器件

2.2、主要功能设计

2.2.1、时钟模块

2.2.2、温度传感器模块

2.2.3、定时器

2.2.4、LCD显示屏

2.2.5、主函数

3、展示

4、关于普中A2板载的DS1302与DS18B20冲突


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 被强行拉低。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐