/
tiny-pov.c
301 lines (273 loc) · 6.51 KB
/
tiny-pov.c
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
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
/*
* tiny-pov by Stefan Tomanek <stefan@pico.ruhr.de>
*
* A simple POV system illuminating your bicycle wheel.
*
* 6 LED pixel arranged along a spoke are directly controlled by the I/O pins
* of an ATTiny2313 (using transistors), while additional daughter board can be
* attached to the system to generate a better POV effect even at lower speed.
* Those extra boards each contain a 74HC595 shift register to control their
* LEDs.
*
* A simple reed contact is triggered by a magnet fixed to the bicycle frame:
* By measuring the time between these passes, the controller can estimate the
* position of the LED spoke at any time and enable the appropiate LEDs.
*
*/
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
#include <util/delay.h>
#include <math.h>
/*
* A full revolution is divided into 256 (2^8) segments,
* ranging from 0 to 255 (CYCLE_POS_MAX)
*/
#define CYCLE_POS_MAX UINT8_MAX
#define CYCLE_POS_CNT (CYCLE_POS_MAX+1)
#define set_input(portdir,pin) portdir &= ~(1<<pin)
#define set_output(portdir,pin) portdir |= (1<<pin)
#define ELEMS(x) (sizeof(x)/sizeof((x)[0]))
#define LED_DDR DDRD
#define LED_PORT PORTD
const static uint8_t LED[] = {
PD0,
PD1,
PD2,
PD3,
PD4,
PD5,
};
/*
* Pins controlling the shift register
* of the daughter boards
*/
#define SR_PORT PORTB
#define SR_DDR DDRB
#define SR_DATA PB1
#define SR_CLOCK PB2
#define SR_LATCH PB3
/*
* Reed contact triggered every turn
* by magnet at bicycle frame
*/
#define REED_IPORT PINB
#define REED_DDR DDRB
#define REED_PIN PB0
const static struct {
uint8_t offset;
uint8_t shift;
} daughters[] = {
/*
* The main board, not using a shift register
*/
{ 0, 0 },
/*
* A single daugher board installed ~ 180°
* next to the main device
*/
{ 138, 1 },
};
/*
* Clock storing the duration of the last wheel rotation
* and the current number of ticks
*/
enum clockstate {
INVALID,
PENDING, // first contact has been made, waiting for second one
VALID
};
static volatile struct {
enum clockstate state;
unsigned long last_duration;
unsigned long current;
} clock = {INVALID, 1, 1};
static void init(void) {
for (int i=0; i<ELEMS(LED); i++) {
set_output(LED_DDR, LED[i]);
}
LED_PORT = ~0;
set_input(REED_DDR, REED_PIN);
set_output(SR_DDR, SR_DATA);
set_output(SR_DDR, SR_CLOCK);
set_output(SR_DDR, SR_LATCH);
OCR1A = 2;
TCCR1A = 0x00;
// WGM1=4, prescale at 1024
TCCR1B = (0 << WGM13)|(1 << WGM12)|(1 << CS12)|(0 << CS11)|(1 << CS10);
//Set bit 6 in TIMSK to enable Timer 1 compare interrupt
TIMSK |= (1 << OCIE1A);
sei();
}
/*
* Shift data to the daughter boards.
*/
static void shift_out(uint8_t data) {
/*
* QA is not connected, while
* QB controls the innermost LED
*/
for (int i = 0; i < 8; i++) {
if ((data<<1) & (1<<i)) {
SR_PORT |= 1<<SR_DATA;
} else {
SR_PORT &= ~(1<<SR_DATA);
}
SR_PORT |= (1<<SR_CLOCK);
SR_PORT &= ~(1<<SR_CLOCK);
}
}
/*
* Make daughter boards display new data.
*/
static void trigger_latch(void) {
SR_PORT |= (1<<SR_LATCH);
SR_PORT &= ~(1<<SR_LATCH);
}
/*
* Called whenever a full revoolution is completed:
* Save the duration of the last roundtrip and
* reset the clock counter.
*/
static void cycle_finished(void) {
switch(clock.state) {
case INVALID:
clock.state = PENDING;
break;
case PENDING:
clock.state = VALID;
case VALID:
clock.last_duration = clock.current;
}
clock.current = 0;
}
static void update_leds(void);
/*
* Put the system to sleep after turning all lights off.
*/
static void slumber(void) {
// disable timer interrupt
TIMSK &= ~(1 << OCIE1A);
// make sure we are not interrupted anymore
_delay_ms(10);
// enable PCINT0
PCMSK |= (1<<PCINT0);
GIMSK |= (1<<PCIE);
// invalidate clock
clock.state = INVALID;
// turn off the lights
update_leds();
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
sei();
sleep_mode();
// restore interrupt configuration and reset clock
GIMSK &= ~(1<<PCIE);
TIMSK |= 1 << OCIE1A;
}
static volatile uint8_t trigger_state = 0;
SIGNAL(SIG_TIMER1_COMPA) {
// Check the reed contact
uint8_t temp = REED_IPORT & (1<<REED_PIN);
if ( temp && !trigger_state && clock.current > 20) {
cycle_finished();
} else {
clock.current++;
}
trigger_state = temp;
/*
* If we haven't seen a full revolution after 600
* ticks, the bike is probably standing - time to
* power down.
*/
if (clock.current > 600) {
slumber();
}
}
SIGNAL (SIG_PCINT) {}
#ifdef ARROW
#define IMG_ROWS 13
static const uint8_t arrow[IMG_ROWS] = {
0b000000,
0b001100,
0b001100,
0b001100,
0b001100,
0b001100,
0b111111,
0b111111,
0b011110,
0b011110,
0b001100,
0b000000,
0b000000
};
#else
#define IMG_ROWS 1
static const uint8_t arrow[IMG_ROWS] = {
~0,
};
#endif
/*
* Estimate the current position in the wheel cycle;
* If a full cycle _should_ already have been completed, the funtion
* returns the maximum value.
*/
#define min(a,b) ((a)>(b)?(b):(a))
static uint8_t cycle_position(void) {
return min( CYCLE_POS_MAX, (CYCLE_POS_MAX*clock.current/clock.last_duration));
}
/*
* We can divide the wheel into several spokes which are
* "standing still". The width of the spokes is controlled
* by the percentage value of W_SPOKES
*
* To completey fill the wheel, set N_SPOKES to 1
* and W_SPOKES to 100.0.
*/
#define N_SPOKES 3
#define SEGMENT_WIDTH (CYCLE_POS_CNT/N_SPOKES)
#define W_SPOKES 50.0
/*
* A global offset, depending on the position of your frame magnet.
*/
#define GLOBAL_OFFSET ((int)(-0.26*CYCLE_POS_CNT))
/*
* Calculate the LED content at a specific position
*/
static uint8_t get_content(uint8_t pos) {
// Check whether we are in an area to be painted
if (pos%(SEGMENT_WIDTH) < (W_SPOKES/100.0)*(SEGMENT_WIDTH)) {
// the position within our virtual spoke
double s_percent = 1.0*(pos%SEGMENT_WIDTH)/((W_SPOKES/100.0)*(SEGMENT_WIDTH));
uint8_t row = (int)(s_percent*IMG_ROWS);
return arrow[row];
} else {
// emptyness
return 0;
}
}
static void update_leds(void) {
// only display something with a valid clock content
uint8_t on = (clock.state == VALID);
uint8_t p = on ? (cycle_position() + GLOBAL_OFFSET)%(CYCLE_POS_CNT) : 0;
// handle any daughter boards
for (uint8_t i=0; i<ELEMS(daughters); i++) {
uint8_t content = on ? get_content((p+daughters[i].offset)%CYCLE_POS_CNT) : 0;
// invert the bitmask since LED are activated on LOW ports.
if (daughters[i].shift) {
shift_out(~content);
} else {
// directly connected
LED_PORT = ~content;
}
}
// activate the daughter board LEDs
trigger_latch();
}
int main(void) {
init();
while(1) {
update_leds();
}
return 0;
}