02 - TFT 显示应用
本章节介绍 Pico-G1 扩展板上的 TFT 显示应用示例 —— tft_display。该应用演示了如何使用 SPI 接口驱动 ST7789 TFT 彩屏,实现高分辨率彩色图形显示。这是学习高速 SPI 通信和高级图形编程的进阶示例,展示了从底层 SPI 操作到图形渲染的完整技术栈。
应用源码位于 SDK 目录 source/app/02_TFT_display/,提供了完整的 SPI 字符设备操作和 ST7789 驱动实现,是学习嵌入式图形编程的重要参考。
1 应用概述
1.1 功能特性
- SPI 高速通信:演示 Linux SPI 字符设备接口的高速数据传输
- TFT 驱动:完整的 ST7789 驱动实现(初始化、绘图、刷新)
- 彩色图形显示:支持 RGB565 格式(65536 色)的高分辨率显示
- 图形渲染:支持像素绘制、矩形填充、文字显示等图形功能
- 实时信息:显示系统运行时间、CPU 状态等实时信息
- 字体支持:内置 8×16 ASCII 点阵字库(0x20~0x7E)
1.2 技术参数
| 参数 | 值 |
|---|---|
TFT 型号 | ST7789 240×240 像素 |
通信接口 | SPI(SPI2,/dev/spidev2.0) |
SPI 模式 | MODE3(CPOL=1,CPHA=1) |
SPI 速率 | 24MHz(最大速率) |
色彩格式 | RGB565(16位,65536色) |
显存大小 | 240×240×2 = 115200 字节(113KB) |
字体大小 | 8×16 像素 ASCII 字库 |
刷新周期 | 1 秒(可配置) |
1.3 测试用例列表
| index | 名称 | 测试指令 | 预期现象(成功) | 失败可能原因 |
|---|---|---|---|---|
| 1 | 欢迎画面 | ./tft_display | 显示彩色方块和欢迎信息 | SPI 连接失败、TFT 地址错误 |
| 2 | 持续刷新 | ./tft_display(无参数) | 欢迎画面后每秒刷新系统信息 | 同上 |
| 3 | 图形测试 | 显示彩色矩形和文字 | 各种颜色正常显示 | 颜色格式错误、显存错误 |
1.4 目录结构
source/app/02_TFT_display/
├── Makefile # 构建脚本
├── main.c # 主程序
├── spi_hal.c # SPI HAL 层实现
├── spi_hal.h # SPI HAL 层头文件
├── gpio_hal.c # GPIO HAL 层实现
├── gpio_hal.h # GPIO HAL 层头文件
├── st7789.c # ST7789 驱动实现
├── st7789.h # ST7789 驱动头文件
├── font8x16.h # 8×16 ASCII 点阵字库
└── README.md # 说明文档2 硬件连接说明
2.1 引脚定义
| 信号 | 板上 GPIO | 控制器 / 节点 | 说明 |
|---|---|---|---|
| SCK | GPIO4_7 | SPI2 → /dev/spidev2.0 | SPI 时钟 |
| MOSI | GPIO5_0 | SPI2 → /dev/spidev2.0 | SPI 主机输出 |
| CS | GPIO5_1 | /dev/gpiochip5 line1 | 片选(手动framing) |
| DC | GPIO4_5 | /dev/gpiochip4 line5 | 数据/命令选择 |
| RES | GPIO4_4 | /dev/gpiochip4 line4 | 复位 |
| VCC | 3.3V | — | 供电 |
| GND | GND | — | 地线 |
2.2 硬件电路
标准 SPI TFT 接线:
Pico-G1 ST7789 TFT Module
┌───────────┐ ┌──────────────┐
│ │ │ │
│ GPIO4_7 ──┼────── SCK ──┤ SCK │
│ │ │ │
│ GPIO5_0 ──┼────── MOSI ──┤ SDA/MOSI │
│ │ │ │
│ GPIO5_1 ──┼────── CS ───┤ CS │
│ │ │ │
│ GPIO4_5 ──┼────── DC ───┤ DC/RS │
│ │ │ │
│ GPIO4_4 ──┼────── RES ──┤ RES/RESET │
│ │ │ │
│ 3.3V ──┼─────────────┤ VCC │
│ │ │ │
│ GND ──┼─────────────┤ GND │
│ │ │ │
└───────────┘ └──────────────┘引脚功能说明
- SCK/MOSI:SPI 数据线,由 SPI2 控制器驱动
- CS:片选信号,低电平有效,由 GPIO 手动控制
- DC:数据/命令选择,高电平=数据,低电平=命令
- RES:复位信号,低电平有效
- VCC:供电电压,3.3V(某些模组支持5V)
2.3 引脚复用说明
需要配置的引脚复用:
| pad | 物理地址 | 配置值 | 说明 |
|---|---|---|---|
| SCK (GPIO4_7) | 0x100C0028 | 0x1004 | func4 = SPI2_SCLK |
| MOSI (GPIO5_0) | 0x100C002C | 0x1004 | func4 = SPI2_SDO |
| CS (GPIO5_1) | 0x100C0030 | 0x1000 | func0 = GPIO(手动framing) |
| DC (GPIO4_5) | 0x100C0020 | 0x1005 | func5 = GPIO(默认 JTAG_TDO) |
| RES (GPIO4_4) | 0x100C001C | 0x1005 | func5 = GPIO(默认 JTAG_TDI) |
JTAG 引脚冲突
GPIO4_4 和 GPIO4_5 默认是 JTAG 功能,需要手动配置为 GPIO。如果不配置,SPI 显示将无法正常工作。
3 编译与部署
3.1 前置条件
在编译本应用之前,请确保已完成以下准备工作:
- SDK 环境已搭建:参照 开发环境搭建 完成交叉编译工具链和 SDK 配置
- 内核配置确认:确认内核已启用 SPI 驱动支持
- 硬件已连接:TFT 模组正确连接到相应引脚
3.2 编译应用
# 设置工具链路径
export PATH=$PATH:<SDK>/tools/linux/toolchains/arm-gcc12.2.0-linux-uclibceabi/bin
# 进入示例目录
cd <SDK>/source/app/02_TFT_display
# 编译
make
# 清理
make clean编译成功后,在当前目录下生成可执行文件 tft_display。
3.3 部署到板端
# 使用 SCP 传输到开发板
scp tft_display root@<板端IP>:/usr/bin/
# 或通过 TFTP 下载
tftp -g -r tft_display <板端IP>3.4 运行应用
# 添加执行权限
chmod +x /usr/bin/tft_display
# 运行 TFT 显示示例
/usr/bin/tft_display应用启动后,TFT 屏幕将显示彩色欢迎画面,2 秒后开始显示实时系统信息,按 Ctrl+C 退出。
3.5 预期输出
控制台输出
/mnt # ./spi_display
[spi] init ST7789 @ /dev/spidev2.0 ...
[spi] pad 0x100C0028 -> 0x00001004
[spi] pad 0x100C002C -> 0x00001004
[spi] pad 0x100C0030 -> 0x00001000
[spi] pad 0x100C0020 -> 0x00001005
[spi] pad 0x100C001C -> 0x00001005
[spi] opening /dev/spidev2.0 ...
[spi] spidev opened, fd=3
[spi] spidev mode/bits/speed set (MODE3/8b/24MHz)
[spi] chardev request DC @ /dev/gpiochip4 line 5 ...
[spi] chardev request RES @ /dev/gpiochip4 line 4 ...
[spi] chardev request CS @ /dev/gpiochip5 line 1 ...
[spi] chardev-verify: DC=0(expect0) RES=1(expect1) CS=1(expect1) ==> OK(chardev 真驱动了引脚)
[spi] spi_hal_init done
[tft] >>> st7789_init()
[tft] init: SLPOUT
[tft] init: SLPOUT +120ms ok
[tft] init: config cmds ok
[tft] init: DISPON
[tft] init: DISPON ok
[tft] init: clear-flush start
[tft] flush #1 start
[tft] flush #1 done
[tft] init: clear-flush done
[tft] <<< st7789_init() returned
[spi] 初始化成功,开始显示。
[tft] flush #2 start
[tft] flush #2 done
[tft] flush #3 start
[tft] flush #3 doneTFT 屏幕显示

固定与可变部分说明
- 固定部分:显示格式和布局(对于固定的代码)
- 可变部分:系统运行时间数值(每次刷新都会更新)
4 内部执行逻辑详解
4.1 应用架构
本应用采用分层设计,包括硬件抽象层、驱动层和应用层:
// 应用层(main.c)
int main(int argc, char *argv[])
{
// 1. 初始化引脚复用
padmux_init();
// 2. 初始化 SPI HAL 层
spi_init();
// 3. 初始化 GPIO(DC/RES/CS)
gpio_init();
// 4. 初始化 TFT 驱动
st7789_init();
// 5. 显示欢迎画面
display_welcome();
sleep(2);
// 6. 主循环刷新系统信息
while (1) {
st7789_clear();
display_system_info();
st7789_refresh();
sleep(1);
}
return 0;
}4.2 SPI HAL 层实现
SPI HAL 层封装了 Linux SPI 字符设备操作:
// SPI 初始化
int spi_init(void)
{
int fd = open("/dev/spidev2.0", O_RDWR);
if (fd < 0) {
perror("打开 SPI 设备失败");
return -1;
}
// 设置 SPI 模式为 MODE3
uint8_t mode = SPI_MODE_3;
ioctl(fd, SPI_IOC_WR_MODE, &mode);
// 设置每个事务的字节数为 8 位
uint8_t bits = 8;
ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits);
// 设置 SPI 最大速度
uint32_t speed = 24000000; // 24MHz
ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
return fd;
}
// SPI 数据传输
int spi_transfer(uint8_t *tx_data, uint8_t *rx_data, uint32_t length)
{
struct spi_ioc_transfer tr = {
.tx_buf = (unsigned long)tx_data,
.rx_buf = (unsigned long)rx_data,
.len = length,
.speed_hz = 24000000,
.bits_per_word = 8,
.delay_usecs = 0,
};
return ioctl(spi_fd, SPI_IOC_MESSAGE(1), &tr);
}4.3 ST7789 驱动实现
ST7789 驱动实现了完整的 TFT 屏控制功能:
// TFT 初始化序列
void st7789_init(void)
{
// 硬件复位
gpio_set_value(RES_GPIO, 0);
usleep(10000);
gpio_set_value(RES_GPIO, 1);
usleep(10000);
// 发送初始化命令
st7789_write_command(0x11); // SLPOUT(退出睡眠模式)
usleep(120000); // 等待 120ms
// MADCTL(页地址顺序)
st7789_write_command(0x36);
st7789_write_data(0x00);
// COLMOD(像素格式设置)
st7789_write_command(0x3A);
st7789_write_data(0x05); // 16位/pixel
// ... 其他初始化命令
st7789_write_command(0x21); // INVON(反转显示)
st7789_write_command(0x29); // DISPON(开启显示)
usleep(20000); // 等待显示开启
st7789_clear();
st7789_refresh();
}
// 发送命令
void st7789_write_command(uint8_t cmd)
{
gpio_set_value(DC_GPIO, 0); // DC = 0,命令模式
gpio_set_value(CS_GPIO, 0); // CS = 0,片选有效
spi_transfer(&cmd, NULL, 1);
gpio_set_value(CS_GPIO, 1); // CS = 1,片选无效
}
// 发送数据
void st7789_write_data(uint8_t *data, uint32_t length)
{
gpio_set_value(DC_GPIO, 1); // DC = 1,数据模式
gpio_set_value(CS_GPIO, 0); // CS = 0,片选有效
spi_transfer(data, NULL, length);
gpio_set_value(CS_GPIO, 1); // CS = 1,片选无效
}4.4 图形渲染实现
驱动层提供了基本的图形渲染功能:
// 设置像素
void st7789_set_pixel(int x, int y, uint16_t color)
{
if (x < 0 || x >= TFT_WIDTH || y < 0 || y >= TFT_HEIGHT)
return;
int index = (y * TFT_WIDTH + x) * 2;
framebuffer[index] = color >> 8; // 高字节
framebuffer[index + 1] = color & 0xFF; // 低字节
}
// 清屏
void st7789_clear(void)
{
memset(framebuffer, 0, sizeof(framebuffer));
}
// 刷新显示
void st7789_refresh(void)
{
// 设置列地址范围
st7789_write_command(0x2A); // CASET
uint8_t col_data[] = {0, 0, 0, (TFT_WIDTH - 1) & 0xFF};
st7789_write_data(col_data, 4);
// 设置页地址范围
st7789_write_command(0x2B); // RASET
uint8_t row_data[] = {0, 0, 0, (TFT_HEIGHT - 1) & 0xFF};
st7789_write_data(row_data, 4);
// 写入显存数据
st7789_write_command(0x2C); // RAMWR
st7789_write_data(framebuffer, sizeof(framebuffer));
}5 关键编程要点
5.1 SPI 时序控制
SPI MODE3 特点:
- CPOL = 1:时钟空闲时为高电平
- CPHA = 1:在时钟下降沿采样数据
5.2 RGB565 色彩格式
RGB565 是一种16位色彩格式:
位分配: RRRRRGGG GGGBBBBB
↑5位红↑ ↑6位绿↑↑5位蓝↑
示例:
- 红色(0xF800): 11111 000000 00000
- 绿色(0x07E0): 00000 111111 00000
- 蓝色(0x001F): 00000 000000 11111
- 白色(0xFFFF): 11111 111111 111115.3 显存管理
显存布局:
- 240×240 像素,每像素 2 字节
- 总大小:240 × 240 × 2 = 115,200 字节
- 字节序:大端模式(MSB first)
访问方式:
// 计算 (x,y) 像素的显存偏移
int index = (y * TFT_WIDTH + x) * 2;
// 写入像素数据(大端模式)
framebuffer[index] = color >> 8; // 高字节
framebuffer[index + 1] = color & 0xFF; // 低字节5.4 高速刷新优化
分片传输:
// 由于 TX FIFO < 128 字节,需要分片传输
#define CHUNK_SIZE 16
void st7789_refresh(void)
{
// ... 设置地址范围 ...
gpio_set_value(DC_GPIO, 1); // 数据模式
gpio_set_value(CS_GPIO, 0); // 片选有效
// 分片传输显存数据
for (int i = 0; i < sizeof(framebuffer); i += CHUNK_SIZE) {
int chunk_len = min(CHUNK_SIZE, sizeof(framebuffer) - i);
spi_transfer(&framebuffer[i], NULL, chunk_len);
usleep(100); // 片间喘息,避免 FIFO 溢出
}
gpio_set_value(CS_GPIO, 1); // 片选无效
}6 代码定制说明
6.1 修改 SPI 速率
编辑 spi_hal.c 中的 SPI 速度参数:
uint32_t speed = 24000000; // 24MHz(最大速率)
// uint32_t speed = 12000000; // 12MHz(稳定速率)
// uint32_t speed = 8000000; // 8MHz(兼容速率)SPI 速率选择
- 24MHz:最高速率,可能不稳定
- 12MHz:推荐速率,稳定可靠
- 8MHz:兼容性最好,适合长线
6.2 更改分辨率
支持其他分辨率(如 135×240):
// 修改 st7789.h
#define TFT_WIDTH 135
#define TFT_HEIGHT 240
// 修改初始化序列中的相应参数
st7789_write_command(0x2A); // CASET
uint8_t col_data[] = {0, 0, 0, (TFT_WIDTH - 1) & 0xFF};
st7789_write_data(col_data, 4);6.3 添加图形函数
添加更多图形函数:
// 绘制水平线
void st7789_draw_hline(int x1, int x2, int y, uint16_t color)
{
for (int x = x1; x <= x2; x++)
st7789_set_pixel(x, y, color);
}
// 绘制垂直线
void st7789_draw_vline(int x, int y1, int y2, uint16_t color)
{
for (int y = y1; y <= y2; y++)
st7789_set_pixel(x, y, color);
}
// 绘制矩形
void st7789_draw_rect(int x1, int y1, int x2, int y2, uint16_t color)
{
st7789_draw_hline(x1, x2, y1, color);
st7789_draw_hline(x1, x2, y2, color);
st7789_draw_vline(x1, y1, y2, color);
st7789_draw_vline(x2, y1, y2, color);
}7 常见问题排查
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 屏幕不亮 | SPI 连接失败、供电不足 | 检查接线、确认供电电压 |
| 花屏或乱码 | 初始化序列错误、SPI 速率过高 | 检查初始化参数、降低 SPI 速率 |
| 颜色异常 | RGB565 格式错误、字节序错误 | 检查色彩格式转换、确认字节序 |
| 部分显示异常 | 地址范围设置错误 | 检查 CASET/RASET 参数 |
| 刷新缓慢 | 分片大小不当、速率过低 | 调整分片大小、提高 SPI 速率 |
| 白屏无显示 | 对比度设置、显示未开启 | 调整对比度、确认 DISPON 命令 |
8 进阶扩展
8.1 实现图片显示
支持位图格式显示:
// 显示 RGB565 格式图片
void st7789_draw_image(int x, int y, int width, int height, const uint16_t *image)
{
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
st7789_set_pixel(x + col, y + row, image[row * width + col]);
}
}
}8.2 实现局部刷新
只刷新变化区域,提高刷新效率:
// 局部刷新
void st7789_refresh_partial(int x1, int y1, int x2, int y2)
{
// 设置局部刷新范围
st7789_write_command(0x2A); // CASET
uint8_t col_data[] = {x1 >> 8, x1 & 0xFF, x2 >> 8, x2 & 0xFF};
st7789_write_data(col_data, 4);
st7789_write_command(0x2B); // RASET
uint8_t row_data[] = {y1 >> 8, y1 & 0xFF, y2 >> 8, y2 & 0xFF};
st7789_write_data(row_data, 4);
// 写入局部显存数据
st7789_write_command(0x2C); // RAMWR
// ... 写入局部区域数据 ...
}8.3 实现双缓冲
使用双缓冲技术避免闪烁:
uint16_t framebuffer_front[TFT_WIDTH * TFT_HEIGHT];
uint16_t framebuffer_back[TFT_WIDTH * TFT_HEIGHT];
void st7789_swap_buffers(void)
{
// 交换前后缓冲区指针
uint16_t *temp = framebuffer_front;
framebuffer_front = framebuffer_back;
framebuffer_back = temp;
// 刷新前缓冲区到显示
st7789_refresh();
}