Afstandsbediening met 2.4GHz PS2 controller

In het boek ‘Bauen, erleben, begreifen: fischertechnik®-Roboter mit Arduino’ wordt een klein bestuurbaar autootje, de ‘Flitzer’, beschreven. In het boek worden ook enkele methoden gegeven voor de besturing. Onder andere met een infrarood afstandsbediening en een z.g. ‘nunchuk’ controller.

Dit artikel over de PS2-controller is ook (in de Duitse taal) gepubliceerd in nummer 4 van 2020 van de ft:pedia; een zeer interessant online magazine over techniek in combinatie met het constructiemateriaal fischertechnik.

Video werd geblokkeerd
Instellingen
PS2 Controller

Een draadloze besturingsconsole

De eerste generatie spelcomputers, zoals de Sony PlayStation 2, had nog geen draadloze controllers. Inmiddels zijn hiervoor bij diverse robot-shops en online winkels draadloze game-consoles te koop [1] waarvan de ontvanger-module in het front van de (klassieke versie 2) PlayStation kan worden geplugd. Voor rond de €20,- krijg je op die manier een zender met twee joysticks en vijftien extra functie-knoppen. Wie bereid is enkele weken te wachten, kan de controller ook voor minder dan de helft van deze prijs direct uit China laten komen.
 
De console zendt zijn signaal uit in de 2.4GHz RF frequentieband en de vanaf de controller verzonden signalen komen volgens de specificaties tot op 10 meter afstand digitaal uit de bijgeleverde ontvangermodule. Dit maakt het natuurlijk aantrekkelijk te onderzoeken of zo’n gamepad misschien als goedkope, zeer functionele afstandsbediening voor fischertechnik-modellen zou kunnen dienen!
 
Bijzonder is dat de interface bidirectioneel is. Zowel de interface als de gamepad zijn dus beiden zowel zender als ontvanger. Er is dus ook een data-stroom van de Arduino terug naar de controller voor het aansturen van twee kleine motoren in de controller die feedback kunnen geven door vibratie. Erg interessant voor bijvoorbeeld het aangeven van de grijp- of draai-limieten van robots, mogelijke obstakels in de baan van een bestuurbaar voertuig of, zoals ik nu voor de ‘Flitzer’ bij wijze van experiment heb gerealiseerd, voelbare feedback geven tijdens het remmen van een voertuigje.
 
De controller voelt wel iets meer speelgoed-achtig aan dan de werkelijke Sony PlayStation-controllers. Maar dat is voor deze prijs natuurlijk ook wel te verwachten. Bij mijn model zat geen handleiding, maar online was een, waarschijnlijk uit het Chinees vertaalde, handleiding te vinden. Ook werd het gelukkig al snel duidelijk dat er meer mensen mee aan het knutselen zijn geweest.

De hardware

Op de connector van de ontvangstmodule bevinden zich de noodzakelijke signalen. Als voedingsspanning mag tussen 3.3 en 5 volt op Vcc en Gnd worden aangeboden. De communicatie wordt gedreven door de CLK (Clock) waarbij het protocol wordt geleid door de Attn (Attention). De Ack (Acknowledge) toont slechts een korte puls na elke reeks verzonden en ontvangen bytes en had waarschijnlijk alleen vroeger bij de bedrade variant van deze controllers een nuttige functie. Op de ‘Vibr’ (Vibrate) uitgang leverde een originele PlayStation de (7.6 tot 9V) spanning voor het aandrijven van de feedback-motoren in de (bedrade) controller. Ook dat is inmiddels vanzelfsprekend niet meer van toepassing omdat de draadloze controller zich zelf voedt uit twee AAA-batterijen.
 
Het communicatie-protocol is full-duplex en lijkt op SPI. Met ‘Data’ wordt de controller-data in bursts van diverse bytes binnengelezen. Tegelijk stroomt over CMD (Command) de communicatie van de interface terug naar de game-pad. Waarschijnlijk is deze datastroom alleen noodzakelijk voor de status-informatie richting de gamepad en het aansturen van de vibratie-motoren.

PS2 Connector
Flitzer PS2 Remote

Aansluiten aan het motor shield

De ontvangermodule wordt aangesloten op de digitale in-/uitgangen 10 tot en met 13. Een voedingsspanning van 5V geeft geen problemen, maar omdat bij een klassieke PlayStation2 slechts 3.4V werd gemeten, ben ik maar aan de veilige kant gebleven en heb de 3.3V uitgang van het motor shield hiervoor gebruikt.
 

Voor wie de ‘Flitzer’ uit het boek bouwde, vormen de aansluitingen weinig verrassingen. Het enige is dat ik de rode achterlichten vanuit een motor-uitgang wilde aansturen om deze meerdere licht-intensiteiten te kunnen geven. Hierdoor wordt het mogelijk om tijdens het rijden normale achterlichten te voeren, maar deze feller te laten oplichten bij het remmen (vertragen). De begeleidende Arduino-Sketch ondersteunt deze functionaliteit inmiddels. [5]

Feedback door vibratie

Ik moest mijn exemplaar open schroeven omdat de aan/uit schakelaar soms niet goed werkte. Na netjes vertinnen van de aansluitingen rond het batterijvak en de schakelaar werkte alles onberispelijk. Doordat ik de controller open had, werd me een blik op het inwendige gegund. Duidelijk zichtbaar in de handvatten zijn de twee vibratie-motoren. De kleine motor rechts is aan of uit, de motor rechts met een iets groter contragewicht is traploos regelbaar. Voor extra effect kunnen natuurlijk beide motoren worden gestart. Met een test-Sketch liet de aansturing en werking daarvan zich goed bestuderen. [5]

PS2 rumble motorPS2 rumble motorPS2 rumble motor

De normale drukknoppen zijn desgewenst drukgevoelig uit te lezen. Hoewel dit niet erg precies is, kunnen zij een analoge waarde afhankelijk van de uitgeoefende drukkracht teruggeven. De controller kent, volgens de beschrijving, twee modi. Het is ook mogelijk slechts binaire waarden uit te lezen. Wellicht is dit een overblijfsel uit de begintijd van de PlayStation 2 toen de spellen nog niet zo geavanceerd waren. Bij gebruik van de genoemde PS2-library wordt standaard de volledige analoge-modus geactiveerd waarin van de joysticks een waarde tussen 0 en 255 kan worden uitgelezen en de twaalf functieknoppen desgewenst drukgevoelig zijn.

De verbinding tussen de gamepad en de ontvanger kwam telkens snel en betrouwbaar tot stand. Het maakte hiervoor weinig uit of eerst de game-controller werd ingeschakeld, of dat ik de ‘Flitzer’ inschakelde.

PS2 detailPS2 detail

Na enige tijd niet gebruikt te zijn, schakelt de gamepad zichzelf uit. Volgens de gevonden handleiding zou dit na één minuut zijn. Mijn exemplaar schakelde zich echter pas na vijf minuten uit. Het daarna automatisch opnieuw koppelen van de controller aan de interface, door het drukken van de knop ‘start’ op de gamepad, lukte niet (met de huidige software library). Maar een probleem was dit voor mij (nog) niet, omdat in de praktijk deze time-out niet snel bereikt zal worden bij het normaal besturen van een model.
 
Mocht dit in de toekomst noodzakelijk zijn, dan valt altijd nog een oplossing te bedenken waarbij de ontvanger-interface (die het opnieuw ‘connecten’ initieert) vanuit de software wordt herstart bij het uitblijven van communicatie. De huidige library lijkt een dergelijke softwarematige reset of reconnect echter nog niet te bieden. Een simpele hardwarematige work-around zou een onderbreking van de 3.3v voedingsspanning van de ontvanger kunnen zijn. Hiervoor zou kunnen worden overwogen de ontvanger via een van de vrije motor-uitgangen van het motor-shield te voeden. Het lijkt dan verstandig de Vcc-ingangsspanning (b.v. met een zener) op 3.3v te begrenzen. Ik heb hier echter, ook omdat het voor mij geen probleem was, nog niet verder mee geëxperimenteerd.

De software

Er zijn gelukkig al meer mensen met zo’n PS2-controller aan het experimenteren geweest [2] [3] en is er ook al een library voor de Arduino te vinden [4]. Hoewel deze niet is opgenomen in het bibliotheken-beheer van de Arduino IDE, laat deze zich eenvoudig toevoegen.
 
In de begeleidende Sketch [6] maakte ik bovengenoemde uitbreiding voor feller oplichtende remlichten. Ook deed ik een experiment met de aansturing van de vibratie-motoren om de console tijdens het vertragen (remmen) van de ‘Flitzer’ voelbare feedback in de controller te geven. Dit gedrag is nu in en uit te schakelen met de knop met het roze vierkantje. De verlichting (voor en achter) wordt ingeschakeld met de knop met het rode rondje, en de knop met de groene driehoek schakelt de alarmverlichting in.
 
Met de linker joystick kan de snelheid en rijrichting van het wagentje worden geregeld. Bij vertragen (remmen) lichten de remlichten een seconde op. De rechter joystick verzorgt het sturen met de servo.

De PS2 controller laat zich, zoals reeds vermeld, voeden met twee (eventueel herlaadbare) AAA-batterijen. Voor de voeding van mijn model gebruik ik een herlaadbare 9V blok-batterij. Deze oplossing is klein en licht. Omdat de polariteit bij inschakelen belangrijk is, heb ik een kleine ‘aanslag’ gemaakt. Door een kleine, uit plexiglas gezaagde, stootnok kan de voedingsspanning maar op een manier worden ingeschakeld. De PS2 ontvangstmodule is met zelfklevend klittenband op de batterijhouder gedrukt.

Van mijn bevindingen met deze draadloze gamepad maakte ik een filmpje [7]. Ik kan iedereen die een betrouwbare en betaalbare afstandsbediening met veel uitbreidingsmogelijheden zoekt, een experiment met deze PS2 controller aanraden. Met twee joysticks en maar liefst twaalf functieknoppen voor zelf toe te kennen functies, biedt hij veel bedieningsmogelijkheden voor elk Arduino model. De vibratie-feedback in de controller zelf is een geinige bonus die voor sommige modellen daadwerkelijk functioneel kan worden ingezet.

Polarity safe
// 'Flitzer' with remote controlled PS2 gamepad
//
// Arnoud van Delden - December 2020
//
// Libs:
//   PS2X_lib - Check www.billporter.info for updates and to report bugs
//   Check out PS2X_lib.h for the button defines that may be used in your tests and calls.
//
//   Adafruit AdaFruit MotorShield

#include "Wire.h"
#include "PS2X_lib.h"
#include "Servo.h"
#include "Adafruit_MotorShield.h"

//#define DEBUG

// Various defines...
#define ServoPin 9
#define minAngle 55
#define maxAngle 125
#define MaxSpeed 255
#define FlasherL 0
#define FlasherR 14
#define REARLIGHT 100 // Default read light brightness, with headroom for brake light (=255)

Adafruit_MotorShield MShield = Adafruit_MotorShield(0x60);
Adafruit_DCMotor *Motor = MShield.getMotor(1); // Main motor
Adafruit_DCMotor *Light = MShield.getMotor(2); // Headlights...
Adafruit_DCMotor *RedRearLight = MShield.getMotor(3); // Rear and brake light...
Adafruit_DCMotor *ReverseLight = MShield.getMotor(4); // Reverse light..

PS2X ps2x; // PS2 gamepad Controller Class
int error = 0; 
byte controller_type = 0;

Servo ServoMotor;
int motorspeed = 0;
int lastspeed = 0;
int steerAngle = 90; // Start with default steer angle...
unsigned long lasttime;
unsigned long brakeOffTimer;
volatile unsigned long steps;
boolean FlasherLeft = false, FlasherRight = false, FlasherOn = false, RumbleBrake = false,
        WarningLights = false, BrakeLights = false, autoParking = false, lightsOn = false;

void count() { // Interrupt routine of encoder motor...
  steps++;
}

void driveCar() { // IR controlled car...
   ps2x.read_gamepad(RumbleBrake & BrakeLights, 0); // Read controller, rumble during braking...

  // Servo steering...
  steerAngle = map(ps2x.Analog(PSS_RX), 0, 255, minAngle, maxAngle);
#ifdef DEBUG
    if (steerAngle != 90) {
      Serial.print(" steerAngle: ");
      Serial.println(steerAngle, DEC);
    }
#endif  
  ServoMotor.write(steerAngle);

  // Set motor speed...
  motorspeed = ps2x.Analog(PSS_LY) - 127;
  if (motorspeed<0) {
    // Forward...
    motorspeed = abs(motorspeed);
    motorspeed = map(motorspeed, 0, 128,0, 255);
    Motor->setSpeed(motorspeed);
    Motor->run(FORWARD);
    ReverseLight->setSpeed(0);
#ifdef DEBUG
    if (motorspeed>0) {
      Serial.print("Forward motorspeed: ");
      Serial.println(motorspeed, DEC);
    }
#endif    
  } else {
    // Backward/reverse...
    motorspeed = map(motorspeed, 0, 128,0, 255);
    Motor->run(BACKWARD);
    Motor->setSpeed(motorspeed);
    if (motorspeed>1)
      ReverseLight->setSpeed(255);
    else // Switch off reverse light if not moving...
      ReverseLight->setSpeed(0);
#ifdef DEBUG
    if (motorspeed>0) {
      Serial.print("Reverse motorspeed: ");
      Serial.println(motorspeed, DEC);
    }
#endif   
  }
  if (motorspeedsetSpeed(0);
        RedRearLight->setSpeed(0);
        lightsOn=false;
      } else {
        Light->setSpeed(255);
        RedRearLight->setSpeed(REARLIGHT);
        lightsOn=true;
      }
#ifdef DEBUG
      Serial.println("Headlights toggle");
#endif
    }
  }

  // Toggle RumbleBrake...
  if (ps2x.NewButtonState(PSB_PINK)) {
    if (ps2x.Button(PSB_PINK)) {
      // Pink square was just pressed...
      if (RumbleBrake) {
        RumbleBrake=false;
      } else {
        RumbleBrake=true;
      }
#ifdef DEBUG
      Serial.println("RumbleBrake toggle");
#endif
    }
  }
  
  // Warning lights...
  if (ps2x.NewButtonState(PSB_GREEN)) {
    if (ps2x.Button(PSB_GREEN)) {
      // Green triangle was just pressed...
      if (WarningLights)
        WarningLights = false;
      else
        WarningLights = true;
#ifdef DEBUG
    Serial.println("WarningLights toggle");
#endif
    }
  }

  // Auto-parking procedure...
  if (ps2x.NewButtonState(PSB_BLUE)) {
    if (ps2x.Button(PSB_BLUE)) {
      // Blue X was just pressed...
      autoParking = true;
#ifdef DEBUG
      Serial.println("Auto-parking procedure");
#endif
    }
  }

  delay(50);
}

void setup() {
  Serial.begin(115200); // Start serial monitor
  
  MShield.begin();       // Motor Shield init...
  Wire.setClock(400000); // Set I²C-Frequency to 400 kHz...
  ServoMotor.attach(ServoPin); // Servo with default angle...
  ServoMotor.write(steerAngle); // Default middle...
      
  ReverseLight->setSpeed(0); 
  ReverseLight->run(FORWARD);
  RedRearLight->setSpeed(0); 
  RedRearLight->run(BACKWARD);
  Light->setSpeed(0); 
  Light->run(FORWARD);

  MShield.setPin(FlasherL, false);
  MShield.setPin(FlasherR, false);
    
  steps = 0;
  Motor->run(FORWARD); // Default=Forward...
  Motor->setSpeed(0); 

  // Setup pins and settings: config_gamepad(clock, command, attention, data, Pressures?, Rumble?)
  error = ps2x.config_gamepad(13, 11, 10, 12, true, true);     
  if (error == 0) {
    Serial.println("Found Controller, configured successful");
    controller_type = ps2x.readType();
    if (controller_type==1) {
      Serial.println("DualShock Controller Found");
    } else {
      Serial.println("Unknown Controller type");
    }
  } else {
    Serial.println("No controller found, check wiring");
  }
  
  delay(1000);
}

void Flasher() {
  if (FlasherOn) {
    if (FlasherLeft) MShield.setPin(FlasherL, true);
    if (FlasherRight) MShield.setPin(FlasherR, true);
  }
  else {
    if (FlasherLeft) MShield.setPin(FlasherL, false);
    if (FlasherRight) MShield.setPin(FlasherR, false);
  }
  FlasherOn = !FlasherOn;
}

void loop() {
      
  if (autoParking) { // auto parking...
    // To Do...
    autoParking = false; // End parking procedure...
  } else {
    // Normal loop...
    driveCar(); // Main...

    if (WarningLights) { // Flashers on...
      FlasherRight = true;
      FlasherLeft = true;
      if ((millis()-lasttime) > 500) { // Flasher routine
        lasttime = millis();
        Flasher();
      }
    }
    else { // Flashers off...
      if (steerAngle < 90) { // Steering left
        FlasherLeft = true;
        FlasherRight = false;
        if ((millis()-lasttime) > 500) { // Flasher routine
          lasttime = millis();
          Flasher();
        }
      }
      else if (steerAngle > 90) { // Steering right
        FlasherRight = true;
        FlasherLeft = false;
        if ((millis()-lasttime) > 500) { // Flasher routine
          lasttime = millis();
          Flasher();
        }
      }
      else {
        FlasherRight = false;
        FlasherLeft = false;
        MShield.setPin(FlasherL, false); // Left flasher off
        MShield.setPin(FlasherR, false); // Right flasher off
        FlasherOn = false;
      }
    }

    // Brake light handling and auto-off...
    if (BrakeLights) {
      if (brakeOffTimer <= 0) brakeOffTimer = millis(); // Start timer...
      if ((millis()-brakeOffTimer) > 1000) { // Time-out brake lights...
        BrakeLights = false;
        brakeOffTimer = 0;
      } else {
        RedRearLight->setSpeed(255);         
      }
    } else {
      if (lightsOn)
        RedRearLight->setSpeed(REARLIGHT);
      else
        RedRearLight->setSpeed(0);
   }
 
  }
}

Bronnen

  1. Zoek simpelweg op internet op ‘wireless PS2 controller’ om diverse actuele aanbiedingen van deze ‘2.4G Wireless Dual Shock PS2 Game Controller‘ te vinden. De behuizing van de controller is in verschillende kleuren verkrijgbaar. Electronisch zal er waarschijnlijk weinig verschil zijn.
  2. Curious Inventor: Interfacing a PS2 (PlayStation 2) Controller https://store.curiousinventor.com/guides/PS2
  3. Bill Porter’s pagina over de PS2 interface voor de Arduino http://www.billporter.info/2010/06/05/playstation-2-controller-arduino-library-v1-0/
  4. PS Controller library voor Arduino https://github.com/madsci1016/Arduino-PS2X
  5. Arduino Sketch: Test van de vibratie-motoren. Ter download vanaf de ft:pedia site
  6. Arduino Sketch: Hoofdprogramma Flitzer. Ter download vanaf de ft:pedia site
  7. Filmpje YouTube whizzbizz.com: https://youtu.be/Rt7A9d0niPE