docs: 补充核心分层中文注释

This commit is contained in:
chenjintao
2026-09-29 18:18:44 +08:00
parent a77471a7b1
commit 9fd01bb61b
81 changed files with 1189 additions and 112 deletions
+46 -4
View File
@@ -1,8 +1,8 @@
/*
* Direct ST7789P3 LCD backend.
* ST7789P3 LCD 直接驱动后端。
*
* This backend owns the low-level panel initialization, a serialized render
* loop, and a minimal presentation queue.
* 本模块独占面板底层初始化和 SPI 访问,并通过串行渲染循环处理最小化的
* presentation 队列;调用方不得绕过该任务直接操作面板。
*/
#include "lcd_direct.h"
@@ -44,14 +44,23 @@
#define LCD_DIRECT_WIDTH LCD_DIRECT_NATIVE_WIDTH
#define LCD_DIRECT_HEIGHT LCD_DIRECT_NATIVE_HEIGHT
#endif
/*
* 每次只合成并传输8行,用较小 DMA 缓冲换取更多 SPI 提交次数;100ms 是单批传输
* 故障边界。预热允许5秒是因为首次 PNG 文件读取和解码远慢于缓存命中。
*/
#define LCD_DIRECT_ROWS_PER_TRANSFER 8U
#define LCD_DIRECT_PIXELS_PER_TRANSFER (LCD_DIRECT_WIDTH * LCD_DIRECT_ROWS_PER_TRANSFER)
#define LCD_DIRECT_PRESENTATION_WAIT_POLL_MS 20U
#define LCD_DIRECT_TRANSFER_TIMEOUT_MS 100U
#define LCD_DIRECT_PREHEAT_WAIT_TIMEOUT_MS 5000U
/*
* PNG 缓存固定6项并使用 LRU 淘汰,限制 PSRAM 峰值;边界测试恰好覆盖一次完整
* 缓存命中周期。路径上限同时约束缓存键和 LittleFS 资源路径复制。
*/
#define LCD_DIRECT_FRAME_CACHE_MAX 6U
#define LCD_DIRECT_BOUNDARY_TEST_CACHE_HIT_FRAMES LCD_DIRECT_FRAME_CACHE_MAX
#define LCD_DIRECT_PATH_MAX 96U
/* 状态图标坐标基于逻辑屏幕尺寸计算,旋转屏幕时无需复制另一套绝对坐标。 */
#define LCD_DIRECT_CHARGING_ICON_WIDTH 50U
#define LCD_DIRECT_CHARGING_ICON_X (LCD_DIRECT_WIDTH - LCD_DIRECT_CHARGING_ICON_WIDTH - 8U)
#define LCD_DIRECT_CHARGING_ICON_Y 8U
@@ -59,15 +68,21 @@
#define LCD_DIRECT_LOW_BATTERY_ICON_HEIGHT 36U
#define LCD_DIRECT_LOW_BATTERY_ICON_X ((LCD_DIRECT_WIDTH - LCD_DIRECT_LOW_BATTERY_ICON_WIDTH) / 2U)
#define LCD_DIRECT_LOW_BATTERY_ICON_Y (LCD_DIRECT_HEIGHT - LCD_DIRECT_LOW_BATTERY_ICON_HEIGHT - 8U)
/* 基础表情按100ms节奏播放,有限动画末帧额外停留200ms以避免视觉跳变过快。 */
#define LCD_DIRECT_LAST_FRAME_EXTRA_HOLD_MS 200U
#define LCD_DIRECT_BASIC_FACE_FRAME_HOLD_MS 100U
#define LCD_DIRECT_BASIC_FACE_LAST_FRAME_HOLD_MS 100U
/*
* 20kHz 背光 PWM 高于常见可听频率;10位占空比分辨率提供平滑亮度变化。
* 独立背光锁最多等待100ms,防止控制路径因并发更新无限阻塞。
*/
#define LCD_DIRECT_BACKLIGHT_PWM_FREQUENCY_HZ 20000U
#define LCD_DIRECT_BACKLIGHT_PWM_DUTY_RESOLUTION LEDC_TIMER_10_BIT
#define LCD_DIRECT_BACKLIGHT_PWM_DUTY_MAX ((1U << 10U) - 1U)
#define LCD_DIRECT_BACKLIGHT_PWM_TIMER LEDC_TIMER_0
#define LCD_DIRECT_BACKLIGHT_PWM_CHANNEL LEDC_CHANNEL_0
#define LCD_DIRECT_BACKLIGHT_LOCK_TIMEOUT_MS 100U
/* 5x7 点阵放大4倍,24像素步进为字形留出间隔;颜色为 RGB565 白字黑描边。 */
#define LCD_DIRECT_CLOCK_X 12U
#define LCD_DIRECT_CLOCK_Y 10U
#define LCD_DIRECT_CLOCK_GLYPH_WIDTH 5U
@@ -79,6 +94,7 @@
#define LCD_DIRECT_CLOCK_COLOR 0xFFFFU
#define LCD_DIRECT_CLOCK_OUTLINE_COLOR 0x0000U
/* ST7789P3 的方向控制(MADCTL)和 RAM 数据格式控制(RAMCTRL)命令号。 */
#define LCD_CMD_MADCTL 0x36
#define LCD_CMD_RAMCTRL 0xB0
@@ -92,6 +108,7 @@ unsigned lodepng_esp_decode32(unsigned char **out,
const char *lodepng_esp_error_text(unsigned code);
typedef struct {
/* 请求中的外部指针在入队前复制到此结构,调用返回后不再依赖调用方内存。 */
platform_lcd_presentation_kind_t kind;
uint8_t priority;
bool interrupt_current;
@@ -107,6 +124,7 @@ typedef struct {
} lcd_direct_request_t;
typedef struct lcd_direct_decoded_image_t {
/* 缓存项拥有 pixels/alpha;pinned 项不可被 LRU 淘汰。 */
bool valid;
bool alpha_valid;
char path[LCD_DIRECT_PATH_MAX];
@@ -119,6 +137,7 @@ typedef struct lcd_direct_decoded_image_t {
} lcd_direct_decoded_image_t;
typedef struct {
/* 图标优先引用缓存,缓存不足时 owns_transient 标记本次绘制临时所有权。 */
uint32_t completed_frames;
uint64_t total_draw_us;
uint32_t min_draw_us;
@@ -200,6 +219,7 @@ static uint16_t s_line_buffer[LCD_DIRECT_PIXELS_PER_TRANSFER];
static lcd_direct_decoded_image_t s_frame_cache[LCD_DIRECT_FRAME_CACHE_MAX];
static uint32_t s_cache_use_counter;
/* 渲染状态由 s_state_lock 保护,面板和缓存访问则由 s_lock 串行化。 */
static void lcd_direct_signal_completion(void);
static void lcd_direct_clock_text(char text[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U]);
static void lcd_direct_overlay_clock(uint16_t *pixel_buffer,
@@ -211,6 +231,7 @@ static bool IRAM_ATTR lcd_direct_on_color_trans_done(esp_lcd_panel_io_handle_t p
esp_lcd_panel_io_event_data_t *event_data,
void *user_ctx)
{
/* SPI 完成回调位于中断上下文,只释放信号量并按需触发任务切换。 */
(void)panel_io;
(void)event_data;
(void)user_ctx;
@@ -233,6 +254,7 @@ static uint16_t lcd_direct_swap_color(uint16_t color)
static esp_err_t lcd_direct_draw_rows(uint16_t row_start, uint16_t row_count)
{
/* 每批行发送后等待传输完成,确保静态行缓冲不会在 DMA 使用期间被覆盖。 */
ESP_RETURN_ON_FALSE(s_panel != NULL, ESP_ERR_INVALID_STATE, TAG, "panel is not ready");
ESP_RETURN_ON_FALSE(s_transfer_done_sem != NULL,
ESP_ERR_INVALID_STATE,
@@ -296,6 +318,7 @@ static uint16_t lcd_direct_blend_rgb565(uint16_t dst, uint16_t src, uint8_t alph
static esp_err_t lcd_direct_read_file_psram(const char *path, unsigned char **out_data, size_t *out_size)
{
/* 整个 PNG 文件优先读入 PSRAM;任何短读都视为失败并关闭句柄。 */
ESP_RETURN_ON_FALSE(path != NULL && out_data != NULL && out_size != NULL,
ESP_ERR_INVALID_ARG,
TAG,
@@ -356,6 +379,7 @@ static esp_err_t lcd_direct_decode_png(const char *path,
bool keep_alpha,
lcd_direct_decoded_image_t *out_decoded)
{
/* 解码后立即转换为面板原生 RGB565,仅在确有透明像素时保留 alpha 平面。 */
ESP_RETURN_ON_FALSE(path != NULL && out_decoded != NULL,
ESP_ERR_INVALID_ARG,
TAG,
@@ -443,6 +467,7 @@ static esp_err_t lcd_direct_decode_png(const char *path,
static lcd_direct_decoded_image_t *lcd_direct_find_cached_image(const char *path, bool require_alpha)
{
/* 命中时更新单调递增使用序号,供固定容量缓存执行 LRU 淘汰。 */
if (path == NULL) {
return NULL;
}
@@ -459,6 +484,7 @@ static lcd_direct_decoded_image_t *lcd_direct_find_cached_image(const char *path
static lcd_direct_decoded_image_t *lcd_direct_find_cache_slot(void)
{
/* 优先空槽,否则淘汰最久未使用且未 pinned 的资源。 */
lcd_direct_decoded_image_t *oldest = NULL;
for (size_t i = 0; i < LCD_DIRECT_FRAME_CACHE_MAX; ++i) {
if (!s_frame_cache[i].valid) {
@@ -481,6 +507,7 @@ static esp_err_t lcd_direct_get_png_image(const char *path,
lcd_direct_decoded_image_t **out_image,
lcd_direct_decoded_image_t *transient)
{
/* 缓存已满且全部固定时,允许调用方提供 transient 承接一次性解码结果。 */
ESP_RETURN_ON_FALSE(path != NULL && out_image != NULL,
ESP_ERR_INVALID_ARG,
TAG,
@@ -579,6 +606,7 @@ static esp_err_t lcd_direct_gpio_init(void)
static esp_err_t lcd_direct_panel_init(void)
{
/* 面板命令、方向和颜色格式在显示开启前一次性配置完成。 */
spi_bus_config_t bus_config = {
.mosi_io_num = CONFIG_TQ_SCREEN_SPI_MOSI_GPIO,
.miso_io_num = CONFIG_TQ_SCREEN_SPI_MISO_GPIO,
@@ -735,6 +763,7 @@ static size_t lcd_direct_prepare_icons(const lcd_direct_icon_draw_t *icons,
lcd_direct_prepared_icon_t *prepared,
size_t prepared_capacity)
{
/* 绘制前集中解析全部图标,避免逐行合成时反复访问文件系统。 */
if (icons == NULL || icon_count == 0U || prepared == NULL || prepared_capacity == 0U) {
return 0;
}
@@ -836,6 +865,7 @@ static void lcd_direct_overlay_icons(uint16_t *pixel_buffer,
const lcd_direct_prepared_icon_t *icons,
size_t icon_count)
{
/* 只处理当前传输行范围,并依据 alpha 平面与背景执行 RGB565 混合。 */
if (pixel_buffer == NULL || icons == NULL || icon_count == 0U) {
return;
}
@@ -972,6 +1002,7 @@ static void lcd_direct_overlay_clock(uint16_t *pixel_buffer,
uint16_t row_count,
const char text[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U])
{
/* 时钟作为最终覆盖层绘制,保证 presentation 与状态图标不会遮住时间。 */
if (pixel_buffer == NULL || text == NULL) {
return;
}
@@ -996,6 +1027,7 @@ static esp_err_t lcd_direct_draw_png_image(const lcd_direct_image_t *image,
const lcd_direct_icon_draw_t *icons,
size_t icon_count)
{
/* 按固定行块合成主图、状态图标和时钟,限制单次 DMA 缓冲占用。 */
ESP_RETURN_ON_FALSE(image != NULL,
ESP_ERR_INVALID_ARG,
TAG,
@@ -1138,6 +1170,7 @@ static bool lcd_direct_preheat_is_pending(void)
static bool lcd_direct_take_preheat(lcd_direct_request_t *out_request,
uint32_t *out_generation)
{
/* 取出请求与标记 in-progress 在同一临界区完成,确保只有渲染任务消费。 */
bool has_request = false;
portENTER_CRITICAL(&s_state_lock);
if (s_preheat_pending_valid) {
@@ -1165,7 +1198,7 @@ static void lcd_direct_complete_preheat(uint32_t generation, esp_err_t result)
}
}
/* Called only by the LCD render task while it owns s_lock. */
/* 仅由持有 s_lock 的 LCD 渲染任务调用,预热不会与实际绘制并发访问缓存。 */
static bool lcd_direct_service_preheat(void)
{
lcd_direct_request_t request = {0};
@@ -1465,6 +1498,7 @@ static esp_err_t lcd_direct_run_boundary_test(const lcd_direct_request_t *reques
static esp_err_t lcd_direct_play_asset_sequence(const lcd_direct_request_t *request)
{
/* 每帧间隙检查更高优先级请求和停止标志,使长动画可以被及时抢占。 */
ESP_RETURN_ON_FALSE(request != NULL,
ESP_ERR_INVALID_ARG,
TAG,
@@ -1602,6 +1636,7 @@ static esp_err_t lcd_direct_play_basic_face_until_pending(void)
static void lcd_direct_signal_completion(void)
{
/* 完成计数是事实来源,二值信号量仅用于唤醒等待者,不承担事件计数。 */
portENTER_CRITICAL(&s_state_lock);
s_completion_count++;
portEXIT_CRITICAL(&s_state_lock);
@@ -1612,6 +1647,7 @@ static void lcd_direct_signal_completion(void)
static bool lcd_direct_take_pending(lcd_direct_request_t *out)
{
/* pending 槽采用移动语义,取出后立即清零,渲染任务独占本地副本。 */
bool has_pending = false;
portENTER_CRITICAL(&s_state_lock);
if (s_pending_valid) {
@@ -1626,6 +1662,7 @@ static bool lcd_direct_take_pending(lcd_direct_request_t *out)
esp_err_t lcd_direct_init_device(void)
{
/* 同步原语先于 GPIO/SPI 创建,任意异步完成回调到达时所需对象均已存在。 */
if (s_initialized) {
return ESP_OK;
}
@@ -1713,6 +1750,7 @@ esp_err_t lcd_direct_set_backlight_brightness(uint8_t brightness_percent)
void lcd_direct_start_render_loop(void)
{
/* 当前调用任务本身成为渲染任务,因此此函数在收到停止请求前不会返回。 */
if (!s_initialized) {
ESP_LOGW(TAG, "Direct LCD is not initialized, skip render loop");
return;
@@ -1810,6 +1848,7 @@ void lcd_direct_start_render_loop(void)
esp_err_t lcd_direct_stop_render_loop(int timeout_ms)
{
/* 先协作停止;超过边界后删除已记录任务,并取消所有预热等待者。 */
if (timeout_ms < 0) {
return ESP_ERR_INVALID_ARG;
}
@@ -1887,6 +1926,7 @@ esp_err_t lcd_direct_set_status_overlay(const platform_lcd_status_overlay_t *ove
esp_err_t lcd_direct_request_presentation(const platform_lcd_presentation_request_t *request)
{
/* 校验并深拷贝可变数组后再发布 pending,发布完成即不再引用调用方数据。 */
if (!s_initialized || request == NULL) {
return ESP_ERR_INVALID_STATE;
}
@@ -1934,6 +1974,7 @@ esp_err_t lcd_direct_request_presentation(const platform_lcd_presentation_reques
esp_err_t lcd_direct_preheat_presentation(const platform_lcd_presentation_request_t *request)
{
/* 每次只允许一个同步预热等待者,以 generation 区分迟到的完成信号。 */
if (!s_initialized || request == NULL) {
return ESP_ERR_INVALID_STATE;
}
@@ -2022,6 +2063,7 @@ uint32_t lcd_direct_presentation_completion_count(void)
esp_err_t lcd_direct_wait_presentation_completion(uint32_t since_count, uint32_t timeout_ms)
{
/* 循环检查单调计数而非相信信号量次数,可正确处理合并唤醒和先完成后等待。 */
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}