• I2C通信协议


    简介

    I2C Inter IC Bus )是由 Philips 公司开发的一种通用数据总线
    两根通信线: SCL Serial Clock )、 SDA Serial Data
    同步,半双工
    带数据应答
    支持总线挂载多设备(一主多从、多主多从)

     

    硬件电路 

    所有 I2C 设备的 SCL 连在一起, SDA 连在一起
    设备的 SCL SDA 均要配置成开漏输出模式
    SCL SDA 各添加一个上拉电阻,阻值一般为 4.7KΩ 左右

     

    I2C时序基本单元

    起始条件: SCL 高电平期间, SDA 从高电平切换到低电平
    终止条件: SCL 高电平期间, SDA 从低电平切换到高电平
    起始和终止都是主机产生的,从机不允许产生起始和中止,所以在总线空闲状态时,从机必须始终双手放开,不允许主动跳出来去碰主线

     

            •发送一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节

             接收一个字节:SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)(虚线是从机控制)

    发送应答:主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答
    接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA

    I2C从机地址 

     一主多从模型下,如何发出指令来确定要访问的是哪一个设备呢?这就需要首先把每一个设备都确定一个唯一的设备地址,从机设备地址就相当于每个设备的名字,主机在起始条件之后,要先发送一个字节选择从机名字,所以从机都会收到这个字节(名字),然后和自己的名字做比较,如果不一样,则该设备将不会再接收如何数据,如果一样,就说明主机选择了该设备,则响应接下来主机的读写操作。(这就要求在同一条I2C总线中挂载的每个设备的地址都必须得不一样)

    从机设备地址在I2C协议标准中分为7位地址和10位地址,7位地址最常用(可在相关芯片手册中找到),如果有相同的芯片挂载在同一条总线中该如何解决呢?一般器件地址的最后几位都是可以在电路中改变的。一般高位都是由厂商确定的,低位可以依靠接入的引脚进行灵活切换。(比如MPU6050的地址的最后一位就可以由其引脚AD0确定,接低电平,地址为1101 000,接高电平,地址为1101 001)

    I2C时序

    指定地址写
    对于指定设备( Slave Address ),在指定地址(寄存器地址)( Reg Address )下,写入指定数据( Data

    当前地址读
    对于指定设备( Slave Address ),在当前地址指针指示的地址下,读取从机数据( Data
    因为主机没有指定地址,从机就会返回当前指针指向的寄存器的地址(假如我们之前指定地址0x19写了数据,然后我们再调用当前地址读就会读0x19这个地址的数据,然后下一次读的就是地址+1的数据了)

    指定地址读
    对于指定设备( Slave Address ),在指定地址( Reg Address )下,读取从机数据( Data
    因为不可以指定地址读,所以我们是先指定地址写,选择完地址之后还没开始写就开始当前地址读,接着之前的地址就可以完成指定地址读的操作了

     MPU6050

    简介

    MPU6050 是一个 6 轴姿态传感器,可以测量芯片自身 X Y Z 轴的加速度、角速度参数,通过数据融合,可进一步得到姿态角,常应用于平衡车、飞行器等需要检测自身姿态的场景
    3 轴加速度计( Accelerometer ):测量 X Y Z 轴的加速度
    3 轴陀螺仪传感器( Gyroscope ):测量 X Y Z 轴的角速度

     

    参数 

    16 ADC 采集传感器的模拟信号,量化范围: -32768~32767
    加速度计满量程选择: ±2 ±4 ±8 ±16 g
    陀螺仪满量程选择: ±250 ±500 ±1000 ±2000 °/sec
    可配置的数字低通滤波器
    可配置的时钟源
    可配置的采样分频
    I2C 从机地址: 1101000 AD0=0 )(0x68)

                                        1101001(AD0=1)(0x69)

    我们在I2C时序中输入I2C从机的地址时,输入的第一个字节前七位位从机地址,最后一位是决定读或者写。当我们输入时就需要将我们的从机的地址左移一位,然后或上读1写0.

    当然我们可以直接把左移一位的地址看做I2C从机地址,把读写位也融入I2C从机地址中,即1101 0000(AD0=0)(0xD0)和1101 0010(AD0=1)(0xD2) ,然后再按照读1写0或上对应数字。

    硬件电路

    SDA和SCL是I2C通信脚,可以看到已经挂载了两个上拉电阻,可以不用外接电阻再接到GPIO口上了。                                                                        XCL、XDA:用于拓展芯片功能,MPU6050是6轴姿态传感器,融合出来的姿态角是有缺陷的,可以外接磁力计和气压计,拓展为十轴姿态传感器,MPU6050的主机接口就可以直接访问这些拓展芯片的数据,把这些拓展芯片的数据读取到MPU6050中,通过DMP单元进行数据融合和姿态解算,融合出更准确的姿态角。

    INT:可以配置芯片内部的一些事件来触发中断引脚的输出,例如数据准备好了、I2C主机错误等,芯片中还内置了一些实用的小功能,比如自由落体检测、运动检测、零运动检测等这些信号都可以触发INT引脚产生电平跳变。

    框图

    XYZ....:都是各个方向的有关传感器

    Temp Sensor:温度传感器;

    各个传感器传出来的数据都会由芯片自动进行AD数模转换发送到数据寄存器中,且不会相互覆盖。

    自测系统:我们先使能自测系统,得到传感器的数据,再失能自测系统,得到另一组传感器的数据,两组数据之差在芯片数据手册中有标明一个正常的范围,如果超了这个范围,则代表传感器失灵。 

    Interrupt Status Register:中断状态寄存器,控制内部哪些事件到中断引脚的输出;

    FIFO:先入先出寄存器,可以对数据流进行缓存

    Config Register:配置寄存器,可以对内部的各个电路进行配置

    Sensor Register:传感器数据寄存器

    数组运动处理器(DMP):芯片内部自带的姿态解算的硬件算法,配合官方的DMP库,可以进行姿态解算

    寄存器

    采样分频寄存器:配置采样分频率的分频系数,简单来说,分频越小,内部的AD转换就越快,数据寄存器刷新越快

     配置寄存器

    分为两部分:外部同步设置和低通滤波器配置

    外部我们不用,先不看

    低通滤波器配置——让输出数据更加平滑,配置滤波器参数越大,输出数据抖动越小

     

    陀螺仪配置寄存器

     高三位:XYZ轴的自测使能位,中间两位:满量程选择位

    自测响应的计算公式

     

    自测响应的范围

     满量程选择:量程越大,范围越广,量程越小,分辨率越高

     加速度计配置寄存器

     高三位:XYZ轴的自测使能位,中间两位:满量程选择位,第三位:配置高通滤波器

     加速度计的数据寄存器

    得到的数据时16位的有符号数,以二进制补码的方式存储

    其他数据寄存器也是相同操作

    电源管理寄存器1

    第一位:设备复位,写1,所有寄存器恢复默认值;

    2:睡眠模式,写1芯片睡眠,不工作,进入低功耗;

    3:循环模式,设备进入低功耗,过一段时间启动一次

    4:温度传感器失能,写1,禁用内部的温度传感器

    最后三位:选择系统的时钟来源(建议选择陀螺仪的晶振,比较准确)

    电源管理寄存器2:

    前两位:控制电源管理寄存器1中的循环模式的频率;

     后面6位:可以分别控制6个轴进入待机模式

    ID号:只读(即这个芯片的I2C地址)

    除了ID号和电源管理寄存器1,其他寄存器默认值为0x00

    软件实现I2C通信 

     首先建立I2C通信层的.c和.h模块,在通信层里写好I2C底层的GPIO初始化和6个时序基本单元(起始终止,发送一个字节,接收一个字节,发送应答和接收应答)。

                                                                            |

    再建立MPU6050的.c和.h模块,再这一层基于I2C通信的模块来实现指定地址读、指定地址写,再实现寄存器对芯片进行配置,读寄存器得到传感器数据。

                                                                            |

    最后再在主函数中调用MPU6050模块,初始化,拿到数据,显示数据。

    因为是用软件实现I2C,所以我们可以不用库中的I2C外设的函数,而是自己通过配置GPIO口的函数来实现就行了。

    软件I2C初始化

    1、把SCL和SDA都初始化为开漏输出模式,把SCL和SDA置高电平

    1. void MyI2C_Init(void)
    2. {
    3. RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    4. GPIO_InitTypeDef GPIO_InitStructure;
    5. //开漏输出模式下也可以输入
    6. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
    7. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
    8. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    9. GPIO_Init(GPIOB, &GPIO_InitStructure);
    10. //再把SCL和SDA置高电平,释放总线,此时I2C总线处于空闲状态
    11. GPIO_WriteBit(GPIOA, GPIO_Pin_10 | GPIO_Pin_11, Bit_SET);
    12. }

    2、时序基本单元的配置

    配置起始条件等时序基本单元时需要多次反转SCL和SDA的电平,如果我们仅仅使用Reset和Set来反转电平,并不是不可以,只是这样会导致代码意义不明确,而且在修改时需要更改太多,况且也不好移植到其他开发板上应用,所以我们就需要对反转电平的操作进行简化,目标是使其易更改,易移植且意义明确。

    我们就可以使用宏定义来完成

    想法1是把SET和RESET语句宏定义(有参宏)(OLED模块中有示例)

    1. #define MyI2C_W_SCL(x) GPIO_WriteBit(GPIOB, GPIO_Pin_8, (BitAction)(x))
    2. //BitAction是枚举类型

    但是这种方法移植到其他单片机时,不好修改,而且如果把这种代码移植到一个主频很高的单片机中,需要对软件时序进行延时操作时,不方便进一步修改

    所以不如直接用函数封装起来

    1. /*用于翻转电平的函数*/
    2. //释放或拉低SCL
    3. void MyI2C_W_SCL(uint8_t BitValue)
    4. {
    5. GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);
    6. //以防单片机主频太快
    7. Delay_us(10);
    8. }
    9. void MyI2C_W_SDA(uint8_t BitValue)
    10. {
    11. GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);
    12. Delay_us(10);
    13. }
    14. //读取SDA的值
    15. uint8_t MyI2C_R_SDA(void)
    16. {
    17. uint8_t BitValue;
    18. BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
    19. Delay_us(10);
    20. return BitValue;
    21. }

    这样我们在移植代码时就可以只修改这部分和初始化部分的代码了 

    a、起始条件

    起始条件:SCL高电平期间,SDA从高电平切换到低电平

    1. //起始条件:SCL高电平期间,SDA从高电平切换到低电平
    2. void MyI2C_Start(void)
    3. {
    4. MyI2C_W_SDA(1);
    5. MyI2C_W_SCL(1);
    6. MyI2C_W_SDA(0);
    7. MyI2C_W_SCL(0);
    8. }
    b、终止条件

    终止条件:SCL高电平期间,SDA从低电平切换到高电平

    1. //终止条件:SCL高电平期间,SDA从低电平切换到高电平
    2. void MyI2C_Stop(void)
    3. {
    4. //因为在终止之前SDA可能是高电平,则无法实现从低切换到高
    5. //所以需要创造能从低到高的条件,就必须先拉低SDA
    6. MyI2C_W_SDA(0);
    7. MyI2C_W_SCL(1);
    8. MyI2C_W_SDA(1);
    9. }
    c、发送一个字节

    除了终止条件,我们都会保证SCL以低电平结束,方便各个单元的拼接

    1. //发送一个字节
    2. void MyI2C_SendByte(uint8_t Byte)
    3. {
    4. //下面函数的参数非0即1
    5. //通过与上一个值把每一位数字都取出来
    6. MyI2C_W_SDA(Byte & 0x80);
    7. //高电平读取SCL的数据
    8. MyI2C_W_SCL(1);
    9. MyI2C_W_SCL(0);
    10. MyI2C_W_SDA(Byte & 0x40);
    11. MyI2C_W_SCL(1);
    12. MyI2C_W_SCL(0);
    13. MyI2C_W_SDA(Byte & 0x20);
    14. MyI2C_W_SCL(1);
    15. MyI2C_W_SCL(0);
    16. MyI2C_W_SDA(Byte & 0x10);
    17. MyI2C_W_SCL(1);
    18. MyI2C_W_SCL(0);
    19. MyI2C_W_SDA(Byte & 0x08);
    20. MyI2C_W_SCL(1);
    21. MyI2C_W_SCL(0);
    22. MyI2C_W_SDA(Byte & 0x04);
    23. MyI2C_W_SCL(1);
    24. MyI2C_W_SCL(0);
    25. MyI2C_W_SDA(Byte & 0x02);
    26. MyI2C_W_SCL(1);
    27. MyI2C_W_SCL(0);
    28. MyI2C_W_SDA(Byte & 0x01);
    29. MyI2C_W_SCL(1);
    30. MyI2C_W_SCL(0);
    31. }

     用for循环简化

    1. //发送一个字节
    2. void MyI2C_SendByte(uint8_t Byte)
    3. {
    4. uint8_t i;
    5. for (i = 0; i < 8; i ++)
    6. {
    7. MyI2C_W_SDA(Byte & (0x80 >> i));
    8. MyI2C_W_SCL(1);
    9. MyI2C_W_SCL(0);
    10. }
    11. }
    d、接收一个字节

    接收一个字节:SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)(虚线是从机控制)

    在接收字节前,主机必须放手SDA(SDA置高电平),交由从机控制SDA,这样就可以实现在SCL高电平期间读取从机SDA上的电平而实现读取从机的数据。要读取从机的数据,主机就不可以“乱动”SDA,如果这期间主机SDA变化,那么就是起始位和终止位的条件了。

    1. //接收一个字节
    2. uint8_t MyI2C_ReceiveByte(void)
    3. {
    4. uint8_t i, Byte = 0x00;
    5. //主机先放手SDA
    6. MyI2C_W_SDA(1);
    7. for (i = 0; i < 8; i ++)
    8. {
    9. //在SCL为1时接收数据(从高位开始)
    10. MyI2C_W_SCL(1);
    11. if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}
    12. MyI2C_W_SCL(0);
    13. }
    14. return Byte;
    15. }
    e、发送应答和接收应答

    其实就是发送一个字节和接收一个字节的简化版,只接收一位数据版

    1. //发送应答
    2. void MyI2C_SendAck(uint8_t AckBit)
    3. {
    4. MyI2C_W_SDA(AckBit);
    5. MyI2C_W_SCL(1);
    6. MyI2C_W_SCL(0);
    7. }
    8. //接收应答
    9. uint8_t MyI2C_ReceiveAck(void)
    10. {
    11. uint8_t AckBit;
    12. MyI2C_W_SDA(1);
    13. MyI2C_W_SCL(1);
    14. AckBit = MyI2C_R_SDA();
    15. MyI2C_W_SCL(0);
    16. return AckBit;
    17. }
    总体 

    这样就把六个时序基本单元配置好了

    1. #include "stm32f10x.h" // Device header
    2. #include "Delay.h"
    3. /*用于翻转电平的函数*/
    4. //释放或拉低SCL
    5. void MyI2C_W_SCL(uint8_t BitValue)
    6. {
    7. GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);
    8. //以防单片机主频太快
    9. Delay_us(10);
    10. }
    11. void MyI2C_W_SDA(uint8_t BitValue)
    12. {
    13. GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);
    14. Delay_us(10);
    15. }
    16. //读取SDA的值
    17. uint8_t MyI2C_R_SDA(void)
    18. {
    19. uint8_t BitValue;
    20. BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
    21. Delay_us(10);
    22. return BitValue;
    23. }
    24. void MyI2C_Init(void)
    25. {
    26. RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    27. GPIO_InitTypeDef GPIO_InitStructure;
    28. //开漏输出模式下也可以输入
    29. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
    30. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
    31. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    32. GPIO_Init(GPIOB, &GPIO_InitStructure);
    33. //再把SCL和SDA置高电平,释放总线,此时I2C总线处于空闲状态
    34. GPIO_WriteBit(GPIOA, GPIO_Pin_10 | GPIO_Pin_11, Bit_SET);
    35. }
    36. //时序基本单元
    37. //起始条件:SCL高电平期间,SDA从高电平切换到低电平
    38. void MyI2C_Start(void)
    39. {
    40. MyI2C_W_SDA(1);
    41. MyI2C_W_SCL(1);
    42. MyI2C_W_SDA(0);
    43. MyI2C_W_SCL(0);
    44. }
    45. //终止条件:SCL高电平期间,SDA从低电平切换到高电平
    46. void MyI2C_Stop(void)
    47. {
    48. //因为在终止之前SDA可能是高电平,则无法实现从低切换到高
    49. //所以需要创造能从低到高的条件,就必须先拉低SDA
    50. MyI2C_W_SDA(0);
    51. MyI2C_W_SCL(1);
    52. MyI2C_W_SDA(1);
    53. }
    54. //发送一个字节
    55. void MyI2C_SendByte(uint8_t Byte)
    56. {
    57. uint8_t i;
    58. for (i = 0; i < 8; i ++)
    59. {
    60. MyI2C_W_SDA(Byte & (0x80 >> i));
    61. MyI2C_W_SCL(1);
    62. MyI2C_W_SCL(0);
    63. }
    64. // //下面函数的参数非0即1
    65. // //通过与上一个值把每一位数字都取出来
    66. // MyI2C_W_SDA(Byte & 0x80);
    67. // //高电平读取SCL的数据
    68. // MyI2C_W_SCL(1);
    69. // MyI2C_W_SCL(0);
    70. }
    71. //接收一个字节
    72. uint8_t MyI2C_ReceiveByte(void)
    73. {
    74. uint8_t i, Byte = 0x00;
    75. //主机先放手SDA
    76. //I2C的引脚都是开漏输出+弱上拉的配置
    77. //主机输出1,并不是强制SDA为高电平,而是释放SDA
    78. MyI2C_W_SDA(1);
    79. for (i = 0; i < 8; i ++)
    80. {
    81. //在SCL为1时接收数据(从高位开始)
    82. MyI2C_W_SCL(1);
    83. if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}
    84. MyI2C_W_SCL(0);
    85. }
    86. return Byte;
    87. }
    88. //发送应答
    89. void MyI2C_SendAck(uint8_t AckBit)
    90. {
    91. MyI2C_W_SDA(AckBit);
    92. MyI2C_W_SCL(1);
    93. MyI2C_W_SCL(0);
    94. }
    95. //接收应答
    96. uint8_t MyI2C_ReceiveAck(void)
    97. {
    98. uint8_t AckBit;
    99. MyI2C_W_SDA(1);
    100. MyI2C_W_SCL(1);
    101. AckBit = MyI2C_R_SDA();
    102. MyI2C_W_SCL(0);
    103. return AckBit;
    104. }
    验证

    目前还未写好MPU的代码,可以先验证起始条件、终止条件、发送一个字节和接收应答

    1. #include "stm32f10x.h" // Device header
    2. #include "Delay.h"
    3. #include "OLED.h"
    4. #include "MyI2C.h"
    5. int main(void)
    6. {
    7. OLED_Init();
    8. MyI2C_Init();
    9. MyI2C_Start();
    10. MyI2C_SendByte(0xD0); //1101 000 0
    11. uint8_t Ack = MyI2C_ReceiveAck();
    12. MyI2C_Stop();
    13. OLED_ShowNum(1, 1, Ack, 3);
    14. while(1)
    15. {
    16. }
    17. }

    如果OLED上显示0,则表示接收到了数据,我们再修改发送的值为其他,如果OLED上显示1,则代表没接收到数据,也就代表代码编写正确。

    我们还可以通过连接MPU的AD0引脚到高电平修改从机的地址来验证代码,此时输入的字节必须为0xD2(1101 010 0)

    接下来编写MPU6050模块

    即读写MPU6050的寄存器

    1. #include "stm32f10x.h" // Device header
    2. #include "MyI2C.h"
    3. #define MPU6050_ADDRESS 0xD0
    4. //指定地址写
    5. void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
    6. {
    7. MyI2C_Start();
    8. MyI2C_SendByte(MPU6050_ADDRESS);
    9. //可以验证是否收到数据,具体怎么处理就不加上了
    10. MyI2C_ReceiveAck();
    11. //发送寄存器的地址
    12. MyI2C_SendByte(RegAddress);
    13. MyI2C_ReceiveAck();
    14. //接收一个字节
    15. MyI2C_SendByte(Data);
    16. MyI2C_ReceiveAck();
    17. MyI2C_Stop();
    18. }
    19. //指定地址读
    20. uint8_t MPU6050_ReadtheReg(uint8_t RegAddress)
    21. {
    22. uint8_t Data;
    23. MyI2C_Start();
    24. MyI2C_SendByte(MPU6050_ADDRESS);
    25. MyI2C_ReceiveAck();
    26. MyI2C_SendByte(RegAddress);
    27. MyI2C_ReceiveAck();
    28. MyI2C_Start();
    29. MyI2C_SendByte(MPU6050_ADDRESS | 0x01);
    30. MyI2C_ReceiveAck();
    31. Data = MyI2C_ReceiveByte();
    32. //发送主机应答,已经接收到数据了
    33. MyI2C_SendAck(1);
    34. MyI2C_Stop();
    35. return Data;
    36. }
    37. void MPU6050_Init(void)
    38. {
    39. MyI2C_Init();
    40. }

    然后再在主函数中验证

    先读MPU的ID号

    1. #include "stm32f10x.h" // Device header
    2. #include "Delay.h"
    3. #include "OLED.h"
    4. #include "MPU6050.h"
    5. int main(void)
    6. {
    7. OLED_Init();
    8. MPU6050_Init();
    9. uint8_t Data = MPU6050_ReadtheReg(0x75);
    10. OLED_ShowHexNum(1, 1, Data, 2);
    11. while(1)
    12. {
    13. }
    14. }

    结果是OLED上显示68,即ID号为0x68,则代表读寄存器的代码编写正确

    那我们再验证一下写寄存器的代码

    首先需要关闭芯片的睡眠模式,下图Bit7即为我们需要更改的寄存器

    先关闭睡眠模式,然后再在地址为0x19的寄存器上写入0x66 

    1. #include "stm32f10x.h" // Device header
    2. #include "Delay.h"
    3. #include "OLED.h"
    4. #include "MPU6050.h"
    5. int main(void)
    6. {
    7. OLED_Init();
    8. MPU6050_Init();
    9. MPU6050_WriteReg(0x6B, 0x00);
    10. MPU6050_WriteReg(0x19, 0x66);
    11. uint8_t Data = MPU6050_ReadtheReg(0x19);
    12. OLED_ShowHexNum(1, 1, Data, 2);
    13. while(1)
    14. {
    15. }
    16. }

     结果是OLED上显示为66,即0x66,代表没问题

    我们这里的验证思路是把MPU6050当做一个存储器来验证的,MPU6050里的每个寄存器都对应着芯片的一种状态,寄存器和外设的硬件电路是可以进行互动的,意思是我们可以通过配置寄存器来和MPU6050的硬件电路进行互动。

    课后任务:实现发送一个数组到多个地址,读取多个地址的数据 

    配置寄存器控制电路

    我们初始化MPU6050芯片还需要做到配置其电源管理寄存器、采样分频寄存器等等,所以我们可以在Init函数中调用写入寄存器函数来配置这些寄存器

    为了方便移植,我们可以用宏定义来定义各个寄存器,因为寄存器数量多,我们决定使用头文件模块化这部分宏定义

    MPU6050_Reg.h

    1. #ifndef __MPU6050_REG_H__
    2. #define __MPU6050_REG_H__
    3. #define MPU6050_SMPLRT_DIV 0x19
    4. #define MPU6050_CONFIG 0x1A
    5. #define MPU6050_GYRO_CONFIG 0x1B
    6. #define MPU6050_ACCEL_CONFIG 0x1C
    7. #define MPU6050_ACCEL_XOUT_H 0x3B
    8. #define MPU6050_ACCEL_XOUT_L 0x3C
    9. #define MPU6050_ACCEL_YOUT_H 0x3D
    10. #define MPU6050_ACCEL_YOUT_L 0x3E
    11. #define MPU6050_ACCEL_ZOUT_H 0x3F
    12. #define MPU6050_ACCEL_ZOUT_L 0x40
    13. #define MPU6050_TEMP_OUT_H 0x41
    14. #define MPU6050_TEMP_OUT_L 0x42
    15. #define MPU6050_GYRO_XOUT_H 0x43
    16. #define MPU6050_GYRO_XOUT_L 0x44
    17. #define MPU6050_GYRO_YOUT_H 0x45
    18. #define MPU6050_GYRO_YOUT_L 0x46
    19. #define MPU6050_GYRO_ZOUT_H 0x47
    20. #define MPU6050_GYRO_ZOUT_L 0x48
    21. #define MPU6050_PWR_MGMT_1 0x6B
    22. #define MPU6050_PWR_MGMT_2 0x6C
    23. #define MPU6050_WHO_AM_I 0x75
    24. #endif

    然后配置各个寄存器

    1. void MPU6050_Init(void)
    2. {
    3. MyI2C_Init();
    4. //电源管理寄存器1
    5. //设备复位 睡眠模式 循环模式 无关位 温度传感器失能 选择时钟(后三位)
    6. //0(不复位) 0(解除睡眠) 0(不需要) 0 0(不失能) 000(内部时钟)(或者001-陀螺仪时钟)
    7. MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);
    8. //电源管理寄存器2
    9. //循环模式唤醒频率(不需要-00)
    10. //后6位每个轴的待机位-(000000-不待机)
    11. MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);
    12. //采样率分频(值越小,数据输出越快)(采样分频为10)
    13. MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);
    14. //配置寄存器
    15. //外部同步-000000 数字低通滤波-110
    16. MPU6050_WriteReg(MPU6050_CONFIG, 0x06);
    17. //陀螺仪配置寄存器
    18. //自测使能-000 满量程选择-11 无关位-000
    19. MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);
    20. //加速度计配置寄存器
    21. //自测使能-000 满量程选择-11 高通滤波器-000
    22. MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);
    23. }

     读取特定寄存器的值

    根据任务需求,我们这个函数需要返回6个数据,但是c语言函数不可以同时返回这么多值,所以我们就会用到以下方法

    1、定义全局变量,调用函数时修改这些变量的值——不推荐在大项目中使用

    2、使用指针

    3、使用结构体,将这些数据打包起来

    这里我们使用指针来完成

    1. //读取寄存器
    2. void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ,
    3. int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
    4. {
    5. uint16_t DataH, DataL;
    6. DataH = MPU6050_ReadtheReg(MPU6050_ACCEL_XOUT_H);
    7. DataL = MPU6050_ReadtheReg(MPU6050_ACCEL_XOUT_L);
    8. *AccX = (DataH << 8) | DataL;
    9. DataH = MPU6050_ReadtheReg(MPU6050_ACCEL_YOUT_H);
    10. DataL = MPU6050_ReadtheReg(MPU6050_ACCEL_YOUT_L);
    11. *AccY = (DataH << 8) | DataL;
    12. DataH = MPU6050_ReadtheReg(MPU6050_ACCEL_ZOUT_H);
    13. DataL = MPU6050_ReadtheReg(MPU6050_ACCEL_ZOUT_L);
    14. *AccZ = (DataH << 8) | DataL;
    15. DataH = MPU6050_ReadtheReg(MPU6050_GYRO_XOUT_H);
    16. DataL = MPU6050_ReadtheReg(MPU6050_GYRO_XOUT_L);
    17. *GyroX = (DataH << 8) | DataL;
    18. DataH = MPU6050_ReadtheReg(MPU6050_GYRO_YOUT_H);
    19. DataL = MPU6050_ReadtheReg(MPU6050_GYRO_YOUT_L);
    20. *GyroY = (DataH << 8) | DataL;
    21. DataH = MPU6050_ReadtheReg(MPU6050_GYRO_ZOUT_H);
    22. DataL = MPU6050_ReadtheReg(MPU6050_GYRO_ZOUT_L);
    23. *GyroZ = (DataH << 8) | DataL;
    24. }
    25. //读取寄存器
    26. void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ,
    27. int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
    28. {
    29. uint16_t DataH, DataL;
    30. DataH = MPU6050_ReadtheReg(MPU6050_ACCEL_XOUT_H);
    31. DataL = MPU6050_ReadtheReg(MPU6050_ACCEL_XOUT_L);
    32. *AccX = (DataH << 8) | DataL;
    33. DataH = MPU6050_ReadtheReg(MPU6050_ACCEL_YOUT_H);
    34. DataL = MPU6050_ReadtheReg(MPU6050_ACCEL_YOUT_L);
    35. *AccY = (DataH << 8) | DataL;
    36. DataH = MPU6050_ReadtheReg(MPU6050_ACCEL_ZOUT_H);
    37. DataL = MPU6050_ReadtheReg(MPU6050_ACCEL_ZOUT_L);
    38. *AccZ = (DataH << 8) | DataL;
    39. DataH = MPU6050_ReadtheReg(MPU6050_GYRO_XOUT_H);
    40. DataL = MPU6050_ReadtheReg(MPU6050_GYRO_XOUT_L);
    41. *GyroX = (DataH << 8) | DataL;
    42. DataH = MPU6050_ReadtheReg(MPU6050_GYRO_YOUT_H);
    43. DataL = MPU6050_ReadtheReg(MPU6050_GYRO_YOUT_L);
    44. *GyroY = (DataH << 8) | DataL;
    45. DataH = MPU6050_ReadtheReg(MPU6050_GYRO_ZOUT_H);
    46. DataL = MPU6050_ReadtheReg(MPU6050_GYRO_ZOUT_L);
    47. *GyroZ = (DataH << 8) | DataL;
    48. }
    49. uint8_t MPU6050_GetID(void)
    50. {
    51. return MPU6050_ReadtheReg(MPU6050_WHO_AM_I);
    52. }

    这样是连续调用多次读取指定寄存器的函数,再把两个8位的数据拼接成16位的数据的方法

    但其实有更方便的方法,就是使用课后作业中实现读取连续多个字节的代码,从一个基地址开始,连续读取一片的寄存器——因为这些寄存器的地址是连续在一起的,这样在时序上,读取效率就会大大提升。

    在主函数中调用

    1. #include "stm32f10x.h" // Device header
    2. #include "Delay.h"
    3. #include "OLED.h"
    4. #include "MPU6050.h"
    5. uint8_t ID;
    6. int16_t AX, AY, AZ, GX, GY, GZ;
    7. int main(void)
    8. {
    9. OLED_Init();
    10. MPU6050_Init();
    11. OLED_ShowString(1, 1, "ID:");
    12. ID = MPU6050_GetID();
    13. OLED_ShowHexNum(1, 4, ID, 2);
    14. while(1)
    15. {
    16. MPU6050_GetData(&AX, &AY, &AZ, &GX, &GY, &GZ);
    17. OLED_ShowSignedNum(2, 1, AX, 5);
    18. OLED_ShowSignedNum(3, 1, AY, 5);
    19. OLED_ShowSignedNum(4, 1, AZ, 5);
    20. OLED_ShowSignedNum(2, 8, GX, 5);
    21. OLED_ShowSignedNum(3, 8, GY, 5);
    22. OLED_ShowSignedNum(4, 8, GZ, 5);
    23. }
    24. }

    求出各项具体值的方法:显示值/32768=x/满量程,x即为所求

  • 相关阅读:
    拥抱 Spring 全新 OAuth 解决方案
    记录裁员后第一周的面试记录
    [网上摘录]Gerber RS274X-CAM文件格式详解
    docker安装UnlockMusic(音乐格式转换工具 )
    iMazing兼容Win和Mac2023免费版iOS设备管理器
    vue组件使用与父子传递
    一致性HASH算法介绍,附代码
    Minio入门系列【3】MinIO Client使用详解
    【python绘图—colorbar操作学习】
    Git 修改已提交的用户名和邮箱
  • 原文地址:https://blog.csdn.net/m0_74460550/article/details/133531410