Die Realtime-Clock (DS3231)
Ich möchte eine DS3231 anbinden, was grundsätzlich per I2C Schnittstelle/Protokoll geschieht:
Technische Daten
- RTC-Chip: DS3231 (I2C Adresse 0x68) zählt Sekunden, Minuten, Stunden, Tage, Wochentage, Monate, Jahre (einschließlich Schaltjahr Funktion)
- I2C-EEPROM: AT24CS32 (Adresse 0x57), 4 KB Speicherplatz
- Spannungsversorgung: 3,3 über VCC Pin oder 3,0 Volt per Batterie
- Batterie: Lithium “CR2032” or Akku “LIR2032” (integrierte Ladefunktion vorhanden)
- Stromverbrauch im VCC Betrieb: ca. 0,3 mA ohne LED (die löte ich eh runter)
- Stromverbrauch im Batteriebetrieb: ca. 2.0 µA
- Zwei programmierbare Alarme mit Interrupt Funktion (am SQW Pin)
- Integrierte Pullup-Widerstände für I2C
Verwenden einer CR2023 Lithium Puffer-Batterie
Der DS3231 kann mit einer Puffer-Batterie (“Knopfzelle”) ausgestattet werden, welche die RTC auch ohne externe Stromversorgung weiter laufen lässt. Das macht aus meiner Sicht durchaus Sinn, denn dann saugt das nicht auch noch am System-Akku und es macht das Modul als ganzes stabil und unabhängig von der Systemversorgung.
WICHTIG das Board wird für den Akkubetrieb ausgeliefert, legt man in diesem Zustand eine Lithium-Batterie ein führt das zur Zerstörung der Batterie! Um es mit einer Lithium-Batterie zu betreiben sollten diesen beiden Bauteile (Ladeschaltung für Akku) entfernt werden:
Das Board hat neben dem DS3231 auch noch ein EEPROM draufgelötet. Ein mögliche Anwendung dafür wäre die Speicherung der Timer. Das würde es erlauben dieses Modul extern zu programmieren und in eine Lichterketten-Box einzustecken oder zwischen diesen zu wechseln. So könnte man mit einem einfachen USB-Interface die Schaltzeiten bequem am PC zu planen und zu hinterlegen und dann in die Box zu stecken. Ich sehe also auf meiner Basisplatine eine 6-Poligen Buchsenleiste dafür vor.
Anschluß am ESP-H2
Der ESP32-H2 verfügt intern über zwei interne Hardware I2C-Controller die jeweils als Master oder Slave arbeiten können. Die IO-Ports die der Kontroller nutzt sind quasi frei definierbar, daher verbinde ich meinen ESP32-H2 wie folgt mit dem Modul:
- SCL → GPIO 11
- SDA → GPIO 12
- GND → GND
Damit ich später mit dem RTC-Alarm Pin “SQW” den ESP aufwecken kann verbinde ich zudem:
Stromversorgung des DS3231
Um mit dem RTC (oder dem EEPROM) über I2C zu kommunizieren wird die 3,3 V Betriebsspannung benötigt. Liegt diese nicht an, läuft der RTC auf Batterie mit extrem niedrigem Stromverbrauch von von nur ca. 2 µA, was einer theoretischen Lebensdauer der Batterie von gut 4 Jahren entspricht.
Die LED löte ich ohnehin vom Board runter, die frisst nur unnötig Strom. Es bleibt also nur die Versorgung des RTC und des EEPROMs, wozu selbst im Peak während der Temperaturkompensation nur max. 1,5 mA benötigt werden, ansonsten nur ca. 0,3 mA. Ein GPIO-Port des ESP32 kann typischerweise bis zu 40 mA liefern, ich kann also den RTC problemlos direkt darüber mit Strom versorgen, also verbinde ich wie folgt:
Im Code muss ich den GPIO also im Default LOW halten und wenn ich per I2C kommunizieren will auf HIGH setzen. Danach sollte ich 100 ms warten um den Chips Zeit zum aufwecken zu geben, meine Infos mit ihnen austauschen und den GPIO wieder auf LOW setzen.
Verwendung von I2C und DS3231 im Programm
Um die Hardware nun zu nutzen können wir wieder auf eine fertige Component zurückgreifen
Dazu fügen wir die einfach in der main/idf_component.yml mit ein:
dependencies:
...
esp-idf-lib/ds3231:
version: "^1.1.7"
Beim nächsten kompilieren wird so die Komponente im Projekt hinterlegt:
managed_components/esp-idf-lib__ds3231
Aufgrund der Abhängig zur I2C Library entstehen auch noch diese:
managed_components/esp-idf-lib__i2cdev
managed_components/esp-idf-lib__esp_idf_lib_helpers
Jetzt wie üblich zuerst die Header hinzufügen:
#include "i2cdev.h" // Generic I2C
#include "ds3231.h" // DS3231 RTC (I2C realtime clock)
Den zu verwendenden I2C Controller, Typ und GPIO-Pins definieren:
#define I2C_MASTER_NUM I2C_NUM_0
#define I2C_MASTER_SCL_IO GPIO_NUM_2
#define I2C_MASTER_SDA_IO GPIO_NUM_1
Zur Steuerung der Stromversorgung des RTC verwende ich diese Funktionen:
// GPIO für DS3231 VCC (Power-Control)
#define DS3231_VCC_PIN GPIO_NUM_10
// GPIO für Power-Control initialisieren
static void ds3231_init_power_gpio(void)
{
gpio_config_t io_conf = {
.intr_type = GPIO_INTR_DISABLE,
.mode = GPIO_MODE_OUTPUT,
.pin_bit_mask = (1ULL << DS3231_VCC_PIN),
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.pull_up_en = GPIO_PULLUP_DISABLE
};
gpio_config(&io_conf);
gpio_set_level(DS3231_VCC_PIN, 0); // defaults to POWER OFF
}
// DS3231 VCC einschalten (+ 100 ms Sleep)
static void ds3231_power_on(void)
{
gpio_set_level(DS3231_VCC_PIN, 1);
vTaskDelay(pdMS_TO_TICKS(100)); // Warte auf Stabilisierung
ESP_LOGI(TAG, "DS3231 power ON (run on VCC)");
}
// DS3231 VCC ausschalten (läuft dann auf Batterie)
static void ds3231_power_off(void)
{
gpio_set_level(DS3231_VCC_PIN, 0);
ESP_LOGI(TAG, "DS3231 power OFF (run on VBAT)");
}
What time is it?
Damit will ich erstmal experimentieren bevor ich an die Sleep-Wakeup-Funktionen gehe. Ein erster Test wäre ja einfach mal die aktuelle Uhrzeit aus dem RTC auszulesen. Dazu gibt es die Funktion ds3231_get_time() welche in das als Referenz übergebene Struct vom Typ “tm” die Zeitwerte vom RTC zurückliefert:
struct tm rtctime;
ds3231_get_time(&dev, &rtctime);
ESP_LOGI(TAG, "Zeit: %02d.%02d.%04d %02d:%02d:%02d",
rtctime.tm_mday, // wochentag (1=So, 2=Mo, ..)
rtctime.tm_mon + 1, // tm_mon ist 0-11
rtctime.tm_year + 1900, // tm_year ist Jahre seit 1900
rtctime.tm_hour,
rtctime.tm_min,
rtctime.tm_sec);
Um aber überhaupt mit dem RTC reden zu können muss zunächst der I2C Bus am ESP mit i2cdev_init() initialisiert und mit ds3231_init_desc() ein Handle (Device Descriptor) für den Zugriff auf den Bus erzeugt werden. Die Routine, angereichert mit einer Stromsteuerfunktionalität, schaut dann so aus:
app_main()
{
...
// Power-GPIO initialisieren
ds3231_init_power_gpio();
// I2C Bus des ESP initialisieren
i2cdev_init();
// DS3231 initialisieren und device-handler erzeugen
i2c_dev_t dev;
memset(&dev, 0, sizeof(i2c_dev_t));
ds3231_init_desc(&dev, I2C_MASTER_NUM, I2C_MASTER_SDA_IO, I2C_MASTER_SCL_IO);
struct tm rtctime;
while(1)
{
ds3231_power_on();
ds3231_get_time(&dev, &rtctime);
ds3231_power_off();
ESP_LOGI(TAG, "Zeit: %02d.%02d.%04d %02d:%02d:%02d",
rtctime.tm_mday,
rtctime.tm_mon + 1, // tm_mon ist 0-11
rtctime.tm_year + 1900, // tm_year ist Jahre seit 1900
rtctime.tm_hour,
rtctime.tm_min,
rtctime.tm_sec
);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
Im Ergebnis hat man eine schöne Uhr, die aber natürlich noch falsch geht. Der Grund liegt darin das sie nicht gestellt wurde und bei Erstinbetriebnahme einfach bei 01.01.2000 00:00:00 Uhr startet:
I (410) LED_STRIP_CTRL: DS3231 power ON (run on VCC)
I (410) i2cdev: [Port 0] First initialization. Configuring bus with SDA=12, SCL=11 (Pullups SCL:0 SDA:0)
W (410) i2c.master: Please check pull-up resistances whether be connected properly. Otherwise unexpected behavior would happen. For more detailed information, please read docs
I (420) i2cdev: [Port 0] Successfully installed I2C master bus (Handle: 0x4084e304).
I (430) i2cdev: [0x68 at 0] Device added successfully (Device Handle: 0x4084e7e0, Speed: 400000 Hz).
I (440) LED_STRIP_CTRL: DS3231 power OFF (run on VBAT)
I (440) LED_STRIP_CTRL: Zeit: 01.01.2000 00:00:26
I (1550) LED_STRIP_CTRL: DS3231 power ON (run on VCC)
I (1550) LED_STRIP_CTRL: DS3231 power OFF (run on VBAT)
I (1550) LED_STRIP_CTRL: Zeit: 01.01.2000 00:00:27
I (2650) LED_STRIP_CTRL: DS3231 power ON (run on VCC)
I (2650) LED_STRIP_CTRL: DS3231 power OFF (run on VBAT)
I (2650) LED_STRIP_CTRL: Zeit: 01.01.2000 00:00:28
Korrekte Uhrzeit einstellen
Im Grunde kann uns die Echte Uhrzeit ja egal sein weil sich die gesetzten Timer relativ zueinander ja richtig verhalten würden. Aus Debugging-Gründen und wenn man drüber nachdenkt die Timer später mal über eine Console am PC einstellen zu wollen, wäre es natürlich schick die Uhr einmal richtig zu stellen. Das ginge mit folgendem Code:
struct tm time = {
.tm_year = 2025 - 1900, // Jahre seit 1900
.tm_mon = 11, // Monate 0-11 (Dezember = 11)
.tm_mday = 11, // Tag des Monats
.tm_hour = 14,
.tm_min = 30,
.tm_sec = 0
};
ds3231_set_time(&dev, &time);
ESP_LOGI(TAG, "Zeit gesetzt auf: %02d.%02d.%04d %02d:%02d:%02d",
time.tm_mday, time.tm_mon + 1, time.tm_year + 1900,
time.tm_hour, time.tm_min, time.tm_sec);
Wake me up before you go go…
In diesem Zustand nutzt mir der RTC aktuell noch nichts denn ich baue ja keine Uhr
Also schaue ich mir jetzt mal an wie man beim DS3231 den “Wecker” stellt um damit meinen ESP aufzuwecken sobald der nächste Schaltzeitpunkt erreicht ist.
Man kann zwei unabhängige Alarmzeiten definieren. Dazu gibt es noch verschiedene Arbeitsmodus und Aktionen bei erreichen der Alarmzeit. Ich möchte das dabei der SQW auf LOW geht um damit später den ESP aufwachen zu lassen. Das macht man mit ds3231_set_alarm() der man neben dem I2C-Handle jeweils den Modus und einen tm Record für beide Alarmzeiten übergibt (bzw. NULL wenn einer der Alarme nicht gesetzt werden soll):
struct tm alarm_time = {
.tm_hour = 15,
.tm_min = 30
};
// WICHTIG: Alarm-Flags vorher löschen!
ds3231_clear_alarm_flags(&dev, DS3231_ALARM_1);
// Alarmzeit und Modus einstellen
ds3231_set_alarm(&dev, DS3231_ALARM_1,
&alarm_time, DS3231_ALARM2_MATCH_MINHOUR,
NULL, 0));
// Alarm aktivieren
ds3231_enable_alarm_ints(&dev, DS3231_ALARM_1);
Der Modus DS3231_ALARM1_MATCH_MINHOUR stellt einen Wecker nur mit Stunde und Minute, welcher sich dann automatisch täglich wiederholt. Daher ist es nicht notwendig in der tm struct mehr als diese beiden Werte einzustellen. Sobald der Alarm “scharf” ist (enable) wird er bei erreichen den SQW Pin auf LOW nehmen. Der SQW-Pin hat auf dem DS-Board einen Pull-Up Widerstand. Dieser ist aber vergleichsweise niederohmig weshalb ich ihn aus Stromspargründen entlöten werde. Daher muss der ESP-Eingang auf internen Pullup gestellt werden:
// GPIO14 als Eingang konfigurieren (pull-up)
gpio_config_t io_conf = {
.intr_type = GPIO_INTR_DISABLE,
.mode = GPIO_MODE_INPUT,
.pin_bit_mask = (1ULL << DS3231_ALARM_INT_PIN),
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE
};
gpio_config(&io_conf);
Per Default wird am SQW Pin des DS3231 im Batterybetrieb (also ohne Stromversorgung) ebenfalls abgeschaltet (auf LOW gelegt). Damit das Signal seine Funktion auch da erhält muss das BBSQW Bit (Bit 6 des Control-Registers) gesetzt sein. Dafür gibt es in der ESP-Lib scheinbar keine direkte Funktion, im Gegenteil, die Funktion ds3231_enable_alarm_ints() setzt dieses Bit auf 0 und verhindert somit den Ausgang im Batteriebetrieb!
Daher muss man es unbedingt NACH Aufruf von enable-alarm so einstellen:
// Optional: BBSQW für Batteriebetrieb
uint8_t ctrl;
i2c_dev_read_reg(&dev, 0x0E, &ctrl, 1);
ctrl |= (1 << 6); // BBSQW
i2c_dev_write_reg(&dev, 0x0E, &ctrl, 1);