1. iic硬件电路
首先理解同步和异步,同步通信对于时间的要求没有那么高,一般需要一根时钟线。异步对于时钟要求就非常高,比如在发送接收数据的时候不能被中断打断去做其他的事情。因此可以区别串口和iic通信,串口异步通信,没有时钟线。iic也是两根通讯线,一根是时钟线,一根数据线(一根线兼具发送和接收),同步通信。这也可以解释为什么串口是全双工,而iic是半双工。
SCL: 任何时候都是主机掌握SCL,从机没有SCL的控制权,从机只能读。
SDA: 主机发送读取从机命令后或者从机应答的时候,从机才能短暂的取得SDA的控制权。
所有设备SCL和SDA都要配置为开漏输出
在SCL和SDA各添加了一个上拉电阻,起到一个弱上拉的作用。当总线上所有设备任意一个输出低电平,总线被拉至低电平。因为开漏输出输出高电平相当于浮空,可以认为是一个输入状态,因此在单双工模式下SDA就不用单独配置输入模式输出模式切换,同时只有所有的设备都输出高电平总线才会被拉至高电平,形成了线与。举个例子若主机设置为推挽输出,从机也是推挽输出,除非时序安排非常合理,否则可能出现主机输出1,从机输出0的情况,形成短路。
2. iic时序
起始结束SCL都为高电平
起始SDA在SCL高电平期间 高 - > 低
结束SDA在SCL高电平期间 低 - > 高
可以理解为开始空闲都为高有数据发送时拉低,发送完恢复空闲有拉高
注意区别串口,串口是低位先行
接收一个字节与发送一个字节时序基本没有差别,不过主机在接收之前,需要释放SDA(释放SDA就是把SDA开漏输出为1 相当于输入模式 因上拉电阻SDA电平拉高 所以后续写操作是1)
可以这样理解,所有的设备最开始都是输入模式(包括主机)。当主机想要发送数据的时候就去拉SDA,因为主机有大部分时间掌握SDA,所以当主机处于接收模式的时候就要释放SDA。
就是主机接收完一个字节后,发送一个应答位,表示接收到了。
主机发送完一个字节后,从机发送一个应答位,从机收到了。与此同时也要释放SDA(应为从机要发送数据,主机得释放SDA)
3. 帧数据
起始 + 1B(指定设备 = 设备地址7位 + 1位读写位(0写 1读 写:主机继续发送数据 读:从机开始发送数据)) +1bit RA(应答位)+2B(设备寄存器地址) +1bit RA +....nB(data)+ 停止位
这个有点复杂(这个用来读)
起始 + 1B(指定设备 = 设备地址7位 + 1位读写位 0 先写) + 1bit RA(0) +1B(此时指定要读的寄存器地址,此时只是把要写的地址写进去,但是从机不写) +1bitRA(0) + SR(新的起始位 :从新读必须伴随起始位) +1B(指定设备 = 设备地址7位 + 读 : 重新寻址) + 1bitRA(0) + 读取数据n+1bitRA(0) +停止
注意:因为写字节,读字节地址都是自增的,所以是把数据自动写入下一个地址,同理读数据也是,读完一个字节,地址自增可以读下一个字节数据。但是在读数据的时候,读到最后一个字节把应答位至1,是要告诉主机不继续操作了让主机收回SDA控制权。
写数据
读数据
S:起始位
AD:地址 R:读 W:写
ACK:应答位 NACK:非应答位
RA:寄存器地址
DATA:数据
P:停止位
4.软件模拟iic
选择两个io口配置为开漏输出,默认设置为高电平(释放总线)。
/*主机控制SCL电平*/
void MyI2C_W_SCL(u8 BitValue)
{
GPIO_WriteBit(GPIOB,GPIO_PIN_10,(BitAction)BitValue);
Delay_us(10); //为了让MPU6050反应过来
}
/*主机控制SDA电平*/
void MyI2C_W_SDA(u8 BitValue)
{
GPIO_WriteBit(GPIOB,GPIO_PIN_11,(BitAction)BitValue);
Delay_us(10); //为了让MPU6050反应过来
}
/*主机读取SDA电平*/
u8 MyI2C_R_SDA(void)
{
u8 BitValue;
BitValue = GPIO_ReadInputDataBit(GPIOB,GPIO_PIN_11);
Delay_us(10);
return BitValue;
}
/*起始START先释放sda和scl都为高电平,然后拉低sda和scl表示开始*/
void MyI2C_Start(void)
{
MyI2C_W_SDA(1);
MyI2C_W_SCL(1);
MyI2C_W_SDA(0);
MyI2C_W_SCL(0);
}
/*STOP,在SCL为高电平时SDA为低电平,为了保证SDA要有上升沿,所以先拉低SDA,再拉高SCL
终止条件SCL,SDA都是高电平*/
void MyI2C_STOP(void)
{
MyI2C_W_SDA(0);
MyI2C_W_SCL(1);
MyI2C_W_SDA(1);
}
/*发送一个字节,在SCL为低时更改SDA,从机在SCL为高的时候读取SDA,高位先行*/
void MyI2C_SendByte(u8 Byte)
{
u8 i;
for(i = 0; i < 8; i++)
{
MyI2C_W_SDA(Byte & (0x80 >> i));//此时SDA肯定是低
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
}
}
/*接收一个字节 同样也是在SCL高电平的时候读取SDA*/
u8 MyI2C_ReceiveByte(void)
{
u8 i,Byte = 0x00;
MyI2C_W_SDA(1);// 释放sda总线,把sda控制权交给从机
for(i = 0; i < 8; i++)
{
MyI2C_W_SCL(1);
if (MyI2C_R_SDA() == 1)
{
Byte |= (0x80 >> i);
}
MyI2C_W_SCL(0);
}
return Byte;
}
/*发送应答,就是发送一位,发送0表示应答,1表示不应答*/
void MyI2C_SendAck(u8 AckBit)
{
MyI2C_W_SDA(AckBit);
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
}
/*接收应答*/
u8 MyI2C_ReceiveAck(void)
{
u8 AckBit;
MyI2C_W_SDA(1);// 释放sda总线,把sda控制权交给从机
MyI2C_W_SCL(1);
AckBit = MyI2C_R_SDA();
MyI2C_W_SCL(0);
return AckBit;
}
主程序这边访问mpu6050 地址0xD0看看有没有ack
现象:ack值为0,主机收到从机发送的应答。
指定地址写程序
指定地址读程序
实际工程中IIC软件模拟更加常用,硬件IIC是调用相应库函数,对于接口要求较高,这里不做赘述。