一、描述
I2C(Inter-Integrated Circuit),有时也称为IIC或TWI(Two-Wire Interface),是一种用于低速度串行通信的总线协议,主要用于连接微控制器与其外围设备,如传感器、EEPROM、RTC(实时时钟)等。I2C 由飞利浦半导体(现今的恩智浦半导体)在1980年代初期开发。
I2C 通信完成信息的交互仅需两根线:
SDA(Serial Data):数据线,用于数据的双向传输。
SCL(Serial Clock):时钟线,用于同步信号。
二、硬件电路
I2C总线是一种主从结构(Master/Slave)总线,一个总线上可以挂载多设备,有两种模式:一主多从、多主多从;
- 在一主多从模式下,I2C总线上有一个主设备(Master)和多个从设备(Slave)。
- 主设备负责控制总线上的通信,包括初始化总线、发送时钟信号、发送从设备地址以及读写数据等。
- 从设备则根据主设备的指令进行响应,如发送应答信号、发送数据或接收数据。
- 每个从设备都有一个唯一的地址,主设备通过发送从设备地址来选择与哪个从设备进行通信。
- 在多主多从模式下,I2C总线上可以连接多个主设备和多个从设备。
- 当多个主设备同时尝试访问总线时,需要通过仲裁机制(Arbitration Mechanism)来决定哪个主设备能够继续进行通信。
- 仲裁机制基于总线线“与”逻辑原理,当有一个节点发送低电平时,总线上表现为低电平,而发送高电平的节点将会失去仲裁。
三、数据传输协议
主设备和从设备进行数据传输时遵循以下协议格式。数据通过一条SDA数据线在主设备和从设备之间传输0和1的串行数据。串行数据序列的结构可以分为:
(1)开始位和停止位
当主设备决定开始进行通讯时,需要给从设备发送一个开始信号,同时需要在通讯结束时发送一个停止信号,进而从设备才能知道我什么时候开始什么时候结束:
当主设备发送开始信号之后,所有从机即使处于睡眠模式也将变为活动模式,并等待接收地址位
//软件写SDA和SCL
void MyI2C_W_SDA(uint8_t BitVal)
{
GPIO_WriteBit (GPIOB,GPIO_Pin_11,(BitAction)BitVal);
Delay_us (10);
}
void MyI2C_W_SCL(uint8_t BitVal)
{
GPIO_WriteBit (GPIOB,GPIO_Pin_10,(BitAction)BitVal);
Delay_us (10);
}
//开始位
void MyI2C_Start()
{
MyI2C_W_SDA (1);//拉高SDA和SCL
MyI2C_W_SCL (1);
MyI2C_W_SDA (0);//SDA先出现下降沿
MyI2C_W_SCL (0);//然后再到SCL
}
//停止位
void MyI2C_Stop()
{
MyI2C_W_SDA (0);// 首先确保SDA被拉低
MyI2C_W_SCL (1);// 释放SCL线
MyI2C_W_SDA (1);// 在SCL保持高电平期间,将SDA从低电平拉高
}
(2)地址位
地址位支持7bit、10bit,主设备如果需要向从机发送/接收数据,首先要发送对应从机的地址,然后会匹配总线上挂载的从机的地址,故地址为主要用来辨识不同设备,也就是主机告诉从设备们,现在我需要指定谁来进行通信的一个过程。进而,从设备负责接受并识别该地址是否是自己的地址,如果是就进行下一步的。
(3)读写位
由于I2C是半双工通讯,所有设备需要确定数据传输的方向,因此写入了读写位,去告诉从机我是要进行读的操作还是写入的操作
如果主设备需要将数据发送到从设备,则该位设置为 0;
如果主设备需要往从设备接收数据,则将其设置为 1 ;
读写位由主机发送;1表示读操作,0表示写操作。
(4)应答位RA(Receive Ack)
I2C最大的一个特点就是有完善的应答机制,从机接收到主机的数据时,会回复一个应答信号来通知主机表示“我收到了”/“我还没收到”。
应答信号: 出现在1个字节传输完成之后,即第9个SCL时钟周期内,此时主机需要释放SDA总线,把总线控制权交给从机,由于上拉电阻的作用,此时总线为高电平,如果从机正确的收到了主机发来的数据,会把SDA拉低,表示应答响应。
RA=0应答响应 RA=1应答未响应
void MyI2C_SendAck(uint16_t Byte)
{
uint8_t Ack;
MyI2C_W_SDA (Ack);
MyI2C_W_SCL (1);
MyI2C_W_SCL (0);
}
uint8_t MyI2C_ReciveAck(void)
{
uint8_t ReciveAck;
MyI2C_W_SDA (1);
MyI2C_W_SCL (1);
ReciveAck = MyI2C_R_SDA ();
MyI2C_W_SCL (0);
return ReciveAck ;
}
(4)数据位
I2C数据总线传输要保证:在SCL为高电平时,SDA数据稳定,所以SDA想要改变传输数据时只能在SCL为低电平时,SCL位高电平时SDA不能进行数据的一个修改了,因为当SCL高电平时主设备就会进行数据的读取,就例如你交卷了就不能修改答案。
一次传输的数据总共有8位,由发送方设置,它需要将数据位传输到接收方。发送之后会紧跟一个ACK / NACK位,如果接收器成功接收到数据,则从机发送ACK(ACknowledge应答信号)。否则,从机发送NACK(No acknowledge)。
数据可以重复发送多个,直到接收到停止位为止。
四、数据的发送和接收
//发送字节
void MyI2C_SendByte(uint16_t Byte)
{
uint8_t i;
for(i=0;i<8;i++)
{
MyI2C_W_SDA (Byte & ( 0x80>> i));
MyI2C_W_SCL (1);
MyI2C_W_SCL (0);
}
}
uint16_t MyI2C_ReciveByte()
{
uint8_t i;
uint16_t Byte=0x00;
MyI2C_W_SDA (1);
for(i=0;i<8;i++)
{
MyI2C_W_SCL (1);
if(MyI2C_R_SDA ()== 1){Byte |= (0x80>>i);}
MyI2C_W_SCL (0);
}
return Byte ;
}
其实数据传输步骤都是根据前面的协议进行,只不过接收方和发送方在接受和发送时发生了转变,这里刚开始看的时候可能会有点稀里糊涂,但只是对象发生转变而已。其中释放SCL可以理解为放手把SDA的控制权交给另外一方,主机未接收数据时,SDA是掌握在主机手里,这时候主机需要交出SDA掌控权给从机,让从机把数据发送出去,进而主机才能够进行数据的接收。而主机进行写操作时是不需要释放的,因为主机要写入数据,自然要掌握SDA的控制权。
五、I2C时序
(1)指定地址写
(2)当前地址读
可以看到当前地址读相对于前面的指定地址读缺少了第二步,就是没有指定是哪一个地址,此时当前地址指针指向谁就给谁进行读的操作,如果下一次还进行当前地址读的操作,此时地址指针自动加一。
- 主机在释放SDA线后,开始接收从机当前地址指针所指向位置的数据。
(3)指定地址读
仔细观察会发现:指定地址读是由指定地址写的前半部+当前地址读的后半部分组成
因为我们需要先指定对哪个从设备进行读的操作,此时我们就需要写入这个地址,让地址指针指向那,进而才继续进行当前地址读的操作。
六、SDA线的仲裁
总线仲裁是为了解决多设备同时竞争中线控制权的问题,通过一定的裸机来决定哪个设备能够获得最终的总线控制权。
SDA线的仲裁也是建立在总线具有线“与”逻辑功能的原理上的。节点在发送1位数据后,比较总线上所呈现的数据与自己发送的是否一致(类似于CAN总线的回读机制)。
是,继续发送;
否则,退出竞争;
I2C总线的控制逻辑:低电平优先
SDA线的仲裁可以保证I2C总线系统在多个主节点同时企图控制总线时通信正常进行并且数据不丢失,总线系统通过仲裁只允许一个主节点可以继续占据总线。
七、软件读写MPU6050代码:
(1)配置I2C:
#include "stm32f10x.h" // Device header
#include "Delay.h"
void MyI2C_W_SDA(uint8_t BitVal)
{
GPIO_WriteBit (GPIOB,GPIO_Pin_11,(BitAction)BitVal);
Delay_us (10);
}
void MyI2C_W_SCL(uint8_t BitVal)
{
GPIO_WriteBit (GPIOB,GPIO_Pin_10,(BitAction)BitVal);
Delay_us (10);
}
uint16_t MyI2C_R_SDA(void)
{
uint16_t DATA;
DATA = GPIO_ReadInputDataBit (GPIOB ,GPIO_Pin_11 );
Delay_us (10);
return DATA;
}
void MyI2C_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_SetBits (GPIOB,GPIO_Pin_10 | GPIO_Pin_11);
}
void MyI2C_Start()
{
MyI2C_W_SDA (1);
MyI2C_W_SCL (1);
MyI2C_W_SDA (0);
MyI2C_W_SCL (0);
}
void MyI2C_Stop()
{
MyI2C_W_SDA (0);
MyI2C_W_SCL (1);
MyI2C_W_SDA (1);
}
void MyI2C_SendByte(uint16_t Byte)
{
uint8_t i;
for(i=0;i<8;i++)
{
MyI2C_W_SDA (Byte & ( 0x80>> i));
MyI2C_W_SCL (1);
MyI2C_W_SCL (0);
}
}
uint16_t MyI2C_ReciveByte()
{
uint8_t i;
uint16_t Byte=0x00;
MyI2C_W_SDA (1);
for(i=0;i<8;i++)
{
MyI2C_W_SCL (1);
if(MyI2C_R_SDA ()== 1){Byte |= (0x80>>i);}
MyI2C_W_SCL (0);
}
return Byte ;
}
void MyI2C_SendAck(uint16_t Byte)
{
uint8_t Ack;
MyI2C_W_SDA (Ack);
MyI2C_W_SCL (1);
MyI2C_W_SCL (0);
}
uint8_t MyI2C_ReciveAck(void)
{
uint8_t ReciveAck;
MyI2C_W_SDA (1);
MyI2C_W_SCL (1);
ReciveAck = MyI2C_R_SDA ();
MyI2C_W_SCL (0);
return ReciveAck ;
}
(2)MPU6050代码配置:
#include "stm32f10x.h" // Device header
#include "MPU6050_Register.h"
#include "MyI2C.h"
#define MPU6050_Addgress 0xD0
void MPU6050_W_REG(uint16_t WriteRegister,uint16_t Data)
{
MyI2C_Start ();
MyI2C_SendByte (MPU6050_Addgress);
MyI2C_ReciveAck ();
MyI2C_SendByte (WriteRegister );
MyI2C_ReciveAck ();
MyI2C_SendByte (Data);
MyI2C_ReciveAck ();
MyI2C_Stop ();
}
uint16_t MPU6050_R_REG(uint16_t ReadRegister)
{
uint16_t Data;
MyI2C_Start ();
MyI2C_SendByte (MPU6050_Addgress);
MyI2C_ReciveAck ();
MyI2C_SendByte (ReadRegister);
MyI2C_ReciveAck ();
MyI2C_Start ();
MyI2C_SendByte (MPU6050_Addgress | 0x01);
MyI2C_ReciveAck ();
Data = MyI2C_ReciveByte ();
MyI2C_SendAck (1);
MyI2C_Stop ();
return Data;
}
void MPU6050_Init(void)
{
MyI2C_Init ();
MPU6050_W_REG (MPU6050_PWR_MGMT_1,0x01);
MPU6050_W_REG (MPU6050_PWR_MGMT_2,0x00);
MPU6050_W_REG (MPU6050_SMPLRT_DIV,0x09);
MPU6050_W_REG (MPU6050_CONFIG,0x06);
MPU6050_W_REG (MPU6050_GYRO_CONFIG,0x18);
MPU6050_W_REG (MPU6050_ACCEL_CONFIG,0x18);
}
uint16_t MPU6050_GetID(void)
{
return MPU6050_R_REG (MPU6050_WHO_AM_I);
}
void MPU6050_GetData(int16_t *ACCx,int16_t *ACCy,int16_t *ACCz,
int16_t *GYROx,int16_t *GYROy,int16_t *GYROz)
{
uint8_t DataH,DataL;
DataH = MPU6050_R_REG (MPU6050_ACCEL_XOUT_H);
DataL = MPU6050_R_REG (MPU6050_ACCEL_XOUT_L);
*ACCx = (DataH << 8) | DataL;
DataH = MPU6050_R_REG (MPU6050_ACCEL_YOUT_H);
DataL = MPU6050_R_REG (MPU6050_ACCEL_YOUT_L);
*ACCy = (DataH << 8) | DataL;
DataH = MPU6050_R_REG (MPU6050_ACCEL_ZOUT_H);
DataL = MPU6050_R_REG (MPU6050_ACCEL_ZOUT_L);
*ACCz = (DataH << 8) | DataL;
DataH = MPU6050_R_REG (MPU6050_GYRO_XOUT_H);
DataL = MPU6050_R_REG (MPU6050_GYRO_XOUT_L);
*GYROx = (DataH << 8) | DataL;
DataH = MPU6050_R_REG (MPU6050_GYRO_YOUT_H);
DataL = MPU6050_R_REG (MPU6050_GYRO_YOUT_L);
*GYROy = (DataH << 8) | DataL;
DataH = MPU6050_R_REG (MPU6050_GYRO_ZOUT_H);
DataL = MPU6050_R_REG (MPU6050_GYRO_ZOUT_L);
*GYROz = (DataH << 8) | DataL;
}
(3)MPU6050的寄存器地址定义
#ifndef __MPU6050_REG_H
#define __MPU6050_REG_H
#define MPU6050_SMPLRT_DIV 0x19
#define MPU6050_CONFIG 0x1A
#define MPU6050_GYRO_CONFIG 0x1B
#define MPU6050_ACCEL_CONFIG 0x1C
#define MPU6050_ACCEL_XOUT_H 0x3B
#define MPU6050_ACCEL_XOUT_L 0x3C
#define MPU6050_ACCEL_YOUT_H 0x3D
#define MPU6050_ACCEL_YOUT_L 0x3E
#define MPU6050_ACCEL_ZOUT_H 0x3F
#define MPU6050_ACCEL_ZOUT_L 0x40
#define MPU6050_TEMP_OUT_H 0x41
#define MPU6050_TEMP_OUT_L 0x42
#define MPU6050_GYRO_XOUT_H 0x43
#define MPU6050_GYRO_XOUT_L 0x44
#define MPU6050_GYRO_YOUT_H 0x45
#define MPU6050_GYRO_YOUT_L 0x46
#define MPU6050_GYRO_ZOUT_H 0x47
#define MPU6050_GYRO_ZOUT_L 0x48
#define MPU6050_PWR_MGMT_1 0x6B
#define MPU6050_PWR_MGMT_2 0x6C
#define MPU6050_WHO_AM_I 0x75
#endif