|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Собираем эмуляцию USB-устройства с помощью Buildroot с использованием USB Gadget для запуска в QEMU
Время создания: 10.06.2026 11:26
Автор: empyrean
Текстовые метки: linux, usb, эмуляция, программирование, устройство, qemu, buildroot, usbip
Раздел: Компьютер - Программирование - Язык C++ (Си++) - Библиотека Qt - USB
Запись: xintrea/mytetra_syncro/master/base/1781080011vsyivoc238/text.html на raw.githubusercontent.com
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Пару слов об инструментах USB Gadget - подсистема в ядре Linux, позволяет работать в режиме ведомого USB-устройства (USB Device), а не хоста. Это дает возможность превратить устройство в виртуальную клавиатуру, флешку, сетевую карту или веб-камеру. Buildroot - система сборки, используется для создания индивидуального дистрибутива Linux, который затем компилируется под нужную платформу. QEMU - эмулятор различных устройств, который позволяет запускать операционные системы, предназначенные под одну архитектуру, на другой. TAP-интерфейс в Windows — это виртуальный сетевой адаптер, предназначенный для перехвата, обработки и маршрутизации сетевого трафика. Настройка TAP-интерфейса в Windows для QEMU позволяет виртуальной машине напрямую подключиться к локальной сети, будто это отдельный компьютер. В Windows для этого обычно используется драйвер от VPN-клиентов (например, OpenVPN). USBip - это утилита и драйвер, которые позволяют использовать USB-устройства на удаленном компьютере по сети (IP-сетям, например, локальной сети или интернету). Благодаря этой технологии, физически подключенное к одному компьютеру устройство (флешка, принтер, ключ защиты) распознается другим компьютером так, будто оно вставлено прямо в него.
Собираем Linux-дистрибутив с поддержкой USB Gadget В качестве хоста для сборки используется Ubuntu. Цель - собрать Linux-дистрибутив, который будет при включении настраивать USB Gadget и запускать скрипт Modbus-slave. 1. Установка зависимостей sudo apt update sudo apt install -y \ build-essential \ git \ wget \ unzip \ rsync \ bc \ bison \ flex \ cpio \ libssl-dev \ libelf-dev \ pkg-config \ qemu-system-x86 \ qemu-utils \ libmodbus-dev 2. Скачивание Buildroot git clone https://github.com/buildroot/buildroot.git cd buildroot 3. Базовая конфигурация make qemu_x86_64_defconfig 4. Добавление modbus- скрипта Структура добавляемых папок/скриптов: buildroot/ ├── board/ │ └── modbus/ │ └── rootfs_overlay/ │ ├── etc/ │ │ └── init.d/ │ │ ├── S40network │ │ └── S99modbus │ └── usr/ │ └── bin/ │ └── usb_gadget.sh └── package/ └── modbus_slave/ ├── Config.in ├── modbus_slave.mk └── src/ └── modbus_slave.c Создание структуры проекта: mkdir -p board/modbus/rootfs_overlay/usr/bin mkdir -p board/modbus/rootfs_overlay/etc/init.d mkdir -p package/modbus_slave/src Устанавливаемые поля в usb_gadget.sh:
modbus/rootfs_overlay/usr/bin/usb_gadget.sh #!/bin/sh set -e CMDLINE=$(cat /proc/cmdline) get_arg() { echo "$CMDLINE" | sed -n "s/.*$1=\([^ ]*\).*/\1/p" } VID=$(get_arg vid) PID=$(get_arg pid) IP=$(get_arg ipaddr) SLAVE_ID=$(get_arg slaveid) SERIAL=$(get_arg serial) MANUFACTURER=$(get_arg manufacturer) PRODUCT=$(get_arg product) [ -z "$VID" ] && VID=0x1234 [ -z "$PID" ] && PID=0x5678 [ -z "$IP" ] && IP=192.168.50.10 [ -z "$SLAVE_ID" ] && SLAVE_ID=1 [ -z "$SERIAL" ] && SERIAL=ABCDEF123456 [ -z "$MANUFACTURER" ] && MANUFACTURER=Buildroot [ -z "$PRODUCT" ] && PRODUCT="CDC ACM Device" G=/sys/kernel/config/usb_gadget/g1 echo "[*] Loading modules..." modprobe libcomposite modprobe usbip_host modprobe dummy_hcd mount -t configfs none /sys/kernel/config || true usbipd -D || true echo "[*] Resetting gadget..." if [ -d "$G" ]; then echo "" > $G/UDC 2>/dev/null || true rm -rf $G fi sleep 1 mkdir -p $G cd $G echo "$VID" > idVendor echo "$PID" > idProduct echo 0x0200 > bcdUSB echo 0x0100 > bcdDevice echo 0x02 > bDeviceClass echo 0x02 > bDeviceSubClass echo 0x01 > bDeviceProtocol mkdir -p strings/0x409 echo "$SERIAL" > strings/0x409/serialnumber echo "$MANUFACTURER" > strings/0x409/manufacturer echo "$PRODUCT" > strings/0x409/product mkdir -p configs/c.1 mkdir -p configs/c.1/strings/0x409 echo 120 > configs/c.1/MaxPower echo "[*] Creating ACM function..." mkdir -p functions/acm.usb0 ln -s functions/acm.usb0 configs/c.1/ || true UDC=$(ls /sys/class/udc | head -n 1) if [ -z "$UDC" ]; then echo "[ERROR] No UDC found" exit 1 fi echo "[*] Binding UDC: $UDC" echo "$UDC" > UDC sleep 2 BUSID=1-1 echo "[*] Binding usbip..." usbip bind -b $BUSID || true echo "" echo "=================================" echo " USB GADGET READY" echo "=================================" echo "VID:PID = $VID:$PID" echo "SLAVE_ID = $SLAVE_ID" echo "IP = $IP" echo "SERIAL = $SERIAL" echo "PRODUCT = $PRODUCT" echo "=================================" echo "" echo "Attach from Windows:" echo "usbip attach -r $IP -b $BUSID" echo "" Список USB-устройств, которые можно задать через поле bDeviceClass (USB Class Codes)
buildroot/board/modbus/rootfs_overlay/etc/init.d/S40network #!/bin/sh CMDLINE=$(cat /proc/cmdline) get_arg() { echo "$CMDLINE" | sed -n "s/.*$1=\([^ ]*\).*/\1/p" } IP=$(get_arg ipaddr) NETMASK=$(get_arg netmask) GATEWAY=$(get_arg gateway) [ -z "$IP" ] && IP=192.168.50.10 [ -z "$NETMASK" ] && NETMASK=255.255.255.0 [ -z "$GATEWAY" ] && GATEWAY=192.168.50.1 echo "[NET] Bringing up eth0..." echo "[NET] IP = $IP" echo "[NET] NETMASK = $NETMASK" echo "[NET] GATEWAY = $GATEWAY" ifconfig eth0 "$IP" netmask "$NETMASK" up route add default gw "$GATEWAY" || true buildroot/board/modbus/rootfs_overlay/etc/init.d/S99modbus #!/bin/sh echo "[INIT] Starting USB gadget..." /usr/bin/usb_gadget.sh & sleep 3 echo "[INIT] Starting Modbus slave..." /usr/bin/modbus_slave & Структура пакета modbus_slave: package/modbus_slave/ ├── Config.in ├── modbus_slave.mk └── src/ └── modbus_slave.c создание modbus_slave.c: nano package/modbus_slave/src/modbus_slave.c package/modbus_slave/src/modbus_slave.c #include <stdlib.h> #include <string.h> #include <modbus/modbus.h> #include <stdio.h> #include <unistd.h> #define PORT "/dev/ttyGS0" int read_slave_id() { FILE *f = fopen("/proc/cmdline", "r"); if (!f) return 1; char cmdline[1024]; fgets(cmdline, sizeof(cmdline), f); fclose(f); char *p = strstr(cmdline, "slaveid="); if (!p) return 1; return atoi(p + 8); } int main() { modbus_t *ctx; modbus_mapping_t *mb_mapping; uint8_t query[MODBUS_RTU_MAX_ADU_LENGTH]; while (1) { printf("[*] Waiting for port...\n"); ctx = modbus_new_rtu(PORT, 9600, 'N', 8, 1); if (!ctx) { printf("[ERROR] modbus_new_rtu failed\n"); sleep(1); continue; } int slave_id = read_slave_id(); printf("[*] Slave ID = %d\n", slave_id); modbus_set_slave(ctx, slave_id); if (modbus_connect(ctx) == -1) { printf("[ERROR] connect failed\n"); modbus_free(ctx); sleep(1); continue; } printf("[*] Modbus slave started\n"); mb_mapping = modbus_mapping_new(10000, 10000, 10000, 10000); if (!mb_mapping) { printf("[ERROR] modbus_mapping_new failed\n"); modbus_close(ctx); modbus_free(ctx); sleep(1); continue; } /* REGISTERS */ mb_mapping->tab_registers[1] = 14393; mb_mapping->tab_registers[2] = 14136; mb_mapping->tab_registers[3] = 14134; mb_mapping->tab_registers[4] = 14393; mb_mapping->tab_registers[5] = 14393; mb_mapping->tab_registers[6] = 14393; mb_mapping->tab_registers[7] = 14393; mb_mapping->tab_registers[8] = 14393; mb_mapping->tab_registers[9] = 14393; while (1) { int rc = modbus_receive(ctx, query); if (rc > 0) { printf("[RX] len=%d\n", rc); int reply_rc = modbus_reply(ctx, query, rc, mb_mapping); if (reply_rc == -1) { printf("[ERROR] reply failed\n"); break; } else { printf("[TX] sent %d bytes\n", reply_rc); } } else if (rc == 0) { usleep(1000); } else { printf("[ERROR] connection lost\n"); break; } } modbus_mapping_free(mb_mapping); modbus_close(ctx); modbus_free(ctx); sleep(1); } return 0; } Config.in config BR2_PACKAGE_MODBUS_SLAVE bool "modbus_slave" modbus_slave.mk MODBUS_SLAVE_VERSION = 1.0 MODBUS_SLAVE_SITE = $(TOPDIR)/package/modbus_slave/src MODBUS_SLAVE_SITE_METHOD = local MODBUS_SLAVE_INSTALL_STAGING = NO define MODBUS_SLAVE_BUILD_CMDS $(TARGET_CC) $(@D)/modbus_slave.c \ -o $(@D)/modbus_slave \ -lmodbus endef define MODBUS_SLAVE_INSTALL_TARGET_CMDS $(INSTALL) -D -m 0755 \ $(@D)/modbus_slave \ $(TARGET_DIR)/usr/bin/modbus_slave endef $(eval $(generic-package)) Подключить package echo 'source "package/modbus_slave/Config.in"' >> package/Config.in 5. Настройка menuconfig Выполнить: make menuconfig Здесь нужно включить:
6. Настройка Linux kernel config Выполнить: make linux-menuconfig Здесь нужно включить:
7. Сборка Выполнить: make -j$(nproc) После сборки в папке output/images/ будут следующие файлы:
Настройка TAP-интерфейсов в WindowsДля каждой виртуальной машины потребуется свой TAP-интерфейс. Настроить их можно выполнив несколько действий: 1. Установка OpenVpn (https://openvpn.net/community/) 2. Создание TAP-интерфейсов cd "C:\Program Files\OpenVPN\bin\" .\tapctl.exe create --hwid root\tap0901 --name "tap10" .\tapctl.exe create --hwid root\tap0901 --name "tap11" Для просмотра списка интерфейсов: .\tapctl list Для удаления интерфейса: .\tapctl delete "tap10" 3. Объединить все TAP-интерфейсы с Ethernet через мост “Панель управления\Сеть и Интернет\Центр управления сетями и общим доступом (Изменение параметров адаптера)”. Здесь выделяем tap-адаптеры и Ethernet и создаем сетевой мост.
Запуск через QEMUСтруктура файлов: qemu/ ├── start_vm.ps1 ├── configs/ │ ├── vm1.conf │ └── vm2.conf └── images/ ├── bzImage ├── rootfs.ext2 └── start-qemu.sh 1. Создадим папку qemu/images Скопируем туда файлы bzImage, rootfs.ext2 и start-qemu.sh, созданные с помощью Buildroot. 2. Создадим файлы конфигурации VM (qemu/configs/vm1.conf): VID=0x1234 PID=0x5678 SERIAL=VM0001 MANUFACTURER=TestVendor PRODUCT=ACM_Device SLAVE_ID=1 IP=192.168.1.151 MAC=52:54:00:10:00:01 TAP0=tap10 И второй VM (qemu/configs/vm2.conf): VID=0x1112 PID=0x0002 SERIAL=VM0002 MANUFACTURER=TestVendor2 PRODUCT=ACM_Device2 SLAVE_ID=1 IP=192.168.1.152 MAC=52:54:00:10:00:02 TAP0=tap11 3. Создадим файл qemu/start_vm.ps1 qemu/start_vm.ps1 param( [string]$Name, [string]$Config ) # ========================================================= # PATHS # ========================================================= $qemuExe = "C:\Program Files\qemu\qemu-system-x86_64.exe" $imagesDir = "images" $kernel = "$imagesDir\bzImage" $baseRootfs = "$imagesDir\rootfs.ext2" $runtimeDir = "runtime\$Name" # ========================================================= # LOAD CONFIG # ========================================================= $cfg = @{} Get-Content $Config | ForEach-Object { if ($_ -match "^(.*?)=(.*)$") { $cfg[$matches[1].Trim()] = $matches[2].Trim() } } # ========================================================= # CREATE RUNTIME # ========================================================= New-Item ` -ItemType Directory ` -Force ` -Path $runtimeDir | Out-Null $runtimeRootfs = "$runtimeDir\rootfs.ext2" Copy-Item ` $baseRootfs ` $runtimeRootfs ` -Force # ========================================================= # INFO # ========================================================= Write-Host "" Write-Host "=======================================" Write-Host " STARTING VM" Write-Host "=======================================" Write-Host "NAME = $Name" Write-Host "TAP = $($cfg['TAP0'])" Write-Host "IP = $($cfg['IP'])" Write-Host "MAC = $($cfg['MAC'])" Write-Host "VID:PID = $($cfg['VID']):$($cfg['PID'])" Write-Host "SLAVE_ID = $($cfg['SLAVE_ID'])" Write-Host "=======================================" Write-Host "" # ========================================================= # QEMU ARGS # ========================================================= $qemuArgs = @( "-m", "256", "-nographic", "-kernel", $kernel, "-drive", "file=$runtimeRootfs,format=raw", "-append", "root=/dev/sda console=ttyS0 ipaddr=$($cfg['IP']) vid=$($cfg['VID']) pid=$($cfg['PID']) slaveid=$($cfg['SLAVE_ID']) serial=$($cfg['SERIAL']) manufacturer=$($cfg['MANUFACTURER']) product=$($cfg['PRODUCT'])", "-serial", "mon:stdio",
"-device", "virtio-net,netdev=net0,mac=$($cfg['MAC'])", "-netdev", "tap,ifname=$($cfg['TAP0']),id=net0,script=no,downscript=no" ) # ========================================================= # START VM # ========================================================= & $qemuExe @qemuArgs 4. Запускаем две виртуальный машины Выполнить: .\start_vm.ps1 -Name vm1 -Config configs\vm1.conf .\start_vm.ps1 -Name vm2 -Config configs\vm2.conf Подключение через USBip Для подключения можно использовать старую консольную portable версию USBip или свежую с GUI. Список консольных команд Список USBIP по ip: ./usbip list -r 192.168.1.151 ./usbip list -r 192.168.1.152 Запуск USBIP: ./usbip attach -r 192.168.1.151 -b 1-1 ./usbip attach -r 192.168.1.152 -b 1-1 Отключение USBIP: ./usbip detach -p 0 ./usbip detach -p 1 После открытия USBip виртуальные машины отобразятся в интерфейсе
После подключения (attach) виртуальных машин, они отобразятся в диспетчере задач как COM-порты.
У виртуальных машин будут настройки, заданные в конфигурационных файлах (VID/PID/Serial и т.д.)
Теперь с устройствами можно взаимодействовать по Modbus.
Полезные ссылки
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Так же в этом разделе:
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|