Всем доброго дня! Мой никнейм Arduinum628, я занимаюсь DIY проектами и программированием на Python и C++. В этой статье пойдёт речь о выводе системной информации с ПК на круглый LCD дисплей GC9A01.
Сама идея проекта мне пришла во время разговора с другом Иваном. Я рассказал ему, что заказал пару LCD дисплей GC9A01 с Ali Express для своих будущих DIY проектов. Во время разговора Иван внезапно сказал, что ему-бы пригодился девайс для вывода системной информации с ПК. Я подумал - почему-бы не научиться использовать этот дисплей на подобном проекте?!
Сам проект я буду писать не для нужд друга, а скорее в целях обучения работы с этим дисплеем. Как я понял, что ему нужно что-то более компактное и встраиваемое в корпус ПК. По моему совету он купил компактную плату esp32 с дисплеем и будет писать своё решение сам. Я же собираюсь делать что-то вроде приборной панели и поставлю её за клавиатурой. Это чем-то будет напоминать спидометр автомобиля =)
Идея проекта
Главная идея проекта — выводить системную информацию на дисплей. Какие параметры можно отображать?
CPU Load — загруженность процессора;
CPU Temp — температура процессора;
RAM Usage — загруженность оперативной памяти;
GPU Load — загруженность видеокарты;
GPU Memory Used — загруженность видеопамяти;
GPU Temp — температура видеокарты;
Disk Usage — загруженность диска.
Шкалы будут подписаны: название параметра у начала шкалы и единица измерения в конце.
Кроме того, шкала будет менять цвет в зависимости от уровня нагрузки (пример):
зелёный — низкая нагрузка (0 - 50%);
жёлтый — средняя нагрузка (51 - 80%);
красный — высокая нагрузка (81 - 100%).
Дисплей будет установлен рядом с клавиатурой и работать как приборная панель. Он подключится к Arduino Uno, которая, в свою очередь, будет соединена с ПК через USB.
Дальше начинается самое интересное — получение данных по USB и их вывод на дисплей. Передача данных будет осуществляться через COM-порт. Данные будут отображаться в виде текста на экране (по крайней мере в первой версии, а дальше посмотрим).
Я хочу, чтобы программа работала на трёх основных платформах: MacOS, Linux и Windows. Поэтому для получения системной информации я буду использовать кроссплатформенные библиотеки, такие как psutil и другие, написанные на Python. Пока первая версия будет написана исключительно для Linux, но со временем я добавлю поддержку и других операционных систем.
Код в этой статье является прототипом и не претендует на идеальную реализацию.
Компоненты
Для прототипа мне понадобятся следующие компоненты:
LSD дисплей 240х240 с чипом GC9A01 - ссылка ;
Резисторы на 150 ом;
Провода дюпонты;
Arduino Uno (в дальнейшем заменю на компактную Arduino Nano);
Макетные плата (без пайки);
Пластик.
Сборка конструкции
Главная идея конструкции заключается в том, что она просто стоит за клавиатурой. Так как это прототип, я не стал заморачиваться с дизайном и красивым корпусом. Просто взял пару уголков от упаковки шкафа и пластик от крышки с влажными салфетками.
Приклеил макетные платы на двусторонний скотч, соединил их и вставил друг в друга, чтобы две половинки конструкции держались вместе.
Из пластика вырезал несколько деталей для крепления экрана и просверлил четыре отверстия под его крепление. В итоге получилась конструкция, которая идеально подходит по высоте клавиатуры.

Экран прикрутил четырьмя винтами к пластиковому креплению. Пластину согнул под углом, чтобы дисплей смотрел вверх, как в панели управления или спидометре.
Провода пришлось укоротить и припаять Dupont-папа вместо Dupont-мама.
Для изоляции использовал тонкую термоусадку. Сами провода аккуратно уложил, прижал их хомутом, а разъёмы вставил по порядку в макетную плату.

Вторая макетная плата пригодится позже. Я собираюсь установить туда Arduino Nano, но пока её нет в наличии, поэтому временно использую Arduino Uno, которая будет лежать рядом.
Подключение к Arduino

Пины дисплея:
VIN — +;
GND — земля;
CS - Chip Select — активация дисплея для обмена данными;
DC - Data/Command — определяет, передаётся ли команда (настройка дисплея) или данные (графика, текст);
RES - Reset — используется для сброса дисплея, помогает корректно перезапустить его;
SDA - MOSI (Master Out Slave In) — передача данных;
SCL - SCK (Serial Clock) — тактовый сигнал SPI, синхронизирует передачу данных.
BLK - подсветка;
Подключение GC9A01 -> Arduino:
VIN -> 3.3V / 5V (в зависимости от модуля)
GND -> GND
CS -> resistor 150om/200om -> D10
DC -> resistor 150om/200om -> D9
RES -> D8 (не будет задействован)
SDA -> resistor 150om/200om -> D11 (MOSI)
SCL -> resistor 150om/200om -> D13 (SCK)
BLK -> 3.3V / PWM (например, D6) (не будет задействован)
Собранный проект у меня за клавиатурой:

Код для Arduino
Для начала я напишу код для Arduino, который будет принимать данные от Python-программы через Serial и выводить их на LCD-дисплей GC9A01A.
spec_pc_to_lcd.ino:
#include <SPI.h>
#include <Adafruit_GC9A01A.h>
// Определение пинов:
#define TFT_CS 10 // Chip Select
#define TFT_DC 9 // Data/Command
#define TFT_RES 8 // Reset
// Создаём объект дисплея:
Adafruit_GC9A01A tft(TFT_CS, TFT_DC, TFT_RES);
const int MAX_NUMBERS = 7; // Максимум чисел в одной строке
int numbers[MAX_NUMBERS]; // Массив для хранения чисел
int numberCount = 0; // Сколько чисел было принято
String inputString = ""; // Буфер ввода
bool inputComplete = false;
void setup() {
Serial.begin(9600); // Скорость передачи данных
tft.begin(); // Инициализация дисплея
tft.fillScreen(GC9A01A_BLACK); // Заливаем экран чёрным цветом
tft.setTextSize(2); // Установка размера шрифта
}
void loop() {
while (Serial.available()) {
// Читаем данные из Serial
char inChar = (char)Serial.read();
if (inChar == '\n') { // Конец строки
inputComplete = true;
break;
} else {
inputString += inChar;
}
}
// Если строка получена полностью
if (inputComplete) {
parseInputString(); // Разбор строки в числа
setCpuLd(numbers[0]); // Установка загрузки CPU
setCpuTp(numbers[1]); // Установка температуры CPU
setRumUs(numbers[2]); // Установка загрузки ОЗУ
setGpuLd(numbers[3]); // Установка загрузки GPU
setGpuMe(numbers[4]); // Установка загрузки видеопамяти GPU
setGpuTp(numbers[5]); // Установка температуры GPU
setDscUs(numbers[6]); // Установка загрузки диска
// Сброс буфера
inputString = "";
inputComplete = false;
}
}
// Функция для получения цвета текста в зависимости от уровня загрузки (CPU, RAM, GPU, диск)
uint16_t getRateColorText(int lvl) {
uint16_t color;
if (lvl <= 50) {
color = GC9A01A_GREEN; // Низкая загрузка - зелёный
} else if (lvl <= 80) {
color = GC9A01A_YELLOW; // Средняя загрузка - жёлтый
} else {
color = GC9A01A_RED; // Высокая загрузка - красный
}
return color;
}
// Функция для получения цвета текста в зависимости от температуры CPU
uint16_t getCpuTpColorText(int lvl) {
uint16_t color;
if (lvl <= 65) {
color = GC9A01A_GREEN; // Низкая температура - зелёный
} else if (lvl <= 85) {
color = GC9A01A_YELLOW; // Средняя температура - жёлтый
} else {
color = GC9A01A_RED; // Высокая температура - красный
}
return color;
}
// Функция для получения цвета текста в зависимости от температуры GPU
uint16_t getGpuTpColorText(int lvl) {
uint16_t color;
if (lvl <= 70) {
color = GC9A01A_GREEN; // Низкая температура - зелёный
} else if (lvl <= 85) {
color = GC9A01A_YELLOW; // Средняя температура - жёлтый
} else {
color = GC9A01A_RED; // Высокая температура - красный
}
return color;
}
// Функция устанавливает строку загруженности процессора
void setCpuLd(int lvl) {
tft.setCursor(50, 50);
tft.setTextColor(getRateColorText(lvl));
tft.println("CPU ld: " + String(lvl) + "%");
}
// Функция устанавливает строку температуры процессора
void setCpuTp(int lvl) {
tft.setCursor(50, 75);
tft.setTextColor(getCpuTpColorText(lvl));
tft.println("CPU tp: " + String(lvl) + "C");
}
// Функция устанавливает строку загруженности оперативной памяти
void setRumUs(int lvl) {
tft.setCursor(50, 95);
tft.setTextColor(getRateColorText(lvl));
tft.println("RAM us: " + String(lvl) + "%");
}
// Функция устанавливает строку загруженности видеокарты
void setGpuLd(int lvl) {
tft.setCursor(50, 115);
tft.setTextColor(getRateColorText(lvl));
tft.println("GPU ld: " + String(lvl) + "%");
}
// Функция устанавливает строку загруженности видеопамяти видеокарты
void setGpuMe(int lvl) {
tft.setCursor(50, 135);
tft.setTextColor(getRateColorText(lvl));
tft.println("GPU me: " + String(lvl) + "%");
}
// Функция устанавливает строку температуры видеокарты
void setGpuTp(int lvl) {
tft.setCursor(50, 155);
tft.setTextColor(getGpuTpColorText(lvl));
tft.println("GPU tp: " + String(lvl) + "C");
}
// Функция устанавливает строку загруженности жёсткого диска
void setDscUs(int lvl) {
tft.setCursor(50, 175);
tft.setTextColor(getRateColorText(lvl));
tft.println("DSC us: " + String(lvl) + "%");
}
// Функция парсит числа из строки, кладя их в массив
void parseInputString() {
numberCount = 0;
char inputBuffer[100];
inputString.toCharArray(inputBuffer, 100);
char* token = strtok(inputBuffer, " ");
while (token != NULL && numberCount < MAX_NUMBERS) {
numbers[numberCount++] = atoi(token); // Преобразование строки в число
token = strtok(NULL, " ");
}
}
Импорты:
#include <SPI.h>— библиотека для работы с SPI-интерфейсом;#include <Adafruit_GC9A01A.h>— библиотека для управления дисплеем GC9A01A.
Определение пинов и создание объекта дисплея:
#define TFT_CS 10— пин для Chip Select;#define TFT_DC 9— пин для Data/Command;#define TFT_RES 8— пин для Reset;Adafruit_GC9A01A tft(TFT_CS, TFT_DC, TFT_RES);— создание объекта дисплея.
Переменные для хранения данных:
const int MAX_NUMBERS = 7;— максимальное количество чисел в одной строке;int numbers[MAX_NUMBERS];— массив для хранения чисел;int numberCount = 0;— количество принятых чисел;String inputString = "";— строка для хранения входных данных;bool inputComplete = false;— флаг завершения ввода.
setup() — настройка дисплея и последовательного порта:
Serial.begin(9600);— установка скорости передачи данных;tft.begin();— инициализация дисплея;tft.fillScreen(GC9A01A_BLACK);— заливка экрана чёрным цветом;tft.setTextSize(2);— установка размера шрифта.
loop() — основной цикл программы:
Чтение данных из
Serial;Проверка окончания строки (
'\n');Вызов
parseInputString()для обработки входных данных;Вызов функций для отображения значений на экране;
Очистка буфера
inputString.
getRateColorText(int lvl) — цвет для загрузки CPU, RAM, GPU, диска:
Зелёный (
GC9A01A_GREEN) при загрузке до 50%;Жёлтый (
GC9A01A_YELLOW) при загрузке до 80%;Красный (
GC9A01A_RED) при загрузке выше 80%.
getCpuTpColorText(int lvl) — цвет для температуры CPU:
Зелёный (
GC9A01A_GREEN) при температуре до 65°C;Жёлтый (
GC9A01A_YELLOW) при температуре до 85°C;Красный (
GC9A01A_RED) при температуре выше 85°C.
getGpuTpColorText(int lvl) — цвет для температуры GPU:
Зелёный (
GC9A01A_GREEN) при температуре до 70°C;Жёлтый (
GC9A01A_YELLOW) при температуре до 85°C;Красный (
GC9A01A_RED) при температуре выше 85°C.
setCpuLd(int lvl) — загрузка CPU:
Устанавливает курсор на
(50, 50);Устанавливает цвет текста в зависимости от уровня загрузки;
Выводит
CPU ld: X%.
setCpuTp(int lvl) — температура CPU:
Устанавливает курсор на
(50, 75);Устанавливает цвет текста в зависимости от температуры;
Выводит
CPU tp: X°C.
setRumUs(int lvl) — загрузка оперативной памяти:
Устанавливает курсор на
(50, 95);Устанавливает цвет текста в зависимости от уровня загрузки;
Выводит
RAM us: X%.
setGpuLd(int lvl) — загрузка GPU:
Устанавливает курсор на
(50, 115);Устанавливает цвет текста в зависимости от уровня загрузки;
Выводит
GPU ld: X%.
setGpuMe(int lvl) — загрузка видеопамяти GPU:
Устанавливает курсор на
(50, 135);Устанавливает цвет текста в зависимости от уровня загрузки;
Выводит
GPU me: X%.
setGpuTp(int lvl) — температура GPU:
Устанавливает курсор на
(50, 155);Устанавливает цвет текста в зависимости от температуры;
Выводит
GPU tp: X°C.
setDscUs(int lvl) — загрузка диска:
Устанавливает курсор на
(50, 175);Устанавливает цвет текста в зависимости от уровня загрузки;
Выводит
DSC us: X%.
parseInputString() — парсинг входной строки:
Преобразует строку
inputStringв массивnumbers;Использует
strtok()для разделения строки по пробелам;Преобразует каждое значение в
intи сохраняет вnumbers.
После написания кода я проверяю его и загружаю на Arduino. Первая часть программы готова, и теперь нужно написать Python-код, который будет отправлять данные на Arduino.
Код для Python
Основная задача Python кода это подключаться к Arduino по Serial и передавать данные PC на неё. Для получения данных PC я буду пользоваться готовыми библиотеками.
Начну реализацию с конфига, в котором будут храниться настройки для подключения.
В конфигурации нет секретных данных, поэтому я не буду усложнять её чтением .env, pydantic_settings и другими инструментами, предназначенными для работы с конфиденциальными данными. Я создам самый простой конфиг и просто захардкодю в нём необходимые параметры.
PORT = "/dev/ttyACM1" # заменить на свой
SPEED = 9600
TIMEOUT = 1
Разбор кода:
Этот код определяет параметры для последовательного соединения с устройством;
PORT = "/dev/ttyACM1"— задаёт порт, к которому подключено устройство. Нужно заменить на актуальный порт, если он отличается;SPEED = 9600— устанавливает скорость передачи данных в бодах (битах в секунду);TIMEOUT = 1— задаёт тайм-аут в секундах для ожидания ответа от устройства.
Теперь я напишу функцию, которая будет отправлять данные на Arduino по Serial.
get_spec_pc.py:
# Импорт необходимых библиотек
from psutil import cpu_percent, sensors_temperatures, virtual_memory, disk_usage
# Библиотека для работы с видеокартами Nvidia
from pynvml import (
nvmlInit,
nvmlDeviceGetHandleByIndex,
nvmlDeviceGetUtilizationRates,
nvmlDeviceGetTemperature,
NVML_TEMPERATURE_GPU,
nvmlShutdown
)
from serial import Serial
from time import sleep
def send_spec_str_serial(port: str, speed: int, timeout: int) -> None:
"""Функция для отправки строки со спецификацией ПК в Serial"""
while True:
with Serial(port=port, baudrate=speed, timeout=timeout) as ser:
sleep(1) # Даем время на подключение Arduino
# Получаем загрузку CPU в процентах
cpu_load = int(cpu_percent(interval=1)) # Средняя загрузка за 1 секунду
# Получаем температуру CPU
cpu_temp = int(sensors_temperatures().get("coretemp")[0].current)
# Получаем загрузку ОЗУ в процентах
ram_usage = int(virtual_memory().percent)
# Получаем загрузку диска (работает только на Linux)
dsk_usage = int(disk_usage("/").percent)
# Работаем с видеокартой Nvidia
nvmlInit()
handle = nvmlDeviceGetHandleByIndex(0) # 0 — первая видеокарта
gpu_util = nvmlDeviceGetUtilizationRates(handle)
gpu_temp = nvmlDeviceGetTemperature(handle, NVML_TEMPERATURE_GPU)
# Формируем строку данных
data_str = f"{cpu_load} {cpu_temp} {ram_usage} {gpu_util.gpu} {gpu_util.memory} {gpu_temp} {dsk_usage}\n"
ser.write(data_str.encode("ascii"))
# Для проверки (раскомментируйте при необходимости)
# print(f"CPU Load: {cpu_load}%")
# print(f"CPU Temp: {cpu_temp}C")
# print(f"RAM Usage: {ram_usage}%")
# print(f"GPU Load: {gpu_util.gpu}%")
# print(f"GPU Memory Used: {gpu_util.memory}%")
# print(f"GPU Temp: {gpu_temp}C")
# print(f"Disk Usage: {dsk_usage}%")
# print(data_str)
sleep(4)
nvmlShutdown()
if __name__ == "__main__":
from config import PORT, SPEED, TIMEOUT
send_spec_str_serial(port=PORT, speed=SPEED, timeout=TIMEOUT)
Импорты:
from psutil import cpu_percent, sensors_temperatures, virtual_memory, disk_usage— библиотека для мониторинга загрузки процессора, температуры, оперативной памяти и диска;from pynvml import nvmlInit, nvmlDeviceGetHandleByIndex, nvmlDeviceGetUtilizationRates, nvmlDeviceGetTemperature, NVML_TEMPERATURE_GPU, nvmlShutdown— библиотека для работы с видеокартами Nvidia;from serial import Serial— библиотека для работы с последовательным портом;from time import sleep— библиотека для работы с задержками.
Определение переменных
port: str— строка с именем последовательного порта;speed: int— скорость передачи данных;timeout: int— тайм-аут ожидания ответа от устройства.
Функция send_spec_str_serial(port: str, speed: int, timeout: int) -> None:
Запускается бесконечный цикл для отправки данных;
Создаётся соединение с устройством через
Serial;sleep(1)— даёт время на подключение Arduino перед отправкой данных;Получается загрузка процессора с помощью
cpu_percent(interval=1), вычисляется средняя загрузка за одну секунду;Получается температура процессора через
sensors_temperatures().get("coretemp")[0].current;Получается загрузка оперативной памяти через
virtual_memory().percent;Получается загрузка диска с помощью
disk_usage("/"), работает только на Linux;Инициализируется работа с видеокартой Nvidia через
nvmlInit();Получается загрузка видеокарты с помощью
nvmlDeviceGetUtilizationRates(handle);Получается температура видеокарты через
nvmlDeviceGetTemperature(handle, NVML_TEMPERATURE_GPU);Формируется строка данных, содержащая загрузку и температуру CPU, загрузку RAM, загрузку и температуру GPU, а также загрузку диска;
Строка отправляется через
ser.write(data_str.encode("ascii"));sleep(4)— задержка перед следующим циклом отправки данных;Завершается работа с видеокартой Nvidia через
nvmlShutdown().
Блок if __name__ == "__main__"::
Импортируются параметры
PORT,SPEEDиTIMEOUTиз файлаconfig.py;Вызывается функция
send_spec_str_serial(port=PORT, speed=SPEED, timeout=TIMEOUT), которая начинает отправку данных через последовательный порт.
Да простят меня гуру Python, я не обработал ошибки в прототипе. Я понимаю, насколько это важно, и планирую заняться этим в следующих статьях. Сейчас для меня главное — работоспособность проекта, так как я уже немало намучался с работой самого дисплея.
Запуск
Настало время протестировать мой прототип. Для запуска Python-кода я просто нажимаю кнопку Run в IDE. Для запуска Arduino-проекта достаточно просто подключить USB-кабель к компьютеру.
На экране отображается вся системная информация, которую Python-код передаёт через Serial. Правда, обновление экрана пока не очень плавное, а скорее прерывистое. Кроме того, обновляется не одно число, а вся строка, что вызывает мерцание всех строк каждые 5 секунд. Но я уже рад, что смог получить данные и вывести их на экран.

На втором фото видно экран с тестовыми данными и красным цветом строки. Это было нужно для тестирования всех цветов.

Заключение
Получился вполне рабочий прототип, который пока далёк от идеала. В будущем я добавлю возможность запускать его на разных операционных системах и постараюсь добавить поддержку видеокарт, помимо Nvidia. Также нужно реализовать обработку ошибок, разбить код для Arduino на отдельные файлы, обновлять только числа, а не всю строку, и выводить отладочную информацию в консоль.
В более далёкой перспективе я хочу научиться рисовать кольца и линии на этом экране, чтобы отображать шкалы заполнения, стрелки и графики.
Если у вас есть идеи по улучшению проекта, пишите в комментариях — с удовольствием выслушаю ваши предложения!
Впереди ещё много интересного! =)
Ссылки к статье
Канал в Telegram Arduinum628
Библиотека для дисплея Adafruit_GC9A01A
Репозиторий проекта Spec_pc_to_lcd
Комментарии (17)

x89377
07.06.2025 12:54Я по простому взял USB-SPI адаптер, присоединил дисплей и без всякого Arduino рисую что надо.

Arduinum Автор
07.06.2025 12:54SPI-команды руками пишешь сам? Или есть готовые библиотеки для управления дисплем через SPI? Мельком посмотрел для Python такое и выглядит сложно в реализации если такие команды писать руками. А у тебя какой язык программирования используется?

turbo_f
07.06.2025 12:54
Делал нечто похожее на китайском дисплее с алика. У него была своя утилита с аниме и прочим, но это абсолютно не интересно. Гораздо интереснее было сделать свой вывод инфы) Нашел на гитхабе проект на питоне под микросхему этого дисплея и переписал под текстовый вывод. Цифры меняют цвет в зависимости от критичности показателя от холодных (безопасно) до теплых и красного (не безопасно). Погоду тоже показывал)

Arduinum Автор
07.06.2025 12:54Какой огромный дисплей. Ну реализация интересная, но для моих нужд очень громоздкая.
Ну у меня дисплей круглый и цель конечная будет не использовать одни лишь числа (иначе смысл круга теряется). Да и у меня маленькая приборная панель за клавой, а большой дисплей на столе не вместиться из-за того что два монитора занимают всё место свободное.

Prohard
07.06.2025 12:54Весьма полезно иметь вывод нагрузки Ethernet

Arduinum Автор
07.06.2025 12:54Ты про текущую скорость интернета?

Prohard
07.06.2025 12:54Ну да, полезно наблюдать трафик торрент-клиента, обновлений системы, да вообще тут контроль нужен. Штатный монитор ресурсов под рукой надо держать, а тут на внешнем экранчике и память тебе, и трафик и загрузка проца .... Я похожий девайс повторил, с ардуиной и на стрелочных индикаторах, но там не сети.

Arduinum Автор
07.06.2025 12:54Интересная задумка хотя она не всем и нужна. Но вот если допустим пк спит и качает, то можно понять когда например его выключить

proDream
07.06.2025 12:54У меня в Gnome вот такая панелька, хотелось бы вынести это в отдельный экран, т.к. в полноэкранных приложениях или играх её не видно, а вкорячивать OSD нет желания:

proDream
Класс! Мой экранчик всё ещё в пути, Почта России повезла его по всей стране((
Я буду извращаться на ним ещё больше! Буду писать прошивку и софт полностью на Rust))
Arduinum Автор
Класс, а что будешь использовать для сбора системной информации?
proDream
Тоже Rust)) Но пока особо в подробности не вдавался, хочу сперва получить железку) А потом уже "методом научного тыка" разбираться в его работе)
Arduinum Автор
Ну круто). Буду ждать поглядеть.