Настройка оборудования на раннем этапе загрузки средствами ACPI (на примере FreeBSD)

в 3:49, , рубрики: ACPI, dsdt, freebsd, open source, метки: , ,

Несколько лет назад, когда CardBus и FireWire (IEEE 1394) еще были относительно «в ходу», многие производители ноутбуков в своей продукции использовали контроллеры семейства PCIXX21 и PCIXX11 фирмы Texas Instruments: один небольшой чип обеспечивал поддержку не только упомянутых интерфейсов, но и многих популярных стандартов сменных карт памяти.

Такой чип стоит и в моей NEC Versa S950. Этот малоизвестный ноутбук я в свое время предпочел даже ThinkPad-серии практически исключительно из-за более лучшей поддержки FreeBSD (оборудования в целом и спящего режима в частности) — специально тестировал в новосибирском Техносити перед покупкой. Долгое время я не пользовался встроенным кард-ридером, по привычке обходясь USB'шными «свистками». Но недавно я обнаружил, что FreeBSD до сих пор его не поддерживает. И если лет пять-шесть назад это можно было объяснить отсутствием нормального драйвера для этих контроллеров (нужно было что-то скачивать и собирать самому), теперь я точно знал, что они «из коробки» поддерживаются во FreeBSD драйвером sdhci(4), о чем прямо сказано на странице руководства (и позже подтвердилось чтением исходников).

Я начал неспешно гуглить на эту тему, и картина стала вырисовываться невеселая. Оказалось, что таких «счастливчиков», как я, немало. Многие постили в рассылки и форумы «портянки» dmesg и pciconf -lv, заводили баги в трекерах (например, OpenBSD PR i386/5843), но решения никто не предлагал. Более того, фактически поставив точку в вопросе, Александр Мотин, автор драйвера sdhci(4), в 2010 г. написал на форуме, что-де TI документацию на чип не дают, а значит, если производитель сконфигурировал чип неверно, а его настройка через BIOS не предусмотрена, сделать что-либо затруднительно. В свою очередь, Theo de Raadt закрыл i386/5843 с словами «We do what we can. This vendor, amongst other, have their sdhc controllers locked out and hidden behind little undocumented bits. We've strugged before to find this information, and failed. If you can find this information on some other operating system, or in some vendor documentation, we would be thrilled.».

В отчаянии я загрузился с Ubuntu LiveCD. И очень удивился тому, что в Linux кард-ридер работает. Значит…

Плохо гуглил

Оказывается, еще в 2006 г. Алекс Дубов написал Linux-драйвер для TI FlashMedia ридеров. Я скачал исходники и принялся их изучать, надеясь впоследствии доработать sdhci(4) или даже спортировать драйвер целиком. В первую очередь я посмотрел список поддерживаемых PCI vendor/device ids, чтобы сравнить с «нашим» драйвером. Он оказался небольшим:

$ cat linux/pci_ids.h
#define PCI_VENDOR_ID_TI		0x104c
#define PCI_DEVICE_ID_TI_XX21_XX11_FM	0x8033
#define PCI_DEVICE_ID_TI_XX12_FM	0x803b
#define PCI_DEVICE_ID_TI_XX20_FM	0xac8f

Число 0x8033 мне уже знакомо по выводу pciconf -lv на моем ноуте:

none3@pci0:6:7:3:	class=0x018000 card=0x83191033 chip=0x8033104c rev=0x00 hdr=0x00
    vendor     = 'Texas Instruments (TI)'
    device     = 'PCIxx11/21 Integrated FlashMedia Controller'
    class      = mass storage

Это тот самый кард-ридер, который не работает во FreeBSD, но работает в Linux. А вот кусок кода из sdhci.c (FreeBSD):

static const struct sdhci_device {
	uint32_t	model;
	uint16_t	subvendor;
	char	*desc;
	u_int	quirks;
} sdhci_devices[] = {
	{ 0x08221180, 	0xffff,	"RICOH R5C822 SD",
	    SDHCI_QUIRK_FORCE_DMA },
	{ 0xe8221180, 	0xffff,	"RICOH SD",
	    SDHCI_QUIRK_FORCE_DMA },
	{ 0xe8231180, 	0xffff,	"RICOH R5CE823 SD",
	    SDHCI_QUIRK_LOWER_FREQUENCY },
	{ 0x8034104c, 	0xffff, "TI XX21/XX11 SD",
	    SDHCI_QUIRK_FORCE_DMA },

Можно заметить, что идентификатор устройства TI XX21/XX11 SD (0x8034104c) похож на мой (0x8033104c) с точностью до одной цифры. Кроме того, я обратил внимание, что контроллеры CardBus (0x8031104c) и FireWire (0x8032104c) не просто имеют схожие id'шки, но и PCI-селекторы всех устройств отличаются только номером функции, а устройство у них у всех одно и то же:

none1@pci0:6:7:0:	class=0x060700 card=0x83191033 chip=0x8031104c rev=0x00 hdr=0x02
    vendor     = 'Texas Instruments (TI)'
    device     = 'PCIxx21/x515 Cardbus Controller'
    class      = bridge
    subclass   = PCI-CardBus
none2@pci0:6:7:2:	class=0x0c0010 card=0x83191033 chip=0x8032104c rev=0x00 hdr=0x00
    vendor     = 'Texas Instruments (TI)'
    device     = 'OHCI Compliant IEEE-1394 FireWire Controller'
    class      = serial bus
    subclass   = FireWire

Вспомнив слова Саши Мотина о том, что чип на самом деле реализует оба контроллера (SDHCI и FlashMedia), я стал искать более целенаправленно, и вскоре наткнулся на еще один пост, а затем на сообщение в рассылке freebsd-mobile@ об аналогичной проблеме на HP NC6220. Решение нигде не предлагалось, но, в отличие от большинства дискуссий, которые сводились к дурацким советам типа «попробуйте последнюю версию драйвера» или банальным «сожалею, но, похоже, вы в пролете», теперь, по крайней мере, стало понятно, что конфигурация чипа как-то отображается в дампе PCI function, а значит, возможно, ее получится поменять; а главное, что таки-доступна документация: PCIXXX21/PCIXXX11 Implementation Guide. И вот тут мне стало по-настоящему интересно.

Забегая вперед, скажу, что удивительнее всего то, что люди, раскопав практически datasheet на «капризный» чип, остановились в шаге от решения проблемы. Я так и не нашел ни у кого рецепта, как правильно воспользоваться документацией (что и побудило меня написать этот пост). Но обо всем по-порядку.

PCIXXX21/PCIXXX11 Implementation Guide — документ о 117 страницах для проектировщиков аппаратуры на базе этих контроллеров. Подробно его разбирать смысла не имеет; самое важное, что я из него почерпнул: контроллер действительно реализует пять функций: CardBus, 1394, FlashMedia, SD Host и SmartCard; начальная конфигурация обычно берется из EEPROM. Главный регистр конфигурации — General Control Register (раздел 12.4.28, с. 65) — находится по адресам 1Eh-1Fh в ROM (нас интересует только нулевой байт, т.к. именно там маскируются функции чипа) и соотвествует PCI offset 86h нулевой функции устройства. Теперь —

За дело

Для начала посмотрим, что нам скажет утилита pciconf(8) про PCI configuration space «головной» (нулевой) функции чипа, т.е., в терминологии FreeBSD, селектора pci0:6:7:0. Ради краткости я не буду приводить дамп всех 256 байт, а ограничусь лишь интересующим нас, по смещению 86h):

# pciconf -rb pci0:6:7:0 0x86
d3

Интересно. Смотрим в табличку на 65-й странице pdf'ки, видим, что тройка в нижнем нибле (полубайте) равна типичному значению бит, отвечающих за top level arbitration, SmartCard socket power control и OHCI 1394, это нас мало интересует. А вот верхний нибл как раз маскирует (включает-выключает) логику остальных контроллеров (таблицу целиком не привожу опять же из-за экономии места):

scpu022a.pdf section 12.4.28

0xD это 1101, т.е. установлены биты DISABLE_SC, DISABLE_SD и DISABLE_SKTB, а бит DISABLE_FM сброшен. Следовательно, чтобы «оживить» контроллер SD Host, нам, по логике, надо сбросить DISABLE_SD (разрешить), а DISABLE_FM, напротив, выставить (запретить). Маске 1011 соответсвует значение 0xB, т.е. по сути, нам надо поменять байт 0xD3 на 0xB3. Проблема, однако, в том, что сделать это нужно сильно заранее, до того, как чип будет проинициализирован, вернее, до того, как он определит, какие контроллеры включать. После того, как система загрузилась, менять конфигурацию бесполезно: все устройства уже «в строю». И вот тут нам на помощь приходит

ACPI

Что такое ACPI и для чего оно нужно, я объяснять не стану; это выходит за рамки топика; к тому же, на Хабре уже был хороший пост на эту тему. В данном случае нам важен вопрос: можно ли пропатчить DSDT до инициализации чипа так, чтобы он включил нужный контроллер (SD Host) и выключил ненужный, для которого у нас нет драйвера (FlashMedia).

Посмотрим, какие у нас есть средства отладки и вывода информации с рамках интерпретатора («виртуальной машины») ACPI. В спецификации ACPI (параграф 19.5.25, с. 733) упоминается специальный объект Debug, который операционная система должна «донести» до пользователя. Во FreeBSD за это отвечает системная переменная debug.acpi.enable_debug_objects, которую нужно выставить в единицу:

# sysctl debug.acpi.enable_debug_objects=1
debug.acpi.enable_debug_objects: 0 -> 1

Теперь мы можем писать произвольные строчки в Debug, а ядро FreeBSD будет выводить их на консоль. Осталось придумать, как заставить ACPI выдавать интересующую нас информацию по требованию. Для начала сдампим и дизассемблируем DSDT ноута, и поизучаем его:

# acpidump -dt > s950.asl

Я решил найти метод, который вызывается через какое-либо внешнее воздействие (или внутреннее, но периодическое, типа опроса батарейки), при этом практически не затрагивая работу «железа». Изучая код DSDT, я наткнулся на любопытный кусок:

Method (_Q0C, 0, NotSerialized)
{
    If (_SB.PCI0.PEGA)
    {
        _SB.PCI0.PEGP.VGA.SWIH ()
    }
    Else
    {
        Store (0x01, TLST)
        HKDS (0x0A)
    }
}

Больше нигде метод _SB.PCI0.PEGP.VGA.SWIH не вызывается, а его название намекает, что это переключение дисплея. На клавиатуре многих ноутбуков есть клавиша F4, дополнительная Fn-функция которой как раз должна переключать видео-вывод с внутреннего дисплея на внешний. Попробуем модифицировать код метода следующим образом:

                     Method (_Q0C, 0, NotSerialized)
                     {
+                        Store ("Fn-F4 pressed", Debug)
                         If (_SB.PCI0.PEGA)
                         {

Пересоберем ASL:

# iasl s950-patched.asl
# cp /tmp/acpidump.aml /root/s950-patched.aml

Чтобы FreeBSD использовала нашу таблицу при загрузке, добавим в /boot/loader.conf следующие строчки:

acpi_dsdt_load="YES"
acpi_dsdt_name="/root/s950-patched.aml"

Если все сделано правильно и наш расчет оправдался, то при нажатии на Fn-F4 мы будем видеть на консоли сообщения ядра (повышенной яркости) о том, что клавиша Fn-F4 была нажата. Теперь, когда мы более-менее умеем взаимодействовать с ACPI, самое время попробовать

Достучаться до регистра 86h

Физические адресные пространства всевозможных устройств (оперативная память, порты ввода-вывода, платы расширения, CMOS, IPMI и пр.) отображаются в пространство имен ACPI в виде т.н. операционных регионов (OperationRegion), внутри которых обычно выделяются битовые поля (Field), состоящие из одного или нескольких поименованных «виртуальных регистров», или field units (параграф19.5.96, с. 782 спецификации). OperationRegion для нашего контроллера может выглядеть, например, так:

OperationRegion (PCIC, PCI_Config, 0x00, 256)
Field (PCIC, AnyAcc, NoLock, Preserve)
{
	Offset	(0x86),		// Global Configuration Register по смещению 86h
	TLA,	2,		// Top level arbitration
	SCSP,	1,		// SmartCard socket power
	OHCI,	1,		// Disable OHCI 1394 controller function
	SKTB,	1,		// Disable CardBus socket B
	FM,		1,		// Disable FlashMedia function
	SD,		1,		// Disable SD host controller function
	SC,		1,		// Disable SmartCard function
}

Или даже проще, если объявить не все 256 байт, а только интересующий нас, и не выделять отдельные биты в конфигурационном регистре:

OperationRegion (PCIC, PCI_Config, 0x86, 0x01)
Field (PCIC, AnyAcc, NoLock, Preserve)
{
	GCR0,	8,		// Global Configuration Register (first 8 bits)
}

Нетрудно заметить, что само по себе такое определение бесполезно: оно не привязано к какому-либо устройству, являясь по сути просто структурой из одного байта по известному смещению. Устройства в DSDT задаются через ключевое слово Device (параграф 19.5.30, с. 735); совокупность всех устройств представляет собой этакое дерево. Так, все PCI-устройства находятся чаще всего внутри пространства имен _SB.PCI0, которое соответствует корневой шине PCI (чаще всего такая шина одна, но теоретически их может быть до 256 в одном PCI-домене).

Для идентификации устройства на шине нужно задать его адрес в виде (device << 16) | function. В нашем случае (помните вывод pciconf -lv?) function = 0, device = 7, bus = 6. То есть устройство, видимо, должно выглядеть как-то так:

Device (XX11)
{
	Name (_ADR, 0x00070000)		// pci0:6:7:0
}

Хорошо, но откуда взялась шестая шина? И где она в DSDT? Посмотрим лог загрузки ядра (dmesg):

$ dmesg | grep pci6
pci6: <ACPI PCI bus> on pcib4
pci6: <bridge, PCI-CardBus> at device 7.0 (no driver attached)
pci6: <serial bus, FireWire> at device 7.2 (no driver attached)
pci6: <mass storage> at device 7.3 (no driver attached)
pci6: <network> at device 8.0 (no driver attached)
$ dmesg | grep pcib4
pcib4: <ACPI PCI-PCI bridge> at device 30.0 on pci0
pci6: <ACPI PCI bus> on pcib4

То есть, pci6 — это дополнительная, «виртуальная» шина на мосту PCI-PCI. Номер 6 (как и 4 для моста) ей достался потому, что FreeBSD так распределила устройства. Внутри DSDT никаких шести шин и четырех мостов, конечно, нет. Device (PCIB) там ровно один, как и ожидалось. Полностью наше описание должно выглядеть так (привожу краткий вариант, не раскладывая регистр на отдельные биты):

Scope (_SB.PCI0.PCIB) {
	Device (XX11)
	{
		Name (_ADR, 0x00070000)		// pci0:6:7:0
		OperationRegion (PCIC, PCI_Config, 0x86, 0x01)
		Field (PCIC, AnyAcc, NoLock, Preserve) { GCR0, 8, }
	}
}

Теперь заменим наш отладочный код в методе _Q0C на что-то более осмысленное:

                     Method (_Q0C, 0, NotSerialized)
                     {
+                        Store (Concatenate("GCR0 = 0x", _SB.PCI0.PCIB.XX11.GCR0), Debug)
                         If (_SB.PCI0.PEGA)
                         {

Перезагружаемся, жмем Fn-F4. Если мы все сделали верно, то должны увидеть то же самое значение, которое мы ранее читали через pciconf(8):

vanilla pciconf -lv

(Реализация функции для записи значения регистра напрямую в видеопамять оставляется читателю в качестве легкого упражнения.)

Нам остается ответить на самый главный вопрос: получится ли изменить значение регистра и заставить чип сконфигурировать себя так, как нам нужно?

Стандарт ACPI определяет специальный метод для инициализации устройств, _INI (параграф 6.5.1, с. 349). Добавим в наше устройство следующий код:

Method (_INI)
{
	Store (0xB3, GCR0)
	/* Альтернативный вариант для побитовой конфигурации:
	Store (0x01, FM)
	Store (0x00, SD) */
}

Компилируем ASL, копируем получившийся файл AML в /root/s950-patched.aml, снова перезагружаемся. Смотрим на

Результат

pciconf -lv after patch

Прежде всего, обратим внимание, что контроллер 0x8033104c (FlashMedia) исчез из вывода pciconf -lv, зато появился 0x8034104c (SD Host). Загружаем нужные модули ядра, вставляем карточку и пробуем примонтировать ее:

# kldload sdhci mmc mmcsd
# ls /dev/mmcsd0*
/dev/mmcsd0     /dev/mmcsd0s1
# mount -t msdosfs /dev/mmcsd0s1 /mnt/tmp
# ls /mnt/tmp/DCIM/100CANON
IMG_0403.JPG    IMG_0424.JPG    IMG_0450.JPG    IMG_0494.JPG    IMG_0515.JPG
IMG_0406.JPG    IMG_0425.JPG    IMG_0451.JPG    IMG_0498.JPG    IMG_0517.JPG
IMG_0407.JPG    IMG_0427.JPG    IMG_0452.JPG    IMG_0499.JPG    IMG_0518.JPG
IMG_0409.JPG    IMG_0429.JPG    IMG_0453.JPG    IMG_0500.JPG    IMG_0520.JPG
IMG_0410.JPG    IMG_0430.JPG    IMG_0467.JPG    IMG_0501.JPG    IMG_0522.JPG
IMG_0412.JPG    IMG_0439.JPG    IMG_0473.JPG    IMG_0506.JPG    IMG_0525.JPG
IMG_0413.JPG    IMG_0440.JPG    IMG_0474.JPG    IMG_0507.JPG    IMG_0526.JPG
IMG_0414.JPG    IMG_0445.JPG    IMG_0475.JPG    IMG_0508.JPG    IMG_0534.JPG
IMG_0415.JPG    IMG_0447.JPG    IMG_0478.JPG    IMG_0510.JPG    IMG_0535.JPG
IMG_0421.JPG    IMG_0448.JPG    IMG_0492.JPG    IMG_0512.JPG    IMG_0537.JPG
IMG_0423.JPG    IMG_0449.JPG    IMG_0493.JPG    IMG_0514.JPG    IMG_0538.JPG
# tar cf /dev/null /mnt/tmp/DCIM/100CANON ; echo $?
tar: Removing leading '/' from member names
0

Вроде все работает, ну и славно. Можно убирать отладочный код из DSDT и наслаждаться жизнью пользоваться кард-ридером.

Автор: danfe


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js