简介:ESP8266搭配MicroPython驱动ST7735TFT屏幕的硬件SPI方案,面向嵌入式显示开发者和物联网项目爱好者,重点解决模拟SPI刷屏速率偏低、硬件SPI接口分配不明确等实际问题。压缩包体积仅约4KB,共3个Python文件,分别承担FastTFT核心驱动、LCD封装模块与主程序入口,结构清晰、便于裁剪移植,可直接运行于烧录MicroPython固件的ESP8266。作者在更换硬件SPI接口后,发现刷屏速度并未随之改善,于是从铺色、画点、显示字符串等基础函数逐一优化,最终使刷新速率明显提升;同时说明ESP8266的硬件SPI有两个,其中一片连接flash供系统使用,用户只能使用另一片,设计时需注意引脚复用。资源还给出软件SPI替代方案,提醒spi.write函数须传入bytes类型、初始化相位与极性要对照ST7735数据手册确认,这些细节能帮助开发者避开常见错误。已有536人学习下载,适合正在调试小屏显示、想深挖SPI时序并提升TFT性能的MicroPython玩家。
1. 硬件SPI驱动ST7735:为什么别人30帧你只有8帧
一块128x160的ST7735小屏,接在ESP8266上,用软件SPI全屏刷新要一百多毫秒,画个桌面秒针一卡一顿;换硬件SPI,同一块屏、同一组飞线,刷新时间直接砍掉一半以上。这是我调某块屏幕时最直观的对比——瓶颈往往不在屏,而在数据怎么送进去。
这篇把 ESP8266 在 MicroPython 下用硬件 SPI(HSPI)驱动 ST7735 的完整链路讲透:引脚为什么不能乱接、驱动怎么从软件 SPI 迁过来、初始化序列差在哪、花屏红屏按什么顺序查。适合正在做桌面摆件、环境监测显示、简易仪表盘,嫌刷新慢又不想换主控的人。看完你能照着接线和代码跑起来,也知道每个参数在改什么、哪些地方是玄学。
2. ESP8266的HSPI引脚分配:GPIO15、片选与启动时序的硬约束
动手之前先把硬件约束定下来。软件 SPI 可以用任意 GPIO 模拟时序,硬件 SPI 不行——它绑定固定引脚,想反着来就只能换芯片。这一章把引脚、接线和上电时序一次讲清楚。
2.1 硬件SPI只有一组固定引脚,别传sck/mosi参数
ESP8266 的 SPI 外设实际有两组,SPI(0) 被板载 flash 占用,用户程序不要去碰;剩下 SPI(1) 也叫 HSPI,引脚固定为 GPIO14(时钟)、GPIO13(MOSI)、GPIO12(MISO)、GPIO15(片选)。MISO 对写 ST7735 没有用,但它仍然属于这个外设的管脚集合。这个固定映射意味着 ST7735 的 SCL、SDA、CS 三根线,必须接到上述 GPIO,不能自己改。
在 MicroPython 里初始化 HSPI 的写法很简。较新的固件直接用SPI(1)即可,部分旧教程会把sck、mosi等引脚参数传进去,在 ESP8266 上是无效参数,甚至直接抛ValueError。稳妥做法是只给频率和模式参数,引脚由外设自己接管:
from machine import SPI spi = SPI(1, baudrate=20000000, polarity=0, phase=0, bits=8, firstbit=SPI.MSB)baudrate是 SCLK 时钟频率,ST7735 数据手册标称最大十几 MHz,实际很多模块在 20MHz 甚至 27MHz 也能跑稳。先按 20MHz 起步,跑花屏了再降级排查。polarity和phase都设为 0,这是 ST7735 默认的 SPI 模式 0,四个组合里最省事的一个。
注意:ESP8266 的 SPI 构造函数不要传
sck/mosi/miso引脚参数,引脚是固定的。在 ESP32 上习惯写的SPI(id=1, baudrate=..., sck=Pin(...), mosi=Pin(...))在这块芯片上不适用,照抄会翻车。
清楚了 SPI(1) 的固定映射后,整个接线方案跟着定下来。
2.2 ST7735接线表:DC、RST和背光怎么选
ST7735 模块一般引出 8 个引脚:SCL、SDA、CS、DC(也叫 A0/RS)、RST、BL、VCC、GND。其中 SCL、SDA、CS 被硬件 SPI 锁死,剩下 DC、RST、BL 可以选择任意空闲 GPIO,唯一原则是不要碰启动约束脚。我常用的接线方案如下:
| ST7735引脚 | ESP8266引脚 | 说明 |
|---|---|---|
| SCL / SCK | GPIO14 | 硬件SPI时钟,固定 |
| SDA / MOSI | GPIO13 | 硬件SPI数据,固定 |
| CS | GPIO15 | 硬件SPI片选,固定,需要外部下拉 |
| DC / A0 | GPIO5 | 数据/命令选择,任意输出脚 |
| RST / RES | GPIO4 | 复位脚,任意输出脚 |
| BL / 背光 | GPIO16 | 常亮接法,想控制亮度再接三极管 |
| VCC | 3.3V | 必须3.3V,不能接5V |
| GND | GND | 与ESP8266共地 |
DC 和 RST 选 GPIO4、GPIO5 是因为这两个脚在 boot 阶段没有特殊要求,随便折腾不担心起不来。背光最省事的接法是直接串一个限流电阻到 3.3V,屏幕常亮;如果想要软件开关,接 GPIO16 最安全。注意避开 GPIO0 和 GPIO2——这两个脚在 ESP8266 上电时有 boot 模式约束,外部器件若有下拉或强拉状态,可能让芯片进不了正常启动模式。
2.3 GPIO15的下拉电阻不是可选项:上电时序与启动失败
这是整个接线里最容易忽略、也最像玄学的一条。现象是:接线完全按图来,代码也没问题,但 ESP8266 上电后就是起不来,或者串口里反复打印 boot 报错、无限重启。查了半天发现是 CS 引脚的问题。
原因在于 GPIO15 在 ESP8266 启动阶段必须保持低电平,否则芯片认为自己进入了不该进的启动模式。而很多 ST7735 模块的 CS 引脚内部自带弱上拉,目的是让屏幕在上电瞬间不被误选中。这一上拉,恰好把 GPIO15 抬高了,启动直接失败。解决方式很简单:在 GPIO15 与 GND 之间加一个 10kΩ 下拉电阻,强制启动时拉低。电阻不用太讲究,4.7kΩ 到 20kΩ 我都用过,都能正常启动。
还有一个更隐蔽的时序:SPI 外设初始化前,GPIO15 由固件接管,如果此时 CS 处于高电平,屏幕会把后续的命令字节当成数据,表现就是屏幕亮了但内容全是乱码或雪花。所以驱动类里第一件事就是把 CS 引脚初始化为高电平,等到真正发数据时再拉低,这既是电气要求,也是逻辑要求。后面驱动代码里我会把这个细节写进去。
3. 让MicroPython驱动走硬件SPI:从软件SPI迁移到HSPI的完整改法
网上能找到的大部分 ST7735 驱动,默认用的是SoftSPI,因为软件 SPI 在 ESP32、ESP8266、RP2040 上都能随意指定引脚,通用性好。但对 ESP8266 来说,软件 SPI 的每字节都要靠 GPIO 翻转模拟,CPU 占用高,全屏刷新明显吃力。迁移到硬件 SPI 并不需要重写驱动,核心改动只有两处:初始化方式,以及spi.write()的调用习惯。
3.1 软件SPI与硬件SPI的差异:为什么驱动代码不用大改
软件 SPI 和硬件 SPI 在驱动类里都表现为一个spi对象,都有write()方法。驱动本身不看底层实现,只知道自己要“把这一串字节发给屏幕”。所以迁移时驱动代码不动,只要把传入的 SPI 对象换掉即可。但有一个地方容易踩坑:某些驱动里会调用spi.init()重新设置参数,ESP8266 的硬件 SPI 在运行中重新init()可能会有异常行为,建议把驱动里所有spi.init()调用删掉,参数只在构造时给一次。
两者的实际差异可以用一张表直观看出:
| 对比项 | SoftSPI | 硬件SPI(1) |
|---|---|---|
| 引脚选择 | 任意GPIO | GPIO14/13/12/15固定 |
| 每字节CPU占用 | 高,逐位翻转 | 外设处理,CPU很低 |
| 全屏(160x128)实测刷新 | 约200ms | 约60-80ms(20MHz) |
| 适合场景 | 只显示静态文字 | 动态仪表、波形、秒针 |
这个刷新时间是同一块屏幕上我实测的量级,具体数值和固件版本、GC 状态有关,但比例关系是确定的。硬件 SPI 的优势不只是快,更在于写数据时 CPU 不用一直盯着引脚,MicroPython 的 GC 卡顿和绘制卡顿不会叠加在一起,体感流畅度提升比数字上看到的还明显。
3.2 一个基于HSPI的ST7735最小驱动类
下面这个驱动类只保留核心功能:命令/数据发送、窗口设置、矩形填充、整块 buffer 发送。它不依赖任何第三方库,用 MicroPython 标准库即可:
from machine import Pin, SPI import time class ST7735: def __init__(self, spi, cs, dc, rst, w=128, h=160): self.spi = spi self.cs = Pin(cs, Pin.OUT, value=1) self.dc = Pin(dc, Pin.OUT, value=0) self.rst = Pin(rst, Pin.OUT, value=0) self.w = w self.h = h def _cmd(self, code, data=None): # 命令阶段DC为低,数据阶段DC为高 self.cs.value(0) self.dc.value(0) self.spi.write(bytes([code])) if data is not None: self.dc.value(1) if isinstance(data, int): data = bytes([data]) self.spi.write(data) self.cs.value(1) def reset(self): self.rst.value(0) time.sleep_ms(20) self.rst.value(1) time.sleep_ms(150) def init(self): self.reset() self._cmd(0x01) # SWRES 软复位 time.sleep_ms(150) self._cmd(0x11) # SLPOUT 退出睡眠 time.sleep_ms(200) self._cmd(0x36, 0xC8) # MADCTL 行列方向+BGR self._cmd(0x3A, 0x05) # COLMOD 16位颜色 self._cmd(0x20) # INVOFF 关闭颜色反转 self._cmd(0x13) # NORON 进入正常显示模式 time.sleep_ms(10) self._cmd(0x29) # DISPON 开启显示 time.sleep_ms(100) def window(self, x0, y0, x1, y1): # 设置RAM读写区域,坐标按ST7735的列/行寄存器写入 self._cmd(0x2A, bytes([x0 >> 8, x0 & 0xFF, x1 >> 8, x1 & 0xFF])) self._cmd(0x2B, bytes([y0 >> 8, y0 & 0xFF, y1 >> 8, y1 & 0xFF])) self._cmd(0x2C) # RAMWR 开始写显存 def fill_rect(self, x, y, w, h, color): hi, lo = color >> 8, color & 0xFF self.window(x, y, x + w - 1, y + h - 1) chunk = bytes([hi, lo]) * 4096 # 4K像素块,控制单次内存占用 total = w * h mv = memoryview(chunk) self.cs.value(0) # RAMWR期间CS保持低,不打断传输 while total: n = min(total, 4096) self.spi.write(mv[:n * 2]) total -= n self.cs.value(1) def clear(self, color=0): self.fill_rect(0, 0, self.w, self.h, color)这个类里有两个设计细节值得说明。第一个是_cmd()里对data的处理:命令阶段DC置低,数据阶段DC置高,这是 ST7735 区分“这是命令还是参数”的唯一方式,顺序不能反。第二个是fill_rect()里的分块发送:ESP8266 可用 RAM 只有约 50KB 出头,如果一次性生成 128x160x2=40KB 的像素字节数组,很容易把 MicroPython 堆撑爆,即使勉强成功,GC 也会反复触发造成明显卡顿。分成 4K 像素块循环发送,单次内存占用只有 8KB,代价是多几次spi.write()调用,在 20MHz 下这点开销可以忽略。
使用这段驱动的代码也很简单:
from machine import Pin, SPI spi = SPI(1, baudrate=20000000, polarity=0, phase=0, bits=8, firstbit=SPI.MSB) lcd = ST7735(spi, cs=15, dc=5, rst=4) lcd.init() lcd.clear(0x001F) # 填充成蓝色,验证驱动是否工作如果接线没错、初始化正常,上电后屏幕应该整屏变蓝。如果颜色不对、方向不对或出现花屏,先不要怀疑代码,去查初始化序列和 MADCTL 参数,下一节专门讲这个。
3.3 初始化序列差异:老款ST7735R与新款IPS屏的参数怎么调
ST7735 最坑的地方在于,不同批次、不同厂家的模块,初始化序列不完全相同。网上流传的初始化代码大多是老款 ST7735R 的参数,直接套到新款 IPS 屏上,常见表现就是颜色发灰、红蓝颠倒、或者整屏发紫。原因有几个:
一是颜色反转位。老款 ST7735R 通常需要INVON(0x21)打开反转,新款屏幕很多默认不反转,用INVOFF(0x20)更正常。如果显示出来的内容像照片底片一样,把初始化里的0x20换成0x21试一次。
二是 MADCTL 的 BGR 位。我上面的初始化序列写的是0xC8,二进制是1100 1000,bit3 为 1 表示 BGR 颜色顺序。如果你的屏幕显示纯红色时变成了纯蓝色,把0xC8改成0xC0再试。这两个值只差一位,但颜色顺序完全相反。
三是 SLPOUT 后的延时。某些模块芯片对延时很敏感,复位后进入睡眠退出状态需要时间,SLPOUT之后至少要等 120ms 再做后续设置。我习惯给 200ms,确保寄存器稳定。初始化序列看起来像“照着抄就行”,其实是最需要逐个参数验证的地方,改一位重启一次看效果,不丢人。
4. 绘制与刷新测试:用色块、文本和局部刷新验证驱动
驱动的初始化通过了,接下来要验证的是绘制接口和颜色格式是否正确。这一章的思路是:先做最基础的纯色验证,再做文本显示,最后引入局部刷新。每一步都有明确的检查目标,错了也知道看哪里。
4.1 上电验证顺序:红绿蓝三色块快速定位问题
第一次上电不要急着画复杂图形,先把纯红、纯绿、纯蓝三个颜色各刷一屏,用肉眼确认三件事:颜色是否准确、扫描方向是否正确、显示区域是否完整。测试代码就是一个循环:
import time while True: lcd.clear(0xF800) # 红色 565格式: R=31 G=0 B=0 time.sleep(1) lcd.clear(0x07E0) # 绿色 565格式: R=0 G=63 B=0 time.sleep(1) lcd.clear(0x001F) # 蓝色 565格式: R=0 G=0 B=31 time.sleep(1)如果红蓝互换,说明 MADCTL 的 BGR 位反了,按上一节的方法改0xC8/0xC0。如果颜色看起来“脏”、不纯,大概率是INVON/INVOFF选错,或者屏幕缺了 gamma 校准参数。如果颜色正常但画面只占了一部分,那是窗口偏移问题,留到避坑章讲。
这三个颜色还有一个额外作用:快速确认 565 格式的字节序。驱动里fill_rect()发送的是[高字节, 低字节],如果你的屏幕期待的是低字节在前,纯色块会变成另一种颜色,但不会花屏。这一点很容易被当成“颜色设置不对”,其实是数据字节序问题。
4.2 让framebuffer输出文本:从局部buffer推到屏幕
ST7735 驱动本身不内置字库,最省事的文本方案是用 MicroPython 自带的framebuffer模块。它有内置 8x8 像素的 ASCII 字模,text()方法可以直接把字符串渲染到一块内存 buffer 里。注意不要为整个屏幕开 framebuffer——128x160x2 就是 40KB,ESP8266 会非常紧张。我一般只开一个 64x64 的小区域,渲染完再推给屏幕。
先给驱动补一个blit()方法,负责把 framebuffer 的字节内容送到指定区域:
def blit(self, buf, x, y, w, h): self.window(x, y, x + w - 1, y + h - 1) mv = memoryview(buf) chunk = 2048 self.cs.value(0) for i in range(0, w * h * 2, chunk): self.spi.write(mv[i:i + chunk]) self.cs.value(1)然后配合 framebuffer 使用:
import framebuffer buf = bytearray(64 * 64 * 2) fb = framebuffer.FrameBuffer(buf, 64, 64, framebuffer.RGB565) fb.fill(0x0000) fb.text("12:00", 8, 12, 0xFFFF) lcd.blit(buf, 10, 10, 64, 64) # 把64x64区域放到屏幕(10,10)处这里有一个必须面对的坑:MicroPythonframebuffer的 RGB565 是小端字节序,即颜色低字节在前;而大部分 ST7735 驱动习惯按高字节在前发送。直接blit()上去,文本颜色会红蓝互换。我的做法是把字节序交换放进blit(),或者单独写一个blit_swap():
def blit_swap(self, buf, x, y, w, h): self.window(x, y, x + w - 1, y + h - 1) mv = memoryview(buf) tmp = bytearray(256) for off in range(0, w * h * 2, 256): end = min(off + 256, w * h * 2) tmp[:end - off] = mv[off:end] for i in range(0, end - off - 1, 2): tmp[i], tmp[i + 1] = tmp[i + 1], tmp[i] self.cs.value(0) self.spi.write(tmp[:end - off]) self.cs.value(1)每一对字节交换一次,再整块发出。代价是 Python 层的逐字节交换,但只对局部区域做,开销远小于一次全屏刷新。文本、图标这类小区域更新,用blit_swap()就够了。
4.3 局部刷新:SPI提速之外的第二条腿
硬件 SPI 把单次全屏刷新从 200ms 降到 60-80ms,但对“秒针”“跳动的数字”“波形”这类应用,全屏刷新还是太慢。真正的提速靠局部刷新——每次只更新变化的那一小块区域,把 40KB 的传输量降到几百字节。
画一个秒表页面,传统做法是每秒全屏重绘:先清屏,再画表盘、画刻度、画指针。局部刷新的做法是:表盘和刻度画一次就不再动,每秒只把旧指针区域用背景色覆盖,再在新位置画指针。传输的数据量从 40KB 变成几百字节,刷新耗时直接降到个位数毫秒。
def redraw_hand(fb_buf, old_x, old_y, new_x, new_y): fb_buf.fill(0x0000) # 清空局部buffer为背景色 lcd.blit_swap(fb_buf, old_x, old_y, 32, 32) # 擦掉旧指针 fb_buf.fill(0x0000) fb_buf.line(16, 16, new_x, new_y, 0xFFFF) # 画新指针 lcd.blit_swap(fb_buf, new_x - 16, new_y - 16, 32, 32)这种做法对 framebuffer 不友好,因为 framebuffer 设计成整块重绘,不支持“只改一行像素”。实践中我会维护一个 32x32 的局部 buffer,指针落在哪个区域,就只更新那个区域。脏矩形范围越小,刷新越快,这是嵌入式显示优化的通用思路。
5. 花屏、红屏、启动失败排查:ST7735驱动避坑记录
这一章是血泪经验汇总。以下问题我都在不同模块上真实遇到过,按“现象 → 原因 → 解决”的顺序写,排查时从上往下走。
5.1 背光亮但屏幕无任何内容
现象:上电后背光正常亮,屏幕却是一片死黑或死白,发送clear(0xF800)没有任何反应。
原因通常是两个:一是 RST 引脚没有正确复位,屏幕停留在未知状态。有些模块的 RST 引脚如果悬空,驱动初始化时reset()会对一个没接的引脚操作,等于没复位。二是 SPI 模式不对,ST7735 必须用模式 0,如果初始化时设成了polarity=1或phase=1,控制器收不到正确的命令时序。
解决:先确认 RST 确实接到了 GPIO4,并在init()里先拉低 20ms 再拉高;然后把 SPI 参数锁定为polarity=0, phase=0。这两步都正确后,屏幕至少应该能显示纯色块。
5.2 全屏偏红或背景发紫
现象:屏幕能显示内容,但整个画面蒙着一层红色或紫色,白色区域呈现出明显的色偏。
原因:这是初始化序列不完整最典型的表现。很多 ST7735 模块是 ST7735S 芯片,比老款 ST7735R 多需要几组电源控制寄存器和 gamma 设置;只发基础指令虽然能点亮,但内部电压和偏置没调好,颜色就不正。另外INVON/INVOFF选择错误也会导致颜色发灰、发紫。
解决:先切换INVON和INVOFF试一次,因为这是一行代码的事。如果仍偏色,补上 PWCTR1、PWCTR2、VMCTR1 和 gamma 寄存器组,参数可以找同型号模块卖家给的初始化代码,各厂家之间大同小异。
5.3 显示区域偏移或只有一部分画面
现象:颜色和内容都正常,但画面整体偏移,比如左边少了一列、下边多了一行,或者只有左上角一块有内容。
原因:ST7735 的显示 RAM 是 132x162,屏幕显示窗口的初始偏移因模块而异。控制偏移的是 CASET(0x2A)和 RASET(0x2B)里的起始坐标。我的驱动里window()是按从(0,0)开始计算的,如果模块本身偏移是(2,1),画面就会整体错位。
解决:不需要改驱动逻辑,在init()或window()里对坐标加上偏移量即可。先试偏移(1,1),再试(2,1),大多数 128x160 的模块在这两种之间。如果内容显示不全,还要检查分辨率是否真的 128x160,有些廉价模块实际是 128x128 的简化版。
5.4 高频花屏:baudrate不是越高越好
现象:20MHz 下画面正常,把baudrate调到 40MHz 后出现雪花、横条纹,颜色随机错乱。
原因:ST7735 的实际最大 SPI 时钟受 PCB 布线、飞线长度、模块供电影响很大。数据手册标称值通常是理想条件,飞线一长,信号边沿变差,40MHz 下采样点不稳定就花屏。另外,模块的 VCC 如果和 ESP8266 的 3.3V 共用一个不稳的电源,花屏概率更高。
解决:先把baudrate降回 20MHz 验证。如果必须用高频率,缩短飞线、把杜邦线换成短线或漆包线,并在模块 VCC 和 GND 之间加一个 10-100uF 电容。花屏问题里,供电不稳和频率过高至少占七成。
5.5 上电不启动或反复重启
现象:接入 ST7735 后 ESP8266 完全无法启动,串口输出乱码或不断打印 boot 失败信息。
原因:几乎都是 GPIO15 被拉高导致的,这在第 2 章已经强调过。ST7735 模块的 CS 内部上拉把 GPIO15 抬到了高电平,ESP8266 在启动采样时以为进入了错误模式。还有一种情况是 GPIO16 接了背光,但背光模块内部有下拉,把启动采样引脚拉出问题。
解决:GPIO15 与 GND 之间加 10kΩ 下拉电阻;背光改用 GPIO16 或直接接 3.3V 常亮;排查时先断开屏幕所有连线,只留 SCK、MOSI、CS,确认能启动后再逐一恢复 DC、RST、BL。这种问题排查起来最费时间,因为现象在启动阶段出现,代码根本来不及跑。
6. 把刷新率榨到极限:块写入、预分配buffer与脏矩形习惯
硬件 SPI 迁移完成了,显示也正常了,最后一步是把动态界面的流畅度再往上推一档。这里我给三个我自己养成习惯的做法。
第一,所有发送缓冲在启动时一次性预分配,不要每次绘制时bytearray()。MicroPython 的内存分配会在堆上产生碎片,反复分配会触发频繁 GC,导致 SPI 传输间隙出现停顿。我的做法是模块级定义几个固定大小的bytearray,颜色填充用 8KB 块,framebuffer 用 64x64x2 的区域,全程复用:
_FILL_BUF = bytearray(4096 * 2) _TEX_BUF = bytearray(64 * 64 * 2)第二,把“全屏重绘”拆成“脏矩形批次”。比如一个时钟界面,每秒真正变化的只有秒针所在的 32x32 区域,顶多再加上日期文字所在的 64x16 区域。每次刷新只对这两个区域发数据,从 40KB 降到约 4KB,硬件 SPI 的优势才真正体现出来。
第三,如果绘制场景很复杂,优先用局部 framebuffer 做合成,再一次性 blit,而不是让驱动提供几十个绘图原语。framebuffer 画好后,blit_swap()一次推给屏幕,事务干净,中间状态不会闪烁。
我自己的桌面秒表项目就是这么做起来的:开盘时整屏刷新一次表盘,之后每秒只有指针区域在变,肉眼看起来就是实时响应。之前用软件 SPI 硬扛,指针一卡一顿,后来理解块写入和脏矩形,才真正把 ESP8266 这块小屏玩明白。如果你也卡在同一道坎上,按这个顺序调一遍,多半能省下不少后悔药的时间。希望帮到你。
本文还有配套的精品资源,点击获取