Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Микропроцессорные системы. Средства разработки программного обеспечения для микроконтроллеров семейства AVR. Учебное пособие

.pdf
Скачиваний:
0
Добавлен:
07.09.2026
Размер:
1 Мб
Скачать
☆
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;
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]