最近在做51的课程设计,要用到很多以前只是浅浅学过的通信协议,趁这个机会好好复习一下,学习资料参考普中51单片机开发攻略
常规的I2C共有两条管腿,分别为SCL(时钟)和SDA(数据),这是一种半双工的串行协议,优点为节省硬件资源且传输速度较快,缺点是不能同时收发数据,相较SPI这样的协议传输数据速度较慢,下面按照硬件层和软件层来介绍一下I2C
如上图所示,I2C常规的用法就是一主机多从机的通信方式,数据线SDA可以进行数据的双向传输,时钟线SCL可以控制数据的收发时序
每一个从机有一个独立的地址信息,主机通过这些地址信息来查找对应的从机
总线通过上拉电阻接到电源,当 I2C 设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平
在多主机模式下进行通信则要加入仲裁机制以决定应该让哪个主机来参与通信
I2C中包含了通信的起始&终止状态、数据有效性、应答、仲裁、时钟同步、地址广播等环节,上图表示的是一个基本的数据传输流程
每次传输的数据都以字节为单位,传输的字节数不受限制
之所以传输数据时SDA不能在SCL为高电平时改变,原因就是因为当SCL置高时,SDA的改变影响了数据传输过程的起始
起始和终止信号都是由主机产生的,当起始信号发出时,主机就被下拉,和对应地址的从机通讯,当终止信号到来时,主机被释放,总线处于空闲状态
应答响应由接收方发出,用于告诉发送方应该继续还是终止,因此一个帧有9位,传输数据时先传送最高位
若从机无法产生应答时,SDA的数据位要置高,此时主机就能自行发送一个终止信号
如果从机对主机进行了应答,但其无法接收更多的数据时,从机可以在第一个无法接收的数据处加一个非应答信息,从机释放SDA线,主机收到非应答信息后产生终止信号
如果主机收到数据过程中,在接收完最后一个数据字节后,主机要发出一个非应答信号,从机收到后释放SDA线,以允许主机产生终止信号
从这里看好像产生终止信号的都是主机,只是中间的过程略有不同罢了
寻址方式分为7位和10位寻址方式,我就跟着开发指南走,介绍7线寻址方式,当一个起始信号发出后,后面紧跟着的就是地址字节,前7位为地址信息(最高位在前),最低位决定数据传输的方向
最低位为1时代表主机向从机读取数据,数据传输方向为从机->主机
最低位为0时代表主机向从机写入数据,数据传输方向为主机->从机
当主机发送了一个地址字节后,总线上的所有设备都会与其自身对应的地址(可编程位+固定位)做对比,如果一个7位地址字节有4位是可编程位,3位是固定位,这代表这条总线可以接最多16个一样的从机设备
一个大致的传输数据流程如下
起始信号->地址字节->方向位->(数据字节->应答位)*N次->终止信号
但如果我们在一次传输流程结束后还想继续占用总线,那么我们可以不发送终止信号而是继续发送起始信号进行与其他设备间的通讯
大致传输情况有以下几种
至此软件I2C的介绍大致结束
#include
sbit I2C_SCL=P2^1;
sbit I2C_SDA=P2^0;
/**
* @brief I2C开始
* @param 无
* @retval 无
*/
void I2C_Start(void)
{
I2C_SDA=1; //数据线先置1
I2C_SCL=1; //时钟线置1,在此状态下数据位下降则代表I2C总线开始占用
I2C_SDA=0; //数据线产生下降沿
I2C_SCL=0; //时钟线复位
}
/**
* @brief I2C停止
* @param 无
* @retval 无
*/
void I2C_Stop(void)
{
I2C_SDA=0; //数据位先置0
I2C_SCL=1; //时钟线置1
I2C_SDA=1; //在时钟线置1的情况下使数据线上升,此时为I2C停止
}
/**
* @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); //10000000移位,一次接收一位,从最高位开始接收
I2C_SCL=1;
I2C_SCL=0; //时钟线拉低,此时数据变换才为有效
}
}
/**
* @brief I2C接收一个字节
* @param 无
* @retval 接收到的一个字节数据
*/
unsigned char I2C_ReceiveByte(void)
{
unsigned char i,Byte=0x00;
I2C_SDA=1; //起始和发送一个字节后SCL均为0,数据线拉高,准备接收数据
for(i=0;i<8;i++)
{
I2C_SCL=1;
if(I2C_SDA){Byte|=(0x80>>i);} //10000000移位,将每一位收到的数据写入Byte
I2C_SCL=0; //SCL拉低,以便下一次SDA数据的变化
}
return Byte;
}
/**
* @brief I2C发送应答
* @param AckBit 应答位,0为应答,1为非应答
* @retval 无
*/
void I2C_SendAck(unsigned char AckBit)
{
I2C_SDA=AckBit; //数据位为应答位
I2C_SCL=1; //重置SCL引脚
I2C_SCL=0;
}
/**
* @brief I2C接收应答位
* @param 无
* @retval 接收到的应答位,0为应答,1为非应答
*/
unsigned char I2C_ReceiveAck(void)
{
unsigned char AckBit;
I2C_SDA=1; //接收应答位
I2C_SCL=1; //SCL引脚先拉高再置低
AckBit=I2C_SDA;
I2C_SCL=0;
return AckBit;
}
AR24C02是一款电可擦除芯片,其含有2K的存储空间,带有一个16字节的写缓存器,里面的数据掉电不丢失,基于I2C协议操作,具有写保护功能
我的A2开发板上AT24C02的封装如下
对应的引脚功能如下
我们要是想要操作这款外设,在I2C启动后我们需要先广播我们要控制的从机地址,这款芯片的固定地址位有4位,即1010
因此供我们编程的地址仅有3位,也代表我们最多可以同时操作8个AT24C02,为了简单起见,我们将这三个编程位均设为0,那么很显然,当我们要写入数据时,地址字节为0xA0,要读取数据时,地址字节为0xA1
在进行读写操作前,我们还需要规定好要写入数据的地址信息
在从机接收到器件地址和ACK应答后,接下来要接收的就是8位的字地址,接收到字地址后还需要发出一个ACK应答,完成后终止写操作,此时EEPROM才会进入其内部写数据周期,这期间所有操作无效,直至写周期完成EEPROM才会应答
由2K/16Byte = 128可知我们的芯片有128页即128个地址
和写入数据操作类似,我们只需要给出要读的字地址,就可以按字地址读取字节
#include
#include "I2C.h"
#define AT24C02_ADDRESS 0xA0 //定义器件地址
/**
* @brief AT24C02写入一个字节
* @param WordAddress 要写入字节的地址
* @param Data 要写入的数据
* @retval 无
*/
void AT24C02_WriteByte(unsigned char WordAddress,Data)
{
I2C_Start(); //启动I2C
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启动
I2C_SendByte(AT24C02_ADDRESS); //查找器件地址
I2C_ReceiveAck(); //接收应答
I2C_SendByte(WordAddress); //查找字地址
I2C_ReceiveAck(); //接收应答
I2C_Start(); //I2C重新启动,转换为读模式
I2C_SendByte(AT24C02_ADDRESS|0x01); //更改器件地址,为读字节模式
I2C_ReceiveAck(); //接收应答
Data=I2C_ReceiveByte(); //从EEPROM中读取数据
I2C_SendAck(1); //接收非应答,结束I2C
I2C_Stop();
return Data;
}