In voorbereidingVuurvliegjes

Een simpel project met de AVR ATtiny2313

Zonnecel-tuinlampIk had nog een aantal oude tuinlampen met zonne-cellen liggen; van die dingen met bovenin zonnecellen en een lichtdetector, onderin een LED en van binnen twee oplaadbare NiCd cellen. Was ik op uitgekeken, kan ik daar niet iets anders mee? Wat me leuk leek in de tuin waren 'vuurvliegjes', subtiele oplichtende LEDjes die in een willekeurig patroon aan en uit gingen. En dan niet hard knipperend, maar vloeiend aan en uit.

Gebaseerd op een van die tuinlampen is er toen dit mini-projectje ontstaan. Een AVR ATtiny2313 microprocessor (zie mijn 'Starten met de ATtiny2313 pagina) stuurt via de 4 PWM-uitgangen 4x 2 gele LEDs aan. Het geheel (behalve de LEDs) is ingebouwd in de oude tuinlamp, wordt geladen door de zonnecel, en wordt actief in het donker. De LEDs hangen in een boom.

Hoe is dit geheel opgebouwd? Het bestaat uit een aantal delen:De binnenkant van de vuurvliegjes

  • de zonnecellen uit de lamp laden de NiMH batterijen, die vervolgens 's nachts de ATtiny voeden
  • de ATtiny meet regelmatig of het al donker is, is het donker dan gaat het LED programma draaien
  • de PWM-uitgangen van de ATtiny regelen de helderheid van de LEDs
  • en een voltage booster met een transistor en spoel verhoogt de batterijspanning (2..3V) tot de waarde die voor de LEDs nodig is (4V)

Zonne-energie

Hoeveel zonne-energie krijgen we eigenlijk hier in Nederland? Ongeveer 1000 kWh/m² per jaar, in de zomer ongeveer 10x zo veel als in de winter. Gemiddeld dus zo'n 2.7 kWh per dag, bij 10 uur/dag dus 270 W/m². Voor een cel van 10 cm², een rendement van 10% en een spanning van 3V is dat zo'n 100 mAh in de zomer, en maar 10 mAh in de winter...

Van belang is natuurlijk het energiegebruik. Zon is in Nederland schaars (zie kader Zonne-energie), dus zeker in ruststand moet het ultra low power zijn... In de hardware is er voor gezorgd dat in stand-by mode de onderdelen buiten de ATtiny uitgeschakeld zijn, alleen incidenteel wordt het lichtmeetcircuit een keer per seconde zo'n 20 microseconden van energie voorzien om te kijken of het al donker is. Totale energiegebruik is in rust daardoor slechts 50 µA (of slechts 25 µA als de batterijspanning te ver daalt, omdat de processor dan helemaal plat gaat via de 'brown-out detection op 1.8V).

Het circuit

De vuurvliegjes-demo op de ElektorLive dag

Demo-opstelling op
ElektorLive-dag 2010

Hieronder de beschrijving van de delen van het schema (download het hele schema: vuurvlieg_schema.pdf): processor, laadcircuit, voltage booster, LED control, lichtmeting.

De processor

Schematic entry en PCB

Een plezierig en gratis pakket om schema's en de bijhorende print-layouts te maken is Eagle Light (Easily Applicable Graphical Layout Editor)

De processor is op de 'standaard' manier aangesloten. Het draait op de low-power interne 128 kHz RC oscillator, niet erg nauwkeurig en snel maar dat is voor deze toepassing ook niet nodig. De processor probeert zo veel mogelijk in sleep/idle mode te zijn, om energie te besparen. Na iedere actie schakelt hij naar idle-mode, om vervolgens bij een timer-interrupt weer wakker te worden, bijvoorbeeld een keer per seconde.

De bron voor de klok en dergelijken worden op de ATtiny ingesteld door 'fuses', een soort elektrisch programmeerbare zekeringen. Deze fuses zijn geprogrammeerd op 128 kHz interne low-power RC oscillator, en BOD aan (brown-out detection; het resetten van de processor als de spanning te ver daalt) op 1.8 Volt. Dit laatste heeft tot gevolg dat de processor in ultra low power mode gaat als de spanning daalt (25 µA voor het hele circuit) en pas weer wakker wordt als de spanning weer boven de 1.8 Volt komt; extra batterij-bescherming.

Het laadcircuit

Het laadcircuit stuurt de stroom van de zonnecellen direct naar de NiMH batterijen. Wel met een diode ertussen; in het donker zou anders de batterij via een lekstroom door de zonnecellen weer ontladen. Door de beperkte capaciteit van de zonnecel ben ik in Nederland niet bang voor overladen (zie kader 'zonne-energie'), maar eventueel kan je in de zomer (tip: meet de lengte van de dag) de LEDs overdag inschakelen om het laden te beperken...

De voltage boosterPower booster

De voltage booster is een variant op een geschakelde voeding, waarbij de schakelfrequentie door de ATtiny wordt verzorgt. Omdat de ATtiny op een lage klok loopt, is de schakelfrequentie voor de voeding ook relatief laag (64 kHz, hoogste waarde die ik uit de baudrate-generator kan krijgen met de CPU op 128 kHz). Als gevolg hiervan is er een relatief grote spoel nodig, die ik nog in een rommelbakje had liggen (type-nummer in het schema slaat nergens op, maar had de goede afmetingen voor de PCB, waarde was ongeveer 1 milli-Henry). Als de transistor ingeschakeld wordt gaat er een stroom door de spoel lopen. Wordt nu deze stroom onderbroken (transistor schakelt uit), dan ontstaat er een spanningspuls, die door de diode wordt gelijkgericht.

En uiteraard: als er geen enkele LED voor langere periode aan is, dan schakelt de ATtiny ook de oscillator van de geschakelde voeding af om energie te besparen.

Pas op: onbelast kan deze spanning behoorlijk oplopen (tientallen volts), en dus je circuit beschadigen! Daarom zit er een zenerdiode van 5.1 Volt op de uitgang. Is er geen enkele LED aan, dan beperkt deze de spanning. Zodra een LED aangeschakeld is zakt deze spanning tot de spanningsval over de LEDs (twee gele LEDs in serie geeft 2x2 = 4 Volt). Je moet de waarde van de zener aanpassen aan het type (kleur) en aantal LEDs.

Pas op: als er geen LEDs aan zijn is de voeding onbelast, en laden de condensatoren tot de maximum spanning. Als je te grote condensatoren plaatst laden deze op tot 5.1 Volt, en bij het inschakelen van een LED ontladen ze zich in een keer tot 4 Volt, dit kan grote stromen geven... Daarom beperkt tot 2x 0.1 µF.

Ik had oorspronkelijk geen 5.1V zener als bescherming, en een 33µF elco --> spanning liep op tot >15 Volt; zodra je dan een LED activeert brand'ie nog maar een keer... De elco op 15 Volt bevat een behoorlijke hoeveelheid energie! Oeps... had ik kunnen bedenken. Onbelast kan de piekspanning trouwens wel tot >60V oplopen!

LED-besturing

De LEDs worden via een transistor aangesloten op de PWM-uitgang van de ATtiny geschakeld. De stroom door de LEDs is beperkt, en zou direct door de ATtiny geschakeld kunnen worden. Maar... de spanning (max 5.1 V) is hoger dan de voedingsspanning van de ATtiny (2..3V), en dat gaat niet samen (pinnen mogen max Vcc+0.5V hebben). Vandaar deze extra transistoren!

De helderheid van de LEDs wordt door pulsbreedte modulatie aangestuurd (in phase correct mode), de frequentie hiervan is 128 kHz/512 = 250 Hz. Deze frequentie kan het menselijk oog niet meer als knipperen zien, we zien een mooi gelijkmatig gloeiende LED. De ATtiny heeft 4 PWM uitgangen, op iedere uitgang heb ik op het moment 2 LEDs in serie zitten.

LichtmetingSchema lichtdetectie

De vuurvliegjes-demo op de ElektorLive dag

De vuurvliegjes in de tuin
in het seringenboompje

De lichtmeting wordt gedaan met behulp van de analoge comparator in de ATtiny. Ik wil alleen maar weten of het donker danwel licht is, dus ik bepaal niet de waarde maar kijk alleen of het meer of minder is dan een ingestelde waarde. De LDR (lichtgevoelige weerstand) vormt samen met een weerstand R5 een spanningsdeler, de spanning hier wordt vergeleken met een vaste spanning uit de deler R6 en R7. Door deze delers loopt normaal stroom, wat weer jammer is voor het energiegebruik; daarom schakel ik dit circuit uit met Q6 (aangesloten op uitgang PD6) als er niet gemeten wordt. Alleen tijdens een meting staat het circuit enkele tientallen microseconden aan.

Ik maak geen gebruik van de ingebouwde spanningsreferentie; omdat de batterijspanning varieert  ook de spanning in de deler verandert; door twee delers te gebruiken veranderen deze op dezelfde manier en wordt de invloed van de batterijspanning weggewerkt.

Achteraf gezien had ik die transistor Q6 ook wel kunnen weglaten, en het circuit direct aan de output-pin van de tiny kunnen hangen, om op die manier de slaapstand te implementeren. Ach.

De bijhorende code is vrij simpel, maar leuk is hier het software filter: je wilt niet de zaak verstoren omdat er een vogel overvliegt die de sensor triggert. Daarom wordt er steeds een groepje van 8 metingen bekeken (bij een meting per seconde) en pas als deze 8 keer hetzelfde is vind er een statuswijziging plaats.

uint8_t light_samples = 1;    // 8 latest light samples; all equal?
uint8_t light_on = 0;         // assume dark to start with

uint8_t measure_light(void)   // what is current light level?
{                             // 'filter' over 8 samples
    ACSR = (0 << ACD);        // enable comp using external inputs
    SETBIT(PORTD, 1<<PD6);    // enable power on LDR
    _delay_us(10);            // 10 us stabilise (<util/delay.h>)

    light_samples <<= 1;      // shift one position, fill zero
    if(!(ACSR & (1 << ACO))) light_samples |= 1; // comparator result

    ACSR = (1 << ACD) | (0 << ACBG); // switch off comparator etc
    CLRBIT(PORTD, 1<<PD6);    // disable power on LDR
                              // only change if 8 identical samples:
    if(light_samples == 0xFF)   { light_on = 1;    }
    else if(light_samples == 0) { light_on = 0;    }

    return light_on;          // 1=light, 0=dark
}

SMD prototypingDe montage

Het geheel is op een stukje ouderwets gaatjesprint (een gat per eiland) gemonteerd. Het voordeel hiervan is dat je SMD onderdelen (met name de weerstanden) direct op de onderkant kunt solderen, een '0805' smd component (0.8 x 0.5 tiende inch, dus ongeveer 2 bij 1 mm) past netjes tussen twee eilandjes in, en is nog goed hanteerbaar. Het geheel is met zelfstrippend draad gesoldeerd.

Op de foto bovenaan deze pagina zit de print nog 'ondersteboven', normaal is deze omgedraaid gemonteerd zodat de componenten in de ruimte tussen de batterijen vallen. Ziet er mooi uit (en past dan in de gesloten behuizing), alleen zit de programmeer-connector daarmee op een onhandige plaats... Had beter een haaks type kunnen nemen.

Omdat het geheel buiten in een boom zit, moet de zaak tegen weersomstandigheden kunnen: ik heb de print met een heldere plastic-lak bespoten, om een beschermlaag aan te brengen. De eerste winter heeft het al buiten overleeft.

De software

Download source file: vuurvlieg.cState diagram

Op hoog niveau bestaat de software uit een state machine met drie toestanden: slapen, spelen en laden. In de slaap- en laad-stand is de processor meestal in idle mode; slechts een keer per seconde wordt er gekeken of het licht veranderd is. Hiermee wordt het stroomgebruik geminimaliseerd.

Tijdens het spelen is de processor vaker actief; maar wordt nog het meest gedaan door de peripherals als de hardware pulsbreedte-modulatoren. Deze draaien op 250 Hz; de frequentie waarmee ook de processor uit idle wordt gehaald (timer overflow interrupt). Meestal valt deze vervolgens weer snel in slaap, maar twee keer per seconde wordt er per LED gecheckt of de LED state machine moet worden bijgewerkt. Op dit moment gaat'ie na drie uur spelen via een time-out in nachtmode; dit zou in de zomer wat langer kunnen (meer zon, dus meer lading). Iets voor de volgende software revisie...

Dit wordt misschien nog eens uitgewerkt: details LED state machine, lichtmeting, PWM-gebruik, etc; maar bekijk anders maar de source file.

Alvast enkele fragmenten code: om de processor in idle mode te zetten gebruik ik gedeeltelijk de avr_lib functies (met name <util/atomic.h>), en zet een timer na x seconde om weer wakker te worden. Ook zorg ik dat de PWM outputs 0 zijn (power saving):

void timer_xxx_sec(int secs)  // program timer to wake up from idle
{
  ATOMIC_BLOCK(ATOMIC_RESTORESTATE) // no spurious timer IRQ's while programming
  {
    TIMSK = 0;                // disable all timer interrupts

    GTCCR |= 1;               // timer1: reset prescaler
    TCCR1A = 0;               // outputs normal digital (no PWM), mode 0
    TCCR1B = (SLEEP_PRESCALE << CS10); // prescaler: 0x5: /1024; mode: 0 (counting)
    TCNT1  = 0xFFFF-secs*TICK2SEC;     // set up-counter @ x secs (16-bit write!!)

    TCCR0A = 0;  TCCR0B = 0;  // timer0: no PWM operation, no clock

    TIMSK = (1 << TOIE1);     // IRQ @ timer 1 overflow (0xFFFF->0) enable
  }
}

void go_sleep()               // see doc on <avr/sleep.h>
{
    timer_xxx_sec(1);         // program timer for wake-up after 1 second
    sei();                    // make sure interrupts enabled
    set_sleep_mode(0);        // 0 == IDLE mode
    sleep_mode();             // and return here on interrupt
}

En het fragment dat de helderheid van de LEDs regelt. Het 'echte werk' gebeurt in de TIMER1 interrupt routine, die 250 keer per seconde wordt aangeroepen. Omdat de processor slechts op 128 kHz loopt zijn er in totaal dus maar 512 klokcycles per periode, waarin je 4 LEDs moet bijwerken: de routines moeten dus wel efficiënt zijn! Een aantal trucks van mijn software pagina zijn dan ook toegepast; waarmee het aantal cycli in de interrupt routine naar ongeveer 90 is teruggebracht, ongeveer 40% van de beschikbare hoeveelheid. De rest is voor het hoofdprogramma.

Ik heb per LED twee vlaggen: up en down, die aangeven of de helderheid omhoog gaat danwel naar beneden. Voor de 4 LEDS heb ik deze in een byte gepackt, zie de definitie van IsrStruct. De ATtiny kan heel efficiënt met dit soort gepackte bits omgaan, mits ze in een bit-addresseerbaar register als GPIOR0 staan. Dit wordt gebruikt in de interrupt service routine TIMER1_OVF_vect, waar in de macro's COND_INC en COND_DEC getest wordt of de LED omhoog of omlaag moet. Als dat het geval is wordt met een volgende truck met een temp-register ook de code voor het wijzigen van de PWM met een minimum aan instructies gedaan. Daarnaast wordt in de ISR ook de halve-seconde teller bijgewerkt (deze wordt in het hoofdprogramma gebruikt voor het tijdsgevoel).

// ######################### Interrupt support #############################
 
typedef union isrstruct       // pack some bits indicating the running state
{
  unsigned char state;        // for ease of initialisation/erasure
  struct
  {                           // bitfields start at LSB
    unsigned char up_1:    1; // LSB: pwm_1 up?
    unsigned char up_2:    1; // pwm_2 up?
    unsigned char up_3:    1; // pwm_3 up?
    unsigned char up_4:    1; // pwm_4 up?
    unsigned char dn_1:    1; // pwm_1 down?
    unsigned char dn_2:    1; // pwm_2 down?
    unsigned char dn_3:    1; // pwm_3 down?
    unsigned char dn_4:    1; // MSB: pwm_4 down?
  };
} IsrStruct;
 
// IsrStruct isr_ctrl;        // formal but non-optimised declaration
#define isr_ctrl (*((IsrStruct *)&GPIOR0))  // map on scratch register
 
 
// Interrupt service routine for 500 Hz PWM timer1 interrupt
// which at 128 kHz results in 256 cycles per period.
// ISR takes 8*7 + 30 = ~90 cycles = 40% (approx, worst case)

ISR(TIMER1_OVF_vect)
{
    if(! --timer_tick_hs)     // half second count down
    {
          timer_tick_hs = TICK2QSEC; // restart next interval
          missed_hsec++;      // and indicate one more passed
    }
 
uint8_t temp;                 // to create optimal code, some tricks like temp
 
#define COND_INC(flag, pwm) if(isr_ctrl.flag) { temp=pwm; if(temp != 0xFF) pwm=temp+1; }
#define COND_DEC(flag, pwm) if(isr_ctrl.flag) { temp=pwm; if(temp)         pwm=temp-1; }

    COND_INC(up_1, PWM_1);    // conditionally increment light level
    COND_INC(up_2, PWM_2);    // (using the correct polarity)
    COND_INC(up_3, PWM_3);    // don't increase above max level!
    COND_INC(up_4, PWM_4);
    COND_DEC(dn_1, PWM_1);    // every macro expands into 7 instructions
    COND_DEC(dn_2, PWM_2);    // including the 2 if statements!
    COND_DEC(dn_3, PWM_3);
    COND_DEC(dn_4, PWM_4);
}

Stroomgebruik in de werkelijke schakeling

In mijn vuurvlieg-project, een batterij- en zonnecel-gevoed ontwerp, heb ik de stroom gemeten. Wordt tenslotte gevoed door 2 NiMH batterijen en geladen door een kleine zonnecel, en de datasheets zeggen ook niet altijd alles... Dat viel dus reuze mee (stroomgebruik is van de hele schakeling):

Toestand
      Stroom       Beschrijving
BOD mode
25 µA @ 1.8V
Brown-Out Detected: spanning onder de 1.8 Volt gedaald en chip in reset (om te voorkomen dat de batterijen helemaal leeggezogen worden). Start automatisch weer op als de zon schijnt (en gaat dan naar idle mode)
Idle
50 µA @ 2.5V
Slaapmode, bijvoorbeeld overdag (cellen worden geladen, chip in idle mode); wake-up elke seconde voor een lichtmeting (duurt enkele 10-tallen µS); draaiend op de 128 kHz interne RC oscillator
Active
400 µA @ 2.5V
Actief (maar zonder de LEDs aangesloten); wake-up op 250 Hz en draaiend op de 128 kHz interne RC oscillator
Active
20 mA @ 2.5V
30 mA @ 3.0V
Actief met brandende LEDs (100% aan). Dit is de voedingsstroom uit de batterij, niet de stroom door de LEDs (nog eens meten; wordt met een schakelend circuitje naar de juiste spanning gebracht).
Active
3 mA @ 2.5V
Actief gemiddeld (LEDs nu en dan aan)

Als de LEDs uit staan gaan de batterijen dus wel een tijdje mee (batterij van 1000 mAh bij een stroom van 50 µA --> 20000 uur = 2¼ jaar; tegen die tijd is de NiMH batterij al leeg door zelfontlading).