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寸ILI9341和1.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屏幕,其引脚通常包括VCC、GND、CS、RST、DC、MOSI、SCLK,有时还有MISO和背光控制BL。
推荐接线方式如下:
| 屏幕引脚 | 树莓派GPIO | 物理引脚 | 说明 |
|---|---|---|---|
| VCC | 3.3V | 引脚1或17 | 绝对不要接5V!会烧屏。 |
| GND | GND | 引脚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.3V或GPIO | 引脚1或17 | 接3.3V常亮,接GPIO则可编程控制。 |
重要提示:有些大尺寸屏幕(如某些3.5寸屏)默认是8位并行接口。你需要检查屏幕背面,通常会有焊盘或跳线帽(标记为
IM0,IM1,IM2,IM3)。必须将其配置为SPI模式。具体方法请查阅屏幕手册,通常需要将IM0接高电平(3.3V),IM1、IM2、IM3接低电平(GND)。
2.3 ST7789屏幕接线方案
ST7789常用于1.3寸、1.54寸、2.0寸的IPS屏,接线与ILI9341类似,但需要注意一些屏幕(特别是高分辨率或带偏移的)在初始化时需要特定的width、height、x_offset、y_offset参数。
接线参考表:
| 屏幕引脚 | 树莓派GPIO | 物理引脚 | 说明 |
|---|---|---|---|
| VCC | 3.3V | 引脚1 | |
| GND | GND | 引脚6 | |
| CS | CE0 (GPIO 8) | 引脚24 | |
| RST | GPIO 24 | 引脚18 | |
| DC | GPIO 25 | 引脚22 | |
| MOSI | MOSI (GPIO 10) | 引脚19 | |
| SCLK | SCLK (GPIO 11) | 引脚23 | |
| BL | 3.3V或GPIO 18 | 引脚1或12 | 建议接GPIO以便软件控制亮度。 |
2.4 接线实操心得与避坑点
- 电源是头等大事:绝大多数3.3V逻辑的TFT屏,VCC必须接树莓派的3.3V引脚。接5V瞬间冒烟不是开玩笑。如果你需要驱动大尺寸屏或背光电流很大,可以考虑使用外部3.3V稳压模块单独供电,但信号线仍需连接树莓派。
- GPIO编号与物理引脚:代码里用的是BCM编号(如
board.D25),对应的是GPIO 25,而不是物理引脚号。接线时务必对照GPIO引脚图,我习惯用pinout命令在终端查看。 - SPI使能:默认情况下,树莓派的SPI接口可能是关闭的。需要通过
sudo raspi-config->Interface Options->SPI->Yes来启用。启用后,可以在/dev/下看到spidev0.0和spidev0.1设备。 - 避免引脚冲突:如果你之前为这块屏幕安装过
fbtft内核驱动,务必先卸载或禁用,否则SPI总线会被内核驱动占用,导致我们的Python程序无法访问。通常可以编辑/boot/config.txt,注释掉相关的dtoverlay行并重启。 - 背光控制:如果屏幕背光单独引出了
BL或LED引脚,强烈建议将其连接到一个可用的GPIO(如GPIO 26),而不是直接接3.3V。这样你可以在代码中控制背光开关,实现息屏节能,体验好很多。
3. 软件环境搭建与库安装详解
硬件连好后,我们就进入软件环节。这套方案的核心是三个Python库:Adafruit-Blinka、Adafruit_CircuitPython_RGB_Display和Pillow。
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计算机(如树莓派)上工作。它本质上是board和busio等模块在Linux上的实现。
安装非常简单,使用pip3即可:
sudo pip3 install adafruit-blinka如果你的系统没有pip3,需要先安装:
sudo apt update sudo apt install python3-pip3.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可能缺少一些底层图像库支持,如果运行时报错关于libopenjp2或libtiff,可以一并安装这些系统依赖:
sudo apt-get install libopenjp2-7 libtiff5 libatlas-base-dev3.4 安装字体(可选但推荐)
示例代码中使用了DejaVu字体,树莓派系统通常已预装。如果未安装,可以手动安装:
sudo apt-get install fonts-dejavu3.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初始化行,并可能需要调整
width、height、x_offset、y_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.width和disp.height返回的是屏幕物理像素尺寸。当设置rotation=90后,驱动内部会处理像素数据的排列,但disp.width和disp.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)算法逻辑解读:
- 比较宽高比:目的是判断是图片的宽度过剩还是高度过剩。
- 等比缩放:让图片的短边刚好等于屏幕的对应边。例如,屏幕是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)。 - 居中裁剪:从缩放后的图片中间,裁剪出一个和屏幕一样大(240x320)的区域。
x = (426-240)//2 = 93,y=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) # 每秒刷新一次关键优化点:
- 局部刷新:上述代码每次循环都重绘了整个屏幕。对于只有文字变化的场景,这是低效的。更优的方案是只刷新变化的部分(如数字),但这需要更复杂的逻辑来判断和计算脏区域。对于小型屏幕和简单应用,全屏刷新通常可以接受。
- 刷新率与功耗:
time.sleep(1)让循环每秒运行一次,对于系统监控足够了。如果用于动画,可能需要更短的间隔(如0.1秒),但要考虑SPI写入速度和CPU负载。过高的刷新率可能导致屏幕闪烁或系统卡顿。 - 错误处理:
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(),它是全屏更新。你可以研究驱动库的底层方法,看是否有blit或block操作。 - 双缓冲与直接操作:创建两个
Image对象,一个用于绘制(后台缓冲区),完成后一次性调用disp.image()切换到前台。这可以避免绘制过程中的闪烁。更激进的做法是直接操作图像的像素数据数组(image.tobytes()或image.load()),但这对编程要求较高。
7.2 使用硬件加速(如果可用)
树莓派的GPU(VideoCore)能力强大。虽然我们这个方案是用户空间的,但可以通过其他途径间接利用:
- 使用
pygame或SDL2:它们可能利用硬件加速进行渲染,然后将最终帧缓冲区转换为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 屏幕无显示或花屏
这是最常见的问题,可能的原因和排查步骤:
电源与连接:
- 首要检查:用万用表测量屏幕VCC和GND之间电压是否为稳定的3.3V?背光引脚是否有电压?
- 接线顺序:确保每根线都连接牢固,特别是CLK和MOSI,接触不良会导致数据错乱,表现为花屏或随机条纹。
- 引脚冲突:确认你使用的GPIO(如D24, D25)没有被系统其他功能占用(如串口、I2C)。可以暂时在代码中更换其他GPIO引脚试试。
软件配置:
- SPI是否启用:运行
ls /dev/spi*确认设备存在。 - 用户权限:运行
groups命令查看当前用户是否在spi组中。如果不是,用sudo运行脚本看是否正常,以确定是权限问题。 - 驱动型号选择错误:这是花屏的常见原因。ILI9341和ST7789的初始化序列不同,用错驱动会导致乱码。仔细核对屏幕规格书或卖家提供的资料。
- 初始化参数错误:特别是
width,height,x_offset,y_offset。对于非标准分辨率的屏幕(如240x240的ST7789),必须正确设置。一个错误的偏移会导致显示区域错位,看起来像花屏或显示不全。尝试注释掉rotation参数,或将其设为0,看是否有改善。
- SPI是否启用:运行
速率问题:
- 降低波特率:将代码中的
BAUDRATE = 24000000改为BAUDRATE = 16000000甚至8000000。过高的SPI速率可能导致信号质量差,在长导线或劣质屏上尤其明显。 - 检查逻辑电平:确保树莓派的3.3V电平与屏幕的IO电平匹配。有些5V容忍的屏幕在3.3V下工作可能不稳定。
- 降低波特率:将代码中的
8.2 显示内容错位或旋转不正确
- 症状:文字只显示在一半屏幕,或者方向不对。
- 排查:
- 检查代码中
disp.rotation的设置是否与你物理摆放屏幕的方向一致。 - 检查画布创建部分的逻辑(
if disp.rotation % 180 == 90:那段)。确保width和height变量在旋转后正确交换。 - 对于有偏移的屏幕,仔细查看驱动库源码或示例中对应你屏幕尺寸的注释行,确保
x_offset和y_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绘图操作太慢。
- 优化:
- 降低分辨率:如果允许,在代码中创建小尺寸画布(如160x120),然后让驱动库缩放(如果支持),或者直接使用屏幕的原始小分辨率模式。
- 简化图像:减少颜色数量,避免复杂的抗锯齿绘图操作。
- 预渲染静态内容:将不变化的背景、边框等预先绘制到一个
Image对象中,每次循环只更新变化的文字或图形,然后与背景合成。 - 使用
image.paste():更新局部区域时,用paste操作比通过draw重新绘制可能更快。 - 检查循环中的阻塞操作:如网络请求、复杂的文件读写等,它们会拖慢整个刷新循环。
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语法和逻辑错误。也可以先用一个极简的测试脚本(只初始化屏幕并显示纯色)来隔离问题。