本次实验分为两部分,第一部分我们完成软件I2C协议的时序,第二部分,我们基于I2C协议,来操作读写寄存器,来操控MPU6050.
软件I2C相比硬件I2C的一大优势是端口不受限,可以任意指定。
先来介绍一下思路:
首先建立I2C通信层的.c和.h模块,在通信层里写好I2C底层的GPIO口初始化和6个时序基本单元,分别是起始条件,终止条件,发送一个字节,接收一个字节,发送应答和接收应答。
之后我们再建立MPU6050的.c和.h模块,在这一层,我们将基于I2C通信的模块,来实现指定地址读,指定地址写,再实现写寄存器对芯片进行配置,读寄存器得到传感器数据
最终在main.c里,调用MPU6050的模块,初始化,拿到数据,显示数据。
软件I2C初始化需要做两个任务:
1.把SCL和SDA都初始化成开漏输出模式
2.把SCL和SDA置高电平
我们当前接线:SCL是PB10,SDA是PB11
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_Pin = GPIO_Pin_10 |GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);//开漏输出高电平时因高阻态会变成浮空
//但是MPU那边会有一个上拉电阻使SCL和SDA默认空闲状态高电平
}
接下来我们来实现6个时序基本单元,首先是起始条件和终止条件
我们可以不断地调用SetBits和ResetBits,来手动翻转高低电平,但是这样做的话,会在后面的程序中出现很多指定这个GPIO端口号的地方,一方面,这样的语义不明显,另一方面,如果我们之后需要换一个端口,就需要改动很多地方,所以我们可以在最上面做一个定义,把这个端口号统一换个名字,一种简单的替换方法就是宏定义
#define SCL_PORT GPIOB;
#define SCL_PIN GPIO_Pin_10;
GPIO_SetBits(SCL_PORT, SCL_PIN);
如果觉得每次都要定义PORT和PIN比较麻烦,还可以把整个函数都用宏定义进行替换,可以使用有参宏,在宏定义后面加一个括号,里面写上形参,那在实际引用的时候,比如这里调用OLED_W_SCL,实参为1,那替换的时候实参1对应的就是函数里的x。
#define OLED_W_SCL(x) GPIO_WriteBit(GPIOB, GPIO_Pin_8, (BitAction)(x))//将x的值强制转换为BitAction类型
OLED_W_SCL(1);
这里我们选择使用函数的方法,容易理解也方便加软件延时。

{
MyI2C_W_SDA(1);//释放SCL和SDA
MyI2C_W_SCL(1);//为了兼容重复起始条件Sr,Sr最开始时SCL是低电平,SDA电平不能确定,所以保险起见
//我们趁SCL是低电平,先确保释放SDA,再释放SCL,这时SDA和SCL都是高电平,之后再拉低SDA和SCL
//这样这个函数就能够兼容起始条件和重复起始条件了。
MyI2C_W_SDA(0);//先拉低SDA再拉低SCL,对应起始条件
MyI2C_W_SCL(0);
}
{
MyI2C_W_SDA(0);//在这个时序单元开始时,SDA并不一定是低电平,所以为了确保之后释放SDA能产生上升沿
//我们要在时序单元开始时,先拉低SDA。
MyI2C_W_SCL(1);
MyI2C_W_SDA(0);
}

{
//发送一个字节时SCL是低电平,除了终止条件SCL以高电平结束,所有的单元我们都会保证SCL以低电平结束
//这样方便各个单元的拼接
MyI2C_W_SDA(Byte & 0x80);//与上0x80就是取Byte字节里的最高位
MyI2C_W_SCL(1);//驱动时钟走一个脉冲,这里释放SCL之后,从机就会立刻把刚才放在SDA的数据读走
MyI2C_W_SCL(0);}
{for (uint8_t i = 0; i < 8; i++)
{
MyI2C_W_SDA(Byte & 0x80 >> i);
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
}
}

{
uint8_t Byte = 0x00;
MyI2C_W_SDA(1);//主机释放SDA,让从机写数据到SDA
for (uint8_t i = 0; i < 8; i++)
{
MyI2C_W_SCL(1);
if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}//因为是一位一位传过来的,读函数是1就是高电平
//不然就是低电平,不触发if条件就默认为0低电平了
MyI2C_W_SCL(0);
}
return Byte;
}




{
MyI2C_Start();
MyI2C_SendByte(MPU6050_ADDRESS);//为了方便修改参数,并且突出它是从机地址,使用宏定义替换数据
MyI2C_ReceiveAck();//这里有应答位,可以判断从机有没有收到数据就行了,不对返回值进行判断也行。
MyI2C_SendByte(RegAddress);//第二个字节的内容就是指定寄存器地址了,这个字节,会存在MPU6050的当前地址指针里
//用于指定具体读写哪个寄存器
MyI2C_ReceiveAck();//同样的发送一个字节后也得接收一下应答。
MyI2C_SendByte(Data);//第三个字节就是在我指定要写入的寄存器下要写入的数据了
MyI2C_ReceiveAck();
MyI2C_Stop();
}

{
uint8_t Data;
MyI2C_Start();
MyI2C_SendByte(MPU6050_ADDRESS);
MyI2C_ReceiveAck();
MyI2C_SendByte(RegAddress);
MyI2C_ReceiveAck();//前面这一部分和指定地制写一样,就是在设置MPU6050的当前地址指针。接下来要转入读的时序
//要转入读的时序,就必须重新指定读写位,就必须要重新起始
MyI2C_Start();
MyI2C_SendByte(MPU6050_ADDRESS | 0x01);//改为读操作
MyI2C_ReceiveAck();
Data = MyI2C_ReceiveByte();//总线控制权交给从机了
MyI2C_SendAck(1);//主机接收到数据要发送应答,参数给1不给从机应答,意思是不再读取了,所以该函数只读一个字节
MyI2C_Stop();return Data;
}
MPU6050_Init();
uint8_t ID = MPU6050_ReadReg(0x75);
OLED_ShowHexNum(1, 1, ID, 2);


#ifndef __MPU6050REG_H
#define __MPU6050REG_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的使用手册。
void MPU6050_Init(void)
{
MyI2C_Init();
MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);//配置电源管理寄存器1,接触睡眠,选择陀螺仪时钟
MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);//配置电源管理寄存器2,6个轴均不待机
MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);//配置采样分频寄存器,决定了数据输出的快慢,值越小越快。这里为10
MPU6050_WriteReg(MPU6050_CONFIG, 0x06);//配置寄存器,滤波参数给最大
MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);//陀螺仪配置寄存器,前面三位是自测使能,接下来满量程选择。
MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);//加速度计配置寄存器,陀螺仪和加速度计都选择了最大量程。
//配置完后,陀螺仪内部就在连续不断地进行数据转换了。输出的数据就存放在相应的寄存器里。想要获取数据的话
//只需要再写一个获取数据寄存器的函数。
}
根据任务需求,加下来这个函数需要返回6个uint16_t的数据,分别表示xyz的加速度值和陀螺仪值,但是C语言中,函数的返回值只能有一个,所以需要一些特殊操作来实现返回6个值的任务。
第一种最简单的方法,在函数外面定义6个全局变量,子函数读到数据,直接写到全局变量里,然后6个全局变量,在主函数进行分享。比较适合用在规模比较小的项目。
第二种,用指针,进行变量的地址传递,来实现多返回值
第三种,使用结构体,对多个变量进行打包,再统一进行传递。
这里使用第二种方法。
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,低八位放在DataL
DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);//先读取加速地寄存器X轴的高8位
DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);//然后再读取加速度寄存器的低8位
*AccX = (DataH << 8) | DataL;//高8位左移8位再或上低8位即可得到16位数据,再将其赋到指针传进来的地址中去
DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
*AccY = (DataH << 8) | DataL;
DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
*AccZ = (DataH << 8) | DataL;
DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
*GyroX = (DataH << 8) | DataL;
DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
*GyroY = (DataH << 8) | DataL;
DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
*GyroZ = (DataH << 8) | DataL;
}
这里还有一个更加高效的方法,就是使用I2C读取多个字节的时序,从一个基地址开始,连续读取一片的寄存器,因为这里的寄存器地址是连续的,所以可以从第一个寄存器的地址0x3B开始,连续读取14个字节,这样就可以一次性地把加速度值,陀螺仪值,包括两个字节的温度值,都读取出来。

今天的内容感觉很多,感觉做了有3,4个小时了。做到半夜1点多,还是要提前做,别留的太晚了。还是挺累的,感觉一沾枕头就能睡着了。