1.开发环境介绍

  • 我使用的是CLion开发软件里面搭配platformio插件

2.开发板资源介绍

  • 一块esp32-s3电容触摸屏语音开发板
  • 一根USB数据线
  • 一个4W喇叭

3.产品设计概览

4.配置个人大模型密钥

5.PlatformIO编译太慢解决办法

  • 创建环境变量,然后查看自己的CPU的逻辑处理器是多少个?

6.ESP32-S3启用PSRAM

  • PSRAM什么?是一种拓展内存的部件
  • 默认PSRAM是关闭的,我们需要配置platformio.ini

7.音频相关术语介绍

  • PCM是什么?是一种脉冲编码调制,将连续的数字信号,转化为离散的模拟信号
  • 自然界中的声音通过麦克风等采集设备处理后,首先会将声音信号转换成电信号,但此时的电信号是连续的模拟信号,如果需要在单片机上面去获取的话必须转化成数字信号
  • PCM编码过程:采样,量化,编码

8.使用I2S协议播放音频

  • I2S是什么?I2S(Inter-IC Sound)是由飞利浦公司开发的一种数字音频传输协议,主要用于连接音频编解码器、数字麦克风和扬声器等音频设备。
  • I2S 协议通过差分信号传输数字音频数据,主要包含以下几种关键信号:
  1. 时钟信号:位时钟(BCLK/SCK):用于同步数据传输的时钟信号。帧时钟(LRCK/WS):用于区分左右声道数据,频率等于音频采样率。当 LRCK 为高电平时表示传输左声道数据,为低电平时表示传输右声道数据。
  2. 数据信号:串行数据输出(SDO/Dout):从主设备发送到从设备的音频数据。                                        串行数据输入(SDI/Din):从从设备接收至主设备的音频数据。
  • ESP32 集成了两个 I2S 控制器(I2S0 和 I2S1),具有以下特性:
  1. 全双工数据传输
  • 实例代码:先把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自定义字体

  • 字体网站下载商用免费。然后打开LVGL官网去配置使用。
  • 3500汉字复制到symbol框里面。
  • .c文件复制根目录,然后修改cmake文件
  • 用宏定义引入:
  • #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文件系统显示图片

  • 上传到单片机,而不是模拟系统。
  • 需要先设置分区表。
  1. 启用内置图片解码器(打开宏)
  2. 配置内存池
  3. 移植文件系统
  4. 图片组件中显示图片

61.LVGL显示gif动画

  •  通过SPIFFS上传一段动画到单片机里面

62.LVGL显示二维码

  1. 打开宏才能用QR的功能
  2. 代码操作
    #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.字体图标准备

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代码,更换图标更换代码
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐