Go Down

Topic: Exemple de communication sans fil nRF24L01+ (Read 11644 times) previous topic - next topic

J-M-L

May 30, 2017, 08:20 pm Last Edit: Jun 08, 2018, 12:48 pm by J-M-L
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 :) )





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
Code: [Select]
// ************* 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
}
Hello - Please do not PM me for help,  others will benefit as well if you post your question publicly on the forums.
Bonjour Pas de messages privés SVP, postez dans le forum directement pour que ça profite à tous

J-M-L

#1
May 30, 2017, 08:21 pm Last Edit: Jun 08, 2018, 12:50 pm by J-M-L
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
Hello - Please do not PM me for help,  others will benefit as well if you post your question publicly on the forums.
Bonjour Pas de messages privés SVP, postez dans le forum directement pour que ça profite à tous

Artouste

Bonsoir J-M-L
sympa , ça va permettre à des "débutants" ne pas etre "rebutés"  :smiley-mr-green:  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

J-M-L

#3
May 30, 2017, 11:16 pm Last Edit: May 30, 2017, 11:23 pm by J-M-L
Ok bon point - C'est ajouté
Hello - Please do not PM me for help,  others will benefit as well if you post your question publicly on the forums.
Bonjour Pas de messages privés SVP, postez dans le forum directement pour que ça profite à tous

trimarco232

Quote
if (messageRecu = ecouterRadio()) // si on a reçu un message
Bonjour,
c'est juste ?

J-M-L

#5
May 31, 2017, 07:12 am Last Edit: May 31, 2017, 09:30 am by J-M-L
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
Code: [Select]
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?
Hello - Please do not PM me for help,  others will benefit as well if you post your question publicly on the forums.
Bonjour Pas de messages privés SVP, postez dans le forum directement pour que ça profite à tous

trimarco232


sylvainmahe

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.
www.sylvainmahe.site

J-M-L

#8
May 31, 2017, 01:30 pm Last Edit: May 31, 2017, 01:40 pm by J-M-L
pour CE  c'est une bonne pratique avec la librairie RF24 puisque attendu dans le constructeur

La doc dit:

Quote
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

Hello - Please do not PM me for help,  others will benefit as well if you post your question publicly on the forums.
Bonjour Pas de messages privés SVP, postez dans le forum directement pour que ça profite à tous

sylvainmahe

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é.
www.sylvainmahe.site

J-M-L

Hello - Please do not PM me for help,  others will benefit as well if you post your question publicly on the forums.
Bonjour Pas de messages privés SVP, postez dans le forum directement pour que ça profite à tous

sylvainmahe

#11
Jun 02, 2017, 11:12 am Last Edit: Jun 02, 2017, 11:25 am by sylvainmahe
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.

Code: [Select]
//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...

www.sylvainmahe.site

J-M-L

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.

Hello - Please do not PM me for help,  others will benefit as well if you post your question publicly on the forums.
Bonjour Pas de messages privés SVP, postez dans le forum directement pour que ça profite à tous

sylvainmahe

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.
www.sylvainmahe.site

J-M-L

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
Hello - Please do not PM me for help,  others will benefit as well if you post your question publicly on the forums.
Bonjour Pas de messages privés SVP, postez dans le forum directement pour que ça profite à tous

Go Up