Язык Си

Прежде чем приступать к изучению синтаксиса языка, необходимо обратить внимание на то, как следует писать код. Он может быть как абсолютно непонятным, нечитаемым, так и выглядеть как «рассказ» — код, на который будет приятно смотреть, и происходящее там будет ясно не только разработчику, но и другим людям. Читаемость кода — очень важная вещь. Если вы, находясь в контексте задачи, напишете что-то такое:

то какова вероятность того, что вы поймете, что делает этот код, месяц спустя? А что насчет вашего коллеги, которому придется доделывать ваш проект? Такой код следует привести к более читаемому виду.

Переменным стоит давать \emph{понятные} и осмысленные названия. В таком случае разъяснять в комментариях сам код даже не придется. Другой очевидный совет — нужно соблюдать общепринятое (или принятое в компании) форматирование кода. Хорошим примером стиля написания для языка С++ является Code Style Guide от компании Google.

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

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

Директива #include

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

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

Директива #define

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Комментарии

С одной стороны, хорошему коду комментарии не нужны, он и так хорошо читается. С другой стороны, в больших проектах необходимо документировать код. В языке Си есть два вида комментариев: однострочные и многострочные.

Закомментировать можно что угодно, в том числе часть кода. Переусердствовать не стоит: для хранения предыдущей версии функции лучше использовать систему контроля версий, нежели заключать ее в комментарий и тянуть до конца проекта. А вот для конфигурации через макросы данный подход более чем уместен. Ниже приведен пример кода из стандартной библиотеки STM32.

Раскомментировав макрос, мы подскажем препроцессору, например, номера прерываний, которые соответствуют данному МК.

Кроме того, комментарии часто используют, чтобы автоматически генерировать документацию. Для проектов, написанных на языках Си и C++, стандартом является кроссплатформенная система Doxygen, которая поддерживает форматы HTML, RTF, man, XML и даже LaTeX. Пример комментария-документации выглядит так:

Если вы работаете в среде Eclipse (или производных от нее, как CoIDE), то для организации процесса переписывания плохих участков кода (то есть тех, в которых была обнаружена ошибка или возможно использовать лучший алгоритм для решения задачи) стоит использовать слова-теги.

В среде Eclipse есть специальное окно, в котором отображаются все подобные метки.

Типы данных

В языке Си строгая статическая типизация, в отличии от Python, в котором допустимо создать переменную типа «строка», а в ходе выполнения программы переделать ее в число с плавающей запятой. В Си, если вы создали переменную a как целое число, записать в нее вещественное число получится только с приведением типов (т.е. с потерей дробной части), и то только в случае, если размер целочисленной переменной в байтах больше или равен размеру переменной вещественного типа.

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

Имена переменных имеют некоторые ограничения: они не могут начинаться с цифр или содержать спецсимволы (!, @ и т. д.) Знак подчеркивания _ считается буквой и часто используется для улучшения читаемости.

Данный символ можно использовать и в начале имени, однако в основной программе лучше так не делать, так как по договоренности такое именование принято в сторонних библиотеках. Ниже приведен пример системной функции из библиотеки CMSIS.

Регистр также имеет значение, поэтому переменные с именами X и x будут разными.

ТипРазмерПримечание
void0 байтБуквально означает «ничего». Появился в стандарте c89 и является самым необычным типом. Нельзя создать переменную с этим спецификатором, однако можно использовать его для функций, которые ничего не возвращают, либо как тип указателя (об этом позже).
_Bool1 байтДанный тип появился начиная со стандарта c99, в файле <stdbool.h> для него определен псевдоним bool, а также макросы true (истина) и false (ложь). Любое вписанное ненулевое значение хранится как 1.
char1 байтПрименяется для представления символьной информации. Занимает 8 бит и кодирует печатные и непечатные символы (например пробел, перевод каретки и т.д.) по таблице символов ASCII (абр. American Standard Code for Information Interchange), где первые 128 позиций являются стандартными и неизменяемыми, а остальные отводятся на региональные расширения, зависящие от особенностей языка. Есть и другие системы кодов, например EBCDIC (англ. Extended Binary Coded Decimal Interchange Code).
int4 байтаИспользуется для описания целочисленных переменных, по умолчанию является знаковым и для 32-битной системы занимает 4 байта (32 бита).
float4 байтаИспользуется для описания чисел с плавающей запятой одинарной точности.
double8 байтДля более точных расчетов можно использовать число с плавающей точкой двойной точности.

Размер (в байтах, а значит, и диапазон значений) переменных зависит от разрядности системы. Так, int в 16-разрядном микроконтроллере может занимать 16 бит, а в 32-разрядном будет занимать 32 бита. Следовательно, код, написанный под одну платформу, может некорректно работать на другой. Максимальные и минимальные значения типов можно найти в файле <limits.h>. Тема циклов еще впереди, однако подумайте, в чём может быть проблема при выполнении следующего кода на stm8 и почему он нормально отработает на stm32? Код в фигурных скобках будет выполнятся 65700 раз.

Правильно! Произойдет переполнение: так как int на stm8 равняется всего 16 битам, максимальное значение, которое может принять i, равняется 216-1, что меньше, чем 65700. При достижении максимального значения переменная перейдет в ноль, и счет начнется заново, поэтому данный цикл никогда не завершится.

В стандарте c99 определены дополнительные типы целочисленных переменных, которые можно найти в <inttype.h> (или через <stdint.h>). Если необходимо задать точный размер переменной, можно использовать типы intN_t / uintN_t (где N = { 8, 16, 32, 64 }). Приставка fast (int_fastN_t / uint_fastN_t) позволит использовать максимально производительную переменную на данный платформе (подробнее в главе об оптимизации). Приставка least (int_leastN_t / uint_leastN_t) гарантирует минимальный размер с учетом платформы, но точно не меньший, чем N. Также имеются типы для указателей intptr_t / uintptr_t, которые гарантируют, что с их помощью можно хранить адрес.

... конец ознакомительного фрагмента ...


Назад | Оглавление | Дальше


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