Home

Standlight for power LEDs

November 26, 2010

My brother wanted one of these ages ago, I realized it would probably need software.
Software had two bugs in it, initially — forgot to say “unsigned” when that was what I meant, and had a fencepost error that had the standlight on twice as long as intended.

But the circuit worked the very. first. time. I’ve never built one of these before, I was quite proud that it worked. The standlight controller contains an Atmel ATTiny85 (overkill, for now) that watches the wheel rotate by monitoring zero crossings in the AC output from the hub alternator. When the hub spins too slow, it activates a battery-connecting circuit; when it spins fast enough, it deactivates the battery-connecting circuit. If there is no wheel motion for “long enough”, it deactivates the battery-connecting circuit. I checked, with a multimeter, to verify that no current flowed into the battery when the wheel was spinning fast enough. The supply current is about 70-80 mA, which should support many hours of on-time.

First, the newly reconfigured rectifier-doubler-current-limited-voltage-limited LED controller. Wires are color coded — blue is AC from the “dynamo” (really an alternator), black is ground, red is 5v, white is unregulated rectified DC and shunt+, gray is shunt-, orange is LED+, green is LED-.

IMG_0337.JPG

Next is the standlight controller. It has parts on both sides. Top:

IMG_0346.jpg

and bottom:

IMG_0342.JPG

And the circuit diagram:

Switch4-sch.png

The chip has software in it (this is the latest, as of 2010-01-22, best-working version):

/* Include useful pre-defined functions */
#include <avr/interrupt.h>    // Defines pins, ports, etc to make programs easier to read
#define F_CPU 800000UL        // Sets up the default speed for delay.h
#include <util/delay.h>
#include <avr/sleep.h>
#include <avr/power.h>

volatile unsigned char nothing; /* dummy for empty interrupt handlers. */

volatile unsigned char timerTick;
volatile unsigned char crossingOccurred;

#define MY_HZ 3125
#define DEBOUNCE 313 // 1/10th second.

#define NOT_ROLLING 0 // wait to see what to do next
#define ROLLING_OFF 1 // wheel turning quickly enough to supply power
#define ROLLING_ON 2  // wheel turning, but too slow for power, battery on
#define STOPPED_ON 3  // wheel recently stopped, keep battery on
#define ON 4          // battery on (switch in ON)
#define TOGGLED_OFF 5 // battery off (switch toggle away from ON).  Stay off, unless many turns.

ISR(SIG_COMPARATOR) {
  /* Wheel turn */
  crossingOccurred++;
}

ISR(TIM0_COMPA_vect) {
  timerTick++;
}

ISR(SIG_ADC) {
  nothing = 0;
}

/* Not yet doing this */
unsigned char  adcsra = 204; /* 128=enable,64=start,0,0,8=int act,4=/16 clock scale */
unsigned char  adcsrb = 0; /* 0 */

int main() {
  char state = NOT_ROLLING;
  
  unsigned char debounced_b3;
  unsigned int b3_history = 0; /* Implement debouncing by counting values. */
  unsigned int crossing_history = 0; /* Need to "debounce" this against bobbles at the powerout. */
  unsigned int ticks_since_last_crossing = 65535;
  unsigned int ticks_at_last_crossing = 65535;
  unsigned int secs_since_last_crossing = 65535;
  
  cli();
  
  TCCR0A = 2; /* Clear timer on compare match */
  TCCR0B = 3; /* /64 prescalar = 15625Hz */
  OCR0A =  5; /* /5 = 3125 Hz */
  TIMSK = 1 << 4; /* OCIE0A */  
  TIFR = 1 << 4;  /* OCF0A */
  
  /* B0,1 are analog comparator inputs for wheel spins
     (56 zero crossing/rotation, about 8/foot of travel).
     
     B2 is main voltage (35v scaled to 5) -- currently unused
     
     B3 is external pin, pulled up, connected to toggle.
     0 == on, and stay on (even if spinning).
     0 -> 1 == off now (unless reset by wheel spin).
  */
   
  DDRB = _BV(PB4);  /* enable output on port B, pin 4 */
  DIDR0 = _BV(PB0)|_BV(PB1)|_BV(PB2); /* disable digital input on B0-2 */
  PORTB = _BV(PB3); /* Write a one to B3 for pull-up */
  ADMUX = 3; /* pin PB2, Vcc (5V) reference, ADLAR(32) = 0 */
  ACSR = _BV(ACIE); /* Enable analog compare interrupts */

  /* Initial state of B3 history. */
  debounced_b3 = PINB & _BV(PB3);
  b3_history = (debounced_b3 == 0) ? 0 : DEBOUNCE;
  
  /* Enter loop with interrupts disabled. */
  while(1){
    sleep_enable();
    sei();
    sleep_cpu();
    sleep_disable();
    cli();
     
    {
      unsigned char this_crossing_occurred = crossingOccurred;
      unsigned char this_timer_tick = timerTick;
      // Might zero and then enable interrupts here.
      unsigned char this_b3 = PINB & _BV(PB3);
      unsigned int this_b3_history = b3_history;
      char toggle = 0;
      timerTick = 0;
      crossingOccurred = 0;

     
      if (this_crossing_occurred) {
        ticks_at_last_crossing = ticks_since_last_crossing;
        ticks_since_last_crossing = 0;
        secs_since_last_crossing = 0;
        // Note that the first crossing "won't count".
        // Fewer than 5 ticks means 60mph, assume spurious
        // Note that this is wrong for small wheels.
        // 60 mph = 88 fps = 616Hz.  3125/616 = 5.07
        if (ticks_at_last_crossing < 300  && ticks_at_last_crossing > 4) {
	  unsigned int x = crossing_history+1;
	  if (x > crossing_history)
	    crossing_history = x;
        } 
      }
               
      if (this_timer_tick) {
	/* Following a timer interrupt, do this. */
	
	unsigned int x = ticks_since_last_crossing + this_timer_tick;
	if (x > ticks_since_last_crossing)
	   ticks_since_last_crossing = x;
        
	while (ticks_since_last_crossing >= MY_HZ) {
	   x = secs_since_last_crossing + 1;
	   ticks_since_last_crossing -= MY_HZ;
	   if (x > secs_since_last_crossing)
	      secs_since_last_crossing = x;
	}
	   
	/* Debouncing -- must read DEBOUNCE equal values to register signal */
	if (this_b3 == 0) {
	  if (this_b3_history > 0) {
	    b3_history = this_b3_history - 1;
	  } else {
	    if (debounced_b3 != 0) {
	      toggle = 1;
	      debounced_b3 = 0;
	    }
	  }
	} else {
	  if (this_b3_history < DEBOUNCE) {
	    b3_history = this_b3_history + 1;
	  } else {
	    if (debounced_b3 == 0) {
	      toggle = 1;
	      debounced_b3 = 1;
	    }
	  }
	}
         
	if (secs_since_last_crossing > 0) {
	  crossing_history = 0;
        }
      }
      /* 
	 inputs:
	 ticks_at_last_crossing = ticks for last completed movement
	 ticks_since_last_crossing = ticks accumulated by current incomplete movement
	 debounced_b3 = current value of toggle switch
	 toggle = just changed switch value
	 crossing_history = number of "fast enough" wheel movements in recent history.
      */
    
      if (debounced_b3 == 0) { /* connect the battery, period. */
	state = ON;
      } else if (toggle) {
	state = TOGGLED_OFF;
	crossing_history = 0;  // Bias to be off
      } else 	      
	switch (state) {
	case TOGGLED_OFF:
	  if (crossing_history >= 64) {
	    // be a little sticky about staying off.
	    state = NOT_ROLLING;
	  }
	  break;

	case NOT_ROLLING:
	  if (crossing_history >= 4) {
	    // be a little sticky about staying off.
	    state = ROLLING_OFF;
	  }
	  break;

	case ROLLING_OFF:
	  if (ticks_since_last_crossing > (unsigned int) 70)
	    state = ROLLING_ON;
	  break;

	case ROLLING_ON:
	  if (ticks_at_last_crossing < (unsigned int) 60 &&
	      ticks_since_last_crossing < (unsigned int) 60 && 
	      secs_since_last_crossing == 0 )
	    state = ROLLING_OFF;
	    // no crossings wins -- a truly instant wheel stop might do this.
	  if (crossing_history == 0)
	    state = STOPPED_ON;
	  break;

	case STOPPED_ON:
	  if (secs_since_last_crossing >= (unsigned int) 30)
	    state = NOT_ROLLING;
	  if (crossing_history >= 8 ) 
	    state = ROLLING_ON;
	  break;

	case ON:
	  break;
        }
        
      switch (state) {
      case NOT_ROLLING:
      case TOGGLED_OFF:
      case ROLLING_OFF:
	PORTB = _BV(PB3);
	break;

      case ROLLING_ON:
      case STOPPED_ON:
      case ON:
	PORTB = _BV(PB4)|_BV(PB3);
	break;
      }
    }
  }

  return(0);
}

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.