![]() |
Pilotes de périphériques pour le noyau Linux |
Dans cette page, nous allons voir comment réagir aux interruptions.
Un gestionnaire d’interruption est la fonction, au sein du noyau, qui sera appelée quant une interruption survient. Les interruptions sont identifiées par un numéro (qui peut être partagé : plusieurs sources (périphériques) peuvent générer la même interruption) et les gestionnaires d’interruptions déclarent, au moment de leur enregistrement auprès du noyau, quel numéro d’interruption ils gèrent. Lorsque cette interruption se produit, le noyau appelle ainsi le gestionnaire approprié.
La première étape pour pouvoir réagir à une interruption générée par notre périphérique consiste donc à enregistrer un gestionnaire d’interruption grâce à la fonction définie dans include/linux/interrupt.h :
int devm_request_irq(struct device *dev,
unsigned int irq,
irq_handler_t handler,
unsigned long irqflags,
const char *devname,
void *dev_id);
Ses arguments sont :
struct device *dev
: le périphérique concerné (comme pour les autres fonctions devm_
cela permet de supprimer automatiquement le gestionnaire d’interruption lorsque le périphérique est déconnecté ou que le module correspondant est déchargé)
unsigned int irq
: le numéro de l’interruption demandé
platform_get_irq(struct platform_device *dev, unsigned int num)
permet de récupérer le numéro de l’interruption depuis le Device Tree ou la structure ressources
(num
est l’index du numéro d’interruption à récupérer si plusieurs sont listés, le plus souvent 0
)irq
de la structure struct i2c_client
irq_handler_t handler
: le pointeur vers la fonction servant de gestionnaire d’interruption
unsigned long irqflags
: drapeaux (la liste des drapeaux est disponible dans <linux/interrupt.h>
). Le plus courant est IRQF_SHARED
qui indique que la ligne d’interruption est partagée entre plusieurs périphériques.
const char *devname
: nom du module (apparaîtra dans /proc/interrupts
)
void *dev_id
: pointeur vers des données arbitraires. Ce pointeur sera passé en argument au gestionnaire d’interruption lorsqu’il sera appelé. Il peut être NULL
seulement si la ligne d’interruption n’est pas partagée. Dans le cas contraire, ce pointeur doit être globalement unique.
La fonction retourne 0
si tout s’est bien passé, une valeur négative en cas d’erreur.
Le gestionnaire d’interruption peut être supprimé à l’aide de la fonction :
Comme pour les autres fonctions devm_*
l’appel à cette fonction n’est pas obligatoire, cela sera fait automatiquement quand le périphérique est déconnecté ou que le module est déchargé.
L’ancienne API (fonctions int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)
et void free_irq(unsigned int, void *)
) reste utilisable.
Le prototype du gestionnaire d’interruption est :
Les arguments sont :
int irq
: le numéro de l’interruptionvoid *dev_id
: le pointeur passé à la fonction devm_request_irq
La fonction renvoie :
IRQ_HANDLED
si l’interruption concernait ce périphérique et a été traitée (si la ligne d’interruption est partagée, les autres gestionnaires enregistrés ne seront pas appelés)IRQ_NONE
si l’interruption ne concernait pas ce périphérique (cas par exemple des lignes d’interruptions partagées entre plusieurs périphériques). Le noyau va donc exécuter les autres gestionnaires enregistrés pour cette interruptionPlusieurs contraintes importantes s’imposent au gestionnaire d’interruption :
GFP_ATOMIC
s’il alloue de la mémoire pour éviter que la fonction d’allocation ne bloque. Autre exemple : il ne peut pas non plus bloquer en demandant un mutex.Les rôles classiques du gestionnaire d’interruption sont :
wake_up*
)Dans la plupart des systèmes d’exploitation modernes, et c’est le cas de Linux, le traitement de l’interruption est découpé en deux parties :
Comme le mécanisme de softirq n’est pas accessible directement par les pilotes, nous détaillerons uniquement le mécanisme de tasklet et de workqueue. Il peut être intéressant de noter que le mécanisme de tasklet est construit au dessus de celui de softirq.
Les tasklets on une latence d’exécution inférieure à celle des workqueue (elles s’exécuteront en général plus tôt après l’interruption les ayant déclenché).
Une tasklet est une fonction dont l’exécution peut être programmée par un gestionnaire d’interruption pour une exécution ultérieure.
Elle est représentée par la structure struct tasklet_struct
(définie dans include/linux/interrupt.h).
Cette structure peut être initialisée dynamiquement par la fonction
ou statiquement à l’aide de la macro :
func
est la fonction qui sera appelée lorsque la tasklet sera exécutée et data
un entier qui lui sera fourni en argument.
L’exécution d’une tasklet peut être programmée à l’aide d’une des fonctions suivantes :
void tasklet_schedule(struct tasklet_struct *t); /* Priorité normale */
void tasklet_hi_schedule(struct tasklet_struct *t); /* Haute priorité */
D’après la documentation du noyau :
tasklet_schedule
est appelée, on a la garantie que la tasklet sera exécutée sur un processeur au moins une fois dans le futurtasklet_schedule
a lieu dans la tasklet), elle est reprogrammée pour une exécution ultérieureLa fonction exécutée par la tasklet à pour prototype :
Les tasklets sont exécutées alors que les interruptions sont activées. Elles peuvent donc se permettre de faire des traitements plus long quittes à être interrompues.
Néanmoins, elles s’exécutent toujours dans le contexte d’interruption et donc elles ne doivent pas bloquer ni être mises en sommeil (donc si elles ont besoin de mémoire, elles doivent utiliser le drapeau GFP_ATOMIC
, elle ne peuvent pas utiliser de mutex, etc.).
Le mécanisme de workqueue permet de différer des opérations. Il est utilisable comme mécanisme pour implémenter le bottom half d’un gestionnaire d’interruption mais peut également être utilisé dans n’importe quel contexte où des opérations doivent être différées.
Contrairement aux tasklets, le travail effectué par une workqueue est exécuté dans le contexte d’un processus, c’est-à-dire avec les interruptions activées et la possibilité de bloquer et de se mettre en sommeil.
Les workqueues sont donc appropriées quand l’opération a différer est longue ou doit bloquer.
Une petite documentation est disponible dans Documentation/core-api/workqueue.rst.
Une workqueue est représentée par la structure struct workqueue_struct
définie dans include/linux/workqueue.h. Cette structure est allouée par l’une des deux fonctions :
struct workqueue_struct * create_workqueue(const char *name);
struct workqueue_struct * create_singlethread_workqueue(const char *name);
La première fonction crée un thread noyau par processeur. Les travaux soumis dans cette workqueue s’exécuteront dans l’un de ces thread. La seconde fonction ne crée qu’un seul thread ce qui peut être amplement suffisant.
Une workqueue peut être détruite à l’aide de la fonction :
Enfin, il existe une workqueue globale au noyau qui peut être utilisée plutôt que de créer des workqueues supplémentaires.
Un travail à effectuer par une workqueue est représenté par la structure struct work_struct
ou par la structure struct delayed_work
pour un travail qui doit attendre l’expiration d’un délai avant d’être exécuté.
Ces structure peuvent être déclarées et initialisées statiquement grâce aux macros :
qui déclarent une structure struct work_struct
ou struct delayed_work
appelée n
et dont le travail à effectuer consiste en la fonction f
qui a pour prototype
Ces structures peuvent aussi être initialisées dynamiquement grâce aux macros :
Un travail peut être programmé sur une workqueue à l’aide des fonctions :
bool queue_work(struct workqueue_struct *wq,
struct work_struct *work);
bool queue_delayed_work(struct workqueue_struct *wq,
struct delayed_work *dwork,
unsigned long delay);
Ces fonctions programment l’exécution d’un travail (i.e. l’exécution de la fonction pointée par la structure work_struct
). S’il est déjà programmé, elles renvoient false
. Dans le cas de la seconde fonction, le travail ne débutera pas avant l’expiration du délai delay
exprimé en jiffies.
Pour utiliser la workqueue globale au noyau il faut appeler les fonctions :
bool schedule_work(struct work_struct *work);
bool schedule_delayed_work(struct delayed_work *dwork, unsigned long delay)
Une variante xxx_on(int cpu, ...
de toutes ces fonctions existe pour laquelle il est possible de choisir le processeur sur lequel le travail sera exécuté (cela permet certaines optimisations, notamment si le processeur en question a déjà certaines données dans son cache ou si le processeur à un accès plus rapide à certaines ressources matérielles).
Les fonctions :
bloquent jusqu’à ce qu’un travail soit terminé ou qu’il n’y ai plus de travaux à effectuer sur une workqueue.
Il est possible d’annuler un travail qui n’a pas encore débuté grâce aux fonctions :
bool cancel_work_sync(struct work_struct *work);
bool cancel_delayed_work(struct delayed_work *dwork);
Il est également possible de savoir si un travail est programmé grâce aux macros :
Le dernier mécanisme disponible pour gérer les interruptions est le mécanisme de threaded IRQ.
Il permet de traiter une interruption dans un thread noyau. Les opérations de traitement de l’interruption peuvent alors être bloquantes. Cela peut être utile notamment dans le cas des périphériques pour lesquels l’échange des données est lent (connectés à un bus I2C par exemple) et pour lesquels le gestionnaire d’interruption doit communiquer avec eux.
La fonction devm_request_threaded_irq
permet d’enregistrer un tel gestionnaire d’interruption (à utiliser à la place de devm_request_irq
) :
int devm_request_threaded_irq(struct device *dev, unsigned int irq,
irq_handler_t handler, irq_handler_t thread_fn,
unsigned long irqflags, const char *devname,
void *dev_id);
Les paramètres sont les mêmes que ceux de la fonction devm_request_threaded_irq
à l’exception de :
irq_handler_t handler
: gestionnaire d’interruption proprement dit. Cette fonction s’exécute dans le contexte d’interruption et ne peut donc pas bloquer
IRQ_NONE
), désactiver l’interruption au niveau du périphérique et retourner IRQ_WAKE_THREAD
pour réveiller le thread (ou IRQ_HANDLED
s’il n’y a pas besoin de le faire)NULL
. Dans ce cas, le drapeau IRQF_ONESHOT
doit être utilisé (champ irqflags
) pour que l’interruption reste désactivée jusqu’à la fin de la fonction de traitement (thread_fn
) s’exécutant dans le threadIRQ_WAKE_THREAD
pour réveiller le thread (ou IRQ_HANDLED
s’il n’y a pas besoin de le faire)irq_handler_t thread_fn
: la fonction de traitement de l’interruption qui s’exécutera dans le contexte du thread noyau. Cette fonction sera appelée lorsque l’interruption se produit. Elle doit renvoyer IRQ_HANDLED
si tout s’est bien passé. Si la fonction handler
a explicitement désactivé l’interruption au niveau du périphérique, elle doit être réactivée à la fin de la fonction thread_fn
. Cette fonction s’exécutant dans le contexte d’un thread
noyau, elle peut bloquer (donc elle peut utiliser toutes les primitives de synchronisation, des fonctions d’allocation mémoire bloquantes, etc.). N’oubliez pas d’acquitter l’interruption à un moment.De nombreux périphériques dans drivers/input/touchscreen/ utilisent des threaded IRQ. N’hésitez pas à aller les regarder.
Faites un tableau récapitulant ce qu’à le droit de faire ou pas les fonctions utilisées dans chacun des cas : gestionnaire d’interruption classique, tasklet, workqueue, threaded IRQ, ainsi que la latence d’exécution des trois derniers mécanismes.
Ajoutez la gestion des interruptions à votre pilote de l’accéléromètre :
L’accéléromètre dispose d’une sortie d’interruption (en réalité deux mais une seule seulement est connectée au FPGA). Dans la configuration par défaut de l’accéléromètre, elle est active à l’état haut. Elle est connectée à la patte B22
du FPGA, qui est configurée pour correspondre au GPIO numéro 61
qui lui-même est connecté au contrôleur GPIO numéro 2
(aussi appelé portc
dans le Device Tree qui vous est fourni) sur la ligne numéro 3
.
On peut décrire cette configuration dans l’entrée correspondant à l’accéléromètre dans le Device Tree sous la forme :
interrupt-parent = <&portc>;
interrupts = <3 1>;
Le paramètre interrupt-parent
indique à quel contrôleur d’interruption est connecté le signal d’interruption : ici au contrôleur GPIO portc
qui sert également de contrôleur d’interruption.
Le paramètre interrupts
contient ici deux valeur. La première indique la ligne du contrôleur GPIO à laquelle le signal est connecté (ici la ligne 3
). La seconde valeur indique quelle condition doit déclencher l’interruption (1
: front montant, 2
: front descendant, 4
: niveau logique haut, 8
: niveau logique bas).
Ces deux paramètres du Device Tree sont décrits en détails dans Documentation/devicetree/bindings/interrupt-controller/interrupts.txt.
Commencez par ajouter la configuration des interruptions dans le fichier dts
, recompilez le dtb
et installez-le (voir les instructions données précédemment dans le cours
Dans la fonction d’initialisation de l’accéléromètre, configurez-le pour utiliser les interruptions. Dans un premier temps, nous utiliserons la FIFO en mode stream
et l’interruption watermark
.
L’accéléromètre dispose d’une FIFO permettant de stocker 32 ensembles de résultats de mesure (voir page 20 de la documentation de l’accéléromètre). En mode stream
(registre FIFO_CTL
, voir page 26), la FIFO stocke les 32 dernières mesures effectuées. Si la FIFO est pleine, les mesures les plus anciennes sont supprimées et remplacées par les nouvelles mesures.
L’interruption watermark
indique qu’il y a plus de samples
mesures actuellement stockées dans la FIFO. La valeur de samples
est définie dans le registre FIFO_CTL
. Cette interruption indique que la FIFO est en train de se remplir et qu’il devient nécessaire de la vider.
Configurez samples
à la valeur 20
(arbitraire, il faudrait réfléchir à une valeur plus appropriée en fonction du débit de mesure, de la vitesse de transmission entre l’accéléromètre et le processeur et de la latence de traitement de l’interruption).
Il faut enfin activer l’interruption via le registre INT_ENABLE
(voir page 25).
Écrivez le traitement de l’interruption (top et bottom halves avec le mécanisme qui vous parait le plus approprié). Modifiez également le comportement de votre module, notamment au niveau de la lecture depuis l’espace utilisateur.
Lors de l’interruption, récupérez toutes les données contenues dans la FIFO de l’accéléromètre et stockez-les dans un tampon interne à votre module. La taille maximale de ce tampon sera un paramètre de votre module et aura pour valeur par défaut 64
.
Une lecture depuis l’espace utilisateur doit renvoyer une ou plusieurs mesures en provenance du tampon. Elle doit bloquer si aucune mesure n’est actuellement disponible.
Testez !
© Copyright 2020 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).