51单片机-通信协议(上)
一、通信协议概述
- 通信就是信息的传输和交换,单片机通信是指单片机与单片机,单片机与传感器存储芯片和外围控制芯片的信息交换等等
- 波特率是发送二进制数据位的速率,单位是bps,即每秒传输二进制位的数量,例256bps = 256bps/s,就是每秒钟可以发256个数据位
通信按照基本类型可以分为
- 串行通信:数据逐一传输
- 并行通信:多位数据可以同时传输
通信按照传输方向可以分为
- 单工通信:在通信时,只能在一条线上单向传输,只能从发送端到接收端
- 半双工通信:在通信时,只能在一条线上双向通信,但是不能同时进行,需要分时进行
- 全双工通信:在通信时,可以同时进行双向通信
通信按照传输速率可以分为
- 异步通信:通信双方各自约定通信速率
- 同步通信:通信双方靠一根时钟线来约定通信速率(有时钟线)
二、通信协议介绍
单片机中的通信协议有很多,主流的通信协议为UART(串口)、I2C总线、SPI总线、1-Wire总线和CAN总线等等
总线:连接各个设备的数据传输线路(类似于马路,把路边各住户连接起来,使得住户可以相互交流)
下图为各通信协议的一些简单介绍
2.1 UART(通用异步收发器)— 串口
- UART有两根数据线(发送端TXD和接收端RXD),TXD和RXD要交叉连接
- UART发送的是串行数据,低位先发,想要发送0100 0011,先发送最低位的1
- 当只需要单向的数据传输时,可以只接一根通信线
- 当VCC电平标准不一致时,需要加电平转换芯片
电平标准是数据1和数据0的表达方式,是传输线缆中人为规定的电压与数据的对应关系,串口常用的电平标准有如下三种:
- TTL电平(51单片机)(距离短):5V表示1,0V表示0
- RS232电平(距离短):-3~ -15V表示1,3~ +15V表示0
- RS485电平(距离长):两线压差+2~ +6V表示1,-2~ -6V表示0(差分信号)
51单片机里有1个UART,其主要有四种工作模式:
- 模式0:同步移位寄存器
- 模式1:8位UART(1Byte),波特率可变(常用)
- 模式2:9位UART(1Byte+1bit),波特率固定
- 模式3:9位UART,波特率可变
下图为两个设备之间的串口接线图
下图为51单片机中UART的引脚图
其中P30和P31口同时也复用为RXD和TXD引脚,分别接到了USB转TTL下载模块的TXD和RXD,这是51单片机程序烧录模块,由此而见,程序下载模块是利用UART进行通信的
- 起始位:在九位数据格式里,第1位就是起始位,串口的空闲状态是高电平,第一个数据固定为低电平,意味着当产生第一个下降沿时,告诉接收方,我要开始发送数据了
- 校验位:在九位数据格式里,第9位就是校验位,用于验证数据是否收发成功
- 停止位:在8位数据发完以后,会有一个停止位,固定为高电平,以便于下一帧数据开始
2.1.1 UART工作原理
下图为UART串口工作原理图
如下图是UART串口中断的工作原理图
SBUF是串口数据缓存寄存器,是用于存放发送或者接收的数据的。单片机写(发送)操作时,写入到发送寄存器,单片机读(接收)操作时,在接收寄存器读;接收到一个字节数据时,RI申请中断,发送完一个字节数据时,TI申请中断进入串口中断部分
T1溢出率是由定时器1产生波特率,选择8位自动重载模式,实现波特率的配置
到串口中断部分以后,经过RI/TI=1,打开ES、EA使能和PS优先级配置,进入中断服务函数,完成所需要的工作
2.1.2 UART寄存器配置
下面为串口相关寄存器
2.1.2.1 SCON(串行口控制寄存器)可位寻址
SM0/1:控制串口工作方式,00模式0,01模式1,10模式2,11模式3
SM2:允许方式2或方式3多机通信控制
REN:允许/禁止串行接收控制位,软件置位,REN=1允许,启动串行接收器RXD,开始接收信息
TB8:方式2或方式3,它为要发送的第9位数据,按需要由软件置位或清0
RB8:方式2或方式3,是接收到的第9位数据
TI:发送中断请求标志位,发送完硬件置TI=1,随之软件复位,即TI=0
RI:接收中断请求标志位,接收完硬件置RI=1,随之软件复位,即RI=0
综上所述,在配置SCON时,针对模式1,SM0/1给01,REN看需求给0或者1,其余位先给0
2.1.2.2 PCON(电源控制寄存器)不可位寻址
SMOD:波特率选择位。软件置1,模式1、2、3波特率加倍。SMOD=0,各模式波特率加倍。复位时为0
SMOD0:帧错误检测有效控制位
2.1.2.3 定时器1寄存器配置
串口波特率配置定时器是定时器1
- 与定时器0配置不同之处,定时器1需要工作在8位自动重载模式,所以定时器1的TMOD = 0010 0000。自动重载模式下,定时器初值溢出自动赋值,所以定时器1中断也不需要开启
- SCON寄存器配置,只需要允许定时器1工作即可
具体内容可见定时器章节,也可以利用STC-ISP自动生成对于定时器1的相关配置
2.1.2.4 串口中断寄存器配置IE(可位寻址)、IPH、IP
EA:总中断允许控制位,EA=1,打开中断
ES:串口中断允许位,ES=1,允许串口中断
触发条件是RI或者TI=1时,即发送完毕或者接收完毕时,进入中断服务函数
其余优先级寄存器按需求配置即可
2.1.3 单片机向电脑发送数据
此处代码编写的内容是单片机通过串口向电脑发送数据,在STC-ISP上显示逐一递增的十六进制数
下面给出Uart.c
#include <REGX52.H>
void Uart_Init() //[email protected]
{
//UART寄存器配置
PCON |= 0x80;//1000 0000
SCON = 0x40;//0100 0000
//定时器配置
TMOD &= 0x0F;//TMOD = 0000 0000
TMOD |= 0x20;//TMOD = 0010 0000
TL1 = 0xF4;//配置波特率4800
TH1 = 0xF4;
TR1 = 1;//允许定时器1
//中断配置
ET1 = 0;//禁止定时器1中断
}
/*
函数功能:单片机发送字节
形参:字节数据
*/
void Uart_Sendbyte(unsigned char byte)
{
SBUF = byte;//串行数据寄存器给值
while(TI==0);//如果发送完,硬件置1,退出循环
TI=0;//软件复位,给0
}
代码中有两个函数,第一个函数是串口初始化,里面主要分为三块,第一块是串口寄存器配置,第二块是定时器1寄存器配置去确定波特率等等,第三块是定时器1中断配置,8位重载模式下,会自动重新赋TH1和TL1,不需要中断去重新赋值,所以定时器1中断这块不需要开启。第二个函数是发送数据,直接将8位数据赋给SBUF寄存器,然后判断是否发送完毕,发送完毕即可复位TI,那么电脑就可以接收到单片机发送的数据了
下面是STC-ISP实验现象
2.1.4 电脑端向单片机发送数据
电脑向单片机发数据,是单片机接收数据,所以在SCON寄存器里,REN接收使能需要打开,REN=1,并且需要开启串口中断去实现相应的操作
下面给出Uart.c
#include <REGX52.H>
void Uart_Init() //[email protected]
{
//UART寄存器配置
PCON |= 0x80; //1000 0000
SCON = 0x50; //0101 0000 模式1 REN
//定时器1寄存器配置
TMOD &= 0x0F;
TMOD |= 0x20;
TL1 = 0xF4;
TH1 = 0xF4;
TR1 = 1; //开启定时器1
//定时器1中断寄存器
ET1 = 0;
//串口中断寄存器配置
EA = 1;
ES = 1;
}
void Uart_Sendbyte(unsigned char byte)
{
SBUF = byte;
while(TI == 0);
TI = 0;
}
上述代码中,与单片机发送数据不同的地方是串口寄存器的SCON,还有使能的串口中断寄存器,这里我们需要通过单片机接收到指令实现LED的亮灭
下面给出main.c
#include <REGX52.H>
#include "Uart.h"
void main()
{
Uart_Init();
while(1)
{
}
}
/*
函数功能:串口中断服务函数,实现P2口的自定义,并且可以实现收和发
*/
void UART_Routine() interrupt 4
{
if(RI==1) //MCU接收到了数据
{
P2 = SBUF;
Uart_Sendbyte(SBUF); //MCU同时也给电脑端发SBUF
RI = 0; //软件复位
}
}
本代码可以实现LED的亮灭状态随着SBUF寄存器的内容进行变化
下图是串口调试的界面,从图中可以看出单片机接收到数据并且还发送给电脑端接收到的数据,LED也是满足LED8-LED5点亮,LED4-LED3不亮
2.2 I2C总线
- I2C总线是由Philips公司开发的一种通用数据总线
- 两根通信线:SCL(串行时钟总线)和SDA(串行数据总线),属于同步、半双工通信协议,带数据应答
- 所有I2C设备的SCL和SDA都要连在一起,并且设备的SCL和SDA均要配置成开漏输出模式(输出低电平有效,引脚浮空无驱动能力(高阻态),需要上拉电阻实现输出高电平)
- SCL和SDA各有一个上拉电阻,在外部可以将设备的SCL和SDA上拉到高电平,阻值一般为4.7千欧左右
- 开漏输出和弱上拉模式下,只需要在发送低电平时拉低SDA,发送高电平时,由上拉电阻拉SDA至高电平
- 线与:多个输出引脚接同一条信号线,每个设备都是开漏输出和弱上拉的模式,空闲处于高阻态,只要有一个设备拉低信号线,整个信号线就被拉到低电平,而所有设备都不拉低时,信号线会通过上拉电阻回到高电平,不会存在多设备同时主动强上拉或者强下拉的情况
高阻态是不驱动任何电平,像断开连接的开关,既不拉高也不拉低信号线
SCL由主机控制,可以配置成推挽输出,但是不建议
2.2.1 I2C总线时序图
2.2.1.1 起始条件和终止条件(S P)
- 起始条件:SCL高电平期间,SDA从高到低(先拉低一下热热身)
- 终止条件:SCL高电平期间,SDA从低到高(最后放松,回到空闲期间)
2.2.1.2 发送一个字节(S:BYTE)
- 发送一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位在前),然后待SCL拉高时,从机才会在SCL高电平期间读取SDA上的数据位
从机在SCL高电平期间读SDA的电平,SCL高电平期间SDA的电平不准变化,循环8次就是发送一个字节
2.2.1.3 接收一个字节(R:BYTE)
- 接收一个字节:SCL低电平期间,从机将数据位依次放到SDA线上(高位在前),然后待SCL拉高时,主机才会在SCL高电平期间读取SDA上的数据位
主机在SCL高电平期间读SDA的电平,SCL高电平期间SDA的电平不准变化,循环8次就是接收一个字节(主机在接收之前,需要释放SDA,置高电平,这就是把SDA控制权交给从机)
2.2.1.4 发送应答和接收应答(SA RA)
- 发送应答:主机在接受完一个字节以后,需要在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答
- 接收应答:主机在发送完一个字节以后,需要在下一个时钟接收一个数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接受前需要释放SDA)
应答可以作为发送数据的第9位
2.2.2 I2C总线数据帧
2.2.2.1 发送一帧数据
-
完成任务:向谁(从机地址)发(W)什么(BYTE)
-
步骤:S(起始) —> S:SLAVE ADDRESS+W(从机地址+写) —> RA:0(接收应答) —> S:BYTE 1(发送第一个字节) —> RA:0(接收应答) —> S:BYTE 2(发送第二个字节) —> RA:0(接收应答) + S:BYTE n(发送第n个字节) —> RA:0(接收应答) —> P(终止)
A6-A0是I2C的地址,A6-A3是前缀固定地址(查手册),A2-A0电路中接线I2C地址(可配置)。最后一位是读写标志位,1读就是接收,0写就是发送
理解:老师:小明你记一下我说的问题。小明:收到。老师:…。小明:收到。老师:…。小明:收到。结束
2.2.2.2 接收一帧数据
-
完成任务:向谁(从机地址)收®什么(BYTE)
-
步骤:S(起始) —> S:SLAVE ADDRESS+R(从机地址+读) —> RA:0(接收应答) —> R:BYTE 1(接收第一个字节) —> SA:0(发送应答) —> R:BYTE 2(接收第二个字节) —> SA:0(发送应答) + R:BYTE n(接收第n个字节) —> SA:1(发送应答) —> P(终止)
A6-A0是I2C的地址,A6-A3是前缀固定地址(查手册),A2-A0电路中接线I2C地址(可配置)。最后一位是读写标志位,1读就是接收,0写就是发送
理解:老师:小明你回答一下我说的问题。小明:收到。小明:…。老师:嗯。小明:…。老师:嗯。结束
2.2.2.3 先发送再接收数据帧(复合格式)
-
完成任务:向谁(从机地址)收®指定的什么(BYTE)
-
步骤:S(起始) —> S:SLAVE ADDRESS+W(从机地址+写) —> RA:0(接收应答) —> S:BYTE 1(发送第一个字节) —> RA:0(接收应答) —> S:BYTE 2(发送第二个字节) —> RA:0(接收应答) + S:BYTE n(发送第n个字节) —> RA:0(接收应答) —>
S(起始) —> S:SLAVE ADDRESS+R(从机地址+读) —> RA:0(接收应答) —> R:BYTE 1(接收第一个字节) —> SA:0(发送应答) —> R:BYTE 2(接收第二个字节) —> SA:0(发送应答) + R:BYTE n(接收第n个字节) —> SA:1(发送应答) —> P(终止)
理解:老师:小明你记一下我说的问题。小明:收到。老师:…。小明:收到。老师:…。小明:收到。 老师:小明你回答一下我说的问题。小明:收到。小明:…。老师:嗯。小明:…。老师:嗯。结束
2.2.3 I2C总线驱动代码
I2C总线的驱动代码如下所示
#include <REGX52.H>
sbit I2C_SCL = P2^1;
sbit I2C_SDA = P2^0;
/*
函数功能:I2C起始条件
*/
void I2C_Start(void)
{
I2C_SDA = 1;
I2C_SCL = 1; //空闲高电平
I2C_SDA = 0; //SDA拉低表示开始
I2C_SCL = 0; //保证下次SCL高电平期间读取数据
}
/*
函数功能:I2C终止条件
*/
void I2C_Stop(void)
{
I2C_SDA = 0;
I2C_SDA = 1; //终止下SDA拉高,保持空闲
I2C_SCL = 1; //返回空闲
}
/*
函数功能:发送(写)一个字节
形参:写的数据
*/
void I2C_SendByte(unsigned char Byte)
{
unsigned char i;
for(i=0;i<8;i++)
{
I2C_SDA = Byte&(0x80>>i); //主机将Byte的每一位(高位在前)放在SDA上,SCL高电平期间SDA不允许变化
I2C_SCL = 1; //SCL高电平时,从机读取数据
I2C_SCL = 0; //取到数据,立马拉底,等待下一次高电平读取
}
}
/*
函数功能:接收(读)一个字节
返回值:读的数据
*/
unsigned char I2C_ReceiveByte(void)
{
unsigned char Byte=0x00,i;
I2C_SDA = 1; //主机在接收前需要释放SDA
for(i=0;i<8;i++)
{
I2C_SCL = 1; //SCL高电平读取
if(I2C_SDA){Byte|=(0x80>>i);} //读八位数据(高位在前),此处没有变化SDA,只是在SCL高电平期间,SDA不允许变化,Byte更稳定
I2C_SCL = 0; //读完拉底
}
return Byte;
}
/*
函数功能:发送应答
形参:应答位
*/
void I2C_SendAck(bit AckBit) //bit定义的数据是1位
{
I2C_SDA = AckBit; //应答位 0应答1非应答
I2C_SCL = 1; //读bit位
I2C_SCL = 0;
}
/*
函数功能:接收应答
返回值:应答位
*/
unsigned char I2C_ReceiveAck(void)
{
unsigned char AckBit;
I2C_SDA = 1; //接收前释放SDA
I2C_SCL = 1;
AckBit = I2C_SDA; //读取应答位
I2C_SCL = 0;
return AckBit;
}
2.2.4 I2C总线应用—AT24C02
2.2.4.1 AT24C02介绍
- AT24C02是一种可以实现掉电不丢失的存储器,用于存储单片机运行时想要永久保存的数据信息
- 存储介质:E2PROM
- 通信协议:I2C总线
- 容量:256Byte
下图各引脚定义:
E0-E2:I2C地址线
SCL和SDA:I2C接口
VSS接地,VDD接1.8V-5.5V,WP写保护(高电平有效)
2.2.4.2 AT24C02数据帧
- 字节写:在从机地址处写数据DATA
- 随机读:读出从机地址处的数据DATA
数据帧中除了AT24C02的从机地址,还存在数据地址WORD ADDRESS,这指明在AT24C02的哪个位置写或者读
AT24C02的固定地址(A6-A3)为1010,可配置地址如电路图为000。所以SLAVE ADDRESS+W为1010 0000即0xA0,SLAVE ADDRESS+R为1010 0001即0xA1
2.2.4.3 AT24C02代码
AT24C02数据帧通过调用I2C驱动代码来实现,主要分为两块内容,一块是字节写,一块是随机读
下面是AT24C02.c
#include "I2C.h"
#include <REGX52.H>
#define AT24C02_ADDRESS 0xA0
/*
函数功能:字节写
形参:字地址 数据
*/
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(); //终止
}
/*
函数功能:随机读
形参:字地址
返回值:数据
*/
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); //发送从机地址+读
I2C_ReceiveAck(); //接收应答
Data = I2C_ReceiveByte(); //读数据
I2C_SendAck(1); //发送应答
I2C_Stop(); //停止
return Data; //返回读的数据
}
调用AT24C02.c里面的函数,完成存储数据和读取数据的功能,给出main.c
#include <REGX52.H>
#include "LCD1602.h"
#include "Key.h"
#include "Delayms.h"
#include "AT24C02.h"
unsigned char KeyNum;//0-255 8位数据
unsigned int Num; //0-65535 16位数据
void main()
{
LCD_Init();
// AT24C02_WriteByte(1,256);
// Delayms(5);//写入需要时间,最长5ms
// Data = AT24C02_ReadByte(1);
// LCD_ShowNum(1,1,Data,3);
while(1)
{
LCD_ShowNum(1,1,Num,5);
KeyNum = Key();
if(KeyNum == 1)
{
Num++;
LCD_ShowNum(1,1,Num,5);
}
if(KeyNum == 2)
{
Num--;
LCD_ShowNum(1,1,Num,5);
}
if(KeyNum == 3)
{
AT24C02_WriteByte(0,Num%256); //一次只能存储八位数据,此为低八位
Delayms(5);
AT24C02_WriteByte(1,Num/256); //高八位
Delayms(5);
LCD_ShowString(2,1,"Write OK!");
Delayms(1000);
LCD_ShowString(2,1," ");
}
if(KeyNum == 4)
{
Num = AT24C02_ReadByte(0); //低八位
Num |= AT24C02_ReadByte(1)<<8; //高八位,强制转换(数据类型不一致的时候)
LCD_ShowNum(1,1,Num,5);
LCD_ShowString(2,1,"Read OK!");
Delayms(1000);
LCD_ShowString(2,1," ");
}
}
}
写入数据时需要时间,所以要延迟一段时间
2.3 1-Wire总线
- 单总线(1-Wire)是由Dallas公司开发的一种通用数据总线
- 单总线(1-Wire)只有一根通信线DQ,是一种异步、半双工通信协议
- 单总线(1-Wire)只需要一根通信线即可实现数据的双向传输,当采用寄生供电时,还可以省去设备的VDD线路,此时,供电加通信只需要DQ和GND两根线
- 单总线(1-Wire)的DQ均要配置成开漏输出模式(低电平有效,空闲是高电平),DQ添加一个上拉电阻,阻值一般为4.7千欧左右
- 单总线(1-Wire)的从机采取寄生供电,则主机还应配置一个强上拉输出电路
下图给出1-Wire总线独立供电和寄生供电的原理图
2.3.1 1-Wire总线时序图
2.3.1.1 初始化(S)
- 初始化:主机将总线拉低至少480us,然后释放总线,等待15-60us后,如果存在从机,从机会拉低总线60-240us以响应主机(自动响应,不需要软件拉低),之后从机将释放总线
总线平时空闲状态为Vpu高电平,想要初始化,主机(Bus master)拉低至少480us,然后释放总线(电阻拉高,弱上拉),等待15-60us,如果存在从机,则从机(DS18B20)响应主机,即从机拉低总线60-240us。之后从机释放总线
2.3.1.1 发送一位和一个字节(S:BIT和S:BYTE)
- 发送一位:主机将总线拉低60-120us,然后释放总线,表示发送0;主机将总线拉低1-15us,然后释放总线,表示发送1。从机将在总线拉低30us后读取电平,整个时间片应大于60us
当主机拉低总线时,如果在30us时,总线依旧是低,则发送0,反之则发送1,通过主机拉低时长来判断是30us时刻的高低电平,判断发送1或者0
- 发送一个字节:连续调用8次发送一位的时序,依次发送一个字节的8位(低位在前)
2.3.1.2 接收一位和一个字节(R:BIT和R:BYTE)
- 接收一位:主机将总线拉低1-15us,然后释放总线(给从机控制权),并在拉低后15us内读取总线电平(尽量贴近15us的末尾,要在15us内完成读取操作–主机拉低,释放,从机操作总线,读取总线),读取为低电平则为接收0,读取为高电平则为接收1,整个时间片应大于60us
主机将总线拉低1-15us,然后释放,表示主机开始接收数据,将总线控制权交给从机。在15us末尾时,要是从机也同时保持拉低总线,则主机接收0;若从机保持释放总线,则主机接收1
总线控制权一开始都是主机的,主机释放掉总线给从机总线控制权,从机才可以发,这里避免接收和发送数据混淆
- 接收一个字节:连续调用8次接收一位的时序,依次接收一个字节的8位(低位在前)
2.3.1.3 1-Wire总线驱动代码
下面是1-Wire总线驱动代码,主要分为五个函数,初始化、发送一位数据和接收一位数据,发送一位字节和接收一位字节
#include <REGX52.H>
sbit OneWire_DQ = P3^7;
/*
函数名:OneWire_Init
功能描述:1-wire初始化
输入参数:无
返回值:AckBit(从机响应标志位)
*/
unsigned char OneWire_Init()
{
unsigned char i,AckBit;
OneWire_DQ = 1;
OneWire_DQ = 0; //初始化拉低总线
i = 227;while (--i); //延时500us(忽略函数调用的5us)
OneWire_DQ = 1; //释放总线
i = 29;while (--i); //延时70ms
AckBit = OneWire_DQ; //读出应答,如果有从机存在,则会返回0,无从机存在返回1
i = 227;while (--i); //延时500us(整个周最少480us)
return AckBit;
}
/*
函数名:OneWire_SendBit
功能描述:1-wire发送一位数据
输入参数:Bit
返回值:无
*/
void OneWire_SendBit(unsigned char Bit)
{
unsigned char i;
OneWire_DQ = 0; //拉低总线
i = 4;while (--i); //延时10us(考虑到延时函数调用的5us,10+5us)
OneWire_DQ = Bit; //if Bit == 1 DQ = 1; else DQ = 0
i = 22;while (--i); //延时50us(50+5us,整个时间片60us)
OneWire_DQ = 1; //释放总线
}
/*
函数名:OneWire_ReceiveBit
功能描述:1-wire接收一位数据
输入参数:无
返回值:Bit(接收数据)
*/
unsigned char OneWire_ReceiveBit()
{
unsigned char i,Bit;
OneWire_DQ = 0; //拉低总线
i = 2;while (--i); //延时5us(10+5us)
OneWire_DQ = 1; //释放总线给从机控制权(再过5us后,if DQ == 0 则从机继续拉低, else 从机拉高)
i = 2;while (--i); //延时5us(10+5us)
Bit = OneWire_DQ; //在10us时采样
i = 22;while (--i); //延时50us(整个时间片60us)
return Bit;
}
/*
函数名:OneWire_SendByte
功能描述:1-wire发送一个字节
输入参数:Byte
返回值:无
*/
void OneWire_SendByte(unsigned char Byte)
{
unsigned char i;
for(i=0;i<8;i++)
{
OneWire_SendBit(Byte&(0x01<<i)); //取Byte第i位 发送出去,低位开始,8位转1位,非0即1
}
}
/*
函数名:OneWire_ReceiveByte
功能描述:1-wire接收一个字节
输入参数:无
返回值:Byte(接收数据)
*/
unsigned char OneWire_ReceiveByte()
{
unsigned char i,Byte = 0x00;
for(i=0;i<8;i++)
{
if(OneWire_ReceiveBit()){Byte|=(0x01<<i);} //将DQ线的数据,依次给Byte
}
return Byte;
}
2.3.2 1-Wire总线应用-DS18B20温度传感器
2.3.2.1 DS18B20介绍和工作原理
- DS18B20是一种常见的数字温度传感器,其控制命令和数据都是以数字信号的方式输入输出,相比较于模拟温度传感器,具有功能强大、硬件简单、易拓展、抗干扰性强等特点
- 测温范围:-55℃到+125℃
- 通信协议:1-Wire
- 其他特征:可以形成总线结构、内置温度报警功能、可以寄生供电
模拟传感器:例如热敏电阻,利用分压电路得到模拟电压值,AD转换得到数字量字节;数字传感器,例如DS18B20内置了这些转换器,可以直接输出数字量,不需要变化
下图为DS18B20的电路原理图
VCC:3.0V-5.0V
DQ:单总线接口,空闲状态下拉高
下图为内部工作的框图
64-BIT ROM:作为器件地址,用于总线通信的寻址
SCRATCHPAD(暂存器RAM):用于总线的数据交互
EEPROM(掉电不丢失):保存温度触发阈值和配置参数
VDD直接外部供电,DQ经过64-BIT ROM的ID号(器件地址),也可以直接跳过,然后进入到配置阶段,确定是读还是其他的指令,然后再到RAM,进行数据交互
下图是SCRATCHPAD暂存器和EEPROM的具体细节图
从图中左半部分可以看出来,暂存器有9个字节,Byte0和Byte1共同组成温度的具体值,默认值为85℃。Byte2-Byte4分别对应右半部分的EEPROM,我们不可以直接将数据存到EEPROM里面,需要通过暂存器然后复制到EEPROM里。Byte5-Byte7是保留位,可以自定义功能,最后一位是循环冗余校验位
2.3.2.2 DS18B20工作流程和数据帧
- DS18B20操作流程:
1.初始化:从机复位,主机判断从机是否响应
2.ROM操作:ROM指令+本指令需要的读写操作
3.功能操作:功能指令+本指令需要的读写操作
下面给出DS18B20的ROM和功能指令
ROM指令从上到下:搜寻、读、匹配(设备地址)、跳过(单设备可用)、报警搜索(获取哪个设备报警)
功能指令从上到下:温度变换(温度更新)、写温度(温度放到暂存器)、读温度、复制(暂存器温度复制到EEPROM,掉电不丢失)、返回(复制的反操作)、读供电模式
所以1-Wire做的工作就是发送指令给DS18B20,然后再从DS18B20的暂存器中读取数据,整个流程就是先配置ROM,再配置功能模式
下面给出DS18B20的数据帧
- 温度变换:初始化 —> 跳过ROM(ROM指令) —> 开始温度变化(功能指令)
- 温度读取:初始化 —> 跳过ROM(ROM指令) —> 读暂存器(功能指令) —> 连续读操作
温度数据是如何存在LSB(低字节)和MSB(高字节)的,下图为温度存储格式
前5位(BIT15-BIT11)表示符号位,正温度全0,负温度全1。BIT10-BIT4表示的温度的整数部分,BIT3-BIT0表示温度的小数部分。负数温度是以补码(将负数二进制取反加1)形式存在
下面给出一些温度存储的例子
2.3.2.3 DS18B20数据帧代码
1-Wire总线的驱动代码编写DS18B20的数据帧代码,主要分为温度变换(温度更新)和读取温度两块函数,下面给出具体的代码,解释部分见注释
#include <REGX52.H>
#include "OneWire.h"
#define DS18B20_SKIP_ROM 0xCC
#define DS18B20_CONVERT_T 0x44
#define DS18B20_READ_SCRATCHPAD 0xBE
/*
函数名:DS18B20_ConvertT
功能描述:DS18B20温度转换
输入参数:无
返回值:无
*/
void DS18B20_ConvertT()
{
OneWire_Init();
OneWire_SendByte(DS18B20_SKIP_ROM); //ROM指令:发送跳过指令
OneWire_SendByte(DS18B20_CONVERT_T); //功能指令:发送温度转换指令
}
/*
函数名:DS18B20_ReadT
功能描述:DS18B20温度读取
输入参数:无
返回值:T(读取温度)
*/
float DS18B20_ReadT()
{
unsigned char TLSB,TMSB;
int Temp; //有符号int16位
float T;
OneWire_Init();
OneWire_SendByte(DS18B20_SKIP_ROM); //ROM指令:发送跳过指令
OneWire_SendByte(DS18B20_READ_SCRATCHPAD); //功能指令:发送读字节指令
TLSB = OneWire_ReceiveByte();
TMSB = OneWire_ReceiveByte(); //读暂存器的温度,第0和第1个字节(未转换)
Temp = (TMSB<<8)|TLSB; //M高八位 L低八位 ,强制转换成有符号16位
T = Temp/16.0; //低4位是小数部分,左移八位导致小数点左移4位,二进制中左移1位扩大两倍,所以/16,变成实际温度
return T;
}
在主函数调用函数,即可显示出DS18B20实时变化的温度值
float T;
void main()
{
LCD_Init();
LCD_ShowString(1,1,"Temperature:");
while(1)
{
DS18B20_ConvertT();
T = DS18B20_ReadT();
if(T<0)
{
LCD_ShowChar(2,1,'-');
T = -T;
}
else
{
LCD_ShowChar(2,1,'+');
} //整数部分
LCD_ShowNum(2,2,T,3); //显示整数部分
LCD_ShowChar(2,5,'.'); //点
LCD_ShowNum(2,6,(unsigned long)(T*10000)%10000,3); //显示小数部分
}
}