Introduction: Programming PIC Microcontrollers

PIC microcontrollers are a very useful and versatile tool for use in many electronic projects. They are very inexpensive and easy to find. They are also very powerful and many are capable of speeds up to 64 MIPS using the internal oscillator block, about 16 times faster than most comparable AVR microcontrollers. PICs are also easy to program, however getting the project set up can some times be tricky. These instructions will walk through the process of setting up the software, creating a new project, and programming some very simple functions to test the configuration and ensure everything is working. They are designed to be very open ended; after the project is created and the basics are finished the reader is encouraged to explore all the features and extras not covered in these instructions. Also you will find that these instructions will start out walking through step by step, but as the instructions near the end the reader is encouraged to explore other ways of accomplishing the tasks and make the project their own.
What you will need To build a project with a PIC microcontroller only requires a few items.
  • PIC microcontroller
    • These instructions are for programming a PIC18F series MCU, although others are similar.
    • Obtained from Microchips website.
    • Microchip allows students with valid .edu email addresses sample PIC's for free!
    • The PIC I am using to create these instructions is a PIC18F22K80
  • PICkit 3 In-Circuit Debugger
    • Available from Microchip.
    • Costs $45 for general public, and #34 with student discount if you have an .edu email address.
    • There are also other programmers which will work just as well; however, this is the best one for starting out.
  • Breadboard and breadboard wires
  • LEDs, buttons, potentiometers, or anything else you would like to connect to the PIC

Step 1: Build Hardware

Before doing any programming the first step is to build the hardware. Although the PIC18F portfolio is very large, many of the chips have several commonalities. For more detailed information see the "Guidelines for Getting Started with PIC18Fxxxx Microcontrollers" section in your devices datasheet. For detailed pin-outs of the PIC microcontroller see the "Pin Diagram" section in your devices datasheet.

Note: VDD = Positive Voltage and VSS = Ground.
  1. Connect the MCLR pin through a 1kΩ resistor to VDD.
  2. Connect a 0.1μF capacitor between every pair of adjoining VDD-VSS pairs or AVDD-AVSS pairs.
  3. Connect a 10μF capacitor between VCAP and Vss.
  4. Connect MCLR pin to pin 1 of the PICkit 3.
  5. Connect VDD to pin 2 of the PICkit 3.
  6. Connect VSS to pin 3 of the PICkit 3.
  7. Connect PGD pin to pin 4 of the PICkit 3.
  8. Connect PGC pin to pin 5 of the PICkit 3.
  9. Leave pin 6 of the PICkit 3 unconnected.
  10. Connect any analog inputs to pins with ANx functionality where x is a number.
  11. Connect any digital inputs or outputs to pins with Rxy functionality where x is a letter identifying the port, and y is a number identifying the bit.
For my example I have an LED connected between RA0 and ground, the wiper of a potentiometer connected to AN1, and a DPST switch connected to RA2. You may find it easier to program the PIC if you have sketched down a schematic of your circuit.

Step 2: Get Software

These instructions will use XC8 compiler and MPLAB X IDE by Microchip. This step will explain how to get these tools and ensure they have been installed correctly.
  1. To get the latest version of the software visit Microchips website at http://www.microchip.com/pagehandler/en-us/family/mplabx/
  2. Select the software for your OS and follow the standard installation instructions.
Note: If you are using Windows 8 you may need to run the installers in compatibility mode for Windows 7.
  1. Once the software is installed, start MPLAB X
  2. In the menu bar select Tools->Options
  3. In the Options dialog select the Embedded tab and ensure XC8 is listed in the Toolchain list.
  4. If it is listed select OK and move on the next step.
  5. If it is not listed ensure that instillation has completed, and click Scan for Build Tools button.
  6. If still not listed, look on Microchips forum for help with your specific problem.

Step 3: Create New Project

In this step we will create a new project based on a template from Microchip.
  1. On the menu bar select File->New Project...
  2. In the new file dialog box expand Samples and select Microchip Embedded
  3. In the project box select PIC18 C Template
  4. Select Next
  5. Give the project any name you like
  6. Choose a location to save the project to in the Project Location box
  7. Leave the Project Folder as default options
  8. Check "Set as Main Project" box
  9. Select Finish
The project will now show up in Project Explore on the left hand side of the screen.

Step 4: Build Parameters

Before we can get started programming we need to set the build parameters.
Create Configuration
  1. Right click on the project name in the projects tool bar.
  2. In the Project Properties dialog select Manage Configurations...
  3. In the Configurations dialog select New
  4. In the New Configuration Name dialog enter Default and click OK
  5. In the Configurations dialog make sure Default is selected and click Set Active
  6. Click OK in the Configurations dialog
Set Configuration Properties
  1. In the Project Properties dialog select "Conf: [Default]" in the Categories list
    1. In the Device box type the name of the device you are using. In my case PIC18F26K80
    2. In the Hardware Tools list select PICkit3
    3. In the Compiler Toolchain select XC8 (v...) Where ... is the version you have installed.
    4. Select Apply
  2. Under Conf: [Default] select PICkit 3
    1. For Option categories select Power
    2. Check "Power target circuit from PICkit3
    3. Select Apply.
  3. Under Conf: [Default] select XC8 compiler
    1. For Option categories select Optimizations
    2. Set "Optimization Set" to "none"
    3. Select Apply
  4. Click OK to close the dialog box
Test the Configuration To test the configuration click the clean and build button (the one with the hammer and broom). Text will start scrolling in the output window at the bottom of the page. If everything is successful the this text will say BUILD SUCCESSFUL (total time: ...). If you get an error, go back through this step making sure that you did not miss anything, and that everything was applied.

Step 5: Set Configuration Bits

The next step is setting the configuration bits. The configuration bits tell the MCU its initial conditions for when it turns on. They are used to set the clock source and speed, watchdog time configuration, and other similar features. Configuration bits are device dependent, so check the data sheet for the chip you are using for more information.
  1. In the project explorer expand Source Files and open configuration_bits.c
  2. Remove all the text below the #endif line
  3. Notice a new tab has opened at the bottom of the screen
  4. Set the bits as needed for your project. Since these are chip dependent, check the data sheet for more information about what each does. Some common settings follow:
    1. Extended Instruction Set -- Should be set to OFF when using template
    2. Oscillator -- Used to select the processor. Unless you are using an external crystal, leave set as Internal RC oscillator. See data sheet for other oscillator configurations. Note: CLKOUT will allow for easier debugging, and should be turned on if available.
    3. PLL Enable -- Will allow for future use of the PLL. Note: this will not turn on the PLL, it will only enable it. It is recommended to enable it.
    4. Watchdog Timer -- The watch dog timer is used to ensure the processor will not lock up. It however makes it much harder to debug. It is recommended to disable it while initially programming, and only enable it after the project is nearly done.
    5. Code/Table Write/Read protects -- Used to disable writing or reading to certain ranges of memory. Leave all of these disabled.
    6. If unsure about a setting, it is usually safe to leave it default.
  5. After all configuration bits have been set, click the "Generate Source Code to Output" button at the bottom of the panel.
  6. The panel will now switch to the Output tab. Select all the text in this tab and copy it to the clip board
  7. Paste it at the bottom of the configuration_bits.c file and pres save.
  8. Clean and build the project again by clicking the broom and hammer icon.
  9. Ensure the build was successful. Also check to make sure there was no errors in the output
If everything has worked move on to the next step. If there are errors or warnings fix them before moving on.

Step 6: Configure Oscillator

The next step is to start programming; however, before we get to the application code we must program the system code. The system code are the low level functions such as configuring the oscillator and basic delay functions.

Determining Settings

Before we can program the settings, we must choose what speed we would like to run at. For this example I will use 16MHz as most PIC's can run at this speed. For my configuration I will use the 4MHz postscaller from the HF-INTOSC, and the 4x PLL giving an output frequency of 4MHz*4x=16MHz
  1. In the datasheet find the section labeled Oscillator Configurations
  2. The first thing listed in this section is Oscillator Types. If you are using the internal oscillator then use the settings relating to INTIO1
  3. On the next page or two you will find a schematic drawing of the oscillator similar to the one shown. It is helpful to trace the signal on this drawing to ensure the correct speed is being selected.
  4. The next step is to program these settings to the MCU. This is done by setting registers. The first register to set is OSCCON.
    1. IDLEN -- used to control the action of the sleep command. Can be left as default.
    2. IRCF -- Oscillator selection. Since I am using HF-INTOSC/4 (4MHz) I will need to set this to a binary value of 101
    3. OSTS -- Read only bit
    4. HFIOFS -- Read only bit
    5. SCS -- clock select bits. Since I am using the internal oscillator, I will set to 1x where x can be 0 or 1
  5. The next register is OSCCON2; however, this register is mostly read only and not important at this point
  6. The last oscillator configuration register is OSCTUNE. We will not tune the frequency for this project, however we must use this register to turn on the PLL using the PLLEN bit.

Applying Settings

  1. Return to MPLAB
  2. In the project explorer under Source Files open system.c
  3. At the bottom of this file is the function ConfigureOscillator. Remove the comments in that function.
  4. To set the bits of a register type in all caps the register name, followed by the lowercase word bits and then a period and the bit name.
  5. To set the bits follow that with an equal sign. To use binary type 0bXXXX where XXXX is the binary number. Lastly end the line with a semi-colon.
  6. Set all the bits as determined above for the OSCCON register. Example: OSCCONbits.IRCF = 0b101;
  7. Do the same for all other needed oscillator registers. See below for an example of a finished ConfigureOscillator function.
  8. When finished build and check for warnings/errors

/** * Configure the clock source and speed */void ConfigureOscillator(void){
    OSCCONbits.IRCF     =0b101;
    OSCCONbits.SCS      =0b00;
    OSCTUNEbits.PLLEN   =0b1;}

Step 7: Wait Milli-Second Function

One of the most useful functions is wait_ms. This however is not a function in the standard library, and will need to be programmed by you. For this implementation there will be a loop which will hold the processor until the given time has passed.

PIC18F microcontrollers need 4 clock cycles to execute one line of assembly code. Therefore with a clock of 16MHz, lines will be executed at 4 million lines per second = 4000 lines per milli-second. Since a for loop will take one instruction each time for the comparison, and two for the operation one for the body of the loop, it will work perfectly. We just need the for loop to loop 1000 time per milli-second.
  1. In system.c create a new function at the bottom of the file of type void wait_ms(uint16_t time)
  2. Below is the completed function
/**
 * Wait for a given number of milli-seconds using busy waiting scheme.
 * @param time - time in ms to wait.
 */
void wait_ms(uint16_t time)
{
    static long timel = 0;
    timel = time * 1000l;
    for( ; timel; timel--);// no initial condition, while time is >0, decrement time each loop
}
  1. Open system.h in the Header Files folder in the project browser
  2. At the end add the line void wait_ms(uint16_t); to prototype the function.
  3. Change line 8 from 8000000L to 16000000L
  4. Build and check for errors/warnings

Step 8: Blink an LED

The best way to test that everything is set up correctly is to blink an LED light. If the light blinks at the expected rate then everything has been configured correctly. In this example the LED is connected to PORT A, Pin 0 (RA0 on the datasheet). If you have your LED connected to a different pin, use the appropriate registers and bits.
  1. Open main.c in the project viewer under source files.
The function void main(void) is the main entry point of the program. When the MCU first powers on it will enter into this function. The first line calls the ConfigureOscillator function you filled in to set the clock source and speed. The next line calls InitApp, a function that we will fill in shortly, and finally it enters an infinite loop. Since there is no operating system for the function to return to, there is no return call at the end.
The finished function should look like this:
  1. Immediately above the while loop add the following code.
    1. Set the LED pin as output -- TRISAbits.TRISA0 = 0; // setting a TRIS bit to 0 sets as output, setting to 1 sets as input
  2. Inside the while loop add the following code
    1. Set the LED to OFF -- LATAbits.LATA0 = 0; // the LAT bits control the output of a pin. 0 = LOW, 1 = HIGH
    2. Wait for 1/2 second -- wait_ms(500);
    3. Set the LED to ON -- LATAbits.LATA0 = 1;
    4. Wait for 1/2 second -- wait_ms(500);
void main(void)
{
    /* Configure the oscillator for the device */
    ConfigureOscillator();

    /* Initialize I/O and Peripherals for application */
    InitApp();
   
    TRISAbits.TRISA0 = 0; // set pin as output
    while(1)
    {
        LATAbits.LATA0 = 0; // set pin LOW
        wait_ms(500);       // wait 0.5 seconds
        LATAbits.LATA0 = 1; // set pin HIGH
        wait_ms(500);       // wait 0.5 seconds
    }
}
  1. Build the program and check for errors or warnings
  2. Ensure the PICkit is connected correctly to the PIC and the computer
  3. Click the make and program device button (the button to the right of the clean and build button)
  4. If prompted select PICkit 3 and click OK
  5. When the warning shows double check you have the correct PIC in the circuit and click OK
  6. If a warning shows about Target Device ID click OK to ignore it

Step 9: Reading an Analog Value

So far the program can blink an LED. Next lets give it some user input. We will use a potentiometer to create an analog signal which will change the speed of the LED. The ADC takes an analog voltage, and outputs a digital value.
  1. In the project browser open user.c under Source Files
  2. Above the InitApp function create a new function void init_adc(void)
  3. Enter the following code to initialize the ADC module
/** * Initialize the Analog to Digital Converter. */void init_adc(void){
    TRISAbits.TRISA1    =0b1;// set pin as input
    ANCON0bits.ANSEL1   =0b1;// set pin as analog
    ADCON1bits.VCFG     =0b00;// set v+ reference to Vdd
    ADCON1bits.VNCFG    =0b0;// set v- reference to GND
    ADCON1bits.CHSN     =0b000;// set negative input to GND
    ADCON2bits.ADFM     =0b1;// right justify the output
    ADCON2bits.ACQT     =0b110;// 16 TAD
    ADCON2bits.ADCS     =0b101;// use Fosc/16 for clock source
    ADCON0bits.ADON     =0b1;// turn on the ADC}
  1. Next create another function immediately after called uint16_t adc_convert(uint8_t channel)
/** * Preform an analog to digital conversion. * @param channel The ADC input channel to use. * @return The value of the conversion. */
uint16_t adc_convert(uint8_t channel){
    ADCON0bits.CHS      = channel;// select the given channel
    ADCON0bits.GO       =0b1;// start the conversionwhile(ADCON0bits.DONE);// wait for the conversion to finishreturn(ADRESH<<8)|ADRESL;// return the result}
  1. In the InitApp function add the line init_adc()
  2. In the file user.h add the prototype uint16_t adc_convert(uint8_t);
  3. Change main to match the following:
voidmain(void){
    uint16_t adc_value;// variable to hold ADC conversion result in/* Configure the oscillator for the device */
    ConfigureOscillator();/* Initialize I/O and Peripherals for application */
    InitApp();

    TRISAbits.TRISA0 =0;// set pin as outputwhile(1){
        LATAbits.LATA0 =0;// set pin LOW
        adc_value = adc_convert(1);// preform A/D conversion on channel 1
        wait_ms(adc_value>>2);// wait 0.5 seconds
        LATAbits.LATA0 =1;// set pin HIGH
        adc_value = adc_convert(1);// preform A/D conversion on channel 1
        wait_ms(adc_value>>2);// wait 0.5 seconds}}
  1. Build and download the code. As you turn the POT the speed the LED blinks should change

Step 10: Read a Digital Value

Next lets get digital input from the switch. When the switch is off we will have the program do what it has been doing all along, and when the switch is on the program will light up the LED solid until the switch is turned off again.
  1. To set a pin as an input, write a 1 to that pins TRIS register bit - TRISAbits.TRISA2 = 1;
  2. If a pin share analog features, it may be necessary to set it to digital by clearing the appropriate bit in ANCONx register
  3. When writing a value to a pin use the LAT register; however, when reading a value from a pin use the PORT register - value = PORTAbits.RA2;
  4. Change main to the following:
voidmain(void){
    uint16_t adc_value;// variable to hold ADC conversion result in/* Configure the oscillator for the device */
    ConfigureOscillator();/* Initialize I/O and Peripherals for application */
    InitApp();

    TRISAbits.TRISA0 =0;// set pin as output
    TRISAbits.TRISA2 =1;// set pin as input
    ANCON0bits.ANSEL2=0;// set pin as digitalwhile(1){if(PORTAbits.RA2)// if the pin is high{
            LATAbits.LATA0 =1;// set the pin as high}else// if the pin is low{// blink the LED
            LATAbits.LATA0 =0;// set pin LOW
            adc_value = adc_convert(1);// preform A/D conversion on channel 1
            wait_ms(adc_value>>2);// wait some time
            LATAbits.LATA0 =1;// set pin HIGH
            adc_value = adc_convert(1);// preform A/D conversion on channel 1
            wait_ms(adc_value>>2);// wait some time}}}

That is it! You now have the basic knowledge of how to set up a new project, read and write to digital pins, and how to read from analog pins. These three features will allow you to do 90% of the projects using PICs on the Internet. Also, as you continue your exploration into PIC microcontrollers you will find that most other features require very similar steps to configure peripherals, and read and right to registers.