numworks移植记录:6.移植LCD驱动——为ESP32-S3启用I8080并口模式

4 阅读8分钟

移植LCD驱动——为ESP32-S3启用I8080并口模式

在上一篇文章中,我们搭建好了开发环境。现在,我们将真正开始与硬件打交道,将NumWorks的图形输出到屏幕上。与模拟器不同,在实体硬件上,我们需要为Kandinsky图形库提供一个底层的“画布”。

本文将详细介绍如何在ESP-IDF框架下,使用Intel 8080(I80)并行接口驱动ST7789屏幕,使其作为NumWorks的显示后端。

1. I8080接口与引脚定义

与SPI相比,I8080并口最大的优势在于传输速度快,因为它使用多条数据线(通常是8条或16条)并行传输数据。ST7789支持8位或9位/16位的I8080并行接口 。

为了达到最佳性能和与原版NumWorks内存布局的兼容性,我们选择8位并口 + RGB565颜色格式。这意味着每个像素的16位颜色数据需要通过两次8位传输来完成(即写两次),但这对上层是透明的,我们只需要专注于正确配置总线即可。

ST7789在I8080模式下的主要引脚功能如下:

引脚名称功能说明连接至ESP32-S3
VCC电源3.3V供电3.3V
GND电源地GND
DB[0:7]数据总线8位双向数据总线,用于传输命令和像素数据任意8个GPIO(需连续或不连续,下文详述)
WR写时钟写使能时钟,上升沿锁存数据GPIO 12
RD读时钟读使能时钟(我们一般不用,可接高电平或悬空)NC 或 3.3V
DC数据/命令选择高电平:数据;低电平:命令GPIO 14
CS片选低电平有效,选中该设备GPIO 10
RST复位低电平复位屏幕控制器GPIO 9
BLK背光控制高电平点亮背光GPIO 46

重要提示:在I8080模式下,WR(写时钟)引脚是必不可少的,它由ESP32-S3的LCD外设控制,用于产生精确的写入时序 。DC引脚仍然用于区分命令和数据。

2. ESP32-S3 的 I80 LCD 外设

ESP32-S3内置了LCD外设(也称为I80控制器),专门用于驱动I8080并口屏幕 。它支持8位或16位总线宽度,并集成了DMA控制器,可以高效地从内存(可以是内部SRAM或外部PSRAM)中搬运帧缓冲区数据到屏幕,极大地解放了CPU 。

我们的驱动程序将基于esp_lcd组件,它提供了统一的API来操作这个外设,我们无需直接操作底层寄存器 。

3. 硬件连接示例

ESP32-S3的GPIO功能非常灵活,除了WR和部分特殊功能引脚外,数据线(DB0-DB7)可以任意分配。这里提供一个常用的参考接线:

ST7789引脚连接至ESP32-S3引脚说明
DB0GPIO 0数据位0
DB1GPIO 1数据位1
DB2GPIO 2数据位2
DB3GPIO 3数据位3
DB4GPIO 4数据位4
DB5GPIO 5数据位5
DB6GPIO 6数据位6
DB7GPIO 7数据位7
WRGPIO 12写时钟 (必须)
DCGPIO 14数据/命令选择
CSGPIO 10片选
RSTGPIO 9复位
BLKGPIO 46背光控制

4. 代码实现:在ESP-IDF中初始化I8080 LCD

以下代码基于ESP-IDF v5.x,展示了如何初始化I80总线并驱动ST7789。虽然ESP-IDF自带的ST7789驱动主要基于SPI,但其esp_lcd框架的设计允许我们通过自定义初始化命令来适配I8080模式。

我们将在main.c中实现初始化,并为后续Kandinsky的移植提供一个简单的画点测试函数。

步骤 1: 包含必要的头文件

c

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_ops.h"
#include "esp_lcd_panel_vendor.h" // 包含了 ST7789 的驱动声明
#include "driver/gpio.h"
#include "esp_err.h"
#include "soc/soc_caps.h"

步骤 2: 定义引脚和屏幕参数

c

// 使用 I80 接口,数据总线宽度为 8
#define EXAMPLE_LCD_IO_DATA0        0
#define EXAMPLE_LCD_IO_DATA1        1
#define EXAMPLE_LCD_IO_DATA2        2
#define EXAMPLE_LCD_IO_DATA3        3
#define EXAMPLE_LCD_IO_DATA4        4
#define EXAMPLE_LCD_IO_DATA5        5
#define EXAMPLE_LCD_IO_DATA6        6
#define EXAMPLE_LCD_IO_DATA7        7
#define EXAMPLE_LCD_IO_WR           12  // 写时钟
#define EXAMPLE_LCD_IO_DC            14  // 数据/命令
#define EXAMPLE_LCD_IO_CS            10  // 片选
#define EXAMPLE_LCD_IO_RST           9   // 复位
#define EXAMPLE_LCD_IO_BCKL          46  // 背光

// 屏幕分辨率
#define EXAMPLE_LCD_H_RES           320
#define EXAMPLE_LCD_V_RES           240

// 颜色深度
#define EXAMPLE_LCD_BITS_PER_PIXEL   16  // RGB565

// 背光点亮电平 (根据模块调整,多数为高电平点亮)
#define EXAMPLE_LCD_BK_LIGHT_ON_LEVEL 1

步骤 3: 编写I80总线和LCD初始化函数

c

static esp_lcd_panel_handle_t panel_handle = NULL;

void bsp_lcd_i80_init(void)
{
    esp_err_t ret = ESP_OK;

    // 1. 准备数据总线引脚数组
    int lcd_data_pins[] = {
        EXAMPLE_LCD_IO_DATA0,
        EXAMPLE_LCD_IO_DATA1,
        EXAMPLE_LCD_IO_DATA2,
        EXAMPLE_LCD_IO_DATA3,
        EXAMPLE_LCD_IO_DATA4,
        EXAMPLE_LCD_IO_DATA5,
        EXAMPLE_LCD_IO_DATA6,
        EXAMPLE_LCD_IO_DATA7,
    };

    // 2. 配置 I80 总线
    esp_lcd_i80_bus_handle_t i80_bus = NULL;
    esp_lcd_i80_bus_config_t bus_config = {
        .clk_src = LCD_CLK_SRC_PLL160M,        // 时钟源
        .dc_gpio_num = EXAMPLE_LCD_IO_DC,      // DC 引脚
        .wr_gpio_num = EXAMPLE_LCD_IO_WR,      // WR 引脚
        .data_gpio_nums = lcd_data_pins,       // 数据引脚数组
        .bus_width = 8,                         // 总线宽度
        .max_transfer_bytes = EXAMPLE_LCD_H_RES * 80 * sizeof(uint16_t), // DMA 传输的最大字节数
    };
    ret = esp_lcd_new_i80_bus(&bus_config, &i80_bus);
    ESP_ERROR_CHECK(ret);

    // 3. 创建 I80 面板 IO 句柄 (用于发送命令和数据)
    esp_lcd_panel_io_handle_t io_handle = NULL;
    esp_lcd_panel_io_i80_config_t io_config = {
        .cs_gpio_num = EXAMPLE_LCD_IO_CS,      // CS 引脚
        .pclk_hz = 20 * 1000 * 1000,            // 像素时钟 20MHz
        .trans_queue_depth = 10,                 // 事务队列深度
        .dc_levels = {
            .dc_idle_level = 0,
            .dc_cmd_level = 0,                  // 命令阶段 DC 为低
            .dc_dummy_level = 0,
            .dc_data_level = 1,                  // 数据阶段 DC 为高
        },
        .lcd_cmd_bits = 8,                       // 命令长度 8 位
        .lcd_param_bits = 8,                      // 参数长度 8 位
    };
    ret = esp_lcd_new_panel_io_i80(i80_bus, &io_config, &io_handle);
    ESP_ERROR_CHECK(ret);

    // 4. 创建 ST7789 面板实例
    esp_lcd_panel_dev_config_t panel_config = {
        .reset_gpio_num = EXAMPLE_LCD_IO_RST,   // 复位引脚
        .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, // RGB 顺序,通常 ST7789 需要设置为 BGR?可以尝试 RGB 或 BGR
        .bits_per_pixel = EXAMPLE_LCD_BITS_PER_PIXEL,
    };
    ret = esp_lcd_new_panel_st7789(io_handle, &panel_config, &panel_handle);
    ESP_ERROR_CHECK(ret);

    // 5. 复位并初始化面板
    ret = esp_lcd_panel_reset(panel_handle);
    ESP_ERROR_CHECK(ret);
    ret = esp_lcd_panel_init(panel_handle);
    ESP_ERROR_CHECK(ret);

    // 6. 配置屏幕方向和显示参数 (根据实际硬件调整)
    esp_lcd_panel_invert_color(panel_handle, true);  // 根据模块决定是否需要颜色反转
    esp_lcd_panel_swap_xy(panel_handle, false);       // 是否交换XY轴,用于旋转
    esp_lcd_panel_mirror(panel_handle, false, false); // 镜像

    // 7. 开启显示
    ret = esp_lcd_panel_disp_on_off(panel_handle, true);
    ESP_ERROR_CHECK(ret);

    // 8. 初始化并点亮背光
    gpio_config_t bl_gpio_config = {
        .pin_bit_mask = 1ULL << EXAMPLE_LCD_IO_BCKL,
        .mode = GPIO_MODE_OUTPUT,
        .pull_up_en = GPIO_PULLUP_DISABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_DISABLE,
    };
    gpio_config(&bl_gpio_config);
    gpio_set_level(EXAMPLE_LCD_IO_BCKL, EXAMPLE_LCD_BK_LIGHT_ON_LEVEL);
}

步骤 5: 测试显示

c

void lcd_i80_test(void)
{
    // 分配一个 DMA 兼容的行缓冲区 (一行像素: 320 * 2字节 = 640字节)
    uint16_t *line_buffer = heap_caps_malloc(EXAMPLE_LCD_H_RES * sizeof(uint16_t), MALLOC_CAP_DMA);
    if (!line_buffer) {
        printf("Failed to allocate line buffer\n");
        return;
    }

    // 绘制一个简单的渐变或色块
    for (int y = 0; y < EXAMPLE_LCD_V_RES; y++) {
        for (int x = 0; x < EXAMPLE_LCD_H_RES; x++) {
            // 生成一个简单的颜色模式:红色分量随x变化,绿色随y变化
            uint8_t r = (x * 255) / EXAMPLE_LCD_H_RES;
            uint8_t g = (y * 255) / EXAMPLE_LCD_V_RES;
            uint8_t b = 128;
            // 转换为 RGB565
            line_buffer[x] = ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3);
        }
        // 将一行数据通过 DMA 发送到屏幕
        esp_lcd_panel_draw_bitmap(panel_handle, 0, y, EXAMPLE_LCD_H_RES, y+1, line_buffer);
    }

    free(line_buffer);
    printf("Display test finished\n");
}

void app_main(void)
{
    bsp_lcd_i80_init();
    vTaskDelay(pdMS_TO_TICKS(100)); // 等待初始化完成
    lcd_i80_test();
}

5. 常见问题与调试

  1. 屏幕无显示或花屏
    • 检查硬件连接:特别是WR引脚的连接是否正确,数据线是否有虚焊或接错。I8080模式对时序要求较高,不稳定的连接会导致数据错误 。
    • 调整颜色反转esp_lcd_panel_invert_color()。ST7789在I8080模式下可能需要或不需要反转。
    • 检查RGB顺序rgb_ele_order 设置为 RGB 还是 BGR。如果颜色显示错乱(例如红色变成蓝色),可以尝试切换此选项。
  2. 编译错误:确保在 idf_component.yml 或 CMakeLists.txt 中正确引入了 esp_lcd 组件依赖。
  3. 性能问题:我们使用的是8位并口,每次传输一个字节。DMA (MALLOC_CAP_DMA) 是保证性能的关键。确保用于esp_lcd_panel_draw_bitmap的缓冲区是从DMA-capable内存中分配的。

现在,你的ESP32-S3已经可以通过I8080并口驱动ST7789屏幕了。下一章,我们将把Kandinsky图形库的输出重定向到这个面板句柄上,让NumWorks的UI真正跑起来!