Устройство GPIO-драйверов в Linux

Рассмотрим, как именно устроены GPIO-драйверы в Linux, и почему это сделано именно так. Поймем, почему для простого мигания светодиодом в этой операционной системе надо пройти через N слоев абстракции.

Большая часть сказанного будет актуальна и для других драйверов. GPIO выбран как самый простой пример периферийного устройства.

Disclaimer

Статья не претендует на академичность и может содержать неточности. Автор исходит из подхода «лучше статья с неточностями сейчас поможет кому-то, чем абсолютно точная статья не поможет никому никогда». Замечания по существу приветствуются. Обнаруженные неточности будут исправлены.

Выстроим статью таким образом:

  • рассмотрим аппаратный уровень управления GPIO;

  • изучим, какие требования добавляет нам операционная система;

  • рассмотрим, как эти требования реализуются в общем виде;

  • посмотрим, как реализованы реальные драйверы блоков GPIO в Linux для различных SoC;

  • еще раз кратко подытожим результаты изучения.

Аппаратный уровень

На аппаратном уровне все очень просто. Так или иначе в процессоре есть контроллер выводов GPIO, который с точки зрения программиста представляет из себя десяток-другой регистров, отображенных на общее адресное пространство. Чтобы зажечь светодиод, нам нужно сначала инициализировать нужный вывод (настроить его как выход), а потом задать нужное значение. В общем-то, две записи в регистры. Одна запись — в регистр, определяющий направление вывода. Вторая — в регистр, определяющий значение на выходе. Можно уложиться в десяток инструкций CPU.

Ограничения, вносимые операционной системой

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

Доступ к периферии из режима ядра и из режима пользователя

Приложения, выполняемые в пространстве пользователя, не имеют доступа к периферийным регистрам и прочим служебным областям памяти. Это одно из базовых требований ОС типа Linux — пользовательские процессы не должны иметь возможности испортить память других процессов, а особенно — память ядра. Значит, необходим драйвер, работающий в пространстве ядра, и предоставляющий API для приложений пользовательского режима. Выводами GPIO придется управлять и из ядра (например, из соседних драйверов) — то есть, нужно API и для режима ядра.

Разделение доступа

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

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

Стандартное API

Существует большое количество реализаций портов GPIO, интегрированных в большое количество SoC. Независимо от конкретной реализации, драйвер должен предоставлять одинаковый набор «ручек» (иными словами, одинаковое API). Благодаря этому:

  • приложения пользовательского пространства получают дополнительный уровень платформенной независимости — на любой платформе с Linux выводы GPIO управляются одинаково;

  • аналогичную степень свободы получают драйверы, использующие в своей работе выводы GPIO (например, драйвер тач-контроллера или модуля Wifi может использовать вывод GPIO для управления питанием).

Также у большинства SoC возможно использование выводов GPIO для получения внешних прерываний. Эта задача тоже решается в Linux, но у прерываний в Linux свои абстракции, которые имеет смысл изучать отдельно. Поэтому, хотя в процессе рассмотрения нам и будет встречаться код, работающий с прерываниями, мы не будем о нем говорить в этой статье.

Реализация общих требований к драйверу GPIO

Доступ к периферии из пространства ядра и пользователя

Тут все просто. Драйвер — это обычный модуль ядра Linux, он выполняется, соответственно, в пространстве ядра и имеет доступ ко всему адресному пространству CPU.

Для приложений пользовательского уровня предоставляются специальные файлы в sysfs.

API для пользовательских приложений

Это стандартный для Linux-систем механизм — через файлы в /sys/class/gpio. Когда мы узнали номер нужного нам вывода в системе (он чаще всего не совпадает с номером физического вывода) — мы пишем этот номер в файл /sys/class/gpio/export:

num=416
echo $num > /sys/class/gpio/export

После этого в файловой системе появляется директория /sys/class/gpio/gpio$num. Внутри этой директории есть файлы direction и value.

Для управления направлением вывода:

echo out > /sys/class/gpio/gpio$num/direction # назначить выходом
echo in > /sys/class/gpio/gpio$num/direction # назначить входом

Для управления состоянием выхода:

echo 1 > /sys/class/gpio/gpio$num/value
echo 0 > /sys/class/gpio/gpio$num/value

Для чтения состояния на входе:

cat /sys/class/gpio/gpio$num/value

Как это реализовано — рассмотрим чуть позже.

Разделение доступа для пользовательских приложений

Если в одну и ту же «ручку» будут стучаться несколько приложений — система сама разрешит коллизии. Так же, как она сделает это для случая, когда несколько приложений захотят одновременный доступ к одному и тому же файлу. Эти коллизии не требуют проработки на уровне драйвера.

API для ядра и разделение доступа в ядре

В этом разделе будет рассмотрен главным образом платформенно-независимый код из ядра Linux. Код рассмотрен на примере ветки master репозитория https://github.com/torvalds/linux, то есть на момент публикации статьи соответствует ядру версии 6.x. Конкретные реализации драйверов далее будут рассмотрены на примере ядер 4.x и 5.x. Между версиями ядер есть отличия, но общие концепции не поменялись.

В ядре Linux есть платформенно-независимая абстракция gpio_chip, представляющая собой структуру, содержащую в себе описание некоторого набора выводов GPIO. Отдельной структуры, описывающей отдельный вывод, и содержащей функции для управления этим отдельным выводом, не выделяется.

Отступление о том, как это коррелирует с аппаратным уровнем. На аппаратном уровне обычно в микросхемах (SoC — system-on-a-chip) есть несколько блоков GPIO, каждый из которых содержит в себе несколько десятков выводов. Часто используют обозначения типа GPIOA, GPIOB, etc. Соответственно, выводы обозначаются в формате GPIOA_31…GPIOA_0, и т.п.
В зависимости от конкретной реализации микросхемы и драйвера, могут быть реализованы разные подходы. На каждый блок может заводиться отдельная структура gpio_chip. Но может быть реализован и другой подход. Например, блоки GPIO могут быть разделены по доменам питания. Условно, GPIOA…GPIOC — в одном домене, а GPIOD…GPIOF — в другом. В этом случае вендор в предоставляемом SDK формирует один gpio_chip для блоков одного домена, и второй gpio_chip для блоков второго домена.

В целом, разбиение на gpio_chip не слишком важно с технической точки зрения. В основном тут все зависит от чувства прекрасного у автора конкретного драйвера.

Так или иначе, внутри структуры gpio_chip, помимо прочего, есть указатели на платформенно-зависимые функции, реализованные в драйвере конкретного блока GPIO:

https://github.com/torvalds/linux/blob/master/include/linux/gpio/driver.h#L417

	int			(*request)(struct gpio_chip *chip,
						unsigned offset);
	void			(*free)(struct gpio_chip *chip,
						unsigned offset);
	int			(*get_direction)(struct gpio_chip *chip,
						unsigned offset);
	int			(*direction_input)(struct gpio_chip *chip,
						unsigned offset);
	int			(*direction_output)(struct gpio_chip *chip,
						unsigned offset, int value);
	…
	void			(*set)(struct gpio_chip *chip,
						unsigned offset, int value);

Помимо структуры gpio_chip, в ядре есть платформенно-независимые функции для работы с gpio. Они сосредоточены в файлах drivers/gpio/gliolib.c и drivers/gpio/gpiolib_sysfs.c. Посмотрим на эти файлы.

В первом — как раз функции для ядра. Их много, сосредоточимся на двух:

int gpiod_request(struct gpio_desc *desc, const char *label);
void gpiod_set_value(struct gpio_desc *desc, int value);

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

Именно внутри функции gpiod_request () реализован механизм разделения доступа. Конечно, не прямо в ней, а как это принято — через пару слоев «под» ней:

https://github.com/torvalds/linux/blob/master/drivers/gpio/gpiolib.c#L2280

int gpiod_request(struct gpio_desc *desc, const char *label)
{
	int ret = -EPROBE_DEFER;

	VALIDATE_DESC(desc);

	if (try_module_get(desc->gdev->owner)) {
		ret = gpiod_request_commit(desc, label);
		if (ret)
			module_put(desc->gdev->owner);
		else
			gpio_device_get(desc->gdev);
	}

	if (ret)
		gpiod_dbg(desc, "%s: status %d\n", __func__, ret);

	return ret;
}

https://github.com/torvalds/linux/blob/master/drivers/gpio/gpiolib.c#L2181

static int gpiod_request_commit(struct gpio_desc *desc, const char *label)
{
	struct gpio_chip *gc = desc->gdev->chip;
	unsigned long flags;
	unsigned int offset;
	int ret;

	if (label) {
		label = kstrdup_const(label, GFP_KERNEL);
		if (!label)
			return -ENOMEM;
	}

	spin_lock_irqsave(&gpio_lock, flags);

	/* NOTE:  gpio_request() can be called in early boot,
	 * before IRQs are enabled, for non-sleeping (SOC) GPIOs.
	 */

	if (test_and_set_bit(FLAG_REQUESTED, &desc->flags) == 0) {
		desc_set_label(desc, label ? : "?");
	} else {
		ret = -EBUSY;
		goto out_free_unlock;
	}

	if (gc->request) {
		/* gc->request may sleep */
		spin_unlock_irqrestore(&gpio_lock, flags);
		offset = gpio_chip_hwgpio(desc);
		if (gpiochip_line_is_valid(gc, offset))
			ret = gc->request(gc, offset);
		else
			ret = -EINVAL;
		spin_lock_irqsave(&gpio_lock, flags);

		if (ret) {
			desc_set_label(desc, NULL);
			clear_bit(FLAG_REQUESTED, &desc->flags);
			goto out_free_unlock;
		}
	}
	if (gc->get_direction) {
		/* gc->get_direction may sleep */
		spin_unlock_irqrestore(&gpio_lock, flags);
		gpiod_get_direction(desc);
		spin_lock_irqsave(&gpio_lock, flags);
	}
	spin_unlock_irqrestore(&gpio_lock, flags);
	return 0;

out_free_unlock:
	spin_unlock_irqrestore(&gpio_lock, flags);
	kfree_const(label);
	return ret;
}

Основные моменты тут:

1) оборачиваем в spinlock и смотрим — не находится ли этот вывод уже в статусе запрошенного:

spin_lock_irqsave(&gpio_lock, flags);

	/* NOTE:  gpio_request() can be called in early boot,
	 * before IRQs are enabled, for non-sleeping (SOC) GPIOs.
	 */

	if (test_and_set_bit(FLAG_REQUESTED, &desc->flags) == 0) {
		desc_set_label(desc, label ? : "?");
	} else {
		ret = -EBUSY;
		goto out_free_unlock;
	}

2) если вывод не занят, то проверяем — есть ли у него своя платформенно-зависимая функция request (), и вызываем ее:

	if (gc->request) {
		/* gc->request may sleep */
		spin_unlock_irqrestore(&gpio_lock, flags);
		offset = gpio_chip_hwgpio(desc);
		if (gpiochip_line_is_valid(gc, offset))
			ret = gc->request(gc, offset);
		else
			ret = -EINVAL;
		spin_lock_irqsave(&gpio_lock, flags);

Видно, что вызов этой функции мы производим при разлоченном spinlock. Сложно поручиться на все 100%, но выглядит так, что spinlock мы разлочили, потому что здесь мы выше уже исключили одновременный доступ к конкретному GPIO, а держать spinlock залоченным рекомендуется как можно меньше времени, чтобы минимизировать активное ожидание других процессов/потоков.

Теперь посмотрим, как выглядит установка заданного значения:

https://github.com/torvalds/linux/blob/master/drivers/gpio/gpiolib.c#L3415

void gpiod_set_value(struct gpio_desc *desc, int value)
{
	VALIDATE_DESC_VOID(desc);
	/* Should be using gpiod_set_value_cansleep() */
	WARN_ON(desc->gdev->chip->can_sleep);
	gpiod_set_value_nocheck(desc, value);
}
EXPORT_SYMBOL_GPL(gpiod_set_value);

https://github.com/torvalds/linux/blob/master/drivers/gpio/gpiolib.c#L3392

static void gpiod_set_value_nocheck(struct gpio_desc *desc, int value)
{
	if (test_bit(FLAG_ACTIVE_LOW, &desc->flags))
		value = !value;
	if (test_bit(FLAG_OPEN_DRAIN, &desc->flags))
		gpio_set_open_drain_value_commit(desc, value);
	else if (test_bit(FLAG_OPEN_SOURCE, &desc->flags))
		gpio_set_open_source_value_commit(desc, value);
	else
		https://github.com/torvalds/linux/blob/master/drivers/gpio/gpiolib.c#L3221(desc, value);
}

https://github.com/torvalds/linux/blob/master/drivers/gpio/gpiolib.c#L3221

static void gpiod_set_raw_value_commit(struct gpio_desc *desc, bool value)
{
	struct gpio_chip *gc;

	gc = desc->gdev->chip;
	trace_gpio_value(desc_to_gpio(desc), 0, value);
	gc->set(gc, gpio_chip_hwgpio(desc), value);
}

Видно, что в конечном счете вызывается платформенно-зависимая функция set(). Функция установки значения уже не использует никаких механизмов разделения доступа. Если какой-то код в ядре хочет использовать вывод GPIO, но получил ошибку при вызове gpiod_request(), он не должен пытаться далее использовать этот GPIO.

Теперь рассмотрим реализацию вызовов из пространства пользователя, через sysfs. Вот как выглядит функция, обрабатывающая запись в файл /sys/class/gpio/export:

https://github.com/torvalds/linux/blob/master/drivers/gpio/gpiolib-sysfs.c#L441

static ssize_t export_store(const struct class *class,
				const struct class_attribute *attr,
				const char *buf, size_t len)
{
	struct gpio_desc *desc;
	struct gpio_chip *gc;
	int status, offset;
	long gpio;

	status = kstrtol(buf, 0, &gpio);
	if (status < 0)
		goto done;

	desc = gpio_to_desc(gpio);
	/* reject invalid GPIOs */
	if (!desc) {
		pr_warn("%s: invalid GPIO %ld\n", __func__, gpio);
		return -EINVAL;
	}
	gc = desc->gdev->chip;
	offset = gpio_chip_hwgpio(desc);
	if (!gpiochip_line_is_valid(gc, offset)) {
		pr_warn("%s: GPIO %ld masked\n", __func__, gpio);
		return -EINVAL;
	}

	/* No extra locking here; FLAG_SYSFS just signifies that the
	 * request and export were done by on behalf of userspace, so
	 * they may be undone on its behalf too.
	 */

	status = gpiod_request_user(desc, "sysfs");
	if (status)
		goto done;

	status = gpiod_set_transitory(desc, false);
	if (status) {
		gpiod_free(desc);
		goto done;
	}

	status = gpiod_export(desc, true);
	if (status < 0)
		gpiod_free(desc);
	else
		set_bit(FLAG_SYSFS, &desc->flags);

done:
	if (status)
		pr_debug("%s: status %d\n", __func__, status);
	return status ? : len;
}
static CLASS_ATTR_WO(export);

Главное здесь — это вызов gpiod_request_user(). Который, если поискать по ветке master, сведется к:

https://github.com/torvalds/linux/blob/99bd3cb0d12e85d5114425353552121ec8f93adc/drivers/gpio/gpiolib.h#L194

static inline int gpiod_request_user(struct gpio_desc *desc, const char *label)
{
	int ret;

	ret = gpiod_request(desc, label);
	if (ret == -EPROBE_DEFER)
		ret = -ENODEV;

	return ret;
}

В ядрах 4.x, 5.x еще нет никакого gpiod_request_user(), используется напрямую обычный gpiod_request().

А вот что под капотом у записи значения выхода GPIO:

https://github.com/torvalds/linux/blob/master/drivers/gpio/gpiolib-sysfs.c#L131

static ssize_t value_store(struct device *dev,
		struct device_attribute *attr, const char *buf, size_t size)
{
	struct gpiod_data *data = dev_get_drvdata(dev);
	struct gpio_desc *desc = data->desc;
	ssize_t status;
	long value;

	status = kstrtol(buf, 0, &value);

	mutex_lock(&data->mutex);

	if (!test_bit(FLAG_IS_OUT, &desc->flags)) {
		status = -EPERM;
	} else if (status == 0) {
		gpiod_set_value_cansleep(desc, value);
		status = size;
	}

	mutex_unlock(&data->mutex);

	return status;
}
static DEVICE_ATTR_PREALLOC(value, S_IWUSR | S_IRUGO, value_show, value_store);

https://github.com/torvalds/linux/blob/master/drivers/gpio/gpiolib.c#L3843

void gpiod_set_value_cansleep(struct gpio_desc *desc, int value)
{
	might_sleep();
	VALIDATE_DESC_VOID(desc);
	gpiod_set_value_nocheck(desc, value);
}
EXPORT_SYMBOL_GPL(gpiod_set_value_cansleep);

https://github.com/torvalds/linux/blob/master/drivers/gpio/gpiolib.c#L3392

static void gpiod_set_value_nocheck(struct gpio_desc *desc, int value)
{
	if (test_bit(FLAG_ACTIVE_LOW, &desc->flags))
		value = !value;
	if (test_bit(FLAG_OPEN_DRAIN, &desc->flags))
		gpio_set_open_drain_value_commit(desc, value);
	else if (test_bit(FLAG_OPEN_SOURCE, &desc->flags))
		gpio_set_open_source_value_commit(desc, value);
	else
		gpiod_set_raw_value_commit(desc, value);
}

А это, как мы знаем, приведет к уже известной нам платформенно-зависимой функции set ().

Реализация конкретных драйверов

Посмотрим, как все вышесказанное реализовано на реальном железе.

Сетап для исследования

Мы использовали платы BeagleBone Black (SoC TI AM335) и Orange Pi Zero (SoC Allwinner H2). В качестве «обертки» для ядра — Buildroot 2021.02. В этом сетапе нет ничего сакрального, это просто то, что было под рукой и с чем более-менее привычно работать автору. Две платы использовались, чтобы посмотреть реализацию драйвера GPIO у двух вендоров — TI и Allwinner. Рассмотрели бы и третью, но ничего другого под рукой не оказалось.

Buildroot для платы BeagleBone использует ядро версии 4.19.79. Buildroot для OrangePi Zero использует ядро версии 5.10.10. Как уже говорилось, современные версии ядра имеют отличия от 4.19 и 5.10 в деталях, но общий подход не менялся.

BeagleBone Black (TI AM335)

Запускаем собранный образ Buildroot и сохраняем вывод UART. Отправной точкой для поиска возьмем такую строчку:

  [    0.268291] OMAP GPIO hardware version 0.1

Лирическое отступление про поиск кода

Здесь, конечно, присутствует некоторая присущая автору манера идти не совсем прямым путем. Более академично и правильно было бы пойти в DTS для данной платы, найти там DTSI для использованного процессора, и уже там найти описание блока GPIO, в котором будет поле compatible. Из этого поля уже можно найти правильный драйвер. Но с точки зрения результата оба подхода примерно равнозначны, поэтому выбор зависит от личных предпочтений. Спорить о подходах тут не будем, оговорка просто к сведению.

Поиском находим код, выдающий это сообщение (путь приведен относительно корневой директории с исходниками ядра):

drivers/gpio/gpio-omap.c

static void omap_gpio_show_rev(struct gpio_bank *bank)
{
	static bool called;
	u32 rev;

	if (called || bank->regs->revision == USHRT_MAX)
		return;

	rev = readw_relaxed(bank->base + bank->regs->revision);
	pr_info("OMAP GPIO hardware version %d.%d\n",
		(rev >> 4) & 0x0f, rev & 0x0f);

	called = true;
}

Отлично, мы знаем, где находится исходник. Начнем его изучать. Для начала поймем, что должно быть в DTS для этого драйвера. Напомним, ядро запускает функцию probe () драйвера после того, как поймет, что в системе есть соответствующий девайс.
Контроллер GPIO — это блок внутри микросхемы, подключенный по шине типа AXI. Механизмов типа Plug’n'Play там не предусмотрено. Значит, ядро определяет это из статического описания аппаратуры. Эту роль у нас играет Device Tree (разговорно также «DTS», «DTB» — хотя технически это не совсем верно. DTS и DTB — это формы представления Device Tree. DTS — «device-tree source», DTB — «device-tree binary»).

В драйвере прописаны поля compatible, которые должны быть описаны в DTS:

static const struct of_device_id omap_gpio_match[] = {
	{
		.compatible = "ti,omap4-gpio",
		.data = &omap4_pdata,
	},
	{
		.compatible = "ti,omap3-gpio",
		.data = &omap3_pdata,
	},
	{
		.compatible = "ti,omap2-gpio",
		.data = &omap2_pdata,
	},
	{ },
};
MODULE_DEVICE_TABLE(of, omap_gpio_match);

Сам драйвер зарегистрирован как платформенный драйвер:

static struct platform_driver omap_gpio_driver = {
	.probe		= omap_gpio_probe,
	.remove		= omap_gpio_remove,
	.driver		= {
		.name	= "omap_gpio",
		.pm	= &gpio_pm_ops,
		.of_match_table = of_match_ptr(omap_gpio_match),
	},
};

/*
 * gpio driver register needs to be done before
 * machine_init functions access gpio APIs.
 * Hence omap_gpio_drv_reg() is a postcore_initcall.
 */
static int __init omap_gpio_drv_reg(void)
{
	return platform_driver_register(&omap_gpio_driver);
}
postcore_initcall(omap_gpio_drv_reg);

Немного подробнее о платформенных драйверах, механизмах загрузки и слове initcall было написано здесь. Гораздо подробнее — здесь.'

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

А пока из списка compatible мы поняли, что драйвер поддерживает три версии блока GPIO. Чтобы понять, какая конкретно используется у нас, все же придется идти в DTS:

arch/arm/boot/dts/am33xx.dtsi

		gpio0: gpio@44e07000 {
			compatible = "ti,omap4-gpio";
			ti,hwmods = "gpio1";
			gpio-controller;
			#gpio-cells = <2>;
			interrupt-controller;
			#interrupt-cells = <2>;
			reg = <0x44e07000 0x1000>;
			interrupts = <96>;
		};

Там же есть аналогичные блоки с именами gpio1, gpio2, gpio3.

Выводы:

  1. Существует минимум три поддерживаемых версии блока gpio (omap2-gpio, omap3-gpio, omap4-gpio).

  2. В DTS мы можем увидеть базовые адреса, по которым расположены эти блоки внутри микросхемы.

  3. Эти блоки GPIO умеют генерировать прерывания. В целом, эту информацию можно получить и из даташита, но:

  • В принципе, даташиты открывать линукс-программисту приходится существенно реже, чем программисту микроконтроллеров.

  • На многие процессоры поди еще найди даташит в открытом доступе. Так что в общем случае это знание не является бесполезным. Хотя в конкретном случае — да, бесполезное.

Вот что происхоидит в omap_gpio_probe ():

drivers/gpio/gpio-omap.c

static int omap_gpio_probe(struct platform_device *pdev)
{
	struct device *dev = &pdev->dev;
	struct device_node *node = dev->of_node;
	const struct of_device_id *match;
	const struct omap_gpio_platform_data *pdata;
	struct resource *res;
	struct gpio_bank *bank;
	struct irq_chip *irqc;
...
// здесь много действий вокруг прерываний
// тоже интересная тема, но отложим
...
// а вот уже непосредственно про GPIO
	if (bank->is_mpuio)
		omap_mpuio_init(bank);

	omap_gpio_mod_init(bank);

	ret = omap_gpio_chip_init(bank, irqc);
	if (ret) {
		pm_runtime_put_sync(dev);
		pm_runtime_disable(dev);
		if (bank->dbck_flag)
			clk_unprepare(bank->dbck);
		return ret;
	}

	omap_gpio_show_rev(bank);

Посмотрим детальнее:

	if (bank->is_mpuio)
		omap_mpuio_init(bank);

Из кода непонятно, что это. Если вбить «mpuio» в поисковик, можно найти в даташитах от TI такое:

Даташит на процессор TI из интернета

Даташит на процессор TI из интернета

Пожалуй, этого достаточно, чтоб промежуточно сделать такие выводы:

  1. Это специфичная штука от TI.

  2. Это какой-то специальный режим работы порта GPIO, для которого есть свой драйвер.

  3. Этот режим бывает не во всех портах GPIO у TI.

  4. В данном случае наличие или отсутствие этого режима не очень влияет на рассмотрение основной темы статьи.

Дальше у нас идет функция:

static void omap_gpio_mod_init(struct gpio_bank *bank)
{
	void __iomem *base = bank->base;
	u32 l = 0xffffffff;

	if (bank->width == 16)
		l = 0xffff;

	if (bank->is_mpuio) {
		writel_relaxed(l, bank->base + bank->regs->irqenable);
		return;
	}

	omap_gpio_rmw(base, bank->regs->irqenable, l,
		      bank->regs->irqenable_inv);
	omap_gpio_rmw(base, bank->regs->irqstatus, l,
		      !bank->regs->irqenable_inv);
	if (bank->regs->debounce_en)
		writel_relaxed(0, base + bank->regs->debounce_en);

	/* Save OE default value (0xffffffff) in the context */
	bank->context.oe = readl_relaxed(bank->base + bank->regs->direction);
	 /* Initialize interface clk ungated, module enabled */
	if (bank->regs->ctrl)
		writel_relaxed(0, base + bank->regs->ctrl);
}

Это сплошные записи в регистры блока GPIO. Здесь нет каких-то действий, специфичных для ядра Linux. По большому счету, это тоже не очень важная функция для рассматриваемой темы.
А вот следующая функция — это точно в цель:

static int omap_gpio_chip_init(struct gpio_bank *bank, struct irq_chip *irqc)
{
	struct gpio_irq_chip *irq;
	static int gpio;
	const char *label;
	int irq_base = 0;
	int ret;

	/*
	 * REVISIT eventually switch from OMAP-specific gpio structs
	 * over to the generic ones
	 */
	bank->chip.request = omap_gpio_request;
	bank->chip.free = omap_gpio_free;
	bank->chip.get_direction = omap_gpio_get_direction;
	bank->chip.direction_input = omap_gpio_input;
	bank->chip.get = omap_gpio_get;
	bank->chip.direction_output = omap_gpio_output;
	bank->chip.set_config = omap_gpio_set_config;
	bank->chip.set = omap_gpio_set;
	if (bank->is_mpuio) {
		bank->chip.label = "mpuio";
		if (bank->regs->wkup_en)
			bank->chip.parent = &omap_mpuio_device.dev;
		bank->chip.base = OMAP_MPUIO(0);
	} else {
		label = devm_kasprintf(bank->chip.parent, GFP_KERNEL, "gpio-%d-%d",
				       gpio, gpio + bank->width - 1);
		if (!label)
			return -ENOMEM;
		bank->chip.label = label;
		bank->chip.base = gpio;
	}
	bank->chip.ngpio = bank->width;

#ifdef CONFIG_ARCH_OMAP1
	/*
	 * REVISIT: Once we have OMAP1 supporting SPARSE_IRQ, we can drop
	 * irq_alloc_descs() since a base IRQ offset will no longer be needed.
	 */
	irq_base = devm_irq_alloc_descs(bank->chip.parent,
					-1, 0, bank->width, 0);
	if (irq_base < 0) {
		dev_err(bank->chip.parent, "Couldn't allocate IRQ numbers\n");
		return -ENODEV;
	}
#endif

	/* MPUIO is a bit different, reading IRQ status clears it */
	if (bank->is_mpuio) {
		irqc->irq_ack = dummy_irq_chip.irq_ack;
		if (!bank->regs->wkup_en)
			irqc->irq_set_wake = NULL;
	}

	irq = &bank->chip.irq;
	irq->chip = irqc;
	irq->handler = handle_bad_irq;
	irq->default_type = IRQ_TYPE_NONE;
	irq->num_parents = 1;
	irq->parents = &bank->irq;
	irq->first = irq_base;

	ret = gpiochip_add_data(&bank->chip, bank);
	if (ret) {
		dev_err(bank->chip.parent,
			"Could not register gpio chip %d\n", ret);
		return ret;
	}

	ret = devm_request_irq(bank->chip.parent, bank->irq,
			       omap_gpio_irq_handler,
			       0, dev_name(bank->chip.parent), bank);
	if (ret)
		gpiochip_remove(&bank->chip);

	if (!bank->is_mpuio)
		gpio += bank->width;

В принципе, достаточно комментария, чтобы понять — здесь мы переходим от «OMAP-специфичных» структур (struct gpio_bank) к «общесистемным» (struct gpiochip). По сути, общесистемную структуру мы держим как экземпляр внутри своей OMAP-специфичной. Здесь же есть действия вокруг прерываний, но эту тему мы пока не затрагиваем.

Самый важный для нас сейчас блок находится здесь:

	bank->chip.request = omap_gpio_request;
	bank->chip.free = omap_gpio_free;
	bank->chip.get_direction = omap_gpio_get_direction;
	bank->chip.direction_input = omap_gpio_input;
	bank->chip.get = omap_gpio_get;
	bank->chip.direction_output = omap_gpio_output;
	bank->chip.set_config = omap_gpio_set_config;
	bank->chip.set = omap_gpio_set;

Здесь у нас как раз переход от общего API к его платформенно-зависимому коду. Когда какой-то код дергает платформенно-независимую функцию типа gpiod_request () — система дергает gpio_chip.request (). И если это происходит на процессоре AM335x — мы попадаем в omap_gpio_request ().

Ну и главное после заполнения структуры gpio_chip — мы регистрируем эту структуру в системе. Именно с этого момента система знает о существовании внутри нее этих линий GPIO.

Пробежимся по реализации двух функций — запроса GPIO и выставления значения GPIO:

static int omap_gpio_request(struct gpio_chip *chip, unsigned offset)
{
	struct gpio_bank *bank = gpiochip_get_data(chip);
	unsigned long flags;

	/*
	 * If this is the first gpio_request for the bank,
	 * enable the bank module.
	 */
	if (!BANK_USED(bank))
		pm_runtime_get_sync(chip->parent);

	raw_spin_lock_irqsave(&bank->lock, flags);
	omap_enable_gpio_module(bank, offset);
	bank->mod_usage |= BIT(offset);
	raw_spin_unlock_irqrestore(&bank->lock, flags);

	return 0;
}

Здесь самое важное — это то, что обернуто в spinlock: включение модуля GPIO и запись во внутреннюю структуру bank.mod_usage, которая по сути дублирует запись о «занятости» вывода. Вот что важно во всем этом:

  1. Сам факт оборачивания в спинлок для исключения коллизий.

  2. Хотя здесь и присутствует функция omap_enable_gpio_module (), но в общем случае никаких специальных действий с аппаратурой не требуется. Запрос GPIO — просто способ зафиксировать в системе — «этот GPIO уже занят». Строго говоря, наличие платформенно-зависимой функции request () вообще необязательно.

Функция, задающая значение GPIO-выхода:

static void omap_gpio_set(struct gpio_chip *chip, unsigned offset, int value)
{
	struct gpio_bank *bank;
	unsigned long flags;

	bank = gpiochip_get_data(chip);
	raw_spin_lock_irqsave(&bank->lock, flags);
	bank->set_dataout(bank, offset, value);
	raw_spin_unlock_irqrestore(&bank->lock, flags);
}

Функция set_dataout () задавалась при инициализации драйвера в функции probe ():

	if (bank->regs->set_dataout && bank->regs->clr_dataout) {
		bank->set_dataout = omap_set_gpio_dataout_reg;
		bank->set_dataout_multiple = omap_set_gpio_dataout_reg_multiple;
	} else {
		bank->set_dataout = omap_set_gpio_dataout_mask;
		bank->set_dataout_multiple =
				omap_set_gpio_dataout_mask_multiple;
	}

Видимо, это дает разработчикам драйвера дополнительную степень свободы. Так или иначе, если мы заглянем в omap_set_gpio_dataout_reg (), то увидим там уже непосредственную работу с регистрами блока GPIO:

/* set data out value using dedicate set/clear register */
static void omap_set_gpio_dataout_reg(struct gpio_bank *bank, unsigned offset,
				      int enable)
{
	void __iomem *reg = bank->base;
	u32 l = BIT(offset);

	if (enable) {
		reg += bank->regs->set_dataout;
		bank->context.dataout |= l;
	} else {
		reg += bank->regs->clr_dataout;
		bank->context.dataout &= ~l;
	}

	writel_relaxed(l, reg);
}

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

Теперь попробуем валидировать ранее сделанные теоретические выкладки, полученные путем чтения исходников. Убедимся, что при обращениях через пространство ядра и через пространство пользователя мы проходим именно эту цепочку вызовов. Сделаем это самым примитивным способом — добавлением строки
printk("%s: entered\n”, __func__);
в функции, о которых мы говорим. И снимем логи.

Сначала посмотрим на лог загрузки в UART, и сразу увидим следы наших вмешательств:

[    0.571306] pinctrl-single 44e10800.pinmux: 142 pins, size 568
[    0.576824] gpiod_request_commit: entered
[    0.576852] omap_gpio_request: entered
[    0.576992] gpiod_direction_output_raw_commit: entered
[    0.577051] omap_set_gpio_direction: entered
[    0.580063] Serial: 8250/16550 driver, 6 ports, IRQ sharing enabled
...
[    1.535627] sdhci: Secure Digital Host Controller Interface driver
[    1.541871] sdhci: Copyright(c) Pierre Ossman
[    1.547696] gpiod_request_commit: entered
[    1.551745] omap_gpio_request: entered
[    1.555787] omap_set_gpio_direction: entered
[    1.560134] omap_gpio 44e07000.gpio: Could not set line 6 debounce to 200000)
[    1.568968] omap_hsmmc 48060000.mmc: Got CD GPIO

Видно, что драйверы в процессе загрузки задействовали два GPIO. И видно, что идут они именно по тому маршруту, о котором мы говорили — сначала это вызов платформенно-независимого gpiod_request (), а из него — уже платформенно-зависимого omap_gpio_request ().

Теперь посмотрим, что будет, если мы поуправляем GPIO из пространства ядра:

# cd /sys/class/gpio
# ls
export      gpiochip0   gpiochip32  gpiochip64  gpiochip96  unexport
# echo 15 > export
[  803.092438] gpiod_request_commit: entered
[  803.097022] omap_gpio_request: entered
# ls
export      gpiochip0   gpiochip64  unexport
gpio15      gpiochip32  gpiochip96
# cd gpio15
# echo out > direction
[  815.830522] gpiod_direction_output_raw_commit: entered
[  815.836210] omap_set_gpio_direction: entered
# echo 1 > value
[  820.621108] value_store: entered
[  820.624881] gpiod_set_value_cansleep: entered
[  820.629404] gpiod_set_raw_value_commit: entered
[  820.634088] omap_gpio_set: entered

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

OrangePi Zero (Allwinner H2)

Здесь лог загрузки не балует нас словами про GPIO. На искомое похожи эти строчки:

[    0.089512] sun8i-h3-pinctrl 1c20800.pinctrl: initialized sunXi PIO driver
[    0.091141] sun8i-h3-r-pinctrl 1f02c00.pinctrl: initialized sunXi PIO driver

Здесь сходу смущает название «sun8i-h3», учитывая, что процессор на нашей плате маркирован крупной надписью «H2». Поиск по доступным DTS не дает ничего с названием типа «sun8i-h2». Но сборка работает, а значит — различия H2 vs H3 не настолько велики. Поиск в интернете дал такой результат — https://linux-sunxi.org/H3#Variants:

H2+ is a variant of H3, targeted at low-end OTT boxes, which lacks Gigabit MAC and 4K HDMI output support.

H3 images are proven to run on H2+.

Значит, все валидно, и можем изучать код дальше.

Идем в код, который выводит нам эти строчки. Он находится здесь:

drivers/pinctrl/sunxi/pinctrl-sunxi.c

int sunxi_pinctrl_init_with_variant(struct platform_device *pdev,
				    const struct sunxi_pinctrl_desc *desc,
				    unsigned long variant)
{
...
	pctl->chip->owner = THIS_MODULE;
	pctl->chip->request = gpiochip_generic_request;
	pctl->chip->free = gpiochip_generic_free;
	pctl->chip->set_config = gpiochip_generic_config;
	pctl->chip->direction_input = sunxi_pinctrl_gpio_direction_input;
	pctl->chip->direction_output = sunxi_pinctrl_gpio_direction_output;
	pctl->chip->get = sunxi_pinctrl_gpio_get;
	pctl->chip->set = sunxi_pinctrl_gpio_set;
	pctl->chip->of_xlate = sunxi_pinctrl_gpio_of_xlate;
	pctl->chip->to_irq = sunxi_pinctrl_gpio_to_irq;
	pctl->chip->of_gpio_n_cells = 3;
	pctl->chip->can_sleep = false;
	pctl->chip->ngpio = round_up(last_pin, PINS_PER_BANK) -
			    pctl->desc->pin_base;
	pctl->chip->label = dev_name(&pdev->dev);
	pctl->chip->parent = &pdev->dev;
	pctl->chip->base = pctl->desc->pin_base;
...

А вызывают ее отсюда:

drivers/pinctrl/sunxi/pinctrl-sun8i-v3s.c

static int sun8i_v3s_pinctrl_probe(struct platform_device *pdev)
{
	unsigned long variant = (unsigned long)of_device_get_match_data(&pdev->dev);

	return sunxi_pinctrl_init_with_variant(pdev, &sun8i_v3s_pinctrl_data,
					       variant);
}

static const struct of_device_id sun8i_v3s_pinctrl_match[] = {
	{
		.compatible = "allwinner,sun8i-v3-pinctrl",
		.data = (void *)PINCTRL_SUN8I_V3
	},
	{
		.compatible = "allwinner,sun8i-v3s-pinctrl",
		.data = (void *)PINCTRL_SUN8I_V3S
	},
	{ },
};

static struct platform_driver sun8i_v3s_pinctrl_driver = {
	.probe	= sun8i_v3s_pinctrl_probe,
	.driver	= {
		.name		= "sun8i-v3s-pinctrl",
		.of_match_table	= sun8i_v3s_pinctrl_match,
	},
};
builtin_platform_driver(sun8i_v3s_pinctrl_driver);

Как и у TI — это платформенный драйвер. Собственно, сложно сделать иначе.
А вот ощутимая разница присутствует в том, что драйвер находится не в директории GPIO, а в директории pinctrl.

Драйвер pinctrl управляет также назначением каждого конкретного вывода. Как правило, выводы GPIO на современных SoC мультиплексированы с другими интерфейсами уровня I2C/SPI/UART. Вот драйвер pinctrl включает в себя функции, управляющие
этими мультиплексорами. Драйвер pinctrl есть, например, и на TI AM335x, но там эта сущность отделена от драйвера GPIO. Здесь же решили сделать так.

Вернемся, однако, к GPIO. Как бы то ни было, ключевые действия в этом драйвере на месте — создается, заполняется и регистрируется в системе экземпляр структуры gpio_chip. Посмотрим на это пристальнее.

Во-первых, видно, что для реализации request () используется функция gpiochip_generic_request():

drivers/gpio/gpiolib.c

int gpiochip_generic_request(struct gpio_chip *gc, unsigned offset)
{
#ifdef CONFIG_PINCTRL
	if (list_empty(&gc->gpiodev->pin_ranges))
		return 0;
#endif

	return pinctrl_gpio_request(gc->gpiodev->base + offset);
}
EXPORT_SYMBOL_GPL(gpiochip_generic_request);

Она под капотом вызывает:

drivers/pinctrl/core.c

/**
 * pinctrl_gpio_request() - request a single pin to be used as GPIO
 * @gpio: the GPIO pin number from the GPIO subsystem number space
 *
 * This function should *ONLY* be used from gpiolib-based GPIO drivers,
 * as part of their gpio_request() semantics, platforms and individual drivers
 * shall *NOT* request GPIO pins to be muxed in.
 */
int pinctrl_gpio_request(unsigned gpio)
{
	struct pinctrl_dev *pctldev;
	struct pinctrl_gpio_range *range;
	int ret;
	int pin;

	ret = pinctrl_get_device_gpio_range(gpio, &pctldev, &range);
	if (ret) {
		if (pinctrl_ready_for_gpio_range(gpio))
			ret = 0;
		return ret;
	}

	mutex_lock(&pctldev->mutex);

	/* Convert to the pin controllers number space */
	pin = gpio_to_pin(range, gpio);

	ret = pinmux_request_gpio(pctldev, range, pin, gpio);

	mutex_unlock(&pctldev->mutex);

	return ret;
}
EXPORT_SYMBOL_GPL(pinctrl_gpio_request);

Не вдаваясь в подробности реализации pinctrl и pinmux, попробуем констатировать: эта функция не просто резервирует GPIO,
но также закрепляет его именно в качестве GPIO, а не в качестве I2C/SPI/UART/etc-вывода.

И посмотрим, как выглядит функция установки значения выхода GPIO:

drivers/sunxi/pinctrl-sunxi.c

static void sunxi_pinctrl_gpio_set(struct gpio_chip *chip,
				unsigned offset, int value)
{
	struct sunxi_pinctrl *pctl = gpiochip_get_data(chip);
	u32 reg = sunxi_data_reg(offset);
	u8 index = sunxi_data_offset(offset);
	unsigned long flags;
	u32 regval;

	raw_spin_lock_irqsave(&pctl->lock, flags);

	regval = readl(pctl->membase + reg);

	if (value)
		regval |= BIT(index);
	else
		regval &= ~(BIT(index));

	writel(regval, pctl->membase + reg);

	raw_spin_unlock_irqrestore(&pctl->lock, flags);

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

Чтобы удостовериться в правильном понимании, пройдем аналогичным маршрутом с добавлением отладочного вывода.
Правда, при добавлении этого вывода и запуске с этой прошивкой — тонем в бесконечном зацикленном выводе сообщений:

[   20.884895] gpiod_set_value_nocheck: entered
[   20.889190] gpiod_set_raw_value_commit: entered
[   20.893720] sunxi_pinctrl_gpio_set: entered 

Но в начале лога успеваем увидеть:

[    1.623498] gpiod_request: entered
[    1.630993] gpiod_request_commit: entered
[    1.635007] gpiochip_generic_request: entered
[    1.639378] pinctrl_gpio_request: entered
[    1.643401] pin_request: entered

Что, в принципе, подтверждает сделанные выше выводы, но несколько мешает проверке процедуры обращения через sysfs. В принципе, это очень характерная история для отладки Linux — стоит чуть копнуть, и натыкаешься на какое-то совершенно непонятное поведение, которое не то чтобы критично (раз уж всегда так было), но и оставлять его невыясненным кажется неправильно. Отладку этого поведения мы вынесем чуть ниже под спойлер, а здесь закончим убеждаться, что sysfs работает ожидаемо. Уберем лишний вывод из gpiod_set_…(), пересоберем, перезагрузимся и посмотрим:

# cd /sys/class/gpio
# ls
export       gpiochip0    gpiochip352  unexport
# echo 15 > export
[ 1035.505141] export_store: entered
[ 1035.508561] gpiod_request: entered
[ 1035.511968] gpiod_request_commit: entered
[ 1035.515979] gpiochip_generic_request: entered
[ 1035.520397] pinctrl_gpio_request: entered
[ 1035.524426] pin_request: entered
# cd gpio15
# echo out > direction
# echo 1 > value
[ 1057.037344] value_store: entered

Видно, что вызывается ровно то, что мы ожидаем.

Внеплановая отладка

Кто же дергает GPIO? Рассказ об отладке сильно сокращен, поисков и попыток было существенно больше. В конечном счете вышло так:

1) Добавили в функцию установки значения отладочный вывод:

printk("%s: gpiochip->base=%d, offset=%d\n", __func__, chip->base, offset);

Получили такой лог:

[   59.314332] gpiod_set_value_cansleep: entered
[   59.318718] gpiod_set_value_nocheck: entered
[   59.322989] sunxi_pinctrl_gpio_set: entered
[   59.327174] sunxi_pinctrl_gpio_set: gpiochip->base=352, offset=6

2) Выключили логи, посмотрели:

# cat /sys/class/gpio/gpiochip352/label
1f02c00.pinctrl 

3) Поискали даташит на процессор. Нашли, что по адресу 0×1F0_2C00 находится блок R_PIO. Вообще-то, конечно, могли бы это же понять из DTS. Второй блок выводов называется PIO. В чем разница — непонятно.

4) Ок, пошли в DTS на плату. Поиском по «r_pio» находим:

arch/arm/boot/dts/sun8i-h2-plus-orangepi-zero.dts

	reg_vdd_cpux: vdd-cpux-regulator {
		compatible = "regulator-gpio";
		regulator-name = "vdd-cpux";
		regulator-type = "voltage";
		regulator-boot-on;
		regulator-always-on;
		regulator-min-microvolt = <1100000>;
		regulator-max-microvolt = <1300000>;
		regulator-ramp-delay = <50>; /* 4ms */

		gpios = <&r_pio 0 6 GPIO_ACTIVE_HIGH>; /* PL6 */
		enable-active-high;
		gpios-states = <1>;
		states = <1100000 0>, <1300000 1>;
	};

В комментарии написано PL6. Очень похоже на обозначение физического вывода.

5) Открыли схему платы — https://drive.google.com/drive/folders/1kmPudPO9dENdBm_Z1IHar8vu_lmzNXFR

Видим, что вывод PL6 действительно выведен на цепь с названием CPUX-VSET:

85adf5ab47348cefb8ad036b4463e099.jpg

А эта цепь и правда управляет каким-то питанием:

11dd387379e1bf353582c080e97eec96.jpg

Не то чтоб нам было от этого легче, но хотя бы понятно, что это может быть. Очень похоже, что это Linux в рантайме активно управляет питанием CPU.

6) Комментируем в DTS ноду:

 &cpu0 {
	cpu-supply = <®_vdd_cpux>;
};

7) Пересобираем образ, прошиваем — убеждаемся, что спам из сообщений пропадает. Дело раскрыто. Зачем это дерганье пином происходит — интересно, но лежит за рамками темы статьи.

Резюмируя

Подводя итоги, можно свести все сказанное к одной простой структурной схеме:

Вместо тысячи слов

Вместо тысячи слов

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

До встречи в новых материалах!

© Habrahabr.ru