Arduino FreeRTOS для устройств с питанием от батарей Печать
Добавил(а) microsin   

Arduino IDE и его рабочее окружение в настоящее время имеет множество готовых драйверов и библиотек для поддержки различных устройств. Однако чаще всего программы Arduino ограничены только функциями setup() и loop(), без какой-либо эффективной поддержки многозадачности (multi-tasking). Arduino не предоставляет прямой поддержки режимов пониженного потребления (low power modes), которые нужны для приложений, которые либо работают от батарей, либо в их распоряжении малый бюджет мощности (например, когда нужно питать устройство от слабых солнечных батарей).

В этой статье описывается использование режимов экономии энергии, которые используют аппаратную поддержку микроконтроллеров AVR ATmega (MCU) в среде Arduino FreeRTOS. Эта операционная система доступна в Arduino IDE как библиотека Arduino_FreeRTOS, и использует функции и макросы AVR LibC. Использование FreeRTOS вместе с Arduino позволяет взять все лучшее из обоих этих систем, что дает возможность упростить программирование и эффективно использовать источник питания для приложения.

[Немного теории]

Большинство операционных систем спроектированы таким образом, что позволяют сразу нескольким программам или потокам работать одновременно (даже на одном процессоре). Эта функция операционной системы называется многозадачностью (multi-tasking). Специальная часть операционной системы, которая называется планировщиком (scheduler), отвечает за распределение процессорного времени между задачами. Т. е. планировщик решает, какая программа и в какой момент должна переключиться на выполнение своей задачи. Такое переключение процессорного ядра между программами (потоками) может происходить достаточно быстро, что создать иллюзию одновременной работы сразу нескольких программ.

Традиционные планировщики реального времени (real time scheduler), такие как шедулер FreeRTOS, реализуют свой детерминизм, назначая определенный приоритет каждому выполняемому потоку. Тогда шедулер использует этот приоритет, чтобы узнать, какому потоку в какой момент времени следует передать процессорное время (т. е. какой поток и когда должен продолжить выполнять свой алгоритм). В системе FreeRTOS выполняемый поток обозначается термином Task (задача).

В системе FreeRTOS с помощью её специальной задачи ожидания Idle Task и функций библиотеки AVR LibC (подключаются заголовком avr/sleep.h) мы можем очень просто снизить потребление энергии микроконтроллера Arduino Uno на значительную величину.

Стандатные параметры, которые можно получить из даташита ATmega328p, обеспечивают потребление тока около 10 mA, когда микроконтроллер активен (находится в режиме Active), работает от напряжения 5V при тактовой частоте 16 МГц. Когда ATmega328p входит в режим Power-down (см. в программе SLEEP_MODE_PWR_DOWN), то потребляет токо между 4 μA и 8 μA. Использование этого режима потенциально может в 1000 раз снизить потребление энергии Вашим устройством.

В действительности мы не можем навсегда перевести микроконтроллер Arduino в режим Power-down, так как в этом режиме микроконтроллер спит слишком глубоко и не может выполнять никаких действий (как Белоснежка в известной сказке). В режиме Power-down mode перевести в рабочее состояние микроконтроллер могут только внешние прерывания, прерывание I2C и таймер Watchdog, если он разрешен. Все остальное выключено для снижения потребления энергии.

Трюк с вводом в сон и пробуждением Arduino MCU должен работать таким образом, что при пробуждении любая из задач (Task) системы FreeRTOS завершает проход цикла своего алгоритма, и снова вводит MCU в сон, как только это становится возможным. Таким способом среднее потребление энергии уменьшается пропорционально соотношению времени сна и времени активного состояния.

Так как библиотека Arduino FreeRTOS использует таймер Watchdog для работы планировщика (Scheduler, специальный системный код, который занимается планированием запуска задач Task), мы можем использовать любой из доступных режимов пониженного энергопотребления, и при этом будет гарантироваться пробуждение Arduino MCU, когда это необходимо. Из-за того, что мы используем таймер Watchdog, нет необходимости использовать внешние прерывания для пробуждения Arduino MCU, как советуют большинство описаний.

Но как мы узнаем, что Arduino MCU завершил текущие задачи, и больше нет задач FreeRTOS, которые требуют запуска? К счастью, FreeRTOS делает это для нас автоматически, через использование её специальной задачи Idle Task.

В системе FreeRTOS задача Idle Task запустится, когда планировщик определил, что нет других задач, которые требуют запуска. Т. е. Idle Task работает, когда все другие задачи заблокированы (находятся в состоянии Blocked [3]) на ожидании некого сигнала для своего пробуждения. Часть задачи (Tasks) ожидают определенный, специально указанный период времени, который должен пройти до момента, когда задача должна запуститься снова. В этой ситуации мы можем использовать Idle Task, чтобы запустить код, переводящий Arduino MCU в режим сна, так как мы знаем, что когда задача Idle Task начала работать, у планировщика нет других задач, которым нужно передать управление.

В библиотеке Arduino FreeRTOS задача Idle Task запускается только как функция loop() системы Arduino, чир очень упрощает добавление в скетч кода для перевода MCU в сон.

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

• Выключить все ненужные аппаратные фукнции MCU в функции setup(). Это можно сделать с помощью макросов заголовочного файла avr/power.h.
• Разрешить самый глубокий режим сна мы можем следать в функции loop(), с помощью функций из заголовочного файла avr/sleep.h.
• Выбрать самый медленный тик планировщика (FreeRTOS Scheduler Tick), который нас все еще устроит, чтобы максимально увеличить период пониженного потребления энергии, перед тем как MCU проснется и планировщик запустится для обслуживания приоритетов задач и разблокирования той задачи, которая этого требует. Выбор длительности тика зависит от требований Вашего приложения.
• Убедитесь, что все задачи заблокированы с конце своего тела цикла с помощью использования функций задержек FreeRTOS: vTaskDelayUntil() или vTaskDelay(), или с помощью использования семафоров, или очередей FreeRTOS. Семафоры и очереди используются для синхронизации задач, чтобы разбудить задачу, когда произошло какое-либо событие, или когда появились для обработки какие-либо данные (про использование семафоров Arduino FreeRTOS см. [2], также см. [4, 5], где подробно рассмотрена работа семфоров и очередей).

[Начнем?]

1. Если у Вас все еще не установлена система программирования Arduino IDE и библиотека Arduino FreeRTOS, то выполните необходимые действия, описанные в статье [6].

2. Создайте простейший проект Arduino с использованием FreeRTOS. Можете взять готовые примеры из статей [2, 6]. После этого выполните копирование и вставку в свой скетч кода из врезки ниже для функций setup() и loop(). Дайте своему скетчу подходящее имя и сохраните его.

3. Теперь можно озаботиться о выключение ненужной аппаратуры MCU, что позволит дополнительно снизить энергопотребление. В функции setup() нам требуется отключить те аппаратные функции MCU, которые не используются. Это делается с помощью макросов из заголовочного файла avr/power.h. Внимательно просмотрите код функции setup и сделайте необходимые правки (для начала можно оставить все как есть).

Обычно мы можем выключить цифровые входные буферы на выводах нашей платы Arduino, которые совмещены с аналоговыми входами (Analog Pins, входы мультиплексора АЦП). Для аналоговых входов цифровой входной буфер должен быть всегда запрещен. Аналоговый сигнал уровня около половины напряжения питания Vcc, который подан на любой цифровой или аналоговый вывод, может привести к большому потребляемому току даже при работе MCU в активном режиме. Обычно такое происходит, когда мы подаем на вход аналоговый сигнал. Цифровые входные буферы могут быть запрещены путем записи в регистры запрета входов (Digital Input Disable Registers, DIDR0 как DIDR2), как показано в коде примера.

Также мы можем выключить схему аналогового компаратора MCU (Analog Comparator). Когда осуществляется вход в режим ожидания/сна (Idle sleep mode), компаратор должен быть выключен, если не используется. Когда осуществляется вход в режим ADC Noise Reduction sleep (режим сна АЦП с пониженной генерацией шума), аналоговый компаратор также должен быть запрещен. Во всех других режимах сна аналоговый компаратор запрещается автоматически. Для выключения компаратора в примере также есть соответствующий демонстрационный код.

Общий принцип - мы запрещаем любые не используемые функциональные аппаратные возможности MCU с помощью макросов из заголовочного файла avr/power.h. Эти макросы влияют на биты управления, находящиеся в регистре управления питанием (Power Reduction Register), и установка или очистка соответствующего бита в этом регистре управляет каждой аппаратной функцией MCU. Предположим, что наше приложение не будет использовать функции, которые будут запрещены:

• Запретите модуль АЦП (Analog to Digital Converter, ADC) вызовом power_adc_disable().
• Запретите модуль SPI (Serial Peripheral Interface) вызовом power_spi_disable().
• Запретите модуль Two Wire Interface (I2C) вызовом power_twi_disable().
• Запретите модуль Timer 0 вызовом power_timer0_disable(). Имейте в виду, что millis() перестанет работать.
• Запретите модуль Timer 1 вызовом power_timer1_disable(). Имейте в виду, что перестанет работать analogWrite().
• Запретите модуль Timer 2 вызовом power_timer2_disable(). Имейте в виду, что перестанет работать tone(). Также этот таймер используется для часов реального времени (Real Time Clock, RTC) плат Goldilocks 1284p.

4. Выберите режим сна. Мы запретили (в функции sleep, пример см. во врезке в конце статьи) все ненужные функции, какие только возможно, чтобы уменьшить активный ток потребления Arduino. Теперь нужно решить, какой режим сна (sleep mode) лучше подойдет для Вашего специфичного приложения. Доступно 6 опций для для функции set_sleep_mode() из заголовочного файла avr/sleep.h, перечисленные ниже. В активном режиме ATmega328p платы Arduino Uno потребляет около 10 mA.

SLEEP_MODE_IDLE. Режим ожидания (Idle mode) останавливает CPU, при этом продолжают работу SRAM, таймеры/счетчики (Timer/Counter 0, 1, 2), USART, TWI (2-wire Serial Interface, или SPI), порт SPI, и разрешена работа системы прерываний. Приостановка CPU снижает потребление тока до примерно 2.5 mA.

SLEEP_MODE_ADC. Режим ADC Noise Reduction (режим снижения помех для работы АЦП) останавливает CPU и все модули ввода/вывода, кроме асинхронного таймера и ADC (АЦП), что минимизирует коммутационный шум, влияющий на процесс преобразования ADC. Этот режим сна подходит только для тех случаев, когда нужно повысить точность опцифровки АЦП.

SLEEP_MODE_PWR_DOWN. Режим Power-down сохраняет содержимое регистров, останавливая работу генератора, что запрещает все функции чипа, пока не произойдет прерывание или сброс. Когда происходит пробуждение из режима Power-down, существует задержка от момента наступления условия пробуждения до момента, когда чип MCU полностью запустится. Это дает время на перезапуск генератора, чтобы его частота стала стабильной после запуска, так как в режиме Power-down генератор был остановлен. Период пробуждения (wake-up period) определяется фьюзами CKSEL (около 1 мс). В режиме Power-down (когда работает только таймер Watchdog) MCU потребляет только 6 μA.

SLEEP_MODE_PWR_SAVE. В режиме Power-save асинхронный (внешний или real-time) Timer 2 продолжает работать, позволяя пользователю сохранить базовый отсчет времени, когда остальная часть MCU переведена в сон. При пробуждении из режима Power-save также существует задержка до полного пробуждения, определяемая фьюзами CSEL Fuses (около 1 мм). Разрешение работы асинхронного Timer 2 повышает потребление тока MCU до примерно 6.5 μA.

SLEEP_MODE_STANDBY. В режиме Standby кварцевый генератор работает, но остальная часть MCU находится в режиме сна. Это обеспечивает очень быстрое пробуждение (6 тактов CPU) вместе с достаточно низким энергопотреблением. В режиме Standby потребляемый ток меньше 0.2 mA.

SLEEP_MODE_EXT_STANDBY. В режиме Extended Standby кварцевый генератор работает, и также продолжает работать асинхронный (внешний или real-time) Timer 2, остальная часть MCU находится в режиме сна. Это также обеспечивает очень быстрое пробуждение (6 тактов CPU) вместе с достаточно низким энергопотреблением. В режиме Extended Standby потребляемый ток также меньше 0.2 mA.

Самый простой режим Idle, с которым можно получить экономию до 25% потребления тока от режима Active без каких-либо компромиссов, и с помощью режима Extended Standby можно достичь потребление тока около 2% от режима Active, с задержкой запуска всего лишь 6 тактов CPU.

Выбор самого медленного типа планировщика. Можно подстроить длительность времени, в течение которого Arduino MCU находится в режиме сна перед тем, как его разбудит таймер Watchdog. Длительность тика настраивается правкой файла заголовка FreeRTOSVariant.h из состава библиотеки Arduino_FreeRTOS, который находится в папке Вашего скетча.

// Для длительности тика системы System Tick (таймер планировщика)
// выберите период, с которым будут возникать прерывания для
// запуска планировщика.
 
/* Опции периода Watchdog: WDTO_15MS
                           WDTO_30MS
                           WDTO_60MS
                           WDTO_120MS
                           WDTO_250MS
                           WDTO_500MS*/
 
#define portUSE_WDTO WDTO_15MS

Путем изменения определения portUSE_WDTO мы можем подстроить длительность периода тика от 15 мс до 500 мс. Однако имейте в виду, что во время периода сна MCU полностью находится в мертвом сне, и пока этот период не истечет, он не сможет выполнять никаких действий (кроме тех случаев, когда Вы действительно реализуете одно из внешних прерываний, или будете использовать прерывание I2C/TWI). Таким образом, нужно соблюдать баланс между длительностью периода сна (чем длиннее он, тем будет меньше среднее потребление энергии) и отзывчивостью Вашего приложения.

Также имейте в виду, что задержки вычисляются целочисленной математикой. Это означает, что минимальная задержка может быть временем до следующего возникновения тика системы (эквивалентно vTaskDelay(0);), и нельзя реализовать задержку на дробную часть от периода тика.

Использование коротких периодов сна (малой длительности тика) в действительности неплохо, потому что когда планировщик проснется и не обнаружит необходимости запуска какой-либо задачи, то он сразу вызовет задачу Idle Task, содержащую функцию loop() Arduino, что немедленно вернет MCU в режим мертвого сна.

Помните, что когда Вы используете функцию loop() Arduino вместе с библиотекой Arduino_FreeRTOS, функция loop() никогда не должна блокироваться (переходить в состояние Block), или переходить в пустое занятое ожидание с использованием функции delay(), или иметь в своем теле какую-либо другую функцию задержки, так как функция loop() вызывается задачей Idle Task системы FreeRTOS (эта задача никогда не должна получать блокировку).

В качестве примера предположим, что наше приложение занимает время 1 мс для некоей обработки (оцифровка сигнала или чтение вывода), и это должно происходить 4 раза в секунду. Путем выбора WDTO_250MS для portUSE_WDTO мы может разбудить плату Arduino на 1 мс каждую четверть секунды (250 мс). При этом приложение будет держать MCU в течение 1 мс на токе потребления 10 mA, и оставшееся время 248 мс MCU будет проводить в режтме Power, потребляя при этом только 6.5 μA. Это даст среднее потребление энергии примерно 87 μA.

[Что дальше?]

Когда Вы создали скетч с несколькими задачами и использованием сна, попробуйте поменять использование set_sleep_mode(); проверьте, насколько измениться потребление энергии с разными вариантами выброра параметра для set_sleep_mode().

// Для set_sleep_mode() доступны опции:
// SLEEP_MODE_IDLE// SLEEP_MODE_ADC
// SLEEP_MODE_PWR_DOWN
// SLEEP_MODE_PWR_SAVE
// SLEEP_MODE_STANDBY
// SLEEP_MODE_EXT_STANDBY
set_sleep_mode( SLEEP_MODE_PWR_DOWN );

Изучите даташит ATmega328p, чтобы получить более подробное описание для каждого из разных режимов управления питанием (power reduction modes). Поскольку имеется всего 6 разных режимов экономии энергии, то важно понимать их особенности и отличия, чтобы правильно выбрать нужный вариант опции, который больше всего подойдет для Вашего приложения.

[Правильный выбор железа]

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

Есть несколько вариантов обхода этой проблемы.

• Используйте Arduino-совместимую плату, у которой полностью отключается активный линейный стабилизатор (с помощью перемычек), чтобы была возможность перейти только на необходимое батарейное питание. Например, для этого подойдут платы наподобие Goldilocks Analogue или Freetronics EtherMega (хотя у этих плат есть аппаратные средства, которые также потребляют энергию, и они могут быть избыточными для Вашего приложения). Хороший кандидат для аппаратуры - полностью самодельный макет наподобие Veroduino [7], который можно прошивать программатором, либо с помощью Arduino-загрузчика и специального адаптера USB - TTL UART.
• Используйте Arduino-совместимую плату с эффективным с точки зрения КПД импульсным стабилизатором напряжения (SMPS), с малым током утечки, наподобие FTDI Nero.
• Используйте устройство, изначально разработанное в расчете на низкое потребление и питание от батареи, наподобие Whisper Node [8].

Ниже приведен всего лишь код функций sleep() и loop(), предназначенный для использования режимов экономии энергии в рабочей среде Arduino + FreeRTOS. Для реального рабочего приложения требуется еще определить код задач (примеры см. в [2, 6]).

// Подключение функций AVR LibC, где есть макросы для управления потреблением
// энергии (power reduction):
#include <avr/power.h>
// Подключение функций сна Arduino (AVR):
#include <avr/sleep.h>

////////////////////////////////////////////////////////////////////////////////
// Функция setup запустится один раз, когда нажата кнопка сброса reset,
// или когда на плату Arduino подано питание. Здесь добавлены куски кода,
// выключающие не используемые аппаратные функции микроконтроллера,
// с целью достичь минимального потребления энергии для нашей программы.
void setup() {  
   // Запрет цифровых входов (Digital Input Disable) на входах АЦП (Analogue Pins).
   // Когда в этот бит записана лог. 1, запрещается цифровой входной буфер
   // на выводах микроконтроллера, совмещенных со входами АЦП (ADC).
   // Соответствующий регистр PIN всегда будет читаться как 0, когда этот бит
   // установлен. Когда аналоговый сигнал приходит на выводы ADC7 .. ADC0,
   // и цифровой вход на этих выводах не нужен, этот бит должен быть записан
   // в лог. 1, чтобы отключить цифровой буфер и тем самым снизить потребление
   // энергии.
#if defined(__AVR_ATmega640__) || defined(__AVR_ATmega1280__)
 || defined(__AVR_ATmega1281__) || defined(__AVR_ATmega2560__)
 || defined(__AVR_ATmega2561__)
   // Плата с микроконтроллером наподобие ATmega2560.
   DIDR0 = 0xFF;
   DIDR2 = 0xFF;
#elif defined(__AVR_ATmega644P__) || defined(__AVR_ATmega644PA__)
   || defined(__AVR_ATmega1284P__) || defined(__AVR_ATmega1284PA__)
   // Goldilock с микроконтроллером наподобие ATmega1284p.
   DIDR0 = 0xFF;
#elif defined(__AVR_ATmega328P__) || defined(__AVR_ATmega168__)
   || defined(__AVR_ATmega8__)
   // Предположительно Arduino с микроконтроллером ATmega328P.
   DIDR0 = 0x3F;
#elif defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega16U4__)
   // Предположительно Arduino Leonardo с микроконтроллером ATmega32u4.
   DIDR0 = 0xF3;
   DIDR2 = 0x3F;
#endif

   // Запрет аналогового компаратора.
   // Когда в бит ACD записана лог. 1, питание аналогового компаратора выключено.
   // Этот бит может быть установлен в любой момент, чтобы отключить аналоговый
   // компаратор. Это снизит потребление в режимах Active (активный режим)
   // и Idle (режим ожидания, или режим сна).
   // Когда меняется бит ACD, должно быть запрещено прерывание 
   // Analogue Comparator путем очистки бита ACIE в регистре ACSR.
   // Иначе при изменении бита ACD произойдет прерывание.
   ACSR &= ~_BV(ACIE);
   ACSR |= _BV(ACD);
   
   // ВЫБЕРИТЕ ЛЮБОЙ ИЗ МАКРОСОВ avr/power.h, КОТОРЫЙ ВАМ НУЖЕН.
   // Любой из макросов *_disable() имеет свою обратную альтернативу
   // в виде макроса *_enable().

   // Запрет модуля АЦП (Analog to Digital Converter).
   power_adc_disable();

   // Запрет модуля SPI (Serial Peripheral Interface).
   power_spi_disable();

   // Запрет Two Wire Interface (I2C).
   power_twi_disable();

   // Запрет модуля Timer 0. После этого millis() работать не будет.
   power_timer0_disable();

   // Запрет модуля Timer 1.
   power_timer1_disable();

   // Запрет модуля Timer 2. Используется для RTC на устройствах Goldilocks 1284p.
   power_timer2_disable();

   // Здесь может быть допонительный код для инициализации задач (Tasks)
   // и конфигурирования интерфейсов (которые не запрещены). Сделайте
   // в этом месте все настроечные действия, которые Вам необходимы.
}

////////////////////////////////////////////////////////////////////////////////
// Здесь находится только лишь код, который требуется для перевода в сон
// микроконтроллера (MCU) платы Arduino в периоды времени между тиками
// планировщика (Scheduler Ticks).
// Помните, что loop() это просто задача ожидания FreeRTOS (Idle Task).
// Здесь выполняются только некие служебные действия, но никакой
// "полезной" работы приложения.
loop()
{
   // В заголовочном файле sleep.h имеется несколько макросов для перевода 
   // MCU ATmega328p в режим сна (sleep mode), или режим пониженного
   // потребления энеригии). Таких режимов у AVR несколько, за подробностями
   // обратитесь к даташиту ATmega328p (или того MCU, который у Вас
   // используется).
 
   // SLEEP_MODE_IDLE
   // SLEEP_MODE_ADC
   // SLEEP_MODE_PWR_DOWN
   // SLEEP_MODE_PWR_SAVE
   // SLEEP_MODE_STANDBY
   // SLEEP_MODE_EXT_STANDBY
 
   set_sleep_mode( SLEEP_MODE_PWR_DOWN );
   portENTER_CRITICAL();
   sleep_enable();

   // Код sleep_bod_disable() нужен только если необходимо отключить
   // функцию срабатывания детектора пониженного напряжения питания
   // (brown-out detection). Если brown-out не установлен, то не требуется
   // много действий, чтобы это проверить.
#if defined(BODS) && defined(BODSE)
   sleep_bod_disable();
#endif

   portEXIT_CRITICAL();

   sleep_cpu();   // Спокойной ночи, MCU...
 
   // В этом месте MCU разбужен. Лучше всего запретить режим сна.
   // Сброс с помощью sleep_mode() работает быстрее, чем sleep_disable().
   sleep_reset();
}

[Ссылки]

1. Battery Powered Arduino Applications through FreeRTOS site:hackster.io.
2Использование семафоров FreeRTOS в Arduino IDE.
3. FreeRTOS: практическое применение, часть 1 (управление задачами).
4FreeRTOS: практическое применение, часть 3 (управление прерываниями).
5FreeRTOS: практическое применение, часть 4 (управление ресурсами).
6. Многозадачность FreeRTOS на платформе Arduino.
7veroduino: самодельный дешевый Arduino.
8. Whisper Node site:bitbucket.org.
9Femto OS: RTOS для маломощных MCU наподобие AVR.
10. OS для Arduino: 5 хороших вариантов в 2021 году.