531 lines
18 KiB
Markdown
531 lines
18 KiB
Markdown
# LCD 直绘时钟显示实现说明
|
||
|
||
## 1. 文档范围
|
||
|
||
本文整理提交 `96e975d feat(lcd): display synchronized wall clock` 的问题原因、解决方式、关键代码、验证结果和后续影响。
|
||
|
||
本次功能在不引入 LVGL 的前提下,为 320x240 LCD 增加左上角 `HH:MM` 时间显示。设备尚未完成网络校时的时候显示 `--:--`,二维码页面不显示时间。
|
||
|
||
## 2. 问题原因
|
||
|
||
### 2.1 项目没有文字渲染能力
|
||
|
||
项目当前直接通过 ESP-IDF `esp_lcd` 驱动 ST7789P3,LCD 渲染链路主要支持:
|
||
|
||
- 从应用镜像内嵌素材包读取 PNG;
|
||
- 将 PNG 解码为 RGB565;
|
||
- 按 8 行一组复制到 LCD DMA 行缓冲;
|
||
- 合成充电、低电量等状态图标;
|
||
- 通过 SPI 将行缓冲发送到屏幕。
|
||
|
||
项目没有 LVGL,也没有通用字体库,因此不能直接使用标签控件显示 `HH:MM`。如果为了五个字符引入完整 GUI 框架,会额外增加固件、RAM、任务和迁移成本。
|
||
|
||
### 2.2 校时启动依赖每日运势流程
|
||
|
||
项目原本已经具备 SNTP 校时接口,但校时启动位于每日运势任务中。每日运势任务会先读取出生资料;没有出生资料时,任务会提前等待,不会执行后面的 SNTP 启动逻辑。
|
||
|
||
因此原有逻辑不能保证设备只要联网就具备有效系统时间。LCD 时钟如果直接读取 `time()`,可能长期得到未校准的启动相对时间。
|
||
|
||
### 2.3 时间配置属于特定业务功能
|
||
|
||
原 SNTP 服务器配置名为 `FORTUNE_SNTP_SERVER`,只在每日运势功能开启时出现。墙上时钟属于设备通用能力,不应依赖运势功能是否启用。
|
||
|
||
### 2.4 内部 RAM 需要保持低占用
|
||
|
||
LCD 当前已经使用以下内部 DMA 行缓冲:
|
||
|
||
```c
|
||
#define LCD_DIRECT_ROWS_PER_TRANSFER 8U
|
||
#define LCD_DIRECT_PIXELS_PER_TRANSFER \
|
||
(LCD_DIRECT_WIDTH * LCD_DIRECT_ROWS_PER_TRANSFER)
|
||
|
||
static uint16_t s_line_buffer[LCD_DIRECT_PIXELS_PER_TRANSFER];
|
||
```
|
||
|
||
逻辑宽度为 320 像素,因此该缓冲大小为:
|
||
|
||
```text
|
||
320 x 8 x 2 = 5120 字节
|
||
```
|
||
|
||
如果再创建完整的 320x240 RGB565 framebuffer,需要额外占用 153600 字节;如果新建独立时间任务,还要增加任务栈。这两种方式都没有必要。
|
||
|
||
## 3. 解决方式
|
||
|
||
本次采用“通用联网校时 + Flash 点阵字模 + 复用 LCD 行缓冲”的方案。
|
||
|
||
完整数据流如下:
|
||
|
||
```text
|
||
Wi-Fi 获得 IP
|
||
-> 启动幂等 SNTP 客户端
|
||
-> SNTP 更新系统 wall clock
|
||
-> LCD 渲染当前 PNG 或测试图
|
||
-> 读取本地 HH:MM
|
||
-> 将点阵字符写入现有 8 行 RGB565 缓冲
|
||
-> 合成电池状态图标
|
||
-> SPI 发送到 ST7789P3
|
||
```
|
||
|
||
具体策略:
|
||
|
||
1. 新增设备通用 SNTP 服务器和 POSIX 时区配置。
|
||
2. Wi-Fi 状态任务启动时如果已经联网,立即启动校时。
|
||
3. Wi-Fi 从断开变为连接时,再次调用幂等校时启动接口。
|
||
4. 每日运势调度复用同一套通用时间配置。
|
||
5. 使用 5x7、1 bit 点阵保存 `0-9`、冒号和横线。
|
||
6. 点阵放大 4 倍,绘制白色主体和 1 像素黑色阴影。
|
||
7. 未校时或本地时间转换失败时显示 `--:--`。
|
||
8. 时间直接合成到现有行缓冲,不分配新画布。
|
||
9. 二维码页面跳过时间合成,避免影响扫码。
|
||
|
||
## 4. 具体代码
|
||
|
||
### 4.1 通用时间配置
|
||
|
||
Kconfig 增加两个通用配置:
|
||
|
||
```kconfig
|
||
config TQ_TIME_SNTP_SERVER
|
||
string "Wall-clock SNTP server"
|
||
default "ntp.aliyun.com"
|
||
help
|
||
SNTP server used to establish wall-clock time after Wi-Fi
|
||
obtains an IP address.
|
||
|
||
config TQ_TIME_POSIX_TZ
|
||
string "Wall-clock POSIX time zone"
|
||
default "CST-8"
|
||
help
|
||
POSIX TZ string used for local wall-clock display and daily
|
||
scheduling. For China Standard Time use "CST-8".
|
||
```
|
||
|
||
默认配置:
|
||
|
||
```ini
|
||
CONFIG_TQ_TIME_SNTP_SERVER="ntp.aliyun.com"
|
||
CONFIG_TQ_TIME_POSIX_TZ="CST-8"
|
||
```
|
||
|
||
代码保留编译期兜底值,避免某些旧配置环境缺少新符号时无法构建:
|
||
|
||
```c
|
||
#ifndef CONFIG_TQ_TIME_SNTP_SERVER
|
||
#define CONFIG_TQ_TIME_SNTP_SERVER "ntp.aliyun.com"
|
||
#endif
|
||
|
||
#ifndef CONFIG_TQ_TIME_POSIX_TZ
|
||
#define CONFIG_TQ_TIME_POSIX_TZ "CST-8"
|
||
#endif
|
||
```
|
||
|
||
原 `FORTUNE_SNTP_SERVER` 被移除,每日运势和 LCD 时钟统一使用 `TQ_TIME_*` 配置。
|
||
|
||
### 4.2 Wi-Fi 联网后独立启动校时
|
||
|
||
新增通用启动函数:
|
||
|
||
```c
|
||
static void control_plane_start_wall_clock_sync(void)
|
||
{
|
||
esp_err_t err = domain_runtime_time_sync_start(CONFIG_TQ_TIME_SNTP_SERVER,
|
||
CONFIG_TQ_TIME_POSIX_TZ);
|
||
if (err != ESP_OK) {
|
||
ESP_LOGW(TAG, "Start wall-clock synchronization failed: %s",
|
||
esp_err_to_name(err));
|
||
}
|
||
}
|
||
```
|
||
|
||
Wi-Fi 状态任务启动时,如果 STA 已经连接,则在启动 WebSocket 和用户配置服务之前启动校时:
|
||
|
||
```c
|
||
if (!was_connected) {
|
||
/* 保持原有离线处理。 */
|
||
} else {
|
||
control_plane_start_wall_clock_sync();
|
||
(void)control_plane_schedule_websocket_wait_task();
|
||
esp_err_t web_err = control_plane_ensure_user_config_server();
|
||
/* 保持原有错误处理。 */
|
||
}
|
||
```
|
||
|
||
运行过程中检测到 Wi-Fi 恢复时,再次启动校时:
|
||
|
||
```c
|
||
if (connected && (!was_connected || disconnected_expression_active)) {
|
||
ESP_LOGI(TAG, "Wi-Fi STA connected; restoring idle expression");
|
||
control_plane_start_wall_clock_sync();
|
||
control_plane_request_wifi_state_expression(
|
||
DOMAIN_EXPRESSION_IDLE,
|
||
DOMAIN_EXPRESSION_TRIGGER_WIFI_RECONNECTED,
|
||
"Wi-Fi reconnected");
|
||
(void)control_plane_schedule_websocket_wait_task();
|
||
}
|
||
```
|
||
|
||
底层 `domain_runtime_time_sync_start()` 最终调用幂等的 `platform_time_sync_start()`。SNTP 已经启动时,重复调用只会重新应用时区并返回成功,不会创建多个 SNTP 客户端。
|
||
|
||
每日运势等待有效时间时也改为使用通用配置:
|
||
|
||
```c
|
||
static esp_err_t control_plane_wait_for_synced_time(void)
|
||
{
|
||
esp_err_t err = domain_runtime_time_sync_start(CONFIG_TQ_TIME_SNTP_SERVER,
|
||
CONFIG_TQ_TIME_POSIX_TZ);
|
||
if (err != ESP_OK) {
|
||
return err;
|
||
}
|
||
|
||
/* 保持原有的联网状态、停止请求和超时检查。 */
|
||
}
|
||
```
|
||
|
||
### 4.3 时钟布局参数
|
||
|
||
时钟从屏幕左上角 `(12, 10)` 开始绘制。每个点阵像素放大为 4x4 实际像素,字符步进为 24 像素:
|
||
|
||
```c
|
||
#define LCD_DIRECT_CLOCK_X 12U
|
||
#define LCD_DIRECT_CLOCK_Y 10U
|
||
#define LCD_DIRECT_CLOCK_GLYPH_WIDTH 5U
|
||
#define LCD_DIRECT_CLOCK_GLYPH_HEIGHT 7U
|
||
#define LCD_DIRECT_CLOCK_GLYPH_SCALE 4U
|
||
#define LCD_DIRECT_CLOCK_GLYPH_ADVANCE 24U
|
||
#define LCD_DIRECT_CLOCK_TEXT_LENGTH 5U
|
||
#define LCD_DIRECT_CLOCK_OUTLINE_PX 1
|
||
#define LCD_DIRECT_CLOCK_COLOR 0xFFFFU
|
||
#define LCD_DIRECT_CLOCK_OUTLINE_COLOR 0x0000U
|
||
```
|
||
|
||
五个字符的实际覆盖区域约为:
|
||
|
||
```text
|
||
x: 12~128
|
||
y: 10~38
|
||
```
|
||
|
||
### 4.4 Flash 点阵字模
|
||
|
||
每行仅使用低 5 bit 表示亮点,数组声明为 `static const`,由链接器放入只读区:
|
||
|
||
```c
|
||
static const uint8_t s_clock_glyph_rows[][LCD_DIRECT_CLOCK_GLYPH_HEIGHT] = {
|
||
{0x0E, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0E}, /* 0 */
|
||
{0x04, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x0E}, /* 1 */
|
||
{0x0E, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1F}, /* 2 */
|
||
{0x1E, 0x01, 0x01, 0x0E, 0x01, 0x01, 0x1E}, /* 3 */
|
||
{0x02, 0x06, 0x0A, 0x12, 0x1F, 0x02, 0x02}, /* 4 */
|
||
{0x1F, 0x10, 0x10, 0x1E, 0x01, 0x01, 0x1E}, /* 5 */
|
||
{0x0E, 0x10, 0x10, 0x1E, 0x11, 0x11, 0x0E}, /* 6 */
|
||
{0x1F, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08}, /* 7 */
|
||
{0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E}, /* 8 */
|
||
{0x0E, 0x11, 0x11, 0x0F, 0x01, 0x01, 0x0E}, /* 9 */
|
||
{0x00, 0x04, 0x04, 0x00, 0x04, 0x04, 0x00}, /* : */
|
||
{0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00}, /* - */
|
||
};
|
||
```
|
||
|
||
整个数组为 12x7,即 84 字节。横线用于组成未校时状态 `--:--`。
|
||
|
||
字符到字模索引的映射:
|
||
|
||
```c
|
||
static size_t lcd_direct_clock_glyph_index(char character)
|
||
{
|
||
if (character >= '0' && character <= '9') {
|
||
return (size_t)(character - '0');
|
||
}
|
||
return character == ':' ? 10U : 11U;
|
||
}
|
||
```
|
||
|
||
### 4.5 获取显示文本
|
||
|
||
```c
|
||
static void lcd_direct_clock_text(char text[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U])
|
||
{
|
||
if (!platform_time_is_synced()) {
|
||
memcpy(text, "--:--", LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U);
|
||
return;
|
||
}
|
||
|
||
time_t now = time(NULL);
|
||
struct tm local_tm = {0};
|
||
if (localtime_r(&now, &local_tm) == NULL) {
|
||
memcpy(text, "--:--", LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U);
|
||
return;
|
||
}
|
||
|
||
text[0] = (char)('0' + local_tm.tm_hour / 10);
|
||
text[1] = (char)('0' + local_tm.tm_hour % 10);
|
||
text[2] = ':';
|
||
text[3] = (char)('0' + local_tm.tm_min / 10);
|
||
text[4] = (char)('0' + local_tm.tm_min % 10);
|
||
text[5] = '\0';
|
||
}
|
||
```
|
||
|
||
这里直接生成固定长度字符串,没有使用动态内存,也没有引入通用格式化缓冲。
|
||
|
||
### 4.6 将字符绘制到现有行缓冲
|
||
|
||
绘制函数只处理与当前 `row_start~row_start+row_count` 相交的点阵行:
|
||
|
||
```c
|
||
static void lcd_direct_draw_clock_pass(
|
||
uint16_t *pixel_buffer,
|
||
uint16_t row_start,
|
||
uint16_t row_count,
|
||
const char text[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U],
|
||
int x_offset,
|
||
int y_offset,
|
||
uint16_t color)
|
||
{
|
||
const int row_end = (int)row_start + (int)row_count;
|
||
|
||
for (size_t character_index = 0;
|
||
character_index < LCD_DIRECT_CLOCK_TEXT_LENGTH;
|
||
++character_index) {
|
||
const uint8_t *glyph =
|
||
s_clock_glyph_rows[
|
||
lcd_direct_clock_glyph_index(text[character_index])];
|
||
const int glyph_x = (int)LCD_DIRECT_CLOCK_X +
|
||
(int)(character_index *
|
||
LCD_DIRECT_CLOCK_GLYPH_ADVANCE) +
|
||
x_offset;
|
||
const int glyph_y = (int)LCD_DIRECT_CLOCK_Y + y_offset;
|
||
|
||
for (uint16_t source_y = 0;
|
||
source_y < LCD_DIRECT_CLOCK_GLYPH_HEIGHT;
|
||
++source_y) {
|
||
for (uint16_t scale_y = 0;
|
||
scale_y < LCD_DIRECT_CLOCK_GLYPH_SCALE;
|
||
++scale_y) {
|
||
const int destination_y = glyph_y +
|
||
(int)(source_y * LCD_DIRECT_CLOCK_GLYPH_SCALE + scale_y);
|
||
if (destination_y < (int)row_start || destination_y >= row_end) {
|
||
continue;
|
||
}
|
||
|
||
for (uint16_t source_x = 0;
|
||
source_x < LCD_DIRECT_CLOCK_GLYPH_WIDTH;
|
||
++source_x) {
|
||
const uint8_t mask =
|
||
1U << (LCD_DIRECT_CLOCK_GLYPH_WIDTH - 1U - source_x);
|
||
if ((glyph[source_y] & mask) == 0U) {
|
||
continue;
|
||
}
|
||
|
||
for (uint16_t scale_x = 0;
|
||
scale_x < LCD_DIRECT_CLOCK_GLYPH_SCALE;
|
||
++scale_x) {
|
||
const int destination_x = glyph_x +
|
||
(int)(source_x * LCD_DIRECT_CLOCK_GLYPH_SCALE +
|
||
scale_x);
|
||
if (destination_x < 0 ||
|
||
destination_x >= (int)LCD_DIRECT_WIDTH) {
|
||
continue;
|
||
}
|
||
|
||
const uint32_t destination_index =
|
||
(uint32_t)(destination_y - (int)row_start) *
|
||
LCD_DIRECT_WIDTH +
|
||
(uint32_t)destination_x;
|
||
pixel_buffer[destination_index] =
|
||
lcd_direct_swap_color(color);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
时钟分两次绘制,先画右下偏移 1 像素的黑色阴影,再画白色主体:
|
||
|
||
```c
|
||
static void lcd_direct_overlay_clock(
|
||
uint16_t *pixel_buffer,
|
||
uint16_t row_start,
|
||
uint16_t row_count,
|
||
const char text[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U])
|
||
{
|
||
if (pixel_buffer == NULL || text == NULL) {
|
||
return;
|
||
}
|
||
|
||
lcd_direct_draw_clock_pass(pixel_buffer,
|
||
row_start,
|
||
row_count,
|
||
text,
|
||
LCD_DIRECT_CLOCK_OUTLINE_PX,
|
||
LCD_DIRECT_CLOCK_OUTLINE_PX,
|
||
LCD_DIRECT_CLOCK_OUTLINE_COLOR);
|
||
lcd_direct_draw_clock_pass(pixel_buffer,
|
||
row_start,
|
||
row_count,
|
||
text,
|
||
0,
|
||
0,
|
||
LCD_DIRECT_CLOCK_COLOR);
|
||
}
|
||
```
|
||
|
||
### 4.7 合成到 PNG 和测试图
|
||
|
||
绘制 PNG 前生成一次当前帧的时钟文本;二维码路径直接关闭时钟:
|
||
|
||
```c
|
||
const bool clock_visible = strstr(image->path, "/qrcode.png") == NULL;
|
||
char clock_text[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U] = {0};
|
||
if (clock_visible) {
|
||
lcd_direct_clock_text(clock_text);
|
||
}
|
||
```
|
||
|
||
每组 8 行完成背景复制后,先合成时钟,再合成状态图标,最后发送 LCD:
|
||
|
||
```c
|
||
if (clock_visible) {
|
||
lcd_direct_overlay_clock(s_line_buffer,
|
||
row_start,
|
||
row_count,
|
||
clock_text);
|
||
}
|
||
lcd_direct_overlay_icons(s_line_buffer,
|
||
row_start,
|
||
row_count,
|
||
prepared_icons,
|
||
prepared_icon_count);
|
||
err = lcd_direct_draw_rows(row_start, row_count);
|
||
```
|
||
|
||
测试图和转轮占位画面也使用同一个时钟合成函数:
|
||
|
||
```c
|
||
char clock_text[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U] = {0};
|
||
lcd_direct_clock_text(clock_text);
|
||
|
||
for (uint16_t row_start = 0;
|
||
row_start < LCD_DIRECT_HEIGHT;
|
||
row_start += LCD_DIRECT_ROWS_PER_TRANSFER) {
|
||
/* 生成当前测试图的 8 行背景。 */
|
||
lcd_direct_overlay_clock(s_line_buffer,
|
||
row_start,
|
||
row_count,
|
||
clock_text);
|
||
ESP_RETURN_ON_ERROR(lcd_direct_draw_rows(row_start, row_count),
|
||
TAG,
|
||
"write pixels failed");
|
||
}
|
||
```
|
||
|
||
## 5. 解决成果
|
||
|
||
### 5.1 功能成果
|
||
|
||
| 场景 | 最终行为 |
|
||
| --- | --- |
|
||
| 启动后尚未校时 | 左上角显示 `--:--` |
|
||
| Wi-Fi 已连接 | 自动启动 SNTP,不要求存在出生资料 |
|
||
| SNTP 校时成功 | 显示本地 24 小时制 `HH:MM` |
|
||
| Wi-Fi 临时断开 | 系统时钟继续运行,保留时间显示 |
|
||
| Wi-Fi 重连 | 幂等调用校时启动接口 |
|
||
| 普通 PNG 表情 | 合成时间和原有状态图标 |
|
||
| 测试图/转轮占位图 | 同样显示时间 |
|
||
| 二维码页面 | 隐藏时间,避免遮挡二维码 |
|
||
|
||
### 5.2 内存成果
|
||
|
||
- 没有新增动态内存分配。
|
||
- 没有新增 framebuffer。
|
||
- 没有新增 FreeRTOS 任务、定时器或任务栈。
|
||
- 继续复用原有 5120 字节 LCD DMA 行缓冲。
|
||
- 点阵字模为 84 字节,并经目标文件符号检查确认位于只读区。
|
||
- 每次绘制只增加 6 字节时间字符串、`struct tm` 和少量局部变量,使用现有 LCD 渲染任务栈。
|
||
- 新增 PSRAM 占用为 0。
|
||
|
||
### 5.3 构建成果
|
||
|
||
使用 ESP-IDF 5.5.4 执行重新配置和完整构建,结果成功:
|
||
|
||
```text
|
||
TQ_Cat_Code.bin binary size 0x214200 bytes
|
||
Smallest app partition is 0x380000 bytes
|
||
0x16be00 bytes (41%) free
|
||
```
|
||
|
||
新配置成功进入生成配置:
|
||
|
||
```c
|
||
#define CONFIG_TQ_TIME_SNTP_SERVER "ntp.aliyun.com"
|
||
#define CONFIG_TQ_TIME_POSIX_TZ "CST-8"
|
||
```
|
||
|
||
## 6. 后续影响与注意事项
|
||
|
||
### 6.1 每日运势配置发生迁移
|
||
|
||
原配置:
|
||
|
||
```ini
|
||
CONFIG_FORTUNE_SNTP_SERVER="ntp.aliyun.com"
|
||
```
|
||
|
||
已经改为:
|
||
|
||
```ini
|
||
CONFIG_TQ_TIME_SNTP_SERVER="ntp.aliyun.com"
|
||
CONFIG_TQ_TIME_POSIX_TZ="CST-8"
|
||
```
|
||
|
||
如果外部构建脚本、私有 `sdkconfig.defaults` 或生产配置仍设置 `CONFIG_FORTUNE_SNTP_SERVER`,需要迁移到新名称。每日运势请求中发送给服务器的 IANA 时区 `CONFIG_FORTUNE_TIMEZONE="Asia/Shanghai"` 没有改变。
|
||
|
||
### 6.2 SNTP 会更早产生一次网络请求
|
||
|
||
校时现在在 Wi-Fi 可用后立即启动,不再等待出生资料。这会在联网阶段增加正常的 SNTP UDP 请求,但不会增加常驻工作任务,也不会重复创建 SNTP 客户端。
|
||
|
||
### 6.3 POSIX 时区是进程级全局状态
|
||
|
||
`platform_time_sync_start()` 使用 `setenv("TZ", ...)` 和 `tzset()` 设置进程时区。LCD 时钟和每日运势的本地午夜计算会共享同一时区,这是当前统一配置的预期行为。
|
||
|
||
如果未来设备需要同时处理多个用户时区,不能反复修改进程级 `TZ`;届时需要引入显式时区转换方案。
|
||
|
||
### 6.4 没有外置 RTC 的断电限制
|
||
|
||
设备正常运行期间,即使 Wi-Fi 断开,系统时钟仍会继续走。但是彻底断电后,如果开机时无法联网,设备不能恢复准确绝对时间,会继续显示 `--:--`。
|
||
|
||
如果产品要求断电、无网启动后立即显示准确时间,需要增加带后备电池的外置 RTC,例如 DS3231。
|
||
|
||
### 6.5 时间刷新依赖画面重绘
|
||
|
||
当前基础表情和动画会持续重绘,因此分钟变化会在后续帧中反映出来。当前没有单独创建每分钟刷新定时器。
|
||
|
||
如果未来增加“绘制一次后长期不再刷新的静态画面”,需要在分钟变化时通知 LCD 渲染任务重新绘制当前帧,否则该静态画面上的时间不会自动变化。
|
||
|
||
### 6.6 每帧会读取一次本地时间
|
||
|
||
每个完整画面生成一次 `HH:MM` 文本,不会在每组 8 行中重复调用 `localtime_r()`。现有刷新频率下开销较低。
|
||
|
||
如果后续提高 LCD 帧率或增加更多文字,可缓存当前分钟,仅在分钟变化时重新转换本地时间,以进一步减少 CPU 开销。
|
||
|
||
### 6.7 UI 仍需实机确认
|
||
|
||
当前时钟使用白色像素字和右下黑色阴影,覆盖范围约为 `117x29` 像素。二维码已经排除,但其他业务图片仍会显示时间。
|
||
|
||
后续硬件验证应确认:
|
||
|
||
1. 基础表情下的可读性;
|
||
2. 浅色背景上的黑色阴影是否足够;
|
||
3. 是否遮挡五行、抽签、OTA 或财位画面的重要内容;
|
||
4. 左上角位置和字号是否符合最终 UI 设计;
|
||
5. 充电图标、低电量图标与时间是否同时正常显示。
|
||
|
||
## 7. 结论
|
||
|
||
本次实现复用了已有 SNTP 和 LCD 直绘架构,用 84 字节点阵字模解决了无 LVGL 环境下的时间显示问题。方案没有新增动态内存、PSRAM、任务或 framebuffer,并将校时从每日运势业务中提升为联网后的通用设备能力。
|
||
|
||
当前软件构建验证已经完成;最终上线前仍需要烧录设备,对显示位置、背景对比度、分钟跳变和断网重连场景进行实机验证。
|