Analyse de l'architecture GICv2 ARMv7 pour la gestion des interruptions

  1. 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.
  1. 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;
  1. 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.
  1. 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;
  1. 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;
  1. 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.
  1. Traitement d'une interruption par le GIC

Lorsqu'une interruption est prise en charge par un cœur :

  1. L'interface processeur place le numéro dans GICC_IAR et passe l'interruption à l'état active.

  2. Le noyau lit ce numéro pour déterminer le gestionnaire à exécuter.

  3. À 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.

  4. 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 .
  1. 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 :

  1. Le GIC signale l'interruption physique 32.
  2. Le domaine irq_domain du GIC effectue la correspondance vers un numéro virtuel Linux, par exemple 16.
  3. Le noyau invoque irq_desc[16].handle_irq(), qui parcourt la liste irqaction et exécute le gestionnaire enregistré par request_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 :

  1. Le GIC déclenche l'interruption 33.
  2. Le domaine du GIC la traduit en numéro virtuel 16, puis appelle irq_desc[16].handle_irq().
  3. Ce gestionnaire lit le registre du GPIO et découvre que la ligne 2 est active.
  4. Le domaine du GPIO convertit la ligne matérielle 2 en numéro virtuel 102.
  5. 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é.

  1. 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 (0 pour SPI, 1 pour 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.

  1. 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.

  1. 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/"

Étiquettes: GICv2 ARM Cortex-A7 IRQ Linux Device Tree Embedded Linux

Publié le 19 juin à 01h28