Arduino

Геймпад PS2 для Arduino: управление проектом через джойстик

1 просмотров

Введение

Модуль игрового контроллера PS2 для Arduino даёт готовый, эргономичный интерфейс пользовательского ввода (два аналоговых стика, несколько кнопок) для проектов, требующих более развитого управления, чем простой потенциометр или несколько отдельных кнопок — типичное применение, продолжающее тему робототехники из статьи 389 — ручное управление роботом.

Концепция

Контроллер PS2 (оригинальный геймпад от консоли PlayStation 2, переиспользуемый в хобби-проектах через специальный адаптер/модуль) общается по проприетарному протоколу, похожему на SPI, но с собственными временными требованиями — готовая библиотека (PS2X_lib или аналогичная) полностью абстрагирует этот протокол, давая простой интерфейс опроса состояния каждой кнопки и положения каждого аналогового стика. В отличие от простых, единичных аналоговых входов (потенциометр), геймпад даёт сразу множество одновременно доступных каналов ввода (два двухосевых стика — итого 4 аналоговых значения, и порядка дюжины кнопок), что подходит для проектов, требующих богатого, многоканального управления (робот с управлением движением и дополнительными функциями одновременно).

Пример кода

// Подключение и чтение состояния PS2-геймпада через PS2X_lib
#include <PS2X_lib.h>

PS2X ps2x;

const int PS2_DAT_PIN = 12;
const int PS2_CMD_PIN = 11;
const int PS2_SEL_PIN = 10;
const int PS2_CLK_PIN = 13;

void setup() {
    Serial.begin(9600);

    int error = ps2x.config_gamepad(PS2_CLK_PIN, PS2_CMD_PIN, PS2_SEL_PIN, PS2_DAT_PIN, true, true);

    if (error == 0) {
        Serial.println("Контроллер PS2 найден и настроен");
    } else {
        Serial.println("Ошибка подключения контроллера PS2");
    }
}

void loop() {
    ps2x.read_gamepad(); // обновление состояния — должно вызываться каждую итерацию перед проверками

    // Аналоговые стики — значения от 0 до 255, обычно 128 соответствует нейтральному положению
    int leftStickX = ps2x.Analog(PSS_LX);
    int leftStickY = ps2x.Analog(PSS_LY);

    // Кнопки — проверка нажатия в данный момент
    if (ps2x.Button(PSB_CROSS)) {
        Serial.println("Кнопка X нажата");
    }

    // ButtonPressed — срабатывает только один раз на нажатие (передний фронт), а не всё время удержания
    if (ps2x.ButtonPressed(PSB_TRIANGLE)) {
        Serial.println("Кнопка Треугольник нажата (одно событие)");
    }

    delay(50); // PS2-протокол требует определённой минимальной паузы между опросами
}
// Практическое применение — управление двигателями робота через левый стик
void controlRobotFromJoystick() {
    ps2x.read_gamepad();

    int stickY = ps2x.Analog(PSS_LY); // вперёд/назад
    int stickX = ps2x.Analog(PSS_RX); // поворот

    // Преобразование диапазона стика (0-255, нейтраль 128) в скорость моторов (-255..255)
    int forwardSpeed = map(stickY, 0, 255, 255, -255); // инвертировано — стик вверх обычно даёт меньшее значение Y
    int turnSpeed = map(stickX, 0, 255, -255, 255);

    int leftMotorSpeed = constrain(forwardSpeed + turnSpeed, -255, 255);
    int rightMotorSpeed = constrain(forwardSpeed - turnSpeed, -255, 255);

    // setMotorSpeed() — функция управления конкретным драйвером двигателей,
    // аналогичная подходам из основного Arduino-цикла
}

Пояснения к коду

ps2x.read_gamepad() должен вызываться на каждой итерации основного цикла перед любыми проверками состояния кнопок/стиков — это обновляет внутреннее, кешированное состояние библиотеки актуальным опросом физического контроллера, и пропуск этого вызова означал бы работу с устаревшими, более не актуальными данными. Различие между Button() (проверка, нажата ли кнопка прямо сейчас, истинно на всё время удержания) и ButtonPressed() (истинно только один раз, в момент самого нажатия) — важная деталь API, аналогичная различию между уровневым и фронтовым срабатыванием прерываний (STM32-цикла) — выбор подходящей функции зависит от конкретной логики: для непрерывного управления (газ, руль) подходит Button()/аналоговое значение стика, а для разовых действий (переключение режима, выстрел в игре) — ButtonPressed().

Подводные камни

  • Слишком частый опрос (read_gamepad()) без соблюдения минимальной паузы, требуемой протоколом PS2 — в отличие от большинства цифровых датчиков на этом микроконтроллере, протокол PS2-геймпада имеет собственные временные требования к минимальному интервалу между опросами, и слишком частые вызовы без задержки (как небольшая delay(50) в примере) могут привести к нестабильному, ненадёжному считыванию состояния контроллера.
  • Игнорирование значения, возвращаемого config_gamepad(), и попытка продолжить работу с контроллером даже при неудачной инициализации — без проверки кода ошибки (как в примере, явно проверяющем error == 0) дальнейшие попытки чтения состояния неинициализированного контроллера дадут произвольные, бессмысленные значения без явного сообщения о реальной причине проблемы, что усложняет диагностику при физических проблемах подключения.
  • Использование Button() там, где логически нужно одноразовое событие (что привело бы к многократному, нежелательному срабатыванию действия на каждой итерации цикла, пока кнопка удерживается, вместо однократного срабатывания в момент нажатия) — выбор между Button() и ButtonPressed()/ButtonReleased() должен явно соответствовать желаемой семантике конкретного управляемого действия, а не выбираться произвольно или по привычке использовать одну и ту же функцию для всех случаев.
  • Прямое управление силовыми двигателями непосредственно с тех же пинов, что использует протокол PS2 (CLK/CMD/SEL/DAT, обычно подключаемых к конкретным, не произвольным пинам, иногда совпадающим с пинами, обычно используемыми для других целей, как аппаратный SPI) — перед окончательной разводкой проекта стоит явно сверить занятость пинов между PS2-модулем и любыми другими используемыми в том же проекте SPI-устройствами (модуль SD-карты, тоже использующий SPI), чтобы избежать конфликта пинов между этими разными модулями.