Joystick Shield met 2.4GHz-afstandsbediening

Onlangs heb ik voor een paar dollar een 'Funduino Joystick Shield V1.A' gekocht. Dit is een Arduino Uno-compatible shield waarmee je een gameconsole of robotcontroller kunt maken. Dit handige kleine schildje heeft een 2-assige analoge joystick met schakelaar, vier grote en twee extra kleine schakelaars (drukknoppen) op de print.

Voeg een Arduino en een manier van (draadloze) communicatie toe om een functionele afstandsbediening te krijgen en gamebesturing of handmatige robotbesturingstype functionaliteit aan een project toe te voegen.

Video : toestemming voor cookies nodig
Instellingen

De functies en kenmerken van dit joystick shield (afmetingen 87 x 53mm) zijn:

  • 2-assige analoge joystick met knop
  • 4 grote knoppen
  • 2 kleine knopjes
  • Bluetooth / seriële interface
  • I²C-interface
  • nRF24L01-interface
  • Nokia 5110 LCD-interface
  • Interface-aansluiting
  • Voltage-schakelaar voor instellen 3.3 of 5V
  • Twee zelf te gebruiken analoge in-/uitgangen
  • Indien de SPI-bus niet wordt gebruikt nog enkele vrije digitale in-/uitgangen
Joystick Shield

Voltage-instelling en joystick

Het shield heeft een schuifschakelaar waarmee je kunt selecteren of je het gebruikt met een 5V-board zoals een Arduino Uno of een 3.3V MCU zoals de Arduino Due. Zorg ervoor dat je het op het juiste voltage instelt voor het bord dat je gebruikt.

De X-as potentiometer van de dubbelassige joystick is verbonden met A0. De Y-as potentiometer is verbonden met A1. De analoge ingangen leveren waarden over een bereik van 0-1023 (voor typische 10-bits ADC-ingangen) op. De uitgelezen X-as- en Y-as-waarden zullen rond de 512 (middelpunt) zijn als de joystick in de middenstand staat. Als de joystick wordt bewogen, zullen deze waarden, afhankelijk van hoe de joystick wordt bewogen, varieren. Onder de joystick zit de  ‘K’ knop die wordt geactiveerd door de stick naar beneden te drukken. Deze knop hangt aan ingang D8.

Knoppen en gebruikte ingangen

Er zitten in totaal zes knopjes op het bord (exclusief die onder de joystick) met de labels A-F. De vier grote (blauw / gele) knoppen worden doorgaans gebruikt voor omhoog / omlaag / links / rechts of soortgelijke functies. De twee kleinere smd-knopjes lijken meer zinvol voor minder vaak gebruikte functies zoals ‘select’ of ‘start’, omdat ze minder toegankelijk zijn maar ook omdat ze minder snel per ongeluk worden ingedrukt. Alle knoppen hebben pull-up weerstanden en trekken de betreffende input naar massa (Ground) wanneer ze worden ingedrukt.

  • Knop A - Op ingang D2
  • Knop B - Op ingang D3
  • Knop C - Op ingang D4
  • Knop D - Op ingang D5
  • Knop E - Op ingang D6
  • Knop F - Op ingang D7
  • Joystickknop K - Op ingang D8

Extra connectoren en terminals

Het joystickbordje is voorzien van aparte Bluetooth / serieel, I²C, nRF24L01 en Nokia display connectoren.

De RX / TX-lijnen (respectievelijk D0 en D1) worden samen met 3,3 V en aarde naar een afzonderlijke 4-pins (female) aansluiting geleid. Dit kan worden gebruikt voor het aansluiten van een 4-pins 3.3V Bluetooth-module of TTL-serieel.

De I²C SDA- en SCL-lijnen (respectievelijk A4 en A5) worden samen met 5V en aarde naar een aparte 4-pins (male) header geleid. Dit is gewoon een extra aansluitmogelijkheid naast de normale A4 / A5-locatie van in-/uitgangen. Hierdoor kunnen I²C-apparaten eenvoudig worden aangesloten.

Met de 8-pins (dubbele rij 2x4 female) header aan de linkerkant van het bord kan een nRF24L01 RF-transceivermodule worden aangesloten. Deze 2,4 GHz-modules worden als volgt aangesloten:

  • GND - Ground/Aarde
  • VCC - 3.3V
  • CE - Op input D9
  • CSN - Op input D10
  • SCK - Op input D13
  • MOSI - Op input D11
  • MISO - Op input D12
  • IRQ - Geen verbinding, niet gebruikt

Dan is er de Nokia 5110 LCD-connector. Deze (female) headerconnector is ontworpen om een Nokia 5110 (PCD8544) LCD-scherm te monteren dat oorspronkelijk is ontworpen voor Nokia-telefoons en een matrix van 48 × 84 pixels biedt. Deze interface deelt dezelfde D9-D13-pinnen met de nRF24L01 connector, dus je kunt ze helaas niet beide tegelijk gebruiken.

Ten slotte biedt het bord een universele interfaceconnector. Deze gele (male) headerconnector met twee rijen biedt een alternatief toegangspunt voor alle knoppen, joystick-potten, 3,3 V, 5 V en aarde. De pin-out van deze connector is gedocumenteerd op het bord links van de connector. Ik weet niet zeker of dit erg handig zou kunnen zijn, misschien als je de status van knoppen wilt uitlezen met een ander apparaat of de bedieningselementen over meerdere borden wilt combineren? Hoewel een extra stroomvoorziening altijd handig kan zijn voor extra dingen die dan door dit boardje kunnen worden gevoed.

Weinig vrije in- en uitgangen...

Wie heeft meegeteld, zal net als ik concluderen dat er niet al te veel ongebruikte gewone in- en uitgangen voor eigen experimenten meer over zijn op het bordje. De optionele nRF24L01 of Nokia display delen de SPI-lijnen (D9 ~ D13) en kunnen daarom niet samen worden gebruikt. Alle andere (algemene) ingangen worden gebruikt als drukknop-ingangen, en twee analoge ingangen worden gebruikt om de joystickwaarden te lezen. Omdat A4 en A5 worden gedubbeld met I²C's  SDA (A4) en SCL (A5), blijven alleen A2 en A3 over voor snelle experimenten met b.v. LED's als je bijvoorbeeld de bidirectionele communicatie wil testen.

Een betere optie is om een I²C-display. Of toch het Nokia-display en de nRF24L01 in te ruilen voor bijvoorbeeld Bluetooth. Voor mijn eigen experimentje was een enkele LED voldoende, dus heb ik deze op A2 aangesloten.

nRF240L01 with LNA

De nRF24L01 module

Mijn eerste experiment-idee was het joystick shield te gebruiken voor radiobesturing van een kleine (fischertechnik-)buggy. En omdat het shield er al een interface voor heeft, met de nRF24L01 transceivermodule.

De nRF24L01+ transceivermodule is online verkrijgbaar voor minder dan twee dollar, en het leek een van de meest prijsgunstige bidirectionele datacommunicatie-opties die je kunt krijgen. Volgens de specificaties kun je er mee communiceren over een afstand van 100 meter. Hoewel dat meer dan genoeg is voor mijn simpele experimenten, werd ik voor (voor slechts enkele duppies meer) hebberig en mikte ik op het model met een externe antenne, Power Amplifier (PA) en Low-Noise Amplifier (LNA) dat in staat is om een ​​nog veel groter zendbereik van ongeveer 1000m te behalen.

De nRF24L01 transceivermodules werken in de 2,4 GHz ISM-frequentieband en gebruiken GFSK-modulatie voor de datatransmissie. De gegevensoverdrachtssnelheid kan 250 kbps, 1 Mbps of zelfs 2 Mbps zijn. Voor mijn test leek me 250kbps meer dan voldoende.

Het is een 'transceiver', dus de module kan als zender, maar ook als ontvanger werken. Hierdoor is het relatief eenvoudig met twee van deze modules een zeer betrouwbare en eenvoudige draadloze communicatie over de SPI-bussen van twee Arduino's op te zetten. Deze verbinding is vanuit de software op te vatten als een modem-verbinding. Wie meer technische details zoekt, bekijkt de datasheet of deze erg goede beschrijving.

De afstandsbediening

Door het joystick shield uit te rusten met een nRF24L01+ PA/LNA tranceivermodule, en wat te spelen met de beschikbare configuratieopties vanuit de software, lukte het relatief snel een betrouwbare handzender te maken. Zoals je op de foto (en in het filmpje) kunt zien, heb ik nog niet de moeite genomen er een mooie behuizing omheen te maken.

De stroomvoorziening is een 9V batterij in een compartiment met aan-/uit-schakelaar. Deze is met klittenband onderaan de print bevestigd. Ik heb een klein steunblokje gemaakt zodat de tranceivermodule niet compleet in de connector 'hangt' en wat steun ondervindt van de USB terminal. Voor een nRF24L01+ module zonder antenne lijkt me dat niet nodig.

Joystick Shield transmitter
// Arnoud, last day of 2020 and further :-)
// Based on 'Arduino Joystick shield Code': https://maker.pro/arduino/projects/funduino-arduino-joystick-shield-controlled-robot
//
#include "nRF24L01.h"
#include "RF24.h"
#include "SPI.h"

#define DEBUG

#define CE_PIN  9
#define CSN_PIN 10

#define RECEIVE_LED A2 // Proof of concept to show receiving on the transmitter gamepad

#define button_A  2 // Button Blue - A
#define button_B  3 // Button Yellow - B
#define button_C  4 // Button Blue - C 
#define button_D  5 // Button Yellow - D 
#define button_E  7 // SMD button E on pcb
#define button_F  6 // SMD button F on pcb
#define button_joystick 8 // Button in joystick
#define x_axis A0
#define y_axis A1
int buttons[]={ button_A, button_B, button_C, button_D, button_E, button_F, button_joystick };

byte address[][6] = {"pipe1", "pipe2"}; // Set addresses of the 2 pipes for read and write
RF24 radio(CE_PIN,CSN_PIN);
int joystick[9]; // Array holding state of buttons and joystick X- and Y-reading
int received_value; // Value retreived from the other side... 

void setup(){
  for (int i=0; i <7 ; i++) {
    pinMode(buttons[i], INPUT_PULLUP);
    digitalWrite(buttons[i], HIGH);  
  }
  pinMode(RECEIVE_LED, OUTPUT);
  analogWrite(RECEIVE_LED, 0);

#ifdef DEBUG
  Serial.begin(115200);
#endif 

  // Setup nRF240...
  radio.begin();
  radio.openWritingPipe(address[0]); // Open writing pipe to address pipe 1
  radio.openReadingPipe(1, address[1]); // Open reading pipe from address pipe 2
  radio.setDataRate(RF24_250KBPS);
  radio.setPALevel(RF24_PA_MIN); // Set RF power output to minimum: RF24_PA_MIN (change to RF24_PA_MAX if required) 
  radio.setRetries(3,5); // delay, count
  radio.setChannel(110); // Set frequency to channel 110
}

void loop(){
  // Read digital buttons...
  for (int i=0; i <7 ; i++)
    joystick[i] = digitalRead(buttons[i]);

  // Read joystick values...
  joystick[7] = analogRead(x_axis);
  joystick[8] = analogRead(y_axis);

  // Write out values array...
  radio.write(joystick, sizeof(joystick));
  delay(20);

#ifdef DEBUG
  // Log...
  for (int i=0; i <9 ; i++) {
    Serial.print(joystick[i]);
    if (i<8)
      Serial.print(", ");
  }
  Serial.print("\n");
#endif

  radio.startListening();
  if (radio.available()) { // Get remote transmission
    radio.read(&received_value, sizeof(received_value));
    if (received_value>10) {
      analogWrite(RECEIVE_LED, 255);
#ifdef DEBUG
      Serial.print("received_value=");
      Serial.println(received_value);
#endif
    } 
  } else
    analogWrite(RECEIVE_LED, 0);
  delay(20);
  radio.stopListening();
}

De Arduino Sketch voor het joystick shield

Beide Sketches gebruiken de RF24 library for Arduino. Uitgang A2 is gebruikt om de 'RECEIVE_LED' aan te sturen die vanaf de ontvanger kan worden geschakeld om het full-duplex karakter van de verbinding te testen. Er worden twee 'kanalen' met de namen "pipe1" en "pipe2" opgezet die afwisselend actief kunnen zijn.

De waarden van de zeven drukknoppen (vier grote, twee smd-type en de schakelaar onder de joystick) worden in een array gezet. De laatste twee waarden van het array zijn de X- en Y-waarde van de joystick die tussen 0 en 1023 kan liggen.

Datacommunicatie met de RF24 radio is in bytes, dus het kan in principe economischer, vooral voor het doorsturen van de binaire knop-waarden. Wie de boel nog verder wil optimaliseren zou deze zelfs in één byte kunnen samennemen.

nRF240L01 receiver

De ontvangende kant

Als ontvangende kant heb ik een Arduino Uno uitgerust met een prototype shield zodat ik enkele LEDs kon plaatsen om de verbinding en de, door het joystick shield, verzonden commando's te testen.

De mogelijkheden die ontstaan als twee of meer Arduino-boards draadloos over een afstand met elkaar kunnen communiceren leken me erg interessant. Het wordt dan mogelijk sensorgegevens op afstand te bewaken, robots aan te sturen en deze ook gegevens terug te laten sturen, enz. Als test heb ik daarom de drukschakelaar op het prototype shield gebruikt om een signaal terug te zenden en de LED op de joystick shield te laten oplichten. Deze communicatie terug is ook gelukt.

Natuurlijk zijn dit nog geen echt nuttige functies, maar dat was voor nu de bedoeling ook nog niet. Om het allemaal niet ál te experimenteel te maken ben ik nog wel een stapje verder gegaan en heb de joystick functioneel gemaakt voor het besturen van de buggy.

// Arnoud, last day of 2020 and first week of 2021 :-)
// Based on 'Arduino Car Code': https://maker.pro/arduino/projects/funduino-arduino-joystick-shield-controlled-robot
//
// Connect motors (Adafruit Motor Shield):
//    Left Motor: MotorL (M1)
//    Right Motor: MotorR (M2)

#include "nRF24L01.h"
#include "RF24.h"
#include "RF24_config.h"
#include "SPI.h"
#include "Adafruit_MotorShield.h"

#define DEBUG // Outcomment for stealth/live operation...

// nRF24L01 defines...
#define CE_PIN  9
#define CSN_PIN 10

// Various defines...
#define LED_A 8  // Button A
#define LED_B 7  // Button B
#define LED_C 6  // Button C
#define LED_D 5  // Button D
#define LED_E 3  // Button E
#define LED_F 4  // Button F
#define LED_K 2  // Joystick button
#define SEND_BUTTON A1 // Switch to test talking back to joystick shield...
#define MaxSpeed 255   // Max motor speed
#define safeRange 50   // Safe range to ignore round midpoint of joystick readings...
#define Baudrate 115200

int leds[] = { LED_A, LED_B, LED_C, LED_D, LED_E, LED_F, LED_K };
int sendbutton;
int joystick[9]; // Communications array holding state of buttons and joystick X- and Y-reading 
byte address[][6] = {"pipe1", "pipe2"}; // Set addresses of the 2 pipes for read and write
int dirMotorL = FORWARD; 
int dirMotorR = FORWARD;
int motorSpeedL = 0;
int motorSpeedR = 0;
int speedDiff = 0;

RF24 radio(CE_PIN,CSN_PIN);
Adafruit_MotorShield MShield = Adafruit_MotorShield(0x60);
Adafruit_DCMotor *MotorL = MShield.getMotor(1); // Left motor
Adafruit_DCMotor *MotorR = MShield.getMotor(2); // Right motor

void buggycontrol() { // Control the Buggy
  // Show feedback with LEDs and log to serial during debug...
  for (int i=0; i <9 ; i++) {
    if (i<7) {
     if (joystick[i]==1)
       digitalWrite(leds[i], LOW); 
     else
       digitalWrite(leds[i], HIGH); 
    }
#ifdef DEBUG
    Serial.print(joystick[i]);
    if (i<8)
      Serial.print(", ");
#endif
  }
#ifdef DEBUG
  Serial.print("\n");
#endif

  // Determine (signed/directional) speed based on Y-axis joystick value reading, 
  // ignore range around midpoint (theoretically 512) to prevent unwanted jitter...
  motorSpeedL = motorSpeedR = 0;
  if (joystick[8] < 512-safeRange) {
    motorSpeedL = map(joystick[8], 0, 512, -MaxSpeed, 0);
    motorSpeedR = motorSpeedL;
  } else if (joystick[8] > 512+safeRange) {
      motorSpeedL = map(joystick[8], 512, 1023, 0, MaxSpeed);
      motorSpeedR = motorSpeedL;
  }

  // Determine relative (signed/directional) curve-/turn-speed difference based on X-axis reading,
  // ignore range around midpoint (theoretically 512) to prevent unwanted jitter...
  speedDiff = 0;
  if (joystick[7] < 512-safeRange) {
    speedDiff = map(joystick[7], 0, 512, -255, 0);
  } else if (joystick[7] > 512+safeRange) {
      speedDiff = map(joystick[7], 512, 1023, 0, 255);
  }
  motorSpeedL = (motorSpeedL+speedDiff);
  motorSpeedR = (motorSpeedR-speedDiff);

  // Determine direction per motor...
  dirMotorL = FORWARD;
  dirMotorR = FORWARD;
  if (motorSpeedL<0) dirMotorL = BACKWARD; // Reverse...
  if (motorSpeedR<0) dirMotorR = BACKWARD; // Reverse...
  
  // Crop and absolutize motorspeeds...
  motorSpeedL = abs(motorSpeedL);
  motorSpeedR = abs(motorSpeedR);
  if (motorSpeedL>MaxSpeed) motorSpeedL = MaxSpeed;
  if (motorSpeedR>MaxSpeed) motorSpeedR = MaxSpeed;

#ifdef DEBUG
  Serial.print("\nmotorSpeedL = ");
  Serial.print(motorSpeedL);
  Serial.print(", motorSpeedR = ");
  Serial.print(motorSpeedR);
  Serial.print(", speedDiff = ");
  Serial.print(speedDiff);
  Serial.print(", dirMotorL=");
  Serial.print(dirMotorL);
  Serial.print(", dirMotorR=");
  Serial.print(dirMotorR);
  Serial.print("\n");
#endif

  MotorL->run(dirMotorL);
  MotorR->run(dirMotorR);
  MotorL->setSpeed(motorSpeedL);
  MotorR->setSpeed(motorSpeedR);
}

void setup() {
#ifdef DEBUG
  Serial.begin(Baudrate); // Start serial monitor...
#endif

  // Setup LEDs...
  pinMode(LED_A, OUTPUT);
  pinMode(LED_B, OUTPUT);
  pinMode(LED_C, OUTPUT);
  pinMode(LED_D, OUTPUT);
  pinMode(LED_E, OUTPUT);
  pinMode(LED_F, OUTPUT);
  pinMode(LED_K, OUTPUT);
  pinMode(SEND_BUTTON, INPUT_PULLUP);

  MShield.begin(); // Motor Shield initialize...
  Wire.setClock(400000); // Set I²C-Frequenz at 400 kHz
    
  // Initialize motors...
  MotorL->setSpeed(motorSpeedL);
  MotorR->setSpeed(motorSpeedR);
  
  // Setup nRF24L01 communication...
#ifdef DEBUG
    Serial.println("nRF24L01 setup");
#endif

  // Blink that we're alive: all LEDs on...
  digitalWrite(LED_A, HIGH);
  digitalWrite(LED_B, HIGH);
  digitalWrite(LED_C, HIGH);
  digitalWrite(LED_D, HIGH);
  digitalWrite(LED_E, HIGH);
  digitalWrite(LED_F, HIGH);
  digitalWrite(LED_K, HIGH);

  radio.begin();
  radio.openReadingPipe(1, address[0]); // Open reading pipe from address pipe 1
  radio.openWritingPipe(address[1]); // Open writing pipe to address pipe 2 
  radio.setDataRate(RF24_250KBPS);
  radio.setPALevel(RF24_PA_MIN); // Set RF power output to minimum: RF24_PA_MIN (change to RF24_PA_MAX if required) 
  radio.setRetries(3,5); // delay, count
  radio.setChannel(110); // Set frequency to channel 110
  
  delay(2000); // Signal power-up...

  // Blink: all LEDs off...
  digitalWrite(LED_A, LOW);
  digitalWrite(LED_B, LOW);
  digitalWrite(LED_C, LOW);
  digitalWrite(LED_D, LOW);
  digitalWrite(LED_E, LOW);
  digitalWrite(LED_F, LOW);  
  digitalWrite(LED_K, LOW);
  
  radio.startListening();
}
 
void loop() {
  if (radio.available()) { // Get remote transmission
    radio.read(joystick, sizeof(joystick));
    buggycontrol(); // Control the Buggy...
  }

  sendbutton = analogRead(SEND_BUTTON);
  if (sendbutton<100) {
    radio.stopListening(); 
    radio.write(&sendbutton, sizeof(sendbutton)); // Send state to other Arduino board 
#ifdef DEBUG
    Serial.print("SEND_BUTTON=");
    Serial.println(sendbutton);
#endif
    radio.startListening();
  }
}

De afstandsbediende Buggy

Ik gebruikte de Buggy als praktisch voorbeeld van een nuttige besturing. Eerder had ik die met een infrarood afstandsbediening bestuurbaar gemaakt maar, aangezien de joystickbesturing veel preciezer en analoog is, moest de software voor de aansturing van de beide motoren worden herschreven.

De x-as joystick regelt de verschillen in loopsnelheid tussen beide motoren. Omdat het richtingsverschil op deze wijze dus wordt gesuperponeerd, is het nu mogelijk om bij elke draaisnelheid- of richting heel precieze bochten te nemen. Ook is het hierdoor mogelijk zonder voorwaartse snelheid de buggy op zijn plek te laten draaien. Beide motoren lopen dan even snel, maar in tegengestelde richtingen.

Omdat in de Buggy reeds een motorshield werd gebruikt, heb ik hieronder de aansluitingen daarop getekend. Vanzelfsprekend kan de nRF24L01 ook direct op de corresponderende poorten van een Arduino Uno worden aangesloten zonder motorshield.

 

Buggy + remote nRF24
nRF240L01 wiring

Experiment geslaagd

Het geheel nodigt uit tot verder experimenteren. Zo zal ik de mogelijkheden van Bluetooth en het aansturen van een klein display op het joystick shield (met bijvoorbeeld sensor-data van het bestuurde model) zeker nog gaan onderzoeken.

Wellicht is het de moeite waard om een robuustere behuizing te maken van bijvoorbeeld een paar plaatjes plexiglas. Maar ook nu al is het joystick shield erg handig om eenvoudig gamebesturing of handmatige robotbesturingstype functionaliteit aan een project toe te voegen. Voor de meeste besturing zijn één joystick en enkele aanvullende schakelaars meer dan voldoende.

De communicatie met de nRF24L01 module bleek, na wat mislukte experimenten, uiteindelijk vrij eenvoudig op te zetten. Ik vond echter bij het zoeken naar voorbeelden van bidirectionele communicatie erg weinig goede voorbeelden. De door mij gemaakte Arduino Sketches vindt u hierbij ter download. Houd me op de hoogte van uw eigen verbeteringen! Veel plezier!