[108] Budujemy ze zrozumieniem - elektroniczny budzik cz. 2

[108] Budujemy ze zrozumieniem - elektroniczny budzik cz. 2

Zegar (na razie bez budzika jeszcze), przedstawiony w poprzednim artykule, jako idea testowa jest w porządku, ale zegar z tego – choć działa – jest kiepski, o czym świadczy choćby obsługa klawiatury. To jest ten moment, gdy trzeba sobie powiedzieć: czas sięgnąć po przerwania. Zapewne zaraz dałoby się znaleźć jakąś bibliotekę multipleksującą, lecz ja tym razem chcę, co tylko możliwe uczynić na piechotę. Tak naprawdę zbuduję kopię widocznego powyżej zegara sprzed 30 lat, który pracował jeszcze na Z80, a potem – na 8051. Oba były napisane w asemblerze i dzisiejszy szkic będzie pracował nieco w duchu tamtych czasów, przede wszystkim bazując na prostych skokach, warunkach i liczbach ośmiobitowych. Z różnego rodzaju zegarami problem nie polega na złożoności oprogramowania, a raczej na stworzeniu użytecznego interfejsu. Każdy to wie, kto za każdym razem ustawiał godzinę budzenia, klikając w nieskończoność w jakieś niekontaktujące przyciski oprogramowane fatalnie i nieergonomicznie.


#include <TimerOne.h>  // Biblioteka obsługi przerwań.

const byte segmentA = 5;  // Adresy portów kolejnych segmentów: od A do G oraz dwukropek.
const byte segmentB = 6;
const byte segmentC = 9;
const byte segmentD = 10;
const byte segmentE = 11;
const byte segmentF = 12;
const byte segmentG = 13;
const byte dwukropek = 3;

const byte wspolne1 = A0;  // Adresy portów wspólnych wyprowadzeń kolejnych wyświetlaczy.
const byte wspolne2 = A1;
const byte wspolne3 = A2;
const byte wspolne4 = A3;

const byte plus = 7;  // Adresy portów klawiatury.
const byte minus = 4;

const byte opoznienieDlugie = 100;  // Współczynniki autorepetycji klawiatury.
const byte opoznienieKrotkie = 5;

const byte tablica[11] = {
  B1000000,  // 0, Tablica zawierająca wzorce znaków wyświetlacza siedmiosegmentowego.
  B1111001,  // 1, Schemat: G-F-E-D-C-B-A
  B0100100,  // 2
  B0110000,  // 3
  B0011001,  // 4
  B0010010,  // 5
  B0000010,  // 6
  B1111000,  // 7
  B0000000,  // 8
  B0010000,  // 9
};

int licznikPrzerwan = 0;     // Licznik ramek odmierzających 2,5 ms
byte aktywnaCyfra = 1;       // Numer aktywnej cyfry na wyświetlaczu.
byte minutaJednostka = 9;    // Jednostki minut.
byte minutaDziesiatka = 5;   // Dziesiątki minut.
byte godzinaJednostka = 3;   // Jednostki godzin.
byte godzinaDziesiatka = 2;  // Dziesiątki godzin.

void setup() {
  pinMode(segmentA, OUTPUT);  // Deklaruj porty kolejnych segmentów wyświetlaczy.
  pinMode(segmentB, OUTPUT);
  pinMode(segmentC, OUTPUT);
  pinMode(segmentD, OUTPUT);
  pinMode(segmentE, OUTPUT);
  pinMode(segmentF, OUTPUT);
  pinMode(segmentG, OUTPUT);
  pinMode(dwukropek, OUTPUT);

  pinMode(wspolne1, OUTPUT);  // Deklaruj porty wspólnych wyprowadzeń kolejnych wyświetlaczy.
  pinMode(wspolne2, OUTPUT);
  pinMode(wspolne3, OUTPUT);
  pinMode(wspolne4, OUTPUT);

  pinMode(plus, INPUT_PULLUP);  // Deklaruj porty pstryczków jako wejścia podciągnięte wewnętrznie do wysokiego stanu.
  pinMode(minus, INPUT_PULLUP);

  digitalWrite(dwukropek, HIGH);       // Włącz dwukropek.
  Timer1.initialize(2500);             // Ustaw licznik na 2500 mikrosekund.
  Timer1.attachInterrupt(przerwanie);  // Włącz przerwanie z określeniem miejsca lądowania.
}

void loop() {

  if (digitalRead(plus) == HIGH) {       // Jeśli wciśnięto pstryczek plus...
    zwiekszCzas();                       // Zwiększ czas.
    delay(opoznienieDlugie);             // Opóźnienie przed autorepetycją.
    while (digitalRead(plus) == HIGH) {  // Dopóki przycisk plus jest trzymany...
      zwiekszCzas();                     // Zwiększaj czas.
      delay(opoznienieKrotkie);          // Opóźnienie autorepetycji.
    }
    licznikPrzerwan = 0;      // Zeruj licznik przerwań (a więc też licznik sekund).
    delay(opoznienieDlugie);  // Opóźnienie przed wyjściem z procedury.
  }

  if (digitalRead(minus) == HIGH) {  // Obsługa pstryczka minus.
    zmniejszCzas();
    delay(opoznienieDlugie);
    while (digitalRead(minus) == HIGH) {
      zmniejszCzas();
      delay(opoznienieKrotkie);
    }
    licznikPrzerwan = 0;
    delay(opoznienieDlugie);
  }
}

void przerwanie() {                // Tutaj lądujemy za każdym razem, gdy wewnętrzny timer odliczy 2500 mikrosekund.
  licznikPrzerwan++;               // Zwiększ licznik ramek odmierzających 2,5 ms
  if (licznikPrzerwan == 24000) {  // Jeśli osiągnął 24000 (2,5 ms * 400 * 60 sekund)...
    licznikPrzerwan = 0;           // Zeruj go oraz...
    zwiekszCzas();                 // Zwiększ czas.
  }
  digitalWrite(wspolne1, LOW);  // Wygaś wszystkie cyfry.
  digitalWrite(wspolne2, LOW);
  digitalWrite(wspolne3, LOW);
  digitalWrite(wspolne4, LOW);

  switch (aktywnaCyfra) {           // Załaduj kształty aktywnej cyfry z tablicy...
    case 1:                         // Dla dziesiątek godzin.
      if (godzinaDziesiatka > 0) {  // Ale tylko, jeśli większe od zera.
        digitalWrite(segmentA, bitRead(tablica[godzinaDziesiatka], 0));
        digitalWrite(segmentB, bitRead(tablica[godzinaDziesiatka], 1));
        digitalWrite(segmentC, bitRead(tablica[godzinaDziesiatka], 2));
        digitalWrite(segmentD, bitRead(tablica[godzinaDziesiatka], 3));
        digitalWrite(segmentE, bitRead(tablica[godzinaDziesiatka], 4));
        digitalWrite(segmentF, bitRead(tablica[godzinaDziesiatka], 5));
        digitalWrite(segmentG, bitRead(tablica[godzinaDziesiatka], 6));
        digitalWrite(wspolne1, HIGH);  // Włącz aktywną cyfrę.
      }
      aktywnaCyfra = 2;  // Ustaw numer aktywnej cyfry dla kolejnego wejścia w przerwanie.
      break;
    case 2:  // Dla jednostek godzin.
      digitalWrite(segmentA, bitRead(tablica[godzinaJednostka], 0));
      digitalWrite(segmentB, bitRead(tablica[godzinaJednostka], 1));
      digitalWrite(segmentC, bitRead(tablica[godzinaJednostka], 2));
      digitalWrite(segmentD, bitRead(tablica[godzinaJednostka], 3));
      digitalWrite(segmentE, bitRead(tablica[godzinaJednostka], 4));
      digitalWrite(segmentF, bitRead(tablica[godzinaJednostka], 5));
      digitalWrite(segmentG, bitRead(tablica[godzinaJednostka], 6));
      digitalWrite(wspolne2, HIGH);
      aktywnaCyfra = 3;
      break;
    case 3:  // Dla dziesiątek minut.
      digitalWrite(segmentA, bitRead(tablica[minutaDziesiatka], 0));
      digitalWrite(segmentB, bitRead(tablica[minutaDziesiatka], 1));
      digitalWrite(segmentC, bitRead(tablica[minutaDziesiatka], 2));
      digitalWrite(segmentD, bitRead(tablica[minutaDziesiatka], 3));
      digitalWrite(segmentE, bitRead(tablica[minutaDziesiatka], 4));
      digitalWrite(segmentF, bitRead(tablica[minutaDziesiatka], 5));
      digitalWrite(segmentG, bitRead(tablica[minutaDziesiatka], 6));
      digitalWrite(wspolne3, HIGH);
      aktywnaCyfra = 4;
      break;
    case 4:  // Dla jednostek minut.
      digitalWrite(segmentA, bitRead(tablica[minutaJednostka], 0));
      digitalWrite(segmentB, bitRead(tablica[minutaJednostka], 1));
      digitalWrite(segmentC, bitRead(tablica[minutaJednostka], 2));
      digitalWrite(segmentD, bitRead(tablica[minutaJednostka], 3));
      digitalWrite(segmentE, bitRead(tablica[minutaJednostka], 4));
      digitalWrite(segmentF, bitRead(tablica[minutaJednostka], 5));
      digitalWrite(segmentG, bitRead(tablica[minutaJednostka], 6));
      digitalWrite(wspolne4, HIGH);
      aktywnaCyfra = 1;
      break;
  }
}

void zwiekszCzas() {                 // Procedura zwiększająca czas z uwzględnieniem przeładowań jednostek.
  minutaJednostka++;                 // Zwiększ licznik jednostek minut.
  if (minutaJednostka == 10) {       // Jeśli osiągnęły 10...
    minutaJednostka = 0;             // Zeruj je oraz...
    minutaDziesiatka++;              // Zwiększ liczbę dziesiątek minut.
    if (minutaDziesiatka == 6) {     // Jeśli osiągnęły 6...
      minutaDziesiatka = 0;          // Zeruj je oraz...
      godzinaJednostka++;            // Zwiększ licznik jednostek godzin.
      if (godzinaJednostka == 10) {  // Jeśli osiągnęły 10...
        godzinaJednostka = 0;        // Zeruj je oraz...
        godzinaDziesiatka++;         // Zwiększ liczbę dziesiątek godzin.
      }
    }
  }
  if (godzinaDziesiatka == 2 && godzinaJednostka == 4) {  // Jeśli mamy godzinę 24...
    godzinaDziesiatka = 0;                                // Zeruj dziesiątki godzin oraz...
    godzinaJednostka = 0;                                 // Zeruj jednostki godzin.
  }
}

void zmniejszCzas() {                    // Procedura zmniejszająca czas z uwzględnieniem przeładowań jednostek.
  minutaJednostka--;                     // Zmniejsz licznik jednostek minut.
  if (minutaJednostka == 255) {          // Jeśli osiągnęły 255...
    minutaJednostka = 9;                 // Ustaw dziewiątkę oraz...
    minutaDziesiatka--;                  // Zmniejsz liczbę dziesiątek minut.
    if (minutaDziesiatka == 255) {       // Jeśli osiągnęły 255...
      minutaDziesiatka = 5;              // Ustaw piątkę oraz...
      godzinaJednostka--;                // Zmniejsz licznik jednostek godzin.
      if (godzinaJednostka == 255) {     // Jeśli osiągnęły 255...
        godzinaJednostka = 9;            // Ustaw dziewiątkę  oraz...
        godzinaDziesiatka--;             // Zmniejsz liczbę dziesiątek godzin.
        if (godzinaDziesiatka == 255) {  // Jeśli osiągnęły 255...
          godzinaDziesiatka = 2;         // Ustaw dwójkę dla dziesiątki godzin oraz...
          godzinaJednostka = 3;          // Ustaw trójkę dla jednostki godzin.
        }
      }
    }
  }
}

Zacznijmy więc od… właściwie napisania programu od początku. Przede wszystkim wyrzucimy bibliotekę zegarową, zastępując ją bardzo prostą biblioteką dającą dostęp do przerwań. Pisałem o niej jakiś czas temu i tam zapraszam po szczegóły. Przypomnę tylko w skrócie: odtąd co ustalony z góry czas program będzie lądował w wybranym podprogramie, niezależnie od stanu, w którym się znajduje. W deklaracjach inicjujemy ów czas i nazwę podprogramu:

Timer1.initialize(2500);             // Ustaw licznik na 2500 mikrosekund.
Timer1.attachInterrupt(przerwanie);  // Włącz przerwanie z określeniem miejsca lądowania.

Dlaczego zadeklarowałem tak krótki czas, równy dwóm i pół milisekundy? Ano dlatego, że przerwania będą nam teraz zarządzać multipleksem, czyli po kolei będą wyświetlać cyfry na wyświetlaczu, a musi się to odbywać szybko, by owe cyfry nie mrugały. Jak szybko? Przyjmuje się minimalną częstotliwość 50 Hz, ale ja uważam, że to zbyt mało. 100 Hz będzie już zadowalającą wartością, choć można pokusić się o więcej. Jednak przy tej wartości łatwo będzie nam przeprowadzić obliczenia. Konkretnie już: cykl wyświetlenia wszystkich czterech cyfr to 100 Hz, czyli 10 ms. Skoro cyfry są cztery, każda będzie się świecić 2,5 ms, czyli tak często trzeba będzie wpadać w obsługę przerwań i taką właśnie wartość ustaliłem.

Jak powyżej widać, w głównej pętli w ogóle nie zajmujemy się wyświetlaniem zegara, więc na razie ją opuścimy i przeanalizujemy sobie obsługę przerwań zaczynających się od linii void przerwanie() Jak mówiłem, wpadamy tu co 2 i pół milisekundy. Nie mówiłem jednak, że oprócz obsługi wyświetlacza zrealizujemy tu zegar – tak zupełnie na piechotę i do tego jeszcze w kodzie BCD – przy użyciu czterech bitów, jak to się robi w kalkulatorach albo w pierwszym mikroprocesorze intela 4004. Dlaczego tak? Bo koniec końców jest z tym mniej zamieszania przy konwersji takich liczb na obrazy grafik dla wyświetlacza. Ale oczywiście nie jest to warunek konieczny, tak po prostu wybrałem – jak się okaże – na razie, mając w głowie wciąż żywe sympatie dla asemblera.

licznikPrzerwan++;               // Zwiększ licznik ramek odmierzających 2,5 ms
if (licznikPrzerwan == 24000) {  // Jeśli osiągnął 24000 (2,5 ms * 400 * 60 sekund)...
  licznikPrzerwan = 0;           // Zeruj go oraz...
  zwiekszCzas();                 // Zwiększ czas.

By stworzyć zegar, musimy zliczać co najmniej minuty i godziny. Jednostki takie jak jedna czterysetna sekundy są na wskroś niepraktyczne, więc stworzymy pośredni licznik liczący do 24 tysięcy. Gdy się przepełni, przede wszystkim wyzerujemy go, a następnie przejdziemy do procedury zwiększenia czasu o minutę.

void zwiekszCzas() {                 // Procedura zwiększająca czas z uwzględnieniem przeładowań jednostek.
  minutaJednostka++;                 // Zwiększ licznik jednostek minut.
  if (minutaJednostka == 10) {       // Jeśli osiągnęły 10...
    minutaJednostka = 0;             // Zeruj je oraz...
    minutaDziesiatka++;              // Zwiększ liczbę dziesiątek minut.
    if (minutaDziesiatka == 6) {     // Jeśli osiągnęły 6...
      minutaDziesiatka = 0;          // Zeruj je oraz...
      godzinaJednostka++;            // Zwiększ licznik jednostek godzin.
      if (godzinaJednostka == 10) {  // Jeśli osiągnęły 10...
        godzinaJednostka = 0;        // Zeruj je oraz...
        godzinaDziesiatka++;         // Zwiększ liczbę dziesiątek godzin.
      }
    }
  }
  if (godzinaDziesiatka == 2 && godzinaJednostka == 4) {  // Jeśli mamy godzinę 24...
    godzinaDziesiatka = 0;                                // Zeruj dziesiątki godzin oraz...
    godzinaJednostka = 0;                                 // Zeruj jednostki godzin.
  }
}

Składa się ona z szeregu warunków, bardzo prostych: kolejne liczby zwiększamy i przy przekroczeniu wartości legalnej, to jest 10 dla jednostek minut i godzin, 6 dla dziesiątek minut i 2 dla dziesiątek godzin, ale przy założeniu, że równocześnie 4 dla jednostek godzin, wartości te zerują się, a wartość „oczko wyższa” przyrasta o jeden. Z godziną 24 jest problem i trzeba stworzyć dlań warunek osobny. Przelot przez szereg warunków jest tym dłuższy, im więcej elementów osiąga wartości nielegalne i najdłużej trwa o godzinie 10:00, 20:00 no i 23:59, a także o wszelkich pełnych godzinach z dwójką na początku.

Po przejściu procedury w pamięci zegara będą siedzieć uaktualnione wartości. Następnie będziemy wyświetlać owe wartości na wyświetlaczu. Tutaj skorzystamy z nieco zmienionej procedury z poprzedniego szkicu, więc najpierw wygasimy wszystko, żeby nam jakieś duchy nie latały podczas uaktualnień wartości wyświetlanych. Ponieważ cyfry są cztery, a za każdym razem gdy wejdziemy w przerwanie wyświetla się jedna tylko, musimy stworzyć licznik aktywnej cyfry (aktywnaCyfra). Warunek switch case będzie wybierał jedną z czterech cyfr, ładował z tablicy wzory do wyświetlenia i zmieniał licznik dla następnego wejścia w pętlę. Po załadowaniu wzorów włączymy prąd, czyli podamy wysoki stan na anodę danej cyfry. Wprowadziłem jeszcze wyjątek – gdy liczba dziesiątek godzin jest niższa od jednego, nie wyświetlamy zera. Zegar bez zera na początku jest po prostu ładniejszy.

Jak więc widać, moglibyśmy pozostawić główną pętlę pustą, a i tak zegar pracowałby i wyświetlałby się – oczywiście bez możliwości jego regulacji. Tym jednak zajmiemy się już w kolejnym artykule.

Powiązane tematy

Płytka edukacyjna TME-EDU-ARD-2Płytka edukacyjna TME-EDU-ARD-2Sprawdź tutaj

Przeczytaj również

Nasi partnerzy

TMETech Master EventTME EducationPoweredby
Copyright © 2025 arduino.pl