// Soil Moisture Sensor based on ATTINY13A (http://tol.acritum.com). // Compiled with Arduino IDE 2.3.6, Microcore 2.5.2. // Предупреждение!!! некоторые китайские клоны USBasp продаются со старой прошивкой, которая не поддерживает низкие скорости процессора, такие как 128 килогерц. // Точнее, первый раз вы запишите программу, но потом программатор не будет видеть микроконтроллер. Решение: прошить сам USBasp последней версией прошивки (для // этого понадобится какая-нибудь ардуинка или другой программатор). Либо используйте специальный модифицированный код ArduinoISP для записи на низкой скорости. // Простое решение: не ставить частоту tiny13 меньше той частоты, которую поддерживает Ваш программатор. Точно не помню, где предел старой версии, но 1 мегагерц // должно быть безопасно. Если меняете частоту, меняйте и prescaler для ADC (читайте датащит для подбора). // Настройки Microcore в Arduino IDE, меню Tools: // BOD: disabled // Bootloader: no bootloader // Clock: 128kHz internal #define F_CPU 128000 // частота 128 килогерц - очень низкое токопотребление // назначение выводов ATTINY13A #define BUTTON_PIN PB1 #define VM_ADC_PIN PB4 // ADC2, pin3 #define SENSOR_ADC_PIN PB3 // ADC3, pin2 #define VOLTAGE_SIG_PIN PB2 #define RED_LED PB0 #define BUTTON_TIMEOUT_MS 300 // Таймаут в мс для ожидания следующего нажатия на кнопку #define BUTTON_DEBOUNCE_MS 25 // Дребезг для press/release // Интервалы в периодах по 8 сек (циклах WDT) #define MEASUREMENT_INTERVAL 5400 // 2700 циклов = проверка каждые 6 часов, 5400 циклов = каждые 12 часов, 10800 циклов = раз в сутки. #define MEASUREMENT_STABILIZATION_DELAY_MS 1000 // стабилизация между подачей сигнала и первым замером. #define RED_BLINK_INTERVAL 2 // 1 цикл = 8 секунд, 2 цикла = 16 сек, 3 цикла = 24 сек и т.д. // Определения для ADC #define REF_MV 1034UL // типично 1.1 вольт, но настраивается (или подгоняется) под конкретный микроконтроллер, так как разброс большой /мой 1.034V #define ADC_RESOLUTION 1024 // разрешение ADC #define ADC_PRESCALER_BITS 0 // Прескалер /2. Частота ADC clock: 128 кГц / 2 = 64 кГц (в пределах 50-200 кГц по datasheet), #define LOW_BATTERY_MV 1800 // напряжение батареи (милливольты), при котором изменяем тип сигнализации. изделие будет работать и при 1.5-1.6V, но светодиод практически не видно // Делители напряжения, указывать килоомы. #define UPPER_RESISTOR_PROBE 475 // сопротивление верхнего резистора в делителе для щупа #define THRESHOLD_PROBE 500 // при сопротивлении щупа+подстроечника выше этого значения, почва считается сухой #define COMPENSATION_PROBE 2 // компенсация измеренного сопротивления, если врет линейно на всем измеряемом диапазоне (плюс/минус килоом, а если не врет, ставим 0) #define UPPER_RESISTOR_VM 476 // верхний резистор делителя для вольтметра #define LOWER_RESISTOR_VM 100 // нижний резистор делителя для вольтметра #define RATIO_NUM ((LOWER_RESISTOR_VM * 1000UL) / (UPPER_RESISTOR_VM + LOWER_RESISTOR_VM)) // используется для рассчета напряжения относительно внутреннего референса. #define MEASUREMENTS_COUNT 32 // сколько замеров ADC для усреднения результата. uint8_t status; // Состояние: 1 - сухо и разряж. бат, 2 - сухо и норм. бат, 3 - влажно и разряж бат, 4 - влажно и норм бат volatile bool button_pressed = false; // Флаг нажатия кнопки volatile uint16_t cycle_count = 0; // считаем циклы по 8 секунд, чтобы узнать, когда мерить и когда мигать. // Пробуждение от WDT ISR(WDT_vect) { cycle_count++; wdt_reset(); // сбросить WDT в ISR, чтобы предотвратить сброс после прерывания } // PCINT1 прерывание по кнопке ISR(PCINT0_vect) { if (!(PINB & (1 << BUTTON_PIN))) { // Только если кнопка нажата (низкий уровень) button_pressed = true; } } // мигнуть светодиодом void blink_led(uint8_t count=1) { if (count==0) { // Включить светодиод PORTB |= (1 << RED_LED); _delay_ms(10); // Выключить PORTB &= ~(1 << RED_LED); _delay_ms(300); } else for (uint8_t i = 0; i< count; i++) { // Включить светодиод PORTB |= (1 << RED_LED); _delay_ms(200); // Выключить PORTB &= ~(1 << RED_LED); _delay_ms(300); } } // промигать число (допустимы числа от 0 до 9999) void blink_value(uint16_t value) { // далее извращенный вариант следующего, чтобы уменьшить размер скомпилированного кода /* uint8_t thousands = value / 1000; // Тысячи (0-9) uint8_t hundreds = (value / 100) % 10; // Сотни uint8_t tens = (value / 10) % 10; // Десятки uint8_t units = value % 10; // Единицы */ uint8_t digits[4]; uint16_t temp = value; for (int i = 3; i >= 0; i--) { digits[i] = temp % 10; temp /= 10; } // Находим первую ненулевую цифру и мигаем с неё for (uint8_t i = 0; i < 4; i++) { if (digits[i] != 0) { for (uint8_t j = i; j < 4; j++) { blink_led(digits[j]); _delay_ms(600); // Пауза между цифрами 300+600=900 мс - в 3 раза длиннее, чем между мигами внутри цифры } break; } } } // чтение ADC uint16_t adc_read(uint8_t channel) { // Эти значения из даташита ATtiny13A: ADMUX биты 7-0 // REFS0 = 0=vcc, 1=internal_1v1 // MUX1:MUX0 = 00=adc5, 01=adc1, 10=adc2, 11=adc3 if (channel==3) //SENSOR on ADC3 , VCC ref { ADMUX = (0 << REFS0) | // REFS0=0 (1 << MUX1) | // MUX1=1 (1 << MUX0); // MUX0=1 } else { // VM ON ADC2, 1.1v reference ADMUX = (1 << REFS0) | // REFS0=1 (1 << MUX1) | // MUX1=1 (0 << MUX0); // MUX0=0 } // Начинаем замеры, пропускаем 2 первых замера. Почему 2? Потому что после запуска ADC в датащите рекомендуется // задержка для стабилизации и пропуск первого неточного измерения, но на лишний delay у нас не хватает памяти, поэтому лишний замер - это вместо задержки! // Проводим измерения и усредняем результат uint32_t sum = 0; for (uint8_t i = 0; i < MEASUREMENTS_COUNT+2; i++) { // Запуск преобразования ADCSRA |= (1 << ADSC); while (ADCSRA & (1 << ADSC)); if (i>1) sum += ADC; } return sum / MEASUREMENTS_COUNT; } // Основная функция измерения void measure(uint8_t buttonmode) { // сигнальный пин как выход DDRB |= (1 << VOLTAGE_SIG_PIN) ; // Подать сигнал PORTB |= (1 << VOLTAGE_SIG_PIN); PRR = (1 << PRTIM0) | (0 << PRADC); // вывести ADC из энергосбережения // Включить ADC ADCSRA |= (1 << ADEN); _delay_ms(MEASUREMENT_STABILIZATION_DELAY_MS); // Стабилизация // ИЗМЕРЕНИЕ ПОКАЗАНИЙ uint16_t avg_adc_sensor = adc_read(3); uint16_t avg_adc_vcc = adc_read(2); // Отключить сигнал PORTB &= ~(1 << VOLTAGE_SIG_PIN); // Сигнальный пин - установить как вход без подтяжки для экономии батареи // согласно датащиту, цифровые пины, сконфигурированные на вход, отключены во время сна, кроме тех, что вызывают прерывание. DDRB &= ~((1 << VOLTAGE_SIG_PIN )); //PORTB &= ~((1 << VOLTAGE_SIG_PIN)); не обязательно, так как подтяжка не включена по умолчанию // Отключить ADC ADCSRA &= ~(1 << ADEN); // отключить ADC // напряжение батарейки uint32_t vcc_mv = (avg_adc_vcc * REF_MV * 1000) / (ADC_RESOLUTION * RATIO_NUM); // напряжение на делителе щупа uint32_t v_probe_mv = avg_adc_sensor * vcc_mv / ADC_RESOLUTION; // Расчет probe_resistance (сопротивление щупа+подстроечника в килоомах) uint32_t probe_resistance = (UPPER_RESISTOR_PROBE * v_probe_mv / (vcc_mv - v_probe_mv)) + COMPENSATION_PROBE; if (probe_resistance<=THRESHOLD_PROBE) status = (vcc_mv<=LOW_BATTERY_MV) ? 3:4; //влажно else status = (vcc_mv<=LOW_BATTERY_MV) ? 1:2; //сухо vcc_mv = ((vcc_mv << 16) / 100) >> 16; // экономично делим на 100 с усечением лишнего (vcc_mv = vcc_mv/100), если юзеру нужно промигать, например 2684mV=2.6V = два и шесть миганий // индикация результата только если нажимали на кнопку if (buttonmode==2) blink_value(probe_resistance); else if (buttonmode==3) blink_value(vcc_mv); else if (buttonmode==1) blink_led(status); } int main(void) { // Настройка портов DDRB |= (1 << VOLTAGE_SIG_PIN) | (1 << RED_LED); //выходы DDRB &= ~((1 << BUTTON_PIN) | (1 << VM_ADC_PIN) | (1 << SENSOR_ADC_PIN)); // входы PORTB |= (1 << BUTTON_PIN); // Pull-up для кнопки // PCINT: только для кнопки GIMSK |= (1 << PCIE); PCMSK |= (1 << PCINT1); // PCINT1 (PB1) PRR = (1 << PRTIM0) | (1 << PRADC); // Отключить таймер0 и ADC // Установка prescaler ADC // Для /2 (bits = 0) ADCSRA = (ADC_PRESCALER_BITS & 0x07); //ADCSRA &= ~(1 << ADEN); // отключить // wdt_reset(); // Сброс WDT WDTCR = (1 << WDCE) | (1 << WDE); // Разрешение изменения WDTCR = (1 << WDTIE) | (1 << WDP3) | (1 << WDP0); // Interrupt mode, 8 сек sei(); // Глобальное разрешение прерываний //_delay_ms(1000); bool just_button_press=true; // сделать первый замер при включении while (1) { uint8_t buttonmode = 0; // Переменная для режима кнопки if (button_pressed) { _delay_ms(BUTTON_DEBOUNCE_MS); if (PINB & (1 << BUTTON_PIN)) { button_pressed = false; continue; } buttonmode = 1; // Ждем release первого while (!(PINB & (1 << BUTTON_PIN))) _delay_ms(5); _delay_ms(BUTTON_DEBOUNCE_MS); // Цикл для дополнительных нажатий for (;;) { uint16_t timeout = 0; while (timeout < BUTTON_TIMEOUT_MS && (PINB & (1 << BUTTON_PIN))) { _delay_ms(5); timeout += 5; } if (timeout >= BUTTON_TIMEOUT_MS) break; _delay_ms(BUTTON_DEBOUNCE_MS); if (!(PINB & (1 << BUTTON_PIN))) { if (++buttonmode > 3) buttonmode = 3; while (!(PINB & (1 << BUTTON_PIN))) _delay_ms(5); _delay_ms(BUTTON_DEBOUNCE_MS); } else { break; } } just_button_press = true; button_pressed = false; } if ((just_button_press) or (cycle_count >= MEASUREMENT_INTERVAL)) { measure(buttonmode); cycle_count = 0; just_button_press = false; } else { // мигаем по таймеру только если сухо if (cycle_count % RED_BLINK_INTERVAL == 0) if (status<3) blink_led(status); } wdt_reset(); // сбросить WDT, чтобы после нажатия на кнопку мигания по таймеру раньше времени не мешали считывать данные со светодиода // Блок низкоуровневого сна в PWR_DOWN режиме, позводяет сэкономить ~20 байтов флеша, заменяет следующее: /* set_sleep_mode(SLEEP_MODE_PWR_DOWN); sleep_enable(); // Разрешить сон sleep_mode();// Сон sleep_disable(); // После пробуждения: Отключить сон */ PRR = (1 << PRTIM0) | (1 << PRADC); // Отключить таймер0 и ADC для экономии энергии MCUCR = (MCUCR & 0xE7) | 0x30; // Установить PWR_DOWN (SM1=1, SM0=0) и SE (Sleep Enable) одновременно, сохраняя остальные биты MCUCR __builtin_avr_sleep(); // Выполнить sleep (инструкция SLEEP) //проснулись? MCUCR &= ~(1 << SE); // Сбросить SE после пробуждения } return 0; }