Современная вычислительная платформа — это не просто процессор, память и набор периферийных устройств. За видимой для пользователя работой операционной системы скрывается сложный слой взаимодействия между аппаратной частью, прошивкой и программным обеспечением. Именно в этом слое решаются вопросы обнаружения устройств, управления питанием, обработки аппаратных событий, перехода между состояниями сна и пробуждения, а также множества низкоуровневых сценариев, о которых прикладные программы обычно даже не подозревают.

Одним из ключевых механизмов такого взаимодействия является ACPI (Advanced Configuration and Power Interface). Эта спецификация определяет, каким образом прошивка платформы описывает аппаратную конфигурацию системы, а операционная система получает возможность управлять устройствами, питанием и событиями платформы без необходимости знать все детали конкретной реализации материнской платы или SoC.

Исторически значительная часть этих задач находилась в зоне ответственности BIOS. В модели APM управление питанием фактически оставалось за прошивкой: операционная система могла лишь обращаться к BIOS за выполнением тех или иных операций. Такой подход плохо масштабировался, усложнял поддержку новых устройств и ограничивал возможности ОС. ACPI изменил эту модель: теперь прошивка предоставляет операционной системе набор таблиц, описаний и методов, а сама ОС становится главным управляющим компонентом.

На практике это означает, что прошивка больше не просто инициализирует железо и передает управление загрузчику. Она оставляет после себя формализованное описание платформы: какие устройства присутствуют в системе, по каким адресам они доступны, какие ресурсы используют, какие события могут генерировать и какие методы необходимо выполнить для изменения их состояния. Операционная система читает эти данные, интерпретирует AML-код и на их основе принимает решения о конфигурации и управлении системой.

Но прежде чем ОС сможет выполнить хотя бы один AML-метод или обратиться к описанию устройства, ей необходимо найти входную точку в мир ACPI. Этой точкой является RSDP (Root System Description Pointer). Через него операционная система находит корневые таблицы RSDT или XSDT, а уже через них получает доступ ко всем остальным ACPI-таблицам: FADT, DSDT, SSDT, MADT и другим.

В этой статье мы разберем, как устроена эта цепочка: от поиска RSDP в физической памяти до выполнения AML-методов и взаимодействия с реальными аппаратными блоками. Посмотрим, какую роль играют таблицы ACPI, как они компилируются и интерпретируются, зачем нужны GPE, каким образом ОС может обращаться к контроллерам питания, и почему ACPI до сих пор остается одной из самых важных, но не всегда очевидных частей современной платформенной архитектуры.

Иными словами, попробуем приоткрыть завесу над тем местом, где прошивка и операционная система действительно начинают разговаривать друг с другом.

Спецификация и архитектура

Прошивка знает реальную топологию платы: какие контроллеры есть в SoC, где висят GPIO, какие линии заведены на wake, где находятся PM-регистры, какие устройства нельзя нормально обнаружить через PCI/USB и какие последовательности нужно выполнить перед сном или пробуждением. ОС, в свою очередь, не должна соответствовать каждой конкретной плате. ACPI как раз и закрывает этот разрыв: прошивка описывает платформу в стандартизированном виде, а ОС через OSPM (Operating System-directed Power Management) берет управление конфигурацией и питанием на себя. В спецификации ACPI прямо указано, что таблицы описывают системную информацию, возможности и методы управления этими возможностями, чтобы OSPM мог управлять устройствами без знания конкретной аппаратной реализации.

С архитектурной точки зрения ACPI состоит из двух больших частей:

  1. табличного описания платформы,

  2. исполняемой логики в AML.

Первый шаг ОС — найти RSDP. Это корневой указатель, с которого начинается вся ACPI-иерархия. На UEFI-системах Windows использует EFI System Table для поиска RSDP. Прошивка записывает туда адрес RSDT или XSDT, а при наличии обоих Windows предпочитает XSDT. Это важно для современных 64-битных систем: RSDT содержит 32-битные адреса, а XSDT 64-битные, поэтому XSDT лучше соответствует современному физическому адресному пространству.

Дальше ОС идет по XSDT или RSDT и проверяет сигнатуры таблиц. Спецификация ACPI гласит, что XSDT указывает на другие таблицы в памяти, а первой обязательной таблицей в этой цепочке является FADT (Fixed ACPI Description Table). FADT, в свою очередь, всегда ссылается на DSDT, где лежит описание системных возможностей и устройств.

FADT для embedded-разработчика особенно интересна, потому что это мост между «чистым описанием» и реальными PM-регистрами платформы. FADT описывает статическую информацию, связанную с конфигурацией и управлением питанием, а также детали реализации железных регистров ACPI. Через нее ОС узнает, где находятся PM1 event/control blocks, PM timer, GPE blocks и другие низкоуровневые механизмы.

DSDT и SSDT — уже не просто таблицы, а описание живой модели платформы. В них находятся устройства, методы, ресурсы, operation regions и зависимости питания. Периферийные устройства и аппаратные возможности платформы описываются в DSDT, загружаемой при старте, или в SSDT, которые могут загружаться при старте или динамически во время выполнения. Для SoC-платформ DSDT часто достаточно, но SSDT удобны для модульности.

На практике пространство имен — это то, с чем реально работает ACPI-интерпретатор ОС. Например, устройство может иметь _HID для идентификации, _CRS для описания ресурсов, _STA для статуса, _PS0/_PS3 для переходов питания, _PRW для wake-событий и _DSD для передачи device-specific properties драйверу. В спецификации пространство имен ACPI определяется как иерархическое дерево именованных объектов в памяти ОС.

С инженерной точки зрения здесь важно понимать границу ответственности. Прошивка не должна «магически» управлять всем после старта ОС. Ее задача — корректно описать платформу и предоставить методы там, где невозможно обойтись стандартным драйвером или стандартной шиной. ОС же должна интерпретировать это описание и управлять устройствами сама. Именно поэтому плохая ACPI-таблица может привести к очень странным симптомам: устройство есть в железе, питание подано, регистры отвечают, но драйвер не загружается, потому что _HID не тот, GPIO interrupt не работает, потому что _CRS описывает не ту полярность, система не просыпается, потому что _PRW или GPE описаны некорректно.

Если очень упростить, ACPI-архитектура отвечает на три вопроса:

  1. Что есть на платформе? DSDT, SSDT, MADT, MCFG, GTDT или другие таблицы.

  2. Где это находится и какие ресурсы использует? _CRS, MMIO ranges, IRQ, GPIO, I2C/SPI resources.

  3. Как этим управлять? AML методы, GPE, _PSx, _PRW, _DSM/_DSD.

ACPI — это, по сути, часть ABI-платформы. После релиза ОС будет полагаться на эти таблицы так же, как драйвер полагается на layout регистров.

Компиляторы ASL: Microsoft ASL Compiler и Intel iASL

При работе с ACPI важно различать исходное описание платформы и то, что в итоге видит операционная система. Разработчик описывает устройства, ресурсы и методы на языке ASL (ACPI Source Language). Однако операционная система не исполняет ASL напрямую. В прошивку попадает уже скомпилированный байткод AML (ACPI Machine Language), который затем читает ACPI-драйвер ОС и выполняет AML-интерпретатор.

Для преобразования ASL в AML используются специальные компиляторы. Применяются два инструмента: Microsoft ASL Compiler и Intel ASL Compiler / Disassembler (iASL).

Оба инструмента решают похожую задачу: позволяют компилировать ASL-код в AML-байткод, проверять корректность ACPI-таблиц до их интеграции в прошивку и, при необходимости, сдампить какую-либо таблицу.

Microsoft ASL Compiler

Microsoft ASL Compiler — это компилятор ASL от Microsoft, который поставляется вместе с Windows Driver Kit. Он используется в Windows-ориентированной разработке и особенно полезен при проверке того, как Windows будет воспринимать описание устройств в ACPI-таблицах. Базовая команда компиляции выглядит так:

asl.exe SSDT_gpio.asl

После выполнения команды исходный ASL-файл преобразуется в AML-файл:

SSDT_gpio.asl  --->  SSDT_gpio.aml

Такой AML-файл затем может быть включен в прошивку. В embedded-проектах это удобно использовать до сборки BIOS-образа, чтобы заранее найти часть ошибок: неверное имя метода, ошибку, конфликт имен в пространстве имен или некорректный пакет.

Intel ASL Compiler / iASL

iASL — это компилятор и дизассемблер из набора ACPICA tools. Он широко используется в Linux-ориентированной разработке и при отладке ACPI-таблиц на реальном устройстве. Как и Microsoft ASL Compiler, iASL умеет компилировать ASL в AML:

iasl SSDT_gpio.asl

Результат будет аналогичным:

SSDT_gpio.asl  --->  SSDT_gpio.aml

Также iASL часто используется для обратной операции — дизассемблирования бинарных ACPI-таблиц в читаемый ASL/DSL-вид:

iasl -d dsdt.dat
iasl -d ssdt*.dat

После этого можно открыть полученные .dsl-файлы и проверить, какие объекты реально присутствуют в таблицах: _HID, _CRS, _AEI, _PRW, OperationRegion, Notify() и другие.

Пример минимального SSDT

Почти любой ACPI-файл начинается с блока объявлений. Он задает, какую таблицу нужно собрать: DSDT, SSDT или OEM-specific table. Внутри описываются Scope, Device, Method, Name, OperationRegion, Field, PowerResource и другие объекты.

Пример минимального SSDT:

DefinitionBlock ("", "SSDT", 2, "GRAV", "GPIODEV", 0x00000001)
{
    Scope (\_SB)
    {
        Device (DEV0)
        {
            Name (_HID, "GRV0001")
            Name (_UID, 0)

            Method (_STA, 0, NotSerialized)
            {
                Return (0x0F)
            }
            Name (_CRS, ResourceTemplate ()
            {
                Memory32Fixed (ReadWrite,
                    0xFE000000,     // BaseAddress
                    0x00001000      // Length
                )
                Interrupt (ResourceConsumer, Level, ActiveHigh, Exclusive)
                {
                    45
                }
            })
        }
    }
}

После компиляции операционная система уже не видит исходный ASL-код, комментарии и форматирование. Она работает с AML-байткодом, из которого строится пространство имен ACPI.

Например, исходный ASL:

Device (DEV0)
{
    Name (_HID, "GRV0001")
    Name (_UID, 0)
    Method (_STA) { Return (0x0F) }
}

После загрузки таблицы в ОС это будет представлено как ACPI namespace:

\_SB.DEV0
    ├── _HID = "GRV0001"
    ├── _UID = 0
    └── _STA = executable method

Linux-сценарий анализа ACPI

В Linux-среде iASL часто используется вместе с acpidump и acpixtract. Типовой цикл выглядит так:

sudo acpidump > acpi.dat
acpixtract -a acpi.dat
iasl -d dsdt.dat
iasl -d ssdt*.dat

После дизассемблирования можно искать нужные объекты:

grep -R "_AEI\|_EVT\|GpioInt\|_PRW\|_DEP\|OperationRegion" .

Такой процесс позволяет проверить, какие ACPI-таблицы реально загружены на устройстве. Это важно, потому что исходники в репозитории и таблицы, которые фактически видит ОС, могут не совпадать.

Практическая разница между asl.exe и iASL

Asl.exe и iasl не являются взаимоисключающими инструментами. Их лучше рассматривать как компиляторы для разных рабочих сценариев. Microsoft ASL Compiler удобен для Windows bring-up и проверки ACPI-описаний под Windows. iASL удобен для Linux, EDK2 и ACPICA-процессов, особенно когда нужно выгрузить реальные таблицы с устройства, дизассемблировать их и сравнить с исходниками.

Если платформа должна работать и под Windows, и под Linux, ACPI-таблицы желательно проверять обоими компиляторами. Разные инструменты могут по-разному подсвечивать предупреждения, устаревшие конструкции и неоднозначности в ASL-коде.

acpi_listen не проверяет компиляцию ASL. Он показывает, дошло ли событие до Linux user space через acpid.
acpi_listen не проверяет компиляцию ASL. Он показывает, дошло ли событие до Linux user space через acpid.
Примерный вывод acpi_listen — увеличение/уменьшение яркости.
Примерный вывод acpi_listen — увеличение/уменьшение яркости.

Важно не путать уровни. Если acpi_listen ничего не показывает, это не всегда значит, что ACPI-таблица неправильная. Событие могло не дойти до уровня kernel ACPI, мог не работать acpid, событие могло уйти через input subsystem, или оно вообще обрабатывается драйвером напрямую. Сам acpid предназначен для уведомления user-space программ об ACPI-событиях и обычно работает как фоновый процесс.

К примеру, для GPIO/GPE-раздела это особенно полезно. Например, если мы описали кнопку через _AEI и _EVT, то acpi_listen помогает быстро понять: событие вообще вышло наружу или осталось внутри ACPI-драйвера/ядра. Если _EVT вызывается и делает Notify(\_SB.PWRB, 0x80), но acpi_listen молчит, значит надо смотреть, как именно Linux сопоставил этот Notify() с button/input/acpid-событием.

Взаимодействие с GPIO — GPE

Когда мы говорим, что ACPI умеет реагировать на GPIO, на самом деле речь идет не о том, что AML напрямую сидит в цикле и опрашивает пины. Модель другая: аппаратное событие приходит в ОС как interrupt/event, ACPI-драйвер сопоставляет его с объектом в namespace, выполняет обработчик AML, а уже тот обычно делает Notify() нужному устройству.

GPIO-событие в ACPI не обрабатывается прошивкой в фоне. После загрузки ОС событие принимает OSPM, затем вызывается AML-метод, который уведомляет нужный объект.
GPIO-событие в ACPI не обрабатывается прошивкой в фоне. После загрузки ОС событие принимает OSPM, затем вызывается AML-метод, который уведомляет нужный объект.

GPE: классическая модель ACPI-событий

Исторически ACPI использует GPE (General-Purpose Events). Это события общего назначения, которые приходят через модель событий ACPI. В классической PC-модели источники GPE описываются через GPE-блоки, адреса которых ОС получает из FADT или из специальных GPE Block Device. В ACPI перечислены четыре типа GPE: GPE из GPE-блока в FADT, GPE из GPE Block Device, GPIO-signaled events через _AEI GPIO-контроллера и interrupt-signaled events через _CRS Generic Event Device.

В современной ACPI-модели GPIO-события являются развитием классической GPE-механики, а не отдельным независимым механизмом.
В современной ACPI-модели GPIO-события являются развитием классической GPE-механики, а не отдельным независимым механизмом.

Для классических GPE используются методы с именами:

_Exx - edge-triggered GPE handler=
_Lxx - level-triggered GPE handler

Где xx — номер события в шестнадцатеричном виде. Например:

Scope (\_GPE)
{
    Method (_E17, 0, NotSerialized)
    {
        Notify (\_SB.PWRB, 0x80)
    }
    Method (_L1A, 0, NotSerialized)
    {
        Notify (\_SB.DEV0, 0x80)
    }
}

Логика простая:

Классическая GPE-цепочка: hardware выставляет status bit, SCI будит ACPI-драйвер, ACPI-драйвер выполняет AML-метод.
Классическая GPE-цепочка: hardware выставляет status bit, SCI будит ACPI-драйвер, ACPI-драйвер выполняет AML-метод.

GPIO-signaled events на современных SoC

На SoC-платформах часто нет классической PC-логики с PM1/GPE-блоками в привычном виде. Вместо этого используются GPIO или platform IRQ. В ACPI это называется GPIO-signaled ACPI Events. Спецификация ACPI указывает, что на hardware-reduced ACPI платформах ACPI-события могут сигнализироваться через GPIO interrupt, если этот GPIO interrupt перечислен в объекте _AEI GPIO-контроллера. Такие interrupt’ы забирает OSPM и сопоставляет с ACPI event method.

Для GPIO-signaled events обработчик _EVT должен находиться в scope того GPIO-контроллера, чьи пины перечислены в _AEI.
Для GPIO-signaled events обработчик _EVT должен находиться в scope того GPIO-контроллера, чьи пины перечислены в _AEI.

Пример из ACPI показывает GPIO-контроллер с _CRS и _AEI: _CRS описывает MMIO-интерфейс контроллера и его interrupt line, а _AEI перечисляет GPIO interrupt connections, которые должны обрабатываться OSPM как ACPI-события. В примере один пин используется как thermal zone event, а второй — как power button с wake-возможностью.

Упрощенный пример:

Device (\_SB.GPI2)
{
    Name (_HID, "XYZ0003")
    Name (_UID, 2)
    Name (_CRS, ResourceTemplate ()
    {
        // MMIO-регистры GPIO-контроллера
        Memory32Fixed (ReadWrite, 0x30000000, 0x200)

        // IRQ самого GPIO-контроллера в interrupt controller
        Interrupt (ResourceConsumer, Level, ActiveHigh, Exclusive)
        {
            21
        }
    })

    Name (_AEI, ResourceTemplate ()
    {
        // GPIO 14: событие thermal zone
        GpioInt (Edge, ActiveHigh, Exclusive, PullDown, , "\\_SB.GPI2")
        {
            14
        }
        // GPIO 36: power button, wake-capable
        GpioInt (Edge, ActiveLow, ExclusiveAndWake, PullUp, , "\\_SB.GPI2")
        {
            36
        }
    })

    Method (_EVT, 1, NotSerialized)
    {
        Switch (ToInteger (Arg0))
        {
            Case (14)
            {
                Notify (\_TZ.TZ00, 0x80)
            }

            Case (36)
            {
                Notify (\_SB.PWRB, 0x80)
            }
        }
    }
}

Здесь _EVT получает в Arg0 номер GPIO-пина, который вызвал событие. Спецификация указывает, что для GPIO-signaled events _EVT принимает один аргумент — controller-relative zero-based GPIO pin number, а OSPM вызывает _EVT после обработки interrupt’а, перечисленного в _AEI.

_AEI (ACPI Event Information object) говорит ОС: «вот эти GPIO interrupt’ы не надо отдавать обычному драйверу как ресурс устройства; их надо считать ACPI-событиями». Это принципиально отличается от GPIO interrupt в _CRS какого-нибудь peripheral device.

GpioInt в _CRS и GpioInt в _AEI выглядят похоже, но семантика разная.
GpioInt в _CRS и GpioInt в _AEI выглядят похоже, но семантика разная.

Инженерно это одна из самых частых ловушек. Разработчик видит GpioInt, добавляет его в _CRS устройства и ожидает, что будет вызван AML-метод. Но если событие должно обрабатываться именно ACPI event model, interrupt должен быть указан в _AEI GPIO-контроллера, а не только в _CRS периферийного устройства.

_EVT, _Exx и _Lxx

Для GPIO-signaled events основной современный путь — _EVT. Он удобен тем, что один метод может обрабатывать все ACPI GPIO events данного контроллера:

Method (_EVT, 1, NotSerialized)
{
    Switch (Arg0)
    {
        Case (0x10)
        {
            Notify (\_SB.DEV0, 0x80)
        }

        Case (0x24)
        {
            Notify (\_SB.PWRB, 0x80)
        }
    }
}

Но есть нюанс: для номеров событий меньше 255 допускаются также _Exx и _Lxx, и они имеют приоритет над _EVT. Спецификация отдельно указывает, что для GPIO event numbers меньше 255 методы _Exx и _Lxx могут использоваться вместо _EVT. Если они есть, _EVT не вызывается.

Приоритет обработчиков GPIO-signaled events: для событий меньше 255 _Exx/_Lxx могут перекрыть _EVT.
Приоритет обработчиков GPIO-signaled events: для событий меньше 255 _Exx/_Lxx могут перекрыть _EVT.

Сравнение: классический GPE и GPIO-signaled event

Допустим, на плате кнопка питания заведена на GPIO 36 контроллера GPI2. Аппаратно линия подтянута вверх, при нажатии уходит в ноль. Значит, в ACPI это будет ActiveLow, обычно Edge, и если кнопка должна будить систему (ExclusiveAndWake).

Device (\_SB.GPI2)
{
    Name (_HID, "XYZ0003")
    Name (_UID, 2)

    Name (_CRS, ResourceTemplate ()
    {
        Memory32Fixed (ReadWrite, 0x30000000, 0x200)
        Interrupt (ResourceConsumer, Level, ActiveHigh, Exclusive) { 21 }
    })
    Name (_AEI, ResourceTemplate ()
    {
        GpioInt (Edge, ActiveLow, ExclusiveAndWake, PullUp, , "\\_SB.GPI2")
        {
            36
        }
    })

    Method (_EVT, 1, NotSerialized)
    {
        If (Arg0 == 36)
        {
            Notify (\_SB.PWRB, 0x80)
        }
    }
}

Затем проверить фактические DSDT/SSDT, которые реально загрузились с платформы. Очень часто исходник в репозитории уже исправлен, но в прошивку image попала старая AML-таблица. На Linux обычно начинают с:

sudo acpidump > acpi.dat
acpixtract -a acpi.dat
iasl -d dsdt.dat
iasl -d ssdt*.dat

Дальше ищем:

_AEI
_EVT
GpioInt
_PRW
_PSW
_DSW
Notify

Если фронт на пине есть, но _EVT не вызывается, проблема обычно в _AEI, IRQ GPIO-контроллера, polarity/trigger или в том, что ОС не забрала этот interrupt как ACPI event. Если _EVT вызывается, но драйвер не реагирует, смотрим Notify() и объект, которому он отправляется.

Практический пример ACPI-конфигурации

Разберем практический пример: инициализацию кнопок яркости LVDS-панели. На плате есть кнопки BKL_UP и BKL_DOWN, физически заведенные на GPIO-линии. Наша задача — сделать так, чтобы нажатие этих кнопок не просто меняло уровень на пине, а корректно воспринималось операционной системой как ACPI-событие.

В схемотехнике тестового стенда обозначены некоторые пины, пусть они будут называться BKL_DOWN, BKL_UP, Panel_ON/OFF_IN, Panel_ON/OFF_OUT, связанные с управлением яркостью и LVDS-панелью. Там же указано, что они работают через линии GPIO_D20-23.

Общая цепочка: физическая кнопка → GPIO pad → GPE mapping → AML-метод → Notify() устройству.
Общая цепочка: физическая кнопка → GPIO pad → GPE mapping → AML-метод → Notify() устройству.

Нам нужно понять:

  1. какой сигнал приходит с кнопки,

  2. на какой GPIO pad он заведен,

  3. какая у него полярность,

  4. является ли он входом или выходом,

  5. должен ли он генерировать interrupt,

  6. кому в ОС нужно отправить событие.

В нашем случае для кнопок яркости это выглядит так:

BKL_UP        -> GPIO_D21 / GPP_D21
BKL_DOWN      -> GPIO_D22 / GPP_D22
Panel_ON/OFF  -> GPIO_D20 / GPP_D20

В нашей среде обозначено подключение кнопок LVDS на электрической функциональной схеме и отдельная схема подключения к панели CON_BKL LVDS, где фигурируют линии BKL_UP_CON и BKL_DOWN_CON.

Инженерный смысл здесь простой: ACPI не исправляет ошибку схемотехники. Если мы перепутаем BKL_UP и BKL_DOWN на уровне GPIO-map, то AML будет честно отправлять не те события. С точки зрения ОС все будет работать, просто кнопка увеличения яркости будет уменьшать яркость.

Конфигурируем GPIO pad

Ключевые параметры GPIO-конфигурации:

  • PadMode — определяет режим вывода: обычный GPIO или native-функция контроллера, например I2C, SPI, UART, reset, clock request. Для ACPI-событий от кнопок нужен GPIO-режим, иначе GPIO-контроллер может не увидеть изменение уровня как событие.

  • HostSoftPadOwn — задает владельца pad со стороны host software. Для обычного GPIO-ресурса пин может принадлежать GPIO-драйверу, но для SCI/GPE-события нужен ACPI ownership. В таком случае событие идет не напрямую в драйвер устройства, а в ACPI-драйвер ОС, который затем вызывает AML-метод.

  • Direction — задает направление сигнала: вход или выход. Кнопки вроде BKL_UP и BKL_DOWN должны быть входами, потому что сигнал приходит с платы в SoC/PCH. Для enable-линий, reset-линий и сигналов питания обычно используется выход.

  • OutputState — задает начальный уровень GPIO, если пин настроен как выход. Для входных кнопок обычно используется значение по умолчанию, а для управляющих сигналов питания или panel enable важно явно задать безопасный стартовый уровень.

  • InterruptConfig — описывает, генерирует ли GPIO прерывание, какого оно типа и куда маршрутизируется. Для ACPI-кнопок часто используется GpioIntEdge | GpioIntSci: событие возникает по фронту сигнала и доставляется в ACPI через SCI. Дальше ОС читает GPE status и вызывает _Exx или _Lxx.

  • PowerConfig — задает reset domain, то есть при каком reset конфигурация pad будет сброшена. Это важно для sleep/wake-сценариев: если GPIO должен будить систему или сохранять событийное поведение после resume, reset domain должен быть выбран осознанно.

  • ElectricalConfig — задает электрическую конфигурацию pad: без подтяжки, pull-up или pull-down. Для кнопок это критично: если линия не имеет внешней подтяжки и внутренняя тоже не включена, вход может «плавать» и генерировать ложные события.

  • LockConfig — определяет, можно ли менять конфигурацию pad после инициализации. На этапе bring-up пины часто оставляют unlocked, чтобы упростить отладку. Для критичных GPIO конфигурацию обычно блокируют, чтобы ОС или runtime-код случайно не изменили режим, направление или routing.

Для кнопок яркости конфигурация обычно сводится к такой логике:

GPIO pad в режиме GPIO -> ownership у ACPI -> направление input -> событие по фронту -> маршрут в SCI -> GPE mapping -> AML-метод _Exx -> Notify() нужному устройству.

Пример конфигурации:

GLOBAL_REMOVE_IF_UNREFERENCED GPIO_INIT_CONFIG mGpioTablePostMem[] = {
  // PANEL_ON/OFF_OUT
  { GPIO_VER4_S_GPP_D20, { GpioPadModeGpio, GpioHostOwnGpio,
    GpioDirOut, GpioOutLow, GpioIntDefault,
    GpioPlatformReset, GpioTermNone, GpioPadConfigUnlock } },

  // PANEL_ON/OFF_IN
  { GPIO_VER4_S_GPP_D21, { GpioPadModeGpio, GpioHostOwnAcpi,
    GpioDirInInv, GpioOutDefault, GpioIntEdge | GpioIntSci,
    GpioHostDeepReset, GpioTermNone, GpioPadConfigUnlock } },

  // BKL_UP
  { GPIO_VER4_S_GPP_D22, { GpioPadModeGpio, GpioHostOwnAcpi,
    GpioDirInInv, GpioOutDefault, GpioIntEdge | GpioIntSci,
    GpioHostDeepReset, GpioTermNone, GpioPadConfigUnlock } },

  // BKL_DOWN
  { GPIO_VER4_S_GPP_D23, { GpioPadModeGpio, GpioHostOwnAcpi,
    GpioDirInInv, GpioOutDefault, GpioIntEdge | GpioIntSci,
    GpioHostDeepReset, GpioTermNone, GpioPadConfigUnlock } },
};

Пока GPIO не настроен как ACPI-owned и SCI-capable, никакой AML-метод не спасет ситуацию. Метод _E16 может быть идеально написан, но если физический interrupt не попал в GPE-механику, ОС его просто не увидит.

Добавляем GPIO в GPE-группу

Следующая тонкость: настроить сам GPIO pad недостаточно. Нужно еще проложить маршрут от GPIO-группы к GPE-регистрам. Без добавления пинов в GPE-группу они превратятся в тыкву, которая не реагирует на воздействие. Там же показано, что для этого используются GPE_DWx, а поскольку GPE double word может обработать только 32 пина, для группы можно замапить только один DW за раз.

Для работы GPE нужно настроить mapping сразу в двух местах: со стороны PMC и со стороны GPIO community.
Для работы GPE нужно настроить mapping сразу в двух местах: со стороны PMC и со стороны GPIO community.

В коде это выглядит как два уровня.

Сначала низкоуровневая запись в PMC:

VOID
PmcSetGpioGpe (
  IN UINT32 GpeDw0Value,
  IN UINT32 GpeDw1Value,
  IN UINT32 GpeDw2Value
  )
{
  UINT32 Data32Or;
  UINT32 Data32And;

  Data32And = (UINT32) ~(B_PMC_PWRM_GPIO_CFG_GPE0_DW2 |
                         B_PMC_PWRM_GPIO_CFG_GPE0_DW1 |
                         B_PMC_PWRM_GPIO_CFG_GPE0_DW0);

  Data32Or = (UINT32) ((GpeDw2Value << N_PMC_PWRM_GPIO_CFG_GPE0_DW2) |
                       (GpeDw1Value << N_PMC_PWRM_GPIO_CFG_GPE0_DW1) |
                       (GpeDw0Value << N_PMC_PWRM_GPIO_CFG_GPE0_DW0));

  MmioAndThenOr32 (
    (PCH_PWRM_BASE_ADDRESS + R_PMC_PWRM_GPIO_CFG),
    Data32And,
    Data32Or
    );
}

Затем обертка, которая находит соответствие между GPIO group/DW и GPE_DWx, после чего программирует и PMC, и GPIO community MISCCFG. В GpioSetGroupDwToGpeDwX() проверяется уникальность групп, ищется mapping через GpioGetGroupToGpeMapping(), программируется PmcSetGpioGpe(), а затем выставляются соответствующие биты GPE0_DW0/DW1/DW2 в GPIO MISCCFG.

Пишем ACPI-методы _E15, _E16, _E17

Теперь, когда GPIO и GPE настроены, можно переходить к AML. _Exx берется из ACPI GPE naming convention. По спецификации ACPI — когда приходит GPE, OSPM ищет метод вида \_GPE._Txx, где xx это hex-номер события, а T это тип обработки:

  • E для edge-triggered,

  • L для level-triggered.

То есть GPE 0x16 + edge = _E16. Для наших кнопок GPPD21 имеет value 0x15 = _E15, GPPD22 имеет value 0x16 = _E16, GPPD23 имеет value 0x17 = _E17.

0x86 и 0x87 не произвольные числа. Это стандартизованные ACPI video output notifications:

  • 0x86 = Increase Brightness,

  • 0x87 = Decrease Brightness.

В нашем случае логика такая:

_E15 -> переключить Panel ON/OFF
_E16 -> BKL_UP
_E17 -> BKLDOWN

Готовый ASL-фрагмент:

//
// AdvancedFeaturesBegin
//
// PANEL_ON_OFF
Method (_E15, 0, Serialized)
{
    Store (PENL, Local0)

    If (LEqual (Local0, 1))
    {
        Store (0, PENL)
    }
    Else
    {
        Store (1, PENL)
    }
}

// BKL_UP
Method (_E16, 0, Serialized)
{
    Notify (\_SB.PC00.GFX0.DD1F, 0x86)
}

// BKL_DOWN
Method (_E17, 0, Serialized)
{
    Notify (\_SB.PC00.GFX0.DD1F, 0x87)
}
//
// AdvancedFeaturesEnd
//

Здесь \_SB.PC00.GFX0.DD1F — целевое устройство панели/eDP, а 0x86 и 0x87 используются как события увеличения и уменьшения яркости. Эти уведомления, в нашем примере, обозначены как BKLUP и BKLDOWN: _E16 вызывает Notify(..., 0x86), а _E17 — Notify(..., 0x87).

Мы можем показать, что ACPI — это не абстрактная таблица BIOS, а рабочий событийный механизм. Физическая кнопка на плате приводит к выполнению AML-кода, а AML-код отправляет событие конкретному устройству в пространстве имен.

Вывод

Для разработчика ACPI интересен тем, что он находится на границе нескольких миров. Здесь встречаются схемотехника, регистры SoC, UEFI, AML, драйверная модель ОС, power management и interrupt routing. Ошибка в одном поле _CRS, неверная полярность GpioInt, забытый DEP или неправильный индекс в _PRW могут выглядеть как проблема драйвера, ядра или даже аппаратный дефект, хотя на самом деле причина лежит в описании платформы. Через ACPI можно описать устройства, interrupt controllers, GPIO, I2C, SPI, PCIe, thermal zones, battery, sleep states, wake events, power resources и vendor-specific особенности платформы. Но за эту гибкость приходится платить дисциплиной: аккуратной архитектурой таблиц, минимальной логикой в AML, владением регистрами и обязательной проверкой на реальном железе.

Если ОС что-то неправильно увидела, сначала проверьте, что вы ей правильно показали.

Комментарии (0)