Pull to refresh

Исправляем ACPI на Samsung N250

Reading time 8 min
Views 15K
Original author: Peter Zotov
В этой статье я хотел бы описать типичные ошибки проектирования, найденные в BIOS современного нетбука, и методы их обнаружения, изучения и исправления.


Вступление



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

В теории, такая система должна была сделать драйвера для чипсета ненужными. Она весьма мощная (если не сказать «раздутая»), и вполне может справиться с такой задачей; например, на Маках ACPI используется весьма широко и грамотно.

В реальности, однако, поставщики PC-совместимых систем зачастую включают ошибочные или неполные таблицы ACPI, и не последней причиной для этого является vendor lock-in. Таким образом, эти системы требуют использования нетривиальных обходных путей, зачастую недокументированных и тоже не всегда корректно работающих. Вместо этого, я попытался исправить ACPI для одной такой системы.

Компьютер, который у меня есть — нетбук Samsung N250+. У него достаточно неплохое «железо» (за исключением охочей до батарейки и вообще кривой WiFi-карточки Broadcom, которую я сразу же заменил на аналогичую Atheros), но качество BIOS-а весьма печальное. На момент релиза не было даже возможности включить (или выключить) WiFi из Linux-системы: его состояние можно было изменить только через CMOS Setup Utility. На данный момент драйвер есть, но он использует фундаментально порочный подход, и страдает от некоторых проблем.

Исследование текущего состояния



Поддержка возможностей нетбука, для которых код в ACPI отсутствовал, изначально была реализована в модуле ядра easy slow down manager, который в итоге был принят в ядро как samsung-laptop.c.

Как видно на строке 725 исходного кода, этот драйвер использует вызовы SMI (и интерфейс Samsung под названием SABI) для того, чтобы устанавливать уровень подсветки, изменять «режим производительности» (который на самом деле всего лишь меняет скорость вращения вентиляторов) и включать питание беспроводному модулю. SMI-вызов — это команда, которая заставляет ЦП активировать так называемый режим управления системой (SMM), специальную возможность чипсета и процессора, одинаково похожую на гипервизор и руткит.

BIOS может настроить чипсет так, чтобы он перехватывал определенные операции (например, доступ к выбранным регионам памяти или портов ввода-вывода) и активировал SMM, ОС не может ни обнаружить факт входа в SMM (кроме как косвенными методами), ни прервать его, ни предотвратить. После этого, BIOS может выполнить произвольный код: например, SMM используется для того, чтобы сэмулировать для старых ОС (например, ДОС) поддержку PS/2-мыши в тех случаях, когда подключена USB-мышь. Более того, область памяти, выделенная для обработчика SMM, ни при каких обстоятельствах не доступна ОС, делая прямой анализ логики ее работы невозможным.

К счастью, в этом случае вызовы SMI, скорее всего, меняют лишь пару байтов, и, если повезет, можно будет определить их расположение, не изучая код режима SMM.

Рассмотрим поближе таблицы ACPI. Существует множество их типов, но в данном случае важна только одна, DSDT — таблица с байткодом обработчиков многих системных событий.

Для извлечения таблицы из системы и модификации ее кода нам потребуются две утилиты: «acpidump» и «iasl». На Debian-подобной ОС они находятся в пакетах с тем же названием.

$ sudo acpidump -o dsdt.aml -b
$ iasl -d dsdt.aml # декомпилирует dsdt.aml в dsdt.dsl


Для наглядности, я оформил таблицу с историей моих изменений как репозиторий github; начальное состояние содержится в этом коммите. Как видно, таблица весьма длинная: более 5000 строк. Таблицы длиной более чем в 25000 строк встречаются вполне регулярно.

При попытке скомпилировать таблицу обратно в байткод (наберите make) без каких-либо изменений компилятор выведет несколько ошибок и замечаний. Их достаточно легко исправить, глядя только на сообщения и спецификацию ACPI; в этой теме форума Gentoo есть несколько неплохих подсказок. Кстати, она на семь лет старше моего нетбука, но советы до сих пор актуальны. Исправленную таблицу можно увидеть здесь.

Чиним подсветку



У моего нетбука светодиодная подсветка, и поэтому ее яркость можно менять, просто включая ее на определенную долю одинаковых интервалов времени, например, для затемнения на 30% можно держать ее включенной 70% времени. Чтобы мерцание не было заметно, это переключение (ШИМ) происходит на частоте, заведомо превышающей чувствительность человеческого глаза — скажем, 200кГц вполне достаточно.

В данном случае, скважность ШИМ, вероятно, изменяется встроенным графическим контроллером. Вот он на шине PCI:

$ lspci
00:00.0 Host bridge: Intel Corporation N10 Family DMI Bridge
00:02.0 VGA compatible controller: Intel Corporation N10 Family Integrated Graphics Controller
00:02.1 Display controller: Intel Corporation N10 Family Integrated Graphics Controller
<...>


Цифры «00:02.0» — адрес устройства на шине. Зная этот адрес, можно запросить или изменить параметры устройства, так как Linux предоставляет множество точек управления через sysfs. Одна из них позволяет читать и записывать конфигурационное пространство PCI: блок из 256 байтов, в котором хранятся настройки устройства. Первые 64 байта в этом блоке имеют определенное спецификацией значение, а остальные могут свободно использоваться производителем для своих нужд.

Проверим, что происходит с конфигурацией при изменении уровня подсветки (несмотря на то, что здесь приведен пример для Linux с открытым драйвером, все это можно сделать и для закрытого драйвера или даже на Windows; считать конфигурационное пространство можно и в ней):

# echo 7 >/sys/class/backlight/samsung/brightness
# hexdump -C /sys/bus/pci/devices/0000\:00\:02.0/config >config-1
# echo 5 >/sys/class/backlight/samsung/brightness
# hexdump -C /sys/bus/pci/devices/0000\:00\:02.0/config >config-2
# diff -u config-1 config-2
--- config-1    2011-09-05 01:06:13.326930250 +0400
+++ config-2    2011-09-05 01:06:21.503828025 +0400
@@ -13,5 +13,5 @@
 000000c0  00 00 00 00 01 00 00 00  00 00 00 00 a7 00 00 00  |................|
 000000d0  01 00 22 00 00 00 00 00  00 00 00 00 00 00 00 00  |..".............|
 000000e0  00 00 00 00 00 00 00 00  00 80 00 00 00 00 00 00  |................|
-000000f0  79 00 00 00 ff 00 00 00  ad 0f 00 00 7c 0e 5c 7f  |y...........|.\.|
+000000f0  79 00 00 00 73 00 00 00  ad 0f 00 00 7c 0e 5c 7f  |y...s.......|.\.|
 00000100


Таким образом, байт по адресу 0xf4 управляет уровнем подсветки. Можно в этом убедиться, запустив команду sudo setpci -s 00:02.0 f4.b=80 (заменив 80 на нужный уровень подсветки).

Теперь перепишем DSDT так, чтобы обновлялось это значение (и, возможно, в процессе этого получится узнать, почему управление подсветкой через ACPI вообще не работает):

Согласно спецификации ACPI (приложение B, раздел 6.2, стр. 704), совместимое описание графического адаптера должно реализовывать методы _BCL, _BCM и _BQC. В нашем DSDT эти методы определены на строке 1767. Вот их откоментированный исходный код:

    /*  = Запрос списка поддерживаемых уровней яркости подсветки =
     * Возвращает массив (Package в терминах ACPI), содержащий
     * поддерживаемые и предпочитаемые уровни подсветки.
     */
    Method (_BCL, 0, NotSerialized)
    {
        /* Изменить бит в области памяти под названием GVNS (стр. 132).
           Я не знаю, что при этом происходит. */
        Or (VDRV, 0x01, VDRV)

        Return (Package (0x08)
        {
            0x64, /* Предпочитаемый уровень при питании от сети */
            0x05, /* То же, для автономной работы */
            0x0F, /* Список возможных уровней яркости */
            0x18, /* 24 */
            0x1E, /* 30 */
            0x2D, /* 45 */
            0x3C, /* 60 */
            0x50  /* 80 */
        })
    }

    /*  = Установить уровень яркости подсветки =
     * Получает целевой уровень в Arg0.
     * ОС гарантирует, что он содержится в списке, возвращаемом
     * методом _BCL.
     */
    Method (_BCM, 1, NotSerialized)
    {
        /* Разделить Arg0 на 10. Остаток сохранить в Local0,
         * а результат — в Local1. */
        Divide (Arg0, 0x0A, Local0, Local1)

        /* Названия предикатов в ACPI начинаются с "L" (от "Logic").
         * Таким образом, LEqual(Local0, 0x00) можно записать в Си как
         * (Local0 == 0x00).
         *
         * Как легко заметить, для половины «поддерживаемых» уровней
         * этот метод просто ничего не делает.
         */
        If (LEqual (Local0, 0x00))
        {
            /* Передать уровень функции BRTW (стр. 5324).
             * Она утонула^Wне работает. */
            BRTW (Arg0)
        }
    }

    /*  = Запросить текущий уровень яркости подсветки =
     * Возвращает текущий уровень. Значение должно содержаться
     * в списке, возвращаемом методом _BCL.
     */
    Method (_BQC, 0, NotSerialized)
    {
        /* См. выше. */
        Divide (BRTL, 0x0A, Local0, Local1)
        If (LEqual (Local0, 0x00))
        {
            /* Вернуть значение переменной BRTL, которое должно было
             * быть записано функцией BRTW (на стр. 5341). */
            Return (BRTL)
        }
    }


Чтобы изменить этот код для работы через конфигурационное пространство PCI, нужно добавить новое поле в структуру, описывающую это пространство. Адрес адаптера 00:02.0 соответствует значению 0x0002000 в ACPI (раздел 6.1.1, стр. 200). Устройство с таким адресом определено на строке 1325; за определением следует описание конфигурационного пространства PCI.

Как было упомянуто, первые 64 (0x40) байтов в этом пространстве зарезервированы для внутреннего испольования. Из-за этого ACPI даже не включает их в регион; он определен как OperationRegion(IGDP, PCI_Config, 0x40, 0xC0), где третий аргумент означает отступ с начала области PCI_Config. Поле, управляющее яркостью, расположено по адресу 0xf4 во всем пространстве, и 0xb4 в этом регионе.

За определением региона следуют определения полей. Вся конструкция Field представляет из себя поток битовых полей (длина определена в битах, а не байтах), одно за другим, перемежающееся указанием смещений (Offset), задаваемых, напротив, в байтах. Назовем наше поле BLVL и включим его в структуру:

@@ -1347,7 +1347,8 @@ Device (IGD0)
                             Offset (0xB0),
                             Offset (0xB1),
                     CDVL,   5,
-                            Offset (0xB2),
+                            Offset (0xB4),
+                    BLVL,   8,
                             Offset (0xBC),
                     ASLS,   32
                 }


Так как система имен ACPI иерархическая, это поле теперь доступно глобально под именем _SB.PCI0.IGD0.BLVL (имя составлено из вложенных конструкций Device и Scope), и методы для управления яркостью теперь можно переписать так, чтобы они обращались к полю BLVL напрямую:

    Method (_BCL, 0, NotSerialized)
    {
        /* Не знаешь — не трогай. */
        Or (VDRV, 0x01, VDRV)

        /* Уровни яркости в пространстве PCI — от 0x00 до 0xff.
         * Хочу, чтобы было 16 значений. */
        Return (Package (0x12)
        {
            0xEE, /* питание от сети */
            0x22, /* питание от батарей */
            0x01, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
            0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD,
            0xEE, 0xFF
        })
    }

    Method (_BCM, 1, NotSerialized)
    {
        Store (Arg0, \_SB.PCI0.IGD0.BLVL)
    }

    Method (_BQC, 0, NotSerialized)
    {
        Return (\_SB.PCI0.IGD0.BLVL)
    }


Обновленная DSDT также лежит в репозитории.

Во время тестирования своих изменений мне потребовалось отладить код в DSDT. Отладочный вывод работает при помощи команды вида Store (something, Debug). Для того, чтобы Linux отправил сообщение в лог, нужно добавить параметр ядра acpi.debug_level=0x1f.

Измененную и собранную (make или iasl -tc dsdt.dsl) DSDT теперь нужно отправить на место той, что предоставил поставщик. Для этого можно было бы перепрошить BIOS — но я даже не знаю внутреннюю структуру BIOS-а (и, если уж об этом зашла речь, способа его перепрошивки). Проще и безопаснее проинструктировать Linux использовать нашу DSDT вместо системной. Для этого, нужно собрать dsdt.hex (опция -tc инструктирует iasl генерировать массив Си, что и требуется), положить его в каталог include/ исходников ядра и установить опцию CONFIG_ACPI_CUSTOM_DSDT_FILE в «dsdt.hex». (Она недоступна, если включена опция CONFIG_STANDALONE, «Select only drivers that do not need compile-time external firmware» в «Generic driver options».)

Можно собрать ядро, установить его и перезагрузиться. Вуаля: теперь изменение яркости подсветки работает со стандартным драйвером ACPI. (Например, echo 7 >/sys/class/backlight/acpi_video0/brightness).

Другие функции



Для того, чтобы найти другие похожие поля, изменяемые кодом в SMM, я написал простой скрипт. Нужно отметить, что некоторые устройства, а именно мосты PCI Express и сетевые адаптеры, порождают множество самопроизвольных изменений.

К сожалению, ни скорость вентилятора, ни выключатель беспроводного модуля не оказались связаны ни с какими изменениями в конфигурационном пространстве. Вероятно, они производятся через Embedded Controller или интерфейс SMBus, что означает отсутствие постоянных изменений в системной памяти.

Более того, даже если бы я обнаружил интерфейс отключения беспроводного модуля, я бы не смог использовать стандартный способ его представления системе — из-за отсутствия такого способа в природе. На ноутбуках, где этот интерфейс действительно задан в ACPI, существует платформенно-специфичный драйвер для его обработки (в отличие от подсветки, для которой существует общий стандарт).
Tags:
Hubs:
+78
Comments 8
Comments Comments 8

Articles