Мой долгий путь до GPIO в QEMU

от автора

Тема этой статьи преследует меня, как статуя командора из известной сказки. Почти десять лет назад я сделал возможность чтения и записи 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, а также уведомления об изменении состояния и конфигурации входов/выходов,

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

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


ссылка на оригинал статьи https://habr.com/ru/articles/925860/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *