numworks移植记录:7.移植LCD驱动——添加到numworks中

移植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格式)。
  • 所有绘图操作(pushRectpushRectUniformpullRect)都直接读写这块内存缓冲。
  • 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_mainbsp_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. 注意事项

  1. 色彩格式一致性 :NumWorks的KDColor使用RGB565格式,与ST7789的期望一致。但需要注意字节序:ESP-IDF的esp_lcd默认期望小端序,而KDColor的存储可能也是小端序(取决于编译器)。如果发现颜色错乱(如红蓝颠倒),可以在pushRect中转换字节序,或在初始化时通过esp_lcd_panel_swap_xy/esp_lcd_panel_mirror调整。
  2. DMA缓冲区要求 :帧缓冲必须使用MALLOC_CAP_DMA分配,以确保DMA传输正确。如果使用PSRAM,请确认你的ESP32-S3版本支持PSRAM到LCD的DMA(通常需要启用SPIRAM_CACHE_WORKAROUND等选项)。
  3. 性能优化:全屏刷新一次约需传输153600字节,在20MHz的I8080总线上耗时约7.6ms(理论值),加上CPU开销,帧率可达60fps以上。如果感觉卡顿,可以尝试降低时钟频率或启用双缓冲(但NumWorks本身不依赖双缓冲)。
  4. 多线程安全:NumWorks的绘图通常在单个线程(事件循环)中执行,因此不需要锁。但如果你的项目在多个任务中调用绘图函数,需要对帧缓冲的访问加锁。
  5. 头文件依赖 :确保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的事件系统,让计算器能够响应用户操作。

相关推荐
你家人养牛2 小时前
numworks移植记录:10.编译问题汇总与解决方案
嵌入式
你家人养牛2 小时前
numworks移植记录:11.编译问题汇总与解决方案(二)
嵌入式
序安InToo7 天前
第6课|注释与代码风格
后端·操作系统·嵌入式
济61713 天前
FreeRTOS基础--堆栈概念与汇编指令实战解析
汇编·嵌入式·freertos
嵌入小生00713 天前
线程间通信---嵌入式(Linux)
linux·c语言·vscode·嵌入式·互斥锁·线程间通信·信号量
济61713 天前
ARM Linux 驱动开发篇---GPIO子系统详解-- Ubuntu20.04
linux·嵌入式·嵌入式linux驱动开发
charlie11451419113 天前
嵌入式C++教程——Lambda捕获与性能影响
开发语言·c++·笔记·嵌入式·现代c++·工程实践
嵌入小生00714 天前
线程(2)/ 线程属性 /相关函数接口--- 嵌入式(Linux)
linux·嵌入式·线程·软件编程·僵尸线程·马年开工第一学·线程属性
序安InToo14 天前
第4课|程序结构与编译流程
后端·操作系统·嵌入式