Tuesday, February 28, 2012

Timer interrupts

Does your program seem like it’s trying to do too much at once? Are you using a lot of delay() or while() loops that are holding other things up? If so, your project is a good candidate to use timers. In this tutorial, we’ll discuss AVR and Arduino timers and how to use them to write better code.
In our prior article, we covered interrupt basics and how to use external interrupts that are triggered by a pin change or similar event. Check it out if you’re looking to brush up on interrupts in general.
This chapter moves on to timer interrupts and talks about their applications in Arduino projects or custom AVR circuits. Almost all Arduino boards are powered by AVR 8-bit processors, so to experience the full power of timers you’ll use the same techniques no matter which platform you’re on. Here’s the tutorial’s table of contents:

What is a timer?

You’re probably familiar with the general concept of a timer: something used to measure a given time interval. In microcontrollers, the idea is the same. You can set a timer to trigger an interrupt at a certain point in the future. When that point arrives, you can use the interrupt as an alert, run different code, or change a pin output. Think of it as an alarm clock for your processor.
The beauty of timers is that just like external interrupts, they run asynchronously, or independently from your main program. Rather than running a loop or repeatedly calling millis(), you can let a timer do that work for you while your code does other things.
For example, say you’re building a security robot. As it roams the halls, you want it to blink an LED every two seconds to let potential intruders know they’ll be vaporized if they make a wrong move. Using normal code techniques, you’d have to set a variable with the next time the LED should blink, then check constantly to see if that time had arrived. With a timer interrupt, you can set up the interrupt, then turn on the timer. Your LED will blink perfectly on cue, even while your main program executes its complicated terminateVillian() routine.

How do timers work?

Timers work by incrementing a counter variable, also known as a counter register. The counter register can count to a certain value, depending on its size. The timer increments this counter one step at a time until it reaches its maximum value, at which point the counter overflows, and resets back to zero. The timer normally sets a flag bit to let you know an overflow has occurred. You can check this flag manually, or you can also have the timer trigger an interrupt as soon as the flag is set. Like any other interrupt, you can specify an Interrupt Service Routine (ISR) to run code of your choice when the timer overflows. The ISR will reset the overflow flag behind the scenes, so using interrupts is usually your best option for simplicity and speed.
In order to increment the counter value at regular intervals, the timer must have access to a clock source.  The clock source generates a consistent repeating signal.  Every time the timer detects this signal, it increases its counter by one.
Because timers are dependent on the clock source, the smallest measurable unit of time will be the period of this clock.  For example, if we provide a 1 MHz clock signal to a timer, we can calculate our timer resolution (or timer period) as follows:
T = timer period, f = clock frequency

T = 1 / f
T = 1 / 1 MHz = 1 / 10^6 Hz
T = (1 * 10^-6) s


Our timer resolution is one millionth of a second. You can see how even relatively slow processors can break time into very small chunks using this method.


You can also supply an external clock source for use with timers, but in most cases the chip’s internal clock is used as the clock source. This means that your minimum timer resolution will be based on your processor speed (either 8 or 16 MHz for most 8-bit AVRs).


Types of timers

If you’re using any of the standard Arduino variants or an 8-bit AVR chip, you have several timers at your disposal. In this tutorial, we’ll assume you’re using a board powered by the AVR ATmega168 or ATmega328. This includes the Arduino Uno, Duemilanove, Mini, any of Sparkfun’s Pro series, and many similar designs. You can use the same techniques on other AVR processors like those in the Arduino Mega or Mega 2560, you’ll just have to adjust your pinout and check the datasheet for any differences in the details.


The ATmega168 and ATmega328 have three timers: Timer0, Timer1, and Timer2. They also have a watchdog timer, which can be used as a safeguard or a software reset mechanism. However, we don’t recommend messing with the watchdog timer until you get comfortable with the basics. Here are a few details about each timer:
Timer0
Timer0 is an 8-bit timer, meaning its counter register can record a maximum value of 255 (the same as an unsigned 8-bit byte). Timer0 is used by native Arduino timing functions such as delay() and millis(), so you Arduino users shouldn’t mess with it unless you’re comfortable with the consequences.
Timer1
Timer1 is a 16-bit timer, with a maximum counter value of 65535 (an unsigned 16-bit integer). The Arduino Servo library uses this timer, so be aware if you use it in your projects.
Timer2
Timer2 is an 8-bit timer that is very similar to Timer0. It is utilized by the Arduino tone() function.
Timer3, TIMER4, TIMER5

The AVR ATmega1280 and ATmega2560 (found in the Arduino Mega variants) have an additional three timers.  These are all 16-bit timers, and function similarly to Timer1.


Configuring and running the timer

In order to use these timers, we need to set them up, then make them start running.  To do this, we’ll use built-in registers on the AVR chip that store timer settings.  Each timer has a number of registers that do various things.  Two of these registers hold setup values, and are called TCCRxA and TCCRxB, where x is the timer number (TCCR1A and TCCR1B, etc.).  TCCR stands for Timer/Counter Control Register.  Each register holds 8 bits, and each bit stores a configuration value.  Here are the details, taken from the ATmega328 datasheet:

timerregisters

To start using our timer, the most important settings are the last three bits in TCCR1B, CS12, CS11, and CS10.  These dictate the timer clock setting.  By setting these bits in various combinations, we can tell the timer to run at different speeds.  Here’s the relevant table from the datasheet:

Clock Select bit description
CS12 CS11 CS10 Description
0 0 0 No clock source (Timer/Counter stopped)
0 0 1 clki/o/1 (No prescaling)
0 1 0 clki/o/8 (From Prescaler)
0 1 1 clki/o/64 (From Prescaler)
1 0 0 clki/o/256 (From Prescaler)
1 0 1 clki/o/1024 (From Prescaler)
1 1 0 External clock source on T1 pin. Clock on falling edge
1 1 1 External clock source on T1 pin. Clock on rising edge

By default, these bits are set to zero.  Let’s use a simple example, and say that we want to have Timer1 run at clock speed, with one count per clock cycle.  When it overflows, we’ll run an Interrupt Service Routine (ISR) that toggles a LED tied to pin 2 on or off. We’ll write Arduino code for this example, though we’ll use avr-libc routines wherever they don’t make things overly complicated. AVR pros can adapt as they see fit.

First, we initialize the timer:

// avr-libc library includes

#include <avr/io.h>

#include <avr/interrupt.h>

#define LEDPIN 2

void setup()

{

pinMode(LEDPIN, OUTPUT);

// initialize Timer1

cli();             // disable global interrupts

TCCR1A = 0;        // set entire TCCR1A register to 0

TCCR1B = 0;

// enable Timer1 overflow interrupt:

TIMSK1 = (1 << TOIE1);

// Set CS10 bit so timer runs at clock speed:

TCCR1B |= (1 << CS10);

// enable global interrupts:

sei();

}

You’ll notice that we used a new register, TIMSK1. This is the Timer/Counter1 Interrupt Mask Register. It controls which interrupts the timer can trigger. Setting the TOIE1 bit tells the timer to trigger an interrupt when the timer overflows. We can also set other bits to trigger other interrupts. More on that later.

Once we set the CS10 bit, the timer is running, and since we’ve enabled an overflow interrupt, it will call the ISR(TIMER1_OVF_vect) whenever the timer overflows.

Next, we can define the ISR:

ISR(TIMER1_OVF_vect)

{

digitalWrite(LEDPIN, !digitalRead(LEDPIN));

}

Now we’re free to define our loop() and our LED will toggle on and off regardless of what’s happening in the main program. To turn the timer off, we can set TCCR1B = 0 at any time.

However, let’s think about how this will work. Using the code we’ve written, how fast will our LED blink?

We’ve set Timer1 to interrupt on an overflow, and let’s assume we’re using an ATmega328 with a 16MHz clock. Since Timer1 is 16 bits, it can hold a maximum value of (2^16 – 1), or 65535. At 16MHz, we’ll go through one clock cycle every 1/(16*10^6) seconds, or 6.25e-8 s. That means 65535 timer counts will elapse in (65535 * 6.25e-8s) and our ISR will trigger in, oh… about 0.0041 seconds. Then again and again, every four thousandths of a second after that. Oops. At this rate, we probably won’t even be able to detect blinking. If anything, we’ve created an extremely fast PWM signal for the LED that’s running at a 50% duty cycle, so it may appear to be constantly on but dimmer than normal. An experiment like this shows the amazing power of microprocessors – even an inexpensive 8-bit chip can process information far faster than we can detect.


Timer prescaling and CTC

Luckily, the good engineers at Atmel thought of this problem, and included some options. It turns out you can also set the timer to use a prescaler, which allows you to divide your clock signal by various powers of two, thereby increasing your timer period.  For example, let’s say we’d rather have our LED blink at one second intervals. Going back to the TCCR1B register, we can use the three CS bits to set a better timer resolution.  If we set CS10 and CS12 using TCCR1B |= (1 << CS10); and TCCR1B |= (1 << CS12);, we divide our clock source by 1024. This gives us a timer resolution of 1/(16*10^6 / 1024), or 6.4e-5 seconds. Now the timer will overflow every (65535 * 6.4e-5s), or 4.194s. Hm, too long. What can we do?

It turns out there’s another mode of operation for AVR timers. This mode is called Clear Timer on Compare Match, or CTC. Instead of counting until an overflow occurs, the timer compares its count to a value that was previously stored in a register. When the count matches that value, the timer can either set a flag or trigger an interrupt, just like the overflow case.

To use CTC, let’s start by figuring out how many counts we need to get to our one second interval. Assuming we keep the 1024 prescaler as before, we’ll calculate as follows:

(target time) = (timer resolution) * (# timer counts + 1)

and rearrange to get

(# timer counts + 1) = (target time) / (timer resolution)
(# timer counts + 1) = (1 s) / (6.4e-5 s)
(# timer counts + 1) = 15625
(# timer counts) = 15625 - 1 = 15624

Why did we add the extra +1 to our number of timer counts? In CTC mode, when the timer matches our desired count it will reset itself to zero. This takes one clock cycle to perform, so we need to factor that into our calculations. In many cases, one timer tick isn’t a huge deal, but if you have a time-critical application it can make all the difference in the world.

Now we can rewrite our setup() function to configure the timer for these settings:

void setup()

{

pinMode(LEDPIN, OUTPUT);

// initialize Timer1

cli();          // disable global interrupts

TCCR1A = 0;     // set entire TCCR1A register to 0

TCCR1B = 0;     // same for TCCR1B

// set compare match register to desired timer count:

OCR1A = 15624;

// turn on CTC mode:

TCCR1B |= (1 << WGM12);

// Set CS10 and CS12 bits for 1024 prescaler:

TCCR1B |= (1 << CS10);

TCCR1B |= (1 << CS12);

// enable timer compare interrupt:

TIMSK1 |= (1 << OCIE1A);

sei();          // enable global interrupts

}

And we’ll need to replace our overflow ISR with a compare match version:

ISR(TIMER1_COMPA_vect)

{

digitalWrite(LEDPIN, !digitalRead(LEDPIN));

}

That’s all there is to it! Our LED will now blink on and off at precisely one second intervals. And as always, we’re free to do anything we want in loop(). As long as we don’t change the timer settings, it won’t interfere with our interrupts. With different mode and prescaler settings, there’s no limit to how you use timers.

Here’s the complete example in case you’d like to use it as a starting point for your own project. Double click to copy:

// Arduino timer CTC interrupt example

// www.engblaze.com

// avr-libc library includes

#include <avr/io.h>

#include <avr/interrupt.h>

#define LEDPIN 2

void setup()

{

pinMode(LEDPIN, OUTPUT);

// initialize Timer1

cli();          // disable global interrupts

TCCR1A = 0;     // set entire TCCR1A register to 0

TCCR1B = 0;     // same for TCCR1B

// set compare match register to desired timer count:

OCR1A = 15624;

// turn on CTC mode:

TCCR1B |= (1 << WGM12);

// Set CS10 and CS12 bits for 1024 prescaler:

TCCR1B |= (1 << CS10);

TCCR1B |= (1 << CS12);

// enable timer compare interrupt:

TIMSK1 |= (1 << OCIE1A);

// enable global interrupts:

sei();

}

void loop()

{

// do some crazy stuff while my LED keeps blinking

}

ISR(TIMER1_COMPA_vect)

{

digitalWrite(LEDPIN, !digitalRead(LEDPIN));

}



Going further

Keep in mind that you can use the built-in ISRs to extend timer functionality. For example, if you wanted to read a sensor every 10 seconds, there’s no timer setup that can go this long without overflowing. However, you can use the ISR to increment a counter variable in your program once per second, then read the sensor when the variable hits 10. Using the same CTC setup as in our previous example, our ISR would look something like this:

ISR(TIMER1_COMPA_vect)

{

seconds++;

if (seconds == 10)

{

seconds = 0;

readMySensor();

}

}

Note that in order for a variable to be modified within an ISR, it must be declared as volatile. In this case, we’d need to declare volatile byte seconds; or similar at the beginning of our program.

This tutorial covers the basics of timers. As you start to understand the underlying concepts, you’ll want to check the datasheet for more information on your particular chip. Datasheets are readily available on Atmel’s website. To find them, navigate to the page for your device (8-bit AVRs found here) or do a search for your chip model. There’s a lot of information to wade through, but the documentation is surprisingly readable if you have the patience.

Friday, February 24, 2012

ATtiny for your garden

Since I have a habit of being not home but still need my water gardened I was trying to find a way to do that. Ofcpourse there are the watering clocks like the Gardena or others that can be programmed to water the garden for a certain time ach day or each week and I have used thoes but the have two drawbacks:

They will water even if it is not necessary + I always feel a bit anxious to use those if I am away for a long time, just in case something goes wrong and the mains waterline will start spraying for days on end.

Therefore I decided to turn to a microcontroller. Ofcourse ‘Arduino’pops to mind but that seemed a bit like overkill for a simple task, but it seemed like a good idea to develop a system on the Arduino an then transfer it to an ATtiny45.

The idea was to let the Arduino measure moisture in my plantbed and then depending on a preset value for moisture or dryness, switch on a pump.

As the ATtiny has several pins that could be used as an output or input, I was thinking to find a use for the other pins a swell and I found the perfect solution. As the watering function will probably only be used in the summertime, after planting, I could use the device earlier alrady for heating up a propagator. yes I know there are heated propagators but I just could not find one in my local gardenshops.

First a bit of theory:

spanningsdelerIn this figure, the voltage Va is a function of the Voltage Vcc and the two resistors R1 and R2. The Current I flowing through R2 and R1 is Vcc/(R1+R2) and therefore the voltage over R1 and thus the voltage Va=I x R1.

If we subsitute I then the formula will be (Vcc/(R1+R2))xR1= Vcc*R1/(R1+R2)=Va.

With a Vcc of 5 V and both resistors being 10 k, the voltage Va therefore will be 2.5 V.

If R2 would be replaced by a variable resistor such as an NTC or a humidity sensor, the voltage over Va will vary dpending on the value of the temperature or the humidity and that voltage Va will subsequently be read by an analog port on the Attiny.

The temperature sensor

NTCThe temperature sensor is quite easy. a 10 k NTC will do the trick. It will come in the place of R2 so with a rising temperature the voltage on Va will rise as well.

The value of an NTC (e.g. 10k)  is usually given at an ambient temperature of 25 degrees Celsius. Therefore, at 25C the voltage on Va should be 1/2 Vcc.
Initially I had the intention to calculate the temperature with the Steinhart-Hart formula but as I only needed two temperatures, one to switch on the heating and one to switch off the heating in the propagator, it made more sense to just measure the Va at these specific temperature as this also takes into account a variation in the Vcc, in the resistor R1 and in the resistance of the leads to the NTC.
I wanted to keep the temperature between 21 and 26 degrees and you will find my values for that in the program. The values that you will need might be different. Mind that the Analogue ports will give you a reading between 0 and 1023. The Arduino can map this with the map command, but it is just as useful to divide by 4. The function that does this is the function ‘sample’. It will take the average of 5 readings and then divide that (‘map it’) by four.

The humidity sensor

Build-Your-Moisture-SensorThe humidity sensor is basically not more than 2 spikes driven into the ground. The citcuit is similar to that of the temperature reader, with the spikes taking the place of the NTC. In my experience, soil that is just sufficiently humid gives a resitence (with my humidity resistor) of  about 10 k. Also here if the soil dries out, the resistence of the sensor will increase and the voltage on the analogue port will rise. A specific dryness or wetness of the soil therefore corresponds with a certain value on the analogue port.
Construction
The construction of a humidity sensor is quite simple although there seem to be various ‘schools’ of thought. Some people willimbed their ‘spikes’ in plaster of paris to give a more even reading of moisture. Most people however will juet take two metal spikes that are inserted in the earth. There are however two things to remember: the spikes need to be attached to eachother, otherwise putting them in another position may cause the distance between the two spiks to cahnage and therefore also the resistance; the spikes need to be made of galvanized material will they stand any chance of surviving being in the soil. Galvanized nails are a popular material, but they ar ehard to solder. Th best way is to dile or sand the place where one wants to solder the wire. Wrap the wire around the spike and solder and then to top it off, pit some shrink wrap around it to ensure a good contact between the wire and the spike.

Metal spikes in the soil. especially if they have a DC current going through them, may corrode very quickly. In practice that has not bothered me that much, but there are some things that one could do to prevent corrosion: only put tension to the pins when the device is actually read. In that case the humidity sensor should not be connected to +Vcc but to digital pin 0 that can be switched on right before the reading and switched off  right after it. Another thing one can do –but in our example we are a pin short- is not to connect the pin and its pull down resistor to + V and earth, but to two digital output pinds that then will feed it with an AC current bij alternating the polarity of the two digital pins and only to read it when the proper pin is high.

The program

The program is fairly simple. First it starts with some descriptions and gives the lay-out for the Attiny. It then defines the pins and sets the variables.
It then reads the humidity sensor with the ‘sample’ function and stores the result in the variable ‘moist’. It compares the value for ‘moist’ with the value that is set for ‘irrigation’. If it is lower or equal to that value, it switches on the pump by setting the ‘pumpPin’ high.
If it is a level of 5 above that value, iyt switches the pump off.
This is done to avoid the pump switching on and off repeatedly and to make the soil wet enough. Depending on the flow of water in your particular set up, you may want to alter that value of ‘5’.
It then reads the temperature, again with the ‘sample’ function and stores the result in the variable ‘ntc’. It then sees if this value is less than ‘142’ (which is 21 degrees in my case)  and if so, it switches on the heating in my propagator. It then compares the value with ‘155’(which is 26 degrees in my case) and if so, switches off the heating.
as a heater I have used a 40 W lamp that is switched via a solidstate relais.

So just to summarize: I use this device in summer outside to water my plantbeds and in  early spring I use it inside to heat a propagator. The device therefore is never controlling both temperature and irrigation at the same time, but if you would use it in a greenhouse, ofcourse it could.
Although the irrigation could be done by switching an electric valve in the main waterline, I have opted to use an immersible pump in a large bucket with water. As immersible pumps should not run dry, I have used a reed switch with a magnet on a floating device to cut the power to the pump if the water level is too low.

Protection

protect

I am not going to point out that if you are switching a pump or a lamp that you have to be careful with the mains voltage that can kill you. If you have no knowledge of these things you should not attempt to fiddle with it. But there is something else I need to point out: If the lines of the of your sensors to your ATtiny are long, they may pick up spikes that could be too high for the ports on your microcontroller and you may want to consider a bit of protection. This can be obtained by adding a 1 k resistor and a 5v1 zener as is shown in the picture. Obviously if you use that you need to recalibrate the values that are read from the sensor. 

/* Garden  
February 2012 

Sensors:   
-NTC  aan analoog  pin1 
-Humidity spike aan analoog pin2   
*---------------------------------NTC-------------------------------
* Schematic:
*   [Ground] -- [10k-pad-resistor] -- | -- [NTC] --[Vcc] 

*                                     | 
*                                Analog Pin 3 
* 
*    Rntc=((Vcc * pad / Vpin1) - pad); 
*   =((1024 * pad / RawADC) - pad); 
*  
*-------------------------------Humid------------------------------ 

* Schematic: 
*   [Ground] -- [10k-pad-resistor] -- | -- [Spikes] --[Vcc] 
*                                     | 
*                                Analog Pin 2 
*  
Als de humidity sensor te droog wordt, stijgt de weerstand en
daarmee daalt de spanning op de analoge poort. Inschakelen relais
om te sproeien De sensor is ca 10 kOhm als de grond vochtig genoeg
is. De reading van de analoge poort moet dus op ca 512 komen (met 
een pull down weerstand van 10 k) 
* Als de temperatuur daalt, stijgt de weerstand van de NTC en 
daarmee daalt de spanning op de analoge poort ->Inschakelen relais 
om te verwarmen de weerstand moet zich tussen 12,5 en 10 kOhm 
bewegen (ca 21-23 graden) de reading van de analoge poort moet dus 
tussen de 512 en 455 gehouden worden. Dwz inschakelen relais bij 
455 en uitschakelen bij 512  (als niet wordt gemapt)

ATTiny pins   
Physical  Analoog  Digital     
1           0          5   Reset, PinChange Interrupt 
2           3          3   INT0,3 
3           2          4   INT0,4 
4           Ground 
5                      0   INT0,0 
6                      1   INT0,1 
7           1          2   INT0,2 
8           Vcc 
*/ 
//Pin definities 
int ntcPin= 3;   //analogue Pin3  ->Physical pin2  NTC 
int humidPin=2;   //analogue Pin2  ->Physical pin3  Humidity spike
int pumpPin =2;  //digital  Pin2  ->Physical pin7  Bevloeiingspomp 
int warmPin=1;   //digital  Pin1  ->Physical Pin6  Verwarming 
int signalPin=0;  //Digital  Pin0  ->Physical Pin5  Extra 

// Variabelen setting 
int moist=0;      // maybe float is not necessary  
int ntc=0;       // maybe float is not necessary 
int ldr=0;        // maybe float is not necessary   
int irrigate=512; // level for irrigation ca 126 after mapping 

//The following function, "setup", must always be present 
void setup()  
{
//Serial.begin(9600);// for debugging only, remove for Attiny 
pinMode(ntcPin,INPUT);// meet temperatuur 
pinMode(humidPin,INPUT);// meet vochtigheid 
pinMode(signalPin,OUTPUT); //Signaallamp oid 
pinMode(warmPin,OUTPUT);//  Output for heating element 
pinMode(pumpPin,OUTPUT);//  Output for Relais/waterpump  
digitalWrite(pumpPin, LOW); 
digitalWrite(warmPin, LOW);
}

void loop() //This function must always be present 
{ 
/* First we read the Humidity sensor. If the sensor is >10 k this
is considered dry 
As the sensor is in a voltage divider of 10k the value read
then will be 1/2 Vcc 
In analogue value this will be about 1/2 *1024=512 
and after mapping it will be 127 

So if the value read ('moist') is smaller than what is
considered dry ('irrigate'), the pump should be switched on 
for a specific time. 
This is done by indicating a higher treshhold for switching 
the pump off 

*/

moist=sample(humidPin);  //read humiditySensor De functie leest 5x en neemt het gemiddelde 
// Serial.print("Value Read: "); 
// Serial.println(moist);   
if (moist <= irrigate) digitalWrite(pumpPin, HIGH); 
if (moist >= irrigate+5) digitalWrite(pumpPin, LOW); 

/* Now read the temperature sensor. 
If  the sensor is 10 k this is considered 26 degrees, 
the analogue reading will be 512 and 128 after mapping 
at a higher value the heating should be switched off 

If the sensor is 12k5 this is considered 21 degrees, 
the analogue reading will be 455 and 113 after mapping this 
is the lower treshhold and the heating should be switched on 

In real life however the values 142 and 155 correponded 
better with those temperatures 
*/ 

ntc=sample(ntcPin); 
// Serial.println(ntc);   
  if (ntc <= 142) digitalWrite(warmPin, HIGH); 
  if (ntc >= 155) digitalWrite(warmPin, LOW);
} //end loop 

// Sample Function
int sample(int z)   
/* This function will read the Pin 'z' 5 times and take an average.
Afterwards it will be mapped by dividing by 4 
*/ 

{
int i; int sval = 0;
for (i = 0; i < 5; i++){   sval = sval + analogRead(z);    // sensor on analog pin 'z'  
}
sval = sval / 5;    // average 
sval = sval / 4;    // scale to 8 bits (0 - 255) 
//sval = 255 - sval;  // invert output  
return sval;
}


Getting the code in the ATtiny

Now this may all be very well, but how to get the program in the ATtiny, as it has no bootloader? Well, there are several ways. One can use the Arduino IDE to generate the hexfile and then program that with either a parallelport programmer or a USBtiny programmer, or if one has an Arduino, the Arduino can be used as a programmer. Several websites have described this and I will refer to the procedure as explained by John Boxal in his blog.

In principle the method is as follows.
  • Connect the 6 pins between the Arduino and the ATtiny that make up the official in line programming header.
  • Add a specific ATtiny board file to yr IDE.
  • Load the Arduino with the ‘ArduinoISP’ sketch.
  • Then choose ‘ATtiny 85 (w/ Arduino as ISP)’ as your board.
  • Load the ‘Garden’ sketch and upload as usual.

Do I really need a microcontroller for this

Some people may wonder if perhaps even using an ATtiny for this is overkill? Well, ofcourse it is because what you are basically doing here is comparing two values. The value for a measured temperature (or humidity) with a set value and there are devices that have been used for doing exactly that for years already: Opamps, like the trusty 741 

opamp

Obviously with this design it is harder to set a low and a high level (one can use two opamps for that) but that means it will keep the temperature (or humidity)  at a narrower level.

For other possible op amp solutions see here and here