1 LCD1602显示
1.1 概述
LCD1602(Liquid Crystal Display)是一种工业字符型液晶,能够同时显示 16×02 即 32 字符(16列两行)
引脚说明
第 1 脚: VSS 为电源地
第 2 脚: VDD 接 5V 正电源
第 3 脚: V0为液晶显示器对比度调整端,接正电源时对比度最弱,接地电源时对比度最高(对比度过高时会 产生“鬼影”,使用时可以通过一个10K的电位器调整对比度)。
第 4 脚:RS 为寄存器选择,高电平时选择数据寄存器、低电平时选择指令寄存器。
第 5 脚:R/W 为读写信号线,高电平时进行读操作,低电平时进行写操作。
当 RS 和 R/W 共同为低电平时可以写入指令或者显示地址,
当 RS 为低电平 R/W 为高电平时可以读忙信号,
当 RS 为高电平 R/W 为低电平时可以写入数据。
第 6 脚:E 端为使能端,当 E 端 为高电平(1)时,读取信息,当由高电平跳变成低电平时,负跳变时液晶模块执行命令。
第 7-14 脚:D0~D7 为 8 位双向数据线。
第 15 脚:背光源正极。
第 16 脚:背光源负极。
1.2 接线方法
//电源
VSS -- GND
VDD -- 5V
//对比度
VO -- GND
//控制线
RS -- P1.0
RW -- P1.1
E -- P1.4
//背光灯
A -- 5V
K -- GDN
//数据
D0到D7 -- P0.到P0.7
2、显示原理
要想搞懂1602如何显示,就只需搞懂两个问题(在哪显示,如何显示)。
首先来说一下在哪显示这个问题:
LCD1602可以显示16*2个字符且通过D0-D7八个引脚传输数据八位数据,每一个显示的位置都对应上图的一个地址。例如我想在第一行的第四个位置显示,那么就可以锁定上表中的“03”,换算成二进制就是0000 0011。
听上去很简单,但是LCD1602有个特点就是写入显示地址时要求最高位 D7 恒定为高电平。所以我们想显示真实的地址应该为1000 0011。
在哪显示说清楚了,现在来搞怎么显示这个问题:
在LCD1602的手册中给出了这样一个表格,我们想显示表格中的字符只需获取某个字符的高位和低位即可。例如我想显示“H”这个字符,就是0100 1000转换成16进制就是0x48,刚好对应‘H’的ASCLL码72。当然我们在编程的时候无需这样操作,只需输入字符即可,编译器会自动编译成对应的ASCLL码。
说到这里会发现无论是传输地址的指令还是传输字符的指令都是通过D0-D7这八根线进行传输,那么我们什么时候传输的数据,说明时候传输的是地址呢。
我们靠的是在1602上的RS引脚,
RS为高电平1时选择数据寄存器(写数据)、
低电平0时选择指令寄存器(写地址)。
2.1 读写操作时序
- RS 为寄存器选择,
- 高电平(1)时选择数据寄存器、
- 低电平(0)时选择指令寄存器。
- 读操作时序
首先看RS,RS的时序分析非常简单就正如上文所说我们只需搞清楚到底是是写地址还是写数据 ,写地址为0,写数据为1即可,没有太多好分析的。
R/W:可以从途中看出来R/W的时序低电平贯穿了整个时序,因此我们将R/W置0即可。
E:初始状态为0,然后延时至少tR之后置1(tR的值参考上表,25ns执行一个_nop_();函数即可),置1后要延时至少tPW(上图给出的tPW值为150ns,建议执行两个_nop_();函数),接着再至少延时tF(执行一个_nop_();函数)后置0。
2.2.1 代码如下(基于51单片机),其它单片机此原理都可适用。
#include "reg52.h"
#include "intrins.h" // _nop_()
/*
RS -- P1.0
RW -- P1.1
E -- P1.4 */
#define databuffer P0 //定义8位数据线,Po端口组
// 控制线的连接
sbit RS = P1^0;
sbit RW = P1^1;
sbit EN = P1^4;
void Write_Cmd_Func(char cmd) // 指令函数 (写时序)
{
check_busy();//检测忙信号函数
RS = 0; //RS为低电平:写指令
RW = 0; // 低电平 (读写信号线)
EN = 0; // 执行命令
_nop_(); // 执行一个空函数延时1.085 us
databuffer = cmd; // 写指令
_nop_();
EN = 1;
_nop_();
_nop_();
EN = 0;
_nop_();
}
void Write_Data_Func(char dataShow) // 数据内容 (写时序)
{
check_busy();
RS = 1; // RS为高电平:写内容
RW = 0; // 低电平 (读写信号线)
EN = 0;
_nop_();
databuffer = dataShow;
_nop_();
EN = 1;
_nop_();
_nop_();
EN = 0;
_nop_();
}
2.2 读操作时序:
读操作时序主要应用在检测忙信号,忙信号也在手册初始化中使用到,下文会提到。所以我们分析读操作时序的目的也就是为了写检测忙信号函数。
RS:置0,写命令
RW:高电平贯穿整个时序,因此置1。
E:初始状态为0,延时tR后拉高,之后再延时tPW后拉低。
读操作时序与写操作时序的区别在于:
- 写操作时序在E=0的时候就开始传输数据,
- 读操作时序要等E=1之后才开始传输数据。
2.2.1 代码如下
void check_busy()//检测忙信号函数
{
char tmp = 0x80; //创建一个变量,存放数据
databuffer=0x80; //初始值为忙,只要当单片机发数据后高位变低后才为不忙
while(tmp & 0x80){ //检测tmp的高位bf的值是否为高电平,如果为忙程序卡住不往下执行
//高电平:忙 低电平:不忙
RS = 0;
RW = 1;
EN = 0;
_nop_();
EN = 1;
_nop_();
_nop_();
tmp=databuffer;
EN = 0;
_nop_();
}
}
2.3 LCD1602初始化:
当然这里还需注意LCD1602的手册给出了使用前还需将LCD1602初始化,具体初始化内容如下,我们只需调用我们刚刚封装好的写命令函数一步一步执行手册所给的内容即可。
void LCD1602_INIT() // 初始化
{
//(1)延时 15ms
Delay15ms();
//(2)写指令 38H(不检测忙信号)
Write_Cmd_Func(0x38);
//(3)延时 5ms
Delay5ms();
//(4)以后每次写指令,读/写数据操作均需要检测忙信号 (读操作时序)
//(5)写指令 38H:显示模式设置
Write_Cmd_Func(0x38);
//(6)写指令 08H:显示关闭
Write_Cmd_Func(0x08);
//(7)写指令 01H:显示清屏
Write_Cmd_Func(0x01);
//(8)写指令 06H:显示光标移动设置
Write_Cmd_Func(0x06);
//(9)写指令 0CH:显示开及光标设置}
Write_Cmd_Func(0x0c);
}
2.3LCD1602显示一个字符
我们上面已经把写命令函数,写数据函数,初始化函数等都已经写完了,现在就可以在main函数里面操作让我们的LCD1602显示一个字符了。
代码如下:
#include "reg52.h" // 包含头文件reg52.h,定义了51系列单片机的特殊功能寄存器
#include "intrins.h" // 包含intrins.h头文件,定义了_nop_()等内联汇编函数
/*
RS -- P1.0
RW -- P1.1
E -- P1.4 */
// 定义8位数据线,连接到P0端口组
#define databuffer P0
// 控制线的连接
sbit RS = P1^0; // RS引脚连接到P1.0
sbit RW = P1^1; // RW引脚连接到P1.1
sbit EN = P1^4; // EN引脚连接到P1.4
// 检测忙信号
void check_busy()
{
char tmp = 0x80; // 临时变量,用于存储读回的忙信号
databuffer = 0x80; // 数据缓冲器设置为0x80
while(tmp & 0x80) { // 检测忙信号的最高位
RS = 0; // 选择指令寄存器
RW = 1; // 选择读操作
EN = 0;
_nop_(); // 延时1.085微秒
EN = 1;
_nop_();
_nop_();
tmp = databuffer; // 读取数据缓冲器中的忙信号
EN = 0;
_nop_();
}
}
// 写入指令
void Write_Cmd_Func(char cmd)
{
check_busy(); // 检查忙信号
RS = 0; // 选择指令寄存器
RW = 0; // 选择写操作
EN = 0;
_nop_();
databuffer = cmd; // 向数据缓冲器写入指令
_nop_();
EN = 1;
_nop_();
_nop_();
EN = 0;
_nop_();
}
// 写入数据
void Write_Data_Func(char dataShow)
{
check_busy(); // 检查忙信号
RS = 1; // 选择数据寄存器
RW = 0; // 选择写操作
EN = 0;
_nop_();
databuffer = dataShow; // 向数据缓冲器写入数据
_nop_();
EN = 1;
_nop_();
_nop_();
EN = 0;
_nop_();
}
// 延时15毫秒
void Delay15ms() //@11.0592MHz 延时15ms
{
unsigned char i, j;
i = 27;
j = 226;
do
{
while (--j);
} while (--i);
}
// 延时5毫秒
void Delay5ms() //@11.0592MHz 延时5ms
{
unsigned char i, j;
i = 9;
j = 244;
do
{
while (--j);
} while (--i);
}
// LCD1602初始化
void LCD1602_INIT()
{
Delay15ms(); // 延时15ms
Write_Cmd_Func(0x38); // 写指令 38H(不检测忙信号)
Delay5ms(); // 延时5ms
Write_Cmd_Func(0x38); // 写指令 38H:显示模式设置
Write_Cmd_Func(0x08); // 写指令 08H:显示关闭
Write_Cmd_Func(0x01); // 写指令 01H:显示清屏
Write_Cmd_Func(0x06); // 写指令 06H:显示光标移动设置
Write_Cmd_Func(0x0c); // 写指令 0CH:显示开及光标设置
}
void main()
{
char position = 0x80 + 0x05; // LED屏的显示位置
char dataShow = 'L'; // 显示的内容
LCD1602_INIT(); // LED初始化
Write_Cmd_Func(position); // 选择要显示的地址
Write_Data_Func(dataShow); // 发送要显示的字符
}
效果如下:
2.4 LCD1602显示多行
想要显示一行我们只需基于上面的内容封装一个函数即可。
代码如下:
#include "reg52.h" // 包含头文件reg52.h,定义了51系列单片机的特殊功能寄存器
#include "intrins.h" // 包含intrins.h头文件,定义了_nop_()等内联汇编函数
/*
RS -- P1.0
RW -- P1.1
E -- P1.4
*/
#define databuffer P0 // 定义8位数据线,P0端口组
// 控制线的连接
sbit RS = P1^0; // 定义RS为P1端口的第0位
sbit RW = P1^1; // 定义RW为P1端口的第1位
sbit EN = P1^4; // 定义EN为P1端口的第4位
// 检测忙信号(读操作时序)
void check_busy()
{
char tmp = 0x80; // 创建一个变量tmp,初始值为0x80(即忙信号)
databuffer = 0x80; // 初始值为忙,只要当单片机发数据后高位变低后才为不忙
// 检测tmp的高位bf的值是否为高电平,如果为忙程序卡住不往下执行
while(tmp & 0x80) {
RS = 0; // RS为低电平:读指令
RW = 1; // RW为高电平:读操作
EN = 0; // 使能信号置低,开始读操作
_nop_(); // 执行一个空函数延时1.085 us
EN = 1; // 使能信号置高,数据有效
_nop_();
_nop_();
tmp = databuffer; // 读数据
EN = 0; // 使能信号置低,结束读操作
_nop_();
}
}
// 写指令函数(写操作时序)
void Write_Cmd_Func(char cmd)
{
check_busy(); // 检测忙信号函数
RS = 0; // RS为低电平:写指令
RW = 0; // RW为低电平:写操作
EN = 0; // 使能信号置低,开始写操作
_nop_();
databuffer = cmd; // 写指令
_nop_();
EN = 1; // 使能信号置高,指令有效
_nop_();
_nop_();
EN = 0; // 使能信号置低,结束写操作
_nop_();
}
// 写数据函数(写操作时序)
void Write_Data_Func(char dataShow)
{
check_busy(); // 检测忙信号
RS = 1; // RS为高电平:数据内容
RW = 0; // RW为低电平:写操作
EN = 0; // 使能信号置低,开始写操作
_nop_();
databuffer = dataShow; // 写数据内容
_nop_();
EN = 1; // 使能信号置高,数据有效
_nop_();
_nop_();
EN = 0; // 使能信号置低,结束写操作
_nop_();
}
// 延时15ms函数,@11.0592MHz
void Delay15ms()
{
unsigned char i, j; // 定义两个无符号字符变量i, j
i = 27; // 初始化变量i为27
j = 226; // 初始化变量j为226
do
{
while (--j); // 内部循环,j递减至0
} while (--i); // 外部循环,i递减至0
}
// 延时5ms函数,@11.0592MHz
void Delay5ms()
{
unsigned char i, j; // 定义两个无符号字符变量i, j
i = 9; // 初始化变量i为9
j = 244; // 初始化变量j为244
do
{
while (--j); // 内部循环,j递减至0
} while (--i); // 外部循环,i递减至0
}
// LCD1602初始化函数
void LCD1602_INIT()
{
Delay15ms(); // 延时15ms
Write_Cmd_Func(0x38); // 写指令38H(不检测忙信号)
Delay5ms(); // 延时5ms
Write_Cmd_Func(0x38); // 写指令38H:显示模式设置
Write_Cmd_Func(0x08); // 写指令08H:显示关闭
Write_Cmd_Func(0x01); // 写指令01H:显示清屏
Write_Cmd_Func(0x06); // 写指令06H:显示光标移动设置
Write_Cmd_Func(0x0c); // 写指令0CH:显示开及光标设置
}
// 在指定位置显示字符串函数
void LCD602_showLine(char hang, char lie, char *string)
{
switch(hang) { // 根据行号选择显示位置
case 1:
Write_Cmd_Func(0x80 + lie); // 写指令,设置第一行显示位置
while(*string != '\0') { // 当字符串未结束时循环
Write_Data_Func(*string); // 写数据
string++; // 指向下一个字符
}
break;
case 2:
Write_Cmd_Func(0x80 + 0x40 + lie); // 写指令,设置第二行显示位置
while(*string != '\0') { // 当字符串未结束时循环
Write_Data_Func(*string); // 写数据
string++; // 指向下一个字符
}
break;
}
}
// 主函数
void main()
{
LCD1602_INIT(); // 初始化LCD1602
LCD602_showLine(1, 5, "HELLO"); // 在第一行第5列显示"HELLO"
LCD602_showLine(2, 1, "lxl handsome"); // 在第二行第1列显示"lxl handsome"
}
效果如下:
3 DHT11 温湿度传感器
3.1 产品概述
DHT11数字温湿度传感器是一款含有已校准数字信号输出的温湿度复合传感器,应用领域:暖通空调;汽车;消费品;气象站;湿度调节器;除湿器;家电;医疗;自动控制
- 特点
数据传送逻辑
只有一根数据线DATA,上官一号发送序列指令给DHT11模块,模块一次完整的数据传输为40bit(位),高位先出
数据格式
8bit(位)湿度整数数据+8bit(位)湿度小数数据+8bi(位)温度整数数据+8bit(位)温度小数数据+8bit(位)校验和
通讯过程时序图
3.2 检测模块是否存在
使用DHT11时,第一步单片机必须要根据图中所给的时序给DHT11发送一段信号,DHT11接收到单片机发来的信号后才可以进行后续的工作。图中的主机信号也就是单片机给出的信号,单片机在开始的时候需要在a点拉高,然后再b点拉低至少18ms(不放心的话也可以再拉长一点),在c点再变回高电平持续20-40us到了d点。单片机给dht11的初始化信号就发送完毕了。单片机发送完成后,dht11也会随之响应,由时序图可以看出dht11有一段80us的低电平,我们可以利用这一段低电平来检测dht11模块是否存在,也可以判断出来你的模块是否损坏,以免后续因为模块损坏的问题查找半天错误,得不偿失。
因此我们可以在拿d点来进行判断,在c点拉高60us后我们检测d点是否为低电平,如果为低电平则dht11存在,反之则不存在。如果d点位低电平可以点亮开发板上的一个led便于我们观察。
在编程之前还需一点需要注意,dht11的手册上给出了这样一段话,为了避免上电初始状态不稳定,因此我们要在单片机上电的时候延时1s左右(我本人延时了2s,这个大家可以根据自己的实际情况调整,在没有接收到dht11的数据时可以考虑一下这个原因)。
- 开始
- 结束
- 转折
检测模式是否存在 d 至 E 区间 是否是低电平,是并且持续时间为80us,则表示模块存在
时序逻辑分析
代码实现
#include "reg52.h"
#include "intrins.h"
sbit ledOne = P3^7;
sbit dht = P3^3;//模块的data插在p3.3
void Delay30ms() //@11.0592MHz
{
unsigned char i, j;
i = 54;
j = 199;
do
{
while (--j);
} while (--i);
}
void Delay60us() //@11.0592MHz
{
unsigned char i;
i = 25;
while (--i);
}
void Delay1000ms() //@11.0592MHz
{
unsigned char i, j, k;
_nop_();
i = 8;
j = 1;
k = 243;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
}
void check_DHT()
{
//a : dht = 1
dht = 1;
//b :dht = 0
dht = 0;
//延时30ms
Delay30ms();
//c: dht = 1
dht = 1;
//在60us后读d点,如果d点是低电平(被模块拉低),说明模块存在!
Delay60us();
if(!dht)
ledOne = 0;//亮灯,说明模块存在
}
void main()
{
ledOne = 1;
Delay1000ms();
Delay1000ms();
check_DHT();
while(1);
}
这个while(1)
确保在调用check_DHT()
函数后,程序将一直运行,不会退出,从而保持系统的持续运行。这在嵌入式系统中是非常常见的用法,因为这些系统通常需要24/7运行以执行其任务。
3.2 DHT11数据的时序分析
DHT11传输0的时序分析
DHT11传输1的时序分析
检测完模块存在之后,我们现在进行时序分析。从时序图可以看出在f点和g点之间的时候就已经开始传输数据了,那么我们现在就需要找到具体从哪里开始传输数据才可以进行后续的编程。在时序图中可以看出在c点之后有一段拉高20us-40us,我们给他拉高的30us,但还是无法得出程序会运行在时序图中具体的哪个电上。因此我们用“卡点法”进行分析。
卡d点:用while(dht1),如果是高电平就一直是死循环,当过了高电平那一段时(变成低电平)程序才会继续往下执行,所以我们就找到了d点具体的位置。
卡e点:时序图中得出过80us后到达e点然后拉高(高电平),我们不需要关心80us这件事,只需要用while(!dht1)(不是低电平)就可以得出e点的位置。
卡f点:while(dht1)(不是高电平)
卡g点:while(!dht1)(高电平)。g点之后必然是高电平,d无论传输的是“0”还是“1”都是通过高电平进行传输,只不过是高电平持续时间不同。
下图中蓝色部分是相同的时序段,由此可以看出在g点之后开始传输第一个“数据‘0’”.
紧接着g点,我们可以看出如果在g点之后26us-28us之间如果为高电平则表示0,在70us间如果为高电平则表示1。那么我们可以判断g点之后 40us的状态,如果为低电平则表示0,为高电平则表示1。
DHT 测试数据为 40bit(位) = 5个char 一个char(8bit)
读5轮 ,一轮读8次,每8次可获得数据
#include "reg52.h"
#include "intrins.h"
sbit ledOne = P3^7;
sbit dht = P3^3;//模块的data插在p3.3
char datas[5];
sfr AUXR = 0x8E;
void UartInit(void) //9600bps@11.0592MHz 串口初始化
{
AUXR = 0x01;
SCON = 0x40; //配置串口工作方式1,REN不使能接收
TMOD &= 0xF0;
TMOD |= 0x20;//定时器1工作方式位8位自动重装
TH1 = 0xFD;
TL1 = 0xFD;//9600波特率的初值
TR1 = 1;//启动定时器
}
void sendByte(char data_msg) // 输出字符串
{
SBUF = data_msg; // 将要发送的数据存入串口缓冲寄存器
while(!TI); // 等待发送完成标志位TI变为1,表示发送完成
TI = 0; // 清除发送完成标志位,准备下一次发送
}
void sendString(char* str) // 传输字符串
{
while(*str != '\0') // 遍历字符串直到遇到字符串结束符 '\0'
{
sendByte(*str); // 发送当前字符
str++; // 移动到下一个字符
}
}
void Delay30ms() //@11.0592MHz
{
unsigned char i, j;
i = 54;
j = 199;
do
{
while (--j);
} while (--i);
}
void Delay60us() //@11.0592MHz
{
unsigned char i;
i = 25;
while (--i);
}
void Delay1000ms() //@11.0592MHz
{
unsigned char i, j, k;
_nop_();
i = 8;
j = 1;
k = 243;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
}
void DHT11_Start() // 开始
{
// a :dht =1;
dht = 1;
// b :dht = 0;
dht = 0;
//延时30ms
Delay30ms();
dht = 1;
//卡d点;while(dht); 变成低电平
while(dht);
// 卡e点 while(!dht) 变成高电平
while(!dht);
// 卡f点:while(dht) 变成低电平
while(dht);
}
void Delay40us() //@11.0592MHz
{
unsigned char i;
_nop_();
i = 15;
while (--i);
}
void Read_Data_From_DHT() // 读数据
{
int i;//轮
int j;//每一轮读多少次
char tmp;
char flag;
DHT11_Start();
for(i= 0;i < 5;i++){
//卡g点:while(!dht) 有效数据都是高电平,持续时间不一样,50us读,低电平0 高电平
for(j=0;j<8;j++){
while(!dht);//等待卡g点 变成高电平
Delay40us();
if(dht == 1){ // 高电平
flag = 1;
while(dht); //变成低电平
}else{
flag = 0;
}
tmp = tmp << 1; // 左移1为位
tmp |= flag; // tmp左移1为位之后的值等于flag
}
datas[i] = tmp;
}
}
void main()
{
ledOne = 1;
UartInit();
Delay1000ms();
Delay1000ms();
while(1) {
Delay1000ms(); // 延迟1秒
Read_Data_From_DHT(); // 读取DHT11传感器的数据
// 发送湿度数据
sendString("H:"); // 发送字符串 "H:"
sendByte(datas[0] / 10 + 0x30); // 发送湿度整数部分的十位
sendByte(datas[0] % 10 + 0x30); // 发送湿度整数部分的个位
sendByte('.'); // 发送小数点
sendByte(datas[1] / 10 + 0x30); // 发送湿度小数部分的十位
sendByte(datas[1] % 10 + 0x30); // 发送湿度小数部分的个位
sendString("\r\n"); // 发送回车换行符,表示湿度数据发送完毕
// 发送温度数据
sendString("T:"); // 发送字符串 "T:"
sendByte(datas[2] / 10 + 0x30); // 发送温度整数部分的十位
sendByte(datas[2] % 10 + 0x30); // 发送温度整数部分的个位
sendByte('.'); // 发送小数点
sendByte(datas[3] / 10 + 0x30); // 发送温度小数部分的十位
sendByte(datas[3] % 10 + 0x30); // 发送温度小数部分的个位
sendString("\r\n"); // 发送回车换行符,表示温度数据发送完毕
}
}
详细解释
1. 延迟1秒
Delay1000ms(); // 延迟1秒
- 这个函数调用会让单片机延迟1秒钟。
2. 读取DHT11传感器的数据
Read_Data_From_DHT(); // 读取DHT11传感器的数据
-
- 该函数会从DHT11传感器读取湿度和温度数据,并将其存储在
datas
数组中。
- 该函数会从DHT11传感器读取湿度和温度数据,并将其存储在
3. 发送湿度数据
sendString("H:"); // 发送字符串 "H:"
-
- 通过串口发送字符串 "H:",表示接下来的数据是湿度数据。
sendByte(datas[0] / 10 + 0x30); // 发送湿度整数部分的十位
-
datas[0]
是湿度的整数部分。除以10得到十位数,加上0x30
(ASCII码值48) 将其转换为字符后,通过串口发送。
sendByte(datas[0] % 10 + 0x30); // 发送湿度整数部分的个位
-
datas[0]
对10取余得到个位数,加上0x30
将其转换为字符后,通过串口发送。
sendByte('.'); // 发送小数点
-
- 发送小数点。
sendByte(datas[1] / 10 + 0x30); // 发送湿度小数部分的十位
sendByte(datas[1] % 10 + 0x30); // 发送湿度小数部分的个位
-
datas[1]
是湿度的小数部分。分别发送其十位和个位。
sendString("\r\n"); // 发送回车换行符,表示湿度数据发送完毕
-
- 发送回车换行符,表示湿度数据发送完毕。
4. 发送温度数据
sendString("T:"); // 发送字符串 "T:"
-
- 通过串口发送字符串 "T:",表示接下来的数据是温度数据。
sendByte(datas[2] / 10 + 0x30); // 发送温度整数部分的十位
sendByte(datas[2] % 10 + 0x30); // 发送温度整数部分的个位
sendByte('.'); // 发送小数点
sendByte(datas[3] / 10 + 0x30); // 发送温度小数部分的十位
sendByte(datas[3] % 10 + 0x30); // 发送温度小数部分的个位
-
datas[2]
是温度的整数部分,datas[3]
是温度的小数部分。分别发送其十位和个位。
sendString("\r\n"); // 发送回车换行符,表示温度数据发送完毕
-
- 发送回车换行符,表示温度数据发送完毕。
这段代码的目的是通过串口每秒发送一次从DHT11传感器读取的湿度和温度数据,格式如下:
H:xx.xx
T:xx.xx
在这段代码中,0x30
的作用是将数值转换为其对应的 ASCII 字符。为了更详细地解释,首先需要了解 ASCII 编码的基本知识。
5. ASCII 编码
- ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是一种字符编码标准,用于表示文本中的字符。
- 每个字符在 ASCII 表中都有一个对应的数值。例如,字符
'0'
的 ASCII 值是 48(十进制),也就是 0x30(十六进制)。
6. 为什么需要 0x30
当需要通过串口发送数据时,通常是以字符形式发送的。如果直接发送数值,接收端会将其解释为控制字符或其它非可显示字符。因此,必须将数值转换为对应的字符。
7. 详细解释
假设 datas[0]
的值是 43(湿度的整数部分)。为了将其转换为字符形式,需要分别处理十位和个位:
- 十位数的转换
sendByte(datas[0] / 10 + 0x30);
datas[0] / 10
计算湿度的十位数。对于 43,这个值是 4。4
加上0x30
(48)后得到0x34
(52),对应的 ASCII 字符是'4'
。sendByte(0x34)
通过串口发送字符'4'
。
- 个位数的转换
sendByte(datas[0] % 10 + 0x30);
datas[0] % 10
计算湿度的个位数。对于 43,这个值是 3。3
加上0x30
(48)后得到0x33
(51),对应的 ASCII 字符是'3'
。sendByte(0x33)
通过串口发送字符'3'
。
通过这种方式,原本的数值 43 被转换并发送为字符 '4'
和 '3'
,接收端就可以正确显示湿度值。
8. 输出结果
然后通过电脑的串口助手打印出来,结果如下:
led屏幕显示内容,并且当温度超过24度时,风扇会转动
#include "reg52.h" // 包含51单片机的寄存器定义
#include "intrins.h" // 包含一些常用的内联函数
sbit ledOne = P3^7; // 定义ledOne位于P3口的第7位
sbit dht = P3^3; // 定义DHT11数据引脚位于P3口的第3位
sbit fengshan = P1^6; // 定义风扇控制引脚位于P1口的第6位
char datas[5]; // 存储从DHT11读取的数据
sfr AUXR = 0x8E; // 定义特殊功能寄存器AUXR
#define databuffer P0 // 定义8位数据线,绑定P0端口
sbit RS = P1^0; // 定义RS位于P1口的第0位
sbit RW = P1^1; // 定义RW位于P1口的第1位
sbit EN = P1^4; // 定义EN位于P1口的第4位
char temp[8]; // 存储温度字符串
char huma[8]; // 存储湿度字符串
// 检查LCD是否忙碌
void check_busy()
{
char tmp = 0x80;
databuffer = 0x80;
while(tmp & 0x80) { // 循环检测忙碌标志位
RS = 0;
RW = 1;
EN = 0;
_nop_();
EN = 1;
_nop_();
_nop_();
tmp = databuffer; // 读取忙碌状态
EN = 0;
_nop_();
}
}
// 写命令到LCD
void Write_Cmd_Func(char cmd)
{
check_busy(); // 检查LCD是否忙碌
RS = 0; // RS低电平,选择指令寄存器
RW = 0; // RW低电平,选择写操作
EN = 0;
_nop_();
databuffer = cmd; // 发送命令
_nop_();
EN = 1;
_nop_();
_nop_();
EN = 0;
_nop_();
}
// 写数据到LCD
void Write_Data_Func(char dataShow)
{
check_busy(); // 检查LCD是否忙碌
RS = 1; // RS高电平,选择数据寄存器
RW = 0; // RW低电平,选择写操作
EN = 0;
_nop_();
databuffer = dataShow; // 发送数据
_nop_();
EN = 1;
_nop_();
_nop_();
EN = 0;
_nop_();
}
// 延迟15ms
void Delay15ms() //@11.0592MHz
{
unsigned char i, j;
i = 27;
j = 226;
do
{
while (--j);
} while (--i);
}
// 延迟5ms
void Delay5ms() //@11.0592MHz
{
unsigned char i, j;
i = 9;
j = 244;
do
{
while (--j);
} while (--i);
}
// 串口初始化
void UartInit(void) //9600bps@11.0592MHz
{
AUXR = 0x01;
SCON = 0x40; // 配置串口工作方式1,REN不使能接收
TMOD &= 0xF0;
TMOD |= 0x20; // 定时器1工作方式为8位自动重装
TH1 = 0xFD;
TL1 = 0xFD; // 9600波特率的初值
TR1 = 1; // 启动定时器
}
// 发送一个字节数据到串口
void sendByte(char data_msg)
{
SBUF = data_msg; // 将数据存入串口缓冲寄存器
while(!TI); // 等待发送完成标志位TI变为1
TI = 0; // 清除发送完成标志位
}
// 发送字符串到串口
void sendString(char* str)
{
while(*str != '\0') { // 遍历字符串直到遇到结束符'\0'
sendByte(*str); // 发送当前字符
str++; // 移动到下一个字符
}
}
// 延迟30ms
void Delay30ms() //@11.0592MHz
{
unsigned char i, j;
i = 54;
j = 199;
do
{
while (--j);
} while (--i);
}
// 延迟60us
void Delay60us() //@11.0592MHz
{
unsigned char i;
i = 25;
while (--i);
}
// 延迟1000ms
void Delay1000ms() //@11.0592MHz
{
unsigned char i, j, k;
_nop_();
i = 8;
j = 1;
k = 243;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
}
// 启动DHT11
void DHT11_Start()
{
dht = 1;
dht = 0; // 拉低电平
Delay30ms(); // 延时30ms
dht = 1;
while(dht); // 等待低电平
while(!dht); // 等待高电平
while(dht); // 等待低电平
}
// 延迟40us
void Delay40us() //@11.0592MHz
{
unsigned char i;
_nop_();
i = 15;
while (--i);
}
// 从DHT11读取数据
void Read_Data_From_DHT()
{
int i; // 轮
int j; // 每一轮读多少次
char tmp;
char flag;
DHT11_Start(); // 启动DHT11
for(i= 0;i < 5;i++) { // 读取5个字节
for(j=0;j<8;j++) { // 每个字节8位
while(!dht); // 等待高电平
Delay40us(); // 延时40us
if(dht == 1) { // 高电平表示1
flag = 1;
while(dht); // 等待低电平
} else { // 低电平表示0
flag = 0;
}
tmp = tmp << 1; // 左移1位
tmp |= flag; // 存储当前位
}
datas[i] = tmp; // 存储当前字节
}
}
// 初始化LCD1602
void LCD1602_INIT()
{
Delay15ms(); // 延时15ms
Write_Cmd_Func(0x38); // 显示模式设置
Delay5ms(); // 延时5ms
Write_Cmd_Func(0x38); // 显示模式设置
Write_Cmd_Func(0x08); // 显示关闭
Write_Cmd_Func(0x01); // 显示清屏
Write_Cmd_Func(0x06); // 显示光标移动设置
Write_Cmd_Func(0x0c); // 显示开及光标设置
}
// 在LCD1602的指定位置显示字符串
void LCD1602_showLine(char hang, char lie, char *string)
{
switch(hang) {
case 1:
Write_Cmd_Func(0x80 + lie); // 第一行的指定位置
while(*string) {
Write_Data_Func(*string); // 显示字符串
string++;
}
break;
case 2:
Write_Cmd_Func(0x80 + 0x40 + lie); // 第二行的指定位置
while(*string) {
Write_Data_Func(*string); // 显示字符串
string++;
}
break;
}
}
// 构建湿度和温度的显示字符串
void Build_Datas()
{
huma[0] = 'H';
huma[1] = datas[0] / 10 + 0x30; // 湿度整数部分的十位
huma[2] = datas[0] % 10 + 0x30; // 湿度整数部分的个位
huma[3] = '.';
huma[4] = datas[1] / 10 + 0x30; // 湿度小数部分的十位
huma[5] = datas[1] % 10 + 0x30; // 湿度小数部分的个位
huma[6] = '%';
huma[7] = '\0'; // 字符串结束符
temp[0] = 'T';
temp[1] = datas[2] / 10 + 0x30; // 温度整数部分的十位
temp[2] = datas[2] % 10 + 0x30; // 温度整数部分的个位
temp[3] = '.';
temp[4] = datas[3] / 10 + 0x30; // 温度小数部分的十位
temp[5] = datas[3] % 10 + 0x30; // 温度小数部分的个位
temp[6] = 'C';
temp[7] = '\0'; // 字符串结束符
}
// 主函数
void main()
{
Delay1000ms(); // 延时1秒
UartInit(); // 初始化串口
LCD1602_INIT(); // 初始化LCD1602
Delay1000ms(); // 延时1秒
Delay1000ms(); // 再延时1秒
ledOne = 0; // 关闭LED
while(1) { // 主循环
Delay1000ms(); // 延时1秒
Read_Data_From_DHT(); // 读取DHT11数据
if(datas[2] > 24) { // 当温度大于24度
fengshan = 0; // 启动风扇
}
Build_Datas(); // 构建湿度和温度的显示字符串
sendString(huma); // 通过串口发送湿度字符串
sendString("\r\n"); // 发送换行符
sendString(temp); // 通过串口发送温度字符串
sendString("\r\n"); // 发送换行符
LCD1602_showLine(1, 2, huma); // 在LCD1602的第一行显示湿度字符串
LCD1602_showLine(2, 2, temp); // 在LCD1602的第二行显示温度字符串
}
}
9. 定义和声明的区别
声明:用来告诉编译器变量的名称和类型,而不分配内存,不赋初值。
定义:为了给变量分配内存,可以为变量赋初值。
注:定义要为变量分配内存空间;而声明不需要为变量分配内存空间。
- extern用法
extern是一种“外部声明”的关键字,字面意思就是在此处声明某种变量或函数,在外部定义。
- extern 函数
为什么要用extern 函数呢?直接#include相应的头文件不可以嘛?
例子,如b.c 想调用a.c 中的fun函数,有两种方法:
方法1:include 头文件,即直接 #include "a.h"
方法2: extern 方法 ,extern void fun(...)这句在调用文件中使用,表示引用全局函数fun(),当然,函数默认是全局的。
优点:不inlcude delayms.h就不会引入大量头文件,进而不会引入大量的无关函数。这样做的一个明显的好处是,会加速程序的编译(确切的说是预处理)的过程,节省时间。 在makefile中需要led.o和delay.o写在一起,否则link的时候找不到delayms而报错。
- extern 变量
如果文件b.c需要引用a.c中变量int v,就可以在b.c中声明extern int v,然后就可以引用变量v。能够被其他模块以extern修饰符引用到的变量通常是全局变量。注意,extern int v可以放在a.c中的任何地方,具体作用范围和局部变量相同。
extern的原理很简单,就是告诉编译器:“你现在编译的文件中,有一个标识符虽然没有在本文件中定义,但是它是在别的文件中定义的全局变量,你要放行!”