USB Communication With PIC Microcontroller CDC – XC8
Figure 1: Emulating the Serial Port Interface
The RS-232 serial interface port (COM Port) is now rarely found on a personal computer (PC), this once common interface has been virtually replaced by the Universal Serial Bus or USB for short.
Today USB has grown beyond PCs to become the common interface for many embedded industrial and consumer products like in cameras, GPS, printers, etc.
This has created a problem because many PC Software were designed to communicate with the embedded applications using the RS-232 interface as the USART is still one of the simplest serial protocol to use with any microcontroller, many devices are still using it as their main communication protocol like GSM/GPRS modules, GPS, etc.
If you need to communicate to a PC, then some sort of serial to USB converter is required.
There are many types of USB communications. The functionality of USB devices is defined by class codes, this is used to identify a device’s functionality and to nominally load a device driver based on that functionality, like Communication class, HID, image, mass storage, etc.
The Communication Device Class (CDC) supports a wide range of devices that can perform telecommunications and networking functions. Examples for communications equipment are:
- Telecommunications devices, such as analog phones and modems, ISDN terminal adapters, digital phones, as well as COM-port devices
- Networking devices, such as ADSL and cable modems, as well as Ethernet adapters and hub
A detailed description about CDC is provided by the USB Implementers Forum (USB-IF)
In this article we will discuss the USB Communication Device Class (base class 0x02) that can be used to emulate the serial port and thus no software modification on the computer, or a simpler software development because as serial port interface can be made easily with many PC software design tools like Visual Studio for example.
You can learn more about creating a PC software from this tutorial:
USB is very different to other simpler peripherals like USART or SPI for example where you could simply add the peripheral interface to your project with some few library codes or accessing the registers by reading the datasheet.
With USB it’s different, you really need to write your code off the USB framework like the TCP/IP framework.
USB is an order of magnitude more complicated than the other peripherals. It must be constantly serviced to maintain a connection to a PC.
Fortunately Microchip provides Application Libraries that one can simplify the job for us. You can use Microchip Libraries for Applications (MLA) which have sample projects for different USB classes. Under the folder:
apps –> usb –> device you can choose the sample projects with the USB class you are developing, in this case you could use the cdc_basic or cdc_serial_emulator.
To learn more about Microchip Libraries for Applications and how to copy the sample projects, please read:
Using prepared libraries make your job much easier, but you must use them carefully, not just try to bolt them onto an existing application, read the supplied documentations in the doc folder to understand the structure and different files used in the libraries.
In this tutorial we are not going to use Microchip Libraries for Applications, we will use the USB Framework Lite with MPLAB Code Configurator (MCC).
The USB Framework Lite is an MCC Application Library of the Microchip Libraries for Applications. The current version of writing this tutorial is v1.26.1 and only supports the USB CDC and Vendor classes. We are going to use it with the PIC18F45K50 which has Active Clock Tuning (ACT), feature on some newer 8-bit PIC microcontrollers like the PIC18F45K50 or the PIC16F1459 and many others that can allow us to use the USB full speed without the need of an external crystal oscillator, this can save money in BOM and extra less source of problem from the oscillator circuit components. The PIC18F45K50 is a newer model of the popular PIC18F4550.
Circuit Diagram
Figure 2: USB Communication Circuit Diagram
The above circuit diagram shows the minimum components needed, as we are using a PIC with Active Clock Tuning (ACT), no high-speed, high-accuracy external crystal and its circuity is required, we are going to use the internal oscillator.
The PIC can get power from the host PC. Please keep in mind the limits for devices drawing power from the USB. According to USB specification 2.0, this cannot exceed 100mA per low-power device or 500mA per high-power device.
A decoupling capacitor should be connected VDD and ground ( as close as possible to the PIC), a value of 0.1uF could be fine.
The PIC will utilize the on-chip 3.3V USB voltage regulator to provide power to the internal transceiver and to provide a source for the internal pull-up resistors. By using the internal components, this helps reduce the number of external components. The USB connection can be electrically detached by disabling the USB module in the firmware. By disabling the USB module in firmware (setting the USBEN bit in the UCON register to ‘0’), the on-chip USB voltage regulator will also be disabled. This simulates the physical detachment of the USB cable.
An external capacitor like a 0.47uF is required to be connected to the PIC VUSB pin for the internal voltage regulator (please read the datasheet for more info). If an external regulator is connected to VUSB, you can disable this internal regulator.
Figure 3: USB Interface On-chip components
The figure 4 below shows the USB peripheral and options
Figure 4: USB peripheral and options
Active Clock Tuning
Active Clock Tuning (ACT) is a feature on some newer 8-bit PIC microcontrollers like the PIC18F45K50, PIC16F1459, etc.
The Active Clock Tuning (ACT) module continuously adjusts the 16 MHz Internal Oscillator, using an available external reference (from the USB Host), to achieve ± 0.20% accuracy required by the USB standard. This eliminates the need for a high-speed, high-accuracy external crystal which is required for the USB full speed operation when the system has an available high-accuracy clock source such as the High Frequency Internal Oscillator (HFINTOSC).
Figure 5: Active Clock Tuning (ACT) Block Diagram
To learn more about USB communication system, please read the article:
Buy a USB PIC Microcontroller from Our Online Shop
USB Clock Settings
Start a new MPLAB X project and select the PIC18F45K50. Start the MCC to configure our peripherals.
Click on the System Module in the Project Resources. When the the PIC18F45k50 is used for USB connectivity, a 6MHz or 48MHz must be provided to the USB module for operation in either Low-Speed (1.5 Mbit/s) or Full-Speed modes (12 Mbit/s). To achieve this we will need a USB frequency of 48MHz which we can achieved by using the internal 16MHz clock with PLL (Phase-locked loop). This PIC has 3x and 4xPLL Clock multipliers as shown on the figure below. By using the internal 16MHz with 3x PLL we can raise the USB Clock frequency to 48MHz.
Figure 6: PIC18F45K50 Clock settings for 48MHz USB Full speed using MCC
The HFINTOSC is tuned using Full-speed USB events. The ACT is enabled by setting the ACTEN bit of the ACTCON register. When enabled, the ACT takes control of the OSCTUNE register. The ACT uses the
selected ACT reference clock to tune the 16 MHz Internal Oscillator to an accuracy of 16 MHz ± 0.2%.
The tuning automatically adjusts the OSCTUNE register every reference clock cycle
Figure 7: PIC18F45K50 ACTON Register
MLA USB Device Lite
Under the Device Resources, Double click the MLA USB Device Lite to add it to the Properties Resources under the Peripherals. Select it to set its Properties. This is where we’re gonna configure all the USB settings, with MCC the setup is super easy, most of the default settings can be left unchanged:
- USB Class: The first thing we are going to select CDC
- The rest of the settings can be left at their default values like: Endpoint 0 size: 8, Endpoint Buffer Mode: Full Ping Pong, Enable USB Auto Attach
- USB Tasks: The USB stack can be operated in either USB POLLING mode or USB INTERRUPT mode. In Polling mode the USBDeviceTasks() function should be called periodically to receive and transmit packets through the stack. In this example we will select the Interrupt mode.
- The Internal Pull Up Resistors are enabled.
- Vendor ID (VID) and Product ID (PID): Every USB product line must have a unique combination of VID and PID. All firmware examples use Microchip’s VID (0x04d8) and a unique PID. Prior to manufacturing and marketing a new USB product, the VID and PID need to be changed. New VID and PID numbers can be obtained by purchasing a VID from the USB Implementers Forum: http://www.usb.org/developers/vendor
- Alternatively, Microchip has a free VID sublicensing program. An application form for obtaining a PID (for use with Microchip’s VID: 0x04d8) from Microchip can be obtained through the following link:
http://www.microchip.com/usblicensing/Default.aspx
- Alternatively, Microchip has a free VID sublicensing program. An application form for obtaining a PID (for use with Microchip’s VID: 0x04d8) from Microchip can be obtained through the following link:
- Once a new VID/PID combination is obtained, both the firmware and the .INF file (when applicable) will need to be updated. To modify the VID/PID in the .INF file, open the relevant INF file and search for the “[DeviceList]” sections. There are two sections, one for 32-bit and one for 64-bit, both sections should be identical. In these sections, some text will appear with the form “USB\VID_xxxx&PID_yyyy”. Update the “xxxx” and “yyyy” sections with the new hexadecimal format VID/PID values
- The .inf file is a plain text (ex: editable with Notepad) installation instruction/information file that tells the OS what driver needs to be used for the hardware, and anything else that may need to happen during the driver installation process.
- Manufacturing String and Product String: This is the Product and Manufacturer name as it will appear in your device manager. If you change these details, modify the .inf file as well.
Figure 8: MLA USB Device Lite configuration in MCC
CDC Settings:
We will leave the default Abstract Control Management Capabilities settings.
USB Library Functions/Macros
In MCC Project Resources, click on Generate to generate all the USB files and the Main.c file.
A typical USB CDC bare code could look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | void main(void) { USBDeviceInit(); while(1) { USBDeviceTasks(); //Takes care of enumeration and other USB event if((USBGetDeviceState() < CONFIGURED_STATE) || (USBIsDeviceSuspended() == true)) { //Either the device is not configured or we are suspended // so we don't want to do execute any application code continue; //go back to the top of the while loop } else { //Keep trying to send data to the PC as required CDCTxService(); //Run application code. UserApplication(); } } } |
You can get the descriptions of these functions and more form the MLA USB Library Help document form the Microchip Libraries for Applications installation folder, or read the descriptions in functions prototypes in MCC USB header files (like the usb_device.h, usb_device_cdc.h)
USBDeviceInit():
This function initializes the device stack it in the default state. The USB module will be completely reset including all of the internal variables, registers, and interrupt flags. This function must be called before any of the other USB Device functions can be called, including USBDeviceTasks()
If using MCC, the USBDeviceInit() is called from the SYSTEM_Initialize() function which initializes also the interrupt, oscillator, pin manager also. So the first code which MCC generate by default is:
1 2 | // Initialize the device SYSTEM_Initialize(); |
USBDeviceAttach():
If using MCC, this function is called from the SYSTEM_Initialize() function, no need to do anything . This function indicates to the USB host that the USB device has been attached to the bus. This function needs to be called in order for the device to start to enumerate on the bus.
Please note this function should only be called when USB_INTERRUPT is defined. Also, should only be called from the main() loop context. With MCC, the below code will be generated for you automatically by MCC.
1 2 3 4 5 6 7 8 9 | void SYSTEM_Initialize(void) { INTERRUPT_Initialize(); PIN_MANAGER_Initialize(); OSCILLATOR_Initialize(); USBDeviceInit(); USBDeviceAttach(); } |
USBDeviceAttach()
This function is the main state machine/transaction handler of the USB device side stack. When the USB stack is operated in “USB_POLLING” mode, (usb_config.h user option) the USBDeviceTasks() function should be called periodically to receive and transmit packets through the stack. This function also takes care of control transfers associated with the USB enumeration process, and detecting various USB events (such as suspend).
This function should be called at least once every 1.8ms during the USB enumeration process. After the enumeration process is complete (which can be determined when USBGetDeviceState() returns CONFIGURED_STATE), the
USBDeviceTasks() handler may be called the faster of: either once every 9.8ms, or as often as needed to make sure that the hardware USTAT FIFO never gets full. A good rule of thumb is to call USBDeviceTasks() at a minimum rate of either the frequency that USBTransferOnePacket() gets called, or, once/1.8ms, whichever is faster. See the inline code comments near the top of usb_device.c for more details about minimum timing
requirements when calling USBDeviceTasks().
When the USB stack is operated in “USB_INTERRUPT” mode, it is not necessary to call USBDeviceTasks() from the main loop context. In the USB_INTERRUPT mode, the USBDeviceTasks() handler only needs to execute when a USB interrupt occurs, and therefore only needs to be called from the interrupt context.
USBGetDeviceState():
This function returns the current state of the device on the USB. This function is used to determine when the device is ready to communicate on the bus. Applications should not try to send or receive data until this function returns CONFIGURED_STATE.
For more information about the various device states, please refer to the USB specification section 9.1 available from www.usb.org. The table 1 below shows the USB Visible Device States:
Table 1: USB Visible Device States
USB Device States as returned by USBGetDeviceState():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | typedef enum { /* Detached is the state in which the device is not attached to the bus. When in the detached state a device should not have any pull-ups attached to either the D+ or D- line. */ DETACHED_STATE /*DOM-IGNORE-BEGIN*/ = 0x00 /*DOM-IGNORE-END*/, /* Attached is the state in which the device is attached ot the bus but the hub/port that it is attached to is not yet configured. */ ATTACHED_STATE /*DOM-IGNORE-BEGIN*/ = 0x01 /*DOM-IGNORE-END*/, /* Powered is the state in which the device is attached to the bus and the hub/port that it is attached to is configured. */ POWERED_STATE /*DOM-IGNORE-BEGIN*/ = 0x02 /*DOM-IGNORE-END*/, /* Default state is the state after the device receives a RESET command from the host. */ DEFAULT_STATE /*DOM-IGNORE-BEGIN*/ = 0x04 /*DOM-IGNORE-END*/, /* Address pending state is not an official state of the USB defined states. This state is internally used to indicate that the device has received a SET_ADDRESS command but has not received the STATUS stage of the transfer yet. The device is should not switch addresses until after the STATUS stage is complete. */ ADR_PENDING_STATE /*DOM-IGNORE-BEGIN*/ = 0x08 /*DOM-IGNORE-END*/, /* Address is the state in which the device has its own specific address on the bus. */ ADDRESS_STATE /*DOM-IGNORE-BEGIN*/ = 0x10 /*DOM-IGNORE-END*/, /* Configured is the state where the device has been fully enumerated and is operating on the bus. The device is now allowed to execute its application specific tasks. It is also allowed to increase its current consumption to the value specified in the configuration descriptor of the current configuration. */ CONFIGURED_STATE /*DOM-IGNORE-BEGIN*/ = 0x20 /*DOM-IGNORE-END*/ } USB_DEVICE_STATE; |
USBUSARTIsTxTrfReady():
This macro is used to check if the CDC class handler firmware is ready to send more data to the host over the CDC bulk IN endpoint.
A return value of true indicates that the CDC handler firmware is ready to receive new data, and it is therefore safe to
call other APIs like putrsUSBUSART() and putsUSBUSART(). A return value of false implies that the firmware is still busy sending the last data, or is otherwise not ready to process any new data at this time.
The return value of this function is only valid if the device is in a configured state (i.e. – USBDeviceGetState() returns CONFIGURED_STATE)
Remarks: Make sure the application periodically calls the CDCTxService() handler, or pending USB IN transfers will not be able to advance and complete.
Syntax:
1 | #define USBUSARTIsTxTrfReady (cdc_trf_state == CDC_TX_READY) |
Example:
1 2 3 4 | if(USBUSARTIsTxTrfReady()) { putrsUSBUSART("Hello World"); } |
CDCTxService()
This handles device-to-host transaction(s). This function should be called once per Main Program loop after the device reaches the configured state. This function is needed, in order to advance the internal software state machine that takes care of sending multiple transactions worth of IN USB data to the host, associated with CDC serial data. Failure to call CDCTxService() periodically will prevent data from being sent to the USB host, over the CDC serial data interface
getsUSBUSART():
This copies a string of BYTEs received through USB CDC Bulk OUT endpoint to a user’s specified location. It is a non-blocking function. It does not wait for data if there is no data available. Instead it returns ‘0’ to notify the caller that there is no data available.
1 2 3 4 5 6 7 8 9 | uint8_t numBytes; uint8_t buffer[64] numBytes = getsUSBUSART(buffer,sizeof(buffer)); //until the buffer is free. if(numBytes > 0) { //we received numBytes bytes of data and they are copied into // the "buffer" variable. We can do something with the data here. } |
uint8_t len : The number of BYTEs expected. Value of input argument ‘len’ should be smaller than the maximum endpoint size responsible for receiving bulk data from USB host for CDC class
putrsUSBUSART():
This writes a string of data to the USB including the null character. Use this version, ‘putrs’, to transfer data literals and data located in program memory.
1 2 3 4 | if(USBUSARTIsTxTrfReady()) { putrsUSBUSART("Hello World"); } |
This writes a string of data to the USB including the null character. Use this version, ‘puts’, to transfer data from a RAM buffer.
1 2 3 4 5 | if(USBUSARTIsTxTrfReady()) { char data[] = "Hello World"; putsUSBUSART(data); } |
This writes an array of data to the USB. Use this version, is capable of transferring 0x00 (what is typically a NULL character in any of the string transfer functions).
1 2 3 4 5 | if(USBUSARTIsTxTrfReady()) { char data[] = {0x00, 0x01, 0x02, 0x03, 0x04}; putUSBUSART(data,5); } |
Example
In this project example (circuit on figure 2), we are using the PIC18F45K50, clock configuration (figure 6) to run in Full speed mode. The USB Device Lite configuration in MCC is on figure 8, the USB Task Interrupt mode is selected.
Please make sure to enable the Global Interrupts and the Peripheral Interrupts;
Any received character will be echoed back plus 1. If we receive ‘a‘, we echo ‘b‘, ‘1‘ we echo ‘2‘ and so on. This so that the user knows that it isn’t the echo enabled on their terminal program.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | /** Generated Main Source File Company: (c) 2020 www.studentcompanion.co.za * Date: April 2020 File Name: main.c Summary: This is the main file generated using PIC10 / PIC12 / PIC16 / PIC18 MCUs Description: This header file provides implementations for driver APIs for all modules selected in the GUI. Generation Information : Product Revision : PIC10 / PIC12 / PIC16 / PIC18 MCUs - 1.80.0 Device : PIC18F45K50 XC8 Compiler : Version 2.05 C Standard : C99/C90 MCC : Version 3.95.0 MPLAB X IDE : Version 5.25 */ #include "mcc_generated_files/mcc.h" /* Main application */ static uint8_t readBuffer[64]; static uint8_t writeBuffer[64]; void UserApplication(void); //USB User Application Tasks void main(void) { // Initialize the device SYSTEM_Initialize(); // If using interrupts in PIC18 High/Low Priority Mode you need to enable the Global High and Low Interrupts // If using interrupts in PIC Mid-Range Compatibility Mode you need to enable the Global and Peripheral Interrupts // Use the following macros to: // Enable the Global Interrupts INTERRUPT_GlobalInterruptEnable(); // Disable the Global Interrupts //INTERRUPT_GlobalInterruptDisable(); // Enable the Peripheral Interrupts INTERRUPT_PeripheralInterruptEnable(); // Disable the Peripheral Interrupts //INTERRUPT_PeripheralInterruptDisable(); while (1) { UserApplication(); // Add your application code } } void UserApplication(void) { /* If the USB device isn't configured yet, we can't really do anything * else since we don't have a host to talk to. So jump back to the * top of the while loop. */ if( USBGetDeviceState() < CONFIGURED_STATE ) { return; } /* If we are currently suspended, then we need to see if we need to * issue a remote wakeup. In either case, we shouldn't process any * keyboard commands since we aren't currently communicating to the host * thus just continue back to the start of the while loop. */ if( USBIsDeviceSuspended()== true ) { return; } /* Make sure that the CDC driver is ready for a transmission. */ if( USBUSARTIsTxTrfReady() == true) { uint8_t i; uint8_t numBytesRead; numBytesRead = getsUSBUSART(readBuffer, sizeof(readBuffer)); /* For every byte that was read... */ for(i=0; i<numBytesRead; i++) { switch(readBuffer[i]) { /* If we receive new line or line feed commands, just echo * them direct. */ case 0x0A: case 0x0D: writeBuffer[i] = readBuffer[i]; break; /* If we receive something else, then echo it plus one * so that if we receive 'a', we echo 'b' so that the * user knows that it isn't the echo enabled on their * terminal program. */ default: writeBuffer[i] = readBuffer[i] + 1; break; } } if(numBytesRead > 0) { /* After processing all of the received data, we need to send out * the "echo" data now. */ putUSBUSART(writeBuffer,numBytesRead); } } /* This handles device-to-host transaction(s). * Failure to call CDCTxService() periodically will prevent data from * being sent to the USB host. */ CDCTxService(); } /** End of File */ |
To find out which COM Port has been assigned to your USB device, go to Device Manager and expand the Ports (COM & LPT) to see all the COM ports on your PC. In this example the USB is on COM3
Figure 10: USB in Device Manager
Figure 11 below shows characters “a,1,2,3,4,b,c,d” echoed back to the terminal + 1.
Figure 11: Characters echoed back on Tera Term Terminal
With USB CDC, you can switch seamlessly your RS232 project to USB without ganging any code in your computer software, in the project in the link below, we will use the same PC software in Controlling a PIC Microcontroller from a PC Graphical User Interface (GUI) with USB:
Controlling a PIC Microcontroller from a PC Graphical User Interface (GUI) with USB
You can download the full project files (MPLAB X project) below here. All the files are zipped, you will need to unzip them (Download a free version of the Winzip utility to unzip files).
Download MPLAB X Project: PIC-USB-CDC