The requirements of the control system are pretty simple:
- Be controlled with a single pushbutton.
- Support on and off.
- Turn off the light automatically after a suitable period of time
That’s pretty simple – simple enough that you can do it with an 8-pin AVR controller, like the atTiny12. That programs fine in my STK500 development kit, but on the low-pin-count controllers, many of the pins have shared functions related to programming, so you have to attach/detach them each programming cycle. That’s why I’m using one of the attiny861s that I have left over from another project, where I have plenty of pins. The cost difference doesn’t matter at all in a one-off design.
I spend the usual frustrating time remembering how to set up the programmer so it works. The key to remember is that you need a current build of Atmel’s avr studio so that you can look in the help file to see how to do the wiring. I wasted a couple of hours and almost lost the Magic Smoke before I remembered where to find the information.
The next step is to get the controller configured correctly. Even the simpler AVRs have a ton of options. I’m going to be using one of the timers, so I need to set up the timer registers correctly. In this case, I want a 16-bit counter (set bit 7 of TCCR0A to 1), and I want to divide the 8MHz clock by 256 (set bit 3 off TCCR0B to 1), and so on. All the information is in the atmel data sheet (the 236 page data sheet…) for the controller, but it takes a fair bit of work to get it right.
Or, you can buy a copy of codevision AVR. Not only does that let you write in C rather than assembler, it has a program wizard that lets you use human-understandable settings rather than hex values. So, in this case, you can go into the wizard and say that you want timer 0 to run at 31250 Hz, use a 16-bit counter, and call an interrupt when it overflows, and it will generate the source code (with comments) that does just that. Only two annoying things about it:
- It puts all the initialization code at the beginning of main and then the main loop at the end, so you’re constantly having to scroll over that code to get to your main loop.
- When you want to update the code, you have to run the wizard and then cut & paste the updated code in the proper place – it can’t fill in the areas you want.
Neither of these are more than a little annoyance. As you can tell I’m a big fan of AVR studio.
So, back to the project. First, we need a way to handle the turning the lights off, and for that we need a timebase. We’re going to use 10 Hz (for reasons that will become apparent later), and it would be most convenient to get an interrupt at that rate. Since the interrupt will happen whenever the 16-bit timer overflows, we need a timebase where the count fits in 16bits (ie 65535). Looking at our options, we see that we can get 8 Mhz / 256 = 31250 Hz as our timer frequency. If we can send an interrupt every 3125 counts, we’ll have our 10 Hz. So…. We take 65535 – 3125 = 62410 = F3CA, and initialize the counter to that value after every interrupt.
And that gives us 10Hz. Or, actually, it gives us 10Hz +/- about 10%, which is the factory calibration tolerance of the internal oscillator. It’s possible to get a better calibration than this by writing to the OSCCAL register – Atmel claims you can get +/- 1% through that approach – but it’s not something needed for this application, so we’ll just stick with whatever we get.
Now that we have that, we can write our interrupt service routine.
// Timer 0 overflow interrupt service routine
interrupt [TIM0_OVF] void timer0_ovf_isr(void)
{
// Reinitialize Timer 0 value – 1 second timeout…
TCNT0H=Timer0H;
TCNT0L=Timer0L;
waitCounter++;
if (timeRemainingTenths > 0)
{
if ((timeRemainingTenths % 600 == 0) &&
(timeRemainingTenths <= 3000))
{
PORTA.2 = 0;
}
else
{
PORTA.2 = 1;
}
timeRemainingTenths–;
}
else
{
PORTA.2 = 0;
}
}
We have a timeRemainingTenths that sets the timeout value. The if condition handles flashing the lights off for 1/10 second the last 5 minutes so that I can turn off the snowblower and walk back over and hit the button again.
That leaves only the button-control handling code to write. As part of this, I need to handle debouncing the switch: when a mechanical switch closes, it doesn’t close fully but instead bounces open and closed a few times. This bouncing is slow enough that it’s easy for a microcontroller to detect it multiple times, so you need to debounce the switch. There is are dedicated debounce ICs to deal with this – such as the Maxim 6816 series – but in most cases you can do it in software. Or you could use a hall-effect switch that doesn’t need debouncing. The downside of debouncing is that it slows the speed of response.
In this case I don’t need the quick response, so the code is pretty simple:
void Wait(int seconds)
{
waitCounter = 0;
while (waitCounter < seconds * 10)
{
;
}
}
// Declare your global variables here
void main(void)
{
init();
while (1)
{
if (PINB.0 == 0)
{
PORTA.2 = 1;
timeRemainingTenths = 60 * 60 * 10; // 1 hour
Wait(1);
// Held down, turn off lights…
if (PINB.0 == 0)
{
timeRemainingTenths = 0;
PORTA.2 = 0;
Wait(2);
}
}
};
}
If you look back at the interrupt service routine, you’ll see that the waitCounter variable gets updated at 10Hz. The wait routine uses this variable to provide a way for us to wait a specific number of seconds.
The sensing code takes a bit of explanation. In digital electronics, the concepts “0†and “1†refer to voltage ranges. The crossover point depends on particular semiconductor chemistry used in the electronics, but assume that it’s 2.5 volts in this case (ie 50% of the 5 volt supply we’re using). So, any voltage above 2.5 volts is 1, and below 2.5 volts is 0. If we hook a switch up to a digital input and connect it to ground, when we press the button, the input voltage goes to zero, and the input value is 0. Then, we let go of the button, and the input goes to some indeterminate state. It might be zero, it might be 1, it might go back and forth.
We get around that by using what is called a pull-up resistor, which is connected to Vcc (5V in this case). If the button isn’t pressed, that ensures that we get a high voltage (a 1), and then when it is pressed, we still get zero.
In the past – say in 1980 – you’d use a kind of logic known as TTL, and you had to be really careful how you hooked things up and what values you used, since TTL was a pretty rough approximation of the term “digitalâ€. These days, most logic families are a lot easier to deal with, and in fact on the AVR microcontrollers have built-in switchable pullup resistors.
All of that is a long way of explaining why the code looks for a low value to determine when a switch is pressed rather than a high one.
The code itself is simple. As soon as the button is pressed, we set the time remaining to an hour, and then we wait a second to debounce. If the button is still pressed after a second, we turn off the switch, and then wait 2 seconds to debounce after that press.
That’s about it.
So, we look at pin 0 on the B port, and if it’s zero (pulled to