当前位置: 首页 > news >正文

移植numworks图形计算器:7.移植LCD驱动——添加到numworks中

移植LCD驱动——添加到numworks中

在前一篇文章中,我们成功在ESP32-S3上使用I8080并口驱动了ST7789屏幕,并可以通过esp_lcdAPI进行基本的绘图操作。但要让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格式)。
  • 所有绘图操作(pushRectpushRectUniformpullRect)都直接读写这块内存缓冲。
  • 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_mainbsp_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. 注意事项
  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的事件系统,让计算器能够响应用户操作。

http://www.jsqmd.com/news/478027/

相关文章:

  • IMYAI视频创作系统部署实战:海外服务器配置与8大AI模型接入避坑指南
  • ollama部署本地大模型|embeddinggemma-300m在金融研报相似度分析应用
  • 成为管理者后,必须持续塑造更强的自己
  • 基于 51 单片机的空气浓度检测系统仿真:打造身边的空气卫士
  • ESP8684-WROOM-04C射频特性深度解析与工程落地指南
  • Linux网络驱动开发:PHY状态机与链路检测机制详解(附实战代码分析)
  • 进程资源限制与性能优化实践
  • 【生产实践】基于Docker Compose的StarRocks高可用集群部署与调优指南
  • Spring Boot 3.5.x 实战:SpringDoc 2 与 Swagger3 的深度集成与配置优化
  • 次元画室教程:锁定“风格密码”,批量产出AE可用素材(附案例)
  • 微前端架构实战:从原理到框架选型
  • C语言快速入门10-结构体
  • Cockpit实战指南:如何通过Web界面高效管理Linux服务器集群
  • 【国家级等保2.0强制要求】:MCP 2.0消息完整性校验必须满足的4项密码学硬指标(附国密SM4-GMAC审计清单)
  • Open3DSG复现实战:从数据裁剪到模型调试的完整避坑指南
  • Nunchaku FLUX.1-dev多场景落地:政府宣传图/党建素材/公益广告生成
  • 妈妈级教程:Python 全栈企业实战体系
  • 【硬核预测】AI连Verilog都能写了,FPGA工程师的铁饭碗还能端多久?
  • RK3588嵌入式Linux开发环境搭建避坑指南:从SDK解压到repo同步全流程
  • GAMES101作业7-路径追踪核心算法与性能优化全解析
  • UNIT-00:Berserk Interface 在网络安全领域的应用:威胁情报分析与代码审计
  • uniapp集成leaflet地图实战:移动端开发避坑指南
  • BeeWorks+OpenClaw=“企业专属的虾塘”:养一只听话又能干的“数字员工”
  • YOLO12模型训练全攻略:从数据标注到模型调优
  • ollama+Phi-4-mini-reasoning开源方案:可私有化部署的数学AI推理服务
  • 从零构建ArduPilot全栈仿真:Gazebo、MAVROS与QGC的协同实战
  • Fish Speech 1.5问题解决指南:WebUI无法访问、生成超时怎么办?
  • StructBERT零样本分类-中文-base精彩案例分享:10个真实业务场景分类效果对比
  • 全国路网数据深度解析:从OSM到可视化应用
  • 高通跃龙QCS9100平台上工业缺陷检测实战(2): 安装 QAIRT/QNN,并把 ONNX 跑到 HTP/NPU