Forums d'entraide informatique - Astuces - Conseils

Des experts à votre écoute pour tous vos dysfonctionnements

Vous n'êtes pas identifié.


#1 23-10-2008 19:37:10

Admin
Administrateur
Date d'inscription: 30-07-2008
Messages: 683

Stockage de masse non volatile : un block device pour multimedia card

STOCKAGE DE MASSE NON r
VOLATILE: UN BLOCK DEVICE
POUR MULTIMEDIACARD

De nombreux systèmes embarqués peuvent profiter d'un mode de stockage de masse non volatile fournissant une grande quantité de mémoire (plusieurs MB), sans partie mobile et de consommation réduite. Nous proposons ici d'utiliser à ces fins une MultiMediaCard (MMC et MMC+) telle que celles disponibles dans le commerce grand public pour des prix dérisoires compte tenu de leur capacité et répondant à nos contraintes.
1.INTRODUCTION
Nous proposons ici de nous familiariser dans un premier temps avec les connexions électriques des MMC et MMC+ et les modes de communication entre un processeur et ses périphériques. Ayant été capable de communiquer avec les mémoires, nous aborderons le vif du sujet, à savoir implémenter un block device qui nous permette d'accéder aux données stockées par des commandes Unix classiques, et ce, grâce à un formatage de la carte. Ainsi, nous dépassons le stade du simple enregistreur de données se limitant à stocker des octets successifs, pour réellement proposer un support de stockage souple d'utilisation.
Nous avons initialement développé ce driver pour le processeur Coldfire 5282 équipant la carte SSV DNP/5280 fonctionnant sous uClinux (noyau 2.4.x). Ce circuit fournit en effet une plate-forme réellement embarquée, de consommation réduite si on considère le nombre de périphériques disponibles. Ce processeur fournit notamment une implémentation matérielle du bus SPI utilisé pour la communication avec les MMC et MMC+.
Cependant, afin de faire profiter à un public aussi vaste que possible de nos développements, nous avons implémenté de façon logicielle le protocole SPI via le port parallèle d'ordinateurs personnels compatibles IBM fonctionnant sous GNU/Linux (noyau 2.4.x).
Bien que cette solution soit peu performante en termes de bande passante du fait de la lenteur du port, elle ouvre de nombreuses perspectives, telles qu'équiper des ordinateurs trop anciens pour posséder un port USB avec des espaces de stockage de données de plusieurs centaines de MB, ou fournir les bases d'un portage de ce driver à tout matériel offrant 4 broches d'entrée-sortie généralistes (General Purpose I/O, GP10) qui peuvent être utilisées pour une émulation logicielle du bus SPI (fig. 3).
2.ASPECTS MATÉRIELS
La carte MMC se présente comme un support de mémoire de petites dimensions (32x24x1,4 mm3 [i]) équipé de 7 broches. La MMC+ présente des dimensions identiques, mais une rangée de connecteurs supplémentaires dont nous ne ferons pas usage ici. Notons que les développements qui vont suivre ne sont pas applicables aux cartes SD : bien que de dimensions identiques, ces cartes (équipées de 8 contacts) nécessitent un mode d'initialisation différent que nous n'avons pas encore appréhendé. L'espacement entre les broches est de 2,54 mm, pas standard dans de nombreuses cartes enfichables. De plus, son épaisseur à peine inférieure aux 1,6 mm standards de circuit imprimé nous incite à rechercher dans les supports de cartes d'extensions informatiques un support peu coûteux et plus facilement accessible que les connecteurs spécifiquement dédiés aux MMC que nous n'avons pas été capables de nous procurer en petites quantités.
3.LE BUS SPI
Le bus SPI est un bus synchrone (le maître fournit un signal d'horloge aux esclaves), équipé de deux voies unidirectionnelles (MOSI — Master Out Slave In, et MISO — Master In Slave Out) et d'un signal d'activation de chaque périphérique que nous nommerons CS# (Chip Select, actif au niveau bas) bien que de nombreuses datasheets de MMC nomment cette broche SS. Cette dernière broche est toujours celle la plus proche du coin biseauté de la carte qui sert de détrompeur dans les montages commerciaux.
Il faut de plus fournir une alimentation stable de 3,3V à la MMC par un régulateur de tension connecté à un générateur de tension externe (le port parallèle est incapable de fournir le courant nécessaire au bon fonctionnement d'une MMC) : suivant l'idée proposée il y a longtemps (1994 [2]) par la webcam Connectix Quickcam N&B, nous prendrons
0,80 euro/pièce TTC chez Lextronic ou chez Farnell sous la référence 9755349_

'alimentation sur le port clavier afin de s'affranchir d'une batterie additionnelle [3]. Nous avons pour ceci utilisé un convertisseur LE33CZ
Dans l'ordre, depuis le coin biseauté de la MMC (Fig. I), les signaux sont CS#, la communication du processeur vers la carte Data In (Di), la masse, l'alimentation à 3,3 V, horloge CK du protocole synchrone de communication, à nouveau la masse, et finalement la communication de la carte vers le processeur Do.
3.1 LA OSPI DU COLDFIRE 5282
Le bus SPI et le protocole synchrone associé sont implémentés dans de nombreux microcontrôleurs. C'est notamment le cas dans le Coldfire 5282, dans une version évoluée nommée QSPI qui fournit au niveau matériel une mémoire tampon de 16 éléments définissant chacun la configuration du bus (délai entre les fronts, activation des divers signaux CS# présents) et la donnée associée à transmettre.
Notez que le bus du circuit DNP5280 fourni par SSV ne donne accès qu'à un signal d'activation CS#, à savoir CSO# dans la nomenclature Coldfire (Fig. 2).
Il nous faut dans un premier temps initialiser le port QSPI du Coldfire, en définir la configuration avant de pouvoir placer des données à transmettre dans les mémoires tampon. L'ensemble de ces opérations sont décrites en détail dans [4, chap.22], implémentées sous uClinux selon le code présenté dans la section 5.1.2, dont nous résumons ici les étapes qui ont été nécessaires
Définir le nombre de bits/transfert (8 bits), la vitesse de transfert et la polarité, indiquer que le Coldfire est maître
des transactions avec la MMC. A priori la vitesse peut être aussi élevée que possible, mais il faut prendre soin de définir le transfert de données sur le front montant de l'horloge (bit CPHA de QMR à 0) tel que décrit dans [4, Fig.22-4].Toutes ces opérations se définissent dans le registre QMR du Coldfire à l'adresse MC F_I PSBAR+0x340 (où MC F_ I PSBA R=0x40000000). Les délais tels que définis dans QDLYR sont mis à leur valeur minimale de I.
•    Une interruption est associée aux transactions, sur le bus SPI afin de limiter les attentes vides et ainsL réveiller notre module lorsque l'opération requise est achevée : nous plaçons QIR à 0x1310D afin de générer une interruption en cas de collision et en fin de transaction (QI R est localisé en MC F_ I PSBA R+0x34C)
•    Pour chaque nouvelle transaction, nous disposons de deux
files de 16 mots chacune, l'une contenant les commandes associées à chaque transaction et la seconde aux données à transférer. Le fait de définir le mot à transférer ou la commande associée est sélectionné par la valeur du registre QAR qui indexe le registre QDR contenant les commandes/données
en deçà de 0x20 QDR contient les données, au-delà il s'agit
de commandes. Le nombre de transactions actives est défini dans QWR (en MC F I PSBA R+0 x348), qui sera en pratique égal à 16 afin d'optimiser le nombre de transactions par appel au module de gestion de la QSP1, mais dont la valeur peut varier en fonction de la quantité de données à transférer. La gestion des commandes associées à chaque transfert est très souple puisque nous nous contentons de placer, pour transférer un mot placé dans le ième emplacement de QDR, de définir dans le mot situé au i+32ème octet de QDR une série de bits correspondant à l'état de chacune des lignes de Chip Select (dans notre cas la MMC requiert presque toujours — à l'exception de la phase d'initialisation — un Chip Select actif au niveau bas donc un bit correspondant à O): nous prenons CONT=B I TS E=0 pour indiquer que le transfert est sur 8 bits, DT=DSK=1 et finalement, puisque seul le Chip Select 0 est accessible sur la carte SSV DNP5280,1e niveau du bit de poids le plus faible de QSPI CS pour définir l'état des broches d'activation des périphériques
é    Une fois les registres sous ODR convenablement remplis, la transaction est activée par la mise à I du bit de poids le plus fort de QDLYR (à l'adresse MC F_I PSBAR+0x344). Cette transaction prendra fin avec la génération d'une interruption qui permet au processeur de prendre soin d'autres tâches pendant le transfert.
3.2 EMULATION LOGICIELLE DU SPI SUR PORT PARALLÈLE DE PC
Le processeur Coldfire n'étant pas suffisamment répandu auprès des développeurs de circuits embarqués, et afin de nous familiariser avec les MMC sur un environnement de développement plus facile d'accès et plus répandu que ce microcontrôleur, nous avons développé une implémentation matérielle et logicielle du protocole SPI sur port parallèle de PC compatible IBM [6]. La seule subtilité ici consiste à convertir les tensions issues du port parallèle entre 0 et 5V (électronique compatible TTL) en tensions entre 0 et 3,3 V telles que requises par la MMC. Bien que des composants dédiés à cette tâche existent, nous avons simplifié au maximum
Fig. 1: Connecteur MMC+ et brochage au port parallele avec
conversion des signaux de TTL (5 V) à 3,3 V par des composants
passifs uniquement. Le brochage avec l'uClinux se fait directement
broche à broche (Di à MOSI, Do à MISO, horloge et activation
du composant). Dans le cas d'une MMC, seule la partie utile du
connecteur de MMC+ est accessible.

Fig. 2 : Haut : montage d'une MMC sur la carte de développement
associée au DNP5280 de SSV. Notez l'utilisation d'un bout de
connecteur ISA (au pas de 2,54 mm) pour assurer la connexion
électrique avec la MMC. Les Coldfire ainsi que la MMC fonctionnent
tous deux sous une tension de 3,3 V
Bas : adaptation d'une MMC+
au port parallèle d'un Libretto 100CT. Notez ici l'utilisation de
la sortie 5 V du connecteur PS2 pour alimenter la carte,
évitant ainsi l'ajout d'une batterie externe.
le montage en n'utilisant que des résistances de limitation de courant.
L'émulation logicielle du protocole SPI par le port parallèle du PC est simplifiée d'un point de vue matériel du fait que ce bus inclut deux lignes distinctes de transmission du maître vers l'esclave (Master Out Slave IN : MOSI) et de l'esclave vers le maître (MISO). Du point de vue électronique, nous aurons donc une broche en sortie depuis le PC (la broche 3 du port parallèle, signal Datai) et une broche en entrée (broche I I , signal Status7). Le signal d'horloge est imposé par le maître (le PC) sur une broche de sortie (broche 4, signal Data2). Finalement le signal de sélection de la carte CS# est connecté à la broche 5 — Data3 — du port parallèle. Le seul point important dans l'implémentation du procotole synchrone est que l'échantillonnage de la ligne de données se fait sur le front montant de la ligne d'horloge. Il faut donc placer l'horloge au niveau bas, définir le bit de données, puis faire monter le signal d'horloge. L'autre point important est qu'une lecture et une écriture sont identiques du point de vue SPI : toute transition montante du signal d'horloge s'accompagne soit d'une lecture du signal MOSI par l'esclave (signal imposé par
le maître), soit d'une lecture du signal MISO par le maître. Ainsi, en termes d'implémentation logicielle, chaque transition de l'horloge s'accompagne d'une transmission sur MOSI et d'une lecture sur MISO, avec éventuellement un octet aléatoire transmis sur MOSI lors d'une lecture par le maître. Toutes les transmissions sur le bus SPI se font par octet. À ce niveau, nous ne nous préoccupons donc pas de l'endianness du processeur : ce problème réapparaîtra plus tard dans le cas particulier de la transmission d'adresses sur 32 bits.
4.COMMUNICATION AVEC LES MMC ET MMC+
Ayant maîtrisé le bus de communication SPI, il nous faut maintenant y implémenter le protocole d'initialisation et de communication avec la MMC. Nous rappelons ici que ce protocole n'est pas compatible avec les cartes de type SD, bien que les différences semblent d'après les documentations relativement mineures.
4.1 INITIALISATION DE LA MMC
Les diverses étapes de l'initialisation et de la communication avec une MMC ont déjà été décrites par ailleurs [7] et nous n'en reprenons ici que les grandes lignes. La seule subtilité est de tenir compte de l'endianness du processeur sur lequel s'exécute le code lors de la conversion d'une adresse sur la carte sur un entier (4 octets) qui doit être transmis octet de poids fort (MSB) en premier. Dans un premier temps, nous informons la MMC que nous communiquerons avec elle selon un protocole SPI en envoyant 80 transitions sur le fil d'horloge CK tout en maintenant CS# au niveau haut.
Suit la première commande de réinitialisation de la carte, CMDO. Nous développons ici la nomenclature quelque peu originale des commandes telles que décrites dans les manuels de MMC.Toute commande nommée CMDi se compose de 6 octets : le numéro de la commande i, où i est exprimé en décimal. Ce numéro de commande doit être masqué via un i logique avec 0)(40 avant d'être transmis à la MMC. Suivent 4 octets qui sont les arguments de la commande, et finalement un code redondant cyclique (CRC) qui dans le cas de la communication SPI est ignoré et que nous fixerons donc arbitrairement à OxFF.
Une exception est la première commande de réinitialisation de la carte lors du passage en mode SPI — CMD0— pour laquelle le CRC est pris en compte mais est précalculé : il vaut dans ce cas 0x95 si tous les arguments sont définis à 0x00.Ainsi l'initialisation de la carte s'obtient par la transmission de la séquence de 6 octets
0x40 0x00 0x00 0x00 0x00 0x95
La MMC nous répond 0x01 pour acquitter la CMDO (GO_ I D L E_STAT E qui correspond à une réinitialisation logicielle). Suit la commande CMDI à laquelle la MMC doit répondre par 0x00. Ayant transmis les commandes CMUO et CMD1, nous avons un système à notre écoute et fonctionnel. En prévision de l'application à l'écriture d'un block device capable de supporter n'importe quelle carte MMC ou MMC+, nous allons implémenter une commande supplémentaire qui nous informe sur la géométrie de la carte et notamment sur sa capacité mémoire.
4.2 LE REGISTRE CSD DE LA MMC
Un registre interne à la carte est le CSD  (Card Specific Data register) qui contient sur 128 bits un certain nombre d'informations sur les tension et courant nécessaires au bon fonctionnement de la carte, les temps d'accès mais surtout la géométrie et notamment le nombre de blocs accessibles. Les éléments qui vont nous intéresser pour renseigner les structures de données internes à un block device sont
•    La taille de chaque bloc physique dans la MMC, a priori
toujours égale à 512 octets, nommé READ_BL_LEN (bits 83-80 du CSD);
é    Le nombre de blocs disponibles, qui nous renseigne donc sur la taille totale de la MMC, nommé C_S I ZE (bits 73-62 du CSD)
é    Un facteur de pondération nommé C_SIZE_MULT (bits 49-47 du CSD).
Le CSD est obtenu par la commande CMD9 (SEND_CSD), qui nous renvoie en réponse l'octet Ox FE de début de transmission de la réponse, suivi de 16 octets (128 bits) du CSD lui-même (octet le plus significatif en premier), puis deux octets de checksum que nous ignorons pour le moment. Le calcul de la capacité de la carte en fonction des valeurs lues dans le CSD est décrit dans [ 1 , p.36]
taille=(C_SIZE+ I )x 2C_SIZE_MULT.2,< 2 READ _BL_LEN
À titre d'application numérique, pour une carte de 64 MB nous lisons un CSD de 48 Oe 01 2e Of f9 81 e9 f6 da 01 el 8e 40 0 cl dont nous déduisons que READ_BL_LEN=9 soit des blocs de 512 octets,C_SIZE=E7A7=1959 et C_SIZE_MULT=4 soit au total (1959+1) x 512 x 24+2=64225280 octets.
Exactement selon le même protocole, nous pouvons lire le
CID (Card IDentification register) de la MMC au moyen de-Cf1D10.
Ce registre nous informe sur le fabricant de la carte, sa date de fabrication et fournit un identifiant qui nous permettra éventuellement de détecter un changement de carte.
4.3 ÉCRITURE ET LECTURE DANS LA MMC
La MMC s'attend à recevoir toutes les adresses de blocs octet de poids le plus fort (MSB) en premier : il faut donc penser à convertir une requête exprimée en long (telle qu'imposée par un block device) via la fonction ht on 0 ou, dans sa version noyau, cpu_to_be32( ). La commande de lecture est CMD17 (READ_SINGLE_BLOCK) et la commande d'écriture est CMO24 (WRITE_BLOCK). Ces commandes prennent en argument 4 octets qui représentent l'adresse de débutdu bloc (nécessairement multiple de 512 si READ_BL_LEN=9),puii un dernier octet de validation (CRC) si cette fonction est activée :nous l'ignorons pour le moment. Le protocole diffère ensuite selon que l'on est en écriture ou en lecture
é    Dans le cas d'une lecture, la MMC nous répond si elle accepte notre requête en répondant Ox FE. Toute autre réponse indique le refus de la requête (notamment une réponse de type 0x60 indique que l'on fournit une adresse invalide — soit au-delà de la capacité de la carte, soit qui n'est pas multiple de 512 — erreur possible si l'on a inversé l'ordre des octets représentant l'adresse). Suivent la réception des 512 octets de données, puis 2 octets de validation (CRC) que nous ignorons pour le moment
é    Dans le cas d'une écriture, nous recevons 0x00 comme acquittement de la requête, auquel nous répondons en envoyant OxFE suivi des 512 octets à stocker, et finalement les deux octets de CRC si cette option est activée — nous les ignorons encore pour le moment (envoi d'une valeur quelconque). La transaction s'achève par la lecture du statut de la carte : une réponse finissant par 5 indique que le bloc est en cours d'écriture, opération qui sera complétée lorsque le bit de données du bus SPI passe de l'état bas (0x00) à haut (0x F F).
L'ensemble du protocole implémenté jusqu'ici nous fournit les éléments de base d'un character device, à savoir lire et écrire une donnée à une adresse donnée. Nous désirons maintenant dépasser ce stade et implémenter un block device facilitant l'accès au périphérique de stockage de masse.
5.IMPLÉMENTATION D'UN BLOCK DE VICE
Nous sommes désormais capables de lire et d'écrire des blocs de 512 octets en n'importe quel emplacement de la MMC. L'utilisation d'une énorme quantité de mémoire de cette façon est cependant fastidieuse, et nous allons désormais

proposer le développement d'un block device,mmc_qspi _mod permettant de formater la carte avec un système de fichier et d'y accéder par les commandes Unix standards. Le code source de ce pilote est disponible sur http://www.sequanux. org et http://jmfriedt.free.fr.
Dans toute la présentation qui va suivre — qu'il s'agisse de l'implémentation du driver sous GNU/Linux ou uClinux — la communication entre l'espace utilisateur et le noyau se fera par le block device /dev /mmca créé au moyen de mknod /dev/mmca b 254 0 tandis que l'accès aux partitions de ce périphérique se font par les devices mmcai obtenus par mknod /dev/mmcai b 254 i avec le[1:4].
Fig. 3 : Architecture générale des éléments que nous nous proposons
de décrire dans ce document : une couche de communication
fortement dépendante du matériel. Elle fournit ses méthodes à une
couche de plus haut niveau indépendante de l'architecture.
Cette dernière gère l'accès aux périphériques et l'interface avec
le Virtual File System (VFS) de GNU/Linux.
5.1 ARCHITECTURE DU PILOTE
Le pilote de la MMC est composé de deux modules : mmc_ qspi mod et qspi _mod. Les intérêts de cette modularisation sont multiples. Bien séparer les fonctionnalités du pilote en plusieurs modules permet de clarifier le développement et de simplifier le débogage. Chaque module apparaît comme une « boîte noire » et peut être testé de manière indépendante. Un autre intérêt à cette modularisation est de rendre le pilote portable à moindre coût. L'adapter à un nouveau support matériel ne nécessite que la réécriture du module bas niveau. Lors de l'émulation SPI du port parallèle, nous avons pu expérimenter cette flexibilité. Le port de qspi _mod en ppo r t_q sp __mod ne nous a pris qu'une petite heure !
5.1.1 COUCHE HAUT NIVEAU
Le module mmc_q spi _mod constitue la partie haute du pilote. Il doit être capable de remplir deux fonctions :
♦    I . La première est de s'interfacer avec le système de fichier virtuel (Virtual File System —VFS) et de fournir au noyau les méthodes pour écrire réellement sur la MMC ;
♦    2. La seconde est d'implémenter tout le protocole nécessaire à la communication avec la MMC.
Au vu de ces fonctions et aussi pour simplifier le développement et les évolutions futures, nous avons décidé de séparer mmc_q s p _mo d en deux éléments distincts.
Le premier, mmc_co re est très proche d'un pilote bloc classique et son travail est de réaliser l'interfaçage avec le noyau et IeVFS. Le second, mmc qsp i implémente les différentes commandes utiles pour dialoguer avec la MMC en mode SPI.
La communication entre ces deux éléments est à sens unique. mmc_co re sollicite les fonctions de mmc_q sp i pour lire ou écrire des données sur la MMC. Voici le fichier d'en-tête mmc_q s p . h déclarant les fonctions exportées par mmc_q s p :
#define CSD SITE 16
/* initializate the MMC */
int open_mmc (void);
/* return the CSD register *1
char *get_csd (void);
/* free the MMC */
int release_mmc (void);
/* method to read a data block on the MMC */ int read_block (unsigned char, unsigned char, unsigned char, unsigned char, char *);
/* method ta write a data block on the MMC */ int write_block (unsigned char, unsigned char, unsigned char, unsigned char, char *);
Nous verrons un peu plus tard l'implémentation de quelques- unes de ces fonctions.
5.1.2 COUCHE BAS NIVEAU
Le module Qspi mod constitue la partie basse du pilote. Son travail est d'échanger des octets avec les périphériques connectés au bus SPI.À la différence d'un char device classique, qspi mod n'exporte pas de méthodes à destination des processus en espace utilisateur. Les méthodes qu'il définit seront utilisées par d'autres pilotes du noyau souhaitant communiquer via le bus SPI.
Cela dit, nous avons également développé une version « caractère » de qspi_mod.Ce module s'appelle q spi _cha r_mod et est fourni avec les sources du pilote.
Il nous a d'ailleurs beaucoup servi lors des premiers contacts avec la MMC. Grâce à lui nous avons pu commencer par implémenter et déboguer les commandes MMC depuis des programmes utilisateurs...
Dans le cas du port parallèle, le module pport spi _mod fournira cette interface bas niveau. Bien que les communications sur le bus SPI et sur le port parallèle d'un PC soient très différentes, rien ne change du point de vue du module mmc_q s p _mo d. Les modules « bas niveau » masquent ces différences en lui offrant une API unique.
Voici le fichier d'en-tête q spi . h déclarant les méthodes fournies par les modules q spi_mod et pport_spi_mod :

Bien que le hardware, les performances et l'implémentation soient très différentes, les modules bas niveau q spi _mod et pport_spi _mod présentent une même API, compatible avec mmc_q spi _mod.Que ce soit pour le module ppo rt_spi _mod ou le module qspi _mod, les fonctions qu'ils exportent ont un comportement identique... du moins en apparence...
Par exemple, une différence de taille est le mode utilisé pour transmettre physiquement des données à la MMC. Pour le bus SPI, q s pi _mod utilise une interruption matérielle pour valider les transferts alors que pport_spi_mod utilise le mode polling, l'interruption du port parallèle n'étant pas exploitable pour cette application.
La qualité du service offert par ces deux modules est donc très différente. Dans le cas de pport_spi _mod, le mode polling va bloquer l'ensemble du système pendant les opérations de lecture et d'écriture. Ce genre d'implémentation dégrade considérablement les performances du système tout entier. Il en résultera un manque d'interactivité de l'ensemble des processus.
L'utilisation d'un noyau préemptible pourrait en partie compenser cette implémentation. Cependant un pilote se voulant un minimum portable ne peut décemment pas se reposer sur l'hypothèse de préemptibilité du système qui l'héberge. La seule alternative est donc de procéder à des appels à la fonction s c h ed ul e ( ) pour casser certaines boucles de polling un peu longues.
La commande sc h ed u 1 e( ) appelle l'ordonnanceur et donne une chance à un autre processus de s'exécuter. Le module qspi _mo d n'est lui pas sujet à ces limitations. Associer une interruption à l'achèvement d'un transfert lui permet de s'endormir et de relâcher le processeur à chaque transaction. Lorsque le transfert est complet, l'interruption est prise en charge par un gestionnaire dont le rôle principal est de réveiller le processus endormi...
L'interaction entre le gestionnaire d'interruption q spi i rq ha ndl er ( ) et la méthode q spi _wr i te( ) donne un bon aperçu de l'implémentation de qspi _mod. Le lecteur curieux de détails supplémentaires est bien sûr invité à consulter les sources du pilote.

5.2.1 INTRODUCTION
L'objectif de cette partie est de présenter l'implémentation du module bloc mmc_q s p i _mo d. Mais auparavant, nous allons introduire le fonctionnement d'un block device ainsi que son implémentation pour un noyau 2.4.
Il ne s'agit pas d'entrer dans les détails mais plutôt de fournir des recettes utiles à l'écriture d'un tel pilote. Le lecteur intéressé par la théorie sur la gestion des périphériques blocs par un noyau pourra se référer à l'excellent article de la série Conception d'OS, paru dans Linux Magazine numéro 80 [9]. D'un point de vue plus pratique, le livre Linux Device Drivers [8] est sans conteste une bible pour toute personne souhaitant se lancer dans le développement de pilotes blocs.
5.2.2 GÉNÉRALITÉS SUR LES BLOCK DEVICES
Les périphériques blocs possèdent une structure très proche de celle des pilotes caractères. Ils exportent des méthodes à destination des processus utilisateurs. Ces méthodes peuvent être appelées via des fichiers spéciaux type bloc. Ces fichiers sont généralement contenus dans le répertoire 'dev.
Le nombre majeur (ici 254) sert a désigner le pilote et le nombre mineur (ici de 0 à 3) est utilisé par le pilote en interne. Dans le cas des périphériques blocs, le mineur servira notamment à différencier les partitions. Les méthodes du pilote sont contenues dans une structure struct block_
devi ce_operati ons.

À quelques exceptions près, cette dernière est très semblable à la structure st ruct f 1 e_oper a t on s utilisée par les pilotes caractères.
static struct block_device_operations mmc_blk_ops
open:    mmc_blk_open,
ioctl:    mmc_blk_ioctl,
release:    mmc_blk_release,
check_media_change: mmc_blk_change, revalidate:    mmc_blk_revalidate
Cette structure est enregistrée auprès duVFS via la fonction regi ster_blkdev( ).Encore une fois,cette fonction remplit le même rôle que reg i st er_c h rdev ( ) pour les pilotes caractères. Pour les méthodes open, release  et i octl,rien de nouveau, elles permettent respectivement d'ouvrinfermer et configurer le périphérique. Les méthodes c h ec k_med a_c h a nge( ) et revalidate( ) sont, elles, spécifiques au pilote bloc.
Le rôle de check_media_change( ) est de contrôler si le périphérique a été changé. Si tel est le cas cette fonction doit retourner I, et 0 sinon. À noter que suivant le type de périphérique, ce genre de contrôle n'est pas toujours aisé à implémenter. S'il n'y a aucun moyen d'être sûr de la validité du périphérique, une politique par défaut correcte est de retourner systématiquement I.
static int mmc_blk_change (kdev_t i_rdev)
int minor r MINOR(i_rdev);
int device_num    minor » DEVICE_SHIFT;
struct mmc_blk *dev;
info('%s-minor:%d",__FUNCTION ,minor);
if ((device_num <0) Il (device_num >r nb_dev))
err("%s:DEV for minor %d dont exits",
FUNCTION, device_num);
return (1);
dey = (struct mmc_blk *) (mmc_blk_dev + device_num);
/* as we can't test if the MMC device
has changed or flot by default, we
mark the device as expired */ return (1);
La fonction r evd I nictuç est appelée à chaque fois qu'un changement de périphérique est signalé. Par voie de conséquence, un retour de check_medi a change( ) positif entraînera toujours un appel à la méthode reval i date ( ) du pilote. Le travail de cette fonction est de réinitialiser l'ensemble des structures de données dépendantes du périphérique. Le plus souvent, cela se traduira uniquement par une relecture de la table des partitions.
static int mmc_blk_revalidate (kdev_t i_rdev) int minor = MINOR(i_rdev);
int device_num    minor » DEVICE_SHIFT;
int parti r (device_num    DEVICE_SHIFT) + I;
int npart    (I « DEVICE_SHIFT) - I;
struct mmc_blk *dev;
if ((device_num <0) Il (device_num >= nb_dev))
err("%s : device %d dont exits", FUNCTION__, device_num); return (-ENODEV);
dey = (struct mmc_blk *) (mmc_blk_dev + device_num);
/* first clear old partition information */
memset (mmc_blk_gendisk.sizes + parti,
0, npart * sizeof(int));
memset (mmc_blk_gendisk.part + parti,
0, npart * sizeof (struct hd_struct)); mmc_blk_gendisk.part
[device_num « DEVICE_SHIFT].nr_sects =
mmc_blk_size[device_num « DEVICE_SHIFT] * mmc_blk_blocksizes[device_num « DEVICE_SHIFT] / mmc_blk_sectorsizes[device_num « DEVICE_SHIFT];
/* then fill new info */
info("(device %d) check partition", device_num);
register_disk (bmc_blk_gendisk,
MKDEV(mmc_blk_major,
(device_num « DEVICE_SHIFT)),
npart, himc_blk_ops,
mmc_blk_partitions[device_num    DEVICE_SHIFT].nr_sects);
return (0); /* still valid */
5.2.3 QUELQUES INITIALISATIONS
Afin d'être utilisable, un pilote bloc doit également renseigner un certain nombre de variables globales. Le noyau les utilise pour s'informer sur les caractéristiques du périphérique (taille, etc.). Ces variables sont en fait des tableaux indexés par les nombres majeurs et mineurs des pilotes. Chaque pilote doit donc renseigner ces tableaux pour l'indice correspondant à son majeur. Les nombres mineurs permettront de désigner les différentes partitions.
•    i nt bl k_si ze[ ] El : taille du périphérique en nombre de blocs
•    i nt bl kbl k_si zen El :taille d'un bloc en octets
•    i nt hardsect si ze[]C] : taille d'un secteur en octets.
Dans le cas d'une MMC, un secteur vaudra 512  octets
•    read_ahead[] : nombre de secteurs pouvant être lus
par anticipation. Un read_a beau important permet d'améliorer les performances sur les périphériques aux temps d'accès assez longs. Ce tableau dépend uniquement du major du périphérique. Il est évident que l'anticipation est liée à des contraintes matérielles et ne dépend pas du partitionnement.
La distinction entre bloc et secteur n'est pas toujours évidente. Du point de vue du VFS on parlera de bloc, alors

que du point de vue du matériel, on parlera de secteur. Il est fortement conseillé d'initialiser bl kbl k_sizeH H à 1024 octets. Une valeur différente gênera le noyau lors du calcul de la taille du périphérique. Par exemple, pour une taille de bloc de 5 12 octets, le noyau divisera régulièrement la valeur contenue dans b 1 k_si ze [] par 2.
5.2.4 LA MÉTHODE IOCTLO
La méthode i oct 1 ( ) d'un pilote bloc mérite également qu'on si attarde quelques instants. Le noyau et certains programmes utilisateurs, comme les outils de partitionnement, peuvent utiliser cette méthode. Elle doit être capable de fournir une réponse à quelques commandes :
•    BLKGETS I ZE :demande le renvoi de la taille du périphérique en octets ;
•    BLKRRPART :demande la relecture de la table des partitions du périphérique ;
•    HOW GETGEO : demande le renvoi de la géométrie du périphérique. Cette commande n'a de sens que pour les périphériques de type disque dur. En effet, une MMC ne dispose ni de plateaux, ni de têtes. On se cantonnera donc à retourner une valeur plausible...
Pour toutes les autres commandes, le noyau met à disposition une méthode bl k_i octl. Elle retournera des valeurs par défaut appropriées.
Voici l'implémentation de la méthode i oct 1 ( ) telle que proposée par le pilote mmc_q s p _mod :
static int mmc_blk_ioctl
(struct inode *inode, struct file *file,
unsigned int cmd, unsigned long arg)
int minor = MINOR(inode->i_rdev);
int device_num    minor » PAGE_SHIFT;
long size;
struct hd_geometry geo;
if ((device_num < 0) II (device_num    nb_dev))
{
err("%s:DEV for minor %d don't exits", _FUNCTION_, device_num);
return -ENODEV;
switch (cmd)
/* return device size in sector case BLKGETSIZE :
if (!access_ok (VERIFY_WRITE,
(void *) arg, sizeof (unsigned long))) return -EFAULT;
size    mmc_blk_size[minor] *
mœc_bl k_bl ocksi zes [mi nor] / mmc_bl k_sector si zes [mi nor];
if (copy_to_user ((unsigned long *) arg, &size, sizeof (unsigned long))) return (-EFAULT);
return (0);
/* read again partion table */ case BLKRRPART :
return (mmc_blk_revalidate (inode->i_rdev));
/* return the device geometry */ case HDIO_GETGEO :
if (!access_ok (VERIFY_WRITE, (void *) arg, sizeof (unsigned long))) return -EFAULT;
size    mmc_blk_size[device_num] *
mmc_blk_blocksizes[device_num] / mmc_blk_sectorsizes[device_num];
geo.cylinders = (size & —0x3f) » 6;
geo.heads    4;
geo.sectors    16;
geo.start    4;
if (copy_to_user ((struct hd_geometry *) arg, &geo, sizeof (struct hd_geometry)))
return (-EFAULT);    •
return (0);
/* let the kernel handle for us the unkown command */ default :
return (blk_ioctl (inode->i_rdev, cmd, arg));
return (-ENOTTY);
5.2.5 FILE D'ATTENTE DES REQUETES
La file d'attente des requêtes est un composant fondamental de tout pilote bloc. Les opérations de lectures ou d'écritures à destination du périphérique vont être présentées au driver sous la forme de requêtes (struct r eq ues t). Ces requêtes sont elles-mêmes placées dans la file d'attente des requêtes du pilote chargé de les traiter. Cette file se présente sous la forme d'une structure struct request_queue.
#i ncl ude <1 i nux/bl kdev . h>
typedef struct request_queue request_queue_t; struct request_queue
[...]
struct list_head queue_head;
request_fn_proc * request_fn;
make_request_fn * make_request_fn;
[...]
};
Le pointeur queue_head désigne la tête de la liste chaînée des requêtes. La structure struct request_queue contient également de nombreux pointeurs de fonctions. Nous nous intéresserons tout particulièrement aux fonctions request( ) et ma ke_req uest ( ) désignées respectivement par les pointeurs request_fn et make_request_r nie pilote initialise et libère une file de requêtes en utilisant les fonctions suivantes :

#include <linux/blkdev.h>
extern void blk_init_queue
(request_queue_t *, request_fn_proc *); extern void blk_cleanup_queue
(request_queue_t *);
Exemple d'initialisation par un pilote de sa file par défaut
blk_init_queue(BLK_DEFAULT_QUEUE(majeur_du_pilote),
fonction_request_du_pilote);
Une remarque importante peut être faite au sujet de la concurrence à laquelle la file d'attente des requêtes est sujette.Toute manipulation sur cette file devra être réalisée après acquisition du verrou global io_request_lock.À noter que lorsque la méthode recuest( ) du pilote est appelée, le noyau s'est déjà chargé de l'obtention du verrou...
5.2.6 LA MÉTHODE REQUEST()
Le lecteur attentif aura probablement remarqué la principale originalité de la structure st ruct bl ock_devi ce_operat ons. Il s'agit de l'absence de méthodes write() ou read( ). En effet les entrées-sorties sur les périphériques blocs ne sont pas directes et sont optimisées par l'utilisation des tampons et de pages du noyau [9].
Les processus ne peuvent donc pas accéder aux fonctions d'entrées-sorties du pilote directement. Pour les accès au périphérique, le pilote met à disposition du noyau une unique méthode appelée request ( ). Lorsque le noyau souhaite échanger des données avec le matériel, c'est cette fonction qu'il sollicite.
void (request_fn_proc) (request_queue_t *(1);
Elle reçoit en paramètre d'entrée un pointeur sur la file d'attente des requêtes du pilote.
Le rôle de la méthode request ( ) est de traiter les demandes du noyau pour lire ou écrire des blocs de données sur le périphérique. Une requête se présente sous la forme d'une structure st ruct request.
#include <linux/blkdev.h>
struct request { [...]
struct list_head queue;
kdev_t rq_dev;
/* READ or WRITE */
int cmd;
unsigned long sector;
unsigned long nr_sectors;
char * buffer;
struct buffer_head * bh;
request_queue_t *q;
[...]
Cette structure est principalement une liste chaînée de structures struct buffer_head.
#include <linux/fs.h>
struct buffer_head { [...]
/* block size */ unsigned short b_size;
/* Real device */ kdev_t b_rdev;
/* Time when (dirty) buffer should
be written */
unsigned long b_flushtime;
/* request queue */
struct buffer_head *b_recinext;
/* pointer to data block */
char * b_data;
/* I/O completion */
void (*b_end_io)(struct buffer_head *bh,
int uptodate);
/* Real buffer location on disk */
unsigned long b_rsector;
[...]
Un élément de type st ruct buffer_head désigne un bloc de données que le pilote doit traiter de manière atomique.
Chaque tampon de données manipulé par les caches du noyiu-.„, est représenté par une telle structure. Cela permet au noyau
de conserver une information sur la position physique des données sur le périphérique.
En effet, des blocs adjacents sur le périphérique ne le sont plus forcément en mémoire après manipulation. Le noyau va donc se servir des informations contenues dans les différentes structures st ruct buffer_head pour soumettre au pilote des requêtes concernant des blocs proches ou adjacents. Ce modèle est particulièrement adapté pour des périphériques de stockage comme les disques durs.
En effet le noyau va tenter de minimiser les déplacements de la tête de lecture du disque. Pour cela des algorithmes dits « en ascenseur » sont utilisés pour construire les requêtes. L'idée est de déplacer la tête de lecture du disque le plus longtemps possible dans la même direction. Comme nous le verrons plus tard, ces optimisations ne sont pas spécialement intéressantes dans le cas d'une MMC.
À noter qu'une structure struct request reflète les caractéristiques du premier tampon st ruct buf fer_hea d de la liste. Les champs buffer, sector et nr_sector d'une requête sont en fait une simple recopie des champs b_da t a, b_sector etb size du premier tampon sur la liste. Le champ bendiodelastructurestruot buffer_head pointe sur une fonction de complétion.
Cette dernière doit être appelée chaque fois qu'une opération d'entrée/sortie sur ce tampon s'achève. Cela permet de signaler au noyau le résultat de l'opération. La conséquence peut être le réveil d'un processus en attente.
Une dernière précision importante au sujet de la méthode request( ) est que cette dernière n'est pas forcément appelée dans l'environnement d'un processus.
En effet, les accès aux périphériques blocs sont asynchrones et peuvent être traités dans le contexte d'une interruption matérielle. La fonction request( ) est donc soumise aux

                       
    DÉDIÉ                   
                       
                       
restrictions du genre. Elle ne doit pas de manière directe ou indirecte provoquer un réordonnancement des processus (appel à schedule( )).Cela signifie qu'elle doit être exécutée de manière atomique. Ce qui exclut par exemple l'usage de sémaphores...
5.2.7 GESTION DES PARTITIONS
Pour conclure cette rapide introduction sur les block devices, nous allons montrer comment un pilote peut organiser le partitionnement de ses périphériques.
Tout d'abord, l'emploi du nombre mineur doit être précisé. Un pilote est censé pouvoir gérer plusieurs périphériques de même type simultanément. Chaque périphérique est lui- même susceptible de contenir plusieurs partitions. Le nombre mineur doit donc refléter ces deux informations. Une idée est de séparer l'octet du nombre mineur en deux groupes de 4 bits. Les 4 bits de poids fort désigneront le périphérique et les 4 bits de poids faible, les numéros de partition.
Une des tâches supplémentaires incombant à un pilote supportant les partitions est l'initialisation d'une structure struct gendisk :
#include <li nux/genhd. h>
struct gendisk {
/* major number of driver */
int major;
/* name of major driver */
const char *major_name;
/* number of times minor is shifted to
get real minor */
int minor_shi ft;
/* maximum partitions per devise */
int max_p;
/* [indexed by minor] */
struct hd_struct *part;
/* [idem], devi ce size in blocks */ int *si zes;
/* number of real devices */
int nr_real ;
/* internat use */
void *real_devi ces ;
struct gendisk *next;
struct block_device_operations *fops;
/* one per physical disc */
devfs_handle_t *de_arr;
/* one per physical disc */
char *flags;
1;
extern void add_gendisk(struct gendisk *gp); extern void del_gendisk(struct gendisk *gp);
La signification de quelques-uns des champs de la structure struct gendi sk mérite d'être précisée :
•    Le champ major_name est en fait le nom du périphérique tel qu'il apparaîtra dans les logs du noyau. Le noyau complètera automatiquement ce nom par une lettre et un chiffre. La lettre désignera le périphérique et le chiffre la partition. Par exemple, si ma j or_name est initialisé avec la chaîne de caractères mmc,alors mmca 2 désignera la deuxième partition du premier périphérique ;
•    si zes doit contenir exactement les mêmes informations que bl k_si ze[major][].Le pilote pourra d'ailleurs s'arranger pour les faire pointer sur la même zone mémoire ;
•    Le champ mi nor_shi ft indique au noyau l'offset qu'il doit appliquer au mineur pour récupérer le numéro du périphérique. Si les 4 bits de poids forts du mineur contiennent cette information, mi no r_s h i f t doit être initialisé à 4 ;
•    Le pointeur part contient l'adresse de la table des partitions du périphérique. Le pilote devra juste allouer cette structure. Il sera très rarement amené à la consulter.
Une fois cette structure initialisée, le pilote n'a plus qu'à l'ajouter à la liste chaînée des structures struct gendi sk en utilisant la fonction a dd_gen di sk( ). La suite est très simple. La gestion des partitions est complètement transparente pour le pilote. Il devra juste de temps en temps demander au noyau de relire la table des partitions sur le périphérique pour mettre à jour sa structure st ruct hd_struct. Pour cela, il suffira
au pilote d'utiliser la fonction regi s te r_di sk( ).
#include <li nux/bl kdev. h>
extern void register_disk(struct gendisk *dev, kdev_t first, unsigned minors,
struct block_device_operations *ops,
long size);
Un appel à regi st e r_d sk(  ) est toujours suivi par un appel à la méthode request ( ) du pilote. En effet, le noyau doit lire sur le périphérique pour en extraire la table des partitions. Les appels à regi ste r_d sk( ) peuvent être réalisés pendant l'initialisation du périphérique, depuis la fonction i ni t( )
ou depuis la méthode open(. La méthode r e v al i date( ),
appelée suite à un changement de périphérique, est également un bon emplacement pour forcer la relecture de la table des partitions.
5.2.8 PRÉSENTATION DU MODULE MMC_ CISPI MOD
Il est maintenant temps de voir comment mmc_q sp _mod fait usage des techniques exposées précédemment.
Tout d'abord, comme nous l'avons précisé plus tôt, l'utilisation des files d'attente de requêtes permet souvent d'améliorer les performances d'un pilote bloc. En mode SPI, la MMC propose des commandes de transfert dites « multiblocs ». Il est ainsi possible de transférer en une opération plusieurs blocs physiquement adjacents sur la carte.
Cependant, notre pilote ne supporte pas encore ces commandes. Les opérations de lectures-écritures ne se font donc que par transferts de secteur unique (5 12 octets). Recevoir des requêtes sur des blocs adjacents n'offre aucun gain de temps. Pire encore, construire les requêtes en classant les structures struct bu f f e r_hea d en fonction de la position des données sur le disque ralentit notablement les transferts. Partant de ce constat, nous avons décidé de ne pas utiliser de file d'attente de requêtes. Le pilote n'implémente donc pas de méthode request ( ).À la place, il définit une fonction ma ke_request O.

Habituellement cette fonction construit les requêtes destinées au pilote puis appelle la méthode request ( ) de ce dernier. Le noyau met à disposition des pilotes une fonction ma ke _ request ( ) générique.
Nous allons tout simplement remplacer cette fonction par la nôtre.Son travail sera de transférer directement les données sur la MMC. Cette technique est couramment employée dans l'écriture des pilotes pour des périphériques ne tirant pas avantage des optimisations. Un pilote pour ramdi s k constitue un bon exemple.
Les transferts sont pour lui de simples copies mémoires. Construire des requêtes le ralentirait considérablement. Un exemple d'implémentation de pilote r a md I sk est consultable dans le fichier /[...]/1 inux/dri vers/bl ock/rd.c des sources du noyau Linux.
#include <linux/blkdev.h>
typedef int (make_request_fn) (request_queue_t *q, int rw, struct buffer_head *bh);
extern void blk_queue_make_request(
request_queue_t *, make_request_fn *);
La fonction bl k_queue_ma ke_request est utilisée pour associer une fonction ma ke_request ( ) à une file d'attente de requête
blk_queue_make_request ( BLK_DEFAULT_OUEUE(mmc_blk_major),
mmc_blk_make_request);
La fonction ma k e r eq uest chargée d'exécuter les lectures
et les écritures sur la MMC constitue le coeur du pilote. Elle se trouve dans l'élément mmc_co r e du module mmc_qspi_mod. Voici son implémentation
static int mmc_blk_make_request (request_queue_t *queue, int rw, struct buffer_head *bh)
int minor = MINOR(bh->b_rdev);
int device_num    minor    DEVICE_SHIFT;
struct mmc_blk *dev;
unsigned char *block_addr_ptr[MAX_BLK_NB]; unsigned long block_addr[MAX_BLK_NB];
int i, size = 0, nb_block, status = 1, count;
if ((device_num <0) II (device_num >= nb_dev))
err("%s : device %d dont exits", FUNCTION__, device_num); status = 0;
goto exit;
dey = (struct mmc_blk *) (mmc_blk_dev + device_num);
/* only handle modulo BLKSIZE_SIZE request */ if ((bh->b_size%DFLT_HARDSECT_SIZE) != 0)
err("%s - can't handl@ a %d bytes request (not a x%d size)", __FUNCTION__, bh->b_size, DFLT_BLKSIZE_SIZE);
status = 0;
goto exit;
.nb_block    bh->b_size / DFLT_HARDSECT_SIZE;
/* dont allow ta treat more than
MAX_BLK NB blocks nb_block depend from MMC size and from filesystem type */
if (nb_block > MAX_BLK_NB)
err(ls - request ta handle %d blocks (max FUNCTION , nb_block, MAX_BLK_NB);
status r
goto exit;
for (i    0; i < nb_block; i++)
/* use DFLT_BLKSIZE_SIZE instead of
* mmc_blk_blocksizes[device_num] * hard MMC block == 512 */
block_addr[i] r (mmc_blk_partitions[minor].start_sect +
bh->b_rsector) * mmc_blk_sectorsizes[device_num] +
(i * DFLT_HARDSECT_SIZE);
/* endianness */
block_addr[i] = • cpu_to_be32(block_addr[i]);
block_addr_ptr[i] = (unsigned char *) &block_addr[i];
/* dont allow MMC overflow access */
if ((bh->b_size) > (mmc_blk_partitions[minor].nr_sects * mmc_blk_sectorsizes[device_num]))
err("%s : device %d overflow", FUNCTION , device_num); status = 0;
goto exit;
}
#if CONFIG_HIGHMEM
bh = create_bounce (rw, bh); #endif
switch (rw)
case READ case READA
for (i = 0; i < nb_block; i++)
count = 2;
do
{
size = read_block (block_addr_ptr[i][0],
block_addr_ptr[i][1], block_addr_ptr[i][2], block_addr_ptr[i][3], bh->b_data + (i * DFLT_HARDSECT_SIZE));
while ((size != DFLT_HARDSECT_SIZE) && (count--)); if (size != OFLT_HARDSECT_SIZE)
err("failed ta read block %d-%d-%d-%d",
block_addr_ptr[i][0], block_addr_ptr[i][1], block_addr_ptr[i][2], block_addr_ptr[i][3]): status 0;


Cordialement

L'équipe Parisdepannage.fr

Hors ligne

 

Pied de page des forums


Copyright Parisdepannage.fr


Fermer la fenètre