Cursor auf LCD 16x2 bewegen

Hallo Leute,

ich möchte einen Intervalltimer programmieren. Dazu habe ich einen Arduino Nano und ein LCD I2C 16x2 Display verwendet. Den Code habe ich mit Chat-GPT erstellt und teilweise selber angepasst.

Mein Ziel ist es mit fünft Buttons durch die Zeilen des Displays zu navigieren und die Rundenzeit, Pausenzeit und Rundenanzahl mit den Buttons HOCH und RUNTER, LINKS und RECHTS einstellen zu können.

Ich bin jetzt so weit gekommen, dass die Zeiten richtig ablaufen und auch der Buzzer ertönt, wenn die Zeiten vorbei sind.

Jetzt möchte ich mit dem Cursor aber nur an die Stellen springen die für die Zeit/Rundeneinstellung relevant sind. Der Cursor bewegt sich aber von selbst durch die Zeilen, sobald ich den Button ein Mal gedrückt habe. Erkennt er nicht, dass der Button nicht mehr gedrückt ist?

Im Bild seht ihr grob meinen Aufbau und was das Display anzeigt.

Ziel ist es:

  • Mit den 5 Buttons an die Zeitangaben zu navigieren und diese einzustellen
  • Mit dem mittleren Button den Timer starten/stoppen/neustarten

Habt ihr vielleicht eine bessere Idee, wie man das realisieren könnte? Eventuell mit einem anderen Button oder anderer Hardware? Ich stelle mir diese Funktion sehr einfach vor, aber es scheint als wäre es doch nicht so leicht. Zumindest auf diesem Weg.

#include <Wire.h>
#include <hd44780.h>
#include <hd44780ioClass/hd44780_I2Cexp.h>

hd44780_I2Cexp lcd(0x27);

const int LCD_COLS = 16;
const int LCD_ROWS = 2;

const int cursorButtonPin = 5;
int cursorPositionX = 5;
int cursorPositionY = 0;
bool cursorButtonState = LOW;
bool lastCursorButtonState = LOW;
unsigned long lastDebounceTime = 0;
unsigned long debounceDelay = 50;

const int rundenZeit = 5;  // Rundenzeit in Sekunden
const int pausenZeit = 5;  // Pausenzeit in Sekunden
const int rundenAnzahl = 2; // Anzahl der Runden

int aktuelleRunde = 1;
unsigned long rundenStartzeit;
unsigned long pausenStartzeit;

bool inRundenzeit = true;
const int buzzerPin = 9;

void ertoneBuzzer() {
  tone(buzzerPin, 2500);  // Ton mit 2500Hz abspielen
  delay(1000);            // 1 Sekunde warten
  noTone(buzzerPin);      // Ton ausschalten
  delay(500);             // 0.5 Sekunden Pause nach dem Ton
}

void setup() {
  lcd.begin(LCD_COLS, LCD_ROWS);
  lcd.setCursor(0, 0);
  lcd.print("Zeit");
  lcd.setCursor(11, 0);
  lcd.print("Runde");
  lcd.setCursor(0, 1);
  lcd.print("Ruhe");
  lcd.setCursor(12, 1);
  lcd.print(aktuelleRunde);
  lcd.setCursor(14, 1);
  lcd.print(rundenAnzahl);
  lcd.setCursor(13, 1);
  lcd.print("/");

  // Pausenzeit von Anfang an anzeigen
  anzeigeZeit(5, 1, pausenZeit);

  rundenStartzeit = millis(); // Startzeit für die erste Runde
  pausenStartzeit = millis(); // Startzeit für die erste Pause

  pinMode(buzzerPin, OUTPUT);
  pinMode(cursorButtonPin, INPUT);
}

void loop() {
  // Tastenstatus lesen und entprellen
  int reading = digitalRead(cursorButtonPin);
  if (reading != lastCursorButtonState) {
    lastDebounceTime = millis();
  }
  if ((millis() - lastDebounceTime) > debounceDelay) {
    if (reading != cursorButtonState) {
      cursorButtonState = reading;
      if (cursorButtonState == HIGH) {
        moveCursor();
        delay(200);  // Kurze Verzögerung, um das Springen zu erleichtern
      }
    }
  }
  lastCursorButtonState = reading;

  unsigned long aktuelleZeit = millis();

  // Rundenzeit
  int verbleibendeRundenzeit = rundenZeit - (aktuelleZeit - rundenStartzeit) / 1000;
  int verbleibendePausenzeit = pausenZeit - (aktuelleZeit - pausenStartzeit) / 1000;

  if (inRundenzeit) {
    // Zeige die Rundenzeit an, auch wenn sie abgelaufen ist
    anzeigeZeit(5, 0, max(0, verbleibendeRundenzeit));

    // Wenn die Rundenzeit abgelaufen ist
    if (verbleibendeRundenzeit == 0) {
      if (aktuelleRunde < rundenAnzahl) {
        // Wechsle zur Pausenzeit für alle Runden außer der letzten
        inRundenzeit = false;
        pausenStartzeit = millis(); // Startzeit für die Pausenzeit zurücksetzen
        anzeigeZeit(5, 0, rundenZeit);
        ertoneBuzzer(); // Buzzer für 1 Sekunde ertönen lassen
      } else {
        // Direkt zum Ende, wenn die letzte Runde abgeschlossen ist
        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("ENDE!");
        tone(buzzerPin, 2500);  // Ton mit 2500Hz abspielen
        delay(300);             // 0.3 Sekunden warten
        noTone(buzzerPin);      // Ton ausschalten
        delay(200);             // 0.2 Sekunden warten
        tone(buzzerPin, 2500);  // Ton mit 2500Hz abspielen
        delay(400);             // 0.4 Sekunden warten
        noTone(buzzerPin);      // Ton ausschalten
        delay(500);             // 0.5 Sekunden warten
        tone(buzzerPin, 2500);  // Ton mit 2500Hz abspielen
        delay(1000);            // 1.0 Sekunden warten
        noTone(buzzerPin);      // Ton ausschalten
        while (true) {
          // Hier könnten zusätzliche Aktionen nach dem Abschluss erfolgen
        }
      }
    }
  } else {
    anzeigeZeit(5, 1, max(0, verbleibendePausenzeit));

    // Wenn die Pausenzeit abgelaufen ist
    if (verbleibendePausenzeit == 0) {
      inRundenzeit = true;
      aktuelleRunde++;
      lcd.setCursor(12, 1);
      lcd.print(aktuelleRunde);
      anzeigeZeit(5, 1, pausenZeit);
      ertoneBuzzer(); // Buzzer für 1 Sekunde ertönen lassen

      rundenStartzeit = millis(); // Startzeit für die nächste Runde zurücksetzen
    }
  }
}

void moveCursor() {
  // Bewege den Cursor nach rechts
  cursorPositionX++;
  if (cursorPositionX >= LCD_COLS || !isAllowedPosition(cursorPositionX, cursorPositionY)) {
    cursorPositionX = 5;
    cursorPositionY = (cursorPositionY + 1) % LCD_ROWS;
  }

  // Setze den Cursor an die neue Position
  lcd.setCursor(cursorPositionX, cursorPositionY);
  lcd.blink();  // Cursor blinken lassen
}

bool isAllowedPosition(int x, int y) {
  // Überprüfe, ob die Position erlaubt ist
  int allowedX[] = {5, 6, 7, 8, 9, 5, 6, 7, 8, 9, 14};
  int allowedY[] = {0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1};

  for (int i = 0; i < sizeof(allowedX) / sizeof(allowedX[0]); i++) {
    if (x == allowedX[i] && y == allowedY[i]) {
      return true;
    }
  }
  return false;
}

void anzeigeZeit(int position, int zeile, int zeit) {
  int minuten = zeit / 60;
  int sekunden = zeit % 60;

  lcd.setCursor(position, zeile);
  if (minuten < 10) lcd.print("0");
  lcd.print(minuten);
  lcd.print(":");
  if (sekunden < 10) lcd.print("0");
  lcd.print(sekunden);
}

Und da kann dir ChatGPT nicht helfen. Wie ärmlich ist das von ChatGPT.
Mein Tipp:
Frage ChatGPT so lange bis dein Sketch das macht, was du möchtest.
Dies Forum ist nicht das Repair Cafè von ChatGPT.

2 Likes

wenn du bereits genug verstehst, wozu dann überhaupt KI involvieren? schreibe Sketch selbst, dann weißt du sicherlich wo sind die Stellen

Wozu sind die Widerstände bei den Tasten da? Mach den Schaltplan mal so, das man die Leitungen auch verfolgen kann. Und frage mal die KI was INPUT_PULLUP macht.

Die Widerstände habe ich aus dieser Anleitung von Arduino.cc
Dient dazu ein eindeutiges Signal zu erzeugen und Verfälschungen zu verhindern. Dann muss wenigstens ein kleiner Widerstand überwunden werden, um "Rauschen" zu unterdrücken. Wenn ich es richtig verstanden habe, dann kann man mit einem als INPUT_PULLUP deklarierten Buttonpin den Widerstand sparen.

Ich habe vorher noch nichts mit Arduino Programmierung gemacht, habe aber Programmiererfahrung. Ich verstehe den Code, kann aber, weil ich die Syntax nicht kenne, nur sehr langsam programmieren und muss alles ewig nachschauen und im Prinzip neu lernen. Das ist mein erstes Projekt.

Tatsächlich nicht. Ich habe das Problem auf verschiedenster Weise erklärt, aber er dreht sich am Ende immer im Kreis und man kommt von einer fehlerhaften Programmierung zur nächsten. Das bisherige Ergebnis ist das beste.

Aus diesem Grund solltest du das Programm selber schreiben.
Nur dann lernst du es richtig.

Und das wird dir die KI auch nicht beibringen können.

Und warum haben nicht alle Taster diese Widerstände ?
Und warum verwendest du nur grüne Kabel und das teilweise auf grünem Hintergrund ?
So kann man nicht richtig den Verlauf der Kabel erkennen.

Die Syntax ist recht einfach, was Dir vermutlich fehlt ist die Logik.
Schmeiss Deine Widerstände raus.
Benutze die PULLUP des Controllers und schalte die Tasten nach GND.
Das dazugehörige Stück Code findest Du unter DATEI - BEISPIELE - 02 DIGITAL - DigitalInputPullup
Damit ist das erste Problem gelöst.

Dann musst Du nur noch die Position auf dem Display der jeweiligen Ziffer/Zahl zuordnen und die jeweilige Zahl verändern.
Dazu folgende Frage:
Willst Du jede Stelle einzeln verändern oder den Cursor auf die letzte Stelle der Zahl und dann normal die gesamte Zahl verändern?

Willst Du die Zahl verändern und dann mit dem mittkeren Button diese übernehmen, oder darf der Wert schon während der Einstellung verändert werden?
Wenn ersteres, brauchst Du einen Zwischenspeicher.
Die Logik währe dann:
Zahl in Zwischenspeicher übernehmen
Zwischenspeicher verändern
Zwischenspeicher in Zahl übernehmen

Du siehst: Alles eine reine Logikfrage, die muss aber vorher geklärt sein.

Und las das Code schreiben auf Deiner Konsole passieren, sonst wird das nie was.

1 Like

Wahre Worte

Da ist zwar nur ein Widerstand und ein Taster aber würde daraus folgern entweder (falsch) eine für mehrere Taster oder einer pro Taster. Dein Schaltbild ist einfach nur unverständlich.
Grüße Uwe

Zugegeben, die Simulation hat hier mehr Verwirrung gestiftet, als geholfen. Ich habe das nur für mich als Stütze erstellt, damit ich meinen Code laufend testen kann, ohne dass mein Buzzer mit 80dB jedes Mal meine Ohren belästigt. Die Widerstände sind nur für die zwei Knöpfe, weil ich erstmal ein bis zwei Knöpfe programmieren wollte, bevor ich weitere reinbringe, um den Überblick zu behalten.
Die Kabel sind deshalb auch nur ungeführt/unsauber aufgeschaltet. An sich läuft immer ein Pin des Buttons auf 5V, der andere auf GND und der andere auf einen der DIs.

Ich hatte mir vorgestellt, jede Zahl einzeln verändern zu können.

Ja so habe ich es mir vorgestellt. Den Zwischenspeicher hatte ich nicht beachtet. Das macht den Aufbau und damit auch mein 3D Druck für's Gehäuse aufwendiger. Danke für den Input, ich werde versuchen darüber nachzudenken.

Sorry, aber das sollen wir uns jetzt dabei denken ?

Ich frage nochmal: Zahl oder Stelle?
Nur um sicher zu gehen: Die Zahl 1000 bestejt aus 4 Stellen

Was soll beim Überlauf passieren? Am Ende aufhören oder Überlauf und von vorn beginnen oder Überlauf und die nächste Stelle entsprechend verändern?
9 -> Es geht nicht höher
9 -> wird 0
9 -> wird 0 und nächste Stelle +1
entsprechend so bei 0 -> 9 ..

Ich dachte, da es ein einfach angeschlossener Button ist, dass es unwichtig sei wie die Kabelführung aussieht, weil es die meisten hier verstehen würden. Ein Kabel an Spannung, GND und Signal. Da mein Problem nicht dort liegt, sah ich es als unnötig an die Kabel sauber zu zeichnen. Nächstes mal werde ich es sauberer ausführen. Wie gesagt, es sollte eigentlich nur als Skizze für mich dienen.

Da es sich um Zeitangaben im Format mm:ss handelt, hätte ich jetzt gesagt, dass es einfacher ist die Stelle zu verändern und die 9 wird beim nächsten Erhöhen wieder zur 0. Jede Stelle nur für sich und von 0-9 ohne Einfluss auf die anderen Stellen. Guter Tip, danke.

Das wird komplizierter als ich dachte.

Nein.
Wenn man die Logik dahinter auflöst, ist das alles nicht so schlimm.
Darum: erst Logik.
Ich schreib mir das alles mit einem Stft und einem Zettel auf, wenn ich das nicht auf einmal so im Kopf habe, das ich weiss, was zu Schluß rauskommt.
Versuch mal.
Einfach nach dem Motto wenn dann.... :slight_smile:

1 Like

Das ist dein Problem.
Nur aus einer korrekten Zeichnung können wir Fehler erkennen.
Und wenn der falsch gezeichnet ist, sollen wir dann annehmen, dein Aufbau ist aber richtig ?
Wie soll das dann funktionieren ? Achja, wir müssen deine Gedanken lesen.

Ok, ich werde meine Glaskugel holen, warte kurz....

Ahh....mein Glaskugel sagt "Nachdenken ist besser als denken."

um so besser, weil : ist schnell gemacht = ist schnell vergessen.
Syntax ist einfach: ";" am Ende der Kommando, meistens es sind vordefinierte Funktionen, sie haben immer "()" auch wenn leer; man darf bestimmte Kommando verschachteln wie:

for (...) Serial.print(...);
if (...) digitalWrite(...);

um zu erfahren was für Kommandos stellt eine Bibliothek zur Verfügung, kann man im ihren Ordner nachschauen , Datei "keywords.txt", was tun die Kommandos in .CPP (oder wie auch immer) Datei/en.
im Bezug auf LCD der Kommando heißt "setCursor" und Parameter sind die Position 0-15 und die Zeile 0-1.

es ist hilfreich klein an zu fangen und alles in einem Simulator testen, ohne Gewehr. dann Stück für Stück um weitere Blöcke erweitern. Vermulich kann auch ChatGPT dir bestimmte Sachen erklären, aber Endlösung wird es nie ausdenken, nie. Nicht in diesem Universum.

wenn du es für dich nicht wichtig findest, was glaubst du wie wichtig ist es dann für uns?

Das Problem ist, das ChatGPT neben vernünftigen Lösungen oft auch ziemlichen Unsinn ausspuckt. Das muss man erkennen und aussortieren können, sonst wird das mit ChatGPT Programmen nichts. Dazu braucht es aber soviel Programmierwissen, dass man ChatGPT gar nicht braucht um das Programm selbst zu schreiben - deshalb ist ChatGPT beim Programmieren letztendlich wertlos.
Und bei dem leider oft vernachlässigten (und oben angesprochenen) vorherigen Klären der Logik hilft ChatGPT gar nichts. Da muss man schon die eigenen Gehirnwindungen bemühen :wink:.