#include "timekeeper.h" #if (defined HAS_DCF77 && defined HAS_IF482) #error You must define at most one of IF482 or DCF77! #endif // Local logging tag static const char TAG[] = __FILE__; // symbol to display current time source // G = GPS / R = RTC / L = LORA / * = no sync / ? = never synced const char timeSetSymbols[] = {'G', 'R', 'L', '*', '?'}; DRAM_ATTR bool TimePulseTick = false; DRAM_ATTR unsigned long lastPPS = millis(); timesource_t timeSource = _unsynced; TaskHandle_t ClockTask = NULL; hw_timer_t *ppsIRQ = NULL; #ifdef HAS_IF482 HardwareSerial IF482(2); // use UART #2 (#1 may be in use for serial GPS) static TickType_t txDelay = pdMS_TO_TICKS(1000 - IF482_SYNC_FIXUP) - tx_Ticks(IF482_FRAME_SIZE, HAS_IF482); #if (HAS_SDS011) #error cannot use IF482 together with SDS011 (both use UART#2) #endif #endif // HAS_IF482 Ticker timesyncer; void setTimeSyncIRQ() { xTaskNotify(irqHandlerTask, TIMESYNC_IRQ, eSetBits); } void calibrateTime(void) { // kick off asynchronous lora timesync if we have #if (HAS_LORA_TIME) timesync_request(); if (timeSource == _lora) // did have lora time before? return; #endif #if ((HAS_GPS) || (HAS_RTC)) time_t t = 0; uint16_t t_msec = 0; // get GPS time, if we have #if (HAS_GPS) t = get_gpstime(&t_msec); if (setMyTime((uint32_t)t, t_msec, _gps)) return; #endif // fallback to RTC time, if we have #ifdef HAS_RTC t = get_rtctime(&t_msec); if (setMyTime((uint32_t)t, t_msec, _rtc)) return; #endif #endif } // calibrateTime() // set system time (UTC), calibrate RTC and RTC_INT pps bool setMyTime(uint32_t t_sec, uint16_t t_msec, timesource_t mytimesource) { struct timeval tv = {0}; // called with invalid timesource? if (mytimesource == _unsynced) return false; // increment t_sec if t_msec > 1000 time_t time_to_set = (time_t)(t_sec + t_msec / 1000); // do we have a valid time? if (timeIsValid(time_to_set)) { // if we have msec fraction, then wait until top of second with // millisecond precision if (t_msec % 1000) { time_to_set++; vTaskDelay(pdMS_TO_TICKS(1000 - t_msec % 1000)); } // from here on we are on top of next second tv.tv_sec = time_to_set; tv.tv_usec = 0; sntp_sync_time(&tv); ESP_LOGI(TAG, "[%0.3f] UTC time: %d.000 sec", _seconds(), time_to_set); // if we have a precise time timesource, set RTC time and shift RTC_INT // pulse to top of second #ifdef HAS_RTC if ((mytimesource == _gps) || (mytimesource == _lora)) set_rtctime(time_to_set); #endif // if we have a software pps timer, shift it to top of second if (ppsIRQ != NULL) { timerWrite(ppsIRQ, 0); // reset pps timer CLOCKIRQ(); // fire clock pps to advance wall clock by 1 sec } timeSource = mytimesource; // set global variable timesyncer.attach(TIME_SYNC_INTERVAL * 60, setTimeSyncIRQ); ESP_LOGD(TAG, "[%0.3f] Timesync finished, time was set | timesource=%d", _seconds(), mytimesource); return true; } else { timesyncer.attach(TIME_SYNC_INTERVAL_RETRY * 60, setTimeSyncIRQ); ESP_LOGV(TAG, "[%0.3f] Failed to synchronise time from source %c | unix sec " "obtained from source: %d | unix sec at program compilation: %d", _seconds(), timeSetSymbols[mytimesource], time_to_set, compileTime()); return false; } } // helper function to setup a pulse per second for time synchronisation uint8_t timepulse_init() { // set esp-idf API sntp sync mode // sntp_init(); sntp_set_sync_mode(SNTP_SYNC_MODE_IMMED); // if we have, use PPS time pulse from GPS for syncing time on top of second #ifdef GPS_INT // setup external interupt pin for rising edge of GPS PPS pinMode(GPS_INT, INPUT_PULLDOWN); attachInterrupt(digitalPinToInterrupt(GPS_INT), GPSIRQ, RISING); #endif // if we have, use pulse from on board RTC chip as time base for calendar time #if defined RTC_INT // setup external rtc 1Hz clock pulse if (I2C_MUTEX_LOCK()) { Rtc.SetSquareWavePinClockFrequency(DS3231SquareWaveClock_1Hz); Rtc.SetSquareWavePin(DS3231SquareWavePin_ModeClock); I2C_MUTEX_UNLOCK(); pinMode(RTC_INT, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(RTC_INT), CLOCKIRQ, FALLING); ESP_LOGI(TAG, "Timepulse: external (RTC)"); return 1; // success } else { ESP_LOGE(TAG, "RTC initialization error, I2C bus busy"); return 0; // failure } return 1; // success #else // use ESP32 hardware timer as time base for calendar time ppsIRQ = timerBegin(1, 8000, true); // set 80 MHz prescaler to 1/10000 sec timerAlarmWrite(ppsIRQ, 10000, true); // 1000ms timerAttachInterrupt(ppsIRQ, &CLOCKIRQ, false); timerAlarmEnable(ppsIRQ); ESP_LOGI(TAG, "Timepulse: internal (ESP32 hardware timer)"); return 1; // success #endif // start cyclic time sync timesyncer.attach(TIME_SYNC_INTERVAL * 60, setTimeSyncIRQ); // get time if we don't have one if (timeSource != _set) setTimeSyncIRQ(); // init systime by RTC or GPS or LORA } // timepulse_init // interrupt service routine triggered by GPS PPS void IRAM_ATTR GPSIRQ(void) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; // take timestamp lastPPS = millis(); // last time of pps // yield only if we should if (xHigherPriorityTaskWoken) portYIELD_FROM_ISR(); } // interrupt service routine triggered by esp32 hardware timer void IRAM_ATTR CLOCKIRQ(void) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; // advance wall clock, if we have #if (defined HAS_IF482 || defined HAS_DCF77) xTaskNotifyFromISR(ClockTask, uint32_t(time(NULL)), eSetBits, &xHigherPriorityTaskWoken); #endif // flip time pulse ticker, if needed #ifdef HAS_DISPLAY TimePulseTick = !TimePulseTick; // flip global variable pulse ticker #endif // yield only if we should if (xHigherPriorityTaskWoken) portYIELD_FROM_ISR(); } // helper function to check plausibility of a given epoch time bool timeIsValid(time_t const t) { // is t a time in the past? we use compile time to guess // compile time is some local time, but we do not know it's time zone // thus, we go 1 full day back to be sure to catch a time in the past return (t > (compileTime() - 86400)); } // helper function to calculate serial transmit time TickType_t tx_Ticks(uint32_t framesize, unsigned long baud, uint32_t config, int8_t rxPin, int8_t txPins) { uint32_t databits = ((config & 0x0c) >> 2) + 5; uint32_t stopbits = ((config & 0x20) >> 5) + 1; uint32_t txTime = (databits + stopbits + 1) * framesize * 1000.0 / baud; // +1 for the startbit return round(txTime); } void clock_init(void) { // setup clock output interface #ifdef HAS_IF482 IF482.begin(HAS_IF482); #elif defined HAS_DCF77 pinMode(HAS_DCF77, OUTPUT); #endif xTaskCreatePinnedToCore(clock_loop, // task function "clockloop", // name of task 3072, // stack size of task (void *)1, // task parameter 6, // priority of the task &ClockTask, // task handle 1); // CPU core _ASSERT(ClockTask != NULL); // has clock task started? } // clock_init void clock_loop(void *taskparameter) { // ClockTask uint32_t current_time = 0, previous_time = 0; time_t tt; struct tm t = {0}; #ifdef HAS_TWO_LED static bool led1_state = false; #endif #ifdef HAS_DCF77 uint64_t ClockPulse = 0; int8_t ClockMinute = -1; #endif // output the next second's pulse/telegram after pps arrived for (;;) { // wait for timepulse and store UTC time xTaskNotifyWait(0x00, ULONG_MAX, ¤t_time, portMAX_DELAY); if ((sntp_get_sync_status() == SNTP_SYNC_STATUS_IN_PROGRESS) || !(timeIsValid(current_time)) || (current_time == previous_time)) continue; // set calendar time for next second of clock output tt = (time_t)(current_time + 1); localtime_r(&tt, &t); tt = mktime(&t); #if defined HAS_IF482 // wait until moment to fire. Normally we won't get notified during this // timespan, except when next pps pulse arrives while waiting, because pps // was adjusted by recent time sync, then advance next_time one second if (xTaskNotifyWait(0x00, ULONG_MAX, ¤t_time, txDelay) == pdTRUE) { tt = (time_t)(current_time + 1); localtime_r(&tt, &t); tt = mktime(&t); } // send IF482 telegram IF482.print(IF482_Frame(tt)); // note: telegram is for *next* second ESP_LOGD(TAG, "[%0.3f] IF482: %s", _seconds(), IF482_Frame(tt).c_str()); #elif defined HAS_DCF77 // load new frame if second 59 is reached if (t.tm_sec == 0) { ClockMinute = t.tm_min; t.tm_min++; // follow-up minute mktime(&t); // normalize calendar time ClockPulse = DCF77_Frame(t); // generate pulse frame /* to do here: leap second handling in second 59 */ ESP_LOGD(TAG, "[%0.3f] DCF77: new frame for min %d", _seconds(), t.tm_min); } else { // generate impulse if (t.tm_min == ClockMinute) { // ensure frame is recent DCF77_Pulse(ClockPulse & 1); // output next second ClockPulse >>= 1; } } #endif // pps blink on secondary LED if we have one #ifdef HAS_TWO_LED if (led1_state) switch_LED1(LED_OFF); else switch_LED1(LED_ON); led1_state = !led1_state; #endif previous_time = current_time; } // for } // clock_loop() // we use compile date to create a time_t reference "in the past" time_t compileTime(void) { char s_month[5]; int year; struct tm t = {0}; static const char month_names[] = "JanFebMarAprMayJunJulAugSepOctNovDec"; // store compile time once it's calculated static time_t secs = -1; if (secs == -1) { // determine date sscanf(__DATE__, "%s %d %d", s_month, &t.tm_mday, &year); t.tm_mon = (strstr(month_names, s_month) - month_names) / 3; t.tm_year = year - 1900; // determine time sscanf(__TIME__, "%d:%d:%d", &t.tm_hour, &t.tm_min, &t.tm_sec); // convert to secs local time secs = mktime(&t); } return secs; } static bool IsLeapYear(short year) { if (year % 4 != 0) return false; if (year % 100 != 0) return true; return (year % 400) == 0; } // convert UTC tm time to time_t epoch time time_t mkgmtime(const struct tm *ptm) { const int SecondsPerMinute = 60; const int SecondsPerHour = 3600; const int SecondsPerDay = 86400; const int DaysOfMonth[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; time_t secs = 0; // tm_year is years since 1900 int year = ptm->tm_year + 1900; for (int y = 1970; y < year; ++y) { secs += (IsLeapYear(y) ? 366 : 365) * SecondsPerDay; } // tm_mon is month from 0..11 for (int m = 0; m < ptm->tm_mon; ++m) { secs += DaysOfMonth[m] * SecondsPerDay; if (m == 1 && IsLeapYear(year)) secs += SecondsPerDay; } secs += (ptm->tm_mday - 1) * SecondsPerDay; secs += ptm->tm_hour * SecondsPerHour; secs += ptm->tm_min * SecondsPerMinute; secs += ptm->tm_sec; return secs; }