Exemple de communication sans fil nRF24L01+ (dans les 2 sens)

Bonsoir

dans une file de discussion un membre du forum avait besoin d'un code d'exemple de communication sans fil bi-directionnelle entre 2 arduino

J'ai donc créé ce petit code d'exemple.

Voici à quoi le montage ressemble

2 x Arduino UNO
2 x Membrane 4 boutons
2 x (2 Leds + 2 résistances de limitation de courant adéquates)
2 x (NRF24L01+ avec leur support adaptateur de tension 5V)

L'idée est toute simple, on met le même code sur les 2 arduinos mais le code va se configurer dynamiquement en fonction de la valeur de la Pin A0. si elle est à GND vous aurez le rôle 0, si elle est à une autre valeur alors vous avez le rôle 1.


Connexions depuis les composants vers l'arduino

Membrane 4 boutons. elle a 5 connecteurs:
1 --> GND (le fil blanc qui part de la gauche du connecteur de la membrane sur la photo)
2 --> D3 (le fil gris)
3 --> D4 (le fil violet)
4 --> D5 (le fil bleu)
5 --> D6 (le fil vert)

NRF24L01+ avec leur support adaptateur de tension:
VCC --> 5V
GND --> GND
CE --> D7
CSN --> D8
SCK --> D13
MOSI (MO) --> D11
MISO (MI) --> D12
IRQ --> Non connecté

les Leds et résistances

GND --> Résistance 220Ω --> Cathode --> LED 1 --> Anode --> Pin D9
GND --> Résistance 220Ω --> Cathode --> LED 2 --> Anode --> Pin D10

Configuration du logiciel
Arduino Pin A0 --> GND sur le premier Arduino (rôle = 0)
Arduino Pin A0 --> 3.3V sur le second Arduino (rôle = 1) (j'ai pris 3.3V car la pin 5V était prise :slight_smile: )


L'idée du code de démonstration est la suivante

Vous pressez un bouton de 1 à 4 d'un côté, la valeur 1 à 4 est envoyée à l'autre Arduino qui fait une action.
1 = leds OFF OFF
2 = leds ON OFF
3 = leds OFF ON
4 = leds ON ON

Une fois l'action exécutée, l'arduino renvoie une confirmation sous forme d'un octet 100+valeur ce qui nous permet de confirmer que l'action demandée a été effectuée.

Pour simplifier au maximum le code, j'utilise la librairie OneButton qui va gérer pour vous l'appui sur les boutons, le bouncing etc. (cf mon tuto de programmation par machine à états où j'en parle un peu)

Le setup() configure tout ce qu'il faut
la loop() est toute simple:

  • elle regarde si un bouton est appuyé en si oui envoie le message
  • elle écoute si un message est arrivé et si oui fait l'action associée

Voici le code

// ************* La Radio *************
#include <SPI.h>
#include <RF24.h> // voir http://tmrh20.github.io/RF24/

// Configurer vos radio nRF24L01+ sur le bus SPI et mettre  CE sur D7 et CSN sur D8
RF24 radio(7, 8);

// Le nom des "pipes" de communication, un en lecture, un en écriture
const byte adresses[][6] = {"0pipe", "1pipe"}; // Pipes 1-5 should share the same address, except the first byte. Only the first byte in the array should be unique

// A CONFIGURER sur la pin A0
// si A0 est à GND alors rôle = 0 --> le premier Arduino
// si A0 est à  3.3V ou 5V alors rôle = 1 --> pour le second
const byte configurationPin = A0;
uint8_t role;


// ****************** Les Boutons ******************
// library = https://github.com/mathertel/OneButton
// documentation = http://www.mathertel.de/Arduino/OneButtonLibrary.aspx

#include <OneButton.h>

OneButton bouton1(4, true); // true pour le mettre en INPUT_PULLUP
OneButton bouton2(3, true);
OneButton bouton3(6, true);
OneButton bouton4(5, true);

// *************  Deux Leds avec résitances de limiation de courant *************
const byte pinLed0 = 9;
const byte pinLed1 = 10;


// ----------------------------------------------------------------------------------------
void bouton1Click()
{
  Serial.println(F("Bouton 1 local, envoi du message 1"));
  envoyerMessage((uint8_t) 1);
}

void bouton2Click()
{
  Serial.println(F("Bouton 2 local, envoi du message 2"));
  envoyerMessage((uint8_t) 2);
}

void bouton3Click()
{
  Serial.println(F("Bouton 3 local, envoi du message 3"));
  envoyerMessage((uint8_t) 3);
}

void bouton4Click()
{
  Serial.println(F("Bouton 4 local, envoi du message 4"));
  envoyerMessage((uint8_t) 4);
}

// ----------------------------------------------------------------------------------------

void verifierBoutons()
{
  bouton1.tick();
  bouton2.tick();
  bouton3.tick();
  bouton4.tick();
}

// ----------------------------------------------------------------------------------------
// envoi d'un octet vers l'autre radio
// ----------------------------------------------------------------------------------------

void envoyerMessage(uint8_t nombre)
{
  radio.stopListening();   // On arrête d'écouter pour qu'on puisse émettre

  if (!radio.write( &nombre, sizeof(nombre) )) {
    Serial.println(F("erreur d'envoi"));
  }
  radio.startListening(); // On se remet en mode écoute
}

// ----------------------------------------------------------------------------------------
// vérifie si on a reçu une commande de la part de l'autre radio (1 octet)
// ----------------------------------------------------------------------------------------
uint8_t ecouterRadio()
{
  uint8_t message = 0; // 0 = pas de commande

  if ( radio.available()) {
    while (radio.available()) {
      radio.read( &message, sizeof(message) );  // on lit l'octet reçu (si plusieurs messages on ne conserve que le dernier)
    }
    Serial.print(F("J'ai recu ")); Serial.println(message);
  }
  return message;
}

// ----------------------------------------------------------------------------------------

void executerAction(uint8_t messageRecu)
{
  if (messageRecu >= 1 && messageRecu <= 4) {
    switch (messageRecu) {
      case 1:
        Serial.println(F("Action Bouton 1 distant"));
        digitalWrite(pinLed0, LOW);
        digitalWrite(pinLed1, LOW);
        break;
      case 2:
        Serial.println(F("Action Bouton 2 distant"));
        digitalWrite(pinLed0, HIGH);
        digitalWrite(pinLed1, LOW);
        break;
      case 3:
        Serial.println(F("Action Bouton 3 distant"));
        digitalWrite(pinLed0, LOW);
        digitalWrite(pinLed1, HIGH);
        break;
      case 4:
        Serial.println(F("Action Bouton 4 distant"));
        digitalWrite(pinLed0, HIGH);
        digitalWrite(pinLed1, HIGH);
        break;
    }
    // on envoie un accusé de réception qui vaut 100 + la valeur du message
    envoyerMessage((uint8_t) 100 + messageRecu);
  } else if (messageRecu >= 100) { // c'est un ACK d'une action distante
    Serial.print(F("Confiramtion Execution action distante Bouton "));
    Serial.println(messageRecu - 100);
  }

}

// ------------------------------------------------------------------
// ------------------------------------------------------------------
// ------------------------------------------------------------------

void setup() {
  pinMode(pinLed0, OUTPUT);
  pinMode(pinLed1, OUTPUT);
  pinMode(A0, INPUT);

  Serial.begin(115200);

  role = (digitalRead(configurationPin) == LOW) ? 0 : 1 ;
  Serial.print(F("\nMon Role = ")); Serial.println(role);

  // On configure la radio
  radio.begin();
  // pour le test on règle le niveau d'énergie à RF24_PA_LOW pour éviter les interférences
  // mettre à RF24_PA_MAX si on veut la puissance d'émission max
  radio.setPALevel(RF24_PA_LOW);

  // On ouvre un pipe de lecture et un d'écriture avec des noms opposés en fonction du rôle
  // comme ça un parle sur "pipe0" et l'autre écoute sur "pipe0"
  // et l'autre parle sur "pipe1" tandisque Le premier écoute sur "pipe1"

  radio.openWritingPipe(adresses[role]); // role doit être 0 ou 1
  radio.openReadingPipe(1, adresses[1 - role]); // 1 - role = l'autre adresse

  // Start the radio listening for data
  radio.startListening();

  // On attache la fonction boutonXClick() comme callBack en cas de simple click rapide
  // ou si le bouton est tenu longtemps appuyé (au relachement)
  // il existe 3 callback attachLongPressStart, attachLongPressStop, attachDuringLongPress
  bouton1.attachClick(bouton1Click);  bouton1.attachLongPressStop(bouton1Click);
  bouton2.attachClick(bouton2Click);  bouton2.attachLongPressStop(bouton2Click);
  bouton3.attachClick(bouton3Click);  bouton3.attachLongPressStop(bouton3Click);
  bouton4.attachClick(bouton4Click);  bouton4.attachLongPressStop(bouton4Click);
}

// ------------------------------------------------------------------

void loop() {
  uint8_t messageRecu;
  verifierBoutons(); // regarde si les boutons sont enfoncés et déclenche une action dans ce cas

  if (messageRecu = ecouterRadio()) // si on a reçu un message
    executerAction(messageRecu);    // on execute l'action associée
}

Pour tester

  • chargez le code sur 2 arduinos configurés comme décrit plus haut (avec A0 soit à GND ou 3.3V)
    J'utilise cela pour définir le rôle de l'arduino - ces rôles ne servent qu'à définir sur quel canal de communication l'arduino va écouter et parler. il faut que l'un parle sur un canal et que l'autre écoute sur ce canal et inversement. Cette variable role me permet donc de différencier les 2 arduinos (le code est identique dans cet exemple sur les 2 arduinos puisque les rôles sont symétriques).

  • Si vous n'avez qu'un seul ordinateur l'IDE Arduino ne sait pas ouvrir 2 moniteurs Série donc j'utilise une appli tierce (sur PC prenez Putty, sur Mac CoolTerm par exemple)

Paramètrez vos moniteurs série (115200 bauds) sur les 2 ports correspondant aux ports USB de chaque arduino

Si tout va bien vous devez voir le role de chacun des arduinos s'afficher
Si j'appuie sur le bouton 2 du côté droit. Le moniteur de droite me dit
Bouton 2 local, envoi du message 2
l'Arduino de gauche reçoit le message et affiche
J'ai recu 2
Action Bouton 2 distant
allume une LED puis envoie l'ack sous forme de 100+2 = 102
l'Arduino de droite reçoit le message et me dit
J'ai recu 102
Confiramtion Execution action distante Bouton 2

Si J'appuie maintenant sur le bouton 4 sur l'arduino de gauche. Le moniteur de gauche me dit
Bouton 4 local, envoi du message 4
l'Arduino de droite reçoit le message et affiche
J'ai recu 4
Action Bouton 4 distant
L'Arduino de droite allume les 2 LED puis envoie l'ack sous forme de 100+4 = 104
l'Arduino de gauche reçoit le message et affiche
J'ai recu 104
Confiramtion Execution action distante Bouton

voilà - bons bidouillages !

EDIT: Je vous laisse changer la faute d'orthographe dans le code => Confirmation pas Confiramtion

Bonsoir J-M-L
sympa , ça va permettre à des "débutants" ne pas etre "rebutés" :grin: par la connexion des NRF24

Suggestion :

  • mettre dans le .ino en commentaire le lien vers la lib RF24 ?
    dans mon esprit
  • pas de commentaire concernant une lib , elle est fournie "de base" avec l'IDE
  • commentaire , le lien est celui utilisé par l'auteur

Ok bon point - C'est ajouté

if (messageRecu = ecouterRadio()) // si on a reçu un message

Bonjour,
c'est juste ?

Oui c'est correct

Une expression d'affectation du genre a = b met b dans a et s'évalue à b. Donc if (a=b) {} met b dans a et fait le test comme si on avait écrit if (b) {} qui en C/C++ veut dire "si b est non nul alors ..."

Donc dans mon cas ci dessus, ça appelle la fonction ecouterRadio(), qui retourne la valeur indiquant quelle touche a été appuyée sur l'autre arduino (ou confirmation de lecture) ou 0 si pas de message. Cette valeur est stockée dans la variable messageRecu et le if est exécuté et donc ça veut dire "si ecouterRadio() est non nul" et je peux ensuite utiliser la variable messageRecu dans la suite car elle a été initialisé - ça fait d'une pierre deux coups. j'aurais pu écrire

messageRecu = ecouterRadio();
if (messageRecu  != 0) ... // si on a reçu un message

À ne pas confondre avec == qui testerait l'égalité entre les 2 expressions de chaque côté du ==

OK?

ok, merci ! :confused:

Bel exemple, mais pour simplifier, pas besoin de la pin CE, ni de différencier les deux montages via un "rôle" (pipeline dans le nrf) et pin A0. C'est valable pour 2 (ou plus) montages.

pour CE c'est une bonne pratique avec la librairie RF24 puisque attendu dans le constructeur

La doc dit:

RF24 makes use of the standard hardware SPI pins (MISO,MOSI,SCK) and requires two additional pins, to control the chip-select and chip-enable functions.
These pins must be chosen and designated by the user, in RF24 radio(ce_pin,cs_pin); and can use any available pins.

Pour le rôle - même si je concède que le terme est effectivement mal choisi - c'est plus uniquement un différentiateur qui est nécessaire pour attribuer les pipes symétriques en lecture et écriture

Oui la doc de la lib.

Pour ma part je fait sans différencier les montages (symétriques comme tu dis) via une petite astuce que j'ai trouvé.

N'hésitez pas à partager !

En fait lors de la création de cette classe qui permet de faire fonctionner le nRF24L01+, je me suis fixé plusieurs contraintes:

  • N'utiliser que les broches spi (SS, MOSI, MISO, SCK) comme les autres périphériques en spi que j'ai l'habitude de programmer (pour rester cohérent).
  • N'utiliser que du spi hardware (comme le reste de la bibliothèque).
  • Le nRF24L01+ sera en mode réception tout le temps, sauf au moment d'envoyer une info.
  • Tout le monde discute sur le même pipeline (pipe 0) (donc théoriquement (même si c'est pas vrai) pas de limite en nombre de nrf possibles), seul un tri est fait à l'arrivée pour distinguer qui s'adresse à qui.

C'est une façon de faire dont j'ai pris la décision pour que au niveau de la couche au dessus (l'utilisateur qui programme avec ma classe) cela soit très facile, et qu'il y ait toujours d'une façon transparente l'idée de toile (réseau de nrf) sans poser aucun soucis malgré que je n'utilise qu'un pipeline, et sans irq.

J'avais fini de programmer cette classe, j'essaye donc de tester à peu prêt tout ce qu'il est possible à ma portée pour voir la robustesse du code, montages loufoques (10ène de ping pong avant d'afficher une info sur un écran), communication sans antenne à travers des murs en béton, etc...
Lorsque j'ai souhaité tester une symétrique parfaite, 2 montages identiques avec le même programme mis sous tension simultanément; Comme quoi les cadences des microcontrôleurs et différents systèmes embarqués sont précises; la communication est alors devenue bien saccadée, voir impossible certains instants (les 2 (ou plus) nrf se retrouvant simultanément en réception ou en transmission).

J'ai essayé pas mal d'idées qui me sont venues en faisant des tests, l'accusé de réception notamment avec le registre 01 (auto acknowledgement), ou encore le nombre de retry lorsque le récepteur ne répond pas, mais aucun d'eux n'a fonctionné... sauf un: le registre 07 (STATUS) d'une grande aide.

Grâce au bit 6 (RX_DR) de ce registre: Data Ready RX FIFO interrupt. Asserted when new data arrives RX FIFO c

Donc, quand l'utilisateur décides dans son code de transmettre une information avec le nrf, la fonction fait d'abord une lecture de ce registre et de ce bit avant d'engager les blocs de code relatifs à la transmission.

//lecture du registre STATUS

if (//si le bit 6 est à 0)
{
	//attendre 1 milliseconde
}

Cette attente (arbitraire) permet lorsque 2 montages sont à leur allumage parfaitement (avec une certaine tolérance je vous l'accorde) synchronisés, de les désynchroniser. Si besoin ce code reste la tout au long de l'utilisation, et donc même si on imagine qu'une resynchronisation qu'elle qu'elle soit referait surface (à 2 ou plus nrf), cette petite logique d'attente s'activerait et redésynchronisait les montages.

Cette attente peut être difficile à comprendre car elle est effective sur les 2 montages (ou plus) en symétrie, mais via la lecture du registre, elle vient en fait chercher (la petite bête) des chouillemes temporels pour créer une plus grosse différence d'1 millisecondes entre les montages. On peux dire que quelques micro ou nanosecondes se retrouvent (si besoin) affublés d'1 milliseconde pour créer de façon significative une différence (ou décalage) quelque part.

C'est pas compliqué du tout mais ça fonctionne super.

Encore une fois ceci n'est effectif que si besoin (lors du démarrage des montages la plupart du temps), le reste du temps ce petit bout de code est bypassé naturellement, et je dois vous avouer que c'est la seule et unique fois ou j'ai dû dans la programmation (de ma bibliothèque) rajouter un temps arbitraire qui ne soit pas clairement mentionné dans un datasheet de composant électronique...

Les pipes partagent la même bande de fréquence donc oui c'est une approche possible empirique qui fonctionne, qui ne perd pas vraiment en fonctionnalité - Cette approche déplace simplement à un niveau d'abstraction plus élevé la décision d'à qui on parle. (Votre approche avec des composants capables de gérer des pipes sur plusieurs bandes RF et gérer de l'agrégation ou plusieurs antennes perdrait alors en efficacité)

Si vous utilisez des pipes au niveau hardware vous bénéficiez de ce qui est déjà implémenté dans le silicone - donc on ne réinvente pas la roue, c'est efficace au niveau puissance aussi et plus "propre" - mais si vous voulez aller plus loin qu'un seul canal alors vous êtes un peu coincé. (de nombreux systèmes auront un canal de discussion et un canal de contrôle par exemple)

Une petite remarque — c'est dommage de perdre une milliseconde complète, il existe des algos anti-collision qui se basent sur une fonction random (avec des délais qui augmentent) qui créera donc des attentes différentes sur les différents participants et donner l'opportunité à l'un d'entre eux de prendre la parole.

Oui je comprends bien, après c'est un choix que j'ai fait, mais tout est possible et envisageable ça c'est sûr.

J'ai également pensé à l'aléatoire, mais encore une fois cette milliseconde n'est "perdue" que lorsqu'il y a symétrie au démarrage des systèmes.

Ah j'avais cru comprendre que vous testiez à chaque "prise de parole"

Notez que pour le bit 6 (RX_DR) dont vous parlez il sera mis à high après une réception (adresse du pipe correcte et CRC correct) donc en fait la transmission d'un packet est déjà terminée...

Si vous ne le faites qu'une seule fois et que votre but est de désynchroniser des processeurs qui ensuite pourraient être trop synchrones, autant mettre un delay aléatoire dans le main()/setup()

Notez que les collision dans cette puce sont gérées par le Enhanced ShockBurst et donc en cas de collision vous obtiendrez dynamiquement cette désynchronisation (Enhanced ShockBurst = ack des paquets et retransmission si nécessaire et d'ailleurs notez que donc on n'est pas obligé de se cantonner à du hardware SPI car il y a moins de pression sur le micro-controlleur)

les 6 data pipe en réception multiple ont vraiment été rajoutés dans la nRF24L01+ pour permettre des réseau en étoile 1:6 - mon point de vue était, puisque l'on voulait avoir que 2 arduinos - d'utiliser la fonction matérielle

Bonjour

  • Si vous n'avez qu'un seul ordinateur l'IDE Arduino ne sait pas ouvrir 2 moniteurs Série donc j'utilise une appli tierce (sur PC prenez Putty, sur Mac CoolTerm par exemple)

Pour programmer 2 arduinos sur le même ordinateur, il suffit d'ouvrir plusieurs fois l'IDE en passant par le menu démarrer, ce qui permettra de configurer deux cartes différentes sur des port différents et donc, de pouvoir tout faire en double sans devoir changer le port à chaque fois.
NE PAS UTILISER le menu ouvrir de l'IDE sinon, ça ne fonctionne pas.

Truc que j'utilise tout le temps

A+

Ce n'est pas forcément une bonne idée d'avoir deux process accédant les mêmes fichiers de préférences ...

Je n'ai jamais eu le moindre problème à faire comme ça.
Je n'avais pas non plus le même fichier source à envoyer sur 2 arduinos où la je changerais effectivement le port. Mais i j'avais a le faire pour programmer ET debugguer en même temps. Je passerais en mode "editeur externe" type notepad++ et j'utiliserais la technique du double lancement pour avoir les deux debugger em même temps.
Utiliser un logiciel externe type Putty me forcerais a toujours devoir aller déconnecter le port série avant chaque reprogrammation puis le reconnecter pour faire le debug. Des deux solutions, je préfère la mienne, mais a chacun de choisir.
A+

Utiliser un logiciel externe type Putty me forcerais a toujours devoir aller déconnecter le port série avant chaque reprogrammation puis le reconnecter pour faire le debug. Des deux solutions, je préfère la mienne, mais a chacun de choisir.

oui effectivement pour cela c'est un bon point - mais vous allez avoir 2 process qui écrivent dans le fichier des préférences éventuellement, c'est là où ça peut "coincer"

Si on ne modifie pas les préférences, ça ne sauvegarde que le port COM de la dernière IDE quittée ainsi que le type de carte. Cela ne m'a jamais posé de problème.
A si, ça m'ouvre aussi toutes les fenetres des sources qui étaient ouvertes dans des IDE différentes et ceci pour chaque instance de l'ide si quand j'ai quitté, il y avait plusieurs sources d'ouverts.