Тема этой статьи преследует меня, как статуя командора из известной сказки. Почти десять лет назад я сделал возможность чтения и записи GPIO для виртуальной машины QEMU. GPIO был нужен для тестирования алгоритмов контроллера взвешивания в движении (Weigh In Motion, WIM). С тех пор проект получил некоторое количество упоминаний, а я — несколько писем. И вот к десятилетнему юбилею я решил поставить точку в этой работе.

Меня зовут Никита Шубин, я ведущий инженер по разработке СнК в YADRO. Моя первая реализация GPIO основывалась на QEMU ivshmem и представляла собой просто область памяти, разделяемой между машиной и пользовательским пространством. Эту работу я подробно описал в статье «Драйвер виртуальных GPIO с контроллером прерываний на базе QEMU ivshmem для Linux». Ее недостатки были очевидны с самого начала:

  • использование ivshmem в качестве базы накладывало дополнительные требования,

  • инструментарий для взаимодействия с GPIO внутри QEMU носил скорее демонстрационный, чем практический характер,

  • возможность использования была только на машинах с PCI-шиной.

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

Новое устройство в QEMU

Для начала я решил отказаться от ivshmem, ведь он изначально не предназначен для этой цели. В свое время я пошел по пути наименьшего сопротивления и не стал модифицировать QEMU — десять лет назад я просто не так хорошо его знал. ivshmem требует наличия:

  • «сервера», запускаемого отдельно от QEMU,

  • шины PCI в запускаемой машине.

Кроме того, поскольку ivshmem — это всего лишь разделяемая между QEMU и хостом память, то для симуляции GPIO мы фактически перекладываем всю структуру на клиентское приложение и не можем контролировать корректность доступа внутри.

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

Имя регистра

Смещение

Свойства

Описание

DATA

0x00

RO

Текущее состояние входов/выходов

SET

0x04

W1S

Задать выход как 1

CLEAR

0x08

W1C

Сбросить выход к 0

DIR_OUT

0x0c

RW

Назначить линию как выход

ISTATUS

0x10

RW

Состояние прерываний

EOI

0x14

W1C

Сбросить прерывание

IEN

0x18

RW

Включить/выключить прерывание

RISEN

0x1c

RW

Включить/выключить прерывание по переднему фронту

FALLEN

0x20

RW

Включить/выключить прерывание по заднему фронту

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

Компоненты проекта:

  • qemu v10.0.0 c моделью MMIO GPIO и модифицированной машиной RISC-V virt,

  • linux v6.12 c патчами для dtb-инъекций и драйвером для QEMU MMIO GPIO,

  • ванильный buildroot 2025.02.2.

Скачать проект-обертку.

Пример использования

Как я уже упоминал, внешнее управление входами в QEMU использовалось для алгоритмов WIM. На реальном железе через блок сопряжения NAMUR к входам подключены датчики колеса. Возможность управления входом/выходом и считывания состояния линии входа может пригодиться при тестировании и разработке прошивок для:

  • датчиков,

  • кнопок и лампочек,

  • и даже такой экзотики, как шина SGPIO.

Готовые артефакты для ядра и rootfs я дам, а для QEMU — нет. Чтобы просто поиграться, достаточно собрать QEMU и скачать артефакты:

$ git clone https://gitflic.ru/project/maquefel/qemu-gpio-playground
$ git submodule update --init --depth 1 -- qemu
$ make .build-qemu
$ wget https://gitflic.ru/project/maquefel/qemu-gpio-playground/release/037789b9-f059-409b-9087-df5fe92d6c5a/be46609c-7755-4086-a92d-6d20a4fa3889/download -O initramfs.cpio.xz
$ wget https://gitflic.ru/project/maquefel/qemu-gpio-playground/release/037789b9-f059-409b-9087-df5fe92d6c5a/ad6d8ae3-6301-404d-9681-c263658d9da2/download -O Image

Пример запуска модифицированной машиной RISC-V virt с GPIO SYSBUS:

$ build-qemu/qemu-system-riscv64 -machine virt -m 1G -kernel Image -initrd initramfs.cpio.xz -append "root=/dev/ram" -qmp unix:./qmp-sock,server,wait=off -nographic -serial mon:stdio
 
qemu-system-riscv64 # modprobe gpio-qemu
qemu-system-riscv64 # gpioinfo
gpiochip0 - 32 lines:
        line   0:       unnamed                 input
        line   1:       unnamed                 input
        line   2:       unnamed                 input
        line   3:       unnamed                 input
        line   4:       unnamed                 input
        line   5:       unnamed                 input
        line   6:       unnamed                 input
        line   7:       unnamed                 input
        line   8:       unnamed                 input
        line   9:       unnamed                 input
        line  10:       unnamed                 input
        line  11:       unnamed                 input
        line  12:       unnamed                 input
        line  13:       unnamed                 input
        line  14:       unnamed                 input
        line  15:       unnamed                 input
        line  16:       unnamed                 input
        line  17:       unnamed                 input
        line  18:       unnamed                 input
        line  19:       unnamed                 input
        line  20:       unnamed                 input
        line  21:       unnamed                 input
        line  22:       unnamed                 input
        line  23:       unnamed                 input
        line  24:       unnamed                 input
        line  25:       unnamed                 input
        line  26:       unnamed                 input
        line  27:       unnamed                 input
        line  28:       unnamed                 input
        line  29:       unnamed                 input
        line  30:       unnamed                 input
        line  31:       unnamed                 input

Пока остается единственный вариант — задать или опросить состояние через QMP. Так мы сможем указать состояние входа/выхода следующим образом:

host $ tools/gpio-mmio-toggle.sh ./qmp-sock 0 1
{"QMP": {"version": {"qemu": {"micro": 91, "minor": 2, "major": 9}, "package": "v10.0.0-rc1-9-gc25d917239-dirty"}, "capabilities": ["oob"]}}
{"return": {}}
{"return": {}}

И получить ожидаемую реакцию внутри «гостя»:

qemu-system-riscv64 # gpiomon -c 0 0
5112.382098300  rising  gpiochip0 0
5115.131181200  falling gpiochip0 0

Считать состояние входа/выхода:

qemu-system-riscv64 #  gpioset -c 0 0=1
host $ tools/gpio-mmio-toggle.sh ./qmp-sock 0
{"QMP": {"version": {"qemu": {"micro": 91, "minor": 2, "major": 9}, "package": "v10.0.0-rc1-9-gc25d917239-dirty"}, "capabilities": ["oob"]}}
{"return": {}}
{"return": 1}

Модель GPIO для QEMU

Рассмотрим, как создать простейшую модель QEMU в двух вариантах, которые я условно решил назвать MMIO и PCI. Последний — тоже MMIO, но в QEMU они добавляются разными путями.

Мы начнем с сердца любой MMIO-модели — апертуры.

Апертура и адресное пространство

Как я упоминал в одной из своих статей, любое MMIO-устройство — это MemoryRegion с заданными шириной доступа и размером. Для того, чтобы он был виден CPU или другому устройству, такому как DMA, его нужно разместить в соответствующем адресном пространстве — например, пространстве, назначенном для cpu0:

      0x0                                    0xffffffffffffffff
      |------|------|------|------|------|------|------|------|
0:    [                    address-space: cpu-memory-0        ]
0:    [                    address-space: memory              ]
                    0x102000           0x1023ff
0:                  [             gpio        ]

Рекомендую прочитать официальную документацию QEMU, эта тема там хорошо описана.

В любое время можно посмотреть существующие адресные пространства и регионы памяти в мониторе QEMU:

(qemu) info mtree
[...]
address-space: cpu-memory-0
address-space: memory
  0000000000000000-ffffffffffffffff (prio 0, i/o): system
    0000000000102000-00000000001023ff (prio 0, i/o): gpio
[...]

Тогда в модели устройства нам нужно всего лишь создать такой регион и назначить ему соответствующие функции записи и чтения:

static const MemoryRegionOps mmio_mmio_ops = {
    .read = mmio_gpio_register_read_memory,
    .write = mmio_gpio_register_write_memory,
    .endianness = DEVICE_NATIVE_ENDIAN,
    .valid = {
        .min_access_size = 4,
        .max_access_size = 4,
    },
};
 
[...]
memory_region_init_io(iomem, obj, &mmio_mmio_ops, s,
                      "gpio", APERTURE_SIZE);
[...]

Фактически это означает, что все семейство инструкций Load/Store будет вызывать mmio_gpio_register_read_memory()/mmio_gpio_register_write_memory() при совпадении адреса чтения/записи с адресом региона в адресном пространстве.

static uint64_t mmio_gpio_register_read_memory(void *opaque, hwaddr addr, unsigned size);
static void mmio_gpio_register_write_memory(void *opaque, hwaddr addr, uint64_t value, unsigned size);

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

Нам остается лишь создать устройство и добавить его регион в файле машины:

gpio = qdev_new(TYPE_MMIO_GPIO);
sysbus_mmio_map(SYS_BUS_DEVICE(gpio), 0, ADDRESS);

Здесь ADDRESS— это абсолютный адрес устройства, а TYPE_MMIO_GPIO — просто строка, определенная в заголовочном файле:

#define TYPE_MMIO_GPIO "mmio-gpio"

mmio_gpio_register_read_memory()/mmio_gpio_register_write_memory()

Вернемся к нашим функциям чтения/записи апертуры. Здесь нам необходимо смоделировать реакцию устройства на чтение/запись его регистров. В простейшем случае мы можем просто хранить значение записанного значения для входа/выхода и возвращать его при чтении. Тогда псевдокод для записи можно представить таким образом:

static void mmio_gpio_register_write_memory(void *opaque, hwaddr addr,
                                            uint64_t value, unsigned size)
{
  uint32_t val32 = value;
 
  switch(addr) {
  case 0x04: // SET
    data |= val32;
    break;
  case 0x08: // CLEAR
    data &= ~val32;
    break;
  case 0x00: // DATA
    /* только для чтения */
  default:
    /* можно сообщить об ошибке */
  }
 
  [...]
}

А таким — для чтения:

static uint64_t mmio_gpio_register_read_memory(void *opaque, hwaddr addr,
                                               unsigned size)
{
  uint32_t val32 = 0;
 
  switch(addr) {
    case 0x00: // DATA
      val32 = data;
      break;
    case 0x04: // SET
    case 0x08: // CLEAR
      /* только для записи */
      break;
    default:
      /* можно сообщить об ошибке */
  }
 
  [...]
 
  return val32;
}

Это не значит, что в каждой модели обязательно должны быть регистры — например, мы можем моделировать отображенную в адресное пространство FLASH-память. Тогда наша модель будет позволять писать и читать произвольный адрес условно произвольного размера — то есть любого из 1/2/4/8, если есть инструкции Load/Store соответствующей ширины. Для регистровых моделей нам доступен Register API.

Register API и прочий сахар

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

Во-первых, это макросы REG8/16/32/64(reg, addr). Их цель — из названия и относительного адреса сделать соответствующие значения вида:

enum { A_ ## reg = (addr) };
enum { R_ ## reg = (addr) / (1, 2, 4, 8) };

Во-вторых — макросы FIELD_EX/SEX(storage, reg, field) и FIELD_DP/SDP(storage, reg, field, val) для каждой длины. Они представляют собой обертки для функций extract/deposit, которые предназначены для работы с битовыми полями.

В-третьих — структура struct RegisterAccessInfo и семейство функций register_init_block() для описания и регистрации блока регистров, соответственно. Они достаточно хорошо документированы, поэтому просто выделю основные моменты:

  • Чтение и запись регистра берет на себя Register API с учетом масок и сообщает об ошибке — например, при записи в биты, помеченные только для чтения (-d guest_errors,unimp):

    • ro — read-only,

    • w1c — write one to clear,

    • rsvd — reserved bits,

    • unimp — unimplemented bits.

  • Мы можем добавить реакцию на чтение/запись с помощью назначенных функций:

    • pre_write — позволяет поменять значение до записи в соответствующий регистр и предпринять действия, связанные с записью в этот регистр,

    • post_write — получить значение после применения всех масок и предпринять какие-либо действия,

    • post_read — обязательные после чтения регистра действия, например сбросить прерывание. 

В нашей модели регистры «монолитные», то есть без полей:

/* common gpio regs */
REG32(GPIO_QEMU_DATA,       0x00)
REG32(GPIO_QEMU_SET,        0x04)
REG32(GPIO_QEMU_CLEAR,      0x08)
REG32(GPIO_QEMU_DIR_OUT,    0x0c)
 
/* intr gpio regs */
REG32(GPIO_QEMU_ISTATUS,    0x10)
REG32(GPIO_QEMU_EOI,        0x14)
REG32(GPIO_QEMU_IEN,        0x18)
REG32(GPIO_QEMU_RISEN,      0x1c)
REG32(GPIO_QEMU_FALLEN,     0x20)

Хотя можно было бы принять и такую форму записи:

REG32(GPIO_QEMU_DATA,       0x00)
    FIELD(GPIO_QEMU_DATA, PIN0, 0, 1)
    FIELD(GPIO_QEMU_DATA, PIN1, 1, 1)
    [...]
    FIELD(GPIO_QEMU_DATA, PIN31, 31, 1)

Но я счел это нецелесообразным.

К описанию регистров в REG32 добавляем описание RegisterAccessInfo. Эта форма записи мне нравится своей формализованностью и наглядностью:

static const RegisterAccessInfo mmio_gpio_regs_info[] = {
    {
        .name  = "DATA",
        .addr  = A_GPIO_QEMU_DATA,
        .reset = 0x00000000,
        .ro    = 0xffffffff,
    }, {
        .name  = "SET",
        .addr  = A_GPIO_QEMU_SET,
        .reset = 0x00000000,
        .pre_write = mmio_gpio_set,
    }, {
        .name  = "CLEAR",
        .addr  = A_GPIO_QEMU_CLEAR,
        .reset = 0x00000000,
        .pre_write = mmio_gpio_clear,
    }, {
        .name  = "DIROUT",
        .addr  = A_GPIO_QEMU_DIR_OUT,
        .reset = 0x00000000,
    }, {
        .name = "ISTATUS",
        .addr = A_GPIO_QEMU_ISTATUS,
        .ro    = 0xffffffff,
        .reset = 0x00000000,
    }, {
        .name = "EOI",
        .addr = A_GPIO_QEMU_EOI,
        .reset = 0x00000000,
        .pre_write = mmio_gpio_intr_eoi,
    }, {
        .name = "IEN",
        .addr = A_GPIO_QEMU_IEN,
        .reset = 0x00000000,
    }, {
        .name = "RISEN",
        .addr = A_GPIO_QEMU_RISEN,
        .reset = 0x00000000,
    }, {
        .name = "FALLEN",
        .addr = A_GPIO_QEMU_FALLEN,
        .reset = 0x00000000,
    },
};

После этого достаточно зарегистрировать и добавить блок:

static void gpio_instance_init(Object *obj) {
    [...]
    memory_region_init(&s->mmio, obj,
                       "container."TYPE_GPIO, MMIO_GPIO_APERTURE_SIZE);
 
    reg_array =
        register_init_block32(DEVICE(obj), mmio_gpio_regs_info,
                              ARRAY_SIZE(mmio_gpio_regs_info),
                              s->regs_info, s->regs,
                              &mmio_mmio_ops, 0,
                              MMIO_GPIO_APERTURE_SIZE);
 
    memory_region_add_subregion(&s->mmio, 0x0, &reg_array->mem);
    [...]
}

Мы добавляем блок регистров как еще один регион памяти внутри основной апертуры. Вместо этого можно использовать &reg_array->mem как основной регион или создать несколько таких блоков, если регистры не являются непрерывными или у нас блоки с разным размером регистров.

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

  • mmio_gpio_set(),

  • mmio_gpio_clear(),

  • mmio_gpio_intr_eoi().

Все они — pre_write(). Как я уже отмечал выше, для регистра DATA в модели использованы семантики Write To Clear (w1c) и Write To Set (w1s). За каждую отвечает отдельный регистр с отдельной функцией обработки. Поэтому нам нужно просто выставить или очистить биты согласно переданному аргументу и вернуть 0, так как ни SET, ни CLEAR состояние не хранят — оно хранится в DATA.

Например, для mmio_gpio_set() код выглядит следующим образом:

static uint64_t mmio_gpio_set(RegisterInfo *reg, uint64_t val)
{
    GpioState *s = GPIO(reg->opaque);
    uint32_t val32 = val;
    unsigned idx;
 
    /* for each bit in val32 set DATA */
    idx = find_first_bit((unsigned long *)&val32, s->nr_lines);
    while (idx < s->nr_lines) {
        unsigned bit = test_and_set_bit32(idx, &s->regs[R_GPIO_QEMU_DATA]);
        if (!bit) {
            qemu_irq_raise(s->output[idx]);
        }
 
        idx = find_next_bit((unsigned long *)&val32, s->nr_lines, idx + 1);
    }
 
    return 0;
}

Очевидно, что mmio_gpio_clear() будет отличаться лишь в очень небольшой части:

static uint64_t mmio_gpio_clear(RegisterInfo *reg, uint64_t val)
    [...]
        unsigned bit = test_and_clear_bit32(idx, &s->regs[R_GPIO_QEMU_DATA]);
        if (bit) {
            qemu_irq_lower(s->output[idx]);
        }
    [...]
}

Функции qemu_irq_raise/lower() нужны, чтобы передать состояние выхода в другую модель: reset для I2C-датчика, LED, I2C Bitbang, SPGIO и так далее.

Если нам не нужно различать, какой именно вход/выход изменил свое состояние, то все еще проще, как это сделано в mmio_gpio_intr_eoi():

static uint64_t mmio_gpio_intr_eoi(RegisterInfo *reg, uint64_t val)
{
    GpioState *s = GPIO(reg->opaque);
    uint32_t val32 = val;
    uint32_t changed = val32 & s->regs[R_GPIO_QEMU_ISTATUS];
 
    if (!changed) {
        return 0;
    }
 
    s->regs[R_GPIO_QEMU_ISTATUS] &= ~val32;
    if (!s->regs[R_GPIO_QEMU_ISTATUS]) {
        gpio_lower_irq(s);
    }
 
    return 0;
}

Если значение регистра GPIO_QEMU_ISTATUS равно 0, тогда мы сбрасываем прерывание. В качестве альтернативы можно было бы убрать регистр EOI и назначить обработчик post_write() на ISTATUS, сделав его .w1c = 0xffffffff. В этом случае все свелось бы к:

static void mmio_gpio_intr_eoi(RegisterInfo *reg, uint64_t val)
{
    GpioState *s = GPIO(reg->opaque);
     
    if (!val) {
        gpio_lower_irq(s);
    }
}

Я не пользуюсь этим способом из-за непредсказуемости поведения qemu_irq_lower() в разных архитектурах. Функции qemu_irq_lower/raise() лучше использовать, только если нужно гарантированно вызвать или сбросить прерывание. 

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

На этом общие для MMIO и PCI моменты заканчиваются и начинаются различия.

MMIO GPIO

Сразу обозначу проблему: в QEMU нет механизмов добавления MMIO-устройств через командную строку или конфигурационный файл. Это нужно делать прямо в коде машины:

//  memmap[VIRT_MMIO_GPIO].base с адресом 0x102000 и апертурой 0x1000
//  GPIO_IRQ c номером 12 в RISC-V PLIC
sysbus_create_simple("mmio-gpio", memmap[VIRT_MMIO_GPIO].base,
                     qdev_get_gpio_in(mmio_irqchip, GPIO_IRQ));

Для добавления моделей через командную строку есть старый патч, но Xilinx пошел еще дальше и генерирует машину из описания в виде DTB (на основе заранее известных блоков, сопоставляя compatible и TYPE). В этот проект патч и Xilinx я не включал, но надеюсь, что очередь дойдет и до них.

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

// mmio_gpio_add_fdt() находится в коде самой модели
// 32 - количество входов выходов
mmio_gpio_add_fdt(&virt_memmap[VIRT_MMIO_GPIO], GPIO_IRQ,
                  irq_mmio_phandle, 32);

Тогда на выходе мы получим следующую запись:

gpio@102000 {
        gpio-controller;
        ngpios = <0x20>;
        interrupts = <0x0c>;
        interrupt-parent = <0x03>;
        compatible = "qemu,mmio-gpio";
        reg = <0x00 0x102000 0x00 0x400>;
};

Впрочем, запись можно добавить и в исходный dts-файл, либо как overlay в уже стартовавшее ядро. Последний вариант работает только с модифицированным ядром (патч OF: DT-Overlay configfs interface (v8)):

# /bin/mount -t configfs none /sys/kernel/config/
# mkdir /sys/kernel/config/device-tree/overlays/gpio
# cat gpio-mmio.dtb > /sys/kernel/config/device-tree/overlays/gpio/dtbo
# gpiodetect
gpiochip0 [102000.gpio] (32 lines)

Также можно использовать ACPI + ASL: встроенный в QEMU или инъектированный в работающее ядро. Но это уже совсем другая история.

PCI/PCIe GPIO

Эту модель, как и в случае с ivshmem, можно добавить только если есть PCI-шина. Зато это можно сделать с помощью аргумента:

$ build-qemu/qemu-system-riscv64 -machine virt,aia=aplic-imsic [...] -device pcie-gpio [...]

Или из командной строки QEMU (использован pcie-root-port, чтобы сработал hotplug):

$ build-qemu/qemu-system-riscv64 -machine virt,aia=aplic-imsic [...] -device pcie-root-port,id=pcie.1 [...]
(qemu) device_add pcie-gpio,bus=pcie.1

Драйвер для Linux

Нам более интересна модель QEMU, а не очередной драйвер для GPIO, но я предлагаю обратить внимание на динамику сокращения кода в самом драйвере. С появлением таких конструкций, как devm_regmap_add_irq_chip() и devm_gpio_regmap_register(), наша задача сводится к конфигурации этих функций, причем мы даже можем обойтись без хранения внутреннего состояния в драйвере.

После запроса ресурсов в qgpio_pci_probe()/qgpio_mmio_probe() мы просто создаем regmap:

struct regmap *map;
 
map = devm_regmap_init_mmio(dev, regs, &qgpio_regmap_config);

Его мы используем как для прерываний:

struct regmap_irq_chip_data *chip_data;
struct regmap_irq_chip *chip;
 
chip->status_base = GPIO_QEMU_ISTATUS;
chip->ack_base = GPIO_QEMU_EOI;
chip->unmask_base = GPIO_QEMU_IEN;
chip->num_regs = 1;
 
chip->irqs = qgpio_regmap_irqs;
chip->num_irqs = ARRAY_SIZE(qgpio_regmap_irqs);
chip->set_type_config = qgpio_mmio_set_type_config;
chip->irq_drv_data = map;
 
err = devm_regmap_add_irq_chip(dev, map, irq, 0, 0, chip, &chip_data);

Здесь мы указываем смещения регистров для управления прерываниями и задаем функцию управления типом прерывания qgpio_mmio_set_type_config(). Как я говорил выше, если в модели выделить два регистра по 32 бита, где каждый тип будет определен двумя битами (0 — выключено, 1 — по переднему фронту, 2 — по заднему фронту), то можно обойтись исключительно средствами regmap_irq_chip по умолчанию.

Так и для обычного управления входами/выходами:

struct gpio_regmap_config gpio_config = { 0 };
 
gpio_config.regmap = map;
gpio_config.ngpio  = 32;
gpio_config.reg_dat_base = GPIO_REGMAP_ADDR(GPIO_QEMU_DATA);
gpio_config.reg_set_base = GPIO_REGMAP_ADDR(GPIO_QEMU_SET);
gpio_config.reg_clr_base = GPIO_REGMAP_ADDR(GPIO_QEMU_CLEAR);
gpio_config.reg_dir_out_base = GPIO_REGMAP_ADDR(GPIO_QEMU_DIR_OUT);
gpio_config.irq_domain = regmap_irq_get_domain(chip_data);
 
return PTR_ERR_OR_ZERO(devm_gpio_regmap_register(dev, &gpio_config));

Так мы сократили код драйвера почти в два раза по сравнению с версией 5.14.

Заключение

В итоге мы с вами пришли к возможности использовать GPIO с любой машиной QEMU, на которой есть PCI-шина. А если ее нет, то встроить GPIO в существующее адресное пространство.

Кажется, мы выработали неплохое решение, но на поверку это не так:

  • в большинстве случаев пользователи не хотят GPIO в виде PCI или чужеродной для SoC модели MMIO — им нужно управлять тем, что уже есть в машинах по тем же адресам, что и в реальном железе: aspeed, rpi, stm32, gd32 и так далее,

  • пользователям нужна обратная связь от GPIO внутри QEMU, а также уведомления об изменении состояния и конфигурации входов/выходов,

  • пользователям нужны нормальные инструменты и библиотеки.

Об этом мы поговорим в следующей статье.

Комментарии (0)