Micro contrôleurs AVR/Travail pratique/Télécommande NRF24L01 pour Robot
Le but de ce TP est de partir d'un châssis existant de robot composé d'une carte de puissance (de type L298N) et d'une carte de commande qui sera composée d'un Arduino Nano placé sur une plaque à essai et qui sera connecté au L298N et au composant radio NRF24L01.
L'objectif de ce TP est donc de faire réaliser une télécommande (sur plaque à essais). Cela nécessite un Joystick qui fait partie des périphériques Arduino que l'on peut trouver pour quelques Euros. La partie robotique sera elle réalisée avec un châssis de votre choix,des moteurs de votre choix, un L298N et pour finir le module NRF24L01. Tous ces modules sont extrêmement bon marchés.
Nous vous présenterons le matériel que nous avons utilisé mais vous devez adapter le matériel à votre budget.
Introduction
modifierNous allons dans ce TP réaliser une télécommande pour un robot à l'aide du circuit NRFL01. C'est un composant qui est fabriqué par une entreprise norvégienne Nordic Semiconductor qui permet de transmettre des données par liaison radio. Le gros intérêt de ce composant est son prix (6 € pour 10 pièces en Chine). Son concurrent le plus connu, XBee, frôle plutôt les 20€, même en Chine.
Ce composant NRF24L01 est un module radio intégrant tout le nécessaire pour émettre et recevoir des données sur la gamme de fréquences de 2,4 GHz (comme le WiFi ou le Bluetooth) en utilisant le protocole de communication propriétaire de Nordic nommé "ShockBurst".
L'intérêt de ce TP est une révision assez importante des concepts présentés dans ce cours. D'autre part, comme le composant NRF24L01 est relativement configurable, vous serez amené à lire un peu de sa documentation. Il vous sera ainsi possible de le configurer correctement pour un fonctionnement particulier : un émetteur et un récepteur.
Ce TP consiste donc à réaliser logiciellement et matériellement deux parties qui communiquent. Mettre en œuvre les deux parties simultanément n'est pas spécialement simple. Ainsi les enseignants intéressés par ce TP devront fournir une partie réceptive opérationnelle qui sort ce qu'elle reçoit sur la liaison série. Cela permettra de valider la partie émission lorsqu'elle sera opérationnelle.
La partie châssis sera laissée à l'initiative des enseignants. Nous avons utilisé nous-même un châssis robot assez volumineux, mais vous pouvez utiliser un châssis de dimension plus restreinte.
Vous pouvez concevoir ces deux parties avec des plaques à essais pour laisser les étudiants câbler (complètement ou partiellement).
Vous pouvez aussi remplacer le châssis Robot par quelque chose de plus simple : deux servomoteurs ou tout autre idée.
Il existe des librairies sur Internet pour réaliser ce TP. Nous proposons ici de construire complètement l'ensemble. Le code qui en résultera ne sera pas adaptable à d'autres architectures car trop dépendant du matériel ! Nous avons par exemple réussi à faire fonctionner la librairie MIRF sans changement avec un ARM Texas Instrument dans l'environnement Energia ! Le code de ce TP ne sera pas aussi général.
Nous allons donc commencer par la partie révision qui utilise donc des chapitres précédents de ce livre.
Liaison série
modifierLa difficulté de vérifier que ce qui est envoyé est bien ce qui est reçu nécessite l'utilisation d'une liaison série. Elle est évidemment disponible si vous utilisez une programmation de type Arduino (avec un setup() et un loop()). Mais rappelons encore une fois qu'il est possible d'utiliser l'environnement Arduino pour faire du C standard : il suffit d'écrire un main() en lieu et place du setup() et du loop(). Ainsi, dès qu'un main() est détecté dans un programme l'environnement sait qu'il doit travailler sans les librairies Arduino. Nous n'avons alors plus accès au "Serial.print()" de l'Arduino. Dans ce chapitre nous allons donc écrire des fonctions RS232 simples. Nous aurons besoin de celles-ci essentiellement pour déboguer.
Voir aussi le chapitre correspondant dans ce cours.
Réalisation d'une transmission série à 9600 bauds
modifierLa transmission série n'a pas besoin d'être rapide puisqu'elle servira seulement à vérifier le bon fonctionnement de chacune des parties : émetteur (la télécommande) et récepteur le robot. C'est pour cela qu'on a choisi 9600 bauds. Cette valeur peut naturellement être augmentée.
Réalisation de l'initialisation de la liaison série
modifierLe code donné ailleurs dans ce cours sera notre point de départ :
#include <avr/io.h>
#define F_CPU 16000000 // 16 MHz oscillator.
#define BaudRate 9600
#define MYUBRR (F_CPU / 16 / BaudRate ) - 1
void serialInit(void) {
//Serial Initialization
/*Set baud rate 9600 */
UBRR0H = (unsigned char)(MYUBRR>>8);
UBRR0L = (unsigned char) MYUBRR;
/* Enable receiver and transmitter */
UCSR0B = (1<<RXEN0)|(1<<TXEN0);
/* Frame format: 8data, No parity, 1stop bit */
UCSR0C = (3<<UCSZ00);
}
Exercice 1
modifierPouvez-vous écrire la configuration du registre UCSR0C de manière plus traditionnelle en désignant tous les bits configurés à 1 ?
UCSR0C = ((1<<UCSZ01) | (1<<UCSZ00)); // seulement deux bits à configurer
Les deux bits UCSZ01 et UCSZ00 sont configurés à 1.
Sous-programme d'émission d'un octet
modifierNous donnons le programme un peu modifié de la section sur la liaison série. On utilise les macros présentées ici qui à priori sont plus faciles à utiliser pour les étudiants.
#include <avr/sfr_defs.h>
void serialWrite(uint8_t DataOut)
{
loop_until_bit_is_set(UCSR0A,UDRE0); // wait while NOT ready to transmit
UDR0 = DataOut;
}
Transformation d'un octet en décimal
modifierLa liaison série est supposée envoyer des caractères affichables. Si vous prenez un octet venant de n'importe quoi et que vous l'envoyez sur la liaison série vous risquez d'avoir une surprise. C'est pour cela que dans le chapitre sur la liaison série a été développé le sous-programme suivant :
void usart_puts_hexa(uint8_t val) {
uint8_t tab[2];
tab[0] = val >> 4; //poids fort
tab[1] = val & 0x0F; //poids faible
if (tab[0] < 10) tab[0] += '0'; else tab[0] += '7';
if (tab[1] < 10) tab[1] += '0'; else tab[1] += '7';
serialWrite(tab[0]); // poids fort en premier
serialWrite(tab[1]); // puis poids faible
}
qui transforme un octet en sa valeur hexadécimale.
Exercice 2
modifierLes étudiants ayant des difficultés (normales) avec l'hexadécimal, nous allons plutôt utiliser le décimal.
- Écrire un sous-programme d'affichage d'un octet en décimal en s'inspirant (vaguement) de l'écriture hexadécimale.
- Écrire un sous-programme d'affichage de deux octets (uint16_t) en décimal. On rappelle pour ceux qui se poseraient la question de savoir pourquoi deux octets, que les valeurs données par le convertisseur analogique numérique (donc le joystick) sont sur deux octets (10 bits pour être exact).
#include <avr/io.h>
#include <avr/sfr_defs.h>
#include <util/delay.h>
#define F_CPU 16000000 // 16 MHz oscillator.
#define BaudRate 9600
#define MYUBRR (F_CPU / 16 / BaudRate ) - 1
void serialInit(void) {
//Serial Initialization
/*Set baud rate 9600 */
UBRR0H = (unsigned char)(MYUBRR>>8);
UBRR0L = (unsigned char) MYUBRR;
/* Enable receiver and transmitter */
UCSR0B = (1<<RXEN0)|(1<<TXEN0);
/* Frame format: 8data, No parity, 1stop bit */
UCSR0C = ((1<<UCSZ01)|(1<<UCSZ00)); // seulement deux bits à configurer
}
void serialWrite(uint8_t DataOut) {
loop_until_bit_is_set(UCSR0A,UDRE0); // while NOT ready to transmit
UDR0 = DataOut;
}
void serialWrite8Deci(uint8_t DataOut) {
uint8_t tab[3];
tab[0] = DataOut / 100 + '0';
tab[1] = ((DataOut / 10) % 10) + '0';
tab[2] = (DataOut % 10) + '0';
serialWrite(tab[0]); // centaine
serialWrite(tab[1]); // dizaine
serialWrite(tab[2]); // unité
}
void serialWrite16Deci(uint16_t DataOut) {
uint8_t tab[4]; // on se limite à 4 car CAN ne dépasse pas 1023
tab[0] = DataOut / 1000 + '0';
tab[1] = ((DataOut / 100) % 10) + '0';
tab[2] = ((DataOut /10)% 10) + '0';
tab[3] = (DataOut % 10) + '0';
serialWrite(tab[0]); // millier
serialWrite(tab[1]); // centaine
serialWrite(tab[2]); // dizaine
serialWrite(tab[3]); // unité
}
void usart_puts_hexa(uint8_t val) {
uint8_t tab[2];
tab[0] = val >> 4; //poids fort
tab[1] = val & 0x0F; //poids faible
if (tab[0] < 10) tab[0] += '0'; else tab[0] += '7';
if (tab[1] < 10) tab[1] += '0'; else tab[1] += '7';
serialWrite(tab[0]); // poids fort en premier
serialWrite(tab[1]); // puis poids faible
}
int main() {
uint16_t cmpt=987;
// setup
serialInit();
// loop
while (1) {
cmpt++;
serialWrite16Deci(cmpt);serialWrite(0x0D);serialWrite(0x0A);
_delay_ms(1000);
}
}
Conversion Analogique Numérique
modifierLa télécommande est réalisée avec une manette joystick qui donne une information analogique. Plus exactement, deux informations analogiques, une pour l'axe x et une pour l'axe y.
La conversion analogique numérique a déjà été traitée dans ce livre au chapitre 11.
Vous pouvez aussi trouver sur Internet un tutoriel pour utiliser un joystick 2 axes. Il est programmé en langage Arduino mais nous voulons nous l'utiliser en langage C.
Exercice 3
modifierCherchez un code d'utilisation du convertisseur analogique/numérique dans le au chapitre 11 et transformez le simplement en deux sous-programmes :
- void ADC_Init(void)
- uint16_t ADC_get(uint8_t channel)
On pourra naturellement câbler un joystick pour des tests qui pourront être validés par la liaison série déjà réalisée.
On utilisera A0 et A1 pour les deux axes du Joystick. Ces entrées sont compatibles avec le branchement du futur NRF24L01.
#include <avr/io.h>
#include <avr/sfr_defs.h>
#include <util/delay.h>
#define F_CPU 16000000 // 16 MHz oscillator.
#define BaudRate 9600
#define MYUBRR (F_CPU / 16 / BaudRate ) - 1
void serialInit(void) {
//Serial Initialization
/*Set baud rate 9600 */
UBRR0H = (unsigned char)(MYUBRR>>8);
UBRR0L = (unsigned char) MYUBRR;
/* Enable receiver and transmitter */
UCSR0B = (1<<RXEN0)|(1<<TXEN0);
/* Frame format: 8data, No parity, 1stop bit */
UCSR0C = ((1<<UCSZ01)|(1<<UCSZ00)); // seulement deux bits à configurer
}
void serialWrite(uint8_t DataOut) {
loop_until_bit_is_set(UCSR0A,UDRE0); // while NOT ready to transmit
UDR0 = DataOut;
}
void serialWrite8Deci(uint8_t DataOut) {
uint8_t tab[3];
tab[0] = DataOut / 100 + '0';
tab[1] = ((DataOut / 10) % 10) + '0';
tab[2] = (DataOut % 10) + '0';
serialWrite(tab[0]); // centaine
serialWrite(tab[1]); // dizaine
serialWrite(tab[2]); // unité
}
void serialWrite16Deci(uint16_t DataOut) {
uint8_t tab[4]; // on se limite à 4 car CAN ne dépasse pas 1023
tab[0] = DataOut / 1000 + '0';
tab[1] = ((DataOut / 100) % 10) + '0';
tab[2] = ((DataOut /10)% 10) + '0';
tab[3] = (DataOut % 10) + '0';
serialWrite(tab[0]); // millier
serialWrite(tab[1]); // centaine
serialWrite(tab[2]); // dizaine
serialWrite(tab[3]); // unité
}
void ADC_Init(void) {
// Choose AVCC pin for the comparison voltage
ADMUX = (1 << REFS0) ;
// Start the ADC unit,
// set the conversion cycle 16 times slower than the duty cycle
ADCSRA = (1 << ADEN) | (1 << ADPS2);
}
uint16_t ADC_get(uint8_t channel) {
// Choose channel in the multiplexer
ADMUX &= 0xF0; // (MUX3,MUX2,MUX1,MUX0) = (0,0,0,0)
ADMUX |= (channel & 0x0F) << MUX0; //on veut channel<16
// on lance la conversion
ADCSRA |= (1 << ADSC);
// on attend qu'elle soit finie
loop_until_bit_is_clear(ADCSRA, ADSC);
// ici le bit ADSC vient de passer à 0
return ADC;
}
int main() {
uint16_t joystick_x,joystick_y;
// setup
serialInit();
ADC_Init();
// loop
while (1) {
joystick_x = ADC_get(0);
joystick_y = ADC_get(1);
serialWrite16Deci(joystick_x);
serialWrite(' ');serialWrite('-');serialWrite(' ');
serialWrite16Deci(joystick_y);
serialWrite(0x0D);serialWrite(0x0A);
_delay_ms(1000);
}
}
Notez que la relation entre le numéro de channel et le nombre à mettre dans le multiplexeur de sélection ne sont pas toujours identiques (voir l'Arduino Leonardo).
Liaison SPI
modifierLa liaison SPI ne nous est pas inconnue non plus. Il nous faudra cependant écrire des fonctions plus pratiques que celles qui ont été faites dans le chapitre 7. Nous allons partir d'un code donné et l'adapter au problème qui nous intéresse, à savoir, l'écriture et la lecture des registres du NRF24L01.
Exercice 4
modifierReprendre le code du chapitre 7 en transformant la boucle d'attente en choisissant entre loop_until_bit_is_set et loop_until_bit_is_clear. On en profitera pour changer SS en CSN (du NRF24L01) qui pour nous sera en broche 8 Arduino (PB0).
Comme nous n'avons pas de périphérique SPI pour tester notre code, nous avons décidé d'utiliser un Arduino UNO en esclave SPI. Le code donné dans la documentation officielle du SPI de l'ATMega328 ne fonctionnait pas correctement. Nous pensons seulement y avoir ajouté du code pour mettre SPIF à 0. La liaison série est pour le débogage.
//SPI slave
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sfr_defs.h>
#define F_CPU 16000000 // 16 MHz oscillator.
#define BaudRate 9600
#define MYUBRR (F_CPU / 16 / BaudRate ) - 1
void serialInit(void) {
//Serial Initialization
/*Set baud rate 9600 */
UBRR0H = (unsigned char)(MYUBRR>>8);
UBRR0L = (unsigned char) MYUBRR;
/* Enable receiver and transmitter */
UCSR0B = (1<<RXEN0)|(1<<TXEN0);
/* Frame format: 8data, No parity, 1stop bit */
UCSR0C = ((1<<UCSZ01)|(1<<UCSZ00)); // seulement deux bits à configurer
}
void serialWrite(uint8_t DataOut) {
loop_until_bit_is_set(UCSR0A,UDRE0); // while NOT ready to transmit
UDR0 = DataOut;
}
void serialWrite8Deci(uint8_t DataOut) {
uint8_t tab[3];
tab[0] = DataOut / 100 + '0';
tab[1] = ((DataOut / 10) % 10) + '0';
tab[2] = (DataOut % 10) + '0';
serialWrite(tab[0]); // centaine
serialWrite(tab[1]); // dizaine
serialWrite(tab[2]); // unité
}
//SPI init
void SPISlaveInit(void) {
uint8_t data;
//set MISO as output
DDRB |= (1<<PB4);
//enable SPI and enable SPI interrupt
SPCR |= (1<<SPE)|(1<<SPIE);
SPCR &= ~(1<<MSTR); //passage en mode esclave
// clear SPIF
data = SPSR;
data = SPDR;
}
ISR(SPI_STC_vect) {
serialWrite8Deci(SPDR);
serialWrite(0x0D);
serialWrite(0x0A);
}
int main(void) {
//initialize slave SPI
SPISlaveInit();
// liaison série 9600 bauds
serialInit();
serialWrite('a');
serialWrite('b');
serialWrite(0x0D);
serialWrite(0x0A);
serialWrite8Deci(157);
serialWrite(0x0D);
serialWrite(0x0A);
sei();
while (1) {
//loop
}
return 0;
}
Faire du code pour un SPI maître a été encore plus difficile. Nous avons très vite fait fonctionner du code Arduino comme maître pour le code esclave que l'on vient tout juste de présenter. Voici le code en question :
#include <SPI.h>
#define CS 7
uint8_t cmpt = 0;
void setup()
{
Serial.begin(9600);
SPI.begin(); // initialisation du port SPI
SPI.setDataMode(SPI_MODE0); // configuration de la liaison SPI en mode 0
SPI.setClockDivider(SPI_CLOCK_DIV16); // configuration de l'horloge à 1MHz
pinMode(CS, OUTPUT);
delay(1);
digitalWrite(CS, HIGH);
}
void loop() {
digitalWrite(CS, LOW); // activation de la ligne CS
delayMicroseconds(15); // voir doc: pause de 15us après l'activation de la ligne CS
SPI.transfer(cmpt++);
Serial.println(SPCR,BIN);
digitalWrite(CS, HIGH);
//Serial.println(" ");
delay(1000);
}
Dès que l'on passait en C direct plus rien ne fonctionnait. Quel était le problème ?
Après trois jours de travail autour du "loop_until_bit_is_set(SPSR, SPIF);" qui bloquait tout, nous avons décidé d'envoyer le registre SPCR à travers une liaison série pour nous apercevoir que le bit MSTR de ce registre n'était pas positionné à 1 malgré une écriture à 1 !!!
C'est vrai que dans la documentation officielle du SPI il y a cette phrase un peu sibylline en anglais : "This bit selects Master SPI mode when written to one, and Slave SPI mode when written logic zero. If SS is configured as an input and is driven low while MSTR is set, MSTR will be cleared, and SPIF in SPSR will become set. The user will then have to set MSTR to re-enable SPI Master mode."
Nous avons réussi à résoudre le problème de deux façons :
- Relier le bit PB2 du PORTB à 5V : ce bit correspond à SS (Slave Select) qui pour moi n'avait d'importance que pour un esclave SPI.
- Écrire un 1 dans ce bit qui est par défaut en entrée, ce qui le fait donc passer en pull-up.
Nous sommes estomaqué par la première solution purement électrique ! Mais nous n'utiliserons que la deuxième qui est plus générale. Depuis l'écriture de ces lignes nous avons décortiqué plusieurs librairies qui n'utilisent pas notre technique mais passent le bit PB2 en sortie avec en commentaire qu'il peut être utilisé de manière générale (mais en sortie).
Voici donc le code final dans lequel nous avons laissé la liaison série qui nous a permis de déboguer.
#include <avr/io.h>
#include <avr/sfr_defs.h>
#include <util/delay.h>
#define F_CPU 16000000 // 16 MHz oscillator.
#define BaudRate 9600
#define MYUBRR (F_CPU / 16 / BaudRate ) - 1
//SPI init
void SPIMasterInit(void) {
uint8_t data;
//set MOSI, SCK and CSN as output
DDRB |= ((1<<PB3)|(1<<PB5));
DDRD |= (1<<PD7); // CSN
PORTB |= (1<<PB2); //set SS to high (pullup) TRES TRES IMPORTANT MEME SI PAS UTILISE COMME SLAVE SELECT
_delay_ms(1); // peut être un peu long
//enable master SPI at clock rate Fck/16
SPCR |= ((1<<SPE)|(1<<MSTR)|(1<<SPR0)); // MSTR ne passe PAS!!!!!!!!!!!!!!!
//set CSN to high
PORTD |= (1<<PD7);
_delay_ms(1); // peut être un peu long
// clear SPIF
data = SPSR;
data = SPDR;
}
//master send function
void SPIMasterSend(uint8_t data){
//select slave
//CSN to LOW
PORTD &= ~(1<<PD7);
_delay_us(15);
//send data
SPDR=data;
asm volatile("nop");
//wait for transmition complete
loop_until_bit_is_set(SPSR, SPIF);
//CSN to high
PORTD |= (1<<PD7);
}
void serialInit(void) {
//Serial Initialization
/*Set baud rate 9600 */
UBRR0H = (unsigned char)(MYUBRR>>8);
UBRR0L = (unsigned char) MYUBRR;
/* Enable receiver and transmitter */
UCSR0B = (1<<RXEN0)|(1<<TXEN0);
/* Frame format: 8data, No parity, 1stop bit */
UCSR0C = ((1<<UCSZ01)|(1<<UCSZ00)); // seulement deux bits à configurer
}
void serialWrite(uint8_t DataOut) {
loop_until_bit_is_set(UCSR0A,UDRE0); // while NOT ready to transmit
UDR0 = DataOut;
}
void serialWrite8Deci(uint8_t DataOut) {
uint8_t tab[3];
tab[0] = DataOut / 100 + '0';
tab[1] = ((DataOut / 10) % 10) + '0';
tab[2] = (DataOut % 10) + '0';
serialWrite(tab[0]); // centaine
serialWrite(tab[1]); // dizaine
serialWrite(tab[2]); // unité
}
int main() {
uint8_t cmpt = 0;
// setup()
SPIMasterInit();
//serialInit();
// loop()
while(1) {
SPIMasterSend(cmpt++);
//serialWrite8Deci(SPCR);
//serialWrite('-');
//serialWrite8Deci(SPSR);
//serialWrite(0x0D);
//serialWrite(0x0A);
_delay_ms(1000);
}
return 0;
}
Le code donné en correction n'est plus utilisable de manière générale puisque SS (Slave Select) est utilisé sur PD7 de manière systématique. Par contre vous pouvez l'adapter facilement pour un autre positionnement de SS. Nous avons choisi PD7 comme nous aurions pu utiliser n'importe quelle autre broche ! On rappelle que seule l'utilisation du SPI en esclave impose une broche précise pour le SPI (PB2 pour l'ATMega 328).
Au vu des problèmes rencontrés et exprimés dans la solution ci-dessus, nous conseillons à tout lecteur d'utiliser la broche PB2 (du PORTB) comme Slave Select en maître dès que vous le pouvez. Cela vous évitera normalement les ennuis associés au fait qu'à elle toute seule cette broche peut vous empêcher de passer en maître SPI ! Nous ne suivons pas ce conseil uniquement parce que nous allons utiliser une télécommande que nous n'avons pas réalisé. Le Slave Select nous est donc imposé.
Exercice 5
modifierNous allons maintenant continuer à spécialiser le SPI pour réaliser le protocole SPI du NRF24L01. L'architecture vue du SPI est la suivante :
- des registres caractérisés par une adresse dans lesquels on peut écrire une ou plusieurs valeurs et que l'on peut lire aussi bien sûr.
- des instructions qui sont des valeurs sur 3 bits de poids fort qui définissent le type d'action :
- R_REGISTER 0x00 : lecture d'un registre
- W_REGISTER 0x20 : écriture dans un registre
Les 5 bits de poids faible représentent alors l'adresse du registre.
Ainsi l'envoi de 0x21 comme première donnée précise qu'il s'agit d'une écriture à cause du 0x20 et dans le registre EN_AA qui a l'adresse 1. Cette donnée doit être immédiatement suivie par la valeur que l'on souhaite écrire dans le registre EN_AA.
Ainsi le logiciel capable d'utiliser le NRF24L01 aura les possibilités suivantes :
- Écriture d'une seule donnée (octet) dans la majorité des registres du NRF24L01. Un registre est caractérisé par une adresse (sur 5 bits et une valeur sur un octet. En clair, il serait intéressant d'avoir un sous-programme de prototype uint8_t SPI_NRF24_W_Reg(uint8_t reg, uint8_t value) capable d'écrire "value" dans le registre d'adresse "reg".
- Lecture de la valeur d'un registre de prototype uint8_t SPI_NRF24_R_Reg(uint8_t reg).
- Écriture d'une donnée pouvant atteindre 32 octets comme les adresses de réception et d'émission, mais aussi des données à transmettre. Un prototype du type uint8_t SPI_NRF24_W_Reg_Buf(uint8_t reg, uint8_t *pBuf, uint8_t bytes) serait parfait.
- Lecture d'un ensemble de données pouvant atteindre 32 octets ayant comme prototype uint8_t SPI_NRF24_R_Ref_Buf(uint8_t reg, uint8_t *pBuf, uint8_t bytes)
Écrire les sous-programmes correspondant en les testant deux par deux : le test de SPI_NRF24_Reg qui écrit dans un registre se fera avec SPI_NRF24_Read qui doit lire le même registre pour confirmer l'exécution correcte de l'écriture. De même pour les écritures/lectures multiples.
Le code qui est donné ici est maintenant fonctionnel et testé : on est capable d'écrire un octet (ou plusieurs octets) dans un registre et de relire dans ce même registre.
#include <avr/io.h>
#include <avr/sfr_defs.h>
#include <util/delay.h>
//********** On spécialise le SPI pour le NRF24L01 ***********
// ne gère pas CSN
uint8_t SPI_NRF24_RW(uint8_t data) {
SPDR=data;
loop_until_bit_is_set(SPSR, SPIF);
return SPDR;
}
uint8_t SPI_NRF24_W_Reg(uint8_t reg, uint8_t value) {
uint8_t status;
//CSN to LOW
PORTD &= ~(1<<PD7);
status = SPI_NRF24_RW(reg);
SPI_NRF24_RW(value);
//CSN to high
PORTD |= (1<<PD7);
return(status);
}
uint8_t SPI_NRF24_R_Reg(uint8_t reg) //OK
{
uint8_t reg_val;
//CSN to LOW
PORTD &= ~(1<<PD7);
SPI_NRF24_RW(reg);
reg_val = SPI_NRF24_RW(0);
//CSN to high
PORTD |= (1<<PD7);
return(reg_val);
}
uint8_t SPI_NRF24_R_Reg_Buf(uint8_t reg, uint8_t *pBuf, uint8_t bytes)
{
uint8_t status1,uchar_ctr;
//CSN to LOW
PORTD &= ~(1<<PD7);
status1 = SPI_NRF24_RW(reg);
for(uchar_ctr=0;uchar_ctr<bytes;uchar_ctr++)
pBuf[uchar_ctr] = SPI_NRF24_RW(0);
//CSN to high
PORTD |= (1<<PD7);
return(status1);
}
uint8_t SPI_NRF24_W_Reg_Buf(uint8_t reg, uint8_t *pBuf, uint8_t bytes)
{
uint8_t status1,uchar_ctr;
//CSN to LOW
PORTD &= ~(1<<PD7;
status1 = SPI_NRF24_RW(reg);
for(uchar_ctr=0; uchar_ctr<bytes; uchar_ctr++) //
SPI_NRF24_RW(*pBuf++);
//CSN to high
PORTD |= (1<<PD7);
return(status1); //
}
L'idée générale de ces codes consiste à positionner le SS (ou CSN pour NRF24) en 0 à faire autant de transmissions que nécessaire et à repasser SS à 1.
Il est bon de tester ces primitives avec le composant. Puisque l'on écrit une valeur dans un registre, il suffit de la relire pour et de comparer à ce qui a été écrit :
#define EN_AA 0x01
#define R_REGISTER 0x00
#define W_REGISTER 0x20
// W_REGISTER est une instruction et EN_AA un registre
uint8_t status1 = SPI_NRF24_W_Reg(W_REGISTER + EN_AA, 0x01);
uint8_t reg = SPI_NRF24_R_Reg(R_REGISTER+EN_AA);
serialWrite16Deci(reg);serialWrite(' ');serialWrite('-');serialWrite(' ');
serialWrite16Deci(status1);
serialWrite(0x0D);serialWrite(0x0A);
_delay_ms(1000);
Tout cela dans un main bien sûr. La valeur de 0x01 est écrite puis relue par la liaison série.
#define EN_AA 0x01
#define RX_ADDR_P0 0x0A
#define R_REGISTER 0x00
#define W_REGISTER 0x20
#define TX_ADR_WIDTH 5 // send address length, the maxium length is 5 5*8=40 bit
#define RX_ADR_WIDTH 5 // receive date length
uint8_t TX_ADDRESS[TX_ADR_WIDTH]={0x34,0x43,0x10,0x10,0x01}; //send address
uint8_t RX_ADDRESS[RX_ADR_WIDTH]={'s','e','r','v','1'}; //receive address
uint8_t status1 = SPI_NRF24_W_Reg_Buf(W_REGISTER + RX_ADDR_P0, RX_ADDRESS, RX_ADR_WIDTH); //write receive address
status1 = SPI_NRF24_R_Reg_Buf(R_REGISTER + RX_ADDR_P0,TX_ADDRESS,RX_ADR_WIDTH);
serialWrite(TX_ADDRESS[0]);serialWrite(TX_ADDRESS[1]);serialWrite(TX_ADDRESS[2]);serialWrite(TX_ADDRESS[3]);
serialWrite(' ');serialWrite('-');serialWrite(' ');
serialWrite16Deci(status1);
serialWrite(0x0D);serialWrite(0x0A);
_delay_ms(1000);
Nous allons commencer par réaliser la programmation de la carte de commande du Robot avec le NRF24L01 car nous pensons qu'il est plus facile pour l'enseignant de réaliser une télécommande qui servira tour à tour à chacun des binômes pour les essais. Une fois que tout sera fonctionnel avec cette télécommande, les étudiants pourront se lancer dans la réalisation et la programmation de leur propre télécommande.
Réalisation et programmation de la carte de commande du robot
modifierIl suffit donc de spécialiser encore un peu le code du SPI pour résoudre l'ensemble des problèmes pour réaliser une réception. Deux sous-programmes particuliers sont donc à réaliser :
- un sous-programme d'initialisation du NRF24L01 en mode réception
- un sous-programme de réception des données
Cette partie est délicate car elle nécessite la réalisation de l'émetteur pour les tests. Nous allons donc commencer par décrire matériellement une télécommande et donner un programme fonctionnel. Ce programme nécessite l'installation d'une librairie MIRF qui n'est probablement pas la librairie la plus à jour mais qui est facilement utilisable (voir par exemple : communiquer sans fil avec un module nrf24l01 et la bibliotheque mirf).
Télécommande : réalisation matérielle simplifiée
modifierNous avons la chance de posséder une télécommande achetée avec un kit robotique. Pour ceux qui n'ont pas cette chance, il vous faut en fabriquer une. Nous rappelons encore une fois que pour tester le fonctionnement correct de la carte de commande du Robot il faut fournir aux étudiants une télécommande fonctionnelle (même si vous décidez plus tard d'en faire réaliser une avec sa programmation dans ce TP). Nous allons montrer dans cette section comment on peut envisager cela de manière très simple.
Une image de ce qu'il y a à réaliser est donnée ci-contre. Elle montre l'utilisation d'un Arduino Nano (circuit de taille inférieure à l'Arduino UNO) très pratique à utiliser dans ce genre de situation. On lui ajoute un joystick et naturellement un NRF24L01.
Télécommande : réalisation logicielle
modifierOn rappelle qu'ici, ce qui est donné, est un programme fonctionnel de télécommande utilisant la bibliothèque MIRF que l'on se refusera d'utiliser plus tard lorsque les étudiants auront à faire ce travail. Ce programme doit réaliser les fonctions suivantes :
- une conversion analogique numérique (sur 10 bits) pour chacun des axes
- une concaténation des deux valeurs sur 10 bits en deux nombres de 16 bits, soit 32 bits stockés dans un long
- envoi par NRF24L01 des 4 octets
Nous donnons le programme source dans la boîte déroulante ci-dessous :
#include "SPI.h"
#include "Mirf.h"
#include "nRF24L01.h"
#include "MirfHardwareSpiDriver.h"
#include "Wire.h"
long Joystick_1_X;
long Joystick_1_Y;
long Joystick_2_X;
long Joystick_1;
long Joystick_2;
void setup(){
Serial.begin(9600);
Mirf.spi = &MirfHardwareSpi;
// Pour shield :
Mirf.cePin = 8; // Broche CE sur B0
Mirf.csnPin = 7; // Broche CSN sur D7
Mirf.init();
Mirf.setTADDR((byte *)"serv1");
Mirf.setRADDR((byte *) 0XE8E8F0F0E1);
Mirf.payload = sizeof(long);
Mirf.channel = 1;
Mirf.config();
}
void loop(){
Joystick_1_X = analogRead(A0); // joystick droit
Joystick_1_Y = analogRead(A1); // joystick droit
Serial.print("Joystick_1_X=");Serial.print(Joystick_1_X); Serial.print(" Joystick_1_Y=");Serial.println(Joystick_1_Y);
Joystick_2_X = Joystick_1_X << 16;
Joystick_1 = Joystick_2_X | Joystick_1_Y;
Mirf.send((byte *)&Joystick_1);
while(Mirf.isSending()){
}
delay(100);
}
Exercice 6
modifierL'objectif de cet exercice est de réaliser une communication fonctionnelle entre la télécommande donnée et votre crircuit. Pour cela vous réaliserez :
- un sous-programme capable de passer le NRF24 en mode réception :
// passage en mode reception du NRF24
void SetRX_Mode(void);
- un sous-programme capable d'écrire dans un registre "reg" une valeur "value" :
// lecture des 32 bits reçus si c'est le cas
uint8_t nRF24L01_RxPacket(uint8_t* rx_buf);
Indications pour l'exercice 6 pour setRX_Mode
modifierIl va vous falloir rentrer dans la documentation pour réaliser cet exercice.
Commençons par documenter le sous-programme setRX_Mode. Voilà les opérations qu'il doit exécuter pour être compatible avec la télécommande et sa bibliothèque MIRF :
- utiliser la définition de certaines constantes en les disposant en début de programme ou mieux dans un fichier .h
#define CONFIG 0x00
#define EN_AA 0x01
#define EN_RXADDR 0x02
#define SETUP_AW 0x03
#define SETUP_RETR 0x04
#define RF_CH 0x05
#define RF_SETUP 0x06
#define STATUS 0x07
#define OBSERVE_TX 0x08
#define CD 0x09
#define RX_ADDR_P0 0x0A
#define RX_ADDR_P1 0x0B
#define RX_ADDR_P2 0x0C
#define RX_ADDR_P3 0x0D
#define RX_ADDR_P4 0x0E
#define RX_ADDR_P5 0x0F
#define TX_ADDR 0x10
#define RX_PW_P0 0x11
#define RX_PW_P1 0x12
#define RX_PW_P2 0x13
#define RX_PW_P3 0x14
#define RX_PW_P4 0x15
#define RX_PW_P5 0x16
#define FIFO_STATUS 0x17
/* Instruction Mnemonics */
#define R_REGISTER 0x00
#define W_REGISTER 0x20
#define REGISTER_MASK 0x1F
#define R_RX_PAYLOAD 0x61
#define W_TX_PAYLOAD 0xA0
#define FLUSH_TX 0xE1
#define FLUSH_RX 0xE2
#define REUSE_TX_PL 0xE3
#define NOP 0xFF
#define TX_ADR_WIDTH 5 // send address length, the maxium length is 5 5*8=40 bit
#define RX_ADR_WIDTH 5 // receive date length
#define TX_PLOAD_WIDTH 4 // send bytes length
#define RX_PLOAD_WIDTH 4 // Receive bytes length
uint8_t TX_ADDRESS[TX_ADR_WIDTH]={0x34,0x43,0x10,0x10,0x01}; //send address
uint8_t RX_ADDRESS[RX_ADR_WIDTH]={'s','e','r','v','1'}; //receive address
- passage de CE (PB0) en sortie
- passage de CE à 0
- écriture dans RX_ADDR_P1 de l'adresse de réception qui se trouve dans le tableau RX_ADDRESS
uint8_t RX_ADDRESS[RX_ADR_WIDTH]={'s','e','r','v','1'}; //receive address
- écriture dans RX_ADDR_P1 de l'adresse d'émission qui se trouve dans le tableau TX_ADDRESS
uint8_t TX_ADDRESS[TX_ADR_WIDTH]={0x34,0x43,0x10,0x10,0x01}; //send address
- écriture dans le registre RF_CH de la valeur 1
- écriture dans le registre RX_PW_P0 de la valeur 4 (4 octets reçus)
- écriture dans le registre RX_PW_P1 de la valeur 4 (4 octets envoyés)
- mise en route de la réception : écriture dans le registre CONFIG des bits EN_CRC,PWR_UP et PRIM_RX (valeur 0x0B en tout)
- passage de CE à 1
- écriture dans le registre STATUS des bits TX_DS et MAX_RT
- écriture de l'instruction flushRx avec un SPIMasterSend
Indications pour l'exercice 6 pour nRF24L01_RxPacket
modifierLa réception d'un pacquet se fait de la manière suivante :
- obtention du statut avec SPI_NRF24_R_Reg_Buf qui doit lire le registre R_RX_PAYLOAD
- écriture dans statut du bit RX_DR pour dire que la lecture a été faite
!!!!Nous donnons pour l'instant une solution précaire mais fonctionnelle qui sera remaniée plus tard. La correction de l'exercice 7 nous a montré que ce code possède une latence de parfois 4 à 5 secondes !!!!!!!
#include <avr/io.h>
#include <avr/sfr_defs.h>
#include <util/delay.h>
#define F_CPU 16000000 // 16 MHz oscillator.
#define BaudRate 9600
#define MYUBRR (F_CPU / 16 / BaudRate ) - 1
void serialInit(void) {
//Serial Initialization
/*Set baud rate 9600 */
UBRR0H = (unsigned char)(MYUBRR>>8);
UBRR0L = (unsigned char) MYUBRR;
/* Enable receiver and transmitter */
UCSR0B = (1<<RXEN0)|(1<<TXEN0);
/* Frame format: 8data, No parity, 1stop bit */
UCSR0C = ((1<<UCSZ01)|(1<<UCSZ00)); // seulement deux bits à configurer
}
void serialWrite(uint8_t DataOut) {
loop_until_bit_is_set(UCSR0A,UDRE0); // while NOT ready to transmit
UDR0 = DataOut;
}
void serialWrite8Deci(uint8_t DataOut) {
uint8_t tab[3];
tab[0] = DataOut / 100 + '0';
tab[1] = ((DataOut / 10) % 10) + '0';
tab[2] = (DataOut % 10) + '0';
serialWrite(tab[0]); // centaine
serialWrite(tab[1]); // dizaine
serialWrite(tab[2]); // unité
}
void serialWrite16Deci(uint16_t DataOut) {
uint8_t tab[4]; // on se limite à 4 car CAN ne dépasse pas 1023
tab[0] = DataOut / 1000 + '0';
tab[1] = ((DataOut / 100) % 10) + '0';
tab[2] = ((DataOut /10)% 10) + '0';
tab[3] = (DataOut % 10) + '0';
serialWrite(tab[0]); // millier
serialWrite(tab[1]); // centaine
serialWrite(tab[2]); // dizaine
serialWrite(tab[3]); // unité
}
void ADC_Init(void) {
// Choose AVCC pin for the comparison voltage
ADMUX = (1 << REFS0) ;
// Start the ADC unit,
// set the conversion cycle 16 times slower than the duty cycle
ADCSRA = (1 << ADEN) | (1 << ADPS2);
}
uint16_t ADC_get(uint8_t channel) {
// Choose channel in the multiplexer
ADMUX &= 0xF0; // (MUX3,MUX2,MUX1,MUX0) = (0,0,0,0)
ADMUX |= (channel & 0x0F) << MUX0; //on veut channel<16
// on lance la conversion
ADCSRA |= (1 << ADSC);
// on attend qu'elle soit finie
loop_until_bit_is_clear(ADCSRA, ADSC);
// ici le bit ADSC vient de passer à 0
return ADC;
}
//SPI init
void SPIMasterInit(void) {
uint8_t data;
//set MOSI, SCK and CSN as output
DDRB |= (1<<PB3)|(1<<PB5);
DDRD |= (1<<PD7); // CSN
PORTB |= (1<<PB2); //set SS to high (pullup)
_delay_ms(1); // le passage en pull-up n'est pas instantané
//enable master SPI at clock rate Fck/16
SPCR = (1<<SPE)|(1<<MSTR)|(1<<SPR0);
//set CSN to high
PORTD |= (1<<PD7);
_delay_ms(1);
// clear SPIF
data = SPSR;
data = SPDR;
}
//master send function
void SPIMasterSend(uint8_t data){
//select slave
PORTD &= ~(1<<PD7); // CSN
//send data
SPDR=data;
//wait for transmition complete
loop_until_bit_is_set(SPSR, SPIF);
//CSN to high
PORTD |= (1<<PD7);
}
//********** On spécialise le SPI pour le NRF24L01 ***********
// ne gère pas CSN
uint8_t SPI_NRF24_RW(uint8_t data) {
SPDR=data;
loop_until_bit_is_set(SPSR, SPIF);
return SPDR;
}
// écriture d'une valeur dans un registre : ajouter W_REGISTER dans reg
uint8_t SPI_NRF24_W_Reg(uint8_t reg, uint8_t value) {
uint8_t status;
//CSN to LOW
PORTD &= ~(1<<PD7);
status = SPI_NRF24_RW(reg);
SPI_NRF24_RW(value);
//CSN to high
PORTD |= (1<<PD7);
return(status);
}
// Lecture de données d'un registre
uint8_t SPI_NRF24_R_Reg_Buf(uint8_t reg, uint8_t *pBuf, uint8_t bytes)
{
uint8_t status1,uchar_ctr;
//CSN to LOW
PORTD &= ~(1<<PD7);
status1 = SPI_NRF24_RW(reg);
for(uchar_ctr=0;uchar_ctr<bytes;uchar_ctr++)
pBuf[uchar_ctr] = SPI_NRF24_RW(0);
//CSN to high
PORTD |= (1<<PD7);
return(status1);
}
// Écriture de données dans un registre
uint8_t SPI_NRF24_W_Reg_Buf(uint8_t reg, uint8_t *pBuf, uint8_t bytes)
{
uint8_t status1,uchar_ctr;
//CSN to LOW
PORTD &= ~(1<<PD7);
status1 = SPI_NRF24_RW(reg);
for(uchar_ctr=0; uchar_ctr<bytes; uchar_ctr++) //
SPI_NRF24_RW(*pBuf++);
//CSN to high
PORTD |= (1<<PD7);
return(status1); //
}
//#include "nRF24L01.h"
#define CONFIG 0x00
#define EN_AA 0x01
#define EN_RXADDR 0x02
#define SETUP_AW 0x03
#define SETUP_RETR 0x04
#define RF_CH 0x05
#define RF_SETUP 0x06
#define STATUS 0x07
#define OBSERVE_TX 0x08
#define CD 0x09
#define RX_ADDR_P0 0x0A
#define RX_ADDR_P1 0x0B
#define RX_ADDR_P2 0x0C
#define RX_ADDR_P3 0x0D
#define RX_ADDR_P4 0x0E
#define RX_ADDR_P5 0x0F
#define TX_ADDR 0x10
#define RX_PW_P0 0x11
#define RX_PW_P1 0x12
#define RX_PW_P2 0x13
#define RX_PW_P3 0x14
#define RX_PW_P4 0x15
#define RX_PW_P5 0x16
#define FIFO_STATUS 0x17
/* Instruction Mnemonics */
#define R_REGISTER 0x00
#define W_REGISTER 0x20
#define REGISTER_MASK 0x1F
#define R_RX_PAYLOAD 0x61
#define W_TX_PAYLOAD 0xA0
#define FLUSH_TX 0xE1
#define FLUSH_RX 0xE2
#define REUSE_TX_PL 0xE3
#define NOP 0xFF
#define TX_ADR_WIDTH 5 // send address length, the maxium length is 5 5*8=40 bit
#define RX_ADR_WIDTH 5 // receive date length
#define TX_PLOAD_WIDTH 4 // send bytes length
#define RX_PLOAD_WIDTH 4 // Receive bytes length
uint8_t TX_ADDRESS[TX_ADR_WIDTH]={0x34,0x43,0x10,0x10,0x01}; //send address
uint8_t RX_ADDRESS[RX_ADR_WIDTH]={'s','e','r','v','1'}; //receive address
void SetRX_Mode(void)
{
// gestion de CE en PB0
DDRB |= (1<<PB0);
//CE to LOW
PORTB &= ~(1<<PB0);
SPI_NRF24_W_Reg_Buf(W_REGISTER + RX_ADDR_P1, RX_ADDRESS, RX_ADR_WIDTH); //write receive address
SPI_NRF24_W_Reg_Buf(W_REGISTER + RX_ADDR_P0, TX_ADDRESS, TX_ADR_WIDTH); //write transmit address
SPI_NRF24_W_Reg(W_REGISTER + RF_CH,1); //RF channel
SPI_NRF24_W_Reg(W_REGISTER + RX_PW_P0, RX_PLOAD_WIDTH);//write channel 0 receive data length
SPI_NRF24_W_Reg(W_REGISTER + RX_PW_P1, RX_PLOAD_WIDTH);//write channel 1 receive data length
// Start receiver
SPI_NRF24_W_Reg(W_REGISTER + CONFIG, 0x0B); //receive mode :EN_CRC,PWR_UP, PRIM_RX
//CE to HIGH
PORTB |= (1<<PB0);
_delay_ms(1);//can't be too small
#define TX_DS 5
#define MAX_RT 4
SPI_NRF24_W_Reg(W_REGISTER + STATUS,(1 << TX_DS) | (1 << MAX_RT));
//Instruction : flushRx
SPIMasterSend(FLUSH_RX);
}
uint8_t SPI_NRF24_R_Reg(uint8_t reg) //OK
{
uint8_t reg_val;
//CSN to LOW
PORTD &= ~(1<<PD7);
SPI_NRF24_RW(reg);
reg_val = SPI_NRF24_RW(0);
//CSN to high
PORTD |= (1<<PD7);
return(reg_val);
}
uint8_t nRF24L01_RxPacket(uint8_t* rx_buf)
{
uint8_t status1;
status1=SPI_NRF24_R_Reg_Buf(R_RX_PAYLOAD,rx_buf,TX_PLOAD_WIDTH);// read receive payload from RX_FIFO buffer
#define RX_DR 6
SPI_NRF24_W_Reg(W_REGISTER+STATUS,(1<<RX_DR));
return status1;
}
int main() {
uint16_t joystick_x,joystick_y;
uint8_t rx_buffer[RX_PLOAD_WIDTH],status1,cmpt=0;
// setup
serialInit();
//ADC_Init();
SPIMasterInit();
SetRX_Mode();
// loop
while (1) {
status1=nRF24L01_RxPacket(rx_buffer);
if (status1 & (1<<RX_DR)) {
joystick_y = (rx_buffer[1]<<8)+rx_buffer[0];
joystick_x = (rx_buffer[3]<<8)+rx_buffer[2];
serialWrite16Deci(joystick_x);
serialWrite(' ');serialWrite('-');serialWrite(' ');
serialWrite16Deci(joystick_y);
serialWrite(0x0D);serialWrite(0x0A);
} else {
serialWrite('*');serialWrite('*');
serialWrite(0x0D);serialWrite(0x0A);
}
}
}
Nous sommes arrivés au point où nous avons une télécommande et une carte de réception fonctionnelle. Il nous faut maintenant étudier comment nous allons prendre les données envoyées par la télécommande et les convertir en deux valeurs de MLI pour transformer les ordres de la télécommande en vitesse de rotation du moteur droit et du moteur gauche.
Exercice 7
modifierNotre objectif maintenant est d'ajouter la partie du code destinée à gérer le pwm à partir des données qui nous arrivent du circuit NRF24L01. La correction de l'exercice 6 fournit le moyen d'obtenir les deux données du joystick
- joystick_y = (rx_buffer[1]<<8)+rx_buffer[0];
- joystick_x = (rx_buffer[3]<<8)+rx_buffer[2];
rx_buffer[] est un tableau d'octets qui est automatiquement rempli par le programme avec les données en provenance de la radio.
Notre problème va consister à trouver à partir de ces deux données deux autres données :
- int16_t pwm qui dépend de joystick_y et qui consiste à transformer uint16_t joystick_y en int16_t pwm, autrement dit un nombre non signé en un nombre signé. Le problème de cette transformation est que le zéro du joystick n'est pas un point mais plutôt un intervalle. Vous allez mesurer un point mais si vous faites la mesure plus tard vous en obtiendrez un autre. Vous prenez donc le point qui vous semble le mieux et vous le retranchez à la valeur joystick_y ce qui fera un nombre signé dépendant de votre avancement ou recul de votre joystick (à l'incertitude près sur le zéro). Ce problème du zéro sera géré plus tard.
- int16_t deltaPWM qui dépend de joystick_x et qui représente d'une manière peu rigoureuse une rotation. Il sera lui aussi transformé en nombre signé en lui retranchant le zéro du joystick sur l'axe des x avec les mêmes difficultés que pour la conversion précédente.
Vient ensuite une série de tests sur les variables que l'on vient de calculer : pwm et deltaPWM :
- Tableau des tests
pwm > 30 pwm < -30 deltaPWM > 30 deltaPWM < -30 cas Dpwm 1 0 0 0 0x08 Dpwm=0 0 1 0 0 0x04 Dpwm=0 0 0 1 0 0x02 Dpwm=100 0 0 0 1 0x01 Dpwm=-100 1 0 1 0 0x0A Dpwm = deltaPWM 1 0 0 1 0x09 Dpwm = deltaPWM 0 1 1 0 0x06 Dpwm = -deltaPWM 0 1 0 1 0x05 Dpwm = -deltaPWM
- Les tests de type >30 ou <-30 permettent de gérer l'intervalle autour du 0 pour le joystick.
- "cas" est une variable qui permet de gérer numériquement un switch, c'est une sortie du tableau. Sa valeur est la valeur hexadécimale des 4 entrées.
- Dpwm est aussi un calcul réalisé avec deltaPWM. Nous l'avons limité à 100 (sur 255) pour éviter de trop importantes vitesse du robot. Les deux valeurs +100 et -100 qui apparaissent dans le tableau sont liés au fait que nous voulions que pour aucun avancement ou recul le robot tourne avec sa vitesse maximale.
- Ces deux sorties vont permettre de gérer le calcul des deux PWM, un pour le moteur droit pwmD et un pour le moteur gauche pwmG.
1°) Écrire' la gestion du pwm avec le timer0. Les broches 0C0A et OC0B du timer 0 portent respectivement le numéro 6/PD6 et 5/PD5.
2°) Écrire la partie de programme qui prend pwmD et pwmG et calcule les valeurs à envoyer sur le L298D sachant que ces deux données peuvent être positives (avancement) ou négatives (recul): void deplaceToi(int16_t pwmD, int16_t pwmG).
3°) Écrire la partie de programme qui calcule cas, Dpwm, pwm, pwmD et pwmG. Pour laisser de la place en RAM, les variables deltaPWM et Dpwm seront fusionnées en une seule variable.
Solution non vérifiée donc à vérifier au plus vite !
Question 1°
// ****** gestion du timer
//Configuration (6 et 5 pour NANO)
DDRD |= ((1<<DDD6)|(1<<DDD5));
// Set the Timer Mode to PWM fast
TCCR0A |= ((1 << WGM01) | (1<<WGM00));
// clear OC0A on compare match set OC0A at TOP and same for OCOB
TCCR0A |= ((1 << COM0A1)|(1 << COM0B1));
// set prescaler to 256 and start the timer
TCCR0B |= (1 << CS01)|(1 << CS00);
// gestion du sens d'avancement
DDRC |= 0x0F; // PCO/A0, PC1/A1, PC2/A2 et PC3/A3 en sortie
Question 2°
#define MAXPWM 100
// question 2°
void deplaceToi(int16_t pwmD, int16_t pwmG) {
if (pwmD > MAXPWM) pwmD = MAXPWM;
if (pwmD < -MAXPWM) pwmD = -MAXPWM;
if (pwmG > MAXPWM) pwmG = MAXPWM;
if (pwmG < -MAXPWM) pwmG = -MAXPWM;
if (pwmD > 0) {
PORTC |= (1<<PC2);
PORTC &= ~(1<<PC3);
OCR0A = pwmD;
//analogWrite(6,pwmD);
}
if (pwmD == 0) {
PORTC |= (1<<PC2);
PORTC |= (1<<PC3);
OCR0A = pwmD;
//analogWrite(6,pwmD);
}
if (pwmD < 0) {
PORTC &= ~(1<<PC2);
PORTC |= (1<<PC3);
OCR0A = -pwmD;
//analogWrite(6,-pwmD);
}
if (pwmG > 0) {
PORTC |= (1<<PC1);
PORTC &= ~(1<<PC0);
OCR0B = pwmG;
//analogWrite(5,pwmG);
}
if (pwmG == 0) {
PORTC |= (1<<PC1);
PORTC |= (1<<PC0);
OCR0B = pwmG;
//analogWrite(5,pwmG);
}
if (pwmG < 0) {
PORTC |= (1<<PC0);
PORTC &= ~(1<<PC1);
OCR0B = -pwmG;
//analogWrite(5,-pwmG);
}
}
Question 3°
int main() {
int16_t joystick_x,joystick_y,pwmD=60,pwmG,pwm,deltaPWM;
uint8_t rx_buffer[RX_PLOAD_WIDTH],status1,cas=0;
// setup
//serialInit();
//ADC_Init();
SPIMasterInit();
SetRX_Mode();
// ****** gestion du timer
//Configuration (6 et 5 pour NANO)
DDRD |= ((1<<DDD6)|(1<<DDD5));
// Set the Timer Mode to PWM fast
TCCR0A |= ((1 << WGM01) | (1<<WGM00));
// clear OC0A on compare match set OC0A at TOP and same for OCOB
TCCR0A |= ((1 << COM0A1)|(1 << COM0B1));
// set prescaler to 256 and start the timer
TCCR0B |= (1 << CS01)|(1 << CS00);
// gestion du sens d'avancement
DDRC |= 0x0F; // PCO/A0, PC1/A1, PC2/A2 et PC3/A3 en sortie
// loop
while (1) {
status1=nRF24L01_RxPacket(rx_buffer);
if (status1 & (1<<RX_DR)) {
joystick_y = (rx_buffer[1]<<8)+rx_buffer[0];
joystick_x = (rx_buffer[3]<<8)+rx_buffer[2];
//serialWrite16Deci(joystick_x);serialWrite(' ');serialWrite('-');serialWrite(' ');
//serialWrite16Deci(joystick_y);
//serialWrite(0x0D);serialWrite(0x0A);
// question 3° Exo 7
pwm = joystick_y-513;
// calcul de deltaPWM
deltaPWM = joystick_x-504;
// gestion de différentes possibilités avec la variable cas
if (pwm > 30) cas |= 0x08; else cas &= 0x07;
if (pwm < -30) cas |= 0x04; else cas &= 0x0B;
if (deltaPWM > 30) cas |= 0x02; else cas &= 0x0D;
if (deltaPWM < -30) cas |= 0x01; else cas &= 0x0E;
switch (cas) {
case 0x08 :
case 0x04 : pwm = (pwm >> 2); deltaPWM = 0; break;
case 0x02 : pwm=0; deltaPWM=-100; break;
case 0x01 : pwm=0; deltaPWM=100;break;
case 0x0A :
case 0x09 : pwm = (pwm >> 2); deltaPWM = (deltaPWM >> 3); break;
case 0x06 :
case 0x05 : pwm = (pwm >> 2); deltaPWM = -(deltaPWM >> 3); break;
default : pwm = 0; deltaPWM=0;
} // switch
pwmG = pwm - deltaPWM;
pwmD = pwm + deltaPWM;
deplaceToi(pwmD,pwmG);
} // if (status1 & (1<<RX_DR))
} // while(1)
} // main
Réalisation et programmation de la télécommande
modifierNous avons déjà présenté la télécommande de manière matérielle et nous redonnons son schéma de principe. Nous rappelons que la télécommande utilisée pour la communication avec la carte de commande du robot utilisait la bibliothèque MIRF et qu'ici nous allons remplacer ce code par notre propre code.
Tout est prêt maintenant pour réaliser le code de la télécommande. On a du code pour faire
- une conversion analogique numérique
- gérer une liaison série
- gérer du SPI
Il suffit donc de spécialiser encore un peu le code du SPI pour résoudre l'ensemble des problèmes pour réaliser une transmission.
Exercice 8
modifierOn vous demande de réaliser un sous-programme capable d'initialiser complètement le NRF24L01 en émission et un sous-programme d'émission de données.
Documentation NRF24L01
modifierLe composant NRF24L01 est caractérisé par un ensemble de registres de configuration. On accède à ces registres à travers le protocole SPI qui est le seul protocole disponible sur ce composant.
Registre de configuration
modifier- NRF24L01+
Adresse (hexa) Mnémonique Bit Valeur de reset Type Description 00 CONFIG Registre de configuration Réservé 7 0 R/W '0' seulement autorisé MASK_RX_DR 6 0 R/W masque d'interruption par RX_DR 1 : interruption non reflectée
0 : refléchie RX_DR active basse sur la broche IRQ
MASK_TX_DS 5 0 R/W masque d'interruption par RX_DS 1 : interruption non reflectée
0 : refléchie RX_DR active basse sur la broche IRQ
MASK_MAX_RT 4 0 R/W masque d'interruption par RX_DT 1 : interruption non reflectée
0 : refléchie RX_DR active basse sur la broche IRQ
EN_CRC 3 1 R/W autorise le CRC. Forcé à 1 si un bit de EN_AA est à 1 CRCO 2 0 R/W Schéma d'encodage de CRC 0 : 1 octet
1 : 2 octets
PWR_UP 1 0 R/W 1 : en action 0 : éteint
PRIM_RX 0 0 R/W contrôle RX/TX 1 : PRX
0 : PTX
Gestion de l'accusé de réception
modifier- NRF24L01+
Adresse (hexa) Mnémonique Bit Valeur de reset Type Description 01 EN_AA Enhanced ShockBurstTM desactiver cette fonction pour être compatible nrf2401 Réservé 7:6 00 R/W '00' seulement autorisé ENAA_P5 5 1 R/W accusé de réception autorisé pour canal 5 ENAA_P4 4 1 R/W accusé de réception autorisé pour canal 4 ENAA_P3 3 1 R/W accusé de réception autorisé pour canal 3 ENAA_P2 2 1 R/W accusé de réception autorisé pour canal 2 ENAA_P1 1 1 R/W accusé de réception autorisé pour canal 1 ENAA_P0 0 1 R/W accusé de réception autorisé pour canal 0
Gestion de l'autorisation des adresses de réception
modifier- NRF24L01+
Adresse (hexa) Mnémonique Bit Valeur de reset Type Description 02 EN_RXADDR Autoriser l'adresse de réception Réservé 7:6 00 R/W '00' seulement autorisé ERX_P5 5 1 R/W autorisation pour canal 5 ERX_P4 4 1 R/W autorisation pour canal 4 ERX_P3 3 1 R/W autorisation pour canal 3 ERX_P2 2 1 R/W autorisation pour canal 2 ERX_P1 1 1 R/W autorisation pour canal 1 ERX_P0 0 1 R/W autorisation pour canal 0
Gestion de la largeur commune des adresses de réception et d'émission
modifier- NRF24L01+
Adresse (hexa) Mnémonique Bit Valeur de reset Type Description 03 SETUP_AW définition de la taille de l'adresse pour tous les canaux Réservé 7:2 000000 R/W '000000' seulement autorisé AW 1:0 11 R/W Largeur d'adresse d'émission/réception 00 : illégal
01 : 3 octets
10 : 4 octets
11 : 5 octets
Gestion de la retransmission automatique
modifier- NRF24L01+
Adresse (hexa) Mnémonique Bit Valeur de reset Type Description 04 SETUP_RETR réglage de retransmission automatique ARD 7:4 0000 R/W délai retransmission 0000 : attente de 250 us
0001 : attente de 500 us
....
1111 : attente de 4000 us
ARC 3:0 0011 R/W nombre de retransmissions 0000 : pas de retransmission
0001 : jusqu'à une retransmission en cas d'échec
....
1111 : jusqu'à 15 retransmissions en cas d'échec
Gestion du canal radio fréquence
modifier- NRF24L01+
Adresse (hexa) Mnémonique Bit Valeur de reset Type Description 05 RF_CH canal RF Reserved 7 0 R/W alloué à 0 seulement RF_CH 6:0 0000010 R/W réglage des fréquences des canaux
Registre de configuration RF
modifier- NRF24L01+
Adresse (hexa) Mnémonique Bit Valeur de reset Type Description 06 RF_SETUP Registre de configuration RF CONT_WAVE 7 0 R/W autorise l'émission continue de la porteuse Réservée 6 0 R/W Seulement '0' autorisé RF_DR_LOW 5 0 R/W positionne le débit à 250 kbps PLL_LOCK 4 0 R/W Force le verrouillage de la PLL (seulement pour les tests) RF_DR_HIGH 3 1 R/W Combiné à RF_DR_LOW permet éventuellement de changer le débit de transmission RF_PWR 2:1 11 R/W Gère la puissance d'émission en sortie 00 : -18 dBm
01 : -12 dBm
10 : -6 dBm
11 : 0 dBm
Obsolète 0 0 R/W Ne pas utiliser
Registre de statut
modifier- NRF24L01+
Adresse (hexa) Mnémonique Bit Valeur de reset Type Description 07 STATUS Une écriture SPI dans ce registre permet de retrouver son contenu en série sur la broche MISO Réservé 7 0 R/W Seul un 0 est possible RX_DR 6 0 R/W Mis à '1' quand une donnée arrive dans le FIFO de réception TX_DS 5 0 R/W Mis à '1' quand une donnée est émise MAX_RT 4 0 R/W Si ce bit est à 1 il faut le remettre à 0 pour continuer toute utilisation. La mise à 0 se fait en écrivant un 1. RX_P_NO 3:1 111 R Numéro du canal correspondant à la réception dans RX_FIFO 000 - 101 : unméro du canal
110 : non utilisé
111 : RX_FIFO vide
TX_FULL 0 0 R Drapeau pour remplissage du FIFO de réception 1 : FIFO rempli
0 : Possibilité d'utiliser le FIFO
Registre d'observation de la transmission
modifier- NRF24L01+
Adresse (hexa) Mnémonique Bit Valeur de reset Type Description 08 OBSERVE_TX Registre d'observation de la transmission PLOS_CNT 7:4 0 R Compte les paquets perdus. Ce compteur est limité à 15 et rest à 15 jusqu'à une initialisation par écriture dans RF_CH ARC_CNT 3:0 0 R Compteur de paquets retransmis. Ce compteur est automatiquement réinitialisé à 0 lors d'une transmission d'un nouveau paquet
Registre de détection de la porteuse
modifier- NRF24L01+
Adresse (hexa) Mnémonique Bit Valeur de reset Type Description 09 RPD Registre de détection de la porteuse Réservé 7:1 0000000 R Ne pas utiliser RPD 0 0 R
Registres de gestion des adresses de réception pour les canaux
modifier- NRF24L01+
Adresse (hexa) Mnémonique Bit Valeur de reset Type Description 0A RX_ADDR_P0 39:0 0xE7E7E7E7E7 R/W Adresse de réception du canal 0 0B RX_ADDR_P1 39:0 0xC2C2C2C2C2 R/W Adresse de réception du canal 1 0C RX_ADDR_P2 7:0 0xC3 R/W Adresse de réception du canal 2 (bits 39:8 égaux à canal 1) 0D RX_ADDR_P3 7:0 0xC4 R/W Adresse de réception du canal 3 (bits 39:8 égaux à canal 1) 0E RX_ADDR_P4 7:0 0xC5 R/W Adresse de réception du canal 4 (bits 39:8 égaux à canal 1) 0F RX_ADDR_P5 7:0 0xC6 R/W Adresse de réception du canal 5 (bits 39:8 égaux à canal 1)
Registres de gestion de l'adresse de transmission pour les canaux
modifier- NRF24L01+
Adresse (hexa) Mnémonique Bit Valeur de reset Type Description 10 TX_ADDR 39:0 0xE7E7E7E7E7 R/W Adresse de transmission. Positionner l'adresse RX_ADDR_P0 à cette valeur pour automatiser le Enhanced ShockBurstTM
Configuration du nombre d'octets utiles dans une trame
modifier- NRF24L01+
Adresse (hexa) Mnémonique Bit Valeur de reset Type Description 11 RX_PW_P0 Réservé 7:6 00 R/W Seuls 00 sont autorisés RX_PW_P0 5:0 0 R/W nombre d'octets utilisables pour un envoi/réception (1 à 32 octets) 0 : canal non utilisé
1 : 1 octet
...
32 : 32 octets
Adresse (hexa) Mnémonique Bit Valeur de reset Type Description 12 RX_PW_P1 Réservé 7:6 00 R/W Seuls 00 sont autorisés RX_PW_P1 5:0 0 R/W nombre d'octets utilisables pour un envoi/réception (1 à 32 octets) 0 : canal non utilisé
1 : 1 octet
...
32 : 32 octets
Adresse (hexa) Mnémonique Bit Valeur de reset Type Description 13 RX_PW_P2 Réservé 7:6 00 R/W Seuls 00 sont autorisés RX_PW_P2 5:0 0 R/W nombre d'octets utilisables pour un envoi/réception (1 à 32 octets) 0 : canal non utilisé
1 : 1 octet
...
32 : 32 octets
Adresse (hexa) Mnémonique Bit Valeur de reset Type Description 14 RX_PW_P3 Réservé 7:6 00 R/W Seuls 00 sont autorisés RX_PW_P3 5:0 0 R/W nombre d'octets utilisables pour un envoi/réception (1 à 32 octets) 0 : canal non utilisé
1 : 1 octet
...
32 : 32 octets
Adresse (hexa) Mnémonique Bit Valeur de reset Type Description 15 RX_PW_P4 Réservé 7:6 00 R/W Seuls 00 sont autorisés RX_PW_P4 5:0 0 R/W nombre d'octets utilisables pour un envoi/réception (1 à 32 octets) 0 : canal non utilisé
1 : 1 octet
...
32 : 32 octets
Adresse (hexa) Mnémonique Bit Valeur de reset Type Description 16 RX_PW_P5 Réservé 7:6 00 R/W Seuls 00 sont autorisés RX_PW_P5 5:0 0 R/W nombre d'octets utilisables pour un envoi/réception (1 à 32 octets) 0 : canal non utilisé
1 : 1 octet
...
32 : 32 octets
Registre de statut du FIFO
modifier- NRF24L01+
Adresse (hexa) Mnémonique Bit Valeur de reset Type Description 17 FIFO_STATUS Registre de statut du FIFO Réservé 7 0 R/W Seul un 0 est possible TX_REUSE 6 0 R TX_FULL 5 0 R Mis à '1' quand le FIFO de transmission est plein TX_EMPTY 4 0 R Si ce bit est à 1 le FIFO de transmission est vide Réservé 3:2 00 R/W Seulement 00 est autorisé RX_FULL 1 0 R Mis à '1' quand le FIFO de réception est plein RX_EMPTY 0 0 R Si ce bit est à 1 le FIFO de réception est vide
Gestion dynamique des longueurs de paquets
modifier- NRF24L01+
Adresse (hexa) Mnémonique Bit Valeur de reset Type Description 1C DYNPD Autorise la gestion dynamique des longueurs de paquets Réservé 7:6 00 R/W '00' seulement autorisé DPL_P5 5 0 R/W autorise une longueur dynamique pour canal 5 DPL_P4 4 0 R/W autorise une longueur dynamique pour canal 4 DPL_P3 3 0 R/W autorise une longueur dynamique pour canal 3 DPL_P2 2 0 R/W autorise une longueur dynamique pour canal 2 DPL_P1 1 0 R/W autorise une longueur dynamique pour canal 1 DPL_P0 0 0 R/W autorise une longueur dynamique pour canal 0
Les instructions SPI pour NRF24L01
modifier- NRF24L01+
Nom de l'instruction Instruction binaire Nombre d'octets Opération R_REGISTER 000A AAAA 1 à 5 octets Poids fort en premier Lit le registre d'adresse A AAAA W_REGISTER 001A AAAA 1 à 5 octets Poids fort en premier Écrit dans le registre d'adresse A AAAA R_RX_PAYLOAD 0110 0001 1 à 32 octets Poids fort en premier Lit le registre de réception jusqu'à 32 octets W_TX_PAYLOAD 1010 0000 1 à 32 octets Poids fort en premier Écrit dans le registre de réception jusqu'à 32 octets FLUSH_TX 1110 0001 0 Vide le FIFO de transmission (utilisé en TX mode seulement) FLUSH_RX 1110 0010 0 Vide le FIFO de réception (utilisé en RX mode seulement) REUSE_TX_PL 1110 0011 0 Utilisé pour les circuits PTX NOP 1111 1111 0 Pas d'opération
Voir aussi
modifier- communiquer sans fil avec un module nrf24l01 la bibliotheque mirf et une carte arduino genuino
- nrf24l01 FPGA (VHDL) Lien innactif depuis 2020
- GitHub librairie en c
- GitHub librairie MIRF pour Arduino
- Librairie Mirf en C
- WIKI:NRF24L01-Mirf
- (GitHub) Librairie NRFLite qui comme son nom l'indique est légère