sdcc for PIC HowTo

A simple walkthrough for getting started with C programming on PIC micros using the sdcc C compiler.


1 Introduction

Traditionally the bulk of development on PICs, both by professionals and enthusiasts, has been done in assembly language. In part this was due to performance requirements, and part due to the lack of any real alternatives. However, with the increase in the power of available PICs and the availability of several high level languages for PICs, it is becoming increasingly appropriate to consider HLLs for PIC projects.

While C compilers have been available to professional programmers for some time, their cost has limited their use by enthusiasts. Now, through the open source development model, free tools are becoming available.

The purpose of this HowTo is to provide help getting started with one such tool “sdcc” - the Small Device C Compiler. Although sdcc supports both 14 and 16 bit core PICs, this HowTo only considers using sdcc to produce code for 16 bit devices (i.e. the 18F series).



1.1 What you should know from the start

This is NOT a tutorial on C programming! In order to make use of sdcc you will need to learn C. Here is a link to a useful C tutorial.

This is NOT a tutorial on PIC programming either, but this is less important. Also you should already know how to use your PIC programmer and how to build a simple test rig with a PIC, crystal and LED. If you need help, have a look at Wouter's Bkink a Led page.

This is NOT a tutorial on the various package management schemes and tools used by different Linux distributions and it aims to be distribution independent.

This WILL show you how to installing sdcc and getting it working.

SDCC supports many small device families, and PICs are not the best supported but as with all open source projects improvements are only made if someone is willing to make the effort. [Add reference to the PIC developers here].

2 The Tool Chain

SDCC does not operate in isolation, and when targeted on PIC devices it makes use of the gputils tools to provide the back end of the compilation process. The simplest tool chain is shown below.




The starting point is the “C Source File”. You may create this with what ever editor you like. This is read by SDCC which produces a PIC assembly language “.asm” file. The gputils tools take over at this point and gpasm is used to convert the .asm file to a object “.o” file. If you have used gpasm before but only to directly to produce a hexadecimal image “.hex” file, then Craig Franklin's GPUTILS Relocatable Object HowTo. will be of interest. The next step is to use gplink to convert the .o file into a .hex file suitable for use with whatever device programmer you chose to use.

Note: If you are familiar with using C on PCs there you will realise there are a few simplifications in the above description, but these won't matter until we move onto programs more complex that the obligatory blink_a_led.c .

2.1 Installing SDCC

Due to sdcc's constant development, it is best to build it from the sources rather than to install a pre-built binary package which will very quickly become out of date. Building sdcc from the sources does require you to have a working C development environment on your Linux system. To test this run the following command..

gcc -v

If this produces a few lines of information finishing with a version number you're probably OK. If not, then use your distribution's package management tool to install the C development tools packages.

2.1.1 Getting the sources.

The SDCC project has a home page on sourceforge. The sourceforge download page will allow you to download the latest released version, or preferably one of the nightly snapshots which have the latest bug fixes and enhancements included. You'll need a “SDCC Source Code (sdcc-src)” tar ball. Unpack the tar-ball into somewhere appropriate. (ED: Should this be /usr/local/src or some where else ?)The instructions that follow will build just the PIC compiler.

2.1.2 Compiling sdcc from the source code

Sdcc uses a standard process for building from source code which automates nearly all the required steps.

In the top level of the source tree (where you'll find a file called “ChangeLog”) run the following command:

./configure --disable-mcs51-port --disable-gbz80-port --disable-z80-port \
--disable-avr-port --disable-ds390-port --disable-ds400-port \
--disable-hc08-port --disable-xa51-port

You should see lots of output that starts off like this...

checking for gawk... gawk
checking version of the package... 2.5.1
checking for gcc... gcc
checking for C compiler default output file name... a.out
checking whether the C compiler works... yes

And ends like this....

config.status: creating gui.src/Makefile
config.status: creating gui.src/serio.src/Makefile
config.status: creating doc/Makefile
config.status: creating ddconfig.h

This has checked that all the “stuff” needed to build sdcc is present on your system, and has writen the “makefiles” that will automatically build sdcc from the source code. All that is needed is to run the commands

make 

Assuming there are no errors, sdcc has now been built. The next step is to build some function libraries that come with sdcc. The next few command will build these.

cd device/lib/pic16
make lib-io

It now only remains to put all the sdcc execuatables and libraries into the correct directories. Only priviledged users can do this so you will need to know the “root password” for your machine. The next command will prompt you to enter the root password and then it will run the install scripts to put the files in the correct places.

su -c “make install”

And we're done building sdcc.

2.2 gputils

The gputils package contains the tools that are needed to convert the assembly language files (.asm) produced by sdcc into object files (.o), and then to link them together with any required library code to make the final executable. The gpuils home page is at http://gputils.sourceforge.net/

Tool

Inputs

Action

Outputs

gpasm

Assembly language file converted from C language file by sdcc

Convert “.asm” file to “.o” file

COFF format object fil

gplink

Object files “.o” & library files “.lib” & target configuration files “.lkr”

Place code from object and library files into PIC memory, allocate RAM to variables

Intel format hex files, listing file, map file.

2.2.1 Installing gputils

At the time of writing the current stable release of gputils is 0.13.2 , but unfortunatley this is not suitable for use with sdcc. The problems have been fixed and either the version from CVS or the nightly snapshot need to be installed. The snapshot can be found at http://gputils.sourceforge.net/snapshots/gputils.tar.gz

Untar this somewhere appropriate and then in the top directory run

./configure
make
su -c “make install

If the installation has worked properly, then you should be able to run the following command which will output the version of gpasm:

gpasm -v
That's all there is to it ! 
3 Blink-a-LED

It's now time to compile your first C program for a PIC. The following program file can be downloaded from HERE

// blinkled.c
//
// simple minimal program that blinks a LED

// ------------------------------------------------
// configuration
// change these if you're wiring the LED to a pin other than RB1

#define LED_TRIS  TRISBbits.TRISB1
#define LED_PIN   PORTBbits.RB1


// ------------------------------------------------
// this #include pulls in the correct processor-specific registers
// definition file

#include "pic18fregs.h"


#pragma stack 0x200 64

code char at __CONFIG1H conf1 = 0x22; // Select HS OSC
code char at __CONFIG4L conf2 = 0x81; // Disable LVP
code char at __CONFIG2H conf3 = 0x0E; // DIsable WDT

// ------------------------------------------------
// a simple delay function

void delay_ms(long ms)
{
    long i;

    while (ms--)
        for (i=0; i < 330; i++)
            ;
}

// --------------------------------------------------
// and our main entry point

void main()
{

    // set pin to output
    LED_TRIS = 0;

    // sit in an endless loop blinking the led
    for (;;)
    {
        LED_PIN = 0;
        delay_ms(250);
        LED_PIN = 1;
        delay_ms(250);
    }
}



If everything has gone to plan in the installation, then this file can be compiled by the command

sdcc -mpic16 -p18f452 blinkled.c
This will create a number of files and running “ls -l blink*” should produce a listing similar to that below.
ponion@HP:~/pic/18F/sdcc> ls -l blink*
-rw-r--r--  1 ponion users  6316 2005-07-28 21:47 blinkled.asm
-rw-r--r--  1 ponion users  1105 2005-07-28 21:42 blinkled.c
-rw-r--r--  1 ponion users  8704 2005-07-28 21:47 blinkled.cod
-rw-r--r--  1 ponion users  1396 2005-07-28 21:47 blinkled.hex
-rw-r--r--  1 ponion users 21434 2005-07-28 21:47 blinkled.lst
-rw-r--r--  1 ponion users  7627 2005-07-28 21:47 blinkled.o
ponion@HP:~/pic/18F/sdcc>



blinkled.asm contains the output of compiler and contains the assembly language version of the C code.

blinkled.o contains the output of gpasm.

blinkled.lst contains debugging information form the linker.

blinkled.hex contains the final PIC program in a format suitable to download into your programmer.

blinkled.cod contains a version of the final program with additional information that can be used with gpsim.

Now you need to use your programmer to transfer blinkled.hex into your 18FXXX pic and connect a LED and resistor (220 ohms) in series between PB0 and the Gnd supply. Running the program should cause the LED to flash (at a rate determined by the crystal fitted).



4. More Complex examples

Here is a more complex example which I've annotated in blue .

/* Morse output 
   sdcc demo program
   compile with  "sdcc -mpic16 -p18f452 morse.c"
   (c) P.J.Onion 2005
*/


#include "pic18fregs.h"

include the definitions needed to use the interrupt handler/signal techniques
#include "signal.h"

define the location and size of the runtime stack
#pragma stack 0x200 0x40

set the configuration bytes (Set HS xtal oscillator, disable LVP, disable WDT) 
code char at __CONFIG1H conf1 = 0x22;
code char at __CONFIG4L conf2 = 0x81;
code char at __CONFIG2H conf3 = 0x0E;

symbolic definitions of numbers to control length of dashes and dots
#define DASH 250
#define DOT 100

symbolic definitions for the pin used to drive the LED
#define LED_TRIS  TRISBbits.TRISB7
#define LED_PIN   PORTBbits.RB7



An initialised array of bytes containing the dot/dash codes for each letter
unsigned char codes[3] = {
/* A */ 0x60,
/* B */ 0x88,
/* C */ 0xA8
};

unsigned char intCounter = 0;
unsigned char timer;

Definitions for using Timer0 interrupts
DEF_INTHIGH(high_int)
DEF_HANDLER(SIG_TMR0, _tmr0_handler)
END_DEF 

This is the interrupt handler called when timer0 overflows
SIGHANDLER(_tmr0_handler)
{

  /* action to be taken when timer 0 overflows */
  /* decrement intCounter until it reaches zero */
    if(intCounter) intCounter -= 1;
    INTCONbits.T0IF = 0;   /* Reset the Timer0 interrupt pending flag */
} 
  


/* Pause while intCounter is counting down to zero */
void delay(unsigned char time)
{
 
    intCounter = time;
    while(intCounter) ;

}
    

void sendLetter(unsigned char index)
{
    unsigned char shift;

    shift = codes[index];

    do
    {
        LED_PIN = 1;
        if(shift & 0x80)
        {
            delay(DASH);
        }
        else
        {
            delay(DOT);
        }
        LED_PIN = 0;
        delay(DOT);
        shift <<= 1;
    } while(shift != 0x80);

    delay(DOT);

}
    

void main(void)
{
    /* Set LED pin as an output */
    LED_TRIS = 0;

    /*Enable high and low priority interrupts */
    RCON = 0;
    RCONbits.IPEN = 1;

    /* Configure timer0 */
    T0CONbits.T0PS0 = 0;
    T0CONbits.T0PS1 = 0;
    T0CONbits.T0PS2 = 1;

    T0CONbits.PSA = 0;
    
    T0CONbits.T0SE = 0;
    T0CONbits.T0CS = 0;
    T0CONbits.T08BIT = 1;
    T0CONbits.TMR0ON = 1;

    /* enable timer0 interrupt as a high priority source */ 
    INTCON2bits.T0IP = 1;
    INTCONbits.T0IE = 1;
    
    /* Enable interrupts */
    INTCONbits.GIE = 1;

    while(1)
    {

    sendLetter(0);

    sendLetter(1);

    sendLetter(2);
    }

}



Using the C libraries

For all but the very simplest of C programs you will want to use some of the library functions that are included with sdcc. In general these are equivalent to those provided by C compilers for bigger devices. The following diagram shows how things work when using existing libraries...






“Library ,h files” refers to files such as “stdio.h” or “malloc.h” which contain declarations for the functions in the “.lib files”. The code in “Startup .o file” sets up the PIC ready to run your C programmes.

Dynamic Memory Allocation

Even on the 18F series of PICs data memory is quite small. This makes dynamic allocation very useful as it can avoid static memory definitions for data that is only used for a short time (for example a message received over a serial link). The sdcc dynamic memory allocation scheme is quite simple and relies on a statically allocated area of memory to provide the heap. This is then divided up and allocated and freed as required. The maximun heap size is 512 bytes, and the maximum number of bytes that can be allocated at a time is 126. Here is a small example....

#include "malloc.h"

/* Memory for the heap */
unsigned char _MALLOC_SPEC heap[MAX_HEAP_SIZE];

union PACKET
{
    unsigned char raw[21];  /* 2 + 16 + 3 */
    struct
    {
        unsigned Len:4;
        unsigned Dst:4;
        unsigned Pro:4;
        unsigned Src:4;
         
        unsigned char bytes[16];
        union PACKET *next;
    };
};
 
/* allocate space for a packet from the heap */
union PACKET *newPacket(void)
{
union PACKET *ret;

    ret  = (union PACKET *) malloc(sizeof(union PACKET));

return(ret);
}



/* Return processed packet memory to the heap */ 
void doneWithPacket(union *packet)
{
      free( (unsigned char _MALLOC_SPEC *) packet);
}

If the heap is full, then the return value from malloc will be NULL. It should be tested like this

packet = newPacket();
if(packet == NULL)
{
/* HEAP FULL */
}
else
{
     /* Do stuff with the memory */
}

Have a look in malloc.h to see more functions that are available.

Using the I/O libraries

There is a small library of functions for accessing the USART module for serial input and output, the A2D module for analogue voltage readings and the I2C module for communications with other chips. The usart library can be used in conjunction with the C stdio library to allow formatted input and output via the usart.

#include “pic18fregs.h”
#include  “stdio.h”
#include “usart.h”



void main(void)
{
    /* Initialise the USART hardware (see usart.h for details */
    /* The number 79 gives 9600 baud with a 12.288Mhz PIC clock */
    usart_open(
           USART_TX_INT_OFF
        & USART_RX_INT_OFF
        & USART_BRGH_HIGH
        & USART_ASYNCH_MODE
        & USART_EIGHT_BIT,
           79
        );
    /* Direct outout to the USART */
    stdout = STREAM_USART;

    /* Say “hello” */
    printf("Hello World\n");
}





Using existing assembly language modules

It is quite easy to make use of assembly language code you may have already written. For example here is stripped down version of an existing LCD driver module....

        PROCESSOR 18F452
        Radix DEC
        EXPAND


        include "p18f452.inc"

        UDATA

LcdFlags        res 1
        
        CODE

_LCDinit:
        banksel LcdFlags
        clrf    LcdFlags,b

        movlw   B'00110011' ;initialise lcd - first byte
         call    _LCDwrite
         [ SNIP ]
        return

_LCDwrite:      
         [ SNIP ]
         return

_LCDclear:
         [ SNIP ]
         return
 

         GLOBAL _LCDinit,_LCDwrite,_LCDclear
        
         END

Note that the exported functions all begin with an underscore character and that they are listed in the “GLOBAL” statement at the end.

This file must be assembled by the following command to produce lcd.o

gpasm -c lcd.asm

To use these functions in a C program they must be declared as external by using the “extern” keyword.

/* hellolcd.c  ( C )  P.J.Onion 2005 */

#include “stdio.h”

/* Funcions in lcd.asm */
extern void LCDinit(void);
extern void LCDwrite(char) wparam;
extern void LCDclear(void);

/* Send output from stdio to the LCD */
void putchar(unsigned char arg) __wparam
{
    static unsigned char lastch;
       
    if(lastch == '\n')
    {
        LCDclear();
    }
 
    if(arg != '\n')
    {
        LCDwrite(arg)
    }
 
    lastch = arg;
}


void main(void)
{
    LCDinit();
    LCDclear();

    stdout = STREAM_USER;
    printf(“Hello World”);
}

This is compiled and linked to the LCD code with this command:

sdcc -mpic16 -ppic18f452 hellolcd.c lcd.o



Other Techniques

Using Interrrupt driven I/O

If you look through the example PIC code you can find on the web you could come to the conclusion the using interrupts for I/O is very difficult. In fact it is quite easy. Here is the general structure I use for interrupt driven serial comms with the USART module.




Hints and tips

Saving data space. Here is a code snippet to print out month names:

char *months[12]  =  {
    "January","February","March","April","May","June",
    "July","August","September","October","November","December" 
};


void printMonthName(unsigned char monthNumber)
{
     puts(months[monthNumber]);
}

Although the data for the strings is placed in code memory, array of pointers is placed in the data space and each pointer takes 3 bytes. A better way to define the array is like this:

char * __code months[12]  =  {
    "January","February","March","April","May","June",
    "July","August","September","October","November","December" 
};

The inclusion of the word “__code” also places the array into code memory thus saving 36 bytes of data memory.

[Note for C beginners: The declaration “char *months[12]” is read as “months is an array of 12 pointers to characters” ]







6 Conclusions