Assembly functions in C for PIC18F

I have become increasingly interested in using Assembly in some microcontroller projects to speed up small routines. I want to be able to write the base of the program in C and just have certain functions implemented in Assembly.

I first had a look at an Atmel ATtiny85 programmed with GCC. From the GCC documentation it was quite clear what general purpose registers were used for storing the parameters and return values when calling Assembly from C – from there on, the Assembly coding was quite simple.

Later, I wanted to do a small bit-banging routine for debug printing from a PIC18F2550 through emulated I2C and I figured this would be a good project for learning Assembly for PIC18Fs. I soon figured out that the architecture of the PICs does not contain general purpose registers in the way the AVRs do so the parameters would need to be passed on in another way.

After playing around a bit, I found that using inline Assembly in a C function made it quite easy to mix C and Assembly using the XC8 compiler from Microchip. An example of adding two numbers is shown below. Note that my hardware uses an external 8 MHz crystal and that the configuration words therefore are set for 8 MHz operation using this crystal.

Add function in Assembly
#define _XTAL_FREQ 8000000UL

#include <xc.h>
#include <stdint.h>
#include <pic18f2550.h>

////////////////////////////////////////////////////////////////////////////////
// Configuration words /////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

#pragma config PLLDIV   = 2          // 8 MHz crystal -> 4 MHz PLL input
#pragma config CPUDIV   = OSC1_PLL2
#pragma config USBDIV   = 2          // Clock source from 96MHz PLL/2
#pragma config FOSC     = HS         // Don't enable PLL
#pragma config FCMEN    = OFF
#pragma config IESO     = OFF
#pragma config PWRT     = OFF
#pragma config BOR      = ON
#pragma config BORV     = 3
#pragma config VREGEN   = OFF        // USB Voltage Regulator
#pragma config WDT      = OFF
#pragma config WDTPS    = 32768
#pragma config MCLRE    = ON
#pragma config LPT1OSC  = OFF
#pragma config PBADEN   = OFF
#pragma config STVREN   = ON
#pragma config LVP      = OFF
#pragma config XINST    = OFF        // Extended Instruction Set
#pragma config CP0      = OFF
#pragma config CP1      = OFF
#pragma config CPB      = OFF
#pragma config WRT0     = OFF
#pragma config WRT1     = OFF
#pragma config WRTB     = OFF        // Boot Block Write Protection
#pragma config WRTC     = OFF
#pragma config EBTR0    = OFF
#pragma config EBTR1    = OFF
#pragma config EBTRB    = OFF

////////////////////////////////////////////////////////////////////////////////
// Inline Assembly function ////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

// First argument must be the one returned by the function!
uint8_t add(uint8_t ret, uint8_t a, uint8_t b)
{
    // Instruction set:
    // http://technology.niagarac.on.ca/staff/mboldin/18F_Instruction_Set/

    // Local variables should be volatile.
    volatile uint8_t accumulator = 0;

    // Inline Assembly part (W == working register)
#asm
    movf add@a,W                     // W = a
    addwf add@accumulator            // accumulator += W
    movf add@b,W                     // W = b
    addwf add@accumulator            // accumulator += W
    movff add@accumulator,add@ret    // ret = accumulator
#endasm

    return ret;    // Return a
}

////////////////////////////////////////////////////////////////////////////////
// Main function ///////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

void main(void)
{
    uint8_t aa = 0x01;
    uint8_t bb = 0x02;
    uint8_t result;

    result = 1; // Breakpoint here to step through Assembly
    result = add(0, aa, bb);

    while (1);
}

The Assembly function is of course much more complicated than it needs to be, but I wanted to use both local variables as well as parameters.

Each parameter passed to the function is available in the Assembly listing as FUNCTIONNAME@PARAMETERNAME, as it should be clear from the add() function. The Assembly part is encapsulated between #asm and #endasm.

Some things I have noted (please correct me if I am wrong!) are:

  • The first parameter passed to a function is the one that should be used as return value. Returning the local variable accumulator makes the program misbehave but returning ret shows no problems. The same is true if ret is not included as parameter and a is returned instead.

  • Local variables should be declared volatile – I think I read this somewhere. It makes sense as the C compiler should not mess with the variables.

In case you have more information on how to use inline Assembly with PICs, please send me an email – I would love to learn more :-)