Semaine Athens Linux Device Drivers


Introduction (MMIO vs. PMIO)

Un pilote contrôle un périphérique en effectuant des lectures et des écritures vers ses registres.

On distingue deux types de périphériques :

La première méthode (MMIO) est la plus couramment utilisée sur les plates-formes supportant Linux (à l'exception des plates-formes x86 et x86_64 qui pour des raisons principalement historiques continuent à conserver certains périphériques de base sur un port à part).

Accès aux périphériques PMIO

Allocation

Pour accéder aux périphériques PMIO (ceux apparaissant dans un espace d'adressage spécial dédié aux périphériques), il faut tout d'abord informer le noyau de la plage d'adresses (on parle aussi de numéros de ports) à laquelle on souhaite accéder. Cela se fait grâce à la fonction (définie dans include/linux/ioport.h):

struct resource * request_region(resource_size_t start, resource_size_t n, const char *name)

Ses arguments sont :

Cette fonction renvoie NULL en cas d'erreur (principalement si la zone demandée est déjà utilisée par un autre pilote).

L'appel à request_region n'est pas absolument nécessaire (il est possible de s'en passer). Il s'agit plus d'une convention.

Une fois que l'on n'a plus besoin, on peut désallouer la zone à l'aide de la fonction :

void release_region(resource_size_t start, resource_size_t n)

La liste des plages actuellement allouées est accessible dans /proc/ioports.

Accès

Les fonctions unsigned inb(unsigned port), unsigned inw(unsigned port) et unsigned inl(unsigned port) permettent de lire respectivement 8, 16 et 32 bits depuis l'adresse I/O passée en argument.

De même, les fonctions void outb(unsigned char byte, unsigned port), void outw(unsigned short word, unsigned port) et void outl(unsigned word, unsigned port) permettent d'écrire respectivement 8, 16 et 32 bits vers une adresse I/O.

Il est également possible de lire plusieurs mots de 8, 16 ou 32 bits :

void ins[b|w|l](unsigned port, void *address, unsigned long count);
void outs[b|w|l](unsigned port, void *address, unsigned long count);

Accès aux périphériques MMIO

Allocation

De la même manière que pour les périphériques PMIO, il faut tout d'abord réserver la plage d'adresses correspondant au périphérique grâce à la fonction :

struct resource * request_mem_region(resource_size_t start,
                                     resource_size_t n,
                                     const char *name)

La zone peut ensuite être désallouée grâce à la fonction :

void release_mem_region(resource_size_t start, resource_size_t n)

La liste des régions allouées est disponible dans /proc/iomem.

Mapping mémoire

Une fois la zone réservée, il faut ensuite mapper la plage d'adresses (physiques) correspondant au périphérique dans l'espace d'adressage virtuel afin que les instructions de lecture et d'écriture mémoire, qui travaillent dans l'espace d'adressage virtuel, puisse accéder à ces adresses physiques.

Cette opération s'effectue avec la fonction devm_ioremap définie dans <linux/io.h>:

void __iomem *devm_ioremap(struct device *dev, resource_size_t offset,
                           resource_size_t size);

Cette fonction mappe la plage d'adresses physiques débutant à l'adresse physique start et de taille size dans l'espace d'adressage virtuel du noyau. Elle renvoie la première adresse virtuelle de la zone ou NULL si une erreur s'est produite.

Comme les autres fonctions devm_* ce mapping est automatiquement supprimé lorsque le périphérique disparaît. L'ancienne fonction reste disponible :

void __iomem *ioremap(resource_size_t start, unsigned long size)

Une fois que la zone n'est plus utilisée, elle peut être démappée à l'aide des fonctions :

void devm_iounmap(struct device *dev, void __iomem *addr);
void iounmap(volatile void __iomem *addr)

Accès

Sur certaines architectures, il n'est pas possible d'accéder directement au périphérique en utilisant l'adresse renvoyée par ioremap.

Des primitives de transfert bas niveau (sans conversion d'endianness ni barrières mémoires) sont définies dans <asm/io.h> :

u8  __raw_readb(const volatile void __iomem *addr);
u16 __raw_readw(const volatile void __iomem *addr);
u32 __raw_readl(const volatile void __iomem *addr);

void __raw_writeb(u8  val, volatile void __iomem *addr);
void __raw_writew(u16 val, volatile void __iomem *addr);
void __raw_writel(u32 val, volatile void __iomem *addr);

Des primitives de transferts à la PCI (lectures : données lues en little endian et éventuellement converties dans l'endianness du processeur ; écriture : données converties depuis l'endianness du processeur vers little endian) sont également disponibles. Ces fonctions contiennent les barrières mémoires nécessaires :

u8  readb(const volatile void __iomem *addr);
u16 readw(const volatile void __iomem *addr);
u32 readl(const volatile void __iomem *addr);

void writeb(u8  val, volatile void __iomem *addr);
void writew(u16 val, volatile void __iomem *addr);
void writel(u32 val, volatile void __iomem *addr);

Problèmes

Le noyau prend soin pour vous de marquer les zones où sont mappés les périphériques comme non cachables. Cela évite que le cache ne vienne perturber sérieusement les communications avec les périphériques.

Néanmoins un autre problème subsiste. Le compilateur et le processeur peuvent réordonner les lectures et les écritures mémoires (donc les lectures et les écritures vers les périphériques), ce qui peut poser problème si les registres de votre périphériques doivent être utilisés dans un ordre particulier.

La solution est l'utilisation de barrières mémoires (définies dans <asm/barrier.h> :

Les primitives d'accès vues précédemment sont protégées à l'exception des fonctions __raw*. Vous n'avez donc pas besoin d'utiliser explicitement des barrières mémoires dans ce cas.

Voir Documentation/memory-barriers.txt pour tout savoir sur les barrières mémoires et notamment dans quels autres cas elles sont utiles.

Accès direct à la mémoire physique par les applications

Le fichier spécial /dev/mem fournit un accès direct à la mémoire physique pour les applications. Une lecture au déplacement (offset) X dans ce fichier retourne la donnée située à l'adresse physique X. Il en va de même pour les écritures.

Ce fichier peut être utilisé par une application pour dialoguer directement avec un périphérique MMIO (cas notamment du serveur X dans certaines configurations). Pour des raisons évidentes de sécurité ce fichier n'est accessible que par une application avec les droits super-utilisateur (root). Pour plus de sécurité, sur certaines architectures, l'option STRICT_DEVMEM de la configuration du noyau permet de restreindre les accès autorisés via /dev/mem à uniquement les zones mémoires utilisées par les périphériques (pour éviter d'avoir accès à la RAM et donc au code et aux données du noyau).

Travail à faire

Objectif : Écrire un pilote de périphérique pour une IP (en l'occurrence une UART) chargée dans la partie FPGA.

La première étape consiste à programmer le FPGA avec l'UART :

  1. Récupérez le design FPGA et copiez-le sur la carte :

    $ cp ~duc/enseignements/tpt35/design_fpga_uart.tar.xz .
    $ scp design_fpga_uart.tar.xz toto@a406-YY-arm.enst.fr:
  2. Sur la carte, décompressez l'archive :

    toto@de1soc:~$ tar xJf design_fpga_uart.tar.xz
  3. Chargez le design sur le FPGA :

    toto@de1soc:~$ cd design_fpga_uart
    toto@de1soc:~/design_fpga_uart$ sudo ./load.sh

    Cette dernière opération devra être effectuée à chaque fois que vous éteignez et rallumez la carte.

La documentation de l'UART est disponible ici : doc/opencore_UART_spec.pdf. La partie intéressante de la documentation commence page 8 du PDF (page 4/16 selon la numérotation des pages du document) jusqu'à la page 16 du PDF (page 12/16).

Attention : Il y a une petite subtilité sur les adresses des registres. Il faut multiplier l'adresse par 4. Exemple : le registre IER (Interrupt Enable) est indiqué à l'adresse 1 dans la documentation. En réalité, son adresse sur le bus (par rapport à l'adresse de base de l'UART) sera 4.

L'UART est connectée sur le bus lwhps2fpga à partir de l'adresse 0. Les périphériques sur ce bus sont accessibles du processeur à partir de l'adresse physique 0xFF20_0000. Le registre IER sera donc accessible à l'adresse physique 0xFF20_0004. Le signal d'interruption de l'UART est connecté au contrôleur d'interruption principal du processeur.

Le bitstream du FPGA contenant l'UART ainsi que le morceau de Device Tree décrivant l'UART sont chargés par un mécanisme de mise à jour dynamique du Device Tree.

Voici le fragment de Device Tree (uart.dtso) qui est chargé par le script load.sh :

/dts-v1/ /plugin/;

/ {
  fragment@0 {
    target-path = "/soc/base-fpga-region";

    #address-cells = <0x1>;
    #size-cells = <0x1>;
    __overlay__ {
        #address-cells = <0x1>;
        #size-cells = <0x1>;

        firmware-name = "uart.rbf";

        myuart@ff200000 {
            compatible = "foo,myuart";
            reg = <0xff200000 0x1f>;
            interrupts = <0 42 4>;
        };
    };
  };
};

Ce dernier va ajouter dynamiquement dans la branche soc/base-fpga-region un périphérique myuart dont le firmware (ici le fichier de configuration du FPGA) est uart.rbf (ce fichier doit se situer dans le répertoire /lib/firmware).

L'horloge de l'UART est de 50 MHz (system clock speed dans les spécifications de l'UART). Cette information est importante pour calculer les rapports de division pour générer le bon débit sur la liaison série.

Le travail demandé est donc de développer un pilote pour cette UART, en utilisant au maximum les interruptions. Côté applications, utilisez dans un premier temps le framework misc, puis le framework spécialisé pour les liaisons séries (à vous de chercher les informations nécessaires).

Pour tester, demandez à un de vos enseignants qui branchera un convertisseur série-USB sur les ports d'extension du FPGA auxquels sont connectées les entrées et sorties de l'UART.


Retour au sommaire du cours


© Copyright 2017 Guillaume Duc. Le contenu de cette page est mis à disposition selon les termes de la Licence Creative Commons Attribution - Partage dans les Mêmes Conditions 4.0 International (à l'exception des exemples de code tirés du noyau Linux et qui sont distribués sous leurs licences d'origine).

Licence
Creative Commons