Всем доброго дня! Мой никнейм 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
Комментарии (8)
x89377
07.06.2025 12:54Я по простому взял USB-SPI адаптер, присоединил дисплей и без всякого Arduino рисую что надо.
Arduinum Автор
07.06.2025 12:54SPI-команды руками пишешь сам? Или есть готовые библиотеки для управления дисплем через SPI? Мельком посмотрел для Python такое и выглядит сложно в реализации если такие команды писать руками. А у тебя какой язык программирования используется?
turbo_f
07.06.2025 12:54Делал нечто похожее на китайском дисплее с алика. У него была своя утилита с аниме и прочим, но это абсолютно не интересно. Гораздо интереснее было сделать свой вывод инфы) Нашел на гитхабе проект на питоне под микросхему этого дисплея и переписал под текстовый вывод. Цифры меняют цвет в зависимости от критичности показателя от холодных (безопасно) до теплых и красного (не безопасно). Погоду тоже показывал)
proDream
Класс! Мой экранчик всё ещё в пути, Почта России повезла его по всей стране((
Я буду извращаться на ним ещё больше! Буду писать прошивку и софт полностью на Rust))
Arduinum Автор
Класс, а что будешь использовать для сбора системной информации?
proDream
Тоже Rust)) Но пока особо в подробности не вдавался, хочу сперва получить железку) А потом уже "методом научного тыка" разбираться в его работе)
Arduinum Автор
Ну круто). Буду ждать поглядеть.