Cet article porte sur l'utilisation des DMA dans un driver RTDM Xenomai. Il présentera des principes généraux sans se reposer sur un matériel en particulier. Une première partie expliquera tout d'abord ce qu'est un driver RTDM, le fonctionnement des DMA puis nous verrons dans quelle mesure l'utilisation des DMA peut être pertinente pour des applications temps réel. Une deuxième partie expliquera les principales étapes pour implémenter un transfert par DMA dans un driver RTDM.
Un peu de théorie
Généralités sur les drivers RTDM
Xenomai permet d'éxecuter des applications dans un contexte temps réel. Ces applications sont dites dans le "primary domain", ou domaine de Xenomai, tant qu'elles utilisent des fonctions de Xenomai. En revanche, si une des ces applications utilise un appel système Linux, alors elle bascule le domaine de Linux (secondary domain) le temps d'exécuter cette fonction. Ce changement de domaine est assez coûteux en temps mais surtout perturbe le comportement du temps réel dur.
Une application temps réel (Xenomai) en espace utilisateur est souvent limitée quant à l'accès au matériel et est obligée de passer par l'utilisation d'un driver. Or l'utilisation d'un driver Linux classique provoque un changement de domaine vers un contexte non-temps réel, ce que nous voulons à tout prix éviter dans ce type d'application. La solution : Les drivers RTDM. Ces derniers peuvent s’exécuter à la fois dans le domaine de Xenomai et dans le domaine de Linux.
Un driver RTDM peut implémenter les fonctions classiques : open, read, write, ioctl, listen, etc. Au niveau du code, il y a un handler "RT" et un "non-RT" pour chacune de ces fonctions. Les handlers "RT" seront exécutés si l'application était dans le domaine de Xenomai durant l'appel à la fonction RTDM. Si l'application était dans le domaine de Linux, c'est le handler "non-RT" qui sera exécuté à place.
Le code d'un driver RTDM est très similaire à un driver Linux classique. Les fonctions de Linux peuvent être appelées depuis le driver RTDM. Cependant, les fonctions "RT" du driver ne doivent jamais appeler l’ordonnanceur Linux, car c'est l'ordonnanceur Xenomai qui fait foi dans le domaine primaire. Or, dans Linux, la plupart des mécanismes possèdent des points de préemption et donc ne peuvent pas être utilisés directement dans un contexte temps réel. Par exemple, l'utilisation de timers Linux ou de KThread ne peut pas se faire dans une des fonctions "RT" d'un driver RTDM dans la mesure où l’exécution de ces derniers est "monitorée" par l’ordonnanceur Linux. Par "point de préemption", on entend tout mécanisme appelant de manière directe ou indirecte l’ordonnanceur. Les mécanismes d'interruptions, de timers, de gestion de ressources (sémaphores par exemple) en font partie. En général, pour chaque fonctionnalité minimale du kernel nécessitant des points de préemptions, une fonction analogue "RT" est proposée par Xenomai (voir la documentation Xenomai).
Lorsqu'on veut faire un driver RTDM, la difficulté principale est de savoir quelles fonctions de Linux peuvent être appelées dans le domaine temps réel. Un driver peut nécessiter des fonctionnalités délivrées par d'autres drivers, par exemple pour configurer des horloges ou utiliser un contrôleur DMA. A chaque fois que le code Linux que nous avons besoin d'utiliser contient des points de préemption, il sera nécessaire de ré-implémenter le même mécanisme en utilisant l'API de Xenomai à la place.
On notera également que les fonctions de l'API Xenomai peuvent être appelées depuis les handlers "non-RT".
Généralités sur les DMA
Le DMA (Direct Memory Access) est une méthode permettant de soulager le CPU en transférant en parallèle des données de la mémoire vers un périphérique ou dans l'autre sens. Le contrôleur DMA accède donc à la mémoire en concurrence avec le CPU. La mémoire étant alors une ressource partagée entre le(s) contrôleur(s) DMA et le CPU, des mécanismes permettent de gérer les conflits d'accès et les priorités. La fin d'un transfert déclenche généralement une interruption pouvant être traité par le noyau.
La configuration d'un DMA consiste à définir où se trouve les données, où les mettre et comment les transférer. Suivant les utilisations, on peut définir des registres de contrôle, c'est à dire des valeurs que prendront certains registres pendant le transfert des données. Le fonctionnement d'un contrôleur DMA dépend bien sûr du type périphérique vers/depuis lequel les données sont transférées.
Prenons l'exemple du contrôleur DMA de l'i.MX28 de Freescale. Pour faire un transfert par DMA, il faut créer des "mots de commande". Ce sont des structures de données à placer en mémoire qui contiennent :
- L'adresse mémoire physique de la prochaine commande
- Le nombre d'octets à transférer
- Le nombre de registres de contrôle à écrire
- Des options (sens du transfert, flag pour exécuter la prochaine commande, flag pour déclencher une IRQ à la fin, ...)
- L'adresse mémoire physique des données à transférer
- La valeurs des registres de contrôle
Le contrôleur DMA lit cette structure et exécute la commande renseignée. Il écrit d’abord les registres de contrôle associés puis transfert les données vers ou pointées par le champ 5). Dans notre exemple, il y a donc en mémoire à la fois des mots de commande et les zones mémoire contenant les données à lire ou écrire. Pour lancer le transfert, l'adresse du premier "mot de commande" doit être écrit dans un des registres du contrôleur DMA.
/!\ Il est important de noter qu'une adresse mémoire utilisée par un contrôleur DMA est une adresse "I/O", qui correspond à une adresse physique en l'absence d'IOMMU. Le CPU, quant à lui, utilise des adresses virtuelles qui sont traduites par la MMU en adresse physique. Concrètement, les pointeurs manipulés dans le code C d'un driver sont des adresses virtuelles alors que les adresses écrites dans les registres des contrôleurs DMA devront être des adresses "I/O".
/!\ Les contrôleurs DMA lisent ou écrivent dans la mémoire à partir de deux informations : l'adresse de départ et le nombre d'octets. Cela implique que la mémoire accédée doit être contiguë d'un point de vue du contrôleur. En l'absence d'IOMMU, cela veut dire que cet espace mémoire doit être contigu physiquement. Or il est très difficile de réserver une zone mémoire contiguë de grande taille. Pour pallier à ce problème, on peut fragmenter un transfert DMA en chaînant plusieurs ordres de transfert de plus petite taille.
DMA et temps réel
Avant toute chose, il est important de se demander dans quel cas l'utilisation du DMA est plus intéressante que transférer les données avec le CPU. Nous avons vu que l'avantage du DMA est de transférer des données vers ou depuis la mémoire sans monopoliser le CPU. Or le fait de libérer le CPU pour une autre tâche n'est pas toujours une bonne chose dans la mesure où un changement de tâche peut être coûteux en temps.
Prenons un exemple simple : un driver RTDM veut transférer cinq octets vers un périphérique de communication en utilisant le DMA. Le driver va devoir initialiser le contrôleur DMA associé, construire ses ordres de transfert et mettre les données en mémoire dans des zones contiguës. Ensuite, il lance le transfert et attend l'IRQ de fin. Le fait d'attendre signifie que l’ordonnanceur temps réel va lancer une autre tâche, par exemple "Linux", qui est considéré comme une tâche de priorité inférieure par Xenomai. Après que le périphérique de communication ait fini d'envoyer ses cinq octets, il déclenche interruption attendue ce qui provoque la préemption de Linux pour continuer l'exécution du driver RTDM.
Par rapport à un transfert en utilisant le CPU, un driver RTDM sera toujours plus long en utilisant le DMA dans la mesure où on ajoute le temps de changement vers une autre tâche (ici Linux) puis la préemption de celle-ci. Or la préemption d'une tâche comme Linux peut prendre un temps assez important. Pour un i.MX28, nous avons constaté une latence maximale de l'ordre de la centaine de microsecondes. Plus la taille des données à transférer est grande, plus la durée du transfert sera longue et plus l'utilisation du DMA sera pertinente. L'important est de n'utiliser le DMA que lorsque le temps de préemption maximum est négligeable par rapport au temps de transfert.
Implémentation d'un driver RTDM
Un driver RTDM voulant utiliser un contrôleur DMA ne peut pas utiliser l'API DMA, très complète, de Linux dans la mesure où ces fonctions contiennent des points de préemption (gestion des IRQ par Linux). Il vous faut donc les ré-implémenter en utilisant l'API de Xenomai.
Initialisation du driver
Pour que le DMA fonctionne, il faut d’abord initialiser le "channel" DMA correspondant à votre périphérique. Cette initialisation dépend du matériel que vous utilisez, on ne donnera donc pas d'exemple ici.
Si vous souhaitez optimiser votre driver RTDM, il peut être intéressant d'anticiper toutes les actions que vous devriez faire avant une communication, ce qui présente un gain de temps important pour les tâches périodiques. Par exemple, vous pouvez réserver votre mémoire contiguë ou vos IRQ dès cette phase si vous ne voulez pas le faire à chaque transfert.
Il est important de noter que le chargement d'un driver RTDM se fait dans le domaine de Linux, non temps réel.
Utiliser la mémoire cohérente
Linux gère une "pool" de mémoire cohérente d'une taille prédéfinie qui peut être écrasée par un paramètre dans la ligne de commande du noyau : coherent_pool=nn[KMG]. Dans votre driver RTDM, la fonction suivante permet de réserver de la mémoire cohérente :
void *cpu_addr; // Adresse virtuelle utilisée par le CPU (OUT) dma_addr_t phys_addr; // Adresse physique (OUT) size_t size; // Taille de la mémoire (IN) struct device *dev; // Pointeur "device" du driver (IN) // Allocation cpu_addr = dma_alloc_coherent(dev, size, &phys_addr, GFP_ATOMIC); // Libération dma_free_coherent(dev, size, cpu_addr, phys_addr);
La fonction réserve dans la "coherent_pool" l'espace mémoire désiré et retourne un pointeur vers le début de cette zone, ou NULL en cas d'échec. L'adresse physique correspondante est également écrite dans phys_addr. La structure dev est celle obtenue grâce à la fonction "probe" de votre driver RTDM. L'option GFP_ATOMIC permet de spécifier que la fonction ne doit pas être préemptable, elle n'est pas nécessaire si vous réservez la mémoire pendant la phase d'initialisation.
Pour écrire des données dans cette mémoire cohérente, vous devrez utiliser le pointeur cpu_addr. En revanche, il faudra utiliser le pointeur phys_addr pour écrire dans les registres du contrôleur DMA et dans les mots de commande. Vous pouvez appeler plusieurs fois la fonction d'allocation tant que vous vous assurez qu'il y a assez de mémoire disponible. Par exemple, on peut réserver de la mémoire cohérente pour stocker un tableau de "mots de commande" et réserver d'autres zones pour stocker les bufffers de données à envoyer ou recevoir.
Gestion de l'IRQ du DMA
Avant de pouvoir utiliser une IRQ, il faut la réserver. Xenomai propose un ensemble de fonctions permettant de gérer les interruptions. Il faudra veiller à ce que Linux n'utilise pas les mêmes interruptions, cela peut arriver si un driver Linux contrôle le même périphérique.
struct private_data { rtdm_event_t *irq_event; int success; ... }; static int dma_irq_handler(rtdm_irq_t *irq_context) { struct private_data *pdata = rtdm_irq_get_arg(irq_context, struct private_data *); // Vérifier qu'il n'y a pas eu d'erreur pdata->success = 1; // On réveille la fonction "transfer()" rtdm_event_signal(pdata->irq_event); return RTDM_IRQ_HANDLED; } void transfer(...) { int dma_int = ...; // Numéro de l'IRQ (/!\ numéro virtuel Linux) rtdm_irq_t dma_handle; // Structure pour gérer les IRQ sous Xenomai rtdm_event_t irq_event; // Structure pour faire de l'asynchrone sous Xenomai struct private_data pdata; // Pointeur passé au handler de l'IRQ // ----------------------------- // INIT // ----------------------------- pdata.irq_event = &irq_event; pdata.success = 0; rtdm_irq_request(&dma_handle, dma_int, dma_irq_handler, 0, "DRIVER_NAME", &pdata); rtdm_irq_enable(&dma_handle); rtdm_event_init(&irq_event, 0); // ----------------------------- // TRANSFERT // ----------------------------- // Configurer le DMA // Construire les mots de commandes (en mémoire cohérente) // Si le sens de transfert est mémoire => périphérique, on rempli la mémoire (en mémoire cohérente) // Démarrer le transfert // Attente de l'IRQ de fin status = rtdm_event_timedwait(&irq_event, TIMEOUT_NS, NULL); if (status || !pdata.success) { rtdm_printk(KERN_ERR "DMA error detected\n"); } // ----------------------------- // END // ----------------------------- rtdm_irq_free(&dma_handle); rtdm_event_destroy(&irq_event); }
On utilise le système d’événement de Xenomai pour réveiller la fonction de transfert à la réception de l'IRQ de fin. On peut également définir un timeout pour ne pas être bloqué si l'IRQ n'arrive pas dans un temps raisonnable, c'est d'autant plus utile en temps réel pour garantir un temps d'exécution maximum de la fonction de transfert.
/!\ Dans cet exemple, la fonction transfer() ne doit pas être appelée par plusieurs tâches simultanément. Il faudra donc utiliser un mécanisme de gestion de ressources, par exemple avec un mutex RTDM.
Conclusion
Le DMA est par essence un mécanisme asynchrone mais son utilisation dans un contexte temps réel est parfois pertinente. Cet article a retracé les principales problématiques que nous pouvons rencontrer en voulant implémenter un driver RTDM utilisant du DMA.
Vous trouverez de plus amples informations au sujet des DMA et des drivers RTDM dans les documents suivants :
- Article sur l'utilisation de l'API DMA de Linux : http://www.linuxjournal.com/article/7104
- Livre sur le développement de driver : https://lwn.net/Kernel/LDD3/
- Doc Xenomai : http://www.xenomai.org/documentation/trunk/html/api/index.html
- Premiers pas avec Xenomai : http://dchabal.developpez.com/tutoriels/linux/xenomai/