树莓派SPI驱动TFT屏幕实战:从硬件连接到Python图形显示
1. 项目缘起为什么选择SPI驱动TFT屏幕最近在折腾一个需要本地显示状态的小项目手头正好有一块闲置的1.5寸的TFT彩屏。这种小尺寸屏幕在物联网设备、便携式仪表或者智能家居的中控面板上很常见。直接使用HDMI输出对于树莓派来说当然最简单但一来功耗高二来对于只需要显示几行文本或简单图形的应用来说属于“杀鸡用牛刀”。而GPIO直接驱动又太复杂需要自己处理时序和显存。这时候SPI接口就成了一个绝佳的选择。SPI全称Serial Peripheral Interface是一种高速、全双工、同步的通信总线。用它来驱动TFT屏幕核心优势在于“省引脚”。一块典型的SPI TFT屏幕除了电源和背光通常只需要4根线MOSI, SCLK, DC, CS就能完成数据和命令的传输如果再加上一根RST复位线也才5根。这极大地节省了树莓派宝贵的GPIO资源让其他引脚可以用于连接传感器、执行器或通信模块。对于像我手头这块基于GC9305或类似驱动芯片的屏幕SPI几乎是标准配置。这个项目的目标很明确让树莓派通过其自带的SPI硬件接口点亮并控制这块TFT屏幕最终能显示我们想要的任何内容无论是静态图片、动态图形还是文本信息。整个过程会涉及到硬件连接、系统配置、驱动安装和软件编程几个核心环节。下面我就把从零开始踩坑、调试到最终成功显示的完整过程梳理一遍其中几个关键配置点如果搞错屏幕可能永远是一片漆黑。2. 硬件连接理清线序与电源是成功的第一步拿到一块TFT屏幕第一步不是急着写代码而是正确连接。这一步错了后面所有软件调试都是徒劳。我用的是一块常见的1.5寸SPI TFT驱动芯片可能是ST7735、ILI9341或GC9305中的一种它们的SPI接口定义大同小异。2.1 引脚定义与对应关系我们需要先明确屏幕和树莓派两边的引脚定义。屏幕一般会有一排引脚通常标记为VCC、GND、SCL或SCLK、SDA或MOSI、RES或RST、DC或RS、CS、BL背光。VCC/GND电源正极和地。绝大多数3.3V屏幕可以直接接树莓派的3.3V引脚但务必确认你的屏幕电压有些老屏幕或5V屏幕需要接5V接错可能烧毁屏幕或树莓派。SCLK (Serial Clock)SPI时钟线由主设备树莓派产生用于同步数据。MOSI (Master Out Slave In)主设备输出从设备输入。树莓派通过这根线向屏幕发送命令和数据。DC (Data/Command) 也称为RS或A0。这是SPI驱动TFT屏幕非常关键的一根线。它告诉屏幕当前在MOSI线上传输的是“命令”还是“数据”。例如设置屏幕扫描方向是一个命令而填充颜色的像素值则是数据。通常高电平代表数据低电平代表命令。CS (Chip Select)片选线低电平有效。当树莓派有多个SPI设备时用这根线来选择当前要和哪个设备通信。如果只有一块屏幕可以将其永久接地低电平但为了规范建议还是连接到GPIO并控制。RST (Reset)复位线低电平有效。用于硬件复位屏幕驱动芯片。可以接GPIO控制也可以直接接3.3V即不进行硬件复位但软件初始化时需要进行软复位。树莓派这边我们需要找到硬件SPI0的引脚在40Pin GPIO排针上MOSI: GPIO 10 (物理引脚19)MISO: GPIO 9 (物理引脚21) -驱动屏幕通常不需要但建议保留SCLK: GPIO 11 (物理引脚23)CE0 (Chip Enable 0): GPIO 8 (物理引脚24) - 可作为屏幕的CSCE1 (Chip Enable 1): GPIO 7 (物理引脚26) - 如果有第二个SPI设备可用此外我们还需要两个普通的GPIO口来连接DC和RST线例如DC: GPIO 25 (物理引脚22)RST: GPIO 24 (物理引脚18)背光BL可以接一个GPIO进行开关控制或者直接接3.3V让其常亮。2.2 我的连接方案与避坑点根据以上分析我的实际连接如下表所示树莓派 GPIO (BCM编号)树莓派物理引脚TFT屏幕引脚说明3.3V引脚1VCC关键确认屏幕电压GND引脚6GND共地GPIO 11 (SCLK)引脚23SCL/SCLKSPI时钟GPIO 10 (MOSI)引脚19SDA/MOSISPI数据输出GPIO 8 (CE0)引脚24CSSPI片选0GPIO 25引脚22DC/RS数据/命令选择GPIO 24引脚18RST复位低电平有效3.3V引脚1BL背光常亮注意1电源确认这是我踩的第一个坑。我最初想当然地接了3.3V但屏幕毫无反应。后来用万用表量了一下屏幕板子的稳压芯片输出发现其逻辑电压是3.3V但VCC输入脚实际需要5V。接上5V后屏幕背光亮起。所以务必查阅屏幕资料或测量确认电压。注意2电平匹配树莓派GPIO是3.3V电平。如果你的屏幕是5V逻辑虽然少见则需要电平转换模块否则可能通信不稳定甚至损坏树莓派GPIO。注意3接线牢固使用杜邦线连接时确保接触良好。接触不良是导致“时好时坏”问题的常见元凶。对于长期项目建议使用排针焊接或专用扩展板。3. 系统配置启用SPI与安装必要软件库硬件连接无误后我们需要在树莓派系统层面启用SPI接口并安装图形库来简化编程。3.1 启用树莓派的SPI接口树莓派的SPI接口默认是关闭的。启用方法如下打开终端运行配置工具sudo raspi-config使用方向键选择Interface Options-SPI。当询问 “Would you like the SPI interface to be enabled?” 时选择Yes。确认后工具会提示需要重启选择Yes立即重启。重启后可以通过以下命令验证SPI是否已启用lsmod | grep spi如果看到spi_bcm2835等相关模块说明SPI驱动已加载。也可以检查/dev/下是否有SPI设备节点ls /dev/spi*正常情况下应该能看到/dev/spidev0.0和/dev/spidev0.1分别对应CE0和CE1。3.2 安装Python图形库Pillow与Adafruit_CircuitPython_RGB_Display为了便于在屏幕上绘图、显示文字和图片我们使用Python编程。需要安装两个核心库Pillow (PIL Fork)强大的图像处理库用于创建、加载和处理图像。sudo apt update sudo apt install python3-pil -yAdafruit_CircuitPython_RGB_DisplayAdafruit出品的、针对各种RGB显示屏包括SPI TFT的Python驱动库。它封装了底层SPI通信和屏幕初始化命令让我们可以专注于应用逻辑。sudo apt install python3-pip -y sudo pip3 install adafruit-circuitpython-rgb-display这个库会自动安装一系列依赖如adafruit-blinka用于GPIO和SPI控制、RPi.GPIO的替代品等。实操心得在安装adafruit-circuitpython-rgb-display时可能会遇到编译依赖问题。如果报错关于python3-dev或libjpeg等需要先安装这些系统依赖sudo apt install python3-dev libjpeg-dev zlib1g-dev libopenjp2-7 libtiff5 -y然后再重新执行pip3 install命令。4. 驱动适配与初始化让屏幕“活”过来这是最核心也最容易出错的一步。adafruit-circuitpython-rgb-display库支持很多型号的屏幕我们需要找到与自己屏幕匹配的驱动类并正确初始化。4.1 确定屏幕驱动芯片型号通常屏幕卖家会提供资料或者屏幕上最大的那个黑色芯片上会印有型号。如果都没有可以尝试常见的驱动型号如ST7735、ST7789、ILI9341、ILI9486、HX8357等。我的屏幕资料显示是GC9305但经过测试它与ST7735的初始化序列兼容。这是一个常见情况很多国产屏会兼容主流驱动芯片的命令集。4.2 编写初始化代码下面是一个针对ST7735驱动、分辨率为128x160的1.5寸屏幕的完整Python示例。请根据你的屏幕分辨率常见还有128x128, 240x240, 320x240等和引脚连接修改参数。# 导入必要的库 import digitalio import board from PIL import Image, ImageDraw, ImageFont from adafruit_rgb_display import st7735 # 如果驱动是ST7789则导入st7789 # 配置SPI spi board.SPI() # 使用默认SPI0 # 定义控制引脚 (根据你的实际连接修改) cs_pin digitalio.DigitalInOut(board.CE0) # 片选接GPIO8 (CE0) dc_pin digitalio.DigitalInOut(board.D25) # 数据/命令接GPIO25 reset_pin digitalio.DigitalInOut(board.D24) # 复位接GPIO24 # 创建显示屏对象 # 关键参数解析 # spi: SPI总线对象 # cs: 片选引脚对象 # dc: 数据/命令引脚对象 # rst: 复位引脚对象 # width160, height128: 屏幕分辨率宽x高。注意有些屏幕是竖屏模式可能需要交换width和height。 # rotation90: 屏幕旋转角度0, 90, 180, 270。如果你的屏幕显示方向不对调整这个参数。 # baudrate24000000: SPI通信速率。可以尝试提高如40000000或降低如8000000以适配屏幕和线长。 # x_offset1, y_offset2: 有些屏幕的显示区域在内存中有偏移如果显示内容偏了可以微调这两个值。 disp st7735.ST7735R(spi, cscs_pin, dcdc_pin, rstreset_pin, width160, height128, rotation90, baudrate24000000, x_offset1, y_offset2) # 初始化屏幕 disp.reset() # 硬件复位如果接了RST线 disp.begin() # 发送初始化序列 # 创建一个空白图像模式为RGB大小与屏幕一致 # 注意这里创建的是内存中的图像对象用于绘制。 image Image.new(RGB, (disp.width, disp.height)) # 获取一个绘图对象以便在图像上画图 draw ImageDraw.Draw(image) # 用黑色清屏 draw.rectangle((0, 0, disp.width, disp.height), outline0, fill(0, 0, 0)) disp.image(image) # 将图像数据通过SPI发送到屏幕显示 print(屏幕初始化完成)4.3 常见初始化问题排查如果运行上述代码后屏幕仍是空白请按以下步骤排查检查电源和背光屏幕的背光亮了吗如果没亮检查BL引脚是否接电。检查SPI是否启用再次运行ls /dev/spi*确认设备存在。检查引脚编号确认代码中的board.D25等与你实际的物理连接BCM编号一致。board.D25对应的是BCM GPIO 25。调整SPI波特率过高的波特率可能导致长导线通信失败。将baudrate从24000000降低到12000000或8000000试试。尝试不同的驱动类如果你的屏幕不是ST7735将st7735.ST7735R替换为st7789.ST7789或ili9341.ILI9341等。调整旋转和偏移rotation参数严重影响显示方向。x_offset和y_offset用于微调显示起始位置。查看控制台输出代码是否有报错权限问题需要sudo库未安装逻辑分析仪或示波器这是终极手段。用逻辑分析仪抓取SCLK、MOSI、DC、CS的波形看是否有数据发出。可以对照屏幕驱动芯片的数据手册看初始化命令序列是否正确。踩坑实录我的屏幕在初始化后显示区域只有左上角一小块大部分区域是花屏。经过排查是width和height参数设置反了并且需要设置rotation270才能全屏正确显示。同时我发现baudrate设为40000000时在短线连接下稳定但换成长线后就出现雪花点降至20000000后问题解决。SPI速率和线长、屏幕质量强相关需要实际调试确定。5. 图形与文字显示实战从清屏到动态刷新屏幕成功初始化后我们就可以在上面“作画”了。所有操作都围绕在内存中创建一个Image对象对其进行绘制最后调用disp.image()将其一次性发送到屏幕。5.1 基本图形绘制PIL.ImageDraw模块提供了丰富的绘图功能。# 假设disp和image对象已按上一节创建好 draw ImageDraw.Draw(image) # 1. 清屏为白色 draw.rectangle((0, 0, disp.width, disp.height), fill(255, 255, 255)) disp.image(image) # 2. 画一个红色的矩形框 draw.rectangle((10, 10, 50, 50), outline(255, 0, 0), width2) # 3. 画一条蓝色的对角线 draw.line((0, 0, disp.width, disp.height), fill(0, 0, 255), width3) # 4. 画一个绿色的实心圆 circle_center (80, 64) # 屏幕中心附近 circle_radius 20 draw.ellipse((circle_center[0]-circle_radius, circle_center[1]-circle_radius, circle_center[0]circle_radius, circle_center[1]circle_radius), fill(0, 255, 0)) # 将绘制好的图像显示到屏幕 disp.image(image)5.2 显示文字显示文字需要加载字体文件。树莓派系统自带一些字体也可以使用自己的TTF字体文件。# 尝试加载系统字体如果失败则指定一个路径 try: font ImageFont.load_default() # 默认小字体 # 或者使用指定大小的字体 # font ImageFont.truetype(/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf, 16) except IOError: # 如果默认字体加载失败使用一个备用的内置字体可能像素化 font ImageFont.load_default() # 定义要显示的文本和位置 text Hello, TFT! text_width, text_height draw.textsize(text, fontfont) # 获取文本占据的像素大小 text_x (disp.width - text_width) // 2 # 水平居中 text_y (disp.height - text_height) // 2 # 垂直居中 # 设置文字颜色这里用黑色 text_color (0, 0, 0) # 在绘制文字前你可能想先画一个背景色矩形例如白色来覆盖原有内容避免文字重叠 draw.rectangle((text_x-2, text_y-2, text_xtext_width2, text_ytext_height2), fill(255,255,255)) # 绘制文字 draw.text((text_x, text_y), text, fontfont, filltext_color) # 显示到屏幕 disp.image(image)注意事项ImageFont.load_default()加载的字体非常小可能只适合显示少量字符。对于中文或大字体必须使用ImageFont.truetype()并指定一个完整的TTF字体文件路径。确保该字体文件存在于树莓派上。5.3 显示图片我们可以将JPG、PNG等格式的图片缩放至屏幕分辨率后显示。# 打开一张图片 original_image Image.open(your_image.jpg) # 替换为你的图片路径 # 将图片缩放至屏幕大小。 # Image.Resampling.LANCZOS 是一种高质量的重采样滤波器。 scaled_image original_image.resize((disp.width, disp.height), Image.Resampling.LANCZOS) # 如果图片模式不是RGB需要转换例如RGBA的PNG if scaled_image.mode ! RGB: scaled_image scaled_image.convert(RGB) # 显示图片 disp.image(scaled_image)5.4 实现动态刷新与动画动态效果的核心是循环在每次循环中根据新的数据或状态在内存中绘制新的图像帧然后刷新到屏幕。import time # 一个简单的弹跳球动画示例 ball_pos [disp.width // 2, disp.height // 2] ball_radius 5 ball_velocity [2, 3] # [x方向速度, y方向速度] try: while True: # 1. 清空上一帧用黑色填充整个图像 draw.rectangle((0, 0, disp.width, disp.height), fill(0, 0, 0)) # 2. 更新小球位置 ball_pos[0] ball_velocity[0] ball_pos[1] ball_velocity[1] # 3. 边界碰撞检测 if ball_pos[0] - ball_radius 0 or ball_pos[0] ball_radius disp.width: ball_velocity[0] -ball_velocity[0] if ball_pos[1] - ball_radius 0 or ball_pos[1] ball_radius disp.height: ball_velocity[1] -ball_velocity[1] # 4. 绘制当前帧的小球白色 draw.ellipse((ball_pos[0]-ball_radius, ball_pos[1]-ball_radius, ball_pos[0]ball_radius, ball_pos[1]ball_radius), fill(255, 255, 255)) # 5. 将图像发送到屏幕 disp.image(image) # 6. 控制帧率短暂延时 time.sleep(0.02) # 大约50 FPS except KeyboardInterrupt: print(动画停止)这个例子展示了动态刷新的基本模式。对于更复杂的UI你可以定义不同的界面状态如主页面、设置页在循环中根据状态绘制不同的内容。6. 性能优化与高级话题当显示复杂图形或需要较高刷新率时性能就成为需要考虑的问题。6.1 局部刷新与双缓冲上面的例子每次都是全屏刷新draw.rectangle清全屏这对于简单动画可以接受但效率不高。更优的方法是只重绘发生变化的部分区域。真正的“双缓冲”在adafruit-circuitpython-rgb-display中不是必须的因为disp.image()调用内部已经处理了数据发送。但我们可以通过优化绘图逻辑来模拟脏矩形更新记录哪些区域需要更新只清除和重绘这些区域。离屏渲染始终在一个内存中的Image对象上绘制完整帧然后一次性disp.image()发送。这避免了屏幕撕裂虽然SPI屏通常不易察觉也是我们一直在用的方法。关键在于减少disp.image()调用之间的冗余绘图计算。6.2 SPI速率与帧率瓶颈SPI总线速率是帧率的主要瓶颈。计算公式简化如下传输一帧数据所需时间 ≈ (屏幕宽度 * 屏幕高度 * 每个像素字节数 * 8) / SPI波特率对于160x128的16位色2字节屏幕数据量 160 * 128 * 2 40960 字节比特数 40960 * 8 327680 比特在24Mbps (24,000,000 bps) 的SPI速率下理论最短时间 327680 / 24,000,000 ≈ 0.0137 秒理论最高帧率 ≈ 73 FPS但这只是纯数据传输的理想时间未包含命令开销、Python代码执行时间、SPI协议开销等。实际帧率会低很多可能在20-40 FPS左右。提高baudrate参数可以提升帧率但受硬件和接线质量限制。6.3 使用硬件加速树莓派的GPUVideoCore通常用于HDMI输出渲染很难直接用于驱动SPI TFT。因此驱动SPI屏的图形性能主要取决于CPU处理PIL绘图的速度复杂的ImageDraw操作如抗锯齿图形、大量文字会消耗大量时间。SPI数据传输速度如上所述是硬性瓶颈。Python解释器开销对于极高帧率需求可以考虑使用C语言库如wiringPifbdev或MicroPython但开发复杂度会显著增加。对于大多数状态显示、仪表盘、简单动画等应用Python PIL Adafruit库的组合在性能上已经完全足够且开发效率最高。7. 项目集成与实用案例将SPI TFT屏幕集成到实际项目中通常不仅仅是显示动画而是作为信息输出的窗口。7.1 系统状态监视器一个经典应用是制作一个树莓派系统状态监视屏实时显示CPU温度、负载、内存使用率、IP地址等信息。import psutil import socket def get_ip_address(): # 获取本机IP地址非回环地址 try: s socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect((8.8.8.8, 80)) ip s.getsockname()[0] s.close() return ip except Exception: return N/A try: while True: # 获取系统信息 cpu_percent psutil.cpu_percent(intervalNone) cpu_temp psutil.sensors_temperatures().get(cpu_thermal, [None])[0] temp cpu_temp.current if cpu_temp else N/A memory psutil.virtual_memory() ip_addr get_ip_address() # 创建新图像并绘制 draw.rectangle((0, 0, disp.width, disp.height), fill(0, 0, 50)) # 深蓝背景 y_offset 10 line_height 15 font_small ImageFont.load_default() info_lines [ fIP: {ip_addr}, fCPU: {cpu_percent:5.1f}%, fTemp: {temp:5.1f}C, fMem: {memory.percent:5.1f}%, fUsed: {memory.used//1024//1024:3d}M, fFree: {memory.free//1024//1024:3d}M, ] for line in info_lines: draw.text((5, y_offset), line, fontfont_small, fill(255, 255, 255)) y_offset line_height # 画一个简单的CPU使用率条形图 bar_width 100 bar_height 10 bar_x 5 bar_y disp.height - bar_height - 5 # 背景条 draw.rectangle((bar_x, bar_y, bar_xbar_width, bar_ybar_height), outline(200,200,200), fill(50,50,50)) # 前景条根据CPU使用率 fill_width int((cpu_percent / 100.0) * bar_width) draw.rectangle((bar_x, bar_y, bar_xfill_width, bar_ybar_height), fill(0, 255, 0)) disp.image(image) time.sleep(2) # 每2秒更新一次 except KeyboardInterrupt: pass这个脚本需要安装psutil库sudo pip3 install psutil。它创建了一个简洁的状态面板并带有动态更新的CPU使用率条。7.2 结合传感器数据显示你可以很容易地将屏幕与树莓派上的其他传感器如DHT11温湿度传感器、BMP280气压计结合创建一个环境监测站。# 假设已安装Adafruit_DHT库 import Adafruit_DHT DHT_SENSOR Adafruit_DHT.DHT11 DHT_PIN 4 # GPIO4 物理引脚7 try: while True: humidity, temperature Adafruit_DHT.read(DHT_SENSOR, DHT_PIN) draw.rectangle((0, 0, disp.width, disp.height), fill(0, 0, 0)) if humidity is not None and temperature is not None: draw.text((10, 20), fTemp: {temperature:.1f}C, fontfont, fill(255,255,0)) draw.text((10, 50), fHum: {humidity:.1f}%, fontfont, fill(0,255,255)) else: draw.text((10, 35), Sensor Error!, fontfont, fill(255,0,0)) disp.image(image) time.sleep(3) except KeyboardInterrupt: pass通过这种方式SPI TFT屏幕成为了一个低成本、低功耗的本地人机交互界面非常适合各种嵌入式项目和物联网终端。