Nous continuons à envisager de configurer des interruptions à partir de périphériques externes dans le système x86.
Dans la partie 1 (
Evolution des contrôleurs d'interruption ), nous avons examiné les fondements théoriques des contrôleurs d'interruption et les termes généraux, dans la partie 2 (
options de démarrage du noyau Linux ), nous avons examiné comment le système d'exploitation fait un choix entre les contrôleurs dans la pratique. Dans cette partie, nous verrons comment le BIOS configure le routage IRQ pour les contrôleurs d'interruption du chipset.
Aucune entreprise de développement de BIOS moderne (AwardBIOS / AMIBIOS / Insyde) ne divulgue le code source de leurs programmes. Mais heureusement, il y a
Coreboot , un projet pour remplacer le BIOS propriétaire par un logiciel gratuit. Dans son code, nous verrons comment le routage d'interruption dans le chipset est configuré.

Théorie
Tout d'abord, rafraîchissez et complétez nos connaissances théoriques. Dans la
partie 1, nous avons identifié un chemin d'interruption commun pour le cas de PIC et APIC.
Pic:
APIC:
Sur ces figures, le mappage périphérique PCI → PIR est illustré de manière abstraite; en fait, c'est un peu plus compliqué. En réalité, chaque périphérique PCI possède 4 lignes d'interruption (INTA #, INTB #, INTC #, INTD #). Chaque périphérique PCI peut avoir jusqu'à 8 fonctions et chaque fonction a déjà une interruption INTx #. La ligne de INTx # que chaque fonction du périphérique va tirer est soit fixée dans le matériel, soit déterminée par la configuration du périphérique.

En substance, les fonctions sont des blocs logiques séparés. Par exemple, dans un périphérique PCI, il peut y avoir une fonction de contrôleur Smbus, une fonction de contrôleur SATA, une fonction de pont LPC. Côté OS, chaque fonction est un périphérique distinct avec son propre espace de configuration PCI Config.
Dans le cas le plus simple (et le plus courant) d'un périphérique PCI, il n'y a qu'une seule fonction, dont l'interruption se fait via la ligne INTA #. Mais en général, le périphérique peut même avoir plus de 4 fonctions (comme nous l'avons dit précédemment 8), alors certaines d'entre elles devront être plantées sur une ligne INTx # (les interruptions PCI peuvent partager la ligne). De plus, pour les périphériques PCI inclus dans le chipset en écrivant dans des registres spéciaux, il est souvent possible d'indiquer quelles fonctions utilisent quelles lignes INTx # (et si elles sont utilisées du tout).
Systématisant nos connaissances, nous désignons le chemin (routage) des interruptions de toute fonction PCI via INTx # → PIRQy → IRQz, où:
- INTx # - ligne INT # (INTA #, INTB #, INTC #, INTD #) du périphérique PCI que la fonction utilisera
- PIRQy - la ligne PIRQ (PIRQA, PIRQB, ...) du PIR auquel la ligne INTx # est connectée
- IRQz - Ligne IRQ (0, 1, 2, ...) sur le contrôleur d'interruption (APIC / PIC), qui est connecté à la ligne PIRQy
Pourquoi ne pouvez-vous pas simplement vous connecter partout INTA # → PIRQA, INTB # → PIRQB, ...?
Pourquoi prendre la peine de configurer le routage? Supposons que nous décidions de ne pas déranger et d'obtenir toutes les lignes d'interruption de tous les périphériques PCI vers les mêmes lignes PIRQ. Disons ceci:
- INTA # → PIRQA
- INTB # → PIRQB
- INTC # → PIRQC
- INTD # → PIRQD
Comme nous l'avons dit ci-dessus, le cas le plus courant est lorsqu'un périphérique PCI a une fonction et que son interruption est connectée à la ligne INTA # (car pourquoi le développeur du périphérique devrait-il la démarrer différemment?). Donc, si nous décidons soudainement de démarrer toutes les lignes comme nous l'avons écrit, alors presque toutes les interruptions des appareils seront divisées en lignes PIRQA. Disons qu'elle s'est retrouvée sur IRQ16. Ensuite, chaque fois que le processeur est informé qu'une interruption s'est produite sur la ligne IRQ16, il devra interroger les pilotes de tous les périphériques connectés à la ligne IRQ16 (PIRQA) s'ils ont une interruption. S'il existe de nombreux appareils de ce type, cela n'accélérera naturellement pas la réponse du système à une interruption. Et les lignes PIRQB-PIRQD dans ce cas seront pour la plupart inactives. Pour plus de clarté, la figure illustrant le problème:

Mais tout pourrait ĂŞtre fait comme ceci:

L'image est un peu déroutante, mais le fait est que nous connectons simplement les lignes INTx # avec PIRQy au round robin (PIRQA, PIRQB, PIRQC, PIRQD, PIRQA, PIRQB, PIRQC, PIRQD, PIRQA, PIRQB, PIRQC, PIRQD ,. ..)
Il est à noter qu'ici il faut tenir compte non seulement du fait que le même nombre de fonctions PCI sont chargées sur chaque ligne PIRQ. Après tout, certaines fonctions peuvent créer des interruptions très rarement, et certaines de façon permanente (contrôleur Ethernet par exemple). Dans ce cas, même l'attribution d'une ligne PIRQ distincte pour les interruptions avec une telle fonction peut être tout à fait justifiée.
Sur la base de ce qui précède, le développeur du BIOS a, entre autres, la tâche de s'assurer que les lignes PIRQ sont uniformément chargées d'interruptions.
Que doit faire le BIOS?
Nous systématisons dans la figure:

- 1) Indiquez quelle ligne de INTx # chaque fonction des périphériques PCI tire
Pour les périphériques PCI externes, cet élément n'est pas effectué, mais pour les fonctions des périphériques PCI inclus dans le chipset, il peut très bien l'être. - 2) Configurer le mappage INTx # → PIRQy pour chaque périphérique PCI
Il convient de noter qu'il peut y avoir plus de quatre signaux PIRQy standard (PIRQA, PIRQB, PIRQC, PIRQD). Par exemple 8: PIRQA-PIRQH.
Les signaux PIRQy vont sur la ligne IRQz du contrôleur d'interruption sélectionné (APIC / PIC). Puisque nous voulons prendre en charge toutes les méthodes de chargement possibles (voir la
partie 2 ), nous devons remplir les deux mappages:
- 3a) Remplissez le mappage PIRQy → IRQz1 pour la communication PIR → I / O APIC
Mais ce n'est généralement pas nécessaire, car les lignes PIRQy sont fixées sur la ligne APIC. La solution courante est PIRQA → IRQ16, PIRQB → IRQ17, ... La solution la plus simple, car En plaçant des lignes PIRQy sur des lignes de contrôleur ≥ 16, vous n'avez pas à vous soucier des conflits avec des interruptions inséparables des appareils ISA. - 3b) Remplissez le mappage PIRQy → IRQz2 pour la communication PIR → PIC
Cela doit être fourni dans le cas où nous utilisons le routage via le contrôleur PIC. Il n'y a pas de solution non ambiguë comme dans le cas de l'APIC, car dans le cas du PIC, il faut être conscient de la possibilité de conflits avec des interruptions inséparables des périphériques ISA.
Le dernier quatrième élément est nécessaire pour aider le système d'exploitation à déterminer le routage d'interruption. Le périphérique lui-même n'utilise généralement pas ces registres.
- 4) Remplissez les registres Interrupt Line / Interrupt Pin pour chaque fonction PCI
En général, le registre des broches d'interruption est automatiquement rempli et est généralement en lecture seule, donc le remplissage ne nécessitera probablement que le remplissage du registre de la ligne d'interruption. Cela doit être fourni dans le cas où nous utilisons le routage via le contrôleur PIC sans fournir au système d'exploitation une table sur les interruptions de routage (voir à nouveau la partie 2 ). Si des tables sont fournies et que ce mappage est cohérent avec les tables de routage ($ PIR / ACPI), le système d'exploitation le quitte souvent.
Il convient de noter que nous ne touchons pas encore aux tables $ PIR / MPtable / ACPI et considérons comment configurer les registres du chipset en termes d'interruptions de routage avant de transférer le contrôle au chargeur du système. Les tables d'interruption sont un sujet pour un article séparé (éventuellement un futur).
Alors, les fondements théoriques sont étudiés, enfin on commence à pratiquer!
Pratique
À titre d'exemple pour les articles de cette série, j'utilise une carte personnalisée avec un processeur Intel Haswell i7 et un chipset LynxPoint-LP. Sur cette carte, j'ai lancé coreboot en collaboration avec SeaBIOS. Coreboot fournit une initialisation spécifique au matériel et la charge utile SeaBIOS fournit une interface BIOS pour les systèmes d'exploitation. Dans cet article, je ne décrirai pas le processus de configuration de coreboot, mais j'essaierai simplement de montrer avec un exemple quel type de paramètres BIOS doivent être définis dans le chipset pour router les interruptions IRQ à partir de périphériques externes.
Étant donné que le projet coreboot se développe activement afin que l'article soit toujours à jour, nous considérerons le code en utilisant l'exemple de la dernière version fixe
4.9 (version 2018-12-20).
La carte mère la plus proche de la mienne est Google Beltino avec la variation Panther. Le dossier principal de cette carte mère est le dossier
"src \ mainboard \ google \ beltino" . Tous les paramètres sont concentrés ici et le code spécifique à cette carte.
Commençons donc à trier où les éléments ci-dessus sont configurés:
1) Indiquez quelle ligne de INTx # chaque fonction des périphériques PCI tire
Ces informations sont définies dans le fichier
«src / mainboard / google / beltino / romstage.c» dans la structure rcba_config via les registres DxxIP (Device xx Interrupt Pin Register (IP)). Ce registre indique quelle broche INTx # (A / B / C / D) chacune des fonctions de l'appareil émet une interruption.
Options possibles (voir le fichier
"src / southbridge / intel / lynxpoint / pch.h" ):
0h = No interrupt 1h = INTA# 2h = INTB# 3h = INTC# 4h = INTD#
On suppose que plusieurs fonctions utilisent la mĂŞme broche.
Il est supposé que les fonctions ne peuvent pas utiliser la broche pour les interruptions (pas d'interruption).
Tout comme nous l'avons vu dans la figure au début de l'article.
Le code complet est responsable de l'article désigné par nous:
RCBA_SET_REG_32(D31IP, (INTC << D31IP_TTIP) | (NOINT << D31IP_SIP2) | (INTB << D31IP_SMIP) | (INTA << D31IP_SIP)), RCBA_SET_REG_32(D29IP, (INTA << D29IP_E1P)), RCBA_SET_REG_32(D28IP, (INTA << D28IP_P1IP) | (INTC << D28IP_P3IP) | (INTB << D28IP_P4IP)), RCBA_SET_REG_32(D27IP, (INTA << D27IP_ZIP)), RCBA_SET_REG_32(D26IP, (INTA << D26IP_E2P)), RCBA_SET_REG_32(D22IP, (NOINT << D22IP_MEI1IP)), RCBA_SET_REG_32(D20IP, (INTA << D20IP_XHCI)),
Pour une meilleure compréhension, considérons quelques exemples:
Exemple 1:Le périphérique 0x1d (29 en décimal) a une fonction (contrôleur EHCI).
Dans ce cas, affectez une interruption Ă INTA #.
00: 1d.0 - INTA #
RCBA_SET_REG_32(D29IP, (INTA << D29IP_E1P)),
Exemple 2:Le périphérique 0x1f (31 en décimal) a les fonctions contrôleur de capteur thermique (00: 1f.6), contrôleur SATA 2 (00: 1f.2), contrôleur SMBus (00: 1f.3), contrôleur SATA 1 (00: 1f .2). Nous voulons utiliser uniquement le contrôleur SMBus, le contrôleur SATA 1 et le contrôleur de capteur thermique.
00: 1f.2 - INTA # (contrĂ´leur SATA 1)
00: 1f.3 - INTB # (contrĂ´leur SMBus)
00: 1f.2 - Aucune interruption (le contrôleur SATA 2 n'est pas utilisé)
00: 1f.6 - INTC # (contrĂ´leur de capteur thermique)
Pour cette configuration, vous devez écrire:
RCBA_SET_REG_32(D31IP, (INTC << D31IP_TTIP) | (NOINT << D31IP_SIP2) | (INTB << D31IP_SMIP) | (INTA << D31IP_SIP)),
Exemple 3:Dans un périphérique, le nombre de fonctions dont nous avons besoin est supérieur à 4. Dans le périphérique 0x1c, chaque fonction est responsable du port PCI Express. Pour que les ports 0-5 fonctionnent et que les interruptions soient réparties uniformément entre les lignes, vous pouvez configurer ceci:
00: 1c.0 - INTA # (port PCI Express 0)
00.1c.1 - INTB # (port PCI Express 1)
00.1c.2 - INTC # (port PCI Express 2)
00.1c.3 - INTD # (port PCI Express 3)
00.1c.4 - INTA # (port PCI Express 4)
00.1c.5 - INTB # (port PCI Express 5)
00.1c.6 - Aucune interruption (port non utilisé)
00.1c.7 - Aucune interruption (port non utilisé)
RCBA_SET_REG_32(D28IP, (INTA << D28IP_P1IP) | (INTB << D28IP_P2IP) | (INTC << D28IP_P3IP) | (INTD << D28IP_P4IP) | (INTA << D28IP_P5IP) | (INTB << D28IP_P6IP) | (NOINT << D28IP_P7IP) | (NOINT << D28IP_P8IP)),
2) Configurer le mappage INTx # → PIRQy pour chaque périphérique PCI
Ces informations sont également définies dans le fichier
"src \ mainboard \ google \ beltino \ romstage.c"dans la structure rcba_config, mais déjà via les registres DxxIR (Device xx Interrupt Route Register).
Les informations de ce registre indiquent à quelle ligne PIRQx (A / B / C / D / E / F / G / H) chaque ligne d'interruption INTx # est connectée.
RCBA_SET_REG_32(D31IR, DIR_ROUTE(PIRQG, PIRQC, PIRQB, PIRQA)), RCBA_SET_REG_32(D29IR, DIR_ROUTE(PIRQD, PIRQD, PIRQD, PIRQD)), RCBA_SET_REG_32(D28IR, DIR_ROUTE(PIRQA, PIRQB, PIRQC, PIRQD)), RCBA_SET_REG_32(D27IR, DIR_ROUTE(PIRQG, PIRQG, PIRQG, PIRQG)), RCBA_SET_REG_32(D22IR, DIR_ROUTE(PIRQA, PIRQA, PIRQA, PIRQA)), RCBA_SET_REG_32(D21IR, DIR_ROUTE(PIRQE, PIRQF, PIRQF, PIRQF)), RCBA_SET_REG_32(D20IR, DIR_ROUTE(PIRQC, PIRQC, PIRQC, PIRQC)), RCBA_SET_REG_32(D23IR, DIR_ROUTE(PIRQH, PIRQH, PIRQH, PIRQH)),
Exemple 1:Le périphérique 0x1c (28 dans le système décimal) est le port PCIe comme nous l'avons déjà découvert.
Nous établissons une connexion «directe»:
- INTA # → PIRQA
- INTB # → PIRQB
- INTC # → PIRQC
- INTD # → PIRQD
RCBA_SET_REG_32(D28IR, DIR_ROUTE(PIRQA, PIRQB, PIRQC, PIRQD))
Exemple 2:Périphérique 0x1d (29 en décimal) - une fonction (contrôleur EHCI) sur INTA #, les autres lignes ne sont pas utilisées.
Connectez la ligne INTA # Ă PIRQD:
RCBA_SET_REG_32(D29IR, DIR_ROUTE(PIRQD, PIRQD, PIRQD, PIRQD))
Dans ce cas, seul le premier enregistrement PIRQD (pour INTA #) a du sens, le reste n'a pas de sens.
3a) Remplissez la cartographie PIRQy → IRQz1 (PIR → APIC)
Comme nous l'avons déjà dit, la cartographie est souvent fixée ici, et ce cas ne fait pas exception.
- PIRQA → IRQ16
- PIRQB → IRQ17
- ...
- PIRQH → IRQ23
3b) Remplissez le mapping PIRQy → IRQz2 (PIR → PIC)
Dans coreboot, le contenu pour remplir ces registres est défini dans le fichier
devicetree.cb dans le dossier de la carte mère "src \ mainboard \ google \ beltino \".
devicetree.cb (le nom devicetree pour la communication avec un concept similaire dans le noyau Linux, et «cb» est l'abréviation de coreboot) est un fichier spécial qui reflète la configuration de cette carte mère: quel processeur, chipset sont utilisés, quels périphériques sont inclus, qui éteint etc. De plus, des informations spéciales pour la configuration du chipset peuvent être spécifiées dans ce fichier. C'est juste le cas dont nous avons besoin:
register "pirqa_routing" = "0x8b" register "pirqb_routing" = "0x8a" register "pirqc_routing" = "0x8b" register "pirqd_routing" = "0x8b" register "pirqe_routing" = "0x80" register "pirqf_routing" = "0x80" register "pirqg_routing" = "0x80" register "pirqh_routing" = "0x80"
Ces lignes spécifient le mappage PIRQy → IRQz2. Dans le code, après avoir analysé le fichier devicetree.cb, ils sont transformés en variables «config-> pirqX_routing».
La variable "config-> pirqa_routing = 0x8b" signifie que le PIRQA est connecté à la ligne d'interruption IRIC11 (0x0b = 11) du contrôleur PIC, cependant, le bit le plus élevé (qui est 0x80) signifie que le routage d'interruption n'est pas effectué. Honnêtement, d'après mon expérience, c'est une erreur, par défaut, cela vaut la peine d'activer le routage PIC, le système d'exploitation lui-même pourra passer à I / O APIC en définissant ce bit sur 1 si nécessaire.
Autrement dit, dans ce cas, il serait plus correct d'écrire:
register "pirqa_routing" = "0x0b" register "pirqb_routing" = "0x0a" register "pirqc_routing" = "0x0b" register "pirqd_routing" = "0x0b" register "pirqe_routing" = "0x80"
Nous n'avons pas activé les 4 dernières interruptions, car L'interruption IRQ0 est toujours utilisée sous la minuterie système et est clairement indisponible (voir
Informations générales sur les interruptions compatibles IBM-PC ).
Mais si nous regardons de plus près le point 2), nous verrons que certains périphériques PCI utilisent les lignes PIRQE-PIRQH, donc les laisser non connectés est la bonne voie pour les périphériques cassés.
Il vaut donc mieux écrire quelque chose comme ceci:
register "pirqa_routing" = "0x03" register "pirqb_routing" = "0x04" register "pirqc_routing" = "0x05" register "pirqd_routing" = "0x06" register "pirqe_routing" = "0x0a" register "pirqf_routing" = "0x0b" register "pirqg_routing" = "0x0e" register "pirqh_routing" = "0x0f"
Le remplissage réel des registres correspondants se produit dans le fichier
src \ southbridge \ intel \ lynxpoint \ lpc.c dans la fonction pch_pirq_init.
Extrait de code responsable du remplissage du registre:
config_t *config = dev->chip_info; pci_write_config8(dev, PIRQA_ROUT, config->pirqa_routing); pci_write_config8(dev, PIRQB_ROUT, config->pirqb_routing); pci_write_config8(dev, PIRQC_ROUT, config->pirqc_routing); pci_write_config8(dev, PIRQD_ROUT, config->pirqd_routing); pci_write_config8(dev, PIRQE_ROUT, config->pirqe_routing); pci_write_config8(dev, PIRQF_ROUT, config->pirqf_routing); pci_write_config8(dev, PIRQG_ROUT, config->pirqg_routing); pci_write_config8(dev, PIRQH_ROUT, config->pirqh_routing);
Les constantes d'adresse de registre sont décrites dans le même fichier
pch.h #define PIRQA_ROUT 0x60 #define PIRQB_ROUT 0x61 #define PIRQC_ROUT 0x62 #define PIRQD_ROUT 0x63 #define PIRQE_ROUT 0x68 #define PIRQF_ROUT 0x69 #define PIRQG_ROUT 0x6A #define PIRQH_ROUT 0x6B
Le mappage PIRQy → IRQz2 pour ce chipset est écrit sur le périphérique PCI LPC (adresse 00: 1f.0) dans les registres PIRQy_ROUT. Il convient de noter que souvent, les 15 lignes IRQz2 par PIC ne sont pas autorisées à être utilisées, mais seulement une partie (par exemple, 3,4,5,6,7,9,10,11,12,14,15). La description de ces registres doit contenir des informations sur les IRQ disponibles pour leur affecter des interruptions des lignes PIRQ. Ainsi, la cartographie que nous proposons ci-dessus n'est possible que si l'affectation de PIRQ sur la ligne IRQ3, IRQ4, IRQ5, IRQ6, IRQ10, IRQ11, IRQ14, IRQ15 est disponible. Mais si nous regardons attentivement le commentaire avant la fonction pch_pirq_init, nous verrons que c'est:
4) Remplissez les registres Interrupt Line / Interrupt Pin pour chaque fonction PCI
Dans l'espace de configuration PCI (chaque PCI a des fonctions selon la norme) il y a 2 registres qui nous intéressent:
- 3Ch: Interrupt Line - ici, vous devez écrire le numéro IRQz2 (un nombre de 0 à 15), le numéro d'interruption que la fonction tire finalement lors de l'utilisation du contrôleur PIC
- 3Dh: Pin d'interruption - montre quelle ligne INTx # (A / B / C / D) la fonction utilise
Commençons par le dernier. Le registre des broches d'interruption sera rempli automatiquement en fonction des paramètres du chipset (registres DxxIP) que nous avons définis au paragraphe 1 et sera en lecture seule.
Il ne reste donc plus qu'Ă remplir le registre Interrupt Line avec une interruption IRQz2 pour chaque fonction PCI.
Connaissant le mappage PIRQy → IRQz2 (élément 3b) et le mappage INTx # → PIRQy (élément 2), vous pouvez facilement remplir le registre de ligne d'interruption pour chaque fonction, en sachant quelle interruption INTx # elle utilise (élément 1).
Dans coreboot, les registres de ligne d'interruption sont également remplis dans le
fichier src \ southbridge \ intel \ lynxpoint \ lpc.c dans la fonction pch_pirq_init:
for (irq_dev = all_devices; irq_dev; irq_dev = irq_dev->next) { u8 int_pin=0, int_line=0; if (!irq_dev->enabled || irq_dev->path.type != DEVICE_PATH_PCI) continue; int_pin = pci_read_config8(irq_dev, PCI_INTERRUPT_PIN); switch (int_pin) { case 1: int_line = config->pirqa_routing; break; case 2: int_line = config->pirqb_routing; break; case 3: int_line = config->pirqc_routing; break; case 4: int_line = config->pirqd_routing; break; } if (!int_line) continue; pci_write_config8(irq_dev, PCI_INTERRUPT_LINE, int_line); }
Pour une raison quelconque, ce code implique que le mappage est dans tous les cas INTA # → PIRQA, INTB # → PIRQB, INTC # → PIRQC, INTD # → PIRQD. Bien qu'en pratique, nous avons vu que cela peut être différent (voir paragraphe 2).
Généralement «Eric Biederman a dit une fois», et nous l'avons copié n'importe où:
$ grep "Eric Biederman once said" -r src/ src/southbridge/intel/fsp_bd82x6x/lpc.c: /* Eric Biederman once said we should let the OS do this. src/southbridge/intel/i82801gx/lpc.c: /* Eric Biederman once said we should let the OS do this. src/southbridge/intel/i82801ix/lpc.c: /* Eric Biederman once said we should let the OS do this. src/southbridge/intel/lynxpoint/lpc.c: /* Eric Biederman once said we should let the OS do this. src/southbridge/intel/sch/lpc.c: /* Eric Biederman once said we should let the OS do this.
En général, coreboot ne se soucie pas vraiment de la prise en charge des interruptions héritées. Tellement d'être surpris de cette erreur n'en vaut pas la peine. Lorsque vous chargez un système d'exploitation moderne, cela ne vous dérange pas, mais si vous devez soudainement charger Linux avec les options «acpi = off nolapic», cela n'est guère possible.
Conclusion
En conclusion, nous répéterons les informations typiques qui doivent être configurées dans le chipset pour le routage des interruptions PCI:
- Indiquez quelle ligne INTx # chaque fonction PCI tire
- Configurer le mappage INTx # → PIRQy pour chaque périphérique PCI
- Remplissage mappage PIRQy → IRQz1 (PIR → APIC) et mappage PIRQy → IRQz2 (PIR → PIC)
- Remplissez les registres Interrupt Line / Interrupt Pin de l'espace de configuration PCI pour chaque fonction PCI.