1. Introduction
Human Interface Device (HID) is a class consisting primarily of devices that are used by humans to control the operation of computer systems. Typical examples are keyboard, mouse and joystick. In addition to that, some non-interactive devices are also using HID specification for data exchange, such as UPSes, scales and weather stations. HID has been around for a while and is very popular among peripheral manufacturers thanks to support in many OSes and simplicity of exchange protocol.
This article is the first one in series describing Arduino USB Host interaction with HID devices. It outlines basic principles, shows how to read HID report descriptor, and also contains two practical code examples. Arduino platform is used to run programs and USB Host Shield is used to provide low-level interface to USB devices. To run code examples you will also need USB Host Shield Arduino Library.
2. Devices and Report Descriptors
When user operates HID device, the device produces a piece of data called report. Computer learns what happened by polling device from time to time, parsing received reports and changes program flow accordingly. Devices operate with many different types of information – for example, keyboard has many buttons and sends key codes, mouse has just a few buttons so it sends just the state of those buttons, but is also capable to report its’ X and Y coordinates, while a steering wheel-type game controller sends wheel and pedal positions along with button presses. At the same time, various data can be sent from a computer to the device – LEDs on a keyboard or force-feedback on joystick or game controller, just to name a few. Simple devices, like mouse or keyboard, usually generate single report, while more complex devices often generate several.
A report is simple data structure, in most cases less than 10 bytes long. Format of this report is contained in much bigger and complex data structure called report descriptor. Report descriptor outlines what is contained in each byte (sometimes even each bit) of the report, type of data, units of measurement, range of values and other good stuff. Therefore, the format of report can be (and often is) determined by parsing report descriptor. The format and contents of report descriptors are well documented. The USB.org website has HID Page containing many useful documents, the main two being Device Class Definition for Human Interface Devices and HID Usage Tables. These two documents give good picture of what kind of information may be expected from HID device. Jan Axelson’s USB Complete book is also a good source of information. In addition to this, many web resources exist presenting topic of HID formats in more humane way – googling for “HID format”, “HID report”, etc., produces plenty of links to HID-related content.
Now I will show how to read simple HID report descriptor and derive report format from it. Below you can see descriptors of a Logitech M-UAE96 optical mouse, which reports usual X and Y coordinates as well as a wheel and 3 buttons. The output is produced by descriptor parser Arduino sketch, hosted on Github. The HID report descriptor resides at lines 47-73. The explanation continues below the listing, if you don’t understand something please check sources mentioned above.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | Device descriptor: Descriptor Length: 12 USB version: 2.0 Class: 00 Use class information in the Interface Descriptor Subclass: 00 Protocol: 00 Max.packet size: 08 Vendor ID: 046D Product ID: C018 Revision ID: 4301 Mfg.string index: 01 Length: 18 Contents: Logitech Prod.string index: 02 Length: 36 Contents: USB Optical Mouse Serial number index: 00 Number of conf.: 01 Configuration number 0 Total configuration length: 34 bytes Configuration descriptor: Total length: 0022 Number of interfaces: 01 Configuration value: 01 Configuration string: 00 Attributes: A0 Remote Wakeup Max.power: 32 100ma Interface descriptor: Interface number: 00 Alternate setting: 00 Endpoints: 01 Class: 03 HID (Human Interface Device) Subclass: 01 Protocol: 02 Interface string: 00 HID descriptor: Descriptor length: 09 9 bytes HID version: 1.11 Country Code: 0 Not Supported Class Descriptors: 1 Class Descriptor Type: 22 Report Class Descriptor Length:52 bytes HID report descriptor: Length: 1 Type: Global Tag: Usage Page Generic Desktop Controls Data: 01 Length: 1 Type: Local Tag: Usage Data: 02 Length: 1 Type: Main Tag: Collection Application (mouse, keyboard) Data: 01 Length: 1 Type: Local Tag: Usage Data: 01 Length: 1 Type: Main Tag: Collection Physical (group of axes) Data: 00 Length: 1 Type: Global Tag: Usage Page Button Data: 09 Length: 1 Type: Local Tag: Usage Minimum Data: 01 Length: 1 Type: Local Tag: Usage Maximum Data: 03 Length: 1 Type: Global Tag: Logical Minimum Data: 00 Length: 1 Type: Global Tag: Logical Maximum Data: 01 Length: 1 Type: Global Tag: Report Size Data: 01 Length: 1 Type: Global Tag: Report Count Data: 03 Length: 1 Type: Main Tag: Input Data,Variable,Absolute,No Wrap,Linear,Preferred State,No Null Position,Non-volatile(Ignore for Input), Data: 02 Length: 1 Type: Global Tag: Report Size Data: 05 Length: 1 Type: Global Tag: Report Count Data: 01 Length: 1 Type: Main Tag: Input Constant,Array,Absolute,No Wrap,Linear,Preferred State,No Null Position,Non-volatile(Ignore for Input), Data: 01 Length: 1 Type: Global Tag: Usage Page Generic Desktop Controls Data: 01 Length: 1 Type: Local Tag: Usage Data: 30 Length: 1 Type: Local Tag: Usage Data: 31 Length: 1 Type: Local Tag: Usage Data: 38 Length: 1 Type: Global Tag: Logical Minimum Data: 81 Length: 1 Type: Global Tag: Logical Maximum Data: 7F Length: 1 Type: Global Tag: Report Size Data: 08 Length: 1 Type: Global Tag: Report Count Data: 03 Length: 1 Type: Main Tag: Input Data,Variable,Relative,No Wrap,Linear,Preferred State,No Null Position,Non-volatile(Ignore for Input), Data: 06 Length: 0 Type: Main Tag: End Collection Length: 0 Type: Main Tag: End Collection Endpoint descriptor: Endpoint address: 01 Direction: IN Attributes: 03 Transfer type: Interrupt Max.packet size: 0005 Polling interval: 0A 10 ms |
First thing to look at is Report ID tag. Devices which produce multiple reports send them separately placing report ID in the first byte of report. Our mouse doesn’t have Report IDs – it means that it produces single report with all its data packed into it.
Find first Input tag (line 59). This is the first piece of data. Look above it – Report Size (line 57 ) times Report Count (line 58) gives the size, in this case 3 bits. Now jump to line 52 – Usage Page “Button” answers the question what kind of data this is. Buttons have only two states – one for pressed and zero for released, which is defined by Logical Maximum (line 56) and Logical Minimum (line 55) tags. Usage Minimum (line 53) and Usage Maximum (line 54) gives names to bits, in this case Button 1, Button 2 and Button3.
The second Input tag (line 62) shows padding, 5 bits (line 60 times line 61 ) with no Usage and ranges defined. The idea of padding is to align next data piece on a byte boundary. We now know the contents of a first byte – 3 buttons in bits 0-2 and the rest of the byte empty.
Next Input tag (line 71) and its Report Size, Report Count pair defines 3 bytes, the first one being X-axis (line 64), the second Y-axis (line 65), and the last one being Wheel (line 66). See HID Usage Tables document on usb.org. At the time of this writing, the latest version of this doc was 1.12, and Usage (lines 64-66) meanings table for “Generic Desktop Controls” (line 63) is on page 26 of the document.
The Logical Minimum (line 67) is -127, Logical Maximum (line 68) is 127. Also note, that data is defined as “Relative” in Input tag, which means report will contain distance traveled by mouse since previous report reading – for the same distance, reading report more often during movement will give smaller values. We are all familiar with the phenomena when mouse cursor movement on heavily-loaded PC becomes “jumpy”. We now can conclude that jumpy movement is nothing more than the same movement represented in bigger chunks.
We now know the whole report length and data layout. It is 4 bytes, the first is a bit field, other three are X, Y and wheel movement. It is now possible to start writing the application. It has to be noted that there is an easier way to decipher mouse report. HID class defines “boot” protocol for keyboard and mouse. Some time ago I wrote an article showing how to read a keyboard using boot protocol. Device in boot protocol mode has its report descriptor predefined; there is no need to look at report descriptor. Most keyboards and mice support boot protocol, which is indicated by “1” in Interface descriptor Subclass field (line 33). However, boot protocol defines only basic features – additional controls, like volume control buttons on a keyboard or wheel on a mouse are not available.
3. Reading reports
There are three HID report types – Input, Output, and Feature. Input reports are used to transmit device state change, like key press on a keyboard or mouse movement. Output reports are used to change device state, for example, LEDs on a keyboard are turned on and off using output report. At present, I don’t have good example of Feature report.
An input report can be read in one of two ways. One way is to poll Interrupt In endpoint. Another – to send “Get Report” control request. Even though HID spec doesn’t recommend using Get Report for regular device polls, I found this method quite handy in situations when memory budget is tight – since we are working with single control endpoint AKA “default control pipe”, no extra memory is needed for other endpoints data, such as Max.packet size, data toggles and such. Get Report request method has drawbacks, too, this will be explained later. The following listing demonstrates Get Report request polling method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | /* Mouse communication via control endpoint */ #include <spi.h> #include <max3421e.h> #include <usb.h> #define DEVADDR 1 #define CONFVALUE 1 void setup(); void loop(); MAX3421E Max; USB Usb; void setup() { Serial.begin( 115200 ); Serial.println("Start"); Max.powerOn(); delay( 200 ); } void loop() { byte rcode; Max.Task(); Usb.Task(); if( Usb.getUsbTaskState() == USB_STATE_CONFIGURING ) { mouse0_init(); }//if( Usb.getUsbTaskState() == USB_STATE_CONFIGURING... if( Usb.getUsbTaskState() == USB_STATE_RUNNING ) { //poll the keyboard rcode = mouse0_poll(); if( rcode ) { Serial.print("Mouse Poll Error: "); Serial.println( rcode, HEX ); }//if( rcode... }//if( Usb.getUsbTaskState() == USB_STATE_RUNNING... } /* Initialize mouse */ void mouse0_init( void ) { byte rcode = 0; //return code /**/ Usb.setDevTableEntry( 1, Usb.getDevTableEntry( 0,0 ) ); //copy device 0 endpoint information to device 1 /* Configure device */ rcode = Usb.setConf( DEVADDR, 0, CONFVALUE ); if( rcode ) { Serial.print("Error configuring mouse. Return code : "); Serial.println( rcode, HEX ); while(1); //stop }//if( rcode... Usb.setUsbTaskState( USB_STATE_RUNNING ); return; } /* Poll mouse using Get Report and print result */ byte mouse0_poll( void ) { byte rcode,i; char buf[ 4 ] = { 0 }; //mouse buffer static char old_buf[ 4 ] = { 0 }; //last poll /* poll mouse */ rcode = Usb.getReport( DEVADDR, 0, 4, 0, 1, 0, buf ); if( rcode ) { //error return( rcode ); } for( i = 0; i < 4; i++) { //check for new information if( buf[ i ] != old_buf[ i ] ) { //new info in buffer break; } } if( i == 4 ) { return( 0 ); //all bytes are the same } /* print buffer */ if( buf[ 0 ] & 0x01 ) { Serial.print("Button1 pressed "); } if( buf[ 0 ] & 0x02 ) { Serial.print("Button2 pressed "); } if( buf[ 0 ] & 0x04 ) { Serial.print("Button3 pressed "); } Serial.println(""); Serial.print("X-axis: "); Serial.println( buf[ 1 ], DEC); Serial.print("Y-axis: "); Serial.println( buf[ 2 ], DEC); Serial.print("Wheel: "); Serial.println( buf[ 3 ], DEC); for( i = 0; i < 4; i++ ) { old_buf[ i ] = buf[ i ]; //copy buffer } Serial.println(""); return( rcode ); } |
This sketch initializes the mouse and then polls it and prints report if it is different from the previous one. The sketch can be pasted from this page into Arduino IDE directly, no other files except, of course, USB library, is necessary. A screen shot of sketch output and line by line explanation of the code follows.
- Lines 2-4 Define libraries necessary for the sketch. Spi is one of the standard libraries of Arduino IDE, Max3421 and Usb should be downloaded from Github and placed into “libraries” folder
- Line 6 Defines device address. Current version of USB library works with single device and gives it address 1
- Line 7 Defines configuration value – see line 23 of mouse descriptor listing at the beginning of this article
- Lines 9-10 Forward declarations of standard Arduino setup() and loop() functions
- Lines 12-13 USB objects
- Lines 15-21 setup() function, where serial port and MAX3421E USB Host controller are initialized
- Lines 23-39 loop() function, where Max.Task() handles device connect/disconnect events on physical level and Usb.Task() state machine performs enumeration. As soon as Usb.Task() reaches USB_STATE_CONFIGURING state, mouse initialization function mouse0_init() is called. The state machine then is moved to RUNNING state, which causes mouse0_poll() function to be called. The loop() function gets called indefinitely.
- Lines 41-55 Mouse initialization function. First, it points device 0 table entry (that’s default address for freshly connected device, and endpoint 0 data is retrieved automatically by Usb.Task() ) to device 1 table entry (see line 45). This way, no extra memory is used to support new device. Then, it sends “Set Configuration” request( see line 47 ). If Set Configuration returns an error, function prints a message and goes to endless loop (lines 49-51), otherwise it switches Usb.Task() state machine to RUNNING state and returns to the main loop.
- Lines 56-96 Mouse polling function. “Get Report” request is sent on line 62, then received data is checked with data from the previous poll. If no difference is found, function returns to the main loop, otherwise contents of the buffer is printed out and then copied to old_buf[] array to be used as “previous data” in the next poll.
Important feature of Get Report request is that it returns a report whether anything changed since last poll or not. If I were to print out every report received the screen would soon be filled by meaningless data. In order to show only ones that make sense, mouse0_poll function skips a report which is identical to the previous one. In order to do this, the function saves previous report using four statically allocated bytes and memory saved on endpoint structure gets consumed in the parser. However, in many real life applications current state of whatever is affected by mouse movement (screen cursor, say) needs to be stored anyway so polling method demonstrated above makes more sense.
The second method of getting reports from the device is more flexible. Every HID device has one interrupt IN endpoint. Also, HID device supports “Set Idle” request which can be used to set report frequency on this endpoint. If Idle is set to zero, no reports will be returned unless some control on the device changes state. It can also be set to some number (in 4 millisecond increments), defining time after which a report will be returned even if nothing has changed. For example, keyboard repetition rate on a PC is made this way. The default idle rate is not standardized but 500ms is recommended for keyboards and zero for mice and joysticks.
Second code example below demonstrates polling a mouse via interrupt endpoint. A couple of changes has been made to the logic – first, the initialization function determines default idle rate of the device using “Get Idle” request and then changes it to zero. Second, the polling function is shorter – we now know that if there is no new information, mouse immediately returns NAK and the rest of processing can be skipped. The rest of the code is very similar to the previous one so only differences between two sketches will be explained after the listing.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | /* Mouse communication via interrupt endpoint */ /* Assumes EP1 as interrupt IN ep */ #include <spi.h> #include <max3421e.h> #include <usb.h> #define DEVADDR 1 #define CONFVALUE 1 #define EP_MAXPKTSIZE 5 EP_RECORD ep_record[ 2 ]; //endpoint record structure for the mouse void setup(); void loop(); MAX3421E Max; USB Usb; void setup() { Serial.begin( 115200 ); Serial.println("Start"); Max.powerOn(); delay( 200 ); } void loop() { byte rcode; Max.Task(); Usb.Task(); if( Usb.getUsbTaskState() == USB_STATE_CONFIGURING ) { mouse1_init(); }//if( Usb.getUsbTaskState() == USB_STATE_CONFIGURING... if( Usb.getUsbTaskState() == USB_STATE_RUNNING ) { //poll the keyboard rcode = mouse1_poll(); if( rcode ) { Serial.print("Mouse Poll Error: "); Serial.println( rcode, HEX ); }//if( rcode... }//if( Usb.getUsbTaskState() == USB_STATE_RUNNING... } /* Initialize mouse */ void mouse1_init( void ) { byte rcode = 0; //return code byte tmpdata; byte* byte_ptr = &tmpdata; /**/ ep_record[ 0 ] = *( Usb.getDevTableEntry( 0,0 )); //copy endpoint 0 parameters ep_record[ 1 ].MaxPktSize = EP_MAXPKTSIZE; ep_record[ 1 ].sndToggle = bmSNDTOG0; ep_record[ 1 ].rcvToggle = bmRCVTOG0; Usb.setDevTableEntry( 1, ep_record ); //plug kbd.endpoint parameters to devtable /* Configure device */ rcode = Usb.setConf( DEVADDR, 0, CONFVALUE ); if( rcode ) { Serial.print("Error configuring mouse. Return code : "); Serial.println( rcode, HEX ); while(1); //stop }//if( rcode... rcode = Usb.getIdle( DEVADDR, 0, 0, 0, (char *)byte_ptr ); if( rcode ) { Serial.print("Get Idle error. Return code : "); Serial.println( rcode, HEX ); while(1); //stop } Serial.print("Idle Rate: "); Serial.print(( tmpdata * 4 ), DEC ); //rate is returned in multiples of 4ms Serial.println(" ms"); tmpdata = 0; rcode = Usb.setIdle( DEVADDR, 0, 0, 0, tmpdata ); if( rcode ) { Serial.print("Set Idle error. Return code : "); Serial.println( rcode, HEX ); while(1); //stop } Usb.setUsbTaskState( USB_STATE_RUNNING ); return; } /* Poll mouse via interrupt endpoint and print result */ /* assumes EP1 as interrupt endpoint */ byte mouse1_poll( void ) { byte rcode,i; char buf[ 4 ] = { 0 }; //mouse report buffer /* poll mouse */ rcode = Usb.inTransfer( DEVADDR, 1, 4, buf, 1 ); // //rcode = Usb.getReport( DEVADDR, 0, 4, 0, 1, 0, buf ); if( rcode ) { //error if( rcode == 0x04 ) { //NAK rcode = 0; } return( rcode ); } /* print buffer */ if( buf[ 0 ] & 0x01 ) { Serial.print("Button1 pressed "); } if( buf[ 0 ] & 0x02 ) { Serial.print("Button2 pressed "); } if( buf[ 0 ] & 0x04 ) { Serial.print("Button3 pressed "); } Serial.println(""); Serial.print("X-axis: "); Serial.println( buf[ 1 ], DEC); Serial.print("Y-axis: "); Serial.println( buf[ 2 ], DEC); Serial.print("Wheel: "); Serial.println( buf[ 3 ], DEC); Serial.println(""); return( rcode ); } |
- Line 9 defines maximum packet size for interrupt endpoint
- Line 10 declares data structure for the device
- Lines 62 – 78 This piece reads default idle rate and then sets it to infinity
- Lines 91 – 96 Error code 0x04 (NAK) is normal, it just means that no new data is available, therefore mouse1_poll returns 0
Note: some mice don’t support GetIdle/SetIdle commands and would return “Stall”. As a result, the sketch will stop on error. If you are running the sketch and see this:
Start
Data packet error: 5Get Idle error. Return code : 5
comment out while(1)
statements on lines 66 and 76, recompile and try again. Also, make sure that endpoint 1 of your mouse has maximum packet size of 5 (some have it set to 4, 6, or 8 bytes) and change EP_MAXPKTSIZE
accordingly.
As you can see, communicating with basic single-report HID device is easy. At the same time, complex and precise control can be implemented. Even more can be done with more feature-rich gadgets like HID R/C controllers and USB PC remotes. In second part of this series (available soon), I will show how to interpret and use multi-report data.