feat(radio): Automatic clock drift compensation (#7763)
Co-authored-by: 3djc <3djc@gh.com> Co-authored-by: Peter Feerick <5500713+pfeerick@users.noreply.github.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
3djc
Peter Feerick
Claude Sonnet 5
parent
6fe19b2bfd
commit
ea44383da7
+85
-4
@@ -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;
|
||||
@@ -1500,8 +1567,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")) {
|
||||
|
||||
+179
-4
@@ -22,8 +22,6 @@
|
||||
#include <limits.h>
|
||||
#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<int64_t>(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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -31,7 +31,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
|
||||
|
||||
@@ -48,7 +48,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;
|
||||
@@ -62,6 +64,16 @@ 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(LSE_DRIVE_STRENGTH)
|
||||
@@ -94,6 +106,63 @@ static void rtcSetLSEDriveStrength()
|
||||
}
|
||||
#endif
|
||||
|
||||
#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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+165
@@ -0,0 +1,165 @@
|
||||
#!/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.
|
||||
|
||||
Right after a run the hardware RTC matches the host to within a few
|
||||
milliseconds, since writing the time restarts its sub-second counter. The time
|
||||
the radio displays comes from a separate software clock, seeded from the RTC in
|
||||
whole seconds at boot, so that one can lag the RTC by up to a second.
|
||||
|
||||
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())
|
||||
Reference in New Issue
Block a user