移植LCD驱动------添加到numworks中
在前一篇文章中,我们成功在ESP32-S3上使用I8080并口驱动了ST7789屏幕,并可以通过esp_lcd API进行基本的绘图操作。但要让NumWorks的图形库(Kandinsky)能够使用这块屏幕,我们需要将底层的LCD驱动封装到NumWorks的硬件抽象层(Ion)中。本文将详细介绍如何将ESP32-S3的LCD驱动集成到NumWorks的Ion::Display接口中,实现上层绘图函数与底层硬件的对接。
1. 理解NumWorks的显示接口
NumWorks的图形输出最终都会调用Ion::Display命名空间下的函数,特别是Context类的三个虚函数:
cpp
arduino
void pushRect(KDRect rect, const KDColor* pixels);
void pushRectUniform(KDRect rect, KDColor color);
void pullRect(KDRect rect, KDColor* pixels);
pushRect:将一块矩形区域的像素数据(来自pixels数组)绘制到屏幕上指定的位置。pushRectUniform:用单一颜色填充整个矩形区域。pullRect:从屏幕上读取一块矩形区域的像素数据,存入pixels数组(用于窗口拖动时的内容恢复等)。
此外,还有几个辅助函数:
cpp
csharp
bool waitForVBlank(); // 等待垂直消隐(用于同步刷新)
void refreshDisplay(); // 将帧缓冲内容刷新到屏幕(通常由系统事件循环调用)
Context类本身是一个KDContext的子类,由全局单例SharedContext管理,所有绘图操作最终都会通过这个单例转发到上述三个虚函数。
因此,我们的任务就是实现这三个虚函数以及refreshDisplay,使其操作我们实际的LCD硬件。
2. 总体设计:帧缓冲方案
为了获得最佳性能和简化实现,我们采用帧缓冲(Frame Buffer)方案:
- 在内存中开辟一块与屏幕分辨率相同的缓冲区(320×240×2字节,RGB565格式)。
- 所有绘图操作(
pushRect、pushRectUniform、pullRect)都直接读写这块内存缓冲。 - 在
refreshDisplay()被调用时,将整个帧缓冲通过I8080 DMA一次性发送到LCD控制器。 - 这样既避免了频繁的小块传输,又能充分利用ESP32-S3的DMA能力,提高刷新率。
帧缓冲可以放在内部SRAM或外部PSRAM中。考虑到NumWorks的UI通常需要全屏刷新,建议使用PSRAM(如果可用),以节省宝贵的内部SRAM。
3. 实现步骤
我们将创建一个新的源文件 ion/src/esp32s3/display.cpp,在其中实现所有需要的函数。
3.1 包含必要的头文件
cpp
arduino
#include <ion/display.h>
#include "esp_lcd_panel_ops.h"
#include "esp_lcd_panel_io.h"
#include "driver/gpio.h"
#include <stdlib.h>
#include <string.h>
// 声明在板级初始化中创建的LCD面板句柄
extern esp_lcd_panel_handle_t panel_handle;
3.2 分配帧缓冲
定义一个静态指针,并在首次使用时分配内存。注意使用MALLOC_CAP_DMA标志以确保缓冲区可用于DMA传输。
cpp
less
static uint16_t* sFrameBuffer = nullptr;
static void initFrameBuffer() {
if (sFrameBuffer == nullptr) {
// 优先使用PSRAM,否则回退到内部SRAM
sFrameBuffer = (uint16_t*)heap_caps_malloc(
Ion::Display::Width * Ion::Display::Height * sizeof(uint16_t),
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT
);
if (sFrameBuffer == nullptr) {
sFrameBuffer = (uint16_t*)heap_caps_malloc(
Ion::Display::Width * Ion::Display::Height * sizeof(uint16_t),
MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT
);
}
// 初始化为黑色
memset(sFrameBuffer, 0, Ion::Display::Width * Ion::Display::Height * sizeof(uint16_t));
}
}
3.3 实现 pushRect
将传入的像素数组复制到帧缓冲的对应矩形区域。
cpp
arduino
void Ion::Display::Context::pushRect(KDRect rect, const KDColor* pixels) {
initFrameBuffer();
int x = rect.x();
int y = rect.y();
int width = rect.width();
int height = rect.height();
// 边界裁剪(NumWorks通常保证不越界,但加上安全检查更稳妥)
if (x < 0 || y < 0 || x + width > Ion::Display::Width || y + height > Ion::Display::Height) {
return;
}
uint16_t* fb_line_start = sFrameBuffer + y * Ion::Display::Width + x;
for (int row = 0; row < height; row++) {
// KDColor可以直接转换为uint16_t(内部表示为RGB565)
memcpy(fb_line_start + row * Ion::Display::Width,
pixels + row * width,
width * sizeof(uint16_t));
}
}
3.4 实现 pushRectUniform
用单一颜色填充帧缓冲的矩形区域。
cpp
ini
void Ion::Display::Context::pushRectUniform(KDRect rect, KDColor color) {
initFrameBuffer();
int x = rect.x();
int y = rect.y();
int width = rect.width();
int height = rect.height();
if (x < 0 || y < 0 || x + width > Ion::Display::Width || y + height > Ion::Display::Height) {
return;
}
uint16_t color16 = (uint16_t)color; // KDColor隐式转换为uint16_t
uint16_t* fb_line_start = sFrameBuffer + y * Ion::Display::Width + x;
for (int row = 0; row < height; row++) {
uint16_t* fb_row = fb_line_start + row * Ion::Display::Width;
for (int col = 0; col < width; col++) {
fb_row[col] = color16;
}
}
}
3.5 实现 pullRect
从帧缓冲读取像素数据到传入的数组。
cpp
arduino
void Ion::Display::Context::pullRect(KDRect rect, KDColor* pixels) {
initFrameBuffer();
int x = rect.x();
int y = rect.y();
int width = rect.width();
int height = rect.height();
if (x < 0 || y < 0 || x + width > Ion::Display::Width || y + height > Ion::Display::Height) {
return;
}
uint16_t* fb_line_start = sFrameBuffer + y * Ion::Display::Width + x;
for (int row = 0; row < height; row++) {
memcpy(pixels + row * width,
fb_line_start + row * Ion::Display::Width,
width * sizeof(uint16_t));
}
}
3.6 实现 refreshDisplay
将整个帧缓冲通过LCD面板的draw_bitmap函数发送到屏幕。
cpp
php
void Ion::Display::refreshDisplay() {
if (panel_handle == nullptr || sFrameBuffer == nullptr) {
return;
}
// 全屏刷新
esp_lcd_panel_draw_bitmap(panel_handle,
0, 0,
Ion::Display::Width, Ion::Display::Height,
sFrameBuffer);
}
如果希望提高刷新率,可以在此处实现"脏矩形追踪",只发送被修改的区域。但NumWorks的UI通常是全屏刷新或逐行刷新,全屏刷新已经足够。
3.7 实现 waitForVBlank
垂直同步等待。如果屏幕没有提供TE(Tearing Effect)引脚,可以简单返回true(表示"无需等待")。如果需要精确同步,可以配置一个GPIO中断并等待信号。
cpp
arduino
bool Ion::Display::waitForVBlank() {
// 如果屏幕TE引脚连接到GPIO,可以在这里实现等待
// 目前简单返回true
return true;
}
3.8 实现 Context 单例
Ion::Display::Context::SharedContext是一个全局单例,需要在源文件中定义,并实现构造函数。
cpp
rust
OMG::GlobalBox<Ion::Display::Context> Ion::Display::Context::SharedContext;
Ion::Display::Context::Context() : KDContext(KDPointZero, KDRect(0, 0, Ion::Display::Width, Ion::Display::Height)) {
// 构造函数中不需要额外初始化,LCD初始化应在板级启动时完成
}
// 可选的调试输出函数
void Ion::Display::Context::Putchar(char c) {
printf("%c", c); // 映射到ESP-IDF的printf
}
void Ion::Display::Context::Clear(KDPoint newCursorPosition) {
// 清屏可通过pushRectUniform实现,这里留空或调用全屏填充
}
4. 集成到系统
4.1 修改板级初始化
在ESP32-S3的板级初始化代码(通常是app_main或bsp_init)中,必须先调用LCD驱动初始化(前一篇文章中的bsp_lcd_i80_init),确保panel_handle有效。然后NumWorks的Ion层才能正常工作。
cpp
scss
extern "C" void app_main() {
// 初始化LCD (I8080并口)
bsp_lcd_i80_init();
// 初始化其他硬件:键盘、存储等...
// 进入NumWorks主循环
ion_main(0, nullptr);
}
4.2 确保帧缓冲分配时机
由于initFrameBuffer()在第一次调用绘图函数时才会执行,因此无需额外操作。但如果你希望提前分配,可以在LCD初始化后显式调用一次Ion::Display::Context::SharedContext->pushRectUniform(KDRectScreen, KDColorBlack);来触发分配。
5. 注意事项
- 色彩格式一致性 :NumWorks的
KDColor使用RGB565格式,与ST7789的期望一致。但需要注意字节序:ESP-IDF的esp_lcd默认期望小端序,而KDColor的存储可能也是小端序(取决于编译器)。如果发现颜色错乱(如红蓝颠倒),可以在pushRect中转换字节序,或在初始化时通过esp_lcd_panel_swap_xy/esp_lcd_panel_mirror调整。 - DMA缓冲区要求 :帧缓冲必须使用
MALLOC_CAP_DMA分配,以确保DMA传输正确。如果使用PSRAM,请确认你的ESP32-S3版本支持PSRAM到LCD的DMA(通常需要启用SPIRAM_CACHE_WORKAROUND等选项)。 - 性能优化:全屏刷新一次约需传输153600字节,在20MHz的I8080总线上耗时约7.6ms(理论值),加上CPU开销,帧率可达60fps以上。如果感觉卡顿,可以尝试降低时钟频率或启用双缓冲(但NumWorks本身不依赖双缓冲)。
- 多线程安全:NumWorks的绘图通常在单个线程(事件循环)中执行,因此不需要锁。但如果你的项目在多个任务中调用绘图函数,需要对帧缓冲的访问加锁。
- 头文件依赖 :确保
ion/src/esp32s3/display.cpp能够找到ESP-IDF的头文件。在CMakeLists.txt中需要添加对应的依赖路径和组件链接。
6. 测试验证
完成上述代码后,编译并烧录到ESP32-S3。如果一切正常,NumWorks的启动画面应该会显示在屏幕上。你可以通过修改apps/中的某个应用(如计算器)来测试绘图功能,例如改变背景颜色或绘制简单图形。
若屏幕无显示,请检查:
- LCD初始化是否成功(背光是否点亮,SPI/I8080时序是否正确)。
- 帧缓冲是否成功分配(可打印指针值)。
refreshDisplay是否被调用(NumWorks的系统事件循环会定期调用它)。- 色彩格式是否匹配(尝试在
pushRect中将像素数据进行字节交换)。
7. 总结
通过将LCD驱动封装到NumWorks的Ion层,我们成功地将ESP32-S3的硬件显示能力与NumWorks的图形库对接起来。现在,所有Kandinsky的绘图命令都会经过pushRect等函数更新帧缓冲,并在refreshDisplay时通过高效的DMA传输刷新到屏幕。这为后续移植键盘、存储等其他模块打下了坚实的基础。
在下一篇文章中,我们将开始处理输入部分------将ESP32-S3的GPIO按键映射到NumWorks的事件系统,让计算器能够响应用户操作。