Automatisch fietsachterlicht

Het is weer herfst geworden. Een heerlijke tijd van het jaar, met een aantal traditionele evenementen zoals kastanjeszoeken en het controleren van de fietsverlichting. De achterlichten van onze beide kinderen zijn van het handmatige type en hoewel het veel goedkoper niet al te duur is om deze door een kant-en-klare automatische, zelfaanschakelende lamp te vervangen, ging me dat deze keer te ver.

LED fietsachterlichten
Een standaard fietsachterlicht met LED bestaat uit twee penlite-batterijen, een schakelaar, één of meer LED’s met wat weerstanden. De fietseigenaar schakelt de verlichting in met de schakelaar en schakelt deze na gebruik weer uit. Wat ik graag wil, is dat de fietseigenaar de fietsachterlicht wel inschakelt, maar dat dit achterlicht zelf de LED’s aanzet als dat nodig is. En het is pas nodig als er onvoldoende omgevingslicht is, én de fiets in beweging is. Ik heb enkele goedkope LED-fietsachterlichten gekocht. Hierin zitten 2 penlight-batterijen en 3 LED’s met elk een serieweerstand van 390 ohm. De 3 LED’s gebruiken gezamenlijk 70 mA.

Beetje Arduino, beetje Processing, beetje ISP
Uit de vorige jaren heb ik nog wat Arduino-spullen verzameld. Dit is allemaal te groot (en te duur) voor een fietsachterlicht, maar Atmel, de fabrikant van de microcontrollers die ook voor de Arduino’s worden gebruikt, heeft kleine chips op de markt die de ATtiny heten. Deze is er ondermeer in een 8-pins behuizing, met 5 universele I/O pennen, 8K aan flash-programmageheugen en 512 bytes werkgeheugen (de “ATtiny85”). En met een beetje moeite programmeerbaar met een Arduino, die zich voor die gelegenheid als “In Circuit Programmer” gedraagt. In dezelfde Processing taal als voor de Arduino geschikt is. En, belangrijk, de ATtiny85 is voor minder dan een euro te verkrijgen.

Sensoren
Om vast te stellen dat er onvoldoende omgevingslicht beschikbaar is, is een LDR prima geschikt. De ATtiny85 kan de weerstand hiervan inlezen met een analoge ingangspin. Een tiltschakelaar gedraagt zich als een soort bewegingssensor, prima te detecteren met een digitale ingangspin. En de LED’s kunnen, middels een transistor met open collector, met een digitale uitgangspin worden geschakeld (deze digitale pinnen kunnen maximaal 40 mA aansturen, nipt te weinig voor de benodigde 70 mA, vandaar de transistor). Kortom: het hele circuit gaat bestaan uit de volgende onderdelen:

  • ATtiny85 als regelneef
  • LDR voor het meting van omgevingslicht
  • Tiltschakelaar voor het bepalen van beweging
  • BC547 transistor voor het aansturen van de LED’s
  • Enkele weerstanden
  • De bestaande onderdelen: batterijen, schakelaar, LED’s
Het schema hieronder laat zien, dat de ATtiny85 het hart vormt van een schakeling waarin verder een tilt-schakelaar (horizontaal installeren zodat bij beweging van de fiets steeds het contact “stuitert”), een lichtgevoelige weerstand en een transistor zijn opgenomen. De LED’s en bijbehorende serieweerstanden, de batterijen en de schakelaar S1 zijn onderdeel van de bestaande achterlamp. Het geheel past op een stukje gaatjesprint van 3 x 3 centimeter.
Het leukst is natuurlijk, om hier een eigen stukje software voor te schrijven. Immers, dat opent de deur naar een persoonlijke noot. Ik heb ervoor gekozen om bij het aanschakelen van het achterlight de lamp even 3 keer te laten knipperen. Op die manier is het goed vast te stellen dat de elektronica het doet.
/*
 Bicycle automatic tail light
 Placed in the public domain
 Written by Rudi Niemeijer (10-2012)
*/

// Some constant values
#define ledPin 0 // Digital Output 0 (also PWM), AtTiny85 pin 5
#define ldrPin 3 // Analog Input 3, AtTiny85 pin 2 
#define vibPin 4 // Digital Input 1
#define hasNoMovementSensor true
#define milliSecondsToMeasure 1000
#define lightTreshold 10 // The higher the value, the sooner the LED switches on
#define movementTreshold 5 // At least this many diffs between 0 and 1 in milliSecondsToMeasure
#define milliSecondsToPauseInDarkBeforeParking 30000 // Stay on a while

// Define the four state the bicycle will be in
#define stateParked 0
#define stateMoveInLight 1
#define stateMoveInDark 2
#define statePauseInDark 3 // We stopped moving while the light was on

int i;
unsigned long t;
int inDaylight = true;
int hasMoved = false;
int curState = stateParked;
unsigned long sumLightValue;
int numMovementsLow;
int numMovementsHigh;
unsigned long timeSinceStartPauseInDark = 0;
int numMeasurements;

void flashLed(int numTimes) { // Flash the LED numTimes
 for (i = 0; i < numTimes; i++) {
   digitalWrite(ledPin, HIGH); // turn the LED on (HIGH is the voltage level)
   delay(200); // wait for a second
   digitalWrite(ledPin, LOW); // turn the LED off by making the voltage LOW
   delay(100); // wait for a second
 }
}

int readLdr() { // Read the LDR values, while switching temporary off the LED
 int analogValue;
 digitalWrite(ledPin, LOW);
 analogValue = analogRead(ldrPin);
 if (curState == stateMoveInDark) {
   digitalWrite(ledPin, HIGH);
 }
 return analogValue;
}

void burstLed() {
 for (i = 0; i < 25; i++) {
   digitalWrite(ledPin, HIGH);
   delay(25);
   digitalWrite(ledPin, LOW);
   delay(25);
 }
}

int difference(int a, int b) {
 if (a > b) {
   return a - b;
 } else {
   return b - a;
 }
}

void setup() { 
 pinMode(ledPin, OUTPUT);
 pinMode(vibPin, INPUT);
 flashLed(3); 
}

// the loop routine runs over and over again forever:
void loop() {
 switch (curState) {
   case stateParked:
     digitalWrite(ledPin, LOW);
     break;
   case stateMoveInLight:
     digitalWrite(ledPin, LOW);
     break;
   case stateMoveInDark:
     digitalWrite(ledPin, HIGH);
     break;
   case statePauseInDark:
     burstLed();
     break;
 }

 t = millis();
 sumLightValue = 0;
 numMovementsLow = 0;
 numMovementsHigh = 0;
 numMeasurements = 0;

 while (millis() - t < milliSecondsToMeasure) {
   sumLightValue = sumLightValue + readLdr();
   if (digitalRead(vibPin) == 0) {
     numMovementsLow = numMovementsLow + 1;
   } else {
     numMovementsHigh = numMovementsHigh + 1;
   }
   numMeasurements = numMeasurements + 1;
 }

 if ((difference(numMovementsHigh, numMovementsLow) > movementTreshold) || hasNoMovementSensor) {
   hasMoved = true;
 } else {
   hasMoved = false;
 }

 if (sumLightValue / numMeasurements < lightTreshold) {
   inDaylight = false;
 } else {
   inDaylight = true;
 }

 // Determine if curState has to change
 if (!hasMoved && inDaylight) {
   curState = stateParked;
 }
 if (hasMoved && inDaylight) {
   curState = stateMoveInLight;
 }
 if (hasMoved && !inDaylight) {
   curState = stateMoveInDark;
 }
 if (!hasMoved && !inDaylight && (curState == stateMoveInDark)) {
   timeSinceStartPauseInDark = millis();
   curState = statePauseInDark;
 }
 if (!hasMoved && !inDaylight && (curState == statePauseInDark) && (millis() - timeSinceStartPauseInDark) > milliSecondsToPauseInDarkBeforeParking) {
   curState = stateParked;
 }
}

Slotwoord
Het maken van een zelfschakelend achterlicht is ook voor een minder ervaren hobbyist eenvoudig genoeg te doen. Het is niet moeilijk. Maar het kost wat tijd om de ontwikkelstraat op poten te zetten. Als je eenmaal de smaak te pakken hebt om een ATtiny wat willekeurig eenvoudig regelwerk te laten doen, dan zijn de mogelijkheden onbeperkt.

Het achterlicht doet het ondertussen.