last update: Jan 25, 2010
Italian version

USB and PIC: quick guide to an USB HID framework

How to use an open source USB framework with PIC microcontrollers



Introduction
HID class
Using the firmware
Compile
Simulate
Communicate with a PC
Linux
Windows
Download
Links
Contacts

Quick facts

Open source GPL2 firmware
USB 2.0 full speed
HID class
Works on 18F2550, 2450, 2455, 2553, 4450, 4455, 4550, 4553
C code, to be compiled with MCC18 (evaluation version also)
No need for drivers
Communication example for Windows and Linux
No need for DDK or proprietary libraries
Linux version uses hiddev, the native HID manager

Introduction

This guide is not meant to be complete in any way. The purpose is to present and document a quick example of how to implement an USB device using a PIC18 microcontroller, on both firmware and software side.
For more detailed info I suggest to read the resources section.

The USB interface has been around for many years, but only recently it has become common in the low cost microcontroller world.
It is now very easy to set up a USB peripheral with a PIC18, as shown in the following diagram (LEDs only show the state of connection):
USB circuit
Unlike serial and parallel interfaces, USB needs a complicated enumeration process in order establish a communication channel.
The so called USB frameworks are code libraries that hide the details of the protocol, so that is possible to use the interface very quickly to communicate with a PC.
One of the most used comes from Microchip, but has a closed license and a somewhat complicated structure; in addition to it more or less all the compiler vendors give a framework with their highly priced products.
On the open source front there are examples of generic USB frameworks, but here I want to document a project that is virtually unknown and hard to find, that also implements the HID class.
The author is Alexander Enzmann; he released his work in 2005 with the periodical Nuts&Volts, but has not really documented or promoted his wonderful project.
I discovered this firmware only recently, and have converted all my projects to use it (see my USB programmer).
Some changes were required to compile with MCC18; it originally required SDCC.

The HID class

The USB protocol divides all peripherals in different classes, according to data transfer requirements and limitations; it is even possible to specifiy a new class, but using a standard one has the advantage that all major operating systems already include the proper software driver.
In particular the HID class (Human Interface Device), which comprises keyboards and mice, is perfect for interfacing small microcontrollers: data is generally exchanged using interrupt transfers with a maximum of 64 bytes per packet and a maximum speed of 64 KB/s.
During enumeration of HID devices the system reads a data structure called HID report descriptor, which specifies how many bytes to transfer and how to interpret the data.
This method is very flexible and allows to define all types of devices, for example a keyboard with extra keys or with integrated pointer, and in general any combination that would not be explicitly described in an operating system.
For more information see the specification of HID class.
However, in electronics projects we are often not interested in describing to the system what single bits or bytes mean; if we only care about transferring data the report descriptor will only specifiy the packet size.
Data is exchanged by means of an output report and an input report, with direction respectively from and to the PC; it is also possible to use a feature report in both directions.

Configuring and using the firmware

The first thing to do is to install MPLAB and MCC18 (the trial version is fine); dowload both from the Microchip website.
Here I put an example of application; it includes C source files and an MPLAB project.
The resulting device uses the first byte of the incoming packet to control port B (PORTB<7:2>); RB1 and RB0 indicate the transfer method used:
01 = output report via interrupt,
10 = feature report,
11 = output report via control endpoint.
The response is composed by:
the state of port A (1 byte);
the transfer type, 0xF0 if interrupt, 0xF1 if feature, 0xF2 if control;
a 2 byte timestamp incremented every 10.93 ms;
the remaining bytes received.
The source file usb.c contains the data structures that identify the peripheral; the first to be read is the device descriptor:
	rom byte deviceDescriptor[] =
	{
		0x12, 0x01, // bLength, bDescriptorType
		0x00, 0x02, // bcdUSB (low byte), bcdUSB (high byte)
		0x00, 0x00, // bDeviceClass, bDeviceSubClass
		0x00, E0SZ, // bDeviceProtocl, bMaxPacketSize
		0xD8, 0x04, // idVendor (low byte), idVendor (high byte)
		0xFF, 0x01, // idProduct (low byte), idProduct (high byte)
		0x01, 0x00, // bcdDevice (low byte), bcdDevice (high byte)
		0x01, 0x02, // iManufacturer, iProduct
		0x00, 0x01  // iSerialNumber (none), bNumConfigurations
	};
Vid and Pid are usually assigned by the USB consortium under payment, but of course for non commercial projects it's possible to use any number, for example 4D8 1FF; 0x4D8 is the Microchip ID.
The report descriptor specifies a 64 bytes report with reserved application; this means that the system won't attempt to decode and use it, as it would with keyboards and mice; the feature report is also 64 bytes long:
	rom byte HIDReport[HID_REPORT_SIZE] = {
		0x06, 0xa0, 0xff, 	// USAGE_PAGE (Vendor Defined Page 1)
		0x09, 0x01,       	// USAGE (Vendor Usage 1)
		0xa1, 0x01,       	// COLLECTION (Application)

		// The Input report
		0x09, 0x03,     	// Usage ID - vendor defined
		0x15, 0x00,     	// Logical Minimum (0)
		0x26, 0xFF, 0x00, 	// Logical Maximum (255)
		0x75, 0x08,     	// Report Size (8 bits)
		0x95, 0x40,     	// Report Count (64 fields)
		0x81, 0x02,     	// Input (Data, Variable, Absolute)

		// The Output report
		0x09, 0x04,     	// Usage ID - vendor defined
		0x15, 0x00,     	// Logical Minimum (0)
		0x26, 0xFF, 0x00, 	// Logical Maximum (255)
		0x75, 0x08,     	// Report Size (8 bits)
		0x95, 0x40,     	// Report Count (64 fields)
		0x91, 0x02,      	// Output (Data, Variable, Absolute)

		// The Feature report
		0x09, 0x01,     	// Usage ID - vendor defined
		0x15, 0x00,     	// Logical Minimum (0)
		0x26, 0xFF, 0x00, 	// Logical Maximum (255)
		0x75, 0x08,     	// Report Size (8 bits)
		0x95, 0x40,     	// Report Count (64 fields)
		0xB1, 0x02,      	// Feature (Data, Variable, Absolute)

		0xc0              	// END_COLLECTION
	};
The difference between input/output report and feature report is that the former uses generally an interrupt transfer (64KB/s max) to endpoint 1, the latter always a control transfer to endpoint 0, which is shared with the standard USB message flow; in this case, unlike interrupt transfers, the operating system does not guarantee a maximum latency but balances the data flow as it thinks is better; transfers are split in packets of bMaxPacketSize0, which is specified in the device descriptor; in usb.h it is defined as E0SZ.
The feature report was conceived to sporadically transfer configuration options or attributes to other data (for example the state of keyboard LEDs); input/output reports would carry the real data; it is however up to the designer to choose which one(s) to use.
If there's no need for a feature report it's possible to delete the corresponding section from the report descriptor; also delete HIDFeatureBuffer[HID_FEATURE_REPORT_BYTES] and functions SetupFeatureReport(byte reportID), SetFeatureReport(byte reportID), and GetFeatureReport(byte reportID); finally delete the calls to those functions in ProcessControlTransfer(void) and ProcessHIDRequest(void)
Another way to access input/output reports is using HID messages GET_REPORT and SET_REPORT directed to endpoint 0 (control mode); it is similar to the feature report transfer, and the same considerations made before apply here as well; however the operating system documentation advises to use interrupt mode for continuous transfers.
If there's no need to use GET_REPORT and SET_REPORT it's possible to delete functions SetupOutputReport(byte reportID), SetOutputReport(byte reportID), and GetInputReport(byte reportID); also delete calls in ProcessControlTransfer(void) and ProcessHIDRequest(void).
Back to the configuration, string descriptors are read by the system to give a textual description of vendor and product; they must be 2 byte unicode as the following example:
	rom byte stringDescriptor1[] =
	{
	    18, STRING_DESCRIPTOR, // bLength, bDscType
	    'S',0,'t',0,'r',0,'i',0,'n',0,'g',0,' ',0,'1',0,
	};
In usb.h is specified the size of data reports; these must of course match what is written in the report descriptor:
#define HID_INPUT_REPORT_BYTES   64
#define HID_OUTPUT_REPORT_BYTES  64
#define HID_FEATURE_REPORT_BYTES 64
USB communication routines are implemented without interrupts; this is possible because the protocol specifies very long response times (after enumeration there's a 5 second limit for data transfers) and the USB peripheral keeps sending NAck packets automatically if new data is not available.
The advantage is that user functions can take full control of the microcontroller.
The main cycle repeatedly calls the USB transaction manager and the user function ProcessIO():
	while(1){
		EnableUSBModule();
		if(UCFGbits.UTEYE != 1) ProcessUSBTransactions();
		ProcessIO();
	}
Evidently ProcessUSBTransactions() handles the protocol, i.e. what comes to endpoint 0.
What is interesting to the user is how to send and receive data; input and output reports are transferred to endpoint1 buffers (HIDRxBuffer and HIDTxBuffer, in USB RAM), and from there they must be copied to a user location by calling the reading function:
	number_of_bytes_read = HIDRxReport(receive_buffer, HID_OUTPUT_REPORT_BYTES);
Reading data empties the input buffer and allows the device to receive a new report; up to that point all incoming data is not accepted.
Here is the corresponding write:
	number_of_bytes_written = HIDTxReport(transmit_buffer, HID_INPUT_REPORT_BYTES);
When using GET_REPORT and SET_REPORT it's necessary to insert user code in functions SetOutputReport(byte reportID) and GetInputReport(byte reportID).
So with interrupt transfers the user code must check if something was received; with control transfers it is the protocol handling routine that calls the user function whenever a transfer is complete.
Regarding the feature report, the procedure is similar to that described above, only change functions: SetFeatureReport(byte reportID) and GetFeatureReport(byte reportID).

How to compile

This MPLAB project uses the MCC18 compiler, but probably the executable path will be different; the first time you compile MPLAB will report this and will allow to use the right paths.
It's also necessary to change the library path: from project options, section directories, click on suite defaults or set paths manually.
If you want to change device (presently is 18F2550), go to Configure->Select Device.
It will be necessary to change configuration bits accordingly, and to specify a new linker script; default scripts are ok, except for devices with less RAM, as 18F2450 and 4450: here you need to reduce stack size to something like 40 bytes.
The quartz oscillator can run at any multiple of 4 MHz, as long as the input divider is set accordingly; now it runs at 12 MHz.

How to simulate

MPSIM does not presently support USB peripherals, but anyways it wouldn't be useful to simulate the enumeration process; what is important is to check the code that uses the transferred data.
It can be done as follows:
add a breakpoint on the line
	if((deviceState < CONFIGURED)||(UCONbits.SUSPND==1)) return;
execute (Run, F9, or go button);
from File registers or Watch window change the value of deviceState to 5 (which corresponds to CONFIGURED state).
This way you skip enumeration; now you have to stop after function HIDRxReport and change the number of bytes read (number_of_bytes_read variable).
Transferred data should be written manually on HIDrxBuffer.
From now on it's like a regular debugging session.
To simulate transmission is sufficient to change number_of_bytes_written, if used.

Communication with PC

Communication code depends from the operating system; it is generally possible to use all-in-one libraries that take care of device detection and communication, but I prefer not to depend from external packages (which carry their own licensing problems) and to use the operating system directly.

Windows

My C example (compiled with DevC++) is a command line program that sends and receives a 64 byte packet using various methods.
To avoid using the DDK (driver development kit) I link explicitly the system libraries needed, hid.dll and setupapi.dll, and load manually the functions needed.
The program scans all HID devices until it finds one with the correct vid&pid.
There are 3 methods to exchange data: with input/output report in interrupt mode, in control mode, or with a feature report.
The firmware must of course support the method you choose.
Microsoft advises to use interrupt mode for continuous transfers; in this case the functions are similar to file I/O:
	Result=WriteFile(WriteHandle,bufferU,DIMBUF,&BytesWritten,NULL);
to write, and
	Result = ReadFile(ReadHandle,bufferI,DIMBUF,&NumberOfBytesRead,(LPOVERLAPPED) &HIDOverlapped);
	Result = WaitForSingleObject(hEventObject,10);
	ResetEvent(hEventObject);
to read.
This is an asynchronous read, i.e. ReadFile returns immediately and you have to call WaitForSingleObject to wait for I/O completion.
Communication in control mode (through SET_REPORT and GET_REPORT) can be commanded with:
	HidD_SetOutputReport(DeviceHandle,bufferOut,n)
to write, and
	HidD_GetOutputReport(DeviceHandle,bufferIn,n)
to read.
Similarly to use the feature report:
	HidD_SetFeature(DeviceHandle,bufferOut,n)
to write, and
	HidD_GetFeature(DeviceHandle,bufferIn,n)
to read.
In all these cases the first byte transferred indicates the report ID (usually 0); other data follows, so the total size increases by one (here 65 byte).
If you request less data than specified in the report descriptor the system reports an error; if you request more the response is padded with zeroes.
See actual code for details.

HidCom options:
-h, --help     	 help
-c, --control    use control transfer [no]\
-d, --delay    	 read delay (ms) [0]
-f, --feature  	 use feature report [no]
-i, --info     	 device info [no]
-I, --increment	 increment byte 6 every transfer [no]\
-p, --pid      	 Product ID [0x1FF]
-q, --quiet    	 print response only [no]
-r, --repeat   	 repeat N times [1]
-s, --size     	 report size [64]
-v, --vid      	 Vendor ID [0x4D8]
Example:
HidCom -i -r 5 -d 16 1 2 3 4 5 6 7 8 9 a b c d e f 0

Linux

Of all possible methods to communicate with HID devices I use what is probably the lowest level: interacting with hiddev, the Linux HID driver; in this way there's no need for external libraries.
After plugging an HID peripheral the operating system creates a device /dev/usb/hiddevX, where X is a progressive number.
You need reading rights to communicate with it:
	>sudo chmod a+r /dev/usb/hiddev0
To permanently enable a user do the following (on Ubuntu and other Debian based distributions, check for others):
as root create a file /etc/udev/rules.d/99-hiddev.rules
if you want to enable a user group write:
	KERNEL=="hiddev[0-9]", SUBSYSTEM=="usb", SYSFS{idProduct}=="01ff", SYSFS{idVendor}=="04d8", GROUP="<group>"
where <group> is one of the user groups (to get a list type "groups"); select a suitable group and if your user desn't belong to it execute "addgroup <user> <group>".

Or, if you want to enable all users, change reading permissions:
	KERNEL=="hiddev[0-9]", SUBSYSTEM=="usb", SYSFS{idProduct}=="01ff", SYSFS{idVendor}=="04d8", MODE="0664"
restart udev to apply changes:
	>/etc/init.d/udev reload
Pid and vid come from the example firmware, change them according to what is in the device.

The program communicates using ioctl calls, passing as parameter some data srutctures that specify the transfer mode.
	struct hiddev_report_info rep_info_i,rep_info_u;
	struct hiddev_usage_ref_multi ref_multi_i,ref_multi_u;
Hiddev was developed for classic input/output devices, so has many functions to check the single usages specified in the report descriptor; however we need a way to transfer the whole report in one time; this is possible with HIDIOCSUSAGES and HIDIOCGUSAGES; data is in ref_multi_u.values and ref_multi_i.values.
Interrupt transfer:
	ioctl(fd,HIDIOCSUSAGES, &ref_multi_u); //Write ref_multi_u.values
	ioctl(fd,HIDIOCSREPORT, &rep_info_u);
	...
	ioctl(fd,HIDIOCGUSAGES, &ref_multi_i); //Read to ref_multi_i.values
	ioctl(fd,HIDIOCGREPORT, &rep_info_i);
Control transfer:
	ioctl(fd,HIDIOCSUSAGES, &ref_multi_u); //Write ref_multi_u.values
	ioctl(fd,HIDIOCSREPORT, &rep_info_u);
	...
	ioctl(fd,HIDIOCGREPORT, &rep_info_i);
	ioctl(fd,HIDIOCGUSAGES, &ref_multi_i); //Read to ref_multi_i.values
In reality only reads are forced to control endpoint; I couldn't find a way to force writes.
Anyways there's no reason to choose control transfers when interrupt is available; and if a device doesn't support interrupt it is not given to hiddev.

Transfer using the feature report (after specifying HID_REPORT_TYPE_FEATURE in the data structures):
	ioctl(fd,HIDIOCSUSAGES, &ref_multi_u); //Write ref_multi_u.values
	ioctl(fd,HIDIOCSREPORT, &rep_info_u);
	...
	ioctl(fd,HIDIOCGREPORT, &rep_info_i);
	ioctl(fd,HIDIOCGUSAGES, &ref_multi_i); //Read to ref_multi_i.values
See actual code for details.

HidCom options:
-h, --help     	 help
-c, --control    use control transfer [no]\
-d, --delay    	 read delay (ms) [0]
-f, --feature  	 use feature report [no]
-i, --info     	 device info [no]
-I, --increment	 increment byte 6 every transfer [no]\
--path           device path [/dev/usb/hiddev0]\
-p, --pid      	 Product ID [0x1FF]
-q, --quiet    	 print response only [no]
-r, --repeat   	 repeat N times [1]
-s, --size     	 report size [64]
-v, --vid      	 Vendor ID [0x4D8]
Example:
HidCom -i -r 5 -I 16 1 2 3 4 5 6 7 8 9 a b c d e f 0

Downloads

Firmware: complete MPLAB project or compiled firmware for 18F2550 (.hex)
HidCom for Linux
HidCom for Windows

Links

DevC++
USB 2.0 standard
HID page on USB.org
USB Central
USB & PIC
Microchip
hiddev documentation
GNU/GPL

Contacts

For informations or comments:
Alberto Maccioni  email.png

Top

Main page