EspMon:使用 T-Display S3 的简单 PC 硬件监视器






4.90/5 (13投票s)
用这个小项目监视您的 CPU 和 GPU 活动。
引言
我玩《辐射 4》。这是一款可无限扩展的游戏,所以即使玩了 7 年,我仍然会回来玩。我为游戏添加内容并修改它。
好吧,最终我买了 2080ti,这样我就可以玩 4K 了,它也轻松胜任,所以我决定通过在整个景观中添加茂密的森林来对其施加一些高强度的计算。最后,我开始听到显卡风扇转动,甚至有点“冒汗”。
我想要的不仅仅是一个听得见的提示,而且我真的很不喜欢屏幕上出现遮挡画面的覆盖层,所以我用我那个小小的 T-Display S3 做了这个。
它通过 PC 上的配套应用程序从串行端口读取 PC 的数据。您可以使用上面的按钮在使用率/温度和频率之间切换。
必备组件
此项目假设您拥有 T-Display S3,或者愿意将其代码改编到单独的设备上。
它假定使用 PlatformIO,尽管您也可以使用 Arduino IDE 进行一些准备工作和调整,主要是重命名和/或移动文件。
它假定您正在运行 Windows 10 或 11,并且在该系统上拥有管理员权限。
它假定您正在使用 Visual Studio 2019 或更高版本以及 .NET Framework。
理解这段乱码
这里涉及两个项目:T-Display 固件和 PC 应用程序。
PC 应用程序使用 Open Hardware Monitor 以每十分之一秒的频率收集 CPU 和 GPU 的信息,并使用串行端口定期将该信息中继到连接的 T-Display,由 T-Display 请求。
T-Display 使用 LVGL 来处理图形。它会定期通过串行端口请求数据,然后最多每十分之一秒更新一次显示。
编写这个混乱的程序
PC 应用程序
PC 应用程序是一个单窗体,带有几个控件用于选择 COM 端口。该应用程序在这方面目前不够健壮,可以改进,但足以证明并说明这个概念。一旦选择了 COM 端口,它就会开始监听 T-Display。
与此同时,每十分之一秒,它都会更新包含 PC 硬件各种统计信息的成员变量。
最初,代码通过挂钩 SerialPort.DataReceived
来监听串行端口传入的 '#' 或 '@' 字符。当它找到 '#' 时,它会发送包含 CPU 使用率和温度、GPU 使用率和温度的四个浮点数。如果它找到 '@',它会发送 CPU 和 GPU 的频率。如果它找到其他内容,它只会读取所有等待的数据。这样做的原因是在 T-Display 启动时,它会向串行端口发送无关的乱码 -basically一个消息。
这是 PC 端串行通信的核心部分。
private void _port_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
if (_port!=null && _port.IsOpen)
{
var cha = new byte[1];
if (_port.BytesToRead != 1)
{
var ba = new byte[_port.BytesToRead];
_port.Read(ba, 0, ba.Length);
if (Created && !Disposing)
{
Invoke(new Action(() =>
{
Log.AppendText(Encoding.ASCII.GetString(ba));
}));
}
}
else
{
_port.Read(cha, 0, cha.Length);
if ((char)cha[0] == '#')
{
var ba = BitConverter.GetBytes(cpuUsage);
if(!BitConverter.IsLittleEndian)
{
Array.Reverse(ba);
}
_port.Write(ba, 0, ba.Length);
ba = BitConverter.GetBytes(cpuTemp);
if (!BitConverter.IsLittleEndian)
{
Array.Reverse(ba);
}
_port.Write(ba, 0, ba.Length);
ba = BitConverter.GetBytes(gpuUsage);
if (!BitConverter.IsLittleEndian)
{
Array.Reverse(ba);
}
_port.Write(ba, 0, ba.Length);
ba = BitConverter.GetBytes(gpuTemp);
if (!BitConverter.IsLittleEndian)
{
Array.Reverse(ba);
}
_port.Write(ba, 0, ba.Length);
_port.BaseStream.Flush();
} else if((char)cha[0]=='@')
{
var ba = BitConverter.GetBytes(cpuSpeed);
if (!BitConverter.IsLittleEndian)
{
Array.Reverse(ba);
}
_port.Write(ba, 0, ba.Length);
ba = BitConverter.GetBytes(gpuSpeed);
if (!BitConverter.IsLittleEndian)
{
Array.Reverse(ba);
}
_port.Write(ba, 0, ba.Length);
}
}
}
}
您可以看到,除了我们已经涵盖的内容之外,它还可以处理大端序系统。由于这是一个传统的 Windows .NET Framework 应用程序,这并非必需,但现在基本上是我的本能。另外,如果这段代码将来在其他地方被重用,它将能够处理所有情况。
串行通信还有另一个方面,那就是获取 COM 端口列表。列出端口很简单。我们只需枚举它们并将它们添加到组合框中。然而,选择端口会变得有些奇怪。我们所做的是从最后一个可用的 COM 端口开始 - 这可能是最近连接的端口 - 并依次打开它们以查看它们是否可用。如果一个可用,我们就停止。如果不,我们就移动到列表中前面的 COM 端口。
void RefreshPortList()
{
var p = PortCombo.Text;
PortCombo.Items.Clear();
var ports = SerialPort.GetPortNames();
foreach(var port in ports)
{
PortCombo.Items.Add(port);
}
var idx = PortCombo.Items.Count-1;
if(!string.IsNullOrWhiteSpace(p))
{
for(var i = 0; i < PortCombo.Items.Count; ++i)
{
if(p==(string)PortCombo.Items[i])
{
idx = i;
break;
}
}
}
var s = new SerialPort((string)PortCombo.Items[idx]);
if (!s.IsOpen)
{
try
{
s.Open();
s.Close();
}
catch
{
--idx;
if (0 > idx)
{
idx = PortCombo.Items.Count - 1;
}
}
}
PortCombo.SelectedIndex = idx;
}
最后,另一个重要的部分是定期收集硬件信息,我们通过一个计时器来完成。
void CollectSystemInfo()
{
foreach (var hardware in _computer.Hardware)
{
if (hardware.HardwareType == HardwareType.CPU)
{
hardware.Update();
foreach (var sensor in hardware.Sensors)
{
if (sensor.SensorType == SensorType.Temperature &&
sensor.Name.Contains("CPU Package"))
{
cpuTemp = sensor.Value.GetValueOrDefault();
}
else if (sensor.SensorType == SensorType.Load &&
sensor.Name.Contains("CPU Total"))
{
cpuUsage = sensor.Value.GetValueOrDefault();
}
else if (sensor.SensorType == SensorType.Clock &&
sensor.Name.Contains("CPU Core #1"))
{
cpuSpeed = sensor.Value.GetValueOrDefault();
}
}
}
if (hardware.HardwareType == HardwareType.GpuAti ||
hardware.HardwareType == HardwareType.GpuNvidia)
{
hardware.Update();
foreach (var sensor in hardware.Sensors)
{
if (sensor.SensorType == SensorType.Temperature &&
sensor.Name.Contains("GPU Core"))
{
gpuTemp = sensor.Value.GetValueOrDefault();
}
else if (sensor.SensorType == SensorType.Load &&
sensor.Name.Contains("GPU Core"))
{
gpuUsage = sensor.Value.GetValueOrDefault();
}
else if (sensor.SensorType == SensorType.Clock &&
sensor.Name.Contains("GPU Core"))
{
gpuSpeed = sensor.Value.GetValueOrDefault();
}
}
}
}
}
这就是所有相关的应用程序代码。其余的只是样板代码。
T-Display S3 固件
注意:在固件构建之前,您必须将 /include/lv_conf.h 复制到项目的 .pio 的 libdeps 子文件夹中,否则它将无法构建。
设置大部分是样板代码,几乎适用于所有应用程序。基本上,我们所做的就是设置显示驱动程序,将其连接到 LVGL,初始化显示器,并激活 USB 串行桥。
void setup() {
pinMode(PIN_POWER_ON, OUTPUT);
digitalWrite(PIN_POWER_ON, HIGH);
Serial.begin(115200);
pinMode(PIN_LCD_RD, OUTPUT);
digitalWrite(PIN_LCD_RD, HIGH);
esp_lcd_i80_bus_handle_t i80_bus = NULL;
esp_lcd_i80_bus_config_t bus_config = {
.dc_gpio_num = PIN_LCD_DC,
.wr_gpio_num = PIN_LCD_WR,
.clk_src = LCD_CLK_SRC_PLL160M,
.data_gpio_nums =
{
PIN_LCD_D0,
PIN_LCD_D1,
PIN_LCD_D2,
PIN_LCD_D3,
PIN_LCD_D4,
PIN_LCD_D5,
PIN_LCD_D6,
PIN_LCD_D7,
},
.bus_width = 8,
.max_transfer_bytes = LVGL_LCD_BUF_SIZE * sizeof(uint16_t),
};
esp_lcd_new_i80_bus(&bus_config, &i80_bus);
esp_lcd_panel_io_i80_config_t io_config = {
.cs_gpio_num = PIN_LCD_CS,
.pclk_hz = EXAMPLE_LCD_PIXEL_CLOCK_HZ,
.trans_queue_depth = 20,
.on_color_trans_done = notify_lvgl_flush_ready,
.user_ctx = &disp_drv,
.lcd_cmd_bits = 8,
.lcd_param_bits = 8,
.dc_levels =
{
.dc_idle_level = 0,
.dc_cmd_level = 0,
.dc_dummy_level = 0,
.dc_data_level = 1,
},
};
ESP_ERROR_CHECK(esp_lcd_new_panel_io_i80(i80_bus, &io_config, &io_handle));
esp_lcd_panel_handle_t panel_handle = NULL;
esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = PIN_LCD_RES,
.color_space = ESP_LCD_COLOR_SPACE_RGB,
.bits_per_pixel = 16,
};
esp_lcd_new_panel_st7789(io_handle, &panel_config, &panel_handle);
esp_lcd_panel_reset(panel_handle);
esp_lcd_panel_init(panel_handle);
esp_lcd_panel_invert_color(panel_handle, true);
esp_lcd_panel_swap_xy(panel_handle, true);
esp_lcd_panel_mirror(panel_handle, false, true);
// the gap is LCD panel specific, even panels with the same driver IC, can
// have different gap value
esp_lcd_panel_set_gap(panel_handle, 0, 35);
/* Lighten the screen with gradient */
ledcSetup(0, 10000, 8);
ledcAttachPin(PIN_LCD_BL, 0);
for (uint8_t i = 0; i < 0xFF; i++) {
ledcWrite(0, i);
delay(2);
}
lv_init();
lv_disp_buf = (lv_color_t *)heap_caps_malloc
(LVGL_LCD_BUF_SIZE * sizeof(lv_color_t), MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);
lv_disp_buf2 = (lv_color_t *)heap_caps_malloc
(LVGL_LCD_BUF_SIZE * sizeof(lv_color_t), MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);
lv_disp_draw_buf_init(&disp_buf, lv_disp_buf, lv_disp_buf2, LVGL_LCD_BUF_SIZE);
/*Initialize the display*/
lv_disp_drv_init(&disp_drv);
/*Change the following line to your display resolution*/
disp_drv.hor_res = LCD_H_RES;
disp_drv.ver_res = LCD_V_RES;
disp_drv.flush_cb = lvgl_flush_cb;
disp_drv.draw_buf = &disp_buf;
disp_drv.user_data = panel_handle;
lv_disp_drv_register(&disp_drv);
is_initialized_lvgl = true;
ui_init();
ui_patch();
lv_canvas_set_buffer(ui_CpuGraph, cpu_graph_buf, 135, 37, LV_IMG_CF_TRUE_COLOR);
lv_canvas_set_buffer(ui_GpuGraph, gpu_graph_buf, 135, 37, LV_IMG_CF_TRUE_COLOR);
lv_canvas_set_buffer(ui_CpuGhzGraph, cpu_graph_buf, 135, 37, LV_IMG_CF_TRUE_COLOR);
lv_canvas_set_buffer(ui_GpuGhzGraph, gpu_graph_buf, 135, 37, LV_IMG_CF_TRUE_COLOR);
button_prev.callback(button_prev_cb);
button_next.callback(button_next_cb);
USBSerial.begin(115200);
}
以上的一个奇怪之处在于,我们为两个屏幕的画布使用了相同的缓冲区。这是因为我们一次只需要一个,所以我们可以重复使用内存。
我们真正开始执行是在 loop()
中。
static int ticker = 0;
void loop() {
button_prev.update();
button_next.update();
if (ticker++ >= 33) {
ticker = 0;
switch (screen) {
case 0:
update_screen_0();
break;
case 1:
update_screen_1();
break;
}
}
lv_timer_handler();
delay(3);
}
首先,我们让按钮有机会被触发。然后大约每十分之一秒,我们更新当前屏幕。最后,我们让 LVGL 执行其操作。
最后是屏幕更新。基本上,对于第一个屏幕,我们只是在有串行数据等待时读取使用率和温度值。使用这些数据,我们更新 CPU 和 GPU 条形图,将数据添加到 cpu_graph
和 gpu_graph
缓冲区。如果任何一个缓冲区已满,我们就移除最旧的项目。如果我们 K需要重绘其中任何一个,我们就会构建一个 LVGL 可用的线条路径,使用缩放值,然后进行绘制。
对于第二个屏幕,尽管我们只读取 CPU 和 GPU 频率,但事情会变得更复杂一些。原因是由于我们不知道您的 CPU 和 GPU 速度的上限和下限,因此没有有效的可用值范围。
鉴于上述情况,代码会跟踪看到的最小值和最大值,并将其用作我们的有效范围。代码还确保值被归零到最小值。这两个例程非常相似,所以我们只看一下第二个屏幕的代码。
static float screen_1_cpu_min=NAN,screen_1_cpu_max=NAN;
static float screen_1_gpu_min=NAN,screen_1_gpu_max=NAN;
static void update_screen_1() {
float tmp;
float v;
bool redraw_cpu, redraw_gpu;
float cpu_scale, gpu_scale;
char sz[64];
union {
float f;
uint8_t b[4];
} fbu;
redraw_cpu = false;
redraw_gpu = false;
if (USBSerial.available()) {
int i = USBSerial.readBytes(fbu.b, sizeof(fbu.b));
if (i == 0) {
USBSerial.write('@');
} else {
if (cpu_graph.full()) {
cpu_graph.get(&tmp);
}
v = (fbu.f);
cpu_graph.put(v);
if(screen_1_cpu_min!=screen_1_cpu_min||v<screen_1_cpu_min) {
screen_1_cpu_min = v;
}
if(screen_1_cpu_max!=screen_1_cpu_max||v>screen_1_cpu_max) {
screen_1_cpu_max = v;
}
cpu_scale = screen_1_cpu_max-screen_1_cpu_min+1;
float offs = - (screen_1_cpu_min/cpu_scale);
redraw_cpu = true;
lv_bar_set_value(ui_CpuGhzBar, ((v/cpu_scale)+offs)*100, LV_ANIM_ON);
snprintf(sz, sizeof(sz), "%0.1fGHz", fbu.f/1000.0);
lv_label_set_text(ui_CpuGhzLabel, sz);
if (USBSerial.available()) {
i = USBSerial.readBytes(fbu.b, sizeof(fbu.b));
if (i != 0) {
if (gpu_graph.full()) {
gpu_graph.get(&tmp);
}
v = (fbu.f);
gpu_graph.put(v);
if(screen_1_gpu_min!=screen_1_gpu_min||v<screen_1_gpu_min) {
screen_1_gpu_min = v;
}
if(screen_1_gpu_max!=screen_1_gpu_max||v>screen_1_gpu_max) {
screen_1_gpu_max = v;
}
gpu_scale = screen_1_gpu_max-screen_1_gpu_min+1;
offs = - (screen_1_gpu_min/gpu_scale);
redraw_gpu = true;
lv_bar_set_value(ui_GpuGhzBar, ((v/gpu_scale)+offs)*100, LV_ANIM_ON);
snprintf(sz, sizeof(sz), "%0.1fGHz", fbu.f/1000.0);
lv_label_set_text(ui_GpuGhzLabel, sz);
} else {
USBSerial.write('@');
}
} else {
USBSerial.write('@');
}
}
} else {
USBSerial.write('@');
}
if (redraw_cpu) {
float offs = - (screen_1_cpu_min/cpu_scale);
lv_point_t pts[sizeof(cpu_graph)];
lv_draw_line_dsc_t dsc;
lv_draw_line_dsc_init(&dsc);
dsc.width = 1;
dsc.color = lv_color_hex(0x0000FF);
dsc.opa = LV_OPA_100;
lv_canvas_fill_bg(ui_CpuGhzGraph, lv_color_white(), LV_OPA_100);
v = *cpu_graph.peek(0);
pts[0].x = 0;
pts[0].y = 36 - ((v/cpu_scale)+offs) * 36;
for (size_t i = 1; i < cpu_graph.size(); ++i) {
v = *cpu_graph.peek(i);
pts[i].x = i;
pts[i].y = 36 - ((v/cpu_scale)+offs) * 36;
}
lv_canvas_draw_line(ui_CpuGhzGraph, pts, cpu_graph.size(), &dsc);
}
if (redraw_gpu) {
float offs = - (screen_1_gpu_min/gpu_scale);
lv_point_t pts[sizeof(gpu_graph)];
lv_draw_line_dsc_t dsc;
lv_draw_line_dsc_init(&dsc);
dsc.width = 1;
dsc.color = lv_color_hex(0xFF0000);
dsc.opa = LV_OPA_100;
lv_canvas_fill_bg(ui_GpuGhzGraph, lv_color_white(), LV_OPA_100);
v = *gpu_graph.peek(0);
pts[0].x = 0;
pts[0].y = 36 - ((v/gpu_scale)+offs) * 36;
for (size_t i = 1; i < gpu_graph.size(); ++i) {
v = *gpu_graph.peek(i);
pts[i].x = i;
pts[i].y = 36 - ((v/gpu_scale)+offs) * 36;
}
lv_canvas_draw_line(ui_GpuGhzGraph, pts, gpu_graph.size(), &dsc);
}
}
我们还没有介绍 ui.h 中的 UI 本身。原因是这段代码不是我写的,而是由 Squareline Studio 生成的,这是一个可视化设计器,可以生成 LVGL 代码,有点像 Windows Forms 设计器生成 C# 代码。
结论
这是一个很有用的实用工具,但可以轻松地扩展以满足您自己的需求。祝您编码愉快!
历史
- 2022年10月1日 - 初始提交
- 2022年10月2日 - 添加了迷你图
- 2022年10月3日 - 添加了频率屏幕