1. 项目概述

SPI_TFT_ILI9225 是一个面向嵌入式平台的轻量级图形驱动库,专为基于 ILI9225 控制器的 160×128 像素、16 位色(RGB:565)并行/串行接口 TFT 液晶模组设计。该库采用纯 SPI 主机模式(Software Bit-Banging 或 Hardware SPI),不依赖特定 MCU 架构,但默认适配 STM32 系列(HAL 库环境),亦可快速移植至 ESP32、nRF52、RP2040 等主流平台。

与同系列 SPI_TFT_ILI9341 库高度协同——二者共享同一套字体系统、绘图 API 接口、坐标系定义及内存管理逻辑。这意味着:

  • 所有 .fnt 字体文件(含 ASCII、GB2312、自定义图标字模)可 零修改复用
  • tft_print() , tft_draw_rect() , tft_fill_circle() 等函数签名完全一致;
  • HTML/PHP 字体编辑工具(如作者维护的 font_editor_v2.php )生成的二进制字体资源可直接加载;
  • 用户在迁移显示设备时,仅需替换底层初始化与寄存器配置代码,上层应用逻辑无需重构。

该库定位为“裸机友好型驱动”:无 RTOS 依赖、无动态内存分配( malloc/free )、无 C++ 类封装,全部使用 C99 标准编写,核心代码体积小于 8KB(ARM Cortex-M3 编译后 Flash 占用),RAM 静态占用仅约 240 字节(不含帧缓冲区)。适用于资源受限的工业 HMI、便携仪器、教学实验板等场景。


2. ILI9225 控制器特性与硬件接口约束

2.1 显示面板关键参数

参数 说明
分辨率 160 × 128 实际有效像素区域,非物理屏尺寸
色深 16-bit RGB565 R:5bit, G:6bit, B:5bit,共 65536 色
刷新率(典型) ≤ 15 Hz(全屏刷新) 受限于 SPI 速率与命令开销,局部更新可达 30+ Hz
接口类型 8080 并行 / 3/4 线 SPI 本库仅实现 3 线 SPI 模式(D/C# + SDA + SCL) ,兼容大多数低成本模组
供电电压 3.3 V(IO) / 3.0–3.6 V(VCC) 不支持 5V 直接驱动,需电平转换

⚠️ 注意:ILI9225 与 ILI9341 在寄存器映射、时序要求、伽马校正机制上存在本质差异。 不可将 ILI9341 初始化序列直接用于 ILI9225 ,否则将导致白屏、花屏或控制器锁死。

2.2 SPI 通信协议详解

ILI9225 的 SPI 接口采用 3 线制(非标准四线) ,信号定义如下:

引脚名 功能 驱动方式 说明
SCL 串行时钟 MCU 输出 上升沿采样,频率建议 ≤ 10 MHz(实测 STM32F103 @ 72MHz 下 8MHz 稳定)
SDA 串行数据 MCU 开漏/推挽双向 复用为数据/命令线 ,由 DC# 电平决定当前传输内容
DC# Data/Command Select MCU 输出 低电平 = 发送命令(Command);高电平 = 发送参数/数据(Data)

✅ 此设计省去独立的 CS# 引脚(片选由软件模拟),降低引脚占用。 DC# 必须严格同步于 SCL 边沿:在 SCL 为低时改变 DC# ,并在 SCL 上升前稳定。

2.3 关键时序约束(依据 ILI9225 Datasheet Rev.1.1)

时序参数 最小值 典型值 测量点 工程建议
tCSS (CS setup) 本库无硬件 CS,忽略 软件确保 DC# 稳定后 ≥ 100 ns 再拉高 SCL
tCH (Clock high) 25 ns SCL 高电平持续时间 HAL_SPI_Transmit() 默认满足;Bit-bang 需插入 __NOP()
tCL (Clock low) 25 ns SCL 低电平持续时间 同上
tDS (Data setup) 15 ns SDA SCL 上升沿前建立时间 GPIO 输出速度设为 GPIO_SPEED_FREQ_HIGH (STM32)
tDH (Data hold) 10 ns SCL 上升沿后 SDA 保持时间 通常自动满足

🔧 实践提示:在 STM32 HAL 环境中,推荐使用 HAL_SPI_Transmit() 配合 HAL_GPIO_WritePin() 切换 DC# ,避免中断打断时序;若使用 LL 库或寄存器操作,务必在 SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG_TXE) 为 SET 后写入下字节。


3. 驱动架构与核心模块设计

3.1 整体分层结构

+---------------------+
|   Application Layer | ← tft_print(), tft_draw_line(), etc.
+---------------------+
|    Graphics Engine  | ← 像素操作、Bresenham 算法、区域填充
+---------------------+
|   Font Rendering    | ← .fnt 解析、ASCII/Unicode 字形查表、抗锯齿(可选)
+---------------------+
|   ILI9225 Driver    | ← 寄存器配置、GRAM 写入、睡眠控制、方向设置
+---------------------+
|     SPI Interface   | ← HAL_SPI_Transmit() 封装 / Bit-bang 实现
+---------------------+
|    GPIO Abstraction | ← DC#, RESET# 控制(宏定义可移植)
+---------------------+

所有模块通过 tft.h 统一暴露接口, tft.c 实现具体逻辑, ili9225_reg.h 定义寄存器地址与掩码。

3.2 关键数据结构

tft_t —— 显示器句柄(静态全局单例)
typedef struct {
    SPI_HandleTypeDef *spi;      // HAL SPI handle(可为 NULL,启用 bit-bang)
    uint16_t width;            // 160
    uint16_t height;           // 128
    uint8_t  rotation;         // 0~3,对应 MADCTL 值
    uint16_t *frame_buffer;    // 可选:若定义 TFT_USE_FRAMEBUFFER,则指向 RAM 缓冲区
    uint8_t  is_sleeping;      // 1=进入睡眠模式,禁止任何写操作
} tft_t;

extern tft_t g_tft; // 全局实例,用户无需手动创建

💡 设计考量: frame_buffer 为可选字段。当 MCU RAM 充足(≥ 40KB)且需离屏合成时启用;否则所有绘图直写 GRAM( ILI9225_CMD_GRAM_DATA_WRITE ),牺牲部分性能换取内存节省。

font_t —— 字体描述符(与 ILI9341 完全兼容)
typedef struct {
    const uint8_t *data;     // 指向 .fnt 文件起始地址(Flash)
    uint16_t width;        // 字符宽度(px)
    uint16_t height;       // 字符高度(px)
    uint8_t  first_char;   // 起始 ASCII 码(如 0x20)
    uint8_t  count;        // 字符总数(如 95)
    uint8_t  bytes_per_char;// 每字符字节数(= width * height / 8)
} font_t;

extern const font_t font_8x12; // 示例:8×12 点阵字体

.fnt 文件格式为紧凑二进制:头部 10 字节(含版本、宽高、首码),后续连续存储字模位图(MSB 在前,每行字节对齐)。


4. 核心 API 接口详解

4.1 初始化与基础控制

函数 原型 说明 典型调用
tft_init() void tft_init(SPI_HandleTypeDef *hspi) 初始化 SPI、GPIO、ILI9225 寄存器序列;执行软复位、伽马校正、MADCTL 设置 tft_init(&hspi1);
tft_sleep_in() void tft_sleep_in(void) 进入低功耗睡眠模式( ILI9225_CMD_SLEEP_IN ),关闭振荡器与显示 tft_sleep_in(); // 电流 < 100μA
tft_sleep_out() void tft_sleep_out(void) 退出睡眠( ILI9225_CMD_SLEEP_OUT ),需等待 5ms 后再发其他命令 tft_sleep_out(); HAL_Delay(5);
tft_set_rotation() void tft_set_rotation(uint8_t r) 设置屏幕旋转(0=0°, 1=90°, 2=180°, 3=270°),自动更新 g_tft.rotation MADCTL tft_set_rotation(1); // 竖屏

📌 tft_init() 内部执行的关键寄存器配置:

ili9225_write_cmd(ILI9225_CMD_DRIVER_OUTPUT_CTRL);   // 0x01 → 0x00FF, 0x007F, 0x0000
ili9225_write_cmd(ILI9225_CMD_ENTRY_MODE);          // 0x03 → 0x1030 (BGR, scan dir)
ili9225_write_cmd(ILI9225_CMD_DISPLAY_CTRL1);       // 0x07 → 0x0000 (disable display)
ili9225_write_cmd(ILI9225_CMD_POWER_CTRL1);          // 0x10 → 0x1030 (AVDD, AVSS on)
ili9225_write_cmd(ILI9225_CMD_POWER_CTRL2);          // 0x11 → 0x0006 (GVDD=4.8V)
ili9225_write_cmd(ILI9225_CMD_POWER_CTRL3);          // 0x12 → 0x0000 (no booster)
ili9225_write_cmd(ILI9225_CMD_POWER_CTRL4);          // 0x13 → 0x0000 (no booster)
ili9225_write_cmd(ILI9225_CMD_GAMMA_CTRL1);         // 0x30 → 0x0000...0x000F (gamma curve)
ili9225_write_cmd(ILI9225_CMD_DISPLAY_CTRL1);       // 0x07 → 0x0001 (enable display)

4.2 绘图与填充函数

函数 原型 说明 性能特征
tft_draw_pixel() void tft_draw_pixel(uint16_t x, uint16_t y, uint16_t color) 绘制单像素 O(1),需先设置地址窗口
tft_draw_line() void tft_draw_line(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) Bresenham 直线算法 O(
tft_draw_rect() void tft_draw_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) 绘制空心矩形边框 O(w+h)
tft_fill_rect() void tft_fill_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) 填充实心矩形 O(w×h),若启用 frame buffer 则为内存拷贝
tft_fill_screen() void tft_fill_screen(uint16_t color) 全屏填充 O(160×128)=20480 次 GRAM 写入

⚡ 高效填充原理: tft_fill_rect() 先调用 ili9225_set_window(x,y,x+w-1,y+h-1) 设置地址窗口,再连续发送 w×h color 值至 ILI9225_CMD_GRAM_DATA_WRITE 。SPI DMA 模式下可显著提升速度。

4.3 文本渲染接口

函数 原型 说明 注意事项
tft_set_font() void tft_set_font(const font_t *f) 加载当前字体 必须在 tft_init() 后调用
tft_set_text_color() void tft_set_text_color(uint16_t fg, uint16_t bg) 设置前景/背景色 bg=0x0000 表示透明背景(逐像素绘制)
tft_print() void tft_print(const char *str) 从光标位置打印字符串 自动换行(超出右边界则 y+=font.height
tft_print_at() void tft_print_at(uint16_t x, uint16_t y, const char *str) 指定坐标打印 不改变内部光标位置

🌐 中文支持:通过 font_gb2312_16x16.fnt (含 7445 个汉字)即可实现 GB2312 编码文本渲染。 tft_print() 内部检测 0xA1≤c≤0xFE 判断双字节起始,自动组合高位/低位查表。


5. 移植指南与硬件抽象层(HAL)

5.1 GPIO 与 SPI 抽象宏定义

所有硬件相关操作通过以下宏控制, 用户仅需修改 tft_hal.h

// tft_hal.h —— 用户可编辑区域
#define TFT_DC_PORT       GPIOA
#define TFT_DC_PIN        GPIO_PIN_5
#define TFT_RST_PORT      GPIOA
#define TFT_RST_PIN       GPIO_PIN_6
#define TFT_SPI_HANDLE    hspi1

// 位带操作(高效切换 DC#)
#define TFT_DC_HIGH()     do { SET_BIT(TFT_DC_PORT->BSRR, TFT_DC_PIN); } while(0)
#define TFT_DC_LOW()      do { SET_BIT(TFT_DC_PORT->BSRR, TFT_DC_PIN << 16); } while(0)
#define TFT_RST_HIGH()    HAL_GPIO_WritePin(TFT_RST_PORT, TFT_RST_PIN, GPIO_PIN_SET)
#define TFT_RST_LOW()     HAL_GPIO_WritePin(TFT_RST_PORT, TFT_RST_PIN, GPIO_PIN_RESET)

5.2 SPI 传输函数定制

库提供两种 SPI 后端:

方式一:HAL SPI(推荐)
static void ili9225_spi_write(uint8_t cmd, const uint8_t *data, uint16_t len) {
    TFT_DC_LOW();  // 发送命令
    HAL_SPI_Transmit(&TFT_SPI_HANDLE, &cmd, 1, HAL_MAX_DELAY);
    if (data && len) {
        TFT_DC_HIGH(); // 发送数据
        HAL_SPI_Transmit(&TFT_SPI_HANDLE, (uint8_t*)data, len, HAL_MAX_DELAY);
    }
}
方式二:Bit-bang(无 SPI 外设时)
static void ili9225_spi_bitbang(uint8_t cmd, const uint8_t *data, uint16_t len) {
    TFT_DC_LOW();
    for (int i = 0; i < 8; i++) {
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, (cmd & 0x80) ? GPIO_PIN_SET : GPIO_PIN_RESET);
        cmd <<= 1;
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // SCL high
        HAL_Delay(1); // 调整此值匹配时序
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
    }
    // ... data 循环同理
}

✅ 移植验证清单:

  • [ ] TFT_DC_PIN TFT_RST_PIN 物理连接正确
  • [ ] TFT_SPI_HANDLE 已在 MX_SPI1_Init() 中使能且时钟配置 ≥ 8MHz
  • [ ] GPIO 输出速度设为 GPIO_SPEED_FREQ_HIGH (STM32CubeMX 中勾选)
  • [ ] HAL_Delay() 基于 SysTick,精度 ≥ 1ms

6. 字体系统深度解析与工具链

6.1 .fnt 文件规范(v1.0)

偏移 长度 字段 值示例 说明
0x00 2 magic 0x464E ("FN") 标识文件类型
0x02 1 version 0x01 当前版本
0x03 1 width 8 字符宽度(px)
0x04 1 height 12 字符高度(px)
0x05 1 first_char 0x20 起始 ASCII 码
0x06 1 count 95 字符总数
0x07 1 bytes_per_char 12 = ceil(width * height / 8)
0x08 2 reserved 0x0000 保留字段
0x0A glyph_data 0xFF,0x00,... 连续存储,每个字符 bytes_per_char 字节

🔍 字模存储规则:

  • 每字符按行存储,从上到下;
  • 每行按字节存储,从左到右;
  • 每字节最高位(MSB)对应行内最左像素;
  • 黑色像素为 1 ,白色为 0 (正显)。

6.2 PHP 字体编辑器使用流程

作者提供的 font_editor_v2.php 是一个 Web 端交互式工具:

  1. 访问 http://localhost/font_editor_v2.php (需 PHP 环境);
  2. 设置字符集(ASCII / GB2312 / Unicode)、宽高、起始码;
  3. 在画布中点击绘制字形,支持放大镜、橡皮擦、反色;
  4. 点击 “Export Binary” 生成 xxx.fnt 文件;
  5. 将文件复制到工程 fonts/ 目录,添加 extern const font_t xxx_font; 声明;
  6. main.c 中调用 tft_set_font(&xxx_font);

💡 工程技巧:为减小 Flash 占用,可对 .fnt 文件进行 LZ4 压缩,解压后加载至 RAM 使用(需额外 2KB RAM)。


7. 性能优化与常见问题排查

7.1 关键性能瓶颈与对策

瓶颈 表现 优化方案
SPI 速率不足 全屏填充 > 500ms hspi1.Init.BaudRatePrescaler 设为 SPI_BAUDRATEPRESCALER_2 (主频 72MHz → SPI 36MHz);启用 SPI_CR1_DFF (16-bit 模式)一次传 2 字节
频繁 DC# 切换 命令/数据切换延迟高 合并连续写入: tft_fill_rect() 内部不重复切 DC# ,而是在窗口设置后批量写 GRAM
未启用 DMA CPU 占用率 100% 配置 hspi1.hdmatx HAL_SPI_Transmit_DMA() 替代轮询模式
帧缓冲区过大 RAM 不足 禁用 TFT_USE_FRAMEBUFFER ,改用直写模式;或仅对动画区域启用局部缓冲

7.2 典型故障现象与根因分析

现象 可能原因 排查步骤
白屏 / 无显示 RESET# 未正确释放; VCC 电压不足; DC# 电平反接 用万用表测 TFT_RST_PIN 是否为 3.3V;示波器抓 DC# SCL 时序是否符合 datasheet
花屏 / 错位 地址窗口未设置; rotation 值错误;SPI 相位/极性配置错误 检查 ili9225_set_window() 是否被调用;确认 SPI_CR1_CPOL=0, CPHA=0 (Mode 0)
文字模糊 / 重影 字体高度与行距不匹配; tft_set_text_color() 背景色非纯黑/白 tft_print_at() 后手动调用 tft_set_cursor(0,0) 重置光标;检查 bg 是否为 0xFFFF (白底)或 0x0000 (黑底)
触摸无响应 本库 不包含触摸驱动 !ILI9225 仅为显示控制器 需额外集成 XPT2046 或 STMPE610 触摸芯片驱动

🛠️ 调试利器:在 ili9225_write_cmd() 中添加 printf("CMD: 0x%02X\n", cmd); 串口日志,比对初始化序列与官方 datasheet 是否一致。


8. 实际项目集成示例(STM32F103C8T6 + Keil MDK)

8.1 main.c 关键片段

#include "tft.h"
#include "fonts/font_16x24.h" // 自定义大字体

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_SPI1_Init();

    tft_init(&hspi1);
    tft_set_rotation(1); // 竖屏
    tft_set_font(&font_16x24);
    tft_set_text_color(0xFFFF, 0x0000); // 白字黑底

    // 显示温度(模拟)
    char buf[16];
    int temp = 25;
    sprintf(buf, "TEMP: %d C", temp);
    tft_print_at(10, 10, buf);

    // 绘制状态指示条
    tft_fill_rect(10, 40, 140, 8, 0x07E0); // Green
    tft_fill_rect(10, 40, (temp * 140) / 100, 8, 0xF800); // Red progress

    while (1) {
        HAL_Delay(1000);
        temp = (temp + 1) % 100;
        tft_fill_rect(10, 10, 120, 24, 0x0000); // 清除旧文本区
        sprintf(buf, "TEMP: %d C", temp);
        tft_print_at(10, 10, buf);
    }
}

8.2 Keil 工程配置要点

  • Target 页: Code Generation One ELF Section per Function
  • C/C++ 页: Define 添加 ARM_MATH_CM3, USE_HAL_DRIVER, TFT_USE_FRAMEBUFFER (如启用)
  • Linker 页: Use Memory Layout from Target Dialog ✔,确保 RW_IRAM1 ≥ 40KB(若启用 framebuffer)
  • Utilities 页: Flash Download Add STM32F1xx_Flash_Large.sct (支持 >64KB Flash)

✅ 编译结果(ARMCC v5.06):
Program Size: Code=7820 RO-data=1240 RW-data=24 ZI-data=40960
—— 其中 ZI-data=40960 即为 uint16_t frame_buffer[160*128] 占用 RAM。


9. 与同类库对比及选型建议

维度 SPI_TFT_ILI9225 Adafruit_ILI9341 (Arduino) STemWin (ST 官方)
代码体积 < 8 KB Flash ~25 KB Flash > 120 KB Flash
RAM 占用 240 B(无 framebuffer) 5 KB(含 buffer) 32 KB(最小配置)
移植难度 ★★☆(纯 C,HAL 封装) ★★★(Arduino API 依赖) ★★★★(需 ST CubeMX + Middlewares)
字体支持 ✅ 共享 .fnt ,PHP 编辑器 ❌ 仅内置 3 种字体,无编辑工具 ✅ 支持 TrueType,但需编译进固件
实时性 硬实时(无 OS 依赖) 软实时(Arduino loop()) 依赖 FreeRTOS 配置
适用场景 工业仪表、电池供电设备、教学板 快速原型、创客项目 高端 HMI、医疗设备

📌 选型结论:

  • 若项目要求 超低功耗、确定性响应、Flash < 64KB → 选 SPI_TFT_ILI9225
  • 若需 复杂 GUI、多窗口、触摸手势 → 应评估 LVGL + SPI 驱动;
  • 若已使用 STM32CubeIDE 且需商用认证 → STemWin 更稳妥。

10. 结语:回归嵌入式本质

SPI_TFT_ILI9225 的价值不在于炫技,而在于将一块 160×128 的廉价 TFT,还原为嵌入式工程师手中可控、可测、可预测的确定性外设。它不隐藏寄存器,不抽象时序,不强加框架——每一行 HAL_SPI_Transmit() 调用背后,都是对 ILI9225 datasheet 第 47 页时序图的精确复现;每一个 tft_print() 的输出,都源于对 .fnt 二进制格式的逐字节解析。

在 RTOS 泛滥、GUI 框架堆叠的今天,坚持用 200 行 C 代码完成初始化、用 3 个 GPIO 引脚驱动屏幕、用静态数组替代 malloc——这并非守旧,而是对资源边界的清醒认知,对硬件本质的敬畏,以及对“让事情真正发生”这一嵌入式初心的坚守。

当你在示波器上看到 DC# SCL 完美咬合,当第一行白字在黑底上清晰浮现,那一刻,你不是在调用 API,而是在与硅基世界对话。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐