Трудно недооценить роль 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 контакта каждый)
Имя регистра | Смещение | Имя в драйвере | Комментарий |
---|---|---|---|
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 контактов каждый)
Имя регистра | Смещение | Имя в драйвере | Описание |
---|---|---|---|
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_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, например:
#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)
{
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)
{
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)
{
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)
{
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)
{
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)
{
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)
{
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)
{
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)
{
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)
{
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 caughtn", 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 — кнопки, диоды, питание и сброс
Так нельзя упускать из виду так же последние изменения в gpiolib — sysfs gpio теперь является устаревшей. Новый основанный на ioctl интерфейс для gpiolib на пути становления как новый стандарт для общения с GPIO. Но младшие версии еще долго будут использоваться, к тому никто не собирается на данный момент убирать из ядра старый интерфейс. У меня например до сих пор есть устройства успешно работающие на версии ядра 2.6.34.
Список материалов:
- http://nairobi-embedded.org/category/device-drivers.html [Siro Mugabi]
- http://lxr.free-electrons.com/source
- Professional Linux Kernel Architecture [Wolfgang Mauerer]
- LDD3 [Jonathan Corbet, Alessandro Rubini, and Greg Kroah-Hartman]
Материалы рекомендованные к дополнительному чтению:
- http://derekmolloy.ie/writing-a-linux-kernel-module-part-1-introduction/ (все три части)
- https://developer.ridgerun.com/wiki/index.php?title=Gpio-int-test.c
- http://www.assert.cc/2015/01/03/selects-exceptional-conditions.html
Исходные коды, Makefile и README:
https://github.com/maquefel/virtual_gpio_basic
Автор: maquefel