As promised, a blog about USB-HID devices. This blog will demonstrate how to create a USB-HID keyboard device running on the PIC 18f14k50 USB Interface Board. Making a HID device is a bit more complicated that getting the USB serial communication, becuase there are a wide range of HID devices, it is very hard to make a generic library. However, HID devices are nice, because you don't need a special drivers on the Host PC. There are many HID devices that can directly interact with the host operating systems, like keyboards, mice, joysticks etc, it is even possible to make a generic HID device, which can be controlled from a host application (but I will save that for another blog). Today were making a keyboard device, once you press the boot button it will generate a character. It is an educational example and does not have any practical use, but you can use it as a starting point to make for example a IR remote receiver that generate keystrokes.
The Code
I've splitted up the code description in a couple of section, and it will explain it step by step. We start of with the regular stuff, not very existing and I won't explain it in more detail
-- chip setup
include 18f14k50
pragma target clock 48_000_000
include delay
Before creating the USB descriptors, we need to include the USB constant definitions by
include usb_defs
Ok, that was the easy stuff. USB works with enpoints, and we have to indicate which endpoints will be enabled, where there are located and specify their size, and endpoint can be considered more or less a communcation channel between the Host PC and the PIC device. An endpoint can consist of an input endpoint (seen from the Host computer, so output for the PIC device) and an output endpoints (again, seen from the Host computer, so input for the PIC device). An USB device should always have input & output enpoint zero, which is used for e.g. exchanging device configuration settings. In this example we will have and additional enpoint, which is used for the exchange of HID reports (i.e. the keyboard information). Furthermore, the an enpoint consist of two parts, the Endpoint Descriptor and the Enpoint Buffers, the setup of the Endpoint Descriptors are taken care of by the USB library, however, the Enpoint buffers have to be setup by the user of the library. The following code shows how to enable the endpoints and Endpoints Buffers:
const bit USB_EP0 = 1
const byte USB_EP0_OUT_SIZE = 8
const word USB_EP0_OUT_ADDR = ( USB_BASE_ADDRESS + 0x0010 )
const byte USB_EP0_IN_SIZE = 8
const word USB_EP0_IN_ADDR = ( USB_EP0_OUT_ADDR + USB_EP0_OUT_SIZE )
const bit USB_EP1 = 1
const byte USB_EP1_OUT_SIZE = 8
const word USB_EP1_OUT_ADDR = ( USB_EP0_IN_ADDR + USB_EP0_IN_SIZE )
const byte USB_EP1_IN_SIZE = 8
const word USB_EP1_IN_ADDR = ( USB_EP1_OUT_ADDR + USB_EP1_OUT_SIZE )
const bit USB_EP2 = 0
const bit USB_EP3 = 0
var volatile byte usb_ep1in_buf[ 8 ] at USB_EP1_IN_ADDR
const bit USB_EPx = 1 indicates that the endpoint is enabled, if the endpoint is enabled the address of the Endpoint buffer must be specified, in addition, it should map within a special memory region. The USB hardware "talks" with the microcontroller via a dual access memory, this memory location is dependent on the PIC type, however its base addres USB_BASE_ADDRESS constant is defined usb_defs, so we can use this constant to setup the memory regions for the usb buffers independent of the PIC type. You might wonder why the enpoint buffers are not mapped at the beginning of the memory regions (USB_BASE_ADDRESS), this is because the USB library needs to setup the Endpoint descriptors, they shall be located starting at the beginning of the dual access memory. To calculate the start of the memory region that can be used for the Endpoint buffers, take the highest enpoint number, add one, and multiply it by 8. So in this example we have endpoint 0 and 1, so 2 x 8 = 16 (0x10 hex).
Well, that just beginning, next "record" we have to specify is the USB_DEVICE_DESCRIPTOR, it consists of 18 bytes and describes the device properties. Take a look at the usb specifiction (see http://www.usb.org/) for the details.
const byte USB_DEVICE_DESCRIPTOR[USB_DEVICE_DESCRIPTOR_SIZE] = {
USB_DEVICE_DESCRIPTOR_SIZE, -- 18 bytes long
USB_DT_DEVICE, -- DEVICE 01h
0x00,
0x02, -- usb version 2.00
0x00, -- class
0x00, -- subclass
0x00, -- protocol
USB_EP0_OUT_SIZE, -- max packet size for end point 0
0xd8,
0x04, -- Microchip's vendor
0x55,
0x00, -- Microchip keyboard demo
0x01,
0x00, -- version 1.0 of the product
0x01, -- string 1 for manufacturer
0x02, -- string 2 for product
0x00, -- string 3 for serial number
0x01 -- number of configurations
}
const byte USB_STRING0[] =
{
0x04, -- bLength
USB_DT_STRING, -- bDescriptorType
0x09, -- wLANGID[0] (low byte)
0x04 -- wLANGID[0] (high byte)
}
const byte USB_STRING1[0x36] =
{
0x36, -- bLength
USB_DT_STRING, -- bDescriptorType
"M", 0x00,
"i", 0x00,
"c", 0x00,
"r", 0x00,
"o", 0x00,
"c", 0x00,
"h", 0x00,
"i", 0x00,
"p", 0x00,
" ", 0x00,
"T", 0x00,
"e", 0x00,
"c", 0x00,
"h", 0x00,
"n", 0x00,
"o", 0x00,
"l", 0x00,
"o", 0x00,
"g", 0x00,
"y", 0x00,
",", 0x00,
" ", 0x00,
"I", 0x00,
"n", 0x00,
"c", 0x00,
".", 0x00
}
const byte USB_STRING2[42] =
{
42, -- bLength
USB_DT_STRING, -- bDescriptorType
"J", 0x00,
"A", 0x00,
"L", 0x00,
" ", 0x00,
"H", 0x00,
"I", 0x00,
"D", 0x00,
" ", 0x00,
"K", 0x00,
"e", 0x00,
"b", 0x00,
"o", 0x00,
"a", 0x00,
"r", 0x00,
" ", 0x00,
" ", 0x00,
"D", 0x00,
"e", 0x00,
"m", 0x00,
"o", 0x00
}
I'll not go into all the details of the USB descriptors, however, we need to specify USB_STRING0..3 because we filled in the USB string identifiers in the USB_DEVICE_DESCRIPTOR record. These string have to conform a predfined format, i.e. the first byte should specify the total record length, and the second byte indicates that it is a string USB_DT_STRING, furthermore, it has to be a unicode string, therefore after each character a 0x00 value needs to be defined. USB_String0 is a special string, since it specifies the language code, which is set to US English. Furthermore we specified there is one configuration record (last byte in the USB_DEVICE_DESCRIPTOR record.
The USB_CONFIGURATION_DESCRIPTOR record specifies the properties of the USB device, again the details are available the USB specification at http://www.usb.org/. High level description is that were setting up one configuration record for a HID device, which specifies the HID interface by pointing to the HID descriptor and it will have one endpoint (is number of enpoint exluding enpoint 0 which should always be present).
const byte USB_HID_REPORT1[]=
{
0x05, 0x01, -- USAGE_PAGE (Generic Desktop)
0x09, 0x06, -- USAGE (Keyboard)
0xa1, 0x01, -- COLLECTION (Application)
0x05, 0x07, -- USAGE_PAGE (Keyboard)
0x19, 0xe0, -- USAGE_MINIMUM (Keyboard LeftControl)
0x29, 0xe7, -- USAGE_MAXIMUM (Keyboard Right GUI)
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)
0x95, 0x01, -- REPORT_COUNT (1)
0x75, 0x08, -- REPORT_SIZE (8)
0x81, 0x03, -- INPUT (Cnst,Var,Abs)
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 (Cnst,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 (Keyboard)
0x19, 0x00, -- USAGE_MINIMUM (Reserved (no event indicated))
0x29, 0x65, -- USAGE_MAXIMUM (Keyboard Application)
0x81, 0x00, -- INPUT (Data,Ary,Abs)
0xc0
}
const USB_CONFIGURATION_DESCRIPTOR_SIZE = 0x09 + 0x09 + 0x09 + 0x07
const byte USB_CONFIGURATION_DESCRIPTOR[ USB_CONFIGURATION_DESCRIPTOR_SIZE ]=
{
-- configuration descriptor - - - - - - - - - -
0x09, -- length,
USB_DT_CONFIGURATION, -- descriptor_type
USB_CONFIGURATION_DESCRIPTOR_SIZE,
0x00, -- total_length;
0x01, -- num_interfaces,
0x01, -- configuration_value,
0x00, -- configuration_string_id,
0b10000000, -- attributes (bus powered, no remote wake up)
100, -- max_power; (200ma)
-- interface descriptor - - - - - - - - - - - -
0x09, -- length,
USB_DT_INTERFACE, -- descriptor_type,
0x00, -- interface_number, (starts at zero)
0x00, -- alternate_setting, (no alternatives)
0x01, -- num_endpoints,
USB_HID_INTF, -- interface_class, (HID)
USB_BOOT_INTF_SUBCLASS, -- interface_subclass, (boot)
USB_HID_PROTOCOL_KEYBOARD, -- interface_protocol, (keyboard)
0x00, -- interface_string_id;
-- hid descriptor - - - - - - - - - - - - - - -
0x09, -- length,
USB_DT_HID, -- descriptor_type;
0x11,
0x01, -- hid_spec in BCD (1.11)
0x00, -- country_code, (0=not country specific)
0x01, -- num_class_descriptors, (1)
USB_DT_HID_REPORT, -- class_descriptor_type; (0x22 = report)
(count( USB_HID_REPORT1 ) & 0xFF ),
(count( USB_HID_REPORT1 ) >> 8 ),
0x07, -- length,
USB_DT_ENDPOINT, -- descriptor_type,
0b10000001, -- endpoint_address, (Endpoint 1, IN)
USB_EPT_INT, -- attributes; (Interrupt)
USB_EP1_IN_SIZE,
0x00, -- max_packet_size
0x01 -- interval (1ms)
}
The HID Report specifies which information can be exchanged between the Host PC and the
USB device. There are seperate specification available which describes the details of a HID report (again, see http://www.usb.org/).
Making a HID report can be quite cumbersome, and there are even tools available to make HID report. Anyhow, for now assume that the specified HID report can exchange keyboard related information between the Host PC and the USB device
OK that was the toughest part, now some real JAL code.
-- include remaining USB libraries
include usb_drv_core
include usb_drv
-- HID report layout (see USB specification for more details)
-- 0 Modifier byte
-- bit
-- 0 LEFT CTRL
-- 1 LEFT SHIFT
-- 2 LEFT ALT
-- 3 LEFT GUI
-- 4 RIGHT CTRL
-- 5 RIGHT SHIFT
-- 6 RIGHT ALT
-- 7 RIGHT GUI
-- 1 reserved
-- 2 keycode array (0)
-- 3 keycode array (1)
-- 4 keycode array (2)
-- 5 keycode array (3)
-- 6 keycode array (4)
-- 7 keycode array (5)
var byte hid_report_in[8] = { 0,0,0,0,0,0,0,0 }
var byte key_value = 4
var bit sw3 is pin_c2
var bit latched_sw3 = sw3
-- disable analog unit, all ports set to digital
enable_digital_io()
-- setup the USB device
usb_setup()
-- enable USB device
usb_enable_module()
-- set input where switch is attached to input
pin_c2_direction = input
-- main loop
forever loop
-- poll the usb ISR function on a regular base, in order to
-- serve the USB requests
usb_handle_isr()
-- check if USB device has been configured by the HOST
if usb_is_configured() then
-- prepare the HID buffer
if ( sw3 != latched_sw3 ) then
latched_sw3 = sw3
if ( sw3 == low )then
hid_report_in[2] = key_value
key_value = key_value + 1
if ( key_value == 40 ) then
key_value = 4
end if
else
hid_report_in[2] = 0
end if
-- Send the 8 byte packet over USB to the host.
usb_send_data(USB_HID_ENDPOINT, hid_report_in, count( hid_report_in ) , low )
-- debounce
delay_1ms(50)
end if
end if
end loop
First the remaing USB libraries have to be included, since the library need to have access
to the device and configuration records it has to be included after the definition of these recods. Next an array of 8 bytes is defined for the HID report were sending out from the device towards the HOST. The first byte is a modifier key, while the remaing bytes will contain the key character(s). More details can be found in the USB HID keyboard specification (see also www.usb.org). Keep in mind though that the characters you send are not ASCII character, for example the "a" character has a byte value of 4.
Next the switch is initialized, and all inputs are set to digital io. Then the USB will be initialized by calling the usb_setup() and usb_enable_module() procedures. Then the main loop, first it has to call the usb_handle_isr() procedure on a regular base, in order to service the USB hardware. Next it will check if the USB interface is already configure by the host, via the usb_is_configured() function. If the USB device is recognized by the Host OS, it will check for state changes of the program switch of the PIC 18f14k50 USB Interface Board. If the value has changed and the button is pressed, it will send a HID report (i.e. it will send a character to the Host PC). The value of the character starts at 4 (the "a" character) and will increase till the "z" character, when reached it will start over again with the "a" character. It will fill in the character in the HID report and then it sends the HID report via USB towards the Host PC by calling the usb_send_data() procedure. Which will send the data via endpoint 1.
Executing the demo
Compile the code (available at the Jallib SVN repository projects\pic14k50_usb_io\examples\blog_part3.jal) using the -loader18 flag. Reboot the PIC 18f14k50 USB Interface Board, while holding down the program and reset switch together and release the reset switch while keep holding down the program switch. After a couple of seconds you can release the program switch. Start the PDFSUSB.exe application to download the hex file. Once downloaded, press the reset button once again. After a couple of seconds the Host PC should recgonize the a new "Keyboard". Wait a couple of seconds, start an editor, for example notepad on the Host PC, if you now press & release the program button on the board, it should type a character within the editor, next time you press & release the program button it will show you the next character of the alphabet.
OK, a long story this time, I know not everything has been explained in detail yet about the Jallib USB libraries, but maybe in a future blog I can tell more about the inner working of the Jallib USB libraries. The next episode will likely building a Generic HID device.