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 0×60.
    • 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 (0×36) instead of their I/O space address value (0×16).
  • Essentially this subtracts 0×20 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.

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.
ce2810/candasm.txt · Last modified: 2009/06/03 11:22 (external edit)
 

This website is not owned or managed by the Milwaukee School of Engineering.

© 2003-2010 Dr. Christopher C. Taylor, et. al. • Office: L-343 • Phone: 277-7339 • npǝ˙ǝosɯ@ɹolʎɐʇ • -> RSS <-