从这一贴开始,和大家一起聊聊有关通讯协议,通讯协议是指单片机和外部设备进行数据交互的基本准则.先说说物理连接吧!
地球上的通讯端口,从根本上来讲只有两种:一种是串行接口,即指数据按照高地位的一定顺序,一位一位分时进行传输;一种是并行接口,即指所有数据,一次性同时进行传输;由此可以看来,并行接口理论上的速度会比串行接口快.为了简化单片机(主)和外部设备(从)的物理连接复杂度,一般情况下会使用串行接口.而单片机内部的总线,一般就是并行的结构.俺们这里主要聊聊串行通信,再说协议,这个是非常重要的通信手段.
举个栗子,咱(主机)和女神(从机)聊天,首先得确认两个原则性的东西:第一,相同的语言.咱说国语,女神说那美克星语,直接后果就是双方完全不能沟通.类比下来,就是通讯中的信号协议,必须遵循一定的信号发送接收的格式.还记得韩梅梅和李雷吗?
韩:hi,lilei,how are you
李:fine,thank you,and you?
韩:I’m fine too,goodbye
李:goodbye
韩用英语先喊名字打招呼,李听到喊自己名字且听懂后,才会有下面的交流.这就是一次完整的通信过程,以hi开始,以goodbye结束.
第二,相同的语速.虽然咱和女神都说国语.但是咱每秒说10个字,女神每秒只能听见并理解5个字,这样下来,明显也无法正常沟通.不信你试试飞快地读“西安”一词,会不会有人理解成“先”一字呢?呵呵,速度快了,意思完全就不通了
抛开上面两点,接下来就是一些细节内容了,譬如,咱向女神阐述长达1分钟的故事.女神在这1分钟内,会隔一段时间回复一个“嗯”,来表示正在聆听,并且做好准备继续聆听.咱才能继续讲下去哈
反过来女神讲事情给咱也是一样,咱也得时不时回复一个“嗯”.好让女神继续讲下去,如果换个场景,咱一个人(主机)给多个人(多从机)吹水.也基本上就是上面的过程,个人认为一个串行通信比较重要的方面也就这么些了,下面来看看今天咱聊的具体对象IIC.
IIC,有些写成I2C,简单读成i方c,全称Inter-Integrated Circuit,是由飞利浦在上世纪80年代设计推广的一种通信总线协议,呃,现在应该叫恩智浦(NXP)了.它只需要两个IO口即可构成,一根是SCL时钟线,有些也标为SCK之类,说法不一;另外一根是SDA数据线,SCL是一个单向的IO,由主机向从机发送.SDA则是条双向IO,主机需要对其进行读写操作,每次8位数据.读的时候接收数据,写的时候发送数据.
IIC的速度分为三个等级:标准模式100kbit/s,快速模式400kbit/s,高速模式3.4Mbit/s.由SCL的频率来决定.一般情况下,需要参照外围器件的要求来决定SCL频率.在硬件上,SDA和SCL是需要有上拉电阻,从机设备需要是OC/OD状态,集电极开路或者漏极开路,且这个上拉电阻的取值,会对速度产生比较大的影响.10kohm以下比较常见,个人喜欢4.7k,IIC要求的细则很多,有兴趣可以下载英文原版的手册研读.这里聊聊一些基本要求:
1、基本原则
SCL为高时,SDA不能乱动.SCL为低时,SDA随便玩.SCL必须由主机控制.在寻址过程中,一次发送数据8位,其中高7位为从机地址,最后一位为读写标志位.So,每个从机一个地址,一条IIC总线理论上最多挂载127个从设备.主机呼叫,从机听到叫自己才作出回应
2、起始信号
类似于打招呼hi,告诉别人,咱要发话了
时序图
SCL在高电平器件,SDA出现一次下降沿,也就是SDA从高电平跳变到低电平,看代码:
void start()
{
SDA=1; //SDA拉高
delay();
SCL=1; //SCL拉高
delay();
SDA=0; //SDA拉低,出现一次下降沿,形成起始信号
delay();
SCL=0; //SCL拉低
delay();
}
3、停止信号
时序图
类似于再见goodbye,告诉别人交流结束,SCL高电平期间,SDA由低电平跳变到高电平 ,也就是出现一次上升沿 ,看代码:
void stop()
{
SDA=0; //SDA拉低
delay();
SCL=1; //SCL拉高
delay();
SDA=1; //SDA拉高,出现一次上升沿
delay();
}
4、应答信号
应答信号有两类:一类是主机发送给从机的,另一类是从机发送给主机的,出现在8位数据传输结束后,第九个时钟到来之时,应答信号一般称为ACK信号.至于NACK(非应答信号),其实就是ACK的另一种说法,时序图:
表示从机已经收到之前传输的8位数据,简单点理解,就是女神的“嗯”(邪恶了!!!!!!!) .这个需要由主机读取SDA的值,来判断是否有ACK信号.看代码:
bit respons()
{
bit temp=0;
SDA=1; //主机将SDA拉高,释放SDA控制权(SDA是上拉到电源的哟)
delay();
SCL=1; //SCL拉高
delay();
temp = SDA; //读取SDA的值,赋给temp
SCL=0; //SCL拉低
delay();
return temp; //返回temp值
}
So,temp=1的话,是个NACK信号,从机无响应 ,temp=0的话,则是个ACK信号咯,那就可以认为,从机已经接收到主机发送过来的数据了.
5、数据发送过程
每次传递8位的数据,这些数据啥时候发送呢 ?看时序图:
So easy ,SCL高电平期间,保持SDA数据稳定,就是1位数据传过去了 .SCL低电平期间,SDA可以进行数据变化...瞧代码:
void wr_byte(uchar date)
{
uchar i;
for(i=0;i<8;i++)
{
date=date<<1; //先左移一位
SDA=CY; //将左移进位信号1或0赋给SDA,详见REG51.h中SY寄存器
delay();
SCL=1; //SDA稳定后,SCL拉高
delay();
SCL=0; //SCL拉低
delay();
}
}
上面2-4肢解了IIC一次数据通信过程,就这么些.但是不同的IIC设备,读写时序略有不同,咱来看些具体的例子吧!正好手头上有块24C08
看看这货的地址情况
8位寻址数据,最后一位为读写标志位,0写1读,D7-D1为地址位,其中高四位固定为1010,B2、B1、B0可操作.但是注意一下,对于24C08而言,B2位是由外部管脚确定的,写无效.也就是说,一条IIC总线上可以挂2个24C08,B2为0或者1.而其他,特别是24C02,B0、B1、B2都是由外部管脚确定,可挂8片.不过24c08可以软件操作B1和B0.B1B0=00时,指向block0的256个字节空间.后面依次类推.总共有4x256=1024k字节=8kbit.所以叫做24c08,呵呵。在写寻址的时候,地址为0xa1.在读寻址的时候,地址为0xa0.看看这货的操作过程
其他细节,可以参考数据手册 ,动手撸代码:
#include<in;
#include<reg51.h>
#define uint unsigned int /*宏定义*/
#define uchar unsigned char /*宏定义*/
uchar WRDADDS= 0xa0 ; /*I2C器件地址,寻址写*/
uchar RDDADDS =0xa1 ; /*I2C器件地址,寻址读*/
sbit SDA = P1^0; /*数据线*/
sbit SCL = P1^1; /*时钟线*/
sbit LED = P1^3;
void Write(uchar address,uchar date); /*向24c02的地址address中,写入一字节数据date*/
uchar Read(uchar address); /*从24c02的地址address中,读取一个字节数据(返回值)*/
void wr_byte(uchar date); /*I2C总线写一个字节(有参数)*/
uchar rd_byte(); /*I2C总线读一个字节(返回值)*/
void start(); /*启动I2C总线*/
void stop(); /*停止I2C总线*/
bit respons(); /*I2C总线应答信号检查*/
void delay(); /*延时微秒*/
void delayms(uint xms); /*延时毫秒*/
/*向24c02的地址address中,写入一字节数据date*/
void Write(uchar address,uchar date)
{
start(); //启动信号
wr_byte(WRDADDS); //I2C器件地址,寻址写
while(respons()); //等待应答信号
wr_byte(address); //选择单元地址
while(respons()); //等待应答信号
wr_byte(date); //写数据
while(respons()); //等待应答信号
stop(); //停止信号
}
/*从24c02的地址address中读取一个字节数据*/
uchar Read(uchar address)
{
uchar temp;
start(); //开始信号
wr_byte(WRDADDS); //I2C器件地址,寻址写
while(respons()); //等待应答信号
wr_byte(address); //选择单元地址 信号
while(respons()); //等待应答信号
start(); //重发启动信号
wr_byte(RDDADDS); //I2C器件地址,寻址读
while(respons()); //等待应答信号
temp = rd_byte(); //读数据(函数返回值)
stop(); //停止信号
return temp;
} /*启动I2C总线*/
void start()
{
SDA=1;
delay();
SCL=1;
delay();
SDA=0;
delay();
SCL=0;
delay();
}
/*停止I2C总线*/
void stop()
{
SDA=0;
delay();
SCL=1;
delay();
SDA=1;
delay();
}
/*I2C总线应答信号检查*/
bit respons()
{
bit temp=0;
SDA=1;
delay();
SCL=1;
delay();
temp = SDA;
SCL=0;
delay();
return temp;
}
/*I2C总线写一个字节*/
void wr_byte(uchar date)
{
uchar i;
for(i=0;i<8;i++)
{
date=date<<1;
SDA=CY;
delay();
SCL=1;
delay();
SCL=0;
delay();
}
}
/*I2C总线读一个字节*/
uchar rd_byte()
{
uchar i,temp;
for(i=0;i<8;i++)
{
SCL=1;
delay();
temp=(temp<<1)|SDA;
delay();
SCL=0;
delay();
}
return temp;
}
/*NOP延时函数*/
void delay()
{
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
}
/*延时函数*/
void delayms(uint xms)
{
uint i,j;
for(i=0;i<xms;i++)
for(j=0;j<110;j++);
}
void main()
{
uchar num,x;
uint i,count1,count2;
LED = 1;
for(i=0;i<1024;i++) {
count2++;
if(i<256) {
Write(i,i);
delayms(20);
num = Read(i);
if(num==i)
{
count1++;
LED = ~LED;
}
}
if((256<=i)&&(i<=512)) {
x = (i-256);
WRDADDS = 0xa2;
RDDADDS = 0xa3;
Write(x,x);
delayms(20);
num = Read(x);
if(num==x)
{
count1++;
LED = ~LED;
}
}
if((512<i)&&(i<=768)) {
x = (i-512);
WRDADDS = 0xa4;
RDDADDS = 0xa5;
Write(x,x);
delayms(20);
num = Read(x);
if(num==x)
{
count1++;
LED = ~LED;
}
}
if((768<i)&&(i<1024)) {
WRDADDS = 0xa6;
RDDADDS = 0xa7;
x = (i-768);
Write(x,x);
delayms(20);
num = Read(x);
if(num==x)
{
count1++;
LED = ~LED; }
}
}
if(count1==count2)
{LED=0;}
else{LED = 1;}
while (1)
{
}
}
可以参照对比上面的时序,把代码简单读一下.写得比较随意,了解一下大概过程即可.尤其是Read和Write两个函数.大致功能就是把24C08所有的存储单元都进行一次读写同时blink.如果每个单元写入和读出的数据相同,最后LED点亮.上个GIF
有些时候咱操作的不一定是EEPROM,还有一些其他传感器器件 ,正好手头上有块TMP75B:
很早以前向TI申请的样片,温度传感器 ,看看这货的地址和读写时序吧
A0-A2均可由外部管脚确定,不过高4位固定为1001,tmp75b地址写为0x90,地址读为0x91
因为TMP75B需要16位来完成温度的读取,所以需要连续读2个字节来获得温度值.注意红圈内的时序要求.在多字节传输的时候,每个字节传输结束后,主机需要发送一个ACK.也就是SCL高电平期间,SDA保持低电平.表示接收到了一个8位数据.然后将SDA拉高.简单修改一下上面的代码,完成连续读的操作.
#define WRDADDS 0x90 ; /*宏定义I2C器件tmp75b地址,寻址写*/
#define RDDADDS 0x91 ; /*宏定义I2C器件tmp75b地址,寻址读*/
void ack_master()
{
SDA=0;
delay();
SCL=1;
delay();
SCL=0;
delay();
SDA=1;
delay();
}
uint Read_temprature(uchar address)//连续读2个字节的温度数据
{
uint temp;
start(); //开始信号
wr_byte(WRDADDS); //I2C器件地址,寻址写
while(respons()); //应答信号
wr_byte(address); //选择单元地址 信号
while(respons()); //应答信号
stop();
start(); //重发启动信号
wr_byte(RDDADDS); //I2C器件地址,寻址读
while(respons()); //应答信号
temp = rd_byte()<<8; //读高8位数据(函数返回值)
ack_master();
temp |= rd_byte(); //低8位数据
ack_master();
stop(); //停止信号
return temp;
}
void main()
{
float temprature;
LED = 1; //熄灭LED
while (1)
{
Write(0x00,0x00);
delayms(20);
nums = Read_temprature(0x00); //获取16位温度HEX
temprature = (nums>>4)*0.0625; //换算温度值
if(temprature>25.0){LED = 0;} //如果温度大于25,点LED
else {LED = 1;} //否则熄灭
}
}
看看
Read_temprature
函数,是不是和贴图的时序一致呢?代码基本功能就是,通过IIC与传感器TMP75B通信.获取温度信息
如果超过25度就点亮LED, 否则熄灭LED!上GIF:
好累!这次就先到这里.
了解更多51系列教程,请关注“云汉电子社区”官方微信公众号ickeybbs,或者登录云汉电子社区官方网站(bbs.ickey.cn)