ARDUINO Arduino SEGA game pad nrf24l01 для RC моделей.

Геннадий П

★★★★★★✩
14 Апр 2021
1,934
624
46
необходимость контроля целостности передачи,
NRF вроде как поддерживает аппаратно контроль целостности передаваемых данных, только не знаю поддерживается ли это ардуиновской библиотекой.
 
  • Красота! +2
Реакции: Stamp

Stamp

★✩✩✩✩✩✩
12 Янв 2021
64
34
@bort707, Байт это не структура. Это просто байт. Структурой его делает мозг программиста.
Здесь задача слишком проста, что бы заморачиваться о протоколах и прочей фигне. Если сильно хочется, можно CRC добавить.
Просто структура даёт более внятный доступ к данным.
А так решений 100500, нужно выбрать приемлемое для себя.
Просто заставлять человека делать так, как делают Ардуинщики, в лоб, это неправильно.
Нужно смотреть классику программирования и следовать ей, если не изобрёл более лучшего.
@Геннадий П, Это поддерживается на уровне железа, хочет этого библиотека или нет.
 

te238s

★★✩✩✩✩✩
14 Ноя 2021
376
98
Пока доделываю свою радиосвязь. Но я решил что лучше делать сразу универсальную базу,поэтому организовал многопоточную работу. Первый поток периодически отправляет пакеты с массивом. Второй занимается обработкой органов управления. По крайней мере мне так проще систему масштабировать потом под любое управление чем-либо. Не знаю,может усложнил сильно,зато всё прозрачно.
Приёмная часть на меге128(ну просто была в наличии). Там параллельная работа по и2с,спай и уарту тоже независимо работают для связи с другими модулями в перспективе.
Массив единый на приёмнике и передатчике. Там информация идёт в обе стороны.
По поводу периодичности отправки. Я так думаю,будет проще оценивать и просчитывать качество связи. Это важно. Одно дело детская машинка,и другое лодка,затупившая на середине озера;)
 
Изменено:

Геннадий П

★★★★★★✩
14 Апр 2021
1,934
624
46
@te238s, 20мсек - за глаза. У человека средняя реакция на визуальный раздражитель - 150-300мсек, а если еще с распознаванием типа раздражителя - и того больше. Я не понимаю людей, которые гонятся за около-нулевой задержки ввода.
 

Старик Похабыч

★★★★★★★
14 Авг 2019
4,381
1,343
Москва
По моему опыту передача небольших пактов информации (6 байт ) с интервалом менее 15мс приводит к пропусками пакетов на нрф. С интервалом в 20 мс передается стабильно на любых скоростях.
 

te238s

★★✩✩✩✩✩
14 Ноя 2021
376
98
@Старик Похабыч,с чем может быть связано? С модулями ли? Может где-то в другом месте затык?
Все ли тайминги соблюдали по даташиту?
 

bort707

★★★★★★★
21 Сен 2020
3,473
1,000
20мс это 50 Гц. Кто различит отдельные мерцания лампы накаливания?)))
мерцания лампы накаливания вы не видите не потому, что не можете уловить 50 Гц, а в основном потому, что нить лампы за 20мс просто не успевает погаснуть.
Это хорошо доказывает видео - мерцания светодиодов на видео есть, а мерцания лампочек - нет
 

ТехнарьКто

★★★★★✩✩
13 Янв 2020
276
461
@te238s, 20мсек - за глаза. У человека средняя реакция на визуальный раздражитель - 150-300мсек, а если еще с распознаванием типа раздражителя - и того больше. Я не понимаю людей, которые гонятся за около-нулевой задержки ввода.
150 мсек это 0,15 секунды. Если я Вас правильно понял. Это чуть менее 7 Герц. Человек различает картинку со сменой кадров вплоть до 300 герц. Но задержек в 20 мкс вполне достаточно для управления в рассматриваемом случае. А вот почему так, сильно долго объяснять и мало кому будет интересно.

PS За около нулевой задержкой гонятся военные. И там оно не надо, а критично. А в самодельном пульте оно мало критично и соответственно совсем не нужно.
 

te238s

★★✩✩✩✩✩
14 Ноя 2021
376
98
А в самодельном пульте оно мало критично и соответственно совсем не нужно.
Именно. Коряво выражаясь,пальцы так не могут быстро на кнопки нажимать:D прибавить ещё энерцию механизмов.
Пример с глазами,конечно,не совсем удачен.

Пишут что максимум у nrf24l01 около 6500 пакетов в секунду.
В принципе так и выходит. Около 150мкс без подтверждения приёма на скорости 2 Мбит/с и spi на 10МГц(что нереально на авр). Да и то,в теории. Никто не знает какие там микросхемы,Китай или Китай :D В даташите есть подробнейшие и муторные расчёты всех таймингов. Другое дело что нам для "бытовухи" это излишне.
 

Старик Похабыч

★★★★★★★
14 Авг 2019
4,381
1,343
Москва
Около 150мкс без подтверждения приёма на скорости 2 Мбит/с
Да, я тоже печатаю 1534 знака в минуту, но такая хня выхордит! (с)
Я отслеживал пакеты именно с подтверждением, по нему и смотрел задержки. Вот скорость передачи не помню, но скорее всего пробовал на разных.
 

Nick

✩✩✩✩✩✩✩
1 Авг 2018
38
5
Тут жеж получается что нажатие одной кнопки блокирует другие,ниже расположеные в коде.
Ну да, так и получается, пока одна зажата вторую не нажмёшь:)
Но это первый скетч, я не уверен был, что он вообще заработает.
Я за скоростью не гонюсь проста ардуинка тоже должна обработать сам джойстик сеги а уже патом передавать сигнал и всё такое.
 

Nick

✩✩✩✩✩✩✩
1 Авг 2018
38
5
Вот что у меня получилось,
но есть нюансы.
C++:
#include <SPI.h>
#include "nRF24L01.h"
#include "RF24.h"
RF24 radio(9, 10);
#define UP_OR_Z 2
#define DOWN_OR_Y 3
#define LEFT_OR_X 4
#define RIGHT_OR_MODE 5
#define B_OR_A 6
#define SEL 7
#define C_OR_START 8
bool sega_up = false;
bool sega_down = false;
bool sega_left = false;
bool sega_right = false;
bool sega_start = false;
bool sega_mode = false;
bool sega_a = false;
bool sega_b = false;
bool sega_c = false;
bool sega_x = false;
bool sega_y = false;
bool sega_z = false;
byte up, down, left, right, start, mode, a, b, c, x, y, z;
byte batton_transmit[12];
void segaRead() {
  digitalWrite(SEL, HIGH);
  sega_up = (digitalRead(UP_OR_Z) == LOW);
  sega_left = (digitalRead(LEFT_OR_X) == LOW);
  sega_right = (digitalRead(RIGHT_OR_MODE) == LOW);
  sega_down = (digitalRead(DOWN_OR_Y) == LOW);
  sega_c = (digitalRead(C_OR_START) == LOW);
  sega_b = (digitalRead(B_OR_A) == LOW);
  digitalWrite(SEL, LOW);
  sega_a = (digitalRead(B_OR_A) == LOW);
  sega_start = (digitalRead(C_OR_START) == LOW);
  digitalWrite(SEL, HIGH);
  digitalWrite(SEL, LOW);
  digitalWrite(SEL, HIGH);
  digitalWrite(SEL, LOW);
  digitalWrite(SEL, HIGH);
  sega_x = (digitalRead(LEFT_OR_X) == LOW);
  sega_y = (digitalRead(DOWN_OR_Y) == LOW);
  sega_z = (digitalRead(UP_OR_Z) == LOW);
  sega_mode = (digitalRead(RIGHT_OR_MODE) == LOW);
  digitalWrite(SEL, LOW);
  digitalWrite(SEL, HIGH);
}
void batton_byte () {
  up = sega_up;
  left = sega_left;
  right = sega_right;
  down = sega_down;
  start = sega_start;
  mode = sega_mode;
  a = sega_a;
  b = sega_b;
  c = sega_c;
  x = sega_x;
  y = sega_y;
  z = sega_z;
}
void setup() {
  radio.begin();
  radio.setChannel(0x70);
  radio.setPALevel (RF24_PA_MAX);
  radio.setDataRate (RF24_1MBPS);
  radio.openWritingPipe(0x0123456789LL);
  radio.powerUp();
  radio.stopListening();
  pinMode(SEL, OUTPUT);
  pinMode(UP_OR_Z, INPUT);
  pinMode(DOWN_OR_Y, INPUT);
  pinMode(LEFT_OR_X, INPUT);
  pinMode(RIGHT_OR_MODE, INPUT);
  pinMode(B_OR_A, INPUT);
  pinMode(C_OR_START, INPUT);
}
void loop() {
  segaRead();
  batton_byte();
  {
    batton_transmit[0] = up;
    batton_transmit[1] = left;
    batton_transmit[2] = right;
    batton_transmit[3] = down;
    batton_transmit[4] = start;
    batton_transmit[5] = mode;
    batton_transmit[6] = a;
    batton_transmit[7] = b;
    batton_transmit[8] = c;
    batton_transmit[9] = x;
    batton_transmit[10] = y;
    batton_transmit[11] = z;
    radio.write(&batton_transmit, sizeof(batton_transmit));
    delay(20);
  }
}
 
Изменено:

Геннадий П

★★★★★★✩
14 Апр 2021
1,934
624
46
radio.setPALevel (RF24_PA_MAX);
Ну зачем? Зачем долбить максимальной мощностью для устройства которое по сути находится на паре метров дистанции? Минимальной мощности для домашних устройств - за глаза, да и потребление гораздо меньше будет.
 

Stamp

★✩✩✩✩✩✩
12 Янв 2021
64
34
@te238s, Что то здесь есть.
Я уже писал прошивку поиска, но, похоже, потерял.
 

Вложения

Nick

✩✩✩✩✩✩✩
1 Авг 2018
38
5
Ну зачем? Зачем долбить максимальной мощностью для устройства которое по сути находится на паре метров дистанции? Минимальной мощности для домашних устройств - за глаза, да и потребление гораздо меньше будет.
Ну я не совсем для домашнего устройства делаю. Машинка RC и на улице может эксплуатироваться.
Так да можно и уменьшить. Я в самом начале специально по максимуму сделал, что бы на дальность работы и на ошибки проверить.
Получилось приёмник (маленький со встроенной антенной) дома на втором этаже хрущёвки, а я от дома метров на 30м - 35м отдалился пока сигнал совсем не пропал.
Ну это как говорится, нюансы можно к нормальному виду всё привести, главное с основным разобраться.;)
 

Nick

✩✩✩✩✩✩✩
1 Авг 2018
38
5
О,уже более унивесальный алгоритм. А что за нюансы?
Пользуясь случаем,хочу замутить ещё алгоритм автоматического поиска наилучшего канала.
Попробовал ещё дописать режим энергоэкономии, как у Гайвера в "заметках ардуинщика".
Стали ошибки вылизать, ложные срабатывания кнопок.
Скеч в приёмной части нужно переписать, что бы полноценно протестировать геймпад на срабатывание кнопок.
 

Nick

✩✩✩✩✩✩✩
1 Авг 2018
38
5
C++:
void segaRead() {
  digitalWrite(SEL, HIGH);
  sega_up = (digitalRead(UP_OR_Z) == LOW);
  sega_left = (digitalRead(LEFT_OR_X) == LOW);
  sega_right = (digitalRead(RIGHT_OR_MODE) == LOW);
  sega_down = (digitalRead(DOWN_OR_Y) == LOW);
  sega_c = (digitalRead(C_OR_START) == LOW);
  sega_b = (digitalRead(B_OR_A) == LOW);
  digitalWrite(SEL, LOW);
  sega_a = (digitalRead(B_OR_A) == LOW);
  sega_start = (digitalRead(C_OR_START) == LOW);
  digitalWrite(SEL, HIGH);
  digitalWrite(SEL, LOW);
  digitalWrite(SEL, HIGH);
  digitalWrite(SEL, LOW);
  digitalWrite(SEL, HIGH);
  sega_x = (digitalRead(LEFT_OR_X) == LOW);
  sega_y = (digitalRead(DOWN_OR_Y) == LOW);
  sega_z = (digitalRead(UP_OR_Z) == LOW);
  sega_mode = (digitalRead(RIGHT_OR_MODE) == LOW);
  digitalWrite(SEL, LOW);
  digitalWrite(SEL, HIGH);
}
Ещё в этом куске SEL должен шарашить с определённой периодичностью LOW/HIGH.
Вроди там с задержкой опрос кнопок должен происходить в 15 - 20мсек, я этим пренебрёг.
Может из за этого проблемы выплывают, а может и нет:cool:.
Ещё я геймпад собрал всё утрамбовал во внутрь, а как к нему подключаться для прошивки не подумал.
Кароче нужно допилить геймпад , что бы из обычного юзерского превратиля в Developer Kit SEGA gamepad:LOL:.
 

Nick

✩✩✩✩✩✩✩
1 Авг 2018
38
5
Тестовый скетч для проверки геймпада.
Выводит данные геймпада в монитор порта приёмника.
Arduino SEGA game pad nrf24l01.
C++:
#include <SPI.h>
#include "nRF24L01.h"
#include "RF24.h"
RF24 radio(9, 10);
uint8_t pipe;
bool up, down, left, right, start, mode, a, b, c, x, y, z;
byte batton_transmit[12];
int total_battons;
void batton_bool () {
  up = batton_transmit[0];
  left = batton_transmit[1];
  right = batton_transmit[2];
  down = batton_transmit[3];
  start = batton_transmit[4];
  mode = batton_transmit[5];
  a = batton_transmit[6];
  b = batton_transmit[7];
  c = batton_transmit[8];
  x = batton_transmit[9];
  y = batton_transmit[10];
  z = batton_transmit[11];
}
void setup() {
  Serial.begin(57600);
  radio.begin();
  radio.setChannel(0x70);
  radio.setDataRate(RF24_1MBPS);
  radio.setPALevel(RF24_PA_MAX);
  radio.openReadingPipe(1, 0x0123456789LL);
  radio.startListening();
}
void loop() {
  batton_bool ();
  if (radio.available(&pipe)) {
    radio.read(&batton_transmit, sizeof(batton_transmit));
  }
  Serial.println ("TEST battons");
  Serial.println ("SEGA game PAD");
  Serial.println ("NRF 24L01 2.4Ghz");
  for (int i; i < 12; i++) {
    batton_transmit[i];
    Serial.print(batton_transmit[i]);
  }
  Serial.println (" ");
  if (up == 1) {
    Serial.println ("UP");
  } else if (left == 1)
  { Serial.println("LEFT");
  } else if (right == 1)
  { Serial.println("RIGHT");
  } else if (down == 1)
  { Serial.println("DOWN");
  } else if (start == 1)
  { Serial.println("START");
  } else if (mode == 1)
  { Serial.println("MODE");
  } else if (a == 1)
  { Serial.println("A");
  } else if (b == 1)
  { Serial.println("B");
  } else if (c == 1)
  { Serial.println("C");
  } else if (x == 1)
  { Serial.println("X");
  } else if (y == 1)
  { Serial.println("Y");
  } else if (z == 1)
    Serial.println("Z");
  delay(150);
  Serial.println("Total buttons game pad");
  total_battons = up + down + left + right + start + mode + a + b + c + x + y + z;
  Serial.println(total_battons);
}
 
Изменено:

Nick

✩✩✩✩✩✩✩
1 Авг 2018
38
5
Схема и скетч пульта управления и машины, переработанные и усовершенствованные. В пульте управление - машиной и самолётом, самолёт я так и не сделал. Управление машиной реализовано в полной мере, можно проста не переключаться в меню на самолёт.



В архиве SEGA_RX_TX два скетча пульт и машина, две схемы сборки:

Пульт – папка SEGA_pad_TX_menu3 в ней два файла SEGA_pad_TX_menu3 и graphics.

SEGA_pad_TX_menu3 – скетч пульта, основной код

graphics – графика (отрисовка фона и эффектов)

Машина – папка SEGA_RX_car_2 в ней файл SEGA_RX_car_2 – скетч машины



Используемые библиотеки в скетче пульта :

#include <OLED_I2C.h>

#include <SPI.h>

#include <EEPROM.h>

#include "nRF24L01.h"

#include "RF24.h"



Используемые библиотеки в скетче машины:

#include <SPI.h>

#include "nRF24L01.h"

#include "RF24.h"





Код пульта имеет модульную структуру:

чтение входов --> обработка логики --> отправка данных --> отрисовка интерфейса.



1. Инициализация и старт (setup)

Экран: Запускается дисплей, устанавливается шрифт и проигрывается стартовая анимация openSpaceDoors() — разъезжающиеся двери с надписью «SYSTEM READY».



Радио (nRF24L01): Настраивается на 102 канал, скорость 250 KBPS, максимальную мощность передатчика и активирует динамический размер пакета. Режим прослушивания отключается, так как пульт только передает команды.



Память (EEPROM): Из энергонезависимой памяти считываются сохраненные режимы для 6 кнопок (A, B, C, X, Y, Z) и глобальный тип модели (CAR или PLANE). Если там мусор (значения вне диапазона), они сбрасываются на дефолтные.



2. Чтение физических кнопок (readSega)

Функция общается с контроллером SEGA по принципу мультиплексирования.

Дергая пин SEL (HIGH/LOW), Ардуино считывает разные состояния кнопок с одних и тех же контактов.



Все нажатия упаковываются в одну 16-битную переменную current_mask, где каждый бит (от 0 до 11) соответствует своей кнопке (Вверх, Вниз, Влево, Вправо, A, B, C, X, Y, Z, START, MODE).



3. Ядро логики (handleLogic)

Логика разделена на два глобальных состояния: Меню и Рабочий режим.

Режим Меню (in_menu = true):



Вход осуществляется одновременным кликом MODE + START.


Кнопки Вверх/Вниз перемещают курсор по 7 пунктам (6 кнопок + выбор модели).

Кнопки Влево/Вправо меняют настройки выбранного пункта.

При выходе из меню новые параметры записываются в EEPROM, и вызывается анимация сохранения showSaveAnimation().



Рабочий режим (in_menu = false):

Сырая маска нажатий current_mask преобразуется в финальную out_mask в зависимости от настроек из меню:



MOMENTARY: Бит передается как есть (нажал — есть сигнал, отпустил — нет).



TRIGGER: Клик переключает состояние бита на противоположное (нажал и отпустил — включилось, повторил — выключилось). Учитывается защита от дребезга через click_mask.



PULSE: Сигнал отправляется только в момент самого клика (один импульс).



TURBO: Если кнопка зажата, сигнал мигает (включается и выключается) в зависимости от таймера millis(), создавая эффект частых нажатий.



Разделение логики моделей:

Машина (MODEL_CAR): Кнопка С работает как переключатель передач (от 0 до 5). Используется флаговая переменная c_was_pressed, чтобы передача переключалась строго по одному разу за клик.



Самолёт (MODEL_PLANE): Здесь реализована продвинутая полетная математика. Кнопки реагируют на обработанную маску out_mask. Кнопки C и Z управляют общим газом (oled_power_target), а клики по A и B меняют триммеры. Кнопка Y сбрасывает все значения тяги в ноль.



4. Радиосвязь и Телеметрия (lossPercentage)

Функция вызывается каждый цикл (если пульт не в меню).

Функция radio.write отправляет готовую out_mask на приёмник машинки или самолёта.

Если данные доставлены (radio_link = true), пульт запрашивает количество попыток отправки (radio.getARC()). На основе этого параметра высчитывается signal_quality — чем больше было повторов (пропусков пакетов), тем ниже качество связи.



5. Отрисовка интерфейса (draw)

Интерфейс полностью динамический и отрисовывается без использования очистки с мерцанием (за исключением меню).

Фон и Меню: Отрисовывается либо фоновая картинка fon, либо текстовый список настроек.

Уровень связи: С права рисуется вертикальная шкала качества сигнала из 13 делений, опирающаяся на расчеты из lossPercentage.

Индикация кнопок: На экране загораются буквы (A, B, C, X, Y, Z, U, D, L, R), если соответствующий бит активен в out_mask.



Специфичная отрисовка:

Для МАШИНЫ:
Полоски газа жестко привязаны к кнопкам A и B (показывают дискретное включение моторов моста L298N). Внизу выводится строка с текущей передачей,

5G|MODE+START-MENU.



Для САМОЛЁТА: Верхние бары загораются от кренов (Влево/Вправо). Нижние бары плавно меняют длину, визуализируя текущую математику газа с учетом триммеров.



Включение фар кнопкой X (out_mask & (1 << 8))

В блоке handleLogic() кнопка X обрабатывается так же, как и все остальные, через выбранный в меню режим (например, если поставить PULSE, то фары будут включаться и выключаться по одному клику).



Включение света при потере сигнала (Failsafe по радиосвязи)

Это критически важная функция безопасности (Failsafe), которая заложена в логику приёмника, а пульт готовит для неё почву:

Со стороны пульта: Функция lossPercentage() постоянно проверяет, дошел ли пакет до машинки. Если связь пропадает, переменная radio_link становится false, а индикатор circle меняет положение.

Со стороны приёмника: Приёмник внутри своего loop() постоянно ждет новых данных от radio.available(). Если пакетов нет определенное время (больше 500 мс), приёмник понимает, что связь потеряна. Он тут же обесточивает моторы L298N (чтобы неуправляемая машина не улетела в дальние дали), но при этом принудительно включает пин 7 (фары), сигнализируя ярким светом: «Я потерялась, спасите!».

Архив с файлами проекта

IMG_20260717_205001_639.jpg

IMG_20260717_205033_362.jpg
 

Nick

✩✩✩✩✩✩✩
1 Авг 2018
38
5
Продублирую код если не хотите скачивать архив а проста код посмотреть
Код пульта
основной код SEGA_pad_TX_menu3
C++:
#include <OLED_I2C.h>
#include <SPI.h>
#include <EEPROM.h>
#include "nRF24L01.h"
#include "RF24.h"
OLED myOLED(SDA, SCL, 4);
extern uint8_t door_L[];
extern uint8_t door_R[];
extern uint8_t fon[];
extern uint8_t circle[];
extern uint8_t SmallFont[];
RF24 radio(9, 10);
#define SEL 7
#define UP_OR_Z 2
#define DOWN_OR_Y 3
#define LEFT_OR_X 4
#define RIGHT_OR_MODE 5
#define B_OR_A 6
#define C_OR_START 8
// Режимы кнопок
#define MODE_MOMENTARY 0
#define MODE_TRIGGER 1
#define MODE_PULSE 2
#define MODE_TURBO 3
const char* mode_names[] = { "MOMENT", "TRIGGR", "PULSE ", "TURBO " };
// Режимы пульта (Глобальный тип модели)
#define MODEL_CAR 0
#define MODEL_PLANE 1
const char* model_names[] = { "CAR  ", "PLANE" };
byte device_mode = MODEL_CAR;  // По умолчанию машина
uint16_t current_mask = 0;
uint16_t last_mask = 0;
uint16_t out_mask = 0;
uint16_t trigger_states = 0;
uint8_t signal_quality = 0;
byte btn_modes[6];  // A, B, C, X, Y, Z
const char* btn_names[] = { "A", "B", "C", "X", "Y", "Z" };
bool in_menu = false;
int menu_cursor = 0;  // От 0 до 6 (всего 7 пунктов)
bool radio_link = false;
int oled_power_target = 0;
int oled_trimL = 0;
int oled_trimR = 0;
// Переменные для коробки передач машинки
int car_gear = 5;            // По умолчанию 5 скорость (макс)
bool c_was_pressed = false;  // Переменная для отслеживания клика кнопки C
void openSpaceDoors() {
  for (int offset = 0; offset <= 64; offset++) {
    myOLED.clrScr();
    myOLED.drawBitmap(0 - offset, 0, door_L, 64, 64);
    myOLED.drawBitmap(64 + offset, 0, door_R, 64, 64);
    if (offset > 35) {
      myOLED.print("SYSTEM READY", CENTER, 28);
    }
    myOLED.update();
  }
}
void setup() {
  myOLED.begin();
  myOLED.setFont(SmallFont);
  openSpaceDoors();
  radio.begin();
  radio.setChannel(102);
  radio.setDataRate(RF24_250KBPS);
  radio.setPALevel(RF24_PA_MAX);
  radio.setRetries(5, 15);
  radio.enableDynamicPayloads();
  radio.openWritingPipe(0x0123456789LL);
  radio.stopListening();
  pinMode(SEL, OUTPUT);
  pinMode(UP_OR_Z, INPUT_PULLUP);
  pinMode(DOWN_OR_Y, INPUT_PULLUP);
  pinMode(LEFT_OR_X, INPUT_PULLUP);
  pinMode(RIGHT_OR_MODE, INPUT_PULLUP);
  pinMode(B_OR_A, INPUT_PULLUP);
  pinMode(C_OR_START, INPUT_PULLUP);
  // Читаем настройки кнопок (адреса 0..5)
  for (int i = 0; i < 6; i++) {
    btn_modes[i] = EEPROM.read(i);
    if (btn_modes[i] > 3) btn_modes[i] = 0;
  }
  // Читаем тип модели из ячейки 6
  device_mode = EEPROM.read(6);
  if (device_mode > 1) device_mode = MODEL_CAR;
}
void showSaveAnimation() {
  myOLED.clrScr();
  myOLED.setFont(SmallFont);
  myOLED.print("SAVE SETTINGS", CENTER, 15);
  int xStart = 14;
  int yStart = 30;
  int width = 100;
  int height = 10;
  myOLED.drawRect(xStart, yStart, xStart + width, yStart + height);
  for (int i = 1; i < width; i++) {
    myOLED.drawLine(xStart + i, yStart + 1, xStart + i, yStart + height);
    if (i % 2 == 0) {
      myOLED.update();
      delay(20);
    }
  }
  myOLED.update();
  delay(500);
}
void readSega() {
  uint16_t state = 0;
  digitalWrite(SEL, HIGH);
  delayMicroseconds(20);
  if (digitalRead(UP_OR_Z) == LOW) state |= (1 << 0);
  if (digitalRead(DOWN_OR_Y) == LOW) state |= (1 << 1);
  if (digitalRead(LEFT_OR_X) == LOW) state |= (1 << 2);
  if (digitalRead(RIGHT_OR_MODE) == LOW) state |= (1 << 3);
  if (digitalRead(B_OR_A) == LOW) state |= (1 << 4);
  if (digitalRead(C_OR_START) == LOW) state |= (1 << 5);
  digitalWrite(SEL, LOW);
  delayMicroseconds(20);
  if (digitalRead(B_OR_A) == LOW) state |= (1 << 6);
  if (digitalRead(C_OR_START) == LOW) state |= (1 << 7);
  digitalWrite(SEL, HIGH);
  delayMicroseconds(20);
  digitalWrite(SEL, LOW);
  delayMicroseconds(20);
  digitalWrite(SEL, HIGH);
  delayMicroseconds(20);
  if (digitalRead(LEFT_OR_X) == LOW) state |= (1 << 8);
  if (digitalRead(DOWN_OR_Y) == LOW) state |= (1 << 9);
  if (digitalRead(UP_OR_Z) == LOW) state |= (1 << 10);
  if (digitalRead(RIGHT_OR_MODE) == LOW) state |= (1 << 11);
  digitalWrite(SEL, LOW);
  current_mask = state;
}
void handleLogic() {
  uint16_t click_mask = current_mask & (~last_mask);
  // Вход/Выход из меню (MODE + START)
  if ((current_mask & (1 << 11)) && (click_mask & (1 << 7))) {
    in_menu = !in_menu;
    if (!in_menu) {
      // Сохраняем кнопки
      for (int i = 0; i < 6; i++) EEPROM.update(i, btn_modes[i]);
      // Сохраняем глобальный режим модели
      EEPROM.update(6, device_mode);
      // БАГФИКС: Сбрасываем коробку передач и флаг кнопки при смене режима
      if (device_mode == MODEL_CAR) {
        car_gear = 5;            // Возвращаем максимальную 5 скорость по умолчанию
        c_was_pressed = false;   // Обнуляем флаг клика, чтобы не было фантомных нажатий
      }
      showSaveAnimation();
    }
    return;
  }
  if (in_menu) {
    // Навигация вверх/вниз по 7 пунктам (0..6)
    if (click_mask & (1 << 0)) menu_cursor--;
    if (click_mask & (1 << 1)) menu_cursor++;
    if (menu_cursor < 0) menu_cursor = 6;
    if (menu_cursor > 6) menu_cursor = 0;
    // Изменение значений кликами влево/вправо
    if (click_mask & (1 << 3)) {  // RIGHT
      if (menu_cursor == 6) {
        device_mode = (device_mode + 1) % 2;
      } else {
        btn_modes[menu_cursor] = (btn_modes[menu_cursor] + 1) % 4;
      }
    }
    if (click_mask & (1 << 2)) {  // LEFT
      if (menu_cursor == 6) {
        device_mode = (device_mode == 0) ? 1 : 0;
      } else {
        btn_modes[menu_cursor] = (btn_modes[menu_cursor] == 0) ? 3 : btn_modes[menu_cursor] - 1;
      }
    }
  } else {
    out_mask = 0;
    out_mask |= (current_mask & 0x0F);       // Стрелки
    out_mask |= (current_mask & (1 << 7));   // START
    out_mask |= (current_mask & (1 << 11));  // MODE
    // Логика коробки передач (Кнопка C) — ТОЛЬКО ДЛЯ МАШИНЫ
    if (device_mode == MODEL_CAR) {
      bool c_now_pressed = (current_mask & (1 << 5));  // Кнопка C (бит 5)
      if (c_now_pressed && !c_was_pressed) {
        car_gear++;
        if (car_gear > 5) car_gear = 0;  // Круговой сброс 0..5
      }
      c_was_pressed = c_now_pressed;
    }
    int mapping[] = { 6, 4, 5, 8, 9, 10 };  // A, B, C, X, Y, Z
    for (int i = 0; i < 6; i++) {
      int bitIdx = mapping[i];
      switch (btn_modes[i]) {
        case MODE_MOMENTARY:
          if (current_mask & (1 << bitIdx)) out_mask |= (1 << bitIdx);
          break;
        case MODE_TRIGGER:
          if (click_mask & (1 << bitIdx)) trigger_states ^= (1 << bitIdx);
          if (trigger_states & (1 << bitIdx)) out_mask |= (1 << bitIdx);
          break;
        case MODE_PULSE:
          if (click_mask & (1 << bitIdx)) out_mask |= (1 << bitIdx);
          break;
        case MODE_TURBO:
          if ((current_mask & (1 << bitIdx)) && ((millis() / 100) % 2 == 0)) {
            out_mask |= (1 << bitIdx);
          }
          break;
      }
    }
    // Расчеты полетных данных 
    static uint16_t prev_out_mask = 0;
    uint16_t out_click_mask = out_mask & (~prev_out_mask);  // Клик из ОБРАБОТАННЫХ кнопок
    static uint32_t oled_timer = 0;
    if (millis() - oled_timer > 80) {
      oled_timer = millis();
      // Кнопки C и Z меняют газ только в режиме самолёта (смотрим в out_mask!)
      if (device_mode == MODEL_PLANE) {
        if (out_mask & (1 << 5)) oled_power_target = min(oled_power_target + 1, 255);  // C
        if (out_mask & (1 << 10)) oled_power_target = max(oled_power_target - 1, 0);   // Z
      }
      // Сброс триммеров по удержанию Z (смотрим в out_mask)
      if (out_mask & (1 << 10)) {
        if (oled_trimL > 0) oled_trimL--;
        else if (oled_trimL < 0) oled_trimL++;
        if (oled_trimR > 0) oled_trimR--;
        else if (oled_trimR < 0) oled_trimR++;
      }
    }
    // Триммеры реагируют на КЛИКИ из меню (out_click_mask)
    if (out_click_mask & (1 << 6)) oled_trimL = constrain(oled_trimL + 5, -50, 255);  // A
    if (out_click_mask & (1 << 4)) oled_trimR = constrain(oled_trimR + 5, -50, 255);  // B
    // Сброс всего по кнопке Y (смотрим в out_mask)
    if (out_mask & (1 << 9)) {
      oled_power_target = 0;
      oled_trimL = 0;
      oled_trimR = 0;
    }
    prev_out_mask = out_mask;  // Запоминаем обработанную маску для следующего цикла
  }
}
void lossPercentage() {
  if (!in_menu) {
    radio_link = radio.write(&out_mask, sizeof(out_mask));
    if (radio_link) {
      uint8_t retries = radio.getARC();
      signal_quality = map(retries, 0, 15, 13, 1);
    } else {
      signal_quality = 0;
    }
  }
}
void draw() {
  myOLED.clrScr();
  myOLED.drawBitmap(0, 0, fon, 128, 64);
  if (in_menu) {
    myOLED.clrScr();
    myOLED.print("- CONFIGURATION -", CENTER, 0);
    // Выводим первые 6 пунктов (Кнопки)
    for (int i = 0; i < 6; i++) {
      String line = (i == menu_cursor ? "> " : "  ");
      line += btn_names[i];
      line += ": ";
      line += mode_names[btn_modes[i]];
      myOLED.print(line, 0, 10 + (i * 7)); 
    }
    // Выводим 7-й глобальный пункт выбора модели
    String line = (menu_cursor == 6 ? "> " : "  ");
    line += "MODEL: ";
    line += model_names[device_mode];
    myOLED.print(line, 0, 10 + (6 * 7));
  } else {
    // --- ГЛАВНЫЙ ЭКРАН ---
    myOLED.print("r/ch 102 SEGA-PAD", LEFT, 0);
    int barX = 126;
    int barY = 44;
    if (radio_link) {
      for (int i = 0; i < 13; i++) {
        if (signal_quality > i) {
          int yPos = barY - (i * 3);
          myOLED.drawLine(barX, yPos, barX - 8, yPos);
          myOLED.drawLine(barX, yPos - 1, barX - 8, yPos - 1);
        }
      }
    }
    myOLED.print("RF24 PA_MAX 250KBPS", LEFT, 8);
    // Локальные переменные длин баров для отрисовки
    int barLengt_tL = 0;
    int barLengt_tR = 0;
    int turboL = 0;
    int turboR = 0;
    // =========================================================================
    // ЖЕСТКОЕ РАЗГРАНИЧЕНИЕ ЭКРАННОЙ ЛОГИКИ
    // =========================================================================
    if (device_mode == MODEL_CAR) {
      // ----------------- РЕЖИМ МАШИНЫ -----------------
      turboL = 0;
      turboR = 0;
      turboL = (out_mask & (1 << 2)) ? 32 : 0;
      turboR = (out_mask & (1 << 3)) ? 32 : 0;
      // Нижние бары четко показывают дискретное питание мостов L298N
      if (out_mask & (1 << 4)) barLengt_tR = 38;  // Нажата B
      if (out_mask & (1 << 6)) barLengt_tL = 38;  // Нажата A
      String bottomStr = String(car_gear);
      myOLED.print("G |MODE+START-MENU", LEFT, 56);
      myOLED.print(bottomStr, 7, 56);
    } else {
      // ----------------- РЕЖИМ САМОЛЁТА -----------------
      // Верхние бары заполняются от кренов (Влево/Вправо)
      turboL = (out_mask & (1 << 2)) ? 32 : 0;
      turboR = (out_mask & (1 << 3)) ? 32 : 0;
      // Нижние бары плавно растут от общего газа и триммеров
      int cruiseL_val = constrain(oled_power_target + oled_trimL, 0, 255);
      int cruiseR_val = constrain(oled_power_target + oled_trimR, 0, 255);
      barLengt_tL = map(cruiseL_val, 0, 255, 0, 38);
      barLengt_tR = map(cruiseR_val, 0, 255, 0, 38);
      myOLED.print("MODE+START-MENU", CENTER, 56);
    }
    // --- ФИЗИЧЕСКАЯ ОТРИСОВКА ВЕРХНИХ БАРОВ (Самолетные крены) ---
    if (turboL > 0) {
      for (int h = 20; h <= 24; h++) {
        myOLED.drawLine(16, h, 15 + turboL, h);
      }
    }
    if (turboR > 0) {
      for (int h = 20; h <= 24; h++) {
        myOLED.drawLine(73, h, 72 + turboR, h);
      }
    }
    // --- ФИЗИЧЕСКАЯ ОТРИСОВКА НИЖНИХ БАРОВ (Базовый Газ / Моторы Машины) ---
    if (barLengt_tL > 0) {
      for (int h = 28; h <= 31; h++) {
        myOLED.drawLine(18, h, 17 + barLengt_tL, h);
      }
    }
    if (barLengt_tR > 0) {
      for (int h = 28; h <= 31; h++) {
        myOLED.drawLine(75, h, 74 + barLengt_tR, h);
      }
    }
    // =========================================================================
    if (radio_link) {
      myOLED.drawBitmap(112, 48, circle, 16, 16);
    }
    int yRow1 = 35;
    int yRow2 = 44;
    if (out_mask & (1 << 8)) myOLED.print("[X]", 65, yRow1);
    if (out_mask & (1 << 9)) myOLED.print("[Y]", 82, yRow1);
    if (out_mask & (1 << 10)) myOLED.print("[Z]", 99, yRow1);
    if (out_mask & (1 << 6)) myOLED.print("A", 71, yRow2);
    if (out_mask & (1 << 4)) myOLED.print("B", 88, yRow2);
    if (out_mask & (1 << 5)) myOLED.print("C", 105, yRow2);
    if (out_mask & (1 << 11)) myOLED.print("MODE", 37, yRow1);
    if (out_mask & (1 << 7)) myOLED.print("START", 37, yRow2);
    if (out_mask & (1 << 0)) myOLED.print("[U]", 4, yRow1);
    if (out_mask & (1 << 1)) myOLED.print("[D]", 20, yRow1);
    if (out_mask & (1 << 2)) myOLED.print("[L]", 4, yRow2);
    if (out_mask & (1 << 3)) myOLED.print("[R]", 20, yRow2);
  }
  myOLED.update();
}
void loop() {
  readSega();
  handleLogic();
  lossPercentage();
  draw();
  last_mask = current_mask;
}
код graphics
C++:
#include <avr/pgmspace.h>
const uint8_t door_L[] PROGMEM = {
  0x00, 0x00, 0x00, 0xfc, 0x04, 0x04, 0xc4, 0xe4, 0xf4, 0xf4, 0xf4, 0xf4, 0xf4, 0xf4, 0xf4, 0xf4,
  0xf4, 0xf4, 0x74, 0x74, 0xe4, 0xc4, 0x04, 0x04, 0x04, 0xc4, 0xe4, 0xf4, 0xd4, 0x94, 0x94, 0x94,
  0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94,
  0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x14,
  0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x3f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x6f, 0x67, 0x73,
  0x39, 0x1c, 0x0e, 0x07, 0x83, 0x41, 0x20, 0x90, 0xa0, 0x20, 0x41, 0x53, 0x53, 0x93, 0xb3, 0xb3,
  0x33, 0x73, 0x53, 0x53, 0x93, 0x93, 0x13, 0x13, 0x13, 0x93, 0x13, 0x93, 0x13, 0x93, 0x13, 0x93,
  0x13, 0x93, 0x13, 0x93, 0x13, 0x93, 0x13, 0x93, 0x13, 0x93, 0x13, 0x93, 0x13, 0x93, 0x13, 0x13,
  0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0xf8, 0xfc, 0x1c, 0x6c, 0x04, 0x04, 0xe4, 0x14, 0x04, 0xf8,
  0x00, 0x00, 0x00, 0xfc, 0xfd, 0xd9, 0x5a, 0x12, 0x34, 0x34, 0x64, 0x68, 0x4a, 0xd2, 0xd0, 0x90,
  0xa1, 0x29, 0x4a, 0x42, 0x42, 0x84, 0xa5, 0xa9, 0x09, 0x0a, 0x12, 0x12, 0xa5, 0xa4, 0x29, 0x4a,
  0x49, 0x52, 0x95, 0x92, 0x25, 0x2a, 0x15, 0x4a, 0x55, 0xaa, 0x95, 0xaa, 0x55, 0x2a, 0x55, 0x00,
  0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0xff,
  0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
  0x01, 0x01, 0x03, 0x07, 0x1a, 0xe2, 0xfe, 0xe4, 0x15, 0x09, 0x0a, 0x0a, 0x12, 0x14, 0x24, 0x28,
  0x28, 0x4a, 0xd2, 0x90, 0xa1, 0x21, 0x11, 0xca, 0x26, 0x10, 0x08, 0x08, 0x09, 0x29, 0xea, 0xea,
  0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0xff, 0xff, 0x3f, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0xff,
  0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80,
  0x80, 0x80, 0xc0, 0xe0, 0x58, 0x47, 0x7f, 0x27, 0xa8, 0x90, 0x50, 0x50, 0x48, 0x28, 0x24, 0x14,
  0x14, 0x52, 0x4b, 0x09, 0x85, 0x84, 0x88, 0x53, 0x64, 0x08, 0x13, 0x12, 0x94, 0x94, 0x54, 0x55,
  0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x1f, 0x3f, 0x38, 0x36, 0x20, 0x20, 0x27, 0x28, 0x20, 0x1f,
  0x00, 0x00, 0x00, 0x3f, 0xbf, 0x9b, 0x5a, 0x48, 0x2c, 0x2c, 0x26, 0x16, 0x52, 0x4b, 0x0b, 0x09,
  0x85, 0x94, 0x52, 0x42, 0x42, 0x21, 0xa5, 0x95, 0x90, 0x50, 0x48, 0x48, 0x25, 0xa5, 0x54, 0x92,
  0x52, 0xaa, 0x49, 0xa9, 0x54, 0xa4, 0x50, 0xaa, 0x52, 0xa9, 0x55, 0xa9, 0x54, 0xaa, 0x55, 0x00,
  0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0xfc, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xf6, 0xe6, 0xce,
  0x9c, 0x38, 0x70, 0xe0, 0xc1, 0x82, 0x04, 0x09, 0x05, 0x04, 0x82, 0xca, 0xca, 0xc9, 0xcd, 0xcd,
  0xcc, 0xce, 0xca, 0xca, 0xc9, 0xc9, 0xc8, 0xc8, 0xc8, 0xc8, 0xc9, 0xc8, 0xc9, 0xc8, 0xc9, 0xc8,
  0xc9, 0xc8, 0xc9, 0xc8, 0xc9, 0xc8, 0xc9, 0xc8, 0xc9, 0xc8, 0xc9, 0xc8, 0xc9, 0xc8, 0xc9, 0xc8,
  0x00, 0x00, 0x00, 0x3f, 0x20, 0x20, 0x23, 0x27, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f,
  0x2f, 0x2f, 0x2e, 0x2e, 0x27, 0x23, 0x20, 0x20, 0x20, 0x23, 0x27, 0x2f, 0x2b, 0x29, 0x29, 0x29,
  0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
  0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x28
};

const uint8_t door_R[] PROGMEM = {
  0x14, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94,
  0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94,
  0x94, 0x94, 0x94, 0xd4, 0xf4, 0xe4, 0xc4, 0x04, 0x04, 0x04, 0xc4, 0xe4, 0x74, 0x74, 0xf4, 0xf4,
  0xf4, 0xf4, 0xf4, 0xf4, 0xf4, 0xf4, 0xf4, 0xf4, 0xe4, 0xc4, 0x04, 0x04, 0xfc, 0x00, 0x00, 0x00,
  0x13, 0x13, 0x93, 0x13, 0x93, 0x13, 0x93, 0x13, 0x93, 0x13, 0x93, 0x13, 0x93, 0x13, 0x93, 0x13,
  0x93, 0x13, 0x93, 0x13, 0x93, 0x13, 0x93, 0x13, 0x13, 0x13, 0x93, 0x93, 0x53, 0x53, 0x73, 0x33,
  0xb3, 0xb3, 0x93, 0x53, 0x53, 0x41, 0x20, 0xa0, 0x90, 0x20, 0x41, 0x83, 0x07, 0x0e, 0x1c, 0x39,
  0x73, 0x67, 0x6f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x3f, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
  0x00, 0x55, 0x2a, 0x55, 0xaa, 0x95, 0xaa, 0x55, 0x4a, 0x15, 0x2a, 0x25, 0x92, 0x95, 0x52, 0x49,
  0x4a, 0x29, 0xa4, 0xa5, 0x12, 0x12, 0x0a, 0x09, 0xa9, 0xa5, 0x84, 0x42, 0x42, 0x4a, 0x29, 0xa1,
  0x90, 0xd0, 0xd2, 0x4a, 0x68, 0x64, 0x34, 0x34, 0x12, 0x5a, 0xd9, 0xfd, 0xfc, 0x00, 0x00, 0x00,
  0xf8, 0x04, 0x14, 0xe4, 0x04, 0x04, 0x6c, 0x1c, 0xfc, 0xf8, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00,
  0xea, 0xea, 0x29, 0x09, 0x08, 0x08, 0x10, 0x26, 0xca, 0x11, 0x21, 0xa1, 0x90, 0xd2, 0x4a, 0x28,
  0x28, 0x24, 0x14, 0x12, 0x0a, 0x0a, 0x09, 0x15, 0xe4, 0xfe, 0xe2, 0x1a, 0x07, 0x03, 0x01, 0x01,
  0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00,
  0xff, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xfc, 0xff, 0xff, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00,
  0x55, 0x54, 0x94, 0x94, 0x12, 0x13, 0x08, 0x64, 0x53, 0x88, 0x84, 0x85, 0x09, 0x4b, 0x52, 0x14,
  0x14, 0x24, 0x28, 0x48, 0x50, 0x50, 0x90, 0xa8, 0x27, 0x7f, 0x47, 0x58, 0xe0, 0xc0, 0x80, 0x80,
  0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00,
  0xff, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00,
  0x00, 0x55, 0xaa, 0x54, 0xa9, 0x55, 0xa9, 0x52, 0xaa, 0x50, 0xa4, 0x54, 0xa9, 0x49, 0xaa, 0x52,
  0x92, 0x54, 0xa5, 0x25, 0x48, 0x48, 0x50, 0x90, 0x95, 0xa5, 0x21, 0x42, 0x42, 0x52, 0x94, 0x85,
  0x09, 0x0b, 0x4b, 0x52, 0x16, 0x26, 0x2c, 0x2c, 0x48, 0x5a, 0x9b, 0xbf, 0x3f, 0x00, 0x00, 0x00,
  0x1f, 0x20, 0x28, 0x27, 0x20, 0x20, 0x36, 0x38, 0x3f, 0x1f, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00,
  0xc8, 0xc9, 0xc8, 0xc9, 0xc8, 0xc9, 0xc8, 0xc9, 0xc8, 0xc9, 0xc8, 0xc9, 0xc8, 0xc9, 0xc8, 0xc9,
  0xc8, 0xc9, 0xc8, 0xc9, 0xc8, 0xc9, 0xc8, 0xc8, 0xc8, 0xc8, 0xc9, 0xc9, 0xca, 0xca, 0xce, 0xcc,
  0xcd, 0xcd, 0xc9, 0xca, 0xca, 0x82, 0x04, 0x05, 0x09, 0x04, 0x82, 0xc1, 0xe0, 0x70, 0x38, 0x9c,
  0xce, 0xe6, 0xf6, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfc, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
  0x28, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
  0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
  0x29, 0x29, 0x29, 0x2b, 0x2f, 0x27, 0x23, 0x20, 0x20, 0x20, 0x23, 0x27, 0x2e, 0x2e, 0x2f, 0x2f,
  0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x27, 0x23, 0x20, 0x20, 0x3f, 0x00, 0x00, 0x00
};

const uint8_t fon[] PROGMEM = {
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x0c, 0x06, 0x02, 0x0a, 0x0a, 0x32, 0xe6, 0x0c, 0xf8, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00,
  0x00, 0xe0, 0x38, 0x0c, 0x06, 0x02, 0x03, 0xf9, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0xe1, 0xf1,
  0x19, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09,
  0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09,
  0x09, 0x09, 0x29, 0x29, 0x29, 0x29, 0x49, 0x19, 0xf1, 0xe1, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
  0xf9, 0x49, 0xc9, 0x49, 0x31, 0x01, 0x01, 0xe1, 0xf1, 0x19, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09,
  0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09,
  0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x29, 0x29, 0x29, 0x29, 0x49,
  0x19, 0xf1, 0xe1, 0x01, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00,
  0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfb, 0x0a, 0x02, 0x12, 0xf2, 0x10, 0x00, 0xf0, 0x81,
  0x83, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
  0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
  0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x09, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0xfb, 0x08, 0x00, 0x11, 0xf2, 0x10, 0x00, 0xf0, 0x51, 0xa3, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
  0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
  0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
  0x03, 0x09, 0xf8, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00,
  0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
  0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
  0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
  0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
  0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
  0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
  0x02, 0x02, 0x03, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00,
  0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x60, 0x40, 0x40, 0x40, 0x40, 0xc0, 0xcf, 0xe0, 0xff, 0x00,
  0x00, 0x01, 0x07, 0x0c, 0x18, 0x10, 0x30, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00,
  0xe0, 0x38, 0xcc, 0xf6, 0xfa, 0xfb, 0x3d, 0xbd, 0x01, 0x81, 0x82, 0x82, 0x84, 0x19, 0xe3, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x03, 0x0c, 0x10, 0x20, 0x20, 0x40, 0x40, 0x5e, 0x5e, 0x6f, 0x2f, 0x37, 0x19, 0x0e, 0x03, 0x00
};

const uint8_t circle[] PROGMEM = {
  0xE0, 0x38, 0x8C, 0x86, 0x82, 0x83, 0x01, 0xBD, 0x3D, 0xFB, 0xFA, 0xF6, 0xCC, 0x39, 0xE3, 0x00,  // 0x0010 (16) pixels
  0x03, 0x0E, 0x19, 0x37, 0x2F, 0x6F, 0x5E, 0x5E, 0x40, 0x60, 0x20, 0x30, 0x18, 0x0E, 0x03, 0x00,  // 0x0020 (32) pixels
};
 
Изменено: