diff --git a/radio/src/cli.cpp b/radio/src/cli.cpp index 372348ae8db..f009b4a7e6b 100644 --- a/radio/src/cli.cpp +++ b/radio/src/cli.cpp @@ -248,6 +248,40 @@ int toInt(const char ** argv, int index, int * val) return result; } +// "30" -> 30 s, "30.52" -> 30 s 520 ms +static int toSeconds(const char ** argv, int index, int * sec, int * ms) +{ + const char * s = argv[index]; + if (s == nullptr || *s == '\0') return 0; + + char * endptr = nullptr; + long v = strtol(s, &endptr, 10); + if (endptr == s || v < 0 || v > 59) { + cliSerialPrint("%s: Invalid argument \"%s\"", argv[0], s); + return -1; + } + *sec = (int)v; + *ms = 0; + if (*endptr == '\0') return 1; + if (*endptr != '.') { + cliSerialPrint("%s: Invalid argument \"%s\"", argv[0], s); + return -1; + } + + int scale = 100; + for (const char * p = endptr + 1; *p; p++) { + if (*p < '0' || *p > '9') { + cliSerialPrint("%s: Invalid argument \"%s\"", argv[0], s); + return -1; + } + if (scale) { + *ms += (*p - '0') * scale; + scale /= 10; + } + } + return 2; +} + int cliBeep(const char ** argv) { int freq = BEEP_DEFAULT_FREQ; @@ -1083,10 +1117,11 @@ int cliSet(const char **argv) { if (!strcmp(argv[1], "rtc")) { struct gtm t; - int year, month, day, hour, minute, second; + int year, month, day, hour, minute, second, ms = 0; + int secOk = toSeconds(argv, 7, &second, &ms); if (toInt(argv, 2, &year) > 0 && toInt(argv, 3, &month) > 0 && toInt(argv, 4, &day) > 0 && toInt(argv, 5, &hour) > 0 && - toInt(argv, 6, &minute) > 0 && toInt(argv, 7, &second) > 0) { + toInt(argv, 6, &minute) > 0 && secOk > 0) { t.tm_year = year - TM_YEAR_BASE; t.tm_mon = month - 1; t.tm_mday = day; @@ -1095,13 +1130,45 @@ int cliSet(const char **argv) t.tm_sec = second; // update local timestamp and get wday calculated g_rtcTime = gmktime(&t); - rtcSetTime(&t); + // the CLI is driven by a host, the menu is where someone sets it by hand + rtcSetTimeAt(&t, (uint16_t)ms); +#if defined(DEBUG) + const struct RtcCalibReport * rep = rtcGetCalibrationReport(); + if (rep->elapsed != 0) { + int32_t ppm10 = 0; + if (rep->elapsed > 0) + ppm10 = (int32_t)(((int64_t)rep->errorMs * 10000) / rep->elapsed); + cliSerialPrint("rtc drift = %d ms over %d s (%d ppm x10)", (int)rep->errorMs, + (int)rep->elapsed, (int)ppm10); + } + int32_t units = rtcGetCalibration(); + cliSerialPrint("rtc calibration: %s (now %d units, %d ppm x10)", + rtcCalibrationResultText(rep->result), (int)units, + (int)rtcCalibrationPpm10(units)); +#endif } else { cliSerialPrint("%s: Invalid arguments \"%s\" \"%s\"", argv[0], argv[1], argv[2]); return -1; } } +#if defined(DEBUG) + else if (!strcmp(argv[1], "rtccal")) { + int ppm = 0; + if (!strcmp(argv[2], "reset")) { + rtcResetCalibration(); + } else if (toInt(argv, 2, &ppm) > 0) { + rtcSetCalibration(rtcCalibrationUnits(ppm)); + } else { + cliSerialPrint("%s: expected \"reset\" or a ppm value", argv[0]); + return -1; + } + int32_t units = rtcGetCalibration(); + cliSerialPrint("rtc calibration = %d units (%d ppm x10), reference = %u", + (int)units, (int)rtcCalibrationPpm10(units), + (unsigned)rtcGetCalibrationRef()); + } +#endif #if !defined(SOFTWARE_VOLUME) && defined(AUDIO) else if (!strcmp(argv[1], "volume")) { int level = 0; @@ -1494,8 +1561,22 @@ int cliDisplay(const char ** argv) } else if (!strcmp(argv[1], "rtc")) { struct gtm utm; + uint8_t sw100 = g_ms100; gettime(&utm); - cliSerialPrint("rtc = %4d-%02d-%02d %02d:%02d:%02d.%02d0", utm.tm_year+TM_YEAR_BASE, utm.tm_mon+1, utm.tm_mday, utm.tm_hour, utm.tm_min, utm.tm_sec, g_ms100); + cliSerialPrint("rtc = %4d-%02d-%02d %02d:%02d:%02d.%02d0", utm.tm_year+TM_YEAR_BASE, utm.tm_mon+1, utm.tm_mday, utm.tm_hour, utm.tm_min, utm.tm_sec, sw100); + + // gettime() reports the software clock, which free runs on the system tick + struct gtm htm = {}; + uint16_t hwMs = rtcGetTimeMs(&htm); + cliSerialPrint("rtc hw = %4d-%02d-%02d %02d:%02d:%02d.%03d", htm.tm_year+TM_YEAR_BASE, + htm.tm_mon+1, htm.tm_mday, htm.tm_hour, htm.tm_min, htm.tm_sec, (int)hwMs); + int32_t skew = (int32_t)(((int64_t)gmktime(&utm) - gmktime(&htm)) * 1000 + + (int32_t)sw100 * 10 - hwMs); + cliSerialPrint("rtc software clock offset = %d ms", (int)skew); + int32_t units = rtcGetCalibration(); + cliSerialPrint("rtc calibration = %d units (%d ppm x10), reference = %u", + (int)units, (int)rtcCalibrationPpm10(units), + (unsigned)rtcGetCalibrationRef()); } #if defined(VOLUME_I2C_ADDRESS) else if (!strcmp(argv[1], "volume")) { diff --git a/radio/src/rtc.cpp b/radio/src/rtc.cpp index 96a33a3f042..0640739f5ac 100644 --- a/radio/src/rtc.cpp +++ b/radio/src/rtc.cpp @@ -22,8 +22,6 @@ #include #include "edgetx.h" -extern void rtcdriver_settime(struct gtm * t); - #define LEAP_SECONDS_POSSIBLE 0 /* Shift A right by B bits portably, by dividing A by 2**B and @@ -445,8 +443,6 @@ void gettime(struct gtm * tm) filltm(&g_rtcTime, tm); // create a struct tm date/time structure from global unix time stamp } -void rtcGetTime(struct gtm * t); - #define RTC_ADJUST_PERIOD 60 // how often RTC is checked for accuracy [seconds] #define RTC_ADJUST_TRESHOLD 20 // how much clock must differ before adjustment is made [seconds] /* @@ -494,6 +490,185 @@ uint8_t rtcAdjust(uint16_t year, uint8_t mon, uint8_t day, uint8_t hour, uint8_t return 0; } +// Crystal drift measured between two clock settings, compensated by the RTC +// smooth calibration hardware. Reference kept in the RTC backup domain. + +#define RTC_CALIB_MIN_ELAPSED SECS_PER_DAY // no reliable measurement below that +#define RTC_CALIB_MAX_ELAPSED (10 * 365 * SECS_PER_DAY) // nonsense reference +#define RTC_CALIB_MIN_ERROR 4000 // [ms] below that it is input noise +#define RTC_CALIB_MIN_ERROR_TIMED 500 // [ms] host timed: link latency only +#define RTC_CALIB_SET_ACCURACY 2000 // [ms] hand setting accuracy +#define RTC_CALIB_MAX_PPM 200 // beyond any crystal: clock was set +#define RTC_CALIB_TZ_STEP 900000 // [ms] time zone granularity +#define RTC_CALIB_TZ_BAND 60000 // [ms] tolerance around it +#define RTC_CALIB_MIN_YEAR 101 // 2001, RTC resets to 2000 +#define RTC_CALIB_SESSION_GAP (5 * 60 * 100) // [10ms] same edit session + +// Sampled before the clock got changed +static bool rtcCalibSession = false; +static tmr10ms_t rtcCalibLastSet = 0; +static gtime_t rtcCalibSessionRef = 0; +static gtime_t rtcCalibSessionRtc = 0; +static uint16_t rtcCalibSessionMs = 0; +static int32_t rtcCalibSessionUnits = 0; +static bool rtcCalibSessionValid = false; + +int32_t rtcCalibrationPpm10(int32_t units) +{ + return (int32_t)(((int64_t)units * 10000000) / RTC_CALIB_UNITS_PER_SECOND); +} + +int32_t rtcCalibrationUnits(int32_t ppm) +{ + int64_t v = (int64_t)ppm * RTC_CALIB_UNITS_PER_SECOND; + v += (v < 0) ? -500000 : 500000; + return (int32_t)(v / 1000000); +} + +static void rtcCalibrationStart() +{ + struct gtm before = {}; + uint16_t ms = rtcGetTimeMs(&before); + + rtcCalibSessionValid = (before.tm_year >= RTC_CALIB_MIN_YEAR); + rtcCalibSessionRtc = rtcCalibSessionValid ? gmktime(&before) : 0; + rtcCalibSessionMs = rtcCalibSessionValid ? ms : 0; + rtcCalibSessionRef = rtcGetCalibrationRef(); + rtcCalibSessionUnits = rtcGetCalibration(); + rtcCalibSession = true; +} + +#if defined(DEBUG) +static RtcCalibReport rtcCalibReport = {}; + +const struct RtcCalibReport * rtcGetCalibrationReport() +{ + return &rtcCalibReport; +} + +const char * rtcCalibrationResultText(uint8_t result) +{ + switch (result) { + case RTC_CALIB_APPLIED: return "applied"; + case RTC_CALIB_NO_REF: return "no reference yet, this setting becomes one"; + case RTC_CALIB_CLOCK_INVALID: return "previous clock invalid, RTC had lost power"; + case RTC_CALIB_REF_AHEAD: return "reference is in the future"; + case RTC_CALIB_TOO_SOON: return "less than a day since the reference"; + case RTC_CALIB_REF_TOO_OLD: return "reference older than 10 years"; + case RTC_CALIB_TOO_SMALL: return "drift under 4 s, indistinguishable from input noise"; + case RTC_CALIB_TOO_LARGE: return "over 200 ppm, clock assumed moved on purpose"; + case RTC_CALIB_TIME_ZONE: return "near a 15 min step, assumed time zone or DST change"; + default: return "unknown"; + } +} +#endif + +// Recomputed from the calibration in use at session start, so that successive +// edits converge instead of piling up +static uint8_t rtcCalibrationUpdate(gtime_t newTime, uint16_t newMs, bool timed) +{ +#if defined(DEBUG) + rtcCalibReport.elapsed = 0; + rtcCalibReport.errorMs = 0; +#endif + + // Initial setting, or clock that lost power + if (rtcCalibSessionRef == 0) return RTC_CALIB_NO_REF; + if (!rtcCalibSessionValid) return RTC_CALIB_CLOCK_INVALID; + + gtime_t elapsed = newTime - rtcCalibSessionRef; + // > 0 when the clock runs fast + int64_t errorMs = ((int64_t)rtcCalibSessionRtc - newTime) * 1000 + + rtcCalibSessionMs - newMs; + +#if defined(DEBUG) + rtcCalibReport.elapsed = elapsed; + rtcCalibReport.errorMs = (int32_t)limit(INT32_MIN, errorMs, INT32_MAX); +#endif + + if (elapsed < 0) return RTC_CALIB_REF_AHEAD; + if (elapsed < (gtime_t)RTC_CALIB_MIN_ELAPSED) return RTC_CALIB_TOO_SOON; + if (elapsed > (gtime_t)RTC_CALIB_MAX_ELAPSED) return RTC_CALIB_REF_TOO_OLD; + + int64_t absError = (errorMs < 0) ? -errorMs : errorMs; + + // Too small to tell from input noise + if (absError < (timed ? RTC_CALIB_MIN_ERROR_TIMED : RTC_CALIB_MIN_ERROR)) + return RTC_CALIB_TOO_SMALL; + + // Faster than any crystal: the clock was moved + if (absError * 1000 > (int64_t)elapsed * RTC_CALIB_MAX_PPM) return RTC_CALIB_TOO_LARGE; + + // A whole time zone step is a time zone or DST change + if (absError >= RTC_CALIB_TZ_STEP - RTC_CALIB_TZ_BAND) { + int64_t rem = absError % RTC_CALIB_TZ_STEP; + if (rem <= RTC_CALIB_TZ_BAND || rem >= RTC_CALIB_TZ_STEP - RTC_CALIB_TZ_BAND) + return RTC_CALIB_TIME_ZONE; + } + + // Only a hand set needs that margin, a host timed one is good to the link latency + if (!timed) absError -= RTC_CALIB_SET_ACCURACY; + + int64_t denom = (int64_t)elapsed * 1000; + int32_t delta = (int32_t)((absError * RTC_CALIB_UNITS_PER_SECOND + denom / 2) / denom); + if (errorMs < 0) delta = -delta; + + int32_t units = rtcCalibSessionUnits - delta; + if (units > RTC_CALIB_UNIT_MAX) units = RTC_CALIB_UNIT_MAX; + if (units < RTC_CALIB_UNIT_MIN) units = RTC_CALIB_UNIT_MIN; + + rtcSetCalibration(units); + + TRACE("RTC drift %d ms over %d s, calibration %d -> %d units (%d -> %d ppm x10)", + (int)errorMs, (int)elapsed, (int)rtcCalibSessionUnits, (int)units, + (int)rtcCalibrationPpm10(rtcCalibSessionUnits), + (int)rtcCalibrationPpm10(units)); + + return RTC_CALIB_APPLIED; +} + +// Clears the hardware trim and the stored reference, so the next two settings +// start a fresh measurement +void rtcResetCalibration() +{ + rtcSetCalibration(0); + rtcClearCalibrationRef(); + rtcCalibSession = false; + rtcCalibLastSet = 0; +} + +// A host timed set is a one shot, never part of a menu edit session +static void rtcSetTimeInternal(const struct gtm * t, uint16_t ms, bool timed) +{ + struct gtm tm = *t; + gtime_t newTime = gmktime(&tm); + + tmr10ms_t now = get_tmr10ms(); + if (timed || !rtcCalibSession || (now - rtcCalibLastSet) > RTC_CALIB_SESSION_GAP) { + rtcCalibrationStart(); + } + rtcCalibLastSet = now; + + rtcDriverSetTime(t); + +#if defined(DEBUG) + rtcCalibReport.result = rtcCalibrationUpdate(newTime, ms, timed); +#else + rtcCalibrationUpdate(newTime, ms, timed); +#endif + rtcSetCalibrationRef(newTime); +} + +void rtcSetTime(const struct gtm * t) +{ + rtcSetTimeInternal(t, 0, false); +} + +void rtcSetTimeAt(const struct gtm * t, uint16_t ms) +{ + rtcSetTimeInternal(t, ms, true); +} + bool rtcIsValid() { struct gtm t; diff --git a/radio/src/rtc.h b/radio/src/rtc.h index 40a985ef0dd..d4bc6925ee9 100644 --- a/radio/src/rtc.h +++ b/radio/src/rtc.h @@ -52,9 +52,58 @@ extern uint8_t g_ms100; // global to allow time set function to reset to zero bool rtcIsValid(); void rtcInit(); void rtcSetTime(const struct gtm * tm); +// Host timed the second boundary itself, ms is how far into it the true time was +void rtcSetTimeAt(const struct gtm * tm, uint16_t ms); gtime_t gmktime (struct gtm *tm); uint8_t rtcAdjust(uint16_t year, uint8_t mon, uint8_t day, uint8_t hour, uint8_t min, uint8_t sec); +// Driver interface, rtcSetTime() wraps rtcDriverSetTime() +void rtcDriverSetTime(const struct gtm * tm); +void rtcGetTime(struct gtm * tm); +uint16_t rtcGetTimeMs(struct gtm * tm); // fills tm, returns 0..999 within that second + +// Smooth calibration, one unit is one clock pulse out of 2^20 (~0.954 ppm) +#define RTC_CALIB_UNITS_PER_SECOND 1048576 +#define RTC_CALIB_UNIT_MAX 512 +#define RTC_CALIB_UNIT_MIN (-511) + +int32_t rtcGetCalibration(); +void rtcSetCalibration(int32_t units); +int32_t rtcCalibrationPpm10(int32_t units); +int32_t rtcCalibrationUnits(int32_t ppm); + +// Why the last clock setting did or did not move the calibration +enum { + RTC_CALIB_APPLIED, + RTC_CALIB_NO_REF, + RTC_CALIB_CLOCK_INVALID, + RTC_CALIB_REF_AHEAD, + RTC_CALIB_TOO_SOON, + RTC_CALIB_REF_TOO_OLD, + RTC_CALIB_TOO_SMALL, + RTC_CALIB_TOO_LARGE, + RTC_CALIB_TIME_ZONE, +}; + +#if defined(DEBUG) +struct RtcCalibReport { + uint8_t result; + gtime_t elapsed; + int32_t errorMs; // clamped, > 0 when the clock runs fast +}; + +const struct RtcCalibReport * rtcGetCalibrationReport(); +const char * rtcCalibrationResultText(uint8_t result); +#endif + +// Time of the last known good setting, 0 when unknown +gtime_t rtcGetCalibrationRef(); +void rtcSetCalibrationRef(gtime_t t); +void rtcClearCalibrationRef(); + +// Back to a factory-fresh state, as if the backup domain had been lost +void rtcResetCalibration(); + #if defined(__cplusplus) && !defined(SIMU) extern "C" { #endif diff --git a/radio/src/targets/common/arm/stm32/f4/CMakeLists.txt b/radio/src/targets/common/arm/stm32/f4/CMakeLists.txt index 72477d740bd..3872d8df029 100644 --- a/radio/src/targets/common/arm/stm32/f4/CMakeLists.txt +++ b/radio/src/targets/common/arm/stm32/f4/CMakeLists.txt @@ -41,6 +41,7 @@ if(NOT NATIVE_BUILD) ${STM32CUBE_DIR}/Src/stm32f4xx_hal_rcc.c ${STM32CUBE_DIR}/Src/stm32f4xx_hal_rcc_ex.c ${STM32CUBE_DIR}/Src/stm32f4xx_hal_rtc.c + ${STM32CUBE_DIR}/Src/stm32f4xx_hal_rtc_ex.c ${STM32CUBE_DIR}/Src/stm32f4xx_hal_i2c.c ${STM32CUBE_DIR}/Src/stm32f4xx_hal_i2c_ex.c ${STM32CUBE_DIR}/Src/stm32f4xx_hal_sd.c diff --git a/radio/src/targets/common/arm/stm32/h7/CMakeLists.txt b/radio/src/targets/common/arm/stm32/h7/CMakeLists.txt index 8455b8335fc..bc1751a2d7e 100644 --- a/radio/src/targets/common/arm/stm32/h7/CMakeLists.txt +++ b/radio/src/targets/common/arm/stm32/h7/CMakeLists.txt @@ -44,6 +44,7 @@ if(NOT NATIVE_BUILD) ${STM32CUBE_SRC_PREFIX}_hal_rcc.c ${STM32CUBE_SRC_PREFIX}_hal_rcc_ex.c ${STM32CUBE_SRC_PREFIX}_hal_rtc.c + ${STM32CUBE_SRC_PREFIX}_hal_rtc_ex.c ${STM32CUBE_SRC_PREFIX}_hal_i2c.c ${STM32CUBE_SRC_PREFIX}_hal_i2c_ex.c ${STM32CUBE_SRC_PREFIX}_hal_i2s.c @@ -85,6 +86,7 @@ if(NOT NATIVE_BUILD) ${STM32CUBE_SRC_PREFIX}_hal_rcc.c ${STM32CUBE_SRC_PREFIX}_hal_rcc_ex.c ${STM32CUBE_SRC_PREFIX}_hal_rtc.c + ${STM32CUBE_SRC_PREFIX}_hal_rtc_ex.c ${STM32CUBE_SRC_PREFIX}_hal_i2c.c ${STM32CUBE_SRC_PREFIX}_hal_i2c_ex.c ${STM32CUBE_SRC_PREFIX}_hal_i2s.c diff --git a/radio/src/targets/common/arm/stm32/rtc_driver.cpp b/radio/src/targets/common/arm/stm32/rtc_driver.cpp index ae9141c7512..85d2c65e6f4 100644 --- a/radio/src/targets/common/arm/stm32/rtc_driver.cpp +++ b/radio/src/targets/common/arm/stm32/rtc_driver.cpp @@ -24,7 +24,7 @@ RTC_HandleTypeDef rtc = {}; -void rtcSetTime(const struct gtm * t) +void rtcDriverSetTime(const struct gtm * t) { g_ms100 = 0; // start of next second begins now @@ -41,7 +41,9 @@ void rtcSetTime(const struct gtm * t) HAL_RTC_SetDate(&rtc, &RTC_DateStruct, RTC_FORMAT_BIN); } -void rtcGetTime(struct gtm * t) +// SSR must be read before TR, and TR before DR, or the shadow registers stay +// frozen. HAL does that within one call, so seconds and sub-seconds match. +uint16_t rtcGetTimeMs(struct gtm * t) { RTC_TimeTypeDef RTC_TimeStruct; RTC_DateTypeDef RTC_DateStruct; @@ -55,8 +57,75 @@ void rtcGetTime(struct gtm * t) t->tm_year = RTC_DateStruct.Year + 100; // STM32 year is two decimals only (so base is currently 2000), gtm is based on number of years since 1900 t->tm_mon = RTC_DateStruct.Month - 1; t->tm_mday = RTC_DateStruct.Date; + + // SSR counts down over PREDIV_S+1 steps, 1/256 s each with the usual setup + uint32_t fraction = RTC_TimeStruct.SecondFraction; + if (fraction == 0 || RTC_TimeStruct.SubSeconds > fraction) return 0; + return (uint16_t)(((fraction - RTC_TimeStruct.SubSeconds) * 1000) / (fraction + 1)); +} + +void rtcGetTime(struct gtm * t) +{ + rtcGetTimeMs(t); } +#if defined(RTC_CALR_CALM) && !defined(BOOT) + +// DR0 is left alone, legacy code uses it for shutdown/soft reset requests +#define RTC_CALIB_BKP_MAGIC_REG RTC_BKP_DR1 +#define RTC_CALIB_BKP_REF_REG RTC_BKP_DR2 +#define RTC_CALIB_BKP_MAGIC 0x52544301 + +int32_t rtcGetCalibration() +{ + uint32_t calr = READ_REG(rtc.Instance->CALR); + int32_t units = -(int32_t)(calr & RTC_CALR_CALM); + if (calr & RTC_CALR_CALP) units += 512; + return units; +} + +void rtcSetCalibration(int32_t units) +{ + if (units > RTC_CALIB_UNIT_MAX) units = RTC_CALIB_UNIT_MAX; + if (units < RTC_CALIB_UNIT_MIN) units = RTC_CALIB_UNIT_MIN; + + // CALP adds 512 pulses, CALM removes up to 511 + uint32_t plus = (units > 0) ? RTC_SMOOTHCALIB_PLUSPULSES_SET + : RTC_SMOOTHCALIB_PLUSPULSES_RESET; + uint32_t minus = (units > 0) ? (512 - units) : -units; + + HAL_RTCEx_SetSmoothCalib(&rtc, RTC_SMOOTHCALIB_PERIOD_32SEC, plus, minus); +} + +gtime_t rtcGetCalibrationRef() +{ + if (HAL_RTCEx_BKUPRead(&rtc, RTC_CALIB_BKP_MAGIC_REG) != RTC_CALIB_BKP_MAGIC) + return 0; + return (gtime_t)HAL_RTCEx_BKUPRead(&rtc, RTC_CALIB_BKP_REF_REG); +} + +void rtcSetCalibrationRef(gtime_t t) +{ + HAL_RTCEx_BKUPWrite(&rtc, RTC_CALIB_BKP_REF_REG, (uint32_t)t); + HAL_RTCEx_BKUPWrite(&rtc, RTC_CALIB_BKP_MAGIC_REG, RTC_CALIB_BKP_MAGIC); +} + +void rtcClearCalibrationRef() +{ + HAL_RTCEx_BKUPWrite(&rtc, RTC_CALIB_BKP_MAGIC_REG, 0); + HAL_RTCEx_BKUPWrite(&rtc, RTC_CALIB_BKP_REF_REG, 0); +} + +#else // no smooth calibration hardware + +int32_t rtcGetCalibration() { return 0; } +void rtcSetCalibration(int32_t units) { (void)units; } +gtime_t rtcGetCalibrationRef() { return 0; } +void rtcSetCalibrationRef(gtime_t t) { (void)t; } +void rtcClearCalibrationRef() {} + +#endif + void rtcInit() { rtc.Instance = RTC; diff --git a/radio/src/targets/simu/simulib.cpp b/radio/src/targets/simu/simulib.cpp index 3a7aa0bc742..1db8b815d5a 100644 --- a/radio/src/targets/simu/simulib.cpp +++ b/radio/src/targets/simu/simulib.cpp @@ -447,10 +447,22 @@ void rtcGetTime(struct gtm * t) { } -void rtcSetTime(const struct gtm * t) +uint16_t rtcGetTimeMs(struct gtm * t) { + rtcGetTime(t); + return 0; } +void rtcDriverSetTime(const struct gtm * t) +{ +} + +int32_t rtcGetCalibration() { return 0; } +void rtcSetCalibration(int32_t units) { (void)units; } +gtime_t rtcGetCalibrationRef() { return 0; } +void rtcSetCalibrationRef(gtime_t t) { (void)t; } +void rtcClearCalibrationRef() {} + #if defined(PCBTARANIS) void sdPoll10ms() {} #endif diff --git a/tools/set-radio-time.py b/tools/set-radio-time.py new file mode 100755 index 00000000000..4238abb22ce --- /dev/null +++ b/tools/set-radio-time.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +"""Set an EdgeTX radio's clock from the host, over the CLI serial port. + +The radio must be in USB Serial (VCP) mode, running a build with CLI enabled. + +The command body is sent ahead of time and only the closing newline is timed, so +what has to land on the second boundary is a single byte rather than a whole +line. The radio treats a CLI set as host timed and skips the margin it allows +for someone turning the dials by hand, which leaves the drift measurement good +to the USB round trip, a few milliseconds. + +That accuracy is in the measurement, not in the resulting clock: the RTC is +written in whole seconds and keeps whatever sub-second phase it already had, so +the radio can still read up to a second away from the host afterwards. + +Every setting rewrites the drift reference, and two settings less than a day +apart are discarded, so do not put this on a short schedule or the radio never +learns its crystal error. Every day or two is enough for it to converge. +""" + +import argparse +import sys +import time + +try: + import serial + from serial.tools import list_ports +except ImportError: + sys.exit("pyserial is required: python3 -m pip install pyserial") + + +# radio/src/targets/common/arm/stm32/usbd_desc.c, fixed for the Windows ST driver +USB_CDC_VID = 0x0483 +USB_CDC_PID = 0x5740 + + +def _device(port): + """macOS lists both tty and cu for one device; only cu opens without DCD.""" + dev = port.device + if sys.platform == "darwin" and dev.startswith("/dev/tty."): + return "/dev/cu." + dev[len("/dev/tty."):] + return dev + + +def _looks_like_cdc(dev): + name = dev.upper() + return name.startswith("COM") or "TTYACM" in name or "USBMODEM" in name + + +def list_candidates(): + """Best first: exact USB ids, then self-named, then any plausible CDC port.""" + exact, named, generic = [], [], [] + for port in list_ports.comports(): + dev = _device(port) + if port.vid == USB_CDC_VID and port.pid == USB_CDC_PID: + bucket = exact + else: + blob = " ".join(x for x in (port.description, port.manufacturer, + port.product) if x).lower() + if "edgetx" in blob or "opentx" in blob: + bucket = named + elif _looks_like_cdc(dev): + bucket = generic + else: + continue + if dev not in bucket: + bucket.append(dev) + return exact, named, generic + + +def find_port(): + exact, named, generic = list_candidates() + if len(exact) > 1: + sys.exit("several radios found, pick one with --port:\n " + + "\n ".join(exact)) + for bucket in (exact, named, generic): + if bucket: + return bucket[0] + return None + + +def drain(ser, seconds): + out = bytearray() + end = time.time() + seconds + while time.time() < end: + chunk = ser.read(256) + if chunk: + out += chunk + else: + time.sleep(0.01) + return out.decode("ascii", "replace") + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("-p", "--port", + help="serial port, e.g. /dev/cu.usbmodem1234 on macOS, " + "/dev/ttyACM0 on Linux, COM4 on Windows " + "(default: autodetect)") + ap.add_argument("--list", action="store_true", + help="list the ports autodetection can see, then exit") + ap.add_argument("-u", "--utc", action="store_true", + help="send UTC instead of host local time") + ap.add_argument("-l", "--lead", type=float, default=0.004, + help="seconds to send the newline early, to cover USB latency " + "(default: 0.004)") + ap.add_argument("-n", "--dry-run", action="store_true", + help="show the command without opening the port") + ap.add_argument("-c", "--check", action="store_true", + help="run 'p rtc' afterwards and show the result") + args = ap.parse_args() + + if args.list: + for label, bucket in zip(("usb id match", "named", "possible"), + list_candidates()): + for dev in bucket: + print("%-12s %s" % (label, dev)) + return 0 + + port = args.port or find_port() + if not port and not args.dry_run: + sys.exit("no radio found; is it plugged in and set to USB Serial (VCP)?") + + # aim at the start of a whole second, far enough out to get the body there + target = int(time.time()) + 2 + tm = time.gmtime(target) if args.utc else time.localtime(target) + # the fractional second says the newline is meant to land on the boundary, + # which lets the radio skip the margin it allows for a hand set + body = "set rtc %04d %02d %02d %02d %02d %02d.000" % ( + tm.tm_year, tm.tm_mon, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec) + + if args.dry_run: + print("port: %s" % (port or "none found")) + print("would send: %s" % body) + return 0 + + with serial.Serial(port, 115200, timeout=0.05) as ser: + ser.write(b"\n") # wake the prompt + time.sleep(0.25) + ser.reset_input_buffer() + + ser.write(body.encode()) # everything except the newline + ser.flush() + + while time.time() < target - args.lead: + time.sleep(0.0005) + ser.write(b"\n") + ser.flush() + + print("%s (%s)" % (body, "UTC" if args.utc else "local")) + print(drain(ser, 1.0).strip()) + + if args.check: + ser.reset_input_buffer() + ser.write(b"p rtc\n") + ser.flush() + print(drain(ser, 1.0).strip()) + + return 0 + + +if __name__ == "__main__": + sys.exit(main())