Introduction à SPI
L' interface SPI ( Serial Peripheral Interface Bus ) est utilisée pour la communication entre plusieurs périphériques sur de courtes distances et à grande vitesse.
Il existe généralement un seul périphérique "maître" qui initie la communication et fournit l'horloge qui contrôle le débit de transfert de données. Il peut y avoir un ou plusieurs esclaves. Pour plusieurs esclaves, chacun a son propre signal "sélection d'esclave", décrit plus loin.
Signaux SPI
Dans un système SPI complet, vous aurez quatre lignes de signal:
- Master Out, Slave In ( MOSI ) - ce sont les données allant du maître à l'esclave
- Entrée maître, sortie esclave ( MISO ) - ce sont les données allant de l'esclave au maître
- Serial Clock ( SCK ) - lorsque cette option bascule les bits maître et esclave sur le bit suivant
- Slave Select ( SS ) - Ceci dit à un esclave particulier de devenir "actif"
Lorsque plusieurs esclaves sont connectés au signal MISO, ils sont supposés passer à l'état trois fois (conserver une haute impédance) de la ligne MISO jusqu'à ce qu'ils soient sélectionnés par Slave Select. Normalement, Slave Select (SS) devient faible pour l'affirmer. C'est-à-dire qu'il est actif bas. Une fois qu'un esclave particulier est sélectionné, il convient de configurer la ligne MISO en tant que sortie afin de pouvoir envoyer des données au maître.
Cette image montre la manière dont les données sont échangées lorsqu’un octet est envoyé:
Notez que trois signaux sont émis par le maître (MOSI, SCK, SS) et l’un est une entrée (MISO).
Timing
La séquence d'événements est la suivante:
SS
va bas pour l'affirmer et activer l'esclave
- La
SCK
ligne bascule pour indiquer quand les lignes de données doivent être échantillonnées
- Les données sont échantillonnées par le maître et l' esclave sur le premier bord
SCK
( en utilisant la phase d'horloge par défaut)
- Le maître et l'esclave préparent le bit suivant sur le bord arrière de
SCK
(en utilisant la phase d'horloge par défaut), en modifiant MISO
/ MOSI
si nécessaire
- Une fois que la transmission est terminée (éventuellement après l'envoi de plusieurs octets), elle
SS
passe ensuite à l'état haut pour l'annuler.
Notez que:
- Le bit le plus significatif est envoyé en premier (par défaut)
- Les données sont envoyées et reçues au même instant (duplex intégral)
Les données étant envoyées et reçues sur la même impulsion d'horloge, il n'est pas possible pour l'esclave de répondre immédiatement au maître. Les protocoles SPI attendent généralement du maître qu'il demande des données lors d'une transmission et qu'il reçoive une réponse lors d'une transmission ultérieure.
En utilisant la bibliothèque SPI sur l’Arduino, effectuer un seul transfert ressemble à ceci dans le code:
byte outgoing = 0xAB;
byte incoming = SPI.transfer (outgoing);
Exemple de code
Exemple d'envoi uniquement (en ignorant les données entrantes):
#include <SPI.h>
void setup (void)
{
digitalWrite(SS, HIGH); // ensure SS stays high
SPI.begin ();
} // end of setup
void loop (void)
{
byte c;
// enable Slave Select
digitalWrite(SS, LOW); // SS is pin 10
// send test string
for (const char * p = "Fab" ; c = *p; p++)
SPI.transfer (c);
// disable Slave Select
digitalWrite(SS, HIGH);
delay (100);
} // end of loop
Câblage pour SPI de sortie uniquement
Le code ci-dessus (qui envoie uniquement) peut être utilisé pour piloter un registre à décalage série de sortie. Ce ne sont que des périphériques de sortie, nous n'avons donc pas à nous soucier des données entrantes. Dans leur cas, la broche SS pourrait être appelée "broche" ou "verrou".
Le registre à décalage série 74HC595 et diverses bandes de LED en sont quelques exemples. Par exemple, cet écran LED de 64 pixels piloté par une puce MAX7219:
Dans ce cas, vous pouvez voir que le fabricant de la carte a utilisé des noms de signaux légèrement différents:
- DIN (entrée de données) est MOSI (sortie maître, entrée esclave)
- CS (Chip Select) est SS (Slave Select)
- CLK (horloge) est SCK (horloge série)
La plupart des conseils vont suivre un modèle similaire. Parfois, DIN est juste DI (Data In).
Voici un autre exemple, cette fois une carte d'affichage à LED à 7 segments (également basée sur la puce MAX7219):
Ceci utilise exactement les mêmes noms de signaux que l’autre tableau. Dans ces deux cas, vous pouvez voir que la carte n'a besoin que de 5 fils, les trois pour SPI, plus l'alimentation et la terre.
Phase d'horloge et polarité
Vous pouvez échantillonner l'horloge SPI de quatre manières.
Le protocole SPI permet des variations sur la polarité des impulsions d'horloge. CPOL est la polarité de l'horloge et l'ACSP est la phase d'horloge.
- Mode 0 (par défaut) - l'horloge est normalement basse (CPOL = 0) et les données sont échantillonnées lors du passage de bas à haut (bord d'attaque) (CPHA = 0)
- Mode 1 - l'horloge est normalement basse (CPOL = 0) et les données sont échantillonnées lors du passage de haut en bas (bord arrière) (CPHA = 1)
- Mode 2 - l'horloge est normalement haute (CPOL = 1) et les données sont échantillonnées lors du passage de la position haute à la position basse (front montant) (CPHA = 0)
- Mode 3 - l'horloge est normalement haute (CPOL = 1) et les données sont échantillonnées lors du passage de bas à haut (bord de fuite) (CPHA = 1)
Ceux-ci sont illustrés dans ce graphique:
Vous devez vous reporter à la fiche technique de votre appareil pour obtenir la phase et la polarité correctes. Il y aura généralement un diagramme qui montre comment échantillonner l'horloge. Par exemple, à partir de la fiche technique de la puce 74HC595:
Comme vous pouvez le constater, l'horloge est normalement basse (CPOL = 0) et est échantillonnée sur le bord d'attaque (CPHA = 0). Il s'agit donc du mode SPI 0.
Vous pouvez changer la polarité de l'horloge et la phase dans le code comme ceci (choisissez-en un, bien sûr):
SPI.setDataMode (SPI_MODE0);
SPI.setDataMode (SPI_MODE1);
SPI.setDataMode (SPI_MODE2);
SPI.setDataMode (SPI_MODE3);
Cette méthode est déconseillée depuis les versions 1.6.0 et supérieures de l'IDE Arduino. Pour les versions récentes, vous modifiez le mode d'horloge dans l' SPI.beginTransaction
appel, comme suit:
SPI.beginTransaction (SPISettings (2000000, MSBFIRST, SPI_MODE0)); // 2 MHz clock, MSB first, mode 0
Commande de données
Le bit par défaut est le plus significatif en premier, mais vous pouvez demander au matériel de traiter le bit le moins significatif en premier, comme ceci:
SPI.setBitOrder (LSBFIRST); // least significant bit first
SPI.setBitOrder (MSBFIRST); // most significant bit first
Encore une fois, ceci est déconseillé dans les versions 1.6.0 et suivantes de l'IDE Arduino. Pour les versions récentes, vous modifiez l'ordre des bits dans l' SPI.beginTransaction
appel, comme suit:
SPI.beginTransaction (SPISettings (1000000, LSBFIRST, SPI_MODE2)); // 1 MHz clock, LSB first, mode 2
La vitesse
Le paramètre par défaut pour SPI consiste à utiliser la vitesse d'horloge système divisée par quatre, c'est-à-dire une impulsion d'horloge SPI toutes les 250 ns, en supposant une horloge de processeur de 16 MHz. Vous pouvez changer le diviseur d'horloge en utilisant setClockDivider
comme ceci:
SPI.setClockDivider (divider);
Où "diviseur" est l'un des:
- SPI_CLOCK_DIV2
- SPI_CLOCK_DIV4
- SPI_CLOCK_DIV8
- SPI_CLOCK_DIV16
- SPI_CLOCK_DIV32
- SPI_CLOCK_DIV64
- SPI_CLOCK_DIV128
Le taux le plus rapide est "diviser par 2" ou une impulsion d'horloge SPI toutes les 125 ns, en supposant une horloge de processeur de 16 MHz. Il faudrait donc 8 * 125 ns ou 1 µs pour transmettre un octet.
Cette méthode est déconseillée depuis les versions 1.6.0 et supérieures de l'IDE Arduino. Pour les versions récentes, vous modifiez la vitesse de transfert dans l' SPI.beginTransaction
appel, comme suit:
SPI.beginTransaction (SPISettings (4000000, MSBFIRST, SPI_MODE0)); // 4 MHz clock, MSB first, mode 0
Cependant, des tests empiriques montrent qu'il est nécessaire de disposer de deux impulsions d'horloge entre octets, de sorte que la cadence maximale à laquelle les octets peuvent être cadencés est de 1,125 µs chacun (avec un diviseur d'horloge de 2).
Pour résumer, chaque octet peut être envoyé à un débit maximal de un par 1,125 µs (avec une horloge de 16 MHz), ce qui donne un taux de transfert maximal théorique de 1 / 1,125 µs, soit 888 888 octets par seconde (sans le temps système comme le réglage SS sur).
Connexion à Arduino
Arduino Uno
Connexion via les broches numériques 10 à 13:
Connexion via l'en-tête ICSP:
Arduino Atmega2560
Connexion via les broches numériques 50 à 52:
Vous pouvez également utiliser l'en-tête ICSP, similaire à l'Uno ci-dessus.
Arduino Leonardo
Le Leonardo et le Micro n’exposent pas les broches SPI sur les broches numériques, contrairement aux Uno et Mega. Votre seule option est d'utiliser les broches d'en-tête ICSP, comme illustré ci-dessus pour l'Uno.
Plusieurs esclaves
Un maître peut communiquer avec plusieurs esclaves (mais un seul à la fois). Pour ce faire, il affirme SS pour un esclave et le désaffirme pour tous les autres. L'esclave qui a SS affirmé (généralement cela signifie LOW) configure sa broche MISO en tant que sortie afin que l'esclave, et cet esclave seul, puisse répondre au maître. Les autres esclaves ignorent les impulsions d'horloge entrantes si SS n'est pas affirmé. Ainsi, vous avez besoin d’un signal supplémentaire pour chaque esclave, comme ceci:
Dans ce graphique, vous pouvez voir que MISO, MOSI, SCK sont partagés entre les deux esclaves, mais chaque esclave a son propre signal SS (sélection d'esclave).
Protocoles
La spécification SPI ne spécifie pas les protocoles en tant que tels, il appartient donc à chaque paire maître / esclave de s’entendre sur la signification des données. Bien que vous puissiez envoyer et recevoir des octets simultanément, l’octet reçu ne peut pas être une réponse directe à l’octet envoyé (car ils sont assemblés simultanément).
Il serait donc plus logique pour une extrémité d'envoyer une requête (par exemple, 4 pourrait signifier "lister le répertoire du disque"), puis d'effectuer des transferts (éventuellement en envoyant des zéros vers l'extérieur) jusqu'à réception d'une réponse complète. La réponse peut se terminer par un caractère de nouvelle ligne ou 0x00.
Lisez la fiche technique de votre périphérique esclave pour connaître les séquences de protocole attendues.
Comment faire un esclave SPI
L'exemple précédent montre que l'Arduino est le maître, envoyant des données à un périphérique esclave. Cet exemple montre comment l’Arduino peut être un esclave.
Configuration materielle
Connectez deux Arduino Unos ensemble avec les broches suivantes connectées l'une à l'autre:
- 10 (SS)
- 11 (MOSI)
- 12 (MISO)
13 (SCK)
+ 5v (si nécessaire)
- GND (pour le retour du signal)
Sur l’Arduino Mega, les broches sont 50 (MISO), 51 (MOSI), 52 (SCK) et 53 (SS).
Dans tous les cas, MOSI à une extrémité est connecté à MOSI à l'autre, vous ne les échangez pas (c'est-à-dire que vous n'avez pas MOSI <-> MISO). Le logiciel configure l'une des extrémités de MOSI (extrémité principale) en tant que sortie et l'autre extrémité (extrémité esclave) en tant qu'entrée.
Exemple de maître
#include <SPI.h>
void setup (void)
{
digitalWrite(SS, HIGH); // ensure SS stays high for now
// Put SCK, MOSI, SS pins into output mode
// also put SCK, MOSI into LOW state, and SS into HIGH state.
// Then put SPI hardware into Master mode and turn SPI on
SPI.begin ();
// Slow down the master a bit
SPI.setClockDivider(SPI_CLOCK_DIV8);
} // end of setup
void loop (void)
{
char c;
// enable Slave Select
digitalWrite(SS, LOW); // SS is pin 10
// send test string
for (const char * p = "Hello, world!\n" ; c = *p; p++)
SPI.transfer (c);
// disable Slave Select
digitalWrite(SS, HIGH);
delay (1000); // 1 seconds delay
} // end of loop
Exemple esclave
#include <SPI.h>
char buf [100];
volatile byte pos;
volatile bool process_it;
void setup (void)
{
Serial.begin (115200); // debugging
// turn on SPI in slave mode
SPCR |= bit (SPE);
// have to send on master in, *slave out*
pinMode (MISO, OUTPUT);
// get ready for an interrupt
pos = 0; // buffer empty
process_it = false;
// now turn on interrupts
SPI.attachInterrupt();
} // end of setup
// SPI interrupt routine
ISR (SPI_STC_vect)
{
byte c = SPDR; // grab byte from SPI Data Register
// add to buffer if room
if (pos < sizeof buf)
{
buf [pos++] = c;
// example: newline means time to process buffer
if (c == '\n')
process_it = true;
} // end of room available
} // end of interrupt routine SPI_STC_vect
// main loop - wait for flag set in interrupt routine
void loop (void)
{
if (process_it)
{
buf [pos] = 0;
Serial.println (buf);
pos = 0;
process_it = false;
} // end of flag set
} // end of loop
L'esclave est entièrement guidé par des interruptions, il peut donc faire d'autres choses. Les données SPI entrantes sont collectées dans une mémoire tampon et un indicateur est défini lorsqu'un "octet significatif" (dans ce cas, une nouvelle ligne) arrive. Cela indique à l'esclave de démarrer et de traiter les données.
Exemple de connexion de maître à esclave à l'aide de SPI
Comment obtenir une réponse d'un esclave
Dans la suite du code ci-dessus, qui envoie des données d’un maître SPI à un esclave, l’exemple ci-dessous montre l’envoi de données à un esclave, le fait en faire quelque chose et renvoie une réponse.
Le maître est similaire à l'exemple ci-dessus. Cependant, un point important est la nécessité d'ajouter un léger délai (environ 20 microsecondes). Sinon, l'esclave n'a pas la possibilité de réagir aux données entrantes et de faire quelque chose avec.
L'exemple montre l'envoi d'une "commande". Dans ce cas, "a" (ajouter quelque chose) ou "s" (soustraire quelque chose). Cela montre que l'esclave est en train de faire quelque chose avec les données.
Après avoir activé la sélection par esclave (SS) pour lancer la transaction, le maître envoie la commande, suivie d’un nombre quelconque d’octets, puis déclenche la SS pour mettre fin à la transaction.
Un point très important est que l'esclave ne peut pas répondre à un octet entrant au même moment. La réponse doit être dans l'octet suivant. Cela est dû au fait que les bits envoyés et les bits reçus sont envoyés simultanément. Ainsi, pour ajouter quelque chose à quatre chiffres, nous avons besoin de cinq transferts, comme ceci:
transferAndWait ('a'); // add command
transferAndWait (10);
a = transferAndWait (17);
b = transferAndWait (33);
c = transferAndWait (42);
d = transferAndWait (0);
Nous demandons d’abord une action sur le numéro 10. Mais nous n’obtenons pas de réponse avant le prochain transfert (celui sur 17). Toutefois, "a" sera défini sur la réponse à 10. Enfin, nous finirons par envoyer un "factice" numéro 0, afin d'obtenir la réponse pour 42.
Maître (exemple)
#include <SPI.h>
void setup (void)
{
Serial.begin (115200);
Serial.println ();
digitalWrite(SS, HIGH); // ensure SS stays high for now
SPI.begin ();
// Slow down the master a bit
SPI.setClockDivider(SPI_CLOCK_DIV8);
} // end of setup
byte transferAndWait (const byte what)
{
byte a = SPI.transfer (what);
delayMicroseconds (20);
return a;
} // end of transferAndWait
void loop (void)
{
byte a, b, c, d;
// enable Slave Select
digitalWrite(SS, LOW);
transferAndWait ('a'); // add command
transferAndWait (10);
a = transferAndWait (17);
b = transferAndWait (33);
c = transferAndWait (42);
d = transferAndWait (0);
// disable Slave Select
digitalWrite(SS, HIGH);
Serial.println ("Adding results:");
Serial.println (a, DEC);
Serial.println (b, DEC);
Serial.println (c, DEC);
Serial.println (d, DEC);
// enable Slave Select
digitalWrite(SS, LOW);
transferAndWait ('s'); // subtract command
transferAndWait (10);
a = transferAndWait (17);
b = transferAndWait (33);
c = transferAndWait (42);
d = transferAndWait (0);
// disable Slave Select
digitalWrite(SS, HIGH);
Serial.println ("Subtracting results:");
Serial.println (a, DEC);
Serial.println (b, DEC);
Serial.println (c, DEC);
Serial.println (d, DEC);
delay (1000); // 1 second delay
} // end of loop
Le code de l'esclave fait pratiquement tout dans la routine d'interruption (appelée lorsque les données SPI entrantes arrivent). Il prend l'octet entrant et ajoute ou soustrait selon "l'octet de commande" mémorisé. Notez que la réponse sera "collectée" la prochaine fois dans la boucle. C'est pourquoi le maître doit envoyer un dernier transfert "factice" pour obtenir la réponse finale.
Dans mon exemple, j'utilise la boucle principale pour détecter simplement quand SS monte et efface la commande sauvegardée. De cette façon, lorsque SS est à nouveau bas pour la transaction suivante, le premier octet est considéré comme l'octet de commande.
De manière plus fiable, cela se ferait avec une interruption. En d’autres termes, vous devez connecter physiquement le SS à l’une des entrées d’interruption (par exemple, sur l’Uno), connecter la broche 10 (SS) à la broche 2 (une entrée d’interruption) ou utiliser une interruption de changement de broche sur la broche 10.
Ensuite, l’interruption peut être utilisée pour remarquer que le SS est tiré bas ou haut.
Esclave (exemple)
// what to do with incoming data
volatile byte command = 0;
void setup (void)
{
// have to send on master in, *slave out*
pinMode(MISO, OUTPUT);
// turn on SPI in slave mode
SPCR |= _BV(SPE);
// turn on interrupts
SPCR |= _BV(SPIE);
} // end of setup
// SPI interrupt routine
ISR (SPI_STC_vect)
{
byte c = SPDR;
switch (command)
{
// no command? then this is the command
case 0:
command = c;
SPDR = 0;
break;
// add to incoming byte, return result
case 'a':
SPDR = c + 15; // add 15
break;
// subtract from incoming byte, return result
case 's':
SPDR = c - 8; // subtract 8
break;
} // end of switch
} // end of interrupt service routine (ISR) SPI_STC_vect
void loop (void)
{
// if SPI not active, clear current command
if (digitalRead (SS) == HIGH)
command = 0;
} // end of loop
Exemple de sortie
Adding results:
25
32
48
57
Subtracting results:
2
9
25
34
Adding results:
25
32
48
57
Subtracting results:
2
9
25
34
Sortie de l'analyseur logique
Cela montre le délai entre l'envoi et la réception dans le code ci-dessus:
Nouvelle fonctionnalité dans IDE 1.6.0 et versions ultérieures
La version 1.6.0 de l'EDI a, dans une certaine mesure, modifié le fonctionnement de SPI. Vous devez toujours faire SPI.begin()
avant d'utiliser SPI. Cela configure le matériel SPI. Cependant maintenant, lorsque vous êtes sur le point de commencer à communiquer avec un esclave , vous aussi faites SPI.beginTransaction()
pour mettre en place SPI (pour cet esclave) avec le bon:
- Vitesse de l'horloge
- Ordre de bits
- Phase d'horloge et polarité
Lorsque vous avez fini de communiquer avec l'esclave, vous appelez SPI.endTransaction()
. Par exemple:
SPI.beginTransaction (SPISettings (2000000, MSBFIRST, SPI_MODE0));
digitalWrite (SS, LOW); // assert Slave Select
byte foo = SPI.transfer (42); // do a transfer
digitalWrite (SS, HIGH); // de-assert Slave Select
SPI.endTransaction (); // transaction over
Pourquoi utiliser SPI?
J'ajouterais une question préliminaire: quand / pourquoi utiliseriez-vous SPI? Un besoin de configuration multi-maîtres ou un très grand nombre d'esclaves inclinerait la balance vers I2C.
Ceci est une excellente question. Mes réponses sont:
- Certains appareils (assez nombreux) ne prennent en charge que la méthode de transfert SPI. Par exemple, le registre à décalage de sortie 74HC595, le registre à décalage d'entrée 74HC165, le pilote de DEL MAX7219 et quelques bandes de DEL que j'ai vues. Vous pouvez donc l'utiliser car le périphérique cible ne le prend en charge que.
- SPI est vraiment la méthode la plus rapide disponible sur les puces Atmega328 (et similaires). Le débit le plus rapide cité ci-dessus est de 888 888 octets par seconde. En utilisant I 2 C, vous ne pouvez obtenir qu’environ 40 000 octets par seconde. Les frais généraux de l'I 2 C sont assez importants, et si vous essayez d'interfacer très rapidement, SPI est le choix préféré. Tout à fait quelques familles de puces (par exemple. MCP23017 et MCP23S17) soutiennent en fait les deux I 2 C et SPI de sorte que vous pouvez choisir souvent entre la vitesse et la possibilité d'avoir plusieurs périphériques sur un seul bus.
- Les périphériques SPI et I 2 C sont tous deux pris en charge matériellement sur l’Atmega328. Vous pouvez donc éventuellement effectuer un transfert via SPI simultanément avec I 2 C, ce qui vous donnerait un coup de pouce en vitesse.
Les deux méthodes ont leur place. I 2 C vous permet de connecter plusieurs périphériques à un seul bus (deux fils, plus la terre). Ce serait donc le choix à privilégier si vous aviez besoin d'interroger un nombre important de périphériques, peut-être assez rarement. Cependant, la vitesse de SPI pourrait être plus pertinente dans les situations où vous devez produire rapidement (par exemple une bande de LED) ou entrer rapidement (par exemple un convertisseur ADC).
Les références