Did you ever wanted your room decoration to be a smoothly fading light strip in a jar? Inspired by the Pimoroni firefly light I built my own variation with an ATtiny and two AA batteries instead of using a Raspberry Pi Zero and a huge battery pack.

There are three LED strings that fade in a sine pattern. Two of them fade anti parallel at one of 16 adjustable speeds. The third fades differently at a second configurable sine. This is how the final result looks like! I’ve built this thing as a birthday gift for my girlfriend, she really loves it.

After some planning I ended up buying three SÄRDAL lights and one ENSIDIG jar at IKEA for about €15.

Then after unboxing the lights, I completely disassembled them and desoldered the cables in order to attach them to the ATtiny later on. Some measuring showed that the inbuilt circuit boosts or drops down the voltage to about 2.6 V if load is applied. If the output doesn’t draw enough current the voltage jumps up like crazy. That is why I decided to power at least to strips anti parallel to manage a continuous minimum draw of one strip in sum. This means the two strips share the duty cycle of the PWM output. If one strip is of, the other one is on. I just added a third strip to give the whole thing a more beautiful attitude. This one runs on the second PWM channel output.

All three LED strips are power directly from the controller because each of them draws about 30 mA at around 2.6 V which is in the specs of the ATtiny.

In order to make some settings I added a jumper and a push button. The push button toggles through PWM 16 different prescalers to adjust the sine frequency whereas the jumper is used to decide which PWM channel should be adjusted.

Everything you need in addition to the IKEA stuff is

  • 1x Atmel ATtiny85
  • 2x 100nF
  • 1x quite big elko around 330µF to smooth the voltage
  • 1x Push button
  • some double sided tape to attach the circuit to the battery housing

Hardware

The images below show the schematics, pin mappings and the final circuit.

Software

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
/*
 * FireflyLight.c
 *
 * Created: 08.03.2017 12:18:04
 * Author : Dennis
 */

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <avr/eeprom.h>

#define PRESCALER_MAX 32

typedef struct  
{
  volatile uint16_t position;
  volatile uint8_t prescaler;
  volatile uint8_t count;
  volatile uint8_t doSave;
} sine_t;

volatile sine_t sineA;
volatile sine_t sineB;

/* on the fly calc:
//#define PI 3.14159265f
//value = (sinf(((float)count)*PI/180.f)+1.)*254./2.;  //254 to limit pwm at top
//OCR0A = (uint8_t)lrint(value);

jsfiddle:
var count = 0;
var a=[];
for (var i=0; i<360; i++) {
  var value = (Math.sin(count*3.14159265/180)+1)*254/2;
  console.log(count, value.toFixed(0));
  a.push(parseInt(value.toFixed(0), 10));
  count = (count+1)%360;
}

var pad = function(number) {
  if (number < 10) {
    return "  " + number;
  }
  if (number < 100) {
    return " " + number;
  }
  return number;
}

var res = "";
for (var i=0; i<360; i++) {
  res += pad(a[i]) + ", ";
  if (i != 0 && (i+1) % 20 == 0) {
    res += "\n";
  }
}
console.log(res);
*/

const uint8_t sine[360] PROGMEM = {
  127, 129, 131, 134, 136, 138, 140, 142, 145, 147, 149, 151, 153, 156, 158, 160, 162, 164, 166, 168,
  170, 173, 175, 177, 179, 181, 183, 185, 187, 189, 190, 192, 194, 196, 198, 200, 202, 203, 205, 207,
  209, 210, 212, 214, 215, 217, 218, 220, 221, 223, 224, 226, 227, 228, 230, 231, 232, 234, 235, 236,
  237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 246, 247, 248, 248, 249, 250, 250, 251, 251, 252,
  252, 252, 253, 253, 253, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 253, 253, 253, 252,
  252, 252, 251, 251, 250, 250, 249, 248, 248, 247, 246, 246, 245, 244, 243, 242, 241, 240, 239, 238,
  237, 236, 235, 234, 232, 231, 230, 228, 227, 226, 224, 223, 221, 220, 218, 217, 215, 214, 212, 210,
  209, 207, 205, 203, 202, 200, 198, 196, 194, 192, 191, 189, 187, 185, 183, 181, 179, 177, 175, 173,
  170, 168, 166, 164, 162, 160, 158, 156, 153, 151, 149, 147, 145, 142, 140, 138, 136, 134, 131, 129,
  127, 125, 123, 120, 118, 116, 114, 112, 109, 107, 105, 103, 101,  98,  96,  94,  92,  90,  88,  86,
   84,  81,  79,  77,  75,  73,  71,  69,  67,  65,  64,  62,  60,  58,  56,  54,  52,  51,  49,  47,
   45,  44,  42,  40,  39,  37,  36,  34,  33,  31,  30,  28,  27,  26,  24,  23,  22,  20,  19,  18,
   17,  16,  15,  14,  13,  12,  11,  10,   9,   8,   8,   7,   6,   6,   5,   4,   4,   3,   3,   2,
    2,   2,   1,   1,   1,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   1,   1,   1,   2,
    2,   2,   3,   3,   4,   4,   5,   6,   6,   7,   8,   8,   9,  10,  11,  12,  13,  14,  15,  16,
   17,  18,  19,  20,  22,  23,  24,  26,  27,  28,  30,  31,  33,  34,  36,  37,  39,  40,  42,  44,
   45,  47,  49,  51,  52,  54,  56,  58,  60,  62,  63,  65,  67,  69,  71,  73,  75,  77,  79,  81,
   84,  86,  88,  90,  92,  94,  96,  98, 101, 103, 105, 107, 109, 112, 114, 116, 118, 120, 123, 125
};

ISR(TIMER1_COMPA_vect)
{
  if (sineA.count == 0)
  {
    OCR0A = pgm_read_byte(&(sine[sineA.position]));
    OCR0B = OCR0A;
    sineA.position = (sineA.position+1)%360;
  }

  if (sineB.count == 0)
  {
    OCR1B = pgm_read_byte(&(sine[sineB.position]));
    sineB.position = (sineB.position+1)%360;
  }

  sineA.count = (sineA.count+1) % sineA.prescaler;
  sineB.count = (sineB.count+1) % sineB.prescaler;
}

ISR(INT0_vect)
{
  if (PINB & (1<<PB3))
  {
    sineA.prescaler = (sineA.prescaler+2) % PRESCALER_MAX;
    sineA.doSave = 1;
  }
  else
  {
    sineB.prescaler = (sineB.prescaler+2) % PRESCALER_MAX;
    sineB.doSave = 1;
  }
}

void sleep()
{
  ADCSRA &= ~(1<<ADEN); //disable adc
  ACSR = 0x80; // disable analouge comp
  //MCUCSR |= (1<<JTD); // disable jtag

  //MCUCR &= ~0x3;
  //sei();
  //MCUCR |= (1<<SM1) | (1<<SM0); // Power down
  MCUCR |= (1<<SE); // Sleep enable
  asm("sleep");
  MCUCR &= ~(1<<SE); // Sleep disable
}

int main(void)
{
  DDRB |= (1<<PB0) | (1<<PB1) | (1<<PB4);
  PORTB |= (1<<PB2) | (1<<PB3); //pullup int0, select jumper
  TCCR0A |= (1<<COM0A1);
  TCCR0A |= (1<<COM0B1) | (1<<COM0B0); //inverted B
  TCCR0A |= (1<<WGM01) | (1<<WGM00); //fast PWM
  TCCR0B |= (1<<CS00); //prescaler
  OCR0A = 0;
  OCR0B = 0xFF;

  TCCR1 |= (1<<CS10); //prescaler
  OCR1A = 0xFF;
  TIMSK |= (1<<OCIE1A); //int on compare match

  GTCCR |= (1<<PWM1B); //pwm output
  GTCCR |= (1<<COM1B1) | (1<<COM1B1); //output mode
  OCR1B = 128;
  OCR1C = 0xFF;

  MCUCR |= (1<<ISC01); //falling edge
  GIMSK |= (1<<INT0);

  //power reduction
  PRR |= (1<<PRUSI) | (1<<PRADC);

  sineA.count = 0;
  sineA.position = 0;
  sineA.doSave = 0;
  sineB.count = 0;
  sineB.position = 0;
  sineB.doSave = 0;

  sineA.prescaler = 16;
  sineB.prescaler = 10;

  eeprom_busy_wait();
  uint8_t val = eeprom_read_byte((uint8_t*)0x00);
  if (val >= 0x00 && val < 0xFF)
  {
    sineA.prescaler = val;
  }
  eeprom_busy_wait();
  val = eeprom_read_byte((uint8_t*)0x01);
  if (val >= 0x00 && val < 0xFF)
  {
    sineB.prescaler = val;
  }

  sei();

  while (1)
  {
    if (sineA.doSave)
    {
      eeprom_busy_wait();
      eeprom_update_byte((uint8_t*)0x00, sineA.prescaler);
      sineA.doSave = 0;
    }

    if (sineB.doSave)
    {
      eeprom_busy_wait();
      eeprom_update_byte((uint8_t*)0x01, sineB.prescaler);
      sineB.doSave = 0;
    }
    sleep();
  }
}