1. 项目概述与设计初衷
几年前,我给自己那台装了分体水冷的电脑做了一次大升级。机器是安静了,性能也上去了,但机箱侧板后面总感觉空荡荡的,少了点“灵魂”。更重要的是,每次想看看CPU、GPU的温度和负载,要么得切到桌面看软件,要么就得忍受游戏内OSD(屏幕显示)信息挡在画面角落。市售的硬件监控屏要么尺寸不合适,要么外观太“电竞风”,跟我追求的简洁风格格格不入。于是,一个念头冒了出来:为什么不自己动手做一个完全符合自己审美和需求的监控屏呢?
这个项目的核心,就是打造一个基于Arduino的PC硬件监控器。它不依赖HDMI输出,不需要在Windows里额外开个窗口,而是作为一个独立的“外设”,通过USB连接到主板,静静地躺在机箱里,实时显示你最关心的那些硬件数据。听起来有点复杂?别担心,我本身也不是科班出身的工程师或程序员,只是一个喜欢折腾电烙铁和代码的爱好者。这个项目从硬件选型、电路连接,到Arduino编程、Windows端服务程序编写,每一步我都会拆开揉碎了讲清楚。你会发现,只要跟着步骤走,把各个模块像拼乐高一样组合起来,最终实现一个稳定、美观且完全个性化的硬件监控器,并没有想象中那么难。
整个系统的工作流程可以概括为:PC端(我们称之为“服务端”)负责采集硬件数据,通过USB串口发送给Arduino;Arduino(我们称之为“客户端”)接收数据,并驱动一块3.5英寸的TFT显示屏将信息可视化地呈现出来。下面,我们就从最开始的“为什么这么设计”说起,一步步把它实现出来。
2. 核心硬件选型与设计思路解析
2.1 微控制器:为什么是Arduino Nano?
在嵌入式项目里,微控制器是大脑。选择Arduino Nano,主要是基于以下几点考量:
- 尺寸与兼容性:Nano可以看作是Arduino Uno的迷你版,核心芯片同样是ATmega328P,但体积小巧得多,非常适合塞进机箱内部。其引脚布局与Uno高度兼容,意味着Uno的扩展板(Shield)经过简单飞线也能适配,这为我们使用TFT显示屏扩展板提供了便利。
- 开发环境成熟:Arduino IDE生态极其丰富,有海量的库和教程。对于实现串口通信、驱动显示屏、读取模拟传感器这些功能,几乎都有现成的、经过验证的库可用,能极大降低开发门槛。
- 成本与功耗:Nano价格低廉,且通过USB取电,功耗极低,非常适合作为常驻机箱内部的设备,不会给电源带来额外负担。
注意:市面上有不同版本的Nano(如CH340芯片版、FT232RL芯片版),它们主要区别在于USB转串口芯片。对于本项目,任何版本都可以,但在安装Arduino IDE驱动时需要注意选择对应的芯片驱动。
2.2 显示单元:TFT显示屏的抉择
显示部分是本项目的门面。我排除了字符型LCD和OLED屏,主要因为尺寸和视觉效果。
- 字符LCD:只能显示文字,信息密度低,美观度不足。
- OLED:虽然对比度高,但普遍尺寸较小,在机箱内稍远距离观看可能费力。
- TFT液晶屏:色彩丰富,尺寸选择多。我最终选择了一块3.5英寸、分辨率320x480、驱动芯片为ILI9486/ILI9488L的屏幕。这类屏幕通常以“MCUFRIEND”系列扩展板的形式出售,直接插在Uno上就能用,对我们来说意味着接线极其简单。
关键点:选择屏幕时,务必确认其兼容MCUFRIEND_kbv或Adafruit_GFX库。这两个库是驱动这类TFT屏的基石,兼容性直接决定了你后续编程的难易程度。
2.3 传感器:机箱环境温度的补充
PC内部的硬件温度(CPU、GPU)由软件获取,但我们还可以增加一个物理传感器来监测机箱内部的“环境温度”。我选择了经典的TMP36模拟温度传感器。
- 为什么是模拟传感器?因为它使用简单,只需要一个模拟输入引脚,通过测量输出电压即可换算出温度值,无需复杂的数字通信协议(如I2C、OneWire)。
- TMP36 vs DHT22:DHT22能同时测温度和湿度,精度也更高,但它需要数字信号引脚和特定的时序库来读取。对于仅仅监测机箱温度这个需求来说,TMP36的精度(±2°C)和简易性完全足够,是更“经济”的选择。
2.4 通信桥梁:USB串口
Arduino Nano通过其USB口与PC通信,本质上是USB转串口(UART)通信。这是整个项目的“数据高速公路”。我们将在PC端编写一个程序,定期将采集到的硬件数据打包成特定格式的字符串,通过虚拟串口发送给Nano;Nano则负责解析这些字符串并更新显示。这种方式的优点是稳定、可靠,并且几乎所有操作系统都原生支持,无需安装额外的硬件驱动(Arduino基础驱动除外)。
3. 电路搭建与硬件连接实战
在动手焊接之前,强烈建议在面包板上完成所有连接和测试。这能帮你验证逻辑是否正确,避免因接线错误损坏宝贵的元器件。
3.1 核心连接原理图
整个系统的接线可以看作是两个部分的组合:TFT屏与Arduino的连接以及温度传感器与Arduino的连接。
TFT屏连接(基于MCUFRIEND Shield适配Nano): 大多数3.5寸 TFT Shield是为Arduino Uno设计的,引脚是排母。而Nano是排针。我们需要将Shield的引脚“转换”到Nano上。核心思路是:Shield上除了电源和地,其他数据和控制引脚都对应到Nano的特定数字引脚。通常,这类Shield会占用数字引脚D2到D9,以及部分模拟引脚作为控制信号。你需要根据你购买的屏幕的具体引脚定义图来连接。一个常见的映射关系(可能需要根据屏幕测试调整)是:
- TFT
RD-> NanoD7 - TFT
WR-> NanoD6 - TFT
RS-> NanoD5 - TFT
CS-> NanoD4 - TFT
RST-> NanoD3 - TFT
D0~D7-> NanoD8,D9,D2,D3,A0,A1,A2,A3(这是一个示例,务必以你屏幕的资料为准)
实操心得:我采用了一个取巧的方法。将Shield下排的电源引脚(VCC, GND)和部分数据引脚焊接在一块万用板上,同时把Arduino Nano也焊在这块板上。对于上排不方便焊接的引脚,则使用杜邦线(母对母)进行连接。这样既保证了主要连接的稳固,又保留了灵活性以便调试。
TMP36温度传感器连接: 连接非常简单,只有三根线:
- VCC:连接至Arduino Nano的
5V引脚。 - GND:连接至Arduino Nano的
GND引脚。 - OUT:连接至Arduino Nano的
A5模拟输入引脚。 为了稳定读数,建议在VCC和GND之间并联一个0.1uF的陶瓷电容,紧挨着传感器引脚焊接,以滤除电源噪声。
3.2 供电与参考电压设置
供电:整个系统通过Nano的USB口取电。你可以使用一根废弃的USB数据线,剪断后只保留USB-A公头(插主板的那端)和四根线(红-VCC,黑-GND,白-D-,绿-D+),将红黑线焊接到Nano的VIN和GND(注意,不是5V引脚),这样就能利用主板内部的USB接口供电,无需占用机箱后部的接口。
模拟参考电压:为了更精确地读取TMP36的模拟值,我们使用了外部参考电压。将Nano的3.3V引脚连接到AREF引脚,并在代码中设置analogReference(EXTERNAL)。这样,模拟数字转换器(ADC)将以3.3V作为满量程基准,而不是默认的5V,可以提高在较低电压范围内的测量分辨率。
重要检查:焊接完成后,务必用万用表测量一下Nano上
3.3V引脚的实际输出电��(最好在连接PC并运行后测量)。因为不同批次、不同厂商的LDO稳压芯片输出可能有细微差异。假设你测出来是3.28V,那么在计算温度时,就应该以3.28V作为参考电压,而不是理想的3.3V。这个值将直接写入Arduino代码的温度换算公式中。
3.3 最终组装与走线
将所有元件紧凑地布局在万用板上,使用尼龙柱或热熔胶固定。连接主板的USB线最好选择较细的线材,并从主板背部走线,保持机箱内部整洁。确保所有焊接点牢固,无短路风险。完成后,可以先不装入机箱,连接PC进行下一步的软件测试。
4. Arduino端程序深度剖析与编写
Arduino程序是整个系统的“显示与通信中枢”。它的任务很明确:初始化屏幕、与PC握手、接收数据、解析数据、读取传感器、更新显示。
4.1 库文件准备与全局定义
首先,在Arduino IDE中安装两个核心库:MCUFRIEND_kbv和Adafruit_GFX。前者是驱动,后者提供了丰富的图形绘制函数。
程序开头,我们需要定义一系列全局变量和数组来存储数据、管理状态。
#include <MCUFRIEND_kbv.h> #include <Adafruit_GFX.h> MCUFRIEND_kbv tft; // 定义颜色(RGB565格式) #define BACKGROUND 0x0000 // 黑色 #define TEXT_COLOR 0xFFFF // 白色 #define HIGHLIGHT_COLOR 0xF800 // 红色,用于高亮数据 // 数据存储数组 String allData[3][4]; // 假设我们接收3类数据,每类最多4个值 // allData[0][x] 存储硬件名称 // allData[1][x] 存储左侧数据(CPU温度、负载等) // allData[2][x] 存储右侧数据(GPU温度、负载等) boolean printNames = false; // 标志位,是否需要打印硬件名称 boolean connected = false; // 标志位,是否已与PC连接 // 串口通信相关 String inputString = ""; // 存储接收到的字符串 boolean stringComplete = false; // 标志位,是否收到完整数据包(以';'结尾)4.2 通信协议设计:简洁至上
一个清晰可靠的通信协议是双向通信的基石。我设计了一个基于字符串的简单协议,格式如下:i:value1,value2,value3,value4;
i: 数据索引(componentSelector),0代表硬件名称,1代表左侧数据,2代表右侧数据,3代表执行打印命令。:: 索引与数据的分隔符。value1,value2...: 具体的数据值,用逗号分隔。;: 数据包的结束符。
例如,PC发送0:Intel i7-12700K,NVIDIA RTX 3080,ASUS ROG,;来更新硬件名称。发送1:45,78,32,38;来更新CPU温度(45°C)、负载(78%)等信息。
握手流程:PC程序启动后,会遍历所有串口,发送握手字符串*****;。Arduino一旦收到这个字符串,立即回复一个字符R。PC收到R后,便认定连接成功,开始发送数据。这实现了“即插即用”的自动连接功能。
4.3 核心函数详解
setup()函数:这里完成一次性初始化。
- 初始化串口:
Serial.begin(9600);。波特率9600足够稳定,且兼容性好。 - 初始化显示屏:
tft.begin();并设置旋转方向tft.setRotation(1);(根据屏幕实际安装方向调整)。 - 设置模拟参考电压:
analogReference(EXTERNAL);。 - 绘制静态UI界面:包括背景、分割线、图标、文字标签等。为了追求一点质感,我用了两层绘制(比如画两次矩形,第二次用浅色且偏移一个像素)来模拟简单的阴影效果,虽然简单,但比纯平面好看。
loop()函数:这是程序的主循环,核心是处理串口数据。
void loop() { // 1. 接收串口数据 while (Serial.available()) { char inChar = (char)Serial.read(); inputString += inChar; if (inChar == ';') { // 检测到结束符 stringComplete = true; } } // 2. 解析并处理完整的数据包 if (stringComplete) { parseSerialData(inputString); inputString = ""; stringComplete = false; } // 3. 如果收到打印命令(索引3),则读取传感器并更新显示 // 这部分逻辑在 parseSerialData 函数中触发 }parseSerialData(String data)函数:这是协议解析的核心。
void parseSerialData(String data) { int colonIndex = data.indexOf(':'); if (colonIndex == -1) return; // 格式错误,丢弃 String selectorStr = data.substring(0, colonIndex); int selector = selectorStr.toInt(); String valuesStr = data.substring(colonIndex + 1, data.length() - 1); // 去掉末尾的';' switch (selector) { case 0: // 硬件名称 // 解析valuesStr,用逗号分割,存入allData[0][] printNames = true; // 设置标志,下次打印数据时更新名称 break; case 1: // 左侧数据 // 解析并存入allData[1][] break; case 2: // 右侧数据 // 解析并存入allData[2][] break; case 3: // 执行打印命令 readExtTemp(); // 读取外部温度传感器 printData(); // 更新显示屏 break; } }readExtTemp()函数:读取TMP36。
void readExtTemp() { int sensorValue = 0; for (int i = 0; i < 32; i++) { // 读取32次取平均,滤波 sensorValue += analogRead(A5); delay(1); } sensorValue /= 32; // 将模拟值转换为电压(参考电压为实测的3.28V) float voltage = sensorValue * (3.28 / 1023.0); // TMP36输出电压与温度关系:10mV/°C,0°C时输出500mV float tempC = (voltage - 0.5) * 100.0; // 格式化温度值,存入用于显示的数据变量 if (tempC > 100.0) { extTempStr = "---"; } else { extTempStr = String((int)tempC); // 补足3位字符,保持显示对齐 while (extTempStr.length() < 3) extTempStr = " " + extTempStr; } }printData()函数:负责将allData数组和外部温度数据绘制到屏幕上。这里大量使用了Adafruit_GFX库的setCursor()和print()函数。为了减少屏幕闪烁,只在数据真正发生变化时才更新对应的区域。
4.4 图标与字体的处理
为了追求速度和节省内存,我没有将图标存放在SD卡中,而是直接将位图数据以字节数组的形式存储在程序的PROGMEM(程序存储器)中。我使用了一个叫Junior Icon Editor的免费软件来绘制16x16像素的黑白图标,然后通过在线工具(如SKAARHOJ)将PNG图片转换为Arduino可用的C语言数组格式。这样,在绘制图标时,直接调用tft.drawBitmap()函数即可,速度极快。
5. PC端服务程序(Visual Basic)开发指南
PC端的任务是采集硬件数据,并按照协议发送给Arduino。我选择用Visual Basic (VB.NET) 来写,主要是因为上手快,能快速构建带系统托盘图标的GUI程序。
5.1 开发环境与依赖库
- 安装Visual Studio:下载免费的Visual Studio Community版,安装时选择“.NET桌面开发”工作负载,其中包含VB.NET。
- 获取核心库:
- OpenHardwareMonitorLib.dll: 从OpenHardwareMonitor官网下载其软件包,解压后找到这个DLL文件。它是我们获取CPU、主板温度、风扇转速等信息的核心。
- RTSSSharedMemoryNET.dll: 从Github或相关项目页面获取这个库的源代码,在Visual Studio中编译生成DLL文件。它用于从RivaTuner Statistics Server (RTSS) 中读取游戏帧率(FPS)。
- 添加引用:在VB.NET项目中,右键点击“引用” -> “添加引用” -> “浏览”,将上述两个DLL文件添加进来。
5.2 程序架构与主逻辑
程序主要是一个Windows Forms应用,但主界面最小化到系统托盘。核心逻辑由一个Timer控件驱动,每隔设定的时间(如2秒)执行一次。
' 在Form Load事件或启动时初始化 Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load ' 1. 初始化OpenHardwareMonitor实例 computer = New Computer With {.CPUEnabled = True, .GPUEnabled = True, .MainboardEnabled = True, .RAMEnabled = True} computer.Open() ' 2. 初始化RTSS共享内存读取 ' ... 初始化代码 ' 3. 初始化串口,尝试自动连接 AutoConnect() ' 4. 启动定时器 Timer1.Interval = 2000 ' 2秒 Timer1.Start() End Sub ' 定时器Tick事件:采集并发送数据 Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick If serialPort.IsOpen Then ' 1. 采集数据 Dim cpuTemp As String = GetCPUTemperature() Dim gpuTemp As String = GetGPUTemperature() Dim fps As String = GetFPS() ' ... 获取其他数据 ' 2. 格式化数据为协议字符串 Dim dataString As String = FormatData(cpuTemp, gpuTemp, fps, ...) ' 3. 通过串口发送 serialPort.WriteLine(dataString) End If End Sub数据采集难点:主板温度传感器识别OpenHardwareMonitor有时会将主板上的传感器简单命名为“Temperature #1”、“#2”等,你无法知道哪个对应CPU插座,哪个对应主板芯片组。我的解决办法是:
- 同时运行OpenHardwareMonitor和主板官方软件(如华硕AI Suite)。
- 对比两者显示的温度值,找到在OpenHardwareMonitor中编号与官方软件中“主板温度”或“System Temperature”对应的那个传感器。
- 在代码中,遍历所有传感器,寻找名称包含特定编号(如“#2”)的传感器,然后读取其值。 这个过程需要一些耐心和尝试,但一旦找到,代码就固定了。
5.3 实现“开机自启”与“自动连接”
开机自启:由于程序需要以管理员权限运行(为了访问硬件传感器),直接创建启动文件夹快捷方式会触发UAC弹窗。我采用了创建Windows计划任务的方法。通过VB.NET代码,动态生成一个XML任务定义文件,并使用schtasks.exe命令来创建/删除一个在用户登录时触发、以最高权限运行的计划任务。这样就能实现静默开机启动。
自动连接:在AutoConnect()函数中,程序遍历当前系统所有可用的COM端口(从COM1到COM20),尝试打开端口,发送握手信号*****;,然后等待片刻看是否收到R回复。如果收到,则保持该端口连接;如果超时或出错,则关闭端口,尝试下一个。这个过程被封装在Try...Catch块中,以优雅地处理端口被占用或无设备等情况。
5.4 系统托盘图标与用户交互
使用NotifyIcon和ContextMenuStrip控件可以轻松创建托盘图标和右键菜单。菜单项包括:
- “开机启动” 复选框
- “自动连接” 复选框
- “手动连接/断开” 按钮
- “刷新频率” 子菜单(1秒到10秒)
- “显示窗口”
- “退出”
所有设置(如刷新频率、是否自启)可以保存到My.Settings或一个配置文件中,实现程序状态的持久化。
6. 系统集成、调试与问题排查
6.1 完整组装与上电测试
将焊接好的模块装入机箱合适位置(如硬盘仓侧面、前面板后方)。连接USB线到主板内部的USB 2.0插针。首次上电前,务必再次检查所有接线,特别是电源正负极。
上电顺序:
- 先打开PC电源。
- 观察Arduino Nano上的电源指示灯是否亮起,TFT屏幕是否背光亮起并显示初始界面(可能是白屏或库自带的测试图案)。
- 在Windows设备管理器中查看“端口(COM和LPT)”,确认是否识别出一个新的USB串行设备(如COM5)。记下这个端口号。
6.2 分步调试与验证
- Arduino独立测试:上传一个最简单的TFT屏测试程序(如显示“Hello World”),确保屏幕驱动和接线正确。
- 串口通信测试:在Arduino IDE的串口监视器中,手动发送握手字符串
*****;,查看是否收到R回复。然后尝试发送一条数据协议,如1:50,20,80,30;,观察屏幕对应位置是否更新。 - PC端程序测试:先不连接Arduino,运行VB程序。检查系统托盘图标是否出现。打开程序主窗口(如果有调试信息输出),查看是否能正常获取到CPU温度、GPU温度等数据。
- 联合调试:连接Arduino,在VB程序中选择正确的COM端口(或启用自动连接),点击连接。观察数据是否开始发送,以及屏幕是否实时更新。
6.3 常见问题与解决方案速查表
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 屏幕无显示或白屏 | 1. 电源未接通或接反。 2. 背光控制线未接或接错。 3. 复位引脚未正确连接或电平不对。 4. 屏幕驱动库不匹配或初始化失败。 | 1. 用万用表检查Nano和屏幕的VCC/GND电压。 2. 查阅屏幕资料,确认背光引脚(LED+, LED-)是否需接限流电阻并正确连接。 3. 检查RST引脚连接,尝试在代码初始化前手动拉低再拉高复位。 4. 在Arduino代码中检查 tft.begin()返回值,或尝试MCUFRIEND_kbv库中的识别示例。 |
| PC程序无法获取硬件数据 | 1. OpenHardwareMonitor库未正确引用或版本不兼容。 2. 程序未以管理员权限运行。 3. 硬件传感器在OHM中未启用或识别不到。 | 1. 确认DLL文件路径正确,且与项目目标平台(x86/x64)一致。 2. 右键以管理员身份运行程序。 3. 单独运行OpenHardwareMonitor官方软件,确认能读到数据。在VB代码中检查 computer.Open()是否成功。 |
| 串口连接失败 | 1. COM端口号错误。 2. 波特率不匹配。 3. USB线仅供电无数据线。 4. 端口被其他程序占用。 | 1. 在设备管理器中确认Arduino使用的COM口,并在VB程序中设置一致。 2. 确保Arduino代码 Serial.begin(9600)与VB程序serialPort.BaudRate = 9600一致。3. 使用完好的USB数据线。 4. 关闭Arduino IDE的串口监视器或其他可能占用端口的软件。 |
| 数据显示错乱或不全 | 1. 通信协议解析错误。 2. 数据格式(如字符串长度)超出预期。 3. 屏幕坐标计算错误。 | 1. 在VB端和Arduino端添加调试输出,打印发送和接收的原始字符串,对比是否一致。 2. 确保发送的数据(如温度值)长度不超过Arduino端预留的字符数(如3位)。 3. 检查 printData()函数中setCursor()的坐标值,确保每个数据项显示在正确位置。 |
| 外部温度读数不准 | 1. 模拟参考电压设置不准确。 2. TMP36传感器离热源(如CPU散热器)太近。 3. 未进行软件滤波。 | 1. 用万用表精确测量AREF引脚电压,并更新代码中的参考电压值。 2. 将传感器放置在机箱内气流畅通、能代表环境温度的位置。 3. 确保使用了多次读取取平均值的滤波算法。 |
| 程序开机不自启 | 1. 计划任务创建失败。 2. 任务路径或参数错误。 3. 系统组策略限制。 | 1. 以管理员身份运行程序,再次勾选“开机启动”。检查Windows“任务计划程序”库中是否存在该任务。 2. 检查VB代码中生成任务XML文件时,程序路径 %ProgramPath%变量是否正确替换为实际路径。3. 对于某些企业或教育版Windows,可能需要修改本地组策略。 |
6.4 性能优化与扩展思路
- 刷新率:根据个人需求调整VB程序中定时器的间隔。1秒刷新很实时,但可能增加系统负载。2-3秒对于温度监控来说通常足够平滑。
- 降低Arduino功耗:如果屏幕一直常亮,可以考虑让Arduino在检测到PC进入睡眠或关机(USB断电)后,自动关闭屏幕背光。这需要额外电路检测USB VBUS电压。
- 增加更多传感器:Arduino Nano还有多余的模拟引脚,可以连接更多的TMP36来监测不同区域的温度(如进风口、出风口、硬盘温度)。只需在协议中增加数据字段即可。
- 美化UI:
Adafruit_GFX库支持绘制圆形、三角形、位图等。可以设计更炫酷的仪表盘、曲线图来展示数据。 - 改用Wi-Fi或蓝牙:如果想摆脱USB线,可以使用ESP8266或ESP32模块,通过Wi-Fi将数据发送给PC,甚至可以直接从路由器获取数据,实现无线监控。
这个项目最让我满意的,不是最终那个显示着数字的小屏幕,而是从需求定义、硬件选型、电路焊接、协议设计、两端编程到最终调试的完整过程。它完美地诠释了“DIY”的精神:用现有的、触手可及的技术模块,通过自己的思考和动手,解决一个真实存在的、个性化的问题。当你第一次看到自己机箱里的屏幕亮起,并实时显示出那些关键的硬件信息时,那种成就感是购买任何成品都无法替代的。希望这篇详细的分享,能为你点亮自己动手的那盏灯。如果在复现过程中遇到任何问题,不妨回到“常见问题”部分对照排查,或者放慢脚步,将系统拆分成更小的模块逐一验证。