Introduction
Dans le cadre d'un développement d'une nouvelle carte électronique fonctionnant sous Linux, il peut arriver qu'un driver ne soit pas (encore) disponible. Il est cependant fort probable que Linux dispose déjà d'un driver similaire et donc facilement modifiable.
Prenons l'exemple d'une mémoire FRAM (Ferroelectric RAM) connectée au processeur par un bus SPI et utilisée en remplacement d'une mémoire flash.
Le driver de la FRAM doit mettre en place une interface d'accès à la mémoire par l'intermédiaire d'un driver caractère /dev/mtdX.
Comment l'intégrer ?
Pour commencer, notre driver doit être basé d'un côté, sur l'API MTD (Memory Technology Device) pour la gestion de l'espace mémoire, de l'autre côté sur l'API SPI (Serial Peripheral Interface) pour la gestion du protocole de communication.
Figure 1: Couches logicielles utilisées pour contrôler la mémoire.
Vérification de l'interface SPI
Avant d'aller plus loin dans le développement du driver à proprement parler, il faut s'assurer que le support du bus SPI soit déjà disponible sur la cible.
Cela comprend la vérification:
- de la configuration des GPIOs utilisés pas le bus SPI: CLK, MISO, MOSI, Chip Select
- de l'enregistrement du contrôleur SPI dans le kernel avec la fonction spi_register_board_info()
Voici un exemple d'initialisation pour une carte ARM davinci da830:
L'extrait de code ci-dessous provient du fichier arch/arm/mach-davinci/da830.c qui permet de décrire la configuration de cette carte. Dans les dernières version du noyau Linux ce genre d'informations est progressivement déplacé dans un fichier "dtb" (Device Tree Binary).
Ici, la structure flash_plateform_data indique au noyau que la carte comporte une mémoire flash de type w25x32 capable d'être gérée par le driver m25p80. Nous remarquons que la définition des partitions de l'espace mémoire est définie dans le BSP et non dans le driver.
En complément d'autres informations spécifiques à cette carte, la structure spi_board_info définit la configuration du bus SPI.
static struct flash_platform_data da830evm_spiflash_data = { .name = "m25p80", .parts = da830evm_spiflash_part, .nr_parts = ARRAY_SIZE(da830evm_spiflash_part), .type = "w25x32", }; static struct davinci_spi_config da830evm_spiflash_cfg = { .io_type = SPI_IO_TYPE_DMA, .c2tdelay = 8, .t2cdelay = 8, }; static struct spi_board_info da830evm_spi_info[] = { { .modalias = "m25p80", .platform_data = &da830evm_spiflash_data, .controller_data = &da830evm_spiflash_cfg, .mode = SPI_MODE_0, .max_speed_hz = 30000000, .bus_num = 0, .chip_select = 0, }, };
spi_register_board_info(ARRAY_AND_SIZE(da830evm_spi_info));
La configuration du bus SPI est très spécifique au modèle de processeur utilisé et ce dernier peut nécessiter son propre driver pour le contrôleur.
Recherche de drivers similaires
Toutes les mémoires flash SPI utilisent les mêmes opcodes pour les fonctions de base. Cela permet de réutiliser facilement le code d'un driver existant pour écrire un nouveau driver spécifique.
Les FRAM ont la particularité d'utiliser la même interface de commandes (même opcodes) que les mémoires flash. En regardant le code des drivers existant, certains sont très similaires à ce que l'on cherche et ne demandent qu'à être adaptés en conséquence.
Le driver m25p80 (datasheet: ici ) servira de base pour notre driver FRAM.
Celui-ci supporte plusieurs modèles de mémoires flash :
- Atmel at25 at26
- EON en25
- Macronix mx25
- Spansion s25
- SST sst25
- ST Microelectronics m25 m45
- Winbond w25
L'écriture du driver FRAM nécessite quelques adaptations par rapport au driver m25p80, mais la majorité du code est commun.
Les principales différences sont:
- Le nombre de cycle d'écriture est largement supérieur à celle d'une FLASH.
- L'effacement de la mémoire FRAM n'est pas nécessaire.
(La config MTD_NO_ERASE peut être activée afin de désactiver les commandes d'effacement avant l'écriture en mémoire.) - Il n'existe pas de notion de page lors de l'écriture.
(La totalité de la mémoire FRAM peut être écrite en une seule commande.) - Le temps d'écriture est négligeable comparé au temps de communication SPI.
(Il n'est pas nécessaire de lire le registre de statut (bit WIP, Write In Progress) avant de commencer une nouvelle écriture en mémoire.)
Détection du composant sur le bus SPI
La détection du composant lors du montage du driver peut se faire de deux façons:
- Soit en utilisant le nom donné par la structure platform_data fournie par le BSP de la carte utilisée (ex: le nom donné dans structure da830evm_spiflash_data). Ce nom est ensuite comparé avec les noms des modèles gérés par le driver. La détection n'est pas réellement réalisée (on fait confiance à la description du BSP) mais c'est la seule méthode disponible lorsqu'un composant ne possède pas son propre numéro JEDEC.
- Soit en effectuant la lecture du registre d'identification, en envoyant sur le bus SPI la commande RDID. Le registre d'identification contient le numéro du fabricant fournis par JEDEC, le numéro du type de mémoire ainsi que la capacité mémoire.
Par défaut, le driver vérifie si le BSP fournit un nom de modèle avant d'envoyer une commande RDID.
Initialisation de la couche MTD
Une fois la mémoire détectée et identifiée, le driver initialise la structure mtd_info avec les caractéristiques associées à ce type de mémoire. Cela permet de donner à l'API MTD toutes les informations nécessaires sur les caractéristiques de la mémoire.
Le tableau ci dessous liste les champs à initialiser:
Le nom du modèle | flash->mtd.name | "m25P80" |
Le type de mémoire | flash->mtd.type | MTD_NORFLASH |
La taille max d'écriture | flash->mtd.writesize | 1 octet |
Taille d'une page | flash->page_size | page_size (256 octets) |
flag de configuration | flash->mtd.flags | MTD_CAP_NORFLASH |
La taille de la mémoire | flash->mtd.size | sector_size * n_sectors |
Les fonctions d'effacement/lecture/écriture(dépend du type de mémoire) | flash->mtd._erase flash->mtd._read flash->mtd._write |
m25p80_erase m25p80_read m25p80_write |
La taille du bloque d'effacement. (le plus petit possible) | flash->mtd.erasesize | sector_size |
Liaison avec le driver SPI | flash->mtd.dev.parent | &spi->dev |
(les définitions MTD_* sont dans le fichier mtd-abi.h)
La couche MTD est maintenant capable de gérer entièrement l'espace mémoire disponible sur le composant. Reste à écrire les fonctions de communication entre la couche MTD et SPI. Celles-ci ont pour rôle de préparer les trames SPI respectant le format défini dans la documentation de la mémoire (opcode, adressage, écriture par pages...). Leur prototype est imposé par la couche MTD qui est défini dans linux/include/linux/mtd/mtd.h
Fonction de lecture :
int (*read) (struct mtd_info *mtd, loff_t from, size_t len, size_t *retlen, u_char *buf);
from : adresse où démarrer la lecture
len : nombre d'octets à lire
retlen: nombre d'octets lu
buf : données lues depuis la mémoire
Fonction d'écriture :
int (*write) (struct mtd_info *mtd, loff_t to, size_t len, size_t *retlen, const u_char *buf);
to : adresse où démarrer l'écriture
len : nombre d'octets à écrire
retlen: nombre d'octets écrit
buf : données à écrire dans la mémoire
Les mémoires flash ne sont capables d'écrire qu'un nombre limité d'octets à la fois, ce nombre correspond à la taille d'une page (ex: 256 octets).
La couche MTD ne connaît pas la taille d'une page et il peut donc arriver que la couche MTD demande d'envoyer des données dépassant la taille d'une page. De ce fait, la fonction d'écriture doit découper les données à transférer et envoyer autant de trames SPI que nécessaire pour achever l'écriture des données. Entre chaque transfert, le driver doit s'assurer que l'écriture précédente est terminée.
Fonction d'effacement :
int (*erase) (struct mtd_info *mtd, struct erase_info *instr);
La structure erase_info contient l'adresse où commencer l'effacement et le nombre d'octets à effacer.
Cette fonction doit mettre à jour le statut instr->status avec MTD_ERASE_DONE en cas de succès ou MTD_ERASE_FAILED s'il y a eu un problème lors de l'effacement.
Utilisation de l'API SPI
Chacune des fonctions précédentes doit utiliser l'API SPI (documentation SPI) pour envoyer des trames sur le bus. Les envois de trames longues nécessitent la préparation d'une structure spi_message contenant, comme son nom l'indique, le message à envoyer.
Par exemple:
- l'opcode de la commande d'écriture
- l'adresse où commencer l'écriture
- les données à écrire
struct spi_transfer t[2];
struct spi_message m;
spi_message_init(&m);
[…]
Première partie de la trame:
opcode de la commande
t[0].tx_buf = OPCODE_PP;
nombre d'octets servant à l'adressage dans la mémoire
t[0].len = m25p_cmdsz(flash);
ajout de la trame t[0] aux messages à envoyer
spi_message_add_tail(&t[0], &m);
[…]
Deuxième partie de la trame:
buf contient les données à envoyer
t[1].tx_buf = buf;
ajout de la trame t[1] aux messages à envoyer
spi_message_add_tail(&t[1], &m);
len contient la taille des données à envoyer, celle-ci doit être inférieur à la taille d'une page.
t[1].len = len
Sinon envoyer le contenu du buffer en plusieurs trames SPI.
les données sont échangées entre le processeur et la mémoire SPI
spi_sync(flash->spi, &m);
Pour la configuration de la mémoire et la lecture des registres de contrôle, les transferts sont réalisés avec des fonctions simples:
Demande d'identification :
spi_write_then_read(flash->spi, OPCODE_RDID, 1, NULL, 0);
Écriture dans le registre de statut :
spi_write(flash->spi, OPCODE_WRSR, 1);
Conclusion:
Dès lors que la couche SPI est fonctionnelle au niveau du BSP, l'ajout d'un driver pour un nouveau composant mémoire SPI ne demande que de réaliser la liaison entre l'API MDT et l'API SPI.
Dans le cas d'une mémoire FRAM, le driver est encore simplifié grâce aux caractéristiques de ce type de mémoire. Les FRAM sont aussi utilisées en remplacement de mémoire EEPROM sur le bus I2C. Dans ce cas, elles adoptent la même interface de programmation que les composants I2C.
Cependant les FRAM sont chères et ne disposent que d'une quantité de mémoire limitée à quelques centaines de Ko. Elles sont principalement utilisées comme espace de stockage de configuration ou de petite quantité de données.