简介:面向嵌入式开发者的ESP8266与MicroPython联合开发资源包,围绕ST7735驱动芯片的TFT屏幕展开,核心目标是解决模拟SPI方式刷新过慢、直接切换硬件SPI后速度依旧不理想的问题。作者从画点、铺色、显示字符串等基础绘图函数入手逐项重写,优化底层数据写入与传输方式,让屏幕刷新速度获得明显提升。资源中特别提醒MicroPython环境下ESP8266设有两个硬件SPI,其中一个被Flash占用无法供用户使用,因此实际可用的硬件SPI只有一个;若改用软件SPI,则需注意spi.write函数必须传入bytes类型,且SPI相位和极性要依据芯片数据手册时序图正确配置。压缩包为zip格式,大小仅4KB,共3个py文件,涵盖LCD屏幕驱动、功能加载与主程序入口,代码精简、结构清晰,便于移植到其他工程。目前已有535人浏览学习,适合正在调试TFT显示、希望提高屏幕刷新效率的嵌入式开发爱好者参考。 如果你在ESP8266上用MicroPython做过TFT屏幕显示,大概率经历过这样一个场景:屏幕能亮,但全屏刷个图就像在慢放,一行一行往下爬,肉眼能清楚看到扫描过程。稍微复杂一点的界面,刷一次等得人心烦。
这个问题多半不是代码逻辑有毛病,而是数据传输通路选错了。把单片机引脚当成人肉SPI时钟,用Python一句一句去翻转GPIO,速度当然上不去。想让ESP8266配合ST7735这类彩屏跑出比较体面的刷新效果,正确做法是走硬件SPI——让芯片内部的移位寄存器帮你去发时钟和数据,CPU只需要把数据塞给外设就行,速度完全不在一个量级。
这篇内容我按自己实际调这块屏幕的经验来写,从硬件SPI和软件SPI的差异讲起,把接线、固件准备、驱动文件、初始化参数和各类坑一次说完。适合刚入手ESP8266开发、准备给项目配一块小巧TFT屏的朋友参考,也适合已经在用软件SPI刷屏、觉得卡顿想换硬件SPI提速的开发者。
1. 为什么非要用硬件SPI:软件GPIO刷屏是真的难受
1.1 从一次全屏刷新说起
先算一笔账。ST7735常见的分辨率是128x160,颜色深度一般是16bit,也就是每个像素占用2字节。一屏完整数据量是128x160x2 = 40960字节,约40KB。
如果是软件SPI,每发送一个字节,流程是:拉低时钟、在MOSI上设置电平、拉高时钟、再重复8次。在MicroPython的解释环境下,每次GPIO翻转差不多要1~3us,加上循环判断、函数调用开销,发一个字节的耗时能走到30~50us甚至更慢。按50us算,40KB就是40960x50us,约2秒。就算优化一点,也差不多要1秒以上。这个速度意味着,你写个简单的动画,刷新一次至少一秒,界面必然肉眼可见地卡顿。
硬件SPI就不一样了。时钟信号由外设电路产生,MicroPython只需要把数据块交给SPI外设,它会自动按设定的波特率同步输出。以20MHz为例,理论上发完40KB只需要16.4ms,是软件方案一个量级以上的提升。实际项目里因为还要发命令、切换DC电平、处理绘图开销,全屏刷新在几十毫秒级别是能达到的。我的板子上实测,20MHz硬件SPI下执行一次整屏填充大约在40~80ms,跟软件的“秒级”完全是两种体验。
1.2 ESP8266硬件SPI的引脚和一些认知误区
ESP8266在MicroPython环境下的用户可操作硬件SPI只有一个,就是machine.SPI(1),引脚是固定的三根:SCK在GPIO14、MOSI在GPIO13、MISO在GPIO12。和某些单片机不一样,ESP8266的硬件SPI引脚不能随意映射,想在代码里写“SCK=GPIO5”这种操作在硬件SPI下行不通。
这里有个容易误会的点:很多人一听“硬件SPI”,怕引脚太死板,下意识还是去用软件SPI。实际上ST7735这块屏本身只需要SPI的写方向,读取操作基本用不上,所以MISO那个脚不接任何设备也无所谓。但MicroPython构造硬件SPI对象时,有些固件版本还是要求你指定miso引脚,那就把GPIO12填进去,让这个脚悬空或做普通IO处理即可,不影响写屏幕。
还有一个误区是觉得硬件SPI一定比软件SPI难配置。梳理清楚了就知道,两者在MicroPython里的API差别不大,硬件SPI只是额外限制了一组固定引脚,换来的是几十倍的性能提升。用屏幕这种数据吞吐量大的设备,选硬件SPI是必须的,而不是可选项。
2. ST7735模块接线与供电细节
2.1 引脚对照与选脚避坑
市面上的ST7735模块虽然卖家不同,但接口基本是统一的,常见引脚有VCC、GND、CS、RESET、A0(有些标DC)、SDA(MOSI)、SCK、LED(背光)。对照ESP8266,接线可以这样规划:
| ST7735模块引脚 | ESP8266引脚 | 说明 |
|---|---|---|
| VCC | 3.3V | 模块供电,不要接5V |
| GND | GND | 共地 |
| SCK | GPIO14 | 硬件SPI时钟 |
| SDA | GPIO13 | 硬件SPI MOSI |
| CS | GPIO5 | 片选,普通GPIO即可 |
| A0/DC | GPIO4 | 数据/命令选择,普通GPIO |
| RESET | GPIO16 | 复位,普通GPIO即可 |
| LED | 3.3V | 背光,可常亮或GPIO控制 |
选GPIO的时候有讲究。ESP8266启动时会检查几个特殊引脚的状态:GPIO0、GPIO2上电必须为高,GPIO15上电必须为低,否则会进入错误的启动模式。如果把这些特殊引脚用来接外部模块,并且模块在上电瞬间把电平拉反,就有可能导致板子启动异常。稳妥起见,CS、DC、RST这类信号引脚尽量挑GPIO4、GPIO5、GPIO16这种没有额外启动约束的脚位。我自己喜欢用GPIO5做CS、GPIO4做DC、GPIO16做RST,跑了好几个项目都很稳定。
MISO那根线再次强调:SPI构造时需要指定GPIO12,但硬件上可以不连。如果你买的模块引出了MISO,留着悬空或者接到GPIO12都行,代码里只写屏不读屏,完全无所谓。
2.2 供电与干扰问题
ST7735模块上的VCC丝印虽然是5V兼容的样子,但很多小模块板载稳压和电平转换并不完善,习惯上还是按3.3V供电最稳。背光LED如果直接接3.3V,模块自带限流电阻,常亮没问题;如果想让背光可以软件控制,就接一个GPIO,但要注意GPIO驱动能力有限,最好通过一个三极管或MOS管去控制背光的开关。
供电这块经常有人忽略,却最容易出花屏和闪屏的毛病。ESP8266在开启WiFi后电流尖峰很猛,瞬间可能冲到几百毫安,如果和TFT屏共用一颗小功率LDO,很容易在WiFi收发瞬间把3.3V电压拉低,屏幕就会随机花屏甚至重启。我的习惯是在电源输入端并联一颗100uF到470uF的电解电容,再在屏的VCC脚附近加一颗0.1uF陶瓷电容。别小看这两颗电容,能把很多“莫名其妙花屏”的问题挡在发生之前。
3. 烧录固件并准备驱动文件
3.1 ESP8266的MicroPython固件烧录
如果板子还是出厂自带的AT固件,第一步是烧录MicroPython。去MicroPython官网下载ESP8266对应的固件bin文件,注意选择匹配你板子Flash容量的版本,常规NodeMCU一般是4MB。烧录工具用esptool,先完整擦除Flash,再写入固件:
esptool.py --port COM3 erase_flash esptool.py --port COM3 --baud 460800 write_flash -fm dio -fs 4MB 0x0 esp8266-20220618-v1.19.1.binWindows的串口是COMx,Linux下一般是/dev/ttyUSB0,改成自己实际看到的端口即可。先擦除再写入这个顺序别跳,如果旧固件残留,MicroPython启动后文件系统可能挂载失败,出现莫名其妙的重启循环。
烧录完可以用PuTTY或Thonny连接REPL,看到>>>提示符说明固件已经正常跑起来了。
3.2 上传ST7735驱动和main.py
MicroPython本身没有内置ST7735驱动,需要把驱动文件放到设备上。社区里常见的做法是下载一份现成的st7735.py,放到ESP8266的文件系统,然后在main.py里import使用。上传工具有很多,最省事的是Thonny,图形界面里直接右键上传文件,对新手非常友好。也可以用ampy走命令行:
ampy --port COM3 put st7735.py ampy --port COM3 put main.py拿到驱动文件后,别急着运行,先打开源码扫一眼构造函数。ST7735驱动的开源版本很多,接口细节有差异,有的版本初始化时直接在驱动内部创建SPI对象,有的版本要求你从外面传一个SPI实例进来。标题这里强调“使用硬件SPI”,所以我们要用的是后一种方式:外部创建machine.SPI(1),再把SPI对象传给显示设备类。如果下载的驱动是内部创建SPI的写法,需要微调一下,把驱动里创建SPI那段代码替换为接收外部传入的SPI参数。很多人在这一步卡住,其实把源码打开对比几行就看明白了。
4. 核心代码与参数调优
4.1 初始化硬件SPI和显示屏
下面这段是典型的外部传入SPI实例的写法,细节可能因驱动版本略有不同,但思路一致:
from machine import Pin, SPI import st7735 spi = SPI(1, baudrate=20000000, polarity=0, phase=0, bits=8, firstbit=SPI.MSB, sck=Pin(14), mosi=Pin(13), miso=Pin(12)) cs = Pin(5, Pin.OUT) dc = Pin(4, Pin.OUT) rst = Pin(16, Pin.OUT) cs.value(1) tft = st7735.ST7735(spi, cs, dc, rst, rotation=0, offset=(0, 0), bgr=True, invert=True) tft.init() tft.fill(0xFFFF)baudrate=20000000就是20MHz。先别一上来就追求极限,这个速率能保证大部分模块稳定工作。等整屏显示正常了,再试着改到40MHz,如果出现花屏、条纹就降回去。网上有一种说法是ST7735数据手册里写的最大写周期约15MHz,20MHz已经算超频,实际很多模块配合杜邦线短距离连接也能跑,但不要因此忽略线材质量。杜邦线太长、接触不良,都会影响高速SPI的波形。
代码中polarity=0, phase=0对应SPI Mode 0,ST7735一般工作在Mode 0。firstbit=SPI.MSB表示高位先出,这是SPI设备的常规要求。如果你的固件不支持firstbit这个参数,去掉即可,不影响默认行为。
cs、dc、rst三个引脚初始化成输出后,记得把CS先拉高,避免初始化前SPI总线上有其他干扰信号被屏幕误解析成命令。rst在init()内部会自己拉低再拉高完成复位,外部不需要额外处理。
4.2 几个最影响显示效果的参数
驱动初始化里最折磨人的三个参数是:rotation、bgr、invert。这三者配置不对,屏幕能亮,但显示效果千奇百怪。
rotation控制旋转方向,取值范围通常是0到3,对应0度、90度、180度、270度。改了旋转方向后,如果屏幕显示区域偏移了,还需要配合offset参数调整起点位置。很多模块不是标准的128x160全屏可视区域,比如有些80x160或128x128的屏,安装方向不同,坐标偏移也不同。出现显示内容不在屏幕中心的时候,就是offset该派上用场的时候。
bgr控制RGB还是BGR颜色顺序。这个参数错了,最典型的表现是红色和蓝色互换。比如你画一个红色矩形,屏幕显示出来是蓝色。遇到这种状况,把bgr从True改成False,或者反过来,问题立刻解决。
invert是颜色反相。有的ST7735面板需要开启反色,否则显示效果像相片底片一样,整个世界都是反色的。这个参数没有绝对标准,跟具体面板的初始化序列有关,最好的方法就是写一个纯白填充和纯红填充的测试程序,多试几次True/False,选显示正常的那组。
最后还有一个隐性参数是MADCTL,也就是ST7735寄存器0x36,控制扫描方向、RGB/BGR顺序和行列交换。rotation和bgr从根本上说都是在帮你设置这个寄存器。有些驱动把这两个参数封装好了,而有的驱动需要你直接改源码里的MADCTL值,原理明白后看到相关代码就不会懵。
4.3 刷新性能优化思路
显示大面积图形时,绘制和刷屏要配合好。ST7735控制芯片内部自带GRAM,我们可以一次性把所有像素数据推给屏幕,让控制器自己管理刷新。问题在ESP8266的内存很紧张,128x160x2的完整帧缓冲需要40KB,而MicroPython环境能稳定使用的大块连续内存没那么多,直接分配很容易MemoryError。
实际项目里我一般用“行缓冲+分块刷新”的方式:不需要一整个40KB的buffer,先绘制一小块区域到内存,然后立刻通过SPI刷出去,再绘制下一块。比如用bytearray(128*2)先准备一行数据,绘制完一行就推一行。这样做内存占用小,效果上也几乎看不出差别。如果是显示静态界面,可以算好一个区域,用fill_rect配合局部刷新,比整屏重绘高效得多。
如果确实需要整屏缓冲,那么在程序启动早期、文件系统加载完但还没开启WiFi时分配,成功率会高一些。分配之前先执行gc.collect()整理堆内存,减少碎片。分配完就不要再频繁创建大对象,否则后期内存碎片会越来越严重。
5. 踩坑实录与排查清单
5.1 常见问题速查表
把这段时间用ST7735碰到的典型问题整理成了一张表,排查的时候对照着看能省很多时间:
| 现象 | 大概率原因 | 解决办法 |
|---|---|---|
| 全屏白屏 | 复位时序不对、SPI速率太高、供电不足 | 检查RST引脚接线,降低baudrate,电源并电容 |
| 全屏花屏、横条纹 | 电源干扰或SPI速度过高 | 加强滤波电容,逐步降低SPI速率测试 |
| 颜色红蓝互换 | bgr参数不对 | 切换bgr的True/False |
| 显示像底片反色 | invert参数不对 | 切换invert的True/False |
| 内容偏移/显示不全 | offset或rotation不对 | 修改offset坐标,测试不同rotation |
| 背光不亮但屏幕有显示 | 背光引脚没接对 | LED引脚接3.3V,或确认GPIO控制电平 |
| 上电后板子反复重启 | Flash固件没擦干净 | 重新完整erase_flash再烧录 |
| 刷新时WiFi偶尔掉线 | SPI中断与WiFi争抢资源 | 降低SPI速率,减少整屏刷新频率 |
5.2 几个容易被忽略的坑
第一个坑是RST复位等待时间。有些驱动初始化序列里Sleep Out之后只等待几十毫秒,个别ST7735面板需要更长时间才能稳定。我的做法是在init()里的0x11命令之后主动加一个time.sleep_ms(200),宁可多等一下也不要让屏幕带着半睡状态就开始收数据,能明显减少白屏概率。
第二个坑是GPIO初始状态。CS、DC、RST在初始化SPI之前就应该明确设置成输出并给到正确电平。尤其CS,如果不小心悬空或默认拉低,上电瞬间芯片可能把GPIO上的噪声当成片选信号,导致屏幕吃进一堆垃圾命令。养成习惯:cs = Pin(5, Pin.OUT); cs.value(1),初始化屏幕之前CS保持高,需要通信时才拉低。
第三个坑是SPI速率并非越高越好。我测试过40MHz下刷新速度确实更快,但MicroPython在推大块数据时会长时间占用CPU和SPI外设,此时如果WiFi正在收发数据,两边可能互相拖累,表现为屏幕撕帧、WiFi偶发断连。很多场景下20MHz的稳定性比40MHz的极限速度更重要。如果你做的是传感器数据展示这种低频刷新场景,20MHz完全够用。
第四个坑是绘图坐标和屏幕物理方向的组合。常见的小屏幕安装方向五花八门,有的屏接口在左边,有的在右边,有的排线朝下。rotation参数调好后,务必用英文和中文混合文本、矩形框、实心圆各画一遍,确认上下左右没有镜像或偏移,再写正式界面代码。一次调好,后面省心很多。
我自己在实际项目中还习惯把显示刷新和业务逻辑分离:WiFi联网、传感器采集走一个流程,屏幕刷新走另一个流程,用标志位控制,避免两者在同一时间片里抢CPU。这样即使SPI偶发耗时较长,也不会导致整个系统卡死。这个思路在ESP8266这种资源受限的板子上尤其重要,屏幕只是显示终端,不能让它拖垮主逻辑。
本文还有配套的精品资源,点击获取