MicroPython驱动ST7789与ST7735 TFT显示屏:从硬件连接到中文字库优化 1. 硬件连接ESP32与TFT屏的“握手”如果你手头有一块ESP32开发板又刚好拿到了一块小巧的ST7789或ST7735 TFT屏幕想把它们连起来跑点东西第一步也是最关键的一步就是硬件连接。这就像给两个设备“牵线搭桥”线接对了后面的一切才可能顺利。我刚开始玩的时候也在这上面踩过坑比如屏幕死活不亮或者显示花屏折腾半天才发现是引脚接错了。所以咱们先把这个基础打牢。ST7735和ST7789这两款屏幕虽然分辨率不同一个常见128x160一个常见240x240或240x320但它们的通信接口和基本控制引脚是高度相似的都使用SPI接口。这意味着你连接ESP32的方式几乎可以一模一样这给我们后续的代码复用带来了极大的便利。你需要准备的硬件很简单一块支持MicroPython的ESP32开发板比如ESP32-S3、ESP32-C3都行一块TFT屏幕几根杜邦线如果为了方便测试加个面包板就更好了。具体的引脚连接是很多新手最容易迷糊的地方。屏幕背面通常会有一排引脚标着VCC、GND、SCL、SDA、RES、DC、CS、BLK等。这里要特别注意虽然名字叫SCL/SDA但在SPI模式下我们实际用的是SPI的时钟线SCK和数据线MOSI屏幕上的SCL对应ESP32的SPI时钟引脚SCKSDA对应ESP32的SPI数据输出引脚MOSI。下面是我在ESP32-S3上实测稳定可用的连接方式你可以直接照搬TFT屏幕引脚ESP32-S3引脚作用说明VCC3.3V电源正极务必接3.3V接5V可能烧屏GNDGND电源地SCL (SCK)GPIO 4SPI时钟线SDA (MOSI)GPIO 5SPI数据线主机输出从机输入RES (RST)GPIO 6复位引脚低电平有效DC (A0)GPIO 7数据/命令选择引脚这是关键控制线CSGPIO 15片选引脚低电平选中该设备BLK (LED)GPIO 18背光控制高电平点亮这里有几个细节需要强调。第一是电源ESP32的3.3V输出引脚通常可以驱动小尺寸TFT屏但如果你的屏幕背光特别耗电或者同时连接了其他外设可能会供电不足导致屏幕闪烁或工作不稳定这时可以考虑外接一个3.3V的稳压模块。第二是DC引脚它决定了你通过SPI发送的数据是命令如初始化指令还是真正的显示数据驱动代码里全靠它来切换所以这个引脚必须接不能省略。第三是BLK背光有些驱动代码里会默认将其置高点亮如果你接上后发现屏幕有内容但非常暗记得检查一下这个引脚是否被正确驱动了。连接好硬件后先别急着写代码。我建议你用一个简单的MicroPython脚本测试一下SPI通信是否正常。你可以先不初始化屏幕而是尝试用SPI向一个不存在的地址发送数据看看有没有错误产生。或者更直接的办法是先运行我们后面会讲到的、已经验证过的驱动代码如果屏幕能亮起并显示一些内容哪怕是乱码也说明硬件连接基本没问题。硬件连接是物理基础这一步稳了后面的软件调试才能有的放矢。2. 驱动代码移植从ST7735到ST7789硬件连通后我们就进入了软件核心——驱动代码。网上能找到的MicroPython驱动代码很多是针对某一块特定屏幕写的换一块屏可能就不工作了。但好消息是ST7735和ST7789同属一个家族驱动代码的框架高度相似。就像我最近做的一个项目原来用ST7735128x160客户想要显示更多信息但屏幕尺寸不能变大我们就换成了ST7789240x240分辨率更高尺寸反而更小。这时最经济的做法就是在原有ST7735的成熟驱动上修改而不是从头重写。整个移植过程主要围绕三个核心点进行修改初始化序列、分辨率配置和底层数据写入函数。我们一个一个来看。首先初始化序列是屏幕醒来后接收的第一组指令告诉它如何配置内部寄存器比如色彩模式、扫描方向、供电配置等。ST7735和ST7789的初始化命令大部分不同需要对照数据手册逐一修改。在原始代码中_init函数里的init_commands列表就是干这个的。ST7735的初始化命令有近20条而ST7789的通常少一些但参数更复杂。例如设置内存数据访问控制MADCTL命令0x36的字节决定了屏幕的旋转方向这个值需要根据你的实际安装方向来调整。其次分辨率配置的修改无处不在。因为从128x160变成了240x240所有涉及到屏幕宽度和高度的参数都要更新。这主要体现在三个地方一是类初始化时的宽度和高度参数二是在show()函数中设置列地址0x2A和行地址0x2B的命令参数这些参数定义了刷新的屏幕区域三是如果你有图形绘制函数如画点、画线、画矩形它们的边界检查逻辑也需要更新。例如在ST7735的show()里列地址结束是0x7F127行地址结束是0x9F159而在ST7789里因为我们的屏幕是240x240所以结束地址都变成了0xEF239。最后是底层通信函数的兼容性检查。通常_write函数负责通过SPI发送命令和数据这部分代码一般不需要改动因为它只关心如何把字节通过DC引脚区分为命令或数据发送出去。但是你需要确认SPI的时钟频率baudrate是否合适。ST7789在较高分辨率下需要更快的数据吞吐你可以尝试将SPI频率从40MHz提高到80MHz甚至更高前提是你的ESP32和屏幕布线能支持这个速度否则可能导致显示异常。在我的测试中ESP32-S3在80MHz下驱动240x240的ST7789非常稳定。为了让你更清楚我写一个简单的代码对比片段。假设我们有一个基础的TFT类然后派生出ST7735和ST7789类。关键的不同就在__init__和_init函数里import machine, time from machine import Pin, SPI class TFT: def __init__(self, width, height, spi, dc, cs, rst, blNone, rotate0): self.width width self.height height self.spi spi self.dc dc self.cs cs self.rst rst self.bl bl self.rotate rotate self.buffer bytearray(width * height * 2) # 16位色深每个像素2字节 # ... 其他初始化 def _write(self, cmd, dataNone): self.dc(0) # 命令模式 self.cs(0) self.spi.write(bytearray([cmd])) if data: self.dc(1) # 数据模式 self.spi.write(data) self.cs(1) class ST7735(TFT): def _init(self): self._reset() # 硬件复位 # ST7735 特有的初始化命令序列 init_cmds [ (0x01, None), # 软件复位 (0x11, None), # 退出睡眠 (0x36, b\xC8), # MADCTL: 内存访问控制这里设置旋转 (0x3A, b\x05), # 色彩模式: 16位RGB565 # ... 更多ST7735命令 (0x29, None), # 打开显示 ] for cmd, data in init_cmds: self._write(cmd, data) time.sleep_ms(10) def show(self): # 设置刷新区域为全屏 (128x160) self._write(0x2A, b\x00\x00\x00\x7F) # 列地址: 0-127 self._write(0x2B, b\x00\x00\x00\x9F) # 行地址: 0-159 self._write(0x2C, self.buffer) # 写入显存数据 class ST7789(TFT): def _init(self): self._reset() # ST7789 特有的初始化命令序列 init_cmds [ (0x01, None), (0x11, None), (0x36, b\xC0), # 注意ST7789的MADCTL参数可能不同 (0x3A, b\x55), # 色彩模式: 16位RGB565 (0x55), 18位(0x66) # ... 更多ST7789命令如伽马校正等 (0x21, None), # 打开反色显示有时需要 (0x2A, b\x00\x00\x00\xEF), # 列地址: 0-239 (0x2B, b\x00\x00\x00\xEF), # 行地址: 0-239 (0x29, None), ] for cmd, data in init_cmds: self._write(cmd, data) time.sleep_ms(10) def show(self): # 对于ST7789初始化时已经设置了全屏地址这里有时可以省略 # 但为了通用性通常还是写上 self._write(0x2A, b\x00\x00\x00\xEF) self._write(0x2B, b\x00\x00\x00\xEF) self._write(0x2C, self.buffer)通过这样的结构我们最大程度地复用了基类的代码只需要在子类中重写关键的不同部分。移植完成后记得在主程序里根据你实际使用的屏幕型号选择初始化对应的类。这样一套代码框架就能灵活支持两种屏幕了。3. 中文字库的痛点与解决方案驱动屏幕显示英文和图形相对简单但一到中文问题就来了。这是很多国内开发者用MicroPython玩TFT屏时遇到的共同瓶颈。网上很多开源驱动要么只支持ASCII字符要么就像我最初找到的那些号称支持中文但实现方式是“硬编码”——你需要把要显示的每个汉字的点阵数据手动提取出来然后像常量一样写在代码里。想象一下你要显示“欢迎使用”四个字就得先找到这四个字对应的16x16或24x24的点阵数组然后font_data {‘欢’ [0x00,0x40,...], ‘迎’[...]}这样写进去。这显然不现实项目里文字稍一变动改代码就能把人累死。所以一个独立、完整、可灵活调用的中文字库文件是必须的。我们需要的是一个包含常用汉字点阵数据的二进制文件以及一个能高效读取、解析并渲染到屏幕上的字库类。这里通常采用“点阵字库”格式比如在PC上常见的HZK1616x16点阵、HZK2424x24点阵等。但直接把这些文件放到ESP32的闪存里并读取会面临两个挑战一是文件体积一个完整的16点阵字库大约260KB24点阵的更大二是读取速度ESP32的SPIFFS文件系统读取小文件尚可但频繁随机读取字库文件的不同位置可能会成为性能瓶颈。我采用的解决方案是使用一种经过优化和裁剪的自定义二进制字库格式。这种格式通常包含一个文件头标识符、版本、字库信息和一个按汉字区位码排序的点阵数据区。文件头可以设计得很精简例如只包含字体大小、点阵数据起始偏移量等关键信息。这样做的好处是我们可以根据项目需求只打包需要用到的汉字极大减小字库文件体积。比如一个只包含1000个常用汉字的24点阵字库可能只有几十KB。有了字库文件下一步就是编写一个Font类来操作它。这个类的核心功能是给定一个汉字或其Unicode/GB2312编码能快速计算出这个汉字的点阵数据在文件中的位置读取出来并转换成MicroPython的framebuf能理解的位图格式。这里的关键是索引算法。GB2312编码的汉字是分区位存放的我们可以利用这个规律offset ((区码 - 起始区) * 94 (位码 - 起始位)) * font_size_in_bytes。计算出的offset加上文件头长度就是点阵数据在文件中的绝对位置。为了提升显示效率还可以在Font类中加入缓存机制。最近显示过的汉字其点阵数据可以暂时保存在内存字典里下次再显示同一个字时就直接从内存读取避免了重复的文件I/O操作。这对于显示重复性较高的文字如传感器数据标签、菜单项效果显著。下面是一个简化版的字库类核心方法示例import struct class FontRenderer: def __init__(self, font_path, width, height): self.font_file open(font_path, rb) # 读取文件头解析字库信息 header self.font_file.read(16) # 假设自定义格式前2字节BM标识第3字节版本第4字节映射模式5-7字节点阵数据起始偏移... self.font_size header[7] # 例如 16 或 24 self.bitmap_start struct.unpack(I, b\x00 header[4:7])[0] self.screen_width width self.screen_height height self.cache {} # 简单缓存字典 def get_char_bitmap(self, char): # 先查缓存 if char in self.cache: return self.cache[char] # 计算编码这里以GB2312为例 try: gb_code char.encode(gb2312) except: # 非GB2312字符返回空白或默认字符 return bytearray([0] * (self.font_size * self.font_size // 8)) qw_code gb_code[0] - 0xA0 # 区码 ww_code gb_code[1] - 0xA0 # 位码 # 计算文件偏移量 (假设字库按GB2312顺序排列) offset self.bitmap_start ((qw_code - 1) * 94 (ww_code - 1)) * (self.font_size * self.font_size // 8) self.font_file.seek(offset) bitmap_data self.font_file.read(self.font_size * self.font_size // 8) # 存入缓存 self.cache[char] bitmap_data return bitmap_data def render_text(self, display, text, x, y, color): current_x x for ch in text: if ch \n: y self.font_size current_x x continue bitmap self.get_char_bitmap(ch) # 将点阵数据转换为 framebuf # 这里需要根据你的显示驱动将bitmap转换为对应格式的FrameBuffer对象 # 例如对于单色位图 char_fb framebuf.FrameBuffer(bitmap, self.font_size, self.font_size, framebuf.MONO_HLSB) display.blit(char_fb, current_x, y, color) # color用于指定前景色在单色模式下可能无效需特殊处理 current_x self.font_size # 自动换行检查 if current_x self.font_size self.screen_width: current_x x y self.font_size这个类提供了最基础的文字渲染功能。在实际使用中你还需要考虑更多细节比如中英文混排英文字符通常宽度是汉字的一半、字体缩放、抗锯齿对于高分辨率屏等。但有了这个基础框架你已经可以自由地在你的ST7789或ST7735屏幕上显示任意中文语句了再也不用为改几个字而去折腾代码。4. 性能优化与高级显示技巧当基础功能都跑通后我们自然会追求更流畅的体验和更酷的效果。尤其是在ESP32这种资源有限的微控制器上驱动240x240甚至更高分辨率的屏幕并显示动态内容如动画、实时数据曲线性能优化就变得至关重要。我在这上面花了不少时间调试总结出几个立竿见影的技巧。第一减少SPI通信开销。屏幕刷新的本质是向屏幕的显存GRAM写入数据。每次调用show()或局部刷新函数都会通过SPI发送大量数据。优化点在于1)使用DMA直接内存访问如果硬件SPI支持DMA可以解放CPU在传输数据的同时CPU可以去处理其他任务。ESP32的SPI硬件通常支持DMA在MicroPython中初始化SPI时指定dma_chan参数即可如SPI(2, ..., dma_chan2)。2)批量写入尽量避免单像素写入。一次性将整个framebuffer或一大块区域的数据通过spi.write()发送出去比多次调用写单个字节效率高得多。我们的驱动代码中show()函数一次性发送整个self.buffer就是遵循这个原则。第二优化帧缓冲区Framebuffer操作。MicroPython的framebuf模块提供了基本的绘图函数但有些操作比较慢。例如fb.text()方法在绘制文字时会逐个像素计算对于中文字体如果每次都从字库文件读取并渲染会很慢。我们的优化方法是1)预渲染静态界面对于不常变化的文字、图标、背景可以预先绘制到一个离屏缓冲区Off-screen Buffer需要时直接blit到主缓冲区这比重新绘制所有元素快得多。2)局部刷新如果只是屏幕上一小部分区域的数据变化如一个不断更新的数字可以只刷新那一小块矩形区域而不是全屏刷新。这需要驱动支持设置局部刷新地址0x2A,0x2B命令并且只发送该区域对应的缓冲区数据。第三利用硬件加速特性。ST7789等现代驱动芯片有一些内置功能可以减轻主控负担。例如硬件滚动Scrolling你可以设置一个滚动区域屏幕硬件会自动循环滚动显示这部分内容无需CPU干预去移动缓冲区数据。还有局部颜色填充有些驱动支持用一条命令填充一个矩形区域为指定颜色这比用软件画矩形快很多。你需要查阅ST7789的数据手册找到这些高级命令并集成到驱动中。第四高级显示效果的实现。有了稳定的驱动和字库就可以玩些花样了。比如实现平滑滚动字幕。思路是在内存中创建一个比屏幕宽的缓冲区把要显示的文字先绘制到这个长缓冲区里然后在屏幕刷新时每次只blit这个长缓冲区的一部分到屏幕并不断偏移起始位置。再比如伪透明效果如果想在背景图上显示文字可以先blit背景图到缓冲区再以“异或”或特定的混合模式blit文字上去。虽然MicroPython的framebuf原生不支持Alpha混合但我们可以通过预先计算好带透明度的颜色值来实现简单的效果。这里分享一个我常用的实时数据波形显示的简单实现它综合运用了局部刷新和缓冲区管理class WaveformDisplay: def __init__(self, tft, x, y, width, height, grid_color, wave_color, bg_color): self.tft tft self.x x self.y y self.width width self.height height self.grid_color grid_color self.wave_color wave_color self.bg_color bg_color self.data_buffer [] # 存储数据点 self.max_points width # 横坐标最多容纳的点数 # 绘制静态网格背景预渲染到临时buffer self.bg_buffer bytearray(width * height * 2) self.bg_fb framebuf.FrameBuffer(self.bg_buffer, width, height, framebuf.RGB565) self.bg_fb.fill(self.bg_color) # 画网格线... # 将背景一次性blit到屏幕对应位置 tft.blit(self.bg_fb, x, y) def add_data_point(self, value): 添加一个新的数据点并更新显示 # 将值映射到显示高度范围内 y_pos int(self.y self.height - (value * self.height)) self.data_buffer.append(y_pos) if len(self.data_buffer) self.max_points: self.data_buffer.pop(0) # 移除最旧的点 # 局部刷新只清除和绘制波形区域 # 1. 用背景色清除旧的波形线这里简化处理直接重绘背景 # 更优做法是只擦除上一次绘制的线段 self.tft.blit(self.bg_fb, self.x, self.y) # 重绘背景 # 2. 绘制新的波形线 for i in range(1, len(self.data_buffer)): x0 self.x i - 1 y0 self.data_buffer[i-1] x1 self.x i y1 self.data_buffer[i] self.tft.line(x0, y0, x1, y1, self.wave_color) self.tft.show() # 刷新屏幕这个例子中我们为波形图区域创建了一个独立的背景缓冲区每次更新数据时先快速恢复背景再绘制新的曲线避免了全屏刷新大大提高了刷新效率。对于更复杂的UI你可以考虑使用LVGL等嵌入式图形库的MicroPython移植它们提供了更完善的控件和图形功能但也会消耗更多资源。对于大多数简单应用自己优化后的驱动和绘图逻辑已经完全够用。5. 实战构建一个天气信息显示站现在让我们把前面所有的知识串联起来做一个有趣又有用的实战项目一个基于ESP32和ST7789屏幕的迷你天气信息显示站。这个项目会用到网络连接从网络API获取天气、JSON解析、中文字库显示、以及动态图标绘制几乎涵盖了本文讨论的所有技术点。我会分享我实际做这个项目时的步骤和遇到的坑。第一步硬件与基础连接。硬件就按第一章讲的把ESP32-S3和ST7789屏幕连接好。确保ESP32能通过Wi-Fi连接到互联网。我用的是一块240x240的圆形ST7789屏幕视觉效果很棒。在MicroPython固件中需要提前安装urequests或requests库用于网络请求以及ujson库用于解析数据。如果你的固件没有可以通过mip或upip工具安装。第二步获取并解析天气数据。国内有很多免费的天气API比如和风天气、心知天气等它们通常提供免费的额度足够个人项目使用。你需要注册一个账号获取API密钥。这里以请求一个简单的天气JSON为例注意实际URL和参数需根据你选择的API调整import network, urequests, ujson import time def connect_wifi(ssid, password): wlan network.WLAN(network.STA_IF) wlan.active(True) if not wlan.isconnected(): print(connecting to network...) wlan.connect(ssid, password) while not wlan.isconnected(): time.sleep(1) print(network config:, wlan.ifconfig()) def get_weather(api_key, city): # 示例URL请替换为真实的API地址 url https://api.seniverse.com/v3/weather/now.json?key%slocation%slanguagezh-Hansunitc % (api_key, city) try: response urequests.get(url) data ujson.loads(response.text) response.close() # 解析数据结构因API而异 weather data[results][0][now] temperature weather[temperature] condition weather[text] # 如“晴”、“多云” humidity weather[humidity] return temperature, condition, humidity except Exception as e: print(获取天气失败:, e) return None, None, None # 在主程序中 connect_wifi(你的WiFi名, 你的密码) temp, cond, humi get_weather(你的API密钥, beijing)第三步设计显示界面。这是最体现创意的地方。我们的屏幕是240x240可以设计一个简洁的布局。例如顶部显示城市名称和日期时间中间大字体显示当前温度和一个简单的天气图标晴、雨、雪等底部显示湿度、风速等信息。为了美观我们可以绘制一些简单的图形元素比如温度计图标、湿度水滴图标。天气图标可以用framebuf的绘图函数fill_rect,circle,line等来绘制也可以预先做好小尺寸的位图数组直接blit上去。第四步整合驱动与字库显示。将我们之前调试好的ST7789驱动类和FontRenderer类引入项目。在主循环中先初始化屏幕和字库。然后定期比如每10分钟调用get_weather函数获取新数据。当数据更新后重新绘制屏幕。绘制时要注意1) 对于不变的部分如标签文字“温度”、“湿度”可以预先画好。2) 对于变化的数据温度值、天气状况文字需要先清除旧的数据区域用背景色填充矩形再绘制新的文本。这里就用到我们字库类的render_text方法了确保中文能正确显示。第五步处理网络异常与低功耗。实际部署时网络可能不稳定。我们需要增加异常处理当获取天气失败时保留上一次成功的数据继续显示并在屏幕角落显示一个小的错误标志或重试倒计时。另外如果你希望设备是电池供电那么功耗就很重要。可以在每次更新显示后让ESP32进入深度睡眠deepsleep模式睡上几分钟再醒来工作这样可以极大地延长电池续航。不过要注意深度睡眠会断开Wi-Fi连接醒来后需要重新连接。我在实现这个项目时遇到的一个典型问题是内存不足MemoryError。尤其是在同时分配了帧缓冲区、网络接收缓冲区、JSON解析对象时ESP32的RAM可能捉襟见肘。我的解决办法是1) 使用gc.collect()在关键操作后手动回收内存2) 尽量复用缓冲区比如用同一个字节数组来接收网络数据3) 如果使用中文字库文件确保它存放在外部SPI Flash文件系统如LittleFS中而不是一次性读入内存。经过这些优化系统就能稳定运行了。最终你将得到一个显示美观、信息实时更新的桌面天气站。它不仅能显示天气你还可以扩展其功能比如显示室内温湿度传感器数据、日程提醒甚至作为一个迷你相框。通过这个完整的项目你不仅学会了驱动屏幕和显示中文更掌握了如何将MicroPython、硬件和网络服务结合起来创造出真正有用的智能硬件应用。这其中的乐趣和成就感远不是点个灯、打印个“Hello World”可以比拟的。希望你能在此基础上继续探索做出更棒的作品。