串口功能实现

本章目标:让你的ESP32-S3智能终端具备串口通信功能,能够:

  1. 发送数据:在UI的文本框中输入内容,点击发送按钮通过硬件串口输出。

  2. 接收数据:实时显示通过串口接收到的数据(如来自PC或其他设备的信息)。

  3. 清空接收区:一键清除接收文本框的内容。

  4. 通过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. 集成步骤

  1. 定义全局变量:在 config.h 中声明 cfg_uart 结构体和 Uart 变量。

  2. 初始化串口:在 setup() 中添加 Serial.begin(115200);

  3. 创建任务(可选):如需要后台接收,创建串口任务。

  4. 关联事件回调:确保SquareLine Studio生成的UI代码中,为上述部件绑定了正确的事件回调函数(通常会自动关联)。

  5. 编译烧录:测试发送与接收功能。

4. 常见问题

  • 文本框不显示键盘:检查是否在 ui_event_TextAreaTX 中正确切换了 ui_KeyboardSerial 的隐藏标志。

  • 发送数据乱码:确保PC串口监视器的波特率与 Serial.begin 一致(115200)。

  • 接收数据显示不完整readStringUntil('\n') 会等待直到收到换行符,如果对方发送不带换行的数据,可能一直阻塞。可改用 readString() 或按字节读取。

  • 任务中直接更新UI导致崩溃:FreeRTOS任务中不能直接调用LVGL函数(除非使用 lvgl 的线程安全机制)。建议使用队列或全局标志,在主LVGL任务或 loop 中处理UI更新。


十、音乐播放器功能实现保姆级教程

本章目标:让你的ESP32-S3智能终端能够播放存储在SD卡中的MP3音乐,实现:

  1. 扫描并列出SD卡中的MP3文件

  2. 选择歌曲并通过点击播放/暂停、切歌控制。

  3. 调节音量

  4. 使用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. 集成步骤

  1. 硬件连接:按照定义的引脚连接SD卡和I2S音频模块。

  2. 添加库:在 platformio.ini 中添加 ESP32-audioI2S

  3. 定义结构体:在 config.h 中声明 cfg_music 和 Music

  4. 实现初始化函数SD_initmusic_initmusic_read

  5. 编写事件回调:确保SquareLine Studio生成的UI代码中,为音乐部件绑定了上述回调。

  6. 创建音乐任务:在 setup 中创建 music_task

  7. 测试:将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. 效果演示

打开串口:

发送/接收消息:

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐