简介
桌上一直想找一块小屏,能直接看到 CPU、显卡、内存的实时占用,不用切到任务管理器,也不用打开 HWMonitor。手里刚好有一块 Seeed 的 Wio Terminal,自带 320×240 的 LCD 和五向按键,于是就有了这个USB 副屏方案:电脑端用 PowerShell 采集系统数据,每秒往串口发一行;Wio 端只负责解析、画图和按键响应。
最终效果是一块独立的桌面小屏,可以显示 CPU/GPU/RAM/VRAM 四张卡片,摇杆左右切换指标,按下进入最近 60 秒的趋势曲线,再按一下或按 A 键返回主页。串口断开时屏幕会切到"OFFLINE"提示,不会出现假数据。
整体方案
整个系统分两端,运行在两台设备上:
端硬件/平台主要任务
| 电脑端 | Windows 10/11 + PowerShell | 采集 CPU/RAM(CIM)、GPU/VRAM(nvidia-smi),按 1 Hz 发送 |
| 设备端 | Seeeduino Wio Terminal(SAMD51) | 解析串口数据、TFT 渲染、按键交互、记录 60 秒历史 |
两端之间是一条 115200 波特率的串口链路,ASCII 文本协议,每秒交互一次。Wio 端的固件和电脑端的脚本独立运行,电脑端不需要 Python,也不需要管理员权限。
准备工作
硬件只需要一块 Wio Terminal 和一根 Micro-USB 线。
软件侧:
Arduino IDE 里安装 Seeed 的 SAMD 开发板包(Seeeduino SAMD),开发板选 Seeeduino Wio Terminal。
安装 TFT_eSPI 库(Arduino IDE 库管理器直接搜)。
关闭 Arduino IDE 的串口监视器——它会独占串口,导致 PowerShell 拿不到端口。
电脑端只要 NVIDIA 驱动自带 nvidia-smi.exe,没有别的依赖。
串口协议设计
协议设计是我比较在意的一环,因为它决定了两端能不能鲁棒地握手。
请求格式(一行 ASCII,行结束 \n):
WIO1,cpu,gpu,ramUsedMiB,ramTotalMiB,vramUsedMiB,vramTotalMiB
固定前缀 WIO1,方便以后升级协议时区分;
CPU、GPU 是 0–100 的百分比,GPU 不可用时填 -1;
RAM、VRAM 用 MiB(1024 KiB)传绝对值,Wio 端自己换算成百分比和 GiB 显示,传输的是整数倍数,不会有浮点精度问题;
VRAM 两项同时为 -1 表示显存不可用。
每收到一个合法包,Wio 必须回一行 OK,电脑脚本据此判断这一帧真的写进去了。没有收到 OK 就当成串口被 Arduino IDE 占用了,进入重试。
Wio 端的 accept() 在写状态之前做了四道检查:解析值数对得上、行尾没有多余字符、isfinite() 排除 NaN/Inf、数值在合法范围。任何一条不过就不回 OK,电脑侧也不会误显示。
电脑端:PowerShell 采集脚本
最初想用 Python,但最终选择了纯 PowerShell,目标是"装完驱动就能跑"。脚本结构很短,三个函数加一个主循环:
Find-WioPort:用 Get-CimInstance Win32_SerialPort 过滤 VID_2886(Seeed 的厂商 VID)或设备名含 Wio Terminal,唯一匹配才认;
Read-Metrics:CPU/RAM 走 Win32_PerfFormattedData_PerfOS_Processor 和 Win32_OperatingSystem,GPU 走 nvidia-smi --query-gpu=utilization.gpu,memory.used,memory.total;
主循环每 1 秒采一次、用 Stopwatch 校正下一次 sleep,避免漂移。
串口打开时有两处要注意:
$serial.DtrEnable=$true $serial.RtsEnable=$false
Wio Terminal 的 USB CDC 在上电或重枚举时会被某些主机触发自动复位;如果不把 DTR/RTS 拉成特定状态,每次开串口板子就会重启一次。这里把 DTR 拉高、RTS 拉低,跳过自动复位。打开端口后还 Start-Sleep -Milliseconds 1200 等板子稳定,再清输入缓冲区、发数据、读回 OK。
NVIDIA 那边的解析也做了校验:utilization.gpu 在 0–100、已用不超总量、总量大于 0,否则当成不可用走 -1 分支,不影响 CPU/RAM 显示。
Wio 固件:界面与交互
固件部分主要解决两个问题:闪屏和按键抖动。
TFT 的闪屏问题用 TFT_eSPI 的 Sprite(离屏画布)解决:每帧先在内存里画完,再 pushSprite(0, 0) 一次性推上去。我把 color depth 设成 8 位,刚好够区分几个色块,省下显存:
canvas.setColorDepth(8); canvas.createSprite(320, 240);
按键抖动用一个简单的软件去抖:每个按键记录最后一次电平变化时间,稳定 30 ms 才认为是真的按下:
if (now - changed[i] >= 30 && p != stable[i]) {
stable[i] = p;
if (p) { /* 仅在确认按下时执行 */ }
}按下确认后映射成四个操作:
左/右摇杆:selected = (selected ± 1) % 4,循环切换 CPU/GPU/RAM/VRAM;
摇杆中键:在总览和趋势页面之间切;
A 键:直接回总览。
60 秒历史的实现是 4×60 的 int8_t 环形缓冲区——一个指标一字节,整数百分比,不存就写 -1。算下来 240 字节,把 60 秒的历史塞在 RAM 里绰绰有余。
趋势曲线的渲染逻辑放在 draw() 末尾的 else 分支里:Y 轴 0/25/50/75/100 五条参考线,X 轴上把环形缓冲按 count 实际长度映射到 0–266 像素:
int a = (head + 60 - count + i - 1) % 60;
int b = (a + 1) % 60;
if (hist[selected][a] >= 0 && hist[selected][b] >= 0) {
canvas.drawLine(38 + (60 - count + i - 1) * 266 / 59,
193 - hist[selected][a],
38 + (60 - count + i) * 266 / 59,
193 - hist[selected][b],
colors[selected]);
}head 是下一个写入位置,所以读的时候要 (head + 60 - count + i) % 60 这样倒推真实下标。
测试情况
数据通路:powershell -NoProfile -ExecutionPolicy Bypass -File ./pc-monitor.ps1 -Once 跑过一次,能稳定打印一行 WIO1,...,Wio 端回复 OK,电脑端打 [device OK]。
显卡:只在 RTX 3070 上验证过 nvidia-smi 的输出格式。非 NVIDIA 卡会显示 N/A。
GPU 利用率定义:NVIDIA 驱动统计的是 SM 利用率,和 Windows 任务管理器计算得到的"GPU %"不一定相等。
串口长时间运行没有专门做压力测试,但脚本里写了重试和缓冲丢弃,理论上拔插重连会自动恢复。
总结
这个项目实现的功能:
PowerShell 端按 1 Hz 采集 CPU/RAM/GPU/VRAM,自动识别 Wio Terminal 串口,无 Python/无管理员;
Wio 端解析协议、绘制总览/趋势两屏、按键切换、自动检测在线状态、60 秒环形历史;
串口协议做了多道合法性校验,断线不显示假数据,重连自动重置历史。
如果只是想要一块放着就能看的副屏,现在已经够用。后面若把非 N 卡适配补上、温度也接进来,就基本能替代一个简易的 HWMonitor 了。
效果展示
等待串口连接

主页面

总览

历史曲线

历史曲线RAM


我要赚赏金
