La partie manette se compose d'un Arduino Uno et d'un Joystick Shield de chez Funduino, et la communication se fait via des modules 2.4GHz à base de nRF24L01+.
Elle est alimentée par une batterie au Lithium de 3.7V. Le récepteur est lui aussi construit autour d'une carte Arduino.
Celui qui est présenté ici est basé sur une Arduino Nano, et trois ponts en H à base de BTS7960. Ces derniers sont idéaux pour alimenter les moteurs de visseuse électrique. Tout ce matériel se trouve facilement et à vil prix sur eBay

Voici le code de la manette :
Code : Tout sélectionner
#include <SPI.h>
#include "RF24.h"
#include "printf.h"
/** Brochage de la radio nRF24L01+ sur le Joystick Shield
* Signal nRF pin Arduino pin
* GND 1 GND
* VCC 2 3V3
* CE 3 9
* CSn 4 10
* SCK 5 13
* MOSI 6 11
* MISO 7 12
* IRQ 8 -
*/
const byte nRF_CE = 9;
const byte nRF_CSn = 10;
RF24 nRF(nRF_CE, nRF_CSn);
/* Configuration des adresses pour la communication radio
* Ces adresses doivent être définies identiques entre la
* manette et le robot pour que la communication fonctionne...
*/
/* Adresse de la radio montée sur le robot */
const byte nRF_robot_address[6] = "1Node";
/* Adresse de la radio montée sur la télécommande */
const byte nRF_joystick_address[6] = "2Node";
/* Valeur des axes et boutons de la manette
* Voir le code du robot pour le détail des champs...
*/
struct joystick_state {
byte buttons;
int axis_x;
int axis_y;
} joystate;
const byte button_mask_up = 0x01;
const byte button_mask_right = 0x02;
const byte button_mask_down = 0x04;
const byte button_mask_left = 0x08;
const byte button_mask_start = 0x10;
const byte button_mask_select = 0x20;
const byte button_mask_joystick = 0x40;
void read_joystick()
{
/* Lecture des boutons du joystick
* Les boutons sont attribués de la façon suivante :
* Bouton Broche Arduino Broche Atmega
* ---------------------------------------
* UP 2 PD2
* RIGHT 3 PD3
* DOWN 4 PD4
* LEFT 5 PD5
* START 6 PD6
* SELECT 7 PD7
* JOYSTICK 8 PB0
*
* Comme les 6 premiers boutons sont situés sur le même port,
* on lit la valeur de celui-ci et on décale de deux bits
* vers la droite, comme ça le bouton UP se retrouve sur le
* bit 0, le bouton RIGHT sur le bit 1, et ainsi de suite.
* Les boutons sont actifs à l'état bas (reliés à GND), ainsi
* on inverse la valeur du port pour convertir en logique
* positive. Ainsi, le bit correspondant à un bouton est mis à
* 1 dans <buttons> si le bouton est pressé
*/
joystate.buttons = (~PIND >> 2) & 0x7F;
/* Le bouton situé sous le joystick est relié à la broche 0
* du port B. On le traite donc séparément des précédents.
* Si la broche physique est à 1, on met le bit correspondant
* à 0 dans <buttons>, car le bouton n'est pas pressé.
*/
if (PINB & 0x01)
joystate.buttons &= ~button_mask_joystick;
/* Lecture de la position des sticks analogiques */
/* Axe horizontal du joystick */
joystate.axis_x = analogRead(A0);
/* Axe vertical du joystick */
joystate.axis_y = analogRead(A1);
}
void setup() {
Serial.begin(115200);
Serial.println("nRF24L01+ Joystick");
printf_begin();
/* Initialisation des ports du joystick
* Ports en entrées, activer les pull-ups
*/
DDRD &= 3;
PORTD |= 0xFC;
DDRB &= 0xFE;
PORTB |= 0x01;
/* Initialisation de la radio nRF24L01 */
nRF.begin();
nRF.openWritingPipe(nRF_robot_address);
nRF.openReadingPipe(1, nRF_joystick_address);
nRF.printDetails();
}
void loop() {
/* Lecture de l'état du joystick */
read_joystick();
/* Ecrit l'état du joystick dans la console, pour info */
Serial.print("X = ");
Serial.print(joystate.axis_x);
Serial.print("\tY = ");
Serial.print(joystate.axis_y);
Serial.print("\tButtons = ");
if (joystate.buttons & button_mask_up) Serial.print("UP ");
if (joystate.buttons & button_mask_right) Serial.print("RIGHT ");
if (joystate.buttons & button_mask_down) Serial.print("DOWN ");
if (joystate.buttons & button_mask_left) Serial.print("LEFT ");
if (joystate.buttons & button_mask_start) Serial.print("START ");
if (joystate.buttons & button_mask_select) Serial.print("SELECT ");
if (joystate.buttons & button_mask_joystick) Serial.print("JOYSTICK ");
/* Envoie l'état du joystick au robot
* Si le robot ne répond pas dans le délai imparti,
* affiche un message d'erreur dans la console
*/
if (!nRF.write(&joystate, sizeof(struct joystick_state))) {
Serial.println(F("(Not Ack'd from bot)"));
}
else
Serial.println("(OK)");
/* Attente de 20ms avant le prochain envoi */
delay(20);
}
Mis à part les adresses (voir plus bas), il n'est pas nécessaire de modifier le reste du code pour se construire un ensemble manette/récepteur
Le code du récepteur est le suivant :
Code : Tout sélectionner
#include <SPI.h>
#include "RF24.h"
#include "printf.h"
/* Brochage de la radio nRF24L01+ */
const byte nRF_CE = 9;
const byte nRF_CSn = 10;
RF24 nRF(nRF_CE, nRF_CSn);
/* Adresse de la radio montée sur le robot */
const byte nRF_robot_address[6] = "1Node";
/* Adresse de la radio montée sur la télécommande */
const byte nRF_joystick_address[6] = "2Node";
/* Valeur des axes et boutons de la manette */
struct joystick_state {
/* <buttons> stocke l'état des 7 boutons de la manette dans 1 octet.
* chaque bit de <buttons> est mis à 1 si le bouton correspondant est pressé
* Par exemple, pour vérifier si le bouton du haut est pressé, on utilise :
* if (joystate.buttons & button_mask_up)
* {
* ...
* }
*/
byte buttons;
/* <axis_x> est la valeur analogique de l'axe horizontal du joystick
* La valeur est comprise entre 0 et 1023
* 0 : stick à gauche
* 1023 : stick à gauche
* ~512 : stick au centre
*/
int axis_x;
/* <axis_y> est la valeur analogique de l'axe vertical du joystick
* La valeur est comprise entre 0 et 1023
* 0 : stick en bas
* 1023 : stick en haut
* ~512 : stick au centre
*/
int axis_y;
} joystate;
/* Masques binaires pour décoder la valeur <buttons> de <joystate> */
const byte button_mask_up = 0x01; /* Bouton haut ou A */
const byte button_mask_right = 0x02; /* Bouton droit ou B */
const byte button_mask_down = 0x04; /* Bouton bas ou C */
const byte button_mask_left = 0x08; /* Bouton gauche ou D */
const byte button_mask_start = 0x10; /* Bouton start ou E */
const byte button_mask_select = 0x20; /* Bouton select ou F */
const byte button_mask_joystick = 0x40; /* Bouton sous le stick */
/* Classe permettant de piloter simplement un moteur relié via un pont en H */
class motor {
private:
byte pin_A, pin_B, pin_PWM;
public:
/* Initialise une instance de la classe <moteur>
* <pin_A> correspond à la commande du moteur dans un sens
* <pin_B> correspond à la commande du moteur dans l'autre sens
* <pin_PWM> est le signal de modulation de largeur d'impulsion
* et permet de faire varier la vitesse du moteur
* Pour la connexion à un pont chinois à base de BTS7960, le cablage
* est le suivant :
* <pin_A> et <pin_B> sont respectivement reliés à L_PWM et R_PWM
* (ou R_PWM et L_PWM si on souhaite inverser le sens de rotation)
* <pin_PWM> est reliée à L_EN et R_EN mis ensemble
*/
motor(byte pin_A, byte pin_B, byte pin_PWM){
this->pin_A = pin_A;
this->pin_B = pin_B;
this->pin_PWM = pin_PWM;
}
/* Initialise les broches de commande du moteur en tant que sorties
* Cette fonction est de préférence appelée dans setup()
*/
void begin() {
pinMode(pin_A, OUTPUT);
pinMode(pin_B, OUTPUT);
pinMode(pin_PWM, OUTPUT);
stop();
}
/* Arrête le moteur
* Les deux branches du pont sont déconnectées, le moteur est en roue libre
*/
void stop() {
digitalWrite(pin_A, 0);
digitalWrite(pin_B, 0);
digitalWrite(pin_PWM, 0);
}
/* Définit la vitesse de rotation du moteur
* <value> est un entier compris entre -255 et +255
* Si <value> dépasse ces valeurs, <value> sera borné à -255 ou +255
*/
void setPower(int value) {
if (value > 0) {
digitalWrite(pin_A, HIGH);
digitalWrite(pin_B, LOW);
analogWrite(pin_PWM, min(value, 255));
} else if (value < 0) {
digitalWrite(pin_A, LOW);
digitalWrite(pin_B, HIGH);
analogWrite(pin_PWM, min(-value, 255));
} else {
digitalWrite(pin_A, LOW);
digitalWrite(pin_B, LOW);
analogWrite(pin_PWM, 0);
}
}
};
/* Moteur gauche
* pin_A sur broche 2
* pin_B sur broche 4
* pin_PWM sur broche 3
*/
motor left_motor(2,4,3);
/* Moteur droit
* pin_A sur broche 7
* pin_B sur broche 8
* pin_PWM sur broche 6
*/
motor right_motor(7,8,6);
void setup() {
Serial.begin(115200);
Serial.println("nRF24L01+ Robot");
printf_begin();
/* Initialisation de la radio nRF24L01 */
nRF.begin();
nRF.openWritingPipe(nRF_joystick_address);
nRF.openReadingPipe(1, nRF_robot_address);
nRF.printDetails();
nRF.startListening();
/* Initialisation des commandes de moteur */
left_motor.begin();
right_motor.begin();
}
/* Crée une zone morte sur <inval>
* si <inval> est compris entre -<thres> et +<thres>, renvoie 0
* sinon, retranche <thres> à <inval> (ajoute si <inval> < 0)
*/
int deadZone(int inval, int thres)
{
if (inval > thres) {
return inval - thres;
} else if (inval < -thres) {
return inval + thres;
} else {
return 0;
}
}
void loop() {
/* Date à laquelle on a reçu le dernier message de la télécommande.
* Cette valeur est en millisecondes depuis le démarrage de l'Arduino
* (obtenu avec la fonction millis())
*/
static uint32_t last_joystick_time = 0;
/* Teste si l'on a reçu un message provenant de la télécommande */
while (nRF.available()) {
/* Oui, alors on extrait la structure de donnée du message reçu */
nRF.read(&joystate, sizeof(struct joystick_state));
/* Ecrit l'état de la manette dans la console, pour info */
Serial.print("X = ");
Serial.print(joystate.axis_x);
Serial.print("\tY = ");
Serial.print(joystate.axis_y);
Serial.print("\tButtons = ");
if (joystate.buttons & button_mask_up) Serial.print("UP ");
if (joystate.buttons & button_mask_right) Serial.print("RIGHT ");
if (joystate.buttons & button_mask_down) Serial.print("DOWN ");
if (joystate.buttons & button_mask_left) Serial.print("LEFT ");
if (joystate.buttons & button_mask_start) Serial.print("START ");
if (joystate.buttons & button_mask_select) Serial.print("SELECT ");
if (joystate.buttons & button_mask_joystick) Serial.print("JOYSTICK ");
Serial.println("");
/* Met à jour la date de dernière bonne réception */
last_joystick_time = millis();
/* Vitesse d'avance/recul du robot
* Définie à partir de l'axe vertical du joystick
* On retire 512 à <axis_y> pour avoir 0 au centre
* On divise par 2 pour avoir au maximum +/- 256
*/
int throttle = deadZone(joystate.axis_y - 512, 16) / 2;
/* Vitesse de rotation du robot
* Définie à partir de l'axe horizontal du joystick
* On retire 512 à <axis_x> pour avoir 0 au centre
* On divise par 3 pour limiter la vitesse de rotation du robot
*/
int steering = deadZone(joystate.axis_x - 512, 16) / 3;
/* Définit la vitesse de rotation des moteurs gauche et droit
* Le signal <throttle> est une action commune
* Le signal <steering> est une action différentielle
*/
left_motor.setPower(throttle + steering);
right_motor.setPower(throttle - steering);
}
/* Si l'on n'a pas reçu de message de la télécommande depuis plus de 500 ms */
if (millis() - last_joystick_time > 500)
{
Serial.println("No joystick data !");
/* On arrête les moteurs */
left_motor.stop();
right_motor.stop();
delay(100);
}
}
En l'absence de données reçues (perte de signal, plus de batterie dans la télécommande, etc.), les moteurs sont arrêtés pour ne pas avoir un robot hors de contrôle.
Pour compiler le code, il est nécessaire d'installer la bibliothèque RF24 dans l'environnement Arduino. On peut la trouver à l'adresse suivante :
http://tmrh20.github.io/RF24/
Ou dans les versions récentes de l'IDE Arduino :
Croquis -> Inclure une bibliothèque -> Gérer les bibliothèques, puis rechercher "RF24" et l'installer.
Pour le câblage du module nRF24L01+, se référer à la documentation de la bibliothèque RF24.
Les adresses utilisées par les modules nRF24L01+ sont définies par l'extrait de code suivant :
Code : Tout sélectionner
/* Adresse de la radio montée sur le robot */
const byte nRF_robot_address[6] = "1Node";
/* Adresse de la radio montée sur la télécommande */
const byte nRF_joystick_address[6] = "2Node";

Dans le code du récepteur, une classe motor permet de gérer facilement les moteurs. Elle s'utilise de la façon suivante :
- On créée l'objet mon_moteur de la façon suivante :
broche_sens_A et broche_sens_B définissent le sens de rotation du moteur. Si les deux sont à 0 ou à 1, le moteur est arrêté (bloqué). Si une des deux broches est à 1, le moteur peut tourner.
Code : Tout sélectionner
motor mon_moteur(broche_sens_A,broche_sens_B,proche_PWM);
broche_PWM est la broche permettant de régler la vitesse du moteur. Elle doit correspondre à une sortie PWM du moteur (broches 3,5,6,9 et 10 de l'Arduino Uno, la broche 11 étant utilisée par le module radio).
Pour les modules à base de BTS7960, les entrées R_EN et L_EN sont reliées ensemble sur la broche PWM, tandis que les broches R_PWM et L_PWM sont reliées aux broches sens_A et sens_B. - On intialise le moteur avec la commande mon_moteur.begin(), dans la fonction setup().
- Pour définir la vitesse et le sens de rotation, on utilise la commande mon_moteur.setPower(valeur), où valeurest la vitesse souhaitée entre -255 et 255.
- Pour arrêter le moteur, on utilise la fonction mon_moteur.stop().
La boucle principale (fonction loop()) du programme récepteur comporte deux blocs d'instruction
- Le bloc commençant par while (nRF.available()) contient les instructions à exécuter lorsque l'on reçoit un message de la télécommande.
C'est dans celui-ci que l'on pilote nos moteurs de déplacement, en utilisant les valeurs des axes du joystick. On peut rajouter à la suite le pilotage d'autres moteurs, de sorties tout ou rien, etc. - Le bloc commençant par if (millis() - last_joystick_time > 500) contient les instructions à exécuter lorsque l'on n'a plus reçu de message de la télécommande depuis un certain temps
Il contient les instructions nécessaire à mettre le robot en sécurité : arrêt des moteurs, blocage des armes, etc.