I2C通信协议
I2C通信
- I2C(Inter IC Bus)是由Philips公司开发的一种通用数据总线
- 两根通信线:SCL(Serial Clock)、SDA(Serial Data)
SCL:串口时钟线:同步的时序,降低对硬件的依赖;
SDA:串口数据线:半双工,一根线兼具发送和接收;
- 同步,半双工
- 带数据应答
- 支持总线挂载多设备(一主多从、多主多从)
一主多从:单片机作为主机,主导I2C总线的运行,挂载在I2C总线的所有外部模块都是从机,从机只有被主机点名之后才能控制I2C总线------常用模型
在多主多从的情况下,需要额外调用SCL来达成同步,且复杂的协议

硬件电路(一主多从)
- 所有I2C设备的SCL连在一起,SDA连在一起
- 设备的SCL和SDA均要配置成开漏上拉输出模式
- SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右

禁止所有设备输出强上拉的高电平,采用外置弱上拉电阻加开漏输出的电路结构
开漏输出:原本的推挽输出,是上下都接入高低电平,如果导通则强上拉导通高电平,反之;现在不需要上面电路,当电流导通,会进入浮空状态。

所以 这就是对应的开漏输出,当导通后属于浮空状态,这时就需要电阻
,把此电路改成弱上拉;
好处:
一、完全杜绝了电源短路的现象,保证电路安全------无论电流高或低,都不会存在同时高电平和低电平的情况;
二、避免了引脚模式的频繁切换,开漏加弱上拉的模式,同时兼具了输入和输出的功能------想换高电平可直接导通即可,低电平就连GND即可;
三、存在"线与"的现象,指只要有任意一个或多个设备输出了低电平,总线就处于低电平,反之;因此可以利用这特性,实现执行多主机模式下的时钟同步和总线仲裁
I2C时序基本单元
- 起始条件:SCL高电平期间,SDA从高电平切换到低电平
- 终止条件:SCL高电平期间,SDA从低电平切换到高电平

- 发送一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节
低电平主机放数据,高电平从机读数据

- 接收一个字节:SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)
释放SDA:就证明切换为了输入模式,主机一般默认是输入模式,当需要发送的时候,会将SDA拉低;在被动接收的时候,就必须先释放SDA,避免有数据重叠
低电平从机放数据,高电平主机读数据。
此时从机获得控制权,实现表示主机控制部分,虚线表从机控制的部分;

- 发送应答:主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答
- 接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)
- 发送一位或接收一位:这一位作为应答

地址:当主机发送指令,去访问对应设备,这些设备就要有一个独立的地址:
在I2C协议标准里分为7位和10位地址:7位为例,且7位使用范围最广:
厂商会为它分配一个7位的地址,以MPU6050芯片为例,通过芯片手册可以得知是:
1101 000,一般器件地址的最后几位是可以在电路中改变,这里是1101 000x;用来区分同器件的不同地址,当对应引脚接低电平,地址为1101 0000,反之
I2C时序:
都是一个字节的操作
- 指定地址写
- 对于指定设备(Slave Address),在指定地址(Reg Address)下,写入指定数据(Data)
示波器:

- 当前地址读
- 对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)

- 但是对应读地址,并不能确定当前的读入的地址,所以需要使用当前地址指针,去指向指定地址读
- 对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data)

若要多个字节的操作,就需要把读和写的操作重复执行,这样时许就变为,在指定的位置开始按顺序连续读写入多个字节

MPU6050简介
- MPU6050是一个6轴姿态传感器,可以测量芯片自身X、Y、Z轴的加速度、角速度参数,通过数据融合,可进一步得到姿态角,常应用于平衡车、飞行器等需要检测自身姿态的场景
- 3轴加速度计(Accelerometer):测量X、Y、Z轴的加速度,能确定静态稳定性
- 3轴陀螺仪传感器(Gyroscope):测量X、Y、Z轴的角速度(绕X、Y、Z轴旋转的速度),能确定动态稳定性

我们通常都是把芯片内部的器件,想象为机器设备,至于怎么放进芯片的,就是公司的秘籍。
MPU6050参数
- 16位ADC采集传感器的模拟信号,分为2个字节存储,量化范围:-32768~32767
- 加速度计满量程选择:±2、±4、±8、±16(g:1g=9.8m/s)
- 陀螺仪满量程选择: ±250、±500、±1000、±2000(°/sec)
- 可配置的数字低通滤波器
- 可配置的时钟源
- 可配置的采样分频
- I2C从机地址:1101000(AD0=0)------7位转换:110 1000------0x68
1101001(AD0=1)
在之前的时序中,读写的操作可以先拆分为7位:0x68,再<<1作为从机操作地址;也可以作4分的地址:0xD0的从机地址,直接修改即可;
硬件电路


MPU6050框图

接线图
10-1 软件I2C读写MPU6050

整体架构:
I2C模块-初始化:GPIO口和6个时序基本单元;
MPU6050模块:基于I2C模块实现指定地址读写操作等
Main模块中,调用MPU6050模块
cpp
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyI2C.h"
#include "MPU6050.h"
//#include "OLED_Font.h"
//软件I2C读写MPU6050
int16_t AX,AY,AZ,GX,GY,GZ;
int main(void){
OLED_Init();
MPU6050_Init();
// MyI2C_Start();
// //根据通信协议,第一个内容必须是从机地址+读写位
// MyI2C_SendByte(0xD0); //1101 000 0 :MPU6050的地址+读写位
// uint8_t Ack=MyI2C_ReceiveAck();
// MyI2C_Stop();
// OLED_ShowNum(1,1,Ack,3);
// //返回000,符合说明MPU应答了,对此可以对其他的从机进行扫描(扫7位),返回为0的做记录。
//
// //读MPU的寄存器
// uint8_t ID=MPU_ReadingReg(0x75);//MPU手册中0x75:芯片的ID号
// OLED_ShowHexNum(1,1,ID,2);
// //写寄存器,需要先把寄存器的睡眠模式关了,否则无效,相关位置在电源管理器寄存器1的0x6B处
// MPU_WriteReg(0x6B,0x00);
//
// MPU_WriteReg(0x19,0xAA);
// uint8_t ID=MPU_ReadingReg(0x19);
// OLED_ShowHexNum(1,1,ID,2);
// 目前就是相当于存储器的读和写的操作,对于寄存器是与外部设备相关的
while(1){
MPU_Getdata(&AX, &AY, &AZ, &GX, &GY, &GZ); //获取MPU6050的数据
OLED_ShowSignedNum(2, 1, AX, 5); //OLED显示数据
OLED_ShowSignedNum(3, 1, AY, 5);
OLED_ShowSignedNum(4, 1, AZ, 5);
OLED_ShowSignedNum(2, 8, GX, 5);
OLED_ShowSignedNum(3, 8, GY, 5);
OLED_ShowSignedNum(4, 8, GZ, 5);
}
}
MyI2C.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
//对引脚的操作进行优化-用宏或函数都可以,只不过在某些大模块中需要去考虑延时
void MyI2C_W_SCL(uint8_t BitValue){
GPIO_WriteBit(GPIOB,GPIO_Pin_10,(BitAction)BitValue);
Delay_us(10);
}
void MyI2C_W_SDA(uint8_t BitValue){
GPIO_WriteBit(GPIOB,GPIO_Pin_11,(BitAction)BitValue);
Delay_us(10);
}
uint8_t MyI2C_R_SDA(void){
uint8_t BitValue=GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11);
Delay_us(10);
return BitValue;
}
//初始化I2C
void MyI2C_Init(void){
//软件调用:只需要调用GPIO的读写函数即可
//1.把SCL和SDA都初始化为开漏输出模式(既可以输出也可输入)
//2.把SCL和SDA置为高电平
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);
}
//在通讯中,随着主机SCL的变化,从机也会使SDA的电平进行对应的更改
//时序单元-开始
void MyI2C_Start(void){
//根据I2C通信的高低电平变化得知:先把SCL和SDA都释放,都输出1
//之后先拉低SDA,再拉低SCL
MyI2C_W_SDA(1);
MyI2C_W_SCL(1);//优先把SDA上去,兼容后续的重复的判断条件
MyI2C_W_SDA(0);
MyI2C_W_SCL(0);
}
//终止条件
void MyI2C_Stop(void){
MyI2C_W_SDA(0);//需要帮SDA先拉于低电平
//而SCL,在接收应答后默认就是低电平,所以不用拉低
//
MyI2C_W_SCL(1);
MyI2C_W_SDA(1);
}
//除了开始和结束,其他的操作都是让SCL以低电平的方式结束,方便单元的拼接
//发送一个字节
void MyI2C_SendByte(uint8_t Byte){
uint8_t i;
for(i=0;i<8;i++){
MyI2C_W_SDA(Byte&(0x80>>i)); //根据SDA传入的最高位来确定电平
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);//先释放再读取,是给从机读取之前的在SDA的数据,一段脉冲时间
}
}
//接收一个字节
//从机在SDA上,需要先把SDA释放,也就是设置为输入模式。当SCL低电平时,从机把数据放到SDA;SCL为高电平时,读取SDA;
//如果此时SDA出现乱动,则会被认为结束或开始的单元
uint8_t MyI2C_ReceiveByte(void){
uint8_t i, 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(uint8_t AckBit){
MyI2C_W_SDA(AckBit);
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
}
uint8_t MyI2C_ReceiveAck(void){
uint8_t AckBit=0x00;
MyI2C_W_SDA(1);
MyI2C_W_SCL(1);
AckBit=MyI2C_R_SDA();
MyI2C_W_SCL(0);
return AckBit;
}
MPU6050.c
#include "stm32f10x.h" // Device header
#include "MyI2C.h"
//在I2C的模块上运行
//宏定义对于的寄存器地址
#define MPU_ADD 0xD0
#include "MPU_Reg.h"
//封装指定地址写和指定地址读的时序
void MPU_WriteReg(uint8_t RegAddress,uint8_t Data){
MyI2C_Start();
//发送从机地址,做应答
MyI2C_SendByte(MPU_ADD);
MyI2C_ReceiveAck();
//发送对应的寄存器位置,做应答
MyI2C_SendByte(RegAddress);
MyI2C_ReceiveAck();
//发送数据进对应寄存器,做应答
MyI2C_SendByte(Data);
MyI2C_ReceiveAck();
MyI2C_Stop();
}
uint8_t MPU_ReadingReg(uint8_t RegAddress){
uint8_t Data;
MyI2C_Start();
//发送从机地址,做应答
MyI2C_SendByte(MPU_ADD);
MyI2C_ReceiveAck();
//发送对应的寄存器位置,做应答
MyI2C_SendByte(RegAddress);
MyI2C_ReceiveAck();
//读的时序,要先重新指定的读写位,所以要重新起始
MyI2C_Start();
//传入MPU写的时序
MyI2C_SendByte(MPU_ADD|0x01);
MyI2C_ReceiveAck();
//此时控制权给到从机,由从机进行发送,读后,需要把应答发给从机
Data=MyI2C_ReceiveByte();
MyI2C_SendAck(1);//最后一次应答发送1
MyI2C_Stop();
return Data;
}
void MPU6050_Init(void){
MyI2C_Init();
MPU_WriteReg(MPU6050_PWR_MGMT_1,0x01); //解除睡眠,选择陀螺仪时钟
MPU_WriteReg(MPU6050_PWR_MGMT_2,0x00); //6个轴均不待机
MPU_WriteReg(MPU6050_SMPLRT_DIV,0x09); //采样分频为10
MPU_WriteReg(MPU6050_CONFIG,0x06); //滤波参数最大
MPU_WriteReg(MPU6050_GYRO_CONFIG,0x18); //陀螺仪和加速度选择最大
MPU_WriteReg(MPU6050_ACCEL_CONFIG,0x18);
//此时的MPU就在进行大量的数据转换,数据存放在其他的寄存器里
}
//获取寄存器的数据
//6个返回值:XYZ的加速度值和陀螺仪值:多返回值的设计:1.外部定义6个全局变量;2.指针,进行变量的地址传递(实参传递);3.用结构体,成员实现参数打包
void MPU_Getdata(int16_t *AccX, int16_t *AccY, int16_t *AccZ,
int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{
//读取加速度寄存器XYZ轴的高8位和低8位
uint8_t DataH, DataL; //定义数据高8位和低8位的变量
DataH = MPU_ReadingReg(MPU6050_ACCEL_XOUT_H); //读取加速度计X轴的高8位数据
DataL = MPU_ReadingReg(MPU6050_ACCEL_XOUT_L); //读取加速度计X轴的低8位数据
*AccX = (DataH << 8) | DataL; //数据拼接,通过输出参数返回
DataH = MPU_ReadingReg(MPU6050_ACCEL_YOUT_H); //读取加速度计Y轴的高8位数据
DataL = MPU_ReadingReg(MPU6050_ACCEL_YOUT_L); //读取加速度计Y轴的低8位数据
*AccY = (DataH << 8) | DataL; //数据拼接,通过输出参数返回
DataH = MPU_ReadingReg(MPU6050_ACCEL_ZOUT_H); //读取加速度计Z轴的高8位数据
DataL = MPU_ReadingReg(MPU6050_ACCEL_ZOUT_L); //读取加速度计Z轴的低8位数据
*AccZ = (DataH << 8) | DataL; //数据拼接,通过输出参数返回
DataH = MPU_ReadingReg(MPU6050_GYRO_XOUT_H); //读取陀螺仪X轴的高8位数据
DataL = MPU_ReadingReg(MPU6050_GYRO_XOUT_L); //读取陀螺仪X轴的低8位数据
*GyroX = (DataH << 8) | DataL; //数据拼接,通过输出参数返回
DataH = MPU_ReadingReg(MPU6050_GYRO_YOUT_H); //读取陀螺仪Y轴的高8位数据
DataL = MPU_ReadingReg(MPU6050_GYRO_YOUT_L); //读取陀螺仪Y轴的低8位数据
*GyroY = (DataH << 8) | DataL; //数据拼接,通过输出参数返回
DataH = MPU_ReadingReg(MPU6050_GYRO_ZOUT_H); //读取陀螺仪Z轴的高8位数据
DataL = MPU_ReadingReg(MPU6050_GYRO_ZOUT_L); //读取陀螺仪Z轴的低8位数据
*GyroZ = (DataH << 8) | DataL; //数据拼接,通过输出参数返回
}