Постановка задачи
В Linux есть стандартный интерфейс для работы с GPIO через sysfs. Документацию на него можно посмотреть тут.
Если кратко, то в папке "/sys/class/gpio" есть файлы «export» и «unexport». С помощью записи числа X в файл export можно открыть интерфейс в user space для управления GPIOX
# открыть интерфейс в user space для управления GPIO12
$ echo 12 > /sys/class/gpio/export
После открытия интерфейса появится папка /sys/class/gpio/gpioX/ в которой будут такие файлы как «value» или «direction», и путём записи «in» или «out» в файл «direction» и записи 1 или 0 в файл «value» можно управлять выводом GPIO напрямую через командную строку.
# настроить GPIO на вывод
$ echo "out" > /sys/class/gpio/gpio12/direction
# установить 1 на выводе GPIO
$ echo 1 > /sys/class/gpio/gpio12/value
Чтобы команда «echo X > /sys/class/gpio/export» приводила к созданию папки «gpioX», в ядре должен быть зарегистрирован драйвер контроллера GPIO, открывающий интерфейс к линиям GPIO.
Так получилось, что я работаю над портированием coreboot для кастомной платы на базе процессора Intel Haswell i7 [Для тех, кто не знает, coreboot это open source проект по созданию BIOS с открытым исходным кодом (https://www.coreboot.org/)]. В мой процессор встроен южный мост LynxpointLP в котором есть 94 линии GPIO. И я захотел открыть их в sysfs…
Решение задачи (связь драйвера и устройства в Linux)
Выполнив небольшой поиск по коду ядра, я обнаружил, что данный драйвер уже написан, находится в файле «drivers\gpio\gpio-lynxpoint.c» и включается с помощью Kconfig
config GPIO_LYNXPOINT
tristate "Intel Lynxpoint GPIO support"
depends on ACPI && X86
select GPIOLIB_IRQCHIP
help
driver for GPIO functionality on Intel Lynxpoint PCH chipset
Requires ACPI device enumeration code to set up a platform device.
Опция GPIO_LYNXPOINT была включена в ядре с которым я работал, однако в папке "/sys/class/gpio/" не было ни одной папки «gpiochipN» для GPIO контроллера (что должно быть), и даже такой скрипт не приводил к экспорту ни одной линии.
$ for i in {0..255}; do echo $i > /sys/class/gpio/export; done
Посмотрев код coreboot или изучив документацию для данного южного моста можно понять, что GPIO контроллер не является отдельным PCI устройством. Он входит в состав другого PCI устройства: LPC Interface Bridge. С помощью регистров конфигурационного пространства PCI этого устройства необходимо включить GPIO controller и назначить ему BASE_ADDRESS в пространстве I/O. Это откроет окно в I/O пространстве размером 1КВ. Записывая/считывая байты в данном окне, можно управлять линиями GPIO.
Что мы и можем увидеть в коде coreboot:
southbridge\intel\lynxpoint\pch.h:
#define DEFAULT_GPIOBASE 0x1400
#define DEFAULT_GPIOSIZE 0x400
...
#define GPIO_BASE 0x48 /* LPC GPIO Base Address Register */
#define GPIO_CNTL 0x4C /* LPC GPIO Control Register */
...
/* PCI Configuration Space (D31:F0): LPC */
#define PCH_LPC_DEV PCI_DEV(0, 0x1f, 0)
southbridge\intel\lynxpoint\early_pch.c:
/* Setup GPIO Base Address */
pci_write_config32(PCH_LPC_DEV, GPIO_BASE, DEFAULT_GPIOBASE|1);
/* Enable GPIO functionality. */
pci_write_config8(PCH_LPC_DEV, GPIO_CNTL, 0x10);
Если мы посмотрим регистры устройства LPC в Linux через «lspci -xxx», то увидим что в данных регистрах находится записанная нами информация. Так что вроде бы всё настраивается как надо.
Продолжая смотреть код драйвера я обратил внимание, что связь Linux драйвера с устройством осуществляется через поле .acpi_match_table. Так как наше устройство не поддаётся энумерации (оно ведь не расположено ни на PCI, ни на USB шине), то для него необходим platform driver, а связь этого драйвера с устройством осуществляется через ACPI таблицы. Обычный случай для x86, в случае с ARM мы бы прописывали наше устройство в DeviceTree, или по старому хардкодили в ядро.
drivers\gpio\gpio-lynxpoint.c:
static const struct acpi_device_id lynxpoint_gpio_acpi_match[] = {
{ "INT33C7", 0 },
{ "INT3437", 0 },
{ }
};
MODULE_DEVICE_TABLE(acpi, lynxpoint_gpio_acpi_match);
static struct platform_driver lp_gpio_driver = {
.probe = lp_gpio_probe,
.remove = lp_gpio_remove,
.driver = {
.name = "lp_gpio",
.pm = &lp_gpio_pm_ops,
.acpi_match_table = ACPI_PTR(lynxpoint_gpio_acpi_match),
},
};
Это работает так: если ядро при парсинге ACPI таблицы увидит в ней устройство с _HID идентификатором «INT33C7», то оно будет пытаться найти platform driver для него с совпадающими идентификаторами в полях структуры ".driver->acpi_match_table".
При найденном совпадении Linux выполнит .probe функцию драйвера.
Как оказалось ACPI код для данного устройства был представлен в coreboot, просто у меня он был закомментирован. Закомментирован по причине того, что для данного устройства Windows не мог найти драйвер и выводил «Неизвестное устройство» в диспетчере устройств. Подробнее об этом будет ниже.
Итак нас интересует информация из файла
src\southbridge\intel\lynxpoint\acpi\serialio.asl (код немного упрощён):
/* Несколько констант из файла
* src\southbridge\intel\lynxpoint\pch.h
* #define DEFAULT_GPIOBASE 0x1400
* #define DEFAULT_GPIOSIZE 0x400
*/
Scope (\_SB) {
Device (PCI0)
{
...
Device (GPIO)
{
// GPIO Controller
Name (_HID, "INT33C7")
Name (_CID, "INT33C7")
Name (_UID, 1)
Name (RBUF, ResourceTemplate()
{
DWordIo (ResourceProducer,
MinFixed, // IsMinFixed
MaxFixed, // IsMaxFixed
PosDecode, // Decode
EntireRange, // ISARanges
0x00000000, // AddressGranularity
0x00000000, // AddressMinimum
0x00000000, // AddressMaximum
0x00000000, // AddressTranslation
0x00000001, // RangeLength
, // ResourceSourceIndex
, // ResourceSource
BAR0)
Interrupt (ResourceConsumer,
Level, ActiveHigh, Shared, , , ) {14}
})
Method (_CRS, 0, NotSerialized)
{
CreateDwordField (^RBUF, ^BAR0._MIN, BMIN)
CreateDwordField (^RBUF, ^BAR0._MAX, BMAX)
CreateDwordField (^RBUF, ^BAR0._LEN, BLEN)
Store (DEFAULT_GPIOSIZE, BLEN)
Store (DEFAULT_GPIOBASE, BMIN)
Store (Subtract (Add (DEFAULT_GPIOBASE,
DEFAULT_GPIOSIZE), 1), BMAX)
Return (RBUF)
}
Method (_STA, 0, NotSerialized)
{
Return (0xF)
}
}
...
}
}
Чтобы подробно разобрать данный код следует ознакомиться с синтаксисом языка ASL в спецификации ACPI.
Но если коротко, то данный код создаёт устройство с идентификатором «INT33C7» у которого есть 2 ресурса:
I/O memory: 1400-17ff;
IRQ: 14;
Внутри своей функции .probe Linux драйвер получает вышеобозначенные ресурсы устройства так:
io_rc = platform_get_resource(pdev, IORESOURCE_IO, 0);
irq_rc = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
На основании этих данных код драйвера заполнит структуру gpio_chip и зарегистрирует gpio controller в системе, что сделает его доступным через интерфейс sysfs.
Вернув ASL код устройства и перекомпилировав образ BIOS, в системе удалось получить доступ к GPIO через sysfs.
Для начала в /sys/class/gpio появилась папка «gpiochip162». В данной папке содержится файл «base» и «ngpio». Файл base отвечает за номер первого GPIO данного контроллера, ngpio за их количество.
$ cat /sys/class/gpio/gpiochip162/base
162
$ cat /sys/class/gpio/gpiochip162/ngpio
94
Таким образом всё экспортировалось как надо. Выполняем скрипт:
$ for i in {162..255}; do echo $i > /sys/class/gpio/export; done
После этого в папке /sys/class/gpio/ появятся подпапки gpioN, внутри которых будут файлы для управления состоянием линии.
Пара замечаний:
- За управление GPIO0 отвечает папка /sys/class/gpio162/, за GPIO1 отвечает папка /sys/class/gpio163/ и т.д. Такой сдвиг произошёл из-за того, что драйвер в процессе инициализации управляющей структуры «struct gpio_chip» назначил «gc->base = -1;». То есть предоставил ядру самому выбрать номера. Это в целом не критично, но стоит помнить об этом.
- Доступ открывается только к линиям GPIO, которые настроены как GPIO, а не как какие-либо native функции южного моста. Для таких линий драйвер выводит информацию в dmesg: «gpio %d reserved for ACPI». В случае coreboot настройка GPIO производится в файле «gpio.h» в папке с материнской платой.
- Сопоставление устройства и драйвера может идти также по методу _CID (Compatible ID), а документация на нашу тему в ядре представлена в документе «ACPI based device enumeration»
Стоит отметить, что устройства «INT33C7» нет в ACPI таблицах 2 проприетарных материнских плат на этом же чипсете (от компаний IBASE и DFI). Правда там скорее всего GPIO линии и не выведены (подробно документацию на этот момент я не смотрел).
Идентификатор «INT33C7»
После поднятия функционала sysfs у меня возник вопрос, откуда вообще взялся идентификационный номер «INT33C7»?
Посмотрев документацию на метод _HID стало понятно, что стоит смотреть в http://www.uefi.org/PNP_ACPI_Registry
_HID (Hardware ID)
_HID (Hardware ID)
This object is used to supply OSPM with the device’s PNP ID or ACPI ID*
When describing a platform, use of any _HID objects is optional. However, a _HID object must be
used to describe any device that will be enumerated by OSPM. OSPM only enumerates a device
when no bus enumerator can detect the device ID. For example, devices on an ISA bus are
enumerated by OSPM. Use the _ADR object to describe devices enumerated by bus enumerators
other than OSPM.
Arguments:
None
Return Value:
An Integer or String containing the HID
A _HID object evaluates to either a numeric 32-bit compressed EISA type ID or a string. If a
string, the format must be an alphanumeric PNP or ACPI ID with no asterisk or other leading
characters.
A valid PNP ID must be of the form «AAA####» where A is an uppercase letter and # is a hex
digit. A valid ACPI ID must be of the form «NNNN####» where N is an uppercase letter or a
digit ('0'-'9') and # is a hex digit. This specification reserves the string «ACPI» for use only
with devices defined herein. It further reserves all strings representing 4 HEX digits for
exclusive use with PCI-assigned Vendor IDs.
*-PNP ID and ACPI ID Registry is at http://www.uefi.org/PNP_ACPI_Registry
This object is used to supply OSPM with the device’s PNP ID or ACPI ID*
When describing a platform, use of any _HID objects is optional. However, a _HID object must be
used to describe any device that will be enumerated by OSPM. OSPM only enumerates a device
when no bus enumerator can detect the device ID. For example, devices on an ISA bus are
enumerated by OSPM. Use the _ADR object to describe devices enumerated by bus enumerators
other than OSPM.
Arguments:
None
Return Value:
An Integer or String containing the HID
A _HID object evaluates to either a numeric 32-bit compressed EISA type ID or a string. If a
string, the format must be an alphanumeric PNP or ACPI ID with no asterisk or other leading
characters.
A valid PNP ID must be of the form «AAA####» where A is an uppercase letter and # is a hex
digit. A valid ACPI ID must be of the form «NNNN####» where N is an uppercase letter or a
digit ('0'-'9') and # is a hex digit. This specification reserves the string «ACPI» for use only
with devices defined herein. It further reserves all strings representing 4 HEX digits for
exclusive use with PCI-assigned Vendor IDs.
*-PNP ID and ACPI ID Registry is at http://www.uefi.org/PNP_ACPI_Registry
По данной ссылке есть 3 пункта:
- все виды 3 буквенных идентификаторов (PNP ID) обозначены тут
- идентификаторы PNP ID начинающиеся с «PNP» зарезервиранные Microsoft обозначены тут
- все виды 4 буквенных идентификаторов (ACPI ID) обозначены тут
Не очень понятно почему, но по списку PNP ID можно найти, что идентификаторы «INT» зарезервированы на INTERPHASE CORPORATION:
INTERPHASE CORPORATION INT 11/29/1996
Судя по всему единый список полных идентификаторов устройств (буквенная часть+цифровая) не публикуется. Но с помощью гугла удалось найти списки устройств и их _HID например тут или тут.
В них указано:
INT33C7=Intel Serial I/O GPIO Host Controller
И судя по остальным строкам из этого списка все устройства «INTxxxx» — это устройства Intel (теперь это звучит достаточно очевидно, но связь с INTERPHASE CORPORATION так и не понятна; также не очень ясно почему нумерация начинается с таких больших чисел, но это видимо на усмотрение корпорации Intel).
Связь драйвера и устройства в Windows
Удовлетворив своё любопытство я решил загрузить на своей плате Windows. Как и ожидалось, система не смогла найти драйвер для устройства. Помощи от драйверов для плат IBASE и DFI не было, что и понятно, ведь в BIOS этих плат это устройство и не обозначено.
Удалось найти драйвер на сайте Microsoft
Однако там данный драйвер представлен только для Windows 8.1 и выше. Я же всё ещё работаю с Windows 7.
Тем не менее я попытался скачать один из драйверов и указать его папку при поиске драйвера для моего неизвестного устройства.
Однако диспетчер не смог сопоставить драйвер устройству. Хотя в inf файле явно содержалась информация об устройстве INT33C7.
[Manufacturer]
%INTEL%=Intel,NTamd64.6.3
[Intel.NTamd64.6.3]
%iaLPSS_GPIO.DeviceDesc_LPT%=iaLPSS_GPIO_Device, ACPI\INT33C7
%iaLPSS_GPIO.DeviceDesc_WPT%=iaLPSS_GPIO_Device, ACPI\INT3437
В процессе разбора INF файла выяснилось, что в секции [Manufacturer] явно указано, что он предназначен не для моей системы:
Что значит Intel.NTamd64.6.3 можно понять по описанию:
nt[Architecture][.[OSMajorVersion][.[OSMinorVersion]
OSMajorVersion=6 => Windows 7/Windows 8.1/Windows Server 2012 R2/...
OSMinorVersion=3 => Windows 8.1/Windows Server 2012 R2
Попытаться подпихнуть драйвер Windows 7 путём замены Intel.NTamd64.6.3 на Intel.NTamd64.6.1 мягко говоря не удалось, так как это дало мне синий экран смерти и незагружаемую ОС, причём так, что пришлось делать восстановление.
Драйвер для Win7 удалось найти только на непонятном сайте в интернете, и то после этого устройство в диспетчере устройств отображается с восклицательным знаком.
Осознав своё бессилие, решил проверить функциональность на Windows 10. Тут ждал приятный сюрприз. «Intel Chipset Device Software (INF Update Utility)» без каких-либо проблем установило драйвер для моего контроллера.
Как видно, у данного устройства обозначенные нами ресурсы
В теории после установки драйвера с GPIO controller можно будет скорее всего работать через IOCTL функции (типа как в данном документе).
Однако задачи программирования GPIO из Windows не стояло, поэтому поиск аналогичного документа для моего чипсета был отложен.
Заключение:
В данной статье была рассмотрена связь драйвера и устройства по _HID методу ACPI. Такая связь может потребоваться на x86 системе для устройств не поддающихся энумерации.
- В случае Linux связь с драйвером осуществляется через .acpi_match_table
- В случае Windows связь с драйвером осуществляется через INF файл
a1ien_n3t
Если что, то работа с gpio через /sys/class/gpio deprecated.
Всесто него теперь рекомендуется использовать gpio uapi, статья на хабре