Добавил:
ivanov666
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз:
Предмет:
Файл:Микропроцессорные системы. Средства разработки программного обеспечения для микроконтроллеров семейства AVR. Учебное пособие
.pdf
51
if(already_runned){
return;
}
// Тело функции
………………………………………………..
// Исключение повторного вызова
already_runned=false;
}
В этой функции объявлена статическая переменная already_runned,
имеющая начальное значение false.
После выполнения функции значение переменной already_runned
изменяется на true.
Каждый раз, при вызове функции one_running_func(), проверяет-
ся значение переменной already_runned, если оно равно true, значит,
что функция уже выполнялась и происходит выход без повторного вы-
полнения.
Поскольку область видимости переменной already_runned огра-
ничена функцией one_running_func(), то использование этого имени в
других функциях не вызовет никаких конфликтов имен. Кроме того, любые другие функции не смогут, например в результате ошибки, изменить
значение переменной already_runned, поскольку ее область видимости
недоступна никаким внешним функциям.
7.7. Использование областей видимости
Область видимости должна быть ограничена минимально необхо-
димой. Это значит, что если переменная используется только внутри
блока, то незачем задавать ее вне этого блока. Если переменная используется в файле, но не за его пределами, то ограничиваем ее область видимости файлом. И так далее. Таким образом мы уменьшаем вероят-
ность конфликта имен переменных и их ошибочного использования.
7.8. Модификатор volatile
Еще одним важным модификатором, который используется при
объявлении переменных, является модификатор volatile. Поля, объяв-
ленные как volatile, не проходят оптимизацию компилятором.
Современные компиляторы по умолчанию пытаются оптимизиро-
вать код, написанный программистом. В частности, если компилятор
«решит», что какая-то ветвь программы никогда не выполняется, он
просто проигнорирует все команды в этой ветви.

52
Пример ошибки оптимизации. Пусть у нас есть функция
wail_for_false(), которая ожидает приход некоего прерывания:
/* Флаг. В прерывании переменная
iflag устанавливается в значение false
*/
bool iflag;
// Функция ожидания прерывания
void wail_for_false(){
iflag=true;
while(iflag){
// Делаем что-то, пока
// значение переменной iflag равно true
}
}
void interrupt_handler(){
iflag=true;
}
и пусть имеется процедура обработки прерывания interrupt_handler(),
в которой значение переменной iflag изменяется на false, после чего
цикл в функции wail_for_false() должен завершиться.
Вызывая функцию wail_for_false(), программист ожидает, что она
завершится после прихода прерывания, обработчиком которого является
функция interrupt_handler().
Однако если практически попробовать реализовать такой пример,
то функция wail_for_false(), скорее всего, не завершится никогда, пото-
му что цикл в ней будет бесконечным.
Это происходит потому, что оптимизатор компилятора «решает»,
что раз внутри функции wail_for_false() значение переменной iflag никогда не изменяется, то после оператора iflag=true параметр оператора
while() всегда будет иметь значение true и цикл никогда не завершится.
Приняв такое решение, оптимизатор компилятора может вообще исключить машинный код, связанный с переменной iflag из функции
wail_for_false(), и заменить while(iflag) на while(true) на уровне ма-
шинного кода.
Избежать такой ошибочной оптимизации помогает модификатор
volatile.
Если переменную iflag объявить как

53
volatile bool iflag;
то оптимизация для данной переменной будет отключена и ее значение
будет проверяться всегда.
Отметим, что ошибки оптимизации часто не так очевидны, как в
приведенном примере, и их поиск может отнять много времени. Поэтому всегда следует внимательно подходить к переменным, которые используются для обмена данными между различными программными потоками или между процедурой обработки прерывания и основной про-
граммой.
С практической точки зрения следует руководствоваться следующими правилами использования модификатора volatile:
Если переменная изменяется аппаратно (например, является ре-
гистром порта ввода-вывода) – объявляйте ее как volatile.
Если переменная изменяется в одном потоке, а ее значение ис-
пользуется в другом потоке – объявляйте ее как volatile.
Если переменная изменяется в обработчике прерывания, а ее
значение используется вне этого обработчика прерывания – объявляйте
ее как volatile.
7.9. Размещение произвольных данных в памяти программ
В языках С и С++ существуют специальные атрибуты, которые
позволяют помещать те или иные данные в любые секции, на выбор
программиста. Это иногда очень полезно.
Например, применительно к микроконтроллерам семейства AVR,
имеющим весьма ограниченный объем встроенного ОЗУ, разумно расположить большие объемы неизменяемых данных (константы, таблицы
и т. п.) в памяти программ, и извлекать их при необходимости непосредственно оттуда, не занимая память данных (ОЗУ).
Пример. Таблица в памяти программ
Например, у нас имеется таблица перекодировки символов в код
ЖКИ-индикатора. Допустим, что каждый символ ЖКИ-индикатора ко-
дируется 16 битами, или двумя байтами. Для индикатора, в котором
каждый символ представлен 16-ю сегментами, это действительно так.
Всего требуется перекодировать 256 символов ASCII-таблицы в
16-битные представления на ЖКИ-индикаторе. Это означает, что размер
таблицы будет равен 512 байт, т. к. если таблицу описать как
uint16_t table[256] = { 0xEAA8, 0x2A80, 0x4000,
/* И так далее, до 256 значений*/

54
};
то компилятор разместит ее в секции .data. Для полной перекодировки
ASCII-таблицы в 256 символов такая таблица займет 512 байт памяти
данных.
Такой расход памяти данных крайне нерационален. Во-первых,
объем ОЗУ и так относительно мал (единицы килобайт). Во-вторых –
данные таблицы не изменяются в течение выполнения программы, т. е.
являются константными. Поэтому хранить эти данные в ОЗУ не имеет
смысла.
Расширенная гарвардская архитектура микроконтроллеров семей-
ства AVR допускает считывание памяти программ, поэтому выход
прост – хранить эту таблицу в памяти программ, считывая по мере
надобности.
Для того чтобы поместить какие-либо данные (в том числе и про-
граммный код) в произвольную секцию, существует способ прямого
указания секции, в которой должны быть размещены те или иные дан-
ные. Например, так:
const uint16_t table[256] __attribute__((section(".text"))) =
{ 0xEAA8, 0x2A80, 0x4000,
/* И так далее, до 256 значений*/
};
Директива __attribute__((section(".text"))) указывает, что опреде-
ляемая переменная или константа должна располагаться в указанной
секции (в данном случае – .text). Вместо секции .text может быть указа-
на любая другая секция.
Таким же образом можно поместить, например, в секцию инициализации ссылку на любую необходимую функцию пользователя.
При обращении к avr-gcc рекомендуется использовать для разме-
щения данных в памяти программ макрос PROGMEM, который опре-
делен в заголовочном файле avr/pgmspace.h как
#define PROGMEM __attribute__((__progmem__))
и выполняет те же действия.
Поэтому наш пример в случае avr-gcc будет наиболее корректно
выглядеть так:
// Подключим заголовочный файл с макросами
#include <avr/pgmspace.h>

55
// Определяем таблицу в памяти программ
const uint16_t table[256] PROGMEM =
{ 0xEAA8, 0x2A80, 0x4000,
/* И так далее, до 256 значений*/
};
Следует помнить, что адресное пространство памяти программ
микроконтроллеров с гарвардской архитектурой – отдельное от памяти
данных. Поэтому к данным, размещенным в памяти программ, нельзя
обращаться обычными операторами присваивания.
Например, если мы обратимся к нашей таблице, желая считать
символ по индексу 48 таким образом:
uint16_t led_sym = table[48]; /* ОШИБКА! */
то получим ошибочные данные, поскольку данные будут считываться не
из памяти программ, а из памяти данных.
Считывать данные из памяти программ можно только специаль-
ными командами микроконтроллера AVR. Для использования этих команд из языков С и С++ в файле avr/pgmspace.h имеются заранее опре-
деленные макросы:
// Считать char или unsigned char из памяти программ
pgm_read_byte(address_short);
// Считать int или unsigned int из памяти программ
pgm_read_word(address_short);
// Считать float из памяти программ
pgm_read_float(address_short);
В качестве входного параметра все макросы принимают адрес в
памяти программ address_short, имеющий значение от 0 до 65535. Данные макросы пригодны для использования в микроконтроллерах семей-
ства AVR с объемом памяти программ не более 64 Кбайт.
Таким образом, наш пример размещения таблицы символов ЖКИиндикатора в памяти программ будет выглядеть так:
// Подключим заголовочный файл с макросами
#include <avr/pgmspace.h>
// Определяем таблицу в памяти программ
const uint16_t table[256] PROGMEM =

56
{ 0xEAA8, 0x2A80, 0x4000,
/* И так далее, до 256 значений*/
};
// Пример. Считываем слово из памяти программ (элемент
// массива номер 48 в переменную led_sym
uint16_t led_sym = pgm_read_word( &table[48] );
Аналогично можно размещать в памяти программ любые другие
константы, таблицы и строки.
Ввиду того, что текстовые сообщения часто являются констант-
ными и часто размещаются в памяти программ, то имеются специаль-
ные строковые функции для работы с памятью программ.
Эти функции также описаны в заголовочном файле
avr/pgmspace.h.
Пример. Строка в памяти программ.
// Подключим заголовочный файл с макросами
#include <avr/pgmspace.h>
// Определяем строку в памяти программ
char s[] PROGMEM = "String";
// Копируем строку в буфер памяти данных
char buf[0x20];
// Функция копирования строки из памяти программ
strcpy_P(buf, s);
// Обращаемся к 0-му элементу строки
char c = pgm_read_byte( &s[0] );
Следует всегда помнить, что если какие-то данные размещены в
памяти программ, то к ним возможно обращение только с помощью
специальных функций и никак иначе.

57
8. ПРОЕКТИРОВАНИЕ ПРОГРАММЫ ДЛЯ МИКРОКОНТРОЛЛЕРА
При проектировании ПО необходимо найти приемлемые решения,
зависящие от требований задачи и от ограничений аппаратной части
микропроцессорной системы – объема памяти программ, объема памяти
данных, быстродействия и разрядности процессорного ядра и т. п.
В настоящем издании рассмотрены вопросы проектирования ПО
для микроконтроллеров, не имеющих операционных систем. Отметим,
что для старших представителей семейства AVR имеются специализированные операционные системы, с возможностями которых можно по-
знакомиться в [10].
В общем случае существует несколько способов проектирования
ПО и программирования МПУ.
8.1. Программирование без использования операционной системы
Данный метод применяется при разработке ПО для тех микропроцессорных устройств, которые не имеют какого-либо системного про-
граммного обеспечения. Чаще всего это микроконтроллеры с низкой
производительностью, например семейства AVR или PIC [1, 2], а также
младшие представители семейств ARM [7].
Достоинства данного метода – полный доступ программиста к ап-
паратному обеспечению процессора (микроконтроллера), что позволяет
оптимальным образом сконфигурировать периферийные устройства и
добиться, например, максимально возможной производительности.
Недостаток данного метода – трудоемкость, обусловленная отсут-
ствием стандартных средств и необходимостью писать «с нуля» поддержку всех необходимых функций работы с аппаратурой, протоколами
и проч.
Для компенсации этого недостатка производители микроконтрол-
леров поставляют набор программных библиотек для работы с аппаратурой. Такие библиотеки поставляются в виде исходных кодов на языках
ассемблера и С и содержат функции управления терминальным вводом-
выводом по последовательному порту, функции управления таймерами,
линиями ввода-вывода и другим периферийным оборудованием, интегрированным в микроконтроллер.
8.2. Специализированные операционные системы
При реализации ПО для микропроцессорных устройств часто
встают типовые задачи, такие как переключение процессов, организация
буферов ввода-вывода, обработка прерываний и др.

58
С целью облегчения проектирования и реализации ПО разработа-
ны специализированные операционные системы для встраиваемых
устройств. К таким ОС относятся, например, eCos, ChibiOS/RT,
FreeRTOS и др. [10]. Обычно специализированные ОС являются систе-
мами реального времени.
Данные ОС специализированы в том смысле, что содержат огра-
ниченный набор функций по сравнению с ОС общего назначения.
Как правило, набор функций специализированной ОС не содержит
функций файлового ввода-вывода, не подразумевает разделения прав
доступа к ресурсам. Поскольку данные ОС чаще всего применяются в
микроконтроллерах и находятся физически в ПЗУ вместе с ПО пользователя, то они не содержат функций, позволяющих загружать и запус-
кать программы с внешних носителей.
Относительная простота организации специализированных ОС
позволяет, в случае необходимости, легко добавлять нужные модули,
а также избавлять программиста от рутинных операций по переключению процессов, организации функций терминального ввода-вывода и
других подобных вещей.
Как правило, специализированные ОС имеют несколько базовых
компонентов:
планировщик задач;
уровень абстракции оборудования;
интерфейс пользовательского ПО.
Кроме этого, в состав специализированной ОС могут включаться
дополнительные компоненты, расширяющие функциональность ОС,
например: математические библиотеки, поддержка многопоточности,
отладочные функции и проч.
Рассмотрим назначение различных компонентов специализированной ОС.
Планировщик задач позволяет организовать в пользовательском
ПО несколько независимых процессов (задач), выполняющихся параллельно. Такая необходимость возникает, например, при организации обмена данными по нескольким каналам связи одновременно, что акту-
ально для МПУ в составе РМПС.
Как правило, специализированная ОС содержит несколько раз-
личных планировщиков задач, один из которых можно выбрать при ее
конфигурации.
Уровень абстракции оборудования представляет собой, по сути,
набор драйверов устройств. Каждый драйвер имеет стандартный для
данной ОС программный интерфейс, что позволяет улучшить перено-
симость ПО с одного устройства на другое.

59
Интерфейс пользовательского ПО представляет собой набор си-
стемных функций, с помощью которых ПО пользователя обращается к
различным компонентам ОС – планировщику задач, драйверам ввода-
вывода (уровню абстракции оборудования) и осуществляет межпро-
цессное и межпоточное взаимодействие.
При правильном построении ПО все взаимодействие между спе-
циализированной ОС и ПО пользователя происходит только через ин-
терфейс пользовательского ПО.
8.3. Операционные системы общего назначения
С ростом производительности и снижением стоимости микропро-
цессорной техники в микропроцессорных устройствах все чаще приме-
няются ОС общего назначения, такие как Linux, QNX, WinCE и т. п.
Создание ПО для таких систем наиболее комфортно, с точки зрения проектировщиков и программистов.
Это обусловлено тем, что, во-первых, для данных ОС существует
обширнейший инструментарий для написания и отладки программ, а во-
вторых, тем, что можно создавать и отлаживать ПО на персональном
компьютере с той же ОС, что и на целевом МПУ, а затем путем кросскомпиляции перенести уже отлаженное ПО на требуемое МПУ.
Для микроконтроллеров семейства AVR не существует ОС общего
назначения, поскольку, во-первых, такие ОС требуют значительных вычислительных ресурсов и, во-вторых, такие ОС не реализуются на микропроцессорах с гарвардской архитектурой из-за ее ограничений на изменение памяти программ.

60
9. АРХИТЕКТУРА ПРОГРАММЫ ДЛЯ МИКРОКОНТРОЛЛЕРА
Существует два принципиально разных подхода к построению
программной архитектуры микроконтроллера – однопоточная архитек-
тура (пример – рис. 8) и многопоточная архитектура (пример – рис. 9).
Рис. 8. Вариант однопоточной архитектуры построения программы
для микроконтроллера
Общая структура программы для микроконтроллера, написанной
без использования ОС, обычно включает следующие программные мо-
дули:
модуль инициализации;
модули обработки прерываний;
модули управления периферийными устройствами;
модуль управления программными потоками (для многопоточ-
ной архитектуры);
модуль основного программного цикла (для однопоточной ар-
хитектуры).
Однопоточная архитектура ПО. Программа имеет единственный
основной цикл, который реализует весь алгоритм программы. Из этого
единственного цикла производится обращение к программным модулям
управления периферийными устройствами по мере необходимости.
В свою очередь, модули управления периферийными устройствами могут обмениваться данными с процедурами обработки прерываний.
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]
