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

  1. using the CCI compiler flag: --EXT=cci for XC8 and --mcci for XC16 (SDCC doesn't seem to have an equivalent)
  2. including <xc.h> for XC8 or <pic18fregs.h> in every module
  3. ensuring ANSI C compliance
  4. observing refinements to ANSI by the CCI and
  5. 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.