树莓派驱动TFT屏:CircuitPython+Pillow用户空间SPI方案详解
2026/5/15 14:17:28 网站建设 项目流程

1. 项目概述与核心思路

如果你手头有一块闲置的TFT显示屏,想把它接到树莓派上做个信息面板、迷你相框或者简单的游戏机界面,但又不想去折腾复杂的内核驱动和编译,那今天聊的这个方案可能正合你意。我最近在几个物联网仪表盘项目里,反复用到了CircuitPython配合Pillow库来驱动各种SPI接口的TFT屏,实测下来,这套组合拳在灵活性和开发效率上,比传统的方案要舒服不少。

简单来说,这个方案的核心是“用户空间驱动”。它不依赖fbtft这类内核帧缓冲驱动,意味着你的系统桌面控制台不会抢占这块屏幕。相反,你通过纯Python代码,直接通过SPI总线向显示屏发送绘图指令和数据。这样做的好处显而易见:环境依赖少,跨平台兼容性好,调试和修改代码就像运行普通Python脚本一样简单。无论是Adafruit的ILI9341屏,还是市面上更常见的ST7789、ST7735,甚至是OLED屏如SSD1331,都能用同一套逻辑搞定。

整个流程可以拆解为几个关键步骤:首先是硬件连接,把屏幕的SPI引脚(CLK, MOSI)和几个控制引脚(CS, DC, RST)正确接到树莓派的GPIO上;然后是软件环境搭建,主要是安装Adafruit-Blinka(它让CircuitPython库能在普通Linux/Python3上运行)和Pillow图像库;最后就是写Python脚本,初始化屏幕、创建画布、绘制内容并刷新显示。下面,我就结合自己踩过的坑和总结的技巧,把这套流程掰开揉碎了讲清楚。

2. 硬件连接详解与避坑指南

硬件连接是第一步,也是容易出错的一步。不同的屏幕虽然控制器可能相同,但引脚排列和功能可能略有差异。这里我以最常见的2.4寸ILI93411.54寸ST7789两款SPI屏为例,详细说明接线逻辑和注意事项。

2.1 树莓派SPI引脚定义

首先,必须清楚树莓派上硬件SPI0的引脚定义(以40针GPIO接口的树莓派3B+/4B为例):

  • SCLK (SPI时钟):GPIO 11(物理引脚23)
  • MOSI (主设备输出,从设备输入):GPIO 10(物理引脚19)
  • MISO (主设备输入,从设备输出):GPIO 9(物理引脚21) -注意:对于纯输出的显示屏,此引脚通常不需要连接。
  • CE0 (片选0):GPIO 8(物理引脚24)
  • CE1 (片选1):GPIO 7(物理引脚26)

此外,我们还需要两个通用的GPIO引脚来充当屏幕的数据/命令选择线(D/C或DC)复位线(RST)

2.2 ILI9341屏幕接线方案

对于大多数2.2寸、2.4寸、2.8寸、3.2寸的ILI9341屏幕,其引脚通常包括VCCGNDCSRSTDCMOSISCLK,有时还有MISO和背光控制BL

推荐接线方式如下:

屏幕引脚树莓派GPIO物理引脚说明
VCC3.3V引脚1或17绝对不要接5V!会烧屏。
GNDGND引脚6, 9, 14, 20, 25, 30, 34, 39等任选一个接地引脚。
CS (片选)CE0 (GPIO 8)引脚24使用SPI0的默认片选0。
RST (复位)GPIO 24引脚18可自定义,代码中需对应修改。
DC (数据/命令)GPIO 25引脚22可自定义,代码中需对应修改。
MOSI (数据输入)MOSI (GPIO 10)引脚19主输出,从输入。
SCLK (时钟)SCLK (GPIO 11)引脚23时钟信号。
BL (背光)3.3VGPIO引脚1或17接3.3V常亮,接GPIO则可编程控制。

重要提示:有些大尺寸屏幕(如某些3.5寸屏)默认是8位并行接口。你需要检查屏幕背面,通常会有焊盘或跳线帽(标记为IM0,IM1,IM2,IM3)。必须将其配置为SPI模式。具体方法请查阅屏幕手册,通常需要将IM0接高电平(3.3V),IM1IM2IM3接低电平(GND)。

2.3 ST7789屏幕接线方案

ST7789常用于1.3寸、1.54寸、2.0寸的IPS屏,接线与ILI9341类似,但需要注意一些屏幕(特别是高分辨率或带偏移的)在初始化时需要特定的widthheightx_offsety_offset参数。

接线参考表:

屏幕引脚树莓派GPIO物理引脚说明
VCC3.3V引脚1
GNDGND引脚6
CSCE0 (GPIO 8)引脚24
RSTGPIO 24引脚18
DCGPIO 25引脚22
MOSIMOSI (GPIO 10)引脚19
SCLKSCLK (GPIO 11)引脚23
BL3.3VGPIO 18引脚1或12建议接GPIO以便软件控制亮度。

2.4 接线实操心得与避坑点

  1. 电源是头等大事:绝大多数3.3V逻辑的TFT屏,VCC必须接树莓派的3.3V引脚。接5V瞬间冒烟不是开玩笑。如果你需要驱动大尺寸屏或背光电流很大,可以考虑使用外部3.3V稳压模块单独供电,但信号线仍需连接树莓派。
  2. GPIO编号与物理引脚:代码里用的是BCM编号(如board.D25),对应的是GPIO 25,而不是物理引脚号。接线时务必对照GPIO引脚图,我习惯用pinout命令在终端查看。
  3. SPI使能:默认情况下,树莓派的SPI接口可能是关闭的。需要通过sudo raspi-config->Interface Options->SPI->Yes来启用。启用后,可以在/dev/下看到spidev0.0spidev0.1设备。
  4. 避免引脚冲突:如果你之前为这块屏幕安装过fbtft内核驱动,务必先卸载或禁用,否则SPI总线会被内核驱动占用,导致我们的Python程序无法访问。通常可以编辑/boot/config.txt,注释掉相关的dtoverlay行并重启。
  5. 背光控制:如果屏幕背光单独引出了BLLED引脚,强烈建议将其连接到一个可用的GPIO(如GPIO 26),而不是直接接3.3V。这样你可以在代码中控制背光开关,实现息屏节能,体验好很多。

3. 软件环境搭建与库安装详解

硬件连好后,我们就进入软件环节。这套方案的核心是三个Python库:Adafruit-BlinkaAdafruit_CircuitPython_RGB_DisplayPillow

3.1 系统准备与SPI验证

首先,确保你的树莓派系统是最新的,并且启用了SPI接口(如上文所述)。可以通过以下命令检查SPI是否已启用:

lsmod | grep spi

如果看到spi_bcm2835等相关模块,说明SPI已加载。也可以检查设备文件:

ls -l /dev/spi*

应该能看到/dev/spidev0.0/dev/spidev0.1

3.2 安装Adafruit Blinka

Adafruit-Blinka是一个兼容层,它让为CircuitPython设计的硬件控制库(比如我们的RGB Display库)能够在运行CPython的Linux计算机(如树莓派)上工作。它本质上是boardbusio等模块在Linux上的实现。

安装非常简单,使用pip3即可:

sudo pip3 install adafruit-blinka

如果你的系统没有pip3,需要先安装:

sudo apt update sudo apt install python3-pip

3.3 安装RGB Display驱动库和Pillow

接下来安装显示屏驱动库和图像处理库:

sudo pip3 install adafruit-circuitpython-rgb-display

这个库包含了ILI9341、ST7789、ST7735、HX8357、SSD1351、SSD1331等常见控制器的驱动。

然后安装Pillow(PIL的友好分支),它是我们处理图像、绘制图形和文字的核心:

sudo apt-get install python3-pil

有时通过pip安装的Pillow可能缺少一些底层图像库支持,如果运行时报错关于libopenjp2libtiff,可以一并安装这些系统依赖:

sudo apt-get install libopenjp2-7 libtiff5 libatlas-base-dev

3.4 安装字体(可选但推荐)

示例代码中使用了DejaVu字体,树莓派系统通常已预装。如果未安装,可以手动安装:

sudo apt-get install fonts-dejavu

3.5 环境验证与常见问题

安装完成后,可以写一个最简单的测试脚本test_spi.py来验证基础SPI通信是否正常(不涉及具体屏幕):

import board import busio spi = busio.SPI(board.SCK, board.MOSI, board.MISO) print(“SPI对象创建成功:”, spi)

运行python3 test_spi.py,如果没有报错,说明Adafruit-Blinka和SPI基础访问是正常的。

踩坑记录:最常遇到的问题是权限不足。普通用户可能无法访问/dev/spidev0.0。解决方法有两种:一是使用sudo运行你的Python脚本(不推荐长期使用);二是将你的用户加入spi组:sudo usermod -a -G spi $USER,然后注销并重新登录使组生效。推荐第二种方法。

4. 核心代码解析与图像显示实战

环境就绪,我们来深入看看代码。第一个例子是显示一张图片,并实现自适应缩放和居中裁剪,这是很多项目(如电子相册)的必备功能。

4.1 代码结构与初始化

我们以ILI9341为例,完整代码如下,我将逐段解析:

# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries # SPDX-License-Identifier: MIT import digitalio import board from PIL import Image, ImageDraw from adafruit_rgb_display import ili9341 # 其他控制器导入已省略 # 1. 引脚配置 cs_pin = digitalio.DigitalInOut(board.CE0) # 片选,接GPIO8 dc_pin = digitalio.DigitalInOut(board.D25) # 数据/命令,接GPIO25 reset_pin = digitalio.DigitalInOut(board.D24) # 复位,接GPIO24 # 2. SPI总线与显示屏参数配置 BAUDRATE = 24000000 # SPI通信速率,24MHz是许多屏的极限,不稳定可降至16MHz spi = board.SPI() # 使用硬件SPI0 # 3. 创建显示屏驱动对象 disp = ili9341.ILI9341( spi, rotation=90, # 屏幕旋转90度 cs=cs_pin, dc=dc_pin, rst=reset_pin, baudrate=BAUDRATE, )

关键点解析:

  • 引脚对象digitalio.DigitalInOut用于将GPIO引脚对象化,并交由库管理方向。
  • SPI对象board.SPI()会自动选择系统默认的SPI总线(通常是/dev/spidev0.0)。你也可以使用busio.SPI指定具体的SCK和MOSI引脚,但硬件SPI性能更好。
  • 驱动初始化ili9341.ILI9341()是核心。rotation参数非常实用,可以设置0、90、180、270度,物理上不变动屏幕方向,通过软件旋转显示内容。
  • 更换屏幕:如果你用的是ST7789屏,只需注释掉ILI9341那行,取消注释对应的ST7789初始化行,并可能需要调整widthheightx_offsety_offset等参数。这些参数用于告诉驱动屏幕的实际可绘制区域,对于有“黑边”的屏幕尤其重要。

4.2 创建画布与处理旋转

接下来,我们需要创建一个与屏幕当前方向匹配的内存画布(Image对象)。

# 根据旋转角度,确定画布的宽高。 # 因为驱动对象的.width/.height属性是物理宽高,旋转后逻辑宽高要对调。 if disp.rotation % 180 == 90: height = disp.width # 旋转90或270度,逻辑高度=物理宽度 width = disp.height # 逻辑宽度=物理高度 else: width = disp.width height = disp.height # 创建RGB模式的画布 image = Image.new("RGB", (width, height)) # 创建绘图对象,所有绘制操作都基于这个draw对象 draw = ImageDraw.Draw(image)

这里有个容易混淆的点:disp.widthdisp.height返回的是屏幕物理像素尺寸。当设置rotation=90后,驱动内部会处理像素数据的排列,但disp.widthdisp.height的值不会变。因此,我们需要在应用层(Pillow画布)手动对调宽高,以确保我们绘制的坐标体系与最终显示方向一致。

4.3 图像缩放与居中裁剪算法

这是显示任意图片的核心技巧,目的是让图片在不失真的情况下填满屏幕。

# 打开图片文件 image_original = Image.open("blinka.jpg") # 计算图片和屏幕的宽高比 image_ratio = image_original.width / image_original.height screen_ratio = width / height # 决定缩放策略:以高度为基准,还是以宽度为基准? if screen_ratio < image_ratio: # 屏幕更“胖”或图片更“瘦”。以屏幕高度为基准缩放图片,宽度会超出。 scaled_width = image_original.width * height // image_original.height scaled_height = height else: # 屏幕更“瘦”或图片更“胖”。以屏幕宽度为基准缩放图片,高度会超出。 scaled_width = width scaled_height = image_original.height * width // image_original.width # 执行缩放,使用BICUBIC插值算法保证质量 image_scaled = image_original.resize((scaled_width, scaled_height), Image.BICUBIC) # 计算裁剪区域,使图片居中 x = scaled_width // 2 - width // 2 y = scaled_height // 2 - height // 2 image_cropped = image_scaled.crop((x, y, x + width, y + height)) # 最终显示 disp.image(image_cropped)

算法逻辑解读

  1. 比较宽高比:目的是判断是图片的宽度过剩还是高度过剩。
  2. 等比缩放:让图片的短边刚好等于屏幕的对应边。例如,屏幕是240x320(竖屏,ratio=0.75),图片是800x600(ratio≈1.33)。由于screen_ratio (0.75) < image_ratio (1.33),我们选择以屏幕高度320为基准缩放图片。缩放后图片尺寸为(800*320/600, 320) ≈ (426, 320)。此时图片宽度(426)大于屏幕宽度(240)。
  3. 居中裁剪:从缩放后的图片中间,裁剪出一个和屏幕一样大(240x320)的区域。x = (426-240)//2 = 93y=0。这样就得到了完美居中填充屏幕的图片,且无黑边。

实操心得Image.BICUBIC在质量和速度间取得了很好的平衡。对于小尺寸屏幕,Image.NEAREST(最近邻)最快但可能有锯齿;Image.LANCZOS质量最好但最慢。根据你的刷新需求选择。另外,如果图片路径错误,Image.open会抛出异常,建议用try...except包裹或先检查文件是否存在。

5. 图形绘制与文字渲染进阶

仅仅显示图片还不够,我们经常需要绘制UI元素、图表或显示文字。第二个例子展示了如何使用Pillow的ImageDraw模块进行基本绘图。

5.1 绘制基础图形

import digitalio import board from PIL import Image, ImageDraw, ImageFont # 注意导入了ImageFont from adafruit_rgb_display import ili9341 # ... 引脚、SPI、显示屏初始化与之前相同 ... BORDER = 20 FONTSIZE = 24 # ... 创建画布draw对象 ... # 1. 绘制绿色背景全屏矩形 draw.rectangle((0, 0, width, height), fill=(0, 255, 0)) disp.image(image) # 可以分步刷新,但通常最后统一刷新 # 2. 绘制一个内嵌的紫色矩形 draw.rectangle( (BORDER, BORDER, width - BORDER - 1, height - BORDER - 1), fill=(170, 0, 136) # RGB颜色值 )

draw.rectangle的参数是一个四元组(x0, y0, x1, y1),代表矩形左上角和右下角的坐标。fill参数指定填充色。outline参数可以指定边框颜色。

5.2 加载字体与渲染文字

显示文字是GUI的基础,Pillow支持TrueType字体。

# 加载系统字体,指定大小 font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", FONTSIZE) text = "Hello World!" # 获取文字将要占据的像素尺寸,这对居中计算至关重要 (font_width, font_height) = font.getsize(text) # 计算文字左上角的起始坐标,使其在屏幕上居中 text_x = width // 2 - font_width // 2 text_y = height // 2 - font_height // 2 # 绘制黄色文字 draw.text((text_x, text_y), text, font=font, fill=(255, 255, 0)) # 最终刷新显示 disp.image(image)

字体使用要点

  • 字体路径:示例中使用的是系统字体。你可以使用任何.ttf字体文件,只需提供完整路径。将字体文件放在项目目录下,用“./myfont.ttf”引用更便于移植。
  • getsize()的替代:注意,Pillow 9.2.0及以上版本已弃用font.getsize()。推荐使用font.getbbox()font.getlength()。例如,获取包围盒:bbox = font.getbbox(text),然后text_width = bbox[2] - bbox[0],text_height = bbox[3] - bbox[1]
  • 性能考虑:频繁创建ImageFont对象会影响性能。如果程序中多次使用同一种字体和大小,应该将其创建为全局对象。

5.3 更多绘图功能

ImageDraw模块功能丰富,你还可以:

  • 画线draw.line([(x0, y0), (x1, y1), ...], fill=color, width=width)
  • 画圆/椭圆draw.ellipse([(x0, y0), (x1, y1)], fill=color, outline=color)指定外接矩形。
  • 画多边形draw.polygon([(x0,y0), (x1,y1), ...], fill=color, outline=color)
  • 画点draw.point((x, y), fill=color)

结合这些基础图形,你已经可以绘制出简单的图表、进度条和按钮等UI元素了。

6. 动态信息显示:系统监控仪表盘

第三个例子将前面所学结合起来,创建一个动态更新的系统信息仪表盘。这非常实用,比如可以做一个树莓派资源监视器。

6.1 获取系统信息

我们使用Python的subprocess模块调用shell命令来获取系统数据。

import time import subprocess def get_ip_address(): """获取树莓派的IP地址""" cmd = "hostname -I | cut -d' ' -f1" try: ip = subprocess.check_output(cmd, shell=True).decode("utf-8").strip() return f"IP: {ip}" if ip else "IP: N/A" except: return "IP: Error" def get_cpu_load(): """获取CPU负载(1分钟平均)""" cmd = "top -bn1 | grep load | awk '{printf \"CPU Load: %.2f\", $(NF-2)}'" try: return subprocess.check_output(cmd, shell=True).decode("utf-8").strip() except: return "CPU Load: N/A" def get_memory_usage(): """获取内存使用情况""" cmd = "free -m | awk 'NR==2{printf \"Mem: %s/%sMB %.1f%%\", $3,$2,$3*100/$2 }'" try: return subprocess.check_output(cmd, shell=True).decode("utf-8").strip() except: return "Mem: N/A" def get_disk_usage(): """获取根分区磁盘使用情况""" cmd = "df -h | awk '$NF==\"/\"{printf \"Disk: %s/%s %s\", $3,$2,$5}'" try: return subprocess.check_output(cmd, shell=True).decode("utf-8").strip() except: return "Disk: N/A" def get_cpu_temp(): """获取CPU温度""" cmd = "cat /sys/class/thermal/thermal_zone0/temp | awk '{printf \"CPU Temp: %.1fC\", $1/1000}'" try: return subprocess.check_output(cmd, shell=True).decode("utf-8").strip() except: return "CPU Temp: N/A"

命令解析

  • top -bn1: 以批处理模式运行一次top命令。
  • grep load: 筛选出包含“load”的行。
  • awk ‘{printf “CPU Load: %.2f”, $(NF-2)}’:NF是字段数,$(NF-2)就是倒数第三个字段,即1分钟平均负载。
  • free -m: 以MB为单位显示内存信息。NR==2处理第二行(Mem行)。
  • df -h: 显示磁盘空间使用情况。$NF==”/”筛选出挂载点为根目录/的行。

安全与效率提示:使用shell=True存在潜在安全风险,如果命令字符串来自不可信输入,应避免使用。对于固定命令,风险可控。此外,频繁调用subprocess开销较大。对于高刷新率的需求,可以考虑使用psutil这样的Python库来获取系统信息,效率更高。

6.2 主循环与动态刷新

有了获取数据的函数,主循环就清晰了:

# ... 初始化显示屏、画布、字体等 ... padding = 2 # 文字起始的顶部边距 x = 0 # 文字起始的左边界 while True: # 1. 清屏(绘制黑色背景) draw.rectangle((0, 0, width, height), fill=(0, 0, 0)) # 2. 获取所有系统信息 lines = [] lines.append(get_ip_address()) lines.append(get_cpu_load()) lines.append(get_memory_usage()) lines.append(get_disk_usage()) lines.append(get_cpu_temp()) # 3. 逐行绘制文字 y = padding colors = ["#FFFFFF", "#FFFF00", "#00FF00", "#0000FF", "#FF00FF"] # 为每行定义不同颜色 for i, line in enumerate(lines): draw.text((x, y), line, font=font, fill=colors[i % len(colors)]) # 更新y坐标,为下一行预留位置(文字高度 + 行间距) y += font.getsize(line)[1] + 2 # 4. 刷新到屏幕 disp.image(image) # 5. 控制刷新频率,避免CPU占用过高 time.sleep(1.0) # 每秒刷新一次

关键优化点

  1. 局部刷新:上述代码每次循环都重绘了整个屏幕。对于只有文字变化的场景,这是低效的。更优的方案是只刷新变化的部分(如数字),但这需要更复杂的逻辑来判断和计算脏区域。对于小型屏幕和简单应用,全屏刷新通常可以接受。
  2. 刷新率与功耗time.sleep(1)让循环每秒运行一次,对于系统监控足够了。如果用于动画,可能需要更短的间隔(如0.1秒),但要考虑SPI写入速度和CPU负载。过高的刷新率可能导致屏幕闪烁或系统卡顿。
  3. 错误处理getsize()在Pillow新版本中已废弃,建议改用font.getbbox()。同时,subprocess调用可能因命令不存在而失败,try...except包裹是必要的。

7. 性能优化与高级技巧

当项目变得复杂时,性能就成了问题。这里分享几个我实践中总结的优化技巧。

7.1 减少SPI数据传输量

屏幕刷新的本质是将内存中的图像数据(Image对象)通过SPI总线发送到屏幕的GRAM中。数据量=宽度 x 高度 x 颜色深度(如16位色为2字节)。对于320x240的16位色屏幕,一帧数据约150KB。24MHz的SPI总线理论上每秒可传输约3MB,但算上协议开销,实际帧率可能在15-30FPS。

优化策略

  • 降低颜色深度:如果你的项目不需要全彩,可以尝试使用“P”模式(调色板)或“1”模式(单色)创建图像。但adafruit_rgb_display库通常期望“RGB”模式,你可能需要转换。有些屏幕控制器支持16位RGB565格式,库内部可能已做优化。
  • 局部刷新:只更新屏幕上变化的部分。你需要维护一个“脏矩形”列表,每次只将这些区域的数据发送到屏幕。这需要驱动库支持部分刷新,或者你自己计算并发送特定矩形区域的数据。对于disp.image(),它是全屏更新。你可以研究驱动库的底层方法,看是否有blitblock操作。
  • 双缓冲与直接操作:创建两个Image对象,一个用于绘制(后台缓冲区),完成后一次性调用disp.image()切换到前台。这可以避免绘制过程中的闪烁。更激进的做法是直接操作图像的像素数据数组(image.tobytes()image.load()),但这对编程要求较高。

7.2 使用硬件加速(如果可用)

树莓派的GPU(VideoCore)能力强大。虽然我们这个方案是用户空间的,但可以通过其他途径间接利用:

  • 使用pygameSDL2:它们可能利用硬件加速进行渲染,然后将最终帧缓冲区转换为Image对象,再通过我们的SPI库发送。这增加了复杂性,但对于复杂2D图形或简单游戏可能值得。
  • 使用numpy加速图像处理:Pillow的许多操作底层是C实现的,已经很快。但对于像素级的批量操作(如颜色变换、阿尔法混合),将图像转换为numpy数组进行处理,再转回Image对象,速度可能有数量级的提升。

7.3 背光控制与低功耗

如果你的屏幕背光连接到了GPIO(如GPIO 26),可以在代码中轻松控制:

import digitalio backlight_pin = digitalio.DigitalInOut(board.D26) backlight_pin.direction = digitalio.Direction.OUTPUT def turn_on_backlight(): backlight_pin.value = True def turn_off_backlight(): backlight_pin.value = False # 在需要时调用,例如屏幕休眠时关背光 turn_off_backlight() time.sleep(5) turn_on_backlight()

这对于电池供电的项目至关重要,可以显著延长续航时间。

7.4 多屏幕支持与SPI总线共享

树莓派的硬件SPI0有两个片选(CE0和CE1),理论上可以连接两个SPI从设备。你需要为每个屏幕分配独立的CS、DC、RST引脚,并在代码中初始化两个独立的disp对象。关键是要确保同一时间只有一个设备被选中(CS拉低)。库会通过你传入的cs_pin对象自动管理片选。

# 屏幕1 (使用CE0) cs_pin1 = digitalio.DigitalInOut(board.CE0) dc_pin1 = digitalio.DigitalInOut(board.D22) rst_pin1 = digitalio.DigitalInOut(board.D27) disp1 = ili9341.ILI9341(spi, cs=cs_pin1, dc=dc_pin1, rst=rst_pin1, ...) # 屏幕2 (使用CE1) cs_pin2 = digitalio.DigitalInOut(board.CE1) dc_pin2 = digitalio.DigitalInOut(board.D23) rst_pin2 = digitalio.DigitalInOut(board.D24) disp2 = st7789.ST7789(spi, cs=cs_pin2, dc=dc_pin2, rst=rst_pin2, ...) # 使用时,库会自动控制对应的CS引脚 disp1.image(image1) disp2.image(image2)

注意GPIO资源的分配,避免冲突。如果还需要连接其他SPI设备(如SD卡、传感器),需要考虑总线负载和时序。

8. 常见问题排查与调试记录

即使按照教程操作,也难免会遇到问题。这里汇总了我遇到的一些典型问题及解决方法。

8.1 屏幕无显示或花屏

这是最常见的问题,可能的原因和排查步骤:

  1. 电源与连接

    • 首要检查:用万用表测量屏幕VCC和GND之间电压是否为稳定的3.3V?背光引脚是否有电压?
    • 接线顺序:确保每根线都连接牢固,特别是CLK和MOSI,接触不良会导致数据错乱,表现为花屏或随机条纹。
    • 引脚冲突:确认你使用的GPIO(如D24, D25)没有被系统其他功能占用(如串口、I2C)。可以暂时在代码中更换其他GPIO引脚试试。
  2. 软件配置

    • SPI是否启用:运行ls /dev/spi*确认设备存在。
    • 用户权限:运行groups命令查看当前用户是否在spi组中。如果不是,用sudo运行脚本看是否正常,以确定是权限问题。
    • 驱动型号选择错误:这是花屏的常见原因。ILI9341和ST7789的初始化序列不同,用错驱动会导致乱码。仔细核对屏幕规格书或卖家提供的资料。
    • 初始化参数错误:特别是width,height,x_offset,y_offset。对于非标准分辨率的屏幕(如240x240的ST7789),必须正确设置。一个错误的偏移会导致显示区域错位,看起来像花屏或显示不全。尝试注释掉rotation参数,或将其设为0,看是否有改善。
  3. 速率问题

    • 降低波特率:将代码中的BAUDRATE = 24000000改为BAUDRATE = 16000000甚至8000000。过高的SPI速率可能导致信号质量差,在长导线或劣质屏上尤其明显。
    • 检查逻辑电平:确保树莓派的3.3V电平与屏幕的IO电平匹配。有些5V容忍的屏幕在3.3V下工作可能不稳定。

8.2 显示内容错位或旋转不正确

  • 症状:文字只显示在一半屏幕,或者方向不对。
  • 排查
    1. 检查代码中disp.rotation的设置是否与你物理摆放屏幕的方向一致。
    2. 检查画布创建部分的逻辑(if disp.rotation % 180 == 90:那段)。确保widthheight变量在旋转后正确交换。
    3. 对于有偏移的屏幕,仔细查看驱动库源码或示例中对应你屏幕尺寸的注释行,确保x_offsety_offset参数正确。这些偏移值通常是屏幕驱动IC周围的“死区”像素。

8.3 程序报错“OSError: [Errno 13] Permission denied” 或 “OSError: [Errno 16] Device or resource busy”

  • 权限被拒绝:用户不在spi组。解决:sudo usermod -a -G spi $USER,然后重新登录
  • 设备忙:SPI总线被其他进程占用。可能的原因:
    • 之前运行的程序未正常退出,GPIO未释放。重启树莓派是最快的方法。
    • 内核驱动(如fbtft)占用了设备。运行dtoverlay -l查看已加载的overlay,或在/boot/config.txt中检查并注释掉类似dtoverlay=fb-ili9341的行,然后重启。
    • 其他软件(如pigpio守护进程)可能使用了SPI。尝试停止相关服务。

8.4 图像显示速度慢,刷新卡顿

  • 原因:SPI数据传输是瓶颈,或者Python绘图操作太慢。
  • 优化
    1. 降低分辨率:如果允许,在代码中创建小尺寸画布(如160x120),然后让驱动库缩放(如果支持),或者直接使用屏幕的原始小分辨率模式。
    2. 简化图像:减少颜色数量,避免复杂的抗锯齿绘图操作。
    3. 预渲染静态内容:将不变化的背景、边框等预先绘制到一个Image对象中,每次循环只更新变化的文字或图形,然后与背景合成。
    4. 使用image.paste():更新局部区域时,用paste操作比通过draw重新绘制可能更快。
    5. 检查循环中的阻塞操作:如网络请求、复杂的文件读写等,它们会拖慢整个刷新循环。

8.5 Pillow字体相关错误

  • IOError: cannot open resource:找不到字体文件。检查ImageFont.truetype()中的路径是否正确。可以使用绝对路径,或将字体文件复制到脚本所在目录,使用相对路径“./DejaVuSans.ttf”
  • AttributeError: ‘ImageFont’ object has no attribute ‘getsize’:Pillow版本过高。解决方法:
    # 新版本Pillow获取文字尺寸的方法 try: # Pillow < 10.x bbox = font.getbbox(text) text_width = bbox[2] - bbox[0] text_height = bbox[3] - bbox[1] except AttributeError: # Pillow >= 10.x (或使用getlength) # 注意:getlength只返回宽度,高度需用font.getmetrics() from PIL import ImageFont left, top, right, bottom = font.getbbox(text) text_width = right - left text_height = bottom - top

调试时,一个非常有效的方法是在关键步骤后添加print语句,输出变量状态(如图像尺寸、颜色值),或者先在不连接屏幕的情况下运行,确保没有Python语法和逻辑错误。也可以先用一个极简的测试脚本(只初始化屏幕并显示纯色)来隔离问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询