Open source USB echo demo

From DP

Jump to: navigation , search



This is an open source USB stack for PIC 24F and PIC 18F microcontrollers with USB hardware. It's licensed Creative Commons Attribution, so you can use pretty much however you want. It was first started by forum member Honken, then advanced and perfected by JTR. Many others have contributed bug reports and improvements.

The stack has these features:

  • USB CDC (virtual serial) support
  • USB HID support
  • Double buffered
  • Interrupt or polling driven

This page documents key parts of the stack. A USB CDC (virtual serial port) demo is included, it will run on most USB-enabled projects from Dangerous Prototypes.

Tested chips (many more will work):

  • 18F2550 (USB IR Toy, USB and Serial LCD backpack)
  • 18F24J50 (Logic Sniffer, Logic Shrimp, POV Toy)
  • 24FJ256GB (Bus Pirate v4)

The stack is written and compiled with Microchip's C18 and C30 compilers and MPLAB IDE. More about using Microchip tools here.


File structure

Common USB files are located in \dp_usb\ so they can be shared by many projects. To avoid dependency problem we highly recommend you start with this setup and make changes later.

Common USB files

  • \dp_usb\cdc.c - USB CDC (virtual serial) driver functions
  • \dp_usb\cdc.h - USB CDC (virtual serial) driver function declarations, and CDC codes definitions
  • \dp_usb\picusb.h - common definitions used by the stack for PIC 18F and PIC 24F processor families
  • \dp_usb\usb_stack.c - USB stack functions
  • \dp_usb\usb_stack.h - USB stack function declarations
  • \dp_usb\usb_stack_globals.h - header file that includes all the files needed for the stack into your project

These files can be shared by many projects. All configuration and customization is done in the project USB files listed below.

Generally only usb_stack_globals.h needs to be included in the project for USB to work. It includes all the other files as needed.

Project USB files

  • \echo\prj_usb_config.h - Project USB configuration. USB PID, VID, device number. Hardware setup for the board you'll use. Interrupt/polling setup
  • \echo\descriptors.h - USB descriptors, including the manufacturer and device name strings
  • \echo\configwords.h - Board or chip specific configuration words go here, the oscillator speed settings are particularly important for USB

These project specific USB files should be in the project directory. That's \echo\ for the echo demo. All customization happens here, don't touch the \dp_usb\ files.

USB setup

Here are the main USB configuration options and where to find them.


#define USB_VID (0x4d8)
#define USB_PID (0x000a)  // Microchip CDC 
#define USB_DEV 0x0002

Every USB device must have a unique vendor ID and product ID. Enter them at the top of prj_usb_config.h. These should be unique to your device, a custom PID for an initial production of 10,000 units is available free from Microchip for use with PIC microcontrollers. Microchip's CDC demo PID is shown here as an example, it cannot be used in a released firmware.

Manufacturer and device names

ROM const unsigned char cdc_str_descs[] = {       
/* 0 */                  4, USB_STRING_DESCRIPTOR_TYPE, LOWB(USB_LANGID_English_United_States), HIGHB(USB_LANGID_English_United_States),       
/* USB_iManufacturer */ 42, USB_STRING_DESCRIPTOR_TYPE, 'D',0,'a',0,'n',0,'g',0,'e',0,'r',0,'o',0,'u',0,'s',0,' ',0,'P',0,'r',0,'o',0,'t',0,'o',0,'t',0,'y',0,'p',0,'e',0,'s',0,       
/* USB_iProduct */      18, USB_STRING_DESCRIPTOR_TYPE, 'C',0,'D',0,'C',0,' ',0,'T',0,'e',0,'s',0,'t',0,       
/* USB_iSerialNum */    18, USB_STRING_DESCRIPTOR_TYPE, '0',0,'0',0,'0',0,'0',0,'0',0,'0',0,'0',0,'1',0 };

The manufacturer and device name are shown when the USB device attaches to the computer. Set a custom string at the bottom of descriptors.h


Several functions must be called periodically for the stack to stay connected to the PC. These can be called inside your main program loop (polling) or driven by interrupts in the background. We highly recommend the interrupt method, though it is slightly more complicated.

 #define USB_INTERRUPTS //use interrupts instead of polling

Enable this option in prj_usb_config.h for Interrupts, comment it out (//) for polling mode.

//insert project specific code here

In polling mode you have to regularly call the usb_handler() function in your code. Your code usually must be written in a giant loop and state machine so that it can service the usb_handler() function every few milliseconds. See the echo demo for examples.


Interrupt driven USB responds to activity in the background as needed, so you're free to write code however you want. It's slightly more difficult to get going, but they work a lot smoother and don't impact chip performance as much.


EnableUsbPerifInterrupts(USB_TRN + USB_SOF + USB_UERR + USB_URST);
EnableUsbGlobalInterrupt(); // Only enables global USB interrupt.

These two functions setup the PIC24F USB interrupts. Call them after initializing and starting the USB stack (see the echo demo for a complete example).

#pragma interrupt _USB1Interrupt
void __attribute__((interrupt, auto_psv)) _USB1Interrupt() {
    //USB interrupt
    //IRQ enable IEC5bits.USB1IE
    //IRQ flag    IFS5bits.USB1IF
    //IRQ priority IPC21<10:8>

This is an example USB interrupt service routine for the PIC 24FJ256GB from the echo demo. It calls the same usb_handler() function the polling method does, then clears the USB interrupt flags.

Polling and interrupt methods both call usb_handler(), but interrupt driven communication is much more efficient. The usb_handler() function is called only when there is a USB event, instead of being called all the time.


RCONbits.IPEN = 1;          // Enable priority levels on interrupts
IPR2bits.USBIP = 1; //configure USB interrupts for high or low priority

PIC 18Fs have the option of high or low priority interrupts. This needs to be configured manually, the stack leaves this step to you.

RCONbits.IPEN enables (1) interrupt levels. IPR2bits.USBIP sets the USB interrupt priority to high (1) or low (0).

EnableUsbPerifInterrupts(USB_TRN + USB_SOF + USB_UERR + USB_URST);
INTCONbits.PEIE = 1; //peripheral interrupt enable
INTCONbits.GIE = 1;  //global interrupt enable
EnableUsbGlobalInterrupt(); // Only enables global USB interrupt.

These lines setup the PIC 18F USB interrupts, and enable the PIC 18 interrupt system. Call them after initializing and starting the USB stack (see the echo demo for a complete example).

//High level and legacy mode interrupt
#pragma interrupt InterruptHandlerHigh
void High_ISR(void) {

If you configured USB interrupts for high priority (IPR2bits.USBIP = 1), or disabled the interrupt priority feature (RCONbits.IPEN = 1), then USB interrupts are handled here. Add this function to the end of main.c.

//USB stack on low priority interrupts,
#pragma interruptlow InterruptHandlerLow
void Low_ISR(void) {   

If you configured USB interrupts for low priority (IPR2bits.USBIP = 0), then USB interrupts are handled here. Add this function to the end of main.c.

Sending and receiving data

Here are 3 examples of sending and receiving data. All of them use double buffer mode, and all the functions are located in the cdc.c file.

Note that the echo demo increments the letter by +1
Remove +1 before using it in your applicaton

Method 1

if (poll_getc_cdc(&RecvdByte))

The function poll_getc_cdc returns the number of characters in the buffer, and moves the next character to the RecvdByte variable.

The putc_cdc function moves the RecvdByte variable to the out buffer.

Note that the echo demo increments the letter by +1, remove before using in your project.

Method 2

if (peek_getc_cdc(&RecvdByte)) {
  RecvdByte = getc_cdc();

This method first checks if there is a character waiting with the peek_getc_cdc function. If a character is available, getc_cdc is used to retrieve it.

Note that the echo demo increments the letter by +1, remove before using in your project.

Method 3

if (poll_getc_cdc(&RecvdByte)) {
  putc_cdc(RecvdByte+1); //

This method is similar to the first, but here the data is sent immediately to the PC instead of waiting for the USB stack to flush the buffer.

CDC_Flush_IN_Now function is called at the end. This sends the output buffer immediately, while the other two methods wait for the usb_handler function to be called.

Includes and setup

This section gives some details on the required files, settings, and functions for the USB stack. Your best bet though is to look at main.c in the echo demo for a working example.

//USB stack
#include "..\dp_usb\usb_stack_globals.h"   // USB stack only defines 
#include "descriptors.h" // JTR Only included in main.c

You need to include two USB files at the very top of the main.c file of your project, and anywhere else you use USB functions. usb_stack_globals.h brings in all the common USB stack goodness in a single include.

void USBSuspend(void);
void USBSuspend(void){}

USB suspend function. Experimental. Declare it because the stack expects it, but we leave it empty.

extern BYTE usb_device_state;

This external variable included with usb_stack_globals.h holds the current USB device state. It's needed to figure out when the USB connection is properly enumerated.

initCDC(); // setup the CDC state machine   
usb_init(cdc_device_descriptor, cdc_config_descriptor, cdc_str_descs, USB_NUM_STRINGS); // initialize USB
usb_start(); //start the USB peripheral

A few setup functions need to be called during chip initialization.

  • First initialize the CDC by calling the initCDC function
  • Then setup the usb by calling the usb_init function and passing the descriptor variables
  • Finally, run the usb_start function to start USB
// Wait for USB to connect   
do {       
usb_handler(); //only needed for polling!!   
} while (usb_device_state < CONFIGURED_STATE);    
usb_register_sof_handler(CDCFlushOnTimeout); // Register our CDC timeout handler after device configured

This loop polls the usb_device_state variable waiting for the USB connection to enumerate. When it reaches CONFIGURED_STATE the loop stops and the CDC handler is started.

That's it, now add code that sends and receives USB using one of the three methods above. If polling, you'll also need to call usb_handler() once in a while.

Adding a new board

These step outline how to add new custom hardware to the echo demo.

CONFIG words

You'll need to setup CONFIG bits for your hardware. It's especially important that you configure an external oscillator to provide the correct 48MHz clock for the USB core. This is done with a series of PLL multipliers and dividers that can be configured on the PIC.

Demos are located in configwords.h for some of our projects, like the Logic Sniffer, USB IR Toy, and Bus Pirate v4. Here are example CONFIG words for the PICs we've tested.

PIC 18F24J50

#pragma config PLLDIV = 4        
#pragma config CPUDIV = OSC1
#pragma config OSC = HSPLL

The Logic Sniffer uses the PIC 18F24J50 with a 16 MHz external quartz crystal.

The PLL multiplies a 4 MHz base signal to 96MHz, then divides by two for a 48MHz USB clock. We get the base frequency by dividing the 16 MHz crystal by 4 with a PLLDIV setting of 4 (16MHz/4=4MHz). The other two settings enable the external oscillator and PLL.

PIC 18F2550

#pragma config PLLDIV   = 5        
#pragma config CPUDIV   = OSC1_PLL2  
#pragma config USBDIV   = 2        
#pragma config FOSC     = HSPLL_HS

The USB IR Toy uses the PIC18F2550 with a 20 MHz crystal. Other popular PICs in this family are the 18F2450, 18F4550, 18F4450, etc.

The PLL multiplies a 4 MHz base signal to 96MHz, then divides by two for a 48MHz USB clock. We get the base frequency by dividing the 20 MHz crystal by 5 with a PLLDIV setting of 5 (20MHz/5=4MHz). The other settings enable the external oscillator, PLL, and USB PLL divide-by-two (96MHZ/2=48MHz) options.

PIC 24F256GB


Bus Pirate V4 uses the PIC 24F256GB106 with a 12 MHz external crystal.

The PLL multiplies a 4 MHz base signal to 96MHz, then divides by two for a 48MHz USB clock. We get the base frequency by dividing the 12 MHz crystal by 3 with the PLLDIV_DIV3 config word (12MHz/3=4MHz). The other settings enable the external oscillator and PLL.

Hardware setup function

Any code needed to setup a custom board should be added to the SetupBoard function in main.c.


    #define CDC_BUFFER_SIZE 64u
    #define CLOCK_FREQ 32000000
    #define BAUDCLOCK_FREQ 16000000 //  required for baud rate calculations
    #define UART_BAUD_setup(x)  U1BRG = x
    #define CDC_FLUSH_MS 4 // how many ms timeout before cdc in to host is sent

Within the prj_usb_config.h file are hardware definitions for some of our boards. The bare minimum required for the USB stack to function are the definitions listed above. The values are set for the Bus Pirate v4 hardware, see the echo demo for other examples.

CDC_BUFFER_SIZE and CLOCK_FREQ should be set correctly, the other functions are used primarily to configure a serial UART for USB->serial converter type setups.

Linker scripts

USB memory must be correctly allocated in the linker files, especially for some PIC 18Fs. See the example linker files in the echo demo.

Many chips, especially PIC 24Fs, do not need a customized linker script to run the echo demo.