Постановка задачи


В 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

По данной ссылке есть 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 файл

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


  1. a1ien_n3t
    10.11.2018 01:44

    Если что, то работа с gpio через /sys/class/gpio deprecated.
    Всесто него теперь рекомендуется использовать gpio uapi, статья на хабре