本教材现以连载的方式由网络发布,并将于2014年由清华大学出版社出版最终完整版,版权归作者和清华大学出版社所有。本着开源、分享的理念,本教材可以自由传播及学习使用,但是务必请注明出处来自金沙滩工作室
16.1 红外光的基本原理红外线是波长介于微波和可见光之间的电磁波,波长在760纳米到1毫米之间,是波形比红光长的非可见光。自然界中的一切物体,只要它的温度高于绝对零度(-273)就存在分子和原子的无规则运动,其表面就会不停的辐射红外线。当然了,虽然是都辐射红外线,但是不同的物体辐射的红外强度是不一样的,而我们正是利用了这一点把红外技术应用到我们实际开发中。 红外发射管很常用,在我们的遥控器上都可以看到,他类似发光二极管,但是他发射出来的是红外光,是我们肉眼所看不到的。第二课我们学过发光二极管会随着电流的增大亮度逐渐增加,同样的道理,红外发射管会随着电流的增大,红外线的强度越来越强,常见的红外发射管如图16-1所示。
图16-1 红外发射管 红外接收管内部带了一个具有红外光敏感特征的PN节,属于光敏二极管,但是它只对红外光有反应。无红外光时,光敏管不导通,有红外光时,光敏管导通形成光电流,并且在一定范围内电流随着红外光的强度的增强而增大。典型的红外接收管如图16-2所示。
图16-2 红外接收管 这种红外发射和接收对管在小车、机器人避障以及红外循迹小车中有所应用,这部分内容在我们的KST-51开发板上并没有实现,但是属于红外部分的内容,所以我提供一个原理图给大家作为学习之用,如图16-3所示。
图16-3 红外避障、循迹原理图 在图16-3这个原理图中,发射控制和接收检测都是接到我们单片机的IO口上。 发射部分:当发射控制输出高电平时,三极管Q1不导通,红外发射管L1不会发射红外信号;当发射控制输出低电平的时候,通过三极管Q1导通让L1发出红外光。 接收部分:R4是一个电位器,也就是“传说”中的滑动变阻器。我们通过调整这个滑动变阻器给LM393的2脚一个阈值电压,这个电压值大小可以根据实际情况来确定。而红外光敏二极管L2收到红外光的时候,会产生电流,并且随着红外光的从弱变强,电流会从小变大。当没有红外光或者说红外光很弱的时候,3脚的电压就会接近VCC,如果3脚比2脚的电压高的话,通过LM393比较器后,接收检测引脚输出一个高电平。当随着光强变大,电流变大,3脚的电压值等于VCC-I*R3,电压就会越来越小,当小到一定程度,比2脚的电压还小的时候,接收检测引脚就会变为低电平。 这个电路用于避障的时候,发射管先发送红外信号,红外信号会随着传送距离的加大逐渐衰减,如果遇到障碍物,就会形成红外反射。当反射回来的信号比较弱时,光敏二极管L2接收的红外光较弱,比较器LM393的3脚电压高于2脚电压,接收检测引脚输出高电平,说明障碍物比较远;当反射回来的信号比较强,接收检测引脚输出低电平,说明障碍物比较近了。 用于小车循迹的时候,必须要有黑色和白色的轨道。当红外信号发送到黑色轨道时,黑色因为吸光能力比较强,红外信号发送出去后就会被吸收掉,反射部分很微弱。白色轨道就会把大部分红外信号返回来。通常情况下的循迹小车,需要应用多个红外模块同时检测,从多个角度判断轨道,根据判断的结果来调整小车使其按照正常循迹前行。 16.2 红外遥控通信原理在实际的通信领域,发出来的信号一般有较宽的频谱,而且都是在比较低的频率段分布大量的能量,所以称之为基带信号,这种信号是不适合直接在信道中传输的。为便于传输、提高抗干扰能力和有效的利用带宽,通常需要将信号调制到适合信道和噪声特性的频率范围内进行传输,这就叫做信号调制。在通信系统的接收端要对接收到的信号进行解调,恢复出原来的基带信号。这部分通信原理的内容,大家了解一下即可。如需了解更多可上 51hei.com搜索“红外”会有很多详细的资料.
我们平时用到的红外遥控器里的红外通信,通常是使用38K左右的载波进行调制的,下面我把原理大概给大家介绍一下,了解一下,先看发送部分原理。 调制:就是用待传送信号去控制某个高频信号的幅度、相位、频率等参量变化的过程,即用一个信号去装载另一个信号。比如我们的红外遥控信号要发送的时候,先经过38K调制,如图16-4所示。
图16-4 红外信号调制 原始信号就是我们要发送的一个数据“0”位或者一位数据“1”位,而所谓38K载波就是频率为38K的方波信号,调制后信号就是最终我们发射出去的波形。我们使用原始信号来控制38K载波,当信号是数据“0”的时候,38K载波毫无保留的全部发送出去,当信号是数据“1”的时候,不发送任何载波信号。 那在原理上,我们如何从电路的角度去实现这个功能呢?如图16-5所示。
图16-5 红外发射原理图 38K载波,我们可以用455K晶振,经过12分频得到37.91K,也可以由时基电路NE555来产生,或者使用单片机的PWM来产生。当信号输出引脚输出高电平时,Q2截止,不管38K载波信号如何控制Q1,右侧的竖向支路都不会导通,红外管L1不会发送任何信息。当信号输出是低电平的时候,那么38K载波就会通过Q1释放出来,在L1上产生38K的载波信号。这里要说明的是,大多数家电遥控器的38K的占空比是1/3,也有1/2的,但是相对少一些。 正常的通信来讲,接收端要首先对信号通过监测、放大、滤波、解调等等一系列电路处理,然后输出基带信号。但是红外通信的一体化接收头HS0038B,已经把这些电路全部集成到一起了,我们只需要把这个电路接上去,就可以直接输出我们所要的基带信号了,如图16-6所示。
图16-6 红外接收原理图 由于红外接收头内部放大器的增益很大,很容易引起干扰,因此在接收头供电引脚上必须加上滤波电容,官方手册给的值是4.7uF,我们这里直接用的10uF,手册里还要求在供电引脚和电源之间串联100欧的电阻,进一步降低干扰。 图16-6所示的电路,用来接收图16-5电路发送出来的波形,当HS0038监测到有38K的红外信号时,就会在OUT引脚输出低电平,当没有38K的时候,OUT引脚就会输出高电平。那我们把OUT引脚接到单片机的IO口上,通过编程,就可以获取红外通信发过来的数据了。 大家想想,OUT引脚输出的数据是不是又恢复成为基带信号数据了呢?那我们单片机在接收这个基带信号数据的时候,如何判断接收到的是什么数据,应该遵循什么协议呢?像我们前边学到的UART、I2C、SPI等通信协议都是基带通信的通信协议,而红外的38K仅仅是对基带信号进行调制解调,让信号更适合在信号中传输。 由于我们的红外调制信号是半双工的,而且同时空间只能允许一个信号源,所以我们红外的基带信号不适合在I2C或者SPI通信协议中进行的,我们前边提到过UART虽然是2条线,但是通信的时候,实际上一条线即可,所以红外可以在UART中进行通信。当然,这个通信也不是没有限制的,比如在HS0038B的数据手册中标明,要想让HS0038B识别到38K的红外信号,那么这个38K的载波必须要大于10个周期,这就限定了我们红外通信的基带信号的比特率必须不能高于3800,那如果把串口输出的信号直接用38K调制的话,波特率也就不能高于3800。当然还有很多其他基带协议可以利用红外来调制,下面我们介绍一种遥控器常用的红外通信协议——NEC协议。 16.3 NEC协议红外遥控器家电遥控器通信距离往往要求不高,而红外的成本比其他无线设备要低的多,所以家电遥控器应用中红外始终占据着一席之地。遥控器的基带通信协议很多,大概有几十种,常用的就有ITT协议、NEC协议、Sharp协议、Philips RC-5协议、Sony SIRC协议等。用的最多的就是NEC协议了,因此我们KST-51开发板随板的遥控器直接采用NEC协议,我们这节课也以NEC协议标准来讲解一下。 NEC协议的数据格式包括了引导码、用户码、用户码(或者用户码反码)、按键键码和键码反码,最后一个停止位,停止位主要起隔离作用,一般不进行判断,编程时我们也不予理会。其中数据编码总共是4个字节32位,如图16-7所示。第一个字节是用户码,第二个字节可能也是用户码,或者是用户码的反码,具体由生产商决定,第三个字节就是当前按键的键数据码,而第四个字节是键数据码的反码,可用于对数据的纠错。
图16-7 NEC协议数据格式 这个NEC协议,表示数据的方式不像我们之前学过的比如uart那样直观,而是每一位数据本身也需要进行编码,编码后再进行载波调制。 引导码:9ms的载波+4.5ms的空闲。 比特值“0”:560us的载波+560us的空闲。 比特值“1”:560us的载波+1.68ms的空闲。 结合图16-7我们就能看明白了,最前面黑乎乎的一段,是引导码的9ms载波,紧接着是引导码的4.5ms的空闲,而后边的数据码,是众多载波和空闲交叉,它们的长短就由其要传递的具体数据来决定。我们的HS0038B这个红外一体化接收头,当收到有载波的信号的时候,会输出一个低电平,空闲的时候会输出高电平,我们用逻辑分析仪抓出来一个红外按键通过HS0038解码后的图形来了解一下,如图16-8所示。
图16-8 红外遥控器按键编码 从图上可以看出,先是9ms载波加4.5ms空闲的起始码,数据码是低位在前,高位在后,数据码第一个字节是8组560us的载波加560us的空闲,也就是0x00,第二个字节是8组560us的载波加1.68ms的空闲,可以看出来是0xFF,这两个字节就是用户码和用户码的反码。按键的键码二进制是0x0B,反码就是0xF3,最后跟了一个560us载波停止位。对于我们的遥控器来说,不同的按键,就是键码和键码反码的区分,用户码是一样的。这样我们就可以通过单片机的程序,把当前的按键的键码给解出来。 我们前边学习中断的时候,学到51单片机有外部中断0和外部中断1这两个外部中断。我们的红外接收引脚接到了P3.3引脚上,这个引脚的第二功能就是外部中断1。在寄存器TCON中的bit3和bit2这两位,是和外部中断1相关的两位。其中IE1是外部中断标志位,当外部中断发生后,这一位被自动置1,和定时器中断标志位TF相似,进入中断后会自动清零,也可以软件清零。bit2位是设置外部中断类型的,如果bit2位为0,那么只要P3.3为低电平就可以触发中断,如果bit2位为1,那么P3.3从高电平到低电平的下降沿发生才可以触发中断。此外,外部中断1使能位是EX1。那下面我们就把程序写出来,使用数码管把遥控器的用户码和键码显示出来。 Infrared.c文件主要是用来检测红外通信的,当发生外部中断后,进入外部中断,通过定时器1定时,首先对引导码判断,而后对数据码的每个位逐位获取高低电平的时间,从而得知每一位是0还是1,最终把数据码解出来。 /***********************infrared.c文件程序源代码*************************/ #include <reg52.h> sbit IR_INPUT = P3^3; //红外接收引脚 bit irflag = 0; //红外接收标志,收到一帧正确数据后置1 unsigned char ircode[4]; //红外代码接收缓冲区 void InitInfrared(void) //红外功能的初始化函数 { TMOD &= 0x0F; //清零T1的控制位 TMOD |= 0x10; //配置T1为模式1 TR1 = 0; //停止T1计数 ET1 = 0; //禁止T1中断 IT1 = 1; //设置INT1为负边沿触发 EX1 = 1; //使能INT1中断 } unsigned int GetHighTime(void) //获取高电平时间 { TH1 = 0; //清零T1计数初值 TL1 = 0; TR1 = 1; //启动T1计数 while (IR_INPUT) //红外输入引脚为1时循环检测等待,变为0时则结束本循环 { if (TH1 >= 0x40) { //当T1计数值大于0x4000,即高电平持续时间超过约18ms时, break; //强制退出循环,是为了避免信号异常时,程序假死在这里。 } } TR1 = 0; //停止T1计数 return (TH1*256 + TL1); //返回T1的计数值 } unsigned int GetLowTime(void) //获取低电平时间 { TH1 = 0; //清零T1计数初值 TL1 = 0; TR1 = 1; //启动T1计数 while (!IR_INPUT) //红外输入引脚为0时循环检测等待,变为1时则结束本循环 { if (TH1 >= 0x40) { //当T1计数值大于0x4000,即低电平持续时间超过约18ms时, break; //强制退出循环,是为了避免信号异常时,程序假死在这里。 } } TR1 = 0; //停止T1计数 return (TH1*256 + TL1); //返回T1的计数值 } void EXINT1_ISR() interrupt 2 //INT1中断服务函数,执行红外接收及解码 { unsigned char i, j; unsigned char byt; unsigned int time; //接收并判定引导码的9ms低电平 time = GetLowTime(); if ((time<7833) || (time>8755)) //时间判定范围为8.5~9.5ms, { //超过此范围则说明为误码,直接退出 IE1 = 0; //退出前清零INT1中断标志 return; } //接收并判定引导码的4.5ms高电平 time = GetHighTime(); if ((time<3686) || (time>4608)) //时间判定范围为4.0~5.0ms, { //超过此范围则说明为误码,直接退出 IE1 = 0; return; } //接收并判定后续的4字节数据 for (i=0; i<4; i++) //循环接收4个字节 { for (j=0; j<8; j++) //循环接收判定每字节的8个bit { //接收判定每bit的560us低电平 time = GetLowTime(); if ((time<313) || (time>718)) //时间判定范围为340~780us, { //超过此范围则说明为误码,直接退出 IE1 = 0; return; } //接收每bit高电平时间,判定该bit的值 time = GetHighTime(); if ((time>313) && (time<718)) //时间判定范围为340~780us, { //在此范围内说明该bit值为0 byt >>= 1; //因低位在先,所以数据左移,高位为0 } else if ((time>1345) && (time<1751)) //时间判定范围为1460~1900us, { //在此范围内说明该bit值为1 byt >>= 1; //因低位在先,所以数据左移, byt |= 0x80; //高位置1 } else //不在上述范围内则说明为误码,直接退出 { IE1 = 0; return; } } ircode[ i] = byt; //接收完一个字节后保存到缓冲区 } irflag = 1; //接收完毕后设置标志 IE1 = 0; //退出前清零INT1中断标志 } 大家在阅读这个文件里的代码时,会发现我们在获取高低电平时间的时候做了超时判断if (TH1 >= 0x40),这个超时判断一方面是应对空间突发的红外干扰信号,如果我们不做超时判断,程序有可能会一直等待下一个跳变才会停止检测,造成程序假死。另外一个方面,遥控器的单按按键和持续按住按键发出来的信号是不同的。我们先来对比一下两种按键方式的信号状态,如图16-9和16-10所示。
图16-9 红外单次按键时序图
图16-10 红外持续按键时序图 单次按键的结果16-9和我们之前的图16-8是一样的,这个不需要再解释。而持续按键,首先会发出一个和单次按键一样的波形出来,经过大概40ms后,会产生一个9ms载波加2.25ms空闲,再跟一个停止位的波形,而后只要你还在按住按键,每经过大概96ms就会产生9ms载波加2.25ms空闲加停止位这样的重复波形。我们人为按下按键的时候,很难控制按下的时间,因此后边的很容易出现这种延续波形,我们加上超时判断也可以有效的避免进入延续波形的死循环中去。 /***********************main.c文件程序源代码*************************/ #include <reg52.h> sbit ADDR3 = P1^3; //LED选择地址线3 sbit ENLED = P1^4; //LED总使能引脚 unsigned char code LedChar[] = { //数码管显示字符转换表 0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E }; unsigned char LedBuff[6] = { //数码管显示缓冲区 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; unsigned char T0RH = 0; //T0重载值的高字节 unsigned char T0RL = 0; //T0重载值的低字节 extern bit irflag; extern unsigned char ircode[4]; void ConfigTimer0(unsigned int ms); extern void InitInfrared(void); void main () { P0 = 0xFF; //P0口初始化 ADDR3 = 1; //选择数码管 ENLED = 0; //LED总使能 InitInfrared(); //初始化红外功能 ConfigTimer0(1); //配置T0定时1ms EA = 1; //开总中断 //PT0 = 1; //配置T0中断为高优先级 while(1) { if (irflag) //接收到红外数据时刷新显示 { irflag = 0; LedBuff[5] = LedChar[ ircode[0] >> 4]; //用户码显示 LedBuff[4] = LedChar[ ircode[0]&0x0F]; LedBuff[1] = LedChar[ ircode[2] >> 4]; //键码显示 LedBuff[0] = LedChar[ ircode[2]&0x0F]; } } } void ConfigTimer0(unsigned int ms) //T0配置函数 { unsigned long tmp; tmp = 11059200 / 12; //定时器计数频率 tmp = (tmp * ms) / 1000; //计算所需的计数值 tmp = 65536 - tmp; //计算定时器重载值 tmp = tmp + 15; //修正中断响应延时造成的误差 T0RH = (unsigned char)(tmp >> 8); //定时器重载值拆分为高低字节 T0RL = (unsigned char)tmp; TMOD &= 0xF0; //清零T0的控制位 TMOD |= 0x01; //配置T0为模式1 TH0 = T0RH; //加载T0重载值 TL0 = T0RL; ET0 = 1; //使能T0中断 TR0 = 1; //启动T0 } void InterruptTimer0() interrupt 1 //T0中断服务函数 { static unsigned char iled = 0; TH0 = T0RH; //定时器重新加载重载值 TL0 = T0RL; //LED数码管动态扫描 P0 = 0xFF; //关闭所有段选位,显示消隐 P1 = (P1 & 0xF8) | iled; //位选索引值赋值到P1口低3位 P0 = LedBuff[ iled]; //相应显示缓冲区的值赋值到P0口 if (iled < 5) //位选索引0-5循环,因有6个数码管 iled++; else iled = 0; } main.c文件程序的主要功能就是把获取到的红外遥控器的用户码和键码信息,传送到数码管上显示出来,并且通过定时器0的1ms中断进行数码管的动态刷新。不知道大家经过试验发现没有,当我们按下遥控器按键的时候,数码管显示的数字会闪烁,这是什么原因呢?单片机的程序都是顺序执行的,一旦我们按下遥控器按键,我们的程序就会进入遥控器解码段,而这个解码段的时间比较长,要几十个毫秒,而我们的数码管动态刷新间隔超过了10ms后就会有闪烁的感觉了,因此这个闪烁主要是由于我们程序执行红外解码时,延误了数码管动态刷新造成的。 如何解决?前边我们讲过中断优先级问题,如果设置了中断优先级,就会产生中断嵌套。中断嵌套的原理,我们在前边讲中断的时候已经讲过一次了,大家可以回头再复习一下。那么这个程序中,有2个中断程序,一个是外部中断程序,一个是定时器中断程序。如果设置外部中断优先级比较高的话,由于在外部中断中要接收红外信号,耗时几十毫秒,会耽误数码管的动态刷新。而在定时器中断程序中,执行时间只有几十个us,即使进入中断,也不会干扰到红外信号的正常接收,因此这个地方我们把定时器0的中断优先级设置为高优先级。在主程序main函数中,大家把这句注释掉的程序 “//PT0 = 1;”取消注释,再编译一下,下载到单片机里,然后再试试发送按键,是不是没有任何闪烁了呢?而中断嵌套的意义也有所体会了吧。 16.4 温度传感器DS18B20DS18B20是美信公司的一款温度传感器,单片机可以通过1-Wire和DS18B20进行通信,最终将温度读出。1-Wire总线的硬件接口很简单,只需要把18B20的数据引脚和单片机的一个IO口接上就可以通信。硬件的简单,随之而来的,就是软件时序的复杂。1-Wire总线的时序比较复杂,很多同学在这里独立看时序图都看不明白,所以这里还要带着大家来研究18B20的时序图。我们先来看一下DS18B20的硬件原理图,如图16-11所示。
图16-11 DS18B20 DS18B20通过编程,可以实现最高12位的温度存储值,在寄存器中,以补码的格式存储,如图16-12所示。
图16-12 DS18B20温度表示 一共2个字节,LSB是低字节,MSB是高字节,其中MSb是字节的高位,LSb是字节的低位。大家可以看出来,二进制数字,每一位代表的温度的含义,都表示出来了。其中S表示的是符号位,低11位都是2的幂,用来表示最终的温度。DS18B20的温度测量范围是从-55度到+125度,而温度数据的表现形式,有正负温度,寄存器中每个数字如同卡尺的刻度一样分布,如图16-13所示。
图16-13 DS18B20温度显示 二进制数字最低位变化1,代表温度变化0.0625度的映射关系。当0度的时候,那就是0x0000,当温度125度的时候,对应十六进制是0x07D0,当温度是零下55度的时候,对应的数字是0xFC90。反过来说,当数字是0x0001的时候,那温度就是0.0625度了。 首先,我先根据手册上DS18B20工作协议过程大概讲解一下。 1、初始化。和I2C的寻址类似,1-Wire总线开始也需要检测这条总线上是否存在DS18B20这个器件。如果这条总线上存在DS18B20,总线会根据时序要求返回一个低电平脉冲,如果不存在的话,也就不会返回脉冲,即总线保持为高电平,所以习惯上称之为检测存在脉冲。此外,获取存在脉冲不仅仅是检测是否存在DS18B20,还要通过这个脉冲过程通知DS18B20准备好,单片机要进行操作它了,如图16-14所示。
图16-14 获取存在脉冲 大家注意看图,实粗线是我们单片机IO口拉低这个引脚,虚粗线是DS18B20拉低这个引脚,细线是单片机和DS18B20释放总线后,依靠上拉电阻的作用把IO口引脚拉上去的。这个我们前边提到过了,51单片机释放总线就是给高电平即可。 存在脉冲检测过程,首先我们单片机要拉低这个引脚,持续大概480us到960us之间的时间即可,我们的程序中持续了500us。然后,单片机释放总线,就是给高电平,DS18B20等待大概15到60us后,会主动拉低这个引脚大概是60到240us,而后DS18B20会主动释放总线,这样IO口会被上拉电阻自动拉高。 有的同学还是不能够彻底理解,程序列出来逐句解释。首先,由于DS18B20时序要求非常严格,所以在操作时序的时候,为了防止中断干扰总线时序,先关闭总中断。然后第一步,拉低DS18B20这个引脚,持续500us;第二步,延时60us;第三步,读取存在脉冲,并且等待存在脉冲结束。 bit Get18B20Ack(void) //复位总线,获取存在脉冲,以启动一次读写操作 { bit ack; EA = 0; //禁止总中断 IO_18B20 = 0; //产生500us复位脉冲 DelayX10us(50); IO_18B20 = 1; DelayX10us(6); //延时60us ack = IO_18B20; //读取存在脉冲 while(!IO_18B20); //等待存在脉冲结束 EA = 1; //重新使能总中断 return ack; } 很多同学对第二步不理解,时序图上明明是DS18B20等待15us到60us,为什么要延时60us呢?举个例子,妈妈在做饭,告诉你大概5分钟到10分钟饭就可以吃了,那么我们什么时候去吃,能够绝对保证吃上饭呢?很明显,10分钟以后去吃肯定可以吃上饭。同样的道理,DS18B20等待大概是15us到60us,我们要保证读到这个存在脉冲,那么60us以后去读肯定可以读到。当然,不能延时太久,太久,超过75us,就可能读不到了,为什么是75us,大家自己思考一下。 2、ROM操作指令。我们学I2C总线的时候,总线上可以挂多个器件,通过不同的器件地址来访问不同的器件。同样,1-Wire总线也可以挂多个器件,但是他只有一条线,如何区分不同的器件呢? 在每个DS18B20内部都有一个唯一的64位长的序列号,这个序列号值就存在DS18B20内部的ROM中。开始的8位是产品类型编码(DS18B20是10H),接着的48位是每个器件唯一的序号,最后的8位是CRC校验码。DS18B20可以引出去很长的线,最长可以到几十米,测不同位置的温度。单片机可以通过和DS18B20之间的通信,获取每个传感器所采集到的温度信息,也可以同时给所有的DS18B20发送一些指令。这些指令相对来说比较复杂,而且应用很少,所以这里大家有兴趣自己查手册自己完成,我们这里只讲一条总线上只接一个器件的指令和程序。 Skip ROM(跳过ROM):0xCC。当总线上只有一个器件的时候,可以跳过ROM,不进行ROM检测。 3、RAM存储器操作指令。 RAM读取指令,只讲2条,其他的大家有需要可以随时去查资料。 Read Scratchpad(读暂存寄存器):0xBE 这里要注意的是,我们的DS18B20的温度数据是2个字节,我们读取数据的时候,先读取到的是低字节的低位,读完了第一个字节后,再读高字节的低位,一直到两个字节全部读取完毕。 Convert Temperature(启动温度转换):0x44 当我们发送一个启动温度转换的指令后,DS18B20开始进行转换。从转换开始到获取温度,DS18B20是需要时间的,而这个时间长短取决于DS18B20的精度。前边说DS18B20最高可以用12位来存储温度,但是也可以用11位,10位和9位一共四种格式。位数越高,精度越高,9位模式最低位变化1温度变化0.5度,同时转换速度也要快一些,如图16-15所示。
图16-15 DS18B20温度转换时间 其中寄存器R1和R0决定了转换的位数,出场默认值就是11,也就是12位表示温度,最大的转换时间是750ms。当启动转换后,至少要再等750ms之后才能读取温度,否则读到的温度有可能是错误的值。这就是为什么很多同学读DS18B20的时候,第一次读出来的是85度,这个值要么是没有启动转换,要么是启动转换了,但还没有等待一次转换彻底完成,读到的是一个错误的数据。 4、DS18B20的位读写时序。 DS18B20的时序图不是很好理解,大家对照时序图,结合我的解释学明白。写时序图如图16-16所示。
图16-16 DS18B20位写入时序 当要给DS18B20写入‘0’的时候,单片机直接将引脚拉低,持续时间大于60us小于120us就可以了。图上显示的意思是,单片机先拉低15us之后,DS18B20会在从15us到60us之间的时间来读取这一位,DS18B20最早会15us的时刻读取,典型值是30us的时刻读取,最多不会超过60us,DS18B20必然读取完毕,所以持续时间超过60us即可。 当要给DS18B20写入‘1’的时候,单片机先将这个引脚拉低,拉低时间大于1us,然后马上释放总线,即拉高引脚,并且持续时间也要大于60us。和写‘0’类似的是,DS18B20会在15到60us之间来读取这个‘1’。 可以看出来,DS18B20的时序比较严格,写的过程中最好不要有中断打断,但是在两个“位”之间的间隔,是大于1小于无穷的,那在这个时间段,我们是可以开中断来处理其他程序的。发送一个字节的数据程序如下。 void Write18B20(unsigned char dat) //向DS18B20写入一个字节数据 { unsigned char mask; EA = 0; //禁止总中断 for (mask=0x01; mask!=0; mask<<=1) //低位在先,依次移出8个bit { IO_18B20 = 0; //产生2us低电平脉冲 _nop_(); _nop_(); if ((mask&dat) == 0) //输出该bit值 IO_18B20 = 0; else IO_18B20 = 1; DelayX10us(6); //延时60us IO_18B20 = 1; //拉高通信引脚 } EA = 1; //重新使能总中断 } 读时序图如图16-17所示。
图16-17 DS18B20位读取时序 当要读取DS18B20的数据的时候,我们的单片机首先要拉低这个引脚,并且至少保持1us的时间,然后释放引脚,释放完毕后要尽快读取。从拉低这个引脚到读取引脚状态,不能超过15us。大家从图16-17可以看出来,主机采样时间,也就是MASTER SAMPLES,是在15us之内必须完成的,读取一个字节数据的程序如下。 unsigned char Read18B20(void) //从DS18B20读取一个字节数据 { unsigned char dat; unsigned char mask; EA = 0; //禁止总中断 for (mask=0x01; mask!=0; mask<<=1) //低位在先,依次采集8个bit { IO_18B20 = 0; //产生2us低电平脉冲 _nop_(); _nop_(); IO_18B20 = 1; //结束低电平脉冲,等待18B20输出数据 _nop_(); //延时2us _nop_(); if (!IO_18B20) //读取通信引脚上的值 dat &= ~mask; else dat |= mask; DelayX10us(6); //再延时60us } EA = 1; //重新使能总中断 return dat; } DS18B20所表示的温度值中,有小数和整数两部分。常用的带小数的数据处理方法有两种,一种是定义成浮点型直接小数整数处理,第二种是定义成整型,然后把小数和整数部分分离出来,在合适的位置点上小数点即可。我们在程序中使用的是第二种方法,下面我们就写一个程序,将我们读到的温度值显示在1602液晶上,并且保留一位小数数字。 /***********************lcd1602.c文件程序源代码*************************/ #include <reg52.h> #define LCD1602_DB P0 sbit LCD1602_RS = P1^0; sbit LCD1602_RW = P1^1; sbit LCD1602_E = P1^5; void LcdWaitReady() //等待液晶准备好 { unsigned char sta; LCD1602_DB = 0xFF; LCD1602_RS = 0; LCD1602_RW = 1; do { LCD1602_E = 1; sta = LCD1602_DB; //读取状态字 LCD1602_E = 0; } while (sta & 0x80); //bit7等于1表示液晶正忙,重复检测直到其等于0为止 } void LcdWriteCmd(unsigned char cmd) //写入命令函数 { LcdWaitReady(); LCD1602_RS = 0; LCD1602_RW = 0; LCD1602_DB = cmd; LCD1602_E = 1; LCD1602_E = 0; } void LcdWriteDat(unsigned char dat) //写入数据函数 { LcdWaitReady(); LCD1602_RS = 1; LCD1602_RW = 0; LCD1602_DB = dat; LCD1602_E = 1; LCD1602_E = 0; } void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str) //显示字符串,屏幕起始坐标(x,y),字符串指针str { unsigned char addr; //由输入的显示坐标计算显示RAM的地址 if (y == 0) addr = 0x00 + x; //第一行字符地址从0x00起始 else addr = 0x40 + x; //第二行字符地址从0x40起始 //由起始显示RAM地址连续写入字符串 LcdWriteCmd(addr | 0x80); //写入起始地址 while (*str != '\0') //连续写入字符串数据,直到检测到结束符 { LcdWriteDat(*str); str++; } } void LcdInit() //液晶初始化函数 { LcdWriteCmd(0x38); //16*2显示,5*7点阵,8位数据接口 LcdWriteCmd(0x0C); //显示器开,光标关闭 LcdWriteCmd(0x06); //文字不动,地址自动+1 LcdWriteCmd(0x01); //清屏 } /***********************DS18B20.c文件程序源代码*************************/ #include <reg52.h> #include <intrins.h> sbit IO_18B20 = P3^2; //DS18B20通信引脚 void DelayX10us(unsigned char t) //软件延时函数,延时时间(t*10)us { do { _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); } while (--t); } bit Get18B20Ack(void) //复位总线,获取存在脉冲,以启动一次读写操作 { bit ack; EA = 0; //禁止总中断 IO_18B20 = 0; //产生500us复位脉冲 DelayX10us(50); IO_18B20 = 1; DelayX10us(6); //延时60us ack = IO_18B20; //读取存在脉冲 while(!IO_18B20); //等待存在脉冲结束 EA = 1; //重新使能总中断 return ack; } void Write18B20(unsigned char dat) //向DS18B20写入一个字节数据 { unsigned char mask; EA = 0; //禁止总中断 for (mask=0x01; mask!=0; mask<<=1) //低位在先,依次移出8个bit { IO_18B20 = 0; //产生2us低电平脉冲 _nop_(); _nop_(); if ((mask&dat) == 0) //输出该bit值 IO_18B20 = 0; else IO_18B20 = 1; DelayX10us(6); //延时60us IO_18B20 = 1; //拉高通信引脚 } EA = 1; //重新使能总中断 } unsigned char Read18B20(void) //从DS18B20读取一个字节数据 { unsigned char dat; unsigned char mask; EA = 0; //禁止总中断 for (mask=0x01; mask!=0; mask<<=1) //低位在先,依次采集8个bit { IO_18B20 = 0; //产生2us低电平脉冲 _nop_(); _nop_(); IO_18B20 = 1; //结束低电平脉冲,等待18B20输出数据 _nop_(); //延时2us _nop_(); if (!IO_18B20) //读取通信引脚上的值 dat &= ~mask; else dat |= mask; DelayX10us(6); //再延时60us } EA = 1; //重新使能总中断 return dat; } bit Start18B20() //启动一次18B20温度转换,返回值代表是否启动成功 { bit ack; ack = Get18B20Ack(); //执行总线复位,并获取18B20应答 if (ack == 0) //如18B20正确应答,则启动一次转换 { Write18B20(0xCC); //跳过ROM操作 Write18B20(0x44); //启动一次温度转换 } return ~ack; //ack==0表示操作成功,所以返回值为其取反值 } bit Get18B20Temp(int *temp) //读取DS18B20温度值,返回值代表是否读取成功 { bit ack; unsigned char LSB, MSB; //16bit温度值的低字节和高字节 ack = Get18B20Ack(); //执行总线复位,并获取18B20应答 if (ack == 0) //如18B20正确应答,则读取温度值 { Write18B20(0xCC); //跳过ROM操作 Write18B20(0xBE); //发送读命令 LSB = Read18B20(); //读温度值的低字节 MSB = Read18B20(); //读温度值的高字节 *temp = ((int)MSB << 8) + LSB; //合成为16bit整型数 } return ~ack; //ack==0表示操作应答,所以返回值为其取反值 } /***********************main.c文件程序源代码*************************/ #include <reg52.h> bit flag1s = 0; //1s定时标志 unsigned char T0RH = 0; //T0重载值的高字节 unsigned char T0RL = 0; //T0重载值的低字节 void ConfigTimer0(unsigned int ms); unsigned char IntToString(unsigned char *str, int dat); extern bit Start18B20(); extern bit Get18B20Temp(int *temp); extern void LcdInit(); extern void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str); void main () { bit res; int temp; //读取到的当前温度值 int intT, decT; //温度值的整数和小数部分 unsigned char len; unsigned char str[12]; LcdInit(); //初始化液晶 Start18B20(); //启动DS18B20 ConfigTimer0(10); //T0定时10ms EA = 1; //开总中断 while(1) { if (flag1s) //每秒更新一次温度 { flag1s = 0; res = Get18B20Temp(&temp); //读取当前温度 if (res) //读取成功时,刷新当前温度显示 { intT = temp >> 4; //分离出温度值整数部分 decT = temp & 0xF; //分离出温度值小数部分 len = IntToString(str, intT); //整数部分转换为字符串 str[len++] = '.'; //添加小数点 decT = (decT*10) / 16; //二进制的小数部分转换为1位十进制位 str[len++] = decT + '0'; //十进制小数位再转换为ASCII字符 while (len < 6) //用空格补齐到6个字符长度 { str[len++] = ' '; } str[len] = '\0'; //添加字符串结束符 LcdShowStr(0, 0, str); //显示到液晶屏上 } else //读取失败时,提示错误信息 { LcdShowStr(0, 0, "error!"); } Start18B20(); //重新启动下一次转换 } } } unsigned char IntToString(unsigned char *str, int dat) //整型数转换为十进制字符串,返回值为转换后的字符串长度 { signed char i; unsigned char len = 0; unsigned char buf[6]; if (dat < 0) //如果为负数,首先取绝对值,并添加负号 { dat = -dat; *str++ = '-'; len++; } for (i=0; i<=4; i++) //由低到高转换为十进制位 { buf[ i] = dat % 10; dat /= 10; } for (i=4; i>=1; i--) //查找有效数字最高位,以忽略更高位的‘0’ { if (buf[ i] != 0) { break; } } for ( ; i>=0; i--) //有效数字位转换为ASCII码 { *str++ = buf[ i] + '0'; len++; } *str = '\0'; //添加字符串结束符 return len; //返回字符串长度 } void ConfigTimer0(unsigned int ms) //T0配置函数 { unsigned long tmp; tmp = 11059200 / 12; //定时器计数频率 tmp = (tmp * ms) / 1000; //计算所需的计数值 tmp = 65536 - tmp; //计算定时器重载值 tmp = tmp + 12; //修正中断响应延时造成的误差 T0RH = (unsigned char)(tmp >> 8); //定时器重载值拆分为高低字节 T0RL = (unsigned char)tmp; TMOD &= 0xF0; //清零T0的控制位 TMOD |= 0x01; //配置T0为模式1 TH0 = T0RH; //加载T0重载值 TL0 = T0RL; ET0 = 1; //使能T0中断 TR0 = 1; //启动T0 } void InterruptTimer0() interrupt 1 //T0中断服务函数 { static unsigned char tmr1s = 0; TH0 = T0RH; //定时器重新加载重载值 TL0 = T0RL; tmr1s++; if (tmr1s >= 100) //定时1s { tmr1s = 0; flag1s = 1; } } 16.5 作业1、理解红外通信调制解调的原理,掌握NEC红外通信编码的原理。 2、将显示跳线帽调到左侧控制步进电机,使用红外遥控器控制电机的正反转。 3、掌握DS18B20的时序过程,能够理解每一位读写的时序。 4、结合DS1302的可调万年历程序,将温度显示加入进去,做一个万年历加温度显示,并且实现按键可调时间,按键可调温度报警值,当温度超过一预定值,蜂鸣器报警。
上一章:第15章 实时时钟DS1302
|