Оптимизация кода Ардуино и ускорение работы.

Оптимизация кода Ардуино и ускорение работы.


В этом примере, я покажу как можно сократить использование памяти и ускорить работу программы в 5 раз. Думаете это невозможно или трудно? Как оказалось совсем не трудно. Надо всего лишь придерживаться нескольких правил и ваш код будет работать в 5 раз быстрее, а памяти Ардуино вам хватит на любую проект.

Приветствую всех моих подписчиков и гостей канала.
Сегодня поговорим про оптимизацию кода в скетче. Что нужно сделать чтобы не тратить драгоценную память и как сделать так, чтобы ваш проект летал.
Начнём с самого начала. Сколько занимает пустой скетч. Для этого откроем Arduino IDE и загрузим в плату пустой скетч. Вообще то он не пустой, так как в нём уже есть пара функций, это setup и loop. Как от них избавиться я покажу чуть позже. Видим, что сейчас мы уже заняли некоторый объём памяти.
444 байта или 1% FLASH памяти.

Эта строка показывает объем флеш-памяти в Arduino, занятым скетчем, и процент от предела в 30 Кбайт, так как 2 Кбайт уже занято загрузчиком. Внимательно смотрите за этими данными, при превышении 70 процентов возможны сбои в работе программы.
и 9 байт динамической памяти ОЗУ или RAM.

ОЗУ в Arduino используется для хранения переменных и других данных, имеющих отношение к выполняющейся в данный момент программе. ОЗУ - энергозависимая память и после отключения питания память очищается.

Теперь добавим Serial.begin что бы иметь возможность выводить информацию в монитор порта. Значения сразу изменились. Программа пока ничего не делает, но уже потребляет
1438 байта или 4% памяти FLASH и 184 байта или 8% динамической памяти. Получается, что эта функция занимает 1000 байт в памяти Ардуино и 175 байт динамической памяти. Пустячок, но не приятно.

Добавим простую строчку вывода на экран. Значения подскочили. И теперь памяти стало
1496 байта или 4% памяти FLASH и 210 байта или 10% динамической памяти. Небольшое изменение, но это всего 1 ничего не значащая строчка.
Этим мы подготовили программу для того чтобы измерить скорость скетча. Измерять будем в микросекундах, так как это самое минимальное что может измерить процессор.

Для начала создадим несколько глобальных переменных для хранения текущего времени и напишем код для вывода микросекунд.
В этом примере я покажу обычный блик, то есть мигание светодиодами и тот же пример, но уже с обращением к портам микроконтроллера. Не пугайтесь если вы пока ничего не поймёте, дальше я всё объясню. Это не сложно.

Узнаем и сохраним в переменную TIME1 текущее время начала выполнения кода.
Создадим цикл на 1000 итераций. Если проще, то светодиод мигнёт 1000 раз, но вы этого не заметите, так как это произойдёт очень быстро. Затем мы сохраним время после выполнения кода и подсчитаем разницу времени.
То же самое сделаем с другим примером, но там мы не будем обращаться к digitalwrite а будем работать напрямую с регистрами контроллера. Как я сказал дальше я всё объясню. Так же сохраним время до и после, и подсчитаем разницу.

Ну и в конце узнаем во сколько раз второй код выполняется быстрее первого.
Откроем монитор порта и посмотрим. Так как весь код я разместил в setup, то он сработает всего 1 раз.
Видите, на выполнение 1 примера контроллеру понадобилось 7544 микросекунды, а второй пример занял всего 1324. Итого получилось что обращаясь непосредственно к портам контроллера мы получаем ускорение работы в 5 раз. Правда не плохо?
Это значит, что 1 выполнение команды digitalwrite занимает примерно 7,5 микросекунды, а обращение к порту и запись непосредственно в регистр 1,3 микросекунды.

Памяти стало еще немного меньше.
1716 байта или 5% памяти FLASH и 194 байта или 9% динамической памяти. Мы истратили ещё 220 байт памяти и 20 байт динамической памяти.
Теперь выведем на экран значения. Так как цикл loop крутится бесконечно, то и время работы будет постоянно увеличиваться. Вычислив среднее значение мы получим 200 – 208 микросекунд. Так что среднее работы почти пустого скетча 200 микросекунд. Много это или мало решайте сами. Кстати так вы узнаете оптимист вы или пессимист. Кто не понял, это отсылка на стакан, какой он полупустой или наполовину полный.

Подведём итог.
Пустой скетч занимает 444 байта FLASH памяти и 9 байт динамической памяти ОЗУ. Слегка заполненный занимает уже 1716 байта или 5% памяти FLASH и 194 байта или 9% динамической памяти.
Мы истратили 4% FLASH памяти, и 9% памяти ОЗУ.

Это было про ускорение работы, а теперь снова вернёмся к оптимизации кода.

Теперь загрузим обычный пример блинк и посмотрим сколько он занимает. Мигать будем светодиодом который находится на плате Ардуино и который подключен к 13 выводу Ардуино УНО и Ардуино НАНО.
Вы сами видите сколько памяти задействовано при работе этого скетча. А можно ли как-то уменьшить это значение. Оказывается да, и несколькими способами. Самым агрессивным, но немного трудный способ я сейчас покажу.
А потом перейдём к более простому, и если останется время, то я более подробно остановлюсь на сложном способе.
Во первых полностью уйдём от setup и loop. Заменим их одной main.
Сначала посмотрим на распиновку платы Ардуино НАНО, у УНО будет аналогично.
Возьмём порт B. Именно там и находится наш светодиод.

  • Нулевой бит этого порта соответствует выходу d8
  • Первый порту d9
  • Второй порту d10
  • Третий порту d11
  • Четвёртый порту d12
  • Пятый порту d13

А так как светодиод находится на тринадцатом выводе получается нам нужен пятый бит, так как отсчёт ведётся от нуля.
Отсчитываем пятый бит справа на лево и ставим единичку для включения светодиода и нолик для выключения. Delay здесь немного отличается от привычного нам, но 1000 миллисекунд, по прежнему равно 1 секунде. Загружаем скетч и смотрим результат. Светодиоды мигают так же как и в прежнем скетче, раз в секунду, но посмотрите на то сколько занято памяти.
Мы получили экономию памяти больше чем в 5 раз. Думаю, что это не плохой результат. Но сомневаюсь, что многие будут использовать этот метод, поэтому будем использовать более простой и привычный нам Ардуинщикам способ.
Для этого примера я собрал небольшую схему состоящую из 8 светодиодов и LED индикатора. Конечно можно было бы работать и с 1 светодиодом, но тогда будет очень маленький разрыв в значениях и это будет не так заметно.
Скетч я специально не оптимизировал, что бы показать как не надо делать. И наверняка многие из так делают, и им будет интересно посмотреть почему так делать не правильно.
В начале идёт блок комментариев и я советую вам всегда вначале писать что это за программа, когда вы её сделали и что она делает. Потому что через некоторое время вы полностью забудете что это и это будет вам подсказкой.

Дальше идёт подключение LCD индикатора. Затем я создал именованные переменные соответствующие цветам светодиодов. Всегда старайтесь называть переменные так что бы было понятно для чего он были созданы, и номера выходов Ардуино к которым они были подключены.
Затем я сделал несколько пауз для мигания светодиодами. Все они имеют разный интервал. Сначала они мигают часто, но с каждым новым светодиодом мигание становится реже.
Подключил Serial.begin и сделал несколько настроек индикатора. Так как для работы светодиодов выводы Ардуино должны быть установлены как выходы, то я так и сделал. Выходы надо указывать обязательно в отличие от входов. Так что если вы подключаете какой-нибудь датчик и он передаёт данные на вход Ардуино, то указывать INPUT не обязательно, хотя и не запрещено.
Ну и в цикле loop мы по очереди включаем светодиоды делаем паузу, выводим сообщение на индикатор и пишем в монитор порта, что включили светодиод. Затем делаем паузу чтобы видеть что светодиод зажёгся, и гасим его, опять же пишем в монитор что светодиод погас. Снова делаем паузу и переходим к следующему светодиоду. В конце включаем все светодиоды и выключаем их. И вот такой простенький пример занимает аж 27% памяти Ардуино и целых 67% памяти ОЗУ.
Теперь будем оптимизировать код.

Начнём с комментариев. Многие не пишут комментарии, потому что боятся что это занимает память контроллера. Проверим так ли это. Для теста помимо обычного комментария я добавил ещё стихотворение, что бы было побольше текста.
Вы видите сколько памяти сейчас занимает наш код. Я выведу этот результат сверху экрана, что бы постоянно видеть первоначальный результат. А теперь удалим комментарий и посмотрим сколько у нас теперь памяти. Как видите, памяти осталось столько же. Так что не экономьте на комментариях, они вам ещё не раз пригодятся.

Поехали дальше.
Разберёмся с переменными которые мы создаём в которых мы сохраняем номера выводов микроконтроллера. Например, я использовал 8 светодиодов и подключил их к выходам с 5 по 12 микроконтроллера и использовал для этого переменные с типом int. Это можно делать, но это не правильно, так как каждая такая переменная отбирает по 2 байта памяти.
И вообще переменные надо использовать только для тех значений которые изменяются в процессе работы, поэтому они и называются переменными. Нам же здесь надо использовать константы или директивs препроцессора, типа define. Давайте проверим тот и другой способ.
Сначала изменим значение на константы и загрузим скетч. Как видите значения не изменились. Теперь попробуем поменять на define. Объём памяти остался неизменным. Как же так. А всё дело в том, что умный компилятор может распознать какие переменные в процессе работы кода не изменяются и оптимизировать их. Но лучше на это не надеяться, а сразу указать один из этих двух способов. По желанию. Я в примере оставлю define. Так как он был последним.

Теперь подробнее разберёмся с паузами.
Так как мы заранее знаем какие они должны быть, то мы можем подобрать для них тип переменной. Для экономии памяти старайтесь выбирать наименьший из возможных типов. Так как это напрямую влияет на размер памяти. Вот самые распространённые типы. Так как у меня максимальное значение 1500, то я выбираю тип int. Так как он поддерживает значение от -32768 до 32767. Если у вас значение до 65535, то вы можете указать тип переменной unsigned int то есть беззнаковое и у вас всё равно будет занято 2 байта в памяти, а вот если у вас 65536, то тогда придётся указать тип переменной long А ЭТО УЖЕ 4 БАЙТА ПЯМЯТИ. Ну надеюсь понятно рассказал.
Теперь давайте попробуем эти переменные заменить на константы и посмотрим сможем ли мы сэкономить немного памяти. У нас получилось сэкономить 2 байта. Если честно, то странно, я думал, что получится сэкономить побольше. Проверим, может мы что делаем не так?

И если компилятор действительно такой умный и не используемые в коде переменные делает константами, то попробуем изменить их значение в коде и посмотреть что будет. Будем изменять паузу на единичку. Мы получили ошибку, так как все паузы сейчас объявлены как константы и в коде их нельзя изменить. Сделаем их снова переменными типа int.
Прошиваем код и видим, что у нас уменьшился объём флэш памяти и увеличился объём динамической памяти на 2 байта. Как раз столько занимает переменная типа инт. Значит всё работает  и компилятор действительно умный, и не используемые в коде переменные преобразует в константы. Теперь изменим все паузы и посмотрим на сколько вырастет объём памяти.
Объём действительно подрос. И динамическая память теперь весит на 20 байт больше – это как раз 10 переменных пауза по 2 байта. Но наша задача уменьшить код а не увеличить. Поэтому возвращаем все на место и паузы, уже больше по привычке чем по необходимости ставим в define.

Пойдём дальше. Основная наша задача освободить как можно больше динамической памяти. Флэш можно забивать почти под завязку. А вот динамическая память нам очень нужна, а её всего 2 килобайта. Поэтому все строки что мы выводим в монитор порта мы перебросим во флэш, тем самым освободим от них ОЗУ.
Для этого перед каждой строчкой ставим макрос F() он  размещает строку во флеш память, тем самым освобождая динамическую память. Флэш 32 килобайта а динамическая всего  2 кило, есть разница?

Прошиваем ардуино и смотрим результат. И вот эта не сложная операция освободила нам больше 30% динамической памяти, уменьшив размер в байтах в 2 раза. Задействовав всего 6% флэш памяти.

Посмотрим не пропало ли чего. Нет, строки как выводились, так и продолжают выводиться. Так что всё работает.

Теперь как и обещал рассмотрим сравнение двух примеров обычный и с доступом к регистрам портов. Это конечно тема для отдельного видео, поэтому здесь я расскажу очень коротко.
Из основного примера урока выкинем всё лишнее. Лишним оказались все переменные – это паузы и номера выводов микроконтроллера. Все эти значения будем сразу писать в код. Это конечно не правильно, позволит нам существенно сократить объём памяти. У нас получился вот такой короткий код, на 64 строчки.
А здесь этот же код, но с обращением к регистрам. Светодиоды мы подключили к выводам с 5 по 12 Ардуино. Смотрите насколько короче запись обращения к регистрам чем обычная запись с pinmode().
А так мы мигаем светодиодами. Эти строчки замена digitalWrite. Код сократился уже до 47 строчек.

Здесь, на примере установки цифрового пина Ардуино d7 я показал два способа установки регистров в HIGH и LOW, какой использовать выбирайте сами.
D7 находится на порту D, в седьмом регистре если считать с 0. Чтобы включить светодиод, надо установить его в единицу, а выключить в ноль.
Но как и у любого способа здесь тоже есть положительные и отрицательные сторона, свои за и против.
Это результат двух скомпилированных примеров. Какой из них с доступом к регистром думаю говорить не надо.

Со своей задачей мы справились. Мне хотелось рассказать побольше, но и так видео получилось большим, целых 17 минут, при среднем просмотре моих уроков в 3 минуты. Редко кто доходит и до половины. Если будут желающие то я сниму продолжение, есть ещё очень много способов уменьшить размер кода и увеличить скорость. Это только вершина айсберга. Спасибо тем кто досмотрел до конца.