Mixing C and Assembly Languages
- We may wish to have a C function call an assembly function.
- We may wish to have an assembly function call a C function.
- AVR studio's assembler is not able to integrate the two languages.
- Each project creates a single .hex file from a single .asm file.
- Additional .asm files have to be .included in the main .asm file.
- The GCC compiler allows both C and assembly to be used together.
- .s files (instead of .asm file) for assembly language code.
- .c files for C language code.
- .cpp files for C++ language code.
- The choice of assembler is made when an AVR studio project is created.
- Be aware that there are some syntax differences between the AVR studio's assembler and the GCC tools.
Syntax Differences with GCC
- #include <avr/io.h> instead of .include "m32def.inc"
- .segment .data instead of .dseg
- .segment .text instead of .cseg
- .asciz "message" instead of .db "message", 0 (Notice that the character array is automatically null terminated.)
- lo8() instead of HIGH()
- hi8() instead of LOW()
- File suffix of .s instead of .asm
- No need for .org directives in code segement since the compiler handles this automatically.
- .org directives in data segements are offset from the last location needed by the compiler.
- If the compiler doesn't reserve any data memory, then .org 0x0 would actually map to 0x60.
- The compiler uses data memory to store global variables and character string literals.
- Lines containing preprocessor directives that begin with a # (e.g., #include ..., #define ...) must use C/C++ style comments (cannot use semicolon).
Common Code in .s Files
Every .s file should contain the following GCC directives:
- #include <avr/io.h> — Definitions for PORTB, SREG, etc
#define _SFR_ASM_COMPAT 1 /* Not sure when/if this is needed */ #define __SFR_OFFSET 0
- Without the second line, labels like PORTB resolve to their data space address value (0x36) instead of their I/O space address value (0x16).
- Essentially this subtracts 0x20 from the data space address so that it matches up with how we used the I/O ports in AVR studio's assembler.
C Compiler's Register Usage
- The C compiler views registers in one of three ways:
- Temporary — registers whose value need not be preserved (R0, R18:27, R30:31)
- If calling assembly from C, there is no need for us to preserve the register(s) in the assembly function.
- If calling C from assembly, we should push the register(s) before calling the C function and pop the register(s) immediately following the return (since the C compiler doesn't attempt to preserve the register's value).
- Saved — registers whose value must be preserved (R2:17, R28:29).
- If calling assembly from C, we must push the register(s) onto the stack at the beginning of the assembly function and pop the register(s) off the stack just prior to returning from the assembly function.
- If calling C from assembly, there is no need for us to preserve the register(s) in the assembly function.
- R1 — Assumed to have a 0 in it.
- If calling assembly from C — the assembly function may use R1 but it must be cleared before returning from the function.
- If calling C from assembly — the assembly function must make sure that R1=0 before calling the C function.
- Temporary — registers whose value need not be preserved (R0, R18:27, R30:31)
Passing Parameters between C and Assembly
- Every C compiler has it's own rules/conventions for passing parameters to subroutines.
- Parameters may be passed via Registers and/or the stack.
- uint8_t — An unsigned 8-bit integer.
- Consider the function prototype:
uint8_t function(uint8_t i, uint8_t j);
- Parameters are passed via R25:8 (R25 to R8).
- A minimum of two registers (16 bits) are used for each parameter passed.
- Parameters are passed left to right.
- In our example,
- i would be stored in R25:24 (with the actual 8-bit value stored in R24).
- j would be stored in R23:22 (with the actual 8-bit value stored in R22).
- If the parameters passed require more memory than is available in the registers R25:8, then the stack is used to pass additional parameters.
- Return values are placed in registers beginning at R25.
- An 8-bit value gets returned in R24.
- An 16-bit value gets returned in R25:24.
- An 32-bit value gets returned in R25:22.
- An 64-bit value gets returned in R25:18.
Example Code
Consider the following trivially simple program:
// driver.c #include <avr/io.h> extern void asmfunc_calledfrom_c(uint8_t val); int main() { DDRB = 0xff; asmfunc_calledfrom_c(3); return 0; }
- asmfunc_calledfrom_c is a subroutine that will be implemented in assembly (see below).
- In order to call the subroutine from C, we need to create a function prototype so that C knows how to interface with the function.
- The extern keyword specifies that the function is defined in a different (external) file.
- When writing an assembly function that would be called by C we just use R24 as the register containing the value passed to the function.
// raw.s #define __SFR_OFFSET 0 // Use 0 for the I/O register offset #include <avr/io.h> // Defines I/O port aliases .global asmfunc_calledfrom_c ; Makes asmfunc_calledfrom_c visible in other source files .section .text ; Defines a code section asmfunc_calledfrom_c: ; Start of asmfunc_calledfrom_c subroutine out PORTB, r24 ; Send value passed to asmfunc_calledfrom_c to PORTB ret
- The .global directive makes the label visible in other source files.
When compiled and linked, the above two source files produce the following .lss file (only a portion of the file is included below):
00000092 <main>: 92: cf 93 push r28 94: df 93 push r29 96: cd b7 in r28, 0x3d ; 61 98: de b7 in r29, 0x3e ; 62 9a: e7 e3 ldi r30, 0x37 ; 55 9c: f0 e0 ldi r31, 0x00 ; 0 9e: 8f ef ldi r24, 0xFF ; 255 a0: 80 83 st Z, r24 a2: 83 e0 ldi r24, 0x03 ; 3 a4: 0e 94 59 00 call 0xb2 ; 0xb2 <asmfunc_calledfrom_c> a8: 80 e0 ldi r24, 0x00 ; 0 aa: 90 e0 ldi r25, 0x00 ; 0 ac: df 91 pop r29 ae: cf 91 pop r28 b0: 08 95 ret 000000b2 <asmfunc_calledfrom_c>: b2: 88 bb out 0x18, r24 ; 24 b4: 08 95 ret
More Example Code
Consider the following slightly more complicated program:
// driver.c #include <avr/io.h> /** * Send value passed to function to PORTB * * Implemented in assembly * * @param val Value to be output to PORTB */ extern void send_to_portb_in_asm(uint8_t val); /** * Divide value passed by 256 and return integer result. * * Implemented in assembly * * @param val Value to be divided by 256 * @return integer result of val/256 */ extern uint8_t divide_by_256_in_asm(uint16_t val); /** * A completely useless function that accepts two arguments * and writes a value to PORTB based on the relative values * of the arguments passed. * If the first argument is larger, then the value of the * first argument is written to PORTB. Otherwise, 0x00 is * written to PORTB. * * Implemented in assembly * * @param val Value to be acted upon * @param minus Value to be subtracted from val */ extern void strange_silliness_in_asm(uint8_t val, uint8_t minus); /** * Main program that demonstrates calling assembly subroutines from * C functions. */ int main() { DDRB = 0xff; send_to_portb_in_asm(3); PORTB = divide_by_256_in_asm(0x083f); strange_silliness_in_asm(32,16); return 0; } /** * Returns the absolute value of the arugment passed to the function. * * @param val A signed integer value * @return absolute value of the argument passed to the function */ uint8_t abs_in_c(int8_t val) { if(val<0) { val *= -1; } return val; }
and
// raw.s #define __SFR_OFFSET 0 // Use 0 for the I/O register offset #include <avr/io.h> // Defines I/O port aliases .global send_to_portb_in_asm ; Makes send_to_portb_in_asm visible in other source files .global divide_by_256_in_asm .global strange_silliness_in_asm .section .text ; Defines a code section ; Send value passed to subroutine to PORTB send_to_portb_in_asm: ; Start of asmfunc_calledfrom_c subroutine out PORTB, r24 ; Send value passed to asmfunc_calledfrom_c to PORTB ret ; Return number passed to subroutine divided by 256 divide_by_256_in_asm: mov r24, r25 ; Shift MSB into the LSB (divides by 256) ldi r25, 0x00 ; Clear MSB ret ; Return number passed to subroutine divided by 256 strange_silliness_in_asm: sub r24, r22 ; subtract minus (r22) from val (r24) mov r22, r24 ; copy value in r24 into r22 call abs_in_c ; call c function that returns absolute value in r24 sub r24, r22 ; subtract val from abs(val) breq equal out PORTB, 0x00 equal: out PORTB, r22 ret
- The strange_silliness_in_asm subroutine calls the abs_in_c function that is implemented in C.
- If the function is implemented in C and we want to call if from assembly we would need to:
- Load R24 with the value of the parameter to be passed.
- Call the C function.
- Relevant portions of the .lss are shown below:
00000092 <abs_in_c>: 92: 87 fd sbrc r24, 7 94: 81 95 neg r24 96: 90 e0 ldi r25, 0x00 ; 0 98: 08 95 ret 0000009a <main>: 9a: 8f ef ldi r24, 0xFF ; 255 9c: 87 bb out 0x17, r24 ; 23 9e: 83 e0 ldi r24, 0x03 ; 3 a0: 0e 94 5e 00 call 0xbc ; 0xbc <send_to_portb_in_asm> a4: 8f e3 ldi r24, 0x3F ; 63 a6: 98 e0 ldi r25, 0x08 ; 8 a8: 0e 94 60 00 call 0xc0 ; 0xc0 <divide_by_256_in_asm> ac: 88 bb out 0x18, r24 ; 24 ae: 60 e1 ldi r22, 0x10 ; 16 b0: 80 e2 ldi r24, 0x20 ; 32 b2: 0e 94 63 00 call 0xc6 ; 0xc6 <strange_silliness_in_asm> b6: 80 e0 ldi r24, 0x00 ; 0 b8: 90 e0 ldi r25, 0x00 ; 0 ba: 08 95 ret 000000bc <send_to_portb_in_asm>: bc: 88 bb out 0x18, r24 ; 24 be: 08 95 ret 000000c0 <divide_by_256_in_asm>: c0: 89 2f mov r24, r25 c2: 90 e0 ldi r25, 0x00 ; 0 c4: 08 95 ret 000000c6 <strange_silliness_in_asm>: c6: 86 1b sub r24, r22 c8: 68 2f mov r22, r24 ca: 0e 94 49 00 call 0x92 ; 0x92 <abs_in_c> ce: 86 1b sub r24, r22 d0: 09 f0 breq .+2 ; 0xd4 <equal> d2: 08 ba out 0x18, r0 ; 24 000000d4 <equal>: d4: 68 bb out 0x18, r22 ; 24 d6: 08 95 ret
Passing Arguments with Stack
Earlier it was noted that the stack is used to store argument values if there isn't sufficient space in registers R25:8. The following example contains a function, stack_pass_example, which requires the stack to pass the last two arguments.
#include <avr/io.h> /** * Example that uses the stack to pass arguments. * * Implemented in C * * @param val1 First long * @param val2 Second long * @param val3 Third long * @param val4 Fourth long * @param val5 Fifth long * @param val6 An eight bit integer * @return Nothing important */ long stack_pass_example(long val1, long val2, long val3, long val4, long val5, int8_t val6); /** * Main program that demonstrates calling assembly subroutines from * C functions. */ int main() { DDRB = stack_pass_example(1, 2, 3, 4, 5, 6); return 0; } /** * Example that uses the stack to pass arguments. * * @author t a y l o r@msoe.edu */ long stack_pass_example(long val1, long val2, long val3, long val4, long val5, int8_t val6) { DDRB = val1; DDRB = val2; DDRB = val3; DDRB = val4; DDRB = val5; DDRB = val6; return val1; }
Relevant portions of the .lss are found below:
00000092 <stack_pass_example>: 92: af 92 push r10 94: bf 92 push r11 96: cf 92 push r12 98: df 92 push r13 9a: ef 92 push r14 9c: ff 92 push r15 9e: 0f 93 push r16 a0: 1f 93 push r17 a2: cf 93 push r28 a4: df 93 push r29 a6: cd b7 in r28, 0x3d ; 61 a8: de b7 in r29, 0x3e ; 62 aa: 67 bb out 0x17, r22 ; 23 ac: 27 bb out 0x17, r18 ; 23 ae: e7 ba out 0x17, r14 ; 23 b0: a7 ba out 0x17, r10 ; 23 b2: 2d 85 ldd r18, Y+13 ; 0x0d b4: 27 bb out 0x17, r18 ; 23 b6: 29 89 ldd r18, Y+17 ; 0x11 b8: 27 bb out 0x17, r18 ; 23 ba: df 91 pop r29 bc: cf 91 pop r28 be: 1f 91 pop r17 c0: 0f 91 pop r16 c2: ff 90 pop r15 c4: ef 90 pop r14 c6: df 90 pop r13 c8: cf 90 pop r12 ca: bf 90 pop r11 cc: af 90 pop r10 ce: 08 95 ret 000000d0 <main>: d0: af 92 push r10 d2: bf 92 push r11 d4: cf 92 push r12 d6: df 92 push r13 d8: ef 92 push r14 da: ff 92 push r15 dc: 0f 93 push r16 de: 1f 93 push r17 e0: 86 e0 ldi r24, 0x06 ; 6 e2: 8f 93 push r24 e4: 85 e0 ldi r24, 0x05 ; 5 e6: 90 e0 ldi r25, 0x00 ; 0 e8: a0 e0 ldi r26, 0x00 ; 0 ea: b0 e0 ldi r27, 0x00 ; 0 ec: bf 93 push r27 ee: af 93 push r26 f0: 9f 93 push r25 f2: 8f 93 push r24 f4: 94 e0 ldi r25, 0x04 ; 4 f6: a9 2e mov r10, r25 f8: b1 2c mov r11, r1 fa: c1 2c mov r12, r1 fc: d1 2c mov r13, r1 fe: 83 e0 ldi r24, 0x03 ; 3 100: e8 2e mov r14, r24 102: f1 2c mov r15, r1 104: 01 2d mov r16, r1 106: 11 2d mov r17, r1 108: 22 e0 ldi r18, 0x02 ; 2 10a: 30 e0 ldi r19, 0x00 ; 0 10c: 40 e0 ldi r20, 0x00 ; 0 10e: 50 e0 ldi r21, 0x00 ; 0 110: 61 e0 ldi r22, 0x01 ; 1 112: 70 e0 ldi r23, 0x00 ; 0 114: 80 e0 ldi r24, 0x00 ; 0 116: 90 e0 ldi r25, 0x00 ; 0 118: 0e 94 49 00 call 0x92 ; 0x92 <stack_pass_example> 11c: 67 bb out 0x17, r22 ; 23 11e: 0f 90 pop r0 120: 0f 90 pop r0 122: 0f 90 pop r0 124: 0f 90 pop r0 126: 0f 90 pop r0 128: 80 e0 ldi r24, 0x00 ; 0 12a: 90 e0 ldi r25, 0x00 ; 0 12c: 1f 91 pop r17 12e: 0f 91 pop r16 130: ff 90 pop r15 132: ef 90 pop r14 134: df 90 pop r13 136: cf 90 pop r12 138: bf 90 pop r11 13a: af 90 pop r10 13c: 08 95 ret
- The values passed to stack_pass_example are placed in the correct locations in main:
- val6 (6) placed on stack first.
- val5 (5) placed on stack next (this involves 4 PUSHes since it is of type long).
- val4 (4) place in R13:10 (low byte in R10).
- val3 (3) place in R17:14 (low byte in R14).
- val2 (2) place in R21:18 (low byte in R18).
- val1 (1) place in R25:22 (low byte in R22).
- The CALL instruction causes two more bytes to be pushed onto the stack and the stack_pass_example to be executed.
- The stack_pass_example does a lot more pushing, but the location of val5 and val6 is remembered.
- The Y pointer is set to the top of the stack and val5 and val6 are accessed via Y offset by the appropriate amount.