HOW-TO: Open Source USB stack for PIC microcontrollers

This guide shows how to configure and customize the latest version of the JTR-Honken open source USB stack for PIC microcontrollers. The demo will run on most USB-enabled projects from Dangerous Prototypes. Be sure to check the wiki for the latest documentation and downloads.

Many PIC microcontrollers have USB hardware. We use some of these chips in the IR Toy, Bus Pirate v4, Logic Sniffer, LCD Backpack, and more. Microchip provides free code to run the USB hardware, but it can’t be distributed with open source software. This has always been a disappointment to lots of people, and a barrier to using Microchip stuff in open source and education.

Over the last few years forum members wrote and tested an open source alternative. The stack has these features:

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

All our projects are now running on the most recent version of the USB stack, but there’s never been a simple example and complete documentation. We put together a bare-bones USB to serial echo demo to help get you started with the stack. The example has lots of comments, several pages of documentation, and it can be used as the basis for a new project. It works on many PIC 18Fs and 24Fs right out of the box.

This guide shows how to configure and customize the USB stack. The stack and example code are licensed Creative Commons Attribution, so you can use it pretty much however you want. A huge thanks to Honken, JTR, and many others who contributed bug reports and improvements.

For more details continue reading below.

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.

Downloads

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.

USB VID/PID

#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

Polling
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.

 while(1){
 usb_handler();
 //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
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.
PIC24F

 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
 usb_handler();
 ClearGlobalUsbInterruptFlag();
 }

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.

PIC18F

 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) {
 usb_handler();
 ClearGlobalUsbInterruptFlag();
 }

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) {
 usb_handler();
 ClearGlobalUsbInterruptFlag();
 }

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))
 putc_cdc(RecvdByte+1);

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();
 putc_cdc(RecvdByte+1);
 }

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); //
 CDC_Flush_In_Now();
 }

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

 _CONFIG2( IESO_OFF & FCKSM_CSDCMD & OSCIOFNC_ON & POSCMOD_HS & FNOSC_PRIPLL & PLLDIV_DIV3 & IOL1WAY_ON & PLL_96MHZ_ON & DISUVREG_OFF)
 

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”.

prj_usb_config.h

 #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.

Resources

Join the Conversation

27 Comments

  1. Nice work and thanks for the good explanation. And just when I need it as well. As I was just about to start using it in a project of mine.

  2. Will there be an explanation of the USB bootloader you are using in the Logic Shrimp as well?

    1. That’s the Diolan bootloader, they already have some examples and documentation for it. Depending on your chip, you might be able to use the compiled version without any changes.

    1. I guess the PIC32 USB peripheral is totally different and a bunch of changes would be needed.

  3. Hi!
    I tried to compile p24FJ version for PIC24FJ64GB004 without success:
    In file included from main.c:10:
    descriptors.h:114: error: ‘CDC_BUFFER_SIZE’ undeclared here (not in a function)
    main.c:14: error: syntax error before string constant
    main.c: In function ‘main’:
    main.c:32: warning: implicit declaration of function ‘SetupBoard’
    main.c: At top level:
    main.c:106: warning: conflicting types for ‘SetupBoard’
    main.c:32: warning: previous implicit declaration of ‘SetupBoard’ was here

    Language tool versions: pic30-as.exe v3.31, pic30-gcc.exe v3.31, pic30-ld.exe v3.31, pic30-ar.exe v3.31

    I see directory names in USBdemo-24FJ256GB106-BPv4.mcp are not the same as in demo.

    How about working HID USB demo for p24FJ?

    Dmitri

    1. It sounds like some project include paths may be incorrect.

      There is a HID version in the forum, not sure if it is a working demo for 24fj

    2. PIC24FJ [32] [64] GB00 [2] [4] added. Only the FJ64GB002 has been tested to work but it follows that the others will also work. There are no changes required in the code for the other devices, just add the correct linker script to the folder and change linker script name in MPLAB and then select the required device.

      By default a 4MHz crystal is assumed however for other speeds see the bottom of “configwords.h” for directions to change this. (Also applies to other 24FJ family parts, see their entry at the top of “configwords.h” for details.)

      http://www.newfoundelectronics.com/USB_stack echo test june 2012.zip

  4. Hi,

    Thanks a lot for this open source stack and for this guide, that’s really great stuff !
    I would just add to this guide a link/info about the .inf file and the fact that it must match the VID/PID used in the firmware…

    The next natural step for me is to use the USB bootloader… but the usage and the code for the BPv4 bootloader code is far from being obvious to me… : /

  5. The code is great, thank you very much :)
    I had no problems porting it to 24FJ256DA206. So far it works fine for me in MPLAB 8.86 and also in MPLAB X v.1.30. The default linker files of MPLAB seem to work ok, is there any reason to explicitly include them in the projects ?

    The next step for me is ‘void puts_cdc(char* str);’ but that’s easy to build on top of your code :)

    1. Hi Zig,

      Can’t tell you how pleased I am to see that someone has tried it out on the DA series. I never had any silicon to test with myself and the amount of hours that went into checking and checking again and again the code without any hardware was ball breaking. So glad it works. Thanks for your feedback.

      Yes, you are right, one thing missing from the API is the ability to handle strings (arrays) as easily as it does with bytes and full packets. I thought about this recently and it would be a nice addition to round out the usefulness of the API.

      As it is, the putc byte handling is so efficient speed wise a string handler simply could “wrapper” this and send the bytes one at a time and not lose all that much as opposed to directly using the InPtr and cdc_In_len to calculate available space and etc and do fancy array concatenations etc. If you just wrapper the putc function then you get ZLP and automatic flush on timeout handling and USB stack maintenance (in polled mode) for free too. ;) (Try that with the microchip stack and see what sort of problems you will get.)

      If you do want to get fancy then have a look at the irS module in the IR TOY. This handles the CDC more directly using the InPtr and cdc_In_len variables and is at least part way to building / handling arrays directly.

      Anyway, good to see that someone has used the stack in a new way and if you add code we all would love to see it.

      -Jim

      The CDC portion of the stack is very

      1. Hi Jim,

        I have just finished one project using your code (a simple wrapper for strings). It was really great to be able to concentrate on bugs in my application rather than cogs and wheels of USB comms. You took loads of worries off my head. So – thank you, JTR :)

        I am really tempted to fiddle with string handling. I hope to find some time to get deeper into it (USB interface is not simple at all!) and try it soon. I will let you know what happens. I believe your knowledge of inner workings of USB modules is far superior to mine, so please let me know if you decide to tweek your code just a little bit.

      2. JTR?
        Are you serious ? No silicon to test? Eeek :)

        If you need any proto board, with any chip on it, then contact me privately for all the details.
        I will design and make a board for you for free. Estimated time to delivery is about 2 weeks after we agree on design.

  6. Hi!
    First of all, I’m not English, so please be merciful for my grammar mistakes! :-P

    I’m trying to port this code to the XC8 compiler (for 18F2550) but I’m experiencing lots of troubles.
    The most important one is about the #pragma directives, because the “code” and “udata” pragmas aren’t known by XC8. How can I replace them?

    These two lines in “picusb.h” are producing a huge quantity of errors too:
    #define ROM far rom
    #define ROMPTR far rom
    It seems XC8 doesn’t know these commands too!!
    What the “far rom” command mean? How can I replace them?

    A. L.

  7. Hi,

    This is code I have just successfully compiled with MPLAB-X and C18 in Linux.
    Here is two tips if any one want to try it in Linux

    1. Change the #include”\” to #include”/”

    2. When you add a custom linker script C18 (mplink) can not find the *.lib files. Actually you need to change the lib file names in the standard location to lower case. I have posted a script in microchip forum

    “http://www.microchip.com/forums/tm.aspx?tree=true&m=619490”

    Hope this will help others to build it under Linux.

    I may even try to build it with SDCC+Piklab, which was my favourite choice before the MPLAB-X released.

    Thanks.

  8. Let me echo everyone’s gratitude for making this code available. I have a project on the 24FJ256DA206 as well, and just finished compiling and testing the code. It works great. I also added both a puts_cdc and gets_cdc to transfer full arrays in and out of the cdc handler, and they work well.
    One issue I have is with the data speed. The transfer peaks at about 1500 bytes per second (equivalent to 15.0kbps serial speed) from the PC. The same data transfer code using a serial port runs full speed at 115kbps, so I am pretty sure the application code is efficient enough.
    Before I delve deeper into the interrupt handler looking for inefficiencies (which I do not expect to find, btw), do you know of any hardware limitations on the PIC or inherent inefficiencies in the stack that would account for the slow speed?

    Again, many thanks for the hard work, and if I can contribute any improvements, I certainly will.

    HR

    1. Hello, HR, great to see you on the forum :)

      >The transfer peaks at about 1500 bytes per second …

      >Before I delve deeper into the interrupt handler looking for inefficiencies (which I do >not expect to find, btw), do you know of any hardware limitations on the PIC or inherent >inefficiencies in the stack that would account for the slow speed?

      I may be wrong, but I gather that you are feeding a PIC with data from computer ?
      Please tell us what do you do with that data? How do you process it?

      As far as I can see in current JTR’s code – a 64 byte endpoint buffer can be sent and/or received once per millisecond (every USB tick). That gives up to 64 K/s… so you are right, something is amiss. Maybe the application? On a PIC running at 16mips you have 250 instructions to process a single incoming or outgoing byte. So, if for example, you want to flash that data – a lot of USB packets may get NACKed while flash clears or writes.

      I think some speed improvements can be done by re-coding your most time critical data processing functions in assembler, but at the most, re-coding in assembler usually improves overall performance by 50% (maybe).

      I will run speed tests on my application (it actually goes the other way, PIC feeding data to computer :) I’ll let you know how it works here.

  9. Zig – Thanks for the reply. Your calculation is correct- those are the same numbers that I got. Account for the fact that the UOWN flag is set in application and not interrupt code, and you have to assume that the 64K/s number is not reachable, but we should come close.
    The application is a remote data logger which simply collects input from either a UART or the USB port and passes it along via a Wifi connection (using MCHP MRF24WB0MA and MCHP TCP stack). Before you point out that OTS solutions exist, let me say that our application has some unique requirements that made it beneficial to design our own solution.
    When collecting data from the PIC UART, the application keeps up with a full speed data stream at 115KBPS. The application code is pretty streamlined, and I have tweaked the TCP stack parameters for maximum efficiency for a one-way data stream.
    I am going to experiment with adding multiple USB buffers and rotating them in the USB interrupt code. The application can then process them more efficiently. I will post updates, and if it works, perhaps someone else can benefit from it.

  10. I could get the code compile successfully using MPLAB X IDE and using C18 compiler and programmed on 18F4550 ….

    BUT, nothing seems working at all …

    Note: I had to remove the file rm18f2550 – HID Bootload.lkr from the project source files

  11. Does anyone know of a HID USB package like this written in ASM (assembly)? That’s all I use for PIC programming. Thanks in advance for any help in finding it. :)

      1. Thanks for the Diolan info, however, I am trying to avoid using a bootloader if at all possible. I’m just looking for an HID stack, I guess, preferably open source. I do appreciate the reply.

  12. hy, i’m using pic 18F4550 and usb , i’canot programming in mikro c , please give me a code of programming usb in mikro c

Leave a comment

Your email address will not be published. Required fields are marked *

Notify me of followup comments via e-mail. You can also subscribe without commenting.