CИСТЕМА КЛИМАТ-КОНТРОЛЯ "MADBCC1.1"
У меня на даче батя отгрохал здоровенный трехэтажный дом с тремя туалетами и двумя ванными душевыми комнатами. Вторую душевую обычно используют "дачники", которые арендуют у родителей пару помещений, а в основной душевой есть одна злоебучая проблема, которая меня просто, сука, бесит. Каждый раз после умывания там надо щелкать специальной кнопочкой, чтобы включить вентилятор, удаляющий лишнюю влагу, а если забыть, то потом можно получить "леща" за невнимательность. Мне каждый раз ужасно лень тыкать эту чертову кнопку и, тем более, держать подобные вещи в уме. Решил сделать плату на основе микроконтроллера Attiny85, которая будет следить за температурой и влажностью сама. Когда-то я уже занимался системами климат-контроля для экзотических растений (скажем так)... Жаль, что исходники того проекта не сохранились. Хоть он и базировался на Ардуине, но все-таки, там была реализована интересная концепция централизованного контроля температуры и влажности с индикацией через IPS экран 128 линий и с датчиками, подключаемыми по радиоканалу через модуль NRF24L01. Вот как раз с этим модулем я так до сих пор и не справился, хотя теперь это уже и не очень интересно. И все же, как мне видится, вот эта плата "madBCC" - уже новый уровень, не какой-нибудь деребас, собранный из говна и палок на гейской ардуино-платформе, а практически реальный коммерческий продукт, изготовленный на китайском заводе по моему PCB-проекту.
Мне почему-то ужасно лень рисовать электронные схемы, я обычно начинаю с рисунка самой платы, и для конкретно этой не заморачивался чего-то там дополнительно чертить - это как если бы сделать чертеж простого деревенского топора или даже лома - никто такой херней не мается. Как всегда, после изготовления платы, едва ее получил - сразу же заметил косяки. Резистор 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; }