Продолжаем рассматривать настройку прерываний от внешних устройств в системе x86.
В части 1 (Эволюция контроллеров прерываний) мы рассмотрели теоретические основы контроллеров прерываний и общие термины, в части 2 (Опции загрузки ядра Linux) посмотрели как на практике ОС осуществляет выбор между контроллерами. В этой части мы рассмотрим как BIOS настраивает роутинг IRQ на контроллеры прерываний в чипсете.
Никакие современные компании по разработке BIOS (AwardBIOS/AMIBIOS/Insyde) не раскрывают исходники своих программ. Но к счастью есть Coreboot — проект по замене проприетарного BIOS на свободное программное обеспечение. В его коде мы и посмотрим, как настраивается роутинг прерываний в чипсете.
Теория
Для начала освежим и дополним свои теоретические знания. В части 1 мы обозначили общий путь прерывания для случая PIC и APIC.
PIC:
APIC:
На данных рисунках маппинг PCI device → PIR изображён абстрактно, на самом деле он несколько сложнее. В реальности каждый PCI device имеет 4 линии прерываний (INTA#, INTB#, INTC#, INTD#). У каждого PCI устройства (device) может быть до 8 функций (functions) и вот каждой функции соответствует уже одно прерывание INTx#. Какую именно линию INTx# будет дёргать каждая функция устройства либо фиксировано в железе, либо определяется конфигурацией устройства.
По сути функции это отдельные логические блоки. Например в одном PCI устройстве может быть функция Smbus controller, функция SATA controller, функция LPC bridge. Со стороны ОС каждая функция — это как отдельное устройство со своим конфигурационным пространством PCI Config.
В самом простейшем (и самом распространённом) случае в PCI устройстве всего одна функция, прерывание от которой идёт по линии INTA#. Но в целом в устройстве может быть даже больше 4 функций (как мы уже говорили до 8), тогда какие-то из них придётся сажать на одну линию INTx# (прерывания PCI могут делить линию). Также для PCI устройств входящих в состав чипсета записью в специальные регистры часто возможно указать какие функции используют какие линии INTx# (и используют ли вообще).
Систематизируя наши знания, обозначим путь (роутинг) прерываний от какой-либо PCI функции через INTx#→PIRQy→IRQz, где:
- INTx# — линия INT# (INTA#, INTB#, INTC#, INTD#) PCI устройства, которую будет использовать функция
- PIRQy — линия PIRQ (PIRQA, PIRQB, ...) от PIR, к которой подсоединена линия INTx#
- IRQz — линия IRQ (0, 1, 2, ...) на контроллере прерываний (APIC/PIC), которая соединена с линией PIRQy
Почему нельзя просто соединить везде INTA#→PIRQA, INTB#→PIRQB,… ?
Зачем вообще уделять внимание настройке роутинга? Допустим мы решили не заморачиваться и завести все линии прерываний от всех PCI устройств на одинаковые линии PIRQ. Скажем так:
- INTA#→PIRQA
- INTB#→PIRQB
- INTC#→PIRQC
- INTD#→PIRQD
Как мы уже сказали выше, наиболее распространённый случай, это когда у PCI устройства есть одна функция, и её прерывание заведено на линию INTA# (потому что зачем разработчику устройства заводить её иначе?). Так что если мы вдруг решим завести все линии так как мы написали, то почти все прерывания от устройств будут разделяться на линии PIRQA. Допустим она заведена на IRQ16. Тогда каждый раз, когда процессору сообщается, что возникло прерывание по линии IRQ16, ему придётся опросить драйверы всех устройств подключённых к линии IRQ16 (PIRQA), нет ли у них прерывания для него. Если таких устройств много, это естественно не ускорит отклик системы на прерывание. А линии PIRQB-PIRQD в этом случае будут по большей части простаивать. Для наглядности рисунок иллюстрирующий проблему:
А ведь всё могло было быть сделано так:
Картинка получилась немного запутанной, но смысл в том, что мы тут просто соединяем линии INTx# с PIRQy по round-robin (PIRQA, PIRQB, PIRQC, PIRQD, PIRQA, PIRQB, PIRQC, PIRQD, PIRQA, PIRQB, PIRQC, PIRQD, ...)
Следует заметить, что тут надо учитывать не только то, чтобы на каждую линию PIRQ было нагружено одинаковое количество PCI функций. Ведь какие-то функции могут создавать прерывания очень редко, а какие-то постоянно (контроллер Ethernet например). В этом случае даже выделение отдельной линии PIRQ для прерываний с такой функции может быть вполне оправдано.
Исходя из всего вышесказанного перед разработчиком BIOS помимо всего прочего стоит задача сделать так, чтобы линии PIRQ были равномерно нагружены прерываниями.
Что вообще должен сделать BIOS?
Систематизируем на рисунке:
- 1) Указать какую линию INTx# дёргает каждая из функций PCI устройств
Для внешних PCI устройств этот пункт не производится, но для функций PCI устройств входящих в состав чипсета вполне может быть. - 2) Настроить маппинг INTx#→PIRQy для каждого из PCI устройств
Стоить заметить что сигналов PIRQy может быть больше стандартных четырёх (PIRQA, PIRQB, PIRQC, PIRQD). Например 8: PIRQA-PIRQH.
Сигналы PIRQy идут на линии IRQz выбранного контроллера прерываний (APIC/PIC). Так как мы хотим поддерживать все возможные способы загрузки (см. часть 2) надо заполнить оба маппинга:
- 3a) Заполнить маппинг PIRQy→IRQz1 для связи PIR→I/O APIC
Но обычно этого делать не нужно, так как линии PIRQy фиксированы на линии APIC. Распространённое решение PIRQA→IRQ16, PIRQB→IRQ17,… Самое простое решение, т.к. ставя линии PIRQy на линии контроллера ≥16 можно не волноваться о конфликтах с неразделяемыми прерываниями от ISA устройств. - 3b) Заполнить маппинг PIRQy→IRQz2 для связи PIR→PIC
Это необходимо предоставить на случай если мы будем использовать роутинг через PIC контроллер. Тут нет такого однозначного решения как в случае APIC, ведь в случае с PIC следует помнить о возможности конфликтов с неразделяемыми прерываниями от ISA устройств.
Последний четвёртый пункт необходим для помощи ОС в определении роутинга прерываний. Самим устройством эти регистры обычно не используются.
- 4) Заполнить регистры Interrupt Line/Interrupt Pin для каждой PCI функции
Вообще регистр Interrupt Pin заполняется автоматически и обычно является Read-Only, так что заполнить скорее всего нужно будет заполнить только Interrupt Line регистр. Это необходимо предоставить на случай если мы будем использовать роутинг через PIC контроллер не предоставив ОС никакой таблицы о роутинге прерываний (см. опять же часть 2). Если таблицы предоставлены и данный маппинг согласуется с таблицами роутинга ($PIR/ACPI), то ОС часто и оставляет его.
Следует отметить, что мы пока не касаемся таблиц $PIR/MPtable/ACPI и рассматриваем как нужно сконфигурировать регистры чипсета в плане роутинга прерываний перед передачей управления загрузчику системы. Таблицы прерываний это тема отдельной статьи (возможно будущей).
Итак, теоретические основы изучены, приступаем наконец к практике!
Практика
В качестве примера для статей данной серии я использую кастомизированную плату с процессором Intel Haswell i7 и чипсетом LynxPoint-LP. На данной плате я запустил coreboot в связке с SeaBIOS. Coreboot обеспечивает аппаратно-зависимую инициализацию, а нагрузка (payload) SeaBIOS для него предоставляет интерфейс BIOS для операционных систем. В данной статье я не буду описывать процесс конфигурации coreboot, а лишь постараюсь показать на примере, какого рода настройки BIOS должен производить в чипсете для роутинга прерываний IRQ от внешних устройств.
Так как проект coreboot активно развивается, чтобы статья всегда была актуальна, будем рассматривать код на примере последней фиксированной версии 4.9 (релиз 2018-12-20).
Наиболее близкая материнская плата к моей — это Google Beltino с вариацией Panther. Главной папкой для данной материнской платы является папка «srcmainboardgooglebeltino». Тут сосредоточены все настройки и код специфичный для данной платы.
Итак, начнём разбирать, где происходит настройка вышеупомянутых пунктов:
1) Указать какую линию INTx# дёргает каждая из функций PCI устройств
Данная информация определяется в файле «src/mainboard/google/beltino/romstage.c» в структуре rcba_config через регистры DxxIP (Device xx Interrupt Pin Register (IP)). Данный регистр показывает на какой пин INTx# (A/B/C/D) каждая из функций устройства выводит прерывание.
Возможные варианты (см. файл «src/southbridge/intel/lynxpoint/pch.h»):
0h = No interrupt
1h = INTA#
2h = INTB#
3h = INTC#
4h = INTD#
Допускается, что несколько функций используют один и тот же пин.
Допускается, что функции могут не использовать пин для прерываний (No interrupt).
Всё как мы видели на рисунке в начале статьи.
Полный код отвечающий за обозначенный нами пункт:
/* Device interrupt pin register (board specific) */
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)),
Для более лучшего понимания рассмотрим несколько примеров:
Пример 1:
В устройстве 0x1d (29 в десятичной системе) одна функция (EHCI controller).
В этом случае назначаем прерывание на INTA#.
00:1d.0 — INTA#
RCBA_SET_REG_32(D29IP, (INTA << D29IP_E1P)),
Пример 2:
В устройстве 0x1f (31 в десятичной системе) есть функции Thermal Sensor controller (00:1f.6), SATA controller 2 (00:1f.2), SMBus controller (00:1f.3), SATA controller 1 (00:1f.2). Мы хотим использовать только SMBus controller, SATA controller 1 и Thermal Sensor controller.
00:1f.2 — INTA# (SATA controller 1)
00:1f.3 — INTB# (SMBus controller)
00:1f.2 — No interrupt (SATA controller 2 не используется)
00:1f.6 — INTC# (Thermal Sensor controller)
Для такой конфигурации следует написать:
RCBA_SET_REG_32(D31IP, (INTC << D31IP_TTIP) | (NOINT << D31IP_SIP2) | (INTB << D31IP_SMIP) | (INTA << D31IP_SIP)),
Пример 3:
В одном Device количество нужных нам functions больше 4. В устройстве 0x1c каждая функция отвечает за порт PCI Express. Чтобы работали порты 0-5, и прерывания были распределены между линиями равномерно, можно произвести такую настройку:
00:1c.0 — INTA# (PCI Express Port 0)
00.1c.1 — INTB# (PCI Express Port 1)
00.1c.2 — INTC# (PCI Express Port 2)
00.1c.3 — INTD# (PCI Express Port 3)
00.1c.4 — INTA# (PCI Express Port 4)
00.1c.5 — INTB# (PCI Express Port 5)
00.1c.6 — No interrupt (порт не используется)
00.1c.7 — No interrupt (порт не используется)
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) Настроить маппинг INTx#→PIRQy для каждого из PCI устройств
Данная информация также определяется в файле «srcmainboardgooglebeltinoromstage.c»
в структуре rcba_config, но уже через регистры DxxIR (Device xx Interrupt Route Register).
Информация в данном регистре показывает к какой линии PIRQx (A/B/C/D/E/F/G/H) присоединена каждая из линий прерываний INTx#.
/* Device interrupt route registers */
RCBA_SET_REG_32(D31IR, DIR_ROUTE(PIRQG, PIRQC, PIRQB, PIRQA)),/* LPC */
RCBA_SET_REG_32(D29IR, DIR_ROUTE(PIRQD, PIRQD, PIRQD, PIRQD)),/* EHCI */
RCBA_SET_REG_32(D28IR, DIR_ROUTE(PIRQA, PIRQB, PIRQC, PIRQD)),/* PCIE */
RCBA_SET_REG_32(D27IR, DIR_ROUTE(PIRQG, PIRQG, PIRQG, PIRQG)),/* HDA */
RCBA_SET_REG_32(D22IR, DIR_ROUTE(PIRQA, PIRQA, PIRQA, PIRQA)),/* ME */
RCBA_SET_REG_32(D21IR, DIR_ROUTE(PIRQE, PIRQF, PIRQF, PIRQF)),/* SIO */
RCBA_SET_REG_32(D20IR, DIR_ROUTE(PIRQC, PIRQC, PIRQC, PIRQC)),/* XHCI */
RCBA_SET_REG_32(D23IR, DIR_ROUTE(PIRQH, PIRQH, PIRQH, PIRQH)),/* SDIO */
Пример 1:
Устройство 0x1c (28 в десятичной системе) — это порты PCIe как мы уже выяснили.
Производим «прямое» соединение:
- INTA#→PIRQA
- INTB#→PIRQB
- INTC#→PIRQC
- INTD#→PIRQD
RCBA_SET_REG_32(D28IR, DIR_ROUTE(PIRQA, PIRQB, PIRQC, PIRQD))
Пример 2:
Устройство 0x1d (29 в десятичной системе) — одна функция (EHCI controller) на INTA#, остальные линии не используются.
Соединяем линию INTA# с PIRQD:
RCBA_SET_REG_32(D29IR, DIR_ROUTE(PIRQD, PIRQD, PIRQD, PIRQD))
В данном случае имеет смысл только первая запись PIRQD (для INTA#), остальные не имеют смысла.
3a) Заполнить маппинг PIRQy→IRQz1 (PIR→APIC)
Как мы уже сказали, тут маппинг часто фиксирован, и этот случай не исключение.
- PIRQA→IRQ16
- PIRQB→IRQ17
- ...
- PIRQH→IRQ23
3b) Заполнить маппинг PIRQy→IRQz2 (PIR→PIC)
В coreboot содержимое для заполнения этих регистров определяется в файле devicetree.cb в папке материнской платы «srcmainboardgooglebeltino».
devicetree.cb (название devicetree для связи с аналогичной концепцией в ядре Linux, а «cb» — сокращение от coreboot) — это специальный файл, в котором отражается конфигурация данной материнской платы: какие процессор, чипсет используются, какие устройства на них включены, какие выключены и т.д. Помимо этого в данном файле может задаваться и специальная информация для конфигурации чипсета. Это как раз нужный нам случай:
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"
Эти строки и задают маппинг PIRQy→IRQz2. В коде после парсинга файла devicetree.cb они трансформируются в переменные «config->pirqX_routing».
Переменная «config->pirqa_routing = 0x8b» будет означать, что PIRQA соединён с линией прерываний IRQ11 (0x0b = 11) контроллера PIC, однако страший бит (который 0x80) означает, что роутинг прерывания не производится. Честно говоря по моему опыту это ошибка, по умолчанию стоит включать роутинг на PIC, операционная система сама сможет осуществить переключение на I/O APIC выставив данный бит в 1 если нужно.
То есть в этом случае правильней было бы писать:
register "pirqa_routing" = "0x0b"
register "pirqb_routing" = "0x0a"
register "pirqc_routing" = "0x0b"
register "pirqd_routing" = "0x0b"
register "pirqe_routing" = "0x80" # not used
register "pirqf_routing" = "0x80" # not used
register "pirqg_routing" = "0x80" # not used
register "pirqh_routing" = "0x80" # not used
Последние 4 прерывания мы не включили, т.к. прерывание IRQ0 используется всегда под системный таймер и явно недоступно (см. General IBM-PC Compatible Interrupt Information).
Но если мы посмотрим внимательней на пункт 2), мы увидим, что некоторые PCI device используют линии PIRQE-PIRQH, поэтому оставлять их неподключенными верный путь к неработающим устройствам.
Так что лучше написать что-нибудь такое:
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"
Фактическое заполнение соответствующих регистров происходит в файле srcsouthbridgeintellynxpointlpc.c в фунции pch_pirq_init.
Фрагмент кода, отвечающий за заполнение регистров:
/* Get the chip configuration */
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);
Константы адресов регистров описаны в том же файле 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
Маппинг PIRQy→IRQz2 для данного чипсета записывается в PCI устройство LPC (адрес 00:1f.0) в регистры PIRQy_ROUT. Следует учесть, что часто для использования разрешены не все 15 линий IRQz2 на PIC, а только часть (к примеру 3,4,5,6,7,9,10,11,12,14,15). В описании на данные регистры должна быть информация о том, какие IRQ доступны для назначения на них прерываний от линий PIRQ. Так что предложенный нами выше маппинг возможен только если назначение PIRQ на линии IRQ3, IRQ4, IRQ5, IRQ6, IRQ10, IRQ11, IRQ14, IRQ15 доступно. Но если мы внимательно посмотрим на комментарий перед функцией pch_pirq_init, то увидим, что так и есть:
/* PIRQ[n]_ROUT[3:0] - PIRQ Routing Control
* 0x00 - 0000 = Reserved
* 0x01 - 0001 = Reserved
* 0x02 - 0010 = Reserved
* 0x03 - 0011 = IRQ3
* 0x04 - 0100 = IRQ4
* 0x05 - 0101 = IRQ5
* 0x06 - 0110 = IRQ6
* 0x07 - 0111 = IRQ7
* 0x08 - 1000 = Reserved
* 0x09 - 1001 = IRQ9
* 0x0A - 1010 = IRQ10
* 0x0B - 1011 = IRQ11
* 0x0C - 1100 = IRQ12
* 0x0D - 1101 = Reserved
* 0x0E - 1110 = IRQ14
* 0x0F - 1111 = IRQ15
* PIRQ[n]_ROUT[7] - PIRQ Routing Control
* 0x80 - The PIRQ is not routed.
*/
4) Заполнить регистры Interrupt Line/Interrupt Pin для каждой PCI функции
В конфигурационном пространстве PCI (есть у каждой PCI функции по стандарту) есть 2 интересующих нас регистра:
- 3Ch: Interrupt Line — cюда необходимо записать номер IRQz2 (число от 0 до 15), номер прерывания, которое в итоге дёргает функция при использовании PIC контроллера
- 3Dh: Interrupt Pin — показывает какую линию INTx# (A/B/C/D) использует функция
Начнём с последнего. Регистр Interrupt Pin заполнится автоматически исходя из настроек чипсета (регистров DxxIP), сделанных нами в пункте 1 и будет Read-Only.
Так что остаётся только заполнить регистр Interrupt Line прерыванием IRQz2 для каждой PCI функции.
Зная маппинг PIRQy→IRQz2 (пункт 3b), и маппинг INTx#→PIRQy (пункт 2) можно легко заполнить регистр Interrupt Line для каждой функции, зная какое прерывание INTx# она использует (пункт 1).
В coreboot регистры Interrupt Line заполняются также в файле srcsouthbridgeintellynxpointlpc.c в фунции pch_pirq_init:
/* Eric Biederman once said we should let the OS do this.
* I am not so sure anymore he was right.
*/
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: /* INTA# */ int_line = config->pirqa_routing; break;
case 2: /* INTB# */ int_line = config->pirqb_routing; break;
case 3: /* INTC# */ int_line = config->pirqc_routing; break;
case 4: /* INTD# */ int_line = config->pirqd_routing; break;
}
if (!int_line)
continue;
pci_write_config8(irq_dev, PCI_INTERRUPT_LINE, int_line);
}
В данном коде почему-то подразумевается, что маппинг в любом случае INTA#→PIRQA, INTB#→PIRQB, INTC#→PIRQC, INTD#→PIRQD. Хотя на деле мы видели, что он может быть и другим (см пункт 2).
Вообщем «Eric Biederman once said», а мы скопипастили куда попало:
$ 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.
В целом в coreboot не слишком заботятся о поддержке legacy режимов прерываний. Так что сильно удивляться подобной ошибке не стоит. При загрузке современной ОС вам это не помешает, но если вдруг потребуется загрузить Linux с опциями «acpi=off nolapic», то это вряд ли получится сделать.
Заключение
В заключении повторим типичную информацию, которую надо сконфигурировать в чипсете для роутинга прерываний PCI:
- Указать какую линию INTx# дёргает каждая из функций PCI устройств
- Настроить маппинг INTx#→PIRQy для каждого из PCI устройств
- Заполнить маппинг PIRQy→IRQz1 (PIR→APIC) и маппинг PIRQy→IRQz2 (PIR→PIC)
- Заполнить регистры Interrupt Line/Interrupt Pin конфигурационного пространства PCI для каждой PCI функции.
Автор: Константин Аладышев