Программирование AVR Советы по программированию на C в IAR AVR Embedded Workbench 4.0 Thu, March 28 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.

Советы по программированию на C в IAR AVR Embedded Workbench 4.0 Печать
Добавил(а) microsin   

В этой статье решил записывать все интересное, которое удалось накопать при работе в среде программирования IAR Embedded Workbench for AVR (и для программирования с использованием AVR GCC).

void main (void)
{
   while (1)
   {
      //... здесь потом нужно добавить код программы
   }
}

2. Добавьте в начало main.c заголовочный файл, который описывает регистры вашего процессора:

#include "iotiny24.h"

Просмотрите этот файл и попытайтесь запомнить имена регистров, которые можно использовать в программе.

3. Добавьте в заголовочный в начало модуля кода:

#define false 0
#define true  1

#define uchar unsigned char
#define uint  unsigned int
#define ulong unsigned long

4. После добавления файла iotiny24.h (имя заголовочного файла определяется типом выбранного процессора, здесь в качестве примера взят процессор ATTINY24) можно удобно управлять портами. Используется также флажок ENABLE_BIT_DEFINITIONS, включающий определение битов. После включения заголовочного файла iotiny24.h и флажка ENABLE_BIT_DEFINITIONS возможны такие операции, как, например:

#define ENABLE_BIT_DEFINITIONS
#include < iotiny24.h >
...
//так мы переключили разряды 4, 5 и 6 порта A в режим выхода.
DDRA_DDA6 = 1;
DDRA_DDA5 = 1;
DDRA_DDA4 = 1;

Очень эффективны операторы типа регистр = (1 << имя_бита), например:

//отключаем цифровой буфер на ножках 0, 4 и 5 порта A
DIDR0 = (1<<PA0)|(1<<PA4)|(1<<PA5);

5. Пример программы, мигающей красным светодиодом (светодиоды _RED, _GRN и _YEL подключены к +5V питания через токоограничивающие резисторы):

#include < iotiny24.h >
//Строка #include < INTRINSICS.H > нужна, чтобы работала
// функция __delay_cycles.
#include < INTRINSICS.H >
#define _RED PORTA_PORTA6
#define _GRN PORTA_PORTA5
#define _YEL PORTA_PORTA4

void main (void)
{
  #define delay 1000000

  //переключаем светодиодные порты в режим выхода
  DDRA_DDA6 = 1;
  DDRA_DDA5 = 1;
  DDRA_DDA4 = 1;

  //выключаем светодиоды
  _RED = 1;
  _YEL = 1;
  _GRN = 1;
  while (true)
  {
     _RED = 0; //на выходе 0, красный LED горит
     __delay_cycles(delay / 10);
     _RED = 1; //на выходе 1, красный LED потушен
     __delay_cycles(delay - (a / 10));
  }
}

5a. Добавьте анализ кнопки, подключенный на ножку PA6 и землю - для простоты без всякого анализа дребезга. Мы можем читать лог. сигнал на этой ножке, несмотря на то, что она работает как выход, и на неё нагружен светодиод! Правда, для этот нужна специальная техника (см. ниже). Если бы мы сконфигурировали PA6 как вход, то нужно было бы либо включить внутренний pull-up резистор (записать 1 в бит PORTA6), либо припаять между PA6 и плюсом питания pull-up.

...
#define _KEY PINA_PINA6
...
if (_KEY == false)
{
   //кнопка нажата!
}

Теперь по поводу управления светодиодом и одновременного чтения кнопки по одному и тому же выводу. Если бы на порте был открытый коллектор, как в архитектуре MCS51, то проблем бы не было, там нет переключения режима порта между входом и выходом, порт всегда работает и как вход, и как выход, и мы можем читать кнопку, просто записав в выходной порт 1, при этом нижний ключ закроется, светодиод погаснет и будет служить pull-up резистором для кнопки. Но в случае с AVR порт двухтактный, и имеет верхний транзистор. Это значит, что мы не сможем жать на кнопку, когда ножка порта сконфигурирована как выход, и на выходе лог. 1 - через верхний транзистор потечет ток короткого замыкания, что неправильно. Для того, чтобы можно было все-таки управлять светодиодом, и одновременно читать кнопку, нужно переключать ножку порта из режима выхода с лог. 0 (когда светодиод горит) в режим входа (при этом светодиод погашен). Начальную установку после включения питания можно не делать, поскольку после сброса наши порты и так окажутся в третьем состоянии, и светодиод на ножке будет погашен. Предположим, у нас ATtiny45, нам нужно управлять желтым светодиодом подключенным к выв. 7 PB2, и анализировать кнопку на этой же ножке (см. [10], там же есть пример схемы макетной платы).

После включения питания светодиод не горит, поскольку PB2 работает как вход, и мы можем читать состояние кнопки:

#include < iotiny45.h >
#define _KEY PINB_PINB2
#define YEL PB2
#define _YEL PORTB_PORTB2
...
if (_KEY == false)
{
  //кнопка нажата!
  ...
}

Теперь светодиод горит, и читать кнопку уже нельзя (так же, как и с MCS51). Чтобы снова выключить светодиод (тогда можно будет читать кнопку):

_YEL = 0; //погаснет только желтый светодиод, можно читать кнопку

или

DDRx = 0; //в этом случае погаснут все светодиоды, если они
         
// горели на порте B, кнопку тоже можно читать.

6. Список ранее использовавшихся рабочих пространств (он по умолчанию открывается при старте) хранится в реестре по адресу

HKEY_CURRENT_USER\Software\IAR Systems\IAR Embedded Workbench IDE\Recent Workspaces\
C--Program Files-IAR AVR Embedded Workbench 4.0-common-bin-IarIdePm.exe.

Почистить его можно только с помощью regedit.

7. Чтобы работали старые добрые комбинации Copy/Paste - Ctrl+Ins/Shift+Ins, нужно добавить в меню Edit алиасы в Tools\Options..., закладка Key Bindings.

Целесообразно еще добавить следующие комбинации кнопок (в меню Debug):
Reset                              Ctrl+R
Stop Debugging                     Alt+D
Break                              Esc

8. Простейший способ войти в режим Power Down на ATtiny45:

MCUCR = (1<<SE)|(1<<SM1);
__sleep();

После этого выйти из режима Sleep можно только по прерыванию Watchdog, прерыванию INT0 или прерыванию Pin Change, USI Start condition и по сбросу.

9. То же самое, но с разрешением выхода из Power Down по прерыванию PCI - Pin Change Interrupt. В примере используется ножка 7 ATtiny45, на которую повешена кнопка (см. схему макетной платы):

//SE=1 - Sleep Enable
//SM1=1 и SM0=0 - после команды __sleep() включится режим
// Power Down

MCUCR = (1<<SE)|(1<<SM1);
//PCIE=1 - Pin Change Interrupt Enable
GIMSK = 1<<PCIE;
//PCINT2=1 - разрешить прерывание от ножки 7
// (KEY==PCINT2==PB2)

PCMSK = 1<<KEY;
//разрешение всех прерываний
SREG_I = 1;
__sleep();
//дальше идет код, который будет выполняться только после
// нажатия на кнопку.

...

Несмотря на то, что тут будет происходить прерывание, обработчик для прерывания PCI мы не делали - IDE и компилятор Си IAR Embedded Workbench генерирует в начале дампа кода заглушки - пустые команды выхода из прерывания (на это влияет включенная по умолчанию галочка Initialize unused interrupt vectors with RETI instructions, которая находится в свойствах проекта, категория General Options, закладка System):
_program_start:
000000   C0C5            RJMP   ?C_STARTUP
000002   9518            RETI
//сюда будет попадать вызов прерывания Pin Change Interrupt:
000004   9518            RETI
000006   9518            RETI
000008   9518            RETI
00000A   9518            RETI
00000C   9518            RETI
00000E   9518            RETI
000010   9518            RETI
000012   9518            RETI
000014   9518            RETI
000016   9518            RETI
000018   9518            RETI
00001A   9518            RETI
00001C   9518            RETI

В нашем примере будет срабатывать переход по вектору PCINT0, это адрес 000004 в байтовом выражении.

10. Как написать обработчик прерывания, например, для вектора PCINT0:

//директиве #pragma vector= нужно указать номер вектора 
// прерывания, список векторов задан в файле iotiny45.h
#pragma vector=PCINT0_vect
//имя PCINT0_routine взято произвольно
__interrupt void PCINT0_routine(void)
{
   __no_operation();
}

Теперь, если у нас настроено прерывание Pin Change Interrupt, как было указано в предыдущем совете, то будет вместо заглушки RETI вызываться этот код (__no_operation();).

11. Как настроить таймер Watchdog (WDT):

#define WDT128ms  0x03
WDTCR = (1<<WDTIE)|(1<<WDE)|WDT128ms;
//разрешение всех прерываний
SREG_I = 1;

Все, теперь после 128 мс произойдет вызов вектора прерывания WDT_routine (байтовый адрес 0x000018. Там расположена заглушка RETI. Этот первый вызов WDT_routine аппаратно сбрасывает флаг WDTIE (Watchdog Timeout Interrupt Enable), и уже следующее переполнение таймера через 128 мс вызовет не прерывание, а сброс (переход на адрес 0). Чтобы этого не происходило, нужно добавить обработчик WDT_routine и в нем устанавливать бит WDTIE:

#pragma vector=WDT_vect
__interrupt void WDT_routine(void)
{
  __no_operation();
  WDTCR_WDTIE = 1;
}

Теперь сброса не будет, просто процессор будет каждые 128 мс просыпаться (если была вызвана команда __sleep();).

12. Команда SLEEP выполняется очень быстро, так что иногда не успевают примениться результаты действия предыдущей команды:

DDRB = 0;
__no_operation();
__no_operation();
__no_operation();
__no_operation();
__sleep();

Если бы не было четырех команд NOP, то порт B не успел бы переключиться на вход. У меня, например, продолжал гореть светодиод, подключенный к ножке PB2 (после SLEEP он продолжал работать как выход). Количество холостых операций перед командой SLEEP приходится подбирать экспериментально (иногда для этого лучше использовать циклы).

13. Переменная float занимает 4 байта.

14. Чтобы переменная сохранялась в EEPROM и была доступна при перезагрузке и переподключении питания, нужно указать атрибут памяти __eeprom и ключевое слово __no_init (без __no_init переменная при старте будет обнуляться), например:

__no_init __eeprom float fVar;

Самая первая переменная размещается в EEPROM по адресу 0x01 (не 0x00!). Адрес переменной EEPROM в виде числа uchar можно получить выражением:

uchar Adr;
Adr = (uchar)&fVar;

15. Рано радоваться и расслабляться, пользуясь прелестями программированиями на Си - память улетает в трубу совершенно незаметно, особенно если использовать типы float, строки (это сразу включает в код библиотечные функции) - за удобства надо платить. Наверное, самое большое неудобство архитектуры AVR - расход как минимум сразу 2 байт на одну команду ассемблера.

При компиляции остатки памяти можно выжать "нахаляву" (128..256 байт на 4096 байтах программ), задав полную оптимизацию в свойствах проекта - категория C/C++ Compiler, закладка Optimizations, выбрать Size - High (Maximum optimization), при этом все галочки в окошке "Enabled optimizations" должны быть установлены.

16. Генерировать двоичную прошивку для программатора можно, задав в свойствах проекта, категория Linker, закладка Extra Output, галочку Generate extra output file. В области Format там же нужно выбрать Output format: intel-standard и Format variant: None (либо другой, например Output format: raw-binary и Format variant: None выбирают генерацию чистого двоичного файла).

17. Для генерирования случайного целого числа в диапазоне 0..32767 (или от 0 до константы RAND_MAX) существует имеющаяся в библиотеке CLIB или DLIB функция rand(). Чтобы её можно было использовать, надо добавить в main.c включаемый файл stdlib.h. Чтобы генерить случайное число в любом нужном диапазоне, нужно воспользоваться операцией деления по модулю (%). Например, так генерируется случайное бинарное число (true или false, т. е. случайное число от 0 до 1):

val = rand()%2;

А так генерится число от 0 до 9:

val = rand()%10;

На самом деле генерируемое функцией число не случайное, а псевдослучайное. Это означает, что ряд получаемых чисел при последовательных вызовах rand() всегда один и тот же, если генератор псевдослучайных чисел предварительно проинициализирован. Инициализирует этот генератор функция srand(целое_число). Этим удобно пользоваться, чтобы например, тестировать память. Последовательность действий такая:
srand(1);
- заполняем память данными, сгенерированными вызовами rand();
srand(1);
- читаем данные из памяти и снова вызываем при этом каждый раз rand(). То, что прочитано из памяти, и то, что дала rand() должно совпадать.

18. Чтобы распечатать знак процента % с помощью printf (или sprintf), нужно его повторить дважды:

printf("Это знак процента: %%");

19. Некоторые глобальные или статические переменные можно поместить прямо в регистры процессора R4–R15, что существенно повысит скорость работы с переменными. Для этого нужно при декларировании переменной использовать расширенное ключевое слово __regvar, а также использовать для компилятора опцию --lock_regs. Пример декларирования регистровой переменной (будет использоваться регистровая пара R15:R14):

__regvar __no_init int counter @ 14;

Ограничения: Максимальный размер объекта, который можно выделить в регистрах с помощью директивы __regvar, равен 4 байтам. Невозможно использовать указатели на переменную, выделенную с помощью директивы __regvar. Кроме того, нельзя при декларировании задать объекту начальную величину. Кроме того, если Вы используете __regvar, то блокируете таким образом регистры под переменную, и используемые библиотеки тоже должны быть перекомпилированы с тем же набором заблокированных регистров. Поэтому такие извращения лучше использовать в крайних случаях - наверное, лучшей альтернативой будет написать отдельный модуль на ассемблере.

Все сказанное в этом совете можно найти в справочнике по компилятору IAR, файл c:\Program Files\IAR AVR Embedded Workbench 4.0\avr\doc\EWAVR_CompilerReference.pdf. См. __regvar, --lock_regs, --memory_model.

20. Проблема с Build Actions -> Post-build command line: невозможно запустить необходимые утилиты после компилирования.

Обычно проблема связана с путями запуска программ. В этом случае окне View -> Messages -> Build после компиляции появляется запись "Total number of errors: 1" (хотя ошибок синтаксиса при компиляции не было), и нужные действия, указанные в командной строке запуска Post-build command line не происходят. Трудность выявления и исправления таких ошибок в том, что нет никакой информации о причине проблемы.

Первое, что нужно проверить - путь до места нахождения исполняемой утилиты. Она должна быть запущена по полному прямо указанному пути, либо путь запуска должен быть прописан в переменной окружения %Path%. Если исполняемая программа находится в пределах папок проекта, то используйте встроенный в IAR макрос $PROJ_DIR$, который указывает на месторасположение файлов настроек проекта *.ewp и *.eww. Профит использования $PROJ_DIR$ в том, что она позволяет отвязаться от абсолютных путей, и можно спокойно копировать или переносить проект в любую папку на диске, не теряя его работоспособности.

Второе, что нужно проверить - видит ли запускаемая программа те файлы, которые надо обработать. Если программа ожидает увидеть входные файлы в своем текущем каталоге, то используйте командный файл для запуска утилиты, и внутри командного файла используйте команду cd для перехода в каталог утилиты. Текущий путь проекта передавайте через опцию командного файла. Пример грамотно настроенной строки Post-build command line:

$PROJ_DIR$\..\..\Exe\post_build_pu.bat $PROJ_DIR$

Пример файла post_build_pu.bat:

rem в параметре передается путь до файла настроек проекта $PROJ_DIR
$
cd %1....Exe
@rem утилита to_prog.exe делает файл firmware.bin для программатора SAM-BA. to_prog.exe

21. Как вычислить количество элементов в массиве структур или массиве элементов какого-то определенного типа? Подойдет макрос countof:

#define countof(a) (sizeof(a) / sizeof(*(a)))

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

Как решить проблему, не прибегая к выключению отладки (опции -O0 и т. п.)? Иногда выключить отладку не представляется возможным. ИМХО самый простой способ - присвоить переменной атрибут volatile, тогда компилятор не будет её оптимизировать, и выделит для этой переменной отдельную ячейку памяти.

void placetimesectors (void)
{
   volatile u8 ss, mm, hh;
   RGB_t color;
   
   ss = BCDtoBIN(rtc.reg.ss);
   mm = BCDtoBIN(rtc.reg.mm);
   hh = BCDtoBIN(rtc.reg.hh & 0x1F);

Теперь переменные ss, mm и hh будут легко доступны при пошаговой отладки. Этот совет хорошо подходит для большинства компиляторов, как IAR, так и GCC.

Также для AVR GCC (начиная с версии 4.4) можно отключить оптимизацию для блока кода с помощью директивы pragma:

#pragma GCC push_options
#pragma GCC optimize ("O0")
 
//Код, где будет отключена оптимизация
...
 
#pragma GCC pop_options

Для функции можно отключить оптимизацию добавлением атрибута __attribute__((optimize("O0"))), например так:

void __attribute__((optimize("O0"))) foo(unsigned char data) {
    // не модифицируемый компилятором код
    ...
}

[Ссылки]

1. AVR - как избавиться от чисел с плавающей точкой.
2. AVR JTAGICE mkII: отладка программы на C в IAR AVR Embedded Workbench 4.0.
3. IAR Embedded Workbench IDE, использование ассемблера в C-проекте.
4. AVR - Application Notes (указания по применению микроконтроллеров AVR).
5. AVR: полезные ссылки.
6. Макетные платы с интерфейсом USB.
7. Программы для AVR.
8. Программаторы для AVR.
9. Литература по применению и программированию AVR.
10. AVR JTAGICE mkII: отладка программы ATtiny24, ATtiny45 на C.

 

Добавить комментарий


Защитный код
Обновить

Top of Page