Motivation and theory of operation
Over the couse of the past few days I have designed and built a system
which fits in the lid of a snus box intended to help me with reducing my
snus consumption. The project was inspired by an overwhelming need to
focus on something other than finals (come to think about it, my best work
is done while procrastinating about something far more important).
Anyways: The system I built is properly over-engineered, hopelessly
over-complicated and may even be clever enough to help me cut down on this
nasty habit.
The system works like this: Whenever the green LED is lit continuously,
I am allowed to take a snus. After I do so, I press on the lid in short
bursts, activating the tactile push-button underneath, and the system
goes idle and waits for a set amount of time. The LED blinks briefly
every three seconds to show that the system hasn't died. Each time the
system goes idle, a small amount of extra time is added to the delay
time. These extra intervals add up to 1h 40m over the course of one
month, more than halving my snus consumption. There really isn't
anything more to it than that, and I am relying solely on my vanity to
keep me from cheating the system.
Hardware design
The circuit went through several revisions before I was happy with the
design, though some things were decided immediately. The brains of the
operations is the versatile
ATtiny85, powered by a 3 V coin cell lithium battery with 240 mAh
capacity. (CR2032). The green LED is in
series with a 47 kOhm
resistor, and draws around 17 µA. Button debouncing is done in
hardware by the 10 nF ceramic capacitor.
The first revisions featured on a photosensitive resistor placed
underneath the lid, pointing into the box, and relied on sudden changes
in brightness (like when I open the lid) to determine whether or not I
had grabbed a snus or not. This was not only unreliable, but the voltage
divider drew way too much continuous current, thus draining the coin
cell too quickly. After discarding magnetic switches as well, I settled
on a simple push-button, which works nicely. The physical tolerances in
the lid coincidentally allow me to activate the button through the top
of the lid, which makes for a rather seamless user experience.
The battery and battery holder is screwed in with M2 screws and nuts
from the underside of the lid, while everything else is simply
super-glued to the inside of the lid. This also means that every time I
switch to a new box, I simply have to transfer the lid.
Circuit layout
Code: Interrupts and heavy sleep modes
If you run the ATtiny without bothering putting it to sleep when it is
not doing something, you will drain the coin cell in a matter of hours.
I learned this the hard way. Luckily, you can shut down most of the
processors modules, as well as ADC's, comparators, timers, brown-out
protection circuitry and much more. According to the datasheet you are
able to push the current draw to well below 1 µA in power-down mode!
I wanted the ATtiny to wake up at consistent intervals of time while in
delay mode, and it was natural to look to the Timer/Counter modules for
that task. The problem is that the interrupt requests generated by timer
overflows are not able to wake the processor from anything deeper than
idle sleep. The current draw in idle mode is still too large. There is
an alternative, though: The watchdog timer is able to wake the chip from
power down mode. The downside of relying on the watchdog, though, is
that is is run by a terribly inaccurate 128 kHz oscillator, which can
vary as much as 10 % in either direction.
Circumstances forced me to use the watchdog, though. I proceeded to
check all watchdog timer prescale factors at all possible voltage
levels and seeing what interrupt frequencies I could achieve. I
settled for a 16k prescaler, which gave 130 ms timeout intervals.
The code compensates for the offsets in the long delay functions,
and runs two seconds off every hour. That is definitely acceptable.
You will find a download link for the source code at the bottom
of this page. At system start I disable just about every feature
of the ATtiny. Before entering the delay mode, I start the
watchdog timer and enable timeout interrupts. When the delay
function void disallow(uint32_t current_delay)
is
happy and has returned, the system enters
"wait-for-button-trigger-sequence" mode by calling bool
allow(void)
. This function pulls the button high before
going to sleep and returns true
when the correct
sequence of short bursts are registered on the push-button, and
is therefore continuously called as long as it returns false --
for example when I sit on the box and trigger the button.
External pin change interrupts are able to wake the system
from power down sleep, thus everything but pin change
interrupts are disabled before entering button mode. All in
all the system draws around 3 µA in delay mode and 50 µA in
button mode (most of which goes through the LED), and spikes
to around 3 mA when the processor wakes up. An online
battery longevity calculator estimated that my coin cells
will last well over a year, which is more than enough.
Disabling all ATtiny85 modules:
command
|
description
|
avr/io.h
avr/interrupt.h
avr/wdt.h
|
necessary libraries
|
cli();
|
disable interrupts to safely write to registers
|
DDRB = 0xff;
|
turn all pins to output (or input) -- undefined pins may draw
current
|
ADCSRA = 0; PRR |= (1 << PRADC);
|
disable ADC. Important: write ADCSRA before PRR
|
ACSR &= ~(1 << ACIE);
|
disable analog comparator interrupts
|
ACSR |= (1 << ACD);
|
disable analog comparator
|
PRR |= (1 << PRUSI);
|
shut down serial interface clock
|
PRR |= (1 << PRTIM0); PRR |= (1 << PRTIM1);
|
disable timers
|
#include <avr/wdt.h> wdt_disable();
|
disable watchdog timer
|
DIDR0 |= (1 << AIN1D); DIDR0 |= (1 << AIN0D);
|
disable input buffers
|
Putting the ATtiny85 to deep sleep:
command
|
description
|
avr/sleep.h
|
necessary library
|
set_sleep_mode( SLEEP_MODE_PWR_DOWN );
|
enable power down sleep mode
|
sleep_enable();
|
set the SE (sleep enable) bit
|
sleep_bod_disable();
|
disable brown-out detection
|
sei(); sleep_cpu(); cli();
|
this is where the ATtiny sleeps
|
sleep_disable(); sei();
|
clears SE (sleep enable) bit
|
Note that running all the code above is the same as meticulously
killing your ATtiny: It will not wake up until you reset it. Test your
code thoroughly line by line and wake the processor with suitable
interrupts. Refer to the datasheet and library documentation for more
information.
Over-thinking the distribution of delay increments
This part of the design definitely got out of hand. The four plots
below explain how I ended up distributing the delay increments over one
month and why I did it in this fashion (click images to expand).
Imagine if I blatantly (in other words: probably more than adequately
satisfactory) decided to increment the delay time by equal amounts every
time. Since the delay increments occur at longer and longer intervals as
time progresses, the delay increment curve would be skewed and non-linear in
the time domain (but it would of course be in the "snus domain"). In
addition to addressing that issue, I wanted the delay increment curve to be
symmetric and have a gaussian
shape
in order to ease myself in and out of the difficult time to come.
Furthermore, there are a couple constraints on the distribution: The sum of
all delay increments must roughly equal 100 minutes, and the total elapsed
time from the first increment is added to the last must stretch the course
of one month. After spending the better part of one afternoon trying to
solve this nightmare analytically, I went to MATLAB and tried to simulate
it. The simulations proved somewhat more fruitful.
It turns out that, if you take a discrete chi-squared probability
density function with 9 degrees of freedom, flip it left-right,
numerically integrate it twice and use the result as a time-basis for
plotting the original, you make a huge mess. Playing with all the
constants involved though, the resulting graph (bottom stem plot in the
image above) approaches a very nice gaussian-esque curve. You'll notice
that the spacings between the plot stems are not constant; That's
because this plot shows the delay increments as a function of seconds
since system start, not as a function of snus number. The delay
increments cease at 1.899 million seconds, which is the closest I can
get to one month minus 7 hours of sleep every night. It is, in other
words, exactly what I was looking for.
Download