4445

Every spring time I admire the flowering of the fruit trees in my garden. The first to flourish is an apricot tree. So early (begin of march this year) that the March and April frost in the past years destroyed all harvest. Fruit farmers utilize the heat of crystallization when water freezes below 4° C. So I decided to irrigate my apricot tree to avoid frost destruction.

=> Advanced version with ATtiny84 controller (see "Updates" at the end of the article)

Commercial frost guard systems are quite expensive. Not the watering part but the controller part. My self-made solution consists of
  • the AVR ATTiny-85 basedFrost Guard controller
  • a valve to switch irrigation on and off
  • a circular sprinkler[1] and some meters of hose

Frost Guard controller features

The frost guard controller has two main operation modes.

Main mode watch

In watch mode the temperature is monitored all 10[s]. If the temperature reaches the low threshold temperature (adjustable, default 1° C) the irrigation starts. Irrigation stops if the temperature raises above the high threshold temperature (adjustable, default 3° C). When the temperature raises above the low threshold temperature the irrigation is pulsed. For each 0.5 °C temperature increase a 30[s] pause is inserted after 60[s] of irrigation
.
Each irrigation event is recorded with a time stamp and the corresponding temperature. The recorded data is written into the controller’s eeprom memory (until it’s full: 83 events). Data is dumped @19200 Baud in an ascii JSON pretty print format utilizing a mobile phone with a USB terminal software[2], a USB OTG adapter and an FT232RL USB to TTL serial adapter[3] (see attachment file serial-adapter.jpg).

Main mode menu

The menu comprises selection of modes:
  • Date and time setup
  • Threshold temperatures setup
  • Brightness setup
  • Irrigation on/off (for tests)
  • Data transfer

The different modes are explained in the project documentation (see attachment file FrostGuard.pdf).


Hardware

The hardware is simple state of the art (see attachment file  frostguard-schem.png):
  • an ATTiny85 micro controller [4]
  • a TM1637 controlled 4 digits 7 segments display [5]
  • a DS18S20 temperature sensor [6]
  • the TM1637 is also utilized to interface 3 push buttons
  • a 5V relay for irrigation control

The prototype is housed in a IP65 protected module box sized 100 mm x 68 mm x 50 mm. The display is mounted to a 5mm plexiglass epoxied to the module box front. Left of the 7 segments display the irrigation control LED is positioned.

Three push buttons are connected to the TM1637 keyboard interface. The utilized display from az-delivery[5] does not support keyboard input, so I had to pimp the display. A 4 pins male connector is epoxied to the display pcb supplying keyboard connectors K1, KS2, KS2 and KS3 (see attachment file pimped-tm1637.jpg).

The temperature sensor needs a 15 meters cable. Available pre confectioned sensors do have a maximum of 10 meters cable. Coax cables with BNS connectors of 15 meters length are available for less than 10 EURO. Resulting a two wires connection for the temperature sensor (see data sheet[6], page 7, parasite power mode) was chosen. The sensor is epoxied into a 6mm Ø aluminum tube and hence is water proof (see attachment file temp-sensor-real.png).

I’ve seen a lot of discussions in the web about possible cable lengths for the temperature sensor. I agree it’s a matter of controller timing on the one hand and a matter of reflections in a long cable on the other. The sensor software uses bit banging with some µs timing. Hence the power up of the sensor running in parasite power mode is important. This is reflecting in file module_watch.c. 

The prototype module box has three connectors: 

  • BNC connector for temperature sensor and serial data 
  • Power supply connector 
  • Irrigation valve connector 

The external power supply is adjusted to the irrigation water valve operation voltage of 24 VAC. Vcc for the microcontroller logic is generated by a 4 x 1N4001 bridge rectifier with 220µF capacitor and an az-delivery LM2596S DC-DC step down module[8] (see attachment file protoype-inside.jpg). 


BOM

C1         100nF
C2         100µF
D1         1N4148
D2         LED - optical irrigation control
J1          Conn_01x02_Male - power supply
J2          Conn_01x02_Male - temperature sensor
J3          Conn_01x02_Male - irrigation valve
K1          ZETTLER-AZ943
Q1         BC559
Q2         BC559
R1         10k
R2         4k7
R3         6k8
R4         47
SW1     UP – pushbutton
SW2     DOWN – pushbutton
SW3     SET – pushbutton
U1        ATtiny85-20PU
U2        az-delivery 4 Digit 7 Segment Display [5]
U3        DS18S20 temperature sensor [6]

Placement of components see attachment file prototype-pcb.jpg.

The three pins male connector located below the Attiny85 socket are test pins (right to left) for GND, sensor parasite power and sensor DIO.


Software

The software is set up as a state machine - see attachment file state-machine.png.

On initial reset, when date and time as well as threshold temperature values are unset, the state machine executes date/time setting and threshold temperature setting before it enters the watch state.

As shown in the hardware section (attachment file module-box-front.jpg) there are three push buttons available for the user interface: UP, DOWN and SET

  • Push UP or DOWN button to step through the available options
  • Push SET button to select an option or to show current temperature in watch mode
  • Long push SET button
    • to interrupt menu mode or any sub mode to return to watch mode
    • to interrupt watch mode to get to menu mode

The state machine is controlled by a 100[ms] period timer interrupt. The timer interrupt and some more
initializations are performed in the main() function finally going to sleep in sleep mode IDLE (see [4] page 34, Sleep Modes).

The timer interrupt service routine has several roles:
  • scan push button input (key)
  • 400[ms] period display blink function
  • 1000[ms] period time stamp ticker
  • mode dispatcher

Each mode function is called by the mode dispatcher passing the current key code as argument and returns from execution within the 100[ms] period.

The mode dispatcher is controlled by globals.mode variable. Any mode function may manipulate the variable globals.mode. Within each mode function the different states of a mode function is reflected in a variable globals.submode. On globals.submode == SUBMODE_EXIT any mode function does set the globals.mode variable to the next mode and clears the globals.submode variable.

None of the mode functions has any loop construction. All loop-alike constructions are realized as counters depending on the 100[ms] timer interrupt period. No polling loops are implemented.

File list (all in attachment file FrostGuard.pdf):
  • frostguard.c / frostguard.h main() function and major definitions
  • globals.c / globals.h global variables
  • mode_brightness.c mode for brightness control
  • mode_data.c mode for data transfer
  • mode_datetime.c mode for date and time setting
  • mode_irrigate.c mode for irrigation test
  • mode_menu.c menu mode
  • mode_temp.c mode for threshold temperatures setting
  • mode_watch.c watch mode
  • ds18x20.c / ds18x20.h temperature sensor control
  • tm1637.c / tm1637.h display and push buttons control
  • uart.c / uart.h serial TTL output control  

Let’s have a look at some of the source code files.

The temperature sensor code resides in files ds18x20.h and ds18x20.c. It’s taken from Davide Gironi. My modifications are related to support both DS18B20 and DS18S20 sensor types.

File ds18x20.h contains the sensor type definition for my project: 

/* ----------------- sensor definition section -----------------
*
* Type of sensor - set to DS18B10 or DS18S10
*/
#define DS18x20_TYPE DS18S10
/*
* Sensor resolution - set to 9, 10, 11 or 12 for DS18B20
* (no effect for DS18S20)
*/
#define DS18x20_RESOL 9
/*
* Operation mode - set to
* - 0 for no parasite-powered operation
* - 1 for parasite-power on output high
* - 2 for parasite-power on output low
*/
#define DS18x20_PARAPWR 2
/*
* connection setup
*/
#define DS18x20_PORT PORTB
#define DS18x20_DDR DDRB
#define DS18x20_PIN PINB
#define DS18x20_PWR PB4
#define DS18x20_DQ PB3 

File ds18x20.h further defines the sensor functions, sensor macros (sensor type dependent), some specific return value and sensor commands. 

Davide’s original sensor lib provides one function for temperature sensor request: 
 
int16_t DS18x20_gettemp(); 

This function starts the temperature conversion and then polls for the “ready” signal of the sensor. Next it reads and returns the sensor data. This is a synchronous mode of retrieving the sensor data. Which is not applicable in my project (no poll loops).

For asynchronous retrieval of temperature sensor data the function DS18x20_gettemp() is split up in an initialization part (function DS18x20_startcv()) and a sensor data retrieval part (function DS18x20_readtemp()). To distinguish from successful operation and failure there are specific return values defined in file ds18x20.h: 

/* 
 * return values (int16_t) 
 * 
 * 0x07D0...0xFC90 -> +125[°]...-55[°] on 12 Bit resolution 
 * 0x00AA...0xFF92 ->  +85[°]...-55[°] on  9 Bit resolution 
 */ 
#define DS18x20_NO_VALUE     (DS18x20_MAX + 1)   // ok - no value sampled 
#define DS18x20_NO_RESET     (DS18x20_MAX + 2)   // error - no sensor reset 
#define DS18x20_NO_DATA      (DS18x20_MAX + 3)   // error - no sensor data

The code calling functions DS18x20_startcv() and DS18x20_readtemp() must comply to the conversion times between calling the two functions. Conversion times are taken from the DS18B20 / DS18S20 data sheets [6, 7] and defined depending on sensor type and sensor resolution (all in [ms]): 
 
#if DS18x20_TYPE == DS18S10 
#  define DS18x20_CVT 750 
#else 
#  if DS18x20_RESOL == 9 
#    define DS18x20_CVT 94 
#  endif 
#  if DS18x20_RESOL == 10 
#    define DS18x20_CVT 188 
#  endif 
#  if DS18x20_RESOL == 11 
#    define DS18x20_CVT 375 
#  endif 
#  if DS18x20_RESOL == 12 
#    define DS18x20_CVT 750 
#  endif 
#endif // DS18x20__TYPE == DS18S10 

The conversion time is defined in file frostguard.h as all other time constants are. 

CONVERSION_TIME is aligned to the 100[ms] system tick. 
 
/** 
 * timer ISR(TIM0_COMPA_vect)@100[ms] related stuff 
 */ 
#define CONVERSION_TIME (((DS18x20_CVT / 100) * 100) < DS18x20_CVT \
   ? (DS18x20_CVT / 100) + 1 \
   : (DS18x20_CVT / 100)) 
 
#define ONE_SECOND    10 
#define TEN_SECONDS   100 
#define THIRTY_SECONDS 300 
#define SIXTY_SECONDS 600 

Modes are defined as bit vector values so the code does not need to combine multiple if requests when there is defined an operation for multiple modes.
 
/** 
 * modes of the state machine 
 */ 
#define MODE_UNSET    0 // no mode set 
#define MODE_RESET  _BV(0)  // power on 
#define MODE_TEMPS  _BV(1)  // set temperatures 
#define MODE_DATIME _BV(2)  // set date/time 
#define MODE_WATCH  _BV(3)  // watch temperature 
#define MODE_MENU   _BV(4)  // menu 
#define MODE_IRRIG  _BV(5)  // manual irrigation 
#define MODE_BRIGHT _BV(6)  // set brightness 
#define MODE_DATA   _BV(7)  // retrieve irrigation data 
 
#define SUBMODE_EXIT   99    // submode: exit mode 

Each mode function returns a mode status value telling the mode dispatcher if there is some wrap up to perform (see end of timer interrupt service routine in file frostguard.c). 
 
/* 
 * mode function status return codes 
 */ 
#define MDS_RUN     0 
#define MDS_DONE    1 

Key scanning in the timer interrupt service routine supplies one of the key codes: 
 
/** 
 * key codes from TM1637_keyscan() 
 */ 
#define KEY_NONE     0xFF    // all keys released 
#define KEY_UP      0x07    // key UP hit 
#define KEY_DOWN     0x06    // key DOWN hit 
#define KEY_SET     0x05    // key SET hit 
#define KEY_UP_L     0x77    // key UP long hold 
#define KEY_DOWN_L  0x66    // key DOWN long hold 
#define KEY_SET_L   0x55    // key SET long hold 

The display may have one of three different states: 
 
/** 
 * display status 
 */ 
#define DSP_OFF     0 
#define DSP_ON      1 
#define DSP_BLINK   2 

Irrigation is controlled by Port B pin 2 and R3 / Q2 / K1: 
 
/** 
 * port B pin 2 used for irrigation relay control (low = on) 
 */ 
#define IRRI_INIT() (DDRB |= _BV(DDB2)) 
#define IRRI_ON()   (PORTB &= ~_BV(PB2)) 
#define IRRI_OFF()  (PORTB |= _BV(PB2)) 

The display can operate in eight brightness steps: 
 
/** 
 * brightness (0...TM1637_BRIGHTNESS_MAX) 
 */ 
#define DEFAULT_BRIGHTNESS  5 
#define MAX_BRIGHTNESS      7 

According to the resolution of 0.5[°] the binary representation is calculated: 
 
/** 
 * binary temperature (0.5[°] resolution) 
 */ 
#define BINTEMP(x)  (x * 2) 

File globals.h defines the data types used in all global variables. 

typedef struct 
{ 
    int8_t  low; 
    int8_t  high; 
    
} temperatures_t; 
 
typedef struct params   // runtime parameters / copy in eeprom 
{ 
    temperatures_t  temperatures;   // threshold temperatures 
    temperatures_t  minmax;         // min/max temperatures 
    uint32_t        timestamp;      // reference January 1st 1970 00:00:00 in [s] 
    uint8_t         brightness; 
    int8_t          write;          // event data eeprom write index 
        
} params_t; 

Date and time format is the Unix based timestamp. Initial value is 2021-04-05 12:00:00. 
 
#define DT_2021_4_5_12_0_0 ((((uint32_t)(2021 - 1970) * 365 \
    + (uint32_t)((2021 - 1968) / 4) + (31 + 28 + 31 + 3)) * 24 + 12) * 60 * 60)

Each irrigation event is recorded. An irrigation event is defined as the change of irrigation mode from 0 (no irrigation) to 1 (permanent irrigation), 2 or more and back to 0. To each irrigation event the time and (binary) temperature is recorded. 
 
 
typedef struct      // irrigation event data 
{ 
    uint32_t    timestamp;  // 1[s] resolution timestamp since 1970-01-01 00:00:00 
    int8_t      temp;       // binary temperature 0.5[°] resolution 
    uint8_t     irri_mode;  // irrigation mode 
    
} event_t;

All global variables are stored in a structure containing the runtime parameters, mode and submode value as well as display status values. 
 
/** 
 * globals 
 */ 
typedef struct 
{ 
    params_t     params; 
    uint8_t     mode; 
    uint8_t     submode; 
    uint8_t     blinker; 
    uint8_t     col_stat;   // colon status off/on/blinking 
    uint8_t     dsp_stat;   // display status off/on/blinking 
    
} globals_t; 
 
extern globals_t globals; 

All messages shown in the display are defined here. The corresponding binary values are defined in file globals.c. If you want to modify the code, please keep the menu entries in the original order as the first ones. They correspond to a mode array in file mode_menu.c. 
 
/** 
 * display messages for menus et.al. 
 */ 
extern const uint8_t messages[]; 
// menu messages definitions - keep at begin and in order (see mode_menu.c) 
#define MSG_dAtA     ((uint8_t *)(messages +  0)) 
#define MSG_irri     ((uint8_t *)(messages +  4)) 
#define MSG_bri      ((uint8_t *)(messages +  8)) 
#define MSG_tEnP     ((uint8_t *)(messages + 12)) 
#define MSG_dAtE     ((uint8_t *)(messages + 16)) 
// end of menu messages - other messages 
#define MSG_SEnd     ((uint8_t *)(messages + 20)) 
#define MSG_on       ((uint8_t *)(messages + 24)) 
#define MSG_oFF      ((uint8_t *)(messages + 28)) 
#define MSG_CLr      ((uint8_t *)(messages + 32)) 
#define MSG_rEt      ((uint8_t *)(messages + 36)) 
#define MSG_no_d     ((uint8_t *)(messages + 40)) 
#define MSG_no_r     ((uint8_t *)(messages + 44)) 

With a little bit of phantasy, it is possible to display all the message words with a seven segments display (see attachment file messages.png).
 
The EEPROM of the ATTiny85 controller is used to store the program parameters and the recorded irrigation events. The number of irrigation events is limited by the EEPROM data size. It’s taken from the E2END constant from include file avr/eeprom.h. 

/** 
 * eeprom data 
 */ 
#define EEPROM_SIZE (E2END + 1) 
#define MAX_EVENTS  ((EEPROM_SIZE - sizeof(params_t) - sizeof(int8_t)) \
                      / sizeof(event_t)) 
 
#define EEUNSET 0xFF // eeprom data unset 
 
typedef struct 
{ 
    params_t    params; 
    event_t     events[MAX_EVENTS]; 
 
} eedata_t; 
 
extern eedata_t EEMEM eedata; 

File globals.c holds the globals, the EEPROM data and the message binary codes array.

Next let’s have a look at the menu mode function defined in file mode_menu.c. As shown at the beginning of the Software section the menu mode lets the user select between different functionalities: 
 
  • data transfer 
  • date and time set up 
  • temperature threshold set up 
  • display brightness set up 
  • irrigation test 

The variable globals.submode is used to hold the selected menu item. It is modified by keys UP or DOWN. On SET key the selected mode is set into variable globals.mode, globals.submode cleared and MDS_DONE returned. On the next timer interrupt the mode dispatcher calls the mode function corresponding to the selected mode. 
 
 
/** 
 * menu mode 
 * 
 * - KEY_UP/KEY_DOWN -> incr/decr menu item 
 * - KEY_SET -> set selected menu item into globals.mode, leave 
 * - KEY-SET_L -> set MODE_WATCH into globals.mode, leave 
 * 
 * const uint8_t messages[] in globals.c holds the 4 digit messages for the menu entries 
 * 
 * resulting message pointers are defined in globals.h 
 * 
 * keep order in array next[] as index in next[] is proportional to index in messages[] 
 */ 
static const uint8_t next[] = { MODE_DATA, MODE_IRRIG, MODE_BRIGHT, MODE_TEMPS, MODE_DATIME }; 
#define MAX_NEXT (sizeof(next) - 1) 
 
uint8_t mode_menu(uint8_t key) 
{ 
    uint8_t rc = MDS_RUN; 
    register uint8_t sm = globals.submode; 
 
    if (globals.submode == SUBMODE_EXIT) { 
        globals.mode = MODE_WATCH; 
        globals.submode = 0; 
        globals.dsp_stat = DSP_OFF; 
        rc = MDS_DONE; 
    } else { 
        globals.dsp_stat = DSP_ON; 
        TM1637_display_msg(messages + 4 * sm); 
 
        if (key == KEY_UP ||key == KEY_DOWN) { 
           sm += key == KEY_UP ? (sm == MAX_NEXT ? -MAX_NEXT : 1 )
                               : (sm == 0 ? MAX_NEXT : -1); 
        } else if (key == KEY_SET) { 
           globals.mode = next[globals.submode]; 
           sm = 0; 
           rc = MDS_DONE; 
        } 
        globals.submode = sm; 
    } 
    return rc; 
} 

 Please note the “round robin” feature of menu items:  
sm += key == KEY_UP ? (sm == MAX_NEXT ? -MAX_NEXT : 1 ) : (sm == 0 ? MAX_NEXT : -1); 

Such you’ll find in other mode files evaluating keys UP and DOWN. As an example, let’s look at file mode_temp.c

uint8_t mode_temperatures(uint8_t key) 
{ 
    uint8_t rc = MDS_RUN; 
    uint8_t dir; 
    
    switch (globals.submode) { 

Submode 0 is used to set up this function. The display shows a leading “H” (for High threshold temperature) and the currently set high threshold temperature. Next the submode is set to 1.  

       case 0: 
           globals.dsp_stat = DSP_BLINK; 
           displayTemp((int)globals.params.temperatures.high); 
           TM1637_display_digit(0, _DSP_H); 
           globals.submode++; 
           break; 

On the next execution of this function the current key code is evaluated. On key UP/DOWN the display blinking stops and the high threshold temperature value is increased/decreased within the limits of 0[°C] <= high temp <= 20[°C] in 0.5° steps (as this is the temperature sensor resolution). In this function there is no “round robin” implemented for the threshold temperatures settings. On key SET the submode is increased to 2. 

       case 1: 
           if (key == KEY_UP || key == KEY_DOWN) { 
               globals.dsp_stat = DSP_ON; 
               dir = key == KEY_UP ? (globals.params.temperatures.high < 20 ? 1 : 0) 
                                   : (globals.params.temperatures.high > 0 ? -1 : 0); 
               if (dir != 0) { 
                   globals.params.temperatures.high += dir; 
                   displayTemp((int)globals.params.temperatures.high); // DSP_ON 
                   TM1637_display_digit(0, _DSP_H); 
               } 
           } else if (key == KEY_SET) { 
               globals.submode++; 
           } 
           break;   

Submode 2 shows the current low threshold temperature with a leading “L” in the display, blinking, and sets the submode to 3. 

       case 2: 
           globals.dsp_stat = DSP_BLINK; 
           displayTemp((int)globals.params.temperatures.low); 
           TM1637_display_digit(0, _DSP_L); 
           globals.submode++; 
           break;

On the next execution of this function again the current key code is evaluated. On key UP/DOWN the display blinking stops and the low threshold temperature value is increased/decreased within the limits of 0[°C] <= high temp <= 20[°C] in 0.5° (again no “round robin”). On key SET the submode is set to SUBMODE_EXIT. 
 
       case 3: 
           if (key == KEY_UP || key == KEY_DOWN) { 
               globals.dsp_stat = DSP_ON; 
               dir = key == KEY_UP ? (globals.params.temperatures.low < 20 ? 1 : 0)
                                   : (globals.params.temperatures.low > 0 ? -1 : 0); 
               if (dir != 0) { 
                   globals.params.temperatures.low += dir; 
                   displayTemp((int)globals.params.temperatures.low); // DSP_ON 
                   TM1637_display_digit(0, _DSP_L); 
               } 
           } else if (key == KEY_SET) { 
               globals.submode = SUBMODE_EXIT; 
           } 
           break; 

On the next execution this function performs all wrap up. 

       case SUBMODE_EXIT: 
           globals.dsp_stat = DSP_OFF; 
           globals.mode = MODE_WATCH; 
           globals.submode = 0; 
           rc = MDS_DONE; 
           break; 
    } 
    return rc; 
} 

Similar functionality is given by the mode functions in files mode_datetime.c, mode_brightness.c and mode_irrigate.c.

The file mode_data.c holds the mode function for the data transfer. Data is dumped @19200 Baud in an ascii JSON pretty print format,  good for human reading and interpretation. 

Here’s an excerpt of logged data transfer from 2021-04-09. 

Members “tH” and “tL” represent the threshold temperature parameters. 

Members “mH” and “mL” represent the measured maximum and minimum temperatures. 

The irrigation event data is transferred in an array. Each array entry has the members 

  • n             entry number 
  • ts            time stamp 
  • tm          temperature of irrigation event 
  • im           irrigation mode 

{ 
  "tH": 3.0, 
  "tL": 1.0, 
  "mH": 19.5, 
  "mL": -0.5, 
  "ev": [{ 
    "n": 0, 
    "ts": "2021-04-08 23:56:50", 
    "tm": 1.0, 
    "im": 1 
  },{ 
    "n": 1, 
    "ts": "2021-04-08 23:59:21", 
    "tm": 1.5, 
    "im": 2 
  }, 
... 
  { 
    "n": 59, 
    "ts": "2021-04-09 05:54:42", 
    "tm": -0.5, 
    "im": 1 
  },{ 
    "n": 60, 
    "ts": "2021-04-09 07:25:16", 
    "tm": 1.5, 
    "im": 2 
  }, 
... 
  { 
    "n": 75, 
    "ts": "2021-04-09 08:22:20", 
    "tm": 3.0, 
    "im": 5 
  },{ 
    "n": 76, 
    "ts": "2021-04-09 08:29:55", 
    "tm": 3.5, 
    "im": 0 
  }] 
} 

 Event data shows 
 
  • temperature did fall at 1.0[°C] on 2021-04-08 23:56:50, irrigation starts at mode 1 
  • at 23:59:21 the temperature raised at 1.5[°C], irrigation changed to mode 2 
  • lowest temperature was next day at 05:54:42 (-0.5[°C]) 
  • high threshold temperature (3.0[°C]) was reached at 08:22:20 with mode 5 
  • irrigation stopped at 08:29:55 (mode 0 at 3.5[°C]) 

The data transfer utilizes the serial to TTL functions uart_tx() and uart_tx_string() in file uart.c

The base function uart_tx() uses bit-banging at 19.200 Baud having 52,1[µs] bit time. 

void uart_tx(register char data) 
{ 
    register uint8_t bit = _BV(0); 
    register uint8_t pb; 
 
    UART_TXDRR |= _BV(UART_TXBIT);   // out 
    UART_TXPORT &= ~_BV(UART_TXBIT); // start bit 
    _delay_us(42); 
    while (bit) { 
        pb = UART_TXPORT; 
        if (data & bit) 
            pb |= _BV(UART_TXBIT); 
        else pb &= ~_BV(UART_TXBIT); 
        UART_TXPORT = pb; 
        _delay_us(41); 
        bit <<= 1; 
    } 
    _delay_us(7);   // compensation for end of while - last bit 
    
    UART_TXPORT |= _BV(UART_TXBIT);  // stop bit 
    _delay_us(40); 
    UART_TXDRR &= ~_BV(UART_TXBIT);  // in 
}   

The delay times are adjusted to the 52,1[µs] bit time. In real the bit time is 52[µs], which is close enough to operate at a wide temperature range on data transfer. My first implementation was at 57.600 Baud having 17,4[µs] bit time. This worked fine in my heated dev shack but was unreliable at low temperatures in my unheated garden cabin.
 
Eventually let’s have a look at file mode_watch.c containing the watch mode workhorse function. 

The function has the three sub modes 0 (for initialization), SUBMODE_EXIT (for exit wrap up) and 1 (for regular operation). 

On key SET hit the last sampled temperature is displayed. Display time is controlled by a counter variable “display”. On value zero the display is off. On values 1 to TEN_SECONDS the display is on. 

The measure (sample) cycle is controlled by a counter variable “measure_count” having initial value zero. On value 0 the temperature sensor is powered up (parasite power mode!) by setting DS18x20_PWRON(). After 2 cycles (value of measure_count is 2) the parasite power is set off and the conversion started. After CONVERSION_TIME + 2 cycles the sensor value is picked and (in case of a meaningful value) the irrigation mode is calculated. 

From the calculated irrigation mode, the pulse irrigation is controlled by a variable “pulse_timer” (initial value: 0) and an irrigation variable “irri_timer” (initial value: 0).  

The variable “irri_timer” is the timer counter for the irrigation and pause phases, set if value 0: 

  • set to SIXTY_SECONDS at the beginning of each irrigation phase, indicated by variable “pulse_timer” == 1 
  • set to THIRTY_SECONDS at the beginning of each irrigation pause, indicated by variable “pulse_timer” > 1 
  • variable “irri_timer” is decremented on each function call (all 100[ms]) if value > 0 

The variable “puls_timer” reflects the irrigation mode: 

  • variable “pulse-timer” is set to 0 (irrigation off) on irrigation mode 0 (off) 
  • it’s set to 1 on irrigation mode 1 (start of irrigation) 
  • on change of irrigation the variable “pulse_timer” is set to the irrigation mode value when variable “pulse_timer” is having value 1 
  • variable “pulse-timer” is decremented on each function call (all 100[ms]) if value of variable “irri_timer” reaches 0 

By this the pulse irrigation is controlled by irrigation mode: 

  • 0 - off 
  • 1 - constant on 
  • 2 - 60[s] on /       30[s] off 
  • 3 - 60[s] on / 2 x 30[s] off 
  • 4 - 60[s] on / 3 x 30[s] off 
  • and so on  


uint8_t mode_watch(uint8_t key) 
{ 
    uint8_t rc = MDS_RUN; 
 
    if (globals.submode == 0) { 
        /* 
         * init MODE_WATCH 
         */ 
        measure_count = 0; 
        display_count = 1; 
        globals.dsp_stat = DSP_ON; 
        globals.col_stat = DSP_OFF; 
        TM1637_clear(); 
        globals.submode = 1; 
        
    } else if (globals.submode == SUBMODE_EXIT) { 
        /* 
         * end MODE_WATCH -> MODE_MENU 
         */ 
        globals.mode = MODE_MENU; 
        globals.submode = 0; 
        globals.col_stat = DSP_OFF; 
        irri_mode = 0; 
        IRRI_OFF(); 
        rc = MDS_DONE; 
        
    } else { // globals.submode == 1 
        if (key == KEY_SET) { 
           display_count = 1; 
        } 
        /* 
         * temperature measurement 
         */ 
        switch (measure_count) { 
           case 0: 
               globals.col_stat = DSP_ON; 
               DS18x20_PWRINIT(); 
               DS18x20_PWRON(); // give sensor 200[ms] power 
               measure_count++; 
               break; 
               
           case 2: 
               DS18x20_PWROFF(); 
               temp = DS18x20_startcv(); 
               measure_count = temp == DS18x20_NO_RESET ? 0 : (measure_count + 1); 
               break; 
 
           case CONVERSION_TIME + 2: 
               temp = DS18x20_readtemp(); 
               if (   temp == DS18x20_NO_DATA 
                   || temp < BINTEMP(-20.0) || temp >= BINTEMP(40.0)) { 
                   measure_count = 0; 
               } else { 
                   /* 
                    * calculate irrigation mode from temperature 
                    */ 
                   if ((int8_t)temp > globals.params.temperatures.high) { 
                     // stop irrigation & pulse timer 
                     irri_mode = pulse_timer = 0; 
                   } else if ((int8_t)temp <= globals.params.temperatures.low) { 
                     // start irrigation & pulse timer 
                     irri_mode = pulse_timer = 1; 
                   } else if (irri_mode >= 1) { 
                     irri_mode = (int8_t)temp - globals.params.temperatures.low + 1; 
                   } 
                   store_event(temp, irri_mode); 
                   globals.col_stat = DSP_OFF; 
                   measure_count++; 
               } 
               break; 
 
           case TEN_SECONDS: 
               measure_count = 0; 
               break; 
 
           default: 
               measure_count++; 
               break; 
        } 
        /* 
         * show temp (or error) if applicable 
         */ 
        if (temp > DS18x20_NO_VALUE) {      // error 
           globals.col_stat = DSP_OFF; 
           display_count = 1; 
        } else { 
           /* 
            *  pulse irrigation from irri_mode 
            * 
            *   0: off 
            *   1: constant on 
            *   2: 60[s] on /     30[s] off 
            *   3: 60[s] on / 2 x 30[s] off 
            *   4: 60[s] on / 3 x 30[s] off 
            *   ... 
            */ 
           if (pulse_timer == 0) { 
               // stop pulse timer 
               IRRI_OFF(); 
               irri_timer = 0; 
           } else if (pulse_timer == 1) { 
               // on phase 
               if (irri_timer == 0) { 
                   IRRI_ON(); 
                   irri_timer = SIXTY_SECONDS; 
               } 
               pulse_timer = irri_mode; 
           } else { 
                // off phase 
               if (irri_timer == 0) { 
                   IRRI_OFF(); 
                   irri_timer = THIRTY_SECONDS; 
               } 
           } 
           if (irri_timer > 0) { 
               if (--irri_timer == 0) { 
                   if (pulse_timer == 1) { 
                     pulse_timer = irri_mode; 
                   } else { 
                     pulse_timer--; 
                   } 
               } 
           } 
        } 
        if (display_count > TEN_SECONDS) { 
           display_count = 0; 
           TM1637_clear(); 
        } else if (display_count > 0) { 
           displayTemp(temp); 
           display_count++; 
        } 
    } 
    return rc; 
} 


User manual

The project documentation in attachment file  FrostGuard.pdf contains a brief user manual section.


References

[1]  Circular sprinkler
       https://www.amazon.de/-/en/Circular-sprinkler-different-surfaces-securely/dp/B086FWFNRX?th=1

[2]  Android USB terminal software
       https://play.google.com/store/apps/details?id=de.kai_morich.serial_usb_terminal

[3]  FT232RL USB to TTL serial adapter
       https://www.az-delivery.de/en/products/ftdi-adapter-ft232rl

[4]  Atmel ATtiny25, ATtiny45, ATtiny85 Datasheet
       https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-2586-AVR-8-bit-Microcontroller-ATtiny25-ATtiny45-ATtiny85_Datasheet.pdf

[5]  7 segments 4 digits display from az-delivery
       https://www.az-delivery.de/en/collections/displays/products/4-digit-display

[6]  DS18S20 temperature sensor data sheet
       https://datasheets.maximintegrated.com/en/ds/DS18S20.pdf

[7]  DS18B20 temperature sensor data sheet
       https://datasheets.maximintegrated.com/en/ds/DS18B20.pdf

[8]  az-delivery LM2596S DC-DC step down module
       https://www.az-delivery.de/en/products/lm2596s-dc-dc-step-down-modul-1