воскресенье, 25 февраля 2018 г.

Режимы энергосбережения Ардуино

Продолжаем тему минимизации энергопотребления Ардуино и теперь пора разобраться в режимах работы МК. Все AVR микроконтроллеры поддерживают различные режимы энергосбережения. Их описание можно найти в документации к МК в разделе Power Management and Sleep Modes. ATmega328P имеет 6 режимов:

  • Idle Mode (режим ожидания). В нем процессор приостанавливается, но остальная периферия - SPI, USART, аналоговый компаратор, шина TWI (I2C), таймеры/счетчики, сторожевой таймер и система прерываний - продолжают работать. Поэтому выход из режима Idle возможен как по внешнему, так и по внутреннему прерыванию. Основным преимуществом данного режима является быстрая реакция на события, приводящие к пробуждению МК. Другими словами, выполнение программы начинается сразу же после перехода из режима Idle в рабочий режим.
  • ADC Noise Reduction Mode (режим снижения шумов АЦП). Данный режим присутствует только в моделях МК, содержащих в своем составе АЦП и предназначен для уменьшения всевозможных наводок во время его работы. В этом режиме прекращает работу процессор и отключается синхронизация ввода-вывода, остальные устройства продолжают работу. Вывести МК из этого режима, кроме прерывания по завершению преобразования АЦП, могут:
    • прерывание от сторожевого таймера;
    • прерывание по совпадению адреса от интерфейса TWI;
    • прерывание от асинхронного таймера/счетчика 2;
    • прерывание по готовности EEPROM;
    • прерывание при изменения уровня (pin change interrupt)
    • внешние прерывания INT0 и INT1; 
    • а так же сбросы (аппаратного, от сторожевого таймера, от схемы BOD).
  • Power-Down mode (режим микропотребления) - самый экономный режим, присутствует во всех AVR. В этом режиме отключаются все внутренние тактовые сигналы, соответственно, прекращается функционирование всех узлов МК, работающих в синхронном режиме. Единственными узлами, продолжающими работать в этом режиме, являются асинхронные модули МК: сторожевой таймер (если он включен), подсистема обработки внешних прерываний и блок сравнения адреса модуля TWI. Пробуждение из режима Power-Down возможно при возникновении сброса (аппаратного, от сторожевого таймера, от схемы BOD) или в результате генерации следующих прерываний:
    • прерывание от сторожевого таймера;
    • прерывание по совпадению адреса от интерфейса TWI;
    • прерывание изменения уровня (pin change interrupt)
    • внешние прерывания INT0 и INT1.
  • Power Save Mode (экономичный режим). Идентичен режиму Power Down с одним отличием: в режиме Power Save таймер/счетчик 2 может продолжать работу, причем как в синхронном, так и асинхронном режиме. Вывести МК из этого режима могут те же события, что и в случае использования режима Power Down, а так же прерывания от таймера/счетчика 2.
  • Standby Mode (режим ожидания). Этот режим рекомендуется задействовать только при использовании внешнего резонатора. Он идентичен режиму Power Down, за исключением того, что тактовый генератор продолжает функционировать. Благодаря этому пробуждение МК и переход в рабочий режим требует всего 6 тактов.
  • Extended Standby Mode (расширенный режим ожидания). Как и режим Standby этот режим рекомендуется использовать совместно с внешним резонатором. Он идентичен режиму Power Save, за исключением того, что тактовый генератор продолжает функционировать. Благодаря этому пробуждение МК и переход в рабочий режим требует всего 6 тактов.
Для того чтобы перевести Ардуино в один из перечисленных режимов достаточно выполнить 2 функции: set_sleep_mode(<mode>) для задания конкретного режима и sleep_mode(). Они объявлены в файле sleep.h, поэтому он должен быть добавлен в секцию include вашего скетча.

#include <avr/sleep.h>

...
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
sleep_mode();

Для указания режима используются константы:

  • SLEEP_MODE_IDLE
  • SLEEP_MODE_ADC
  • SLEEP_MODE_PWR_DOWN
  • SLEEP_MODE_PWR_SAVE
  • SLEEP_MODE_STANDBY
  • SLEEP_MODE_EXT_STANDBY
В вышеприведенном фрагменте кода Ардуино переводится в режим Power Down и выполнение программы прекращается на функции sleep_mode() . Если после нее добавить, например, вывод текста в Serial или мигание светодиодом, то ничего этого мы не увидим. Для того чтобы выполнение программы продолжилось, необходимо вывести МК из спящего режима. События, пробуждающие процессор, перечислены выше в описании режимов. Давайте попробуем разбудить МК прерыванием от сторожевого таймера.

Пробуждение по сторожевому таймеру

В основе сторожевого таймера (WatchDog Timer) лежит многоразрядный счетчик, снабженный собственным тактовым генератором. Если таймер включен, то значение счетчика будет постоянно увеличиваться и при его переполнении будет сгенерирован сигнал сброса МК. Чтобы избежать сброса по переполнению программа должна постоянно обнулять счетчик специальной командой. Если программа зависла и счетчик не был вовремя сброшен, то сигнал сброса выведет МК из зависшего состояния, таким образом повышается стабильность системы на основе МК.

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

#include <avr/wdt.h>
#include <avr/sleep.h>

volatile bool f = 0;

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
  wdt_enable(WDTO_2S); //Задаем интервал сторожевого таймера (2с)
  WDTCSR |= (1 << WDIE); //Устанавливаем бит WDIE регистра WDTCSR для разрешения прерываний от сторожевого таймера
  set_sleep_mode(SLEEP_MODE_PWR_DOWN); //Устанавливаем интересующий нас режим
  sleep_mode(); // Переводим МК в спящий режим
  digitalWrite(LED_BUILTIN, f);
}

ISR (WDT_vect) {
  wdt_disable();
  f = !f;
}

Здесь задается интервал сторожевого таймера и режим его работы для генерации прерываний через 2 секунды. После выполнения функции sleep_mode() Ардуино переходит в спящий режим. При генерации прерывания от сторожевого таймера и пробуждении МК, управление передается соответствующему обработчику (ISR (WDT_vect)). После его выполнения продолжится работа основной программы, т.е. выполнится следующая после sleep_mode() команда. Для задания значения предделителя сторожевого таймера можно использовать одну из следующих констант:

  • WDTO_15MS
  • WDTO_30MS
  • WDTO_60MS
  • WDTO_120MS
  • WDTO_250MS
  • WDTO_500MS
  • WDTO_1S
  • WDTO_2S
  • WDTO_4S
  • WDTO_8S

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

Пробуждение при нажатии кнопки

Пробуждение микроконтроллера при замыкании/размыкании контактов, будь то кнопка, геркон, энкодер и т.п. - это пробуждение по внешнему прерыванию (INT) или по прерыванию изменения уровня (PCINT). Для работы с первыми в IDE Arduino предусмотрены удобные функции. Для вторых ничего подобного в IDE нет, поэтому придется работать с регистрами микроконтроллера, отвечающими за настройку PCINT (хотя, наверняка, существуют библиотеки для работы с ними, я не искал). Для тех и других у меня есть подробные публикации с примерами использования: Прерывания в Ардуино. Часть 1, и Часть 2.

Предположим, что микроконтроллер в нашем устройстве должен всегда спать, просыпаться при нажатии кнопки, подключенной к входу запроса прерывания INT0, что-то делать и снова засыпать. Вход запроса прерывания (для INT0 - это вывод D2 Ардуино) должен быть подтянут к питанию или земле. Публикация про подтяжку в тему. Мы воспользуемся внутренними подтягивающими резисторами микроконтроллера. Кнопку установим между выводами D2 и Gnd. Простой скетч для ухода в сон и пробуждения при нажатии может быть таким:

#include <avr/sleep.h>

#define INTERRUPT_PIN  2

void setup() {
  Serial.begin(9600);
  pinMode(INTERRUPT_PIN, INPUT_PULLUP); // Задействуем подтягивающий резистор
}

void loop() {
  byte b = 0;
  // Просыпаться будем при при изменении сигнала на входе INTERRUPT_PIN от HIGH к LOW
  attachInterrupt(digitalPinToInterrupt(INTERRUPT_PIN), myISR, FALLING);
  set_sleep_mode(SLEEP_MODE_PWR_DOWN); // PowerDown - самый экономный режим
  sleep_mode(); // Переводим МК в сон
  // Проснулись, запрещаем обработку внешнего прерывания
  detachInterrupt(digitalPinToInterrupt(INTERRUPT_PIN));

  // А здесь добавляем свой код, например, вывод в Serial
  Serial.println("Wake up!");
  
  // Перед тем как снова уйти в сон дождемся стабильного HIGH на входе INTERRUPT_PIN
  while (b < 5) { // Стабильным будем считать сигнал, который не изменится за 5 выборок
    if (digitalRead(INTERRUPT_PIN) == HIGH) 
      b++; // На входе HIGH, увеличиваем счетчик
    else
      b = 0; // На входе LOW, обнуляем счетчик
    delay(1); // Следующая выборка через 1мс
  }
  // Дождались HIGH, можем засыпать
}

void myISR() {

}

Скетч содержит достаточно комментариев, я поясню лишь назначение цикла while. Все электромеханические коммутирующие устройства подвержены дребезгу контактов. В нашем случае дребезг на входе запроса прерывания будет воспринят микроконтроллером как многократное нажатие кнопки, в результате чего он будет выходить из спящего режима, выполнять заданный код и уходить в сон несколько раз подряд. Чтобы этого не происходило мы откладываем уход в сон до момента отпускания кнопки и возвращения сигнала на входе D2 к высокому уровню. Если сигнал остается высоким в течение 5 выборок с интервалом в 1мс, то считаем, что он стабилизировался и можно снова переводить микроконтроллер в режим энергосбережения.

Если добавить в схему аппаратное подавление дребезга, то можно обойтись без описанного цикла.

Пробуждение при нажатии кнопки, уход в сон по таймауту

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

#include <avr/sleep.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>

#define INTERRUPT_PIN  2 // Для Arduino UNO можно использовать 2 или 3 пин
#define TIME_BEFORE_SLEEP 10000 // Время до ухода в сон 10с

//LiquidCrystal_I2C lcd(0x27, 16, 2);
LiquidCrystal_I2C lcd(0x27, 20, 4);
unsigned long tm = 0;
bool buttonPrev = 0;
bool f = 0;

void setup() {
  lcd.begin();
  lcd.print("Time: 12:00");
  pinMode(INTERRUPT_PIN, INPUT_PULLUP); // Задействуем подтягивающий резистор
}

void loop() {
  // Проверим не пора ли спать
  if (millis() - tm > TIME_BEFORE_SLEEP) { // Пора
    lcd.noBacklight(); // Выключаем подсветку дисплея
    //Разрешаем внешнее прерывание, которое разбудит МК
    attachInterrupt(digitalPinToInterrupt(INTERRUPT_PIN), myISR, FALLING);
    set_sleep_mode(SLEEP_MODE_PWR_DOWN); // PowerDown - самый экономный режим
    sleep_mode(); // Переводим МК в сон
    // Проснулись, запрещаем обработку внешнего прерывания
    detachInterrupt(digitalPinToInterrupt(INTERRUPT_PIN));
    tm = millis();
    lcd.backlight(); // Включаем подсветку дисплея
    buttonPrev = 0;
  }

  // А здесь основной код: опрос кнопок, вывод информации и т.п.
  if (isButtonPressed(INTERRUPT_PIN)) {
    f = !f;
    lcd.clear();
    if (f) lcd.print("Temp: 24C");
    else lcd.print("Time: 12:00");
    tm = millis();
  }
  delay(100);
}

void myISR() {

}

bool isButtonPressed(uint8_t pin) {
  if (digitalRead(pin) == HIGH) {
    buttonPrev = 1;
  }
  else if (buttonPrev) {
    // Нажата кнопка
    buttonPrev = 0;
    return true;
  }
  return false;
}

Здесь уход микроконтроллера в сон возможен только при выполнении условия: разница между текущим значением millis() и значением переменной tm должно быть больше 10 секунд. Для того чтобы отложить сон, достаточно записать в tm значение millis(), что и делается при пробуждении и при нажатии на кнопку.

Также в данном примере отключается подсветка дисплея (текстовый дисплей типа 20*4 или 16*2) при переходе микроконтроллера в режим энергосбережения и включается при выходе из него. Сделано это в большей степени для демонстрации. В реальном же устройстве лучше отключать питание дисплея, поскольку он  не имеет режима энергосбережения, а отключение подсветки уменьшит ток недостаточно - экономия должна быть экономной.


Для минимизации энергопотребления необходимо активнее использовать энергосберегающие режимы, причем выбирать режим следует так, чтобы в нем функционировало минимально необходимое количество узлов микроконтроллера. Также следует помнить о модулях МК, работа которых может продолжаться в спящих режимах. Если эти модули не используются в программе, то их работу следует запретить. К таким модулям относятся:
  • Аналого-цифровой преобразователь;
  • Аналоговый компаратор;
  • Детектор пониженного напряжения питания (Brown-Out Detector);
  • Внутренний источник опорного напряжения;
  • Сторожевой таймер;
  • Порты ввода-вывода;
  • Модуль внутрисхемной отладки.
Итак, продолжим эксперименты по снижению энергопотребления Ардуино. В предыдущей статье мы уже научились управлять тактовой частотой и сократили потребляемый Arduino Pro Mini ток до 1,24мА при напряжении питания 3.3В и частоте 1МГц. Попробуем теперь перевести микроконтроллер в спящий режим Power Down и отключить ненужные узлы.

#include <avr/sleep.h>

void setup() {
  ADCSRA &= ~(1 << ADEN); // Отключаем АЦП
  set_sleep_mode(SLEEP_MODE_PWR_DOWN); //Устанавливаем интересующий нас режим
  cli(); // Временно запрещаем обработку прерываний
  sleep_enable();
  // Отключаем детектор пониженного напряжения питания
  MCUCR != (1 << BODS) | (1 << BODSE);
  MCUCR &= ~(1 << BODSE);
  sei(); // Разрешаем обработку прерываний
  sleep_cpu(); // Переводим МК в спящий режим
  sleep_disable();
}

void loop() {

}

В режиме Power Down продолжают работать АЦП и схема контроля питания. В приведенном скетче есть команды для их отключения. АЦП при необходимости потом можно будет включить установкой бита ADEN. Потребляемый Arduino Pro Mini ток с данным скетчем составил 136мкА.

Arduino Pro Mini режим энергосбережения

Я считаю, это неплохой результат по сравнению с начальными 15мА.

Чтобы уменьшить потребление еще больше, придется отказаться от использования плат Ардуино и перейти на использование "голых" микроконтроллеров. Оцените, например, потребление ATmega328P при тактировании от внутреннего RC генератора в этой статье.

Советую также посмотреть следующую мою статью об управлении режимами энергосбережения Ардуино - Библиотека Low-Power.

66 комментариев:

  1. У меня на нано почему то не работает функция снижения частоты процессора (ругается при прошивке), но сбив её диоды и с помощью фунции (WatchDog Timer) я добился энергопотребления 1.7 mA. На блоке с восьми батареек спаянного по четыре в параллели, даже с периодическим выходом из сна для опроса датчиков, она должна проработать около 2 месяцев.

    ОтветитьУдалить
    Ответы
    1. Странно, что ругается при прошивке. У меня с этим проблем не было.
      Мне на днях пришли суперконденсаторы (2.7В, 4Ф) с Али, хочу попробовать запитать от них Ардуино, а для подзарядки использовать солнечную батарейку. Для периодического пробуждения и опроса датчиков этого должно хватить. Если получится что интересное, то отпишусь о результатах.

      Удалить
  2. ADCSRA &= ~(1 << ADEN); // Отключаем АЦП
    // Отключаем детектор пониженного напряжения питания
    MCUCR = bit (BODS) | bit (BODSE);
    MCUCR = bit (BODS);
    Как включить обратно?). Если можно толковую книгу посоветуйте где такие тонкости описывают. За ранее спасибо!

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Могу посоветовать книгу "Микроконтроллеры AVR семейств Tiny и Mega фирмы Atmel", плюс читайте datasheet.
      Для разрешения работы АЦП необходимо записать логическую единицу в разряд ADEN регистра управления модулем АЦП (ADCSR), а для выключения - логический ноль. Таким образом:
      ADCSRA &= ~(1 << ADEN); // Отключаем АЦП
      ADCSRA |= 1 << ADEN; // Включаем АЦП
      - это обычная логика: ADEN - седьмой бит регистра (т.е. ADEN = 7), выражение 1 << ADEN выполняет сдвиг единицы влево на 7, результат операции 10000000 - маска для изменения значения бита ADEN. Дальше мы либо сбрасываем, либо устанавливаем этот бит.

      С выключением схемы BOD чуть посложнее. За ее работу отвечает бит BODS регистра управления микроконтроллером MCUCR. Чтобы запретить работу схемы при переходе в спящий режим нужно записать в этот бит логическую единицу. Запись в BODS разрешается битом BODSE, поэтому для выключения схемы BOD нужно записать в оба этих бита логическую единицу, затем в течение 4 тактов записать в них 1 и 0 соответственно. Отключать BOD следует непосредственно перед переходом в спящий режим, пока бит BODS установлен, потому что по истечении 3 тактов он будет автоматически сброшен. Соответственно, включать его потом специально не нужно. Это все можно найти в даташите на ATmega328 в разделе 14.12.2 MCU Control Register.
      В скетче для отключения BOD я использовал функцию bit(n) - она эквивалентна сдвигу 1 << n.

      Удалить
  3. Спасибо за информацию, но для себя я сделал так - пишу фонарик:)))
    // КОМПИЛИРОВАТЬ В 1.6.5!!!!!!!!
    #include

    bool f = 0;
    byte keyPin = 2; // задаем вывод для кнопки кнопки (ардуиновский)

    void setup() {
    pinMode(LED_BUILTIN, OUTPUT);
    pinMode(keyPin, INPUT_PULLUP); // подтягиваем вывод кнопки к питанию (кнопка на землю)
    set_sleep_mode(SLEEP_MODE_PWR_DOWN); // Устанавливаем интересующий нас режим
    attachInterrupt(digitalPinToInterrupt(keyPin), myISR, LOW); // Прерывание по низкому уровню
    }

    void loop() {
    sleep_mode(); // Переводим МК в спящий режим, проснется когда на keyPin будет 0
    }

    void myISR() { //выполняется каждый раз, при нажатии кнопки
    while (!digitalRead(keyPin)) delay(20); // антидребезг
    f = !f;
    digitalWrite(LED_BUILTIN, f); // Далее выполняем интересующие нас действия
    }

    ОтветитьУдалить
  4. Владимир, можно узнать вашу почту, есть несколько вопросов, если можно?

    ОтветитьУдалить
  5. спасибо. Теперь ардуино не распознаётся!!!

    ОтветитьУдалить
    Ответы
    1. Вы, наверное, удивитесь, но код, записанный в микроконтроллер, не влияет на распознавание Ардуино. И у других все работает. Так что разбирайтесь, что вы там напортачили.

      Удалить
  6. прекрасная статья! подскажите, почему в одном случае вы используете sleep_mode(), а в другом slep_enable() и sleep_cpu()?

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Макросы sleep_enable и sleep_cpu вызываются внутри sleep_mode, поэтому одного его вызова достаточно. Но если требуется выключить brownout detection (детектор пониженного напряжения питания), то это должно быть сделано непосредственно перед уходом в сон (перед sleep_cpu). Об этом я писал чуть выше в комментариях, что бит BODS (отвечающий за разрешение работы bod) сбрасывается автоматически по истечении 3 тактов. Поэтому все некритичные по времени настройки выполняем в первую очередь и только потом отключаем bod и уходим в сон.

      Удалить
    2. у меня ардуино уно+лед дисплей16*2+радиомодуль в режиме ожидания потребляют 35мА, в активном режиме 55мА, а в режиме передачи данных - 100мА. Отключение ацп и детектора пониженного напряжения не уменьшает энергопотребление совершенно(

      Удалить
    3. Когда вы перейдете с использования Ардуино на отдельный микроконтроллер, вы заметите, что bod и ацп весьма прожорливые и их отключение в спящем режиме поможет снизить ток с порядка 100мкА до 1мкА и ниже.
      С Ардуино такого потребления не добиться. Пробуйте советы из предыдущей статьи (уменьшить напряжение, частоту, откажитесь от линейных регуляторов), но это все равно будут миллиамперы, не микро.

      Удалить
    4. догнать хотя бы до 20мА. частоту снижаю - сразу все перестает работать, напряжение от аккумулятора 3,7В - слабовато работает дисплей, ему 5В необходимо. а что за линейные регуляторы и как от них отказаться?

      Удалить
    5. В одной из последних статей я описывал способ изменения источника тактирования. Частоту можно будет менять в меню ide arduino, правда при этом нужно перезаписывать загрузчик, но это минутное дело с usbasp. Дальше микроконтроллер можно вынимать из ардуино (он будет работать от внутреннего RC-генератора) и использовать с небольшой обвязкой. Для перепрошивки микроконтроллер можно снова вставить в ардуино или использовать usb-ttl адаптер. Если в ардуино микроконтроллер не в dip корпусе, т.е. не вынимается, то можно купить отдельный. Да и тактирование можно оставить от кварца, а не от RC - вариантов масса. Но чтобы увидеть те самые микроамперы, заявленные в даташите, от Ардуино придется уйти.

      Удалить
    6. Линейные регуляторы установлены на платах ардуино и снижают подаваемое напряжение до необходимых 5В. Все что выше уходит в тепло. Отказаться от них - в смысле использовать для этих целей dc-dc преобразователь и подавать питание на пин 5V (в обход стабилизатора). Если у вас 3.7В то о преобразователе и речи нет. Разве что, наоборот, повысить напряжение, если дисплею этого мало.
      Касательно периферии - по ним тоже нужно изучать документацию, есть ли в них энергосберегающие режимы. Как вариант отключать их перед уходом в сон (используя транзисторный ключ)

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

      Удалить
  7. Владимир! У меня есть старый пульт, он сломался и я его переделал на Ардуино, в режиме работы потребляет 14 мА, быстро жрёт аккумулятор. Можно попросить вас за деньги дописать мой скрипт, чтобы пульт работал с режимами сна и потреблял меньше энергии. Цена вопроса?

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Пришлите мне скетч на почту, я посмотрю что можно сделать. VladimirTsibrov@yandex.ru

      Удалить
  8. Здравствуйте Владимир!
    Спасибо за ваши труды.
    Хочу спросить, есть ли у вас в планах раскрыть ваши темы на примерах работы в FLProg?
    Эта очень удобная платформа для тех кто занят проектами малой и средней автоматизации.

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      В планах такого не было. Могу посмотреть, прикинуть, что из этого получится. Речь именно об энергосбережении в FLProg?

      Удалить
    2. Энергосбережение и прерывания можно для каждого Пина ардуино. Там не очень сложно сделать блок. Блок это по сути код

      Удалить
  9. Есть ардуинка нано + радиомодуль, который отсылает каждые 15 мин на "базу" сигнал: подскажите - как усыпить на эти 15 мин и понизить потребление от батарейки.

    ОтветитьУдалить
    Ответы
    1. void loop()

      {
      radio.powerUp();
      //Serial.println("nrf on");
      delay(2000);
      data[0] = 200; // подаем сигнал (флаг 200) на базу о присутствии в радиусе действия
      //Serial.println("Send 200");

      for (int m=0; m <= 14; m++) // отправляем данные и указываем сколько байт пакет:
      // отправка 15 пакетов через 5 секунд
      {
      radio.write(&data, sizeof(data));
      delay(5000);
      //Serial.print ("Send signal: ");
      //Serial.println (m);
      }

      radio.powerDown();
      //Serial.println("nrf off and sleep 10 sec");
      delay(900000); // сон на 15 мин (900000 мс)

      }

      Удалить
    2. Возьмите скетч для пробуждения от сторожевого таймера, установите интервал таймера 8 секунд. Код для ухода в сон поместите в цикл for. 112 итераций цикла дадут примерно 15 минут сна.

      Удалить
    3. где то не так

      void loop() {

      for (int s=0; s <= 114; s++) {
      wdt_enable(WDTO_8S); //Задаем интервал сторожевого таймера (8с)
      WDTCSR |= (1 << WDIE); //Устанавливаем бит WDIE регистра WDTCSR для разрешения прерываний от сторожевого таймера
      set_sleep_mode(SLEEP_MODE_PWR_DOWN); //Устанавливаем интересующий нас режим
      sleep_mode(); // Переводим МК в спящий режим
      }

      digitalWrite(LED_BUILTIN, f);
      }

      ISR (WDT_vect) {
      wdt_disable();
      f = !f;
      }

      Удалить
    4. Зачем вам при каждом событии wdt инвертировать переменную f?
      Если вы хотите увидеть как Ардуино спит, то изменяйте значение f после цикла. Для тестов уменьшите продолжительность сна (интервал wdt и количество проходов цикла): чтобы проверить работоспособность ждать 15 минут не требуется.
      Также нет необходимости разрешать/запрещать работу wdt внутри цикла. Поставьте wdt_enable перед циклом, wdt_disable - после.

      Удалить
  10. Для чего в прерывании нужна эта строка?
    wdt_disable();

    ОтветитьУдалить
    Ответы
    1. Название функции говорит само за себя - она останавливает работу сторожевого таймера.
      Если WDT был запущен для того чтобы выйти из спящего режима, то после пробуждения он больше не нужен, поэтому отключается.

      Удалить
  11. Добрый день. А какая точность будет при пробуждении от Watch Dog Timer ?
    У меня 40 устройств, должны просыпаться в заданное время и отсылать по радиоканалу информацию, если их усыпить в одно время они все просыпаются как им захочется. Я устанавливаю что бы просыпались от Watch Dog Timer, Сон самый глубокий - SLEEP_MODE_PWR_DOWN, точность времени просыпания при таких параметрах и на большом времени уплывает в небытие.. Подскажите пожалуйста как можно добиться максимальной точности для пробуждений от таймеров при минимальном потреблении?

    ОтветитьУдалить
    Ответы
    1. Добрый день.
      Внутренний генератор на 128кГц, который служит источником тактирования для WDT, не годится для приложений, требующих точности. Его частота меняется в зависимости от температуры и напряжения питания.

      Я бы для более точной привязки к времени добавил в устройство RTC часы, а WDT использовал для выхода из сна и проверки текущего времени. Далее принимается решение опять перевести микроконтроллер в сон или выполнить какие-то действия. Да, это будет более затратно и по стоимости устройства, и в плане энергосбережения. Но зато наши события уже будут привязаны ко времени с некоторой известной нам погрешностью. Например, просыпаясь каждые 500мс, можно рассчитывать на погрешность этого же порядка.

      Чтобы сократить время выхода из сна можно использовать режимы Standby, в которых генератор не останавливается.

      Удалить
    2. Что касается синхронизации устройств, то с этим уже сложнее. Все зависит от требуемой точности. И RTC кстати также имеют свою погрешность, что нужно учитывать при их выборе.

      Удалить
    3. А если использовать режим SLEEP_MODE_PWR_SAVE + Timer2 для пробуждения, точность будет лучше? Забыл сказать что у меня плата на базе Atmega 328p (https://www.electrodragon.com/product/atmega328p-arduino-plus-lora-sx1278-board-loraduino/) я не пойму стоит ли там кварц, или нечто другое..

      Удалить
    4. Можно попробовать с таймером, это будет точнее WDT. Кварц там есть.

      Удалить
    5. Владимир с чего это вы взяли что при спящем режиме таймеры останавливаются, с каких это пор?

      Удалить
    6. Из даташита взял. Раздел 14. PM - Power Management and Sleep Modes

      Удалить
  12. Ясно. Большое спасибо за статьи и помощь!

    ОтветитьУдалить
  13. Очень полезная статья, Владимир!
    Используя периодическое пробуждение по сторожевому таймеру и режим SLEEP_MODE_PWR_DOWN снизил на моей Arduino RF-Nano энергопотребление примерно в 3 раза.
    Но есть одна проблема, которую не могу решить. На контроллере реализована некая система команд, которые я передаю ему с клавиатуры через Serial. Так вот, когда МК спит, то эти команды до него не доходят. Надо видимо как-то пробуждать его в момент передачи команд, чтобы он их принимал и выполнял. Не подскажете как можно это реализовать? Думаю, что тоже через прерывания, но признаться совсем не силен в них...

    ОтветитьУдалить
    Ответы
    1. Попробуйте перед уходом в сон разрешить PCINT16 (Pin Change Interrupt на пине D0, он же RX). При отправке данных на микроконтроллер возникнет прерывание, которое его разбудит. При пробуждении запрещаете PCINT16. На пробуждение и выход в рабочий режим нужно время, и часть передаваемых по Serial данных будет потеряна. Поэтому первая передача должна быть фиктивной.
      Про PCINT я подробно расписал здесь: https://tsibrov.blogspot.com/2019/06/arduino-interrupts-part2.html
      Там есть пример скетча с функцией разрешения PCINT на конкретном пине. Если ничего не путаю, вам нужен вызов: pciSetup(0); Обработчик прерывания PCINT2_vect оставьте пустым.

      Удалить
  14. Здравствуйте! Спасибо большое за Ваши статьи - очень полезны. Приведите, пожалуйста, пример с засыпанием и побудкой процессора в режиме аналогового измерения ADC Noise Reduction Mode. Толкового примера я к сожалению не нашел. Спасибо!

    ОтветитьУдалить
    Ответы
    1. #include "avr/sleep.h"

      void setup() {
      ADMUX = B01000000;
      // REFS[1:0] = 01 - источник опор. напряжения - AVcc
      // ADLAR = 0 - отключаем левое вырвнивание результата
      // MUX[3:0] = 0000 - к АЦП подключен вход A0

      ADCSRA = B10001111;
      // ADEN = 1 - включаем АЦП
      // ADIE = 1 - разрешаем прерывание от АЦП
      // ADPS[2:0] = 111 - устанавливаем делитель = 128 (16000КГц/128 = 125КГц)

      Serial.begin(9600);
      }

      void loop() {
      set_sleep_mode(SLEEP_MODE_ADC);
      ADCSRA |= 1 << ADSC; // устанавливаем бит ADC Start Conversion
      sleep_mode();
      // Проснулись. Результат оцифровки ждет нас в регистрах ADCL ADCH
      int V = ADCL | (ADCH << 8);
      Serial.println(V);
      delay(1000);
      }

      EMPTY_INTERRUPT(ADC_vect);

      Удалить
    2. Добрый день! Попробуйте такой код. В нем запускается АЦП перед уходом в сон. При завершении оцифровки модуль генерирует прерывание, которое пробуждает микроконтроллер. Обработчик как таковой не нужен, поэтому в скетче вместо макроса ISR используется EMPTY_INTERRUPT.
      За выбор канала отвечают биты MUX: 0000 - ADC0, 0111 - ADC7.

      Кроме программных приемов для повышения точности оцифровки есть рекомендации к проектированию устройств. Например, использование LC фильтра при подключении AVcc к источнику питания, минимизация длины проводников и т.п.

      Удалить
    3. Команду ADCSRA |= 1 << ADSC можно даже убрать, преобразование запустится автоматически при переходе в сон.

      Удалить
    4. Здравствуйте! Спасибо большое. Собираюсь изучить вопрос снижения шума подробнее с разных сторон. Сообщу результат.

      Удалить
  15. Здравствуйте! Прошу прощения за задержку ответа. Обработчик прерывания заменил на ISR(ADC_vect){} и всё заработало (нашёл в другом примере). До замены - не просыпался. Проверял на Arduino Nano, установленную на макетную беспаечную плату, - пульсации 1-2 дискреты при сигнале 0,8 максимального и различия с обычным измерением без засыпания не заметил. Думаю, что для снижения шума эффективнее сначала использовать "голый" процессор с правильной разводкой питания аналоговой и цифровой частей (в Nano они соединены вместе), а дальше использовать измерение во сне. Спасибо большое!

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Дело еще в том, что analogRead не худший вариант. Эта функция запускает АЦП и гоняет пустой цикл в ожидании окончания преобразования:
      while (bit_is_set(ADCSRA, ADSC));

      Другое дело, если бы мы запустили АЦП и пошли выполнять программу дальше: работать с таймерами, Serial, I2C и т.п. Вероятно тогда бы можно было ощутить те шумы, о которых говорится в даташите. Я бы проверил такой вариант для сравнения.

      Удалить
    2. Странно, что не работало с EMPTY_INTERRUPT. Макрос ISR отличается от него тем, что даже для пустого обработчика он добавляет пролог и эпилог - лишний код. Это, кстати, тоже интересная тема, можете почитать об этом здесь: https://tsibrov.blogspot.com/2019/06/arduino-interrupts-part2.html#Ключевое слово ISR

      Удалить
  16. Здравствуйте! Отличная статья. Но я столкнулся с проблемой. Устанавливаю arduino pro mini в состояние сна кнопкой. Для вывода из сна использую прерывание по второму пину и делаю перезагрузку. После включения питания цикл выкл-вкл проходит, но только 1 раз. reset не помогает, только повторное выключение питания. Не пойму кто сбрасывает состояния сна?! Если ставить блок в setup, все работает. В программе использую только softwareserial, его прерывания запрещаю командой end().
    Можно ли запретить все прерывания кроме одного(для вывода из сна)?

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Перезагрузку как делаете? Переход на нулевой адрес или через wdt?

      Удалить
  17. Меня удивляет разный результат от нажатия на кнопку reset и отключения и затем включения питания. Во втором случае работает один цикл сон - пробуждение от кнопки, а потом опять, сон "сбрасывается" каким-то прерыванием. Если оставить cli(), то все нормально, но разбудить можно только кнопкой reset.

    ОтветитьУдалить
    Ответы
    1. В старом загрузчике была проблема с зависанием после перезагрузки по wdt. Возможно, у вас как раз такой и прошит. В загрузчике optiboot (используется в уно) такой проблемы нет, поэтому, если еще сменили загрузчик в этой про мини, советую это сделать (при условии, что плата на 16мгц, а не на 8). Я описывал данную процедуру в статье https://tsibrov.blogspot.com/2018/07/arduino-as-isp.html
      В качестве целевой платы укажите не про мини, а уно. После этого при её программировании так же выбирать уно.
      Отпишитесь о результатах после обновления загрузчика.

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

      Удалить
  18. У меня в системе используется wdt, загрузчик был optiboot, а сейчас вообще его убрал. Программирую через SPI. Я тоже думал про ОЗУ или флаги/регистры (но какие?). С другой стороны, пробуждение может быть от wdt или внешние, но я их отключаю, если бы нет, то режим сна не реализовывался бы вообще. Помехи на кнопку? Но режим сна не реализовывался бы вообще, кроме этого, sleep_cpu() сбрасывается практически сразу, после установки. Softwareserial по команде end() запрещает прерывание, проверял. Подозрения на UART, к нему подключен приемопередатчик E32. Я его тоже перевожу в режим глубокого сна, но может E32 ведет себя не так, как надо. А сбрасывается E32 только при выключении питания.

    ОтветитьУдалить
  19. Нет, отключение E32 не влияет.

    ОтветитьУдалить
    Ответы
    1. wdt отключаете в сетапе? попробуйте добавить следующий код в самое начало функции setup:
      byte _mcusr = MCUSR;
      MCUSR = 0;
      if (_mcusr & 1 << WDRF) wdt_disable();

      Удалить
    2. Бит WDE регистра WDTCSR не сбросится пока установлен WDRF в MCUSR. Поэтому сначала нужно сбросить флаг, потом бит WDE.

      Удалить
  20. Заработало! Спасибо большое! А то я всю голову сломал.

    ОтветитьУдалить
  21. А я правильно понимаю, что, если проверка в setup показывает, что установлен WDRF, то микропроцессор выполнил перезагрузку, а если нет, то было включено питание (мне желательно знать была перезагрузка, или включение питания).

    ОтветитьУдалить
    Ответы
    1. Не совсем. Загляните в даташит, раздел 15.9.1. MCU Status Register
      WDRF - это Watchdog System Reset Flag, говорит о том, что источником перезагрузки был wdt
      PORF - это Power-on Reset Flag - сигнал сброса был сгенерирован схемой PoR, т.е. при подаче питания.
      Другие 2 флага для оставшихся источников сброса: BOD и внешний сброс.

      Удалить
  22. Можете подсказать как сделать сон 15 минут.
    Я делал так, но он все равно спит 8 секунд:
    wdt_enable(WDTO_8S);
    for(int s=0;s <= 112;s++){
    WDTCSR |= (1 << WDIE);
    }

    float h = dht.readHumidity(); //Измеряем влажность
    float t = dht.readTemperature(); //Измеряем температуру
    Serial.print(h);
    Serial.print("\t");
    Serial.print(t);
    Serial.println();
    if(millis() - tm > TIME_BEFORE_SLEEP){
    set_sleep_mode(SLEEP_MODE_PWR_DOWN);
    sleep_mode();
    tm = millis();
    }


    }
    ISR (WDT_vect) {
    wdt_disable();
    }

    ОтветитьУдалить
    Ответы
    1. А что вы собственно хотели сделать циклом for? Установить бит wdie 113 раз? Если же усыпить микроконтроллер, то в цикле этого нет. Ну и наворотили!

      #include
      #include

      void setup() {
      Serial.begin(9600);
      set_sleep_mode(SLEEP_MODE_PWR_DOWN);
      }

      void loop() {
      wdt_enable(WDTO_1S); // Запускаем wdt
      WDTCSR |= (1 << WDIE);
      for (int s = 0; s <10; s++) {
      sleep_mode();
      WDTCSR |= (1 << WDIE);
      }
      wdt_disable(); // Останавливаем wdt
      Serial.println("123");
      delay(100);

      }

      ISR (WDT_vect) {

      }

      Погуглите еще RTC модули с будильником.

      Удалить
  23. Этот комментарий был удален автором.

    ОтветитьУдалить
  24. Исправь бессмысленное
    MCUCR != (1 << BODS) | (1 << BODSE);

    ОтветитьУдалить
  25. Вместо lcd.begin(); надо lcd.init(); иначе ошибка скетча

    ОтветитьУдалить