LCD 驱动与字库引擎
本页介绍 AC82N SDK 中 TFT 彩屏的 LCD 驱动框架(lcd_drive)与 Unicode 点阵字库引擎(font_unicode),涵盖面板适配、SPI/PAP 推屏、DMA 中断同步、字库文件格式、页索引查找与文本排版输出的完整机制。
Purpose and Scope
本页覆盖两块紧密协作的显示能力:
- LCD 驱动层:
sdk/apps/common/lcd/下的lcd_drive.c/h与各面板驱动(ST7735、ST7789 系列),提供屏幕初始化、画布管理、绘图原语与 SPI DMA 推屏通道。 - 字库引擎:
sdk/apps/common/font/下的font_textout.h、font_unicode.c,提供从 SD 卡字库文件读取 Unicode 点阵位图、按文本排版输出到 LCD 的能力。
以下相关内容属于兄弟主题,不在本页展开:
- 段码屏(segment code LCD)驱动:见
sdk/cpu/cd09/segment_code_lcd_drv.c(对应ui_devices_type中的LED_7/LCD_SEG3X9)。 - 字库文件的生成工具(
FontTool.exe)与字库 XML 工程文件:见doc/软件工具/unicode字库工具/。 - 上层 UI 框架与触摸屏处理:由独立 UI 页面负责。
Overview
在 AC82N 这类 MCU 方案中,LCD 显示链路通常为:应用代码 → 字库引擎(将字符串解析为位图)→ LCD 驱动(将位图写入显存/直接推屏)→ SPI/PAP 硬件(DMA 搬运)→ 屏幕面板。
设计动机
- 面板多样性:同一套 SDK 需支持 128×128、80×160、240×240、240×320 等多种分辨率与 ST7735/ST7789 不同控制 IC。设计上用
struct spi_lcd_init抽象出面板差异(初始化序列、命令/数据写函数、绘图区域设置、背光控制等),配合REGISTER_LCD_DRIVE()宏让每个面板 .c 文件声明一个lcd_drive实例即可接入框架。 - 推屏方式可选:
LCD_PUSH_MODE支持SPI(硬件 SPI + DMA)与PAP(并行/专用屏接口)两种通道,编译期选择,避免运行时开销。 - 资源受限:MCU 内存有限,采用"分块推屏"(
lcd_clear_screen按行分块)而非整屏缓冲;字库数据不常驻内存,按需从 SD 卡随机读取(页索引 + 位图数据两级定位)。 - 异步不阻塞:SPI 发送走 DMA + 中断(
spi_isr),is_lcd_busy标志协调"设置绘图区域 → 写数据 → DMA 完成"的时序,避免屏幕撕裂与数据覆盖。
关键概念
| 概念 | 说明 |
|---|---|
spi_lcd_init | 面板驱动结构体,函数指针集合(Init/WriteComm/WriteData/WriteMap/SetDrawArea/reset/BackLightCtrl/EnterSleep/ExitSleep) |
InitCode | 面板初始化命令序列(cmd + cnt + dat[64]),REGFLAG_DELAY(0xFF) 表示延时 |
LCD_PUSH_MODE | 推屏通道选择:SPI(0) 或 PAP(1) |
font_info | 字库运行上下文:像素字库文件句柄、字号、位图缓冲、排版标志与文本尺寸 |
Font_PageEntry_t | 字库文件页索引(charIndex + codesMask 位图),实现 O(1) 级字符定位 |
FONT_SHOW_PIXEL / FONT_SHOW_MULTI_LINE | 排版标志:输出位图到屏幕 / 允许多行自动换行 |
Architecture
下图展示了从应用层到屏幕面板的完整显示链路,以及字库引擎在其中的位置:
flowchart TD
subgraph sg_App["应用层"]
App["应用代码 / 测试函数"]
end
subgraph sg_Font["字库引擎 (font_unicode.c)"]
FontAPI["font_unicode_open / TextOutW_Unicode"]
CharIdx["Font_GetCharIndex()"]
CharBits["Font_GetCharBits()"]
PixBuf["font_copy_pixbuf()"]
end
subgraph sg_Lcd["LCD 驱动层 (lcd_drive.c)"]
DrvInit["lcd_driver_init()"]
DrawArea["lcd_set_draw_area()"]
LcdDraw["lcd_draw()"]
Prims["lcd_drawpoint / lcd_draw_line / lcd_fill_rect"]
Dma["spi_dma_send_map()"]
end
subgraph sg_Panel["面板驱动 (lcd_*.c)"]
Panel["struct spi_lcd_init lcd_drive"]
InitSeq["InitCode 初始化序列"]
end
subgraph sg_Hw["硬件层"]
Spi["SPI1 + DMA"]
Screen["LCD 屏幕"]
FontFile["SD 卡字库文件"]
end
App --> DrvInit
App --> FontAPI
FontAPI --> CharIdx
CharIdx --> CharBits
CharBits --> PixBuf
PixBuf --> LcdDraw
LcdDraw --> DrawArea
LcdDraw --> Dma
DrvInit --> Panel
Panel --> InitSeq
Panel --> Spi
Spi --> Screen
CharBits --> FontFile
Prims --> LcdDraw
各组件职责:
- 应用层:调用
lcd_driver_init()完成屏幕初始化,或调用font_unicode_open()/TextOutW_Unicode()将字符串渲染到屏幕;push_screen_test()与font_unicode_test()是 SDK 提供的自测入口(见 lcd_drive.h)。 - 字库引擎:解析 UTF-16 字符串,通过页索引在 SD 卡字库文件中定位每个字符的点阵位图,把位图经
font_copy_pixbuf()送交 LCD 驱动。 - LCD 驱动层:屏蔽面板差异,提供
lcd_set_draw_area()、lcd_draw()、lcd_clear_screen()等高层接口;内部通过spi_dma_send_map()走 DMA 通道,由spi_isr()中断同步完成状态。 - 面板驱动:每个面板 .c 文件声明一个
lcd_drive实例(通过REGISTER_LCD_DRIVE()宏),包含初始化命令表(InitCode数组)与底层读写函数指针。 - 硬件层:
HW_SPI1(默认)承担推屏总线,RES/CS/RS/BL 走普通 GPIO;字库文件存放在 SD 卡(sdfile_open)。
面板驱动接口(类图)
struct spi_lcd_init 是面板适配的核心契约,面板 .c 文件只需填充该结构并导出为全局符号 lcd_drive:
classDiagram
class spi_lcd_init {
+char *name
+u8 spi_pending
+u8 soft_spi
+u16 lcd_width
+u16 lcd_height
+u8 color_format
+u8 interface
+u8 column_addr_align
+u8 row_addr_align
+u8 backlight_status
+char *dispbuf
+u32 bufsize
+InitCode *initcode
+u16 initcode_cnt
+void Init()
+void WriteComm(u8 cmd)
+void WriteData(u8 dat)
+void WriteMap(char *map, u32 size)
+void SetDrawArea(int xs, int xe, int ys, int ye)
+void reset()
+void BackLightCtrl(u8)
+void EnterSleep()
+void ExitSleep()
}
字段说明:lcd_width/lcd_height 为面板物理分辨率;color_format 取自 LCD_COLOR(RGB565/MONO);interface 取自 LCD_IF(SPI/EMI);initcode/initcode_cnt 为初始化命令表;函数指针是驱动层与面板的唯一交互通道。框架通过 REGISTER_LCD_DRIVE() 宏声明 struct spi_lcd_init lcd_drive,并以 extern 方式引用(见 lcd_drive.h)。
LCD 驱动层深入
编译期配置与通道选择
lcd_drive.h 顶部通过一组宏在编译期完成推屏通道与面板选择(见 lcd_drive.h):
LCD_PUSH_MODE:SPI(0) 走硬件 SPI,PAP(1) 走 PAP 专用接口。选择 SPI 时自动启用TCFG_TFT_LCD_DEV_SPI_HW_NUM(默认 1)与LCD_SPI_INTERRUPT_ENABLE;选择 PAP 时两者强制为 0。TCFG_LCD_9BIT_SPI_ENABLE:9-bit SPI(数据/命令附加位由硬件 SPI CON1 寄存器 BIT4 控制,用于如lcd_spi_st7789_BOE1.54_update_240x240这类屏)。SPI_MODULE_CHOOSE:选择 SPI0/1/2 作为推屏通道,并据此映射中断向量IRQ_SPI_IDX(IRQ_SPI0_IDX/IRQ_SPI1_IDX/IRQ_SPI2_IDX)。注释明确提示 SPI0 通常被外部 flash 占用,一般不用。- 面板使能开关:
TCFG_LCD_SPI_ST7735V_128X128_ENABLE、TCFG_LCD_SPI_ST7735V_80X160_ENABLE、TCFG_LCD_SPI_ST7789V_ENABLE、TCFG_LCD_PAP_SPI_ST7789V_240X240_ENABLE、TCFG_LCD_PAP_SPI_ST7789V_240X320_ENABLE,对应sdk/apps/common/lcd/下各面板 .c 文件。
这种"编译期剪裁"的意图:MCU 方案 ROM/RAM 紧张,把不用的面板与中断逻辑直接裁剪掉,避免运行时选择带来的代码体积与功耗开销。
平台数据与硬件绑定
lcd_drive.c 中定义了默认 SPI 通道的硬件信息与引脚绑定(见 lcd_drive.c):
#if (LCD_PUSH_MODE == SPI)
static spi_hardware_info spi1_p_data = {
.port = {
IO_PORTC_01, //clk any io
IO_PORTC_00, //do any io
-1, //di any io
-1,
-1,
-1,
},
.role = SPI_ROLE_MASTER,
.mode = SPI_MODE_BIDIR_1BIT,//SPI_MODE_UNIDIR_2BIT,//SPI_MODE_BIDIR_1BIT,
.bit_mode = SPI_FIRST_BIT_MSB,
.cpol = 0,//clk level in idle state:0:low, 1:high
.cpha = 0,//sampling edge:0:first, 1:second
.ie_en = 0, //ie enbale:0:disable, 1:enable
.spi_isr_callback = NULL, //spi isr callback
.clk = 2000000L,
};
const static struct lcd_spi_platform_data lcd_spi_data = {
.pin_reset = IO_PORTC_11,
.pin_rs = IO_PORTC_02,
.pin_cs = IO_PORTD_02,
.pin_bl = IO_PORTA_08,
.spi_cfg = HW_SPI1,
.spi_pdata = &spi1_p_data,
};
#elif (LCD_PUSH_MODE == PAP)
...
#endif
要点:
- SPI 采用单线双向模式(
SPI_MODE_BIDIR_1BIT)、MSB 优先、CPOL/CPHA 均为 0(空闲低电平、首沿采样),时钟默认 2 MHz —— 这是兼容绝大多数 ST77xx 面板的保守配置,需要提速时可在此调整。 lcd_spi_platform_data将 RES/RS/CS/BL 四个控制脚与 SPI 实例绑定;PAP 模式复用同一结构(引脚不同),并注明"触摸屏和屏公用复位,先初始化触摸"的时序约束。ui_devices_cfg将整块 LCD 注册为TFT_LCD类型设备,private_data指向lcd_spi_data,供上层 UI 系统按类型分派。
初始化流程
lcd_driver_init() 的职责链:spi_init() 调用 spi_open() 打开 SPI 外设,并在 LCD_SPI_INTERRUPT_ENABLE 下 request_irq(IRQ_SPI_IDX, 3, spi_isr, 0) 注册中断(见 lcd_drive.c);随后调用面板的 lcd_drive.Init() 依次执行 InitCode 初始化序列(其中 REGFLAG_DELAY 表示插入延时)。初始化完成后,屏幕处于可绘制状态,背光由 lcd_backlight_ctrl() 控制。
SPI 中断与 DMA 同步机制
这是本驱动最关键的并发设计(见 lcd_drive.c):
static void spi_dma_wait_finish()
{
if (spi_pnd) {
while (!spi_get_pending(spi_dat->spi_cfg)) {
}
spi_clear_pending(spi_dat->spi_cfg);
spi_pnd = false;
}
}
static int __spi_dma_send(spi_dev spi, void *buf, u32 len, u8 wait)
{
int err = 0;
if (!wait || spi_pnd) {
spi_dma_wait_finish();
}
spi_dma_transmit_for_isr(spi_dat->spi_cfg, buf, len, 0);
spi_pnd = true;
asm("csync");
if (wait) {
spi_dma_wait_finish();
}
return err;
}
spi_pnd(静态标志)记录"是否有 DMA 发送尚未完成";spi_dma_wait_finish()通过轮询spi_get_pending()等待上次发送完成,再清标志。asm("csync")是 CPU 指令屏障,保证 DMA 配置写入在启动发送前可见。- 中断路径:
spi_isr()在 DMA 完成中断到来时清除is_lcd_busy并关闭 SPI 中断使能(spi_set_ie(..., 0))。注释说明 DMA 模式在发送时内部已清理中断 pending。 - 这种"轮询等待 + 中断复位忙标志"组合的意图:推屏是周期性大批量操作,DMA 启动后 CPU 可继续执行其他任务,仅在关键切换点同步,从而降低帧推送的 CPU 占用。
绘图原语与画布操作
驱动对外提供统一的绘图 API(声明见 lcd_drive.h):
| 函数 | 作用 |
|---|---|
lcd_driver_init / lcd_driver_deinit | 屏幕初始化 / 反初始化 |
lcd_set_draw_area(xs, xe, ys, ye) | 设置绘图窗口,内部设 is_lcd_busy=1 并调用面板 SetDrawArea |
lcd_draw(buf, len, wait) | 将 len 字节写入屏幕;wait=1 时开启 SPI 中断等待,否则立即返回 |
lcd_clear_screen(color) | 按行分块清屏(避免一次性大缓冲) |
lcd_fill_rect / lcd_drawpoint / lcd_draw_line | 矩形填充 / 画点 / 画线 |
lcd_backlight_ctrl(on) | 背光开关 |
lcd_get/set_screen_width/height | 屏幕分辨率读写 |
lcd_st7735_set_direction(dir) | 旋转方向(LCD_DIRECTION 枚举:0°/90°/180°/270°) |
lcd_draw() 的实现体现了忙状态保护(见 lcd_drive.c):
int lcd_draw(char *buf, u32 len, u8 wait)
{
if ((is_lcd_busy == 0x11) || lcd_sleep_in) {
return 0;
}
if (wait) {
__this->WriteMap(buf, len);
#if LCD_SPI_INTERRUPT_ENABLE
spi_set_ie(spi_dat->spi_cfg, 1);
#else
is_lcd_busy = 0;
#endif
} else {
__this->WriteMap(buf, len);
is_lcd_busy = 0;
}
#if (LCD_PUSH_MODE == SPI)
spi_dma_wait_finish();
#endif
return 0;
}
设计意图:is_lcd_busy == 0x11(休眠/异常态)或 lcd_sleep_in(睡眠中)时直接丢弃绘制请求,防止向未就绪的控制器写数据;wait 模式依赖中断复位忙标志实现"写完后可被下一帧覆盖"的流水线化。
核心流程:从字符串到屏幕像素
sequenceDiagram
participant App as 应用层
participant Font as 字库引擎
participant SD as SD 卡
participant Lcd as lcd_drive
participant SPI as SPI1 DMA
participant Panel as 面板驱动
App->>Font: font_unicode_open(info)
Font->>SD: sdfile_open(字库文件, "r")
SD-->>Font: FILE*
Font-->>App: info (含 pixelbuf)
App->>Font: TextOutW_Unicode(info, str, len)
loop 每个 UTF-16 字符
Font->>SD: Font_GetCharIndex(unicode) 页索引定位
Font->>SD: Font_GetCharBits(unicode) 读取位图
SD-->>Font: pixelbuf + 宽度
Font->>Lcd: font_copy_pixbuf → lcd_draw(buf)
Lcd->>Panel: SetDrawArea / WriteMap
Panel->>SPI: spi_dma_transmit_for_isr
SPI-->>Panel: DMA 完成(中断复位 is_lcd_busy)
end
Font-->>App: 返回已处理字符数 i
时序要点:
- 字库打开阶段只做一次文件打开与
"unic"magic 校验,不加载整库到内存。 - 每个字符经历"页索引查询 → 位图读取 → 排版坐标计算 → 推屏"四步;
TextOutW_Unicode以 2 字节为步长解析 UTF-16(受bigendian标志控制字节序)。 - 推屏阶段
lcd_set_draw_area先锁定忙标志,lcd_draw送数据后由 SPI 中断复位,保证下一字符/下一帧不会与未完成的 DMA 竞争。
字库引擎深入
字库文件格式
font_unicode.c 定义了字库文件的三大区域(见 font_unicode.c):
struct font_header {
u8 magic[4];
u32 version;
u32 timestamp;
u8 fontsize;
u8 reserved[19];
};
typedef struct {
u8 width;
u8 height;
s8 left;
s8 top;
u32 baseaddr;
} __attribute__((packed, aligned(1))) UnicInfo;
#define Font_NumPages 1024
#define Font_PageSize (0x10000 / Font_NumPages)
typedef struct {
u16 charIndex;
u8 codesMask[Font_PageSize / 8];
} Font_PageEntry_t;
文件布局:文件头(struct font_header,magic 固定为 "unic")→ 1024 条页索引(Font_PageEntry_t)→ 每字符一条 UnicInfo(含位图基地址 baseaddr)→ 点阵位图数据区。
设计意图:Unicode 码点空间为 0x0000–0xFFFF(0x10000),被平均切成 Font_NumPages = 1024 页,每页覆盖 Font_PageSize = 64 个码点。页索引中的 codesMask 是 8 字节位图,标记本页 64 个码点哪些存在;charIndex 记录本页第一个字符在 UnicInfo 表中的起始下标。这样查找一个字符是否收录、以及其位图位置,都只需要一次 8 字节读 + 少量位运算,无需扫描全表。
页索引查找算法
/* 获取字符索引 */
static int Font_GetCharIndex(struct font_info *info, u16 unicode)
{
Font_PageEntry_t pageEntry;
u16 m = unicode / Font_PageSize;
u16 n = unicode % Font_PageSize;
u16 i, j;
sdfile_seek(info->pixel.file.fd, sizeof(struct font_header) + m * sizeof(Font_PageEntry_t), SEEK_SET);
sdfile_read(info->pixel.file.fd, &pageEntry, sizeof(Font_PageEntry_t));
if (!(pageEntry.codesMask[n / 8] & (1 << (n % 8)))) {
return -1;
}
for (i = j = 0; i < n; i++) {
if (pageEntry.codesMask[i / 8] & (1 << (i % 8))) {
j++;
}
}
i = pageEntry.charIndex + j;
return i;
}
(源码见 font_unicode.c)
算法步骤:
- 由
unicode计算页号m与页内偏移n(每页 64 码点)。 - 定位并读取该页
Font_PageEntry_t(文件头之后按m * sizeof(entry)偏移)。 - 检查
codesMask中n对应位;未置位说明字符未收录,返回-1。 - 若收录,统计页内
< n的已收录字符数j,则字符索引 =pageEntry.charIndex + j。由于页内字符按码点顺序连续编号,UnicInfo表无需按页对齐,节省了大量索引空间。
Font_GetCharBits() 随后读取 UnicInfo(宽度/高度/偏移/baseaddr),计算位图字节数 width * ((size+7)/8),并将位图读入 info->pixel.pixelbuf(见 font_unicode.c)。注意它处理了缓冲越界:若本次字符所需字节数超过既有缓冲,会 free 后按需 malloc,避免固定缓冲浪费或溢出。
文本排版与输出
TextOutW_Unicode() 是排版核心(见 font_unicode.c),其行为要点:
- 字节序:
info->bigendian决定 UTF-16 高低字节顺序,兼容不同字库工具生成的字节序。 - 换行:遇到
'\n'重置 x 坐标、y 下移一行;超出text_height则停止。 - 自动换行:
xpos > text_width时,若置FONT_SHOW_MULTI_LINE则折行(并把i回退 2 字节重新处理该字符),否则截断退出。 - 缺字回退:
width == 0(未收录)时用'_'字符替代,避免乱码占位。 - 像素输出:置
FONT_SHOW_PIXEL时逐字符调用font_copy_pixbuf(info, width, pixel_size, xpos-width, ypos)将位图拷贝到显存/推屏缓冲。 - 尺寸统计:累计
string_width,最终string_height = ypos + pixel_size,供调用方做对齐/居中布局。
数据模型
erDiagram
FONT_FILE ||--o{ PAGE_ENTRY : "1024 条页索引"
FONT_FILE ||--o{ UNIC_INFO : "每字符一条"
FONT_FILE ||--o{ PIXEL_DATA : "点阵位图区"
PAGE_ENTRY {
uint16 charIndex "本页首个字符索引"
uint8 codesMask "8 字节收录位图"
}
UNIC_INFO {
uint8 width "字符宽度"
uint8 height "字符高度"
int8 left "左偏移"
int8 top "上偏移"
uint32 baseaddr "位图基地址"
}
FONT_FILE {
char magic "unic"
uint32 version "版本"
uint32 timestamp "生成时间"
uint8 fontsize "字号"
}
运行时上下文 struct font_info(见 font_textout.h)将上述文件与排版状态绑定:pixel.file(文件名 + FILE*)、pixel.size(字号)、pixel.pixelbuf(位图缓冲)、pixel.nbytes(缓冲大小)、flags(FONT_SHOW_PIXEL / FONT_SHOW_MULTI_LINE)、text_width/text_height(文本区约束)与 string_width/string_height(排版结果)。LCD_FONT_DRAW_TOP(80)是 SDK 示例中文本在屏幕上的起始 y 坐标。
Usage Examples
示例一:初始化字库并打开
bool InitFont_Unicode(struct font_info *info)
{
struct font_header header;
memset(&header, 0, sizeof(header));
info->pixel.file.fd = sdfile_open((char *)info->pixel.file.name, "r");
if (!info->pixel.file.fd) {
log_error("unicode file not open!");
return FALSE;
}
sdfile_seek(info->pixel.file.fd, 0, SEEK_SET);
sdfile_read(info->pixel.file.fd, &header, sizeof(struct font_header));
log_info("magic : %s\n", header.magic);
log_info("version : 0x%x\n", header.version);
log_info("timestamp : %d\n", header.timestamp);
log_info("fontsize : %d\n", header.fontsize);
if (memcmp(header.magic, "unic", 4)) {
log_error("unicode magic not paired!");
fclose(info->pixel.file.fd);
info->pixel.file.fd = NULL;
return FALSE;
}
info->pixel.size = header.fontsize;
info->pixel.nbytes = info->pixel.size * ((info->pixel.size + 7) / 8);
log_info("unicode file open succ!\n");
return TRUE;
}
(源码见 font_unicode.c)
打开时即校验 "unic" magic 并从文件头读取字号,据此计算默认位图缓冲大小 size * ((size+7)/8)(向上取整到字节)。文件校验失败会立即关闭句柄,防止后续对非法文件的随机读取。
示例二:设置绘图区域并推屏
int lcd_set_draw_area(int xs, int xe, int ys, int ye)
{
if ((is_lcd_busy == 0x11) || lcd_sleep_in) {
return 0;
}
is_lcd_busy = 1;
#if (LCD_PUSH_MODE == SPI)
spi_set_ie(spi_dat->spi_cfg, 0);
spi_dma_wait_finish();
#endif
__this->SetDrawArea(xs, xe, ys, ye);
return 0;
}
(源码见 lcd_drive.c)
典型调用序列是 lcd_set_draw_area(0, w-1, 0, h-1) + lcd_draw(buf, w*h*2, 1):先锁定忙标志并关闭 SPI 中断、等待前一 DMA 结束,再下发窗口命令,随后 lcd_draw 以 wait=1 模式写完数据并开启中断等待完成。字库引擎产出的每行位图正是通过这一序列逐行/逐块上屏的。
示例三:面板注册宏
#define REGISTER_LCD_DRIVE() \
struct spi_lcd_init lcd_drive
extern struct spi_lcd_init lcd_drive;
(源码见 lcd_drive.h)
新增面板时(如 lcd_spi_st7735_80x160.c),在该文件顶部执行 REGISTER_LCD_DRIVE(),并填充 name/分辨率/color_format/initcode/initcode_cnt 与各函数指针,框架即可通过 extern 引用统一驱动,无需修改 lcd_drive.c —— 这是典型的"表驱动 + 全局符号注册"扩展模式。
Configuration Options
LCD 驱动与字库引擎的配置集中在 lcd_drive.h 编译期宏与 font_textout.h 运行期标志中:
LCD 驱动编译期宏
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
LCD_PUSH_MODE | enum | SPI(0) | 推屏通道:SPI=硬件 SPI+DMA,PAP=并行屏接口 |
TCFG_LCD_SPI_ST7735V_128X128_ENABLE | bool | 0 | 使能 128×128 ST7735 SPI 面板 |
TCFG_LCD_SPI_ST7735V_80X160_ENABLE | bool | 1 | 使能 80×160 ST7735 SPI 面板(示例默认屏) |
TCFG_LCD_SPI_ST7789V_ENABLE | bool | 0 | 使能通用 ST7789 SPI 面板 |
TCFG_LCD_PAP_SPI_ST7789V_240X240_ENABLE | bool | 0 | 使能 240×240 ST7789 PAP 面板 |
TCFG_LCD_PAP_SPI_ST7789V_240X320_ENABLE | bool | 0 | 使能 240×320 ST7789 PAP 面板 |
TCFG_TFT_LCD_DEV_SPI_HW_NUM | int | 1(SPI 模式) | 推屏 SPI 硬件序号 |
LCD_SPI_INTERRUPT_ENABLE | bool | 1(SPI 模式) | 是否使用 SPI 中断同步 DMA 完成 |
TCFG_LCD_9BIT_SPI_ENABLE | bool | 0 | 9-bit SPI 模式(命令/数据附加位走硬件) |
SPI_MODULE_CHOOSE | int | TCFG_TFT_LCD_DEV_SPI_HW_NUM | 选择 SPI0/1/2;SPI0 通常被外部 flash 占用 |
IRQ_SPI_IDX | irq | 依 SPI_MODULE_CHOOSE | 映射到 IRQ_SPI0/1/2_IDX |
REGFLAG_DELAY | u8 | 0xFF | InitCode 命令表中表示"延时"的特殊命令 |
以上均定义于 lcd_drive.h。
硬件绑定(可裁剪)
| 配置 | 位置 | 默认值 | 说明 |
|---|---|---|---|
spi1_p_data.port | lcd_drive.c | CLK=IO_PORTC_01, DO=IO_PORTC_00 | SPI 时钟/数据脚 |
spi1_p_data.clk | lcd_drive.c | 2000000L (2MHz) | SPI 时钟频率 |
lcd_spi_data.pin_reset/rs/cs/bl | lcd_drive.c | PORTC_11 / PORTC_02 / PORTD_02 / PORTA_08 | 面板控制脚(SPI 模式) |
lcd_spi_data.spi_cfg | lcd_drive.c | HW_SPI1 | SPI 外设实例 |
见 lcd_drive.c。
字库引擎标志与常量
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
FONT_SHOW_PIXEL | u16 flag | 0x01 | 排版时将位图输出到屏幕 |
FONT_SHOW_MULTI_LINE | u16 flag | 0x02 | 超过 text_width 时自动换行(否则截断) |
FONT_DEFAULT | u16 | FONT_SHOW_PIXEL | 默认排版标志组合 |
LCD_FONT_DRAW_TOP | int | 80 | 示例中文本起始 y 坐标 |
Font_NumPages | int | 1024 | 字库文件页索引条数 |
Font_PageSize | int | 64 | 每页覆盖的 Unicode 码点数(0x10000/1024) |
见 font_textout.h 与 font_unicode.c。
API Reference
LCD 驱动
void lcd_driver_init(void) / void lcd_driver_deinit(void)
初始化/反初始化 LCD:spi_open + 注册中断 + 执行面板 Init() 初始化序列;反初始化释放对应资源。
int lcd_draw(char *buf, u32 len, u8 wait)
将缓冲区写入屏幕当前绘图区域。
- 参数:
buf像素数据(RGB565);len字节数;wait=1 时等待 DMA 完成(依赖 SPI 中断),=0 时立即返回。 - 返回:0 成功;当
is_lcd_busy == 0x11或lcd_sleep_in时直接返回 0 并丢弃数据。 - 抛出/日志:DMA 发送错误时
log_error("spi dma send map timeout")(见spi_dma_send_map)。
int lcd_set_draw_area(int xs, int xe, int ys, int ye)
设置面板绘图窗口。
- 参数:窗口的 x/y 起止坐标(含端点)。
- 行为:置
is_lcd_busy = 1,SPI 模式下先关中断并等待前一 DMA 完成,再调用面板SetDrawArea。
int lcd_clear_screen(u16 color)
按行分块清屏,避免整屏大缓冲;内部循环调用推屏。
int lcd_backlight_ctrl(u8 on)
背光控制:通过 lcd_bl_h/lcd_bl_l 切换 pin_bl 电平。
void lcd_fill_rect(u16 xs, u16 xe, u16 ys, u16 ye, u16 color) / void lcd_drawpoint(u16 x, u16 y, u16 color) / void lcd_draw_line(u16 xs, u16 ys, u16 xe, u16 ye, u16 color)
基础绘图原语,均以 lcd_set_draw_area + lcd_draw 为基础实现。
u16 lcd_get_screen_width(void) / u16 lcd_get_screen_height(void) / void lcd_set_screen_width(u16) / void lcd_set_screen_height(u16)
屏幕逻辑分辨率读写(旋转后由上层更新)。
void lcd_st7735_set_direction(enum LCD_DIRECTION dir)
设置显示方向:ROTATE_0/90/180/270_CLOCKWISE。
void spi_dma_send_map(char *map, u32 size)
直接经 DMA 发送整块数据(不等待),由 spi_isr 后续复位忙标志。
GPIO 控制函数
lcd_reset_l/h()、lcd_cs_l/h()、lcd_rs_l/h()、lcd_bl_l/h():直接操作 RES/CS/RS/BL 引脚电平(9-bit SPI 模式下 lcd_rs_* 改为操作 JL_SPI1->CON1 BIT4)。声明见 lcd_drive.h。
字库引擎
bool InitFont_Unicode(struct font_info *info)
打开并校验字库文件(magic "unic"),设置字号与默认位图缓冲大小。失败返回 FALSE 并关闭句柄。
struct font_info *font_unicode_open(struct font_info *info)
打开字库(封装 InitFont_Unicode),返回 info 供后续排版使用。
u16 TextOutW_Unicode(struct font_info *info, u8 *str, u16 len)
将 UTF-16 字符串排版到文本区。
- 参数:
info字库上下文(含 flags、text_width/height);strUTF-16 字符串;len字节长度(应为偶数)。 - 返回:已处理的字符位置(字节数)。
- 行为:支持
'\n'换行、FONT_SHOW_MULTI_LINE自动换行、缺字回退'_';置FONT_SHOW_PIXEL时经font_copy_pixbuf输出位图;更新string_width/string_height。
void font_copy_pixbuf(struct font_info *info, u16 width, u16 height, u16 x, u16 y)
将当前 pixelbuf 中单个字符位图按坐标 (x, y) 拷贝至显存/推屏缓冲。
int font_unicode_display(struct font_info *info, u8 *pixbuf, u16 strlen)
封装"打开 → 排版输出"的高层显示接口。
void font_unicode_uninit(struct font_info *info)
释放字库资源(关闭文件、释放位图缓冲)。
以上声明见 font_textout.h。
失败模式、边界情况与并发
字库文件相关
| 失败场景 | 处理方式 | 源码位置 |
|---|---|---|
| 字库文件打开失败 | sdfile_open 返回 NULL,log_error("unicode file not open!"),返回 FALSE | font_unicode.c |
| magic 校验失败 | memcmp(header.magic, "unic", 4) 不等则关闭句柄并返回 FALSE,防止对非法文件随机读取 | font_unicode.c |
| 字符未收录 | Font_GetCharIndex 返回 -1,Font_GetCharBits 打日志并返回 0;排版层回退为 '_' 字符 | font_unicode.c |
| 位图缓冲不足 | nbytes > info->pixel.nbytes 时 free 旧缓冲并按需 malloc;分配失败则 nbytes=0 并返回 0(丢弃该字符) | font_unicode.c |
| 换行越界 | ypos + pixel_size > text_height 时回退并终止排版,避免绘制越出文本区 | font_unicode.c |
推屏时序与并发
- 忙状态保护:
is_lcd_busy声明为volatile u8,由lcd_set_draw_area置 1、spi_isr中断清零。lcd_set_draw_area/lcd_draw入口处检查is_lcd_busy == 0x11与lcd_sleep_in,命中即静默丢弃绘制 —— 这是"宁可丢帧也不撕裂/越序"的取舍(见 lcd_drive.c)。 - DMA 悬挂:
spi_pnd静态标志记录未完成的 DMA;spi_dma_wait_finish()以忙等方式轮询spi_get_pending。若 DMA 异常不完成会导致阻塞,因此关键路径(设置绘图区域前)先spi_set_ie(...,0)关中断再等待,避免中断竞争。 - 内存屏障:
asm("csync")保证spi_dma_transmit_for_isr的寄存器配置在启动前对硬件可见,属于 MCU 裸机编程中必须的序列化操作。 - 中断上下文:
spi_isr标注__attribute__((interrupt(""))),仅清 pending/复位忙标志/关中断使能,不做任何耗时操作,符合中断处理最小化原则。 - 文件句柄:字库
FILE*为全局上下文持有,TextOutW_Unicode每次字符随机 seek/read;若上层并发操作同一font_info会破坏文件偏移,因此字库渲染设计上为单线程使用模型。
性能与运维考虑
- SPI 时钟:默认 2 MHz 为保守值;实际量产屏(ST7735/ST7789)通常支持更高频率,可在
spi1_p_data.clk上调,推屏吞吐随之线性提升(见 lcd_drive.c)。 - DMA 非阻塞:
lcd_draw(buf, len, 0)非等待模式下 DMA 搬运不占 CPU,配合中断复位忙标志可实现双缓冲/流水线推屏;代价是调用方须自行保证缓冲存活期。 - 字库随机读取:每字符两次
sdfile_seek/read(页索引 + 位图),SD 卡顺序访问友好度一般;对于滚动文本,建议预渲染整行到缓冲再一次性推屏,减少 seek 次数。 - 内存占用:位图缓冲按需扩容(最大字符宽度 × 每像素字节),不会为整库分配内存;
lcd_clear_screen分块清屏避免整屏 RGB565 缓冲。 - 调试手段:SDK 提供
push_screen_test()与font_unicode_test()两个自测入口(见 lcd_drive.h);驱动日志 TAG 为[LCD_DRV],字库为[UNICODE],可在debug.h的LOG_TAG_CONST开关控制下查看初始化与错误信息。
扩展点
- 新增面板:在
sdk/apps/common/lcd/新建lcd_xxx.c,执行REGISTER_LCD_DRIVE()声明lcd_drive,填充分辨率、color_format、InitCode初始化序列与全部函数指针;在lcd_drive.h增加对应TCFG_LCD_*_ENABLE开关。框架无需任何改动。 - 更换推屏通道:调整
LCD_PUSH_MODE(SPI↔PAP);PAP 面板(lcd_pap_st7789_240x240.c等)自带 PAP 平台数据与初始化序列。 - 9-bit SPI 屏:置
TCFG_LCD_9BIT_SPI_ENABLE=1,lcd_rs_*自动切换为操作JL_SPI1->CON1BIT4 附加位。 - 自定义字库:
doc/软件工具/unicode字库工具/提供FontTool.exe与font.xml工程文件,可生成符合上述页索引格式的自定义字号字库文件,替换 SD 卡上的字库即可,代码零改动。 - 其他显示设备:
ui_devices_type枚举(LED_7/LCD_SEG3X9/TFT_LCD/DOT_LCD)预留了点阵屏与段码屏的扩展位;段码屏实现见sdk/cpu/cd09/segment_code_lcd_drv.c,属兄弟主题。
测试
SDK 自带两个显示自测函数(声明见 lcd_drive.h):
push_screen_test():向屏幕整幅推送测试图案,验证lcd_set_draw_area/lcd_draw/DMA 链路是否正常。font_unicode_test():以font_unicode接口渲染测试字符串,验证字库打开、页索引查找、位图读取与排版输出(含LCD_FONT_DRAW_TOP坐标基线)。
两者组合可快速定位问题在"推屏硬件链路"还是"字库/排版软件链路";配合 [LCD_DRV]/[UNICODE] 日志可进一步缩小到初始化序列、magic 校验或字符索引等具体环节。仓库中 sdk/cpu/demo/segment_code_lcd_demo.c 展示了兄弟主题段码屏的测试方式,可作对比参考。
Related Links
- lcd_drive.h — 驱动接口与配置宏
- lcd_drive.c — 驱动实现(DMA/中断/绘图)
- font_textout.h — 字库上下文与 API
- font_unicode.c — 字库引擎实现
- 面板驱动:
lcd_spi_st7735_80x160.c、lcd_spi_st7735_128x128.c、lcd_spi_st7789_BOE1.54_update_240x240.c、lcd_pap_st7789_240x240.c、lcd_pap_st7789_240x320.c(均在sdk/apps/common/lcd/) - 兄弟主题:段码屏驱动
sdk/cpu/cd09/segment_code_lcd_drv.c、sdk/cpu/demo/segment_code_lcd_demo.c - 字库生成工具:
doc/软件工具/unicode字库工具/FontTool.exe与font.xml