基于STC89C52RC的温度报警器(二)
2026/9/11 22:08:53 网站建设 项目流程

整个代码部分的逻辑主要是:

1.如按键,lcd1602等不同模块基础功能(如按键读取,lcd显示)分别写在各自底层中。

2.整体逻辑大概是在主函数内先读取一次温度,然后对温度上下限e2proom功能进行读取恢复掉电前的数据,然后在循环内不断读取键码并根据键码进行相应判断。

(或者还有一种思路,就是不用矩阵键盘,用独立按键,在定时器中断内对独立按键不进行一次按键驱动调用,然后在主程序循环内根据独立按键读取来进行相应逻辑运算,这个思路放在最后来说)

接下来将按照不同模块进行底层编写:

一、LCD1602

在进行项目编写的时候,为防止漏洞较多,一般我们编写一部分代码就会执行检查而非所有写完再运行,故这里先介绍LCD1602液晶屏,这样便于在后续代码完成后检查代码。(其实用数码管也可以检查,不过LCD能显示的多一点看着舒服就用LCD了)

LCD1602显示容量:2行16列,每个字符5*7点阵

1.1 LCD相关原理

各引脚功能:

RS数据指令,R/W读写。E使能,DB0-7八位数据端口

运行逻辑:

LCD的运行逻辑主要是在DDRAM(数据显示区)中写入要显示的字符,然后回通过写入的数据在GGRAM(字模库)里找到字符对应点阵显示的字模,然后会在DDRAM地址对应的LCD上的位置上显示字符。

在应用函数的时候,常见字符十六进制asc码和字模是一样的,比如用LCDShowString(1,1,'A');会自动转换,不用担心流程繁琐。

需要注意的是图中显示的是前七位纯地址,第八位表示光标所在的位置,也就是说选中第一行第一列实际是1 000 0000.

时序图:

此处以写为例,RS是数据就高电平,是指令就给低电平;R/W读高写低,E先高后低,要写进的DB0-7的就是字符对应字模。(以下举例)

1.2 LCD相关底层代码

#include <REGX52.H>

//引脚配置:
sbit LCD_RS=P2^6;
sbit LCD_RW=P2^5;
sbit LCD_EN=P2^7;
#define LCD_DataPort P0

接下来我们逐步根据时序图进行代码编写:

考虑到E脉冲时间太短无法读取DB0-7数据,所以我们加一个一毫秒的延时函数

void LCD_Delay()
{
unsigned char i, j;

i = 2;
j = 239;
do
{
while (--j);
} while (--i);
}

然后根据时序图写一个写命令和写数据(RS高低电平的区别)

//LCD1602写命令
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();
}

//LCD1602写数据
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();
}

根据手册提示和上文命令集进行初始化函数编写

//LCD1602初始化函数
void LCD_Init()
{
LCD_WriteCommand(0x38);//八位数据接口,两行显示,5*7点阵
LCD_WriteCommand(0x0c);//显示开,光标关,闪烁关
LCD_WriteCommand(0x06);//数据读写操作后,光标自动加一,画面不动
LCD_WriteCommand(0x01);//光标复位,清屏
}

然后写一个写入字符的函数,写入字符流程其实是光标选中需要lcd位置,然后写入具体字符,所以我们就分成两个函数写:(其中逻辑运算0x80或和行判断相关内容可以参考前面LCD不同位置的地址那张图)

//LCD1602设置光标位置
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));
}
}

//在LCD1602指定位置上显示一个字符
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char)
{
LCD_SetCursor(Line,Column);
LCD_WriteData(Char);
}

同理我们可以完成写入字符串(运用数组指针)

//在LCD1602指定位置开始显示所给字符串
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]);
}
}

至于显示十进制数,就采用光标定位,显示数字(十进制数除以合适的十次方数,比如234就是第一位显示234÷100)的逻辑,为此我们需要先写一个获取合适的十次方数的函数:

//返回值=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;
}

//在LCD1602指定位置开始显示所给数字
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');
}
}

有符号数,十六进制二进制数同理:

//在LCD1602指定位置开始以有符号十进制显示所给数字
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');
}
}

//在LCD1602指定位置开始以十六进制显示所给数字
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');
}
}
}

//在LCD1602指定位置开始以二进制显示所给数字
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');
}
}

二、Key按键

2.1 运行原理

键盘扫描逻辑相对简单,传统逻辑(如课内以AT89C51为例)主要是按照行扫描进行检测(即逐次对一行进行给低电平0,然后检测行内是否存在为0引脚也就是被按下的按键)。

而普中A2开发板因为内部电路原因P15引脚接在步进电机与蜂鸣器相连,时高时低的电平变化可能导致蜂鸣器响,故这里我们采用列扫描进行矩阵键盘读取。

PS:从原理图来看,按键是一个一头接高电平一头接地的短路模型,但是这个不用担心,因为89C52的P123都是弱上拉强下拉的设计。即在引脚输出高电平的时候输出能力较弱,外部结构将其拉低;而在引脚输出低电平的时候驱动能力极强。

2.2 代码思路

以此为思路进行底层编写:

首先对P1引脚复位八个引脚全部置一,然后先将第一列也就是P1_3引脚置零,并进行四次判断,这一列是否有按键被按下(加上延时函数消抖,延时函数在STC公司官方资源软件ISP中选择正确参数生成即可,这里不多赘述)

重复四次,返回读取的键码KeyNumber。

(江协科技带出来的兵是这样的,零基础时间充裕还是听江协的课好,讲的特别清楚,主包51和32听的都是江协的,pid估计也会听江协的)

另外,引脚的定义在头文件<REGX52.H>中,“P1_3”之类的引脚不是瞎写的,是头文件重定义声明过的。

#include <REGX52.H>
#include "Delay.h"

unsigned char MatrixKey()
{
unsigned char KeyNumber=0;

P1=0xFF;
P1_3=0;
if(P1_7==0){Delay(20);while(P1_7==0);Delay(20);KeyNumber=1;}
if(P1_6==0){Delay(20);while(P1_6==0);Delay(20);KeyNumber=5;}
if(P1_5==0){Delay(20);while(P1_5==0);Delay(20);KeyNumber=9;}
if(P1_4==0){Delay(20);while(P1_4==0);Delay(20);KeyNumber=13;}

P1=0xFF;
P1_2=0;
if(P1_7==0){Delay(20);while(P1_7==0);Delay(20);KeyNumber=2;}
if(P1_6==0){Delay(20);while(P1_6==0);Delay(20);KeyNumber=6;}
if(P1_5==0){Delay(20);while(P1_5==0);Delay(20);KeyNumber=10;}
if(P1_4==0){Delay(20);while(P1_4==0);Delay(20);KeyNumber=14;}

P1=0xFF;
P1_1=0;
if(P1_7==0){Delay(20);while(P1_7==0);Delay(20);KeyNumber=3;}
if(P1_6==0){Delay(20);while(P1_6==0);Delay(20);KeyNumber=7;}
if(P1_5==0){Delay(20);while(P1_5==0);Delay(20);KeyNumber=11;}
if(P1_4==0){Delay(20);while(P1_4==0);Delay(20);KeyNumber=15;}

P1=0xFF;
P1_0=0;
if(P1_7==0){Delay(20);while(P1_7==0);Delay(20);KeyNumber=4;}
if(P1_6==0){Delay(20);while(P1_6==0);Delay(20);KeyNumber=8;}
if(P1_5==0){Delay(20);while(P1_5==0);Delay(20);KeyNumber=12;}
if(P1_4==0){Delay(20);while(P1_4==0);Delay(20);KeyNumber=16;}

return KeyNumber;
}

三、ds18b20模块及1-Wire单总线

3.1 单总线协议与单总线底层代码

单总线(1-wire Bus)是一种异步半双工通用数据总线,通信线为DQ。相比i2c,优势为线数少适合远距离使用,但相对的无法同时发送和接收。

ds18b20运行的本质是根据主机通过1-wire对从机发送的命令字节来执行命令,所以首先先要学会1-wire的工作原理的底层。

初始化:主机将总线拉低至少480us,然后释放总线(区别于强上拉,也是图中释放总线后会有一段圆弧的原因),等待15~60us后,存在的从机会拉低总线60~240us以响应主机,之后从机将释放总线。

发送一位:主机将总线拉低60~120us,然后释放总线,表示发送0(图左上);主机将总线拉低1~15us,然后释放总线,表示发送1(图右上)。从机将在总线拉低30us后(典型值)读取电平,整个时间片应大于60us。

接收一位:主机将总线拉低1~15us,然后释放总线,并在拉低后15us内读取总线电平,考虑到释放后等待电平所需的时间,应尽量靠近15us末尾读取,读取为低电平则为接收0(图左下),读取为高电平则为接收1(图右下),整个时间片应大于60us。

而发送和接收一个字节简单来说就说把发送接收一位重复八次。接下来我们来根据上面的1-wire工作流程编写底层代码。

下面有一点需要注意这里提前说一下,因为这个流程中大多数等待时间是微秒级的,等待时间太短了我们封装的delay函数无法调用,这里在ISP里面生成就行

i = 247;while (--i); //Delay 500us

比如上面这个,就是生成个504,然后去掉外壳空函数就行。(比如一个nop就是一微秒,一个delay空函数是思维秒)

c文件:

#include <REGX52.H>

//引脚定义
sbit OneWire_DQ=P3^7;

//初始化:主机将总线拉低至少480us,然后释放总线
//等待15~60us后,存在的从机会拉低总线60~240us以响应主机,之后从机将释放总线。
unsigned char OneWire_Init(void)
{
unsigned char i;
unsigned char AckBit;
EA=0;
OneWire_DQ=1;
OneWire_DQ=0;
i = 247;while (--i); //Delay 500us
OneWire_DQ=1;
i = 32;while (--i); //Delay 70us 此处未标明典型数,故取率高于等待的70us
AckBit=OneWire_DQ;
i = 247;while (--i); //Delay 500us
EA=1;
return AckBit;
}

//发送一位:主机将总线拉低60~120us,然后释放总线,表示发送0;
//主机将总线拉低1~15us,然后释放总线,表示发送1。
//从机将在总线拉低30us后(典型值)读取电平,整个时间片应大于60us。
void OneWire_SendBit(unsigned char Bit)
{
unsigned char i;
OneWire_DQ=0; //发送位肯定跟在初始化后面,所以没必要先置一再拉低了
i = 4;while (--i); //Delay 10us,发送0就一直是0,发送1差不多是在这个时候置一
OneWire_DQ=Bit;
i = 24;while (--i); //Delay 50us
OneWire_DQ=1;
}

//接收一位:主机将总线拉低1~15us,然后释放总线,并在拉低后15us内读取总线电平,
//考虑到释放后等待电平所需的时间,应尽量靠近15us末尾读取,
//读取为低电平则为接收0,读取为高电平则为接收1,整个时间片应大于60us。
unsigned char OneWire_ReceiveBit(void)
{
unsigned char i;
unsigned char Bit;
OneWire_DQ=0;
i = 2;while (--i); //Delay 5us
OneWire_DQ=1;
i = 2;while (--i); //Delay 5us
Bit=OneWire_DQ;
i = 24;while (--i); //Delay 50us
return Bit;
}

//单总线发送一个字节
void OneWire_SendByte(unsigned char Byte)
{
unsigned char i;
for(i=0;i<8;i++)
{
OneWire_SendBit(Byte&(0x01<<i));
}
}

//单总线接收一个字节
unsigned char OneWire_ReceiveByte(void)
{
unsigned char i;
unsigned char Byte=0x00;
for(i=0;i<8;i++)
{
if(OneWire_ReceiveBit()){Byte|=(0x01<<i);}
}
return Byte;
}

头文件:

#ifndef __ONEWIRE_H__
#define __ONEWIRE_H__

unsigned char OneWire_Init(void);
void OneWire_SendBit(unsigned char Bit);
unsigned char OneWire_ReceiveBit(void);
void OneWire_SendByte(unsigned char Byte);
unsigned char OneWire_ReceiveByte(void);

#endif

另一点特别重要的是由于1-wire中精度要求较高(里面出现的时间都是几十微秒的),使用绝对时间,不像i2c有独立时钟线,所以当程序中有中断的时候就会很影响1-wire工作,对于这点,我们需要在1-wire相关函数开始关闭终端开关EA,结束再打开。如图所示,三个底层都需要这样操作。

3.2 ds18b20运行逻辑与底层

ds18b20属于数字温度传感器,其中集成了模拟温度传感器(就是一头高电平一头低电平,中间一个电阻一个热敏电阻,然后读取两个电阻中间电压的那种),读取内部模拟温度传感器数据然后存储在RAM里面,通过引脚用1-wire通信协议把协议转化出来。

前文提到ds18b20是根据主机通过1-wire通信线发送的命令字节执行命令的,那么我们来看看ds18b20寄存器地址存储内容和不同命令字节对应的命令:

一般来说我们只需要用到寄存器的前两位温度存储位置。温度存储为分为低位和高位,存储内容如图所示。

这里列举一些常见的命令字节,至于运行流程的逻辑框图因为手册里是英文的就不放出来了,等会儿会在写代码的注释里。

c文件:

#include <REGX52.H>
#include "OneWire.h"

//DS18B20指令
#define DS18B20_SKIP_ROM 0xCC
#define DS18B20_CONVERT_T 0x44
#define DS18B20_READ_SCRATCHPAD 0xBE

//DS18B20开始温度变换
//流程:初始化,跳过ROM,开始温度变换
void DS18B20_ConvertT(void)
{
OneWire_Init();
OneWire_SendByte(DS18B20_SKIP_ROM);
OneWire_SendByte(DS18B20_CONVERT_T);
}

//DS18B20读取温度
//流程:初始化,跳过ROM,读暂存器,连续读操作(高位低位),转化为实际温度值
float DS18B20_ReadT(void)
{
unsigned char TLSB,TMSB;
int Temp;
float T;
OneWire_Init();
OneWire_SendByte(DS18B20_SKIP_ROM);
OneWire_SendByte(DS18B20_READ_SCRATCHPAD);
TLSB=OneWire_ReceiveByte();
TMSB=OneWire_ReceiveByte();
Temp=(TMSB<<8)|TLSB;
T=Temp/16.0; //除以16.0避免损失精度,因为暂存器最低位表示2的负四次方故是16
return T;
}

头文件:

#ifndef __DS18B20_H__
#define __DS18B20_H__

void DS18B20_ConvertT(void);
float DS18B20_ReadT(void);

#endif

另外,还有一点,需要注意,根据精确度的不同,转换时间快慢也会受到影响,所以在复位或者刚上电的时候会存在显示上一次读取或者显示默认值(85度)的情况,所以在主函数调用的时候会用个温度读取和delay再进入循环显示。

见后续文章。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询