1. 项目概述当Wio Terminal遇见树莓派最近在折腾一个远程监控的小项目核心需求是想在不依赖网络、不占用额外显示设备的情况下实时查看我那台跑着服务的树莓派的状态。树莓派本身没有屏幕每次想看看CPU温度、内存占用或者磁盘空间都得SSH登录或者打开网页管理界面总觉得不够直观和“物理”。直到我手边的Wio Terminal进入了视线——这块自带屏幕、按键和丰富接口的开发板不正好可以作为一个完美的“状态仪表盘”吗这个项目的核心就是让Wio Terminal通过USB串口连接树莓派实时读取并显示树莓派的系统状态信息。听起来像是简单的串口通信但实际做下来从硬件连接到协议设计再到数据解析和显示优化每一步都有不少细节值得琢磨。它完美解决了“无头”树莓派状态可视化的痛点特别适合部署在机柜、工作台或者任何你需要一眼掌握系统健康度的地方。无论你是嵌入式爱好者、运维工程师还是单纯的极客玩家这个项目都能让你对串口通信、系统监控和嵌入式UI有更深的体会。2. 核心思路与方案选型为什么选择Wio Terminal和USB串口这个组合这背后是基于几个关键的考量。首先Wio Terminal本身就是一个All-in-One的解决方案。它集成了彩色LCD屏幕、多个物理按键、蜂鸣器、光线传感器等这意味着我们不需要再额外连接任何显示模块或输入设备极大地简化了硬件结构。其核心MCU是性能足够的SAM D51驱动一个小型信息界面绰绰有余。其次通信方式的选择。连接树莓派和外设的方式有很多比如GPIO、I2C、SPI、网络Wi-Fi/以太网。GPIO直接连接需要飞线不够优雅且占用树莓派宝贵的GPIO资源。I2C/SPI速度虽快但通常用于板间短距离通信且需要额外的电平转换考虑。网络方式固然强大但会引入网络配置、IP地址、协议栈等复杂度对于这个简单的状态监控任务来说有点“杀鸡用牛刀”。USB转串口USB-to-UART成为了最平衡的选择。现代树莓派3B、4B、5等的硬件设计已经将板载蓝牙/Wi-Fi芯片的UART引出到了GPIO排针上通常是TXD0和RXD0但这个UART默认可能被系统控制台占用。更通用、更稳定的方法是使用一个独立的USB转TTL串口模块或者直接利用Wio Terminal作为一个USB CDC通信设备类设备。在我们的方案中我们将树莓派视为“主机”Wio Terminal作为“设备”。通过一根普通的Micro-USB数据线连接两者在树莓派上Wio Terminal会被识别为一个/dev/ttyACM0或类似的串口设备我们只需要在树莓派上运行一个简单的Python脚本定期将系统状态信息通过这个串口发送出去即可。这种方式连接简单、稳定、通用几乎不需要额外的硬件成本一根USB线足矣并且完全独立于树莓派的GPIO和网络栈。注意这里有一个常见的混淆点。Wio Terminal本身也可以通过其内部的ATmega4809协处理器模拟为一个USB转串口桥用于给主MCUSAM D51烧录程序。但在我们项目的软件配置中我们将直接使用SAM D51的USB堆栈将其配置为一个虚拟串口CDC与树莓派通信。这是最直接高效的方式。最后数据协议与显示。我们需要定义一套简单有效的应用层协议。树莓派端脚本负责收集数据如使用vcgencmd读取温度free和df命令读取内存磁盘mpstat读取CPU负载并将其格式化为结构化的字符串例如JSON或自定义的键值对格式然后通过串口发送。Wio Terminal端则负责接收、解析这些字符串并驱动屏幕将其以美观、易读的方式如图表、进度条、数字显示出来。3. 硬件连接与软件环境准备3.1 硬件清单与连接这个项目所需的硬件非常简单树莓派一台以Raspberry Pi 4B为例其他型号原理相同。Wio Terminal一台。Micro-USB数据线一根用于连接Wio Terminal到树莓派的USB接口。连接步骤再简单不过将Micro-USB线的一端插入Wio Terminal侧面的USB-C口用于数据和供电另一端插入树莓派的任意一个USB-A口。此时树莓派会为Wio Terminal供电并开始识别这个新的USB设备。3.2 树莓派端环境配置树莓派上我们需要准备两件事一是确保能识别Wio Terminal的USB串口二是准备好用于收集系统状态和发送数据的Python脚本。首先连接Wio Terminal后在树莓派终端中输入ls /dev/tty*你应该能看到类似/dev/ttyACM0的新设备。这是Wio Terminal的CDC串口在Linux系统中的设备节点。如果看到的是/dev/ttyUSB0那可能是Wio Terminal的bootloader模式或不同固件导致的不影响使用后续脚本中对应修改设备名即可。实操心得为了确保普通用户权限也能访问串口通常需要将你的用户如pi加入到dialout组。命令是sudo usermod -a -G dialout $USER执行后需要注销并重新登录才能生效。否则运行Python脚本时可能会遇到Permission denied: /dev/ttyACM0的错误。其次安装必要的Python库。我们主要会用到的有pyserial用于串口通信psutil是一个强大的跨平台系统监控库能非常方便地获取CPU、内存、磁盘等信息。# 更新软件包列表 sudo apt update # 安装Python3和pip如果尚未安装 sudo apt install python3 python3-pip # 安装必要的Python库 pip3 install pyserial psutilpsutil不是必须的你可以完全通过调用Linux命令如os.popen并解析输出来获取信息。但使用psutil会让代码更简洁、更跨平台。例如获取CPU温度树莓派上有特有的路径/sys/class/thermal/thermal_zone0/temp而psutil在Linux上也能通过sensors功能获取但对于树莓派直接读文件更可靠。3.3 Wio Terminal端开发环境配置Wio Terminal支持Arduino框架和CircuitPython。这里我们选择Arduino框架因为它性能更好社区资源丰富并且Wio Terminal的TFT库在Arduino下非常成熟。安装Arduino IDE从Arduino官网下载并安装最新版IDE。添加Seeed SAMD板支持打开Arduino IDE进入“文件”-“首选项”在“附加开发板管理器网址”中添加https://files.seeedstudio.com/arduino/package_seeeduino_boards_index.json。然后进入“工具”-“开发板”-“开发板管理器”搜索“Seeed SAMD”安装“Seeed SAMD Boards”。安装库文件我们需要两个核心库TFT_eSPI这是驱动Wio Terminal屏幕的高性能库。可以通过IDE的库管理器搜索安装。Seeed_Arduino_FS用于支持文件系统如果未来需要存储图标等。 安装后需要对TFT_eSPI库进行配置。找到Arduino库安装目录下的TFT_eSPI文件夹编辑其中的User_Setup.h文件。你需要注释掉其他所有板型的定义然后找到并启用关于Wio Terminal的定义通常是#define SEEED_WIO_TERMINAL。Wio Terminal的屏幕驱动芯片是ILI9341确保相关设置正确。4. 树莓派状态收集与发送脚本详解树莓派端的Python脚本是数据生产者。它的核心任务是周期性地收集关键系统指标打包成约定格式的字符串并通过串口发送。4.1 关键状态指标的选择与获取对于系统监控我们关注几个核心指标CPU温度树莓派在高负载下可能过热降频。CPU使用率反映系统当前的计算负载。内存使用率包括已用内存和可用内存。磁盘使用率根分区的剩余空间。系统负载1分钟、5分钟、15分钟的平均负载。网络活动可选上行/下行速率。以下是使用Python获取这些信息的示例代码片段import os import psutil import time def get_cpu_temperature(): 获取CPU温度树莓派专用 try: with open(/sys/class/thermal/thermal_zone0/temp, r) as f: temp int(f.read()) / 1000.0 # 读取值为毫摄氏度 return temp except: return None def get_cpu_usage(): 获取CPU总使用率百分比 return psutil.cpu_percent(interval0.5) # 阻塞0.5秒获取瞬时值 def get_memory_info(): 获取内存信息返回已用百分比和可用内存MB mem psutil.virtual_memory() return mem.percent, mem.available // (1024**2) def get_disk_info(): 获取根分区磁盘使用率百分比 disk psutil.disk_usage(/) return disk.percent def get_load_average(): 获取系统平均负载 load1, load5, load15 os.getloadavg() return load1, load5, load154.2 数据格式化与串口发送为了便于Wio Terminal解析我们选择使用JSON格式。JSON结构清晰Arduino端有优秀的ArduinoJson库可以轻松解析。我们将上述数据打包成一个JSON对象。import json import serial from datetime import datetime # 配置串口端口和设备名根据实际情况调整 SERIAL_PORT /dev/ttyACM0 BAUD_RATE 115200 def main(): # 初始化串口 # 注意如果第一次打开端口失败可能是设备未就绪可以加个延时重试 try: ser serial.Serial(SERIAL_PORT, BAUD_RATE, timeout1) print(f串口 {SERIAL_PORT} 打开成功) except serial.SerialException as e: print(f无法打开串口 {SERIAL_PORT}: {e}) return try: while True: # 收集数据 cpu_temp get_cpu_temperature() cpu_usage get_cpu_usage() mem_percent, mem_avail_mb get_memory_info() disk_percent get_disk_info() load1, load5, load15 get_load_average() timestamp datetime.now().strftime(%H:%M:%S) # 构建JSON数据 status_data { ts: timestamp, cpu: { temp: cpu_temp, usage: cpu_usage }, mem: { percent: mem_percent, available_mb: mem_avail_mb }, disk: { usage_percent: disk_percent }, load: { 1min: load1, 5min: load5, 15min: load15 } } # 转换为JSON字符串并添加换行符作为帧分隔符 json_str json.dumps(status_data) \n # 发送数据 ser.write(json_str.encode(utf-8)) print(f已发送: {json_str.strip()}) # 等待一段时间后再次收集例如每2秒一次 time.sleep(2) except KeyboardInterrupt: print(\n程序被用户中断) finally: if ser and ser.is_open: ser.close() print(串口已关闭) if __name__ __main__: main()注意事项串口通信中帧分隔符非常重要。这里我们在每个JSON字符串后面加了一个换行符\n。在Wio Terminal端我们将以\n为标志读取完整的一帧数据。波特率115200是一个在稳定性和速度之间取得良好平衡的值确保双方设置一致。5. Wio Terminal端程序设计与实现Wio Terminal端的程序主要负责三件事通过USB CDC串口接收数据、解析JSON数据、在屏幕上绘制UI并更新状态。5.1 程序框架与库引入首先在Arduino IDE中新建项目引入必要的库并定义全局变量。#include TFT_eSPI.h #include ArduinoJson.h TFT_eSPI tft TFT_eSPI(); // 定义颜色 #define BACKGROUND TFT_BLACK #define TEXT_COLOR TFT_WHITE #define CPU_COLOR TFT_RED #define MEM_COLOR TFT_GREEN #define DISK_COLOR TFT_BLUE #define TEMP_COLOR TFT_YELLOW // 定义显示区域 #define MARGIN 10 #define ROW_HEIGHT 30 #define BAR_WIDTH 200 #define BAR_HEIGHT 15 // 用于存储解析后的数据 float cpuUsage 0.0; float cpuTemp 0.0; float memUsage 0.0; int memAvailableMB 0; float diskUsage 0.0; float load1 0.0; String timeStr ; // 串口接收缓冲区 String serialBuffer ;5.2 串口数据接收与解析在loop()函数中我们需要持续检查串口是否有数据到达并累积到缓冲区直到遇到换行符\n表示一帧数据接收完成。void loop() { // 1. 接收串口数据 while (Serial.available()) { char c Serial.read(); if (c \n) { // 接收到完整一帧 parseStatusData(serialBuffer); serialBuffer ; // 清空缓冲区 } else { serialBuffer c; } } // 2. 更新屏幕显示可以设定一个刷新间隔避免过于频繁重绘 static unsigned long lastDraw 0; if (millis() - lastDraw 1000) { // 每秒刷新一次UI drawUI(); lastDraw millis(); } }parseStatusData函数负责解析JSON字符串。这里我们使用ArduinoJson库它非常节省内存适合嵌入式环境。void parseStatusData(String jsonStr) { // 建议使用StaticJsonDocument根据JSON大小调整容量 const size_t capacity JSON_OBJECT_SIZE(5) JSON_OBJECT_SIZE(2) * 3 120; StaticJsonDocumentcapacity doc; DeserializationError error deserializeJson(doc, jsonStr); if (error) { Serial.print(JSON解析失败: ); Serial.println(error.c_str()); return; } // 提取数据 timeStr doc[ts].asString(); cpuTemp doc[cpu][temp]; cpuUsage doc[cpu][usage]; memUsage doc[mem][percent]; memAvailableMB doc[mem][available_mb]; diskUsage doc[disk][usage_percent]; load1 doc[load][1min]; }避坑技巧StaticJsonDocument的容量需要仔细估算。容量过小会导致解析失败过大则会浪费宝贵的内存。可以使用ArduinoJson Assistant工具在线来帮助计算。对于我们这个简单的状态JSONcapacity设为300-400字节通常足够。5.3 UI绘制与状态可视化UI绘制在drawUI()函数中完成。我们将屏幕划分为几个区域分别显示不同的状态信息。使用进度条显示百分比数据用数字显示温度和负载。void drawUI() { tft.fillScreen(BACKGROUND); int yPos MARGIN; // 1. 绘制标题和时间 tft.setTextColor(TEXT_COLOR, BACKGROUND); tft.setTextSize(2); tft.setCursor(MARGIN, yPos); tft.print(RPi Status); tft.setTextSize(1); tft.setCursor(tft.width() - 80, yPos); tft.print(timeStr); yPos 30; // 2. 绘制CPU温度 drawLabelValueBar(CPU Temp:, String(cpuTemp, 1) C, cpuTemp, 50.0, TEMP_COLOR, yPos); // 假设50度为参考上限 yPos ROW_HEIGHT; // 3. 绘制CPU使用率 drawLabelValueBar(CPU Usage:, String(cpuUsage, 1) %, cpuUsage, 100.0, CPU_COLOR, yPos); yPos ROW_HEIGHT; // 4. 绘制内存使用率 drawLabelValueBar(Memory:, String(memUsage, 1) %, memUsage, 100.0, MEM_COLOR, yPos); tft.setTextSize(1); tft.setCursor(MARGIN BAR_WIDTH 15, yPos - 8); tft.print(String(memAvailableMB) MB free); yPos ROW_HEIGHT; // 5. 绘制磁盘使用率 drawLabelValueBar(Disk (/):, String(diskUsage, 1) %, diskUsage, 100.0, DISK_COLOR, yPos); yPos ROW_HEIGHT; // 6. 绘制系统负载 tft.setTextSize(2); tft.setTextColor(TEXT_COLOR, BACKGROUND); tft.setCursor(MARGIN, yPos); tft.print(Load Avg: ); tft.print(load1, 2); } // 一个辅助函数用于绘制“标签数值 进度条”的组合 void drawLabelValueBar(String label, String value, float current, float maxVal, uint16_t color, int y) { tft.setTextSize(2); tft.setTextColor(TEXT_COLOR, BACKGROUND); tft.setCursor(MARGIN, y); tft.print(label); tft.setCursor(tft.width() - 60, y); tft.print(value); // 绘制背景条 tft.fillRoundRect(MARGIN, y 18, BAR_WIDTH, BAR_HEIGHT, 3, TFT_DARKGREY); // 计算填充长度 int fillWidth (int)((current / maxVal) * BAR_WIDTH); fillWidth constrain(fillWidth, 0, BAR_WIDTH); // 绘制前景条 tft.fillRoundRect(MARGIN, y 18, fillWidth, BAR_HEIGHT, 3, color); }在setup()函数中我们需要初始化屏幕和串口。void setup() { // 初始化串口波特率必须与树莓派发送端一致 Serial.begin(115200); // 等待串口连接对于USB CDC可能需要一点时间 while (!Serial) { delay(10); } // 初始化屏幕 tft.begin(); tft.setRotation(3); // 根据你的摆放方向调整旋转角度3为USB口在右侧 tft.fillScreen(BACKGROUND); // 显示启动信息 tft.setTextColor(TEXT_COLOR, BACKGROUND); tft.setTextSize(2); tft.setCursor(MARGIN, MARGIN); tft.println(Wio Terminal); tft.setCursor(MARGIN, MARGIN 25); tft.println(RPi Monitor); tft.setTextSize(1); tft.setCursor(MARGIN, MARGIN 55); tft.println(Waiting for data...); delay(2000); }6. 系统集成、调试与优化将两部分代码分别部署到树莓派和Wio Terminal后就可以进行系统联调了。6.1 部署与启动流程树莓派端将Python脚本例如命名为rpi_status_sender.py保存到树莓派上。可以通过scp命令从开发机上传。然后通过SSH登录树莓派运行python3 rpi_status_sender.py。首次运行前确保已安装pyserial和psutil。Wio Terminal端用Arduino IDE打开完整的.ino项目文件选择开发板为“Seeed Wio Terminal”选择正确的端口点击上传。上传完成后Wio Terminal会自动重启运行新程序。如果一切正常你将在Wio Terminal的屏幕上看到“Waiting for data...”的提示几秒后树莓派的数据开始发送屏幕内容就会刷新为实时的系统状态信息。6.2 常见问题与排查技巧在实际操作中你可能会遇到以下问题问题现象可能原因排查步骤与解决方案Wio Terminal屏幕白屏或乱码1. TFT_eSPI库配置错误。2. 屏幕初始化失败。1. 仔细检查User_Setup.h确保SEEED_WIO_TERMINAL被正确定义其他板型被注释。2. 尝试运行TFT_eSPI库中的示例程序如graphicstest验证屏幕驱动是否正常。树莓派Python脚本报错Permission denied用户没有串口设备的读写权限。将当前用户加入dialout组sudo usermod -a -G dialout $USER然后注销并重新登录。树莓派找不到/dev/ttyACM0设备1. USB线仅供电无数据传输。2. Wio Terminal未正确进入CDC模式。3. 系统识别为其他设备名。1. 使用数据线而非仅充电线。2. 检查Wio Terminal程序是否正确初始化了Serial.begin()。3. 运行ls /dev/tty*前后对比或检查dmesgWio Terminal收不到数据1. 波特率不匹配。2. 串口未成功打开。3. 帧格式不对。1. 确认双方Serial.begin()和serial.Serial()的波特率完全相同如115200。2. 在Wio Terminal的setup()里加while(!Serial);确保USB连接就绪。3. 在Wio Terminal端打印原始接收到的字符串检查树莓派发送的JSON格式是否正确是否以\n结尾。JSON解析失败1. JSON格式错误。2. ArduinoJson文档容量不足。3. 字符串包含非法字符。1. 在Wio Terminal端打印serialBuffer复制到在线JSON校验器检查。2. 适当增大StaticJsonDocument的容量。3. 确保树莓派发送的是UTF-8编码且没有多余的空格或换行在JSON内部。屏幕刷新闪烁每次drawUI()都全屏清空重绘。采用局部刷新策略。只更新变化的部分比如数值和进度条。对于静态的标签和边框只在初始化时绘制一次。这需要更精细的代码控制但能极大提升视觉体验。数据更新延迟或卡顿1. 树莓派发送间隔太短串口缓冲区溢出。2. Wio Terminal绘制耗时太长。1. 适当增加树莓派脚本的time.sleep间隔如2-5秒。2. 优化Wio Terminal的绘制代码避免在loop()中做复杂的全屏绘制。使用状态标志仅在收到新数据后才触发重绘。6.3 功能扩展与优化建议基础功能跑通后你可以考虑以下方向进行扩展让这个状态显示器更加强大增加更多传感器数据Wio Terminal自带的光线传感器、麦克风、三轴加速度计等可以采集环境信息一并显示或者作为交互触发条件如敲击切换页面。实现双向交互目前是单向通信。你可以定义一些简单的命令如通过Wio Terminal的按键发送REBOOT、SHUTDOWN字符串让树莓派端脚本接收并执行相应操作实现简单的远程控制。历史趋势图在Wio Terminal上开辟一块区域存储最近一段时间如60个点的CPU温度或使用率数据并绘制成简单的折线图直观反映历史趋势。报警功能当某个指标超过阈值如CPU温度70°C让Wio Terminal的蜂鸣器发出警报或者屏幕闪烁红色提示。多页面显示利用Wio Terminal的按键实现不同信息页面的切换。比如A键显示系统状态B键显示网络信息C键显示进程列表需要树莓派发送更多数据。低功耗优化如果希望它长时间独立工作可以考虑用电池供电并优化代码在无操作时降低屏幕亮度或进入睡眠模式按按键唤醒。这个项目就像一把钥匙打开了Wio Terminal作为“智能交互终端”的大门也深化了对树莓派系统管理和串口通信的理解。从一根USB线连接开始到最终形成一个稳定、直观的状态监控器整个过程遇到的每一个问题和解法都是嵌入式开发中宝贵的经验。