home
erste Version am 20.08.2020
letzte Änderung am 06.03.2021

Ausschalttimer


Es soll eine Art Schaltuhr entstehen, mit der man einen 230V-Verbraucher nach einstellbarer Zeit stromlos machen kann.
Der konkrete Anwendungsfall dafür ist das EBike-Ladegerät vom Sohnemann. Oft kommt er erst spät Abends nachhause und muss (alle zwei/drei Tage) seinen Fahrrad-Akku laden. Üblicherweise ist niemand mehr wach, wenn der Ladevorgang abgeschlossen ist. Folglich bleibt das Ding bis zum nächsten Morgen unter Strom. Und das tut ja nun nicht not....

Eingeschaltet wird die Schaltuhr mittels eines Tasters, der 230V auf das Netzteil der Schaltuhr und auf den 230V-Ausgang schaltet. Hinter dem Netzteil sitzt eine Schaltung mit einer Atmel-CPU, die als Erstes ein parallel zum Taster geschaltetes Relais einschaltet und somit die Stromversorgung für beide Verbraucher für z.B. 10 Minuten aufrecht erhält.
Innerhalb dieser 10 Minuten hat man Zeit, eine Betriebsdauer für den Verbraucher einzustellen. Nach Ablauf dieser Dauer wird das Relais abgeschaltet und dadurch nicht nur der Verbraucher, sondern auch die Schaltuhr selbst komplett vom Strom getrennt.
Die Einstellung der Zeitdauer wird durch einen Drehgeber erfolgen, die aktuell eingestellte Dauer wird entweder auf einem LC-Display oder einer LED-Reihe angezeigt.
Das Gehäuse wird natürlich aus dem 3D-Drucker kommen und der 230V-Ausgang wird aus Gründen der Stabilität (sehr wahrscheinlich) als Kabel+SchukoBuchse ausgelegt sein (also analog zu dem hier - nur weniger lang und mit Schuko-Buchsen).


Inhaltsverzeichnis

Vorüberlegungen
FirmwareV1.0
FirmwareV1.1 - Planung
FirmwareV1.1
der ATtiny85 ist raus
Schaltplan - Entwicklungsversion
Gehäuse-Design
Test-Drucke und Gehäuse-Druck
erste Verkabelungen und Erweiterungsideen
finaler Schaltplan und Leiterplatte
mit Kondensator klappt alles
habe fertig
ein kleiner Upgrade


Vorüberlegungen

Idealerweise soll die Steuerung von einem ATtiny85 kommen - wenn es von der Pin-Anzahl nicht langt, wird es notgedrungen ein ATmega328.
Der Drehgeber + Bestätigungs-Button braucht drei Pins, das Relais einen. Der ATtiny85 hat maximal sechs programmierbare Pins. Folglich müsste die Anzeige mit zwei Pins auskommen.
Damit scheidet das bei meinem Glotzregulator verwendete Display schon mal (sehr deutlich) aus.
Ich hätte aber auch noch dieses Display rumliegen, von dem ich mir vor vier Jahren zwei Exemplare aus China (für zusammen EUR 8,58) habe kommen lassen. Das Ding hat überhaupt nur vier Pins zur Außenwelt und spricht offenbar I2C-Protokoll.
Somit sollte ein ATtiny85 zumindest Pin-technisch gerade so reichen... Programmspeicher-technisch muss man sehen...
Während der Entwicklung werde ich jedenfalls garantiert einen ArduinoUno verwenden. Einfach deswegen, weil sich der Workflow damit hinreichend komfortabel realisieren lässt.
Das Netzteil sollte wohl eine 12V-Version sein, weil ich meinen sämtlichen 5V-Relais jetzt erstmal keine 230V mit bis zu 2000W zumuten wollen würde. Andererseits würde ein 5V-Netzteil den 5V-Festspannungsregler sparen. Also muss ich wohl mal in die jeweiligen Specs gucken....

Den Drehgeber über einen ATtiny85 abfragen kann ich schon.
Die Ansteuerung eines Relais ist maximal trivial.
Bleibt also nur noch das I2C-Display. Damit habe ich irgendwann zwar schon mal etwas rumgespielt...aber mehr auch nicht.
Das Display kann offenbar Grafik darstellen. Jedoch dürfte das den ATtiny85 bezüglich des zur Verfügung stehenden Speicherplatzes etwas überfordern. Dann will ich mal hoffen, das das Ding ein integriertes Character-Set mitbringt, bei dem sich die einzelnen Character auf eine sinnvolle Größe hochskalieren lassen.

Nach etwas rumsurfen ...... scheint der verwendete Display-Chip auf den Namen SSD1306 zu hören. Hier gibts was und hier sogar speziell für den ATtiny85.



FirmwareV1.0

Der Sonntag ist verregnet und bisher steuert ein ArduinoUNO das OLED (über Adafruit_SSD1306) gemäß des Drehgebers+Taster.
Mit dem Taster wird zwischen dem Einstell- und dem Timer-Modus gewechselt.
Im Einstell-Modus kann man durch Drehen des Drehgebers eine Zeit zwischen null und acht Stunden einstellen.
Jeder Schritt am Drehgeber entspricht vier Minuten.
Der Fortschrittsbalken ist 120 Pixel breit.
Damit geht das zwar sehr schön auf ( 120[Pixel]*4[Minuten/Schritt]  = 480 =  60[Minuten/Stunde]*8[Stunden] ), jedoch ist eine Schrittweite von vier Minuten irgendwie komisch.... fünf Minuten wären besser. Das wären dann 10 Stunden, damit es wieder so schön aufgeht (120*5=600=60*10).
Außerdem möchte ich auch die Sekunden angezeigt bekommen, um nicht immer eine ganze Minute warten zu müssen, bis sich was ändert.

Etwa so sieht die Anzeige derzeit im Einstell-Modus aus, wenn man auf 6:00 Stunden hochgedreht hat:
████████████
  setup
  6:00:00
Und nach Umschaltung auf den Timer-Modus dann so:
████████████
  run
  6:00:00

Weil ich meine erste Machbarkeits-Studie für diese Änderungen nun aber deutlich umbauen muss, will ich vor dem zweiten Anlauf erstmal festlegen, was ich wie lösen kann.


FirmwareV1.1 - Planung

Nach dem Einschalten befindet man sich im Einstell-Modus. Die Drehgeber-ISR ist aktiv und verwaltet ihre aktuelle Stellung [derzeit] in einer Variable, die Werte zwischen 0 und 120 annehmen kann. Alternativ könnte sie auch direkt auf den Sekunden operieren und Werte von 0 bis 10h*60m*60s=36.000 bei einer Schrittgröße von 5m*60s=300 annehmen. Ein Integer würde dafür reichen, wenn er als unsigned deklariert wird.

Der Mikrocontroller liefert seine Uptime mit der Funktion millis() als unsigned long. Bis zum Überlauf braucht es ca. 50 Tage. Langt also dicke.
Mittels
unsigned long start_millis=millis();
unsigned int elapsed_seconds;

elapsed_seconds=(millis()-start_millis)/1000;
bekomme ich die abgelaufenen Sekunden seit dem Setzen von start_millis.

Die Breite des Balkens in der Anzeige berechnet sich als remaining_seconds/300.
Für die Zeitanzeige gälte:
hours=remaining_seconds/3600;
minutes=(
remaining_seconds-hours*3600)/60;
seconds=
remaining_seconds-hours*3600-minutes*60;
Mal testen: remaining_seconds=18065(5:01:05h)  ->  18065/3600=5, (18065-5*3600)/60=1, 18065-5*3600-1*60=5  ->   QED

Allerdings werden die Werte von millis() bzw. elapsed_seconds immer größer, die Restzeit (remaining_seconds = verbleibende Zeit bis zum Abschalten) soll aber sinken und gleichzeitig synchron zum Drehgeber-Basiswert bleiben (falls man nach z.B. einer Stunde nochmal in den Einstell-Modus wechselt, um die Restzeit zu ändern).
Dazu müsste ich die Restzeit einmal pro Sekunden dekrementieren. Und das will ich eigentlich nicht, weil ich damit wahrscheinlich jede Sekunde ein paar Millisekunden Fehler einbauen würde. Die Restzeit soll sich jederzeit aus x-elapsed_seconds berechnen lassen.

Also brauche ich eine Variable remaining_seconds_base, die immer dann auf den Wert von remaining_seconds gesetzt wird, wenn der Timer-Modus aktiviert wird. Zu jeder Sekunde berechnet sich remaining_seconds aus remaining_seconds_base-elapsed_seconds.
Und falls nachträglich wieder in den Einstell-Modus gewechselt wird, muss der aktuelle Wert der Drehgeber-Position aus remaining_seconds berechnet werden.


FirmwareV1.1

Das hier funktioniert jetzt soweit:

#include <Wire.h>
#include <Adafruit_SSD1306.h>

Adafruit_SSD1306 display(4);

#define DR_INT0 2 // Pin2 für Interrupt 0
#define DR_INT1 3 // Pin3 für Interrupt 1
#define DR_BTN 4 // Pin 4 für den Taster am Drehgeber

#define HOURS_MAX 10 // maximal einstellbare Ausschalt-Verzögerung in Stunden
#define MINUTES_MAX HOURS_MAX*60 // maximal einstellbare Ausschalt-Verzögerung in Minuten (8*60=480)
#define SECONDS_MAX MINUTES_MAX*60 // maximal einstellbare Ausschalt-Verzögerung in Sekunden (480*60=28.800)
#define BAR_WIDTH 120 // Breite des Balkens in Pixeln
#define MINUTES_PER_STEP 5 // Schrittweite in Minuten pro Drehgeber-Schritt
#define SECONDS_PER_STEP MINUTES_PER_STEP*60 // Schrittweite in Sekunden pro Drehgeber-Schritt (4*60=240)

#define RUN_MODE 1
#define SET_MODE 0

#define DEBUG false

// 0=00 (Schritt), 1=01 (rückwärts), 2=10 (vorwärts), 3=11 (Grundstellung)
volatile unsigned char dr_t=0;
volatile int dr_counter=2;

/* -------------------------------------------------------------------------------------------
*/
void dr(void) {
static unsigned char p;
switch(p=digitalRead(DR_INT0) | digitalRead(DR_INT1)<<1) {
case 0:
if(dr_t==1) {
dr_t=p;
if(dr_counter>1)
dr_counter--;
} else if(dr_t==2) {
dr_t=p;
if(dr_counter<BAR_WIDTH)
dr_counter++;
}
break;
case 1:
case 2:
if(dr_t==3) dr_t=p;
break;
case 3:
dr_t=p;
}
}

/* -------------------------------------------------------------------------------------------
*/
void dr_start(void) {
attachInterrupt(digitalPinToInterrupt(DR_INT0), dr, CHANGE);
attachInterrupt(digitalPinToInterrupt(DR_INT1), dr, CHANGE);
}

/* -------------------------------------------------------------------------------------------
*/
void dr_stop(void) {
detachInterrupt(digitalPinToInterrupt(DR_INT0));
detachInterrupt(digitalPinToInterrupt(DR_INT1));
}

/* -------------------------------------------------------------------------------------------
*/
void updateDisplay(int mode, unsigned int remaining_seconds, unsigned int remaining_seconds_base) {
int hours=remaining_seconds/3600;
int minutes=(remaining_seconds-hours*3600)/60;
int seconds=remaining_seconds-hours*3600-minutes*60;
static char buffer[20];
static float width_percent;

#if DEBUG
Serial.println(remaining_seconds);
#endif
display.clearDisplay();
if(mode==RUN_MODE) {
display.invertDisplay(false);
snprintf(buffer, 20, "off in\n %1d:%02d:%02d", hours, minutes, seconds);
} else {
display.invertDisplay(true);
snprintf(buffer, 20, "setup\n %1d:%02d:%02d", hours, minutes, seconds);
}
if(mode==RUN_MODE) {
width_percent=BAR_WIDTH/100.0*(100.0/remaining_seconds_base*remaining_seconds);
display.fillRect(4, 10, width_percent, 18, WHITE);
display.fillRect(4, 28, 124, 2, WHITE);
} else {
display.fillRect(4, 10, remaining_seconds/300, 20, WHITE); // (upperLeftX, upperLeftY, width, height, WHITE)
}
display.setCursor(10, 30);
display.println(buffer);
display.display();
}

/* -------------------------------------------------------------------------------------------
*/
void setup() {
#if DEBUG
Serial.begin(115200);
#endif
pinMode(DR_INT0, INPUT_PULLUP);
pinMode(DR_INT1, INPUT_PULLUP);
pinMode(DR_BTN, INPUT_PULLUP);
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, HIGH); // später das Relais

display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.clearDisplay();
display.setTextColor(WHITE);
display.setTextSize(2);

dr_start();
}

/* -------------------------------------------------------------------------------------------
*/
void loop() {
static int cnt_old=-1, but, but_old=1, mode=SET_MODE;
static unsigned int remaining_seconds=dr_counter*SECONDS_PER_STEP;
static unsigned int remaining_seconds_base;
static unsigned long start_millis=millis();
static unsigned int elapsed_seconds=0, elapsed_seconds_old=0;

if(dr_counter!=cnt_old) { // wenn am Drehgeber gedreht wurde
cnt_old=dr_counter;
remaining_seconds=dr_counter*SECONDS_PER_STEP;
updateDisplay(mode, remaining_seconds, remaining_seconds_base);
}

if((but=digitalRead(DR_BTN))!=but_old) { // wenn der Button betätigt oder losgelassen wurde
but_old=but;
if(but==LOW) { // wenn der Button betätigt wurde
mode^=1;
if(mode==RUN_MODE) {
dr_stop();
start_millis=millis();
remaining_seconds_base=remaining_seconds;
} else {
dr_counter=min(BAR_WIDTH, max(1, remaining_seconds/long(SECONDS_PER_STEP)+1));
dr_start();
}
updateDisplay(mode, remaining_seconds, remaining_seconds_base);
}
}

if(mode==RUN_MODE) {
elapsed_seconds=(millis()-start_millis)/1000;
if(elapsed_seconds!=elapsed_seconds_old) {
elapsed_seconds_old=elapsed_seconds;
remaining_seconds=remaining_seconds_base-elapsed_seconds;
updateDisplay(mode, remaining_seconds, remaining_seconds_base);
if(remaining_seconds<1)
digitalWrite(LED_BUILTIN, LOW);
}
}
delay(10);
}


Die IDE meldet dafür:
Der Sketch verwendet 13.848 Bytes (42%) des Programmspeicherplatzes. Das Maximum sind 32.256 Bytes.
Globale Variablen verwenden 1.410 Bytes (68%) des dynamischen Speichers, 638 Bytes für lokale Variablen verbleiben. Das Maximum sind 2.048 Bytes.
Also wirds wohl nix werden mit dem ATtiny85.
Oder ich tausche die SSD1306-Library gegen eine weniger Speicherplatz-verschwendende Alternative aus...

Gerade habe ich erkennen müssen. dass ich zwar diverse Schalter für 230V habe - jedoch keine Taster. Diese Wippschalter habe ich in meinem Fundus gefunden. Laut Beschriftung schaffen die 6A bei 250VAC. Und bei einem 3D-gedruckten Gehäuse ließe sich damit ein sehr schicker Einschalter designen.
Andererseits....ein kleines Steckernetzteil würde ich damit ja ganz schmerzfrei unter Strom nehmen. Es soll ja aber gleichzeitig auch die eigentliche Last ihren Einschaltstrom darüber beziehen. Und wenn ich den Stecker meines Flyer-eBike-Ladegerätes in die Steckdose stecke, funkt es jedesmal gewaltig....

Dann vielleicht doch lieber erstmal eine Bestellung bei Reichelt: ein ordentliches Relais, ein ordentlicher Taster und vielleicht auch noch ein kleines Netzteil (damit ich nicht eins der diversen Billig-Stecker-Netzteile fleddern muss).

Wie unerfreulich: 5V-Relais gibt es mit maximal 8A Schaltstrom. Und dann auch gleich für fast 10€.
Bei 12V-Relais kommt man auf 16A Schaltstrom für knapp 3€.
Dann also dieses Netzteil für 8,33€.
Und dieser Taster zu knapp 3€ sowie dieses Kabel für unter 5€.
5V-Regler samt Beschaltung sollte ich noch reichlich haben.


der ATtiny85 ist raus

Gerade habe ich die Bestellung bei Reichelt auf den Weg gebracht.
Wegen des nicht mehr ausreichenden Speicherplatzes für den ATtiny85 (ohne Quarz) hatte ich heute Morgen die Eingebung, dass ein ATmega328 (mit 16MHz-Quarz) wohl ohnehin die bessere Idee wäre. Schließlich soll das Ding bis zu 10 Stunden laufen. Selbst wenn die Abweichung ohne Quarz nur eine Minute betragen sollte, würde mich das stören.


Schaltplan - Entwicklungsversion

Endlich mal wieder ein verregneter Sonntag... Natürlich ist das Päckchen von Reichelt längst angekommen.
Bisher hatte ich aber weder Lust, ein Gehäuse zu designen - noch mich an die Leiterplatte zu machen.
Aber heute habe ich immerhin schon mal einen Schaltplan gezeichnet:

Schaltplan

Statt des nackigen ATmega328 ist der Einfachheit halber ein ArduinoUNO im Schaltplan gelandet.
Final wird der noch gegen den ATmega328 + 16MHz Quarz + 2x22pF + 1x100nF ersetzt (analog zu dem hier).

Die Fortsetzung wird beizeiten auf der nächsten Seite folgen.