名词解释:
I2C:Inter-Integrated Circuit protocol
MSB:Most Significant Bit
LSB:Least Significant Bit
SDA:Serial data line
SCL:Serial clock line
OAR:Own address register
DMP:Digital Motion Processor
本期开始统一文章结构(数字后加*):
第一部分: 阐述外设工作原理;第二部分:芯片参考手册对应外设的学习;第三部分:使用STM32CubeMX进行外设初始化;第四部分:添加应用代码;第五部分:附上本篇文章的工程代码的下载地址。
本文将介绍I2C的概念、相关函数以及STM32CubeMX生成I2C的配置函数。最后针对于I2C实践:模拟(软件)/硬件I2C控制0.96寸4脚oled显示中文、图片;六轴传感器mpu6050与VOFA+显示 - STM32CubeMX
一、什么是I2C?(串行同步半双工,MSB->LSB)
1.1 I2C简介
1.1.1 I2C介绍
- I2C(集成电路间协议)广泛用于短距离通信,尤其是在芯片间或模块间的低速数据传输。
- IIC是一种两线式串行总线。用于连接微控制器及其外围设备,多用于主控制器(Master)和从器件(Slave)间的主从通信,在小数据量场合使用,传输距离短,任意时刻只能有一个主机等特性。
- 协议要求每次放到SDA上的字节包长度必须为8位,并且每个字节包后须跟一个ACK位。
1.1.2 I2C总线拓扑图
I2C核心机制 - IO口为开漏输出(即I2C 总线上的所有设备(主/从)的 SDA 和 SCL 引脚均为开漏输出。当设备不主动驱动总线时,引脚处于高阻态,总线通过外接上拉电阻(Rp)被拉至高电平(Vdd);当任一设备需要发送低电平时,只需主动拉低总线。)
1.2 I2C物理层
1.2.1 I2C总线接口(SDA/SCL)
I2C设备分为主设备(Master)和从设备(Slave),谁控制时钟线(即控制SCL的电平高低变换)谁就是主设备。
-
IIC一共有只有两个总线:
一条是双向的串行数据线 SDA
一条是串行时钟线 SCL -
SDA(Serial data line) - 数据线:主、从设备之间数据传输。
-
SCL(Serial clock line) - 时钟线:主、从设备之间的同步时钟控制(由主设备控制)。
1.2.2 I2C物理层相关知识点
-
I2C 允许多个主机、多个从机挂载在总线上(因为每个从机设备的地址是独特的)。但是,在任何时间点上只能有一个主控或一个从设备发送数据。
-
支持不同速率的通讯速度
-
SCL和SDA都需要接上拉电阻 (大小由速度和容性负载决定一般在3.3K-10K之间) 保证数据的稳定性,减少干扰。
-
SDA和SCL线路上都使用开漏连接,并且连接到NMOS晶体管。
-
开漏输出不会产生总线竞争,不会使信号处于不确定性。
1.3 I2C协议层
1.3.1 I2C 三种信号以及数据帧
I2C 总线在传送数据过程中共有三种类型信号,它们分别是:起始信号、结束信号、应答信号。I2C数据帧(寻址或者数据)。
起始信号和停止信号都是由主机发出,起始信号产生后总线处于占用状态,停止信号产生后总线处于空闲状态。
-
起始信号:
SCL 为高电平时,SDA 由高电平向低电平跳变,开始传送数据。
-
结束信号:
SCL 为高电平时,SDA 由低电平向高电平跳变,结束传送数据。
-
逻辑1和0:
SCL高电平时,检测SDA的电平。
-
应答信号(0是应答,1是非应答):
接收数据的 IC 在接收到 8bit 数据后,向发送数据的 IC 发出特定的低电平脉冲,表示已收到数据。(即:上拉电阻影响下SDA默认为高,而从机拉低SDA就是确认收到数据即ACK,否则NACK。)
-
I2C数据帧(寻址或者数据):
IIC总线在进行数据传送时,时钟线SCL为低电平期间发送器向数据线上发一位数据,在此期间数据线上的信号允许发生变化,时钟线SCL为高电平期间接收器从数据线上读取一位数据,在此期间数据线上的信号不允许发生变化,必须保持稳定。(数据在SCL处于低电平时放到SDA上,并在SCL变为高电平后进行采样。读写数据和SCL上升沿之间的时间间隔是由总线上的设备自己定义的,不同芯片可能有差异。)
1.3.2 I2C 读写时序
下图来源于正点原子的教程中。(AT24C02 是一个7位地址的EEPROM芯片)
1.3.3 硬件I2C 与 软件I2C
1. 硬件I2C
硬件I2C是通过专门的硬件电路实现的,通常由微控制器或其他IC上的硬件模块提供支持。
2. 软件I2C(模拟I2C)
软件I2C是通过软件控制GPIO管脚来模拟I2C协议的时序。
二、I2C(STM32F4xx-硬件I2C)
2.1 硬件I2C简介
2.1.1 硬件I2C特性
● 多主模式功能:同一接口既可用作主模式也可用作从模式
● I2C 主模式特性: 时钟生成 ;起始位和停止位生成
● I2C 从模式特性:
— 可编程 I2C 地址检测
— 双寻址模式,可对 2 个从地址应答
— 停止位检测
● 7 位/10 位寻址以及广播呼叫的生成和检测
10 位寻址模式下,比较对象还包括头序列 (11110xx0),其中,xx 表示该地址的两个最高 有效位。
● 状态标志:
— 发送/接收模式标志
— 字节传输结束标志
— I2C 忙碌标志
● 错误标志:
— 主模式下的仲裁丢失情况
— 地址/数据传输完成后的应答失败
— 检测误放的起始位和停止位
— 禁止时钟延长后出现的上溢/下溢
● 2 个中断向量:
● —个中断由成功的地址/数据字节传输事件触发
● 一个中断由错误状态触发
● 带 DMA 功能的 1 字节缓冲
2.1.2 硬件I2C默认模式
默认情况下,它以从模式工作。接口在生成起始位后会自动由从模式切换为主模式,并在出 现仲裁丢失或生成停止位时从主模式切换为从模式,从而实现多主模式功能。
2.1.3 硬件I2C框图
2.2 I2C从模式
2.2.1 从模式匹配
1. 寻找地址
检测到起始位后,便会立即接收到来自 SDA 线的地址并将其送到移位寄存器。
● 会将其与接口地址 I2C.OAR1(自有地址寄存器)进行比较。
I2C.OAR1.ADDMODE (Addressing mode)从模式:0为7为从地址、1为10位从地址
I2C.OAR1.ADD[7:1]:接口地址:地址的第 7:1 位
● 使能双寻址:I2C.OAR2.ENDUAL=1,I2C_OAR2.ADD2[7:1] 进行比较。
● 广播呼叫地址(如果 I2C_CR1.ENGC = 1)进行比较。
2. 地址匹配
● 头或地址不匹配:接口会忽略它并等待下一个起始位。
● 头匹配(仅针对 10 位模式):如果 ACK 位置 1,则接口会生成一个应答脉冲并等待 8 位从地址。
● 地址匹配:接口会依次:
— 发出应答脉冲(如果 ACK 位置 1)
— ADDR 位会由硬件置 1 并在 ITEVFEN 位置 1 时生成一个中断。
— 如果 ENDUAL=1,则软件必须读取 DUALF 位状态来核对哪些从地址进行了应答。
在 10 位模式下,完成地址序列接收后,从模式始终处于接收模式。在接收到重复起始位以及一个匹配地址位和最低有效位均置1的头序列 (11110xx1) 后,它会进入发送模式。
3. 从设备的发送/接收器
I2C_SR2.TRA位:0为接收器、1为发送
2.2.2 从发射器
1. I2C_SR1.ADDR 地址已发送(主模式)/地址匹配(从模式)
接收地址并将ADDR清零,从设备通过内部移位寄存器将I2C.DR中的字节发送到SDA线。
2. 接收到ACK后
TxE 位会由硬件置 1 并在 I2C_CR2.ITEVFEN 和 I2C_CR2.ITBUFEN 位均置 1 时生成一个中断。
● I2C_SR1.TxE;0-DR非空、1-DR为空
● I2C_CR2.ITEVTEN
● I2C_CR2.ITBUFEN
3. I2C_SR1.BTF
如果在下一次数据传输结束之前TxE位已置1,但某些数据尚未写入I2C_DR寄存器,则BTF位会置1,而接口会一直延长SCL低电平,直到通过软件对I2C_SR1读操作,以及对I2C_DR写操作后,把BTF清零为止。
4. 从发送器的传输序列图
2.2.3 从接收器
● 发出应答脉冲(如果 ACK 位置 1)
● RxNE 位会由硬件置 1 并在 I2C_CR2.ITEVFEN 和I2C_CR2.ITBUFEN 位均置 1 时生成一个中断。
如果在下一次数据接收结束之前 RxNE 位已置 1 但 DR 寄存器中的数据尚未读取,则 BTF 位会置 1,而接口会一直延长 SCL 低电平,直到软件通过读取 I2C_DR 寄存器来把 BTF 清零。
2.2.4 关闭从设备通信
● 传输完最后一个数据字节之后,主设备会生成一个停止位。
● 将 I2C_SR1.STOPF 位置 1 并在I2C_CR2.ITEVFEN 位置 1 时生成一个中断。
● 通过先读取 SR1 寄存器然后写入 CR1 寄存器的方式将 STOPF 位清零
2.3 I2C主模式
2.3.1 主模式配置
只要通过I2C_CR1.START 位在总线上生成了起始位,即会选中主模式。
1. 初始化时序配置:设置外设输入时钟(I2C_CR2寄存器)
I2C_CR2.FREQ[5:0]:外设时钟频率 (Peripheral clock frequency)
● 标准模式(100kHz)要求输入时钟 ≥ 2MHz
● 快速模式(400kHz)要求输入时钟 ≥ 4MHz
2. 配置时钟控制寄存器(I2C_CCR)
计算分频值以生成目标SCL频率
3. 设置上升时间寄存器(I2C_TRISE)
根据SCL频率调整信号上升沿时间(满足I²C时序规范)
4. 使能外设(I2C_CR1.PE)
激活I2C硬件模块,开始监控总线状态
5. 发送起始信号及之后(I2C_CR1.START)
1. 向I2C_CR1寄存器的START位写1
2. I2C_SR2.BUSY 清零之后生成一个起始位,切换主模式(I2C_SR2.MSL位置1)
3. 起始位发出,I2C_SR1.SB位(起始位 (Start bit)(主模式))置1
4. SB 位会由硬件置 1 ,如果 ITEVFEN 位置 1 时生成一个中断
2.3.2 向总线发送从设备地址
1. 来从地址会通过内部移位寄存器发送到 SDA 线。
● 1. 在 10 位寻址模式中,发送头序列会产生以下事件:
— I2C_SR1.ADD10位会由硬件置 1 并在 ITEVFEN 位置 1 时生成一个中断。 接下来主设备会等待软件读取 SR1 ,然后把第二个地址字节写入 DR 寄存器。
2. 接下来主设备会等待软件读取 SR1 ,然后把第二个地址字节写入 DR 寄存器
— ADDR 位会由硬件置 1 并在 ITEVFEN 位置 1 时生成一个中断。 接下来主设备会等待对 SR1 寄存器执行读操作,然后对 SR2 寄存器执行读操作。
● 在 7 位寻址模式下,会发送一个地址字节。(地址字节被发出后)
— ADDR 位会由硬件置 1 并在 ITEVFEN 位置 1 时生成一个中断。 接下来主设备会等待对 SR1 寄存器执行读操作,然后对 SR2 寄存器执行读操作。
2. 从地址字节 LSB 来决定是进入发送模式还是接收模式。
● 在 7 位寻址模式下
— 要进入发送模式,主设备会发送从地址并将 LSB 置 0。
— 要进入接收模式,主设备会发送从地址并将 LSB 置 1。
● 在 10 位寻址模式下
— 要进入发送模式,主设备会先发送头序列 (11110xx0),然后发送从地址(其中 xx 表示该地址的两个最高有效位)。
— 要进入接收模式,主设备会先发送头序列 (11110xx0),然后发送从地址。接下来会发送一个重复起始位,然后再发送头序列 (11110xx1)(其中 xx 表示地址的两个最高有效位)。
3. I2C_SR2.TRA位:0为接收器、1为发送
2.3.3 主发送器
● 在发送出地址并将 I2C_SR1.ADDR 清零后,主设备会通过内部移位寄存器将 I2C_DR寄存器中的字节发送到 SDA 线。
● 主设备会一直等待,直到首个数据字节被写入 I2C_DR 为止(参考传输序列 EV8_1)。
● 接收到应答脉冲后,I2C_SR1.TxE 位会由硬件置 1 并在I2C_CR2.ITEVFEN 和I2C_CR2.ITBUFEN 位均置 1 时生成一个 中断。
● 如果在上一次数据传输结束之前 TxE 位已置 1 但数据字节尚未写入I2C_ DR 寄存器,则I2C_SR1.BTF 位 会置 1,而接口会一直延长 SCL 低电平,等待I2C_DR 寄存器被写入,以将I2C_SR1.BTF清零。
结束通信
● 当最后一个字节写入 DR 寄存器后,软件会将I2C_CR1.STOP 位置 1 以生成一个停止位(参考传输序列 EV8_2)。接口会自动返回从模式(I2C_SR2.MSL 位清零)。
2.3.4 主接收器
完成地址传输并将I2C_SR1.ADDR位清零后,I2C 接口会进入主接收模式。在此模式下,接口会通过内部移位寄存器接收 SDA 线中的字节并将其保存到 DR 寄存器。在每个字节传输结束后, 接口都会依次:
1. 发出应答脉冲(如果 I2C_CR1.ACK 位置 1)
2. I2C_SR1.RxNE 位置 1 并在 I2C_CR2.ITEVFEN 和I2C_CR2.ITBUFEN 位均置 1 时生成一个中断。
如果在上一次数据接收结束之前 RxNE 位已置 1 但 DR 寄存器中的数据尚未读取,则I2C_SR1.BTF 位会由硬件置 1,而接口会一直延长 SCL 低电平,等待 I2C_DR 寄存器被写入,以将I2C_SR1.BTF 清零。
结束通信
主设备会针对自从设备接收的最后一个字节发送 NACK。在接收到此 NACK 之后,从设备会 释放对 SCL 和 SDA 线的控制。随后,主设备可发送一个停止位/重复起始位。
1. 为了在最后一个接收数据字节后生成非应答脉冲,必须在读取倒数第二个数据字节后 (倒数第二个 RxNE 事件之后)立即将 ACK 位清零。
2. 要生成停止位/重复起始位,软件必须在读取倒数第二个数据字节后(倒数第二个 RxNE 事件之后)将 STOP/START 位置 1。
3. 在只接收单个字节的情况下,会在 EV6 期间(在 ADDR 标志清零之前)禁止应答并在 EV6 之后生成停止位。 生成停止位后,接口会自动返回从模式(I2C_SR2.MSL 位清零)。
2.4 DMA请求
注意:DMA中断使能 不能和 I2C的发送、接收中断同时使能。
即 ITBUFFEN(缓冲中断使能(TXE、RXNE))与DMA.SxCR.TCIE(DMA传输完成使能)
2.4.1 DMA触发条件
● 发送:当发送数据寄存器变空(TXE)时触发DMA请求,自动填充数据。
● 接收:当接收数据寄存器变满(RXNE)时触发DMA请求,自动读取数据。
2.4.2 DMA触发使能
● 传输前必须初始化并启用DMA控制器。
● I2C_CR2寄存器中的DMAEN位必须在ADDR事件(地址匹配完成)前置1。
● 若启用时钟延长(Clock Stretching),DMAEN位可在ADDR事件期间置1。
2.4.3 DMA请求时机
结束当前字节传输之前,必须发出 DMA 请求。当传输的数据量达到相应 DMA 通道编程设定的值时,DMA 控制器会发送一个结束传输 EOT 信号给 I2C 接口,并生成一个传输完成中断(如果已使能 DMA_CCRx.TCIE = 1)
● 主发送器:
在 EOT 中断后的中断程序中,禁止 DMA 请求,然后在等到 BTF 事件后设置停止位。
● 主接收器:
— 当要接收的字节数等于或大于二时,DMA 控制器会在收到倒数第二个数据字节(第 N - 1 个数据时)发送一个硬件信号 EOT_1。如果 I2C_CR2 寄存器中的 LAST 位 置 1,I2C 会在 EOT_1 后的下一个字节之后自动发送一个 NACK。用户可在 DMA 传输完成中断(如果已使能)程序中生成停止位。
— 当必须接收单个字节时:必须在 EV6 事件期间于 ADDR 标志清零之前对 NACK 进行编程,即当 ADDR=1 时编程设定 ACK=0。接下来,用户可在 ADDR 标志清零之后或者在执行DMA 传输完成中断程序时编程设定停止位。
2.4.4 DMA发送配置步骤
将 I2C_CR2 寄存器中的 DMAEN 位置 1 可以使能 DMA 模式进行发送。当 TXE 位置 1 时, 数据将由 DMA 从预置的存储区装载进 I2C_DR 寄存器。要映射一个 DMA 通道以便进行 I2C 发送,请按以下步骤操作:
1. 设置DMA目标地址(外设端)
DMA_CPARx(通道x外设地址寄存器)写入I2C数据寄存器(I2C_DR)的地址。
2. 设置DMA源地址(内存端)
DMA_CMARx(通道x内存地址寄存器)写入存放待发送数据的内存缓冲区地址。。
3. 配置传输总字节数
DMA_CNDTRx(通道x数据数量寄存器)写入需要传输的总字节数。
4. 配置DMA通道优先级
DMA_CCRx(通道x配置寄存器)中的 PL[0:1],设置优先级等级(如高、中、低)。
5. 配置传输完成中断
DMA_CCRx中的TCIE(传输完成中断使能位)和HTIE(半传输中断使能位)
DMA_CCRx中的 DIR 位(传输方向)
6. 激活DMA通道
DMA_CCRx中的 EN 位置1,启动DMA通道。
7. DMA传输完成后的处理
EOT信号:当DMA传输完所有字节后,发送EOT信号给I2C接口
中断处理:
若已使能TCIE
位,DMA会触发传输完成中断。
在中断服务程序中需执行:
关闭DMA通道:清除DMA_CCRx.EN位。
等待BTF事件:确保最后一个字节已通过I2C发送。
生成停止位:设置I2C_CR1.STOP=1,结束传输。
2.4.5 DMA接收配置步骤
将 I2C_CR2 寄存器中的 DMAEN 位置 1 可以使能 DMA 模式进行发送。当 TXE 位置 1 时, 数据将由 DMA 从预置的存储区装载进 I2C_DR 寄存器。要映射一个 DMA 通道以便进行 I2C 发送,请按以下步骤操作:
1. 设置DMA目标地址(外设端)
DMA_CPARx(通道x外设地址寄存器)写入I2C数据寄存器(I2C_DR)的地址。
2. 设置DMA源地址(内存端)
DMA_CMARx(通道x内存地址寄存器)写入内存缓冲区的起始地址(存放接收数据的位置)。
3. 配置传输总字节数
DMA_CNDTRx(通道x数据数量寄存器)写入需要接收的总字节数。
4. 配置DMA通道优先级
DMA_CCRx(通道x配置寄存器)中的 PL[0:1],设置优先级等级(如高、中、低)。
5. 配置传输完成中断
DMA_CCRx中的TCIE(传输完成中断使能位)和HTIE(半传输中断使能位)
DMA_CCRx中的 DIR 位(传输方向)
6. 激活DMA通道
DMA_CCRx中的 EN 位置1,启动DMA通道。
7. DMA传输完成后的处理
EOT信号:当DMA传输完所有字节后,发送EOT信号给I2C接口
中断处理:
若已使能TCIE
位,DMA会触发传输完成中断。
在中断服务程序中需执行:
关闭DMA通道:清除DMA_CCRx.EN位。
生成停止位:设置I2C_CR1.STOP=1,结束传输。
处理接收数据:从内存缓冲区读取数据。
2.5 I2C中断
三、基于HAL库配置I2C外设
3.1 CubeMX配置I2C外设(软件I2C)
3.2 CubeMX配置I2C外设(硬件I2C)
四、I2C外设实践-模块介绍
4.1 4脚0.96寸oled
4.1.1 oled介绍
高分辨率:128*64
OLED的内部芯片:SSD1306
IIC从设备地址:(SA0为0(和电阻焊接有关),一般买到的是 0b001110 -> 0x3C)
IIC写从设备地址:0x78
IIC读从设备地址:0x79
b7 b6 b5 b4 b3 b2 b1 b0
0 1 1 1 1 0 SA0 R/W#
4.1.2 oled写数据格式
数据位在每个SCL脉冲期间传输,必须在时钟脉冲的“高”电平期间保持稳定状态。只有当SCL为低电平时,SDA数据线才能被切换。
1. 写字节 - 从机设备地址(Slave Address)
b7 b6 b5 b4 b3 b2 b1 b0
0 1 1 1 1 0 SA0 R/W#
2. 写字节 - 控制字节(Control byte)
Co D/C# b5 - b0
0 1 0(必须全是0,6位)
Co:
Co=0,当前控制字节后仅跟随数据字节(无后续控制字节)
Co=1,允许后续发送新的控制字节(用于混合传输命令和数据)
D/C#:
D/C#=0,后续数据为命令(写入命令寄存器)
D/C#=1,后续数据为显存数据(写入GDDRAM)
3. 写字节 - 命令/数据(Data byte)格式
发送三个字节:从机地址+写命令/数据(Co/D/C#控制)+具体命令/数据
4.1.3 Graphic Display Data RAM
RAM分成8个页(Page),每页有8行row,128列column。
每次写数据,8位MSB->LSB,按下述方法写入。
4.1.4 定义起始光标(即起始RAM访问指针位置)
1. 设置页起始地址
写命令(B0h -> B7h,定义第一页到第八页)
2. 设置 低4位 起始列位置
写命令(00h -> 0Fh,0x00 | (Column_Low & 0x0F))
3. 设置 高4位 起始列位置
写命令(10h -> 1Fh,0x10 | (Column_High & 0x0F))
4. 列地址:高4位值 * 16 + 低4位值。
4.1.5 设置内存的寻址模式
1. Page addressing mode (页寻址模式,A[1:0]=10xb)
2. Horizontal addressing mode (水平寻址模式,A[1:0]=00b)
3. Vertical addressing mode: (垂直寻址模式,A[1:0]=01b)
4.1.5 可以设置指定的RAM刷新范围
(给水平寻址/垂直寻址模式,设置起始/结束的行列)
1. Set Column Address (0x21H):
- This is a triple byte command. First byte specifies the command for setting column address (0x21 H).
- Second byte specifies the column start address and third byte specifies column end address.
- This command also sets the column address pointer to column start address.
2. Set Page Address (0x22H):
- This is a triple byte command. First byte specifies the command for setting page address (0x22 H).
- Second byte specifies page start address and third byte is page end address.
- This command also sets the page address pointer to page start address.
3. Example of column and row address pointer movement:
In the following example, Horizontal addressing mode is used. Column start address is set to 2 and column end address is set to 125.
- Page start address is set to 1 and end address is set to 6. In this case, the graphic display data RAM column accessible range is from column 2 to column 125 and from page 1 to page 6 only.
- In addition, the column address pointer is set to 2 and page address pointer is set to 1. After finishing read/write one pixel of data, the column address is increased automatically by 1 to access the next RAM location for next read/write operation.
- Whenever the column address pointer finishes accessing the end column 125, it gets reset back to the column 2 and page address is automatically increased by 1.
- While the end page 6 and end column 125 RAM location is accessed, the page address gets reset back to 1 and the column address is reset back to 2.3.
4.2 六轴传感器mpu6050
4.2.1 mpu6050介绍
MPU6050是一个六轴运动跟踪器,内部集成3轴陀螺仪、3轴加速度计和数字动作处理器(DMP),同时片内内置了一个温度传感器。芯片通过I2C协议与控制器通信。
MPU6050还有一个Auxiliary I2C bus(附属I2C),可以连接3轴磁力计、压力传感器等。
当连接3轴磁力计时,mpu6050就可以提供9轴运动融合输出。
通过姿态计算可以得到横滚roll,俯仰pitch,偏航yaw。
4.2.2 mpu6050 - 3轴陀螺仪
3轴陀螺仪用于检测沿x,y,z轴的旋转速度(角速度)。
4.2.3 mpu6050 - 3轴加速度计
3轴加速度计用于检测设备沿X、Y和Z轴的倾斜角度或倾角。
4.3 MPU6050 时序与重要寄存器
4.3.1 MPU6050写时序
4.3.2 MPU6050读时序
4.4 MPU6050重要寄存器
4.4.1 PWR_MGMT_1(Power Management 1-电源管理1)0x6B
-
DEVICE_RESET(设备复位)
当设置为1时,此位将所有内部寄存器重置为其默认值。参数:重置完成后,此位自动清零。 -
SLEEP(睡眠模式)
当此位设置为1
时,MPU-60X0 进入低功耗睡眠模式,暂停所有传感器数据采集。
1. 睡眠模式下,功耗显著降低,但 无法获取数据。
2. 退出睡眠模式需将此位设为0
。 -
CYCLE(循环采样模式)
当SLEEP
位为0
(非睡眠模式)且CYCLE
设为1
时,传感器会在 睡眠模式 和 唤醒单次采样 之间循环切换。
· 采样速率控制:循环间隔由寄存器LP_WAKE_CTRL
(地址108
)的值决定。 -
TEMP_DIS(温度传感器禁用)
将此位设为1
时,禁用 MPU-60X0 内部的温度传感器。禁用后,温度传感器数据寄存器(如OUT_TEMP
)将不再更新。若不需要温度数据,禁用可略微降低功耗。 -
CLKSEL(时钟源选择)
通过 3 位无符号值 选择 MPU-60X0 的时钟源。
1. 默认值0
适合大多数场景。
2. 需高精度时选择外部时钟源(如导航系统)。
4.4.2 PWR_MGMT_2(Power Management 2-电源管理2)0x6C
-
LP_WAKE_CTRL(低功耗唤醒频率控制)
通过 2位无符号值 设置加速度计在 仅加速度计低功耗模式(Accelerometer Only Low Power Mode) 下的唤醒频率。
唤醒频率决定传感器从休眠到采样的间隔时间,直接影响功耗和数据更新速率。 -
-
STBY_xA / STBY_YA / STBY_ZA(加速度计待机模式控制)
STBY_xA:设为1
时,X轴加速度计 进入待机模式(停止工作)。
STBY_YA:设为1
时,Y轴加速度计 进入待机模式。
STBY_ZA:设为1
时,Z轴加速度计 进入待机模式。
STBY_xG:设为1
时,X轴陀螺仪 进入待机模式(停止工作)。
STBY_YG:设为1
时,Y轴陀螺仪 进入待机模式。
STBY_ZG:设为1
时,Z轴陀螺仪 进入待机模式。
4.4.3 GYRO_CONFIG(Gyroscope Configuration-陀螺仪配置寄存器)0x1B
-
XG_ST / YG_ST / ZG_ST(陀螺仪自检触发位)
XG_ST:设置此位为1
时,触发 X轴陀螺仪 自检(Self-Test)。
YG_ST:设置此位为1
时,触发 Y轴陀螺仪 自检。
ZG_ST:设置此位为1
时,触发 Z轴陀螺仪 自检。
等待自检完成(通常数毫秒)。自检结果通过特定寄存器(如SELF_TEST_X
/Y
/Z
)的值反馈。 -
FS_SEL(陀螺仪满量程范围选择)
通过 2位无符号值 设置陀螺仪的测量范围(满量程)。
4.4.4 ACCEL_CONFIG(Accelerometer Configuration -加速度传感器配置寄存器)0x1C
4.4.5 TEMP_OUT_H/L(Temperature Measurement-温度传感器数据输出寄存器)0x41、0x42
寄存器地址为 0x41 的寄存器(高 8 位)和寄存器地址位 0x42 的寄存器(低 8 位)组成的 16 位数,计算公式:
Temperature in degrees C = (TEMP_OUT Register Value as a signed quantity)/340 + 36.53
4.4.6 GYRO_XOUT_H, GYRO_XOUT_L, GYRO_YOUT_H, GYRO_YOUT_L, GYRO_ZOUT_H, and GYRO_ZOUT_L(Gyroscope Measurements-陀螺仪数据输出寄存器)0x43~0x48
寄存器地址为 0x43 的寄存器(高 8 位)和寄存器地址位 0x44 的寄存器(低 8 位)组成的 16 位数,即为陀螺仪 X 轴的数据, Y、Z 轴的数据,以此类推。
int16_t g_x = (GYRO_XOUT_H << 8) | GYRO_XOUT_L; // 合并高8位和低8位
int16_t g_y = (GYRO_YOUT_H << 8) | GYRO_YOUT_L;
int16_t g_z = (GYRO_ZOUT_H << 8) | GYRO_ZOUT_L;
4.4.7 ACCEL_XOUT_H, ACCEL_XOUT_L, ACCEL_YOUT_H, ACCEL_YOUT_L, ACCEL_ZOUT_H, and ACCEL_ZOUT_L(Accelerometer Measurements-加速度传感器数据输出寄存器)0x3B~0x40
int16_t a_x = (ACCEL_XOUT_H << 8) | ACCEL_XOUT_L; // 合并高8位和低8位
int16_t a_y = (ACCEL_YOUT_H << 8) | ACCEL_YOUT_L;
int16_t a_z = (ACCEL_ZOUT_H << 8) | ACCEL_ZOUT_L;
4.4.8 FIFO_EN(FIFO Enable-使能寄存器 )0x23
4.4.9 SMPRT_DIV(Sample Rate Divider-陀螺仪采样率分频寄存器 )0x19
计算公式:Sample Rate = Gyroscope Output Rate / (1 + SMPLRT_DIV)
4.4.10 CONFIG(Configuration-使能寄存器 )0x1A
1. EXT_SYNC_SET(外部同步设置)
用于 控制外部同步信号(FSYNC引脚)与传感器数据采样的交互方式。
(没研究、不知道怎么用)
2. DLPF_CFG(数字低通滤波器配置)
根据 DLPF_CFG[2:0]这三个比特位的配置进行滤波的,DLPF_CFG 的配置描述,如下表所示:
4.5 姿态测量基础
本节具体推导参考下列文章
Extrinsic & intrinsic rotation: Do I multiply from right or left? | by Dominic Plein | Medium
Rotation matrix - Wikipedia
三维旋转 【四 】-内外旋顺序经验分享_哔哩哔哩_bilibili
[铁头山羊stm32入门教程] 6.4. MPU6050(上)_哔哩哔哩_bilibili
4.5.1 旋转矩阵计算(其推导出的旋转矩阵基于标准基求出)
标准基:e1=[1,0,0]; e2=[0,1,0] e3=[0,0,1];
明确,使用内旋时,标准基也随着变化
4.5.2 旋转顺序:(具体了解请看第三个参考网址)
外旋(Extrinsic)与内旋(Intrinsic)旋转旋转矩阵的左乘与右乘问题。
对一个物体的操作旋转顺序:Z-Y-X
· 外旋(绕固定坐标轴旋转)
· 内旋(绕自身动态坐标轴旋转)
4.5.3 姿态测量 - 加速度计
1. 3轴加速度计测量值:
2. 3轴加速度计姿态测量:
· 当物体静止或匀速运动时,加速度计测得的总加速度由 重力加速度(1g) 主导。
· 通过重力在三个轴上的投影(如 ax,ay,aza_x, a_y, a_zax,ay,az),可计算俯仰角(Pitch)和横滚角(Roll):
3. 3轴加速度计姿态测量缺点:
· 无法直接测量航向角(Yaw):
重力在水平面(X-Y)的投影与航向角无关,需依赖磁力计或 GPS。
· 动态误差大:
物体垂直加速运动时,加速度计测量值包含 运动加速度 + 重力加速度,导致姿态解算失真。
· 振动敏感:
机械振动会引入高频噪声,需通过低通滤波处理。
4.5.4 姿态测量 - 陀螺仪(角速度)
1. 3轴陀螺仪姿态测量:对角速度积分得到姿态角变化。
2. 3轴陀螺仪姿态测量问题:(零漂)
陀螺仪输出的角速度包含 零偏误差(非零静止输出)和 白噪声,积分后误差随时间累积,导致姿态角漂移。
4.5.5 姿态测量 - 传感器融合
1. 传感器融合姿态测量:
2. 传感器融合姿态测量问题:
yaw偏航角存在零漂:
通过历史数据拟合直线方程 ,估计斜率 k(漂移率)和截距 c,用于实时补偿。
4.6 上位机VOFA+实现实时查看
4.6.1 利用外部中断服务函数采集数据并上传给上位机
正常应该使用串口DMA来发送数据。(printf十分的费时间,我这里偷懒了)
4.6.2 上位机VOFA+显示
五、I2C外设实践-模拟/硬件IIC基础函数
5.1 模拟IIC函数
5.1.1 bsp_i2c_soft.c
/*
* bsp_i2c_soft.c
*
* Created: 2025-04-29 15:02:23
* Author: user
* function: 软件I2C
*
*/
#include "bsp_i2c_soft.h"
#include "delay.h"
/* 软件I2C外设初始化
* 1. 初始化对应GPIO时钟
* 2. 配置GPIO为开漏输出、上拉电阻
* Configure GPIO pins : PBPin PBPin
* GPIO_InitStruct.Pin = I2C_SCL_Pin|I2C_SDA_Pin;
* GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
* GPIO_InitStruct.Pull = GPIO_PULLUP;
* GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
* HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
*/
static void i2c_delay(void)
{
delay_us(3);
}
/* 产生I2C起始信号
* 在SCL高电平期间,SDA出现下降沿
*/
void i2c_start(void)
{
i2c_sda(1);
i2c_scl(1);
i2c_delay();
i2c_sda(0);
i2c_delay();
i2c_scl(0);
i2c_delay();
}
/* 产生I2C停止信号
* 在SCL高电平期间,SDA出现上升沿
*/
void i2c_stop(void)
{
i2c_sda(0);
i2c_scl(0);
i2c_delay();
i2c_scl(1);
i2c_delay();
i2c_sda(1);
i2c_delay();
}
/*产生ack信号
* SCL高电平期间,SDA低电平
*/
void i2c_ack(void)
{
i2c_sda(0);
i2c_delay();
i2c_scl(1);
i2c_delay();
i2c_scl(0);
i2c_delay();
i2c_sda(1);
i2c_delay();
}
/* 产生nack信号
* SCL高电平期间,SDA高电平
*/
void i2c_nack(void)
{
i2c_sda(1);
i2c_delay();
i2c_scl(1);
i2c_delay();
i2c_scl(0);
i2c_delay();
}
/* 等待应答信号(ACK)
* 0 接收到应答
* 1 未接收到应答
*/
uint8_t i2c_wait_ack(void)
{
uint8_t ack = 0;
uint16_t wait_time = 0;
i2c_sda(1); // 释放SDA
i2c_delay();
i2c_scl(1); // 拉高SCL
i2c_delay();
while (i2c_read_sda())
{
wait_time++;
if (wait_time > 50) // 超时
{
i2c_stop(); // 产生停止信号
ack = 1; // 未接收到应答
break;
}
}
i2c_delay();
i2c_scl(0); // 拉低SCL
return ack;
}
/* 发送一个字节数据
* @input: data 要发送的数据(uint8_t)
* MSB 先发送,LSB 后发送
*/
void i2c_send_byte(uint8_t txd)
{
uint8_t i = 0;
for (i = 0; i < 8; i++)
{
i2c_sda((txd & 0x80) >> 7); // 发送最高位
i2c_delay();
i2c_scl(1); // 拉高SCL
i2c_delay();
i2c_scl(0); // 拉低SCL
txd <<= 1; // 左移一位
}
i2c_sda(1); // 释放SDA
}
/* 接收一个字节数据
* @input: ack 应答信号(0: 应答,1: 非应答)
* @return: 接收到的数据(uint8_t)
* MSB 先接收,LSB 后接收
*/
uint8_t i2c_read_byte(uint8_t ack)
{
uint8_t i = 0;
uint8_t rxd = 0;
for (i = 0; i < 8; i++)
{
rxd <<= 1; // 左移一位
i2c_scl(1); // 拉高SCL
i2c_delay();
if (i2c_read_sda()) // 读取SDA
{
rxd |= 0x01; // 接收到1
}
i2c_scl(0); // 拉低SCL
i2c_delay();
}
if (!ack) // 非应答
{
i2c_nack(); // 发送应答信号
}
else // 应答
{
i2c_ack(); // 发送非应答信号
}
return rxd;
}
5.1.2 bsp_i2c_soft.h
#ifndef __BSP_I2C_SOFT_H__
#define __BSP_I2C_SOFT_H__
#include "main.h"
#define i2c_scl(x) \
do \
{ \
x ? HAL_GPIO_WritePin(I2C_SCL_GPIO_Port, I2C_SCL_Pin, GPIO_PIN_SET) : HAL_GPIO_WritePin(I2C_SCL_GPIO_Port, I2C_SCL_Pin, GPIO_PIN_RESET); \
} while (0) /* SCL */
#define i2c_sda(x) \
do \
{ \
x ? HAL_GPIO_WritePin(I2C_SDA_GPIO_Port, I2C_SDA_Pin, GPIO_PIN_SET) : HAL_GPIO_WritePin(I2C_SDA_GPIO_Port, I2C_SDA_Pin, GPIO_PIN_RESET); \
} while (0) /* SDA */
#define i2c_read_sda() HAL_GPIO_ReadPin(I2C_SDA_GPIO_Port, I2C_SDA_Pin) /* SDA */
void i2c_start(void);
void i2c_stop(void);
void i2c_ack(void);
void i2c_nack(void);
uint8_t i2c_wait_ack(void);
void i2c_send_byte(uint8_t txd);
uint8_t i2c_read_byte(uint8_t ack);
#endif
5.2 硬件IIC函数
5.2.1 bsp_i2c_hard.c
/*
* bsp_i2c_soft.c
*
* Created: 2025-04-29 15:02:23
* Author: user
* function: 软件I2C
*
*/
#include "bsp_i2c_hard.h"
//I2C写一个字节
//reg:寄存器地址
//data:数据
//返回值:0,正常
// 其他,错误代码
uint8_t MPU6050_Hard_Write_Byte(uint8_t reg,uint8_t data)
{
return HAL_I2C_Mem_Write(&I2C_device, MPU6050_WRITE_ADDR, reg, 1, &data, 1, 30);
}
//I2C读一个字节
//reg:寄存器地址
//返回值:读到的数据
uint8_t MPU6050_Hard_Read_Byte(uint8_t reg)
{
uint8_t data;
HAL_I2C_Mem_Read(&I2C_device, MPU6050_WRITE_ADDR, reg, 1, &data, 1, 30);
// HAL_Delay(1);
//
// HAL_I2C_Master_Transmit(MPU6050_I2C, MPU6050_WRITE_ADDR, ®, 1, 100);
// HAL_I2C_Master_Receive(MPU6050_I2C, MPU6050_READ_ADDR, &data, 1, 100);
return data;
}
//IIC连续写
//addr:器件地址
//reg:寄存器地址
//len:写入长度
//buf:数据区
//返回值:0,正常
// 其他,错误代码
uint8_t MPU6050_Hard_Write_Len(uint8_t addr,uint8_t reg,uint8_t len,uint8_t *buf)
{
return HAL_I2C_Mem_Write(&I2C_device, MPU6050_WRITE_ADDR, reg, 1, buf, len, 30);
}
//IIC连续读
//addr:器件地址
//reg:要读取的寄存器地址
//len:要读取的长度
//buf:读取到的数据存储区
//返回值:0,正常
// 其他,错误代码
uint8_t MPU6050_Hard_Read_Len(uint8_t addr,uint8_t reg,uint8_t len,uint8_t *buf)
{
HAL_I2C_Mem_Read(&I2C_device, MPU6050_WRITE_ADDR, reg, 1, buf, len, 30);
return 0;
}
//I2C写一个字节
//reg:寄存器地址
//data:数据
//返回值:0,正常
// 其他,错误代码
uint8_t OLED_Hard_Write_Data(uint8_t data)
{
uint8_t tmpbuf[2];
tmpbuf[0] = 0x40;
tmpbuf[1] = data;
return HAL_I2C_Master_Transmit(&I2C_device, OLED_WRITE_ADDR, tmpbuf, 2, 30);
}
//I2C写一个字节
//reg:寄存器地址
//data:数据
//返回值:0,正常
// 其他,错误代码
uint8_t OLED_Hard_Write_Command(uint8_t cmd)
{
uint8_t tmpbuf[2];
tmpbuf[0] = 0x00;
tmpbuf[1] = cmd;
return HAL_I2C_Master_Transmit(&I2C_device, OLED_WRITE_ADDR, tmpbuf, 2, 30);
}
5.2.2 bsp_i2c_hard.h
#ifndef __BSP_I2C_HARD_H__
#define __BSP_I2C_HARD_H__
#include "main.h"
extern I2C_HandleTypeDef hi2c1;
#define I2C_device hi2c1
//AD0引脚(9脚)接低,表示0x68为地址,再左移1位为0xd0,最低位表示读写选择
#define MPU_ADDR 0X68
#define MPU6050_READ_ADDR (MPU_ADDR<<1)|1
#define MPU6050_WRITE_ADDR (MPU_ADDR<<1)|0
#define OLED_ADDR 0X3c
#define OLED_WRITE_ADDR (OLED_ADDR<<1)|0
uint8_t MPU6050_Hard_Write_Byte(uint8_t reg,uint8_t data);
uint8_t MPU6050_Hard_Read_Byte(uint8_t reg);
uint8_t MPU6050_Hard_Write_Len(uint8_t addr,uint8_t reg,uint8_t len,uint8_t *buf);
uint8_t MPU6050_Hard_Read_Len(uint8_t addr,uint8_t reg,uint8_t len,uint8_t *buf);
uint8_t OLED_Hard_Write_Command(uint8_t data);
uint8_t OLED_Hard_Write_Data(uint8_t cmd);
#endif
六、I2C外设实践-OLED/MPU6050应用代码
6.1 4脚0.96oled
6.1.0 中文字库以及图像生成方法
1. 中文字库(使用本文章的中文显示函数的设置方法)
2. 图片(使用本文章的图片显示函数的设置方法)
6.1.1 bsp_096oled.c
/*
* bsp_0.96oled.c
*
* Created: 2025-04-29 15:02:23
* Author: user
* function: 4脚 0.96寸 oled显示
*
*/
#include "bsp_096oled.h"
#include "oled_font.h"
#include "delay.h"
#if OLED_SOFT_I2C
#include "bsp_i2c_soft.h"
#elif OLED_HARD_I2C
#include "bsp_i2c_hard.h"
#endif
static uint8_t g_oled_gram[128][8];
/* @brief:计算m的n次方
@param1:m为底数
@param2:n为指数
@retval:m的n次方
*/
static uint32_t oled_pow(uint8_t m, uint8_t n)
{
uint32_t result = 1;
while (n--)
result *= m; // 计算m的n次方
return result;
}
static void oled_i2c_write_command(uint8_t data)
{
#if OLED_SOFT_I2C
i2c_start();
i2c_send_byte(OLED_WRITE_ADDRESS); // 写地址
i2c_wait_ack();
i2c_send_byte(0x00); // 写命令
i2c_wait_ack();
i2c_send_byte(data); // 写命令
i2c_wait_ack();
i2c_stop();
#elif OLED_HARD_I2C
OLED_Hard_Write_Command(data);
#endif
}
static void oled_i2c_write_data(uint8_t data)
{
#if OLED_SOFT_I2C
i2c_start();
i2c_send_byte(OLED_WRITE_ADDRESS); // 写地址
i2c_wait_ack();
i2c_send_byte(0x40); // 写数据
i2c_wait_ack();
i2c_send_byte(data); // 写数据
i2c_wait_ack();
i2c_stop();
#elif OLED_HARD_I2C
OLED_Hard_Write_Data(data);
#endif
}
void oled_write_byte(uint8_t data, uint8_t mode)
{
if (mode)
{
oled_i2c_write_data(data);
}
else
{
oled_i2c_write_command(data);
}
}
void oled_set_pos(uint8_t x, uint8_t y)
{
oled_write_byte(0xb0 + y, OLED_CMD); // 设置页地址
oled_write_byte((x & 0x0f) | 0x00, OLED_CMD); // 设置列地址低4位
oled_write_byte(((x & 0xf0) >> 4) | 0x10, OLED_CMD); // 设置列地址高4位
}
void oled_draw_point(uint8_t x, uint8_t y_all, uint8_t dot)
{
uint8_t pos, bx, temp = 0;
if (x > 127 || y_all > 63)
return; /* 超出范围了. */
pos = y_all / 8; /* 计算GRAM里面的y坐标所在的字节, 每个字节可以存储8个行坐标 */
bx = y_all % 8; /* 取余数,方便计算y在对应字节里面的位置,及行(y)位置 */
temp = 1 << bx; /* 高位表示低行号, 得到y对应的bit位置,将该bit先置1 */
if (dot) /* 画实心点 */
{
g_oled_gram[x][pos] |= temp;
}
else /* 画空点,即不显示 */
{
g_oled_gram[x][pos] &= ~temp;
}
}
void oled_refresh_gram(void)
{
uint8_t i, n;
for (i = 0; i < 8; i++)
{
oled_write_byte(0xb0 + i, OLED_CMD); /* 设置页地址(0~7) */
oled_write_byte(0x00, OLED_CMD); /* 设置显示位置—列低地址 */
oled_write_byte(0x10, OLED_CMD); /* 设置显示位置—列高地址 */
for (n = 0; n < 128; n++)
{
oled_write_byte(g_oled_gram[n][i], OLED_DATA);
g_oled_gram[n][i] = 0;
}
}
}
void oled_display_on(void)
{
oled_write_byte(0x8d, OLED_CMD); // 电荷泵使能
oled_write_byte(0x14, OLED_CMD); // 开启电荷泵
oled_write_byte(0xaf, OLED_CMD); // 开启显示
}
void oled_display_off(void)
{
oled_write_byte(0x8d, OLED_CMD); // 电荷泵使能
oled_write_byte(0x10, OLED_CMD); // 关闭电荷泵
oled_write_byte(0xae, OLED_CMD); // 关闭显示
}
void oled_clear(void)
{
uint8_t i, j;
for (i = 0; i < 8; i++)
{
oled_set_pos(0, i); // 设置页地址
for (j = 0; j < 128; j++)
{
oled_write_byte(0x00, OLED_DATA); // 写数据
}
}
}
void oled_clear_row(uint8_t x1, uint8_t x2, uint8_t y)
{
uint8_t i;
oled_set_pos(x1, y); // 设置页地址
for (i = x1; i < x2; i++)
{
oled_write_byte(0x00, OLED_DATA); // 写数据
}
}
void oled_fill(void)
{
uint8_t i, j;
for (i = 0; i < 8; i++)
{
oled_set_pos(0, i); // 设置页地址
for (j = 0; j < 128; j++)
{
oled_write_byte(0xff, OLED_DATA); // 写数据
}
}
}
/* @brief:oled显示字符
@param1:x(0-127)列
@param2:y(0-7)页
@param3:chr(字符 ASCII码)
@param4:size(字符大小 16(8*16) / 12(12*8))
@retval:void
*/
void oled_show_char(uint8_t x, uint8_t y, uint8_t chr, uint8_t size)
{
uint8_t c = 0, i = 0;
c = chr - ' '; // 得到偏移后的值 ,' '的ASCII码为32
if (size == 16) // 16*8 字体
{
oled_set_pos(x, y);
for (i = 0; i < 8; i++)
oled_write_byte(f8X16[c][i], OLED_DATA); // 写数据
oled_set_pos(x, y + 1);
for (i = 0; i < 8; i++)
oled_write_byte(f8X16[c][i + 8], OLED_DATA); // 写数据
}
else if (size == 12) // 12*8 字体
{
oled_set_pos(x, y);
for (i = 0; i < 6; i++)
oled_write_byte(f12x8[c][i], OLED_DATA); // 写数据
}
}
/* @brief:oled显示数字
@param1:x(0-127)列
@param2:y(0-7)页
@param3:num(数字 0-2的32次方-1)
@param4:len(数字长度 0-10,和num的大小有关)
@param5:size(字符大小 16(8*16) / 12(12*8))
@retval:void
*/
void oled_show_num(uint8_t x, uint8_t y, uint32_t num, uint8_t len, uint8_t size)
{
uint8_t len_rest = len; // 存储一下数字长度
uint8_t i = 0;
while (len_rest != 0)
{
oled_show_char(x, y, num / oled_pow(10, len - i - 1) % 10 + '0', size);
if (size == 16) // 字体16
{
x = x + 8;
if (x > 120)
{
x = 0;
y += 16; // 列超出范围,换行
}
}
if (size == 12) // 字体12
{
x = x + 6;
if (x > 122)
{
x = 0;
y += 8; // 列超出范围,换行
}
}
len_rest--;
i++;
}
}
/* @brief:oled显示字符串
@param1:x(0-127)列
@param2:y(0-7)页
@param3:str(字符串)
@param4:size(字符大小 16(8*16) / 12(12*8))
@retval:void
*/
void oled_show_string(uint8_t x, uint8_t y, uint8_t *str, uint8_t size)
{
uint8_t j = 0;
while (str[j] != '\0') // 字符串显示未结束
{
oled_show_char(x, y, str[j], size); // 显示字符串单个字符
if (size == 16) // 字体16
{
x = x + 8;
if (x > 120)
{
x = 0;
y += 16; // 列超出范围,换行
}
}
if (size == 12) // 字体12
{
x = x + 6;
if (x > 122)
{
x = 0;
y += 8; // 列超出范围,换行
}
}
j++;
}
}
/* @brief:oled显示中文16*16
@note:
1. 显示的汉字为16*16的
2. 显示的汉字为阴码、逆向(低位在前)、列行
@param1:x(0-127)列
@param2:y(0-7)页
@param3:str(字符串)
@param4:size(字符大小 16(8*16) / 12(12*8))
@retval:void
*/
void oled_show_chinese(uint8_t x, uint8_t y, uint8_t number)
{
uint8_t t;
oled_set_pos(x, y); // 设置光标
for (t = 0; t < 16; t++)
{
oled_write_byte(Dic[2 * number][t], OLED_DATA); // 显示某个文字的第一行数据
}
oled_set_pos(x, y + 1); // 设置下一行的光标
for (t = 0; t < 16; t++)
{
oled_write_byte(Dic[2 * number + 1][t], OLED_DATA); // 显示某个文字的第二行数据
}
}
/* @brief:oled显示图片
@note:上到下(先y++),从左到右(再x++)的取模方式来编写
@param1:x(0-127)列
@param2:y_all (0-63)行
@param3:width : 0~127(图片大小,单位为像素)
@param4:height : 0~63(图片大小,单位为像素)
@param5:pic : 图片数据首地址(取模方式为上到下,从左到右)
@param6:mode : 0 反色显示 1 正常显示
@retval:void
*/
void oled_show_picture(uint8_t x, uint8_t y_all, uint8_t width, uint8_t height, const uint8_t *pic, uint8_t mode)
{
uint8_t temp, j;
uint8_t y0 = y_all;
uint8_t *g_pic = NULL;
uint16_t i, psize = 0;
/* 获取该图片的总字节数 */
psize = (height / 8 + ((height % 8) ? 1 : 0)) * width;
/* 超出范围 直接返回 */
if ((x + width > 128) || (y_all + height > 64))
return;
g_pic = (uint8_t *)pic;
for (i = 0; i < psize; i++)
{
temp = g_pic[i];
for (j = 0; j < 8; j++) /* 对一个字节中的8个位数据进行判断 */
{
if (temp & 0x80) /* 高位存放的是低坐标 */
{
oled_draw_point(x, y_all, mode);
}
else
{
oled_draw_point(x, y_all, !mode);
}
temp <<= 1;
y_all++;
if ((y_all - y0) == height) /* 一列数据已经处理完毕 */
{
y_all = y0;
x++;
break;
}
}
}
}
void oled_init(void)
{
delay_ms(200);
oled_write_byte(0xAE, OLED_CMD); // 设置显示开启/关闭,0xAE关闭,0xAF开启
oled_write_byte(0xD5, OLED_CMD); // 设置显示时钟分频比/振荡器频率
oled_write_byte(0x80, OLED_CMD); // 0x00~0xFF
oled_write_byte(0xA8, OLED_CMD); // 设置多路复用率
oled_write_byte(0x3F, OLED_CMD); // 0x0E~0x3F
oled_write_byte(0xD3, OLED_CMD); // 设置显示偏移
oled_write_byte(0x00, OLED_CMD); // 0x00~0x7F
oled_write_byte(0x40, OLED_CMD); // 设置显示开始行,0x40~0x7F
oled_write_byte(0xA1, OLED_CMD); // 设置左右方向,0xA1正常,0xA0左右反置
oled_write_byte(0xC8, OLED_CMD); // 设置上下方向,0xC8正常,0xC0上下反置
oled_write_byte(0xDA, OLED_CMD); // 设置COM引脚硬件配置
oled_write_byte(0x12, OLED_CMD);
oled_write_byte(0x81, OLED_CMD); // 设置对比度
oled_write_byte(0xCF, OLED_CMD); // 0x00~0xFF
oled_write_byte(0xD9, OLED_CMD); // 设置预充电周期
oled_write_byte(0xF1, OLED_CMD);
oled_write_byte(0xDB, OLED_CMD); // 设置VCOMH取消选择级别
oled_write_byte(0x30, OLED_CMD);
oled_write_byte(0xA4, OLED_CMD); // 设置整个显示打开/关闭
oled_write_byte(0xA6, OLED_CMD); // 设置正常/反色显示,0xA6正常,0xA7反色
oled_write_byte(0x8D, OLED_CMD); // 设置充电泵
oled_write_byte(0x14, OLED_CMD);
oled_write_byte(0xAF, OLED_CMD); // 开启显示
oled_clear();
}
6.1.2 bsp_096oled.h
#ifndef __BSP_096OLED_H__
#define __BSP_096OLED_H__
#include "main.h"
// 模式选择
#define OLED_SOFT_I2C 0 // 软件IIC
#define OLED_HARD_I2C 1 // 硬件IIC
#define OLED_ADDRESS 0x3C // OLED IIC地址
#define OLED_WRITE_ADDRESS ((0x3C << 1) | 0) // OLED 写地址
#define OLED_CMD 0x00 // 命令
#define OLED_DATA 0x01 // 数据
void oled_init(void); // OLED初始化
void oled_clear(void); // OLED清屏
void oled_clear_row(uint8_t x1, uint8_t x2, uint8_t y); // OLED清行
void oled_display_on(void); // OLED显示开
void oled_display_off(void); // OLED显示关
void oled_fill(void); // OLED全屏
void oled_set_pos(uint8_t x, uint8_t y); // OLED设置光标位置
void oled_refresh_gram(void); // OLED刷新GRAM
void oled_draw_point(uint8_t x, uint8_t y, uint8_t dot);
void oled_write_byte(uint8_t data, uint8_t mode);
void oled_show_char(uint8_t x, uint8_t y, uint8_t chr, uint8_t size); // OLED显示一个字符
void oled_show_string(uint8_t x, uint8_t y, uint8_t *str, uint8_t size); // OLED显示字符串
void oled_show_num(uint8_t x, uint8_t y, uint32_t num, uint8_t len, uint8_t size); // OLED显示数字
void oled_show_chinese(uint8_t x, uint8_t y, uint8_t number); // OLED显示汉字16*16
void oled_show_picture(uint8_t x, uint8_t y_all, uint8_t width, uint8_t height, const uint8_t *pic, uint8_t mode); // OLED显示图片
#endif
6.2 MPU6050
下面两个是正点原子修改过的DMP库。
'
6.2.1 bsp_mpu6050.h
/*
* bsp_mpu6050.c
*
* Created: 2025-05-01 01:00:00
* Author: user
* function: mpu6050数据读取
*
*/
#include "bsp_mpu6050.h"
#include "delay.h"
#include "usart.h"
#include "inv_mpu.h"
#include "inv_mpu_dmp_motion_driver.h"
#if MPU6050_SOFT_I2C
#include "bsp_i2c_soft.h"
#elif MPU6050_HARD_I2C
#include "bsp_i2c_hard.h"
#endif
/**
* @brief 从MPU6050的指定寄存器写入数据
* @param 1 addr (MPU6050 IIC地址)
* @param 2 reg (寄存器地址)
* @param 3 len (数据长度)
* @param 4 dat (数据)
* @retval MPU6050_EOK : 函数执行成功
* MPU6050_EACK: IIC通讯ACK错误,函数执行失败
*/
uint8_t mpu6050_write(uint8_t addr, uint8_t reg, uint8_t len, uint8_t *dat)
{
#if MPU6050_SOFT_I2C
uint8_t i;
i2c_start();
i2c_send_byte((addr << 1) | 0);
if (i2c_wait_ack() == 1)
{
i2c_stop();
return MPU6050_EACK;
}
i2c_send_byte(reg);
if (i2c_wait_ack() == 1)
{
i2c_stop();
return MPU6050_EACK;
}
for (i = 0; i < len; i++)
{
i2c_send_byte(dat[i]);
if (i2c_wait_ack() == 1)
{
i2c_stop();
return MPU6050_EACK;
}
}
i2c_stop();
return MPU6050_EOK;
#elif MPU6050_HARD_I2C
return MPU6050_Hard_Write_Len(addr, reg, len, dat);
#endif
}
uint8_t mpu6050_write_byte(uint8_t addr, uint8_t reg, uint8_t dat)
{
return mpu6050_write(addr, reg, 1, &dat);
}
/**
* @brief 从MPU6050的指定寄存器读取数据
* @param 1 addr (MPU6050 IIC地址)
* @param 2 reg (寄存器地址)
* @param 3 len (数据长度)
* @param 4 dat (数据)
* @retval MPU6050_EOK : 函数执行成功
* MPU6050_EACK: IIC通讯ACK错误,函数执行失败
*/
uint8_t mpu6050_read(uint8_t addr, uint8_t reg, uint8_t len, uint8_t *dat)
{
#if MPU6050_SOFT_I2C
i2c_start();
i2c_send_byte((addr << 1) | 0);
if (i2c_wait_ack() == 1)
{
i2c_stop();
return MPU6050_EACK;
}
i2c_send_byte(reg);
if (i2c_wait_ack() == 1)
{
i2c_stop();
return MPU6050_EACK;
;
}
i2c_start();
i2c_send_byte((addr << 1) | 1);
if (i2c_wait_ack() == 1)
{
i2c_stop();
return MPU6050_EACK;
}
while (len)
{
*dat = i2c_read_byte((len > 1) ? 1 : 0);
len--;
dat++;
}
i2c_stop();
return MPU6050_EOK;
#elif MPU6050_HARD_I2C
return MPU6050_Hard_Read_Len(addr, reg, len, dat);
#endif
}
uint8_t mpu6050_read_byte(uint8_t addr, uint8_t reg, uint8_t *dat)
{
return mpu6050_read(addr, reg, 1, dat);
}
/**
* @brief mpu6050软件复位
* @param 1 void
* @retval void
*/
void mpu6050_sw_reset(void)
{
mpu6050_write_byte(MPU6050_ADDRESS, MPU_PWR_MGMT1_REG, 0x80);
delay_ms(100);
mpu6050_write_byte(MPU6050_ADDRESS, MPU_PWR_MGMT1_REG, 0x00);
}
/**
* @brief mpu6050设置陀螺仪传感器量程范围
* @param frs 0 --> ±250dps
* 1 --> ±500dps
* 2 --> ±1000dps
* 3 --> ±2000dps
* @retval MPU6050_EOK : 函数执行成功
* MPU6050_EACK: IIC通讯ACK错误,函数执行失败
*/
uint8_t mpu6050_set_gyro_fsr(uint8_t fsr)
{
return mpu6050_write_byte(MPU6050_ADDRESS, MPU_GYRO_CFG_REG, fsr << 3);
}
/**
* @brief mpu6050设置加速度传感器量程范围
* @param frs 0 --> ±2g
* 1 --> ±4g
* 2 --> ±8g
* 3 --> ±16g
* @retval MPU6050_EOK : 函数执行成功
* MPU6050_EACK: IIC通讯ACK错误,函数执行失败
*/
uint8_t mpu6050_set_accel_fsr(uint8_t fsr)
{
return mpu6050_write_byte(MPU6050_ADDRESS, MPU_ACCEL_CFG_REG, fsr << 3);
}
/**
* @brief mpu6050设置数字低通滤波器频率
* @param lpf: 数字低通滤波器的频率(Hz)
* @retval MPU6050_EOK : 函数执行成功
* MPU6050_EACK: IIC通讯ACK错误,函数执行失败
*/
uint8_t mpu6050_set_lpf(uint16_t lpf)
{
uint8_t dat;
if (lpf >= 188)
{
dat = 1;
}
else if (lpf >= 98)
{
dat = 2;
}
else if (lpf >= 42)
{
dat = 3;
}
else if (lpf >= 20)
{
dat = 4;
}
else if (lpf >= 10)
{
dat = 5;
}
else
{
dat = 6;
}
return mpu6050_write_byte(MPU6050_ADDRESS, MPU_CFG_REG, dat);
}
/**
* @brief mpu6050设置采样率
* @param rate 采样率(4~1000Hz)
* @retval MPU6050_EOK : 函数执行成功
* MPU6050_EACK: IIC通讯ACK错误,函数执行失败
*/
uint8_t mpu6050_set_rate(uint16_t rate)
{
uint8_t ret;
uint8_t dat;
if (rate > 1000)
{
rate = 1000;
}
if (rate < 4)
{
rate = 4;
}
dat = 1000 / rate - 1;
ret = mpu6050_write_byte(MPU6050_ADDRESS, MPU_SAMPLE_RATE_REG, dat);
if (ret != MPU6050_EOK)
{
return ret;
}
ret = mpu6050_set_lpf(rate >> 1);
if (ret != MPU6050_EOK)
{
return ret;
}
return MPU6050_EOK;
}
/**
* @brief mpu6050获取温度值
* @param temp 获取到的温度值(扩大了100倍)
* @retval MPU6050_EOK : 函数执行成功
* MPU6050_EACK: IIC通讯ACK错误,函数执行失败
*/
uint8_t mpu6050_get_temperature(int16_t *temp)
{
uint8_t dat[2];
uint8_t ret;
int16_t raw = 0;
ret = mpu6050_read(MPU6050_ADDRESS, MPU_TEMP_OUTH_REG, 2, dat);
if (ret == MPU6050_EOK)
{
raw = ((uint16_t)dat[0] << 8) | dat[1];
*temp = (int16_t)((36.53f + ((float)raw / 340)) * 100);
}
return ret;
}
/**
* @brief mpu6050获取陀螺仪值
* @param 1 gx
* @param 2 gy
* @param 3 gz, 陀螺仪x、y、z轴的原始度数(带符号)
* @retval MPU6050_EOK : 函数执行成功
* MPU6050_EACK: IIC通讯ACK错误,函数执行失败
*/
uint8_t mpu6050_get_gyroscope(int16_t *gx, int16_t *gy, int16_t *gz)
{
uint8_t dat[6];
uint8_t ret;
ret = mpu6050_read(MPU6050_ADDRESS, MPU_GYRO_XOUTH_REG, 6, dat);
if (ret == MPU6050_EOK)
{
*gx = ((uint16_t)dat[0] << 8) | dat[1];
*gy = ((uint16_t)dat[2] << 8) | dat[3];
*gz = ((uint16_t)dat[4] << 8) | dat[5];
}
return ret;
}
/**
* @brief mpu6050获取加速度值
* @param 1 ax
* @param 2 ay
* @param 3 az, 加速度x、y、z轴的原始度数(带符号)
* @retval MPU6050_EOK : 函数执行成功
* MPU6050_EACK: IIC通讯ACK错误,函数执行失败
*/
uint8_t mpu6050_get_accelerometer(int16_t *ax, int16_t *ay, int16_t *az)
{
uint8_t dat[6];
uint8_t ret;
ret = mpu6050_read(MPU6050_ADDRESS, MPU_ACCEL_XOUTH_REG, 6, dat);
if (ret == MPU6050_EOK)
{
*ax = ((uint16_t)dat[0] << 8) | dat[1];
*ay = ((uint16_t)dat[2] << 8) | dat[3];
*az = ((uint16_t)dat[4] << 8) | dat[5];
}
return ret;
}
/**
* @brief mpu6050初始化
* @param 无
* @retval MPU6050_EOK: 函数执行成功
* MPU6050_EID: 获取ID错误,函数执行失败
*/
uint8_t mpu6050_init(void)
{
uint8_t id;
// mpu6050_hw_init(); /* MPU605硬件初始化 */
// mpu6050_iic_init(); /* 引脚初始化IIC接口 */
mpu6050_sw_reset(); /* MPU6050软件复位 */
mpu6050_set_gyro_fsr(3); /* 陀螺仪传感器,±2000dps */
mpu6050_set_accel_fsr(0); /* 加速度传感器,±2g */
mpu6050_set_rate(50); /* 采样率,50Hz */
mpu6050_write_byte(MPU6050_ADDRESS, MPU_INT_EN_REG, 0X00); /* 关闭所有中断 */
mpu6050_write_byte(MPU6050_ADDRESS, MPU_USER_CTRL_REG, 0X00); /* 关闭IIC主模式 */
mpu6050_write_byte(MPU6050_ADDRESS, MPU_FIFO_EN_REG, 0X00); /* 关闭FIFO */
mpu6050_write_byte(MPU6050_ADDRESS, MPU_INTBP_CFG_REG, 0X80); /* INT引脚低电平有效 */
mpu6050_read_byte(MPU6050_ADDRESS, MPU_DEVICE_ID_REG, &id); /* 读取设备ID */
if (id != MPU6050_ADDRESS)
{
return MPU6050_EID;
}
mpu6050_write_byte(MPU6050_ADDRESS, MPU_PWR_MGMT1_REG, 0x01); /* 设置CLKSEL,PLL X轴为参考 */
mpu6050_write_byte(MPU6050_ADDRESS, MPU_PWR_MGMT2_REG, 0x00); /* 加速度与陀螺仪都工作 */
mpu6050_set_rate(50); /* 采样率,50Hz */
return MPU6050_EOK;
}
/**
* @brief mpu6050 外部中断处理
* @param 无
* @retval void
*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == MPU6050_INT_Pin)
{
float pit, rol, yaw;
atk_ms6050_dmp_get_data(&pit, &rol, &yaw);
printf("%.2f,%.2f,%.2f\n", pit, rol, yaw);
}
}
6.2.2 bsp_mpu6050.h
#ifndef __BSP_MPU6050_H__
#define __BSP_MPU6050_H__
// 模式选择
#define MPU6050_SOFT_I2C 1 // 软件IIC
#define MPU6050_HARD_I2C 0 // 硬件IIC
// MPU6050 IIC地址 (A0从原理图可知为0,所以地址为0x68)
// 地址由AD0引脚决定,AD0为低电平时地址为0x68,AD0为高电平时地址为0x69
#define MPU6050_ADDRESS 0x68
/* MPU6050寄存器地址定义 */
#define MPU_ACCEL_OFFS_REG 0X06 // accel_offs寄存器,可读取版本号,寄存器手册未提到
#define MPU_PROD_ID_REG 0X0C // prod id寄存器,在寄存器手册未提到
#define MPU_SELF_TESTX_REG 0X0D // 自检寄存器X
#define MPU_SELF_TESTY_REG 0X0E // 自检寄存器Y
#define MPU_SELF_TESTZ_REG 0X0F // 自检寄存器Z
#define MPU_SELF_TESTA_REG 0X10 // 自检寄存器A
#define MPU_SAMPLE_RATE_REG 0X19 // 采样频率分频器
#define MPU_CFG_REG 0X1A // 配置寄存器
#define MPU_GYRO_CFG_REG 0X1B // 陀螺仪配置寄存器
#define MPU_ACCEL_CFG_REG 0X1C // 加速度计配置寄存器
#define MPU_MOTION_DET_REG 0X1F // 运动检测阀值设置寄存器
#define MPU_FIFO_EN_REG 0X23 // FIFO使能寄存器
#define MPU_I2CMST_CTRL_REG 0X24 // IIC主机控制寄存器
#define MPU_I2CSLV0_ADDR_REG 0X25 // IIC从机0器件地址寄存器
#define MPU_I2CSLV0_REG 0X26 // IIC从机0数据地址寄存器
#define MPU_I2CSLV0_CTRL_REG 0X27 // IIC从机0控制寄存器
#define MPU_I2CSLV1_ADDR_REG 0X28 // IIC从机1器件地址寄存器
#define MPU_I2CSLV1_REG 0X29 // IIC从机1数据地址寄存器
#define MPU_I2CSLV1_CTRL_REG 0X2A // IIC从机1控制寄存器
#define MPU_I2CSLV2_ADDR_REG 0X2B // IIC从机2器件地址寄存器
#define MPU_I2CSLV2_REG 0X2C // IIC从机2数据地址寄存器
#define MPU_I2CSLV2_CTRL_REG 0X2D // IIC从机2控制寄存器
#define MPU_I2CSLV3_ADDR_REG 0X2E // IIC从机3器件地址寄存器
#define MPU_I2CSLV3_REG 0X2F // IIC从机3数据地址寄存器
#define MPU_I2CSLV3_CTRL_REG 0X30 // IIC从机3控制寄存器
#define MPU_I2CSLV4_ADDR_REG 0X31 // IIC从机4器件地址寄存器
#define MPU_I2CSLV4_REG 0X32 // IIC从机4数据地址寄存器
#define MPU_I2CSLV4_DO_REG 0X33 // IIC从机4写数据寄存器
#define MPU_I2CSLV4_CTRL_REG 0X34 // IIC从机4控制寄存器
#define MPU_I2CSLV4_DI_REG 0X35 // IIC从机4读数据寄存器
#define MPU_I2CMST_STA_REG 0X36 // IIC主机状态寄存器
#define MPU_INTBP_CFG_REG 0X37 // 中断/旁路设置寄存器
#define MPU_INT_EN_REG 0X38 // 中断使能寄存器
#define MPU_INT_STA_REG 0X3A // 中断状态寄存器
#define MPU_ACCEL_XOUTH_REG 0X3B // 加速度值,X轴高8位寄存器
#define MPU_ACCEL_XOUTL_REG 0X3C // 加速度值,X轴低8位寄存器
#define MPU_ACCEL_YOUTH_REG 0X3D // 加速度值,Y轴高8位寄存器
#define MPU_ACCEL_YOUTL_REG 0X3E // 加速度值,Y轴低8位寄存器
#define MPU_ACCEL_ZOUTH_REG 0X3F // 加速度值,Z轴高8位寄存器
#define MPU_ACCEL_ZOUTL_REG 0X40 // 加速度值,Z轴低8位寄存器
#define MPU_TEMP_OUTH_REG 0X41 // 温度值高八位寄存器
#define MPU_TEMP_OUTL_REG 0X42 // 温度值低8位寄存器
#define MPU_GYRO_XOUTH_REG 0X43 // 陀螺仪值,X轴高8位寄存器
#define MPU_GYRO_XOUTL_REG 0X44 // 陀螺仪值,X轴低8位寄存器
#define MPU_GYRO_YOUTH_REG 0X45 // 陀螺仪值,Y轴高8位寄存器
#define MPU_GYRO_YOUTL_REG 0X46 // 陀螺仪值,Y轴低8位寄存器
#define MPU_GYRO_ZOUTH_REG 0X47 // 陀螺仪值,Z轴高8位寄存器
#define MPU_GYRO_ZOUTL_REG 0X48 // 陀螺仪值,Z轴低8位寄存器
#define MPU_I2CSLV0_DO_REG 0X63 // IIC从机0数据寄存器
#define MPU_I2CSLV1_DO_REG 0X64 // IIC从机1数据寄存器
#define MPU_I2CSLV2_DO_REG 0X65 // IIC从机2数据寄存器
#define MPU_I2CSLV3_DO_REG 0X66 // IIC从机3数据寄存器
#define MPU_I2CMST_DELAY_REG 0X67 // IIC主机延时管理寄存器
#define MPU_SIGPATH_RST_REG 0X68 // 信号通道复位寄存器
#define MPU_MDETECT_CTRL_REG 0X69 // 运动检测控制寄存器
#define MPU_USER_CTRL_REG 0X6A // 用户控制寄存器
#define MPU_PWR_MGMT1_REG 0X6B // 电源管理寄存器1
#define MPU_PWR_MGMT2_REG 0X6C // 电源管理寄存器2
#define MPU_FIFO_CNTH_REG 0X72 // FIFO计数寄存器高八位
#define MPU_FIFO_CNTL_REG 0X73 // FIFO计数寄存器低八位
#define MPU_FIFO_RW_REG 0X74 // FIFO读写寄存器
#define MPU_DEVICE_ID_REG 0X75 // 器件ID寄存器
// error 标志
#define MPU6050_EOK 0x00 // 无错误
#define MPU6050_EID 0x01 // 错误ID
#define MPU6050_EACK 0x02 // 无应答
#include "main.h"
uint8_t mpu6050_write_byte(uint8_t addr, uint8_t reg, uint8_t dat);
uint8_t mpu6050_read(uint8_t addr, uint8_t reg, uint8_t len, uint8_t *dat);
uint8_t mpu6050_read_byte(uint8_t addr, uint8_t reg, uint8_t *dat);
uint8_t mpu6050_write(uint8_t addr, uint8_t reg, uint8_t len, uint8_t *dat);
void mpu6050_sw_reset(void);
uint8_t mpu6050_set_gyro_fsr(uint8_t fsr);
uint8_t mpu6050_set_accel_fsr(uint8_t fsr);
uint8_t mpu6050_set_lpf(uint16_t lpf);
uint8_t mpu6050_set_rate(uint16_t rate);
uint8_t mpu6050_get_temperature(int16_t *temp);
uint8_t mpu6050_get_gyroscope(int16_t *gx, int16_t *gy, int16_t *gz);
uint8_t mpu6050_get_accelerometer(int16_t *ax, int16_t *ay, int16_t *az);
uint8_t mpu6050_init(void);
#endif
七、本文的工程文件下载链接
工程Github下载链接:https://github.com/chipdynkid/MCU-DL-STM32
(国内)工程Gitcode下载链接https://gitcode.com/chipdynkid/MCU-DL-STM32