PIC compiler comparison
This article compares the XC8 and SDCC compilers using a blink program, written using interrupts for a PIC18F4550. A hardware timer will be used to time the delay and an interrupt service routine used to toggle the LED. The length of the delay depends on the precise number of instruction cycles and the clock speed. Interrupts should not typically be used in this way, but it's the simplest case for learning purposes. The differences between compilers will be evident in both the syntax and the compiled program.
Portable code
To switch between compilers easily, the code had to be ''portable''. Portable code is useful not only when switching compilers, but also when moving code to a different MCU.
Portable code means
- using the CCI compiler flag:
--EXT=cci
for XC8 and--mcci
for XC16 (SDCC doesn't seem to have an equivalent) - including
<xc.h>
for XC8 or<pic18fregs.h>
in every module - ensuring ANSI C compliance
- observing refinements to ANSI by the CCI and
- using the CCI extensions to the language instead of native language extensions.
There are limits to portability. For example, if the registers the code refers to do not exist on a different MCU. In general, writing code for an MCU is very specific to its features, what it can do, how many registers it has etc. Whatever MCU you decide to use, the datasheet will be your best guide. Almost everything in this article was learned from either the SDCC user guide, the XC8 user guide or the PIC18F4550 datasheet (pdf).
Description of a pic18 interrupt
There are some common features between 8-bit pic MCUs, although they may have different labels.
These include the clock, the hardware timer, TMR0
, the timer0 control register, T0CON
, and the interrupt register, INTCON
.
At the end of each instruction cycle, TMR0
is incremented.
When it overflows (i.e. reaches 256) it will set the interrupt flag, TMR0IF
, if and only if GIE
is set.
When the interrupt flag is enabled, the current instruction is halted and the corresponding interrupt service routine is called.
These routines are stored at particular locations in memory.
The pic18 interrupt vectors are:
address | interrupt |
---|---|
0x000 | reset interrupt vector |
0x008 | high priority interrupt vector |
0x018 | low priority interrupt vector |
The interrupt is handled and TMR0IF
and GIE
bits are reset.
TMR0
is then also reset.
Thus, the delay between interrupts can be set with
TMR0_initial = 256 - delay
For delays longer than 256 instruction cycles, a prescaler must be used to increase the number of instruction cycles per TMR0
increment.
The prescaler is set using the timer0 control register, T0CON
.
This register for a PIC18F4550 is described below.
bit | label | description |
---|---|---|
7 | TMR0ON |
timer0 on/off (1 = on, 0 = off) |
6 | T08BIT |
timer0 is (1 = 8-bit, 0 = 16-bit) |
5 | T0CS |
counter or clock source (1 = T0CKI pin, 0 = CLKO internal) |
4 | T0SE |
edge selection (1 = high-to-low, 0 = low-to-high) |
3 | PSA |
prescaler assignment bit (1 = off, 0 = on) |
2:0 | T0PS |
2..256 = prescale value |
e.g. T0CON = 0b11010111
timer on, 8-bit, clock source, high-to-low edge, with a prescale value of 256.
The interrupt register, INTCON
, tells the MCU how to handle an interrupt.
The interrupt register for a PIC18F4550 is described below.
bit | label | description |
---|---|---|
7 | GIE |
global interrupt enable |
6 | PEIE |
peripheral interrupt enable |
5 | TMR0IE |
TMR0 interrupt enable |
4 | INT0IE |
RBO/INT pin enable interrupt (button?) |
3 | RBIE |
PORTB input change interrupt enable |
2 | TMR0IF |
TMR0 overflow interrupt request active |
1 | INT0IF |
RBO/INT pin interrupt request active |
0 | RBIF |
PORTB input change interrupt request active |
e.g. INTCON = 0b11111000
enable interrupt, enable peripheral interrupt, use timer0, enable pin interrupt etc.
All this information is available in the datasheet.
The configuration bits
Configuration bits must be set in the preprocessor section. These are defined the file ''PIC18 Configuration Settings Addendum'' (see pdf). The configuration bit settings were written in a separate header file for clarity (h). Fortunately, the same header file can be used for both compilers.
The delay function
Each instruction cycle takes 4 clock cycles.
To calculate the number of instruction cycles required for a delay of, say, 1ms, you need CLOCK/4000
.
The clock itself can be an external component (as here) or internal.
The frequency of an external clock is determined by the part, 20MHz in this instance.
The frequency of an internal clock speed is determined by the PLLDIV configuration bit.
If you want a long delay, set T08BIT
to 16-bit and max out the prescaler to 0b111
.
Then adjust the delay by giving the TMR0 initial value.
For example, for a 1s interrupt and a 20MHz clock:
TMR0 = 20e6/4/256
= 0x4c4b
or TMR0L = 0x4c
and TMR0H = 0x4b
.
A blink program using interrupts is listed below:
// file: tmr0_blink.c
// author: bto
// brief: a portable blink function using interrupts
//#include <pic18fregs.h> // SDCC
#include <xc.h> // XC8
#include "defs.h"
#include "tmr0_blink.h"
#include "pic18f4550-config.h" // configuration bits
// The LED is on pin RD3, the button is on pin RB4
#define LED LATDbits.LATD3 // latch RD3
#define BUT PORTBbits.RB4 // read RB4
// assembly variables
volatile unsigned char asmi; // chars for assembly
volatile unsigned char asmj; // chars for assembly
unsigned int i;
//void timer0_isr(void) __interrupt 1
void __interrupt(high_priority, auto_psv) timer0_isr(void)
{
if (INTCONbits.TMR0IE && INTCONbits.TMR0IF) {
// handle timer0 overflow interrupt
// toggle LED
if (LED == ON) {
LED = OFF;
}
else {
LED = ON;
}
TMR0H = 0x4c;
TMR0L = 0x4b;
INTCONbits.TMR0IF = 0; // clear the TMR0 flag
return;
}
return;
}
void
delay_ms(unsigned long n)
{
for (i = 0; i < n; ++i) {
//__asm
#asm
; 1ms delay (54 * 74 = 3996 cycles)
movlw 0x36
movwf _asmi
movlw 0x4a
movwf _asmj
iloop:
movwf _asmj
jloop:
decfsz _asmj,f
goto jloop
decfsz _asmi,f
goto iloop
#endasm
//__endasm;
}
}
int
main(void)
{
// reset control register
RCONbits.IPEN = 0; // enable priority interrupts (1 = on, 0 = off)
// interrupt control register
INTCONbits.RBIF = 0; // RB port change interrupt flag
INTCONbits.INT0IF = 0; // external interrupt flag
INTCONbits.TMR0IF = 0; // timer0 interrupt flag
INTCONbits.RBIE = 0; // RB port interrupt enable
INTCONbits.INT0IE = 0; // external interrupt enable
INTCONbits.TMR0IE = 1; // timer0 interrupt enable
if (RCONbits.IPEN) {
INTCONbits.GIEL = 1; // low priority interrupt enable (1 = enable, 0 = disable)
INTCONbits.GIEH = 1; // high priority interrupt enable (1 = enable, 0 = disable)
} else {
INTCONbits.PEIE = 0; // peripheral interrupt enable (1 = enable, 0 = disable)
INTCONbits.GIE = 1; // global interrupt enable (1 = enable, 0 = disable)
}
// interrupt control 2 register
if (RCONbits.IPEN) {
INTCON2bits.TMR0IP = 1; // Overflow interrupt priority bit (0=low priority, 1=high priority)
}
// timer0 control register
T0CONbits.T0PS = 0b111; // prescale value (0 = 2, 111 = 256)
T0CONbits.PSA = 0; // prescale (0 = on, 1 = off)
T0CONbits.T0SE = 0; // edge select (1 = high-to-low, 0 = low-to-high)
T0CONbits.T0CS = 0; // clock source (1 = T0CKI, 0 = internal)
T0CONbits.T08BIT = 0; // bit (1 = 8-bit, 0 = 16-bit)
T0CONbits.TMR0ON = 1; // enable tmr0 (1 = on, 0 = off)
if (T0CONbits.T08BIT) {
TMR0H = 0; // Ensure high bytes of timer register are 0 if using 8-bit timer
}
// define ports as input or output
TRISDbits.TRISD3 = OUTPUT; // LED
TRISBbits.TRISB4 = INPUT; // button
// interrupt every 100ms
TMR0H = 0x4c;
TMR0L = 0x4b;
LED = OFF;
while (1) {
//LED = ON;
//delay_ms(1000);
//LED = OFF;
//delay_ms(1000);
}
}
Timer0 interrupts the while loop every second, then toggles the LED. A more suitable use of interrupts would, for example, toggle the LED on a button push.
Differences between compilers
The differences between code for XC8 and SDCC compilers include the header file
the prototype for the interrupt service routine
and the assembly directives for the delay_ms
function.
XC8 | SDCC |
---|---|
#include <xc.h> |
#include <pic18fregs.h> |
void __interrupt(high_priority, auto_psv) timer0_isr(void) |
void timer0_isr(void) __interrupt (1) |
#asm ... #endasm |
__asm ... __endasm; |
Other differences were removed. For example, SDCC does not allow variables to be defined within functions, it seems, whereas XC8 does, so global variables were used instead. Switching between compilers produced code that was as ''portable'' as possible.
The main difference between the compiled hex files was the size: XC8 produced a 880 byte hex file whereas SDCC produced a 1.7K hex file. Thus, despite the so-called non-optimized XC8, it still produced much smaller code. It was also better with memory management, allowing for more (or larger) variables, such as floats, to be used.
Conclusion
So it appears that XC8 is, at least for the moment, a more mature compiler than SDCC for the pic18 MCU. This was not too surprising given that SDCC support for pic is still under development. However, SDCC targets at a far wider range of MCUs, including Intel MCS51, Dallas DS80C390, Freescale HC08, Zilog Z80 and STMicroelectronics STM8.
If only XC8 will be used for PIC MCUs, is it still beneficial to write portable code? I would say yes, given that some functions, such as a delay function or an LCD driver, would be useful for any MCU, and minimizing the number of changes required when porting these to a different MCU is a good thing.
Optional: Assembly delay function
The reader will notice a delay_ms
function in the above program.
When interfacing with hardware, small delays are sometimes needed before a response is received.
While interrupt service routines are useful for user input and real-time clocks, they're not useful for simply idling the MCU.
The best way to write this sort of delay is in assembly, as different compilers will produce different assembly code from the same C, and hence produce different delays.
Also, if the C compiler decides the function doesn't do anything ''useful'' it could optimize it out altogether.
Both XC8 and SDCC allow for inline assembly, that is, assembly included within C routines. An example function for XC8 is:
asm("movlw 0xff"); // copy 255 into working register
asm("movwf _i"); // copy working register into i
asm("iloop:");
asm(" decfsz _i,f"); // decrement f register, 0 skips next line
asm("goto iloop");
The same function written for SDCC is:
__asm__("movlw 0xff\n"); // copy 255 into working register
__asm__("movwf _i"); // copy working register into i
__asm__("iloop:\n");
__asm__("\tdecfsz _i,f"); // decrement f register, 0 skips next line
__asm__("goto iloop");
While this is legal C-syntax, it's still compiler specific and difficult to read.
Another way of including inline assembly is with block formatting. For XC8, the assembly directives are
#asm
movlw 0xff ; copy 255 into working register
movwf _i ; copy working register into i
iloop:
decfsz _i,f ; decrement f register, 0 skips next line
goto iloop
#endasm
and for SDCC, they are
__asm
movlw 0xff ; copy 255 into working register
movwf _i ; copy working register into i
iloop:
decfsz _i,f ; decrement f register, 0 skips next line
goto iloop
__endasm;
This is far more portable and easier to maintain, despite being non-standard.
If a lot of assembly is written, it would be better to keep it in separate files.
For assembly, different compilers are used, specifically sdas8051
for SDCC and MPASM
for XC8.
Unfortunately, assembly is different for these two compilers, even for the same MCU.
Linking separate assembly files into a program will have to form part of another article.