В этом параграфе мы расширим наши знания о технике программирования на Си посредством знакомства с заголовочными файлами (header file). Заголовочный файл — это внешний файл, помещаемый в начало программы с помощью директивы #include, обычно содержащий определения типов и переменных, используемых в программе. Язык Си предоставляет программисту некоторый набор стандартных функций, определения которых находятся в нескольких заголовочных файлах. Например, в созданной нами функции compute мы использовали функцию извлечения квадратного корня sqrt, которая определена в файле математических функций math.h.
Выражения языка С для включения файлов заголовков в модуль разрабатываемой программы обычно располагаются в начале программы. Заголовочные файлы содержат определения переменных, макросы, объявления функций, позволяя программисту возможность вызывать эти функции, использовать переменные и макросы без дополнительного определения их в тексте создаваемой программы. В процессе компиляции значения постоянных переменных замещают их символьные значения, упомянутые в основной программе. Далее в примерах программ мы будем использовать заголовочный файл stdio.h, в котором определены функции библиотеки стандартного ввода/вывода. Эти функции позволяют отобразить результаты преобразования данных в МК на экране дисплея. А также передать в МК код нажатой клавиши на клавиатуре. Поставляемые фирмами производителями программного обеспечения компиляторы уже содержат библиотеки и соответствующие им заголовочные файлы. Например, нами будет использована библиотека математических функций, и соответствующий ей файл math.h. Во многих случаях у пользователя возникает необходимость создания своего собственного заголовочного файла, в котором будут содержаться определения констант. Для того чтобы включить файл заголовка в разрабатываемый программный модуль, следует воспользоваться директивой #include. Приведем три примера:
#include
#include
#include "myheader.h"
В первых двух записях имя подключаемого файла заключено в «<>», что информирует компилятор о том, что названные файлы располагаются в определенной директории (папке). Обычно это папка с именем include, которая располагается в основном каталоге компилятора Си. В третьей записи имя подключаемого файла заключено в двойные кавычки. Для компилятора это означает, что данный файл располагается в той же папке, что и создаваемый программный модуль.
3.6. Директивы компилятора
Директивы компилятора — это инструкции для программы компилятора, которые указывают ему каким образом следует обрабатывать исходный текст программы. Достаточно часто эти инструкции называют директивами препроцессора компилятора, акцентируя внимание пользователей на том, что эти директивы выполняют обработку исходного текста программы перед тем, как компилятор начнет генерацию ассемблерного текста программы. Известно 11 директив компилятора Си: #if, #ifdef, #ifndef, #else, #elif, #include, #define, #undef, #line, #error, #pragma. Из приведенного списка понятно, что директивы отмечаются символом # в первом знаке имени. Далее мы рассмотрим наиболее часто используемые директивы.
3.6.1. Директивы условной компиляции
Директивы #if, #ifdef, #ifndef, #else, #elif и #endif относятся к группе директив условной компиляции. Эти директивы используются для того, чтобы обозначенный фрагмент исходного текста программы можно было бы включать или не включать в компилируемый код в зависимости от выполнения некоторого наперед заданного условия. Такое действие может быть полезным, например, в процессе отладки программы. Тогда в отладочной версии программы промежуточные результаты вычислений будут выводиться на экран дисплея, в рабочей версии эти действия выполняться не будут.
Директивы #if и #endif обязательно используются вместе, чтобы обозначить начало и конец условно компилируемого текста программы. В строке с директивой #if записывается условие компиляции. Если это условие выполняется, то выражения, записанные в программе между директивами #if и #endif включаются в компилируемый текст программы. В противном случае эти выражения исключаются из генерированного кода программы. Например, в процессе отладки мы хотим вывести на экран указанную в тексте программы фразу:
1 #include
2 #define DEBUG 1
3 void main(void)
4. {
:
:
m #if DEBUG
m+l printf{"The program reached this point in the program\n"};
m+2 #endif
:
:
n }
Для этого в строке 2 мы присвоили переменной DEBUG значение 1, используя директиву препроцессора #define. Эту директиву мы обсудим несколько позже, а пока констатируем, что единичное значение переменной DEBUG соответствует условию «истина» в строке m с директорией #if. Поэтому вызов функции prinf, записанный в строке m+1, будет включен в компилируемый текст программы. При ее исполнении мы увидим строку «The program reached this point in the program» на экране монитора. Обратите внимание, что точка с запятой в конце строки с директивой не ставится.
В рассмотренном выше примере предполагалось только две возможности: включать или не включать в исполняемый код программы определенный фрагмент. Директивы #else и #elif расширяют возможности условной компиляции и позволяют выбрать для компиляции один из нескольких фрагментов текста. Например:
1 #define М68НС11 0
2 #define М68НС12 1
3 #define М8051 2
4 #define Processor 1
5 void main(void)
6 {
7 #if Processor == М68НС11
8 Instruction(s) А
9 #elif processor == М68НС12
10 Instruction(s) В
11 #elif processor == М8051
12 Instruction(s) С
13 #else
14 Instruction(s) D
15 #endif
16 }
В этом примере исходный текст программы написан таким образом, что он может быть компилирован для исполнения различными микроконтроллерами: Motorola HC11, Motorola HC12 и Intel 8051. Директивы #if, #elif и #else позволяют для данного сеанса компиляции выбрать конкретный тип МК. Для этого в строках 1…3 программы каждому символьному имени МК присвоено определенное численное значение. Далее в зависимости от выбранного типа МК переменной Processor присваивается желаемое значение. В примере мы собираемся компилировать программу для МК HC12, поэтому присвоили переменной Processor значение 1. В строке 7 компилятор проверяет истинность выражения, записанного в качестве условия директивы #if. Это условие не выполняется, поскольку Processor = 1 ≠ М68НС11 = 0. Поэтому группа инструкций Instruction(s) A не будет включена в программу. Далее в строке 9 компилятор обнаружит выполнение условия директивы #elif, и выражения Instruction(s) B будут присутствовать в конечном варианте программы. Условие строки 11 не выполняется, и группа инструкций Instruction(s) C в исполняемом коде программы присутствовать не будет. Если ни одно из условий для директив #elif не выполнено, то выражения, следующие за директивой #else, будут включены в программу автоматически.
Воспользуемся приведенной конструкцией условной компиляции. Допустим, мы предполагаем исполнение некоторого программного кода как на МК семейства Motorola HC11, так и на МК семейства Motorola 68HC12. Эти МК имеют различные карты памяти, и, соответственно, их порты ввода/вывода расположены по различным адресам. Для возможной адаптации текста программы к одному из типов МК воспользуемся директивами условной компиляции:
1 #if (Processor == 68НС11)
2 #define PORTA *(unsigned char volatile *) (0х1000)
3 #elsif (processor == 68НС12)
4 #define PORTA *(unsigned char volatile *) (0х0000)
5 #endif
В строках 1 и 3 располагаются директивы, которые проверяют условия компиляции. Значение переменной Processor должно быть определено выше по тексту программы директивой #define, или в подключаемом заголовочном файле. Строки 2 и 4 содержат директивы определения адреса для порта PORTA для двух различных типов МК. Директива #endif в строке 5 отмечает окончание фрагмента текста, который подлежит условной компиляции.
Директивы #ifdef и #ifndef используются для организации процесса компиляции при условии, что некоторая переменная с указанным именем была определена (#ifdef) или не определена (#ifndef) в тексте программы. Например:
1 #ifdef OUTPUT
2 Instruction(s) А
3 #else
4 Instruction(s) B
5 #endif
Если переменная с именем OUTPUT была определена в тексте программы до строки 1 с директивой #ifdef, то группа инструкций Instruction(s) А будет включена исполняемый код программы. В противном случае в конечный вариант программы будет включена группа инструкций Instruction(s) B.
Другой пример:
1 #ifndef OUTPUT
2 Instruction(s) А
3 #else
4 Instruction(s) B
5 #endif
Если переменная с именем OUTPUT не была определена в тексте программы до строки 1 с директивой #ifndef, то в конечный вариант программы будет включена группа инструкций Instruction(s) А. Если же эта переменная была определена ранее, то исполняемый код программы будет включена группа инструкций Instruction(s) B.
Директива #define используется в двух случаях. Во первых, она позволяет задать численные значения для символьных констант. Например, константе с именем HIGH необходимо присвоить значение 98:
#define HIGH 98
После записи этого выражения, если в тексте программы будет использовано имя HIGH, то при компиляции оно будет заменяться числом 98. Это удобно, поскольку в тексте программы имя HIGH может быть упомянуто сколь угодно большое число раз. Но для изменения его численного значения понадобится внести изменения только в одну строку с директивой #define.
Во вторых, директива #define используется для определения макросов. Макрос — это набор выражений языка Си, которому поставлено в соответствие определенное имя. При записи этого имени в программе, компилятор произведет замену этого имени обозначенным набором выражений. Например, Вам необходимо разрешить прерывания в МК. Для этого в МК 68HC12 используется команда ассемблера CLI. Для ее записи в тексте программы на Си определяют макрос:
#define CLI() asm("cli\n"); //разрешить маскируемые прерывания
Далее в программе используют только имя макроса:
CLI();
Кроме директивы определения символа или макроса #define, существует директива обратного действия #undef. Приведем пример ее использования:
#define VALUE 10
int number[VALUE];
#undef VALUE
В этом примере мы сначала назначили переменной VALUE значение 10. Далее в строке 2 мы воспользовались этим значением, чтобы определить массив целых чисел из 10 элементов. Далее переменная VALUE нам не нужна. И мы отменили ее определение директивой #undef.
Следующая рассматриваемая нами директива — это директива #include. Ранее мы установили, что эта директива используется для присоединения к разрабатываемому программному модулю другого файла. При этом у программиста появляется возможность использовать в тексте программы ранее объявленные переменные или вызывать функции, которые были определены в другом файле. Присоединяемые файлы называют заголовочными файлами. Например, следующая запись необходима для присоединения к разрабатываемой программе файла стандартных функций ввода/вывода:
#include
Символы <> указывают на определенное место расположение файла stdio.h в папках директории компилятора.
Назначение директивы #error — упрощение процесса отладки разрабатываемой программы. Вы можете записать следующее выражение:
#error Programm made a logic error
Если в процессе выполнения программа достигнет приведенной строки, то на экран будет выведено приведенное сообщение.
Также для целей отладки используется директива #line. Эта директива отмечает номерами те инструкции программы, которые следуют за директивой. В результате, в процессе отладки можно идентифицировать тот фрагмент программы, который исполняется в текущий момент отладки.
Функции директивы #pragma определяются конкретным типом используемого компилятора. Для компилятора ICC12 эта директива определения сегментов данных и программы в исходном тексте программы на Си, для объявления подпрограмм прерывания, а также для присвоения желаемых значений ячейкам памяти с фиксированными адресами. Последнее позволяет инициализировать таблицу векторов прерываний в микроконтроллерах. Приведенный ниже пример демонстрирует использование директивы #pragma для объявления подпрограммы с именем TOISR в качестве подпрограммы прерывания:
#pragma interrupt_handler TOISR()
void TOISR(void);
Объявление подпрограммы TOISR как подпрограммы прерывания информирует компилятор о том, что в конце этой подпрограммы он должен расположить ассемблерную инструкцию возврата из прерывания rti. В конце обычной функции компилятор подставляет инструкцию возврата из подпрограммы rts.
Директива #pragma также используется для задания начального адреса расположения в памяти сегментов программного кода или кодов данных. Запишем вектор прерывания для подпрограммы TOISR в таблицу векторов прерывания МК. Мы знаем, что в соответствие с картой памяти МК, вектор прерывания по переполнению таймера должен располагаться по адресу 0x0B1E. Следующая запись помещает адрес начала подпрограммы TOISR в две ячейки памяти, начиная с адреса 0x0B1E:
#pragma abs_adress:0xB1E
void (*Timer_Overflow_interrupt_vector[])() = {TOISR};
#pragma end _abs_adress
Более подробно оформление подпрограмм прерывания мы обсудим в главе 4.
3.7. Конструкции программирования