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

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🔗

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

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

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)
