【立创·实战派ESP32-S3】文档教程
第 17 章 掌机示例
本章使用 LVGL 设计制作了一个简单的类似于手机界面的示例。
开机后,先是开机画面,logo 炫酷出场,并伴有开机音乐,然后进入主界面,主界面如下图所示。主界面顶部显示文字和图标示例,下面部分显示六种应用,从上到下,从左到右,依次分别是:运动监测、音乐播放、TF 卡浏览、摄像头、WiFi 连接、蓝牙控制器。每一种应用,都可以点击屏幕进入,进入应用后,点击左上角返回按钮返回主界面。
17.1 使用例程
把开发板提供的【14-handheld】例程复制到你的实验文件夹当中,并使用 VSCode 打开工程。
本例程有 TF 卡文件浏览功能,上电之前,可以插入一张 TF 卡到开发板,不要带电插拔 SD 卡。
连接开发板到电脑,在 VSCode 上选择串口号,选择目标芯片为 esp32s3,串口下载方式,然后点击“一键三联”按钮,等待编译下载打开终端。
开发板开始运行程序后,液晶屏显示 6 个图标。
第 1 个应用:姿态和运动监测。进入应用后,显示当前的 XYZ 角度,角度值会随着开发板旋转而变化。同时,在标题栏,显示当前运动状态。当发生运动和震动时,标题栏显示“运动或震动”,否则显示“静止”状态。把开发板放到桌子上,你敲击桌子,它就会检测到震动。如果想要改变灵敏度,可以修改 QMI8658 初始化配置代码。
第 2 个应用:音乐播放器。界面基本上和第 14 章的一样,功能也一样。
第 3 个应用:TF 卡浏览器。可以浏览 SD 卡中文件,只能打开目录看文件名称,没有对任何类型文件做解析处理。
第 4 个应用:摄像头。点击打开摄像头,点击左上角返回箭头图标回到主界面。
第 5 个应用:WiFi 连接。界面基本上和第 12 章的一样,点进来之后,扫描到周围的 WiFi 名称,点击你自己的,连接后,自动回到主界面,主界面最上面将删除欢迎语,显示当前年月日和分时秒。受到网络授时服务器稳定性的影响,获取速度有时候快,有时候慢,如果获取不到,每 2 秒钟获取一次,将一直获取,直到获取到为止。
第 6 个应用:蓝牙控制器。界面基本上和第 13 章的一样,进入应用后,开发板作为 HID 设备,对外蓝牙名称为 HID,用笔记本电脑或手机连接后,可以用开发板控制手机和笔记本电脑的音量。
17.2 例程讲解
本例程中的开机 logo、开机音乐、主界面,以及每个图标对应界面的创建,以及主界面图标制作等教程,在《实战派 ESP32-C3》教程中已经介绍过,这里就不重复了。
下面我们按照单片机执行流程走一遍,在这个过程中,介绍一下大家可能比较难以理解的地方。
主函数:
// 主函数
void app_main(void)
{
// Initialize NVS.
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK( ret );
bsp_i2c_init(); // I2C初始化
pca9557_init(); // IO扩展芯片初始化
bsp_lvgl_start(); // 初始化液晶屏lvgl接口
bsp_spiffs_mount(); // SPIFFS文件系统初始化
bsp_codec_init(); // 音频初始化
lv_gui_start(); // 显示开机界面
my_event_group = xEventGroupCreate();
xTaskCreatePinnedToCore(power_music_task, "power_music_task", 4*1024, NULL, 5, NULL, 1); // 播放开机音乐
xTaskCreatePinnedToCore(main_page_task, "main_page_task", 4*1024, NULL, 5, NULL, 0); // 主界面任务
while (true) {
displayMemoryUsage();
vTaskDelay(pdMS_TO_TICKS(5 * 1000));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
第 4~10 行初始化 NVS,大家只要记住,只要使用 wifi 和蓝牙,就先把这个初始化了,就可以了。
第 18 行,就是大家开机先看到的画面。
第 22 行,就是大家开机先听到的音乐。
这里定义了一个事件组,用来控制开机音乐停止播放后,才会进入主界面。
最后在 while 循环里面,间隔 5 秒钟,终端输出当前内存使用情况。
内存占用情况函数 displayMemoryUsage()
代码如下:
// 打印内存使用情况
void displayMemoryUsage() {
size_t totalDRAM = heap_caps_get_total_size(MALLOC_CAP_INTERNAL);
size_t freeDRAM = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
size_t usedDRAM = totalDRAM - freeDRAM;
size_t totalPSRAM = heap_caps_get_total_size(MALLOC_CAP_SPIRAM);
size_t freePSRAM = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
size_t usedPSRAM = totalPSRAM - freePSRAM;
size_t DRAM_largest_block = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL);
size_t PSRAM_largest_block = heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM);
float dramUsagePercentage = (float)usedDRAM / totalDRAM * 100;
float psramUsagePercentage = (float)usedPSRAM / totalPSRAM * 100;
ESP_LOGI(TAG, "DRAM Total: %zu bytes, Used: %zu bytes, Free: %zu bytes, DRAM_Largest_block: %zu bytes", totalDRAM, usedDRAM, freeDRAM, DRAM_largest_block);
ESP_LOGI(TAG, "DRAM Used: %.2f%%", dramUsagePercentage);
ESP_LOGI(TAG, "PSRAM Total: %zu bytes, Used: %zu bytes, Free: %zu bytes, PSRAM_Largest_block: %zu bytes", totalPSRAM, usedPSRAM, freePSRAM, PSRAM_largest_block);
ESP_LOGI(TAG, "PSRAM Used: %.2f%%", psramUsagePercentage);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
heap_caps_get_total_size()
函数用来获取内存总容量,单位字节。
heap_caps_get_free_size()
函数用来获取内存空闲总容量,单位字节。
heap_caps_get_largest_free_block()
函数用来获取最大的连续空闲总容量,单位字节。上一个函数获取的是所有空闲的内存,这个是获取最大的连续空闲内存。
它们的参数,MALLOC_CAP_INTERNAL
表示的是 ESP32 片内内存,MALLOC_CAP_SPIRAM
表示的是 ESP32 外部的 PSRAM。
%zu
用来输入 size_t
型数据。
主界面
/******************************** 主界面 ******************************/
void lv_main_page(void)
{
lvgl_port_lock(0);
lv_obj_del(lckfb_logo); // 删除开机logo
// 创建主界面基本对象
lv_obj_set_style_bg_color(lv_scr_act(), lv_color_hex(0x000000), 0); // 修改背景为黑色
static lv_style_t style;
lv_style_init(&style);
lv_style_set_radius(&style, 10);
lv_style_set_bg_opa( &style, LV_OPA_COVER );
lv_style_set_bg_color(&style, lv_color_hex(0x00BFFF));
lv_style_set_bg_grad_color( &style, lv_color_hex( 0x00BF00 ) );
lv_style_set_bg_grad_dir( &style, LV_GRAD_DIR_VER );
lv_style_set_border_width(&style, 0);
lv_style_set_pad_all(&style, 0);
lv_style_set_width(&style, 320);
lv_style_set_height(&style, 240);
main_obj = lv_obj_create(lv_scr_act());
lv_obj_add_style(main_obj, &style, 0);
// 显示右上角符号
lv_obj_t * sylbom_label = lv_label_create(main_obj);
lv_obj_set_style_text_font(sylbom_label, &lv_font_montserrat_20, 0);
lv_obj_set_style_text_color(sylbom_label, lv_color_hex(0xffffff), 0);
lv_label_set_text(sylbom_label, LV_SYMBOL_BLUETOOTH" "LV_SYMBOL_WIFI); // 显示蓝牙和wifi图标
lv_obj_align_to(sylbom_label, main_obj, LV_ALIGN_TOP_RIGHT, -10, 10);
// 显示左上角欢迎语
main_text_label = lv_label_create(main_obj);
lv_obj_set_style_text_font(main_text_label, &font_alipuhui20, 0);
lv_label_set_long_mode(main_text_label, LV_LABEL_LONG_SCROLL_CIRCULAR); /*Circular scroll*/
lv_obj_set_width(main_text_label, 280);
lv_label_set_text(main_text_label, "欢迎使用立创实战派开发板");
lv_obj_align_to(main_text_label, main_obj, LV_ALIGN_TOP_LEFT, 8, 5);
// 设置应用图标style
static lv_style_t btn_style;
lv_style_init(&btn_style);
lv_style_set_radius(&btn_style, 16);
lv_style_set_bg_opa( &btn_style, LV_OPA_COVER );
lv_style_set_text_color(&btn_style, lv_color_hex(0xffffff));
lv_style_set_border_width(&btn_style, 0);
lv_style_set_pad_all(&btn_style, 5);
lv_style_set_width(&btn_style, 80);
lv_style_set_height(&btn_style, 80);
// 创建第1个应用图标
lv_obj_t *icon1 = lv_btn_create(main_obj);
lv_obj_add_style(icon1, &btn_style, 0);
lv_obj_set_style_bg_color(icon1, lv_color_hex(0x30a830), 0);
lv_obj_set_pos(icon1, 15, 50);
lv_obj_add_event_cb(icon1, att_event_handler, LV_EVENT_CLICKED, NULL);
lv_obj_t * img1 = lv_img_create(icon1);
LV_IMG_DECLARE(img_att_icon);
lv_img_set_src(img1, &img_att_icon);
lv_obj_align(img1, LV_ALIGN_CENTER, 0, 0);
// 创建第2个应用图标
lv_obj_t *icon2 = lv_btn_create(main_obj);
lv_obj_add_style(icon2, &btn_style, 0);
lv_obj_set_style_bg_color(icon2, lv_color_hex(0xf87c30), 0);
lv_obj_set_pos(icon2, 120, 50);
lv_obj_add_event_cb(icon2, music_event_handler, LV_EVENT_CLICKED, NULL);
lv_obj_t * img2 = lv_img_create(icon2);
LV_IMG_DECLARE(img_music_icon);
lv_img_set_src(img2, &img_music_icon);
lv_obj_align(img2, LV_ALIGN_CENTER, 0, 0);
// 创建第3个应用图标
lv_obj_t *icon3 = lv_btn_create(main_obj);
lv_obj_add_style(icon3, &btn_style, 0);
lv_obj_set_style_bg_color(icon3, lv_color_hex(0x008b8b), 0);
lv_obj_set_pos(icon3, 225, 50);
lv_obj_add_event_cb(icon3, sdcard_event_handler, LV_EVENT_CLICKED, NULL);
lv_obj_t * img3 = lv_img_create(icon3);
LV_IMG_DECLARE(img_sd_icon);
lv_img_set_src(img3, &img_sd_icon);
lv_obj_align(img3, LV_ALIGN_CENTER, 0, 0);
// 创建第4个应用图标
lv_obj_t *icon4 = lv_btn_create(main_obj);
lv_obj_add_style(icon4, &btn_style, 0);
lv_obj_set_style_bg_color(icon4, lv_color_hex(0xd8b010), 0);
lv_obj_set_pos(icon4, 15, 147);
lv_obj_add_event_cb(icon4, camera_event_handler, LV_EVENT_CLICKED, NULL);
lv_obj_t * img4 = lv_img_create(icon4);
LV_IMG_DECLARE(img_camera_icon);
lv_img_set_src(img4, &img_camera_icon);
lv_obj_align(img4, LV_ALIGN_CENTER, 0, 0);
// 创建第5个应用图标
lv_obj_t *icon5 = lv_btn_create(main_obj);
lv_obj_add_style(icon5, &btn_style, 0);
lv_obj_set_style_bg_color(icon5, lv_color_hex(0xcd5c5c), 0);
lv_obj_set_pos(icon5, 120, 147);
lv_obj_add_event_cb(icon5, wifiset_event_handler, LV_EVENT_CLICKED, NULL);
lv_obj_t * img5 = lv_img_create(icon5);
LV_IMG_DECLARE(img_wifiset_icon);
lv_img_set_src(img5, &img_wifiset_icon);
lv_obj_align(img5, LV_ALIGN_CENTER, 0, 0);
// 创建第6个应用图标
lv_obj_t *icon6 = lv_btn_create(main_obj);
lv_obj_add_style(icon6, &btn_style, 0);
lv_obj_set_style_bg_color(icon6, lv_color_hex(0xb87fa8), 0);
lv_obj_set_pos(icon6, 225, 147);
lv_obj_add_event_cb(icon6, btset_event_handler, LV_EVENT_CLICKED, NULL);
lv_obj_t * img6 = lv_img_create(icon6);
LV_IMG_DECLARE(img_btset_icon);
lv_img_set_src(img6, &img_btset_icon);
lv_obj_align(img6, LV_ALIGN_CENTER, 0, 0);
lvgl_port_unlock();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
在这个函数中,解释一下最开始的 lvgl_port_lock(0)
和最后面的 lvgl_port_unlock()
。
lvgl_port_lock(0)
函数执行后,LVGL 界面会停止更新。
lvgl_port_unlock()
函数执行后,LVGL 界面才会更新。
使用这两个函数,是因为我们例程中使用了 esp_lvgl_port 这个组件作为 lvgl 的接口。
中文字库
本例程使用的中文字库,就是第 12 章 wifi 例程中制作好的字库,包含了“全部”中文。
wifi 扫描到的名称,可能包含中文,且未知。
SD 卡浏览时,文件夹有可能是中文,且未知。
所以需要使用“全部”中文字库。
这个字库几乎占了 4M 左右 FLASH 大小,程序下载速度也会慢。
在不需要使用全部中文字库的情况下,需要使用哪个中文,就制作哪个中文的字库,这样就会减小字库占用体积和加快程序下载速度。
第 1 个应用:姿态和运动监测。
本例程中的姿态传感器初始化函数,和之前第 5 章中的初始化函数不同,除了开启加速度和陀螺仪,还开启了运动监测功能,函数代码如下:
// 初始化qmi8658
esp_err_t qmi8658_init(void)
{
esp_err_t ret = ESP_OK;
uint8_t id = 0; // 芯片的ID号
uint8_t count = 0;
qmi8658_register_read(QMI8658_WHO_AM_I, &id ,1); // 读芯片的ID号
while (id != 0x05) // 判断读到的ID号是否是0x05
{
vTaskDelay(100 / portTICK_PERIOD_MS); // 延时100豪秒
qmi8658_register_read(QMI8658_WHO_AM_I, &id ,1); // 读取ID号
count++;
if (count>=3){
ret = ESP_FAIL;
return ret;
}
}
ESP_LOGI(TAG, "QMI8658 OK!"); // 打印信息
qmi8658_register_write_byte(QMI8658_RESET, 0xb0); // 复位
vTaskDelay(10 / portTICK_PERIOD_MS); // 延时10ms
// 配置运动状态检测
qmi8658_register_write_byte(QMI8658_CATL1_L, 1); // AnyMotionXThr 必须是0~32之间的数
qmi8658_register_write_byte(QMI8658_CATL1_H, 1); // AnyMotionYThr 必须是0~32之间的数
qmi8658_register_write_byte(QMI8658_CATL2_L, 1); // AnyMotionZThr 必须是0~32之间的数
qmi8658_register_write_byte(QMI8658_CATL2_H, 1); // NoMotionXThr 必须是0~32之间的数
qmi8658_register_write_byte(QMI8658_CATL3_L, 1); // NoMotionYThr 必须是0~32之间的数
qmi8658_register_write_byte(QMI8658_CATL3_H, 1); // NoMotionZThr 必须是0~32之间的数
qmi8658_register_write_byte(QMI8658_CATL4_L, 0x77); // MOTION_MODE_CTRL 0111 0111
qmi8658_register_write_byte(QMI8658_CATL4_H, 0x01); // 0x01(means 1st command)
qmi8658_register_write_byte(QMI8658_CTRL9, 0x0E); // CTRL_CMD_CONFIGURE_MOTION
qmi8658_register_write_byte(QMI8658_CATL1_L, 1); // AnyMotionWindow
qmi8658_register_write_byte(QMI8658_CATL1_H, 1); // NoMotionWindow
qmi8658_register_write_byte(QMI8658_CATL2_L, 0xE8); // SigMotionWaitWindow[7:0]
qmi8658_register_write_byte(QMI8658_CATL2_H, 0x03); // SigMotionWaitWindow [15:8]
qmi8658_register_write_byte(QMI8658_CATL3_L, 0xE8); // SigMotionConfirmWindow[7:0]
qmi8658_register_write_byte(QMI8658_CATL3_H, 0x03); // SigMotionConfirmWindow[15:8]
qmi8658_register_write_byte(QMI8658_CATL4_H, 0x02); // 0x02(means 2nd command)
qmi8658_register_write_byte(QMI8658_CTRL9, 0x0E); // CTRL_CMD_CONFIGURE_MOTION
qmi8658_register_write_byte(QMI8658_CTRL1, 0x40); // CTRL1 设置地址自动增加
qmi8658_register_write_byte(QMI8658_CTRL7, 0x03); // CTRL7 允许加速度和陀螺仪
qmi8658_register_write_byte(QMI8658_CTRL2, 0x95); // CTRL2 设置ACC 4g 250Hz
qmi8658_register_write_byte(QMI8658_CTRL3, 0xd5); // CTRL3 设置GRY 512dps 250Hz
qmi8658_register_write_byte(QMI8658_CTRL8, 0x0E); // CTRL7 允许Any-Motion No-Motion and Significant-Motion
return ret;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
函数最开始,查询传感器 ID 号,如果查询到正确的 ID 号,就继续向下执行,否则就返回 ESP_ERROR。
第 25~42 行,是配置运动监测流程,这个配置过程和方法,在 QMI8658 的手册上就可以查询到。
在 task_process_att 任务中,创建了一个 lvgl 定时器,用于更新 XYZ 三轴的角度值,200 毫秒更新一次。
// 创建一个lv_timer 用于更新角度
my_lv_timer = lv_timer_create(att_update_cb, 200, NULL);
2
lvgl 定时器服务函数 att_update_cb()
如下所示:
// 定时更新姿态角度值
void att_update_cb(lv_timer_t * timer)
{
t_sQMI8658 QMI8658;
int att_x, att_y, att_z;
// 获取XYZ角度
qmi8658_fetch_angleFromAcc(&QMI8658);
att_x = round(QMI8658.AngleX); // 四舍五入
att_y = round(QMI8658.AngleY); // 四舍五入
att_z = round(QMI8658.AngleZ); // 四舍五入
// 更新角度值
lv_label_set_text_fmt(label_x, "X: %d", att_x);
lv_label_set_text_fmt(label_y, "Y: %d", att_y);
lv_label_set_text_fmt(label_z, "Z: %d", att_z);
// 更新角度bar
lv_bar_set_start_value(x_bar, att_x-10, LV_ANIM_OFF);
lv_bar_set_value(x_bar, att_x+10, LV_ANIM_OFF);
lv_bar_set_start_value(y_bar, att_y-10, LV_ANIM_OFF);
lv_bar_set_value(y_bar, att_y+10, LV_ANIM_OFF);
lv_bar_set_start_value(z_bar, att_z-10, LV_ANIM_OFF);
lv_bar_set_value(z_bar, att_z+10, LV_ANIM_OFF);
// 判断运动状态
uint8_t status = qmi8658_fetch_motion();
if (status & 0x20) // 判断是否发生Any-Motion
{
lv_label_set_text(att_label, "运动或震动");
}
else if (status & 0x40) // 判断是否发生No-Motion
{
lv_label_set_text(att_label, "静止");
}
else if (status & 0x80) // 判断是否发生Significant-Motion
{
lv_label_set_text(att_label, "剧烈运动");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
第 8~10 行使用 round 函数,把获取到的数四舍五入成整数。
第 24 行,通过读取 QMI8658 的状态寄存器,获取当前的运动状态。
第 2 个应用:音乐播放器
这个例程的代码,就是复制之前第 14 章的代码,把标题的显示方式改了一下,大家对比一下两个例程就可以发现。
本例程中在 spiffs 存储的音乐,来自第 15 章的例程,不过,第 15 章为了迎合 AI,音乐的波特率是 32000,这里我波特率改成了 16000。大家使用 ffprobe 命令就可以查看。
第 3 个应用:SD 卡文件浏览
点击进入 SD 卡浏览界面后,会执行 task_process_sdcard 函数。
// SD卡处理任务
static void task_process_sdcard(void *arg)
{
esp_err_t ret;
ret = bsp_sdcard_mount(); // 挂载SD卡
if(ret != ESP_OK){ // 如果没有挂载成功
ESP_LOGE(TAG, "Failed to mount filesystem.");
lvgl_port_lock(0);
lv_label_set_text(sdcard_label, "SD卡挂载不成功");
lvgl_port_unlock();
vTaskDelay(1000 / portTICK_PERIOD_MS); // 给上面一点显示的时间
lvgl_port_lock(0);
lv_obj_del(icon_in_obj);
lvgl_port_unlock();
}else{ // 如果挂载成功
// 终端显示SD卡信息
sdmmc_card_print_info(stdout, sdmmc_card);
// 液晶屏标题栏显示SD卡容量
lvgl_port_lock(0);
lv_label_set_text_fmt(sdcard_label, "SD: %lluGB",
(((uint64_t)sdmmc_card->csd.capacity) * sdmmc_card->csd.sector_size) >> 30);
lvgl_port_unlock();
// 创建返回按钮
lvgl_port_lock(0);
lv_obj_t *btn_back = lv_btn_create(sdcard_title);
lv_obj_align(btn_back, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_set_size(btn_back, 60, 30);
lv_obj_set_style_border_width(btn_back, 0, 0); // 设置边框宽度
lv_obj_set_style_pad_all(btn_back, 0, 0); // 设置间隙
lv_obj_set_style_bg_opa(btn_back, LV_OPA_TRANSP, LV_PART_MAIN); // 背景透明
lv_obj_set_style_shadow_opa(btn_back, LV_OPA_TRANSP, LV_PART_MAIN); // 阴影透明
lv_obj_add_event_cb(btn_back, btn_sdback_cb, LV_EVENT_CLICKED, NULL); // 添加按键处理函数
lv_obj_t *label_back = lv_label_create(btn_back);
lv_label_set_text(label_back, LV_SYMBOL_LEFT); // 按键上显示左箭头符号
lv_obj_set_style_text_font(label_back, &lv_font_montserrat_20, 0);
lv_obj_set_style_text_color(label_back, lv_color_hex(0xffffff), 0);
lv_obj_align(label_back, LV_ALIGN_CENTER, -10, 0);
// 创建文件列表
sdcard_file_list = lv_list_create(icon_in_obj);
lv_obj_set_size(sdcard_file_list, 320, 200);
lv_obj_align(sdcard_file_list, LV_ALIGN_TOP_LEFT, 0, 40);
lv_obj_set_style_border_width(sdcard_file_list, 0, 0);
lv_obj_set_style_text_font(sdcard_file_list, &font_alipuhui20, 0);
lv_obj_set_scrollbar_mode(sdcard_file_list, LV_SCROLLBAR_MODE_OFF); // 隐藏wifi_list滚动条
lvgl_port_unlock();
// 列出 SD 卡中的文件
file_path_info.path_index = 0; // 表示当前在根目录
strcpy(file_path_info.path_now, SD_MOUNT_POINT); // 装入当前路径
list_sdcard_files(file_path_info.path_now); // 列出当前目录文件
}
vTaskDelete(NULL);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
函数最开始,先挂载 SD 卡,如果挂载不成功,会在界面上显示“SD 卡挂载不成功”,显示维持 1 秒钟,然后自动回到主界面。
如果挂载成功,会在标题栏显示当前 SD 卡总大小,并列出根目录文件名称。
第 53 行,list_sdcard_files()
函数,用来列出文件名称,函数定义如下:
// 列出SD卡中的文件
esp_err_t list_sdcard_files(char * path)
{
esp_err_t ret;
DIR *dir;
struct dirent *ent;
lv_obj_t * btn;
if ((dir = opendir(path))!= NULL) { // 打开目录
while ((ent = readdir(dir))!= NULL) { // 读取目录中的文件
/* 常规文件处理 */
if (ent->d_type == DT_REG){ // 如果是常规文件
int file_type_flag = 0;
const char* extension = strrchr(ent->d_name, '.'); // 从后往前 找到字符'.'
if (extension != NULL) { // 如果找到了'.'
extension++; // 指针地址+1
if (strcmp(extension, "mp3") == 0) { // 如果是mp3
file_type_flag = 1; // 标记为音乐文件
} else if (strcmp(extension, "wav") == 0) { // 如果是wav
file_type_flag = 1; // 标记为音乐文件
} else if (strcmp(extension, "mp4") == 0) {
file_type_flag = 2; // 标记为视频文件
} else if (strcmp(extension, "avi") == 0) {
file_type_flag = 2; // 标记为视频文件
} else if (strcmp(extension, "jpg") == 0) {
file_type_flag = 3; // 标记为图片
} else if (strcmp(extension, "jpeg") == 0) {
file_type_flag = 3; // 标记为图片
} else if (strcmp(extension, "png") == 0) {
file_type_flag = 3; // 标记为图片
} else if (strcmp(extension, "bmp") == 0) {
file_type_flag = 3; // 标记为图片
} else if (strcmp(extension, "gif") == 0) {
file_type_flag = 3; // 标记为图片
} else {
file_type_flag = 0; // 除了以上文件 其它文件都归类为普通文件
}
}
lvgl_port_lock(0);
switch (file_type_flag)
{
case 1:
btn = lv_list_add_btn(sdcard_file_list, LV_SYMBOL_AUDIO, (const char *)ent->d_name); // 显示音乐文件图标
break;
case 2:
btn = lv_list_add_btn(sdcard_file_list, LV_SYMBOL_VIDEO, (const char *)ent->d_name); // 显示视频文件图标
break;
case 3:
btn = lv_list_add_btn(sdcard_file_list, LV_SYMBOL_IMAGE, (const char *)ent->d_name); // 显示图片文件图标
break;
default:
btn = lv_list_add_btn(sdcard_file_list, LV_SYMBOL_FILE, (const char *)ent->d_name); // 显示普通文件图标
break;
}
lv_obj_t *icon = lv_obj_get_child(btn, 0); // 获取图标指针
lv_obj_set_style_text_font(icon, &lv_font_montserrat_24, 0); // 修改图标的字体
lv_obj_add_event_cb(btn, file_list_btn_cb, LV_EVENT_CLICKED, NULL); // 添加点击回调函数
lvgl_port_unlock();
}
/* 文件夹处理 */
else if (ent->d_type == DT_DIR) { // 如果是文件夹
lvgl_port_lock(0);
btn = lv_list_add_btn(sdcard_file_list, LV_SYMBOL_DIRECTORY, (const char *)ent->d_name);
lv_obj_t *icon = lv_obj_get_child(btn, 0); // 获取图标指针
lv_obj_set_style_text_font(icon, &lv_font_montserrat_24, 0); // 修改图标的字体
lv_obj_add_event_cb(btn, file_list_btn_cb, LV_EVENT_CLICKED, NULL); // 添加点击回调函数
lvgl_port_unlock();
}
}
closedir(dir);
ret = ESP_OK;
} else {
ESP_LOGE(TAG, "Failed to open directory %s.", path);
ret = ESP_FAIL;
}
return ret;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
第 6 行,定义了一个目录文件指针 ent。第 9 行,获取到文件信息。第 11 行,ent 中的 d_type 用来判断文件类型,是常规文件还是目录。
如果是常规文件,进入 if 后继续根据该文件的后缀名称,判断该文件是什么文件。文件名称使用 ent 的 d_name 获取。之所以判断是文件还是文件夹,以及是什么类型的文件,是因为我们要给不同的文件不同的图标。如果不加如表,可以不用这么麻烦。
对于这里的图标,再强调一下。在之前第 12 章 WiFi 例程中,wifi 名称列表,使用了中文字库,但是还需要加一个 wifi 图标,所以我们在做字库的时候,把 wifi 图标也一并做到了中文字库里面。如果不把 wifi 图标做到字库里面,图标就不能正常显示。
因为制作中文字库比较耗时,如果遇到一个需要添加的图标,就加进入在做字库,这样比较麻烦。
这里使用了一个简单的办法解决,就是上述代码中第 54、55 行和第 63、64 行。列表字体已经被定义为了 font_alipuhui20 中文字体。这里再通过获取子对象的办法,把图标字库修改成了 lv_font_montserrat_24,这个字体是 lvgl 自带的字体。
例程中,只添加了打开目录的功能,只要路径的长度不超过 512 个字节,都可以正常打开,也就是说,能打开几级目录,主要看文件夹名称的长短。这个是在下面结构体中定义,如果你的文件夹名称很长,且需要打开的目录路径超过了 512 字节,可以修改下面结构体中的数字。
struct file_path_info
{
uint8_t path_index; // 在第几级目录
char path_now[512]; // 当前文件路径
char path_back[512]; // 上级文件路径
};
struct file_path_info file_path_info;
2
3
4
5
6
7
第 4 个应用:摄像头
点击进入摄像头应用后,打开摄像头,同时在屏幕左上角显示了一个返回按钮。
这里显示摄像头图像,和之前的显示原理有些不同。
之前例程中,显示摄像头图像,是直接使用液晶屏绘制图像函数 esp_lcd_panel_draw_bitmap()
。
这里显示图像,是使用了 lvgl 的 img 组件对象。
点击进入摄像头应用后,创建了一个 img 组件对象,如下所示:
img_camera = lv_img_create(icon_in_obj);
lv_obj_set_pos(img_camera, 0, 0);
lv_obj_set_size(img_camera, 320, 240);
2
3
然后在 task_process_camera 任务中,使用 lv_img_set_src 函数,把 img 的源指向摄像头获取到的图像 buf。它的原理和我们显示主界面的图片图标一样,因为摄像头获取到的也是一张图片。例如,主界面中的第 1 个图标,我们使用 lvgl 在线工具,把图片制作成了 img_att_icon.c 文件,然后显示主界面的程序中,使用下面这行语句来显示图标:
lv_img_set_src(img1, &img_att_icon);
这其中的 img_att_icon,是在 img_att_icon.c 文件中定义的,我们打开 img_att_icon.c 文件,翻到最后面就可以看到如下代码:
const lv_img_dsc_t img_att_icon = {
.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA,
.header.always_zero = 0,
.header.reserved = 0,
.header.w = 75,
.header.h = 75,
.data_size = 5625 * LV_IMG_PX_SIZE_ALPHA_BYTE,
.data = img_att_icon_map,
};
2
3
4
5
6
7
8
9
这些内容,都是 lvgl 在线图片工具自动生成的。
这里我们要显示摄像头来的图像,所以需要自己定义一个上面的结构体,然后再使用它,代码如下:
lv_obj_t * img_camera;
// 摄像头图像
lv_img_dsc_t img_camera_dsc = {
.header.cf = LV_IMG_CF_TRUE_COLOR,
.header.always_zero = 0,
.header.reserved = 0,
.header.w = 320,
.header.h = 240,
.data_size = 240*320*2,
};
// 摄像头处理任务
static void task_process_camera(void *arg)
{
while (icon_flag == 4)
{
camera_fb_t *frame = esp_camera_fb_get();
img_camera_dsc.data = frame->buf;
lv_img_set_src(img_camera, &img_camera_dsc);
esp_camera_fb_return(frame);
}
esp_camera_deinit(); // 取消初始化摄像头
lvgl_port_lock(0);
lv_obj_del(icon_in_obj); // 删除摄像头画布
lvgl_port_unlock();
dvp_pwdn(1); // 摄像头进入掉电模式
vTaskDelete(NULL);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
点击退出按钮后,会把 icon_flag 变成 0,看第 16 行,当 icon_flag 变成 0 后会退出 while 循环,从而执行下面的函数。
esp_camera_deinit()函数用来取消摄像头初始化产生的内容,这个函数位于 esp32_camera 组件中,在执行这个函数的时候,终端会打印一个行红色的错误提示,并不会影响程序运行,不必处理。
E (10155) gdma: gdma_disconnect(238): no peripheral is connected to the channel
SPI 驱动屏幕显示摄像头图像效果并不理想,ESP32S3 本来有 RGB 接口,可以驱动 RGB 接口屏幕,效果会好很多,但是由于 ESP32S3 引脚太少了,摄像头就接了大部分引脚,再接一点其它外设,就没有接 RGB 屏幕的引脚了。所以,使用摄像头,就只能接 SPI 屏;接 RGB 屏,基本就使用不了摄像头了。
第 5 个应用:WiFi 连接
这里的代码,是从第 12 章例程复制过来的,基本上没有动,就不做详细介绍了。
再次强调一点,这里的密码输入,我只做了数字和大小写英文,如果你需要其它符号,可以把你的其它符号加入到数字那个 roller 里面。
第 6 个应用:蓝牙控制器
这里的代码,是从第 13 章例程复制过来的,基本上没有动,就不做详细介绍了。
这里把复制过来的蓝牙相关代码,放到了 main 文件夹下的 bt 文件夹里面。同时,在 main 文件夹下的 CMakeLists.txt 文件里面,路径前面也加了 bt,并且包含路径也添加了 bt,如下所示:
idf_component_register(#.....
#.....
SRCS "bt/ble_hidd_demo.c" "bt/esp_hidd_prf_api.c" "bt/hid_dev.c" "bt/hid_device_le_prf.c"
#.....
INCLUDE_DIRS "bt")
2
3
4
5
在 app_ui.c 中,头文件的包含也需要带上 bt。
#include "bt/ble_hidd_demo.h"
17.3 例程制作
本例程相当于是对之前的大部分例程进行了一个融合,使用了开发板上的所有硬件外设,也可以作为检测开发板硬件的用途。
本例程是在【11-mp3_player】例程的基础上进行修改。
开机 logo,开机界面,主界面,直接复制实战派 C3 开发板掌机示例中的代码,然后做一些修改,主要是对修改图标。
然后点击图标按钮对应的事件处理框架,也是参考实战派 C3 开发板掌机示例中的代码修改。
每个应用图标的运行代码,都是从前面对应章节例程中复制过来,然后做一些修改。
具体的修改细节,就不做介绍了。
教程到这里,暂时终结。