Главная > AVR > AVR-GCC: Совмещение C и ассемблера в одном проекте

AVR-GCC: Совмещение C и ассемблера в одном проекте

19/02/2012

Как и многие, я люблю ассемблер за скорость, а также возможность полного контроля над машинным кодом и железом. Однако, писать проект целиком на ассемблере утомительно и не всегда целесообразно, особенно в тех участках, где реализуется общая логика работы, не требующая детального контроля — тут С дает существенный выигрыш. Тем не менее, при написании кода для МК довольно часто встречаются отдельные места, где возможности ассемблера действительно необходимы.

В таких случаях существуют два выхода: первый — ассемблерные вставки, второй — написание функции отдельно и целиком на ассемблере с последующим вызовом ее из С. Здесь я затрону второй метод. Во-первых, потому, что статья про вставки уже была, а, во-вторых, формат ассемблерных вставок AVR-GCC кажется мне слишком перегруженным, и потому сам я всегда действую вторым способом. Кроме того, если не хочется терять такты на вызов, фактическую ассемблерную вставку всегда легко получить, объявив функцию с атрибутом inline.

Итак, предлагаю простую задачу: написать на ассемблере функцию, которая вернет сумму своего параметра и глобальной переменной. Это позволит продемонстрировать передачу параметров, возвращение результата и доступ к символам, объявленным в С.

Итак, начнем с основной программы на С:

#include <avr/io.h>
//Подключаем заголовочный файл, в котором объявлена
//наша функция (и, если необходимо, еще что-то)
#include "asmroutine.h"

//Глобальная переменная
volatile char global_var=10;

void main(void)
{
  volatile char m=5,d;

  //Вызываем ассемблерную функцию, которая вернет
  //сумму своего параметра и глобальной переменной.
  d=asm_func(m);

  while (1);
}

Для простоты переменные и возвращаемое значение имеют тип char, а для избежания возможных неожиданностей объявлены как volatile.

В asmroutine.h нет ничего интересного, просто объявлена функция:

#ifndef _ASMROUTINE_
#define _ASMROUTINE_

//Общие определения

#ifdef __ASSEMBLER__

//Определения только для ассемблера

#endif

#ifndef __ASSEMBLER__

//Определения только для С

char asm_func(char b);

#endif

#endif

Чтобы написать функцию, вызываемую из С, необходимо знать соглашения AVR-GCC о вызове. Полностью их можно найти в соответствующем разделе FAQ AVR-libc. Здесь я приведу основные моменты:

— аргументы передаются в регистрах, с R25 (первый) по R8, то, что не влезает, передается в стеке. При этом очередной параметр всегда начинается в регистре с четным номером, т.е., наш байт в примере будет лежать в R24. Если бы это был int — он бы расположился в R25:R24, и т.д.

— результат выполнения тоже возвращается в регистрах: char в R24, int в R25:R24, и т.д.

— регистры с R18 по R27, а также R30 и R31 можно использовать как угодно, остальные без нужды лучше не трогать.

Рассмотрим asmroutine.s, содержащий код функции:

;Включаем заголовочный файл, на случай, если 
;в нем есть какие-либо необходимые определения
#include "asmroutine.h"

;Экспортируем asm_func
.global asm_func
;и говорим, что где-то есть нужный нам символ global_var
; - наша глобальная переменная
.extern global_var

;Реализуем функцию
asm_func:

; С регистрами R30 - R31 можно делать что угодно, но это,
; по-совместительству, регистровая пара Z. Загружаем в 
; нее адрес глобальной переменной

    ldi R30,lo8(global_var)
    ldi R31,hi8(global_var)

; Добываем значение глобальной переменной в регистр R25,
; с которым тоже можно делать что угодно

    ld R25,Z

; Складываем параметр, который лежит в R24,
; и значение глобальной переменной

    add R24,R25

; Все, больше ничего делать не надо, потому что 
; возвращаемое значение и должно лежать в R24, 
; выходим из подпрограммы

    ret

Компилируем и запускаем в отладчике:

Видно, что после выполнения функции значение d равно пятнадцати, т.е., все работает.

Вот и все. Как я уже говорил, такая организация кода кажется мне гораздо более наглядной и читабельной, чем мешанина букв во вставках.

Рубрики:AVR