Введение в язык программирования C для встраиваемых приложений

Добавлено 25 мая 2019 в 14:28

В данной статье рассматриваются основные характеристики языка C, простого языка, который до сих пор широко используется для программирования микроконтроллеров.

Введение в язык программирования 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-код значительно превосходит ассемблерный код с точки зрения организации и читабельности, и это в значительной степени связано с использованием функций.

Функции – это блоки кода, которые могут быть легко включены в другие фрагменты кода. Принуждение процессора выполнить инструкции, содержащиеся в функции, называется «вызовом» функции. Функция может принимать один или несколько аргументов в качестве входных данных и может предоставлять одно возвращаемое значение в качестве выходных данных.

Функции, операторы условий и циклы в C позволяют довольно легко перевести алгоритм выполнения в рабочий код. Этот пример взят из проекта, в котором я использовал шину SPI для управления LCD.
Функции, операторы условий и циклы в C позволяют довольно легко перевести алгоритм выполнения в рабочий код. Этот пример взят из проекта, в котором я использовал шину SPI для управления LCD.

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

Вот пример функции, которая принимает три числа в качестве входных данных и использует эти данные для генерирования возвращаемого значения 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, о которых вы хотели бы узнать больше, напишите об этом в комментариях ниже.

Теги

MCUВысокоуровневые языки программированияВычисления во встраиваемых системахМикроконтроллерРазработка ПО для встраиваемых системЯзык C

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.