/ Kinetic_POV / bikewheel / bikewheel.ino
bikewheel.ino
  1  // SPDX-FileCopyrightText: 2019 Phillip Burgess for Adafruit Industries
  2  //
  3  // SPDX-License-Identifier: MIT
  4  
  5  /*-----------------------------------------------------------------------
  6    POV LED bike wheel sketch.  Uses the following Adafruit parts:
  7  
  8    - Pro Trinket 5V (www.adafruit.com/product/2000)
  9      (NOT Trinket or 3V Pro Trinket)
 10    - Waterproof 3xAA battery holder with on/off switch (#771)
 11    - 144 LED/m DotStar strip (#2328 or #2329) ONE is enough for
 12      both sides of one bike wheel
 13    - Tactile switch button (#1119) (optional)
 14  
 15    Needs Adafruit_DotStar library: github.com/adafruit/Adafruit_DotStar
 16    
 17    Full instructions: https://learn.adafruit.com/bike-wheel-pov-display
 18  
 19    This project is based on Phil B's Genesis Poi:
 20    learn.adafruit.com/genesis-poi-dotstar-led-persistence-of-vision-poi
 21    and has been adapted to the Pro Trinket to accomodate more and larger
 22    images than Trinket.
 23  
 24    Adafruit invests time and resources providing this open source code,
 25    please support Adafruit and open-source hardware by purchasing
 26    products from Adafruit!
 27  
 28    Written by Phil Burgess / Paint Your Dragon for Adafruit Industries.
 29    MIT license, all text above must be included in any redistribution.
 30    See 'COPYING' file for additional notes.
 31    ------------------------------------------------------------------------*/
 32  
 33  #include <Arduino.h>
 34  #include <Adafruit_DotStar.h>
 35  #include <avr/power.h>
 36  #include <avr/sleep.h>
 37  #include <SPI.h> // Enable this line on Pro Trinket
 38  
 39  #ifdef __AVR_ATtiny85__
 40  typedef uint8_t  line_t; // Max 255 lines/image on Trinket
 41  #else
 42  typedef uint16_t line_t; // Bigger images OK on other boards
 43  #endif
 44  
 45  // CONFIGURABLE STUFF ------------------------------------------------------
 46  
 47  #include "graphics.h" // Graphics data is contained in this header file.
 48  // It's generated using the 'convert.py' Python script.  Various image
 49  // formats are supported, trading off color fidelity for PROGMEM space
 50  // (particularly limited on Trinket).  Handles 1-, 4- and 8-bit-per-pixel
 51  // palette-based images, plus 24-bit truecolor.  1- and 4-bit palettes can
 52  // be altered in RAM while running to provide additional colors, but be
 53  // mindful of peak & average current draw if you do that!  Power limiting
 54  // is normally done in convert.py (keeps this code relatively small & fast).
 55  // 1/4/8/24 were chosen because the AVR can handle these operations fairly
 56  // easily (have idea for handing arbitrary bit depth w/328P, but this margin
 57  // is too narrow to contain).
 58  
 59  // Ideally you use hardware SPI as it's much faster, though limited to
 60  // specific pins.  If you really need to bitbang DotStar data & clock on
 61  // different pins, optionally define those here:
 62  //#define LED_DATA_PIN  0
 63  //#define LED_CLOCK_PIN 1
 64  
 65  // Select from multiple images using tactile button (#1489) between pin and
 66  // ground.  Requires suitably-built graphics.h file w/more than one image.
 67  #define SELECT_PIN 3
 68  
 69  // Optional feature -- not enabled here, no space -- a vibration switch
 70  // (aligned perpendicular to leash) is used as a poor man's accelerometer.
 71  // Poi then lights only when moving, saving some power.  The 'fast'
 72  // vibration switch is VERY sensitive and will trigger at the slightest
 73  // bump, while the 'medium' switch requires a certain spin rate which may
 74  // not trigger if you're doing mellow spins.  Neither is perfect.  To leave
 75  // that out and simply have the poi run always-on, comment out this line:
 76  //#define MOTION_PIN 2
 77  
 78  // Another optional feature not enable due to physical size -- powering down
 79  // DotStars when idle conserves more battery.  Use a PNP transistor (e.g.
 80  // 2N2907) (w/220 Ohm resistor to base) as a 'high side' switch to DotStar
 81  // +V.  DON'T do this NPN/low-side, may damage strip.  MOTION_PIN must also
 82  // be defined to use this (pointless without).
 83  //#define POWER_PIN 4
 84  
 85  #define SLEEP_TIME 2000   // Not-spinning time before sleep, in milliseconds
 86  
 87  // Empty and full thresholds (millivolts) used for battery level display:
 88  #define BATT_MIN_MV 3350  // Some headroom over battery cutoff near 2.9V
 89  #define BATT_MAX_MV 4000  // And little below fresh-charged battery near 4.1V
 90  // These figures are based on LiPoly cell and will need to be tweaked for
 91  // 3X NiMH or alkaline batteries!
 92  
 93  boolean autoCycle = true; // Set to true to cycle images by default
 94  #define CYCLE_TIME 10     // Time, in seconds, between auto-cycle images
 95  
 96  // -------------------------------------------------------------------------
 97  
 98  #if defined(LED_DATA_PIN) && defined(LED_CLOCK_PIN)
 99  // Older DotStar LEDs use GBR order.  If colors are wrong, edit here.
100  Adafruit_DotStar strip = Adafruit_DotStar(NUM_LEDS,
101    LED_DATA_PIN, LED_CLOCK_PIN, DOTSTAR_BRG);
102  #else
103  Adafruit_DotStar strip = Adafruit_DotStar(NUM_LEDS, DOTSTAR_BGR);
104  #endif
105  
106  void     imageInit(void);
107  uint16_t readVoltage(void);
108  #ifdef MOTION_PIN
109  void     sleep(void);
110  #endif
111  
112  void setup() {
113  #if defined(__AVR_ATtiny85__) && (F_CPU == 16000000L)
114    clock_prescale_set(clock_div_1);   // Enable 16 MHz on Trinket
115  #endif
116  
117  #ifdef POWER_PIN
118    pinMode(POWER_PIN, OUTPUT);
119    digitalWrite(POWER_PIN, LOW); // Power-on LED strip
120  #endif
121    strip.begin();                // Allocate DotStar buffer, init SPI
122    strip.clear();                // Make sure strip is clear
123    strip.show();                 // before measuring battery
124  
125    // Display battery level bargraph on startup.  It's just a vague estimate
126    // based on cell voltage (drops with discharge) but doesn't handle curve.
127    uint16_t mV  = readVoltage();
128    uint8_t  lvl = (mV >= BATT_MAX_MV) ? NUM_LEDS : // Full (or nearly)
129                   (mV <= BATT_MIN_MV) ?        1 : // Drained
130                   1 + ((mV - BATT_MIN_MV) * NUM_LEDS + (NUM_LEDS / 2)) /
131                   (BATT_MAX_MV - BATT_MIN_MV + 1); // # LEDs lit (1-NUM_LEDS)
132    for(uint8_t i=0; i<lvl; i++) {                  // Each LED to batt level...
133      uint8_t g = (i * 5 + 2) / NUM_LEDS;           // Red to green
134      strip.setPixelColor(i, 4-g, g, 0);
135      strip.show();                                 // Animate a bit
136      delay(250 / NUM_LEDS);
137    }
138    delay(1500);                                    // Hold last state a moment
139    strip.clear();                                  // Then clear strip
140    strip.show();
141  
142    imageInit(); // Initialize pointers for default image
143  
144  #ifdef SELECT_PIN
145    pinMode(SELECT_PIN, INPUT_PULLUP);
146  #endif
147  #ifdef MOTION_PIN
148    pinMode(MOTION_PIN, INPUT_PULLUP);
149    sleep();     // Sleep until motion detected
150  #endif
151  }
152  
153  // GLOBAL STATE STUFF ------------------------------------------------------
154  
155  uint32_t lastImageTime = 0L; // Time of last image change
156  #ifdef MOTION_PIN
157  uint32_t prev          = 0L; // Used for sleep timing
158  #endif
159  uint8_t  imageNumber   = 0,  // Current image being displayed
160           imageType,          // Image type: PALETTE[1,4,8] or TRUECOLOR
161          *imagePalette,       // -> palette data in PROGMEM
162          *imagePixels,        // -> pixel data in PROGMEM
163           palette[16][3];     // RAM-based color table for 1- or 4-bit images
164  line_t   imageLines,         // Number of lines in active image
165           imageLine;          // Current line number in image
166  #ifdef SELECT_PIN
167  uint8_t  debounce      = 0;  // Debounce counter for image select pin
168  #endif
169  
170  void imageInit() { // Initialize global image state for current imageNumber
171    imageType    = pgm_read_byte(&images[imageNumber].type);
172  #ifdef __AVR_ATtiny85__
173    imageLines   = pgm_read_byte(&images[imageNumber].lines);
174  #else
175    imageLines   = pgm_read_word(&images[imageNumber].lines);
176  #endif
177    imageLine    = 0;
178    imagePalette = (uint8_t *)pgm_read_word(&images[imageNumber].palette);
179    imagePixels  = (uint8_t *)pgm_read_word(&images[imageNumber].pixels);
180    // 1- and 4-bit images have their color palette loaded into RAM both for
181    // faster access and to allow dynamic color changing.  Not done w/8-bit
182    // because that would require inordinate RAM (328P could handle it, but
183    // I'd rather keep the RAM free for other features in the future).
184    if(imageType == PALETTE1)      memcpy_P(palette, imagePalette,  2 * 3);
185    else if(imageType == PALETTE4) memcpy_P(palette, imagePalette, 16 * 3);
186    lastImageTime = millis(); // Save time of image init for next auto-cycle
187  }
188  
189  void nextImage(void) {
190    if(++imageNumber >= NUM_IMAGES) imageNumber = 0;
191    imageInit();
192  }
193  
194  // MAIN LOOP ---------------------------------------------------------------
195  
196  void loop() {
197    uint32_t t = millis();               // Current time, milliseconds
198  #ifdef MOTION_PIN
199    // Tried to do this with watchdog timer but encountered gas pains, so...
200    if(!digitalRead(MOTION_PIN)) {       // Vibration switch pulled down?
201      prev = t;                          // Yes, reset timer
202    } else if((t - prev) > SLEEP_TIME) { // No, SLEEP_TIME elapsed w/no switch?
203      sleep();                           // Power down
204      prev = t;                          // Reset timer on wake
205    }
206  #endif
207  
208    if(autoCycle) {
209      if((t - lastImageTime) >= (CYCLE_TIME * 1000L)) nextImage();
210      // CPU clocks vary slightly; multiple poi won't stay in perfect sync.
211      // Keep this in mind when using auto-cycle mode, you may want to cull
212      // the image selection to avoid unintentional regrettable combinations.
213    }
214  #ifdef SELECT_PIN
215    if(digitalRead(SELECT_PIN)) {        // Image select?
216      debounce = 0;                      // Not pressed -- reset counter
217    } else {                             // Pressed...
218      if(++debounce >= 25) {             // Debounce input
219        nextImage();                     // Switch to next image
220        while(!digitalRead(SELECT_PIN)); // Wait for release
221        // If held 1+ sec, toggle auto-cycle mode on/off
222        if((millis() - t) >= 1000L) autoCycle = !autoCycle;
223        debounce = 0;
224      }
225    }
226  #endif
227  
228    // Transfer one scanline from pixel data to LED strip:
229  
230    // If you're really pressed for graphics space and need just a few extra
231    // scanlines, and know for a fact you won't be using certain image modes,
232    // you can comment out the corresponding blocks below.  e.g. PALETTE8 and
233    // TRUECOLOR are somewhat impractical on Trinket, and commenting them out
234    // can free up nearly 200 bytes of extra image storage.
235  
236    switch(imageType) {
237  
238      case PALETTE1: { // 1-bit (2 color) palette-based image
239        uint8_t  pixelNum = 0, byteNum, bitNum, pixels, idx,
240                *ptr = (uint8_t *)&imagePixels[imageLine * NUM_LEDS / 8];
241        for(byteNum = NUM_LEDS/8; byteNum--; ) { // Always padded to next byte
242          pixels = pgm_read_byte(ptr++);  // 8 pixels of data (pixel 0 = LSB)
243          for(bitNum = 8; bitNum--; pixels >>= 1) {
244            idx = pixels & 1; // Color table index for pixel (0 or 1)
245            strip.setPixelColor(pixelNum++,
246              palette[idx][0], palette[idx][1], palette[idx][2]);
247          }
248        }
249        break;
250      }
251  
252      case PALETTE4: { // 4-bit (16 color) palette-based image
253        uint8_t  pixelNum, p1, p2,
254                *ptr = (uint8_t *)&imagePixels[imageLine * NUM_LEDS / 2];
255        for(pixelNum = 0; pixelNum < NUM_LEDS; ) {
256          p2  = pgm_read_byte(ptr++); // Data for two pixels...
257          p1  = p2 >> 4;              // Shift down 4 bits for first pixel
258          p2 &= 0x0F;                 // Mask out low 4 bits for second pixel
259          strip.setPixelColor(pixelNum++,
260            palette[p1][0], palette[p1][1], palette[p1][2]);
261          strip.setPixelColor(pixelNum++,
262            palette[p2][0], palette[p2][1], palette[p2][2]);
263        }
264        break;
265      }
266  
267      case PALETTE8: { // 8-bit (256 color) PROGMEM-palette-based image
268        uint16_t  o;
269        uint8_t   pixelNum,
270                 *ptr = (uint8_t *)&imagePixels[imageLine * NUM_LEDS];
271        for(pixelNum = 0; pixelNum < NUM_LEDS; pixelNum++) {
272          o = pgm_read_byte(ptr++) * 3; // Offset into imagePalette
273          strip.setPixelColor(pixelNum,
274            pgm_read_byte(&imagePalette[o]),
275            pgm_read_byte(&imagePalette[o + 1]),
276            pgm_read_byte(&imagePalette[o + 2]));
277        }
278        break;
279      }
280  
281      case TRUECOLOR: { // 24-bit ('truecolor') image (no palette)
282        uint8_t  pixelNum, r, g, b,
283                *ptr = (uint8_t *)&imagePixels[imageLine * NUM_LEDS * 3];
284        for(pixelNum = 0; pixelNum < NUM_LEDS; pixelNum++) {
285          r = pgm_read_byte(ptr++);
286          g = pgm_read_byte(ptr++);
287          b = pgm_read_byte(ptr++);
288          strip.setPixelColor(pixelNum, r, g, b);
289        }
290        break;
291      }
292    }
293  
294    strip.show(); // Refresh LEDs
295  #if !defined(LED_DATA_PIN) && !defined(LED_CLOCK_PIN)
296    delayMicroseconds(900);  // Because hardware SPI is ludicrously fast
297  #endif
298    if(++imageLine >= imageLines) imageLine = 0; // Next scanline, wrap around
299  }
300  
301  // POWER-SAVING STUFF -- Relentlessly non-portable -------------------------
302  
303  #ifdef MOTION_PIN
304  void sleep() {
305  
306    // Turn off LEDs...
307    strip.clear();                 // Issue '0' data
308    strip.show();
309  #ifdef POWER_PIN
310    digitalWrite(POWER_PIN, HIGH); // Cut power
311  #if !defined(LED_DATA_PIN) && !defined(LED_CLOCK_PIN)
312  #ifdef __AVR_ATtiny85__
313    pinMode(1, INPUT);             // Set SPI data & clock to inputs else
314    pinMode(2, INPUT);             // DotStars power parasitically, jerks.
315  #else
316    pinMode(11, INPUT);
317    pinMode(13, INPUT);
318  #endif // ATtiny
319  #endif // Data/clock/pins
320  #endif // POWER_PIN
321  
322    power_all_disable(); // Peripherals ALL OFF, best sleep-state battery use
323  
324    // Enable pin-change interrupt on motion pin
325  #ifdef __AVR_ATtiny85__
326    PCMSK = _BV(MOTION_PIN);  // Pin mask
327    GIMSK = _BV(PCIE);        // Interrupt enable
328  #else
329    volatile uint8_t *p = portInputRegister(digitalPinToPort(MOTION_PIN));
330    if(p == &PIND) {          // Pins 0-7 = PCINT16-23
331      PCMSK2 = _BV(MOTION_PIN);
332      PCICR  = _BV(PCIE2);
333    } else if(p == &PINB) {   // Pins 8-13 = PCINT0-5
334      PCMSK0 = _BV(MOTION_PIN- 8);
335      PCICR  = _BV(PCIE0);
336    } else if(p == &PINC) {   // Pins 14-20 = PCINT8-14
337      PCMSK1 = _BV(MOTION_PIN-14);
338      PCICR  = _BV(PCIE1);
339    }
340  #endif
341  
342    // If select pin is enabled, that wakes too!
343  #ifdef SELECT_PIN
344    debounce = 0;
345  #ifdef __AVR_ATtiny85__
346    PCMSK |= _BV(SELECT_PIN); // Add'l pin mask
347  #else
348    volatile uint8_t *p = portInputRegister(digitalPinToPort(SELECT_PIN));
349    if(p == &PIND) {        // Pins 0-7 = PCINT16-23
350      PCMSK2 = _BV(SELECT_PIN);
351      PCICR  = _BV(PCIE2);
352    } else if(p == &PINB) { // Pins 8-13 = PCINT0-5
353      PCMSK0 = _BV(SELECT_PIN- 8);
354      PCICR  = _BV(PCIE0);
355    } else if(p == &PINC) { // Pins 14-20 = PCINT8-14
356      PCMSK1 = _BV(SELECT_PIN-14);
357      PCICR  = _BV(PCIE1);
358    }
359  #endif // ATtiny
360  #endif // SELECT_PIN
361  
362    set_sleep_mode(SLEEP_MODE_PWR_DOWN); // Deepest sleep mode
363    sleep_enable();
364    interrupts();
365    sleep_mode();                        // Power down
366  
367    // Resumes here on wake
368  
369    // Clear pin change settings so interrupt won't fire again
370  #ifdef __AVR_ATtiny85__
371    GIMSK = PCMSK = 0;
372  #else
373    PCICR = PCMSK0 = PCMSK1 = PCMSK2 = 0;
374  #endif
375    power_timer0_enable();        // Used by millis()
376  #if !defined(LED_DATA_PIN) && !defined(LED_CLOCK_PIN)
377  #ifdef __AVR_ATtiny85__
378    pinMode(1, OUTPUT);           // Re-enable SPI pins
379    pinMode(2, OUTPUT);
380    power_usi_enable();           // Used by DotStar
381  #else
382    pinMode(11, OUTPUT);          // Re-enable SPI pins
383    pinMode(13, OUTPUT);
384    power_spi_enable();           // Used by DotStar
385  #endif // ATtiny
386  #endif // Data/clock pins
387  #ifdef POWER_PIN
388    digitalWrite(POWER_PIN, LOW); // Power-up LEDs
389  #endif
390    prev = millis();              // Save wake time
391  }
392  
393  EMPTY_INTERRUPT(PCINT0_vect); // Pin change (does nothing, but required)
394  #ifndef __AVR_ATtiny85__
395  ISR(PCINT1_vect, ISR_ALIASOF(PCINT0_vect));
396  ISR(PCINT2_vect, ISR_ALIASOF(PCINT0_vect));
397  #endif
398  
399  #endif // MOTION_PIN
400  
401  // Battery monitoring idea adapted from JeeLabs article:
402  // jeelabs.org/2012/05/04/measuring-vcc-via-the-bandgap/
403  // Code from Adafruit TimeSquare project, added Trinket support.
404  // In a pinch, the poi code can work on a 3V Trinket, but the battery
405  // monitor will not work correctly (due to the 3.3V regulator), so
406  // maybe just comment out any reference to this code in that case.
407  uint16_t readVoltage() {
408    int      i, prev;
409    uint8_t  count;
410    uint16_t mV;
411  
412    // Select AVcc voltage reference + Bandgap (1.8V) input
413  #ifdef __AVR_ATtiny85__
414    ADMUX  = _BV(MUX3) | _BV(MUX2);
415  #else
416    ADMUX  = _BV(REFS0) |
417             _BV(MUX3)  | _BV(MUX2) | _BV(MUX1);
418  #endif
419    ADCSRA = _BV(ADEN)  |                          // Enable ADC
420             _BV(ADPS2) | _BV(ADPS1) | _BV(ADPS0); // 1/128 prescaler (125 KHz)
421    // Datasheet notes that the first bandgap reading is usually garbage as
422    // voltages are stabilizing.  It practice, it seems to take a bit longer
423    // than that.  Tried various delays, but still inconsistent and kludgey.
424    // Instead, repeated readings are taken until four concurrent readings
425    // stabilize within 10 mV.
426    for(prev=9999, count=0; count<4; ) {
427      for(ADCSRA |= _BV(ADSC); ADCSRA & _BV(ADSC); ); // Start, await ADC conv.
428      i  = ADC;                                       // Result
429      mV = i ? (1100L * 1023 / i) : 0;                // Scale to millivolts
430      if(abs((int)mV - prev) <= 10) count++;   // +1 stable reading
431      else                          count = 0; // too much change, start over
432      prev = mV;
433    }
434    ADCSRA = 0; // ADC off
435    return mV;
436  }