Introduction
The goal is to make the CM4 present itself to a host machine as a
standard USB keyboard over the USB-C OTG port. The overall flow is:
enable the DWC2 controller in peripheral mode, load the required
kernel modules, create the USB gadget with libcomposite,
and bind it through a small systemd service.
What You'll Learn
- How to enable USB OTG mode on the CM4-NANO-A.
- How to expose a HID keyboard gadget with
configfs. - How to make the gadget come up automatically at boot.
- How to test keystroke output via
/dev/hidg0.
Prerequisites
Hardware
- Raspberry Pi Compute Module 4 mounted on a Waveshare CM4-NANO-A.
- USB-C cable connected from the OTG port to the host computer.
- Power for the CM4 board.
Software
- Debian Trixie installed on the CM4.
- Shell access to the device, ideally over SSH after boot.
- Basic familiarity with editing files using
nano.
USB OTG Configuration
On Debian Trixie, the relevant firmware configuration file is
usually /boot/firmware/config.txt.
sudo nano /boot/firmware/config.txt
Add or confirm this line:
# Enable the DWC2 USB controller in peripheral mode for USB gadget functionality
dtoverlay=dwc2,dr_mode=peripheral
dwc2 enables the DWC2 USB controller.
dr_mode=peripheral forces device mode so the
board behaves as a gadget rather than a USB host.
Then reboot the board:
sudo reboot
Gadget Setup
Step 1: Load Kernel Modules
Edit /etc/modules so the required modules load at boot:
sudo nano /etc/modules
Add these lines:
dwc2
libcomposite
Step 2: Create the USB Gadget Script
Create the script file:
sudo nano /usr/local/bin/usb-hid-keyboard.sh
Paste the following:
Show start script
#!/bin/bash
set -u
GADGET_DIR="/sys/kernel/config/usb_gadget/rpi_hid"
FUNCTION_NAME="hid.usb0"
FUNCTIONS_DIR="${GADGET_DIR}/functions/${FUNCTION_NAME}"
CONFIG_NAME="c.1"
CONFIGS_DIR="${GADGET_DIR}/configs/${CONFIG_NAME}"
if [[ $EUID -ne 0 ]]; then
echo "Run as root"
exit 1
fi
modprobe libcomposite 2>/dev/null || true
cleanup() {
if [ -d "${GADGET_DIR}" ]; then
echo "" > "${GADGET_DIR}/UDC" 2>/dev/null || true
unlink "${CONFIGS_DIR}/${FUNCTION_NAME}" 2>/dev/null || true
rmdir "${FUNCTIONS_DIR}" 2>/dev/null || true
rmdir "${CONFIGS_DIR}/strings/0x409" 2>/dev/null || true
rmdir "${CONFIGS_DIR}" 2>/dev/null || true
rmdir "${GADGET_DIR}/strings/0x409" 2>/dev/null || true
rmdir "${GADGET_DIR}" 2>/dev/null || true
fi
}
cleanup
mkdir -p "${GADGET_DIR}"
cd "${GADGET_DIR}" || exit 1
echo 0x1d6b > idVendor
echo 0x0104 > idProduct
echo 0x0100 > bcdDevice
echo 0x0200 > bcdUSB
mkdir -p strings/0x409
echo "$(hostname)" > strings/0x409/manufacturer
echo "CM4 HID Keyboard" > strings/0x409/product
echo "CM4HID001" > strings/0x409/serialnumber
mkdir -p "${FUNCTIONS_DIR}"
echo 1 > "${FUNCTIONS_DIR}/protocol"
echo 1 > "${FUNCTIONS_DIR}/subclass"
echo 8 > "${FUNCTIONS_DIR}/report_length"
printf '\x05\x01\x09\x06\xa1\x01\x05\x07\x19\xe0\x29\xe7\x15\x00\x25\x01\x75\x01\x95\x08\x81\x02\x95\x01\x75\x08\x81\x01\x95\x05\x75\x01\x05\x08\x19\x01\x29\x05\x91\x02\x95\x01\x75\x03\x91\x01\x95\x06\x75\x08\x15\x00\x25\x65\x05\x07\x19\x00\x29\x65\x81\x00\xc0' > "${FUNCTIONS_DIR}/report_desc"
mkdir -p "${CONFIGS_DIR}/strings/0x409"
echo "Config 1: HID Keyboard" > "${CONFIGS_DIR}/strings/0x409/configuration"
echo 250 > "${CONFIGS_DIR}/MaxPower"
ln -s "${FUNCTIONS_DIR}" "${CONFIGS_DIR}/${FUNCTION_NAME}"
UDC_NAME="$(find /sys/class/udc -mindepth 1 -maxdepth 1 -printf '%f\n' | head -n 1)"
[ -n "${UDC_NAME}" ] || { echo "Error: No UDC found"; exit 1; }
echo "${UDC_NAME}" > "${GADGET_DIR}/UDC"
echo "Binding gadget to UDC: ${UDC_NAME}"
ls -l /dev/hidg*
Step 3: Create the Stop Script
Create the stop script file:
sudo nano /usr/local/bin/usb-hid-keyboard-stop.sh
Use this content:
Show stop script
#!/bin/bash
set -u
GADGET_DIR="/sys/kernel/config/usb_gadget/rpi_hid"
FUNCTION_NAME="hid.usb0"
FUNCTIONS_DIR="${GADGET_DIR}/functions/${FUNCTION_NAME}"
CONFIGS_DIR="${GADGET_DIR}/configs/c.1"
[ -e "${GADGET_DIR}/UDC" ] && echo "" > "${GADGET_DIR}/UDC" 2>/dev/null || true
rm -f "${CONFIGS_DIR}/${FUNCTION_NAME}" 2>/dev/null || true
rmdir "${CONFIGS_DIR}/strings/0x409" 2>/dev/null || true
rmdir "${CONFIGS_DIR}" 2>/dev/null || true
rmdir "${FUNCTIONS_DIR}" 2>/dev/null || true
rmdir "${GADGET_DIR}/strings/0x409" 2>/dev/null || true
rmdir "${GADGET_DIR}" 2>/dev/null || true
Make both scripts executable:
sudo chmod +x /usr/local/bin/usb-hid-keyboard.sh
sudo chmod +x /usr/local/bin/usb-hid-keyboard-stop.sh
Step 4: Create the Systemd Service
Create the service file:
sudo nano /etc/systemd/system/usb-hid-keyboard.service
Use this content:
Show systemd unit
[Unit]
Description=USB HID Keyboard Gadget
After=sys-kernel-config.mount
Requires=sys-kernel-config.mount
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/usb-hid-keyboard.sh
ExecStop=/usr/local/bin/usb-hid-keyboard-stop.sh
[Install]
WantedBy=multi-user.target
Reload systemd to pick up the new unit file:
sudo systemctl daemon-reload
Enable the service and start it:
sudo systemctl enable usb-hid-keyboard.service
sudo systemctl start usb-hid-keyboard.service
Verify the status:
systemctl status usb-hid-keyboard.service
Testing
Connection and Recognition
- Connect the CM4-NANO-A USB-C port to the host computer.
- The host should detect a new USB HID keyboard device.
Sending Keystrokes
Write 8-byte HID reports to /dev/hidg0. Byte 0 carries
modifier keys, byte 1 is reserved, and bytes 2-7 hold up to six
concurrent keycodes.
If plain shell redirection runs into permissions issues, use
sudo tee instead:
echo -ne "\x00\x00\x00\x00\x00\x00\x00\x00" | sudo tee /dev/hidg0 > /dev/null
# Press Shift + a
echo -ne "\x02\x00\x04\x00\x00\x00\x00\x00" > /dev/hidg0
# Release all keys
echo -ne "\x00\x00\x00\x00\x00\x00\x00\x00" > /dev/hidg0
# H (Shift + h)
echo -ne "\x02\x00\x0b\x00\x00\x00\x00\x00" > /dev/hidg0
echo -ne "\x00\x00\x00\x00\x00\x00\x00\x00" > /dev/hidg0
echo -ne "\x00\x00\x08\x00\x00\x00\x00\x00" > /dev/hidg0
echo -ne "\x00\x00\x00\x00\x00\x00\x00\x00" > /dev/hidg0
echo -ne "\x00\x00\x0f\x00\x00\x00\x00\x00" > /dev/hidg0
echo -ne "\x00\x00\x00\x00\x00\x00\x00\x00" > /dev/hidg0
echo -ne "\x00\x00\x0f\x00\x00\x00\x00\x00" > /dev/hidg0
echo -ne "\x00\x00\x00\x00\x00\x00\x00\x00" > /dev/hidg0
echo -ne "\x00\x00\x12\x00\x00\x00\x00\x00" > /dev/hidg0
echo -ne "\x00\x00\x00\x00\x00\x00\x00\x00" > /dev/hidg0
Troubleshooting
CM4 Not Recognized by the Host
- Verify
dtoverlay=dwc2,dr_mode=peripheralin/boot/firmware/config.txt. - Confirm the modules are loaded:
lsmod | grep dwc2
lsmod | grep libcomposite
- Check whether the service is active:
systemctl status usb-hid-keyboard.service
Device Exists but Input Does Not Work
- Verify that
/dev/hidg0exists. - Double-check the HID report descriptor in the script.
- Make sure the gadget is bound to a valid UDC under
/sys/class/udc.