回顾知识点: 【STM32】I2C通信协议&MPU6050芯片-学习笔记-CSDN博客
接线图

整体思路

I2C初始化
软件I2C只需要用GPIO读取函数就可以,不用I2C库函数;
① 把SCL和SDA都初始化成开漏输出模式(开漏输出不只是只能输出、也可以输入;输入时,先输出1,再直接读取输入数据寄存器就可以了)
②把SCL和SDA置高电平
cs
void HerI2C_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); //将PB10和PB11引脚初始化为开漏输出
GPIO_SetBits(GPIOB,GPIO_Pin_10 | GPIO_Pin_11); //设置PB10和PB11引脚初始化后默认为高电平(释放总线状态)
}
配置完之后,陀螺仪内部就在连续不断地进行转换,转换后输出的数据就放在数据寄存器里面。
六个时序单元
- 起始条件:SCL高电平期间,SDA从高电平切换到低电平
- 终止条件:SCL高电平期间,SDA从低电平切换到高电平
首先对操着端口的库函数进行封装:
cs
/**
* 函 数:I2C写SCL引脚电平
* 参 数:BitValue 协议层传入的当前需要写入SCL的电平,范围0~1
* 返 回 值:无
* 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCL为低电平,当BitValue为1时,需要置SCL为高电平
*/
void HerI2C_W_SCL(uint8_t BitV)
{
GPIO_WriteBit(GPIOB,GPIO_Pin_10,(BitAction)BitV);//根据BitValue,设置SCL引脚的电平
Delay_us(10); //延时10us,防止时序频率超过要求
}
void HerI2C_W_SDA(uint8_t BitV)
{
GPIO_WriteBit(GPIOB,GPIO_Pin_11,(BitAction)BitV);//根据BitValue,设置SDA引脚的电平,BitV要实现非0即1的特性
Delay_us(10);
}
I2C起始和终止函数:
cs
void HerI2C_Start(void)
{
HerI2C_W_SDA(1); //释放SDA,确保SDA为高电平
HerI2C_W_SCL(1); //释放SCL,确保SCL为高电平
HerI2C_W_SDA(0); //在SCL高电平期间,拉低SDA,产生起始信号
HerI2C_W_SCL(0); //起始后把SCL也拉低,即为了占用总线,也为了方便总线时序的拼接
}
void HerI2C_Stop(void)
{
HerI2C_W_SDA(0); //拉低SDA,确保SDA为低电平
HerI2C_W_SCL(1); //释放SCL,使SCL呈现高电平
HerI2C_W_SDA(1); //在SCL高电平期间,释放SDA,产生终止信号
}
Start函数这里,如果起始条件之前,SCL和SDA已经是高电平了,那先释放哪个都是一样的效果; 但是在后面还要兼容一个重复起始条件Sr,Sr最开始,SCL是低电平,SDA电平不确定,所以保险起见,趁SCL低电平期间,先确保SDA释放为高电平再释放SCL,然后在两个高电平期间再拉低SDA起始,就可以Start兼容起始条件和重复起始条件

Stop函数注意:

实际上,除了终止条件SCL以高电平结束,所有的单元我们都会保证SCL以低电平结束。
I2C读SDA函数:
cs
/**
* 函 数:I2C读SDA引脚电平
* 参 数:无
* 返 回 值:协议层需要得到的当前SDA的电平,范围0~1
* 注意事项:此函数需要用户实现内容,当前SDA为低电平时,返回0,当前SDA为高电平时,返回1
*/
uint8_t HerI2C_R_SDA(void)
{
uint8_t SDABit;
SDABit =GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11); //读取SDA电平
Delay_us(10); //延时10us,防止时序频率超过要求
return SDABit; //返回SDA电平
}
**发送一个字节:**SCL低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节

cs
void HerI2C_SendByte(uint8_t Bytevalue)
{
uint8_t i;
for(i =0;i <8;i ++) //循环8次,主机依次发送数据的每一位
{
HerI2C_W_SDA(Bytevalue & (0x80 >>i));//使用掩码的方式取出Byte的指定一位数据并写入到SDA线
HerI2C_W_SCL(1); //释放SCL,从机在SCL高电平期间读取SDA
HerI2C_W_SCL(0); //拉低SCL,主机开始发送下一位数据
}
}
**接收一个字节:**SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA,交给从机控制)

cs
uint8_t HerI2C_ReceiveByte(void)
{
uint8_t i,Bytevalue =0x00;
HerI2C_W_SDA(1); //接收前,主机先确保释放SDA,避免干扰从机的数据发送
for(i =0;i <8;i ++) //循环8次,主机依次接收数据的每一位
{
HerI2C_W_SCL(1); //释放SCL,主机机在SCL高电平期间读取SDA
if(HerI2C_R_SDA() == 1)
{ //读取SDA数据,并存储到Bytevalue变量
Bytevalue |= (0x80 >> i);//当SDA为1时,置变量指定位为1,当SDA为0时,不做处理,指定位为默认的初值0
} // Bytevalue = Bytevalue | (0x80 >> i)
HerI2C_W_SCL(0); //拉低SCL,从机在SCL低电平期间写入SDA
}
return Bytevalue;
}

cs
/**
* 函 数:I2C发送应答位
* 参 数:Askvalue要发送的应答位,范围:0~1,0表示应答,1表示非应答
* 返 回 值:无
*/
void HerI2C_SendAsk(uint8_t Askvalue)
{
HerI2C_W_SDA(Askvalue); //主机把应答位数据放到SDA线
HerI2C_W_SCL(1); //释放SCL,从机在SCL高电平期间,读取应答位
HerI2C_W_SCL(0); //拉低SCL,开始下一个时序模块
}
uint8_t HerI2C_ReceiveAsk(void)
{
uint8_t Askvalue; //定义应答位变量
HerI2C_W_SDA(1); //接收前,主机先确保释放SDA,避免干扰从机的数据发送
HerI2C_W_SCL(1); //释放SCL,主机机在SCL高电平期间读取SDA
Askvalue = HerI2C_R_SDA(); //将应答位存储到变量里
HerI2C_W_SCL(0); //拉低SCL,开始下一个时序模块
return Askvalue; //返回定义应答位变量
}
通过AD0引脚改名功能:
此时从机地址为1101 000

正常会给应答位000

然后在MPU6050DA0引脚插上飞线引到Vcc, 置高电平,此时从机地址为1101 010 ,然后按下复位键,应答位就为1,没有应答了。

这是需要在程序中修改寻址,0xD2就又收到应答位了。
MPU6050初始化
cs
#define MPU6050_ADDRESS 0xD0 //MPU6050的I2C从机地址
cs
void MPU6050_Init(void)
{
HerI2C_Init(); //I2C初始化
/*MPU6050寄存器初始化,需要对照MPU6050手册的寄存器描述配置,此处仅配置了部分重要的寄存器*/
MPU6050_WriteReg(MPU6050_PWR_MGMT_1,0x01); //电源管理寄存器1,取消睡眠模式,选择时钟源为X轴陀螺仪
MPU6050_WriteReg(MPU6050_PWR_MGMT_2,0x00); //电源管理寄存器2,保持默认值0,所有轴均不待机
MPU6050_WriteReg(MPU6050_SMPLRT_DIV,0x09); //采样率分频寄存器,配置采样率
MPU6050_WriteReg(MPU6050_CONFIG,0x06); //配置寄存器,配置DLPF
MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x18); //陀螺仪配置寄存器,选择满量程为±2000°/s
MPU6050_WriteReg(MPU6050_ACCEL_CONFIG,0x18);//加速度计配置寄存器,选择满量程为±16g
}


MPU6050获取数据函数
cs
/**
* 函 数:MPU6050获取数据
* 参 数:AccX AccY AccZ 加速度计X、Y、Z轴的数据,使用输出参数的形式返回,范围:-32768~32767
* 参 数:GyroX GyroY GyroZ 陀螺仪X、Y、Z轴的数据,使用输出参数的形式返回,范围:-32768~32767
* 返 回 值:无
*/
void MPU6050_GetData(int16_t *AcceX,int16_t *AcceY,int16_t *AcceZ,
int16_t *GyroX,int16_t *GyroY,int16_t *GyroZ)
{
uint8_t DataH;
uint8_t DataL;
DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H); //读取加速度计X轴的高8位数据
DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L); //读取加速度计X轴的低8位数据
*AcceX = (DataH << 8) | DataL; //数据拼接,通过输出参数返回
//(DataH << 8) | DataL;这16位是用补码表示的有符号数,直接赋给int_16t也没问题
DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
*AcceY = (DataH << 8) | DataL;
DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
*AcceZ = (DataH << 8) | DataL;
DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H); //读取陀螺仪X轴的高8位数据
DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L); //读取陀螺仪X轴的低8位数据
*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;
}
数据寄存器宏定义:
cs
#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指定地址写寄存器:

cs
/**
* 函 数:MPU6050写寄存器
* 参 数:RegAddress 寄存器地址,范围:参考MPU6050手册的寄存器描述
* 参 数:Data 要写入寄存器的数据,范围:0x00~0xFF
* 返 回 值:无
*/
void MPU6050_WriteReg(uint8_t RegAddress,uint8_t Data)
{
HerI2C_Start(); //I2C起始
HerI2C_SendByte(MPU6050_ADDRESS); //发送从机地址,读写位为0,表示即将写入
HerI2C_ReceiveAsk(); //接收应答
HerI2C_SendByte(RegAddress); //发送寄存器地址
HerI2C_ReceiveAsk(); //接收应答
HerI2C_SendByte(Data); //发送要写入寄存器的数据
HerI2C_ReceiveAsk(); //接收应答
HerI2C_Stop(); //I2C终止
}
这里的函数会返回一个应答位,这里没有做处理应答位,如果要处理应答位要增加很多代码,处理比较麻烦,为了时序清晰,方便学习,这里不对返回值进行判断。这里知道有应答位可以判断从机有没有收到数据就可以了。
MPU6050指定地址读寄存器:

cs
/**
* 函 数:MPU6050读寄存器
* 参 数:RegAddress 寄存器地址,范围:参考MPU6050手册的寄存器描述
* 返 回 值:读取寄存器的数据,范围:0x00~0xFF
*/
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
uint8_t Data;
HerI2C_Start();
HerI2C_SendByte(MPU6050_ADDRESS);
HerI2C_ReceiveAsk();
HerI2C_SendByte(RegAddress);
HerI2C_ReceiveAsk();
HerI2C_Start(); //I2C重复起始
HerI2C_SendByte(MPU6050_ADDRESS | 0x01);//发送从机地址,读写位为1,表示即将读取
HerI2C_ReceiveAsk(); //接收应答
Data = HerI2C_ReceiveByte(); //接收指定寄存器的数据
HerI2C_SendAsk(1); //发送应答,给从机非应答,终止从机的数据输出
HerI2C_Stop(); //I2C终止
return Data;
}
测试读写函数



寄存器也是一种存储器,只不过普通的存储器只能读和写,里面的数据并没有赋予什么实际意义,但是寄存器就不一样了,寄存器的每一位数据都对应了硬件电路的状态,寄存器和外设的硬件电路是可以进行互动的。程序到这里就可以进行寄存器控制硬件电路了。
MPU6050获取ID号
cs
uint8_t MMPU6050_GetID(void)
{
return MPU6050_ReadReg(MPU6050_WHO_AM_I); //返回MPU6050_WHO_AM_I寄存器值
}
main函数
cs
uint8_t ID;
int16_t AX, AY, AZ, GX, GY, GZ;
int main(void)
{
OLED_Init();
MPU6050_Init();
ID = MMPU6050_GetID();
OLED_ShowString(1,1,"ID:");
OLED_ShowHexNum(1,4,ID,2);
while(1)
{
MPU6050_GetData(&AX , &AY, &AZ, &GX, &GY, &GZ); //获取MPU6050的数据
OLED_ShowSignedNum(2,1,AX,5);
OLED_ShowSignedNum(3,1,AY,5);
OLED_ShowSignedNum(4,1,AZ,5);
OLED_ShowSignedNum(2,9,GX,5);
OLED_ShowSignedNum(3,9,GY,5);
OLED_ShowSignedNum(4,9,GZ,5);
}
}