Введение в язык программирования C для встраиваемых приложений
В данной статье рассматриваются основные характеристики языка C, простого языка, который до сих пор широко используется для программирования микроконтроллеров.
По стандартам современных технологий C – довольно старый язык. Его первоначальная разработка происходила в начале 70-х годов, после чего были изменения в конце 70-х и стандартизация в 80-х. Тем не менее, по моему мнению, он не потерял своей силы. Это всё еще отличный язык для встраиваемых приложений, и, по моему опыту, он обеспечивает подходящую среду программирования для всего: от простых устройств на микроконтроллерах до сложной цифровой обработки сигналов.
Необходимость в C
Я не сомневаюсь, что есть, по крайней мере, несколько инженеров-электронщиков, которые не знают, как написать программу на C, и никогда не будут нуждаться в этом. Если вы из тех людей, которые предпочитают аппаратное обеспечение программному, вы можете считать этих людей «счастливчиками».
Нравится нам это или нет, программирование становится всё более важной частью электротехники, и на самом деле я нахожу большое удовлетворение в том, что могу не только проектировать печатные платы, но и писать прошивки для этих плат. Эти два аспекта разработки систем тесно связаны, и я подозреваю, что конечный результат часто оказывается лучше, когда проектирование платы и разработка прошивки выполняются одним и тем же человеком.
C против ассемблера
В теории я сторонник ассемблера. В действительности я достиг той точки, когда язык ассемблера представляет угрозу, как моей финансовой безопасности, так и моему рассудку. Написание прошивок на ассемблере медленно и подвержено ошибкам, а поддержание адекватного уровня организации в длинных и сложных программах безнадежно сложно.
Однако я, безусловно, буду настаивать на том, что вы не можете по-настоящему понимать языки высокого уровня, если не понимаете ассемблер. Если у вас никогда не было возможности получить солидный опыт работы с языком ассемблера, вы должны хотя бы ознакомиться с некоторыми основными понятиями, прежде чем погрузиться в C.
Что понимают процессоры?
Только машинный код. Единицы и нули. Все «дружественные к программисту» аспекты языка C должны быть в конечном итоке переведены в низкоуровневую реальность цифрового аппаратного обеспечения процессора (то есть в двоичную арифметику, логические операции, передачу данных, регистры и области данных).
Конечно можно успешно написать программу на C, ничего не зная о реальном оборудовании, но в контексте разработки встраиваемых систем полезно и иногда необходимо понимать и ваше аппаратное обеспечение, и то, как ваш код на C взаимодействует с этим аппаратным обеспечением.
Основы
Программы на C варьируются от довольно простых до очень сложных. В мире встраиваемой электроники многие программы стремятся к простоте, а описанные ниже основные элементы программирования обеспечивают хорошую основу для дальнейшего изучения разработки прошивок на языке C.
Директивы включения
Программа на C для встраиваемой системы будет начинаться хотя бы с одной директивы #include
. Эти директивы используются для включения содержимого отдельного файла в исходный файл. Это удобный способ для упорядочивания вашего кода, а также он позволяет использовать функциональные возможности библиотек, процедуры конфигурирования аппаратного обеспечения и определения регистров, предоставляемые производителем.
Фрагмент кода, приведенный ниже, показывает директивы включения, которые я использовал в одном из своих проектов на микроконтроллере. Обратите внимание, что файл “Project_DefsVarsFuncs.h” – это пользовательский заголовочный файл, созданный программистом (то есть мной). Я использовал его как удобный способ для включения определений препроцессора, переменных и прототипов функций в несколько исходных файлов.
//-----------------------------------------------------------------------------
// Includes
//-----------------------------------------------------------------------------
#include "Project_DefsVarsFuncs.h"
#include "InitDevice.h"
#include "cslib_config.h"
#include "cslib.h"
Определения препроцессора
Вы можете использовать оператор #define
для создания строки, которая будет заменена числом. Определения препроцессора не обязательны, но в некоторых ситуациях они чрезвычайно полезны, поскольку позволяют легко изменять значение, которое появляется в различных частях вашей программы.
Например, предположим, что вы используете АЦП микроконтроллера, и что ваш код использует частоту дискретизации АЦП в нескольких отдельных вычислениях. Определение препроцессора позволяет вам использовать интуитивно понятную строку (например, SAMPLE_RATE) вместо самого числа в коде вычислений, и если вы экспериментируете с разными частотами дискретизации, вам нужно всего лишь изменить одно числовое значение в определении препроцессора.
#define SAMPLE_RATE 100000
Вы можете изменить 100000 на любое другое число, и это новое число будет использоваться для замены всех экземпляров строки SAMPLE_RATE.
Определения препроцессора также являются отличным способом сделать код более читабельным. Ниже приведет список удобных директив #define
, которые я включаю во все мои проекты по разработке прошивок.
#define BIT7 0x80
#define BIT6 0x40
#define BIT5 0x20
#define BIT4 0x10
#define BIT3 0x08
#define BIT2 0x04
#define BIT1 0x02
#define BIT0 0x01
#define HIGH 1
#define LOW 0
#define TRUE 1
#define FALSE 0
#define SET 1
#define CLEARED 0
#define LOWBYTE(v) ((unsigned char) (v))
#define HIGHBYTE(v) ((unsigned char) (((unsigned int) (v)) >> 8))
Важно понимать, что определения препроцессора не имеют прямого отношения к аппаратному обеспечению. Вы просто говорите препроцессору о необходимости заменить одну строку символов другой строкой символов перед компиляцией программы.
Переменные
Процессоры хранят данные в регистрах и ячейках памяти. На самом деле, в отношении аппаратного обеспечения не существует такой вещи, как переменная. Однако для программиста написание кода становится намного проще, когда вместо адресов памяти или номеров регистров мы можем использовать переменные с интуитивно понятными именами.
Компиляторы могут управлять низкоуровневыми деталями, связанными с переменными, без особого ввода со стороны программиста, но если вы хотите оптимизировать использование переменных, вам необходимо знать кое-что о конфигурации памяти устройства и о том, как оно обрабатывает данные с различной шириной в битах.
В следующем фрагменте кода приведен пример определения переменной. Это было написано для компилятора Keil Cx51, который резервирует один байт памяти для определения "unsigned char
", два байта для определения "unsigned int
" и четыре байта для определения "unsigned long
".
unsigned long Accumulated_Capacitance_Sensor1;
unsigned long Accumulated_Capacitance_Sensor2;
unsigned int Sensor1_Unpressed;
unsigned int Sensor2_Unpressed;
unsigned int Sensor1_Measurement;
unsigned int Sensor2_Measurement;
unsigned int AngularPosition;
unsigned int TouchDuration;
unsigned char CurrentDigit;
unsigned int CharacterEntry;
unsigned char DisplayDivider;
Операторы, операторы условий и циклы
Ядро вычислительных возможностей состоит из перемещения данных, выполнения математических вычислений и логических операций с данными, а также принятия программных решений на основе ценности хранимых или генерируемых данных.
Математические операции и битовые манипуляции выполняются с помощью операторов. C имеет довольно много операторов: равно (=
), сложение (+
), вычитание (-
), умножение (*
), деление (/
), побитовое И (&
), побитовое ИЛИ (|
) и так далее. «Входные данные» для выражения оператора являются переменными или константами, а результат сохраняется в переменной.
Операторы условий позволяют вам выполнять или не выполнять действие в зависимости от того, является ли данное условие истинным или ложным. В этих операторах используются слова "if
" (если) и "else
" (иначе); например:
if(Sensor1 < Sensor2 && Sensor1 < Sensor3)
return SENSOR_1;
else if(Sensor2 < Sensor1 && Sensor2 < Sensor3)
return SENSOR_2;
else if(Sensor3 < Sensor2 && Sensor3 < Sensor1)
return SENSOR_3;
else
return 0;
Циклы for
и циклы while
обеспечивают удобное средство многократного выполнения блока кода. Эти типы задач во встраиваемых приложениях возникают очень часто. Циклы for
в большей степени ориентированы на ситуации, в которых блок кода должен выполняться определенное количество раз, а циклы while
удобны, когда процессор должен продолжать повторять один и тот же блок кода, пока условие не изменится с true
на false
. Вот примеры обоих типов.
for (n = 0; n < 16; n++)
{
Accumulated_Capacitance_Sensor1 += Measure_Capacitance(SENSOR_1);
Delay_us(50);
Accumulated_Capacitance_Sensor2 += Measure_Capacitance(SENSOR_2);
Delay_us(50);
}
while(CONVERSION_DONE == FALSE);
{
LED_STATE = !LED_STATE;
Delay_ms(100);
}
Функции
Хороший C-код значительно превосходит ассемблерный код с точки зрения организации и читабельности, и это в значительной степени связано с использованием функций.
Функции – это блоки кода, которые могут быть легко включены в другие фрагменты кода. Принуждение процессора выполнить инструкции, содержащиеся в функции, называется «вызовом» функции. Функция может принимать один или несколько аргументов в качестве входных данных и может предоставлять одно возвращаемое значение в качестве выходных данных.
Использование функций связано с некоторыми накладными расходами, поэтому мы должны быть осторожны, чтобы не обременять процессор чрезмерным числом вызовов функций, но, в целом, преимущества функций намного превышают затраты.
Вот пример функции, которая принимает три числа в качестве входных данных и использует эти данные для генерирования возвращаемого значения true
или false
.
bit Is_In_Range(int input, int LowerBound, int UpperBound)
{
if(input >= LowerBound && input <= UpperBound)
return TRUE;
else
return FALSE;
}
Заключение
Подробное обсуждение языка C может продолжаться практически до бесконечности, и данная статья сделала это только поверхностно. Мне стыдно даже публиковать что-то, что упускает так много важной информации, но мы должны с чего-то начать. Мы планируем опубликовать еще немало статей об использовании языка C во встраиваемых приложениях, и ссылки на эти статьи будут появляться в содержании к серии статей.
Если у вас есть какие-либо темы, связанные с C, о которых вы хотели бы узнать больше, напишите об этом в комментариях ниже.