The following guide describes how to setup the ODROID-C2 as a HID gadget device, in this case it will be used as either a keyboard or a basic gamepad.
The following steps could be adapted for any another device that
- Support for USB OTG device mode
- Has a Linux kernel at above version 3.19 with FunctionFS HID module built, we will use Linux version 4.15.
Before we begin, it should be noted that every command has to be run as root.
Preparation
The easiest method for running a recent kernel, for now, is to use the ArchLinuxARM image at https://archlinuxarm.org/platforms/armv8/amlogic/odroid-c2. After following the instructions and having botted into the default installation, update the system to use the mainline kernel (4.15 at the time of writing):
$ sudo pacman -Syu $ sudo pacman -R uboot-odroid-c2 $ sudo pacman -S uboot-odroid-c2-mainline linux-aarch64 dtcMake sure NOT to reboot the device yet if you are booting from eMMC. The default mainline installation above has some quirky defaults that render the system read-only and disable the OTG module. We must first disassemble the Device Tree Blob, or DTB, file used by the C2 for initializing onboard peripherals into its source form the DTS
$ cd /boot/dtbs/amlogic/ $ sudo cp -p meson-gxbb-odroidc2{,_backup}.dtb $ sudo dtc -I dtb -O dts meson-gxbb-odroidc2.dtb > meson-gxbb-odroidc2.dtsOnce done, edit the source:
$ sudo nano meson-gxbb-odroidc2.dtsIn the section [mmc@74000], change the following line to re-enable write access to the eMMC (even if not using eMMC now, it’s always good to change it):
max-frequency = <0xbebc200>;to:
max-frequency = <0x8f0d180>;And in the section [usb@c9000000], change:
dr_mode = "host";to:
dr_mode = "peripheral";usb@c9000000 is the OTG peripheral which is in “host” mode by default here. Be careful not to touch anything regarding usb@c910000 which is the 4-ports USB hub. Once this is done, we can rebuild the DTB file and reboot:
$ sudo dtc -I dts -O dtb meson-gxbb-odroidc2.dts > meson-gxbb-odroidc2.dtb $ sudo rebootWe can check the running kernel with the following command:
$ uname -rIt should be 4.15+ and if everything worked fine, the following command should show a symbolic link [c9000000.usb]:
$ ls /sys/class/udc/
Configuration
Consider the following python3 script that performs setup and teardown of the device automatically:
import sys import os import shutil import pwd import asyncio import subprocess import argparse import atexit class HIDReportDescriptorKeyboard(object): def __len__(self): return 8 def __bytes__(self): return bytes([ 0x05, 0x01, # Usage Page (Generic Desktop Ctrls) 0x09, 0x06, # Usage (Keyboard) 0xA1, 0x01, # Collection (Application) 0x05, 0x07, # Usage Page (Kbrd/Keypad) 0x19, 0xE0, # Usage Minimum (0xE0) 0x29, 0xE7, # Usage Maximum (0xE7) 0x15, 0x00, # Logical Minimum (0) 0x25, 0x01, # Logical Maximum (1) 0x75, 0x01, # Report Size (1) 0x95, 0x08, # Report Count (8) 0x81, 0x02, # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) 0x95, 0x01, # Report Count (1) 0x75, 0x08, # Report Size (8) 0x81, 0x03, # Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) 0x95, 0x05, # Report Count (5) 0x75, 0x01, # Report Size (1) 0x05, 0x08, # Usage Page (LEDs) 0x19, 0x01, # Usage Minimum (Num Lock) 0x29, 0x05, # Usage Maximum (Kana) 0x91, 0x02, # Output (Data,Var,Abs) 0x95, 0x01, # Report Count (1) 0x75, 0x03, # Report Size (3) 0x91, 0x03, # Output (Const,Var,Abs) 0x95, 0x06, # Report Count (6) 0x75, 0x08, # Report Size (8) 0x15, 0x00, # Logical Minimum (0) 0x25, 0x65, # Logical Maximum (101) 0x05, 0x07, # Usage Page (Kbrd/Keypad) 0x19, 0x00, # Usage Minimum (0x00) 0x29, 0x65, # Usage Maximum (0x65) 0x81, 0x00, # Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) 0xC0, # End Collection ]) class HIDReportDescriptorGamepad(object): def __len__(self): return 4 def __bytes__(self): return bytes([ 0x05, 0x01, # USAGE_PAGE (Generic Desktop) 0x15, 0x00, # LOGICAL_MINIMUM (0) 0x09, 0x04, # USAGE (Joystick) 0xa1, 0x01, # COLLECTION (Application) 0x05, 0x02, # USAGE_PAGE (Simulation Controls) 0x09, 0xbb, # USAGE (Throttle) 0x15, 0x81, # LOGICAL_MINIMUM (-127) 0x25, 0x7f, # LOGICAL_MAXIMUM (127) 0x75, 0x08, # REPORT_SIZE (8) 0x95, 0x01, # REPORT_COUNT (1) 0x81, 0x02, # INPUT (Data,Var,Abs) 0x05, 0x01, # USAGE_PAGE (Generic Desktop) 0x09, 0x01, # USAGE (Pointer) 0xa1, 0x00, # COLLECTION (Physical) 0x09, 0x30, # USAGE (X) 0x09, 0x31, # USAGE (Y) 0x95, 0x02, # REPORT_COUNT (2) 0x81, 0x02, # INPUT (Data,Var,Abs) 0xc0, # END_COLLECTION 0x09, 0x39, # USAGE (Hat switch) 0x15, 0x00, # LOGICAL_MINIMUM (0) 0x25, 0x03, # LOGICAL_MAXIMUM (3) 0x35, 0x00, # PHYSICAL_MINIMUM (0) 0x46, 0x0e, 0x01, # PHYSICAL_MAXIMUM (270) 0x65, 0x14, # UNIT (Eng Rot:Angular Pos) 0x75, 0x04, # REPORT_SIZE (4) 0x95, 0x01, # REPORT_COUNT (1) 0x81, 0x02, # INPUT (Data,Var,Abs) 0x05, 0x09, # USAGE_PAGE (Button) 0x19, 0x01, # USAGE_MINIMUM (Button 1) 0x29, 0x04, # USAGE_MAXIMUM (Button 4) 0x15, 0x00, # LOGICAL_MINIMUM (0) 0x25, 0x01, # LOGICAL_MAXIMUM (1) 0x75, 0x01, # REPORT_SIZE (1) 0x95, 0x04, # REPORT_COUNT (4) 0x55, 0x00, # UNIT_EXPONENT (0) 0x65, 0x00, # UNIT (None) 0x81, 0x02, # INPUT (Data,Var,Abs) 0xc0 # END_COLLECTION ]) class HidDaemon(object): def __init__(self, vendor_id, product_id, manufacturer, description, serial_number, hid_report_class): self._descriptor = hid_report_class() self._hid_devname = 'odroidc2_hid' self._vendor = vendor_id self._product = product_id self._manufacturer = manufacturer self._desc = description self._serial = serial_number self._libcomposite_already_running = self.check_libcomposite() self._usb_f_hid_already_running = self.check_usb_f_hid() self._loop = asyncio.get_event_loop() self._devname = 'hidg0' self._devpath = '/dev/%s' % self._devname def _cleanup(self): udc_path = '/sys/kernel/config/usb_gadget/%s/UDC' % self._hid_devname if os.path.exists(udc_path): with open(udc_path, 'w') as fd: fd.truncate() try: shutil.rmtree('/sys/kernel/config/usb_gadget/%s' % self._hid_devname, ignore_errors=True) except: pass if not self._usb_f_hid_already_running and self.check_usb_f_hid(): self.unload_usb_f_hid() if not self._libcomposite_already_running and self.check_libcomposite(): self.unload_libcomposite() @staticmethod def check_libcomposite(): r = int(subprocess.check_output("lsmod | grep 'libcomposite' | wc -l", shell=True, close_fds=True).decode().strip()) return r != 0 @staticmethod def load_libcomposite(): if not HidDaemon.check_libcomposite(): subprocess.check_call("modprobe libcomposite", shell=True, close_fds=True) @staticmethod def unload_libcomposite(): if HidDaemon.check_libcomposite(): subprocess.check_call("rmmod libcomposite", shell=True, close_fds=True) @staticmethod def check_usb_f_hid(): r = int( subprocess.check_output("lsmod | grep 'usb_f_hid' | wc -l", shell=True, close_fds=True).decode().strip()) return r != 0 @staticmethod def load_usb_f_hid(): if not HidDaemon.check_libcomposite(): subprocess.check_call("modprobe usb_f_hid", shell=True, close_fds=True) @staticmethod def unload_usb_f_hid(): if HidDaemon.check_libcomposite(): subprocess.check_call("rmmod usb_f_hid", shell=True, close_fds=True) def _setup(self): f_dev_name = self._hid_devname os.makedirs('/sys/kernel/config/usb_gadget/%s/strings/0x409' % f_dev_name, exist_ok=True) os.makedirs('/sys/kernel/config/usb_gadget/%s/configs/c.1/strings/0x409' % f_dev_name, exist_ok=True) os.makedirs('/sys/kernel/config/usb_gadget/%s/functions/hid.usb0' % f_dev_name, exist_ok=True) with open('/sys/kernel/config/usb_gadget/%s/idVendor' % f_dev_name, 'w') as fd: fd.write('0x%04x' % self._vendor) with open('/sys/kernel/config/usb_gadget/%s/idProduct' % f_dev_name, 'w') as fd: fd.write('0x%04x' % self._product) with open('/sys/kernel/config/usb_gadget/%s/bcdDevice' % f_dev_name, 'w') as fd: fd.write('0x0100') with open('/sys/kernel/config/usb_gadget/%s/bcdUSB' % f_dev_name, 'w') as fd: fd.write('0x0200') with open('/sys/kernel/config/usb_gadget/%s/strings/0x409/serialnumber' % f_dev_name, 'w') as fd: fd.write(self._serial) with open('/sys/kernel/config/usb_gadget/%s/strings/0x409/manufacturer' % f_dev_name, 'w') as fd: fd.write(self._manufacturer) with open('/sys/kernel/config/usb_gadget/%s/strings/0x409/product' % f_dev_name, 'w') as fd: fd.write(self._desc) with open('/sys/kernel/config/usb_gadget/%s/configs/c.1/strings/0x409/configuration' % f_dev_name, 'w') as fd: fd.write('Config 1 : %s' % self._desc) with open('/sys/kernel/config/usb_gadget/%s/configs/c.1/MaxPower' % f_dev_name,'w') as fd: fd.write('250') with open('/sys/kernel/config/usb_gadget/%s/functions/hid.usb0/protocol' % f_dev_name, 'w') as fd: fd.write('1') with open('/sys/kernel/config/usb_gadget/%s/functions/hid.usb0/subclass' % f_dev_name, 'w') as fd: fd.write('1') with open('/sys/kernel/config/usb_gadget/%s/functions/hid.usb0/report_length' % f_dev_name, 'w') as fd: fd.write(str(len(self._descriptor))) with open('/sys/kernel/config/usb_gadget/%s/functions/hid.usb0/report_desc' % f_dev_name, 'wb') as fd: fd.write(bytes(self._descriptor)) os.symlink( '/sys/kernel/config/usb_gadget/%s/functions/hid.usb0' % f_dev_name, '/sys/kernel/config/usb_gadget/%s/configs/c.1/hid.usb0' % f_dev_name, target_is_directory=True ) with open('/sys/kernel/config/usb_gadget/%s/UDC' % f_dev_name, 'w') as fd: fd.write('\r\n'.join(os.listdir('/sys/class/udc'))) def run(self): if not self._libcomposite_already_running: self.load_libcomposite() atexit.register(self._cleanup) # Setup HID gadget (keyboard) self._setup() # Use asyncio because we can then do thing on the side (web ui, polling attached devices using pyusb ...) try: self._loop.run_forever() except KeyboardInterrupt: pass if __name__ == '__main__': user_root = pwd.getpwuid(0) user_curr = pwd.getpwuid(os.getuid()) print('Running as <%s>' % user_curr.pw_name) if os.getuid() != 0: print('Attempting to run as ') sys.exit(os.system("/usr/bin/sudo /usr/bin/su root -c '%s %s'" % (sys.executable, ' '.join(sys.argv)))) parser = argparse.ArgumentParser() parser.add_argument('hid_type', choices=['keyboard', 'gamepad']) args = parser.parse_args() if args.hid_type == 'keyboard': print('Emulating: Keyboard') # Generic keyboard hid = HidDaemon(0x16c0, 0x0488, 'author', 'ODROID C2 KBD HID', 'fedcba9876543210', HIDReportDescriptorKeyboard) hid.run() elif args.hid_type == 'gamepad': print('Emulating: Gamepad') # Teensy FlightSim for the purpose of this example (and since it's intended for DIY, it fits ou purpose) hid = HidDaemon(0x16c0, 0x0488, 'author', 'ODROID C2 GAMEPAD HID', 'fedcba9876543210', HIDReportDescriptorGamepad) hid.run()The classes HIDReportDescriptorKeyboard and HIDReportDescriptorGamepad are where we describe our device such as its type, buttons, and axis count. Note that the vendorId and productId are also important since even if you describe your device as a gamepad, if the VID/PID are those of a keyboard, the operating system will most likely identity it as a keyboard.
Next, run the following command, which requires root privileges, in order to create a /dev/hidg0 device to which you can freely write:
$ sudo python3 script.py keyboardor
$ sudo python3 script.py gamepadWe can then test it with the keyboard argument:
$ sudo sleep 5 && echo -ne "\0\0\x4\0\0\0\0\0" > /dev/hidg0 && echo -ne "\0\0\0\0\0\0\0\0" > /dev/hidg0This command will write “A” (or “Q” if your use an azerty layout), after 5 seconds. Now, to test it with the gamepad argument use:
$ sudo sleep 5 && echo -ne "\0\0\0\x8c" > /dev/hidg0 && echo -ne "\0\0\0\0" > /dev/hidg0This will trigger the fourth button on the gamepad device.
Making different devices
The two examples use very basic descriptors which use the following bit format when writing to /dev/hidg0:
Keyboard (6-rollover)
BYTE 1 | BYTE 2 | BYTE 3 | BYTE 4 | BYTE 5 | BYTE 6 | BYTE 7 | BYTE 8 |
Modifiers | Reserved | Key 1 | Key 2 | Key 3 | Key 4 | Key 5 | Key 6 |
Gamepad
BYTE 1 | BYTE 2 | ||||||||||||||
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
Throttle (-127 to 127) | X-Axis (-127 to 127) |
BYTE 4 | |||||||||||||||
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
Y-Axis (-127 to 127) | B4 | B3 | B2 | B1 | (1) | (2) |
(1)When equals to 0b11 the HAT buttons are all set to non active, which should be a default. (2)HAT buttons bit-mask, given that bits 5-6 are set to 0 (a)0b00 => UP (b)0b01 => RIGHT (c)0b10 => DOWN (d)0b11 => LEFT
Conclusion
This is just a very basic example but it shows the options available to us. Delving deeper into the HID and USB specifications should make for plenty of use cases:
- Simulating a device, such as a specific keyboard, gamepad, mouse for development purposes.
- Making a device appear as another. For legacy devices.
- Pure USB debugging / reverse engineering (USBProxy project at https://github.com/dominicgs/USBProxy).
- Penetration testing.
- And surely many others I didn’t think of.
For comments, questions, and suggestions about the HID gadget, please visit the original thread at https://forum.odroid.com/viewtopic.php?f=139&t=30267.
Sources
USB HID 1.1 specs http://www.usb.org/developers/hidpage/HID1_11.pdf
Where if found the key scancodes for the keyboard https://gist.github.com/MightyPork/6da26e382a7ad91b5496ee55fdc73db2
To better understand report descriptors https://hamaluik.com/posts/making-a-custom-teensy3-hid-joystick/ and http://eleccelerator.com/tutorial-about-usb-hid-report-descriptors/
Tool for making descriptors (pretty clunky but official) http://www.usb.org/developers/hidpage#HID%20Descriptor%20Tool
Keyboard report descriptor http://isticktoit.net/?p=1383
Utility to check report descriptors http://eleccelerator.com/usbdescreqparser/
OTG peripheral DTB modification https://community.nxp.com/thread/383191
C2 mainline kernel support (frequency tweaks) https://forum.odroid.com/viewtopic.php?f=135&t=22717
Be the first to comment