Препроцессор

Препроцессор — это программа, которая подготавливает исходный код программы к компиляции, совершая подстановки и включения файлов, а также вводит макроопределения. Рассмотрим основные директивы препроцессора (они начинаются со знака решетки, #).

Директива #include

Директива include позволяет включать содержимое файлов на место строчки, где она написана. Если этот файл располагается в стандартной директории (имеется в системных путях операционной системы), то его имя, как правило, заключается в угловые скобки <>, если же файл находится в текущем каталоге, то используются кавычки "".

#include <stdio.h>          // system path directory
#include "ds18b20.h"        // current directory
#include "config/device.h"  // nested folder

В последней строке подключаемый файл лежит во вложенном каталоге. Такой подход редко используется, обычно в настройках проекта (в IDE) указывают все пути к исходным файлам.

Директива #define

Хороший код от плохого отличается в том числе тем, что в нем отсутствуют непонятные константы в середине исходного файла, которые еще называют хардкодом (англ. hardcode). Для создания констант часто применяется директива #define. Общая форма записи при этом выглядит следующем образом:

#define [ИДЕНТИФИКАТОР][ОТСТУП][ЗАМЕНА] 

Это очень удобно, когда одно и то же значение используется множество раз в одном и том же файле и участвует, скажем, в настройке периферии. Например,

#define REG_DIGIT_0 (1 << 8)
// ...
void max7219_clean() {
    send_data(REG_DIGIT_0 | 0x00);
    // ...
}

Достаточно один раз определить значение REG_DIGIT_0, и (1 << 8) будет подставлено во все места, где указывается макрос.

С помощью суффиксов можно определять тип данных:

#define A             1200U    // unsigned int
#define B             1200L    // long int
#define C             1200UL   // unsigned long int
#define D             1200LL   // long long int
#define E             1200ULL  // unsigned long long int
#define F             0.0      // double
#define G             0.0F     // float

Для того что бы гарантировать тип данных на этапе компиляции, можно использовать макросы вида UINT32_C. Их часто используют при определении констант.

#define UINT32_C(value) \
    __CONCAT(value, UL)

Используя ту же директиву, можно создавать макросы для каких-нибудь формул или, наоборот, вставлять туда участки кода.

// circle area
#define S(x)            (3.1415926f * x * x)
// enable port A clocking
#define RCC_PORT_A_ON()   (RCC->APB2ENR |= RCC_APB2ENR_IOPBEN)
// ...
int main(void) {
    RCC_PORT_A_ON();
    int a = S(10);
    return 0;
}

Данную директиву можно использовать еще одним способом, в частности, создавая «метку» для препроцессора.

//#define PCB_REV_A
#define PCB_REV_B

Совмещая ее с условными директивами, можно добиться включения нужного кода в сборке. Допустим, у нас имеется две версии платы, причем на разных платах для управления реле используются разные ножки микроконтроллера. Тогда, используя макросы, можно в зависимости от раскомментированной метки включить в сборку инициализацию именно той ножки, к которой подключен управляющий контакт.

Чтобы убрать метку, можно воспользоваться директивой #undef.

Условные директивы

Описанный выше сценарий с включением нужного кода в сборку можно реализовать при помощи директив #ifdef / #ifndef. Например так:

#ifdef PCB_REV_A
#define BUTTON_PORT GPIOA
#else
#define BUTTON_PORT GPIOB
#endif

Если PCB_REV_A была где-то определена ранее, то создается макрос BUTTON_PORT со значением GPIOA, в противном случае — с GPIOB. По такому же принципу построена «защита» от двойного включения файлов при использовании директивы #include. Описать словами это можно следующим образом: если не определена метка, определим ее, далее делаем что-то полезное. В противном случае ничего не делаем либо переходим к секции #else.

Помимо защитных скобок #ifndef / #define / #endif допустимо использовать директиву #pragma once. Она не входит в стандарт языка, но поддерживается большинством компиляторов. При ее использовании предотвращает появление коллизий имён (включение одного и того же файла несколько раз) компилятор, без участия препроцессора. В общем случае время компиляции уменьшается.

Можно поступить по-другому: определить некоторую константу и в зависимости от ее содержимого принимать решение.

#define PCB_VERSION     2
#if PCB_VERSION == 1
#define BUTTON_PORT     GPIOA
#elif PCB_VERSION == 2
#define BUTTON_PORT     GPIOB
#else
#define BUTTON_PORT     GPIOC
#endif

Другие директивы и макросы

В работе могут пригодится и другие директивы. Допустим, для предотвращения зависаний при работе устройства используется сторожевой таймер (англ. watchdog timer). Суть его работы заключается в том, что если некоторый регистр не обнулить до того, как таймер дойдет до определенного значения, то микроконтроллер будет принудительно перезагружен. При отладке эта функция скорее вредит, чем помогает: микроконтроллер просто перезагрузится, пока вы обдумываете значение какой-нибудь переменной. В таком случае таймер лучше отключить.

Космический аппарат Clementine занимался картографированием поверхности Луны. Маневровыми двигателями управляли с земной станции, но в случае аварийной ситуации бортовой компьютер мог принять решение самостоятельно. 7 мая 1994 года, когда аппарат уже сошёл с орбиты Луны, произошла исключительная ситуация при работе с плавающей точкой. Данное событие не было чем-то необычным, предыдущие ~3000 были обработаны корректно. Однако после этого события аппарат стал отправлять случайные данные, а затем и вовсе одно и тоже. Операторы потратили 20 минут в попытках перезапустить систему, отправляя команды, но все они были проигнорированы. Через какое-то время произошёл аппаратный сброс (по всей видимости, когда закончилось топливо), и аппарат вернулся к жизни. Как оказалось, бортовой компьютер завис, и включив маневровый двигатель, израсходовал всё топливо и раскрутил аппарат до 80 оборотов в минуту. Встроенный в процессор Honeywell 1750 сторожевой таймер не был использован, несмотря на возражения главного инженера. Пара строк кода могла спасти миссию. // http://www.ganssle.com/item/great-watchdog-timers.htm

При некоторых условиях сторожевой таймер может зависнуть, как это происходит в контроллерах AVR в поле сильного ЭМИ.

Никто не застрахован от человеческого фактора: в финальной сборке можно просто забыть включить его обратно. На помощь приходит директива #warning.

int main(void) {
#ifdef RELEASE
    watchdog_init();
#else
#warning "ATTENTION! Debug mode is on. WatchDog is off."
#endif
    //...
    return 0;
}

При компиляции сторожевой таймер будет включен, если определена метка RELEASE. В противном случае компилятор выдаст предупреждение.

Есть и другие директивы. #error выведет сообщение и остановит работу компилятора. Директива #line укажет имя файла и номера текущей строки для компилятора.

Кроме директив, есть предопределенные константы. Например:

  • __LINE__ заменяется на номер текущей строки;
  • __FILE__ на имя файла;
  • __FUNCTION__ на имя текущей функции;
  • __DATE__ на текущую дату;
  • __TIME__ на текущее время (на момент обработки кода препроцессором);
  • и другие.

Изменено:

Язык Си: 3 комментария

  1. >Вместо нуля может быть почти любой другой символ. Плюс и минус работают по-другому. Для выравнивания числа по по левому краю, а не по правому, перед числом нужно поставить знак минус.
    Лишне по

  2. Термин «регистр ядра» довольно специфичен. Звучит не привычно. А почему не использовать термин CPU или процессор или на худой конец микроконтроллер?

  3. Смысл статических переменных вне функции мне кажется не раскрыт. Такой модификатор делает переменную недоступной из других единиц трансляции. Т.е. не будет конфликта имен между разными .с файлами.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.