
一.
https://github.com/nopnop2002/esp-idf-st7789
1. 前言

2. 开发环境准备
2.1 硬件清单
- ESP32-C3 开发板
- ST7789 1.54 寸 LCD
- 其他辅助元件(杜邦线、电源)
2.2 软件安装
软件安装,环境配置过程中肯定会遇到很多问题,我就遇到了以下几个问题,解决办法参考如下:
- 【小技巧】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"
中的屏幕分辨率没有改。
c
/**
* @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
文件:
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);
}
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"
#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 代码
bash
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
为什么会这样?
-
LVGL 自带的字体
LVGL 默认只开启了
lv_font_montserrat_14
,大多数其他大小(如 20, 28, 48)默认是 关闭的 。所以你直接用
lv_font_montserrat_48
就会报错。 -
SquareLine Studio
在 SquareLine 里选了 48 号字体,生成代码时会写
&lv_font_montserrat_48
,但如果 LVGL 配置里没启用这个字体,就会报错。
解决方法 :
在 lv_conf.h
开启 48 号字体

6. WiFi NTP 服务器获取网络时间
WiFi NTP测试程序
c
/*
* 功能:
* - 连接 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时钟)
c
/**
* @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效果、布局等)