Shopping Cart

Posts

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.

Sample output of mouse poll sketch

Sample output of mouse poll sketch

  1. 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
  2. Line 6 Defines device address. Current version of USB library works with single device and gives it address 1
  3. Line 7 Defines configuration value – see line 23 of mouse descriptor listing at the beginning of this article
  4. Lines 9-10 Forward declarations of standard Arduino setup() and loop() functions
  5. Lines 12-13 USB objects
  6. Lines 15-21 setup() function, where serial port and MAX3421E USB Host controller are initialized
  7. 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.
  8. 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.
  9. 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 );
}
  1. Line 9 defines maximum packet size for interrupt endpoint
  2. Line 10 declares data structure for the device
  3. Lines 62 – 78 This piece reads default idle rate and then sets it to infinity
  4. Lines 91 – 96 Error code 0×04 (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.