Как ускорить компиляцию arduino ide

Обновлено: 07.07.2024

Версия 2.0 от 01.02.2020
Быстрое и лёгкое ядро для Arduino IDE с расширенной конфигурацией.
Основано на оригинальном ядре Arduino версии 1.8.9, большинство функций заменены на более быстрые и лёгкие аналоги, убрано всё лишнее и не относящееся к микроконтроллеру ATmega328p, убран почти весь Wiring-мусор, код упрощён и причёсан. Добавлено несколько функций и интересных вариантов компиляции.
Разработано by Александр AlexGyver и Egor ‘Nich1con’ Zaharov

Известные баги

  • На компиляторе версии 8 не работает библиотека ServoSmooth. Будьте бдительны с этой версией!
  • Вариант “GyverUART вместо Serial” не работает для Ethernet модулей

Установка

Автоматическая

  • Открой the Arduino IDE
  • Зайди в Файл > Настройки
  • Вставь этот адрес в Дополнительные ссылки для менеджера плат:
  • Открой Инструменты > Плата > Менеджер плат…
  • Подожди загрузку списка
  • Листай в самый низ, пока не увидишь GyverCore
  • Жми Установка
  • Закрой окно
  • Выбери плату в Инструменты > Плата > GyverCore > ATmega328 based
  • Готово!
  • Примечание: новая версия компилятора по умолчанию идёт для Windows 64, если нужно для win32 или Linux – нужно установить вручную. Идём в C:\Users\Username\AppData\Local\Arduino15\packages\GyverCore\hardware\avr\2.0\tools\avr-gcc, удаляем оттуда всё и кладём туда файлы из архива нужной версии (папка avr-gcc в корне данного репозитория)

Ручная

  • Файлы из папки GyverCore в этом репозитории положить по пути C:\Users\Username\AppData\Local\Arduino15\packages\GyverCore\hardware\avr\2.0\
  • Версия компилятора по умолчанию для Windows 64, если нужна другая – читай выше как установить

Изменения

Облегчено и ускорено

Время выполнения функций, мкс (при 16 МГц кварце)

ФункцияArduinoGyverCoreБыстрее в, раз
millis0.69 us0.69 us
micros0.81 us0.81 us
pinMode2.56 us0.25 us10.25
digitalWrite2.40 us0.125 us19
digitalWrite PWM3.25 us0.30 us7.4
digitalRead2.80 us0.063 us46
analogWrite3.8 us0.33 us8.4
analogRead111.2 us5.63 us20
analogReference0.19 us0.19 us
attachInterrupt1.06 us0.8 us
detachInterrupt0.5 us0.25 us2
tone9.0 us2.25 us4
shiftIn111 us8 us13
shiftOut117 us24 us4.5

Занимаемое место, Flash, байт

ФункцияArduinoGyverCoreРазница, Flash
millis26242
micros24204
pinMode1142490
digitalWrite20024176
digitalRead19024166
analogWrite40648358
analogRead3272-40
analogReference022-22
attachInterrupt21218032
detachInterrupt19815048
tone1410740670
Serial begin1028166862
print long1094326768
print string21001484616
print float20214461575
parseInt1030214816
readString23341594740
parseFloat1070246824

Примечание: analogRead и analogReference имеют расширенную функциональность и весят чуть больше
Скетч, состоящий из однократного вызова перечисленных выше функций, занимает

Все библиотеки, работа которых зависит от стандартных функций (время, I/O), работают быстрее:

uart является практически полным аналогом Serial, но весит в разы меньше и работает быстрее. Список функций смотри ниже в Добавлено.

Добавлено

  • Расширена подсветка синтаксиса (вплоть до названий регистров и битов)
  • Макрос bitToggle(value, bit), инвертирует состояние бита bit в байте value
  • Быстрая функция digitalToggle(pin), инвертирует состояние пина
  • Расширенная работа с АЦП
    • Предделитель АЦП по умолчанию изменён на 4, это в разы ускоряет analogRead, измерения по нашим тестам менее точными не становятся
    • Убрано в 2.0 analogStartConvert(byte pin) – начать преобразование с выбранного пина
    • Убрано в 2.0 analogGet() – получить преобразованное значение (между analogStartConvert и analogGet можно выполнять действия, в отличие от ожидания в analogRead())
    • analogPrescaler(uint8_t prescl) – установить предделитель для АЦП (2, 4, 8, 16, 32, 64, 128) – управляет скоростью работы АЦП (скоростью оцифровки). Prescaler:
      • 2: 3.04 мкс (частота оцифровки 329 000 кГц)
      • 4: 4.72 мкс (частота оцифровки 210 000 кГц)
      • 8: 8.04 мкс (частота оцифровки 125 000 кГц)
      • 16: 15.12 мкс (частота оцифровки 66 100 кГц)
      • 32: 28.04 мкс (частота оцифровки 35 600 кГц)
      • 64: 56.04 мкс (частота оцифровки 17 800 кГц)
      • 128: 112 мкс (частота оцифровки 8 900 Гц)
      • uart.begin() – запустить соединение по последовательному порту со скоростью 9600
      • uart.begin(baudrate) – запустить соединение по последовательному порту со скоростью baudrate
      • uart.end() – выключить сериал
      • uart.peek() – вернуть крайний байт из буфера, не убирая его оттуда
      • uart.flush() – ждать принятия данных
      • uart.clear() – очистить буфер
      • uart.read() – вернуть крайний байт из буфера, убрав его оттуда
      • uart.write(val) – запись в порт
      • uart.print(val) – печать в порт (числа, строки, char array)
      • uart.println(val) – печать в порт с переводом строки
      • uart.available() – возвразает true, если в буфере что-то есть
      • uart.setTimeout(val) – установить таймаут для функций парсинга (по умолчанию 100 мс)
      • uart.parseInt() – принять целочисленное число
      • uart.readString() – принять строку
      • uart.readStringUntil() – принять строку по терминатору
      • uart.parseFloat() – принять число float
      • uart.parsePacket(dataArray) – принять пакет вида $50 60 70; в массив dataArray (смотри пример)
      • Выбор загрузчика
      • Выбор источника тактирования (внешний, внутренний)
      • Сохранять или очищать EEPROM
      • Вывод тактирования на ногу МК
      • Возможность отключить системный таймер 0 и освободить для себя вектор прерывания ovf
      • Замена Serial быстрым uart
      • Настройка или отключение B.O.D.
      • Возможность отключить стандартную инициализацию периферии
      • Выбор версии компилятора

      Убрано

      • Убраны всякие сервисные файлы и прочий хлам, не относящийся к ATmega328 (wifi, USB), почищен код. Ядро полностью совместимо с остальными библиотеками, ничего из стандартных функций не вырезано.
      • analogWrite(pin, 255) не заменяется на digitalWrite(pin, HIGH) для корректной работы 10 бит ШИМ. Шумы при работе ШИМ на заполнении 255 отсутствуют. Чтобы выключить шим, нужно сделать пин analogWrite 0, также генерацию отключает digitalWrite любого уровня на этот пин.

      Настройки платы

      Bootloader – выбор загрузчика (требует перезаписи загрузчика):

      • old bootloader – cтарый загрузчик (стоит на большинстве китайских плат)
      • Новый с optiBoot, киатйцы тоже потихоньку начинают продавать платы с ним
      • optiBoot v8 – optiboot самой свежей версии
      • Вариант without bootloader для прошивки скетча во всю доступную (32 кБ) память МК

      Clock – выбор частоты и источника тактирования (требует перезаписи загрузчика):

      • External 16 MHz (стандартный вариант для платы Nano 16 МГц)
      • External 8 MHz (стандартный вариант для платы Nano 8 МГц)
      • Internal 8 MHz (внутренний генератор: можно работать с голым камнем без кварца)
      • Internal 1 MHz (внутренний генератор)
      • Internal 128 kHz (внутренний генератор) – загрузчик будет стёрт! Используйте without bootloader!
      • Примечания:
        • Функции времени (delay/millis) скорректированы под выбранную частоту
        • После прошивки на частоту 128 кГц дальнейшая загрузка по ISP возможна только с понижением частоты ISP на стороне программатора!

        Save EEPROM – сохранять EEPROM после перепрошивки (очистки) камня (требует перезаписи загрузчика):

        Clock Out – на пине D8 (NANO/Mini) будет продублировано тактирование с частотой источника (требует перезаписи загрузчика):

        System timer – преднастройка таймера 0:

        • enable – таймер 0 настроен по умолчанию, работают функции времени delay/millis
        • disable – вектор прерываний OVF таймера 0 освобождён для пользователя, delay/delayMicroseconds работают, millis/micros – нет
        • Примечание: при отключенном таймере 0 функции delay и delayMicroseconds автоматически заменяются на _delay_ms и _delay_us из avr/util.h, а millis и micros заменены на 0

        Serial – работа с Serial:

        • default Serial – при работе с Serial работает стандартная библиотека Serial
        • GyverUART – все обращения к Serial в коде автоматически заменяются на uart из библиотеки GyverUART – код становится быстрее и легче!
        • Примечание: в GyverUART нет функций find, findUntil, readBytes и readBytesUntil!

        B.O.D. (Brown-out detector) – reset при падении напряжения (требует перезаписи загрузчика):

        • disable – отключен
        • 1.8V – сброс при напряжении питания ниже 1.7-2.0V
        • 2.7V (default) – сброс при напряжении питания ниже 2.5-2.9V
        • 4.3V – сброс при напряжении питания ниже 4.1-4.5V

        Initialization – инициализация периферии (таймеры, ацп) в начале скетча:

        • enable – стандартная инициализация
        • disable – инициализация отключена

        Compiler version – версия компилятора

        • default v5.4.0 – встроенная в IDE версия компилятора
        • avr-gcc v8.3.0 – новая версия компилятора: компилирует быстрее, скетчи весят меньше! Билд взял отсюда

        Больше контроля!

        Для большего контроля за периферией микроконтроллера рекомендую попробовать следующие наши библиотеки:

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

        Почему это важно?

        Многие скетчи занимают несколько тысяч байт и, выключив проверку, вы сэкономите лишь несколько секунд при их загрузке. Но сколько раз вы загружаете скетч, когда вы работаете над проектом? 10 раз? 50? И над сколькими проектами вы могли бы работать? Некоторые, наверное, загрузили уже десятки тысяч скетчей. Теперь умножьте на казалось бы незначительную экономию при загрузке и вы получите внушительное количество сэкономленного времени. В любом случае, будет точно не хуже, если вы меньше времени будете тратить на наблюдение за миганием светодиодов на TX и RX.

        Случаи, при которых проверка не помешает

        Так выглядит ошибка при проверке в Arduino IDE

        Так выглядит ошибка при проверке в Arduino IDE

        В некоторых случаях вы, возможно, все же захотите проверить свой ​​код. Если вы собираетесь поместить Arduino в спутник или в какой-либо другой ответственный проект, вы можете спать спокойнее, зная, что загруженный в него код является корректным. Если вы подкючаетесь к Arduino при помощи 15-метрового USB-кабеля или у вас провод длиной 2 км при передаче через интерфейс RS485, вы, возможно, захотите произвести проверку после загрузки своего скетча. Хотя также маловероятно, что ошибка проскочет через проверку контрольных сумм, так что решайте сами делать проверку или нет.

        На каких платах этот трюк работает?

        Это работает с любой платой Arduino, которая использует соединение последовательное USB-соединение (Uno, Mega, Pro Mini, LilyPad Simple, Fio и т.д.). Эти все платы используют один и тот же загрузчик avrdude, в котором по умолчанию устанавливается флаг проверки.

        Любые платы, использующие загрузчик Catarina (Leonardo, Micro и т.д.) или загрузчик Halfkay (платы Teensy) имеют гораздо более быстрее загрузчики, и для них преимущество в скорости неочевидно.

        Еще раз повторюсь, что вероятность пропуска ошибки в процессе минимальна, а поэтому, вполне можно довериться компилятору и снять этот флажок!

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

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

        Осциллограмма выполнения команды digitalWrite


        Похоже, что команда digitalWrite (13, HIGH) выполняется за 6.6 мкс, а digitalWrite (13, LOW) за 7.2 мкс. Итого 13.8 мкс. Это намного дольше, чем 62.5 нс, в действительности, в 220 раз дольше. Также, можно заметить, что нахождение с состоянии LOW (7.2 мкс) занимает больше времени, чем нахождение в состоянии HIGH (6.6 мкс).

        Проведем следующий эксперимент.

        Теперь мы переводим дважды 13 пин в состояние HIGH, а затем один раз в LOW и цикл повторятся заново. Я ожидал увидеть значение времени для состояния HIGH равное 6.6 × 2 = 13.2 мкс и для LOW равное по прежнему 7.2 мкс. Посмотрим на фактическую осциллограмму сигнала, полученного в результате выполнения второго скетча.

        Осциллограмма digitalWrite со скважностью 2

        По факту, две инструкции, переводящие вывод в состояние HIGH дважды занимают 19.4 мкс, или, в среднем, по 9.7 мкс на одну команду, на нахождение в состоянии LOW, по прежнему, уходит 7.2 мкс.

        Попробуем реализовать теперь еще одну последовательность состояний: HIGHLOWLOW.

        В результате выполнения этого кода, осциллограмма сигнала на 13 пине выглядит так:

        Осциллограмма digitalWrite со скажностью 0.5

        Единственная инструкция HIGH занимает 6.8 мкс — примерно так же как и ожидалось (6.6 мкс). Две подряд команды, переводящие вывод в состояние LOW занимают 13.8 мкс — это чуть меньше, чем ожидаемые 14.4 мкс (7.2 × 2 ).

        Что получатеся? В цикле loop (), используя функцию digitalWrite, мы можем менять состояние пина с частотой максимум 72 кГц, а в отдельных случаях, эта частота может быть и ниже, например, как во втором случае — около 37 кГц. Такая частота значительно меньше тактовых 16 Мгц, но если использовать прерывания по таймеру, то мы можем значительно увеличить этот показатель.

        Реализация функции digitalWrite в языке Wiring является, мягко говоря, не оптимальной. Если посмотреть на ее реализацию на Ассемблере, то можно обнаружить несколько проверок, в частности, проверятся не нужно ли выключить таймер ШИМ после предыдущей функции analogWrite (). Наиболее быстрая реализация, но, в то же время и наиболее затратная по времени ее написания, могла бы быть на языке Ассемблер. Но написание кода на Ассемлере — то еще насилие над собой. Я уже не говорю про отладку ассемблерного кода. Одним из компромиссных решений может быть использование оптимизированных библиотек, реализующих различные функции ввода/вывода. В дальнейших своих публикациях я рассмотрю некоторые из таких библиотек.

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

        Для того, чтобы сократить время изменения состояния какого-либо вывода можно использовать команды прямой записи в регистр порта. Для изменения значений пинов с 8 по 13 используется регистр PORTB в который необходимо записать слово данных (два младших бита этого слова данных, то есть 1 и 2 биты не используются, а оставшиеся шесть — как раз и устанавливают состояния цифровых портов с 8 по 13). Для изменения состояний цифровых пинов с 0 по 7 используется PORTD.


        С чем компилятор справится сам (NEW!)

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

        Модификатор volatile

        Вырезание неиспользуемых переменных и функций

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

        Оптимизация вычислений

        Компилятор сам старается максимально оптимизировать вычисления:

        • Заменяет типы данных на более оптимальные там, где это возможно и не повлияет на результат. Например val /= 2.8345 выполняется в 4 раза дольше, чем val /= 2.0 , потому что 2.0 была заменена на 2 .
        • Заменяет операции целочисленного умножения на степени двойки (2^n) битовым сдвигом. Например, val * 16 выполняется в два раза быстрее, чем val * 12 , потому что будет заменена на val << 4 ;
          • Примечание: для операций целочисленного деления такая оптимизация не проводится, и её можно сделать вручную: val >> 4 выполняется в 15 раз быстрее, чем val / 16 ;

          Вырезание условий и свитчей

          Компилятор вырежет целую ветку условий или свитчей, если заранее будет уверен в результате сравнения или выбора. Как его в этом убедить? Правильно, константой! Рассмотрим элементарный пример: условие или свитч (неважно) с тремя вариантами:

          volatile variable constant define external const template const
          digitalRead 58 58 58 52 52 52
          pinRead 6 6 6 1 1 1
          bitRead(PIND, pin); 3 1 1 1 1 1

          Оптимизация скорости

          Использовать переменные соответствующих типов

          Тип переменной/константы не только влияет на занимаемый ей объём памяти, но и на скорость вычислений! Привожу таблицу для простейших не оптимизированных компилятором вычислений. В реальном коде время может быть меньше. Примечание: время приведено для кварца 16 МГц.

          Тип данных Время выполнения, мкс
          Сложение и вычитание Умножение Деление, остаток
          nt8_t 0.44 0.625 14.25
          uint8_t 0.44 0.625 5.38
          int16_t 0.89 1.375 14.25
          uint16_t 0.89 1.375 13.12
          int32_t 1.75 6.06 38.3
          uint32_t 1.75 6.06 37.5
          float 8.125 10 31.5

          Как вы можете заметить, время вычислений отличается в разы даже для целочисленных типов данных, так что всегда нужно прикидывать, какая максимальная величина будет храниться в переменной, и выбирать соответствующий тип данных. Стараться не использовать 32-битные числа там, где они не нужны, а также по возможности не использовать float . В то же время, умножить long на float будет выгоднее, чем делить long на целое число. Такие моменты можно считать заранее как 1/число и умножать вместо деления в критических ко времени выполнения моментах кода. Также читай об этом чуть ниже.

          Отказаться от float

          Выбирать множители степенями двойки

          Заменить деление битовым сдвигом

          Что касается целочисленного деления на степени двойки, то компилятор не заменяет его сдвигом, и это можно и нужно сделать вручную. Например, деление long числа на 16 ( val / 16 ) выполняется в 15 раз дольше, чем операция сдвига с таким же результатом: val >> 4 (сдвинуть на 4 бита, 16 == 2 в степени 4). Для лонгов получаем 40 мкс на деление, и 2.5 мкс на сдвиг. Экономия! Примечание: слово целочисленный здесь не просто так, для float трюк не работает!

          Заменить деление умножением на float

          Заменить возведение в степень умножением

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

          Оптимизировать остаток от деления

          Предварительно вычислять то, что можно вычислить

          Не использовать delay() и подобные задержки

          Вполне очевидный совет: не используйте delay() там, где можно обойтись без него. А это 99.99% случаев. Используйте таймер на millis() , как мы изучали в уроке

          Заменить Ардуино-функции их быстрыми аналогами

          Использовать switch вместо else if

          В ветвящихся конструкциях со множественным выбором по значению целочисленной переменной стоит отдавать предпочтение конструкции switch-case , она работает быстрее else if (изучали в уроках про условия и выбор). Но помните, что switch работает только с целочисленными! Под спойлером найдёте результаты синтетического (не оптимизированного компилятором) теста.

          Помнить про порядок условий

          Если проверяется одновременно несколько логических выражений, то при наступлении первого результата, при котором всё условие однозначно получит известное значение, остальные выражения даже не проверяются. Например:

          Если flag имеет значение false , функция getSensorState() даже не будет вызвана! if будет сразу пропущен (или выполнен else , если он есть). Этим нужно пользоваться, расставляя условия в порядке возрастания процессорного времени, которое требуется для их вызова/выполнения, если это функции. Например, если наша getSensorState() тратит какое-то время для выполнения, то мы ставим её после флага, который является просто переменной. Это позволит сэкономить процессорное время в те моменты, когда флаг имеет значение false .

          Использовать битовые операции

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

          Использовать указатели и ссылки

          Использовать макро и встроенные функции

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

          Использовать константы

          Почему это происходит? Компилятор оптимизирует код, и с константными аргументами он может выбросить из функции почти весь лишний код (если там есть, например, блоки if-else ), и она будет работать быстрее.

          Миновать loop

          Кодить на ассемблере (шутка)

          Arduino IDE поддерживает ассемблерные вставки, в которых на одноимённом языке можно давать прямые команды процессору, что обеспечивает максимально быстрый и чёткий код. Но у нас в семье о таком не шутят =)

          Оптимизация памяти

          Использовать переменные соответствующих типов

          Как вы помните из урока о типах данных, каждый тип имеет ограничение на максимально хранимое значение, от чего прямо зависит вес этого типа в памяти. Вот они все:

          Название Вес Диапазон
          boolean 1 байт 0 или 1, true или false
          char (int8_t) 1 байт -128… 127
          byte (uint8_t) 1 байт 0… 255
          int (int16_t) 2 байта -32 768… 32 767
          unsigned int (uint16_t) 2 байта 0… 65 535
          long (int32_t) 4 байта -2 147 483 648… 2 147 483 647
          unsigned long (uint32_t) 4 байта 0… 4 294 967 295
          float (double) 4 байта -3.4028235E+38… 3.4028235E+38

          Просто не используйте переменные более тяжёлых типов там, где это не нужно.

          Использовать define

          Использовать директивы препроцессора

          Использовать progmem

          Внимание! При чтении отрицательных ( signed ) чисел, нужно привести тип данных. Пример:

          Также есть более удобный способ записывать и читать данные, реализован в библиотеках. Смотреть тут

          Использовать F() макро

          Ограничить использование библиотек

          Не использовать float

          Не использовать объекты классов Serial и String

          Использовать однобитные флаги

          Вы должны быть в курсе, что логический тип данных boolean занимает в памяти Arduino не 1 бит, как должен занимать, а целых 8, т.е. 1 байт. Это вселенская несправедливость, ведь по сути мы можем сохранить в одном байте 8 флагов true / false , а на деле храним только один. Но выход есть: паковать биты вручную в байт, для чего нужно добавить несколько макросов. Пользоваться этим не очень удобно, но в критической ситуации, когда важен каждый байт, можно и заморочиться. Смотрите примеры:

          Использовать битовое сжатие и упаковку

          Таким образом можно сжимать, разжимать и просто хранить маленькие данные в стандартных типах данных. Давайте ещё пример: нужно максимально компактно хранить несколько чисел в диапазоне от 0 до 3, то есть в бинарном представлении это 0b00 , 0b01 , 0b10 и 0b11 . Видим, что в один байт можно запихнуть 4 таких числа (максимальное занимает два бита). Запихиваем:

          Как и в примере со светодиодами, мы просто брали нужные биты ( в этом случае младшие два, 0b11 ) и сдвигали их на нужное расстояние. Для распаковки делаем в обратном порядке:

          И получим обратно наши байты. Также маску можно заменить на более удобную для работы запись, задвинув 0b11 на нужное расстояние:

          Ну и теперь, проследив закономерность, можно сделать для себя функцию или макрос чтения пакета:

          Выбор загрузчика

          Отказаться от стандартной инициализации

          Функции setup() и loop() в данном скетче уже не нужны, т.к. они не используются в нашем личном main() .

          Попробовать GyverCore

          Купить Arduino Mega

          Читайте также: