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.

Promo photo. The light source is my Nixie tube alarm clock, which does a rather decent job of lighting stuff for photos.

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

Source code: Download .rar (4 kB)