移植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引脚 | 说明 |
|---|---|---|
| DB0 | GPIO 0 | 数据位0 |
| DB1 | GPIO 1 | 数据位1 |
| DB2 | GPIO 2 | 数据位2 |
| DB3 | GPIO 3 | 数据位3 |
| DB4 | GPIO 4 | 数据位4 |
| DB5 | GPIO 5 | 数据位5 |
| DB6 | GPIO 6 | 数据位6 |
| DB7 | GPIO 7 | 数据位7 |
| WR | GPIO 12 | 写时钟 (必须) |
| DC | GPIO 14 | 数据/命令选择 |
| CS | GPIO 10 | 片选 |
| RST | GPIO 9 | 复位 |
| BLK | GPIO 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. 常见问题与调试
- 屏幕无显示或花屏:
- 检查硬件连接:特别是WR引脚的连接是否正确,数据线是否有虚焊或接错。I8080模式对时序要求较高,不稳定的连接会导致数据错误 。
- 调整颜色反转:
esp_lcd_panel_invert_color()。ST7789在I8080模式下可能需要或不需要反转。 - 检查RGB顺序:
rgb_ele_order设置为RGB还是BGR。如果颜色显示错乱(例如红色变成蓝色),可以尝试切换此选项。
- 编译错误:确保在
idf_component.yml或 CMakeLists.txt 中正确引入了esp_lcd组件依赖。 - 性能问题:我们使用的是8位并口,每次传输一个字节。DMA (
MALLOC_CAP_DMA) 是保证性能的关键。确保用于esp_lcd_panel_draw_bitmap的缓冲区是从DMA-capable内存中分配的。
现在,你的ESP32-S3已经可以通过I8080并口驱动ST7789屏幕了。下一章,我们将把Kandinsky图形库的输出重定向到这个面板句柄上,让NumWorks的UI真正跑起来!