Глава 3: Игра „Двоичное Число“ и GPIO-индикация
В этой главе мы создадим интерактивную обучающую игру для Raspberry Pi Zero 2 W, которая поможет освоить двоичную систему счисления. Приложение будет использовать GPIO-пины для визуализации двоичных чисел с помощью светодиодов, а пользователь будет вводить десятичные эквиваленты через графический интерфейс.
—
Введение
Это приложение превратит ваш Raspberry Pi в увлекательный инструмент для практики перевода чисел из двоичной системы в десятичную. Оно будет генерировать случайные двоичные числа, отображать их на светодиодах, а вы будете вводить свои догадки через GUI. Мы также добавим подсчет правильных и неправильных ответов.
—
Подключение компонентов
Для этой главы нам понадобятся следующие компоненты и их подключение к Raspberry Pi:
- 8 светодиодов с резисторами 220 Ом каждый: Подключаются к следующим GPIO-пинам (каждый светодиод через резистор к пину, а другой конец светодиода к GND):
GPIO17 (пин 11)
GPIO18 (пин 12)
GPIO27 (пин 13)
GPIO22 (пин 15)
GPIO23 (пин 16)
GPIO24 (пин 18)
GPIO25 (пин 22)
GPIO4 (пин 7)
Ниже представлена схема подключения светодиодов:
—
Индекс бита (в коде) |
GPIO-пин (BCM) |
Физический пин |
Значимость бита |
|---|---|---|---|
0 |
GPIO4 |
7 |
Младший бит (LSB) |
1 |
GPIO25 |
22 |
|
2 |
GPIO24 |
18 |
|
3 |
GPIO23 |
16 |
|
4 |
GPIO22 |
15 |
|
5 |
GPIO27 |
13 |
|
6 |
GPIO18 |
12 |
|
7 |
GPIO17 |
11 |
Старший бит (MSB) |
—
Принцип работы
Логика игры будет следующей:
Начало игры: При нажатии кнопки «Старт» приложение генерирует случайное число от 0 до 255.
Двоичная индикация: Это число преобразуется в 8-битное двоичное представление. Каждый бит этого числа управляет состоянием одного из восьми светодиодов: если бит равен 1, соответствующий светодиод включается; если 0 — выключается.
Пользовательский ввод: Игрок вводит десятичное число, которое, по его мнению, соответствует двоичному числу, отображаемому на светодиодах, в текстовое поле.
Проверка ответа: При нажатии кнопки «Ваш ответ» приложение сравнивает введенное пользователем число с загаданным.
Подсчет очков: В зависимости от правильности ответа, обновляются счетчики «Правильных» и «Ошибок».
Новое число: После каждого ответа генерируется новое случайное число, и светодиоды обновляются.
Завершение игры: Кнопка «Стоп» сбрасывает все светодиоды и останавливает игру.
Для взаимодействия с GPIO используется библиотека libgpiod, а графический интерфейс построен с помощью GTK+3.
—
Интерфейс
Графический интерфейс игры будет включать следующие элементы:
8 цветных квадратов/кругов: Визуальное представление светодиодов на экране. Они будут менять цвет (например, с белого на красный), имитируя включение/выключение.
Поле ввода: Для ввода пользователем десятичного числа.
- Кнопки управления:
«Старт»: Начинает игру и генерирует первое число.
«Ваш ответ»: Проверяет введенный ответ и генерирует следующее число.
«Стоп»: Завершает игру и сбрасывает счетчики.
Счетчики: Два лейбла, отображающие количество правильных ответов и ошибок.
Вот как может выглядеть макет GUI:
—
Код
Ниже представлен полный C-код для реализации описанной функциональности.
#include <gtk/gtk.h> // Подключаем библиотеку GTK для создания графического интерфейса
#include <gpiod.h> // Подключаем библиотеку libgpiod для работы с GPIO
#include <stdlib.h> // Подключаем стандартную библиотеку для функций, таких как rand() и atoi()
#include <time.h> // Подключаем библиотеку для работы со временем, используемой для инициализации rand()
// Определение констант для имени GPIO-чипа и потребителя (имя, которое будет видно в системе)
#define CHIPNAME "gpiochip0"
#define CONSUMER "BinaryGame"
// Массив номеров GPIO-пинов, к которым подключены светодиоды
// Эти пины будут использоваться для отображения 8-битного двоичного числа
int gpio_pins[8] = {17, 18, 27, 22, 23, 24, 25, 4};
// Структура для хранения всех данных приложения, включая указатели на виджеты GTK,
// линии GPIO, счетчики игры и состояние игры.
struct app_data {
struct gpiod_chip *chip; // Указатель на GPIO-чип
struct gpiod_line *lines[8]; // Массив указателей на линии GPIO для светодиодов
GtkWidget *entry; // Указатель на виджет поля ввода (GtkEntry)
GtkWidget *correct_label; // Указатель на лейбл для отображения количества правильных ответов (GtkLabel)
GtkWidget *wrong_label; // Указатель на лейбл для отображения количества ошибок (GtkLabel)
GtkWidget *start_button; // Указатель на кнопку "Старт" (GtkButton)
GtkWidget *stop_button; // Указатель на кнопку "Стоп" (GtkButton)
GtkWidget *submit_button; // Указатель на кнопку "Ваш ответ" (GtkButton)
GtkWidget *led_indicators[8]; // Массив указателей на виджеты-лейблы, имитирующие светодиоды в GUI
int correct_count; // Счетчик правильных ответов
int wrong_count; // Счетчик ошибок
int current_value; // Текущее загаданное десятичное число
gboolean game_running; // Флаг состояния игры (TRUE - игра активна, FALSE - нет)
};
// Функция update_leds: Устанавливает состояние физических светодиодов и их GUI-индикаторов
// в соответствии с двоичным представлением заданного числа.
void update_leds(struct app_data *data, int value) {
for (int i = 0; i < 8; i++) {
// Извлекаем i-й бит из числа 'value'
// Оператор >> (побитовый сдвиг вправо) сдвигает биты числа на 'i' позиций вправо.
// Оператор & 1 (побитовое И с 1) извлекает самый младший бит (то есть, текущий i-й бит).
int bit = (value >> i) & 1;
// Устанавливаем значение на соответствующей GPIO-линии.
// 0 - выключить светодиод, 1 - включить светодиод.
gpiod_line_set_value(data->lines[i], bit);
// Визуально обновляем индикатор в GUI:
// Делаем его видимым (включенным), если бит равен 1, и невидимым (выключенным), если бит равен 0.
gtk_widget_set_visible(data->led_indicators[i], bit);
}
}
// Функция reset_leds: Выключает все физические светодиоды и их GUI-индикаторы.
void reset_leds(struct app_data *data) {
update_leds(data, 0); // Вызываем update_leds с значением 0, чтобы выключить все светодиоды
}
// Обработчик кнопки "Старт": Начинает новую игру.
void start_game(GtkButton *btn, gpointer user_data) {
struct app_data *data = (struct app_data *)user_data;
// Генерируем случайное число от 0 до 255.
// rand() % 256 дает остаток от деления на 256, что гарантирует число в диапазоне [0, 255].
data->current_value = rand() % 256;
// Обновляем светодиоды в соответствии с новым загаданным числом.
update_leds(data, data->current_value);
// Устанавливаем флаг, что игра запущена.
data->game_running = TRUE;
// Очищаем текстовое поле ввода, чтобы пользователь мог ввести новый ответ.
gtk_entry_set_text(GTK_ENTRY(data->entry), "");
}
// Обработчик кнопки "Ваш ответ": Проверяет ответ пользователя.
void check_answer(GtkButton *btn, gpointer user_data) {
struct app_data *data = (struct app_data *)user_data;
// Если игра не запущена, игнорируем нажатие кнопки "Ваш ответ".
if (!data->game_running) return;
// Получаем текст из поля ввода.
const char *input = gtk_entry_get_text(GTK_ENTRY(data->entry));
// Преобразуем введенную строку в целое число.
int user_value = atoi(input);
// Сравниваем ответ пользователя с текущим загаданным числом.
if (user_value == data->current_value) {
data->correct_count++; // Увеличиваем счетчик правильных ответов.
} else {
data->wrong_count++; // Увеличиваем счетчик ошибок.
}
// Обновляем текст на лейблах, отображающих статистику.
char buf[64]; // Буфер для форматирования строк
snprintf(buf, sizeof(buf), "Правильных: %d", data->correct_count);
gtk_label_set_text(GTK_LABEL(data->correct_label), buf);
snprintf(buf, sizeof(buf), "Ошибок: %d", data->wrong_count);
gtk_label_set_text(GTK_LABEL(data->wrong_label), buf);
// Генерируем новое случайное число для следующего раунда игры.
data->current_value = rand() % 256;
update_leds(data, data->current_value); // Обновляем светодиоды для нового числа.
gtk_entry_set_text(GTK_ENTRY(data->entry), ""); // Очищаем поле ввода для нового ответа.
}
// Обработчик кнопки "Стоп": Завершает текущую игру.
void stop_game(GtkButton *btn, gpointer user_data) {
struct app_data *data = (struct app_data *)user_data;
reset_leds(data); // Выключаем все светодиоды.
data->game_running = FALSE; // Устанавливаем флаг, что игра остановлена.
// Сбрасываем счетчики правильных и неправильных ответов.
data->correct_count = 0;
data->wrong_count = 0;
// Обновляем лейблы статистики.
gtk_label_set_text(GTK_LABEL(data->correct_label), "Правильных: 0");
gtk_label_set_text(GTK_LABEL(data->wrong_label), "Ошибок: 0");
gtk_entry_set_text(GTK_ENTRY(data->entry), ""); // Очищаем поле ввода.
}
// Обработчик сигнала "destroy" окна: Освобождает все захваченные ресурсы и завершает приложение.
void on_destroy(GtkWidget *widget, gpointer user_data) {
struct app_data *data = (struct app_data *)user_data;
reset_leds(data); // Убедимся, что все светодиоды выключены перед выходом.
// Освобождаем каждую GPIO-линию, которая была запрошена.
for (int i = 0; i < 8; i++) {
if (data->lines[i]) { // Проверяем, что указатель на линию не NULL (т.е. линия была успешно получена)
gpiod_line_release(data->lines[i]);
}
}
// Закрываем GPIO-чип.
if (data->chip) { // Проверяем, что указатель на чип не NULL (т.е. чип был успешно открыт)
gpiod_chip_close(data->chip);
}
gtk_main_quit(); // Завершаем основной цикл обработки событий GTK, что приводит к завершению приложения.
}
// Главная функция приложения, точка входа.
int main(int argc, char *argv[]) {
gtk_init(&argc, &argv); // Инициализация библиотеки GTK. Это должно быть вызвано в начале.
srand(time(NULL)); // Инициализация генератора случайных чисел.
// time(NULL) возвращает текущее время, обеспечивая разную последовательность чисел при каждом запуске.
struct app_data data = {0}; // Создаем и инициализируем нулями структуру app_data.
data.correct_count = 0; // Устанавливаем начальное количество правильных ответов.
data.wrong_count = 0; // Устанавливаем начальное количество ошибок.
data.game_running = FALSE; // Игра изначально не запущена.
// Инициализация GPIO
data.chip = gpiod_chip_open_by_name(CHIPNAME); // Открываем GPIO-чип по его имени.
if (!data.chip) {
perror("Ошибка: не удалось открыть GPIO chip"); // Выводим сообщение об ошибке, если чип не открылся.
return 1; // Завершаем программу с кодом ошибки.
}
// Запрашиваем и настраиваем все 8 GPIO-линий как выходы.
for (int i = 0; i < 8; i++) {
// Получаем указатель на конкретную GPIO-линию по ее номеру.
data.lines[i] = gpiod_chip_get_line(data.chip, gpio_pins[i]);
// Запрашиваем линию как выход с начальным значением 0 (выключено).
// Проверяем, что линия получена и запрос успешен.
if (!data.lines[i] || gpiod_line_request_output(data.lines[i], CONSUMER, 0) < 0) {
perror("Ошибка: не удалось настроить пины"); // Сообщение об ошибке.
// В случае ошибки, освобождаем все линии, которые были успешно запрошены до этого.
for (int j = 0; j < i; j++) {
if (data.lines[j]) gpiod_line_release(data.lines[j]);
}
gpiod_chip_close(data.chip); // Закрываем чип.
return 1; // Завершаем программу.
}
}
// Создание и настройка GTK интерфейса
GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL); // Создаем главное окно приложения.
gtk_window_set_title(GTK_WINDOW(window), "Игра: Двоичное Число"); // Устанавливаем заголовок окна.
gtk_window_set_default_size(GTK_WINDOW(window), 400, 300); // Устанавливаем размер окна по умолчанию.
// Подключаем обработчик сигнала "destroy" (закрытие окна) к функции on_destroy.
g_signal_connect(window, "destroy", G_CALLBACK(on_destroy), &data);
GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); // Создаем вертикальный контейнер с отступом 10 пикселей.
gtk_container_add(GTK_CONTAINER(window), vbox); // Добавляем контейнер в главное окно.
// Контейнер для светодиодов (визуальное представление битов)
GtkWidget *led_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5); // Создаем горизонтальный контейнер для 8 "светодиодов" с отступом 5 пикселей.
// Создаем 8 виджетов-лейблов, которые будут имитировать светодиоды.
for (int i = 7; i >= 0; i--) { // Идем от 7 к 0, чтобы старший бит (самый значимый) был слева в GUI.
GtkWidget *led = gtk_label_new("⬤"); // Используем Unicode символ "черный круг" как "светодиод".
gtk_widget_set_visible(led, FALSE); // Изначально все "светодиоды" выключены (невидимы).
gtk_widget_set_name(led, "led"); // Устанавливаем имя для CSS-стилизации (если потребуется).
gtk_box_pack_start(GTK_BOX(led_box), led, TRUE, TRUE, 0); // Добавляем "светодиод" в горизонтальный контейнер.
data.led_indicators[i] = led; // Сохраняем указатель на виджет в структуре данных.
}
gtk_box_pack_start(GTK_BOX(vbox), led_box, FALSE, FALSE, 0); // Добавляем контейнер светодиодов в основной вертикальный контейнер.
// Поле ввода для ответа пользователя
data.entry = gtk_entry_new(); // Создаем новое текстовое поле ввода.
gtk_entry_set_placeholder_text(GTK_ENTRY(data.entry), "Введите число"); // Устанавливаем текст-подсказку.
gtk_box_pack_start(GTK_BOX(vbox), data.entry, FALSE, FALSE, 0); // Добавляем поле ввода в основной контейнер.
// Контейнер для кнопок управления игрой
GtkWidget *button_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5); // Горизонтальный контейнер для кнопок.
data.start_button = gtk_button_new_with_label("Старт"); // Создаем кнопку "Старт".
g_signal_connect(data.start_button, "clicked", G_CALLBACK(start_game), &data); // Подключаем обработчик нажатия.
gtk_box_pack_start(GTK_BOX(button_box), data.start_button, TRUE, TRUE, 0); // Добавляем кнопку.
data.submit_button = gtk_button_new_with_label("Ваш ответ"); // Создаем кнопку "Ваш ответ".
g_signal_connect(data.submit_button, "clicked", G_CALLBACK(check_answer), &data); // Подключаем обработчик.
gtk_box_pack_start(GTK_BOX(button_box), data.submit_button, TRUE, TRUE, 0); // Добавляем кнопку.
data.stop_button = gtk_button_new_with_label("Стоп"); // Создаем кнопку "Стоп".
g_signal_connect(data.stop_button, "clicked", G_CALLBACK(stop_game), &data); // Подключаем обработчик.
gtk_box_pack_start(GTK_BOX(button_box), data.stop_button, TRUE, TRUE, 0); // Добавляем кнопку.
gtk_box_pack_start(GTK_BOX(vbox), button_box, FALSE, FALSE, 0); // Добавляем контейнер кнопок в основной контейнер.
// Лейблы для отображения статистики (правильных/неправильных ответов)
data.correct_label = gtk_label_new("Правильных: 0"); // Создаем лейбл для правильных ответов.
data.wrong_label = gtk_label_new("Ошибок: 0"); // Создаем лейбл для ошибок.
gtk_box_pack_start(GTK_BOX(vbox), data.correct_label, FALSE, FALSE, 0); // Добавляем лейбл в основной контейнер.
gtk_box_pack_start(GTK_BOX(vbox), data.wrong_label, FALSE, FALSE, 0); // Добавляем лейбл в основной контейнер.
gtk_widget_show_all(window); // Отображаем все виджеты, содержащиеся в окне.
gtk_main(); // Запускаем основной цикл обработки событий GTK. Приложение будет активно до вызова gtk_main_quit().
// Ресурсы GPIO освобождаются в on_destroy, но здесь можно добавить дополнительную очистку,
// если on_destroy не будет вызвана (например, при аварийном завершении).
// В данном случае on_destroy гарантированно вызывается при закрытии окна.
return 0; // Успешное завершение программы.
}
Компиляция и запуск Для компиляции и запуска этой игры на вашем Raspberry Pi Zero 2 W выполните следующие шаги:
Сохраните код: Сохраните приведенный выше C-код в файл, например, binary_game.c.
Компиляция: Откройте терминал на вашем Raspberry Pi и перейдите в директорию, где вы сохранили файл binary_game.c. Затем выполните следующую команду:
gcc binary_game.c -o binary_game $(pkg-config --cflags --libs gtk+-3.0 libgpiod) -Wall -Wextra
gcc: Компилятор C.
binary_game.c: Исходный файл с вашим кодом.
-o binary_game: Имя выходного исполняемого файла, который будет создан после компиляции.
$(pkg-config –cflags –libs gtk+-3.0 libgpiod): Эта команда автоматически находит необходимые флаги компилятора (–cflags) и библиотеки (–libs) для GTK3 и libgpiod, упрощая процесс компиляции.
-Wall -Wextra: Включает все стандартные и дополнительные предупреждения компилятора, что помогает выявлять потенциальные ошибки и улучшать качество кода.
Запуск: После успешной компиляции запустите программу, выполнив в терминале:
./binary_game
Теперь вы должны увидеть окно GTK с игрой «Двоичное Число». Нажмите «Старт», чтобы начать игру, и попробуйте угадать десятичное значение, отображаемое на светодиодах!
Создание ярлыка (Launcher) для рабочего стола Чтобы запускать игру прямо с рабочего стола Raspberry Pi OS без использования терминала, вы можете создать файл .desktop (ярлык).
Откройте текстовый редактор (например, nano или geany) на вашем Raspberry Pi.
nano ~/.local/share/applications/binary_game.desktop
(Если папки ~/.local/share/applications/ не существует, создайте ее: mkdir -p ~/.local/share/applications/)
Вставьте следующее содержимое в файл binary_game.desktop:
[Desktop Entry]
Version=1.0
Type=Application
Name=Игра "Двоичное Число"
Comment=Обучающая игра для Raspberry Pi: угадай двоичное число
Exec=/home/alex/projects/Github/GUI_for_Zero2W/binary_game
Icon=applications-games
Terminal=false
Categories=Game;Education;
Важные моменты:
Exec: Укажите полный путь к вашему исполняемому файлу binary_game. Убедитесь, что путь /home/alex/projects/Github/GUI_for_Zero2W/ соответствует фактическому расположению вашего проекта.
Icon: Вы можете использовать стандартные иконки системы (например, applications-games, education-science, utilities-terminal) или указать путь к собственному файлу иконки (например, /home/alex/projects/Github/GUI_for_Zero2W/icon.png).
Terminal=false: Указывает, что приложение не требует запуска в терминале. Если ваше приложение выводит что-то важное в терминал, можете установить true.
Сохраните файл (Ctrl+O, Enter, Ctrl+X для nano).
Сделайте ярлык исполняемым:
chmod +x ~/.local/share/applications/binary_game.desktop
Теперь вы должны увидеть ярлык «Игра „Двоичное Число“» в меню приложений (обычно в разделе «Игры» или «Образование») или на рабочем столе. Возможно, потребуется перезагрузить рабочий стол или систему, чтобы ярлык появился.
Поздравляю!