MicroPython 驱动 ST7789:从 SPI 通信到屏幕点亮的实战全解析
你有没有遇到过这种情况——手头有一块小小的圆形彩屏,引脚标着SCL、SDA、DC、RST,网上搜了一堆代码,复制粘贴后屏幕不是白屏就是花屏?明明用的是 MicroPython,语法简单了,怎么控制一块 TFT 屏反而比写个 LED 闪烁还难?
问题不在于你不会编程,而在于大多数示例代码只告诉你“怎么做”,却没讲清楚“为什么这么做”。尤其是像ST7789这类需要复杂初始化流程的显示驱动芯片,盲目调用封装好的库函数,一旦出错就无从下手。
今天我们就来一次彻底拆解:不用任何第三方库,从零开始,用原生 MicroPython 精确控制 ST7789 液晶屏。目标不仅是让屏幕亮起来,更要让你明白每一行代码背后的硬件逻辑。
为什么是 ST7789?它凭什么成为小尺寸彩屏的首选?
在当前的嵌入式开发中,图形界面不再是奢侈功能。无论是 DIY 温湿度监测仪、音频可视化设备,还是智能手表原型,一块能显示彩色内容的小屏幕极大提升了交互体验。
而在这类应用中,ST7789几乎成了默认选项。原因很简单:
- 支持高达240×320 分辨率
- 色彩深度达65,536 色(RGB565)
- 可适配多种外形:矩形、圆形(常见 240x240)
- 接口简洁:仅需 5 个 GPIO 引脚即可驱动
相比老一代驱动如 ILI9341,ST7789 更轻量、响应更快,且对现代 MCU 的 SPI 总线利用率更高。更重要的是,它的初始化序列虽然精细,但结构清晰,非常适合通过 MicroPython 实现底层控制。
核心机制:SPI 是如何把数据“送进”屏幕的?
要搞懂 ST7789 的工作方式,先得理解它和主控之间的通信桥梁 ——SPI 协议。
SPI 不只是“发数据”,而是有节奏的对话
很多人以为 SPI 就是“不停地往 MOSI 发数据”,其实不然。ST7789 并不能自动分辨哪段是命令、哪段是参数。它依赖额外的控制信号来建立上下文。
我们使用的四线 SPI + 两个 GPIO 控制组合如下:
| 信号线 | 功能说明 |
|---|---|
SCK(SCLK) | 时钟线,由主控生成,决定传输速率 |
MOSI | 主机输出,发送命令和像素数据 |
CS(Chip Select) | 片选,低电平有效,表示开始一次通信 |
DC(Data/Command) | 关键!高=数据,低=命令 |
RST(Reset) | 硬件复位引脚,确保上电状态一致 |
⚠️ 注意:尽管名字叫“四线 SPI”,实际需要6 条物理连接(含 DC 和 RST),别漏接!
数据与命令切换:DC 引脚的灵魂作用
这是最容易被忽视的一点:ST7789 所有的操作都始于一个字节的命令码。比如你想设置显示区域,必须先发0x2A(列地址设定),再跟上起始列和结束列的数据。
如果没有正确切换DC引脚的状态,芯片就会误解你的意图 —— 把本该是命令的内容当成数据写入显存,结果就是花屏或无响应。
举个生活化的比喻:
你可以把 SPI 通信想象成去银行办业务。
-CS是叫号机取号(开始服务)
-DC是你递上的单据类型:红色单据=开户(命令),蓝色单据=存款金额(数据)
-MOSI是你填写的信息内容
如果柜员拿错了单据颜色,哪怕内容是对的,也会办错事。
初始化不是“走流程”,而是给屏幕“校准生命体征”
很多开发者发现,即使接线正确、代码运行无报错,屏幕依然不亮。最常见的原因就是:初始化序列错误或缺失关键步骤。
ST7789 上电后处于未知状态,内部电源、伽马曲线、内存映射等全部需要重新配置。这个过程就像给一台刚组装好的电脑安装操作系统和驱动程序。
正确的初始化顺序至关重要
以下是经过验证的标准流程骨架:
def init_display(): reset() # 第一步:硬件复位 time.sleep_ms(150) # 等待内部电路稳定 # 开始发送配置命令 write_cmd(0x36) # 内存访问控制 write_data(b'\x00') # 设置方向:横向、正常扫描 write_cmd(0x3A) # 像素格式设置 write_data(b'\x05') # 16位色(RGB565) # 电源管理相关配置... write_cmd(0xB7); write_data(b'\x35') write_cmd(0xBB); write_data(b'\x19') write_cmd(0xC0); write_data(b'\x2C') write_cmd(0xC6); write_data(b'\x0F') # 60Hz帧率 # 伽马校正(影响色彩表现) write_cmd(0xE0); write_data(b'\xD0\x04\x0D\x11\x13\x2B\x3F\x54\x4C\x18\x0D\x0B\x1F\x23') write_cmd(0xE1); write_data(b'\xD0\x04\x0C\x11\x13\x2C\x3F\x44\x51\x2F\x1F\x1F\x3F\x3F') write_cmd(0x11) # 退出睡眠模式 time.sleep_ms(120) # 必须等待至少 120ms! write_cmd(0x29) # 开启显示看到这么多write_cmd和write_data,是不是觉得眼花缭乱?其实它们可以分为几类:
| 类型 | 典型命令 | 作用 |
|---|---|---|
| 显示控制 | 0x11,0x29 | 控制睡眠/唤醒、开显示 |
| 内存配置 | 0x36,0x3A | 设置旋转方向、颜色格式 |
| 电源管理 | 0xB7,BB,C0 | 设置 VCOM、VGHL 电压 |
| 刷新率 | 0xC6 | 设定帧频(影响功耗与流畅度) |
| 伽马调节 | 0xE0,0xE1 | 调整色彩对比度与饱和度 |
其中最易出错的是0x3A和0x36:
- 若将0x3A设为0x03,则启用 12 位色,颜色严重失真;
- 若0x36参数设置不当,图像会倒置、镜像甚至偏移。
这些参数并非“通用”,不同厂商的模组可能略有差异。建议首次使用时查阅模块说明书,或参考卖家提供的 Arduino 示例。
底层通信封装:构建可靠的命令通道
所有高级操作都建立在两个基础函数之上:发送命令和发送数据。
下面是基于 ESP32 或 RP2040 的标准实现:
from machine import Pin, SPI import time # 初始化 SPI(Mode 0,最高支持 40MHz) spi = SPI(1, baudrate=40_000_000, polarity=0, phase=0, sck=Pin(14), mosi=Pin(13), miso=None) # 控制引脚定义 cs = Pin(12, Pin.OUT, value=1) # 默认禁用片选 dc = Pin(15, Pin.OUT, value=0) rst = Pin(11, Pin.OUT, value=1) def write_cmd(cmd): cs.off() # 拉低 CS,选中设备 dc.off() # DC=0,表示接下来是命令 spi.write(bytes([cmd])) cs.on() # 完成通信,释放总线 def write_data(buf): cs.off() dc.on() # DC=1,表示接下来是数据 spi.write(buf) cs.on()✅ 最佳实践:每次通信后立即拉高
CS,避免干扰其他 SPI 设备。
这两个函数构成了整个驱动的基石。后续所有绘图、清屏、设窗口操作,都是它们的组合调用。
如何真正“画”出第一个像素?GRAM 访问全流程
终于到了激动人心的时刻:我们要向屏幕写入真正的图像数据。
但请注意,ST7789不会自动刷新整个屏幕。你必须明确告诉它:“我要更新哪个区域”,然后才能开始传像素。
Step 1:定义绘图窗口(CASET & RASET)
def set_window(x0, y0, x1, y1): write_cmd(0x2A) # Column Address Set write_data( bytes([x0 >> 8, x0 & 0xFF, x1 >> 8, x1 & 0xFF]) ) write_cmd(0x2B) # Row Address Set write_data( bytes([y0 >> 8, y0 & 0xFF, y1 >> 8, y1 & 0xFF]) )这相当于划定一块“画布”。例如要更新全屏(240x240 圆形屏):
set_window(0, 0, 239, 239)Step 2:进入“写 GRAM”模式(0x2C)
write_cmd(0x2C) # Memory Write此后所有通过write_data()发送的数据都会被视为连续的 RGB565 像素值,并按行依次填入 GRAM。
Step 3:发送像素流
假设我们要填充整个屏幕为红色(RGB565 中红色为0xF800):
# 构造一个红色像素(大端格式) red_pixel = b'\xF8\x00' # 填充整个 240x240 屏幕(共 57,600 像素) buffer = red_pixel * (240 * 240) write_data(buffer)⚠️ 警告:直接构造如此大的缓冲区会在内存紧张的设备(如 ESP32-S2)上引发MemoryError。生产环境中应分块发送或使用framebuf预渲染。
常见坑点与调试秘籍
❌ 白屏 / 花屏 → 检查这三个地方
SPI 速率太高
杜邦线+面包板系统建议不超过 20MHz;PCB 板载可尝试 40MHz。DC 引脚接反或未连接
这是最常见的接线错误。可用万用表测量电平变化确认。缺少关键延时
尤其是在0x11(Sleep Out)之后,必须等待 ≥120ms 才能发0x29。
🎨 颜色异常(偏绿/倒色)→ 查看字节序与扫描方向
- RGB565 数据是否以大端(MSB 在前)发送?
0x36命令中的MY(行反转)、MX(列反转)、MV(行列交换)位是否正确?
例如,对于常见的竖屏显示,可能需要:
write_cmd(0x36) write_data(b'\x60') # MX=1, MV=1 → 旋转 90°🐢 刷新卡顿 → 优化策略在这里
- 批量写入优于逐像素更新:合并多个小
write_data调用 - 使用 framebuf 模块预合成图像:
import framebuf # 创建内存缓冲区 fb = framebuf.FrameBuffer(bytearray(240*240*2), 240, 240, framebuf.RGB565) fb.fill(0) # 清屏黑 fb.text("Hello!", 100, 100, 1) # 写文字 write_data(fb.buffer()) # 一次性刷屏这种方式显著减少 SPI 事务开销,提升响应速度。
工程级建议:如何写出稳定可维护的驱动代码?
当你准备将这段代码用于正式项目时,请考虑以下几点:
1. 封装成类,提高复用性
class ST7789: def __init__(self, spi, cs, dc, rst, width=240, height=240): self.spi = spi self.cs = cs self.dc = dc self.rst = rst self.width = width self.height = height self.init_display() def write_cmd(self, cmd): ... def write_data(self, buf): ... def set_window(self, x0, y0, x1, y1): ... def fill_screen(self, color): ...这样可以在不同项目中轻松替换引脚或分辨率。
2. 添加错误日志与降级模式
在初始化失败时尝试降低 SPI 频率重试,或切换至基本黑白模式提示用户。
3. 注意电源设计
ST7789 工作电流可达 50mA 以上,尤其在全亮白色时。建议:
- 使用独立 LDO 供电
- 并联 10μF + 0.1μF 电容滤波
- 避免与电机、继电器共用电源路径
4. PCB 布局建议
- SPI 走线尽量短且平行
- 远离高频信号线(如 Wi-Fi 天线)
RST和DC引脚加 10kΩ 上拉电阻防误触发
结语:掌握原理,才能自由创造
现在你应该已经明白,驱动一块 ST7789 屏幕并不是神秘的技术黑箱。它的每一个行为都有迹可循:
- 通过 SPI 发送命令 → 配置内部寄存器
- 设置地址窗口 → 锁定绘图区域
- 写入 RGB565 数据流 → 更新显存
- 自动刷新机制 → 输出到液晶面板
当你不再依赖“别人写的库能不能用”,而是能够根据数据手册自行调整初始化参数、修复颜色偏差、优化刷新性能时,你就真正掌握了嵌入式图形开发的核心能力。
下次当你看到一块陌生的 TFT 模块,也可以自信地说:“让我看看它的驱动型号,我能搞定。”
如果你正在做一个基于 ESP32 或 Raspberry Pi Pico 的可视化项目,不妨试试亲手点亮这块小彩屏。你会发现,原来 GUI 并不远,就在你敲下的每一行write_cmd()里。