Когда-то в отделе разработки встраиваемого ПО в YADRO мне задали вопрос: «А как с этим взаимодействовать?». Речь шла в первую очередь о I2C для QEMU, а не GPIO. И я некоторое время был одержим идеей «прозрачного» взаимодействия с устройствами внутри QEMU — использовать те же библиотеки и инструменты, как и для реальных устройств, что может быть прекраснее? Не какой-то там скрипт для посылки команды по QMP, а знакомый и целостный gpioset/gpioget из библиотеки libgpiod или поставляемые с ядром инструменты из tools/gpio.
Получилось ли это у меня? Да, но какой ценой…
QEMU GPIODEV: введение
Немного энтропии — это не так уж и плохо. В прошлой статье мы пришли к выводу, что QMP — это лучше, чем ничего. Но хочется большего — библиотеку или программу (желательно, уже готовую), которая умеет читать/писать и узнавать об изменении состояния через poll() / pselect() / select() / epoll() / read().
В таком случае для каждой модели GPIO нужен «клей», похожий на тот, что используется с chardev — мы включаем его прямо в модифицированный QEMU. Очевидное название такого «клея» — gpiodev. Вот его основные функции, которые сейчас почти полностью соответствуют GPIO UAPI в Linux:
сообщать количество линий, конфигурацию, название и потребителя каждой линии,
читать и задавать состояние линии,
отслеживать изменения состояния и конфигурации линии (вход/выход, запрос/освобождение).
«Клей» состоит из двух групп, первая — это индивидуальные для каждого модуля GPIO функции, которые gpiodev использует, чтобы запросить специфическую информацию:
LineInfoHandler() — информация о линии: имя, флаги и потребитель,
LineGetValueHandler() — состояние линии: условный 0 или 1,
LineSetValueHandler() — задать состояние линии: 0 или 1.
По аналогии с GPIO UAPI напрашиваются также функции LineGetMultiValueHandler() и LineSetMultiValueHandler() для запроса и выставления линий, но я решил ограничиться минимальным набором.
Запросы объединены в структуру, которая используется каждым чипом для связи с gpiodev:
/* qemu/include/gpiodev/gpio-fe.h */
struct GpioBackend {
Gpiodev *gpio;
LineInfoHandler *line_info;
LineGetValueHandler *get_value;
LineSetValueHandler *set_value;
void *opaque;
};
Вторая группа — это функции, с помощью которых модуль GPIO сообщает gpiodev об изменении своего состояния. Как правило, состояние может изменить эмулируемый процессор или другие блоки QEMU, например «внешний» датчик I2C:
qemu_gpio_fe_line_event() — линия изменила свое состояние,
qemu_gpio_fe_config_event() — линия изменила свою конфигурацию.
Самые первые прототипы gpiodev использовали структуры из uapi/linux/gpio.h напрямую, но позже я от них отказался, когда понял, что использование gpiodev может выйти за пределы локальной машины.
Для использования gpiodev каждый чип должен инициализировать свой интерфейс:
bool qemu_gpio_fe_init(GpioBackend *b, Gpiodev *s, uint32_t nlines,
const char *name, const char *label,
Error **errp);
Чип сообщает свое имя, количество линий, а также устанавливает соответствие между собой и конкретным -gpiodev.
Далее нужно зарегистрировать функции обработчики запросов, которые мы упоминали ранее:
void qemu_gpio_fe_set_handlers(GpioBackend *b,
LineInfoHandler *line_info,
LineGetValueHandler *get_value,
LineSetValueHandler *set_value,
void *opaque);

Этого вполне достаточно для наших целей. Пример использования для ASPEED рассмотрим в конце статьи, поэтому переходим к описанию нескольких вариантов внешних интерфейсов.
Компоненты проекта:
qemu v10.0.0:
модели MMIO/PCI GPIO,
машина RISC-V virt с MMIO GPIO и дополнительной генерацией dtb,
gpiodev с бэкендами CHARDEV, CUSE, GUSE,
buildroot 2025.02.2 ванильный (соответствует предыдущей итерации),
qemu-gpio-tools — необходимы для chardev,
libgpiod — модификации для CUSE и GUSE,
libfuse — модификации для поддержки GUSE,
linux v6.12:
патчи для dtb-инъекций,
драйвер для QEMU MMIO/PCIE GPIO,
модуль GUSE.
→ Скачать проект-обертку (часть компонентов нужна только для GUSE)
Минимальная сборка
В качестве примера возьмем ASPEED ast2600-evb — одноплатный компьютер (SBC), который (уверяю, временно — следите за нашим блогом) является наиболее полно эмулируемой в QEMU машиной с самым широким набором периферийных блоков. Также в ASPEED ast2600-evb реализована эмуляция режима I2C slave.
Прикладываю артефакты для ASPEED, так как для примера нужно собрать только QEMU:
$ git clone -b nshubin/qemu-gpiodev 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/f34da376-f208-4b0b-943c-0d183f038da8/68d857b3-f61b-482e-b9e4-70e7cb551ea4/download -O initramfs.cpio.xz
$ wget https://gitflic.ru/project/maquefel/qemu-gpio-playground/release/f34da376-f208-4b0b-943c-0d183f038da8/acd48054-e894-40c4-a351-bafb447353bb/download -O zImage
$ wget https://gitflic.ru/project/maquefel/qemu-gpio-playground/release/f34da376-f208-4b0b-943c-0d183f038da8/8c4d2cec-12c9-4cdb-ba41-cf564abced95/download -O aspeed-ast2600-evb.dtb
Общая строка запуска:
host $ build-qemu/qemu-system-arm -M ast2600-evb,bmc-console=uart5 \
-kernel buildroot_aspeed/build/linux-6.12.17/arch/arm/boot/zImage \
-dtb ./buildroot_aspeed/images/aspeed-ast2600-evb.dtb \
-initrd buildroot_aspeed/images/rootfs.cpio.xz \
-nographic -serial mon:stdio
Набор дополнительных команд для подключения gpiodev зависит от того, какой внешний интерфейс взаимодействия вы используете.
CHARDEV
Начну с примеров, а потом перейду к пояснениям. Запускаем эмуляцию ASPEED ast2600-evb:
# небходимы qemu-gpio-tools
host $ make .build-qemu-gpio-tools
host $ build-qemu/qemu-system-arm -M ast2600-evb,bmc-console=uart5 \
-kernel buildroot_aspeed/build/linux-6.12.17/arch/arm/boot/zImage \
-dtb ./buildroot_aspeed/images/aspeed-ast2600-evb.dtb \
-initrd buildroot_aspeed/images/rootfs.cpio.xz \
-nographic -serial mon:stdio \
-gpiodev chardev,id=aspeed-gpio0,chardev=gpio0 \
-chardev socket,path=/tmp/gpio0,id=gpio0,server=on,wait=off \
-d guest_errors
Выводим список входов/выходов для aspeed-gpio0:
host $ qemu-gpio-tools/lsgpio -n /tmp/gpio0
sending 0x8044b401
GPIO chip: aspeed-gpio0, "ASPEED GPIO", 208 GPIO lines
line 0: "gpioA0" unused [input]
[...]
line 207: "gpioZ7" unused [input]
Проверяем, что прерывания и изменение состояния входов обрабатываются при воздействии со стороны хоста:
host $ ./gpio-hammer -n /tmp/gpio0 -o 8
qemu # gpiomon -c 0 8
gpio_reg_direction: 0x0 0x0
36.791326811 rising gpiochip0 8
37.679910405 falling gpiochip0 8
38.568927503 rising gpiochip0 8
39.457922388 falling gpiochip0 8
40.346842481 rising gpiochip0 8
[...]
Теперь проверяем, что изменения состояния линии, инициированные в гостевой системе, отслеживаются на хосте:
qemu # gpioset -c 0 8=0
^C
qemu # gpioset -c 0 8=1
^C
host $ ./gpio-event-mon -n /tmp/gpio0 -o 8
No flags specified, listening on both rising and falling edges
Monitoring line 8 on /tmp/gpio0
Initial line value: 0
GPIO EVENT at 572196930648 on line 8 (0|0) falling edge
GPIO EVENT at 574302333416 on line 8 (0|0) rising edge
gpiodev-chardev представляет собой надстройку над -chardev, то есть теоретически может работать поверх любого транспорта, поддерживаемого -chardev: stdio, serial, pipe, pty, socket и так далее. Теоретически — потому что -chardev socket требует для работы qemu-gpio-tools, которые я реализовал поверх UNIX-сокета (это просто переработанные утилиты из набора tools/gpio в Linux). Это, конечно, не то ограничение, которое нельзя было бы обойти с помощью socat, но, тем не менее.

В этом одновременно и сила, и слабость подхода: с одной стороны, для работы требуются специальные утилиты, с другой — мы не ограничены только локальным использованием.
FUSE
Проблема «прозрачного» взаимодействия заключается в том, что все i2c-dev, gpiochip, spidev и так далее требуют для своей работы ioctl() и, соответственно, все библиотеки и инструменты завязаны на его использовании.
Если чтение или запись реализовать достаточно легко, то способа вызвать ioctl() я не знаю — эта возможность есть только у устройств или специальных файлов ядра. Получается, что в любом случае нам необходима помощь ядра.
И здесь нам пригодится FUSE, а точнее, его часть под названием CUSE (Userland Character Device Library).
CUSE
Традиционно начну с примеров. Запускаем эмуляцию платы ASPEED ast2600-evb в QEMU с подключением gpiodev через CUSE:
# небходим libgpiod с модификациями
host $ make .build-libgpiod
host $ sudo build-qemu/qemu-system-arm -M ast2600-evb,bmc-console=uart5 \
-kernel buildroot_aspeed/build/linux-6.12.17/arch/arm/boot/zImage \
-dtb ./buildroot_aspeed/images/aspeed-ast2600-evb.dtb \
-initrd buildroot_aspeed/images/rootfs.cpio.xz \
-nographic -serial mon:stdio \
-gpiodev cuse,id=aspeed-gpio0,devname=gpiochip10 \
-d guest_errors
Выводим список входов/выходов для aspeed-gpio0:
host $ sudo libgpiod/tools/gpioinfo -c 10
line 0: "gpioA0" input
[...]
line 207: "gpioZ7" input
Проверяем прерывания для гостя:
host $ sudo libgpiod/tools/gpioset -t 0 -c 10 8=1 9=1 10=1
host $ sudo libgpiod/tools/gpioset -t 0 -c 10 8=0 9=0 10=0
qemu # gpiomon -c 0 8 9 10
69.422108579 rising gpiochip0 9
69.422015591 rising gpiochip0 8
69.422173796 rising gpiochip0 10
124.508747142 falling gpiochip0 9
124.508841782 falling gpiochip0 10
124.508572457 falling gpiochip0 8
Проверяем прерывания для хоста:
qemu # gpioset -t 0 -c 0 8=1 9=1 10=1
qemu # gpioset -t 0 -c 0 8=0 9=0 10=0
host $ sudo libgpiod/tools/gpiomon -c 10 8 9 10
1749204303.043403870 rising aspeed-gpio0 8
1749204303.043546291 rising aspeed-gpio0 9
1749204303.043650331 rising aspeed-gpio0 10
1749204308.437501487 falling aspeed-gpio0 8
1749204308.437650077 falling aspeed-gpio0 9
1749204308.437757098 falling aspeed-gpio0 10
Обратите внимание на sudo: нам нужен доступ к /dev/cuse, а затем — к /dev/gpiochip10. Доступ можно обеспечить с помощью udev-rules или контейнеризации. Код можно посмотреть в файле qemu/gpiodev/gpio-cuse.c — на мой взгляд, он достаточно простой, если не считать работу с GPIO UAPI. Тем более, мы не используем CUSE_UNRESTRICTED_IOCTL, который сильно усложняет ioctl(), делая его двух- или даже трехстадийным. В общем виде это выглядит так:

Перейдем к итогу и начнем с правок, которые нужны для libgpiod:
libgpiod — довольно параноидальная библиотека, поэтому пришлось «уговорить» ее видеть gpiochip по нестандартным путям в /sys/class/cuse, совместно со стандартным /sys/bus/gpio,
заставить ее переиспользовать тот же самый файловый дескриптор для запроса линий, так как у CUSE нет механизмов создания новых файловых дескрипторов.
Что самое интересное, пока у нас в компании «прижился» именно этот вариант. Скорее всего, это связано с тем, что нет необходимости в поддержке нескольких клиентов для одного чипа, доступен Python, а поддержка CUSE есть уже давно и присутствует во всех дистрибутивах.
GUSE
Нам нужно поменять ветку в libgpiod, собрать libfuse и модуль для ядра хоста. Вообще, делать это не рекомендую, так как качество модуля ядра на уровне PoC — только если очень сильно хочется и только в отдельной машине QEMU.
Я собрал GUSE на отдельной ветке. libfuse и guse нужно собрать локально, так как первый идет для QEMU, а второй мы грузим в ядро хоста:
# QEMU нужно пересобрать
host $ rm -rf build-qemu && make .build-qemu
# libgpiod нужно пересобрать
host $ make -C libgpiod clean && make .build-libgpiod
# Модуль собираем под текущее ядро
host $ make -C guse
# Или под конкретный линукс, который будем запускать внутри QEMU
host $ make guse/guse.ko
# В любом случае загружаем модуль в ядро (или текущее, а еще лучше внутри QEMU)
host/guest $ sudo insmod guse.ko
host/guest $ sudo build-qemu/qemu-system-arm -M ast2600-evb,bmc-console=uart5 \
-kernel buildroot_aspeed/build/linux-6.12.17/arch/arm/boot/zImage \
-dtb ./buildroot_aspeed/images/aspeed-ast2600-evb.dtb \
-initrd buildroot_aspeed/images/rootfs.cpio.xz \
-nographic -serial mon:stdio \
-gpiodev guse,id=aspeed-gpio0,devname=gpiochip10 \
-d guest_errors
Примеры использования я приводить не буду, так как они аналогичны CUSE.

Как обычно, дьявол кроется в деталях.
Начну с хороших новостей: для libgpiod нужна всего одна маленькая правка, чтобы «уговорить» его видеть /sys/class/guse. Я уже упоминал, что libgpiod — параноидальная библиотека. Дело в том, что разницы между реальным устройством и эмулированным для libgpiod практически нет.
А плохие новости заключаются в том, что пришлось:
сделать отдельный модуль ядра guse, который умеет корректно обрабатывать GPIO_V2_GET_LINE_IOCTL и возвращать новый ассоциированный с запрошенными линиями файловый деcкриптор,
добавить поддержку guse в libfuse.
Из-за того, что часть функций FUSE модулю ядра недоступна, он получился странным — новые файлы в FUSE нам создавать нельзя, поэтому мне пришлось добавить флаг в inode, чтобы различать файловые дескрипторы для gpiochip и созданные для GPIO_V2_GET_LINE_IOCTL.
Впрочем, мне это было очевидно почти с самого начала, но все же я решил посмотреть на более или менее рабочий прототип GUSE.
Модификации для VIRTUAL MMIO/PCI GPIO
Начнем с регистрации и инициализации. Так как у нас общий компонент для MMIO- и PCI-моделей, инициализация у них тоже общая:
static void gpio_realize(DeviceState *dev, Error **errp)
{
GpioState *s = GPIO(dev);
Object *backend;
Gpiodev *gpio;
if (dev->id) {
backend = object_resolve_path_type(dev->id, TYPE_GPIODEV, NULL);
if (backend) {
gpio = GPIODEV(backend);
qemu_gpio_fe_init(&s->gpiodev, gpio, s->nr_lines, dev->id,
"VIRTUAL MMIO GPIO", NULL);
qemu_gpio_fe_set_handlers(&s->gpiodev, mmio_gpio_line_info,
mmio_gpio_get_line,
mmio_gpio_set_line, s);
}
}
}
Мы просто сообщаем количество линий, название и способ взаимодействия с моделью.
Достаточно простой запрос о статусе линий (без именования каждой), в котором, в зависимости от состояния регистра DIR_OUT, мы сообщаем, является ли линия входом или выходом:
static void mmio_gpio_line_info(void *opaque, gpio_line_info *info)
{
uint32_t offset = info->offset;
GpioState *s = GPIO(opaque);
if (test_bit32(offset, &s->regs[R_GPIO_QEMU_DIR_OUT])) {
info->flags |= GPIO_LINE_FLAG_OUTPUT;
} else {
info->flags |= GPIO_LINE_FLAG_INPUT;
}
}
Функция, которая запрашивает состояние линии:
static int mmio_gpio_get_line(void *opaque, uint32_t offset)
{
GpioState *s = GPIO(opaque);
return test_bit32(offset, &s->regs[R_GPIO_QEMU_DATA]);
}
А эта функция задает состояние — мы просто переиспользуем метод, созданный ранее для QMP:
static int mmio_gpio_set_line(void *opaque, uint32_t offset, uint8_t value)
{
GpioState *s = GPIO(opaque);
mmio_gpio_set_pin(s, offset, value);
return 0;
}
Мы не можем просто выставить бит в регистре DATA — возможно, нам также нужно будет сигнализировать об этом через прерывание.
Функции выше отвечают за изменение состояния модели, если они инициированы за пределами QEMU, но нам также нужно сигнализировать об изменении состояния со стороны гостя в QEMU. За это отвечают методы mmio_gpio_line_event() и mmio_gpio_config_event().
Если произошло изменение состояния входа low->high или high->low, то мы всегда сообщаем об этом gpiodev:
static uint64_t mmio_gpio_set(RegisterInfo *reg, uint64_t val)
{
[...]
unsigned bit = test_and_set_bit32(idx, &s->regs[R_GPIO_QEMU_DATA]);
if (!bit) {
+ mmio_gpio_line_event(s, idx, GPIO_EVENT_RISING_EDGE);
qemu_irq_raise(s->output[idx]);
}
[...]
}
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) {
+ mmio_gpio_line_event(s, idx, GPIO_EVENT_FALLING_EDGE);
qemu_irq_lower(s->output[idx]);
}
[...]
}
Так как сведения о внешних клиентах содержит gpiodev, а не сама модель, то нужно всегда сообщать об изменениях конфигурации в mmio_gpio_out():
static uint64_t mmio_gpio_out(RegisterInfo *reg, uint64_t val)
{
GpioState *s = GPIO(reg->opaque);
uint32_t val32 = val;
uint32_t changed = val ^ s->regs[R_GPIO_QEMU_DIR_OUT];
unsigned idx;
/* for each bit in val32 changed */
idx = find_first_bit((unsigned long *)&changed, s->nr_lines);
while (idx < s->nr_lines) {
mmio_gpio_config_event(s, idx);
idx = find_next_bit((unsigned long *)&val32, s->nr_lines, idx + 1);
}
/* simply apply what was set */
return val;
}
Мы добавили mmio_gpio_out() специально для gpiodev, так как теперь, в отличие от предыдущей версии, нужно различать, какая линия изменила конфигурацию.
Модификации для ASPEED GPIO
У ASPEED ast2600-evb есть особенности в плане количества входов/выходов на порт: их 208 с одним общим прерыванием. Порты видны как один gpiochip, аналогично работает и эмулятор «одноплатника» в QEMU. Поэтому модель получилась запутанная, а вместе с ней и aspeed_gpio_line_info(), который сообщает нам информацию о каждой линии:
static void aspeed_gpio_line_info(void *opaque, gpio_line_info *info)
{
AspeedGPIOState *s = ASPEED_GPIO(opaque);
AspeedGPIOClass *agc = ASPEED_GPIO_GET_CLASS(s);
uint32_t group_idx = 0, pin_idx = 0, idx = 0;
uint32_t offset = info->offset;
const GPIOSetProperties *props;
bool direction;
const char *group;
int i, set_idx, grp_idx, pin;
for (i = 0; i < ASPEED_GPIO_MAX_NR_SETS; i++) {
props = &agc->props[i];
uint32_t skip = ~(props->input | props->output);
for (int j = 0; j < ASPEED_GPIOS_PER_SET; j++) {
if (skip >> j & 1) {
continue;
}
group_idx = j / GPIOS_PER_GROUP;
pin_idx = j % GPIOS_PER_GROUP;
if (idx == offset) {
goto found;
}
idx++;
}
}
return;
found:
group = &props->group_label[group_idx][0];
set_idx = get_set_idx(s, group, &grp_idx);
snprintf(info->name, sizeof(info->name), "gpio%s%d", group, pin_idx);
pin = pin_idx + group_idx * GPIOS_PER_GROUP;
direction = !!(s->sets[set_idx].direction & BIT_ULL(pin));
if (direction) {
info->flags |= GPIO_LINE_FLAG_OUTPUT;
} else {
info->flags |= GPIO_LINE_FLAG_INPUT;
}
}
Сложность возникает из-за того, что модель проектировалась как универсальная — с учетом того, что в массиве линий могут быть пропуски. Например, в ast2500 в банке Y часть линий не может быть сконфигурирована как вход или выход. Именно этот факт и проверяется в выражении uint32_t skip = ~(props->input | props->output);.
Здесь есть явный намек для gpiodev на то, что стоит добавить отдельную функцию для запроса диапазона линий — aspeed_gpio_lines_info() — и передавать информацию обо всех линиях при инициализации, а не перебирать их при каждом запросе.
Далее, чтобы сообщать об изменении конфигурации и о событиях на линии, определяем функции aspeed_gpio_line_event() и aspeed_gpio_config_event():
static void aspeed_gpio_line_event(AspeedGPIOState *s, uint32_t set_idx, uint32_t pin_idx)
{
uint32_t offset = set_idx * ASPEED_GPIOS_PER_SET + pin_idx;
QEMUGpioLineEvent event = GPIO_EVENT_FALLING_EDGE;
if (aspeed_gpio_get_pin_level(s, set_idx, pin_idx)) {
event = GPIO_EVENT_RISING_EDGE;
}
qemu_gpio_fe_line_event(&s->gpiodev, offset, event);
}
static void aspeed_gpio_config_event(AspeedGPIOState *s, uint32_t set_idx, uint32_t pin_idx)
{
uint32_t offset = set_idx * ASPEED_GPIOS_PER_SET + pin_idx;
qemu_gpio_fe_config_event(&s->gpiodev, offset, GPIO_LINE_CHANGED_CONFIG);
}
Тут уже нет ничего сложного: aspeed_gpio_line_event() вызываем в aspeed_gpio_update() — но только в случае, если произошло изменение состояния. А aspeed_gpio_config_event() вызываем при изменении состояния вход/выход, то есть при записи в регистр *_DIRECTION.
Для aspeed_gpio_get/set_line() используются те же функции, что и для манипуляций через QMP:
static int aspeed_gpio_get_line(void *opaque, uint32_t offset)
{
AspeedGPIOState *s = ASPEED_GPIO(opaque);
int set_idx, pin_idx;
set_idx = offset / ASPEED_GPIOS_PER_SET;
pin_idx = offset % ASPEED_GPIOS_PER_SET;
return aspeed_gpio_get_pin_level(s, set_idx, pin_idx);
}
static int aspeed_gpio_set_line(void *opaque, uint32_t offset, uint8_t value)
{
AspeedGPIOState *s = ASPEED_GPIO(opaque);
int set_idx, pin_idx;
set_idx = offset / ASPEED_GPIOS_PER_SET;
pin_idx = offset % ASPEED_GPIOS_PER_SET;
aspeed_gpio_set_pin_level(s, set_idx, pin_idx, value);
return 0;
}
Наконец, регистрация и инициализация, причем последняя идентична инициализации для virtual gpio:
static void aspeed_gpio_realize(DeviceState *dev, Error **errp)
[...]
if (d->id) {
backend = object_resolve_path_type(d->id, TYPE_GPIODEV, NULL);
if (backend) {
gpio = GPIODEV(backend);
qemu_gpio_fe_init(&s->gpiodev, gpio, agc->nr_gpio_pins, d->id,
"ASPEED GPIO", NULL);
qemu_gpio_fe_set_handlers(&s->gpiodev, aspeed_gpio_line_info,
aspeed_gpio_get_line,
aspeed_gpio_set_line, s);
}
}
[...]
}
Заключение
Параллельно с CUSE/GUSE я рассматривал вариант использования gpiosim/gpiomockup, которые применяются для тестирования GPIO и libgpiod. К сожалению, они оказались весьма ограниченными:
нет обратной связи со стороны configfs при изменении состояния чипа,
нет возможности менять конфигурацию входа/выхода со стороны configfs,
множество файлов в configfs, что иронично напоминает старый GPIO sysfs, от которого GPIO UAPI как раз и призывает отказаться.
Однако, gpiosim/gpiomockup привели меня к мысли: если все равно не обойтись без модуля ядра, можно хотя бы сделать «виртуальную» пару gpiochip со связанным состоянием — один можно использовать в QEMU, а другой — на стороне хоста. Правда, и у этого решения есть очевидный минус. Одно дело, когда необходимый функционал достигается только за счет модификаций в QEMU, и совсем другое — когда приходится дополнительно что-то менять.
CUSE в итоге показал себя хорошо, но не для GPIO, а для I2C — вот там мы действительно получаем стандартное взаимодействие за сравнительно низкую цену. Основное применение — расширенное тестирования прошивок для микроконтроллеров отделом разработки встраиваемого ПО в YADRO, которое позволяет выявлять часть ошибок еще до тестов на реальном железе. О полной эмуляции микроконтроллеров и прозрачном I2C расскажем позже — следите за новыми статьями в нашем блоге.
В итоге я решил, что если уж приходится менять что-то за пределами QEMU, почему бы не сделать это кардинально и с размахом? Тогда вариант с chardev выглядит привлекательно, если:
не ограничиваться только QEMU, а создать протокол, подходящий для работы с GPIO в целом — как, например, для MOXA ioLogik,
сделать его пригодным не только для больших систем, но и для встраиваемых,
представить его в виде описания, библиотеки, а также обвязок для Python и других языков, чтобы интегрировать в большие системы.
Готового решения я не нашел, поэтому буду рад, если поделитесь своими соображениями в комментариях.
Комментарии (6)
rtd_leetch
31.07.2025 12:24Возможно вопрос не в тему но все же. Вы пробовали вместо qemu использовать renode или какой-либо другой фреймворк, если да можете поделиться опытом, лучше хуже.
maquefel Автор
31.07.2025 12:24Вообще говоря вопрос в тему, поскольку не вы один его задаёте. Я смотрел renode и он, если я не ошибаюсь, является не функциональным, а так называем точным эмулятором, как например spike. То есть сравнивать их уже не очень корректно.
А именно прямых аналогов QEMU, я назвать не могу. Разве что Bochs, VirtualBox но они ограничены x86 архитектурой, что делает их для меня неинтересными.
Помимо этого, renode:
мне показался несколько более бедным по количеству модулей (периферии);
в большой степени написан на C#, который я изучать просто не хочу;
AVKinc
31.07.2025 12:24То что GPIO это интерфейс совершенно очевидно. А если копнуть чуть глубже то это просто область памяти.
IgorKKK
У Вас на главной картинке светодиод к ноге МК без резистора приставлен. Жуть жуткая :-)
maquefel Автор
Вот да! Будем считать, что это всё совсем виртуальное ... К сожалению на КДПВ у меня не хватило ни таланта ни фантазии.
haqreu
Я ненастоящий сварщик, но светодиод-то между МК и землёй. Можно поставить ногу на вход (не на выход!) и включать-выключать внутреннюю подтяжку.