Export page to Open Document format

PIC Programming Basics

Getting Started

This tutorial will detail the PIC Programming Basics and the first steps to programming and using a Microchip PIC18 microcontroller. While this tutorial uses the PIC18F4520, any Microchip PIC18 series microcontroller can be substituted with minimal changes. The PIC18F4520 is pin compatible with the now obsolete former flagship model, PIC18F452. Therefore, any instances of the PIC18F4520 in this tutorial can be replaced with the PIC18F452. Processor specific lines will be noted with notes on modifications required to implement the code on different processors.

Before Starting

It is important to make sure the latest software is installed on the development PC. The two software packages needed are the free MPLAB IDE and the C18 Compiler. The student version of C18 is free and will suffice for this and most following tutorials.

Hardware

At bare minimum a PIC, oscillator crystal, a couple capacitors, resistors and an LED are needed either set up on a solderless bread board or PCB with a DC power supply. The bare minimum circuit is given below.

Basic PIC Schematic

Starting with the MPLAB IDE

While it is possible to use the C18 compiler from the command line without without MPLAB IDE, however this and future tutorials will use the MPLAB IDE interface to the C18 compiler and any programmers.

Creating a Project

Project MenuThe first step to any project with the PIC in MPLAB is to create a project file. The easiest way to do this is to use the Project wizard found in the project menu. The screenshots below present a walkthrough of setting up a project with the PIC18F4520.

Project Wizard]]


{{tutorials:pic_basics_project_wizard1.png|Project Wizard Step 1

When using a different processor, select the appropriate processor model in the following step.

Project Wizard Step 2

Ensure that the Microchip C18 Toolsuite is listed as the active toolsuite.

Project Wizard Step 3

Give the project a name and specify a project directory.

By default, the Microchip Linker has a file path limit of 62 characters. Try to keep the project directory close to the root of the drive.

This is actually configurable in the project options, thought it’s not immediately obvious. Go to ProjectBuild Options…Project Under the “MPLINK Linker” tab put a check by: Suppress COD-file generation

Project Wizard Step 4

If there were any existing files needed, they could be added here.

Project Wizard Step 5

Click Finish to exit the Project Wizard.

MPLAB also creates a workspace file upon completion of the project wizard. The workspace contains all window settings, configuration bits, device settings, files and, in MPLAB 7.40 and up, symbols (variables, functions, etc.).

Adding a Source File

Add files to workspace context menu Basic MPLAB IDE Workspace After a project and workspace have been created, source code file(s) are needed. First, create a new file by either clicking the new file icon (looks like a blank page), selecting New from the File menu or using the keyboard shortcut ctrl+N. Save the file by clicking the save icon (looks like a floppy disk), selecting Save As from the File menu or using the keyboard shortcut ctrl+N. Name the file main.c.

Leave the file blank for now as the actual code will be added later in the tutorial. The source file must now be added to the project. To do this, right-click the “Source Files” folder icon in the workspace window as in the first thumbnail to the left and selecting Add Files…. Browse to the recently created file, main.c. A linker file is also needed. Repeat the process with the linker file, only this time find the appropriate linker script in the C18 linker script folder. Typically, these files are located in C:\MCC18\lkr folder. Select the 18f4520.lkr file for the PIC18F4520.

If using the PIC18F452, the appropriate file is 18f452.lkr. The naming scheme should be obvious if another processor is being used.

The Workspace window should now look like the second thumbnail on the right.

Setting Configuration Bits

Configuration Bits for the PIC18F4520 One important and often overlooked step is setting the configuration bits. If these are not properly configured the processor will operate in an unknown manner or often not start at all. The important settings are the oscillator, watchdog timer and low voltage programmer fields.

The common oscillator settings are XT, HS, EC, RC and INT. The XT and HS settings are for use with external crystal oscillators—XT for 4 MHz and below, HS for 4 MHz and above. EC is for use with an external TTL/CMOS clock source. RC oscillator mode is for using an external RC resonator (usually 8 MHz and below) and INT specifies the processors internal oscillator if equipped.

The low voltage programming field should always be disabled unless low voltage programming is required to prevent possible processor resets. The watchdog timer should also be disabled since an infinite loop will be used later in this tutorial. Otherwise, the internal microprocessor will continuously reset.

Option - An alternative to using the Configuration Bits menu is by coding the configuration bits inside the file. The following are some common settings for the configuration bits.

#pragma config OSC = HS    /* Sets the oscillator mode to HS */
#pragma config WDT = OFF   /* Turns the watchdog timer off */
#pragma config LVP = OFF   /* Turns low voltage programming off */
#pragma config DEBUG = OFF /* Compiles without extra debug code */

By placing any of these #pragma config statements after any #include statements, essentially is the same as choosing configuration bit settings from the menu. The configuration settings for any PIC18 processor can be found in the PIC18 Configuration Settings Addendum.

Setting Up a Programmer

While setting up the project, go ahead and set up the programmer. Check that the programmer is connected to the PC. From the Programmer menu select the appropriate programmer in the Select Programmer submenu. If using the PICSTART Plus, MPLAB PM3 or ProMate II enable the programmer by selecting Enable Programmer from the Programmer menu. If using the MPLAB ICD2, make sure that the ICD2 is connected to the circuit as per the ICD2 manual and select Connect from the Programmer menu. The programmer is now ready to program.

Writing the First Program

By this point, everything should be set up to start programming.

Hello World...Almost

Brian Kernighan, authored the first known “hello, world” program in 1974 for an internal memo at Bell Laboratories titled Programming in C: A Tutorial. The “hello, world” program later appeared in the book, The C Programming Language. Since then, the de facto program for learning almost any programming language has been to print to some sort of output. However, in an embedded system, having a display available and adding a communications bus or LCD adds complexity well beyond the intent of the “hello, world” program. Subsequently, a more basic output format is a simple lone LED.

Programming with C18

The C18 compiler mostly conforms to the older ANSI 89 standard C language specification. There are some differences documented in the MPLAB C18 Users Guide. An excellent starting reference can be found in the MPLAB C18 Getting Started Guide.

Include Proper Header Files

At the beginning of the file, main.c add a line to include the C18 processor definitions file. The simplest solution is to add the generic p18cxxx.h file. This file includes the appropriate processor specific file, p18f4520.h in our case. The p18f4520.h file could also be directly included in place of p18cxxx.h, but makes the code less portable between processors.

#include <p18cxxx.h>

main()

The heart of any C program is the main() function. The syntax of the main function often varies between compilers. With the C18 compiler the main function has a return type of void and a void parameter. This makes sense semantically since there is no OS to return a value to or receive a parameter from.

#include <p18cxxx.h>
 
void main(void)
{
 
}

I/O Port Configuration

Since the LED is connected to bit 0 of PORTD, PORTD must first be configured for output. Refer to the device datasheet for more information on port I/O. Configuring the port as a digital input or output is done through the TRIS registers. Each bit in the TRIS register represents its respective bit in the PORT. Setting the bit to a one sets the bit as an input and clearing the bit to a zero sets the bit as an output. To set PORTD to all digital outputs, TRISD is set to all zeros or 0x00 in hex. All or most registers are made available as mapped variables in the processor header file. Notice that before setting PORTD to an output, the register LATD is cleared to 0x00. The LAT registers latch out the value at the proceeding clock cycle after being written to. Using the LAT registers is the safest way to modify the output value of a port as it avoids the Read-Modify-Write problem.

#include <p18cxxx.h>
 
void main(void)
{
    /* Set PORT D as digital outputs */
    LATD = 0x00;
    TRISD = 0x00;
}

Important Note on Read-Modify-Write

Read Modify Write Example It is important to be careful and avoid Read-Modify-Write issues with I/O ports, especially when using interrupts. Otherwise, ports or registers may end up in an unknown state. The screenshot to the write shows an example of the consequences of a Read-Modify-Write problem in a PIC18F4550 running FreeRTOS taken using the MPLAB ICD2 debugger. Notice in the watch window that LATC and PORTC are different values. The PIC was interrupted during a port write using the PORTC register.

PORT Manipulation

There are two basic methods of controlling the output of an I/O port on the PIC microcontroller using the C18 compiler. The first is to directly address the entire port, i.e. LATD = 0x01 setting only bit 0 to a 1 or high logic value. This could also take the form of LATD |= 0x01 (or LATD &= 0xFE to clear the bit). The second method is to use the LATDbits struct in the form LATDbits.LAT0 = 1

#include <p18cxxx.h>
 
void main(void)
{
    /* Set PORT D as digital outputs */
    LATD = 0x00;
    TRISD = 0x00;
 
    /* Turn on LED, D1 */
 
    LATDbits.LATD0 = 1;
    /* PORTDbits.RD0 would have worked but is
     * inappropriate due to Read-Write-Modify
     * issues.  As such is a better practice to
     * use LATD for outputs and PORTD for inputs.
     * The same holds for all ports
     */
 
    /* Do nothing until reset */
    while(1);
}

To compile this code, select Build All from the Project menu, click the Build All icon or use the keyboard shortcut ctrl+F10. If everything compiled cleanly, the Output window should appear and contain the following or similar in the Build tab.

 Executing: "c:\mcc18\bin\mcc18.exe" -p=18F4520 "main.c" -fo="main.o" -Ou- -Ot- -Ob- -Op- -Or- -Od- -Opa-
 Executing: "c:\mcc18\bin\mplink.exe" /l"c:\mcc18\lib" "C:\mcc18\lkr\18f4520.lkr" "C:\PIC_Projects\PIC_Tutorial\main.o" /o"PIC Tutorial.cof" /M"PIC Tutorial.map"
 MPLINK 4.00, Linker
 Copyright (c) 2005 Microchip Technology Inc.
 Errors    : 0
 
 MP2COD 4.00, COFF to COD File Converter
 Copyright (c) 2005 Microchip Technology Inc.
 Errors    : 0
 
 MP2HEX 4.00, COFF to HEX File Converter
 Copyright (c) 2005 Microchip Technology Inc.
 Errors    : 0
 
 Loaded C:\PIC_Projects\PIC_Tutorial\PIC Tutorial.cof.
 BUILD SUCCEEDED: Wed Jan 31 08:31:54 2007
The PIC can now be programmed and upon power up will turn the LED on pin RD0 on. Programming the PIC varies from programmer to programmer, but typically all programming functions are handled in the Programmer menu. An erase followed by a program is almost always sufficient.

Blinking LED

To make the LED blink bit 0 of PORTD must be set high and then low for every blink. To accomplish this simply place the instruction LATDbits.LAT0 = 0; after the line turning the LED on. Also, to make the LED blink ad infinitum, place the LED code inside the while(1) loop.

while(1)
{
    /* Turn on LED, D1 */
    LATDbits.LATD0 = 1;
 
    /* Turn off LED, D1 */
    LATDbits.LATD0 = 0;
}

Notice that if this code is compiled, programmed and ran in a PIC that the LED will seem to stay on, though dimmer. This is because the “on” time is only one instruction cycle (clock speed divided by 4 in the PICs) and off time is only a couple clock cycles due to the comparison to see whether 1 is actually true (nonzero). At a 10 MHz clock speed, this means the LED is only on for 250 nanoseconds and off for 500 to 750 nanoseconds. For the human eye to be able to detect the flickering the on and off periods would have to be in the millisecond range. To actually see the blinking effect, this period would need to be several tens of milliseconds. One hundred milliseconds is still a fast blink.

Basic Method of Delays

To achieve a human viewable blink rate a delay between the on and off commands is needed as well as between the off command and the next on command. The simplest method of this is to sit in a tight loop until a predetermined number of iterations occur.

int i;
...
LATD.LAT0 = 1;
for ( i = 0; i < 32768; i++);
LATD.LAT0 = 0;
for ( i = 0; i < 32768; i++);

The C18 compiler compiles this to 20 instructions giving a 5 microseconds delay per iteration. However, at 10 MHz this only gives us 32,768 (the max positive value of an int is 32,767, but i starts at 0) iterations of 250 nanoseconds or about 163 milliseconds. Since that still is not quite enough of a delay, i can be declared as an unsigned int and doubling the number of iterations to 65,536.

unsigned int i;
...
for ( i = 0; i < 65536; i++);

This gives a 320 milliseconds delay, but if a 500 millisecond delay is needed there are a couple choices. One choice is to increase i to a long (32 bits) or even the C18 specific short long (24 bits) and increase the value of i appropriately. Another choice is to insert time taking operations into the for loop. C18 provides a function called Nop() which is directly mapped to the assembly instruction NOP. NOP stands for no operation and consumes one instruction cycle without modifying RAM or any registers other than the program counter. To achieve a 500 millisecond delay, {500 milliseconds}/({65,536 iterations}/{250 nanoseconds}) = 30.5 instructions are needed per iteration. Adding 10 Nop() instructions to the inside of the loop would give a delay of about 492 milliseconds. Using 11 Nop() instructions gives a delay of 508 milliseconds. If more accuracy is needed then 11 Nop() instructions can be used with a max i value of {500 milliseconds}/({65,536 iterations} * {31 instructions}) = 64,516 iterations giving a delay of 499.999 milliseconds. Using Nop() in this example would look like the following.

#include <p18cxxx.h>
 
void main(void)
{
 
 
    /* Set PORT D as digital outputs */
    LATD = 0x00;
    TRISD = 0x00;
 
    /* Loop forever */
    while(1)
 
    {
        /* Turn on LED, D1 */
        LATDbits.LATD0 = 1;
 
        /* Delay for 500 milliseconds */
        for ( i = 0; i < 64516; i++)
        {
            Nop();
            Nop();
            Nop();
            Nop();
            Nop();
            Nop();
            Nop();
            Nop();
            Nop();
            Nop();
            Nop();
        }
 
        /* Turn off LED, D1 */
        LATDbits.LATD0 = 0;
 
        /* Delay for 500 milliseconds */
        for ( i = 0; i < 64516; i++)
        {
            Nop();
            Nop();
            Nop();
            Nop();
            Nop();
            Nop();
            Nop();
            Nop();
            Nop();
            Nop();
            Nop();
        }
    }
}

Easier Method Using delays.h

Luckily, C18 provides a library called delays which provides several delay functions in terms of instruction cycles. To use these functions add a line that includes the delays.h header file. The functions provided are:

Function Functionality Provided Parameters Example Usage Delay in Example
Delay1TCY Delay one instruction cycle. Same as Nop(). None
Delay1TCY();
1 Instruction Cycle
Delay10TCYx Delay in multiples of 10 instruction cycles. unsigned char (8 bit value)
Delay10TCYx(10);
100 Instruction Cycles
Delay100TCYx Delay in multiples of 100 instruction cycles. unsigned char (8 bit value)
Delay100TCYx(10);
1,000 Instruction Cycles
Delay1KTCYx Delay in multiples of 1,000 instruction cycles. unsigned char (8 bit value)
Delay1KTCYx(10);
10,000 Instruction Cycles
Delay10KTCYx Delay in multiples of 10,000 instruction cycles. unsigned char (8 bit value)
Delay10TCYx(10);
100,000 Instruction Cycles

Remember, the parameter in the Delay…TCYx functions is an 8 bit number and cannot exceed 255. If a larger number is passed it will simply roll over and a Delay10TCYx(256) will be the same as a Delay10TCYx(1). To use the 500 millisecond delay in the previous example, first the total number of instruction cycles is needed. Continue to assume a 10 MHz clock with a 250 nanosecond instruction cycle time. {500 milliseconds}/{250 nanoseconds} = 2,000,000 instruction cycles are needed for our 500 millisecond delay. To achieve this delay, the largest delay function, Delay10KTCYx is needed with a value of 200 as the parameter. Plugging this into the above example cleans up the code nicely.

#include <p18cxxx.h>
#include <delays.h> /* This is needed for the delay functions */
 
void main(void)
{
    /* Set PORT D as digital outputs */
    LATD = 0x00;
    TRISD = 0x00;
 
    /* Loop forever */
    while(1)
    {
        /* Turn on LED, D1 */
        LATDbits.LATD0 = 1;
 
        /* Delay for 500 milliseconds */
        Delay10KTCYx(200);
 
        /* Turn off LED, D1 */
        LATDbits.LATD0 = 0;
 
        /* Delay for 500 milliseconds */
        Delay10KTCYx(200);
    }
}

Semantic Programming Methods

There is a renewal of the push for semantic programming happening as of late. The idea behind using semantic methods of coding is the improvement of code readability and maintainability. Using semantic variable names, functions names, etc. make it easier for an outsider to sort through the code and understand the logic and algorithms behind it. This applies to revisiting older code written by the programmer as well.

Redefine Pins

One method of maintaining semantics in embedded systems is to use compiler macros to relabel I/O pins according to their purpose in the system. In the case of the LED example, pin RD0 can be redefined as LED0.

#define LED0 LATDbits.LATD0

Now instead of having to assign LATDbits.LATD0 = 1, LED0 can be assigned the value of 1 and LATDbits.LATD0 will receive the value 1.

#include <p18cxxx.h>
#include <delays.h> /* This is needed for the delay functions */
 
/* Define LED0 as LATDbits.LAT0 */
#define LED0 LATDbits.LAT0
 
void main(void)
{
    /* Set PORT D as digital outputs */
    LATD = 0x00;
    TRISD = 0x00;
 
    /* Loop forever */
    while(1)
    {
        /* Turn on LED, D1 */
        LED0 = 1;
 
        /* Delay for 500 milliseconds */
        Delay10KTCYx(200);
 
        /* Turn off LED, D1 */
        LED0 = 0;
 
        /* Delay for 500 milliseconds */
        Delay10KTCYx(200);
    }
}

This can be taken a step further by defining constant values ON and OFF as 1 and 0, respectively

#define ON  1
#define OFF 0

Now the code becomes:

#include <p18cxxx.h>
#include <delays.h> /* This is needed for the delay functions */
 
/* Define LED0 as LATDbits.LAT0 */
#define LED0 LATDbits.LAT0
 
/* Define values for ON and OFF states */
#define ON  1
#define OFF 0
 
void main(void)
{
    /* Set PORT D as digital outputs */
    LATD = 0x00;
    TRISD = 0x00;
 
    /* Loop forever */
    while(1)
    {
        /* Turn on LED, D1 */
        LED0 = ON;
 
        /* Delay for 500 milliseconds */
        Delay10KTCYx(200);
 
        /* Turn off LED, D1 */
        LED0 = OFF;
 
        /* Delay for 500 milliseconds */
        Delay10KTCYx(200);
    }
}

Now reading through the code makes its operation obvious without knowing the schematic or that RD0 is attached to an LED.

Extending the Use of Macros

Macros can also be used to assign function like calls to simple operations. One example is to quickly define functions to turn LED0 on and off.

/* Turn LED0 on */
#define mLED0_On()         LED0 = ON;
 
/* Turn LED0 off */
#define mLED0_Off()        LED0 = OFF;

This transforms the code to:

#include <p18cxxx.h>
#include <delays.h> /* This is needed for the delay functions */
 
/* Define LED0 as LATDbits.LAT0 */
#define LED0 LATDbits.LAT0
 
/* Define values for ON and OFF states */
#define ON  1
#define OFF 0
 
/* Turn LED0 on */
#define mLED0_On()         LED0 = ON;
 
/* Turn LED0 off */
#define mLED0_Off()        LED0 = OFF;
 
void main(void)
{
    /* Set PORT D as digital outputs */
    LATD = 0x00;
    TRISD = 0x00;
 
    /* Loop forever */
    while(1)
    {
        /* Turn on LED, D1 */
        mLED0_On();
 
        /* Delay for 500 milliseconds */
        Delay10KTCYx(200);
 
        /* Turn off LED, D1 */
        mLED0_Off();
 
        /* Delay for 500 milliseconds */
        Delay10KTCYx(200);
    }
}

If a macro is created which toggles the LED the code can further be reduced to:

#include <p18cxxx.h>
#include <delays.h> /* This is needed for the delay functions */
 
/* Define LED0 as LATDbits.LAT0 */
#define LED0 LATDbits.LAT0
 
/* Define values for ON and OFF states */
#define ON  1
#define OFF 0
 
/* Turn LED0 on */
#define mLED0_On()         LED0 = ON;
 
/* Turn LED0 off */
#define mLED0_Off()        LED0 = OFF;
 
/* Toggle LED0 */
#define mLED0_Toggle()     LED0 = !LED0;
 
 
void main(void)
{
    /* Set PORT D as digital outputs */
    LATD = 0x00;
    TRISD = 0x00;
 
    /* Turn on LED, D1 */
    mLED0_On();
 
    /* Loop forever */
    while(1)
    {
        /* Delay for 500 milliseconds */
        Delay10KTCYx(200);
 
        /* Toggle LED, D1 */
        mLED0_Toggle();
    }
}

Next up: PIC Programming Basics Part 2 ==>

Further Reading

See Also

duty free alcohol airport duty free cigs uk buy duty free cuban cigars where to buy cosmetics duty free fragrances buy tobacco duty free