Skip to content

Instantly share code, notes, and snippets.

@sleventyeleven
Created September 25, 2026 04:06
Show Gist options
  • Select an option

  • Save sleventyeleven/76700b54194d0d90c2649442e3b423d4 to your computer and use it in GitHub Desktop.

Select an option

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
#!/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