Background

The iCufflinks use an Atmel ATtiny4 microcontroller (MCU) as the brains to controlling the LED lighting pattern. The MCU is an 8-bit processor with 32 bytes of SRAM, only a handful of registers, and 512 bytes of flash for program storage. The stack is stored in the SRAM so you don’t really get to use it for anything.

The original hardware design and software are all open source and can be found on the Adafruit GitHub. One of the things about the design is that it runs on CR1220 batteries and it is recommended that they be changed after 24 hours of use. That is what got me thinking that I could improve this product to increase the amount of time between battery changes.

I have also never read nor written assembly code for an AVR processor and the last time I probably looked at assembly was 386 stuff about 20 years ago. So excuse any minor assembly style issues. I was temped to rewrite the code in C but with the limited flash space I had to rule this out. Had this been a ATtiny9 with 1k bytes I would have gone this route. The small overhead that AVR Studio introduces was just a tiny bit too much for this limited memory space.

Baseline

I needed to measure the baseline power usage of the circuit so I could see what my changes were doing. The problem was that I didn’t have any iCufflinks nor any ATtiny4 devices. So I had to order some components from Digi-Key. I also ordered the same LED model that was used in the cufflinks (this was really more of a guess based on the schematics). Once the order arrived I set to building my test circuit shown in the picture below. The version 1.0 code is running on the left and my modified code is running on the right.

Now that the circuit was built, I measured the current draw to get my baseline usage. Note that when I’m measuring I’m only measuring one of the little circuits at a time. In the rest of this article I will be mainly using the Average reading when comparing results.

Max

Average

Min

Whole Circuit

1.923 mA

0.848 mA

0.458 mA

MCU Only

465.29 μA

452.53 μA

442.65 μA

I’m making all these measurements with a Fluke 289 multimeter using the mA and μA current settings. Power is coming from a bench top supply at 3V.

Sleep mode

The first thing I noticed about the code was that the processor was active all the time and was basically just constantly counting in a loop intended to introduce a delay in the code.Here is the delay loop which is intended to make a 17ms delay.

As far as power goes, this is really inefficient as it is just sitting there burning power the whole time counting. It would be much better to have the processor sleep for those 17ms. So I introduced the Idle sleep mode to the code. Since we want to keep the PWM constantly running to drive the LED we can’t shut down the chip completely in sleep and need to keep CLKIO active but we can shut down CLKCPU. As it turns out there is only one sleep mode called Idle Sleep Mode where the CLKIO is left running. This code snippet put in your reset vector will enable sleep mode and the default is Idle mode.

The other issue is how to wake up from sleep so we can continue the work. The PWM is using our one counter so we can’t use that and there is no external component to trigger the interrupt pin INT0. So the only option is to repurpose the watchdog timer to generate an interrupt. The default watchdog timer is set for 2k cycles at 128kHz which is about 16ms. That delay is close enough to the 17ms of the original code so I’m going to just use that default value. The following code snippet put in your reset vector will setup the watchdog timer in interrupt mode and enable interrupts.

Now that we have sleep mode enabled and the watchdog all setup, the delay loop can now be replaced with a simple reset of the watchdog timer and a sleep call. You also need to add in the watchdog interrupt vector and the interrupt handler, see the code where WDT is defined and used.

; reset the watchdog timer to full value and sleep until it pops an interrupt wdr sleep

Measuring the power savings with just the sleep mode added is pretty significant.

Baseline

Modified

MCU Only

452.53 μA

191.31 μA

Saves about 261 μA.

Enable pull-ups

Next up is a little trick I learned about while reading an application note on picoPower (see link in references section at the end). PicoPower is one of Atmel’s power saving technologies in some of the newer ATtiny devices (not the ATtiny4). It turns out that the chip wastes power switching on I/O pins if the pins are floating and don’t have any pull-up resistor on them. This is also mentioned in the data sheet. The easiest method to fix this is to enable the built-in pull-ups on unused port pins. Since we are using PB0 for the PWM we wont touch that one, but everything else is unused and can be fixed. The following code snippet in your reset vector is all you need.

Slow the clock down

The ATtiny4 can run at up to 8MHz with its’ built-in oscillator. From the factory it is set with a clock prescale of 8 so it is really running at 1MHz. The main thing this code does is load a value from memory and set the PWM to this value. This does not need to run at 1MHz and we can really slow this down to the slowest possible setting and it will still be plenty fast for our purposes. The largest clock division factor available is 256 which will result in the clock running at 32kHz. So again, adding this little code snipped in the reset vector will slow the clock down.

I looked at that data and saw so many duplicate values right next to each other that I though I could at least halve the data and still retain the same visual look at the LED. It turns out that the big 300 byte block of PWM values in the code is a bit redundant. So I chopped the data in half while rounding down.

This reduced nicely and with my other changes in the code the main code only grew by 18 bytes.

ATtiny4 memory use summary [bytes]: Segment Begin End Code Data Used Size Use% --------------------------------------------------------------- [.cseg] 0x000000 0x0000e8 68 150 218 512 42.6%Not only does this save 150 bytes of precious space, but there is one more benefit. Since I had changed the code to sleep between changes of PWM values, I needed to now double the sleep time because I had half the data. Turns out this is really easy by just changing the watchdog timer to go twice as long. It was going at 2k cycles (about 16ms) and this change moves that to 4k cycles (about 32ms). This snippet replaces the previous watchdog setup.