Пять советов по использованию функций в прошивках на языке C
В данной статье мы закончим наше исследование функций в языке C некоторой дополнительной информацией и несколькими полезными методами реализации.
Базовая функция на C – например, один или два аргумента и возвращаемое значение – это не сложная вещь. Тем не менее, функции C довольно гибки, и, выходя за рамки основ, вы можете писать код проще и добавлять в свою прошивку некоторые полезные особенности.
1. Помещайте прототипы своих функций в заголовочный файл
Иногда я задаюсь вопросом, должен ли человек быть специалистом в IT, чтобы полностью понять прототипы функций на языке C. Это действительно довольно сложный вопрос. Я думаю, что инженеры по разработке встроенного программного обеспечения могут спокойно игнорировать детали и просто принять стандартный курс действий, такой как следующий: всегда помещать прототипы в заголовочный файл и включать этот заголовочный файл в исходный файл.
Прототип функции – это одна строка кода, которая предоставляет компилятору типы данных для аргументов функции и возвращаемого значения. Например:
float ArithmeticMean(char Value1, char Value2, char Value3);
На самом деле нет необходимости включать имя входного параметра, потому что всё, что компилятор хочет знать на данном этапе, это тип данных. Таким образом, вы также можете написать прототип следующим образом:
float ArithmeticMean(char, char, char);
Я предпочитаю включать имена параметров, вероятно, потому что это позволяет мне создавать прототип функции путем копирования и вставки из определения функции.
Когда прототипы пропущены или неуместны, могут происходить странные вещи, и, по моему опыту, самый простой способ устранить эти проблемы – поместить все ваши прототипы функций в заголовочный файл, который включен во все исходные файлы вашего проекта. Это гарантирует, что вы можете безопасно использовать любую из ваших функций в любом из ваших исходных файлов, потому что каждая функция всегда будет объявляться (то есть в заголовочном файле) до ее вызова.
Например, следующий фрагмент кода взят из файла с именем "Project_DefsVarsFuncs.h".
// прототипы функций
void Delay_us(unsigned int DelayCount);
void Delay_10ms(unsigned int DelayCount);
void Delay_seconds(unsigned int DelayCount);
void Update_LCD(unsigned char SensorNumber, unsigned long DisplayValue);
void LCD_Clear_All();
Этот заголовочный файл включен в мои исходные файлы следующим образом:
//-----------------------------------------------------------------------------
// Includes
//-----------------------------------------------------------------------------
#include "Project_DefsVarsFuncs.h"
2. Объявляйте функции как статические, чтобы избежать конфликтов имен
По мере того, как проект прошивки становится всё больше и сложнее, вы можете обнаружить, что хотите использовать одно и то же имя функции в разных исходных файлах. Вы можете сделать это, используя ключевое слово "static
". Это ситуация, в которой вы бы не поместили прототип функции в заголовочный файл, как описано выше.
Ключевое слово "static
" ограничивает область действия функции одним исходным файлом. Это позволяет использовать такое же имя функции для функции в другом исходном файле. В приведенном ниже примере требуются три разных функции ProcessData()
– одна для анализа данных АЦП, одна для ответа на команды UART и одна для обработки сообщений I2C.
Я поместил бы ключевое слово "static
" перед прототипом функции, а затем прототип функции переместил бы в начало исходного файла (т.е. перед «обычным» кодом, состоящим из инструкций процессора внутри тел функций).
Ключевое слово "static
" также удобно, когда несколько инженеров работают над одним и тем же проектом прошивки. Если два инженера работают с разными исходными файлами, использование статических функций позволяет инженеру A выбирать имена функций, не беспокоясь о возможности того, что инженер B выберет такое же имя для другой функции.
3. Используйте указатель для передачи массива в функцию
Встраиваемые приложения часто используют массивы – последовательности показаний датчиков, значений АЦП, короткие сообщения ASCII и так далее. Может показаться, что функции немного неуклюжи в такого рода контексте разработки, потому что вы не можете передать массив в функцию на C. Что ж, это правда, что вы не можете передать массив так, как вы передаете отдельную переменную, но вы можете предоставить функции доступ к данным массива с помощью указателей.
Если вы включите указатель в качестве одного из аргументов, а затем при вызове функции передадите идентификатор массива, операторы в теле функции могут использовать этот указатель для чтения и изменения содержимого массива. Если вы находите эту концепцию немного запутанной, я рекомендую сначала прочитать мою статью о массивах в C, а затем первую статью об указателях.
4. Вы можете вызывать функции с помощью указателя
Указатель – это переменная, которая содержит адрес в памяти. Этот адрес часто идентифицирует местоположение переменной или нулевого элемента массива. Однако указатели также могут указывать на функции.
Если вы читали мою первую статью о функциях в C, вы знаете, что функции хранятся в определенных местах в памяти программ, так же как переменные хранятся в определенных местах памяти данных. Если значение, хранящееся в указателе, является начальным адресом функции, вы можете вызвать эту функцию с помощью указателя. Если вы измените значение указателя так, чтобы оно равнялось начальному адресу другой функции, вы можете вызвать эту другую функцию, используя тот же указатель.
В следующем фрагменте кода показано, как объявить указатель на функцию, которую можно использовать с функциями, имеющими три аргумента char
и возвращаемый тип float
.
float (*Ptr_to_Function)(char, char, char);
Идентификатор массива, по сути, является указателем на нулевой элемент массива. Аналогично, имя функции интерпретируется как начальный адрес (в памяти программ) функции. Таким образом, вы можете назначить функцию указателю на функцию следующим образом:
Ptr_to_Function = ArithmeticMean;
Теперь вы можете вызвать функцию ArithmeticMean()
, используя Ptr_to_Function
. Следующие два выражения эквивалентны:
Average = ArithmeticMean(Temperature1, Temperature2, Temperature3);
Average = (*Ptr_to_Function)(Temperature1, Temperature2, Temperature3);
5. Используйте локальные переменные везде, где это возможно
Я думаю, во многих случаях наше первое желание – определить все переменные в верхней части исходного файла и оставить всё как есть. Тем не менее, полезно привыкнуть использовать в своих функциях локальные переменные. Область действия локальной переменной ограничена функцией, в которой она определена; другими словами, переменная создается при вызове функции и исчезает, когда функция выполнила свою задачу.
Я рекомендую использовать локальные переменные по двум причинам. Во-первых, они позволяют повторно использовать одно и то же имя переменной в разных функциях. Сложный проект может включать в себя множество функций, которые нуждаются в базовых переменных, таких как счетчик цикла. Мне нравится для моих счетчиков многократно использовать "n" вместо постепенного перебора всех букв в алфавите.
Во-вторых, использование локальных переменных может привести к более быстрому коду. Я не эксперт по компиляторам, но я думаю, что локальная переменная, скорее всего, будет помещена в регистр, а не в область памяти, и доступ к регистрам быстрее, чем доступ к памяти.
Заключение
Здесь мы рассмотрели довольно много подробностей, связанных с использованием функций в языке C, и я надеюсь, что эта информация поможет вам сделать вашу прошивку более эффективной и функциональной. Если у вас есть какие-либо советы, связанные с функциями, не стесняйтесь поделиться ими в комментариях ниже.