相信大家在I2C与MPU6050通信时总会碰到一些问题,作者的问题是可以读ID号,但是无法对MPU6050寄存器进行写入,读取寄存器的数据总是0
问题

我们可以看到,数据一直是0,作者在网上看到部分帖子,但是按照上述解决办法没有解决,作者会将文章的链接给出,希望对大家有帮助:MPU6050可以读取ID值,温度值和原始数据值为零问题解决 - Darren_pty - 博客园 (cnblogs.com)
嵌入式C语言应用
do-while
do-while循环是一个后测试循环,它在执行循环内部的代码之后检查循环条件。这意味着循环内部的代码至少会被执行一次,即使循环条件一开始也达不到满足。
do {
// 循环体内的代码
// 这部分代码会至少被执行一次
} while (循环条件);
C语言代码换行
do {
// 循环体内的代码
// 这部分代码会至少被执行一次} \(\表示换行,并且注意在'\'后仅能跟回车,不能加其他字符,否则编译报错)
while (循环条件);
通信协议前置知识
串行通信与并行通信
1.并行通信的特点为同时传输多bit位数据,优点:传输效率高;缺点:数据线多,设计复杂

2.串行通信特点是只能一位一位传输

异步串行通信与同步串行通信
视频:你知道同步通信和异步通信有什么区别吗?_哔哩哔哩_bilibili
同步:发送方发出数据后,等接收方发回响应才能发送下一个数据包的通信方式
异步:发送方发送数据后,不等接收方发回响应接着发送下个数据包的通信方式
1.同步串行通信

2.异步串行通信

单工、半双工、全双工

I2C前置理论知识
支持总线挂载多设备(一主多从、多主多从),在多主多从模式下会进行总线仲裁,时钟线也是由主机控制的,多主机模式下还需要进行时钟的同步
硬件电路
1.SCL线:对于SCL时钟线,在任何时刻都只能被动的读取,从机不允许控制SCL
2.因为主机拥有SCL的绝对控制权,所以主机的SCL可以设置为推挽输出模式,所有的从机的SCL都配置成浮空输入或者上拉输入
3.SDA线:主机在发送的时候是输出,在接收的时候是输入,同样从机的SDA也会在输入和输出之间反复切换(如果输入输出的时序遇到问题,就会出现麻烦,极有可能发生两个引脚同时处于输出状态,如果这时应该输出高电平,一个输出低电平就是电源短路)
4.避免短路设计:I2C的设计是禁止所有设备输出强上拉的高电平,采用外置弱上拉的电阻+开漏输出的电路结构

线与逻辑:什么叫线与逻辑? - NewLook的回答 - 知乎 https://www.zhihu.com/question/534999506/answer/3081799468

I2C时序分析
本文借鉴了以下文章:STM32软件I2C驱动MPU6050 - Sightseer观光客 - 博客园 (cnblogs.com)
起始条件和终止条件
起始条件:SCL高电平期间,拉低SDA;终止条件:SCL高电平期间,拉高SDA

起始条件与终止条件的程序
黄线部分是我们的重复起始条件:在SCL高电平期间,拉低SDA

我们为什么需要在重复开始条件前,先释放SDA再释放SCL(准确来说,只有先拉高了SDA,就可以拒绝一切的影响,拉高SCL是为了创造起始条件)

终止条件是:在SCL高电平期间,释放SDA
下面的文字说错了:一个是把SDA拉低,就可以解决无效的终止条件

我们为什么要在终止条件前先把SDA拉低,是为了解决重复的终止条件,再拉高是为了创造终止条件的环境;最后一步拉低SDA才是正确的终止条件
//起始条件
void MyI2C_Start(){
//为保证兼容重复开始条件,先释放SDA再释放SCL
SDA_SET(1);
SCL_SET(1);
/*
起始条件是:先拉低SDA,再拉低SCL
*/
SDA_SET(0);
SCL_SET(0);
}
//终止条件
void MyI2C_Stop(){
//为了避免在释放前SDA是高电平,我们需要先拉低SDA,再释放SCL,再释放SDA
SDA_SET(0);
SCL_SET(1);
SDA_SET(1);
}
发送一个字节
主机发送,从机读取;一般是在SCL高电平期间,从机才能读取数据(但是为了尽快读取,一般从机在SCL上升沿期间就读取了SDA的数据,因为时钟是主机控制的,从机并不知道什么时候SCL就是下降沿了,所以上升沿就赶快进行读取)
发送一个字节的代码
void MyI2C_SendByte(uint8_t byte){
//除了终止条件,SCL以高电平结束,所有的单元我们都会保证SCL以低电平结束,这样方便各个单元的拼接
/*这里我们是高位先行,我们就需要按位与取出来最高位
MyI2C_W_SDA(Byte & 0x80);//(x x x x x x x x)&(1 0 0 0 0 0 0 0)=x 0 0 0 0 0 0 0*/
//为了避免重复书写,我们使用for循环,然后每次右移1为,再与
for(uint8_t i = 0;i < 8;i++){
SDA_SET(byte & (0x80 >> i)); //SDA写数据,I2C是高位先行
SCL_SET(1); //给SCL高电平,让从机把SDA的数据读走
SCL_SET(0); //再拉低,发送下一位
}
}
接收一个字节
从机发送,主机接收:在SCL低电平期间,从机把数据放到SDA上,高电平期间主机读取数据(主机在接收之前,需要先释放SDA(因为线与的特点,只要有一个是低电平,就是都是低电平,释放SDA相当于切换为输入模式)
接收一个字节的代码
uint8_t MyI2C_ReceiveByte(){
uint8_t byte = 0x00;
SDA_SET(1); //先释放SDA
for(uint8_t i = 0; i < 8;i++){
SCL_SET(1); //设置SCL为高,此时从机把数据放在SDA上,主机就可以读取数据了
if(READ_SDA() == 1){
/*第一次读SDA为1,把byte最高位置1,如果读SDA为0,if不成立,byte=0x00
就相当于写入0了,因为我们是一个for循环,每次都是会刷新byte的值,所以
第一次为1:(0 0 0 0 0 0 0 0)&(1 0 0 0 0 0 0 0)=1 0 0 0 0 0 0 0
第二次为0:byte=(1 0 0 0 0 0 0 0)
第二次为1:byte=(1 0 0 0 0 0 0 0)&(0 0 1 0 0 0 0 0)=1010 0 0 0 0
*/
byte |= (0x80 >> i);
} //由高到低位读SDA
SCL_SET(0); //设置SCL为低,一个时钟结束
}
return byte;
}
接收应答与发送应答

接收应答与发送应答的代码
//发送应答
void MyI2C_SendACK(uint8_t ackbit){
SDA_SET(ackbit); //把应答位放在SDA上
SCL_SET(1); //SCL高电平,从机读取应答
SCL_SET(0); //SCL低电平,进入下一个时序单元,完毕
}
//接收应答其实就是接收一个字节的一位
uint8_t MyI2C_ReceiveACK(){
/*函数进来时,SCL低电平,主机释放SDA,防止干扰从机
同时,从机把应答位放到SDA上,SCL高电平,主机读取应答位
*/
uint8_t ackbit;
SDA_SET(1); //释放SDA
SCL_SET(1); //给SCL一个脉冲,让从机把应答位写到SDA上
ackbit = READ_SDA();
SCL_SET(0); //设置SCL为低,一个时钟结束
return ackbit;
}
I2C时序之指定地址写
指定地址写 对于指定设备(Slave Address),在指定地址(Reg Address)下,写入指定数据(Data)

I2C时序之当前地址读
当前地址读 对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)
读写位:0表示写,1表示读

当前地址指针:在RA接收应答之后,SDA的控制权就要交给从机了,I2C协议的规定是,主机进行寻址时,一旦读写标志位给1了,下一个字节就要立马转换为读的时序,所以主机还来不及指定我想要读那个寄存器,就得开始接收了,所以这里就没有指定地址这个环节,(主机并没有指定地址,那么从机到底一个发那个寄存器的数据呢,这就需要当前地址指针了)。
在从机中,所有的寄存器被分配到了一个线性区域中,并且会有一个单独的指针变量,指示着这个寄存器,这个指针上电默认,一般指向0地址(并且每写入一个字节和读出一个字节后,这个指针就会自增一次,移动到下一个位置,那么在调用当前地址读的时序时,主机没有指定地址,从机就会返回当前指针指向的寄存器的值);
假如我刚刚调用了在指定地址写的时序,在0x19的位置写入了0XAA,那么指针就会+1,移动到0X1A的位置,我再调用当前地址读的时序,返回的就是0X1A地址下的值,如果再调用一次就是0x1B下的值
I2C时序之指定地址读


就是指定地址读的时序了

最开始依旧是启动条件,然后发送一个字节进行寻址,指定从机地址是1101000,读写标志位是0,代表写的操作,进过从机应答之后,再发送一个字节;第二个字节用来指定地址(从机的寄存器的地址),然后ox19这个地址就写入到了从机的地址指针中去了,从机接收到0x19之后,它的寄存器指针就指向了0x19;之后我们直接重新起始条件,指定好从机地址为读,这时指针已经指向了0x19,后面就直接读就行了

模拟I2C的全部代码
1.MyI2C.c
#include "stm32f10x.h" // Device header
#include "MYI2C.h"
#include "Delay.h"
//设置I2C引脚端口,注意如端口号修改,时钟使能也要修改
#define SCL_PORT GPIOB
#define SCL_LINE GPIO_Pin_10
#define SDA_PORT GPIOB
#define SDA_LINE GPIO_Pin_11
//设置I2C操作延迟(速度)
#define I2C_DELAY do{Delay_us(10);}while(0);
//I2C引脚电平写
#define SCL_SET(x) do{GPIO_WriteBit(SCL_PORT,SCL_LINE,(BitAction)(x)); I2C_DELAY;} \
while(0);
#define SDA_SET(x) do{GPIO_WriteBit(SDA_PORT,SDA_LINE,(BitAction)(x)); I2C_DELAY;} \
while(0);
//I2C引脚电平读
uint8_t READ_SDA(void){
uint8_t val;
val = GPIO_ReadInputDataBit(SDA_PORT,SDA_LINE);
I2C_DELAY;
return val;
}
void MyI2C_Init(){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Pin = SCL_LINE | SDA_LINE;
GPIO_InitStructure.GPIO_Speed= GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
}
void MyI2C_Start(){
//为保证兼容重复开始条件,先释放SDA再释放SCL
SDA_SET(1);
SCL_SET(1);
SDA_SET(0);
SCL_SET(0);
}
void MyI2C_Stop(){
SDA_SET(0);
SCL_SET(1);
SDA_SET(1);
}
void MyI2C_SendByte(uint8_t byte){
for(uint8_t i = 0;i < 8;i++){
SDA_SET(byte & (0x80 >> i)); //SDA写数据,I2C是高位先行
SCL_SET(1); SCL_SET(0); //给SCL一个脉冲,让从机把SDA的数据读走
}
}
uint8_t MyI2C_ReceiveByte(){
uint8_t byte = 0x00;
SDA_SET(1); //先释放SDA
for(uint8_t i = 0; i < 8;i++){
SCL_SET(1); //设置SCL为高,此时从机把数据放在SDA上,主机就可以读取数据了
if(READ_SDA() == 1){
/*第一次读SDA为1,把byte最高位置1,如果读SDA为0,if不成立,byte=0x00
就相当于写入0了,因为我们是一个for循环,每次都是会刷新byte的值,所以
第一次为1:(0 0 0 0 0 0 0 0)&(1 0 0 0 0 0 0 0)=1 0 0 0 0 0 0 0
第二次为0:byte=(1 0 0 0 0 0 0 0)
第二次为1:byte=(1 0 0 0 0 0 0 0)&(0 0 1 0 0 0 0 0)=1010 0 0 0 0
*/
byte |= (0x80 >> i);
} //由高到低位读SDA
SCL_SET(0); //设置SCL为低,一个时钟结束
}
return byte;
}
//发送应答
void MyI2C_SendACK(uint8_t ackbit){
SDA_SET(ackbit); //把应答位放在SDA上
SCL_SET(1); //SCL高电平,从机读取应答
SCL_SET(0); //SCL低电平,进入下一个时序单元,完毕
}
//接收应答其实就是接收一个字节的一位
uint8_t MyI2C_ReceiveACK(){
/*函数进来时,SCL低电平,主机释放SDA,防止干扰从机
同时,从机把应答位放到SDA上,SCL高电平,主机读取应答位
*/
uint8_t ackbit;
SDA_SET(1); //释放SDA
SCL_SET(1); //给SCL一个脉冲,让从机把应答位写到SDA上
ackbit = READ_SDA();
SCL_SET(0); //设置SCL为低,一个时钟结束
return ackbit;
}
2.MyI2C.H
#ifndef _MYI2C_H
#define _MYI2C_H
void MyI2C_Init(void);
void MyI2C_Start(void);
void MyI2C_Stop(void);
void MyI2C_SendByte(uint8_t byte);
uint8_t MyI2C_ReceiveByte(void);
void MyI2C_SendACK(uint8_t ackbit);
uint8_t MyI2C_ReceiveACK(void);
#endif
MPU6050代码分析
指定地址读+指定地址写
//指定地址读寄存器
uint8_t MPU6050_ReadReg(uint8_t reg_addr){
uint8_t data;
MyI2C_Start(); //起始位
MyI2C_SendByte(MPU6050_WRITE_ADDR); //从机地址+读
MyI2C_ReceiveACK(); //接收应答,这里有应答位可以判断从机有没有接收到数据
//寻址找到从机之后就可以发送下一个字节了
MyI2C_SendByte(reg_addr); //用来指定我写的地址,这个地址就写到从机的地址指针中了
//从机接收到reg_addr之后,它的寄存器指针就指向了0x19这个位置
MyI2C_ReceiveACK(); //接收应答
MyI2C_Start(); //再来一个起始条件
MyI2C_SendByte(MPU6050_READ_ADDR); //再次指定设备的ID号,这次我们读
MyI2C_ReceiveACK(); //主机接收应答
data = MyI2C_ReceiveByte(); //指针指向的寄存器的数据
MyI2C_SendACK(1); //主机接收后,发送应答
MyI2C_Stop(); //结束
return data;
}
//指定地址写寄存器
void MPU6050_WriteReg(uint8_t reg_addr,uint8_t data){
MyI2C_Start(); ////起始位
MyI2C_SendByte(MPU6050_WRITE_ADDR); //从机地址+写
MyI2C_ReceiveACK(); ////接收应答,这里有应答位可以判断从机有没有接收到数据
MyI2C_SendByte(reg_addr); // //用来指定我写的寄存器地址
MyI2C_ReceiveACK(); //接收应答
MyI2C_SendByte(data); //写入的数据
MyI2C_ReceiveACK(); //接收应答
MyI2C_Stop(); //结束
}
MPU6050代码
1.MPU6050.c
#include "stm32f10x.h" // Device header
#include "MPU6050_Reg.h"
#include "MYI2C.h"
#include "stdio.h"
#include "stdlib.h"
#define MPU6050_I2C_ADDR (0x68)
#define MPU6050_WRITE_ADDR (((MPU6050_I2C_ADDR) << 1) | 0x00) //写
#define MPU6050_READ_ADDR (((MPU6050_I2C_ADDR) << 1) | 0x01) //读
//指定地址读寄存器
uint8_t MPU6050_ReadReg(uint8_t reg_addr){
uint8_t data;
MyI2C_Start(); //起始位
MyI2C_SendByte(MPU6050_WRITE_ADDR); //从机地址+读
MyI2C_ReceiveACK(); //接收应答,这里有应答位可以判断从机有没有接收到数据
//寻址找到从机之后就可以发送下一个字节了
MyI2C_SendByte(reg_addr); //用来指定我写的地址,这个地址就写到从机的地址指针中了
//从机接收到reg_addr之后,它的寄存器指针就指向了0x19这个位置
MyI2C_ReceiveACK(); //接收应答
MyI2C_Start(); //再来一个起始条件
MyI2C_SendByte(MPU6050_READ_ADDR); //再次指定设备的ID号,这次我们读
MyI2C_ReceiveACK(); //主机接收应答
data = MyI2C_ReceiveByte(); //指针指向的寄存器的数据
MyI2C_SendACK(1); //主机接收后,发送应答
MyI2C_Stop(); //结束
return data;
}
//指定地址写寄存器
void MPU6050_WriteReg(uint8_t reg_addr,uint8_t data){
MyI2C_Start(); ////起始位
MyI2C_SendByte(MPU6050_WRITE_ADDR); //从机地址+写
MyI2C_ReceiveACK(); ////接收应答,这里有应答位可以判断从机有没有接收到数据
MyI2C_SendByte(reg_addr); // //用来指定我写的寄存器地址
MyI2C_ReceiveACK(); //接收应答
MyI2C_SendByte(data); //写入的数据
MyI2C_ReceiveACK(); //接收应答
MyI2C_Stop(); //结束
}
void MPU6050_Init(){
MyI2C_Init();
MPU6050_WriteReg(MPU6050_PWR_MGMT_1,0x01); //关闭睡眠模式
MPU6050_WriteReg(MPU6050_PWR_MGMT_2,0x00);
MPU6050_WriteReg(MPU6050_SMPLRT_DIV,0x09);
MPU6050_WriteReg(MPU6050_CONFIG,0x06);
MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x00);
MPU6050_WriteReg(MPU6050_ACCEL_CONFIG,0x00);
}
void MPU6050_GetDate(int16_t *AccX,int16_t *AccZ,int16_t *AccY,
int16_t *GyroX,int16_t *GyroZ,int16_t *GyroY
){
uint8_t DataH, DataL;
DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);//读取加速度寄存器X轴的高8位
DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);//读取加速度寄存器X轴的高低位
*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;
}
2.MPU6050.H
#ifndef _MPU6050_H
#define _MPU6050_H
uint8_t MPU6050_ReadReg(uint8_t reg_addr);
void MPU6050_WriteReg(uint8_t reg_addr,uint8_t data);
void MPU6050_Init();
void MPU6050_GetDate(int16_t *AccX,int16_t *AccZ,int16_t *AccY,
int16_t *GyroX,int16_t *GyroZ,int16_t *GyroY
);
#endif
MPU6050寄存器文件
1.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
最后
我们想使用不同的方式来完成对加速度计和陀螺仪六个数据的读取
/*在C语言中,函数的返回值只有一个,我们想要返回多个数据,就是多返回值的设计
1.使用全局变量
2.使用指针,进行变量的地址传递,来实现多返回值
3.用结构体对多个变量进行打包,然后再统一进行传递
*/
使用指针的方式
MUC6050.c
void MPU6050_GetDate(int16_t *AccX,int16_t *AccZ,int16_t *AccY,
int16_t *GyroX,int16_t *GyroZ,int16_t *GyroY
){
uint8_t DataH, DataL;
DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);//读取加速度寄存器X轴的高8位
DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);//读取加速度寄存器X轴的高低位
*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;
}
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "USART.h"
#include "MYI2C.h"
#include "MPU6050.h"
#include "LED.h"
int16_t AX, AY, AZ, GX, GY, GZ;
uint8_t ID;
int main(void)
{
Usart_Init();
MyI2C_Init();
MPU6050_Init();
while(1)
{
MPU6050_GetDate(&AX, &AY, &AZ, &GX, &GY, &GZ);
printf("AX=%d,AY=%d,AZ=%d,GX=%d,GY=%d,GZ=%d\n",AX,AY,AZ,GX,GY,GZ);
Delay_ms(200);
}
}
使用结构体方式
1.MPU6050.c
#include "stm32f10x.h" // Device header
#include "MPU6050_Reg.h"
#include "MYI2C.h"
#include "stdlib.h"
#include "MPU6050.h"
#define MPU6050_I2C_ADDR (0x68)
#define MPU6050_WRITE_ADDR (((MPU6050_I2C_ADDR) << 1) | 0x00) //写
#define MPU6050_READ_ADDR (((MPU6050_I2C_ADDR) << 1) | 0x01) //读
//指定地址读寄存器
extern MPU6050_DATA;
MPU6050_DATA pDATA;
uint8_t MPU6050_ReadReg(uint8_t reg_addr){
uint8_t data;
MyI2C_Start(); //起始位
MyI2C_SendByte(MPU6050_WRITE_ADDR); //从机地址+读
MyI2C_ReceiveACK(); //接收应答,这里有应答位可以判断从机有没有接收到数据
//寻址找到从机之后就可以发送下一个字节了
MyI2C_SendByte(reg_addr); //用来指定我写的地址,这个地址就写到从机的地址指针中了
//从机接收到reg_addr之后,它的寄存器指针就指向了0x19这个位置
MyI2C_ReceiveACK(); //接收应答
MyI2C_Start(); //再来一个起始条件
MyI2C_SendByte(MPU6050_READ_ADDR); //再次指定设备的ID号,这次我们读
MyI2C_ReceiveACK(); //主机接收应答
data = MyI2C_ReceiveByte(); //指针指向的寄存器的数据
MyI2C_SendACK(1); //主机接收后,发送应答
MyI2C_Stop(); //结束
return data;
}
//指定地址写寄存器
void MPU6050_WriteReg(uint8_t reg_addr,uint8_t data){
MyI2C_Start(); ////起始位
MyI2C_SendByte(MPU6050_WRITE_ADDR); //从机地址+写
MyI2C_ReceiveACK(); ////接收应答,这里有应答位可以判断从机有没有接收到数据
MyI2C_SendByte(reg_addr); // //用来指定我写的寄存器地址
MyI2C_ReceiveACK(); //接收应答
MyI2C_SendByte(data); //写入的数据
MyI2C_ReceiveACK(); //接收应答
MyI2C_Stop(); //结束
}
void MPU6050_Init(){
MyI2C_Init();
MPU6050_WriteReg(MPU6050_PWR_MGMT_1,0x01); //关闭睡眠模式
MPU6050_WriteReg(MPU6050_PWR_MGMT_2,0x00);
MPU6050_WriteReg(MPU6050_SMPLRT_DIV,0x09);
MPU6050_WriteReg(MPU6050_CONFIG,0x06);
MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x00);
MPU6050_WriteReg(MPU6050_ACCEL_CONFIG,0x00);
}
// 读取MPU6050数据并存储在指定结构体中
void MPU6050_GetData(MPU6050_DATA* pDATA) {
uint16_t dataH, dataL;
// AccX
dataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);
dataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);
pDATA->AccX = (dataH << 8) | dataL;
// AccY
dataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
dataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
pDATA->AccY = (dataH << 8) | dataL;
// AccZ
dataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
dataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
pDATA->AccZ = (dataH << 8) | dataL;
// GyroX
dataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
dataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
pDATA->GyroX = (dataH << 8) | dataL;
// GyroY
dataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
dataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
pDATA->GyroY = (dataH << 8) | dataL;
// GyroZ
dataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
dataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
pDATA->GyroZ = (dataH << 8) | dataL;
// TEMP
dataH = MPU6050_ReadReg(MPU6050_TEMP_OUT_H);
dataL = MPU6050_ReadReg(MPU6050_TEMP_OUT_L);
pDATA->Temp = (dataH << 8) | dataL;
}
2.MPU6050.h
#ifndef _MPU6050_H
#define _MPU6050_H
typedef struct{
int16_t AccX,AccY,AccZ;
int16_t GyroX,GyroY,GyroZ;
int16_t Temp;
}MPU6050_DATA;
void MPU6050_Init();
void MPU6050_WriteReg(uint8_t reg_addr,uint8_t data);
uint8_t MPU6050_ReadReg(uint8_t reg_addr);
void MPU6050_GetData(MPU6050_DATA *pDATA);
#endif
3.main.c
#include "Delay.h"
#include "OLED.h"
#include "USART.h"
#include "MPU6050.h"
#include "LED.h"
#include "stdio.h"
#include <stdio.h>
#include <math.h>
float ConvertTemperature(int16_t rawTemp) {
return (rawTemp / 340.0f) + 36.53f;
}
// 函数用于将原始陀螺仪数据转换为角速度(度/秒)
float ConvertGyro(int16_t rawGyro) {
return rawGyro / 131.0f;
}
// 函数用于将原始加速度数据转换为加速度(g)
float ConvertAcc(int16_t rawAcc) {
return rawAcc / 16384.0f;
}
int main(void){
MPU6050_Init();
Usart_Init();
MPU6050_DATA mpuData;
while (1){
MPU6050_GetData(&mpuData);
// 转换并打印温度
float temperature = ConvertTemperature(mpuData.Temp);
printf("温度是:%.2f°C\n", temperature);
// 转换并打印陀螺仪数据
float gyroX = ConvertGyro(mpuData.GyroX);
float gyroY = ConvertGyro(mpuData.GyroY);
float gyroZ = ConvertGyro(mpuData.GyroZ);
printf("GyroX=%.2f°/s, GyroY=%.2f°/s, GyroZ=%.2f°/s\n", gyroX, gyroY, gyroZ);
// 转换并打印加速度数据
float accX = ConvertAcc(mpuData.AccX);
float accY = ConvertAcc(mpuData.AccY);
float accZ = ConvertAcc(mpuData.AccZ);
printf("AccX=%.2fg, AccY=%.2fg, AccZ=%.2fg\n", accX, accY, accZ);
Delay_ms(100);
}
}
数据显示结果

测试代码
/*指定地址写*/
MyI2C_Start();//开始传输
//起始之后,主机必须首先发送一个字节,内容是从机地址+读写位,进行寻址
MyI2C_SendByte(0xD0); //1101 000 0(mpu6050地址+读写位)
//发送一个字节后,我们要接收一个应答位,看看从机有没有收到刚才的数据
uint8_t Ack=MyI2C_ReceiveAck();//我们判断Ack的值就知道从机有没有给我们应答了
//接收应答之后,我们要继续再发送一个字节,写入寄存器地址
MyI2C_Stop();
printf("Ack=%d\n",Ack);
//测试读寄存器
uint8_t ID;
Usart_Init();
MPU6050_Init();
// who I am I寄存器,只读,地址0x75,默认是ID号,默认值就是0x60
ID=MPU6050_ReadReg(0x75);//返回值是ID号的内容
printf("ID=%x\n",ID);
本文详细介绍了STM32通过模拟I2C与MPU6050传感器通信的过程,包括I2C通信协议的基础知识、起始和终止条件、发送与接收字节的时序,以及在实际代码中的应用。在实践中遇到了MPU6050寄存器读写问题,通过分析和调试,提供了完整的模拟I2C通信代码和MPU6050的C语言实现。
794

被折叠的 条评论
为什么被折叠?



