Timer med Arduino

I den här guiden visar vi hur du kan bygga en timer eller äggklocka med ett Arduino-kort, en display-modul baserad på TM1637 och ett par knappar och en aktiv summer. Timern tillåter användaren att ställa in tiden med två knappar – en för minuter och en för sekunder, har en start knapp och en summer som piper när timern räknat ner till noll. Alarmet stängs av och tiden återställs när man trycker in någon av knapparna. Inställd tid går att nollställa genom att trycka in minut- och sekund-knapparna samtidigt. Det går även att avbryta timern efter att den startas på samma sätt genom att trycka ner minut- och sekund-knapparna.

Kopplingen

Kopplingen använder sig av ett Arduino-kort, i vårt exempel ett Arduino UNO-kort, men de flesta andra modeller fungerar lika bra. För att visa tiden använder vi oss en 7-segments-display som drivs av ett TM1637-chip. Knapparna och summern kopplar vi in via en kopplingsplatta.

Beskrivning av koden

Importera bibliotek

För att kommunicera med display-modulen använder vi oss av ett bibliotek som har samma namn som chippet som driver modulen, dvs ”TM1637” skapat av Avishay Orpaz. Det finns många andra bibliotek för det här chippet så kontrollera att du väljer rätt i bibliotekshanteraren.

#include <TM1637Display.h>

Konstanter

För att lätt kunna ändra vilka anslutningar vi kopplar in allt på så använder vi konstanter som vi sätter överst i koden.

const int display_clock = 3;
const int display_data = 2;
const int min_button = 7;
const int sec_button = 6;
const int set_button = 5;
const int beeper_pin = 4;

Aktivera skärmen

För att använda display-biblioteket behöver vi skapa en variabel (objekt). Vi använder våra konstanter från ovan för att ange vilka anslutningar som vi kopplat in displayens klocka- och data-ingångar till.

TM1637Display display(display_clock, display_data);

Tillstånd

Vår kod använder sig av en så kallad state machine för att fungera. Denna state machine behöver ha en variabel som håller reda på i vilket tillstånd (state) som den är i för tillfället. Vi kan använda oss av en enkel siffra, men det gör det svårt att läsa koden. Så istället använder vi oss av en så kallad enum. En enum är i grunden ett heltal (int), men där man namnger de olika värdena istället för att använda siffror. Man kan ange vilken siffra som varje namn ska motsvara, men för oss spelar det ingen roll. Vi behöver bara ha olika värden så vi anger bara namnen och låter datorn se till att de får unika värden.

enum states {
  set,
  start,
  countdown,
  zero,
  alarm,
  reset
};

Globala variabler

Vi behöver ett antal globala variabler för att hålla koll på aktuell tid, inställd tid, tillstånd m.m.

  • state – Variabel av enum-typen vi definierade ovan. Denna variabel håller koll på i vilket tillstånd som vår state machine är i.
  • state_timer – Lagrar tid för senaste händelse. Används för både nerräkning och för att pipa med summern.
  • dots_on – Säger åt funktionen som visar tiden på skärmen som prickarna mellan sekunder och minuter ska vara på eller inte.
  • beep – Används vid alarm för att hålla koll på om summern är på eller av.
  • minutes, seconds – Aktuell tid som ska visas på skärmen
  • set_minutes, set_seconds – Inställd tid som timern ska räkna ner ifrån. Lagras i dessa separata variabler för att timern ska kunna återgå till inställd tid efter att den är färdig med nerräkningen.
enum states state = reset;
unsigned long int state_timer;
bool dots_on = true;
bool beep = false;
int minutes = 0;
int seconds = 0;
int set_minutes = 0;
int set_seconds = 0;

I setup() ställer vi som vanligt in vilka anslutningar som ska vara ingångar och utgångar. Vi aktiverar även seriekommunikationen till datorn för att vi ska kunna skicka ut meddelande om vad som händer för att underlätta felsökning. Det sista vi gör är att rensa skärmen och ställa in ljusstyrkan på den till max.

Setup

void setup() {
  Serial.begin(115200);
  Serial.println("INIT");
  pinMode(set_button, INPUT_PULLUP);
  pinMode(min_button, INPUT_PULLUP);
  pinMode(sec_button, INPUT_PULLUP);
  pinMode(beeper_pin, OUTPUT);
  display.clear();
  display.setBrightness(255);
}

Visa siffror på skärmen

Funktionen displayTime() visar aktuell tid på skärmen. Den använder sig av de globala variablerna seconds och minutes för detta. Den kollar även variabeln dots_on för att avgöra om den ska visa punkterna mellan sekunder och minuter.

void displayTime() {
  display.showNumberDecEx(seconds, 0, true, 2, 2);
  if (dots_on) {
    display.showNumberDecEx(minutes, 0x40, true, 2, 0);
  }
  else {
    display.showNumberDecEx(minutes, 0, true, 2, 0);
  }
}

State machine

Själva huvudprogrammet är en så kallad state machine. Idén bakom en state machine är att man identiferar vilka olika funktioner som man behöver. Man delar sedan upp koden så att varje funktion hanteras av ett kod-block, så kallade tillstånd eller states. Dessa kodblock läggs in i någon form av kod som kan välja ut och köra endast ett kodblock. Vilket kodblock som körs bestäms av en variabel. När en funktion är färdig så ändrar den om denna variabel för att berätta för systemet vilket kodblock som ska köras härnäst. På detta visa förflyttar sig systemet från tillstånd till tillstånd.

I vår kod här använder vi oss av en switch-case-sats (oftast kallad switch-select). switch() tittar på värdet på variabeln man ger den och kör sedan det kodblock vars case-värde är samma som variabeln. I vår kod använder vi variabeln ”state” som ju är en enum som vi själva skapade. Vi kan därför använda namnen vi angav i vår enum istället för siffror. Det gör vår kod mer läsbar och lättare att ändra om vi skulle behöva.

  switch (state) {
    case set:
      // Kod för att ställa in start-tid
      break;

    case start:
      // Starta nerräkningen
      break;

    case countdown:
      // Utför nerräkningen, en sekund i taget
      break;

    case zero:
      // Nerräkningen har nått 00:00
      break;

    case alarm:
      // Aktivera alarmet
      break;

    case reset:
      // Nollställ allt och börja om från början
      break;
  }

Tillstånd: set

När systemet starta hamnar det i set-tillståndet. Här läser vi av knapparna så att användaren kan ställa in tiden och kunna starta nerräkningen. Först kollar vi om start-knappen är nertryckt. Om den är det så ändrar vi om tillstånds-variabeln (state) till start vilket gör att nästa gång loopen körs så kommer state machinen att hoppa till start-tillståndet istället.

Varje gång sekund-knappen trycks in kollar vi först om även minut-knappen är intryckt. Om så är fallet så nollställer vi tiden (set_seconds- och set_minutes-variablarna). Om bara sekund-knappen är nertryckt så ökar vi sekunderna och väntar 200ms för att undvika knappstuds och för att inte räkna upp många gånger. Det är vitkigt att komma ihåg att vår kod körs om och om igen väldigt snabbt – tusentals gåner per sekund – så om en knapp trycks in måste vi antingen pausa lite för att räkna upp sekunderna många gånger när användaren bara vill öka med bara ett steg.

Om inte sekund-knappen är nertryckt kollar vi istället minut-knappen. Om den är nertryckt så ökar vi minutrarna med ett. Vi behöver sedan kolla så vi inte räknar upp sekunder eller minuter för mycket. Om sekunderna är över 59 så sätter vi dem till noll och ökar minuterna med ett istället. Detta gör att tiden kommer att hoppa från t.ex. 02:59 till 03:00. Vi tillåter att minutrarna går upp till 99 eftersom det är det högsta vi kan visa på vår display. Om vi går över det nollställer vi minutrarna så de börjar om på noll igen.

Det sista vi gör är att kopiera den instälda tiden till variablerna som vi använder för att visa tiden på displayen.

    case set:
      if (digitalRead(set_button) == LOW) {
        state = start;
      }
      if (digitalRead(sec_button) == LOW) {
        if (digitalRead(min_button) == LOW) {
          set_seconds = 0;
          set_minutes = 0;
        }
        else {
          set_seconds++;
        }
        delay(200);
      }
      else if (digitalRead(min_button) == LOW) {
        delay(200);
        set_minutes++;
      }
      if (set_seconds > 59) {
        set_minutes++;
        set_seconds = 0;
      }
      if (set_minutes > 99) {
        set_minutes = 0;
      }
      minutes = set_minutes;
      seconds = set_seconds;
      break;

Tillstånd: start

Start ställer in timer-variabeln till aktuell tid och byter sedan direkt tillståndet till countdown.

    case start:
      state_timer = millis();
      state = countdown;
      Serial.println("START");
      break;

Tillstånd: countdown

Countdown-tillståndet sköter själva nerräkningen av timern. Först så kollar den om både sekund- och minut-knappen är nertryckta. Är de det betyder det att användaren vill avbryta nerräkningen. Vi byter därför tillstånd till reset.

För att avgöra om exakt en sekund har förflutit sedan sist så jämför vi aktuellt tid med variabeln state_timer som innehåller tiden för den senaste gången vi räknade ner. Om denna skillnad är mer än 1000 ms (en sekund) vet vi att det är hög tid att minska räknaren. Först ökar vi på vårt timer-värde med exakt 1000 så att det kommer att ta exakt 1 sekund till nästa gång vi kör den här koden. Vi minskar sekunder med ett. Om sekunderna är mindre än 0 så sätter vi om dem till 59 och minskar minutrarna med ett. Om båda minuter och sekunder är lika med noll är nerräkningen färdig och vi byter tillstånd till ”zero”.

Vi kollar även när det gått 500 ms, dvs exakt halva tiden och sätter variabeln ”dots_on” till falskt. Detta säger åt vår displayTime()-funktion att släcka prickarna mellan sekunder och minuter. dots_on sätts sedan till sant igen när 1000 ms förflutit vilket gör att prickarna kommer att blinka i takt med att timern räknar ner.

    case countdown:
      if (digitalRead(min_button) == LOW && digitalRead(sec_button) == LOW) {
        state = reset;
        delay(200);
      }
      if (millis() - state_timer > 500) {
        dots_on = false;
      }
      if (millis() - state_timer > 1000) {
        dots_on = true;
        state_timer += 1000;
        seconds--;
        if (seconds < 0) {
          seconds = 59;
          minutes--;
        }
        char status_str[10];
        sprintf(status_str, "%00i:%02i", minutes, seconds);
        Serial.println(status_str);
        if (minutes == 0 && seconds == 0) {
          state = zero;
        }
      }
      break;

Tillstånd: zero

Tillståndet zero gör inte något mer än hoppar vidare till alarmet, men finns med i koden för att kunna lägga till fler funktioner om man vill.

    case zero:
      Serial.println("ZERO");
      state = alarm;
      break;

Tillstånd: alarm

För alarmet skulle vi kunna bara aktivera summern, men vi gör det lite mer komplicerat genom att använda samma timer-teknik som när vi räknar ner tiden, fast snabbare och slår på och av summern så att vi får ett mer ”tradionellt” timer-ljud. Vi använder variabeln beep för att komma ihåg om summern är på eller av och använder not-operatorn (!) för att växla den mellan sant och falskt.

    case alarm:
      if (digitalRead(set_button) == LOW || digitalRead(min_button) == LOW || digitalRead(sec_button) == LOW) {
        state = reset;
        delay(200);
      }
      if (millis() - state_timer > 250) {
        state_timer = millis() + 250;
        beep = ! beep;
        digitalWrite(beeper_pin, beep);
      }
      break;

Tillstånd: reset

Reset-tillståndet är till för att återställa allt. I vårt fall är det bara summern som behöver stängas av, men om du lägger till fler funktioner till timern kan fler saker behöva återställas. Sedan byter vi stillstånd till set igen för att börja om.

    case reset:
      digitalWrite(beeper_pin, LOW);
      state = set;
      break;

Visa tiden

Det absolut sista vi gör i vår kod är att visa aktuell tid på skärmen. Vi gör det genom att anropa funktionen displayTime() som vi skapade tidigare. Detta gör vi alltid oavsett i vilket tillstånd vi befinner oss i.

  displayTime();

Hela koden

#include <TM1637Display.h>

// Konstanter för vilka anslutningar vi kopplat in de olika komponenterna till
const int display_clock = 3;
const int display_data = 2;
const int min_button = 7;
const int sec_button = 6;
const int set_button = 5;
const int beeper_pin = 4;

TM1637Display display(display_clock, display_data);

enum states {
  set,
  start,
  countdown,
  zero,
  alarm,
  reset
};


enum states state = reset;
unsigned long int state_timer;
bool dots_on = true;
bool beep = false;
int minutes = 0;
int seconds = 0;
int set_minutes = 0;
int set_seconds = 0;


// Ställ in alla in- och utgångar och nollställ displayen.
void setup() {
  Serial.begin(115200);
  Serial.println("INIT");
  pinMode(set_button, INPUT_PULLUP);
  pinMode(min_button, INPUT_PULLUP);
  pinMode(sec_button, INPUT_PULLUP);
  pinMode(beeper_pin, OUTPUT);
  display.clear();
  display.setBrightness(255);
}


// Denna funktion visar aktuell tid på displayen
void displayTime() {
  display.showNumberDecEx(seconds, 0, true, 2, 2);
  if (dots_on) {
    display.showNumberDecEx(minutes, 0x40, true, 2, 0);
  }
  else {
    display.showNumberDecEx(minutes, 0, true, 2, 0);
  }
}


void loop() {
  // State machine
  switch (state) {
    // Set - läser av knappar och låter användaren ställa in önskad tid
    case set:
      if (digitalRead(set_button) == LOW) {
        state = start;
      }
      if (digitalRead(sec_button) == LOW) {
        if (digitalRead(min_button) == LOW) {
          set_seconds = 0;
          set_minutes = 0;
        }
        else {
          set_seconds++;
        }
        delay(200);
      }
      else if (digitalRead(min_button) == LOW) {
        delay(200);
        set_minutes++;
      }
      if (set_seconds > 59) {
        set_minutes++;
        set_seconds = 0;
      }
      if (set_minutes > 99) {
        set_minutes = 0;
      }
      minutes = set_minutes;
      seconds = set_seconds;
      break;

    // Start - Förbereder nerräkning genom att sätta timer-variabeln
    case start:
      state_timer = millis();
      state = countdown;
      Serial.println("START");
      break;

    // Countdown - Räkna ned tiden en gång i sekunden och blinka med
    // kolonet mellan minuter och siffror två gånger per sekund.
    // Kontrollera om räknaren nått noll.
    case countdown:
      if (digitalRead(min_button) == LOW && digitalRead(sec_button) == LOW) {
        state = reset;
        delay(200);
      }
      if (millis() - state_timer > 500) {
        dots_on = false;
      }
      if (millis() - state_timer > 1000) {
        dots_on = true;
        state_timer += 1000;
        seconds--;
        if (seconds < 0) {
          seconds = 59;
          minutes--;
        }
        char status_str[10];
        sprintf(status_str, "%00i:%02i", minutes, seconds);
        Serial.println(status_str);
        if (minutes == 0 && seconds == 0) {
          state = zero;
        }
      }
      break;

    // Zero - Räknaren har nått noll, aktivera alarmet.
    case zero:
      Serial.println("ZERO");
      state = alarm;
      break;

    // Alarm - Aktivera summern i korta pulser
    case alarm:
      if (digitalRead(set_button) == LOW || digitalRead(min_button) == LOW || digitalRead(sec_button) == LOW) {
        state = reset;
        delay(200);
      }
      if (millis() - state_timer > 250) {
        state_timer = millis() + 250;
        beep = ! beep;
        digitalWrite(beeper_pin, beep);
      }
      break;

    // Reset - Stäng av alarmet och återställ räknaren
    case reset:
      digitalWrite(beeper_pin, LOW);
      state = set;
      break;
  }

  // Via aktuell tid på displayen
  displayTime();
}

Lämna ett svar

Din e-postadress kommer inte publiceras. Obligatoriska fält är märkta *