一、简介
IIC是一种常用于集成电路间通信的通信协议。iic总线由两条线构成,即串行数据线(SDA)和串行时钟线(SCL)。所有IC总线上的设备都与这两条线相连,数据通过SDA线传输,时钟信号由SCL线提供。
总线上有主设备和从设备之分。主设备负责发起通信、控制传输过程,如微控制器;从设备则响应主设备的请求,如传感器、存储器等。
在stm32中有硬件iic和软件iic。硬件iic是stm32中集成的iic接口,通过这些接口来实现i2c的功能。除了硬件iic,我们还可以使用软件模拟的方式来实现iic的功能。正常情况下一般我会使用软件模拟的方式来实现,因为硬件iic十分复杂,而且不稳定。
二、工作原理
首先我们要知道iic属于同步半双工通信。通过SCL和SDA两条线进行数据传输以及同步。
SCL线主要用于同步数据传输,为I2C总线上的数据传输提供时钟基准。在数据传输过程中,SCL线的电平变化决定了数据传输的节奏,确保发送方和接收方在数据传输过程中保持同步。SCL信号是一个周期性的时钟信号,其电平在高电平和低电平之间交替变化。一个完整的时钟周期包括高电平阶段和低电平阶段。在SCL为高电平期间,数据必须保持稳定;只有在SCL为低电平时,数据才允许发生变化,以便接收方能够在稳定的时钟信号下准确地读取数据。
SDA线主要用于传输实际的数据和地址信息。在I2C通信中,所有的数据字节、设备地址以及应答信号等都是通过SDA线进行传输的。SDA线上的数据信号是随时间变化的串行信号,其电平状态(高电平或低电平)代表了传输的二进制数据0或1。在数据传输过程中,数据的每一位都在SCL时钟信号的控制下,按照一定的顺序在SDA线上进行传输。
总线结构了解后,再来看数据具体是如何传输的。
IIC的数据传输过程主要包括以下几个阶段:
1、首先主设备在SCL线为高电平时,将SDA线从高电平拉至低电平,产生起始信号,表明一次数据传输开始,此时总线被主设备占用,其他设备不能使用。
2、起始信号后,主设备发送一个字节的地址信息,高7位是从设备地址,最低位表示读写方向,0为写操作,1为读操作。从设备接收到地址后,与自身地址比较,若匹配则在第9个时钟周期将SDA线拉低,发送应答信号给主设备。
3、通过发送过来的指令来选择进行读/写操作。
写操作:主设备发送完地址字节且收到从设备应答后,开始发送数据字节,每发送一个字节,从设备接收后会发送应答信号,主设备可连续发送多个数据字节。
读操作:主设备发送地址字节和读方向位后,从设备应答,然后从设备开始发送数据字节,主设备接收数据,每接收一个字节,主设备会发送应答信号告诉从设备继续发送,若主设备不再需要数据,会在第9个时钟周期发送非应答信号。
4、主设备完成数据传输后,在SCL为高电平时,将SDA从低电平拉高到高电平,产生停止信号,释放总线,标志本次数据传输结束,其他设备此时可使用总线进行通信。
注:当多个主设备同时尝试使用总线时,会进行仲裁。仲裁过程中,每个主设备都在发送数据,若某个主设备发现自己发送的电平与总线上的实际电平不一致,就会退出竞争,直到总线上只剩下一个主设备,该主设备获得总线控制权,继续进行数据传输。
以下是iic的时序图
三、实现iic
了解完原理后我们来实现软件iic来传输数据。
首先我们要新建一个文件来储存我们软件模拟iic的代码。
在.h文件中写以下定义
//IO方向设置
#define SDA_IN() {GPIOC->CRH&=0XFFFF0FFF;GPIOC->CRH|=8<<12;}
#define SDA_OUT() {GPIOC->CRH&=0XFFFF0FFF;GPIOC->CRH|=3<<12;}
//IO操作函数
#define IIC_SCL PCout(12) //SCL
#define IIC_SDA PCout(11) //SDA
#define READ_SDA PCin(11) //输入SDA
//IIC所有操作函数
void IIC_Init(void); //初始化IIC的IO口
void IIC_Start(void); //发送IIC开始信号
void IIC_Stop(void); //发送IIC停止信号
void IIC_Send_Byte(u8 txd); //IIC发送一个字节
u8 IIC_Read_Byte(unsigned char ack);//IIC读取一个字节
void IIC_Ack(void); //IIC发送ACK信号
void IIC_NAck(void); //IIC不发送ACK信号
这里我用的是之前写的oled的iic定义,前面省略了部分定义。
这里可以看到我将SCL和SDA设为PC12和PC11.
然后我们来看每个函数的定义与作用。
首先先将iic进行初始化
//初始化IIC
void IIC_Init(void)
{
RCC->APB2ENR|=1<<4;//先使能外设IO PORTC时钟
GPIOC->CRH&=0XFFF00FFF;//PC11/12 推挽输出
GPIOC->CRH|=0X00033000;
GPIOC->ODR|=3<<11; //PC11,12 输出高
}
然后是start和stop两个函数
//产生IIC起始信号
void IIC_Start(void)
{
SDA_OUT(); //sda线输出
IIC_SDA=1;
IIC_SCL=1;
delay_us(4);
IIC_SDA=0;
delay_us(4);
IIC_SCL=0;//钳住I2C总线,准备发送或接收数据
}
//产生IIC停止信号
void IIC_Stop(void)
{
SDA_OUT();//sda线输出
IIC_SCL=0;
IIC_SDA=0;
delay_us(4);
IIC_SCL=1;
delay_us(4);
IIC_SDA=1;//发送I2C总线结束信号
}
这两个函数的意义其实就是对应上述数据传输中的第一步和最后一步,当我们要进行数据传输时,让SCL保持高电平,将SDA由高电平拉至低电平,产生起始信号,随后放开SCL,为之后的数据传输进行准备。当传输结束时,让SCL保持高电平,SDA由低电平向高电平转换,表示停止信号,释放总线。
然后是传输一个字节的数据。
void IIC_Send_Byte(u8 txd)
{
u8 t;
SDA_OUT();
IIC_SCL=0;//拉低时钟开始数据传输
for(t=0;t<8;t++)
{
IIC_SDA=(txd&0x80)>>7;
txd<<=1;
delay_us(2);
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
delay_us(2);
}
}
这个函数所表达的过程是主设备按照从高位到低位的顺序,在SCL线的每个时钟周期内发送一位数据。在SCL为低电平时,主设备将待发送的数据位放到SDA线上,在SCL变为高电平时,从设备读取SDA线上的数据。这样依次发送8位数据,完成一个字节的主体数据传输。
但当我们传输数据的时候计算机是不会知道我们究竟传了多少所以我们就需要以下两个函数来判断是否传输完毕。
void IIC_Ack(void)
{
IIC_SCL=0;
SDA_OUT();
IIC_SDA=0;
delay_us(2);
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
}
//不产生ACK应答
void IIC_NAck(void)
{
IIC_SCL=0;
SDA_OUT();
IIC_SDA=1;
delay_us(2);
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
}
以上面发送一个字节为例,主设备发送完一个字节的8位数据后,会释放SDA线,使其变为高阻态。从设备在第9个时钟周期时,根据自身是否成功接收数据来决定将SDA线拉低还是保持高电平。若拉低SDA线,表示应答信号(ACK),说明从设备已成功接收数据;若保持高电平,则表示非应答信号(NACK),告知主设备数据接收出现问题。主设备通过读取SDA线的电平状态来判断从设备是否正确接收数据。
如此我们就能实现读取数据了。
u8 IIC_Read_Byte(unsigned char ack)
{
unsigned char i,receive=0;
SDA_IN();//SDA设置为输入
for(i=0;i<8;i++ )
{
IIC_SCL=0;
delay_us(2);
IIC_SCL=1;
receive<<=1;
if(READ_SDA)receive++;
delay_us(1);
}
if (!ack)
IIC_NAck();//发送nACK
else
IIC_Ack(); //发送ACK
return receive;
}
这里只是iic的一个简单的运用,还有许多情况未考虑到,例如在多从机时如何处理。这里只是简单介绍和使用。