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:
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:
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.