MCU开发学习记录13* - I2C学习与实践(HAL库) - 模拟(软件)/硬件I2C控制0.96寸4脚oled显示中文、图片;六轴传感器mpu6050与VOFA+显示 - STM32CubeMX

名词解释:

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介绍

  1. I2C(集成电路间协议)广泛用于短距离通信,尤其是在芯片间或模块间的低速数据传输。
  2. IIC是一种两线式串行总线。用于连接微控制器及其外围设备,多用于主控制器(Master)和从器件(Slave)间的主从通信,在小数据量场合使用,传输距离短,任意时刻只能有一个主机等特性。
  3. 协议要求每次放到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的电平高低变换)谁就是主设备。

  1. IIC一共有只有两个总线:
    一条是双向的串行数据线 SDA
    一条是串行时钟线 SCL

  2. SDA(Serial data line) - 数据线:主、从设备之间数据传输。

  3. SCL(Serial clock line) - 时钟线:主、从设备之间的同步时钟控制(由主设备控制)。

1.2.2 I2C物理层相关知识点

  1. I2C 允许多个主机、多个从机挂载在总线上(因为每个从机设备的地址是独特的)。但是,在任何时间点上只能有一个主控或一个从设备发送数据。

  2. 支持不同速率的通讯速度

  3. SCL和SDA都需要接上拉电阻 (大小由速度和容性负载决定一般在3.3K-10K之间) 保证数据的稳定性,减少干扰。

  4. SDA和SCL线路上都使用开漏连接,并且连接到NMOS晶体管。

  5. 开漏输出不会产生总线竞争,不会使信号处于不确定性。

1.3 I2C协议层

1.3.1 I2C 三种信号以及数据帧

        I2C 总线在传送数据过程中共有三种类型信号,它们分别是:起始信号、结束信号、应答信号。I2C数据帧(寻址或者数据)。

        起始信号和停止信号都是由主机发出,起始信号产生后总线处于占用状态,停止信号产生后总线处于空闲状态。

  1. 起始信号:

    SCL 为高电平时,SDA 由高电平向低电平跳变,开始传送数据。

  2. 结束信号:

    SCL 为高电平时,SDA 由低电平向高电平跳变,结束传送数据。​

  3. 逻辑1和0:
    SCL高电平时,检测SDA的电平。

  4. 应答信号(0是应答,1是非应答):

    接收数据的 IC 在接收到 8bit 数据后,向发送数据的 IC 发出特定的低电平脉冲,表示已收到数据。(即:上拉电阻影响下SDA默认为高,而从机拉低SDA就是确认收到数据即ACK,否则NACK。)

  5. 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.ITEVFENI2C_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.ITEVFENI2C_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.ITEVFENI2C_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.ITEVFENI2C_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 位 置 1I2C 会在 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

        

  1. DEVICE_RESET(设备复位)​
    当设置为1时,此位将所有内部寄存器重置为其默认值。参数:重置完成后,此位自动清零。

  2. SLEEP(睡眠模式)​
    当此位设置为 1 时,MPU-60X0 进入低功耗睡眠模式,暂停所有传感器数据采集。
    1. 睡眠模式下,功耗显著降低,但 ​无法获取数据
    2. 退出睡眠模式需将此位设为 0

  3. CYCLE(循环采样模式)​
    SLEEP 位为 0(非睡眠模式)且 CYCLE 设为 1 时,传感器会在 ​睡眠模式​ 和 ​唤醒单次采样​ 之间循环切换。
    · 采样速率控制​:循环间隔由寄存器 LP_WAKE_CTRL(地址 108)的值决定。

  4. TEMP_DIS(温度传感器禁用)​
    将此位设为 1 时,禁用 MPU-60X0 内部的温度传感器。禁用后,温度传感器数据寄存器(如 OUT_TEMP)将不再更新。若不需要温度数据,禁用可略微降低功耗。

  5. CLKSEL(时钟源选择)
    通过 ​3 位无符号值​ 选择 MPU-60X0 的时钟源。
    1. 默认值 0 适合大多数场景。
    2. 需高精度时选择外部时钟源(如导航系统)。

4.4.2 PWR_MGMT_2(Power Management 2-电源管理2)0x6C

        

  1. LP_WAKE_CTRL(低功耗唤醒频率控制)
    通过 ​2位无符号值​ 设置加速度计在 ​仅加速度计低功耗模式(Accelerometer Only Low Power Mode)​​ 下的唤醒频率。
    唤醒频率决定传感器从休眠到采样的间隔时间,直接影响功耗和数据更新速率。

  2. 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

        

  1. 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)的值反馈。

  2. 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轴加速度计测量值:a_z=g, a_x, a_y

2. 3轴加速度计姿态测量:
        · 当物体静止或匀速运动时,加速度计测得的总加速度由 ​重力加速度(1g)​​ 主导。
        · 通过重力在三个轴上的投影(如 ax,ay,aza_x, a_y, a_zax​,ay​,az​),可计算俯仰角(Pitch)和横滚角(Roll):

           \begin{matrix} \theta_{\text {pitch }}=\arctan \left(\frac{a_{y}}{\sqrt{a_{x}^{2}+a_{z}^{2}}}\right) \\ \quad \theta_{\text {roll }}=\arctan \left(\frac{a_{x}}{\sqrt{a_{y}^{2}+a_{z}^{2}}}\right) \end{matrix}

3. 3轴加速度计姿态测量缺点:

        · 无法直接测量航向角(Yaw)​​:
                重力在水平面(X-Y)的投影与航向角无关,需依赖磁力计或 GPS。

        · 动态误差大​:
                物体垂直加速运动时,加速度计测量值包含 ​运动加速度 + 重力加速度,导致姿态解算失真。

        · 振动敏感​:
                机械振动会引入高频噪声,需通过低通滤波处理。

        

4.5.4 姿态测量 - 陀螺仪(角速度)

1. 3轴陀螺仪姿态测量:对角速度积分得到姿态角变化。

        \begin{array}{c} \operatorname{roll}(n+1)=\operatorname{roll}(n)+\Delta \operatorname{roll}=\operatorname{roll}(n)+\frac{d r}{d t} \Delta t \\ \operatorname{pitch}(n+1)=\operatorname{pitch}(n)+\Delta \operatorname{pitch}=\operatorname{pitch}(n)+\frac{d p}{d t} \Delta t \\ \operatorname{yanv}(n+1)=y \operatorname{yav}(n)+\Delta y a w=y \operatorname{yav}(n)+\frac{d r}{d t} \Delta t \end{array}

2.  3轴陀螺仪姿态测量问题:(零漂)

        陀螺仪输出的角速度包含 ​零偏误差​(非零静止输出)和 ​白噪声,积分后误差随时间累积,导致姿态角漂移。

        

4.5.5 姿态测量 - 传感器融合

1. 传感器融合姿态测量:

        

2. 传感器融合姿态测量问题:

        yaw偏航角存在零漂:
通过历史数据拟合直线方程 \psi_{\text{drift}} = k \cdot t + c,估计斜率 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, &reg, 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

### 1.44OLED屏幕I2C接口使用配置 对于1.44OLED屏而言,其相较于常见的0.96版本增加了额外的控制端口(RES、DC、CS、BLK),而同样支持基本的数据传输接口VCC、GND、SCL以及SDA[^1]。当利用I2C总线来操作这块显示屏时,主要关注的是如何正确连接并初始化这些针。 #### 接线说明 为了使能I2C通信模式,在硬件层面需确保如下连线: - VCC 连接到电源正极 - GND 连接到公共接地 - SDA (Serial Data Line) 数据线应连到微控制器对应的SDA引- SCL (Serial Clock Line) 时钟信号则要对应MCU上的SCL引 - RES 复位信号通常拉低一段时间后再释放以完成复位动作;此过程可以在软件内编程实现也可以通过外部电路自动处理 - DC 数据/命令选择器用于区分发送给显示器的内容是指令还是图像数据;一般情况下将其固定为某一电平状态即可满足需求 - CS 片选信号在此处可能并不重要因为选择了I2C作为通讯手段而非SPI - BLK 背光亮度调节输入端可依据实际应用场景决定是否接入PWM波形发生装置从而动态调整背光源强度 #### 初始化设置 一旦完成了物理层面上的准备之后,则需要编写相应的驱动程序来进行进一步的操作。这涉及到向特定寄存器写入预定义值以便激活设备的各项功能特性。具体来说,可以通过调用函数或者自定义代码片段来执行必要的初始化序列,比如清除显示缓冲区、设定对比度等级等参数。 ```c // 假设已经包含了必要的头文件并且声明了一个指向I2C外设结构体实例化的指针变量i2c_handle void oled_init(I2C_HandleTypeDef *hi2c){ uint8_t init_command[]={ 0xAE, // 关闭显示 0xD5, 0xF0, // 设置振荡频率 ... 0xA1, // 反转显示方向 0xAF // 打开显示 }; HAL_I2C_Master_Transmit(hi2c,OLED_ADDRESS,sizeof(init_command),init_command,HAL_MAX_DELAY); } ``` 上述示例展示了部分典型的初始化命令列表及其传送方法。值得注意的是,确切的命令集会因制造商的不同有所差异,因此建议查阅官方文档获得最权威的信息来源。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值