Controlling a PIC Microcontroller from a PC Graphical User Interface (GUI) through USB HID
Figure 1: Controlling a PIC Microcontroller from a PC Graphical User Interface diagram
A Graphical User Interface is a man-machine interface device, in which objects to handle are drawn as icons on the screen, so the user can send controls by a pointing device, usually a mouse or a keyboard.
It is always easy and requires less skills to operate a device from a visual representations by simply clicking a mouse or using a keyboard rather than a command line. The GUI can also be used to interface with other external devices located in different places.
There are a lot of different kinds of software which can be used to design a GUI platform, the choice will usually depend on personal preferences, software capabilities and the operating systems (Windows, Linux, Mac…).
Among the popular ones we have Microsoft Visual studio with its popular programming languages visual basic and C#, there is also Labview, Python, Matlab and many more.
In the project: Controlling a PIC Microcontroller from a PC Graphical User Interface (GUI) through USB, we designed a Graphical User Interface (GUI) software using Microsoft Visual C# to control the LEDs connected to the PIC microcontroller. This software could be installed in any computer running windows operating systems. The computer connects to the microcontroller using a USB cable. The PIC microcontroller will receive commands from the computer to control devices connected to it such as motors, LEDs etc.
In this project, we emulated the serial port interface by using the USB CDC class, in this way the same PC GUI software can be used wither with the old RS232 serial connection (COM Port) or with USB cable.
One of the drawbacks of this method, is that the software is not plug and play as it always expected with USB, you plug a device and it’s immediately recognize by the computer operating system, it loads the drivers automatically and you can start using the device immediately without hassle or any setting required.
With the serial port, the USB device will emulate a COMP port, in the software, you will have to know which COMP port number it is to select it, click on Connect before communication between the PC and the microcontroller can be established. If a connection is lost during transfer of data, the same process has to be repeated.
With USB HID (Human Interface Device) usually used for interface devices like mice and keypads, it’s plug and play, you plug a USB mouse and the computer will detect it and you can start using it with zero configuration, no need to install any drivers, the operating system will detect it.
You can also use the USB HID class to transfer custom application data, in this project, we are going to control again three LEDs connected to the PIC microcontroller.
Once the application is started, it will automatically detect the USB device and display a status message in red if the correct USB device is not found as shown on figure 2 below.
Figure 2: Microcontroller not detected
To learn more, please read these article first:
USB Communication With PIC Microcontroller HID – XC8
Controlling a PIC Microcontroller from a PC Graphical User Interface (GUI) through USB
Buy a USB PIC Microcontroller from Our Online Shop
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 project, we will modify the hid_custom project. The GUI software, we also modify the sample supplied plug_and_play_example in hid_custom –> utilities. So all credits to Microchip Technology. We used Visual Studio 2019, but almost any version could also be used, note that we used .NET Framework and not .NET Core project.
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.
You can learn how to create a PC software with Microsoft Visual Studio from this tutorial:
PIC Microcontroller Project
We are using the PIC18F4550, but almost any other USB PIC could be used in this project. Three LEDs are connected to PORTD, Red LED on RD1, Yellow LED on RD2 and Green LED on RD3. Another STATUS LED is connected to RD0 to indicate the status of the USB communication when used in interrupt mode.
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 | MAIN_RETURN main(void) { SYSTEM_Initialize(SYSTEM_STATE_USB_START); USBDeviceInit(); USBDeviceAttach(); while(1) { SYSTEM_Tasks(); #if defined(USB_POLLING) // Interrupt or polling method. If using polling, must call // this function periodically. This function will take care // of processing and responding to SETUP transactions // (such as during the enumeration process when you first // plug in). USB hosts require that USB devices should accept // and process SETUP packets in a timely fashion. Therefore, // when using polling, this function should be called // regularly (such as once every 1.8ms or faster** [see // inline code comments in usb_device.c for explanation when // "or faster" applies]) In most cases, the USBDeviceTasks() // function does not take very long to execute (ex: <100 // instruction cycles) before it returns. USBDeviceTasks(); #endif //Application specific tasks APP_DeviceCustomHIDTasks(); }//end while }//end main |
In the Main code, we are calling the function: APP_DeviceCustomHIDTasks(); which has our specific tasks to control the LEDs in the app_device_custom_hid.c file.
When the user Click on one of the three buttons on the GUI, the application will send a command data to the PIC, the PIC will detect which command was sent and toggle the appropriate LED.
1 2 3 4 5 6 7 8 | /** DEFINITIONS ****************************************************/ typedef enum { COMMAND_TOGGLE_LED_RED = 0x80, COMMAND_TOGGLE_LED_YELLOW = 0x81, COMMAND_TOGGLE_LED_GREEN = 0x82, COMMAND_SWITCH_OFF_LEDs = 0x83 } CUSTOM_HID_DEMO_COMMANDS; |
We have 4 commands, when the PIC receives 0x80, it will toggle the RED LED, 0x81 the Yellow LED, 0x82 the Green LED and 0x83 to switch OFF all LEDs. This last command is sent when the GUI application closes so that we can switch OFF all LEDs on exist.
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 133 134 135 136 137 | /** INCLUDES *******************************************************/ #include "usb.h" #include "usb_device_hid.h" #include <string.h> #include "system.h" /** VARIABLES ******************************************************/ /* Some processors have a limited range of RAM addresses where the USB module * is able to access. The following section is for those devices. This section * assigns the buffers that need to be used by the USB module into those * specific areas. */ #if defined(FIXED_ADDRESS_MEMORY) #if defined(COMPILER_MPLAB_C18) #pragma udata HID_CUSTOM_OUT_DATA_BUFFER = HID_CUSTOM_OUT_DATA_BUFFER_ADDRESS unsigned char ReceivedDataBuffer[64]; #pragma udata HID_CUSTOM_IN_DATA_BUFFER = HID_CUSTOM_IN_DATA_BUFFER_ADDRESS unsigned char ToSendDataBuffer[64]; #pragma udata #elif defined(__XC8) unsigned char ReceivedDataBuffer[64] HID_CUSTOM_OUT_DATA_BUFFER_ADDRESS; unsigned char ToSendDataBuffer[64] HID_CUSTOM_IN_DATA_BUFFER_ADDRESS; #endif #else unsigned char ReceivedDataBuffer[64]; unsigned char ToSendDataBuffer[64]; #endif volatile USB_HANDLE USBOutHandle; volatile USB_HANDLE USBInHandle; /** DEFINITIONS ****************************************************/ typedef enum { COMMAND_TOGGLE_LED_RED = 0x80, COMMAND_TOGGLE_LED_YELLOW = 0x81, COMMAND_TOGGLE_LED_GREEN = 0x82, COMMAND_SWITCH_OFF_LEDs = 0x83 } CUSTOM_HID_DEMO_COMMANDS; /** FUNCTIONS ******************************************************/ /********************************************************************* * Function: void APP_DeviceCustomHIDInitialize(void); * * Overview: Initializes the Custom HID demo code * * PreCondition: None * * Input: None * * Output: None * ********************************************************************/ void APP_DeviceCustomHIDInitialize() { //initialize the variable holding the handle for the last // transmission USBInHandle = 0; //enable the HID endpoint USBEnableEndpoint(CUSTOM_DEVICE_HID_EP, USB_IN_ENABLED|USB_OUT_ENABLED|USB_HANDSHAKE_ENABLED|USB_DISALLOW_SETUP); //Re-arm the OUT endpoint for the next packet USBOutHandle = (volatile USB_HANDLE)HIDRxPacket(CUSTOM_DEVICE_HID_EP,(uint8_t*)&ReceivedDataBuffer[0],64); } /********************************************************************* * Function: void APP_DeviceCustomHIDTasks(void); * * Overview: Keeps the Custom HID demo running. * * PreCondition: The demo should have been initialized and started via * the APP_DeviceCustomHIDInitialize() and APP_DeviceCustomHIDStart() demos * respectively. * * Input: None * * Output: None * ********************************************************************/ void APP_DeviceCustomHIDTasks() { /* 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; } //Check if we have received an OUT data packet from the host if(HIDRxHandleBusy(USBOutHandle) == false) { //We just received a packet of data from the USB host. //Check the first uint8_t of the packet to see what command the host //application software wants us to fulfill. switch(ReceivedDataBuffer[0]) //Look at the data the host sent, to see what kind of application specific command it sent. { case COMMAND_TOGGLE_LED_RED: //Toggle the Red LED command //LED_Toggle(LED_USB_DEVICE_HID_CUSTOM); //LED_Toggle(LED_USB_DEVICE_HID_RED); LATDbits.LATD1 =~LATDbits.LATD1; break; case COMMAND_TOGGLE_LED_YELLOW: //Toggle the Yellow LED command // LED_Toggle(LED_USB_DEVICE_HID_YELLOW); LATDbits.LATD2 =~LATDbits.LATD2; break; case COMMAND_TOGGLE_LED_GREEN: //Toggle the Green LED command // LED_Toggle(LED_USB_DEVICE_HID_GREEN); LATDbits.LATD3 =~LATDbits.LATD3; break; case COMMAND_SWITCH_OFF_LEDs: //Switch OFF all LEDs command LATDbits.LATD3 =0; LATDbits.LATD2 =0; LATDbits.LATD1 =0; break; } //Re-arm the OUT endpoint, so we can receive the next OUT data packet //that the host may try to send us. USBOutHandle = HIDRxPacket(CUSTOM_DEVICE_HID_EP, (uint8_t*)&ReceivedDataBuffer[0], 64); } } |
PC GUI Software
Figure 2 above shows the GUI software displaying: USB device is not found in red when it does not detect the USB device or if the cable is disconnected.
In order for this program to “find” a USB device with a given VID and PID, both the VID and PID in the USB device descriptor (in the USB firmware on the microcontroller), as well as in this PC application source code, must match.
In the Microcontroller code in usb_descriptors.c, the VID and PID values were declared as:
1 2 | 0x04D8, // Vendor ID 0x003F, // Product ID: Custom HID device demo |
In the PC GUI software in the function: CheckIfPresentAndGetUSBDevicePath(); the VID and PID are initialized:
1 | String DeviceIDToFind = "Vid_04d8&Pid_003f"; |
Figure 3: PC GUI Software interface sending commands to PIC microcontroller
The GUI software has two more threads beside the main thread:
ReadWriteThread_DoWork
This thread does the actual USB read/write operations (but only when AttachedState == true) to the USB device.
It is generally preferable to write applications so that read and write operations are handled in a separate
thread from the main form. This makes it so that the main form can remain responsive, even if the I/O operations
take a very long time to complete.
Since this is a separate thread, this code below executes independently from the rest of the code in this application. All this thread does is read and write to the USB device. It does not update the form directly with the new information it obtains (such as information to or from the microcontroller). The information that this thread obtains is stored in atomic global variables. Form updates are handled by the FormUpdateTimer Tick event handler function.
This application sends packets to the endpoint buffer on the USB device by using the “WriteFile()” function.
This application receives packets from the endpoint buffer on the USB device by using the “ReadFile()” function.
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 | Byte[] OUTBuffer = new byte[65]; //Allocate a memory buffer equal to the OUT endpoint size + 1 Byte[] INBuffer = new byte[65]; //Allocate a memory buffer equal to the IN endpoint size + 1 uint BytesWritten = 0; //uint BytesRead = 0; while(true) { try { if (AttachedState == true) //Do not try to use the read/write handles unless the USB device is attached and ready { //Check if this thread should send a Toggle LED(s) command to the firmware. ToggleLEDsPending will get set //by the ToggleLEDs_btn click event handler function if the user presses the button on the form. if (ToggleRedLEDPending == true) { OUTBuffer[0] = 0; //The first byte is the "Report ID" and does not get sent over the USB bus. Always set = 0. OUTBuffer[1] = 0x80; //0x80 is the "Toggle Red LED" command in the firmware for (uint i = 2; i < 65; i++) //This loop is not strictly necessary. Simply initializes unused bytes to OUTBuffer[i] = 0xFF; //0xFF for lower EMI and power consumption when driving the USB cable. //Now send the packet to the USB firmware on the microcontroller WriteFile(WriteHandleToUSBDevice, OUTBuffer, 65, ref BytesWritten, IntPtr.Zero); //Blocking function, unless an "overlapped" structure is used ToggleRedLEDPending = false; } if (ToggleYellowLEDPending == true) { OUTBuffer[0] = 0; //The first byte is the "Report ID" and does not get sent over the USB bus. Always set = 0. OUTBuffer[1] = 0x81; //0x81 is the "Toggle Yellow LED" command in the firmware for (uint i = 2; i < 65; i++) //This loop is not strictly necessary. Simply initializes unused bytes to OUTBuffer[i] = 0xFF; //0xFF for lower EMI and power consumption when driving the USB cable. //Now send the packet to the USB firmware on the microcontroller WriteFile(WriteHandleToUSBDevice, OUTBuffer, 65, ref BytesWritten, IntPtr.Zero); //Blocking function, unless an "overlapped" structure is used ToggleYellowLEDPending = false; } if (ToggleGreenLEDPending == true) { OUTBuffer[0] = 0; //The first byte is the "Report ID" and does not get sent over the USB bus. Always set = 0. OUTBuffer[1] = 0x82; //0x82 is the "Toggle Green LED" command in the firmware for (uint i = 2; i < 65; i++) //This loop is not strictly necessary. Simply initializes unused bytes to OUTBuffer[i] = 0xFF; //0xFF for lower EMI and power consumption when driving the USB cable. //Now send the packet to the USB firmware on the microcontroller WriteFile(WriteHandleToUSBDevice, OUTBuffer, 65, ref BytesWritten, IntPtr.Zero); //Blocking function, unless an "overlapped" structure is used ToggleGreenLEDPending = false; } if (SwitchOffLEDPending == true) { OUTBuffer[0] = 0; //The first byte is the "Report ID" and does not get sent over the USB bus. Always set = 0. OUTBuffer[1] = 0x83; //0x83 is the "Switch OFF all LEDs" command in the firmware for (uint i = 2; i < 65; i++) //This loop is not strictly necessary. Simply initializes unused bytes to OUTBuffer[i] = 0xFF; //0xFF for lower EMI and power consumption when driving the USB cable. //Now send the packet to the USB firmware on the microcontroller WriteFile(WriteHandleToUSBDevice, OUTBuffer, 65, ref BytesWritten, IntPtr.Zero); //Blocking function, unless an "overlapped" structure is used SwitchOffLEDPending = false; } } //end of: if(AttachedState == true) else { Thread.Sleep(5); //Add a small delay. Otherwise, this while(true) loop can execute very fast and cause //high CPU utilization, with no particular benefit to the application. } } catch { //Exceptions can occur during the read or write operations. For example, //exceptions may occur if for instance the USB device is physically unplugged //from the host while the above read/write functions are executing. //Don't need to do anything special in this case. The application will automatically //re-establish communications based on the global AttachedState boolean variable used //in conjunction with the WM_DEVICECHANGE messages to dyanmically respond to Plug and Play //USB connection events. } } //end of while(true) loop |
FormUpdateTimer_Tick
This timer tick event handler function is used to update the user interface on the form, based on data obtained asynchronously by the ReadWriteThread and the WM_DEVICECHANGE event handler functions.
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 | private void FormUpdateTimer_Tick(object sender, EventArgs e) { //obtained asynchronously by the ReadWriteThread and the WM_DEVICECHANGE event handler functions. //Check if user interface on the form should be enabled or not, based on the attachment state of the USB device. if (AttachedState == true) { //Device is connected and ready to communicate, enable user interface on the form lblStatusMessage.Text = "Microcontroller Found: Attached State = TRUE"; lblStatusMessage.ForeColor = Color.LimeGreen; groupRed.Enabled = true; //enable the red LED button groupYellow.Enabled = true; //enable the Yellow LED button groupGreen.Enabled = true; //enable the Green LED button } if ((AttachedState == false) || (AttachedButBroken == true)) { //Device not available to communicate. Disable user interface on the form. lblStatusMessage.Text = "Microcontroller Not Detected: Verify Connection/Correct Firmware"; lblStatusMessage.ForeColor = Color.Red; groupRed.Enabled = false; //disable the red LED button groupYellow.Enabled = false; //disable the Yellow LED button groupGreen.Enabled = false; //disable the Green LED button greenLEDOFF.Visible = true; greenLEDON.Visible = false; yellowLEDOFF.Visible = true; yellowLEDON.Visible = false; redLEDOFF.Visible = true; redONLED.Visible = false; } } |
When the buttons are clicked, they will update the ToggleLEDPending variables which are checked in the ReadWriteThread.
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 | //Exit the application private void btnExit_Click(object sender, EventArgs e) { SwitchOffLEDPending = true; // Send command to Switch OFF all LEDs first before exiting this.Close(); } //LEDs buttons click events start here private void redONLED_Click(object sender, EventArgs e) { ToggleRedLEDPending = true; //Will get used asynchronously by the ReadWriteThread redLEDOFF.Visible = true; redONLED.Visible = false; } private void redLEDOFF_Click(object sender, EventArgs e) { ToggleRedLEDPending = true; //Will get used asynchronously by the ReadWriteThread redLEDOFF.Visible = false; redONLED.Visible = true; } private void yellowLEDOFF_Click(object sender, EventArgs e) { ToggleYellowLEDPending = true; //Will get used asynchronously by the ReadWriteThread yellowLEDOFF.Visible = false; yellowLEDON.Visible = true; } private void greenLEDOFF_Click(object sender, EventArgs e) { ToggleGreenLEDPending = true; //Will get used asynchronously by the ReadWriteThread greenLEDOFF.Visible = false; greenLEDON.Visible = true; } private void yellowLEDON_Click(object sender, EventArgs e) { ToggleYellowLEDPending = true; //Will get used asynchronously by the ReadWriteThread yellowLEDOFF.Visible = true; yellowLEDON.Visible = false; } private void greenLEDON_Click(object sender, EventArgs e) { ToggleGreenLEDPending = true; //Will get used asynchronously by the ReadWriteThread greenLEDOFF.Visible = true; greenLEDON.Visible = false; } //LEDs buttons click events end here |
USB Simulation with Proteus
You can simulate USB communication using one of the supported microcontrollers in Proteus if you don’t want to build the USB hardware. You will be able to test both the firmware and the hardware by simulating the circuit. Communication is modelled down to Windows driver level, with all requests to and replies from the simulated USB device displayed in the USB Transaction Analyser
You can simulate these USB Device classes:
- Mass Storage Device Class (MSD.)
- Human Interface Device Class (HID).
- Communications Device Class (CDC)
For this you will need the correct Proteus license to enable you to simulate USB and Install the USB Drivers.
You can download the full project files (MPLAB XC8 source code, C# Project and Proteus Simulation 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_GUI_USB_HID
Download C# GUI Project: Control PIC GUI USB HID C#
Download Proteus Simulation Project: Proteus Project Control PIC GUI USB HID