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.
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
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.
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
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.
||disable interrupts to safely write to registers|
||turn all pins to output (or input) -- undefined pins may draw current|
||disable ADC. Important: write ADCSRA before PRR|
||disable analog comparator interrupts|
||disable analog comparator|
||shut down serial interface clock|
||disable watchdog timer|
||disable input buffers|
||enable power down sleep mode|
||set the SE (sleep enable) bit|
||disable brown-out detection|
||this is where the ATtiny sleeps|
||clears SE (sleep enable) bit|
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.
Source code: Download .rar (4 kB)