CИСТЕМА КЛИМАТ-КОНТРОЛЯ "MADBCC1.1"

 madBath

У меня на даче батя отгрохал здоровенный трехэтажный дом с тремя туалетами и двумя ванными душевыми комнатами. Вторую душевую обычно используют "дачники", которые арендуют у родителей пару помещений, а в основной душевой есть одна злоебучая проблема, которая меня просто, сука, бесит. Каждый раз после умывания там надо щелкать специальной кнопочкой, чтобы включить вентилятор, удаляющий лишнюю влагу, а если забыть, то потом можно получить "леща" за невнимательность. Мне каждый раз ужасно лень тыкать эту чертову кнопку и, тем более, держать подобные вещи в уме. Решил сделать плату на основе микроконтроллера Attiny85, которая будет следить за температурой и влажностью сама. Когда-то я уже занимался системами климат-контроля для экзотических растений (скажем так)... Жаль, что исходники того проекта не сохранились. Хоть он и базировался на Ардуине, но все-таки, там была реализована интересная концепция централизованного контроля температуры и влажности с индикацией через IPS экран 128 линий и с датчиками, подключаемыми по радиоканалу через модуль NRF24L01. Вот как раз с этим модулем я так до сих пор и не справился, хотя теперь это уже и не очень интересно. И все же, как мне видится, вот эта плата "madBCC" - уже новый уровень, не какой-нибудь деребас, собранный из говна и палок на гейской ардуино-платформе, а практически реальный коммерческий продукт, изготовленный на китайском заводе по моему PCB-проекту.

madBathPCB

Мне почему-то ужасно лень рисовать электронные схемы, я обычно начинаю с рисунка самой платы, и для конкретно этой не заморачивался чего-то там дополнительно чертить - это как если бы сделать чертеж простого деревенского топора или даже лома - никто такой херней не мается. Как всегда, после изготовления платы, едва ее получил - сразу же заметил косяки. Резистор R3 оказался лишним. По идее, это должна была быть часть системы перезагрузки микроконтроллера или система установки нормы температуры и влажности. Уже не помню, где подглядел такую схему, но тут явно косяк. Возможно, эта часть придумывалась с бодуна. Кроме того, конкретно здесь отсутствует "декап", то есть, фильтрующие конденсаторы по питанию, а так же подпись "16A" не соответствует действительности. Хоть компоненты и рассчитаны на такой ток, но дорожки слишком узкие и будут сильно греться: при 3-х Амперах в условиях комнатной температуры 20 градусов Цельсия, будет превышение еще на 20, а далее по экспоненте. В пределах 2-х Ампер можно вообще не париться.  Теоретически, если обеспечить достаточное охлаждение, то можно подключить что-нибудь мощное, но я не бы не советовал заниматься такими экспериментами дома, без присмотра взрослых. В крайнем случае, если уж очень надо, в качестве промежуточного звена между силовой нагрузкой и модулем можно использовать контакторы или реле.

Ну и вот я накатал программку...

Суть следующая....

Здесь на PB5 кнопка аппаратного сброса. Такой сброс считается легитимным - ели ее нажать, то при следующей загрузке микроконтроллер загрузит какие-то настройки по умолчанию. Однако, тут есть еще свободный пин PB0. Изначально я думал его использовать под пин выбора датчика DHT11 или DHT22, чтобы программа "понимала", какой из них используется. Сейчас, немного подумав, я полагаю, что такую проверку можно организовать чисто программно, исходя из данных, получаемых с датчика, без использования соответствующего пина. В вот этой конкретной реализации пин проверки типа DHT используется, грубо говоря, под кнопку. Суть работы - она реагирует на размыкание, на падение напряжения. Если произошло размыкание - тогда текущие значение влажности и температуры принимается программмой как эталон, а так же записывается в энергонезависимую память, чтобы после аварии (любое выключение, кроме штатного) восстановить соответствующие настройки. "Соответствующие настройки" - это те значения температуры и влажности, которые были зафиксированны при нажатии кнопки PB0. "Вачдог" - это переферийный механизм, который защищает микроконтроллер от "зависаний" и вяских непредвиденных "глюков". Если он не получит своевременный сброс от микроконтроллера, то сам сбросит микроконтроллер, что приведет к загрузке настроек, ранее установленных кнопкой PB0, поскольку у нас есить регистр, умеющий определять причины перезагрузки. Основной алгоритам программы еще проще: если влажность выше эталонной, тогда включается основное реле, управляющее вытяжкой, а если ниже - то оно выключается. То же самое с температурным реле (Relay 2), только наоборот: если Температура ниже эталонной, тогда включается реле нагревателя, а если выше - тогда реле выключает нагреватель. Тут надо понимать, что конструкция платы предполагает для контроля температуры какое-то внешнее реле.

 /*
 * main.c
 *
 * Created: 8/18/2024 10:13:18 AM
 * Author: madmentat
 *
 *             ATtiny85
 *             +--------+
 * Reset/ PB5 -|1     8 |- Vcc
 * (ADC0) PB3 -|2     7 |- PB2 (SCK/ADC1/T0)
 * (ADC3) PB4 -|3     6 |- PB1 (MISO/OC0B/INT0/PCINT1)
 *        GND -|4     5 |- PB0 (MOSI/DI/SDA/OC0A/AREF/PCINT0)
 *             +--------+
 */
#define F_CPU 8000000UL
#include <avr/io.h>
#include <util/delay.h>
#include <stdint.h>
#include <avr/eeprom.h>
#define SOFT_UART_TX_PIN PB4
#define SOFT_UART_BAUDRATE 9600
#define SOFT_UART_DELAY (104)
#define BUTTON_PIN PB0
#define R1 PB3
#define R2 PB1
#define DHT_PIN PB2
#define SET_BIT(PORT, PIN)   (PORT |=  (1 << PIN))
#define CLEAR_BIT(PORT, PIN) (PORT &= ~(1 << PIN))
uint8_t temperature = 0, humidity = 0;
uint8_t target_temperature = 25;
uint8_t target_humidity = 50;
uint8_t EEMEM saved_temperature;  // Переменные для хранения значений в EEPROM
uint8_t EEMEM saved_humidity;
char temperatureStr[6];
char humidityStr[6];
uint8_t EEMEM eeprom_temperature = 25;
uint8_t EEMEM eeprom_humidity = 50;
uint8_t measurement_count = 0;
uint8_t stable_temperature = 0;
uint8_t stable_humidity = 0;
uint8_t button_was_pressed = 0;  // Переменная для отслеживания срабатывания кнопки
uint8_t resetReason;        // Переменная для хранения причины последней перезагрузки
int save = 0;
// Преобразование числа в строку char* itoa(int value, char* str, int base) { char* rc; char* ptr; char* low; static const char digits[] = "0123456789abcdefghijklmnopqrstuvwxyz"; if (base < 2 || base > 36) { *str = '\0'; return str; } rc = ptr = str; if (base == 10 && value < 0) { *ptr++ = '-'; value = -value; } low = ptr; do { *ptr++ = digits[value % base]; value /= base; } while (value); *ptr-- = '\0'; while (low < ptr) { char tmp = *low; *low++ = *ptr; *ptr-- = tmp; } return rc; } // Инициализация программного UART void soft_uart_init(void) { DDRB |= (1 << SOFT_UART_TX_PIN); PORTB |= (1 << SOFT_UART_TX_PIN); } // Функция для отправки одного байта по программному UART void soft_uart_send_byte(uint8_t byte) { CLEAR_BIT(PORTB, SOFT_UART_TX_PIN); _delay_us(SOFT_UART_DELAY);   for (uint8_t i = 0; i < 8; ++i) { if (byte & (1 << i)) { SET_BIT(PORTB, SOFT_UART_TX_PIN); } else { CLEAR_BIT(PORTB, SOFT_UART_TX_PIN); } _delay_us(SOFT_UART_DELAY); }   SET_BIT(PORTB, SOFT_UART_TX_PIN); _delay_us(SOFT_UART_DELAY); } // Функция для отправки строки по программному UART void soft_uart_send_string(const char* str) { while (*str) { soft_uart_send_byte(*str++); } } // Чтение данных с DHT11 void DHT_start() { DDRB |= (1 << DHT_PIN); PORTB &= ~(1 << DHT_PIN); _delay_ms(18); PORTB |= (1 << DHT_PIN); _delay_us(20); DDRB &= ~(1 << DHT_PIN); } uint8_t DHT_check_response() { _delay_us(40); if (!(PINB & (1 << DHT_PIN))) { _delay_us(80); if (PINB & (1 << DHT_PIN)) { _delay_us(40); return 1; } } return 0; } uint8_t DHT_read_byte() { uint8_t data = 0; for (int i = 0; i < 8; i++) { while (!(PINB & (1 << DHT_PIN))); _delay_us(30); if (PINB & (1 << DHT_PIN)) { data |= (1 << (7 - i)); } while (PINB & (1 << DHT_PIN)); } return data; } uint8_t DHT11_read(uint8_t* temperature, uint8_t* humidity) { uint8_t bits[5] = {0}; DHT_start(); if (DHT_check_response()) { bits[0] = DHT_read_byte(); bits[1] = DHT_read_byte(); bits[2] = DHT_read_byte(); bits[3] = DHT_read_byte(); bits[4] = DHT_read_byte(); uint8_t checksum = bits[0] + bits[1] + bits[2] + bits[3]; if (checksum == bits[4]) { *humidity = bits[0]; *temperature = bits[2]; return 1; } } return 0; } // Загрузка настроек из EEPROM void load_settings_from_eeprom(void) { target_temperature = eeprom_read_byte(&eeprom_temperature); target_humidity = eeprom_read_byte(&eeprom_humidity);   if (target_temperature == 0xFF || target_humidity == 0xFF) { target_temperature = 25; target_humidity = 50; } soft_uart_send_string("Loaded settings from EEPROM:\r\n"); soft_uart_send_string("Target temperature: "); itoa(target_temperature, temperatureStr, 10); soft_uart_send_string(temperatureStr); soft_uart_send_string(" C\n"); soft_uart_send_string("Target humidity: "); itoa(target_humidity, humidityStr, 10); soft_uart_send_string(humidityStr); soft_uart_send_string("%\r\n"); } // Сохранение настроек в EEPROM void save_settings_to_eeprom(void) { eeprom_write_byte(&eeprom_temperature, target_temperature); eeprom_write_byte(&eeprom_humidity, target_humidity); soft_uart_send_string("Saved settings to EEPROM:\r\n"); soft_uart_send_string("Target temperature: "); itoa(target_temperature, temperatureStr, 10); soft_uart_send_string(temperatureStr); soft_uart_send_string(" C\n"); soft_uart_send_string("Target humidity: "); itoa(target_humidity, humidityStr, 10); soft_uart_send_string(humidityStr); soft_uart_send_string("%\r\n"); } void setup() { soft_uart_init(); soft_uart_send_string("\r\nmadmentat.ru\n"); soft_uart_send_string("madBCC climate controller initialized!\n"); DDRB |= (1 << PB4) | (1 << PB3) | (1 << R1) | (1 << R2); DDRB &= ~(1 << BUTTON_PIN); // Считываем причину последней перезагрузки resetReason = MCUSR; MCUSR = 0; // Очистка флагов сброса // Проверка причины сброса if (resetReason & (1 << WDRF)) { soft_uart_send_string("Watchdog Reset.\r\n"); load_settings_from_eeprom(); } else if (resetReason & (1 << EXTRF)) { soft_uart_send_string("External Reset (Button).\r\n"); target_temperature = 25; target_humidity = 50; save = 1; } else if (resetReason & (1 << PORF)) { soft_uart_send_string("Power-off Reset.\r\n"); load_settings_from_eeprom(); } else if (resetReason & (1 << BORF)) { soft_uart_send_string("Brown-out Reset.\r\n"); load_settings_from_eeprom(); } else { soft_uart_send_string("Unknown Reset.\r\n"); load_settings_from_eeprom(); } // Игнорируем состояние перемычки сразу после запуска button_was_pressed = PINB & (1 << BUTTON_PIN); } void loop() { static uint8_t prev_temperature = 0; static uint8_t prev_humidity = 0; static uint8_t first_run = 1; // Флаг первого запуска while (1) { uint8_t button_state = PINB & (1 << BUTTON_PIN); // Если перемычка была разомкнута, сбрасываем флаг срабатывания if (!button_state) { button_was_pressed = 0; save = 1; } // Обработка замыкания перемычки if (button_state && !button_was_pressed) { _delay_ms(50); // Дебаунсинг if (PINB & (1 << BUTTON_PIN)) { // Дополнительная проверка состояния target_temperature = temperature; target_humidity = humidity; save_settings_to_eeprom(); soft_uart_send_string("\r\nSettings saved!\n"); button_was_pressed = 1; // Фиксируем срабатывание } } // Считывание данных с DHT11 if (DHT11_read(&temperature, &humidity)) { if (save == 1) { target_humidity = humidity; target_temperature = temperature; save_settings_to_eeprom(); save = 0; } // Проверка изменений температуры и влажности или первый запуск if (temperature != prev_temperature || humidity != prev_humidity || first_run) { first_run = 0; // Снимаем флаг первого запуска после первой отправки // Отправляем данные в UART soft_uart_send_string("\r\nReading data from DHT11...\r\n"); // Реле и сообщения для управления температурой if (temperature < target_temperature) { PORTB |= (1 << R1); soft_uart_send_string("Temperature relay ON\n"); } else if (temperature > target_temperature) { PORTB &= ~(1 << R1); soft_uart_send_string("Temperature relay OFF\n"); } // Реле и сообщения для управления влажностью if (humidity > target_humidity) { PORTB |= (1 << R2); // Включаем реле при превышении влажности soft_uart_send_string("Humidity relay ON\n"); } else { PORTB &= ~(1 << R2); // Выключаем реле, если влажность ниже целевой soft_uart_send_string("Humidity relay OFF\n"); } // Сохранение предыдущих значений prev_temperature = temperature; prev_humidity = humidity; // Отправка значений в UART itoa(temperature, temperatureStr, 10); itoa(humidity, humidityStr, 10); soft_uart_send_string("Current temperature is "); soft_uart_send_string(temperatureStr); soft_uart_send_string(" C\n"); soft_uart_send_string("Target temperature: "); itoa(target_temperature, temperatureStr, 10); soft_uart_send_string(temperatureStr); soft_uart_send_string(" C\n"); soft_uart_send_string("Current humidity is "); soft_uart_send_string(humidityStr); soft_uart_send_string("\r\n"); soft_uart_send_string("Target humidity: "); itoa(target_humidity, humidityStr, 10); soft_uart_send_string(humidityStr); soft_uart_send_string("%\r\n"); } } else { soft_uart_send_string("No data. Failed to read.\r\n"); PORTB &= ~(1 << R1); PORTB &= ~(1 << R2); soft_uart_send_string("All relays DISABLED!!!"); } _delay_ms(2000); } } int main(void) { setup(); loop(); return 0; }

Кстати говоря, я уже, возможно, где-то упомянал, что такие задержки, типа _delay_ms(2000) - это дурной тон, поскольку тут речь идет о тупом пропуске рабочих тактов, однако конкретно в данном случае у микроконтроллера нет каких-то других задач и поэтому все равно. С усложнением алгоритма подобный подход только навредит и тогда потребуется настройка прерываний по аппаратному таймеру.  Вот небольшая статейка о том, как их настроить и использовать.

Тут еще одна реализация, в которой все-таки можно использовать немного более навороченный датчик DHT22, установив перемычку J1 (PB0) в правое положение или вынув ее вовсе:

/*
*       https://madmentat.ru
*       2024
*
*                            ATtiny85
*                           +--------+
*               Reset/ PB5 -|1     8 |- Vcc
*               (ADC0) PB3 -|2     7 |- PB2 (SCK/ADC1/T0)
*               (ADC3) PB4 -|3     6 |- PB1 (MISO/OC0B/INT0/PCINT1)
*                      GND -|4     5 |- PB0 (MOSI/DI/SDA/OC0A/AREF/PCINT0)
*                           +--------+
*                            
*/
#define F_CPU 8000000UL
#include <avr/io.h>
#include <util/delay.h>
#include <stdint.h>
#define SOFT_UART_TX_PIN PB4 // Определения для программного UART
#define SOFT_UART_BAUDRATE 9600
#define SOFT_UART_DELAY (104) // Задержка в микросекундах для 9600 бод
#define CHECK PB0 //Определяем тип датчика. Замкнутый контакт, высокий уровень - DHT11, разомкнутый, низкий уровень - DHT22
#define R1 PB3
#define R2 PB1
#define DHT_PIN PB2 // Определение для DHT11
#define SET_BIT(PORT, PIN)   (PORT |=  (1 << PIN))
#define CLEAR_BIT(PORT, PIN) (PORT &= ~(1 << PIN))
uint8_t temperature = 0, humidity = 0;
char temperatureStr[6];
char    humidityStr[6];
// Преобразование числа в строку
char* itoa(int value, char* str, int base) {
    char* rc;
    char* ptr;
    char* low;
    static const char digits[] = "0123456789abcdefghijklmnopqrstuvwxyz";
    if (base < 2 || base > 36) {
        *str = '\0';
        return str;
    }
    rc = ptr = str;
    if (base == 10 && value < 0) {
        *ptr++ = '-';
        value = -value;
    }
    low = ptr;
    do {
        *ptr++ = digits[value % base];
        value /= base;
    } while (value);
    *ptr-- = '\0';
    while (low < ptr) {
        char tmp = *low;
        *low++ = *ptr;
        *ptr-- = tmp;
    }
    return rc;
}
// Инициализация программного UART
void soft_uart_init(void) {
    DDRB |= (1 << SOFT_UART_TX_PIN);  // Настроить TX_PIN как выход
    PORTB |= (1 << SOFT_UART_TX_PIN); // Установить TX_PIN в высокий уровень
}
// Функция для отправки одного байта по программному UART
void soft_uart_send_byte(uint8_t byte) {
    // Стартовый бит
    CLEAR_BIT(PORTB, SOFT_UART_TX_PIN);
    _delay_us(SOFT_UART_DELAY);
    // Отправка данных
    for (uint8_t i = 0; i < 8; ++i) {
        if (byte & (1 << i)) {
            SET_BIT(PORTB, SOFT_UART_TX_PIN);
            } else {
            CLEAR_BIT(PORTB, SOFT_UART_TX_PIN);
        }
        _delay_us(SOFT_UART_DELAY);
    }
    // Стоповый бит
    SET_BIT(PORTB, SOFT_UART_TX_PIN);
    _delay_us(SOFT_UART_DELAY);
}
// Функция для отправки строки по программному UART
void soft_uart_send_string(const char* str) {
    while (*str) {
        soft_uart_send_byte(*str++);
    }
}
// Чтение данных с DHT11
void DHT_start() {
    DDRB |= (1 << DHT_PIN); // Настроить DHT_PIN как выход
    PORTB &= ~(1 << DHT_PIN); // Потянуть линию вниз
    _delay_ms(18); // Держим линию вниз не менее 18 мс
    PORTB |= (1 << DHT_PIN); // Потянуть линию вверх
    _delay_us(20); // Ждем 20-40 мкс
    DDRB &= ~(1 << DHT_PIN); // Настроить DHT_PIN как вход
}
uint8_t DHT_check_response() {
    _delay_us(40);
    if (!(PINB & (1 << DHT_PIN))) {
        _delay_us(80);
        if (PINB & (1 << DHT_PIN)) {
            _delay_us(40);
            return 1;
        }
    }
    return 0;
}
uint8_t DHT_read_byte() {
    uint8_t data = 0;
    for (int i = 0; i < 8; i++) {
        while (!(PINB & (1 << DHT_PIN))); // Ждем пока линия не станет высокой
        _delay_us(30);
        if (PINB & (1 << DHT_PIN)) {
            data |= (1 << (7 - i));
        }
        while (PINB & (1 << DHT_PIN)); // Ждем пока линия не станет низкой
    }
    return data;
}
uint8_t DHT11_read(uint8_t* temperature, uint8_t* humidity) {
    uint8_t bits[5] = {0};
    DHT_start();
    if (DHT_check_response()) {
        bits[0] = DHT_read_byte();
        bits[1] = DHT_read_byte();
        bits[2] = DHT_read_byte();
        bits[3] = DHT_read_byte();
        bits[4] = DHT_read_byte();
 
        uint8_t checksum = bits[0] + bits[1] + bits[2] + bits[3];
        if (checksum == bits[4]) {
            *humidity = bits[0];
            *temperature = bits[2];
            return 1;
        }
    }
    return 0;
}
uint8_t DHT22_read(uint8_t* temperature, uint8_t* humidity) {
    uint8_t bits[5] = {0};
    DHT_start();
    if (DHT_check_response()) {
        for (uint8_t i = 0; i < 5; i++) {
            bits[i] = DHT_read_byte();
        }
        uint8_t checksum = bits[0] + bits[1] + bits[2] + bits[3];
        if (checksum == bits[4]) {
            uint16_t raw_humidity = (bits[0] << 8) | bits[1];
            uint16_t raw_temperature = (bits[2] << 8) | bits[3];
 
            *humidity = raw_humidity / 10;
            *temperature = raw_temperature / 10;
            if (bits[2] & 0x80) {
                *temperature = -(*temperature);
            }
            return 1;
        }
    }
    return 0;
}
void setup() {
    soft_uart_init(); // Инициализация программного UART
    soft_uart_send_string("\r\n");
    soft_uart_send_string("https://madmentat.ru");    
    soft_uart_send_string("\n");
    soft_uart_send_string("madBCC climate controller initialized!");
    DDRB   |=  (1 << PB4) |  (1 << PB3) |  (1 << R1)|  (1 << R2);
    DDRB   &= ~(1 << CHECK);
    PORTB  &= ~(1 << PB4) |  (1 << PB3);
}
void loop() {
        while (1) {
            if(PINB & (1 << CHECK)){
                  DHT11_read(&temperature, &humidity);
                  soft_uart_send_string("\r\nReading data from DHT11...\r\n");
            }
            else  {
                  DHT22_read(&temperature, &humidity);
                  soft_uart_send_string("\r\nReading data from DHT22...\r\n");
            }
            if (temperature == 0 && humidity == 0) {
                soft_uart_send_string("No data. Failed to read.\r\n");
                } else {
                itoa(temperature, temperatureStr, 10);
                itoa(humidity, humidityStr, 10);
                soft_uart_send_string("Current temperature is ");
                soft_uart_send_string(temperatureStr);
                soft_uart_send_string(" C");
                soft_uart_send_byte(0xC2); // первый байт символа градуса
                soft_uart_send_byte(0xB0); // второй байт символа градуса
                soft_uart_send_string("\n");
                    soft_uart_send_string("Current humidity is ");
                    soft_uart_send_string(humidityStr);
                    soft_uart_send_string("\r\n");
                if(temperature < 27){
                    PORTB |= (1 << R1);//  |  (1 << PB3
                    soft_uart_send_string("Relay 1 ON");
                    soft_uart_send_string("\n");
                }
                if(temperature > 26){
                    PORTB  &= ~(1 << R1);//  |  (1 << PB3);
                    soft_uart_send_string("Relay 1 OFF");
                    soft_uart_send_string("\n");
                }
                    if(humidity > 75){
                        PORTB |= (1 << R2);//  |  (1 << PB3
                        soft_uart_send_string("Relay 2 ON");
                        soft_uart_send_string("\n");
                    }
                    if(humidity < 75){
                        PORTB  &= ~(1 << R2);//  |  (1 << PB3);
                        soft_uart_send_string("Relay 2 OFF");
                        soft_uart_send_string("\n");
                    }
            }
            _delay_ms(2000); // Считывание каждые 2 секунды
        }
}
int main(void) {
    setup();
    loop();
    return 0;
}