当前位置: 首页 > news >正文

ESP32-C3 入门09:基于 ESP-IDF + LVGL + ST7789 的 1.54寸 WiFi 时钟(SquareLine Studio 移植)

在这里插入图片描述


一.
在这里插入图片描述


https://github.com/nopnop2002/esp-idf-st7789


1. 前言

在这里插入图片描述

2. 开发环境准备

2.1 硬件清单

  • ESP32-C3 开发板
  • ST7789 1.54 寸 LCD
  • 其他辅助元件(杜邦线、电源)

2.2 软件安装

  • ESP-IDF 环境安装(Windows+VScode)
  • VSCode 插件配置
  • LVGL 官方SquareLine Studio 1.5.3 下载注册

软件安装,环境配置过程中肯定会遇到很多问题,我就遇到了以下几个问题,解决办法参考如下:

  • 【小技巧】ESP-IDF安装卡在“Running command: submodule foreach --recursive git config --local core.fileMode“
  • 【小技巧】解‘d:\ESP_IDF_541\ins\Espressif\tools\idf-python\3.11.2\python.exe -m pip“ is not valid. (ERROR_
  • 【小技巧】ESP-IDF“Failed to set target esp32c3: non zero exit code 1:Requirement ‘setuptools<71.0.1,>=21’

2.3 驱动与依赖

  • st7789 驱动选择
  • lvgl 版本说明
  • 需要修改的 CMakeLists.txt

3. 参考例程测试

3.1 ESP-IDF 《tjpgd》示例程序运行

在这里插入图片描述

  • 参数修改

芯片配置那些就不说了,针对代码方面的修改,例如引脚,背光电平逻辑,屏幕分辨率修改成你的硬件对应的参数。
改动分辨率的时候一定要检查是不是全部都改过来了,我就遇到花屏的现象,后来发现是#include "jpeg_decoder.h"中的屏幕分辨率没有改。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

/*** @file app_main.c* @brief ESP32 ST7789 LCD 显示 JPEG 图片示例(精简版)** 功能:* - 初始化 SPI 总线* - 初始化 LCD 面板* - 解码嵌入式 JPEG 图片* - 直接显示整张图片(无特效、无动画)** 注意事项:* - 图片必须是 RGB565 格式* - 若显示镜像或方向错误,可调整 swap_xy 或 mirror 参数*/#include <stdio.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_vendor.h"
#include "esp_lcd_panel_ops.h"
#include "esp_heap_caps.h"
#include "driver/spi_master.h"
#include "driver/gpio.h"
#include "decode_image.h"// --------------------------- LCD 配置 --------------------------------
#define LCD_HOST       SPI2_HOST                     /**< 使用 SPI2 总线 */
#define EXAMPLE_LCD_PIXEL_CLOCK_HZ (20 * 1000 * 1000) /**< LCD 像素时钟频率 (Hz) */
#define EXAMPLE_LCD_BK_LIGHT_ON_LEVEL 1            /**< 背光打开电平 */
#define EXAMPLE_LCD_BK_LIGHT_OFF_LEVEL 0           /**< 背光关闭电平 */#define EXAMPLE_PIN_NUM_DATA0  7                   /**< 数据线 0 / MOSI */
#define EXAMPLE_PIN_NUM_PCLK   6                   /**< 像素时钟 */
#define EXAMPLE_PIN_NUM_CS     10                  /**< 片选 */
#define EXAMPLE_PIN_NUM_DC     3                   /**< 数据/命令选择 */
#define EXAMPLE_PIN_NUM_RST    4                   /**< 复位引脚 */
#define EXAMPLE_PIN_NUM_BK_LIGHT 5                 /**< 背光控制 */#define EXAMPLE_LCD_H_RES 240                       /**< 水平分辨率(像素) */
#define EXAMPLE_LCD_V_RES 240                       /**< 垂直分辨率(像素) */
#define EXAMPLE_LCD_CMD_BITS 8                      /**< LCD 命令位数 */
#define EXAMPLE_LCD_PARAM_BITS 8                    /**< LCD 参数位数 */// --------------------------- 主函数 ----------------------------------
/*** @brief 主应用程序入口** 初始化 LCD 并显示嵌入式 JPEG 图片*/
void app_main(void)
{// -------------------- 背光 GPIO 初始化 --------------------gpio_config_t bk_gpio_config = {.mode = GPIO_MODE_OUTPUT,                      /**< 输出模式 */.pin_bit_mask = 1ULL << EXAMPLE_PIN_NUM_BK_LIGHT /**< 选择背光引脚 */};ESP_ERROR_CHECK(gpio_config(&bk_gpio_config));ESP_ERROR_CHECK(gpio_set_level(EXAMPLE_PIN_NUM_BK_LIGHT, EXAMPLE_LCD_BK_LIGHT_ON_LEVEL)); /**< 打开背光 */// -------------------- SPI 总线初始化 --------------------spi_bus_config_t buscfg = {.sclk_io_num = EXAMPLE_PIN_NUM_PCLK,          /**< SPI 时钟 */.mosi_io_num = EXAMPLE_PIN_NUM_DATA0,        /**< SPI MOSI 数据 */.miso_io_num = -1,                            /**< 未使用 MISO */.quadwp_io_num = -1,                          /**< 未使用 QUAD WP */.quadhd_io_num = -1,                          /**< 未使用 QUAD HD */.max_transfer_sz = EXAMPLE_LCD_H_RES * EXAMPLE_LCD_V_RES * 2 + 8 /**< 最大传输大小,RGB565 */};ESP_ERROR_CHECK(spi_bus_initialize(LCD_HOST, &buscfg, SPI_DMA_CH_AUTO)); /**< 初始化 SPI 总线 */// -------------------- LCD 面板 IO 初始化 --------------------esp_lcd_panel_io_handle_t io_handle = NULL;    /**< LCD IO 句柄 */esp_lcd_panel_io_spi_config_t io_config = {.dc_gpio_num = EXAMPLE_PIN_NUM_DC,          /**< 数据/命令选择引脚 */.cs_gpio_num = EXAMPLE_PIN_NUM_CS,          /**< 片选引脚 */.pclk_hz = EXAMPLE_LCD_PIXEL_CLOCK_HZ,      /**< 像素时钟频率 */.lcd_cmd_bits = EXAMPLE_LCD_CMD_BITS,       /**< LCD 命令位数 */.lcd_param_bits = EXAMPLE_LCD_PARAM_BITS,   /**< LCD 参数位数 */.spi_mode = 0,                              /**< SPI 模式 */.trans_queue_depth = 10,                     /**< 传输队列深度 */};ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(LCD_HOST, &io_config, &io_handle)); /**< 创建 LCD IO 句柄 */// -------------------- LCD 面板初始化 --------------------esp_lcd_panel_handle_t panel_handle = NULL;    /**< LCD 面板句柄 */esp_lcd_panel_dev_config_t panel_config = {.reset_gpio_num = EXAMPLE_PIN_NUM_RST,      /**< 复位引脚 */.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, /**< RGB 元素顺序 */.bits_per_pixel = 16                         /**< 每像素位数 */};ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(io_handle, &panel_config, &panel_handle)); /**< 创建 LCD 面板句柄 */ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle));             /**< 复位 LCD 面板 */ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle));              /**< 初始化 LCD 面板 */ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle, true)); /**< 打开显示 */ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel_handle, true)); /**< 反转颜色 */ESP_ERROR_CHECK(esp_lcd_panel_swap_xy(panel_handle, false));    /**< 不交换 XY 坐标,防止镜像 */// -------------------- 解码 JPEG 图片 --------------------uint16_t *pixels = NULL;                                           /**< 解码后的 RGB565 像素数组 */ESP_ERROR_CHECK(decode_image(&pixels));                            /**< 解码嵌入式 JPEG 文件 */// -------------------- 显示整张图片 --------------------ESP_ERROR_CHECK(esp_lcd_panel_draw_bitmap(panel_handle,0, 0,EXAMPLE_LCD_H_RES, EXAMPLE_LCD_V_RES,pixels)); /**< 将像素写入 LCD */// -------------------- 主循环 --------------------// 保持程序运行,防止任务退出while (1) {// 如果需要刷新图片,可重复显示(此处每秒刷新一次)ESP_ERROR_CHECK(esp_lcd_panel_draw_bitmap(panel_handle,0, 0,EXAMPLE_LCD_H_RES, EXAMPLE_LCD_V_RES,pixels));vTaskDelay(pdMS_TO_TICKS(1000)); /**< 延时 1 秒 */}
}
  • 效果演示

带旋转,镜像,动态波浪效果的动图
在这里插入图片描述

  • 问题
    WiFi一起运行的时候,崩溃,而且背景+前景显示难实现。

3.2 ESP-IDF《spi_lcd_touch》测试例程

  • 参考《立创实战派-C3》第9章 LCD显示
    在这里插入图片描述

  • 效果
    在这里插入图片描述

3.2 GitHub 《nopnop2002esp-idf-st7789》开源程序测试

  • GitHub esp-idf-st7789

https://github.com/nopnop2002/esp-idf-st7789

在这里插入图片描述
这是修改过后的main.c 文件:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_system.h"
#include "esp_vfs.h"
#include "esp_spiffs.h"#include "st7789.h"
#include "fontx.h"
#include "bmpfile.h"
#include "decode_jpeg.h"
#include "decode_png.h"
#include "pngle.h"#define INTERVAL 400
#define WAIT vTaskDelay(INTERVAL)static const char *TAG = "ST7789";// You have to set these CONFIG value using menuconfig.
#if 1
#define CONFIG_WIDTH  240
#define CONFIG_HEIGHT 240
#define CONFIG_MOSI_GPIO 7
#define CONFIG_SCLK_GPIO 6
#define CONFIG_CS_GPIO 10
#define CONFIG_DC_GPIO 3
#define CONFIG_RESET_GPIO 4
#define CONFIG_BL_GPIO 5
#endif// 追踪并打印当前系统堆内存和任务栈的使用情况
void traceHeap() {// 静态变量 _free_heap_size 用来保存初始时的可用堆大小(只赋值一次)static uint32_t _free_heap_size = 0;// 第一次调用时,记录当前的可用堆大小if (_free_heap_size == 0) _free_heap_size = esp_get_free_heap_size();// 计算自上次记录以来,堆内存减少的大小(负数代表堆内存消耗增加)int _diff_free_heap_size = _free_heap_size - esp_get_free_heap_size();// 打印堆内存的变化值ESP_LOGI(__FUNCTION__, "_diff_free_heap_size=%d", _diff_free_heap_size);// 打印当前的可用堆大小ESP_LOGI(__FUNCTION__, "esp_get_free_heap_size() : %6"PRIu32"\n", esp_get_free_heap_size());#if 0// 打印历史上“最小剩余堆大小”(反映系统堆内存使用的峰值)printf("esp_get_minimum_free_heap_size() : %6"PRIu32"\n", esp_get_minimum_free_heap_size());// FreeRTOS 提供的当前可用堆大小printf("xPortGetFreeHeapSize() : %6zd\n", xPortGetFreeHeapSize());// FreeRTOS 提供的历史最小剩余堆大小(类似上面的函数)printf("xPortGetMinimumEverFreeHeapSize() : %6zd\n", xPortGetMinimumEverFreeHeapSize());// 查询指定内存类型的可用堆大小,这里是 32bit 对齐的堆(常用于 DMA 或特定硬件需求)printf("heap_caps_get_free_size(MALLOC_CAP_32BIT) : %6d\n", heap_caps_get_free_size(MALLOC_CAP_32BIT));// 获取当前任务栈的“水位线”(即曾经使用过的最大深度,数值越小说明栈使用越多)// 返回值表示任务栈剩余的最小值(单位:word),反映任务栈的使用安全性printf("uxTaskGetStackHighWaterMark() : %6d\n", uxTaskGetStackHighWaterMark(NULL));
#endif
}// 功能:读取 BMP 图片文件并显示到 TFT LCD 上,同时统计耗时
TickType_t BMPTest(TFT_t * dev, char * file, int width, int height) {TickType_t startTick, endTick, diffTick;startTick = xTaskGetTickCount();   // 记录起始时间(系统 tick)lcdSetFontDirection(dev, 0);       // 设置字体方向为默认方向lcdFillScreen(dev, BLACK);         // 屏幕填充黑色背景,准备显示图片// 申请 BMP 文件结构体内存bmpfile_t *bmpfile = (bmpfile_t*)malloc(sizeof(bmpfile_t));if (bmpfile == NULL) {ESP_LOGE(__FUNCTION__, "Error allocating memory for bmpfile"); // 内存分配失败return 0;}// 打开指定的 BMP 文件esp_err_t ret;FILE* fp = fopen(file, "rb");if (fp == NULL) {ESP_LOGW(__FUNCTION__, "File not found [%s]", file); // 文件未找到return 0;}// 读取 BMP 文件头前两个字节,必须为 "BM"ret = fread(bmpfile->header.magic, 1, 2, fp); assert(ret == 2);if (bmpfile->header.magic[0]!='B' || bmpfile->header.magic[1] != 'M') {ESP_LOGW(__FUNCTION__, "File is not BMP"); // 文件格式错误free(bmpfile);fclose(fp);return 0;}// 依次读取 BMP 文件头剩余字段ret = fread(&bmpfile->header.filesz, 4, 1 , fp);   assert(ret == 1); // 文件大小ret = fread(&bmpfile->header.creator1, 2, 1, fp); assert(ret == 1);  // 保留字段1ret = fread(&bmpfile->header.creator2, 2, 1, fp); assert(ret == 1);  // 保留字段2ret = fread(&bmpfile->header.offset, 4, 1, fp);   assert(ret == 1);  // 像素数据偏移位置// 读取 BMP DIB 信息头ret = fread(&bmpfile->dib.header_sz, 4, 1, fp);  assert(ret == 1); // DIB 头大小ret = fread(&bmpfile->dib.width, 4, 1, fp);      assert(ret == 1); // 图像宽度ret = fread(&bmpfile->dib.height, 4, 1, fp);     assert(ret == 1); // 图像高度ret = fread(&bmpfile->dib.nplanes, 2, 1, fp);    assert(ret == 1); // 色彩平面数(通常为1)ret = fread(&bmpfile->dib.depth, 2, 1, fp);      assert(ret == 1); // 每像素位数ret = fread(&bmpfile->dib.compress_type, 4, 1, fp); assert(ret == 1); // 压缩方式ret = fread(&bmpfile->dib.bmp_bytesz, 4, 1, fp); assert(ret == 1);   // 图像数据大小ret = fread(&bmpfile->dib.hres, 4, 1, fp);       assert(ret == 1);   // 水平分辨率ret = fread(&bmpfile->dib.vres, 4, 1, fp);       assert(ret == 1);   // 垂直分辨率ret = fread(&bmpfile->dib.ncolors, 4, 1, fp);    assert(ret == 1);   // 调色板颜色数ret = fread(&bmpfile->dib.nimpcolors, 4, 1, fp); assert(ret == 1);   // 重要颜色数// 仅支持 24 位无压缩的 BMP 图片if((bmpfile->dib.depth == 24) && (bmpfile->dib.compress_type == 0)) {// 每行像素数据必须按 4 字节对齐(BMP 格式规定)uint32_t rowSize = (bmpfile->dib.width * 3 + 3) & ~3;int w = bmpfile->dib.width;int h = bmpfile->dib.height;ESP_LOGD(__FUNCTION__,"w=%d h=%d", w, h);// 计算水平居中/裁剪位置int _x, _w, _cols, _cole;if (width >= w) {    // 屏幕宽度大于等于图片宽度 → 居中显示_x = (width - w) / 2;_w = w;_cols = 0;_cole = w - 1;} else {             // 屏幕宽度小于图片宽度 → 居中裁剪_x = 0;_w = width;_cols = (w - width) / 2;_cole = _cols + width - 1;}// 计算垂直居中/裁剪位置int _y, _rows, _rowe;if (height >= h) {   // 屏幕高度大于等于图片高度 → 居中显示_y = (height - h) / 2;_rows = 0;_rowe = h -1;} else {             // 屏幕高度小于图片高度 → 居中裁剪_y = 0;_rows = (h - height) / 2;_rowe = _rows + height - 1;}#define BUFFPIXEL 20uint8_t sdbuffer[3*BUFFPIXEL];   // 临时像素缓冲区(一次读取20个像素)uint16_t *colors = (uint16_t*)malloc(sizeof(uint16_t) * w); // 一行像素转换成 RGB565if (colors == NULL) {ESP_LOGE(__FUNCTION__, "Error allocating memory for color"); // 内存不足free(bmpfile);fclose(fp);return 0;}// 按行读取 BMP 像素并转换为 RGB565for (int row=0; row<h; row++) {if (row < _rows || row > _rowe) continue;  // 跳过裁剪区域// 定位到该行的起始地址(BMP 自底向上存储)int pos = bmpfile->header.offset + (h - 1 - row) * rowSize;fseek(fp, pos, SEEK_SET);int buffidx = sizeof(sdbuffer); // 强制首次加载数据int index = 0;for (int col=0; col<w; col++) {if (buffidx >= sizeof(sdbuffer)) {   // 读取一批像素数据fread(sdbuffer, sizeof(sdbuffer), 1, fp);buffidx = 0;}if (col < _cols || col > _cole) continue; // 跳过裁剪列// 读取 BGR 三通道并转换为 RGB565 格式uint8_t b = sdbuffer[buffidx++];uint8_t g = sdbuffer[buffidx++];uint8_t r = sdbuffer[buffidx++];colors[index++] = rgb565(r, g, b);}// 将整行像素发送到 LCD 显示lcdDrawMultiPixels(dev, _x, _y, _w, colors);_y++; // 屏幕 Y 坐标递增}free(colors); // 释放像素缓冲区}lcdDrawFinish(dev);  // 通知 LCD 绘制完成free(bmpfile);       // 释放 BMP 文件头内存fclose(fp);          // 关闭文件endTick = xTaskGetTickCount();  // 记录结束时间diffTick = endTick - startTick;ESP_LOGI(__FUNCTION__, "elapsed time[ms]:%"PRIu32,diffTick*portTICK_PERIOD_MS); // 打印耗时return diffTick; // 返回耗时(tick)
}void ST7789(void *pvParameters)
{// set font fileFontxFile fx32G[2];FontxFile fx32L[2];InitFontx(fx32G,"/fonts/ILGH32XB.FNT",""); // 16x32Dot GothicInitFontx(fx32L,"/fonts/LATIN32B.FNT",""); // 16x32Dot LatinFontxFile fx32M[2];InitFontx(fx32M,"/fonts/ILMH32XB.FNT",""); // 16x32Dot MincyoTFT_t dev;// Change SPI Clock Frequency//spi_clock_speed(40000000); // 40MHz//spi_clock_speed(60000000); // 60MHzspi_master_init(&dev, CONFIG_MOSI_GPIO, CONFIG_SCLK_GPIO, CONFIG_CS_GPIO, CONFIG_DC_GPIO, CONFIG_RESET_GPIO, CONFIG_BL_GPIO);lcdInit(&dev, CONFIG_WIDTH, CONFIG_HEIGHT, CONFIG_OFFSETX, CONFIG_OFFSETY);char file[32];while(1) {traceHeap();strcpy(file, "/images/image.bmp");BMPTest(&dev, file, CONFIG_WIDTH, CONFIG_HEIGHT);WAIT;// Multi Font Testuint16_t color;uint8_t ascii[40];uint16_t margin = 10;lcdFillScreen(&dev, BLACK);color = WHITE;lcdSetFontDirection(&dev, 0);uint16_t xpos = 0;uint16_t ypos = 15;int xd = 0;int yd = 1;if (CONFIG_WIDTH >= 240) {xpos = xpos - (32 * xd) - (margin * xd);;ypos = ypos + (24 * yd) + (margin * yd);strcpy((char *)ascii, "32Dot Mincyo Font");lcdDrawString(&dev, fx32M, xpos, ypos, ascii, color);}lcdDrawFinish(&dev);lcdSetFontDirection(&dev, 0);WAIT;}}
// ============================= SPIFFS 文件系统工具函数 =============================// 功能:遍历指定路径下的 SPIFFS 文件系统,打印目录内容
// 参数:
//   path - 要遍历的路径,例如 "/fonts"
// 说明:
//   使用 opendir 打开目录,然后通过 readdir 逐个读取文件/目录信息,直到读取结束。
//   每个文件的名称、inode 节点号、类型都会被打印出来。
static void listSPIFFS(char * path) {DIR* dir = opendir(path);           // 打开目录assert(dir != NULL);                // 如果目录不存在,则触发断言(程序终止)while (true) {struct dirent* pe = readdir(dir); // 读取下一个目录项if (!pe) break;                   // 没有更多文件则退出循环ESP_LOGI(__FUNCTION__,"d_name=%s d_ino=%d d_type=%x", // 打印文件名、inode 节点号、文件类型pe->d_name, pe->d_ino, pe->d_type);}closedir(dir);                      // 关闭目录
}// 功能:挂载 SPIFFS 文件系统到指定路径
// 参数:
//   path       - 挂载到的虚拟路径(如 "/fonts")
//   label      - 对应的分区标签(如 "storage1"),需在分区表中配置
//   max_files  - SPIFFS 文件系统同时允许打开的最大文件数
// 返回值:
//   ESP_OK         - 成功挂载
//   其他错误代码   - 挂载失败(可能是找不到分区、文件系统损坏等)
// 说明:
//   使用 esp_vfs_spiffs_register 进行挂载,如果失败会根据错误码打印详细日志。
//   挂载成功后,还会打印该分区的总容量与已使用容量。
esp_err_t mountSPIFFS(char * path, char * label, int max_files) {esp_vfs_spiffs_conf_t conf = {.base_path = path,               // 挂载点路径.partition_label = label,        // 分区标签(需在分区表中声明).max_files = max_files,          // 允许同时打开的最大文件数.format_if_mount_failed = true   // 如果挂载失败则格式化分区};// 使用配置挂载 SPIFFS 文件系统esp_err_t ret = esp_vfs_spiffs_register(&conf);if (ret != ESP_OK) {if (ret == ESP_FAIL) {ESP_LOGE(TAG, "Failed to mount or format filesystem"); // 挂载或格式化失败} else if (ret == ESP_ERR_NOT_FOUND) {ESP_LOGE(TAG, "Failed to find SPIFFS partition");      // 未找到对应分区} else {ESP_LOGE(TAG, "Failed to initialize SPIFFS (%s)", esp_err_to_name(ret));}return ret; // 返回错误码}#if 0// 可选:检查 SPIFFS 文件系统完整性ESP_LOGI(TAG, "Performing SPIFFS_check().");ret = esp_spiffs_check(conf.partition_label);if (ret != ESP_OK) {ESP_LOGE(TAG, "SPIFFS_check() failed (%s)", esp_err_to_name(ret));return ret;} else {ESP_LOGI(TAG, "SPIFFS_check() successful");}
#endif// 打印 SPIFFS 分区信息(总容量 & 已用容量)size_t total = 0, used = 0;ret = esp_spiffs_info(conf.partition_label, &total, &used);if (ret != ESP_OK) {ESP_LOGE(TAG,"Failed to get SPIFFS partition information (%s)", esp_err_to_name(ret));} else {ESP_LOGI(TAG,"Mount %s to %s success", path, label);  // 打印挂载成功信息ESP_LOGI(TAG,"Partition size: total: %d, used: %d", total, used);}return ret;
}// ============================= 主程序入口 =============================// 功能:程序入口函数 app_main
// 说明:
//   1. 依次挂载 /fonts、/images、/icons 三个 SPIFFS 分区
//   2. 遍历并打印分区内容
//   3. 创建 LCD 显示任务 ST7789(分配 4KB 栈空间,优先级为 2)
void app_main(void)
{ESP_LOGI(TAG, "Initializing SPIFFS");// 挂载 /fonts 分区,最大同时打开文件数为 7ESP_ERROR_CHECK(mountSPIFFS("/fonts", "storage1", 7));listSPIFFS("/fonts/");// 挂载 /images 分区,最大同时打开文件数为 1ESP_ERROR_CHECK(mountSPIFFS("/images", "storage2", 1));listSPIFFS("/images/");// 挂载 /icons 分区,最大同时打开文件数为 1ESP_ERROR_CHECK(mountSPIFFS("/icons", "storage3", 1));listSPIFFS("/icons/");// 创建 ST7789 显示任务// 参数://   任务函数:ST7789//   任务名  :"ST7789"//   栈大小  :1024*4 = 4096 字节(4KB)//   任务参数:NULL//   优先级  :2//   任务句柄:NULL(不保存任务句柄)xTaskCreate(ST7789, "ST7789", 1024*4, NULL, 2, NULL);
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_system.h"
#include "esp_vfs.h"
#include "esp_spiffs.h"#include "st7789.h"
#include "fontx.h"
#include "bmpfile.h"
#include "decode_jpeg.h"
#include "decode_png.h"
#include "pngle.h"#include <time.h>
#include <sys/time.h>
#include "esp_wifi.h"
#include "esp_event.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#include "esp_sntp.h"#define INTERVAL 400
#define WAIT vTaskDelay(INTERVAL)static const char *TAG = "APP_st7789_wifi";
static bool has_restarted_today = false; // 标记今天是否已重启
// You have to set these CONFIG value using menuconfig.
#if 0
#define CONFIG_WIDTH 240
#define CONFIG_HEIGHT 240#define CONFIG_MOSI_GPIO 7
#define CONFIG_SCLK_GPIO 6
#define CONFIG_CS_GPIO 10
#define CONFIG_DC_GPIO 3
#define CONFIG_RESET_GPIO 4
#define CONFIG_BL_GPIO 5
#endif// ---------------- Wi-Fi 配置 ----------------
#define WIFI_SSID "TP-LINK-CQJY"
#define WIFI_PASS "cqjy187166"// Wi-Fi 事件处理函数
static void wifi_event_handler(void *arg, esp_event_base_t event_base,int32_t event_id, void *event_data)
{if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START){esp_wifi_connect();}else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED){ESP_LOGI(TAG, "Wi-Fi 断开,尝试重连...");esp_wifi_connect();}else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP){ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;ESP_LOGI(TAG, "获取到IP地址: " IPSTR, IP2STR(&event->ip_info.ip));}
}// ---------------- NTP 时间同步 ----------------
static void initialize_sntp(void)
{ESP_LOGI(TAG, "初始化 SNTP...");esp_sntp_setoperatingmode(SNTP_OPMODE_POLL);esp_sntp_setservername(0, "ntp.aliyun.com"); // 你也可以换成 pool.ntp.orgesp_sntp_init();
}static void obtain_time(void)
{initialize_sntp();// 等待时间同步time_t now = 0;struct tm timeinfo = {0};int retry = 0;const int retry_count = 10;while (timeinfo.tm_year < (2016 - 1900) && ++retry < retry_count){ESP_LOGI(TAG, "等待时间同步... (%d/%d)", retry, retry_count);vTaskDelay(2000 / portTICK_PERIOD_MS);time(&now);localtime_r(&now, &timeinfo);}// 设置时区为 北京时间 (UTC+8)setenv("TZ", "CST-8", 1);tzset();// 获取本地时间time(&now);localtime_r(&now, &timeinfo);ESP_LOGI(TAG, "当前北京时间: %04d-%02d-%02d %02d:%02d:%02d",timeinfo.tm_year + 1900,timeinfo.tm_mon + 1,timeinfo.tm_mday,timeinfo.tm_hour,timeinfo.tm_min,timeinfo.tm_sec);
}/*** @brief 打印当前北京时间,并在 00:00 执行一次软件重启*/
static void print_local_time_and_restart_at_midnight(void)
{time_t now;struct tm timeinfo;time(&now);localtime_r(&now, &timeinfo);int hour = timeinfo.tm_hour;int minute = timeinfo.tm_min;int second = timeinfo.tm_sec;ESP_LOGI(TAG, "北京时间: %04d-%02d-%02d %02d:%02d:%02d",timeinfo.tm_year + 1900,timeinfo.tm_mon + 1,timeinfo.tm_mday,hour,minute,second);// 检查是否为 00:00:00 ~ 00:00:30,并且今天还没有重启过if (hour == 0 && minute == 0 && second <= 30) {if (!has_restarted_today) {ESP_LOGI(TAG, "到达午夜零点,正在重启设备...");has_restarted_today = true;esp_restart();}} else {// 如果不是 00:00:00,则重置标志位(允许明天再次重启)// 注意:更精确的方式是检测日期变化,但简单场景下可接受if (hour > 0) {has_restarted_today = false;}}
}
/*** @brief 初始化 Wi-Fi STA 模式*        - 初始化 NVS 存储*        - 创建默认网络接口*        - 注册 Wi-Fi/IP 事件回调*        - 启动 Wi-Fi 并连接到路由器*/
static void wifi_init_sta(void)
{esp_err_t ret = nvs_flash_init();if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {ESP_ERROR_CHECK(nvs_flash_erase());ret = nvs_flash_init();}ESP_ERROR_CHECK(ret);ESP_ERROR_CHECK(esp_netif_init());ESP_ERROR_CHECK(esp_event_loop_create_default());esp_netif_create_default_wifi_sta();wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();ESP_ERROR_CHECK(esp_wifi_init(&cfg));ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,ESP_EVENT_ANY_ID,&wifi_event_handler,NULL,NULL));ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,IP_EVENT_STA_GOT_IP,&wifi_event_handler,NULL,NULL));wifi_config_t wifi_config = {.sta = {.ssid = WIFI_SSID,.password = WIFI_PASS,.threshold.authmode = WIFI_AUTH_WPA2_PSK,},};ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));ESP_ERROR_CHECK(esp_wifi_start());ESP_LOGI(TAG, "Wi-Fi 初始化完成,等待连接...");
}/*** @brief Get_wifiInfo_task 获取实时信息任务* @param pvParameters FreeRTOS 任务参数*/void Get_wifiInfo_task(void *pvParameters)
{wifi_init_sta();vTaskDelay(5000 / portTICK_PERIOD_MS); // 等待 Wi-Fi 连接obtain_time(); // SNTP 同步时间while (1) {print_local_time_and_restart_at_midnight(); // 打印时间并在午夜重启vTaskDelay(5000 / portTICK_PERIOD_MS); // 每5秒打印一次时间}
}
// 追踪并打印当前系统堆内存和任务栈的使用情况
void traceHeap()
{static uint32_t _free_heap_size = 0;	// 静态变量 _free_heap_size 用来保存初始时的可用堆大小(只赋值一次)if (_free_heap_size == 0)_free_heap_size = esp_get_free_heap_size();// 第一次调用时,记录当前的可用堆大小int _diff_free_heap_size = _free_heap_size - esp_get_free_heap_size();// 计算自上次记录以来,堆内存减少的大小(负数代表堆内存消耗增加)ESP_LOGI(__FUNCTION__, "_diff_free_heap_size=%d", _diff_free_heap_size);// 打印堆内存的变化值ESP_LOGI(__FUNCTION__, "esp_get_free_heap_size() : %6" PRIu32 "\n", esp_get_free_heap_size());// 打印当前的可用堆大小#if 0// 打印历史上“最小剩余堆大小”(反映系统堆内存使用的峰值)printf("esp_get_minimum_free_heap_size() : %6"PRIu32"\n", esp_get_minimum_free_heap_size());// FreeRTOS 提供的当前可用堆大小printf("xPortGetFreeHeapSize() : %6zd\n", xPortGetFreeHeapSize());// FreeRTOS 提供的历史最小剩余堆大小(类似上面的函数)printf("xPortGetMinimumEverFreeHeapSize() : %6zd\n", xPortGetMinimumEverFreeHeapSize());// 查询指定内存类型的可用堆大小,这里是 32bit 对齐的堆(常用于 DMA 或特定硬件需求)printf("heap_caps_get_free_size(MALLOC_CAP_32BIT) : %6d\n", heap_caps_get_free_size(MALLOC_CAP_32BIT));// 获取当前任务栈的“水位线”(即曾经使用过的最大深度,数值越小说明栈使用越多)// 返回值表示任务栈剩余的最小值(单位:word),反映任务栈的使用安全性printf("uxTaskGetStackHighWaterMark() : %6d\n", uxTaskGetStackHighWaterMark(NULL));
#endif
}// 功能:读取 BMP 图片文件并显示到 TFT LCD 上,同时统计耗时
TickType_t BMPTest(TFT_t *dev, char *file, int width, int height)
{TickType_t startTick, endTick, diffTick;startTick = xTaskGetTickCount(); // 记录起始时间(系统 tick)lcdSetFontDirection(dev, 0); // 设置字体方向为默认方向lcdFillScreen(dev, BLACK);	 // 屏幕填充黑色背景,准备显示图片// 申请 BMP 文件结构体内存bmpfile_t *bmpfile = (bmpfile_t *)malloc(sizeof(bmpfile_t));if (bmpfile == NULL){ESP_LOGE(__FUNCTION__, "Error allocating memory for bmpfile"); // 内存分配失败return 0;}// 打开指定的 BMP 文件esp_err_t ret;FILE *fp = fopen(file, "rb");if (fp == NULL){ESP_LOGW(__FUNCTION__, "File not found [%s]", file); // 文件未找到return 0;}// 读取 BMP 文件头前两个字节,必须为 "BM"ret = fread(bmpfile->header.magic, 1, 2, fp);assert(ret == 2);if (bmpfile->header.magic[0] != 'B' || bmpfile->header.magic[1] != 'M'){ESP_LOGW(__FUNCTION__, "File is not BMP"); // 文件格式错误free(bmpfile);fclose(fp);return 0;}// 依次读取 BMP 文件头剩余字段ret = fread(&bmpfile->header.filesz, 4, 1, fp);assert(ret == 1); // 文件大小ret = fread(&bmpfile->header.creator1, 2, 1, fp);assert(ret == 1); // 保留字段1ret = fread(&bmpfile->header.creator2, 2, 1, fp);assert(ret == 1); // 保留字段2ret = fread(&bmpfile->header.offset, 4, 1, fp);assert(ret == 1); // 像素数据偏移位置// 读取 BMP DIB 信息头ret = fread(&bmpfile->dib.header_sz, 4, 1, fp);assert(ret == 1); // DIB 头大小ret = fread(&bmpfile->dib.width, 4, 1, fp);assert(ret == 1); // 图像宽度ret = fread(&bmpfile->dib.height, 4, 1, fp);assert(ret == 1); // 图像高度ret = fread(&bmpfile->dib.nplanes, 2, 1, fp);assert(ret == 1); // 色彩平面数(通常为1)ret = fread(&bmpfile->dib.depth, 2, 1, fp);assert(ret == 1); // 每像素位数ret = fread(&bmpfile->dib.compress_type, 4, 1, fp);assert(ret == 1); // 压缩方式ret = fread(&bmpfile->dib.bmp_bytesz, 4, 1, fp);assert(ret == 1); // 图像数据大小ret = fread(&bmpfile->dib.hres, 4, 1, fp);assert(ret == 1); // 水平分辨率ret = fread(&bmpfile->dib.vres, 4, 1, fp);assert(ret == 1); // 垂直分辨率ret = fread(&bmpfile->dib.ncolors, 4, 1, fp);assert(ret == 1); // 调色板颜色数ret = fread(&bmpfile->dib.nimpcolors, 4, 1, fp);assert(ret == 1); // 重要颜色数// 仅支持 24 位无压缩的 BMP 图片if ((bmpfile->dib.depth == 24) && (bmpfile->dib.compress_type == 0)){// 每行像素数据必须按 4 字节对齐(BMP 格式规定)uint32_t rowSize = (bmpfile->dib.width * 3 + 3) & ~3;int w = bmpfile->dib.width;int h = bmpfile->dib.height;ESP_LOGD(__FUNCTION__, "w=%d h=%d", w, h);// 计算水平居中/裁剪位置int _x, _w, _cols, _cole;if (width >= w){ // 屏幕宽度大于等于图片宽度 → 居中显示_x = (width - w) / 2;_w = w;_cols = 0;_cole = w - 1;}else{ // 屏幕宽度小于图片宽度 → 居中裁剪_x = 0;_w = width;_cols = (w - width) / 2;_cole = _cols + width - 1;}// 计算垂直居中/裁剪位置int _y, _rows, _rowe;if (height >= h){ // 屏幕高度大于等于图片高度 → 居中显示_y = (height - h) / 2;_rows = 0;_rowe = h - 1;}else{ // 屏幕高度小于图片高度 → 居中裁剪_y = 0;_rows = (h - height) / 2;_rowe = _rows + height - 1;}#define BUFFPIXEL 20uint8_t sdbuffer[3 * BUFFPIXEL];							 // 临时像素缓冲区(一次读取20个像素)uint16_t *colors = (uint16_t *)malloc(sizeof(uint16_t) * w); // 一行像素转换成 RGB565if (colors == NULL){ESP_LOGE(__FUNCTION__, "Error allocating memory for color"); // 内存不足free(bmpfile);fclose(fp);return 0;}// 按行读取 BMP 像素并转换为 RGB565for (int row = 0; row < h; row++){if (row < _rows || row > _rowe)continue; // 跳过裁剪区域// 定位到该行的起始地址(BMP 自底向上存储)int pos = bmpfile->header.offset + (h - 1 - row) * rowSize;fseek(fp, pos, SEEK_SET);int buffidx = sizeof(sdbuffer); // 强制首次加载数据int index = 0;for (int col = 0; col < w; col++){if (buffidx >= sizeof(sdbuffer)){ // 读取一批像素数据fread(sdbuffer, sizeof(sdbuffer), 1, fp);buffidx = 0;}if (col < _cols || col > _cole)continue; // 跳过裁剪列// 读取 BGR 三通道并转换为 RGB565 格式uint8_t b = sdbuffer[buffidx++];uint8_t g = sdbuffer[buffidx++];uint8_t r = sdbuffer[buffidx++];colors[index++] = rgb565(r, g, b);}// 将整行像素发送到 LCD 显示lcdDrawMultiPixels(dev, _x, _y, _w, colors);_y++; // 屏幕 Y 坐标递增}free(colors); // 释放像素缓冲区}lcdDrawFinish(dev); // 通知 LCD 绘制完成free(bmpfile);		// 释放 BMP 文件头内存fclose(fp);			// 关闭文件endTick = xTaskGetTickCount(); // 记录结束时间diffTick = endTick - startTick;ESP_LOGI(__FUNCTION__, "elapsed time[ms]:%" PRIu32, diffTick * portTICK_PERIOD_MS); // 打印耗时return diffTick; // 返回耗时(tick)
}/*** @brief 显示 BMP 图片* @param dev LCD 设备结构体指针*/
static void display_bmp(TFT_t *dev)
{char file[32];strcpy(file, "/images/image.bmp");BMPTest(dev, file, CONFIG_WIDTH, CONFIG_HEIGHT);WAIT;
}/*** @brief 显示当前北京时间的时钟 (HH:MM)* @param dev   LCD 设备结构体指针* @param fx32M 字体文件 (32Dot Mincyo Font)* display_clock(&dev, fx32M);  // 显示时钟*/
static void display_clock(TFT_t *dev, FontxFile *fx32M)
{time_t now;struct tm timeinfo;time(&now);localtime_r(&now, &timeinfo);// 格式化为 HH:MMchar time_str[10];snprintf(time_str, sizeof(time_str), "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min);// 计算居中坐标uint16_t font_width = 32;   // 每个字符宽度,32点阵约32pxuint16_t str_len = strlen(time_str);uint16_t text_width = font_width * str_len;uint16_t xpos = (CONFIG_WIDTH - text_width) / 2;uint16_t ypos = CONFIG_HEIGHT / 2;  // 居中显示// 清屏并显示lcdFillScreen(dev, BLACK);lcdSetFontDirection(dev, 0);lcdDrawString(dev, fx32M, xpos, ypos, (uint8_t *)time_str, WHITE);lcdDrawFinish(dev);
}
/*** @brief 初始化字体文件* @param fx32G 哥特体* @param fx32L 拉丁体* @param fx32M 明朝体*/
static void init_fonts(FontxFile *fx32G, FontxFile *fx32L, FontxFile *fx32M)
{InitFontx(fx32G, "/fonts/ILGH32XB.FNT", "");InitFontx(fx32L, "/fonts/LATIN32B.FNT", "");InitFontx(fx32M, "/fonts/ILMH32XB.FNT", "");
}
/*** @brief 初始化 LCD 硬件* @param dev  LCD 设备结构体指针*/
static void lcd_init_device(TFT_t *dev)
{spi_master_init(dev, CONFIG_MOSI_GPIO, CONFIG_SCLK_GPIO,CONFIG_CS_GPIO, CONFIG_DC_GPIO,CONFIG_RESET_GPIO, CONFIG_BL_GPIO);lcdInit(dev, CONFIG_WIDTH, CONFIG_HEIGHT, CONFIG_OFFSETX, CONFIG_OFFSETY);
}
/*** @brief ST7789_Show_task 显示任务* @param pvParameters FreeRTOS 任务参数*/
void ST7789_Show_task(void *pvParameters)
{FontxFile fx32G[2], fx32L[2], fx32M[2];init_fonts(fx32G, fx32L, fx32M);TFT_t dev;lcd_init_device(&dev);display_bmp(&dev);// 显示 BMP 图片while (1) {display_clock(&dev, fx32M);  // 显示时钟vTaskDelay(1000 / portTICK_PERIOD_MS); // 每秒更新一次}
}
// ============================= SPIFFS 文件系统工具函数 =============================// 功能:遍历指定路径下的 SPIFFS 文件系统,打印目录内容
// 参数:
//   path - 要遍历的路径,例如 "/fonts"
// 说明:
//   使用 opendir 打开目录,然后通过 readdir 逐个读取文件/目录信息,直到读取结束。
//   每个文件的名称、inode 节点号、类型都会被打印出来。
static void listSPIFFS(char *path)
{DIR *dir = opendir(path); // 打开目录assert(dir != NULL);	  // 如果目录不存在,则触发断言(程序终止)while (true){struct dirent *pe = readdir(dir); // 读取下一个目录项if (!pe)break; // 没有更多文件则退出循环ESP_LOGI(__FUNCTION__,"d_name=%s d_ino=%d d_type=%x", // 打印文件名、inode 节点号、文件类型pe->d_name, pe->d_ino, pe->d_type);}closedir(dir); // 关闭目录
}// 功能:挂载 SPIFFS 文件系统到指定路径
// 参数:
//   path       - 挂载到的虚拟路径(如 "/fonts")
//   label      - 对应的分区标签(如 "storage1"),需在分区表中配置
//   max_files  - SPIFFS 文件系统同时允许打开的最大文件数
// 返回值:
//   ESP_OK         - 成功挂载
//   其他错误代码   - 挂载失败(可能是找不到分区、文件系统损坏等)
// 说明:
//   使用 esp_vfs_spiffs_register 进行挂载,如果失败会根据错误码打印详细日志。
//   挂载成功后,还会打印该分区的总容量与已使用容量。
esp_err_t mountSPIFFS(char *path, char *label, int max_files)
{esp_vfs_spiffs_conf_t conf = {.base_path = path,			   // 挂载点路径.partition_label = label,	   // 分区标签(需在分区表中声明).max_files = max_files,		   // 允许同时打开的最大文件数.format_if_mount_failed = true // 如果挂载失败则格式化分区};// 使用配置挂载 SPIFFS 文件系统esp_err_t ret = esp_vfs_spiffs_register(&conf);if (ret != ESP_OK){if (ret == ESP_FAIL){ESP_LOGE(TAG, "Failed to mount or format filesystem"); // 挂载或格式化失败}else if (ret == ESP_ERR_NOT_FOUND){ESP_LOGE(TAG, "Failed to find SPIFFS partition"); // 未找到对应分区}else{ESP_LOGE(TAG, "Failed to initialize SPIFFS (%s)", esp_err_to_name(ret));}return ret; // 返回错误码}#if 0// 可选:检查 SPIFFS 文件系统完整性ESP_LOGI(TAG, "Performing SPIFFS_check().");ret = esp_spiffs_check(conf.partition_label);if (ret != ESP_OK) {ESP_LOGE(TAG, "SPIFFS_check() failed (%s)", esp_err_to_name(ret));return ret;} else {ESP_LOGI(TAG, "SPIFFS_check() successful");}
#endif// 打印 SPIFFS 分区信息(总容量 & 已用容量)size_t total = 0, used = 0;ret = esp_spiffs_info(conf.partition_label, &total, &used);if (ret != ESP_OK){ESP_LOGE(TAG, "Failed to get SPIFFS partition information (%s)", esp_err_to_name(ret));}else{ESP_LOGI(TAG, "Mount %s to %s success", path, label); // 打印挂载成功信息ESP_LOGI(TAG, "Partition size: total: %d, used: %d", total, used);}return ret;
}// ============================= 主程序入口 =============================// 功能:程序入口函数 app_main
// 说明:
//   1. 依次挂载 /fonts、/images、/icons 三个 SPIFFS 分区
//   2. 遍历并打印分区内容
//   3. 创建 LCD 显示任务 ST7789(分配 4KB 栈空间,优先级为 2)
void app_main(void)
{ESP_LOGI(TAG, "Initializing SPIFFS");// 挂载 /fonts 分区,最大同时打开文件数为 7ESP_ERROR_CHECK(mountSPIFFS("/fonts", "storage1", 7));listSPIFFS("/fonts/");// 挂载 /images 分区,最大同时打开文件数为 1ESP_ERROR_CHECK(mountSPIFFS("/images", "storage2", 1));listSPIFFS("/images/");// 挂载 /icons 分区,最大同时打开文件数为 1ESP_ERROR_CHECK(mountSPIFFS("/icons", "storage3", 1));listSPIFFS("/icons/");// 创建 ST7789_Show_task 显示任务// 参数://   任务函数:ST7789_Show_task//   任务名  :"ST7789_Show_task"//   栈大小  :1024*4 = 4096 字节(4KB)//   任务参数:NULL//   优先级  :2//   任务句柄:NULL(不保存任务句柄)xTaskCreate(ST7789_Show_task, "ST7789_Show_task", 1024 * 4, NULL, 2, NULL);// 创建 Get_wifiInfo_task 获取实时信息任务// 参数://   任务函数:Get_wifiInfo_task//   任务名  :"Get_wifiInfo_task"//   栈大小  :1024*4 = 4096 字节(4KB)//   任务参数:NULL//   优先级  :1//   任务句柄:NULL(不保存任务句柄)xTaskCreate(Get_wifiInfo_task, "Get_wifiInfo_task", 1024 * 4, NULL, 1, NULL);
}
  • 效果
    在这里插入图片描述

  • 问题

可以实现图片解码显示和WiFi联网获取时间,但是实现前景+背景显示比较复杂。

5. SquareLine Studio 移植指南

在线转换图片格式工具
在这里插入图片描述
用这个工具提前准备好需要的图片素材,导入 SquareLine Studio。

5.1 SquareLine Studio 简介

SquareLine Studio 是一款专业的嵌入式 GUI(图形用户界面)开发工具,由 LVGL(Light and Versatile Graphics Library)官方团队开发。它的核心理念是让开发者能够以拖拽式、所见即所得的方式,为嵌入式设备(如智能手表、家电面板、工业控制器等)设计美观且功能丰富的用户界面,而无需编写大量的底层绘图代码。

简单来说,它就像是 “嵌入式界的 Figma 或 Sketch”,但最终生成的是可以直接在微控制器(如 ESP32、STM32、Raspberry Pi Pico 等)上运行的 C 代码。

5.2 项目创建与 UI 设计

  • 新建 240x240 UI 工程
  • 添加背景图片(时钟底图)
  • 添加 Label(显示时间)
    在这里插入图片描述

5.3 代码导出

  • 导出 UI 文件结构说明
    在这里插入图片描述

5.4 移植到 ESP-IDF工程

  • 复制ui文件到项目文件夹
    在这里插入图片描述

  • 修改 CMakeLists.txt,引入 UI 代码

file(GLOB_RECURSE SRC_SOURCES components/*.c fonts/*.c images/*.c screens/*.c)
idf_component_register(SRCS "spi_lcd_touch_example_main.c" "lvgl_demo_ui.c" "ui_helpers.c" "ui.c" ${SRC_SOURCES} INCLUDE_DIRS ".")

在这里插入图片描述

  • main.c 调用 UI 初始化函数

添加ui.h头文件
在这里插入图片描述
调用 ui_init初始化函数
在这里插入图片描述

  • 调试 UI 显示效果
    解决lv_font_montserrat_48报错

C:/Users/xsshu/Desktop/spi_lcd/main/screens/ui_Screen1.c: In function 'ui_Screen1_screen_init': C:/Users/xsshu/Desktop/spi_lcd/main/screens/ui_Screen1.c:39:44: error: 'lv_font_montserrat_48' undeclared (first use in this function); did you mean 'lv_font_montserrat_14'? 39 | lv_obj_set_style_text_font(ui_Label1, &lv_font_montserrat_48, LV_PART_MAIN | LV_STATE_DEFAULT); | ^~~~~~~~~~~~~~~~~~~~~ | lv_font_montserrat_14 C:/Users/xsshu/Desktop/spi_lcd/main/screens/ui_Screen1.c:39:44: note: each undeclared identifier is reported only once for each function it appears in [11/17] Building C object esp-idf/main/CMakeFiles/__idf_main.dir/spi_lcd_touch_example_main.c.obj

为什么会这样?

  1. LVGL 自带的字体
    LVGL 默认只开启了 lv_font_montserrat_14,大多数其他大小(如 20, 28, 48)默认是 关闭的
    所以你直接用 lv_font_montserrat_48 就会报错。

  2. SquareLine Studio
    在 SquareLine 里选了 48 号字体,生成代码时会写 &lv_font_montserrat_48,但如果 LVGL 配置里没启用这个字体,就会报错。

解决方法 :

lv_conf.h 开启 48 号字体

在这里插入图片描述

6. WiFi NTP 服务器获取网络时间

WiFi NTP测试程序

/** 功能:* - 连接 Wi-Fi* - 从 NTP 服务器获取网络时间* - 设置为北京时间 (UTC+8)* - 打印当前时间*/#include <stdio.h>
#include <string.h>
#include <time.h>
#include <sys/time.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#include "esp_sntp.h"static const char *TAG = "APP";// ---------------- Wi-Fi 配置 ----------------
#define WIFI_SSID "TP-LINK-CQJY"
#define WIFI_PASS "xxx"// Wi-Fi 事件处理函数
static void wifi_event_handler(void *arg, esp_event_base_t event_base,int32_t event_id, void *event_data)
{if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START){esp_wifi_connect();}else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED){ESP_LOGI(TAG, "Wi-Fi 断开,尝试重连...");esp_wifi_connect();}else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP){ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;ESP_LOGI(TAG, "获取到IP地址: " IPSTR, IP2STR(&event->ip_info.ip));}
}// ---------------- NTP 时间同步 ----------------
static void initialize_sntp(void)
{ESP_LOGI(TAG, "初始化 SNTP...");esp_sntp_setoperatingmode(SNTP_OPMODE_POLL);esp_sntp_setservername(0, "ntp.aliyun.com"); // 你也可以换成 pool.ntp.orgesp_sntp_init();
}static void obtain_time(void)
{initialize_sntp();// 等待时间同步time_t now = 0;struct tm timeinfo = {0};int retry = 0;const int retry_count = 10;while (timeinfo.tm_year < (2016 - 1900) && ++retry < retry_count){ESP_LOGI(TAG, "等待时间同步... (%d/%d)", retry, retry_count);vTaskDelay(2000 / portTICK_PERIOD_MS);time(&now);localtime_r(&now, &timeinfo);}// 设置时区为 北京时间 (UTC+8)setenv("TZ", "CST-8", 1);tzset();// 获取本地时间time(&now);localtime_r(&now, &timeinfo);ESP_LOGI(TAG, "当前北京时间: %04d-%02d-%02d %02d:%02d:%02d",timeinfo.tm_year + 1900,timeinfo.tm_mon + 1,timeinfo.tm_mday,timeinfo.tm_hour,timeinfo.tm_min,timeinfo.tm_sec);
}// ---------------- 主函数 ----------------
void app_main(void)
{// 初始化 NVSesp_err_t ret = nvs_flash_init();if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND){ESP_ERROR_CHECK(nvs_flash_erase()); // 擦除 NVS 分区ret = nvs_flash_init();             // 重新初始化}ESP_ERROR_CHECK(ret);ESP_ERROR_CHECK(esp_netif_init());ESP_ERROR_CHECK(esp_event_loop_create_default());// 创建默认 Wi-Fi STAesp_netif_create_default_wifi_sta();wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();ESP_ERROR_CHECK(esp_wifi_init(&cfg));// 注册 Wi-Fi 和 IP 事件ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,ESP_EVENT_ANY_ID,&wifi_event_handler,NULL,NULL));ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,IP_EVENT_STA_GOT_IP,&wifi_event_handler,NULL,NULL));// 设置 Wi-Fi STA 模式wifi_config_t wifi_config = {.sta = {.ssid = WIFI_SSID,.password = WIFI_PASS,.threshold.authmode = WIFI_AUTH_WPA2_PSK,},};ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));ESP_ERROR_CHECK(esp_wifi_start());ESP_LOGI(TAG, "Wi-Fi 初始化完成,等待连接...");// 等待 Wi-Fi 连接成功(简单延时)vTaskDelay(5000 / portTICK_PERIOD_MS);// 获取并打印北京时间obtain_time();// 循环打印时间while (1){time_t now;struct tm timeinfo;time(&now);localtime_r(&now, &timeinfo);ESP_LOGI(TAG, "北京时间: %04d-%02d-%02d %02d:%02d:%02d",timeinfo.tm_year + 1900,timeinfo.tm_mon + 1,timeinfo.tm_mday,timeinfo.tm_hour,timeinfo.tm_min,timeinfo.tm_sec);vTaskDelay(10000 / portTICK_PERIOD_MS); // 每 10 秒打印一次}
}

ui移植成功,V1.0.0版本(图片背景+WiFi时钟)

/*** @file main.c* @brief ST7789 LCD显示控制器与LVGL图形库集成示例* @version 1.0* @date 2021-2022* @copyright Copyright (c) 2021-2022 Espressif Systems (Shanghai) CO LTD* @license CC0-1.0*/#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_timer.h"
#include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_vendor.h"
#include "esp_lcd_panel_ops.h"
#include "driver/gpio.h"
#include "driver/spi_master.h"
#include "esp_err.h"
#include "esp_log.h"
#include "lvgl.h"
//  #include "esp_log.h"
// #include "bsp/esp-box.h"
// #include "lvgl.h"
#include "ui.h"#include <time.h>
#include <sys/time.h>
#include "esp_wifi.h"
#include "esp_event.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#include "esp_sntp.h"// 不再需要触摸控制器头文件
//  static const char *TAG = "example";  // 日志标签
static const char *TAG = "APP_st7789_wifi";
static bool has_restarted_today = false; // 标记今天是否已重启
// 使用SPI2主机
#define LCD_HOST SPI2_HOST////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////// 请根据您的LCD规格更新以下配置 //////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#define EXAMPLE_LCD_PIXEL_CLOCK_HZ (20 * 1000 * 1000)                 // LCD像素时钟频率,20MHz
#define EXAMPLE_LCD_BK_LIGHT_ON_LEVEL 1                               // 背光开启电平
#define EXAMPLE_LCD_BK_LIGHT_OFF_LEVEL !EXAMPLE_LCD_BK_LIGHT_ON_LEVEL // 背光关闭电平// GPIO引脚配置
#define EXAMPLE_PIN_NUM_SCLK 6     // SPI时钟引脚
#define EXAMPLE_PIN_NUM_MOSI 7     // SPI主出从入引脚
#define EXAMPLE_PIN_NUM_MISO -1    // SPI主入从出引脚(未使用)
#define EXAMPLE_PIN_NUM_LCD_DC 3   // LCD数据/命令选择引脚
#define EXAMPLE_PIN_NUM_LCD_RST 4  // LCD复位引脚
#define EXAMPLE_PIN_NUM_LCD_CS 10  // LCD片选引脚
#define EXAMPLE_PIN_NUM_BK_LIGHT 5 // 背光控制引脚// 已删除触摸控制器片选引脚定义// 水平和垂直方向的像素数量
#define EXAMPLE_LCD_H_RES 240
#define EXAMPLE_LCD_V_RES 240// 用于表示命令和参数的位数
#define EXAMPLE_LCD_CMD_BITS 8   // 命令位数
#define EXAMPLE_LCD_PARAM_BITS 8 // 参数位数#define EXAMPLE_LVGL_TICK_PERIOD_MS 2 // LVGL定时器周期(毫秒)/////////////////////////////////////////////////
// ---------------- Wi-Fi 配置 ----------------
#define WIFI_SSID "1-2-3"
#define WIFI_PASS "x.cm"// Wi-Fi 事件处理函数
static void wifi_event_handler(void *arg, esp_event_base_t event_base,int32_t event_id, void *event_data)
{if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START){esp_wifi_connect();}else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED){ESP_LOGI(TAG, "Wi-Fi 断开,尝试重连...");esp_wifi_connect();}else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP){ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;ESP_LOGI(TAG, "获取到IP地址: " IPSTR, IP2STR(&event->ip_info.ip));}
}// ---------------- NTP 时间同步 ----------------
static void initialize_sntp(void)
{ESP_LOGI(TAG, "初始化 SNTP...");esp_sntp_setoperatingmode(SNTP_OPMODE_POLL);esp_sntp_setservername(0, "ntp.aliyun.com"); // 你也可以换成 pool.ntp.orgesp_sntp_init();
}static void obtain_time(void)
{initialize_sntp();// 等待时间同步time_t now = 0;struct tm timeinfo = {0};int retry = 0;const int retry_count = 10;while (timeinfo.tm_year < (2016 - 1900) && ++retry < retry_count){ESP_LOGI(TAG, "等待时间同步... (%d/%d)", retry, retry_count);vTaskDelay(2000 / portTICK_PERIOD_MS);time(&now);localtime_r(&now, &timeinfo);}// 设置时区为 北京时间 (UTC+8)setenv("TZ", "CST-8", 1);tzset();// 获取本地时间time(&now);localtime_r(&now, &timeinfo);ESP_LOGI(TAG, "当前北京时间: %04d-%02d-%02d %02d:%02d:%02d",timeinfo.tm_year + 1900,timeinfo.tm_mon + 1,timeinfo.tm_mday,timeinfo.tm_hour,timeinfo.tm_min,timeinfo.tm_sec);
}/*** @brief 打印当前北京时间,并在 23:46 时 执行一次软件重启*/
static void print_local_time_and_restart_at_midnight(void)
{time_t now;struct tm timeinfo;time(&now);localtime_r(&now, &timeinfo);int hour = timeinfo.tm_hour;int minute = timeinfo.tm_min;int second = timeinfo.tm_sec;ESP_LOGI(TAG, "北京时间: %04d-%02d-%02d %02d:%02d:%02d",timeinfo.tm_year + 1900,timeinfo.tm_mon + 1,timeinfo.tm_mday,hour,minute,second);// 定义一个缓冲区存放拼好的字符串char time_str[16];  snprintf(time_str, sizeof(time_str), "%02d:%02d", hour, minute);lv_label_set_text(ui_Label1, time_str);// 刷新到 label// 检查是否为 00:00:00 ~ 00:00:00,并且今天还没有重启过if (hour == 23 && minute == 46 && second == 0){if (!has_restarted_today){ESP_LOGI(TAG, "到达午夜零点,正在重启设备...");has_restarted_today = true;esp_restart();}}else{// 如果不是 00:00:00,则重置标志位(允许明天再次重启)// 注意:更精确的方式是检测日期变化,但简单场景下可接受if (hour > 0){has_restarted_today = false;}}
}/*** @brief 初始化 Wi-Fi STA 模式*        - 初始化 NVS 存储*        - 创建默认网络接口*        - 注册 Wi-Fi/IP 事件回调*        - 启动 Wi-Fi 并连接到路由器*/
static void wifi_init_sta(void)
{esp_err_t ret = nvs_flash_init();if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND){ESP_ERROR_CHECK(nvs_flash_erase());ret = nvs_flash_init();}ESP_ERROR_CHECK(ret);ESP_ERROR_CHECK(esp_netif_init());ESP_ERROR_CHECK(esp_event_loop_create_default());esp_netif_create_default_wifi_sta();wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();ESP_ERROR_CHECK(esp_wifi_init(&cfg));ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,ESP_EVENT_ANY_ID,&wifi_event_handler,NULL,NULL));ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,IP_EVENT_STA_GOT_IP,&wifi_event_handler,NULL,NULL));wifi_config_t wifi_config = {.sta = {.ssid = WIFI_SSID,.password = WIFI_PASS,.threshold.authmode = WIFI_AUTH_WPA2_PSK,},};ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));ESP_ERROR_CHECK(esp_wifi_start());ESP_LOGI(TAG, "Wi-Fi 初始化完成,等待连接...");
}/*** @brief Get_wifiInfo_task 获取实时信息任务* @param pvParameters FreeRTOS 任务参数*/void Get_wifiInfo_task(void *pvParameters)
{wifi_init_sta();vTaskDelay(5000 / portTICK_PERIOD_MS); // 等待 Wi-Fi 连接obtain_time();                         // SNTP 同步时间while (1){print_local_time_and_restart_at_midnight(); // 打印时间并在午夜重启vTaskDelay(1000 / portTICK_PERIOD_MS);      // 每x秒打印一次时间}
}////////////////////////////////////////////////
// 声明LVGL演示UI函数
//  extern void example_lvgl_demo_ui(lv_disp_t *disp);/*** @brief LVGL刷新完成通知回调函数* @param panel_io LCD面板IO句柄* @param edata 事件数据* @param user_ctx 用户上下文(LVGL显示驱动)* @return 总是返回false*/
static bool example_notify_lvgl_flush_ready(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx)
{lv_disp_drv_t *disp_driver = (lv_disp_drv_t *)user_ctx;lv_disp_flush_ready(disp_driver); // 通知LVGL刷新完成return false;
}/*** @brief LVGL刷新回调函数* @param drv LVGL显示驱动* @param area 需要刷新的区域* @param color_map 颜色数据映射*/
static void example_lvgl_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_map)
{esp_lcd_panel_handle_t panel_handle = (esp_lcd_panel_handle_t)drv->user_data;int offsetx1 = area->x1; // 区域左上角X坐标int offsetx2 = area->x2; // 区域右下角X坐标int offsety1 = area->y1; // 区域左上角Y坐标int offsety2 = area->y2; // 区域右下角Y坐标// 将缓冲区内容复制到显示器的特定区域esp_lcd_panel_draw_bitmap(panel_handle, offsetx1, offsety1, offsetx2 + 1, offsety2 + 1, color_map);
}/*** @brief 当LVGL中旋转屏幕时更新显示方向* @param drv LVGL显示驱动*/
static void example_lvgl_port_update_callback(lv_disp_drv_t *drv)
{esp_lcd_panel_handle_t panel_handle = (esp_lcd_panel_handle_t)drv->user_data;switch (drv->rotated){case LV_DISP_ROT_NONE:// 旋转LCD显示esp_lcd_panel_swap_xy(panel_handle, false);esp_lcd_panel_mirror(panel_handle, true, false);break;case LV_DISP_ROT_90:// 旋转LCD显示esp_lcd_panel_swap_xy(panel_handle, true);esp_lcd_panel_mirror(panel_handle, true, true);break;case LV_DISP_ROT_180:// 旋转LCD显示esp_lcd_panel_swap_xy(panel_handle, false);esp_lcd_panel_mirror(panel_handle, false, true);break;case LV_DISP_ROT_270:// 旋转LCD显示esp_lcd_panel_swap_xy(panel_handle, true);esp_lcd_panel_mirror(panel_handle, false, false);break;}
}/*** @brief 增加LVGL定时器计数* @param arg 参数(未使用)*/
static void example_increase_lvgl_tick(void *arg)
{// 告诉LVGL已经过去了多少毫秒lv_tick_inc(EXAMPLE_LVGL_TICK_PERIOD_MS);
}/*** @brief 主应用程序入口*/
void app_main(void)
{static lv_disp_draw_buf_t disp_buf; // 包含内部图形缓冲区(称为绘制缓冲区)static lv_disp_drv_t disp_drv;      // 包含回调函数ESP_LOGI(TAG, "关闭LCD背光");// 配置背光GPIOgpio_config_t bk_gpio_config = {.mode = GPIO_MODE_OUTPUT,                        // 输出模式.pin_bit_mask = 1ULL << EXAMPLE_PIN_NUM_BK_LIGHT // 背光引脚位掩码};ESP_ERROR_CHECK(gpio_config(&bk_gpio_config));ESP_LOGI(TAG, "初始化SPI总线");// SPI总线配置spi_bus_config_t buscfg = {.sclk_io_num = EXAMPLE_PIN_NUM_SCLK,                          // 时钟引脚.mosi_io_num = EXAMPLE_PIN_NUM_MOSI,                          // MOSI引脚.miso_io_num = EXAMPLE_PIN_NUM_MISO,                          // MISO引脚(未使用).quadwp_io_num = -1,                                          // QUADWP引脚(未使用).quadhd_io_num = -1,                                          // QUADHD引脚(未使用).max_transfer_sz = EXAMPLE_LCD_H_RES * 80 * sizeof(uint16_t), // 最大传输大小};ESP_ERROR_CHECK(spi_bus_initialize(LCD_HOST, &buscfg, SPI_DMA_CH_AUTO)); // 初始化SPI总线ESP_LOGI(TAG, "安装面板IO");esp_lcd_panel_io_handle_t io_handle = NULL;// SPI面板IO配置esp_lcd_panel_io_spi_config_t io_config = {.dc_gpio_num = EXAMPLE_PIN_NUM_LCD_DC,                  // 数据/命令选择引脚.cs_gpio_num = EXAMPLE_PIN_NUM_LCD_CS,                  // 片选引脚.pclk_hz = EXAMPLE_LCD_PIXEL_CLOCK_HZ,                  // 像素时钟频率.lcd_cmd_bits = EXAMPLE_LCD_CMD_BITS,                   // 命令位数.lcd_param_bits = EXAMPLE_LCD_PARAM_BITS,               // 参数位数.spi_mode = 0,                                          // SPI模式.trans_queue_depth = 10,                                // 传输队列深度.on_color_trans_done = example_notify_lvgl_flush_ready, // 颜色传输完成回调.user_ctx = &disp_drv,                                  // 用户上下文};// 将LCD连接到SPI总线ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)LCD_HOST, &io_config, &io_handle));esp_lcd_panel_handle_t panel_handle = NULL;// 面板设备配置esp_lcd_panel_dev_config_t panel_config = {.reset_gpio_num = EXAMPLE_PIN_NUM_LCD_RST,  // 复位引脚.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, // RGB元素顺序.bits_per_pixel = 16,                       // 每像素位数};// 根据配置选择LCD控制器ESP_LOGI(TAG, "安装ST7789面板驱动");ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(io_handle, &panel_config, &panel_handle));// 初始化LCD面板ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle)); // 复位面板ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle));  // 初始化面板// 特定面板配置
#if CONFIG_EXAMPLE_LCD_CONTROLLER_GC9A01ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel_handle, true)); // 反转颜色
#endif// 通用面板配置ESP_ERROR_CHECK(esp_lcd_panel_mirror(panel_handle, false, false)); // 设置镜像ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel_handle, true));   // 反转颜色// 用户可以在打开屏幕或背光之前将预定义图案刷新到屏幕ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle, true)); // 打开显示// 已删除触摸控制器初始化代码ESP_LOGI(TAG, "打开LCD背光");gpio_set_level(EXAMPLE_PIN_NUM_BK_LIGHT, EXAMPLE_LCD_BK_LIGHT_ON_LEVEL); // 设置背光电平ESP_LOGI(TAG, "初始化LVGL库");lv_init(); // 初始化LVGL库// 分配LVGL使用的绘制缓冲区// 建议选择至少为屏幕大小1/10的绘制缓冲区大小lv_color_t *buf1 = heap_caps_malloc(EXAMPLE_LCD_H_RES * 20 * sizeof(lv_color_t), MALLOC_CAP_DMA);assert(buf1); // 断言确保分配成功lv_color_t *buf2 = heap_caps_malloc(EXAMPLE_LCD_H_RES * 20 * sizeof(lv_color_t), MALLOC_CAP_DMA);assert(buf2); // 断言确保分配成功// // 初始化LVGL绘制缓冲区lv_disp_draw_buf_init(&disp_buf, buf1, buf2, EXAMPLE_LCD_H_RES * 20);ESP_LOGI(TAG, "向LVGL注册显示驱动");lv_disp_drv_init(&disp_drv);                                // 初始化显示驱动disp_drv.hor_res = EXAMPLE_LCD_H_RES;                       // 设置水平分辨率disp_drv.ver_res = EXAMPLE_LCD_V_RES;                       // 设置垂直分辨率disp_drv.flush_cb = example_lvgl_flush_cb;                  // 设置刷新回调disp_drv.drv_update_cb = example_lvgl_port_update_callback; // 设置驱动更新回调disp_drv.draw_buf = &disp_buf;                              // 设置绘制缓冲区disp_drv.user_data = panel_handle;                          // 设置用户数据lv_disp_t *disp = lv_disp_drv_register(&disp_drv);          // 注册显示驱动ESP_LOGI(TAG, "安装LVGL定时器");// LVGL的定时器接口(使用esp_timer生成2ms周期性事件)const esp_timer_create_args_t lvgl_tick_timer_args = {.callback = &example_increase_lvgl_tick, // 回调函数.name = "lvgl_tick"                      // 定时器名称};esp_timer_handle_t lvgl_tick_timer = NULL;ESP_ERROR_CHECK(esp_timer_create(&lvgl_tick_timer_args, &lvgl_tick_timer));                     // 创建定时器ESP_ERROR_CHECK(esp_timer_start_periodic(lvgl_tick_timer, EXAMPLE_LVGL_TICK_PERIOD_MS * 1000)); // 启动定时器// 已删除触摸输入设备初始化代码ESP_LOGI(TAG, "显示LVGL仪表部件");//  example_lvgl_demo_ui(disp);  // 显示LVGL演示UIui_init(); //// 创建 Get_wifiInfo_task 获取实时信息任务// 参数://   任务函数:Get_wifiInfo_task//   任务名  :"Get_wifiInfo_task"//   栈大小  :1024*4 = 4096 字节(4KB)//   任务参数:NULL//   优先级  :1//   任务句柄:NULL(不保存任务句柄)xTaskCreate(Get_wifiInfo_task, "Get_wifiInfo_task", 1024 * 6, NULL, 1, NULL);// 主循环while (1){// 提高LVGL的任务优先级和/或减少处理程序周期可以提高性能vTaskDelay(pdMS_TO_TICKS(10)); // 延迟10毫秒// 运行lv_timer_handler的任务优先级应低于运行`lv_tick_inc`的任务lv_timer_handler(); // 处理LVGL定时器}
}

7. 常见问题与调试经验

  • VSCode 编译报错
  • SPI Flash 容量警告
  • 字体缺失问题(montserrat_48)
  • Label 背景阴影设置

8. 效果展示

  • UI 界面截图
    在这里插入图片描述

  • 实机运行效果图/视频

《ESP-IDF/C3/LVGL+Square Line Studio驱动ST7789 TFT屏幕》

9. 总结与展望

  • 总结:SquareLine Studio 极大地降低了嵌入式 GUI 开发的门槛和成本。它将开发者从重复性的造轮子工作中解放出来。
  • 下一步待优化、扩展功能(背光调节,屏幕UI效果、布局等)

资料下载

  • ESP32-C3 桌面WiFi时钟20250912(V1.0.0)

文章转载自:

http://2p4EL4tn.qrzqd.cn
http://HYcrmMjy.qrzqd.cn
http://aBPZdAfr.qrzqd.cn
http://EKajK8tp.qrzqd.cn
http://FHvFbcbH.qrzqd.cn
http://2B3629hb.qrzqd.cn
http://hWGJrJSm.qrzqd.cn
http://iFKH2joF.qrzqd.cn
http://LJA6RXDt.qrzqd.cn
http://MkyEQbf8.qrzqd.cn
http://UONRKZbL.qrzqd.cn
http://TpphP3Ku.qrzqd.cn
http://xjPE3s6w.qrzqd.cn
http://OHDqv5Za.qrzqd.cn
http://EGywyrjk.qrzqd.cn
http://ZnBqlqg1.qrzqd.cn
http://Nqu5Fzxh.qrzqd.cn
http://v1sgJstY.qrzqd.cn
http://QaZKlwqk.qrzqd.cn
http://QSoNFGzf.qrzqd.cn
http://7ClGWYH9.qrzqd.cn
http://lPsYfRzK.qrzqd.cn
http://mtLqDOB1.qrzqd.cn
http://rZh7Fc3q.qrzqd.cn
http://tnC1Z82W.qrzqd.cn
http://92hBAb6J.qrzqd.cn
http://HXOwHD6t.qrzqd.cn
http://qEcRUgnS.qrzqd.cn
http://J5t1n8T6.qrzqd.cn
http://EI5pGZ7l.qrzqd.cn
http://www.dtcms.com/a/381839.html

相关文章:

  • 大数据毕业设计选题推荐-基于大数据的健康与生活方式数据可视化分析系统-Spark-Hadoop-Bigdata
  • 可配日志输出
  • 学习笔记:Python的起源
  • vcpkg:面向C/C++的跨平台库管理工具软件配置笔记经验教程
  • Claude Code的交互方式
  • 使用atop工具监控Linux系统指标
  • 工具链部署实用技巧 7|模型设计帧率推理时耗时与带宽分析
  • 《SRE 系列(八)| 高效组织协作经验》
  • 数据结构---链式队列
  • 【C++实战⑦】C++函数实战:从基础到项目应用
  • 通过语义AI管道检测文本数据中的潜在异常值
  • 这是第二篇
  • Mamba模型介绍
  • rock linux 9 安装mysql 5.7.44
  • 基于STM32智能农业大棚检测控制系统设计
  • 05 回归问题和分类问题
  • Linux应用(4)——进程通信
  • 用C语言解决喝汽水问题
  • 【开题答辩全过程】以 4S店汽车维修保养管理系统为例,包含答辩的问题和答案
  • 边缘计算技术深入解析
  • 三生原理的“素性塔“结构是否暗含共形场论中的算子乘积展开层级?‌
  • 如何解决pip安装报错ModuleNotFoundError: No module named ‘cugraph’问题
  • 评估硬件兼容性时如何快速判断老旧设备是否支持新协议
  • [2025]使用echarts制作一个漂亮的天气预报曲线图
  • 每日算法题推送
  • DataSet-深度学习中的常见类
  • Python编辑器的安装及配置(Pycharm、Jupyter的安装)从0带你配置,小土堆视频
  • SystemVerilog 学习之SystemVerilog简介
  • 中国联通卫星移动通信业务分析
  • 学习游戏制作记录(实现震动效果,文本提示和构建游戏)9.13