Тема этой статьи преследует меня, как статуя командора из известной сказки. Почти десять лет назад я сделал возможность чтения и записи 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, ®_array->mem);
[...]
}
Мы добавляем блок регистров как еще один регион памяти внутри основной апертуры. Вместо этого можно использовать ®_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, а также уведомления об изменении состояния и конфигурации входов/выходов,
пользователям нужны нормальные инструменты и библиотеки.
Об этом мы поговорим в следующей статье.