Препроцессор
Препроцессор — это программа, которая подготавливает исходный код программы к компиляции, совершая подстановки и включения файлов, а также вводит макроопределения. Рассмотрим основные директивы препроцессора (они начинаются со знака решетки, #
).
Директива #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__
на текущее время (на момент обработки кода препроцессором);- и другие.