ESP8266硬件SPI驱动ST7735:从接线到局部刷新优化 简介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 下用硬件 SPIHSPI驱动 ST7735 的完整链路讲透引脚为什么不能乱接、驱动怎么从软件 SPI 迁过来、初始化序列差在哪、花屏红屏按什么顺序查。适合正在做桌面摆件、环境监测显示、简易仪表盘嫌刷新慢又不想换主控的人。看完你能照着接线和代码跑起来也知道每个参数在改什么、哪些地方是玄学。2. ESP8266的HSPI引脚分配GPIO15、片选与启动时序的硬约束动手之前先把硬件约束定下来。软件 SPI 可以用任意 GPIO 模拟时序硬件 SPI 不行——它绑定固定引脚想反着来就只能换芯片。这一章把引脚、接线和上电时序一次讲清楚。2.1 硬件SPI只有一组固定引脚别传sck/mosi参数ESP8266 的 SPI 外设实际有两组SPI(0) 被板载 flash 占用用户程序不要去碰剩下 SPI(1) 也叫 HSPI引脚固定为 GPIO14时钟、GPIO13MOSI、GPIO12MISO、GPIO15片选。MISO 对写 ST7735 没有用但它仍然属于这个外设的管脚集合。这个固定映射意味着 ST7735 的 SCL、SDA、CS 三根线必须接到上述 GPIO不能自己改。在 MicroPython 里初始化 HSPI 的写法很简。较新的固件直接用SPI(1)即可部分旧教程会把sck、mosi等引脚参数传进去在 ESP8266 上是无效参数甚至直接抛ValueError。稳妥做法是只给频率和模式参数引脚由外设自己接管from machine import SPI spi SPI(1, baudrate20000000, polarity0, phase0, bits8, firstbitSPI.MSB)baudrate是 SCLK 时钟频率ST7735 数据手册标称最大十几 MHz实际很多模块在 20MHz 甚至 27MHz 也能跑稳。先按 20MHz 起步跑花屏了再降级排查。polarity和phase都设为 0这是 ST7735 默认的 SPI 模式 0四个组合里最省事的一个。注意ESP8266 的 SPI 构造函数不要传sck/mosi/miso引脚参数引脚是固定的。在 ESP32 上习惯写的SPI(id1, baudrate..., sckPin(...), mosiPin(...))在这块芯片上不适用照抄会翻车。清楚了 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 / SCKGPIO14硬件SPI时钟固定SDA / MOSIGPIO13硬件SPI数据固定CSGPIO15硬件SPI片选固定需要外部下拉DC / A0GPIO5数据/命令选择任意输出脚RST / RESGPIO4复位脚任意输出脚BL / 背光GPIO16常亮接法想控制亮度再接三极管VCC3.3V必须3.3V不能接5VGNDGND与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)引脚选择任意GPIOGPIO14/13/12/15固定每字节CPU占用高逐位翻转外设处理CPU很低全屏(160x128)实测刷新约200ms约60-80ms20MHz适合场景只显示静态文字动态仪表、波形、秒针这个刷新时间是同一块屏幕上我实测的量级具体数值和固件版本、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, w128, h160): self.spi spi self.cs Pin(cs, Pin.OUT, value1) self.dc Pin(dc, Pin.OUT, value0) self.rst Pin(rst, Pin.OUT, value0) self.w w self.h h def _cmd(self, code, dataNone): # 命令阶段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, color0): self.fill_rect(0, 0, self.w, self.h, color)这个类里有两个设计细节值得说明。第一个是_cmd()里对data的处理命令阶段DC置低数据阶段DC置高这是 ST7735 区分“这是命令还是参数”的唯一方式顺序不能反。第二个是fill_rect()里的分块发送ESP8266 可用 RAM 只有约 50KB 出头如果一次性生成 128x160x240KB 的像素字节数组很容易把 MicroPython 堆撑爆即使勉强成功GC 也会反复触发造成明显卡顿。分成 4K 像素块循环发送单次内存占用只有 8KB代价是多几次spi.write()调用在 20MHz 下这点开销可以忽略。使用这段驱动的代码也很简单from machine import Pin, SPI spi SPI(1, baudrate20000000, polarity0, phase0, bits8, firstbitSPI.MSB) lcd ST7735(spi, cs15, dc5, rst4) lcd.init() lcd.clear(0x001F) # 填充成蓝色验证驱动是否工作如果接线没错、初始化正常上电后屏幕应该整屏变蓝。如果颜色不对、方向不对或出现花屏先不要怀疑代码去查初始化序列和 MADCTL 参数下一节专门讲这个。3.3 初始化序列差异老款ST7735R与新款IPS屏的参数怎么调ST7735 最坑的地方在于不同批次、不同厂家的模块初始化序列不完全相同。网上流传的初始化代码大多是老款 ST7735R 的参数直接套到新款 IPS 屏上常见表现就是颜色发灰、红蓝颠倒、或者整屏发紫。原因有几个一是颜色反转位。老款 ST7735R 通常需要INVON(0x21)打开反转新款屏幕很多默认不反转用INVOFF(0x20)更正常。如果显示出来的内容像照片底片一样把初始化里的0x20换成0x21试一次。二是 MADCTL 的 BGR 位。我上面的初始化序列写的是0xC8二进制是1100 1000bit3 为 1 表示 BGR 颜色顺序。如果你的屏幕显示纯红色时变成了纯蓝色把0xC8改成0xC0再试。这两个值只差一位但颜色顺序完全相反。三是 SLPOUT 后的延时。某些模块芯片对延时很敏感复位后进入睡眠退出状态需要时间SLPOUT之后至少要等 120ms 再做后续设置。我习惯给 200ms确保寄存器稳定。初始化序列看起来像“照着抄就行”其实是最需要逐个参数验证的地方改一位重启一次看效果不丢人。4. 绘制与刷新测试用色块、文本和局部刷新验证驱动驱动的初始化通过了接下来要验证的是绘制接口和颜色格式是否正确。这一章的思路是先做最基础的纯色验证再做文本显示最后引入局部刷新。每一步都有明确的检查目标错了也知道看哪里。4.1 上电验证顺序红绿蓝三色块快速定位问题第一次上电不要急着画复杂图形先把纯红、纯绿、纯蓝三个颜色各刷一屏用肉眼确认三件事颜色是否准确、扫描方向是否正确、显示区域是否完整。测试代码就是一个循环import time while True: lcd.clear(0xF800) # 红色 565格式: R31 G0 B0 time.sleep(1) lcd.clear(0x07E0) # 绿色 565格式: R0 G63 B0 time.sleep(1) lcd.clear(0x001F) # 蓝色 565格式: R0 G0 B31 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 就是 40KBESP8266 会非常紧张。我一般只开一个 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如果初始化时设成了polarity1或phase1控制器收不到正确的命令时序。解决先确认 RST 确实接到了 GPIO4并在init()里先拉低 20ms 再拉高然后把 SPI 参数锁定为polarity0, phase0。这两步都正确后屏幕至少应该能显示纯色块。5.2 全屏偏红或背景发紫现象屏幕能显示内容但整个画面蒙着一层红色或紫色白色区域呈现出明显的色偏。原因这是初始化序列不完整最典型的表现。很多 ST7735 模块是 ST7735S 芯片比老款 ST7735R 多需要几组电源控制寄存器和 gamma 设置只发基础指令虽然能点亮但内部电压和偏置没调好颜色就不正。另外INVON/INVOFF选择错误也会导致颜色发灰、发紫。解决先切换INVON和INVOFF试一次因为这是一行代码的事。如果仍偏色补上 PWCTR1、PWCTR2、VMCTR1 和 gamma 寄存器组参数可以找同型号模块卖家给的初始化代码各厂家之间大同小异。5.3 显示区域偏移或只有一部分画面现象颜色和内容都正常但画面整体偏移比如左边少了一列、下边多了一行或者只有左上角一块有内容。原因ST7735 的显示 RAM 是 132x162屏幕显示窗口的初始偏移因模块而异。控制偏移的是 CASET0x2A和 RASET0x2B里的起始坐标。我的驱动里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 这块小屏玩明白。如果你也卡在同一道坎上按这个顺序调一遍多半能省下不少后悔药的时间。希望帮到你。本文还有配套的精品资源点击获取