iic有两条线,节省了硬件资源,是半双工通信,因为有时钟线所以是实时通信。
以为iic节省了硬件资源,所以在硬件设计上较为巧妙
因为端口设置为开漏模式,在开漏模式下是可以读输入电平的(如下图),是一种弱驱动,无法输出高电平,所以在硬件电路上连接了一个4.7k的上拉电阻,当cpu输出高电平时,因为是开漏模式,pmos关闭,此时高电平由外接上拉电阻提供,又因为是上拉电阻,对数据线或时钟线拉高不是很灵敏。
iic协议主机对时钟线有绝对的控制权,对数据线(sda)可以拉高(释放总线)交由从机控制。
iic起始条件
SCL高电平期间,SDA从高电平切换到低电平(主机先把数据线拉低,再把时钟线拉低。)
iic终止条件
SCL高电平期间,SDA从低电平切换到高电平(主机先释放时钟线,再释放数据线)
发送一个字节:
SCL低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节
接收一个字节:
SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)
一句话总结因为主机从机都遵循IIC协议,所以对主机来说在时钟线低电平期间放数据,在时钟线高电平期间读数据
同理对于从机也是在低电平期间放数据,在高电平期间读数据。
发送应答:主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答
接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)
指定地址写(以mpu6050为例)
对于指定设备(Slave Address),在指定地址(Reg Address)下,写入指定数据(Data)
以上图为例,s代表start 起始信号,在scl高电平期间拉低sda(先拉低sda,再低scl),
第一个字节: 一般为从机的 7位寻址地址(可从厂家手册查找)+一位读写位(写0,读1),共八位,然后等待应答 。
第二个字节 :为从机寄存器地址, 等待应答,
第三个字节:要写入的数据
最后终止信号 : 在scl高电平期间拉高sda(先拉高scl,再拉高sda)
当前地址读
对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)
起始信号:在scl高电平期间拉低sda(先拉低sda,再低scl),
第一个字节: 一般为从机的 7位寻址地址(可从厂家手册查找)+一位读写位(写0,读1),共八位,然后等待应答 。(读最后寻址8位最后位 读写位,置1为读,同时拉高释放了sda,此时sda由从机拉低,从机控制sda)
第二个字节:要接收的数据,
等待应答,
最后停止条件在scl高电平期间拉高sda(先拉高scl,再拉高sda)
指定地址读
对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data)
起始信号:在scl高电平期间拉低sda(先拉低sda,再低scl
第一个字节: 一般为从机的 7位寻址地址(可从厂家手册查找)+一位读写位(写0,读1),共八位,然后等待应答 。
第二个字节 :为从机寄存器地址, 等待应答,
重复起始条件
第三个字节: 一般为从机的 7位寻址地址(可从厂家手册查找)+一位读写位(写0,读1),共八位,然后等待应答 。(读最后寻址8位最后位 读写位,置1为读,同时拉高释放了sda,此时sda由从机拉低,从机控制sda)
第四个字节:要接收的数据,等待应答
终止信号在scl高电平期间拉高sda(先拉高scl,再拉高sda)
代码实现
基于上面的时序编写软件代码(模拟i2c)
//初始化io口,并同时拉高(起始条件都是由高变低)
void myi2c_init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitTypeDef gpio_initstruct;
gpio_initstruct.GPIO_Mode=GPIO_Mode_Out_OD;
gpio_initstruct.GPIO_Pin=GPIO_Pin_10|GPIO_Pin_11;
gpio_initstruct.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&gpio_initstruct);
GPIO_SetBits(GPIOB, GPIO_Pin_10|GPIO_Pin_11);
}
//定义scl 写sda 读 sda 开漏模式下是可以读取输入引脚电平
static void myi2c_w_scl(uint8_t value)
{
GPIO_WriteBit(GPIOB,GPIO_Pin_10,(BitAction)value);
}
static void myi2c_w_sda(uint8_t value)
{
GPIO_WriteBit(GPIOB,GPIO_Pin_11,(BitAction)value);
}
static uint8_t myi2c_r_sda(void)
{
return GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
}
//起始条件
void myi2c_start(void)
{
myi2c_w_sda(1);
myi2c_w_scl(1);
myi2c_w_sda(0);
myi2c_w_scl(0);
}
//终止条件
void myi2c_stop(void)
{
myi2c_w_sda(0);
myi2c_w_scl(1);
myi2c_w_sda(1);
}
//发送一个字节,因为iic 协议高位先行,所以高位移向地位
void myi2c_sendbyte(uint8_t byte)
{
for(int i=0;i<8;i++)
{
myi2c_w_sda(byte&(0x80>>i));
myi2c_w_scl(1);
myi2c_w_scl(0);
}
}
//接受一个字节 高位先行
uint8_t myi2c_recvbyte(void)
{
uint8_t byte=0x00;
for(int i=0;i<8;i++)
{
myi2c_w_sda(1);
myi2c_w_scl(1);
if(myi2c_r_sda()){byte|=(0x80>>i);}
myi2c_w_scl(0);
}
return byte;
}
//发送应答
void myi2c_sendack(uint8_t ack)
{
myi2c_w_sda(ack);
myi2c_w_scl(1);
myi2c_w_scl(0);
}
//接收应答
uint8_t myi2c_recvack(void)
{
uint8_t ack;
myi2c_w_sda(1);
myi2c_w_scl(1);
ack=myi2c_r_sda();
myi2c_w_scl(0);
return ack;
}
起始条件为兼容指定地址读的重复起始 为保证在scl高电平期间sda拉低,所以先拉高sda。