Přeskočit na obsah
BuresIndustries v3
Účet
  1. Úvod
  2. Arduino blog
  3. Počítadlo na displeji, které přežije vypnutí
Arduino blog · 2. 11. 2026

Počítadlo na displeji, které přežije vypnutí

Arduino Uno, displej 16×2 a jedno tlačítko: počítadlo stisků v EEPROM s rozložením opotřebení a reset, který se nedá provést omylem.

Co to je

Arduino Uno, displej 16×2 a jedno tlačítko. Na prvním řádku svítí „Like a Bosch“, na druhém „Stisku: N“. Každý stisk přičte jedničku a číslo přežije vypnutí napájení. Dlouhým podržením tlačítka jde počítadlo vynulovat, ale program se několikrát ujistí, že to opravdu chcete.

Displej: start: počítadlo na nule
Start: počítadlo na nule. Po zapnutí. Úvodní stav, v EEPROM zatím nic platného není. Přesný snímek ze simulace programu.

Zapojení

  • Arduino Uno (ATmega328P, 16 MHz), displej HD44780 16×2 na rozšiřovací desce ze startovací sady, 4bitový režim, knihovna LiquidCrystal.
  • Displej: RS = D12, E = D11, D4–D7 = D5, D4, D3, D2.
  • Tlačítko na D6 s INPUT_PULLUP. V klidu čte pin LOW, stisk je HIGH. Program klidovou úroveň zjistí sám po startu, takže funguje s oběma zapojeními.

Zapojení jsme nehádali, zjistili jsme ho diagnostickým programem. I2C scan nenašel nic, tlačítko se hlásilo na D6 a displej odpověděl až na zapojení výše. Pro příště: plná horní řada čtverečků znamená „napájení je, komunikace není“, tedy špatné piny, ne rozbitý displej.

Ovládání tlačítkem

Běžný stisk se počítá při puštění tlačítka, s odskokem 50 ms.

Displej: pět stisků
Pět stisků. Běžné počítání. Počítá se při puštění tlačítka. Přesný snímek ze simulace programu.

Když tlačítko držíte, displej se postupně přidává varováními:

Displej: držení od 2 s
Držení od 2 s. První varování. Přesný snímek ze simulace programu.
Displej: držení od 5 s
Držení od 5 s. Druhé, důraznější varování. Přesný snímek ze simulace programu.

Od 7. do 10. vteřiny odpočítává 3, 2, 1:

Displej: odpočet: 3
Odpočet: 3. 7. vteřina držení. Přesný snímek ze simulace programu.
Displej: odpočet: 2
Odpočet: 2. 8. vteřina. Přesný snímek ze simulace programu.
Displej: odpočet: 1
Odpočet: 1. 9. vteřina. Přesný snímek ze simulace programu.

Pak displej na vteřinu zhasne a v 11. vteřině vyzve k pustění. Nic se zatím nesmazalo. Pustíte-li tlačítko dřív než v 11. vteřině, vezme se to jako obyčejný stisk a displej se vrátí k počítadlu.

Displej: prázdný displej
Prázdný displej. 10.–11. vteřina. Poslední šance pustit tlačítko dřív, než se zeptá. Přesný snímek ze simulace programu.
Displej: výzva k pustění
Výzva k pustění. 11. vteřina. Nic se zatím nesmazalo. Přesný snímek ze simulace programu.

Po puštění program čeká, dokud něco nestisknete. Jeden stisk (a 600 ms bez druhého) resetuje, dva stisky reset zruší a počet zůstane.

Displej: potvrdit, nebo zrušit
Potvrdit, nebo zrušit. Program čeká, dokud něco nestisknete. Přesný snímek ze simulace programu.
Displej: reset hotov
Reset hotov. Po jednom stisku (a 600 ms bez druhého). Přesný snímek ze simulace programu.
Displej: po resetu
Po resetu. Zpátky na počítadle, tentokrát na nule. Přesný snímek ze simulace programu.

Proč je paměť šetřená

EEPROM v ATmega328P vydrží zhruba 100 000 zápisů na buňku. Počítadlo, které stále přepisuje jednu buňku, by tedy po zhruba 100 000 stiscích začalo chybovat. Proto je EEPROM kruhový buffer 12bajtových záznamů {seq, count, check}:

  • Každý zápis jde do dalšího slotu. Slotů je 85, takže vydrží zhruba 8 milionů stisků místo 100 000.
  • Při startu se vezme platný záznam s nejvyšším seq.
  • check = seq ^ count ^ 0xA5C3A5C3 odfiltruje staré a náhodné bajty. Původně tam po dřívějším programu zbylo číslo 65414 a bez kontroly by se objevilo jako počet stisků.
  • Zapisuje se jen při přičtení stisku a při potvrzeném resetu, ne při každém průchodu smyčkou.

Jak byl odzkoušen

Logiku jsme ověřili v simulátoru avr8js (ATmega328P v Node.js) s vlastním modelem displeje HD44780 a tlačítka. Přes 25 testů pokrývá počítání, přežití restartu, všechny fáze držení, potvrzení i zrušení dvěma stisky. Snímky v článku jsou přesné stavy displeje z těchto testů.

Simulace ale neodhalí skutečné zapojení. Tlačítko jsme našli až na desce, takže se hodí na logiku, ne na kabely.

Chyby po cestě

  1. Přepsaný program. První nahrání ukázkového programu Blink přepsalo původní program v Arduinu, protože jsme předtím neověřili, co v desce běží. Lekce: před nahráním si desku vždycky zazálohujte.
  2. Obrácené tlačítko. Tlačítko bylo zapojené opačně, než se čekalo (v klidu LOW). První verze se proto sama resetovala 5 s po zapnutí. Oprava: automatické zjištění klidové úrovně po startu.
  3. Plná horní řada. Viz výše: napájení je, komunikace není.

Celý kód

arduino/DisplejTlacitko/DisplejTlacitko.ino

#include <LiquidCrystal.h>
#include <EEPROM.h>

// Zapojeni zjistene diagnostikou: RS=12, E=11, D4..D7 = 5,4,3,2
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);

const byte BUTTON_PIN = 6;  // klidova uroven se zjistuje pri startu (u tohoto zapojeni LOW, stisk = HIGH)

// Casy drzeni tlacitka (ms od stisku)
const unsigned long WARN1_MS = 2000;      // "Nedrz to.. / Resetujes to"
const unsigned long WARN2_MS = 5000;      // "Nebud debil / Smazes to"
const unsigned long COUNTDOWN_MS = 7000;  // odpocet 3, 2, 1 (7-10 s)
const unsigned long BLANK_MS = 10000;     // prazdny displej
const unsigned long RESET_PROMPT_MS = 11000;  // "Pust tlacitko" - reset se provede az po potvrzeni
const unsigned long DOUBLE_PRESS_MS = 600;    // okno pro druhy stisk (zruseni)

// Ukladani s rozlozenim opotrebeni: EEPROM je kruhovy buffer zaznamu.
// Kazdy zapis jde do dalsiho zaznamu, nejnovejsi ma nejvyssi poradi (seq).
struct Record {
  unsigned long seq;
  unsigned long count;
  unsigned long check;  // seq ^ count ^ KEY, odfiltruje stare/nahodne data
};

const unsigned long KEY = 0xA5C3A5C3UL;
const int SLOTS = EEPROM.length() / sizeof(Record);

int slot = 0;
unsigned long seq = 0;
unsigned long count = 0;

bool validRecord(const Record &r) {
  return r.check == (r.seq ^ r.count ^ KEY);
}

void loadCount() {
  bool found = false;
  for (int i = 0; i < SLOTS; i++) {
    Record r;
    EEPROM.get(i * sizeof(Record), r);
    if (validRecord(r) && (!found || r.seq > seq)) {
      found = true;
      seq = r.seq;
      count = r.count;
      slot = i;
    }
  }
  if (!found) {
    count = 0;
    seq = 0;
    slot = SLOTS - 1;  // prvni zapis pujde do slotu 0
  }
}

void saveCount() {
  slot = (slot + 1) % SLOTS;
  seq++;
  Record r = {seq, count, seq ^ count ^ KEY};
  EEPROM.put(slot * sizeof(Record), r);
}

enum Mode { NORMAL, WAIT_RELEASE, CONFIRM };
Mode mode = NORMAL;

bool idleLevel = LOW;     // klidova uroven pinu, zjisti se pri startu
bool lastState = LOW;
unsigned long lastChange = 0;
unsigned long pressStart = 0;
unsigned long lastRelease = 0;
int shownStage = -1;
byte presses = 0;
bool ignoreNextRelease = false;

void show() {
  lcd.clear();
  lcd.print(F("Like a Bosch"));
  lcd.setCursor(0, 1);
  lcd.print(F("Stisku: "));
  lcd.print(count);
}

void twoLines(const __FlashStringHelper *a, const __FlashStringHelper *b) {
  lcd.clear();
  lcd.print(a);
  lcd.setCursor(0, 1);
  lcd.print(b);
}

void setup() {
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  delay(100);
  idleLevel = digitalRead(BUTTON_PIN);  // tlacitko se pri startu nedrzi
  lastState = idleLevel;
  loadCount();
  lcd.begin(16, 2);
  show();
}

// Upozorneni behem drzeni tlacitka, meni se jen pri zmene faze
void showHoldStage(unsigned long held) {
  int stage;
  if (held >= BLANK_MS) stage = 5;
  else if (held >= COUNTDOWN_MS) stage = 10 + (3 - (int)((held - COUNTDOWN_MS) / 1000));  // 13, 12, 11
  else if (held >= WARN2_MS) stage = 2;
  else if (held >= WARN1_MS) stage = 1;
  else return;
  if (stage == shownStage) return;
  shownStage = stage;
  if (stage == 1) {
    twoLines(F("Nedrz to.."), F("Resetujes to"));
  } else if (stage == 2) {
    twoLines(F("Nebud debil"), F("Smazes to"));
  } else if (stage >= 10) {
    twoLines(F("Reset za"), F(""));
    lcd.setCursor(7, 1);
    lcd.print(stage - 10);
  } else {
    lcd.clear();
  }
}

void doReset() {
  count = 0;
  saveCount();
  twoLines(F("Reset"), F("hotovo"));
  delay(1000);
  mode = NORMAL;
  show();
}

void loop() {
  bool state = digitalRead(BUTTON_PIN);
  if (state != lastState && millis() - lastChange > 50) {
    lastChange = millis();
    lastState = state;
    bool pressed = (state != idleLevel);
    if (mode == NORMAL) {
      if (pressed) {
        pressStart = millis();
        shownStage = -1;
        ignoreNextRelease = false;
      } else if (ignoreNextRelease) {
        ignoreNextRelease = false;
      } else {
        count++;  // pusteno pred 11 s = bezny stisk
        saveCount();
        show();
      }
    } else if (mode == WAIT_RELEASE) {
      if (!pressed) {
        mode = CONFIRM;
        presses = 0;
        twoLines(F("Potvrdit: 1x"), F("Zrusit: 2x"));
      }
    } else {  // CONFIRM
      if (pressed) {
        presses++;
        if (presses >= 2) {  // dvakrat = zrusit
          mode = NORMAL;
          ignoreNextRelease = true;  // druhy stisk se jeste drzi
          show();
        }
      } else {
        lastRelease = millis();
      }
    }
  }

  if (mode == NORMAL && lastState != idleLevel && !ignoreNextRelease) {
    unsigned long held = millis() - pressStart;
    if (held >= RESET_PROMPT_MS) {
      mode = WAIT_RELEASE;
      twoLines(F("Pust tlacitko"), F("a potvrd reset"));
    } else {
      showHoldStage(held);
    }
  }

  if (mode == CONFIRM && presses == 1 && lastState == idleLevel &&
      millis() - lastRelease > DOUBLE_PRESS_MS) {
    doReset();  // jednou = potvrdit
  }
}