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

发布于:2025-09-14 ⋅ 阅读:(23) ⋅ 点赞:(0)

在这里插入图片描述


一.
在这里插入图片描述


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


1. 前言

在这里插入图片描述

2. 开发环境准备

2.1 硬件清单

2.2 软件安装

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

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》测试例程

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);  // 保留字段1
	ret = fread(&bmpfile->header.creator2, 2, 1, fp); assert(ret == 1);  // 保留字段2
	ret = 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 20
		uint8_t sdbuffer[3*BUFFPIXEL];   // 临时像素缓冲区(一次读取20个像素)
		uint16_t *colors = (uint16_t*)malloc(sizeof(uint16_t) * w); // 一行像素转换成 RGB565
		if (colors == NULL) {
			ESP_LOGE(__FUNCTION__, "Error allocating memory for color"); // 内存不足
			free(bmpfile);
			fclose(fp);
			return 0;
		}

		// 按行读取 BMP 像素并转换为 RGB565
		for (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 file
	FontxFile fx32G[2];
	FontxFile fx32L[2];
	InitFontx(fx32G,"/fonts/ILGH32XB.FNT",""); // 16x32Dot Gothic
	InitFontx(fx32L,"/fonts/LATIN32B.FNT",""); // 16x32Dot Latin

	FontxFile fx32M[2];
	InitFontx(fx32M,"/fonts/ILMH32XB.FNT",""); // 16x32Dot Mincyo

	TFT_t dev;

	// Change SPI Clock Frequency
	//spi_clock_speed(40000000); // 40MHz
	//spi_clock_speed(60000000); // 60MHz

	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);
	char file[32];

	while(1) {
		traceHeap();
		
		strcpy(file, "/images/image.bmp");
		BMPTest(&dev, file, CONFIG_WIDTH, CONFIG_HEIGHT);
		WAIT;
		
		// Multi Font Test
		uint16_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 分区,最大同时打开文件数为 7
	ESP_ERROR_CHECK(mountSPIFFS("/fonts", "storage1", 7));
	listSPIFFS("/fonts/");

	// 挂载 /images 分区,最大同时打开文件数为 1
	ESP_ERROR_CHECK(mountSPIFFS("/images", "storage2", 1));
	listSPIFFS("/images/");

	// 挂载 /icons 分区,最大同时打开文件数为 1
	ESP_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.org
	esp_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); // 保留字段1
	ret = fread(&bmpfile->header.creator2, 2, 1, fp);
	assert(ret == 1); // 保留字段2
	ret = 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 20
		uint8_t sdbuffer[3 * BUFFPIXEL];							 // 临时像素缓冲区(一次读取20个像素)
		uint16_t *colors = (uint16_t *)malloc(sizeof(uint16_t) * w); // 一行像素转换成 RGB565
		if (colors == NULL)
		{
			ESP_LOGE(__FUNCTION__, "Error allocating memory for color"); // 内存不足
			free(bmpfile);
			fclose(fp);
			return 0;
		}

		// 按行读取 BMP 像素并转换为 RGB565
		for (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:MM
    char time_str[10];
    snprintf(time_str, sizeof(time_str), "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min);

    // 计算居中坐标
    uint16_t font_width = 32;   // 每个字符宽度,32点阵约32px
    uint16_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 分区,最大同时打开文件数为 7
	ESP_ERROR_CHECK(mountSPIFFS("/fonts", "storage1", 7));
	listSPIFFS("/fonts/");

	// 挂载 /images 分区,最大同时打开文件数为 1
	ESP_ERROR_CHECK(mountSPIFFS("/images", "storage2", 1));
	listSPIFFS("/images/");

	// 挂载 /icons 分区,最大同时打开文件数为 1
	ESP_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.org
    esp_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)
{
    // 初始化 NVS
    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()); // 擦除 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 STA
    esp_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.org
    esp_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背光");
    // 配置背光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_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_GC9A01
    ESP_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演示UI
    ui_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效果、布局等)

资料下载