Как стать автором
Обновить

Драйвер виртуальных GPIO с контроллером прерываний на базе QEMU ivshmem для Linux

Время на прочтение21 мин
Количество просмотров17K
Природа прерываний

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

Основываясь на собственном скромном опыте, могу сказать, что прерывания далеко не самая освященная тема в сообществе Linux. Из-за своих особенностей, а так же сильной привязки к аппаратной части, все обучающие материалы посвященные прерываниям лишены реального и легко воспроизводимого примера. Данный факт мешает пониманию того, что очень часто прерывания и GPIO неразделимы, особенно в области встраиваемого Linux. Многие начинают верить, что GPIO это очень простая и скучная вещь (которая кстати и стала таковой благодаря подсистеме sysfs).


Даже в примере приведенном в LDD3 (драйвер snull) прерывания эмитируются явным вызовом функции парного устройства. Так же имеются примеры в курсах USFCA (http://cs.usfca.edu/~cruse/cs686s08/), но они используют чужое прерывание, тесно связаны с архитектурой x86 и сильно устарели.

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

ivshmem — разделяемая память Inter-VM



Разработано для совместного использования разделяемой памяти (выделенной на хост-платформе через механизм POSIX shared memory API) множественными процессами QEMU с различными гостевыми платформами. Для того чтобы все гостевые платформы имели доступ к области разделяемой памяти, ivshmem моделирует PCI устройство предоставляя доступ к памяти как PCI BAR.



С точки зрения виртуальной машины, устройство ivshmem PCI содержит три базовых адресных регистра (BAR).
  • BAR0 представляет из себя область MMIO поддерживающую регистры и прерывания в случае если MSI не используется, размером один килобайт
  • BAR1 используется для MSI-X, если поддержка MSI включена.
  • BAR2 для доступа к объекту разделяемой памяти.


Данный механизм был представлен Cam Macdonnel в оригинальном докладе “Nahanni — a shared memory interface for KVM” (впоследствии стал известен как ivshmem), в котором выдвинул следующие тезисы:
  • zero-copy доступ к данным
  • механизм прерываний
  • взаимодействие гость/гость и хозяин/гость


и проанализировал быстродействие в целом.

В настоящей момент, официально, сопровождение ivshmem никто не осуществляет, тем не менее большой вклад в развитие ivshmem вносят сотрудники Red Hat.

Цель



ivshmem может послужить основой для симуляции и отладки многих классов устройств.
В данной статье мы рассматриваем виртуальную pci плату ввода/вывода общего назначения (general-purpose input/output, GPIO), которая так же является источником прерываний, и соответствующий драйвер с предоставлением доступа и управления посредством механизма sysfs.

Предпосылки:
  • Исходный код Qemu 2.5.1.1 (не рекомендуется брать более младшую версию)
  • Исходный код linux-kernel 4.1


Для разработки и тестирования использовалась виртуальная плата qemu versatilepb (system ARM).

Опционально:
  • arm-cross-toolchain
  • nairobi-embedded — Guest-side ivshmem PCI device test sources


Условные обозначения:

g>> — команды или вывод выполняемые на гостевой системе.
h>> — на основной.

Пример и оригинальный код



Для начала продемонстрируем оригинальный код, основанный на оригинальном коде ( https://github.com/henning-schild/ivshmem-guest-code ), и модифицированном, в последствии, Siro Mugabi.
h>> qemu: +=  -device ivshmem,shm=ivshmem,size=1
g>> # insmod ne_ivshmem_ldd_basic.ko
ivshmem 0000:00:0d.0: data_mmio iomap base = 0xc8c00000
ivshmem 0000:00:0d.0: data_mmio_start = 0x60000000 data_mmio_len = 1048576
ivshmem 0000:00:0d.0: regs iomap base = 0xc88ee400, irq = 27
ivshmem 0000:00:0d.0: regs_addr_start = 0x50002400 regs_len = 256
g>> # ./ne_ivshmem_shm_guest_usr -w "TEST STRING"
h>> $ xxd -l 16 /dev/shm/ivshmem
0000000: 5445535420535452 494e 4700 0000 0000  TEST STRING.....


В принципе этого вполне достаточно для эмуляции GPIO уже в таком виде. И во многих случаях так и поступали, когда достаточно простого состояния входа или записи в выход, использование sysfs и прерываний предполагают небольшую надстройку на I/O mem.

Реализация



Заметим, что /dev/ivshmem0 и ne_ivshmem_shm_guest_usr.c нам более не нужны, вся работа с устройством со стороны гостевой машины из пространства пользователя (user-space) будет осуществляться средствами интерфейса sysfs.

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

Во-первых все входа/выхода gpio разделены на порты, как правило по 8, 16, 32 входа. Каждый порт имеет, как минимум, регистр состояния входов (GPIO_DATA), регистр направления, если переключение in/out поддерживается (GPIO_OUTPUT). Далее (если есть поддержка в самом устройстве), регистр состояния прерываний, регистры прерывания по переднему фронту (rising) и заднему фронту (falling) и по уровню (high и low). Аппаратное прерывание, поставляемое главным контроллером прерываний, как правило, одно на весь порт и делится между всеми входами порта.

Примеры существующих реализаций с комментариями



Sitara am335x



более известна в составе платы beaglebone

Разработчик: Texas Instruments
Документация: AM335x Sitara Processors Technical Reference Manual (page 4865)
Соответствующий ему драйвер gpio: linux/drivers/gpio/gpio-omap.c
Соответствующий заголовок: linux/include/linux/platform_data/gpio-omap.h
Количество входов/выходов: 128 (4 gpio порта — по 32 контакта каждый)

am335x Sitara таблица регистров gpio - порт A
Имя регистра Смещение Имя в драйвере Комментарий
GPIO_IRQSTATUS_0 0x02С OMAP4_GPIO_IRQSTATUS_0 Состояние прерывания для заданного входа
GPIO_IRQSTATUS_1 0x030 OMAP4_GPIO_IRQSTATUS_1 Состояние прерывания для заданного входа
GPIO_IRQSTATUS_SET_0 0x034 OMAP4_GPIO_IRQSTATUS_SET_0 Включает прерывания по заданному входу
GPIO_IRQSTATUS_SET_1 0x038 OMAP4_GPIO_IRQSTATUS_SET_1 Включает прерывания по заданному входу
GPIO_IRQSTATUS_CLR_0 0x03С OMAP4_GPIO_IRQSTATUS_CLR_0 Выключает прерывания по заданному входу
GPIO_IRQSTATUS_CLR_1 0x040 OMAP4_GPIO_IRQSTATUS_CLR_1 Выключает прерывания по заданному входу
GPIO_OE 0x134 OMAP4_GPIO_OE Контролирует состояние вход/выход (in/out)
GPIO_DATAIN 0x138 OMAP4_GPIO_DATAIN Состояние входа/выхода
GPIO_DATAOUT 0x13C OMAP4_GPIO_DATAOUT Задание состояния для выходов (low/high)
GPIO_LEVELDETECT0 0x140 OMAP4_GPIO_LEVELDETECT0 Включение/выключения прерывания для входа по низкому уровню сигнала
GPIO_LEVELDETECT1 0x144 OMAP4_GPIO_LEVELDETECT1 Включение/выключения прерывания для входа по высокому уровню сигнала
GPIO_RISINGDETECT 0x148 OMAP4_GPIO_RISINGDETECT Включение/выключения прерывания для входа по переднему фронту
GPIO_FALLINGDETECT 0x14С OMAP4_GPIO_FALLINGDETECT Включение/выключения прерывания для входа по заднему фронту
GPIO_CLEARDATAOUT 0x190 OMAP4_GPIO_CLEARDATAOUT Переключает соответствующий вход в состояние low
GPIO_SETDATAOUT 0x194 OMAP4_GPIO_SETDATAOUT Переключает соответствующий вход в состояние high


Примечание: GPIO_IRQSTATUS_N также используется для IRQ ACK. Управление дребезгом, а так же питанием выходит за рамки данной статьи.

Наличие регистров GPIO_CLEARDATAOUT и GPIO_SETDATAOUT помимо регистра GPIO_DATAOUT, а так же GPIO_IRQSTATUS_SET_N и GPIO_IRQSTATUS_CLR_N помимо GPIO_IRQSTATUS_N, объясняется двумя способами записи состояния выхода:
  • Стандартный: Чтение запись регистра полностью по основному адресу
  • Задание и очистка (рекомендуемый производителем): Для задания и очистки соответствующего контакта как выхода используются два соответствующих регистра, то же самое относится к управлению прерываниями.


ep9301



Разработчик: Cirrus Logic
Документация: EP9301 User’s Guide (page 523)
Соответствующий ему драйвер gpio: linux/drivers/gpio/gpio-ep93xx.c
Соответствующий заголовок: linux/arch/arm/mach-ep93xx/include/mach/gpio-ep93xx.h
Количество входов/выходов: 56 (7 портов gpio — по 8 контактов каждый)

ep9301 таблица регистров gpio - порт A
Имя регистра Смещение Имя в драйвере Описание
PADR 0x00 EP93XX_GPIO_REG(0x0) Регистр состояние входов/выходов доступен для чтения записи
PADDR 0x10 EP93XX_GPIO_REG(0x10) Контролирует состояние вход/выход (in/out)
GPIOAIntEn 0x9C int_en_register_offset[0] Включает прерывания по заданному входу
GPIOAIntType1 0x90 int_type1_register_offset[0] Задает тип прерывания level/edge
GPIOAIntType2 0x94 int_type2_register_offset[0] Задает high/rising или low/fallingв зависимости от выбранного типа прерываний
GPIOAEOI 0x98 eoi_register_offset[0] Регистр для оповещения об обработанном прерывании
IntStsA 0xA0 EP93XX_GPIO_A_INT_STATUS Регистр состояние прерывания


Примечание:
Из них для доступны 7 портов по 8, 8, 1, 2, 3, 2, 4 входов/выходов причем регистрами прерываний обладают только первый, второй и пятый порты.
В таблице рассмотрен только порт A.
Одной из особенностей ep9301, является то что тип прерываний both на аппаратном уровне не поддерживается, в драйвере происходит переключение в момент срабатывания прерывания. Другая интересная особенность — на порту F каждый контакт имеет свое собственное прерывание.

Bt848



Последний пример: pci плата Bt848, с gpio.

Разработчик: Intel
Документация: Bt848/848A/849A (page 68)
Соответствующий драйвер gpio: linux/drivers/gpio/gpio-bt8xx.c
Соответствующий заголовок: linux/drivers/media/pci/bt8xx/bt848.h
Количество входов/выходов: 24

Bt848 является платой видеозахвата.

Bt848 таблица регистров gpio
Имя регистра Смещение Имя в драйвере Описание
BT848_GPIO_OUT_EN 0x118 BT848_GPIO_OUT_EN Регистр состояние входов/выходов доступен для чтения и записи
BT848_GPIO_DATA 0x200 BT848_GPIO_DATA Контролирует состояние вход/выход (in/out)


Поддержки прерываний нет. Всего два регистра — состояние и настройка in/out.

Размечаем в памяти наше устройство



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

Пусть устройство обладает 8 входами/выходами общего назначения, тогда:
Имя регистра Смещение Имя в драйвере Описание
DATA 0x00 VIRTUAL_GPIO_DATA Регистр состояние входов/выходов доступен для чтения и записи
OUTPUTEN 0x01 VIRTUAL_GPIO_OUT_EN Контролирует состояние вход/выход (in/out)


Краткая справка по интерфейсу gpio


struct gpio_chip {
  /* имя порта gpio */
  const char *label;
  /* функция задания как входа */
  int (*direction_input)(struct gpio_chip *chip, unsigned offset); 
  /* состояние контакта */
  int (*get)(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);     
  /* номер первого контакта в контексте ядра, присваивается динамически в случае значения равном -1 */
  int base;
  /* количество контактов */
  u16 ngpio; 
};


Документация:
https://www.kernel.org/doc/Documentation/gpio/sysfs.txt

Ссылка на исходный код:
linux-kernel 4.1

Состояние выхода при переключении



Необходимо отметить параметр int value в функции direction_output, которая обслуживает файл /sys/class/gpio/gpioN/direction, принимающий значение не только “in”/”out”, но так же и “high”/“low”, значения которых передаются как параметр value (этот простой факт, по какой-то причине, редко упоминается в руководствах для начинающих).
g>> /sys/class/gpio # echo low > gpio0/direction
g>> /sys/class/gpio # cat gpio0/value
0

g>> /sys/class/gpio # echo high > gpio0/direction
g>> /sys/class/gpio # cat gpio0/value
1


Динамическое присвоение int base и наследие ARCH_NR_GPIOS



Исторически, количество GPIO в ядре было ограничено параметром ARCH_NR_GPIOS, по умолчанию равном 256 и, впоследствии увеличенном до 512 (версия 3.18).

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

Причиной такого поведения было определение таблицы описаний GPIO как статической и максимальная величина смещения для каждого порта была ограничена:
static struct gpio_desc gpio_desc[ARCH_NR_GPIOS];


Порты GPIO и их смещения были жестко определены в файлах описывающих аппаратную часть конкретного SOC, например:
EP93XX_GPIO_BANK
/source/arch/arm/mach-ep93xx/gpio.c
#define EP93XX_GPIO_BANK(name, dr, ddr, base_gpio)                       \
       {                                                                 \
               .chip = {                                                 \
                       .label             = name,                        \
                       .direction_input   = ep93xx_gpio_direction_input, \
                       .direction_output  = ep93xx_gpio_direction_output,\
                       .get               = ep93xx_gpio_get,             \
                       .set               = ep93xx_gpio_set,             \
                       .dbg_show          = ep93xx_gpio_dbg_show,        \
                       .base              = base_gpio,                   \
                       .ngpio             = 8,                           \
               },                                                        \
               .data_reg       = EP93XX_GPIO_REG(dr),                    \
               .data_dir_reg   = EP93XX_GPIO_REG(ddr),                   \
       }

static struct ep93xx_gpio_chip ep93xx_gpio_banks[] = {
       EP93XX_GPIO_BANK("A", 0x00, 0x10, 0),
       EP93XX_GPIO_BANK("B", 0x04, 0x14, 8),
       EP93XX_GPIO_BANK("C", 0x08, 0x18, 40),
       EP93XX_GPIO_BANK("D", 0x0c, 0x1c, 24),
       EP93XX_GPIO_BANK("E", 0x20, 0x24, 32),
       EP93XX_GPIO_BANK("F", 0x30, 0x34, 16),
       EP93XX_GPIO_BANK("G", 0x38, 0x3c, 48),
       EP93XX_GPIO_BANK("H", 0x40, 0x44, 56),
};


Начиная с версии 3.19 статический массив был заменен на динамические для каждого порта GPIO, выделяемого в фукнции gpiochip_add().

Тем не менее ARCH_NR_GPIOS все еще здесь (на момент версии 4.7) и используется для поиска смещения при динамическом присваивании base.
/* dynamic allocation of GPIOs, e.g. on a hotplugged device */
static int gpiochip_find_base(int ngpio);


Параметр base структуры gpio_chip может быть определен как -1, тогда смещение будет определено как первый свободный диапазон начиная с конца, то есть если у порта количество контактов равно 8 смещение будет равно 248 при параметре ARCH_NR_GPIOS равном 256 (ARCH_NR_GPIOS — ngpio) в случае если порт регистрируется в системе первым.

Определим следующие функции нашего драйвера



Задать соответствующий контакт как вход:
static int virtual_gpio_direction_input(struct gpio_chip *gpio, unsigned nr)
static int virtual_gpio_direction_input(struct gpio_chip *gpio, unsigned nr)
{
   struct virtual_gpio *vg = to_virtual_gpio(gpio);
   unsigned long flags;
   u8 outen, data;

   spin_lock_irqsave(&vg->lock, flags);

   data = vgread(VIRTUAL_GPIO_DATA);
   data &= ~(1 << nr);
   vgwrite(data, VIRTUAL_GPIO_DATA);

   outen = vgread(VIRTUAL_GPIO_OUT_EN);
   outen &= ~(1 << nr);
   vgwrite(outen, VIRTUAL_GPIO_OUT_EN);

   spin_unlock_irqrestore(&vg->lock, flags);

   return 0;
}


Чтение текущего состояния контакта:
static int virtual_gpio_get(struct gpio_chip *gpio, unsigned nr)
static int virtual_gpio_get(struct gpio_chip *gpio, unsigned nr)
{
   struct virtual_gpio *vg = to_virtual_gpio(gpio);
   unsigned long flags;
   u8 data;

   spin_lock_irqsave(&vg->lock, flags);
   data= vgread(VIRTUAL_GPIO_DATA);
   spin_unlock_irqrestore(&vg->lock, flags);

   return !!(data & (1 << nr));
}


Задать соответствующий контакт как выход:
static int virtual_gpio_direction_output(struct gpio_chip *gpio, unsigned nr, int val)
static int virtual_gpio_direction_output(struct gpio_chip *gpio, unsigned nr, int val)
{
   struct virtual_gpio *vg = to_virtual_gpio(gpio);
   unsigned long flags;
   u8 outen, data;

   spin_lock_irqsave(&vg->lock, flags);

   outen = vgread(VIRTUAL_GPIO_OUT_EN);
   outen |= (1 << nr);
   vgwrite(outen, VIRTUAL_GPIO_OUT_EN);

   data = vgread(VIRTUAL_GPIO_DATA);
   if (val)
       data |= (1 << nr);
   else
       data &= ~(1 << nr);
   vgwrite(data, VIRTUAL_GPIO_DATA);

   spin_unlock_irqrestore(&vg->lock, flags);

   return 0;
}


Задать состояние выхода:
static void virtual_gpio_set(struct gpio_chip *gpio, unsigned nr, int val)
static void virtual_gpio_set(struct gpio_chip *gpio, unsigned nr, int val)
{
   struct virtual_gpio *vg = to_virtual_gpio(gpio);
   unsigned long flags;
   u8 data;

   spin_lock_irqsave(&vg->lock, flags);

   data = vgread(VIRTUAL_GPIO_DATA);

   if (val)
       data |= (1 << nr);
   else
       data &= ~(1 << nr);

   vgwrite(data, VIRTUAL_GPIO_DATA);

   spin_unlock_irqrestore(&vg->lock, flags);
}


Функция регистрации нашего драйвера как устройства gpio_chip:
static void virtual_gpio_setup(struct virtual_gpio *gpio)
static void virtual_gpio_setup(struct virtual_gpio *gpio)
{
   struct gpio_chip *chip = &gpio->chip;

   chip->label = dev_name(&gpio->pdev->dev);
   chip->owner = THIS_MODULE;
   chip->direction_input = virtual_gpio_direction_input;
   chip->get = virtual_gpio_get;
   chip->direction_output = virtual_gpio_direction_output;
   chip->set = virtual_gpio_set;
   chip->dbg_show = NULL;
   chip->base = modparam_gpiobase;
   chip->ngpio = VIRTUAL_GPIO_NR_GPIOS;
   chip->can_sleep = 0; // gpio never sleeps!
}


vgread и vgwrite это просто обертки для функций iowrite8 и ioread8:
#define vgwrite(dat, adr)   iowrite8((dat), vg->data_base_addr+(adr))
#define vgread(adr)         ioread8(vg->data_base_addr+(adr))


Передача значения gpiobase в качестве параметра при динамической загрузки модуля



Примечание: Начиная с версии 4.2 являетя рекомендованным способом регистрации порта GPIO.
static int modparam_gpiobase = -1; /* dynamic */
module_param_named(gpiobase, modparam_gpiobase, int, 0444);
MODULE_PARM_DESC(gpiobase, "The GPIO base number. -1 means dynamic, which is the default.");


Загрузка и тестирования модуля


h>> $ rm /dev/shm/ivshmem 

h>> Adding parameters to qemu launch command line  += -device ivshmem,shm=ivshmem,size=1

g>> # ls /sys/class/gpio/
export    unexport

g>> # insmod virtual_gpio_basic.ko
PCI: enabling device 0000:00:0d.0 (0100 -> 0102)
ivshmem_gpio 0000:00:0d.0: data_mmio iomap base = 0xc8a00000
ivshmem_gpio 0000:00:0d.0: data_mmio_start = 0x60000000 data_mmio_len = 1048576
ivshmem_gpio 0000:00:0d.0: regs iomap base = 0xc88e6400, irq = 27
ivshmem_gpio 0000:00:0d.0: regs_addr_start = 0x50002400 regs_len = 256

g>> # ls /sys/class/gpio/
export       gpiochip248  unexport

g>> # cat /sys/class/gpio/gpiochip248/label
0000:00:0d.0

g>> # cat /sys/class/gpio/gpiochip248/base
248

g>> # cat /sys/class/gpio/gpiochip248/ngpio
8

g>> # rmmod virtual_gpio_basic
Unregister virtual_gpio device.

g>> # insmod virtual_gpio_basic.ko gpiobase=0
g>> # ls /sys/class/gpio/
export     gpiochip0  unexport

g>> # echo 0 > /sys/class/gpio/export
g>> # echo high > /sys/class/gpio/gpio0/direction


Простая проверка:
h>>  $ xxd -b -l 2 -c 2 /dev/shm/ivshmem
0000000: 00000001 00000001  ..


DATA выставлен, OUTPUTEN выставлен.

Добавляем прерывания



Разметка регистров прерываний и базовая обработка прерывания



Примечание: В виртуальном драйвере рассматриваются только EDGEDETECT_RISE и EDGEDETECT_FALL.

Примечание: Пожалуйста, используйте только qemu версии старше 2.5.0 или qemu-linaro. Поддежрка прерываний ivshmem сломана в 2.5.0 или просто не работает в некоторых версиях младше 2.5.0. Если использование 2.5.0 необходимо воспользуйтесь патчем для 2.5.0 ( http://lists.gnu.org/archive/html/qemu-stable/2015-12/msg00034.html ).

Добавляем следующие регистры:
Имя регистра Смещение Имя в драйвере Описание
INTERRUPT_EN 0x01 VIRTUAL_GPIO_INT_EN Включает прерывания по заданному входу
INTERRUPT_ST 0x02 VIRTUAL_GPIO_INT_ST Регистр состояния прерывания
INTERRUPT_EOI 0x03 VIRTUAL_GPIO_INT_EOI Регистр для оповещения об обработанном прерывании
EDGEDETECT_RISE 0x04 VIRTUAL_GPIO_RISING Включение/выключения прерывания для входа по переднему фронту
EDGEDETECT_FALL 0x05 VIRTUAL_GPIO_FALLING Включение/выключения прерывания для входа по заднему фронту
LEVELDETECT_HIGH NC NOT CONNECTED
LEVELDETECT_LOW NC NOT CONNECTED


За обработку прерывания от pci шины отвечает следующая функция, на данный момент её роль заключается всего лишь в уведомлении об обработанном прерывании:
static irqreturn_t virtual_gpio_interrupt(int irq, void *data)
 static irqreturn_t virtual_gpio_interrupt(int irq, void *data)
 {
       u32 status;             

       struct virtual_gpio *vg = (struct virtual_gpio *)data;

       status = readl(vg->regs_base_addr + IntrStatus);

       if (!status || (status == 0xFFFFFFFF))
               return IRQ_NONE;

       printk(KERN_INFO "VGPIO: interrupt (status = 0x%04x)\n", status); 

       return IRQ_HANDLED;
 }


Для данного этапа потребуется внешний демон, которой включен в стандартную поставку qemu — ivshmem-server. В строку запуска qemu добавляется параметр -chardev путь к UNIX сокету, обмен сообщениями между запущенными экзеплярами qemu, ivshmem-server и ivshmem-client реализован с помощью механизма eventfd.
h>> $ ivshmem-server -v -F -p ivshmem.pid -l 1M
# запускаем qemu с новыми параметрами
h>> $ += -chardev socket,path=/tmp/ivshmem_socket,id=ivshmemid -device ivshmem,chardev=ivshmemid,size=1,msi=off

g>> # echo 8 > /proc/sys/kernel/printk
g>> # insmod virtual_gpio_basic.ko

h>> $ ivshmem-client
# каждый экземпляр qemu ivshmem региструет себя в ivshmem-server и ему присваивается уникальный id
cmd> int 0 0

# Примечание: листинг доступных команд можно посмотреть командой cmd> help

# Вывод гостевой машины:

g>> VGPIO: interrupt (status = 0x0001)


irq_chip и концепция chained_interrupt



Мы не будем углубляться в детали, данная тема хорошо раскрыта в первом патче представившим irq_chip , документации ядра и книге “Professional Linux Kernel Architecture” (к настоящему моменту она устарела, но irq_chip это так же не новая вещь).

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

Вот почему часть драйвера GPIO отвечающего за прерывания использует irq_chip. Другими словами такой драйвер использует две подсистемы одновременно: gpio_chip и irq_chip.

Беглый взгляд на подсистему irq дает нам следующую картину:


High-Level Interrupt Service Routines (ISRs) — Выполняет всю необходимую работу по обслуживанию прерывания на драйвере устройства. Например, если прерывание используется для индикации доступных для чтения новых данных, работа ISR будет заключаться в копировании данных в соответствующее место.

Interrupt Flow Handling — Данная подсистема отвечает за особенности в реализации обработок прерываний, таких как срабатывание по уровню сигнала (level) или по фронту (edge).

Срабатывание по фронту (Edge-triggering) происходит при определении, что на линии произошло изменение потенциала. Срабатывание по уровню (Level-triggering), определяется как определенное значение потенциала, при этом изменение потенциала не играет роли.

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

Chip-Level Hardware Encapsulation — Используется для инкапсуляции особенностей реализации работы с аппаратной частью. Данную подсистему можно рассматривать как разновидность “драйвера устройства” для контроллеров прерываний.


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

IRQ Domains



Подсистема IRQ Domain появившееся в патче irq: add irq_domain translation infrastructure позволила отделить локальные для контроллера номера прерываний от номеров прерываний в ядре, предоставив общий массив номеров прерываний. Цитируя официальную документацию: «Сегодня номер IRQ, это просто номер».

До данного обновления аппаратные номера отображались на номерами ядра как 1:1, а каскадирование не поддерживалось. Под аппаратными номерами, понимается локальные для контроллера номера прерывания, которые в нашем случае совпадают с локальными номерами GPIO.

В IRQ Domain существуют следующие типы отображения:
  • Линейное
  • В виде дерева
  • И тип "No map" (Без отображения)


Поскольку наш вектор прерываний достаточно мал, и у нас точно нет интереса в "No map" отображении, наше отображение линейно, фактически номера сопоставляются 1:1 со смещением, разница со старым подходом состоит в том что за присвоение номеров irq и за вычисление смещения отвечает ядро, при этом гарантируется непрерывность выделяемого диапазона.

В каждую функцию интерфейса irq_chip передается указатель на структуру struct irq_data, где irq_data->irq это номер прерывания в ядре linux, a irq_data->hwirq это наш локальный номер прерывания в рамках драйвера. Так же в struct irq_data передается указатель на нашу структуру struct virtual_gpio, что неудивительно.

Связывание irq_chip и gpio_chip



Если бы мы ориентировались на более младшие версии ядра, нам пришлось бы воспользоваться функцией irq_domain_add_simple для отображения наших номер, но с версии 3.15 в патче gpio: add IRQ chip helpers in gpiolib patch нет необходимости напрямую использовать интерфейс IRQ Domain.

Поэтому вместо прямого использования интерфейса IRQ Domain и предоставления инфраструктуры для отображения локальных номеров на глобальные (.map() ops), мы воспользуемся функциями gpiochip_irqchip_add и gpiochip_set_chained_irqchip (зависят от параметра GPIOLIB_IRQCHIP Kconfig).

Прекрасным примером использования и простоты в применении, является драйвер gpio-pl061.

Привязываем наш irq_chip к уже существующему gpio_chip:
gpiochip_irqchip_add(&vg->chip,
                     &virtual_gpio_irq_chip,
                     0,
                     handle_edge_irq,
                     IRQ_TYPE_NONE);


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

Примечание: прерывания по фронтам является наиболее распространенным. Главное отличие от прерываний по уровню заключается как раз в управлении цепочкой, прерывание по уровню маскируется в ядре сразу после получения.
gpiochip_set_chained_irqchip(&vg->chip,
                             &virtual_gpio_irq_chip,
                             pdev->irq,
                             NULL);


Вызовом функции gpiochip_set_chained_irqchip мы сообщаем ядру, что наш irq_chip использует прерывание от PCI шины и наши прерывания каскадируются от pdev->irq.

Доработаем наш обработчик, чтобы он генерировал прерывания в зависимости от состояния VIRTUAL_GPIO_INT_ST:
pending = vgread(VIRTUAL_GPIO_INT_ST);
/* check if irq is really raised */
if(pending)
{
    for_each_set_bit(i, &pending, VIRTUAL_GPIO_NR_GPIOS)       
        generic_handle_irq(irq_find_mapping(vg->chip.irqdomain, i));
}


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

Собираем все вместе



Прежде всего, отметим, что интерфейс irq_chip нашего драйвера, выглядит следующим образом:
static struct irq_chip virtual_gpio_irq_chip = {
    .name           = "GPIO",
    .irq_ack        = virtual_gpio_irq_ack,
    .irq_mask       = virtual_gpio_irq_mask,
    .irq_unmask     = virtual_gpio_irq_unmask,
    .irq_set_type   = virtual_gpio_irq_type,
};


Функция ack() всегда тесна связана с аппаратной спецификой контроллера. Некоторым устройствам, например требуется подтверждение обработки запроса прерывания, прежде чем могут быть обслужены последующие запросы.
static void virtual_gpio_irq_ack(struct irq_data *d)
static void virtual_gpio_irq_ack(struct irq_data *d)
{   
   unsigned long flags;
   u8 nr = d->hwirq;
   u8 mask = 1 << nr;

   struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
   struct virtual_gpio *vg = to_virtual_gpio(gc);

   spin_lock_irqsave(&vg->lock, flags);
   vgwrite(mask, VIRTUAL_GPIO_INT_EOI);
   spin_unlock_irqrestore(&vg->lock, flags);
}


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

Маскирование и демаскирование производится записью соответствующего значения в регистр INTERRUPT_EN.

Маскирование прерывания:
static void virtual_gpio_irq_mask(struct irq_data *d)
static void virtual_gpio_irq_mask(struct irq_data *d)
{
  u8 mask;
  unsigned long flags;
  u8 nr = d->hwirq;

  struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
  struct virtual_gpio *vg = to_virtual_gpio(gc);

  spin_lock_irqsave(&vg->lock, flags);
  mask = vgread(VIRTUAL_GPIO_INT_EN);
  mask &= ~(1 << nr);
  vgwrite(mask, VIRTUAL_GPIO_INT_EN);
  spin_unlock_irqrestore(&vg->lock, flags);
}


Демаскирование прерывания:
static void virtual_gpio_irq_unmask(struct irq_data *d)
static void virtual_gpio_irq_unmask(struct irq_data *d)
{
  u8 mask;
  unsigned long flags;
  u8 nr = d->hwirq;

  struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
  struct virtual_gpio *vg = to_virtual_gpio(gc);

  spin_lock_irqsave(&vg->lock, flags);
  mask = vgread(VIRTUAL_GPIO_INT_EN);
  mask |= (1 << nr);
  vgwrite(mask, VIRTUAL_GPIO_INT_EN);
  spin_unlock_irqrestore(&vg->lock, flags);
}


irq_type позволяет задать тип триггера — на текущий момент в ядре определены следующие типы:
IRQ_TYPE_NONE — тип не задан
IRQ_TYPE_EDGE_RISING — по переднему фронту
IRQ_TYPE_EDGE_FALLING — по заднему фронту
IRQ_TYPE_EDGE_BOTH — по переднему и заднему фронту
IRQ_TYPE_LEVEL_HIGH — по высокому уровню
IRQ_TYPE_LEVEL_LOW — по низкому уровню
static int virtual_gpio_irq_type(struct irq_data *d, unsigned int type)
static int virtual_gpio_irq_type(struct irq_data *d, unsigned int type)
{
  unsigned long flags;

  struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
  struct virtual_gpio *vg = to_virtual_gpio(gc);

  u8 mask;
  u8 nr = d->hwirq;

  spin_lock_irqsave(&vg->lock, flags);
  switch (type) {
  case IRQ_TYPE_EDGE_RISING:
      mask = vgread(VIRTUAL_GPIO_RISING);
      mask |= (1 << nr);
      vgwrite(mask, VIRTUAL_GPIO_RISING);

      mask = vgread(VIRTUAL_GPIO_FALLING);
      mask &= ~(1 << nr);
      vgwrite(mask, VIRTUAL_GPIO_FALLING);
      break;
  case IRQ_TYPE_EDGE_FALLING:
      mask = vgread(VIRTUAL_GPIO_FALLING);
      mask |= (1 << nr);
      vgwrite(mask, VIRTUAL_GPIO_FALLING);

      mask = vgread(VIRTUAL_GPIO_RISING);
      mask &= ~(1 << nr);
      vgwrite(mask, VIRTUAL_GPIO_RISING);
      break;
  default:
      retval = -EINVAL;
      goto end;
  }

  /* enable interrupt */
  mask = vgread(VIRTUAL_GPIO_INT_EN);
  mask &= ~(1 << nr);
  vgwrite(mask, VIRTUAL_GPIO_INT_EN);

end:
  spin_unlock_irqrestore(&vg->lock, flags);
  return retval;
}

Тестирование и результаты



Для тестирования передачи информации о прерываниях в user space, воспользуемся специально написанной утилитой vg_guest_client. Согласно документации по gpio_sysfs, “Если вы используете select для отслеживания событий, задайте файловый дескриптор (входа) в exceptfds”.

Соответствующий код:
 FD_ZERO(&efds);       
 maxfd = 0;

 for(i = 0; i < gpio_size; i++)
 {
     FD_SET(gpios[i].fd, &efds);
     maxfd = (maxfd < gpios[i].fd) ? gpios[i].fd : maxfd;
 }

 ready = pselect(maxfd + 1, NULL, NULL, &efds, NULL, NULL);

 if(ready > 0)
     for(i = 0; i < gpio_size; i++)
         if(FD_ISSET(gpios[i].fd, &efds)) {
             read(gpios[i].fd, &value, 1);
 /* для пояснений использования lseek  смотрите http://lxr.free-electrons.com/source/fs/kernfs/file.c?v=4.1#L769 */ 
             if(lseek(gpios[i].fd, 0, SEEK_SET) == -1)
                 perror("lseek");
             printf("gpio number=%d interrupt caught\n", gpios[i].number);
         }


Подготавливаем входы к работе при помощи sysfs:
g>> # echo 504 > /sys/class/gpio/export
g>> # echo 505 > /sys/class/gpio/export
g>> # echo 506 > /sys/class/gpio/export
g>> # echo rising > /sys/class/gpio/gpio504/edge
g>> # echo rising > /sys/class/gpio/gpio505/edge
g>> # echo rising > /sys/class/gpio/gpio506/edge


Примечание: gpio на подавляющем большинстве устройств по умолчанию инициализируются как входы.
# в качестве аргумента используется номер gpiochip в системе
g>> # ./vg_guest_client 504
gpio_chip:
       base: 504
       ngpio: 8
Added gpio 504 to watchlist.
Added gpio 505 to watchlist.
Added gpio 506 to watchlist.
Entering loop with 3 gpios.

h>> $ ./vg_get_set -p 1 -i 0
g>> gpio number=504 interrupt caught


Цепочка вызовов от нашего обработчика прерывания к уведомлению pselect:
static irqreturn_t virtual_gpio_interrupt (int irq, void *data)
int generic_handle_irq(unsigned int irq);
...
static irqreturn_t gpio_sysfs_irq(int irq, void *priv);
static inline void sysfs_notify_dirent(struct kernfs_node *kn);
void kernfs_notify(struct kernfs_node *kn);
static void kernfs_notify_workfn(struct work_struct *work);


Заключение



Данная статья подразумевалась мной, как базовая для материала, который сложно, или даже невозможно, представить без какого-либо общего вступления. Qemu в паре с ivshmem послужили отличным и понятным базисом для этой цели. Причиной выбора этой конкретной связки является наличие вменяемой документации и прозрачности использования.

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

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

Тем не менее, взяв в качестве основы данный драйвер, следующие темы могут быть раскрыты и объяснены:
  • Интеграция с подсистемой Device Tree и использование в качестве источника прерываний
  • Использование драйвера generic-gpio для упрощения разработки mmio gpio драйверов
  • Реализация на базе нетипичных устройств, например GPIO на АЦП
  • Специальные драйвера основанные на gpio — кнопки, диоды, питание и сброс


Так же нельзя упускать из виду последние изменения в gpiolibsysfs gpio теперь является устаревшей. Новый основанный на ioctl интерфейс для gpiolib на пути становления как новый стандарт для общения с GPIO. Но младшие версии еще долго будут использоваться, к тому же никто не собирается на данный момент убирать из ядра старый интерфейс. У меня например до сих пор есть устройства успешно работающие на версии ядра 2.6.34.

Список материалов:
  1. http://nairobi-embedded.org/category/device-drivers.html [Siro Mugabi]
  2. http://lxr.free-electrons.com/source
  3. Professional Linux Kernel Architecture [Wolfgang Mauerer]
  4. LDD3 [Jonathan Corbet, Alessandro Rubini, and Greg Kroah-Hartman]


Материалы рекомендованные к дополнительному чтению:
  1. http://derekmolloy.ie/writing-a-linux-kernel-module-part-1-introduction/ (все три части)
  2. https://developer.ridgerun.com/wiki/index.php?title=Gpio-int-test.c
  3. http://www.assert.cc/2015/01/03/selects-exceptional-conditions.html


Исходные коды, Makefile и README:
https://github.com/maquefel/virtual_gpio_basic
Теги:
Хабы:
Всего голосов 15: ↑15 и ↓0+15
Комментарии3

Публикации

Истории

Работа

Программист С
32 вакансии

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань