(1)因为我需要使用MPU6050的组件,但是又需要在这条I2C总线上挂载多个设备,所以我本人打算自己对官方的MPU6050的组件进行微调。建立一个I2C总线,设备依赖于这个总线挂载。
(2)既然要做移植工作,所以就需要弄明白这个组件应当如何使用。
(1)此函数用于创建一个指向MPU6050初始化信息的
mpu6050_handle_t
无符号类型指针。我们可以通过这个指针访问到mpu6050.c
这个组件中的mpu6050_dev_t
结构体。
(2)这样做能够做到常说的高内聚,低耦合的作用。如果不明白也没事,记住他返回的这个数据是和MPU6050的信息有关,我们能用就行。
(3)如何传入数据:
<1>当你的MPU6050是挂载在ESP32的I2C0时候,传入I2C_NUM_0
。如果是I2C1,传入I2C_NUM_1
。如果是挂载在低功耗I2C中,传入LP_I2C_NUM_0
。需要注意的是,不是所有的ESP32都具有I2C1和低功耗I2C,这个需要自行查阅芯片手册。
<2>关于MPU6050的地址,有两种。当MPU6050的第9号引脚AD0为低电平的时候地址为0x68,即MPU6050_I2C_ADDRESS
。当这个引脚为高电平时候地址为0x69,即MPU6050_I2C_ADDRESS_1
。
/**
* @brief 初始化MPU6050相关信息
*
* @param port 要挂载在哪个I2C总线上
* -dev_addr MPU6050的地址
*
* @return NULL 初始化MPU6050的信息失败
* -非NULL指针 初始化MPU6050的信息成功
*/
mpu6050_handle_t mpu6050_create(i2c_port_t port, const uint16_t dev_addr)
(1)
mpu6050_handle_t
存放在头文件中,用于暴露接口。
(2)mpu6050_dev_t
这个结构体具体作用讲实话我也没有搞太明白,只能说我知道的认为重要的两个部分。
<1>bus
,存放mpu6050是挂载在哪个I2C下。
<2>dev_addr
,MPU6050的地址。
(3)函数实现部分介绍,注意,我也只能讲我懂的部分:
<1>使用calloc()
函数分配1个mpu6050_dev_t
类型的空间,并且将这块空间全部初始化为0。
<2>对申请到的sensor
变量进行初始化,将MPU6050挂载的I2C信息,MPU6050地址信息存入这个变量。可能有些人会有疑惑,为什么地址信息dev_addr << 1
需要进行一次右移操作。这个和I2C的时序逻辑有关,一般I2C设备的地址为7bit,最后1bit负责存放是对设备读还是写的信息。因此这里需要进行右移一位。
<3>最后返回的数据进行强制类型转换为mpu6050_handle_t
这样就能够实现我上述所说的高内聚,低耦合的功能。
/*--- mpu6050.h ---*/
typedef void *mpu6050_handle_t;
/*--- mpu6050.c ---*/
typedef struct {
i2c_port_t bus;
gpio_num_t int_pin;
uint16_t dev_addr;
uint32_t counter;
float dt; /*!< delay time between two measurements, dt should be small (ms level) */
struct timeval *timer;
} mpu6050_dev_t;
mpu6050_handle_t mpu6050_create(i2c_port_t port, const uint16_t dev_addr)
{
mpu6050_dev_t *sensor = (mpu6050_dev_t *) calloc(1, sizeof(mpu6050_dev_t));
sensor->bus = port;
sensor->dev_addr = dev_addr << 1;
sensor->counter = 0;
sensor->dt = 0;
sensor->timer = (struct timeval *) calloc(1, sizeof(struct timeval));
return (mpu6050_handle_t) sensor;
}
(1)用于设置MPU6050的加速度计满量程和陀螺仪满量程。
(2)如何传入数据:
<1>传入mpu6050_create()函数创建的mpu6050_handle_t指针。
<2>ACCE_FS_2G
,加速度计满量程为+/-2g。ACCE_FS_4G
,为+/-4g。ACCE_FS_8G
,为+/-8g。ACCE_FS_16G
,为+/-16g。
<3>GYRO_FS_250DPS
,陀螺仪满量程是+/- 250度每秒。GYRO_FS_500DPS
,为500度每秒。GYRO_FS_1000DPS
,为1000度每秒。GYRO_FS_2000DPS
,为2000度每秒。
(3)关于这个设置由你自己看情况决定。如果值太高将会降低分辨率,值太低就无法测量过高的数据。
/**
* @brief 设置MPU6050的加速度计满量程和陀螺仪满量程
*
* @param sensor mpu6050_create()函数创建的mpu6050_handle_t指针
* -acce_fs 设置加速度计满量程
* -gyro_fs 设置陀螺仪满量程
*
* @return ESP_OK MPU6050配置成功
* -ESP_FAIL MPU6050配置失败
*/
esp_err_t mpu6050_config(mpu6050_handle_t sensor,
const mpu6050_acce_fs_t acce_fs,
const mpu6050_gyro_fs_t gyro_fs)
(1)进入
mpu6050_config()
函数,我们会看到他先建立了一个8bit的无符号数组,将陀螺仪配置放在前面,加速度计配置放在后面。这是因为MPU6050的陀螺仪配置寄存器GYRO_CONFIG
在加速度计寄存器ACCEL_CONFIG
的前面,而MPU6050的每个寄存器为8bit。
esp_err_t mpu6050_config(mpu6050_handle_t sensor,
const mpu6050_acce_fs_t acce_fs,
const mpu6050_gyro_fs_t gyro_fs)
{
uint8_t config_regs[2] = {gyro_fs << 3, acce_fs << 3};
return mpu6050_write(sensor, MPU6050_GYRO_CONFIG, config_regs, sizeof(config_regs));
}
(1)前面我们知道了
mpu6050_config()
配置如何实现的,但是又有一个问题mpu6050_write()
里面做了什么。
(2)我们讲解mpu6050_write()
函数前,需要知道主机和MPU6050的通讯格式。
(3)原图在MPU-6000 and MPU-6050 Product Specification Revision 3.4手册的35页,也就是9.3 I2C Communications Protocol章节。直接看图,我不想过多讲解,图依旧很清晰了。
(1)前面了解了MPU6050的写数据的格式之后,就可以开始解析函数了。
<1>首先对sensor
这个mpu6050_handle_t
类型指针强制类型转换,前面我们说了,这个mpu6050_handle_t
本质就是一个无符号类型指针,利用他能够做到高内聚低耦合。这里你只需传入创建的指针数据,然后强制类型转换即可对这个指针访问。
<2>i2c_cmd_link_create()
创建一个I2C 连接的句柄。这个听起来是不是很术语,看不懂。讲实话,我看到这个我也是懵逼的。我查看了一下这个函数的源码,个人理解就是,一个双向链表,里面存储了一些I2C数据传输的信息。之后的I2C数据传输,需要利用这个链表来进行设置。至于为什么使用链表,原因很简单,你不知道I2C通讯会传输多少个数据,而且I2C数据传输肯定是从头往下走,所以采用的链表是很好的决定。
其实和上面对sensor
这个mpu6050_handle_t
类型指针强制类型转换是一个道理。I2C相关的函数都在i2c.c里面,用于实现高内聚低耦合。
<3>i2c_master_start()
,由于I2C协议规定,我们需要先发送一个起始信号。所以这个函数就是将起始信号写入缓存区。
<4>i2c_master_write_byte()
,由于I2C协议规定,你发送起始信号之后,I2C上的从机都被激活。这个时候主机需要告诉从机我是在和谁通讯,因此需要传入从机地址信息,也就是MPU6050地址信息。当从机知道主机是在和谁交互时候,I2C总线上其他没有被选中的从机将会进入休眠。只有主机和从机开始通讯。
<5>主机和从机通讯建立完成之后,主机将会告诉从机,我要对那个寄存器进行操作。
<6>i2c_master_write()
,现在主机和从机联系和对那个寄存器操作都有一个清晰的认识,于是可以开始传输数据了。
<7>i2c_master_stop()
,数据传输完成之后主机需要告诉从机,我数据写完了,你可以休息了。
<8>i2c_master_cmd_begin()
,上述操作进行完之后,起始ESP32的I2C并没有真正的工作。上述就是在缓冲区写入数据,调用这个函数,才是真正的将缓冲区的数据输出。
<9>i2c_cmd_link_delete()
,通讯结束之后,我们需要调用这个函数删除与I2C的连接。
/**
* @brief 设置MPU6050的加速度计满量程和陀螺仪满量程
*
* @param sensor mpu6050_create()函数创建的mpu6050_handle_t指针
* -reg_start_addr 要进行写入数据的寄存器
* -data_buf 写入寄存器中的数据
* -data_len 写入寄存器中的数据长度
*
* @return ESP_OK MPU6050寄存器数据写入成功
* -ESP_FAIL MPU6050寄存器数据写入失败
*/
static esp_err_t mpu6050_write(mpu6050_handle_t sensor,
const uint8_t reg_start_addr,
const uint8_t *const data_buf,
const uint8_t data_len)
{
mpu6050_dev_t *sens = (mpu6050_dev_t *) sensor;
esp_err_t ret;
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
ret = i2c_master_start(cmd);
assert(ESP_OK == ret);
ret = i2c_master_write_byte(cmd, sens->dev_addr | I2C_MASTER_WRITE, true);
assert(ESP_OK == ret);
ret = i2c_master_write_byte(cmd, reg_start_addr, true);
assert(ESP_OK == ret);
ret = i2c_master_write(cmd, data_buf, data_len, true);
assert(ESP_OK == ret);
ret = i2c_master_stop(cmd);
assert(ESP_OK == ret);
ret = i2c_master_cmd_begin(sens->bus, cmd, 1000 / portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd);
return ret;
}
(1)这里只需要传入MPU6050的句柄即可唤醒MPU6050。
/**
* @brief 唤醒MPU6050
*
* @param sensor mpu6050_create()函数创建的mpu6050_handle_t指针
* @return RT_EOK MPU6050唤醒成功
* -RT_ERROR MPU6050唤醒失败
*/
esp_err_t mpu6050_wake_up(mpu6050_handle_t sensor)
(1)这里其实也不难,就是对MPU6050的PWR_MGMT_1寄存器bit6清零即可。因为PWR_MGMT_1寄存器的bit6如果为1,MPU6050将会进入低功耗休眠状态。
(2)至于为什么需要先读取MPU6050的数据,很简单,如果直接写入一个数据,可能导致其他数据位数据被破坏。
esp_err_t mpu6050_wake_up(mpu6050_handle_t sensor)
{
esp_err_t ret;
uint8_t tmp;
ret = mpu6050_read(sensor, MPU6050_PWR_MGMT_1, &tmp, 1);
if (ESP_OK != ret) {
return ret;
}
tmp &= (~BIT6);
ret = mpu6050_write(sensor, MPU6050_PWR_MGMT_1, &tmp, 1);
return ret;
}
(1)
mpu6050_wake_up()
函数里面有一个mpu6050_read()
没有进行介绍,这里介绍一下。
(2)MPU6050的读数据有两次起始信号,而写数据只有一次起始信号。第二次起始信号开始之后,就可以读数据了。
(3)原图在MPU-6000 and MPU-6050 Product Specification Revision 3.4手册的36页,也就是9.3 I2C Communications Protocol章节。
(1)这里要根据上图一起理解。与
mpu6050_write()
函数相同部分我就不再赘述了。
<1>依旧是强制类型转换,建立I2C连接,发送起始信号。
<2>两个i2c_master_write_byte()
,这里注意,虽然我们主机ESP32是要读取数据,但是第一次还是写数据,因为从机需要知道,主机接下来是要读取那个寄存器的数据。
<3>第二次i2c_master_start()
和i2c_master_write_byte()
,这个是告诉从机,主机已经可以开始读取你的数据了。
<4>i2c_master_read()
,读取数据,这个函数最后一个参数为I2C_MASTER_LAST_NACK
表示主机每次收到数据返回一个ACK,不过主机最后一次收到数据返回NACK,告诉从机要停止发数据了。
<5>最后依旧是发送停止信息,使用i2c_master_cmd_begin()
函数将缓冲区数据输出。删除与I2C的联系。
/**
* @brief 设置MPU6050的加速度计满量程和陀螺仪满量程
*
* @param sensor mpu6050_create()函数创建的mpu6050_handle_t指针
* -reg_start_addr 要进行读取数据的寄存器
* -data_buf 读取到的数据存入空间
* -data_len 要读取数据的长度
*
* @return ESP_OK MPU6050寄存器数据读取成功
* -ESP_FAIL MPU6050寄存器数据读取失败
*/
static esp_err_t mpu6050_read(mpu6050_handle_t sensor,
const uint8_t reg_start_addr,
uint8_t *const data_buf,
const uint8_t data_len)
{
mpu6050_dev_t *sens = (mpu6050_dev_t *) sensor;
esp_err_t ret;
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
ret = i2c_master_start(cmd);
assert(ESP_OK == ret);
ret = i2c_master_write_byte(cmd, sens->dev_addr | I2C_MASTER_WRITE, true);
assert(ESP_OK == ret);
ret = i2c_master_write_byte(cmd, reg_start_addr, true);
assert(ESP_OK == ret);
ret = i2c_master_start(cmd);
assert(ESP_OK == ret);
ret = i2c_master_write_byte(cmd, sens->dev_addr | I2C_MASTER_READ, true);
assert(ESP_OK == ret);
ret = i2c_master_read(cmd, data_buf, data_len, I2C_MASTER_LAST_NACK);
assert(ESP_OK == ret);
ret = i2c_master_stop(cmd);
assert(ESP_OK == ret);
ret = i2c_master_cmd_begin(sens->bus, cmd, 1000 / portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd);
return ret;
}
(1)因为本人精力有限,不可能每个函数都完完全全介绍一边。经过上面的了解,重要的部分读者应该都了解了。其他的一些函数,对底层实现感兴趣的可以自行查看MPU-6050_Register_Map手册,对照着代码进行理解。
(2)上面的几个函数了解了之后,就只有如下这三个函数需要了解了。
<1>mpu6050_get_acce()
函数,获取MPU6050的加速度值。
<2>mpu6050_get_gyro()
函数,获取MPU6050的陀螺仪值。
<2>mpu6050_get_temp()
函数,获取MPU6050的温度值。
(4)这三个函数,第一个都是传入的I2C句柄。(mpu6050_create()函数创建的mpu6050_handle_t指针)第二个参数略有不同:
<1>mpu6050_get_acce()
函数,他需要传入一个mpu6050_acce_value_t类型结构体指针,最终对数据进行处理是采用acce.acce_x方法。
typedef struct {
float acce_x; //x轴加速度
float acce_y; //y轴加速度
float acce_z; //z轴加速度
} mpu6050_acce_value_t;
typedef struct {
float gyro_x; //x轴的角速度
float gyro_y; //y轴的角速度
float gyro_z; //z轴的角速度
} mpu6050_gyro_value_t;
typedef struct {
float temp; //MPU6050温度
} mpu6050_temp_value_t;
(1)在官方MPU6050的组件中,你会看到很多TEST_ASSERT开头的函数,这个是Unity测试单元。作用类似于assert的断言,当测试结果失败,将会终止程序进行复位操作。
(2)GitHub链接:https://github.com/ThrowTheSwitch/Unity
(3)函数简单介绍:
<1>TEST_ASSERT_NOT_NULL_MESSAGE()
,用于测试传入的第一个参数是不是空指针,如果是空指针控制台将会输出第二个参数数据,并且终止程序。
<2>TEST_ASSERT_EQUAL()
,判断第二个参数是否和第一个参数相等。如果不相等将会终止程序,并且输出信息。
<3>TEST_ASSERT_EQUAL_MESSAGE()
,判断第二个参数是否和第一个参数相等。如果不相等将会输出第三个参数数据。
(1)如下为官方测试例程。
/*
* SPDX-FileCopyrightText: 2015-2021 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include
#include "unity.h"
#include "driver/i2c.h"
#include "mpu6050.h"
#include "esp_system.h"
#include "esp_log.h"
#define I2C_MASTER_SCL_IO 26 /*!< gpio number for I2C master clock */
#define I2C_MASTER_SDA_IO 25 /*!< gpio number for I2C master data */
#define I2C_MASTER_NUM I2C_NUM_0 /*!< I2C port number for master dev */
#define I2C_MASTER_FREQ_HZ 100000 /*!< I2C master clock frequency */
static const char *TAG = "mpu6050 test";
static mpu6050_handle_t mpu6050 = NULL;
/**
* @brief i2c master initialization
*/
static void i2c_bus_init(void)
{
i2c_config_t conf;
conf.mode = I2C_MODE_MASTER;
conf.sda_io_num = (gpio_num_t)I2C_MASTER_SDA_IO;
conf.sda_pullup_en = GPIO_PULLUP_ENABLE;
conf.scl_io_num = (gpio_num_t)I2C_MASTER_SCL_IO;
conf.scl_pullup_en = GPIO_PULLUP_ENABLE;
conf.master.clk_speed = I2C_MASTER_FREQ_HZ;
conf.clk_flags = I2C_SCLK_SRC_FLAG_FOR_NOMAL;
esp_err_t ret = i2c_param_config(I2C_MASTER_NUM, &conf);
TEST_ASSERT_EQUAL_MESSAGE(ESP_OK, ret, "I2C config returned error");
ret = i2c_driver_install(I2C_MASTER_NUM, conf.mode, 0, 0, 0);
TEST_ASSERT_EQUAL_MESSAGE(ESP_OK, ret, "I2C install returned error");
}
/**
* @brief i2c master initialization
*/
static void i2c_sensor_mpu6050_init(void)
{
esp_err_t ret;
i2c_bus_init();
mpu6050 = mpu6050_create(I2C_MASTER_NUM, MPU6050_I2C_ADDRESS);
TEST_ASSERT_NOT_NULL_MESSAGE(mpu6050, "MPU6050 create returned NULL");
ret = mpu6050_config(mpu6050, ACCE_FS_4G, GYRO_FS_500DPS);
TEST_ASSERT_EQUAL(ESP_OK, ret);
ret = mpu6050_wake_up(mpu6050);
TEST_ASSERT_EQUAL(ESP_OK, ret);
}
TEST_CASE("Sensor mpu6050 test", "[mpu6050][iot][sensor]")
{
esp_err_t ret;
uint8_t mpu6050_deviceid;
mpu6050_acce_value_t acce;
mpu6050_gyro_value_t gyro;
mpu6050_temp_value_t temp;
i2c_sensor_mpu6050_init();
ret = mpu6050_get_deviceid(mpu6050, &mpu6050_deviceid);
TEST_ASSERT_EQUAL(ESP_OK, ret);
TEST_ASSERT_EQUAL_UINT8_MESSAGE(MPU6050_WHO_AM_I_VAL, mpu6050_deviceid, "Who Am I register does not contain expected data");
ret = mpu6050_get_acce(mpu6050, &acce);
TEST_ASSERT_EQUAL(ESP_OK, ret);
ESP_LOGI(TAG, "acce_x:%.2f, acce_y:%.2f, acce_z:%.2f\n", acce.acce_x, acce.acce_y, acce.acce_z);
ret = mpu6050_get_gyro(mpu6050, &gyro);
TEST_ASSERT_EQUAL(ESP_OK, ret);
ESP_LOGI(TAG, "gyro_x:%.2f, gyro_y:%.2f, gyro_z:%.2f\n", gyro.gyro_x, gyro.gyro_y, gyro.gyro_z);
ret = mpu6050_get_temp(mpu6050, &temp);
TEST_ASSERT_EQUAL(ESP_OK, ret);
ESP_LOGI(TAG, "t:%.2f \n", temp.temp);
mpu6050_delete(mpu6050);
ret = i2c_driver_delete(I2C_MASTER_NUM);
TEST_ASSERT_EQUAL(ESP_OK, ret);
}