En dan vertoont je sketch plotseling gekke kuren…

Herkenbaar? Het ontwikkelen van Uw Arduino sketch verloopt tot nu toe zonder al te grote problemen. Maar dan, na slechts een kleine wijziging of uitbreiding gedraagt de sketch zich plotseling heel vreemd. LEDjes en schakelaars reageren niet meer zoals bedoeld, de seriele poort geeft geen debug informatie meer door. Of erger nog, de Arduino loopt onverwacht vast. U spit uw broncode nog eens stevig door, probeert nog wat, maar het vreemde gedrag blijft onverklaarbaar.

Het overkwam mij onlangs bij het uitbreiden van de LED kubus sketch. De software had al wekenlang zonder problemen gedraaid maar nadat ik een aantal nieuwe LED patronen had toegevoegd begon deze plotseling gekke kuren te vertonen. Uiteraard op plekken die je niet verwacht en waar je op dat moment ook helemaal geen code aan het wijzigen bent.

Ad-hoc aanpak

“Trial-and-error” aanpak

De eerste stap is natuurlijk om te kijken of je ergens een domme type-fout hebt gemaakt, maar een zorgvuldige code inspectie leverde mij niet direct iets op. Dan maar snel even via de trial-and-error aanpak proberen grip te krijgen op de situatie. Maar trial-and-error is een soort van paniekreactie en paniek is meestal geen goede raad- gever. Niet zelden introduceer je hiermee meer nieuwe bugs dan dat je er bestaande bugs mee oplost. Even achterover leunen en een goede analyse van het probleem uitvoeren werkt beter. En enige praktijkervaring helpt natuurlijk ook enorm…

Uit eerdere projecten heb ik geleerd ik dat dit soort problemen vaak veroorzaakt worden doordat er op een ongecontrolleerde manier in het werkgeheugen (SRAM) geschreven wordt, juist op plekken waar dat op dat moment niet hoort. Programma-variabelen veranderen hierdoor onterecht van waarde en het gedrag van je sketch wordt er compleet onvoorspelbaar door.

Basiskennis

Nog even wat basiskennis opfrissen want hoe zat het ook alweer? De meeste μ-controllers beschikken over twee soorten geheugen: ‘FLASH geheugen’ voor de opslag van programmacode en andersoortige constante informatie en ‘SRAM geheugen’ voor de opslag van allerlei veranderlijke informatie (lees: variabelen).

Een Arduino Uno beschikt over 32kByte aan FLASH geheugen en 2kByte aan SRAM. Niet zo veel als je dat vergelijkt met een PC. Het is dus zaak om er zuinig mee om te springen. Zeker het SRAM geheugen dient zo efficient mogelijk ingezet te worden. Vandaar dat er in de programmeertaal onderscheid gemaakt wordt tussen zgn ‘statische’ en ‘dynamische‘ variabelen.

SRAM gebruik

Statische variabelen zijn bedoeld voor de opslag van informatie waarvan de inhoud (=waarde) voortdurend bewaard moet blijven, dwz zolang het programma aktief is. In de broncode vind je deze variabelen bijvoorbeeld bovenaan de sketch, dus buiten de setup() en de loop(). Statische variabelen worden normaliter opgeslagen in het onderste deel van de SRAM, het zgn ‘heap-geheugen‘.

Tijdens het compileren van de sketch bouwt de compiler een lijst op van alle statische variabelen, incl het type (char, int, long, etc). Met die lijst wordt de benodigde SRAM opslagcapaciteit berekend en bij het starten van het programma wordt die ruimte gereserveerd in het heap-geheugen. Dit heap-geheugen heeft normaliter een vaste grootte.

Dynamische variabelen daarintegen zijn bedoeld voor informatie die slechts tijdelijk beschikbaar hoeft te zijn. Dynamische variabelen worden dus pas in het geheugen opgeslagen zodra en voor zolang ze echt nodig zijn. Voorbeelden zijn functie-parameters en variabelen die binnen de functies gedeclareerd worden. Ze claimen pas geheugen zodra de functie aangeroepen wordt en geven het geheugen weer vrij weer zodra de functie afgerond is. Dynamische variabelen worden opgeslagen in het ‘stack-geheugen‘.

int gemiddelde;                    // gemiddelde = static

void meting(int aantal)            // aantal = dynamic
{
  int i;                           // i = dynamic
  int meetwaarde = 0;              // meetwaarde = dynamic

  for (i=0; i<aantal; i++)
    meetwaarde += analogRead(0);

  gemiddelde = meetwaarde/aantal;
}

void loop()
{
  meting(4);                       // "4" = constant
  serial.println(gemiddelde);
}

Vaak hangt het van externe gebeurtenissen af (schakelaars, sensoren, etc) welke functies aangeroepen zullen worden en in ook welke volgorde. Daarbij komt het ook vaak voor dat functies elkaar aanroepen. Daarom is het tijdens het compileren van je sketch vrijwel onmogelijk om uit te rekenen hoeveel stack-geheugen je sketch maximaal nodig zal hebben. Het stack-geheugen zal gedurende het verloop van je programma dus willen ‘ademen’ (= groeien en weer krimpen). Wees gerust, je hebt er zelf geen omkijken naar, de compiler regelt dit allemaal netjes voor je. Tenminste, zolang er voldoende vrij geheugen is.

Stack overflow

Wanneer het vrije geheugengebied tussen de stack en de heap onvoldoende groot is dan ontstaat het risico dat de ‘stack’ over de ‘heap‘ heen kan groeien. We spreken dan van ‘stack overflow‘ en da’s een gevaarlijke situatie want meerdere variabelen gebruiken op dat moment hetzelfde gebiedje in het SRAM geheugen. Het wijziging van de inhoud van de ene variabele betekent automatisch ook dat de waarde van een andere variabele mee verandert, met onvoorspelbaar gedrag van de sketch tot gevolg.

Stack-overflow‘ kan verschillende oorzaken hebben. Vaak is het een combinatie van teveel aanroepen van functies binnen functies die ieder wellicht ook nog eens veel dynamische variabelen gebruiken. Een typisch voorbeeld is het gebruik van recursief programmeren. Op een PC zelden een probleem, maar op een microcontroller loop je al snel uit het SRAM geheugen. Illustratief hiervoor is het uitrekenen van de faculteit van een getal ( N! ), zoals bijvoorbeeld 6! = 6*5*4*3*2*1 = 720. Een beetje goochelen met cijfers leert dat je 6! ook als volgt zou kunnen uitrekenen: 6! = 6*5!. En 5! op zijn beurt weer als 5! = 5*4!. Wanneer je recursief programmeert riskeer je al vrij snel stack-overflow. Immers, bij elke keer dat de functie zichzelf aanroept worden de zowel de functie-parameter alsook de lokale variable tijdelijk op de stack bewaard. De stack groeit dus met de waarde van N, zie voorbeeld hieronder.

unsigned long faculteit_recursief(int N)
{
  unsigned long uitkomst;

  if (N > 1)
    uitkomst = N * faculteit(N-1);
  else if (N == 1)
    uitkomst = 1; 
  else if (N == 0) 
    uitkomst = 1;  
  else
    uitkomst = 0; // error!!
  return(uitkomst);
}

void loop()
{ 
  unsigned long einduitkomst;

  einduitkomst = faculteit_recursief(10);
  serial.println(einduitkomst);
}

Memory leakage

Free-The-Malloc()s

Een beginnende programmeur zal het niet snel doen maar soms wil of kun je het beheer van het SRAM geheugen niet aan de compiler overlaten. Bijvoorbeeld voor het beheren van een tabel waarvan je tevoren de lengte of de inhoudsvolgorde niet kunt voorspellen, denk aan een telefoonlijst. Aan zo’n tabel wil je flexibel nieuwe contacten kunnen toevoegen of verwijderen. In zo’n geval gebruik je bijvoorkeur een linked-list maar dan moet je daarvoor wel zelf geheugen kunnen reserveren en weer vrijgeven. De Arduino programmeertaal biedt daarvoor net als veel andere programmeertalen de functies ‘malloc()‘ voor het reserveren van stukje SRAM geheugen en ‘free()‘ om stukjes SRAM geheugen weer vrij te geven.

Dynamisch geheugenbeheer vraagt een goede administratie, een strakke discipline en zorgvuldig programmeren. Want wanneer je geheugen per ongeluk niet meer vrij geeft maar wel telkens nieuwe stukjes geheugen reserveert dan ontstaat zgn memory leakage. Het vrije geheugen raakt langzaam maar zeker op, er treedt een stack-overflow op en je programma loopt vast. Op een PC merk je dat waarschijnlijk pas na dagen gebruik (famous blue screens), maar op een Arduino loop je snel uit het beschikbare geheugen. Dus alleen gebruiken als je voldoende programmeerkennis hebt!

Hoeveel SRAM geheugen heb ik eigenlijk nog beschikbaar?

De Arduino beschikt jammer genoeg niet over een standaard functie waarmee je de hoeveelheid vrije SRAM rechtstreeks kunt opvragen. Eigenlijk best jammer, maar niet getreurd. In de Arduino playground omgeving hebben andere programmeurs hiervoor meerdere bruikbare functies beschikbaar gesteld. Ikzelf heb positieve ervaring met de availableMemory() functie van David A. Mellis. Niet de snelste maar wel effectief en kort en ook eenvoudig te doorgronden. Door te experimenteren deze ‘availableMemory()’ functie leerde dat ik dat een Arduino sketch standaard de helft van het beschikbare SRAM (1024 bytes) reserveert voor de heap (en voor interne administratie) en dat de andere helft (ook 1024 bytes) beschikbaar blijft voor de stack.

Het declareren van een handjevol LED-kubuspatronen bleek de hoeveelheid vrije SRAM dan ook niet te beinvloeden, die paar patronen passen kennelijk prima binnen de standaard heap-grootte. Maar wanneer je 1-voor-1 nieuwe LED-kubuspatronen blijft toevoegen dan kom je op een gegeven moment voorbij het punt waarop de standaard heap-grootte niet meer toereikend is. Vanaf dat punt reserveert de compiler extra heap-geheugen en zal de hoeveelheid vrije SRAM evenredig afnemen, tot het op een gegeven moment gewoon op is. En hoewel het toevoegen van LED-kubuspatronen ervoor zorgt dat juist de heap en niet zozeer de stack groeit, neemt het risico op ‘stack-overflow‘ wel toe. Op het punt dat mijn sketch rare kuren begon te vertonen had ik niet meer dan 23 bytes vrij SRAM geheugen meer over. Met mijn analyse zat ik dus op het juiste spoor.

Dat de nieuwe LED-kubuspatronen juist in SRAM terecht kwamen verbaasde mij in eerste instantie. Ik was in de veronderstelling dat ze in FLASH opgeslagen zouden worden en dus geen significante invloed zouden hebben op de beschikbare hoeveelheid SRAM. Achteraf (altijd achteraf) is dat toch wel verklaarbaar; in mijn sketch waren de patronen als een ‘unsigned long’ array gedefinieerd, zie onder, dus een SRAM variabele.

unsigned long LED_pattern[] = {
  0b00000000000000000000000111111111,
  0b00000000000000111101111000010000,
  0b00000111101111000010000000000000,
  0b00000111111111000000000000000000,
  0b00000000010000111101111000000000,
  0b00000000000000000010000111101111,
};

Op zoek naar de oplossing

Mijn eerste ingeving was om er ‘constantes’ van te maken, dus het toevoegen van het keyword ‘const‘ zou het probleem vast oplossen, zie onder.

const unsigned long LED_pattern[] = {
  0b00000000000000000000000111111111,
  0b00000000000000111101111000010000,
  0b00000111101111000010000000000000,
  0b00000111111111000000000000000000,
  0b00000000010000111101111000000000,
  0b00000000000000000010000111101111,
};

Maar helaas, het ‘const’-keyword is een qualifier die welliswaar het gedrag van de variabele verandert, maar het blijft een SRAM variabele. Het toevoegen heeft alleen tot gevolg dat de compiler weet dat de inhoud niet overschreven mag worden en dus een foutmelding geeft als je dat toch probeert.

Het keyword waarmee je kunt forceren dat een constante wel in het FLASH geheugen terecht komt is PROGMEM, een onderdeel van de ‘pgmspace‘-library. Het toevoegen van de library en het keyword is voldoende, de rest van de sketch kan ongewijzigd blijven.

#include <avr/pgmspace.h>

PROGMEM unsigned long LED_pattern[] = {
  0b00000000000000000000000111111111,
  0b00000000000000111101111000010000,
  0b00000111101111000010000000000000,
  0b00000111111111000000000000000000,
  0b00000000010000111101111000000000,
  0b00000000000000000010000111101111,
};

Pfjew… PROBLEM SOLVED! De voorbeeldcode van een eerdere blog over de 3x3x3 LED kubus heb ik inmiddels aangepast. Veel succes en plezier met het zelf aanmaken van nieuwe LED-kubuspatronen.