6.1 🚧 Projekt Unterrichtsuhr

Für eine „gescheite“ Uhr im Klassenzimmer hätten wir wirklich Bedarf. So kam mir die Idee einer speziellen Unterrichtsuhr:

  • Zeitskala von 7:45 bis 17:05
  • Unterrichtszeiten und Pausen farbig markiert
  • Stellt sich entweder via Zeitserver (ESP32 notwendig) oder DCF77 (Empfänger notwendig) von selber richtig ein 🚧
  • Kann mit Solarzelle und Akku funktionieren 🚧
  • Sieht ansprechend aus und hat noch ein paar Gags…

Zifferblatt mit Processing erstellen

Zifferblatt für Unterrichtsuhr
Zifferblatt für Unterrichtsuhr

Zum Erstellen des Zifferblattes und Testen habe ich mit Processing ein Programm geschrieben. Es kann eine PDF “Zifferblatt.pdf” erzeugen, die zum Ausdruck geeignet ist.

Zu neuen Ufern mit ESP32

Als Zeitbasis soll ein Time-Server verwendet werden, daher ist eine WLAN-Anbindung nötig, deswegen der Umstieg auf ESP32.

Obgleich das Modul viele Pins hat können nur wenige universell verwendet werden: oer-informatik.de/esp_pinout🔗

Steckplatine mit ESP32
Steckplatine mit ESP32

SchrittmotorPoti-Code anpassen

#include <Wire.h> // Wire Bibliothek einbinden
#include <LiquidCrystal_PCF8574.h>
#define P_NULLSCHALTER 32 // Kontakt bei Nullpunkt
#define P_USERB 0  // UserButton oder BootButton
#define P_POTI 35 // Poti
#define P_SM0 33 // Schrittmotor 0 Blau Fun IN4
#define P_SM1 25 // Schrittmotor 1 Pink Fun IN3
#define P_SM2 26 // Schrittmotor 2 Gelb Fun IN2
#define P_SM3 27 // Schrittmotor 3 Oran Fun IN1
const int SM_PIN[]={P_SM0,P_SM1,P_SM2,P_SM3};
LiquidCrystal_PCF8574 lcd (0x27); // LCD-Adresse auf 0x27 für 16 Zeichen und 2 Zeilen ein


const unsigned char motorCW[]={0b0001,0b0010,0b0100,0b1000}; // Wave-Drive (Vollschritt weniger Strom)
//unsigned char motorCW[]={0b0011,0b0110,0b1100,0b1001}; // Full-Step (Vollschritt normal)
//unsigned char motorCW[]={0b0001,0b0011,0b0010,0b0110,0b0100,0b1100,0b1000,0b1001}; // Half-Step
int frequenz = 200; // in Herz
int istPosition = 2048; // maximale Schrittzahl
int sollPosition = 0;
int phase = 0; // Schrittmotorphase
bool smAn = false; // stehen die Spulen unter Strom?
bool nullpunkt = false; // Nullpunkt getroffen?
int npOffset; // Abstand zwischen Nullpunkt und Skalenanfang
enum zustandstyp {NULLPUNKT_ENTRY,NULLPUNKT,NP_OFFSET,POTI_LESEN,NULLPUNKT_CHECK,FEHLER}; // definiere Aufzählungstyp
enum zustandstyp zustand = NULLPUNKT_ENTRY; // Definiere und initialisiere Variable

hw_timer_t *timer = NULL;  // ESP32-Timer

void IRAM_ATTR isr_Timer() { // wird mit frequenz aufgerufen
  if(istPosition==sollPosition){
    ausgebenPins(0);
    smAn=false;
    return;
  }
  if(!smAn){ // Spulen aus und Position stimmt nicht
    ausgebenPins(motorCW[phase]); // Spulen einschalten
    smAn=true;
    return;
  }
  if(sollPosition>istPosition){
    istPosition++;
    phase++;
    if(phase>=sizeof(motorCW))phase=0;
  } else{ // sollPosition < istPosition
    if(!digitalRead(P_NULLSCHALTER)){ // wurde Endschalter getroffen?
      ausgebenPins(0); // Spulen abschalten
      smAn=false;
      istPosition=0; // Nullposition getroffen
      nullpunkt=true;
      return;
    }
    istPosition--;
    if(phase==0)phase=sizeof(motorCW)-1;
    else phase--;
  }
  ausgebenPins(motorCW[phase]); // neue Position ausgeben
}
void ausgebenPins(int n){ // Wenn die Pins nicht nebeneinadner liegen
  for(int i=0;i<4;i++){
    digitalWrite(SM_PIN[i],n&1);
    n>>=1; // nächstes Bit
  }
}
void setup() {
  lcd.begin(16, 2);      // LCD initialisieren
  lcd.clear();           // Displaypuffer löschen
  lcd.setBacklight(255); // Hintergrundbeleuchtung einschalten
  lcd.setCursor(0,0);    // Cursor auf erstes Zeichen, erste Zeile setzen
  pinMode(P_SM0,OUTPUT);
  pinMode(P_SM1,OUTPUT);
  pinMode(P_SM2,OUTPUT);
  pinMode(P_SM3,OUTPUT); 
  pinMode(P_NULLSCHALTER,INPUT_PULLUP); // für Nullpunkt
  pinMode(P_USERB,INPUT); // User Button
  Serial.begin(9600); // Serielle Schnittstelle starten und Baudrate festlegen
  timer = timerBegin(1000000); // Set timer frequency to 1 MHz
  timerAttachInterrupt(timer, &isr_Timer);
  timerAlarm(timer, 1000000/frequenz, true, 0); // Call onTimer every 1 second
  istPosition = 2048;
  sollPosition = 0;
}
void loop() {
  int poti;
  switch(zustand){
    case NULLPUNKT_ENTRY: // Nach Einschalten Nullpunkt finden
      istPosition = 2048;    // Maximale Umdrehung
      sollPosition=0; // Motor auf Nullpunkt steuern
      lcd.setCursor(0,0);
      lcd.print("Nullpunkt finden");
      zustand = NULLPUNKT;
      break;
    case NULLPUNKT: // Nullpunkt finden
      if(istPosition==0){
        if(!nullpunkt){  // kein Nullpunkt gefunden
          zustand = FEHLER;
          break;
        }
        zustand = NP_OFFSET;
        lcd.setCursor(0,0);
        lcd.print("Offset einstellen");
      }
      break;
    case NP_OFFSET: // Skalenoffset mit Poti einstellen
      poti = 0;
      for(int i=0;i<10;i++){  // 10 Messungen machen
        poti += analogRead(P_POTI);
        delay(10);
      }
      poti /= 200; // ESP32 hat 12Bit Auflösung
      npOffset = sollPosition = poti;
      lcd.setCursor(0,1);
      lcd.printf("Offset %4d",npOffset);
      if(!digitalRead(P_USERB)){
        zustand = POTI_LESEN;
        lcd.setCursor(0,0);
        lcd.print("Poti lesen       ");
        delay(500);
      }
      break;  
    case POTI_LESEN:
      if(!digitalRead(P_USERB)){ // Nullpunkt-Test auslösen
        zustand = NULLPUNKT_ENTRY;
        break;
      }
      poti = 0;
      for(int i=0;i<10;i++){  // 10 Messungen machen
        poti += analogRead(P_POTI);
        delay(10);
      }
      poti /= 40; // ESP32 hat 12Bit Auflösung
      sollPosition = poti+npOffset;
      lcd.setCursor(0,1);
      lcd.printf("S %4d I %4d",sollPosition,istPosition); // Soll- Istposition
      break;
    case FEHLER:
      lcd.setCursor(0,1);
      lcd.print("Nullpunktfehler!");
      break;  
  }
}

ESP32-Timer sind anders: Timer-API 3.0.x🔗

Prototyp

Unterrichtsuhr Vorne
Unterrichtsuhr Vorne

Um das Zahnradspiel zu meistern wurde eine Feder verbaut.
Endlich stimmt die Anzeige!

Unterrichtsuhr Hinten
Unterrichtsuhr Hinten

Prototypencode

#include <Wire.h> // Wire Bibliothek einbinden
#include <LiquidCrystal_PCF8574.h>
#include <WiFi.h>
#include <time.h>
#define P_NULLSCHALTER 32 // Kontakt bei Nullpunkt
#define P_USERB 0  // UserButton oder BootButton
#define P_POTI 35 // Poti
#define P_SM0 33 // Schrittmotor 0 Blau Fun IN4
#define P_SM1 25 // Schrittmotor 1 Pink Fun IN3
#define P_SM2 26 // Schrittmotor 2 Gelb Fun IN2
#define P_SM3 27 // Schrittmotor 3 Oran Fun IN1
#define P_PIEZO 13 // Piezolautsprecher
#define START_ZEIT (7*3600+45*60)  // Zeit in Tagessekunden
#define END_ZEIT   (17*3600+15*60) 
#define SM_TICK 30 // alle 30s
const int SM_PIN[]={P_SM0,P_SM1,P_SM2,P_SM3};
const char* ssid = "Dionysos";
const char* password = "********";
#define NTP_SERVER "de.pool.ntp.org"
#define TZ_INFO "CET-1CEST,M3.5.0/02,M10.5.0/03" // Mitteleuropäische Zeit
LiquidCrystal_PCF8574 lcd (0x27); // LCD-Adresse auf 0x27 für 16 Zeichen und 2 Zeilen ein

const unsigned char motorCW[]={0b0001,0b0010,0b0100,0b1000}; // Wave-Drive (Vollschritt weniger Strom)
//unsigned char motorCW[]={0b0011,0b0110,0b1100,0b1001}; // Full-Step (Vollschritt normal)
//unsigned char motorCW[]={0b0001,0b0011,0b0010,0b0110,0b0100,0b1100,0b1000,0b1001}; // Half-Step
int frequenz = 200; // in Herz
int istPosition = 2048; // maximale Schrittzahl
int sollPosition = 0;
int phase = 0; // Schrittmotorphase
bool smAn = false; // stehen die Spulen unter Strom?
bool nullpunkt = false; // Nullpunkt getroffen?
int npOffset; // Abstand zwischen Nullpunkt und Skalenanfang
bool oldTaster=false;
int poti;
int tagesSekunde;
int zeitmarke;
bool einstellenOffset=false;
enum zustandstyp {NULLPUNKT_ENTRY,NULLPUNKT,OFFSET,WLAN,UHRZEIT,FEHLER}; // definiere Aufzählungstyp
enum zustandstyp zustand = NULLPUNKT_ENTRY; // Definiere und initialisiere Variable

hw_timer_t *timer = NULL;

void IRAM_ATTR isr_Timer() { // wird mit frequenz aufgerufen
  if(istPosition==sollPosition){
    ausgebenPins(0);
    smAn=false;
    return;
  }
  if(!smAn){ // Spulen aus und Position stimmt nicht
    ausgebenPins(motorCW[phase]); // Spulen einschalten
    smAn=true;
    return;
  }
  if(sollPosition>istPosition){
    istPosition++;
    phase++;
    if(phase>=sizeof(motorCW))phase=0;
  } else{ // sollPosition < istPosition
    if(!digitalRead(P_NULLSCHALTER)){ // wurde Endschalter getroffen?
      ausgebenPins(0); // Spulen abschalten
      smAn=false;
      istPosition=0; // Nullposition getroffen
      nullpunkt=true;
      return;
    }
    istPosition--;
    if(phase==0)phase=sizeof(motorCW)-1;
    else phase--;
  }
  ausgebenPins(motorCW[phase]); // neue Position ausgeben
}
void ausgebenPins(int n){ // Wenn die Pins nicht nebeneinadner liegen
  for(int i=0;i<4;i++){
    digitalWrite(SM_PIN[i],n&1);
    n>>=1; // nächstes Bit
  }
}
bool taster(){          // Taste gedrückt
  bool ausgabe = false;
  bool test = !digitalRead(P_USERB); // Taster ist low aktiv
  if (oldTaster != test){ // hat sich was getan?
    delay(10); // 10 ms warten
    test=!digitalRead(P_USERB); // noch mal einlesen
    if (oldTaster != test){  // immer noch anders?
      ausgabe = !oldTaster & test; // steigende:fallende Flanke
      oldTaster = test;
    }
  }
  if(ausgabe) tone(P_PIEZO,1000,300); // Tastenton
  return ausgabe;
} 
void setup() {
  lcd.begin(16, 2);      // LCD initialisieren
  lcd.clear();           // Displaypuffer löschen
  lcd.setBacklight(255); // Hintergrundbeleuchtung einschalten
  lcd.setCursor(0,0);    // Cursor auf erstes Zeichen, erste Zeile setzen
  pinMode(P_SM0,OUTPUT);
  pinMode(P_SM1,OUTPUT);
  pinMode(P_SM2,OUTPUT);
  pinMode(P_SM3,OUTPUT); 
  pinMode(P_NULLSCHALTER,INPUT_PULLUP); // für Nullpunkt
  pinMode(P_USERB,INPUT); // User Button
  Serial.begin(9600); // Serielle Schnittstelle starten und Baudrate festlegen
  timer = timerBegin(1000000); // Set timer frequency to 1 MHz
  timerAttachInterrupt(timer, &isr_Timer);
  timerAlarm(timer, 1000000/frequenz, true, 0); // Call onTimer every 1 second
  istPosition = 0;
  sollPosition = 0;
  einstellenOffset=false;
}

void loop() {
  struct tm timeinfo;
  switch(zustand){
    case NULLPUNKT_ENTRY: // Nach Einschalten Nullpunkt finden
      istPosition = 2048;    // Maximale Umdrehung
      sollPosition=0; // Motor auf Nullpunkt steuern
      lcd.setCursor(0,0);
      lcd.print("Nullpunkt finden");
      zustand = NULLPUNKT;
      break;
    case NULLPUNKT: // Nullpunkt finden
      if(istPosition==0){
        if(!nullpunkt){  // kein Nullpunkt gefunden
          zustand = FEHLER;
          lcd.setCursor(0,1);
          lcd.print("Nullpunktfehler!");
          break;
        }
        zustand = OFFSET;
        lcd.setCursor(0,0);
        lcd.print("Offset einstellen");
        zeitmarke=millis();
      }
      break;
    case OFFSET:
      if(taster()){ // Offsettimeout aufhalten
        einstellenOffset=!einstellenOffset;
      }
      if(!einstellenOffset && millis()>zeitmarke+6000){
        zustand=WLAN;
        lcd.setCursor(0,0);
        lcd.print("WLAN verbinden");
        WiFi.begin(ssid, password);
        lcd.setCursor(0,1);
        zeitmarke=millis();
      }
      poti = 0;
      for(int i=0;i<10;i++){  // 10 Messungen machen
        poti += analogRead(P_POTI);
        delay(10);
      }
      sollPosition=npOffset = poti/400;
      lcd.setCursor(0,1);
      lcd.printf("Offset %4d",npOffset);
      break;  
    case WLAN:
      if(WiFi.status() == WL_CONNECTED){ // warten auf Anschluß
        zustand = UHRZEIT;
        configTzTime(TZ_INFO, NTP_SERVER);
        lcd.clear();
        break;
      }
      lcd.print(".");
      delay(500);
      if(millis()>zeitmarke+6000){
         ESP.restart();
      }
      break;
    case UHRZEIT:
      if(!getLocalTime(&timeinfo)){
        lcd.setCursor(0,1);
        lcd.print("Zeitfehler!  ");
        zustand=FEHLER;
        break;
      }
      lcd.setCursor(0,0);
      lcd.printf("%02d:%02d:%02d %5d",timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec,WiFi.RSSI());
      lcd.setCursor(0,1);
      lcd.printf("%02d.%02d.%04d  ",timeinfo.tm_mday, timeinfo.tm_mon + 1, timeinfo.tm_year + 1900);
      tagesSekunde = timeinfo.tm_hour*3600+timeinfo.tm_min*60+timeinfo.tm_sec;
      if(tagesSekunde>=START_ZEIT&&tagesSekunde<=END_ZEIT){
        sollPosition=(tagesSekunde-START_ZEIT)/SM_TICK + npOffset;
      }
      delay(1000);
      break;  
    case FEHLER:
      break;  
  }
}

Stärkstes WLAN finden, ins Schulnetz kommen

Schulnetz: ESP32-eduroam
ESP32 WiFiMulti: Connect to the Strongest Wi-Fi Network (from a list of networks)