logo
HCS12 с применением языка С - royallib

3.6.1. Директивы условной компиляции

Директивы #if, #ifdef, #ifndef, #else, #elif и #endif относятся к группе директив условной компиляции. Эти директивы используются для того, чтобы обозначенный фрагмент исходного текста программы можно было бы включать или не включать в компилируемый код в зависимости от выполнения некоторого наперед заданного условия. Такое действие может быть полезным, например, в процессе отладки программы. Тогда в отладочной версии программы промежуточные результаты вычислений будут выводиться на экран дисплея, в рабочей версии эти действия выполняться не будут.

Директивы #if и #endif обязательно используются вместе, чтобы обозначить начало и конец условно компилируемого текста программы. В строке с директивой #if записывается условие компиляции. Если это условие выполняется, то выражения, записанные в программе между директивами #if и #endif включаются в компилируемый текст программы. В противном случае эти выражения исключаются из генерированного кода программы. Например, в процессе отладки мы хотим вывести на экран указанную в тексте программы фразу:

1   #include <stdio.h>

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>

Символы <> указывают на определенное место расположение файла 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.