Всем доброго дня! Мой никнейм 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);

  • Макетные плата (без пайки);

  • Пластик.


Сборка конструкции

Главная идея конструкции заключается в том, что она просто стоит за клавиатурой. Так как это прототип, я не стал заморачиваться с дизайном и красивым корпусом. Просто взял пару уголков от упаковки шкафа и пластик от крышки с влажными салфетками.

Приклеил макетные платы на двусторонний скотч, соединил их и вставил друг в друга, чтобы две половинки конструкции держались вместе.
Из пластика вырезал несколько деталей для крепления экрана и просверлил четыре отверстия под его крепление. В итоге получилась конструкция, которая идеально подходит по высоте клавиатуры.

Construction_1.jpg
Конструкция_1

Экран прикрутил четырьмя винтами к пластиковому креплению. Пластину согнул под углом, чтобы дисплей смотрел вверх, как в панели управления или спидометре.

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

Construction_2.jpg
Конструкция 2

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


Подключение к Arduino

pins.jpg
Пины дисплея GC9A01

Пины дисплея:

  • 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_to_display.jpg
Дисплей подключен к arduino

Код для 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 я буду пользоваться готовыми библиотеками.

Начну реализацию с конфига, в котором будут храниться настройки для подключения.
В конфигурации нет секретных данных, поэтому я не буду усложнять её чтением .envpydantic_settings и другими инструментами, предназначенными для работы с конфиденциальными данными. Я создам самый простой конфиг и просто захардкодю в нём необходимые параметры.

config.py:

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__"::

  • Импортируются параметры PORTSPEED и TIMEOUT из файла config.py;

  • Вызывается функция send_spec_str_serial(port=PORT, speed=SPEED, timeout=TIMEOUT), которая начинает отправку данных через последовательный порт.

Да простят меня гуру Python, я не обработал ошибки в прототипе. Я понимаю, насколько это важно, и планирую заняться этим в следующих статьях. Сейчас для меня главное — работоспособность проекта, так как я уже немало намучался с работой самого дисплея.


Запуск

Настало время протестировать мой прототип. Для запуска Python-кода я просто нажимаю кнопку Run в IDE. Для запуска Arduino-проекта достаточно просто подключить USB-кабель к компьютеру.

На экране отображается вся системная информация, которую Python-код передаёт через Serial. Правда, обновление экрана пока не очень плавное, а скорее прерывистое. Кроме того, обновляется не одно число, а вся строка, что вызывает мерцание всех строк каждые 5 секунд. Но я уже рад, что смог получить данные и вывести их на экран.

result.jpg
Дисплей отображает данные

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

fake_data.jpg
Дисплей отображает тестовые данные

Заключение

Получился вполне рабочий прототип, который пока далёк от идеала. В будущем я добавлю возможность запускать его на разных операционных системах и постараюсь добавить поддержку видеокарт, помимо Nvidia. Также нужно реализовать обработку ошибок, разбить код для Arduino на отдельные файлы, обновлять только числа, а не всю строку, и выводить отладочную информацию в консоль.

В более далёкой перспективе я хочу научиться рисовать кольца и линии на этом экране, чтобы отображать шкалы заполнения, стрелки и графики.

Если у вас есть идеи по улучшению проекта, пишите в комментариях — с удовольствием выслушаю ваши предложения!

Впереди ещё много интересного! =)

Ссылки к статье

Комментарии (8)


  1. proDream
    07.06.2025 12:54

    Класс! Мой экранчик всё ещё в пути, Почта России повезла его по всей стране((

    Я буду извращаться на ним ещё больше! Буду писать прошивку и софт полностью на Rust))


    1. Arduinum Автор
      07.06.2025 12:54

      Класс, а что будешь использовать для сбора системной информации?


      1. proDream
        07.06.2025 12:54

        Тоже Rust)) Но пока особо в подробности не вдавался, хочу сперва получить железку) А потом уже "методом научного тыка" разбираться в его работе)


        1. Arduinum Автор
          07.06.2025 12:54

          Ну круто). Буду ждать поглядеть.


  1. x89377
    07.06.2025 12:54

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


    1. Arduinum Автор
      07.06.2025 12:54

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


  1. turbo_f
    07.06.2025 12:54

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


  1. Prohard
    07.06.2025 12:54

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