ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

GC9A01圆屏驱动实战:从ST7789移植到ESP32避坑指南

2026/10/2 3:35:46 拓冰建站 浏览量
GC9A01圆屏驱动实战:从ST7789移植到ESP32避坑指南 简介针对ESP32与GC9A01彩色TFT屏的MicroPython驱动资源面向物联网及嵌入式开发者解决从零编写屏驱代码的耗时难题。压缩包共25个文件以py源码为主辅以mpy预编译模块、raw图像与bmp位图素材另有license与md说明整体仅233KB轻量易部署。已有190人学习下载具备一定参考热度。资源内含完整的SPI通信初始化、基础图形绘制、文字显示、图像渲染及滚动、弧线等示例代码均经实测可用并附带注释作者承诺提供技术支持遇到问题可寻求协助有助于开发者快速完成屏幕接入并专注于项目逻辑实现。1. 圆屏上手前先知道GC9A01和ST7789的差别在哪里GC9A01这块240x240圆形LCD很多人第一次拿到就当ST7789用结果不是偏色就是整个圆环错位一圈我看着也踩过同样的坑。最后把ST7789驱动改了三个点才稳定下来init序列里的厂商解锁、行列地址偏移、MADCTL的RGB/BGR位。这套方案在ESP32系列上跑得很稳SPI 40MHz时全屏填充一帧大约30ms连续运行几小时不花屏。适合做手表表盘、旋钮面板、桌面小摆件这类需要一块圆屏显示状态信息的设备。如果你手里正好有一块GC9A01想用MicroPython快速点亮并让它显示文字、图形和图片这篇就是给你准备的。2. 选型与接线用ST7789驱动改GC9A01的必经路GC9A01本身是一颗SPI接口的TFT驱动芯片命令集和ST7789高度相似所以现成的ST7789驱动代码大部分可以直接复用但直接复制过去会出各种怪问题。先讲清楚硬件怎么接、驱动为什么这么改后面跑代码才不会到处找原因。2.1 引脚与接线先排除黑屏的硬件问题常见的GC9A01模组是7个引脚VCC、GND、SCL、SDA、RES、DC、CS部分模组还单独引出BLK背光控制。接线先按这个表来我一般把背光单独接PWM引脚而不是直接接3.3V这样后面调亮度方便也避免背光一直满电流导致发热。模组引脚作用接ESP32-S3说明VCC电源3V3不要接5V部分带稳压的模组可以5V但逻辑电平还是3.3VGND地GND必须和单片机共地SCLSPI时钟GPIO12对应SPI2 SCKSDASPI数据GPIO11对应SPI2 MOSI只写不读所以MISO不接RES复位GPIO14低电平复位不能用悬空DC数据/命令切换GPIO9高电平写数据低电平写命令CS片选GPIO10拉低选中BLK背光GPIO15接PWM频率建议1kHz以上注意几个细节SCL和SDA之间不要飞太长的线尤其是用杜邦线时超过15cm后40MHz时钟就很容易出现串扰。RES引脚如果板上已经接了上拉电阻可以不接MCU但我习惯还是接一个GPIO控制这样初始化时能手动复位避免上电时序不稳定。CS必须接不能直接接地虽然只挂一块屏时接地也能用但后续想加第二个SPI设备就必须靠CS区分。接好线后先不急着写驱动直接用一个最简单的GPIO测试确认背光和复位正常from machine import Pin import time blk Pin(15, Pin.OUT) rst Pin(14, Pin.OUT) blk.value(1) rst.value(0) time.sleep_ms(50) rst.value(1)这段代码先把背光点亮、复位脚拉低再释放。如果这时屏幕有轻微闪烁或者背光亮了说明电源和基本引脚没问题。如果背光都不亮先检查VCC和GND再用万用表量模块背面LDO输出是否有3.3V很多模组虽然标注5V输入但板上其实有一颗RT9013之类的稳压芯片供电问题在圆屏模组上比例不低。2.2 驱动选型为什么不自造轮子而是改ST7789的驱动MicroPython下给GC9A01写驱动最常见也最可靠的做法是从现有的ST7789驱动改而不是从寄存器手册一行一行写。原因是GC9A01的像素格式、窗口设置、SPI时序和ST7789基本一致真正不同的地方就三处初始化时多了厂商解锁命令、行列起始地址可能有偏移、颜色顺序位不同。ST7789驱动的经典代码结构一般是构造函数里传SPI、CS、DC、RST初始化时发送一串寄存器命令然后提供fill、pixel、hline、blit_buffer这些绘图方法。改造时重点看init函数def _init(self): # 第一处差异GC9A01需要先解锁厂商命令 # ST7789的init里没有这段直接跳转到其他设置 self._cmd(0xFE) self._cmd(0xEF) self._cmd(0xFE) self._cmd(0xEF) # 第二处差异像素格式统一设为RGB565 self._cmd(0x3A, 0x05) # 第三处差异行列地址范围 # 很多GC9A01模组的起始地址不是0,0而是整体偏移了1~2像素 self._cmd(0x2A, bytes([0x00, 0x00, 0x01, 0x6F])) self._cmd(0x2B, bytes([0x00, 0x00, 0x01, 0x6F])) self._cmd(0x36, self._madctl(self._rotation)) self._cmd(0x11) time.sleep_ms(120) self._cmd(0x29)这里三个差异点都需要解释。第一处0xFE/0xEF是GC9A01的厂商命令解锁ST7789不需要很多现成驱动改过来之后屏幕灰白一片就是因为这段被当成无效命令跳过了。第二处0x3A设为0x05表示16位色RGB565如果这里保持ST7789的某些默认值颜色会发紫或者显示雪花。第三处行列地址范围0到2390x6F是十六进制的111也就是239但如果你的模组整体显示偏移需要把0x2B的起始地址改成1或2这在第5章排查里细说。颜色顺序差异在MADCTL里。_madctl这个方法会按旋转角度计算MX、MY、MV几个位但GC9A01和ST7789默认的RGB/BGR位可能相反。def _madctl(self, rotation): # rotation: 0, 1, 2, 3 对应 0°, 90°, 180°, 270° # MV表示行列交换MX左右镜像MY上下镜像 # bit30x08是RGB/BGR顺序0表示RGB1表示BGR if rotation 0: return 0x48 # 实测我的模组是BGR顺序 elif rotation 1: return 0x28 elif rotation 2: return 0x88 elif rotation 3: return 0xA8我手里的模组MADCTL设为0x48时颜色正常如果你的屏幕红蓝互换就把这个值加减0x08再试。旋转角度也可以在这里一次性调整不需要改代码逻辑。选型结论很明确去找一份结构干净的ST7789驱动把init函数按上面替换保留原有的绘图方法就得到了一份可用的GC9A01驱动。不必从零写也不必去网上找所谓的专用完美驱动因为很多专用驱动反而绑定了特定硬件平台。2.3 固件与开发环境用Thonny连上板子再谈显示代码改好之后需要一个能交互调试的环境。常见做法是装Thonny连接ESP32-S3的USB口在解释器里选择MicroPython(ESP32)。注意连接前先按住板子上的BOOT按键再插USB这样可以进入烧录模式然后在Thonny的“解释器”菜单里选择对应的端口点击“安装或更新固件”把下载好的MicroPython固件烧进去。固件版本建议选带SPI驱动支持的新版但具体哪个版本不是决定性因素只要不是特别老的1.19之前版本都能跑。连上之后在Thonny的Shell窗口里先做一次基础验证import machine print(machine.freq())能打印出CPU频率就说明REPL正常工作。然后把上面改好的驱动命名为gc9a01.py通过Thonny保存到开发板根目录后面所有示例代码都能import gc9a01直接使用。这里有个小坑如果你把脚本保存为main.py板子上电后会自动执行调试阶段建议先不要写main.py直接用Shell里的代码手动测试否则显示代码崩溃一次就进不了REPL还得重新擦除固件。3. 在Thonny里跑通第一块圆屏最小驱动代码与验证驱动文件放好后真正的乐趣从点亮开始。建议用三步递进验证先全屏纯色再显示文字最后显示图片。每一步都能筛掉一类问题。3.1 最小点亮代码全屏纯色是最快的自检新建一个文件输入下面的代码注意调整引脚编号与你的接线一致from machine import Pin, SPI import gc9a01 # SPI2: SCKGPIO12, MOSIGPIO11, MISOGPIO13 # GC9A01只写不读MISO可以不接但SPI对象里仍要指定一个引脚 spi SPI(2, baudrate40_000_000, sckPin(12), mosiPin(11), misoPin(13)) display gc9a01.GC9A01( spi, csPin(10, Pin.OUT, value1), dcPin(9, Pin.OUT, value0), rstPin(14, Pin.OUT, value1), backlightPin(15, Pin.OUT, value1), rotation0, width240, height240, ) # 全屏填充绿色RGB565格式的颜色值 display.fill(0x07E0)这个Demo的逻辑很简单创建一个SPI对象实例化GC9A01屏对象然后fill填充整屏。看三个参数baudrate设40M这是我在杜邦线条件下反复试出的稳定值cs和dc都指定了初始电平避免上电瞬间误操作rotation0表示不旋转因为圆屏本身无所谓横竖坐标原点在左上角。如果这一步屏幕全绿恭喜接线和init正确。如果屏幕花屏、偏色、或者只有背光亮先检查SPI模式很多ST7789驱动用mode 0polarity0, phase0但部分GC9A01模组在mode 3下更稳。临时改成下面这样对比spi SPI(2, baudrate40_000_000, polarity1, phase1, sckPin(12), mosiPin(11), misoPin(13))mode切换是排查花屏最高效的手段一次不行就换另一种。如果仍然异常回到第2章的引脚测试确认背光和复位。3.2 显示文字与简单图形确认坐标系和旋转方向纯色通过后接着验证文字和坐标定位。驱动里常见的做法是外挂一个字体模块比如把vga2_8x16这类字体文件存到板子上然后配合Writer类输出字符串from writer import Writer import vga2_8x16 as font w Writer(display, font, verboseFalse) w.set_textpos(60, 80) w.printstring(GC9A01 OK) # 画一个圆确认坐标和颜色 display.fill_circle(120, 120, 50, 0xF800)Writer类的set_textpos接收(x, y)坐标原点在屏幕左上角x向右y向下。textpos参数顺序各版本可能相反如果文字跑到了想不到的位置把两个参数对调试试。fill_circle的中心坐标也是同样的坐标系圆屏虽然外形是圆的但驱动内部仍是240x240的矩形坐标四个角区域在物理上被外壳遮挡画图时不用刻意避开。这一步常见问题是文字颜色不对或文字背景一大块黑色。原因通常是字体模块默认把背景色设为黑色如果屏是深色背景看不出来换成浅色背景就露馅。解决方法是Writer初始化后手动设text背景色和前景色w.set_color(0xFFFF, 0x0000) # 白字黑底set_color两个参数分别是前景色和背景色前景色就是文字颜色背景色可以改成透明屏同色这样不会有黑色方块。字体模块选择上8x16的英文字体省内存想显示中文需要单独的Unicode字库文件后面进阶章节会提。3.3 显示一张图RGB565裸数据与blit_buffer圆屏最实用的功能是显示小尺寸图片比如logo。常见做法是先在一台电脑上用Python把图片转换成RGB565格式的裸数据再传到开发板上。下面的代码假设你已经有一张120x120的图片裸数据文件logo.rawIMAGE_W 120 IMAGE_H 120 with open(logo.raw, rb) as f: raw f.read() # 把图片画到屏幕中心 display.blit_buffer(raw, x60, y60, wIMAGE_W, hIMAGE_H)blit_buffer是驱动里最核心的显示方法四个参数含义x和y是图片左上角坐标w和h是图片宽高raw数据每两个字节对应一个像素的RGB565值。如果你的图片是ARGB8888格式需要先做通道转换常见做法是在转换脚本里按小端序把RGB三个通道拼成16位颜色值。图片尺寸不要超过240x240圆屏边角会被裁剪所以设计素材时把主要内容放在中心半径120像素以内。如果显示出来的图片颜色发暗或偏色基本就是raw数据格式不对。RGB565里红色占高5位绿色占中间6位蓝色占低5位字节顺序是小端即低字节在前高字节在后。转换脚本里最容易出错的是颜色通道顺序我在转换时会把每个像素值拆开打印几个样本对比屏幕效果确认无误后再整体刷入。4. 刷新与性能把SPI时钟、缓冲区、背光PWM调到能长期用一块圆屏不能只点亮一次就完事实际项目里要做表盘、数值刷新、动态状态图标这些都对刷新效率和稳定性有要求。这一章说清楚性能瓶颈在哪里、怎么调。4.1 SPI时钟不是越高越好GC9A01内置GRAM写入数据后立即显示不需要软件控制刷新率所以帧率瓶颈完全在SPI传输速度。全屏填充240x240x2字节等于115200字节SPI时钟40MHz时理论传输时间约28.8毫秒加上命令和地址位实际一帧约35毫秒也就是大约28帧。如果升到60MHz理论时间约19毫秒但有木有杜邦线、模块背面走线、电源纹波这些变量60MHz下很容易偶发花屏。SPI时钟理论全屏传输时间实际感受稳定性10MHz115ms明显卡顿非常稳定20MHz58ms可接受稳定40MHz29ms流畅杜邦线下稳定60MHz19ms更快但挑线材容易花屏我个人习惯最终产品用PCB排线或FPC连接时可以上60MHz开发调试用杜邦线时固定在40MHz。还有一种折中做法是多块区域刷新屏幕大部分区域不动只更新变化的数字或图标这时SPI时钟反而不用太高因为每次传输的数据量小时钟低一点换来稳定性非常划算。4.2 行缓冲和区域刷新省内存也减少花屏MicroPython下内存是稀缺资源全屏缓冲需要115KB大多数ESP32板子已经吃紧。所以驱动里fill、pixel这类方法内部通常是一次写一行或几行缓冲而不是一次性申请整帧缓冲区。你自己做局部刷新时也要顺着这个思路只提交变化区域。一个典型场景是刷新温度数字每次只更新一个小矩形区域代码比全屏刷新高效得多def update_temp(temp_str): # 先清除旧数值区域假设数值在(40, 100)开始占60x32像素 display.fill_rect(40, 100, 60, 32, 0x0000) # 在清除后的位置重新写入字符串 w.set_textpos(40, 100) w.printstring(temp_str)fill_rect的参数依次是x、y、宽、高、颜色。这里清除区域必须比文字的实际显示范围大一点否则会留下笔画残影。如果你用Writer输出文字还需要算好字体宽高8x16字体下每个字符占8像素宽、16像素高一个4位数字字符串就是32像素宽加上左右各留4像素就是40像素的清除宽度。局部刷新还有一个隐藏好处SPI传输的数据量小总线负载低长线场景下不容易出现最后几行花屏。我在做旋钮面板时整个界面只有当前参数值在变局部刷新后SPI时钟降到20MHz连续跑一天没有一次性花屏。4.3 背光PWM别让闪烁成为“玄学”圆屏的背光如果不做PWM直接接高电平也能亮但亮度不可调而且某些模组的背光电路对电源纹波敏感全亮时会让屏显出现横条纹。常见做法是用PWM控制BLK引脚from machine import Pin, PWM blk_pwm PWM(Pin(15), freq1000, duty_u1632768)freq设1000Hzduty_u16设32768也就是约50%占空比。注意freq不能太低低于500Hz时人眼能感觉到闪烁尤其是在屏幕边缘和圆形过渡区域看起来像水波纹。Freq超过5000Hz时部分模组的背光驱动电路会发出轻微啸叫声所以1000Hz是最常用的中间值。如果你的驱动文件里已经有backlight引脚参数通常内部会自动创建PWM这时只要传入Pin对象就行不需要再单独建一个PWM实例。有些驱动里的backlight参数只做开关不提供亮度控制那你就在驱动外自己建PWM然后调到最小亮度验证一下如果最低亮度还是刺眼说明PWM频率和占空比参数需要调整而不是驱动有问题。5. 避坑指南GC9A01最容易翻车的几个现场这一段是亲测过程中最值钱的部分。下面五条按“现象→原因→解决”记录遇到问题直接对照查。5.1 画面整体偏移一圈现象是圆屏显示的内容不是居中而是整个向左或向上偏了1到3个像素圆环边缘出现一条固定的杂色边。原因大多是GC9A01模组在出厂时面板安装位置和IC内部GRAM默认窗口没有对齐导致行列起始地址需要偏移。解决方法是修改set_window里的行列起始地址。通常驱动里会包装成_set_window或类似方法你直接调用底层命令调整# 原起始地址是0,0若画面向右偏移了2像素 # 把列地址起点改成2 display._cmd(0x2A, bytes([0x00, 0x02, 0x01, 0x6F])) # 把行地址起点也改成2 display._cmd(0x2B, bytes([0x00, 0x02, 0x01, 0x6F]))0x2A控制列地址范围0x2B控制行地址范围。后面四个字节分别是起始高8位、起始低8位、结束高8位、结束低8位。240像素的结束地址是239二进制0x6F对应十进制的111所以末尾固定为0x01 0x6F。如果偏移方向是左上就把起点改成0如果画面原本偏了但方向不确定可以逐个试起点0到3每次改动后全屏填充纯色观察边缘是否干净。5.2 屏幕灰白或只有噪点现象是背光亮、屏幕色彩花乱或一片灰白。原因基本上可以锁定在两种init时没有执行厂商解锁命令或者SPI读写模式不匹配。GC9A01的厂商解锁命令0xFE/0xEF必须先发否则后续寄存器命令全部无效屏幕只能显示乱码。这个错误在从ST7789驱动改过来的过程中太常见了因为ST7789没有这一段。解决方法是确认init序列里0xFE/0xEF出现了两次并且出现在所有其他命令之前。第二步把SPI的polarity和phase从(0,0)改成(1,1)再一次一次试。注意修改后需要重新初始化屏幕不能简单重发几个命令最好是复位整个display对象或者调用display.reset()再执行init。5.3 颜色发紫、红蓝互换现象是屏幕上红色像素显示成蓝色蓝色显示成红色整个画面像滤镜翻转。原因很明确MADCTL里的RGB/BGR位设置反了。这个位在0x36寄存器里是bit3值为0x08。如果你的MADCTL值设成了0x40那实际颜色顺序是RGB而模组本身是BGR就会互换。解决方法是保持旋转参数不变单独翻转0x08这一位。比如当前rotation0时MADCTL是0x48改成0x40就是RGB顺序改成0x48就是BGR顺序。判断方法很简单显示纯红0xF800如果看起来发蓝就把这一位翻转。颜色验证代码display.fill(0xF800) # 纯红 time.sleep_ms(1000) display.fill(0x07E0) # 纯绿 time.sleep_ms(1000) display.fill(0x001F) # 纯蓝三种颜色分别显示正常就说明RGB顺序和像素格式都对了。白色0xFFFF最容易掩盖顺序问题所以别用白色验证。5.4 长线花屏和偶发乱码现象是刷新过程中屏幕某些行出现彩色噪点或者整屏随机闪烁一下但复位后又能正常。原因通常是SPI时钟过高、杜邦线过长或模块供电不稳。这个坑在40MHz以下很少出现一旦超过40MHz就容易冒出来所以它在接线阶段就要预防。结合经验核心原因往往不是单一因素而是“时钟高线长电源纹波”三件事叠加。解决思路从三个方向同时下手把baudrate降到20MHz或10MHz看是否复现缩短SCL和SDA线缆长度并尽量避免两根线平行走太长距离。电源方面在模块的VCC和GND之间并联一个10uF到100uF的陶瓷电容如果模组引脚附近空间不够就焊在杜邦线连接处。我调试时遇到过一例降频后仍有偶发花屏最后发现是USB供电的5V转3.3V稳压模块带负载能力差换了一个低压差LDO后问题消失。注意背光电流必须计入电源余量。5.5 Thonny里执行一半死机或黑屏现象是REPL里运行刷新代码刷到一半开发板突然重启或者屏幕黑掉再亮但程序停住。原因两大类一类是内存不足MicroPython在动态申请大块buffer时失败会抛出MemoryError然后REPL复位另一类是电源瞬时跌落尤其当背光全亮、全屏填充同时进行时电流峰值可能让板载稳压器进入保护。解决方法是先看REPL输出有没有MemoryError。如果有把全屏填充改成局部刷新或者用gc.collect()手动回收内存。如果是电源问题先把背光PWM占空比降低到50%再测试或者初始化时先延时200ms再开启背光避开上电瞬间的大电流。一个实用习惯把display.init()里最后开的命令从0x29改为先延时再开时序更稳self._cmd(0x11) time.sleep_ms(150) self._cmd(0x29) time.sleep_ms(50)6. 把驱动封成一个小部件圆屏局部刷新与帧率验证GC9A01驱动跑通之后建议你做两件收尾工作把常用更新逻辑封装成一个状态显示类以及测量一下真实帧率知道自己在这块屏上还能做多复杂的动画。封装数字显示类是减少反复写fill_rect和text代码的好办法class UpdateText: def __init__(self, display, writer, x, y, w, h): self.d display self.w writer self.x x self.y y self.w w self.h h self.last None def show(self, s): if s self.last: return # 清除旧区域 self.d.fill_rect(self.x, self.y, self.w, self.h, 0x0000) # 写新字符串 self.w.set_textpos(self.x, self.y) self.w.printstring(s) self.last s这里把位置和大小写死每次只清除固定区域再画新内容字符串没变化时跳过刷新能大幅减少没必要的SPI传输。使用它的典型场景是循环更新一个计数器counter UpdateText(display, writer, 40, 100, 60, 32) for i in range(100): counter.show(str(i)) time.sleep_ms(200)帧率验证可以用一个简单的计时循环来完成。下面代码测的是全屏填充的实际帧数注意只测fill而不是复杂绘图因为fill的速度基本等于SPI最大吞吐import time start time.ticks_ms() for _ in range(50): display.fill(0x001F) # 蓝色 display.fill(0xF800) # 红色 dt time.ticks_diff(time.ticks_ms(), start) fps 100 / (dt / 1000) print(f全屏填充帧率: {fps:.1f} fps)以40MHz SPI为例这个数字应该在25到30之间。如果明显偏低看SPI时钟是否真生效如果花屏先降频。我自己的习惯是把这三个数值作为驱动改动的“体检指标”PWM频率、SPI时钟、全屏填充帧率每次换模组或改板子先跑一遍这三项全部符合才算通过。真正做界面时则尽量不全屏填充而是用局部刷新把单次更新控制在几百微秒内这样动画和数字滚动都能保持流畅。这套方案已经被我在两块不同批次的GC9A01模组上验证过init序列和偏移虽然略有差异但排查路径完全一致先确认解锁命令再调行列地址最后查颜色顺序。希望帮到你。本文还有配套的精品资源点击获取