J’ai eu besoin, pour un projet, d’un nombre d’entrées/sorties conséquent. J’ai décidé d’utiliser des expander d’IO sur bus SPI. Ca tombe bien, le S3C2440 (processeur qui équipe la mini2440) intègre deux interfaces SPI. Par contre, il n’y a pas de driver SPI dans le BSP de la mini2440... Je vais donc le développer moi-même.
Remarque : le processeur peut fonctionner en maître ou en esclave sur le bus SPI. Le driver que je vais développer ne va supporter que le mode maître.
SPI sur S3C2440
J’ai commencé par regarder comment le S3C2440 gère le bus SPI en lisant le datasheet (téléchargeable ici)
Le S3C2440 intègre 2 interfaces SPI, le fonctionnement est le même pour chacune.
La gestion du bus SPI passe par les registres suivants :
SPCONx : configuration du module SPI du processeur
SPSTAx : état du module SPI
SPPINx : configuration de la pin nSS
SPPREx : configuration de la vitesse de transmission utilisée
SPTDATx : registre d’émission
SPRDATx : registre de réception
Mon but ici n’est pas d’expliquer toutes les subtilités du fonctionnement du bus SPI sur le S3C2440, pour faire court, disons qu’un transfert SPI va suivre les étapes suivantes :
1. configuration de la pin nSS : en mode maître, rien à faire, on laisse les valeurs par défaut.
2. configuration du module SPI : voir paragraphe suivant.
3. activation du composant SPI avec lequel on veut communiquer (= mise à 0 du signal CS).
4. attente que le module soit prêt avant d’émettre (=que le bit REDY vaille 1 avant d’écrire l’octet à transmettre dans le registre SPTDATx). La transmission commence dès l’écriture de l’octet en question.
5. attente de la fin de transmission (=que le bit 1 du registre SPCONx passe à 1)
6. récupération de l’octet reçu dans le registre SPRDATx.
7. désactivation du composant SPI avec lequel on veut communiquer (= mise à 1 du signal CS).
Quelques explications sur la configuration du module SPI.
SPIMOD = 0 ou 1 suivant qu’on veut, lors d’un transfert, attendre la fin du transfert via une interruption (bien) ou en lisant en boucle un bit dans un registre (pas bien)
ENSCK = 1 : on active l’horloge (je n’ai pas trop cherché à quoi peut bien servir la désactivation de celle-ci, j’imagine que c’est pour diminuer la consommation de la carte lorsqu’on n’utilise plus le bus SPI)
MSTR = 1 : je veux fonctionner en mode master
CPOL, CPHA et TAGD correspondent à différents modes de transmission (voir figure 22.2 du datasheet)
Un dernier point intéressant, la formule qui permet de calculer la valeur à mettre dans SPPREx.
Valeur = (PCLK / 2 / BaudRate) - 1 avec PCLK = 50Mhz sur la mini2440.
Exemple : baud rate voulu : 1Mhz, valeur = 24
Driver de type stream
Windows CE supporte plusieurs types de drivers. Le type « stream » est sans doute le plus simple à développer/utiliser dans des applications, c’est ce type que j’ai choisi.
Depuis une application, on accède à un driver de type « stream » avec les fonctions CreateFile, ReadFile, WriteFile, DeviceIoControl, CloseHandle. La fonction CreateFile ouvre le driver et retourne un handle qui permet ensuite d’accéder aux fonctions exportées par le driver. Dans mon cas, je ne vais pas supporter les fonctions ReadFile/WriteFile, mais passer par DeviceIoControl. En effet, un transfert SPI est en même temps une lecture et une écriture, et un transfert nécessite des paramètres supplémentaires (numéro de pin utilisés pour le signal CS par exemple).
Allez, il est temps de coder le driver.
Je commence par créer le répertoire C :\WINCE600\PLATFORM\Mini2440\SRC\DRIVERS\SPI qui va contenir les fichiers sources de mon driver. Pour que Platform Builder, lors de la génération de l’image, compile mon driver, je dois rajouter le nom de mon répertoire dans le fichier C :\WINCE600\PLATFORM\Mini2440\SRC\DRIVERS\dirs (ce fichier contient la liste des sous-répertoires explorés par le processus de génération).
J’ai choisi de coder mon driver dans 2 fichiers .cpp :
drv.cpp : contient les fonctions d’interface d’un driver « stream », ce sont ces fonctions qui seront directement appelée par le système lors de l’accès au driver par une application.
spi.cpp : le code proprement dit de mon driver.
Interface d’un driver « stream »
Un driver « stream » doit exporter les fonctions suivantes :
DllEntry : appelée lors du chargement et du déchargement de notre driver.
SPI_Init : appelée par le device manager pour initialiser notre driver.
SPI_Open : appelée lorsqu’une application effectue un CreateFile de notre driver.
SPI_Close : appelée lorsqu’une application effectue un CloseHandle de notre driver.
SPI_Write : appelée lorsqu’une application effectue un WriteFile de notre driver.
SPI_Read : appelée lorsqu’une application effectue un ReadFile de notre driver.
SPI_IOControl : appelée lorsqu’une application effectue un DeviceIoControl de notre driver.
Accéder aux registres du processeur
Il n’est pas possible d’accéder directement aux registres du processeur en déclarant un pointeur à l’adresse du registre voulu. Il est nécessaire de passer par la mémoire virtuelle. Grosso modo, on déclare une zone mémoire virtuelle, une zone mémoire physique (à l’adresse du bloc de registre auxquels on veut accéder), et on mappe la zone de mémoire virtuelle sur la zone de mémoire physique. Les accès à la mémoire virtuelles iront alors directement dans la mémoire physique.
C’est le rôle de la fonction InternalMapRegisters. J’utilise ici 3 zones de mémoire, correspondant chacune à un bloc de registres du processeur.
Le code du driver
Pour être honnête, plutôt que réinventer la roue, je suis parti du code du driver i2c que j’ai adapté au bus SPI.
Je ne vais pas détailler ici la totalité du code, j’ai essayé de le commenter le plus possible pour qu’il soit lisible tel quel.
J’ai choisi de stocker les paramètres de transmission dans la base de registres. C’est très bien dans mon cas, mais ce choix peut poser problème à ceux qui voudraient communiquer avec des composants SPI à des vitesses ou des formats de transmission différents. Dans ce cas, il n’est pas très difficile d’adapter le code en ajoutant une commande IOCTL permettant de régler ces paramètres.
Le driver peut à priori fonctionner sous interruption ou en polling. Je n’ai pas testé le mode polling qui ne présente à mon avis aucun intérêt.
Un mot quand-même sur l’interruption. Quand une interruption se déclenche, elle est récupérée par le système. Celui-ci regarde si une fonction a été enregistrée pour l’interruption en question et l’appelle si celle-ci existe. Pour déclarer une fonction d’interruption dans le système, il est nécessaire de connaître son numéro. On peut le trouver dans le fichier C :\WINCE600\PLATFORM\Mini2440\SRC\INC\s3c2440a_intr.h.
Enfin, le driver ne gère que l’interface SPI 0.
Intégration du driver dans notre image
Vous pouvez récupérer le code de mon driver ici.
Pour l’ajouter à votre image, il faut copier le répertoire spi dans C :\WINCE600\PLATFORM\Mini2440\SRC\DRIVERS\.
Il faut ensuite copier le fichier spi.h dans le répertoire C :\WINCE600\PLATFORM\Mini2440\SRC\INC.
Pour que le fichier binaire de notre driver (spi.dll) soit ajouté dans notre image, il faut rajouter la ligne suivante dans le fichier platform.bib dans la section MODULES (cette section commence à MODULES et se termine à FILES dans le fichier, entre les lignes 13 et 205) :
spi.dll $(_FLATRELEASEDIR)\spi.dll NK SHK
Enfin, il faut ajouter les clés suivantes dans la base de registre :
[HKEY_LOCAL_MACHINE\Drivers\BuiltIn\SPI]
"Prefix"="SPI"
"Dll"="SPI.DLL"
"Order"=dword:200
"Index"=dword:0
"Mode"=dword:1 ; 0 = POLLING, 1 = INTERRUPT
"ClockPolarity"=dword:1 ; 0 = ACTIVE_LOW, 1 = ACTIVE_HIGH
"ClockPhase"=dword:1 ; 0 = FORMAT_A, 1 = FORMAT_B
"TxAutoGarbage"=dword:1 ; 0 = NORMAL_MODE, 1 = TX_AUTO_GARBAGE
"PrescalerValue"=dword:255
"FriendlyName"="SPI Bus Driver"
C’est grâce à ces clés que le système, lorsqu’une application effectue un CreateFile("SPI0 :", etc...), sait qu’il faut appeler la fonction SPI_Open() du fichier spi.dll.
Après génération de l’image, on doit normalement avoir le fichier spi.dll présent dans l’image.
Utilisation du driver SPI
Pour valider mon driver SPI, j’ai connecté une sonde de température SPI tmp125. Pourquoi une sonde de température ? Parce que j’en ai trouvé une qui traînait dans un tiroir, mais surtout parce qu’avec un tel composant, on connaît la valeur qu’on attend en lecture (la température de la pièce) et il suffit de souffler dessus pour la faire varier.
Au niveau électronique, le schéma est ultra simple :
pin SI (2) du tmp125 sur la pin SPIMOSI du s3c2440 (broche 26, connecteur CON4)
pin SO (4) du tmp125 sur la pin SPIMISO du s3c2440 (broche 25, connecteur CON4)
pin SCK (6) du tmp125 sur la pin SPICLK du s3c2440 (broche 27, connecteur CON4)
pin /CS (5) du tmp125 sur la pin de son choix du s3c2440 (j’ai choisi la pin 1 du port F = broche 10, connecteur CO4)
pin V+ (3) et GND (1) du tmp125 sur broches VDD33V (2) et GND (3) du connecteur CON4.
Au niveau logiciel, ce n’est pas très compliqué. Vous pouvez télécharger les sources de mon application de test ici.
L’appel suivant permet de récupérer un handle sur notre driver :
hSPI = CreateFile(L"SPI0 :", GENERIC_READ|GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, 0) ;
Un transfert SPI s’effectue via l’appel suivant :
DeviceIoControl(hSPI, IOCTL_SPI_READ, &spiDesc, sizeof(SPI_IO_DESC), NULL, 0, &rCount, NULL) ;
Cet appel prend en paramètre une structure de type SPI_IO_DESC qui contient les paramètres suivants :
portNumber : numéro du port de la pin /CS du composant (PORT_F dans mon cas)
pinNumber : numéro de la pin /CS du composant (1 dans mon cas)
Data : pointeur sur un buffer contenant les données à émettre/recevoir
Count : taille du buffer en octets.
Pour les fans du compact framework, voici la même application en VB.NET.
Voilà , c’est la fin de cet article, j’espère ne pas avoir été trop brouillon, je n’ai pas voulu trop rentrer dans les détails (cet article n’est pas un tutoriel exhaustif sur la création d’un driver) en donnant quand même suffisamment d’infos à ceux qui voudraient comprendre comment le driver fonctionne.