Ардуино уроки функции

Arduino.ru

Функции

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

Для программистов, работающих с BASIC, функции в Arduino позволяют использовать подпрограммы (GOSUB в BASIC).

Разделения кода на функции имеет ряд преимуществ:

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

Существуют две обязательные функции в скетчах Arduino setup() и loop(). Другие функции должны создаваться за скобками этих функций. В следующем примере будет создана простая функция умножения двух чисел.

Пример

Для вызова функции умножения ей передаются параметры данных:

Созданную функцию необходимо задекларировать вне скобок любой другой функции, таким образом «myMultiplyFunction()» может стоять выше или ниже функции «loop()».

Весь скетч будет выглядеть следующим образом:

Следующая функция будет считывать данные с датчика функцией analogRead() и затем рассчитывать среднее арифметическое. Затем созданная функция будет масштабировать данные по 8 битам (0-255) и инвертировать их. // датчик подключен к выводу 0

Вызов функции осуществляется присвоением ее переменной.

Источник

Функции. Урок 3. Ардуино

Привет. Сегодня поговорим про организацию своей функции в языке программирования ардуино.

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

В прошлый раз мы рассматривали программу устранения дребезга кнопки. Если вы уже забыли или пропустили предыдущую статью, пожалуйста, посмотрите её.

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

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

Чтобы выполнить этот урок нам понадобятся.

  • Ардуино UNO
  • Макетная плата
  • Перемычки
  • 4 Резистора номиналом 220 Ом
  • Резистор номиналом 10 кОм
  • Кнопка
  • 4 Светодиода 5 мм
  • Кабель USB

Функции

Мы уже встречались с функциями. Поэтому разобраться с этим будет довольно просто. В предыдущих проектах мы использовали функции delay(), millis(), pinMode(), digitalWrite(), setup() и loop().

Функция — это часть программы, которая выполняет определенные действия и предоставляет результат своей работы. У функций должно быть имя, чтобы к ним можно было обратиться. Функции могут принимать какие-то значения из основной части программы. И вызывать другие функции.

Давайте напишем простую функцию для примера.

Мы создали функцию plus(). Она получает на вход две переменные типа int и возвращает их сумму. Если мы присвоим другой переменной значение этой функции, то получим результат сложения.

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

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

Сначала соберем схему на макетной плате.

Принципиальная схема подключения светодиодов и кнопки

Аноды светодиодов подключаем к пинам на плате ардуино, используем 5,7,9 и 11 пины. Катоды светодиодов подключаем к земле через резисторы 220 Ом.

Кнопку подключаем ко 2 пину на ардуино с одной стороны. А также к шине 5 вольт и к земле через резистор 10 кОм.

Программа

Мы больше не будем использовать 13 пин и диод на плате, поэтому уберем связанные с ним переменные из кода. Но добавим массив пинов для светодиодов и счетчик.

В массиве есть элемент 13. Однако мы не будем подключать к нему диод, а будем использовать его для флага, что все светодиоды нужно выключить.

В функции setup() настроим пины на ввод и вывод.

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

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

В основном цикле будем вызывать функцию debounce() и передавать в нее предыдущее значение кнопки. И если значения отличаются, а текущее значение = HIGH, то есть кнопка была нажата, вызываем другую функцию ledON().

Функция ledON() хранит текущее значение переменной счетчика i. И включает соответствующий светодиод из массива. Но до того, как включить нужный диод, функция выключает все. Если i = 4, это значение 13 в массиве, мы выключаем все диоды. А следующим нажатием, i = 5, сбрасываем счетчик на 0.

Работа программы

Полный текст программы

Заключение

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

Источник

Using Functions in a Sketch

LAST REVISION: 10/05/2022, 01:00 PM

Segmenting code into functions allows a programmer to create modular pieces of code that perform a defined task and then return to the area of code from which the function was «called». The typical case for creating a function is when one needs to perform the same action multiple times in a program.

For programmers accustomed to using BASIC, functions in Arduino provide (and extend) the utility of using subroutines (GOSUB in BASIC).

Standardizing code fragments into functions has several advantages:

Functions help the programmer stay organized. Often this helps to conceptualize the program.

Functions codify one action in one place so that the function only has to be thought out and debugged once.

This also reduces chances for errors in modification, if the code needs to be changed.

Functions make the whole sketch smaller and more compact because sections of code are reused many times. They make it easier to reuse code in other programs by making it more modular, and as a nice side effect, using functions also often makes the code more readable. There are two required functions in an Arduino sketch, setup() and loop(). Other functions must be created outside the brackets of those two functions. As an example, we will create a simple function to multiply two numbers.

Example

To «call» our simple multiply function, we pass it parameters of the datatype that it is expecting:

Our function needs to be declared outside any other function, so «myMultiplyFunction()» can go either above or below the «loop()» function.

The entire sketch would then look like this:

Another example

This function will read a sensor five times with analogRead() and calculate the average of five readings. It then scales the data to 8 bits (0-255), and inverts it, returning the inverted result.

To call our function we just assign it to a variable.

As you can see, even if a function does not have parameters and no returns is expected «(» and «)» brackets plus «;» must be given.

Источник

Функции

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

Где тип данных – это тип данных, который возвращает функция, имя функции – имя, по которому функция вызывается, набор аргументов (параметров) – необязательный набор переменных, и тело функции – код, который будет выполняться.

Вызов функции осуществляется именем функции и передачей набора аргументов, если таковые имеются:

Если аргументы не передаются, в любом случае нужно указать пустые скобки!

Функция в C++ всегда возвращает результат. Этот термин означает, что после выполнения функции она выдаёт некое значение, которое можно присвоить другой переменной.

Функция может принимать аргументы, может не принимать, может возвращать какое-то значение, может не возвращать. Давайте рассмотрим эти варианты.

Функция, которая ничего не принимает и ничего не возвращает

Самый простой для понимания вариант, с него и начнём. Помимо типов данных, которые я перечислял в уроке о типах данных, есть ещё один – void , который переводится с английского как “пустота”. Создавая функцию типа void мы указываем компилятору, что никаких значений возвращаться не будет (точнее будет – функция вернёт “ничего”).

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

Это очень плохой пример с точки зрения оптимальности кода, но далее мы будем этот пример улучшать и в итоге получим конфетку. Чем он плох на данном этапе: у нас используются глобальные переменные, которые работают внутри функции, а одним из главных принципов программирования является разделение данных и действий. Будучи новичком, не стоит об этом сильно задумываться, позже вы сами к этому придёте. Разделяя данные и действия можно создавать универсальные инструменты, рассмотренная выше функция не является универсальной: она складывает глобальную а с глобальной b и записывает результат в глобальную же c . Сделаем следующий шаг к оптимизации: пусть функция возвращает значение.

Функция, которая ничего не принимает и возвращает результат

Чтобы функция могла вернуть численное значение, она должна быть описана с типом данных, который будет возвращаться. Нужно заранее подумать, какой тип будет возвращён, чтобы избежать ошибок. Например я знаю, что моя суммирующая функция работает с типом данных byte , она складывает два таких числа. Это означает, что результат вполне может превысить предел типа byte (сложили 100 и 200 и получили 300), значит функции следовало бы возвращать например тип данных int . Собственно поэтому у переменной c из предыдущего примера тип данных int .

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

Давайте перепишем наш код так, чтобы числа a и b складывались и результат возвращался функцией, и этот результат мы уже “ручками” приравняем к c .

Ну вот, функция стала чуть более универсальной. Теперь результат сложения a и b как функцию можно использовать в других местах и приравнивать к другим переменным. Чтобы сделать код ещё более универсальным, давайте передавать величины для сложения в виде аргументов.

Функция, которая принимает аргументы и возвращает результат

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

И вот так мы получили универсальную функцию sumFunction , которая принимает две величины типа byte , складывает их и возвращает результат типа int . Это и есть выполнение концепции “отделение кода от данных”, функция живёт сама по себе и не зависит от других переменных! Казалось бы, можно использовать функцию как sumFunction(100, 200) , и она вернёт значение 300 . Но не всё так просто, потому что любое целое число в программе обрабатывается компилятором как int, и при попытке передать такой вот int в нашу функцию, которая принимает byte , мы получим ошибку “нельзя передать int вместо byte “. В этом случае можно привести тип числа к byte вручную, вот так это будет выглядеть:

Но лучше всё-таки сделать функцию более универсальной, пусть она принимает int paramA и int paramB , ведь данные переменные являются локальными, то есть создадутся при вызове функции и удалятся из памяти при завершении работы функции, и их “размер” в принципе не играет роли.

А как быть, если мы хотим складывать уже имеющийся функцией другие типы данных? Например, float . Можно преобразовать типы данных при передаче аргументов, но функция всё равно вернёт целое число. Сделать нашу функцию ещё более универсальной сможет такая штука C++ как перегруженная функция.

Перегруженные функции

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

Итак, у нас теперь целых три функции с одинаковым именем, но разными наборами аргументов и типами возвращаемого значения. Программа сама разберётся какую из функций использовать на основе передаваемых аргументов. Передали два float – работает третья функция, вернёт float . Передали три int – получили их сумму при помощи второй по счёту функции. Передали два int – получили их сумму при помощи первой функции. Вот такая удобная штука! Ещё одним вариантом перегруженной функции является шаблонная функция, она позволяет работать с данными любого типа в рамках одной функции, без перегрузов. Читайте ниже.

Функция, которая меняет значение переменных

Забегая немного вперёд (в урок про указатели и ссылки) сразу рассмотрим ещё один полезный вариант: функция меняет значения указанных переменных. В рассмотренных выше примерах мы передавали в функцию переменные по значению. Это означает, что внутри тела функции мы имеем дело с копиями переменных, которые не влияют на изначальные переменные. Если передать переменную по ссылке – внутри функции мы будем работать именно с теми переменными, которые передали в качестве аргументов! Для передачи аргумента по адресу достаточно добавить всего лишь один символ – & . Напишем функцию, которая увеличивает значение переданной переменной на 10 :

Таким образом мы снова отделили данные от выполняемого кода, но сделали это ещё более элегантно.

Типы данных

Функция может возвращать и принимать как аргумент любые типы данных в любых сочетаниях: целые числа, float , String , указатели, struct , enum и так далее. Например следующая функция принимает два числа, склеивает их в строку через запятую и возвращает:

При вызове Serial.println(toStr(10, 3.14)) увидим в мониторе порта 10, 3.14

Как вернуть несколько значений?

В языке C++, в отличие от того же Питона, нельзя просто так взять и вернуть из функции два значения. К слову, принять их тоже нельзя, такого механизма в языке нет. Но есть структуры! Функция может принять структуру, вернуть структуру, а мы можем присвоить структуру. Давайте сделаем функцию, которая принимает два числа, находит их сумму, разность и произведение и возвращает результат в виде структуры. Для начала опишем структуру:

Она хранит сумму, разность и произведение. Теперь сделаем функцию, которая принимает два числа, считает, забивает результат в структуру и возвращает тип данных MyStruct

Готово! Пользоваться этим можно так:

Выведет в порт 7, -1, 12 Если изучить урок по структурам, то функцию с вычислением и возвратом структуры можно сократить всего до одной строчки кода:

Описание и реализация

Хорошим тоном считается объявлять функции отдельно от реализации. Что это значит: в начале документа, или в отдельном файле, мы описываем функцию (это будет называться прототип функции), а в другом месте пишем реализацию. Так делают в серьёзных программных проектах, Ардуино-библиотеки – не исключение. Также такое написание позволяет слегка ускорить компиляцию кода, потому что компилятор уже знает, что он в нём найдёт. Алгоритм такой:

Описание (прототип) функции

Реализация функции

То бишь всё то же самое, но с телом функции и фигурными скобками. Пример:

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

Передача массива в функцию (Pro)

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

Что из этого нужно запомнить: при описании функции аргумент массива указывается со звёздочкой, т.е. тип_данных* имя_массива . При вызове массив передаётся как имя_массива . В целом всё. Давайте покажу как сделать универсальную функцию, которая суммирует массив любого размера. Для этого нам поможет оператор sizeof() , возвращающий размер в байтах. Этот размер нам нужно будет передать как аргумент функции:

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

Другие типы функций (Pro)

Шаблонные функции

Шаблон – ещё один мощный инструмент языка C++, позволяющий создавать алгоритмы без привязки к типам данных. Тема очень обширная, мы рассмотрим её только применительно к “универсальным” функциям, полностью про шаблоны можно почитать тут, и тут продолжение.

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

Описание функции можно раскрыть:

Сделаем функцию, которая возвращает квадрат числа любого типа

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

В некоторых ситуациях шаблоны могут быть очень удобны! Также при вызове шаблонной функции можно вручную передать тип данных:

Точно таким же образом будет работать изменение возвращаемого типа данных (см. первый пример, функция возвращает шаблонный тип). Можно переписать предыдущий пример функции, которая суммирует массив. Преимущество шаблонной версии в том, что передавать размер массива и даже тип переменной не нужно, его можно посчитать внутри функции, вычислив размер через шаблон: длина массива (кол-во) = sizeof(T) / sizeof(someArray[0]) , то есть размер всего массива в байтах делим на размер любого элемента.

Макро-функции

Вы наверное уже помните такую директиву препроцессора, как #define . Из урока про переменные и константы мы узнали, что при помощи дефайна можно задавать константы. Ключевая особенность работы #define заключается в том, что он заменяет последовательность символов чем угодно, что мы там напишем, и это даёт возможность создавать так называемые макро-функции (macro), которые не создаются как функции, а вставляются в код при компиляции. Например вот так будет выглядеть макро, складывающая два числа:

На этапе компиляции все встречающиеся sum(значение1, значение2) будут заменены на значение1 + значение2 , таким образом это будет сумма.

Зачем это нужно? Обычная функцию с именем, как разбиралось выше, имеет свой “адрес” в памяти, и при каждом вызове функции ядро обращается к ней по этому адресу, что занимает какое-то время. Макро функция же “встраивается” в код программы и выполняется сразу.

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

В “языке” Arduino есть несколько готовых инструментов, которые кажутся функциями, но на самом деле являются макросами. Заглянем в Arduino.h и увидим следующее:

Вот так вот, всем нам знакомые функции оказались макросами!

Ещё можно “переносить строку” для более комфортного создания макро-функций, перенос делается при помощи обратного слэша – \

Обратите внимание, на последней строке перенос не нужен! Таким образом, во время работы препроцессора все вызовы “функции” printWords() будут заменены на указанные строки кода. Можно и аргументы таким же образом передавать:

Как вы можете видеть, объявлять большую макро-функцию неудобно, и тут на помощь приходят встроенные функции.

Встроенные функции (Pro)

Встроенная функция имеет тот же смысл, что и макро-функция: все вызовы функции в коде заменяются на код внутри функции, что ускоряет быстродействие, но занимает дополнительную Flash память при каждом новом вызове. В отличие от макро, которая заменяется препроцессором, встроенная функция заменяется уже компилятором, т.е. после работы препроцессора, об этом нужно помнить и это можно использовать.

Объявляется встроенная функция очень просто: достаточно дописать ключевое слово inline перед объявлением функции. Компилятор может отказаться встраивать функцию (зависит от его настроек), поэтому можно попросить его принудительно встроить функцию при помощи атрибута __attribute__((always_inline)) . Таким образом, для объявления встроенной функции нужно в самом начале написать inline __attribute__((always_inline)) . Рассмотрим функцию, которая просто увеличивает значение переменной:

Всё! Теперь вызовы incr будут заменены на код. Если нужно разделить описание и реализацию, то можно записать так:

Статические функции (Pro)

В уроке про переменные и типы данных мы обсуждали спецификатор static , который позволяет скрыть глобальную переменную от других файлов программы. С функцией спецификатор static делает то же самое – статическая функция скрыта от вызова из других файлов, т.е. её область определения – файл, в котором она находится. static void printHello();

Указатель на функцию (Pro)

Указатели – очень сложная тема, которая практически не пригодится новичку, но некоторые алгоритмы знать нужно (читай урок про указатели). Например передачу указателя на функцию, это полезно при создании своих функций в стиле ардуиновской attachInterrupt() , в которой мы указываем свою функцию, которую сами создаём, а она вызывается в другом месте. Сделать это можно так:

Функция, которая принимает любое кол-во аргументов (Pro)

Функция может принимать “переменное” количество аргументов, такие аргументы называются variadic arguments и передаются в функцию в виде многоточия . . Чтобы работать с такими аргументами существуют специальные функции. Также для удобства нужно передать в функцию количество аргументов, например первым аргументом:

Функция такого вида может принимать от одного до любого количества аргументов. Что делать с этими аргументами дальше? Давайте выведем их в порт, и из этого примера всё будет понятно:

  • Создаём список аргументов типа va_list список
  • Инициализируем его, указав количество аргументов: va_start(список, кол-во)
  • Теперь можем получать значения по порядку, указав их тип: va_arg(список, тип)
    • Делать это нужно в цикле, чтобы вызвать только существующие аргументы!
  • В конце освобождаем список: va_end(список)

Видео

Источник

Adblock
detector