La création d'un logiciel pour interroger un capteur SPI recèle un certain nombre de pièges, liés aux particularités de ce mode de communication. Cet article vous fournira quelques conseils pour éviter les écueils les plus fréquents.
Nous allons prendre l'exemple de l'inclinomètre de précision Murata SCL3300, qu'on peut acheter directement soudé sur un break-out board très pratique pour des tests.
Break-out board pour le SCL3300, avec un Yocto-SPI pour l'interfacer
Etape 1: Configurer le Yocto-SPI
Prenez le temps de lire avec soin le datasheet ou le manuel du capteur, et n'hésitez pas à y revenir ultérieurement autant de fois que nécessaire. Si vous avez l'impression qu'il y a une erreur dans le datasheet, rappelez vous que ce genre de cas est extrêmement rare, et que c'est probablement plutôt un problème de protocole de communication à corriger.
Pour implémenter correctement le logiciel de communication, les premiers éléments à chercher dans le datasheet sont:
a. Les tensions d'alimentation et de communication, ainsi que la consommation de l'alimentation du capteur. Le Yocto-SPI peut fournir une alimentation 3.3V (200mA) ou 5V (450mA). Pour le capteur SCL3300, ces informations sont à deux endroits différents.
Nous allons donc pouvoir alimenter le capteur directement par le Yocto-SPI, que notre logiciel pourra configurer comme suit:
powerOutput.set_voltage(YPowerOutput.VOLTAGE_OUT3V3);
b. La fréquence de communication minimale et maximale supportées. Commencez toujours par une fréquence assez basse pour le développement, pour éviter de cumuler les sources de problèmes potentiels, vous pourrez monter la vitesse ultérieurement une fois que vous serez sûr de vous. Voici les spécifications pour notre capteur:
Nous utiliserons donc la fréquence minimale de 100 KHz pour le développement.
c. Le mode de communication SPI, qui correspond à la convention de synchronisation sur l'horloge. Parfois le numéro du mode est donnée explicitement, parfois c'est les polarités CPOL et CPHA, et parfois c'est juste un diagramme de communication qui est donné. Référez-vous à la page Wikipedia si vous avez un doute sur la correspondance. La spécification du Murata est très claire:
Attention, le capteur peut parfois donner l'impression de fonctionner avec un mode erroné, par exemple lorsque la mauvaise polarité d'horloge est utilisée, mais des erreurs de communication occasionnelles se produiront alors.
d. L'ordre de transfert des bits. Le plus souvent c'est le bit de poids fort qui est transmis en premier (msb), mais il vaut toujours mieux vérifier.
En combinant ces trois dernières informations, on peut effectuer la configuration de base de la communication SPI:
e. La convention d'utilisation de la ligne SS (Slave Select), parfois appelée CS (Chip Select) ou CSB (Chip Select Bar). Pour certains capteurs, cette ligne ne sert qu'à multiplexer plusieurs chips sur le bus SPI mais n'intervient pas directement dans le protocole de communication. Pour d'autres, comme le capteur Murata, la ligne CS sert à séparer les messages (frames) et doit donc impérativement être activée et désactivée pour chaque groupe de 4 octets. Il doit y avoir au minimum 10[us] de délai entre chaque message:
Le Yocto-SPI peut gérer automatiquement le pilotage de la ligne SS (CSB) par message: il suffit de le configurer en mode Frame et d'indiquer l'espace minimal désiré entre les messages, ainsi que la polarité de la ligne SS:
spiPort.set_ssPolarity(YSpiPort.SSPOLARITY_ACTIVE_LOW);
Ainsi chaque commande write envoyée sur le port SPI conduira à activer la ligne SS, envoyer les octets demandés et désactiver la ligne SS. Mais pour cela il faudra bien entendu prendre soin d'envoyer les quatre octets à la fois, et non un par un. Les octets en retour arriveront aussi spontanément par messages, et l'utilisation d'un protocole Frame les traduira automatiquement sous forme hexadécimale.
Etape 2: Comprendre comment communique le capteur
Chaque capteur SPI a une logique un peu différente, mais on retrouve des principes communs à presque tous les capteurs SPI.
L'accès aux capteurs SPI est en général structuré sous forme de différents registres que l'on peut lire ou écrire. Certains registres servent à configurer le capteur, alors que d'autres contiennent les dernières mesures ou les indications d'état du capteur. On va donc vouloir commencer par configurer le capteur en écrivant dans quelques registres, puis lire continuellement les registres de mesure.
S'il s'agissait d'une liaison série asynchrone (RS232, RS485, etc.), ce serait effectivement aussi simple que cela. Mais la particularité des protocoles de communication SPI est que chaque échange est nécessairement bidirectionnel: pour chaque octet envoyé, on reçoit forcément simultanément un octet en retour, et de même on ne peut pas lire un octet sans en envoyer un autre. Par ailleurs, comme le capteur n'a en général pas la possibilité de réagir instantanément à une commande en cours de transmission, les octets qu'ils transmet sont toujours la réponse au message précédent envoyé. C'est ce qui est illustré dans la datasheet du capteur Murate:
Au niveau de l'implémentation du protocole, il va donc falloir tenir compte de cela, ce qui donnerait par exemple le code suivant, qui peut paraître déroutant:
spiPort.writeHex(READ_ID); // demande le sensor ID
spiPort.readHex(4); // ignore la "1ère réponse"
spiPort.writeHex(READ_STATUS); // demande le statut
string ID = spiPort.readHex(4); // lit le sensor ID
spiPort.writeHex(ANY_COMMAND); // déclenche une trame de plus
string status = spiPort.readHex(4); // lit le statut
Notez l'utilisation de la méthode reset() au début pour effacer le contenu des tampons de communication dans le module avant le début de l'opération, c'est très important pour éviter de lire accidentellement des messages issus de communications précédentes!
A ce stade, il faut encore prendre garde aux délais de transmission, car les fonctions writeHex et readHex sont des fonctions non-bloquantes. Dans le cas de writeHex, la trame à envoyer est stockée dans le tampon d'envoi module, et l'appel rend immédiatement la main, avant que l'envoi soit terminé. L'appel suivant à readHex va lire les 4 prochains octets du tampon de lecture, mais seulement si la transmission a effectivement déjà eu lieu. Sinon, l'appel risque de retourner une chaîne vide. Il y a plusieurs manières de palier à ce problème.
Solution 1: temporiser arbitrairement. Si on sait le temps nécessaire à la transmission, on peut ajouter un simple appel à YAPI.Sleep() pour ajouter un délai de quelques millisecondes avant chaque appel à readHex(). C'est le plus simple, mais pas le plus efficace.
Solution 2: répéter la lecture. On peut aussi faire une fonction qui appelle readHex() en boucle jusqu'à obtenir des données. Cela peut être plus rapide que la solution précédente, mais cela fait une attente active, qui n'est pas non plus la manière la plus efficace.
Solution 3: attendre la réponse. Plutôt que d'utiliser writeHex/readHex, on peut utiliser la méthode queryHex() qui effectue l'envoi et attend l'arrivée de la réponse avant de rendre le contrôle (lecture bloquante). C'est sémantiquement plus correct, mais cela va conduire à un code assez étrange en raison du décalage entre l'envoi des commandes et la réception des réponses lors de la commande suivante.
spiPort.queryHex(READ_ID);
string ID = spiPort.queryHex(READ_STATUS);
string status = spiPort.queryHex(ANY_COMMAND);
Solution 4: sérialiser les appels. La méthode que nous préférons consiste à utiliser les tampons du Yocto-SPI plutôt que d'attendre entre chaque commande: autant envoyer toutes les commandes de suite, attendre la fin des transactions, et lire tous les résultats. Cela rend aussi le code plus lisible, puisque le décalage ne se traduit plus que par un envoi supplémentaire à la fin, et une lecture supplémentaire au début:
spiPort.writeHex(READ_ID); // demande le sensor ID
spiPort.writeHex(READ_STATUS); // demande le statut
spiPort.writeHex(ANY_COMMAND); // déclanche une trame de plus
while(spiPort.read_avail() < 12) {
YAPI.Sleep(2, ref errmsg);
}
string reply = spiPort.readHex(12); // lit tous les 3 messages
string ID = reply.Substring(8, 8); // lit le sensor ID
string status = reply.Substring(16, 8); // lit le statut
Dans tous les cas, cela sera plus rapide que les méthodes précédentes car à la place d'attendre entre chaque commande, les commandes sont toutes regroupées et il n'y a plus qu'une seule attente nécessaire.
Etape 3: Initialiser le capteur et tester la communication
Une fois que l'on a compris comment communiquer avec le capteur, on peut essayer de s'y lancer. En général les capteurs ont toujours besoin d'une séquence d'initialisation et de configuration. Au strict minimum, l'initialisation devrait comporter la lecture d'un registre dont la valeur est connue à l'avance, comme un identifiant, de manière à s'assurer que la communication SPI est opérationnelle.
Respectez scrupuleusement les indications données dans le datasheet, en particulier en ce qui concerne les délais minimums entre la mise sous tension et la première commande, et la séquence des commandes à appliquer. Et surtout, vérifiez toutes les valeurs retournées par chaque commande, et non seulement les bits qui vous semblent utiles. Ainsi, vous détecterez beaucoup plus rapidement les éventuels décalages de protocole, erreur assez fréquente, ce qui vous gagnera un temps considérable.
Dans le cas du capteur SCL3300 par exemple, il faut attendre 20 millisecondes, sélectionner le mode de fonctionnement désiré, attendre 5 millisecondes, lire le registre de statut deux fois et ensuite seulement on peut vérifier si tout va bien, comme indiqué dans la table 10 du datasheet. Voici donc à quoi ressemble notre code d'initialisation au final:
spiPort.set_voltageLevel(YSpiPort.VOLTAGELEVEL_TTL3V);
spiPort.set_spiMode("100000,0,msb");
spiPort.set_protocol("Frame:1ms");
spiPort.set_ssPolarity(YSpiPort.SSPOLARITY_ACTIVE_LOW);
module.saveToFlash();
YAPI.Sleep(25, ref errmsg);
spiPort.writeHex(SET_MODE_4);
YAPI.Sleep(5, ref errmsg);
string[] commands = {
READ_STATUS, READ_STATUS, READ_STATUS, READ_ID, SET_ANGLES
};
Frame[] result;
if(!SendAndReceive(commands, out result))
{
Console.WriteLine("Failed to initialize SCL3300 (communication error)");
return false;
}
if(!_chip_ready)
{
Console.WriteLine("SCL3300 startup failed (rs={4})", result[2].rs);
return false;
}
if((result[3].data & 0xff) != 0xc1)
{
Console.WriteLine("Unexpected SCL3300 identification (WHOAMI={0})",
(result[3].data & 0xff));
return false;
}
if(!DecodeStatus(result[2]))
{
Console.WriteLine("SCL3300 Status bad, chip reset is required");
return false;
}
Console.WriteLine("SCL3300 is ready");
return true;
Si vous désirez plus de détails, vous trouverez le code complet sur GitHub.
Etape 4: Interroger le capteur
Une fois que vous avez une initialisation qui fonctionne, l'interrogation ne représente plus aucun problème. Il vous suffit de trouver le registre qui contient les données et de le lire périodiquement:
READ_ANGLE_X, READ_ANGLE_Y, READ_ANGLE_Z, READ_STATUS
};
Frame[] result;
if (!SendAndReceive(commands, out result))
{
Console.WriteLine("Failed to read from SCL3300 (communication error)");
return;
}
_angle_x = (double)result[0].data / (1 << 14) * 90.0;
_angle_y = (double)result[1].data / (1 << 14) * 90.0;
_angle_z = (double)result[2].data / (1 << 14) * 90.0;
DecodeStatus(result[3]);
Conclusion
Avec ces explications, vous devriez être capable de coder dans n'importe quel langage supporté par les librairies de programmation Yoctopuce une communication par messages SPI.
La semaine prochaine, on vous montrera comment rendre le code de votre application bien plus simple et plus efficace en configurant le Yocto-SPI pour qu'il effectue lui-même l'interrogation du capteur.