一篇文章教会你I2C通信(软件I2C和硬件I2C)以读取MPU6050为例,附STM32代码示例
目录
一、I2C通信介绍:
(1)基本概念:
(2)特点:
(3)工作原理:
二、I2C通信原理:
(1)I2C 物理层:
(2)开漏连接:
(3)I2C协议:
I2C 启动和停止:
逻辑 1 和 0:
I2C通信帧:
三、硬件I2C:
(1)特点:
(2)硬件I2C读取MPU6050:
初始化I2C接口:
向MPU6050寄存器写入数据(传输一个数据为例):
读取MPU6050一个寄存器的值:
MPU6050寄存器初始化配置:
MPU6050获取ID号:
MPU6050获取数据三轴加速度和角速度:
MPU6050获取温度:
主函数及完成效果:
四、软件I2C:
(1)特点:
(2)软件I2C读取MPU6050:
初始化引脚配置(开漏输出):
SCL和SDA引脚读写操作:
生成Start条件和Stop条件:
发送和读取一个字节:
接收发送应答信号:
向MPU6050寄存器写入数据(传输一个数据为例):
读取MPU6050一个寄存器的值:
MPU6050寄存器初始化配置:
MPU6050获取ID号:
MPU6050获取数据三轴加速度和角速度:
MPU6050获取温度:
主函数及完成效果:
五、源码下载及MPU6050资料:
一、I2C通信介绍:
I2C(Inter-Integrated Circuit)是一种通用的数字通信协议,主要用于微控制器和各种外围设备之间的通信。
(1)基本概念:
- I2C是一种多主机、两线制、低速串行通信总线,使用两条线路:串行数据线(SDA)和串行时钟线(SCL)进行双向传输。
(2)特点:
- 两线制总线:I2C仅使用两条线——串行数据线(SDA)和串行时钟线(SCL)进行通信,有效降低了连接复杂性。
- 多主多从设备支持:I2C支持多个主设备和多个从设备连接到同一总线上。每个设备都有唯一的地址。
- 可变的时钟速率:I2C总线支持不同的速率模式,如标准模式(100kbps)、快速模式(400kbps)和高速模式(3.4Mbps)。
- 同步通信:I2C是一种同步通信协议,数据传输由时钟信号(SCL)来控制。
- 简单的连接:I2C通信对硬件的要求比较低,很容易在微控制器和外围设备间实现连接。
- 地址分配:每个I2C设备都通过一个7位或10位的地址来识别,这使得总线上可以连接多个设备。
- 阻塞传输:I2C支持阻塞传输机制,即主设备可以在传输过程中等待从设备准备好数据。

(3)工作原理:
- I2C通信基于一个主设备和一个或多个从设备的多主机架构。通信开始于主设备向从设备发送起始信号,然后在时钟信号的同步下,主设备向从设备发送或接收数据。通信结束时,主设备发送停止信号,释放总线。
二、I2C通信原理:
(1)I2C 物理层:
I2C 系统具有两条共享通信线路,用于总线上的所有设备。这两条线路用于双向、半双工通信。I2C 允许多个控制器和多个目标设备。这两条线路都需要上拉电阻。
I2C 成为通用协议的原因之一是只有两条线路用于通信。第一条线路是 SCL,它是一个主要由控制器设备控制的串行时钟。SCL 用于同步输入或输出目标器件的数据。第二条线路是 SDA,它是串行数据线。SDA 用于与目标设备之间传输数据。例如,控制器设备可以将配置数据和输出代码发送到目标数模转换器 (DAC),或者目标模数转换器 (ADC) 可以将转换数据发送回控制器设备。
I2C 是半双工通信,一次只有一个控制器或目标设备在总线上发送数据。相比之下,串行外设接口 (SPI) 是一种全双工协议,可以同时发送和接收数据。SPI 需要四条线路进行通信,两条数据线用于向目标设备发送数据或从目标设备发送数据。除了串行时钟之外,唯一的 SPI 片选线还选择用于通信的设备,还有两条数据线,用于目标设备的输入和输出。
I2C 控制器器件启动和停止通信,从而消除了总线争用的潜在问题。与目标设备的通信通过总线上的唯一地址发送。这允许在 I2C 总线上同时使用多个控制器和多个目标设备。SDA 和 SCL 线路与总线上的所有器件均采用漏极开路连接。这需要一个上拉电阻器连接到公共电源
(2)开漏连接:
开漏连接用于 SDA 和 SCL 线路,并连接到 NMOS 晶体管。这种漏极开路连接控制 I2C 通信线路,并将线路拉低或释放线路高电平。漏极开路是指 NMOS 关闭时的 NMOS 总线连接。

要设置 SDA 或 SCL 线路的电压电平,需要将 NMOS 设置为打开或关闭。当 NMOS 导通时,该器件通过电阻器将电流拉至地。这会将开漏线拉低。通常,I2C 从高到低的转换是一个快速转换,因为 NMOS 在 SDA 或 SCL 上拉低。转换速度由 NMOS 驱动强度和 SDA 或 SCL 上的任何总线电容决定。当 NMOS 关闭时,该器件停止拉电流,上拉电阻将 SDA 或 SCL 线路拉至 VDD。如图显示了 NMOS 关闭时的漏极开路。上拉电阻将线路拉高。漏极开路线路的转换速度较慢,因为线路是靠着总线电容上拉的,而不是被主动驱动的。
通过控制这种开漏连接,SDA 和 SCL 都可以设置为高电平和低电平,从而实现 I2C 通信。上拉器件 I2C SDA 或 SCL 电压 SDA 或 SCL 电压由于 I2C 通信线上的电容,SDA 和 SCL 线路以指数建立 RC 时间常数放电,具体取决于上拉电阻的大小和 I2C 总线上的电容。较高的电容会限制 I2C 通信的速度、器件数量以及总线上器件之间的物理距离。较小的上拉电阻具有更快的上升时间,但需要更多的通信功率。较大的上拉电阻上升时间较慢,导致通信速度较慢,但需要的功率较小。
(3)非破坏性总线争用:
I2C 使用开漏的一个好处是总线争用不会使总线进入破坏性状态。通过漏极开路输出,许多器件可以连接在一起,而不会发生破坏性的争用。对于该连接上的任何输出,如果任何输出将线路 拉低,则线路为低电平。这种连接称为有线与门连接。输出是所有输出的逻辑与。
如果输出是推挽式的连接类型,则输出不能捆绑在一起,而不会产生破坏性状态。推挽输出(通常用于 SPI 通信)具有互补的 NMOS 和 PMOS 晶体管,可驱动输出为高电平或低电平。如图显示了争用中开漏输出和推挽输出之间的比较。

通过开漏连接,任何设备都可以随时将连接拉低。每当任何设备将线路拉低时,该线路就会显得较低,但不会显示为破坏性争用。器件I2C在推挽输出中,输出也连接在一起。如果总线上有两个器件处于活动状态,并且一个输出为高电平,另一个输出为低电平,则此总线争用具有未确定的状态,可能在中间电源点建立。
此外,一个器件具有 NMOS 导电电流,另一个器件具有 PMOS 导电电流。这些器件通过非常低阻抗的路径将电流从 VDD 源到 GND,在晶体管允许的范围内传导尽可能多的电流。这种争用的结果可能是大量的电流,可能会损坏设备。
(3)I2C协议:
I2C 启动和停止:
I2C通信由控制器设备通过发送START条件启动,该过程涉及将SDA线拉低后紧接着将SCL线拉低,以声明总线并开始通信。通信结束后,控制器通过释放SCL线使其回到高电平,然后释放SDA线回到高电平,形成STOP条件,从而释放总线,允许其他控制器设备进行通信。简而言之,I2C通信的开始和结束分别由START和STOP条件来标识,确保了总线上数据传输的有序进行。

逻辑 1 和 0:
I2C通信协议通过SDA线传输数据位,SCL线提供时钟信号来同步这些数据位。当SDA线处于释放状态时,上拉电阻将其拉高至逻辑1,表示数据位为1;而当SDA线被下拉时,表示数据位为0,即逻辑0,此时线路接近地电位。简而言之,I2C通信中SDA线的电平状态由数据位决定,SCL线则负责同步数据传输。

当 SCL 脉冲时接收 1 和 0。对于有效的位, SDA 在该位的时钟线的上升沿和下降沿之间不会变化。SCL 上升沿和下降沿之间的 SDA 变化可以解释为 I2C 总线上的 START 或 STOP 条件。
I2C通信帧:
I2C 协议被分解为帧。通信从控制器设备在 START 后发送地址帧开始。地址帧后跟一个或多个数据帧,每个数据帧由一个字节组成。每个帧还有一个确认位,用于提醒控制器目标设备或控制器设备已收到通信。

在I2C通信中,控制器设备通过首先将SDA拉低然后拉低SCL来启动START条件,以此认领总线并开始通信。每个I2C目标设备都有一个唯一的7位I2C地址,控制器在通信时使用这个地址来指定特定的目标设备,确保数据能够准确地发送或接收到正确的设备。I2C通信的开始是通过START条件来实现的,而目标设备的识别则是通过其唯一的7位I2C地址来完成的。
I2C通信使用7位地址,理论上可以有128个唯一地址,但由于存在一些保留地址,实际可用的地址数量会少于128个。这些地址通过SDA线发送数据,SCL线提供时钟信号。在地址帧之后,第8位是读写位(R/W),用于指示控制器是要从目标设备读取数据(1)还是向目标设备写入数据(0)。通信过程中,第9位用于确认通信是否成功,称为ACK位。如果目标设备在SCL脉冲期间下拉SDA,表示成功接收到地址并确认(ACK),如果SDA保持高电平,则表示没有设备响应,通信不成功(NACK)。简而言之,I2C通信通过7位地址识别设备,第8位确定读写方向,第9位确认通信成功与否。
在I2C通信中,地址帧之后是一个或多个数据帧,每个数据帧包含一个字节的数据。每次传输数据字节后,都会跟随一个ACK位,用于确认数据传输成功。对于写操作,目标设备通过拉低SDA来发送ACK;对于读操作,控制器通过拉低SDA来发送ACK。缺少ACK可能意味着通信过程中存在问题,如地址错误或数据未按预期接收。通信结束后,控制器通过释放SCL和SDA发出STOP条件,释放I2C总线,标志着通信的完成。这个基本协议允许控制器和目标设备之间进行有效的数据交换,支持多字节数据传输,并允许通过读写操作访问目标设备的多个数据和配置寄存器。I2C通信通过地址帧和数据帧实现设备间的字节级数据交换,并以STOP条件结束通信。
三、硬件I2C:
硬件I2C是指直接利用微控制器芯片中的硬件I2C外设来实现I2C通信协议的方式。这种硬件I2C外设类似于USART串口外设,通过配置对应的寄存器,外设就会产生符合I2C协议的时序。使用硬件I2C可以方便地通过外设寄存器来控制硬件I2C外设产生I2C协议方式的通讯,而不需要内核直接控制引脚的电平。
(1)特点:
-
通信引脚:硬件I2C必须要使用特定的引脚,因为这些引脚才连接到I2C外设。例如,在STM32中,PB6与PB7引脚就连接到芯片内部的I2C1外设。
-
效率:硬件I2C的效率要远高于软件I2C,因为它直接调用内部寄存器进行配置,而软件I2C是通过GPIO,软件模拟寄存器的工作方式。
-
速度与DMA:硬件I2C速度比模拟快,并且可以用DMA(直接内存访问),这是软件I2C所不具备的。
-
引脚限制:使用硬件I2C时必须使用某些固定的引脚作为SCL和SDA,而软件I2C可以使用任意GPIO引脚,相对比较灵活。
-
减轻CPU负担:硬件I2C直接使用外设来控制引脚,可以减轻CPU的负担。
-
硬件实现简单:I2C硬件实现简单,可扩展性强,广泛应用于集成电路模块之间的通信。
-
支持多主机和多从机:I2C支持多个设备共用的信号线,支持多个通讯主机及多个通讯从机。
(2)硬件I2C读取MPU6050:
STM32F103C8T6的硬件I2C通信过程主要包括初始化I2C接口、发送起始信号、发送设备地址、发送数据或接收数据以及发送停止信号。
初始化I2C接口:
// 初始化MPU6050传感器
void MPU6050_Init(void)
{/*开启时钟*/RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE); // 开启I2C2接口的时钟,使其能够工作RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // 开启GPIOB的时钟,因为I2C2的SCL和SDA线连接在GPIOB上/*GPIO初始化*/GPIO_InitTypeDef GPIO_InitStructure; // 定义一个GPIO初始化结构体GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 设置GPIO模式为复用开漏输出模式GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11; // 设置GPIOB的10号和11号引脚,对应I2C2的SCL和SDAGPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 设置GPIO的速度为50MHzGPIO_Init(GPIOB, &GPIO_InitStructure); // 应用上述配置到GPIOB/*I2C初始化*/I2C_InitTypeDef I2C_InitStructure; // 定义一个I2C初始化结构体I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; // 设置I2C模式为I2C模式I2C_InitStructure.I2C_ClockSpeed = 50000; // 设置I2C时钟速度为50kHzI2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; // 设置I2C时钟的占空比为2:1I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; // 使能应答机制I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; // 设置I2C设备地址长度为7位I2C_InitStructure.I2C_OwnAddress1 = 0x00; // 设置I2C设备自己的地址为0x00I2C_Init(I2C2, &I2C_InitStructure); // 应用上述配置到I2C2接口/*I2C使能*/I2C_Cmd(I2C2, ENABLE); // 使能I2C2接口Reg_Init();
}
在I2C通信中,设备可以被配置为主机(Master)或从机(Slave)。STM32微控制器可以同时支持主机和从机模式,这取决于你的应用需求和硬件设计。
主机模式(Master Mode):
- 在I2C通信中,主机模式允许设备控制整个通信过程,包括发送起始和停止条件、管理时钟信号以及与多个从机设备进行数据交换,这通常用于需要主动发起数据请求或控制操作的场景,如从传感器读取数据或控制LED等。在STM32微控制器中,通过配置I2C_Init函数并设置I2C_Mode为I2C_Mode_I2C,设备可以被设置为主机模式,进而使用I2C_GenerateSTART等函数来管理I2C总线上的数据传输。简而言之,主机模式使设备能够主动地在I2C总线上发起和管理通信。
从机模式(Slave Mode):
- 在I2C通信中,从机模式使设备能够响应主机的通信请求,根据主机的指令来发送或接收数据,通常只响应一个特定的地址,适用于需要提供数据给主机或接收主机命令的场景,如传感器数据传输或执行器控制。在STM32微控制器中,通过设置I2C_Mode为I2C_Mode_I2C并配置I2C_AcknowledgedAddress为7位地址,设备可以被设置为从机模式,需要设置I2C_OwnAddress1为设备的唯一地址,并使能应答,以便正确响应主机的通信请求。简而言之,从机模式允许设备在I2C总线上被动地响应主机的通信指令。
向MPU6050寄存器写入数据(传输一个数据为例):

第一步:发送起始信号,向I2C2总线发送起始条件,开始一次I2C通信。
第二步:等待I2C2总线事件,直到I2C主模式被选中(EV5事件)。
第三步:向I2C2总线发送MPU6050的7位I2C地址,设置为传输器模式(写操作)。
第四步:等待I2C2总线事件,直到主传输模式被选中(EV6事件)。
第五步:向I2C2总线发送要写入的寄存器地址。
第六步:等待I2C2总线事件,直到当前字节正在传输(EV8事件)。
第七步:向I2C2总线发送要写入的数据。
第八步: 等待I2C2总线事件,直到当前字节传输完成(EV8_2事件)。
第九步:向I2C2总线发送停止条件,结束这次I2C通信。
等待I2C2总线事件函数:
在STM32的I2C通信中,等待事件(或称为等待状态)是一种同步机制,确保在I2C总线上的每个操作步骤都正确完成,然后再进行下一步操作。
// 等待I2C事件,返回1表示成功,返回0表示超时
uint8_t MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{uint32_t Timeout = 10000; // 给定超时计数时间while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS) // 循环等待指定事件{Timeout--; // 等待时,计数值自减if (Timeout == 0) // 自减到0后,等待超时{// 超时的错误处理代码,可以添加到此处// 例如:记录错误、重置I2C总线等return 0; // 返回0表示超时}}return 1; // 返回1表示成功等待到事件
}
向MPU6050寄存器写入数据函数:
//写入MPU6050寄存器
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{I2C_GenerateSTART(I2C2, ENABLE); // 向I2C2总线发送起始条件,开始一次I2C通信MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); // 等待I2C2总线事件,直到I2C主模式被选中(EV5事件)I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter); // 向I2C2总线发送MPU6050的7位I2C地址,设置为传输器模式(写操作)MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); // 等待I2C2总线事件,直到主传输模式被选中(EV6事件)I2C_SendData(I2C2, RegAddress); // 向I2C2总线发送要写入的寄存器地址MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING); // 等待I2C2总线事件,直到当前字节正在传输(EV8事件)I2C_SendData(I2C2, Data); // 向I2C2总线发送要写入的数据MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED); // 等待I2C2总线事件,直到当前字节传输完成(EV8_2事件)I2C_GenerateSTOP(I2C2, ENABLE); // 向I2C2总线发送停止条件,结束这次I2C通信
}
读取MPU6050一个寄存器的值:

第一步:发送起始信号,向I2C2总线发送起始条件,开始一次I2C通信。
第二步:等待I2C2总线事件,直到I2C主模式被选中(EV5事件)。
第三步:向I2C2总线发送MPU6050的7位I2C地址,设置为传输器模式(写操作)。
第四步:等待I2C2总线事件,直到主传输模式被选中(EV6事件)。
第五步:向I2C2总线发送要读取的寄存器地址。
第六步:等待I2C事件,直到寄存器地址被成功发送(EV8_2事件)。
第七步:再次向I2C2总线发送起始条件,开始第二次I2C通信,用于读取数据。
第八步: 再次等待I2C事件,直到I2C主模式被选中(EV5事件)。
第九步:向I2C2总线发送MPU6050的7位I2C地址,设置为接收器模式(读操作)。
第十步:等待I2C事件,直到主接收模式被选中(EV6事件)。
第十一步:禁用I2C2的应答机制,因为只需要读取一个字节,不需要发送ACK。
第十二步:向I2C2总线发送停止条件,结束这次I2C通信。
第十三步:等待I2C事件,直到一个字节的数据被成功接收(EV7事件)。
第十四步:从I2C2总线读取接收到的数据,并存储到变量中。
第十五步:重新启用I2C2的应答机制,以便后续的I2C通信可以使用ACK。
// MPU6050读取一个寄存器的值,参数是寄存器地址
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{uint8_t Data; // 定义一个变量Data,用于存储从MPU6050读取的数据I2C_GenerateSTART(I2C2, ENABLE); // 向I2C2总线发送起始条件,开始一次I2C通信MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); // 等待I2C事件,直到I2C主模式被选中(EV5)I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter); // 向I2C2总线发送MPU6050的7位I2C地址,设置为传输器模式(写操作)MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); // 等待I2C事件,直到主传输模式被选中(EV6)I2C_SendData(I2C2, RegAddress); // 向I2C2总线发送要读取的寄存器地址MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED); // 等待I2C事件,直到寄存器地址被成功发送(EV8_2)I2C_GenerateSTART(I2C2, ENABLE); // 再次向I2C2总线发送起始条件,开始第二次I2C通信,用于读取数据MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); // 再次等待I2C事件,直到I2C主模式被选中(EV5)I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver); // 向I2C2总线发送MPU6050的7位I2C地址,设置为接收器模式(读操作)MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED); // 等待I2C事件,直到主接收模式被选中(EV6)I2C_AcknowledgeConfig(I2C2, DISABLE); // 禁用I2C2的应答机制,因为只需要读取一个字节,不需要发送ACKI2C_GenerateSTOP(I2C2, ENABLE); // 向I2C2总线发送停止条件,结束这次I2C通信MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED); // 等待I2C事件,直到一个字节的数据被成功接收(EV7)Data = I2C_ReceiveData(I2C2); // 从I2C2总线读取接收到的数据,并存储到变量Data中I2C_AcknowledgeConfig(I2C2, ENABLE); // 重新启用I2C2的应答机制,以便后续的I2C通信可以使用ACKreturn Data; // 返回从MPU6050读取的数据
}
MPU6050寄存器映射地址:


#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 // 加速度计X轴高8位数据寄存器地址。
#define MPU6050_ACCEL_XOUT_L 0x3C // 加速度计X轴低8位数据寄存器地址。
#define MPU6050_ACCEL_YOUT_H 0x3D // 加速度计Y轴高8位数据寄存器地址。
#define MPU6050_ACCEL_YOUT_L 0x3E // 加速度计Y轴低8位数据寄存器地址。
#define MPU6050_ACCEL_ZOUT_H 0x3F // 加速度计Z轴高8位数据寄存器地址。
#define MPU6050_ACCEL_ZOUT_L 0x40 // 加速度计Z轴低8位数据寄存器地址。
#define MPU6050_TEMP_OUT_H 0x41 // 温度传感器高8位数据寄存器地址。
#define MPU6050_TEMP_OUT_L 0x42 // 温度传感器低8位数据寄存器地址。
#define MPU6050_GYRO_XOUT_H 0x43 // 陀螺仪X轴高8位数据寄存器地址。
#define MPU6050_GYRO_XOUT_L 0x44 // 陀螺仪X轴低8位数据寄存器地址。
#define MPU6050_GYRO_YOUT_H 0x45 // 陀螺仪Y轴高8位数据寄存器地址。
#define MPU6050_GYRO_YOUT_L 0x46 // 陀螺仪Y轴低8位数据寄存器地址。
#define MPU6050_GYRO_ZOUT_H 0x47 // 陀螺仪Z轴高8位数据寄存器地址。
#define MPU6050_GYRO_ZOUT_L 0x48 // 陀螺仪Z轴低8位数据寄存器地址。#define MPU6050_PWR_MGMT_1 0x6B // 电源管理寄存器1地址,用于设置传感器的电源模式。
#define MPU6050_PWR_MGMT_2 0x6C // 电源管理寄存器2地址,用于设置Z轴和Y轴的待机模式。
#define MPU6050_WHO_AM_I 0x75 // 用于读取设备ID的寄存器地址,用于验证传感器。#endif
MPU6050寄存器初始化配置:
void Reg_Init(void)
{// 写入电源管理寄存器1 (PWR_MGMT_1),设置为0x01,取消睡眠模式,并选择PLL时钟作为主时钟源。// 这意味着传感器将被唤醒,并且时钟源被设置为X轴陀螺仪的输出。MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01); // 写入电源管理寄存器2 (PWR_MGMT_2),设置为0x00,保持所有电源管理设置为默认值。// 这通常意味着不进入待机模式,所有传感器都保持活动状态。MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00); // 写入采样率分频寄存器 (SMPLRT_DIV),设置为0x09。// 这个寄存器用于控制传感器数据输出的速率。具体来说,它决定了采样率与内部采样率的比例。// 例如,如果内部采样率是1kHz,设置为0x09意味着输出数据速率是100Hz。MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09); // 写入配置寄存器 (CONFIG),设置为0x06。// 这个寄存器用于配置数字低通滤波器 (DLPF) 的截止频率。// 设置为0x06通常意味着DLPF的截止频率被设置为21.2Hz,这有助于减少噪声。MPU6050_WriteReg(MPU6050_CONFIG, 0x06); // 写入陀螺仪配置寄存器 (GYRO_CONFIG),设置为0x18。// 这个寄存器用于设置陀螺仪的满量程范围。// 设置为0x18意味着选择±2000°/s的满量程范围。MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18); // 写入加速度计配置寄存器 (ACCEL_CONFIG),设置为0x18。// 这个寄存器用于设置加速度计的满量程范围。// 设置为0x18意味着选择±16g的满量程范围。MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);
}
MPU6050获取ID号:
//MPU6050获取ID号
uint8_t MPU6050_GetID(void)
{return MPU6050_ReadReg(MPU6050_WHO_AM_I); //返回WHO_AM_I寄存器的值
}
MPU6050获取数据三轴加速度和角速度:
从MPU6050的相应寄存器中读取加速度和陀螺仪的高8位和低8位数据,然后将这些数据拼接成16位的数据,并通过指针参数返回。
//MPU6050获取数据
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; // 定义数据高8位和低8位的变量DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H); // 读取加速度计X轴的高8位数据DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L); // 读取加速度计X轴的低8位数据*AccX = (DataH << 8) | DataL; // 数据拼接,通过输出参数返回DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H); // 读取加速度计Y轴的高8位数据DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L); // 读取加速度计Y轴的低8位数据*AccY = (DataH << 8) | DataL; // 数据拼接,通过输出参数返回DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H); // 读取加速度计Z轴的高8位数据DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L); // 读取加速度计Z轴的低8位数据*AccZ = (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); // 读取陀螺仪Y轴的高8位数据DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L); // 读取陀螺仪Y轴的低8位数据*GyroY = (DataH << 8) | DataL; // 数据拼接,通过输出参数返回DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H); // 读取陀螺仪Z轴的高8位数据DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L); // 读取陀螺仪Z轴的低8位数据*GyroZ = (DataH << 8) | DataL; // 数据拼接,通过输出参数返回
}
MPU6050获取温度:
void MPU6050_TEMP(float *Temp){uint8_t DataH, DataL; // 定义数据高8位和低8位的变量int16_t Demo ;DataH = MPU6050_ReadReg(MPU6050_TEMP_OUT_H); // 读取温度的高8位数据DataL = MPU6050_ReadReg(MPU6050_TEMP_OUT_L); // 读取温度的低8位数据// 将高8位和低8位拼接成一个16位的数据,并转换为摄氏温度// 注意:这里使用int16_t类型来确保结果是有符号的Demo = (int16_t)((DataH << 8) | DataL);// 将原始温度值转换为摄氏温度*Temp = 36.53+(double)(Demo/340)-10; //读取发现与实际温度相差10摄氏度
}
主函数及完成效果:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MPU6050.h"uint8_t ID; //定义用于存放ID号的变量
int16_t AX, AY, AZ, GX, GY, GZ; //定义用于存放各个数据的变量
float Temp;
int main(void)
{/*OLED初始化*/OLED_Init();MPU6050_Init();/*显示ID号*/OLED_ShowString(0, 0, "ID:",OLED_8X16); //显示静态字符串ID = MPU6050_GetID(); //获取MPU6050的ID号OLED_ShowHexNum(24, 0, ID, 2, OLED_8X16); //OLED显示ID号,16进制 //OLED_ShowSignedNum(48, 0, ID, 3, OLED_8X16); //OLED显示ID号,10进制OLED_ShowString(48, 6, "temp:",OLED_6X8); //显示静态字符串/*调用OLED_Update函数,将OLED显存数组的内容更新到OLED硬件进行显示*/OLED_Update();while (1){MPU6050_TEMP(&Temp);OLED_ShowFloatNum(80, 6, Temp, 3,2,OLED_6X8);MPU6050_GetData(&AX, &AY, &AZ, &GX, &GY, &GZ); //获取MPU6050的数据OLED_ShowSignedNum( 0, 16, AX, 5,OLED_8X16); //OLED显示数据OLED_ShowSignedNum( 0, 32, AY, 5,OLED_8X16);OLED_ShowSignedNum( 0, 48, AZ, 5,OLED_8X16);OLED_ShowSignedNum(64, 16, GX, 5,OLED_8X16);OLED_ShowSignedNum(64, 32, GY, 5,OLED_8X16);OLED_ShowSignedNum(64, 48, GZ, 5,OLED_8X16);OLED_Update();}
}

四、软件I2C:
软件I2C(Software I2C)是一种通过软件模拟I2C通信协议的方法,它不依赖于微控制器上的硬件I2C外设,而是完全由软件控制GPIO引脚来实现I2C通信的所有特性。
(1)特点:
- 基本原理:软件I2C通过编程控制GPIO引脚的高低电平变化来模拟I2C总线上的SCL(时钟线)和SDA(数据线)的通信过程。这包括生成起始信号、发送数据位、接收应答位、生成停止信号等。
- 灵活性:软件I2C的一个主要优点是灵活性高,可以使用任意的GPIO引脚作为SCL和SDA,不受硬件I2C引脚的限制。
- 可移植性和实现方法:软件I2C驱动库可以设计成可移植性高的模块,可以轻松地移植到不同的单片机平台上。实现软件I2C时,需要编写代码来控制GPIO引脚的输出(高电平或低电平)和输入(读取SDA线状态),以及实现I2C协议的时间序列。
- 初始化函数和通信过程:软件I2C的初始化函数通常包括配置GPIO引脚为开漏输出模式,并设置为高电平,以准备I2C通信。 在软件I2C的通信过程中,需要手动控制SCL和SDA的时序,包括起始条件、数据传输、应答和停止条件。
- 优点和缺点:软件I2C的优点是节省硬件资源,提高引脚的利用率,缺点是占用CPU资源较多,可能影响CPU处理其他任务的能力。
(2)软件I2C读取MPU6050:
初始化引脚配置(开漏输出):
//引脚配置,初始化I2C引脚,开漏输出模式
void SoftI2C_Init(void)
{//开启时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟//初始化GPIOGPIO_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引脚读写操作:
软件I2C需要通过添加适当的延时来模拟硬件I2C控制器的时序控制功能,以确保信号的上升时间、下降时间和数据保持时间符合I2C协议的严格要求,保证信号稳定,减少信号冲突,提高与不同I2C设备的兼容性,并减少过快电平变化可能引起的电磁干扰,从而确保主从设备间的正确同步和数据传输的可靠性。
//I2C写SCL引脚电平,参数1或0,对应高低电平
void I2C_W_SCL(uint8_t BitValue)
{ // 写入GPIOB的第10脚(SCL),设置为高电平或低电平,取决于BitValue参数GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue); // 延时大约10微秒,以确保SCL线上的电平变化符合I2C协议的时序要求 Delay_us(10);
}//I2C写SDA引脚电平
void I2C_W_SDA(uint8_t BitValue)
{// 写入GPIOB的第11脚(SDA),设置为高电平或低电平,取决于BitValue参数GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue); // 延时大约10微秒,以确保SCL线上的电平变化符合I2C协议的时序要求Delay_us(10);
}//I2C读SDA引脚电平
uint8_t I2C_R_SDA(void)
{ uint8_t BitValue;//读取SDA电平BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11); // 延时大约10微秒,以确保SCL线上的电平变化符合I2C协议的时序要求Delay_us(10);//返回SDA电平 return BitValue;
}
生成Start条件和Stop条件:

//生成Start条件
void I2C_Start(void)
{I2C_W_SDA(1); // 将SDA(数据线)设置为高电平,确保在起始信号之前,数据线是空闲的(高电平)。I2C_W_SCL(1); // 将SCL(时钟线)设置为高电平,确保在起始信号之前,时钟线也是空闲的(高电平)。I2C_W_SDA(0); // 将SDA拉低,生成I2C起始信号的第一个条件(SDA在SCL高电平时由高变低)。I2C_W_SCL(0); // 将SCL拉低,生成I2C起始信号的第二个条件(SCL保持低电平,以完成起始信号的生成)。
}//生成Stop条件
void I2C_Stop(void)
{I2C_W_SDA(0); // 将SDA(数据线)设置为低电平,开始生成I2C停止信号的第一个条件(SDA在SCL高电平时由高变低)。I2C_W_SCL(1); // 将SCL(时钟线)设置为高电平,确保在停止信号期间,时钟线是空闲的(高电平)。I2C_W_SDA(1); // 将SDA释放为高电平,完成I2C停止信号的生成(SDA在SCL高电平时由低变高)。
}
发送和读取一个字节:

//I2C发送一个字节
void I2C_SendByte(uint8_t Byte)
{// 定义一个uint8_t类型的变量i,用作循环计数器uint8_t i; // 使用for循环,从0到7,共8次迭代,每次迭代发送一个字节的一位for (i = 0; i < 8; i++) {I2C_W_SDA(0x01 & (Byte >> (7 - i))); // 将Byte的每一位从最高位到最低位依次发送到I2C总线上,每次循环发送一位。I2C_W_SCL(1); // 将SCL线设置为高电平,允许从设备读取SDA线上的位I2C_W_SCL(0); // 将SCL线设置为低电平,完成当前位的传输,并准备发送下一位}
}//I2C接收一个字节
uint8_t I2C_ReceiveByte(void)
{// 定义循环计数器i和用于存储接收到的字节的变量Byte,并初始化Byte为0x00(即所有位都为0)。uint8_t i, Byte = 0x00; // 将SDA线设置为高电平,配置为输入模式,准备接收从设备发送的数据。I2C_W_SDA(1); // 使用for循环,从0到7,共8次迭代,每次迭代接收一个字节的一位for (i = 0; i < 8; i++) {I2C_W_SCL(1); // 将SCL线设置为高电平,允许从设备在SDA线上输出数据,同时主设备可以读取数据。if (I2C_R_SDA() == 1) { Byte |= (0x01 << (7 - i)); } // 读取SDA线的状态,如果为高电平(1),则将Byte的相应位设置为1。这里使用0x01(即00000001b)左移7-i位来确定Byte中哪一位应该被设置。I2C_W_SCL(0); // 将SCL线设置为低电平,完成当前位的读取,并准备读取下一位。}return Byte; // 返回接收到的字节。
}
接收发送应答信号:
在I2C通信中,数据以8位字节的形式传输,每传输一个字节后,发送器在第九个时钟脉冲期间释放数据线,等待接收器的应答信号。接收器通过在第九个时钟脉冲前将SDA线拉低来发送有效应答位(ACK),表示成功接收字节;如果拉高SDA线则发送非应答位(NACK),表示未成功接收。对于主控接收器,在接收完最后一个字节后,会发送NACK信号告知发送器停止数据传输,并准备接收停止信号。

//发送一个应答信号(ACK)
void I2C_SendAck(uint8_t AckBit)
{I2C_W_SDA(AckBit); // 将AckBit的值写入到SDA线上,AckBit为0时发送一个非应答信号(NACK),为1时发送一个应答信号(ACK)。I2C_W_SCL(1); // 将SCL线设置为高电平,允许从设备读取SDA线上的应答信号。I2C_W_SCL(0); // 将SCL线设置为低电平,完成当前的应答信号周期。
}//接收来自从设备的应答信号(ACK)
uint8_t I2C_ReceiveAck(void)
{// 定义一个uint8_t类型的变量AckBit,用于存储从设备返回的应答信号状态。uint8_t AckBit; // 将SDA线设置为高电平,这是在等待应答信号时主设备应该处于的状态,因为SDA是双向线,这里将其释放,准备读取从设备的数据。I2C_W_SDA(1); // 将SCL线设置为高电平,开始一个时钟周期,以便从设备可以在SDA线上发送应答信号。I2C_W_SCL(1); // 读取SDA线的状态,这个状态就是从设备返回的应答信号,如果从设备拉低SDA线,则AckBit为0(非应答NACK),如果从设备释放SDA线,则AckBit为1(应答ACK)。AckBit = I2C_R_SDA(); // 将SCL线设置为低电平,结束当前的时钟周期,准备进行下一个I2C操作。I2C_W_SCL(0); // 返回从设备发送的应答信号状态。return AckBit;
}
向MPU6050寄存器写入数据(传输一个数据为例):
在软件I2C的实现中,通常不需要等待特定的“事件”,因为所有的时序和通信都是通过软件控制GPIO引脚来手动管理的。与硬件I2C不同,硬件I2C有内置的事件和中断来处理通信的不同阶段,例如地址发送完成、数据发送完成等。而在软件I2C中,这些都需要自己通过编写代码来控制。
//MPU6050寄存器写入
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{I2C_Start(); // 生成I2C起始信号,开始一次通信。I2C_SendByte(MPU6050_ADDRESS); // 向I2C总线发送MPU6050的设备地址,准备进行数据传输。I2C_ReceiveAck(); // 接收从设备发送的应答信号(ACK),确认设备地址已被成功接收。I2C_SendByte(RegAddress); // 向I2C总线发送要写入的寄存器地址。I2C_ReceiveAck(); // 再次接收从设备发送的应答信号,确认寄存器地址已被成功接收。I2C_SendByte(Data); // 向I2C总线发送要写入寄存器的数据。I2C_ReceiveAck(); // 接收从设备发送的应答信号,确认数据已被成功接收。I2C_Stop(); // 生成I2C停止信号,结束当前的I2C通信。
}
读取MPU6050一个寄存器的值:
// MPU6050读取一个寄存器的值,参数是寄存器地址
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{uint8_t Data; // 定义一个uint8_t类型的变量Data,用于存储从MPU6050读取的数据。I2C_Start(); // 生成I2C起始信号,开始一次I2C通信。I2C_SendByte(MPU6050_ADDRESS); // 向I2C总线发送MPU6050的设备地址,准备进行数据传输。I2C_ReceiveAck(); // 接收从MPU6050发送的应答信号(ACK),确认设备地址已被成功接收。I2C_SendByte(RegAddress); // 向I2C总线发送要读取的寄存器地址。I2C_ReceiveAck(); // 再次接收从MPU6050发送的应答信号,确认寄存器地址已被成功接收。I2C_Start(); // 生成另一个I2C起始信号,开始第二次通信,用于读取数据。I2C_SendByte(MPU6050_ADDRESS | 0x01); // 向I2C总线发送MPU6050的设备地址,同时设置读位(R/W位为1),表示接下来是读取操作。I2C_ReceiveAck(); // 接收从MPU6050发送的应答信号,确认读取请求已被接收。Data = I2C_ReceiveByte(); // 从I2C总线上接收一个字节的数据,存储到变量Data中。I2C_SendAck(1); // 向MPU6050发送一个非应答信号(NACK),因为这是要接收的最后一个字节。I2C_Stop(); // 生成I2C停止信号,结束当前的I2C通信。return Data; // 返回从MPU6050读取的数据。
}
MPU6050寄存器初始化配置:
void Reg_Init(void)
{SoftI2C_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); //配置寄存器,配置DLPFMPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18); //陀螺仪配置寄存器,选择满量程为±2000°/sMPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18); //加速度计配置寄存器,选择满量程为±16g
}
MPU6050获取ID号:
//MPU6050获取ID号
uint8_t MPU6050_GetID(void)
{return MPU6050_ReadReg(MPU6050_WHO_AM_I); //返回WHO_AM_I寄存器的值
}
MPU6050获取数据三轴加速度和角速度:
//MPU6050获取数据
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; // 定义数据高8位和低8位的变量DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H); // 读取加速度计X轴的高8位数据DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L); // 读取加速度计X轴的低8位数据*AccX = (DataH << 8) | DataL; // 数据拼接,通过输出参数返回DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H); // 读取加速度计Y轴的高8位数据DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L); // 读取加速度计Y轴的低8位数据*AccY = (DataH << 8) | DataL; // 数据拼接,通过输出参数返回DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H); // 读取加速度计Z轴的高8位数据DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L); // 读取加速度计Z轴的低8位数据*AccZ = (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); // 读取陀螺仪Y轴的高8位数据DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L); // 读取陀螺仪Y轴的低8位数据*GyroY = (DataH << 8) | DataL; // 数据拼接,通过输出参数返回DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H); // 读取陀螺仪Z轴的高8位数据DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L); // 读取陀螺仪Z轴的低8位数据*GyroZ = (DataH << 8) | DataL; // 数据拼接,通过输出参数返回
}
MPU6050获取温度:
//MPU6050获取温度
void MPU6050_TEMP(float *Temp){uint8_t DataH, DataL; // 定义数据高8位和低8位的变量int16_t Demo ;DataH = MPU6050_ReadReg(MPU6050_TEMP_OUT_H); // 读取温度的高8位数据DataL = MPU6050_ReadReg(MPU6050_TEMP_OUT_L); // 读取温度的低8位数据// 将高8位和低8位拼接成一个16位的数据,并转换为摄氏温度// 注意:这里使用int16_t类型来确保结果是有符号的Demo = (int16_t)((DataH << 8) | DataL);// 将原始温度值转换为摄氏温度*Temp = 36.53+(double)(Demo/340)-10;
}
主函数及完成效果:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MPU6050.h"uint8_t ID;
int16_t AX, AY, AZ, GX, GY, GZ;
float Temp;
int main(void)
{/*OLED初始化*/OLED_Init();Reg_Init();/*显示ID号*/OLED_ShowString(0, 0, "ID:",OLED_8X16); //显示静态字符串ID = MPU6050_GetID(); //获取MPU6050的ID号OLED_ShowHexNum(24, 0, ID, 2, OLED_8X16); //OLED显示ID号,16进制 //OLED_ShowSignedNum(48, 0, ID, 3, OLED_8X16); //OLED显示ID号,10进制OLED_ShowString(48, 6, "temp:",OLED_6X8); //显示静态字符串/*调用OLED_Update函数,将OLED显存数组的内容更新到OLED硬件进行显示*/OLED_Update();while (1){MPU6050_TEMP(&Temp);OLED_ShowFloatNum(80, 6, Temp, 3,2,OLED_6X8);MPU6050_GetData(&AX, &AY, &AZ, &GX, &GY, &GZ); //获取MPU6050的数据OLED_ShowSignedNum( 0, 16, AX, 5,OLED_8X16); //OLED显示数据OLED_ShowSignedNum( 0, 32, AY, 5,OLED_8X16);OLED_ShowSignedNum( 0, 48, AZ, 5,OLED_8X16);OLED_ShowSignedNum(64, 16, GX, 5,OLED_8X16);OLED_ShowSignedNum(64, 32, GY, 5,OLED_8X16);OLED_ShowSignedNum(64, 48, GZ, 5,OLED_8X16);OLED_Update();}
}

五、源码下载及MPU6050资料:
通过网盘分享的文件:8- 软件I2C读取MPU6050
链接: https://pan.baidu.com/s/1cryGk8LnYWmWOfT9UuTiKw?pwd=6xus 提取码: 6xus
通过网盘分享的文件:9- 硬件I2C读取MPU6050
链接: https://pan.baidu.com/s/1IJ6ZaBX23o67exbPQYK_1A?pwd=3pdf 提取码: 3pdf
通过网盘分享的文件:MPU6050资料
链接: https://pan.baidu.com/s/1gNOobkqYC3SWDouhb68fkA?pwd=8w1v 提取码: 8w1v
相关文章:
一篇文章教会你I2C通信(软件I2C和硬件I2C)以读取MPU6050为例,附STM32代码示例
目录 一、I2C通信介绍: (1)基本概念: (2)特点: (3)工作原理: 二、I2C通信原理: (1)I2C 物理层: &…...
Python实现SPFA算法
目录 Python实现SPFA算法引言一、SPFA算法的理论基础1.1 最短路径问题1.2 SPFA算法的基本原理1.3 SPFA算法的复杂度 二、SPFA算法的Python实现2.1 基本实现2.2 案例一:使用SPFA算法进行城市交通最短路径计算2.2.1 实现代码 2.3 案例二:负权重边的处理2.3…...
MYSQL安装(ubuntu系统)
rpm -qa 查询安装软件包 ps axj 查询服务 卸载mysql(万不得已) ps axj | grep mysql 查看是否存在mysql服务 systemctl stop mysqld 关闭该服务 rpm -qa | grep mysql 查安装mysql安装包 rmp -qa | grep mysql | xargs (yum apt) -y remove进行批量…...
Cpp二叉搜索树的讲解与实现(21)
文章目录 前言一、二叉搜索树的概念定义特点 二、二叉树的实现基本框架查找插入删除当只有0 ~ 1个孩子的时候当有2个孩子的时候 三、二叉树的应用K模型KV模型 四、二叉树的性能分析总结 前言 这是全新的一个篇章呢,二叉搜索树是我们接下来学习set、map的前提 迈过它…...
微服务设计模式 — 补偿事务模式(Compensating Transaction Pattern)
微服务设计模式 — 补偿事务模式(Compensating Transaction Pattern) 定义 在云计算和分布式系统中,管理跨多个微服务或组件的事务一致性是一项极具挑战性的任务,补偿事务模式Compensating Transaction Pattern)是一种…...
20 实战:形状编码、运动补偿和纹理编码的实现(基于python)
在当今多媒体时代,视频处理与编码已经成为各个领域中不可或缺的一部分。无论是视频编辑、流媒体传输,还是计算机视觉应用,视频编码技术都扮演着关键角色。本文将详细解析一个基于Python的图形用户界面(GUI)视频编码器。通过对代码的逐行讲解、功能分析以及参数调节方法的探…...
区块链-C++挖矿软件XMRIG源码分析
C++挖矿软件源码分析 3rdpartybackendgrgon2Obfusheader.hmain 程序 xmrig.cppxmrig命名空间process类Entry::IdApp类CoreControllerbasetoolkernelinterfacesDonateStrategy.cppdonate.h/2/dmiCmake 跨平台的自动化构建系统CMakeLists.txt.cmake 13个引入算力哈希率 HashrateE…...
C语言指针的介绍
零.导言 在日常生活中,我们常常在外出时居住酒店,细心的你一定能发现酒店不同的房间上有着不同的门牌号,上面写着像308,512之类的数字。当你定了酒店之后,你就会拿到一个写有门牌号的钥匙,凭着钥匙就能进入…...
八大排序算法——堆排序
目录 前言 一、向上调整算法建堆 二、向下调整算法建堆 三、堆排序 前言 堆排序是基于堆结构的一种排序思想,因此要为一个乱序的数组进行排序的前提是数组必须要是一个堆,所以要先对数组进行建堆操作 一、向上调整算法建堆 时间复杂度:O…...
U盘文件不翼而飞?这些数据恢复工具帮你找回!
U盘因其便携性是我们日常工作和生活中不可或缺的工具。不过有时候它也会出点小状况。如果你U盘里的数据突然不见了,不要着急,可以先试试这几款数据恢复工具! 福昕数据恢复 直达链接:www.pdf365.cn/foxit-restore/ 操作教程&…...
在Java中 try catch 会影响性能吗?
1、在Java中,异常处理确实会对性能产生影响,但在正常执行的代码路径中,即没有发生异常的情况下,try-catch块的性能影响是微不足道的 2、但是,如果出现异常被抛出时,Java虚拟机需要执行一些额外的操作来处理…...
吞吐量最高飙升20倍!破解强化学习训练部署难题
**强化学习(RL)对大模型复杂推理能力提升有关键作用,然而,RL 复杂的计算流程以及现有系统局限性,也给训练和部署带来了挑战。近日,字节跳动豆包大模型团队与香港大学联合提出 HybridFlow(开源项…...
redis的数据过期策略
Redis对数据设置了数据的有效时间,数据过期之后,就需要将数据从内存中删除掉.可以按照不同的规则进行删除,这种删除规则就被称之为数据的删除策略(数据过期策略),而这种策略有两种:惰性删除和定期删除 惰性删除:设置key过期时间后,我们不去管它,当需要该key时,我们在检查其是否…...
三周精通FastAPI:27 使用使用SQLModel操作SQL (关系型) 数据库
官网文档:https://fastapi.tiangolo.com/zh/tutorial/sql-databases/ SQL (关系型) 数据库 FastAPI不需要你使用SQL(关系型)数据库。 但是您可以使用任何您想要的关系型数据库。 这里我们将看到一个使用SQLModel的示例。 SQLModel是在SQLAlchemy和Pydantic的基础…...
Kubernetes金丝雀发布
华子目录 Canary金丝雀发布什么是金丝雀发布Canary发布方式基于header(http包头)灰度发布基于权重的金丝雀发布 Canary金丝雀发布 什么是金丝雀发布 金丝雀发布也称为灰度发布,是一种软件发布策略主要目的是在将新版本的软件全面推广到生产环…...
树形DP讲解
文章目录 树形DP讲解一、引言二、树形DP基础1、树的定义2、树形DP的基本思想3、代码示例:子树大小 三、经典例题解析1、树的平衡点1.1、代码示例 2、没有上司的舞会(树的最大独立集)2.1、代码示例 四、总结 树形DP讲解 一、引言 树形动态规…...
容器:如何调试容器
调试容器,主要是指的调试Dockerfile,调试Dockerfile中的各个命令的执行,大小等 1、docker history查看构建过程和所有的中间层 2、docker run rm -it -u root XXX sh,通过临时容器的方式启动,可以调试中间层文件 3、do…...
用图说明 CPU、MCU、MPU、SoC 的区别
CPU CPU 负责执行构成计算机程序的指令,执行这些指令所指定的算术、逻辑、控制和输入/输出(I/O)操作。 MCU (microcontroller unit) 不同的 MCU 架构如下,注意这里的 MPU 表示 memory protection unit MPU (microprocessor un…...
牛客周赛 Round 65
文章目录 超市思路:Solved: 雨幕思路:Solved: 闺蜜思路:Solved: 医生思路:Solved: 降温(easy)思路:Solved: F-降温(hard&a…...
超级经典的79个软件测试面试题(内含答案)
1、软件的生命周期(prdctrm) 计划阶段(planning)-〉需求分析(requirement)-〉设计阶段(design)-〉编码(coding)->测试(testing)->运行与维护(running maintrnacne) 测试用例 用例编号 测试项目 测试标题 重要级别 预置条件 输入数据 执行步骤 预期结果 2、问…...
基于算法竞赛的c++编程(28)结构体的进阶应用
结构体的嵌套与复杂数据组织 在C中,结构体可以嵌套使用,形成更复杂的数据结构。例如,可以通过嵌套结构体描述多层级数据关系: struct Address {string city;string street;int zipCode; };struct Employee {string name;int id;…...
使用VSCode开发Django指南
使用VSCode开发Django指南 一、概述 Django 是一个高级 Python 框架,专为快速、安全和可扩展的 Web 开发而设计。Django 包含对 URL 路由、页面模板和数据处理的丰富支持。 本文将创建一个简单的 Django 应用,其中包含三个使用通用基本模板的页面。在此…...
TDengine 快速体验(Docker 镜像方式)
简介 TDengine 可以通过安装包、Docker 镜像 及云服务快速体验 TDengine 的功能,本节首先介绍如何通过 Docker 快速体验 TDengine,然后介绍如何在 Docker 环境下体验 TDengine 的写入和查询功能。如果你不熟悉 Docker,请使用 安装包的方式快…...
CVPR 2025 MIMO: 支持视觉指代和像素grounding 的医学视觉语言模型
CVPR 2025 | MIMO:支持视觉指代和像素对齐的医学视觉语言模型 论文信息 标题:MIMO: A medical vision language model with visual referring multimodal input and pixel grounding multimodal output作者:Yanyuan Chen, Dexuan Xu, Yu Hu…...
DeepSeek 赋能智慧能源:微电网优化调度的智能革新路径
目录 一、智慧能源微电网优化调度概述1.1 智慧能源微电网概念1.2 优化调度的重要性1.3 目前面临的挑战 二、DeepSeek 技术探秘2.1 DeepSeek 技术原理2.2 DeepSeek 独特优势2.3 DeepSeek 在 AI 领域地位 三、DeepSeek 在微电网优化调度中的应用剖析3.1 数据处理与分析3.2 预测与…...
Python:操作 Excel 折叠
💖亲爱的技术爱好者们,热烈欢迎来到 Kant2048 的博客!我是 Thomas Kant,很开心能在CSDN上与你们相遇~💖 本博客的精华专栏: 【自动化测试】 【测试经验】 【人工智能】 【Python】 Python 操作 Excel 系列 读取单元格数据按行写入设置行高和列宽自动调整行高和列宽水平…...
如何为服务器生成TLS证书
TLS(Transport Layer Security)证书是确保网络通信安全的重要手段,它通过加密技术保护传输的数据不被窃听和篡改。在服务器上配置TLS证书,可以使用户通过HTTPS协议安全地访问您的网站。本文将详细介绍如何在服务器上生成一个TLS证…...
Linux-07 ubuntu 的 chrome 启动不了
文章目录 问题原因解决步骤一、卸载旧版chrome二、重新安装chorme三、启动不了,报错如下四、启动不了,解决如下 总结 问题原因 在应用中可以看到chrome,但是打不开(说明:原来的ubuntu系统出问题了,这个是备用的硬盘&a…...
C# SqlSugar:依赖注入与仓储模式实践
C# SqlSugar:依赖注入与仓储模式实践 在 C# 的应用开发中,数据库操作是必不可少的环节。为了让数据访问层更加简洁、高效且易于维护,许多开发者会选择成熟的 ORM(对象关系映射)框架,SqlSugar 就是其中备受…...
Android Bitmap治理全解析:从加载优化到泄漏防控的全生命周期管理
引言 Bitmap(位图)是Android应用内存占用的“头号杀手”。一张1080P(1920x1080)的图片以ARGB_8888格式加载时,内存占用高达8MB(192010804字节)。据统计,超过60%的应用OOM崩溃与Bitm…...
