Writing Code with the C18 Compiler

From Mech
Jump to navigationJump to search

Code Structure

A basic PIC program usually has a structure similar to the following:

#include <p18xxxxx.h>          //include header for PIC
#include ...                   //include any other .h files needed for the project.

#pragma config OSC=IRCIO, ...  //set configuration bits

int someGlobalInteger;         //define global variables
char someGlobalCharacter; 

int someSubFunction(int someInput){  //define functions
   return someValue;

void main( void ){          //Code entry point
   OSCCON = 0b11111111;     //Configure oscillator, if needed
   TRISA  = 0b11110000;     //Configure ports as input or output
   TRISB  = 0b00000000;     //Configure unused ports as output to prevent floating inputs
   ANSEL0 = 0b00000000;     //Set input ports as analog or digital inputs
   ANSEL1 = 0b00000000;
   LATA   = 0b00000000;     //Set initial conditions for output ports
   LATB   = 0b11111111;
   ...                      //Configure other peripherals like PWM, QEI, interrupts, etc.

   while(1){                //Main program loop.  PIC will execute loop until reset.
      ...                   //Put code here.

Setting Up the Project

If you are using the ICD2 programmer, be sure to follow the installation instructions carefully, or Windows may attempt to install the wrong drivers. If you've installed the wrong drivers, follow instructions in the user guide to remove the drivers. After installing both MPLAB and C18, create a new project with the project wizard. Follow the prompts (be sure to choose the C18 toolsuite), and you should end up with an empty project if you did not add any existing files.

We must now add a linker script to your project. Make sure View>Project is checked, and that the Files tab is selected in the Project window. Then, right-click on Linker Scripts in the project panel and click on Add Files.... The linker scripts are in folder MCC18\lkr. Find the file that corresponds to your microcontroller (p18f4431.h (or p18f4431i.h if you are using the ICD2 programmer) for the PIC18F4431 microcontroller).

We now add source and header files by right-clicking on Source Files and Header Files, and selecting Add Files.... To make a new source or header file, go to File>New, and then save the file as a .c or .h file. With the default settings, all the source and header files that you write must be in the same folder or MPLAB will give you an error message stating that it cannot find the files. You can change the environment variables by going to Project>Build Options...>Project.

C18 tutorial add files.gif

Documentation for the ICD2 Programmer

Configuration Bits

Certain configurations of the PIC, such as selecting the clock source or enabling the watchdog timer, must be set using configuration bits. In C18, this is done using the #pragma config directive. For example, for the PIC18F4431, the line

#pragma config OSC=IRC

tells the PIC to use the internal oscillator and set pin RA6 as the clock output. You can put multiple bit settings on one line, for example,

#pragma config OSC=IRC, WDTEN=OFF, PWM4MX=RB5

Be aware that the configuration bits can be different for different PICs. Check the PIC18 Configuration Settings Addendum for the different options for the configuration settings. If your part number is not listed in the document, check the Microchip website for an updated version of this document.

Timers and Counters

PICs generally have several timers or counters, each with different capabilities. For the PIC18F4431, Timer0, Timer1, and Timer5 can be used as counters as well as timers, while Timer2 can only be used as a timer. The difference between using a Timer in timer mode or counter mode is simply the source of the pulses — a timer runs off the system clock, while a counter increments when it sees a rising/falling edge on a certain pin. Timers on the PIC can only count up. Timer2 and Timer5 can generate interrupts on a period match, so are particularly suitable for implementing a real-time operating system. Read the datasheet for a detailed explanation for the operation of these timers. In the datasheet, FOSC/4 refers to the frequency of the system oscillator divided by four.

Reading and Writing to Registers

The I/O ports and hardware peripherals on the PIC are controlled by SFRs, or special function registers. The values in these registers determines the behavior of the hardware. For example, Port B is an 8 channel digital I/O port that has corresponding SFRs TRISB, PORTB, and LATB (TRISB stands for Tri-State B, PORTB stands for Port B, and LATB stands for Latch B). Each of these registers is eight bits long, and each bit corresponds to a certain pin (channel) on the PIC. In this case, the least significat bit of the register affects pin RB0, and the most significant bit affects pin RB7. To write values to SFRs, we simply treat them like a global variable. Individual bits can usually be accessed by using the convention [SFR_name]bits.[bit_name], for example, PORTBbits.RB0. The names of the SFRs and bits can be found in the datasheet and the header file for the PIC (e.g. p18f4431.h).

TRISB determines whether the pins on Port B are configured as inputs or outputs. If we wanted RB0, RB1, and RB5 to be inputs and the rest to be outputs, we would write:

TRISB=0b00100011; //a "0b" prefix denotes that the following is a binary number

Of course, we could have used a hexademical or decimal number instead.

If we then wanted to set all the output ports high, we could write:


LATB will not affect the pins designated as inputs.

If we wanted to read the state of pin RB0 and store the result into a variable named RB0_Status, then we would write:


Notice that when we read an input, we used PORTB, and when we wrote to an output, we used LATB. LATB will give us the values that we want the output pins to be, while PORTB will give us the actual state of the pin. Writing to PORTB will usually do the same thing, but could also lead to a Read-Modify-Write problem that could distort the states of the pins.

Math with Different Data Types

When performing match operations, the compiler will perform the operation at the level of the longest operand, for example, if you are adding a char and an int, the result will be an int. However, if you are adding two char variables, then the result will be a char.

unsigned int someInteger;
unsigned char byte1 = 100;
unsigned char byte2 = 200;

someInteger = byte1 + byte2;

In the piece of code above, the PIC will first add byte1 and byte2 and then assign the result to someInteger. The unsigned char data type has a maximum value of 255, so the result of the addition will wrap back to zero and the add upwards again. Since the wrap-around occurs at 256, the final result will be 300-256=44, even though someInteger is perfectly capable of holding the number 300. To work around this, we can revise our code like this:

unsigned int someInteger;
unsigned char byte1 = 100;
unsigned char byte2 = 200;

someInteger = (unsigned int)byte1 + (unsigned int)byte2;

The (unsigned int) keywords will cast byte1 and byte2 as unsigned integers before performing the operations.


Since a microcontroller can count from 0 to 65535 (sixteen bits) quite quickly, prescalers and postscalers can be used to slow down the timer or counter. A 1:8 prescaler will count once for every eight counts it receives. In essence, the scaler will decrease the frequency of the timer or counter by some factor.

Reading and Writing 16-bit registers

The registers that hold the value of the 16-bit timers are split up into two 8-bit registers (e.g. TMR0H and TMR0L). TMR0H is not the actual counter register, but a buffer. Each time TMR0L is read, TMR0H is updated with the contents of the actual high byte of the timer register. Therefore, you must read TMR0L before you read TMR0H. This method prevents you from reading an erroneous vale due to changes in TMR0H while you are reading TMR0L.

When writing to a 16-bit register, you must write the high byte first, and both bytes will be loaded into the register simultaneously when you write the low byte.

(A full explanation can be found in Section 11.4 of the datahseet.)


(Beware that equations 17.1, 17.2, and 17.3 in the PIC18F4431 datasheet are wrong. See the errata for the correct equations.)

The PIC18F4431 has four Power Control PWM modules, as well as two CCP (Capture/Compare/PWM modules). This section will discuss the Power Control PWM modules (see section 17 in the datasheet). These PWM modules have the same frequency, but each can have its own duty cycle. The core of the PWM module is a 12-bit timer, whose count is kept by the eight bits of SFR PTMRL and the four LSBs of PTMRH. This timer will increment once every four clock cycles if the prescale is 1:1 (prescaling is determined by bits 3-2 of PTCON0). Another 12-bit SFR, PTPER (which is split into PTPERH and PTPERL), determines the period of the PWM. Each time PTMER reaches the same value as PTPER, it will reset to zero and start counting again if you used the default free-running count mode. (You can also make it start counting downwards; if you use the up-down counting mode; see the datasheet for more information on this.) PTMR and PTPER is shared all four PWM modules.

Each of the four PWM modules has a 14-bit register PDC (PDC0L and PDC0H for PWM module 0) which specifies the duty cycles for that module. The output of the PWM module is set high when PTMR is less than the most significant 12 bits of PDC, and the output is set low when PTMR is greater than the most significant 12 bits of PDC but less than PTPER. When PTMR is equal to PTPER, PTMR is reset to zero and the PWM module's output is set high again. For example, if the most significant 12 bits of PDC have a value of 50 (decimal representation) and PTPER has a value of 80, the PWM module's output will be high while PTMR is less than 50, and will be set low when PTMR is equal to 50. Once PTMR reaches 80, it will be reset to zero and the output is set high again. When the prescaler is 1:1, then the 2 least significant bits of PDC come into play. These two bits give you another two bits of resolution by allowing you to specify which of the four clock cycles you want the output to go low on (remember, PTMR only increments every four clock cycles). Using the same numbers as in the previous example, the last two bits essentially let you set PDC to be 50.0, 50.25, 50.5, or 50.75. If the prescaler is not 1:1, then these two bits are ignored.

Quadrature Encoder Interface

Some PICs, such as the PIC18F4431, have a built-in quadrature encoder interface. The PIC reads the two channels from the encoder, and will increment or decrement a counter, depending on which way the encoder is spinning. This value of this counter is read in the exact same way one would read any other counter or timer.

You can configure the QEI as 2x mode or 4x mode. In 2x mode, the counter will increment or decrement each time it sees a rising edge either one of the channels. In 4x mode, the counter will increment or decrement on both rising and falling edges.

Analog-Digital Converter

The Analog-Digital Converter (ADC) on the PIC takes an analog signal and converts it to a digital number (usually a 10-bit number for PIC ADCs). This section will cover the basics for PIC18 A-D conversions, but there are many important notes in the datasheet so be sure to read it. (Note: The ADC in this section refers to the one on the PIC18F4431. The one on your PIC may or may not be different.

To set up the ADC, first set the pin as an analog input with the corresponding TRIS register and ANSEL registers. The PIC may also have VREF+ and VREF- pins. These pins allow you to give the PIC an external reference that corresponds to the upper and lower bounds of the analog signal. If you choose not to use these references, you can simply use AVDD and AVSS as references.

PIC18 ADCs can operate in either single-shot or continuous-loop modes. Single-shot mode will perform a single conversion, while continuous-loop mode will sample and convert the pin or pins continually and write the result to a buffer. When sampling multiple channels, you can choose whether you want to sample them all simultaneously and then convert the results sequentially, or sample and convert each channel by itself sequentially. The 10-bit result is held in the SFRs ADRESH and ADRESL. Since ADRESH and ADRESL are together 16-bits, you must also choose whether you want the result to be right-justified or left-justified. The unused bits will be padded with zeros.

Performing A-D conversions takes a long time compared to executing other instructions, so it is possible to make the PIC do something else while waiting for the PIC to finish the conversion. However, if you use the continuous-loop mode, you must make sure that you pull the data out of the buffer quickly enough, or data will be lost.

The PIC samples the analog signal by charging a capacitor for a certain time (of which the length you set manually), then disconnecting the capacitor from the input before performing the conversion. This prevents a changing input from corrupting the result. Since the capacitor takes time to charge, you must make sure the delay is long enough for the capacitor to charge all the way to the input level (a equation for calculating the required time can be found in the datasheet). If you wish, you can choose to let the PIC automatically insert one of several pre-set delays; the PIC will insert this delay after you trigger the conversion by setting the GO/DONE bit and before the PIC actually starts the conversion process. If you use the continuous-loop mode, you must set this delay because the PIC will continuously sample and perform conversions after it is started.

You must manually configure the clock source for the ADC, which is either the internal RC oscillator or a scaled version of your main clock source. The basic time unit for the ADC is TAD, which is the time you will allow for the ADC to convert 1 out of the 10 bits. TAD can be set to one of several multiples of the ADC clock source's period (denoted by TOSC in the datasheet). The datasheet will also tell you the minimum value for TAD. For the PIC18F4431, it is about 416 ns. You want to set TAD as small as possible while still being larger than the minimum time. This allows your PIC to perform the conversion as quickly as possible, which minimizes the distortion due to the capacitor discharging through leakage currents.

You start the sampling process by selecting your input pins and setting the ADON bit in ADCON0. After sampling, you start the conversion process by setting the GO/DONE bit in the ADCON0 register. The GO/DONE bit can be set either manually in software or by one of several hardware triggers. In single-shot mode, the GO/DONE bit will be reset to 0 after the conversion.


The Universal Synchronous Asynchronous Receiver Transmitter module is a serial communications module that is typically used to communicate RS-232 or RS-485 devices. The C18's standard library has several functions that can make it easier to set up serial communications; see the C18 Compiler Libraries documentation for more information.

Baud Rates

The baud rate determines how fast the device will communicate. The baud rates for both transmitting and receiving devices must be matched. Section 19.2 in the PIC18F4431 data sheet has more information on how to configure the baud rate generator.