La communication par lien SPI (Serial Peripheral Interface) est un élément important pour les systèmes embarqués. Ceci concerne les micro-contrôleurs (systèmes industriels par exemple), mais également les cartes à micro-processeurs que l’on emploie de plus en plus pour les piloter. Le system-on-chip qui équipe le Raspberry Pi ne fait pas exception, nous pouvons le vérifier.
Présentation
Le SPI permet une communication entre un maître et un ou plusieurs esclaves. Le maître impose la fréquence d’horloge et sélectionne l’esclave auquel les données sont envoyées. La ligne MOSI permet d’envoyer des données depuis le maître vers l’esclave. La ligne MISO est en haute-impédance jusqu’au moment où l’esclave est sélectionné et où il doit envoyer des données.
Lorsqu’on dispose de plusieurs esclaves, ils peuvent être connectés sur un principe de bus (le maître sélectionne parmi ses multiples sorties SS0, SS1, etc. l’esclave auquel il s’adresse) ou en daisy-chain (les lignes MISO et MOSI sont connectées en cascade, le nombre de coups d’horloge permettant de sélectionner le périphérique parmi l’ensemble qui se comporte alors comme une sorte de registre à décalage).
Connexion du Raspberry Pi
Le Raspberry Pi ne fonctionne (pour le moment) qu’en mode master. Il dispose de deux sorties SS0 et SS1 pour la sélection du périphérique esclave auquel il s’adresse. Les connexions se font sur les bornes suivantes du port d’extension P1.
Configuration du noyau
Lors de la compilation du kernel, il faut activer l’option « BCM2708 SPI controller driver » dans le sous-menu « SPI support » du menu « Device Drivers » comme indiqué sur la figure ci-dessous. L’option « User mode SPI device driver support » permet de disposer d’entrées /dev/spi0.0
(pour communiquer avec le premier esclave) et /dev/spi0.1
pour communiquer avec le second. Ceci offre alors la possibilité d’utiliser facilement l’interface SPI depuis l’espace utilisateur.
Écriture vers un esclave
Après compilation et installation du noyau et de ses modules, nous démarrons le Raspberry Pi et chargeons les modules nécessaires.
root@R-Pi# modprobe spi_bcm2708 [ 49.245907] bcm2708_spi bcm2708_spi.0: SPI Controller at 0x20204000 (irq 80) root@R-Pi# modprobe spidev root@R-Pi# ls -l /dev/spi* crw------- 1 root root 153, 0 Jan 1 00:00 /dev/spidev0.0 crw------- 1 root root 153, 1 Jan 1 00:00 /dev/spidev0.1 root@R-Pi#
Micro-contrôleur en lecture
J’ai relié le Raspberry Pi avec un micro-contrôleur ATmega32 (en mode Slave) se trouvant sur un carte de développement STK500. Dans un premier temps, le micro-contrôleur lit les données arrivant sur le port SPI et configure ses sorties numériques en conséquence. J’ai modifié l’état des fusibles de l’ATmega32 afin qu’il utilise une horloge interne à 8MHz plutôt que l’horloge à 1MHz par défaut. Pour cela je lis d’abord l’état initial du fusible Low.
[STK500]$ sudo avrdude -P /dev/ttyUSB0 -c stk500 -p m32 -U lfuse:r:-:h [...] avrdude: writing output file "" 0xe1 [...] [STK500]$
Je dois modifier les trois bits de poids faibles en suivant la documentation de l’ATmega32 (pages 29, 257 et 258).
[STK500]$ sudo avrdude -P /dev/ttyUSB0 -c stk500 -p m32 -U lfuse:w:0xe4:m [...] avrdude: input file 0xe4 contains 1 bytes avrdude: reading on-chip lfuse data: [...] avrdude: 1 bytes of lfuse verified [STK500]$
Ensuite je flashe dans l’ATmega32 le petit programme suivant.
/* test-spi-01.c - ATmega32 */ #include <avr/io.h> #include <avr/interrupt.h> ISR(SPI_STC_vect) { PORTD = ~ SPDR; // Write on leds the byte received unsigned char sreg = SPSR; // Clear Interrupt Flag } int main (void) { DDRB = (1<<PB6); // Only MISO on output SPCR = (1<<SPE | 1<<SPIE); // SPI Enable with Interrupt DDRD = 0xFF; // PORTD for output (LEDs) sei(); // Enable global interrupts while (1) ; return 0; }
Ce programme configure le port SPI en mode Slave, puis il effectue une boucle active (vide dans ce cas). Tout le travail est réalisé dans le handler d’interruption qui copie sur le port D le caractère reçu sur le port SPI. Programmons le micro-contrôleur.
[STK500]$ make avr-gcc -Os -g -mmcu=atmega32 -c -o test-spi-01.o test-spi-01.c avr-gcc -Os -g -mmcu=atmega32 -o test-spi-01.out test-spi-01.o avr-objcopy -j .text -O ihex test-spi-01.out test-spi-01.hex rm test-spi-01.out test-spi-01.o [STK500]$ sudo avrdude -P /dev/ttyUSB0 -c stk500 -p m32 -e -U flash:w:test-spi-01.hex avrdude: AVR device initialized and ready to accept instructions [...] Writing | ################################################## | 100% 0.24s [...] Reading | ################################################## | 100% 0.21s [...] avrdude done. Thank you. [STK500]$
Notez que pendant la programmation de l’ATmega32 il est nécessaire de débrancher la ligne SPI Clock du Raspberry Pi (du moins avec mon programmateur STK500).
Rasberry Pi en écriture
Puis j’installe sur la Raspberry Pi le programme suivant qui lit son entrée standard et écrit la valeur indiquée sur le port SPI fourni sur sa ligne de commande.
/* test-spi-rpi-01.c - Programme pour Raspberry Pi */ #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <linux/types.h> #include <linux/spi/spidev.h> #include <sys/ioctl.h> int main(int argc, char *argv[]) { int fd_spi; char ligne[80]; int value; unsigned char byte; unsigned int speed = 250000; if (argc != 3) { fprintf(stderr, "usage: %s <spi-port> <spi-speed>\n", argv[0]); exit(EXIT_FAILURE); } fd_spi = open(argv[1], O_RDWR); if (fd_spi < 0) { perror(argv[1]); exit(EXIT_FAILURE); } if (sscanf(argv[2], "%d", & speed) != 1) { fprintf(stderr, "Wrong value for speed: %s\n", argv[2]); exit(EXIT_FAILURE); } if (ioctl(fd_spi, SPI_IOC_WR_MAX_SPEED_HZ, & speed) != 0) { perror("ioctl"); exit(EXIT_FAILURE); } while (fgets(ligne, 80, stdin) != NULL) { if (sscanf(ligne, "%d", & value) != 1) { fprintf(stderr, "integer value expected\n"); continue; } byte = (unsigned char) (value & 0xFF); if (write(fd_spi, & byte, 1) != 1) { perror("write"); exit(EXIT_FAILURE); } } close(fd_spi); return EXIT_SUCCESS; }
Nous lançons le programme sur le Raspberry Pi en lui indiquant une fréquence d’horloge de 2MHz (le maximum acceptable par l’ATmega32 lorsqu’il fonctionne avec une horloge à 8MHz).
root@R-Pi# ./test-rpi-spi-01 /dev/spidev0.0 2000000 1 2 4 8 16 32 64 128 85 170 237
Les motifs binaires correspondant aux valeurs envoyées sont visibles sur les leds de la carte de développement. Par exemple la valeur 237 (0xED ou 11101101b) est affichée sur la figure ci-dessous.
De même un analyseur logique connecté entre le Raspberry Pi et l’ATmega32 nous permet de voir l’horloge SPI (ligne 1) et la ligne MOSI (ligne 2). Nous pouvons vérifier que la fréquence de l’horloge (mesurée entre les deux repères verticaux O
et M
et affichée en bas de l’écran) est bien de 2MHz.
L’état de la ligne MOSI est lu par l’esclave sur les fronts montants de l’horloge (SPI mode 0). Nous pouvons retrouver la valeur binaire précédente (inscrite manuellement ici).
Lecture depuis un esclave
Micro-contrôleur en écriture
Cette fois l’ATmega32 va transmettre en permanence l’état de son entrée analogique (huit bits de poids forts) vers le Raspberry Pi. Le convertisseur analogique-numérique est programmé en mode Free Running, une interruption est déclenchée à la fin de chaque conversion qui enregistre dans une variable globale la valeur à envoyer au Raspberry Pi.
Le programme est le suivant.
/* test-spi-02.c - ATmega32 */ #include <avr/io.h> #include <avr/interrupt.h> static unsigned char byte_to_write = 0; ISR(SPI_STC_vect) { PORTD = ~ SPDR; unsigned char sreg = SPSR; SPDR = byte_to_write; } ISR(ADC_vect) { unsigned char l = ADCL; unsigned char h = ADCH; unsigned int v = (h << 8) | l; byte_to_write = (v >> 2) & 0xFF; } int main (void) { // Configure SPI Slave DDRB = (1<<PB6); // Only MISO on output SPCR = (1<<SPE | 1<<SPIE); // SPI Enable with Interrupt // Configure ADC on PA0 DDRA = 0x00; // PORTA for input (PA0 for ADC) ADCSRA = 0x80; // ADC Enable ADMUX = 0; // Single Ended Input Channel 0 ADCSRA = 0xE8; // ADC Enable + Start + Interrupt Enable + Auto Trigger // Configure digital outputs DDRD = 0xFF; // PORTD for output (LEDs) sei(); while (1) ; return 0; }
Encore une fois, le micro-contrôleur fonctionne en bouclant « à vide » et tout le travail est réalisé dans les gestionnaires d’interruption.
Raspberry Pi en lecture
Le programme de lecture sur le Raspberry Pi est identique au précédent, seule la boucle centrale de la fonction main()
change.
/* test-spi-rpi-02.c - Programme pour Raspberry Pi */ [...] while (read(fd_spi, & byte, 1) == 1) fprintf(stdout, "r%d (%02X) ", byte); [...]
En lançant le programme sur le Raspberry Pi, nous voyons en continu l’état de l’entrée analogique (reliée ici à un potentiomètre).
root@R-Pi# ./test-rpi-spi-02 /dev/spidev0.0 2000000 163(A3)
L’analyseur logique nous présente sur la figure suivante la ligne MISO sur son canal 3, où l’on reconnaît la valeur 0xA3 (10100011b).
Conclusion
Nous avons abordé la mise en oeuvre simple du lien SPI sur le Raspberry Pi. Nous avons utilisé simplement des opérations read()
et write()
depuis l’espace utilisateur. Nous pouvons également réaliser des opérations plus complexes comme une communication full-duplex – toujours depuis l’espace utilisateur – ou le transfert de données depuis l’espace kernel. Ceci fera l’objet de prochains articles.
PS : Les sources des programmes présentés plus haut sont disponibles ici.
salut,
Tout d’abord je souhaite confirmer le plaisir de lire tes articles ici, qui abordent le sujet traité un peu plus profondément que pour la plupart, installer une chaine de compilation c’est utile, mais juste au début.
Ensuite, je souhaiterai savoir si tu as prévu d’aborder une autre carte que la PI avec les mêmes thèmes ? Je pensais à la i.MX53 QSB ou une olinuXino (A13[1] par ex) Cette dernière à l’avantage d’être vraiment open source + open hardware et ne bénéficie pas trop de documentations/expérimentations.
Bon je te laisse j’ai la 2e partie de Linux From scratch sur Linux Mag à terminer de lire…
DL
[1] https://www.olimex.com/Products/OLinuXino/A13/
Merci de ces commentaires.
Je vais prochainement explorer un peu ici les possibilités de la carte OlinuXino micro avec un iMX233. Pour celle avec un A13, cela viendra un petit peu plus tard (en janvier/février sûrement).
Bon article !
Par contre il manque une parenthèse après la ligne « if (sscanf(ligne, « %d », & value) != 1) ». Je n’ai pas vérifié dans le tarbz2.
J’ai perdu un max de temps à essayer de communiquer avec la nouvelle puce FT220X de ftdi, qui se veut une interface usb vers « SPI » / FT1248. D’après le datasheet, en l’utilisant en mode 1-bit il correspond à un SPI traditionnel. Malheureusement, j’ai constaté assez tardivement que leur SPI est SCLK, MOSI et au lieu du MISO il y a un MIOSIO, ce qui ressemble plutot à un 3-wire SPI. J’ai testé le 3-wire avec le petit script spidev_test mais le raspberry ne semble pas le supporter?
Blague à part: c’est bien la confirmation qu’il ne faut pas toujours se fier à wikipedia! « When someone says a part supports SPI or Microwire, you can normally assume that means the four-wire version. » (http://en.wikipedia.org/wiki/Serial_Peripheral_Interface_Bus#Three-wire_serial_buses)
En attendant je suis passé sur FT200X qui est la version I2C de la puce et là, aucun problème. Evidemment c’est une solution sans être la solution.
Bon article !
Par contre il manque une parenthèse après la ligne « if (sscanf(ligne, « %d », & value) != 1) ». Je n’ai pas vérifié dans le tarbz2.
J’ai perdu un max de temps à essayer de communiquer avec la nouvelle puce FT220X de ftdi, qui se veut une interface usb vers « SPI » / FT1248. D’après le datasheet, en l’utilisant en mode 1-bit il correspond à un SPI traditionnel. Malheureusement, j’ai constaté assez tardivement que leur SPI est SCLK, MOSI et au lieu du MISO il y a un MIOSIO, ce qui ressemble plutot à un 3-wire SPI. J’ai testé le 3-wire avec le petit script spidev_test mais le raspberry ne semble pas le supporter?
Blague à part: c’est bien la confirmation qu’il ne faut pas toujours se fier à wikipedia! « When someone says a part supports SPI or Microwire, you can normally assume that means the four-wire version. » (http://en.wikipedia.org/wiki/Serial_Peripheral_Interface_Bus#Three-wire_serial_buses)
En attendant je suis passé sur FT200X qui est la version I2C de la puce et là, aucun problème. Evidemment c’est une solution sans être la solution.
Bonjour,
D’habitude je branche des trucs et suis les docs, puis j’adapte.
Je n’arrive pas a faire fonctionner mon MFRC522 et du coup j’approfondis mes connaissances sur le bus SPI…
Merci pour la qualité de l’article…
JOSS
Bonjour Monsieur Blaess,
merci beaucoup pour cette article très intéressant. J’ai réussi à envoyer des trames sur le SPI en passant par le shell, je vais maitenant essayer en C. j’ai cependant remarqué qu’entre le moment où le signal !ss passe à zéro et l’envoie des trames sur MISO, il y a un temps long et variable. Réciproquement, quand la trame est terminée, il existe un temps long et variable entre la fin de la trame et le passage à 1 du !ss. Savez-vous si il est possible d’obtenir un temps plus court et plus prédictible ?
Merci d’avance pour votre réponse.
Olivier