Сегодня разберёмся как работает простое, но тем не менее интересное устройство, которое можно применить во множестве систем и поделок. Джойстик (прямо как в playstation) – состоит из двух переменных резисторов, которые изменяют своё сопротивление по оси X и оси Y. Из этих двух параметров можно легко сделать, например курсор навигации по меню, или использовать игровую механику. Взглянув на принципиальную схему, можно легко понять логику работы.

Исходя из этой схемы очень легко подключать джойстик к любому микроконтроллеру. Обратите внимание, что выводы на разных джойстиках могут быть размещены по разному. Нужно будет подать питание на джойстик, аналоговые оси соединить с АЦП, а кнопку можно вывести на любой дискретный вход.

В программе для удобства определяем аналоговые входы для осей X и Y.
#define axis_X A1 // Ось Х подключена к Analog 0 #define axis_Y A2 // Ось Y подключена к Analog 1
Затем определим переменные для хранения результатов считывания АЦП для осей (int val_X, val_Y;).
int val_X, val_Y; // Переменные для хранения значений осей
Организуем вывод данных в командную строку, чтобы понять границы и пределы показаний джойстика ky-023.
Serial.print("X:"); Serial.println(val_X, DEC); // Выводим значение в Serial Monitor X Serial.print(" | Y:"); Serial.println(val_Y, DEC); // Выводим значение в Serial Monitor Y
Запустим программу и посмотрим результаты работы джойстика. В состоянии покоя результаты Х и Y равны около 450, с отклонениями до 900 и 0 при работы джойстика.

Теперь попробуем написать небольшую игру, так сказать применить джойстик на деле. Введём координаты прицела Х и Y. Значения задавать не будем, сразу считаем их с джойстика.
unsigned int x; unsigned int y;
Экран я взял из прошлой статьи, разрешением 320х240, а границы считывания джойстика составляют 900, поэтому можно смело поделить результат считывания на 3. Получается в состоянии покоя 450/3 = 150px. Если экран расположить вертикально, то середина будет 320/2=160px. Почти подходит, можно как раз сделать полоску статуса сверху. А вот для горизонтали нужно значение 120px, поэтому вычтем 30px от результата. Вообще в итоге я сделал так в функции считывания показателей джойстика.
void read_joy() { val_X = analogRead(axis_X); // Считываем аналоговое значение оси Х val_Y = analogRead(axis_Y); // Считываем аналоговое значение оси Y x = (val_X / 3)- 30; y = (val_Y / 3)+ 10; }
Всё хорошо, но сами координаты нужно чем то отобразить. Я не придумал ничего лучше, как сделать это кружочками из библиотеки adafruit . Четыре с разными интервалами смотрелись лучше всего.
void cursor_aim (void) { tft.drawCircle(x, y, 3, ILI9341_RED); tft.drawCircle(x, y, 4, ILI9341_RED); tft.drawCircle(x, y, 7, ILI9341_RED); tft.drawCircle(x, y, 8, ILI9341_RED); }
Сразу после отрисовки я немного жду, и стираю результат, перед считыванием следующих показаний. Так на экране не образуется шлейфа, и курсор всегда будет отрисован один раз на новых координатах.
void cursor_aim (void) { tft.drawCircle(x, y, 3, ILI9341_RED); tft.drawCircle(x, y, 4, ILI9341_RED); tft.drawCircle(x, y, 7, ILI9341_RED); tft.drawCircle(x, y, 8, ILI9341_RED); }
Теперь сверху сделаем строку состояния.
tft.fillScreen(ILI9341_BLACK); tft.setTextColor(ILI9341_RED); tft.setTextSize(2); tft.print("SCORE:::");
Ну и чтобы прицел не стирал эту строку, ограничим координаты перед отрисовкой.
if (y<30) { y=30; }
Должна получится такая картина.

Прицел двигается, всё хорошо. Теперь продумаем появление врага. Ему тоже нужны будут координаты (случайные), Для этого в setup добавим следующую команду, которая возьмёт со свободного аналогового пина A0 случайное значение (контакт в воздухе).
randomSeed(analogRead(0));
Теперь сделаем функцию случайных координат, которую будем вызывать на старте, и после того, как нашего монстра убили.
void random_enemy (void) { x_enemy = random(2, 210); y_enemy = random(30, 290); }
Создадим переменную для хранения флага нового врага.
unsigned int flag_enemy = 1;
При первом запуске она будет равна 1, поэтому отработает следующая функция. Также при попадании в монстра, флаг снова будет обновляться (это сделаем далее).
void new_enemy (void) { if (flag_enemy == 1) { random_enemy(); flag_enemy = 0; } }
Теперь нарисуем монстра. Я сделал его из 4 прямоугольников зелёного цвета. Монстра нужно отрисовывать каждый кадр в loop(), потому-что курсор может стереть его.
void monster (void) { tft.fillRect(x_enemy, y_enemy, 30, 15, ILI9341_GREEN); tft.fillRect(x_enemy, (y_enemy - 4), 7, 4, ILI9341_GREEN); tft.fillRect((x_enemy + 23), (y_enemy - 4), 7, 4, ILI9341_GREEN); tft.fillRect((x_enemy + 5), (y_enemy + 15), 20, 5, ILI9341_GREEN); }
Логика такая – монстр рисуется каждый раз, но вот координаты обновляются только при его смерти. Смерть наступает при обновлении флага new_enemy, а значит нужно придумать условие. Так как курсор и монстр имеют координаты, можно придумать уравнение. Проблема, что курсор рисуется из центра, а прямоугольники нет, но немного применив математику я быстро придумал решение из 4 формул. Также пятым условием я добавил нажатие на кнопку. Также здесь мы будем обновлять счётчик очков (добавлена переменная count2, и сбрасывать флаг монстра flag_enemy, чтобы отрисовать нового, по случайным координатам.
void strike (void) { if (digitalRead(button)==0 && x_enemy + 30 > x && x_enemy < x && y_enemy + 20 > y && y_enemy < y) { count2++; tft.fillRect(x_enemy, y_enemy, 30, 15, ILI9341_BLACK); tft.fillRect(x_enemy, (y_enemy - 4), 7, 4, ILI9341_BLACK); tft.fillRect((x_enemy + 23), (y_enemy - 4), 7, 4, ILI9341_BLACK); tft.fillRect((x_enemy + 5), (y_enemy + 15), 20, 5, ILI9341_BLACK); flag_enemy = 1; } }
Только не забудьте объявить кнопку с джойстика, у меня она подключена на D2.
#define button 2 // Кнопка подключена к D2
Смотрим на результат.

Не хватает конечно счётчика. Мы увеличиваем переменную count2, а сравнивать будем её с count, и если она поменялась, то меняем значение на экране и также её инкрементируем, для дальнейшего сравнения.
void scores (void) { if (count != count2) { count++; tft.fillRect(200, 0, 50, 25, ILI9341_BLACK); tft.setTextColor(ILI9341_RED); tft.setTextSize(2); tft.setCursor(200, 0); tft.println(count); } }
Не забываем объявить эти переменные
unsigned int count; unsigned int count2;
Теперь проверяем наш файл loop(). Я добавил небольшую задержку, для более красивого свечения курсора.
void loop(void) { new_enemy(); scores(); clear_aim(); read_joy(); cursor_aim(); monster(); strike(); delay(30); }

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