转载来自公众号: yuanfeng8586
I2C总线大家应该比较熟悉,即便没有写过驱动代码,但总也听过他的大名吧,哈哈,该总线广泛应用于各种通用芯片,诸如LCD驱动、数码管驱动、移动电源芯片、存储器等等,可以节省IO口数量,方便连线布板。
下面列举一下I2C的特点:
1,双线串行总线,由SCL时钟线和SDA数据线组成。
2,SCL和SDA都是双线总线,即要求参与通信的MCU IO口可以配置为输入和输出模式。
3,每次传输到总线的数据必须是8位,即一个字节,且高位在前。
4,连接到总线的器件有唯一的地址识别码,以便实现包含多I2C器件的系统控制。
5,数据传输时以“应答”机制来进行校验和同步。
6,支持多主机通信,采用仲裁方案协调。
下面以单主机单从机模型简要分析I2C总线数据传输时序,并用代码实现驱动。
I2C总线的硬件连接电路如下:
一,如以上原理图,总线空闲时SCL和SDA都为高电平。先做定义:
sbit I2C_SCL = IO1;
sbit I2C_SDA = IO2;
void SET_SDA_PIN_MODE(char mode); //当mode=IN时,SDA线为输入模式,当mode=OUT时,SDA线为输出模式
void READ_SDA(void); //读取SDA线电平状态
//设置I2C为空闲状态
void I2C_SET_IDLE(void)
{
I2C_SCL=1;
I2C_SDA=1;
}
二,I2C起始与停止时序,下图是时序图:
从时序上分析我们可以看到,I2C总线起始信号是当时钟线SCL为高电平时,数据线SDA由高电平跳变到电平。
//I2C起始函数
void I2C_START(void)
{
SET_SDA_PIN_MODE(OUT); //设置SDA线为输出模式
I2C_SCL=1; //根据时序图SCL线设置高电平
I2C_SDA=1; //根据时序图SDA线设置高电平,以产生下降沿
delay_us(5); //保持
I2C_SDA=0; //产生下降沿
delay_us(5);
I2C_SCL=0; //拉低时钟线,准备通信
}
I2C停止信号从时序图上可以看出,是当SCL为高电平时,SDA线由低电平产生到高电平的一个上升沿,以后都是高电平,进入空闲状态。
//I2C停止函数
void I2C_STOP(void)
{
SET_SDA_PIN_MODE(OUT); //设置SDA线为输出模式
I2C_SCL=1; //设置SCL为高电平
I2C_SDA=0; //设置SDA为低电平,以便产生上升沿
delay_us(5); //保持
I2C_SDA=1; //产生上升沿
delay_us(5);
}
三,I2C传输数据时必须产生应答信号,作为校验和同步传输使用。从机产生应答信号的时序如下图:
可以看出,应答信号是在一个字节数据传输完成后的下一个时钟产生,应答条件为SCL时钟变化过程中要求SDA数据线保持低电平。
//I2C产生应答函数
void I2C_SEND_ACK(void)
{
SET_SDA_PIN_MODE(OUT); //设置SDA线为输出模式
I2C_SDA=0; //设置SDA为低电平
I2C_SCL=0; //设置SCL为低电平,以便产生时钟跳变
delay_us(5);
I2C_SCL=1; //产生SCL时钟跳变
delay_us(5);
I2C_SCL=0;
}
如果从机由于内存不足或者其他原因不能接受数据,也可以发送非应答信号给主机,通知主机停止数据传输。时序如下图:
可以看出,非应答信号和应答信号正好相反,是在传输完一个字节数据后,在时钟线SCL跳变时,SDA一直保持为高电平。
//I2C产生非应答信号
void I2C_SEND_NACK(void)
{
SET_SDA_PIN_MODE(OUT); //设置SDA线为输出模式
I2C_SDA=1; //设置SDA为高电平
I2C_SCL=0; //设置SCL为低电平,以便产生时钟跳变
delay_us(5); //保持
I2C_SCL=1;
delay_us(5);
I2C_SCL=0;
}
四,主机等待应答,当接收到应答信号时继续传输数据,若接收不到则终止传输。时序图和产生应答信号一致,反向处理,即当时钟线SCL产生跳变时检测数据线SDA,检查SDA是否在这一过程中一直处于低电平,若是则是从机有应答,否则为无应答。
//I2C等待应答信号,若返回为0则为有应答,返回为1无应答
unsigned char I2C_WAIT_ACK(void)
{
unsigned char time_out=0;
SET_SDA_PIN_MODE(IN); //设置SDA线为输入模式,以便读取数据线状态
I2C_SCL=0;
delay_us(5);
I2C_SCL=1;
delay_us(5);
while(READ_SDA()) //在SCL时钟跳变时读取SDA数据线电平状态
{
time_out++; //滤掉毛刺,若SDA始终为高电平,则为非应答
if(time_out>1000)
{
time_out=0;
I2C_STOP(); //等待不到应答信号则产生停止信号,终止传输数据
return 1;
}
}
I2C_SCL=0;
return 0;
}
五,进行数据传输,在I2C起始信号以后,可以进行数据传输,传输数据时需要注意,在SDA数据线上传输的数据必须在SCL时钟信号周期内保持稳定,否则数据无效。SDA数据线的高低电平变化只可以在SCL时钟线为低电平时变化。且每次8位,先传输高位。时序图如下:
参照时序图,写出发送一个字节的驱动代码:
//I2C发送单个字节函数,函数参数接收要发送的数据
void I2C_SEND_BYTE(unsigned char dat)
{
unsigned char i=0;
SET_SDA_PIN_MODE(OUT); //设置SDA线为输出模式
for(i=0;i<8;i++)
{
//在时钟线SCL跳变之前先准备好数据待传输
if(dat&0x80) //分离出待传输数据的最高位
I2C_SDA=1; //若最高位是1则SDA线是高电平,否则是低电平
else
I2C_SDA=0;
//产生时钟跳变,发送数据
I2C_SCL=0;
delay_us(5);
I2C_SCL=1;
delay_us(5);
I2C_SCL=0;
//发送完一个位后数据移位,发送下一个位的数据
dat<<=1;
}
从SDA数据线上读取数据时操作过程和上面正好相反,即在时钟线SCL每发生一次跳变时可以从SDA数据线读取到一位数据,先读取到的是高位。驱动代码如下:
//I2C从SDA数据线读取一个字节函数,返回数据
unsigned char I2C_READ_BYTE(void)
{
unsigned char i=0,dat=0;
SET_SDA_PIN_MODE(IN); //设置SDA线为输入模式,以便读取数据线状态
for(i=0;i<8;i++)
{
//时钟线产生上升沿
I2C_SCL=0;
delay_us(5);
I2C_SCL=1;
delay_us(5);
dat<<=1; //接收到的数据先左移一位,等待然后接收下一位,这样接收完成后最开始接收到的数据就移位到最高位了
if(READ_SDA())
dat++;
I2C_SCL=0; //SCL产生下降沿,时钟跳变完成
}
I2C_SEND_ACK(); //读取完成一个字节返回一个应答
}
以上基本的总线驱动已经完成,应用到具体芯片,只需要跟进读写时序进行调用即可,注意一般支持I2C的芯片地址为7位,第8位是读写位,一般0为写操作,1为读操作。比如一个芯片的七位地址为0B1010,000,十六进制为0XA0,考虑读写位时,进行读时需要先写入0XA1,进行写操作时先写入0XA0,如下参考代码:
//实例应用代码
void I2C_WRITE_DAT_to_CHIP(unsigned char dat) //主机MCU发送数据给芯片
{
I2C_START(); //起始信号,准备传输数据
I2C_SEND_BYTE(0XA0); //寻址,写入写命令;
I2C_WAIT_ACK(); //等待应答
I2C_SEND_BYTE(dat); //写入需要的数据
I2C_STOP(); //发送完成后停止
}
unsigned char I2C_READ_DAT_FORM_CHIP(void) //主机MCU从芯片读取数据
{
unsigned char dat;
I2C_START(); //起始信号,准备接收数据
I2C_SEND_BYTE(0XA1); //寻址,写入读命令;
I2C_WAIT_ACK(); //等待应答
dat=I2C_READ_BYTE(); //写入需要的数据
I2C_SEND_ACK(); //接收完一个字节产生一个应答
I2C_STOP(); //接收完成后停止
return dat;
} |