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

Природа переривань

Важко недооцінити роль 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 0х02С 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 0х03С 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 0х14С 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("С", 0x08, 0x18, 40),
EP93XX_GPIO_BANK("D", 0x0c, 0x1c, 24),
EP93XX_GPIO_BANK("Е", 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;
}

/* interrupt enable */
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

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.