Снова о автономной Arduino-метеостанции на батарейках +29


AliExpress RU&CIS

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


Это было донедавна. Несколько дней назад меня поразил очередной проект @Berkseo, как поражают все его проекты: "Беспроводная мини погодная станция с e-paper экраном на батарейках". Тут все на уровне промышленного продукта. Удивляет единственное — в устройстве нет внешнего датчика.


Полтора года назад разместил статью Автономная метеостанция на контроллере ATMEGA328P и питанием от батареек с беспроводным выносным датчиком. Хотя прототип и выполнял свои функции, но имел серьезные недостатки — слишком малая частота обновления данных и большое энергопотребление.


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



Что сделано:


Датчики DHT22 и DS18B20, которые использовались в предыдущем проекте, заменены энергосберегающим модулем — это датчик температуры и влажности HTU21D. Период измерений, отправки/приема данных уменьшен с 15 мин до 53,5 сек. Сделан переход на устойчивую частоту работы контроллера (8 МГц) при напряжении питания ниже 3 В. Для уменьшения объемов занимаемой памяти в скетчах использованы некоторые функции С/С++. И главное, принципиально изменен алгоритм передачи пакетов с выносного датчика и алгоритм приема этих пакетов базой метеостанции. Теперь для обеспечения надежного приема пакетов с выносного датчика в нем формируется и отправляется с интервалом около 0,3 сек не один, а три пакета с данными о параметрах воздуха на улице и состоянии батареек. Только после отправки третьего пакета контроллер в. датчика вместе с периферией уходит в сон. База метеостанции уходит спать после приема одного из 6-ти пакетов с выносного датчика и просыпается за полсекунды до поступления очередной серии пакетов с выносного датчика.


Метеостанция состоит из двух автономных узлов с питанием от двух батареек AA: базы и выносного датчика. Назовем их для простоты анализатором (по-другому – база) и беспроводным в.датчиком (выносным датчиком).


Анализатор, построен на контроллере ATMEGA328P, измеряет температуру и влажность (датчик температуры и влажности HTU21D) в помещении, а также измеряет и анализирует величину напряжения питания узла, которое обеспечивают две батарейки АА 1,5 В. На контроллер также поступает сигнал с приемника LoRa, который по эфиру принимает информацию с выносного датчика. Вся инфа с контроллера выводится на ЖК-дисплей NOKIA 5110.


В в.датчике, тоже собранном на контроллере ATMEGA328P, измеряется температура и влажность воздуха на улице (модуль HTU21D), а также напряжение питания выносного узла, организованного на двух батарейках АА 1,5 В. Передатчик LoRa этого узла передает инфу о температуре, влажности и состоянии батарейки на анализатор. С в.датчика выполняется отправка 3 пакетов с интервалом около 0,3 сек, затем контроллер ATMEGA328P, передатчик LoRa и модуль HTU21D для экономного расходования заряда батареек переводятся в режим сна. Измерения и отправка данных с в.датчика выполняется с циклом несколько меньше 1-ой минуты.


Работа анализатора построена по следующему алгоритму:


Вначале, при включении обоих узлов метеостанции, контроллер анализатора подает команды на измерение температуры и влажности внутри помещения и выводит эти параметры на дисплей, затем устанавливает приемник LoRa в режим прослушивания эфира. После приема сигнала с в.датчика и успешной дешифрации принятых данных контролер подает команду на повторное измерение температуры, влажности и выводит инфу в полном объеме на экран. Затем анализатор уходит в сон, просыпаясь примерно за полсекунды до планируемого поступления сигнала с в.датчика. Приняв и дешифровав один из трех пакетов с в.датчика, повторно выполняет свои измерения, выводит информацию на экран и снова уходит спать. Если по каким-то причинам сигнал с в.датчика отсутствует около одной минуты (например, сели батарейки), что по времени соответствует отправке 6 пакетов с в.датчика, анализатор проводит измерения только в помещении, изредка сканируя эфир: а вдруг в.датчик появился в эфире?! Это сделано для того, чтобы постоянно работающий на прием модуль LoRa не посадил за короткое время батарейки анализатора.


Для сборки устройства понадобятся радиодетали:


  1. Контроллер ATMEGA328P-PU — 2 шт.
  2. Датчик влажности и температуры HTU21D/SHT21/Si7021 — 2 шт.
  3. ЖК-дисплей NOKIA 5110 — 1 шт.
  4. Приемник-передатчик LoRa Rа-01 — 2 шт.
  5. Макетная плата (стеклотекстолит), монтажные провода, батарейки АА, кварцевые резонаторы 8 МГц, резисторы, конденсаторы, другие мелочи.

Ориентировочная стоимость компонентов по ценам AliExpress примерно $25.


Для работы с контроллерами ATMEGA328P в качестве программатора я использую плату Arduino UNO. На Youtube есть хорошее видео по установке загрузчика и загрузки скетчей в контроллер ATMEGA328P с помощью платы Arduino UNO.


На этот раз мы не будем устанавливать новые фьюзы программой SinaProg, а воспользуемся, на мой взгляд, более универсальным способом — созданием новых конфигураций плат в платформе Arduino IDE.


В новые контроллеры надо установить загрузчик Arduino as ISP и надо учитывать то, что контроллеры ATMEGA328P поступают в продажу с заводской настройкой фьюз для мониторинга (контроля) напряжения питания не ниже 2,7 В. Мы же будем работать от батареек, напряжение на которых при разряде может быть ниже установленного заводского порога 2,7 В, и с кварцем 8 МГц. Установим загрузчик и изменим фьюзы под наши условия, используя в качестве программатора плату Arduino UNO, в такой последовательности:


  1. Найти по адресу c:\Program Files\Arduino\hardware\arduino\avr\ файл boards.txt и открыть его текстовом редакторе с форматированием, например, AkelPad.
  2. Дополнить файл блоком, который приведен под спойлером, и сохранить файл.

    блок установок 1
    ##############################################################

    amega.name=Mega Low (8 MHz, >1.8V)

    amega.upload.tool=avrdude
    amega.upload.protocol=arduino
    amega.upload.maximum_size=32256
    amega.upload.maximum_data_size=2048
    amega.upload.speed=57600

    amega.bootloader.tool=avrdude
    amega.bootloader.low_fuses=0xFF
    amega.bootloader.high_fuses=0xDA
    amega.bootloader.extended_fuses=0xFE
    amega.bootloader.unlock_bits=0x3F
    amega.bootloader.lock_bits=0x0F
    amega.bootloader.file=optiboot/optiboot_atmega328.hex

    amega.build.mcu=atmega328p
    amega.build.f_cpu=8000000L
    amega.build.board=AVR_UNO
    amega.build.core=arduino
    amega.build.variant=standard

  3. В плату Arduino UNO загрузить скетч ArduinoISP.ino из примеров платформы Arduino IDE (Файл –> Примеры –> ArduinoISP).
  4. Собрать схему (плата Arduino UNO, контроллер ATMEGA328P, кварц 16 МГц) для установки в контроллер загрузчика ArduinoISP (инструкции – тут), подключить ее компьютеру и записать в контроллер бутлоадер Arduino as ISP.
  5. Заменить кварц в схеме 16 МГц на 8 Мгц. В меню ИНСТРУМЕНТЫ выбрать из списка плату Mega Low (8 MHz, >1.8V), которая появилась в меню после дополнения файла boards.txt новым блоком, выбрать тут же Программатор: “Arduino as ISP” и, нажав Записать загрузчик изменить фьюзы и другие установки в контроллере.
  6. Далее загружаем в контроллер необходимый скетч, используя ту же схему, что и для установки загрузчика (п.4), через Скетч –> Загрузить через программатор.

Выносной датчик


В.датчик построен на контроллере ATMEGA328P. В нем осуществляется прием данных с HTU21D по протоколу I2C, измерение и анализ величины напряжения питания узла и управление передатчиком LoRa.


скетч в.датчика
/*
   Снова о автономной Arduino-метеостанции на батарейках, выносной датчик
   https://habr.com/ru/post/544936/
*/

#include <avr/io.h>
#include <util/delay.h>

#include <SPI.h>
#include <LoRa.h>
#include <LowPower.h>
#include <Wire.h>
#include <avr/power.h>
#include "HTU21D.h"

#define VccHTU 8  //питание и подтяжка HTU21D (pin 14 AtMega328P, D8)
HTU21D myHTU21D;
float Tout; // температура
int Hout;  // влажность

unsigned int sleepCounter, sleepCounter0; // счетчик, задающий время сна
int pct;  //счетчик числа пакетов перед уходом в сон
String messageOut; // LoRa-сообщение
float BatOut; // напряжение батареек
const int batteryPin = A0; // pin 23 (Atmega328P), к которому подключена батарея для измерения напряжения
const float typVbg = 1.132; //калибровочная константа, 1.0 - 1.2

int counter = 0;
// измерение опорного напряжения
float readVcc() {
  byte i;
  float result = 0.0;
  float tmp = 0.0;

  for (i = 0; i < 1; i++) {
    // Read 1.1V reference against AVcc
    // set the reference to Vcc and the measurement to the internal 1.1V reference
#if defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
    ADMUX = _BV(REFS0) | _BV(MUX4) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
#elif defined (__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
    ADMUX = _BV(MUX5) | _BV(MUX0);
#elif defined (__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__)
    ADMUX = _BV(MUX3) | _BV(MUX2);
#else
    // works on an Arduino 168 or 328
    ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
#endif

    _delay_ms(3); // Wait for Vref to settle
    ADCSRA |= _BV(ADSC); // Start conversion
    while (bit_is_set(ADCSRA, ADSC)); // measuring

    uint8_t low  = ADCL; // must read ADCL first - it then locks ADCH
    uint8_t high = ADCH; // unlocks both

    tmp = (high << 8) | low;
    tmp = (typVbg * 1023.0) / tmp;
    result = result + tmp;
    _delay_ms(5);
  }
  return result;
}

void Measurement () {
  // измерение температуры и влажности
  Hout = myHTU21D.readHumidity();
  Hout = 62;  //delete!
  float Tout_p = myHTU21D.readTemperature();
  Tout = 0.1 * int(Tout_p * 10 + 0.5);  //округление до десятых
  // измерение напряжения батареек
  BatOut = 0.1 * int(readVcc() * 10 + 0.5);
  if (BatOut < 2.2) {
    BatOut = 0.0;
  } else {
    BatOut = 2.2;
  }
}

void SendMessage () {
  // отправка данных (температура, влажность, состояние батареек)
  if (BatOut > 2.1) {
    messageOut = String(Tout) + "#" + String(Hout) + "$" + String("BGood");
  }
  else {
    messageOut = String(Tout) + "#" + String(Hout) + "$" + String("BLow");
  }

  LoRa.beginPacket();
  LoRa.print(messageOut);
  LoRa.endPacket();
}

void setup() {
  Serial.begin(9600);
  Serial.println("Power ON");
  analogReference(DEFAULT);

  pinMode(VccHTU, OUTPUT);
  digitalWrite(VccHTU, 1);
  _delay_ms(200);
  myHTU21D.begin();

  int counter = 0;
  while (!LoRa.begin(433E6) && counter < 10) {
    Serial.println("Не удалось найти LoRa-передатчик!");
    counter++;
    _delay_ms(500);
  }
  LoRa.setTxPower(4); //мощность передатчика, 2...20 дБ
  LoRa.setSyncWord(0xF3);
}

void loop() {
  digitalWrite(VccHTU, 1);
  if (pct < 3)
  { // измерения, отправка пакетов
    Serial.println(messageOut);
    Measurement ();
    SendMessage ();
  } else {// измерения, отправка пакета и длительный сон
    Serial.println(messageOut);
    Serial.println("sleep ...");
    Measurement ();
    SendMessage ();
    for (sleepCounter = 6; sleepCounter > 0; sleepCounter--)
    {
      digitalWrite(VccHTU, 0);
      digitalWrite(VccHTU, 1);
      LoRa.sleep ();
      LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF);
    }
    pct = 0;
  }
  pct++;
  if (pct >= 3) pct = 3; //защита от переполнения счетчика
}

int main() {
  init();
  setup();

  for (;;) {
    loop();
  }
}

Электрическая схема в.датчика:



Питание и подтяжка выводов модуля HTU21D осуществляется с пина 14 контроллера ATMEGA328P. Это сделано для того, чтобы программно обнулить питание HTU21D и перевести этот датчик в режим низкого энергопотребления во время сна.


В в.датчике формируется и отправляется с интервалом около 0,3 сек три пакета с данными о температуре и влажности на улице и состоянии батареек. Если напряжение на батарейках выше установленного порога (2,2 В), то в коде пакета присутствует BGood, а ниже — BLow. После отправки третьего пакета контроллер в.датчика вместе с периферией уходят в сон. Цикл отправки серий пакетов — 53,5 сек.


Анализатор


Мозг анализатора – контроллер ATMEGA328P. Он принимает сигналы с датчика HTU21D по протоколу I2С и по SPI взаимодействует с приемником LoRa и дисплеем NOKIA 5110.


скетч анализатора
/*
   Снова о автономной Arduino-метеостанции на батарейках, анализатор
   https://habr.com/ru/post/544936/
*/

#include <avr/io.h>
#include <util/delay.h>

#include <SPI.h>
#include <LoRa.h>
#include <LowPower.h>
#include "HTU21D.h"
#include <LCD5110_Graph.h>

#define VccHTU 8  //питание и подтяжка HTU21D(pin 14 AtMega328P, D8)
HTU21D myHTU21D;
float Tin; // температура в помещении
int Hin;  // влажность в помещении

LCD5110 myNokia(3, 4, 5, 6, 7);
extern uint8_t SmallFont[];
extern uint8_t MediumNumbers[];

float BatIn = 0; // напряжение батареи
const int batteryPin = A0; // pin 23(Atmega328P), к которому подключена батарея для измерения напряжения
const float typVbg = 1.132; //калибровочная константа, 1.0 - 1.2

unsigned int sleepCounter;  //счетчик, задающий время сна

int r; //счетчик циклов прослушивания эфира
int mlc;  //счетчик циклов работы без в.датчика

String LoRaData, Tout_str, Hout_str, BatIn_str, BatOut_str;

// измерение напряжения батареек
float readVcc() {
  byte i;
  float result = 0.0;
  float tmp = 0.0;

  for (i = 0; i < 1; i++) {
    // Read 1.1V reference against AVcc
    // set the reference to Vcc and the measurement to the internal 1.1V reference
#if defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
    ADMUX = _BV(REFS0) | _BV(MUX4) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
#elif defined (__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
    ADMUX = _BV(MUX5) | _BV(MUX0);
#elif defined (__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__)
    ADMUX = _BV(MUX3) | _BV(MUX2);
#else
    // works on an Arduino 168 or 328
    ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
#endif

    _delay_ms(3); // Wait for Vref to settle
    ADCSRA |= _BV(ADSC); // Start conversion
    while (bit_is_set(ADCSRA, ADSC)); // measuring

    uint8_t low  = ADCL; // must read ADCL first - it then locks ADCH
    uint8_t high = ADCH; // unlocks both

    tmp = (high << 8) | low;
    tmp = (typVbg * 1023.0) / tmp;
    result = result + tmp;
    _delay_ms(5);
  }
  return result;
}

void Measurement() {
  float Tin0;
  // измерение напряжения батареи:
  BatIn = readVcc();
  // измерение температуры  и влажности в помещении
  Hin = myHTU21D.readHumidity();
  // Hin = 58; // delete!
  float Tin_p = myHTU21D.readTemperature();
  Tin = 0.1 * int(Tin_p * 10 + 0.5);  //округление до десятых
  //  Tin = 21.4; // delete!
}

void draw() {
  myNokia.enableSleep();
  myNokia.clrScr();

  //Tin
  char chr_Tin [5];
  String Tin_str = String(Tin);
  myNokia.setFont(SmallFont);
  myNokia.print("            C", LEFT, 0);
  myNokia.print("In", LEFT, 8);
  myNokia.setFont(MediumNumbers);
  Tin_str.toCharArray(chr_Tin, 5); //количество знаков+1
  myNokia.print(String(chr_Tin), CENTER, 0);

  //Tout
  char chr_Tout [5];
  myNokia.setFont(SmallFont);
  myNokia.print("            C", LEFT, 16);
  myNokia.print("Out", LEFT, 24);
  myNokia.setFont(MediumNumbers);
  Tout_str.toCharArray(chr_Tout, 5);
  myNokia.print(String(chr_Tout), CENTER, 16);

  // Hin, Hout
  char chr_Hout [5];
  Hout_str.toCharArray(chr_Hout, 4);
  myNokia.setFont(MediumNumbers);
  myNokia.print(String(Hout_str), RIGHT, 32);
  myNokia.setFont(SmallFont);
  myNokia.print("    In Out", LEFT, 40);
  myNokia.print("      %", LEFT, 32);
  myNokia.setFont(MediumNumbers);
  myNokia.print(String(Hin), LEFT, 32);
  myNokia.setFont(SmallFont);

  // Battery Level
  if (BatIn < 2.2) {
    myNokia.setFont(SmallFont);
    myNokia.print("Bat", LEFT, 0);
  }

  if (BatOut_str == "BLow") {
    myNokia.setFont(SmallFont);
    myNokia.print("Bat", LEFT, 16);
  }

  myNokia.disableSleep();
  _delay_ms(5);
}

void drawStart() {
  myNokia.enableSleep();
  myNokia.clrScr();

  //Tin
  char chr_Tin [5];
  String Tin_str = String(Tin);
  myNokia.setFont(SmallFont);
  myNokia.print("            C", LEFT, 0);
  myNokia.print("In", LEFT, 8);
  myNokia.setFont(MediumNumbers);
  Tin_str.toCharArray(chr_Tin, 5); //количество знаков+1
  myNokia.print(String(chr_Tin), CENTER, 0);

  // Battery Level
  if (BatIn < 2.2)
  {
    myNokia.setFont(SmallFont);
    myNokia.print("Bat!", RIGHT, 28);
  }

  //Hin
  myNokia.setFont(SmallFont);
  myNokia.print("         %", LEFT, 18);
  myNokia.print("In", LEFT, 28);
  myNokia.setFont(MediumNumbers);
  myNokia.print(String(Hin), CENTER, 18);

  //No signal!
  myNokia.setFont(SmallFont);
  myNokia.print("Out - - -", CENTER, 40);

  myNokia.update();

  myNokia.disableSleep();
  _delay_ms(5);
}

void setup() {
  Serial.begin(9600);

  pinMode(VccHTU, OUTPUT);
  digitalWrite(VccHTU, 1);
  Serial.println("Power ON!");
  analogReference(DEFAULT);

  // инициализация дисплея
  myNokia.InitLCD();

  myNokia.setFont(SmallFont);
  myNokia.clrScr();
  myNokia.print(">>>>>", CENTER, 20);
  myNokia.update();
  _delay_ms(1000);
  myNokia.setFont(SmallFont);
  myNokia.clrScr();
  myNokia.print("))-->", CENTER, 20);
  myNokia.update();

  if (!LoRa.begin(433E6)) {
    Serial.println("Ошибка загрузки LoRa-приемника!");
    while (1);

    myNokia.setFont(SmallFont);
    myNokia.clrScr();
    myNokia.print(" ->  ->", CENTER, 20);
    myNokia.update();
  }

  // Диапазон для синхрослова – между "0-0xFF".
  LoRa.setSyncWord(0xF3);
  Serial.println("Прослушивание эфира. Ожидание пакета с в.датчика ...");

  myHTU21D.begin();
  Measurement();
  drawStart();
  digitalWrite(VccHTU, 0);
  _delay_ms(1000);

  myNokia.clrScr();
  myNokia.print("Waiting", CENTER, 10);
  myNokia.print("Message from", CENTER, 22);
  myNokia.print("OUTSIDE", CENTER, 34);
  myNokia.update();
}

void loop() {
  r++;
  digitalWrite(VccHTU, 1);
  if (r < 600)  // 8 MHz;
  {
    mlc = 0;
    // Прослушивание эфира, прием, дешифрация, если сигнал с в.датчика принят,
    // то измерения в помещении, вывод инфы на экран и - в спячку.
    {
      int packetSize = LoRa.parsePacket();
      if (packetSize) {
        while (LoRa.available()) {
          LoRaData = LoRa.readString();
        }
        int pos1 = LoRaData.indexOf('#');
        int pos2 = LoRaData.indexOf('$');
        Tout_str = LoRaData.substring(0, pos1);
        Hout_str = LoRaData.substring(pos1 + 1, pos2);
        BatOut_str = LoRaData.substring(pos2 + 1, LoRaData.length());

        if ((LoRaData).substring(pos1, pos1 + 1) == "#") {
          Serial.println("Принято, декодировано! r = " +  String(r));
          r = 0;
          Measurement();
          draw();
          digitalWrite(VccHTU, 0);
          // sleepCounter = 49; 16 MHz
          // sleepCounter = 48; 8 MHz
          for (sleepCounter = 48; sleepCounter > 0; sleepCounter--)
          {
            digitalWrite(VccHTU, 1);
            LoRa.sleep ();
            LowPower.powerDown(SLEEP_1S, ADC_OFF, BOD_OFF);
          }
        }
      }
    }
  } else {
    r = 600;
    if (mlc < 250) //4 часа, время работы без датчика
    {
      Serial.println("Работа без в.датчика.");
      LoRa.sleep ();
      Measurement();
      drawStart();
      digitalWrite(VccHTU, 0);

      for (sleepCounter = 6; sleepCounter > 0; sleepCounter--)
      {
        digitalWrite(VccHTU, 1);
        LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF);
      }
      mlc++;
    } else {
      r = 0;
      mlc = 0;
    }
  }
  _delay_ms(110); 
}

int main() {
  init();
  setup();

  for (;;) {
    loop();
  }
}

Работа анализатора начинается в setup'e с инициализации модулей, измерения параметров воздуха, анализа напряжения на батарейках и вывода этой инфы на дисплей. Далее уже в loop'e прослушивается эфир приемником LoRa. После приема и дешифрации сигнала с в.датчика повторно проводятся измерения, анализа напряжения на батарейках и вывод измеренной и принятой инфы на дисплей. Выполнив эту работу все элементы схемы уходят поспать примерно на полсекунды меньше, чем период отправки пакетов с в.датчика. В следующем цикле контроллер просыпается и включает приемник приблизительно за 0,5 сек до ожидаемого прихода сигнала с в.датчика. Таким образом, контроллер и периферия анализатора работают около полсекунды с периодом (циклом) меньше минуты (53,5 сек). Если радиосигнал с в.датчика не поступает на приемник анализатора на протяжении приблизительно одной минуты (время, достаточное для приема одного из 6 пакетов), то анализатор переходит в режим работы без в.датчика на 4 часа, измеряя параметры воздуха и оценивая состояние батареек только в помещении с индикацией на дисплее этих данных. Период обновления данных в режиме работы без в. датчика — 56,7 сек. В конце четырехчасового цикла работы анализатора без в.датчика он прослушивает эфир: а вдруг в.датчик снова в эфире?




Для перевода модуля HTU21D в режим низкого энергопотребления во время сна его питание также, как и в в.датчике, организовано с контроллера ATMEGA328P (пин 14).


В целом, на дисплее анализатора видна такая картинка:



Дисплей из-за низкого разрешения и малого размера экрана плотно забит символами. Эта картинка смотрелась бы намного лучше на современном дисплее с электронными чернилами. В будущем в своих проектах буду использовать e-paper дисплей.


Ресурс батареек и другое


Для расчета срока работы батареек понадобится время и потребляемый ток во время выполнения работы (операционное время) и сна. Операционное время и рабочий ток измерялись с использованием тестовых скетчей, идея которых взята отсюда. Архив с тестовыми скетчами и данными хронометража лежит здесь.


Рабочий ток измерялся с использованием тех же тестовых скетчей. Для исключения разрывов цепи питания или значительного увеличения величины выходного сопротивления батареек можно использовать шунт 3,9...5,6 Ом и параллельно подключенный к нему цифровой мультиметр с механическим переключением в режиме вольтметра на диапазоне 2000 мкВ. Это критично при измерении потребления тока сна анализатора, поскольку разрыв питания или значительное ограничение тока приводят к циклическому ресету анализатора. Да и выносной датчик может переходить в постоянный рестарт. По мере возможности необходимо проверять ток потребления разными способами на разных диапазонах шкал прибора и с батарейками, которые планируется использовать, притом, обязательно без вывода результатов на монитор порта Ардуино. Невыполнение этих правил сказались на результатах измерений тока в предыдущем моем посте на тему метеостанции — в одних случаях они занижены, в других — завышены.


Результаты измерений сведены в таблицу:


  в.датчик анализатор
Операционное время функции измерений параметров воздуха, состояния батареек 0,25 сек 0,39 сек
Операционный ток функции измерений параметров воздуха, состояния батареек 3,4 мА 3,5 мА
Операционное время функции передачи/приема сигнала 42 мсек 83 мсек
Операционный ток функции передачи/приема сигнала 30,0 мА

(4 дБ)


11,5 мА
Ток сна 10 мкА 190 мкА

Что бросается в глаза, глядя на эту таблицу. Операционный ток передачи сигнала — 30,0 мА при мощности передатчика LoRa 4 дБ. Для сравнения, ток передачи для модуля nRF24L01 13,5 мА. Вывод очевиден: надо переходить на nRF24L01.


Средний ток потребления по данным таблицы в. датчика 0,13 мА. Емкости батареек типа АА GP Litium для выносного датчика должно хватить на 2,5 года.


Средний ток потребления анализатора 0,27 мА. Ресурс батареек АА GP Litium в анализаторе — 1,2 года. Для беспроводного комнатного термостата Computherm Q7RF, например, срок действия батареек:  около 1 года.


Еще на тему энергопотребления — долго копался в этой теме и хочется выговориться.


Составил код на С в Atmel Studio и эмулировал его в Proteus'е для для барометра-термометра.



На картинке ниже показаны результаты сравнения кода для одного и того же устройства на языке С и в среде разработки Arduino IDE.



Объем флеш-памяти, занимаемой в коде в Ардуино — 12968 байт, на С — 5954 байта и оценочно на Ассемблере при таком относительно большом проекте, как у меня, объем будет такой же, как и на С. Притом, в маленьких проектах (например, помигать светодиодом) выигрыш на С будет около 6 раз по сравнению с объемом флеш-памяти в Ардуино. Ассемблер уменьшит этот размер еще процентов на 10 или около этого.


Из этих чисел сделал простой вывод, в котором убедился на собственном опыте:
Благодаря использованию компилируемых в Arduino IDE библиотек и функций на С/С++ в некоторых скетчах этого поста удалось уйти от предупреждения: Недостаточно памяти, программа может работать нестабильно.


И, наконец, искренне благодарю AlexanderS, который донес до меня идею виртуальной шкалы времени или синхронизации, а также других участников обсуждения статьи «Автономная метеостанция на контроллере ATMEGA328P и питанием от батареек с беспроводным выносным датчиком» за предложения, конструктивную критику и замечания.


Спасибо, кто дочитал. Всем — отличного иммунитета во времена коронавируса и не только.


Ссылки по теме


Узел беспроводного датчика с низким энергопотреблением


Беспроводная мини погодная станция с e-paper экраном на батарейках


Автономная метеостанция на контроллере ATMEGA328P и питанием от батареек с беспроводным выносным датчиком


Превращаем Arduino в полноценный AVRISP программатор


LoRa и сон


Узнайте о битах конфигурации ATmega328P и о том, как использовать их с внешним кварцевым резонатором


Калькулятор фьюзов AVR


Почему многие не любят Arduino




Комментарии (44):

  1. VelocidadAbsurda
    /#22757076 / +1

    Коль уж с Arduino уходим на C/asm, возникает резонный вопрос: может уже "гулять на все" и с самой atmega уйти на что-нибудь специально созданное для низкого потребления? STM8L или STM32L например.

    • Cadil_TM
      /#22757718

      Коль уж с Arduino уходим на C/asm, возникает резонный вопрос: может уже «гулять на все» и с самой atmega уйти на что-нибудь специально созданное для низкого потребления? STM8L или STM32L например.

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

      • SignallerK
        /#22761156

        А причем тут асм. STM32 также програмируются на Си как и большинство мк в мире. Более того ARM Asm куда более сложен чем AVR, и имхо лучше без сильной необходимости в него не лезть.
        Также в сети полно страшилок про то, что stm32 мегасложные.
        Но это уже пару лет как не так, благодаря кодогенератору CubeMX все настройки можно за 10 минут выставить и контроллер заведётся. А потом к этому всему обращаться через HAL функции (почти как в ардуине).
        Ну и на крайняк stm32 blue pill eсть в ардуине.
        P. S. HAL при всей моще имеет тот же минус что и ардуина (хоть и гибче) — много универсального кода == много больше места. С другой стороны при 100-200К памяти нет так и страшно.
        P. P. S. Тема использования Cuba и HAL холиварная, но если вы с ардуины на Си съехали то разберётесь.

    • slog2
      /#22761278

      Atmega вполне себе низкопотребляющий, если правильно пользоваться. Если хочется выжать максимум то atmel picopower. Но в данном случае бессмысленно экономить пикоамперы, контроллер тут далеко не на первом месте в потреблении энергии.

    • devprodest
      /#22761866 / +2

      Сударь, на счёт потребления атмеги и СТМ вы не правы, СТМ потребляет куда больше, но правда и вычислительные возможности лучше. Все зависит от применения и прямоты рук программиста и схемотехника)

  2. sav13
    /#22757384

    Экран E-ink 2.13 + Контроллер NRF52 < 2мкА потребления в режиме сна
    Либо взять E-ink + Zigbee как на Xiaomi термометрах

    • AlexanderS
      /#22757480

      С Zigbee придется же ещё полноценный шлюз организовывать? Это всё проект сильно удорожает.

      • sav13
        /#22757642

        Ну да. Как и с Mysensors
        Готовый USB брелок или модуль к малинке стоит порядка 2-х тысяч
        Можно взять SOC на алишке за 500р и спаять самому
        Зато сяоминые, акваровские и др. устройства работаю готовые

        • AlexanderS
          /#22757976

          С брелком-то ещё хуже всё будет — он же просто функцию координатора играет. Это надо будет разворачивать какую-то систему автоматизации и его туда интегрировать. У автора, кажется, задачи сильно проще)

    • Cadil_TM
      /#22757812

      Экран E-ink 2.13 + Контроллер NRF52 < 2мкА потребления в режиме сна
      Либо взять E-ink + Zigbee как на Xiaomi термометрах

      Дисплей 2.13 будет маловат для комнатного термостата и метеостанции в одном. В списке желаний на Али уже несколько месяцев e-paper дисплей 3,7 дюйма.
      NRF52832 уже в пути.

  3. ittakir
    /#22757512

    Снизить энергопотребление в режиме сна можно, если питание внешних модулей коммутировать p-канальным полевиком или даже просто запитать их от одного или нескольких пинов GPIO микроконтроллера. Максимальный ток 20мА на пин.

    VelocidadAbsurda

    Коль уж с Arduino уходим на C/asm, возникает резонный вопрос: может уже «гулять на все» и с самой atmega уйти на что-нибудь специально созданное для низкого потребления? STM8L или STM32L например.

    В ATMega328PA нет ничего плохого. Это уже pico-power контроллер. У неё достаточно возможностей отключать периферию и уходить в глубокий сон с потреблением около 1мкА.
    В данном случае, все равно большую часть потребления уходит на прием и передачу данных. Если датчик находится недалеко, метров 30 до базы, то я бы поставил лучше NRF24L01. У него низкое потребление и высокая скорость передачи. А это значит, что в активном состоянии его нужно держать очень короткое время.

    Объем флеш-памяти, занимаемой в коде в Ардуино — 12968 байт, на С — 5954 байта и оценочно на Ассемблере не больше 200 байт.

    Думаю, что все дело тут в функции println, которая притягивает библиотечную функцию printf. А в ней, в зависимости от настроек, может быть либо только самая базовая функциональность, либо дополнительные параметры форматирования, вещественные числа и т.п.
    В любом случае, большой объем занятой флеш памяти не обязательно означает, что она вся будет исполняться. Чаще всего это просто мертвый груз.
    Также переписывание с С на ассемблер не имеет почти никакого смысла. Зато имеет смысл порой избавиться от стандартной функции printf() и написать свою, если вы выводите, например, только целые числа.

    • Cadil_TM
      /#22758318

      Снизить энергопотребление в режиме сна можно, если питание внешних модулей коммутировать p-канальным полевиком или даже просто запитать их от одного или нескольких пинов GPIO микроконтроллера. Максимальный ток 20мА на пин.

      Питание периферии с коммутацией транзистором или от пинов контроллера у меня практически во всех проектах.
      В nRF24L01+ требуется повторная инициализация контроллера радиомодуля. Инициализация несовместима с командами библиотеки LowPower.h, которые задают у меня время сна.
      Короче, чтобы восстановить настройки радиомодуля после сна надо сделать Reset контроллера, который ним управляет. У меня — это atmega328p. При ресетах с интервалом минута ресурса флеш-памяти хватит на несколько суток.
      Возможно я заблуждаюсь, еще поиграюсь с nRF24L01+ уж очень низкое у него энергопотребление.
      Также переписывание с С на ассемблер не имеет почти никакого смысла. Зато имеет смысл порой избавиться от стандартной функции printf() и написать свою, если вы выводите, например, только целые числа

      Перед отправкой данных с десятыми-сотыми умножаю на 100, а дальше — дело техники.

      • V1RuS
        /#22760766

        При ресетах с интервалом минута ресурса флеш-памяти хватит на несколько суток.

        При чем ресурс флеш-памяти к ресету? Ее ресурс "расходуется" только при перепрошивке. Или у вас бутлоадер при каждом сбросе перепрошивает контроллер?

  4. Javian
    /#22757588

    Для максимальной экономии лучше отключать потребителей наподобие этого примера:
    image
    habr.com/ru/post/386735

  5. Polaris99
    /#22757594

    А почему нет кода на ассемблер, откуда эти "оценочные" оценки? Очень интересно посмотреть, как Вы уместили в 200 байт шрифты для экранчика и преобразование данных с датчиков.

    • Cadil_TM
      /#22758456

      А почему нет кода на ассемблер, откуда эти «оценочные» оценки? Очень интересно посмотреть, как Вы уместили в 200 байт шрифты для экранчика и преобразование данных с датчиков.

      Работал с кодом термометра-гидрометра на ATtiny13. Получил объем занимаемой флеш-памяти 40 байт. Навскидку увеличил это число в 5 раз. Подредактирую эту цифру, если будут аргументы.
      Вот код:
      термометр-гидрометр на Ассемблере
      // *********************************************
      // *** Simple digital thermometer/hygrometer ***
      // *********************************************
      // *** (c) SD, 14.03.2016 ***
      // *********************************************

      // Based on ATtiny13, AM2303 and MAX7219

      // **************
      // *** Clocks ***
      // **************

      // MCU clock frequency is 9.6MHz (internal oscillator)
      // Timer frequency is 75KHz = 9.6MHz/128
      // (13.3 us between interrupts)

      #define SKIPNEXT1W (PC + 2)
      #define DS(var) Y + var - _dataStart

      // ************
      // *** Pins ***
      // ************

      // MAX7219 output pins
      .equ MAX_DIN = 0
      .equ MAX_CS = 1
      .equ MAX_CLK = 4

      // AM2302 input pin
      .equ AM2302_PIN = 3

      // MAX7219 registers
      .equ MAX_DECODE = 0x09
      .equ MAX_INTENSITY = 0x0A
      .equ MAX_SCANLIMIT = 0x0B
      .equ MAX_SHUTDOWN = 0x0C
      .equ MAX_DISPTEST = 0x0F

      // Temperature measurement state register
      // Bits 0 - 2 define the byte number being received
      // Bit 3 is set when there are valid data received
      // Bits 4 - 7 define the current receiver state
      .def R_TS = R0

      // Temperature measurement tick
      .def R_TT = R1

      // Temperature data register
      .def R_TD = R2

      // Temperature measurement states
      .equ TMS_NONE = 0x00 // TMS_NONE - do nothing an wait until
      // somebody changes the state
      .equ TMS_START = 0x10 // Start of the measurement cycle
      .equ TMS_ST_LOW = 0x20 // Initial low signal is being sent
      // (1 ms = 75 timer ticks)
      .equ TMS_WRSP_LOW = 0x30 // Initial low signal has been sent,
      // waiting for the response low signal
      .equ TMS_WRSP_HIGH = 0x40 // Response low signal has been received,
      // waiting for the response high signal
      .equ TMS_W1ST_BIT_LOW = 0x50 // Waiting for the first bit low signal
      .equ TMS_WBIT_HIGH = 0x60 // Waiting for the bit high signal
      .equ TMS_WBIT_LOW = 0x70 // Waiting for the bit low signal
      .equ TMS_WHIGH = 0x80 // Waiting for the final high signal

      // Timer 100Hz tick counter
      // (counts upwards from 0 to 255)
      .def R_TICK100 = R3

      // Timer 16bit 75KHz tick counter
      // (counts downwords from 749 to 0)
      .def R_TICKL = R4
      .def R_TICKH = R5

      // ************
      // *** Data ***
      // ************

      .dseg
      _dataStart: // Data start label

      tempData: .byte 5 // Data, received from the AM2302 sensor
      displayData: .byte 4 // Decimal printing result

      .equ DATA_BUF_SIZE = 8 // AM2302 data buffer size in samples
      // (each sample is 4 bytes)

      dataBuffer: .byte DATA_BUF_SIZE*4

      .cseg
      .org 0

      // *** Interrupts ***

      // Reset Handler
      rjmp start

      // IRQ0 Handler
      reti

      // PCINT0 Handler
      reti

      // Timer0 Overflow Handler
      rjmp timerOvfl

      // EEPROM Ready Handler
      reti

      // Analog Comparator Handler
      reti

      // Timer0 CompareA Handler
      rjmp timerCompA

      // Timer0 CompareB Handler
      reti

      // Watchdog Interrupt Handler
      reti

      // ADC Conversion Handler
      reti

      // Table to convert decimal digit into 7-segment code
      hexTable:
      .db 0b01111110, 0b00110000, 0b01101101, 0b01111001
      .db 0b00110011, 0b01011011, 0b01011111, 0b01110010
      .db 0b01111111, 0b01111011

      start:
      cli
      ldi R16, RAMEND
      out (SPL), R16

      // Init watchdog (4s interval)
      wdr
      ldi R16, (1 << WDCE) | (1 << WDE)
      out (WDTCR), R16
      ldi R16, (1 << WDE) | (1 << WDP3)
      out (WDTCR), R16

      // Init registers
      ldi YL, low (_dataStart)
      ldi YH, high (_dataStart)
      clr R_TS
      clr R_TT
      clr R_TICKL
      clr R_TICKH
      clr R_TICK100

      // Init ports
      out (PORTB), R_TS
      ldi R16, (1 << MAX_DIN) | (1 << MAX_CS) | (1 << MAX_CLK)
      out (DDRB), R16

      // Init LED driver
      // Set all digits to "-"
      ldi XL, 0b00000001
      ldi XH, 1
      init1:
      rcall maxWriteWord
      cpi XH, 9
      brne init1

      // Set control registers
      ldi XL, 0 // Decode
      rcall maxWriteWord
      ldi XL, 4 // Intensity
      rcall maxWriteWord
      ldi XL, 7 // Scan limit
      rcall maxWriteWord
      ldi XL, 1 // Shutdown
      rcall maxWriteWord
      ldi XH, 0x0F
      ldi XL, 0 // Display test
      rcall maxWriteWord

      // Init timer for 1 interrupt each 128 CPU cycles
      ldi R16, 127
      out (OCR0A), R16
      ldi R16, 0b00000110
      out (TIMSK0), R16
      ldi R16, 0b00000001
      out (TCCR0B), R16

      // First part of the initialization is done.
      // Enable interrupts
      sei

      // Wait 2 sec (while AM2302 initialize itself)
      // with little animation
      ldi XH, 1
      ldi XL, 0
      init2:
      ldi R16, 25
      rcall wait100Hz
      rcall maxWriteWord
      cpi XH, 9
      brne init2

      // R6 will contain the number of
      // measurement values received
      clr R6

      // R7 will contain the number of
      // continious errors
      clr R7

      loop:
      // Reset watchdog timer
      wdr

      // Initiate measurement
      ldi R16, TMS_START
      mov R_TS, R16

      loop1:
      // Wait for the TMS_NONE state
      // which indicates that the measurement
      // is done
      sleep

      mov R16, R_TS
      andi R16, 0xF0
      brne loop1

      // Do we have the valid data?
      sbrs R_TS, 3
      loop_error1:
      rjmp loop_error

      // Check control sum of the received data
      ldd R16, DS (tempData)
      ldd ZL, DS (tempData + 1)
      add R16, ZL
      ldd ZL, DS (tempData + 2)
      add R16, ZL
      ldd ZL, DS (tempData + 3)
      add R16, ZL
      ldd ZL, DS (tempData + 4)
      cp R16, ZL
      brne loop_error1

      // We have valid new measurement data,
      // reset error count
      clr R7

      // Move up data in the buffer
      // and count the sum at the same time.
      // R12:R13 will contain the humidity value and
      // R14:R15 the temperature value
      clr R12
      clr R13
      clr R14
      clr R15
      ldi ZL, low (dataBuffer + (DATA_BUF_SIZE - 2)*4)
      ldi ZH, 0
      buf1:
      ldd R16, Z + 0
      ldd R17, Z + 1
      std Z + 4, R16
      std Z + 5, R17
      add R12, R16
      adc R13, R17

      ldd R16, Z + 2
      ldd R17, Z + 3
      std Z + 6, R16
      std Z + 7, R17
      add R14, R16
      adc R15, R17

      subi ZL, 4
      cpi ZL, low (dataBuffer - 4)
      brne buf1

      // Add new humidity value to the buffer
      // and to the sum
      ldd R16, DS (tempData + 1)
      ldd R17, DS (tempData)
      std DS (dataBuffer + 0), R16
      std DS (dataBuffer + 1), R17
      add R12, R16
      adc R13, R17

      // Add new temperature value to the buffer
      // and to the sum
      ldd R16, DS (tempData + 3)
      ldd R17, DS (tempData + 2)

      // Check for a negative value
      and R17, R17
      brpl buf2

      // Convert negative temperature to the 2's
      // complement form
      clr ZL
      andi R17, 0x7F
      neg R16
      sbc ZL, R17
      mov R17, ZL

      buf2:
      std DS (dataBuffer + 2), R16
      std DS (dataBuffer + 3), R17
      add R14, R16
      adc R15, R17

      // Divide the humidity and temperature
      // sum values by 8 (by shifting them right
      // three times)
      ldi R16, 3
      buf3:
      asr R15
      ror R14
      asr R13
      ror R12
      dec R16
      brne buf3

      // Do we have 8 full measurements?
      mov R16, R6
      cpi R16, 7

      // If so, use the average values from
      // the buffer
      breq buf4

      // Otherwise use the latest measurement
      ldd R12, DS (dataBuffer + 0)
      ldd R13, DS (dataBuffer + 1)
      ldd R14, DS (dataBuffer + 2)
      ldd R15, DS (dataBuffer + 3)
      inc R6

      buf4:
      // Print out values

      // *** Humidity ***
      movw X, R12
      rcall printDecX

      ldi XH, 1
      ldd XL, DS (displayData + 3)
      rcall maxWriteWord

      ldd XL, DS (displayData + 2)
      ori XL, 0x80
      rcall maxWriteWord

      ldd XL, DS (displayData + 1)
      rcall maxWriteWord

      ldd XL, DS (displayData)
      rcall maxWriteWord

      // *** Temperature ***
      movw X, R14

      // Check for a negative value
      and XH, XH
      brpl buf5

      // Calculate the absolute value
      clr ZL
      neg XL
      sbc ZL, XH
      mov XH, ZL

      buf5:
      rcall printDecX

      ldi XH, 5
      ldd XL, DS (displayData + 3)
      rcall maxWriteWord

      ldd XL, DS (displayData + 2)
      ori XL, 0x80
      rcall maxWriteWord

      ldd XL, DS (displayData + 1)
      rcall maxWriteWord

      // If temperature is negative
      // write the minus sign to the first digit
      // (temperatures of -100.0 and below
      // are not supported anyway)
      ldd XL, DS (displayData)
      and R15, R15
      brpl SKIPNEXT1W
      ldi XL, 1
      rcall maxWriteWord

      loop2:
      // Wait for 1 sec
      ldi R16, 100
      rcall wait100Hz

      // And repeat
      rjmp loop

      loop_error:
      // An error had occured.
      // Increment error count
      inc R7

      // Do we have 3 or more errors in a row?
      mov R16, R7
      cpi R16, 3

      // No? Just do nothing
      brne loop2

      // Prevent error count from growing
      dec R7

      // Display error
      ldi ZL, low (errText*2)
      ldi ZH, high (errText*2)
      rcall maxWrite8Bytes
      rjmp loop2

      errText:
      // "Sn Error"
      .db 0b00000101, 0b00011101, 0b00000101, 0b00000101
      .db 0b01001111, 0b00000000, 0b00010101, 0b01011011

      // **********
      // Waits given number (R16) of 100Hz ticks
      // Uses: Z
      wait100Hz:
      // Enable sleep
      ldi ZL, 0b00100000
      out (MCUCR), ZL

      mov ZL, R_TICK100
      w100:
      sleep
      mov ZH, R_TICK100
      sub ZH, ZL
      cp ZH, R16
      brcs w100
      ret

      // Timer interrupt

      timerOvfl:
      timerCompA:
      push R16
      in R16, (SREG)
      push R16
      push ZL
      push ZH

      // Receive AM2303 data
      rcall am2302proc

      // Decrement current 75KHz tick
      ldi R16, 1
      sub R_TICKL, R16
      brcc timerRet
      sub R_TICKH, R16
      brcc timerRet

      // Initialize 75KHz tick value
      ldi ZL, low (750 - 1)
      ldi ZH, high (750 - 1)
      movw R_TICKL, Z

      // Increment current 100Hz tick
      inc R_TICK100

      timerRet:
      pop ZH
      pop ZL
      pop R16
      out (SREG), R16
      pop R16
      reti

      // **************
      // *** AM2302 ***
      // **************

      amStart:
      // Send the start low signal.
      // Switch corresponding PORTB pin to output
      // (there is already 0 in the PORTB register)
      sbi (DDRB), AM2302_PIN
      ldi R16, TMS_ST_LOW
      rjmp amSetState

      amStartLow:
      // Initial start low signal is being sent.
      // Wait for 75 ticks
      cpi R16, 75
      brne amNone

      // Switch PORTB pin back to input
      cbi (DDRB), AM2302_PIN
      ldi R16, TMS_WRSP_LOW

      // Do not check AM2303 input pin at this tick
      // since it's possible that it has not recovered
      // from the low state yet.
      rjmp amSetState

      amWRespLow:
      // Waiting for the response low signal
      sbrc ZH, AM2302_PIN
      ret

      ldi R16, TMS_WRSP_HIGH
      rjmp amSetState

      amWRespHigh:
      // Waiting for the response high signal
      sbrs ZH, AM2302_PIN
      ret

      ldi R16, TMS_W1ST_BIT_LOW
      rjmp amSetState

      amW1StBitLow:
      // Waiting for the first bit low signal
      sbrc ZH, AM2302_PIN
      ret

      // Get ready to receive the first bit
      ldi R16, 1
      mov R_TD, R16

      // Set new state and reset the byte counter
      ldi ZL, TMS_WBIT_HIGH
      rjmp amSetState2

      amBitHigh:
      sbrs ZH, AM2302_PIN
      ret

      // If the bit low signal was there too long
      // (longer than 5 ticks (5*13.3 = 66.5us)
      // something went wrong)
      cpi R16, 6
      brcc amResetState

      ldi R16, TMS_WBIT_LOW
      rjmp amSetState

      am2302proc:
      // First, check for the TMS_NONE state.
      // In this case just do nothing to
      // not waste MCU cycles.
      mov ZL, R_TS
      andi ZL, 0xF0

      cpi ZL, TMS_NONE
      breq amNone

      // Increment receiver tick
      inc R_TT

      // If we are waiting for too long,
      // something went wrong, reset the state
      breq amResetState

      // Save the current tick into a more
      // convenient register
      mov R16, R_TT

      // Get input signal
      in ZH, (PINB)

      // Branch depending on the current state.
      // Check for TMS_WBIT_LOW first since it
      // has the longest service routine
      cpi ZL, TMS_WBIT_LOW
      breq amBitLow

      cpi ZL, TMS_START
      breq amStart

      cpi ZL, TMS_ST_LOW
      breq amStartLow

      cpi ZL, TMS_WRSP_LOW
      breq amWRespLow

      cpi ZL, TMS_WRSP_HIGH
      breq amWRespHigh

      cpi ZL, TMS_W1ST_BIT_LOW
      breq amW1StBitLow

      cpi ZL, TMS_WBIT_HIGH
      breq amBitHigh

      cpi ZL, TMS_WHIGH
      breq amWHigh

      amResetState:
      // In case of an error, reset state to
      // the default TMS_NONE
      ldi R16, TMS_NONE

      amSetState:
      // Preserve the current byte number
      mov ZL, R_TS
      andi ZL, 0x07
      or ZL, R16

      amSetState2:
      mov R_TS, ZL

      // Clear receiver tick counter
      clr R_TT

      amNone:
      ret

      amBitLow:
      sbrc ZH, AM2302_PIN
      ret

      // The high bit signal was too long?
      cpi R16, 8
      brcc amResetState

      // Store input bit (inverted, since cpi produces
      // inverted result in the carry flag)
      cpi R16, 4
      rol R_TD

      // Initally we set R_TD to 1, so when all 8
      // bits are received, the carry flag will be set
      // indicating that a full byte has been received.
      // Otherwise, receive the next bit
      ldi R16, TMS_WBIT_HIGH
      brcc amSetState

      // We have the full byte. Invert it
      com R_TD

      // Save it
      mov ZL, R_TS
      andi ZL, 0x07
      subi ZL, low (-tempData)
      ldi ZH, high (tempData)
      st Z+, R_TD

      // Did we receive all 5 bytes?
      cpi ZL, low (tempData + 5)
      ldi R16, TMS_WHIGH
      breq amSetState

      // OK, receive the next byte.
      // Increment the byte counter
      inc R_TS

      // Initialize R_TD
      ldi R16, 1
      mov R_TD, R16

      ldi R16, TMS_WBIT_HIGH
      rjmp amSetState

      amWHigh:
      sbrs ZH, AM2302_PIN
      ret

      cpi R16, 6
      brcc amResetState

      // We received everything. Set
      // the state to TMS_NONE and set
      // the data validity bit
      ldi R16, 0x08
      mov R_TS, R16
      ret

      // *********

      /*
      // Write data from Z
      // Uses R16 - R19, X, Z
      maxWriteData:
      lpm XH, Z+
      tst XH
      brne SKIPNEXT1W
      ret
      lpm XL, Z+
      rcall maxWriteWord
      rjmp maxWriteData

      maxInit:
      .db MAX_DECODE, 0
      .db MAX_INTENSITY, 4
      .db MAX_SCANLIMIT, 7
      .db MAX_SHUTDOWN, 1
      .db MAX_DISPTEST, 0
      .db 0, 0

      maxTest:
      .db 0, 0b00011101, 0b00010101, 0b00010000, 0b00011100, 0b00111101, 0b00000101, 0b01110111
      */

      // Writes 8 bytes from (Z) (program memory)
      // to MAX7219
      // Uses R16 - R19, X, Z
      maxWrite8Bytes:
      ldi XH, 0x01

      mw8b1:
      lpm XL, Z+
      rcall maxWriteWord
      cpi XH, 9
      brne mw8b1
      ret

      // Write word X (XL = data, XH = address) to MAX2719
      // Uses R16 - R19, X
      maxWriteWord:
      // Set all pins to zero
      in R17, (PORTB)
      andi R17, ~((1 << MAX_DIN) | (1 << MAX_CS) | (1 << MAX_CLK))
      out (PORTB), R17

      ldi R19, (1 << MAX_CLK)

      mov R16, XH
      rcall mww1

      mov R16, XL
      rcall mww1

      // Set LOAD(CS) to high thus writing all 16 bits into
      // MAX register
      sbi (PORTB), MAX_CS

      // Increment MAX register number
      inc XH
      ret

      mww1:
      ldi R18, 8

      mww2:
      bst R16, 7
      bld R17, MAX_DIN
      out (PORTB), R17

      lsl R16
      dec R18

      // Create clock impulse by toggling clock output twice
      out (PINB), R19
      out (PINB), R19

      brne mww2
      ret

      // *********

      printDecX:
      ldi ZH, low (1000)
      ldi R16, high (1000)
      rcall pdx

      // Change zero digit to empty space
      cpi ZL, 0b01111110
      brne SKIPNEXT1W
      ldi ZL, 0
      std DS (displayData), ZL

      ldi ZH, 100
      ldi R16, 0
      rcall pdx

      // If this digit is zero and the first
      // digit is empty (i.e. it was zero too)
      // change this digit to empty space
      ldi R16, 0b01111110
      eor R16, ZL
      ldd ZH, DS (displayData)
      or R16, ZH
      brne SKIPNEXT1W
      ldi ZL, 0
      std DS (displayData + 1), ZL

      ldi ZH, 10
      ldi R16, 0
      rcall pdx
      std DS (displayData + 2), ZL

      mov ZL, XL
      rcall pdx3
      std DS (displayData + 3), ZL

      // Clear carry flag to indicate that
      // no error occurred
      clc
      ret

      pdx:
      ldi ZL, 0
      pdx1:
      sub XL, ZH
      sbc XH, R16
      brcs pdx2

      cpi ZL, 9
      breq pdxOverflow
      inc ZL
      rjmp pdx1

      pdx2:
      add XL, ZH
      adc XH, R16

      pdx3:
      subi ZL, -low (hexTable << 1)
      ldi ZH, high (hexTable << 1)
      lpm ZL, Z
      ret

      pdxOverflow:
      // Set carry flag to indicate error
      sec

      // Pop return address out of the stack
      // so we can return to the caller of printDecX
      pop R16
      pop R16
      ret

      • dmitryrf
        /#22758634

        Аргументы:
        — Датчик другой
        — Экран другой
        Приведённый код не имеет смысла применительно к описанному проекту.

      • Polaris99
        /#22759242

        Вы свой код собрать пробовали?

        ATtiny13 memory use summary [bytes]:
        Segment Begin End Code Data Used Size Use%
        ---------------------------------------------------------------
        [.cseg] 0x000000 0x0002f4 738 18 756 1024 73.8%
        [.dseg] 0x000060 0x000089 0 41 41 64 64.1%
        [.eseg] 0x000000 0x000000 0 0 0 64 0.0%

        Assembly complete, 0 errors. 0 warnings


        Видимо, разницы между 40 байтами и 738 байтами совсем нет. И это мы еще никак знакогенератор на несколько шрифтов для экранчика не трогали.
        На уровне таких «оценок» и фраз

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

        • Cadil_TM
          /#22760056

          Ну, если Вы не понимаете как размер кода влияет на энергопотребление, тогда о чем говорить. Посмотрите, пожалуйста, техописание комнатного термостата стр 11 — это оттуда: Напряжение питания 2 x 1.5V алкальных батарейки (LR6 AA), Потребляемая мощность 1.3 mW, Срок действия батареек ~ 1 год.
          Вряд ли прибор прошит в коде Ардуино или у Вас есть сомнения? )

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

          тестовый скетч
          /*скетч для измерения операционного времени приемником LoRa и макc. операц. тока
          операционное время 0,083сек (83 мсек)
          операционный ток 11.5 mA (Vbat=3V, 8 MHz)
          */

          #include <avr/io.h>
          #include <util/delay.h>

          #include <SPI.h>
          #include <LoRa.h>
          #include <LowPower.h>
          #include "HTU21D.h"
          #include <LCD5110_Graph.h>

          String LoRaData;

          void setup() {
          Serial.begin(38400);
          Serial.println("Power ON!");

          int counter = 0;
          while (!LoRa.begin(433E6) && counter < 10) {
          Serial.println("Не удалось найти LoRa-передатчик!");
          counter++;
          _delay_ms(500);
          }

          // Диапазон для синхрослова – между "0-0xFF".
          LoRa.setSyncWord(0xF3);
          Serial.println("Прослушивание эфира. Ожидание пакета с в.датчика ...");

          _delay_ms(2000);
          }

          void loop() {
          int packetSize = LoRa.parsePacket();
          Serial.println("Прослушивание эфира.");
          if (packetSize) {
          while (LoRa.available()) {
          LoRaData = LoRa.readString();
          }
          int pos1 = LoRaData.indexOf('#');
          if ((LoRaData).substring(pos1, pos1 + 1) == "#")
          {
          Serial.println("Принято, декодировано!");
          }
          }
          }

          int main() {
          init();
          setup();

          for (;;) {
          loop();
          }
          }

          • Polaris99
            /#22760100

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

          • Andy_Big
            /#22773918

            если Вы не понимаете как размер кода влияет на энергопотребление

            Если и влияет, то очень косвенно. Практически — никак, ни на пиковое, ни на среднее.

            • Cadil_TM
              /#22774186

              Готов поспорить: размер кода > количество операций > энергопотребление. Ведь очевидно, что на две операции затраты меньше, чем, допустим, на 10 таких же. Что легче: вытащить 2 ведра воды из колодца или 10?

              • Andy_Big
                /#22774250

                2 операции, выполняющиеся непрерывно в цикле, или 100 операций, выполняющиеся однократно раз в 10 секунд. По Вашей же аналогии — вытащить 2 ведра из колодца одним махом или по 1 литру в день за 20 дней.
                Если же говорить о более продвинутых контроллерах с настраиваемой частотой, DMA и т.д., то примеров можно привести множество.

                • Cadil_TM
                  /#22774354

                  Читаем еще раз :

                  … Ведь очевидно, что на две операции затраты меньше, чем, допустим, на 10 таких же.

                  И неважно, как они выполняются — одним махом или не одним.

                  • Andy_Big
                    /#22775010

                    Вы не признаете режимы энергосбережения микроконтроллеров? :) Ведь совершенно неочевидно, что код с меньшим объемом будет более энергоэффективным. Можете проверить сами — напишите прошивку, в которой функция main() будет содержать только строку:
                    while (1);
                    и сравните объем кода и энергопотребление этой прошивки и Вашей прошивки метеостанции из статьи. А если хотите еще большего контраста, то напишите не пустой бесконечный цикл, а бесконечный цикл с отправкой пары байт через радиоканал без всяких powerdown и sleep :)

      • Andy_Big
        /#22773938

        Вот код:

        Как-то совсем не похоже на 40 байт кода. Одна только таблица прерываний и таблица преобразования в 7-сегментное отображение уже тянут на 30 байт. И это только самое начало всей портянки кода.

        • Cadil_TM
          /#22774136 / +1

          Да, вы правы! Дикая описка и выводы соответственно.

  6. d1zz3l
    /#22757850

    Не всегда понимаю чем руководствуются когда ставят тип переменной int если в эту переменную максимум что прилетает или записывается в самом начале в виде константы нечто в пределах 0-255.

    • Cadil_TM
      /#22758488

      Учту на будущее. Как-то не было проблем до сих пор. Спасибо!

  7. av0000
    /#22757940 / +1

    Первый раз слышу про «спячку» BM*280 от дёрганья питания. Мануал вполне однозначен: при сбросе/подаче питания датчик в режиме «sleep» — 0.1uA потребление, дальше — командуем по I2C.
    Ардуиновские библиотеки, ЕМНИП, переключают датчик в «normal» режим и обратно есс-но не отключают. А это — измерения раз в секунду. «Правильный» ход — поправить используемую библиотеку (точнее запись в регистр ctrl_meas) для работы в «forced» режиме или написать свою обёртку. Я писал свою, чтобы читать сразу все данные за один запрос по I2C.

    Javian, Крутовато для поделок :) Использовал похожий приём — супервизор на примерно 2.1В в качестве источника прерываний и полевик на отключение себя от батарейки. Дальше спим без watchdog-ов, пока не сработает IRQ0, на остатках конденсатора подключаем батарейку обратно. Идея долго бродила, но оформилась после чтения комментов сайта hallard.me про Ultra Low Power Node.

    ЗЫ: прошелся по ссылке и нашел себя же во втором комменте. Идём на следующий круг )))

    • V1RuS
      /#22760790

      Ардуиновские библиотеки (конкретно от Adafruit) умеют выставлять режим датчика

  8. shadrap
    /#22759404

    немного не понял зачем Лора? выносной блок на км удалён? хотя Лора интересно, я не рискнул работать на 433- очень много помех, решил делать на 868 на 1276. интересно, что мне не удалось добиться потребления в режиме передачи, как в даташитах, спит, да 2мка или что то на грани чувствительности мультиметра. Что с реальным потреблением у Ра01?

    • slog2
      /#22761446

      А почему бы и не Лора? Ra-01 это SX1278. В sleep там наноамперы какие-то. И с помехоусточивостью всё отлично.

      • Cadil_TM
        /#22761832

        А почему бы и не Лора?

        Поддерживаю. Да и мощности в пределах квартиры достаточно 4 дБ.

  9. slog2
    /#22761312

    Ассемблер даст выигрыш в размере кода ну может процентов 10 по сравнению с Си. Это если проект не большой, сотня-две байт. Если проект больше, то выигрыш в размере кода стремится к нулю, а в больших проектах может и Си скомпилировать меньший код за счёт лучшей оптимизации. В общем не тот это случай чтобы париться с асмом. Не пользуйтесь printf(), float, string и что там ещё у вас многожрущее. И самый главный выигрыш — нафиг ардуину, пишите на обычном си.

    • Cadil_TM
      /#22761672

      Ассемблер даст выигрыш в размере кода ну может процентов 10 по сравнению с Си. Это если проект не большой, сотня-две байт. Если проект больше, то выигрыш в размере кода стремится к нулю, а в больших проектах может и Си скомпилировать меньший код за счёт лучшей оптимизации. В общем не тот это случай чтобы париться с асмом. Не пользуйтесь printf(), float, string и что там ещё у вас многожрущее. И самый главный выигрыш — нафиг ардуину, пишите на обычном си.

      Спасибо! Коротко, внятно и однозначно.

  10. DmitryVS
    /#22762014

    Добавлю немного своего экспириенса. Использую датчики построенные на довольно близкой базе: Mega328P@12MHz, Ra-01, HTU21D, TPS61291, 2xAAA (щелочные «мизинчики»). Связь информационным пакетом в одну сторону на хост, пакет 11 байт, мощность 14дБм, диапазон 433МГц. Дальше режим приёма длительностью 10 символов для получения возможного управляющего пакета от хоста. Везде где можно сон в моде PowerDown. Не нужную периферию отключать. Никаких повторных пересылок я не делаю, наоборот, используется адаптивный интервал, чтобы не флудить избыточными данными. Прошивка объёмом 9412 байт собрана с использованием выпотрошенных и отученных либ от ардуины. Самые старые элементы питания живут уже 15 месяцев, умирать пока не спешат. Больше времени с последнего апгрейда HW+FW просто не прошло. Потерь и повреждений данных я не фиксировал ни разу (отдельные случаи потерь за весь период, конечно, возможны, на общей картине не видно). Радиоканал работает на 25+ метров с несколькими деревянными перекрытиями, стенами, стальной крышей на пути, сигнал LoRa хороший и стабильный. Узкополосный сигнал в диапазоне 2.4ГГц (всевозможные nRF24L01+) ничего подобного и близко не показал. Но да, хост у меня не на батарейках, там режим приёма постоянный.

  11. Cadil_TM
    /#22764748

    В статье добавил ссылку на архив с набором тестовых скетчей для измерений времени выполнения и тока операций.
    Результаты измерений, как и раньше, — в таблице статьи.

  12. zeblong
    /#22764818

    Несколько лет как сделал «умную квартиру». Сейчас уже вторая квартира. Для климатических датчиков использую связку Arduino Mini Pro 8Мгц nRF24L01+ ds18b20 am2320 и все это на аккумуляторе 16340. Библиотеку для связи использую mysensors. Никаких хитростей типа отключения питания датчикам или радиомодулю не использую. Так же не хочу заменять на что то ds18b20 по тому что ему верю больше всех (проводил испытания разных). Хотя часто читаю что он якобы не пригоден для батарейного питания. Код примитивен. Передаю данные раз в 5 минут. При этом датчики живут полгода — год на одном аккумуляторе легко. Все кроме уличного. Уличный кроме того что морозится так еще находится дальше всех от базы. С ним постоянные танцы. Если уличный питать батарейками — то мороз по ним бьет прямо сразу. Я к чему это все — достаочно поставить один аккумулятор 18650 в базу и перехать на 8 мгц и больше никаких танцев по моему не надо. Ну это если надо устройство работающее от аккумулятора а не стремится повторить энергопотребление промышленных устройств.
    Еще заметка с опытом выяснилось что сам модуль nRF24L01+ надо подбирать. Есть модули которые спят с током 6мА а есть которые с 0.05мА (ниже я уже не измеряю). BM*280 и правда надо усыплять програмно. Вешал его питание на ногу микроконтроллера. Так вот даже опустив ногу в ноль датчик умудрялся есть аккумулятор. Так что его нужно принудительно усыплять. Ну а точнее перевести в режим единичного измерения.

  13. Cadil_TM
    /#22765114

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

    А почему бы не вывести за пределы помещения тонким кабелем только датчик ds18b20, а остальное оставить в помещении. В промышленных образцах реализовано именно такое решение.

    • zeblong
      /#22765396

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

      • Cadil_TM
        /#22765996

        … к тому же не красивый проводок и где то установленная коробка с батарейками. жена не оценит такую красоту

        Тоже есть варианты:
        • Красивый многожильный проводок. Плюс красивая коробочка для жены. Замечу, красивая коробочка — это работа посложнее. Эстетику разделения проводка на два — оставить на суд голубей.
        • По результатам своих наблюдений внести корректировку температуры в скетч и оставить только am2320.
        • Побороть недоверие, разобравшись в его причинах.
        • Продолжать гробить не дешевые аккумуляторы, оставив все как есть.

        • zeblong
          /#22769448

          Причины недоверия — я тестировал разные датчики. Кстати на самом деле am2320 весьма нормально показывает. Но веры все равно больше ds18b20. Когда я устал от того что десяток термометров стоящих радом (и иногда и в одном корпусе) показывает совершенное разное я купил поверенный термометр ВИТ и уже всех сравнивал с ним. Влажность кстати как оказалось am2320 тоже показывает весьма хорошо. Но у них опять же есть такое что конкретные экземпляры немного отличаются.
          Сейчас повесил за окно датчик уже на здоровом аккумуляторе. Работает стабильно )
          даже на narodmon вытащил свои данные.

  14. nikolz
    /#22777804

    Автономная метеостанция на батарейках. Цена 4$ на Али.
    esp8266.ru/forum/threads/tlsr8251-lcd-termometr-lywsd03mmc-xiaomi-bluetooth-termometr.5263