Добавил:
ivanov666
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз:
Предмет:
Файл:Микропроцессорные системы. Средства разработки программного обеспечения для микроконтроллеров семейства AVR. Учебное пособие
.pdf
41
левого. Соответствие формальных адресов памяти данных, генерируемых компилятором, и физических адресов памяти данных показано на
рис. 7. В левой части рисунка показаны формальные адреса памяти
данных, которые генерирует компилятор, а в правой части рисунка – со-
ответствующие им физические адреса памяти данных.
Рис. 6. Привязка секции .text к адресному пространству памяти
данных микроконтроллера ATMega169
Линкер учитывает, что с адреса 0x800000 начинается отдельное
адресное пространство памяти данных, и генерирует правильные адреса
памяти данных, начиная с 0, но в адресном пространстве памяти данных, а не программ. Или, иначе говоря, физический адрес памяти данных вычисляется по формуле: Аф = А
gcc
– A
.data
, где А
ф
– физический ад-
рес памяти данных (начиная с нуля); А
gcc
– формальный адрес памяти
данных, который сгенерирован компилятором (начиная с 0x800000);
A
.data
– формальный адрес начала памяти данных. В нашем случае
A
.data
≡ 0x800000.

42
Секция .data начинается с адреса 0x800100, поскольку первые
0x100 ячеек памяти данных занимают регистры общего назначения и
регистры ввода-вывода. Максимальный размер секции .data равен
1 Кбайт.
Вся память данных делится на две секции – .data и .bss. Размер
секции .bss определяется автоматически. Под секцию .bss отдается вся
память данных, которая не используется секцией .data.
В секцию .data помещаются все инициализированные во время
объявления статические переменные. В секции .bss находятся статические переменные, не инициализированные во время объявления, а так-
же программный стек и стек возвратов.
Рис. 7. Соответствие формальных и физических адресов памяти данных
Если в программе на С и С++ отсутствуют статические и глобаль-
ные переменные и классы, то размер секции .data равен нулю, а вся па-
мять данных отдается под секцию .bss.
Кроме того, имеются дополнительные секции, формально располо-
женные в адресном пространстве, но на самом деле физические не пред-

43
ставленные ни в памяти программ, ни в памяти данных, – секция энергонезависимого ОЗУ .eeprom, секция битов глобальной конфигурации .fuse
и секция битов защиты ПЗУ .lock. Эти секции считаются расположенны-
ми в своих адресных пространствах. Данные секций .eeprom, .fuse, .lock,
как и любых других секций, можно выделить из исполняемого файла, и
запрограммировать соответствующие разделы памяти микроконтроллера.
Данные этих секций непосредственно недоступны в адресных простран-
ствах памяти программ и памяти данных.
Директива SECTIONS задает описание секций и подсекций. Сек-
ции и подсекции располагаются в памяти в том же порядке, в каком они
описаны в файле-описателе распределения памяти.
Дополнительные секции (т. е. все секции, кроме описанных ди-
рективой MEMORY) не являются необходимыми для выполнения программы микроконтроллером. В дополнительные секции компилятор помещает отладочную информацию и данные, которые не нужны непосредственно для исполнения программы микроконтроллером, но необ-
ходимы для функций отладки, тестирования, профилирования и т. п.
В настоящем пособии мы не будем рассматривать дополнительные
секции файла-описателя распределения памяти и сосредоточим свое
внимание на трех основных секциях, которые имеют отношение к непо-
средственному выполнению программы, – .text, .data, .bss.
Рассмотрим, как компиляторы языков С и С++ используют секции
памяти, а также особенности генерируемого компилятором кода.
7.2. Начальная инициализация программы на языках С и С++
С точки зрения программиста на ЯВУ, программа на языках C и
C++ начинает выполняться с функции main(), которая описана как
int main(const int argc, const char* argv[]);
Однако до того, как данная функция начнет выполняться, проис-
ходит инициализация указателя стека, начальных значений переменных,
статических классов и много других предварительных действий.
Рассмотрим подробнее процесс начальной инициализации про-
граммного обеспечения микроконтроллера семейства AVR до запуска
функции main().
С адреса 0 памяти программ расположена секция *(.vectors), где
находятся вектора прерываний. После включения питания или подачи
сигнала «сброс» программа начинает выполняться с адреса 0, содержа-
щего переход на процедуру инициализации.
Ссылки на процедуры инициализации хранятся в подсекциях па-
мяти программ, именующихся в нашем примере как *(.init*), где * –
любое сочетание символов.

44
Инициализация выполняется в следующем порядке.
Шаг 1
Выставляется указатель стека возвратов и указатель программного
стека. Это важнейшая операция, поскольку стек возвратов обеспечивает
вызов подпрограмм и возврат из них.
Шаг 2
Секция неинициализированных данных .bss заполняется нулевым
значением. Эта операция производится согласно стандарту языков С и
С++ – перед началом выполнения все неинициализированные статиче-
ские переменные заполняются нулевым значением.
Шаг 3
Производится инициализация секции инициализированных дан-
ных (статических переменных). Обратите внимание, что, хотя все адреса
секции .data привязаны линкером к памяти данных, секция .data после
линковки помещается в память программ с помощью директивы AT.
В процессе инициализации секции .data данные этой секции копируют-
ся из памяти программ в память данных по реальным адресам секции .data. Так делается потому, что память данных является энергозависимой и в ней невозможно сохранить данные при выключении питания.
В свою очередь, необходимо учитывать, что в память программ должен
помещаться не только исполняемый код, но и данные секции .data. Если
секция .data имеет нулевую длину (нет инициализированных данных),
то этот шаг пропускается.
Шаг 4
Инициализируются статические классы С++ путем вызова их кон-
структоров. Если программа не содержит статических классов, то этот
шаг пропускается.
Шаг 5
Управление передается функции main(). С этого момента управ-
ление полностью передается программе пользователя.
Отметим, что с помощью специальных директив языка С/C++
программист может, при необходимости, добавить в любую секцию
инициализации собственную дополнительную процедуру. Например,
некоторые типы микроконтроллеров требуют программной настройки
тактового генератора до того, как начнется выполнение основной программы. Такую операцию удобно поместить в одну из секций инициали-
зации.

45
7.3. Инициализированные при создании
и не инициализированные при создании переменные
Все переменные, которые объявляются в программах на языках С
и С++, вне зависимости от типа, подразделяются на два основных клас-
са – статические и автоматические.
Статическая переменная – это переменная, существующая все
время, пока выполняется программа, и имеющая один определенный
адрес в памяти, не изменяющийся во время выполнения программы.
Адрес статической переменной задается на этапе линковки программы.
На языках С и С++ все переменные, объявленные вне тела функции,
являются статическими.
Автоматическая переменная – это переменная, которая создает-
ся и удаляется во время выполнения программы. Все переменные, которые объявлены внутри тела функции и не имеют модификатора static,
являются автоматическими. Автоматические переменные существуют,
только пока они необходимы. Время «жизни» автоматических переменных на языках С и С++ ограничено блоком ({} – фигурными скобками),
внутри которого они объявлены. Для хранения автоматических пере-
менных используется программный стек.
Согласно стандарту языков С и С++, все объявленные, но не ини-
циализированные в момент объявления, статические переменные к мо-
менту выполнения начала программы (т. е. функции main()) должны
быть инициализированы нулевым значением. Это реализовано простым
заполнением нулевым байтом всей секции .bss в процессе инициализации ПО. С точки зрения программиста, объявления переменных вне те-
ла функции:
int a=0;
или
int a;
являются идентичными. И в том и в другом случае к началу выполнения
функции main() переменная a примет значение 0. Однако в первом случае переменная а будет располагаться в секции .data, а во втором слу-
чае – в секции .bss.
Обратите внимание на оговорку вне тела функции. Если мы объ-
являем переменную внутри тела функции, то ситуация совершенно
иная. Дело в том, что нестатические, т. е. автоматические, переменные в
функции создаются на стеке и существуют только во время выполнения
функции. Поэтому, несмотря на сходство объявления, следует помнить,
что записи:

46
void a(){
int a=0;
….
}
и
void a(){
int a;
….
}
полностью отличаются по смыслу.
В первом случае – переменная a примет значение 0 после объяв-
ления, а во втором – переменная a примет неопределенное значение, ко-
торое зависит от того, какие значения ранее помещались на стек.
Этот факт является источником очень большого числа трудно вы-
являемых ошибок. Поэтому очень хорошо будет запомнить и применять
следующее правило:
Всегда инициализируйте любую автоматическую переменную
в момент создания.
7.4. Область видимости переменных
Область видимости переменных обозначает область программы,
в пределах которой имя некоторой переменной продолжает быть связан-
ным с этой переменной и возвращать ее значение. За пределами области
видимости тот же самый идентификатор может быть связан с другой пе-
ременной либо быть свободным.
Другими словами, область видимости переменной – это та часть
программного кода, в которой мы можем обратиться к данной перемен-
ной по ее идентификатору (имени).
В программе на С и С++ может существовать неограниченное
число переменных с одинаковым идентификатором при условии, что их
области видимости не пересекаются.
Ограничение области видимости – это очень удобный и полезный
инструмент, позволяющий писать понятный программный код, структу-
рировать его и избегать некоторых ошибок.
Область видимости переменных в языках С и С++ может быть
ограничена программой, файлом, классом, блоком.
Рассмотрим каждое ограничение области видимости подробнее.
Область видимости – программа. Такие переменные видны в
любой функции программы, и к ним можно обратиться из любого программного блока. Эти переменные называются глобальными перемен-

47
ными. Такие переменные всегда являются статическими. Любая объявленная вне тела функции переменная без модификатора statiс является
глобальной. Примеры объявлений глобальных переменных:
int a;
char b;
char* s="Строка";
Область видимости – файл. Обычно программист объединяет в
один файл сходные по назначению функции. В этом случае удобно, что-
бы какая-то переменная, общая для всех этих функций, была доступна в
одном файле, но недоступна в других файлах проекта. Такой подход реализуется модификатором static перед объявлением переменной вне те-
ла функции. Примеры объявлений таких переменных:
static int a;
static char b;
static char* s="Строка";
Область видимости – класс. В процессе описания класса воз-
можно задать переменные класса с любой желаемой областью видимости. Область видимости задается с помощью ключевых слов public,
protected и private. Переменные, объявленные в секции public, доступны как методам класса, так и всем, кто пожелает обратиться к этим переменным извне. Переменные, объявленные в секции protected, доступны как методам данного класса, так и всем его потомкам, но недоступны извне. И, наконец, переменные, объявленные в секции private,
доступны только методам данного класса, но не его потомкам, и недо-
ступны извне. Пример объявления переменных класса:
class myClass{
public:
int publ_var; // Видна всем извне
protected:
int prot_var; // Видна только классу myClass и его потом-
кам
private:
int priv_var; // Видна только классу myClass
}
Переменные статических экземпляров класса можно рассматри-
вать как статические, а автоматических или динамических экземпля-
ров – как автоматические.
Область видимости – блок. Все переменные, объявленные внут-
ри блока, ограниченного фигурными скобками, доступны только внутри

48
данного блока. Например, каждая функция языков С и С++ является отдельным блоком. Пример объявления переменных, ограниченных обла-
стью видимости «блок»:
void f(){
int a=1;
// Переменная a доступна
for(int i=2; i<5; i++){
// Переменные a и i доступны
a=a*i;
}
// Переменная a – доступна
// Переменная i – недоступна
}
7.5. Пересечение областей видимости
Рассмотрим вариант, когда имеются переменные с одинаковым
идентификатором в пересекающихся областях видимости.
Правило 1. Не может существовать более чем одна переменная с
заданным идентификатором в одной области видимости. Иными словами, все переменные в одной области видимости должны иметь разные
идентификаторы (имена).
Следствие 1 из правила 1. Не может существовать более чем од-
на глобальная переменная с заданным идентификатором. Это значит,
что все глобальные переменные должны иметь разные идентификаторы
(имена).
Следствие 2 из правила 1. В одном файле не может существовать
более чем одна переменная с заданным идентификатором, если область
ее видимости – файл.
Правило 2. Переменная, объявленная в более узкой области ви-
димости, перекрывает переменную с тем же идентификатором, объяв-
ленную в более широкой области видимости. Поясним на примерах.
Пример. Пусть имеются два файла s1.cxx и s2.cxx, являющихся
составными частями одной программы.
В файле s1.cxx объявлена переменная
int a=1;
В файле s2.cxx объявлена переменная
static int a=2;

49
Если мы в какой-либо функции, описанной в файле s1.cxx, выве-
дем на печать значение переменной a, например, так:
printf("a=%i\n",a);
то мы получим на экране
a=1
Если мы в какой-либо функции, описанной в файле s2.cxx, выве-
дем на печать значение переменной a, то мы получим на экране
a=2
Следовательно, для всех функций файла s2.cxx является приори-
тетным объявление переменной a из этого же файла. Поскольку область
видимости переменной a, объявленной в файле s2.cxx, ограничена файлом, то она перекрывает объявление глобальной переменной a из файла
s1.cxx. Это значит, что переменная с более узкой областью видимости
(в данном случае – файл) перекрывает объявление переменной с более
широкой областью видимости (в данном случае – глобальную перемен-
ную).
Точно так же переменные, объявленные внутри функции, пере-
крывают объявления глобальных переменных и переменных с областью
видимости, ограниченной файлом, с тем же идентификатором; переменные, объявленные во вложенном блоке, перекрывают переменные с тем
же идентификатором, объявленные во внешнем блоке.
Более подробно об областях видимости переменных и правилах
работы с ними можно узнать в [9].
7.6. Модификатор static
Ключевое слово-модификатор static имеет различные смысловые
значения в зависимости от того, применяется оно для объявления пере-
менной внутри или вне тела функции.
Как уже говорилось, вне тела функции, модификатор static ограничивает область видимости переменной файлом.
Внутри тела функции, модификатор static не изменяет области ви-
димости переменной, но делает ее статической. Значит, переменная доступна только внутри функции, но при этом существует все время.
Ее значение не изменяется между вызовами функции, в которой она
объявлена. Рассмотрим две функции:

50
int a(){
int v=0;
v++;
return(v);
}
и
int b(){
static int v;
v++;
return(v);
}
Функция a() при каждом ее вызове будет возвращать значение 1. Функция b() при каждом ее вызове будет возвращать последовательно значе-
ния 1, 2, 3 … и т. д. Объясним, почему это происходит.
В функции a(), переменная v – автоматическая. Каждый раз, при
вызове функции a(), для переменной v на стеке выделяется место и туда
заносится значение 0. Затем переменная v увеличивается на 1 (0 + 1 = 1)
и результат возвращается. Разумеется, что всегда будет возвращаться
одно и то же число – 1.
В функции b(), переменная v – статическая. Это значит, что ком-
пилятор выделил для нее на этапе компиляции место в секции .bss, так
как переменная не инициализируется при объявлении. Как и все данные
секции .bss, переменная v инициализирована нулевым значением.
При первом вызове функции b(), нулевое значение переменной v увели-
чится на 1 и функция вернет значение 1. При втором вызове функции b(),
значение переменной v снова увеличится на 1 и функция вернет значе-
ние 2. И так далее.
Пример использования модификатора static.
Исключение повторного выполнения функции
Иногда необходимо исключить повторное выполнение некоторой
функции. Например, при первом вызове функции необходимо произве-
сти инициализацию какого-то устройства или переменных, а при последующих вызовах – не делать повторной инициализации.
Статические переменные предоставляют возможность красивого
решения этой проблемы.
Рассмотрим функцию one_running_func().
void one_running_func(){
static bool already_runned=false;
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]
