USB Gadget¶
This is the layer that makes the Pi be a keyboard. The Ansible usb_gadget
role automates everything here; this page explains what it does and how to do it
by hand.
1. Peripheral mode¶
The Pi's USB-OTG controller defaults to a host-only driver (dwc_otg). To act as
a USB device it must load the dwc2 driver in peripheral mode.
In /boot/firmware/config.txt (older OS: /boot/config.txt), under [all]:
[all]
dtoverlay=dwc2,dr_mode=peripheral
Watch the section filters
If this line lands under a section like [cm4] or [cm5], it is silently
skipped on a Pi Zero and you'll get an empty /sys/class/udc/. It must be
under [all] (or unsectioned).
In /boot/firmware/cmdline.txt (single line), add after rootwait:
modules-load=dwc2
Reboot, then confirm:
cat /proc/device-tree/soc/usb@*/dr_mode # -> peripheral
ls /sys/class/udc/ # -> 20980000.usb (non-empty!)
2. The gadget script¶
src/usb_gadget.sh
builds a composite gadget via configfs with two HID functions and binds it to the
UDC. Installed to /usr/local/sbin/pi-remote-usb-gadget and run once at boot by
usb-gadget.service.
It creates:
hid.usb0→/dev/hidg0— boot keyboard, 8-byte reports[modifier][reserved][key1..key6].hid.usb1→/dev/hidg1— consumer control, 2-byte little-endian usage codes (HID usage page0x0C).
The script begins with modprobe libcomposite (which creates
/sys/kernel/config/usb_gadget) and ends with ls /sys/class/udc > UDC to
activate the gadget.
3. Autostart¶
Debian/Raspberry Pi OS does not run /etc/rc.local by default anymore, so a
systemd unit owns startup:
[Unit]
Description=pi-remote USB HID gadget
After=sys-kernel-config.mount
Requires=sys-kernel-config.mount
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/pi-remote-usb-gadget
[Install]
WantedBy=multi-user.target
sudo systemctl enable --now usb-gadget.service
ls -l /dev/hidg0 /dev/hidg1
HID report reference¶
Keyboard (/dev/hidg0) — 8 bytes. To press a then release:
open('/dev/hidg0','rb+').write(bytes([0,0,0x04,0,0,0,0,0])) # 0x04 = 'a'
open('/dev/hidg0','rb+').write(bytes(8)) # release
Always send the release report or the host sees a stuck key.
Consumer (/dev/hidg1) — 2 bytes, little-endian usage code, then 0x0000 to
release. Example play/pause (0x00CD): bytes([0xCD,0x00]).