1. 项目概述:一块“麻雀虽小,五脏俱全”的交互式显示盾板
如果你玩过Arduino或者树莓派Pico这类微控制器,肯定遇到过这样的烦恼:想给项目加个屏幕显示点信息,或者做个简单的交互界面,结果发现连线一大堆,光是驱动屏幕就要占用好几个宝贵的GPIO引脚,更别提还要接按钮、摇杆了。整个项目面包板上线缆纵横交错,既不稳定也不美观。
Adafruit的1.8英寸TFT Shield V2,就是为了解决这个痛点而生的。它不仅仅是一块屏幕,更是一个高度集成的“交互子系统”。这块盾板的核心是一块128x160像素的彩色TFT显示屏,通过SPI接口与主控通信。但它的精髓在于那颗小小的seesaw I2C扩展芯片——它把屏幕背光控制、复位信号,以及板上所有的按钮(A、B、C)和那个五向摇杆的检测,全部“打包”到了I2C总线上。这意味着,你只需要占用主控的SPI引脚(SCK, MOSI)和两个额外的数字引脚(D10, D8),再加上I2C的两根线(SDA, SCL),就能获得一个完整的、带输入设备的显示模块。对于GPIO资源紧张的微控制器项目来说,这种设计简直是雪中送炭。
我最近在一个环境监测仪表项目里用到了它,需要实时显示温湿度、气压数据,并通过摇杆和按钮切换显示页面、设置阈值。V2 Shield的集成度让我省去了大量底层连线和驱动调试的时间,能更专注于应用逻辑本身。接下来,我就结合自己的实操经验,带你从硬件拆解到软件驱动,彻底玩转这块高效的显示盾板。
2. 硬件深度解析:不只是屏幕,更是智能外设集
刚拿到这块盾板时,别急着上电写代码。花几分钟理解它的硬件架构,能让你在后续开发中避开很多坑,尤其是区分V1和V2版本,这至关重要。
2.1 核心显示单元:ST7735S驱动芯片与接口
位于盾板中央的1.8英寸TFT显示屏,其背后的“大脑”是一颗ST7735S驱动芯片。这是一款非常流行的中小尺寸TFT驱动IC,支持最高18位色深(262K色),在这里以16位RGB(565格式)模式工作。
通信接口:它采用标准的4线SPI接口,这是其高速刷新的基础。
- SCK (Serial Clock):SPI时钟线,由主控提供。
- MOSI (Master Out Slave In):主控输出、屏幕输入的数据线。
- CS (Chip Select, 连接至 D10):片选信号,低电平有效。这是告诉屏幕:“现在数据是发给你的。”
- DC (Data/Command, 连接至 D8):数据/命令选择线。这是SPI屏驱动中的一个关键引脚。当DC为高电平时,MOSI上传送的是要显示的像素数据;当DC为低电平时,传送的是给ST7735S的配置命令(如初始化序列、设置扫描方向等)。很多驱动不成功的问题,都源于对这个引脚的理解或配置有误。
电源与背光:屏幕需要3.3V供电。背光由一组LED提供,其亮度并非简单的高低电平控制,而是由seesaw芯片产生一个PWM(脉冲宽度调制)信号来调节,这意味着你可以在代码中实现背光亮度的平滑调节。
注意:务必确认你的主控板逻辑电平是3.3V。虽然很多5V Arduino(如Uno)也能工作,因为屏上可能有电平转换电路,但最稳妥的方式是使用3.3V逻辑的主控(如大多数32位ARM Cortex-M系列板卡),以避免长期使用可能对屏幕造成的损害。
2.2 交互输入与智慧管理:seesaw I2C扩展器
这是V2 Shield相对于老版本最大的升级,也是其设计巧妙之处。
为什么要用seesaw?试想,如果没有它,你需要如何连接?
- 屏幕背光控制:1个GPIO(PWM)。
- 屏幕复位:1个GPIO。
- 3个按钮(A, B, C):各需1个GPIO(配置为上拉输入)。
- 1个五向摇杆(上、下、左、右、按下):理想情况需要5个GPIO,或者通过模拟引脚+电阻分压网络读取。
这样算下来,至少需要8-10个专用GPIO,这对于只有20个左右GPIO的常见微控制器(如ATmega328P)来说是巨大的负担。seesaw芯片通过I2C接口,将所有这些输入/输出功能“聚合”起来。主控只需要通过两根I2C线(SDA, SCL)与seesaw通信,就能读取所有按钮和摇杆的状态,并控制背光。这极大地释放了主控的GPIO资源。
seesaw的工作原理:你可以把它理解为一个“远程的GPIO扩展器”。主控通过I2C向seesaw发送指令,例如“请把连接背光的那个引脚输出PWM信号,占空比设为50%”,或者“请告诉我所有按钮引脚当前的输入状态”。seesaw内部处理这些请求,并操作其真实的物理引脚。Adafruit提供了封装好的adafruit_seesaw库,使得这些操作像读写本地GPIO一样简单。
一个关键细节:即使你的程序暂时不需要读取按钮或摇杆,也必须初始化seesaw。因为屏幕的复位(tft_reset())和背光控制(set_backlight())函数,都是通过seesaw芯片来执行的。忽略这一步会导致屏幕无法正常点亮或初始化。
2.3 其他实用组件:SD卡与复位按钮
- Micro SD卡槽:通过SPI接口连接(与屏幕共享SCK、MOSI,但使用独立的片选引脚
D4)。它主要用于存储图片、字体文件或数据日志。在显示静态图片(如启动logo)或需要大量字体的项目中非常有用。注意:当同时使用屏幕和SD卡时,要确保SPI总线的操作是分时复用的,避免冲突。通常的库会处理好片选信号。 - 复位按钮:直接连接到主控的复位引脚。按下它会导致整个微控制器重启,程序从头开始运行。在调试时非常方便。
2.4 V1与V2版本的关键区别与识别
原始资料中提到了“Original V1 Shield”,区分它们非常重要,因为驱动方式完全不同。
- V1(旧版):没有seesaw芯片。按钮和摇杆直接占用大量GPIO,摇杆通常使用一个模拟引脚通过电阻网络读取。屏幕的背光和复位可能由独立引脚控制。代码上需要使用软件SPI或特定的引脚定义,兼容性较差(尤其在非ATmega328P主控上)。
- V2(新版,带seesaw):就是我们重点讨论的版本。板上有一颗明显的seesaw芯片(通常印有标识)。所有输入和背光控制都通过I2C。代码使用硬件SPI,并依赖
adafruit_seesaw.tftshield18库。
如何识别:最直观的方法是看板子。如果屏幕下方有三个标有A、B、C的按钮,并且板子上有一颗除了主控和屏幕驱动IC以外的芯片(seesaw),那基本就是V2。也可以查看产品页面或包装上的型号。
3. 软件环境搭建与CircuitPython驱动详解
硬件了然于胸后,我们来搞定软件部分。我将以CircuitPython为例进行讲解,因为它对显示和高级外设的支持越来越完善,代码也更简洁易读。
3.1 准备工作:固件与库文件
- 刷写CircuitPython固件:首先,确保你的主控板(如Adafruit Metro M4 Express、RP2040等)已经刷写了最新版本的CircuitPython固件。去Adafruit官网找到对应板子的页面,下载最新的
.uf2文件,通常通过按住板上的BOOT/USB按钮再上电,会出现一个可移动磁盘,把.uf2文件拖进去即可。 - 获取必要的库文件:CircuitPython通过“库”来扩展功能。你需要下载最新的“Adafruit CircuitPython Library Bundle”。解压后,找到以下库文件/文件夹,并将其复制到你的CircuitPython设备的
lib文件夹中(如果lib文件夹不存在,就创建一个):adafruit_st7735r.mpy- ST7735R显示屏驱动。adafruit_seesaw.mpy- seesaw芯片的核心库。adafruit_seesaw/- seesaw库的辅助文件夹(通常包含tftshield18.py等)。adafruit_bus_device/- 提供I2C、SPI等总线设备的支持。
实操心得:在Windows系统下,直接复制
.mpy文件有时会遇到设备繁忙的提示。一个可靠的方法是:先安全弹出CIRCUITPY磁盘,然后重新插拔USB线,在它被系统重新识别但CircuitPython还未完全挂载文件系统的瞬间快速粘贴。或者,使用Mac或Linux系统进行文件操作会更稳定。
3.2 核心代码逐行解析
下面是一个完整的、带详细注释的测试代码,它初始化屏幕,并持续检测所有按钮和摇杆的按下状态,通过串口打印信息。
# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries # SPDX-License-Identifier: MIT """ 1.8英寸 TFT Shield V2 功能测试 初始化显示屏并读取所有按键与摇杆输入 """ import time import board import displayio # 导入针对此盾板优化的seesaw子模块 from adafruit_seesaw.tftshield18 import TFTShield18 # 导入ST7735R显示屏驱动 from adafruit_st7735r import ST7735R # 处理displayio库版本兼容性问题 (CircuitPython 8.x vs 9.x) try: from fourwire import FourWire except ImportError: from displayio import FourWire # 关键步骤:释放之前可能占用的显示资源 # 特别是在软复位后,避免引脚冲突 displayio.release_displays() # 初始化seesaw芯片,这是控制背光和读取输入的前提 ss = TFTShield18() # 设置SPI总线。board.SPI()会自动选择主控板默认的SPI引脚 spi = board.SPI() # 定义TFT屏的片选(CS)和数据/命令(DC)引脚 tft_cs = board.D10 tft_dc = board.D8 # 创建FourWire显示总线对象,它封装了SPI通信和DC引脚控制 display_bus = FourWire(spi, command=tft_dc, chip_select=tft_cs) # 通过seesaw发送复位信号给TFT屏幕 ss.tft_reset() # 初始化ST7735R驱动 # width=160, height=128: 定义显示分辨率 # rotation=90: 将屏幕旋转90度,通常是为了适应盾板的物理安装方向 # bgr=True: 某些ST7735屏幕的像素颜色顺序是BGR而非RGB,需要此设置 display = ST7735R(display_bus, width=160, height=128, rotation=90, bgr=True) # 打开屏幕背光。你也可以传入0.0到1.0之间的值来调节亮度,例如ss.set_backlight(0.5) ss.set_backlight(True) print("TFT Shield V2 初始化完成!开始检测输入...") # 主循环:持续读取输入状态 while True: # 从seesaw一次性读取所有按钮和摇杆的状态 # buttons是一个对象,其属性(如.up, .a等)在按下时为True buttons = ss.buttons # 检测五向摇杆的各个方向及按下动作 if buttons.right: print("摇杆: 右") if buttons.down: print("摇杆: 下") if buttons.left: print("摇杆: 左") if buttons.up: print("摇杆: 上") if buttons.select: # 注意:在seesaw库中,摇杆的按下动作通常映射为‘select’ print("摇杆: 按下(SELECT)") # 检测A、B、C三个独立按钮 if buttons.a: print("按钮 A 被按下") if buttons.b: print("按钮 B 被按下") if buttons.c: print("按钮 C 被按下") # 短暂延时,降低CPU占用率,并起到简单的防抖作用 # 对于需要快速响应的游戏,可以减小或取消这个延时 time.sleep(0.01)代码要点与避坑指南:
displayio.release_displays():这行代码至关重要。在CircuitPython中,显示对象会“锁定”所使用的硬件引脚。如果程序崩溃后软复位,或者你重新运行了代码,之前的显示对象可能没有正确释放引脚,导致新的初始化失败。加上这行代码是一个好习惯。rotation=90:屏幕的物理0度方向(即(0,0)坐标所在的位置)可能和你的安装预期不符。通过调整rotation参数(0, 90, 180, 270)可以轻松旋转显示内容。bgr=True:这是一个常见的坑点。不同批次的ST7735屏幕,其内部颜色字节顺序可能不同。如果显示颜色异常(比如红色和蓝色反了),尝试将bgr设置为False。- 按钮读取:
ss.buttons返回的是一个状态“快照”。在主循环中,如果按住按钮不放,会连续打印信息。在实际项目中,你可能需要记录“按下”和“释放”事件,这通常通过比较当前状态和上一次状态来实现。
3.3 进阶应用:使用displayio创建图形界面
仅仅打印文本到串口显然浪费了这块彩屏。CircuitPython的displayio模块提供了强大的图形原语支持。下面我们创建一个简单的信息面板,显示模拟的系统状态。
import terminalio from adafruit_display_text import label import vectorio import adafruit_imageload # ... 前面的初始化代码与之前相同,直到创建display对象 ... # 创建一个显示组(Group),它是所有显示元素的容器 main_group = displayio.Group() display.root_group = main_group # 将组设置为屏幕的根内容 # 1. 绘制一个背景矩形 # 使用vectorio创建矩形,Palette定义颜色 palette = displayio.Palette(1) palette[0] = 0x000033 # 深蓝色 background = vectorio.Rectangle(pixel_shader=palette, width=display.width, height=display.height, x=0, y=0) main_group.append(background) # 2. 创建文本标签 # 使用内置字体 font = terminalio.FONT text_area1 = label.Label(font, text="System Info", color=0xFFFFFF, x=10, y=10) text_area2 = label.Label(font, text="CPU: 45%", color=0xFFFF00, x=10, y=30) text_area3 = label.Label(font, text="MEM: 78%", color=0x00FF00, x=10, y=50) text_area4 = label.Label(font, text="TEMP: 32C", color=0xFF00FF, x=10, y=70) main_group.append(text_area1) main_group.append(text_area2) main_group.append(text_area3) main_group.append(text_area4) # 3. 加载并显示一张位图图片(需将图片放入CIRCUITPY根目录) try: image, palette = adafruit_imageload.load("/bg_image.bmp", bitmap=displayio.Bitmap, palette=displayio.Palette) # 创建一个TileGrid来放置图片 tile_grid = displayio.TileGrid(image, pixel_shader=palette, x=90, y=20) main_group.append(tile_grid) except OSError: print("未找到图片文件 /bg_image.bmp") print("图形界面已加载。使用摇杆和按钮交互。") # 主循环:更新界面并响应输入 counter = 0 while True: buttons = ss.buttons # 示例:用A按钮切换一个文本的颜色 if buttons.a: text_area1.color = 0xFF0000 if text_area1.color == 0xFFFFFF else 0xFFFFFF time.sleep(0.2) # 简单防抖 # 示例:用摇杆上下移动第二个文本标签 if buttons.up: text_area2.y = max(10, text_area2.y - 2) if buttons.down: text_area2.y = min(110, text_area2.y + 2) # 模拟动态更新文本内容 counter += 1 text_area3.text = f"COUNT: {counter % 100}" time.sleep(0.05)这个例子展示了displayio的核心概念:Group、TileGrid、Label和Palette。你可以通过动态修改这些对象的属性(如text、color、x、y)来创建丰富的动态界面。
4. 常见问题排查与实战技巧
即使按照步骤操作,也可能会遇到问题。下面是我在多个项目中总结的常见故障点及其解决方法。
4.1 屏幕无显示或花屏
这是最常见的问题,通常与初始化顺序、引脚配置或电源有关。
| 现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 屏幕完全空白,背光也不亮 | 1. 电源问题。 2. 背光未开启。 3. seesaw初始化失败。 | 1. 用万用表检查VIN和3.3V引脚电压是否正常。 2. 确认代码中执行了 ss.set_backlight(True)。3. 检查I2C连接。在代码开头添加 import busio并运行i2c = busio.I2C(board.SCL, board.SDA); print(“I2C devices found:”, i2c.scan()),看是否能找到seesaw的地址(通常是0x49或0x50)。 |
| 背光亮,但屏幕全白、全黑或有规律条纹 | 1. 复位时序问题。 2. SPI速率过快。 3. bgr参数设置错误。4. 分辨率或旋转设置错误。 | 1. 确保在初始化显示驱动之前调用了ss.tft_reset()。2. 尝试在初始化SPI时降低波特率: spi = board.SPI(baudrate=1000000)(默认通常很高)。3. 尝试将 ST7735R初始化中的bgr=True改为bgr=False。4. 核对 width和height是否为160和128,尝试不同的rotation值。 |
| 显示内容错位、撕裂或部分区域异常 | 1. 内存不足或帧缓冲区错误。 2. 主循环处理太慢,导致刷新不同步。 | 1. 确保主控板有足够RAM。简化displayio的Group结构,减少同时显示的复杂对象。2. 优化代码,避免在主循环中进行大量计算或阻塞操作。使用 time.monotonic()进行非阻塞延时。 |
4.2 按钮或摇杆无响应
输入功能依赖I2C通信和seesaw芯片。
| 现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 所有按钮和摇杆均无响应 | 1. I2C总线通信失败。 2. TFTShield18()对象初始化失败。3. 库文件缺失或版本不匹配。 | 1. 同上述“屏幕无显示”中的I2C扫描步骤,确认seesaw被识别。 2. 检查 lib文件夹中是否有完整的adafruit_seesaw.mpy及其子目录。3. 尝试更新到最新版本的CircuitPython库Bundle。 |
| 个别按钮无响应 | 1. 硬件接触不良(如虚焊)。 2. 代码中读取的逻辑错误。 | 1. 仔细检查该按钮对应的引脚与seesaw的焊接点。 2. 使用示例代码测试,确认是硬件问题还是软件问题。示例代码中 buttons.a等属性名是固定的,不要写错。 |
| 摇杆方向识别不准 | 1. 摇杆是模拟量+数字按键,而V2 Shield已将其数字化。此问题在V2上较少见。 2. 机械磨损或损坏。 | 1. V2 Shield的摇杆状态是通过seesaw以数字方式(按下/未按下)报告的,而非模拟值。确认你使用的是正确的属性(buttons.up,.down等)。2. 如果是老V1 Shield,需要校准模拟读取的阈值,代码中 CheckJoystick()函数的数值范围(50, 150, 250...)可能需要根据实际硬件调整。 |
4.3 性能优化与高级技巧
当项目复杂后,你可能会觉得刷新速度不够快,或者内存紧张。这里有一些进阶技巧:
使用
displayio.OnDiskBitmap显示图片:前面的例子用adafruit_imageload将整个位图加载到内存中,对于大图片很耗RAM。OnDiskBitmap可以直接从存储设备流式读取并显示图片,极大节省内存。odb = displayio.OnDiskBitmap("/large_image.bmp") tile_grid = displayio.TileGrid(odb, pixel_shader=displayio.ColorConverter(), x=0, y=0) group.append(tile_grid)局部刷新与脏矩形:
displayio会自动管理刷新,但如果你在频繁更新一个很小的区域(如一个数字),可以尝试将变化的内容放在一个独立的Group中,但更有效的方法是理解displayio的刷新机制——它通常足够智能。避免频繁创建和销毁对象,重用对象并只修改其属性。处理按钮的长按与短按:示例代码只检测了“按下”状态。要实现“长按”、“短按”、“双击”,需要记录时间戳。
import time a_pressed_time = None while True: buttons = ss.buttons if buttons.a: if a_pressed_time is None: # 首次按下 a_pressed_time = time.monotonic() else: if a_pressed_time is not None: # 刚刚释放 press_duration = time.monotonic() - a_pressed_time if press_duration > 1.0: print("A按钮长按") else: print("A按钮短按") a_pressed_time = None time.sleep(0.05)与SD卡共用SPI总线:如果你需要同时使用屏幕和SD卡,确保在访问其中一个设备时,正确控制其片选(CS)引脚。
adafruit_sdcard库和displayio通常能很好地协同工作,因为它们都会在操作前后拉低和拉高各自的CS引脚。关键是确保你的接线正确(SD卡CS接D4),并且在代码中为SD卡创建SPI对象时,使用与屏幕相同的spi总线对象,但指定不同的片选引脚。
这块1.8英寸TFT Shield V2是我项目箱里的常客,它的高度集成性在快速原型开发阶段无可替代。从简单的传感器数据显示,到带有菜单的交互设备,它都能胜任。最关键的是,吃透了seesaw那套I2C管理GPIO的思路后,你会发现在资源受限的嵌入式环境里,通过I2C扩展器来管理多个低速外设是一种非常优雅和高效的设计模式。下次当你面对一堆需要连接的按钮和LED时,不妨想想是否也能用一颗小小的seesaw来简化你的电路和代码。