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.
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.
Když tlačítko držíte, displej se postupně přidává varováními:
Od 7. do 10. vteřiny odpočítává 3, 2, 1:
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.
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.
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 ^ 0xA5C3A5C3odfiltruje 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ě
- 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.
- 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.
- 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
}
}