ESP32 - mpu6050
[toc]
硬件
引脚说明
引脚 | 说明 |
---|---|
VCC | 电源端,mpu6050自带稳压芯片,因此接受5V和3.3V供电 |
GND | 电源地 |
SCL | I2C时钟线 |
SDA | I2C数据线 |
XDA | 外接设备的数据线 |
XCL | 外接设备的时钟线 |
ADO | mpu6050设备地址控制,ADO=1,地址为0x69, ADO=0或者悬空,地址为0x68 |
INT | 中断引脚:一旦mpu6050有数据输出时,该引脚有相应的电平信号,可以用于保持MPU信号的实时性 |
和ESP32的连接
MPU6050 | ESP32 |
---|---|
VCC | 3.3V |
GND | GND |
SCL | 2 |
SDA | 18 |
XDA | 未接 |
XCL | 未接 |
ADO | 悬空 |
INT | 未接 |
I2C
I2C数据帧
- 起始位:SCL高电平,SDA下降沿;
- 设备地址:7位;
- 读写位:0:主设备向从设备写数据,1:主设备向从设备读数据;
- 应答信号:从设备应答,从设备正常应答时,SDA为低电平,从设备忙,SDA为高电平;
- 寄存器地址:8位;
- 应答信号:
- 数据:写入寄存器数据(8位);
- 停止位:SCL高电平,SDA上升沿;
ESP-idf提供I2C驱动程序,因此I2C的配置比较简单。
步骤:
- 配置驱动程序:设置初始化参数(如主机模式或从机模式,SDA 和 SCL 使用的 GPIO 管脚,时钟速度等),使用结构体
i2c_config_t conf
- 安装驱动程序:激活一个 I2C 控制器的驱动,该控制器可为主机也可为从机;使用函数
i2c_driver_install()
#define I2C_MASTER_SCL_IO 8 // 时钟线
#define I2C_MASTER_SDA_IO 18 // 数据线
#define I2C_MASTER_NUM 0 // I2C master i2c port number
#define I2C_MASTER_FREQ_HZ 400000 // I2C频率
#define I2C_MASTER_TX_BUF_DISABLE 0 /*!< I2C master doesn't need buffer */
#define I2C_MASTER_RX_BUF_DISABLE 0 /*!< I2C master doesn't need buffer */
esp_err_t i2c_master_init(void)
{
int i2c_master_port = I2C_MASTER_NUM;
i2c_config_t conf = {
.mode = I2C_MODE_MASTER,
.sda_io_num = I2C_MASTER_SDA_IO,
.scl_io_num = I2C_MASTER_SCL_IO,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.master.clk_speed = I2C_MASTER_FREQ_HZ,
};
i2c_param_config(i2c_master_port, &conf);
return i2c_driver_install(i2c_master_port, conf.mode, I2C_MASTER_RX_BUF_DISABLE, I2C_MASTER_TX_BUF_DISABLE, 0);
}
MPU6050的相关名词
-
陀螺仪:测量的是绕xyz轴转动的角速度,单位:Degree Per Second的缩写°/S ;
-
加速度:测量的是xyz方向受到的加速度,静止状态下,三个方向的合加速度为重力加速度;
-
DMP:Digital Motion Processor,直接输出四元数,并非完全开源;
-
四元数:四元数可以方便的表示3维空间的旋转。基本形式:$q_0+q_1i+q_2j+q_3*k$,一个实部和三个虚部;
-
欧拉角:pitch:俯仰(绕y轴旋转)、roll滚动(绕X轴旋转):、yaw:偏航(绕Z轴旋转)的表示形式,方便观察姿态。不过存在死锁现象;
MPU6050 配置
MPU6050配置主要根据数据手册配置相关寄存器来。
#define MPU_DEVICE_ID_REG 0X75 //器件ID寄存器
#define MPU_PWR_MGMT1_REG 0X6B //电源管理寄存器1
#define MPU_CFG_REG 0X1A //配置寄存器
#define MPU_SAMPLE_RATE_REG 0X19 //采样频率分频器
#define MPU_GYRO_CFG_REG 0X1B //陀螺仪配置寄存器
#define MPU_ACCEL_CFG_REG 0X1C //加速度计配置寄存器
/*读取设备ID,判断I2C通信是否正常*/
ESP_ERROR_CHECK(mpu6050_register_read(MPU_DEVICE_ID_REG, data, 1));
ESP_LOGI(TAG, "WHO_AM_I = %X", data[0]);
/*重置设备*/
ESP_ERROR_CHECK(mpu6050_register_write_byte(MPU_PWR_MGMT1_REG, 0x80));
vTaskDelay(100 / portTICK_PERIOD_MS);
/*禁用睡眠模式,选择X轴陀螺仪为时钟源*/
ESP_ERROR_CHECK(mpu6050_register_write_byte(MPU_PWR_MGMT1_REG, 0x01));
/*配置数字低通滤波器,带宽为5,陀螺仪输出频率为1kHz*/
ESP_ERROR_CHECK(mpu6050_register_write_byte(MPU_CFG_REG, 0x06));
/*配置采样频率:50Hz*/
ESP_ERROR_CHECK(mpu6050_register_write_byte(MPU_SAMPLE_RATE_REG, 19));
/*配置陀螺仪量程:± 2000 °/s, 不自检*/
ESP_ERROR_CHECK(mpu6050_register_write_byte(MPU_GYRO_CFG_REG, 0x18));
/*配置加速度计量程: ± 16g, 不自检*/
ESP_ERROR_CHECK(mpu6050_register_write_byte(MPU_ACCEL_CFG_REG, 0x18));
1 WHO_AM_I
读取mpu6050的ID;默认为0x68,可以用来验证IIC底层是否配置好。其最后一位由ADO决定。如果ADO=1,寄存器的的值为0x69,否则为0x68;
ESP_ERROR_CHECK(mpu6050_register_read(MPU_DEVICE_ID_REG, data, 1));
2 Power Management 1
配置电源和时钟源,用于重置设备和禁用温度传感器;
/*重置设备*/
ESP_ERROR_CHECK(mpu6050_register_write_byte(MPU_PWR_MGMT1_REG, 0x80));
vTaskDelay(100 / portTICK_PERIOD_MS);
/*禁用睡眠模式,选择X轴陀螺仪为时钟源*/
ESP_ERROR_CHECK(mpu6050_register_write_byte(MPU_PWR_MGMT1_REG, 0x01));
- 参数:
参数 | 描述 |
---|---|
DEVICE_RESET | 为1:所有寄存器重置为默认值 |
SLEEP | 为1:进入休眠模式 |
CYCLE | SLEEP=0 && CYCLE = 1 MPU在休眠和唤醒中循环,循环周期由LP_WAKE_CTRL(寄存器108)来确定 |
TEMP_DIS | 为1:禁用温度传感器 |
CLKSEL | 指定时钟源 |
CLKSEL | 描述 |
---|---|
0 | 使用内部时钟源:8MHz |
1 | X轴陀螺仪作为参考(一般选择) |
2 | y轴陀螺仪作为参考 |
3 | Z轴陀螺仪作为参考 |
4 | 使用外部时钟源:32.768MHz |
5 | 使用外部时钟源:19.2MHz |
6 | 保留 |
7 | 停用时钟源,定时器复位 |
一般配置:
- 重置寄存器,Bit7 = 1,所以,
Power_Management_1 = 0x80
; - 解除休眠,时钟源设置为X轴,Bit0=1:
Power_Management_1 = 0x01
;
注意:重置寄存器后SLEEP自动为1, 所以必须解除休眠,否则没有数据输出
3 Configuration
/*配置数字低通滤波器,带宽为5,陀螺仪输出频率为1kHz*/
ESP_ERROR_CHECK(mpu6050_register_write_byte(MPU_CFG_REG, 0x06));
主要用于配置数字低通滤波器(DLPF_CFG),确定陀螺仪的输出频率。
如果启用数字低通滤波器(DLPF_CFG != 0 orDLPF_CFG != 7
),则陀螺仪的采样频率只能为1KHz,
配置:
- Bit2 =1 and Bit1 = 1,所以,
Configuration = 0x06
4 SMPRT_DIV
/*配置采样频率:50Hz*/
ESP_ERROR_CHECK(mpu6050_register_write_byte(MPU_SAMPLE_RATE_REG, 19));
设置传感器的采样速率
$$
采样率 = \frac{陀螺仪输出频率}{(1 + SMPLRT_DIV)}
$$
陀螺仪的输出频率由DLPF_CFG (寄存器26)进行配置,目前陀螺仪输出频率配置为1KHz。
配置:
- 采样速率配置为50Hz,由$50 = \frac{1000}{(1 + SMPLRT_DIV)}$知,
SMPRT_DIV = 19
5 Gyroscope Configuration
是否启用自检和设置陀螺仪量程
/*配置陀螺仪量程:± 2000 °/s, 不自检*/
ESP_ERROR_CHECK(mpu6050_register_write_byte(MPU_GYRO_CFG_REG, 0x18));
:
参数 | 描述 |
---|---|
XG_ST | 为1,启动X轴陀螺仪自检 |
YG_ST | 为1,启动y轴陀螺仪自检 |
ZG_ST | 为1,启动Z轴陀螺仪自检 |
FS_SEL | 设置陀螺仪量程 |
自检:简单理解为重新设置零位(有可能理解不对)。
量程设置
FS_SEL | 满量程 | LSB灵敏度 |
---|---|---|
0 | ± 250 °/s | 131 LSB/°/s |
1 | ± 500 °/s | 65.5 LSB/°/s |
2 | ± 1000 °/s | 32.8 LSB/°/s |
3 | ± 2000 °/s | 16.4 LSB/°/s |
设置:
- 一般设置为不自检,最大满量程:Bit4 = 1 & Bit3 = 1, 所以
Gyroscope Configuration = 0x18
6 Accelerometer Configuration
/*配置加速度计量程: ± 16g, 不自检*/
ESP_ERROR_CHECK(mpu6050_register_write_byte(MPU_ACCEL_CFG_REG, 0x18));
- 描述:配置加速度是否自检和量程范围
参数 | 描述 |
---|---|
XA_ST | 为1,启动X轴加速度自检 |
YA_ST | 为1,启动y轴加速度自检 |
ZA_ST | 为1,启动Z轴加速度自检 |
AFS_SEL | 设置加速度量程 |
量程设置
AFS_SEL | 满量程 | LSB灵敏度 |
---|---|---|
0 | ± 2g | 16384 LSB/mg |
1 | ± 4g | 8192 LSB/mg |
2 | ± 8g | 4096 LSB/mg |
3 | ± 16g | 2048 LSB/mg |
设置:
- 一般设置为不自检,最大满量程:Bit4 = 1 & Bit3 = 1, 所以
Gyroscope Configuration = 0x18
读取原始数据
以下代码读取温度,陀螺仪,加速度计的原始数据
/**
* @brief 读取温度值
*/
esp_err_t mpu6050_get_temperature(float *temp)
{
int ret;
uint8_t data[2];
int16_t raw;
ret = mpu6050_register_read(MPU_TEMP_OUTH_REG, data, 2);
raw = (int16_t)((data[0] << 8) + data[1]);
*temp = 36.53 + raw/340.0;
return ret;
}
/**
* @brief 读取陀螺仪值
*/
esp_err_t mpu6050_get_gyroscope(float *gxyz)
{
int ret;
uint8_t data[6];
int16_t raw_gxyz[3];
float LSB[4] = {131.0, 65.5, 32.8, 16.4};
/*读取原始数据*/
ret = mpu6050_register_read(MPU_GYRO_XOUTH_REG, data, 6);
if (ret == ESP_OK)
{
/*转化原始数据*/
raw_gxyz[0] = (int16_t)((data[0] << 8) + data[1]);
raw_gxyz[1] = (int16_t)((data[2] << 8) + data[3]);
raw_gxyz[2] = (int16_t)((data[4] << 8) + data[5]);
}
/*读取量程*/
ret = mpu6050_register_read(MPU_GYRO_CFG_REG, data, 1);
if (ret == ESP_OK)
{
uint8_t a = (data[0] & 0X18) >> 3;
printf("ga = %d\n", a);
// 实际值
for (uint8_t i = 0; i < 3; i++)
{
gxyz[i] =raw_gxyz[i] / LSB[a];
}
}
return ret;
}
/**
* @brief 读取加速度值
*/
esp_err_t mpu6050_get_accelerometer(float *axyz)
{
int ret;
uint8_t data[6];
int16_t raw_axyz[3];
float LSB[4] = {16384.0, 8192.0, 4096.0, 2048.0};
/*原始值*/
ret = mpu6050_register_read(MPU_ACCEL_XOUTH_REG, data, 6);
if (ret == ESP_OK)
{
raw_axyz[0] = (int16_t)((data[0] << 8) + data[1]);
raw_axyz[1] = (int16_t)((data[2] << 8) + data[3]);
raw_axyz[2] = (int16_t)((data[4] << 8) + data[5]);
}
/*读取量程*/
ret = mpu6050_register_read(MPU_ACCEL_CFG_REG, data, 1);
if (ret == ESP_OK)
{
uint8_t a = (data[0] & 0X18) >> 3 ;
printf("aa = %d\n", a);
/*转化为实际值*/
for (uint8_t i = 0; i < 3; i++)
{
axyz[i] =raw_axyz[i] / LSB[a];
}
}
return ret;
}
7 Temperature Measurement
读取温度的值
函数esp_err_t mpu6050_get_temperature(float *temp)
寄存器
每个数据由两个寄存器组成,数据更新速度由寄存器25的采样速度控制。
和实际温度的转化关系: *temp = 36.53 + raw/340.0;
没有在手册中查到,抄别人的代码。
8 Gyroscope Measurements
读取陀螺仪的数据
esp_err_t mpu6050_get_gyroscope(float *gxyz)
寄存器:
每个数据由两个寄存器组成,数据更新速度由寄存器25的采样速度控制
由于XYZ的数据连续,所以直接读了6个值,并根据LSB转化为实际值。
9 Accelerometer Measurements
读取加速度的值
esp_err_t mpu6050_get_accelerometer(float *axyz)
每个数据由两个寄存器组成,数据更新速度由寄存器25的采样速度控制。
由于XYZ的数据连续,所以直接读了6个值,并根据LSB转化为实际值。
其他自认为有用的寄存器
9 FIFO Enable
- 描述:决定那些寄存器的数据会被放入缓冲器中
参数 | 描述 |
---|---|
TEMP_FIFO_EN | 温度传感器 |
XG_FIFO_EN、YG_FIFO_EN、ZG_FIFO_EN | 陀螺仪输出的寄存器 |
ACCEL_FIFO_EN | 加速度输出的寄存器 |
... | 后面三个未做了解 |
10 Interrupt Enable
- 描述,是否是能相应中断
11 User Control
- 描述:配置是否启用FIFO,IIC的主从,重置FIFO和传感器
参数 | 描述 |
---|---|
FIFO_EN | 使能FIFO |
I2C_MST_EN | 是能i2c为主模式 |
I2C_IF_DIS | 禁用I2c,启用SPI |
FIFO_RESET | 重置FIFO |
I2C_MST_RESET | 重置I2C主模式,重置以后自动变为0 |
SIG_COND_RESET | 重置所有传感器 |
读取结果
将MPU6050静止平放在桌面上,获取连续3秒的数据
while (1)
{
ESP_ERROR_CHECK(mpu6050_get_temperature(&temp));
ESP_LOGI(TAG, "temp = %.2f", temp);
ESP_ERROR_CHECK(mpu6050_get_gyroscope(gxyz));
ESP_LOGI(TAG, "gx = %.2f\t gy = %.2f\t gz = %.2f", gxyz[0], gxyz[1], gxyz[2]);
ESP_ERROR_CHECK(mpu6050_get_accelerometer(axyz));
ESP_LOGI(TAG, "ax = %.2f\t ay = %.2f\t az = %.2f", axyz[0], axyz[1], axyz[2]);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
读取周期为1S
I (6415) mpu6050-example: temp = 25.52
I (6415) mpu6050-example: gx = -4.21 gy = -0.49 gz = -1.59
I (6415) mpu6050-example: ax = 0.11 ay = 0.03 az = 1.03
I (7415) mpu6050-example: temp = 25.55
I (7415) mpu6050-example: gx = -4.21 gy = -0.49 gz = -1.59
I (7415) mpu6050-example: ax = 0.11 ay = 0.03 az = 1.04
I (8415) mpu6050-example: temp = 25.55
I (8415) mpu6050-example: gx = -4.21 gy = -0.49 gz = -1.59
I (8415) mpu6050-example: ax = 0.11 ay = 0.03 az = 1.04
定性分析:
- 温度:芯片摸着不发烫,25度左右算正常;
- 陀螺仪:理论上禁止,陀螺仪的数据应该为0,但是实际上存在一点数据,原因为止;
- 加速度计:静止时,加速度的合力应该为9.8,目前合力估计为10左右,说明读取的数据基本正常。
从定性分析的角度来说,根据datasheed配置应该没有错。
下一步计划
原始数据意义不大,纯粹用于学习。后面将进一步融合获取欧拉角。
- 使用MDP融合;
- 使用卡尔曼滤波融合;
DMP移植
上次通过直接读取寄存器的方式获取到mpu6050的温度、陀螺仪和加速度的数据,但是mpu6040一般使用欧拉角和四元数表示姿态。mpu6050通过自带的DMP(DigitalMotion Processor),可以在减轻控制器的计算压力下,轻松得到姿态数据。DMP的移植无论是网上的讲解还是github上的代码,都挺丰富的。基本上能做到开箱就用。原先我也是直接使用现成的代码。但是呢,目前想从头开始,想体验下从最初的源码开始移植是怎么样的。
DMP简介
DMP就是MPU6050内部的运动引擎,由Invensense提供。支持设备有MPU6050、MPU6500、MPU9150、MPU9250
能实现的功能有:
- 3 轴四元数:仅仅使用陀螺仪数据计算;
- 6轴四元数:使用陀螺仪和加速度融合计算。不能和3 轴四元数同时开启;
- 手势识别 : 使用传感器数据来检测设备方向是否有纵向、横向、反向纵向和反向方向的变化。很大程度上取决于方向矩阵;
- 点击手势识别 -:检测设备上的多方向点击检测。此功能将检测设备在哪个轴上发生敲击事件它可以检测最多 4 次的多次抽头。API 可以配置此功能的阈值、死区时间和点击次数;
- 计步器手势识别 :提供步数和时间戳。此功能会自动启用,但只有在检测到 5 秒的连续脚步时才会触发。;
- DMP 中断 :可以将中断配置为在传感器数据准备就绪时或检测到点击或方向手势;
移植
既然是从头开始,就要从源码开始,本次使用Embedded MotionDriver 5.1,手中没有磁力计或者mpu9250等带有磁力计的mpu,所以就从5.1版本开始。另一个原因是Embedded MotionDriver 6没有找到最初的源码。
源码结构
+---core
| \---driver
| +---eMPL
| | dmpKey.h // 对于DMP实现各种功能的内存定义
| | dmpmap.h // DMP 内存定义
| | inv_mpu.c // 驱动程序,用于适配微控制器
| | inv_mpu.h
| | inv_mpu_dmp_motion_driver.c // DMP 加载和配置相关的API
| | inv_mpu_dmp_motion_driver.h
| |
| \---msp430
|
\---simple_apps
\---msp430
| motion_driver_test.c // 实现demo(移植参考的最原版本)
|
\---motion-driver-client // 用python写的上位机程序,和motion_driver_test.c适配
从源码的结构可以看出eMPL
中的文件是DMP的核心代码,motion_driver_test.c
是运用DMP的参考代码。
另外,还有一份文档,是参考之一:embedded-motion-5-1-1-tutorial.pdf
移植思路
- 在工程中新建
components
用于存放esp32自定义组件,组件名称为mpu6050DMP
。esp32构建规定,工程目录下components
文件夹中的组件能自动被依赖,可以简化CMakeCMakeLists.txt
的书写; - 将
eMPL
放在mpu6050DMP
中; - 在
mpu6050DMP
中新建mpu6050.c
和mpu6050.h
文件,用于组合DMP和esp32; inv_mpu.c
是用于适配微控制器的,因此从该文件的注释可以了解到,我们需要提供以下函数:- i2c读函数:
i2c_write()
- i2c写函数:
i2c_read()
- 延迟函数:
delay_ms()
- 获取毫秒函数:
get_ms()
- 回调函数:
reg_int_cb()
- i2c读函数:
- 编译,修正编译错误
motion_driver_test.c
是demo函数,所以参考该文件实现DMP的配置;- 将DMP计算出来的四元数转化为欧拉角;
- 编译,保证获取数据;
移植
1、 补充相关函数
通过inv_mpu.c
中的注释可知,需要补充以下四个函数:
i2C写函数:i2c_write(unsigned char slave_addr, unsigned char reg_addr,unsigned char length, unsigned char const *data)
和i2C读函数: i2c_read(unsigned char slave_addr, unsigned char reg_addr, unsigned char length, unsigned char *data)
- slave_addr:从机地址;
- reg_addr:寄存器地址;
- length:读/写长度;
- data:读/写的数据指针;
/**
* @brief 写数据
*/
esp_err_t esp32s3_i2c_write_bytes(uint8_t slave_addr, uint8_t reg_addr, uint8_t length, uint8_t *data)
{
int ret;
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
ret = i2c_master_write_byte(cmd, (slave_addr << 1) | I2C_MASTER_WRITE, 1);
if (ret != ESP_OK)
return ESP_FAIL;
ret = i2c_master_write_byte(cmd, reg_addr, 1);
if (ret != ESP_OK)
return ESP_FAIL;
ret = i2c_master_write(cmd, data, length, 1);
if (ret != ESP_OK)
return ESP_FAIL;
i2c_master_stop(cmd);
i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, I2C_MASTER_TIMEOUT_MS / portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd);
return ret;
}
/**
* @brief 读数据
*
*/
esp_err_t esp32s3_i2c_read_bytes(uint8_t slave_addr, uint8_t reg_addr,uint8_t length, uint8_t *data)
{
int ret;
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
ret = i2c_master_write_byte(cmd, (slave_addr << 1) | I2C_MASTER_WRITE, 1);
if(ret != ESP_OK)
return ESP_FAIL;
ret = i2c_master_write_byte(cmd, reg_addr, 1);
if(ret != ESP_OK)
return ESP_FAIL;
i2c_master_start(cmd);
ret = i2c_master_write_byte(cmd, (slave_addr << 1) | I2C_MASTER_READ, 1);
if(ret != ESP_OK)
return ESP_FAIL;
ret = i2c_master_read(cmd, data, length, I2C_MASTER_LAST_NACK);
if(ret != ESP_OK)
return ESP_FAIL;
i2c_master_stop(cmd);
i2c_master_cmd_begin(I2C_NUM_0, cmd, I2C_MASTER_TIMEOUT_MS/portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd);
return ret;
}
实现主要参照esp32d的在线文档;
延迟函数:delay_ms(unsigned long num_ms)
- num_ms:延迟描秒数指针;
int esp32s3_delay_ms(unsigned long num_ms)
{
vTaskDelay(num_ms / portTICK_PERIOD_MS);
return 0;
}
实现基于vTaskDelay()
函数;
时钟函数: get_ms(unsigned long *count)
- count:毫秒数;
int esp32s3_get_clock_ms(unsigned long *count)
{
*count = xTaskGetTickCount() * 10;
return 0;
}
实现基于获取时钟节拍函数,再乘以时钟周期(10ms)
回调函数舍弃
最后通过宏定义指向:
/*定义传感器*/
#define MPU6050
/*定义单片机*/
#define MOTION_DRIVER_TARGET_ESP32S3
#if defined MOTION_DRIVER_TARGET_ESP32S3
#include <stdio.h>
#include "esp_log.h"
#include "driver/i2c.h"
#include "../mpu6050.h"
/*映射函数*/
#define i2c_write esp32s3_i2c_write_bytes
#define i2c_read esp32s3_i2c_read_bytes
#define delay_ms esp32s3_delay_ms
#define get_ms esp32s3_get_clock_ms
#define log_i printf
#define log_e printf
#define min(a,b) ((a<b)?a:b)
#endif
2 编译,修错
错误1:error: implicit declaration of function '__no_operation' [-Werror=implicit-function-declaration]
-
分析:
__no_operation
为停止一个周期,esp32中没有。可以忽视; -
处理:直接屏蔽;
错误2:error: implicit declaration of function 'reg_int_cb' [-Werror=implicit-function-declaration]
- 分析:没有提供回调函数导致;
- 处理:直接屏蔽;
到此为止,移植结束,代码可以正常编译了
应用
MDP的应用需要好好参考motion_driver_test.c
文件,大致思路如下:
- 初始化I2C;
- 初始化mpu6050部分寄存器:
mpu_init()
; - 唤醒mpu6050:
mpu_set_sensors()
- 将需要的数据放在FIFO中:
mpu_configure_fifo()
- 定义采样频率:
mpu_set_sample_rate()
- 记载内存:
dmp_load_motion_driver_firmware
- 设置方向(坐标系):
dmp_set_orientation()
- 使能MDP的功能:
dmp_enable_feature()
- 启用MDP:
mpu_set_dmp_state()
- 周期读取四元数:
dmp_read_fifo()
- 将四元数转化为欧拉角。
MDP初始化函数:
esp_err_t MDP_init(void)
{
int ret;
/*MPU6050初始化*/
ret = mpu_init(NULL);
if (ret != 0)
printf("0, %d\n", ret);
/*唤醒*/
ret = mpu_set_sensors(INV_XYZ_GYRO | INV_XYZ_ACCEL);
/*将加速度和陀螺仪数据放入FIFO中*/
ret=mpu_configure_fifo(INV_XYZ_GYRO | INV_XYZ_ACCEL);
/*设置采样频率*/
mpu_set_sample_rate(100);
/*加载内存*/
dmp_load_motion_driver_firmware();
/*设置方向*/
dmp_set_orientation(inv_orientation_matrix_to_scalar(gyro_orientation));
/*启用功能
* DMP_FEATURE_6X_LP_QUAT:产生四元数
* DMP_FEATURE_TAP:检测敲击事件
* DMP_FEATURE_ANDROID_ORIENT:实现了与Google Motion_driver设备兼容的显示方向
* DMP_FEATURE_SEND_RAW_ACCEL:计步器功能
* DMP_FEATURE_SEND_CAL_GYRO:设备处于无运动状态超过8秒,就会校准陀螺仪偏置
* DMP_FEATURE_GYRO_CAL:将原始加速度计数据添加到FIFO
*/
unsigned short dmp_features = DMP_FEATURE_6X_LP_QUAT | DMP_FEATURE_TAP |
DMP_FEATURE_ANDROID_ORIENT | DMP_FEATURE_SEND_RAW_ACCEL | DMP_FEATURE_SEND_CAL_GYRO |
DMP_FEATURE_GYRO_CAL;
dmp_enable_feature(dmp_features);
dmp_set_fifo_rate(100);
if (ret != 0)
printf("6, %d\n", ret);
run_self_test();
/*启用DMP*/
ret = mpu_set_dmp_state(1);
if (ret != 0)
printf("7, %d\n", ret);
return ret;
}
周期读取和转化:
if(dmp_read_fifo(gyro, accel, quat, &sensor_timestamp, &sensors, &more) == ESP_OK)
{
if(sensors & INV_WXYZ_QUAT)
{
/*转化为欧拉角*/
q0 = quat[0] / q30;
q1 = quat[1] / q30;
q2 = quat[2] / q30;
q3 = quat[3] / q30;
pitch = asin(-2 * q1 * q3 + 2 * q0* q2)* 57.3; // pitch
roll = atan2(2 * q2 * q3 + 2 * q0 * q1, -2 * q1 * q1 - 2 * q2* q2 + 1)* 57.3; // roll
yaw = atan2(2*(q1*q2 + q0*q3),q0*q0+q1*q1-q2*q2-q3*q3) * 57.3; //yaw
}
}
姿态数据可视化
上位机主要使用匿名上位机,通讯方式采用UDP, mpu6050为一个任务,给UDP发送数据为一个任务,两个任务之间通过队列进行数据传输:
![流程图](studying/esp32/../../picture/esp32-mpu6050/流程图.png)
匿名上位机对于欧拉角的传输协议如下:
对于mpu6050_task
任务,主要是将姿态数据扩大100。
/*发送数据*/
mpu6050_data[0] = (int16_t)(roll * 100);
mpu6050_data[1] = (int16_t)(pitch * 100);
mpu6050_data[2] = (int16_t)(yaw * 100);
对于udp_client_task
,按照协议封装数据,并通过UDP发送数据
/**
* 发送给匿名上位机(欧拉角)
*/
void tranportdata(int16_t *esp23data, uint8_t *pcdata)
{
pcdata[11] = 0;
pcdata[12] = 0;
pcdata[0] = 0XAA;
pcdata[1] = 0XFF;
pcdata[2] = 0X03;
pcdata[3] = 7;
for (uint8_t i = 0; i < 3; i++)
{
pcdata[4 + 2*i] = esp23data[i] & 0x00ff;
pcdata[4 + 2*i + 1] = esp23data[i] >> 8;
}
/*融合状态*/
pcdata[10] = 1;
/*校验位*/
for (uint8_t i = 0; i < pcdata[3]+4; i++)
{
pcdata[11] += pcdata[i];
pcdata[12] += pcdata[11];
}
}
/*封装数据*/
tranportdata(mpu6050_data, euler_esp32_to_pc);
// printf("%d\n", mpu6050_data[1]);
/*发送*/
int err = sendto(sock, euler_esp32_to_pc, 13, 0, (struct socka
每个任务周期为10为10毫秒,通过上位机发现数据发送频率为101Hz,说明数据发送漏包情况不严重。
结果:
总结
- 由于MDP给msp430提供比较完整的demo,所以移植起来比较顺利,由于msp430采用中断接收数据,而我目前采用周期轮询,因此注意轮询的周期,如果周期较大,会出现MDP由于FIFO满,导致自动清除缓存数据,使
dmp_read_fifo
没有数据可读。 - 还有就是,在可视化情况下,当mpu6050静止时,数据传输比较稳定,但是一旦动起来,会出现卡顿的现象,原因未知。
MCU和PC通信:浮点数传输
[toc]
上次做mpu6050可视化时,匿名上位机对于浮点的处理为扩大100倍,然后转化为整数进行传输。那是就疑惑,扩大一定倍数,然后转化为整数,相当于牺牲了float数的精度,当然,对于一般的场景,对于float的精度要求并不高。但是,是否有不影响float的精度的条件下,完成float的数据传输?
发送float的核心在于如何将表示float的四个字节提取出来,如何在PC端将四个字节还原成float?
flaot 发送的常见两种方案
当然有了,以下两种方案比较常见
- 使用
union
:union
中的成员共享内存,如果union
中的成员为uint8 u8data[4]
和float fdata
,我们对于fdata
成员赋值以后,float
的四个字节存储在u8data
数组中,在发送时只需要发送u8data[4]
,自然可以把表示floatd的四个字节发送出去;
/*声明一个共用体*/
typedef union
{
uint8_t u8data[4];
float fdata;
}float_to_u8;
/*定义*/
float_to_u8 data;
/*写入float*/
data.fdata = 12.375;
/*发送flaot*/
sendto(..., data.u8data, sizof(data.u8data), ...)
- 使用指针:float占用四个字节,定义一个指针
uint8 *p
, 让指针p
指向flaot的首地址,然后不断移动指针,就可以依次读取flaotd的四个字节;
/*定义一个flaot*/
flaot data = 12.375;
/*定义一个指针*/
uint8 *p;
/*指针指向data的地址*/
p = &data;
/*发送*/
for(int i; i< sizeof(float); i++)
{
sendto(.., *(p + i), 1, ...)
}
flaot传输测试
测试使用union
方案
MCU和PC的传输方式使用udp,MCU使用esp32s3,PC端的数据解析使用python
传输的数据为3个flaot,模拟mpu6050 的欧拉角传输
ESP32 端:打包四字节
/*
* 声明共用体
*/
typedef union
{
uint8_t u8data[4];
float fdata;
}float_to_u8;
/*定义共用体数组*/
float_to_u8 euler_mpu[3];
/*初值*/
euler_mpu[0].fdata = 0.0001;
euler_mpu[1].fdata = 1.1234;
euler_mpu[2].fdata = 123.123;
while()
{
*发送*/
sendto(sock, euler_mpu, sizeof(euler_mpu), 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
/*修改值*/
euler_mpu[0].fdata += 0.0001;
euler_mpu[1].fdata += 0.1;
euler_mpu[2].fdata += 100;
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
PC端:解析四字节
import socket
import struct
def main():
udp_addr = ('192.168.7.7', 8888)
# UDP通信类型
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_socket.bind(udp_addr)
while True:
# 等待接收数据
buff, addr = udp_socket.recvfrom(1024)
print(f'[From {addr[0]}:{ addr[1]}]:resive {len(buff)} bytes')
# 判断数据是否为12个
if len(buff) == 12:
data = struct.unpack('<fff', buff)
print(data)
print()
if __name__ == '__main__':
main()
解析使用struct
类进行。
data = struct.unpack('<fff', buff)
struct.unpack
:将字节流解析成相应格式化的数据;<fff
:>
表示数据为大端模式,f
表示解析成float型;
结果:
[From 192.168.7.2:58248]:resive 12 bytes
(9.999999747378752e-05, 1.1233999729156494, 123.12300109863281)
From 192.168.7.2:58248]:resive 12 bytes
(0.00019999999494757503, 1.2233999967575073, 223.1230010986328)
[From 192.168.7.2:58248]:resive 12 bytes
(0.00029999998514540493, 1.3234000205993652, 323.12298583984375)
[From 192.168.7.2:58248]:resive 12 bytes
(0.00039999998989515007, 1.4234000444412231, 423.12298583984375)
[From 192.168.7.2:58248]:resive 12 bytes
(0.0004999999655410647, 1.523400068283081, 523.1229858398438)
内容扩展
float的传输过程挺简单,但是存在三个问题:
- float数据在计算机的存储方式是什么?
- 数据的大小端模式?
struct
类的其他用法?
float的存储方式
存储原理
在C中,浮点类型的数据采用单精度类型float
和双精度类型double
来存储,float数据占用32bit,double数据占用64bit。float遵从的是IEEE R32.24 规范,而double 遵从的是R64.53规范。
无论是单精度还是双精度在存储中都分为三个部分:
- 符号位(Sign):0代表正,1代表负;
- 指数位(Exponent):用于存储科学计数法中的指数数据,并且采用移位存储;float的指数位为8,double的指数位11
- 尾数部分(Mantissa):尾数部分;float的尾数位为23,double的尾数位为52
上面所说的科学计数法指二进制的下的指数计数法;
上面所说的移位存储是因为指数有正有负,以float来说,其指数范围为
-127 - 128
,因此存储的数据为原数据+127
demo
假如一个float
数为$12.375$(不要选择不能精准转化为二进制的数)
步骤:
- 将十进制的数转化为二进制的数;
- 将二进制的数用科学计数法表示为$1.M\times2^E$;
- 将指数$E+127$,表示指数位;
- $M$则是尾数部分,不足23位补零;
对于$12.375$来说,转为二进制:$1100.011$
对于整数部分:除2取余,逆序排列;
对于小数部分:乘2取整,顺序排列
使用科学计数法为$1.100011\times2^3$,
对于指数$3$,移位存储:,指数位的值为3+127=130
,转化为二进制为$1000 \ 0010$
尾数部分则为$1000 \ 1100 \ 0000 \ 0000 \ 0000 \ 000$
符号位正,所以符号位为0;
所以$12.375$的在存储为$0 \ 10000010 \ 10001100000000000000000$
通过网上的在线工具查询,$12.375$d的二进制计算正确
代码测试(PC)
#include"stdio.h"
#include <stdlib.h>
int main()
{
float fdata = 13.375;
unsigned char *p;
p = (unsigned char *)&fdata;
for (int i = 0; i < 4; i++)
{
printf("0x%x\n", *(p+i));
}
printf("\n");
return 0;
}
输出:
0x0 0x0 0x46 0x41
和代码测试和网上在线工具测试的不一致,是因为数据字序存在大小端之分。
大小端模式
int和float都是4个字节,那自然就存在一个问题,即是高字节在前还是低字节在前面。因此就出现两种字序,大端模式和小端模式。
- 大端模式:数据的低位存在内存的高地址中,而数据的高位存在内存的低地址中。
- 小端模式:数据的低位存在内存的低地址中,而数据的高位存在内存的高地址中。
在X86电脑上验证
int main()
{
int fdata = 0x12345678;
unsigned char *p;
// 指针p指向fdata的地址
p = (unsigned char *)&fdata;
printf("十六进制: 0x%x\n", fdata);
for (int i = 0; i < 4; i++)
{
printf("第%d寄存器地址: %p, 值: 0x%x\n", i, p+i, *(p+i));
}
printf("\n");
}
输出:
六进制: 0x12345678
第0寄存器地址: f99c22e8, 值: 0x78
第1寄存器地址: f99c22e9, 值: 0x56
第2寄存器地址: f99c22ea, 值: 0x34
第3寄存器地址: f99c22eb, 值: 0x12
可知,x86为的字序为大端模式。
用相同的代码在在esp32上验证:
六进制: 0x12345678
第0寄存器地址: 0x3fc996a0, 值: 0x78
第1寄存器地址: 0x3fc996a1, 值: 0x56
第2寄存器地址: 0x3fc996a2, 值: 0x34
第3寄存器地址: 0x3fc996a3, 值: 0x12
可知,esp32为的字序为大端模式。
如果再去查STM32,它应该是小端模式。
python struct模块
struct
模块提供了用于在字节字符串和Python原生数据类型之间转换函数。Struct
支持将数据packing(打包)成字节字符串,并能从字节字符串中逆向unpacking(解压)出数据。
它具有以下功能:
- 按照指定格式将Python数据转换为字节字符串,该字符串为字节流。如网络传输时,不能传输int,此时先将int转化为字节流,然后再发送;
- 按照指定格式将字节流转换为Python指定的数据类型;
相关函数如下:
函数 | 返回值 | 说明 |
---|---|---|
pack(fmt,v1,v2…) | string | 按照给定的格式(fmt),把数据转换成字符串(字节流),并将该字符串返回. |
unpack(fmt,v1,v2…..) | tuple | 按照给定的格式(fmt)解析字节流,并返回解析结果 |
calcsize(fmt) | size of fmt | 计算给定的格式(fmt)占用多少字节的内存,注意对齐方式 |
还有两个函数,因为用得不多,没有列
其中所谓的指定格式有格式符号和对其方式组成
格式符号:
格式 | C 类型 | Python 类型 | 标准大小 |
---|---|---|---|
x | 填充字节 | 无 | |
c | char | 长度为 1 的字节串 | 1 |
b | signed char | 整数 | 1 |
B | unsigned char | 整数 | 1 |
? | _Bool | bool | 1 |
h | short | 整数 | 2 |
H | unsigned short | 整数 | 2 |
i | int | 整数 | 4 |
I | unsigned int | 整数 | 4 |
l | long | 整数 | 4 |
L | unsigned long | 整数 | 4 |
q | long long | 整数 | 8 |
Q | unsigned long long | 整数 | 8 |
n | ssize_t | 整数 | |
N | size_t | 整数 | |
e | (6) | float | 2 |
f | float | float | 4 |
d | double | float | 8 |
s | char[] | 字节串 | |
p | char[] | 字节串 | |
P | void* | 整数 |
对齐方式:
Character | Byte order | Size | Alignment |
---|---|---|---|
@ | native(本机) | native | native,凑够四字节 |
= | native(本机) | standard | none,按照原字节 |
< | little-endian(小端) | standard | none,按照原字节 |
> | big-endian(大端) | standard | none,按照原字节 |
! | network (= big-endian) | standard | none,按照原字节 |
测试:将$12.375$打包字符串,再将字符串解包为float数。
if __name__ == '__main__':
# 格式化为字符串
a = 12.375
print(f"原始浮点数: {a}")
print(f"'>f'格式占用字节数: {struct.calcsize('>f')}")
buff = struct.pack('>f', 12.375)
# 字符串格式不容易看懂,转化为16进制
print("16进制:", end="\t")
for data in buff:
print(hex(data), end="\t")
print()
# 将字符串格式为float(大端模式)
print("字符串转float:", end="\t")
a = struct.unpack('>f', buff)[0]
print(a)
输出
原始浮点数: 12.375
'>f'格式占用字节数: 4
16进制: 0x41 0x46 0x0 0x0
字符串转float: 12.375
小结
- 当然,无论MCU和谁通信,一般对方都规定了通信协议,因此我们只要按照通信协议书写就行。之所以研究研究float的数据传输,一方面是想复习下float的存储方式和大小端。另一方面,想自己在PC端可视化数据。因为只有自由度原越高,才能学习到更多东西,以后的复用性越强。
下一步,想在PC上可视化欧拉角数据,欧拉角额理论传输速率为100HZ,所以得保证数据的曲线的流畅度。不知道基于python
是否有推荐的库?
esp32-debug
.vscode/launch.json
配置文件
example
{
"version": "0.2.0",
"configurations": [
{
"type": "espidf",
"name": "Launch",
"request": "launch",
"debugPort": 9998,
"logLevel": 2,
"mode": "manual",
"verifyAppBinBeforeDebug": false,
"tmoScaleFactor": 1,
"initGdbCommands": [
"target remote :3333",
"symbol-file /path/to/program.elf",
"mon reset halt",
"flushregs",
"thb app_main"
],
"env": {
"CUSTOM_ENV_VAR": "SOME_VALUE"
}
}
]
}
type
:只能是espidf
;name
:调试名称,不重要request
debugPort
:调试器的端口,默认43474logLevel
:日志级别,默认为2mode
参数为auto
和manual
,auto
自动启动openOCD服务器,manual
手动启动
verifyAppBinBeforeDebug
:默认为false
,调试只能调用工程下的可执行文件,改为ture
,可以手动指定可执行文件tmoScaleFactor
:超时时间initGdbCommands
env
:设置环境变量
ESP32S3(ESP-idf v5.1)移植lvgl(v8.3)
[toc]
前言
现在开始学习ESP32了,所以在网上买了块ESP32s3的开发板,点完灯以后的首要事则是两点亮屏幕,在点亮屏幕的过程中,发现lvgl仓库中提供的lvgl_esp32_drivers
并不能顺利点亮屏幕,原因在于ESP-idf和lvgl更新的太快,导致有的函数不在适用,lvgl_esp32_drivers
不适用。本文将使用最新的ESP-idf(v5.1)和lvgl(v8.3)点亮屏幕。
准备工作
硬件
没有使用官方的(有点小贵),顺便买个平替版。
屏幕使用一款TFT屏幕,分辨率为240*240,通信方式为SPI,驱动为ST7789。没有触摸芯片
环境
使用vscode插件:vscode-esp-idf-extension。其安装方法见官网;选择最新的稳定版本v5.1;
移植
1.新建工程
在vscode中打开命令面板,输入ESP-IDF:New Project
填写工程名字,选择路径,选择芯片,最后选择模板
本次选择ESP-idf
2.下载额外组件
ESP-idf中不包含lvgl,所以需要将lvgl和相应的驱动lvgl_esp32_drivers作为额外组件,放在工程中
- 新建在工程中新建文件夹
components
, - 下载
lvg
和lvgl_esp32_drivers
git clone git@github.com:lvgl/lvgl_esp32_drivers.git
git clone -b release/v8.3 git@github.com:lvgl/lvgl.git
工程结构如下
3. 修改menuconfig
修改flash
lvgl_esp32_drivers
- 驱动芯片:ST7789;
- 引脚配置;
TFT | ESP32 | 工程配置 |
---|---|---|
SCL | 13 | CLK |
SDA | 12 | MISO |
RES | 11 | Rest |
DC | 10 | DC |
CS | 9 | CS |
BLC | 46 | 勾选高电平为点亮屏幕 |
4 编译
编译阶段主要根据错误提示修改
fatal error: driver/ledc.h: No such file or directory
D:/myself/ESP32s3/lvgl/components/lvgl_esp32_drivers/lvgl_tft/esp_lcd_backlight.c:10:10: fatal error: driver/ledc.h: No such file or directory
10 | #include "driver/ledc.h"
| ^~~~~~~~~~~~~~~
compilation terminated.
- 原因:没有依赖driver组件
- 办法:在
components\lvgl_esp32_drivers\CMakeLists.txt
中增加driver的依赖
idf_component_register(SRCS ${SOURCES}
INCLUDE_DIRS ${LVGL_INCLUDE_DIRS}
REQUIRES lvgl
REQUIRES driver)
implicit declaration of function 'gpio_pad_select_gpio' and 'portTICK_RATE_MS' undeclared
D:/myself/ESP32s3/lvgl/components/lvgl_esp32_drivers/lvgl_tft/st7789.c:89:5: error: implicit declaration of function 'gpio_pad_select_gpio'; did you mean 'esp_rom_gpio_pad_select_gpio'? [-Werror=implicit-function-declaration]
89 | gpio_pad_select_gpio(ST7789_DC);
| ^~~~~~~~~~~~~~~~~~~~
| esp_rom_gpio_pad_select_gpio
D:/myself/ESP32s3/lvgl/components/lvgl_esp32_drivers/lvgl_tft/st7789.c:100:22: error: 'portTICK_RATE_MS' undeclared (first use in this function); did you mean 'portTICK_PERIOD_MS'?
100 | vTaskDelay(100 / portTICK_RATE_MS);
| ^~~~~~~~~~~~~~~~
| portTICK_PERIOD_MS
- 原因:
gpio_pad_select_gpio()
和portTICK_RATE_MS
发生变化; - 办法:根据提示替换:
gpio_pad_select_gpio()
替换为esp_rom_gpio_pad_select_gpio()
;portTICK_RATE_MS
替换为portTICK_PERIOD_MS
error: 'LV_HOR_RES_MAX' undeclared
D:/myself/ESP32s3/lvgl/components/lvgl_esp32_drivers/lvgl_helpers.h:41:25: error: 'LV_HOR_RES_MAX' undeclared (first use in this function); did you mean 'LV_HOR_RES'?
41 | #define DISP_BUF_SIZE (LV_HOR_RES_MAX * 40)
- 原因:缺少宏定义
- 办法:添加
在components\lvgl_esp32_drivers\lvgl_helpers.h
添加缺少的宏定义
// 屏幕分辨率
#define LV_HOR_RES_MAX 240
#define LV_VER_RES_MAX 240
error: 'ledc_timer_config_t' has no member named 'bit_num'
D:/myself/ESP32s3/lvgl/components/lvgl_esp32_drivers/lvgl_tft/esp_lcd_backlight.c:52:14: error: 'ledc_timer_config_t' has no member named 'bit_num'
52 | .bit_num = LEDC_TIMER_10_BIT,
- 原因:结构体成员发生变化
- 办法:按照新的结构体删除
.bit_num
成员
typedef struct {
ledc_mode_t speed_mode; /*!< LEDC speed speed_mode, high-speed mode or low-speed mode */
ledc_timer_bit_t duty_resolution; /*!< LEDC channel duty resolution */
ledc_timer_t timer_num; /*!< The timer source of channel (0 - LEDC_TIMER_MAX-1) */
uint32_t freq_hz; /*!< LEDC timer frequency (Hz) */
ledc_clk_cfg_t clk_cfg; /*!< Configure LEDC source clock from ledc_clk_cfg_t.
Note that LEDC_USE_RC_FAST_CLK and LEDC_USE_XTAL_CLK are
non-timer-specific clock sources. You can not have one LEDC timer uses
RC_FAST_CLK as the clock source and have another LEDC timer uses XTAL_CLK
as its clock source. All chips except esp32 and esp32s2 do not have
timer-specific clock sources, which means clock source for all timers
must be the same one. */
} ledc_timer_config_t;
error: implicit declaration of function 'gpio_matrix_out'
D:/myself/ESP32s3/lvgl/components/lvgl_esp32_drivers/lvgl_tft/esp_lcd_backlight.c:58:9: error: implicit declaration of function 'gpio_matrix_out'; did you mean 'gpio_iomux_out'? [-Werror=implicit-function-declaration]
58 | gpio_matrix_out(config->gpio_num, ledc_periph_signal[LEDC_LOW_SPEED_MODE].sig_out0_idx + config->channel_idx, config->output_invert, 0);
| ^~~~~~~~~~~~~~~
| gpio_iomux_out
- 原因:函数名发生变化
- 办法:将
gpio_iomux_out()
修改为esp_rom_gpio_connect_out_signal()
基于参数uint32_t gpio_num, uint32_t signal_idx, bool out_inv, bool oen_inv
在ESP-idf文件夹中进行查找,可以查找替换函数。
error: 'SIG_GPIO_OUT_ID ' undeclared (first use in this function)
- 原因:
SIG_GPIO_OUT_ID
宏定义不存在 - 办法:通过查找,发现
SIG_GPIO_OUT_ID
在gpio_sig_map.h
中,添加该头文件
#include "soc/gpio_sig_map.h"
5 编写main函数
#include <stdio.h>
#include "esp_log.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#include "lvgl.h"
#include "lvgl_helpers.h"
#include "esp_timer.h"
#include "demos/lv_demos.h"
#define TAG "main"
void lv_tick_task(void *arg)
{
lv_tick_inc(1);
}
void app_main(void)
{
/* Initialize SPI or I2C bus used by the drivers */
lvgl_driver_init();
lv_init();
lv_color_t *buf1 = heap_caps_malloc(DISP_BUF_SIZE * sizeof(lv_color_t), MALLOC_CAP_DMA);
assert(buf1 != NULL);
static lv_color_t *buf2 = NULL;
static lv_disp_draw_buf_t disp_buf;
uint32_t size_in_px = DISP_BUF_SIZE;
lv_disp_draw_buf_init(&disp_buf, buf1, buf2, size_in_px);
lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.hor_res = LV_HOR_RES_MAX;
disp_drv.ver_res = LV_VER_RES_MAX;
disp_drv.flush_cb = disp_driver_flush;
disp_drv.draw_buf = &disp_buf;
lv_disp_drv_register(&disp_drv);
const esp_timer_create_args_t periodic_timer_args = {
.callback = &lv_tick_task,
.name = "periodic_gui"};
esp_timer_handle_t periodic_timer;
ESP_ERROR_CHECK(esp_timer_create(&periodic_timer_args, &periodic_timer));
ESP_ERROR_CHECK(esp_timer_start_periodic(periodic_timer, 1 * 1000));
// lvgl demo演示
lv_demo_widgets();
while (1)
{
/* Delay 1 tick (assumes FreeRTOS tick is 10ms */
vTaskDelay(pdMS_TO_TICKS(10));
lv_task_handler();
}
}
6 下载
报错
E (398) spi: spi_bus_initialize(774): invalid dma channel, chip only support spi dma channel auto-alloc
在lvgl_helpers.c
在添加:
#if defined (CONFIG_IDF_TARGET_ESP32C3)
dma_channel = SPI_DMA_CH_AUTO;
#elif defined (CONFIG_IDF_TARGET_ESP32S3) # 添加
dma_channel = SPI_DMA_CH_AUTO; # 添加
#endif
屏幕颜色错误:
修改lv_conf.h
, LV_COLOR_16_SWAP
修改为1
/*Swap the 2 bytes of RGB565 color. Useful if the display has an 8-bit interface (e.g. SPI)*/
#define LV_COLOR_16_SWAP 1
成果
不足
- 对于
lv_conf.h
中的参数了解不深入,基本上采用默认配置; - lvgl没有开线程;
- 没有启用触摸功能;
基于wsl安装esp32-idf开发环境
[toc]
编译时间对比
均编译Heelo_Wold
demo((先清除编译,再开始编译)
Windows | wsl |
---|---|
04:11.05 | 01:00.59 |
wsl 安装
WSL(Windows Subsyetem for Linux) 目前已发布两代产品:WSL1 和 WSL2。WSL2 则利用 Windows 的 Hyper-V 功能,通过虚拟化技术直接运行 Linux 虚拟机,更接近原生 Linux 操作系统体验。
注意:以下的wsl均指wsl2
1. 检查 BIOS 是否开启了虚拟化
ctrl+shfit+esc
进入任务管理器,查看 BIOS 是否开启虚拟化。
如果显示禁用,则要进入BIOS,在config
中将Intel Virtual Technology
设置为true
。
2. 启用wsl功能
打开控制面板 -> 程序和功能 -> 启动或关闭Windows功能 -> Hyper & 适用于linux的Windows系统-> 点击确认,等待安装完成。
3.安装 Ubuntu
直接在Microsoft Store中搜索“ubuntu”
直接选择安装即可。
安装完毕后,点击“启动”,或者按 Win+R
,输入 wsl
,即可开始按照提示创建用户名(username)和密码(password)了。
之后,可以在 Win+R
中输入 wsl
,开始WSL。
安装usbipd-win
wsl无法获取USB设备,必须使用usbipd-win将连接到windows上的usb设备转接到wsl中。
1. 下载
直接点击subipd-win.msi
安装,安装完毕后根据提示重启电脑
2. 常用命令
usbipd attach
Description:
连接一个USB设备到客户端
Usage:
usbipd attach [options]
Options:
-a, --auto-attach 当设备断开时,重新连接
-b, --busid <BUSID> 通过 <BUSID>连接设备
-i, --hardware-id <VID:PID> 通过<VID>:<PID>连接设备
-w, --wsl <[DISTRIBUTION]> (REQUIRED) 链接到WSL(可以指定Ubuntu的版本)
-?, -h, --help 显示帮助信息
usbipd bind
Description:
注册一个设备用于共享
Usage:
usbipd bind [options]
Options:
-b, --busid <BUSID> 通过 <BUSID>共享设备
-f, --force 强制绑定,Windows无法使用
-i, --hardware-id <VID:PID> 通过 <VID>:<PID>共享设备
-?, -h, --help 帮助
usbipd detach
Description:
断开一个或者多个设备
Usage:
usbipd detach [options]
Options:
-a, --all 断开所以设备
-b, --busid <BUSID> 断开 <BUSID>设备
-i, --hardware-id <VID:PID> 断开 <VID>:<PID>设备
-?, -h, --help 帮助
usbipd list
列出当前连接的 USB 设备
usbipd unbind
Description:
取消共享,如果设备已经连接到其他客户端,设备立即和客户端断开
Usage:
usbipd unbind [options]
Options:
-a, --all 停止共享所有设备
-b, --busid <BUSID> 停止<BUSID>设备
-g, --guid <GUID> Stop sharing persisted device having <GUID>
-i, --hardware-id <VID:PID> 停止共享 <VID>:<PID>设备
-?, -h, --help 帮助
3. 使用demo
挂载设备
常用命令
# 1.查看帮助,可以列出所有的usbipd命令
usbipd --help
# 2. 列出目前所有的USB设备
usbipd list
# 3. 共享某个设备
usbipd bind --busid=<BUSID>
# 4. 将设备挂载在wsl下
usbipd attach -wsl --busid=<BUSID>
-
将esp32连接到电脑;
-
在Windows下打开终端,通过
usbipd list
查看目前Windows下挂了哪些设备;
PS C:\Users\haijun> usbipd list
Connected:
BUSID VID:PID DEVICE STATE
2-1 17ef:60ff USB 输入设备 Not shared
2-3 06cb:00fc Synaptics UWP WBDI Not shared
2-4 174f:2454 Integrated Camera, Integrated IR Camera Not shared
2-7 303a:1001 USB 串行设备 (COM8), USB JTAG/serial debug unit Not shared
2-10 8087:0026 英特尔(R) 无线 Bluetooth(R) Not shared
可知本台电脑,挂载5个设备,和esp32连接的USB设备为2-7
,其状态为not shared
。
- 通过
usbipd bind
共享USB设备;
PS C:\Users\haijun> usbipd bind -b 2-7
再次查看设备状态
PS C:\Users\haijun> usbipd list
Connected:
BUSID VID:PID DEVICE STATE
2-1 17ef:60ff USB 输入设备 Not shared
2-3 06cb:00fc Synaptics UWP WBDI Not shared
2-4 174f:2454 Integrated Camera, Integrated IR Camera Not shared
2-7 303a:1001 USB 串行设备 (COM8), USB JTAG/serial debug unit Shared
2-10 8087:0026 英特尔(R) 无线 Bluetooth(R) Not shared
Persisted:
GUID DEVICE
5fcf8573-b9b6-4cbb-81e1-4cdafbb7982d Silicon Labs CP210x USB to UART Bridge (COM3)
设备状态变成shared
,说明共享成功。
注意:一旦设备被共享,重新拔插以后,该设备仍处于共享状态,即对于同一设备,该命令只需要运行一次
- 通过
usbipd attach
将设备挂载到wsl下;
PS C:\Users\haijun> usbipd attach -w -b 2-7
usbipd: info: Using WSL distribution 'Ubuntu-20.04' to attach; the device will be available in all WSL 2 distributions.
usbipd: info: Using IP address 172.19.32.1 to reach the host.
再次查看设备状态
PS C:\Users\haijun> usbipd list
Connected:
BUSID VID:PID DEVICE STATE
2-1 17ef:60ff USB 输入设备 Not shared
2-3 06cb:00fc Synaptics UWP WBDI Not shared
2-4 174f:2454 Integrated Camera, Integrated IR Camera Not shared
2-7 1a86:55d3 USB-Enhanced-SERIAL CH343 (COM4) Shared
2-10 8087:0026 英特尔(R) 无线 Bluetooth(R) Not shared
Persisted:
GUID DEVICE
2abd5a66-7692-48f4-8cc4-f4b90ac84aff USB 串行设备 (COM8), USB JTAG/serial debug unit
5fcf8573-b9b6-4cbb-81e1-4cdafbb7982d Silicon Labs CP210x USB to UART Bridge (COM3)
设备状态变成Attached
,说明挂载成功。
注意:在使用 usbipd attach -w -b 2-7
前必须先开启wsl
- 验证
在wsl中查看USB设备
haijun@zs-duhaijun:~$ ls /dev/ttyA*
/dev/ttyACM0
可以看出多了个/dev/ttyACM0
设备
卸载设备
# 卸载指定设备
PS C:\Users\haijun> usbipd detach -b 2-7
# or 卸载全部设备
usbipd detach -a
再次查看设备状态
PS C:\Users\haijun> usbipd list
Connected:
BUSID VID:PID DEVICE STATE
2-1 17ef:60ff USB 输入设备 Not shared
2-3 06cb:00fc Synaptics UWP WBDI Not shared
2-4 174f:2454 Integrated Camera, Integrated IR Camera Not shared
2-7 1a86:55d3 USB-Enhanced-SERIAL CH343 (COM4) Shared
2-10 8087:0026 英特尔(R) 无线 Bluetooth(R) Not shared
可知设备变成Shared
状态,说明卸载成功;
如果想停止共享使用unbind
命令,因为只要卸载了设备,Windows下就可以正常使用了,所以一般不需要使用unbind
.
如果不像每次usbpipd list
时都看到以前共享的设备,但是目前又没有连接到电脑上的设备,可以使用unbind
usbipd unbind -g <GUID>
安装esp32-idf开发环境
安装vscode-esp-idf插件
- 检查
remote-ssh
插件是否安装;
- 连接wsl;
![image-20231208225502008](studying/esp32/../../picture/esp32-wsl/image-20231208225502008.png)
- 连接以后,安装ESP32-idf;
注意:本地安装的插件不会自动安装到wsl中,所以在安装ESP32-idf插件时,将和c、cmake、python相关的插件一起安装到wsl中
安装esp32开发环境
- 在wsl环境中安装相关依赖
sudo apt-get install git wget flex bison gperf python3-pip python3-venv python3-setuptools cmake ninja-build ccache libffi-dev libssl-dev dfu-util
- 在vscode中通过
ctr+shift+p
打开命令面板 -> 输入figconESP-IDF:Config ESP-IDF extension
打开安装esp-idf安装界面。
- 下载服务器尽量选择
Espressif
,因为它可以防止因为网络问题,导致下载失败; - esp-idf版本尽量选择最新的,即第一个(我原先选择过v5.1,可以正常下载,但是环境总有一点小问题:提示找不到编译链)
- 后面的安装路径随意;
- python版本默认就行
- 等待安装完成
安装时间和网络有关,可以通过终端的输出查看安装信息,有任何的报错均会显示。
安装完成的界面如下
如果要使用openocd,请按照提示设置操作。
使用
通过hello_world
demo检验是否可以正常编译和下载
在hello_world.vscode/c_cpp_properties.json
中的编译路径显示正常,编译一般就不会出错。
下载时可能报以下错误:
A fatal error occurred: Could not open /dev/ttyACM0, the port is busy or doesn't exist.
([Errno 13] could not open port /dev/ttyACM0: [Errno 13] Permission denied: '/dev/ttyACM0'
原因:通过查询USB设备权限知道,设备没有可执行权限
haijun@zs-duhaijun:~$ ll /dev/ttyA*
crw------- 1 root root 166, 0 Dec 9 23:56 /dev/ttyACM0
解决:添加可执行权限
haijun@zs-duhaijun:~$ sudo chmod 700 /dev/ttyACM0
[sudo] password for haijun:
haijun@zs-duhaijun:~$ ll /dev/ttyA*
crwx------ 1 root root 166, 0 Dec 10 11:09 /dev/ttyACM0
Windows和wsl编译环境来回切换
如果从wsl切换到Windows,esp-idf的插件会报以下乱码
Command failed: dfu-util --version
'dfu-util' �����ڲ����ⲿ���Ҳ���ǿ����еij���
���������ļ���
Error checking dfu-util version
Error: Command failed: dfu-util --version
'dfu-util' �����ڲ����ⲿ���Ҳ���ǿ����еij���
���������ļ���
这应该是sp-idf的插件配置使用的是wsl那一套,导致环境不兼容。需要手动选择选择下环境
-
通过
ctr+shift+p
打开命令面板 -> 输入figconESP-IDF:Config ESP-IDF extension
打开安装esp-idf安装界面。 -
点击
USE EXISTING SETUP
-
选择在Windows下的安装路径
等待它重新安装虚拟环境。
https://github.com/dorssel/usbipd-win/releases/tag/v4.0.0
esp32-驱动步进电机
[toc]
步进电机
所使用的步进电机和驱动器如下:
驱动器的引脚
引脚名称 | 引脚定义 |
---|---|
ENA-(ENA) | 输出使能负端 |
ENA+(ENA) | 输出使能正端 |
DIR-(DIR) | 方向控制负端 |
DIR+(DIR) | 方向控制正端 |
PUL-(PUL) | 脉冲控制负端 |
PUL+(PUL) | 脉冲控制正端 |
B- | B相正 |
B+ | B相负 |
A+ | A相正 |
A- | A相负 |
GND | 电源地 |
VCC | 电源正 |
- EAN:控制输出信号是否使能,当EAN有效时,输出关闭,电机绕组电流为零, 电机为无力矩状态,可以自由转动电机,适合需要手动调整电机的场合。
- DIR:控制电机的转动方向,当此信号有效时,电机顺时针转动,当此信号无效时,电机逆时针旋转。
- PUL:控制电机转动,驱动器接收到的脉冲信号电机就会按照既定的方向旋转。电机的角位移与脉冲的数量成正比, 速度与脉冲的频率成正比。通常脉冲的有效宽度>=5us,频率<=125KHz。
控制器的有两组拨码开关,用于控住细分和电流:
- 细分设置:步进电机的步进角都是1.8°, 因此电机旋转一圈需要360° /1.8° =200个脉冲,若细分选择16,则表示不仅电机转一圈需要200*16=3200个脉冲;
- 电流设置:电流值需要根据步进电机的额定电流来设定。 一般建议驱动器的输出电流设定和电机额定电流差不多或者小一点;
驱动器的接线方式一般分为共阴极和供阳极两种:
共阴极接法如图所示:
共阳极接法如图所示:
控制方法
通过对步进电机的驱动器了解,可知控制步进电机需要使用3个GPIO:
- 使能端:一个普通的GPIO,能输出高低电平;
- 方向端:一个普通的GPIO,能输出高低电平;
- 脉冲端:能够输出脉冲数可控的GPIO;
ESP32有PWM的输出外设,但是没有发现能控制脉冲个数的方式(可能是我对该外设了解不清楚),因此采用定时器+GPIO的方式,使其能输出可控的脉冲
ESP32S3 定时器外设
ESP32-S3 包含两个定时器组,即定时器组 0 和定时器组 1
定时器具有如下功能:
- 16 位时钟预分频器,分频系数为 2 到 65536
- 54 位时基计数器可配置成递增或递减
- 可读取时基计数器的实时值
- 暂停和恢复时基计数器
- 可配置的报警产生机制
- 计数器值重新加载(报警时自动重新加载或软件控制的即时重新加载)
- 电平触发中断
定时器的架构如下:
-
时钟源的选择,可以选择APB时钟源(APB_CLK)或者外部时钟(XTAL_CLK ),时钟源16 位预分频器分频,产生时基计数器使用的时基计数器时钟TB_CLK。
-
54 位时基计数器:54 位时基计数器基于 TB_CLK,每经过TB_CLK,技计数值递增或者递减。其中,时钟使能(TIMG_Tx_EN 能控制计数器的是否停止;
-
比较器:当当计数值和设定的报警值一致时,触发定时器中断;
定时器的配置
当然,esp32是不需要我们操作寄存器的,只需要按照官方说明进行配置即可
1 资源分配
配置结构体:gptimer_config_t
成员:
clk_src
:选择定时器的时钟源direction
:设置定时器的计数方向resolution_hz
设置内部计数器的分辨率intr_priority
设置中断的优先级。如果设置为0
,则会分配一个默认优先级的中断,否则会使用指定的优先级。flags
intr_shared
backup_before_sleep
配置结构体传递给gptimer_new_timer()
/*创建定时器*/
gptimer_handle_t gptimer = NULL;
/*定时器计数周期1us*/
gptimer_config_t timer_config = {
.clk_src = GPTIMER_CLK_SRC_DEFAULT, // APB时钟源
.direction = GPTIMER_COUNT_UP, // 向上计数
.resolution_hz = 1 * 1000 * 1000, // 计数分辨率(1MHz)
.intr_priority = 0, // 中断优先级:默认
};
gptimer_new_timer(&timer_config, &gptimer);
2 设置报警动作
配置结构体 gptimer_alarm_config_t
成员
alarm_count
:设置触发警报事件的目标计数值reload_count
代表警报事件发生时要重载的计数值flags.auto_reload_on_alarm
标志设置是否使能自动重载功能
配置结构体传递给gptimer_set_alarm_action()
/*设置周期性动作*/
gptimer_alarm_config_t alarm_config = {
.alarm_count = 50, // 事件周期50us
.reload_count = 0, // 重新加载的计数值为0
.flags.auto_reload_on_alarm = true // 自动重载
};
gptimer_set_alarm_action(gptimer, &alarm_config);
设置为周期性动作,定时周期为50us。
3. 注册事件回调函数
通过gptimer_register_event_callbacks()
将函数挂载到中断服务例程 (ISR)。请在 gptimer_enable()
之前调用这一函数
/*设置周期性回调函数*/
gptimer_event_callbacks_t cbs = {
.on_alarm = generate_pluse_motor_cb
};
gptimer_register_event_callbacks(gptimer, &cbs, &expected_value);
回调函数
/**
* @brief 定时器周期事件回调函数
* @details 使用GPIO4产生脉冲信号
* @param timer: 定时器句柄
* @param edata: 事件数据
* @param user_data: 用户数据
* @return 无
*/
static bool generate_pluse_motor_cb(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_data)
{
uint16_t exp_pluse = *(uint32_t *)(user_data);
static uint16_t count;
count++;
if(count > 2*exp_pluse)
{
count = 0;
gptimer_stop(timer);
}
else{
gpio_set_level(PLUSE_GPIO, count % 2);
}
return true;
}
回调函数中,则是每次进入定时器中断就翻转电平,即脉冲的周期为50us*2=100us,另外通过user_data
将期望的脉冲数带入回调函数,一旦达到期望脉冲数则停止定时器,实现脉冲数可控的目标。
4 使能和禁用定时器
/*使能定时器*/
gptimer_enable(gptimer);
5 启动或者停止定时器
/*启动定时器*/
// gptimer_start(gptimer);
GPIO 配置
#define PLUSE_GPIO GPIO_NUM_4 // 步进电机脉冲
#define DIR_GPIO GPIO_NUM_5 // 步进电机方向
#define ENABLE_GPIO GPIO_NUM_48 // 步进电机使能
/*配置GPIO: 配置为输出*/
gpio_config_t io_conf = {
.intr_type = GPIO_INTR_DISABLE, // 初始电平
.mode = GPIO_MODE_OUTPUT, // 输出模式
.pin_bit_mask = (1ULL << PLUSE_GPIO | 1ULL << DIR_GPIO | 1ULL << ENABLE_GPIO),
.pull_down_en = GPIO_PULLDOWN_DISABLE, // 禁止下拉
.pull_up_en = GPIO_PULLUP_DISABLE // 禁止上拉
};
gpio_config(&io_conf);
设置脉冲数和方向
/**
* @brief 设置脉冲数和转动方向
* @param timer: 定时器句柄
* @param *exp_pluse: 期望的脉冲数(和set_pluse一致,它将传入定时器的回调函数中)
* @param set_pluse:设置的脉冲数
* @param dir:控制方向
*/
bool set_pluse_count(gptimer_handle_t timer, uint32_t *exp_pluse, uint32_t set_pluse, int8_t dir)
{
esp_err_t st;
st = gptimer_start(timer);
if(st == ESP_OK)
{
*exp_pluse = set_pluse;
gpio_set_level(DIR_GPIO, dir);
ESP_LOGI(TAG, "pluse=%ld, dir=%d", set_pluse, dir);
}
else{
ESP_LOGE(TAG, "gptimer_start failed with %s", esp_err_to_name(st));
}
return st;
}
调用:
while (1)
{
set_pluse_count(gptimer, &expected_value, 1600, 1);
vTaskDelay(5000 / portTICK_PERIOD_MS);
set_pluse_count(gptimer, &expected_value, 1600, 0);
vTaskDelay(5000 / portTICK_PERIOD_MS);
}
完整代码
git clone -b motor https://github.com/navy-to-haijun/esp32-s3.git
Makefile
gcc
$$ a_b = a^2+c^3 $$
main.c
#include<stdio.h>
int main()
{
printf("Hello world\n");
return 0;
}
1.预处理
gcc -E main.c -o main.i
-E
:预处理;-o
: 将结果输出到指定文件中;
2. 编译,生成汇编语言
gcc -S mian.c -o main.s
-S
:进行预处理和汇编操作;
3. 生成目标文件
gcc -c mian.c -o mian.o
-c
:生成目标文件
4.生成可执行文件
gcc mian.c -o main
静态文件库
三个.c文件:main.c
, add.c
, minus.c
目标:将add.c
, minus.c
编译为静态文件库
mian.c
#include<stdio.h>
int add(int a, int b);
int minus(int a, int b);
int main()
{
printf("Hello world\n");
printf("add = %d\n", add(7, 5));
printf("minus= %d\n", minus(7, 5));
return 0;
}
add.c
int add(int a, int b)
{
return a + b;
}
minus.c
int minus(int a, int b)
{
return a - b;
}
# 1.编译成.o文件
gcc -c [.c] [.c] ...
# example
gcc -c add.c minus.c
# 2. 编译静态库
ar -r [lib自定义库名.a] [.o] [.o] ...
# example
ar -r liboperation.a add.o minus.o
# 3. 链接为可执行文件
gcc [.c] [.a] -o [自定义输出文件名]
or
gcc [.c] -o [自定义输出文件名] -l[库名] -L[库路径]
# example
gcc main.c liboperation.a -o main
总结:
- 静态库以
.a
为后缀; - 静态库名字以
lib
作为前缀;
动态文件库
# 1. 编译为.o文件
gcc -c -fpic [.c] [.c]...
# example
gcc -c -fpic add.c minus.c
# 编译为静态库
gcc -shared [.o] [.o]... -o [lib自定义库名.so]
# example
gcc -shared add.o minus.o -o liboperation.so
# 链接
gcc [.c/.cpp] -o [自定义可执行文件名] [动态库路径]
or
gcc [.c] -o [自定义输出文件名] -l[库名] -L[库路径] -WL,rpath=[库路径]
# example
gcc main.c -o main ./liboperation.so
-fpic
:生成与位置无关的代码(Position Independent Code)- 动态文件库后缀为
.so
Makefile
基本格式
targets : prerequisties
[tab键]command
- targets:目标文件
- prerequisties:依赖文件
- command:执行命令
变量
$@
: 目标(target)的完整名称$<
: 第一个依赖文件(prerequisties)的名称$^
: 所有的依赖文件(prerequisties),以空格分开,不包含重复的依赖文件
#定义变量
obj := main.o
cfile := main.c
${obj}: ${cfile}
gcc -c $^ -o $@
debug:
@echo ${obj}
@echo ${cfile}
clear:
rm *.o
.PHONY: clear
.PHONY
伪目标@
可以隐藏命令,终端不输出
常用符号
=
- 简单的赋值运算符
- 用于将右边的值分配给左边的变量
- 如果在后面的语句中重新定义了该变量,则将使用新的值
:=
- 立即赋值运算符
- 用于在定义变量时立即求值
- 该值在定义后不再更改
- 即使在后面的语句中重新定义了该变量
?=
- 默认赋值运算符
- 如果该变量已经定义,则不进行任何操作
- 如果该变量尚未定义,则求值并分配
+=
- 累加
*和%
*
: 通配符表示匹配任意字符串,可以用在目录名或文件名中%
: 通配符表示匹配任意字符串,并将匹配到的字符串作为变量使用
常用函数
$(fn arguments) or ${fn arguments}
- fn: 函数名
- arguments: 函数参数,参数间以逗号
,
分隔,而函数名和参数之间以“空格”分隔
1. shell: 调用shell命令
$(shell <command> <arguments>)
- 功能:调用 shell 命令 command
- 返回:函数返回 shell 命令 command 的执行结果
#获取src下的所有.c文件
c_src := $(shell find src -name "*.c")
debug:
@echo $(c_src)
.PHONY:debug
2 subst:字符串替换
$(subst <from>,<to>,<text>)
- 功能:把字串
中的 字符串替换成 - 返回:函数返回被替换过后的字符串
#获取src下的所有.c文件
c_src := $(shell find src -name "*.c")
# 将src文件夹更换为obj文件夹
c_obj := $(subst src, obj, $(c_src))
debug:
@echo $(c_src)
@echo $(c_obj)
.PHONY:debug
3. patsubst: 字符串替换
- 功能:通配符
%
,表示任意长度的字串,从 text 中取出 patttern, 替换成 replacement - 返回:函数返回被替换过后的字符串
#获取src下的所有.c文件
c_src := $(shell find src -name "*.c")
# 将src文件夹更换为obj文件夹并将后缀替换为.o
c_obj := $(patsubst src%.c, obj%.o, $(c_src))
debug:
@echo $(c_src)
@echo $(c_obj)
.PHONY:debug
4. foreach:循环函数
$(foreach <var>,<list>,<text>)
- 功能:把字串
- 中的元素逐一取出来,执行
包含的表达式 - 返回:
所返回的每个字符串所组成的整个字符串(以空格分隔)
# .头文件的地址
library_paths := ./inc \
/usr/local/include
# 将头文件前面加上-L
library_paths := $(foreach iterm, $(library_paths), -L$(library_paths))
debug:
@echo $(library_paths)
.PHONY:debug
5 dir:返回子目录
$(dir <names...>)
- 功能:从文件名序列中取出目录部分。目录部分是指最后一个反斜杠(“/”)之前 的部分。如果没有反斜杠,那么返回“./”。
- 返回:返回文件名序列的目录部分。
#获取src下的所有.c文件
c_src := $(shell find src -name "*.c")
# 获取src文件夹路径
src_path := $(dir $(c_src))
debug:
@echo $(src_path)
.PHONY:debug
6 notdir : 去掉文件绝对路径,保留文件名
$(notdir <names...>)
7. filter:过滤
$(filter <names...>)
8 basename:去掉后缀
$(basename <names...>)
# 获取静态库和动态库名称
#获取/usr/lib 下以lib为前缀的路径
lib_path := $(shell find /usr/lib -name "lib*")
# 去掉文件路径,获取文件名
lib_name := $(notdir $(lib_path))
# 过滤出静态库和动态库
a_libs := $(filter %.a, $(lib_name))
so_libs := $(filter %.so, $(lib_name))
# 获取库文件名称
a_libs := $(basename $(a_libs))
so_libs := $(basename $(a_libs))
# 去掉前缀lib
a_libs := $(subst lib, , $(a_libs))
so_libs := $(subst lib, , $(so_libs))
debug:
@echo $(so_libs)
.PHONY:debug
编译选项
-std=
: 指定编译标准,例如:-std=c++11、-std=c++14-g
: 包含调试信息-w
: 不显示警告-O
: 优化等级,通常使用:-O3-I
: 加在头文件路径前fPIC
: (Position-Independent Code), 产生的没有绝对地址,全部使用相对地址,代码可以被加载到内存的任意位置,且可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的
连接选项
-l
: 加在库名前面-L
: 加在库路径前面-Wl,<选项>
: 将逗号分隔的 <选项> 传递给链接器-rpath=
: "运行" 的时候,去找的目录。运行的时候,要找 .so 文件,会从这个选项里指定的地方去找
example
文件夹结构
.
├── inc
│ ├── add.h
│ └── minus.h
├── Makefile
└── src
├── add.c
├── main.c
└── minus.c
add.h
#ifndef ADD_H
#define ADD_H
int add(int a, int b);
#endif
minus.h
#ifndef MINUX_H
#define MINUX_H
int minus(int a, int b);
#endif
add.c
# include"add.h"
int add(int a, int b)
{
return a + b;
}
minus.c
# include"minus.h"
int minus(int a, int b)
{
return a - b;
}
main.c
#include<stdio.h>
#include "add.h"
#include "minus.h"
int main()
{
printf("Hello world\n");
printf("add = %d\n", add(7, 5));
printf("minus= %d\n", minus(7, 5));
return 0;
}
将add和minus编译成静态库,然后链接成可执行文件
Makefile
# 库源文件
lib_srcs := $(filter-out src/main.c, $(shell find src -name "*.c"))
lib_objs := $(patsubst src%.c, objs%.o, $(lib_srcs))
# 头文件
include_path := ./inc
# 编译选项
I_flag := $(include_path:%=-I%)
compile_flag := -g -O3 $(I_flag)
# 链接选项
library_path := ./lib
linking_lib := XXX
l_flag := $(linking_lib:%=-l%)
L_flag := $(library_path:%=-L%)
linking_flag := $(l_flag) $(L_flag)
# 加上编译选项
objs/%.o: src/%.c
@mkdir -p $(dir $@)
gcc -c $^ -o $@ $(compile_flag)
# 编译静态库
lib/libXXX.a: $(lib_objs)
@mkdir -p $(dir $@)
ar -r $@ $^
static_lib: lib/libXXX.a
# 链接静态库
objs/main.o: src/main.c
@mkdir -p $(dir $@)
gcc -c $< -o $@ $(compile_flag)
# 加上链接选项
workspace/exec: objs/main.o
@mkdir -p $(dir $@)
gcc $^ -o $@ $(linking_flag)
run: workspace/exec
@./$<
debug:
@echo $(lib_srcs)
@echo $(compile_flag)
clear:
rm -rf objs workspace lib
.PHONY: clear debug run
编译成动态库,然后链接为可执行文件
Makefile
# 库源文件
lib_srcs := $(filter-out src/main.c, $(shell find src -name "*.c"))
lib_objs := $(patsubst src%.c, objs%.o, $(lib_srcs))
# 头文件
include_path := ./inc
# 编译选项
I_flag := $(include_path:%=-I%)
compile_flag := -g -O3 -fpic $(I_flag)
# 链接选项
library_path := ./lib
linking_lib := XXX
l_flag := $(linking_lib:%=-l%)
L_flag := $(library_path:%=-L%)
# 加上链接选项-Wl,-rpath=[库路径]
linking_flag := $(l_flag) $(L_flag) -Wl,-rpath=$(library_path)
# 加上编译选项
objs/%.o: src/%.c
@mkdir -p $(dir $@)
gcc -c $^ -o $@ $(compile_flag)
# 编译动态库: gcc -shared
lib/libXXX.so: $(lib_objs)
@mkdir -p $(dir $@)
gcc -shared $^ -o $@
share_lib: lib/libXXX.so
# 链接动态库
objs/main.o: src/main.c
@mkdir -p $(dir $@)
gcc -c $< -o $@ $(compile_flag)
# 加上链接选项
workspace/exec: objs/main.o
@mkdir -p $(dir $@)
gcc $^ -o $@ $(linking_flag)
run: workspace/exec
@./$<
debug:
@echo $(lib_srcs)
@echo $(r_flag)
clear:
rm -rf objs workspace lib
.PHONY: clear debug run