从零点亮一块彩屏:手把手教你用SPI驱动ST7789显示屏
你有没有想过,自己亲手让一块小小的彩色屏幕亮起来,显示文字、图形甚至动画?听起来像是高手才玩得转的事,但其实只要掌握正确的方法,哪怕你是嵌入式开发的“小白”,也能在几小时内完成这个看似高深的操作。
今天我们要聊的是ST7789—— 一块在1.3英寸到2.0英寸小屏幕上几乎无处不在的TFT控制器芯片。它被广泛用于智能手表、便携仪表、DIY项目中,而最常用、也最适合初学者上手的方式,就是通过SPI接口来控制它。
别担心没基础。这篇文章不堆术语、不甩理论,只讲你能看懂、能动手、能成功的实战路径。我们会从接线开始,一步步走到屏幕全亮、刷出颜色,最后告诉你怎么避免那些让人抓狂的“黑屏”、“花屏”坑。
为什么是 ST7789?又为什么选 SPI?
先说个现实:现在做嵌入式项目,谁还愿意为一个屏幕拉十几根线搞并口通信?MCU引脚宝贵得很,尤其是像ESP32-Sx系列或者STM32G0这类资源紧张的芯片,省一根是一根。
ST7789 的好处就在于——功能强,还省事。
它支持最高 240×320 分辨率,原生支持 RGB565 色彩格式(也就是常说的16位色,约6.5万色),内置升压电路和伽马校正,最关键的是,它可以用四根线就跑起来:SCK、MOSI、CS、DC。加上 RST 和电源,总共也就七八个引脚搞定。
而这四根核心信号线走的就是SPI 协议。SPI 是什么?你可以把它想象成一种“对讲机式”的通信方式:主控(比如你的单片机)说话,屏幕听着,不需要回话(因为大多数时候我们只写不读)。简单、高效、通用性强。
更重要的是,几乎所有主流开发平台都原生支持 SPI:
- Arduino 有SPI.h
- ESP32 支持硬件 SPI + DMA 加速
- 树莓派 Pico(RP2040)用 C/C++ SDK 轻松配置
- STM32 HAL 库直接调用传输函数
所以,选择 ST7789 + SPI 组合,等于选择了低成本、低门槛、高兼容性的入门方案。
接线很简单,但每根线都有讲究
先来看一张最基础的连接图:
| MCU 引脚 | → | ST7789 模块 |
|---|---|---|
| GPIOx (SCK) | → | SCK / CLK |
| GPIOy (MOSI) | → | MOSI / DIN |
| GPIOz (CS) | → | CS / SS |
| GPIOa (DC) | → | DC / A0 |
| GPIOb (RST) | → | RST |
| 3.3V | → | VCC |
| GND | → | GND |
| (可选)PWM | → | BLK / LED_K |
⚠️ 注意:有些模块标的是 VIN 而不是 VCC,其实是同一个意思;BLK 是背光控制脚,接 PWM 可调亮度。
这里面最容易忽略的细节是电平匹配。虽然 ST7789 支持 1.8V~3.3V IO 电压,但如果你用的是 5V 系统(比如老款 Arduino Uno),必须加电平转换器或串电阻限流,否则可能烧毁模块!
另外,RST 引脚不能省。虽然有些代码里看到“不用硬件复位”,但强烈建议接上。很多初始化失败的问题,根源就是芯片没真正重启。
SPI 模式怎么选?Mode 0 还是 Mode 3?
这是新手最容易栽的第一个坑。
SPI 有四种工作模式,由两个参数决定:CPOL(时钟极性)和 CPHA(时钟相位)。ST7789 官方文档写着支持 Mode 0 和 Mode 3,那到底该用哪个?
答案是:优先试 Mode 0(CPOL=0, CPHA=0)
什么意思?
- 空闲时 SCK 为低电平
- 数据在上升沿采样(即每个时钟周期的后半段稳定)
这几乎是市面上绝大多数开发板默认的设置。你在 Arduino 或 STM32CubeMX 中启用 SPI 外设时,默认就是这个模式。
如果 Mode 0 不行,再尝试切换到 Mode 3(CPOL=1, CPHA=1)试试。有时候某些厂商改了内部逻辑,会要求空闲高电平。
✅ 实战提示:可以用示波器抓一下 SCK 波形,看看是否符合预期;没有设备的话,就靠换代码配置多试几次。
命令与数据交替传输:ST7789 的“语言规则”
理解这一点,你就掌握了和屏幕“对话”的钥匙。
ST7789 并不像普通外设那样收发固定协议的数据包,它是靠命令 + 参数的方式工作的。就像点菜一样:
- 先告诉它你要干嘛(下命令)
- 再把具体信息传过去(送参数)
而区分“命令”和“数据”的关键,就是那根叫DC的引脚。
| DC 状态 | 含义 |
|---|---|
| DC = 0 | 当前传输的是命令字节(例如0x2A表示设置列地址) |
| DC = 1 | 当前传输的是数据(参数或像素值) |
举个例子,你想设置显示区域为左上角 (0,0) 到右下角 (239,319):
拉低 CS → 发送 0x2A (DC=0) → 发起始高位 (DC=1) → 起始低位 → 结束高位 → 结束低位 → CS 拉高整个过程如下:
void ST7789_WriteCmd(uint8_t cmd) { CS_LOW; DC_LOW; // 命令模式 spi_write(&cmd, 1); DC_HIGH; // 自动切回数据模式,方便后续连续写数据 } void ST7789_WriteData(uint8_t *buf, size_t len) { CS_LOW; spi_write(buf, len); // 此时 DC 已为高,表示数据 CS_HIGH; }注意看,我们在发送完命令后立刻把 DC 拉高——这是一个小技巧,因为接下来大概率要写参数,提前准备好状态可以减少一次 GPIO 切换,提升效率。
初始化序列:让屏幕“醒过来”的魔法咒语
刚上电的 ST7789 是沉睡的。它需要一系列特定的命令才能进入正常工作状态。这些命令组合起来,叫做初始化序列(Initialization Sequence)。
你可以把它理解为“开机自检+系统配置”。
下面是典型的一段初始化流程(适用于大多数 240x320 屏):
void ST7789_Init(void) { // 硬件复位 RST_LOW; delay_ms(10); RST_HIGH; delay_ms(150); // 软件复位 ST7789_WriteCmd(0x01); delay_ms(150); // 退出睡眠模式 ST7789_WriteCmd(0x11); delay_ms(200); // 必须等够!手册要求 ≥120ms // 设置色彩格式为 16-bit (RGB565) ST7789_WriteCmd(0x3A); ST7789_WriteByte(0x05); // 0x05 = 16位色 // 设置内存访问方向(旋转/镜像) ST7789_WriteCmd(0x36); ST7789_WriteByte(0xC0); // 常见竖屏方向,可根据实际调整 // 设置列地址范围(0~239) ST7789_WriteCmd(0x2A); uint8_t col_addr[] = {0x00, 0x00, 0x00, 0xEF}; // 240列 ST7789_WriteData(col_addr, 4); // 设置行地址范围(0~319) ST7789_WriteCmd(0x2B); uint8_t row_addr[] = {0x00, 0x00, 0x01, 0x3F}; // 320行 ST7789_WriteData(row_addr, 4); // 开启显示 ST7789_WriteCmd(0x29); }其中最关键的几个点:
-0x11(Sleep Out)之后必须延时至少120ms
-0x36(MADCTL)决定了屏幕怎么“躺着”显示,常见值有0x00,0x60,0xC0,0xA0,分别对应不同旋转角度
-0x29(Display On)才是真正点亮屏幕的开关
如果你的屏幕一直黑着,先检查是不是漏了0x11或者0x29,或者延时不达标。
如何画满屏?向 GRAM 写入像素数据
GRAM 是什么?它是 Graphics RAM 的缩写,即图形内存。虽然 ST7789 本身没有大容量 RAM 存储整帧图像,但它提供了一个“窗口机制”:你告诉它一个区域,然后往里面不停地写颜色数据,它就会自动映射到屏幕上。
写像素的核心命令是0x2C(Write Memory Start),意思是:“接下来所有数据都是像素点了。”
比如,我们想把整个屏幕刷成白色(RGB565 下白色是0xFFFF):
void ST7789_FillScreen(uint16_t color) { ST7789_SetWindow(0, 0, 239, 319); // 设定区域 ST7789_WriteCmd(0x2C); // 开始写显存 uint8_t hi = color >> 8; uint8_t lo = color & 0xFF; // 构造重复颜色数组(建议用DMA或缓冲区优化) for (int i = 0; i < 240 * 320; i++) { ST7789_WriteByte(hi); ST7789_WriteByte(lo); } }当然,这种轮询写法非常慢,尤其是在 SPI 频率只有 4MHz 的情况下,刷一屏可能要几百毫秒。但在调试阶段完全可用。
💡 提升建议:使用 DMA 传输、双缓冲机制,或将颜色预存在数组中批量发送。
屏幕方向怎么调?MADCTL 寄存器详解
很多人第一次点亮屏幕,发现图像是倒的、歪的、镜像的……别慌,这很正常。
这一切都由MADCTL(Memory Access Control)寄存器控制,地址是0x36。它是一个8位寄存器,每一位都有含义:
| Bit | 名称 | 功能 |
|---|---|---|
| 7 | MY | 行扫描顺序:1=从下往上 |
| 6 | MX | 列扫描顺序:1=从右往左 |
| 5 | MV | X/Y 是否交换:1=行列互换(实现90度旋转) |
| 4 | ML | 扫描方向:1=从底到顶逐行 |
| 3 | RGB | 接口颜色顺序:1=RGB,0=BGR(重要!) |
| 2:0 | - | 保留 |
常用组合举例:
| 值(十六进制) | 效果说明 |
|---|---|
0x00 | 默认横向,左上起点 |
0x60 | 旋转90度(适合竖屏) |
0xC0 | 旋转180度 |
0xA0 | 旋转270度 |
0x20 | 水平翻转 |
🎯 实战经验:如果你发现颜色偏蓝或偏红,很可能是 RGB/BGR 搞反了。试试把 bit3 取反。
常见问题排查清单:那些年我们一起踩过的坑
❌ 屏幕完全不亮?
- 检查供电是否正常(3.3V?有无短路?)
- RST 是否有效触发?可用万用表测复位电平变化
- 是否发送了
0x11和0x29?缺一不可 - SPI 是否工作?可用示波器看 SCK 是否有波形
❌ 显示花屏、错位、条纹?
CASET和RASET设置的坐标范围是否正确?- MADCTL 配置是否与物理安装方向一致?
- SPI 速率是否过高?面包板上超过 8MHz 就容易出错
❌ 颜色不对(发绿、发红)?
- RGB565 字节顺序是否颠倒?试试交换高低字节
- MADCTL 的 RGB/BGR 位是否正确?
- 测试纯色:
0xF800=红,0x07E0=绿,0x001F=蓝
❌ 刷新太慢卡顿?
- 放弃轮询写法,改用 DMA 或 SPI 双缓冲
- 减少全屏刷新,改为局部更新(Partial Update)
- 提高 SPI 主频至 10~15MHz(需确保线路质量)
工程级设计建议:不只是点亮,更要稳定可靠
当你不再满足于“能亮”,而是要做产品级应用时,以下几点值得重视:
✅ 电源去耦不能省
在 VCC 引脚附近加一个0.1μF 陶瓷电容,离模块越近越好。最好再并联一个 10μF 钽电容,吸收瞬态电流波动。
✅ 背光单独控制
BLK 引脚通常连接背光LED阴极。若电流较大(>100mA),建议用三极管或MOSFET驱动,避免直接由MCU引脚供电。
✅ 使用分层架构
构建清晰的软件结构:
底层:SPI 读写抽象 │ ├─ 中间层:ST7789 控制(初始化、窗口设置、旋转等) │ └─ 上层:图形库(绘制点线圆、文字、UI框架如LVGL)这样未来换平台或换屏幕都更容易移植。
✅ 区域更新优于全屏刷新
对于仅变动一小部分画面的应用(如仪表盘指针移动),只需重绘变化区域即可大幅降低带宽占用。
更进一步:结合 GUI 框架打造交互界面
一旦你能稳定驱动 ST7789,下一步就可以接入轻量级 GUI 框架,比如:
- LVGL:功能强大,支持触摸、动画、主题,适合 STM32/ESP32
- u8g2:资源占用极低,适合 AVR、nRF 等小内存设备
- TFT_eSPI + Arduino GFX:ESP32 上最受欢迎的组合之一
它们的背后,其实都是基于我们刚才讲的这套 SPI + 命令-数据模型。你现在打下的基础,正是通往更复杂应用的跳板。
写在最后:每一个高手,都曾从点亮第一块屏开始
你看,整个过程并没有那么神秘。从认识每一根线的作用,到理解命令与数据的区别,再到写出第一个FillScreen()函数——你已经走完了从“看不懂”到“能做到”的全过程。
技术从来不是天才的专利,而是坚持实践的结果。也许你现在连 SPI 是什么都还不太清楚,但只要动手接一次线、烧录一段代码、看到屏幕真的亮起来那一刻,那种成就感,足以让你爱上嵌入式开发。
如果你正在做一个天气站、音乐播放器、或是带界面的小工具,不妨加上这块 ST7789 屏。它不大,却能让你的作品瞬间生动起来。
如果你在调试过程中遇到问题,欢迎留言交流。我们一起解决下一个“黑屏”难题。