
简介面向嵌入式开发者的ESP8266MicroPython硬件SPI驱动ST7735 TFT屏幕示例包直击软件模拟SPI刷新速度不足的痛点适合正在学习MicroPython和ESP8266底层外设的开发者。压缩包内共3个python文件包含主程序、LCD驱动以及优化后的显示加速模块整体仅4KB结构紧凑便于对照学习。已有535人学习下载代码中说明了ESP8266硬件SPI一个用于flash、另一个可供用户使用的区别也提示spi.write需传入bytes类型并强调SPI相位与极性需对照芯片时序图确定。开发者通过该示例能了解硬件SPI的正确配置流程掌握铺色、显示字符串等基础函数的优化思路从而显著提升TFT刷新速度是一份实用的显示驱动调试参考。 最近折腾 ESP8266发现很多入门教程还在用软件模拟 SPI 驱动 TFT 屏幕代码写得长不说刷一屏能明显看到从上往下慢慢扫描的拉窗帘效果。我也试过把 ST7735 彩屏接到 NodeMCU 上一开始同样踩了软件 SPI 的坑后来切回硬件 SPI刷新速度直接上了一个量级。这篇就把我调通 ESP8266 MicroPython 硬件 SPI 驱动 ST7735 的完整过程写下来包括引脚选择、驱动文件准备、初始化序列、图像显示和几个比较隐蔽的排查点。走硬件 SPI 不是炫技而是 ST7735 这类屏幕刷新本身就是一个持续喂数据的过程软件模拟 GPIO 翻转的频率上限就在那再加上 MicroPython 的解释执行开销帧率很难看。对只想快速点亮屏幕、显示传感器数据或做小菜单的开发者来说硬件 SPI 是性价比最高的方案。1. 为什么非要用硬件 SPI软件模拟的痛大家都懂1.1 软件 SPI 在 TFT 驱动上的三个致命问题软件 SPI 的实现逻辑并不复杂核心就是通过 GPIO 分别拉高拉低 CLK 和 MOSI 引脚模拟出时钟沿。对 OLED 这类低速小屏软件模拟绰绰有余但到了 128x160 分辨率的 ST7735 彩屏情况就变了。第一个问题是时钟频率上限。MicroPython 在 ESP8266 上用 GPIO 翻转模拟时钟单次位操作经过 Python 层的函数调用和解释执行实际翻转频率能跑到 2~5MHz 就算不错了。而 TFT 屏刷新一个像素需要 16bit 数据128x160 全屏就是 32 万多个 bit算下来单帧传输就要几百毫秒刷新率一度低到 2~3fps。第二个问题是 CPU 占用率。软件 SPI 在传输过程中需要全程占用 CPUESP8266 还要兼顾 WiFi 协议栈。运行时如果同时开启 WiFi 连接或处理定时器会发现屏幕刷新明显卡顿甚至出现整块撕裂。第三个问题是不够稳定。接线稍长、GPIO 翻转快一点就容易产生波形畸变表现在屏幕上就是随机杂点和花屏。这类问题往往不是逻辑错了而是时序不达标。所以只要手头有 ST7735尽量用硬件 SPI别在软件模拟上浪费时间。1.2 ESP8266 硬件 SPI 的两路 SPI 选型真相ESP8266 内部有两路 SPI 控制器但 MicroPython 暴露出来的接口并不像很多人想的那样有多个SPI实例随便选。在标准 MicroPython 固件里SPI(1)对应的是 HSPI这也是最常用的一路。HSPI 的默认引脚是固定的功能GPIONodeMCU 丝印SCKGPIO14D5MOSIGPIO13D7MISOGPIO12D6CS0硬件片选GPIO15D8这里要特别说明驱动 ST7735 这类从设备其实不需要 MISO 引脚因为屏幕不会主动回传数据。但是machine.SPI(1)初始化时可能仍会占用 GPIO12 作为 MISO我在实际使用中并没有因为这个遇到过问题只是建议大家别把 GPIO12 分配给其他外设免得被 SPI 初始化逻辑搞糊涂。更需要注意的是 GPIO15。它虽然在硬件上可以作为 HSPI 的 CS0 片选但同时也是一个 strapping 引脚。GPIO15 在 ESP8266 上电时必须保持低电平否则芯片可能无法正常启动。很多教程会顺手用 GPIO15 做屏幕的 CS用软件控制高低电平来选中设备表面看没问题但如果外部电路把 GPIO15 上拉或者模块上的逻辑恰好让它在上电瞬间拉高就可能在掉电重启时出现芯片不启动的情况。为了避免这类麻烦我直接把 CS 交给了 GPIO4用软件片选稳定省心。2. 引脚分配与接线表照着插基本就不会烧2.1 最稳妥的一组 GPIO 组合我手头是一块 1.8 寸 128x160 的 ST7735 模块板载 3.3V 稳压和电平转换电路。这种模块通常有 8 个引脚VCC、GND、SCL、SDA、RES、DC、CS、BL。接线时我用的是这一组组合TFT 引脚ESP8266 GPIONodeMCU 丝印VCC3.3V3V3GNDGNDGNDSCLGPIO14D5SDAGPIO13D7RESGPIO16D0DCGPIO5D1CSGPIO4D2BLGPIO2D4这个组合的好处是避开了 GPIO0、GPIO2、GPIO15 等启动时序相关的引脚GPIO4、GPIO5、GPIO16 都比较自由做软件片选和指令/数据切换很顺手。需要提醒的是NodeMCU 板上的丝印编号和 ESP8266 的 GPIO 编号不是一回事接线时一定要以 GPIO 编号为准否则照着 D5 找错引脚是很常见的事。2.2 为什么 CS 不能顺手接到 GPIO15前面提过 GPIO15 的 strapping 问题这里展开说一下。ESP8266 的上电流程会检测 GPIO0、GPIO2、GPIO15 的初始电平GPIO15 拉高会让芯片进入异常状态。ST7735 模块的 CS 引脚通常通过电阻上拉到高电平用于表示默认不选中设备这个上拉如果刚好接到 GPIO15上电瞬间就把启动配置破坏了。有些模块上 CS 并不是强上拉可能问题不容易暴露但做硬件设计图省事埋的雷迟早会在某次碰掉电源又重新上电时爆发。与其赌人品不如直接用普通 GPIO 做软件片选。软件片选和硬件片选在 ST7735 这种低速外设上没有本质性能差异因为 ST7735 没有反馈信号CS 拉低后直接开始传数据即可。2.3 MISO 不接线但必须留一个心眼ST7735 屏幕的 SDA 引脚是单向的只接收主控发来的数据所以 MISO 完全不需要接。这跟读 Flash、SD 卡这类需要回读数据的设备不一样。不过有个细节我想补充如果你的 TFT 模块同时引出了 SDO/MISO 引脚建议把它悬空不要接到 GPIO12 上。原因是我在调试时发现部分模块的 SDO 引脚内部有辅助功能接上和主控连在一起可能引入不必要的干扰虽然不至于烧板子但排查花屏问题时会多一个变量。接线完成后建议先用万用表确认 VCC 和 GND 没有接反再上电。ST7735 模块的供电电压虽然标注支持 3.3V但很多板载稳压芯片对反向电压和过压很敏感接反一次可能直接报废。3. 固件和驱动文件准备MicroPython 也有被坑的时候3.1 st7735.py 驱动代码选择标准 MicroPython 固件里并没有内置 ST7735 的驱动类网上能搜到很多版本质量参差不齐。我最后用的是类似传统 Arduino ST7735 初始化序列移植过来的 Python 类里面包含最基本的init、fill_rect、blit等方法。在选择驱动文件时有一个标准看它初始化序列是否完整。便宜的 TFT 模块出厂时通常没有做完整配置驱动一旦缺少 FRMCTR1、PWCTR1、VMCTR1 这些寄存器配置很可能会出现屏幕亮度异常、对比度不对、甚至完全白屏的问题。我自己的st7735.py精简下来核心结构大致是这个样子from machine import Pin, SPI import time class ST7735: def __init__(self, spi, cs, dc, rst, width128, height160): self.spi spi self.cs cs self.dc dc self.rst rst self.width width self.height height def write_cmd(self, cmd): self.dc(0) self.cs(0) self.spi.write(bytes([cmd])) self.cs(1) def write_data(self, data): self.dc(1) self.cs(0) if isinstance(data, int): data bytes([data]) self.spi.write(data) self.cs(1) def init(self): self.rst(0) time.sleep_ms(50) self.rst(1) time.sleep_ms(120) # 软件复位和退出睡眠 self.write_cmd(0x01) time.sleep_ms(120) self.write_cmd(0x11) time.sleep_ms(120) # 帧率、电源、电压等配置 for cmd, data in [ (0xB1, [0x05, 0x3A, 0x3A]), (0xB2, [0x05, 0x3A, 0x3A]), (0xB3, [0x05, 0x3A, 0x3A, 0x05, 0x3A, 0x3A]), (0xC0, [0x2C, 0x2D]), (0xC1, [0x01]), (0xC2, [0x02]), (0xC3, [0x02]), (0xC4, [0x02]), (0xC5, [0x3C, 0x38]), (0xC6, [0x11]), (0x36, [0xC8]), (0x3A, [0x05]), (0x2A, [0x00, 0x02, 0x00, 0x81]), (0x2B, [0x00, 0x01, 0x00, 0xA0]), ]: self.write_cmd(cmd) for d in data: self.write_data(d) self.write_cmd(0x29) time.sleep_ms(20)这段代码参考了常见的 ST7735 驱动序列实际使用时要根据屏幕模块微调尤其是0x2A和0x2B里设置的列地址和行地址偏移不同模块差别很大。3.2 固件里没有内置 st7735 驱动时的应对很多人会先烧一个最新版 MicroPython 固件然后发现import st7735报错。这很正常ST7735 驱动不是标准库内容需要自己把驱动文件上传到 ESP8266 的文件系统。上传驱动文件的方式我推荐用ampy或rshell把本地st7735.py传到开发板上。比如用 ampyampy --port /dev/ttyUSB0 put st7735.py ampy --port /dev/ttyUSB0 run main.py如果你不想每次启动都手动导入可以把驱动文件放在根目录然后在main.py里from st7735 import ST7735。这里有一个很容易踩的坑有些版本的ampy对文件大小有限制驱动文件如果超过几十 KB上传时会报错另外开发板如果连接了串口监视器要先把监视器关掉否则串口冲突导致上传失败。4. 上电初始化与第一个彩色像素4.1 初始化序列的要点ST7735 不是上电就能用的屏幕它内部有一个状态机必须按顺序执行软件复位、退出睡眠模式、设置像素格式、设置显示窗口、打开显示等步骤整套序列跑完才能正常显示。时序上稍有不对屏幕就可能停留在白屏状态。整个初始化里最关键的三个寄存器是0x3ACOLMOD设置像素格式为 16bit即 RGB565每个像素占 2 字节。0x36MADCTL控制扫描方向和 RGB/BGR 颜色顺序。0x2A/0x2BCASET/RASET设置显示窗口的列地址和行地址范围。如果你的屏幕显示了内容但颜色不对或者内容偏移到了屏幕外面多半就是0x36和0x2A/0x2B需要调整。4.2 最简样例铺满颜色初始化完成后先测试纯色填充确认基础链路是否通畅。下面的代码会在屏幕中间画一个红色的矩形from machine import Pin, SPI import time from st7735 import ST7735 spi SPI(1, baudrate40000000, polarity0, phase0, bits8, firstbitSPI.MSB) cs Pin(4, Pin.OUT) dc Pin(5, Pin.OUT) rst Pin(16, Pin.OUT) lcd ST7735(spi, cs, dc, rst, width128, height160) lcd.init() def fill_rect(x, y, w, h, color): high (color 8) 0xFF low color 0xFF chunk bytearray([high, low]) * 64 lcd.write_cmd(0x2A) for b in [(x 8) 0xFF, x 0xFF, ((x w - 1) 8) 0xFF, (x w - 1) 0xFF]: lcd.write_data(b) lcd.write_cmd(0x2B) for b in [(y 8) 0xFF, y 0xFF, ((y h - 1) 8) 0xFF, (y h - 1) 0xFF]: lcd.write_data(b) lcd.write_cmd(0x2C) lcd.dc(1) lcd.cs(0) total w * h for _ in range(total // 64): lcd.spi.write(chunk) lcd.cs(1) fill_rect(10, 10, 60, 40, 0xF800) # 红色这里我没有直接用fill_rect一个像素一个像素地传而是把数据分成 64 字节的 chunk 循环写既避免了申请一个几十 KB 的 bytearray 把内存撑爆又能发挥硬件 SPI 的优势。baudrate40000000是 ESP8266 HSPI 实际能稳定跑的上限之一如果你发现传输过程中花屏可以降到 20000000。4.3 内存超标问题framebuf 别直接上整屏MicroPython 提供了一个framebuf模块可以在内存里画点、画线、写文字然后再统一推送到屏幕。128x160 的 RGB565 画布需要 1281602 40960 字节也就是 40KB。ESP8266 的可用内存本身只有 80KB 左右MicroPython 解释器和 WiFi 协议栈还要占掉一大块直接申请 40KB 的帧缓冲大概率会抛出MemoryError。我实测过几次初始化完 WiFi 连接后再创建这么大的FrameBuffer基本都会失败。两个可行的方案一个是不开 WiFi把内存全留给显示另一个是只在内存中维护一个小型画布比如 128x80 或更小的 64x160然后分区域推送。对大多数显示传感器数据或菜单的需求小画布足够用了。所以我的建议是如果只是简单显示文字和图形直接用write_cmdwrite_data组合逐区域刷新不要追求整屏 framebuf如果一定要做帧缓冲考虑换用 ESP32它的内存充裕很多。5. 显示中文、图片和简单动画的实测5.1 framebuf 的部分刷新策略既然整屏 framebuf 内存不够用一个折中的办法是使用小尺寸 framebuf然后只刷新屏幕上的一个局部区域。比如屏幕左上角显示时间右下角显示温度可以把这两块各做成一个小画布更新时只推送对应区域的数据。关键是在推送数据前要正确设置 ST7735 的显示窗口。ST7735 的窗口设置由0x2A和0x2B完成窗口有多宽后续 RAMWR 命令就会按这个宽高连续接收像素数据。如果你设置窗口是 20x20但送了 50 个像素点屏幕就会把多余的像素写到窗口之外形成错位。以下是我实际使用的部分刷新框架def show_region(x, y, w, h, buffer): lcd.write_cmd(0x2A) for b in [(x 8) 0xFF, x 0xFF, ((x w - 1) 8) 0xFF, (x w - 1) 0xFF]: lcd.write_data(b) lcd.write_cmd(0x2B) for b in [(y 8) 0xFF, y 0xFF, ((y h - 1) 8) 0xFF, (y h - 1) 0xFF]: lcd.write_data(b) lcd.write_cmd(0x2C) lcd.dc(1) lcd.cs(0) lcd.spi.write(buffer) lcd.cs(1)配合一个 64x32 的 framebuf既能显示四位数字和单位又不会把内存吃光。实测刷新一个 64x32 的小区域不到 3ms用来做数据仪表盘非常舒服。5.2 显示图片前先在电脑上转换格式ST7735 不能直接显示 JPG、PNG需要先把图片转成 RGB565 原始像素数据。我一般用 Python 的 PIL 库在电脑上做转换把图片缩放成 128x160然后导出成.bin文件再用 ampy 上传到 ESP8266 文件系统。转换脚本非常简单from PIL import Image img Image.open(pic.png).resize((128, 160)).convert(RGB) with open(pic.bin, wb) as f: for y in range(160): for x in range(128): r, g, b img.getpixel((x, y)) rgb565 ((r 0xF8) 8) | ((g 0xFC) 3) | (b 3) f.write(rgb565.to_bytes(2, big))显示时由于pic.bin有 40KB不能一次性读入内存需要分块读取并推送。读取文件对象时会占用一些内存这时候最好关掉 WiFi 连接再显示否则很容易触发内存不足。另一个技巧是先把文件读取进bytearray如果发觉内存不够就改成每次读 512 字节推一个区域。5.3 滚动动画的帧率实测用硬件 SPI 做简单的动画是完全可行的。比如一个 64x64 的小方块在屏幕上移动每次移动只刷新它移动前后的两个 64x64 区域整体帧率可以稳定在 25fps 左右。如果加上网络请求或 CPU 密集型计算帧率会掉到 10fps 以下这是因为 MicroPython 本身执行字节码也需要时间。要进一步提升动画流畅度可以降低刷新区域大小、减少频繁的 Python 层函数调用。一个很实用的优化是把像素数据提前拼好放在bytearray里而不是每次write_data一个字节。SPI 的write方法支持一次性传整个bytearray这种批量化操作的效率远高于逐字节传输。6. 花屏、白屏、偏色的排查链路回顾6.1 白屏问题定位顺序白屏是 ST7735 最常见的故障原因也最多。我的排查顺序是先检查 RES 复位时序再看初始化序列是否完整然后检查显示开关命令0x29有没有执行最后排查 CS/DC 是否接反。如果你发现屏幕能亮但没有任何内容先在代码里手动把某个区域填成纯红色同时缩小窗口大小比如只填充屏幕中间 10x10 的区域。如果这个区域有色块说明 SPI 数据通路和窗口设置是正常的问题出在初始化参数或显示范围上。复位时序是一个很隐蔽的点。有些驱动代码在rst(1)之后没有加sleep_ms(120)紧接着就发送初始化命令这时候 ST7735 还没完成内部复位命令会被忽略表现就是白屏。我建议复位后保持至少 120ms 延迟不要省。6.2 花屏的独门解决思路花屏一般表现为随机色点、条纹交错或内容错位。首先检查 SPI 速率是否太高ESP8266 的 HSPI 虽然理论上能跑到 80MHz但受排线长度和模块电路影响40MHz 有时候就不稳定降到 10~20MHz 测一下往往立刻恢复。如果降速有效说明是信号完整性问题可以考虑缩短杜邦线、尽量不要用面包板长跳线。花屏的另一个来源是片选时序。软件片选时CS 拉低到写入第一个字节之间应该有足够的建立时间如果刚cs(0)就立刻dc切换和spi.write有些 ST7735 模块会识别失败。我通常在cs(0)后加一个极短的延时或者把所有片选逻辑调整到数据写入完成后一起处理。6.3 颜色不对别急着怀疑屏幕如果屏幕能显示内容但颜色整体偏蓝、偏红或者反相问题多半出在颜色格式上。ST7735 支持 RGB 和 BGR 两种颜色过滤顺序由0x36寄存器的第三位BGR bit控制。如果你用红色0xF800填充实际屏幕上却显示蓝色说明屏幕工作在 BGR 模式把0x36的值从0xC8改成0x08或者反过来试试基本能解决。还有一个常见原因是 RGB565 的高低字节顺序。ST7735 在 16bit 模式下期望先收到高字节再收到低字节。如果驱动代码里字节推反了颜色会整体错乱。我的show_region里用(color 8) 0xFF先发高字节(color 0xFF)后发低字节这个顺序不要动。最后提一个不太容易察觉的点部分 ST7735 屏幕模块需要执行0x21INVON命令才能正常显示有些则必须保持0x20INVOFF。如果你的屏幕出现了负片效果也就是颜色和预期完全相反可以去初始化序列里加一条0x21或确认当前是0x20不需要换屏幕。我在实际开发中踩得最深的一个坑就是偏移地址。不同厂家、不同尺寸的 ST7735 模块0x2A和0x2B里的起始地址并不一样有些从 0 开始有些从 2 开始有些甚至要偏 26 个像素。点亮屏幕后如果发现内容紧贴某一边或明显偏移可以在初始化里调整0x2A/0x2B的起始和结束值用纯色边框辅助标定一般几分钟就能对准。这套流程走通之后后面再做上位机显示、小型仪表盘或者温湿度可视化的项目就只是往这个骨架上填内容的事了。本文还有配套的精品资源点击获取