ESP32-S3使用LVGL与AI大模型接入实战学习总结
1.开发环境介绍
- 我使用的是CLion开发软件里面搭配platformio插件
2.开发板资源介绍
- 一块esp32-s3电容触摸屏语音开发板
- 一根USB数据线
- 一个4W喇叭
3.产品设计概览

4.配置个人大模型密钥
- 首先第一步是下载GitHub里的小智机器人源码
- 小智机器人源码
- 然后用CLion打开文件夹,我们只需要修改data文件夹下的settings.json文件的格式

- 开通豆包语音技术相关功能:豆包大模型



- 语音识别密钥

- 语音合成密钥

- 然后是扣子智能体的2个密钥:获取Tokenhttps://www.coze.cn/home



- 语音音色列表知识库:https://welink-cloud-sys.oss-cn-chengdu.aliyuncs.com/welink-cloud-sys/application/x-zip-compressed/%E9%9F%B3%E8%89%B2%E5%88%97%E8%A1%A8.zip

- 这里选择豆包模型后面需要带上lite的标签,因为反应速度快

- 用扣子的原因是插件方便,但是经过使用不太好用。。。。。

- 然后用API管理去获取Token密钥

- 然后我们获取完,输入到CLion里面,我们就先上传配置文件

- 然后我们再上传代码

- 此刻我们就搭建自己的智能体以及配置自己的KEY完成了
- 注意:!!!我们最重要的是AI回答的构建,直接复制粘贴的

5.PlatformIO编译太慢解决办法
- 创建环境变量,然后查看自己的CPU的逻辑处理器是多少个?


6.ESP32-S3启用PSRAM
- PSRAM什么?是一种拓展内存的部件
- 默认PSRAM是关闭的,我们需要配置platformio.ini
7.音频相关术语介绍
- PCM是什么?是一种脉冲编码调制,将连续的数字信号,转化为离散的模拟信号
- 自然界中的声音通过麦克风等采集设备处理后,首先会将声音信号转换成电信号,但此时的电信号是连续的模拟信号,如果需要在单片机上面去获取的话必须转化成数字信号
- PCM编码过程:采样,量化,编码
8.使用I2S协议播放音频
- I2S是什么?I2S(Inter-IC Sound)是由飞利浦公司开发的一种数字音频传输协议,主要用于连接音频编解码器、数字麦克风和扬声器等音频设备。
- I2S 协议通过差分信号传输数字音频数据,主要包含以下几种关键信号:
- 时钟信号:位时钟(BCLK/SCK):用于同步数据传输的时钟信号。帧时钟(LRCK/WS):用于区分左右声道数据,频率等于音频采样率。当 LRCK 为高电平时表示传输左声道数据,为低电平时表示传输右声道数据。
- 数据信号:串行数据输出(SDO/Dout):从主设备发送到从设备的音频数据。 串行数据输入(SDI/Din):从从设备接收至主设备的音频数据。
- ESP32 集成了两个 I2S 控制器(I2S0 和 I2S1),具有以下特性:
- 全双工数据传输
- 实例代码:先把data文件夹下面的代码传到单片机

9.使用I2S协议录制音频
- 流程:分别初始化MAX98357数字功放模块的I2S通道和麦克风I2S通道,监听串口,如果串口有数据输入,通过麦克风录制5s音频,通过喇叭播放这5s音频数据
- 没有敲代码,也是用的实例代码。
10.FreeRTOS多任务创建
- ESP32是双核芯片,没必要一直用loop函数单核去调用,浪费资源。
-
#include <Arduino.h> TaskHandle_t printMessageTask1; TaskHandle_t printMessageTask2; TaskHandle_t printMessageTask3; TaskHandle_t printMessageTask4; void printMessage(void* ptr) { const char* msg = static_cast<char*>(ptr); while (true) { Serial.printf("当前任务参数: %s, 运行CPU: %d\n", msg, xPortGetCoreID()); vTaskDelay(pdMS_TO_TICKS(1000)); } // 这是第一个永久运行的任务,所以该行代码不会被运行,如果是会结束的任务, // 则需要在任务的最后,删除当前任务,参数为NULL表示删除自己 vTaskDelete(nullptr); } void setup() { Serial.begin(9600); vTaskDelay(pdMS_TO_TICKS(3000)); xTaskCreate(printMessage, // 任务函数指针 "printMessage1", // 任务名称 2048, // 任务堆栈大小,简单的任务堆栈可以设置小一些,复杂的任务需要设置大一点得到堆栈,不然容易堆栈溢出 const_cast<char*>("这是第1个任务参数"), // 需要传递什么参数到任务处理函数中 1, // 任务优先级,可选范围为0-24,数字越大,优先级越高 &printMessageTask1); // 任务句柄,如果后续对该任务有控制需求,可以设置该参数,TaskHandle_t类型 xTaskCreate(printMessage, // 任务函数指针 "printMessage2", // 任务名称 4096, // 任务堆栈大小,简单的任务堆栈可以设置小一些,复杂的任务需要设置大一点得到堆栈,不然容易堆栈溢出 const_cast<char*>("这是第2个任务参数"), // 需要传递什么参数到任务处理函数中 2, // 任务优先级,可选范围为0-24,数字越大,优先级越高 &printMessageTask2); // 任务句柄,如果后续对该任务有控制需求,可以设置该参数,TaskHandle_t类型 vTaskDelay(pdMS_TO_TICKS(5000)); // 5s以后,删除以上两个任务 vTaskDelete(printMessageTask1); vTaskDelete(printMessageTask2); xTaskCreatePinnedToCore(printMessage, "printMessage", 2048, const_cast<char*>("第3个任务参数"), 1, &printMessageTask3, 0); xTaskCreatePinnedToCore(printMessage, "printMessage", 2048, const_cast<char*>("第4个任务参数"), 2, &printMessageTask4, 1); vTaskDelay(pdMS_TO_TICKS(5000)); // 5s以后,删除以上两个任务 vTaskDelete(printMessageTask3); vTaskDelete(printMessageTask4); } void loop() { }
11.FreeRTOS信号量使用
-
有些时候,我们需要在不同的任务中相互等待,协同工作,其中一种解决方案就是使用信号量才进行操作,信号量可分为二值信号量和计数信号量,其中二值信号量使用较为广泛
-
#include <Arduino.h> #include <freertos/FreeRTOS.h> #include <freertos/task.h> #include <freertos/semphr.h> SemaphoreHandle_t semaphore; void count(void* pvParameters) { Serial.println("开始报数..."); for (int i = 1; i <= 10; i++) { Serial.printf("当前报数:%d\n", i); vTaskDelay(pdMS_TO_TICKS(1000)); } xSemaphoreGive(semaphore); // 报数结束释放信号量,这里释放时候,setup函数中才能Take成功 vTaskDelete(nullptr); } void setup() { Serial.begin(9600); semaphore = xSemaphoreCreateBinary(); // 创建一个报数任务,从1一直数到10报数完成 xTaskCreate(count, "count", 2048, nullptr, 1, nullptr); // 等待获取信号量(上一个任务报数完成才会释放信号量),获取不到就一直等着 xSemaphoreTake(semaphore, portMAX_DELAY); Serial.println("成功获取信号量"); } void loop() { }
12.FreeRTOS互斥信号量使用
- 互斥量的作用的保证某一个时刻,只有一个任务能够干某件事情,我们后续使用lvgl驱动屏幕的时候,会大量使用这种互斥量来保证线程安全。
#include <Arduino.h>
volatile int totalCount;
SemaphoreHandle_t semaphore1;
SemaphoreHandle_t semaphore2;
SemaphoreHandle_t mutex;
void count1(void* ptr)
{
Serial.println("任务1开始计数");
for (int i = 0; i < 10000; i++)
{
// if (xSemaphoreTake(mutex, portMAX_DELAY) == pdTRUE)
// {
totalCount++;
// xSemaphoreGive(mutex);
// }
// vTaskDelay(pdMS_TO_TICKS(1));//避免for循环耗费大量时间触发看门狗。此时for时间不长
}
xSemaphoreGive(semaphore1);
vTaskDelete(nullptr);
}
void count2(void* ptr)
{
Serial.println("任务2开始计数");
for (int i = 0; i < 10000; i++)
{
// if (xSemaphoreTake(mutex, portMAX_DELAY) == pdTRUE)
// {
totalCount++;
// xSemaphoreGive(mutex);
// }
// vTaskDelay(pdMS_TO_TICKS(1));
}
xSemaphoreGive(semaphore2);
vTaskDelete(nullptr);
}
void setup()
{
Serial.begin(9600);
vTaskDelay(pdMS_TO_TICKS(3000));
totalCount = 0;
semaphore1 = xSemaphoreCreateBinary();
semaphore2 = xSemaphoreCreateBinary();
mutex = xSemaphoreCreateMutex();
// 创建两个计数任务,每个任务分别往totalCount上累加10000
xTaskCreatePinnedToCore(count1, "count-task1", 1024, nullptr, 1, nullptr, 0);
xTaskCreatePinnedToCore(count2, "count-task2", 1024, nullptr, 1, nullptr, 1);
// 等待两个任务都全部结束
xSemaphoreTake(semaphore1, portMAX_DELAY);
xSemaphoreTake(semaphore2, portMAX_DELAY);
// 打印最终计数值(期望是20000)
Serial.printf("total count = %d\n", totalCount);
}
void loop()
{
}
- 这里推荐使用递归型的函数。创建:xSemaphoreCreateRecursiveMutex()。获取:xSemaphoreTakeRecursiveMutex()。释放:xSemaphoreGiveRecursive()。作用:同一个线程前面获取了,后面也能获取
13.剩余的FreeRTOS的相关内容
14.ESP日志系统使用
- 使用日志需要把波特率设置为115200。
- 串口和日志同时使用需要在setup中把波特率设置为115200。
- 为什么要使用日志:查看一些状态值。
15.大模型语音识别

- 登录火山引擎。
- WebSocket是什么?全双工一边接受数据一边发送给云端。从第三方库去引用WebSocket。
- 根据文档去书写代码:文档
- 主要目的:语音去转文字
-
#include <Arduino.h> #include "WebSocketsClient.h" #include "ArduinoJson.h" #include "utils.h" #include "driver/i2s.h" #include "freertos/ringbuf.h" #define MICROPHONE_I2S_NUM I2S_NUM_1 #define AUDIO_SAMPLE_RATE 16000 #define AUDIO_RECORDING_SAMPLE_NUMBER 160 // 10ms音频样本数 #define MICROPHONE_I2S_BCLK 42 #define MICROPHONE_I2S_LRC 2 #define MICROPHONE_I2S_DOUT 1 #define CHUNK_SIZE 800 // 50ms音频大小 #define TASK_COMPLETED_EVENT (1<<0) // 表示一次语音识别任务结束的时间位 const char* TAG = "ASR"; // 默认头部 constexpr byte DoubaoASRDefaultFullClientWsHeader[] = {0x11, 0x10, 0x10, 0x00}; constexpr byte DoubaoASRDefaultAudioOnlyWsHeader[] = {0x11, 0x20, 0x10, 0x00}; constexpr byte DoubaoASRDefaultLastAudioWsHeader[] = {0x11, 0x22, 0x10, 0x00}; int16_t buffer[CHUNK_SIZE]; RingbufHandle_t ringBuffer; // 用来暂存录音的环形缓冲区 EventGroupHandle_t eventGroup; size_t bytesRead; std::vector<uint8_t> requestBuilder; WebSocketsClient client; void parseResponse(const uint8_t* response) { const uint8_t messageType = response[1] >> 4; const uint8_t* payload = response + 4; ESP_LOGI(TAG, "message type: %d", messageType); switch (messageType) { case 0b1001: { // 服务端下发包含识别结果的 full server response const uint32_t payloadSize = readInt32(payload); payload += 4; std::string recognizeResult = readString(payload, payloadSize); JsonDocument jsonResult; const DeserializationError err = deserializeJson(jsonResult, recognizeResult); if (err) { ESP_LOGE(TAG, "parse speech recognize result failed: %s", err.c_str()); return; } const String reqId = jsonResult["reqid"]; const int32_t code = jsonResult["code"]; const String message = jsonResult["message"]; const int32_t sequence = jsonResult["sequence"]; const JsonArray result = jsonResult["result"]; ESP_LOGI(TAG, "sequence = %d, code = %d, message = %s, result size = %d", sequence, code, message.c_str(), result.size()); if (code == 1000 && result.size() > 0) { for (const auto& item : result) { String text = item["text"]; ESP_LOGD(TAG, "text = %s", text.c_str()); // sequence小于0,表示这是最后一个数据包,直接可以打印语音识别全部内容 if (sequence < 0) { ESP_LOGI(TAG, "speech recognize result: %s", text.c_str()); // 这是服务器返回的最后一个数据包,表示任务结束,往事件组发送事件,通知另一个任务可以结束了 xEventGroupSetBits(eventGroup, TASK_COMPLETED_EVENT); } } } break; } case 0b1111: { // 服务端处理错误时下发的消息类型(如无效的消息格式,不支持的序列化方法等) const uint32_t errorCode = readInt32(payload); payload += 4; const uint32_t messageLength = readInt32(payload); payload += 4; const std::string errorMessage = readString(payload, messageLength); ESP_LOGE(TAG, "speech recognize failed: "); ESP_LOGE(TAG, " errorCode = %u\n", errorCode); ESP_LOGE(TAG, "errorMessage = %s\n", errorMessage.c_str()); } default: { break; } } } void eventCallback(WStype_t type, uint8_t* payload, size_t length) { switch (type) { case WStype_PING: case WStype_ERROR: break; case WStype_CONNECTED: ESP_LOGI(TAG, "websocket连接成功"); break; case WStype_DISCONNECTED: ESP_LOGI(TAG, "websocket断开连接"); break; case WStype_TEXT: { break; } case WStype_BIN: parseResponse(payload); break; default: break; } } void buildFullClientRequest() { JsonDocument doc; doc.clear(); const JsonObject app = doc["app"].to<JsonObject>(); app["appid"] = "自己的火山ID"; app["cluster"] = "volcengine_streaming_common"; app["token"] = "自己的token"; const JsonObject user = doc["user"].to<JsonObject>(); user["uid"] = getChipId(nullptr); const JsonObject request = doc["request"].to<JsonObject>(); request["reqid"] = generateTaskId(); request["nbest"] = 1; // request["result_type"] = "full"; request["sequence"] = 1; request["workflow"] = "audio_in,resample,partition,vad,fe,decode,itn,nlu_ddc,nlu_punctuate"; const JsonObject audio = doc["audio"].to<JsonObject>(); audio["format"] = "raw"; audio["codec"] = "raw"; audio["channel"] = 1; audio["rate"] = AUDIO_SAMPLE_RATE; String payloadStr; serializeJson(doc, payloadStr); uint8_t payload[payloadStr.length() + 1]; for (int i = 0; i < payloadStr.length(); i++) { payload[i] = static_cast<uint8_t>(payloadStr.charAt(i)); } payload[payloadStr.length()] = '\0'; std::vector<uint8_t> payloadSize = uint32ToUint8Array(payloadStr.length()); requestBuilder.clear(); // 先写入报头(四字节) requestBuilder.insert(requestBuilder.end(), DoubaoASRDefaultFullClientWsHeader, DoubaoASRDefaultFullClientWsHeader + sizeof(DoubaoASRDefaultFullClientWsHeader)); // 写入payload长度(四字节) requestBuilder.insert(requestBuilder.end(), payloadSize.begin(), payloadSize.end()); // 写入payload内容 requestBuilder.insert(requestBuilder.end(), payload, payload + payloadStr.length()); } void buildAudioOnlyRequest(uint8_t* audio, const size_t size, const bool lastPacket) { requestBuilder.clear(); std::vector<uint8_t> payloadLength = uint32ToUint8Array(size); if (lastPacket) { // 先写入报头(四字节) requestBuilder.insert(requestBuilder.end(), DoubaoASRDefaultLastAudioWsHeader, DoubaoASRDefaultLastAudioWsHeader + sizeof(DoubaoASRDefaultLastAudioWsHeader)); } else { // 先写入报头(四字节) requestBuilder.insert(requestBuilder.end(), DoubaoASRDefaultAudioOnlyWsHeader, DoubaoASRDefaultAudioOnlyWsHeader + sizeof(DoubaoASRDefaultAudioOnlyWsHeader)); } // 写入payload长度(四字节) requestBuilder.insert(requestBuilder.end(), payloadLength.begin(), payloadLength.end()); // 写入payload内容 requestBuilder.insert(requestBuilder.end(), audio, audio + size); } void asr(uint8_t* buffer, const size_t size, const bool firstPacket, const bool lastPacket) { ESP_LOGI(TAG, "开始语音识别, 音频长度: %d, fistPacket = %d, lastPacket = %d", size, firstPacket, lastPacket); if (firstPacket) { xEventGroupClearBits(eventGroup, TASK_COMPLETED_EVENT); while (!client.isConnected()) { // 如果websocket没有连接,持续调用websocket的loop函数(函数内部会有连接的创建逻辑),知道连接成功才继续往后 client.loop(); vTaskDelay(1); } // 构建第一个语音识别请求的相关报文头,可以参考官方文档:https://www.volcengine.com/docs/6561/80818 buildFullClientRequest(); // 第一个数据包发往服务器,开启识别任务 if (!client.sendBIN(requestBuilder.data(), requestBuilder.size())) { ESP_LOGD(TAG, "发送语音识别请求第一个数据包失败"); } // 给loop一个执行的机会,接收可能的服务器端下发的数据 client.loop(); } // 构建语音数据包 buildAudioOnlyRequest(buffer, size, lastPacket); if (!client.sendBIN(requestBuilder.data(), requestBuilder.size())) { ESP_LOGE(TAG, "发送语音识别音频数据包失败..."); } // 继续给loop函数执行的机会 client.loop(); if (lastPacket) { // 如果已经往服务器发送了最后一个语音识别数据包,则等待任务结束 while ((xEventGroupWaitBits(eventGroup, TASK_COMPLETED_EVENT, false, true, pdMS_TO_TICKS(1)) & TASK_COMPLETED_EVENT) == 0) { // 持续调用loop,接收服务器下发的数据 client.loop(); vTaskDelay(1); } // 任务完成,关闭websocket连接 client.disconnect(); } } void consumeRingBuffer(void* ptr) { size_t bytesRead; bool firstPacket = true; // 流式语音识别,用这个表示这是识别的第一个语音包 while (true) { void* buffer = xRingbufferReceive(ringBuffer, &bytesRead, pdMS_TO_TICKS(100)); if (buffer != nullptr) { ESP_LOGI(TAG, "从RingBuffer读到音频数据,长度: %d", bytesRead); auto* audioData = static_cast<uint8_t*>(buffer); asr(audioData, bytesRead, firstPacket, false); if (firstPacket) { firstPacket = false; } vRingbufferReturnItem(ringBuffer, buffer); } else if (!firstPacket) { // 模拟最后一个空报文,没有任何音频数据,主要作用是让服务端结束一轮识别任务,返回最终的识别内容 uint8_t fakeAudio[1] = {0}; asr(fakeAudio, 1, firstPacket, true); firstPacket = true; } vTaskDelay(pdMS_TO_TICKS(1)); } } void setup() { Serial.begin(9600); constexpr i2s_config_t i2s_config = { .mode = static_cast<i2s_mode_t>(I2S_MODE_MASTER | I2S_MODE_RX), .sample_rate = AUDIO_SAMPLE_RATE, .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, // 这里的左右声道要和电路保持一致 .communication_format = I2S_COMM_FORMAT_STAND_I2S, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = 4, .dma_buf_len = 1024, .use_apll = false }; constexpr i2s_pin_config_t pin_config = { .bck_io_num = MICROPHONE_I2S_BCLK, .ws_io_num = MICROPHONE_I2S_LRC, .data_out_num = -1, .data_in_num = MICROPHONE_I2S_DOUT }; i2s_driver_install(MICROPHONE_I2S_NUM, &i2s_config, 0, nullptr); i2s_set_pin(MICROPHONE_I2S_NUM, &pin_config); i2s_zero_dma_buffer(MICROPHONE_I2S_NUM); WiFiClass::mode(WIFI_MODE_STA); WiFi.begin("WiFi名称", "密码"); ESP_LOGI(TAG, "正在联网"); while (WiFiClass::status() != WL_CONNECTED) { ESP_LOGI(TAG, "."); vTaskDelay(1000); } ESP_LOGI(TAG, "联网成功"); // 这里的4YOzBPBOFizGvhWbqZroVA3fTXQbeWOW需要换成你自己的access token client.setExtraHeaders("Authorization: Bearer; 4YOzBPBOFizGvhWbqZroVA3fTXQbeWOW"); client.beginSSL("openspeech.bytedance.com", 443, "/api/v2/asr"); client.onEvent(eventCallback); ringBuffer = xRingbufferCreate(80000 * sizeof(int16_t), RINGBUF_TYPE_BYTEBUF); eventGroup = xEventGroupCreate(); xTaskCreate(consumeRingBuffer, "consumeRingBuffer", 4096, nullptr, 1, nullptr); } void loop() { if (Serial.available() > 0) { Serial.readStringUntil('\n'); ESP_LOGI(TAG, "开始录音,请说话,持续时间5s..."); // 录制100次,每次录制50ms的音频 for (int i = 0; i < 100; i++) { const esp_err_t err = i2s_read(MICROPHONE_I2S_NUM, buffer, CHUNK_SIZE * sizeof(int16_t), // 每次录取50ms音频 &bytesRead, portMAX_DELAY); if (err == ESP_OK) { // 录到的音频,直接发往环形缓冲区 BaseType_t result = xRingbufferSend(ringBuffer, buffer, bytesRead, portMAX_DELAY); if (result != pdTRUE) { ESP_LOGE(TAG, "将录音数据发送到RingBuffer失败"); } } } ESP_LOGI(TAG, "录音结束"); } }
16.大模型语音识别实现
- 登录火山引擎,打开网页界面

- 目的:给一段文字,让AI读出来。
17.ESP32异常堆栈分析方法
- 将报错的堆栈地址,转化为代码出错的具体行号。

- 然后再分析具体哪里出错了。
18.Coze智能体接入
- 目的:串口输入一个问题。通过流式调用coze智能体获取回复。将智能体回复的内容打印在串口

- 其中200个结果里面,只有answer有用,且是累加字符回答的,代码中的输出也是一样的
19.将上述三个结合起来,形成完整语音对话流程
- 目的:串口监听信号,键盘按下回车键后开始说话 持续录音5s,将录音进行语音识别得到文字 将文字发送给coze智能体进行大模型对话 返回的回复内容进行语音合成 合成后的音频通过喇叭播放出来
20.使用TFT_eSPI库点亮屏幕
- 引入第三方库。

- 启用PSRAM。2行代码:

- 启用我们外设对应的芯片


- 还有一些配置,没有书写上面。到时候可以看视频。
- 以后可能库会更新,我们在PlatformIO里面去配置我们刚刚的东西。无论以后库再怎么更新都不会影响我们的配置。

21.LVGL点亮屏幕
-
引入lvgl库
-

-
拷贝lvgl库根目录下的lv_conf_template.h文件到项目include目录下,并重命名为lv_conf.h,并且在项目platformio.ini文件中加上-I include 选项
22.LVGL运行内置示例程序
- 这个看课程文档方便理解。运行示例代码。
23.LVGL触摸驱动接入
- 看课程文档
24.LVGL使用PSRAM
- 类似这样灰色的宏定义是没有启用的。

25.LVGL本地模拟器环境搭建
- 为什么要部署本地模拟器?因为效率提高的原因。减去调试,烧录,编译库函数的耗时问题。
- 部署完成快速验证和快速试错。

26.LVGL本地模拟器编译太慢解决方法
- 看文档,更详细。
27.LVGL模拟器项目运行机制

- 在上述红圈去绘制我们的代码,绘制一些图像信息。
28.LVGL的基本概念



-
对象(Object)
屏幕(Screen)
控件(Widget)
样式(Style)
事件(Event)
布局(Layout)
显示缓冲区(Display Buffer)
输入设备(Input Device)
29.LVGL对象(Object)
- 图层是堆叠状态。sys是最高图层。

- 代码记忆。

30.对象的对齐方式

- 函数参数填这些可能的值的结果。
31.LVGL盒子模型
32.LVGL样式(Style)
- 一般定义样式,要设置为static。让生命周期不再局限于自定义函数。避免函数运行完后销毁样式,导致一些内存问题。

33.LVGL事件(Event)
- 写完函数代码然后在括号紧跟.var,会帮我们生成别的代码。
- 看视频原文档记忆函数,并且跟着书写代码。
-
// 引入LVGL核心库头文件,必须引入才能使用所有LVGL控件、事件、绘图API #include "lvgl.h" // 引入自定义UI功能头文件,存放本模块对外声明、宏定义、函数声明 #include "my_ui.h" /** * @brief 自定义UI控件事件回调函数 * @param e: LVGL事件结构体指针,存储当前触发的事件信息、控件信息、用户数据等 * @note 绑定到UI控件上,控件产生任意事件时自动触发该回调函数 * @retval 无 */ void my_ui_event_cb(lv_event_t * e) { // 获取当前控件触发的事件类型(点击、滑动、按下、释放等) lv_event_code_t code = lv_event_get_code(e); // 获取绑定时传入的自定义用户数据,可用于区分不同控件、传递自定义参数 char* user_data = lv_event_get_user_data(e); // 根据不同事件类型,执行对应逻辑 switch (code) { // 监听控件点击事件(鼠标点击/触摸屏点击抬起) case LV_EVENT_CLICKED: // 在当前活动屏幕上创建一个弹窗消息框控件 lv_obj_t * msgbox = lv_msgbox_create(lv_scr_act()); // 将消息框居中显示在屏幕上 lv_obj_center(msgbox); // 定义字符数组,用于存储弹窗展示的文本内容,预留30字节缓存空间 char message[30]; // 格式化字符串:将自定义用户数据写入文本缓存,避免乱码、格式异常 // 参数:缓存地址、缓存最大长度、格式化规则、待填充数据 lv_snprintf(message, sizeof(message), "%s", user_data); // 给消息框添加展示文本,显示自定义内容 lv_msgbox_add_text(msgbox, message); // 为消息框添加关闭按钮,支持手动关闭弹窗 lv_msgbox_add_close_button(msgbox); break; // 其余所有未知/未监听事件,直接忽略,不执行任何逻辑 default: break; } } /** * @brief UI界面初始化展示函数 * @note 程序调用该函数即可创建并显示自定义UI界面 * @retval 无 */ void show_ui() { // 在当前活动屏幕创建一个基础矩形容器控件(空白画布) lv_obj_t * obj = lv_obj_create(lv_scr_act()); // 设置容器控件尺寸:宽240像素、高200像素 lv_obj_set_size(obj, 240, 200); // 将容器控件在屏幕居中显示 lv_obj_center(obj); // 为容器控件绑定事件回调函数 // 参数说明: // 1. 目标控件对象 2. 回调函数地址 3. 监听所有事件类型 4. 自定义用户数据 // cb 为 callback(回调函数)的缩写,控件触发任意事件都会进入my_ui_event_cb逻辑 lv_obj_add_event_cb(obj, my_ui_event_cb, LV_EVENT_ALL, "gaoss"); }34.LVGL按钮(Button)
- 标准三行代码:

- 空格加字符串里面:

- 怎么将代码烧录到我们的学习板上面?
- 用触摸驱动接入那节课的文件去学习。
- 怎么将WS2812小灯启动?引入FastLED库
-
#include <lvgl.h> #include <TFT_eSPI.h> #include "FT6336.h" #include "RGB.h" #define TOUCH_SDA 10 #define TOUCH_SCL 13 #define TOUCH_INT 12 #define TOUCH_RST 9 #define TFT_HOR_RES 240 #define TFT_VER_RES 320 #define TFT_ROTATION LV_DISPLAY_ROTATION_0 #define DRAW_BUF_SIZE (TFT_HOR_RES * TFT_VER_RES / 10 * (LV_COLOR_DEPTH / 8)) uint32_t draw_buf[DRAW_BUF_SIZE / 4]; FT6336 ft(TOUCH_SDA, TOUCH_SCL, TOUCH_INT, TOUCH_RST, TFT_HOR_RES, TFT_VER_RES); /*Read the touchpad*/ void my_touchpad_read(lv_indev_t* indev, lv_indev_data_t* data) { ft.read(); if(!ft.isTouched) { data->state = LV_INDEV_STATE_RELEASED; } else { data->state = LV_INDEV_STATE_PRESSED; data->point.x = ft.points[0].x; data->point.y = ft.points[0].y; } } /*use Arduinos millis() as tick source*/ static uint32_t my_tick(void) { return millis(); } void event_callback(lv_event_t * e) { lv_event_code_t lv_event_code = lv_event_get_code(e); lv_obj_t * button = (lv_obj_t *)lv_event_get_target(e); lv_obj_t * label = lv_obj_get_child(button,0); const char * label_get_text = lv_label_get_text(label); switch (lv_event_code) { case LV_EVENT_CLICKED: if (lv_strcmp(label_get_text,LV_SYMBOL_POWER" ON")==0) { lv_label_set_text(label,LV_SYMBOL_POWER" OFF"); lv_obj_set_style_bg_color(button,lv_color_hex(0xFF0000),0); RGB::powerOn(); } else { lv_label_set_text(label,LV_SYMBOL_POWER" ON"); lv_obj_set_style_bg_color(button,lv_color_hex(0x00AA00),0); RGB::powerOff(); } break; default: break; } } void setup() { lv_init(); ft.begin(); RGB::begin(); /*Set a tick source so that LVGL will know how much time elapsed. */ lv_tick_set_cb(my_tick); lv_display_t* disp; disp = lv_tft_espi_create(TFT_HOR_RES, TFT_VER_RES, draw_buf, sizeof(draw_buf)); lv_display_set_rotation(disp, TFT_ROTATION); lv_indev_t* indev = lv_indev_create(); lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER); /*Touchpad should have POINTER type*/ lv_indev_set_read_cb(indev, my_touchpad_read); lv_obj_t * obj = lv_button_create(lv_scr_act()); lv_obj_set_size(obj,100,30); lv_obj_center(obj); lv_obj_t * label = lv_label_create(obj);//创建label lv_obj_center(label); lv_label_set_text(label,LV_SYMBOL_POWER" ON"); lv_obj_set_style_bg_color(obj,lv_color_hex(0x00AA00),0); lv_obj_add_event_cb(obj,event_callback,LV_EVENT_CLICKED,NULL); } void loop() { lv_timer_handler(); /* let the GUI do its work */ delay(5); /* let this time pass */ }代码里面很多是复制23小节的触摸驱动接入。
35.LVGL样式选择器
- 标准三行代码:

- 运行结果:



- 上述代码的选择器:主体加默认以及主体加按下。
- 效果:按下变颜色,字体变红色

-
void show_ui() { lv_obj_t * button = lv_button_create(lv_scr_act()); lv_obj_set_size(button, 200, 50); lv_obj_center(button); lv_obj_t * label = lv_label_create(button); lv_label_set_text(label, "click me"); lv_obj_center(label); lv_obj_set_style_bg_color(button,lv_color_hex(0x0000FF),LV_PART_MAIN|LV_STATE_DEFAULT); lv_obj_set_style_bg_color(button,lv_color_hex(0x00FFFF),LV_PART_MAIN|LV_STATE_PRESSED); lv_obj_set_style_text_color(button,lv_color_hex(0xFF0000),LV_PART_MAIN|LV_STATE_PRESSED); }
36.LVGL开关(Switch)
-
目的:在屏幕上显示一个开关,当开关打开时,显示一个按钮,当开关关闭时,按钮隐藏
-
灵活设置开关的颜色
-
练习之前学习的对象之间的各种对齐方式
-
开关开启才叫指示器indicator。后面是开关状态为滑动切换颜色。
-

-

-
回调函数模板。
-

-
效果截图:

-

//
// Created by Admin on 2025/7/7.
//
#include "my_ui.h"
#include "lvgl.h"
static lv_obj_t *button;
void switch_event_cb(lv_event_t *e) {
const lv_event_code_t code = lv_event_get_code(e);
switch (code) {
case LV_EVENT_VALUE_CHANGED: {
lv_obj_t *Switch = lv_event_get_target(e);
bool has_state = lv_obj_has_state(Switch,LV_STATE_CHECKED);
if(button==NULL) {//开启开关
button=lv_button_create(lv_scr_act());
lv_obj_set_size(button, 50, 30);
lv_obj_align(button,LV_ALIGN_CENTER,0,-100);
} if(has_state) {
lv_obj_remove_flag(button,LV_OBJ_FLAG_HIDDEN);//显示按钮
} else {
lv_obj_add_flag(button,LV_OBJ_FLAG_HIDDEN);//隐藏按钮
}
break;
}
default:
break;
}
}
void show_ui()
{
lv_obj_t * Switch= lv_switch_create(lv_scr_act());
lv_obj_set_size(Switch, 200, 30);
lv_obj_center(Switch);
lv_obj_set_style_bg_color(Switch,lv_color_hex(0x00FF00),LV_PART_INDICATOR|LV_STATE_CHECKED);
lv_obj_set_style_bg_color(Switch,lv_color_hex(0xEE0000),LV_PART_MAIN);
lv_obj_set_style_bg_color(Switch,lv_color_hex(0x00FFFF),LV_PART_KNOB);
lv_obj_t * left_label = lv_label_create(lv_scr_act());
lv_obj_t * right_label = lv_label_create(lv_scr_act());
lv_label_set_text(left_label,"hide");
lv_label_set_text(right_label,"show");
lv_obj_align_to(left_label,Switch,LV_ALIGN_OUT_LEFT_MID,-10,0);
lv_obj_align_to(right_label,Switch,LV_ALIGN_OUT_RIGHT_MID,10,0);
lv_obj_add_event_cb(Switch,switch_event_cb,LV_EVENT_VALUE_CHANGED,NULL);
}
37.LVGL滑块(Slider)
- 看视频文档,代码差不多。
- 三种模式:普通,对称,范围。
38.LVGL下拉列表(Dropdown)
- 看视频文档学习即可。有默认文字内容,列表内容,以及符号三个地方书写代码。
39.LVGL复选框(Checkbox)
- 函数整体接口较少。看示例代码即可。
40.LVGL进度条(Bar)
- 普通,对称,范围。三种模式。
- 只作为数据展示,无法拖动,slider可以拖动,只读的状态。
-
// // Created by Admin on 2025/7/7. // #include "my_ui.h" #include "lvgl.h" void show_ui() { //创建进度条 lv_obj_t * bar = lv_bar_create(lv_scr_act()); lv_obj_set_size(bar,250,15); //lv_obj_set_size(bar,15,250); //lv_obj_set_size(bar,200,200); //进度条居中 lv_obj_center(bar); //普通模式 //lv_bar_set_mode(bar,LV_BAR_MODE_NORMAL); //lv_bar_set_mode(bar,LV_BAR_MODE_SYMMETRICAL);//对称模式 lv_bar_set_mode(bar,LV_BAR_MODE_RANGE);//范围模式 //设置进度条范围 lv_bar_set_range(bar,-100,100); //设置动画时间为500ms lv_obj_set_style_anim_time(bar,500,LV_PART_MAIN); //设置进度条指示器背景颜色为红色 lv_obj_set_style_bg_color(bar,lv_color_hex(0xff0000),LV_PART_INDICATOR); //设置进度条指示器背景颜色渐变为绿色 lv_obj_set_style_bg_grad_color(bar,lv_color_hex(0x00ff00),LV_PART_INDICATOR); //设置进度条指示器背景颜色是水平渐变 lv_obj_set_style_bg_grad_dir(bar,LV_GRAD_DIR_HOR,LV_PART_INDICATOR); //范围模式下,设置区间左值(注意:该函数必须在范围模式下才生效) lv_bar_set_start_value(bar,-50,LV_ANIM_ON); //lv_bar_set_value(bar,88,LV_ANIM_ON); }
41.LVGL图像
- 怎么在LVGL显示图片。
- 熟悉的三行代码:

- 其中设置完图标,可以在图标后面加入文字内容
- 通过这个网站把图片生成代码格式:图片必须提前适合我们屏幕240*320。

- 把生成的.c文件拖到我们的根目录下。然后为了引入我们工程,把.c文件改为.h

-
// // Created by Admin on 2025/7/7. // #include "my_ui.h" #include "lvgl.h" #include "IMG_202607048743_240x320.h" void show_ui() { lv_obj_t * image = lv_image_create(lv_scr_act()); lv_obj_set_size(image,240,320); lv_obj_center(image); //lv_image_set_src(image,LV_SYMBOL_WIFI"WiFi Settings");//设置图标 lv_image_set_src(image,&IMG_202607048743_240x320); //设置透明度 lv_obj_set_style_img_opa(image,255,0); //设置缩放比例 lv_image_set_scale(image,255); //设置颜色滤镜,也要设置透明度 lv_obj_set_style_img_recolor(image,lv_color_hex(0xFF0000),LV_PART_MAIN); lv_obj_set_style_img_recolor_opa(image,100,0); //图片旋转 lv_image_set_pivot(image,10,100);//设置旋转中心 lv_image_set_rotation(image,180); }
42.LVGL文本框(Textarea)和键盘(keyboard)
- lv_obj_align_to()这个函数是相对于某个对象进行对齐。
-
#include "my_ui.h" #include "lvgl.h" static lv_obj_t *keyboard; void textarea_event_callback(lv_event_t* e) { const lv_event_code_t code = lv_event_get_code(e); // 输入框获得焦点或者被点击 if (code == LV_EVENT_FOCUSED || code == LV_EVENT_CLICKED) { // 显示键盘 lv_obj_remove_flag(keyboard, LV_OBJ_FLAG_HIDDEN); } else if (code == LV_EVENT_DEFOCUSED || code == LV_EVENT_READY || code == LV_EVENT_LEAVE) // 输入框失去焦点,或者按下键盘上的OK按键 { // 隐藏键盘 lv_obj_add_flag(keyboard, LV_OBJ_FLAG_HIDDEN); } } void show_ui() { lv_obj_t* ta = lv_textarea_create(lv_scr_act()); lv_obj_set_size(ta, 200, 40); lv_obj_align(ta, LV_ALIGN_TOP_MID, 10, 20); lv_obj_t* label = lv_label_create(lv_scr_act()); lv_label_set_text(label, LV_SYMBOL_CALL); lv_obj_align_to(label, ta, LV_ALIGN_OUT_LEFT_MID, -5, 0); // 设置为密码模式 lv_textarea_set_password_mode(ta, false); // 设置单行模式 lv_textarea_set_one_line(ta, true); // 设置输入框可以输入的字符集合 lv_textarea_set_accepted_chars(ta, "1234567890-"); // 设置输入框最大输入字符数 lv_textarea_set_max_length(ta, 13); // 设置输入框输入提示 lv_textarea_set_placeholder_text(ta, "phone number"); // 创建一个键盘 keyboard = lv_keyboard_create(lv_scr_act()); // 设置键盘模式为数字模式(其余还有大写模式、小写模式、字符模式) lv_keyboard_set_mode(keyboard, LV_KEYBOARD_MODE_NUMBER); // 将键盘与输入框进行绑定 lv_keyboard_set_textarea(keyboard, ta); // 开始时隐藏键盘 lv_obj_add_flag(keyboard, LV_OBJ_FLAG_HIDDEN); // 为输入框添加事件响应函数 lv_obj_add_event_cb(ta, textarea_event_callback, LV_EVENT_ALL, NULL); }
43.LVGL列表(list)
- 有很多代码,看示例代码即可。
44.LVGL表格(Table)
-
// // Created by Admin on 2025/7/7. // #include "my_ui.h" #include "lvgl.h" static lv_obj_t * label; void table_event_callback(lv_event_t* e) { const lv_event_code_t code = lv_event_get_code(e); lv_obj_t* table = lv_event_get_target(e); if (code == LV_EVENT_FOCUSED) { // 获取表格中当前被点击的单元格的坐标 uint32_t row, col; lv_table_get_selected_cell(table, &row, &col); // 拿到指定坐标的单元格的值 const char* value = lv_table_get_cell_value(table, row, col); // 显示在标签上 lv_label_set_text(label, value); lv_obj_align_to(label, table, LV_ALIGN_OUT_BOTTOM_MID, 0, 5); } } void show_ui() { lv_obj_t * table = lv_table_create(lv_scr_act()); lv_obj_set_size(table,220,250); lv_obj_center(table); lv_table_set_column_count(table,2);//设置2列 lv_table_set_column_width(table,0,150); lv_table_set_column_width(table,1,70); //设置单元格字体对齐方式为居中对齐 lv_obj_set_style_text_align(table,LV_TEXT_ALIGN_CENTER, LV_PART_ITEMS); //设置单元格被选中时,字体颜色变成红色 lv_obj_set_style_text_color(table,lv_color_hex(0xFF0000), LV_PART_ITEMS|LV_STATE_FOCUSED); //设置第一行 lv_table_set_cell_value(table,0,0,"Student Info"); //设置第二行 lv_table_set_cell_value(table,1,0,"Name"); lv_table_set_cell_value(table,1,1,"Age"); //设置第三行 lv_table_set_cell_value(table,2,0,"Tom"); lv_table_set_cell_value(table,2,1,"18"); //设置第四行 lv_table_set_cell_value(table,3,0,"Jerry"); lv_table_set_cell_value(table,3,1,"19"); //设置第五行 lv_table_set_cell_value(table,4,0,"Jane"); lv_table_set_cell_value(table,4,1,"20"); // 设置第一行、第一列与右边得到单元格合并 lv_table_set_cell_ctrl(table, 0, 0, LV_TABLE_CELL_CTRL_MERGE_RIGHT); // 创建一个标签,用于展示表格上被点击的单元格的内容 label = lv_label_create(lv_scr_act()); lv_label_set_text(label, ""); lv_obj_align_to(label, table, LV_ALIGN_OUT_BOTTOM_MID, 0, 5); // 为表格增加事件回调函数 lv_obj_add_event_cb(table, table_event_callback, LV_EVENT_FOCUSED, NULL); }
45.LVGL微调框(Spinbox)
- 看示例代码即可。记函数以及代码模板。
46.LVGL线段(Line)
- 看示例代码即可。
47.LVGL日志系统
- 先去配置2个信息:宏定义为1,选择信息。

- 模拟器项目默认是以窗口任务的模式启动的,会自动屏蔽控制台输出的日志信息,如果想要在控制台终端看到LVGL中打印的日志,可以在模拟器项目的
CMakeLists.txt文件中增加如下配置项:set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--subsystem,console")

48.LVGL滚动选择器(Roller)
-
#include "my_ui.h" #include "lvgl.h" void roller_event_callback(lv_event_t *e) { const lv_event_code_t code = lv_event_get_code(e); if (code == LV_EVENT_VALUE_CHANGED) { char buf[32]; const lv_obj_t *roller = lv_event_get_target(e); lv_roller_get_selected_str(roller, buf, sizeof(buf)); LV_LOG_WARN("Clicked, ID: %d Text: %s\n", lv_roller_get_selected(roller), buf); } } void show_ui() { lv_obj_t *roller = lv_roller_create(lv_scr_act()); lv_obj_set_width(roller, lv_display_get_horizontal_resolution(NULL)); lv_obj_center(roller); // 设置选项 lv_roller_set_options(roller, "Here With You\nYellow\nHello\nSmile\nDiamonds", LV_ROLLER_MODE_NORMAL); // 设置条目文字对齐方式为居中对齐 lv_obj_set_style_text_align(roller, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN); // 设置默认被选中的条目的索引编号,从0开始 // lv_roller_set_selected(roller, 2, LV_ANIM_ON); // 设置默认被选中的条目的文本 lv_roller_set_selected_str(roller, "Yellow", LV_ANIM_OFF); // 设置显示多少个条目(会自动设置对应的高度) lv_roller_set_visible_row_count(roller, 5); lv_obj_add_event_cb(roller, roller_event_callback, LV_EVENT_ALL, NULL); }
49.LVGL圆弧
-
#include "my_ui.h" #include "lvgl.h" static lv_obj_t* label; void arc_event_callback(lv_event_t* e) { const lv_event_code_t code = lv_event_get_code(e); if (code == LV_EVENT_VALUE_CHANGED) { const lv_obj_t* arc = lv_event_get_target(e); // 获取圆弧的值,并且显示在中间的标签上 lv_label_set_text_fmt(label, "%d", lv_arc_get_value(arc)); // 标签的值变化了(尺寸发生变化),对齐方式要再设置一次 lv_obj_align_to(label, arc, LV_ALIGN_CENTER, 0, 0); } } void show_ui() { lv_obj_t* arc = lv_arc_create(lv_scr_act()); lv_obj_set_size(arc, 150, 150); lv_obj_center(arc); label = lv_label_create(lv_scr_act()); lv_label_set_text(label, ""); lv_obj_align_to(label, arc, LV_ALIGN_CENTER, 0, 0); // 设置上半圆弧线 lv_arc_set_bg_angles(arc, 180, 0); // 设置圆弧主体的线条宽度为15 lv_obj_set_style_arc_width(arc, 15, LV_PART_MAIN); // 设置圆弧被选中部分的线条的宽度为15 lv_obj_set_style_arc_width(arc, 15, LV_PART_INDICATOR); // 设置圆弧主体部分颜色为0xe1e1d1 lv_obj_set_style_arc_color(arc, lv_color_hex(0xe1e1d1), LV_PART_MAIN); // 设置圆弧指示器部分颜色为0x1ab2ff lv_obj_set_style_arc_color(arc, lv_color_hex(0x1ab2ff), LV_PART_INDICATOR); // 设置圆弧中间的操作杆的颜色为0xff0000 lv_obj_set_style_bg_color(arc, lv_color_hex(0xff0000), LV_PART_KNOB); // 设置取值范围 // lv_arc_set_range(arc, 0, 100); // 顺时针旋转90°(注意:精度为1,和之前讲解的图片的旋转精度0.1不一样) // lv_arc_set_rotation(arc, 90); // 设置初始值 lv_arc_set_value(arc, 50); // 添加事件响应回调函数 lv_obj_add_event_cb(arc, arc_event_callback, LV_EVENT_VALUE_CHANGED, NULL); }
50.LVGL图表(Chart)
- 点击有反应:一般是cb函数。
- 函数太多了,记框架。
- 查看某个对象相关函数?给AI翻译分析学习。

51.LVGL弹性布局
- 看文档
52.LVGL网格布局
- 看文档
53.LVGL自定义字体
-
#include "my_ui.h" #include "lvgl.h" LV_FONT_DECLARE(Ziti_16);//宏定义 void show_ui() { lv_obj_t * button = lv_button_create(lv_scr_act()); lv_obj_set_size(button,100,30); lv_obj_center(button); lv_obj_t * label = lv_label_create(button); lv_label_set_text(button,"高勇生"); lv_obj_set_style_text_font(label,&Ziti_16,0); lv_obj_center(label); }
54:LVGL自定义图标(Symbol)
- 图标放Range里面

- 查看16进制编码:


- 生成的.c文件,如果是单片机就放在src文件夹下面,模拟器就放在跟目录下面
- 找到名字索引:

- 生成UTF8的编码:

- 一定要把.c文件名放在CmakeList里面声明。
-
#include "my_ui.h" #include "lvgl.h" LV_FONT_DECLARE(Ziti_16);//宏定义 LV_FONT_DECLARE(iconfont_40); #define LV_SYMBOL_emotion_40 "\xEE\xA4\x9B" void show_ui() { lv_obj_t * button = lv_button_create(lv_scr_act()); lv_obj_set_size(button,100,100); lv_obj_center(button); lv_obj_t * label = lv_label_create(button); lv_label_set_text(label,LV_SYMBOL_emotion_40""); lv_obj_set_style_text_font(label,&iconfont_40,0); lv_obj_center(label); }
55.LVGL定时器
- 完成实验:本节课实验目标,点击屏幕上按钮,ESP32S3扫描周边WiFi,并且将扫描结果显示在列表中。先把界面做出来。
- 做列表:在触摸屏的模板工程里面

- 一般在show_ui和回调函数都能用到的参数,我们设置为全局静态变量。
- 这个代码值得手敲一遍。代码讲解很好。
56.LVGL动画(Anim)
57.ESP32 SPIFFS文件系统
- SPIFFS作用:将电脑文件上传至单片机系统
- 其余的看文档即可
- 用这个系统,首先一定是在代码初始化,SPIFFS.begin(ture)
58.ESP32分区表
- esp32-s3一共16MB的内存,但是分区了,每个区都有各自的作用

- 先在根目录下面创建一个.csv的分区表,然后在platformIO里面配置

59.LVGL文件系统显示图片(模拟器篇)
- 对比之前的41节课的操作相对于简单一点

- 这里的第二个参数之前用的是数组。现在直接用图片路径


60.LVGL文件系统显示图片
- 上传到单片机,而不是模拟系统。
- 需要先设置分区表。
- 启用内置图片解码器(打开宏)
- 配置内存池
- 移植文件系统
- 图片组件中显示图片
61.LVGL显示gif动画
- 通过SPIFFS上传一段动画到单片机里面
62.LVGL显示二维码
- 打开宏才能用QR的功能
- 代码操作
#include "my_ui.h" #include "lvgl.h" #include "lv_spiffs.h" void show_ui() { auto qrcode = lv_qrcode_create(lv_scr_act()); lv_obj_center(qrcode); const char*url="www.baidu.com"; lv_qrcode_update(qrcode,url,lv_strlen(url)); }对于二维码不能使用lv_obj_set_size去设置大小,用专属lv_qrcode_set_size
63.LVGL菜单使用(Menu)
- 代码重复多,可以手敲看看。
64.LVGL线程安全
- NULL:C语言写法
- nullptr:C++写法
- 互斥锁里面的函数体越小越好。(用锁来解决任务,并发情况)
- 递归锁:已将拿到锁的任务里面调用其他任务里面的函数,那个任务也会自然拿到锁
- 方法:在主函数体里面创建锁和任务xtaskcreate,分别在任务拿锁和释放锁。
- 第二种方法:用LVGL的定时器。
- 方法:改任务的参数为lv_timer_t* timer
- 然后再主函数里面设置:lv_timer_create(add,25,nullptr);
65.LVGL屏幕切换
- lv_obj_t*screen1=lv_obj_create(NULL);创建完屏幕还得加载。
- lv_screen_load(screen)
- 屏幕本身也是一个对象,只要创建对象的时候,执行父对象为NULL即可.lv_obj_create(NULL);
项目复盘
1.字体图标准备
- 第一步打开LVGL字体生成工具Font Converter — LVGL,去商用免费的思源字体的免费下载和在线预览-字体天下下载商用免费的ZIP压缩包。
- 还需要字的txt文件。7000字,里面内容直接粘贴到Symbols
- 图标怎么找?iconfont网站。选中图标放在购物车里面进行下载。打开里面的html文件。

- 如何在代码里面使用?LV_FONT_DECLAER(找字体名称)声明阿里巴巴中文字体。
- 符号的Unicode编码需要转换为UTF编码Unicode编码转换,UTF编码转换(UTF-8、UTF-16、UTF-32)


- 注意此刻的宏定义用法。
- 示例文件中的gui.c和gui.h
- 使用示例代码,需要更改的设置。

- 我的SDL2路径:D:\OneDrive\Desktop\mingw64\x86_64-w64-mingw32\lib\cmake\SDL2


- 还是有错误的,错误码:


- 复制SDL2.dll到工程bin下面

2.整体UI架构
- 分了很多.c和.h文件
3.顶部状态栏UI设计
- 看一下gui.c和.h是干嘛的。
- 每个页面都不变,是公共部分。
4.首页UI设计
5.首页顶部控制栏UI设计
6.菜单首页UI设计
7.网络设置UI设计
8.智能体设置UI设计
9.恢复出厂设置UI设计
自己手敲代码
1.screen_settings_about
- .C文件的代码逻辑:
- 第 1 行:创建菜单页(拿到 lv_ui.menu_page_about)
第 2 行:设置这个菜单页的 flex 对齐方式
第 3 行:创建 label1
第 4 行:创建 label2
第 5 行:设置两个 label 的文字
第 6 行:设置两个 label 的垂直外边距 

-
#include "screen_settings_about.h" #include "gui.h" #include "lvgl.h" void setup_settings_about() { //创建菜单页 lv_ui.menu_page_about=lv_menu_page_create(lv_ui.menu,"关于我们"); //设置flex对齐 lv_obj_set_flex_align(lv_ui.menu_page_about,LV_FLEX_ALIGN_START,LV_FLEX_ALIGN_END,LV_FLEX_ALIGN_CENTER); //创建2个label lv_ui.menu_page_about_label1 = lv_label_create(lv_ui.menu_page_about); lv_ui.menu_page_about_label2 = lv_label_create(lv_ui.menu_page_about); //设置label文字内容 lv_label_set_text(lv_ui.menu_page_about_label1,"Arduino 大模型聊天机器人V1.0"); lv_label_set_text(lv_ui.menu_page_about_label2,"Gaoss"); //设置垂直外边距(上下各留空白) lv_obj_set_style_margin_ver(lv_ui.menu_page_about_label1,10,0); lv_obj_set_style_margin_ver(lv_ui.menu_page_about_label2,10,0); }
2.screen_settings_reset

- .C文件中的2个回调函数:

#include "screen_settings_reset.h"
#include "gui.h"
#include "lvgl.h"
void reset_button_click_event_callback(lv_event_t *event) {
if (lv_event_get_code(event)==LV_EVENT_CLICKED) {
//移除隐藏标志
lv_obj_remove_flag(lv_ui.menu_page_reset_msgbox,LV_OBJ_FLAG_HIDDEN);
}
}
void cancel_button_click_event_callback(lv_event_t *event) {
if (lv_event_get_code(event)==LV_EVENT_CLICKED) {
//添加隐藏标志
lv_obj_add_flag(lv_ui.menu_page_reset_msgbox, LV_OBJ_FLAG_HIDDEN);
}
}
void setup_settings_reset() {
//创建菜单页
lv_ui.menu_page_reset=lv_menu_page_create(lv_ui.menu,"重置");
//设置布局为NONE(让子元素可以自由定位)
lv_obj_set_layout(lv_ui.menu_page_reset,LV_LAYOUT_NONE);
//创建按钮
lv_ui.menu_page_reset_button = lv_button_create(lv_ui.menu_page_reset);
//设置按钮大小(宽度60%,高度30像素)
lv_obj_set_size(lv_ui.menu_page_reset_button,lv_pct(60),30);
//让按钮居中
lv_obj_center(lv_ui.menu_page_reset_button);
//设置按钮颜色为红色
lv_obj_set_style_bg_color(lv_ui.menu_page_reset_button,lv_color_hex(0xFF0000),0);
//创建按钮里的label
lv_ui.menu_page_reset_button_label=lv_label_create(lv_ui.menu_page_reset_button);
//设置按钮里label的文字
lv_label_set_text(lv_ui.menu_page_reset_button_label,"恢复出厂设置");
//让按钮里的label居中
lv_obj_center(lv_ui.menu_page_reset_button_label);
//创建弹窗
lv_ui.menu_page_reset_msgbox=lv_msgbox_create(lv_ui.menu_page_reset);
//设置弹窗内边距
lv_obj_set_style_pad_all(lv_ui.menu_page_reset_msgbox,5,0);
//设置弹窗大小(宽度90%,高度自适应)
lv_obj_set_size(lv_ui.menu_page_reset_msgbox,lv_pct(90),LV_SIZE_CONTENT);
//给弹窗添加标题
lv_msgbox_add_title(lv_ui.menu_page_reset_msgbox,"确定恢复出厂设置吗?");
//设置弹窗文字居中
lv_obj_set_style_text_align(lv_ui.menu_page_reset_msgbox,LV_TEXT_ALIGN_CENTER,0);
//给弹窗添加"确认"按钮
lv_ui.menu_page_reset_msgbox_confirm_button = lv_msgbox_add_footer_button(lv_ui.menu_page_reset_msgbox,"重置");
//设置确认按钮颜色为红色
lv_obj_set_style_bg_color(lv_ui.menu_page_reset_msgbox_confirm_button,lv_color_hex(0xFF0000),0);
//给弹窗添加"取消"按钮
lv_ui.menu_page_reset_msgbox_cancel_button = lv_msgbox_add_footer_button(lv_ui.menu_page_reset_msgbox,"取消");
//让弹窗居中
lv_obj_center(lv_ui.menu_page_reset_msgbox);
//默认隐藏弹窗
lv_obj_add_flag(lv_ui.menu_page_reset_msgbox,LV_OBJ_FLAG_HIDDEN);
//给红色大按钮绑定点击事件(点一下显示弹窗)
lv_obj_add_event_cb(lv_ui.menu_page_reset_button,reset_button_click_event_callback,LV_EVENT_CLICKED,NULL);
//给取消按钮绑定点击事件(点一下隐藏弹窗)
lv_obj_add_event_cb(lv_ui.menu_page_reset_msgbox_cancel_button,cancel_button_click_event_callback,LV_EVENT_CLICKED,NULL);
}
3.screen_settings_llm
- 菜单页
├── 容器1:音色行
│ ├── label "音色"(左)
│ └── dropdown 下拉框(右)
├── 容器2:语速行
│ ├── label "语速"(左)
│ └── slider 滑块(右)
└── 容器3:性格行
├── label "性格"(左)
└── dropdown 下拉框(右) 


- 主要是滑块和下拉框的设置。
-
#include "screen_settings_llm.h" #include "gui.h" #include "lvgl.h" void setup_settings_llm() { //创建菜单页 lv_ui.menu_llm_page=lv_menu_page_create(lv_ui.menu,"智能体设置"); //音色设置 lv_ui.menu_llm_page_cont1 = lv_menu_cont_create(lv_ui.menu_llm_page); lv_obj_set_style_pad_all(lv_ui.menu_llm_page_cont1,10,0); lv_ui.menu_llm_page_voice_label = lv_label_create(lv_ui.menu_llm_page_cont1); lv_label_set_text(lv_ui.menu_llm_page_voice_label,LV_CUSTOM_SYMBOL_VOICE" 音色"); lv_ui.menu_llm_page_voice_dropdown = lv_dropdown_create(lv_ui.menu_llm_page_cont1); lv_obj_set_style_pad_hor(lv_ui.menu_llm_page_voice_dropdown,10,0); lv_obj_set_flex_grow(lv_ui.menu_llm_page_voice_dropdown,1); lv_dropdown_set_options(lv_ui.menu_llm_page_voice_dropdown,"湾湾小何\n御姐\n武则天"); lv_dropdown_set_symbol(lv_ui.menu_llm_page_voice_dropdown,LV_CUSTOM_SYMBOL_DOWN); //语速设置(滑块) lv_ui.menu_llm_page_cont2 = lv_menu_cont_create(lv_ui.menu_llm_page); lv_obj_set_style_pad_all(lv_ui.menu_llm_page_cont2,10,0); lv_ui.menu_llm_page_speed_label = lv_label_create(lv_ui.menu_llm_page_cont2); lv_label_set_text(lv_ui.menu_llm_page_speed_label,LV_CUSTOM_SYMBOL_SPEED" 语速"); lv_ui.menu_llm_page_speed_slider=lv_slider_create(lv_ui.menu_llm_page_cont2); lv_slider_set_range(lv_ui.menu_llm_page_speed_slider,8,20); lv_obj_set_height(lv_ui.menu_llm_page_speed_slider,5); lv_obj_set_style_pad_hor(lv_ui.menu_llm_page_speed_slider,10,0); lv_obj_set_flex_grow(lv_ui.menu_llm_page_speed_slider,1); //性格设置 lv_ui.menu_llm_page_cont3 = lv_menu_cont_create(lv_ui.menu_llm_page); lv_obj_set_style_pad_all(lv_ui.menu_llm_page_cont3,10,0); lv_ui.menu_llm_page_persona_label = lv_label_create(lv_ui.menu_llm_page_cont3); lv_label_set_text(lv_ui.menu_llm_page_persona_label,LV_CUSTOM_SYMBOL_PERSONA" 性格"); lv_ui.menu_llm_page_persona_dropdown = lv_dropdown_create(lv_ui.menu_llm_page_cont3); lv_obj_set_style_pad_hor(lv_ui.menu_llm_page_persona_dropdown,10,0); lv_obj_set_flex_grow(lv_ui.menu_llm_page_persona_dropdown,1); lv_dropdown_set_options(lv_ui.menu_llm_page_persona_dropdown,"温柔\n泼辣"); lv_dropdown_set_symbol(lv_ui.menu_llm_page_persona_dropdown,LV_CUSTOM_SYMBOL_DOWN); }
4.screen_settings_network
-
lv_ui.menu_network_page(菜单页)
├── lv_ui.menu_network_page_cont1(顶部一行)
│ ├── label "连接附近的WLAN"
│ └── button "刷新"
└── lv_ui.menu_network_page_cont2(下方占满剩余空间)
├── wifi_list(WiFi列表)
└── loading_image(加载动画,默认隐藏)lv_ui.menu_network_page_keyboard_mask(遮罩层,隐藏)
├── lv_ui.menu_network_page_password_dialog(密码对话框)
│ ├── label(提示文字)
│ ├── textarea(密码输入框)
│ └── button_cont
│ ├── confirm_button(确定)
│ └── cancel_button(取消)
└── lv_ui.menu_network_page_keyboard(键盘) -
cont是容器的意思。


-
这里的加载和wifi列表是在同一行和列,并且加载动画是覆盖的wifi列表的
-

-
一般flag标志默认是隐藏的
-

-
为什么需要遮罩层?
-
键盘弹出来的时候,用户不应该能点到底下的 WiFi 列表,遮罩层就像一块完全透明的玻璃板无法点击玻璃里面的东西,只能看见
-
lv_obj_set_flex_flow(父对象,排列方式) = 设置子元素的排列方向(横着排还是竖着排)
-

-
举例:

-
├── 菜单页 (menu_network_page)
├── 容器1 (cont1):提示文字 + 刷新按钮
├── 容器2 (cont2):WiFi 列表 + 加载动画(叠加布局)
├── 遮罩层 (keyboard_mask):铺满屏幕、隐藏、底部对齐
│ ├── 密码对话框 (password_dialog):白色弹窗
│ │ ├── 提示文字
│ │ ├── 密码输入框 (textarea)
│ │ └── 按钮容器:确定 + 取消
│ └── 键盘 (keyboard):绑定到 textarea
#include "screen_settings_network.h"
#include "lvgl.h"
#include "gui.h"
void setup_network_setting() {
//创建菜单页
lv_ui.menu_network_page = lv_menu_page_create(lv_ui.menu,"网络设置");
//创建容器1(顶部:提示文字+刷新按钮)
lv_ui.menu_network_page_cont1 = lv_menu_cont_create(lv_ui.menu_network_page);
//创建容器2(下方:wifi列表+加载动画)
lv_ui.menu_network_page_cont2 = lv_menu_cont_create(lv_ui.menu_network_page);
//给容器2设置布局为NONE
lv_obj_set_layout(lv_ui.menu_network_page_cont2,LV_LAYOUT_NONE);
//让容器2占满剩余空间
lv_obj_set_flex_grow(lv_ui.menu_network_page_cont2,1);
//创建提示文字
lv_ui.menu_network_page_tips_label = lv_label_create(lv_ui.menu_network_page_cont1);
lv_label_set_text(lv_ui.menu_network_page_tips_label,"连接附近的WLAN");
//创建刷新按钮
lv_ui.menu_network_page_refresh_button = lv_button_create(lv_ui.menu_network_page_cont1);
lv_obj_set_style_margin_left(lv_ui.menu_network_page_refresh_button,10,0);
lv_obj_set_style_pad_all(lv_ui.menu_network_page_refresh_button,5,0);
//创建刷新按钮里的label
lv_ui.menu_network_page_refresh_button_label = lv_label_create(lv_ui.menu_network_page_refresh_button);
lv_label_set_text(lv_ui.menu_network_page_refresh_button_label,LV_CUSTOM_SYMBOL_RESET" 刷新");
//让按钮里的label居中
lv_obj_center(lv_ui.menu_network_page_refresh_button_label);
//创建WiFi列表
lv_ui.menu_network_page_wifi_list = lv_list_create(lv_ui.menu_network_page_cont2);
lv_obj_set_size(lv_ui.menu_network_page_wifi_list,lv_pct(100),lv_pct(100));
//创建加载动画
lv_ui.menu_network_page_wifi_list_loading_image = lv_image_create(lv_ui.menu_network_page_cont2);
lv_obj_set_size(lv_ui.menu_network_page_wifi_list_loading_image,50,50);
lv_image_set_src(lv_ui.menu_network_page_wifi_list_loading_image,LV_CUSTOM_SYMBOL_LOADING);
lv_obj_set_style_transform_pivot_x(lv_ui.menu_network_page_wifi_list_loading_image,25,0);
lv_obj_set_style_transform_pivot_y(lv_ui.menu_network_page_wifi_list_loading_image,25,0);
lv_obj_center(lv_ui.menu_network_page_wifi_list_loading_image);
//默认隐藏加载动画
lv_obj_add_flag(lv_ui.menu_network_page_wifi_list_loading_image,LV_OBJ_FLAG_HIDDEN);
//创建遮罩层(放在顶层)
lv_ui.menu_network_page_keyboard_mask = lv_obj_create(lv_layer_top());
lv_obj_remove_style_all(lv_ui.menu_network_page_keyboard_mask);
//设置遮罩层大小为整个屏幕
lv_obj_set_size(lv_ui.menu_network_page_keyboard_mask,LV_HOR_RES,LV_VER_RES);
//设置遮罩层位置为左上角
lv_obj_set_pos(lv_ui.menu_network_page_keyboard_mask,0,0);
//设置遮罩层背景完全透明
lv_obj_set_style_bg_opa(lv_ui.menu_network_page_keyboard_mask,LV_OPA_0,0);
//设置遮罩层内边距为0
lv_obj_set_style_pad_all(lv_ui.menu_network_page_keyboard_mask,0,0);
//设置遮罩层布局为flex(竖排)
lv_obj_set_layout(lv_ui.menu_network_page_keyboard_mask,LV_LAYOUT_FLEX);
//设置flex流动方向为竖排
lv_obj_set_flex_flow(lv_ui.menu_network_page_keyboard_mask,LV_FLEX_FLOW_COLUMN);
//设置flex对齐方式(底部居中)
lv_obj_set_flex_align(lv_ui.menu_network_page_keyboard_mask,LV_FLEX_ALIGN_END,LV_FLEX_ALIGN_CENTER,LV_FLEX_ALIGN_END);
//设置遮罩层文字字体
lv_obj_set_style_text_font(lv_ui.menu_network_page_keyboard_mask,&AlibabaPuHuiTi_Regular_15,0);
//默认隐藏遮罩层
lv_obj_add_flag(lv_ui.menu_network_page_keyboard_mask,LV_OBJ_FLAG_HIDDEN);
//创建密码对话框(放在遮罩层上)
lv_ui.menu_network_page_password_dialog = lv_obj_create(lv_ui.menu_network_page_keyboard_mask);
//设置对话框大小(宽度95%,高度自适应)
lv_obj_set_size(lv_ui.menu_network_page_password_dialog,lv_pct(95),LV_SIZE_CONTENT);
//设置对话框背景为白色
lv_obj_set_style_bg_color(lv_ui.menu_network_page_password_dialog,lv_color_hex(0xFFFFFF),0);
//设置对话框边框宽度
lv_obj_set_style_border_width(lv_ui.menu_network_page_password_dialog,1,0);
//设置对话框边框颜色
lv_obj_set_style_border_color(lv_ui.menu_network_page_password_dialog,lv_color_hex(0x85adad),0);
//设置对话框垂直内边距
lv_obj_set_style_pad_ver(lv_ui.menu_network_page_password_dialog,10,0);
//设置对话框布局为flex竖排
lv_obj_set_flex_flow(lv_ui.menu_network_page_password_dialog,LV_FLEX_FLOW_COLUMN);
//设置对话框flex对齐方式
lv_obj_set_flex_align(lv_ui.menu_network_page_password_dialog,LV_FLEX_ALIGN_SPACE_AROUND,LV_FLEX_ALIGN_CENTER,LV_FLEX_ALIGN_CENTER);
//创建对话框里的提示文字(label)
lv_ui.menu_network_page_password_tips_label = lv_label_create(lv_ui.menu_network_page_password_dialog);
//创建密码输入框(textarea)
lv_ui.menu_network_page_password_textarea = lv_textarea_create(lv_ui.menu_network_page_password_dialog);
lv_obj_set_size(lv_ui.menu_network_page_password_textarea,lv_pct(95),35);
//设置textarea边框宽度
lv_obj_set_style_border_width(lv_ui.menu_network_page_password_textarea,1,0);
//设置textarea边框颜色
lv_obj_set_style_border_color(lv_ui.menu_network_page_password_textarea,lv_color_hex(0x00ace6),0);
//设置textarea为单行模式
lv_textarea_set_one_line(lv_ui.menu_network_page_password_textarea,true);
//设置textarea占位符文字
lv_textarea_set_placeholder_text(lv_ui.menu_network_page_password_textarea,"请输入WiFi密码");
//移除textarea的滚动条
lv_obj_remove_flag(lv_ui.menu_network_page_password_textarea,LV_OBJ_FLAG_SCROLLABLE);
//创建按钮容器
lv_ui.button_cont = lv_obj_create(lv_ui.menu_network_page_password_dialog);
//清除按钮容器的默认样式
lv_obj_remove_style_all(lv_ui.button_cont);
//设置按钮容器大小
lv_obj_set_size(lv_ui.button_cont,lv_pct(75),30);
//设置按钮容器布局为flex横排
lv_obj_set_layout(lv_ui.button_cont,LV_LAYOUT_FLEX);
//设置按钮容器flex流动方向为横排
lv_obj_set_flex_flow(lv_ui.button_cont,LV_FLEX_FLOW_ROW);
//设置按钮容器flex对齐方式(按钮之间均匀分布)
lv_obj_set_flex_align(lv_ui.button_cont,LV_FLEX_ALIGN_SPACE_AROUND,LV_FLEX_ALIGN_CENTER,LV_FLEX_ALIGN_CENTER);
//创建确定按钮
lv_ui.confirm_button = lv_button_create(lv_ui.button_cont);
//创建取消按钮
lv_ui.cancel_button = lv_button_create(lv_ui.button_cont);
//设置按钮大小
lv_obj_set_size(lv_ui.confirm_button,60,30);
lv_obj_set_size(lv_ui.cancel_button,60,30);
//设置取消按钮背景为红色
lv_obj_set_style_bg_color(lv_ui.cancel_button,lv_color_hex(0xFF0000),0);
//创建确定按钮里的label
lv_ui.confirm_button_label = lv_label_create(lv_ui.confirm_button);
//创建取消按钮里的label
lv_ui.cancel_button_label = lv_label_create(lv_ui.cancel_button);
//设置按钮文字
lv_label_set_text(lv_ui.confirm_button_label,"确定");
lv_label_set_text(lv_ui.cancel_button_label,"取消");
//让按钮里的label居中
lv_obj_center(lv_ui.confirm_button_label);
lv_obj_center(lv_ui.cancel_button_label);
//创建键盘(放在遮罩层上)
lv_ui.menu_network_page_keyboard = lv_keyboard_create(lv_ui.menu_network_page_keyboard_mask);
//把键盘对齐到屏幕底部中间
lv_obj_align(lv_ui.menu_network_page_keyboard,LV_ALIGN_BOTTOM_MID,0,0);
//让键盘绑定到密码输入框
lv_keyboard_set_textarea(lv_ui.menu_network_page_keyboard,lv_ui.menu_network_page_password_textarea);
}
5.gui.h 具体做什么?
// gui.h 的内容
// 1. 定义所有屏幕都要用的图标(全局共享)
#define LV_CUSTOM_SYMBOL_WIFI "\xEE\xA3\x81"
#define LV_CUSTOM_SYMBOL_VOLUME "\xEE\x98\xA6"
...
// 2. 定义全局尺寸常量
#define STATUS_BAR_HEIGHT 25 // 顶部状态栏高度
#define SPEAK_BUTTON_HEIGHT 35 // 底部按钮高度
// 3. 定义全局结构体(所有 UI 对象的"仓库")
typedef struct {
lv_obj_t *status_bar; // 状态栏
lv_obj_t *screen_home; // 首页
lv_obj_t *screen_settings; // 设置页
lv_obj_t *control_bar; // 控制栏
lv_obj_t *menu_network_page; // 网络设置页
... // 所有页面的组件都存在这里
} lv_screens_info;
// 4. 声明入口函数
void setup_ui();
6. gui.c 具体做什么?
// gui.c 的内容
void setup_ui() {
// 1. 创建状态栏(所有页面都显示在最上面)
lv_ui.status_bar = lv_obj_create(lv_layer_top());
lv_obj_set_size(lv_ui.status_bar, LV_HOR_RES, STATUS_BAR_HEIGHT);
// ... 状态栏的样式、布局、时间标签、wifi图标
// 2. 调用首页初始化(首页登场!)
setup_screen_home();
// 3. 调用设置页初始化(但先不显示)
setup_screen_settings();
}
7.screen_home.h 具体做什么?
// screen_home.h 的内容
// 只声明一个函数,告诉别人"我可以初始化首页"
void setup_screen_home();
8.screen_home.c 具体做什么?
// screen_home.c 的内容
// 1. 创建首页屏幕
lv_ui.screen_home = lv_obj_create(NULL);
// 2. 创建消息列表
lv_ui.screen_home_message_list = lv_list_create(lv_ui.screen_home);
// 3. 创建底部说话按钮
lv_ui.screen_home_speak_button = lv_button_create(lv_ui.screen_home);
// 4. 初始化顶部控制栏(音量、背光、设置按钮)
setup_control_bar();
// 5. 绑定各种事件
lv_obj_add_event_cb(lv_ui.status_bar, status_bar_event_callback, ...);
lv_obj_add_event_cb(lv_ui.control_bar_more_settings_button, ...);
// 6. 加载显示首页
lv_screen_load(lv_ui.screen_home);
项目总结复盘:
项目整体架构:
gui.c → 入口:创建状态栏 + 调用子模块初始化
├── gui.h → 全局结构体定义 + 图标宏定义 + 尺寸宏定义
│
├── screen_home.c → 首页(消息列表 + 底部按钮 + 顶部控制栏)
│ ├── 控制栏:音量滑块 + 背光滑块 + 设置按钮
│ └── 动画:下拉/收起动画
│
├── screen_settings.c → 设置页入口(Menu主框架)
│ ├── 网络设置页面(screen_settings_network.c)
│ ├── 智能体设置页面(screen_settings_llm.c)
│ ├── 重置页面(screen_settings_reset.c)
│ └── 关于页面(screen_settings_about.c)
- LVGL代码怎么和硬件联系起来的?

- 整体逻辑总结:硬件只跟两个驱动打交道(TFT_eSPI 用 SPI 推 ST7789,FT6336 用 I2C 读坐标),LVGL 也只跟这两个驱动打交道(lv_tft_espi_create + lv_indev_set_read_cb);中间用 lvglUpdateLock 互斥锁解决多任务并发,再用 lv_ui 这个全局结构体 + LvglEventManager 把 LVGL 的事件/对象暴露给业务代码,从而把"画 UI"和"做业务"解耦。
模拟器代码移植:
- 将整个ui文件夹里面的.C以及.h文件copy到单片机项目(已经写好硬件代码的小智文件夹)下的src文件夹下面
gui_guider可视化软件使用:
1.gui_guider安装
- 通过可视化的方式直接布局界面,生成LVGL代码。
- gui_guider安装

- 注册登录然后安装即可。
2.gui_guider设计初体验
- 创建一个项目。

- 不使用上述版本,使用另一个版本。上述版本2.0,我们使用1.0


- 上传一张图片:

- 上传字体的文件不能有中文和中划线,直接重命名即可

3.gui_guider项目移植
- 把刚刚在Gui guider的项目移植到单片机上面来
- 生成C代码,然后打开工程目录,复制整个generate目录以及custom目录。

- 打开gui_guider.h文件找到,去main.cpp定义这个文件。

- 三部曲:包含头文件,定义全局变量lv_ui guider_ui,在main.cpp的setup中写入setup_ui(&guider_ui);
- 现在还需要把点击按钮实现的逻辑实现,在events_init.h

- 更换图标:

- 图标要添加到文字旁边,这两个对应的字体需要保持一致所以我们还需要在LVGL字体生成文件工具里面去统一字体,统一字体的名称需要和我们工程文件下字体的名称保持一致


- 地址码是四位数前面加0x表示16进制
- 需要上传字体文件的ttf以及图标文件的ttf


- 然后我们点击submit,得到一个.c文件

- 删除我们代码中的
替换为刚刚得到的点C文件。 
- 现在我们需要的图标创建好了,我们更换图标即可,首先去定义我们的图标宏格式,在我们主要写代码的文件下去定义

- 然后这个宏定义的后面需要16进制,我们需要Unicode编码去转换为UTF8编码,Unicode编码需要八位才能进行转换


- 更改代码即可。

- 整体思路:先将gui_guider工程部署到单片机上(基于触摸示例代码),然后添加按钮cb代码,更换图标更换代码
更多推荐










所有评论(0)