I2C通信协议
- I2C总线是由飞利浦公司开发的一种通用数据总线;
- 两根通信线:SCL(Serial Clock)、SDA(Serial Data);
- 同步时钟,半双工;
- 带数据应答;
- 支持在总线上挂载多个设备(一主多从、多主多从)
I2C的硬件电路

硬件电路最重要的特征是"弱上拉+开漏输出",当SCL、SDA释放总线时,弱上拉将电平拉高;当SCL、SDA控制总线时,强下拉将电平拉低。
I2C的六大时序要素
分别是:起始条件、终止条件、发送一个字节,接收一个字节,发送应答、接收应答。
起始条件
当SCL为高电平时,SDA从高电平变为低电平。
终止条件
当SCL为高电平时,SDA从低电平变为高电平。

注:除了产生起始条件和终止条件时,其余情况下,SCL为高电平时,SDA的电平不允许有变化。
发送一个字节
SCL为低电平期间,主机将数据依次放到SDA上(高位先行,且一次只能放一个bit位的数据),当SCL释放变为高电平时,从机到SDA读取数据(一次读取一个bit位数据),上述过程循环8次后,可完整发送一个字节数据。

接收一个字节
主机先释放SDA交出数据总线控制权,当SCL低电平期间,从机将数据位放到SDA总线上(高位先行),当SCL高电平时,主机到SDA总线上读取数据位,重复上述步骤8次,即可完整接收一个字节数据。

发送应答
主机在接收到一个字节数据后,在下一个时钟给从机发送一位数据,数据0为应答,数据1为非应答。(发送数据的应答位的过程和发送一个bit位数据的过程一致)

接收应答
主机在发送一个字节后,先释放SDA,在下一个时钟接收一位数据,数据0应答,数据1非应答。

以上六个时序要素构成了I2C协议的完整时序。
指定地址写
对于指定从设备(Slave Address),在指定地址下(Reg Adddress),写入指定数据(Data)。

首先触发起始条件,再对从机地址和I2C模式选择写入,进行接收应答,再对要写入的寄存器地址进行写入,进行接收应答,再对要发送到指定地址的具体数据进行发送,进行接收应答(要发送多个字节数据就重复这个步骤),最后触发终止条件。
指定地址读
对于指定从设备(Slave Address),在指定地址下(Reg Adddress),读出指定数据(Data)。
指定地址读 = 指定地址写的前两步 + 当前地址读
当前地址读的时序如下图:

指定地址读的时序图如下:

首先触发起始条件,再对从机地址和I2C模式选择写入,进行接收应答,再对要写入的寄存器地址进行写入,进行接收应答,再次触发起始条件,再对从机地址和I2C模式选择读取,进行接收应答,对指定地址的数据进行读取,进行发送应答(需要接收多个数据时,重复此步骤),最后触发终止条件。
MPU6050
简介
- MPU6050是一个6轴姿态传感器,可以测量芯片自身的X、Y、Z轴的加速度角、角速度参数,通过数据融合,可进一步得到姿态角,常应用于平衡车、飞行器等需要检测自身姿态的场景;
- 3轴加速度计(Acceleromrter):测量X、Y、Z轴的加速度;
- 3轴陀螺仪传感器(Gyroscope):测量X、Y、Z轴的角速度;(3+3 = 6轴)
参数
- 16位ADC采集传感器的模拟信号,量化范围:-32768~32767;
- 加速度计满量程选择:±2、±4、±8、±16(g);
- 陀螺仪满量程选择:±250、±500、±1000、±2000(°/sec);
- 可配置的数字低通滤波器;
- 可配置的时钟源;
- 可配置的时钟源;
- 12C从机地:1101000 (AD0=0)
- 1101001 (AD0=1)
以上参数均在对MPU6050芯片进行初始化时,对相应寄存器进行写入来配置,最后一个参数AD0的选择可以直接在模块AD0引脚上接上高低电平来控制。
硬件电路

如上图所示为MPU6050的硬件电路图。

上图为MPU6050的一个整体结构框图。
一、软件I2C
本实验将MPU6050的SDA、SCL引脚连接到单片机的PB10、PB11引脚,用软件模拟I2C通信,将MPU6050的加速值和角速度值传输至单片机。
程序
先对I2C模块进行编写,MyI2C.c文件:
#include "stm32f10x.h" // Device header
#include "Delay.h"
//函数:将对SCL总线的写操作进行封装
void MyI2C_W_SCL(uint16_t Value)
{
GPIO_WriteBit (GPIOB ,GPIO_Pin_10,(BitAction)Value);//对连接SCL的PB10进行电平的写入
Delay_us (10);//延时一段时间,生成波形,稳定时序
}
//函数:对SDA总线的电平进行写入
void MyI2C_W_SDA(uint16_t Value)
{
GPIO_WriteBit (GPIOB ,GPIO_Pin_11,(BitAction)Value);//对连接SDA的PB11进行电平的写入
Delay_us (10);//延时一段时间,生成波形,稳定时序
}
//函数:对SDA总线的电平进行读取
uint8_t MyI2C_R_SDA(void)
{
uint16_t data;//创建变量,用于存储读取的变量
data = GPIO_ReadInputDataBit (GPIOB,GPIO_Pin_11);//读取电平
Delay_us (10);//延时一段时间,生成波形,稳定时序
return data;//返回读取值
}
//函数:对I2C的软件初始化
void MyI2C_Init(void)
{
//打开GPIOB的时钟
RCC_APB2PeriphClockCmd (RCC_APB2Periph_GPIOB ,ENABLE);
//对PB10、PB11进行初始化配置,这里配置为开漏输出模式
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 );
}
//函数:触发起始条件
void MyI2C_Start(void)
{
//确保SCL为高电平
MyI2C_W_SCL (1);
//确保SDA为高电平
MyI2C_W_SDA (1);
//当SCL为高电平时,将SDA由高电平切换为低电平
MyI2C_W_SDA (0);
//初始条件触发完成后,将SCL释放为高电平
MyI2C_W_SCL (0);
}
//函数:触发终止条件
void MyI2C_Stop(void)
{
//提前控制将SDA电平拉低
MyI2C_W_SDA(0);
//确保SCL为高电平
MyI2C_W_SCL(1);
//当SCL为高电平时,将SDA由低电平切换为高电平
MyI2C_W_SDA(1);
}
//函数:发送一个字节数据
void MyI2C_SendData(uint8_t Data)
{
//创建一个变量,用于存储循环次数
uint8_t i;
//用for循环将一个字节数据一个bit一个bit的发送
for(i=0;i<8;i++)
{
//按位与逻辑实现对每一位数据的发送
MyI2C_W_SDA(Data & (0x80 >> i));
//发送完主机将SCL释放拉高
MyI2C_W_SCL(1);
//在SCL高电平期间从机到SDA上进行数据的读取
//读取后主机将SCL电平拉低
MyI2C_W_SCL(0);
}
}
//函数:主机接收一个字节的数据
uint8_t MyI2C_ReceiveData(void)
{
//创建一个变量,用于存储接收到的数据
uint8_t Data =0x00;
//创建一个变量,用于存储循环次数
uint8_t i;
//在接收数据之前主机将SDA释放交出控制权
MyI2C_W_SDA(1);
//用for循环将一个字节数据一个bit一个bit的接收
for(i=0;i<8;i++)
{
//在SCL低电平期间从机会把数据写入SDA中
//将SCL变为高电平
MyI2C_W_SCL(1);
//当SCL高电平期间,主机到SDA中读取数据
Data |= MyI2C_R_SDA() << (7 - i);
//读取完之后,将SCL电平拉低
MyI2C_W_SCL(0);
}
//返回读取到的数据
return Data;
}
//函数:发出应答;0为应答,1为非应答
void MyI2C_SendACK(uint8_t Value)
{
//在接收完一个字节数据后,SCL低电平期间,主机向SDA中写入应答位
MyI2C_W_SDA(Value);
//SCL释放变为高电平
MyI2C_W_SCL(1);
//在SCL高电平期间,从机读取SDA中的应答位
//读取完成后,将SCL的电平拉低
MyI2C_W_SCL(0);
}
//函数:接收应答
uint8_t MyI2C_ReceiveACK(void)
{
//定义变量,用于存储接收到的应答位
uint8_t ACK;
//在发送完一个字节数据之后,先将SDA释放为高电平交出控制权
MyI2C_W_SDA(1);
//在SCL低电平期间,从机会将应答位数据写入SDA总线
//主机再将SCL电平拉高
MyI2C_W_SCL(1);
//在SCL高电平期间,主机到SDA总线中读取应答位数据
ACK = MyI2C_R_SDA();
//读取后将SCL电平拉低
MyI2C_W_SCL(0);
//返回读取到的应答位数据
return ACK;
}
这八个基础时序要素均只能从主机的角度去写,不能涉及从机的操作,时序到了,从机会自动进行应答。
MyI2C.h文件:
#ifndef _MYI2C_H
#define _MYI2C_H
void MyI2C_Init(void);
void MyI2C_Start(void);
void MyI2C_Stop(void);
void MyI2C_SendData(uint8_t Data);
uint8_t MyI2C_ReceiveData(void);
void MyI2C_SendACK(uint8_t Value);
uint8_t MyI2C_ReceiveACK(void);
#endif
再对MPU6050模块进行编写,MPU6050.c文件:
#include "stm32f10x.h" // Device header
#include "MyI2C.h"
#include "MPU6050_Reg.h"
//对MPU6050的地址进行宏定义
#define MPU6050_ADD 0xD0
//函数:在指定设备、指定地址写特定数据
//参数:寄存器地址、想要写入的数据
void MPU6050_W_reg(uint8_t Reg_Add,uint8_t Data)
{
//触发起始条件
MyI2C_Start ();
//发送从机地址和发送模式
MyI2C_SendData (MPU6050_ADD);
//接收应答
MyI2C_ReceiveACK ();
//发送指定寄存器的地址
MyI2C_SendData (Reg_Add);
//接收应答
MyI2C_ReceiveACK ();
//发送特定数据
MyI2C_SendData (Data);
//接收应答
MyI2C_ReceiveACK ();
//产生终止条件
MyI2C_Stop();
}
//函数:在指定设备、指定地址读数据
//参数:寄存器地址
uint8_t MPU6050_R_reg(uint8_t Reg_Add)
{
//前面这一段代码就是指定地址写的选择从机,在选择寄存器的代码
uint8_t Data;
MyI2C_Start ();
MyI2C_SendData (MPU6050_ADD);
MyI2C_ReceiveACK ();
MyI2C_SendData (Reg_Add);
MyI2C_ReceiveACK ();
MyI2C_Stop();
//再次产生起始条件
MyI2C_Start ();
//发送从机地址和指定I2C为接收模式
MyI2C_SendData (MPU6050_ADD | 0x01);
//接收应答
MyI2C_ReceiveACK ();
//这里只接受一个字节的数据,将应答位置为非应答
MyI2C_SendACK (1);
//接收数据
Data = MyI2C_ReceiveData ();
//产生终止条件
MyI2C_Stop();
//返回接收到的数据
return Data ;
}
//函数:MPU6050的初始化函数
void MPU6050_Init(void)
{
//对I2C软件初始化
MyI2C_Init ();
//对MPU6050的电源进行配置,这里选择陀螺仪时钟作为基准时钟
MPU6050_W_reg(MPU6050_PWR_MGMT_1,0x01);
MPU6050_W_reg(MPU6050_PWR_MGMT_2,0x00);
//对采样频率进行分频,这里选择10分频
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);
}
//函数:对MPU6050所测得的参数进行读取
//参数:加速度计的X、Y、Z轴数据,陀螺仪的X、Y、Z轴数据
void MPU6050_Getdata(int16_t *AX,int16_t *AY,int16_t *AZ,
int16_t *GX,int16_t *GY,int16_t *GZ)
{
// I2C协议传输数据只能一个字节一个字节(8个bit位)的传输,但是MPU6050
// 所测得的数据都是16位的,一次需要将MPU6050的数据分成高八位和第八位两次传输
uint16_t AX_L,AX_H;
//主机接收高八位数据
AX_H = MPU6050_R_reg(MPU6050_ACCEL_XOUT_H);
//主机接收低八位数据
AX_L = MPU6050_R_reg(MPU6050_ACCEL_XOUT_L);
//将数据高八位和低八位拼成一个完整的16位数据
*AX = (AX_H << 8) | AX_L ;
//下面这五个参数同上
uint16_t AY_L,AY_H;
AY_H = MPU6050_R_reg(MPU6050_ACCEL_YOUT_H);
AY_L = MPU6050_R_reg(MPU6050_ACCEL_YOUT_L);
*AY = (AY_H << 8) | AY_L ;
uint16_t AZ_L,AZ_H;
AZ_H = MPU6050_R_reg(MPU6050_ACCEL_XOUT_H);
AZ_L = MPU6050_R_reg(MPU6050_ACCEL_XOUT_L);
*AZ = (AZ_H << 8) | AZ_L ;
uint16_t GX_L,GX_H;
GX_H = MPU6050_R_reg(MPU6050_GYRO_XOUT_H);
GX_L = MPU6050_R_reg(MPU6050_GYRO_XOUT_L);
*GX = (GX_H << 8) | GX_L ;
uint16_t GY_L,GY_H;
GY_H = MPU6050_R_reg(MPU6050_GYRO_YOUT_H);
GY_L = MPU6050_R_reg(MPU6050_GYRO_YOUT_L);
*GY = (GY_H << 8) | GY_L ;
uint16_t GZ_L,GZ_H;
GZ_H = MPU6050_R_reg(MPU6050_GYRO_ZOUT_H);
GZ_L = MPU6050_R_reg(MPU6050_GYRO_ZOUT_L);
*GZ = (GZ_H << 8) | GZ_L ;
}
最后这个读取MPU6050参数的函数是用指针作为函数参数的形式来将参数值进行返回,这样的形式,能够同时返回多个参数值。
将MPU6050要使用到的寄存器地址打包在一个头文件中,便于编程时使用,MPU6050_Reg.h文件:
#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
MPU6050.h文件:
#ifndef _MPU6050_H
#define _MPU6050_H
#include "MPU6050_Reg.h"
void MPU6050_W_reg(uint8_t Reg_Add,uint8_t Data);
uint8_t MPU6050_R_reg(uint8_t Reg_Add);
void MPU6050_Init(void);
void MPU6050_Getdata(int16_t *AX,int16_t *AY,int16_t *AZ,
int16_t *GX,int16_t *GY,int16_t *GZ);
#endif
最后在主函数中,对各模块函数进行调用,main.c文件:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MPU6050.h"
//创建六个变量用于存储从MPU6050中接收到的数据
int16_t AX,AY,AZ,GX,GY,GZ;
//创建变量用于存储MPU6050的ID号
uint16_t ID;
int main(void)
{
OLED_Init();
MPU6050_Init();///各模块初始化
//读取MPU6050的ID号
ID = MPU6050_R_reg (MPU6050_WHO_AM_I);
OLED_ShowString (1,1,"ID");//显示提示字符串
OLED_ShowHexNum (1,3,ID,2);//显示MPU6050ID号
OLED_ShowString (2,1,"AX");//显示提示字符串
OLED_ShowString (3,1,"AY");
OLED_ShowString (4,1,"AZ");
OLED_ShowString (2,8,"GX");
OLED_ShowString (3,8,"GY");
OLED_ShowString (4,8,"GZ");
while(1)
{
//读取MPU6050数据
MPU6050_Getdata(&AX,&AY,&AZ,&GX,&GY,&GZ);
//实时显示MPU6050的六个数据
OLED_ShowSignedNum (2,3,AX,5);
OLED_ShowSignedNum (3,3,AY,5);
OLED_ShowSignedNum (4,3,AZ,5);
OLED_ShowSignedNum (2,10,GX,5);
OLED_ShowSignedNum (3,10,GY,5);
OLED_ShowSignedNum (4,10,GZ,5);
}
}
最终效果:改变面包板姿态,OLED实时显示MPU6050各数据的变化。
注意事项
- 软件I2C需要手动跳转电平去模拟实现基本时序中的六个要素;
- 对时序进行编写时,只能写主机进行的操作,从机的操作不需要在代码中写,时序到了它会自动执行;
- 清楚用I2C来连接片外外设和STM32的整个流程,片外外设换了,只需要查看该外设的数据手册,找到写入哪些寄存器去初始化它,并将读取数据寄存器的地址找到,再进行一个完整的操作来连接。(对其他片外外设的移植)