Interne Echtzeituhr des STM32 nutzen

Ein Vorteil neben mehr Speicher und Geschwindigkeit des STM32F103 gegenüber dem Arduino Uno ist die bereits integrierte Echtzeituhr (Real Time Clock, RTC).

Zusatzplatinen wie das hier vorgestellte RTC DS1307-Modul kann man sich damit sparen. Es ist bereits alles an Bord, was man braucht: Funktionalität im STM32F103 und 32'768 Hz-Oszillator auf dem Board. Nur eine Batterie muss man noch anschließen.

Das hört sich in der Theorie alles ganz einfach an, doch in der Praxis tat sich mir der eine oder andere Stolperstein auf.

Natürlich versuchte ich wieder, mir Arbeit zu sparen und eine Library zu benutzen. Die STM32RTC-Library (https://github.com/stm32duino/STM32RTC) las sich gut. Der Copyright-Hinweis im Source lautete auf (c) 2017 STMicroelectronics und als Author wurder WI6LABS genannt. Natürlich nahm ich an, das WI&LABS ein Team bei STM ist und die Jungs werden ja wohl Ahnung haben. Also sollte meine erste Wahl darauf fallen.

Doch wenn ich diese Lib benutzen wollte, musste ich auch das Board STM32 Cores by STMicroelectronics (https://github.com/stm32duino/BoardManagerFiles/raw/master/STM32/package_stm_index.json in den Voreinstellungen) installieren. Das las sich auch ganz gut. Sie enthält Kern-Libraries wie etwa den HAL (hardware abstraction layer) für die Umsetzung von Standard-API-Aufrufen zu performant Aufrufen, die der STM32 versteht. Außerdem sind die leichtgewichtige Low-Layer (LL) APIs dabei, die performante und effiziente API Aufrufe zur Verfügung stellen. Und das vom Board-Hersteller selbst. Das musste doch taugen.

Ein bisschen in den Sourcen gestöbert machte das alles auch einen guten, vernünftigeren Eindruck.

Jetzt musste ich allerdings das Board umstellen auf Generic STM32F103C, sonst bekam ich Kompilierungsfehler. Die Sourcen für das RTC-Beispiel machte vom den Core-Libraries Gebrauch, die nur eingebunden wurden, wenn dieses Board gewählt wurde.

Das Problem: Damit war kein Upload mehr mit dem STMduino-Bootloader möglich, hier fehlte einfach die Auswahlmöglichkeit.

Aber jetzt ging es erst einmal darum, die Echtzeituhr des STM32 nutzen zu können. Also lud ich das Example mit meinem ST-Link hoch. Diese Option war ja noch da.

Das Beispielprogramm war ganz gut dokumentiert und die Aufrufe zum Zugriff auf die RTC intuitiv zu verstehen.

Doch obwohl es in der readme.md auf der GitHub-Seite heißt:
...Since STM32 Core version > 1.5.0 Reset time management By default, if a time is set it will not be reset afer a reboot. Using begin(true) or begin(true, HOUR_24) will reset the RTC registers.
- und ich hatte die neueste Core Version - wurde bei jedem Reset die RTC zurückgesetzt. Ob ich dabei 3 Volt am VBat-Pin anliegen hatte oder nicht, spielte keine Rolle - immer war die Zeit weg.

Das ist natürlich nicht Sinn einer RTC. Also machte ich mich auf die Suche nach einer funktionierenden RTC-Library für den STM32. Die Core-Boards habe ich dann auch wieder deinstalliert, weil mir der direkte Upload via USB auf den STM32 doch wichtig ist.

Endlich eine STM32-RTC-Lib, die funkioniert

Nach mehreren Tagen fand ich endlich etwas, das funktionierte, die RTC-Library RTClock von Roger Clark, die auch mit meiner normalen STM32-Board-Definition lief.

Und siehe da: hier wird die Zeit nicht zurückgesetzt bei einem Reset und wenn an VBat 3V anliegen, behält der STM32 auch die Zeit, wenn der USB-Stecker abgezogen wird und der STM32 stromlos gemacht wird. Die Uhr läuft, wie sich das gehört, weiter.

Den Source und das Beispiel fand ich nicht ganz so gut dokumentiert und intuitiv wie den von STM, aber dafür funktionierte das Zeug, wie es soll. Und darauf kommt es an.

Eigene Erweiterungen

Das in der Lib zumeist mit time_t, also vergangene Sekunden seit dem 1. Januar 1970, gearbeitet wird, gefiel mir nicht so gut, so dass ich den Code angepasst habe, damit er mit DateTime-Strukturen umgehen kann. Mit den Funktionen void setRtcDateTime (RTClock rtclock, DateTime dat) void setRtcDateTime (RTClock rtclock, time_t tt) DateTime getRtcDateTime (RTClock rtclock) time_t getRtcTimeT (RTClock rtclock) kann ich jetzt die Echtzeituhr setzen und auslesen und Zusatzfunktionen wie DateTime getCompileDateTime () void storeDateTimeInString (DateTime dat, char *buf, boolean withWeekday) helfen beim Setzen der Kompilierungszeit, falls die Uhr noch nicht intialisiert ist und beim Ausgeben von Datum und Zeit z. B. über Serial.print() Mit uint32_t getDateAsUint32 (DateTime dat) uint32_t getTimeAsUint32 (DateTime dat) uint64_t getDateTimeAsUint64 (DateTime dat) kann man sich Datumsangaben und Zeiten als Werte zurückgeben lassen, um sie vergleichen zu können. Und natürlich kann man immer noch mit den time_t-Angaben rechnen.

Einen Alarm zu setzen, der dann eine Interrupt-Funktion zum entsprechenden Zeitpunkt ausführt ist auch möglich: // Alarm setzen in 30 Sekunden rtclock.createAlarm(irq_alarm, (rtclock.getTime() + 30)); // Alarm setzen zu einer bestimmten Zeit DateTime datAlarm; datAlarm=getRtcDateTime (rtclock); datAlarm.hour=17; datAlarm.minute=0; datAlarm.second=0; rtclock.createAlarm(irq_alarmzeit, (timetFromDateTime(rtclock, datAlarm))); Über die serielle Schnittstelle (über USB mit dem STMduino-Bootloader oder über einen Serial Adapter) kann man vom PC aus Datum und Zeit im Format YYYYDDMM-hhmmss, also z. B. 20190515-093124 senden, um die Zeit punktgenau zu setzen. Denn bei Übernahme der Kompilierungszeit entsteht immer eine kleine Verzögerung.

Zeit auch beim stromlosem STM32 erhalten


RTC-Module für den Arduino haben normalerweise eine Aufnahme für eine Knopfzelle, die dafür sorgt, dass die Echtzeituhr auch weiterlaufen kann, wenn der Arduino vom Strom getrennt wird. So muss man nicht jedesmal die Zeit neu stellen.

Die Blue Pill hat dafür einen extra Anschluss namens VBat (meist VB oben rechts auf der Platine), an der man eine Batterie anschließen kann. Ich habe hier einfach 2 AA-Zellen genommen, die zusammen 3V liefern und wohl nahezu ewig halten werden. Daraus wird die sogenannte Backup Domain (und nur diese) weiter mit Strom versorgt, auch wenn der STM32 stromlos ist.

Zur Backup-Domain gehören: Das Datenblatt zum STM32F103xx von ST gibt nähere Auskunft zur Funktionsweise im VBat-Modus, über die Backup Register und die Kalibrierung der RTC mittels Registern.

Hier noch ein kleines Demonstrationsvideo, dass zeigt, dass die Batterie an VBat auch für das Weiterlaufen der RTC ohne Strom am STM32 funktioniert:



Source-Code

Ich habe den Code in drei Dateien aufgespaltet: STM32-RTC.ino, DateTime.h und RTCInterface.h. Diese am besten in der Arduino IDE in eigenen Tabs unter diesen Namen speichern, damit es keine Probleme beim Kompilieren gibt. Außerdem braucht es die oben erwähnte Library RTClock von Roger Clark.

STM32-RTC.ino //////////////////////////////////////////////////////// // (C) 2019 by Oliver Kuhlemann // // Bei Verwendung freue ich mich über Namensnennung, // // Quellenangabe und Verlinkung // // Quelle: http://cool-web.de/arduino/ // //////////////////////////////////////////////////////// // uses https://github.com/rogerclarkmelbourne/Arduino_STM32/tree/master/STM32F1/libraries/RTClock #include <RTClock.h> #include "DateTime.h" #include "RTCInterface.h" // den genauen 32768 Hertz-Timer für die Uhr benutzen (LSE), // auch ist nur der LSE durch VBat abgesichert RTClock rtclock (RTCSEL_LSE); void irq_alarm() { Serial.println ("\nALARM!!!\n"); } void irq_alarmzeit() { Serial.println ("\nIT'S TEA TIME!!!\n"); } void setup() { char buf[25]; //Serial.begin(115200); pinMode(PC13, OUTPUT); delay(1000); // auf die ser. Schnittstelle warten Serial.println (F("Compile-DateTime:")); Serial.println (__DATE__); Serial.println (__TIME__); DateTime compdat; DateTime rtcdat; compdat = getCompileDateTime(); storeDateTimeInString (compdat, buf, false); // Compile-Datum ausgeben Serial.println (buf); storeDateTimeInString (compdat, buf, true); // Compile-Datum mit Wochentag Serial.println (buf); uint64_t ts64; ts64=getDateTimeAsUint64(compdat); // Compile-Datum und Zeit als Zahl Serial.println (ts64); uint32_t ts32; ts32=getDateAsUint32(compdat); // Compile-Datum als Zahl Serial.println (ts32); ts32=getTimeAsUint32(compdat); // Compile-Zeit als Zahl Serial.println (ts32); Serial.println (F("\nRTC-DateTime:")); rtcdat = getRtcDateTime(rtclock); storeDateTimeInString (rtcdat, buf, true); // RTC-Datum ausgeben Serial.println (buf); if ( getDateAsUint32(rtcdat) < getDateAsUint32(compdat)) { // RTC-Zeit aus der Vergangenheit, kann nicht stimmen // Kopilierungszeit in Echtzeituhr speichern setRtcDateTime (rtclock, compdat); Serial.println (F("RTC-Uhrzeit auf Kompilierungszeit gesetzt.")); // ansonsten mit der Zeit in der RTC weitermachen } // Alarm setzen in 30 Sekunden rtclock.createAlarm(irq_alarm, (rtclock.getTime() + 30)); // Alarm setzen zu einer bestimmten Zeit DateTime datAlarm; datAlarm=getRtcDateTime (rtclock); datAlarm.hour=17; datAlarm.minute=0; datAlarm.second=0; rtclock.createAlarm(irq_alarmzeit, (timetFromDateTime(rtclock, datAlarm))); } void loop() { DateTime rtcdat; char buf[25]; time_t tt; // Sekunden seit 01.01.1970 // tm_t mtt; // struct (year (+1970), month, day, weekday, weekday, pm, hour, minute, second if ( Serial.available() > 14 ) { // 20190515-091017 // setzen der Uhrzeit per Ser-Schnittstelle DateTime serdat; for (byte i = 0; i < 15; i++) { buf[i] = Serial.read(); } Serial.flush(); buf [15] =0; sscanf (buf, "%4d%2d%2d-%2d%2d%2d", &serdat.year, &serdat.month, &serdat.day, &serdat.hour, &serdat.minute, &serdat.second); setRtcDateTime (rtclock, serdat); } tt = rtclock.getTime(); Serial.println(tt); rtcdat = getRtcDateTime(rtclock); storeDateTimeInString (rtcdat, buf, true); // RTC-Datum mit Wochentag Serial.println (buf); delay (1000); digitalWrite (PC13, !digitalRead(PC13)); }

DateTime.h //////////////////////////////////////////////////////// // (C) 2019 by Oliver Kuhlemann // // Bei Verwendung freue ich mich über Namensnennung, // // Quellenangabe und Verlinkung // // Quelle: http://cool-web.de/arduino/ // //////////////////////////////////////////////////////// struct DateTime { uint16_t year=0; uint8_t month=0; uint8_t day=0; uint8_t hour=0; uint8_t minute=0; uint8_t second=0; uint8_t weekday=0; }; static const char *weekDays[7] = { "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" }; static const char *monthNames[12] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; uint8_t calcWeekday (int d, int m, int y) { uint8_t w; if (m < 3) y -= 1; w = ((d + (int) (2.6 * ((m + 9) % 12 + 1) - 0.2) + y % 100 + (int) (y % 100 / 4) + (int) (y / 400) - 2 * (int) (y / 100) - 1) % 7 + 7) % 7 + 1; return w; } DateTime getCompileDateTime () { // __DATE__ = May 9 2019 DateTime dat; char monthName[20]; uint8_t day = 0; uint8_t month = 0; uint16_t year = 0; char compDate[13]; strncpy(compDate, __DATE__, 12); sscanf (compDate, "%s %2d %4d", monthName, &day, &year); for (int i = 0; i < 12; i++) { if (strncmp (monthNames[i], monthName, 3) == 0) { month = i + 1; break; } } dat.year = year; dat.month = month; dat.day = day; // __TIME__ = 20:56:25 uint8_t hour = 0; uint8_t minute = 0; uint8_t second = 0; sscanf (__TIME__, "%2d:%2d:%2d", &hour, &minute, &second); dat.hour = hour; dat.minute = minute; dat.second = second; dat.weekday = calcWeekday (dat.day, dat.month, dat.year); return dat; } void storeDateTimeInString (DateTime dat, char *buf, boolean withWeekday) { // buf min. 20 bytes, mit Weekday mit 25 if (withWeekday) { snprintf (buf, 25, "%s, %d-%02d-%02d %02d:%02d:%02d", (dat.weekday <1 || dat.weekday >7) ? "???" : weekDays[dat.weekday - 1], dat.year, dat.month, dat.day, dat.hour, dat.minute, dat.second); } else { snprintf (buf, 20, "%d-%02d-%02d %02d:%02d:%02d", dat.year, dat.month, dat.day, dat.hour, dat.minute, dat.second); } } uint32_t getDateAsUint32 (DateTime dat) { // gibt z. B. 20190510 zurück, alles Ziffern YMD uint32_t ts; ts=dat.year*10000; ts+=dat.month*100; ts+=dat.day; return ts; } uint32_t getTimeAsUint32 (DateTime dat) { // gibt z. B. 20190510155906 zurück, alles Ziffern YMDhms uint32_t ts; ts=dat.hour*10000; ts+=dat.minute*100; ts+=dat.second; return ts; } uint64_t getDateTimeAsUint64 (DateTime dat) { // gibt z. B. 20190510155906 zurück, alles Ziffern YMDhms uint64_t ts; ts=dat.year*10000000000; ts+=dat.month*100000000; ts+=dat.day*1000000; ts+=dat.hour*10000; ts+=dat.minute*100; ts+=dat.second; return ts; }

RTCInterface.h //////////////////////////////////////////////////////// // (C) 2019 by Oliver Kuhlemann // // Bei Verwendung freue ich mich über Namensnennung, // // Quellenangabe und Verlinkung // // Quelle: http://cool-web.de/arduino/ // //////////////////////////////////////////////////////// // uses https://github.com/rogerclarkmelbourne/Arduino_STM32/tree/master/STM32F1/libraries/RTClock time_t timetFromDateTime (RTClock rtclock, DateTime dat) { time_t tt; tm_t mtt; mtt.year = dat.year-1970; mtt.month = dat.month; mtt.day = dat.day; mtt.hour = dat.hour; mtt.minute = dat.minute; mtt.second = dat.second; mtt.weekday = dat.weekday == 1 ? 7 : dat.weekday-1; tt = rtclock.makeTime(mtt); return tt; } void setRtcDateTime (RTClock rtclock, DateTime dat) { time_t tt; tt=timetFromDateTime (rtclock, dat); rtclock.setTime(tt); } void setRtcDateTime (RTClock rtclock, time_t tt) { rtclock.setTime(tt); } DateTime getRtcDateTime (RTClock rtclock) { DateTime dat; time_t tt; tm_t mtt; tt=rtc_get_count(); rtclock.breakTime(tt, mtt); dat.year = mtt.year+1970; dat.month = mtt.month; dat.day = mtt.day; dat.hour = mtt.hour; dat.minute = mtt.minute; dat.second = mtt.second; dat.weekday = mtt.weekday == 7 ? 1 : mtt.weekday+1; return dat; } time_t getRtcTimeT (RTClock rtclock) { time_t tt; tt=rtc_get_count(); return tt; }