学习通信协议: 买一块 逻辑分析仪,示波器(太贵,用公司或实验室)
目前常用的微机与外设之间进行数据传输的串行总线主要有I2C总线、SPI总线和SCI总线。
其中I2C总线以同步串行2线方式进行通信(一条时钟线,一条数据线)。
SPI总线则以同步串行3线方式进行通信(一条时钟线,一条数据输入线,一条数据输出线)。 SCI总线是以异步方式进行通信(一条数据输入线,一条数据输出线)。
1-wire,即单线总线,又叫单总线。
IIC Communication
I2C总线是PHLIPS公司推出的一种串行总线,它只有两根双向信号线。一根是数据线SDA(serial data I/O),另一根是时钟线SCL(serial clock)。
如下图所示,IIC总线上可以挂多个器件,而每个器件都有唯一的地址,这样可以标识通信目标。数据的通信的方式采用主从方式,主机负责主动联系从机,而从机则被动回应数据。
I2C总线通过上拉电阻接正电源。当总线空闲时,两根线均为高电平。连到总线上的任一器件输出的低电平,都将使总线的信号变低,即各器件的SDA及SCL都是线“与”关系。
开发板上的 I2C总线以及总线上的设备AT24C02
A0A1A2: 000 则从机AT24C02的地址:0x00
AT24C02是一个2K位串行CMOS E2PROM, 内部含有256个字节,CATALYST公司的先进CMOS技术实质上减少了器件的功耗。AT24C02有一个8字节页写缓冲器。该器件通过IIC总线接口进行操作,有一个专门的写保护功能。
EEPROM (Electrically Erasable Programmable read only memory)是指带电可擦可编程只读存储器。是一种掉电后数据不丢失的存储芯片。 EEPROM 可以在电脑上或专用设备上擦除已有信息,重新编程。一般用在即插即用。
I2C总线传输协议
数据位的有效性规定
SCL为高电平期间,数据线上的数据必须保持稳定,只有SCL信号为低电平期间,SDA状态才允许变化。
I2C的起始和终止信号
SCL线为高电平期间,SDA线由高电平向低电平的变化表示起始信号;SCL线为高电平期间,SDA线由低电平向高电平的变化表示终止信号。
I2C字节的传送与应答
每一个字节必须保证是8位长度。数据传送时,先传送最高位(MSB),每一个被传送的字节后面都必须跟随一位应答位(即一帧共有9位)。
I2C字节的传送与应答
每一个字节必须保证是8位长度。数据传送时,先传送最高位(MSB),每一个被传送的字节后面都必须跟随一位应答位(即一帧共有9位)。
应答位的作用
主机在发送数据时,每次发送一字节数据,都需要读取从机应答位,当从机空闲可以接收该字节数据时,从机会发出应答(一帧数据的第9位为“0”),当从机正忙于其他工作的处理来不及接收主机发送的数据时,从机会发出非应答(一帧数据的第9位为“1”)主机则应发出终止信号以结束数据的继续传送,主机通过从机发出的应答位来判断从机是否成功接收数据。
当主机接收数据时,它收到最后一个数据字节后,必须向从机发出一个结束传送的信号。这个信号是由对从机的“非应答”来实现的。然后,从机释放SDA线,以允许主机产生终止信号。
I2C写数据流程
在起始信号后必须传送一个从机的地址(7位)我们开发板上的AT24C02地址为0xa0,第8位是数据的传送方向位(R/T),用“0”表示主机发送数据(T),“1”表示主机接收数据(R)。
I2C读数据流程
在读数据时也要先发送器件地址,读写方向为写,因为我们下一帧需要发送从AT24C02内那个单元开始读,之后需在发一次器件地址这个时候读写方向就为读了,接着我们就可以从总线上读取数据。
软件模拟I2C通信时序
I2C总线的数据传送有严格的时序要求。I2C总线的起始信号、终止信号、发送“0”及发送“1”的模拟时序 :
# include <reg52.h> # include <intrins.h> #define uchar unsigned char #define uint unsigned int #define AT24C02_ADDR 0xa0 //AT24C02地址 /*I2C IO口定义*/ sbit SDA = P2^0; sbit SCL = P2^1; /*5us延时*/ void delay_5us() { _nop_(); } /*1Ms延时*/ void delay(uint z) { uint x,y; for(x = z; x > 0; x--) for(y = 114; y > 0 ; y--); } /*I2C初始化*/ void I2C_init() { SDA = 1; _nop_();//1.08506us SCL = 1; _nop_(); } /*I2C起始信号*/ void I2C_Start() { SCL = 1; _nop_(); SDA = 1; delay_5us(); SDA = 0; delay_5us(); } /*I2C终止信号*/ void I2C_Stop() { SDA = 0; _nop_(); SCL = 1; delay_5us(); SDA = 1; delay_5us(); } /*主机发送应答*/ void Master_ACK(bit i) { SCL = 0; // 拉低时钟总线允许SDA数据总线上的数据变化 _nop_(); // 让总线稳定 if (i) //如果i = 1 那么拉低数据总线 表示主机应答 { SDA = 0; } else { SDA = 1; //发送非应答 } _nop_();//让总线稳定 SCL = 1;//拉高时钟总线 让从机从SDA线上读走 主机的应答信号 delay_5us(); SCL = 0;//拉低时钟总线, 占用总线继续通信 _nop_(); SDA = 1;//释放SDA数据总线。 _nop_(); } /*检测从机应答*/ bit Test_ACK() { SCL = 1;//高电平期间才可以读取应答信号 delay_5us(); if (SDA)//非应答 { SCL = 0; _nop_(); I2C_Stop();//没有应答, 直接结束通信 return(0);//跳出这个函数 } else //应答 { SCL = 0; _nop_(); return(1); } } /*发送一个字节*/ void I2C_send_byte(uchar byte) { uchar i; for(i = 0 ; i < 8 ; i++) { SCL = 0;//低电平时发送数据 _nop_(); if (byte & 0x80)//读取最高位数据 { SDA = 1; //1 _nop_(); } else { SDA = 0;//0 _nop_(); } SCL = 1; //拉高,开始取数 _nop_(); byte <<= 1; // 0101 0100B 发送下一位 } SCL = 0;//占用总线,继续通信,以接受应答信号 _nop_(); SDA = 1;//释放SDA线,接受应答信号 _nop_(); } /*I2C 读一字节*/ uchar I2C_read_byte() { uchar dat,i; SCL = 0;//先拉低,因为不知道当前电平状态 _nop_(); SDA = 1;//释放SDA线 _nop_(); for(i = 0 ; i < 8 ; i++) { SCL = 1;//高电平读取 _nop_(); if (SDA) { dat |= 0x01; // } else { dat &= 0xfe; //1111 1110 } _nop_(); SCL = 0 ;//拉低,准备读取下一个数据 _nop_(); if(i < 7) { dat = dat << 1; } } return(dat); } /*I2C发送数据*/ bit I2C_TransmitData(uchar ADDR, DAT) { I2C_Start(); I2C_send_byte(AT24C02_ADDR+0); if (!Test_ACK()) { return(0); } I2C_send_byte(ADDR); if (!Test_ACK())//无应答,返回 { return(0); } I2C_send_byte(DAT); if (!Test_ACK())//无应答,返回 { return(0); } I2C_Stop(); return(1); } /*I2C接收数据*/ uchar I2C_ReceiveData(uchar ADDR) { uchar DAT; I2C_Start(); I2C_send_byte(AT24C02_ADDR+0); if (!Test_ACK()) { return(0); } I2C_send_byte(ADDR); Master_ACK(0); I2C_Start(); I2C_send_byte(AT24C02_ADDR+1); if (!Test_ACK()) { return(0); } DAT = I2C_read_byte(); Master_ACK(0); I2C_Stop(); return(DAT); } void main() {
bit ACK_flag =0; I2C_init();//I2C初始化
I2C_Start();
I2C_send_byte(AT24C02_ADDR+0);//发送从机地址
if(!Test_ACK())
{
ACK_flag =1;//无应答
}
I2C_send_byte(8);//写地址
if(!Test_ACK())
{
ACK_flag =1;
}
I2C_send_byte(0xfe);//在地址0x08写数据
if(!Test_ACK())
{
ACK_flag =1;
}
I2C_Stop();//发完数据
delay(5);//延时1ms,让从机缓一缓
//开始将刚才写进去的数据读取出来
I2C_Start();
I2C_send_byte(AT24C02_ADDR+1);//发送地址
if(!Test_ACK())
{
ACK_flag =1;
}
P1=I2C_read_byte();//主机读从机该寄存器地址的值,点亮LED1
Master_ACK(0);//主机发送非应答,停止传输数据
I2C_Stop();
if(ACK_flag)
{
P1=0x00;
}
while(1); }
# include <reg52.h> # include <intrins.h> #define uchar unsigned char #define uint unsigned int #define AT24C02_ADDR 0xa0 //AT24C02地址 /*I2C IO口定义*/ sbit SDA = P2^0; sbit SCL = P2^1; /*5us延时*/ void delay_5us() { _nop_(); } /*1Ms延时*/ void delay(uint z) { uint x,y; for(x = z; x > 0; x--) for(y = 114; y > 0 ; y--); } /*I2C初始化*/ void I2C_init() { SDA = 1; _nop_();//1.08506us SCL = 1; _nop_(); } /*I2C起始信号*/ void I2C_Start() { SCL = 1; _nop_(); SDA = 1; delay_5us(); SDA = 0; delay_5us(); } /*I2C终止信号*/ void I2C_Stop() { SDA = 0; _nop_(); SCL = 1; delay_5us(); SDA = 1; delay_5us(); } /*主机发送应答*/ void Master_ACK(bit i) { SCL = 0; // 拉低时钟总线允许SDA数据总线上的数据变化 _nop_(); // 让总线稳定 if (i) //如果i = 1 那么拉低数据总线 表示主机应答 { SDA = 0; } else { SDA = 1; //发送非应答 } _nop_();//让总线稳定 SCL = 1;//拉高时钟总线 让从机从SDA线上读走 主机的应答信号 delay_5us(); SCL = 0;//拉低时钟总线, 占用总线继续通信 _nop_(); SDA = 1;//释放SDA数据总线。 _nop_(); } /*检测从机应答*/ bit Test_ACK() { SCL = 1;//高电平期间才可以读取应答信号 delay_5us(); if (SDA)//非应答 { SCL = 0; _nop_(); I2C_Stop();//没有应答, 直接结束通信 return(0); } else //应答 { SCL = 0; _nop_(); return(1); } } /*发送一个字节*/ void I2C_send_byte(uchar byte) { uchar i; for(i = 0 ; i < 8 ; i++) { SCL = 0;//低电平时发送数据 _nop_(); if (byte & 0x80)//读取最高位数据 { SDA = 1; //1 _nop_(); } else { SDA = 0;//0 _nop_(); } SCL = 1; //拉高,开始取数 _nop_(); byte <<= 1; // 0101 0100B 发送下一位 } SCL = 0;//占用总线,继续通信 _nop_(); SDA = 1;//释放SDA线 _nop_(); } /*I2C 读一字节*/ uchar I2C_read_byte() { uchar dat,i; SCL = 0;//先拉低,因为不知道当前电平状态 _nop_(); SDA = 1;//释放SDA线 _nop_(); for(i = 0 ; i < 8 ; i++) { SCL = 1;//高电平读取 _nop_(); if (SDA) { dat |= 0x01; // } else { dat &= 0xfe; //1111 1110 } _nop_(); SCL = 0 ;//拉低,准备读取下一个数据 _nop_(); if(i < 7) { dat = dat << 1; } } return(dat); } /*I2C发送数据*/ bit I2C_TransmitData(uchar ADDR, DAT) { I2C_Start(); I2C_send_byte(AT24C02_ADDR+0); if (!Test_ACK()) { return(0); } I2C_send_byte(ADDR); if (!Test_ACK())//无应答,返回 { return(0); } I2C_send_byte(DAT); if (!Test_ACK())//无应答,返回 { return(0); } I2C_Stop(); return(1); } /*I2C接收数据*/ uchar I2C_ReceiveData(uchar ADDR) { uchar DAT; I2C_Start(); I2C_send_byte(AT24C02_ADDR+0); if (!Test_ACK()) { return(0); } I2C_send_byte(ADDR); Master_ACK(0); I2C_Start(); I2C_send_byte(AT24C02_ADDR+1); if (!Test_ACK()) { return(0); } DAT = I2C_read_byte(); Master_ACK(0); I2C_Stop(); return(DAT); } void main() { I2C_init();//I2C初始化 if(!I2C_TransmitData(255,0xf0)); //往AT24C02第255个单元中写入数据0XF0 { P1 = 0; } delay(5);//一定延时一定时间后再读取数据 /**/ P1 = I2C_ReceiveData(255);//从AT24C02第255个单元中读取数据 while(1); }
往QX-MCS51开发板的AT24C02内任意一个单元写数据,开发板上电后首先读取此单元的原有的数据,然后赋给程序中的计数变量,计数变量再以5秒的速度+1并且写入AT24C02,当计数大于99时清零再写,用数码管显示。
# include <reg52.h> # include <intrins.h> #define uchar unsigned char #define uint unsigned int #define AT24C02_ADDR 0xa0 sbit LED1 = P1^0; sbit SDA = P2^0; sbit SCL = P2^1; sbit we = P2^7; sbit du = P2^6; uchar EEPROM_DATA; //存放从AT24C02单元读取到的数据 uchar code leddata[]={ 0x3F, //"0" 0x06, //"1" 0x5B, //"2" 0x4F, //"3" 0x66, //"4" 0x6D, //"5" 0x7D, //"6" 0x07, //"7" 0x7F, //"8" 0x6F, //"9" 0x77, //"A" 0x7C, //"B" 0x39, //"C" 0x5E, //"D" 0x79, //"E" 0x71, //"F" 0x76, //"H" 0x38, //"L" 0x37, //"n" 0x3E, //"u" 0x73, //"P" 0x5C, //"o" 0x40, //"-" 0x00, //熄灭 0x00 //自定义 }; void delay_5us() //5us { _nop_(); } /*1毫秒延时函数*/ void delay(uint z) { uint x,y; for(x = z; x > 0; x--) for(y = 114; y > 0 ; y--); } void display(uchar i) //数码管显示函数 { uchar shi,ge; shi = i / 10; //求模 ge = i % 10; //求余 P0 = 0xff; //清除段码 we = 1; P0 = 0xfe; //点亮第一位数码管 we = 0; du = 1; P0 = leddata[shi]; du = 0; delay(1); P0 = 0xff; //清除段码 we = 1; P0 = 0xfd; //点亮第二位数码管 we = 0; du = 1; P0 = leddata[ge]; du = 0; delay(1); } void I2C_init() //I2C初始化 { SDA = 1; _nop_(); SCL = 1; _nop_(); } /*定时器0初始化*/ void Timer0_init() { TMOD |= 0x01; //定时器T0 16为计数工作模式 TH0 = 0x4b; TL0 = 0xfe; //T0 定时50ms ET0 = 1; //T0中断 TR0 = 1; //启动T0 EA = 1; //开总中断 } void I2C_Start() //I2C起始信号 { SCL = 1; _nop_(); SDA = 1; delay_5us(); SDA = 0; delay_5us(); } void I2C_Stop() { SDA = 0; _nop_(); SCL = 1; delay_5us(); SDA = 1; delay_5us(); } void Master_ACK(bit i) // 主机发送应答 { SCL = 0; // 拉低时钟总线允许SDA数据总线上的数据变化 _nop_(); // 让总线稳定 if (i) //如果i = 1 那么拉低数据总线 表示主机应答 { SDA = 0; } else { SDA = 1; //发送非应答 } _nop_();//让总线稳定 SCL = 1;//拉高时钟总线 让从机从SDA线上读走 主机的应答信号 _nop_(); SCL = 0;//拉低时钟总线, 占用总线继续通信 _nop_(); SDA = 1;//释放SDA数据总线。 _nop_(); } bit Test_ACK() // 检测从机应答 { SCL = 1;//时钟总线为高电平期间可以读取从机应答信号 delay_5us(); if (SDA) { SCL = 0; I2C_Stop(); return(0); } else { SCL = 0; return(1); } } void I2C_send_byte(uchar byte) //发送一个字节 { uchar i; for(i = 0 ; i < 8 ; i++) { SCL = 0; _nop_(); if (byte & 0x80) // { SDA = 1; _nop_(); } else { SDA = 0; _nop_(); } SCL = 1; _nop_(); byte <<= 1; } SCL = 0; _nop_(); SDA = 1; _nop_(); } uchar I2C_read_byte() //读一个字节 { uchar i, dat; SCL = 0 ; _nop_(); SDA = 1; _nop_(); for(i = 0 ; i < 8 ; i++) { SCL = 1; _nop_(); dat <<= 1; if (SDA) { dat |= 0x01; } _nop_(); SCL = 0; _nop_(); } return(dat); } bit I2C_WriteData(uchar ADDR,uchar DAT) //I2C写数据 { I2C_Start(); I2C_send_byte(AT24C02_ADDR+0); if (!Test_ACK()) { return(0); } I2C_send_byte(ADDR); if (!Test_ACK()) { return(0); } I2C_send_byte(DAT); if (!Test_ACK()) { return(0); } I2C_Stop(); return(1); } uchar I2C_ReadData(uchar ADDR) //I2C读数据 { uchar dat; I2C_Start(); I2C_send_byte(AT24C02_ADDR+0); if (!Test_ACK()) { return(0); } I2C_send_byte(ADDR); if (!Test_ACK()) { return(0); } Master_ACK(0); I2C_Start(); I2C_send_byte(AT24C02_ADDR+1); if (!Test_ACK()) { return(0); } dat = I2C_read_byte(); Master_ACK(0); I2C_Stop(); return(dat); } void main() { I2C_init(); Timer0_init(); EEPROM_DATA = I2C_ReadData(255); //上电后首先读出24C02第255单元的值 while(1) { display(EEPROM_DATA);//数码管显示 } } /*定时器0中断服务程序*/ void timer0() interrupt 1 //T0内部查询顺序1 { uchar i; TH0 = 0x4b; TL0 = 0xfe; //T0 定时50ms i++; if (i == 100) //5秒时间到 { i = 0; //计数清零 if (EEPROM_DATA < 99) //判断待写数据值 { EEPROM_DATA++; } else { EEPROM_DATA = 0; } if(!I2C_WriteData(255,EEPROM_DATA)) //写入待写数据并判断是否成功写入 { LED1 = 0;//如果写失败 让LED1小灯点亮 } else { LED1 = 1; } } }