Когда-то в отделе разработки встраиваемого ПО в 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 и других языков, чтобы интегрировать в большие системы.
Готового решения я не нашел, поэтому буду рад, если поделитесь своими соображениями в комментариях.
Комментарии (28)
rtd_leetch
31.07.2025 12:24Возможно вопрос не в тему но все же. Вы пробовали вместо qemu использовать renode или какой-либо другой фреймворк, если да можете поделиться опытом, лучше хуже.
maquefel Автор
31.07.2025 12:24Вообще говоря вопрос в тему, поскольку не вы один его задаёте. Я смотрел renode и он, если я не ошибаюсь, является не функциональным, а так называем точным эмулятором, как например spike. То есть сравнивать их уже не очень корректно.
А именно прямых аналогов QEMU, я назвать не могу. Разве что Bochs, VirtualBox но они ограничены x86 архитектурой, что делает их для меня неинтересными.
Помимо этого, renode:
мне показался несколько более бедным по количеству модулей (периферии);
в большой степени написан на C#, который я изучать просто не хочу;
engin
31.07.2025 12:24Берите LabView, на нем можно делать всё.
Boozlachu
31.07.2025 12:24Разве LabView является средой эмуляции для для запуска ОС?)) Это всё равно что сказать "берите свою любимую IDE, там можно делать всё"
engin
31.07.2025 12:24LabView это не ради "эмуляции ОС", а ради сокращения цикла "идея - исполнение" при разработке автономных логических систем.
Так что речь не про «берите любую IDE», а про осознанный выбор среды, в которой логика описывается так же наглядно, как монтируется схема — и может быть сразу проверена на железе через стандартный USB GPIO.
Впрочем при желании на LV можно и OC создать, для чего, это уже другой вопрос.Boozlachu
31.07.2025 12:24Можете поделиться таким опытом? Как про "может быть сразу проверена на железе через стандартный USB GPIO.", думаю статья вышла бы не плохая.
НУ и отдель про "LV можно и OC создать". Я бы не писал скрипты, да и остальные не изобретали бы автотесты всякие для ОС. Было бы крутоengin
31.07.2025 12:24LV позволяет построить программную и аппаратную систему, которая выполняет те же функции, что и ОС, в условиях встраиваемых решений, промышленных контроллеров или систем реального времени. Это можно рассматривать как специализированную ОС-подобную среду, полностью разработанную в визуальной среде.
Не полный перечень возможностей:
Запуск кода на RT-контроллерах с детерминированным исполнением;Управление потоками, приоритетами задач, прерываниями, синхронизацией и расписанием - то, что выполняет ядро RTOS;
Проектирование логического уровня "ядра" на уровне FPGA - реализация логики взаимодействия между устройствами, шин, интерфейсов;
Разработка приложений с параллельными вычислениями, распределенной логикой и управлением событиями - по сути, имитация ядра ОС на прикладном уровне;
Управление ресурсами, вводом/выводом, коммуникациями (Modbus, TCP/IP, CAN), памятью и задачами.
Вы можете построить систему, где LabVIEW:Управляет всеми задачами (как диспетчер процессов).
Имеет цикл реального времени с высоким приоритетом (как планировщик).
Реализует драйверы устройств (через FPGA или I/O Nodes).
Управляет прерываниями, вводом-выводом и хранением данных (как ядро).
Работает автономно на хардваре без традиционной ОС.
Важно понимать, что LabVIEW не создает ОС в классическом смысле, но позволяет построить программную и аппаратную систему, которая выполняет те же функции, что и ОС.
Как про "может быть сразу проверена на железе через стандартный USB GPIO.", думаю статья вышла бы не плохая.
здесь требуется конкретика, что проверять со стороны USB GPIO? Выше по ветке я дал описание решения которое применяется в программном контроллере, детали проекта обсуждаются здесь. Если у Вас возникнет желание выпустить авторскую публикацию на хабре с описанием платформы, окажу всяческое содействие. Я не писатель.
Boozlachu
31.07.2025 12:24ОС - подобная среда это всё же не эмуляция. Про не писателя - заметил, жаль что такой бесценный личный опыт не передается в виде некого руководства. Просто список возможностей можно и у гигачата спросить :-D
engin
31.07.2025 12:24Почему бы и нет, спросите его, если это просто любопытство, а интерес к стороннему опыту все же продуктивней иметь из первоисточников, впрочем гигачат на данный момент дает вполне адекватное описание платформы и ее применение. :-
maquefel Автор
31.07.2025 12:24Это же просто копи-паста из какой-нибудь LLM. Набор общих слабосвязанных фраз без какой-либо конкретики.
Вы осознаёте, что слепо копируя, вы похожи на двоечника, который списывает материал, который не понимает ?
AVKinc
31.07.2025 12:24То что GPIO это интерфейс совершенно очевидно. А если копнуть чуть глубже то это просто область памяти.
engin
31.07.2025 12:24Как то все сложно трактуется автором. По сути GPIO - это DAC или ADC с разведеными I/O пинами которые по дизайну могут быть как онбоард контроллере, так и виде его внешних расширений, соответственно с интерфейсом коммуникаций, к примеру в моем случае выходной USB GPIO это ADC который конвертирует частотный пакет сигналов со стороны контроллера в бинарные логические уровни в пределах 0,5 - 5V на 16 каналов с подтяжкой к земле. Что касается входного USB GPIO (у меня это целевой USB-RS232, отдельный модуль) ADC на 16 каналов который 12 bit обеспечивает счет напряжений в пределах от 0,001 - 3,300V.
Оба модуля несут на себе ARM 32 и все управление ими происходит на внешних микросервисах.
Как-то так.maquefel Автор
31.07.2025 12:24Развейте пожалуйста мысль, у меня никак не сочетается ваш комментарий с тематикой статьи.
engin
31.07.2025 12:24Это мой частный случай, в отличие от встроенных GPIO-контроллеров микроконтроллеров, в ПК-системах GPIO возможны реализации через внешние модули, подключаемые по USB. Такие модули представляют собой интеллектуальные расширения, включающие в себя встроенные DAC/ADC, ARM-процессоры и интерфейсы связи (например, USB или RS-232, прочие конвертеры популярных протоколов). Управление осуществляется из основного программного обеспечения на ПК (хост) посредством программных микросервисов на базе FSM или API, обеспечивающие коммуникацию и логическое управление с каждым модулем.
Если не понятно, конкретизируйте вопрос, как Вы указали x86, это не Ваш случай, отсюда и недопонимание.maquefel Автор
31.07.2025 12:24Недопонимание возникло из-за того, что всё что вы говорите безусловно верно, но к тематике статьи отношения не имеет. Ещё раз - мы хотим управлять тем, что есть на SoC, не подключать НОВЫЙ контроллер, не прокидывать из основной системы через USB или PCI (которых вообще говоря может и не быть). Например мы хотим управлять и считывать GD32_GPIO_A 0x40010800 в QEMU из основной системы.
Вот например в первой статье описаны два "чужеродных" GPIO контроллера https://habr.com/ru/companies/yadro/articles/925860/ и описано почему они не всегда подходят.
engin
31.07.2025 12:24Мне сразу в глаза ударило применение QEMU
некоторое время был одержим идеей «прозрачного» взаимодействия с устройствами внутри QEMU — использовать те же библиотеки
где в качестве host применили SBC + QEMU
Я в нем не эмулировал ничего подобного, т.к. моя архитектурра CPUx86 и для подобных задач там больше пространства для творчества, вплоть до построения IDE.
интеграция протокола I2C где для полноценной поддержки I²C GPIO-пины должны быть подключены к I²C-модулю внутри микроконтроллера (то есть быть назначены как SDA/SCL).maquefel Автор
31.07.2025 12:24где в качестве host применили SBC + QEMU
Нет - только QEMU, SBC внутри QEMU =), мы эмулируем ast2600 полностью, хост вообще говоря может быть любым, если на нём Linux (ну в некоторых случаях можно и Windows), конкретно я делал на amd64, но опять же годится практически всё, так как мы используем QEMU System TCG, а не KVM.
интеграция протокола I2C где для полноценной поддержки I²C GPIO-пины должны быть подключены к I²C-модулю внутри микроконтроллера (то есть быть назначены как SDA/SCL).
Ни разу не встречал I2C GPIO расширителей интегрированных в SoC. И опять же если вы эмулируете в QEMU SBC с подключенным GPIO расширителем (неважно каким, главное, что вы его тоже эмулируете), перед вами встаёт та же самая проблема, что и с GPIO которые находятся непосредственно в SoC.
engin
31.07.2025 12:24Ни разу не встречал I2C GPIO расширителей интегрированных в SoC. И опять же если вы эмулируете в QEMU SBC с подключенным GPIO расширителем (неважно каким, главное, что вы его тоже эмулируете), перед вами встаёт та же самая проблема, что и с GPIO которые находятся непосредственно в SoC.
Как и Вы, в свое время я бился над такими задачами, у меня нет прямого ответа в плане - вот Вам SoC с интегрированным I2C GPIO-экспандером на базе PCF8574, MCP23017 и т.д., вместо этого я пришел к выводу - нет смысла искать I2C GPIO внутри SoC, когда можно построить работу ее логики вне SoC, взаимодействуя с периферией как в моем случае по USB + I2C, просто поменял стратегию когда это просто другой способ масштабирования ввода-вывода. Если интересно могу дать дисклеимер, но это выходит за рамки Вашей публикации.
maquefel Автор
31.07.2025 12:24Вы путаете тёплое с мягким PCF8574, MCP23017 это и есть I2C GPIO-экспандеры они никуда не интегрированы, они сами по себе.
Никто в здравом уме не будет интегрировать SPI, I2C расширители, RTC и прочие устройства на кристалл, то есть в SoC.вместо этого я пришел к выводу - нет смысла искать I2C GPIO внутри SoC
Потому что их не существует в природе.
когда можно построить работу ее логики вне SoC, взаимодействуя с периферией как в моем случае по USB + I2C
Вы упорно не желаете замечать проблематики и мешаете всё в кучу.
Вот эти ваши
когда можно построить работу ее логики вне SoC, взаимодействуя с периферией как в моем случае по USB + I2C
Они откуда в QEMU возьмуться - от сырости заведутся ?
Постарайтесь структурированно ответь - "Вы что собственно хотите то ? Какие ваши конкретные предложения или замечания ?"
engin
31.07.2025 12:24Логично, что эмуляторы вроде QEMU не моделируют экспандеры, в контексте статьи хотел лишь указать, что мой подход не заменяет эмуляцию, но показывает, что задачами управления можно заниматься на другом уровне абстракции — в зависимости от целей проекта.
maquefel Автор
31.07.2025 12:24эмуляторы вроде QEMU не моделируют экспандеры
Моделируют.
в контексте статьи хотел лишь указать, что мой подход не заменяет эмуляцию, но показывает, что задачами управления можно заниматься на другом уровне абстракции — в зависимости от целей проекта.
А никто никого не заменяет. Ваш подход тоже прекрасно эмулируется при наличии такого желания и точно так же при эмуляции потребует либо QMP либо решения подобного выше.
engin
31.07.2025 12:24Ваш подход тоже прекрасно эмулируется при наличии такого желания и точно так же при эмуляции потребует либо QMP либо решения подобного выше.
Любопытно узнать, возможна ли в QMP реализации или моделирования реального логического контроллера, исполняющего логику в потоке данных или по триггерам/флагам, как это делается в автоматных логических контроллерах? Насколько мне известно среда не дает таких средств разработки.
maquefel Автор
31.07.2025 12:24Я вас прошу прочитайте всё таки материал, прежде чем задавать вопросы.
Любопытно узнать, возможна ли в QMP
QMP это просто протокол основанный на JSON, чтобы управлять и получать информацию о запущенном экземпляре QEMU.
реализации или моделирования реального логического контроллера
Не ясно, что значит реального и логического контроллера, но можно создать функциональную модель практически любого устройства или, в некоторых случаях, прокинуть внутри эмуляции физическое или реализованное на ПЛИСе.
Если вы имеете ввиду ПЛК, то с одной стороны мне абсолютно непонятно, как вы к этому внезапно пришли, а с другой стороны чем ПЛК принципиально отличается от микроконтроллера помимо того, что там зашит проприетарный софт ?
engin
31.07.2025 12:24Если вы имеете ввиду ПЛК, то с одной стороны мне абсолютно непонятно, как вы к этому внезапно пришли, а с другой стороны чем ПЛК принципиально отличается от микроконтроллера помимо того, что там зашит проприетарный софт ?
пришел не внезапно, а более чем 7 лет по озвученной Вами причине с уходом от проприетарности железа с его содержимым и того букета плюшек который он за собой тянет с клиента. Тема тянет не на одну статью, идея не из лучших отвлекать здесь кого либо на то, что здесь не предусмотрено к обсуждению. На данный момент это коробочное решение со всеми сопутствующими атрибутами и своим сообществом. Если интересно могу скинуть в личке.
IgorKKK
У Вас на главной картинке светодиод к ноге МК без резистора приставлен. Жуть жуткая :-)
maquefel Автор
Вот да! Будем считать, что это всё совсем виртуальное ... К сожалению на КДПВ у меня не хватило ни таланта ни фантазии.
haqreu
Я ненастоящий сварщик, но светодиод-то между МК и землёй. Можно поставить ногу на вход (не на выход!) и включать-выключать внутреннюю подтяжку.