My Resources, PIC C18 code
This shows example programs to use with the C18 compiler. Note that
this must be used with a PIC 18 series.
Further documents may be found from the 'Docs' directory that the C18
compiler is installed in, e.g. C:\MCC\docs.
The documents may also be viewed from here and are the following:
Getting
started with MPLAB and C18
C18 User Guide
C18 Libraries
PIC18F4550 Datasheet
Presentation on introduction to PIC
Contents (on this page)
Setting the configuration
bits
Set an LED on a port to on
Flash an LED
Latches
Reading Inputs
A/D Converters
Setting interrupts
Using interrupts
to control a servo
Read and Write to EEPROM
RS232 Serial communication
Read an encoder
Pulse Width Modulation
These are set to tell which PIC is used, the PIC hardware what
oscillators to expect and if a watchdog timer is installed etc. The
include file should be changed to your particular PIC.
For now, just use the bits set below:
#include <p18F4550.h>
#pragma config FOSC = INTOSC_EC
#pragma config PWRT = OFF
#pragma config BOR = OFF
#pragma config MCLRE = ON
#pragma config PBADEN = OFF
#pragma config ICPRT = OFF
#pragma config LVP = OFF
#pragma config WDT = OFF,DEBUG=OFF
Note that if an external oscillator is installed,
the FOSC will need to be changed:
#pragma config FOSC = HS
To find out the specific configuration bits
for your PIC:
In MPLAB, select: Help-->Topics-->PIC18Config
Settings
/*
Sets LED on port E0 to on */
<<include the configuration bits here>>
void main (void)
{
TRISE=0; //Set port E to outputs
PORTE=1;
}
To flash an LED, you must use a delay. The code
for the delay is in the include file 'Delays.h'.
The commands are:
Delay1TCY Delay one instruction cycle.
Delay10TCYx Delay in multiples of 10 instruction cycles.
Delay100TCYx Delay in multiples of 100 instruction cycles.
Delay1KTCYx Delay in multiples of 1,000 instruction cycles.
Delay10KTCYx Delay in multiples of 10,000 instruction cycles.
The format of the command is followed by a number
in brackets (number of times to repeat the delay):
e.g. to delay for 50 instruction cycles, use:
Delay1TCY(50);
or
Delay10TCYx(5);
Code to flash an LED
/* Sets LED on port E0 to on */
<<include the configuration bits here>>
#include <delays.h>
void main (void)
{
TRISE=0; //Set port E to outputs
while(1)
{
PORTE=1;
Delay10KTCYx(100);
PORTE=0;
Delay10KTCYx(100);
}
}
To control the output of a single bit, use the
latch LATportbits.LATportbit.
E.g. To set bit 4 of port E, LATEbits.LATE4=1
The example below alternately flashes LEDs on bits
0 and 1 of port E:
<<include the configuration bits here>>
#include <delays.h>
void main (void)
{
TRISE=0; //Set port E to outputs
while(1)
{
LATEbits.LATE0=1;
LATEbits.LATE1=0;
Delay10KTCYx(100);
LATEbits.LATE0=0;
LATEbits.LATE1=1;
Delay10KTCYx(100);
}
}
A clearer way of programming is to use '#define'
where the port can be given a useful name. This also works for setting
individual 'TRIS' bits too as in:
#define
trisled1 TRISDbits.TRISD0
#define led1 LATDbits.LATD0
trisled1=0; //Sets led port to output
led1=1; //Turns on LED
Inputs can be read, first, set the direction of the pins to input,
e.g.
TRISB=0b11111111; Sets all pins on port B to
input
Read the pins e.g.
If (PORTB&0b1) ..... Will act when
bit 0 of PORTB is high
A clearer way of programming is to use '#define'
where the port can be given a useful name. This also works for setting
individual 'TRIS' bits too as in:
#define
sw1 PORTBbits.RB3
#define trissw1 TRISBbits.TRISB3
trissw1=1; //Sets to input
//Test switch high
if (sw1==1) …
Pullups
Internal pull-up resistors can be enabled by resetting bit 6 of
INTCON2 thus:
INTCON2=INTCON2&0B0111111; //Set pullups on port
B
For details of why INTCON2 is used etc, see
section 10.2 (page 114) of the PIC18F4550 datasheet.
The A/D converter is built in to most PIC microcontrollers. Port A is
usually configurable as an analogue port to take a voltage between 0 to
5V. This example sets port A0 and A1 to be analogue input ports. Note,
the libraries available with the C18 compiler do not seem to work
properly, it is better to modify the bits directly as in my code, the
'adc.h' file is not necessary.
<<include the configuration bits
here>>
void
setupADC(void)
{
//Set port A bits to input
TRISA=0b11;
//Set FOSC/32, this is detailed in section 21.0, page 261 of the
PIC18f4550 datasheet
ADCON2bits.ADCS0=0;
ADCON2bits.ADCS1=1;
ADCON2bits.ADCS2=0;
//Set voltage reference and port A0 and A1 as analogue only, this is
detailed in section 21.0, page 260 of the PIC18f4550 datasheet
ADCON1=0b00001101;
ADCON0bits.ADON=1; //Turn on ADC
}
unsigned
int readADC(void)
{
unsigned char adh,adl;
unsigned int result;
//Read
port A0
ADCON0bits.CHS0=0;
ADCON0bits.CHS1=0;
ADCON0bits.CHS2=0;
ADCON0bits.CHS3=0;
//CHANNEL 0 set, note, to read Port A1, ADCON0bits.CHS0=1, page 259 of
the PIC18f4550 datasheet
ADCON0bits.GO=1;
while (ADCON0bits.GO);
//Convert
high and low bytes to a 16 bit value
adl= ADRESL;
adh= ADRESH;
result=adh*256;
result=result+adl;
return result;
}
When writing to the EEPROM, first disable the
interrupts. The address and data bytes are set followed by writing 0x55
and then 0xaa to EECON2.
The details are in section 7.0 of the PIC18F4550
datasheet.
void
writeeeprom(unsigned char addr,unsigned char byte)
{
unsigned char i=INTCONbits.GIEH;
INTCONbits.GIEH = 0; //disable interrupts
EECON1bits.EEPGD=0; //Write to EEPROM
EECON1bits.CFGS=0; //EEPROM not config bits
EECON1bits.WREN=1; //Allows write
EEADR=addr;
EEDATA=byte;
EECON2=0x55;
EECON2=0xaa;
EECON1bits.WR=1;
while(EECON1bits.WR); //Wait until written
//while(!PIR2bits.EEIF);
//PIR2bits.EEIF=0;
EECON1bits.WREN=0; //No more write
INTCONbits.GIEH = i; //restore interrupts
}
unsigned char
readeeprom(unsigned char addr)
{
EECON1bits.CFGS=0; //EEPROM not config bits
EECON1bits.EEPGD=0;
EEADR=addr;
EECON1bits.RD=1;
return (unsigned int) EEDATA;
}
If a 16 bit integer needs to be written and
read into EEPROM, use the following code:
void
writeeeprom16(char addr,unsigned int val)
{
char h,l;
h=(char)(val/256);
l=val-(h*256);
writeeeprom(addr,l);
writeeeprom(addr+1,h);
}
unsigned int readeeprom16(char addr)
{
//unsigned int value;
l=readeeprom(addr);
h=readeeprom(addr+1);
//hi=48;li=57;
value=(h*256)+l;
return value;
}
The interrupts are called from either a timer, or from a change in input
port. This example shows how to set a timer (using the 'timers'
library), and set an interrupt to be called by this. The code is
incomplete but shows the extras that must be included for it to work.
The OpenTimer command found in the
C18 library sets timer 0 to be 16 bit with a post scalar of 4,
i.e. if a 40MHz crystal is used, this gives a cycle frequency of 10Mhz
or 0.1u seconds. Now 16 bits means it counts from 0 to 65535 before
rolling over and generating an interrupt, i.e. this is 65536 x 0.1 u secs = 6.5536 milli-secs. With a post scalar of 4, this gives an
interrupt time of 6.5536 milli-secs x 4 = 0.0262 seconds.
Presentation on interrupts
The OpenTimer command takes the following parameters:
TIMER_INT_ON
Interrupt enabled
TIMER_INT_OFF
Interrupt disabled
Timer Width:
T0_8BIT
8-bit mode
T0_16BIT
16-bit mode
Clock Source:
T0_SOURCE_EXT
External clock source (I/O pin)
T0_SOURCE_INT
Internal clock source (TOSC)
External Clock Trigger (for
T0_SOURCE_EXT):
T0_EDGE_FALL
External clock on falling edge
T0_EDGE_RISE
External clock on rising edge
Prescale Value:
T0_PS_1_1
1:1 prescale
T0_PS_1_2
1:2 prescale
T0_PS_1_4
1:4 prescale
T0_PS_1_8
1:8 prescale
T0_PS_1_16
1:16 prescale
T0_PS_1_32
1:32 prescale
T0_PS_1_64
1:64 prescale
T0_PS_1_128
1:128 prescale
T0_PS_1_256
1:256 prescale
The following
calculations are for a 20Mhz clock and are in seconds:

An Excel program to calculate the timings is
here (put the clock frequency in cell
A1).
#include
<p18F4550.h>
#include <timers.h>
#pragma config FOSC = INTOSC_EC
#pragma config PWRT = OFF
#pragma config BOR = OFF
#pragma config MCLRE = ON
#pragma config PBADEN = OFF
#pragma config ICPRT = OFF
#pragma config LVP = OFF
#pragma config WDT = OFF,DEBUG=OFF
void set_timer(void)
{
//Ensure the oscillator is running at a specific speed
OSCCONbits.IRCF2=1;
OSCCONbits.IRCF1=1;
OSCCONbits.IRCF0=1;
OpenTimer0(TIMER_INT_ON & T0_16BIT & T0_SOURCE_INT & T0_PS_1_256);
INTCONbits.GIEH = 1; //enable interrupts
}
//----------------------------------------------------------------------------
// High priority interrupt routine
#pragma code
#pragma interrupt InterruptHandlerHigh
void InterruptHandlerHigh ()
{ //Every 0.026 secs
if (INTCONbits.TMR0IF)
{ //check for TMR0 overflow
INTCONbits.TMR0IF = 0; //clear interrupt flag
WriteTimer0(65000);
//Code used by the interrupt goes here
LATCbits.LATC0 =!LATCbits.LATC0;
}
}
#pragma code InterruptVectorHigh = 0x08
void InterruptVectorHigh (void)
{
_asm
goto InterruptHandlerHigh //jump to interrupt routine
_endasm
}
void main (void)
{
TRISC=0;
set_timer();
// WriteTimer0(0);
while(1){
//interrupt will run now
}
}
The interrupt time can be accurately set using the WriteTimer0(value)
command, where, for a 40Mhz crystal and a pre/post scalar of 1:1, value
will represent time in intervals of 0.0000001 seconds.
An RC servo motor needs a pulse of between 1-2ms every 20ms (see
http://www.seattlerobotics.org/guide/servos.html ).
Here, Timer1 triggers every 20ms, sets an output pin and enables timer0.
When timer0 triggers, it resets the output pin and disables itself.

In the program below, Timer1 triggers every 20ms and timer 0 triggers
every 't' msecs:
#include <p18F2520.h>
#include <timers.h>
#pragma config OSC = HS
#pragma config PWRT = OFF
#pragma config MCLRE = ON
#pragma config PBADEN = OFF
#pragma config LVP = OFF
#pragma config WDT = OFF,DEBUG=OFF
#define SERVO_PULSE_HIGH LATCbits.LATC0
=1
#define SERVO_PULSE_LOW LATCbits.LATC0 =0
#define CONVERT_mS_PULSE 10000.0
unsigned int timer0;
float t;
void set_timer(void)
{
//40Mhz clock, each increment 0.0000001 secs (interrupt at overflow of
65536)
OpenTimer0(TIMER_INT_ON & T0_16BIT & T0_SOURCE_INT & T0_PS_1_1);
OpenTimer1(TIMER_INT_ON & T1_16BIT_RW & T1_SOURCE_INT & T1_PS_1_4 &
T1_OSC1EN_OFF);
T1CONbits.TMR1CS = 0; //Timer 1Clock internal source
WriteTimer1(15536);
//Every 20ms.
//since PS=1:4 increment time is 0.0004ms
//20 / 0.0004 = 50000
// 65536 - 50000 = 15536
INTCONbits.GIEH = 1; /* Enable global Interupt */
}
//----------------------------------------------------------------------------
// High priority interrupt routine
#pragma code
#pragma interrupt InterruptHandlerHigh
void
InterruptHandlerHigh ()
{
if (INTCONbits.TMR0IF)
{
LATCbits.LATC0 =0; //Servo pulse low
T0CONbits.TMR0ON = 0; //Timer0 off
}
//Timer 1 (slow)
//Every 20msecs
if (PIR1bits.TMR1IF)
{
WriteTimer1(15536);
// reset interrupt event flag timer1
PIR1bits.TMR1IF = 0;
LATCbits.LATC0 =1;
timer0=(unsigned int)(65536-(t*CONVERT_mS_PULSE));
WriteTimer0(timer0);
// enable timer0
T0CONbits.TMR0ON = 1;
INTCONbits.TMR0IF = 0; //clear interrupt flag
}
}
#pragma code InterruptVectorHigh = 0x08
void
InterruptVectorHigh (void)
{
_asm
goto InterruptHandlerHigh //jump to interrupt routine
_endasm
}
void main (void)
{
TRISC=0;
t=2.5; //Length of pulse in ms
set_timer();
while(1);
}
PC's usually use asynchronous communication (see
http://en.wikipedia.org/wiki/Asynchronous_serial_communication)
The baud rate is set using the 'spbrg' register using the formula baud=Fosc/(64*(spbrg+1).
So if spbrg=64,the baud rate is almost 9600 using a 40MHz crystal as in
the code below:
#include <usart.h>
#include <stdio.h>
#pragma config OSC = HS
#pragma config PWRT = OFF
#pragma config MCLRE = ON
#pragma config PBADEN = OFF
#pragma config LVP = OFF
#pragma config WDT = OFF,DEBUG=OFF
void main() {
char x[10];
int n=0;
int a;
TRISC=0;
OpenUSART(USART_TX_INT_OFF & USART_RX_INT_ON &
USART_ASYNCH_MODE & USART_EIGHT_BIT &
USART_CONT_RX & USART_BRGH_LOW, 64);
printf("Hello, world!\n");
getsUSART(x,5 );
x[5]=0;
a=atoi(x);
a++;
printf("%d",a);
while(1);
}
A two channel encoder consists of two light sensors slightly offset
(90 degrees phase difference).

The trick with reading an encoder is to count the edge of an
input pulse.
Counting both the rising and falling edges can double the number of
counts per revolution (increase resolution).
If a two output encoder is used with both A and B channels, the
direction of rotation can be determined. The A channel can be
used as the rotation count whilst the B channel can determine
direction by its high or low state. This is done by counting the edge of
the A channel and reading if the B channel is high or low
when the edge is counted.
The resolution can be increased fourfold by counting on both the
rising and falling edges of the A and B channels, the
other channel state determines direction, e.g. if a rising edge is
detected on the B channel, the state of A determines its
direction.
Presentation on encoders
This code snippet assumes the A and B channels are connected to Port
B bits 3 and 4 and shows both rising and falling edge counting using one
channel only:
int read_encoder(void)
{
static unsigned char prev_b3=0;
static int encl=0;
if (PORTB & 0b1000)
if ((PORTB & 0b1000)&&(!prev_b3))
{
if (PORTB & 0b10000) encl++;else encl--;
prev_b3=1;
}
if ((!(PORTB & 0b1000))&&(prev_b3))
{
if (PORTB & 0b10000) encl--;else encl++;
prev_b3=0;
}
return encl;
}
This uses the library 'pwm.h'
/* Sets the PWM value on CCP1 to 50% duty cycle*/
#include <p18F4550.h>
#include <pwm.h>
#include<timers.h>
#pragma config FOSC = INTOSC_EC
#pragma config PWRT = OFF
#pragma config BOR = OFF
#pragma config MCLRE = ON
#pragma config PBADEN = OFF
#pragma config ICPRT = OFF
#pragma config LVP = OFF
#pragma config WDT = OFF,DEBUG=OFF
void main (void)
{
TRISC=0;
OpenTimer2(TIMER_INT_OFF & T2_PS_1_1 & T2_POST_1_1);
OpenPWM1(0xff);
SetOutputPWM1(SINGLE_OUT,PWM_MODE_1);
SetDCPWM1(512);
while(1)
{
}
}
Ultrasonics (needs interrupt as above)
This uses the SRF04 from
Robot Electronics
char ultrasonic(void) //returns 1 is readings ready, else 0
{
static char usstate=0; //state machine state
static int usonic_count1=0,usonicwait1=0;
char ready=0;
switch (usstate)
{
case 17://go on to next sensor, or wait 10ms before retrigger
usonicwait1++;
if (usonicwait1>20000) usstate=0;
break;
case 16:
usonic3=usonic_count1;
usonicwait1=0;
usstate=17;
ready=1;
break;
case 15:
usonic_count1++;
if (!(PORTD & 0b100000)) usstate=16;
if (usonic_count1>800){usonic3=0;usstate=0;}//echo pulse not lowering
after >30ms, restart
break;
case 14:
usonic_count1=0;
usonicwait1++;
if (PORTD & 0b100000) usstate=15;
if (usonicwait1>200) {usstate=0;usonic3=0;}//echo pulse not coming,
restart
break;
case 13:
usonicwait1++;
if (usonicwait1>5)
{
LATDbits.LATD4 =0; //send trigger pulse for 50 interrupt
usstate=14;
}
break;
case 12:
LATDbits.LATD4 =1; //high trigger pulse
usonicwait1=0;
usstate=13;
break;
case 11://go on to next sensor, or wait 10ms before retrigger
usonicwait1++;
if (usonicwait1>20000) usstate=12;
break;
case 10:
usonic2=usonic_count1;
usonicwait1=0;
usstate=11;
break;
case 9:
usonic_count1++;
if (!(PORTD & 0b1000)) usstate=10;
if (usonic_count1>800){usonic2=0;usstate=0;}//echo pulse not lowering
after >30ms, restart
break;
case 8:
usonic_count1=0;
usonicwait1++;
if (PORTD & 0b1000) usstate=9;
if (usonicwait1>200) {usstate=0;usonic2=0;}//echo pulse not coming,
restart
break;
case 7:
usonicwait1++;
if (usonicwait1>5)
{
LATDbits.LATD2 =0; //send trigger pulse for 50 interrupt
usstate=8;
}
break;
case 6:
LATDbits.LATD2 =1; //high trigger pulse
usonicwait1=0;
usstate=7;
break;
case 5://go on to next sensor, or wait 10ms before retrigger
usstate=6;
//usonicwait1++;
//if (usonicwait1>20000) usstate=0;
break;
case 4:
usonic1=usonic_count1;
usonicwait1=0;
usstate=5;
break;
case 3:
usonic_count1++;
if (!(PORTD & 0b10)) usstate=4;
if (usonic_count1>800){usonic1=0;usstate=0;}//echo pulse not lowering
after >30ms, restart
break;
case 2:
usonic_count1=0;
usonicwait1++;
if (PORTD & 0b10) usstate=3;
if (usonicwait1>200) {usstate=0;usonic1=0;}//echo pulse not coming,
restart
break;
case 1:
usonicwait1++;
if (usonicwait1>5)
{
LATDbits.LATD0 =0; //send trigger pulse for 50 interrupt
usstate=2;
}
break;
case 0:
LATDbits.LATD0 =1; //high trigger pulse
usonicwait1=0;
usstate=1;
break;
}//case
return ready;
}
|