Plant Watering Indicator (Part 2)

      1 Comment on Plant Watering Indicator (Part 2)

In part 1 of that post, I showed the PCB design for that project and how I produced it.

This design relies on an Attiny13a:
Capture d’écran 2015-11-16 à 21.01.20

You will see that the following code is not done using regular Arduino functions. I used the registers directly instead. The reason for that is that you have only 1KB of space to store the program on an Attiny 13A. By using that coding style, I only used 2/3 of the available program space.

Just as a reminder, the PCB design assumes that:
– Pin PB0 is used for the blue LED
– Pin PB1 is used for the red LED
– Pin PB3 is used to power the sensor
– Pin ADC2 (same as PB4) is used to read the sensor value

Capture d’écran 2015-11-15 à 16.54.07

So, in the setup function, there are 2 things to do:
– Define PB0, PB1 & PB3 as outputs
– Configure Analog Digital Converter (ADC) so we can read analog values from ADC2 (which is connected to the sensor output).

The rest of the ADC configuration is explained using comments in the code.

#define BLUE_LED_PIN PB0
#define RED_LED_PIN PB1
#define SENSOR_POWER_PIN PB3

...

void setup() {
  DDRB = 0b001011;  // pins PB0, PB1 & PB3 are outputs
  adc_setup();
}

void adc_setup() {
   ADMUX |= (1 << MUX1);    // Reading from ADC2
   ADMUX |= (1 << ADLAR);   // Result left adjusted
   ADCSRA |= (1 << ADPS2);  // Divide clock by 16 (1.2MHz / 16 = 75Hz)
   ADCSRA |= (1 << ADEN);   // Enable ADC
}

The main loop doesn't have much to do. It basically just prepares the micro controller to go to sleep mode and then sends it to sleep. Our code that reads the sensor and turns on/off the LEDs will be coded in an interrupt function that will be triggered by the controller watch dog. We tell the watch dog to call that interrupt every 8 seconds and, once we are done with our "business logic", we send the micro controller back to sleep.

void loop() {
   prepareToSleep();
   while(true) {
      sleep_mode();
   }
}

// Configures the watch dog timer
void prepareToSleep() {
   WDTCR  |=  (1 << WDP3) | (1 << WDP0); // Prescale timer to 8s
   WDTCR  |=  (1 << WDTIE);           // Enable watchdog timer interrupts
   sei();                                // Enable global interrupts
   set_sleep_mode(SLEEP_MODE_PWR_DOWN);  // Use the Power Down sleep mode
}

The interrupt functions are having a standard name: ISR().
What differentiates the interrupt functions is the parameters they receive. Here, we care about interrupts from the Watch Dog, so that function receives parameters of type WDT_vect.

The interrupt function starts by flashing the red LED for 150 milliseconds. It is there simply to show that the whole system is working and that the batteries are not dead.

Then, it checks if it is time to read the sensor. You may not want to read the sensor every 8 seconds (8 seconds is the maximum timeout value we can configure). So, lets say you want to read the sensor every 80 seconds. You will then set the constant SENSOR_READ_DELAY_CALLS to 10, so 8 seconds X 10 calls will have the application reading the sensor every 80 seconds.

Finally, the interrupt function displays the moisture level.

const int SENSOR_READ_DELAY_CALLS = 5 * 60 / 8;  // 5 minutes / 8 seconds per timeout 
volatile int nbCalls = 0;
volatile int sensorReading = 255;  // Last sensor reading

...

// This function is called everytime the watchdog delay is reached
ISR(WDT_vect) {
   flashRedLED();  // Flash red LED to show still alive
   
   if(nbCalls == SENSOR_READ_DELAY_CALLS) {
     sensorReading = readMoistureSensor();  // Read sensor
     nbCalls = 0;
   }
 
   nbCalls++;
   
   displayMoistureLevel(sensorReading);  // Turn on Blue LED according to moisture level
}

The function flashing the red LED is defined as:

#define RED_LED_FLASH_TIME_IN_MILLIS 150

...

void flashRedLED() {
   PORTB |= (1 << RED_LED_PIN);
   delayF(RED_LED_FLASH_TIME_IN_MILLIS);
   PORTB ^= (1 << RED_LED_PIN);
}

Reading the sensor involves a few steps:
- Power on sensor
- Let sensor stabilize (about a second)
- Read sensor value using ADC
- Power off sensor

#define SENSOR_STABILIZATION_TIME_IN_MILLIS 1000

...

int readMoistureSensor() {
   PORTB |= (1 << SENSOR_POWER_PIN); // Turn on sensor
   delayF(SENSOR_STABILIZATION_TIME_IN_MILLIS);  // Let sensor stabilize
   int sensorReading = adc_read();   // Read sensor
   PORTB ^= (1 << SENSOR_POWER_PIN); // Turn off sensor
   return sensorReading;
}

int adc_read (void)
{
   // Start the conversion
   ADCSRA |= (1 << ADSC);

   // Wait for it to finish
   while (ADCSRA & (1 << ADSC));

   return ADCH;
}

What remains is to display the current soil humidity level. I decided to turn on the blue LED for a duration inversely proportional to the humidity level. The longer the LED is turned on, the lowest the humidity level. I experimented with my sensor to see what the readings would be for different moisture levels. In the end, anything under 150 means there is plenty of water.

void displayMoistureLevel(int sensorReading) {
   PORTB |= (1 << BLUE_LED_PIN);
   if(sensorReading < 150) {
      // More than enaugh
   }
   else if (sensorReading < 175) {
      delayF(250);    
   }
   else if (sensorReading < 200) {
      delayF(500);
   }
   else if (sensorReading < 225) {
      delayF(1000);
   }
   else {
      delayF(2000);
   }
   PORTB ^= (1 << BLUE_LED_PIN);
}

For some reasons, I was unable to obtain decent readings using the delay() function, so I created my own version that relies on the millis() function.

void delayF(long waitTimeMillis) {
   long end = millis() + waitTimeMillis;
   while (end > millis()) {
      ;
   }
}

If you look at the board design, you will see that I included an ISP header on the board. This is where I connect my UsbTinyIsp programmer to program the Attiny13A. I will make another post about that specifically.

Here is the whole code in one block:

#include < avr/interrupt.h >
#include < avr/sleep.h >
#include < avr/delay.h >
#include < avr/io.h >

#define BLUE_LED_PIN PB0
#define RED_LED_PIN PB1
#define SENSOR_POWER_PIN PB3
// WatchDog Timeout every 8 seconds and I want to read the sensor every 5 minutes
const int SENSOR_READ_DELAY_CALLS = 5 * 60 / 8;  
#define SENSOR_STABILIZATION_TIME_IN_MILLIS 1000
#define RED_LED_FLASH_TIME_IN_MILLIS 150

volatile int sensorReading = 255;
volatile int nbCalls = 0;

void setup() {
  DDRB = 0b001011;  // pins PB0, PB1 & PB3 are outputs
  adc_setup();
}

void adc_setup() {
   ADMUX |= (1 << MUX1);   // Reading from ADC2
   ADMUX |= (1 << ADLAR);  // Result left adjusted
   ADCSRA |= (1 << ADPS2); // Divide clock by 16 (1.2MHz / 16 = 75Hz)
   ADCSRA |= (1 << ADEN);  // Enable ADC
}

// Runs forever
void loop() {
   prepareToSleep();
   while(true) {
      sleep_mode();
   }
}

// Configures the watch dog timer
void prepareToSleep() {
   WDTCR |= (1 << WDP3) | (1 << WDP0); // Temporarily prescale timer to 8s
   WDTCR |= (1 << WDTIE);              // Enable watchdog timer interrupts
   sei();                              // Enable global interrupts
   set_sleep_mode(SLEEP_MODE_PWR_DOWN);// Use the Power Down sleep mode
}

// This function is called everytime the watchdog delay is reached
ISR(WDT_vect) {
   flashRedLED();  // Flash red LED to show still alive
   
   if(nbCalls == SENSOR_READ_DELAY_CALLS) {
     sensorReading = readMoistureSensor();  // Read sensor
     nbCalls = 0;
   }
 
   nbCalls++;
   
   displayMoistureLevel(sensorReading);  // Turn on Blue LED according to moisture level
}

void flashRedLED() {
   PORTB |= (1 << RED_LED_PIN);
   delayF(RED_LED_FLASH_TIME_IN_MILLIS);
   PORTB ^= (1 << RED_LED_PIN);
}

int readMoistureSensor() {
   PORTB |= (1 << SENSOR_POWER_PIN); // Turn on sensor
   delayF(SENSOR_STABILIZATION_TIME_IN_MILLIS);  // Let sensor stabilize
   int sensorReading = adc_read();   // Read sensor
   PORTB ^= (1 << SENSOR_POWER_PIN); // Turn off sensor
   return sensorReading;
}

int adc_read (void)
{
   // Start the conversion
   ADCSRA |= (1 << ADSC);

   // Wait for it to finish
   while (ADCSRA & (1 << ADSC));

   return ADCH;
}

void displayMoistureLevel(int sensorReading) {
   PORTB |= (1 << BLUE_LED_PIN);

   if(sensorReading < 150) {
      // More than enaugh
   }
   else if (sensorReading < 175) {
      delayF(250);    
   }
   else if (sensorReading < 200) {
      delayF(500);
   }
   else if (sensorReading < 225) {
      delayF(1000);
   }
   else {
      delayF(2000);
   }

   PORTB ^= (1 << BLUE_LED_PIN);
}

void delayF(long waitTimeMillis) {
   long end = millis() + waitTimeMillis;
   while (end > millis()) {
      ;
   }
}

One thought on “Plant Watering Indicator (Part 2)

  1. Pingback: Plant Watering Indicator (Part 1) | Francois' Corner

Leave a Reply

Your email address will not be published. Required fields are marked *

Comments Protected by WP-SpamShield Spam Plugin