移植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
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
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
#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
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
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
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
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
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
bool Ion::Display::waitForVBlank() {
// 如果屏幕TE引脚连接到GPIO,可以在这里实现等待
// 目前简单返回true
return true;
}
3.8 实现 Context 单例
Ion::Display::Context::SharedContext是一个全局单例,需要在源文件中定义,并实现构造函数。
cpp
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
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. 注意事项
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的事件系统,让计算器能够响应用户操作。

