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 }