HID Gadget Device: Using The ODROID-C2

Feature Image for the HID gadget device on ODROID-C2 article

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.

HID gadget device with The ODROID-C2 using USB OTG device mode
Figure 1 - The ODROID-C2 can act as an HID device using USB OTG device mode

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 dtc
Make 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.dts
Once done, edit the source:
$ sudo nano meson-gxbb-odroidc2.dts
In 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 reboot
We can check the running kernel with the following command:
$ uname -r
It 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 keyboard
or
$ sudo python3 script.py gamepad
We 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/hidg0
This 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/hidg0
This 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

Leave a Reply