Created
September 25, 2026 04:06
-
-
Save sleventyeleven/76700b54194d0d90c2649442e3b423d4 to your computer and use it in GitHub Desktop.
Build a Milk-V Duo (CV1800B) SD-card image with USB HID gadget support enabled, using Milk-V's official Docker-based buildroot SDK
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| ############################################################################### | |
| # build-hid-image.sh | |
| # | |
| # Build a Milk-V Duo (CV1800B) SD-card image with USB HID gadget support | |
| # enabled, using Milk-V's official Docker-based buildroot SDK. | |
| # | |
| # The stock Milk-V Duo image (kernel 5.10.4) ships WITHOUT the USB HID gadget | |
| # function driver. Its kernel defconfig enables CONFIG_USB_CONFIGFS and the | |
| # ACM/NCM/ECM/RNDIS/EEM/MASS_STORAGE/F_FS/F_UAC1 functions, but NOT | |
| # CONFIG_USB_CONFIGFS_F_HID. As a result: | |
| # | |
| # mkdir /sys/kernel/config/usb_gadget/g1/functions/hid.usb0 | |
| # -> mkdir: ... No such file or directory | |
| # | |
| # and /dev/hidg0 can never be created. There is no /lib/modules on the image | |
| # (monolithic kernel), so `modprobe usb_f_hid` cannot help either -- the driver | |
| # has to be compiled into the kernel and the board re-flashed. | |
| # | |
| # This script adds exactly ONE kernel symbol and rebuilds: | |
| # | |
| # CONFIG_USB_CONFIGFS_F_HID=y (this in turn selects CONFIG_USB_F_HID) | |
| # | |
| # Verified on: original Milk-V Duo (CV1800B, 64MB), duo-buildroot-sdk-v2, | |
| # milkvtech/milkv-duo:latest, Ubuntu 22.04 build container. | |
| # | |
| # Reference: https://milkv.io/docs/duo/getting-started/buildroot-sdk | |
| # | |
| # Run on a Linux host with Docker installed. Docker must be able to run | |
| # privileged containers. | |
| ############################################################################### | |
| set -euo pipefail | |
| # --------------------------------------------------------------------------- | |
| # Board / target configuration (original CV1800B Milk-V Duo, SD boot, musl) | |
| # --------------------------------------------------------------------------- | |
| SDK_REPO="https://github.com/milkv-duo/duo-buildroot-sdk-v2.git" | |
| SDK_DIR="duo-buildroot-sdk-v2" | |
| DL_TAR_URL="https://github.com/milkv-duo/duo-buildroot-sdk-v2/releases/download/dl/dl.tar" | |
| DOCKER_IMAGE="milkvtech/milkv-duo:latest" | |
| CONTAINER_NAME="duodocker" | |
| # Build target passed to build.sh (see: ls device/ in the SDK for other boards). | |
| BUILD_TARGET="milkv-duo-musl-riscv64-sd" | |
| # Kernel defconfig for this board, relative to the SDK root. | |
| DEFCONFIG="build/boards/cv180x/cv1800b_milkv_duo_musl_riscv64_sd/linux/cvitek_cv1800b_milkv_duo_musl_riscv64_sd_defconfig" | |
| # The kernel .config produced by the build, used to VERIFY the option stuck. | |
| BUILT_KCONFIG="linux_5.10/build/cv1800b_milkv_duo_musl_riscv64_sd/.config" | |
| # The single kernel option we need for a USB HID keyboard gadget. | |
| HID_SYMBOL="CONFIG_USB_CONFIGFS_F_HID=y" | |
| # --------------------------------------------------------------------------- | |
| # 1. Clone the SDK (shallow) and pre-seed the buildroot download cache. | |
| # dl.tar is optional but avoids re-downloading every buildroot package. | |
| # --------------------------------------------------------------------------- | |
| if [ ! -d "$SDK_DIR" ]; then | |
| echo "[*] Cloning $SDK_REPO ..." | |
| git clone "$SDK_REPO" --depth=1 | |
| else | |
| echo "[*] $SDK_DIR already present, skipping clone." | |
| fi | |
| if [ ! -f dl.tar ]; then | |
| echo "[*] Downloading buildroot package cache (dl.tar, ~350MB) ..." | |
| wget -q "$DL_TAR_URL" | |
| fi | |
| echo "[*] Seeding $SDK_DIR/buildroot/ with dl.tar ..." | |
| mkdir -p "$SDK_DIR/buildroot/" | |
| tar xf ./dl.tar -C "$SDK_DIR/buildroot/" | |
| # --------------------------------------------------------------------------- | |
| # 2. Enable the HID gadget function in the kernel defconfig (idempotent). | |
| # --------------------------------------------------------------------------- | |
| CFG="$SDK_DIR/$DEFCONFIG" | |
| if [ ! -f "$CFG" ]; then | |
| echo "!! defconfig not found: $CFG" >&2 | |
| echo "!! Board layout may have changed; run 'ls $SDK_DIR/build/boards/cv180x'" >&2 | |
| exit 1 | |
| fi | |
| if grep -q "^${HID_SYMBOL}$" "$CFG"; then | |
| echo "[*] $HID_SYMBOL already present in defconfig." | |
| else | |
| echo "[*] Adding $HID_SYMBOL to $DEFCONFIG" | |
| cp "$CFG" "$CFG.orig" # keep a backup for reference/diff | |
| # Place it next to the other USB_CONFIGFS_* options to keep things tidy. | |
| if grep -q "^CONFIG_USB_CONFIGFS_F_UAC1=y$" "$CFG"; then | |
| sed -i "/^CONFIG_USB_CONFIGFS_F_UAC1=y$/a ${HID_SYMBOL}" "$CFG" | |
| else | |
| printf '%s\n' "$HID_SYMBOL" >> "$CFG" | |
| fi | |
| echo "[*] defconfig diff:" | |
| diff "$CFG.orig" "$CFG" || true | |
| fi | |
| # --------------------------------------------------------------------------- | |
| # 3. Launch the official build container with the SDK bind-mounted. | |
| # NOTE (Windows/macOS Docker Desktop): bind-mounting an NTFS/exFAT path into | |
| # the Linux container can break buildroot (symlinks/permissions). On those | |
| # hosts, use a named volume instead and clone/build INSIDE it, e.g.: | |
| # docker volume create duosdk | |
| # docker run --privileged -itd --name duodocker -v duosdk:/home/work ... | |
| # docker exec duodocker git clone ... /home/work/duo-buildroot-sdk-v2 | |
| # then docker cp the finished image out. On a native Linux host the bind | |
| # mount below is fine. | |
| # --------------------------------------------------------------------------- | |
| echo "[*] Pulling $DOCKER_IMAGE ..." | |
| docker pull "$DOCKER_IMAGE" | |
| echo "[*] (Re)starting build container '$CONTAINER_NAME' ..." | |
| docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true | |
| docker run --privileged -itd --name "$CONTAINER_NAME" \ | |
| -v "$(pwd)/$SDK_DIR":/home/work \ | |
| "$DOCKER_IMAGE" /bin/bash | |
| # --------------------------------------------------------------------------- | |
| # 4. One-click build. FORCE_UNSAFE_CONFIGURE lets buildroot configure as root. | |
| # On a 32-core host this took ~17 min including toolchain download. | |
| # Two "Error 1 (ignored)" lines are EXPECTED and harmless: | |
| # - cp: cannot stat 'install/pqtool_definition.json' (camera ISP tool) | |
| # - rm: cannot remove '.../fs' (first-build cleanup) | |
| # Success is confirmed by "Build board ... success!" and an image in out/. | |
| # --------------------------------------------------------------------------- | |
| echo "[*] Building $BUILD_TARGET (this takes a while) ..." | |
| docker exec -i "$CONTAINER_NAME" /bin/bash -c \ | |
| "cd /home/work && export FORCE_UNSAFE_CONFIGURE=1 && ./build.sh $BUILD_TARGET" | |
| # --------------------------------------------------------------------------- | |
| # 5. Verify the HID options actually made it into the built kernel .config. | |
| # --------------------------------------------------------------------------- | |
| echo "[*] Verifying HID gadget support in the built kernel config ..." | |
| docker exec -i "$CONTAINER_NAME" /bin/bash -c \ | |
| "grep -E 'CONFIG_USB_F_HID|CONFIG_USB_CONFIGFS_F_HID' /home/work/$BUILT_KCONFIG" \ | |
| || { echo '!! HID options NOT found in built kernel config' >&2; exit 1; } | |
| # --------------------------------------------------------------------------- | |
| # 6. Locate the output image. | |
| # --------------------------------------------------------------------------- | |
| echo "[*] Build output:" | |
| docker exec -i "$CONTAINER_NAME" /bin/bash -c "ls -lh /home/work/out/*.img" | |
| IMG_HOST_PATH="$(pwd)/$SDK_DIR/out" | |
| echo | |
| echo "[+] Done. SD image(s) are in: $IMG_HOST_PATH/" | |
| echo " (On a bind-mounted Linux host they are already on your disk.)" | |
| echo | |
| cat <<'FLASH' | |
| ############################################################################### | |
| # 7. FLASH THE IMAGE (DESTRUCTIVE -- erases the whole microSD) | |
| # | |
| # Remove the microSD from the Duo, insert it into your computer, then use any | |
| # of the following. Double-check the target device first -- writing to the | |
| # wrong disk destroys data. | |
| # | |
| # balenaEtcher (GUI, recommended): select the .img, select the card, Flash. | |
| # Raspberry Pi Imager (GUI): "Use custom image" -> pick the .img. | |
| # | |
| # Linux CLI (replace sdX with your card, e.g. sdb -- NOT a partition sdb1): | |
| # lsblk # identify the card | |
| # sudo dd if=out/milkv-duo-*-sd.img of=/dev/sdX bs=4M conv=fsync status=progress | |
| # sync | |
| # | |
| # macOS CLI (replace diskN with your card): | |
| # diskutil list | |
| # diskutil unmountDisk /dev/diskN | |
| # sudo dd if=out/milkv-duo-*-sd.img of=/dev/rdiskN bs=4m | |
| # sync | |
| # | |
| # Boot the Duo from the new card. HID support is present when this succeeds: | |
| # mkdir /sys/kernel/config/usb_gadget/g1/functions/hid.usb0 | |
| ############################################################################### | |
| FLASH |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment