Товары из Китая

Простой беспроводной метеодатчик на Attiny13


Простой беспроводной метеодатчик на Attiny13

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

Измерение температуры

Atiny13/13A не имеет встроенного датчика температуры, поэтому необходимо использовать внешний. В самоделках в качестве датчиков наиболее часто используются простые и цифровые сенсоры температуры и влажности серии DHT11, DHT22 и их многочисленные китайские клоны (в моем случае — AM2320):

Простой беспроводной метеодатчик на Attiny13

Все они отличаются точностью и диапазонами измерения, но имеют общий протокол для обмена информацией с микроконтроллером:

Простой беспроводной метеодатчик на Attiny13

Для работы с этими датчиками я состряпал простую и «ленивую» мини библиотеку, которая позволяет читать данные из DHT11 и DHT22 (AM2320):

TinyDHT.h
#ifndef _TINYDHT_H_

#define _TINYDHT_H_
#include <avr/io.h>
#include <util/delay.h>
#ifndef TDHT_DDR_REG
#define TDHT_DDR_REG DDRB
#endif
#ifndef TDHT_PORT_REG
#define TDHT_PORT_REG PORTB
#endif
#ifndef TDHT_PIN_REG
#define TDHT_PIN_REG PINB
#endif
#ifndef TDHT_ONE_DELAY_US
#define TDHT_ONE_DELAY_US 30
#endif
#ifndef TDHT_READ_ATTEMPTS
#define TDHT_READ_ATTEMPTS 2
#endif
#define _tdhtPinLow(pin) TDHT_PORT_REG &= ~(1 << pin)
#define _tdhtPinHigh(pin) TDHT_PORT_REG |= (1 << pin)
#define _tdhtPinToOut(pin) TDHT_DDR_REG |= (1 << pin)
#define _tdhtPinToIn(pin) TDHT_DDR_REG &= ~(1 << pin)
#define _tdhtGetPinValue(pin) (TDHT_PIN_REG & (1 << pin))
static inline void _tdhtPinChangeWait(uint8_t pin, uint8_t currentState) {
uint8_t counter = 0;
while ((!_tdhtGetPinValue(pin)) != currentState && ++counter < 255) {
_delay_us(1);
}
}
static inline uint8_t _tdhtReadRawData(uint8_t pin, uint16_t sensorDelay, uint8_t *raw_data_buffer) {
_delay_ms(sensorDelay);
_tdhtPinToOut(pin);
_tdhtPinLow(pin);
_delay_ms(20);
_tdhtPinToIn(pin);
_tdhtPinHigh(pin);
_tdhtPinChangeWait(pin, 1);
_tdhtPinChangeWait(pin, 0);
_tdhtPinChangeWait(pin, 1);
for (uint8_t i = 0; i < 5; i++) {
for (uint8_t j = 0; j < 8; j++) {
_tdhtPinChangeWait(pin, 0);
raw_data_buffer[i] <<= 1;
_delay_us(TDHT_ONE_DELAY_US);
if (_tdhtGetPinValue(pin)) {
raw_data_buffer[i] |= 1;
_tdhtPinChangeWait(pin, 1);
}
}
}
return (raw_data_buffer[4] == (uint8_t) (raw_data_buffer[0] + raw_data_buffer[1] + raw_data_buffer[2] + raw_data_buffer[3]));
}
static inline uint8_t tdhtReadDataDHT11(uint8_t pin, int16_t *temperature, uint16_t *humidity) {
uint8_t raw_data[5] = {0, 0, 0, 0, 0};
for (uint8_t i = 0; i < TDHT_READ_ATTEMPTS; i++) {
if (_tdhtReadRawData(pin, 1500, raw_data)) {
*temperature = (uint16_t) raw_data[2] * 10;
*humidity = (uint16_t) raw_data[0] * 10;
if (i > 0) return 1;
}
}
return 0;
}
static inline uint8_t tdhtReadDataDHT22(uint8_t pin, int16_t *temperature, uint16_t *humidity) {
uint8_t raw_data[5] = {0, 0, 0, 0, 0};
for (uint8_t i = 0; i < TDHT_READ_ATTEMPTS; i++) {
if (_tdhtReadRawData(pin, 2000, raw_data)) {
*temperature = ((uint16_t) raw_data[2] << 8) | raw_data[3];
*humidity = ((uint16_t) raw_data[0] << 8) | raw_data[1];
if (i > 0) return 1;
}
}
return 0;
}
#endif /* _TINYDHT_H_ */

Она не использует никакие прерывания, все взаимодействие происходит через простой bit-banging без какого-либо контроля корректности интервалов. Тем не менее код работает практически на любых МК, не завелось только на Attiny13 с частотой 600КГц — скорее всего причина в высоких накладных расходах. Собственно, сами методы получения данных с датчиков:

uint8_t tdhtReadDataDHT11(uint8_t pin, int16_t *temperature, uint16_t *humidity);

uint8_t tdhtReadDataDHT22(uint8_t pin, int16_t *temperature, uint16_t *humidity);

Передаем номер пина, к которому подключен датчик, и указатели на переменные, куда записывать результат. В случае успешного получения данных методы возвращают 1, в случае ошибки — 0. Параметр TDHT_READ_ATTEMPTS задает количество попыток чтения данных и должен быть не меньше 2, так как многие клоны датчиков выдают при первом чтении мусорные данные, и поэтому результат первого чтения отбрасывается.

Упаковка данных

Для передачи по воздуху данные датчика нужно предварительно и как можно более компактно упаковать в байты. Чем меньше пакет — тем больше вероятность его успешной доставки и меньше затраты энергии на передачу. Кроме того, пакеты длиной не более 32 бит (4 байта) можно принимать практически чем угодно с помощью библиотеки RCSwitch. В общем случае температура с датчиков DHT может принимать значения от -40°C до 80°C, влажность — от 0% до 100%. В итоге я решил использовать следующую структуру 32-битного пакета:

  • 3 бита — ID метеодатчика (значения от 0 до 7, позволяет адресовать до 8 устройств)
  • 1 бит — знак температуры (0 — положительная, 1 — отрицательная)
  • 10 бит — температура
  • 10 бит — влажность
  • 8 бит — контрольная сумма всех предыдущих значений, обрезанная до 8 бит

Таким образом, в посылке можно передать температуру от -102.3°C до 102.3°C и влажность от 0 до 102.3%, что перекрывает возможности датчиков и не вносит большой избыточности. Контрольная сумма позволяет на приемной стороне отсеивать битые пакеты, 8 бит для этого, конечно, маловато, но за все время экспериментов я не заметил, чтобы даже через эту простейшую защиту пробивались некорректные данные.

Собираем воедино

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

  • Сам микроконтроллер ATiny13
  • Датчик DHT11, DHT22 или AM2320 — выбираем в зависимости от требуемой точности и диапазона значений
  • Передатчик FS1000A или любой другой идентичный на 433.92 МГц. Можно в паре с приёмником, он нам тоже ещё пригодится
  • Конденсатор от 0.1 до 1 мкФ
  • Опционально — резистор на 10 кОм. В общем случае не нужен, но я столкнулся с недостаточностью встроенной подтяжки пина на некоторых экземплярах микроконтроллера

Простой беспроводной метеодатчик на Attiny13

Общая схема метеодатчика:

Простой беспроводной метеодатчик на Attiny13

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

Attiny13, как и многие другие микроконтроллеры, имеет встроенные подтягивающие (pull-up) резисторы, но я столкнулся с тем, что на некоторых экземплярах МК они недостаточно «сильные» для корректной работы с датчиком, поэтому мне пришлось усилить подтяжку с помощью резистора на 10 кОм, включив его между PB0 и PB1 (VCC и DATA датчика).

Исходный код программы
/*

* WeatherSensor.cpp
*
* Created: 14.09.2021 15:13:19
* Author : SinuX
*/
#define F_CPU 1200000UL // 1.2 MHz
#define TRF_TX_PIN PB4 // Передатчик на 4 пине
#define TRF_DATA_SIZE 4 // Размер посылки 4 байта
#define TRF_RX_DISABLED // Исключаем приемную часть TinyRF для экономии места
#define DHT22 // Тип датчика: DHT22/AM2320
#define DHT_PIN PB0 // Датчик на 0 пине
// Контакты VCC датчика и передатчика
#define TX_VCC_PIN PB3
#define DHT_VCC_PIN PB1
// Множитель циклов сна (значения от 1 до 255) позволяет настраивать интервал отправки данных от 8 секунд до 34 минут
#define SLEEP_MULTIPLIER 5 // Просыпаемся и отправляем инфу раз в 5 * 8 = 40 секунд
#define SENSOR_ID 0 // ID метеодатчика (значения от 0 до 7)
#include "tinydht.h"
#include "tinyrf.h"
#include <stdlib.h>
#include <avr/sleep.h>
#include <avr/wdt.h>
typedef union {
struct {
uint8_t sensorId : 3;
uint8_t temperatureSign : 1;
uint16_t temperature : 10;
uint16_t humidity : 10;
uint8_t checksum : 8;
};
uint8_t dataBytes[4];
uint32_t data;
} TxData;
uint8_t volatile wakeupCounter = 0;
void sendData(int16_t temperature, uint16_t humidity) {
TxData txData;
txData.data = 0;
txData.sensorId = SENSOR_ID;
txData.temperatureSign = temperature < 0 ? 1 : 0;
txData.temperature = abs(temperature);
txData.humidity = humidity;
txData.checksum = txData.sensorId + txData.temperatureSign + txData.temperature + txData.humidity;

// Включаем передатчик и отправляем данные
trf_init();
PORTB |= (1 << TX_VCC_PIN);
trf_send(txData.dataBytes);
}
ISR (WDT_vect) {
// Если не настало время отправки - погружаемся обратно в сон
if (++wakeupCounter < SLEEP_MULTIPLIER) {
return;
}

wakeupCounter = 0;

// Временно отключаем ватчдог, чтобы он не сбросил МК в процессе получения значений
wdt_disable();

// Включаем датчик и пытаемся получить данные
PORTB |= (1 << DHT_VCC_PIN);
int16_t temperature = 0;
uint16_t humidity = 0;
uint8_t readResult = 0;

// DHT11
#ifdef DHT11
readResult = tdhtReadDataDHT11(DHT_PIN, &temperature, &humidity);
#endif
// DHT22
#ifdef DHT22
readResult = tdhtReadDataDHT22(DHT_PIN, &temperature, &humidity);
#endif

// Отключаем датчик и отправляем инфу
PORTB &= ~(1 << DHT_VCC_PIN);
if (readResult) {
sendData(temperature, humidity);
}
}
int main(void) {
DDRB |= (1 << TX_VCC_PIN);
DDRB |= (1 << DHT_VCC_PIN);
wdt_reset();
sei();
set_sleep_mode(SLEEP_MODE_PWR_DOWN);

while(1) {
// Отключаем пины, заводим "будильник" на 8 секунд и выключаемся
PORTB = 0;
wdt_enable(WDTO_8S);
WDTCR |= (1 << WDTIE);
sleep_enable();
sleep_cpu();
}
}

Со включенной оптимизацией -Os код компилируется в 780 байт, чего достаточно для заливки в t13, и даже остается еще место для добавления каких-нибудь дополнительных фич (например, чтения уровня батарейки и отправки отдельными посылками). Фьюзы для прошивки следующие: -Ulfuse:w:0x62:m -Uhfuse:w:0xDF:m

В коде наибольший интерес представляют константы SENSOR_ID и SLEEP_MULTIPLIER, первая задает идентификатор метеодатчика (должен быть уникальным), вторая позволяет настроить период пробуждения для чтения и отправки данных.

Общий алгоритм работы следующий:

  • МК находится в выключенном состоянии и просыпается каждые 8 * SLEEP_MULTIPLIER секунд по прерыванию WDT
  • Подает питание на датчик и с задержкой (1.5 сек для DHT11, 2 сек для DHT22) делает 2 попытки чтения данных
  • Отключает питание датчика и, если данные были успешно прочитаны, включает передатчик и отправляет данные
  • Снова уходит в отключку

Таким образом, с помощью SLEEP_MULTIPLIER можно гибко настроить длительность сна (а следовательно и период отправки инфы) в пределах от 8 до 2040 секунд. Пакет данных отправляется 10 раз, если нужно другое значение — можно настроить параметром TRF_TX_REPEAT_COUNT из TinyRF.

Благодаря программному управлению питанием датчика и передатчика в режиме сна через них нет никакой утечки тока. В режиме сна схема потребляет всего 7 мкА при питании от 5В, при чтении данных датчика — 0.9 — 3 мА, при передаче — около 10 мА. При периодах обновления, исчисляемых десятками минут, среднее энергопотребление близится к минимально возможному, поэтому такой датчик может очень долгое время (недели и месяцы) работать от литиевого аккумулятора даже небольшой емкости. Микроконтроллер и передатчик могут работать даже от одной 3-вольтовой батарейки CR2032, но вот DHT исключает такой вариант питания, потому что требует на вход минимум 3.3В. От 2xCR2023 питаться также не выйдет — выдаваемые ими 6 вольт выходят за рамки допустимых 5.5В как датчика, так и микроконтроллера, поэтому из батарейных вариантов питания здесь мне видятся только 3 элемента по 1.5В.

Приемная часть

Так как посылка имеет длину 32 бита, то ее можно принять с помощью любой ардуины с библиотекой RCSwitch. Берем любой приемник на 433.92 МГц, подключаем его к Arduino и заливаем скетч (не забыв поправить номер пина данных в соответствии со своей моделью платы):

Скетч приемника для Arduino Nano
#include <RCSwitch.h>

RCSwitch mySwitch = RCSwitch();
unsigned long lastValue;
union {
struct {
uint8_t sensorId : 3;
uint8_t temperatureSign : 1;
uint16_t temperature : 10;
uint16_t humidity : 10;
uint8_t checksum : 8;
};
byte dataBytes[4];
} RxData;
void setup() {
Serial.begin(9600);
mySwitch.enableReceive(0); // Receiver on interrupt 0 => that is pin #2
}
void loop() {
if (mySwitch.available()) {
unsigned long value = mySwitch.getReceivedValue();
// Получена новая посылка, отличная от предыдущей
if (lastValue != value) {
lastValue = value;
// Переворачиваем байты посылки
for (int8_t i = 3; i >= 0; i--) {
RxData.dataBytes[i] = (byte) value;
value >>= 8;
}

// Выводим на экран, если контрольная сумма верна
if (RxData.checksum == (byte)(RxData.sensorId + RxData.temperatureSign + RxData.temperature + RxData.humidity)) {
Serial.print("Sensor: ");
Serial.print(RxData.sensorId);
Serial.print(", Temp: ");
Serial.print(RxData.temperatureSign ? "-" : "");
Serial.print(RxData.temperature / 10);
Serial.print(".");
Serial.print(RxData.temperature % 10);
Serial.print("ºC, Humid: ");
Serial.print(RxData.humidity / 10);
Serial.print(".");
Serial.print(RxData.humidity % 10);
Serial.println("%");
}
}

mySwitch.resetAvailable();
}
}

Открываем консоль, включаем метеодатчики, и если все было выполнено верно — наблюдаем вывод получаемых данных:

Простой беспроводной метеодатчик на Attiny13

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

Областей применения для самодельного метеодатчика можно придумать множество. Единственное, что меня смущает и вызывает вопросы, — его работоспособность при сильно отрицательных температурах. Микроконтроллеры ATtiny имеют не самый стабильный внутренний генератор, который в неблагоприятных условиях может «поплыть» еще сильнее вплоть до невозможности взаимодействия с датчиком или возникновения некорректных таймингов при отправке данных по радио. Попытка заморозки работающего датчика в морозилке до -10ºC показала, что с таким небольшим минусом проблем в его работе не возникает, как будут обстоять дела с более низкими температурами — я думаю надвигающаяся зима покажет.


СМОТРИ ТАКЖЕ

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *