树莓派Zero SPI LCD HAT驱动全解析:从ST7789/ST7735S到帧缓冲优化

树莓派Zero SPI LCD HAT驱动全解析:从ST7789/ST7735S到帧缓冲优化
1. 项目概述Zero LCD HAT (A) 是什么如果你手头有一块小巧的树莓派 Zero想给它配上一块屏幕但又不想占用太多宝贵的 GPIO 引脚或者希望有一个即插即用的显示方案那么 Zero LCD HAT (A) 很可能就是你正在寻找的东西。简单来说它是一款专为树莓派 Zero 系列包括 Zero, Zero W, Zero 2 W设计的 SPI 接口 LCD 扩展板。这块板子最吸引人的地方在于它直接扣在 Zero 的 GPIO 排针上通过 SPI 总线与树莓派通信无需额外的复杂连线就能驱动一块分辨率通常为 240x320 或 240x240 的彩色 LCD 屏幕。从技术角度看这块 HAT 的核心是驱动芯片最常见的是 ST7789 或 ST7735S。这两个都是非常流行的 SPI 接口 TFT LCD 控制器。它们负责接收来自树莓派 SPI 接口的像素数据和控制命令并将其转换为屏幕上的图像。SPI 协议在这里扮演了关键角色它是一种高速、全双工、同步的串行通信总线特别适合驱动这种像素数据量不大但要求实时性较高的显示设备。相比于并行接口SPI 只需要少数几根线数据线、时钟线、片选线等极大地节省了 GPIO 资源这对于引脚数量有限的树莓派 Zero 来说至关重要。我最初接触这块板子是想给一个便携式的小项目做个状态显示器。市面上很多屏幕需要焊接或者飞线而 Zero LCD HAT (A) 的“HAT”设计Hardware Attached on Top让它变得极其友好——对准 GPIO 排针轻轻一按物理连接就完成了。剩下的工作几乎全部在软件层面。无论是用来显示系统状态、作为小型信息终端还是运行一个简单的图形界面它都能胜任。接下来我们就深入拆解一下这块板子背后的技术细节、驱动原理以及如何一步步让它亮起来并稳定工作。2. 核心芯片解析ST7789 与 ST7735S 的异同Zero LCD HAT (A) 的显示效果和驱动复杂度很大程度上取决于它采用的驱动芯片。ST7789 和 ST7735S 是两款最常见的选择它们都来自意法半导体STMicroelectronics功能相似但在性能和特性上有一些细微差别了解这些有助于你在驱动和调试时更有针对性。2.1 ST7789高性能之选ST7789 可以看作是 ST7735S 的升级版或高性能版本。它最大能支持 240x320 (QVGA) 及更高分辨率的屏幕并且内部集成了帧缓存Frame Memory这对于驱动来说是个好消息。拥有帧缓存意味着树莓派可以通过 SPI 一次性将一帧图像数据发送给 ST7789芯片会自己管理这些数据并持续刷新屏幕在此期间树莓派的 CPU 可以被释放出来处理其他任务降低了系统负载。在实际使用中ST7789 驱动的屏幕通常色彩表现更好刷新率也更高一些。它的指令集比 ST7735S 更丰富支持更多的显示功能比如局部刷新、更灵活的色彩格式设置等。如果你拿到手的 HAT 屏幕分辨率是 240x320 的那几乎可以确定用的是 ST7789。在驱动时你需要关注它的初始化序列特别是设置显示方向、像素格式通常是 RGB565即16位色、以及开窗设置读写数据的区域等命令。一个常见的初始化命令是0xE0它用于调整伽马Gamma曲线以改善显示的色彩和对比度。如果初始化后屏幕颜色怪异检查伽马校正相关的命令序列往往是解决问题的第一步。2.2 ST7735S经济实用型ST7735S 则更常见于小尺寸、低成本屏幕典型分辨率是 128x160 或 132x162当然也有用于 240x240 屏幕的变种。它通常没有集成帧缓存或者缓存很小。这意味着在“写屏”模式下树莓派需要持续不断地通过 SPI 发送显示数据来维持画面这对 SPI 总线的速度和 CPU 占用率有一定要求。不过对于静态或更新不频繁的画面这完全不是问题。ST7735S 的指令集相对简单驱动起来也更直接。很多开源驱动库对它的支持非常成熟。它的初始化序列通常比 ST7789 短一些。需要注意的是不同厂家、不同分辨率的 ST7735S 屏幕其初始化参数如行列偏移、颜色顺序可能略有不同。这就是为什么你从网上找到的驱动代码有时需要微调xstart,ystart,colmod等参数才能在你的屏幕上正常显示。中文数据手册里通常会详细列出这些寄存器配置对照修改即可。2.3 如何区分与选型对于终端用户来说最直接的区分方法是看屏幕分辨率。240x320 大概率是 ST7789而 240x240 则两种都有可能需要查看产品说明书或通过调试判断。从驱动角度你可以尝试用 ST7789 的驱动去初始化一块 ST7735S 屏幕如果完全没反应或显示异常再换用 ST7735S 的驱动试试。注意在购买或使用前尽量向卖家索要具体的芯片型号和屏幕参数。这将为后续的驱动开发省去大量猜测和调试的时间。我曾因为想当然地以为一块 240x240 屏是 ST7789结果调了一下午都没点亮最后才发现是 ST7735S换了驱动库瞬间成功。3. SPI 通信协议驱动屏幕的“语言”要让树莓派和 LCD 驱动芯片“对话”必须遵循一套共同的规则这就是 SPI 协议。理解 SPI 是成功驱动任何 SPI 外设包括这块 LCD HAT的基础。3.1 SPI 基础与四种模式SPI 是一种主从式协议树莓派作为主机MasterLCD 驱动芯片作为从机Slave。通信至少需要四根线SCLK (Serial Clock)时钟信号由主机产生用于同步数据。MOSI (Master Out Slave In)主机输出、从机输入数据线。MISO (Master In Slave Out)主机输入、从机输出数据线。注意在驱动单纯的 LCD 屏幕时数据流主要是树莓派向屏幕发送命令和像素数据因此 MISO 线可能未被连接或不需要关心。但有些触摸屏功能或读寄存器操作会用到它。CS/SS (Chip Select / Slave Select)片选信号低电平有效。当主机需要和某个从机通信时就将其对应的 CS 线拉低。SPI 通信的核心是时钟极性CPOL和时钟相位CPHA这两个参数它们组合成了四种模式Mode 0, 1, 2, 3CPOL时钟空闲时的电平。0 表示空闲时为低电平1 表示空闲时为高电平。CPHA数据采样的时钟边沿。0 表示在第一个时钟边沿上升沿或下降沿取决于 CPOL采样1 表示在第二个时钟边沿采样。对于 ST7789 和 ST7735S最常用的是 SPI Mode 0即 CPOL0, CPHA0。这意味着时钟空闲时为低电平数据在时钟的上升沿被采样。在树莓派上配置 SPI 接口时必须确保模式设置正确。3.2 硬件片选与软件片选片选CS信号的管理有两种方式硬件片选使用 SPI 控制器专用的 CS 引脚如树莓派上的 CE0, CE1。由硬件自动控制在发起 SPI 传输时自动拉低对应的 CS 线传输结束后自动拉高。这种方式简单可靠不占用 CPU 资源。软件片选使用一个普通的 GPIO 引脚来模拟 CS 信号。在代码中在传输数据前手动将该 GPIO 拉低传输结束后再拉高。Zero LCD HAT (A) 的设计通常会使用硬件片选连接到树莓派 Zero 的CE0或CE1引脚。你可以在驱动代码中指定使用哪个片选。使用硬件片选是首选因为它更稳定。但在某些特殊情况下比如需要同时驱动多个 SPI 设备而硬件片选引脚不够用时就需要使用软件片选。这时需要注意手动控制 CS 信号的时序要准确特别是在连续发送多条命令或大量数据时要确保 CS 信号在整组操作中保持有效低电平而不是在每条命令间频繁跳变。3.3 SPI 速度与 DMA 应用SPI 的通信速度时钟频率直接影响屏幕的刷新率。树莓派的 SPI 接口最高可以跑到几十 MHz。对于 ST7789/ST7735S通常可以设置在 20MHz 到 40MHz 之间。速度越快刷屏越快但也要考虑屏幕驱动芯片和布线能否承受。一开始调试时可以先用一个较低的速度如 10MHz确保通信稳定后再逐步提高。当需要高速、连续地刷新屏幕比如播放动画时频繁的 SPI 数据传输会大量占用 CPU。这时DMA直接内存访问技术就派上用场了。DMA 控制器可以在不打扰 CPU 的情况下直接在内存和 SPI 外设之间搬运数据。树莓派的 BCM283x 芯片支持 SPI DMA。在高级的驱动库如fbcp-ili9341或一些经过优化的st7789库中会启用 DMA 来传输帧缓冲区数据从而将 CPU 占用率降到极低实现非常流畅的显示效果。不过配置 DMA 相对复杂涉及到内存对齐、通道分配等问题一般的简单应用使用标准的 SPI 轮询Polling方式就足够了。4. 树莓派系统准备与 SPI 启用在连接硬件之前我们需要确保树莓派操作系统Raspberry Pi OS已经为 SPI 通信做好了准备。这个过程主要是通过配置工具和修改系统文件来完成。4.1 启用 SPI 接口树莓派默认可能没有启用 SPI 接口。你需要通过raspi-config工具来开启它。打开终端输入命令sudo raspi-config使用方向键选择Interface Options然后回车。选择SPI然后回车。当询问是否启用 SPI 接口时选择Yes然后回车。完成后选择Finish退出raspi-config。系统会提示需要重启选择Yes重启树莓派。重启后SPI 驱动模块应该已经加载。你可以通过以下命令检查 SPI 设备是否已出现ls /dev/spi*如果看到类似/dev/spidev0.0和/dev/spidev0.1的设备文件说明 SPI 主设备 0 的两个硬件片选通道CE0 和 CE1已经就绪。你的 LCD HAT 很可能使用的是/dev/spidev0.0。4.2 安装必要的库与工具为了编程控制 SPI 设备我们需要安装 Python 的 SPI 库。最常用的是spidev和RPi.GPIO如果需要软件控制额外的 GPIO如背光或复位引脚。sudo apt update sudo apt install python3-dev python3-pip sudo pip3 install spidev RPi.GPIO对于 C 语言开发者可能需要安装wiringPi已停止维护但仍有使用或直接使用 Linux 的 SPI 设备文件接口进行编程。4.3 物理连接与引脚确认将 Zero LCD HAT (A) 对准树莓派 Zero 的 40-pin GPIO 排针轻轻按下确保所有引脚接触良好。这是 HAT 设计最大的便利之处几乎不可能接错线。但了解背后的引脚连接关系对调试有帮助VCC (3.3V)连接到树莓派的 3.3V 引脚为屏幕和驱动芯片供电。GND接地。SCLKSPI 时钟连接到 GPIO 11 (BCM)。MOSISPI 主机输出连接到 GPIO 10 (BCM)。MISO通常未连接或连接至 GPIO 9 (BCM)。CS (CE0)片选 0连接到 GPIO 8 (BCM)。这是硬件片选。DC (Data/Command)这是一个非常重要的引脚。它告诉驱动芯片当前通过 SPI 发送的是命令Command还是数据Data。通常连接到树莓派的一个普通 GPIO例如 GPIO 24 或 25。在代码中需要手动控制这个引脚的电平。RST (Reset)复位引脚低电平有效。也连接到一个普通 GPIO用于在初始化时对驱动芯片进行硬件复位。BL (Backlight)背光控制通常连接到一个支持 PWM 的 GPIO以便调节屏幕亮度。在你的驱动代码中需要根据 HAT 的实际设计正确指定这些 GPIO 的编号使用 BCM 编号即 Broadcom 编号而非物理引脚编号。5. 驱动编写与初始化序列详解一切准备就绪后最核心的部分就是编写驱动代码。我们将以 Python 为例使用spidev和RPi.GPIO库一步步实现一个最基本的 ST7789/ST7735S 驱动框架。5.1 建立 SPI 连接与 GPIO 控制首先初始化 SPI 设备和相关的 GPIO 引脚。import spidev import RPi.GPIO as GPIO import time class LCD_HAT: def __init__(self, spi_device0, spi_cs0, dc_pin24, rst_pin25, bl_pin18): # 初始化 SPI self.spi spidev.SpiDev() self.spi.open(spi_device, spi_cs) # 打开 /dev/spidev0.0 self.spi.max_speed_hz 40000000 # 设置 SPI 速度例如 40MHz self.spi.mode 0 # SPI Mode 0 # 初始化 GPIO self.dc_pin dc_pin self.rst_pin rst_pin self.bl_pin bl_pin GPIO.setmode(GPIO.BCM) GPIO.setup(self.dc_pin, GPIO.OUT) GPIO.setup(self.rst_pin, GPIO.OUT) GPIO.setup(self.bl_pin, GPIO.OUT) # 初始化背光先关闭 GPIO.output(self.bl_pin, GPIO.LOW) self.hardware_reset() time.sleep(0.2) # 等待复位稳定 def hardware_reset(self): 硬件复位 LCD 驱动芯片 GPIO.output(self.rst_pin, GPIO.LOW) time.sleep(0.1) GPIO.output(self.rst_pin, GPIO.HIGH) time.sleep(0.1) def write_command(self, cmd): 发送命令 GPIO.output(self.dc_pin, GPIO.LOW) # DC 置低表示命令 self.spi.xfer2([cmd]) def write_data(self, data): 发送数据单字节或多字节列表 GPIO.output(self.dc_pin, GPIO.HIGH) # DC 置高表示数据 if isinstance(data, list): self.spi.xfer2(data) else: self.spi.xfer2([data])这段代码建立了与 SPI 设备的连接并定义了硬件复位、发送命令和发送数据的基础方法。注意spi.xfer2()方法它执行一次 SPI 传输并返回读取的数据虽然这里我们只关心发送。5.2 发送初始化序列这是最关键的一步。初始化序列是一系列预先定义好的命令和数据用于配置驱动芯片的工作模式、显示方向、颜色格式、伽马曲线等。不同芯片、不同屏幕的初始化序列可能不同。以下是一个 ST7789 驱动 240x320 屏幕的简化初始化序列示例def init_display(self): 初始化显示屏 # 软件复位 self.write_command(0x01) time.sleep(0.15) # 等待复位完成 # 退出睡眠模式 self.write_command(0x11) time.sleep(0.12) # 颜色接口格式设置16位/pixel (RGB565) self.write_command(0x3A) self.write_data(0x55) # 0x55 代表 16bits # 内存数据访问控制 (MADCTL)设置显示方向 # 例如0x00 为竖屏模式0xA0 或 0xC0 可用于旋转 self.write_command(0x36) self.write_data(0x00) # 列地址设置 (CASET)设置 X 方向的起始和结束地址 self.write_command(0x2A) self.write_data([0x00, 0x00, 0x00, 0xEF]) # 0-239 (240列) # 行地址设置 (RASET)设置 Y 方向的起始和结束地址 self.write_command(0x2B) self.write_data([0x00, 0x00, 0x01, 0x3F]) # 0-319 (320行) # 伽马校正设置部分序列 self.write_command(0xE0) self.write_data([0xD0, 0x08, 0x11, ...]) # 具体值需参考数据手册 # 开启显示 self.write_command(0x29) time.sleep(0.1) # 打开背光 GPIO.output(self.bl_pin, GPIO.HIGH)对于 ST7735S初始化序列会有所不同命令和参数需要调整。强烈建议从屏幕供应商或可靠的驱动库如 Adafruit 的 CircuitPython 库中获取准确的初始化序列。一个错误的参数就可能导致屏幕白屏、花屏或颜色错乱。5.3 实现像素绘制与区域填充初始化完成后就可以向屏幕写入图像数据了。基本操作是设置一个“窗口”即要更新的矩形区域然后连续发送该区域内所有像素的颜色数据。def set_window(self, x0, y0, x1, y1): 设置显示窗口更新区域 # 发送列地址范围 self.write_command(0x2A) # CASET self.write_data([x0 8, x0 0xFF, x1 8, x1 0xFF]) # 发送行地址范围 self.write_command(0x2B) # RASET self.write_data([y0 8, y0 0xFF, y1 8, y1 0xFF]) # 发送写内存命令准备接收像素数据 self.write_command(0x2C) # RAMWR def draw_pixel(self, x, y, color): 绘制单个像素点color为16位RGB565颜色值 if x self.width or y self.height: return self.set_window(x, y, x, y) # 发送颜色数据高字节在前 self.write_data([color 8, color 0xFF]) def fill_rect(self, x0, y0, width, height, color): 填充一个矩形区域 x1 x0 width - 1 y1 y0 height - 1 if x1 self.width: x1 self.width - 1 if y1 self.height: y1 self.height - 1 self.set_window(x0, y0, x1, y1) # 计算像素总数 pixel_count (x1 - x0 1) * (y1 - y0 1) # 准备数据缓冲区优化点可以预分配缓冲区 data [] color_hi color 8 color_lo color 0xFF for _ in range(pixel_count): data.append(color_hi) data.append(color_lo) # 一次性发送所有像素数据 GPIO.output(self.dc_pin, GPIO.HIGH) # 分批发送避免数据量过大 chunk_size 4096 # 根据实际情况调整 for i in range(0, len(data), chunk_size): chunk data[i:ichunk_size] self.spi.xfer2(chunk)fill_rect函数演示了如何高效地填充一个区域。关键在于设置好窗口后连续发送像素数据。为了提升速度我们一次性构建了整个区域的颜色数据列表然后通过 SPI 分批发送。对于全屏清屏fill_rect(0, 0, width, height, color)这是最直接的方法。6. 性能优化与高级技巧基础的驱动能让屏幕亮起来但要让显示流畅、高效还需要一些优化技巧。6.1 使用帧缓冲区Framebuffer直接通过 SPI 绘制每个图形指令如画线、画圆效率很低因为每次操作都可能涉及设置窗口和发送少量数据SPI 通信开销很大。更高级的做法是使用“帧缓冲区”Framebuffer。在树莓派的内存中开辟一块区域大小与屏幕分辨率对应例如 2403202 字节对于 RGB565。所有的绘图操作由 Pillow, PyGame 或自定义图形库完成都在这块内存缓冲区中进行。当一帧画面准备好后调用一个update()函数将整个缓冲区的数据通过 SPI DMA 一次性发送到屏幕。这种方法将复杂的图形计算和低速的 SPI 传输解耦能极大提高显示流畅度。Python 中可以使用numpy数组来模拟帧缓冲区并结合PILPillow库进行绘图。C 语言则有更高效的实现可以直接操作内存并与 DMA 结合。6.2 背光 PWM 控制与功耗管理屏幕背光通常是耗电大户。通过 PWM 控制背光引脚不仅可以调节亮度以适应不同环境光线还能在不需要显示时降低亮度以节省电量。def set_backlight(self, brightness): 设置背光亮度brightness 范围 0.0 到 1.0 # 假设 bl_pin 支持硬件 PWM (如 GPIO18) if not hasattr(self, pwm): GPIO.setup(self.bl_pin, GPIO.OUT) self.pwm GPIO.PWM(self.bl_pin, 1000) # 1kHz 频率 self.pwm.start(0) self.pwm.ChangeDutyCycle(brightness * 100)在电池供电的项目中你还可以在系统空闲时完全关闭背光brightness0甚至通过发送命令让 LCD 驱动芯片进入睡眠模式0x10进一步降低功耗。6.3 驱动芯片的“睡眠”与“唤醒”除了关闭背光让驱动芯片本身进入低功耗模式也很重要。这通过发送特定的命令来实现进入睡眠模式发送命令0x10。在此模式下芯片内部振荡器停止功耗极低但显示内容会丢失唤醒后需要重新初始化或刷新屏幕。退出睡眠模式发送命令0x11然后需要等待几毫秒通常 5ms 或 120ms见数据手册让芯片稳定有时还需要重新发送部分初始化命令如颜色格式设置。在需要长时间待机但又要快速恢复显示的场景下可以只关闭背光让芯片保持正常工作。如果对功耗要求极其苛刻则使用睡眠模式。7. 常见问题排查与实战心得驱动 SPI LCD 屏幕的过程很少一帆风顺。下面是我在多个项目中总结的一些常见问题及解决方法。7.1 屏幕白屏、花屏或不亮这是最常见的问题排查思路如下检查电源和背光首先用万用表测量 HAT 上的 3.3V 是否正常。然后检查背光控制引脚BL是否有电压输出。可以尝试直接将背光引脚接到 3.3V看屏幕是否亮起显示可能是白的。确认 SPI 是否启用再次运行ls /dev/spi*和lsmod | grep spi确保 SPI 内核模块已加载。检查接线与引脚定义虽然 HAT 是插接的但也要确认没有虚焊或接触不良。最重要的是核对代码中的dc_pin和rst_pin的 BCM 编号是否与实际 HAT 设计一致。一个错误的 DC 引脚定义会导致命令和数据全部错位。验证初始化序列这是最可能出问题的地方。确保你使用的初始化序列与你的屏幕型号ST7789/ST7735S、分辨率、颜色格式完全匹配。尝试从供应商那里获取最准确的序列。可以逐条注释掉初始化命令看屏幕在哪个命令之后有反应比如背光受控了来定位问题命令。降低 SPI 速度过高的 SPI 速度可能导致通信不稳定。在初始化时将max_speed_hz设为 10000001MHz或更低看是否能正常初始化。成功后再逐步提高速度。逻辑分析仪抓取波形如果条件允许使用逻辑分析仪连接 SCLK, MOSI, DC, CS 引脚抓取初始化阶段的 SPI 波形。对照数据手册检查命令和数据是否正确发出时序是否符合要求特别是 CS 和 DC 信号与数据时钟的对应关系。7.2 显示方向错误或偏移屏幕显示的内容方向不对或者没有居中有黑边。显示方向通过修改MADCTL命令通常是0x36的参数来控制。不同的值代表不同的旋转和镜像。常见的参数有0x00竖屏、0x60旋转90度、0xC0旋转180度、0xA0旋转270度。需要根据屏幕的物理安装方向来调整。显示偏移/黑边这需要通过CASET和RASET命令来调整显示窗口的起始位置。有些屏幕驱动芯片的显存GRAM比物理像素区域大或者屏幕本身有非显示区域。你需要调整xstart,xend,ystart,yend这些参数。例如对于一块 240x240 的屏幕如果显示偏左可以尝试将CASET的起始值设为0x00结束值设为0xEF239或者微调起始值如0x01。这个过程需要反复试验。7.3 颜色错乱红蓝互换等颜色错乱通常是像素数据格式设置不正确导致的。检查颜色格式命令确保0x3A(COLMOD) 命令发送的参数与你的代码中像素数据的格式一致。RGB565 对应0x55RGB666 对应0x66等。检查字节顺序RGB565 颜色是一个16位数在发送时是高字节在前还是低字节在前这需要与驱动芯片的期望匹配。ST7789/ST7735S 通常期望高位在前MSB first这也是 SPI 的默认模式。但如果你在构建像素数据时顺序错了就会导致红蓝通道互换。尝试在发送颜色数据时交换两个字节的顺序。检查 MADCTL 的 RGB/BGR 位MADCTL寄存器中有一个位控制 RGB 顺序。如果这个位设置错了整个屏幕的颜色顺序就会反转。尝试在发送0x36命令时将参数从0x00改为0x08或反之看看颜色是否恢复正常。7.4 刷新率过低与 CPU 占用高当绘制复杂图形或动画时感觉卡顿。优化 SPI 速度在屏幕驱动芯片允许的范围内尽可能提高max_speed_hz。可以从 20MHz 开始尝试。使用 DMA如前所述使用 DMA 传输可以解放 CPU。但这通常需要在 C 语言层面使用像bcm2835或直接操作/dev/spidev并配置ioctl调用才能实现。Python 的spidev库底层可能已经做了一些优化但对于极致性能C 是更好的选择。采用帧缓冲区避免频繁的小数据量 SPI 传输。将所有绘图操作在内存中完成然后一次性更新整个屏幕或一个大的区域。减少传输数据量如果画面只有小部分更新使用set_window只更新脏区域而不是每次都刷新全屏。驱动一块 SPI LCD 屏幕从硬件连接到软件调试是一个典型的嵌入式系统软硬件协同工作过程。Zero LCD HAT (A) 通过其精巧的 HAT 设计极大简化了硬件连接让我们能把精力集中在软件和驱动逻辑上。理解 SPI 协议、熟悉驱动芯片的数据手册、掌握正确的初始化序列是成功点亮屏幕的关键。而在那之后通过帧缓冲、DMA 等技巧进行优化则能让你的显示项目从“能用”变得“好用”甚至“惊艳”。