【51单片机学习】AT24C02(I2C)、DS18B20(单总线)、LCD1602(液晶显示屏)
一、AT24C02(I2C)
1.存储器介绍
RAM:Random Access Member随机存储器,存储速度快,但是掉电丢失。
SRAM内部存储的结构是锁存器,相当于数电中的D触发器,利用电路来存储数据,SRAM是所以存储器中存储速度最快的存储器,一般用于电脑CPU的高速缓存,特殊功能寄存器也是一种SRAM。速度最快,但是容量较小且成本较高。
DRAM通过电容的充放电实现存储数据的功能,电容充满电后表示为高电平,放完电后表示为低电平,但是因为电容的集成度很高,容值较小,当然存在漏电现象,所以电容在存储数据之后很容易漏电数据丢失,所以需要配置一个扫描电路,每隔一定时间进行数据扫描并给电容充电,弥补上因为漏电现象所丢失的电能。容量更大且成本更低。一般用于电脑中的内存条、手机中的运行内存等。
ROM:Random Only Member只读存储器,掉电不丢失,但是存储速度慢。
Mask ROM仅仅依靠电路进行数据存储,读取数据后不能更改,可以保证数据掉电不丢失。
PROM只能写入一次数据。
EPROM如果想要擦除数据,需要紫外线照射30min,此类芯片中间挖了个洞,上面覆盖一层黑纸,打开之后是直接裸露的芯片,所以需要用紫外线照射进行数据擦除。
E2PROM在5V低压电的情况下几毫秒即可完成擦除,但是容量比较小。
Flash应用广泛,例如单片机的程序存储器、U盘、内存卡、电脑的固态硬盘、手机的存储等。
2.存储器简化模型
横向为地址总线,纵向为数据总线。地址总线一次只能选中一行,所以一般在地址总线之前通常加一个译码器,可以把多位数据转变为只有一位被选中的输出端,在译码器之前输入实际地址。
写入数据通过二极管击穿实现。网格交错的地方时不连接的,交错点不能直接相连,会相互干扰。
Mask ROM在交错点处接入二极管,防止出现相互干扰。PROM在交错点处接入两个二极管,使其处于断路的状态,其中蓝色的二极管容易被击穿,击穿后交错点短路,二极管智能杯击穿一次,所以只能编程一次。同理,可用熔丝代替蓝色二极管,未烧断时是短路,加入大电流烧断后是断路,也可以控制编程。
3.AT24C02介绍
4.引脚及应用电路
因为这里用不到写保护,所以引脚7直接接地了,如果需要的话可以接到I/O口或者开关上。
VDD相当于VCC,VSS相当于GND。这里的引脚7是写使能,低电平使能,和写保护是同一个意思。
5.内部结构框图
EEPROM中是网格,DEC是译码器,SERIAL MUX是串行数据选择端。通过Y译码器把数据一位一位的通过逻辑进行输出。DATA RECOVERY是数据恢复,负责数据擦除。DATA WORD ADDR/COUNTER是数据字地址寄存器,用来设置地址,里面的寄存器时用来存储地址的,每写入或读出一个数据,寄存器会自动加一,如果读出不指定地址,会默认把寄存器的数据取出。
对于左上角的I2C部分,主要有开始结束逻辑、器件地址比较器、串行控制逻辑。主要作用是将I2C的数据拿进来,通过逻辑执行操作。
6.I2C总线介绍
同步:有同步的时钟线SCL。
半双工:SDA只有一根线,还要负责来回通信,只能分时复用同一根线。
数据应答:在发送一个字节数据之后,要求对方给出应答,用来判断是否发送成功。
下面三个实物图时常用的I2C的设备:
- 0.96寸的OLED屏幕:屏幕小且像素密度高,128×64的像素点。
- DS3231:是一种时钟芯片,这种时钟芯片比DS1302的精度高,但是比较贵。
- MPU6050:陀螺仪传感器,常用于平衡车、无人机等。
7.I2C电路规范
I2C是一种总线结构,在两根线上可以挂接很多设备。
单片机的I/O口是一种弱上拉模式,P0口其实是一种开漏模式,但是它的外部接了上拉电阻,所以看作弱上拉模式。高电平驱动能力弱,低电平驱动能力强。
开漏模式的开关断开时,引脚处于浮空状态,实际上是断开的状态,此时的电平是不稳定的,引脚的电平极易受到外界的干扰。
对于上拉电阻的阻值一般为kΩ级别的,I2C标准的协议中有规定什么速率下接多大的电阻最好。
CPU若想和其中一个设备通信的话,最好的状态是与其他设备断开,输出高电平时相当于引脚断开,全部输出高电平1(因为高电平1没有驱动能力),但是这样就不能输出高电平了,所以用两个电阻R充当上拉电阻,如果CPU想发送1时,就会通过上拉电阻自动拉到高电平。
右图中,SCLK IN相当于一个内部缓存,可以看作是电压表,可以监测SCLK处的电压,因为其输入阻抗很大,可以看作是开路,SCLKN1 OUT可以看作是电子开关,低电平导通,高电平断开。给高电平时相当于SCLK处完全断开,不会干扰外界。
I2C只能控制发送0,自动变为1,跟弱上拉类似。
弱上拉输出是控制1发送,非1即0;开漏输出是控制0发送,非0即1。
8.I2C时序结构
处于空闲状态时,SCL和SDA均为高电平。
通俗解释:SCL车,SDA人,出发时,人先上车,车载启动;停下时,车先停,人再下。
SDA的高低电平是不确定的,数据在交点处改变。红框内的过程执行一次发送一位,该过程循环八次即可完成字节发送。我们一般只编程主机的代码,从机自动监测SCL和SDA,不需要人为干涉,只需要人为的将主机的时序模拟出来。
SDA置1相当于释放,释放时主机完全不干预数据线,也就是主机释放数据线,将数据线的控制权交给从机。
下图中,SDA的紫色线表示从机控制数据线的时候。
应答可以作为发送数据的第9位,这一味地传输方向是相反的,是为了判断从机是否应答或者主机是否发送应答。
接收应答:主机发送数据给从机,从机在接收到数据后应答,主机接收从机的应答。
都是对主机而言的,发送应答指的是主机接收从机数据,然后主机给从机发送应答。
9.I2C数据帧
I2C规定,在起始条件之后,第一个字节数据一定要先发送程序地址+读写位,这里地址的前四位A6~A3是固定的,AT24C02的固定位1010,其他的芯片不同。后三位A2~A0是可配置的。每发送一个字节都要跟一个接收应答。
接收一帧数据与与发送一帧数据类似,需要注意的是,第一个应答为接收应答,因为不管开始地址那是写还是读都是由主机发出的,所以第一个地址接收需要从机做应答,就是RA。
发送和接收的主体都是主机,发送应答是从机的任务,接收应答是主机的任务。
发送数据和接受数据是对主机而言的(主体是主机),发送应答和接收应答是对从机而言的(主体是从机)。
读完数据发送应答,写完数据接收应答。
10.AT24C02数据帧
11.代码示例
(1) AT24C02数据存储
需要提前加入Delay.c文件、Delay.h文件、Key.c文件、Key.h文件、LCD1602.c文件、LCD1602.h文件。
在开始时,SCL和SDA一般都处于空闲状态,也就是高电平,但是有时候S是在RA后面添加的,为了保证连贯性,需要将SDA手动置1。默认开局全高,然后SDA创造下降沿,SCL再变低为了检测时的上拉。
停止之前,SDA可能是0也可能是1,所以,在停止之前需要保证SDA=0,而无论发送还是接收或者应答结束时SCL都会变0,所以拼接时候已经为0。
在使用不同的单片机时,一定要考虑单片机所能承受的最快频率、最小时间是多少,如果速度怪快的话,需要在程序里面的每一行之后插入一个Delay,进行一定时间的延时。
因为写周期是5ms,所以每次写入数据之后需要Delay5ms,否则会出错。连续读多个字节不用Delay,但是每次写完之后必须Delay。
main.c文件
#include <REGX52.H>
#include "LCD1602.h"
#include "Key.h"
#include "AT24C02.h"
#include "Delay.h"unsigned char KeyNum;
unsigned int Num;void main()
{LCD_Init();LCD_ShowNum(1,1,Num,5);while(1){KeyNum=Key();if(KeyNum==1) //K1按键,Num自增{Num++;LCD_ShowNum(1,1,Num,5);}if(KeyNum==2) //K2按键,Num自减{Num--;LCD_ShowNum(1,1,Num,5);}if(KeyNum==3) //K3按键,向AT24C02写入数据{AT24C02_WriteByte(0,Num%256); //Num是16位数据,取低八位Delay(5);AT24C02_WriteByte(1,Num/256); //取高八位Delay(5);LCD_ShowString(2,1,"Write OK");Delay(1000);LCD_ShowString(2,1," ");}if(KeyNum==4) //K4按键,从AT24C02读取数据{Num=AT24C02_ReadByte(0); //读取低八位数据Num|=AT24C02_ReadByte(1)<<8; //读取高八位数据LCD_ShowNum(1,1,Num,5);LCD_ShowString(2,1,"Read OK ");Delay(1000);LCD_ShowString(2,1," ");}}
}
I2C.c文件
#include <REGX52.H>//引脚定义
sbit I2C_SCL=P2^1;
sbit I2C_SDA=P2^0;/*** @brief I2C开始* @param 无* @retval 无*/
void I2C_Start(void)
{I2C_SDA=1; //保证SDA为高电平I2C_SCL=1;I2C_SDA=0;I2C_SCL=0;
}/*** @brief I2C停止* @param 无* @retval 无*/
void I2C_Stop(void)
{I2C_SDA=0; //保证SDA为低电平I2C_SCL=1;I2C_SDA=1;
}/*** @brief I2C发送一个字节* @param Byte 要发送的字节* @retval 无*/
void I2C_SendByte(unsigned char Byte)
{unsigned char i;for(i=0;i<8;i++){I2C_SDA=Byte&(0x80>>i); //依次取出数据,先取最高位I2C_SCL=1; //此处拉高后立马拉低影响不大I2C_SCL=0;}
}/*** @brief I2C接收一个字节* @param 无* @retval 接收到的一个字节数据*/
unsigned char I2C_ReceiveByte(void)
{unsigned char i,Byte=0x00;I2C_SDA=1; //释放总线for(i=0;i<8;i++) //这里已经把控制权交给从机了,所以SDA的值不是1而是SDA里面存储的值{I2C_SCL=1;if(I2C_SDA){Byte|=(0x80>>i);} //读取数据I2C_SCL=0;}return Byte;
}/*** @brief I2C发送应答* @param AckBit 应答位,0为应答,1为非应答* @retval 无*/
void I2C_SendAck(unsigned char AckBit)
{I2C_SDA=AckBit;I2C_SCL=1;I2C_SCL=0; //scl=1会提醒从机(此刻是释放了总线的主机)开始读取数据,scl=0时读取完毕,准备下一次读取
}/*** @brief I2C接收应答位* @param 无* @retval 接收到的应答位,0为应答,1为非应答*/
unsigned char I2C_ReceiveAck(void)
{unsigned char AckBit;I2C_SDA=1;I2C_SCL=1;AckBit=I2C_SDA;I2C_SCL=0;return AckBit;
}
I2C.h文件
#ifndef __I2C_H__
#define __I2C_H__void I2C_Start(void);
void I2C_Stop(void);
void I2C_SendByte(unsigned char Byte);
unsigned char I2C_ReceiveByte(void);
void I2C_SendAck(unsigned char AckBit);
unsigned char I2C_ReceiveAck(void);#endif
AT24C02.c文件
#include <REGX52.H>
#include "I2C.h"#define AT24C02_ADDRESS 0xA0/*** @brief AT24C02写入一个字节* @param WordAddress 要写入字节的地址* @param Data 要写入的数据* @retval 无*/
void AT24C02_WriteByte(unsigned char WordAddress,Data)
{I2C_Start();I2C_SendByte(AT24C02_ADDRESS);I2C_ReceiveAck();I2C_SendByte(WordAddress);I2C_ReceiveAck();I2C_SendByte(Data);I2C_ReceiveAck();I2C_Stop();
}/*** @brief AT24C02读取一个字节* @param WordAddress 要读出字节的地址* @retval 读出的数据*/
unsigned char AT24C02_ReadByte(unsigned char WordAddress)
{unsigned char Data;I2C_Start();I2C_SendByte(AT24C02_ADDRESS);I2C_ReceiveAck();I2C_SendByte(WordAddress);I2C_ReceiveAck();I2C_Start();I2C_SendByte(AT24C02_ADDRESS|0x01); //将地址变为读地址0x81I2C_ReceiveAck();Data=I2C_ReceiveByte();I2C_SendAck(1);I2C_Stop();return Data;
}
AT24C02.h文件
#ifndef __AT24C02_H__
#define __AT24C02_H__void AT24C02_WriteByte(unsigned char WordAddress,Data);
unsigned char AT24C02_ReadByte(unsigned char WordAddress);#endif
(2)秒表(定时器扫描按键数码管)
需要提前加入Delay.c文件、Delay.h文件、Key.c文件、Key.h文件、Nixie.c文件、Nixie.h文件、Timer0.c文件、Timer0.h文件、I2C.c文件、I2C.h文件、AT24C02.c文件、AT24C02.h文件。
这里需要学习的新思路是定时器复用,当多个函数都需要用到定时器时,可以在函数中再定义一个新的函数,然后在main.c文件中每隔一段时间进行调用。
main.c文件
#include <REGX52.H>
#include "Timer0.h"
#include "Key.h"
#include "Nixie.h"
#include "Delay.h"
#include "AT24C02.h"unsigned char KeyNum;
unsigned char Min,Sec,MiniSec;
unsigned char RunFlag;void main()
{Timer0_Init();while(1){KeyNum=Key();if(KeyNum==1) //K1按键按下{RunFlag=!RunFlag; //启动标志位翻转}if(KeyNum==2) //K2按键按下{Min=0; //分秒清0Sec=0;MiniSec=0;}if(KeyNum==3) //K3按键按下{AT24C02_WriteByte(0,Min); //将分秒写入AT24C02Delay(5);AT24C02_WriteByte(1,Sec);Delay(5);AT24C02_WriteByte(2,MiniSec);Delay(5);}if(KeyNum==4) //K4按键按下{Min=AT24C02_ReadByte(0); //读出AT24C02数据Sec=AT24C02_ReadByte(1);MiniSec=AT24C02_ReadByte(2);}Nixie_SetBuf(1,Min/10); //设置显示缓存,显示数据Nixie_SetBuf(2,Min%10);Nixie_SetBuf(3,11);Nixie_SetBuf(4,Sec/10);Nixie_SetBuf(5,Sec%10);Nixie_SetBuf(6,11);Nixie_SetBuf(7,MiniSec/10);Nixie_SetBuf(8,MiniSec%10);}
}/*** @brief 秒表驱动函数,在中断中调用* @param 无* @retval 无*/
void Sec_Loop(void)
{if(RunFlag){MiniSec++;if(MiniSec>=100){MiniSec=0;Sec++;if(Sec>=60){Sec=0;Min++;if(Min>=60){Min=0;}}}}
}void Timer0_Routine() interrupt 1
{static unsigned int T0Count1,T0Count2,T0Count3;TL0 = 0x18; //设置定时初值TH0 = 0xFC; //设置定时初值T0Count1++;if(T0Count1>=20){T0Count1=0;Key_Loop(); //20ms调用一次按键驱动函数}T0Count2++;if(T0Count2>=2){T0Count2=0;Nixie_Loop();//2ms调用一次数码管驱动函数}T0Count3++;if(T0Count3>=10){T0Count3=0;Sec_Loop(); //10ms调用一次数秒表驱动函数}
}
Key.c文件
#include <REGX52.H>
#include "Delay.h"unsigned char Key_KeyNumber;/*** @brief 获取按键键码* @param 无* @retval 按下按键的键码,范围:0,1~4,0表示无按键按下*/
unsigned char Key(void)
{unsigned char Temp=0;Temp=Key_KeyNumber; //先清零,再返回Key_KeyNumber=0;return Temp;
}/*** @brief 获取当前按键的状态,无消抖及松手检测* @param 无* @retval 按下按键的键码,范围:0,1~4,0表示无按键按下*/
unsigned char Key_GetState()
{unsigned char KeyNumber=0;if(P3_1==0){KeyNumber=1;}if(P3_0==0){KeyNumber=2;}if(P3_2==0){KeyNumber=3;}if(P3_3==0){KeyNumber=4;}return KeyNumber;
}/*** @brief 按键驱动函数,在中断中调用* @param 无* @retval 无*/
void Key_Loop(void)
{static unsigned char NowState,LastState;LastState=NowState; //按键状态更新NowState=Key_GetState(); //获取当前按键状态//如果上个时间点按键按下,这个时间点未按下,则是松手瞬间,以此避免消抖和松手检测if(LastState==1 && NowState==0){Key_KeyNumber=1;}if(LastState==2 && NowState==0){Key_KeyNumber=2;}if(LastState==3 && NowState==0){Key_KeyNumber=3;}if(LastState==4 && NowState==0){Key_KeyNumber=4;}
}
Key.h文件
#ifndef __KEY_H__
#define __KEY_H__unsigned char Key(void);
void Key_Loop(void);#endif
Nixie.c文件
#include <REGX52.H>
#include "Delay.h"//数码管显示缓存区
unsigned char Nixie_Buf[9]={0,10,10,10,10,10,10,10,10};//数码管段码表
unsigned char NixieTable[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x00,0x40};/*** @brief 设置显示缓存区* @param Location 要设置的位置,范围:1~8* @param Number 要设置的数字,范围:段码表索引范围* @retval 无*/
void Nixie_SetBuf(unsigned char Location,Number)
{Nixie_Buf[Location]=Number;
}/*** @brief 数码管扫描显示* @param Location 要显示的位置,范围:1~8* @param Number 要显示的数字,范围:段码表索引范围* @retval 无*/
void Nixie_Scan(unsigned char Location,Number)
{P0=0x00; //段码清0,消影switch(Location) //位码输出{case 1:P2_4=1;P2_3=1;P2_2=1;break;case 2:P2_4=1;P2_3=1;P2_2=0;break;case 3:P2_4=1;P2_3=0;P2_2=1;break;case 4:P2_4=1;P2_3=0;P2_2=0;break;case 5:P2_4=0;P2_3=1;P2_2=1;break;case 6:P2_4=0;P2_3=1;P2_2=0;break;case 7:P2_4=0;P2_3=0;P2_2=1;break;case 8:P2_4=0;P2_3=0;P2_2=0;break;}P0=NixieTable[Number]; //段码输出
}/*** @brief 数码管驱动函数,在中断中调用* @param 无* @retval 无*/
void Nixie_Loop(void)
{static unsigned char i=1;Nixie_Scan(i,Nixie_Buf[i]);i++;if(i>=9){i=1;}
}
Nixie.h文件
#ifndef __NIXIE_H__
#define __NIXIE_H__void Nixie_SetBuf(unsigned char Location,Number);
void Nixie_Scan(unsigned char Location,Number);
void Nixie_Loop(void);#endif
二、DS18B20(单总线)
1.DS18B20介绍
常见的模拟温度传感器有热敏电阻,可以直接测温度,输出电压随着温度变化。但是模拟传感器应用时比较复杂,需要外部分压电路通过AD转换才能读取温度,最终读取到的数据只是正比于温度,还需要对数据进行一定的系数配比才能测出真正的温度。
数字温度传感器中集成了模拟温度传感器和微控制器,在内部读取模拟温度传感器的数据,通过内部电路将电路存储到内部芯片的RAM中,只需要通过引脚和单总线通信协议把温度转化读取出来即可,整个转化过程都是数字量进行输入输出,不涉及AD转换,操作简单。
2.引脚及应用电路
这里的上拉电阻与I2C中的上拉电阻功能相同,为了实现总线操作。
3.内部结构框图
寄生供电可以省去VDD,电源正极由DQ提供,电容Cpp相当于电池,在DQ处于高电平时,给Cpp充电,以此来弥补低电平时没有电的情况,在进行一些比较耗电的操作时,还需要给DQ一个强上拉,因为4.7kΩ上拉电阻比较弱,通过的电流比较小。电源供给感应可以感应VDD是否存在。寄生供电电路是自动运行的,不需要对程序进行任何的配置。
温度传感器,相当于内部的模拟温度传感器,自动处理好温度转换,将数据存入RAM中。
报警高触发寄存器,用来存储温度上限阈值,用于温度报警。
报警低触发寄存器,用来存储温度下限阈值。
配置寄存器,用来设置分辨率,设置精度,出厂默认的最高分辨率是0.0625,通过配置可以降低精度,分辨率最低是0.5℃,分辨率降低温度转换速率就会迅速提升。
8位CRC生成器,CRC是一种校验码的算法,将RAM之前的数据进行校验,得到一个校验码,用于判断数据是否正确。
4.存储器结构
一共9个字节,由Byte0和Byte1共同组成温度的数据,上电默认85℃。
Byte2~Byte4,先将数据写到暂存器中,再将数据复制到EEPROM中,上电时会将EEPROM中的数据搬到RAM中。
Byte5~Byte7,是保留位,暂时未使用。
Byte8,CRC校验。
5.单总线介绍
异步:没有单独的时钟线;半双工:只有一根数据线,在这一根数据线上进行数据的发送和接收。
单总线的目的是为了节省通信线。
下图中分别是DS18B20和DHT11,DHT11是一种温湿度传感器。
6.单总线电路规范
前两条与I2C相同,目的是为了实现多机通信。想输出0就把总线拉下来,想输出1就释放总线。挂载多个设备时,这样可以使相互干扰达到最小。
采取寄生供电时需要将VDD接到GND上,整个设备只需要DQ和GND两根线。
强上拉电路由MOS管构成,MOS管做为电子开关,左侧接低电平时开关闭合,DQ直接接到VCC上。
7.单总线时序结构
总线的空闲状态是高电平,释放总线后,电平会由电阻将其拉至高电平。
初始化可以分为两个部分:复位和响应。
时间片是时序结构的意思。
下图中实际上有两个时序结构,一个流程下来实际上发送了两位,左侧是发送0的时序,右侧是发送1的时序。两个时序结构之间有一个总线恢复时间,也就是说连续发送两位之间时间间隔不能太短,间隔大于1us即可。右侧中的阴影代表总线可以在这一段时间内释放。
与发送一位类似。现在接收一位是相对的,这个单总线上一共有两位机器(假设),有一个主机和一个从机,此时从机想要和主机发消息,则主机线把总线拉低15us左右来接收从机发消息,此时就算是主机将总线释放,总线也无法复位,因为现在从机正在发消息,从机将总线拉低了。
阴影表示在该时间段内随时可能释放总线。
每次初始化之后,总线的控制权都是由主机掌握的,总线拉低时,默认是写(发送)的状态,通过写状态发送指令,变为读状态,此时从机获得总线的控制权,主机转化为接收的状态。
连续调用8次发送一个字节,就可以发送一位。
【注意】单总线发送一个字节时,低位在前;I2C总线发送一个字节时,高位在前。
8.DS18B20操作流程
SEARCH ROM:搜寻ROM,操作流程较复杂,此处不涉及。
READ ROM:读ROM,调用该指令再调用读时序,就可以把ROM读出来,因为只有一个设备,直接读取即可。
MATCH ROM:匹配ROM,发送完该指令之后,发送设备地址,就可以匹配,和某一个设备进行单独通信。
SKIP ROM:跳过ROM,因为只有一个设备,所以可以直接跳过ROM进行设备寻址,当总线上连接多个设备时,不可以使用跳过ROM。
ALARM SEARCH:报警搜索,在有多个设备连接时使用,温度有上下限阈值,如果某个设备处于报警状态,可以通过报警搜索获取到那个设备会报警,操作流程较复杂,此处不涉及。
CONVERT T:温度变换,在读取温度之前,需要先进行温度变换,当执行完ROM指令之后,如果发送该条指令,就会启动温度变换,把温度传感器中的熟知度取出来放到暂存器RAM中,相当于暂存器温度值更新。
WRITE SCRATCHPAD:写暂存器,调用该条指令之后再调用写时序,就会把字节写入Byte2~Byte4这三个字节中。
READ SCRATCHPAD:读寄存器,调用该条指令之后再调用读时序,DS18B20就会依次把暂存器的内容读取出来,最终读取出CRC。此处只需要读取前两个温度字节即可获取到温度。
COPY SCRATCHPAD:复制暂存器,当从机接收到这条指令时,会将RAM中Byte2~Byte4这三个字节写入EEPROM中。
RECALL E2:发送完这条指令,从机就会把EEPROM的三个字节复制到暂存器RAM的Byte2~Byte4这三个字节中。
READ POWER SUPPLY:读取设备供电模式,发送完这条指令,会跟着读取一位的时序,响应是寄生供电还是独立供电。
9.DS18B20数据帧
跳过ROM之后不需要其他的指令,不需要发送和接受,直接再发送时序即可。
10.温度存储格式
由高8位和低8位组成16位数据,前5位 BIT15~BIT11 是符号位,如果温度是负的,则符号位均为1,如果温度是正的,则符号位均为0,最后4位 BIT3~BIT0 是小数位,BIT3如果是1,就代表0.5,BIT2如果是1,就代表0.25,BIT1如果是1,就代表0.125,BIT0如果是1,就代表0.0625。BIT10~BIT4 是数据位,代表温度的整数部分,它的存储格式实际上是以二进制的补码形式来存储的。负数的补码需要取反加一,符号位不变,得到的是负数的原码。除了符号位是负数,其他和正数一样。
11.代码示例
(1)DS18B20温度读取
需要提前加入Delay.c文件、Delay.h文件、LCD1602.c文件、LCD1602.h文件。
Delay函数是有一定误差的,while循环进行跳转的时候也是需要耗时的,由于延时的函数体需要的时间比较长,所以跳转的时间可以忽略不计,此时Delay的时间比较准确。
但是如果想要写一个Delay微秒的函数时,跳转和判断指令的时间就不能忽略了,甚至看会比延时的函数体时间还长,所以不能写Delay微秒的函数。
利用STC_ISP直接生成480us的延时,因为要求是至少480us,保险起见可以设置为500us。
函数名+函数体 总共500us,函数名4us,函数体496us
无符号数转换为有符号数,实际内容是不会变化的,因为无符号数字的字节本身存的就是二进制补码,包含符号位,所以拼接为16位之后,赋值给一个有符号的int型,所以int可以表示正负,而且补码形式都是对应的,因为小数部分的存在,相当于温度值向左移动了4位,扩大了16倍,为了将其转化为实际温度,需要除以16,因为整数除以16会忽略掉小数,为了不损失精度,所以除以16.0。
main.c文件
#include <REGX52.H>
#include "LCD1602.h"
#include "DS18B20.h"
#include "Delay.h"float T;void main()
{DS18B20_ConvertT(); //上电先转换一次温度,防止第一次读数据错误Delay(1000); //等待转换完成LCD_Init();LCD_ShowString(1,1,"Temperature:");while(1){DS18B20_ConvertT(); //转换温度T=DS18B20_ReadT(); //读取温度if(T<0) //如果温度小于0{LCD_ShowChar(2,1,'-'); //显示负号T=-T; //将温度变为正数}else //如果温度大于等于0{LCD_ShowChar(2,1,'+'); //显示正号}LCD_ShowNum(2,2,T,3); //显示温度整数部分LCD_ShowChar(2,5,'.'); //显示小数点LCD_ShowNum(2,6,(unsigned long)(T*10000)%10000,4);//显示温度小数部分}
}
OneWire.c文件
#include <REGX52.H>//引脚定义
sbit OneWire_DQ=P3^7;/*** @brief 单总线初始化* @param 无* @retval 从机响应位,0为响应,1为未响应*/
unsigned char OneWire_Init(void)
{unsigned char i; //定义延时的变量unsigned char AckBit; //返回值OneWire_DQ=1; //保证初始时总线处于高电平OneWire_DQ=0;i = 247;while (--i); //Delay 500usOneWire_DQ=1; //释放i = 32;while (--i); //Delay 70usAckBit=OneWire_DQ; //读取,读出来检测从机回应,从机回应就会把总线拉低i = 247;while (--i); //Delay 500usreturn AckBit;
}/*** @brief 单总线发送一位* @param Bit 要发送的位* @retval 无*/
void OneWire_SendBit(unsigned char Bit)
{unsigned char i;OneWire_DQ=0;i = 4;while (--i); //Delay 10usOneWire_DQ=Bit;i = 24;while (--i); //Delay 50usOneWire_DQ=1;
}/*** @brief 单总线接收一位* @param 无* @retval 读取的位*/
unsigned char OneWire_ReceiveBit(void)
{unsigned char i;unsigned char Bit;OneWire_DQ=0;i = 2;while (--i); //Delay 5usOneWire_DQ=1;i = 2;while (--i); //Delay 5usBit=OneWire_DQ;i = 24;while (--i); //Delay 50usreturn Bit;
}/*** @brief 单总线发送一个字节* @param Byte 要发送的字节* @retval 无*/
void OneWire_SendByte(unsigned char Byte)
{unsigned char i;for(i=0;i<8;i++){OneWire_SendBit(Byte&(0x01<<i));}
}/*** @brief 单总线接收一个字节* @param 无* @retval 接收的一个字节*/
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;
}
OneWire.h文件
#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
DS18B20.c文件
#include <REGX52.H>
#include "OneWire.h"//DS18B20指令
#define DS18B20_SKIP_ROM 0xCC
#define DS18B20_CONVERT_T 0x44
#define DS18B20_READ_SCRATCHPAD 0xBE/*** @brief DS18B20开始温度变换* @param 无* @retval 无*/
void DS18B20_ConvertT(void)
{OneWire_Init();OneWire_SendByte(DS18B20_SKIP_ROM);OneWire_SendByte(DS18B20_CONVERT_T);
}/*** @brief DS18B20读取温度* @param 无* @retval 温度数值*/
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;return T;
}
DS18B20.h文件
#ifndef __DS18B20_H__
#define __DS18B20_H__void DS18B20_ConvertT(void);
float DS18B20_ReadT(void);#endif
(2)DS18B20温度报警器
需要提前加入Delay.c文件、Delay.h文件、Key.c文件、Key.h文件、Timer0.c文件、Timer0.h文件、LCD1602.c文件、LCD1602.h文件、I2C.c文件、I2C.h文件、AT24C02.c文件、AT24C02.h文件、OneWire.c文件、OneWire.h文件、DS18B20.c文件、DS18B20.h文件。
这里用到的按键是通过定时器延时的按键,由于定时器中断进入到主函数中会影响时序的延时,所以需要在OneWire.c文件中将中断屏蔽掉,防止在延时的过程中被中断打断,出去时再将中断打开。
上述操作虽然不会影响单总线的延时,但是会影响定时器延时的特性,因为定时器如果正好在单总线执行时需要进入中断的话,就不能及时的进入定时器执行操作,因为按键对进入中断的时效性要求不严格,所以这里对按键的功能没有影响。而且在单总线中屏蔽了所有的中断,如果有很多模块综合到一起的话,会对其它模块产生很大干扰。
所以在综合使用各个模块的时候,需要注意各模块的优先级。
main.c文件
#include <REGX52.H>
#include "LCD1602.h"
#include "DS18B20.h"
#include "Delay.h"
#include "AT24C02.h"
#include "Key.h"
#include "Timer0.h"float T,TShow;
char TLow,THigh;
unsigned char KeyNum;void main()
{DS18B20_ConvertT(); //上电先转换一次温度,防止第一次读数据错误Delay(1000); //等待转换完成THigh=AT24C02_ReadByte(0); //读取温度阈值数据TLow=AT24C02_ReadByte(1);if(THigh>125 || TLow<-55 || THigh<=TLow){THigh=20; //如果阈值非法,则设为默认值TLow=15;}LCD_Init();LCD_ShowString(1,1,"T:");LCD_ShowString(2,1,"TH:");LCD_ShowString(2,9,"TL:");LCD_ShowSignedNum(2,4,THigh,3);LCD_ShowSignedNum(2,12,TLow,3);Timer0_Init();while(1){KeyNum=Key();/*温度读取及显示*/DS18B20_ConvertT(); //转换温度T=DS18B20_ReadT(); //读取温度if(T<0) //如果温度小于0{LCD_ShowChar(1,3,'-'); //显示负号TShow=-T; //将温度变为正数}else //如果温度大于等于0{LCD_ShowChar(1,3,'+'); //显示正号TShow=T;}LCD_ShowNum(1,4,TShow,3); //显示温度整数部分LCD_ShowChar(1,7,'.'); //显示小数点LCD_ShowNum(1,8,(unsigned long)(TShow*100)%100,2);//显示温度小数部分/*阈值判断及显示*/if(KeyNum) //判断按键是否按下{if(KeyNum==1) //K1按键,THigh自增{THigh++;if(THigh>125){THigh=125;}}if(KeyNum==2) //K2按键,THigh自减{THigh--;if(THigh<=TLow){THigh++;}}if(KeyNum==3) //K3按键,TLow自增{TLow++;if(TLow>=THigh){TLow--;}}if(KeyNum==4) //K4按键,TLow自减{TLow--;if(TLow<-55){TLow=-55;}}LCD_ShowSignedNum(2,4,THigh,3); //显示阈值数据LCD_ShowSignedNum(2,12,TLow,3);AT24C02_WriteByte(0,THigh); //写入到At24C02中保存Delay(5);AT24C02_WriteByte(1,TLow);Delay(5);}if(T>THigh) //越界判断{LCD_ShowString(1,13,"OV:H");}else if(T<TLow){LCD_ShowString(1,13,"OV:L");}else{LCD_ShowString(1,13," ");}}
}void Timer0_Routine() interrupt 1
{static unsigned int T0Count;TL0 = 0x18; //设置定时初值TH0 = 0xFC; //设置定时初值T0Count++;if(T0Count>=20){T0Count=0;Key_Loop(); //每20ms调用一次按键驱动函数}
}
三、LCD1602(液晶显示屏)
1.LCD1602介绍
LCD1602自带控制和扫描电路, 显示时只需要通过数据线将想要显示的内容发送出去即可,不用管扫描,即使不操作也不会产生像数码管那样闪烁的现象。
下图中依次为:LCD1602(以字符数命名)、LCD12864(12864代表其像素点,128*64)、定制的LCD(例如空调遥控器上、电子手表上、字符可以进行定制)、LCD1602背面图、LCD(常用于MP3中,有显示驱动芯片)、全彩的LCD。
2.引脚及应用电路
VO用来调节对比度,如果对比度太低,显示就会很浅,看不清,如果对比度太高,每个像素点都会显示出来,导致无法分辨。调整电位器到合适的位置可以让字符清晰的显示出来,注意电位器的阻值不能太小。
引脚7~引脚14是数据口,D0~D7代表字节的八位,是并行的传输接口。操作逻辑比串行更简单,而且数据传输量大,速度更快。缺点是占用的引脚较多,引脚4~引脚6是控制引脚,RS和RW用来控制数据是干什么的,这里并没有使用到读功能,所以RW一直置零即可。E是使能端,相当于I2C中的SCK,可以将其看作时钟线,当E为高电平时,数据有效,信号在边沿处有效,上升沿时数据有效,下降沿时执行命令。
因为LCD不会自发光,所以每个LCD屏背部都有LED背光显示。
【注意】D0~D7最好接在一组I/O口上,并且要注意高位对高位,低位对低位。
3.内部结构框图
屏幕内部有两个存储单元,一个是字模库,一个是数据显示区域。想要显示的数据实际上是写在DDRAM中的,然后通过DDRAM中的数据从字模库中找到相应字符的显示点阵的字模,最后在对应位置上显示数据。
DDRAM是一个存储器,是40*2的,但是只有前16列映射到了屏幕上,内部有移屏的指令,可以用很简单的方法来实现流动字幕。
AC实际上是地址计数器,可以将其想象为AT24C02,把光标位置设置在某个存储器的地址,写数据时光标的位置会自动加一。AC的位置由指令确定。
4.存储器结构
DDRAM实际上是个RAM存储器,每个存储器都有个地址。
写数据实际上写的是ASCII码表的值,这个值与映射的索引码是一样的。
如果想在第一行第一列显示,就要设置成1000 0000
如果想在第二行第一列显示,就要设置成1100 0000(最高位置1,代表是设置光标位的指令)
5.时序结构
首先设置RS,数据——高电平,指令——低电平,R/W均置为低电平,E置高电平,再置低电平,数据的八位发送到数据端口,即可完成一次写入。
6.LCD1602指令集
清除DDRAM是清屏,清除AC是清除光标。
输入方式一般设置为数据读、写操作后,AC自动加一,画面不动,0x06。
显示开关一般设置为显示开、光标关、闪烁关,0x0c。
功能设置一般设置为八位数据接口,两行显示,5×7点阵,0x38。
如果想设置自定义字符内容,可以调用第7条指令CGRAM地址设置,地址范围只有6位。
7.LCD1602操作流程
根据最高位1的不同位置可以区分不同的指令,其中最高的指令,最高位DB7固定为1,剩下的位才表示它的实际地址,所以实际地址在发送前要把最高位置1,代表这是一个地址设置指令。
8.字符、字符串
\0 是转义字符,可以充当字符串的结束标志。
可见字符是可以直接打出来,用引号引起来的字符。
有些控制字符是不可见的,如果想打这种控制字符就可以通过转义字符,将它的码转义出来。转义字符还可以用来打一些已经被占用的字符。
9.代码示例
需要提前加入Delay.c文件、Delay.h文件。
一个字符可以用一个字节表示,但是字符串需要用多个字节来表示,所以需要通过函数传递一个数组,可以写为 *String,也可以写为String[ ],在C语言中,不能把数组复制一份进行传递,只能传递数组的首地址,因为如果数组很大的话,再复制一份当做实际参数进行传递的话比较花费时间,而且比较占用内存,所以数组只能传递地址。
LCD1602液晶显示屏
main.c文件
#include <REGX52.H>
#include "LCD1602.h"
#include "Delay.h"void main()
{LCD_Init(); //LCD初始化LCD_ShowChar(1,1,'A'); //在1行1列显示字符ALCD_ShowString(1,3,"Hello"); //在1行3列显示字符串HelloLCD_ShowNum(1,9,66,2); //在1行9列显示数字66,长度为2LCD_ShowSignedNum(1,12,-88,2); //在1行12列显示有符号数字-88,长度为2LCD_ShowHexNum(2,1,0xA5,2); //在2行1列显示十六进制数字0xA5,长度为2LCD_ShowBinNum(2,4,0xA5,8); //在2行4列显示二进制数字0xA5,长度为8LCD_ShowChar(2,13,0xDF); //在2行13列显示编码为0xDF的字符LCD_ShowChar(2,14,'C'); //在2行14列显示字符Cwhile(1){}
}
LCD1602.c文件
#include <REGX52.H>//引脚定义
sbit LCD_RS=P2^6;
sbit LCD_RW=P2^5;
sbit LCD_E=P2^7;
#define LCD_DataPort P0/** 私有的延时函数* @brief LCD1602延时函数,12MHz调用可延时1ms* @param 无* @retval 无*/
void LCD_Delay() //@12.000MHz 1ms
{unsigned char i, j;i = 2;j = 239;do{while (--j);} while (--i);
}/*** @brief LCD1602写命令* @param Command 要写入的命令* @retval 无*/
void LCD_WriteCommand(unsigned char Command)
{LCD_RS=0;LCD_RW=0;LCD_DataPort=Command;LCD_E=1;LCD_Delay();LCD_E=0;LCD_Delay();
}/*** @brief LCD1602写数据* @param Data 要写入的数据* @retval 无*/
void LCD_WriteData(unsigned char Data)
{LCD_RS=1;LCD_RW=0;LCD_DataPort=Data;LCD_E=1;LCD_Delay();LCD_E=0;LCD_Delay();
}/*** @brief LCD1602初始化函数* @param 无* @retval 无*/
void LCD_Init(void)
{LCD_WriteCommand(0x38);LCD_WriteCommand(0x0C);LCD_WriteCommand(0x06);LCD_WriteCommand(0x01);
}/*** @brief LCD1602设置光标位置* @param Line 行位置,范围:1~2* @param Column 列位置,范围:1~16* @retval 无*/
void LCD_SetCursor(unsigned char Line,unsigned char Column)
{if(Line==1){LCD_WriteCommand(0x80|(Column-1));}else{LCD_WriteCommand(0x80|(Column-1)+0x40);}
}/*** @brief 在LCD1602指定位置上显示一个字符* @param Line 行位置,范围:1~2* @param Column 列位置,范围:1~16* @param Char 要显示的字符* @retval 无*/
void LCD_ShowChar(unsigned char Line,unsigned char Column,unsigned 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(unsigned char Line,unsigned char Column,unsigned char *String)
{unsigned char i;LCD_SetCursor(Line,Column);for(i=0;String[i]!='\0';i++){LCD_WriteData(String[i]);}
}/*** @brief 返回值=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;
}/*** @brief 在LCD1602指定位置开始显示所给数字* @param Line 起始行位置,范围:1~2* @param Column 起始列位置,范围:1~16* @param Number 要显示的数字,范围:0~65535* @param Length 要显示数字的长度,范围:1~5* @retval 无*/
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('0'+Number/LCD_Pow(10,i-1)%10);}
}/*** @brief 在LCD1602指定位置开始以有符号十进制显示所给数字* @param Line 起始行位置,范围:1~2* @param Column 起始列位置,范围:1~16* @param Number 要显示的数字,范围:-32768~32767* @param Length 要显示数字的长度,范围:1~5* @retval 无*/
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('0'+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(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{unsigned char i;unsigned char SingleNumber;LCD_SetCursor(Line,Column);for(i=Length;i>0;i--){SingleNumber=Number/LCD_Pow(16,i-1)%16;if(SingleNumber<10){LCD_WriteData('0'+SingleNumber);}else{LCD_WriteData('A'+SingleNumber-10);}}
}/*** @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(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('0'+Number/LCD_Pow(2,i-1)%2);}
}
LCD1602.h文件
#include <REGX52.H>//引脚定义
sbit LCD_RS=P2^6;
sbit LCD_RW=P2^5;
sbit LCD_E=P2^7;
#define LCD_DataPort P0/** 私有的延时函数* @brief LCD1602延时函数,12MHz调用可延时1ms* @param 无* @retval 无*/
void LCD_Delay() //@12.000MHz 1ms
{unsigned char i, j;i = 2;j = 239;do{while (--j);} while (--i);
}/*** @brief LCD1602写命令* @param Command 要写入的命令* @retval 无*/
void LCD_WriteCommand(unsigned char Command)
{LCD_RS=0;LCD_RW=0;LCD_DataPort=Command;LCD_E=1;LCD_Delay();LCD_E=0;LCD_Delay();
}/*** @brief LCD1602写数据* @param Data 要写入的数据* @retval 无*/
void LCD_WriteData(unsigned char Data)
{LCD_RS=1;LCD_RW=0;LCD_DataPort=Data;LCD_E=1;LCD_Delay();LCD_E=0;LCD_Delay();
}/*** @brief LCD1602初始化函数* @param 无* @retval 无*/
void LCD_Init(void)
{LCD_WriteCommand(0x38);LCD_WriteCommand(0x0C);LCD_WriteCommand(0x06);LCD_WriteCommand(0x01);
}/*** @brief LCD1602设置光标位置* @param Line 行位置,范围:1~2* @param Column 列位置,范围:1~16* @retval 无*/
void LCD_SetCursor(unsigned char Line,unsigned char Column)
{if(Line==1){LCD_WriteCommand(0x80|(Column-1));}else{LCD_WriteCommand(0x80|(Column-1)+0x40);}
}/*** @brief 在LCD1602指定位置上显示一个字符* @param Line 行位置,范围:1~2* @param Column 列位置,范围:1~16* @param Char 要显示的字符* @retval 无*/
void LCD_ShowChar(unsigned char Line,unsigned char Column,unsigned 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(unsigned char Line,unsigned char Column,unsigned char *String)
{unsigned char i;LCD_SetCursor(Line,Column);for(i=0;String[i]!='\0';i++){LCD_WriteData(String[i]);}
}/*** @brief 返回值=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;
}/*** @brief 在LCD1602指定位置开始显示所给数字* @param Line 起始行位置,范围:1~2* @param Column 起始列位置,范围:1~16* @param Number 要显示的数字,范围:0~65535* @param Length 要显示数字的长度,范围:1~5* @retval 无*/
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('0'+Number/LCD_Pow(10,i-1)%10);}
}/*** @brief 在LCD1602指定位置开始以有符号十进制显示所给数字* @param Line 起始行位置,范围:1~2* @param Column 起始列位置,范围:1~16* @param Number 要显示的数字,范围:-32768~32767* @param Length 要显示数字的长度,范围:1~5* @retval 无*/
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('0'+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(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{unsigned char i;unsigned char SingleNumber;LCD_SetCursor(Line,Column);for(i=Length;i>0;i--){SingleNumber=Number/LCD_Pow(16,i-1)%16;if(SingleNumber<10){LCD_WriteData('0'+SingleNumber);}else{LCD_WriteData('A'+SingleNumber-10);}}
}/*** @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(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('0'+Number/LCD_Pow(2,i-1)%2);}
}