基于51单片机的MPU6050实践学习**一.简单驱动&上位机后处理篇

1. 简介

      惯性测量单元是测量物体三轴姿态角(或角速率)以及加速度的装置。一般的,一个IMU包含了三个单轴的加速度计和三个单轴的陀螺,加速度计检测物体在载体坐标系统独立三轴的加速度信号,而陀螺检测载体相对于导航坐标系的角速度信号,测量物体在三维空间中的角速度和加速度,并以此解算出物体的姿态。IMU通常包含以下主要组件:

1.1 IMU主要组件
  1. 加速度计(Accelerometer):测量物体在x、y、z三个轴上的加速度,包括静态加速度(如重力)和动态加速度(如运动引起的加速度)。

  2. 陀螺仪(Gyroscope):测量物体在x、y、z三个轴上的角速度,即物体绕这些轴的旋转速度。

  3. 磁力计(Magnetometer)(某些IMU包含):测量地磁场强度,用于确定物体的朝向(方位角)。

 1.2 IMU使用场景和发展趋势

        IMU在许多竞赛和实际应用场景中都得到了广泛应用,特别是在涉及运动感知、姿态控制和导航的领域。

1.3 MPU6050

        MPU6050作为一款IMU芯片,由InvenSense公司生产,是一款集成了加速度传感器和陀螺仪的6轴运动传感器。它可以用于测量物体在三维空间中的加速度和旋转速度,并计算出物体的姿态角度。该传感器具有高精度和低功耗的特性,因此也被广泛应用于机器人、飞行器、智能穿戴设备等领域,目前市面上有许多现成的MPU6050模块。

MPU6050模块—GY-521

        目前网上基于STM32的MPU6050资料较多,关于51单片机与其搭配使用的资料较少,因此本章基于51单片机,会以多节对MPU6050的相关原理、简单驱动以及校准和使用方法的完整讲解。

2. 运动传感器与姿态测量

关于欧拉角、旋转矩阵等网上资料层出不穷,但是细细品读可以发现,许多资料众说纷纭,不同资料间的知识点描述无法兼容,甚至矛盾......所以,这块内容的学习重在领会和推导,唯有实践出真章,同时,本文对应这块内容尽量以前后统一的逻辑和表达方式进行介绍。思维导图如下:
 

2.1 坐标系

        欧拉角基本以右手坐标系为主,坐标系类型的确定是欧拉角正负表示的基础。以右手系为例:绕旋转轴呈右手螺旋状转动为正,否则为负(即使用右手的大拇指指向旋转轴,其他4个手指在握拳过程中的指向便是正的角度)。

2.2 欧拉角   

        欧拉角(Euler angle)指的是绕参考坐标系主轴之一旋转的角度。也就是说,若新的坐标系是绕旧的坐标系的主轴之一旋转形成的,那么这个旋转角就是欧拉角。

        在三维空间中,三个独立的旋转足以描述任何旋转。考虑到每个旋转矩阵有三个自由度(例如,欧拉角中的三个角度),三个旋转矩阵的组合能够捕捉所有可能的旋转变换。因此,任何复杂的旋转都可以通过最多三个基本旋转来实现,这就引出了顺规的概念。

        顺规:旋转变换可以归结为若干个沿着坐标轴旋转的组合,组合个数不超过三个并且两个相邻的旋转必须沿着不同坐标轴。

顺规类别表示方法
经典欧拉角(Proper Euler angles)z-x-z、x-y-x、y-z-y、z-y-z、x-z-x、y-x-y
泰特-布莱恩角(Tait–Bryan angles)x-y-z、y-z-x、z-x-y、x-z-y、z-y-x、y-x-z

        内旋 Intrinsic rotations:绕运动轴
        外旋 Extrinsic rotations:绕固定轴
        每次旋转都是绕上一次旋转所新产生的坐标轴旋转的,如下列动图,第二次旋转绕的是x'而不是固定的坐标轴x。所以是绕运动轴,即内旋。

        每次旋转都是绕固定的坐标轴旋转的,如下列”外在旋转示意图“,第二次旋转绕的是固定的坐标轴X。所以是绕运动轴,即外旋。 

内旋&外旋示意

         在绕轴旋转的时候,顺规有12种,内旋、外旋有2种,但是一般情况下,欧拉角都是说的绕自身轴(运动轴)旋转,即 内旋 。所以欧拉角共有 1×12=12种表示方式。

       航空领域通常采用的旋转方式是 ZYX 顺序,下图是一架飞机按照 ZYX 组合进行旋转产生欧拉角的示意,其中,Yaw—偏航角;Roll—翻滚角;Pitch—俯仰角

2.3 旋转矩阵

以上层次图中,有一个需要特别注意的点:主动旋转和被动旋转。
主动旋转—物体本身的旋转,比如坐标系中动点的旋转;
被动旋转—参照系的旋转,比如坐标系的旋转;
本文下面关于旋转矩阵的推导以被动旋转为主!定义较多,相比其他文章的介绍也略有不同,重点是理解每个定义后活学活用,让公式或性质派上用场,为我们所用。

2.3.1 坐标转换矩阵&旋转矩阵

        我们先回忆下:给定二维坐标系下,定点(向量)绕轴旋转后,坐标变化的矩阵表示形式:
        e.g.1. 已知任意一个向量,把向量 OA 绕其起点 O 沿逆时针方向旋转α角得到向量:。推导如下:

        以上是点(向量)在固定坐标系下经旋转后的新的坐标表示,那么我们举一反三:定点(向量)绝对位置不动,然后旋转坐标系,定点(向量)在新坐标系下的坐标表示又是如何的呢?将上面的例子e.g.1.稍微改造下:
        e.g.2 已知任意一个向量 ,把坐标轴xOy绕其原点 O 沿逆时针方向旋转α角得到新坐标系x'Oy',此时向量OA在新坐标系下的坐标为 : 。为帮助理解,下面我们将用3种方法(相对法、矩阵求逆法、直接推导)进行推演,方便大家更好理解,推导如下:

方法①:相对法
方法②:矩阵求逆法
方法③:直接推导

        由此我们清楚了,在定坐标系下,用于表示点或向量旋转后坐标变化关系的矩阵叫做坐标转换矩阵,即上面e.g.1 例题中推导出的矩阵为坐标转换矩阵;用于表示定点相对旋转坐标系下的坐标变化关系的矩阵叫做旋转矩阵。即上面e.g.2 例题中使用3种方法推导出的矩阵为旋转矩阵。
        同时,从方法③中的推导过程可以看出:假设旋转前坐标系为O系,旋转后坐标系为O'系,旋转矩阵中各元素分别为 两坐标系对应坐标轴正方向的旋转角余弦值,即

        上图是二维坐标系的情况,我们引申到三维坐标系中, A坐标系 绕 B坐标系的单轴或复合旋转(也就是坐标系A 相对于 坐标系B 的旋转),定义旋转矩阵为_{B}^{A}\textrm{R},设点p在B坐标系下坐标为p^{B},在A坐标系下坐标为p^{A},则有

p^{A}=_{B}^{A}\textrm{R}p^{B}

        同样地,将二维旋转矩阵延伸到三维旋转矩阵中,矩阵中各元素的意义如下:

        参考二维矩阵下旋转矩阵的推导过程,三维坐标系绕坐标轴单轴旋转的旋转矩阵表示如下:

2.3.2 旋转矩阵说明

        由上一小节可知,旋转矩阵反映了一个坐标系中的坐标在另一个坐标系中表示的转换关系。矩阵的乘法,本质是一种运动矩阵的实质就是将坐标整体线性变换且矢量的前移相当于坐标系的后移。
        为了规范旋转矩阵的描述格式,我们定义:

        A坐标系 绕 B坐标系的单轴或复合旋转(也就是坐标系A 相对于 坐标系B 的旋转),定义旋转矩阵为_{B}^{A}\textrm{R},设点p在B坐标系下坐标为p^{B},在A坐标系下坐标为p^{A},则有

                                                                p^{A}=_{B}^{A}\textrm{R}p^{B}             (1)

2.3.2.1 内旋、外旋下,总体旋转矩阵的表示

还是那句话,网上资料纷纷扰扰,说法各有千秋,细心的你会发现,不同资料中,旋转矩阵左乘、右乘和机体坐标系、固定坐标系的对应关系是不同的。有的是因为定义不同,有的则是因为东拼西凑导致的错误。所以跟随清晰的、完整的定义来自己理解、总结是十分重要的。下面表述或验证较多,希望能够让大家清晰~

注意:主动旋转(物体旋转)和被动旋转(坐标系旋转)下,内旋和外旋下的总体旋转矩阵左乘/右乘规律不同,我们证明过程中选择被动旋转(坐标系旋转)

被动旋转(坐标系旋转)下相对于机体坐标系旋转—对应的旋转矩阵依次左乘

定义坐标系0、1、2,且点p在坐标系0、1、2中的坐标分别为p^{0}p^{1}p^{2},其中:
*坐标系0相对于坐标系1的旋转矩阵,记为_{1}^{0}\textrm{R}
*坐标系1相对于坐标系2的旋转矩阵,记为_{2}^{1}\textrm{R}
*坐标系0相对于坐标系2的旋转矩阵,记为_{2}^{0}\textrm{R}
其中,相对于哪个坐标系旋转,就是绕哪个坐标系的轴旋转。
参考式(1)定义,有
                                                             p^{0}=_{1}^{0}\textrm{R}p^{1}             (2)
                                                             p^{1}=_{2}^{1}\textrm{R}p^{2}             (3)
                                                             p^{0}=_{2}^{0}\textrm{R}p^{2}             (4)

(2)、(4)两式列等式后将(3)带入,有
                                                    _{2}^{0}\textrm{R}p^{2}=_{1}^{0}\textrm{R}_{2}^{1}\textrm{R}p^{2}             (5)
所以有,

_{2}^{0}\textrm{R}=_{1}^{0}\textrm{R}_{2}^{1}\textrm{R}               (6)

至此,被动旋转下,相对于机体坐标系旋转(内旋)—对应的旋转矩阵依次左乘 得证。语言表述为:坐标系2到坐标系0的旋转矩阵,等于按照顺序旋转(先从坐标系2旋转到坐标系1,然后从坐标系1旋转到坐标系0)对应的旋转矩阵依次左乘。图示如下:

被动旋转(坐标系旋转)下相对于固定坐标系旋转—对应的旋转矩阵依次右乘

在推导类似(6)式关系式之前,我们先推导 同一个旋转 在不同坐标系下 有什么关系,推导如下:

***********************************************************************************

由(17)式,我们也可得出:

从2.3.1小节可知:针对一点p,按照相同矢量分别对 该点 和 坐标系进行旋转,最终该点坐标在两种旋转下会相差 2*α 角度;

所以,我们可以得到向量旋转(坐标转换矩阵)和坐标系旋转(旋转矩阵)之间的关系:

所以,(17)、(18)式可变形为:

上述(17)、(18)、(20)、(21)式,无论何种表示形式,其表征的都是 坐标(或坐标系)在A坐标系下的坐标转换矩阵(或旋转矩阵),和其在B坐标系下的坐标转换矩阵(或旋转矩阵)以及A、B坐标系之间的旋转矩阵的等量关系。

好,言归正传,我们需要推导的是 相对于固定坐标系旋转(外旋)—对应的旋转矩阵依次右乘,下面进入正文。
*******************************************************************

重新定义3个坐标系分别为0、1、2,其中坐标系0为固定坐标系。因为旋转是个“事实”,旋转矩阵只不过是描述这个“事实”的工具,因此我们如下表述:
坐标系0旋转得到坐标系1,该过程相对于固定坐标系的旋转矩阵,记为_{0}^{1}\textrm{R}^{0}
坐标系1旋转得到坐标系2,该过程相对于固定坐标系的旋转矩阵,记为 _{1}^{2}\textrm{R}^{0}
坐标系0旋转得到坐标系2,该过程相对于固定坐标系的旋转矩阵,记为_{0}^{2}\textrm{R}^{0}
由公式20及0系为固定坐标系的定义可知,

        此处对公式24如何对应公式20进行解释,此时的_{1}^{2}\textrm{R}是在1系下的旋转矩阵,就相当于_{M_{1}}^{N_{1}}\textrm{R}^{1}_{1}^{2}\textrm{R}^{0}是在固定坐标系(0系)下的坐标系旋转矩阵,相当于_{M_{0}}^{N_{0}}\textrm{R}^{0},从24式也可看出,从矩阵等式等量关系角度看,坐标系1旋转得坐标系2的坐标系旋转矩阵,以及该过程相对某固定坐标系0系得到的旋转矩阵,以上两者的矩阵的转换关系还与0系相对1系的旋转矩阵有关。

至此,由(26)式可得,被动旋转(坐标系旋转)下相对于固定坐标系,旋转矩阵按旋转顺序左乘 得证。

2.3.2.2 重要特性证明

同一个旋转矩阵组合R_{x}(\alpha )R_{y}(\beta )R_{z}(\gamma ),由上面2.3.2.1推导可知,我们有两种理解:

1.相对xyz坐标系,旋转顺序为x-α、y-β、z-γ的外旋;
2.相对xyz坐标系,旋转顺序为z-γ、x-α、y-β的内旋;

根据上面推导归纳我们可知,用欧拉角来表示物体姿态或者旋转时,需要定义以下四个元素:
1.旋转顺序(即顺规);2.旋转角度;3.旋转方向(内旋/外旋);4.主动旋转还是被动旋转;

2.3.2.3 不同顺规下旋转矩阵表示

在明确2.3.2.2描述旋转的四要素后,我们整理出不同旋转顺序、角度、方向下,主动旋转和被动旋转的旋转矩阵表示,如下所示:

⭕关于前面说的主动旋转(物体旋转)和被动旋转(坐标系旋转)下,内旋和外旋下的总体旋转矩阵左乘/右乘规律不同,也就是:

被动旋转(坐标系旋转)下相对于固定坐标系旋转(外旋)—对应的旋转矩阵依次右乘;
                                               相对于载体坐标系旋转(内旋)—对应的旋转矩阵依次左乘;

主动旋转(物体旋转)下:   相对于固定坐标系旋转(外旋)—对应的旋转矩阵依次左乘;
                                              相对于载体坐标系旋转(内旋)—对应的旋转矩阵依次右乘;

我们不进行推导,直接使用实际例子进行验证:
⭕验证逻辑是对同一点进行指定角度下的被动旋转/主动旋转、外旋/内旋,分别使用前面的公式计算旋转后的点坐标,最后和模拟软件得到的坐标结果进行对比,但是这个软件愁坏了我,突然想到读书时候学习的动力学仿真软件Adams拥有十分成熟的数值求解算法,且针对坐标系操作具有较大自由度,4个组合下的验证结果如下,再次验证了主动旋转/被动旋转的旋转矩阵包括左乘和右乘在内的差异点!

软件求解+公式计算验证:被动旋转+外旋
软件求解+公式计算验证:被动旋转+内旋
软件求解+公式计算验证:主动旋转+外旋
软件求解+公式计算验证:主动旋转+内旋

使用IMU的复杂系统的位姿解算一定会使用到上述知识,本文仅作为交流,博客中仅下面上位机后处理涉及此部分。

3. MPU6050原理简介及寄存器说明

3.1 MPU6050原理简介

MPU-60X0由以下关键模块和功能组成:
⭕具有16位ADC和信号调节的三轴MEMS速率陀螺仪传感器
⭕具有16位ADC和信号调节的三轴MEMS加速度计传感器数字运动处理器
⭕数字运动处理器
⭕主I2C和SPI(仅限MPU-6000)串行通信接口
⭕用于第三方磁力计和其他传感器的辅助I 2C串行接口
⭕时钟
⭕传感器数据寄存器
⭕FIFO
⭕中断
⭕数字输出温度传感器
⭕陀螺仪和加速度计自检
⭕偏置和LDO
⭕电荷泵

MPU6050框图

注意: 以上框图中,圆括号( )中的引脚名称仅适用于 MPU-6000;
            方括号 [ ] 中的引脚名称仅适用于 MPU-6050;

3.2 常用寄存器说明

1)采样率分频寄存器(Sample Rate Divider)
该寄存器指定了陀螺仪输出速率的分频器,用于为 MPU-60X0 生成采样率。

描述
⭕采样率 (Sample Rate) 是通过将陀螺仪输出速率除以 SMPLRT_DIV(八位无符号数)生成的:
                                          采样频率=时钟源/(1 + SMPLRT_DIV)
⭕其中,当 DLPF 被禁用时(DLPF_CFG = 0 或 7),陀螺仪输出速率 = 8kHz,当 DLPF 被启用时为 1kHz(参见 配置寄存器)。
⭕注意:加速度计输出速率为 1kHz。这意味着对于大于 1kHz 的 Sample Rate ,相同的加速度计样本可以多次输出到 FIFO、DMP 和 sensor 寄存器。
**********************************************************************

2)配置寄存器(Configuration)
该寄存器配置陀螺仪和加速度计的外部帧同步(FSYNC)引脚采样和数字低通滤波器(DLPF) 设置。

描述
⭕连接到 FSYNC 引脚的外部信号可以通过配置 EXT_SYNC_SET 进行采样。
⭕对 FSYNC 引脚的信号更改被锁存,以便可以捕获短选通。锁存的 FSYNC 信号将以采样率分频寄存器 中定义的 Sampling Rate 进行采样。采样后,锁存器将重置为当前的 FSYNC 信号状态。
⭕根据下表,将报告采样值,以代替传感器数据寄存器中由 EXT_SYNC_SET 值确定的最低有效位。

⭕DLPF由DLPF_CFG配置,加速度计和陀螺仪根据DLPF_CFG的值进行滤波,如下表所示。

*****************************************************************************

3)陀螺仪配置寄存器(Gyroscope Configuration
此寄存器用于触发陀螺仪自检和配置陀螺仪的满量程范围。

描述
⭕陀螺仪自检允许用户测试陀螺仪的机械和电气部分。每个陀螺仪轴的自检可以通过控制该寄存器的 XG_ST、YG_ST 和 ZG_ST 位来激活。每个轴的自检可以独立执行,也可以同时执行。
⭕FS_SEL 根据下表选择陀螺仪输出的满量程范围。

*****************************************************************************

4)加速度计配置寄存器(Accelerometer Configuration)
此寄存器用于触发加速度计自检和配置加速度计满量程范围。该寄存器还配置 Digital High Pass Filter(DHPF)。

描述
⭕加速度计自检允许用户测试加速度计的机械和电气部分。每个 accelerometer 轴的自检可以通过控制该 register 的 XA_ST、 YA_ST 和 ZA_ST 位来激活。每个轴的自检可以独立执行,也可以同时执行。
⭕AFS_SEL 根据下表选择加速度计输出的满量程范围。

*****************************************************************************

5)加速度计测量寄存器(Accelerometer Measurements
这些 registers 存储最新的 accelerometer 测量值。

描述
⭕加速度计测量值以 采样率分频寄存器(Sample Rate Divider) 中定义的 Sample Rate 写入这些寄存器。
⭕加速度计测量寄存器以及温度测量寄存器、陀螺仪测量寄存器和外部传感器数据寄存器由两组寄存器组成:内部寄存器集和面向用户的读取寄存器集。
⭕加速度计传感器内部寄存器集中的数据始终以 Sample Rate 更新。同时,每当串行接口空闲时,面向用户的读寄存器集就会复制内部寄存器集的数据值。这保证了传感器寄存器的突发读取将从同一采样时刻读取测量值。请注意,如果未使用 burst reads,则用户负责通过检查 Data Ready 中断来确保一组单字节读取对应于单个采样时刻。
⭕每个16位加速度计测量都有一个以 ACCEL_FS 定义的满量程(加速度计配置寄存器(Accelerometer Configuration))。对于每个满量程设置,加速度计的灵敏度以 LSB/g 为单位,如下表所示。

*****************************************************************************

6)温度测量寄存器(Temperature Measurement)
这些寄存器存储最新的温度传感器测量值。

描述
⭕温度测量值以采样率分频寄存器(Sample Rate Divider)中定义的 Sample Rate 写入这些寄存器。
⭕这些温度测量寄存器以及加速度计测量寄存器、陀螺仪测量寄存器和外部传感器数据寄存器由两组寄存器组成:内部寄存器集和面向用户的读取寄存器集。
⭕温度传感器内部寄存器集中的数据始终以 Sample Rate 更新。同时,每当串行接口空闲时,面向用户的读寄存器集就会复制内部寄存器集的数据值。这保证了传感器寄存器的突发读取将从同一采样时刻读取测量值。请注意,如果未使用 burst reads,则用户负责通过检查 Data Ready 中断来确保一组单字节读取对应于单个采样时刻。
⭕给定寄存器值的温度(以摄氏度为单位)可以计算为:
以摄氏度为单位的温度 = (TEMP_OUT 作为有符号量的寄存器值)/340 + 36.53
请注意,上述等式中的数学公式以十进制为单位。
*****************************************************************************

7)陀螺仪测量寄存器(Gyroscope Measurements)
这些 registers 存储最新的陀螺仪测量值。

描述
⭕陀螺仪测量值以采样率分频寄存器(Sample Rate Divider)中定义的 Sample Rate 写入这些寄存器。
⭕这些陀螺仪测量寄存器以及加速度计测量寄存器、温度测量寄存器和外部传感器数据寄存器由两组寄存器组成:内部寄存器集和面向用户的读取寄存器集。
⭕陀螺仪传感器内部寄存器集中的数据始终以 Sample Rate 更新。同时,每当串行接口空闲时,面向用户的读寄存器集就会复制内部寄存器集的数据值。这保证了传感器寄存器的突发读取将从同一采样时刻读取测量值。请注意,如果未使用 burst reads,则用户负责通过检查 Data Ready 中断来确保一组单字节读取对应于单个采样时刻。
⭕每个16位陀螺仪测量都有一个以 FS_SEL 定义的满量程(陀螺仪配置寄存器(Gyroscope Configuration)。对于每个满量程设置,陀螺仪灵敏度以 LSB/°/s 为单位,如下表所示:

*****************************************************************************

8)电源管理 1 寄存器(Power Management 1)
该 register 允许用户配置 power mode 和 clock source。它还提供了一个用于重置整个设备的位,以及一个用于禁用温度传感器的位。

描述
⭕通过将 SLEEP 设置为 1,MPU-60X0 可以进入低功耗睡眠模式。当 CYCLE 设置为 1 而 SLEEP 处于禁用状态(Bit6=0)时,MPU-60X0 将进入 循环模式。在 循环模式 下,器件在睡眠模式和唤醒模式之间循环,以 LP_WAKE_CTRL(电源管理 2 寄存器(Power Management 2))确定的速率从加速度计获取单个数据样本。要配置唤醒频率,请使用电源管理 2 寄存器(Power Management 2)中的 LP_WAKE_CTRL。
⭕可以选择内部 8MHz 振荡器、基于陀螺仪的时钟或外部源作为 MPU-60X0 时钟源。当选择内部 8 MHz 振荡器或外部源作为时钟源时,MPU-60X0 可以在禁用陀螺仪的情况下在低功耗模式下运行。
⭕上电后,MPU-60X0 时钟源默认为内部振荡器。但是,强烈建议将器件配置为使用其中一个陀螺仪(或外部 clock source)作为 clock reference,以提高稳定性。可以根据下表选择 clock source。

*****************************************************************************

9)电源管理 2 寄存器(Power Management 2)
此寄存器允许用户在 Accelerometer Only Low Power Mode 中配置唤醒频率。该寄存器还允许用户将 accelerometer 和 gyroscope 的各个轴置于 standby 模式。

描述
⭕MPU-60X0 可以通过以下步骤进入仅加速度计低功耗模式:
(i) 将 CYCLE 位设置为 1
(ii) 将 SLEEP 位设置为 0
(iii) 将 TEMP_DIS 位设置为 1
(iv) 将 STBY_XG、STBY_YG 位和 STBY_ZG 位设置为 1
以上所有位都可以在 电源管理 1 寄存器(Power Management 1)中找到。
⭕在此模式下,该器件将关闭除主 I2 C 接口之外的所有器件,仅以固定间隔唤醒加速度计以进行一次测量。唤醒频率可以通过 LP_WAKE_CTRL 进行配置,如下所示。

⭕用户可以使用此 register将单个 accelemeter 和 gyroscopes 轴置于 standby 模式。如果器件使用陀螺仪轴作为 clock source,并且该轴处于 standby 模式,则 clock source 将自动更改为内部 8MHz 振荡器。

*****************************************************************************

3.3 写入与读出序列说明
3.3.1 单字节写入序列(Single-Byte Write Sequence)

⭕为了写入内部 MPU-60X0 寄存器,主设备发送启动条件 (S),然后是 I2C 地址和写入位 (0)。在第 9 个 clock cycle(当 clock 为高电平时),MPU-60X0 确认 transfer。然后,主控制器将寄存器地址 (RA) 放在总线上。在 MPU-60X0 确认收到寄存器地址后,主站将寄存器数据放到总线上。然后是 ACK 信号,数据传输可以通过停止条件 (P) 来结束。

3.3.2 突发写入序列(Burst Write Sequence)

⭕要在最后一个 ACK 信号之后写入多个字节,主站可以继续输出数据,而不是发送停止信号。在这种情况下,MPU-60X0 会自动增加寄存器地址并将数据加载到适当的寄存器。下图显示了单字节和双字节写入序列。也就是当前面指定RA(寄存器地址)后,多字节写入过程中,寄存器地址会自动增加。

3.3.3 单字节读出序列(Single-Byte Read Sequence)

⭕为了读取内部 MPU-60X0 寄存器,主设备发送一个启动条件,然后是 I2C 地址和一个写入位,然后是要读取的寄存器地址。在接收到 MPU-60X0 的 ACK 信号后,主站发送一个启动信号,后跟从地址和读取位。因此,MPU-60X0 发送 ACK 信号和数据。通信以未确认 (NACK) 信号和来自 master 的停止位结束。定义 NACK 条件,使得 SDA 线在第 9 个 clock cycle保持高电平。下图显示了单字节读取序列。

3.3.4 突发读出序列(Burst Read Sequence)

⭕双字节读取序列如下所示,根据3.3.3 中读出序列逻辑说明,我们发现读取单字节还是多字节数据由Master发送NACK/ACK信号决定。

4. IIC串口通信协议介绍

4.1 常用通信协议

串行通信和并行通信的区别:
1、并行通信指的是并行通信端口,同时传送八路信号,一次并行传送完整的一个字节信息,串行通信指的是串行通信端口, 在一个方向上只能传送一路信号,传送一个字节信息时,只能一位一位地依次传送;
2、并行通信是在同一时刻发送多位数据,串行通信用一根线在不同的时刻发送8位数据;
3、并行通信发送速度快,距离短资源占用多,串行通信发送速度慢,距离远资源占用少。

        GY-521 MPU6050模块,产品参数如下,其IC内部含有一个IIC 接口,用于与微处理器之间的通信,同时,为了直观显示加速度、角速度等数据,使用UART串口将其通过上位机打印出来,因此需要了解IIC串口通信协议UART串口通信协议

产品参数
4.2 IIC串口通信协议

IIC(Inter-Integrated Circuit是IICBus简称,所以中文应该叫集成电路总线,属于串行通信,半双工通信方式,同步通信。

如下图是一个完整IIC传输示意:在开始与 START 条件 (S) 通信后,主设备发送一个 7 位从地址(ADDRESS),后跟一个第 8 位(R/W),即读/写位。读/写位表示主设备是从设备接收数据还是向从设备写入数据。然后,主机释放 SDA 线并等待来自从器件的确认信号 (ACK)。传输的每个字节(DATA)后跟一个确认位(ACK)。为了确认,从器件将 SDA 线拉低,并在 SCL 线的高电平期间保持低电平。数据传输始终由 Master 以 STOP 条件 (P) 终止,从而释放通信线路。但是,主站可以生成重复的 START 条件 (Sr),并在不先生成 STOP 条件 (P) 的情况下寻址另一个从站。
⭕当 SCL 为高电平时,SDA 线路上的低电平到高电平转换定义了停止条件。
⭕所有 SDA 更改都应在 SCL 为低电平时进行,但启动和停止条件除外。

完整IIC数据传输

⭕IIC数据传输中标识位说明如下:

IIC标识位说明

Signal

Description

S

启动条件:SDA从高到低,而SCL为高

AD

从机地址

W

写入位(0)

R

读入位(1)

ACK

应答:SDA线为低电平,而SCL线在第9clock cycle为高电平

NACK

非应答:SDA线在第9个时钟周期保持高电平

RA

MPU-60X0系列IMU内部寄存器地址

DATA

传输或接收数据

P

停止条件:SDA由低转高,而SCL为高电平

4.3 UART串口通信协议介绍

UART(Universal Asynchronous Receiver/Transmitter:UART)是一种采用全双工异步串行通信方式的通用异步收发传输器。它在发送数据时将并行数据转换成串行数据来传输,在接收数据时将接收到的串行数据转换成并行数据。

4.3.1 协议层
4.3.1.1 波特率

 波特率是指每秒钟传输的比特数,是通信双方事先约定好的,它无需时钟信号同步,而是通过设定双方一致的波特率进行数据传输。通信的双方必须使用相同的波特率才能正常通信。单位是 bps (位 / 秒)。常用的波特率有 9600 、 19200 、 38400 、 57600 以及115200 等。如波特率为9600bps的计数器需要计数(8/9600/时钟周期)次才可以传输1Byte数据。

4.3.1.2 数据帧格式

数据帧的格式包括起始位、数据位、奇偶校验位和停止位的组合。
起始位(Start Bit)
        1.起始位用于标志数据传输的开始。
        2.在通信空闲时,通信线保持高电平(逻辑1)。当传输开始时,发送端将通信线拉低到低    电平(逻辑0),持续一个波特率周期,表示起始位。
        3.接收端检测到这一电平变化后,开始采样数据。
⭕数据位(Data Bits):
        1.数据位用于实际传输信息。
        2.通常为5、6、7、8或9位(大多数情况下为8位)。
        3.数据位是从最低有效位(LSB,Least Significant Bit)到最高有效位(MSB,Most            Significant Bit)顺序发送的。
⭕奇偶校验位(Parity Bit,可选):
        1.奇偶校验位用于错误检测。
        2.校验方式有三种:

⭕停止位(Stop Bit):
        1.停止位标志着数据帧的结束。
        2.停止位是一个或多个高电平(逻辑1),常见的长度为1位、1.5位或2位。
        3.接收端通过检测停止位来确认数据帧的结束,如果检测不到停止位,通常会认为传输出错。⭕空闲位(Idle Bit):
        1.在数据帧之间,通信线保持高电平(逻辑1),直到下一次传输开始。空闲状态并不计入数据帧,但保持该状态有助于同步下一次通信。

4.3.2 物理层
4.3.2.1 电气连接

UART至少需要两根数据线,即发送线(TX)和接收线(RX)。TX用于发送数据,RX用于接收数据。

4.3.2.2 接口标准

UART本身不涉及电气信号的电压标准,它只定义了数据如何被发送和接收。这就意味着,UART可以在不同的电气接口标准上工作,依赖于具体的物理传输层(如TTL、RS232等)进行数据传输。

5. 基于51单片机的MPU6050驱动流程及代码演示

MPU6050与单片机开发板接线如下,MPU6050只用到4个引脚:负责供电的VCC、GND引脚、负责与单片机间IIC通信的SCL和SDA引脚。

5.1 驱动流程

通过对多个关键寄存器的配置,初始化 MPU6050 传感器的工作状态,包括禁用低功耗模式设置采样率启用数字低通滤波配置陀螺仪和加速度计的全量程范围。这些操作确保传感器在理想的配置下开始正常工作,并为后续的数据读取和处理提供了基础。

配置电源管理 1 寄存器、电源管理 2 寄存器禁用低功耗模式
配置采样率分频寄存器设置采样率为125Hz
配置配置寄存器启用数字低通滤波—94Hz 低通滤波带宽和 2ms 延迟
配置陀螺仪配置寄存器设置陀螺仪全量程范围为 ±500度/秒(dps)
配置加速度计配置寄存器设置加速度计全量程范围为 ±4g

5.2 代码梳理
5.2.1 宏定义及枚举类型定义

该部分包含:
⭕宏      定     义:器件寄存器地址、寄存器值、简单数据操作定义及引脚定义。
⭕枚举类型定义:主要用于方便设置和控制 MPU6050 传感器的不同配置参数,例如低通滤波器延迟、陀螺仪和加速度计的量程范围、唤醒频率等,也包括一些常规的状态标志和硬件状态。

//****************************宏定义*******************************
// 定义51单片机端口
//****************************************
sbit    SCL=P2^1;			// 定义I2C时钟引脚,连接P2.1
sbit    SDA=P2^0;			// 定义I2C数据引脚,连接P2.0
//****************************************
// 定义MPU6050内部地址
//****************************************
#define	SlaveAddress	                  0xD0	// MPU6050 I2C写入时的地址字节,读取时地址为0xD1
#define MPU6050_REG_SELF_TEST_X         0x0D // RW, X轴自检寄存器
#define MPU6050_REG_SELF_TEST_Y         0x0E // RW, Y轴自检寄存器
#define MPU6050_REG_SELF_TEST_Z         0x0F // RW, Z轴自检寄存器
#define MPU6050_REG_SELF_TEST_A         0x10 // RW, 加速度计自检寄存器
#define MPU6050_REG_SMPLRT_DIV          0x19 // RW, 采样率分频寄存器
#define MPU6050_REG_CONFIG              0x1A // RW, 配置寄存器
#define MPU6050_REG_GYRO_CONFIG         0x1B // RW, 陀螺仪配置寄存器
#define MPU6050_REG_ACCEL_CONFIG        0x1C // RW, 加速度计配置寄存器
#define MPU6050_REG_FIFO_EN             0x23 // RW, FIFO缓冲区使能寄存器
#define MPU6050_REG_I2C_MST_CTRL        0x24 // RW, I2C主机控制寄存器
#define MPU6050_REG_I2C_SLV0_ADDR       0x25 // RW, I2C从机0地址寄存器
#define MPU6050_REG_I2C_SLV0_REG        0x26 // RW, I2C从机0寄存器地址
#define MPU6050_REG_I2C_SLV0_CTRL       0x27 // RW, I2C从机0控制寄存器
#define MPU6050_REG_I2C_SLV1_ADDR       0x28 // RW, I2C从机1地址寄存器
#define MPU6050_REG_I2C_SLV1_REG        0x29 // RW, I2C从机1寄存器地址
#define MPU6050_REG_I2C_SLV1_CTRL       0x2A // RW, I2C从机1控制寄存器
#define MPU6050_REG_I2C_SLV2_ADDR       0x2B // RW, I2C从机2地址寄存器
#define MPU6050_REG_I2C_SLV2_REG        0x2C // RW, I2C从机2寄存器地址
#define MPU6050_REG_I2C_SLV2_CTRL       0x2D // RW, I2C从机2控制寄存器
#define MPU6050_REG_I2C_SLV3_ADDR       0x2E // RW, I2C从机3地址寄存器
#define MPU6050_REG_I2C_SLV3_REG        0x2F // RW, I2C从机3寄存器地址
#define MPU6050_REG_I2C_SLV3_CTRL       0x30 // RW, I2C从机3控制寄存器
#define MPU6050_REG_I2C_SLV4_ADDR       0x31 // RW, I2C从机4地址寄存器
#define MPU6050_REG_I2C_SLV4_REG        0x32 // RW, I2C从机4寄存器地址
#define MPU6050_REG_I2C_SLV4_DO         0x33 // RW, I2C从机4数据寄存器
#define MPU6050_REG_I2C_SLV4_CTRL       0x34 // RW, I2C从机4控制寄存器
#define MPU6050_REG_I2C_SLV4_DI         0x35 // R , I2C从机4输入数据寄存器
#define MPU6050_REG_I2C_MST_STATUS      0x36 // R , I2C主机状态寄存器
#define MPU6050_REG_INT_PIN_CFG         0x37 // RW, 中断引脚配置寄存器
#define MPU6050_REG_INT_ENABLE          0x38 // RW, 中断使能寄存器
#define MPU6050_REG_INT_STATUS          0x3A // R , 中断状态寄存器
#define MPU6050_REG_ACCEL_XOUT_H        0x3B // R , X轴加速度高字节
#define MPU6050_REG_ACCEL_XOUT_L        0x3C // R , X轴加速度低字节
#define MPU6050_REG_ACCEL_YOUT_H        0x3D // R , Y轴加速度高字节
#define MPU6050_REG_ACCEL_YOUT_L        0x3E // R , Y轴加速度低字节
#define MPU6050_REG_ACCEL_ZOUT_H        0x3F // R , Z轴加速度高字节
#define MPU6050_REG_ACCEL_ZOUT_L        0x40 // R , Z轴加速度低字节
#define MPU6050_REG_TEMP_OUT_H          0x41 // R , 温度高字节
#define MPU6050_REG_TEMP_OUT_L          0x42 // R , 温度低字节
#define MPU6050_REG_GYRO_XOUT_H         0x43 // R , X轴陀螺仪高字节
#define MPU6050_REG_GYRO_XOUT_L         0x44 // R , X轴陀螺仪低字节
#define MPU6050_REG_GYRO_YOUT_H         0x45 // R , Y轴陀螺仪高字节
#define MPU6050_REG_GYRO_YOUT_L         0x46 // R , Y轴陀螺仪低字节
#define MPU6050_REG_GYRO_ZOUT_H         0x47 // R , Z轴陀螺仪高字节
#define MPU6050_REG_GYRO_ZOUT_L         0x48 // R , Z轴陀螺仪低字节
#define MPU6050_REG_EXT_SENS_DATA_00    0x49 // R , 外部传感器数据字节0
#define MPU6050_REG_EXT_SENS_DATA_01    0x4A // R , 外部传感器数据字节1
#define MPU6050_REG_EXT_SENS_DATA_02    0x4B // R , 外部传感器数据字节2
#define MPU6050_REG_EXT_SENS_DATA_03    0x4C // R , 外部传感器数据字节3
#define MPU6050_REG_EXT_SENS_DATA_04    0x4D // R , 外部传感器数据字节4
#define MPU6050_REG_EXT_SENS_DATA_05    0x4E // R , 外部传感器数据字节5
#define MPU6050_REG_EXT_SENS_DATA_06    0x4F // R , 外部传感器数据字节6
#define MPU6050_REG_EXT_SENS_DATA_07    0x50 // R , 外部传感器数据字节7
#define MPU6050_REG_EXT_SENS_DATA_08    0x51 // R , 外部传感器数据字节8
#define MPU6050_REG_EXT_SENS_DATA_09    0x52 // R , 外部传感器数据字节9
#define MPU6050_REG_EXT_SENS_DATA_10    0x53 // R , 外部传感器数据字节10
#define MPU6050_REG_EXT_SENS_DATA_11    0x54 // R , 外部传感器数据字节11
#define MPU6050_REG_EXT_SENS_DATA_12    0x55 // R , 外部传感器数据字节12
#define MPU6050_REG_EXT_SENS_DATA_13    0x56 // R , 外部传感器数据字节13
#define MPU6050_REG_EXT_SENS_DATA_14    0x57 // R , 外部传感器数据字节14
#define MPU6050_REG_EXT_SENS_DATA_15    0x58 // R , 外部传感器数据字节15
#define MPU6050_REG_EXT_SENS_DATA_16    0x59 // R , 外部传感器数据字节16
#define MPU6050_REG_EXT_SENS_DATA_17    0x5A // R , 外部传感器数据字节17
#define MPU6050_REG_EXT_SENS_DATA_18    0x5B // R , 外部传感器数据字节18
#define MPU6050_REG_EXT_SENS_DATA_19    0x5C // R , 外部传感器数据字节19
#define MPU6050_REG_EXT_SENS_DATA_20    0x5D // R , 外部传感器数据字节20
#define MPU6050_REG_EXT_SENS_DATA_21    0x5E // R , 外部传感器数据字节21
#define MPU6050_REG_EXT_SENS_DATA_22    0x5F // R , 外部传感器数据字节22
#define MPU6050_REG_EXT_SENS_DATA_23    0x60 // R , 外部传感器数据字节23
#define MPU6050_REG_I2C_SLV0_DO         0x63 // RW, I2C从机0数据寄存器
#define MPU6050_REG_I2C_SLV1_DO         0x64 // RW, I2C从机1数据寄存器
#define MPU6050_REG_I2C_SLV2_DO         0x65 // RW, I2C从机2数据寄存器
#define MPU6050_REG_I2C_SLV3_DO         0x66 // RW, I2C从机3数据寄存器
#define MPU6050_REG_I2C_MST_DELAY_CTRL  0x67 // RW, I2C主机延时控制寄存器
#define MPU6050_REG_SIGNAL_PATH_RESET   0x68 // RW, 信号路径复位寄存器
#define MPU6050_REG_MOT_DETECT_CTRL     0x69 // RW, 运动检测控制寄存器
#define MPU6050_REG_USER_CTRL           0x6A // RW, 用户控制寄存器
#define MPU6050_REG_PWR_MGMT_1          0x6B // RW, 电源管理寄存器1
#define MPU6050_REG_PWR_MGMT_2          0x6C // RW, 电源管理寄存器2
#define MPU6050_REG_FIFO_COUNTH         0x72 // R , FIFO计数高字节
#define MPU6050_REG_FIFO_COUNTL         0x73 // R , FIFO计数低字节
#define MPU6050_REG_FIFO_R_W            0x74 // RW, FIFO读写寄存器
#define MPU6050_REG_WHO_AM_I            0x75 // R , 芯片标识寄存器 (默认值0x68)
//定义数据操作功能
//****************************************
// 宏定义用于将指定的位(__BIT__)设置为RESET值(通常为0)
#define SBIT_RESET(__BIT__)                 ((__BIT__) = RESET)
// 宏定义用于将数据写入到串口1的发送缓冲寄存器SBUF中
// __DATA__ 是要发送的数据
#define UART1_WriteBuffer(__DATA__)         (SBUF = (__DATA__))
// 宏定义用于清除串口1的发送中断标志位TI
// TI(发送中断标志位)为1时表示数据已经发送完成,清零后可以准备发送下一字节
#define UART1_ClearTxInterrupt()            SBIT_RESET(TI)
// 宏定义用于清除串口1的接收中断标志位RI
// RI(接收中断标志位)为1时表示数据已经接收到,清零后可以准备接收下一字节
#define UART1_ClearRxInterrupt()            SBIT_RESET(RI)

//****************************枚举类型定义*******************************
// 定义FlagStatus枚举,用于表示一个标志的状态,可以是RESET或SET
typedef enum
{
    RESET = 0x0,    // RESET表示复位状态,数值为0
    SET = !RESET    // SET表示设置状态,数值为非0,这里为1
} FlagStatus;

// 定义MPU6050_DLPF_t枚举,用于设置MPU6050的低通滤波器(DLPF)延迟
typedef enum
{
    MPU6050_DLPF_Delay0ms  = 0x00,   // DLPF延迟为0ms
    MPU6050_DLPF_Delay2ms  = 0x01,   // DLPF延迟为2ms
    MPU6050_DLPF_Delay3ms  = 0x02,   // DLPF延迟为3ms
    MPU6050_DLPF_Delay5ms  = 0x03,   // DLPF延迟为5ms
    MPU6050_DLPF_Delay8ms  = 0x04,   // DLPF延迟为8ms
    MPU6050_DLPF_Delay13ms = 0x05,   // DLPF延迟为13ms
    MPU6050_DLPF_Delay19ms = 0x06    // DLPF延迟为19ms
} MPU6050_DLPF_t;

// 定义MPU6050_Gyro_FullScaleRange_t枚举,用于设置MPU6050陀螺仪的满量程范围
typedef enum
{
    MPU6050_Gyro_FullScaleRange_250dps  = 0x00,   // 陀螺仪满量程为±250度/秒
    MPU6050_Gyro_FullScaleRange_500dps  = 0x01,   // 陀螺仪满量程为±500度/秒
    MPU6050_Gyro_FullScaleRange_1000dps = 0x02,   // 陀螺仪满量程为±1000度/秒
    MPU6050_Gyro_FullScaleRange_2000dps = 0x03    // 陀螺仪满量程为±2000度/秒
} MPU6050_Gyro_FullScaleRange_t;

// 定义MPU6050_Acc_FullScaleRange_t枚举,用于设置MPU6050加速度计的满量程范围
typedef enum
{
    MPU6050_Acc_FullScaleRange_2g  = 0x00,   // 加速度计满量程为±2g
    MPU6050_Acc_FullScaleRange_4g  = 0x01,   // 加速度计满量程为±4g
    MPU6050_Acc_FullScaleRange_8g  = 0x02,   // 加速度计满量程为±8g
    MPU6050_Acc_FullScaleRange_16g = 0x03    // 加速度计满量程为±16g
} MPU6050_Acc_FullScaleRange_t;

// 定义HAL_State_t枚举,用于表示硬件抽象层(HAL)的状态
typedef enum
{
    HAL_State_OFF = 0x00,  // HAL处于关闭状态
    HAL_State_ON  = 0x01   // HAL处于开启状态
} HAL_State_t;

// 定义MPU6050_Wakeup_Freq_t枚举,用于设置MPU6050在低功耗模式下的唤醒频率
typedef enum
{
    MPU6050_Wakeup_Freq_1p25Hz = 0x00,   // 唤醒频率为1.25Hz
    MPU6050_Wakeup_Freq_5Hz    = 0x01,   // 唤醒频率为5Hz
    MPU6050_Wakeup_Freq_20Hz   = 0x02,   // 唤醒频率为20Hz
    MPU6050_Wakeup_Freq_40Hz   = 0x03    // 唤醒频率为40Hz
} MPU6050_Wakeup_Freq_t;
5.2.2 延时功能代码

在模拟 I2C(IIC)协议通讯时,延时通常是必要的,尤其是在每个数据位发送后以及在发送完整字节后。这是因为 I2C 总线是一个串行通信协议,其速度相对较慢(通常在几百kHz的范围内),并且要求设备在数据线上按时发送和接收数据,延时时间5μs即可。

延时的作用包括:
1.稳定时序: 延时确保了设备能够在数据线上正确地检测到每个数据位的变化和传输。
2.协议要求: 根据 I2C 协议规范,设备之间需要在特定的时间窗口内响应和处理数据,延时帮助确保设备在合适的时间发送和接收数据。

本案例使用STC89C52RC控制器,晶振选用11.0592MHz。通过ISP自带的软件延时计算工具编写延时5μs的函数,如下:

//延时5微秒(STC90C52RC@11.0592M)
//当改用1T的MCU时,请调整此延时函数
//**************************************
void Delay5us()
{

}
//延时X毫秒(STC90C52RC@11.0592M)
//当改用1T的MCU时,请调整此延时函数
//**************************************
void DelayXms(unsigned int xms)		//@11.0592MHz
{
	while(xms--)
	{
		unsigned char i, j;
		i = 2;
		j = 199;
		do
		{
			while (--j);
		} while (--i);
	}
}

设定高电平5μs,低电平8ms。逻辑分析仪实测如下,上述两个函数实现软件延时较为准确。

5.2.3 软件IIC代码

通过I2C(Inter-Integrated Circuit)通信协议与外部设备进行数据的发送和接收,具体包括I2C的起始信号、停止信号、发送和接收应答信号、发送和接收数据等功能。

//I2C起始信号
//**************************************
void I2C_Start()
{
    SDA = 1;                    //拉高数据线
    SCL = 1;                    //拉高时钟线
    Delay5us();                 //延时
    SDA = 0;                    //产生下降沿
    Delay5us();                 //延时
    SCL = 0;                    //拉低时钟线
}
//**************************************
//I2C停止信号
//**************************************
void I2C_Stop()
{
    SDA = 0;                    //拉低数据线
    SCL = 1;                    //拉高时钟线
    Delay5us();                 //延时
    SDA = 1;                    //产生上升沿
    Delay5us();                 //延时
}
//**************************************
//I2C发送应答信号
//入口参数:ack (0:ACK 1:NAK)
//**************************************
void I2C_SendACK(bit ack)
{
    SDA = ack;                  //写应答信号
    SCL = 1;                    //拉高时钟线
    Delay5us();                 //延时
    SCL = 0;                    //拉低时钟线
    Delay5us();                 //延时
}
//**************************************
//I2C接收应答信号
//**************************************
bit I2C_RecvACK()
{
    SCL = 1;                    //拉高时钟线
    Delay5us();                 //延时
    CY = SDA;                   //读应答信号
    SCL = 0;                    //拉低时钟线
    Delay5us();                 //延时
    return CY;
}
//**************************************
//向I2C总线发送一个字节数据
//**************************************
void I2C_SendByte(uchar dat)
{
    uchar i;
    for (i=0; i<8; i++)         //8位计数器
    {
        dat <<= 1;              //移出数据的最高位
        SDA = CY;               //送数据口
        SCL = 1;                //拉高时钟线
        Delay5us();             //延时
        SCL = 0;                //拉低时钟线
        Delay5us();             //延时
    }
    I2C_RecvACK();
}
//**************************************
//从I2C总线接收一个字节数据
//**************************************
uchar I2C_RecvByte()
{
    uchar i;
    uchar dat = 0;
    SDA = 1;                    //使能内部上拉,准备读取数据,
    for (i=0; i<8; i++)         //8位计数器
    {
        dat <<= 1;
        SCL = 1;                //拉高时钟线
        Delay5us();             //延时
        dat |= SDA;             //读数据               
        SCL = 0;                //拉低时钟线
        Delay5us();             //延时
    }
    return dat;
}
//**************************************
//向I2C设备写入一个字节数据
//**************************************
void Single_WriteI2C(uchar REG_Address,uchar REG_data)
{
    I2C_Start();                  //起始信号
    I2C_SendByte(SlaveAddress);   //发送设备地址+写信号
    I2C_SendByte(REG_Address);    //内部寄存器地址,
    I2C_SendByte(REG_data);       //内部寄存器数据,
    I2C_Stop();                   //发送停止信号
}
//**************************************
//从I2C设备读取一个字节数据
//**************************************
uchar Single_ReadI2C(uchar REG_Address)
{
	uchar REG_data;
	I2C_Start();                   //起始信号
	I2C_SendByte(SlaveAddress);    //发送设备地址+写信号
	I2C_SendByte(REG_Address);     //发送存储单元地址,从0开始	
	I2C_Start();                   //起始信号
	I2C_SendByte(SlaveAddress+1);  //发送设备地址+读信号
	REG_data=I2C_RecvByte();       //读出寄存器数据
	I2C_SendACK(1);                //接收应答信号
	I2C_Stop();                    //停止信号
	return REG_data;
}
5.2.4 IIC驱动MPU6050验证(逻辑分析仪) 

⭕验证例程使用函数 CheckMPU6050() ,即通过IIC协议循环读取 WHO_AM_I 寄存器(0x75)数值,代码逻辑及代码如下;

void main()
{ 
	while(1)
	{
		CheckMPU6050();
		DelayXms(250);
		DelayXms(250);
		DelayXms(250);
		DelayXms(250);
	}
}

⭕将逻辑分析仪两通道分别连接本案例IIC引脚P2.0(SDA)、P2.1(SCL),GND与单片机GND引脚相连;
⭕对采样的两通道信号使用分析仪软件工具分析,如图所示。

根据上图逻辑分析仪采样数据可知,实际IIC两路信号与预期一致,且读出数值正确(0x68),所以本案例软件IIC驱动MPU6050通路确认导通。

5.2.5 串口初始化及通信代码

实现了串口通信的初始化和数据发送功能,主要包含以下几个部分:

  1. 串口初始化 (UART_Init)

    • 配置定时器以生成所需的波特率(9600bps)。
    • 设置串口工作模式为8位可变波特率,并允许接收。
    • 启用串口和定时器中断,以便在发送和接收过程中能够响应中断。
  2. 发送字节 (UART_SendByte)

    • 通过将字节写入串口数据寄存器 SBUF,实现单个字节的发送。
    • 通过监测发送中断标志(TI)来确保发送完成。
  3. 发送字符串 (UART_SendString)

    • 循环逐个字符地调用 UART_SendByte 函数,以发送字符串。
  4. 发送字符(串口1) (UART1_TxChar)

    • 使用 UART1_WriteBuffer 宏将字符写入 SBUF,并确保发送完成。
    • 清除发送中断标志以准备下一个字符的发送。
  5. 发送字符串(串口1) (UART1_TxString)

    • 类似于 UART_SendString,通过循环调用 UART1_TxChar 函数发送字符串。
// 初始化串口通信,配置定时器和中断
void UART_Init()
{
    TMOD = 0x21;     // 设置定时器模式,T0为模式1(16位定时器),T1为模式2(8位自动重载)
    TH1 = 0xfd;      // 设置定时器1初值(波特率9600bps, 晶振11.0592MHz)
    TL1 = 0xfd;      // 设置定时器1初值
    SCON = 0x50;     // 设置串口工作方式1(8位可变波特率),允许接收
    PS = 1;          // 设置串口中断为高优先级
    TR0 = 1;         // 启动定时器0
    TR1 = 1;         // 启动定时器1(用于串口波特率发生器)
    ET0 = 1;         // 打开定时器0中断
    ES = 1;          // 打开串口中断
    EA = 1;          // 打开总中断
}

// 发送一个字节的数据
void UART_SendByte(uchar byte) {
    SBUF = byte;     // 将数据字节写入串口数据寄存器SBUF,开始发送
    while (!TI);     // 等待发送完成(TI标志位置1表示发送完成)
    TI = 0;          // 清除TI标志位,为下次发送做准备
}
// 发送一个字符串
void UART_SendString(char *str) {
    while (*str) {            // 当字符串未结束时,循环发送每个字符
        UART_SendByte(*str++); // 调用UART_SendByte函数发送字符并指向下一个字符
    }
}
// 通过串口1发送单个字符
void UART1_TxChar(char dat)
{
    UART1_WriteBuffer(dat);   // 调用宏,写入数据到SBUF寄存器
    while(!TI);               // 等待发送完成(TI标志位置1表示发送完成)
    UART1_ClearTxInterrupt(); // 清除TI标志位
}
// 通过串口1发送字符串
void UART1_TxString(unsigned char *str)
{
    while (*str)              // 当字符串未结束时,循环发送每个字符
        UART1_TxChar(*str++); // 调用UART1_TxChar函数发送字符并指向下一个字符
}
5.2.6 MPU6050初始化代码

*****************************************************************************

  • MPU6050_DisableLowPowerMode();

    • 调用 MPU6050_Write(MPU6050_REG_PWR_MGMT_1, 0x00); 将电源管理寄存器 PWR_MGMT_1 置为 0x00,禁用低功耗模式并从休眠模式中恢复。
    • 寄存器操作: PWR_MGMT_1 (0x6B),通过设置寄存器为 0x00,启动设备,清除睡眠位和循环位。
    • 作用: 确保 MPU6050 正常工作,不处于低功耗模式。

*****************************************************************************

  • MPU6050_SetSampleRateDiv(0x07);

    • 设置采样率分频寄存器 SMPLRT_DIV 为 0x07。采样率公式为:采样率 = 陀螺仪输出速率 / (1 + SMPLRT_DIV),其中陀螺仪的输出速率为 1kHz(低通滤波器启用时)。
    • 寄存器操作: SMPLRT_DIV (0x19),通过将寄存器设为 0x07,采样率会被设为 1000Hz / (1 + 7) = 125Hz
    • 作用: 将采样频率设置为 125Hz,使得每秒进行 125 次数据采集。

*****************************************************************************

  • MPU6050_SetDLPF(MPU6050_DLPF_Delay2ms);

    • 设置数字低通滤波器(DLPF),此处选择延迟 2ms 的配置。该函数将滤波器配置寄存器 CONFIG 设置为 2ms 延迟对应的值。
    • 寄存器操作: CONFIG (0x1A),低通滤波器配置寄存器被设置为 0x02,对应 94Hz 低通滤波带宽和 2ms 延迟。
    • 作用: 通过启用 DLPF,限制高频噪声影响,并设置合适的延迟。

*****************************************************************************

  • MPU6050_SetGyroFullScaleRange(MPU6050_Gyro_FullScaleRange_500dps);

    • 设置陀螺仪全量程范围为 ±500度/秒(dps)。该函数将陀螺仪配置寄存器 GYRO_CONFIG 设置为对应的量程。
    • 寄存器操作: GYRO_CONFIG (0x1B),设置寄存器高三位为 0x01(向左移 3 位)表示 ±500dps。
    • 作用: 限制陀螺仪的输出范围为 ±500度/秒,以更好地适应高转速的应用场景。

*****************************************************************************

  • MPU6050_SetAccFullScaleRange(MPU6050_Acc_FullScaleRange_4g);

    • 设置加速度计全量程范围为 ±4g。该函数将加速度计配置寄存器 ACCEL_CONFIG 设置为对应的量程。
    • 寄存器操作: ACCEL_CONFIG (0x1C),设置寄存器高三位为 0x01(向左移 3 位)表示 ±4g。
    • 作用: 限制加速度计的输出范围为 ±4g,以适应较大的加速度检测场景。

*****************************************************************************

// 重置 MPU6050 传感器
void MPU6050_Reset(void) {
    Single_WriteI2C(MPU6050_REG_PWR_MGMT_1, 0x80); // 向电源管理寄存器写入0x80以重置传感器
}
// 使 MPU6050 进入睡眠模式
void MPU6050_EnterSleepMode(void) {
    Single_WriteI2C(MPU6050_REG_PWR_MGMT_1, 0x40); // 向电源管理寄存器写入0x40以进入睡眠模式
}
// 禁用温度传感器
void MPU6050_DisableTemperature(HAL_State_t state) {
    uchar reg = Single_ReadI2C(MPU6050_REG_PWR_MGMT_1); // 读取当前电源管理寄存器的值
    Single_WriteI2C(MPU6050_REG_PWR_MGMT_1, reg & ~0x08 | (state << 3)); // 根据状态禁用或启用温度传感器
}
// 启用低功耗模式
void MPU6050_EnableLowPowerMode(MPU6050_Wakeup_Freq_t freq) {
    Single_WriteI2C(MPU6050_REG_PWR_MGMT_1, 0x28); // 向电源管理寄存器写入0x28以启用低功耗模式
    Single_WriteI2C(MPU6050_REG_PWR_MGMT_2, freq << 6 | 0x03); // 设置唤醒频率并使能陀螺仪的待机模式
}
// 禁用低功耗模式
void MPU6050_DisableLowPowerMode(void) {
    Single_WriteI2C(MPU6050_REG_PWR_MGMT_1, 0x00); // 向电源管理寄存器写入0x00以禁用所有低功耗设置
    Single_WriteI2C(MPU6050_REG_PWR_MGMT_2, 0x00); // 向电源管理寄存器写入0x00以禁用所有待机模式
}
// 设置采样率分频器
void MPU6050_SetSampleRateDiv(uchar div) {
    Single_WriteI2C(MPU6050_REG_SMPLRT_DIV, div); // 向采样率分频器寄存器写入分频值
}
// 设置数字低通滤波器
void MPU6050_SetDLPF(MPU6050_DLPF_t filter) {
    Single_WriteI2C(MPU6050_REG_CONFIG, filter); // 向配置寄存器写入滤波器设置
}
// 设置陀螺仪满量程范围
void MPU6050_SetGyroFullScaleRange(MPU6050_Gyro_FullScaleRange_t range) {
    Single_WriteI2C(MPU6050_REG_GYRO_CONFIG, range << 3); // 向陀螺仪配置寄存器写入满量程范围
}
// 设置加速度计满量程范围
void MPU6050_SetAccFullScaleRange(MPU6050_Acc_FullScaleRange_t range) {
    Single_WriteI2C(MPU6050_REG_ACCEL_CONFIG, range << 3); // 向加速度计配置寄存器写入满量程范围
}
// 检查 MPU6050 是否存在
bit CheckMPU6050() {
    uchar response;
    response = Single_ReadI2C(MPU6050_REG_WHO_AM_I); // 读取 MPU6050 的身份寄存器
    if (response == 0x68) { // 检查返回值是否为0x68(MPU6050 的 ID)
        return 1;  // 如果匹配,返回真
    }
    return 0;      // 否则返回假
}
// 初始化 MPU6050
void MPU6050_Init(void) {
    MPU6050_DisableLowPowerMode(); // 禁用低功耗模式以开始正常操作
    MPU6050_SetSampleRateDiv(0x07); // 设置采样率分频器为7
    MPU6050_SetDLPF(MPU6050_DLPF_Delay2ms); // 设置低通滤波器延迟为2ms
    MPU6050_SetGyroFullScaleRange(MPU6050_Gyro_FullScaleRange_500dps); // 设置陀螺仪满量程范围为±500度/秒
    MPU6050_SetAccFullScaleRange(MPU6050_Acc_FullScaleRange_4g); // 设置加速度计满量程范围为±4g
}
5.2.7 数据整合&处理代码

数据需要整合和处理的原因包括:
⭕加速度和角速度数据在寄存器中以大小端方式,占据多字节存储,此处涉及数据整合和进制转换;
⭕因为MPU6050输出数据为原始LSB数据,如果想要得到表征实际物理意义的加速度或者角速度数据,需要根据灵敏度进行对应转换。

// 从指定寄存器地址获取陀螺仪数据
int GetGyroData(uchar REG_Address) {
    uchar H, L; // 定义高字节和低字节变量
    H = Single_ReadI2C(REG_Address); // 读取寄存器地址的高字节
    L = Single_ReadI2C(REG_Address + 1); // 读取寄存器地址 + 1 的低字节
    return (H << 8) + L; // 将高字节左移8位并与低字节相加,返回合成的完整数据
}

// 将整数转换为字符串
void IntToString(int num, char *str) {
    sprintf(str, "%d", num); // 使用 sprintf 将整数格式化为字符串
}

// 将浮点数转换为保留两位小数的字符串
void FloatToString(float num, char *str) {
    sprintf(str, "%.2f", num);  // 将浮点数转换为保留两位小数的字符串
}

gyro_x_raw = GetGyroData(MPU6050_REG_GYRO_XOUT_H);  // 获取陀螺仪 X 轴的原始数据
gyro_y_raw = GetGyroData(MPU6050_REG_GYRO_YOUT_H);  // 获取陀螺仪 Y 轴的原始数据
gyro_z_raw = GetGyroData(MPU6050_REG_GYRO_ZOUT_H);  // 获取陀螺仪 Z 轴的原始数据

gyro_x = gyro_x_raw / 131.0;  // 将 X 轴的原始数据转换为角速度值
gyro_y = gyro_y_raw / 131.0;  // 将 Y 轴的原始数据转换为角速度值
gyro_z = gyro_z_raw / 131.0;  // 将 Z 轴的原始数据转换为角速度值
5.3 串口打印
5.3.1 串口助手打印

该部分的串口打印逻辑如下:

if (CheckMPU6050()) {
        UART_SendString("MPU6050 communication successful!\r\n");
        while (1) {	  
            int gyro_x_raw, gyro_y_raw, gyro_z_raw;
			float gyro_x,gyro_y,gyro_z;
            // 获取陀螺仪原始数据
            gyro_x_raw = GetGyroData(MPU6050_REG_GYRO_XOUT_H);
            gyro_y_raw = GetGyroData(MPU6050_REG_GYRO_YOUT_H);
            gyro_z_raw = GetGyroData(MPU6050_REG_GYRO_ZOUT_H);
			// LSB原始数据—>dps
            gyro_x = gyro_x_raw / 131.0;
			gyro_y = gyro_y_raw / 131.0;
			gyro_z = gyro_z_raw / 131.0;


			UART_SendString("Raw X: ");
            FloatToString(gyro_x, str);
			UART_SendString(str);
			UART_SendString("\r\n");

            UART_SendString("Raw Y: ");
            FloatToString(gyro_y, str);
			UART_SendString(str);
			UART_SendString("\r\n");

           UART_SendString("Raw Z: ");
           FloatToString(gyro_z, str);
		   UART_SendString(str);
		   UART_SendString("\r\n");		 
        }
    } else {
        UART_SendString("MPU6050 communication failed!\r\n");
    }

使用STC-ISP软件中串口助手对串口内容进行输出,如下所示:

5.3.2 上位机编写

针对编辑好的单片机串口模块代码,我们通过自己编写上位机,可以达到串口助手相同效果的打印功能,更重要的是,对于打印内容的后处理自由度更高,比如针对本文:陀螺仪输出的raw data(LSB)转换成以°/s为单位的角速度这一过程,既可以放在串口模块的代码中实现,也可以串口直接打印原始数据,通过我们上位机对输出内容进行后处理完成。

我们的上位机使用Matlab编写,计划实现3个功能:

  1. 原始串口内容输出;
  2. 实时二维显示陀螺仪三轴角速度值;
  3. 实时三维显示陀螺仪三轴角度变化;
5.3.2.1 串口打印格式说明

为方便上位机对接收到的串口数据后处理,串口打印格式调整如下:
x轴角速度+空格+y轴角速度+空格z轴角速度(单位°/s)
-1.14 -0.89 -0.28
-1.24 -0.98 -0.31
-1.17 -0.92 -0.26
-1.15 -0.90 -0.24
-1.18 -0.93 -0.30
-1.11 -0.94 -0.29

该部分串口打印代码逻辑如下:

if (CheckMPU6050()) {
        UART_SendString("MPU6050 communication successful!\r\n");
        while (1) {	  
            int gyro_x_raw, gyro_y_raw, gyro_z_raw;
			float gyro_x,gyro_y,gyro_z;
            // 获取陀螺仪原始数据
            gyro_x_raw = GetGyroData(MPU6050_REG_GYRO_XOUT_H);
            gyro_y_raw = GetGyroData(MPU6050_REG_GYRO_YOUT_H);
            gyro_z_raw = GetGyroData(MPU6050_REG_GYRO_ZOUT_H);
			// LSB原始数据—>dps
            gyro_x = gyro_x_raw / 131.0;
			gyro_y = gyro_y_raw / 131.0;
			gyro_z = gyro_z_raw / 131.0;


	        FloatToString(gyro_x, str);
            UART_SendString(str);
            UART_SendString(" ");

            
            FloatToString(gyro_y, str);
            UART_SendString(str);
            UART_SendString(" ");

            
            FloatToString(gyro_z, str);
            UART_SendString(str);
            UART_SendString("\r\n");	 
        }
    } else {
        UART_SendString("MPU6050 communication failed!\r\n");
    }
5.3.2.2 原始串口内容输出&二维实时显示角速度

上位机代码1使用Matlab编写,可以实现上述5.3.21、2点功能,主要模块包含:

  1. 串口设置与配置
  2. 绘图初始化
  3. 原始串口输出窗口
  4. 实时读取串口数据
% 设置串口参数
serialPort = 'COM11'; % 替换为你的串口号
baudRate = 9600;      % 波特率,替换为你的实际波特率
dataBits = 8;         % 数据位
stopBits = 1;         % 停止位
parity = 'none';      % 校验位(可选:'none', 'odd', 'even')
s = serialport(serialPort, baudRate);
% 配置串口参数
configureTerminator(s, 'LF'); % 设置终止符为换行符(根据需要调整)
s.DataBits = dataBits;         % 设置数据位
s.StopBits = stopBits;         % 设置停止位
s.Parity = parity;             % 设置校验位
% 初始化绘图
figure;
t = tiledlayout(3, 1, 'TileSpacing', 'Compact');
title(t, '三轴数据实时显示');
% 设置子图窗口
ax1 = nexttile;
xData = animatedline('Parent', ax1);
title('X轴数据');
ylabel('X值 (°/s)'); % 添加单位
xlabel('时间');
grid on;
ax2 = nexttile;
yData = animatedline('Parent', ax2);
title('Y轴数据');
ylabel('Y值 (°/s)'); % 添加单位
xlabel('时间');
grid on;
ax3 = nexttile;
zData = animatedline('Parent', ax3);
title('Z轴数据');
ylabel('Z值 (°/s)'); % 添加单位
xlabel('时间');
grid on;
% 设定时间轴
time = 0;
timeStep = 0.1; % 设置时间步长(单位秒)
% 创建新窗口用于显示原始串口输出
rawDataFig = figure('Name', '串口原始输出', 'NumberTitle', 'off');
rawDataText = uicontrol('Style', 'text', 'Position', [10, 10, 400, 400], ...
    'FontSize', 10, 'HorizontalAlignment', 'left', 'Max', 10, 'Min', 0);
% 用于保存所有串口原始数据
rawDataBuffer = strings(100, 1); % 初始设置100行空字符串,用于存储数据
bufferIndex = 1; % 数据存储的当前行位置
% 实时读取串口并绘制图像
while true
    if s.NumBytesAvailable > 0
        % 读取一行串口数据
        dataLine = readline(s);
        % 将数据分割为三个轴数据
        data = str2num(dataLine); %#ok<ST2NM> % 将字符串转换为数值数组        
        if length(data) == 3
            % 更新各轴的数据
            time = time + timeStep;
            addpoints(xData, time, data(1)); % 更新X轴数据
            addpoints(yData, time, data(2)); % 更新Y轴数据
            addpoints(zData, time, data(3)); % 更新Z轴数据
            % 更新绘图
            drawnow;
        end
        % 将原始数据添加到缓存中
        rawDataBuffer(bufferIndex) = dataLine;
        bufferIndex = bufferIndex + 1;
        % 如果缓冲区已满,将数据向上移动,移除最早的数据
        if bufferIndex > length(rawDataBuffer)
            rawDataBuffer = [rawDataBuffer(2:end); ""]; % 移除最早的一行
            bufferIndex = length(rawDataBuffer);
        end
        % 更新原始串口数据的显示
        set(rawDataText, 'String', strjoin(rawDataBuffer, newline)); % 将所有数据拼接成字符串显示
        drawnow;
    end
end
原串口内容输出&实时二维显示角速度
5.3.2.3 实时三维显示陀螺仪三轴角度变化

上位机代码2使用Matlab编写,可以实现上述5.3.2中第3点功能,主要模块包含:

  1. 串口初始化
  2. 立方体的3D绘制
  3. 绘图配置
  4. 数据读取
  5. 旋转更新
  6. 旋转矩阵转换
function shangweiji3()
    % 设置串口参数
    serialPort = 'COM11'; % 替换为你的串口号
    baudRate = 9600;      % 波特率,替换为你的实际波特率
    s = serialport(serialPort, baudRate, "Timeout", 1); % 添加超时参数以防止读取错误
    % 初始化绘图
    figure;
    hold on;
    axis equal;
    grid on;

    % 正方体的8个顶点
    vertices = [
        -1 -1 -1;
         1 -1 -1;
         1  1 -1;
        -1  1 -1;
        -1 -1  1;
         1 -1  1;
         1  1  1;
        -1  1  1
    ];
    % 每个面对应的顶点索引
    faces = [
        1 2 3 4; % 侧面1
        5 6 7 8; % 侧面2
        1 2 6 5; % 底面
        2 3 7 6; % 右面
        3 4 8 7; % 顶面
        4 1 5 8  % 左面
    ];
    % 为每个面定义不同的颜色
    faceColors = [
        1 0 0;  % 红色
        0 1 0;  % 绿色
        0 0 1;  % 蓝色
        1 1 0;  % 黄色
        1 0 1;  % 品红
        0 1 1   % 青色
    ];
    % 绘制正方体
    hPatches = gobjects(size(faces, 1), 1); % 用于保存每个面对象
    for i = 1:size(faces, 1)
        hPatches(i) = patch('Vertices', vertices, 'Faces', faces(i,:), ...
                             'FaceColor', faceColors(i,:), 'FaceLighting', 'g', ...
                             'EdgeColor', 'k');
    end
    % 设置坐标轴
    xlim([-2 2]);
    ylim([-2 2]);
    zlim([-2 2]);
    xlabel('X轴');
    ylabel('Y轴');
    zlabel('Z轴');
    view(3); % 3D视图
    camlight; % 添加光源
    % 主循环读取串口数据
    while true
        if s.NumBytesAvailable > 0
            dataLine = readline(s);
            data = str2num(dataLine); %#ok<ST2NM>
            
            if length(data) == 3
                % MPU6050数据处理
                gx = data(1);
                gy = data(2);
                gz = data(3);
                
                % 调用更新正方体的函数
                updateCube(gx, gy, gz, hPatches, vertices);
            end
        end
    end
end

function updateCube(gx, gy, gz, hPatches, vertices)
    % 使用持久变量存储角度
    persistent pitch roll yaw;
    if isempty(pitch) || isempty(roll) || isempty(yaw)
        pitch = 0; roll = 0; yaw = 0;
    end   
    % 使用小时间步长更新欧拉角
    dt = 0.1; % 时间步长,建议根据实际情况调整
    sensitivity = 0.01; % 比例系数,调整这个值来控制运动幅度
    pitch = pitch + sensitivity * gy * dt; % Y轴旋转
    roll = roll + sensitivity * gx * dt;    % X轴旋转
    yaw = yaw + sensitivity * gz * dt;      % Z轴旋转    
    % 将欧拉角转换为旋转矩阵
    R = eul2rotm([yaw, pitch, roll]);    
    % 更新正方体的位置
    % 计算旋转后的顶点
    rotatedVertices = (R * vertices')';    
    % 更新每个面的顶点
    for i = 1:length(hPatches)
        set(hPatches(i), 'Vertices', rotatedVertices);
    end    
    drawnow; % 更新绘图
end
function R = eul2rotm(eul)
    % 将欧拉角转换为旋转矩阵
    yaw = eul(1);
    pitch = eul(2);
    roll = eul(3);    
    Rz = [cos(yaw) -sin(yaw) 0; 
          sin(yaw) cos(yaw)  0; 
          0        0         1];      
    Ry = [cos(pitch) 0 sin(pitch); 
          0          1 0; 
          -sin(pitch) 0 cos(pitch)];      
    Rx = [1 0          0; 
          0 cos(roll) -sin(roll); 
          0 sin(roll) cos(roll)];   
    % 组合旋转矩阵
    R = Rz * Ry * Rx;
end
function shangweiji3()
    % 设置串口参数
    serialPort = 'COM11'; % 替换为你的串口号
    baudRate = 9600;      % 波特率,替换为你的实际波特率
    s = serialport(serialPort, baudRate, "Timeout", 1); % 添加超时参数以防止读取错误
    % 初始化绘图
    figure;
    hold on;
    axis equal;
    grid on;
    % 正方体的8个顶点
    vertices = [
        -1 -1 -1;
         1 -1 -1;
         1  1 -1;
        -1  1 -1;
        -1 -1  1;
         1 -1  1;
         1  1  1;
        -1  1  1
    ];
    % 每个面对应的顶点索引
    faces = [
        1 2 3 4; % 侧面1
        5 6 7 8; % 侧面2
        1 2 6 5; % 底面
        2 3 7 6; % 右面
        3 4 8 7; % 顶面
        4 1 5 8  % 左面
    ];
    % 为每个面定义不同的颜色
    faceColors = [
        1 0 0;  % 红色
        0 1 0;  % 绿色
        0 0 1;  % 蓝色
        1 1 0;  % 黄色
        1 0 1;  % 品红
        0 1 1   % 青色
    ];
    % 绘制正方体
    hPatches = gobjects(size(faces, 1), 1); % 用于保存每个面对象
    for i = 1:size(faces, 1)
        hPatches(i) = patch('Vertices', vertices, 'Faces', faces(i,:), ...
                             'FaceColor', faceColors(i,:), 'FaceLighting', 'g', ...
                             'EdgeColor', 'k');
    end
    % 设置坐标轴
    xlim([-2 2]);
    ylim([-2 2]);
    zlim([-2 2]);
    xlabel('X轴');
    ylabel('Y轴');
    zlabel('Z轴');
    view(3); % 3D视图
    camlight; % 添加光源
    % 主循环读取串口数据
    while true
        if s.NumBytesAvailable > 0
            dataLine = readline(s);
            data = str2num(dataLine); %#ok<ST2NM>
            
            if length(data) == 3
                % MPU6050数据处理
                gx = data(1);
                gy = data(2);
                gz = data(3);
                
                % 调用更新正方体的函数
                updateCube(gx, gy, gz, hPatches, vertices);
            end
        end
    end
end
function updateCube(gx, gy, gz, hPatches, vertices)
    % 使用持久变量存储角度
    persistent pitch roll yaw;
    if isempty(pitch) || isempty(roll) || isempty(yaw)
        pitch = 0; roll = 0; yaw = 0;
    end    
    % 使用小时间步长更新欧拉角
    dt = 0.1; % 时间步长,建议根据实际情况调整
    sensitivity = 0.01; % 比例系数,调整这个值来控制运动幅度
    pitch = pitch + sensitivity * gy * dt; % Y轴旋转
    roll = roll + sensitivity * gx * dt;    % X轴旋转
    yaw = yaw + sensitivity * gz * dt;      % Z轴旋转    
    % 将欧拉角转换为旋转矩阵
    R = eul2rotm([yaw, pitch, roll]);   
    % 更新正方体的位置
    % 计算旋转后的顶点
    rotatedVertices = (R * vertices')';    
    % 更新每个面的顶点
    for i = 1:length(hPatches)
        set(hPatches(i), 'Vertices', rotatedVertices);
    end    
    drawnow; % 更新绘图
end
function R = eul2rotm(eul)
    % 将欧拉角转换为旋转矩阵
    yaw = eul(1);
    pitch = eul(2);
    roll = eul(3);    
    Rz = [cos(yaw) -sin(yaw) 0; 
          sin(yaw) cos(yaw)  0; 
          0        0         1];     
    Ry = [cos(pitch) 0 sin(pitch); 
          0          1 0; 
          -sin(pitch) 0 cos(pitch)];      
    Rx = [1 0          0; 
          0 cos(roll) -sin(roll); 
          0 sin(roll) cos(roll)];    
    % 组合旋转矩阵
    R = Rz * Ry * Rx;
end

实测该代码延迟较小,三维显示跟手性较好 。

实时三维显示陀螺仪三轴角度变化

6 手册及源代码

内容包括:
⭕MPU6050器件手册
⭕STC89C52RC单片机驱动MPU6050代码
⭕上位机代码

学如逆水行舟,不进则退,希望大家能够不吝指正,互相交流!
大家的点赞是小编更新的动力!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Study For All

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值