- Présentation du GICv2 32 bits ARMv7
L'architecture ARMv7 32 bits utilise le contrôleur d'interruption générique (GIC) en version 2. La spécification de référence est publiée par ARM sous la référence IHI0048. L'IP GIC-400 est une implémentation courante de ce contrôleur. Lorsqu'un signal d'interruption externe est détecté, le GIC le signale au cœur ARM.
Le GIC se décompose en deux blocs logiques :
- Distributeur (Distributor) : centralise l'activation globale des interruptions, configure la sensibilité (front ou niveau), les priorités, la destination CPU et l'état pending.
- Interface processeur (CPU Interface) : achemine l'interruption vers le cœur, gère l'acquittement (acknowledge), la fin de traitement (end of interrupt) et la politique de préemption.
- Accès matériel par une structure C
Le fichier d'en-tête d'un SDK tel que celui de l'i.MX6UL définit souvent l'ensemble des registres du GIC par une structure. Ci-dessous une reformulation équivalente avec des noms de champs différents.
typedef struct {
/* Distributeur, base + 0x1000 */
uint32_t res0[1024];
volatile uint32_t dist_ctrl; /* 0x1000 */
volatile uint32_t dist_type; /* 0x1004 */
volatile uint32_t dist_iidr; /* 0x1008 */
uint32_t res1[29];
volatile uint32_t dist_igroupr[16]; /* 0x1080 */
uint32_t res2[16];
volatile uint32_t dist_isenabler[16]; /* 0x1100 */
uint32_t res3[16];
volatile uint32_t dist_icenabler[16]; /* 0x1180 */
uint32_t res4[16];
volatile uint32_t dist_ispendr[16]; /* 0x1200 */
uint32_t res5[16];
volatile uint32_t dist_icpendr[16]; /* 0x1280 */
uint32_t res6[16];
volatile uint32_t dist_isactiver[16]; /* 0x1300 */
uint32_t res7[16];
volatile uint32_t dist_icactiver[16]; /* 0x1380 */
uint32_t res8[16];
volatile uint8_t dist_ipriority[512]; /* 0x1400 */
uint32_t res9[128];
volatile uint8_t dist_itargetsr[512]; /* 0x1800 */
uint32_t res10[128];
volatile uint32_t dist_icfgr[32]; /* 0x1C00 */
uint32_t res11[32];
volatile uint32_t dist_ppisr; /* 0x1D00 */
volatile uint32_t dist_spisr[15]; /* 0x1D04 */
uint32_t res12[112];
volatile uint32_t dist_sgir; /* 0x1F00 */
uint32_t res13[3];
volatile uint8_t dist_cpendsgir[16]; /* 0x1F10 */
volatile uint8_t dist_spendsgir[16]; /* 0x1F20 */
uint32_t res14[40];
volatile uint32_t dist_periph_id[8]; /* 0x1FD0 */
volatile uint32_t dist_comp_id[4]; /* 0x1FF0 */
/* Interface processeur, base + 0x2000 */
volatile uint32_t cpu_ctrl; /* 0x2000 */
volatile uint32_t cpu_pmr; /* 0x2004 */
volatile uint32_t cpu_bpr; /* 0x2008 */
volatile uint32_t cpu_iar; /* 0x200C */
volatile uint32_t cpu_eoir; /* 0x2010 */
volatile uint32_t cpu_rpr; /* 0x2014 */
volatile uint32_t cpu_hppir; /* 0x2018 */
volatile uint32_t cpu_abpr; /* 0x201C */
volatile uint32_t cpu_aiar; /* 0x2020 */
volatile uint32_t cpu_aeoir; /* 0x2024 */
volatile uint32_t cpu_ahppir; /* 0x2028 */
uint32_t res15[41];
volatile uint32_t cpu_apr0; /* 0x20D0 */
uint32_t res16[3];
volatile uint32_t cpu_nsapr0; /* 0x20E0 */
uint32_t res17[6];
volatile uint32_t cpu_iidr; /* 0x20FC */
uint32_t res18[960];
volatile uint32_t cpu_dir; /* 0x3000 */
} GicHwRegs;
- Classification des sources d'interruption
Le GIC distingue trois catégories :
- SPI (Shared Peripheral Interrupt) : interruptions partagées entre plusieurs cœurs, typiquement générées par des périphériques externes.
- PPI (Private Peripheral Interrupt) : interruptions privées d'un cœur donné, par exemple un timer local.
- SGI (Software-generated Interrupt) : déclenchées par logiciel via l'écriture dans
dist_sgir, utilisées pour la communication inter-processeurs.
- Numérotation des interruptions
Chaque cœur peut disposer de 1020 identifiants d'interruption distincts :
- 0 à 15 : SGI.
- 16 à 31 : PPI.
- 32 à 1019 : SPI.
Prenons l'exemple de l'i.MX6UL : ce SoC exploite 128 SPI, auxquels s'ajoutent 32 identifiants réservés pour SGI et PPI, soit 160 numéros au total. Ainsi, un hwirq de 32 correspond au premier numéro utilisable pour un périphérique externe. Le SDK NXP définit ces entrées dans une énumération propre à la plate-forme.
#define NOMBRE_VECTEURS 160
typedef enum {
IRQN_LOGICIEL_0 = 0,
IRQN_LOGICIEL_1 = 1,
IRQN_ENTRETIEN_VIRT = 25,
IRQN_TIMER_HYPERVISEUR = 26,
IRQN_TIMER_VIRTUEL = 27,
IRQN_TIMER_PHYS_SECURISE = 29,
IRQN_TIMER_PHYS_NON_SECURISE = 30,
IRQN_PERIPH_IOMUXC = 32,
IRQN_PERIPH_DAP = 33,
IRQN_PERIPH_SDMA = 34,
IRQN_PERIPH_TSC = 35,
IRQN_PERIPH_SNVS = 36,
/* ... jusqu'à 160 ... */
} NumeroIrq;
- Configuration d'une interruption
5.1 Activation globale IRQ et FIQ
Le registre d'état CPSR possède deux bits : I pour masquer IRQ et F pour masquer FIQ. Des instructions dédiées permettent d'en modifier l'état rapidement.
| Instruction | Effet |
|---|---|
cpsid i |
Masque les interruptions IRQ |
cpsie i |
Autorise les interruptions IRQ |
cpsid f |
Masque les interruptions FIQ |
cpsie f |
Autorise les interruptions FIQ |
5.2 Activation individuelle d'un identifiant
Les registres GICD_ISENABLERn et GICD_ICENABLERn activent ou désactivent chaque interruption. Pour un Cortex-A7, seuls 512 identifiants sont généralement exploités, répartis en 16 bancs de 32 bits. Pour un hwirq donné m :
n = m / 32;
bit = m % 32;
offset = 0x100 + 4 * n; /* pour GICD_ISENABLERn */
offset = 0x180 + 4 * n; /* pour GICD_ICENABLERn */
5.3 Niveaux de priorité
Le GIC peut théoriquement gérer jusqu'à 256 niveaux de priorité, la valeur la plus faible étant la plus prioritaire. Sur l'i.MX6UL, seulement 32 niveaux sont utilisés. Le registre GICC_PMR (Priority Mask Register) détermine le seuil à partir duquel une interruption est transmise au cœur. Avec 32 niveaux, les trois bits de poids faible sont fixes, donc GICC_PMR = 0xF8.
5.4 Priorité de préemption et sous-priorité
Le registre GICC_BPR (Binary Point Register) partitionne les 8 bits de priorité en une partie préemption et une partie sous-priorité. Si 5 bits de priorité sont disponibles, un point binaire à 2 signifie que les 5 bits servent tous à la préemption.
5.5 Programmation de la priorité d'une source
Chaque interruption dispose d'un octet de priorité dans la banque GICD_IPRIORITYR. Pour un hwirq m :
n = m / 4;
octet = m % 4;
offset = 0x400 + 4 * n;
/* Avec 32 niveaux, la priorité réelle s'écrit sur les bits 7:3 */
priorite = niveau << 3;
gic->dist_ipriority[m] = priorite;
- Machine à états d'une interruption
Chaque interruption évolue selon quatre états :
- Inactive : l'interruption n'est pas déclenchée.
- Pending : l'interruption est déclenchée mais n'a pas encore été traitée par le cœur.
- Active : l'interruption est en cours de traitement.
- Active et pending : l'interruption est en cours de traitement et une nouvelle requête du même périphérique est arrivée entre-temps.
- Traitement d'une interruption par le GIC
Lorsqu'une interruption est prise en charge par un cœur :
-
L'interface processeur place le numéro dans
GICC_IARet passe l'interruption à l'état active. -
Le noyau lit ce numéro pour déterminer le gestionnaire à exécuter.
-
À la fin du traitement, le noyau écrit la même valeur dans
GICC_EOIR(End Of Interrupt Register), ce qui ramène l'interruption à l'état inactive ou pending si une nouvelle demande est en attente. -
Exemple de code de démarrage en assembleur
Le fragment suivant illustre un vecteur de démarrage minimal et un gestionnaire IRQ. Les noms de labels et les commentaires ont été adaptés, mais le comportement reste identique.
.global _demarrage
_demarrage:
ldr pc, =_reset_handler
ldr pc, =_undefined_handler
ldr pc, =_svc_handler
ldr pc, =_prefetch_handler
ldr pc, =_data_abort_handler
ldr pc, =_unused_handler
ldr pc, =_irq_handler
ldr pc, =_fiq_handler
_reset_handler:
cpsid i /* masquer les IRQ */
/* Désactiver caches et MMU */
mrc p15, 0, r0, c1, c0, 0
bic r0, r0, #(1 << 12) /* I-Cache */
bic r0, r0, #(1 << 2) /* D-Cache */
bic r0, r0, #(1 << 11) /* prédiction de branchement */
bic r0, r0, #0x3 /* alignement + MMU */
mcr p15, 0, r0, c1, c0, 0
/* Initialiser les pointeurs de pile pour chaque mode */
mrs r0, cpsr
bic r0, r0, #0x1f
orr r0, r0, #0x12 /* mode IRQ */
msr cpsr, r0
ldr sp, =0x80600000
mrs r0, cpsr
bic r0, r0, #0x1f
orr r0, r0, #0x1f /* mode SYS */
msr cpsr, r0
ldr sp, =0x80400000
mrs r0, cpsr
bic r0, r0, #0x1f
orr r0, r0, #0x13 /* mode SVC */
msr cpsr, r0
ldr sp, =0x80200000
cpsie i /* autoriser les IRQ */
b main
_irq_handler:
push {lr}
push {r0-r3, r12}
mrs r0, spsr
push {r0}
mrc p15, 4, r4, c15, c0, 0 /* lire l'adresse de base du GIC */
add r4, r4, #0x2000 /* interface processeur */
ldr r5, [r4, #0x0C] /* GICC_IAR : numéro de l'interruption */
push {r4, r5}
cps #0x13 /* passer en mode SVC */
push {lr}
ldr r6, =traitant_irq_c
mov r0, r5 /* passer le numéro d'interruption */
blx r6
pop {lr}
cps #0x12 /* retour en mode IRQ */
pop {r4, r5}
str r5, [r4, #0x10] /* GICC_EOIR */
pop {r0}
msr spsr_cxsf, r0
pop {r0-r3, r12}
pop {lr}
subs pc, lr, #4
_undefined_handler:
_ svc_handler:
_prefetch_handler:
_data_abort_handler:
_unused_handler:
_fiq_handler:
b .
- Flux logiciel dans le noyau Linux
9.1 Premier niveau de contrôleur
Le GIC est le contrôleur racine. Les numéros matériels 16..1019 sont appelés hwirq. Lorsqu'un périphérique tel qu'une UART déclenche l'interruption physique 32, le noyau effectue les opérations suivantes :
- Le GIC signale l'interruption physique 32.
- Le domaine
irq_domaindu GIC effectue la correspondance vers un numéro virtuel Linux, par exemple 16. - Le noyau invoque
irq_desc[16].handle_irq(), qui parcourt la listeirqactionet exécute le gestionnaire enregistré parrequest_irq(16, ...).
9.2 Contrôleur en cascade
Lorsqu'un GPIO regroupe plusieurs sources d'interruption et est lui-même connecté à une ligne unique du GIC, il se comporte comme un second contrôleur. Supposons qu'un port GPIO possède quatre lignes connectées à l'interruption physique 33 du GIC :
- Le GIC déclenche l'interruption 33.
- Le domaine du GIC la traduit en numéro virtuel 16, puis appelle
irq_desc[16].handle_irq(). - Ce gestionnaire lit le registre du GPIO et découvre que la ligne 2 est active.
- Le domaine du GPIO convertit la ligne matérielle 2 en numéro virtuel 102.
- Le noyau appelle
irq_desc[102].handle_irq(), qui exécute le gestionnaire utilisateur.
9.3 Initialisation du pilote GIC
Lors du démarrage, la chaîne d'appel est :
start_kernel
init_IRQ
irqchip_init
of_irq_init
Plusieurs variantes de GIC sont déclarées dans le noyau :
IRQCHIP_DECLARE(gic_400, "arm,gic-400", gic_of_init);
IRQCHIP_DECLARE(cortex_a9_gic, "arm,cortex-a9-gic", gic_of_init);
IRQCHIP_DECLARE(cortex_a15_gic, "arm,cortex-a15-gic", gic_of_init);
IRQCHIP_DECLARE(cortex_a7_gic, "arm,cortex-a7-gic", gic_of_init);
La macro IRQCHIP_DECLARE place ces entrées dans la section __irqchip_of_table. Le noyau parcourt cette table pour trouver le pilote compatible avec le nœud interrupt-controller de l'arborescence de périphériques. La fonction gic_of_init initialise ensuite les registres du distributeur, les registres de l'interface CPU, puis appelle set_handle_irq(gic_handle_irq) pour enregistrer le point d'entrée général des interruptions.
9.4 Point d'entrée gic_handle_irq
Le gestionnaire d'interruption générique lit GICC_IAR, détermine la catégorie de l'interruption, puis achemine le traitement :
static void __exception_irq_entry gic_traiter_irq(struct pt_regs *regs)
{
u32 numero;
do {
numero = gic_lire_iar();
if ((numero > 15 && numero < 1020) || numero >= 8192) {
gic_ecrire_eoir(numero);
handle_domain_irq(domaine_gic, numero, regs);
continue;
}
if (numero < 16) {
gic_ecrire_eoir(numero);
#ifdef CONFIG_SMP
gerer_IPI(numero, regs);
#endif
continue;
}
} while (numero != IRQ_SPURIEUX);
}
La fonction handle_domain_irq convertit le hwirq en numéro virtuel Linux, repère le descripteur correspondant et exécute generic_handle_irq_desc(). Pour les SPI, le gestionnaire est généralement handle_fasteoi_irq(), qui parcourt la chaîne irqaction et appelle chaque handler enregistré.
- Description dans l'arborescence de périphériques
Le GIC est représenté dans le Device Tree par un nœud interrupt-controller :
intc: interrupt-controller@00a01000 {
compatible = "arm,cortex-a7-gic";
#interrupt-cells = <3>;
interrupt-controller;
reg = <0x00a01000 0x1000>,
<0x00a02000 0x100>;
};
Les trois cellules d'une interruption ont la signification suivante :
- Cellule 0 : type d'interruption (
0pour SPI,1pour PPI). - Cellule 1 : numéro d'interruption (0..987 pour SPI, 0..15 pour PPI).
- Cellule 2 : drapeaux de déclenchement (front montant, front descendant, niveau haut, niveau bas) ; pour les PPI, les bits 15:8 indiquent le masque CPU.
10.1 Exemple avec un GPIO servant de contrôleur secondaire
gpio5: gpio@020ac000 {
compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
reg = <0x020ac000 0x4000>;
interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH>;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
};
Le GPIO5 utilise deux lignes d'interruption SPI : 74 pour les 16 premières broches, 75 pour les 16 suivantes.
10.2 Utilisation côté périphérique
fxls8471@1e {
compatible = "fsl,fxls8471";
reg = <0x1e>;
interrupt-parent = <&gpio5>;
interrupts = <0 IRQ_TYPE_LEVEL_LOW>;
};
Ici, le périphérique utilise la broche GPIO5_IO00 avec un déclenchement sur niveau bas.
- Pilote d'exemple : interruption sur bouton poussoir
11.1 Nœud dans l'arborescence de périphériques
bpoussoir {
compatible = "exemple,bpoussoir";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_bp>;
bouton-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>;
interrupt-parent = <&gpio1>;
interrupts = <18 IRQ_TYPE_EDGE_BOTH>;
status = "okay";
};
11.2 Code de pilote Linux
Le pilote ci-dessous enregistre un gestionnaire sur le bouton, utilise un temporisateur pour le anti-rebond et expose un périphérique caractère.
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/gpio.h>
#include <linux/timer.h>
#include <linux/atomic.h>
#include <linux/interrupt.h>
#include <linux/uaccess.h>
#define NB_DEV_BP 1
#define NOM_DEV_BP "bpoussoir"
#define VAL_BP 0x01
#define VAL_INVAL 0xFF
#define NB_BP 1
struct desc_bouton {
int gpio;
int irq;
u8 valeur;
char nom[16];
irqreturn_t (*traitant)(int, void *);
};
struct periph_bp {
dev_t id;
struct cdev cdev;
struct class *classe;
struct device *periph;
struct device_node *np;
atomic_t valeur;
atomic_t relache;
struct timer_list chrono;
struct desc_bouton boutons[NB_BP];
u8 bouton_actuel;
};
static struct periph_bp periph;
static irqreturn_t traitant_bouton0(int irq, void *ident)
{
struct periph_bp *pdev = ident;
pdev->bouton_actuel = 0;
pdev->chrono.data = (unsigned long)ident;
mod_timer(&pdev->chrono, jiffies + msecs_to_jiffies(10));
return IRQ_HANDLED;
}
static void fonction_chrono(unsigned long arg)
{
u8 etat;
u8 numero;
struct desc_bouton *desc;
struct periph_bp *pdev = (struct periph_bp *)arg;
numero = pdev->bouton_actuel;
desc = &pdev->boutons[numero];
etat = gpio_get_value(desc->gpio);
if (etat == 0) {
atomic_set(&pdev->valeur, desc->valeur);
} else {
atomic_set(&pdev->valeur, 0x80 | desc->valeur);
atomic_set(&pdev->relache, 1);
}
}
static int initialiser_boutons(void)
{
int ret;
unsigned int i;
periph.np = of_find_node_by_path("/bpoussoir");
if (!periph.np)
return -EINVAL;
for (i = 0; i < NB_BP; i++) {
periph.boutons[i].gpio = of_get_named_gpio(periph.np, "bouton-gpio", i);
if (!gpio_is_valid(periph.boutons[i].gpio))
return -EINVAL;
snprintf(periph.boutons[i].nom, sizeof(periph.boutons[i].nom),
"bouton%d", i);
gpio_request(periph.boutons[i].gpio, periph.boutons[i].nom);
gpio_direction_input(periph.boutons[i].gpio);
periph.boutons[i].irq = irq_of_parse_and_map(periph.np, i);
}
periph.boutons[0].traitant = traitant_bouton0;
periph.boutons[0].valeur = VAL_BP;
for (i = 0; i < NB_BP; i++) {
ret = request_irq(periph.boutons[i].irq,
periph.boutons[i].traitant,
IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
periph.boutons[i].nom, &periph);
if (ret)
return ret;
}
setup_timer(&periph.chrono, fonction_chrono, (unsigned long)&periph);
return 0;
}
static int bpoussoir_ouvrir(struct inode *ino, struct file *filp)
{
filp->private_data = &periph;
return 0;
}
static ssize_t bpoussoir_lire(struct file *filp, char __user *buf,
size_t compte, loff_t *ppos)
{
u8 val;
u8 relache;
struct periph_bp *pdev = filp->private_data;
val = atomic_read(&pdev->valeur);
relache = atomic_read(&pdev->relache);
if (!relache)
return -EINVAL;
if (val & 0x80) {
val &= ~0x80;
if (copy_to_user(buf, &val, 1))
return -EFAULT;
}
atomic_set(&pdev->relache, 0);
return 1;
}
static struct file_operations ops_bpoussoir = {
.owner = THIS_MODULE,
.open = bpoussoir_ouvrir,
.read = bpoussoir_lire,
};
static int __init bpoussoir_init(void)
{
alloc_chrdev_region(&periph.id, 0, NB_DEV_BP, NOM_DEV_BP);
cdev_init(&periph.cdev, &ops_bpoussoir);
cdev_add(&periph.cdev, periph.id, NB_DEV_BP);
periph.classe = class_create(THIS_MODULE, NOM_DEV_BP);
periph.periph = device_create(periph.classe, NULL, periph.id,
NULL, NOM_DEV_BP);
atomic_set(&periph.valeur, VAL_INVAL);
atomic_set(&periph.relache, 0);
initialiser_boutons();
return 0;
}
static void __exit bpoussoir_exit(void)
{
unsigned int i;
del_timer_sync(&periph.chrono);
for (i = 0; i < NB_BP; i++) {
free_irq(periph.boutons[i].irq, &periph);
gpio_free(periph.boutons[i].gpio);
}
device_destroy(periph.classe, periph.id);
class_destroy(periph.classe);
cdev_del(&periph.cdev);
unregister_chrdev_region(periph.id, NB_DEV_BP);
}
module_init(bpoussoir_init);
module_exit(bpoussoir_exit);
MODULE_LICENSE("GPL");
Le gestionnaire d'interruption déclenche un temporisateur de 10 ms pour filtrer les rebonds mécaniques. Une fois le délai écoulé, le temporisateur lit l'état du GPIO et stocke la valeur. Le drapeau relache garantit qu'une seule valeur est remontée à l'application par cycle appui-relâchement.
- Outils d'observation
Les correspondances entre interruptions matérielles et numéros virtuels sont visibles via /proc/interrupts. Les gestionnaires exécutés dans un thread noyau apparaissent comme des tâches temps réel SCHED_FIFO et peuvent être listés par :
cat /proc/interrupts
ps -A | grep "irq/"