Использование gpio-generic и irq_chip_generic для драйвера gpio

Введение

Данная статья является логичным продолжением предыдущей и её прочтение рекомендуется после ознакомления с предшествующим материалом. Текущая заметка необходима для понимания последующего материала, дополнительного понимания подсистемы gpio в целом и способствует разработке собственных gpio драйверов. Положения данной статьи уже применимы не только к нашему виртуальному драйверу gpio, но и к любому mmio gpio драйверу в целом. Речь пойдет об упомянутой в предыдущей части "оптимизации".

Цель

Прежде чем приступать к более интересным и полезным вещам, необходимо навести порядок. Как и упоминалось ранее количество кода можно сократить. Для этого воспользуемся двумя подсистемами (драйверами) gpio-generic (gpio-mmio начиная с 4.7) и irq_chip_generic.

Согласно принципам ядра linux лучше пожертвовать производительностью ради понятности и отсутствию повторяющегося типового кода. А использованию существующих реализаций в ядре linux, несомненно служит данной цели.

Реализация

pci_ids

Небольшое отступление.

Первоначально при переходе на bgpio был проведен небольшой успешный эксперимент для виртуального драйвера с поддержкой устройств по 8, 16 и 32 входа. Итоговый код можно посмотреть здесь. Данного результата удалось достичь благодаря небольшой, всего 7 строк, модификации ivshmem, чтобы добавить передаваемые параметры sub-vendor-id и sub-device-id для pci устройства (патч входящий в ветку предназначен для версии qemu 2.5.1.1).

Использование gpio-mmio

В ядре данный драйвер зависит от CONFIG_GPIO_GENERIC.

Начнем с простого, драйвера, который создавался для различных MMIO gpio, и в последствии части которого стали достаточно активно использоваться некоторыми драйверами. Он был представлен Антоном Воронцовым в 2010 году (https://lkml.org/lkml/2010/8/25/303). В своем патче он анонсировал для драйвера следующие возможности:

  • Поддержку 8/16/32/64 битных регистров
  • Поддержку контроллеров gpio с регистрами задания/очистки
  • Поддержку контроллеров gpio только с регистром данных
  • Поддержку big-endian

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

Для использование достаточно всего лишь передать размер и регистр данных, все остальное опционально и зависит от конкретной реализации контроллера gpio, функция инициализации принимает в качестве параметров:

  • Регистр состояния
  • Регистр задания состояния
  • Регистр очистки состояния
  • Регистр переключения контакта в состояние выхода
  • Регистр переключения контакта в состояние входа

То есть возможны следующие ситуации:

  • Регистр dat только для чтения состояния, регистр set для задания и регистр clr для очистки состояния
  • Регистр dat только для чтения, регистр set для задания и очистки
  • Регистр dat для всего

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

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

bgpio_init

#if IS_ENABLED(CONFIG_GPIO_GENERIC)

int bgpio_init(struct gpio_chip *gc, 
               struct device *dev,
               unsigned long sz, 
               void __iomem *dat, 
               void __iomem *set,
               void __iomem *clr, 
               void __iomem *dirout, 
               void __iomem *dirin,
               unsigned long flags);

#define BGPIOF_BIG_ENDIAN               BIT(0)
#define BGPIOF_UNREADABLE_REG_SET       BIT(1) /* reg_set is unreadable */
#define BGPIOF_UNREADABLE_REG_DIR       BIT(2) /* reg_dir is unreadable */
#define BGPIOF_BIG_ENDIAN_BYTE_ORDER    BIT(3)
#define BGPIOF_READ_OUTPUT_REG_SET      BIT(4) /* reg_set stores output value */
#define BGPIOF_NO_OUTPUT                BIT(5) /* only input */

#endif

Необходимо обратить внимание, что в качестве размера bgpio_init принимает не количество входов, а параметр кратный 8, то есть ngpio/8.

    err = bgpio_init(&vg->chip, dev, BITS_TO_BYTES(VIRTUAL_GPIO_NR_GPIOS),
                     data, NULL, NULL, dir, NULL, 0);

Использование irq_chip_generic

В ядре данный драйвер зависит от GENERIC_IRQ_CHIP, что является недостатком, так как нет возможности включить данный параметр через menuconfig или oldconfig.

Теперь рассмотрим немного более сложную часть. irq_chip_generic был представлен в версии ядра v3.0-rc1 и выполняет функции схожие с gpio-mmio, то есть предоставляет стандартную имплементацию для многих случаев irq_chip.

Стандартные функции чтения/записи регистра являются 32 битными, это была одна из причин по которой я решил отказаться от 8/16 битных версий драйвера, тем не менее есть возможность предоставить подсистеме irq_chip_generic свои функции для чтения/записи или указать стандартные (например ioread8, iowrite8).

Совместное использование irq_chip_generic и функций gpiochip_irqchip_add, gpiochip_set_chained_irqchip

Память под irq_chip_generic и инициализация происходит с помощью irq_alloc_generic_chip. Функция, помимо тривиальных параметров, требует еще и irq_base, который, вообще говоря, нам неизвестен до вызова gpiochip_irqchip_add, что в свою очередь требует struct irq_chip. Но irq_alloc_generic_chip отвечает только за выделение памяти и инициализацию некоторых параметров, так что мы можем передать 0 в качестве параметра irq_base и присвоить реальное значение после вызова функции gpiochip_irqchip_add.

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

    gc = irq_alloc_generic_chip(VIRTUAL_GPIO_DEV_NAME, 1, 0, vg->data_base_addr, handle_edge_irq);

Для использования достаточно указать стандартные функции маскирования/демаскирования, подтверждения прерывания и регистры. Функция задания типа прерывания у нас остается неизменной, как и наш обработчик прерывания.

    ct->chip.irq_ack = irq_gc_ack_set_bit;
    ct->chip.irq_mask = irq_gc_mask_clr_bit;
    ct->chip.irq_unmask = irq_gc_mask_set_bit;
    ct->chip.irq_set_type = virtual_gpio_irq_type;

Регистры для подтверждения и маскирования/демаскирования:

    ct->regs.ack = vg->reg_off.irqeoi;
    ct->regs.mask = vg->reg_off.irqen;

Регистрами типа прерывания мы по-прежнему заведуем сами, в функции virtual_gpio_irq_type.

Соответственно в инициализацию gpiochip_irqchip_add и gpiochip_set_chained_irqchip мы передаем экземпляр irq_chip выделенный в irq_chip_generic->chip_types[0] (irq_chip_generic может иметь несколько типов irq_chip ассоциированных с одним и тем же подмножеством irq).

После чего используем полученный irq_base и завершим настройку irq_chip_generic.

    irq_setup_generic_chip(gc, 0, 0, 0, 0);

    gc->irq_cnt = VIRTUAL_GPIO_NR_GPIOS;
    gc->irq_base = vg->chip.irq_base;

    u32 msk = IRQ_MSK(32);
    u32 i;

    for (i = gc->irq_base; msk; msk >>= 1, i++) {
        if (!(msk & 0x01))
               continue;
        struct irq_data *d = irq_get_irq_data(i);
        d->mask = 1 << (i - gc->irq_base);
        irq_set_chip_data(i, gc);
    }

Мы намеренно передаем 0 в качестве параметра msk, чтобы избежать повторной инициализации номеров irq.

irq_chip_generic используется для вполне себе серьёзных контроллеров irq в основном платформенных, поэтому маска для регистров irq вычисляется заранее, чтобы ускорить обработку прерываний и не тратить время на вычисление маски в процессе работы. Поскольку мы передаем 0 в качестве параметра в функцию irq_setup_generic_chip, инициализируем маски самостоятельно.

Остается еще одна проблема, связанная с irq_set_chip_data (которая ничто иное как irq_data.chip_data = (void*)struct irq_chip_generic;). Дело в том, что gpiolib тоже опирается на void* chip_data и полагает, что там должен находится указатель на struct gpio_chip (http://lxr.free-electrons.com/source/drivers/gpio/gpiolib.c#L1138). Проблема решается предоставлением собственных реализаций функций irq_request_resources и irq_release_resources вместо стандартных gpiochip_irq_reqres и gpiochip_irq_relres.

Исходный код для данного примера.

Альтернативный подход

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

Перво-наперво запросим irq_base сами:

    irq_base = irq_alloc_descs(-1, 0, vg->chip.ngpio, 0);

Как мы помним из предыдущей статьи наш случай линейный (функция irq_domain_add_simple делает почти то же самое, но является устаревшей).

    vg->irq_domain = irq_domain_add_linear(0, vg->chip.ngpio, &irq_domain_simple_ops, vg);
    irq_domain_associate_many(vg->irq_domain, irq_base, 0, vg->chip.ngpio);

Следующий шаг остается практически неизменным и заключается в связывании gpio_chip с irq_chip:

    vg->chip.irqdomain = vg->irq_domain;    
    gpiochip_set_chained_irqchip(&vg->chip, &ct->chip, pdev->irq, NULL);

Единственный момент - чтобы был доступен файл "edge" в соответствующей директории управления контактом (gpiolib-sysfs), необходимо указать функцию трансляции to_irq:

    vg->chip.to_irq = virtual_gpio_to_irq;

Заключение

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

Условным недостатком следует считать зависимость от CONFIG_GPIO_GENERIC и GENERIC_IRQ_CHIP, но первый параметр легко включается в конфигурации ярда linux, а второй, вполне возможно, уже включен.

Материалы рекомендованные к дополнительному чтению:
1. High-level IRQ flow handlers
2. Linux Kernel IRQ Domain
3. Edge Triggered Vs Level Triggered interrupts

Исходные коды, Makefile и README:
https://github.com/maquefel/virtual_gpio_basic/tree/v4.6