jeudi 26 novembre 2015

Code minimal nRF24L01 pour un Arduino nano

On peut trouver des modules de communication avec des nRF24L01+ pour utiliser avec des Arduino aux alentours de un euro... Il s'agit de composants qui permettent de communiquer par ondes radios à 2.4 MHz en GFSK à 2 Mbits/seconde. Ils comprennent la partie émission et la partie réception mais il font l'un ou l'autre, ils fonctionnent en half-duplex. Il existe une ou plusieurs librairies Arduino mais ça n'empêche pas de chipoter soi-même...

Quelques fils à raccorder : 4 connexions SPI, 2 pour l'alimentation (3.3V!) et un pour une commande du chip (CE).

En pratique, cela donne :

...À réaliser deux fois.

Pour ce qui est du programme, quand on décide de se passer de l'IDE Arduino, de ses librairies et d'utiliser avr libc, il faut commencer par programmer l'UART et le relier à stdio. Ensuite, programmer l'interface SPI en fonction de ce que l'on trouve dans la datasheet du nRF24L01+ et de l'ATmega328p. Il faut configurer certaines pins en sortie : MOSI, /SS et CLK et activer/configurer le SPI. Comme l'horloge est inactive basse et qu'on échantillonne les données au flanc montant, on est en 'Mode 0'; on spécifie un diviseur pour l'horloge SPI et le fait qu'on est 'MASTER'. Reste à programmer la fonction de transfert... En fait, en SPI, chaque fois qu'il y a un bit qui sort, un bit rentre et les transferts comportent un certain nombre d'octets encadrés par la sélection de l'esclave; chaque octet étant géré par le hardware. Quand on met un octet dans le registre SPDR, le transfert démarre. La fin du transfert est signalée dans un bit du registre SPSR. Bref, on a une fonction spix(in <- out, n).

Reste à programmer le nRF24L01... La datasheet fait 74 pages mais si l'on s'en tient à une fonctionnalité de base : transmettre avec l'un et recevoir avec l'autre, il n'y a presque rien à faire. Le chip, initialisé au RESET, est quasi fonctionnel : la transmission se fait à 2 Mbits/s à une fréquence donnée (à puissance maximum), avec un CRC de 1 byte, des adresses (pré-initialisées) de 5 bytes, les paquets envoyés sont retransmis jusqu'à trois fois si un accusé de réception (acknowledgement) n'est pas reçu... Il faut juste préciser la taille des paquets que l'on entend recevoir. Quelques dizaines de lignes de programmation suffisent pour voir les systèmes interagir.

#include <avr/io.h>
#define F_CPU 16000000UL
#include <util/delay.h>
#include <stdio.h>
#include <string.h>
/*
** UART - stdio
*/
int uart_getchar(FILE *stream)
    {
    while (!(UCSR0A & (1<<RXC0)))
        ;
    return(UDR0);
    }
int uart_putchar(char c, FILE *stream)
    {
    while (!(UCSR0A & (1<<UDRE0)))
        ;
    UDR0 = c;
    }
void uart_init(void)
    {
    UBRR0H = 0x00;
    UBRR0L = 103;  /* 9600 bps | F_CPU/16/baud-1 */
    UCSR0C = (1<<USBS0) | (3<<UCSZ00); /* 8N1 */
    UCSR0B = (1<<RXEN0) | (1<<TXEN0);
    fdevopen(uart_putchar, uart_getchar); /* stdio init */
    }
void xdump(uint8_t *ucp, int len)
    {
    while (--len >= 0)
        printf("%02x ", *ucp++);
    printf("\n\r");
    }
char *getline(char *cp, int n) /* line input with  minimal editing */
    {
    int    i=0;
    char    c;

    for(;;)
        {
        c = fgetc(stdin);
        if (c=='\n' || c=='\r' || i==(n-1))
            {
            cp[i] = '\0';
            return(cp);
            }
        if (c=='\b' && i>0)
            {
            fputc(c, stdout);
            fputc(' ', stdout);
            i -= 1;
            }
        else cp[i++] = c;
        fputc(c, stdout);
        }
    }
/*-----------------------------------------------*/
/*
** SPI
** /SS /CS  D10 PB2
** MOSI     D11 PB3
** MISO     D12 PB4
** SCK      D13 PB5
*/
void spi_init()
    {
    DDRB  |= _BV(PB2)|_BV(PB3)|_BV(PB5);    /* /SS+MOSI+SCK out */
    SPCR   = _BV(SPE)|_BV(MSTR)|_BV(SPR0);  /* Enable SPI, Master, set clock rate fck/16 Mode-0 */
    PORTB |= _BV(PB2);                      /* /CS := 1 spi idle*/
    }
/*
** spix(...) clocks out *ucpout, getting *ucpin
*/
void spix(uint8_t *ucpin, uint8_t *ucpout, int n)  /* IN := spi(OUT) */
    {
    PORTB &= ~_BV(PB2);    /* /CS := 0 */
    while (--n >= 0)
        {
        SPDR = *ucpout++;
        while ((SPSR & (1<<SPIF)) == 0)
            ;
        *ucpin++ = SPDR;
        }
    PORTB |= _BV(PB2);    /* /CS := 1 */
    }
/*-----------------------------------------------*/
/*
** nRF24L01
**    CE = D8 = PB0
*/
/* commands */
#define R_REGISTER(reg) (0x00+reg)  /* read register 'reg' */
#define W_REGISTER(reg) (0x20+reg)  /* write register 'reg' */
#define R_RX_PAYLOAD    0x61        /* read received payload */
#define W_TX_PAYLOAD    0xA0        /* write payload to transmit */

uint8_t bufout[33];   /* Arduino -> nRF24L01 */
uint8_t bufin[33];    /* nRF24L01 -> Arduino */
void ce(int val)
    {
    if (val)
         PORTB |= _BV(PB0);
    else PORTB &= ~_BV(PB0);
    }
void pulse_ce()
    {
    ce(1);
    _delay_us(15L); /* minimum 10 */
    ce(0);
    }
void nrf24_init(int rx)
    {
    if (rx)
        {
        bufout[0] = W_REGISTER(0x11);
        bufout[1] = 32;
        spix(bufin, bufout, 2);        /* R11 = RX_PW_P0 = 32 payload length */
        bufout[0] = W_REGISTER(0x00);
        bufout[1] = (1<<1) + rx;
        spix(bufin, bufout, 2);     /* R00 = CONFIG = PWR_UP+PRIM_RX */
        _delay_ms(2L);            /* PWR_UP delay */
        ce(1);                /* enter listening */
        }
    else{ /* tx */
        bufout[0] = W_REGISTER(0x00);
        bufout[1] = (1<<1);
        spix(bufin, bufout, 2);        /* R00 = CONFIG = PWR_UP (tx default) */
        _delay_ms(2L);            /* PWR_UP delay */
        ce(0);
        }
    }
send(uint8_t *ucp, size_t n)
    {
    memset(bufout, 0x00, sizeof(bufout));
    bufout[0] = W_TX_PAYLOAD;        /* cmd = TX_PAYLOAD */
    n = (n > 32) ? 32 : n;
    memcpy(&bufout[1], ucp, n);
    spix(bufin, bufout, 33);    /* always send 32 bytes */
    pulse_ce();
    }
rcv(uint8_t *ucp, size_t n)
    {
    memset(bufout, 0xff, sizeof(bufout));
    bufout[0] = 0xFF;    /* cmd = NOP */
    do    {
        spix(bufin, bufout, 1);
        } while ((bufin[0] & 0x0e)  == 0x0e);    /* while nothing in */
    bufout[0] = R_RX_PAYLOAD;    /* cmd = R_RX_PAYLOAD */
    spix(bufin, bufout, 33);    /* always read 32 bytes */
    memcpy(ucp, bufin+1, n);
    }
/*-----------------------------------------------*/
int main(void)
    {
    char buf[32];

    spi_init();
    uart_init();
    printf("Test nRF24L01\n\r=============\n\r1. Receive\n\r2. Transmit\n\r> ");
    getline(buf, sizeof(buf));
    if (buf[0] == '1')
        { /* receive loop */
        nrf24_init(1);
        printf("Receiving...\n\r");
        for (;;)
            {
            rcv(buf, sizeof(buf));
            printf("%s\n\r", buf);
            }
        }
    else{ /* transmit loop */
        nrf24_init(0);
        for (;;)
            {
            printf("\n\r> ");
            getline(buf, sizeof(buf));
            send(buf, strlen(buf)+1);
            }
        }
    }
Il faut noter qu'un RESET de l'ATmega328p ne remet pas à zéro le nRF24L01 et que, pour bien faire, il faudrait ajouter un FLUSH_RX et un FLUSH_TX en démarrant pour remettre la transmission à zéro. Pour bien faire, il faudrait faire du contrôle de flux et traiter les erreurs. En fait, cela se passe surtout du côté de l'émetteur; du côté du récepteur, on ne reçoit rien si ce que l'on reçoit n'est pas un message correct (tout au plus peut-on s'apercevoir que la FIFO a débordé si on ne lit pas les messages assez vite). Il y a aussi de nombreuses autres fonctions du composant à explorer... (changement d'adresse, de puissance, sources multiples,...). Mais bon...

À noter aussi que _BV(b) (bit value) est équivalent à (1<<b) et que, pour bien faire, il faudrait utiliser une notation ou l'autre. Ce serait plus élégant... La seconde notation est plus puissante, elle fonctionne avec des champs de plusieurs bits. Par exemple (7<<3) n'est pas simplement représentable dans la première.

Pour ce qui est des commandes, quelque chose comme :
$ avr-gcc  -mmcu=atmega328  -Os -o nrf.elf nrf.c
$ avrdude -c arduino -p atmega328P -P /dev/ttyUSB0 -b 57600 -U flash:w:echo.elf
$ avrdude -c arduino -p atmega328P -P /dev/ttyUSB1 -b 57600 -U flash:w:echo.elf
$ minicom -D /dev/ttyUSB0 -b 9600  # et USB1 dans un autre terminal
Ensuite, on tape '1' dans un des terminaux minicom et '2' dans l'autre. Le premier passe en mode réception et reçoit tout ce que l'on tape dans l'autre (en veillant à garder des lignes de moins de 32 caractères).



Voir aussi