基于ESP32S3的智能终端项目--6.串口功能实现
串口功能实现
本章目标:让你的ESP32-S3智能终端具备串口通信功能,能够:
-
发送数据:在UI的文本框中输入内容,点击发送按钮通过硬件串口输出。
-
接收数据:实时显示通过串口接收到的数据(如来自PC或其他设备的信息)。
-
清空接收区:一键清除接收文本框的内容。
-
通过FreeRTOS任务管理串口数据的后台处理(可选)。
1. 准备工作:了解UI部件
根据SquareLine Studio截图,串口功能相关的UI部件主要包括:
-
ui_SerialBackToMain:返回主屏幕的按钮。 -
ui_TextAreaTX:用于输入待发送数据的文本框。 -
ui_TextAreaRX:用于显示接收数据的文本框。 -
ui_SerialTX:发送按钮。 -
ui_SerialDelete:清空接收区按钮。 -
ui_KeyboardSerial:虚拟键盘,用于在文本框中输入。
请确保你的SquareLine Studio项目中这些部件的名称与代码中的完全一致。
2. 代码深度解析
2.1 串口初始化(在setup()或类似位置)
cpp
Serial.begin(115200);
这是Arduino框架的标准串口初始化函数。它配置ESP32-S3的硬件UART0(默认与USB共用)以115200的波特率工作。你的终端可以通过串口监视器与此波特率通信。
2.2 数据结构定义(推测)
在你的代码片段中出现了 Uart.tx_data 和 Uart.rx_data,这意味着你需要定义一个全局结构体来管理串口数据。例如,在 config.h 中添加:
c
// config.h
typedef struct {
const char* tx_data; // 指向待发送的字符串(注意:可能来自文本框,需要小心生命周期)
String rx_data; // 存储接收到的数据(使用String便于动态管理)
} cfg_uart;
extern cfg_uart Uart;
并在某个 .cpp 文件中初始化:
c
cfg_uart Uart = {
.tx_data = "",
.rx_data = ""
};
2.3 事件回调函数详解(来自你的 ui.cpp)
2.3.1 返回主屏幕 ui_event_SerialBackToMain
cpp
void ui_event_SerialBackToMain(lv_event_t * e)
{
lv_event_code_t event_code = lv_event_get_code(e);
if(event_code == LV_EVENT_CLICKED) {
// 切换到主屏幕(ui_Main),使用淡入动画
_ui_screen_change(&ui_Main, LV_SCR_LOAD_ANIM_FADE_ON, 100, 0, &ui_Main_screen_init);
// 删除当前屏幕(释放内存)
_ui_screen_delete(&ui_Serial);
// 删除串口任务(如果你创建了后台任务)
serialTask_delete();
}
}
讲解:
-
当用户点击返回按钮时,触发
LV_EVENT_CLICKED。 -
_ui_screen_change是SquareLine Studio生成的辅助函数,用于切换屏幕。 -
serialTask_delete()是你需要实现的任务销毁函数(如果创建了独立任务)。
2.3.2 发送文本框点击/值变化 ui_event_TextAreaTX
cpp
void ui_event_TextAreaTX(lv_event_t * e)
{
lv_event_code_t event_code = lv_event_get_code(e);
lv_obj_t * target = lv_event_get_target(e);
if(event_code == LV_EVENT_CLICKED) {
// 点击文本框时,切换虚拟键盘的显示/隐藏
_ui_flag_modify(ui_KeyboardSerial, LV_OBJ_FLAG_HIDDEN, _UI_MODIFY_FLAG_TOGGLE);
}
else if(event_code == LV_EVENT_VALUE_CHANGED) {
// 当文本框内容变化时,将当前文本保存到全局变量
Uart.tx_data = lv_textarea_get_text(target);
}
}
讲解:
-
文本框有双重事件:
-
点击:显示/隐藏键盘,提升用户体验。
-
值变化:每当用户输入或删除字符时,实时将最新内容保存到
Uart.tx_data。这样点击发送按钮时可以直接使用该数据。
-
2.3.3 发送按钮 ui_event_SerialTX
cpp
void ui_event_SerialTX(lv_event_t * e)
{
lv_event_code_t event_code = lv_event_get_code(e);
if(event_code == LV_EVENT_CLICKED) {
// 通过硬件串口打印保存的字符串(并自动添加换行)
Serial.println(Uart.tx_data);
}
}
讲解:
-
当用户点击发送按钮,程序从
Uart.tx_data获取待发送内容,通过Serial.println输出。注意println会在末尾添加回车换行,便于接收端读取。
2.3.4 清空接收区 ui_event_SerialDelete
cpp
void ui_event_SerialDelete(lv_event_t * e)
{
lv_event_code_t event_code = lv_event_get_code(e);
if(event_code == LV_EVENT_CLICKED) {
// 将接收文本框的内容设置为空字符串
lv_textarea_set_text(ui_TextAreaRX, "");
}
}
讲解:简单的清空操作,将 ui_TextAreaRX 的内容置空。
2.4 串口接收处理(在任务或循环中)
你提供的文档片段中有一段接收代码:
cpp
if(Serial.available())
{
String RX = Serial.readStringUntil('\n');
Uart.rx_data = RX.c_str(); // 注意:c_str() 返回的指针可能在RX析构后失效
lv_textarea_add_text(ui_TextAreaRX, Serial.rx_data); // 这里有笔误?应该是 Uart.rx_data 或 RX
}
修正建议:
-
Serial.rx_data似乎是笔误,应为Uart.rx_data或直接使用RX。 -
使用
RX.c_str()并将其地址赋给Uart.rx_data是危险的,因为RX是局部变量,函数结束后内存可能被回收。正确做法是将数据追加到lv_textarea的同时,如果需要保存,应使用String成员或复制到字符数组。
推荐接收处理:
cpp
if (Serial.available()) {
String rxString = Serial.readStringUntil('\n');
// 直接在接收文本框追加内容(可以加上换行)
lv_textarea_add_text(ui_TextAreaRX, rxString.c_str());
lv_textarea_add_text(ui_TextAreaRX, "\n");
// 如果需要保存,可以使用 Uart.rx_data = rxString; (但Uart.rx_data需定义为String)
}
2.5 创建串口后台任务(可选)
如果你希望串口接收不阻塞GUI,可以创建一个任务:
cpp
void serial_task(void *pvParameters) {
while (1) {
if (Serial.available()) {
String rx = Serial.readStringUntil('\n');
// 注意:LVGL操作不能在非LVGL任务中直接调用,除非用 lvgl 的锁机制
// 简单做法:通过全局变量传递数据,在主循环或LVGL任务中更新UI
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
然后在 setup() 中创建任务:
cpp
xTaskCreatePinnedToCore(serial_task, "serial_task", 4096, NULL, 1, NULL, 0);
3. 集成步骤
-
定义全局变量:在
config.h中声明cfg_uart结构体和Uart变量。 -
初始化串口:在
setup()中添加Serial.begin(115200);。 -
创建任务(可选):如需要后台接收,创建串口任务。
-
关联事件回调:确保SquareLine Studio生成的UI代码中,为上述部件绑定了正确的事件回调函数(通常会自动关联)。
-
编译烧录:测试发送与接收功能。
4. 常见问题
-
文本框不显示键盘:检查是否在
ui_event_TextAreaTX中正确切换了ui_KeyboardSerial的隐藏标志。 -
发送数据乱码:确保PC串口监视器的波特率与
Serial.begin一致(115200)。 -
接收数据显示不完整:
readStringUntil('\n')会等待直到收到换行符,如果对方发送不带换行的数据,可能一直阻塞。可改用readString()或按字节读取。 -
任务中直接更新UI导致崩溃:FreeRTOS任务中不能直接调用LVGL函数(除非使用
lvgl的线程安全机制)。建议使用队列或全局标志,在主LVGL任务或loop中处理UI更新。
十、音乐播放器功能实现保姆级教程
本章目标:让你的ESP32-S3智能终端能够播放存储在SD卡中的MP3音乐,实现:
-
扫描并列出SD卡中的MP3文件。
-
选择歌曲并通过点击播放/暂停、切歌控制。
-
调节音量。
-
使用FreeRTOS后台任务管理音频播放,避免卡顿。
1. 准备工作:硬件与库
-
硬件:
-
SD卡模块(SPI接口),用于存放MP3文件。
-
音频解码芯片(如MAX98357 I2S音频放大器)或直接通过I2S输出到DAC。
-
-
库依赖:在
platformio.ini中添加:ini lib_deps = esphome/ESP32-audioI2S@^2.0.7
2. UI部件(根据截图)
-
ui_MusicBackToMain:返回主屏幕按钮。 -
ui_RollerMusic:歌曲列表滚动选择器。 -
ui_StartMusic:播放/暂停按钮(可能是一个开关或按钮,具有选中状态)。 -
ui_ChangeMusic:切歌按钮(点击后播放选中的歌曲)。 -
ui_SliderVoice:音量调节滑块。 -
ui_LabelVoice:显示当前音量值的标签。
3. 数据结构定义(来自你的代码)
c
cfg_music Music = {
.song_options = "", // 用于 LVGL roller 的选项字符串(多个选项用换行分隔)
.song_name = {'\0'}, // 当前选中的歌曲名(字符数组)
.song_list = "", // 扫描得到的歌曲列表字符串
.play_flag = false, // 播放/暂停标志
.start_flag = false, // 是否需要启动新歌曲(用于切歌)
.pause_flag = false, // 是否处于暂停状态
.volume_value = 16 // 当前音量值(0-21,与audio库匹配)
};
这个结构体需要在头文件中定义,并声明全局变量 Music。
4. 核心功能实现
4.1 初始化SD卡和音频(music_init)
你需要实现以下函数(根据之前的文档):
cpp
#include <SPI.h>
#include <SD.h>
#include <Audio.h>
#define SD_CS 9
#define SD_SCK 14
#define SD_MISO 17
#define SD_MOSI 10
#define I2S_DOUT 39
#define I2S_BCLK 40
#define I2S_LRC 41
SPIClass SD_SPI(HSPI);
Audio audio;
void SD_init() {
pinMode(SD_CS, OUTPUT);
digitalWrite(SD_CS, HIGH);
SD_SPI.begin(SD_SCK, SD_MISO, SD_MOSI);
SD_SPI.setFrequency(4000000);
if (!SD.begin(SD_CS, SD_SPI)) Serial.println("Card Mount Failed");
else Serial.println("Card Mount Success");
}
void music_init() {
audio.setPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
audio.setVolume(Music.volume_value); // 初始音量
audio.setBalance(100);
}
4.2 扫描SD卡中的MP3文件(music_read)
cpp
void music_read() {
File root = SD.open("/");
File file = root.openNextFile();
Music.song_list = ""; // 清空
while (file) {
if (!file.isDirectory()) {
const char *filename = file.name();
int len = strlen(filename);
if (len > 4 && strcasecmp(filename + len - 4, ".mp3") == 0) {
Music.song_list += filename;
Music.song_list += "\n";
// 默认选中第一个文件
if (Music.song_name[0] == '\0') {
strcpy(Music.song_name, filename);
}
}
}
file = root.openNextFile();
}
root.close();
Music.song_options = Music.song_list.c_str(); // 供roller使用
}
注意:strcasecmp 用于不区分大小写比较,在ESP32 Arduino中可用。
4.3 音乐播放状态更新任务(music_update)
这个函数需要被周期性调用(通常放在独立任务中),负责处理音频数据流和状态切换。
cpp
void music_update() {
audio.loop(); // 必须定期调用,处理音频数据流
if (Music.play_flag) {
// 如果需要切歌
if (Music.start_flag) {
audio.connecttoFS(SD, Music.song_name); // 开始播放新歌
Music.start_flag = 0;
Music.pause_flag = 0;
}
// 如果处于暂停状态,则恢复播放
if (Music.pause_flag && !audio.isRunning()) {
audio.pauseResume(); // 恢复
Music.pause_flag = 0;
}
} else {
// 如果正在播放但play_flag为假,则暂停
if (!Music.pause_flag && audio.isRunning()) {
audio.pauseResume(); // 暂停
Music.pause_flag = 1;
}
}
}
4.4 事件回调函数详解(来自你的 ui.cpp)
4.4.1 返回主屏幕 ui_event_MusicBackToMain
cpp
void ui_event_MusicBackToMain(lv_event_t * e)
{
lv_event_code_t event_code = lv_event_get_code(e);
if(event_code == LV_EVENT_CLICKED) {
_ui_screen_change(&ui_Main, LV_SCR_LOAD_ANIM_FADE_ON, 100, 0, &ui_Main_screen_init);
_ui_screen_delete(&ui_Music);
}
}
与串口返回类似,切换屏幕并删除当前屏幕。
4.4.2 播放/暂停按钮 ui_event_StartMusic
cpp
void ui_event_StartMusic(lv_event_t * e)
{
lv_event_code_t event_code = lv_event_get_code(e);
lv_obj_t * target = lv_event_get_target(e);
if(event_code == LV_EVENT_CLICKED) {
if(lv_obj_has_state(target, LV_STATE_CHECKED))
Music.play_flag = 1; // 选中状态(按钮按下)表示播放
else
Music.play_flag = 0; // 非选中表示暂停
}
}
讲解:
-
假设
ui_StartMusic是一个具有两种状态的按钮(如开关),通过LV_STATE_CHECKED判断当前状态。 -
点击时切换
play_flag,后台任务会根据此标志控制播放/暂停。
4.4.3 切歌按钮 ui_event_ChangeMusic
cpp
void ui_event_ChangeMusic(lv_event_t * e)
{
lv_event_code_t event_code = lv_event_get_code(e);
if(event_code == LV_EVENT_CLICKED) {
Music.play_flag = 1; // 确保播放状态为真
Music.start_flag = 1; // 标记需要切歌
lv_obj_add_state(ui_StartMusic, LV_STATE_CHECKED); // 同步播放按钮状态
lv_roller_get_selected_str(ui_RollerMusic, Music.song_name, sizeof(Music.song_name));
}
}
讲解:
-
点击切歌按钮时,从
ui_RollerMusic获取当前选中的歌曲名,存入Music.song_name。 -
设置
start_flag = 1,让music_update知道需要加载新歌。 -
强制设置
play_flag = 1并同步播放按钮的选中状态,保证播放器进入播放模式。
4.4.4 音量滑块 ui_event_SliderVoice
cpp
void ui_event_SliderVoice(lv_event_t * e)
{
lv_event_code_t event_code = lv_event_get_code(e);
if(event_code == LV_EVENT_VALUE_CHANGED) {
Music.volume_value = lv_slider_get_value(ui_SliderVoice);
// 实际音量设置在 music_update 中通过 audio.setVolume 完成
}
}
讲解:
-
当滑块值改变时,将值保存到全局变量。
-
注意:这里没有立即调用
audio.setVolume,因为audio.setVolume需要在audio.loop循环外调用或注意线程安全。通常可以在music_update中根据Music.volume_value设置音量(但每次循环都设置可能没必要)。更优做法是在事件回调中直接调用audio.setVolume(Music.volume_value);,但需确保从任务中调用没问题(audio库不是线程安全的,建议在music_update任务中统一处理)。你可以用一个标志来指示音量已改变,然后在任务中设置。
5. 创建音乐后台任务
在 setup() 中,初始化SD卡和音频后,创建音乐任务:
cpp
void music_task(void *pvParameters) {
while (1) {
music_update();
vTaskDelay(5 / portTICK_PERIOD_MS); // 保持音频流顺畅,延迟不能太长
}
}
// 在 setup 中 SD_init(); music_init(); music_read(); // 扫描歌曲并初始化roller选项 // 将歌曲列表设置到 roller 控件 lv_roller_set_options(ui_RollerMusic, Music.song_options, LV_ROLLER_MODE_NORMAL); xTaskCreatePinnedToCore(music_task, "music_task", 4096, NULL, 1, NULL, 0);
6. 集成步骤
-
硬件连接:按照定义的引脚连接SD卡和I2S音频模块。
-
添加库:在
platformio.ini中添加ESP32-audioI2S。 -
定义结构体:在
config.h中声明cfg_music和Music。 -
实现初始化函数:
SD_init,music_init,music_read。 -
编写事件回调:确保SquareLine Studio生成的UI代码中,为音乐部件绑定了上述回调。
-
创建音乐任务:在
setup中创建music_task。 -
测试:将MP3文件放入SD卡根目录,上电后应能扫描并列出,点击切歌即可播放。
7. 常见问题
-
SD卡挂载失败:检查SPI接线和CS引脚是否正确,尝试降低SPI频率。
-
没有声音:确认I2S引脚连接正确,音频模块供电正常,音量值不为0。
-
播放卡顿:可能是SD卡读取速度或任务延时太长,可优化
music_task的延时到5ms以下。 -
切歌无反应:检查
Music.start_flag是否被正确置1,并在music_update中被清零。 -
UI更新与音频任务冲突:LVGL操作必须在LVGL任务中执行(如绑定到Core 1的
lvgl_task),音乐任务中不要直接调用LVGL函数。可以通过全局变量传递数据,在LVGL任务中更新UI。
8. 效果演示
打开串口:
发送/接收消息:

更多推荐
所有评论(0)