Digging inside a Microcontroller C Compiler

One of the powerful features of C – and it’s pain points is the ability to get down to low level details. Given that most of us are brought up on a von-Neumann architecture, there are many assumptions we have about how the code should work. In this entry, I’m going to peek a little under the covers of the SourceBoost C compiler for the PIC.

Let’s take the following example:

1: #include <system.h>

2:

3: int multiply(int a, int b)

4: {

5: return a * b;

6: }

7:

8: voidmain()

9: {

10: int a = 5;

11: int b = 6;

12: int c = multiply(a, b);

13: }

Given a traditional C compiler on a typical microprocessor, we’d expect to see something like the following steps (with optimizations turned off):

Allocate two items on stack for ‘a’ and ‘b’

move ‘5’ to ‘a’ (typically done using an index register)

move ‘6’ to ‘b’

push ‘b’ on the stack

push ‘a’ on the stack

call ‘multiply’

push stack frame pointer (index register, such as ‘bp’ or ‘ix’)

Access to ‘a’ and ‘b’ is relative to index register, e.g. a is [ix+1] and b is [ix+2]

Store result in accumulator or other general return register

return

drop two values off stack

On the PIC microcontroller, things are much more difficult as there is no concept of a data stack pointer. The return stack is also very limited in size. If compiled with optimizer turned completely off, SourceBoost’s C compiler generates the following code for main():

1: 003F main

2: 003F ; { main ; function begin

3: 003F 3005 MOVLW 0x05

4: 0040 1283 BCF STATUS, RP0

5: 0041 1303 BCF STATUS, RP1

6: 0042 00A0 MOVWF main_1_a

7: 0043 01A1 CLRF main_1_a+D'1'

8: 0044 3006 MOVLW 0x06

9: 0045 00A2 MOVWF main_1_b

10: 0046 01A3 CLRF main_1_b+D'1'

11: 0047 0820 MOVF main_1_a, W

12: 0048 00A6 MOVWF multiply_00000_arg_a

13: 0049 0821 MOVF main_1_a+D'1', W

14: 004A 00A7 MOVWF multiply_00000_arg_a+D'1'

15: 004B 0822 MOVF main_1_b, W

16: 004C 00A8 MOVWF multiply_00000_arg_b

17: 004D 0823 MOVF main_1_b+D'1', W

18: 004E 00A9 MOVWF multiply_00000_arg_b+D'1'

19: 004F 202D CALL multiply_00000

20: 0050 0830 MOVF CompTempVarRet579, W

21: 0051 00A4 MOVWF main_1_c

22: 0052 0831 MOVF CompTempVarRet579+D'1', W

23: 0053 00A5 MOVWF main_1_c+D'1'

24: 0054 0008 RETURN

25: 0055 ; } main function end

The first thing to note is that the compiler uses static memory locations for ‘a’ (main_1_a), ‘b’ (main_1_b_ and ‘c’ (main_1_c). This is done because index access is much more expensive. The register ‘W’ is an accumulator. The breakdown of above is as follows:

This technique of using fixed locations is not uncommon for microcontrollers. In fact, HI-TECH PICC compiler uses the same technique. In most cases, this works out fine. Problems start when we introduce recursion. Curious as I was, I tried the following program, which should in theory give the result 15:

1: #include <system.h>

2:

3: unsignedchar recur(unsignedchar r, unsignedchar x)

4: {

5: if(r > 0)

6: {

7: return recur (r-1, x+x) + x;

8: }

9: else

10: {

11: return x;

12: }

13: }

14:

15: voidmain()

16: {

17: unsignedchar c = recur(3, 1);

18: }

When run, this gives the result 32. Interestingly, the HI-TECH PICC compiler fails to compile the same program. So where did 32 come from?

add ‘x’ to the result (which will have been changed by the recursive call)

-

-

return result

-

It’s nice to see that HI-TECH’s compiler fails rather than giving a bad result. Given that recursion is such a fundamental expectation in C, this can be a major stumbling block. I’ve asked the authors of SourceBoost C compiler to at least give a warning.

Function Pointers

Let’s look at the following code sample:

1: #include <system.h>

2:

3: char a()

4: {

5: return'a';

6: }

7:

8: typedefchar (*fnptr)();

9:

10: voidmain()

11: {

12: fnptr p = a;

13: char c = p();

14: }

Given a traditional C compiler on a traditional microprocessor, we’d expect to see something like the following steps:

The address of function ‘a’ is stored into variable ‘p’ as a pointer.

P is placed in a register and a register indirect call is made to ‘p’

In general this is assumed to be almost as quick as calling the function directly.

The steps involved on a PIC is much harder as the data memory and program memory cannot be interchanged or trivially effect each other.

Curious about how SourceBoost solves this, I compiled the above and looked at the assembly for main:

1: ORG 0x0000001A

2: 001A main

3: 001A ; { main ; function begin

4: 001A 3001 MOVLW 0x01

5: 001B 1283 BCF STATUS, RP0

6: 001C 00A1 MOVWF CompTempVar581

7: 001D 00A0 MOVWF main_1_p

8: 001E 0820 MOVF main_1_p, W

9: 001F 00A3 MOVWF __fptr_arg_data

10: 0020 2008 CALL __fptr

11: 0021 0826 MOVF CompTempVarRet582, W

12: 0022 00A2 MOVWF main_1_c

13: 0023 0008 RETURN

14: 0024 ; } main function end

It is interesting to note that the numerical value ‘1’ is stored to ‘p’. Clearly 0 is not used because 0 is NULL and is a special value. The anticipated value would be the address of the function ‘a’ (not shown, but is ‘0x0004’). This ordinal value is stored into an internal variable ‘__fptr_arg_data’, and a function “__fptr” is created to handle the indirect call. The code for this is as follows:

1: ORG 0x00000008

2: 0008 __fptr

3: 0008 ; { __fptr ; function begin

4: 0008 ; AVOID CODE PAGE BOUNDARY BEGIN - page size:2048 words

5: ORG 0x0008

6: 0008 0823 MOVF __fptr_arg_data, W

7: 0009 00A4 MOVWF __fptr_1_data

8: 000A 01A5 CLRF __fptr_1_data+D'1'

9: 000B 3014 MOVLW LOW( label1 )

10: 000C 07A4 ADDWF __fptr_1_data, F

11: 000D 3000 MOVLW HIGH( label1 )

12: 000E 1803 BTFSC STATUS,C

13: 000F 0AA5 INCF __fptr_1_data+D'1', F

14: 0010 0725 ADDWF __fptr_1_data+D'1', W

15: 0011 008A MOVWF PCLATH

16: 0012 0824 MOVF __fptr_1_data, W

17: 0013 0082 MOVWF PCL

18: 0014 label1

19: 0014 2814 GOTO label1

20: 0015 2816 GOTO label2

21: 0016 label2

22: 0016 2004 CALL a_00000

23: 0017 0827 MOVF CompTempVarRet580, W

24: 0018 00A6 MOVWF CompTempVarRet582

25: 0019 0008 RETURN

26: 001A ; AVOID CODE BOUNDARY END

27: 001A ; } __fptr function end

Lines 6 through 17 modifies the program counter to point to one of the “GOTO” entries in the table at line 19 (for ordinal 0) and 20 (for ordinal 1). Lines 22 through 25 handles the indirect call for the function ‘a’.

Phew! Better make sure not to do that too often!

Conclusion

When working with microcontrollers (particularly the PIC), it’s important to put aside all assumptions about how the C program will be translated into the underlying assembly language. Its worth looking at the output of the compiler to both better understand how the compiler works and to ensure that there are no surprises.

-Jamie

The views expressed in this blog are the views of the author. They are provided "AS IS", with no warranties, and confer no rights. Some blog entries contain fictional elements to add dramatic effect.

Disclaimer: Blog contents express the viewpoints of their independent authors and
are not reviewed for correctness or accuracy by
Toolbox for IT. Any opinions, comments, solutions or other commentary
expressed by blog authors are not endorsed or recommended by
Toolbox for IT
or any vendor. If you feel a blog entry is inappropriate,
click here to notify
Toolbox for IT.