AVR035
 

Эффективное программирование на Си AVR-микроконтроллеров

Отличительные особенности:

  • Доступ к ячейкам памяти ввода-вывода
  • Адресация периферийных устройств ввода-вывода с интерфейсом по типу памяти
  • Доступ к данным во флэш-памяти
  • Доступ к данным в ЭСППЗУ
  • Создание файлов данных ЭСППЗУ
  • Эффективное использование переменных и типов данных
  • Использование битовых полей и масок
  • Использование макросов и функций
  • 18 способов снижения размера кода
  • 5 путей снижения требований к размеру ОЗУ
  • Правила отладки программы
  • Содержание обновлено с учетом поддержки компиляторов IAR версии 2

Введение:

Язык высокого уровня Си непрерывно увеличивает свою популярность для программирования микроконтроллеров. Преимуществами использования Си по сравнению с Ассемблером следующие: сокращенное время проектирования, более простые поиск ошибок и переносимость, а также легкость повторного применения. Главные недостатки: более громоздкий результирующий код и сниженное быстродействие. Для компенсации данных недостатков в архитектуре AVR предусмотрена эффективная дешифрация и выполнение инструкций, которые наиболее часто генерируются Си-компилятором.

Разработка Си-компилятора выполнялась компанией IAR systems еще до завершения составления технических требований к AVR-архитектуре и набору инструкций. В результате тесного сотрудничества команды разработчиков компилятора и команды разработчиков микроконтроллеров AVR является микроконтроллер, для которого сгенерированный код характеризуется высокой эффективностью и высокими техническими характеристиками.

В данных "Рекомендациях…" описывается, как использовать преимущества архитектуры AVR и инструментальных средств для более эффективного программирования на Си в отличие от других микроконтроллеров.

Оптимизированная архитектура под Си-код

32 рабочих регистра - один из ключей к эффективности Си-кода. Данные регистры имеют то же назначение, что и традиционный аккумулятор, за исключением того, что их 32. За один машинный цикл AVR может связать два произвольных регистра из Файла регистров с АЛУ, выполнить действие и записать результат обратно в Файл регистров.

После сохранения данных в 32 рабочих регистрах нет необходимости переносить данные в память и из памяти между арифметическими инструкциями. Некоторые регистры могут объединиться в 16-разрядные указатели, с помощью которых более эффективно выполняется доступ к данным в памяти программ и памяти данных. Для адресации к памяти большого размера указатели могут объединяться с третьим 8-разрядным регистром для получения 24-разрядного указателя, который способен адресовать 16 Мбайт данных без разбиения на страницы.

Режимы адресации

Архитектура AVR содержит четыре указателя памяти, которые адресуют память программ и память данных. Указатель стека (SP) предназначен для запоминания адреса возврата после завершения функции. Си-компилятор назначает первый указатель как параметр стека. Три оставшихся указателя используются Си-компилятором для чтения и записи данных. Ниже показан пример эффективного использования указателей при программировании на Си.

  char *pointer1 = &table[0];
  char *pointer2 = &table[49];
  *pointer1++ = *--pointer2;

Это приводит к генерации следующего ассемблерного кода:

  LD R16,-Z ; предварительный декремент Z-указателя и чтение данных
  ST X+,R16 ; запись данных и последующий инкремент указателя

Четыре способа адресации с помощью указателей приведены ниже. Все действия с указателями являются однословной инструкцией, которая выполняется за два машинных цикла.

  1. Косвенная адресация. Используется для адресации массивов и переменных-указателей.
    *pointer = 0x00;
  2. Косвенная адресация со смещением: осуществляет доступ к элементам структуры путем указания на первый элемент и прибавления смещения, при этом, значение указателя не меняется. Также используется для адресации переменных при программном доступе к стеку и массиву.
  3. Косвенная адресация с последующим инкрементом. Используется для повышения эффективности адресации массивов и переменных-указателей с инкрементом после завершения доступа.
    *pointer++ = 0xFF;
  4. Косвенная адресация с предварительным декрементом. Также используется для эффективной адресации массивов и переменных-указателей с декрементированием значения указателя до начала доступа.
    *--pointer = 0xFF

Указатели также используются для осуществления доступа к флэш-памяти программ. В дополнение к косвенной адресации с указателями, доступ к данным в памяти может выполняться с помощью прямой адресации. Таким способом можно адресоваться к любой области памяти с помощью двухсловной инструкции.

Поддержка 16 и 32-разрядных переменных

В набор инструкций AVR входят несколько специальных инструкций для обработки 16-разрядных чисел. К ним относятся сложение/вычитание константы и слова (ADIW, SBIW). Арифметические операции и сравнение 16-разрядных чисел выполняются с помощью двух инструкций, которые выполняются за два машинных цикла. 32-разрядные арифметические операции и сравнение выполняются с помощью четырех инструкций, которые выполняются за четыре машинных цикла. Такие характеристики превышают характеристики большинства 16-разрядных процессоров.

Си-код для AVR

Инициализация указателя стека

После подачи питания или сброса необходимо инициализировать указатель стека до вызова какой-либо функции. Командный файл линкера определяет позицию и размер указателя стека. Конфигурация размеров памяти и установка указателя стека объясняется в рекомендациях по применению "AVR032: Модификация командного файла линкера ".

Доступ к ячейкам памяти ввода-вывода

Доступ к памяти ввода-вывода микроконтроллера AVR легко выполняется на Си. Все регистры в памяти ввода-вывода объявляются в файле-заголовке, который обычно называется "ioxxxx.h", где xxxx указывает на тип микроконтроллера AVR. Ниже показаны примеры осуществления доступа к ячейкам памяти ввода-вывода. Под каждой строкой кода на Си показан генерируемый для нее ассемблерный код.

#include <io8515.h> /* включить файл-заголовок с символьными именами */
__C_task void main(void)
{
char temp              ;/* объявление временной переменной */
                        /*Для чтения и записи в любой регистр ввода-вывода */
temp = PIND            ;/* Считываем PIND в переменную */
// IN R16,LOW(16)      ; Чтение из памяти ввода-вывода
TCCR0 = 0x4F           ;/* Запись значения в область ввода-вывода */
// LDI R17,79          ; Загрузка значения
// OUT LOW(51),R17     ; Запись в память ввода-вывода
                        /*Установка и сброс одного бита */
PORTB |= (1<<PIND2); /* PIND2 - номер линии ввода-вывода (0..7) порта */
// SBI LOW(24),LOW(2)    ; Установка бита в памяти ввода-вывода
ADCSR &= ~(1<<ADEN); /* Сброс бита ADEN  в регистре ADCSR */
// CBI LOW(6),LOW(7)     ; Сброс бита в памяти ввода-вывода
                        /* Установка и сброс с помощью битовых масок */
DDRD |= 0x0C             ; /* Установка битов 2 и 3 в регистре DDRD */
// IN R17,LOW(17)        ; Чтение памяти ввода-вывода
// ORI R17,LOW(12)       ; Модификация
// OUT LOW(17),R17       ; Запись в память ввода-вывода
ACSR &= ~(0x0C)          ; /* Сброс бита 2 и 3 в регистре ACSR */
// IN R17,LOW(8)         ; Чтение из памяти ввода-вывода
// ANDI R17,LOW(243)     ; Модификация
// OUT LOW(8),R17        ; Запись в память ввода-вывода
                      /* Проверка, установлен или сброшен один бит? */
if(USR & (1<<TXC)) /* Проверка, установлен флаг Tx УАПП? */
{
PORTB |= (1<<PB0)  ;
// SBIC LOW(11),LOW(6)   ; Пропуск инструкции, если бит в памяти В/В сброшен
// SBI LOW(24),LOW(0)
while(!(SPSR & (1<<WCOL))) ;/* Ожидание установки флага WCOL */
// ?0003:SBIS LOW(14),LOW(6); Пропуск инструкции, если бит установлен
// RJMP ?0003
/* Проверка на равность содержимого регистра В/В битовой маске */
if(UDR & 0xF3) /* Проверка на неравность нулю лог. И между регистром UDR и 0xF3
{
}
// IN R16,LOW(12)        ; Чтение из памяти ввода-вывода
// ANDI R16,LOW(243)     ; Логическое "И" между значениями
// BREQ ?0008            ; Переход, если равно
//?0008:
}
/* Установка и сброс бит в регистрах ввода-вывода с помощью макроса */
#define SETBIT(ADDRESS,BIT) (ADDRESS |= (1<<BIT))
#define CLEARBIT(ADDRESS,BIT) (ADDRESS &= ~(1<<BIT))
/* Макрос для проверки одного бита в области ввода-вывода */
#define CHECKBIT(ADDRESS,BIT) (ADDRESS & (1<<BIT))
/* Пример использования */
if(CHECKBIT(PORTD,PIND1)) /* Проверка, PIND1 = 1? */
{
CLEARBIT(PORTD,PIND1); /* Сброс PIND1 порта D*/
}
if(!(CHECKBIT(PORTD,PIND1))) /* Проверка, PIND1 = 0? */
{
SETBIT(PORTD,PIND1); /* Установка PIND1 порта D */
}

Адресация периферийных устройств ввода-вывода с интерфейсом по типу памяти

Некоторые микроконтроллеры AVR содержат интерфейс внешней памяти данных. Его можно использовать для доступа к внешнему ОЗУ, ЭСППЗУ или устройствам ввода-вывода, подключаемых как внешняя параллельная память. Ниже показаны примеры объявления, записи и чтения внешней памяти:

#include <io8515.h>
#define reg (* (char *) 0x8004)/* Объявление адреса внешней памяти */
__C_task void main(void)
{
char temp;  /* Локальная временная переменная */
reg = 0x05; /* Запись значения по адресу внешней памяти */
temp = reg; /* Чтение из внешней памяти */
}

Если необходимо адресоваться к следующему адресу внешней памяти, то наиболее эффективно объявить константу-указатель и прибавлять к нему значение смещения. Ниже приведен пример реализации такого подхода. Сгенерированный для каждой инструкции ассемблерный код показан курсивом.

/* Определение адресов внешней памяти */
#define data 0x0003
#define address_high 0x0002
#define address_low 0x0001
__C_task void main(void)
{
/* Начальный адрес карты памяти */
unsigned char *pointer = (unsigned char *) 0x0800;
// LDI R30,LOW(0)             ; Инициализация Z-указателя
// LDI R31,8
*(pointer+address_low) |= 0x40; /* Чтение и модификация одного адреса */
// LDD R18,Z+1                ;Загрузка переменной
// ORI R18,LOW(64)            ; Модификация
// STD Z+1,R18                ; Сохранение обратно
*(pointer+address_high) = 0x00; /* Запись адреса */
// STD Z+2,R30                ; Запись 0
PORTC = *(pointer+data)       ; /* Чтение адреса */
// LDD R16,Z+3                ; Загрузака переменной
// OUT LOW(21),R16            ; Вывод в порт
}

Обратите внимание, что Z-указатель инициализируется перед доступом к памяти, а инструкции LDD и STD (чтение и запись со смещением) используются для доступа к данным. LDD и STD - однословные инструкции, которые выполняются за два машинных цикла. Указатель загружается однократно. Ячейки внешней памяти могут быть объявлены, как volatile, что указывает на возможность аппаратного изменения положения ячейки и что доступ не будет удален оптимизацией.

Доступ к данным В ЭСППЗУ

Встроенное в микроконтроллеры AVR ЭСППЗУ можно считывать и записывать в процессе нормальной работы. Макросы для чтения и записи ЭСППЗУ входят в файл ina90.h компилятора IAR. Следующий макрос обычно определяется для чтения и записи ЭСППЗУ:

#define _EEGET(VAR,ADR)/* Чтение данных по адресу ADR из ЭСППЗУ в переменную VAR
*/ \
{ \
while(EECR & 0x02)    ; /* Ожидание готовности ЭСППЗУ */ \
EEAR = (ADR)          ; /* Запись регистра адреса ЭСППЗУ */ \
EECR |= 0x01          ; /* Установка строба чтения */ \
(VAR) = EEDR          ; /* Считывание данных в переменную */ \
}
#define _EEPUT(ADR,VAL) /* Запись данных из VAL в ЭСППЗУ по адресу ADR*/\
{\
while(EECR&0x02)      ; /* Ожидание готовности ЭСППЗУ */ \
EEAR = (ADR)          ; /* Запись регистра адреса ЭСППЗУ */ \
EEDR = (VAL)          ; /* Запись регистра данных ЭСППЗУ */ \
EECR |= 0x04          ; /* Установка главного сигнала разрешения записи */ \
EECR |= 0x02          ; /* Установка строба записи  */ \
}

Пример кода для чтения и записи ЭСППЗУ с использованием предварительно определенного макроса:

#include <io8515.h>
#include <ina90.h>
#define EE_ADDRESS 0x010 /* Определение константы адреса для данных ЭСППЗУ */
__C_task void main(void)
{
char temp; /* Локальная переменная для временного хранения */
_EEGET(temp,EE_ADDRESS); /* Чтение данных из ЭСППЗУ */
temp += UDR;/*Прибавить содержимое регистра данных УАПП к временной переменной*/
_EEPUT(EE_ADDRESS,temp); /* Запись данных в ЭСППЗУ */
}

Обратите внимание, что если прерывания разрешены, то в процессе записи ЭСППЗУ их необходимо запретить во избежание истечения длительности действия главного бита разрешения записи EEMWE. Если доступ к ЭСППЗУ выполняется внутри процедуры прерывания, то прерывания необходимо запретить также перед чтением ЭСППЗУ во избежание повреждения содержимого регистра адреса ЭСППЗУ.

Создание файлов данных ЭСППЗУ (IAR v2)

Доступ к переменным в ЭСППЗУ существенно улучшен в версии 2 компилятора IAR. В данном разделе объясняется использование переменных в ЭСППЗУ. Использование переменных в ЭСППЗУ в более старых версиях компилятора объясняется в следующем разделе.

Для использования метода, описанного ниже, необходимо активизировать опцию "Use inbuilt EEPROM" (использование встроенного ЭСППЗУ) и указать его размер в настройках проекта. Сегмент EEPROM_I должен быть определен в файле линкера.

Переменные ЭСППЗУ должны быть объявлены как глобальные переменные.

Пример объявления переменной в ЭСППЗУ:

__eeprom volatile unsigned char ee_var = 0xff;

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

__C_task void main(void)
{
unsigned char temp;
temp = ee_var;
// LDI R20,LOW(ee_var)
// LDI R21,(ee_var) >> 8
// CALL __eeget8_16
PORTB = temp;
// OUT 0x18,R16
ee_var = TCNT0;
// IN R16,0x32
// LDI R20,LOW(ee_var)
// LDI R21,(ee_var) >> 8
// CALL __eeput8_16
}

После объявления переменных в ЭСППЗУ с указанием начальных значений они могут быть извлечены в отдельный hex-файл, а затем запрограммированы в ЭСППЗУ с помощью любого AVR-программатора. Для этого в состав компилятора IAR входит отдельная программа. Она называется "postlink.exe" и находится в директории "\bin" после инсталляции IAR. Помощь по использованию этой программы вызывается после ее запуска без параметров в командной строке.

Для использования программы postlink.exe необходимо настроить выходной формат XLINK на генерацию формата simple. Данная опция находится в опциях проекта в категории XLINK на закладке Output.

Создание файла данных в ЭСППЗУ (IAR v1)

В данном разделе описан метод доступа к ЭСППЗУ с помощью компилятора IAR версии 1. В некоторых случаях это может быть удобно для размещения исходных данных в ЭСППЗУ и доступа к ним с помощью Си-кода. Инструментальные средства IAR могут использоваться для генерации начальных данных для ЭСППЗУ. С помощью файлов-заголовков для определения структур можно гарантировать адресуемость ЭСППЗУ из Си-кода.

Данные для ЭСППЗУ и код программы создаются в разных проектах, которые должны компилироваться и линковаться раздельно. Файл-заголовок с описанием структуры памяти ЭСППЗУ используется обоими проектами, чтобы гарантировать возможность ссылаться на данные по их символическим именам.

Пример

Далее рассмотрим пример, в котором полагаются следующие установки ЭСППЗУ:

  1. Массив символов (100 байт).
  2. Целое число (два байта).
  3. Два беззнаковых символа (два байта каждый).

Файл-заголовок ЭСППЗУ

Файл-заголовок ЭССПЗУ подключается как к программе с определением данных ЭСППЗУ, так и к Си-программе, осуществляющий доступ к ЭСППЗУ. Файл-заголовок ЭСППЗУ определяется следующим образом:

#define EEPROMADR(x) (unsigned int) (&((TypeEEPROMStruct *)0x0000)->x)
typedef struct
{
char cArray[100]; /* Массив символов */
int iNumber; /* Целое число */
unsigned char uMinorVersion; /* Первый беззнаковый символ */
unsigned char uMajorVersion; /* Второй беззнаковый символ */
} TypeEEPROMStruct; /* Только тип имени */

Директива #define содержит макрос, используемый в Си-программе для получения доступа к структуре переменных. Он содержит константу-указатель (0x0000). Для замены содержимого ЭСППЗУ данный указатель необходимо изменить (это также может потребовать изменение файла линкера ЭСППЗУ, см. ниже).

Файл программирования ЭСППЗУ

Файл программирования ЭСППЗУ (eeprom.c) содержит инициализацию структуры, определенной в файле-заголовке ЭСППЗУ.

#include "eeprom.h" /* содержит определение типа структуры */
#pragma memory=constseg(EEPROM) /* Делаем его именованным сегментом */
const TypeEEPROMStruct __EEPROMInitializationData =
{"Testing ", /* Инициализация cArray */
0x100 , /* Инициализация iNumber */
0x10 , /* Инициализация uMinorVersion */
0xFF }; /* Инициализация uMajorVersion */

Файл линкера ЭСППЗУ

Для файла программирования ЭСППЗУ требуется очень простой файл линкера (eeprom.xcl):

-ca90 -! Определение типа ЦПУ (AVR) -!
-Z(CODE)EEPROM=0-1FF -! Адресное пространство ЭСППЗУ (внутренняя память ЭСППЗУ -!

Адресный диапазон установлен равным 0-1FF (например, для AT90S8515) и при необходимости может быть откорректирован под используемый тип микроконтроллера. Имя сегмента EEPROM, что соответствует строке #pragma memory=constseg(EEPROM) в исходном файле "eeprom.c".

Для замены содержимого ЭСППЗУ необходимо изменить стартовый адрес в сегменте ЭСППЗУ (см. также комментарий в файле-заголовке ЭСППЗУ).

Создание файла данных ЭСППЗУ в формате Intel-Hex

Для генерации Intel-Hex файла с этим содержим требуются следующие команды:

icca90 eeprom.c (обратите внимание, что -v1 -ms и т.д. не имеют значения)
xlink -f eeprom.xcl -B -Fintel-standard eeprom.r90 -o eeprom.hex

В процессе линкования генерируется следующее сообщение:

Error[46]: Undefined external ?CL0T_1_41_L08 referred in eeprom ( eeprom.r90)

Си-программа ссылается на фиктивный символ, чтобы удостовериться, что откомпилированная программа связана с корректной версией библиотеки. Поскольку мы не связывались с какой-либо библиотекой, игнорируем данное сообщение, а опция -B гарантирует генерацию файла "eeprom.hex" даже при возникновении ошибки. Альтернативно, файл может быть линкован со следующими опциями:

xlink -f eeprom.xcl -D?CL0T_1_41_L08=0 -Fintel-standard eeprom.r90 -o eeprom.hex

Символ определения зависит от версии процессора (-v0, -v1 и т.д.), модели памяти (-mt, -ms, etc) и версии компилятора, т.к. символ может варьироваться между разными инсталляциями.

Ниже приведено содержимое сгенерированного файла "eeprom intel-hex" (eeprom.h):

:1000000054657374696E67202000000000000000D2
:1000100000000000000000000000000000000000E0
:1000200000000000000000000000000000000000D0
:1000300000000000000000000000000000000000C0
:1000400000000000000000000000000000000000B0
:1000500000000000000000000000000000000000A0
:0800600000000000000110FF88
:00000001FF

Доступ к структуре данных ЭСППЗУ из Си-программы

В следующей программе используется определенная структура ЭСППЗУ для доступа к последнему:

(main.c):
#include "eeprom.h" /* Подключаем структуру и макрос */
#include <io8515.h> /* Определяем ячейки ЭСППЗУ */
void error(void) /* Процедура error для поиска ошибок */
{
for(;;) /* ничего не делаем  */
;
}
void C_task main(void)
{
int i; /* Используется для считывания целого числа */
EEAR = EEPROMADR(cArray); /* Установка адреса первого элемента */
EECR |=1; /* Инициируем чтение ЭСППЗУ */
if(EEDR != 'T') /* Проверяем на инициализацию */
error(); /* Если инициализация некорректная, то ошибка */
EEAR = EEPROMADR(iNumber); /* Установка адреса 2-го элемента */
EECR |=1; /* Инициируем чтение ЭСППЗУ */
i = EEDR ; /* Установка младшего байта целого числа*/
EEAR = EEPROMADR(iNumber)+1; /* Установка адреса 2-го байта */
EECR |=1; /* Инициируем чтение ЭСППЗУ */
i |= EEDR<<8; /* Установка старшего байта целого числа */
if(i!=0x100) /* Проверка на инициализацию */
error(); /* Если нет, то ошибка */
EEAR = EEPROMADR(uMinorVersion); /* Установка адреса для 4 элемента */
EECR |=1; /* Чтение ЭСППЗУ */
if(EEDR != 0x10) /* Проверка на инициализацию */
error(); /* Если нет, то ошибка */
EEAR = EEPROMADR(uMajorVersion); /* Установка адреса 3-го элемента */
EECR |=1; /* Чтение ЭСППЗУ */
if(EEDR != 0xFF) /* Проверка на инициализацию */
error(); /* Если нет, то ошибка */
for (;;)
; /* ни чего не делаем (завершено успешно) */
}

Программа может компилироваться и выполняться в AVR Studio®. Файл "eeprom.hex" должен быть загружен в память ЭСППЗУ перед выполнением программы, иначе она перейдет в процедуру обработки ошибки error(). ЭСППЗУ загружается hex-файлом с помощью функции "File-> Up/Download memories" после загрузки программы.

Переменные и типы данных

Типы данных

Поскольку AVR это 8-разрядный микроконтроллер, то использование 16 и 32 разрядных переменных должно быть ограничено и использовать их нужно только там, где это необходимо. В следующем примере показан пример создания 8 и 16-разрядного циклического счетчика:

8-разрядный счетчик

unsigned char count8 = 5; /* Объявление переменной, присвоение значения */
// LDI R16,5 ; Загрузка начального значения
do /* Начало цикла */
{
}while(--count8); /* Декремент циклического счетчика и проверка на ноль */
// ?0004:DEC R16 ; Декремент
// BRNE ?0004 ; Переход, если не равно

16-разрядный счетчик

unsigned int count16 = 6; /* Объявление переменной, присвоение значения */
// LDI R24,LOW(6) ; Загрузка младшего байта
// LDI R25,0 ;Загрузка старшего байта
do /* Начало цикла */
{
}while(--count16); /* Декремент циклического счетчика и проверка на ноль */
// ?0004:SBIW R24,LWRD(1) ; Вычитание 16-разрядного счетчика
// BRNE ?0004 ; Переход, если ноль

Таблица 1. Переменные и размер кода (1)

Переменная Размер кода, байт
8 разр. 6
16 разр. 8

Прим.:

    1. Всегда используйте минимально возможный тип переменной. Это особенно важно для глобальных переменных. Эффективность использования переменных

Си-программа разделена на множество функций, которые вызывают малые или большие задачи. Функции принимают данные через параметры и могут также возвращать данные. Переменные, которые объявляются внутри функций, называются локальными переменными. Переменные, которые объявлены вне функций, называются глобальными переменными. Переменные, являющиеся локальными, но которые необходимо сохранить перед каждым вызовом функции, необходимо объявить как статические локальные переменные.

Глобальные переменные, объявленные вне функций, размещаются в статическом ОЗУ. Область статического ОЗУ зарезервирована для глобальных переменных и не может использоваться для других целей. Чрезмерное количество глобальных переменных ухудшает читабельность кода и усложняет его модификацию.

Локальные переменные по возможности связываются с регистрами после их объявления. Локальные переменные хранятся в том же регистре до завершения функции или до следующего доступа к нему. Глобальные переменные необходимо загрузить из статического ОЗУ в рабочие регистры перед доступом к ним.

Ниже приведен пример, который иллюстрирует различия в размере кода и быстродействии для локальных и глобальных переменных.

char global; /* Это глобальная переменная */
__C_task void main(void)
{
char local; /* Это локальная переменная */
global -= 45; /* Вычитание из глобальной переменной */
// LDS R16,LWRD(global) ; Загрузка переменной из статического ОЗУ в регистр R16
// SUBI R16,LOW(45) ; Выполняем вычитание
// STS LWRD(global),R16 ; Сохранение данных обратно в стат. ОЗУ
local -= 34; /* Вычитание из локальной переменной */
// SUBI R16,LOW(34) ; Непосредственное вычитание константы из регистра R16
}

Обратите внимание, что инструкции LDS и STS (непосредственное чтение и запись статического ОЗУ) используются для доступа к переменным в статическом ОЗУ. Они являются двухсловными инструкциями и выполняются за 2 машинных цикла.

Таблица 2. Размер кода и время его выполнения для переменных

Переменная Размер кода, байт Время выполнения, циклов
Глобальная 10 5
Локальная 2 1

Локальная статическая переменная загружается в рабочий регистр в начале функции, а затем обратно записывается в статическое ОЗУ в конце функции. Статические переменные, таким образом, позволяют более оптимально расходовать память программ по сравнению с глобальными, если доступ к переменной внутри функции выполняется более одного раза.

Для ограничения использования глобальных переменных функции могут вызываться с параметрами и возвращать значение, которое обычно используется в Си. До двух параметров простых типов данных (char, int, long, float, double) проходят между функциями в регистрах R16 - R23. Более двух параметров и комплексных типов данных (массивы, структуры) или размещаются в программном стеке или проходят между функциями как указатели на области статического ОЗУ.

Если глобальные переменные требуются, то они должны быть собраны в структуры. Это делает возможным для Си-компилятора адресоваться к ним косвенно. В следующем примере показано сравнение генерации кода с использованием глобальной переменной и глобальной структуры.

typedef struct
{
char sec;
}t;
t global /* Объявление глобальной структуры */
char min;
__C_task void main(void)
{
t *time = &global;
// LDI R30,LOW(global) ; Инициализация Z-указателя
// LDI R31,(global >> 8) ; Инициализация старшего байта Z-указателя
if (++time->sec == 60)
{
// LDD R16,Z+2 ; Загрузка со смещением
// INC R16; Инкремент
// STD Z+2,R16 ; Запись со смещением
// CPI R16,LOW(60) ; Сравнение
// BRNE ?0005 ; Переход, если не равно
}
if ( ++min == 60)
{
// LDS R16,LWRD(min) ; Прямая загрузка из СОЗУ
// INC R16 ; Инкремент
// STS LWRD(min),R16 ; Прямая запись в статическое ОЗУ
// CPI R16,LOW(60) ; Сравнение
// BRNE ?0005 ; Переход, если не равно
}
}

При доступе к глобальной переменной как к структуре компилятор использует Z-указатель и инструкции LDD и STD (чтение/запись со смещением). Если же доступ к глобальной переменной выполняется без структуры, компилятор использует инструкции LDS и STS (прямая загрузка/запись статического ОЗУ). В таблице 3 приведены различия кодов.

Таблица 3. Размер кода для глобальных переменных

Переменная Размер кода, байт
Структурированная 10
Не структурированная 14

Сюда не входит инициализация Z-указателя (четыре байта) для глобальной структуры. Таким образом, при доступе к одному байту размер кода будет такой же, а, если структура состоит из двух байт и более, то эффективность расходования памяти программ будет выше при доступе к глобальным переменным как к структуре.

Оптимизация глобальных флагов

В некоторых приложениях используются глобальные флаги для управления программным потоком. Размещение их в глобальных переменных не эффективно в AVR-архитектуре, т.к. перед тестированием флага необходимо загрузить значение в регистр. Для оптимизации тестирования флага его необходимо или разместить в отдельном регистре или в неиспользуемой области ввода-вывода.

Прим.: для размещения глобальной переменной в отдельном регистре необходимо, чтобы был назначен опциях компилятора. Это можно сделать только во 2-ой версии компилятора IAR.

Неиспользуемые области в памяти ввода-вывода могут использоваться для хранения глобальных переменных, если какие-то периферийные устройства не используются. Например, если УАПП не используется, то для таких целей можно использовать регистр скорости связи (UBRR), а, если не задействовано ЭСППЗУ, то для хранения глобальных переменных можно использовать регистр данных (EEDR) и регистр адреса ЭСППЗУ (EEAR).

Доступ к памяти ввода-вывода выполняется очень эффективно, а ячейки с адресам ниже 0x1F особенно идеальны для хранения флагов, т.к. являются битноадресуемыми. Область ввода-вывода выше 0x1F несколько хуже по эффективности доступа, но лучше доступа глобальных переменных в статическом ОЗУ.

Далее показываются различия в размерах кодов при размещении глобальных флагов в переменных в статическом ОЗУ, области ввода-вывода и отдельном регистре.

typedef struct bitfield{// Структура битового поля
unsigned char bit0:1;
unsigned char bit1:1;
unsigned char bit2:1;
}bitfield;

Глобальные флаги в ОЗУ:

 bitfield global_flag; // Битовое поле в глобальной переменной
...
global_flag.bit1 = 1;
// LDI R30,LOW(global_flag)
// LDI R31,(global_flag) >> 8
// LD R16,Z
// ORI R16,0x02
// ST Z,R16

Размер кода: 10 байт

Глобальные флаги в регистре R15

Обратите внимание, что битовые поля не используются как регистровые переменные.

__no_init __regvar unsigned char reg_flag@ 15;
//Глобальный регистр R15
...
reg_flag |= 0x01;
// SET
// BLD R15,0

Размер кода: 4 байта

Глобальные флаги в свободных регистрах ввода-вывода (адреса выше 0x1F)

__no_init volatile bitfield high_io_flag@0x55;
//Битовое поле ввода-вывода выше 0x1F,обратите внимание на смещение 0x30 к адресу
...
high_io_flag.bit2 = 1;
// IN R16,0x35
// ORI R16,0x04
// OUT 0x35,R16

Размер кода: 6 байт

Глобальные флаги в свободных регистрах ввода-вывода (адреса ниже 0x1F)

__no_init volatile bitfield low_io_flag@0x35;
// Битовое поле в области ввода-вывода ниже 0x1F
...
low_io_flag.bit1 = 1;
// SBI 0x15,0x01

Размер кода: 2 байта

Регистры универсального ввода-вывода

Преимущества свободных регистров ввода-вывода увеличены во всех новых микроконтроллерах AVR. Начиная с микроконтроллеров ATmega169, ATmega48/88/168 и ATtiny2313 во все микроконтроллеры AVR входят несколько регистров ввода-вывода общего назначения и выше и ниже адресной границы 0x1F. Данные регистры используются как любые другие свободные регистры ввода-вывода и могут потенциально улучшить быстродействие и уменьшить размер кода.

Таблица 4. Размер кода для некоторых общих действий

Действие Данные в статическом ОЗУ Данные в памяти ввода-вывода выше 0x1F Данные в файле регистров Данные в памяти ввода-вывода ниже 0x1F
Установка/сброс одного бита 10 6 4 2
Проверка одного бита 6 4 2 2
Установка/сброс нескольких бит 10 6 4 6
Сравнение с постоянным значением 6 4 4 4

В примерах показывается, что использование свободных ячеек ввода-вывода очень эффективно для хранения переменных флагов, которые работают побитно, при этом, использование предопределенных регистров эффективно для часто используемых переменных. Обратите внимание, что регистры блокировки для глобальных переменных ограничивают способность компилятора оптимизировать программный код. В сложных программах это может вызвать увеличение размера кода при использовании специальных регистров для хранения переменных.

Битовые поля против битовых масок

Чтобы сэкономить часть памяти для хранения данных может полезным оказаться разместить несколько однобитных флагов в одном байте. Наиболее часто используемые из этих флагов, составляют байт статуса.

Флаги можно определить либо с помощью битовых масок либо с помощью битовых полей. Ниже показан пример использования битовых масок и битовых полей для объявления байта статуса.

/* Использование битовой маски для бит статуса */
/* Определяем битовый макрос, обратите внимание, что он похож на макрос ввода-вывода */
#define SETBIT(x,y) (x |= (y)) /* установка бита y в байте x*/
#define CLEARBIT(x,y) (x &= (~y)) /* сброс бита y в байте x*/
#define CHECKBIT(x,y) (x & (y)) /* проверка бита y в байте x*/
/* Определяем маску-константу бита статуса */
#define RETRANS 0x01 /* разряд 0 : Флаг повторной передачи*/
#define WRITEFLAG 0x02 /* разряд 1 : Флаг устанавливается при необходимости записи*/
#define EMPTY 0x04 /* разряд 2 : Флаг пустоты буфера */
#define FULL 0x08 /* разряд 3 : Флаг заполнения буфера */
__C_task void main(void)
{
char status; /* Объявляем байт статуса */
CLEARBIT(status,RETRANS); /* Сброс RETRANS и WRITEFLAG*/
CLEARBIT(status,WRITEFLAG);
/*Проверка, если флаг RETRANS сброшен */
if (!(CHECKBIT(status, RETRANS)))
{
SETBIT(status,WRITEFLAG);
}
}

Битовые маски очень эффективно обрабатываются Си-компилятором, если переменная статуса объявлена как локальная переменная в пределах используемой функции. Альтернативно, используйте незадействованные области ввода-вывода с битовыми масками и битовыми полями.

Битовые поля могут определяться в СОЗУ и памяти ввода-вывода. Глобальные переменные в регистрах допускают только простые переменные. Для применения битовых инструкций над глобальными переменными в регистрах используйте битовые маски.

Стандарт ANSI не определяет, как битовые поля располагаются в байте, т.е. битовое поле в одном компиляторе может быть размещено в старшем разряде, а в другом - в младшем. Используя битовые масками, пользователь полностью контролирует процесс распределения бит внутри переменных.

Инициализация глобальных переменных

Код инициализации при запуске (startup) выполняется сразу после сброса и инициализирует все глобальные переменные. Глобальным переменным, для которых нет начальных значений, должно быть присвоено значение 0.

Пример:

unsigned char global_counter = 100;
unsigned int global_flags ;// Во все глобальные переменные записывается 0

Как правило, нет необходимости инициализировать глобальные переменные в процедурах инициализации. Для максимальной плотности кода все глобальные переменные должны быть инициализированы при объявлении Альтернативно, все переменные могут быть инициализированы в отдельной процедуре и код инициализации при запуске может быть удален.

Доступ к флэш-памяти

Как правило, для объявления констант используется следующий способ:

const char max = 127;

Константа копируется из флэш-памяти в статическое ОЗУ при запуске программы и остается там остальное время ее выполнения. Таким образом, теряется часть статического ОЗУ. Чтобы сэкономить эту часть ОЗУ, константы необходимо хранить во флэш-памяти и при необходимости загружать их из нее:

flash char max = 127;
flash char string[] = "Данная строка запоминается во флэш-памяти ";
__C_task void main(void)
{
char flash *flashpointer; ; Объявление указателя флэш-памяти
flashpointer = &string[0]; ; Назначение указателя к области флэш-памяти
UDR = *flashpointer; ; Считывание данных из флэш-памяти и запись в УАПП
}

Прим.: Компилятор IAR 2-ой версии использует __flash вместо flash.

Если строки записаны во флэш-памяти, как показано выше, то доступ к ним можно осуществлять непосредственно во флэш-памяти программ с помощью прямого доступа или указателей. В Си-компиляторе IAR существует целая специальная библиотека для обработки строк (см. руководство пользователя к компилятору "IAR Compiler Users Manual").

Процедуры обработки прерываний

При входе в процедуру обработки прерывания все регистры, используемые в прерывании, помещаются в стек. Для уменьшения программного кода и оптимизации быстродействия процедуры обработки прерываний должны быть малыми и желательно не использовать вложенных вызовов функций. Аргументом этому является то, что вызываемая внешняя функция дополнительно заполняется стек содержимым используемых регистров.

Функции с возвратом нескольких значений

В некоторых случаях используются глобальные переменные для возврата из функции несколько значений. Архитектура AVR позволяет возвратить до 4 байт из функции путем возврата значений во временных регистрах. Некоторые ухищрения могут использоваться для объединения нескольких переменных в одно возвращаемое значение.

Пример:

unsigned int read_io(void)
{
return (PIND<<8 | TCNT0);// Функция, возвращающая два значения
}
// IN R17,0x10// Первое значение 
// IN R16,0x32// Второе значение 
// RET

Вызов функции:

__C_task void main(void)
{
unsigned int temp;// Объявление временной переменной
temp = sum();// Чтение значения из функции
PORTB = temp>>8;// Использование только старшего байта
TCNT0 = temp&0x00FF;// Использование только младшего байта
}
// CALL sum// Вызов функции
// OUT 0x18,R17// Использование старшего байта
// OUT 0x32,R16// Использование младшего байта

Модель памяти

Самые простые микроконтроллеры AVR с размером ОЗУ до 256 байт во многих случаях могут использоваться с моделью памяти tiny. При использовании данной модели памяти доступ ко всем переменным в статическом ОЗУ выполняется с помощью 8-разрядных указателей, а не 16-разрядных. В этом случае размер кода снижается при загрузке значений указателя. Обратите внимание, что 8-разрядные указатели могут адресовать только 160 байт ОЗУ, регистровый файл и память ввода-вывода. Чтобы убедиться в использовании компилятором всех адресных диапазонов, необходимо удостовериться, что в файле линкера RSTACK и CSTACK размещены в вершине статического ОЗУ.

Управление программным потоком

Функция Main

Функция main обычно содержит основной цикл программы. В большинстве случаев функция main не вызывается из других функций и нет необходимости предварительно сохранять содержимое какого-либо регистра перед входом в нее. Таким образом, функция main может быть объявлена как C_task. Это сэкономит пространство стека и размер кода:

__C_task void main(void) /* Объявление main() как C_task*/
{
}

Прим.: компилятор IAR версии 2 использует __C_tast вместо C_task.

Циклы

Бесконечные циклы наиболее эффективно получаются с помощью следующей конструкции: for( ; ;) { }

Например,

for( ; ;)
{
/* Здесь бесконечный цикл */
}
// ?0001:RJMP ?0001 ; Переход к метке

Конструкция do{ }while(выражение) обычно генерирует более эффективный цикл по размеру кода под сравнению с while{ } и for{expr1; expr2; expr3).

Ниже приведен пример формирования цикла с помощью конструкции do{ } while:

char counter = 100; /* объявление переменной циклического счетчика */
// LDI R16,100 ; Инициализация переменной
do
{
} while(--counter); /* Декремент счетчика и проверка на ноль */
?0004:DEC R16 ; Декремент
// BRNE ?0004 ; Переход, если не равно

Предварительный декремент переменной как циклического счетчика дает, как правило, более эффективный код. Предварительный декремент и последующий инкремент более эффективны, т.к. переходы выполняются в зависимости от состояния флагов после декремента.

Сравнение макросов и функций

Функции, которые компилируются в 3-4 или менее строки ассемблерного кода, в некоторых случаях более выгодно представлять в виде макроса. При использовании макроса его имя будет заменяться фактическим кодом из тела макроса в процессе компиляции. Для очень маленьких функций компилятор генерирует меньший по размерах код и при использовании макроса улучшает быстродействие, т.к. исключается выполнение инструкций вызова функции и возврата.

Ниже показан пример выполнения задачи в виде функции и макроса.

/* Основная функция для вызова задачи */
__C_task void main(void)
{
UDR = read_and_convert(); /* Считывание значения и запись в УАПП */
}
/* Функция для считывания состояния вывода и преобразования его в ASCII*/
char read_and_convert(void)
{
return (PINB + 0x48); /* Возврат значения в виде символа ASCII */
}
/* Макрос для выполнения той же задачи */
#define read_and_convert (PINB + 0x48)

Ниже приведен код после ассемблирования функции:

main:
// RCALL read_and_convert ; вызов функции
// OUT LOW(12),R16 ; запись в память ввода-вывода
read_and_convert:
// IN R16,LOW(22) ; чтение из памяти ввода-вывода
// SUBI R16,LOW(184) ; прибавить к считанному значению 48
// RET ; возврат
Результат ассемблирования программы с макросом:
main:
// IN R16,LOW(22) ; чтение из памяти ввода-вывода
// SUBI R16,LOW(184) ; прибавить значение 48
// OUT LOW(12),R16 ; запись в память ввода-вывода

Таблица 5. Размер кода и время выполнения макросов и функций

Переменная Размер кода, байт Время выполнения, байт
Функция 10 10
Макрос 6 3

18 правил снижения размера кода

  • Компиляция с полной оптимизацией размера.
  • Отдавать предпочтение использованию локальных переменных.
  • Выбирать минимально приемлемый тип данных. По возможности использовать беззнаковый тип.
  • Если нелокальная переменная упоминается только в пределах одной функции, то ее нужно объявить как статическую.
  • По возможности объединяйте нелокальные данные в структуры. Это увеличивает возможность косвенной адресации без перезагрузки указателя.
  • Используйте указатели со смещением или объявляйте структуры для доступа внешней памяти.
  • Для организации вечных циклов используйте (;;) { }.
  • По возможности используйте do { } while(выражение).
  • По возможности используйте вычитающие циклические счетчики и предварительный декремент.
  • Используйте прямой доступ к памяти ввода-вывода (т.е. не используйте указатели).
  • Объявляйте main как C_task, если она не вызывается из других мест программы.
  • Используйте вместо функций макросы для задач, которые генерируют менее 2-3 строк ассемблерного кода.
  • Снижайте размер сегмента векторов прерываний (INTVEC) до фактически необходимого в приложении. Альтернативно, если связать все сегменты CODE в одно объявление, то упомянутое действие будет выполнено автоматически.
  • Собирайте несколько функций в один модуль (т.е. в один файл) для увеличения возможности повторного использования кода.
  • В некоторых случаях полноскоростная оптимизация приводит к снижению размеров кода по сравнению с оптимизацией по полному размеру. Компилируйте помодульно с разными настройками для установления, в каком случае достигается лучший результат.
  • Оптимизируйте C_startup, чтобы в нем не выполнялась инициализация неиспользуемых сегментов (т.е., IDATA0 или IDATA1, если все переменные tiny или small).
  • По возможности избегайте вызова функций из процедуры обработки прерываний.
  • Используйте минимально возможную модель памяти.

5 правил по снижению требований к размеру ОЗУ

  • Все константы и литеры необходимо хранить во флэш-памяти с помощью зарезервированных слов.
  • Избегайте использования глобальных переменных, если фактически эта переменная локальная. Данная рекомендация также позволяет уменьшить размер кода. Локальные переменные размещаются в стеке динамически и удаляются по завершении функции.
  • При использовании больших функций с переменными, для которых ограничена продолжительность использования в пределах функции, то более выгодным может оказаться использование вложенных функций.
  • Уточните точные значения размеров программного стека и стека возврата (файл линкера).
  • Не оставляйте пустого пространства для сегментов IDATA0 и UDATA0, даже при использовании переменных tiny (файл линкера).

Правила отладки программы

  • Убедитесь в достаточности размера сегмента CSTACK
  • Убедитесь в достаточности размера сегмента RSTACK
  • Убедитесь, что интерфейс внешней памяти активизирован, если он используется, и отключен, если он не используется.
  • Если регулярная функция и процедура обработки прерывания связываются через глобальную переменную, то убедитесь, что эта переменная объявлена как volatile, чтобы гарантировать ее повторное считывание из ОЗУ при каждой проверке.

Связанные документы

Руководства по применению
AVR035 (142.7 Кб), 10.12.2009
Эффективное программирование на Си AVR-микроконтроллеров
 
Автор документа: Жанна Свирина , http://www.gaw.ru"
Дата публикации: 10.12.2009
Дата редактирования: 10.12.2009
Кол-во просмотров 6157
 
 Все новости одной лентой