ILI9225 TFT驱动库:轻量级SPI图形库设计与移植
TFT液晶驱动是嵌入式人机交互的基础技术,其核心在于控制器时序控制、SPI通信协议适配与低资源占用的图形渲染。ILI9225作为经典160×128分辨率、RGB565色深的显示控制器,广泛应用于工业HMI与教学实验平台。该驱动采用纯C99实现,支持软件模拟SPI与硬件SPI双模式,无RTOS依赖、零动态内存分配,Flash占用<8KB,RAM静态开销仅240字节。技术价值体现在跨平台可移植性(ST
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 端交互式工具:
- 访问
http://localhost/font_editor_v2.php(需 PHP 环境); - 设置字符集(ASCII / GB2312 / Unicode)、宽高、起始码;
- 在画布中点击绘制字形,支持放大镜、橡皮擦、反色;
- 点击 “Export Binary” 生成
xxx.fnt文件; - 将文件复制到工程
fonts/目录,添加extern const font_t xxx_font;声明; - 在
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→AddSTM32F1xx_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,而是在与硅基世界对话。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐
所有评论(0)