简介:针对ESP8266+MicroPython开发场景,这份资源面向希望用硬件SPI驱动ST7735 TFT屏幕的嵌入式开发者,重点解决模拟SPI刷新缓慢、画面响应迟滞的问题。资源提供了经过优化的驱动代码,重新调整了底层铺色、字符串显示等基础函数,使屏幕刷新速度得到明显提升;同时避开了MicroPython中不可供用户使用的Flash SPI,选择了可用的硬件SPI通道,并补充了初始化相位极性设置、spi.write须传入bytes类型等关键注意事项。压缩包共3个文件,均为.py脚本,包含FastTFT核心驱动、主程序及LCD配置模块,整体仅4KB,轻量易用。已有536人学习下载。借助这些脚本,开发者可快速对比模拟SPI与硬件SPI的实际性能差异,理解ST7735时序配置与MicroPython SPI接口的底层用法,也能将其中积累的加速思路迁移到其他TFT屏幕驱动项目中。
1. 从“能亮”到“能用”:ESP8266 驱动 ST7735 的那道坎
很多人第一次把 ESP8266 和 ST7735 屏幕接在一起,用的是软件 SPI。线接对了、代码也跑通了,屏幕确实亮了,但刷一帧全屏要将近半秒,肉眼可见地一行一行往上扫,稍微复杂一点的界面就开始闪。更麻烦的是,一旦 WiFi 和应用逻辑同时在跑,屏幕刷新直接被拖得频繁停顿。这里的核心矛盾很简单:MicroPython 是解释执行,GPIO 翻转靠 Python 代码一 bit 一 bit 地驱动,速度上限天然卡死。而 ESP8266 内部其实有一组硬件 SPI 外设,能自己完成移位输出,CPU 只需要把整块数据丢进缓存。问题是,大部分网上的 MicroPython 示例都默认走软件 SPI,硬件 SPI 的引脚复用、寄存器配置、初始化时序反而成了黑匣子。这篇文章就把这个黑匣子拆开:从“为什么必须用硬件 SPI”讲起,到具体选哪些引脚、初始化序列怎么写、ST7735 的命令参数怎么调,最后落到 6 条实测踩坑记录。读完你手里这块屏,从“能亮”变成“能流畅用”。
2. 为什么非要硬件 SPI:从软件 SPI 的翻车现场说起
2.1 SPI 在 MicroPython 里的两条路:bit-banging 与外设移位
先把概念拨正:SPI 是一种主从同步串行协议,主设备控制时钟信号 SCLK,数据按位在 MOSI 上送出,从设备在时钟边沿采样。实现方式有两种。第一种叫 bit-banging,也就是软件模拟:用代码把 SCLK 拉高、拉低,同时给 MOSI 置位或清零。GPIO 翻转一次,就是一条 Python 语句;传送一个字节 8 bit,就是至少 16 次翻转;传一帧 160×128 像素的 RGB565 数据,大约是 40960 字节,也就是 327680 次翻转。即便每次都走 MicroPython 底层原生方法,Python 运行时的方法分派和对象转换开销也跑不掉,实测下来纯刷一帧全屏到 400ms 以上,是很现实的数据。
第二种是硬件 SPI 外设。ESP8266 芯片内部集成了一个 SPI 模块,数据从内存搬运到发送移位寄存器后,时钟和位输出由硬件逻辑独立完成,CPU 只需要把字节流写进硬件 FIFO,之后可以做别的。MicroPython 对 ESP8266 的 machine.SPI 类封装,最终会调用 SDK 底层的 spi_transaction 接口,把整个字节串一次性交给硬件处理。这里最关键的区别在于:软件 SPI 的瓶颈是“每条指令都在搬运 bit”,硬件 SPI 的瓶颈是“数据写入 FIFO 的速度”,后者比前者快一个数量级还多。实测同一个 framebuffer 内容,软件 SPI 刷新约 450ms,硬件 SPI 约 90ms,视觉体验基本从“幻灯片”变成“可接受”。
2.2 ESP8266 硬件 SPI 的引脚映射:为什么不是随便挑三个 GPIO
ESP8266 的硬件 SPI 不是所有引脚都能当 SCLK、MOSI 用。芯片内部的 SPI 外设(确切说是 HSPI)固定映射到三个引脚:GPIO14 是 SCLK,GPIO13 是 MOSI,GPIO12 是 MISO。这里有个经常让人犯迷糊的点。在 ESP8266 的 MicroPython 固件里,你已经用 SPI(1) 创建设备时,如果没有显式传入引脚号,默认的 sck=GPIO14、mosi=GPIO13、miso=GPIO12。如果自己手动指定引脚,也必须在上述三个 GPIO 里选,指定成别的引脚不会报错,但波形根本出不来,屏幕就白屏。
控制类引脚就自由得多。ST7735 除了 SPI 数据线,还需要 CS(片选)、DC(数据/命令选择)、RST(复位)。CS 可以接任意空闲 GPIO,比如 GPIO15 或 GPIO5;DC 建议接 GPIO4 或 GPIO2;RST 接 GPIO16 或 GPIO0,或者干脆不接,由代码里手动拉低再拉高模拟复位。但注意 GPIO15 在 ESP8266 上是 strapping 引脚,上电时不能拉高,否则进入特殊启动模式,所以如果 CS 用 GPIO15,要确保它默认低电平。实践中我更倾向于 CS 用 GPIO5,DC 用 GPIO4,RST 用 GPIO16。
2.3 ST7735 是什么:一块需要“唤醒”的屏幕控制芯片
ST7735 是矽创电子出的一款 LCD 驱动控制器,常见于 1.8 英寸 TFT 屏,分辨率 128×160,接口有 SPI、并行等,小屏模块上基本都是 4 线 SPI。它内部有一块显存 RAM,MCU 把像素数据通过 SPI 写入这块 RAM,屏幕面板再按 RAM 内容持续刷新显示。所以屏幕显示过程可以理解为“先写显存,面板自己刷”,MCU 不需要不断重发整屏数据。
明白这个原理对后面调试很关键。很多人遇到“屏幕白屏”或“花屏”时,首先怀疑 SPI 通信,其实大概率是初始化时序不对:ST7735 上电后处于睡眠模式,必须先发 SLPOUT(退出睡眠)、设置像素格式、设置显示方向等一串命令,然后发 DISPON 打开显示。缺了任何一步,显存里的数据写了也看不到。硬件 SPI 能把数据快速写进显存,但如果初始化序列没跑对,越快越白屏。
3. 接线与最小验证:点亮一颗像素前先点亮整块屏幕
3.1 完整的接线表:8 根线的接法、供电与共地
把屏幕模块翻过来,绝大多数 1.8 寸 ST7735 模块有 8 个引脚,分别是 VCC、GND、SCLK、MOSI(有些标 DIN)、CS、DC(有些标 RS)、RST、BL(背光)。ESP8266 开发板(比如常见的 NodeMCU 或 D1 mini)与屏幕的接线我一般按这张表:
| 屏幕引脚 | 接 ESP8266 | 说明 |
|---|---|---|
| VCC | 3.3V | 屏幕逻辑供电,不能接 5V |
| GND | GND | 必须与 ESP8266 共地 |
| SCLK | GPIO14 | 硬件 SPI 时钟 |
| MOSI | GPIO13 | 主出从入 |
| CS | GPIO5 | 片选,低有效 |
| DC | GPIO4 | 数据/命令选择 |
| RST | GPIO16 | 复位,低有效 |
| BL | GPIO2(或 3.3V) | 背光控制,接高电平常亮 |
BL 引脚如果模块上已经接了上拉电阻到 VCC,可以不接。没有的话就接 GPIO2,代码里拉高。GPIO2 是 ESP8266 的 strapping 引脚,上电时不能处于低电平,正好背光高电平点亮不冲突。所有线接完后,用万用表确认 VCC 与 GND 之间没有短路,再上电。很多翻车都是 5V 供电把 ST7735 烧了,模块背面丝印如果有“5V”标记,那是给带电平转换的版本用的,普通模块接了必冒烟。
3.2 最小验证脚本:不做初始化直接写,看白屏是好事
接好线后,先不要跑完整驱动,跑一个最基础的 SPI 通信验证。这个脚本不初始化 ST7735,只是往 SPI 总线写一串字节,目的有两个:确认 ESP8266 的硬件 SPI 能正常输出时钟和数据;确认没有报错。如果屏幕什么都不显示(保持白屏/黑屏),是正常的,因为没初始化;如果 SPI 写入时报错,说明引脚映射或 SPI 外设配置有问题,先解决这个再往下走。
from machine import SPI, Pin import time # 创建硬件 SPI 设备 # 注意:ESP8266 硬件 SPI 固定使用 GPIO14/GPIO13/GPIO12 # baudrate 设置 40MHz 上限,实际受限于模块走线和芯片能力 spi = SPI(1, baudrate=40000000, polarity=0, phase=0) cs = Pin(5, Pin.OUT, value=1) # CS 高电平表示不选中 # 写入 4 个字节测试数据 # 0xAA 的二进制是 10101010,方便用示波器/逻辑分析仪观察时钟沿 cs.value(0) # 拉低 CS,选中屏幕 spi.write(b'\xAA\x55\xAA\x55') # 写入测试数据 cs.value(1) # 释放 CS print('SPI write done') time.sleep(1)这段代码里有两处关键参数:baudrate 设为 40000000,即 40MHz,这是 ESP8266 硬件 SPI 的常见上限。不过在长杜邦线和面包板场景下,建议先降到 10000000(10MHz)再测试,高速率在劣质连接下容易花屏。polarity=0、phase=0 是 SPI Mode 0,ST7735 默认支持模式 0 和模式 2,用模式 0 最省事。逻辑分析仪如果抓不到数据,先检查 GPIO14 是否被其他库占用——注意 ESP8266 的 GPIO14 和板载 LED 在某些开发板上是复用的。
3.3 点最亮的背光:确认供电和基本电流通路
刚接好线,先别急着跑初始化。第一步应该确认屏幕的背光能不能亮起来。在代码里把 BL 引脚拉高,如果屏幕背后透出均匀的白光,说明电源和背光通路没问题;如果一点光都没有,量 VCC 是否 3.3V,确认 GND 是否共地。这一步能筛掉一大半接线错误,特别是那些“全部接对了但屏幕就是黑的”问题,大概率是 BL 没拉高或者线序搞反了。
from machine import Pin import time # 背光控制引脚:高电平点亮 bl = Pin(2, Pin.OUT, value=1) time.sleep(0.5) print('Backlight should be on') # 如果屏幕没有亮,尝试直接将 BL 接 3.3V 测试硬件本身是否正常这个脚本足够简单,但能一次性验证三件事:GPIO 输出是否正常、接线是否导通、屏幕背光模组是否完好。如果用了 GPIO2 做背光,注意 ESP8266 上电启动期间 GPIO2 会被系统短暂拉低,这可能导致屏幕开机时背光闪一下再稳亮,属于正常现象。真正该担心的是背光完全不亮,这时候用万用表测 BL 引脚电压,如果代码里 value(1) 后电压仍为 0,可能是引脚本身烧了或者线接错。
4. 初始化序列拆解:把 ST7735 从睡眠中叫醒并设置颜色格式
4.1 那份“魔法寄存器”清单:从 SLPOUT 到 COLMOD
ST7735 的初始化不是发一条命令就完事,它有一串固定序列,每一条命令背后都有明确含义。常见的翻车原因就是漏了某一行,导致屏幕显示颜色错乱、画面颠倒或干脆不亮。我经过实际测试筛掉冗余后,日常初始化只保留下面这些命令,按顺序发送。
| 命令 | 值 | 作用 |
|---|---|---|
| SWRESET | 0x01 | 软件复位,所有寄存器恢复默认值 |
| SLPOUT | 0x11 | 退出睡眠模式,屏幕开始供电输出 |
| COLMOD | 0x3A=0x05 | 设置像素格式为 16bit/像素(RGB565) |
| DISPON | 0x29 | 打开显示,面板开始刷新显存内容 |
需要特别说下 COLMOD。ST7735 支持 12bit、16bit、18bit 三种像素格式,0x05 表示 RGB565,也就是每个像素两个字节,红 5 位、绿 6 位、蓝 5 位。这个决定后续 framebuffer 的存储格式,如果初始化里没设置 RGB565,而代码里又按两个字节一个像素去填充显存,颜色就会严重错乱。此外,复位后屏幕默认是“关显示”状态,只有发了 DISPON 才能看到内容,很多人卡在“写了很多数据但屏幕一直白屏”,多半是漏了这条。
4.2 写 DC 脚:为什么每一条命令都要切换数据/命令模式
ST7735 的 DC 引脚是数据/命令选择线。DC 为低时,SPI 总线上传输的是命令字节;DC 为高时,传输的是数据。同一个 SPI 通道上区分“这串字节是写给寄存器还是写入显存”,全靠 DC 电平切换。这是新手最常忽略的时序点。
以发命令为例,标准流程是:拉低 DC、拉低 CS、发送命令字节、可能需要继续发送参数(此时 DC 仍为低)、拉高 CS、拉高 DC。发数据则反过来:拉高 DC、拉低 CS、发送数据字节、拉高 CS。MicroPython 里可以用一个简单函数封装:command(cmd) 负责发命令,data(payload) 负责发数据。发命令时 DC 置低,发数据时 DC 置高,CS 在整个传输期间保持低。
from machine import SPI, Pin import time # 初始化引脚 spi = SPI(1, baudrate=20000000, polarity=0, phase=0) dc = Pin(4, Pin.OUT, value=1) # DC 默认高(数据模式) cs = Pin(5, Pin.OUT, value=1) # CS 默认高(未选中) rst = Pin(16, Pin.OUT, value=1) def write_cmd(cmd): dc.value(0) # 命令模式 cs.value(0) # 选中屏幕 spi.write(bytes([cmd])) cs.value(1) dc.value(1) # 恢复到数据模式 def write_data(buf): dc.value(1) # 数据模式 cs.value(0) spi.write(buf) cs.value(1)注意 write_cmd 里把整段操作包在 CS 低电平区间内,防止 CS 悬空时数据线上的毛刺被当成命令。cmd 是单个字节,需要用 bytes([cmd]) 包一层,因为 machine.SPI.write 要求传入 bytes 或 bytearray,直接传整数会报错。参数说明:dc 是 Pin(4),对应 GPIO4;cs 是 Pin(5),对应 GPIO5;if 用 Pin(16),对应 GPIO16。这几个引脚号只是推荐配置,改到其他空闲 GPIO 不影响功能,但 GPIO15 尽量避免做片选。
4.3 完整的屏幕初始化函数:复位时序与命令间隔
初始化 ST7735 时,硬件复位是有讲究的。RST 引脚先拉低至少 20ms、再拉高,芯片完成内部复位后需要等待约 120ms 才能稳定。如果跳过硬件复位直接发 SLPOUT,偶尔能亮、偶尔白屏,这种“玄学”多半是上电时序不稳定。另外,SLPOUT 和 DISPON 命令之间有延时要求,SLPOUT 发出后要等 120ms 让内部 DC-DC 转换器稳定,DISPON 之后等 20ms 再开始写显存数据。
def st7735_init(): # 硬件复位:拉低 50ms,再拉高等待 150ms rst.value(0) time.sleep_ms(50) rst.value(1) time.sleep_ms(150) # 软件复位:有些模块的 RST 引脚没接或复用,发命令兜底 write_cmd(0x01) # SWRESET time.sleep_ms(150) # 退出睡眠模式,等待电源稳压 write_cmd(0x11) # SLPOUT time.sleep_ms(120) # 设置像素格式为 16bit RGB565 write_cmd(0x3A) # COLMOD write_data(b'\x05') time.sleep_ms(10) # 打开显示 write_cmd(0x29) # DISPON time.sleep_ms(20) print('ST7735 initialized')这段初始化函数少了复杂画面偏移设置(MADCTL 等),但静置方向下已经足够显示内容。这里的“静置方向”指屏幕排线在下、可视角向上的默认扫描方向。如果你拿到屏幕后画面倒置或镜像,才需要额外发 MADCTL 命令(0x36)来调整 RGB 顺序和扫描方向,入门阶段可以先跳过。延时参数注意:SWRESET 后的 150ms 比很多示例程序的 50ms 更保守,实测某些批次屏幕对复位敏感,延时长些成功率更高。
4.4 填充一块纯色验证:framebuffer 与像素格式的第一次配合
初始化成功后,往显存写一块纯色区域来验证通信链路。这里先不引入复杂绘图,只做一件事:往屏幕左上角一个 10×10 像素区域写入红色。RGB565 格式下,红色是 0xF800,也就是二进制 11111000 00000000。把两个字节按大端序发送(高字节在前)是 ST7735 的默认约定,许多示例代码直接小端发导致颜色错乱成蓝绿色,这属于常见误用。
import struct def fill_rect(x, y, w, h, color_rgb565): # color_rgb565 是 16 位颜色值,0xF800 是红色 # 按大端序打包:高字节在前,低字节在后 color_bytes = struct.pack('>H', color_rgb565) buf = color_bytes * (w * h) # 设置显存访问窗口(CASET/RASET) write_cmd(0x2A) # CASET:列地址范围 write_data(struct.pack('>HH', x, x + w - 1)) write_cmd(0x2B) # RASET:行地址范围 write_data(struct.pack('>HH', y, y + h - 1)) write_cmd(0x2C) # RAMWR:开始写入显存 write_data(buf) # 在左上角画一个 50x50 的红色方块 fill_rect(0, 0, 50, 50, 0xF800)这段代码引入两个新概念:CASET/RASET 和 RAMWR。CASET 命令设置列访问窗口,RASET 设置行访问窗口,两个窗口圈出的矩形区域就是显存写入范围。RAMWR 表示“开始写显存”,之后 SPI 上所有数据都按这个窗口内的像素坐标依次填充。窗口范围用两个 16 位参数表示,struct.pack('>HH') 就是大端序打包两个无符号短整型。第一次跑完这个函数,屏幕左上角应该出现一个红色方块,如果出现了,说明 SPI、复位、初始化、像素格式全部打通,可以进入下一步。
5. 从点亮到稳定显示:常见翻车现场与六个排查方向
5.1 屏幕白屏但不闪烁:先检查 RST 引脚和复位时序
现象:背光亮,初始化代码跑完没有报错,但屏幕永远是白屏,无论往显存里写什么颜色都不变。原因大概率是 RST 引脚没真正复用到 ST7735。有些屏幕模块上的 RST 和 MCU 之间被一个电容隔开,如果 ESP8266 的 GPIO16 刚好被其他程序占用或处于高阻状态,复位时序根本没传到芯片。另一个可能:GPIO16 在 ESP8266 上有特殊属性,它不能用作普通开漏输出,必须显式配置为 Pin.OUT 并拉低。
解决:先用万用表量 RST 引脚在代码运行期间是否出现低电平。如果没有,记得在初始化函数开头加一句 rst = Pin(16, Pin.OUT),然后 value(0) 再 value(1)。如果仍然看不到低脉冲,把 RST 直接接一个 10k 电阻到 3.3V,在代码里发 SWRESET 命令代替硬件复位。软件复位命令有时可以兜底,但是初始化必须等 150ms 再继续,这也是我前面代码里保留 SWRESET 的原因。
5.2 花屏且颜色像被“拉伸”:像素格式 12bit 和 16bit 不匹配
现象:画面内容能辨认出轮廓,但颜色完全不对,比如红色显示成紫色,蓝色变成绿色,颜色边界扭曲。原因有两个方向:一是初始化 COLMOD 没有真正设置成 0x05,命令参数没发出去,芯片保持在默认的 12bit 模式;二是 send 数据时字节序反了,RGB565 是小端发送。首先检查 write_data 传的是 b'\x05' 还是直接把 0x05 写进 SPI,如果写的是 spi.write(0x05) 会直接报错,这是 MicroPython 类型检查的边界。其次确认 struct.pack('>H', color) 用的大端序,改成小端序 '<H' 后红蓝会互换。
5.3 画面显示正常但闪得厉害:刷新率过高或背光供电不足
现象:字符和图形能显示,但整个屏幕持续闪烁,亮度不均,像老式 CRT 的刷新抖动。原因之一是初始化了屏幕之后,代码里在循环里不断重发整屏数据,整帧重发间隔过长(超过 100ms)且没有把 CS 拉低期间完整发完,屏幕面板在两次刷新之间会短暂掉电压。另一个常见原因是背光供电质量差——杜邦线过长导致 BL 引脚的电流被拉低,屏幕背光不稳定。
解决:优先把刷新逻辑改成“只在内容变化时刷新”,减少整屏重发频率。背光的话,BL 引脚不要直接用 GPIO 驱动,加一个三极管或直接接 3.3V 串联 100Ω 电阻。如果屏幕仍然闪,把 SPI 频率从 40MHz 降到 20MHz 试试,高速率下数据错误会让显存出现瞬时错乱,视觉上类似闪烁。
5.4 画面上下颠倒或左右镜像:MADCTL 参数控制扫描方向
现象:内容完全正常,但上下颠倒或左右镜像。原因是 ST7735 出厂时的扫描方式不一定和你的安装方向一致。解决方案是设置 MADCTL 寄存器(0x36),它是一个字节参数,控制行/列扫描顺序和 RGB 顺序。常用的几个值:0xC0 是正常方向;0xA0 上下颠倒;0x60 左右镜像;0xE0 双向翻转。具体取哪个值要看模块 PCB 上的排线方向,实际调试时四个值轮着试,每次改完填充一个左上角带方块的颜色块,看到左边变成正确的边就固定下来。
5.5 首次上电正常、第二次上电白屏:掉电后初始化未重置
现象:代码烧录完第一次运行屏幕正常,但按一下复位键或断电再上电,屏幕就白屏。原因通常是 GPIO 上电状态不稳定。ST7735 的 DC 引脚如果在上电瞬间被 ESP8266 短暂拉高,而 CS 也被拉高,屏幕会误收数据进入错误状态。另一个可能是 RST 接的 GPIO16 在 ESP8266 深度睡眠唤醒后状态不确定,没有重新拉低。
解决:初始化函数开头加一个更长的硬件复位延时,把 rst.value(0) 前的 time.sleep_ms 从 50ms 提高到 200ms。同时把 DC 和 CS 的初始电平在创建 Pin 时就确定,比如 Pin(4, Pin.OUT, value=1),Pin(5, Pin.OUT, value=1),避免引脚处于高阻态时被环境噪声干扰。这个问题的本质是“上电时序”,也是很多人觉得屏幕有玄学的原因,实际上都是电平时序问题。
5.6 文本显示乱码但图形正常:字体取模方向和显存坐标错位
现象:能用 fill_rect 画方块,但显示中英文字符时出现乱码或左右断裂。原因通常不是 ST7735 坏了,而是 framebuffer 坐标系统和字体取模方向不一致。比如 framebuffer 里数据按行扫描,而字体取模按列扫描,写出来的字形就是破碎的。解决:只调用 MicroPython 自带的 framebuf 模块绘制字形,不接受手写取模数据,等基本流程跑通后再根据屏幕方向微调字库方向。framebuf 的 text 方法会先写入内部缓冲,再一次性刷到屏幕,避免逐像素写 SPI 的低效率。
import framebuf # 创建一个 128x160 的 16bit framebuffer fbuf = framebuf.FrameBuffer(bytearray(128 * 160 * 2), 128, 160, framebuf.RGB565) # 绘制文本:白色文字,位置 (10, 20) fbuf.text('ESP8266', 10, 20, 0xFFFF) # 将整个 framebuffer 刷到屏幕:先设置全窗口,再发数据 write_cmd(0x2A) # CASET write_data(struct.pack('>HH', 0, 127)) write_cmd(0x2B) # RASET write_data(struct.pack('>HH', 0, 159)) write_cmd(0x2C) # RAMWR write_data(fbuf)6. 画点速度不够用:换一个思路把刷新率翻倍
6.1 瓶颈定位:SPI 传输不是唯一的时间消耗
如果已经能让屏幕正常显示文字和色块,下一步最容易踩的坑就是刷新速度。很多人以为硬件 SPI 已经把速度拉满,实测却发现画一屏带文字的界面还是要上百毫秒。这里有个隐藏瓶颈:framebuffer 的更新在 Python 层完成,每次 fbuf.text 或 fbuf.pixel 都是一条 Python 语句,而把整个 framebuffer 刷到屏幕只需要一次 SPI 写入。如果界面密集调用绘图原语,耗时大头其实在 Python 字节码执行,不在 SPI。
解决思路是把绘图操作分组。与其像很多入门教程那样每画一个字符就刷一次屏幕,不如先全部绘制到 framebuffer,最后一次性刷新。我习惯把刷盘动作包成一个 refresh 函数,内部设置 CASET/RASET 覆盖全屏后发整个 fbuf 的数据。这个函数调用开销固定,跟画了多少个字符无关。
6.2 脏矩形刷新:只用一次 SPI 传输解决局部更新
大多数界面每一帧只变化一小块区域,比如数字时钟只有秒位在变,功耗表只有当前数值在变。把整屏 128×160 全部重发,等于把 40960 字节的数据全部走一遍 SPI,其中 95% 的数据和上一帧完全相同,这是极大浪费。我的做法是维护一个变量记录“上一次绘制的位置和大小”,界面每次更新时只重发变化区域对应的 framebuffer 切片。这样可以触发显存窗口更新,ST7735 只刷新屏幕的一小块区域。实测一个 160×128 的界面,局部更新比整屏刷新快约 3 到 5 倍,功耗也更低。
last_rect = None # 上次更新的区域 [x, y, w, h] def refresh_partial(fbuf, x, y, w, h): # 局部刷新:把 framebuffer 中指定矩形区域的字节发给屏幕 # framebuffer 的字节布局是行列连续的;计算起始偏移 stride = 128 * 2 # 每行字节数(128 像素 * 2 字节 RGB565) start = y * stride + x * 2 end = (y + h - 1) * stride + (x + w - 1) * 2 + 2 buf = fbuf[start:end] # 切片会复制数据,适合小区域 # 设置显存窗口 write_cmd(0x2A) write_data(struct.pack('>HH', x, x + w - 1)) write_cmd(0x2B) write_data(struct.pack('>HH', y, y + h - 1)) write_cmd(0x2C) write_data(buf)这个切片有个坑:framebuf 是行主序的,如果切片的宽度不等于矩形宽度,需要按行切片再拼接。上面的代码假设切片从行首开始且宽度正好等于矩形宽度,否则必须用循环按行取数据。局部刷新函数只适合处理规则矩形,不规则形状需要拆成多个矩形分别调用,或者退回整屏刷。实测下来,时钟界面每秒刷 1 次秒位,局部刷新耗时 15ms 左右,整屏刷新每秒约 100ms,前者能把 CPU 时间让给 WiFi 处理。
最后,我想说句经验之谈:做嵌入式显示,最忌讳一上来就搬一套完整的 UI 框架。先用 fill_rect 验证颜色,再用 framebuffer 验证文字,最后才考虑局部刷新和脏矩形。这套流程我能在一个小时内完成从接线到流畅显示,希望你也能少走几个弯路。希望帮到你。
本文还有配套的精品资源,点击获取