Electronika
Nixie klok: software
TechTalk
Zonder software is onze nixie klok waardeloos. De software die naar de arduino geüpload wordt leest de bedieningsknoppen en stuurt het display.

De algemene logica, bijvoorbeeld hoe de toetsen uitgelezen wordt komt hier aan bod.

Arduino programmeringsomgeving

Rechts het programmeren van de Arduino met de initialisatie van de poorten en de procédure om één bit te schrijven. Deze procedure moet viermaal doorlopen worden om een eenheid te schrijven en twee of driemmal om een tiental te schrijven.

Er wordt in C geprogrammeerd, met een paar extra funkties om de poorten te kunnen gebruiken. C is een taal die nog zeer dicht aanleunt bij machinetaal (er bestaan geen objecten, methodes, enz). Zo'n taal is ook nodig omdat de geheugen beperkt is: 32.256 bytes voor het gecompileerd programma en 2048 bytes voor variabelen. Je moet byte-variabelen gebruiken als je kleine getallen moet opslaan, het gebruik van funktieaanroepen beperken en zo weinig mogelijk argumenten doorgeven (in C worden ze altijd "byval" doorgegeven).

Het programma wordt in een EEPROM opgeslagen, de variabelen in RAM. Data kan ook in EEPROM opgeslagen worden, en gaat dan niet verloren bij reset of als de stroom onderbroken wordt. De hoeveelheid EEPROM data hangt af van de arduino.

Studenten informatica zouden verplicht moeten worden om hun eindwerk op een Arduino of gelijkaardig microcontroller te doen. Het is een goedkope werkomgeving (de print kost minder dan 20 euro en er zijn talrijke uitbreidingsprinten of "shields" voorzien). Zo zouden studenten echt leren progreammeren.

Op mijn werk is er een afgestudeerde die enkel kan programmeren in de "hogere programeertalen", talen waarbij met objecten gewerkt wordt, objecten die properties en methodes hebben. De compiler heeft meer dan een gigabyte nodig om te draaien en de code om een cirkel te tekenen heeft een runtime module nodig die megabytes groot is.

Een eenvoudige taak splitst hij op in afzonderlijke modules (met soms maar een paar instructies in de modules). Door al die modules gaat de struktuur van het programma volledig verloren, je hebt geen idee in welke volgorde de modules aangeroepen worden, en waarom de module aangemaakt werd. Misschien dat hij geleerd heeft dat iedere deeltaak in een module moest? Maar wat hij zeker niet geleerd heeft, is dat iedere aanroep van een module tijd kost, extra geheugen nodig heeft en problemen kan veroorzaken (globale en lokale variabelen).

Met de Arduino ben je verplicht efficiente code te schrijven en kan je geen beroep doen op frameworks die een extra interface vormen tussen de programmeur en de processor. Als je tablet, smartphone of computer onnoemlijk traag wordt na een update, dat is omdat de programmeurs bagger schrijven. Zij hebben nooit efficient leren programmeren. Toen ik m'n tablet een update liet doen werd er voor een halve gigabyte aan software binnengehaald (instagram, chrome, office). En werken deze toepassingen beter? Neen, ze zijn nog trager geworden!

Een andere toepassing van een arduino, namelijk de sturing van een centrale verwarming wordt hier uitgelegd.


#include <EEPROM.h>
#include <Wire.h>
#include <RTClib.h>
#define ADD1 2            // pin de l'adresse où envoyer les data
#define ADD2 3
#define ADD3 4

#define DATA 5            // pin qui reçoit un bit à la fois
#define ALARMOUT 9        // sortie alarme

#define HOUR 6            // pin du choix du latch (chip enable)
#define MIN 7
#define SEC 8

Het aanduiden van de gebruikte in- en uitgangen wordt met constanten gedaan, waardoor het programma meer leesbaar wordt. Indien een poort defekt zou gaan kunnen we een andere poort gebruiken door enkel de waarde van de constante te wijzigen.


const byte nbBits[6] = {4, 3, 4, 3, 4, 2};   // nombre bcd bits
const byte nsBits[6] = {SEC, SEC, MIN, MIN, HOUR, HOUR}; // selector à utiliser

De tientallen gebruiken niet alle bits van een BCD getal. Voor de uren volstaan twee bits om de waarden 0, 1 en 2 door te geven, en voor de minuten en seconden volstaan 3 bits voor de waarden 0, 1, 2, 3, 4 en 5. Als er 4 bits gebruikt moeten worden, dan betreft het de eenheden (deze test zal later toegepast worden om de latch te adresseren).

De tweede tabel geeft aan welke latch te gebruiken voor de betreffende cijfer.


byte blinkposition = 0;
bool blinkallume = false;   // chiffre allumé
bool alarmset = false;      // mode programmation alarme
byte h, m, s, ah, am, htemp, mtemp;
byte aset;		              // alarme active
bool alarmsonne;	              // signal d'alarme (buzzer...)
bool alarmreset;
bool hourChanged = true;    // heure ne correspond plus à l'affichage
long lastTime = 0;

De globale variabelen zijn beschikbaar in het volledig programma. Variabelen van het type "bool" nemen ook een byte in beslag, er is dus geen winst ten opzichte van een "byte" variabele. De processor heeft een databus van 8 bits.


void setup() {
  Serial.begin(9600);
  pinMode(ADD1, OUTPUT);  pinMode(ADD2, OUTPUT);  pinMode(ADD3, OUTPUT);
  pinMode(DATA, OUTPUT);  pinMode(ALARMOUT, OUTPUT);
  pinMode(HOUR, OUTPUT);  pinMode(MIN, OUTPUT); pinMode(SEC, OUTPUT);
  h = ah = EEPROM.read(0); m = am = EEPROM.read(1); aset = EEPROM.read(2);
  s = 55; rtc.begin();
}

Deze routine wordt éénmaal uitgevoerd bij de inschakeling of reset. De digitale poorten moeten gedefinieerd worden (worden ze als ingang of uitgang gebruikt). De gegevens worden eveneens uit de EEPROM gelezen (alarm tijdstip en alarm ingeschakeld of niet).

De instructie rtc.begin() is nodig als je een externe klok gebruikt.


void sndAlarm() {
  digitalWrite(ADD3, 0); digitalWrite(ADD2, 1); digitalWrite(ADD1, 1);
  digitalWrite(DATA, aset);
  digitalWrite(HOUR, 0);   delay(1);      digitalWrite(HOUR, 1);
}

Aanduiding (door een externe led) of het alarm ingeschakeld is of niet. Als het alarm ingeschakeld is rinkelt de bel gedurende 5 seconden als bevestiging bij reset of inschakeling.


void sndCh(byte posit, byte valeur) {
  static byte i;
  blinkallume = true;
  for (i = 0; i < nbBits[posit]; i++) {
    digitalWrite(ADD3, bitRead(i, 0));
    digitalWrite(ADD2, bitRead(i, 1));
    if (nbBits[posit] == 4) { digitalWrite(ADD1, 0); }
    else                    { digitalWrite(ADD1, 1); }
    digitalWrite(DATA, bitRead(valeur, i));
    digitalWrite(nsBits[posit], 0);   delay(1);
    digitalWrite(nsBits[posit], 1);
  }
}

Routine om een cijfer te doen oplichten op een bepaalde positie. We schijven 2, 3 of 4 bits naargelang de positie. De funktie bitRead(i, j) levert een 1 of een 0 naargelang er een 1 of een 0 staat op de positie j van de waarde i.

De variabele blinkallume wordt true gezet om aan te geven dat de cijfer oplicht. Als die moet knipperen wordt de waarde ervan later getest om het cijfer te doen doven.

De adressen ADD1, ADD2 en ADD3 is de adres die geschreven moet worden: er wordt een BCD getal bit per bit geschreven om de verschillende posities (aangegeven door dit adres). Indien er tientallen geschreven moeten worden is het adres 1xx. De databus is in feite één bit breed. De bitwaarde wordt opgeslagen door de betreffende latch te activeren.


void incrHour() {       // code à éliminer avec RTC
  hourChanged = true;
  s++;
  if (s > 59) {
    s = 0; m++;
    if (m > 59) {
      m = 0, h++;
      if (h > 23) {
        h = 0;
      }
    }
  }
}

Deze routine wordt enkel gebruikt zolang wij niet over een real time clock beschikken. De routine mag weg als we over een RTC beschikken.

Zonder synchronisatie met een externe RTC loopt de arduino 30 seconden te snel per dag.


void loop() {
  static int inpx, inpxa;
  static bool sela, selb, plua, plub, alaa, alab;
  static long mils;
  mils = millis();
  if (mils - lastTime > 1000) {     // code à éliminer avec RTC
    lastTime = mils;
    // incrHour(); (routine de test tant qu'il n'y a pas de RTC
    hourChanged = true;
    s++;
    if (s > 59) {
      DateTime now = rtc.now();
      h = now.hour(); m = now.minute(); s = now.second();
    }
  }

De "loop" routine is de voornaamste routine die constant wordt uitgevoerd. We definiëren eerst enkele statische variabelen (ze bewaren hun waarde en worden niet gewist als de routine verlaten wordt).

Er is hier ook een deel dat verwijderd zal moeten worden als we naar een real time clock overschakelen, en het rode deel moet bijkomen. We hersynchroniseren slechts éénmaal per minuut.


  // --- buttonread -----------------------------
  inpx = analogRead(0);
  if (abs(inpx - inpxa) < 5) {
    alaa = inpx > 750;
    sela = inpx > 450 && inpx < 750;
    plua = inpx > 200 && inpx < 450;
  }
  inpxa = inpx;

Het lezen van de drukknoppen. We gebruiken hiervoor een analoge ingang. Alle drukknoppen hebben een weerstand, zodat er een andere uitgangsspanning ontstaat bij het drukken van een knop. Om leesfouten te vermijden wordt een waarde pas aanvaardt als die voldoende stabiel is (meting over twee cycli).

Nadien meten we welke knop ingedrukt werd.


  // ------------------- A L A R M ----------
  if (alaa & !alab) {
    if (alarmsonne) { alarmreset = true; }
    else if (alarmset) {
      if (EEPROM.read(0) != ah) { EEPROM.write(0, ah); }
      if (EEPROM.read(1) != am) { EEPROM.write(1, am); }
      if (EEPROM.read(2) != aset) { EEPROM.write(2, aset); }
    }
    alarmset = !alarmset; hourChanged = true;
  }

Om een drukknop te detecteren gebruiken we enkel de stijgende flank om te vermijden dat de funktie constant in- en uitgeschakeld wordt bij iedere lusdoorgang. Een stijgende flank kan slechts éénmaal gebeuren als de toets ingedrukt wordt.

Het programmeren van de alarmfunktie is eenvoudig: we toggelen de alarmset variabele en als men de alarmset (instellen van de alarmtijd) verlaat worden de waarden in EEPROM opgeslagen als die gewijzigd werden.

Als de alarm rinkelt, kan die onderdrukt worden door een druk op de knop.


  // -------------------- S E L E C T I O N -----
  if (sela && !selb)  {    // bouton selection activé
    if (blinkposition == 0)   { blinkposition = 5;  } // minutes
    else if (blinkposition == 2)   { blinkposition = 0;  }
    else                           { blinkposition--;    }
  }

Ook het programmeren van de selectieknop is nog eenvoudiger: we selecteren gewoon de volgende knop.


  // -------------------- + ----------
  if (plua && !plub) {      // bouton plus activé
    if (blinkposition == 0) {
      if (alarmset) {
        if (aset == 0) { aset = 1; }
        else           { aset = 0; }
        digitalWrite(ADD3, 0);
        digitalWrite(ADD2, 1);
        digitalWrite(ADD1, 1);
        digitalWrite(DATA, aset);
        digitalWrite(HOUR, 0);   delay(1);
        digitalWrite(HOUR, 1);
	  }	
    }

In alarmset modus, als er geen cijfer geselecteerd is (knipperend), wordt de "+"-knop gebruikt om de alarm te activeren of niet, aangegeven door een led.


    else {
      if (alarmset) { htemp = ah; mtemp = am; }
      else          { htemp = h;  mtemp = m;  }
      switch (blinkposition) {  // blinkposition va de 5 à 2
        case 2:  // unités de minutes
          if (mtemp % 10 == 9)  { mtemp -= 9; }
          else                  { mtemp++;    }
          break;
        case 3:  // dixaines de minutes
          if (mtemp / 10 == 5)  { mtemp -= 50;}
          else                  { mtemp += 10;}
          break;
        case 4:  // unités d'heures
          if (htemp % 10 == 9)  { htemp -= 9; }
          else if (htemp == 23) { htemp = 20; }
          else                  { htemp++;    }
          break;
        case 5:  // dixaines d'heures
          if (htemp / 10 == 2)  { htemp -= 20;}
          else                  { htemp += 10;}
          break;
      }
      if (htemp > 23)     { htemp = 23; }
      if (alarmset) { ah = htemp; am = mtemp; }
      else          {
        h = htemp; m = mtemp;
        DateTime now = rtc.now(); s = now.second();
        rtc.adjust(DateTime(2018, 1, 1, h, m, s));
		   }
      hourChanged = true;
    }
  }
  

Indien een selectie aktief is, wordt de alarmtijd of de huidige tijd aangepast (naargelang de waarde van alarmset). De waarden worden in twee tijdelijke variabelen opgeslagen om gewijzigd te kunnen worden. We wijzigen enkel de positie (cijfer) die aangegeven is door blinkposition.

Bij het wijzigen van de normale tijd past men ook de externe klok aan. De gebruikte library maakt het niet mogelijk één enkele waarde te wijzigen: ik moet dus alle gegevens wijzigen. De datum wordt ingesteld op 1 januari 2018, maar deze waarde wordt niet gebruikt. De seconden worden ingelezen om correct geschreven te kunnen worden, maar deze leesinstructie is niet strict noodzakelijk omdat de arduino zelf de tijd voldoende nauwkeurig bijhoudt.


  selb = sela; plub = plua; alab = alaa;

  // fonction blink
  if (mils - lastTime > 800 && blinkallume && blinkposition > 0) {
    sndCh(blinkposition, 15);
    blinkallume = false;
  }
  

De afhandeling van de drukknoppen is gedaan. De huidige toestanden van de drukknoppen worden in tijdelijke variabelen opgeslagen voor de flankdetectie.

Indien een cijfer moet knipperen testen we de verlopen tijd, testen we of de cijfer aan staat en testen we of die moet knipperen. Als het resultaat waar is moet het cijfer gedooft worden, wat we kunnen doen door een waarde "15" door te sturen. Dit is geen geldige BCD waarde, waardoor de betreffende cijfer dooft.


  // afficher l'heure
  if (hourChanged) {
    if (alarmset) {
      sndCh(5, ah / 10); sndCh(4, ah % 10);
      sndCh(3, am / 10); sndCh(2, am % 10);
      sndCh(1, 15);      sndCh(0, 15);
    }
    else {
      sndCh(5, h / 10); sndCh(4, h % 10);
      sndCh(3, m / 10); sndCh(2, m % 10);
      sndCh(1, s / 10); sndCh(0, s % 10);
    }
    hourChanged = false;  blinkallume = true;
  }

Aanduiding van het uur of alarmtijd naargelang de modus: normaal of alarmset (instelling alarmtijd).


  alarmsonne = aset == 1 && h == ah && m == am;
  if (!alarmsonne) {    alarmreset = false;  }
  if (alarmsonne && !alarmreset)  { 
    htemp++;  digitalWrite(ALARMOUT, bitRead(htemp, 0));
  }
  delay(1);   // allez fieu, on va un peu faire reposer le processeur
}

We moeten enkel nog de alarmfunktie programmeren. De alarm gaat af als de tijd overeenkomt met de alarmtijd (de alarm rinkelt dus één minuut).

alarmreset onderbreekt de alarm indien true en wordt gereset als de alarm niet rinkelt.

De buzzer wordt aangedreven door een overgebleven analoge uitgangen en wordt afwisselend op 1 en 0 gezet bij iedere lusdoorgang. We gebruiken hiervoor de statische variabele htemp die anders enkel gebruikt wordt voor de tijdsinstelling. De waarde wordt bij iedere lusdoorgang met 1 verhoogd en we gebruiken enkel de bit met laagste waarde.

Links to relevant pages - Liens vers d'autres pages au contenu similaire - Links naar gelijkaardige pagina's