← Reports Home

Static Report

CM4-NANO-A USB OTG Keyboard Setup

This is now a plain static guide instead of a bundled single-page app. It documents how to configure a Raspberry Pi Compute Module 4 on a Waveshare CM4-NANO-A board as a USB HID keyboard gadget on Debian Trixie, with all commands kept directly in readable HTML.

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.
Tip: after rebooting into OTG mode, find the CM4's IP address again and reconnect over SSH before continuing.

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

  1. Connect the CM4-NANO-A USB-C port to the host computer.
  2. 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=peripheral in /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/hidg0 exists.
  • Double-check the HID report descriptor in the script.
  • Make sure the gadget is bound to a valid UDC under /sys/class/udc.
Note: this guide is intentionally plain and readable. If you revise the commands, keep them in raw HTML rather than reintroducing compiled frontend output.

References