USB Communication using PIC microcontrollers
USB Overview
Universal Serial Bus, or USB, is a very common method of communication between electronic devices. USB is universal, meaning that it's standard across all electronic devices, serial, meaning that data is transmitted on only one data line one bit at a time, and is a bus, meaning that all devices are connected through the same parallel electronic wires and therefore require addresses. A simple USB connection requires a host and a device. The host initiates all communication, assigns device addresses, and controls the flow of the data line in the connection. For our purposes, the USB host will be the PC and the USB device will be a PIC chip.
There are many different speeds associated with USB. USB 1.0 introduced data rates of 1.5 Mbits/s (Low-Speed) and 12 Mbits/s (Full-Speed). With USB 2.0 and 3.0, data rates of 480 Mbits/s (Hi-Speed) and 5 Gbits/s (Super-Speed), were added respectively. PIC chips have hardware capable of full speed USB.
Hardware
A USB cable has 4 wires: power (red), ground (black), D+ (green), and D- (white). You may notice that many USB plugs and receptacles have 5 pins. This fifth pin is called ID, and is often not connected, but has been added to the specifications for use with USB-OTG, or On-The-Go. OTG allows devices to also act as a host. Many PIC chips are compatible with USB-OTG communication, but we will not discuss this topic any further at this point.
The two data lines, D+ and D-, provide information about the state of the USB link.
- Detached State, or SE0 - device is unplugged, both data lines are low, pulled down by 15 kOhm resistors within the host hardware.
- Attached State, Idle State, or J State - the device pulls up the D+ line (for a high-speed device) when it is attached.
- K State - opposite polarity from J State. To communicate, the host or device alternates the device between the J and K states in specific patterns.
- Illegal State, or SE1 - both data lines are high. If this happens, there is a hardware problem with the USB link.
Protocol
Useful Links HID Specification USB Made Simple
Microchip USB Stack
Microchip has provided almost all of the code and examples to run USB code on PIC chips. This code is called the USB stack, and we will use this as the main library for communicating to PCs. Unfortunately, there are a few bugs for certain PIC chips and some quirks to getting the libraries setup in MPLAB. This section will document the steps necessary to install the USB stack and setup a proper programming environment in MPLAB.
Installation
Download
We'll begin with the assumption that the reader knows how to setup a project for a PIC chip in MPLAB and program their chip with some basic C code. The Microchip USB stack is a part of the Microchip Application Libraries, which are available at the Microchip website here.
You must download and install the entire Application Libraries package, which will place all the libraries in your C drive on a Windows machine and some other place (**FIND OUT WHERE**) on a Mac. The name of the root directory should be something like "Microchip Solutions v2012...", but from here on out, we'll refer to the root directory of that installation as <MAL>.
Include Libraries and Driver in Project
Once the installer has finished running, the actual USB stack is located at <MAL>/Microchip/USB. <MAL>/USB is not the stack, and only contains demos of the firmware for learning purposes. We will not be using any of those files for our purposes. After starting a project in MPLAB for your PIC chip, you must complete the following steps:
- Include the USB libraries in your project
- In MPLAB X, right-click on your project and select properties. In the left-sidebar, click on your compiler (pic32-gcc for PIC32 chips). In the main window, find the field called "Include Directories" and click the button with "..." Add the directories <MAL>/Microchip/USB, <MAL>/Microchip/Include, and importantly your current project directory. This last one is important because the stack references a Hardware Profile which we will include in our own project directory.
- Add the HID device drivers to your project
- In MPLAB X, right-click on your project source files and select "add existing..." Browse to <MAL>/Microchip/USB and add usb_device.c and ./HID Device Driver/usb_function_hid.c. These files do not need to be moved to your project directory, they just need to be added into your project via MPLAB.
Add Application-Specific USB files
Two additional header files and two additional source files specific to each application are required for the microcontroller to be fully USB functional.
- usb_config.h - provides configuration details for the mode of USB operation
- HardwareProfile.h - gives information about the specific microcontroller we're using
- usb_descriptors.c - defines all the descriptors used to tell the Host PC what kind of USB device we're connecting
- usb_callbacks.c - defines all callback functions executed when the driver detects certain conditions in the connection
The file attached below is a zip containing examples of these four files. This example provides generic USB HID operation under Microchip's VendorID (1240) and a ProductID of 0, operating in USB_INTERRUPT mode (described below).
Microchip Stack Bug Notes
For the pic32mx2xx series, the usb interrupts are not configured correctly in the Microchip stack, so the following changes need to be made.
In <MAL>/Microchip/Include/usb_hal_pic32.h (Line 365):
This section normally sets the bits to configure IPC11, the USB interrupt location, but on the pic32mx2xx series, the USB interrupt bits are located at IPC7, as detailed in the Interrupt Controller chapter of the PIC datasheet. We've added a precompiler if statement to detect if a specific chip is being used. It's simple to change this for your specific chip though.
#if defined(USB_INTERRUPT) // This is edited for use with the PIC32MX250F128B #ifdef __32MX250F128B__ #define USBEnableInterrupts() {IEC1bits.USBIE = 1; IPC7CLR=0x00FF0000;IPC7SET=0x00100000; INTEnableSystemMultiVectoredInt(); INTEnableInterrupts();} #else #define USBEnableInterrupts() {IEC1bits.USBIE = 1; IPC11CLR=0x0000FF00;IPC11SET=0x00001000; INTEnableSystemMultiVectoredInt(); INTEnableInterrupts();} #endif #else #define USBEnableInterrupts() #endif
In <MAL>/Microchip/USB/usb_device.c (Line 673) The Microchip stack assumes that the USB interrupt vector is 45 but this has changed to 30 for the pic32mx2xx series.
The line:
void __attribute__((interrupt(),vector(45))) _USB1Interrupt( void )
Should be:
void __attribute__((interrupt(),vector(_USB_1_VECTOR))) _USB1Interrupt( void )
USB Hardware Notes
It is important to use an external clock or oscillator in order to use the USB module. A 48 MHz clock is required for proper operation. The clock configuration can be seen in the data sheet, but for the pic32mx2xx series a 4 Mhz clock is required before the USB PLL. Since I am using a 20MHz external clock, we use these lines to set DEVCFG2 to proper values.
#pragma config UPLLEN = ON // USB PLL Enable #pragma config UPLLIDIV = DIV_5 // USB PLL Divider // 20MHz / 5 * 24 / 2 = 48MHz
USB Stack Operation
A function called USBDeviceTasks() defined in usb_device.c handles USB events, host requests, and enumeration. This function must be called every ~1.8ms during enumeration, and at specific intervals during other operation. This timing can be faster than the minimum with no hitch, but slower causes failure in communication. The stack may operate in two modes, USB_POLLING, or USB_INTERRUPT as defined in usb_config.h.
USB_POLLING requires the developer to call USBDeviceTasks() manually faster than the minimum rate, whereas USB_INTERRUPT handles all timing automatically. In the demo, we use USB_INTERRUPT to handle this timing.
Below is some sample code in the while loop of a main function of PIC communicating via USB.
while(1){ if((USBGetDeviceState() < CONFIGURED_STATE) || (USBIsDeviceSuspended() == TRUE)) { //Either the device is not configured or we are suspended, // so we don't want to execute any USB related application code continue; //go back to the top of the while loop } if(!HIDRxHandleBusy(USBOutHandle)) //Check if data was received from the host. { //Do something with the data ToSendDataBuffer[0] = ReceivedDataBuffer[0]; //Re-arm the OUT endpoint for the next packet USBOutHandle = HIDRxPacket(HID_EP,(BYTE*)&ReceivedDataBuffer,64); } if(!HIDTxHandleBusy(USBInHandle)){ // Send out new data USBInHandle = HIDTxPacket(HID_EP,(BYTE*)&ToSendDataBuffer[0],64); } }
Demo
This MPLABX Project Directory is an example project written for the PIC32MX250F128B.
This demo communicated with the processing demo explained below, using the 28-DIP package for the pic32 and the following pinout.
- Pin01 - MCLR
- Pin03 - Configured for OC2, connected to an LED
- Pin04 - PGED1
- Pin05 - PGEC1
- Pin07 - Configured for AN5, reads analog voltage from potentiometer
- Pin08 - Vss
- Pin09 - External Clock, running at 20MHz
- Pin12 - Configured for digital in, reads switch state
- Pin13 - Vdd
- Pin15 - Vbus, connected to USB 5 Volt line
- Pin16 - Configured for digital out, connected to an LED
- Pin18 - Configured for OC3, connected to an LED
- Pin19 - Vss
- Pin20 - Vcap, 10nF capacitor to Vss
- Pin21 - USB, D+
- Pin22 - USB, D-
- Pin23 - Vusb, 3.3V
- Pin27 - AVss, 10nF capacitor to AVdd
- Pin28 - AVdd, 3.3V
Processing and the HID API
To send and receive USB packets in Processing on the computer, we use a Java Native Interface library which uses C libraries to communicate via USB. Documentation and downloads for this library can be found here. This is the Processing sketch used to interface with the demo board described above.
The code directory contains both the .jar file containing the Java library, and the .dll, .jnilib, and .so files containing the native USB libraries. These files need to be here, but do not require editing.
In the sketch itself, the USBListener file defines a Thread which listens to the USB port. The function pic.read() will wait until the USB buffer is not empty, and then place the first-in data into the specified variable. This thread is also a good place to update all state variables and send out data to the PIC using pic.write(). The HIDManager extends the HIDManager class defined in the API. This class defines callback functions which are called whenever a device is added or removed.
The main file includes the typical functions, setup() and draw(), as well as an openPic() function used to search the computer for a connected PIC microcontroller and open it for communication.
Here is the API documentation, which explains what each function does.