From b73934d13b461bed0b9b519159dfebe52ec00b56 Mon Sep 17 00:00:00 2001 From: Peter Lewis Date: Sat, 4 Jul 2026 09:57:27 +0100 Subject: [PATCH 01/37] Add $PMTXTS per-PPS host timestamping (opt-in) With `pps = on` in config.txt, emit one proprietary NMEA sentence per PPS edge over the existing CDC stream, carrying the sub-second phase captured at the edge plus calibration / holdover / die-temperature telemetry. Time-critical fields are snapshotted in the PPS ISR; formatting and CDC submission happen in the main loop, serialised against the NMEA passthrough. With no enumerated host the record is dropped before any formatting. mk4-time/Core/Src/main.c: capturePPS(), emitPPSTimestamp(), measure_temp(), a `pps` config key and one main-loop hook. qspi/config.txt documents the key. Co-Authored-By: Claude Opus 4.8 --- mk4-time/Core/Src/main.c | 137 +++++++++++++++++++++++++++++++++++++++ qspi/config.txt | 4 ++ 2 files changed, 141 insertions(+) diff --git a/mk4-time/Core/Src/main.c b/mk4-time/Core/Src/main.c index 2a35026..e6dd655 100644 --- a/mk4-time/Core/Src/main.c +++ b/mk4-time/Core/Src/main.c @@ -177,6 +177,25 @@ uint8_t requestMode = 255; uint8_t nmea_cdc_level=0; int debug_rtc_val = 0; +// --- PPS host timestamping ---------------------------------------------------------------- +// Optional: emit one proprietary NMEA sentence ($PMTXTS) per PPS edge over the CDC port so a +// host can measure the clock's timing stability (phase jitter, oscillator drift, holdover) — +// things the plain NMEA stream cannot convey. Enabled by config "pps = on". Capture happens in +// the PPS ISR (cheap, just snapshots); the sentence is formatted + sent from the main loop. +volatile uint8_t pps_ts_enabled = 0; +volatile _Bool pps_record_pending = 0; +int16_t die_temp_c = 0; // latest STM32 die temperature (°C), a proxy for the crystal temperature +volatile struct { + uint32_t seq; // increments every PPS edge (32-bit: no practical wrap; host detects gaps) + uint32_t systick; // SysTick->VAL at the edge, captured BEFORE the reload (down-counter) + uint16_t subms; // 0..999 modelled ms-of-second at the edge, BEFORE the counters reset + uint32_t epoch; // currentTime at the edge (Unix seconds, UTC) + int32_t calerr; // debug_rtc_val: signed LSE cycle error over CAL_PERIOD s (=> ppm on host) + uint32_t sincecal; // seconds since last successful RTC calibration (holdover age) + int16_t temp; // die temperature (°C) — for host-side ppm-vs-temperature characterisation + uint8_t flags; // bit0 data_valid, bit1 had_pps, bit2 rtc_good +} pps_cap; + #define CHECK_CONFIG_MTIME struct { @@ -1031,6 +1050,10 @@ void parseConfigString(char *key, char *value) { nmea_cdc_level = NMEA_RMC; } else nmea_cdc_level = NMEA_ALL; + } else if (strcasecmp(key, "pps") == 0) { + + pps_ts_enabled = truthy(value); // emit a $PMTXTS timing sentence on each PPS edge + } else if (key[0]=='B' && key[1]=='S' && key[3]==0) { //BS1, BS2, etc if (!key[2] || key[2]<'1' || key[2]>'0'+sizeof(brightnessCurve)/sizeof(brightnessCurve[0])) return; @@ -1247,9 +1270,25 @@ void calibrateRTC(void){ void EXTI9_5_IRQHandler(void){__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_7);} +// Snapshot the timing state at the instant of the PPS edge. MUST run before SysTick->VAL is +// reloaded and before millisec/centisec/decisec are zeroed, so it captures the phase error +// between the firmware's modelled second and the true GPS edge. +#define capturePPS() do { \ + pps_cap.systick = SysTick->VAL; \ + pps_cap.subms = (uint16_t)decisec*100 + (uint16_t)centisec*10 + millisec; \ + pps_cap.epoch = (uint32_t)currentTime; \ + pps_cap.calerr = debug_rtc_val; \ + pps_cap.sincecal = (uint32_t)currentTime - (uint32_t)rtc_last_calibration; \ + pps_cap.temp = die_temp_c; \ + pps_cap.flags = (data_valid?1:0) | (had_pps?2:0) | (rtc_good?4:0); \ + pps_cap.seq++; \ + pps_record_pending = 1; \ + } while(0) + // PPS rising edge void PPS(void) { + capturePPS(); SysTick->VAL = SysTick->LOAD; buffer_c[3].low=cLut[0]; @@ -1278,6 +1317,7 @@ void PPS(void) void PPS_NoUpdate(void) { + capturePPS(); SysTick->VAL = SysTick->LOAD; triggerPendSV(); @@ -1297,6 +1337,7 @@ void PPS_NoUpdate(void) void PPS_Countdown(void) { + capturePPS(); SysTick->VAL = SysTick->LOAD; buffer_c[3].low=cLut[9]; @@ -1333,6 +1374,65 @@ void PPS_Init(void){ SetPPS( &PPS ); } +// usbd_cdc_if.h isn't pulled into main.c; forward-declare the one symbol we need. +extern uint8_t CDC_Copy_Transmit(uint8_t* buf, uint16_t Len); +extern USBD_HandleTypeDef hUsbDeviceFS; + +// Format + send one $PMTXTS sentence from the values captured at the last PPS edge. +// Runs in the main loop (snprintf is fine here, never in the ISR). Clears pps_record_pending +// on a successful send and for any undeliverable record (no host, formatting failure) — a +// fresh record arrives on the next edge, so only USBD_BUSY is worth retrying. +// Sentence: $PMTXTS,,,,,,,,,*CC +// subms+(load-systick)/(load+1) = modelled sub-second position at the edge (phase error); +// ppm = calerr * 1e6 / (32768 * CAL_PERIOD) [CAL_PERIOD=63]; temp = die °C; +// flags: b0 valid, b1 pps, b2 rtc. +static uint8_t emitPPSTimestamp(void){ + // With no enumerated host (e.g. charger-only power) CDC can never accept the sentence; + // drop the record before doing any formatting work, otherwise the pending flag would + // re-run the whole format-and-fail cycle every main-loop pass until a host appears. + if (hUsbDeviceFS.dev_state != USBD_STATE_CONFIGURED) { + pps_record_pending = 0; + return USBD_FAIL; + } + + __disable_irq(); // atomic snapshot of the ISR-written capture + uint32_t snap_seq = pps_cap.seq; + uint32_t st = pps_cap.systick; + uint16_t subms = pps_cap.subms; + uint32_t epoch = pps_cap.epoch; + int32_t calerr = pps_cap.calerr; + uint32_t sincecal = pps_cap.sincecal; + int16_t temp = pps_cap.temp; + uint8_t flags = pps_cap.flags; + __enable_irq(); + + uint32_t load = SysTick->LOAD; // constant; sent so the host needn't assume core clock + + char body[96]; // everything between '$' and '*' + int n = snprintf(body, sizeof body, "PMTXTS,%lu,%lu,%u,%lu,%lu,%ld,%lu,%d,%X", + (unsigned long)snap_seq, (unsigned long)epoch, (unsigned)subms, + (unsigned long)st, (unsigned long)load, (long)calerr, + (unsigned long)sincecal, (int)temp, (unsigned)flags); + if (n < 0 || n >= (int)sizeof body) { pps_record_pending = 0; return USBD_FAIL; } + + uint8_t cks = 0; // standard NMEA XOR checksum + for (int i = 0; i < n; i++) cks ^= (uint8_t)body[i]; + + char line[NMEA_BUF_SIZE]; // must fit the CDC txbuf[NMEA_BUF_SIZE] downstream + int m = snprintf(line, sizeof line, "$%s*%02X\r\n", body, (unsigned)cks); + if (m < 0 || m >= (int)sizeof line) { pps_record_pending = 0; return USBD_FAIL; } + + // The CDC IN endpoint is shared with the ISR NMEA passthrough; serialise the (tiny) submit, + // and clear the pending flag only if no fresh PPS edge arrived since the snapshot (so a + // record captured mid-send isn't silently dropped). FAIL also clears: the record is + // undeliverable (USB de-inited under us), unlike BUSY where the host may drain the FIFO. + __disable_irq(); + uint8_t r = CDC_Copy_Transmit((uint8_t*)line, (uint16_t)m); + if (r != USBD_BUSY && pps_cap.seq == snap_seq) pps_record_pending = 0; + __enable_irq(); + return r; +} + #define timetick() \ millisec++; \ if (millisec>=10) { \ @@ -1545,6 +1645,34 @@ void measure_vbat(void){ vbat = (float)adc *0.0024102564102564104;//3*3.29/4095.0; } +// Read the STM32 internal die-temperature sensor on hadc3 (shared with VBAT) into die_temp_c. +// The die sits slightly above ambient on this low-power board, but it tracks the crystal well +// enough to characterise the oscillator's temperature dependence. +void measure_temp(void){ + ADC_ChannelConfTypeDef s = {0}; + s.Rank = ADC_REGULAR_RANK_1; + s.SamplingTime = ADC_SAMPLETIME_640CYCLES_5; // temp sensor needs a long sampling time + s.SingleDiff = ADC_SINGLE_ENDED; + s.OffsetNumber = ADC_OFFSET_NONE; + s.Offset = 0; + + s.Channel = ADC_CHANNEL_TEMPSENSOR; + HAL_ADC_ConfigChannel(&hadc3, &s); + ADC123_COMMON->CCR |= ADC_CCR_TSEN; + HAL_Delay(1); // tSTART for the temperature sensor (~120 us) + HAL_ADC_Start(&hadc3); + HAL_ADC_PollForConversion(&hadc3, 10); + uint16_t raw = HAL_ADC_GetValue(&hadc3); + ADC123_COMMON->CCR &= ~ADC_CCR_TSEN; + + // Factory-calibrated conversion (TS_CAL1/TS_CAL2 in flash). VREF taken as 3300 mV; absolute + // accuracy isn't critical — the curve is fitted against GPS-measured ppm, not trusted raw. + die_temp_c = (int16_t)__HAL_ADC_CALC_TEMPERATURE(3300, raw, ADC_RESOLUTION_12B); + + s.Channel = ADC_CHANNEL_VBAT; // restore so measure_vbat() keeps working + HAL_ADC_ConfigChannel(&hadc3, &s); +} + uint8_t f_getzcmp(FIL* fp, char * str){ unsigned int rc; char * a = str; @@ -2124,6 +2252,15 @@ int main(void) monitor_vbus(); + if (pps_ts_enabled) { + static uint32_t last_temp_read = 0; + if ((uint32_t)currentTime - last_temp_read >= 4) { // refresh die temp every ~4 s + last_temp_read = (uint32_t)currentTime; + measure_temp(); + } + if (pps_record_pending) emitPPSTimestamp(); // emit clears pending itself on success + } + if (displayMode == MODE_VBAT) measure_vbat(); diff --git a/qspi/config.txt b/qspi/config.txt index 3606e81..28eb75d 100644 --- a/qspi/config.txt +++ b/qspi/config.txt @@ -100,3 +100,7 @@ mode_satview = 0 # show coin cell voltage mode_vbat = 0 + +# output a $PMTXTS timing sentence over USB serial on each GPS PPS pulse, carrying the +# sub-millisecond phase measurement made at the edge, for host-side jitter/drift analysis. +pps = off From 5ba7320d3ed7280a38ff9fe61c334e2efaa20706 Mon Sep 17 00:00:00 2001 From: Peter Lewis Date: Tue, 7 Jul 2026 22:29:59 +0100 Subject: [PATCH 02/37] SOF-correlation timestamping: latch (USB frame, DWT) for sub-ms PPS over USB MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Experimental attack on the ~6ms host-driven USB read jitter that swamps the clock's true ~180us precision — without a hardware PPS wire. The OTG_FS core rules out mitxela's last-microsecond FIFO injection, so instead we let the host anchor each PPS to a USB Start-Of-Frame, whose own arrival time the host can read in hardware (macOS IOKit GetBusFrameNumberWithTime). - Enable DWT->CYCCNT (free-running 12.5ns core-cycle counter) as the timebase both the PPS edge and each SOF are latched against. Unaffected by tempcomp SysTick steering (counts raw core clock), so it's a stable monotonic ruler. - Enable the USB SOF interrupt; PCD_SOFCallback latches (DWT, 11-bit frame from DSTS[13:8]) every 1ms, as its first act for minimal latency. - capturePPS() latches DWT at the edge; emitPPSTimestamp() appends the tail ,dwt_pps,sof_frame,dwt_sof to $PMTXTS (read under __disable_irq, no torn read). - Bump NMEA_BUF_SIZE 90->128 for the longer sentence. Host places the edge at hostTime(sof_frame) + (dwt_pps-dwt_sof)/f_dwt, immune to delivery lateness; dwt_pps deltas self-calibrate f_dwt (no core-clock assumption). Backward-compatible: lenient $PMTXTS parsers read the first 9 fields. Bench-only; +1kHz SOF ISR (~0.05% CPU) is the cost to watch. Builds clean (0 errors). Co-Authored-By: Claude Opus 4.8 --- mk4-time/Core/Inc/main.h | 5 ++-- mk4-time/Core/Src/main.c | 32 ++++++++++++++++++++++---- mk4-time/USB_DEVICE/Target/usbd_conf.c | 16 ++++++++++++- 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/mk4-time/Core/Inc/main.h b/mk4-time/Core/Inc/main.h index 539aba0..18989e5 100644 --- a/mk4-time/Core/Inc/main.h +++ b/mk4-time/Core/Inc/main.h @@ -125,8 +125,9 @@ extern _Bool resendDate; #define DAC_BUFFER_SIZE 20 #define ADC_BUFFER_SIZE 50 -// NMEA 0183 messages have a max length of 82 characters -#define NMEA_BUF_SIZE 90 +// NMEA 0183 messages have a max length of 82 characters; the extended $PMTXTS (with the SOF- +// correlation tail: dwt_pps, sof_frame, dwt_sof) runs ~110, so this sizes the tx/rx buffers for it. +#define NMEA_BUF_SIZE 128 #define CMD_LOAD_TEXT 0x90 #define CMD_SET_FREQUENCY 0x91 diff --git a/mk4-time/Core/Src/main.c b/mk4-time/Core/Src/main.c index e6dd655..0877af3 100644 --- a/mk4-time/Core/Src/main.c +++ b/mk4-time/Core/Src/main.c @@ -194,8 +194,17 @@ volatile struct { uint32_t sincecal; // seconds since last successful RTC calibration (holdover age) int16_t temp; // die temperature (°C) — for host-side ppm-vs-temperature characterisation uint8_t flags; // bit0 data_valid, bit1 had_pps, bit2 rtc_good + uint32_t dwt_pps; // DWT->CYCCNT (free-running 12.5ns) latched at the edge — SOF-correlation timebase } pps_cap; +// --- SOF correlation (experimental: sub-ms USB timestamping without a hardware PPS wire) ----------- +// Latched by the USB SOF interrupt (PCD_SOFCallback, usbd_conf.c) every 1ms: the 11-bit USB frame +// number and the DWT count at that Start-Of-Frame. Emitted in $PMTXTS so a host — which can read each +// USB frame's own arrival time in hardware — can place the PPS edge on its clock via the frame, immune +// to the ~6ms host-driven read jitter. Written in the SOF ISR, read at emit under __disable_irq. +volatile uint32_t pps_sof_dwt = 0; // DWT->CYCCNT at the most recent SOF +volatile uint16_t pps_sof_frame = 0; // USB 11-bit frame number at that SOF (matches host frame mod 2048) + #define CHECK_CONFIG_MTIME struct { @@ -1274,6 +1283,7 @@ void EXTI9_5_IRQHandler(void){__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_7);} // reloaded and before millisec/centisec/decisec are zeroed, so it captures the phase error // between the firmware's modelled second and the true GPS edge. #define capturePPS() do { \ + pps_cap.dwt_pps = DWT->CYCCNT; \ pps_cap.systick = SysTick->VAL; \ pps_cap.subms = (uint16_t)decisec*100 + (uint16_t)centisec*10 + millisec; \ pps_cap.epoch = (uint32_t)currentTime; \ @@ -1382,10 +1392,14 @@ extern USBD_HandleTypeDef hUsbDeviceFS; // Runs in the main loop (snprintf is fine here, never in the ISR). Clears pps_record_pending // on a successful send and for any undeliverable record (no host, formatting failure) — a // fresh record arrives on the next edge, so only USBD_BUSY is worth retrying. -// Sentence: $PMTXTS,,,,,,,,,*CC +// Sentence: $PMTXTS,,,,,,,,,,,,*CC // subms+(load-systick)/(load+1) = modelled sub-second position at the edge (phase error); // ppm = calerr * 1e6 / (32768 * CAL_PERIOD) [CAL_PERIOD=63]; temp = die °C; // flags: b0 valid, b1 pps, b2 rtc. +// SOF-correlation tail (experimental): dwt_pps = DWT cycle count at the PPS edge; sof_frame = USB +// 11-bit frame number of the most recent SOF; dwt_sof = DWT at that SOF. A host that knows each USB +// frame's own arrival time places the edge as hostTime(sof_frame) + (dwt_pps-dwt_sof)/f_dwt, immune +// to USB read jitter. dwt_pps deltas (~80e6/s) self-calibrate f_dwt, so no core-clock assumption. static uint8_t emitPPSTimestamp(void){ // With no enumerated host (e.g. charger-only power) CDC can never accept the sentence; // drop the record before doing any formatting work, otherwise the pending flag would @@ -1404,15 +1418,19 @@ static uint8_t emitPPSTimestamp(void){ uint32_t sincecal = pps_cap.sincecal; int16_t temp = pps_cap.temp; uint8_t flags = pps_cap.flags; + uint32_t dwt_pps = pps_cap.dwt_pps; // DWT at the PPS edge (SOF-correlation timebase) + uint32_t sof_dwt = pps_sof_dwt; // DWT at the most recent SOF ... + uint16_t sof_fr = pps_sof_frame; // ... and that SOF's 11-bit USB frame number __enable_irq(); uint32_t load = SysTick->LOAD; // constant; sent so the host needn't assume core clock - char body[96]; // everything between '$' and '*' - int n = snprintf(body, sizeof body, "PMTXTS,%lu,%lu,%u,%lu,%lu,%ld,%lu,%d,%X", + char body[128]; // everything between '$' and '*' + int n = snprintf(body, sizeof body, "PMTXTS,%lu,%lu,%u,%lu,%lu,%ld,%lu,%d,%X,%lu,%u,%lu", (unsigned long)snap_seq, (unsigned long)epoch, (unsigned)subms, (unsigned long)st, (unsigned long)load, (long)calerr, - (unsigned long)sincecal, (int)temp, (unsigned)flags); + (unsigned long)sincecal, (int)temp, (unsigned)flags, + (unsigned long)dwt_pps, (unsigned)sof_fr, (unsigned long)sof_dwt); if (n < 0 || n >= (int)sizeof body) { pps_record_pending = 0; return USBD_FAIL; } uint8_t cks = 0; // standard NMEA XOR checksum @@ -2017,6 +2035,12 @@ int main(void) /* USER CODE BEGIN SysInit */ + // Enable the DWT cycle counter (free-running at the 80 MHz core clock, 12.5 ns/tick, wraps ~53.7 s): + // the monotonic timebase the PPS edge and each USB SOF are both latched against for host correlation. + CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; + DWT->CYCCNT = 0; + DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; + buffer_c[0].high=0b11001110; buffer_c[1].high=0b11001101; buffer_c[2].high=0b11001011; diff --git a/mk4-time/USB_DEVICE/Target/usbd_conf.c b/mk4-time/USB_DEVICE/Target/usbd_conf.c index ab89fbb..33f8378 100644 --- a/mk4-time/USB_DEVICE/Target/usbd_conf.c +++ b/mk4-time/USB_DEVICE/Target/usbd_conf.c @@ -204,6 +204,20 @@ static void PCD_SOFCallback(PCD_HandleTypeDef *hpcd) void HAL_PCD_SOFCallback(PCD_HandleTypeDef *hpcd) #endif /* USE_HAL_PCD_REGISTER_CALLBACKS */ { + /* USER CODE BEGIN SOF_latch */ + /* SOF-correlation experiment: latch the DWT cycle count and the USB 11-bit frame number at the + instant of this Start-Of-Frame, as early as possible for minimal latency. main.c emits them in + $PMTXTS so the host can anchor the PPS edge to a USB frame (whose host-side arrival time it can + read in hardware), sidestepping the ~6 ms host-driven read jitter. */ + extern volatile uint32_t pps_sof_dwt; + extern volatile uint16_t pps_sof_frame; + pps_sof_dwt = DWT->CYCCNT; + { + /* OTG device register block = global base + USB_OTG_DEVICE_BASE; frame number = DSTS[13:8]. */ + USB_OTG_DeviceTypeDef *dev = (USB_OTG_DeviceTypeDef *)((uint32_t)hpcd->Instance + USB_OTG_DEVICE_BASE); + pps_sof_frame = (uint16_t)((dev->DSTS >> 8) & 0x7FFU); + } + /* USER CODE END SOF_latch */ USBD_LL_SOF((USBD_HandleTypeDef*)hpcd->pData); } @@ -361,7 +375,7 @@ USBD_StatusTypeDef USBD_LL_Init(USBD_HandleTypeDef *pdev) hpcd_USB_OTG_FS.Init.dev_endpoints = 6; hpcd_USB_OTG_FS.Init.speed = PCD_SPEED_FULL; hpcd_USB_OTG_FS.Init.phy_itface = PCD_PHY_EMBEDDED; - hpcd_USB_OTG_FS.Init.Sof_enable = DISABLE; + hpcd_USB_OTG_FS.Init.Sof_enable = ENABLE; /* SOF-correlation experiment: 1 kHz SOF IRQ latches (frame, DWT) */ hpcd_USB_OTG_FS.Init.low_power_enable = DISABLE; hpcd_USB_OTG_FS.Init.lpm_enable = DISABLE; hpcd_USB_OTG_FS.Init.battery_charging_enable = DISABLE; From 1fdbf337ebb4a99f9b9983f569dd759285aa03db Mon Sep 17 00:00:00 2001 From: Peter Lewis Date: Tue, 7 Jul 2026 23:07:06 +0100 Subject: [PATCH 03/37] =?UTF-8?q?SOF=20timestamping:=20audit=20fixes=20?= =?UTF-8?q?=E2=80=94=20valid-gated=20tail,=20latch=20only=20when=20pps=3Do?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adversarial audit of the SOF-correlation change surfaced two real issues: - Stale first anchor: pps_sof_dwt/frame init to 0, so the first PPS emitted after enumeration (or with pps just toggled on, or on the emulator which has no USB SOF) carried a (0,0) anchor → a ~53s/−729ms host error on that record. Now a pps_sof_valid flag gates the tail: emit the plain 9-field sentence until a real SOF has latched. Backward-compatible; also makes the emulator correct. - SOF work ran unconditionally. Gate the latch on pps_ts_enabled (the SOF IRQ still fires — cheap, below the priority-0 display DMA — but does nothing when the feature is off); clear valid when off so a re-enable can't reuse a stale anchor. config.txt documents that pps=on enables the SOF tail. Builds clean (0 errors). Refuted findings (torn read, 16-bit atomicity, buffer truncation, display preemption, scope) left as-is. Co-Authored-By: Claude Opus 4.8 --- mk4-time/Core/Src/main.c | 28 ++++++++++++++++++-------- mk4-time/USB_DEVICE/Target/usbd_conf.c | 13 +++++++++--- qspi/config.txt | 4 ++++ 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/mk4-time/Core/Src/main.c b/mk4-time/Core/Src/main.c index 0877af3..a408076 100644 --- a/mk4-time/Core/Src/main.c +++ b/mk4-time/Core/Src/main.c @@ -198,12 +198,16 @@ volatile struct { } pps_cap; // --- SOF correlation (experimental: sub-ms USB timestamping without a hardware PPS wire) ----------- -// Latched by the USB SOF interrupt (PCD_SOFCallback, usbd_conf.c) every 1ms: the 11-bit USB frame -// number and the DWT count at that Start-Of-Frame. Emitted in $PMTXTS so a host — which can read each -// USB frame's own arrival time in hardware — can place the PPS edge on its clock via the frame, immune -// to the ~6ms host-driven read jitter. Written in the SOF ISR, read at emit under __disable_irq. +// Latched by the USB SOF interrupt (PCD_SOFCallback, usbd_conf.c) every 1ms WHEN pps_ts_enabled: the +// 11-bit USB frame number and the DWT count at that Start-Of-Frame. Emitted in $PMTXTS so a host — +// which can read each USB frame's own arrival time in hardware — can place the PPS edge on its clock +// via the frame, immune to the ~6ms host-driven read jitter. Written in the SOF ISR, read at emit +// under __disable_irq. pps_sof_valid gates emission of the tail: it is 0 until the first SOF has +// latched a real anchor (so the first PPS after enumeration, or with pps just toggled on, or the +// emulator which has no SOF, emits the plain 9-field sentence rather than a stale (0,0) anchor). volatile uint32_t pps_sof_dwt = 0; // DWT->CYCCNT at the most recent SOF volatile uint16_t pps_sof_frame = 0; // USB 11-bit frame number at that SOF (matches host frame mod 2048) +volatile uint8_t pps_sof_valid = 0; // 1 once a real SOF anchor has been latched; 0 = emit 9-field only #define CHECK_CONFIG_MTIME @@ -1420,18 +1424,26 @@ static uint8_t emitPPSTimestamp(void){ uint8_t flags = pps_cap.flags; uint32_t dwt_pps = pps_cap.dwt_pps; // DWT at the PPS edge (SOF-correlation timebase) uint32_t sof_dwt = pps_sof_dwt; // DWT at the most recent SOF ... - uint16_t sof_fr = pps_sof_frame; // ... and that SOF's 11-bit USB frame number + uint16_t sof_fr = pps_sof_frame; // ... and that SOF's 11-bit USB frame number ... + uint8_t sof_ok = pps_sof_valid; // ... valid only once a real SOF has latched an anchor __enable_irq(); uint32_t load = SysTick->LOAD; // constant; sent so the host needn't assume core clock char body[128]; // everything between '$' and '*' - int n = snprintf(body, sizeof body, "PMTXTS,%lu,%lu,%u,%lu,%lu,%ld,%lu,%d,%X,%lu,%u,%lu", + int n = snprintf(body, sizeof body, "PMTXTS,%lu,%lu,%u,%lu,%lu,%ld,%lu,%d,%X", (unsigned long)snap_seq, (unsigned long)epoch, (unsigned)subms, (unsigned long)st, (unsigned long)load, (long)calerr, - (unsigned long)sincecal, (int)temp, (unsigned)flags, - (unsigned long)dwt_pps, (unsigned)sof_fr, (unsigned long)sof_dwt); + (unsigned long)sincecal, (int)temp, (unsigned)flags); if (n < 0 || n >= (int)sizeof body) { pps_record_pending = 0; return USBD_FAIL; } + // Append the SOF-correlation tail only when a real anchor exists — never a stale (0,0). Absent tail = + // the plain sentence a 9-field parser expects (also the emulator's output, which has no USB SOF). + if (sof_ok) { + int t = snprintf(body + n, sizeof body - n, ",%lu,%u,%lu", + (unsigned long)dwt_pps, (unsigned)sof_fr, (unsigned long)sof_dwt); + if (t < 0 || t >= (int)(sizeof body - n)) { pps_record_pending = 0; return USBD_FAIL; } + n += t; + } uint8_t cks = 0; // standard NMEA XOR checksum for (int i = 0; i < n; i++) cks ^= (uint8_t)body[i]; diff --git a/mk4-time/USB_DEVICE/Target/usbd_conf.c b/mk4-time/USB_DEVICE/Target/usbd_conf.c index 33f8378..bc8ce5a 100644 --- a/mk4-time/USB_DEVICE/Target/usbd_conf.c +++ b/mk4-time/USB_DEVICE/Target/usbd_conf.c @@ -208,14 +208,21 @@ void HAL_PCD_SOFCallback(PCD_HandleTypeDef *hpcd) /* SOF-correlation experiment: latch the DWT cycle count and the USB 11-bit frame number at the instant of this Start-Of-Frame, as early as possible for minimal latency. main.c emits them in $PMTXTS so the host can anchor the PPS edge to a USB frame (whose host-side arrival time it can - read in hardware), sidestepping the ~6 ms host-driven read jitter. */ + read in hardware), sidestepping the ~6 ms host-driven read jitter. Only latch when the timestamp + feature is on (SOF still fires — the interrupt is cheap, well below the priority-0 display DMA — + but does no work otherwise); pps_sof_valid tracks whether the anchor is real so a stale (0,0) is + never emitted (and resets to 0 whenever the feature is off, so a later re-enable can't reuse it). */ + extern volatile uint8_t pps_ts_enabled, pps_sof_valid; extern volatile uint32_t pps_sof_dwt; extern volatile uint16_t pps_sof_frame; - pps_sof_dwt = DWT->CYCCNT; - { + if (pps_ts_enabled) { + pps_sof_dwt = DWT->CYCCNT; /* OTG device register block = global base + USB_OTG_DEVICE_BASE; frame number = DSTS[13:8]. */ USB_OTG_DeviceTypeDef *dev = (USB_OTG_DeviceTypeDef *)((uint32_t)hpcd->Instance + USB_OTG_DEVICE_BASE); pps_sof_frame = (uint16_t)((dev->DSTS >> 8) & 0x7FFU); + pps_sof_valid = 1; + } else { + pps_sof_valid = 0; } /* USER CODE END SOF_latch */ USBD_LL_SOF((USBD_HandleTypeDef*)hpcd->pData); diff --git a/qspi/config.txt b/qspi/config.txt index 28eb75d..1fa9b8a 100644 --- a/qspi/config.txt +++ b/qspi/config.txt @@ -103,4 +103,8 @@ mode_vbat = 0 # output a $PMTXTS timing sentence over USB serial on each GPS PPS pulse, carrying the # sub-millisecond phase measurement made at the edge, for host-side jitter/drift analysis. +# When on, the sentence also carries a USB SOF-correlation tail (dwt_pps, sof_frame, dwt_sof) +# that lets a host place the edge on its clock to ~microseconds despite USB delivery jitter; +# a host that only needs the phase can ignore the extra fields. Enabling this turns on the USB +# Start-Of-Frame interrupt (a cheap 1 kHz latch, below the display in priority). pps = off From 4d62738cec8b228875c5e191b86ba2ae07dcf44c Mon Sep 17 00:00:00 2001 From: Peter Lewis Date: Tue, 30 Jun 2026 15:24:01 +0100 Subject: [PATCH 04/37] Add astro pack: sun / moon / Maidenhead / lat-lon display modes Five GPS-derived astronomy read-outs, each opt-in via a MODE_* config key and shown on the 10-char date row while the live clock keeps running on the time row (SATVIEW-style, COUNT_NORMAL): MODE_SUN sunrise / sunset / solar noon (local), auto-paged MODE_SUN_AZEL sun azimuth & elevation, now MODE_MOON moon phase index (0-7) + illuminated % MODE_GRID Maidenhead grid locator MODE_LATLON latitude / longitude, auto-paged The astronomy maths is a self-contained Core/Src/astro.c (+ astro.h): low- precision NOAA/Meeus sun alt-az and event times, moon phase, equation of time, and Maidenhead. It has no firmware dependencies, so it unit-tests natively (test/test_astro.c, 45 reference vectors) and was validated to ~1e-5 before touching the firmware. The L4 has only a single-precision FPU, so the double maths would be slow soft- float. It is therefore computed in the main loop (astro_update(), gated on the active mode exactly as measure_vbat() is for MODE_VBAT) and swapped into a cache under a brief __disable_irq() mask; the sendDate() cases that run inside the SysTick ISR only format the cached scalars -- no trig in the interrupt. Modes are disabled by default; with no GPS fix they use fake_latitude/longitude if set, else show "----". config.txt documents the keys and the moon-phase index legend. Co-Authored-By: Claude Opus 4.8 --- mk4-time/Core/Inc/astro.h | 48 +++++++++++++ mk4-time/Core/Inc/main.h | 9 +++ mk4-time/Core/Src/astro.c | 140 +++++++++++++++++++++++++++++++++++++ mk4-time/Core/Src/main.c | 121 ++++++++++++++++++++++++++++++++ mk4-time/test/test_astro.c | 104 +++++++++++++++++++++++++++ qspi/config.txt | 28 ++++++++ 6 files changed, 450 insertions(+) create mode 100644 mk4-time/Core/Inc/astro.h create mode 100644 mk4-time/Core/Src/astro.c create mode 100644 mk4-time/test/test_astro.c diff --git a/mk4-time/Core/Inc/astro.h b/mk4-time/Core/Inc/astro.h new file mode 100644 index 0000000..c296f6c --- /dev/null +++ b/mk4-time/Core/Inc/astro.h @@ -0,0 +1,48 @@ +/* + * astro.h — minimal solar/lunar/grid astronomy for the Precision Clock Mk IV. + * + * Self-contained C99 + ; NO firmware dependencies, so it compiles and + * unit-tests natively (see test_astro.c). All angles in degrees at the API + * boundary; UTC instants are passed as a double of Unix seconds. + * + * Algorithms are low-precision (NOAA/Meeus-class) approximations — accurate to a + * fraction of a degree / a minute or two, which is all a 7-segment readout shows, + * and cheap enough to evaluate once per mode entry on the STM32's soft-double. + */ +#ifndef ASTRO_H +#define ASTRO_H + +/* (a) Sun apparent alt/az for an observer at (lat, lon) decimal degrees, N+/E+, + * at the given UTC instant. Writes azimuth in [0,360) measured from North + * clockwise, and elevation in [-90,90] (negative = below the horizon). + * No refraction or parallax correction. */ +void sun_az_el(double lat, double lon, double unix_s, double *az, double *el); + +/* (b) Sun event times for the UTC calendar day containing unix_s. + * Every output is a decimal UTC hour and MAY be < 0 or > 24 (the event falls + * on the previous/next day) — callers add the local offset and wrap, they do + * NOT clamp. solar_noon is always written. Returns 0 normally; returns + * nonzero on polar day/night (sun never crosses -0.833 deg), in which case + * sunrise/sunset are left untouched and only solar_noon is meaningful. + * Any of the optional twilight pointers may be NULL. */ +int sun_times(double lat, double lon, double unix_s, + double *sunrise, double *sunset, double *solar_noon, + double *civil_dusk, double *nautical_dusk, double *golden_dusk); + +/* (c) Moon. phase is the synodic fraction [0,1): 0=new, .25=first quarter, + * .5=full, .75=last quarter. */ +double moon_phase(double unix_s); +double moon_illuminated_fraction(double phase); /* (1 - cos(2*pi*phase)) / 2 */ +int moon_phase_index(double phase); /* 0..7, see ASTRO_MOON_NAMES */ + +/* (d) Equation of time in minutes (+ = apparent sun ahead of mean/clock sun). */ +double equation_of_time(double unix_s); + +/* (e) 6-character Maidenhead locator for (lat, lon). out must hold >= 7 bytes. + * Writes "----\0" if either coordinate is non-finite. */ +void maidenhead(double lat, double lon, char out[7]); + +/* Phase-index -> short name. Index from moon_phase_index(). */ +extern const char *const ASTRO_MOON_NAMES[8]; + +#endif /* ASTRO_H */ diff --git a/mk4-time/Core/Inc/main.h b/mk4-time/Core/Inc/main.h index 18989e5..c7cd64a 100644 --- a/mk4-time/Core/Inc/main.h +++ b/mk4-time/Core/Inc/main.h @@ -163,6 +163,15 @@ enum { MODE_DDMMYYYY, #endif + // Astro pack — GPS-derived astronomy read-outs. SATVIEW-style: the payload + // shows on the 10-char date row while the live clock keeps running on the + // time row. Enabled individually via the MODE_* config keys, like any mode. + MODE_SUN, // sunrise / sunset / solar noon (local), auto-paged + MODE_SUN_AZEL, // sun azimuth & elevation, now + MODE_MOON, // moon phase index + illuminated % + MODE_GRID, // Maidenhead grid locator + MODE_LATLON, // latitude / longitude, auto-paged + NUM_DISPLAY_MODES }; diff --git a/mk4-time/Core/Src/astro.c b/mk4-time/Core/Src/astro.c new file mode 100644 index 0000000..e34f680 --- /dev/null +++ b/mk4-time/Core/Src/astro.c @@ -0,0 +1,140 @@ +/* + * astro.c — see astro.h. + * Pure C99 + ; every intermediate is double on purpose (single-precision + * loses the sub-arc-minute accuracy these approximations are otherwise good for). + */ +#include "astro.h" +#include +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +#define J2000_UNIX 946728000.0 /* 2000-01-01 12:00:00 UTC = JD 2451545.0 */ +#define DEG (M_PI / 180.0) +#define RAD (180.0 / M_PI) + +const char *const ASTRO_MOON_NAMES[8] = { + "New", "Waxing Crescent", "First Quarter", "Waxing Gibbous", + "Full", "Waning Gibbous", "Last Quarter", "Waning Crescent", +}; + +static double days_since_j2000(double unix_s) { return (unix_s - J2000_UNIX) / 86400.0; } + +/* The shared low-precision solar block: from days-since-J2000 produce mean + * longitude L, anomaly g, ecliptic longitude lambda, obliquity eps, and the + * apparent right ascension alpha (deg) and declination delta (rad). */ +static void sun_ecliptic(double n, double *L, double *g, double *lambda, + double *eps, double *alpha, double *delta) { + double Lv = fmod(280.460 + 0.9856474 * n, 360.0); + double gv = fmod(357.528 + 0.9856003 * n, 360.0); + double lam = Lv + 1.915 * sin(gv * DEG) + 0.020 * sin(2.0 * gv * DEG); + double ep = (23.439 - 0.0000004 * n) * DEG; + double al = atan2(cos(ep) * sin(lam * DEG), cos(lam * DEG)) * RAD; + double de = asin(sin(ep) * sin(lam * DEG)); + if (L) *L = Lv; + if (g) *g = gv; + if (lambda) *lambda = lam; + if (eps) *eps = ep; + if (alpha) *alpha = al; + if (delta) *delta = de; +} + +void sun_az_el(double lat, double lon, double unix_s, double *az, double *el) { + double n = days_since_j2000(unix_s); + double alpha, delta; + sun_ecliptic(n, NULL, NULL, NULL, NULL, &alpha, &delta); + + double gmst = fmod(18.697374558 + 24.06570982441908 * n, 24.0); /* GMST in hours, used as-is */ + double lst = gmst * 15.0 + lon; /* degrees */ + double ha = (lst - alpha) * DEG; /* radians */ + + double e = asin(sin(lat * DEG) * sin(delta) + cos(lat * DEG) * cos(delta) * cos(ha)) * RAD; + double a = atan2(-sin(ha), tan(delta) * cos(lat * DEG) - sin(lat * DEG) * cos(ha)) * RAD; + if (a < 0.0) a += 360.0; + if (el) *el = e; + if (az) *az = a; +} + +double equation_of_time(double unix_s) { + double n = days_since_j2000(unix_s); + double L, alpha; + sun_ecliptic(n, &L, NULL, NULL, NULL, &alpha, NULL); + double diff = fmod(L - alpha, 360.0); + if (diff > 180.0) diff -= 360.0; + else if (diff <= -180.0) diff += 360.0; + return 4.0 * diff; /* minutes */ +} + +int sun_times(double lat, double lon, double unix_s, + double *sunrise, double *sunset, double *solar_noon, + double *civil_dusk, double *nautical_dusk, double *golden_dusk) { + /* Reference instant = 12:00:00 UTC of the calendar day of unix_s. */ + double noon_unix = trunc(unix_s / 86400.0) * 86400.0 + 43200.0; + double n = days_since_j2000(noon_unix); + double delta; + sun_ecliptic(n, NULL, NULL, NULL, NULL, NULL, &delta); + double eot = equation_of_time(noon_unix); /* minutes */ + + double noon = 12.0 - eot / 60.0 - lon / 15.0; + if (solar_noon) *solar_noon = noon; + + /* Hour angle (deg) at which the sun centre sits at altitude h (deg). */ + double cosO = (sin(-0.833 * DEG) - sin(lat * DEG) * sin(delta)) / (cos(lat * DEG) * cos(delta)); + if (cosO < -1.0 || cosO > 1.0) return 1; /* polar day / night: no rise or set */ + double omega = acos(cosO) * RAD; + if (sunrise) *sunrise = noon - omega / 15.0; + if (sunset) *sunset = noon + omega / 15.0; + + if (civil_dusk) { + double c = (sin(-6.0 * DEG) - sin(lat * DEG) * sin(delta)) / (cos(lat * DEG) * cos(delta)); + double o = (c < -1.0 || c > 1.0) ? omega : acos(c) * RAD; + *civil_dusk = noon + o / 15.0; + } + if (nautical_dusk) { + double c = (sin(-12.0 * DEG) - sin(lat * DEG) * sin(delta)) / (cos(lat * DEG) * cos(delta)); + double o = (c < -1.0 || c > 1.0) ? omega : acos(c) * RAD; + *nautical_dusk = noon + o / 15.0; + } + if (golden_dusk) { + double c = (sin(6.0 * DEG) - sin(lat * DEG) * sin(delta)) / (cos(lat * DEG) * cos(delta)); + double o = (c < -1.0 || c > 1.0) ? omega : acos(c) * RAD; + *golden_dusk = noon + o / 15.0; + } + return 0; +} + +double moon_phase(double unix_s) { + double n = days_since_j2000(unix_s); + double phase = fmod((n - 5.26) / 29.53059, 1.0); + if (phase < 0.0) phase += 1.0; + return phase; +} + +double moon_illuminated_fraction(double phase) { return (1.0 - cos(2.0 * M_PI * phase)) / 2.0; } + +int moon_phase_index(double phase) { return ((int)(phase * 8.0 + 0.5)) & 7; } + +void maidenhead(double lat, double lon, char out[7]) { + if (!isfinite(lat) || !isfinite(lon)) { strcpy(out, "----"); return; } + lat = fmin(nextafter(90.0, -INFINITY), fmax(-90.0, lat)); + lon = fmin(nextafter(180.0, -INFINITY), fmax(-180.0, lon)); + double LON = lon + 180.0; /* [0,360) */ + double LAT = lat + 90.0; /* [0,180) */ + + int f0 = (int)(LON / 20.0); if (f0 < 0) f0 = 0; if (f0 > 17) f0 = 17; + int f1 = (int)(LAT / 10.0); if (f1 < 0) f1 = 0; if (f1 > 17) f1 = 17; + int s2 = (int)(fmod(LON, 20.0) / 2.0); if (s2 < 0) s2 = 0; if (s2 > 9) s2 = 9; + int s3 = (int)(fmod(LAT, 10.0)); if (s3 < 0) s3 = 0; if (s3 > 9) s3 = 9; + int u4 = (int)(fmod(LON, 2.0) * 12.0); if (u4 < 0) u4 = 0; if (u4 > 23) u4 = 23; + int u5 = (int)(fmod(LAT, 1.0) * 24.0); if (u5 < 0) u5 = 0; if (u5 > 23) u5 = 23; + + out[0] = (char)('A' + f0); + out[1] = (char)('A' + f1); + out[2] = (char)('0' + s2); + out[3] = (char)('0' + s3); + out[4] = (char)('a' + u4); + out[5] = (char)('a' + u5); + out[6] = '\0'; +} diff --git a/mk4-time/Core/Src/main.c b/mk4-time/Core/Src/main.c index a408076..7ee0501 100644 --- a/mk4-time/Core/Src/main.c +++ b/mk4-time/Core/Src/main.c @@ -32,6 +32,7 @@ #include "qspi_drv.h" #include "zonedetect.h" #include "chainloader.h" +#include "astro.h" /* USER CODE END Includes */ /* Private typedef -----------------------------------------------------------*/ @@ -148,6 +149,20 @@ uint8_t decisec=0, centisec=0, millisec=0; float longitude=-9999, latitude=-9999; _Bool data_valid=0, had_pps=0, rtc_good=0, new_position=1; + +// Astro pack — sun/moon/grid read-outs, computed once a second in the main loop +// (astro_update) and formatted by the sendDate cases, the same compute-in-loop / +// format-in-ISR split MODE_VBAT uses for vbat. +struct astro_cache_s { + uint32_t epoch; // currentTime this was computed for; 0 = never computed + _Bool have_pos; // a usable lat/lon was available + _Bool sun_up_today; // false = polar day/night (no rise/set this date) + int16_t rise_min, set_min, noon_min; // local minutes-of-day [0,1440) + int16_t az, el; // sun azimuth 0..359 / elevation, whole degrees + uint8_t moon_idx, moon_pct; // phase index 0..7 / illuminated % + char grid[8]; // Maidenhead locator, or "----" + float lat_show, lon_show; // the snapshot lat/lon, for MODE_LATLON +} astro = {0}; #define rtc_last_write RTC->BKP30R #define rtc_last_calibration RTC->BKP31R uint32_t last_pps_time = 0; @@ -248,6 +263,57 @@ void memcpyword(volatile uint32_t *dest, volatile uint32_t *src, size_t n){ } // 12 bytes at 115200 8E1 is 1.14ms, 32 bytes would be 3.06ms +// --- Astro pack helpers ---------------------------------------------------- +// A usable position is held in latitude/longitude from either a GPS fix or the +// configured fake_latitude/fake_longitude; both sit at the -9999 sentinel until +// a position is known, so a simple range check is the "have we got a fix" test. +static _Bool astro_pos_ok(float lat, float lon){ + return lat >= -90.0f && lat <= 90.0f && lon >= -180.0f && lon <= 180.0f; +} +// Decimal UTC hour (sun_times may return <0 or >24) -> local minutes-of-day [0,1440). +static int astro_local_minutes(double utc_h){ + double h = fmod(utc_h + currentOffset / 3600.0, 24.0); + if (h < 0) h += 24.0; + int m = (int)(h * 60.0 + 0.5); + if (m >= 1440) m -= 1440; + return m; +} +// Recompute the astro cache (called from the main loop, never the ISR). The +// double soft-float maths runs here, then the small result struct is swapped in +// under a brief IRQ mask so sendDate() always reads a consistent snapshot. +static void astro_update(void){ + if (astro.epoch == (uint32_t)currentTime) return; // at most once a second + struct astro_cache_s c = {0}; + c.epoch = (uint32_t)currentTime; + double ph = moon_phase((double)currentTime); // moon needs no fix + c.moon_idx = moon_phase_index(ph); + c.moon_pct = (uint8_t)(moon_illuminated_fraction(ph) * 100.0 + 0.5); + float lat = latitude, lon = longitude; // one consistent snapshot of the fix + c.have_pos = astro_pos_ok(lat, lon); + if (c.have_pos) { + c.lat_show = lat; + c.lon_show = lon; + double az, el, rise = 0, set = 0, noon = 0; + sun_az_el(lat, lon, (double)currentTime, &az, &el); + int ia = (int)(az + 0.5); if (ia >= 360) ia -= 360; + c.az = (int16_t)ia; + c.el = (int16_t)(el < 0 ? el - 0.5 : el + 0.5); + c.sun_up_today = (sun_times(lat, lon, (double)currentTime, + &rise, &set, &noon, 0, 0, 0) == 0); + c.noon_min = (int16_t)astro_local_minutes(noon); // noon is valid even at the poles + if (c.sun_up_today) { + c.rise_min = (int16_t)astro_local_minutes(rise); + c.set_min = (int16_t)astro_local_minutes(set); + } + maidenhead(lat, lon, c.grid); + } else { + strcpy(c.grid, "----"); + } + __disable_irq(); + astro = c; + __enable_irq(); +} + void sendDate( _Bool now ){ if (waitingForLatch) { if (countMode==COUNT_HIDDEN) { @@ -504,6 +570,47 @@ void sendDate( _Bool now ){ case MODE_FIRMWARE_CRC_D: uart2_tx_buffer[0]=CMD_SHOW_CRC; break; + + // --- Astro pack: format the main-loop-computed cache onto the date row only, + // leaving the time row as the running clock (SATVIEW-style). -------------- + case MODE_SUN: { + if (!astro.have_pos || !astro.epoch) { i = sprintf((char*)&uart2_tx_buffer[1], "RISE ----"); break; } + int page = (currentTime / 2) % 3; // rise -> set -> solar noon, 2 s each + // labels padded to 4 chars in the literal ("SET "/"SOL ") so the time digits + // line up under RISE without relying on the nano printf honouring "%-4s" + const char *lbl = page == 0 ? "RISE" : page == 1 ? "SET " : "SOL "; + int m = page == 0 ? astro.rise_min : page == 1 ? astro.set_min : astro.noon_min; + if (!astro.sun_up_today && page != 2) { // sun never rises/sets today + i = sprintf((char*)&uart2_tx_buffer[1], "%s ----", lbl); + } else { + i = sprintf((char*)&uart2_tx_buffer[1], "%s %02d.%02d", lbl, m / 60, m % 60); + } + break; + } + case MODE_SUN_AZEL: + if (!astro.have_pos || !astro.epoch) { i = sprintf((char*)&uart2_tx_buffer[1], "AZ -- EL--"); } + else if (astro.el < 0) i = sprintf((char*)&uart2_tx_buffer[1], "AZ%03dEL-%02d", astro.az, -astro.el); + else i = sprintf((char*)&uart2_tx_buffer[1], "AZ%03dEL%02d", astro.az, astro.el); + break; + case MODE_MOON: // UTC only; no fix needed + if (!astro.epoch) i = sprintf((char*)&uart2_tx_buffer[1], "MOON -"); + else i = sprintf((char*)&uart2_tx_buffer[1], "MOON %d %3d", astro.moon_idx, astro.moon_pct); + break; + case MODE_GRID: + i = sprintf((char*)&uart2_tx_buffer[1], "%s", astro.epoch ? astro.grid : "----"); + break; + case MODE_LATLON: + if (!astro.have_pos || !astro.epoch) { i = sprintf((char*)&uart2_tx_buffer[1], "LAT ----"); } + else if ((currentTime / 2) % 2 == 0) { // page latitude / longitude, 2 s each + long h = (long)(astro.lat_show * 100.0 + (astro.lat_show < 0 ? -0.5 : 0.5)); // hundredths, rounded + if (h < 0) i = sprintf((char*)&uart2_tx_buffer[1], "LAT-%ld.%02ld", -h / 100, -h % 100); + else i = sprintf((char*)&uart2_tx_buffer[1], "LAT %ld.%02ld", h / 100, h % 100); + } else { + long h = (long)(astro.lon_show * 100.0 + (astro.lon_show < 0 ? -0.5 : 0.5)); + if (h < 0) i = sprintf((char*)&uart2_tx_buffer[1], "LON-%ld.%02ld", -h / 100, -h % 100); + else i = sprintf((char*)&uart2_tx_buffer[1], "LON %ld.%02ld", h / 100, h % 100); + } + break; } if (now) { uart2_tx_buffer[++i]= CMD_RELOAD_TEXT; @@ -1031,6 +1138,16 @@ void parseConfigString(char *key, char *value) { } else if (strcasecmp(key, "MODE_FIRMWARE_CRC") == 0) { set_mode_enabled(MODE_FIRMWARE_CRC_D, value); set_mode_enabled(MODE_FIRMWARE_CRC_T, value); + } else if (strcasecmp(key, "MODE_SUN") == 0) { + set_mode_enabled(MODE_SUN, value); + } else if (strcasecmp(key, "MODE_SUN_AZEL") == 0) { + set_mode_enabled(MODE_SUN_AZEL, value); + } else if (strcasecmp(key, "MODE_MOON") == 0) { + set_mode_enabled(MODE_MOON, value); + } else if (strcasecmp(key, "MODE_GRID") == 0) { + set_mode_enabled(MODE_GRID, value); + } else if (strcasecmp(key, "MODE_LATLON") == 0) { + set_mode_enabled(MODE_LATLON, value); } else if (strcasecmp(key, "Tolerance_time_1ms") == 0) { config.tolerance_1ms = atoi(value); } else if (strcasecmp(key, "Tolerance_time_10ms") == 0) { @@ -2300,6 +2417,10 @@ int main(void) if (displayMode == MODE_VBAT) measure_vbat(); + if (displayMode == MODE_SUN || displayMode == MODE_SUN_AZEL || displayMode == MODE_MOON + || displayMode == MODE_GRID || displayMode == MODE_LATLON) + astro_update(); + /* USER CODE END WHILE */ diff --git a/mk4-time/test/test_astro.c b/mk4-time/test/test_astro.c new file mode 100644 index 0000000..4df94fc --- /dev/null +++ b/mk4-time/test/test_astro.c @@ -0,0 +1,104 @@ +/* Native unit test for ../Core/Src/astro.c against the reference vectors used to + * develop the astro pack. Not part of the firmware build (test/ is not a project + * source path); build & run on a host: + * + * cc -std=c99 -O2 -I ../Core/Inc ../Core/Src/astro.c test_astro.c -lm -o test_astro && ./test_astro + */ +#include "astro.h" +#include +#include +#include + +static int fails = 0, total = 0; + +static void chk(const char *name, double got, double exp, double tol) { + total++; + double d = fabs(got - exp); + if (d > tol || !isfinite(got)) { + printf(" FAIL %-28s got %.6f exp %.6f (|d|=%.6f > %.6f)\n", name, got, exp, d, tol); + fails++; + } else { + printf(" ok %-28s %.6f (|d|=%.2e)\n", name, got, d); + } +} + +static void chks(const char *name, const char *got, const char *exp) { + total++; + if (strcmp(got, exp) != 0) { printf(" FAIL %-28s got \"%s\" exp \"%s\"\n", name, got, exp); fails++; } + else printf(" ok %-28s \"%s\"\n", name, got); +} + +struct loc { const char *tag; double lat, lon, t; }; + +int main(void) { + struct loc V1 = {"Greenwich", 51.4779, -0.0015, 1718971200}; /* 2024-06-21 12:00 */ + struct loc V2 = {"Sydney", -33.8568, 151.2153, 1704067200}; /* 2024-01-01 00:00 */ + struct loc V3 = {"Quito", -0.1807, -78.4678, 1411819200}; /* 2014-09-27 12:00 */ + struct loc V4 = {"Fairbanks", 64.8378, -147.7164, 1671460200}; /* 2022-12-19 14:30 */ + struct loc *V[4] = {&V1, &V2, &V3, &V4}; + + double az, el; + printf("sun_az_el:\n"); + double exp_el[4] = {61.9537, 61.9872, 13.7817, -29.3334}; + double exp_az[4] = {179.0572, 75.1095, 91.7169, 82.8687}; + for (int i = 0; i < 4; i++) { + char nm[40]; + sun_az_el(V[i]->lat, V[i]->lon, V[i]->t, &az, &el); + sprintf(nm, "el %s", V[i]->tag); chk(nm, el, exp_el[i], 0.001); + sprintf(nm, "az %s", V[i]->tag); chk(nm, az, exp_az[i], 0.001); + } + + printf("equation_of_time (min):\n"); + double exp_eot[4] = {-1.92784, -3.09298, 8.99946, 2.88245}; + for (int i = 0; i < 4; i++) { + char nm[40]; sprintf(nm, "eot %s", V[i]->tag); + chk(nm, equation_of_time(V[i]->t), exp_eot[i], 0.0005); + } + + printf("moon_phase / illuminated:\n"); + double exp_ph[4] = {0.49108, 0.64968, 0.10744, 0.86985}; + double exp_il[4] = {0.99921, 0.79471, 0.10966, 0.15807}; + int exp_idx[4] = {4, 5, 1, 7}; + for (int i = 0; i < 4; i++) { + char nm[40]; double p = moon_phase(V[i]->t); + sprintf(nm, "phase %s", V[i]->tag); chk(nm, p, exp_ph[i], 0.0002); + sprintf(nm, "illum %s", V[i]->tag); chk(nm, moon_illuminated_fraction(p), exp_il[i], 0.0005); + sprintf(nm, "idx %s", V[i]->tag); chk(nm, moon_phase_index(p), exp_idx[i], 0.0); + } + + printf("sun_times (UTC h):\n"); + double rise, set, noon, civ, nau, gold; + /* V1 Greenwich */ + sun_times(V1.lat, V1.lon, V1.t, &rise, &set, &noon, &civ, &nau, &gold); + chk("noon V1", noon, 12.032231, 0.0003); + chk("rise V1", rise, 3.715903, 0.0003); + chk("set V1", set, 20.348558, 0.0003); + chk("civil V1", civ, 21.143501, 0.0003); + chk("naut V1", nau, 22.383822, 0.0003); + /* V3 Quito */ + sun_times(V3.lat, V3.lon, V3.t, &rise, &set, &noon, NULL, NULL, NULL); + chk("noon V3", noon, 17.081196, 0.0003); + chk("rise V3", rise, 11.025277, 0.0003); + chk("set V3", set, 23.137114, 0.0003); + /* V4 Fairbanks (events spill past midnight) */ + sun_times(V4.lat, V4.lon, V4.t, &rise, &set, &noon, &civ, &nau, &gold); + chk("noon V4", noon, 21.798861, 0.0005); + chk("rise V4", rise, 19.944940, 0.0005); + chk("set V4", set, 23.652783, 0.0005); + chk("civil V4", civ, 25.076604, 0.0005); + chk("naut V4", nau, 26.273118, 0.0005); + + printf("maidenhead:\n"); + char g[7]; + maidenhead(V1.lat, V1.lon, g); chks("grid V1", g, "IO91xl"); + maidenhead(V2.lat, V2.lon, g); chks("grid V2", g, "QF56od"); + maidenhead(V3.lat, V3.lon, g); chks("grid V3", g, "FI09st"); + maidenhead(V4.lat, V4.lon, g); chks("grid V4", g, "BP64du"); + maidenhead(51.508, -0.128, g); chks("Trafalgar Sq", g, "IO91wm"); + maidenhead(41.714, -72.728, g); chks("ARRL HQ", g, "FN31pr"); + maidenhead(-41.283, 174.745, g); chks("Wellington", g, "RE78ir"); + maidenhead(1.0 / 0.0, 0.0, g); chks("non-finite", g, "----"); + + printf("\n%d/%d passed, %d failed\n", total - fails, total, fails); + return fails ? 1 : 0; +} diff --git a/qspi/config.txt b/qspi/config.txt index 1fa9b8a..25a1444 100644 --- a/qspi/config.txt +++ b/qspi/config.txt @@ -108,3 +108,31 @@ mode_vbat = 0 # a host that only needs the phase can ignore the extra fields. Enabling this turns on the USB # Start-Of-Frame interrupt (a cheap 1 kHz latch, below the display in priority). pps = off + + +## astro modes (GPS-derived) +# These show on the date row while the live clock keeps running on the time row. +# They use the current position; with no GPS fix they fall back to fake_latitude/ +# fake_longitude below (handy indoors), or show "----" if neither is set. + +# Sunrise / sunset / solar noon (local time). Pages every 2 s: RISE -> SET -> SOL. +MODE_SUN = disabled + +# Sun azimuth & elevation right now, e.g. "AZ142EL38" (degrees; elevation may be negative). +MODE_SUN_AZEL = disabled + +# Moon: phase index 0-7 then illuminated %, e.g. "MOON 4 99". +# Index: 0 new, 1 waxing crescent, 2 first quarter, 3 waxing gibbous, +# 4 full, 5 waning gibbous, 6 last quarter, 7 waning crescent. +MODE_MOON = disabled + +# Maidenhead grid locator, e.g. "IO91xl". +MODE_GRID = disabled + +# Latitude / longitude in decimal degrees. Pages every 2 s: LAT -> LON. +MODE_LATLON = disabled + +# Fixed position for the astro modes when there is no GPS fix (decimal degrees, N+/E+). +# Note: setting these also pins the clock's position (GPS position updates are ignored). +#fake_latitude = 51.48 +#fake_longitude = -0.01 From e1eeeb81c1c9b0f8fcb856fc86ad725d06b387b4 Mon Sep 17 00:00:00 2001 From: Peter Lewis Date: Sun, 5 Jul 2026 09:02:17 +0100 Subject: [PATCH 05/37] astro: give LAT/LON a sign slot so negatives keep the label gap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "LAT 51.48" vs "LAT-51.48" let the minus swallow the separator. Add a sign slot after the label space — "LAT 51.48" / "LAT -51.48" — so the digits start in the same column either way, matching the RISE/SET layout. A 3-digit longitude can't fit both the separator and the slot in 10 characters, so the separator alone is dropped there ("LON-179.99"). Co-Authored-By: Claude Opus 4.8 --- mk4-time/Core/Src/main.c | 6481 +++++++++++++++++++------------------- 1 file changed, 3242 insertions(+), 3239 deletions(-) diff --git a/mk4-time/Core/Src/main.c b/mk4-time/Core/Src/main.c index 7ee0501..e967a0c 100644 --- a/mk4-time/Core/Src/main.c +++ b/mk4-time/Core/Src/main.c @@ -1,217 +1,217 @@ -/* USER CODE BEGIN Header */ -/** - ****************************************************************************** - * @file : main.c - * @brief : Main program body - ****************************************************************************** - * @attention - * - *

© Copyright (c) 2020 STMicroelectronics. - * All rights reserved.

- * - * This software component is licensed by ST under BSD 3-Clause license, - * the "License"; You may not use this file except in compliance with the - * License. You may obtain a copy of the License at: - * opensource.org/licenses/BSD-3-Clause - * - ****************************************************************************** - */ -/* USER CODE END Header */ - -/* Includes ------------------------------------------------------------------*/ -#include "main.h" -#include "fatfs.h" -#include "usb_device.h" - -/* Private includes ----------------------------------------------------------*/ -/* USER CODE BEGIN Includes */ -#include -#include -#include -#include -#include "qspi_drv.h" -#include "zonedetect.h" -#include "chainloader.h" -#include "astro.h" -/* USER CODE END Includes */ - -/* Private typedef -----------------------------------------------------------*/ -/* USER CODE BEGIN PTD */ - -/* USER CODE END PTD */ - -/* Private define ------------------------------------------------------------*/ -/* USER CODE BEGIN PD */ -/* USER CODE END PD */ - -/* Private macro -------------------------------------------------------------*/ -/* USER CODE BEGIN PM */ - -/* USER CODE END PM */ - -/* Private variables ---------------------------------------------------------*/ -ADC_HandleTypeDef hadc1; -ADC_HandleTypeDef hadc3; - -CRC_HandleTypeDef hcrc; - -DAC_HandleTypeDef hdac1; -DMA_HandleTypeDef hdma_dac_ch1; - -QSPI_HandleTypeDef hqspi; - -RTC_HandleTypeDef hrtc; - -TIM_HandleTypeDef htim1; -TIM_HandleTypeDef htim2; -TIM_HandleTypeDef htim5; -TIM_HandleTypeDef htim6; -TIM_HandleTypeDef htim7; -DMA_HandleTypeDef hdma_tim1_up; -DMA_HandleTypeDef hdma_tim5_ch1; -DMA_HandleTypeDef hdma_tim5_ch2; -DMA_HandleTypeDef hdma_tim7_up; - -UART_HandleTypeDef huart1; -UART_HandleTypeDef huart2; -DMA_HandleTypeDef hdma_usart1_rx; -DMA_HandleTypeDef hdma_usart2_tx; - -/* USER CODE BEGIN PV */ - -/* USER CODE END PV */ - -/* Private function prototypes -----------------------------------------------*/ -void SystemClock_Config(void); -static void MX_GPIO_Init(void); -static void MX_DMA_Init(void); -static void MX_QUADSPI_Init(void); -static void MX_TIM1_Init(void); -static void MX_USART2_UART_Init(void); -static void MX_USART1_UART_Init(void); -static void MX_TIM2_Init(void); -static void MX_ADC1_Init(void); -static void MX_DAC1_Init(void); -static void MX_TIM6_Init(void); -static void MX_RTC_Init(void); -static void MX_TIM7_Init(void); -static void MX_CRC_Init(void); -static void MX_LPTIM1_Init(void); -static void MX_TIM5_Init(void); -static void MX_ADC3_Init(void); -/* USER CODE BEGIN PFP */ -void tmToBcd(struct tm *in, bcdStamp_t *out ); -uint8_t loadRulesSingle(char * str); -void nextMode(_Bool); -/* USER CODE END PFP */ - -/* Private user code ---------------------------------------------------------*/ -/* USER CODE BEGIN 0 */ -const uint8_t cLut[]= { cSegDecode0, cSegDecode1, cSegDecode2, cSegDecode3, cSegDecode4, cSegDecode5, cSegDecode6, cSegDecode7, cSegDecode8, cSegDecode9 }; -const uint16_t bLut[]={ bSegDecode0, bSegDecode1, bSegDecode2, bSegDecode3, bSegDecode4, bSegDecode5, bSegDecode6, bSegDecode7, bSegDecode8, bSegDecode9 }; - -const char* wday_str[]={"Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"}; - -buffer_c_t buffer_c[80] = {0}; - -uint16_t buffer_b[80] = {0}; - -uint8_t uart2_tx_buffer[32]; - -volatile uint16_t buffer_adc[ADC_BUFFER_SIZE] = {0}; -uint16_t buffer_dac[DAC_BUFFER_SIZE] = {[0 ... DAC_BUFFER_SIZE-1] = 4095}; -float dac_target=4095; -float vbat = 0.0; - -uint16_t buffer_colons_L[200] = {0}; -uint16_t buffer_colons_R[200] = {0}; - -uint8_t nmea[NMEA_BUF_SIZE]; -uint8_t satview[SV_COUNT]; -uint8_t satview_stale = 0; - -time_t currentTime; -bcdStamp_t nextBcd; -int tm_yday; -int8_t tm_wday; -int iso_year; -int8_t iso_wday; -uint8_t iso_week; -uint32_t countdown_days; -int32_t currentOffset=0; - -struct { - uint8_t c; - uint16_t b[5]; -} next7seg; - -uint8_t decisec=0, centisec=0, millisec=0; - -float longitude=-9999, latitude=-9999; -_Bool data_valid=0, had_pps=0, rtc_good=0, new_position=1; - -// Astro pack — sun/moon/grid read-outs, computed once a second in the main loop -// (astro_update) and formatted by the sendDate cases, the same compute-in-loop / -// format-in-ISR split MODE_VBAT uses for vbat. -struct astro_cache_s { - uint32_t epoch; // currentTime this was computed for; 0 = never computed - _Bool have_pos; // a usable lat/lon was available - _Bool sun_up_today; // false = polar day/night (no rise/set this date) - int16_t rise_min, set_min, noon_min; // local minutes-of-day [0,1440) - int16_t az, el; // sun azimuth 0..359 / elevation, whole degrees - uint8_t moon_idx, moon_pct; // phase index 0..7 / illuminated % - char grid[8]; // Maidenhead locator, or "----" - float lat_show, lon_show; // the snapshot lat/lon, for MODE_LATLON -} astro = {0}; -#define rtc_last_write RTC->BKP30R -#define rtc_last_calibration RTC->BKP31R -uint32_t last_pps_time = 0; -uint32_t time_till_first_fix = 0; - -struct { - uint32_t t; - int32_t offset; -} rules[162]; -#define MAX_RULES (sizeof rules / sizeof rules[0]) - -char loadedRulesString[32]; -char preloadRulesString[32]; -char textDisplay[32]; -_Bool delayedLoadRules = 0; -_Bool delayedReadConfigFile = 0; -_Bool delayedCheckOnEject = 0; -uint32_t delayedDisplayFreq = 0; - -_Bool waitingForLatch = 0; -_Bool resendDate = 0; - -uint32_t LPTIM1_high; - -uint8_t displayMode = 0, countMode = 0, colonMode = 0; -uint8_t requestMode = 255; -uint8_t nmea_cdc_level=0; -int debug_rtc_val = 0; - -// --- PPS host timestamping ---------------------------------------------------------------- -// Optional: emit one proprietary NMEA sentence ($PMTXTS) per PPS edge over the CDC port so a -// host can measure the clock's timing stability (phase jitter, oscillator drift, holdover) — -// things the plain NMEA stream cannot convey. Enabled by config "pps = on". Capture happens in -// the PPS ISR (cheap, just snapshots); the sentence is formatted + sent from the main loop. -volatile uint8_t pps_ts_enabled = 0; -volatile _Bool pps_record_pending = 0; -int16_t die_temp_c = 0; // latest STM32 die temperature (°C), a proxy for the crystal temperature -volatile struct { - uint32_t seq; // increments every PPS edge (32-bit: no practical wrap; host detects gaps) - uint32_t systick; // SysTick->VAL at the edge, captured BEFORE the reload (down-counter) - uint16_t subms; // 0..999 modelled ms-of-second at the edge, BEFORE the counters reset - uint32_t epoch; // currentTime at the edge (Unix seconds, UTC) - int32_t calerr; // debug_rtc_val: signed LSE cycle error over CAL_PERIOD s (=> ppm on host) - uint32_t sincecal; // seconds since last successful RTC calibration (holdover age) - int16_t temp; // die temperature (°C) — for host-side ppm-vs-temperature characterisation - uint8_t flags; // bit0 data_valid, bit1 had_pps, bit2 rtc_good +/* USER CODE BEGIN Header */ +/** + ****************************************************************************** + * @file : main.c + * @brief : Main program body + ****************************************************************************** + * @attention + * + *

© Copyright (c) 2020 STMicroelectronics. + * All rights reserved.

+ * + * This software component is licensed by ST under BSD 3-Clause license, + * the "License"; You may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * opensource.org/licenses/BSD-3-Clause + * + ****************************************************************************** + */ +/* USER CODE END Header */ + +/* Includes ------------------------------------------------------------------*/ +#include "main.h" +#include "fatfs.h" +#include "usb_device.h" + +/* Private includes ----------------------------------------------------------*/ +/* USER CODE BEGIN Includes */ +#include +#include +#include +#include +#include "qspi_drv.h" +#include "zonedetect.h" +#include "chainloader.h" +#include "astro.h" +/* USER CODE END Includes */ + +/* Private typedef -----------------------------------------------------------*/ +/* USER CODE BEGIN PTD */ + +/* USER CODE END PTD */ + +/* Private define ------------------------------------------------------------*/ +/* USER CODE BEGIN PD */ +/* USER CODE END PD */ + +/* Private macro -------------------------------------------------------------*/ +/* USER CODE BEGIN PM */ + +/* USER CODE END PM */ + +/* Private variables ---------------------------------------------------------*/ +ADC_HandleTypeDef hadc1; +ADC_HandleTypeDef hadc3; + +CRC_HandleTypeDef hcrc; + +DAC_HandleTypeDef hdac1; +DMA_HandleTypeDef hdma_dac_ch1; + +QSPI_HandleTypeDef hqspi; + +RTC_HandleTypeDef hrtc; + +TIM_HandleTypeDef htim1; +TIM_HandleTypeDef htim2; +TIM_HandleTypeDef htim5; +TIM_HandleTypeDef htim6; +TIM_HandleTypeDef htim7; +DMA_HandleTypeDef hdma_tim1_up; +DMA_HandleTypeDef hdma_tim5_ch1; +DMA_HandleTypeDef hdma_tim5_ch2; +DMA_HandleTypeDef hdma_tim7_up; + +UART_HandleTypeDef huart1; +UART_HandleTypeDef huart2; +DMA_HandleTypeDef hdma_usart1_rx; +DMA_HandleTypeDef hdma_usart2_tx; + +/* USER CODE BEGIN PV */ + +/* USER CODE END PV */ + +/* Private function prototypes -----------------------------------------------*/ +void SystemClock_Config(void); +static void MX_GPIO_Init(void); +static void MX_DMA_Init(void); +static void MX_QUADSPI_Init(void); +static void MX_TIM1_Init(void); +static void MX_USART2_UART_Init(void); +static void MX_USART1_UART_Init(void); +static void MX_TIM2_Init(void); +static void MX_ADC1_Init(void); +static void MX_DAC1_Init(void); +static void MX_TIM6_Init(void); +static void MX_RTC_Init(void); +static void MX_TIM7_Init(void); +static void MX_CRC_Init(void); +static void MX_LPTIM1_Init(void); +static void MX_TIM5_Init(void); +static void MX_ADC3_Init(void); +/* USER CODE BEGIN PFP */ +void tmToBcd(struct tm *in, bcdStamp_t *out ); +uint8_t loadRulesSingle(char * str); +void nextMode(_Bool); +/* USER CODE END PFP */ + +/* Private user code ---------------------------------------------------------*/ +/* USER CODE BEGIN 0 */ +const uint8_t cLut[]= { cSegDecode0, cSegDecode1, cSegDecode2, cSegDecode3, cSegDecode4, cSegDecode5, cSegDecode6, cSegDecode7, cSegDecode8, cSegDecode9 }; +const uint16_t bLut[]={ bSegDecode0, bSegDecode1, bSegDecode2, bSegDecode3, bSegDecode4, bSegDecode5, bSegDecode6, bSegDecode7, bSegDecode8, bSegDecode9 }; + +const char* wday_str[]={"Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"}; + +buffer_c_t buffer_c[80] = {0}; + +uint16_t buffer_b[80] = {0}; + +uint8_t uart2_tx_buffer[32]; + +volatile uint16_t buffer_adc[ADC_BUFFER_SIZE] = {0}; +uint16_t buffer_dac[DAC_BUFFER_SIZE] = {[0 ... DAC_BUFFER_SIZE-1] = 4095}; +float dac_target=4095; +float vbat = 0.0; + +uint16_t buffer_colons_L[200] = {0}; +uint16_t buffer_colons_R[200] = {0}; + +uint8_t nmea[NMEA_BUF_SIZE]; +uint8_t satview[SV_COUNT]; +uint8_t satview_stale = 0; + +time_t currentTime; +bcdStamp_t nextBcd; +int tm_yday; +int8_t tm_wday; +int iso_year; +int8_t iso_wday; +uint8_t iso_week; +uint32_t countdown_days; +int32_t currentOffset=0; + +struct { + uint8_t c; + uint16_t b[5]; +} next7seg; + +uint8_t decisec=0, centisec=0, millisec=0; + +float longitude=-9999, latitude=-9999; +_Bool data_valid=0, had_pps=0, rtc_good=0, new_position=1; + +// Astro pack — sun/moon/grid read-outs, computed once a second in the main loop +// (astro_update) and formatted by the sendDate cases, the same compute-in-loop / +// format-in-ISR split MODE_VBAT uses for vbat. +struct astro_cache_s { + uint32_t epoch; // currentTime this was computed for; 0 = never computed + _Bool have_pos; // a usable lat/lon was available + _Bool sun_up_today; // false = polar day/night (no rise/set this date) + int16_t rise_min, set_min, noon_min; // local minutes-of-day [0,1440) + int16_t az, el; // sun azimuth 0..359 / elevation, whole degrees + uint8_t moon_idx, moon_pct; // phase index 0..7 / illuminated % + char grid[8]; // Maidenhead locator, or "----" + float lat_show, lon_show; // the snapshot lat/lon, for MODE_LATLON +} astro = {0}; +#define rtc_last_write RTC->BKP30R +#define rtc_last_calibration RTC->BKP31R +uint32_t last_pps_time = 0; +uint32_t time_till_first_fix = 0; + +struct { + uint32_t t; + int32_t offset; +} rules[162]; +#define MAX_RULES (sizeof rules / sizeof rules[0]) + +char loadedRulesString[32]; +char preloadRulesString[32]; +char textDisplay[32]; +_Bool delayedLoadRules = 0; +_Bool delayedReadConfigFile = 0; +_Bool delayedCheckOnEject = 0; +uint32_t delayedDisplayFreq = 0; + +_Bool waitingForLatch = 0; +_Bool resendDate = 0; + +uint32_t LPTIM1_high; + +uint8_t displayMode = 0, countMode = 0, colonMode = 0; +uint8_t requestMode = 255; +uint8_t nmea_cdc_level=0; +int debug_rtc_val = 0; + +// --- PPS host timestamping ---------------------------------------------------------------- +// Optional: emit one proprietary NMEA sentence ($PMTXTS) per PPS edge over the CDC port so a +// host can measure the clock's timing stability (phase jitter, oscillator drift, holdover) — +// things the plain NMEA stream cannot convey. Enabled by config "pps = on". Capture happens in +// the PPS ISR (cheap, just snapshots); the sentence is formatted + sent from the main loop. +volatile uint8_t pps_ts_enabled = 0; +volatile _Bool pps_record_pending = 0; +int16_t die_temp_c = 0; // latest STM32 die temperature (°C), a proxy for the crystal temperature +volatile struct { + uint32_t seq; // increments every PPS edge (32-bit: no practical wrap; host detects gaps) + uint32_t systick; // SysTick->VAL at the edge, captured BEFORE the reload (down-counter) + uint16_t subms; // 0..999 modelled ms-of-second at the edge, BEFORE the counters reset + uint32_t epoch; // currentTime at the edge (Unix seconds, UTC) + int32_t calerr; // debug_rtc_val: signed LSE cycle error over CAL_PERIOD s (=> ppm on host) + uint32_t sincecal; // seconds since last successful RTC calibration (holdover age) + int16_t temp; // die temperature (°C) — for host-side ppm-vs-temperature characterisation + uint8_t flags; // bit0 data_valid, bit1 had_pps, bit2 rtc_good uint32_t dwt_pps; // DWT->CYCCNT (free-running 12.5ns) latched at the edge — SOF-correlation timebase -} pps_cap; - +} pps_cap; + // --- SOF correlation (experimental: sub-ms USB timestamping without a hardware PPS wire) ----------- // Latched by the USB SOF interrupt (PCD_SOFCallback, usbd_conf.c) every 1ms WHEN pps_ts_enabled: the // 11-bit USB frame number and the DWT count at that Start-Of-Frame. Emitted in $PMTXTS so a host — @@ -224,1335 +224,1338 @@ volatile uint32_t pps_sof_dwt = 0; // DWT->CYCCNT at the most recent SOF volatile uint16_t pps_sof_frame = 0; // USB 11-bit frame number at that SOF (matches host frame mod 2048) volatile uint8_t pps_sof_valid = 0; // 1 once a real SOF anchor has been latched; 0 = emit 9-field only -#define CHECK_CONFIG_MTIME - -struct { -#ifdef CHECK_CONFIG_MTIME - unsigned short fdate; - unsigned short ftime; -#endif - uint32_t tolerance_1ms; - uint32_t tolerance_10ms; - uint32_t tolerance_100ms; - float fake_long; - float fake_lat; - time_t countdown_to; - float brightness_override; - volatile _Bool zone_override; - _Bool modes_enabled[NUM_DISPLAY_MODES]; - -} config = {0}; - -struct { - float in; - float out; -} brightnessCurve[] = { - {0, 4095-0}, - {1425, 4095-737}, - {2566, 4095-1601}, - {3396, 4095-2725}, - {4095, 4095-4095}, -}; - -// memcpy() appears to move data by bytes, which doesn't work with the word-accessed backup registers -// here we explicitly move data a word at a time -void memcpyword(volatile uint32_t *dest, volatile uint32_t *src, size_t n){ - while (n--){ - dest[n] = src[n]; - } -} - -// 12 bytes at 115200 8E1 is 1.14ms, 32 bytes would be 3.06ms -// --- Astro pack helpers ---------------------------------------------------- -// A usable position is held in latitude/longitude from either a GPS fix or the -// configured fake_latitude/fake_longitude; both sit at the -9999 sentinel until -// a position is known, so a simple range check is the "have we got a fix" test. -static _Bool astro_pos_ok(float lat, float lon){ - return lat >= -90.0f && lat <= 90.0f && lon >= -180.0f && lon <= 180.0f; -} -// Decimal UTC hour (sun_times may return <0 or >24) -> local minutes-of-day [0,1440). -static int astro_local_minutes(double utc_h){ - double h = fmod(utc_h + currentOffset / 3600.0, 24.0); - if (h < 0) h += 24.0; - int m = (int)(h * 60.0 + 0.5); - if (m >= 1440) m -= 1440; - return m; -} -// Recompute the astro cache (called from the main loop, never the ISR). The -// double soft-float maths runs here, then the small result struct is swapped in -// under a brief IRQ mask so sendDate() always reads a consistent snapshot. -static void astro_update(void){ - if (astro.epoch == (uint32_t)currentTime) return; // at most once a second - struct astro_cache_s c = {0}; - c.epoch = (uint32_t)currentTime; - double ph = moon_phase((double)currentTime); // moon needs no fix - c.moon_idx = moon_phase_index(ph); - c.moon_pct = (uint8_t)(moon_illuminated_fraction(ph) * 100.0 + 0.5); - float lat = latitude, lon = longitude; // one consistent snapshot of the fix - c.have_pos = astro_pos_ok(lat, lon); - if (c.have_pos) { - c.lat_show = lat; - c.lon_show = lon; - double az, el, rise = 0, set = 0, noon = 0; - sun_az_el(lat, lon, (double)currentTime, &az, &el); - int ia = (int)(az + 0.5); if (ia >= 360) ia -= 360; - c.az = (int16_t)ia; - c.el = (int16_t)(el < 0 ? el - 0.5 : el + 0.5); - c.sun_up_today = (sun_times(lat, lon, (double)currentTime, - &rise, &set, &noon, 0, 0, 0) == 0); - c.noon_min = (int16_t)astro_local_minutes(noon); // noon is valid even at the poles - if (c.sun_up_today) { - c.rise_min = (int16_t)astro_local_minutes(rise); - c.set_min = (int16_t)astro_local_minutes(set); - } - maidenhead(lat, lon, c.grid); - } else { - strcpy(c.grid, "----"); - } - __disable_irq(); - astro = c; - __enable_irq(); -} - -void sendDate( _Bool now ){ - if (waitingForLatch) { - if (countMode==COUNT_HIDDEN) { - // if we've entered count_hidden while waiting for latch, it will never happen - sendLatch() - waitingForLatch=0; - } else { - resendDate=1; - return; - } - } - - uint8_t i = 10; - HAL_UART_AbortTransmit(&huart2); - uart2_tx_buffer[0] = CMD_LOAD_TEXT; - - switch (displayMode) { - default: - case MODE_ISO8601_STD: - uart2_tx_buffer[1] ='2'; - uart2_tx_buffer[2] ='0'; - uart2_tx_buffer[3] ='0'+nextBcd.tenYears; - uart2_tx_buffer[4] ='0'+nextBcd.years; - uart2_tx_buffer[5] ='-'; - uart2_tx_buffer[6] ='0'+nextBcd.tenMonths; - uart2_tx_buffer[7] ='0'+nextBcd.months; - uart2_tx_buffer[8] ='-'; - uart2_tx_buffer[9] ='0'+nextBcd.tenDays; - uart2_tx_buffer[10]='0'+nextBcd.days; - break; -#ifdef NONCOMPLIANT_DATE_MODES - case MODE_DDMMYYYY: - uart2_tx_buffer[1] ='0'+nextBcd.tenDays; - uart2_tx_buffer[2] ='0'+nextBcd.days; - uart2_tx_buffer[3] ='-'; - uart2_tx_buffer[4] ='0'+nextBcd.tenMonths; - uart2_tx_buffer[5] ='0'+nextBcd.months; - uart2_tx_buffer[6] ='-'; - uart2_tx_buffer[7] ='2'; - uart2_tx_buffer[8] ='0'; - uart2_tx_buffer[9] ='0'+nextBcd.tenYears; - uart2_tx_buffer[10]='0'+nextBcd.years; - break; -#endif - case MODE_ISO_ORDINAL: - uart2_tx_buffer[1] ='2' ;//-2+nextBcd.seconds; - uart2_tx_buffer[2] ='0'; - uart2_tx_buffer[3] ='0'+nextBcd.tenYears; - uart2_tx_buffer[4] ='0'+nextBcd.years; - uart2_tx_buffer[5] ='-'; - i = 5 + sprintf((char*)&uart2_tx_buffer[6], "%d", tm_yday+1); - break; - case MODE_ISO_WEEK: - i = sprintf((char*)&uart2_tx_buffer[1], "%d-W%d-%d", iso_year, iso_week, iso_wday+1); - break; - case MODE_UNIX: - i = sprintf((char*)&uart2_tx_buffer[1], "%010ld", (uint32_t)currentTime); - break; - case MODE_JULIAN_DATE: - i = sprintf((char*)&uart2_tx_buffer[1], "%10f", (double)currentTime/86400.0 + 2440587.5 ); - break; - case MODE_MODIFIED_JD: - i = sprintf((char*)&uart2_tx_buffer[1], "%10f", (double)currentTime/86400.0 + 40587); - break; - case MODE_SHOW_OFFSET: - // This probably isn't the best place to do it, but the data is static anyway - - if (currentOffset<0){ - buffer_b[0]=bCat0 | 0b0000000000; - buffer_b[1]=bCat1 | 0b0100000000; - } else { - buffer_b[0]=bCat0 | 0b0100011000; - buffer_b[1]=bCat1 | 0b0111000000; - } - int minutes = ((abs(currentOffset)/60) %60); - int hours = (abs(currentOffset)/3600); - - buffer_b[2]=bCat2 | bLut[ hours/10 ]; - buffer_b[3]=bCat3 | bLut[ hours%10 ]; - buffer_b[4]=bCat4 | bLut[ minutes/10 ]; - - buffer_c[0].low= cLut[ minutes%10 ]; - buffer_c[0].high=0b11001110; - buffer_c[1].low=0; - buffer_c[2].low=0; - buffer_c[3].low=0; - - uart2_tx_buffer[1] ='u'; - uart2_tx_buffer[2] ='t'; - uart2_tx_buffer[3] ='c'; - uart2_tx_buffer[4] =' '; - uart2_tx_buffer[5] ='o'; - uart2_tx_buffer[6] ='f'; - uart2_tx_buffer[7] ='f'; - uart2_tx_buffer[8] ='s'; - uart2_tx_buffer[9] ='e'; - uart2_tx_buffer[10]='t'; - break; - case MODE_SHOW_TZ_NAME: - if (loadedRulesString[0]) { - char * zo = loadedRulesString; - while (*zo && *zo != '/') zo++; - if (currentTime%4 <2) { - zo++; - i = snprintf((char*)&uart2_tx_buffer[1], 11,"%s", zo); - } else { - i = zo-loadedRulesString; - if (i>10) i=10; -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wformat-truncation" - snprintf((char*)&uart2_tx_buffer[1], i+1,"%s", loadedRulesString); -#pragma GCC diagnostic pop - } - } else { - uart2_tx_buffer[1]='-'; - i=1; - } - break; - case MODE_WEEKDAY: - i = sprintf((char*)&uart2_tx_buffer[1], "%s", wday_str[tm_wday]); - break; - case MODE_WEEKDA_DD: - sprintf((char*)&uart2_tx_buffer[1], "%-7.7s ", wday_str[tm_wday]); - uart2_tx_buffer[9] ='0'+nextBcd.tenDays; - uart2_tx_buffer[10]='0'+nextBcd.days; - break; - case MODE_WDY_MM_DD: - sprintf((char*)&uart2_tx_buffer[1], "%.4s ", wday_str[tm_wday]); - uart2_tx_buffer[6] ='0'+nextBcd.tenMonths; - uart2_tx_buffer[7] ='0'+nextBcd.months; - uart2_tx_buffer[8] ='-'; - uart2_tx_buffer[9] ='0'+nextBcd.tenDays; - uart2_tx_buffer[10]='0'+nextBcd.days; - break; - case MODE_SATVIEW: - if (satview[SV_GPS_L1]==255 && satview[SV_GPS_UNKNOWN]==255) { - i = sprintf((char*)&uart2_tx_buffer[1], "GPS -"); - } else { - uint8_t GPS_sv = 0, GLONASS_sv = 0, GALILEO_sv = 0, BEIDOU_sv = 0; - if (satview[SV_GPS_L1]!=255) GPS_sv += satview[SV_GPS_L1]; - if (satview[SV_GPS_UNKNOWN]!=255) GPS_sv += satview[SV_GPS_UNKNOWN]; - if (satview[SV_GLONASS_L1]!=255) GLONASS_sv += satview[SV_GLONASS_L1]; - if (satview[SV_GLONASS_UNKNOWN]!=255) GLONASS_sv += satview[SV_GLONASS_UNKNOWN]; - if (satview[SV_GALILEO_E1]!=255) GALILEO_sv += satview[SV_GALILEO_E1]; - if (satview[SV_GALILEO_UNKNOWN]!=255) GALILEO_sv += satview[SV_GALILEO_UNKNOWN]; - if (satview[SV_BEIDOU_B1]!=255) BEIDOU_sv += satview[SV_BEIDOU_B1]; - if (satview[SV_BEIDOU_UNKNOWN]!=255) BEIDOU_sv += satview[SV_BEIDOU_UNKNOWN]; - - if (GLONASS_sv>0 && GLONASS_sv>=GALILEO_sv && GLONASS_sv>=BEIDOU_sv) { - i = sprintf((char*)&uart2_tx_buffer[1], "GPS %d L%d", GPS_sv, GLONASS_sv); - } else if (GALILEO_sv>0 && GALILEO_sv>=GLONASS_sv && GALILEO_sv>=BEIDOU_sv){ - i = sprintf((char*)&uart2_tx_buffer[1], "GPS %d A%d", GPS_sv, GALILEO_sv); - } else if (BEIDOU_sv>0 && BEIDOU_sv>=GLONASS_sv && BEIDOU_sv>=GALILEO_sv){ - i = sprintf((char*)&uart2_tx_buffer[1], "GPS %d b%d", GPS_sv, BEIDOU_sv); - } else { - i = sprintf((char*)&uart2_tx_buffer[1], "GPS %d -", GPS_sv); - } - } - break; - case MODE_STANDBY: - return; - case MODE_COUNTDOWN: - i = sprintf((char*)&uart2_tx_buffer[1], "t-%7ldd", countdown_days); - break; - case MODE_DEBUG_BRIGHTNESS: - i = sprintf((char*)&uart2_tx_buffer[1], "%04d %04d", (int)ADC1->DR, 4095-(int)dac_target); - break; - case MODE_DEBUG_RTC: - i = sprintf((char*)&uart2_tx_buffer[1], "rtc %d", debug_rtc_val); - break; - case MODE_TEXT: - if (textDisplay[0]) { - i = snprintf((char*)&uart2_tx_buffer[1], 30,"%s", textDisplay); - } else { - uart2_tx_buffer[1]='-'; - i=1; - } - break; - case MODE_VBAT: - if (vbat == 0.0) { - i = sprintf((char*)&uart2_tx_buffer[1], "bat -"); - } else { - i = sprintf((char*)&uart2_tx_buffer[1], "bat %.4f", vbat); - } - break; - case MODE_TTFF: - // Our assumption is that uwTick is zero at power on - if (!had_pps) time_till_first_fix = (int)(uwTick/1000); - i = sprintf((char*)&uart2_tx_buffer[1], "ttff %3d.%02d", (int)(time_till_first_fix/60), (int)(time_till_first_fix%60)); - break; - case MODE_DISPLAYTEST: - int nn = currentTime%10; - - TIM2->CCR1 = 0; - TIM2->CCR2 = 0; - buffer_c[0].high &= ~cSegDP; - buffer_c[1].high &= ~cSegDP; - buffer_c[2].high &= ~cSegDP; - buffer_c[3].high &= ~cSegDP; - - if ((currentTime%20)<10) { - uart2_tx_buffer[1] = - uart2_tx_buffer[2] = - uart2_tx_buffer[3] = - uart2_tx_buffer[4] = - uart2_tx_buffer[5] = - uart2_tx_buffer[6] = - uart2_tx_buffer[7] = - uart2_tx_buffer[8] = - uart2_tx_buffer[9] = - uart2_tx_buffer[10]= '0'+ nn; - - buffer_b[0]=bCat0 | bLut[ nn ]; - buffer_b[1]=bCat1 | bLut[ nn ]; - buffer_b[2]=bCat2 | bLut[ nn ]; - buffer_b[3]=bCat3 | bLut[ nn ]; - buffer_b[4]=bCat4 | bLut[ nn ]; - - buffer_c[0].low= cLut[ nn ]; - buffer_c[1].low=cLut[ nn ]; - buffer_c[2].low=cLut[ nn ]; - buffer_c[3].low=cLut[ nn ]; - - if ((currentTime%2) ==0) { - TIM2->CCR2 = 300; - } else { - TIM2->CCR1 = 300; - } - } else { - - buffer_b[0]=bCat0 | (nn==0?bLut[8]:0); - buffer_b[1]=bCat1 | (nn==1?bLut[8]:0); - buffer_b[2]=bCat2 | (nn==2?bLut[8]:0); - buffer_b[3]=bCat3 | (nn==3?bLut[8]:0); - buffer_b[4]=bCat4 | (nn==4?bLut[8]:0); - buffer_c[0].low=(nn==5?cLut[8]:0); - buffer_c[1].low=(nn==6?cLut[8]:0); - buffer_c[2].low=(nn==7?cLut[8]:0); - buffer_c[3].low=(nn==8?cLut[8]:0); - - if (nn>=5) buffer_c[nn-5].high |= cSegDP; - - i = sprintf((char*)&uart2_tx_buffer[1], "%*s8.", nn, ""); - } - - break; - case MODE_FIRMWARE_CRC_T: - { - extern uint32_t _app_crc[]; - uint32_t fwt = byteswap32(_app_crc[0]); - i = sprintf((char*)&uart2_tx_buffer[1], "t %08lx", fwt); - } - break; - case MODE_FIRMWARE_CRC_D: - uart2_tx_buffer[0]=CMD_SHOW_CRC; - break; - - // --- Astro pack: format the main-loop-computed cache onto the date row only, - // leaving the time row as the running clock (SATVIEW-style). -------------- - case MODE_SUN: { - if (!astro.have_pos || !astro.epoch) { i = sprintf((char*)&uart2_tx_buffer[1], "RISE ----"); break; } - int page = (currentTime / 2) % 3; // rise -> set -> solar noon, 2 s each - // labels padded to 4 chars in the literal ("SET "/"SOL ") so the time digits - // line up under RISE without relying on the nano printf honouring "%-4s" - const char *lbl = page == 0 ? "RISE" : page == 1 ? "SET " : "SOL "; - int m = page == 0 ? astro.rise_min : page == 1 ? astro.set_min : astro.noon_min; - if (!astro.sun_up_today && page != 2) { // sun never rises/sets today - i = sprintf((char*)&uart2_tx_buffer[1], "%s ----", lbl); - } else { - i = sprintf((char*)&uart2_tx_buffer[1], "%s %02d.%02d", lbl, m / 60, m % 60); - } - break; - } - case MODE_SUN_AZEL: - if (!astro.have_pos || !astro.epoch) { i = sprintf((char*)&uart2_tx_buffer[1], "AZ -- EL--"); } - else if (astro.el < 0) i = sprintf((char*)&uart2_tx_buffer[1], "AZ%03dEL-%02d", astro.az, -astro.el); - else i = sprintf((char*)&uart2_tx_buffer[1], "AZ%03dEL%02d", astro.az, astro.el); - break; - case MODE_MOON: // UTC only; no fix needed - if (!astro.epoch) i = sprintf((char*)&uart2_tx_buffer[1], "MOON -"); - else i = sprintf((char*)&uart2_tx_buffer[1], "MOON %d %3d", astro.moon_idx, astro.moon_pct); - break; - case MODE_GRID: - i = sprintf((char*)&uart2_tx_buffer[1], "%s", astro.epoch ? astro.grid : "----"); - break; - case MODE_LATLON: - if (!astro.have_pos || !astro.epoch) { i = sprintf((char*)&uart2_tx_buffer[1], "LAT ----"); } - else if ((currentTime / 2) % 2 == 0) { // page latitude / longitude, 2 s each - long h = (long)(astro.lat_show * 100.0 + (astro.lat_show < 0 ? -0.5 : 0.5)); // hundredths, rounded - if (h < 0) i = sprintf((char*)&uart2_tx_buffer[1], "LAT-%ld.%02ld", -h / 100, -h % 100); - else i = sprintf((char*)&uart2_tx_buffer[1], "LAT %ld.%02ld", h / 100, h % 100); - } else { - long h = (long)(astro.lon_show * 100.0 + (astro.lon_show < 0 ? -0.5 : 0.5)); - if (h < 0) i = sprintf((char*)&uart2_tx_buffer[1], "LON-%ld.%02ld", -h / 100, -h % 100); - else i = sprintf((char*)&uart2_tx_buffer[1], "LON %ld.%02ld", h / 100, h % 100); - } - break; - } - if (now) { - uart2_tx_buffer[++i]= CMD_RELOAD_TEXT; - } else { - uart2_tx_buffer[++i]= '\n'; - waitingForLatch=1; - } - HAL_UART_Transmit_DMA(&huart2, uart2_tx_buffer, i+1); - -} - -void setNextTimestamp(time_t nextTime){ - - int32_t offset = 0; - for (uint8_t i=0; i< MAX_RULES; i++) { - if (rules[i].t <= nextTime) offset=rules[i].offset; - else break; - } - // in case of the remote chance that we're interrupted while calculating, - // don't assign to currentOffset until the end of the loop - currentOffset = offset; - nextTime += offset; - - struct tm * nextTm = gmtime( &nextTime ); - tmToBcd( nextTm, &nextBcd ); - tm_yday = nextTm->tm_yday; - tm_wday = nextTm->tm_wday; - - if (displayMode == MODE_ISO_WEEK){ - iso_wday = (nextTm->tm_wday + 6) % 7; - nextTm->tm_mday -= iso_wday -3; - mktime(nextTm); - iso_year = nextTm->tm_year + 1900; - iso_week = nextTm->tm_yday/7 + 1; - } - - next7seg.c = cLut[nextBcd.seconds]; - - next7seg.b[0] = bCat0 | cLut[nextBcd.tenHours]<<2; - next7seg.b[1] = bCat1 | cLut[nextBcd.hours]<<2; - next7seg.b[2] = bCat2 | cLut[nextBcd.tenMinutes]<<2; - next7seg.b[3] = bCat3 | cLut[nextBcd.minutes]<<2; - next7seg.b[4] = bCat4 | cLut[nextBcd.tenSeconds]<<2; - -} - -void setNextCountdown(time_t nextTime){ - - int64_t remaining; - if (config.countdown_to < nextTime) { - remaining = 0; - SetPPS( &PPS_NoUpdate ); // don't show 999 at the next pulse - - } else remaining = config.countdown_to - nextTime; - - uint64_t seconds = remaining % 60; - uint64_t minutes = remaining / 60; - uint64_t hours = minutes / 60; - minutes %= 60; - countdown_days = hours / 24; - hours %= 24; - - next7seg.b[0] = bCat0 | cLut[hours / 10]<<2; - next7seg.b[1] = bCat1 | cLut[hours % 10]<<2; - next7seg.b[2] = bCat2 | cLut[minutes / 10]<<2; - next7seg.b[3] = bCat3 | cLut[minutes % 10]<<2; - next7seg.b[4] = bCat4 | cLut[seconds / 10]<<2; - next7seg.c = cLut[seconds % 10]; -} - -// Store UTC on RTC -// need to also write zone into backup registers -// Only called at the start of a second, don't attempt to write subseconds. -void write_rtc(void){ - - RTC_DateTypeDef sdatestructure; - RTC_TimeTypeDef stimestructure; - bcdStamp_t cBcd; - struct tm * cTm = gmtime( ¤tTime ); - - tmToBcd( cTm, &cBcd ); - - sdatestructure.Year = (cBcd.tenYears<<4) | cBcd.years; - sdatestructure.Month = (cBcd.tenMonths<<4) | cBcd.months; - sdatestructure.Date = (cBcd.tenDays<<4) | cBcd.days; - sdatestructure.WeekDay = RTC_WEEKDAY_MONDAY; - - HAL_RTC_SetDate(&hrtc,&sdatestructure,RTC_FORMAT_BCD); - - stimestructure.Hours = (cBcd.tenHours<<4) | cBcd.hours; - stimestructure.Minutes = (cBcd.tenMinutes<<4) | cBcd.minutes; - stimestructure.Seconds = (cBcd.tenSeconds<<4) | cBcd.seconds; - stimestructure.SubSeconds = 0x00; - stimestructure.TimeFormat = RTC_HOURFORMAT12_AM; - stimestructure.DayLightSaving = RTC_DAYLIGHTSAVING_NONE ; - stimestructure.StoreOperation = RTC_STOREOPERATION_RESET; - - HAL_RTC_SetTime(&hrtc,&stimestructure,RTC_FORMAT_BCD); - - // Write zone info to backup registers - // There are 32 words of memory, 128 bytes - // First 8 words are the zone string including separator and null byte (always less than 32 bytes) - // Next 22 words is a chunk of the ruleset in use, i.e. 11 years - // Last two words are time of write, and time of last calibration - - uint8_t i; - for (i=0; i< MAX_RULES; i++) { - if (rules[i].t > currentTime) break; - } - if (i==0) return; //something has gone wrong, data invalid - i--; //include currently active rule - - char numRulesToStore = (i+11>=MAX_RULES-1)? (MAX_RULES-i)*2 : 22; - - memcpyword( (uint32_t*)&(RTC->BKP0R), (uint32_t*)loadedRulesString, 8 ); - memcpyword( (uint32_t*)&(RTC->BKP8R), (uint32_t*)&rules[i], numRulesToStore ); - - rtc_last_write = (uint32_t)currentTime; -} - -time_t bcdToTm(bcdStamp_t *in, struct tm *out ) { - out->tm_isdst = 0; - out->tm_sec = in->seconds + in->tenSeconds*10; - out->tm_min = in->minutes + in->tenMinutes*10; - out->tm_hour = in->hours + in->tenHours*10; - out->tm_mday = in->days + in->tenDays*10; - out->tm_mon = in->months + in->tenMonths*10 -1; - out->tm_year = in->years + in->tenYears*10 + 100; //Years since 1900 - - return mktime(out); -} -void tmToBcd(struct tm *in, bcdStamp_t *out ) { - out->tenYears = (in->tm_year-100) / 10; - out->years = (in->tm_year-100) % 10; - out->tenMonths = (in->tm_mon+1) / 10; - out->months = (in->tm_mon+1) % 10; - out->tenDays = in->tm_mday / 10; - out->days = in->tm_mday % 10; - out->tenHours = in->tm_hour / 10; - out->hours = in->tm_hour % 10; - out->tenMinutes = in->tm_min / 10; - out->minutes = in->tm_min % 10; - out->tenSeconds = in->tm_sec / 10; - out->seconds = in->tm_sec % 10; -} - -void decodeRMC(void){ - - // do checksum - uint8_t *c = &nmea[1], *end = &nmea[sizeof(nmea)]; - uint8_t sum=0; - - bcdStamp_t rmcBcd; - struct tm rmcTm; - - while (*c !='*') { - sum ^= *c; - if (*c==',') *c=0; - c++; - if(c==end) return; //checksum not found - } - - sprintf((char*)nmea, "%02X", sum); - if (nmea[0] != c[1] || nmea[1]!=c[2]) return; //checksum error - -#define nextField() while (*c && c!=end) c++; c++; - - c=&nmea[7]; // Time - - if (*c==0) return; // time not present - - rmcBcd.tenHours = *c++ -'0'; - rmcBcd.hours = *c++ -'0'; - rmcBcd.tenMinutes = *c++ -'0'; - rmcBcd.minutes = *c++ -'0'; - rmcBcd.tenSeconds = *c++ -'0'; - rmcBcd.seconds = *c++ -'0'; - - if (*c++ =='.') { // subseconds not always present - //if (*c!='0') printf("subseconds non-zero: %s\n", c); - } - nextField() // Navigation receiver warning - data_valid = (*c=='A'?1:0); - - float tempLatitude=-9999, tempLongitude=-9999; - - nextField() // Latitude deg - if (*c){ - tempLatitude = (float)(*c++ -'0')*10.0; - tempLatitude += (float)(*c++ -'0'); - tempLatitude += (float)atof((char*)c) / 60.0; - } - nextField() // Latitude N/S - if (*c =='S') tempLatitude =-tempLatitude; - - nextField() // Longitude deg - if (*c){ - tempLongitude = (float)(*c++ -'0')*100.0; - tempLongitude += (float)(*c++ -'0')*10.0; - tempLongitude += (float)(*c++ -'0'); - tempLongitude += (float)atof((char*)c) / 60.0; - } - nextField() // Longitude E/W - if (*c == 'W') tempLongitude =-tempLongitude; - - if (!config.fake_long && !config.fake_lat) { - longitude = tempLongitude; - latitude = tempLatitude; - new_position=1; - } - - nextField() // Speed over ground, Knots - nextField() // Course Made Good, True - nextField() // Date - - if (*c==0) return; // date not present - - rmcBcd.tenDays = *c++ -'0'; - rmcBcd.days = *c++ -'0'; - rmcBcd.tenMonths = *c++ -'0'; - rmcBcd.months = *c++ -'0'; - rmcBcd.tenYears = *c++ -'0'; - rmcBcd.years = *c++ -'0'; - - - // Immediately after power-up, the GPS module does not know the GPS time/UTC leapsecond offset, and makes a guess - // Even if it gets a fix and starts outputting PPS, the time can be off by a few seconds (usually 2 or 3 fast) - // Only make use of this invalid data if there is nothing else to go on - if ( data_valid || (!had_pps && !rtc_good) ) { - currentTime = bcdToTm( &rmcBcd, &rmcTm ); - - if (decisec >= 9) { - currentTime++; - // check we're not <2ms away from rollover - if (centisec==9 && millisec>7) return; - - // Under normal conditions, we should only be parsing nmea at around .300 to .400 - // USART1 preemption priority is currently 1, so we could be interrupted by systick here - setNextTimestamp( currentTime ); - sendDate(0); - } - } - -} - -void decodeGSV(uint8_t rec){ - unsigned int sv = (nmea[11]-'0')*10 + (nmea[12]-'0'); - uint8_t constellation = nmea[2]; - uint8_t signal_id; - - // signal ID is not always present in GSV (on M8Q) - - unsigned int num_fields = 0, r=0; - while (++rODR, bright); - HAL_DMA_Start(&hdma_tim7_up, (uint32_t)buffer_c, (uint32_t)&GPIOC->ODR, bright); -} - -void displayOff(void){ - - uart2_tx_buffer[0]=' '; //in case already waiting for latch - uart2_tx_buffer[1]= CMD_LOAD_TEXT; - uart2_tx_buffer[2]= CMD_RELOAD_TEXT; - HAL_UART_AbortTransmit(&huart2); - HAL_UART_Transmit_DMA(&huart2, uart2_tx_buffer, 3); - - HAL_TIM_PWM_Stop(&htim2, TIM_CHANNEL_1); - HAL_TIM_PWM_Stop(&htim2, TIM_CHANNEL_2); - - HAL_DMA_Abort(&hdma_tim1_up); - HAL_DMA_Abort(&hdma_tim7_up); - GPIOB->ODR=0; - GPIOC->ODR=0; -} -void displayOn(void){ - HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); - HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2); - setDisplayPWM(5); -} - -void setDisplayFreq(uint32_t freq){ - if (waitingForLatch) { - delayedDisplayFreq = freq; - return; - } - - if (freq<1000 || freq>100000) {delayedDisplayFreq=0; return;} - - uint8_t tx_buf[4]; - tx_buf[0]= CMD_SET_FREQUENCY; - tx_buf[1]= (freq>>14) & 0x7F; - tx_buf[2]= (freq>>7) & 0x7F; - tx_buf[3]= (freq) & 0x7F; - if (HAL_UART_Transmit(&huart2, tx_buf, 4, 2) == HAL_OK) { - delayedDisplayFreq = 0; - } - - uint32_t arr = round(16000000.0 / (float)freq) -1.0; - - TIM1->ARR = arr; - TIM7->ARR = arr; -} - -#define colonAnimationStart() \ - TIM5->CNT=0; \ - HAL_DMA_Start(&hdma_tim5_ch1, (uint32_t)buffer_colons_L, (uint32_t)&TIM2->CCR1, 200); \ - HAL_DMA_Start(&hdma_tim5_ch2, (uint32_t)buffer_colons_R, (uint32_t)&TIM2->CCR2, 200); - -#define colonAnimationStop() \ - HAL_DMA_Abort(&hdma_tim5_ch1); \ - HAL_DMA_Abort(&hdma_tim5_ch2); - -#define colonAnimationSync() \ - colonAnimationStop() \ - colonAnimationStart() - -void loadColonAnimation(void){ - - - switch (colonMode) { - case COLON_MODE_SLOWFADE: - for (int k=0;k<100;k++) { - buffer_colons_R[k] = - buffer_colons_L[k] = k*2; - buffer_colons_R[k+100] = - buffer_colons_L[k+100] = 198-k*2; - } - break; - case COLON_MODE_HEARTBEAT: - for (int k=0;k<50;k++) { - buffer_colons_L[k] = k*4; - } - for (int k=0;k<100;k++) { - buffer_colons_L[k+50] = 200 - k*2; - } - for (int k=0;k<50;k++) { - buffer_colons_L[k+150] = 0; - } - for (int k=0;k<200;k++) { - buffer_colons_R[k] = buffer_colons_L[(k+175)%200]; - } - - break; - case COLON_MODE_1PPS_SAWTOOTH: - for (int k=0;k<100;k++) { - buffer_colons_R[k] = - buffer_colons_L[k] = 196-(k*k)/50; - buffer_colons_R[k+100] = - buffer_colons_L[k+100] = 196-(k*k)/50; - } - break; - case COLON_MODE_ALT_SAWTOOTH: - for (int k=0;k<100;k++) { - buffer_colons_R[k] = 0; - buffer_colons_L[k+100] = 0; - buffer_colons_L[k] = 196-(k*k)/50; - buffer_colons_R[k+100] = 196-(k*k)/50; - } - break; - case COLON_MODE_TOGGLE: - for (int k=0;k<100;k++) { - buffer_colons_R[k] = 200; - buffer_colons_L[k] = 200; - buffer_colons_R[k+100] = 0; - buffer_colons_L[k+100] = 0; - } - break; - case COLON_MODE_SOLID: - for (int k=0;k<200;k++) { - buffer_colons_R[k] = 200; - buffer_colons_L[k] = 200; - } - break; - } - -} - -_Bool truthy(char const* str){ - if (strcasecmp(str, "on")==0) return 1; - if (strcasecmp(str, "enabled")==0) return 1; - if (strcasecmp(str, "1")==0) return 1; - return 0; -} - -_Bool falsey(char const* str){ - if (strcasecmp(str, "off")==0) return 1; - if (strcasecmp(str, "disabled")==0) return 1; - if (strcasecmp(str, "0")==0) return 1; - if (strcasecmp(str, "none")==0) return 1; - return 0; -} - -// Accept a float between 0.0 and 1.0, or an int from 0 to 4096 -float parseBrightness(char *v, _Bool invert){ - if (!v[0]) return -1; - float b = strtof(v, NULL); - if (!isfinite(b) || b<0.0) return -1; - if (b<=1.0 && v[1]=='.') - return invert? (1.0-b) * 4095 : b*4095; - if (b<=4095) - return invert? 4095-b : b; - return -1; -} - -#define set_mode_enabled(mode, value) \ - if ((config.modes_enabled[mode] = truthy(value))) requestMode=mode; - -void parseConfigString(char *key, char *value) { - - if (strcasecmp(key, "text") == 0) { - - strcpy(textDisplay, value); - - } else if (strcasecmp(key, "MATRIX_FREQUENCY") == 0) { - - setDisplayFreq(atoi(value)); - - } else if (strcasecmp(key, "zone_override") == 0) { - - if (!value[0] || delayedLoadRules) return; - - strcpy(preloadRulesString, value); - delayedLoadRules=1; - ZDAbort(); - - } else if (strcasecmp(key, "brightness") == 0) { - - config.brightness_override = parseBrightness(value, 1); - - } else if (strcasecmp(key, "countdown_to") == 0) { - - // support fractional seconds?? - struct tm t = {0}; - if( sscanf(value, "%d-%d-%dT%d:%d:%dZ", &t.tm_year, &t.tm_mon, &t.tm_mday, &t.tm_hour, &t.tm_min, &t.tm_sec) >=3) { - - if (t.tm_year > 9999) return; // arbitrary cutoff, ~3e6 days - t.tm_year -= 1900; - t.tm_mon -= 1; - - config.countdown_to = mktime(&t) -1; - - } - } else if (strcasecmp(key, "MODE_ISO8601_STD") == 0) { - set_mode_enabled(MODE_ISO8601_STD, value); - } else if (strcasecmp(key, "MODE_ISO_ORDINAL") == 0) { - set_mode_enabled(MODE_ISO_ORDINAL, value); - } else if (strcasecmp(key, "MODE_ISO_WEEK") == 0) { - set_mode_enabled(MODE_ISO_WEEK, value); - } else if (strcasecmp(key, "MODE_UNIX") == 0) { - set_mode_enabled(MODE_UNIX, value); - } else if (strcasecmp(key, "MODE_JULIAN_DATE") == 0) { - set_mode_enabled(MODE_JULIAN_DATE, value); - } else if (strcasecmp(key, "MODE_MODIFIED_JD") == 0) { - set_mode_enabled(MODE_MODIFIED_JD, value); - } else if (strcasecmp(key, "MODE_SHOW_OFFSET") == 0) { - set_mode_enabled(MODE_SHOW_OFFSET, value); - } else if (strcasecmp(key, "MODE_SHOW_TZ_NAME") == 0) { - set_mode_enabled(MODE_SHOW_TZ_NAME, value); - } else if (strcasecmp(key, "MODE_WEEKDAY") == 0) { - set_mode_enabled(MODE_WEEKDAY, value); - } else if (strcasecmp(key, "MODE_WEEKDA_DD") == 0) { - set_mode_enabled(MODE_WEEKDA_DD, value); - } else if (strcasecmp(key, "MODE_WDY_MM_DD") == 0) { - set_mode_enabled(MODE_WDY_MM_DD, value); - } else if (strcasecmp(key, "MODE_STANDBY") == 0) { - set_mode_enabled(MODE_STANDBY, value); - } else if (strcasecmp(key, "MODE_COUNTDOWN") == 0) { - set_mode_enabled(MODE_COUNTDOWN, value); - } else if (strcasecmp(key, "MODE_SATVIEW") == 0) { - set_mode_enabled(MODE_SATVIEW, value); - } else if (strcasecmp(key, "MODE_DEBUG_BRIGHTNESS") == 0) { - set_mode_enabled(MODE_DEBUG_BRIGHTNESS, value); - } else if (strcasecmp(key, "MODE_DEBUG_RTC") == 0) { - set_mode_enabled(MODE_DEBUG_RTC, value); - } else if (strcasecmp(key, "MODE_TEXT") == 0) { - set_mode_enabled(MODE_TEXT, value); - } else if (strcasecmp(key, "MODE_VBAT") == 0) { - set_mode_enabled(MODE_VBAT, value); - } else if (strcasecmp(key, "MODE_DISPLAYTEST") == 0) { - set_mode_enabled(MODE_DISPLAYTEST, value); - } else if (strcasecmp(key, "MODE_TTFF") == 0) { - set_mode_enabled(MODE_TTFF, value); -#ifdef NONCOMPLIANT_DATE_MODES - } else if (strcasecmp(key, "MODE_DDMMYYYY") == 0) { - set_mode_enabled(MODE_DDMMYYYY, value); -#endif - } else if (strcasecmp(key, "MODE_FIRMWARE_CRC") == 0) { - set_mode_enabled(MODE_FIRMWARE_CRC_D, value); - set_mode_enabled(MODE_FIRMWARE_CRC_T, value); - } else if (strcasecmp(key, "MODE_SUN") == 0) { - set_mode_enabled(MODE_SUN, value); - } else if (strcasecmp(key, "MODE_SUN_AZEL") == 0) { - set_mode_enabled(MODE_SUN_AZEL, value); - } else if (strcasecmp(key, "MODE_MOON") == 0) { - set_mode_enabled(MODE_MOON, value); - } else if (strcasecmp(key, "MODE_GRID") == 0) { - set_mode_enabled(MODE_GRID, value); - } else if (strcasecmp(key, "MODE_LATLON") == 0) { - set_mode_enabled(MODE_LATLON, value); - } else if (strcasecmp(key, "Tolerance_time_1ms") == 0) { - config.tolerance_1ms = atoi(value); - } else if (strcasecmp(key, "Tolerance_time_10ms") == 0) { - config.tolerance_10ms = atoi(value); - } else if (strcasecmp(key, "Tolerance_time_100ms") == 0) { - config.tolerance_100ms = atoi(value); - } else if (strcasecmp(key, "fake_longitude") == 0) { - config.fake_long = atof(value); - } else if (strcasecmp(key, "fake_latitude") == 0) { - config.fake_lat = atof(value); - } else if (strcasecmp(key, "colon_mode") == 0) { - - if (strcasecmp(value, "solid") == 0) { - colonMode = COLON_MODE_SOLID; - } else if (strcasecmp(value, "heartbeat") == 0) { - colonMode = COLON_MODE_HEARTBEAT; - } else if (strcasecmp(value, "sawtooth") == 0) { - colonMode = COLON_MODE_1PPS_SAWTOOTH; - } else if (strcasecmp(value, "alt_sawtooth") == 0) { - colonMode = COLON_MODE_ALT_SAWTOOTH; - } else if (strcasecmp(value, "toggle") == 0) { - colonMode = COLON_MODE_TOGGLE; - } else colonMode = COLON_MODE_SLOWFADE; - - } else if (strcasecmp(key, "nmea") == 0) { - - if (falsey(value)) { - nmea_cdc_level = NMEA_NONE; - } else if (strcasecmp(value, "rmc") == 0) { - nmea_cdc_level = NMEA_RMC; - } else nmea_cdc_level = NMEA_ALL; - - } else if (strcasecmp(key, "pps") == 0) { - - pps_ts_enabled = truthy(value); // emit a $PMTXTS timing sentence on each PPS edge - - } else if (key[0]=='B' && key[1]=='S' && key[3]==0) { //BS1, BS2, etc - if (!key[2] || key[2]<'1' || key[2]>'0'+sizeof(brightnessCurve)/sizeof(brightnessCurve[0])) return; - - char *c = &value[0]; - while (*c++) if(*c==',') break; - if (*c==0) return; - *c=0; c++; - - float in = parseBrightness(value,0); - float out = parseBrightness(c,1); - if (in<0 || out<0) return; - - brightnessCurve[key[2]-'1'].in = in; - brightnessCurve[key[2]-'1'].out = out; - - } - -} - -void postConfigCleanup(void){ - loadColonAnimation(); - - // check at least one mode is enabled - uint8_t j = 0; - for (uint8_t i=0; i=2)) { - parseConfigString(key, value); - postConfigCleanup(); - } - k=0; - v=0; - state=0; - return; - } - - switch (state) { - case 0: // read key - if (k) { - if (c=='=') {state =2; break;} - if (c==' ' || c=='\t') {state =1; break;} - } - key[k++] = c; - if (k==31) k--; - break; - case 1: // whitespace - if (c=='=') state=2; - else if (c!=' ' && c!='\t') {state=0; k=0; key[k++]=c;} - break; - case 2: //second whitespace - if (c!=' ' && c!='\t' && c!='=') {state=3; value[v++]=c;} - break; - case 3: - value[v++]=c; - if (v==31) v--; - } -} - -void readConfigFile(void){ - -#ifdef CHECK_CONFIG_MTIME - FILINFO fno; - if (f_stat(CONFIG_FILENAME, &fno) == FR_OK) { - // if unchanged, exit early before touching any config - // if the file doesn't exist, fall through and fail on the f_open - if (fno.fdate==config.fdate && fno.ftime==config.ftime) return; - config.fdate=fno.fdate; - config.ftime=fno.ftime; - } -#endif - - config.tolerance_1ms = 1000; - config.tolerance_10ms = 10000; - config.tolerance_100ms = 100000; - config.zone_override = 0; - config.brightness_override = -1.0; - colonMode = 0; - - FIL file; - - if (f_open(&file, CONFIG_FILENAME, FA_READ) != FR_OK) { - postConfigCleanup(); - return; - } - - char key[32], value[32], s[1]; - unsigned int rc; - uint16_t col=0; - - - while (1) { - f_read(&file, s, 1, &rc); - if (rc!=1) break; //EOF - - if (s[0]=='\r' || s[0]=='\n') { col=0; continue; } //EOL - - if (col==0 && (s[0]=='#' || s[0]==';')) { // comments - while (rc && s[0]!='\n') f_read(&file, s, 1, &rc); - continue; - } - - if (s[0]!='=') { - if (col CAL_PERIOD) { - - LPTIM1_high=0; - LL_LPTIM_StartCounter(LPTIM1, LL_LPTIM_OPERATING_MODE_CONTINUOUS); - calibStart = currentTime; - - } else if ((uint32_t)currentTime - calibStart == CAL_PERIOD) { - volatile uint16_t x = LPTIM1->CNT; - volatile uint16_t y = LPTIM1->CNT; - if (x!=y) goto skipRtcCal; - - int32_t error = ((LPTIM1_high<<16) + x) - 32768*CAL_PERIOD + LPTIM_START_DELAY; - float e = (float)error * 32.0 / CAL_PERIOD; - - debug_rtc_val = error;//0x100 + round(e); - - if (e>255.0 || e< -255.0) goto skipRtcCal; - - __HAL_RTC_WRITEPROTECTION_DISABLE(&hrtc); - RTC->CALR = 0x100 + (int)round(e); - __HAL_RTC_WRITEPROTECTION_ENABLE(&hrtc); - rtc_last_calibration = (uint32_t)currentTime; - -skipRtcCal: - // Prepare the counter for the next calibration - // LPTIM1->CNT is read only, the only way to zero it is to disable and re-enable the timer. - // There is a further delay associated with this, better to put it here than right at the moment we want to start the timer. - LPTIM1->CR &= ~LPTIM_CR_ENABLE; - LPTIM1->CR |= LPTIM_CR_ENABLE; - LL_LPTIM_SetAutoReload(LPTIM1, 0xFFFF); - LL_LPTIM_ClearFLAG_ARRM(LPTIM1); // just in case there's one pending - } -} - -void EXTI9_5_IRQHandler(void){__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_7);} - -// Snapshot the timing state at the instant of the PPS edge. MUST run before SysTick->VAL is -// reloaded and before millisec/centisec/decisec are zeroed, so it captures the phase error -// between the firmware's modelled second and the true GPS edge. -#define capturePPS() do { \ +#define CHECK_CONFIG_MTIME + +struct { +#ifdef CHECK_CONFIG_MTIME + unsigned short fdate; + unsigned short ftime; +#endif + uint32_t tolerance_1ms; + uint32_t tolerance_10ms; + uint32_t tolerance_100ms; + float fake_long; + float fake_lat; + time_t countdown_to; + float brightness_override; + volatile _Bool zone_override; + _Bool modes_enabled[NUM_DISPLAY_MODES]; + +} config = {0}; + +struct { + float in; + float out; +} brightnessCurve[] = { + {0, 4095-0}, + {1425, 4095-737}, + {2566, 4095-1601}, + {3396, 4095-2725}, + {4095, 4095-4095}, +}; + +// memcpy() appears to move data by bytes, which doesn't work with the word-accessed backup registers +// here we explicitly move data a word at a time +void memcpyword(volatile uint32_t *dest, volatile uint32_t *src, size_t n){ + while (n--){ + dest[n] = src[n]; + } +} + +// 12 bytes at 115200 8E1 is 1.14ms, 32 bytes would be 3.06ms +// --- Astro pack helpers ---------------------------------------------------- +// A usable position is held in latitude/longitude from either a GPS fix or the +// configured fake_latitude/fake_longitude; both sit at the -9999 sentinel until +// a position is known, so a simple range check is the "have we got a fix" test. +static _Bool astro_pos_ok(float lat, float lon){ + return lat >= -90.0f && lat <= 90.0f && lon >= -180.0f && lon <= 180.0f; +} +// Decimal UTC hour (sun_times may return <0 or >24) -> local minutes-of-day [0,1440). +static int astro_local_minutes(double utc_h){ + double h = fmod(utc_h + currentOffset / 3600.0, 24.0); + if (h < 0) h += 24.0; + int m = (int)(h * 60.0 + 0.5); + if (m >= 1440) m -= 1440; + return m; +} +// Recompute the astro cache (called from the main loop, never the ISR). The +// double soft-float maths runs here, then the small result struct is swapped in +// under a brief IRQ mask so sendDate() always reads a consistent snapshot. +static void astro_update(void){ + if (astro.epoch == (uint32_t)currentTime) return; // at most once a second + struct astro_cache_s c = {0}; + c.epoch = (uint32_t)currentTime; + double ph = moon_phase((double)currentTime); // moon needs no fix + c.moon_idx = moon_phase_index(ph); + c.moon_pct = (uint8_t)(moon_illuminated_fraction(ph) * 100.0 + 0.5); + float lat = latitude, lon = longitude; // one consistent snapshot of the fix + c.have_pos = astro_pos_ok(lat, lon); + if (c.have_pos) { + c.lat_show = lat; + c.lon_show = lon; + double az, el, rise = 0, set = 0, noon = 0; + sun_az_el(lat, lon, (double)currentTime, &az, &el); + int ia = (int)(az + 0.5); if (ia >= 360) ia -= 360; + c.az = (int16_t)ia; + c.el = (int16_t)(el < 0 ? el - 0.5 : el + 0.5); + c.sun_up_today = (sun_times(lat, lon, (double)currentTime, + &rise, &set, &noon, 0, 0, 0) == 0); + c.noon_min = (int16_t)astro_local_minutes(noon); // noon is valid even at the poles + if (c.sun_up_today) { + c.rise_min = (int16_t)astro_local_minutes(rise); + c.set_min = (int16_t)astro_local_minutes(set); + } + maidenhead(lat, lon, c.grid); + } else { + strcpy(c.grid, "----"); + } + __disable_irq(); + astro = c; + __enable_irq(); +} + +void sendDate( _Bool now ){ + if (waitingForLatch) { + if (countMode==COUNT_HIDDEN) { + // if we've entered count_hidden while waiting for latch, it will never happen + sendLatch() + waitingForLatch=0; + } else { + resendDate=1; + return; + } + } + + uint8_t i = 10; + HAL_UART_AbortTransmit(&huart2); + uart2_tx_buffer[0] = CMD_LOAD_TEXT; + + switch (displayMode) { + default: + case MODE_ISO8601_STD: + uart2_tx_buffer[1] ='2'; + uart2_tx_buffer[2] ='0'; + uart2_tx_buffer[3] ='0'+nextBcd.tenYears; + uart2_tx_buffer[4] ='0'+nextBcd.years; + uart2_tx_buffer[5] ='-'; + uart2_tx_buffer[6] ='0'+nextBcd.tenMonths; + uart2_tx_buffer[7] ='0'+nextBcd.months; + uart2_tx_buffer[8] ='-'; + uart2_tx_buffer[9] ='0'+nextBcd.tenDays; + uart2_tx_buffer[10]='0'+nextBcd.days; + break; +#ifdef NONCOMPLIANT_DATE_MODES + case MODE_DDMMYYYY: + uart2_tx_buffer[1] ='0'+nextBcd.tenDays; + uart2_tx_buffer[2] ='0'+nextBcd.days; + uart2_tx_buffer[3] ='-'; + uart2_tx_buffer[4] ='0'+nextBcd.tenMonths; + uart2_tx_buffer[5] ='0'+nextBcd.months; + uart2_tx_buffer[6] ='-'; + uart2_tx_buffer[7] ='2'; + uart2_tx_buffer[8] ='0'; + uart2_tx_buffer[9] ='0'+nextBcd.tenYears; + uart2_tx_buffer[10]='0'+nextBcd.years; + break; +#endif + case MODE_ISO_ORDINAL: + uart2_tx_buffer[1] ='2' ;//-2+nextBcd.seconds; + uart2_tx_buffer[2] ='0'; + uart2_tx_buffer[3] ='0'+nextBcd.tenYears; + uart2_tx_buffer[4] ='0'+nextBcd.years; + uart2_tx_buffer[5] ='-'; + i = 5 + sprintf((char*)&uart2_tx_buffer[6], "%d", tm_yday+1); + break; + case MODE_ISO_WEEK: + i = sprintf((char*)&uart2_tx_buffer[1], "%d-W%d-%d", iso_year, iso_week, iso_wday+1); + break; + case MODE_UNIX: + i = sprintf((char*)&uart2_tx_buffer[1], "%010ld", (uint32_t)currentTime); + break; + case MODE_JULIAN_DATE: + i = sprintf((char*)&uart2_tx_buffer[1], "%10f", (double)currentTime/86400.0 + 2440587.5 ); + break; + case MODE_MODIFIED_JD: + i = sprintf((char*)&uart2_tx_buffer[1], "%10f", (double)currentTime/86400.0 + 40587); + break; + case MODE_SHOW_OFFSET: + // This probably isn't the best place to do it, but the data is static anyway + + if (currentOffset<0){ + buffer_b[0]=bCat0 | 0b0000000000; + buffer_b[1]=bCat1 | 0b0100000000; + } else { + buffer_b[0]=bCat0 | 0b0100011000; + buffer_b[1]=bCat1 | 0b0111000000; + } + int minutes = ((abs(currentOffset)/60) %60); + int hours = (abs(currentOffset)/3600); + + buffer_b[2]=bCat2 | bLut[ hours/10 ]; + buffer_b[3]=bCat3 | bLut[ hours%10 ]; + buffer_b[4]=bCat4 | bLut[ minutes/10 ]; + + buffer_c[0].low= cLut[ minutes%10 ]; + buffer_c[0].high=0b11001110; + buffer_c[1].low=0; + buffer_c[2].low=0; + buffer_c[3].low=0; + + uart2_tx_buffer[1] ='u'; + uart2_tx_buffer[2] ='t'; + uart2_tx_buffer[3] ='c'; + uart2_tx_buffer[4] =' '; + uart2_tx_buffer[5] ='o'; + uart2_tx_buffer[6] ='f'; + uart2_tx_buffer[7] ='f'; + uart2_tx_buffer[8] ='s'; + uart2_tx_buffer[9] ='e'; + uart2_tx_buffer[10]='t'; + break; + case MODE_SHOW_TZ_NAME: + if (loadedRulesString[0]) { + char * zo = loadedRulesString; + while (*zo && *zo != '/') zo++; + if (currentTime%4 <2) { + zo++; + i = snprintf((char*)&uart2_tx_buffer[1], 11,"%s", zo); + } else { + i = zo-loadedRulesString; + if (i>10) i=10; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wformat-truncation" + snprintf((char*)&uart2_tx_buffer[1], i+1,"%s", loadedRulesString); +#pragma GCC diagnostic pop + } + } else { + uart2_tx_buffer[1]='-'; + i=1; + } + break; + case MODE_WEEKDAY: + i = sprintf((char*)&uart2_tx_buffer[1], "%s", wday_str[tm_wday]); + break; + case MODE_WEEKDA_DD: + sprintf((char*)&uart2_tx_buffer[1], "%-7.7s ", wday_str[tm_wday]); + uart2_tx_buffer[9] ='0'+nextBcd.tenDays; + uart2_tx_buffer[10]='0'+nextBcd.days; + break; + case MODE_WDY_MM_DD: + sprintf((char*)&uart2_tx_buffer[1], "%.4s ", wday_str[tm_wday]); + uart2_tx_buffer[6] ='0'+nextBcd.tenMonths; + uart2_tx_buffer[7] ='0'+nextBcd.months; + uart2_tx_buffer[8] ='-'; + uart2_tx_buffer[9] ='0'+nextBcd.tenDays; + uart2_tx_buffer[10]='0'+nextBcd.days; + break; + case MODE_SATVIEW: + if (satview[SV_GPS_L1]==255 && satview[SV_GPS_UNKNOWN]==255) { + i = sprintf((char*)&uart2_tx_buffer[1], "GPS -"); + } else { + uint8_t GPS_sv = 0, GLONASS_sv = 0, GALILEO_sv = 0, BEIDOU_sv = 0; + if (satview[SV_GPS_L1]!=255) GPS_sv += satview[SV_GPS_L1]; + if (satview[SV_GPS_UNKNOWN]!=255) GPS_sv += satview[SV_GPS_UNKNOWN]; + if (satview[SV_GLONASS_L1]!=255) GLONASS_sv += satview[SV_GLONASS_L1]; + if (satview[SV_GLONASS_UNKNOWN]!=255) GLONASS_sv += satview[SV_GLONASS_UNKNOWN]; + if (satview[SV_GALILEO_E1]!=255) GALILEO_sv += satview[SV_GALILEO_E1]; + if (satview[SV_GALILEO_UNKNOWN]!=255) GALILEO_sv += satview[SV_GALILEO_UNKNOWN]; + if (satview[SV_BEIDOU_B1]!=255) BEIDOU_sv += satview[SV_BEIDOU_B1]; + if (satview[SV_BEIDOU_UNKNOWN]!=255) BEIDOU_sv += satview[SV_BEIDOU_UNKNOWN]; + + if (GLONASS_sv>0 && GLONASS_sv>=GALILEO_sv && GLONASS_sv>=BEIDOU_sv) { + i = sprintf((char*)&uart2_tx_buffer[1], "GPS %d L%d", GPS_sv, GLONASS_sv); + } else if (GALILEO_sv>0 && GALILEO_sv>=GLONASS_sv && GALILEO_sv>=BEIDOU_sv){ + i = sprintf((char*)&uart2_tx_buffer[1], "GPS %d A%d", GPS_sv, GALILEO_sv); + } else if (BEIDOU_sv>0 && BEIDOU_sv>=GLONASS_sv && BEIDOU_sv>=GALILEO_sv){ + i = sprintf((char*)&uart2_tx_buffer[1], "GPS %d b%d", GPS_sv, BEIDOU_sv); + } else { + i = sprintf((char*)&uart2_tx_buffer[1], "GPS %d -", GPS_sv); + } + } + break; + case MODE_STANDBY: + return; + case MODE_COUNTDOWN: + i = sprintf((char*)&uart2_tx_buffer[1], "t-%7ldd", countdown_days); + break; + case MODE_DEBUG_BRIGHTNESS: + i = sprintf((char*)&uart2_tx_buffer[1], "%04d %04d", (int)ADC1->DR, 4095-(int)dac_target); + break; + case MODE_DEBUG_RTC: + i = sprintf((char*)&uart2_tx_buffer[1], "rtc %d", debug_rtc_val); + break; + case MODE_TEXT: + if (textDisplay[0]) { + i = snprintf((char*)&uart2_tx_buffer[1], 30,"%s", textDisplay); + } else { + uart2_tx_buffer[1]='-'; + i=1; + } + break; + case MODE_VBAT: + if (vbat == 0.0) { + i = sprintf((char*)&uart2_tx_buffer[1], "bat -"); + } else { + i = sprintf((char*)&uart2_tx_buffer[1], "bat %.4f", vbat); + } + break; + case MODE_TTFF: + // Our assumption is that uwTick is zero at power on + if (!had_pps) time_till_first_fix = (int)(uwTick/1000); + i = sprintf((char*)&uart2_tx_buffer[1], "ttff %3d.%02d", (int)(time_till_first_fix/60), (int)(time_till_first_fix%60)); + break; + case MODE_DISPLAYTEST: + int nn = currentTime%10; + + TIM2->CCR1 = 0; + TIM2->CCR2 = 0; + buffer_c[0].high &= ~cSegDP; + buffer_c[1].high &= ~cSegDP; + buffer_c[2].high &= ~cSegDP; + buffer_c[3].high &= ~cSegDP; + + if ((currentTime%20)<10) { + uart2_tx_buffer[1] = + uart2_tx_buffer[2] = + uart2_tx_buffer[3] = + uart2_tx_buffer[4] = + uart2_tx_buffer[5] = + uart2_tx_buffer[6] = + uart2_tx_buffer[7] = + uart2_tx_buffer[8] = + uart2_tx_buffer[9] = + uart2_tx_buffer[10]= '0'+ nn; + + buffer_b[0]=bCat0 | bLut[ nn ]; + buffer_b[1]=bCat1 | bLut[ nn ]; + buffer_b[2]=bCat2 | bLut[ nn ]; + buffer_b[3]=bCat3 | bLut[ nn ]; + buffer_b[4]=bCat4 | bLut[ nn ]; + + buffer_c[0].low= cLut[ nn ]; + buffer_c[1].low=cLut[ nn ]; + buffer_c[2].low=cLut[ nn ]; + buffer_c[3].low=cLut[ nn ]; + + if ((currentTime%2) ==0) { + TIM2->CCR2 = 300; + } else { + TIM2->CCR1 = 300; + } + } else { + + buffer_b[0]=bCat0 | (nn==0?bLut[8]:0); + buffer_b[1]=bCat1 | (nn==1?bLut[8]:0); + buffer_b[2]=bCat2 | (nn==2?bLut[8]:0); + buffer_b[3]=bCat3 | (nn==3?bLut[8]:0); + buffer_b[4]=bCat4 | (nn==4?bLut[8]:0); + buffer_c[0].low=(nn==5?cLut[8]:0); + buffer_c[1].low=(nn==6?cLut[8]:0); + buffer_c[2].low=(nn==7?cLut[8]:0); + buffer_c[3].low=(nn==8?cLut[8]:0); + + if (nn>=5) buffer_c[nn-5].high |= cSegDP; + + i = sprintf((char*)&uart2_tx_buffer[1], "%*s8.", nn, ""); + } + + break; + case MODE_FIRMWARE_CRC_T: + { + extern uint32_t _app_crc[]; + uint32_t fwt = byteswap32(_app_crc[0]); + i = sprintf((char*)&uart2_tx_buffer[1], "t %08lx", fwt); + } + break; + case MODE_FIRMWARE_CRC_D: + uart2_tx_buffer[0]=CMD_SHOW_CRC; + break; + + // --- Astro pack: format the main-loop-computed cache onto the date row only, + // leaving the time row as the running clock (SATVIEW-style). -------------- + case MODE_SUN: { + if (!astro.have_pos || !astro.epoch) { i = sprintf((char*)&uart2_tx_buffer[1], "RISE ----"); break; } + int page = (currentTime / 2) % 3; // rise -> set -> solar noon, 2 s each + // labels padded to 4 chars in the literal ("SET "/"SOL ") so the time digits + // line up under RISE without relying on the nano printf honouring "%-4s" + const char *lbl = page == 0 ? "RISE" : page == 1 ? "SET " : "SOL "; + int m = page == 0 ? astro.rise_min : page == 1 ? astro.set_min : astro.noon_min; + if (!astro.sun_up_today && page != 2) { // sun never rises/sets today + i = sprintf((char*)&uart2_tx_buffer[1], "%s ----", lbl); + } else { + i = sprintf((char*)&uart2_tx_buffer[1], "%s %02d.%02d", lbl, m / 60, m % 60); + } + break; + } + case MODE_SUN_AZEL: + if (!astro.have_pos || !astro.epoch) { i = sprintf((char*)&uart2_tx_buffer[1], "AZ -- EL--"); } + else if (astro.el < 0) i = sprintf((char*)&uart2_tx_buffer[1], "AZ%03dEL-%02d", astro.az, -astro.el); + else i = sprintf((char*)&uart2_tx_buffer[1], "AZ%03dEL%02d", astro.az, astro.el); + break; + case MODE_MOON: // UTC only; no fix needed + if (!astro.epoch) i = sprintf((char*)&uart2_tx_buffer[1], "MOON -"); + else i = sprintf((char*)&uart2_tx_buffer[1], "MOON %d %3d", astro.moon_idx, astro.moon_pct); + break; + case MODE_GRID: + i = sprintf((char*)&uart2_tx_buffer[1], "%s", astro.epoch ? astro.grid : "----"); + break; + case MODE_LATLON: + // RISE/SET-style layout: label, separator space, a sign slot (space when positive), then + // the digits — numbers align whether signed or not, and short values keep clear space at + // the row's end. A 3-digit longitude can't fit both the separator and the sign slot in + // 10 chars, so the separator is dropped just for that case ("LON 179.99" / "LON-179.99"). + if (!astro.have_pos || !astro.epoch) { i = sprintf((char*)&uart2_tx_buffer[1], "LAT ----"); } + else { + _Bool lat = (currentTime / 2) % 2 == 0; // page latitude / longitude, 2 s each + double v = lat ? astro.lat_show : astro.lon_show; + long h = (long)(v * 100.0 + (v < 0 ? -0.5 : 0.5)); // hundredths, rounded + long a2 = h < 0 ? -h : h; + i = sprintf((char*)&uart2_tx_buffer[1], (a2 >= 10000) ? "%s%c%ld.%02ld" : "%s %c%ld.%02ld", + lat ? "LAT" : "LON", h < 0 ? '-' : ' ', a2 / 100, a2 % 100); + } + break; + } + if (now) { + uart2_tx_buffer[++i]= CMD_RELOAD_TEXT; + } else { + uart2_tx_buffer[++i]= '\n'; + waitingForLatch=1; + } + HAL_UART_Transmit_DMA(&huart2, uart2_tx_buffer, i+1); + +} + +void setNextTimestamp(time_t nextTime){ + + int32_t offset = 0; + for (uint8_t i=0; i< MAX_RULES; i++) { + if (rules[i].t <= nextTime) offset=rules[i].offset; + else break; + } + // in case of the remote chance that we're interrupted while calculating, + // don't assign to currentOffset until the end of the loop + currentOffset = offset; + nextTime += offset; + + struct tm * nextTm = gmtime( &nextTime ); + tmToBcd( nextTm, &nextBcd ); + tm_yday = nextTm->tm_yday; + tm_wday = nextTm->tm_wday; + + if (displayMode == MODE_ISO_WEEK){ + iso_wday = (nextTm->tm_wday + 6) % 7; + nextTm->tm_mday -= iso_wday -3; + mktime(nextTm); + iso_year = nextTm->tm_year + 1900; + iso_week = nextTm->tm_yday/7 + 1; + } + + next7seg.c = cLut[nextBcd.seconds]; + + next7seg.b[0] = bCat0 | cLut[nextBcd.tenHours]<<2; + next7seg.b[1] = bCat1 | cLut[nextBcd.hours]<<2; + next7seg.b[2] = bCat2 | cLut[nextBcd.tenMinutes]<<2; + next7seg.b[3] = bCat3 | cLut[nextBcd.minutes]<<2; + next7seg.b[4] = bCat4 | cLut[nextBcd.tenSeconds]<<2; + +} + +void setNextCountdown(time_t nextTime){ + + int64_t remaining; + if (config.countdown_to < nextTime) { + remaining = 0; + SetPPS( &PPS_NoUpdate ); // don't show 999 at the next pulse + + } else remaining = config.countdown_to - nextTime; + + uint64_t seconds = remaining % 60; + uint64_t minutes = remaining / 60; + uint64_t hours = minutes / 60; + minutes %= 60; + countdown_days = hours / 24; + hours %= 24; + + next7seg.b[0] = bCat0 | cLut[hours / 10]<<2; + next7seg.b[1] = bCat1 | cLut[hours % 10]<<2; + next7seg.b[2] = bCat2 | cLut[minutes / 10]<<2; + next7seg.b[3] = bCat3 | cLut[minutes % 10]<<2; + next7seg.b[4] = bCat4 | cLut[seconds / 10]<<2; + next7seg.c = cLut[seconds % 10]; +} + +// Store UTC on RTC +// need to also write zone into backup registers +// Only called at the start of a second, don't attempt to write subseconds. +void write_rtc(void){ + + RTC_DateTypeDef sdatestructure; + RTC_TimeTypeDef stimestructure; + bcdStamp_t cBcd; + struct tm * cTm = gmtime( ¤tTime ); + + tmToBcd( cTm, &cBcd ); + + sdatestructure.Year = (cBcd.tenYears<<4) | cBcd.years; + sdatestructure.Month = (cBcd.tenMonths<<4) | cBcd.months; + sdatestructure.Date = (cBcd.tenDays<<4) | cBcd.days; + sdatestructure.WeekDay = RTC_WEEKDAY_MONDAY; + + HAL_RTC_SetDate(&hrtc,&sdatestructure,RTC_FORMAT_BCD); + + stimestructure.Hours = (cBcd.tenHours<<4) | cBcd.hours; + stimestructure.Minutes = (cBcd.tenMinutes<<4) | cBcd.minutes; + stimestructure.Seconds = (cBcd.tenSeconds<<4) | cBcd.seconds; + stimestructure.SubSeconds = 0x00; + stimestructure.TimeFormat = RTC_HOURFORMAT12_AM; + stimestructure.DayLightSaving = RTC_DAYLIGHTSAVING_NONE ; + stimestructure.StoreOperation = RTC_STOREOPERATION_RESET; + + HAL_RTC_SetTime(&hrtc,&stimestructure,RTC_FORMAT_BCD); + + // Write zone info to backup registers + // There are 32 words of memory, 128 bytes + // First 8 words are the zone string including separator and null byte (always less than 32 bytes) + // Next 22 words is a chunk of the ruleset in use, i.e. 11 years + // Last two words are time of write, and time of last calibration + + uint8_t i; + for (i=0; i< MAX_RULES; i++) { + if (rules[i].t > currentTime) break; + } + if (i==0) return; //something has gone wrong, data invalid + i--; //include currently active rule + + char numRulesToStore = (i+11>=MAX_RULES-1)? (MAX_RULES-i)*2 : 22; + + memcpyword( (uint32_t*)&(RTC->BKP0R), (uint32_t*)loadedRulesString, 8 ); + memcpyword( (uint32_t*)&(RTC->BKP8R), (uint32_t*)&rules[i], numRulesToStore ); + + rtc_last_write = (uint32_t)currentTime; +} + +time_t bcdToTm(bcdStamp_t *in, struct tm *out ) { + out->tm_isdst = 0; + out->tm_sec = in->seconds + in->tenSeconds*10; + out->tm_min = in->minutes + in->tenMinutes*10; + out->tm_hour = in->hours + in->tenHours*10; + out->tm_mday = in->days + in->tenDays*10; + out->tm_mon = in->months + in->tenMonths*10 -1; + out->tm_year = in->years + in->tenYears*10 + 100; //Years since 1900 + + return mktime(out); +} +void tmToBcd(struct tm *in, bcdStamp_t *out ) { + out->tenYears = (in->tm_year-100) / 10; + out->years = (in->tm_year-100) % 10; + out->tenMonths = (in->tm_mon+1) / 10; + out->months = (in->tm_mon+1) % 10; + out->tenDays = in->tm_mday / 10; + out->days = in->tm_mday % 10; + out->tenHours = in->tm_hour / 10; + out->hours = in->tm_hour % 10; + out->tenMinutes = in->tm_min / 10; + out->minutes = in->tm_min % 10; + out->tenSeconds = in->tm_sec / 10; + out->seconds = in->tm_sec % 10; +} + +void decodeRMC(void){ + + // do checksum + uint8_t *c = &nmea[1], *end = &nmea[sizeof(nmea)]; + uint8_t sum=0; + + bcdStamp_t rmcBcd; + struct tm rmcTm; + + while (*c !='*') { + sum ^= *c; + if (*c==',') *c=0; + c++; + if(c==end) return; //checksum not found + } + + sprintf((char*)nmea, "%02X", sum); + if (nmea[0] != c[1] || nmea[1]!=c[2]) return; //checksum error + +#define nextField() while (*c && c!=end) c++; c++; + + c=&nmea[7]; // Time + + if (*c==0) return; // time not present + + rmcBcd.tenHours = *c++ -'0'; + rmcBcd.hours = *c++ -'0'; + rmcBcd.tenMinutes = *c++ -'0'; + rmcBcd.minutes = *c++ -'0'; + rmcBcd.tenSeconds = *c++ -'0'; + rmcBcd.seconds = *c++ -'0'; + + if (*c++ =='.') { // subseconds not always present + //if (*c!='0') printf("subseconds non-zero: %s\n", c); + } + nextField() // Navigation receiver warning + data_valid = (*c=='A'?1:0); + + float tempLatitude=-9999, tempLongitude=-9999; + + nextField() // Latitude deg + if (*c){ + tempLatitude = (float)(*c++ -'0')*10.0; + tempLatitude += (float)(*c++ -'0'); + tempLatitude += (float)atof((char*)c) / 60.0; + } + nextField() // Latitude N/S + if (*c =='S') tempLatitude =-tempLatitude; + + nextField() // Longitude deg + if (*c){ + tempLongitude = (float)(*c++ -'0')*100.0; + tempLongitude += (float)(*c++ -'0')*10.0; + tempLongitude += (float)(*c++ -'0'); + tempLongitude += (float)atof((char*)c) / 60.0; + } + nextField() // Longitude E/W + if (*c == 'W') tempLongitude =-tempLongitude; + + if (!config.fake_long && !config.fake_lat) { + longitude = tempLongitude; + latitude = tempLatitude; + new_position=1; + } + + nextField() // Speed over ground, Knots + nextField() // Course Made Good, True + nextField() // Date + + if (*c==0) return; // date not present + + rmcBcd.tenDays = *c++ -'0'; + rmcBcd.days = *c++ -'0'; + rmcBcd.tenMonths = *c++ -'0'; + rmcBcd.months = *c++ -'0'; + rmcBcd.tenYears = *c++ -'0'; + rmcBcd.years = *c++ -'0'; + + + // Immediately after power-up, the GPS module does not know the GPS time/UTC leapsecond offset, and makes a guess + // Even if it gets a fix and starts outputting PPS, the time can be off by a few seconds (usually 2 or 3 fast) + // Only make use of this invalid data if there is nothing else to go on + if ( data_valid || (!had_pps && !rtc_good) ) { + currentTime = bcdToTm( &rmcBcd, &rmcTm ); + + if (decisec >= 9) { + currentTime++; + // check we're not <2ms away from rollover + if (centisec==9 && millisec>7) return; + + // Under normal conditions, we should only be parsing nmea at around .300 to .400 + // USART1 preemption priority is currently 1, so we could be interrupted by systick here + setNextTimestamp( currentTime ); + sendDate(0); + } + } + +} + +void decodeGSV(uint8_t rec){ + unsigned int sv = (nmea[11]-'0')*10 + (nmea[12]-'0'); + uint8_t constellation = nmea[2]; + uint8_t signal_id; + + // signal ID is not always present in GSV (on M8Q) + + unsigned int num_fields = 0, r=0; + while (++rODR, bright); + HAL_DMA_Start(&hdma_tim7_up, (uint32_t)buffer_c, (uint32_t)&GPIOC->ODR, bright); +} + +void displayOff(void){ + + uart2_tx_buffer[0]=' '; //in case already waiting for latch + uart2_tx_buffer[1]= CMD_LOAD_TEXT; + uart2_tx_buffer[2]= CMD_RELOAD_TEXT; + HAL_UART_AbortTransmit(&huart2); + HAL_UART_Transmit_DMA(&huart2, uart2_tx_buffer, 3); + + HAL_TIM_PWM_Stop(&htim2, TIM_CHANNEL_1); + HAL_TIM_PWM_Stop(&htim2, TIM_CHANNEL_2); + + HAL_DMA_Abort(&hdma_tim1_up); + HAL_DMA_Abort(&hdma_tim7_up); + GPIOB->ODR=0; + GPIOC->ODR=0; +} +void displayOn(void){ + HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); + HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2); + setDisplayPWM(5); +} + +void setDisplayFreq(uint32_t freq){ + if (waitingForLatch) { + delayedDisplayFreq = freq; + return; + } + + if (freq<1000 || freq>100000) {delayedDisplayFreq=0; return;} + + uint8_t tx_buf[4]; + tx_buf[0]= CMD_SET_FREQUENCY; + tx_buf[1]= (freq>>14) & 0x7F; + tx_buf[2]= (freq>>7) & 0x7F; + tx_buf[3]= (freq) & 0x7F; + if (HAL_UART_Transmit(&huart2, tx_buf, 4, 2) == HAL_OK) { + delayedDisplayFreq = 0; + } + + uint32_t arr = round(16000000.0 / (float)freq) -1.0; + + TIM1->ARR = arr; + TIM7->ARR = arr; +} + +#define colonAnimationStart() \ + TIM5->CNT=0; \ + HAL_DMA_Start(&hdma_tim5_ch1, (uint32_t)buffer_colons_L, (uint32_t)&TIM2->CCR1, 200); \ + HAL_DMA_Start(&hdma_tim5_ch2, (uint32_t)buffer_colons_R, (uint32_t)&TIM2->CCR2, 200); + +#define colonAnimationStop() \ + HAL_DMA_Abort(&hdma_tim5_ch1); \ + HAL_DMA_Abort(&hdma_tim5_ch2); + +#define colonAnimationSync() \ + colonAnimationStop() \ + colonAnimationStart() + +void loadColonAnimation(void){ + + + switch (colonMode) { + case COLON_MODE_SLOWFADE: + for (int k=0;k<100;k++) { + buffer_colons_R[k] = + buffer_colons_L[k] = k*2; + buffer_colons_R[k+100] = + buffer_colons_L[k+100] = 198-k*2; + } + break; + case COLON_MODE_HEARTBEAT: + for (int k=0;k<50;k++) { + buffer_colons_L[k] = k*4; + } + for (int k=0;k<100;k++) { + buffer_colons_L[k+50] = 200 - k*2; + } + for (int k=0;k<50;k++) { + buffer_colons_L[k+150] = 0; + } + for (int k=0;k<200;k++) { + buffer_colons_R[k] = buffer_colons_L[(k+175)%200]; + } + + break; + case COLON_MODE_1PPS_SAWTOOTH: + for (int k=0;k<100;k++) { + buffer_colons_R[k] = + buffer_colons_L[k] = 196-(k*k)/50; + buffer_colons_R[k+100] = + buffer_colons_L[k+100] = 196-(k*k)/50; + } + break; + case COLON_MODE_ALT_SAWTOOTH: + for (int k=0;k<100;k++) { + buffer_colons_R[k] = 0; + buffer_colons_L[k+100] = 0; + buffer_colons_L[k] = 196-(k*k)/50; + buffer_colons_R[k+100] = 196-(k*k)/50; + } + break; + case COLON_MODE_TOGGLE: + for (int k=0;k<100;k++) { + buffer_colons_R[k] = 200; + buffer_colons_L[k] = 200; + buffer_colons_R[k+100] = 0; + buffer_colons_L[k+100] = 0; + } + break; + case COLON_MODE_SOLID: + for (int k=0;k<200;k++) { + buffer_colons_R[k] = 200; + buffer_colons_L[k] = 200; + } + break; + } + +} + +_Bool truthy(char const* str){ + if (strcasecmp(str, "on")==0) return 1; + if (strcasecmp(str, "enabled")==0) return 1; + if (strcasecmp(str, "1")==0) return 1; + return 0; +} + +_Bool falsey(char const* str){ + if (strcasecmp(str, "off")==0) return 1; + if (strcasecmp(str, "disabled")==0) return 1; + if (strcasecmp(str, "0")==0) return 1; + if (strcasecmp(str, "none")==0) return 1; + return 0; +} + +// Accept a float between 0.0 and 1.0, or an int from 0 to 4096 +float parseBrightness(char *v, _Bool invert){ + if (!v[0]) return -1; + float b = strtof(v, NULL); + if (!isfinite(b) || b<0.0) return -1; + if (b<=1.0 && v[1]=='.') + return invert? (1.0-b) * 4095 : b*4095; + if (b<=4095) + return invert? 4095-b : b; + return -1; +} + +#define set_mode_enabled(mode, value) \ + if ((config.modes_enabled[mode] = truthy(value))) requestMode=mode; + +void parseConfigString(char *key, char *value) { + + if (strcasecmp(key, "text") == 0) { + + strcpy(textDisplay, value); + + } else if (strcasecmp(key, "MATRIX_FREQUENCY") == 0) { + + setDisplayFreq(atoi(value)); + + } else if (strcasecmp(key, "zone_override") == 0) { + + if (!value[0] || delayedLoadRules) return; + + strcpy(preloadRulesString, value); + delayedLoadRules=1; + ZDAbort(); + + } else if (strcasecmp(key, "brightness") == 0) { + + config.brightness_override = parseBrightness(value, 1); + + } else if (strcasecmp(key, "countdown_to") == 0) { + + // support fractional seconds?? + struct tm t = {0}; + if( sscanf(value, "%d-%d-%dT%d:%d:%dZ", &t.tm_year, &t.tm_mon, &t.tm_mday, &t.tm_hour, &t.tm_min, &t.tm_sec) >=3) { + + if (t.tm_year > 9999) return; // arbitrary cutoff, ~3e6 days + t.tm_year -= 1900; + t.tm_mon -= 1; + + config.countdown_to = mktime(&t) -1; + + } + } else if (strcasecmp(key, "MODE_ISO8601_STD") == 0) { + set_mode_enabled(MODE_ISO8601_STD, value); + } else if (strcasecmp(key, "MODE_ISO_ORDINAL") == 0) { + set_mode_enabled(MODE_ISO_ORDINAL, value); + } else if (strcasecmp(key, "MODE_ISO_WEEK") == 0) { + set_mode_enabled(MODE_ISO_WEEK, value); + } else if (strcasecmp(key, "MODE_UNIX") == 0) { + set_mode_enabled(MODE_UNIX, value); + } else if (strcasecmp(key, "MODE_JULIAN_DATE") == 0) { + set_mode_enabled(MODE_JULIAN_DATE, value); + } else if (strcasecmp(key, "MODE_MODIFIED_JD") == 0) { + set_mode_enabled(MODE_MODIFIED_JD, value); + } else if (strcasecmp(key, "MODE_SHOW_OFFSET") == 0) { + set_mode_enabled(MODE_SHOW_OFFSET, value); + } else if (strcasecmp(key, "MODE_SHOW_TZ_NAME") == 0) { + set_mode_enabled(MODE_SHOW_TZ_NAME, value); + } else if (strcasecmp(key, "MODE_WEEKDAY") == 0) { + set_mode_enabled(MODE_WEEKDAY, value); + } else if (strcasecmp(key, "MODE_WEEKDA_DD") == 0) { + set_mode_enabled(MODE_WEEKDA_DD, value); + } else if (strcasecmp(key, "MODE_WDY_MM_DD") == 0) { + set_mode_enabled(MODE_WDY_MM_DD, value); + } else if (strcasecmp(key, "MODE_STANDBY") == 0) { + set_mode_enabled(MODE_STANDBY, value); + } else if (strcasecmp(key, "MODE_COUNTDOWN") == 0) { + set_mode_enabled(MODE_COUNTDOWN, value); + } else if (strcasecmp(key, "MODE_SATVIEW") == 0) { + set_mode_enabled(MODE_SATVIEW, value); + } else if (strcasecmp(key, "MODE_DEBUG_BRIGHTNESS") == 0) { + set_mode_enabled(MODE_DEBUG_BRIGHTNESS, value); + } else if (strcasecmp(key, "MODE_DEBUG_RTC") == 0) { + set_mode_enabled(MODE_DEBUG_RTC, value); + } else if (strcasecmp(key, "MODE_TEXT") == 0) { + set_mode_enabled(MODE_TEXT, value); + } else if (strcasecmp(key, "MODE_VBAT") == 0) { + set_mode_enabled(MODE_VBAT, value); + } else if (strcasecmp(key, "MODE_DISPLAYTEST") == 0) { + set_mode_enabled(MODE_DISPLAYTEST, value); + } else if (strcasecmp(key, "MODE_TTFF") == 0) { + set_mode_enabled(MODE_TTFF, value); +#ifdef NONCOMPLIANT_DATE_MODES + } else if (strcasecmp(key, "MODE_DDMMYYYY") == 0) { + set_mode_enabled(MODE_DDMMYYYY, value); +#endif + } else if (strcasecmp(key, "MODE_FIRMWARE_CRC") == 0) { + set_mode_enabled(MODE_FIRMWARE_CRC_D, value); + set_mode_enabled(MODE_FIRMWARE_CRC_T, value); + } else if (strcasecmp(key, "MODE_SUN") == 0) { + set_mode_enabled(MODE_SUN, value); + } else if (strcasecmp(key, "MODE_SUN_AZEL") == 0) { + set_mode_enabled(MODE_SUN_AZEL, value); + } else if (strcasecmp(key, "MODE_MOON") == 0) { + set_mode_enabled(MODE_MOON, value); + } else if (strcasecmp(key, "MODE_GRID") == 0) { + set_mode_enabled(MODE_GRID, value); + } else if (strcasecmp(key, "MODE_LATLON") == 0) { + set_mode_enabled(MODE_LATLON, value); + } else if (strcasecmp(key, "Tolerance_time_1ms") == 0) { + config.tolerance_1ms = atoi(value); + } else if (strcasecmp(key, "Tolerance_time_10ms") == 0) { + config.tolerance_10ms = atoi(value); + } else if (strcasecmp(key, "Tolerance_time_100ms") == 0) { + config.tolerance_100ms = atoi(value); + } else if (strcasecmp(key, "fake_longitude") == 0) { + config.fake_long = atof(value); + } else if (strcasecmp(key, "fake_latitude") == 0) { + config.fake_lat = atof(value); + } else if (strcasecmp(key, "colon_mode") == 0) { + + if (strcasecmp(value, "solid") == 0) { + colonMode = COLON_MODE_SOLID; + } else if (strcasecmp(value, "heartbeat") == 0) { + colonMode = COLON_MODE_HEARTBEAT; + } else if (strcasecmp(value, "sawtooth") == 0) { + colonMode = COLON_MODE_1PPS_SAWTOOTH; + } else if (strcasecmp(value, "alt_sawtooth") == 0) { + colonMode = COLON_MODE_ALT_SAWTOOTH; + } else if (strcasecmp(value, "toggle") == 0) { + colonMode = COLON_MODE_TOGGLE; + } else colonMode = COLON_MODE_SLOWFADE; + + } else if (strcasecmp(key, "nmea") == 0) { + + if (falsey(value)) { + nmea_cdc_level = NMEA_NONE; + } else if (strcasecmp(value, "rmc") == 0) { + nmea_cdc_level = NMEA_RMC; + } else nmea_cdc_level = NMEA_ALL; + + } else if (strcasecmp(key, "pps") == 0) { + + pps_ts_enabled = truthy(value); // emit a $PMTXTS timing sentence on each PPS edge + + } else if (key[0]=='B' && key[1]=='S' && key[3]==0) { //BS1, BS2, etc + if (!key[2] || key[2]<'1' || key[2]>'0'+sizeof(brightnessCurve)/sizeof(brightnessCurve[0])) return; + + char *c = &value[0]; + while (*c++) if(*c==',') break; + if (*c==0) return; + *c=0; c++; + + float in = parseBrightness(value,0); + float out = parseBrightness(c,1); + if (in<0 || out<0) return; + + brightnessCurve[key[2]-'1'].in = in; + brightnessCurve[key[2]-'1'].out = out; + + } + +} + +void postConfigCleanup(void){ + loadColonAnimation(); + + // check at least one mode is enabled + uint8_t j = 0; + for (uint8_t i=0; i=2)) { + parseConfigString(key, value); + postConfigCleanup(); + } + k=0; + v=0; + state=0; + return; + } + + switch (state) { + case 0: // read key + if (k) { + if (c=='=') {state =2; break;} + if (c==' ' || c=='\t') {state =1; break;} + } + key[k++] = c; + if (k==31) k--; + break; + case 1: // whitespace + if (c=='=') state=2; + else if (c!=' ' && c!='\t') {state=0; k=0; key[k++]=c;} + break; + case 2: //second whitespace + if (c!=' ' && c!='\t' && c!='=') {state=3; value[v++]=c;} + break; + case 3: + value[v++]=c; + if (v==31) v--; + } +} + +void readConfigFile(void){ + +#ifdef CHECK_CONFIG_MTIME + FILINFO fno; + if (f_stat(CONFIG_FILENAME, &fno) == FR_OK) { + // if unchanged, exit early before touching any config + // if the file doesn't exist, fall through and fail on the f_open + if (fno.fdate==config.fdate && fno.ftime==config.ftime) return; + config.fdate=fno.fdate; + config.ftime=fno.ftime; + } +#endif + + config.tolerance_1ms = 1000; + config.tolerance_10ms = 10000; + config.tolerance_100ms = 100000; + config.zone_override = 0; + config.brightness_override = -1.0; + colonMode = 0; + + FIL file; + + if (f_open(&file, CONFIG_FILENAME, FA_READ) != FR_OK) { + postConfigCleanup(); + return; + } + + char key[32], value[32], s[1]; + unsigned int rc; + uint16_t col=0; + + + while (1) { + f_read(&file, s, 1, &rc); + if (rc!=1) break; //EOF + + if (s[0]=='\r' || s[0]=='\n') { col=0; continue; } //EOL + + if (col==0 && (s[0]=='#' || s[0]==';')) { // comments + while (rc && s[0]!='\n') f_read(&file, s, 1, &rc); + continue; + } + + if (s[0]!='=') { + if (col CAL_PERIOD) { + + LPTIM1_high=0; + LL_LPTIM_StartCounter(LPTIM1, LL_LPTIM_OPERATING_MODE_CONTINUOUS); + calibStart = currentTime; + + } else if ((uint32_t)currentTime - calibStart == CAL_PERIOD) { + volatile uint16_t x = LPTIM1->CNT; + volatile uint16_t y = LPTIM1->CNT; + if (x!=y) goto skipRtcCal; + + int32_t error = ((LPTIM1_high<<16) + x) - 32768*CAL_PERIOD + LPTIM_START_DELAY; + float e = (float)error * 32.0 / CAL_PERIOD; + + debug_rtc_val = error;//0x100 + round(e); + + if (e>255.0 || e< -255.0) goto skipRtcCal; + + __HAL_RTC_WRITEPROTECTION_DISABLE(&hrtc); + RTC->CALR = 0x100 + (int)round(e); + __HAL_RTC_WRITEPROTECTION_ENABLE(&hrtc); + rtc_last_calibration = (uint32_t)currentTime; + +skipRtcCal: + // Prepare the counter for the next calibration + // LPTIM1->CNT is read only, the only way to zero it is to disable and re-enable the timer. + // There is a further delay associated with this, better to put it here than right at the moment we want to start the timer. + LPTIM1->CR &= ~LPTIM_CR_ENABLE; + LPTIM1->CR |= LPTIM_CR_ENABLE; + LL_LPTIM_SetAutoReload(LPTIM1, 0xFFFF); + LL_LPTIM_ClearFLAG_ARRM(LPTIM1); // just in case there's one pending + } +} + +void EXTI9_5_IRQHandler(void){__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_7);} + +// Snapshot the timing state at the instant of the PPS edge. MUST run before SysTick->VAL is +// reloaded and before millisec/centisec/decisec are zeroed, so it captures the phase error +// between the firmware's modelled second and the true GPS edge. +#define capturePPS() do { \ pps_cap.dwt_pps = DWT->CYCCNT; \ - pps_cap.systick = SysTick->VAL; \ - pps_cap.subms = (uint16_t)decisec*100 + (uint16_t)centisec*10 + millisec; \ - pps_cap.epoch = (uint32_t)currentTime; \ - pps_cap.calerr = debug_rtc_val; \ - pps_cap.sincecal = (uint32_t)currentTime - (uint32_t)rtc_last_calibration; \ - pps_cap.temp = die_temp_c; \ - pps_cap.flags = (data_valid?1:0) | (had_pps?2:0) | (rtc_good?4:0); \ - pps_cap.seq++; \ - pps_record_pending = 1; \ - } while(0) - -// PPS rising edge -void PPS(void) -{ - capturePPS(); - SysTick->VAL = SysTick->LOAD; - - buffer_c[3].low=cLut[0]; - buffer_c[2].low=cLut[0]; - buffer_c[1].low=cLut[0]; - loadNextTimestamp(); - millisec=0; - centisec=0; - decisec=0; - - __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_7); - - // clear systick flag if set? - - // During first power up PPS can be emitted before the GPS leapsecond offset is known - // In this case, it is safest to pretend PPS hasn't happened - if (!data_valid) return; - - calibrateRTC(); - - if ((currentTime & 1) ==0) {colonAnimationSync()} - - had_pps = 1; - last_pps_time = (uint32_t)currentTime; -} - -void PPS_NoUpdate(void) -{ - capturePPS(); - SysTick->VAL = SysTick->LOAD; - triggerPendSV(); - - millisec=0; - centisec=0; - decisec=0; - - __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_7); - - if (!data_valid) return; - - calibrateRTC(); - - had_pps = 1; - last_pps_time = (uint32_t)currentTime; -} - -void PPS_Countdown(void) -{ - capturePPS(); - SysTick->VAL = SysTick->LOAD; - - buffer_c[3].low=cLut[9]; - buffer_c[2].low=cLut[9]; - buffer_c[1].low=cLut[9]; - loadNextTimestamp(); - millisec=0; - centisec=0; - decisec=0; - - __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_7); - - if (!data_valid) return; - calibrateRTC(); - if ((currentTime & 1) ==0) {colonAnimationSync()} - - had_pps = 1; - last_pps_time = (uint32_t)currentTime; -} - -void PPS_Init(void){ - GPIO_InitTypeDef GPIO_InitStruct = {0}; - - /*Configure GPIO pin : PC7 */ - GPIO_InitStruct.Pin = GPIO_PIN_7; - GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING; - GPIO_InitStruct.Pull = GPIO_PULLDOWN; - HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); - - /* EXTI interrupt init*/ - HAL_NVIC_SetPriority(EXTI9_5_IRQn, 0, 0); - HAL_NVIC_EnableIRQ(EXTI9_5_IRQn); - - SetPPS( &PPS ); -} - -// usbd_cdc_if.h isn't pulled into main.c; forward-declare the one symbol we need. -extern uint8_t CDC_Copy_Transmit(uint8_t* buf, uint16_t Len); -extern USBD_HandleTypeDef hUsbDeviceFS; - -// Format + send one $PMTXTS sentence from the values captured at the last PPS edge. -// Runs in the main loop (snprintf is fine here, never in the ISR). Clears pps_record_pending -// on a successful send and for any undeliverable record (no host, formatting failure) — a -// fresh record arrives on the next edge, so only USBD_BUSY is worth retrying. + pps_cap.systick = SysTick->VAL; \ + pps_cap.subms = (uint16_t)decisec*100 + (uint16_t)centisec*10 + millisec; \ + pps_cap.epoch = (uint32_t)currentTime; \ + pps_cap.calerr = debug_rtc_val; \ + pps_cap.sincecal = (uint32_t)currentTime - (uint32_t)rtc_last_calibration; \ + pps_cap.temp = die_temp_c; \ + pps_cap.flags = (data_valid?1:0) | (had_pps?2:0) | (rtc_good?4:0); \ + pps_cap.seq++; \ + pps_record_pending = 1; \ + } while(0) + +// PPS rising edge +void PPS(void) +{ + capturePPS(); + SysTick->VAL = SysTick->LOAD; + + buffer_c[3].low=cLut[0]; + buffer_c[2].low=cLut[0]; + buffer_c[1].low=cLut[0]; + loadNextTimestamp(); + millisec=0; + centisec=0; + decisec=0; + + __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_7); + + // clear systick flag if set? + + // During first power up PPS can be emitted before the GPS leapsecond offset is known + // In this case, it is safest to pretend PPS hasn't happened + if (!data_valid) return; + + calibrateRTC(); + + if ((currentTime & 1) ==0) {colonAnimationSync()} + + had_pps = 1; + last_pps_time = (uint32_t)currentTime; +} + +void PPS_NoUpdate(void) +{ + capturePPS(); + SysTick->VAL = SysTick->LOAD; + triggerPendSV(); + + millisec=0; + centisec=0; + decisec=0; + + __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_7); + + if (!data_valid) return; + + calibrateRTC(); + + had_pps = 1; + last_pps_time = (uint32_t)currentTime; +} + +void PPS_Countdown(void) +{ + capturePPS(); + SysTick->VAL = SysTick->LOAD; + + buffer_c[3].low=cLut[9]; + buffer_c[2].low=cLut[9]; + buffer_c[1].low=cLut[9]; + loadNextTimestamp(); + millisec=0; + centisec=0; + decisec=0; + + __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_7); + + if (!data_valid) return; + calibrateRTC(); + if ((currentTime & 1) ==0) {colonAnimationSync()} + + had_pps = 1; + last_pps_time = (uint32_t)currentTime; +} + +void PPS_Init(void){ + GPIO_InitTypeDef GPIO_InitStruct = {0}; + + /*Configure GPIO pin : PC7 */ + GPIO_InitStruct.Pin = GPIO_PIN_7; + GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING; + GPIO_InitStruct.Pull = GPIO_PULLDOWN; + HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); + + /* EXTI interrupt init*/ + HAL_NVIC_SetPriority(EXTI9_5_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(EXTI9_5_IRQn); + + SetPPS( &PPS ); +} + +// usbd_cdc_if.h isn't pulled into main.c; forward-declare the one symbol we need. +extern uint8_t CDC_Copy_Transmit(uint8_t* buf, uint16_t Len); +extern USBD_HandleTypeDef hUsbDeviceFS; + +// Format + send one $PMTXTS sentence from the values captured at the last PPS edge. +// Runs in the main loop (snprintf is fine here, never in the ISR). Clears pps_record_pending +// on a successful send and for any undeliverable record (no host, formatting failure) — a +// fresh record arrives on the next edge, so only USBD_BUSY is worth retrying. // Sentence: $PMTXTS,,,,,,,,,,,,*CC -// subms+(load-systick)/(load+1) = modelled sub-second position at the edge (phase error); -// ppm = calerr * 1e6 / (32768 * CAL_PERIOD) [CAL_PERIOD=63]; temp = die °C; -// flags: b0 valid, b1 pps, b2 rtc. +// subms+(load-systick)/(load+1) = modelled sub-second position at the edge (phase error); +// ppm = calerr * 1e6 / (32768 * CAL_PERIOD) [CAL_PERIOD=63]; temp = die °C; +// flags: b0 valid, b1 pps, b2 rtc. // SOF-correlation tail (experimental): dwt_pps = DWT cycle count at the PPS edge; sof_frame = USB // 11-bit frame number of the most recent SOF; dwt_sof = DWT at that SOF. A host that knows each USB // frame's own arrival time places the edge as hostTime(sof_frame) + (dwt_pps-dwt_sof)/f_dwt, immune // to USB read jitter. dwt_pps deltas (~80e6/s) self-calibrate f_dwt, so no core-clock assumption. -static uint8_t emitPPSTimestamp(void){ - // With no enumerated host (e.g. charger-only power) CDC can never accept the sentence; - // drop the record before doing any formatting work, otherwise the pending flag would - // re-run the whole format-and-fail cycle every main-loop pass until a host appears. - if (hUsbDeviceFS.dev_state != USBD_STATE_CONFIGURED) { - pps_record_pending = 0; - return USBD_FAIL; - } - - __disable_irq(); // atomic snapshot of the ISR-written capture - uint32_t snap_seq = pps_cap.seq; - uint32_t st = pps_cap.systick; - uint16_t subms = pps_cap.subms; - uint32_t epoch = pps_cap.epoch; - int32_t calerr = pps_cap.calerr; - uint32_t sincecal = pps_cap.sincecal; - int16_t temp = pps_cap.temp; - uint8_t flags = pps_cap.flags; +static uint8_t emitPPSTimestamp(void){ + // With no enumerated host (e.g. charger-only power) CDC can never accept the sentence; + // drop the record before doing any formatting work, otherwise the pending flag would + // re-run the whole format-and-fail cycle every main-loop pass until a host appears. + if (hUsbDeviceFS.dev_state != USBD_STATE_CONFIGURED) { + pps_record_pending = 0; + return USBD_FAIL; + } + + __disable_irq(); // atomic snapshot of the ISR-written capture + uint32_t snap_seq = pps_cap.seq; + uint32_t st = pps_cap.systick; + uint16_t subms = pps_cap.subms; + uint32_t epoch = pps_cap.epoch; + int32_t calerr = pps_cap.calerr; + uint32_t sincecal = pps_cap.sincecal; + int16_t temp = pps_cap.temp; + uint8_t flags = pps_cap.flags; uint32_t dwt_pps = pps_cap.dwt_pps; // DWT at the PPS edge (SOF-correlation timebase) uint32_t sof_dwt = pps_sof_dwt; // DWT at the most recent SOF ... uint16_t sof_fr = pps_sof_frame; // ... and that SOF's 11-bit USB frame number ... uint8_t sof_ok = pps_sof_valid; // ... valid only once a real SOF has latched an anchor - __enable_irq(); - - uint32_t load = SysTick->LOAD; // constant; sent so the host needn't assume core clock - + __enable_irq(); + + uint32_t load = SysTick->LOAD; // constant; sent so the host needn't assume core clock + char body[128]; // everything between '$' and '*' int n = snprintf(body, sizeof body, "PMTXTS,%lu,%lu,%u,%lu,%lu,%ld,%lu,%d,%X", - (unsigned long)snap_seq, (unsigned long)epoch, (unsigned)subms, - (unsigned long)st, (unsigned long)load, (long)calerr, + (unsigned long)snap_seq, (unsigned long)epoch, (unsigned)subms, + (unsigned long)st, (unsigned long)load, (long)calerr, (unsigned long)sincecal, (int)temp, (unsigned)flags); - if (n < 0 || n >= (int)sizeof body) { pps_record_pending = 0; return USBD_FAIL; } + if (n < 0 || n >= (int)sizeof body) { pps_record_pending = 0; return USBD_FAIL; } // Append the SOF-correlation tail only when a real anchor exists — never a stale (0,0). Absent tail = // the plain sentence a 9-field parser expects (also the emulator's output, which has no USB SOF). if (sof_ok) { @@ -1561,1719 +1564,1719 @@ static uint8_t emitPPSTimestamp(void){ if (t < 0 || t >= (int)(sizeof body - n)) { pps_record_pending = 0; return USBD_FAIL; } n += t; } - - uint8_t cks = 0; // standard NMEA XOR checksum - for (int i = 0; i < n; i++) cks ^= (uint8_t)body[i]; - - char line[NMEA_BUF_SIZE]; // must fit the CDC txbuf[NMEA_BUF_SIZE] downstream - int m = snprintf(line, sizeof line, "$%s*%02X\r\n", body, (unsigned)cks); - if (m < 0 || m >= (int)sizeof line) { pps_record_pending = 0; return USBD_FAIL; } - - // The CDC IN endpoint is shared with the ISR NMEA passthrough; serialise the (tiny) submit, - // and clear the pending flag only if no fresh PPS edge arrived since the snapshot (so a - // record captured mid-send isn't silently dropped). FAIL also clears: the record is - // undeliverable (USB de-inited under us), unlike BUSY where the host may drain the FIFO. - __disable_irq(); - uint8_t r = CDC_Copy_Transmit((uint8_t*)line, (uint16_t)m); - if (r != USBD_BUSY && pps_cap.seq == snap_seq) pps_record_pending = 0; - __enable_irq(); - return r; -} - -#define timetick() \ - millisec++; \ - if (millisec>=10) { \ - millisec=0; \ - centisec++; \ - if (centisec>=10) { \ - centisec=0; \ - decisec++; \ - if (decisec>=10) { \ - decisec=0; \ - loadNextTimestamp(); \ - } \ - } \ - } - -void SysTick_CountUp_P3(void) -{ - timetick() - - buffer_c[3].low=cLut[millisec]; - buffer_c[2].low=cLut[centisec]; - buffer_c[1].low=cLut[decisec]; - - - - HAL_IncTick(); - - // At the 0.900 mark, we calculate what the display should read at the next pulse - if (decisec==9 && centisec==0 && millisec==0){ - // Calculating the next display from the unix timestamp takes about 32uS with -O2, -O3 or -Os - // takes about 70uS on -O0 so I think it's fine to do this within systick - // If needed, we should move this to a lower priority software-triggered interrupt - currentTime++; - setNextTimestamp( currentTime ); - sendDate(0); - } -} - -void SysTick_CountUp_P2(void) { - timetick() - - buffer_c[2].low=cLut[centisec]; - buffer_c[1].low=cLut[decisec]; - - HAL_IncTick(); - - if (decisec==9 && centisec==0 && millisec==0){ - currentTime++; - setNextTimestamp( currentTime ); - sendDate(0); - } -} -void SysTick_CountUp_P1(void) { - - timetick() - - buffer_c[1].low=cLut[decisec]; - - HAL_IncTick(); - - if (decisec==9 && centisec==0 && millisec==0){ - currentTime++; - setNextTimestamp( currentTime ); - sendDate(0); - } -} - -void SysTick_CountUp_P0(void) { - - timetick() - - HAL_IncTick(); - - if (decisec==9 && centisec==0 && millisec==0){ - currentTime++; - setNextTimestamp( currentTime ); - sendDate(0); - } -} - -void SysTick_CountUp_NoUpdate(void) { - millisec++; - if (millisec>=10) { - millisec=0; - centisec++; - if (centisec>=10) { - centisec=0; - decisec++; - if (decisec>=10) { - decisec=0; - // write_rtc still needs to happen - triggerPendSV(); - } - } - } - - HAL_IncTick(); - - if (decisec==9 && centisec==0 && millisec==0){ - currentTime++; - setNextTimestamp( currentTime ); - //sendDate(0); - } -} - - -void SysTick_CountDown_P3(void) -{ - timetick() - - buffer_c[3].low=cLut[9-millisec]; - buffer_c[2].low=cLut[9-centisec]; - buffer_c[1].low=cLut[9-decisec]; - - - HAL_IncTick(); - - if (decisec==9 && centisec==0 && millisec==0){ - currentTime++; - setNextCountdown( currentTime ); - sendDate(0); - } -} - -void SysTick_CountDown_P2(void) -{ - timetick() - - //buffer_c[3].low=cLut[9-millisec]; - buffer_c[2].low=cLut[9-centisec]; - buffer_c[1].low=cLut[9-decisec]; - - - HAL_IncTick(); - - if (decisec==9 && centisec==0 && millisec==0){ - currentTime++; - setNextCountdown( currentTime ); - sendDate(0); - } -} - -void SysTick_CountDown_P1(void) -{ - timetick() - - //buffer_c[3].low=cLut[9-millisec]; - //buffer_c[2].low=cLut[9-centisec]; - buffer_c[1].low=cLut[9-decisec]; - - - HAL_IncTick(); - - if (decisec==9 && centisec==0 && millisec==0){ - currentTime++; - setNextCountdown( currentTime ); - sendDate(0); - } -} - -// A no precision countdown is going to be really ambiguous, as it will hit zero a second before the target -// Then again it will only be used in situations where the tolerance is worse than a second -void SysTick_CountDown_P0(void) -{ - timetick() - - //buffer_c[3].low=cLut[9-millisec]; - //buffer_c[2].low=cLut[9-centisec]; - //buffer_c[1].low=cLut[9-decisec]; - - HAL_IncTick(); - - if (decisec==9 && centisec==0 && millisec==0){ - currentTime++; - setNextCountdown( currentTime ); - sendDate(0); - } -} - -void SysTick_Dummy(void){ - HAL_IncTick(); -} - -// We cannot use hardware vbus monitoring since the pin is occupied by USART1 TX -// We can't use EXTI on PA8 as it's in the same group as PPS -void monitor_vbus(void){ - static _Bool vbus_state = 1; // power-on state is initialised, even if not connected - - _Bool vbus = (GPIOA->IDR & GPIO_PIN_8); - - if (vbus_state && !vbus) { // disconnected - - MX_USB_Stop(); - - } else if (vbus && !vbus_state) { // connected - - MX_USB_DEVICE_Init(); - - } - vbus_state = vbus; -} - -void measure_vbat(void){ - ADC123_COMMON->CCR |= ADC_CCR_VBATEN; - HAL_Delay(5); - HAL_ADC_Start(&hadc3); - HAL_ADC_PollForConversion(&hadc3, 10); - uint16_t adc = HAL_ADC_GetValue(&hadc3); - ADC123_COMMON->CCR &= ~ADC_CCR_VBATEN; - vbat = (float)adc *0.0024102564102564104;//3*3.29/4095.0; -} - -// Read the STM32 internal die-temperature sensor on hadc3 (shared with VBAT) into die_temp_c. -// The die sits slightly above ambient on this low-power board, but it tracks the crystal well -// enough to characterise the oscillator's temperature dependence. -void measure_temp(void){ - ADC_ChannelConfTypeDef s = {0}; - s.Rank = ADC_REGULAR_RANK_1; - s.SamplingTime = ADC_SAMPLETIME_640CYCLES_5; // temp sensor needs a long sampling time - s.SingleDiff = ADC_SINGLE_ENDED; - s.OffsetNumber = ADC_OFFSET_NONE; - s.Offset = 0; - - s.Channel = ADC_CHANNEL_TEMPSENSOR; - HAL_ADC_ConfigChannel(&hadc3, &s); - ADC123_COMMON->CCR |= ADC_CCR_TSEN; - HAL_Delay(1); // tSTART for the temperature sensor (~120 us) - HAL_ADC_Start(&hadc3); - HAL_ADC_PollForConversion(&hadc3, 10); - uint16_t raw = HAL_ADC_GetValue(&hadc3); - ADC123_COMMON->CCR &= ~ADC_CCR_TSEN; - - // Factory-calibrated conversion (TS_CAL1/TS_CAL2 in flash). VREF taken as 3300 mV; absolute - // accuracy isn't critical — the curve is fitted against GPS-measured ppm, not trusted raw. - die_temp_c = (int16_t)__HAL_ADC_CALC_TEMPERATURE(3300, raw, ADC_RESOLUTION_12B); - - s.Channel = ADC_CHANNEL_VBAT; // restore so measure_vbat() keeps working - HAL_ADC_ConfigChannel(&hadc3, &s); -} - -uint8_t f_getzcmp(FIL* fp, char * str){ - unsigned int rc; - char * a = str; - char b[1] = {1}; - uint8_t ret = 0; - - while (b[0]!=0) { - f_read(fp, &b, 1, &rc); - if (b[0] != *a++) ret=-1; - } - return ret; -} -uint8_t findField( FIL* fp, char* str, uint8_t count, uint8_t padding ) { - char buf[4]; - unsigned int rc; - for (uint8_t i=0; i= currentTime) { - SetPPS( &PPS_Countdown ); - - if (currentTime - last_pps_time < config.tolerance_1ms){ - buffer_c[0].high= 0b11001110 | cSegDP; - SetSysTick( &SysTick_CountDown_P3 ); - } else if (currentTime - last_pps_time < config.tolerance_10ms){ - buffer_c[3].low = 0b01000000; - buffer_c[0].high= 0b11001110 | cSegDP; - SetSysTick( &SysTick_CountDown_P2 ); - } else if (currentTime - rtc_last_calibration < config.tolerance_100ms){ - buffer_c[3].low = 0b01000000; - buffer_c[2].low = 0b01000000; - buffer_c[0].high= 0b11001110 | cSegDP; - SetSysTick( &SysTick_CountDown_P1 ); - } else { - buffer_c[3].low = 0b01000000; - buffer_c[2].low = 0b01000000; - buffer_c[1].low = 0b01000000; - buffer_c[0].high= 0b11001110; - SetSysTick( &SysTick_CountDown_P0 ); - } - - } else { - countMode = COUNT_HIDDEN; - SetSysTick( &SysTick_CountUp_NoUpdate ); - SetPPS( &PPS_NoUpdate ); - buffer_c[0].high= 0b11001110 | cSegDP; - buffer_c[0].low=cSegDecode0; - buffer_c[1].low=cSegDecode0; - buffer_c[2].low=cSegDecode0; - buffer_c[3].low=cSegDecode0; - - next7seg.b[0] = bCat0 | cLut[0]<<2; - next7seg.b[1] = bCat1 | cLut[0]<<2; - next7seg.b[2] = bCat2 | cLut[0]<<2; - next7seg.b[3] = bCat3 | cLut[0]<<2; - next7seg.b[4] = bCat4 | cLut[0]<<2; - next7seg.c = cLut[0]; - } - - } -} - -#define justExited(x) ((oldMode==x) && (displayMode != x)) -void nextMode(_Bool reverse){ - - uint8_t oldMode = displayMode; - - if (requestMode!=255){ - if (!config.modes_enabled[requestMode]) { - requestMode=255; - return; - } - displayMode=requestMode; - requestMode=255; - } else if (reverse) { - do { - if (--displayMode >= NUM_DISPLAY_MODES) displayMode=NUM_DISPLAY_MODES-1; - } while (!config.modes_enabled[displayMode]); - } else { - do { - if (++displayMode >=NUM_DISPLAY_MODES) displayMode=0; - } while (!config.modes_enabled[displayMode]); - } - - if (justExited(MODE_VBAT)) vbat = 0.0; - if (justExited(MODE_STANDBY)) displayOn(); - if (justExited(MODE_DISPLAYTEST)) { - buffer_c[1].high &= ~cSegDP; - buffer_c[2].high &= ~cSegDP; - buffer_c[3].high &= ~cSegDP; - } - if ( displayMode == MODE_ISO_WEEK || justExited(MODE_COUNTDOWN)) { - // If we exit countdown mode at .9 seconds - // it will show the wrong time for .1 seconds - setNextTimestamp(currentTime); - } - - if (displayMode == MODE_SHOW_OFFSET || displayMode == MODE_DISPLAYTEST) { - countMode = COUNT_HIDDEN; - SetSysTick( &SysTick_CountUp_NoUpdate ); - SetPPS( &PPS_NoUpdate ); - colonAnimationStop() - TIM2->CCR1 = 0; // specific to show_offset - TIM2->CCR2 = 300; - } else if (displayMode == MODE_COUNTDOWN) { - - if (config.countdown_to >= currentTime) { - countMode = COUNT_DOWN; - setNextCountdown(currentTime); - } else { - countMode = COUNT_HIDDEN; - countdown_days = 0; - } - setPrecision(); - TIM2->CCR1 = 0; - TIM2->CCR2 = 0; - latchSegments(); - - } - else { - if (countMode != COUNT_NORMAL) { - countMode = COUNT_NORMAL; - setPrecision(); - SetPPS( &PPS ); - TIM2->CCR1 = 0; - TIM2->CCR2 = 0; - latchSegments(); - } - } - sendDate(1); -} -void button1pressed(void){ - nextMode(0); -} -void button2pressed(void){ - nextMode(1); -} -void buttonsBothHeld(void){ - HAL_TIM_PWM_Stop(&htim2, TIM_CHANNEL_1); - HAL_TIM_PWM_Stop(&htim2, TIM_CHANNEL_2); - - HAL_DMA_Abort(&hdma_tim1_up); - HAL_DMA_Abort(&hdma_tim7_up); - GPIOB->ODR=0; - GPIOC->ODR=0; - - NVIC_SystemReset(); -} - -void generateDACbuffer(uint16_t * buf) { - - static float dac_last=4095; - - - if (displayMode == MODE_STANDBY) { - dac_target = dac_target*0.7 + 1.2*4095.0*0.3; - if (dac_target>4094.0) { - dac_target=4095.0; - displayOff(); - } - } else if (config.brightness_override >=0.0) { - dac_target = config.brightness_override; - } else { - float adc = (float)ADC1->DR; - - uint8_t i; - for (i=1; i< sizeof(brightnessCurve)/sizeof(brightnessCurve[0]) -1; i++){ - if (brightnessCurve[i].in > adc) break; - } - float factor = (adc - brightnessCurve[i-1].in) / (brightnessCurve[i].in - brightnessCurve[i-1].in); - - float out = brightnessCurve[i-1].out*(1.0-factor) + brightnessCurve[i].out*factor; - - if (out>4095.0 || !isfinite(out)) out=4095.0; - else if (out<0.0) out=0.0; - - dac_target = dac_target*0.5 + out*0.5; - } - - - HAL_ADC_Start(&hadc1); - - - - float step = (dac_target-dac_last)/(DAC_BUFFER_SIZE*0.5); - for (size_t i=0; iVTOR = (uint32_t)&__VECTORS_RAM; - - SetSysTick( &SysTick_Dummy ); - - - /* USER CODE END 1 */ - - /* MCU Configuration--------------------------------------------------------*/ - - /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ - HAL_Init(); - - /* USER CODE BEGIN Init */ - - /* USER CODE END Init */ - - /* Configure the system clock */ - SystemClock_Config(); - - /* USER CODE BEGIN SysInit */ - + + uint8_t cks = 0; // standard NMEA XOR checksum + for (int i = 0; i < n; i++) cks ^= (uint8_t)body[i]; + + char line[NMEA_BUF_SIZE]; // must fit the CDC txbuf[NMEA_BUF_SIZE] downstream + int m = snprintf(line, sizeof line, "$%s*%02X\r\n", body, (unsigned)cks); + if (m < 0 || m >= (int)sizeof line) { pps_record_pending = 0; return USBD_FAIL; } + + // The CDC IN endpoint is shared with the ISR NMEA passthrough; serialise the (tiny) submit, + // and clear the pending flag only if no fresh PPS edge arrived since the snapshot (so a + // record captured mid-send isn't silently dropped). FAIL also clears: the record is + // undeliverable (USB de-inited under us), unlike BUSY where the host may drain the FIFO. + __disable_irq(); + uint8_t r = CDC_Copy_Transmit((uint8_t*)line, (uint16_t)m); + if (r != USBD_BUSY && pps_cap.seq == snap_seq) pps_record_pending = 0; + __enable_irq(); + return r; +} + +#define timetick() \ + millisec++; \ + if (millisec>=10) { \ + millisec=0; \ + centisec++; \ + if (centisec>=10) { \ + centisec=0; \ + decisec++; \ + if (decisec>=10) { \ + decisec=0; \ + loadNextTimestamp(); \ + } \ + } \ + } + +void SysTick_CountUp_P3(void) +{ + timetick() + + buffer_c[3].low=cLut[millisec]; + buffer_c[2].low=cLut[centisec]; + buffer_c[1].low=cLut[decisec]; + + + + HAL_IncTick(); + + // At the 0.900 mark, we calculate what the display should read at the next pulse + if (decisec==9 && centisec==0 && millisec==0){ + // Calculating the next display from the unix timestamp takes about 32uS with -O2, -O3 or -Os + // takes about 70uS on -O0 so I think it's fine to do this within systick + // If needed, we should move this to a lower priority software-triggered interrupt + currentTime++; + setNextTimestamp( currentTime ); + sendDate(0); + } +} + +void SysTick_CountUp_P2(void) { + timetick() + + buffer_c[2].low=cLut[centisec]; + buffer_c[1].low=cLut[decisec]; + + HAL_IncTick(); + + if (decisec==9 && centisec==0 && millisec==0){ + currentTime++; + setNextTimestamp( currentTime ); + sendDate(0); + } +} +void SysTick_CountUp_P1(void) { + + timetick() + + buffer_c[1].low=cLut[decisec]; + + HAL_IncTick(); + + if (decisec==9 && centisec==0 && millisec==0){ + currentTime++; + setNextTimestamp( currentTime ); + sendDate(0); + } +} + +void SysTick_CountUp_P0(void) { + + timetick() + + HAL_IncTick(); + + if (decisec==9 && centisec==0 && millisec==0){ + currentTime++; + setNextTimestamp( currentTime ); + sendDate(0); + } +} + +void SysTick_CountUp_NoUpdate(void) { + millisec++; + if (millisec>=10) { + millisec=0; + centisec++; + if (centisec>=10) { + centisec=0; + decisec++; + if (decisec>=10) { + decisec=0; + // write_rtc still needs to happen + triggerPendSV(); + } + } + } + + HAL_IncTick(); + + if (decisec==9 && centisec==0 && millisec==0){ + currentTime++; + setNextTimestamp( currentTime ); + //sendDate(0); + } +} + + +void SysTick_CountDown_P3(void) +{ + timetick() + + buffer_c[3].low=cLut[9-millisec]; + buffer_c[2].low=cLut[9-centisec]; + buffer_c[1].low=cLut[9-decisec]; + + + HAL_IncTick(); + + if (decisec==9 && centisec==0 && millisec==0){ + currentTime++; + setNextCountdown( currentTime ); + sendDate(0); + } +} + +void SysTick_CountDown_P2(void) +{ + timetick() + + //buffer_c[3].low=cLut[9-millisec]; + buffer_c[2].low=cLut[9-centisec]; + buffer_c[1].low=cLut[9-decisec]; + + + HAL_IncTick(); + + if (decisec==9 && centisec==0 && millisec==0){ + currentTime++; + setNextCountdown( currentTime ); + sendDate(0); + } +} + +void SysTick_CountDown_P1(void) +{ + timetick() + + //buffer_c[3].low=cLut[9-millisec]; + //buffer_c[2].low=cLut[9-centisec]; + buffer_c[1].low=cLut[9-decisec]; + + + HAL_IncTick(); + + if (decisec==9 && centisec==0 && millisec==0){ + currentTime++; + setNextCountdown( currentTime ); + sendDate(0); + } +} + +// A no precision countdown is going to be really ambiguous, as it will hit zero a second before the target +// Then again it will only be used in situations where the tolerance is worse than a second +void SysTick_CountDown_P0(void) +{ + timetick() + + //buffer_c[3].low=cLut[9-millisec]; + //buffer_c[2].low=cLut[9-centisec]; + //buffer_c[1].low=cLut[9-decisec]; + + HAL_IncTick(); + + if (decisec==9 && centisec==0 && millisec==0){ + currentTime++; + setNextCountdown( currentTime ); + sendDate(0); + } +} + +void SysTick_Dummy(void){ + HAL_IncTick(); +} + +// We cannot use hardware vbus monitoring since the pin is occupied by USART1 TX +// We can't use EXTI on PA8 as it's in the same group as PPS +void monitor_vbus(void){ + static _Bool vbus_state = 1; // power-on state is initialised, even if not connected + + _Bool vbus = (GPIOA->IDR & GPIO_PIN_8); + + if (vbus_state && !vbus) { // disconnected + + MX_USB_Stop(); + + } else if (vbus && !vbus_state) { // connected + + MX_USB_DEVICE_Init(); + + } + vbus_state = vbus; +} + +void measure_vbat(void){ + ADC123_COMMON->CCR |= ADC_CCR_VBATEN; + HAL_Delay(5); + HAL_ADC_Start(&hadc3); + HAL_ADC_PollForConversion(&hadc3, 10); + uint16_t adc = HAL_ADC_GetValue(&hadc3); + ADC123_COMMON->CCR &= ~ADC_CCR_VBATEN; + vbat = (float)adc *0.0024102564102564104;//3*3.29/4095.0; +} + +// Read the STM32 internal die-temperature sensor on hadc3 (shared with VBAT) into die_temp_c. +// The die sits slightly above ambient on this low-power board, but it tracks the crystal well +// enough to characterise the oscillator's temperature dependence. +void measure_temp(void){ + ADC_ChannelConfTypeDef s = {0}; + s.Rank = ADC_REGULAR_RANK_1; + s.SamplingTime = ADC_SAMPLETIME_640CYCLES_5; // temp sensor needs a long sampling time + s.SingleDiff = ADC_SINGLE_ENDED; + s.OffsetNumber = ADC_OFFSET_NONE; + s.Offset = 0; + + s.Channel = ADC_CHANNEL_TEMPSENSOR; + HAL_ADC_ConfigChannel(&hadc3, &s); + ADC123_COMMON->CCR |= ADC_CCR_TSEN; + HAL_Delay(1); // tSTART for the temperature sensor (~120 us) + HAL_ADC_Start(&hadc3); + HAL_ADC_PollForConversion(&hadc3, 10); + uint16_t raw = HAL_ADC_GetValue(&hadc3); + ADC123_COMMON->CCR &= ~ADC_CCR_TSEN; + + // Factory-calibrated conversion (TS_CAL1/TS_CAL2 in flash). VREF taken as 3300 mV; absolute + // accuracy isn't critical — the curve is fitted against GPS-measured ppm, not trusted raw. + die_temp_c = (int16_t)__HAL_ADC_CALC_TEMPERATURE(3300, raw, ADC_RESOLUTION_12B); + + s.Channel = ADC_CHANNEL_VBAT; // restore so measure_vbat() keeps working + HAL_ADC_ConfigChannel(&hadc3, &s); +} + +uint8_t f_getzcmp(FIL* fp, char * str){ + unsigned int rc; + char * a = str; + char b[1] = {1}; + uint8_t ret = 0; + + while (b[0]!=0) { + f_read(fp, &b, 1, &rc); + if (b[0] != *a++) ret=-1; + } + return ret; +} +uint8_t findField( FIL* fp, char* str, uint8_t count, uint8_t padding ) { + char buf[4]; + unsigned int rc; + for (uint8_t i=0; i= currentTime) { + SetPPS( &PPS_Countdown ); + + if (currentTime - last_pps_time < config.tolerance_1ms){ + buffer_c[0].high= 0b11001110 | cSegDP; + SetSysTick( &SysTick_CountDown_P3 ); + } else if (currentTime - last_pps_time < config.tolerance_10ms){ + buffer_c[3].low = 0b01000000; + buffer_c[0].high= 0b11001110 | cSegDP; + SetSysTick( &SysTick_CountDown_P2 ); + } else if (currentTime - rtc_last_calibration < config.tolerance_100ms){ + buffer_c[3].low = 0b01000000; + buffer_c[2].low = 0b01000000; + buffer_c[0].high= 0b11001110 | cSegDP; + SetSysTick( &SysTick_CountDown_P1 ); + } else { + buffer_c[3].low = 0b01000000; + buffer_c[2].low = 0b01000000; + buffer_c[1].low = 0b01000000; + buffer_c[0].high= 0b11001110; + SetSysTick( &SysTick_CountDown_P0 ); + } + + } else { + countMode = COUNT_HIDDEN; + SetSysTick( &SysTick_CountUp_NoUpdate ); + SetPPS( &PPS_NoUpdate ); + buffer_c[0].high= 0b11001110 | cSegDP; + buffer_c[0].low=cSegDecode0; + buffer_c[1].low=cSegDecode0; + buffer_c[2].low=cSegDecode0; + buffer_c[3].low=cSegDecode0; + + next7seg.b[0] = bCat0 | cLut[0]<<2; + next7seg.b[1] = bCat1 | cLut[0]<<2; + next7seg.b[2] = bCat2 | cLut[0]<<2; + next7seg.b[3] = bCat3 | cLut[0]<<2; + next7seg.b[4] = bCat4 | cLut[0]<<2; + next7seg.c = cLut[0]; + } + + } +} + +#define justExited(x) ((oldMode==x) && (displayMode != x)) +void nextMode(_Bool reverse){ + + uint8_t oldMode = displayMode; + + if (requestMode!=255){ + if (!config.modes_enabled[requestMode]) { + requestMode=255; + return; + } + displayMode=requestMode; + requestMode=255; + } else if (reverse) { + do { + if (--displayMode >= NUM_DISPLAY_MODES) displayMode=NUM_DISPLAY_MODES-1; + } while (!config.modes_enabled[displayMode]); + } else { + do { + if (++displayMode >=NUM_DISPLAY_MODES) displayMode=0; + } while (!config.modes_enabled[displayMode]); + } + + if (justExited(MODE_VBAT)) vbat = 0.0; + if (justExited(MODE_STANDBY)) displayOn(); + if (justExited(MODE_DISPLAYTEST)) { + buffer_c[1].high &= ~cSegDP; + buffer_c[2].high &= ~cSegDP; + buffer_c[3].high &= ~cSegDP; + } + if ( displayMode == MODE_ISO_WEEK || justExited(MODE_COUNTDOWN)) { + // If we exit countdown mode at .9 seconds + // it will show the wrong time for .1 seconds + setNextTimestamp(currentTime); + } + + if (displayMode == MODE_SHOW_OFFSET || displayMode == MODE_DISPLAYTEST) { + countMode = COUNT_HIDDEN; + SetSysTick( &SysTick_CountUp_NoUpdate ); + SetPPS( &PPS_NoUpdate ); + colonAnimationStop() + TIM2->CCR1 = 0; // specific to show_offset + TIM2->CCR2 = 300; + } else if (displayMode == MODE_COUNTDOWN) { + + if (config.countdown_to >= currentTime) { + countMode = COUNT_DOWN; + setNextCountdown(currentTime); + } else { + countMode = COUNT_HIDDEN; + countdown_days = 0; + } + setPrecision(); + TIM2->CCR1 = 0; + TIM2->CCR2 = 0; + latchSegments(); + + } + else { + if (countMode != COUNT_NORMAL) { + countMode = COUNT_NORMAL; + setPrecision(); + SetPPS( &PPS ); + TIM2->CCR1 = 0; + TIM2->CCR2 = 0; + latchSegments(); + } + } + sendDate(1); +} +void button1pressed(void){ + nextMode(0); +} +void button2pressed(void){ + nextMode(1); +} +void buttonsBothHeld(void){ + HAL_TIM_PWM_Stop(&htim2, TIM_CHANNEL_1); + HAL_TIM_PWM_Stop(&htim2, TIM_CHANNEL_2); + + HAL_DMA_Abort(&hdma_tim1_up); + HAL_DMA_Abort(&hdma_tim7_up); + GPIOB->ODR=0; + GPIOC->ODR=0; + + NVIC_SystemReset(); +} + +void generateDACbuffer(uint16_t * buf) { + + static float dac_last=4095; + + + if (displayMode == MODE_STANDBY) { + dac_target = dac_target*0.7 + 1.2*4095.0*0.3; + if (dac_target>4094.0) { + dac_target=4095.0; + displayOff(); + } + } else if (config.brightness_override >=0.0) { + dac_target = config.brightness_override; + } else { + float adc = (float)ADC1->DR; + + uint8_t i; + for (i=1; i< sizeof(brightnessCurve)/sizeof(brightnessCurve[0]) -1; i++){ + if (brightnessCurve[i].in > adc) break; + } + float factor = (adc - brightnessCurve[i-1].in) / (brightnessCurve[i].in - brightnessCurve[i-1].in); + + float out = brightnessCurve[i-1].out*(1.0-factor) + brightnessCurve[i].out*factor; + + if (out>4095.0 || !isfinite(out)) out=4095.0; + else if (out<0.0) out=0.0; + + dac_target = dac_target*0.5 + out*0.5; + } + + + HAL_ADC_Start(&hadc1); + + + + float step = (dac_target-dac_last)/(DAC_BUFFER_SIZE*0.5); + for (size_t i=0; iVTOR = (uint32_t)&__VECTORS_RAM; + + SetSysTick( &SysTick_Dummy ); + + + /* USER CODE END 1 */ + + /* MCU Configuration--------------------------------------------------------*/ + + /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ + HAL_Init(); + + /* USER CODE BEGIN Init */ + + /* USER CODE END Init */ + + /* Configure the system clock */ + SystemClock_Config(); + + /* USER CODE BEGIN SysInit */ + // Enable the DWT cycle counter (free-running at the 80 MHz core clock, 12.5 ns/tick, wraps ~53.7 s): // the monotonic timebase the PPS edge and each USB SOF are both latched against for host correlation. CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; - buffer_c[0].high=0b11001110; - buffer_c[1].high=0b11001101; - buffer_c[2].high=0b11001011; - buffer_c[3].high=0b11000111; - buffer_c[4].high=0b11001111; - - /* USER CODE END SysInit */ - - /* Initialize all configured peripherals */ - MX_GPIO_Init(); - MX_DMA_Init(); - MX_QUADSPI_Init(); - MX_TIM1_Init(); - MX_USART2_UART_Init(); - MX_FATFS_Init(); - //MX_USB_DEVICE_Init(); - MX_USART1_UART_Init(); - MX_TIM2_Init(); - MX_ADC1_Init(); - MX_DAC1_Init(); - MX_TIM6_Init(); - MX_TIM7_Init(); - MX_CRC_Init(); - MX_LPTIM1_Init(); - MX_TIM5_Init(); - /* USER CODE BEGIN 2 */ - - - // Configure display matrix - if (HAL_DMA_Start(&hdma_tim7_up, (uint32_t)buffer_c, (uint32_t)&GPIOC->ODR, 5) != HAL_OK) - Error_Handler(); - - if (HAL_DMA_Start(&hdma_tim1_up, (uint32_t)buffer_b, (uint32_t)&GPIOB->ODR, 5) != HAL_OK) - Error_Handler(); - - __HAL_TIM_ENABLE_DMA(&htim1, TIM_DMA_UPDATE); - __HAL_TIM_ENABLE(&htim1); - - __HAL_TIM_ENABLE_DMA(&htim7, TIM_DMA_UPDATE); - __HAL_TIM_ENABLE(&htim7); - - - doDateUpdate(); - MX_USB_DEVICE_Init(); - - // Enable UART2 interrupt for button presses - USART2->CR1 |= USART_CR1_RXNEIE; - - - // Configure UART1 for NMEA strings from GPS module - USART1->CR1 |= USART_CR1_CMIE ; - - USART1->CR1 &= ~(USART_CR1_UE); - USART1->CR2 |= '\n'<<24; - USART1->CR1 |= USART_CR1_UE; - - - MX_ADC3_Init(); - - // Configure ADC and DAC DMA for display brightness - HAL_ADC_Start(&hadc1); - HAL_TIM_Base_Start(&htim6); - - if (HAL_DAC_Start_DMA(&hdac1, DAC_CHANNEL_1, (uint32_t*)buffer_dac, DAC_BUFFER_SIZE, DAC_ALIGN_12B_R) !=HAL_OK) - Error_Handler(); - - // Configure Colon Separators - TIM2->CCR1 = 0; - TIM2->CCR2 = 0; - - //loadColonAnimation(); - - __HAL_TIM_ENABLE_DMA(&htim5, TIM_DMA_CC1 | TIM_DMA_CC2); - __HAL_TIM_ENABLE(&htim5); - - //colonAnimationStart() - - - //Enable DP for subseconds - buffer_c[0].high=0b11001110 | cSegDP; - - - - buffer_c[0].low=cSegDecode0; - buffer_c[1].low=cSegDecode0; - buffer_c[2].low=cSegDecode0; - buffer_c[3].low=cSegDecode0; - - next7seg.c = buffer_c[0].low; - - next7seg.b[0] = buffer_b[0] = bCat0 | bSegDecode0; - next7seg.b[1] = buffer_b[1] = bCat1 | bSegDecode0; - next7seg.b[2] = buffer_b[2] = bCat2 | bSegDecode0; - next7seg.b[3] = buffer_b[3] = bCat3 | bSegDecode0; - next7seg.b[4] = buffer_b[4] = bCat4 | bSegDecode0; - - //setDisplayPWM(5); - displayOn(); - - readConfigFile(); - checkDelayedLoadRules(); - - measure_vbat(); - - if (RTC->ISR & RTC_ISR_INITS) //RTC contains non-zero data - { - RTC_DateTypeDef sdate; - RTC_TimeTypeDef stime; - - if (!config.zone_override){ - char zone[32]; - memcpyword( (uint32_t*)zone, (uint32_t*)&(RTC->BKP0R), 8 ); - zone[31]=0; - - if (loadRulesSingle(zone) != RULES_OK){ // takes ~8ms - memcpyword( (uint32_t*)loadedRulesString, (uint32_t*)&(RTC->BKP0R), 8 ); - loadedRulesString[31]=0;//paranoia - memcpyword( (uint32_t*)rules, (uint32_t*)&(RTC->BKP8R), 22 ); - } - } - - - hrtc.Instance = RTC; - HAL_RTC_GetTime(&hrtc, &stime, RTC_FORMAT_BIN); - HAL_RTC_GetDate(&hrtc, &sdate, RTC_FORMAT_BIN); - - struct tm out; - - out.tm_isdst = 0; - - out.tm_sec = stime.Seconds; - out.tm_min = stime.Minutes; - out.tm_hour = stime.Hours; - out.tm_mday = sdate.Date; - out.tm_mon = sdate.Month -1; - out.tm_year = sdate.Year + 100; //Years since 1900 - - currentTime = mktime(&out); - - float fraction = (float)(32767 - stime.SubSeconds) / 32768.0; - - // SysTick->VAL = SysTick->LOAD; ? - millisec = (uint32_t)(fraction*1000) % 10; - centisec = (uint32_t)(fraction*100) % 10; - decisec = (uint32_t)(fraction*10) % 10; - - if (decisec>=9) currentTime++; - - setNextTimestamp( currentTime ); - sendDate(1); - latchSegments(); - - // As the coin cell goes flat, the RTC stops ticking long before the backup registers die. - // Powering on with a flat battery means the clock thinks no time has passed, and assumes it has good precision. - // Explicitly stop this by checking the battery voltage. - if (vbat > 2.70) { - rtc_good=1; - } else { - // trash the calibration time to ensure lowest precision display - if (currentTime - rtc_last_calibration < config.tolerance_100ms) - rtc_last_calibration -= config.tolerance_100ms +1; - } - - } else { // backup domain reset - - currentTime=946684800; // 2000-01-01T00:00:00 - - // The init process blanks the subsecond registers - MX_RTC_Init(); - } - - vbat = 0.0; // don't allow measurement to go stale - - setPrecision(); - PPS_Init(); - HAL_UART_Receive_DMA(&huart1, nmea, sizeof(nmea)); - -//#define MEASURE_LOOKUP_TIME - - /* USER CODE END 2 */ - - /* Infinite loop */ - /* USER CODE BEGIN WHILE */ - while (1) - { - if (new_position && !qspi_write_time && !config.zone_override - && (data_valid || (config.fake_long && config.fake_lat)) - && latitude>=-90.0 && latitude<=90.0 && longitude>=-180.0 && longitude<=180.0) { - - new_position=0; - FIL mapfile; - if (f_open(&mapfile, MAP_FILENAME, FA_READ) == FR_OK) { -#ifdef MEASURE_LOOKUP_TIME - uint32_t start=uwTick; -#endif - ZoneDetect *const zdb = ZDOpenDatabase(&mapfile); - - if (!zdb) { - // mapfile error - } else { - char* zone = ZDHelperSimpleLookupString(zdb, latitude, longitude); -#ifdef MEASURE_LOOKUP_TIME - uint32_t ztime=uwTick-start; -#endif - if (zone && !delayedLoadRules) { -#ifdef MEASURE_LOOKUP_TIME - start=uwTick; -#endif - loadRulesSingle(zone); -#ifdef MEASURE_LOOKUP_TIME - sprintf(textDisplay,"d%ld L%ld",ztime, uwTick-start); -#endif - } - free(zone); - ZDCloseDatabase(zdb); - //f_close(&mapfile); - } - } - // else no_map = 1 - } - - if (delayedCheckOnEject) firmwareCheckOnEject(); - - if (delayedReadConfigFile) { - FATFS_remount(); - readConfigFile(); - delayedReadConfigFile=0; - } - - checkDelayedLoadRules(); - - if (delayedDisplayFreq) setDisplayFreq(delayedDisplayFreq); - - monitor_vbus(); - - if (pps_ts_enabled) { - static uint32_t last_temp_read = 0; - if ((uint32_t)currentTime - last_temp_read >= 4) { // refresh die temp every ~4 s - last_temp_read = (uint32_t)currentTime; - measure_temp(); - } - if (pps_record_pending) emitPPSTimestamp(); // emit clears pending itself on success - } - - if (displayMode == MODE_VBAT) - measure_vbat(); - - if (displayMode == MODE_SUN || displayMode == MODE_SUN_AZEL || displayMode == MODE_MOON - || displayMode == MODE_GRID || displayMode == MODE_LATLON) - astro_update(); - - - /* USER CODE END WHILE */ - - /* USER CODE BEGIN 3 */ - } - /* USER CODE END 3 */ -} - -/** - * @brief System Clock Configuration - * @retval None - */ -void SystemClock_Config(void) -{ - RCC_OscInitTypeDef RCC_OscInitStruct = {0}; - RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; - RCC_PeriphCLKInitTypeDef PeriphClkInit = {0}; - - /** Configure LSE Drive Capability - */ - HAL_PWR_EnableBkUpAccess(); - __HAL_RCC_LSEDRIVE_CONFIG(RCC_LSEDRIVE_LOW); - /** Initializes the CPU, AHB and APB busses clocks - */ - RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE|RCC_OSCILLATORTYPE_LSE - |RCC_OSCILLATORTYPE_MSI; - RCC_OscInitStruct.HSEState = RCC_HSE_ON; - RCC_OscInitStruct.LSEState = RCC_LSE_ON; - RCC_OscInitStruct.MSIState = RCC_MSI_ON; - RCC_OscInitStruct.MSICalibrationValue = 0; - RCC_OscInitStruct.MSIClockRange = RCC_MSIRANGE_11; - RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; - RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; - RCC_OscInitStruct.PLL.PLLM = 2; - RCC_OscInitStruct.PLL.PLLN = 64; - RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7; - RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2; - RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV4; - if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) - { - Error_Handler(); - } - /** Initializes the CPU, AHB and APB busses clocks - */ - RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK - |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; - RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; - RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; - RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; - RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; - - if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK) - { - Error_Handler(); - } - PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_RTC|RCC_PERIPHCLK_USART1 - |RCC_PERIPHCLK_USART2|RCC_PERIPHCLK_LPTIM1 - |RCC_PERIPHCLK_USB|RCC_PERIPHCLK_ADC; - PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2; - PeriphClkInit.Usart2ClockSelection = RCC_USART2CLKSOURCE_PCLK1; - PeriphClkInit.Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSE; - PeriphClkInit.AdcClockSelection = RCC_ADCCLKSOURCE_SYSCLK; - PeriphClkInit.RTCClockSelection = RCC_RTCCLKSOURCE_LSE; - PeriphClkInit.UsbClockSelection = RCC_USBCLKSOURCE_MSI; - if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) - { - Error_Handler(); - } - /** Configure the main internal regulator output voltage - */ - if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK) - { - Error_Handler(); - } - /** Enable MSI Auto calibration - */ - HAL_RCCEx_EnableMSIPLLMode(); -} - -/** - * @brief ADC1 Initialization Function - * @param None - * @retval None - */ -static void MX_ADC1_Init(void) -{ - - /* USER CODE BEGIN ADC1_Init 0 */ - - /* USER CODE END ADC1_Init 0 */ - - ADC_MultiModeTypeDef multimode = {0}; - ADC_ChannelConfTypeDef sConfig = {0}; - - /* USER CODE BEGIN ADC1_Init 1 */ - - /* USER CODE END ADC1_Init 1 */ - /** Common config - */ - hadc1.Instance = ADC1; - hadc1.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV1; - hadc1.Init.Resolution = ADC_RESOLUTION_12B; - hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; - hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE; - hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV; - hadc1.Init.LowPowerAutoWait = DISABLE; - hadc1.Init.ContinuousConvMode = DISABLE; - hadc1.Init.NbrOfConversion = 1; - hadc1.Init.DiscontinuousConvMode = DISABLE; - hadc1.Init.NbrOfDiscConversion = 1; - hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; - hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; - hadc1.Init.DMAContinuousRequests = DISABLE; - hadc1.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN; - hadc1.Init.OversamplingMode = DISABLE; - if (HAL_ADC_Init(&hadc1) != HAL_OK) - { - Error_Handler(); - } - /** Configure the ADC multi-mode - */ - multimode.Mode = ADC_MODE_INDEPENDENT; - if (HAL_ADCEx_MultiModeConfigChannel(&hadc1, &multimode) != HAL_OK) - { - Error_Handler(); - } - /** Configure Regular Channel - */ - sConfig.Channel = ADC_CHANNEL_10; - sConfig.Rank = ADC_REGULAR_RANK_1; - sConfig.SamplingTime = ADC_SAMPLETIME_92CYCLES_5; - sConfig.SingleDiff = ADC_SINGLE_ENDED; - sConfig.OffsetNumber = ADC_OFFSET_NONE; - sConfig.Offset = 0; - if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) - { - Error_Handler(); - } - /* USER CODE BEGIN ADC1_Init 2 */ - - /* USER CODE END ADC1_Init 2 */ - -} - -/** - * @brief ADC3 Initialization Function - * @param None - * @retval None - */ -static void MX_ADC3_Init(void) -{ - - /* USER CODE BEGIN ADC3_Init 0 */ - - /* USER CODE END ADC3_Init 0 */ - - ADC_ChannelConfTypeDef sConfig = {0}; - - /* USER CODE BEGIN ADC3_Init 1 */ - - - /* USER CODE END ADC3_Init 1 */ - /** Common config - */ - hadc3.Instance = ADC3; - hadc3.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV2; - hadc3.Init.Resolution = ADC_RESOLUTION_12B; - hadc3.Init.DataAlign = ADC_DATAALIGN_RIGHT; - hadc3.Init.ScanConvMode = ADC_SCAN_DISABLE; - hadc3.Init.EOCSelection = ADC_EOC_SINGLE_CONV; - hadc3.Init.LowPowerAutoWait = DISABLE; - hadc3.Init.ContinuousConvMode = DISABLE; - hadc3.Init.NbrOfConversion = 1; - hadc3.Init.DiscontinuousConvMode = DISABLE; - hadc3.Init.NbrOfDiscConversion = 1; - hadc3.Init.ExternalTrigConv = ADC_SOFTWARE_START; - hadc3.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; - hadc3.Init.DMAContinuousRequests = DISABLE; - hadc3.Init.Overrun = ADC_OVR_DATA_PRESERVED; - hadc3.Init.OversamplingMode = ENABLE; - hadc3.Init.Oversampling.Ratio = ADC_OVERSAMPLING_RATIO_16; - hadc3.Init.Oversampling.RightBitShift = ADC_RIGHTBITSHIFT_4; - hadc3.Init.Oversampling.TriggeredMode = ADC_TRIGGEREDMODE_SINGLE_TRIGGER; - hadc3.Init.Oversampling.OversamplingStopReset = ADC_REGOVERSAMPLING_RESUMED_MODE; - - if (HAL_ADC_Init(&hadc3) != HAL_OK) - { - Error_Handler(); - } - - HAL_ADCEx_Calibration_Start(&hadc3, ADC_SINGLE_ENDED); - - /** Configure Regular Channel - */ - sConfig.Channel = ADC_CHANNEL_VBAT; - sConfig.Rank = ADC_REGULAR_RANK_1; - sConfig.SamplingTime = ADC_SAMPLETIME_640CYCLES_5; - sConfig.SingleDiff = ADC_SINGLE_ENDED; - sConfig.OffsetNumber = ADC_OFFSET_NONE; - sConfig.Offset = 0; - if (HAL_ADC_ConfigChannel(&hadc3, &sConfig) != HAL_OK) - { - Error_Handler(); - } - /* USER CODE BEGIN ADC3_Init 2 */ - ADC123_COMMON->CCR &= ~ADC_CCR_VBATEN; - /* USER CODE END ADC3_Init 2 */ - -} - -/** - * @brief CRC Initialization Function - * @param None - * @retval None - */ -static void MX_CRC_Init(void) -{ - - /* USER CODE BEGIN CRC_Init 0 */ - - /* USER CODE END CRC_Init 0 */ - - /* USER CODE BEGIN CRC_Init 1 */ - - /* USER CODE END CRC_Init 1 */ - hcrc.Instance = CRC; - hcrc.Init.DefaultPolynomialUse = DEFAULT_POLYNOMIAL_ENABLE; - hcrc.Init.DefaultInitValueUse = DEFAULT_INIT_VALUE_ENABLE; - hcrc.Init.InputDataInversionMode = CRC_INPUTDATA_INVERSION_BYTE; - hcrc.Init.OutputDataInversionMode = CRC_OUTPUTDATA_INVERSION_ENABLE; - hcrc.InputDataFormat = CRC_INPUTDATA_FORMAT_WORDS; - if (HAL_CRC_Init(&hcrc) != HAL_OK) - { - Error_Handler(); - } - /* USER CODE BEGIN CRC_Init 2 */ - - /* USER CODE END CRC_Init 2 */ - -} - -/** - * @brief DAC1 Initialization Function - * @param None - * @retval None - */ -static void MX_DAC1_Init(void) -{ - - /* USER CODE BEGIN DAC1_Init 0 */ - - /* USER CODE END DAC1_Init 0 */ - - DAC_ChannelConfTypeDef sConfig = {0}; - - /* USER CODE BEGIN DAC1_Init 1 */ - - /* USER CODE END DAC1_Init 1 */ - /** DAC Initialization - */ - hdac1.Instance = DAC1; - if (HAL_DAC_Init(&hdac1) != HAL_OK) - { - Error_Handler(); - } - /** DAC channel OUT1 config - */ - sConfig.DAC_SampleAndHold = DAC_SAMPLEANDHOLD_DISABLE; - sConfig.DAC_Trigger = DAC_TRIGGER_T6_TRGO; - sConfig.DAC_OutputBuffer = DAC_OUTPUTBUFFER_ENABLE; - sConfig.DAC_ConnectOnChipPeripheral = DAC_CHIPCONNECT_DISABLE; - sConfig.DAC_UserTrimming = DAC_TRIMMING_FACTORY; - if (HAL_DAC_ConfigChannel(&hdac1, &sConfig, DAC_CHANNEL_1) != HAL_OK) - { - Error_Handler(); - } - /* USER CODE BEGIN DAC1_Init 2 */ - HAL_DAC_SetValue(&hdac1, DAC_CHANNEL_1, DAC_ALIGN_12B_R, 4095); - /* USER CODE END DAC1_Init 2 */ - -} - -/** - * @brief LPTIM1 Initialization Function - * @param None - * @retval None - */ -static void MX_LPTIM1_Init(void) -{ - - /* USER CODE BEGIN LPTIM1_Init 0 */ - - /* USER CODE END LPTIM1_Init 0 */ - - /* Peripheral clock enable */ - LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_LPTIM1); - - /* LPTIM1 interrupt Init */ - NVIC_SetPriority(LPTIM1_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(),1, 0)); - NVIC_EnableIRQ(LPTIM1_IRQn); - - /* USER CODE BEGIN LPTIM1_Init 1 */ - - /* USER CODE END LPTIM1_Init 1 */ - LL_LPTIM_SetClockSource(LPTIM1, LL_LPTIM_CLK_SOURCE_INTERNAL); - LL_LPTIM_SetPrescaler(LPTIM1, LL_LPTIM_PRESCALER_DIV1); - LL_LPTIM_SetPolarity(LPTIM1, LL_LPTIM_OUTPUT_POLARITY_REGULAR); - LL_LPTIM_SetUpdateMode(LPTIM1, LL_LPTIM_UPDATE_MODE_IMMEDIATE); - LL_LPTIM_SetCounterMode(LPTIM1, LL_LPTIM_COUNTER_MODE_INTERNAL); - LL_LPTIM_TrigSw(LPTIM1); - LL_LPTIM_SetInput1Src(LPTIM1, LL_LPTIM_INPUT1_SRC_GPIO); - LL_LPTIM_SetInput2Src(LPTIM1, LL_LPTIM_INPUT2_SRC_GPIO); - /* USER CODE BEGIN LPTIM1_Init 2 */ - - LL_LPTIM_Enable(LPTIM1); - LL_LPTIM_SetAutoReload(LPTIM1, 0xFFFF); - LL_LPTIM_EnableIT_ARRM(LPTIM1); - - /* USER CODE END LPTIM1_Init 2 */ - -} - -/** - * @brief QUADSPI Initialization Function - * @param None - * @retval None - */ -static void MX_QUADSPI_Init(void) -{ - - /* USER CODE BEGIN QUADSPI_Init 0 */ - - /* USER CODE END QUADSPI_Init 0 */ - - /* USER CODE BEGIN QUADSPI_Init 1 */ - - /* USER CODE END QUADSPI_Init 1 */ - /* QUADSPI parameter configuration*/ - hqspi.Instance = QUADSPI; - hqspi.Init.ClockPrescaler = 0; - hqspi.Init.FifoThreshold = 4; - hqspi.Init.SampleShifting = QSPI_SAMPLE_SHIFTING_HALFCYCLE; - hqspi.Init.FlashSize = 23; - hqspi.Init.ChipSelectHighTime = QSPI_CS_HIGH_TIME_1_CYCLE; - hqspi.Init.ClockMode = QSPI_CLOCK_MODE_0; - if (HAL_QSPI_Init(&hqspi) != HAL_OK) - { - Error_Handler(); - } - /* USER CODE BEGIN QUADSPI_Init 2 */ - - /* USER CODE END QUADSPI_Init 2 */ - -} - -/** - * @brief RTC Initialization Function - * @param None - * @retval None - */ -static void MX_RTC_Init(void) -{ - - /* USER CODE BEGIN RTC_Init 0 */ - - /* USER CODE END RTC_Init 0 */ - - /* USER CODE BEGIN RTC_Init 1 */ - - /* USER CODE END RTC_Init 1 */ - /** Initialize RTC Only - */ - hrtc.Instance = RTC; - hrtc.Init.HourFormat = RTC_HOURFORMAT_24; - hrtc.Init.AsynchPrediv = 0; - hrtc.Init.SynchPrediv = 32759; - hrtc.Init.OutPut = RTC_OUTPUT_DISABLE; - hrtc.Init.OutPutRemap = RTC_OUTPUT_REMAP_NONE; - hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH; - hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN; - if (HAL_RTC_Init(&hrtc) != HAL_OK) - { - Error_Handler(); - } - /* USER CODE BEGIN RTC_Init 2 */ - - // RM page 1236 - __HAL_RTC_WRITEPROTECTION_DISABLE(&hrtc); - RTC->CALR = 0x100; // CALM to midpoint - __HAL_RTC_WRITEPROTECTION_ENABLE(&hrtc); - - /* USER CODE END RTC_Init 2 */ - -} - -/** - * @brief TIM1 Initialization Function - * @param None - * @retval None - */ -static void MX_TIM1_Init(void) -{ - - /* USER CODE BEGIN TIM1_Init 0 */ - - /* USER CODE END TIM1_Init 0 */ - - TIM_ClockConfigTypeDef sClockSourceConfig = {0}; - TIM_MasterConfigTypeDef sMasterConfig = {0}; - - /* USER CODE BEGIN TIM1_Init 1 */ - - /* USER CODE END TIM1_Init 1 */ - htim1.Instance = TIM1; - htim1.Init.Prescaler = 0; - htim1.Init.CounterMode = TIM_COUNTERMODE_UP; - htim1.Init.Period = 256; - htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; - htim1.Init.RepetitionCounter = 0; - htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; - if (HAL_TIM_Base_Init(&htim1) != HAL_OK) - { - Error_Handler(); - } - sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL; - if (HAL_TIM_ConfigClockSource(&htim1, &sClockSourceConfig) != HAL_OK) - { - Error_Handler(); - } - sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; - sMasterConfig.MasterOutputTrigger2 = TIM_TRGO2_RESET; - sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; - if (HAL_TIMEx_MasterConfigSynchronization(&htim1, &sMasterConfig) != HAL_OK) - { - Error_Handler(); - } - /* USER CODE BEGIN TIM1_Init 2 */ - - /* USER CODE END TIM1_Init 2 */ - -} - -/** - * @brief TIM2 Initialization Function - * @param None - * @retval None - */ -static void MX_TIM2_Init(void) -{ - - /* USER CODE BEGIN TIM2_Init 0 */ - - /* USER CODE END TIM2_Init 0 */ - - TIM_MasterConfigTypeDef sMasterConfig = {0}; - TIM_OC_InitTypeDef sConfigOC = {0}; - - /* USER CODE BEGIN TIM2_Init 1 */ - - /* USER CODE END TIM2_Init 1 */ - htim2.Instance = TIM2; - htim2.Init.Prescaler = 8; - htim2.Init.CounterMode = TIM_COUNTERMODE_UP; - htim2.Init.Period = 10000; - htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; - htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; - if (HAL_TIM_PWM_Init(&htim2) != HAL_OK) - { - Error_Handler(); - } - sMasterConfig.MasterOutputTrigger = TIM_TRGO_OC2REF; - sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; - if (HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig) != HAL_OK) - { - Error_Handler(); - } - sConfigOC.OCMode = TIM_OCMODE_PWM2; - sConfigOC.Pulse = 0; - sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; - sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; - if (HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1) != HAL_OK) - { - Error_Handler(); - } - if (HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_2) != HAL_OK) - { - Error_Handler(); - } - /* USER CODE BEGIN TIM2_Init 2 */ - - /* USER CODE END TIM2_Init 2 */ - HAL_TIM_MspPostInit(&htim2); - -} - -/** - * @brief TIM5 Initialization Function - * @param None - * @retval None - */ -static void MX_TIM5_Init(void) -{ - - /* USER CODE BEGIN TIM5_Init 0 */ - - /* USER CODE END TIM5_Init 0 */ - - TIM_ClockConfigTypeDef sClockSourceConfig = {0}; - TIM_MasterConfigTypeDef sMasterConfig = {0}; - TIM_OC_InitTypeDef sConfigOC = {0}; - - /* USER CODE BEGIN TIM5_Init 1 */ - - /* USER CODE END TIM5_Init 1 */ - htim5.Instance = TIM5; - htim5.Init.Prescaler = 7999; - htim5.Init.CounterMode = TIM_COUNTERMODE_UP; - htim5.Init.Period = 99; - htim5.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; - htim5.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; - if (HAL_TIM_Base_Init(&htim5) != HAL_OK) - { - Error_Handler(); - } - sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL; - if (HAL_TIM_ConfigClockSource(&htim5, &sClockSourceConfig) != HAL_OK) - { - Error_Handler(); - } - if (HAL_TIM_OC_Init(&htim5) != HAL_OK) - { - Error_Handler(); - } - sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; - sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; - if (HAL_TIMEx_MasterConfigSynchronization(&htim5, &sMasterConfig) != HAL_OK) - { - Error_Handler(); - } - sConfigOC.OCMode = TIM_OCMODE_TIMING; - sConfigOC.Pulse = 0; - sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; - sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; - if (HAL_TIM_OC_ConfigChannel(&htim5, &sConfigOC, TIM_CHANNEL_1) != HAL_OK) - { - Error_Handler(); - } - if (HAL_TIM_OC_ConfigChannel(&htim5, &sConfigOC, TIM_CHANNEL_2) != HAL_OK) - { - Error_Handler(); - } - /* USER CODE BEGIN TIM5_Init 2 */ - - /* USER CODE END TIM5_Init 2 */ - -} - -/** - * @brief TIM6 Initialization Function - * @param None - * @retval None - */ -static void MX_TIM6_Init(void) -{ - - /* USER CODE BEGIN TIM6_Init 0 */ - - /* USER CODE END TIM6_Init 0 */ - - TIM_MasterConfigTypeDef sMasterConfig = {0}; - - /* USER CODE BEGIN TIM6_Init 1 */ - - /* USER CODE END TIM6_Init 1 */ - htim6.Instance = TIM6; - htim6.Init.Prescaler = 8000; - htim6.Init.CounterMode = TIM_COUNTERMODE_UP; - htim6.Init.Period = 100; - htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; - if (HAL_TIM_Base_Init(&htim6) != HAL_OK) - { - Error_Handler(); - } - sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE; - sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; - if (HAL_TIMEx_MasterConfigSynchronization(&htim6, &sMasterConfig) != HAL_OK) - { - Error_Handler(); - } - /* USER CODE BEGIN TIM6_Init 2 */ - - /* USER CODE END TIM6_Init 2 */ - -} - -/** - * @brief TIM7 Initialization Function - * @param None - * @retval None - */ -static void MX_TIM7_Init(void) -{ - - /* USER CODE BEGIN TIM7_Init 0 */ - - /* USER CODE END TIM7_Init 0 */ - - TIM_MasterConfigTypeDef sMasterConfig = {0}; - - /* USER CODE BEGIN TIM7_Init 1 */ - - /* USER CODE END TIM7_Init 1 */ - htim7.Instance = TIM7; - htim7.Init.Prescaler = 0; - htim7.Init.CounterMode = TIM_COUNTERMODE_UP; - htim7.Init.Period = 256; - htim7.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; - if (HAL_TIM_Base_Init(&htim7) != HAL_OK) - { - Error_Handler(); - } - sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; - sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; - if (HAL_TIMEx_MasterConfigSynchronization(&htim7, &sMasterConfig) != HAL_OK) - { - Error_Handler(); - } - /* USER CODE BEGIN TIM7_Init 2 */ - - /* USER CODE END TIM7_Init 2 */ - -} - -/** - * @brief USART1 Initialization Function - * @param None - * @retval None - */ -static void MX_USART1_UART_Init(void) -{ - - /* USER CODE BEGIN USART1_Init 0 */ - - /* USER CODE END USART1_Init 0 */ - - /* USER CODE BEGIN USART1_Init 1 */ - - /* USER CODE END USART1_Init 1 */ - huart1.Instance = USART1; - huart1.Init.BaudRate = 9600; - huart1.Init.WordLength = UART_WORDLENGTH_8B; - huart1.Init.StopBits = UART_STOPBITS_1; - huart1.Init.Parity = UART_PARITY_NONE; - huart1.Init.Mode = UART_MODE_TX_RX; - huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; - huart1.Init.OverSampling = UART_OVERSAMPLING_16; - huart1.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE; - huart1.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT; - if (HAL_UART_Init(&huart1) != HAL_OK) - { - Error_Handler(); - } - /* USER CODE BEGIN USART1_Init 2 */ - - /* USER CODE END USART1_Init 2 */ - -} - -/** - * @brief USART2 Initialization Function - * @param None - * @retval None - */ -static void MX_USART2_UART_Init(void) -{ - - /* USER CODE BEGIN USART2_Init 0 */ - - /* USER CODE END USART2_Init 0 */ - - /* USER CODE BEGIN USART2_Init 1 */ - - /* USER CODE END USART2_Init 1 */ - huart2.Instance = USART2; - huart2.Init.BaudRate = 115200; - huart2.Init.WordLength = UART_WORDLENGTH_9B; - huart2.Init.StopBits = UART_STOPBITS_1; - huart2.Init.Parity = UART_PARITY_EVEN; - huart2.Init.Mode = UART_MODE_TX_RX; - huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE; - huart2.Init.OverSampling = UART_OVERSAMPLING_16; - huart2.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE; - huart2.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_RXOVERRUNDISABLE_INIT; - huart2.AdvancedInit.OverrunDisable = UART_ADVFEATURE_OVERRUN_DISABLE; - if (HAL_UART_Init(&huart2) != HAL_OK) - { - Error_Handler(); - } - /* USER CODE BEGIN USART2_Init 2 */ - - /* USER CODE END USART2_Init 2 */ - -} - -/** - * Enable DMA controller clock - */ -static void MX_DMA_Init(void) -{ - - /* DMA controller clock enable */ - __HAL_RCC_DMA1_CLK_ENABLE(); - __HAL_RCC_DMA2_CLK_ENABLE(); - - /* DMA interrupt init */ - /* DMA1_Channel3_IRQn interrupt configuration */ - HAL_NVIC_SetPriority(DMA1_Channel3_IRQn, 1, 0); - HAL_NVIC_EnableIRQ(DMA1_Channel3_IRQn); - /* DMA1_Channel4_IRQn interrupt configuration */ - HAL_NVIC_SetPriority(DMA1_Channel4_IRQn, 0, 0); - HAL_NVIC_EnableIRQ(DMA1_Channel4_IRQn); - /* DMA1_Channel5_IRQn interrupt configuration */ - HAL_NVIC_SetPriority(DMA1_Channel5_IRQn, 0, 0); - HAL_NVIC_EnableIRQ(DMA1_Channel5_IRQn); - /* DMA1_Channel6_IRQn interrupt configuration */ - HAL_NVIC_SetPriority(DMA1_Channel6_IRQn, 0, 0); - HAL_NVIC_EnableIRQ(DMA1_Channel6_IRQn); - /* DMA1_Channel7_IRQn interrupt configuration */ - HAL_NVIC_SetPriority(DMA1_Channel7_IRQn, 0, 0); - HAL_NVIC_EnableIRQ(DMA1_Channel7_IRQn); - /* DMA2_Channel4_IRQn interrupt configuration */ - HAL_NVIC_SetPriority(DMA2_Channel4_IRQn, 0, 0); - HAL_NVIC_EnableIRQ(DMA2_Channel4_IRQn); - /* DMA2_Channel5_IRQn interrupt configuration */ - HAL_NVIC_SetPriority(DMA2_Channel5_IRQn, 0, 0); - HAL_NVIC_EnableIRQ(DMA2_Channel5_IRQn); - -} - -/** - * @brief GPIO Initialization Function - * @param None - * @retval None - */ -static void MX_GPIO_Init(void) -{ - GPIO_InitTypeDef GPIO_InitStruct = {0}; - - /* GPIO Ports Clock Enable */ - __HAL_RCC_GPIOC_CLK_ENABLE(); - __HAL_RCC_GPIOH_CLK_ENABLE(); - __HAL_RCC_GPIOA_CLK_ENABLE(); - __HAL_RCC_GPIOB_CLK_ENABLE(); - - /*Configure GPIO pin Output Level */ - HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13|GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2 - |GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5|GPIO_PIN_6 - |GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11 - |GPIO_PIN_12, GPIO_PIN_RESET); - - /*Configure GPIO pin Output Level */ - HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2|GPIO_PIN_12|GPIO_PIN_13|GPIO_PIN_14 - |GPIO_PIN_15|GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5 - |GPIO_PIN_6|GPIO_PIN_7|GPIO_PIN_8|GPIO_PIN_9, GPIO_PIN_RESET); - - /*Configure GPIO pins : PC13 PC0 PC1 PC2 - PC3 PC4 PC5 PC6 - PC8 PC9 PC10 PC11 - PC12 */ - GPIO_InitStruct.Pin = GPIO_PIN_13|GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2 - |GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5|GPIO_PIN_6 - |GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11 - |GPIO_PIN_12; - GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; - GPIO_InitStruct.Pull = GPIO_NOPULL; - GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; - HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); - - /*Configure GPIO pins : PB2 PB12 PB13 PB14 - PB15 PB3 PB4 PB5 - PB6 PB7 PB8 PB9 */ - GPIO_InitStruct.Pin = GPIO_PIN_2|GPIO_PIN_12|GPIO_PIN_13|GPIO_PIN_14 - |GPIO_PIN_15|GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5 - |GPIO_PIN_6|GPIO_PIN_7|GPIO_PIN_8|GPIO_PIN_9; - GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; - GPIO_InitStruct.Pull = GPIO_NOPULL; - GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; - HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); - - /*Configure GPIO pin : PA8 */ - GPIO_InitStruct.Pin = GPIO_PIN_8; - GPIO_InitStruct.Mode = GPIO_MODE_INPUT; - GPIO_InitStruct.Pull = GPIO_PULLUP; - HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); - -} - -/* USER CODE BEGIN 4 */ - -/* USER CODE END 4 */ - -/** - * @brief This function is executed in case of error occurrence. - * @retval None - */ -void Error_Handler(void) -{ - /* USER CODE BEGIN Error_Handler_Debug */ - /* User can add his own implementation to report the HAL error return state */ - - __disable_irq(); - - buffer_c[0].high=0b11011110; - buffer_c[1].high=0b11011101; - buffer_c[2].high=0b11011011; - buffer_c[3].high=0b11010111; - buffer_c[4].high=0b11001111; - buffer_c[0].low=0b01010000; - buffer_c[1].low=0b01010000; - buffer_c[2].low=0b01011100; - buffer_c[3].low=0b01010000; - buffer_c[4].low=0; - - buffer_b[0] = bCat0; - buffer_b[1] = bCat1; - buffer_b[2] = bCat2; - buffer_b[3] = bCat3; - buffer_b[4] = bCat4 | 0b0111100100; - - //setDisplayPWM(5); - - - - - while(1); - /* USER CODE END Error_Handler_Debug */ -} - -#ifdef USE_FULL_ASSERT -/** - * @brief Reports the name of the source file and the source line number - * where the assert_param error has occurred. - * @param file: pointer to the source file name - * @param line: assert_param error line source number - * @retval None - */ -void assert_failed(uint8_t *file, uint32_t line) -{ - /* USER CODE BEGIN 6 */ - /* User can add his own implementation to report the file name and line number, - tex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */ - /* USER CODE END 6 */ -} -#endif /* USE_FULL_ASSERT */ - -/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/ + buffer_c[0].high=0b11001110; + buffer_c[1].high=0b11001101; + buffer_c[2].high=0b11001011; + buffer_c[3].high=0b11000111; + buffer_c[4].high=0b11001111; + + /* USER CODE END SysInit */ + + /* Initialize all configured peripherals */ + MX_GPIO_Init(); + MX_DMA_Init(); + MX_QUADSPI_Init(); + MX_TIM1_Init(); + MX_USART2_UART_Init(); + MX_FATFS_Init(); + //MX_USB_DEVICE_Init(); + MX_USART1_UART_Init(); + MX_TIM2_Init(); + MX_ADC1_Init(); + MX_DAC1_Init(); + MX_TIM6_Init(); + MX_TIM7_Init(); + MX_CRC_Init(); + MX_LPTIM1_Init(); + MX_TIM5_Init(); + /* USER CODE BEGIN 2 */ + + + // Configure display matrix + if (HAL_DMA_Start(&hdma_tim7_up, (uint32_t)buffer_c, (uint32_t)&GPIOC->ODR, 5) != HAL_OK) + Error_Handler(); + + if (HAL_DMA_Start(&hdma_tim1_up, (uint32_t)buffer_b, (uint32_t)&GPIOB->ODR, 5) != HAL_OK) + Error_Handler(); + + __HAL_TIM_ENABLE_DMA(&htim1, TIM_DMA_UPDATE); + __HAL_TIM_ENABLE(&htim1); + + __HAL_TIM_ENABLE_DMA(&htim7, TIM_DMA_UPDATE); + __HAL_TIM_ENABLE(&htim7); + + + doDateUpdate(); + MX_USB_DEVICE_Init(); + + // Enable UART2 interrupt for button presses + USART2->CR1 |= USART_CR1_RXNEIE; + + + // Configure UART1 for NMEA strings from GPS module + USART1->CR1 |= USART_CR1_CMIE ; + + USART1->CR1 &= ~(USART_CR1_UE); + USART1->CR2 |= '\n'<<24; + USART1->CR1 |= USART_CR1_UE; + + + MX_ADC3_Init(); + + // Configure ADC and DAC DMA for display brightness + HAL_ADC_Start(&hadc1); + HAL_TIM_Base_Start(&htim6); + + if (HAL_DAC_Start_DMA(&hdac1, DAC_CHANNEL_1, (uint32_t*)buffer_dac, DAC_BUFFER_SIZE, DAC_ALIGN_12B_R) !=HAL_OK) + Error_Handler(); + + // Configure Colon Separators + TIM2->CCR1 = 0; + TIM2->CCR2 = 0; + + //loadColonAnimation(); + + __HAL_TIM_ENABLE_DMA(&htim5, TIM_DMA_CC1 | TIM_DMA_CC2); + __HAL_TIM_ENABLE(&htim5); + + //colonAnimationStart() + + + //Enable DP for subseconds + buffer_c[0].high=0b11001110 | cSegDP; + + + + buffer_c[0].low=cSegDecode0; + buffer_c[1].low=cSegDecode0; + buffer_c[2].low=cSegDecode0; + buffer_c[3].low=cSegDecode0; + + next7seg.c = buffer_c[0].low; + + next7seg.b[0] = buffer_b[0] = bCat0 | bSegDecode0; + next7seg.b[1] = buffer_b[1] = bCat1 | bSegDecode0; + next7seg.b[2] = buffer_b[2] = bCat2 | bSegDecode0; + next7seg.b[3] = buffer_b[3] = bCat3 | bSegDecode0; + next7seg.b[4] = buffer_b[4] = bCat4 | bSegDecode0; + + //setDisplayPWM(5); + displayOn(); + + readConfigFile(); + checkDelayedLoadRules(); + + measure_vbat(); + + if (RTC->ISR & RTC_ISR_INITS) //RTC contains non-zero data + { + RTC_DateTypeDef sdate; + RTC_TimeTypeDef stime; + + if (!config.zone_override){ + char zone[32]; + memcpyword( (uint32_t*)zone, (uint32_t*)&(RTC->BKP0R), 8 ); + zone[31]=0; + + if (loadRulesSingle(zone) != RULES_OK){ // takes ~8ms + memcpyword( (uint32_t*)loadedRulesString, (uint32_t*)&(RTC->BKP0R), 8 ); + loadedRulesString[31]=0;//paranoia + memcpyword( (uint32_t*)rules, (uint32_t*)&(RTC->BKP8R), 22 ); + } + } + + + hrtc.Instance = RTC; + HAL_RTC_GetTime(&hrtc, &stime, RTC_FORMAT_BIN); + HAL_RTC_GetDate(&hrtc, &sdate, RTC_FORMAT_BIN); + + struct tm out; + + out.tm_isdst = 0; + + out.tm_sec = stime.Seconds; + out.tm_min = stime.Minutes; + out.tm_hour = stime.Hours; + out.tm_mday = sdate.Date; + out.tm_mon = sdate.Month -1; + out.tm_year = sdate.Year + 100; //Years since 1900 + + currentTime = mktime(&out); + + float fraction = (float)(32767 - stime.SubSeconds) / 32768.0; + + // SysTick->VAL = SysTick->LOAD; ? + millisec = (uint32_t)(fraction*1000) % 10; + centisec = (uint32_t)(fraction*100) % 10; + decisec = (uint32_t)(fraction*10) % 10; + + if (decisec>=9) currentTime++; + + setNextTimestamp( currentTime ); + sendDate(1); + latchSegments(); + + // As the coin cell goes flat, the RTC stops ticking long before the backup registers die. + // Powering on with a flat battery means the clock thinks no time has passed, and assumes it has good precision. + // Explicitly stop this by checking the battery voltage. + if (vbat > 2.70) { + rtc_good=1; + } else { + // trash the calibration time to ensure lowest precision display + if (currentTime - rtc_last_calibration < config.tolerance_100ms) + rtc_last_calibration -= config.tolerance_100ms +1; + } + + } else { // backup domain reset + + currentTime=946684800; // 2000-01-01T00:00:00 + + // The init process blanks the subsecond registers + MX_RTC_Init(); + } + + vbat = 0.0; // don't allow measurement to go stale + + setPrecision(); + PPS_Init(); + HAL_UART_Receive_DMA(&huart1, nmea, sizeof(nmea)); + +//#define MEASURE_LOOKUP_TIME + + /* USER CODE END 2 */ + + /* Infinite loop */ + /* USER CODE BEGIN WHILE */ + while (1) + { + if (new_position && !qspi_write_time && !config.zone_override + && (data_valid || (config.fake_long && config.fake_lat)) + && latitude>=-90.0 && latitude<=90.0 && longitude>=-180.0 && longitude<=180.0) { + + new_position=0; + FIL mapfile; + if (f_open(&mapfile, MAP_FILENAME, FA_READ) == FR_OK) { +#ifdef MEASURE_LOOKUP_TIME + uint32_t start=uwTick; +#endif + ZoneDetect *const zdb = ZDOpenDatabase(&mapfile); + + if (!zdb) { + // mapfile error + } else { + char* zone = ZDHelperSimpleLookupString(zdb, latitude, longitude); +#ifdef MEASURE_LOOKUP_TIME + uint32_t ztime=uwTick-start; +#endif + if (zone && !delayedLoadRules) { +#ifdef MEASURE_LOOKUP_TIME + start=uwTick; +#endif + loadRulesSingle(zone); +#ifdef MEASURE_LOOKUP_TIME + sprintf(textDisplay,"d%ld L%ld",ztime, uwTick-start); +#endif + } + free(zone); + ZDCloseDatabase(zdb); + //f_close(&mapfile); + } + } + // else no_map = 1 + } + + if (delayedCheckOnEject) firmwareCheckOnEject(); + + if (delayedReadConfigFile) { + FATFS_remount(); + readConfigFile(); + delayedReadConfigFile=0; + } + + checkDelayedLoadRules(); + + if (delayedDisplayFreq) setDisplayFreq(delayedDisplayFreq); + + monitor_vbus(); + + if (pps_ts_enabled) { + static uint32_t last_temp_read = 0; + if ((uint32_t)currentTime - last_temp_read >= 4) { // refresh die temp every ~4 s + last_temp_read = (uint32_t)currentTime; + measure_temp(); + } + if (pps_record_pending) emitPPSTimestamp(); // emit clears pending itself on success + } + + if (displayMode == MODE_VBAT) + measure_vbat(); + + if (displayMode == MODE_SUN || displayMode == MODE_SUN_AZEL || displayMode == MODE_MOON + || displayMode == MODE_GRID || displayMode == MODE_LATLON) + astro_update(); + + + /* USER CODE END WHILE */ + + /* USER CODE BEGIN 3 */ + } + /* USER CODE END 3 */ +} + +/** + * @brief System Clock Configuration + * @retval None + */ +void SystemClock_Config(void) +{ + RCC_OscInitTypeDef RCC_OscInitStruct = {0}; + RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; + RCC_PeriphCLKInitTypeDef PeriphClkInit = {0}; + + /** Configure LSE Drive Capability + */ + HAL_PWR_EnableBkUpAccess(); + __HAL_RCC_LSEDRIVE_CONFIG(RCC_LSEDRIVE_LOW); + /** Initializes the CPU, AHB and APB busses clocks + */ + RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE|RCC_OSCILLATORTYPE_LSE + |RCC_OSCILLATORTYPE_MSI; + RCC_OscInitStruct.HSEState = RCC_HSE_ON; + RCC_OscInitStruct.LSEState = RCC_LSE_ON; + RCC_OscInitStruct.MSIState = RCC_MSI_ON; + RCC_OscInitStruct.MSICalibrationValue = 0; + RCC_OscInitStruct.MSIClockRange = RCC_MSIRANGE_11; + RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; + RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; + RCC_OscInitStruct.PLL.PLLM = 2; + RCC_OscInitStruct.PLL.PLLN = 64; + RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7; + RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2; + RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV4; + if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) + { + Error_Handler(); + } + /** Initializes the CPU, AHB and APB busses clocks + */ + RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK + |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; + RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; + RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; + RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; + RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; + + if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK) + { + Error_Handler(); + } + PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_RTC|RCC_PERIPHCLK_USART1 + |RCC_PERIPHCLK_USART2|RCC_PERIPHCLK_LPTIM1 + |RCC_PERIPHCLK_USB|RCC_PERIPHCLK_ADC; + PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2; + PeriphClkInit.Usart2ClockSelection = RCC_USART2CLKSOURCE_PCLK1; + PeriphClkInit.Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSE; + PeriphClkInit.AdcClockSelection = RCC_ADCCLKSOURCE_SYSCLK; + PeriphClkInit.RTCClockSelection = RCC_RTCCLKSOURCE_LSE; + PeriphClkInit.UsbClockSelection = RCC_USBCLKSOURCE_MSI; + if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) + { + Error_Handler(); + } + /** Configure the main internal regulator output voltage + */ + if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK) + { + Error_Handler(); + } + /** Enable MSI Auto calibration + */ + HAL_RCCEx_EnableMSIPLLMode(); +} + +/** + * @brief ADC1 Initialization Function + * @param None + * @retval None + */ +static void MX_ADC1_Init(void) +{ + + /* USER CODE BEGIN ADC1_Init 0 */ + + /* USER CODE END ADC1_Init 0 */ + + ADC_MultiModeTypeDef multimode = {0}; + ADC_ChannelConfTypeDef sConfig = {0}; + + /* USER CODE BEGIN ADC1_Init 1 */ + + /* USER CODE END ADC1_Init 1 */ + /** Common config + */ + hadc1.Instance = ADC1; + hadc1.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV1; + hadc1.Init.Resolution = ADC_RESOLUTION_12B; + hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; + hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE; + hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV; + hadc1.Init.LowPowerAutoWait = DISABLE; + hadc1.Init.ContinuousConvMode = DISABLE; + hadc1.Init.NbrOfConversion = 1; + hadc1.Init.DiscontinuousConvMode = DISABLE; + hadc1.Init.NbrOfDiscConversion = 1; + hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; + hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; + hadc1.Init.DMAContinuousRequests = DISABLE; + hadc1.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN; + hadc1.Init.OversamplingMode = DISABLE; + if (HAL_ADC_Init(&hadc1) != HAL_OK) + { + Error_Handler(); + } + /** Configure the ADC multi-mode + */ + multimode.Mode = ADC_MODE_INDEPENDENT; + if (HAL_ADCEx_MultiModeConfigChannel(&hadc1, &multimode) != HAL_OK) + { + Error_Handler(); + } + /** Configure Regular Channel + */ + sConfig.Channel = ADC_CHANNEL_10; + sConfig.Rank = ADC_REGULAR_RANK_1; + sConfig.SamplingTime = ADC_SAMPLETIME_92CYCLES_5; + sConfig.SingleDiff = ADC_SINGLE_ENDED; + sConfig.OffsetNumber = ADC_OFFSET_NONE; + sConfig.Offset = 0; + if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) + { + Error_Handler(); + } + /* USER CODE BEGIN ADC1_Init 2 */ + + /* USER CODE END ADC1_Init 2 */ + +} + +/** + * @brief ADC3 Initialization Function + * @param None + * @retval None + */ +static void MX_ADC3_Init(void) +{ + + /* USER CODE BEGIN ADC3_Init 0 */ + + /* USER CODE END ADC3_Init 0 */ + + ADC_ChannelConfTypeDef sConfig = {0}; + + /* USER CODE BEGIN ADC3_Init 1 */ + + + /* USER CODE END ADC3_Init 1 */ + /** Common config + */ + hadc3.Instance = ADC3; + hadc3.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV2; + hadc3.Init.Resolution = ADC_RESOLUTION_12B; + hadc3.Init.DataAlign = ADC_DATAALIGN_RIGHT; + hadc3.Init.ScanConvMode = ADC_SCAN_DISABLE; + hadc3.Init.EOCSelection = ADC_EOC_SINGLE_CONV; + hadc3.Init.LowPowerAutoWait = DISABLE; + hadc3.Init.ContinuousConvMode = DISABLE; + hadc3.Init.NbrOfConversion = 1; + hadc3.Init.DiscontinuousConvMode = DISABLE; + hadc3.Init.NbrOfDiscConversion = 1; + hadc3.Init.ExternalTrigConv = ADC_SOFTWARE_START; + hadc3.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; + hadc3.Init.DMAContinuousRequests = DISABLE; + hadc3.Init.Overrun = ADC_OVR_DATA_PRESERVED; + hadc3.Init.OversamplingMode = ENABLE; + hadc3.Init.Oversampling.Ratio = ADC_OVERSAMPLING_RATIO_16; + hadc3.Init.Oversampling.RightBitShift = ADC_RIGHTBITSHIFT_4; + hadc3.Init.Oversampling.TriggeredMode = ADC_TRIGGEREDMODE_SINGLE_TRIGGER; + hadc3.Init.Oversampling.OversamplingStopReset = ADC_REGOVERSAMPLING_RESUMED_MODE; + + if (HAL_ADC_Init(&hadc3) != HAL_OK) + { + Error_Handler(); + } + + HAL_ADCEx_Calibration_Start(&hadc3, ADC_SINGLE_ENDED); + + /** Configure Regular Channel + */ + sConfig.Channel = ADC_CHANNEL_VBAT; + sConfig.Rank = ADC_REGULAR_RANK_1; + sConfig.SamplingTime = ADC_SAMPLETIME_640CYCLES_5; + sConfig.SingleDiff = ADC_SINGLE_ENDED; + sConfig.OffsetNumber = ADC_OFFSET_NONE; + sConfig.Offset = 0; + if (HAL_ADC_ConfigChannel(&hadc3, &sConfig) != HAL_OK) + { + Error_Handler(); + } + /* USER CODE BEGIN ADC3_Init 2 */ + ADC123_COMMON->CCR &= ~ADC_CCR_VBATEN; + /* USER CODE END ADC3_Init 2 */ + +} + +/** + * @brief CRC Initialization Function + * @param None + * @retval None + */ +static void MX_CRC_Init(void) +{ + + /* USER CODE BEGIN CRC_Init 0 */ + + /* USER CODE END CRC_Init 0 */ + + /* USER CODE BEGIN CRC_Init 1 */ + + /* USER CODE END CRC_Init 1 */ + hcrc.Instance = CRC; + hcrc.Init.DefaultPolynomialUse = DEFAULT_POLYNOMIAL_ENABLE; + hcrc.Init.DefaultInitValueUse = DEFAULT_INIT_VALUE_ENABLE; + hcrc.Init.InputDataInversionMode = CRC_INPUTDATA_INVERSION_BYTE; + hcrc.Init.OutputDataInversionMode = CRC_OUTPUTDATA_INVERSION_ENABLE; + hcrc.InputDataFormat = CRC_INPUTDATA_FORMAT_WORDS; + if (HAL_CRC_Init(&hcrc) != HAL_OK) + { + Error_Handler(); + } + /* USER CODE BEGIN CRC_Init 2 */ + + /* USER CODE END CRC_Init 2 */ + +} + +/** + * @brief DAC1 Initialization Function + * @param None + * @retval None + */ +static void MX_DAC1_Init(void) +{ + + /* USER CODE BEGIN DAC1_Init 0 */ + + /* USER CODE END DAC1_Init 0 */ + + DAC_ChannelConfTypeDef sConfig = {0}; + + /* USER CODE BEGIN DAC1_Init 1 */ + + /* USER CODE END DAC1_Init 1 */ + /** DAC Initialization + */ + hdac1.Instance = DAC1; + if (HAL_DAC_Init(&hdac1) != HAL_OK) + { + Error_Handler(); + } + /** DAC channel OUT1 config + */ + sConfig.DAC_SampleAndHold = DAC_SAMPLEANDHOLD_DISABLE; + sConfig.DAC_Trigger = DAC_TRIGGER_T6_TRGO; + sConfig.DAC_OutputBuffer = DAC_OUTPUTBUFFER_ENABLE; + sConfig.DAC_ConnectOnChipPeripheral = DAC_CHIPCONNECT_DISABLE; + sConfig.DAC_UserTrimming = DAC_TRIMMING_FACTORY; + if (HAL_DAC_ConfigChannel(&hdac1, &sConfig, DAC_CHANNEL_1) != HAL_OK) + { + Error_Handler(); + } + /* USER CODE BEGIN DAC1_Init 2 */ + HAL_DAC_SetValue(&hdac1, DAC_CHANNEL_1, DAC_ALIGN_12B_R, 4095); + /* USER CODE END DAC1_Init 2 */ + +} + +/** + * @brief LPTIM1 Initialization Function + * @param None + * @retval None + */ +static void MX_LPTIM1_Init(void) +{ + + /* USER CODE BEGIN LPTIM1_Init 0 */ + + /* USER CODE END LPTIM1_Init 0 */ + + /* Peripheral clock enable */ + LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_LPTIM1); + + /* LPTIM1 interrupt Init */ + NVIC_SetPriority(LPTIM1_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(),1, 0)); + NVIC_EnableIRQ(LPTIM1_IRQn); + + /* USER CODE BEGIN LPTIM1_Init 1 */ + + /* USER CODE END LPTIM1_Init 1 */ + LL_LPTIM_SetClockSource(LPTIM1, LL_LPTIM_CLK_SOURCE_INTERNAL); + LL_LPTIM_SetPrescaler(LPTIM1, LL_LPTIM_PRESCALER_DIV1); + LL_LPTIM_SetPolarity(LPTIM1, LL_LPTIM_OUTPUT_POLARITY_REGULAR); + LL_LPTIM_SetUpdateMode(LPTIM1, LL_LPTIM_UPDATE_MODE_IMMEDIATE); + LL_LPTIM_SetCounterMode(LPTIM1, LL_LPTIM_COUNTER_MODE_INTERNAL); + LL_LPTIM_TrigSw(LPTIM1); + LL_LPTIM_SetInput1Src(LPTIM1, LL_LPTIM_INPUT1_SRC_GPIO); + LL_LPTIM_SetInput2Src(LPTIM1, LL_LPTIM_INPUT2_SRC_GPIO); + /* USER CODE BEGIN LPTIM1_Init 2 */ + + LL_LPTIM_Enable(LPTIM1); + LL_LPTIM_SetAutoReload(LPTIM1, 0xFFFF); + LL_LPTIM_EnableIT_ARRM(LPTIM1); + + /* USER CODE END LPTIM1_Init 2 */ + +} + +/** + * @brief QUADSPI Initialization Function + * @param None + * @retval None + */ +static void MX_QUADSPI_Init(void) +{ + + /* USER CODE BEGIN QUADSPI_Init 0 */ + + /* USER CODE END QUADSPI_Init 0 */ + + /* USER CODE BEGIN QUADSPI_Init 1 */ + + /* USER CODE END QUADSPI_Init 1 */ + /* QUADSPI parameter configuration*/ + hqspi.Instance = QUADSPI; + hqspi.Init.ClockPrescaler = 0; + hqspi.Init.FifoThreshold = 4; + hqspi.Init.SampleShifting = QSPI_SAMPLE_SHIFTING_HALFCYCLE; + hqspi.Init.FlashSize = 23; + hqspi.Init.ChipSelectHighTime = QSPI_CS_HIGH_TIME_1_CYCLE; + hqspi.Init.ClockMode = QSPI_CLOCK_MODE_0; + if (HAL_QSPI_Init(&hqspi) != HAL_OK) + { + Error_Handler(); + } + /* USER CODE BEGIN QUADSPI_Init 2 */ + + /* USER CODE END QUADSPI_Init 2 */ + +} + +/** + * @brief RTC Initialization Function + * @param None + * @retval None + */ +static void MX_RTC_Init(void) +{ + + /* USER CODE BEGIN RTC_Init 0 */ + + /* USER CODE END RTC_Init 0 */ + + /* USER CODE BEGIN RTC_Init 1 */ + + /* USER CODE END RTC_Init 1 */ + /** Initialize RTC Only + */ + hrtc.Instance = RTC; + hrtc.Init.HourFormat = RTC_HOURFORMAT_24; + hrtc.Init.AsynchPrediv = 0; + hrtc.Init.SynchPrediv = 32759; + hrtc.Init.OutPut = RTC_OUTPUT_DISABLE; + hrtc.Init.OutPutRemap = RTC_OUTPUT_REMAP_NONE; + hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH; + hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN; + if (HAL_RTC_Init(&hrtc) != HAL_OK) + { + Error_Handler(); + } + /* USER CODE BEGIN RTC_Init 2 */ + + // RM page 1236 + __HAL_RTC_WRITEPROTECTION_DISABLE(&hrtc); + RTC->CALR = 0x100; // CALM to midpoint + __HAL_RTC_WRITEPROTECTION_ENABLE(&hrtc); + + /* USER CODE END RTC_Init 2 */ + +} + +/** + * @brief TIM1 Initialization Function + * @param None + * @retval None + */ +static void MX_TIM1_Init(void) +{ + + /* USER CODE BEGIN TIM1_Init 0 */ + + /* USER CODE END TIM1_Init 0 */ + + TIM_ClockConfigTypeDef sClockSourceConfig = {0}; + TIM_MasterConfigTypeDef sMasterConfig = {0}; + + /* USER CODE BEGIN TIM1_Init 1 */ + + /* USER CODE END TIM1_Init 1 */ + htim1.Instance = TIM1; + htim1.Init.Prescaler = 0; + htim1.Init.CounterMode = TIM_COUNTERMODE_UP; + htim1.Init.Period = 256; + htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; + htim1.Init.RepetitionCounter = 0; + htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; + if (HAL_TIM_Base_Init(&htim1) != HAL_OK) + { + Error_Handler(); + } + sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL; + if (HAL_TIM_ConfigClockSource(&htim1, &sClockSourceConfig) != HAL_OK) + { + Error_Handler(); + } + sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; + sMasterConfig.MasterOutputTrigger2 = TIM_TRGO2_RESET; + sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; + if (HAL_TIMEx_MasterConfigSynchronization(&htim1, &sMasterConfig) != HAL_OK) + { + Error_Handler(); + } + /* USER CODE BEGIN TIM1_Init 2 */ + + /* USER CODE END TIM1_Init 2 */ + +} + +/** + * @brief TIM2 Initialization Function + * @param None + * @retval None + */ +static void MX_TIM2_Init(void) +{ + + /* USER CODE BEGIN TIM2_Init 0 */ + + /* USER CODE END TIM2_Init 0 */ + + TIM_MasterConfigTypeDef sMasterConfig = {0}; + TIM_OC_InitTypeDef sConfigOC = {0}; + + /* USER CODE BEGIN TIM2_Init 1 */ + + /* USER CODE END TIM2_Init 1 */ + htim2.Instance = TIM2; + htim2.Init.Prescaler = 8; + htim2.Init.CounterMode = TIM_COUNTERMODE_UP; + htim2.Init.Period = 10000; + htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; + htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; + if (HAL_TIM_PWM_Init(&htim2) != HAL_OK) + { + Error_Handler(); + } + sMasterConfig.MasterOutputTrigger = TIM_TRGO_OC2REF; + sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; + if (HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig) != HAL_OK) + { + Error_Handler(); + } + sConfigOC.OCMode = TIM_OCMODE_PWM2; + sConfigOC.Pulse = 0; + sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; + sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; + if (HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1) != HAL_OK) + { + Error_Handler(); + } + if (HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_2) != HAL_OK) + { + Error_Handler(); + } + /* USER CODE BEGIN TIM2_Init 2 */ + + /* USER CODE END TIM2_Init 2 */ + HAL_TIM_MspPostInit(&htim2); + +} + +/** + * @brief TIM5 Initialization Function + * @param None + * @retval None + */ +static void MX_TIM5_Init(void) +{ + + /* USER CODE BEGIN TIM5_Init 0 */ + + /* USER CODE END TIM5_Init 0 */ + + TIM_ClockConfigTypeDef sClockSourceConfig = {0}; + TIM_MasterConfigTypeDef sMasterConfig = {0}; + TIM_OC_InitTypeDef sConfigOC = {0}; + + /* USER CODE BEGIN TIM5_Init 1 */ + + /* USER CODE END TIM5_Init 1 */ + htim5.Instance = TIM5; + htim5.Init.Prescaler = 7999; + htim5.Init.CounterMode = TIM_COUNTERMODE_UP; + htim5.Init.Period = 99; + htim5.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; + htim5.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; + if (HAL_TIM_Base_Init(&htim5) != HAL_OK) + { + Error_Handler(); + } + sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL; + if (HAL_TIM_ConfigClockSource(&htim5, &sClockSourceConfig) != HAL_OK) + { + Error_Handler(); + } + if (HAL_TIM_OC_Init(&htim5) != HAL_OK) + { + Error_Handler(); + } + sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; + sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; + if (HAL_TIMEx_MasterConfigSynchronization(&htim5, &sMasterConfig) != HAL_OK) + { + Error_Handler(); + } + sConfigOC.OCMode = TIM_OCMODE_TIMING; + sConfigOC.Pulse = 0; + sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; + sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; + if (HAL_TIM_OC_ConfigChannel(&htim5, &sConfigOC, TIM_CHANNEL_1) != HAL_OK) + { + Error_Handler(); + } + if (HAL_TIM_OC_ConfigChannel(&htim5, &sConfigOC, TIM_CHANNEL_2) != HAL_OK) + { + Error_Handler(); + } + /* USER CODE BEGIN TIM5_Init 2 */ + + /* USER CODE END TIM5_Init 2 */ + +} + +/** + * @brief TIM6 Initialization Function + * @param None + * @retval None + */ +static void MX_TIM6_Init(void) +{ + + /* USER CODE BEGIN TIM6_Init 0 */ + + /* USER CODE END TIM6_Init 0 */ + + TIM_MasterConfigTypeDef sMasterConfig = {0}; + + /* USER CODE BEGIN TIM6_Init 1 */ + + /* USER CODE END TIM6_Init 1 */ + htim6.Instance = TIM6; + htim6.Init.Prescaler = 8000; + htim6.Init.CounterMode = TIM_COUNTERMODE_UP; + htim6.Init.Period = 100; + htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; + if (HAL_TIM_Base_Init(&htim6) != HAL_OK) + { + Error_Handler(); + } + sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE; + sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; + if (HAL_TIMEx_MasterConfigSynchronization(&htim6, &sMasterConfig) != HAL_OK) + { + Error_Handler(); + } + /* USER CODE BEGIN TIM6_Init 2 */ + + /* USER CODE END TIM6_Init 2 */ + +} + +/** + * @brief TIM7 Initialization Function + * @param None + * @retval None + */ +static void MX_TIM7_Init(void) +{ + + /* USER CODE BEGIN TIM7_Init 0 */ + + /* USER CODE END TIM7_Init 0 */ + + TIM_MasterConfigTypeDef sMasterConfig = {0}; + + /* USER CODE BEGIN TIM7_Init 1 */ + + /* USER CODE END TIM7_Init 1 */ + htim7.Instance = TIM7; + htim7.Init.Prescaler = 0; + htim7.Init.CounterMode = TIM_COUNTERMODE_UP; + htim7.Init.Period = 256; + htim7.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; + if (HAL_TIM_Base_Init(&htim7) != HAL_OK) + { + Error_Handler(); + } + sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; + sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; + if (HAL_TIMEx_MasterConfigSynchronization(&htim7, &sMasterConfig) != HAL_OK) + { + Error_Handler(); + } + /* USER CODE BEGIN TIM7_Init 2 */ + + /* USER CODE END TIM7_Init 2 */ + +} + +/** + * @brief USART1 Initialization Function + * @param None + * @retval None + */ +static void MX_USART1_UART_Init(void) +{ + + /* USER CODE BEGIN USART1_Init 0 */ + + /* USER CODE END USART1_Init 0 */ + + /* USER CODE BEGIN USART1_Init 1 */ + + /* USER CODE END USART1_Init 1 */ + huart1.Instance = USART1; + huart1.Init.BaudRate = 9600; + huart1.Init.WordLength = UART_WORDLENGTH_8B; + huart1.Init.StopBits = UART_STOPBITS_1; + huart1.Init.Parity = UART_PARITY_NONE; + huart1.Init.Mode = UART_MODE_TX_RX; + huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; + huart1.Init.OverSampling = UART_OVERSAMPLING_16; + huart1.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE; + huart1.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT; + if (HAL_UART_Init(&huart1) != HAL_OK) + { + Error_Handler(); + } + /* USER CODE BEGIN USART1_Init 2 */ + + /* USER CODE END USART1_Init 2 */ + +} + +/** + * @brief USART2 Initialization Function + * @param None + * @retval None + */ +static void MX_USART2_UART_Init(void) +{ + + /* USER CODE BEGIN USART2_Init 0 */ + + /* USER CODE END USART2_Init 0 */ + + /* USER CODE BEGIN USART2_Init 1 */ + + /* USER CODE END USART2_Init 1 */ + huart2.Instance = USART2; + huart2.Init.BaudRate = 115200; + huart2.Init.WordLength = UART_WORDLENGTH_9B; + huart2.Init.StopBits = UART_STOPBITS_1; + huart2.Init.Parity = UART_PARITY_EVEN; + huart2.Init.Mode = UART_MODE_TX_RX; + huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE; + huart2.Init.OverSampling = UART_OVERSAMPLING_16; + huart2.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE; + huart2.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_RXOVERRUNDISABLE_INIT; + huart2.AdvancedInit.OverrunDisable = UART_ADVFEATURE_OVERRUN_DISABLE; + if (HAL_UART_Init(&huart2) != HAL_OK) + { + Error_Handler(); + } + /* USER CODE BEGIN USART2_Init 2 */ + + /* USER CODE END USART2_Init 2 */ + +} + +/** + * Enable DMA controller clock + */ +static void MX_DMA_Init(void) +{ + + /* DMA controller clock enable */ + __HAL_RCC_DMA1_CLK_ENABLE(); + __HAL_RCC_DMA2_CLK_ENABLE(); + + /* DMA interrupt init */ + /* DMA1_Channel3_IRQn interrupt configuration */ + HAL_NVIC_SetPriority(DMA1_Channel3_IRQn, 1, 0); + HAL_NVIC_EnableIRQ(DMA1_Channel3_IRQn); + /* DMA1_Channel4_IRQn interrupt configuration */ + HAL_NVIC_SetPriority(DMA1_Channel4_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(DMA1_Channel4_IRQn); + /* DMA1_Channel5_IRQn interrupt configuration */ + HAL_NVIC_SetPriority(DMA1_Channel5_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(DMA1_Channel5_IRQn); + /* DMA1_Channel6_IRQn interrupt configuration */ + HAL_NVIC_SetPriority(DMA1_Channel6_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(DMA1_Channel6_IRQn); + /* DMA1_Channel7_IRQn interrupt configuration */ + HAL_NVIC_SetPriority(DMA1_Channel7_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(DMA1_Channel7_IRQn); + /* DMA2_Channel4_IRQn interrupt configuration */ + HAL_NVIC_SetPriority(DMA2_Channel4_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(DMA2_Channel4_IRQn); + /* DMA2_Channel5_IRQn interrupt configuration */ + HAL_NVIC_SetPriority(DMA2_Channel5_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(DMA2_Channel5_IRQn); + +} + +/** + * @brief GPIO Initialization Function + * @param None + * @retval None + */ +static void MX_GPIO_Init(void) +{ + GPIO_InitTypeDef GPIO_InitStruct = {0}; + + /* GPIO Ports Clock Enable */ + __HAL_RCC_GPIOC_CLK_ENABLE(); + __HAL_RCC_GPIOH_CLK_ENABLE(); + __HAL_RCC_GPIOA_CLK_ENABLE(); + __HAL_RCC_GPIOB_CLK_ENABLE(); + + /*Configure GPIO pin Output Level */ + HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13|GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2 + |GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5|GPIO_PIN_6 + |GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11 + |GPIO_PIN_12, GPIO_PIN_RESET); + + /*Configure GPIO pin Output Level */ + HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2|GPIO_PIN_12|GPIO_PIN_13|GPIO_PIN_14 + |GPIO_PIN_15|GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5 + |GPIO_PIN_6|GPIO_PIN_7|GPIO_PIN_8|GPIO_PIN_9, GPIO_PIN_RESET); + + /*Configure GPIO pins : PC13 PC0 PC1 PC2 + PC3 PC4 PC5 PC6 + PC8 PC9 PC10 PC11 + PC12 */ + GPIO_InitStruct.Pin = GPIO_PIN_13|GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2 + |GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5|GPIO_PIN_6 + |GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11 + |GPIO_PIN_12; + GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; + GPIO_InitStruct.Pull = GPIO_NOPULL; + GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; + HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); + + /*Configure GPIO pins : PB2 PB12 PB13 PB14 + PB15 PB3 PB4 PB5 + PB6 PB7 PB8 PB9 */ + GPIO_InitStruct.Pin = GPIO_PIN_2|GPIO_PIN_12|GPIO_PIN_13|GPIO_PIN_14 + |GPIO_PIN_15|GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5 + |GPIO_PIN_6|GPIO_PIN_7|GPIO_PIN_8|GPIO_PIN_9; + GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; + GPIO_InitStruct.Pull = GPIO_NOPULL; + GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; + HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); + + /*Configure GPIO pin : PA8 */ + GPIO_InitStruct.Pin = GPIO_PIN_8; + GPIO_InitStruct.Mode = GPIO_MODE_INPUT; + GPIO_InitStruct.Pull = GPIO_PULLUP; + HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); + +} + +/* USER CODE BEGIN 4 */ + +/* USER CODE END 4 */ + +/** + * @brief This function is executed in case of error occurrence. + * @retval None + */ +void Error_Handler(void) +{ + /* USER CODE BEGIN Error_Handler_Debug */ + /* User can add his own implementation to report the HAL error return state */ + + __disable_irq(); + + buffer_c[0].high=0b11011110; + buffer_c[1].high=0b11011101; + buffer_c[2].high=0b11011011; + buffer_c[3].high=0b11010111; + buffer_c[4].high=0b11001111; + buffer_c[0].low=0b01010000; + buffer_c[1].low=0b01010000; + buffer_c[2].low=0b01011100; + buffer_c[3].low=0b01010000; + buffer_c[4].low=0; + + buffer_b[0] = bCat0; + buffer_b[1] = bCat1; + buffer_b[2] = bCat2; + buffer_b[3] = bCat3; + buffer_b[4] = bCat4 | 0b0111100100; + + //setDisplayPWM(5); + + + + + while(1); + /* USER CODE END Error_Handler_Debug */ +} + +#ifdef USE_FULL_ASSERT +/** + * @brief Reports the name of the source file and the source line number + * where the assert_param error has occurred. + * @param file: pointer to the source file name + * @param line: assert_param error line source number + * @retval None + */ +void assert_failed(uint8_t *file, uint32_t line) +{ + /* USER CODE BEGIN 6 */ + /* User can add his own implementation to report the file name and line number, + tex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */ + /* USER CODE END 6 */ +} +#endif /* USE_FULL_ASSERT */ + +/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/ From 02c9823841acbe30c76a2a57292fc23c276e70e6 Mon Sep 17 00:00:00 2001 From: Peter Lewis Date: Sun, 5 Jul 2026 12:42:20 +0100 Subject: [PATCH 06/37] astro: configurable per-screen page dwell (page_ms, default 5500 ms) The paged modes (SUN, LATLON) previously flipped sub-screen every 2 s hard-coded. Make the dwell a config key (page_ms, ms; unset -> 5500, floored at 250 so a tiny value can't flood the date-board UART), driven off uwTick, and repaint the moment a page flips rather than waiting for the 1 Hz date-row refresh (guarded by decisec!=9 to avoid racing the SysTick sendDate). Co-Authored-By: Claude Opus 4.8 --- mk4-time/Core/Src/main.c | 25 ++++++++++++++++++++++--- qspi/config.txt | 8 ++++++-- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/mk4-time/Core/Src/main.c b/mk4-time/Core/Src/main.c index e967a0c..3c1bb6f 100644 --- a/mk4-time/Core/Src/main.c +++ b/mk4-time/Core/Src/main.c @@ -239,6 +239,7 @@ struct { time_t countdown_to; float brightness_override; volatile _Bool zone_override; + uint16_t page_ms; // paged modes (SUN/LATLON): sub-screen dwell, ms _Bool modes_enabled[NUM_DISPLAY_MODES]; } config = {0}; @@ -270,6 +271,10 @@ void memcpyword(volatile uint32_t *dest, volatile uint32_t *src, size_t n){ static _Bool astro_pos_ok(float lat, float lon){ return lat >= -90.0f && lat <= 90.0f && lon >= -180.0f && lon <= 180.0f; } +// Sub-screen dwell (ms) for the paged modes (SUN, LATLON). Unset -> 5500 ms, a +// subjectively-tuned cadence found by feel. Floored at 250 ms so a tiny value can't +// flood the date-board UART. +static uint32_t page_ms(void){ uint32_t m = config.page_ms; return m == 0 ? 5500 : (m < 250 ? 250 : m); } // Decimal UTC hour (sun_times may return <0 or >24) -> local minutes-of-day [0,1440). static int astro_local_minutes(double utc_h){ double h = fmod(utc_h + currentOffset / 3600.0, 24.0); @@ -575,7 +580,7 @@ void sendDate( _Bool now ){ // leaving the time row as the running clock (SATVIEW-style). -------------- case MODE_SUN: { if (!astro.have_pos || !astro.epoch) { i = sprintf((char*)&uart2_tx_buffer[1], "RISE ----"); break; } - int page = (currentTime / 2) % 3; // rise -> set -> solar noon, 2 s each + int page = (uwTick / page_ms()) % 3; // rise -> set -> solar noon, page_ms each // labels padded to 4 chars in the literal ("SET "/"SOL ") so the time digits // line up under RISE without relying on the nano printf honouring "%-4s" const char *lbl = page == 0 ? "RISE" : page == 1 ? "SET " : "SOL "; @@ -606,7 +611,7 @@ void sendDate( _Bool now ){ // 10 chars, so the separator is dropped just for that case ("LON 179.99" / "LON-179.99"). if (!astro.have_pos || !astro.epoch) { i = sprintf((char*)&uart2_tx_buffer[1], "LAT ----"); } else { - _Bool lat = (currentTime / 2) % 2 == 0; // page latitude / longitude, 2 s each + _Bool lat = (uwTick / page_ms()) % 2 == 0; // page latitude / longitude, page_ms each double v = lat ? astro.lat_show : astro.lon_show; long h = (long)(v * 100.0 + (v < 0 ? -0.5 : 0.5)); // hundredths, rounded long a2 = h < 0 ? -h : h; @@ -1151,6 +1156,9 @@ void parseConfigString(char *key, char *value) { set_mode_enabled(MODE_GRID, value); } else if (strcasecmp(key, "MODE_LATLON") == 0) { set_mode_enabled(MODE_LATLON, value); + } else if (strcasecmp(key, "page_ms") == 0) { + int v = atoi(value); + config.page_ms = v < 0 ? 0 : (v > 65535 ? 65535 : v); // fits uint16; 0 -> default } else if (strcasecmp(key, "Tolerance_time_1ms") == 0) { config.tolerance_1ms = atoi(value); } else if (strcasecmp(key, "Tolerance_time_10ms") == 0) { @@ -2421,8 +2429,19 @@ int main(void) measure_vbat(); if (displayMode == MODE_SUN || displayMode == MODE_SUN_AZEL || displayMode == MODE_MOON - || displayMode == MODE_GRID || displayMode == MODE_LATLON) + || displayMode == MODE_GRID || displayMode == MODE_LATLON) { astro_update(); + // honour the ms page dwell: the date row otherwise only repaints at 1 Hz, so repaint + // the moment a paged mode flips sub-screen. Only with a fix (no-fix shows a + // page-independent "----"), and never in the last decisecond -- there the SysTick ISR + // runs its own (non-reentrant, shared-UART) sendDate(0), so we'd race it. Same + // decisec!=9 guard the existing main-loop sendDate(1) calls use. + if ((displayMode == MODE_SUN || displayMode == MODE_LATLON) && astro.have_pos && astro.epoch) { + static uint32_t last_pg = 0; + uint32_t pg = uwTick / page_ms(); + if (pg != last_pg && decisec != 9) { last_pg = pg; sendDate(1); } + } + } /* USER CODE END WHILE */ diff --git a/qspi/config.txt b/qspi/config.txt index 25a1444..5a4723f 100644 --- a/qspi/config.txt +++ b/qspi/config.txt @@ -115,7 +115,7 @@ pps = off # They use the current position; with no GPS fix they fall back to fake_latitude/ # fake_longitude below (handy indoors), or show "----" if neither is set. -# Sunrise / sunset / solar noon (local time). Pages every 2 s: RISE -> SET -> SOL. +# Sunrise / sunset / solar noon (local time). Pages RISE -> SET -> SOL (see page_ms). MODE_SUN = disabled # Sun azimuth & elevation right now, e.g. "AZ142EL38" (degrees; elevation may be negative). @@ -129,9 +129,13 @@ MODE_MOON = disabled # Maidenhead grid locator, e.g. "IO91xl". MODE_GRID = disabled -# Latitude / longitude in decimal degrees. Pages every 2 s: LAT -> LON. +# Latitude / longitude in decimal degrees. Pages LAT -> LON (see page_ms). MODE_LATLON = disabled +# Dwell per sub-screen for the paged modes (MODE_SUN, MODE_LATLON), in milliseconds. +# Default 5500 (a subjectively-tuned cadence, found by feel); floored at 250. +page_ms = 5500 + # Fixed position for the astro modes when there is no GPS fix (decimal degrees, N+/E+). # Note: setting these also pins the clock's position (GPS position updates are ignored). #fake_latitude = 51.48 From 54d560eadddbd2bf9b594fa0e8b39f1ffb1a44b3 Mon Sep 17 00:00:00 2001 From: Peter Lewis Date: Tue, 30 Jun 2026 22:03:24 +0100 Subject: [PATCH 07/37] Harden firmware: bounds + ISR-safety fixes from broad audit - loadRules: reject untrusted rowLength/numEntries from TZRULES.BIN (RAM overflow) - MODE_TEXT: clamp snprintf untruncated-return index (uart2_tx_buffer OOB) - mk4-date latchDisplay: bound dp_pos per orientation (buffer_a/b OOB) - readConfigFile: don't treat a zero FAT timestamp as a cache hit (first-boot hang) - config-over-USB: defer postConfigCleanup out of the USB ISR (sendDate/nextMode races) - firmwareCheckOnEject: fatfs_busy guard for FATFS reentrancy on USB eject Co-Authored-By: Claude Opus 4.8 --- mk4-date/Core/Src/main.c | 6 ++++++ mk4-time/Core/Inc/main.h | 1 + mk4-time/Core/Src/chainloader.c | 9 ++++++--- mk4-time/Core/Src/main.c | 36 +++++++++++++++++++++++++++++++-- 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/mk4-date/Core/Src/main.c b/mk4-date/Core/Src/main.c index 5190e7f..f2b0c05 100644 --- a/mk4-date/Core/Src/main.c +++ b/mk4-date/Core/Src/main.c @@ -488,7 +488,13 @@ static inline void latchDisplay(void){ buffer_a[3] = pre_buffer_a[3]; buffer_a[4] = pre_buffer_a[4]; + // dp_pos is the 1-based digit the decimal point attaches to (0 = none). It comes from + // text_idx, which the sender can advance past the 10-digit display, and it indexes the + // 5-entry buffer_a/buffer_b below. Clamp to each orientation's valid range so a stray or + // garbled '.' in the UART stream can't drive a negative / out-of-range index into RAM. if (!dp_pos) return; + if (inverted) { if (dp_pos > 9) return; } // inverted: 9-dp_pos goes negative at 10 + else { if (dp_pos > 10) return; } // non-inverted: dp_pos-6 tops out at [4] if (inverted){ if (dp_pos>=5) { diff --git a/mk4-time/Core/Inc/main.h b/mk4-time/Core/Inc/main.h index c7cd64a..86af852 100644 --- a/mk4-time/Core/Inc/main.h +++ b/mk4-time/Core/Inc/main.h @@ -76,6 +76,7 @@ extern uint16_t buffer_b[]; extern _Bool delayedReadConfigFile; extern _Bool delayedCheckOnEject; +extern volatile uint8_t fatfs_busy; extern _Bool waitingForLatch; extern _Bool resendDate; diff --git a/mk4-time/Core/Src/chainloader.c b/mk4-time/Core/Src/chainloader.c index 615bca6..7b77bfd 100644 --- a/mk4-time/Core/Src/chainloader.c +++ b/mk4-time/Core/Src/chainloader.c @@ -360,9 +360,12 @@ void firmwareCheckOnEject(){ unsigned int rc; FIL file1, file2; - // if QSPI is locked in this context, we have interrupted another fatfs read - // we can't wait for it to finish as it's running at lower priority - if (QSPI_Locked()) {delayedCheckOnEject=1; return;} + // This runs from the USB MSC eject command, i.e. in the USB OTG ISR. FATFS is not + // reentrant, so if the lower-priority main loop is mid-operation we must not touch it. + // QSPI_Locked() only catches an in-flight QSPI transfer; fatfs_busy covers the whole + // main-loop FATFS region, including the gaps between transfers where QSPI_Locked() reads + // false. If either says busy, defer back to the main loop (it polls delayedCheckOnEject). + if (fatfs_busy || QSPI_Locked()) {delayedCheckOnEject=1; return;} if (f_open(&file2, "/FWT.BIN", FA_READ) == FR_OK) { f_lseek(&file2, TIME_APP_SIZE - 4); diff --git a/mk4-time/Core/Src/main.c b/mk4-time/Core/Src/main.c index 3c1bb6f..8ec7476 100644 --- a/mk4-time/Core/Src/main.c +++ b/mk4-time/Core/Src/main.c @@ -180,6 +180,10 @@ char textDisplay[32]; _Bool delayedLoadRules = 0; _Bool delayedReadConfigFile = 0; _Bool delayedCheckOnEject = 0; +_Bool delayedPostConfigCleanup = 0; +// Set while the main loop is inside a (non-reentrant) FATFS operation, so the USB-ISR +// firmware-eject check defers instead of corrupting FATFS state. volatile: ISR-visible. +volatile uint8_t fatfs_busy = 0; uint32_t delayedDisplayFreq = 0; _Bool waitingForLatch = 0; @@ -492,6 +496,10 @@ void sendDate( _Bool now ){ case MODE_TEXT: if (textDisplay[0]) { i = snprintf((char*)&uart2_tx_buffer[1], 30,"%s", textDisplay); + // snprintf returns the length it WOULD have written (newlib-nano follows C99), + // not the truncated count; a >29-char TEXT= would otherwise push the ++i below + // past uart2_tx_buffer[31]. Clamp to the bytes actually written. + if (i > 29) i = 29; } else { uart2_tx_buffer[1]='-'; i=1; @@ -1263,7 +1271,10 @@ void rxConfigString(char c){ } if (k && (v || state>=2)) { parseConfigString(key, value); - postConfigCleanup(); + // rxConfigString runs in the USB OTG ISR; postConfigCleanup() calls nextMode() + // and sendDate(), which are non-reentrant against the SysTick repaint. Defer it + // to the main loop so it runs in thread context, like the file-config path does. + delayedPostConfigCleanup=1; } k=0; v=0; @@ -1300,7 +1311,11 @@ void readConfigFile(void){ if (f_stat(CONFIG_FILENAME, &fno) == FR_OK) { // if unchanged, exit early before touching any config // if the file doesn't exist, fall through and fail on the f_open - if (fno.fdate==config.fdate && fno.ftime==config.ftime) return; + // A zero FAT timestamp (the volume's RTC was unset when config.txt was written) + // must not be used as a cache key: config={0} matches it on the very first boot, + // so config is never loaded, no mode is enabled, and the first MODE button press + // then spins nextMode() forever. Only short-circuit on a real, non-zero stamp. + if ((fno.fdate || fno.ftime) && fno.fdate==config.fdate && fno.ftime==config.ftime) return; config.fdate=fno.fdate; config.ftime=fno.ftime; } @@ -1900,6 +1915,14 @@ uint8_t loadRules( char* cat, char* zo ) { f_lseek(&file, zoAddr); + // TZRULES.BIN is host-writable over the USB mass-storage volume, so its length + // fields are untrusted: a rowLength larger than one rule slot, or numEntries larger + // than the array, would overrun rules[] (global RAM corruption / HardFault). Reject. + if (rowLength > sizeof rules[0] || numEntries > MAX_RULES) { + f_close(&file); + return RULES_HEADER_ERR; + } + int i; for (i=0;i=-90.0 && latitude<=90.0 && longitude>=-180.0 && longitude<=180.0) { new_position=0; + fatfs_busy=1; // map lookup + loadRulesSingle touch FATFS; block the eject-time check FIL mapfile; if (f_open(&mapfile, MAP_FILENAME, FA_READ) == FR_OK) { #ifdef MEASURE_LOOKUP_TIME @@ -2400,10 +2424,17 @@ int main(void) } } // else no_map = 1 + fatfs_busy=0; } if (delayedCheckOnEject) firmwareCheckOnEject(); + if (delayedPostConfigCleanup) { + delayedPostConfigCleanup=0; + postConfigCleanup(); + } + + fatfs_busy=1; // FATFS_remount + readConfigFile + checkDelayedLoadRules touch FATFS if (delayedReadConfigFile) { FATFS_remount(); readConfigFile(); @@ -2411,6 +2442,7 @@ int main(void) } checkDelayedLoadRules(); + fatfs_busy=0; if (delayedDisplayFreq) setDisplayFreq(delayedDisplayFreq); From aff121dbfb8781614f79564a6cb2aaf97e23f60a Mon Sep 17 00:00:00 2001 From: Peter Lewis Date: Sat, 4 Jul 2026 09:57:58 +0100 Subject: [PATCH 08/37] Fix charger-only-power CDC hard-fault and date-board CMD_LOAD_TEXT OOB Two further pre-existing bugs from the same read-through. - mk4-time NMEA-over-CDC: CDC_Copy_Transmit() / CDC_Transmit_FS() dereference hUsbDeviceFS.pClassDataCDC unconditionally, but it is NULL until a USB host enumerates. The default nmea_cdc_level = NMEA_ALL forwards every GPS sentence to CDC, so on charger-only power the first forwarded sentence faults in ISR context. NULL guard on both, plus a length clamp before the memcpy. - mk4-date CMD_LOAD_TEXT: 'if (text_idx > MAX_TEXT_LEN) return;' passes at text_idx == MAX_TEXT_LEN and writes text[32], one past the 32-byte array. '>' -> '>='. Co-Authored-By: Claude Opus 4.8 --- mk4-date/Core/Src/main.c | 2 +- mk4-time/USB_DEVICE/App/usbd_cdc_if.c | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/mk4-date/Core/Src/main.c b/mk4-date/Core/Src/main.c index f2b0c05..4ed9122 100644 --- a/mk4-date/Core/Src/main.c +++ b/mk4-date/Core/Src/main.c @@ -607,7 +607,7 @@ static inline void parseByte(uint8_t x){ dp_pos = text_idx; return; } - if(text_idx > MAX_TEXT_LEN) return; + if(text_idx >= MAX_TEXT_LEN) return; // was '>': at text_idx==MAX_TEXT_LEN this wrote text[32], 1 byte past the buffer if (text_idx < 10) setDigitPre(text_idx, x); text[text_idx++] = x; diff --git a/mk4-time/USB_DEVICE/App/usbd_cdc_if.c b/mk4-time/USB_DEVICE/App/usbd_cdc_if.c index 2fbbe07..ee4aa4b 100644 --- a/mk4-time/USB_DEVICE/App/usbd_cdc_if.c +++ b/mk4-time/USB_DEVICE/App/usbd_cdc_if.c @@ -291,6 +291,9 @@ uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len) /* USER CODE BEGIN 7 */ USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*)hUsbDeviceFS.pClassDataCDC; + if (hcdc == NULL){ /* not enumerated — same NULL-deref-on-charger-only pattern */ + return USBD_BUSY; + } if (hcdc->TxState != 0){ return USBD_BUSY; } @@ -307,9 +310,18 @@ uint8_t CDC_Copy_Transmit(uint8_t* nmea, uint16_t Len) static uint8_t txbuf[NMEA_BUF_SIZE]; USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*)hUsbDeviceFS.pClassDataCDC; + /* pClassDataCDC is NULL until a host enumerates the device. On charger-only power it + never gets set, so the unconditional hcdc->TxState below dereferenced NULL and + hard-faulted on every forwarded NMEA sentence. Bail out cleanly when not enumerated. */ + if (hcdc == NULL){ + return USBD_FAIL; + } if (hcdc->TxState != 0){ return USBD_BUSY; } + if (Len > sizeof txbuf){ /* never overrun the static buffer (defensive) */ + return USBD_FAIL; + } memcpy( txbuf, nmea, Len ); USBD_CDC_SetTxBuffer(&hUsbDeviceFS, txbuf, Len); From bd112bc6f51376402bb08eb4937cde31b6c03a0b Mon Sep 17 00:00:00 2001 From: Peter Lewis Date: Sun, 12 Jul 2026 20:59:00 +0100 Subject: [PATCH 09/37] =?UTF-8?q?tz:=20distance-gate=20the=20ZoneDetect=20?= =?UTF-8?q?lookup=20=E2=80=94=20kill=20the=20300=20ms/second=20main-loop?= =?UTF-8?q?=20stall?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit $PMLOOP on real hardware pinned the once-per-second main-loop stall to tag 3 (the timezone lookup): a steady $PMLOOP,300,3 every second on a STATIONARY, GPS-locked clock. Root cause: the NMEA parser sets new_position on every 1 Hz fix, and the main loop answers it by re-running the full ZoneDetect lookup — f_open TZMAP.BIN → ZDOpenDatabase → polygon search, all off the QSPI FATFS, ~300 ms. With no distance threshold, metre-scale GPS jitter re-ran the whole thing every second: the clock re-decided it was in Europe/London 60×/minute. That 300 ms stall is what starved the seg-balance mirror refill (the once-per-second BALANCE freeze — the ISR-fresh-mirrors fix cured the symptom, this cures the cause) and is the dominant contributor to the date-board OVRDIS byte-drop window. Fix: gate the lookup on real movement — skip unless the fix moved >0.005° (≈0.5 km) since the last lookup. ~100× the jitter of a still clock (so it looks up exactly ONCE), far finer than any timezone boundary (so a moving clock still re-detects within ~0.5 km of a crossing). The per-loop distance check is a handful of FP ops. Device-main-loop only — inert in the emulator (which never runs main()'s while(1)); emu + ARM both build clean, date board unchanged. Co-Authored-By: Claude Fable 5 --- mk4-time/Core/Src/main.c | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/mk4-time/Core/Src/main.c b/mk4-time/Core/Src/main.c index 8ec7476..3fa3bc4 100644 --- a/mk4-time/Core/Src/main.c +++ b/mk4-time/Core/Src/main.c @@ -149,6 +149,11 @@ uint8_t decisec=0, centisec=0, millisec=0; float longitude=-9999, latitude=-9999; _Bool data_valid=0, had_pps=0, rtc_good=0, new_position=1; +// Last fix the ZoneDetect timezone lookup actually ran for. The NMEA parser sets new_position on +// EVERY 1 Hz fix, but that lookup reads TZMAP.BIN off the QSPI FATFS and costs ~300 ms — so on a +// stationary clock it re-ran every second on metre-scale GPS jitter, a 300 ms main-loop stall per +// second (starves the balance mirrors, widens the date-board byte-drop window). 999 = never yet. +double zone_lat=999.0, zone_lon=999.0; // Astro pack — sun/moon/grid read-outs, computed once a second in the main loop // (astro_update) and formatted by the sendDate cases, the same compute-in-loop / @@ -2389,11 +2394,18 @@ int main(void) /* USER CODE BEGIN WHILE */ while (1) { + // Distance gate: skip the ~300 ms FATFS/ZoneDetect lookup unless the fix has actually moved far + // enough to plausibly change zone. 0.005° ≈ 0.5 km — ~100× the metre-scale jitter of a stationary + // clock, yet far finer than any timezone boundary, so a moving clock still re-detects its zone + // within ~0.5 km of a crossing while a still one looks up exactly once. (dlat²+dlon² threshold + // 2.5e-5 = 0.005²; the cos-lat foreshortening of longitude only makes the gate MORE conservative.) if (new_position && !qspi_write_time && !config.zone_override && (data_valid || (config.fake_long && config.fake_lat)) - && latitude>=-90.0 && latitude<=90.0 && longitude>=-180.0 && longitude<=180.0) { + && latitude>=-90.0 && latitude<=90.0 && longitude>=-180.0 && longitude<=180.0 + && (zone_lat>90.0 || (latitude-zone_lat)*(latitude-zone_lat) + (longitude-zone_lon)*(longitude-zone_lon) > 2.5e-5)) { new_position=0; + zone_lat=latitude; zone_lon=longitude; fatfs_busy=1; // map lookup + loadRulesSingle touch FATFS; block the eject-time check FIL mapfile; if (f_open(&mapfile, MAP_FILENAME, FA_READ) == FR_OK) { From 01663f992606d55bc0667ab977be2cfc29020c5e Mon Sep 17 00:00:00 2001 From: Peter Lewis Date: Sun, 5 Jul 2026 12:07:00 +0100 Subject: [PATCH 10/37] Add sidereal (LST) and apparent-solar time-row modes MODE_LST and MODE_SOLAR turn the big HH:MM:SS digits into a live ticking Local Sidereal Time or apparent-solar clock. Both reuse the MODE_COUNTDOWN takeover pattern: the heavy GMST/EoT double maths runs once per second in the main loop, staging pre-rendered digits that the SysTick handlers latch at the true GPS PPS boundary -- so accuracy and the P3..P0 sub-second digit-hiding are inherited from the disciplined civil second, and civil timekeeping (currentTime, PPS, the date row) is untouched. The display is quantised to civil second boundaries and reseeded every second, so sidereal's 1.00273791x rate makes the seconds double-step once every ~6 min -- the honest signature of a GPS-disciplined sidereal clock. A dedicated colon animation (alt_colon_mode, default alt_sawtooth, kept distinct from the civil colon) marks the mode so it can never be mistaken for civil time; apparent solar especially can sit within minutes of it. With no position the row shows dashes -- never GMST-as-LST. astro.c regains local_sidereal_time()/local_solar_time() with the GMST series factored into one helper (gmst_hours, +T^2 term); native suite 53/53 incl. the IAU J2000 anchor and Meeus example 12.b. All default-off: stock behaviour unchanged. Co-Authored-By: Claude Opus 4.8 --- mk4-time/Core/Inc/astro.h | 3 + mk4-time/Core/Inc/main.h | 9 +- mk4-time/Core/Src/astro.c | 36 ++++- mk4-time/Core/Src/main.c | 302 +++++++++++++++++++++++++++++++++++-- mk4-time/test/test_astro.c | 19 +++ qspi/config.txt | 17 +++ 6 files changed, 369 insertions(+), 17 deletions(-) diff --git a/mk4-time/Core/Inc/astro.h b/mk4-time/Core/Inc/astro.h index c296f6c..26c5698 100644 --- a/mk4-time/Core/Inc/astro.h +++ b/mk4-time/Core/Inc/astro.h @@ -38,6 +38,9 @@ int moon_phase_index(double phase); /* 0..7, see ASTRO_MOON_NAMES /* (d) Equation of time in minutes (+ = apparent sun ahead of mean/clock sun). */ double equation_of_time(double unix_s); +double local_sidereal_time(double unix_s, double lon); /* LMST, hours [0,24), lon E+ */ +double local_solar_time(double unix_s, double lon); /* apparent solar, hours [0,24) */ + /* (e) 6-character Maidenhead locator for (lat, lon). out must hold >= 7 bytes. * Writes "----\0" if either coordinate is non-finite. */ void maidenhead(double lat, double lon, char out[7]); diff --git a/mk4-time/Core/Inc/main.h b/mk4-time/Core/Inc/main.h index 86af852..ce35333 100644 --- a/mk4-time/Core/Inc/main.h +++ b/mk4-time/Core/Inc/main.h @@ -173,13 +173,20 @@ enum { MODE_GRID, // Maidenhead grid locator MODE_LATLON, // latitude / longitude, auto-paged + // Alternate-timebase TIME-ROW modes: the big digits tick Local Sidereal Time or + // apparent solar time, reseeded from the GPS-disciplined second; the + // date row keeps the civil date and a dedicated colon animation marks the mode. + MODE_LST, + MODE_SOLAR, + NUM_DISPLAY_MODES }; enum { COUNT_NORMAL =0, COUNT_HIDDEN, - COUNT_DOWN + COUNT_DOWN, + COUNT_ALT // time row driven by the alternate timebase (MODE_LST / MODE_SOLAR) }; enum { diff --git a/mk4-time/Core/Src/astro.c b/mk4-time/Core/Src/astro.c index e34f680..3a64a83 100644 --- a/mk4-time/Core/Src/astro.c +++ b/mk4-time/Core/Src/astro.c @@ -22,6 +22,19 @@ const char *const ASTRO_MOON_NAMES[8] = { static double days_since_j2000(double unix_s) { return (unix_s - J2000_UNIX) / 86400.0; } +/* Greenwich Mean Sidereal Time in hours [0,24). Factored out so sun_az_el and + * local_sidereal_time share one series and can never drift apart. The quadratic + * term keeps truncation below ~1 ms for decades (the linear series alone drifts to + * ~10 ms by 2033). Note the input is GPS-derived UTC, not UT1: true-sky sidereal + * accuracy is floored by DUT1 (up to +/-0.9 s) by design. */ +static double gmst_hours(double n) { + double T = n / 36525.0; + double g = fmod(18.697374558 + 24.06570982441908 * n + 0.000026 * T * T, 24.0); + if (g < 0.0) g += 24.0; + if (g >= 24.0) g -= 24.0; + return g; +} + /* The shared low-precision solar block: from days-since-J2000 produce mean * longitude L, anomaly g, ecliptic longitude lambda, obliquity eps, and the * apparent right ascension alpha (deg) and declination delta (rad). */ @@ -46,7 +59,7 @@ void sun_az_el(double lat, double lon, double unix_s, double *az, double *el) { double alpha, delta; sun_ecliptic(n, NULL, NULL, NULL, NULL, &alpha, &delta); - double gmst = fmod(18.697374558 + 24.06570982441908 * n, 24.0); /* GMST in hours, used as-is */ + double gmst = gmst_hours(n); /* hours */ double lst = gmst * 15.0 + lon; /* degrees */ double ha = (lst - alpha) * DEG; /* radians */ @@ -67,6 +80,27 @@ double equation_of_time(double unix_s) { return 4.0 * diff; /* minutes */ } +/* Local Mean Sidereal Time, decimal hours [0,24). LMST = GMST + longitude/15 + * (east-positive). Anchors: GMST(J2000.0) = 18.697374558 h (IAU); Meeus ex. 12.b. */ +double local_sidereal_time(double unix_s, double lon) { + double lst = fmod(gmst_hours(days_since_j2000(unix_s)) + lon / 15.0, 24.0); + if (lst < 0.0) lst += 24.0; + if (lst >= 24.0) lst -= 24.0; /* fmod residue can round the wrap to exactly 24.0 */ + return lst; /* hours [0,24) */ +} + +/* Local apparent solar ("sundial") time, decimal hours [0,24). + * Apparent solar = mean solar (UTC shifted by longitude) + equation of time. + * At the sun's meridian transit this equals 12.0 exactly, consistent with + * sun_times()'s solar_noon = 12 - eot/60 - lon/15. */ +double local_solar_time(double unix_s, double lon) { + double utc_h = fmod(unix_s / 3600.0, 24.0); + double t = fmod(utc_h + lon / 15.0 + equation_of_time(unix_s) / 60.0, 24.0); + if (t < 0.0) t += 24.0; + if (t >= 24.0) t -= 24.0; + return t; /* hours [0,24) */ +} + int sun_times(double lat, double lon, double unix_s, double *sunrise, double *sunset, double *solar_noon, double *civil_dusk, double *nautical_dusk, double *golden_dusk) { diff --git a/mk4-time/Core/Src/main.c b/mk4-time/Core/Src/main.c index 3fa3bc4..47bcef3 100644 --- a/mk4-time/Core/Src/main.c +++ b/mk4-time/Core/Src/main.c @@ -197,6 +197,13 @@ _Bool resendDate = 0; uint32_t LPTIM1_high; uint8_t displayMode = 0, countMode = 0, colonMode = 0; +// Civil vs alternate-timebase colon animation: colonMode is the ACTIVE selection that +// loadColonAnimation() renders; the per-context choices live here and applyColonForMode() +// swaps between them. The sidereal default must stay visually distinct from civil so +// MODE_LST/MODE_SOLAR can never masquerade as civil time. +uint8_t colonModeCivil = 0; +uint8_t colonModeAlt = COLON_MODE_ALT_SAWTOOTH; +_Bool colonAltExplicit = 0; // user explicitly set alt_colon_mode uint8_t requestMode = 255; uint8_t nmea_cdc_level=0; int debug_rtc_val = 0; @@ -346,6 +353,8 @@ void sendDate( _Bool now ){ switch (displayMode) { default: + case MODE_LST: // alt-timebase modes keep the civil date on the date row — + case MODE_SOLAR: // the bottom row stays an unambiguous civil anchor case MODE_ISO8601_STD: uart2_tx_buffer[1] ='2'; uart2_tx_buffer[2] ='0'; @@ -705,6 +714,119 @@ void setNextCountdown(time_t nextTime){ // Store UTC on RTC // need to also write zone into backup registers // Only called at the start of a second, don't attempt to write subseconds. +// --- Alternate timebase (MODE_LST / MODE_SOLAR) ------------------------------------------ +// The TIME ROW ticks Local Sidereal Time or apparent solar time. Heavy double +// math runs in THREAD context once per second (alt_update), staging the reading for the +// coming civil boundary; the SysTick_Alt_* handlers latch it at the .900 prep mark. The +// display is quantized to civil second boundaries — value = floor(alt time at the boundary), +// reseeded every second — so GPS discipline and holdover honesty are inherited from +// currentTime for free. Sidereal runs 1.00273791x civil: the seconds display double-steps +// once every ~6 min 5 s. That skip is the authentic signature of a true sidereal clock. +static volatile struct { + uint8_t hh, mm, ss; + uint32_t for_time; // civil epoch this reading is the floor of; 0 = invalid +} alt_stage; +static uint8_t alt_hh, alt_mm, alt_ss; // ISR-owned: what the row currently shows +static volatile _Bool alt_have_pos = 0; +static volatile _Bool alt_seed_pending = 0; // mode entered: thread must seed the row +static volatile uint8_t alt_gen = 0; // bumped on mode entry; cancels in-flight staging + +// Overlay an alternate HH:MM:SS onto the next7seg staging buffer. The stock +// setNextTimestamp() has just run (keeping nextBcd / DST / date-row bookkeeping fresh); +// only the six time-row digit patterns are replaced. +#define alt_render_next7seg(hh, mm, ss) do { \ + next7seg.c = cLut[(ss) % 10]; \ + next7seg.b[0] = bCat0 | cLut[(hh) / 10] << 2; \ + next7seg.b[1] = bCat1 | cLut[(hh) % 10] << 2; \ + next7seg.b[2] = bCat2 | cLut[(mm) / 10] << 2; \ + next7seg.b[3] = bCat3 | cLut[(mm) % 10] << 2; \ + next7seg.b[4] = bCat4 | cLut[(ss) / 10] << 2; \ + } while (0) + +// The .900 prep for the alternate modes: stock next-second bookkeeping first, then latch +// the staged reading — or, if the main loop was starved past the boundary, advance the last +// shown reading by one second. LST's fallback runs SLOW (2.74 ms/s; the reseed snap is +// always forward), SOLAR's runs fast by at most ~0.35 ms/s at the EoT extremes — a +// visible backwards reseed would need ~48+ minutes of continuous main-loop starvation. +#define alt_prep_next() do { \ + currentTime++; \ + setNextTimestamp( currentTime ); \ + if (alt_stage.for_time == (uint32_t)currentTime) { \ + alt_hh = alt_stage.hh; alt_mm = alt_stage.mm; alt_ss = alt_stage.ss; \ + } else if (++alt_ss >= 60) { \ + alt_ss = 0; \ + if (++alt_mm >= 60) { alt_mm = 0; if (++alt_hh >= 24) alt_hh = 0; } \ + } \ + alt_render_next7seg(alt_hh, alt_mm, alt_ss); \ + sendDate(0); \ + } while (0) + +// Compute floor-HH:MM:SS of the alternate time at `when` (thread context only: doubles). +static _Bool alt_compute(uint32_t when, uint8_t *hh, uint8_t *mm, uint8_t *ss){ + float lat = latitude, lon = longitude; // one consistent snapshot (astro_update pattern) + if (!astro_pos_ok(lat, lon)) return 0; + double hours = (displayMode == MODE_LST) + ? local_sidereal_time((double)when, (double)lon) + : local_solar_time((double)when, (double)lon); + if (!(hours >= 0.0) || hours >= 24.0) hours = 0.0; // NaN / float-residue guard + int h2 = (int)hours; + double fm = (hours - h2) * 60.0; + int m2 = (int)fm; + int s2 = (int)((fm - m2) * 60.0); + if (h2 > 23) h2 = 23; + if (m2 > 59) m2 = 59; + if (s2 > 59) s2 = 59; + *hh = (uint8_t)h2; *mm = (uint8_t)m2; *ss = (uint8_t)s2; + return 1; +} + +// Main-loop staging (thread context — ALL the double math for these modes lives here). +// Two jobs: (a) SEED after mode entry or position go-live — render + latch the current +// reading immediately and install the live handlers, so a PPS latch can never show civil +// digits under the alternate colon; (b) STAGE the reading for the coming civil boundary. +// A generation counter cancels any in-flight computation when the mode flips mid-pass, so +// a stale timebase can never be stamped as valid. +void alt_update(void){ + if (displayMode != MODE_LST && displayMode != MODE_SOLAR) return; + + uint8_t gen = alt_gen; // snapshot: mode flips abort the publish below + + if (alt_seed_pending || !alt_have_pos) { + uint8_t hh, mm, ss; + if (!alt_compute((uint32_t)currentTime, &hh, &mm, &ss)) { + alt_have_pos = 0; // stay dashed; retried every pass + return; + } + __disable_irq(); + if (gen == alt_gen) { + alt_hh = hh; alt_mm = mm; alt_ss = ss; + alt_render_next7seg(alt_hh, alt_mm, alt_ss); // alt digits now staged: any latch is honest + latchSegments() // and shown immediately (countdown precedent) + alt_have_pos = 1; + alt_seed_pending = 0; + } + __enable_irq(); + if (gen == alt_gen) setPrecision(); // install Alt_Px/PPS now — don't wait for PendSV, + // or the NoUpdate .900 prep could stage civil digits + return; // stage the coming boundary on the next pass + } + + uint32_t target = (uint32_t)currentTime + 1; + if (alt_stage.for_time == target) return; + uint8_t hh, mm, ss; + if (!alt_compute(target, &hh, &mm, &ss)) { + alt_have_pos = 0; // position lost: setPrecision dashes it this second + alt_stage.for_time = 0; + return; + } + __disable_irq(); + if (gen == alt_gen) { // publish only if no mode flip happened mid-compute + alt_stage.hh = hh; alt_stage.mm = mm; alt_stage.ss = ss; + alt_stage.for_time = target; // IRQs masked: fields and stamp are one atomic unit + } + __enable_irq(); +} + void write_rtc(void){ RTC_DateTypeDef sdatestructure; @@ -871,6 +993,9 @@ void decodeRMC(void){ // Under normal conditions, we should only be parsing nmea at around .300 to .400 // USART1 preemption priority is currently 1, so we could be interrupted by systick here setNextTimestamp( currentTime ); + // In the alternate time-row modes the civil digits just staged must not reach the + // display: restore the alt overlay so the boundary latch stays honest. + if (countMode == COUNT_ALT) alt_render_next7seg(alt_hh, alt_mm, alt_ss); sendDate(0); } } @@ -1077,6 +1202,26 @@ float parseBrightness(char *v, _Bool invert){ #define set_mode_enabled(mode, value) \ if ((config.modes_enabled[mode] = truthy(value))) requestMode=mode; +static uint8_t parseColonName(const char *value){ + if (strcasecmp(value, "solid") == 0) return COLON_MODE_SOLID; + if (strcasecmp(value, "heartbeat") == 0) return COLON_MODE_HEARTBEAT; + if (strcasecmp(value, "sawtooth") == 0) return COLON_MODE_1PPS_SAWTOOTH; + if (strcasecmp(value, "alt_sawtooth") == 0) return COLON_MODE_ALT_SAWTOOTH; + if (strcasecmp(value, "toggle") == 0) return COLON_MODE_TOGGLE; + return COLON_MODE_SLOWFADE; +} + +// Select the colon animation for the current display mode (idempotent, thread context). +// Alternate-timebase modes get their own animation so they read as "not civil" at a glance. +void applyColonForMode(void){ + uint8_t want = (displayMode == MODE_LST || displayMode == MODE_SOLAR) + ? colonModeAlt : colonModeCivil; + if (want != colonMode) { + colonMode = want; + loadColonAnimation(); + } +} + void parseConfigString(char *key, char *value) { if (strcasecmp(key, "text") == 0) { @@ -1172,6 +1317,10 @@ void parseConfigString(char *key, char *value) { } else if (strcasecmp(key, "page_ms") == 0) { int v = atoi(value); config.page_ms = v < 0 ? 0 : (v > 65535 ? 65535 : v); // fits uint16; 0 -> default + } else if (strcasecmp(key, "MODE_LST") == 0) { + set_mode_enabled(MODE_LST, value); + } else if (strcasecmp(key, "MODE_SOLAR") == 0) { + set_mode_enabled(MODE_SOLAR, value); } else if (strcasecmp(key, "Tolerance_time_1ms") == 0) { config.tolerance_1ms = atoi(value); } else if (strcasecmp(key, "Tolerance_time_10ms") == 0) { @@ -1184,17 +1333,12 @@ void parseConfigString(char *key, char *value) { config.fake_lat = atof(value); } else if (strcasecmp(key, "colon_mode") == 0) { - if (strcasecmp(value, "solid") == 0) { - colonMode = COLON_MODE_SOLID; - } else if (strcasecmp(value, "heartbeat") == 0) { - colonMode = COLON_MODE_HEARTBEAT; - } else if (strcasecmp(value, "sawtooth") == 0) { - colonMode = COLON_MODE_1PPS_SAWTOOTH; - } else if (strcasecmp(value, "alt_sawtooth") == 0) { - colonMode = COLON_MODE_ALT_SAWTOOTH; - } else if (strcasecmp(value, "toggle") == 0) { - colonMode = COLON_MODE_TOGGLE; - } else colonMode = COLON_MODE_SLOWFADE; + colonModeCivil = parseColonName(value); + + } else if (strcasecmp(key, "alt_colon_mode") == 0) { + + colonModeAlt = parseColonName(value); // shared by MODE_LST and MODE_SOLAR + colonAltExplicit = 1; } else if (strcasecmp(key, "nmea") == 0) { @@ -1228,7 +1372,13 @@ void parseConfigString(char *key, char *value) { } void postConfigCleanup(void){ - loadColonAnimation(); + // Keep the alternate-timebase colon distinct unless the user EXPLICITLY matched them. + if (!colonAltExplicit && colonModeAlt == colonModeCivil) { + colonModeAlt = (colonModeCivil != COLON_MODE_ALT_SAWTOOTH) ? COLON_MODE_ALT_SAWTOOTH + : COLON_MODE_TOGGLE; + } + colonMode = 0xFF; // force applyColonForMode to reload exactly once + applyColonForMode(); // check at least one mode is enabled uint8_t j = 0; @@ -1331,7 +1481,9 @@ void readConfigFile(void){ config.tolerance_100ms = 100000; config.zone_override = 0; config.brightness_override = -1.0; - colonMode = 0; + colonModeCivil = 0; + colonModeAlt = COLON_MODE_ALT_SAWTOOTH; + colonAltExplicit = 0; FIL file; @@ -1790,6 +1942,59 @@ void SysTick_CountDown_P0(void) } } +// Alternate-timebase handlers (MODE_LST / MODE_SOLAR): identical to the CountUp family — +// same cascade, same sub-second painting, same precision ladder — except the .900 prep +// overlays the staged alternate HH:MM:SS onto next7seg (see alt_prep_next). +void SysTick_Alt_P3(void) +{ + timetick() + + buffer_c[3].low=cLut[millisec]; + buffer_c[2].low=cLut[centisec]; + buffer_c[1].low=cLut[decisec]; + + HAL_IncTick(); + + if (decisec==9 && centisec==0 && millisec==0){ + alt_prep_next(); + } +} + +void SysTick_Alt_P2(void) { + timetick() + + buffer_c[2].low=cLut[centisec]; + buffer_c[1].low=cLut[decisec]; + + HAL_IncTick(); + + if (decisec==9 && centisec==0 && millisec==0){ + alt_prep_next(); + } +} + +void SysTick_Alt_P1(void) { + timetick() + + buffer_c[1].low=cLut[decisec]; + + HAL_IncTick(); + + if (decisec==9 && centisec==0 && millisec==0){ + alt_prep_next(); + } +} + +void SysTick_Alt_P0(void) { + timetick() + + HAL_IncTick(); + + if (decisec==9 && centisec==0 && millisec==0){ + alt_prep_next(); + } +} + void SysTick_Dummy(void){ HAL_IncTick(); } @@ -1992,6 +2197,51 @@ void setPrecision(void){ SetSysTick( &SysTick_CountUp_P0 ); } + } else if (countMode == COUNT_ALT) { + + if (!alt_have_pos) { + // No usable position (no fix, no fake_longitude): dashes, digits not ticking — + // never GMST-as-LST, never a guessed longitude. Re-evaluated every second; the + // row goes live the moment a position appears. resendDate keeps the civil date + // row refreshing (PendSV's resend check runs right after this) — without it the + // date would freeze across midnight while dashed. + resendDate = 1; + SetPPS( &PPS_NoUpdate ); + SetSysTick( &SysTick_CountUp_NoUpdate ); + buffer_b[0] = bCat0 | 0b01000000 << 2; + buffer_b[1] = bCat1 | 0b01000000 << 2; + buffer_b[2] = bCat2 | 0b01000000 << 2; + buffer_b[3] = bCat3 | 0b01000000 << 2; + buffer_b[4] = bCat4 | 0b01000000 << 2; + buffer_c[0].low = 0b01000000; + buffer_c[3].low = 0b01000000; + buffer_c[2].low = 0b01000000; + buffer_c[1].low = 0b01000000; + buffer_c[0].high= 0b11001110; + } else if (currentTime - last_pps_time < config.tolerance_1ms){ + SetPPS( &PPS ); + buffer_c[0].high= 0b11001110 | cSegDP; + SetSysTick( &SysTick_Alt_P3 ); + } else if (currentTime - last_pps_time < config.tolerance_10ms){ + SetPPS( &PPS ); + buffer_c[3].low = 0b01000000; + buffer_c[0].high= 0b11001110 | cSegDP; + SetSysTick( &SysTick_Alt_P2 ); + } else if (currentTime - rtc_last_calibration < config.tolerance_100ms){ + SetPPS( &PPS ); + buffer_c[3].low = 0b01000000; + buffer_c[2].low = 0b01000000; + buffer_c[0].high= 0b11001110 | cSegDP; + SetSysTick( &SysTick_Alt_P1 ); + } else { + SetPPS( &PPS ); + buffer_c[3].low = 0b01000000; + buffer_c[2].low = 0b01000000; + buffer_c[1].low = 0b01000000; + buffer_c[0].high= 0b11001110; + SetSysTick( &SysTick_Alt_P0 ); + } + } else if (displayMode == MODE_COUNTDOWN) { if (config.countdown_to >= currentTime) { @@ -2067,8 +2317,9 @@ void nextMode(_Bool reverse){ buffer_c[2].high &= ~cSegDP; buffer_c[3].high &= ~cSegDP; } - if ( displayMode == MODE_ISO_WEEK || justExited(MODE_COUNTDOWN)) { - // If we exit countdown mode at .9 seconds + if ( displayMode == MODE_ISO_WEEK || justExited(MODE_COUNTDOWN) + || justExited(MODE_LST) || justExited(MODE_SOLAR)) { + // If we exit countdown/alt mode at .9 seconds // it will show the wrong time for .1 seconds setNextTimestamp(currentTime); } @@ -2094,6 +2345,22 @@ void nextMode(_Bool reverse){ TIM2->CCR2 = 0; latchSegments(); + } else if (displayMode == MODE_LST || displayMode == MODE_SOLAR) { + + countMode = COUNT_ALT; + setNextTimestamp(currentTime); // stock civil bookkeeping (integer path; countdown-arm cost) + // NO double math here: nextMode can run in the USART2 button ISR (priority 0, which + // blocks SysTick and the PPS EXTI), and the LST/solar computation is ~100 µs of + // soft-double. Invalidate and let the main-loop alt_update() seed within one pass + // (<100 ms); until then setPrecision shows the dashed state. + alt_gen++; // cancels any in-flight staging for the previous timebase + alt_stage.for_time = 0; + alt_have_pos = 0; + alt_seed_pending = 1; + setPrecision(); + TIM2->CCR1 = 0; + TIM2->CCR2 = 0; + } else { if (countMode != COUNT_NORMAL) { @@ -2105,6 +2372,7 @@ void nextMode(_Bool reverse){ latchSegments(); } } + applyColonForMode(); // idempotent: alt colon on entry, civil colon on exit sendDate(1); } void button1pressed(void){ @@ -2487,6 +2755,10 @@ int main(void) } } + // MODE_LST / MODE_SOLAR: stage the next civil boundary's alternate reading + // (thread-context doubles; no-op in every other mode) + alt_update(); + /* USER CODE END WHILE */ diff --git a/mk4-time/test/test_astro.c b/mk4-time/test/test_astro.c index 4df94fc..13a1f41 100644 --- a/mk4-time/test/test_astro.c +++ b/mk4-time/test/test_astro.c @@ -99,6 +99,25 @@ int main(void) { maidenhead(-41.283, 174.745, g); chks("Wellington", g, "RE78ir"); maidenhead(1.0 / 0.0, 0.0, g); chks("non-finite", g, "----"); + printf("local_sidereal_time (h):\n"); + /* GMST at J2000.0 = 18.697374558 h (IAU); Meeus ex. 12.b, 1987-04-10 19:21:00 UT + * -> mean GMST 8h34m57.1s = 8.582525 h. LST = GMST + lon/15 checks shift + wrap. */ + chk("LST J2000 lon0", local_sidereal_time(946728000.0, 0.0), 18.697375, 0.0001); + chk("LST Meeus lon0", local_sidereal_time(545080860.0, 0.0), 8.582525, 0.0001); + chk("LST J2000 lon -75", local_sidereal_time(946728000.0, -75.0), 13.697375, 0.0001); + chk("LST J2000 lon +90wrap", local_sidereal_time(946728000.0, 90.0), 0.697375, 0.0001); + + printf("local_solar_time (h):\n"); + /* At meridian transit apparent solar time is 12:00 exactly. Build that instant from + * sun_times()'s solar_noon and confirm -- catches any longitude/EoT sign or wrap error. */ + for (int i = 0; i < 4; i++) { + double nn; + sun_times(V[i]->lat, V[i]->lon, V[i]->t, NULL, NULL, &nn, NULL, NULL, NULL); + double noon_unix = trunc(V[i]->t / 86400.0) * 86400.0 + nn * 3600.0; + char nm[40]; sprintf(nm, "solar@noon %s", V[i]->tag); + chk(nm, local_solar_time(noon_unix, V[i]->lon), 12.0, 0.02); + } + printf("\n%d/%d passed, %d failed\n", total - fails, total, fails); return fails ? 1 : 0; } diff --git a/qspi/config.txt b/qspi/config.txt index 5a4723f..b6a6983 100644 --- a/qspi/config.txt +++ b/qspi/config.txt @@ -135,6 +135,23 @@ MODE_LATLON = disabled # Dwell per sub-screen for the paged modes (MODE_SUN, MODE_LATLON), in milliseconds. # Default 5500 (a subjectively-tuned cadence, found by feel); floored at 250. page_ms = 5500 +# Local Sidereal Time as a LIVE TICKING CLOCK on the time row (big digits). The date row +# keeps the civil date, and the colons animate differently (alt_colon_mode below) so it +# can never be mistaken for civil time. Sidereal runs 1.00273791x faster than civil, so the +# seconds display double-steps about once every 6 minutes -- real sidereal behaviour, not a +# glitch. Needs a position (GPS fix or fake_longitude); shows dashes without one. +MODE_LST = disabled + +# Apparent solar time on the time row -- what a sundial reads: UTC shifted by +# your longitude plus the equation of time. Reads exactly 12:00:00 at local solar noon. +MODE_SOLAR = disabled + +# Colon animation while MODE_LST / MODE_SOLAR is shown, so the alternate timebase is +# unmistakable at a glance -- solar especially, which can sit within minutes of civil. +# Same names as colon_mode; automatically kept different from your civil colon unless you +# explicitly set them equal here. One of: slowfade, heartbeat, sawtooth, alt_sawtooth, +# toggle, solid +#alt_colon_mode = alt_sawtooth # Fixed position for the astro modes when there is no GPS fix (decimal degrees, N+/E+). # Note: setting these also pins the clock's position (GPS position updates are ignored). From 67f2dfb0154087a6abb4191e3d35301f6c3612b2 Mon Sep 17 00:00:00 2001 From: Peter Lewis Date: Sun, 12 Jul 2026 11:54:50 +0100 Subject: [PATCH 11/37] sidereal: rename config key alt_colon_mode -> colon_alt_mode Co-Authored-By: Claude Opus 4.8 --- mk4-time/Core/Src/main.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mk4-time/Core/Src/main.c b/mk4-time/Core/Src/main.c index 47bcef3..c4f7ff7 100644 --- a/mk4-time/Core/Src/main.c +++ b/mk4-time/Core/Src/main.c @@ -203,7 +203,7 @@ uint8_t displayMode = 0, countMode = 0, colonMode = 0; // MODE_LST/MODE_SOLAR can never masquerade as civil time. uint8_t colonModeCivil = 0; uint8_t colonModeAlt = COLON_MODE_ALT_SAWTOOTH; -_Bool colonAltExplicit = 0; // user explicitly set alt_colon_mode +_Bool colonAltExplicit = 0; // user explicitly set colon_alt_mode uint8_t requestMode = 255; uint8_t nmea_cdc_level=0; int debug_rtc_val = 0; @@ -1335,9 +1335,9 @@ void parseConfigString(char *key, char *value) { colonModeCivil = parseColonName(value); - } else if (strcasecmp(key, "alt_colon_mode") == 0) { + } else if (strcasecmp(key, "colon_alt_mode") == 0) { - colonModeAlt = parseColonName(value); // shared by MODE_LST and MODE_SOLAR + colonModeAlt = parseColonName(value); // shared by MODE_LST and MODE_SOLAR ("COLONALT" in the menu) colonAltExplicit = 1; } else if (strcasecmp(key, "nmea") == 0) { From f998de6fbdc54421703ddd82cd307357563f7f0d Mon Sep 17 00:00:00 2001 From: Peter Lewis Date: Tue, 7 Jul 2026 11:30:32 +0100 Subject: [PATCH 12/37] astro: subsolar-point helper (sun_subsolar) + shared-series comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sun_subsolar(t, &lat, &lon) returns the point where the sun is at the zenith: latitude = solar declination, longitude where the local hour angle is zero (lon = alpha - gmst*15). It reuses the same gmst_hours series as sun_az_el and local_sidereal_time, so the three can never drift apart. Library + tests only — no display mode consumes it yet (a companion app plots it). Tests: the sun must sit at elevation 90 deg over its own subsolar point at every existing test instant (self-consistent but not circular — az/el and subsolar take different paths to the sky), plus declination anchors at the solstices and the absolute GMST anchors (IAU J2000, Meeus ex. 12.b) recorded in comments. Co-Authored-By: Claude Opus 4.8 --- mk4-time/Core/Inc/astro.h | 4 ++++ mk4-time/Core/Src/astro.c | 15 +++++++++++++++ mk4-time/test/test_astro.c | 17 +++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/mk4-time/Core/Inc/astro.h b/mk4-time/Core/Inc/astro.h index 26c5698..417b4cc 100644 --- a/mk4-time/Core/Inc/astro.h +++ b/mk4-time/Core/Inc/astro.h @@ -41,6 +41,10 @@ double equation_of_time(double unix_s); double local_sidereal_time(double unix_s, double lon); /* LMST, hours [0,24), lon E+ */ double local_solar_time(double unix_s, double lon); /* apparent solar, hours [0,24) */ +/* (d2) Subsolar point at the given UTC instant: latitude = solar declination, + * longitude in [-180,180] (E+). Time-only — needs no observer position. */ +void sun_subsolar(double unix_s, double *lat, double *lon); + /* (e) 6-character Maidenhead locator for (lat, lon). out must hold >= 7 bytes. * Writes "----\0" if either coordinate is non-finite. */ void maidenhead(double lat, double lon, char out[7]); diff --git a/mk4-time/Core/Src/astro.c b/mk4-time/Core/Src/astro.c index 3a64a83..9a7aa04 100644 --- a/mk4-time/Core/Src/astro.c +++ b/mk4-time/Core/Src/astro.c @@ -70,6 +70,21 @@ void sun_az_el(double lat, double lon, double unix_s, double *az, double *el) { if (az) *az = a; } +void sun_subsolar(double unix_s, double *lat, double *lon) { + double n = days_since_j2000(unix_s); + double alpha, delta; + sun_ecliptic(n, NULL, NULL, NULL, NULL, &alpha, &delta); + /* Subsolar point: latitude = solar declination; longitude where the local hour + * angle is zero, i.e. lst == alpha => lon = alpha - gmst*15 (same GMST series + * as sun_az_el). */ + double gmst = gmst_hours(n); + double l = fmod(alpha - gmst * 15.0, 360.0); + if (l < -180.0) l += 360.0; + else if (l > 180.0) l -= 360.0; + if (lat) *lat = delta * RAD; + if (lon) *lon = l; +} + double equation_of_time(double unix_s) { double n = days_since_j2000(unix_s); double L, alpha; diff --git a/mk4-time/test/test_astro.c b/mk4-time/test/test_astro.c index 13a1f41..3c85600 100644 --- a/mk4-time/test/test_astro.c +++ b/mk4-time/test/test_astro.c @@ -99,6 +99,23 @@ int main(void) { maidenhead(-41.283, 174.745, g); chks("Wellington", g, "RE78ir"); maidenhead(1.0 / 0.0, 0.0, g); chks("non-finite", g, "----"); + printf("sun_subsolar:\n"); + /* Self-consistency (non-circular vs the sun_az_el vectors above): the sun must be + * at the zenith of its own subsolar point — elevation 90 deg at every test instant. */ + for (int i = 0; i < 4; i++) { + double slat, slon, e; + sun_subsolar(V[i]->t, &slat, &slon); + sun_az_el(slat, slon, V[i]->t, NULL, &e); + char nm[40]; sprintf(nm, "zenith %s", V[i]->tag); + chk(nm, e, 90.0, 0.01); + } + /* Declination anchors: June solstice ~ +23.44, deep northern winter ~ -23. */ + { + double slat; + sun_subsolar(V1.t, &slat, NULL); chk("decl solstice", slat, 23.436, 0.05); + sun_subsolar(V2.t, &slat, NULL); chk("decl jan 1", slat, -23.06, 0.10); + } + printf("local_sidereal_time (h):\n"); /* GMST at J2000.0 = 18.697374558 h (IAU); Meeus ex. 12.b, 1987-04-10 19:21:00 UT * -> mean GMST 8h34m57.1s = 8.582525 h. LST = GMST + lon/15 checks shift + wrap. */ From 5c8160ecada51ddd1152b4e73e98d3052293a4ea Mon Sep 17 00:00:00 2001 From: Peter Lewis Date: Sun, 5 Jul 2026 12:18:23 +0100 Subject: [PATCH 13/37] Add self-learning holdover temperature compensation Builds on the $PMTXTS telemetry: while GPS-locked the clock learns each oscillator's frequency error vs die temperature (binned, per-oscillator), then during a GPS-loss holdover it steers the SysTick timebase from the HSE model to cancel temperature-driven drift, and optionally trims RTC->CALR from the LSE model so the battery RTC hands over better time across a power loss. - tc_learn / tc_apply / tc_rtc config keys (all default off = stock behaviour) - steering is a Bresenham SysTick->LOAD stretch inside the tick ISR (integer only; all model maths is main-loop); origin-free HSE model so only the temperature DIFFERENCE since GPS loss is applied, and the per-edge PPS phase snap makes re-lock instantly neutral - 'tc_dump = on' over serial prints the learned coefficients as paste-ready config lines (+ a $PMTXTC sentence); pasting them back freezes the model - MODE_TEMPCOMP diagnostic display: die temp / model offset / sample count - ticks-per-ppm derived from the runtime SysTick period (no core-clock assumption) Hardware-validated on an Mk IV: the LSE model learned 18.68 ppm vs 18.9 ppm measured directly (~1%). Survived an adversarial review pass (ISR-safety, integer math, sign conventions, re-lock neutrality, off-means-identical). Co-Authored-By: Claude Opus 4.8 --- mk4-time/Core/Inc/main.h | 604 ++++++++++++++++++++------------------- mk4-time/Core/Src/main.c | 572 +++++++++++++++++++++++++++++++++++- qspi/config.txt | 294 ++++++++++--------- 3 files changed, 1032 insertions(+), 438 deletions(-) diff --git a/mk4-time/Core/Inc/main.h b/mk4-time/Core/Inc/main.h index ce35333..002a0c4 100644 --- a/mk4-time/Core/Inc/main.h +++ b/mk4-time/Core/Inc/main.h @@ -1,303 +1,307 @@ -/* USER CODE BEGIN Header */ -/** - ****************************************************************************** - * @file : main.h - * @brief : Header for main.c file. - * This file contains the common defines of the application. - ****************************************************************************** - * @attention - * - *

© Copyright (c) 2020 STMicroelectronics. - * All rights reserved.

- * - * This software component is licensed by ST under BSD 3-Clause license, - * the "License"; You may not use this file except in compliance with the - * License. You may obtain a copy of the License at: - * opensource.org/licenses/BSD-3-Clause - * - ****************************************************************************** - */ -/* USER CODE END Header */ - -/* Define to prevent recursive inclusion -------------------------------------*/ -#ifndef __MAIN_H -#define __MAIN_H - -#ifdef __cplusplus -extern "C" { -#endif - -/* Includes ------------------------------------------------------------------*/ -#include "stm32l4xx_hal.h" -#include "stm32l4xx_ll_lptim.h" -#include "stm32l4xx_ll_bus.h" -#include "stm32l4xx_ll_cortex.h" -#include "stm32l4xx_ll_rcc.h" -#include "stm32l4xx_ll_system.h" -#include "stm32l4xx_ll_utils.h" -#include "stm32l4xx_ll_pwr.h" -#include "stm32l4xx_ll_gpio.h" -#include "stm32l4xx_ll_dma.h" - -#include "stm32l4xx_ll_exti.h" - -/* Private includes ----------------------------------------------------------*/ -/* USER CODE BEGIN Includes */ - -/* USER CODE END Includes */ - -/* Exported types ------------------------------------------------------------*/ -/* USER CODE BEGIN ET */ - -typedef struct { - uint8_t tenYears; - uint8_t years; - uint8_t tenMonths; - uint8_t months; - uint8_t tenDays; - uint8_t days; - - uint8_t tenHours; - uint8_t hours; - uint8_t tenMinutes; - uint8_t minutes; - uint8_t tenSeconds; - uint8_t seconds; -} bcdStamp_t; - -typedef struct { - uint8_t low; - uint8_t high; -} buffer_c_t; - -extern buffer_c_t buffer_c[]; - -extern uint16_t buffer_b[]; - -extern _Bool delayedReadConfigFile; -extern _Bool delayedCheckOnEject; -extern volatile uint8_t fatfs_busy; - -extern _Bool waitingForLatch; -extern _Bool resendDate; - -/* USER CODE END ET */ - -/* Exported constants --------------------------------------------------------*/ -/* USER CODE BEGIN EC */ - -#define RULES_FILENAME "/TZRULES.BIN" -#define MAP_FILENAME "/TZMAP.BIN" -#define CONFIG_FILENAME "/CONFIG.TXT" - -#define cSegDP 0b00010000 - -#define cSegDecode0 0b00111111 -#define cSegDecode1 0b00000110 -#define cSegDecode2 0b01011011 -#define cSegDecode3 0b01001111 -#define cSegDecode4 0b01100110 -#define cSegDecode5 0b01101101 -#define cSegDecode6 0b01111101 -#define cSegDecode7 0b00000111 -#define cSegDecode8 0b01111111 -#define cSegDecode9 0b01101111 - -#define bSegDecode0 0b0011111100 -#define bSegDecode1 0b0000011000 -#define bSegDecode2 0b0101101100 -#define bSegDecode3 0b0100111100 -#define bSegDecode4 0b0110011000 -#define bSegDecode5 0b0110110100 -#define bSegDecode6 0b0111110100 -#define bSegDecode7 0b0000011100 -#define bSegDecode8 0b0111111100 -#define bSegDecode9 0b0110111100 - -#define bCat0 0b1111000000000000 -#define bCat1 0b1110001000000000 -#define bCat2 0b1101001000000000 -#define bCat3 0b1011001000000000 -#define bCat4 0b0111001000000000 - - -// ADC timer is 1000Hz, interrupt at TC -// DAC timer is 100Hz, interrupt at HT and TC -#define DAC_BUFFER_SIZE 20 -#define ADC_BUFFER_SIZE 50 - +/* USER CODE BEGIN Header */ +/** + ****************************************************************************** + * @file : main.h + * @brief : Header for main.c file. + * This file contains the common defines of the application. + ****************************************************************************** + * @attention + * + *

© Copyright (c) 2020 STMicroelectronics. + * All rights reserved.

+ * + * This software component is licensed by ST under BSD 3-Clause license, + * the "License"; You may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * opensource.org/licenses/BSD-3-Clause + * + ****************************************************************************** + */ +/* USER CODE END Header */ + +/* Define to prevent recursive inclusion -------------------------------------*/ +#ifndef __MAIN_H +#define __MAIN_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* Includes ------------------------------------------------------------------*/ +#include "stm32l4xx_hal.h" +#include "stm32l4xx_ll_lptim.h" +#include "stm32l4xx_ll_bus.h" +#include "stm32l4xx_ll_cortex.h" +#include "stm32l4xx_ll_rcc.h" +#include "stm32l4xx_ll_system.h" +#include "stm32l4xx_ll_utils.h" +#include "stm32l4xx_ll_pwr.h" +#include "stm32l4xx_ll_gpio.h" +#include "stm32l4xx_ll_dma.h" + +#include "stm32l4xx_ll_exti.h" + +/* Private includes ----------------------------------------------------------*/ +/* USER CODE BEGIN Includes */ + +/* USER CODE END Includes */ + +/* Exported types ------------------------------------------------------------*/ +/* USER CODE BEGIN ET */ + +typedef struct { + uint8_t tenYears; + uint8_t years; + uint8_t tenMonths; + uint8_t months; + uint8_t tenDays; + uint8_t days; + + uint8_t tenHours; + uint8_t hours; + uint8_t tenMinutes; + uint8_t minutes; + uint8_t tenSeconds; + uint8_t seconds; +} bcdStamp_t; + +typedef struct { + uint8_t low; + uint8_t high; +} buffer_c_t; + +extern buffer_c_t buffer_c[]; + +extern uint16_t buffer_b[]; + +extern _Bool delayedReadConfigFile; +extern _Bool delayedCheckOnEject; +extern volatile uint8_t fatfs_busy; + +extern _Bool waitingForLatch; +extern _Bool resendDate; + +/* USER CODE END ET */ + +/* Exported constants --------------------------------------------------------*/ +/* USER CODE BEGIN EC */ + +#define RULES_FILENAME "/TZRULES.BIN" +#define MAP_FILENAME "/TZMAP.BIN" +#define CONFIG_FILENAME "/CONFIG.TXT" + +#define cSegDP 0b00010000 + +#define cSegDecode0 0b00111111 +#define cSegDecode1 0b00000110 +#define cSegDecode2 0b01011011 +#define cSegDecode3 0b01001111 +#define cSegDecode4 0b01100110 +#define cSegDecode5 0b01101101 +#define cSegDecode6 0b01111101 +#define cSegDecode7 0b00000111 +#define cSegDecode8 0b01111111 +#define cSegDecode9 0b01101111 + +#define bSegDecode0 0b0011111100 +#define bSegDecode1 0b0000011000 +#define bSegDecode2 0b0101101100 +#define bSegDecode3 0b0100111100 +#define bSegDecode4 0b0110011000 +#define bSegDecode5 0b0110110100 +#define bSegDecode6 0b0111110100 +#define bSegDecode7 0b0000011100 +#define bSegDecode8 0b0111111100 +#define bSegDecode9 0b0110111100 + +#define bCat0 0b1111000000000000 +#define bCat1 0b1110001000000000 +#define bCat2 0b1101001000000000 +#define bCat3 0b1011001000000000 +#define bCat4 0b0111001000000000 + + +// ADC timer is 1000Hz, interrupt at TC +// DAC timer is 100Hz, interrupt at HT and TC +#define DAC_BUFFER_SIZE 20 +#define ADC_BUFFER_SIZE 50 + // NMEA 0183 messages have a max length of 82 characters; the extended $PMTXTS (with the SOF- // correlation tail: dwt_pps, sof_frame, dwt_sof) runs ~110, so this sizes the tx/rx buffers for it. #define NMEA_BUF_SIZE 128 - -#define CMD_LOAD_TEXT 0x90 -#define CMD_SET_FREQUENCY 0x91 -#define CMD_RELOAD_TEXT 0x92 -#define CMD_SHOW_CRC 0x9D - -//#define NONCOMPLIANT_DATE_MODES - -enum { - MODE_ISO8601_STD =0, - MODE_ISO_ORDINAL, - MODE_ISO_WEEK, - MODE_UNIX, - MODE_JULIAN_DATE, - MODE_MODIFIED_JD, - MODE_SHOW_OFFSET, - MODE_SHOW_TZ_NAME, - MODE_WEEKDAY, - MODE_WEEKDA_DD, - MODE_WDY_MM_DD, - MODE_STANDBY, - MODE_COUNTDOWN, - MODE_SATVIEW, - MODE_DEBUG_BRIGHTNESS, - MODE_DEBUG_RTC, - MODE_TEXT, - MODE_FIRMWARE_CRC_T, - MODE_FIRMWARE_CRC_D, - MODE_VBAT, - MODE_DISPLAYTEST, - MODE_TTFF, -#ifdef NONCOMPLIANT_DATE_MODES - MODE_DDMMYYYY, -#endif - - // Astro pack — GPS-derived astronomy read-outs. SATVIEW-style: the payload - // shows on the 10-char date row while the live clock keeps running on the - // time row. Enabled individually via the MODE_* config keys, like any mode. - MODE_SUN, // sunrise / sunset / solar noon (local), auto-paged - MODE_SUN_AZEL, // sun azimuth & elevation, now - MODE_MOON, // moon phase index + illuminated % - MODE_GRID, // Maidenhead grid locator - MODE_LATLON, // latitude / longitude, auto-paged - - // Alternate-timebase TIME-ROW modes: the big digits tick Local Sidereal Time or - // apparent solar time, reseeded from the GPS-disciplined second; the - // date row keeps the civil date and a dedicated colon animation marks the mode. - MODE_LST, - MODE_SOLAR, - - NUM_DISPLAY_MODES -}; - -enum { - COUNT_NORMAL =0, - COUNT_HIDDEN, - COUNT_DOWN, - COUNT_ALT // time row driven by the alternate timebase (MODE_LST / MODE_SOLAR) -}; - -enum { - COLON_MODE_SLOWFADE = 0, - COLON_MODE_HEARTBEAT, - COLON_MODE_1PPS_SAWTOOTH, - COLON_MODE_ALT_SAWTOOTH, - COLON_MODE_TOGGLE, - COLON_MODE_SOLID -}; - -enum { - NMEA_ALL=0, - NMEA_RMC, - NMEA_NONE -}; - -enum { - RULES_OK=0, - RULES_STR_ERR, - RULES_NO_FILE, - RULES_HEADER_ERR, - RULES_VERSION_UNKNOWN, - RULES_CATEGORY_UNKNOWN, - RULES_ZONE_UNKNOWN -}; - -enum { - SV_GPS_L1=0, - SV_GPS_UNKNOWN, - SV_GLONASS_L1, - SV_GLONASS_UNKNOWN, - SV_GALILEO_E1, - SV_GALILEO_UNKNOWN, - SV_BEIDOU_B1, - SV_BEIDOU_UNKNOWN, - SV_COUNT -}; -/* USER CODE END EC */ - -/* Exported macro ------------------------------------------------------------*/ -/* USER CODE BEGIN EM */ - -#define byteswap32(x) \ - ( ((x & 0xff000000) >> 24) | ((x & 0x00ff0000) >> 8) \ - | ((x & 0x0000ff00) << 8) | ((x & 0x000000ff) << 24)) - -/* USER CODE END EM */ - -void HAL_TIM_MspPostInit(TIM_HandleTypeDef *htim); - -/* Exported functions prototypes ---------------------------------------------*/ -void Error_Handler(void); - -/* USER CODE BEGIN EFP */ -void decodeRMC(void); -void decodeGSV(uint8_t rec); -void setDisplayPWM(uint32_t bright); -void write_rtc(void); -void displayOff(void); -void button1pressed(void); -void button2pressed(void); -void buttonsBothHeld(void); -void setPrecision(void); -void sendDate( _Bool now ); -void generateDACbuffer(uint16_t * buf); -void PPS(void); -void PPS_NoUpdate(void); -void PPS_Countdown(void); -void rxConfigString(char c); -void monitor_vbus(void); - -#define latchSegments() \ - buffer_c[0].low = next7seg.c; \ - buffer_b[0] = next7seg.b[0]; \ - buffer_b[1] = next7seg.b[1]; \ - buffer_b[2] = next7seg.b[2]; \ - buffer_b[3] = next7seg.b[3]; \ - buffer_b[4] = next7seg.b[4]; - -#define triggerPendSV() \ - SCB->ICSR = SCB_ICSR_PENDSVSET_Msk; - -#define sendLatch() \ - huart2.Instance->TDR = 0xFE; - -#define loadNextTimestamp() \ - latchSegments() \ - sendLatch() \ - waitingForLatch=0;\ - triggerPendSV() - -extern uint32_t __VECTORS_FLASH[]; -extern uint32_t __VECTORS_RAM[]; -#define SetSysTick(x) __VECTORS_RAM[ 16 + SysTick_IRQn ] = (uint32_t)x -#define SetPPS(x) __VECTORS_RAM[ 16 + EXTI9_5_IRQn ] = (uint32_t)x - -#define SetVector(x,y) __VECTORS_RAM[ 16 + x ] = (uint32_t)y -#define GetVector(x) ((void (*)(void)) __VECTORS_RAM[ 16 + x ] - -/* USER CODE END EFP */ - -/* Private defines -----------------------------------------------------------*/ -/* USER CODE BEGIN Private defines */ - -/* USER CODE END Private defines */ - -#ifdef __cplusplus -} -#endif - -#endif /* __MAIN_H */ - -/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/ + +#define CMD_LOAD_TEXT 0x90 +#define CMD_SET_FREQUENCY 0x91 +#define CMD_RELOAD_TEXT 0x92 +#define CMD_SHOW_CRC 0x9D + +//#define NONCOMPLIANT_DATE_MODES + +enum { + MODE_ISO8601_STD =0, + MODE_ISO_ORDINAL, + MODE_ISO_WEEK, + MODE_UNIX, + MODE_JULIAN_DATE, + MODE_MODIFIED_JD, + MODE_SHOW_OFFSET, + MODE_SHOW_TZ_NAME, + MODE_WEEKDAY, + MODE_WEEKDA_DD, + MODE_WDY_MM_DD, + MODE_STANDBY, + MODE_COUNTDOWN, + MODE_SATVIEW, + MODE_DEBUG_BRIGHTNESS, + MODE_DEBUG_RTC, + MODE_TEXT, + MODE_FIRMWARE_CRC_T, + MODE_FIRMWARE_CRC_D, + MODE_VBAT, + MODE_DISPLAYTEST, + MODE_TTFF, +#ifdef NONCOMPLIANT_DATE_MODES + MODE_DDMMYYYY, +#endif + + // Astro pack — GPS-derived astronomy read-outs. SATVIEW-style: the payload + // shows on the 10-char date row while the live clock keeps running on the + // time row. Enabled individually via the MODE_* config keys, like any mode. + MODE_SUN, // sunrise / sunset / solar noon (local), auto-paged + MODE_SUN_AZEL, // sun azimuth & elevation, now + MODE_MOON, // moon phase index + illuminated % + MODE_GRID, // Maidenhead grid locator + MODE_LATLON, // latitude / longitude, auto-paged + + // Alternate-timebase TIME-ROW modes: the big digits tick Local Sidereal Time or + // apparent solar time, reseeded from the GPS-disciplined second; the + // date row keeps the civil date and a dedicated colon animation marks the mode. + MODE_LST, + MODE_SOLAR, + + // Temperature-compensation diagnostics: die temp / model offsets / sample count + // paged on the date row (satview pattern). Values come from the tempcomp module. + MODE_TEMPCOMP, + + NUM_DISPLAY_MODES +}; + +enum { + COUNT_NORMAL =0, + COUNT_HIDDEN, + COUNT_DOWN, + COUNT_ALT // time row driven by the alternate timebase (MODE_LST / MODE_SOLAR) +}; + +enum { + COLON_MODE_SLOWFADE = 0, + COLON_MODE_HEARTBEAT, + COLON_MODE_1PPS_SAWTOOTH, + COLON_MODE_ALT_SAWTOOTH, + COLON_MODE_TOGGLE, + COLON_MODE_SOLID +}; + +enum { + NMEA_ALL=0, + NMEA_RMC, + NMEA_NONE +}; + +enum { + RULES_OK=0, + RULES_STR_ERR, + RULES_NO_FILE, + RULES_HEADER_ERR, + RULES_VERSION_UNKNOWN, + RULES_CATEGORY_UNKNOWN, + RULES_ZONE_UNKNOWN +}; + +enum { + SV_GPS_L1=0, + SV_GPS_UNKNOWN, + SV_GLONASS_L1, + SV_GLONASS_UNKNOWN, + SV_GALILEO_E1, + SV_GALILEO_UNKNOWN, + SV_BEIDOU_B1, + SV_BEIDOU_UNKNOWN, + SV_COUNT +}; +/* USER CODE END EC */ + +/* Exported macro ------------------------------------------------------------*/ +/* USER CODE BEGIN EM */ + +#define byteswap32(x) \ + ( ((x & 0xff000000) >> 24) | ((x & 0x00ff0000) >> 8) \ + | ((x & 0x0000ff00) << 8) | ((x & 0x000000ff) << 24)) + +/* USER CODE END EM */ + +void HAL_TIM_MspPostInit(TIM_HandleTypeDef *htim); + +/* Exported functions prototypes ---------------------------------------------*/ +void Error_Handler(void); + +/* USER CODE BEGIN EFP */ +void decodeRMC(void); +void decodeGSV(uint8_t rec); +void setDisplayPWM(uint32_t bright); +void write_rtc(void); +void displayOff(void); +void button1pressed(void); +void button2pressed(void); +void buttonsBothHeld(void); +void setPrecision(void); +void sendDate( _Bool now ); +void generateDACbuffer(uint16_t * buf); +void PPS(void); +void PPS_NoUpdate(void); +void PPS_Countdown(void); +void rxConfigString(char c); +void monitor_vbus(void); + +#define latchSegments() \ + buffer_c[0].low = next7seg.c; \ + buffer_b[0] = next7seg.b[0]; \ + buffer_b[1] = next7seg.b[1]; \ + buffer_b[2] = next7seg.b[2]; \ + buffer_b[3] = next7seg.b[3]; \ + buffer_b[4] = next7seg.b[4]; + +#define triggerPendSV() \ + SCB->ICSR = SCB_ICSR_PENDSVSET_Msk; + +#define sendLatch() \ + huart2.Instance->TDR = 0xFE; + +#define loadNextTimestamp() \ + latchSegments() \ + sendLatch() \ + waitingForLatch=0;\ + triggerPendSV() + +extern uint32_t __VECTORS_FLASH[]; +extern uint32_t __VECTORS_RAM[]; +#define SetSysTick(x) __VECTORS_RAM[ 16 + SysTick_IRQn ] = (uint32_t)x +#define SetPPS(x) __VECTORS_RAM[ 16 + EXTI9_5_IRQn ] = (uint32_t)x + +#define SetVector(x,y) __VECTORS_RAM[ 16 + x ] = (uint32_t)y +#define GetVector(x) ((void (*)(void)) __VECTORS_RAM[ 16 + x ] + +/* USER CODE END EFP */ + +/* Private defines -----------------------------------------------------------*/ +/* USER CODE BEGIN Private defines */ + +/* USER CODE END Private defines */ + +#ifdef __cplusplus +} +#endif + +#endif /* __MAIN_H */ + +/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/ diff --git a/mk4-time/Core/Src/main.c b/mk4-time/Core/Src/main.c index c4f7ff7..cea7e57 100644 --- a/mk4-time/Core/Src/main.c +++ b/mk4-time/Core/Src/main.c @@ -240,6 +240,60 @@ volatile uint32_t pps_sof_dwt = 0; // DWT->CYCCNT at the most recent SOF volatile uint16_t pps_sof_frame = 0; // USB 11-bit frame number at that SOF (matches host frame mod 2048) volatile uint8_t pps_sof_valid = 0; // 1 once a real SOF anchor has been latched; 0 = emit 9-field only +// --- Temperature compensation (opt-in) ------------------------------------------------------ +// Learns ppm-vs-die-temperature for both oscillators while GPS-locked (tc_learn), then during +// GPS-loss holdover steers the SysTick timebase from the HSE model (tc_apply) and optionally +// trims RTC->CALR from the LSE model (tc_rtc) so the battery RTC hands over better time across +// a power loss. "tc_dump = on" over serial prints the learned coefficients as ready-to-paste +// config lines; non-NAN tc_hse_a/tc_lse_a in config freeze the model (config overrides learning). +// All defaults off: with none of the keys set, behaviour is identical to stock. +// Config-key scalars are written from the USB OTG ISR (parseConfigString) and read by the +// main loop: volatile, matching the pps_ts_enabled precedent. +volatile _Bool tc_learn = 0, tc_apply = 0, tc_rtc = 0; +volatile int16_t tc_t0 = 40; // model centre temperature (°C) +volatile uint16_t tc_engage_s = 2; // seconds of PPS absence before steering engages (min 2) +volatile uint16_t tc_max_ppm = 100; // hard clamp on the applied correction magnitude +// Frozen coefficients (ppm units at tc_t0). Elements are single-word (atomic) reads/writes; +// consumers snapshot each element once. HSE has NO 'a': its learned origin is arbitrary and +// steering uses temperature differences only, so freezing needs just b (and optionally c). +float tc_cfg_hse[3] = {NAN, NAN, NAN}; // [0] unused, [1] ppm/°C, [2] ppm/°C² +float tc_cfg_lse[3] = {NAN, NAN, NAN}; // absolute: ppm, ppm/°C, ppm/°C² +volatile _Bool tc_dump_pending = 0; // set by the serial parser, serviced in the main loop +volatile _Bool tc_reset_pending = 0; + +// Validated coefficient parse: garbage/'----'/empty leaves the value untouched (a pasted-back +// commented dump line must not freeze 0.0); an explicit "nan" parses and UNFREEZES the slot. +static void tc_parse_coeff(const char *v, float *out){ + char *end; + float f = strtof(v, &end); + if (end != v) *out = f; +} + +// Steering handoff, governor (main loop) -> tick ISR. base/rem are written together under +// IRQ-off; the ISR Bresenham distributes `rem` one-tick-longer periods per 1000 ms so the +// average period is (tc_load_base+1) + rem/1000 ticks — fractional-ppm rate steering. +volatile uint8_t tc_steer_on = 0; +volatile int32_t tc_load_base = 0; // SysTick->LOAD for the shorter of the two periods +volatile int32_t tc_rem = 0; // extra-tick remainder, always in [0,1000) +volatile int32_t tc_acc = 0; // Bresenham accumulator (ISR-owned) + +// Learned state (main-loop only). 2 °C bins spanning die temp -8..71 °C; sums are bounded by +// the halving-at-32768 aging rule (max |sum| ~ 6400*32768 < 2^31), so int32 cannot overflow. +struct tc_bin { int32_t hse_sum, lse_sum; uint16_t hse_n, lse_n; }; +struct tc_bin tc_bins[40]; +float tc_hse_m[3], tc_lse_m[3]; // learned models (ppm at powers of T - tc_t0) +_Bool tc_hse_valid = 0, tc_lse_valid = 0; +int16_t tc_hse_tmin = 0, tc_hse_tmax = 0; // learned coverage: model is clamped to this range +int16_t tc_lse_tmin = 0, tc_lse_tmax = 0; +uint32_t tc_n_hse = 0, tc_n_lse = 0; // lifetime sample counts (display + dump) + +// Display cache for MODE_TEMPCOMP. Written by the governor (main loop); read by sendDate, +// which ALSO runs from the SysTick ISRs — each field is a single 32-bit (atomic) access, so +// the worst case is a one-repaint-stale value pairing, never a torn read. +float tc_disp_hse = 0, tc_disp_lse = 0; +_Bool tc_disp_hse_ok = 0, tc_disp_lse_ok = 0; +char tc_disp_state = '-'; // A applying · F frozen (config) · L learning · - idle + #define CHECK_CONFIG_MTIME struct { @@ -496,6 +550,31 @@ void sendDate( _Bool now ){ } } break; + case MODE_TEMPCOMP: { + // Pages: die temp -> HSE model -> LSE model -> samples+state, astro_page_ms dwell each. + // Values are the governor's display cache (clamped so the row never overflows). Layout is + // the RISE/SET style: label, separator space, a sign slot (space when positive), then the + // digits — numbers align whether signed or not, and short values keep clear space at the + // row's end beside the time row: "tC 32C" / "HSE -0.25" / "rtC 18.68" / "n 159 L". + int tcp = (int)((uwTick / 5500) % 4); + char num[12]; + if (tcp == 0) { + int t2 = (int)die_temp_c; + i = sprintf((char*)&uart2_tx_buffer[1], "tC %c%dC", t2 < 0 ? '-' : ' ', t2 < 0 ? -t2 : t2); + } else if (tcp == 1 || tcp == 2) { + _Bool ok = (tcp == 1) ? tc_disp_hse_ok : tc_disp_lse_ok; + float v = (tcp == 1) ? tc_disp_hse : tc_disp_lse; + if (!ok) i = sprintf((char*)&uart2_tx_buffer[1], "%s ----", (tcp == 1) ? "HSE" : "rtC"); + else { + sprintf(num, "%.2f", (double)(v < 0 ? -v : v)); + i = sprintf((char*)&uart2_tx_buffer[1], "%s %c%s", (tcp == 1) ? "HSE" : "rtC", v < 0 ? '-' : ' ', num); + } + } else { + unsigned long ns = tc_n_hse > 999999UL ? 999999UL : tc_n_hse; + i = sprintf((char*)&uart2_tx_buffer[1], "n%6lu %c", ns, tc_disp_state); + } + break; + } case MODE_STANDBY: return; case MODE_COUNTDOWN: @@ -1222,7 +1301,7 @@ void applyColonForMode(void){ } } -void parseConfigString(char *key, char *value) { +void parseConfigString(char *key, char *value, _Bool from_serial) { if (strcasecmp(key, "text") == 0) { @@ -1352,6 +1431,34 @@ void parseConfigString(char *key, char *value) { pps_ts_enabled = truthy(value); // emit a $PMTXTS timing sentence on each PPS edge + } else if (strcasecmp(key, "MODE_TEMPCOMP") == 0) { + set_mode_enabled(MODE_TEMPCOMP, value); + } else if (strcasecmp(key, "tc_learn") == 0) { + tc_learn = truthy(value); // accumulate (die temp, ppm) samples while GPS-locked + } else if (strcasecmp(key, "tc_apply") == 0) { + tc_apply = truthy(value); // steer the SysTick timebase during GPS-loss holdover + } else if (strcasecmp(key, "tc_rtc") == 0) { + tc_rtc = truthy(value); // additionally trim RTC->CALR while GPS is absent + } else if (strcasecmp(key, "tc_t0") == 0) { + int v = atoi(value); tc_t0 = v < -30 ? -30 : (v > 80 ? 80 : v); + } else if (strcasecmp(key, "tc_engage_s") == 0) { + // Floor of 2: currentTime pre-increments at the modelled .900 mark, so "fresh" reads 1 + // for the last 100 ms of every LOCKED second — a floor of 1 would engage during lock. + int v = atoi(value); tc_engage_s = v < 2 ? 2 : (v > 3600 ? 3600 : v); + } else if (strcasecmp(key, "tc_max_ppm") == 0) { + int v = atoi(value); tc_max_ppm = v < 1 ? 1 : (v > 200 ? 200 : v); + } else if (strcasecmp(key, "tc_hse_b") == 0) { tc_parse_coeff(value, &tc_cfg_hse[1]); + } else if (strcasecmp(key, "tc_hse_c") == 0) { tc_parse_coeff(value, &tc_cfg_hse[2]); + } else if (strcasecmp(key, "tc_lse_a") == 0) { tc_parse_coeff(value, &tc_cfg_lse[0]); + } else if (strcasecmp(key, "tc_lse_b") == 0) { tc_parse_coeff(value, &tc_cfg_lse[1]); + } else if (strcasecmp(key, "tc_lse_c") == 0) { tc_parse_coeff(value, &tc_cfg_lse[2]); + } else if (strcasecmp(key, "tc_dump") == 0) { + // Serial-only trigger: print the learned model as paste-ready config lines. A stray + // tc_dump left in config.txt must not fire on every (re)load, hence the origin guard. + if (from_serial && truthy(value)) tc_dump_pending = 1; + } else if (strcasecmp(key, "tc_reset") == 0) { + if (from_serial && truthy(value)) tc_reset_pending = 1; // serial-only, same guard + } else if (key[0]=='B' && key[1]=='S' && key[3]==0) { //BS1, BS2, etc if (!key[2] || key[2]<'1' || key[2]>'0'+sizeof(brightnessCurve)/sizeof(brightnessCurve[0])) return; @@ -1425,7 +1532,7 @@ void rxConfigString(char c){ NVIC_SystemReset(); } if (k && (v || state>=2)) { - parseConfigString(key, value); + parseConfigString(key, value, 1); // serial origin: tc_dump/tc_reset may fire // rxConfigString runs in the USB OTG ISR; postConfigCleanup() calls nextMode() // and sendDate(), which are non-reentrant against the SysTick repaint. Defer it // to the main loop so it runs in thread context, like the file-config path does. @@ -1523,7 +1630,7 @@ void readConfigFile(void){ value[col]=0; col=0; - parseConfigString(key, value); + parseConfigString(key, value, 0); // file origin: serial-only triggers inert } } @@ -1763,7 +1870,459 @@ static uint8_t emitPPSTimestamp(void){ return r; } +// ==================== Temperature compensation (opt-in; state near pps_cap) ==================== +// Everything below runs in the MAIN LOOP only (float allowed, calibrateRTC precedent). The tick +// ISRs see just three precomputed int32s via the tc_steer_* handoff in the timetick() hook. + +static uint32_t tc_nom_load = 0; // SysTick->LOAD captured before any steering (80 MHz: 79999) + +// Ticks per ppm, derived from the captured nominal period so no core-clock assumption is baked +// in: one second is (LOAD+1)*1000 ticks, so 1 ppm = (LOAD+1)/1000 ticks (80 at 80 MHz). +// Verified against the live unit: $PMTXTS reports load=79999 (10 MHz TCXO -> PLL -> 80 MHz). +static int32_t tc_tpp(void){ return (int32_t)((tc_nom_load + 1) / 1000); } + +static int tc_bin_i(int t){ int i = (t + 8) / 2; return i < 0 ? 0 : (i > 39 ? 39 : i); } + +// One HSE sample per GPS-locked second. The PPS ISRs re-zero the ms cascade at every edge +// (SysTick->VAL reload + counter reset), so each capture is already a SELF-CONTAINED one-second +// accumulation: pos = const + tpp·ppm(T), where const is a fixed capture/reload offset and +// tpp = ticks per ppm. Measured on the live unit: pos = 79925.8 ± 0.67 ticks (~8 ns RMS) — the +// constant dominates and is unknowable from lock data alone, so the model is learned in ticks +// with an ARBITRARY ORIGIN, rebased to the first accepted sample to keep bin sums small. Its +// differences over temperature are exact, and holdover steering only ever applies +// model(T_now) − model(T_at_loss), from which the origin cancels. (An earlier draft differenced +// consecutive captures — but the per-edge cascade reset makes that identically ~0; verified on +// hardware: dpos = 0.05 ± 1.1 ticks.) +static int32_t tc_e0 = 0; // origin rebase: first accepted sample +static _Bool tc_e0_set = 0; +static int32_t tc_ema = 0; // slow tracker for the glitch gate +static void tc_hse_learn(void){ + static uint32_t last_seq = 0; + static uint8_t warm = 0; + + __disable_irq(); // tear-free copy (emitPPSTimestamp pattern) + uint32_t seq = pps_cap.seq; + uint16_t subms = pps_cap.subms; + uint32_t st = pps_cap.systick; + int16_t temp = pps_cap.temp; + uint8_t flags = pps_cap.flags; + __enable_irq(); + + if (seq == last_seq) return; // no new edge since last pass + _Bool contiguous = (seq == last_seq + 1); + last_seq = seq; + + if ((flags & 0x3) != 0x3 || subms > 999) { warm = 0; return; } + if (!contiguous) { warm = 0; return; } // edges were missed: settle again + if (warm < 10) { warm++; return; } // settle after (re)acquisition + + int32_t half = (int32_t)(tc_nom_load + 1) * 500; // half a second in ticks + int32_t e = (int32_t)subms * (int32_t)(tc_nom_load + 1) + + (int32_t)tc_nom_load - (int32_t)st; // this second's accumulation (+ const) + if (e > half) e -= 2 * half; // fold the origin into ±half a second + if (e < -half) e += 2 * half; + + int32_t tpp = tc_tpp(); // ticks per ppm (80 at 80 MHz) + if (!tc_e0_set) { tc_e0 = e; tc_ema = 0; tc_e0_set = 1; } + e -= tc_e0; // arbitrary-origin rebase (keeps sums int32-safe) + if (e - tc_ema > 100 * tpp || e - tc_ema < -100 * tpp) return; // >100 ppm step: glitch + if (e > 30000 || e < -30000) return; // hard cap so 32768·|e| can never overflow int32 + tc_ema += (e - tc_ema) / 16; + + struct tc_bin *b = &tc_bins[tc_bin_i(temp)]; + if (b->hse_n >= 8) { // per-bin outlier gate: 10 ppm off the mean + int32_t d = e - b->hse_sum / (int32_t)b->hse_n; + if (d > 10 * tpp || d < -10 * tpp) return; + } + if (b->hse_n >= 32768) { b->hse_sum /= 2; b->hse_n /= 2; } // overflow-proof aging + b->hse_sum += e; b->hse_n++; + tc_n_hse++; +} + +// One LSE sample per successful RTC calibration: calibrateRTC only advances the BKP31R stamp +// on an in-range 63 s measurement, so watching the stamp inherits its validity gate for free. +static void tc_lse_learn(void){ + static uint32_t seen = 0; + uint32_t cal = rtc_last_calibration; + if (cal == seen) return; + _Bool first = (seen == 0); + seen = cal; + if (first) return; // boot-time stamp, not a fresh measurement + int32_t v = debug_rtc_val; // raw LSE cycle error over CAL_PERIOD (63 s) + if (v > 1000 || v < -1000) return; + struct tc_bin *b = &tc_bins[tc_bin_i(die_temp_c)]; + if (b->lse_n >= 32768) { b->lse_sum /= 2; b->lse_n /= 2; } + b->lse_sum += v; b->lse_n++; + tc_n_lse++; +} + +// Solve A·x = y for a 3x3 symmetric system by Gaussian elimination with partial pivoting. +static _Bool tc_gauss3(float A[3][3], float y[3], float x[3]){ + int p[3] = {0, 1, 2}; + for (int c = 0; c < 3; c++){ + int best = c; + for (int r = c + 1; r < 3; r++) + if (fabsf(A[p[r]][c]) > fabsf(A[p[best]][c])) best = r; + int t = p[c]; p[c] = p[best]; p[best] = t; + if (fabsf(A[p[c]][c]) < 1e-9f) return 0; + for (int r = c + 1; r < 3; r++){ + float f = A[p[r]][c] / A[p[c]][c]; + for (int k = c; k < 3; k++) A[p[r]][k] -= f * A[p[c]][k]; + y[p[r]] -= f * y[p[c]]; + } + } + for (int c = 2; c >= 0; c--){ + float s = y[p[c]]; + for (int k = c + 1; k < 3; k++) s -= A[p[c]][k] * x[k]; + x[c] = s / A[p[c]][c]; + } + return 1; +} + +// Weighted least-squares fit of y(T) = a + b·x + c·x², x = T - tc_t0, over bin means. +// Falls back quadratic -> linear -> constant as temperature coverage thins. `scale` converts +// bin units (HSE: 1.0 — model stays in ticks, arbitrary origin; LSE: raw 63 s cal cycles → ppm). +// A fit is only accepted if every coefficient is finite: a near-singular system can pass the +// pivot threshold yet overflow to Inf/NaN, and NaN must never reach the steering or display. +static _Bool tc_fin3(const float m[3]){ return isfinite(m[0]) && isfinite(m[1]) && isfinite(m[2]); } + +static _Bool tc_fit_one(_Bool lse, float scale, uint16_t n_quad, float m[3], + int16_t *tmin_out, int16_t *tmax_out){ + float S[5] = {0,0,0,0,0}, T[3] = {0,0,0}; + float S0a = 0, T0a = 0; // all-samples weighted mean (constant fallback) + int nb = 0, tmin = 127, tmax = -128; + int tmin_a = 127, tmax_a = -128; + + for (int i = 0; i < 40; i++){ + uint16_t n = lse ? tc_bins[i].lse_n : tc_bins[i].hse_n; + if (!n) continue; + int32_t sum = lse ? tc_bins[i].lse_sum : tc_bins[i].hse_sum; + int t = i * 2 - 8; // bin low edge; bin holds {t, t+1} + float y = ((float)sum / (float)n) * scale; + float w = (float)n; + S0a += w; T0a += w * y; + if (t < tmin_a) tmin_a = t; + if (t > tmax_a) tmax_a = t; + if (n < n_quad) continue; // curve terms only from well-filled bins + float x = ((float)t + 0.5f) - (float)tc_t0; // true bin centre: t + 0.5 + nb++; + if (t < tmin) tmin = t; + if (t > tmax) tmax = t; + S[0] += w; S[1] += w*x; S[2] += w*x*x; + S[3] += w*x*x*x; S[4] += w*x*x*x*x; + T[0] += w*y; T[1] += w*x*y; T[2] += w*x*x*y; + } + + if (nb >= 3 && (tmax - tmin) >= 6) { // quadratic + float A[3][3] = {{S[0],S[1],S[2]},{S[1],S[2],S[3]},{S[2],S[3],S[4]}}; + float yv[3] = {T[0],T[1],T[2]}; + if (tc_gauss3(A, yv, m) && tc_fin3(m)) { *tmin_out = tmin; *tmax_out = tmax; return 1; } + } + if (nb >= 2 && (tmax - tmin) >= 4) { // linear + float det = S[0]*S[2] - S[1]*S[1]; + if (fabsf(det) > 1e-9f){ + m[0] = (T[0]*S[2] - T[1]*S[1]) / det; + m[1] = (S[0]*T[1] - S[1]*T[0]) / det; + m[2] = 0; + if (tc_fin3(m)) { *tmin_out = tmin; *tmax_out = tmax; return 1; } + } + } + if (S0a >= (lse ? 8.0f : 60.0f)) { // constant: the dominant fixed offset + m[0] = T0a / S0a; m[1] = 0; m[2] = 0; + if (tc_fin3(m)) { *tmin_out = tmin_a; *tmax_out = tmax_a; return 1; } + } + return 0; +} + +static void tc_fit(void){ + tc_hse_valid = tc_fit_one(0, 1.0f, 64, tc_hse_m, &tc_hse_tmin, &tc_hse_tmax); + tc_lse_valid = tc_fit_one(1, 1e6f/(32768.0f*63.0f), 4, tc_lse_m, &tc_lse_tmin, &tc_lse_tmax); +} + +static float tc_poly(const float m[3], float x){ return m[0] + m[1]*x + m[2]*x*x; } + +// LSE model (absolute ppm): non-NAN config a freezes it (the user asserted the values); +// otherwise the learned fit, clamped to its observed temperature range (no extrapolation). +// Config values are USB-ISR-written; snapshot each element once (single-word reads are atomic). +static _Bool tc_model_lse(int t, float *ppm){ + float a = tc_cfg_lse[0], b = tc_cfg_lse[1], c = tc_cfg_lse[2]; + if (!isnan(a)) { + if (isnan(b)) b = 0; + if (isnan(c)) c = 0; + float x = (float)t - (float)tc_t0; + *ppm = a + b*x + c*x*x; + return isfinite(*ppm); + } + if (!tc_lse_valid) return 0; + if (t < tc_lse_tmin) t = tc_lse_tmin; + if (t > tc_lse_tmax) t = tc_lse_tmax; + *ppm = tc_poly(tc_lse_m, (float)t - (float)tc_t0); + return 1; +} + +// HSE steering delta in TICKS between two temperatures. The learned model's origin is +// arbitrary (see tc_hse_learn), so only differences are meaningful — which is exactly what +// holdover needs: at GPS loss the display is phase-true, and the error that then accrues is +// the temperature-driven CHANGE of the oscillator, model(T_now) − model(T_loss). Frozen config +// coefficients are in ppm; a cancels in the difference, so only tc_hse_b/c are required. +static _Bool tc_hse_delta(int t_now, int t_ref, int32_t *dticks){ + float b = tc_cfg_hse[1], c = tc_cfg_hse[2]; + if (!isnan(b)) { // frozen: b (and optionally c) from config + if (isnan(c)) c = 0; + float x1 = (float)t_now - (float)tc_t0, x0 = (float)t_ref - (float)tc_t0; + float dppm = (b*x1 + c*x1*x1) - (b*x0 + c*x0*x0); + if (!isfinite(dppm)) return 0; + *dticks = (int32_t)lroundf(dppm * (float)tc_tpp()); + return 1; + } + if (!tc_hse_valid) return 0; + if (t_now < tc_hse_tmin) t_now = tc_hse_tmin; // no extrapolation past learned coverage + if (t_now > tc_hse_tmax) t_now = tc_hse_tmax; + if (t_ref < tc_hse_tmin) t_ref = tc_hse_tmin; + if (t_ref > tc_hse_tmax) t_ref = tc_hse_tmax; + float d = tc_poly(tc_hse_m, (float)t_now - (float)tc_t0) + - tc_poly(tc_hse_m, (float)t_ref - (float)tc_t0); + if (!isfinite(d)) return 0; + *dticks = (int32_t)lroundf(d); // learned model is already in ticks + return 1; +} + +// Once-per-second control: evaluate the models at the current die temperature, refresh the +// display cache, engage/disengage SysTick steering, and (optionally) trim RTC->CALR. +static void tc_governor(void){ + static uint32_t last_run = 0; + static int32_t applied_E = 0; // ticks/second currently steered + static _Bool was_on = 0; + static int16_t t_loss = 0; // die temp captured when steering engaged + static int32_t last_steps = 0x7FFF; // last CALR trim written (sentinel: none) + static uint32_t last_calr = 0; + + // Snapshot the two ISR-written time variables together: currentTime increments at the + // modelled .900 mark while last_pps_time updates at the edge, and reading them separately + // can interleave with both ISRs and yield a wrapped-huge "fresh" that spuriously engages. + __disable_irq(); + uint32_t now = (uint32_t)currentTime; + uint32_t lpps = last_pps_time; + __enable_irq(); + if (now == last_run) return; + last_run = now; + + uint32_t fresh = now - lpps; // seconds since the last PPS edge + if (fresh > 0x80000000u) fresh = 0; // interleaved-read underflow: treat as fresh + + int t = die_temp_c; + float lp = 0; + _Bool have_l = tc_model_lse(t, &lp); + int32_t tpp = tc_tpp(); + + // Would-be steering delta at the current temperatures (also feeds the display) + int32_t dt_now = 0; + _Bool have_h = tc_hse_delta(t, was_on ? t_loss : t, &dt_now); + + // display cache (clamped so "HSE -99.99" never exceeds the 10-char row). + // HSE page shows the ACTIVE steering correction in ppm (0.00 while locked — the PPS + // discipline owns the phase then); LSE page shows the absolute model ppm. + float dh = was_on ? (float)applied_E / (float)tpp : 0.0f; + float dl = lp; + if (dh > 99.99f) dh = 99.99f; + if (dh < -99.99f) dh = -99.99f; + if (dl > 99.99f) dl = 99.99f; + if (dl < -99.99f) dl = -99.99f; + tc_disp_hse = dh; tc_disp_hse_ok = have_h; + tc_disp_lse = dl; tc_disp_lse_ok = have_l; + tc_disp_state = tc_steer_on ? 'A' + : (!isnan(tc_cfg_hse[1]) || !isnan(tc_cfg_lse[0])) ? 'F' + : (tc_learn && fresh < 5) ? 'L' : '-'; + + // --- HSE steering: engage only in holdover, after first-ever fix, with a usable model. + // The correction is the temperature-driven CHANGE since GPS loss (origin cancels; see + // tc_hse_delta). At the loss instant the delta is 0 by construction and grows only as the + // die temperature moves, so engage is glitch-free and re-lock needs no unwinding beyond + // the LOAD restore (the per-edge phase snap owns lock). + if (tc_apply && had_pps && fresh >= tc_engage_s) { + if (!was_on) t_loss = (int16_t)t; // remember the temperature we lost GPS at + int32_t target = 0; + if (tc_hse_delta(t, t_loss, &target)) { + int32_t lim = (int32_t)tc_max_ppm * tpp; + if (target > lim) target = lim; + if (target < -lim) target = -lim; + if (!was_on) applied_E = target; // 0 at engage by construction... + else { // ...then gentle slew (temp-quantisation steps) + int32_t slew = tpp / 4; // 0.25 ppm per second + int32_t d = target - applied_E; + if (d > slew) d = slew; + if (d < -slew) d = -slew; + applied_E += d; + } + int32_t base = applied_E >= 0 ? applied_E / 1000 : -((-applied_E + 999) / 1000); + int32_t rem = applied_E - base * 1000; // floor-division remainder, always [0,1000) + __disable_irq(); + tc_load_base = (int32_t)tc_nom_load + base; + tc_rem = rem; + tc_steer_on = 1; + __enable_irq(); + was_on = 1; + } else if (was_on) { // model became unusable mid-holdover + tc_steer_on = 0; + SysTick->LOAD = tc_nom_load; + tc_acc = 0; + applied_E = 0; + was_on = 0; + } + } else if (was_on) { + tc_steer_on = 0; // flag first: the ISR stops writing LOAD... + SysTick->LOAD = tc_nom_load; // ...then restore the nominal period + tc_acc = 0; + applied_E = 0; + was_on = 0; + } + + // --- LSE -> RTC->CALR trim: power-loss insurance only (display time is HSE-driven) --- + // calibrateRTC() owns CALR while locked (it runs from the PPS ISRs, which are silent now); + // the first successful calibration after re-lock re-measures and overwrites this trim. + // While PPS is fresh, forget our last write: calibrateRTC has since replaced CALR, so an + // equal-valued model trim in the NEXT outage must not be skipped by the != guard. + if (fresh <= 63) last_steps = 0x7FFF; + if (tc_rtc && have_l && fresh > 63 && now - last_calr >= 60) { + int32_t steps = (int32_t)lroundf(lp * (1048576.0f / 1000000.0f)); // ppm -> CALM steps + if (steps > 255) steps = 255; + if (steps < -255) steps = -255; + if (steps != last_steps && !(RTC->ISR & RTC_ISR_RECALPF)) { + // IRQ-off around the WPR unlock/write/relock triplet: PendSV's write_rtc() (runs each + // second in holdover) does its own WPR sequence, and a preemption between our key + // writes and the CALR store would leave the store silently ignored. + __disable_irq(); + __HAL_RTC_WRITEPROTECTION_DISABLE(&hrtc); + RTC->CALR = 0x100 + steps; // same midpoint convention as calibrateRTC + __HAL_RTC_WRITEPROTECTION_ENABLE(&hrtc); + __enable_irq(); + last_steps = steps; + last_calr = now; // note: BKP31R deliberately NOT updated + } + } +} + +// "tc_dump = on" over serial: emit the learned model as ready-to-paste config.txt lines plus +// two checksummed $PMTXTC sentences (H and L — split so each fits NMEA_BUF_SIZE). One line per +// main-loop pass; each line is FORMATTED ONCE and only the CDC submit is retried on BUSY (float +// snprintf must not re-run thousands of times against the ISR's own float sprintf — newlib-nano +// shares one _reent). A stuck host aborts the dump after a bounded number of BUSY passes. +// HSE coefficients are printed in ppm/°C (per-degree slope b and curvature c, converted from +// the tick-domain model); the HSE 'a' term has an arbitrary instrument origin and is neither +// printed nor needed — steering uses temperature DIFFERENCES only (see tc_hse_delta). +static void tc_dump_step(void){ + static uint8_t idx = 0; + static int dn = -1; // formatted length; -1 = line not built yet + static uint16_t busy_ct = 0; + static char dline[NMEA_BUF_SIZE]; + if (!tc_dump_pending) return; + if (hUsbDeviceFS.dev_state != USBD_STATE_CONFIGURED) { tc_dump_pending = 0; idx = 0; dn = -1; return; } + + if (dn < 0) { // build the current line exactly once + float tpp = (float)tc_tpp(); + int n = 0; + switch (idx) { + case 0: + n = snprintf(dline, sizeof dline, "# tempcomp: hse n=%lu lse n=%lu, die %d..%d C, state %c\r\n", + (unsigned long)tc_n_hse, (unsigned long)tc_n_lse, + (int)tc_hse_tmin, (int)tc_hse_tmax, tc_disp_state); + break; + case 1: n = snprintf(dline, sizeof dline, "tc_t0 = %d\r\n", (int)tc_t0); break; + case 2: // HSE slope, ppm/degC (origin-free) + if (tc_hse_valid) n = snprintf(dline, sizeof dline, "tc_hse_b = %.5f\r\n", (double)(tc_hse_m[1] / tpp)); + else n = snprintf(dline, sizeof dline, "# tc_hse_b = ----\r\n"); + break; + case 3: // HSE curvature, ppm/degC^2 + if (tc_hse_valid) n = snprintf(dline, sizeof dline, "tc_hse_c = %.6f\r\n", (double)(tc_hse_m[2] / tpp)); + else n = snprintf(dline, sizeof dline, "# tc_hse_c = ----\r\n"); + break; + case 4: case 5: case 6: { // LSE a/b/c, absolute ppm at tc_t0 + static const char nm[3] = {'a','b','c'}; + static const char *fm[3] = {"tc_lse_%c = %.4f\r\n", "tc_lse_%c = %.5f\r\n", "tc_lse_%c = %.6f\r\n"}; + int k = idx - 4; + if (tc_lse_valid) n = snprintf(dline, sizeof dline, fm[k], nm[k], (double)tc_lse_m[k]); + else n = snprintf(dline, sizeof dline, "# tc_lse_%c = ----\r\n", nm[k]); + break; + } + case 7: case 8: { // machine-parsable pair for the web app + char body[72]; + int nb; + if (idx == 7) + nb = snprintf(body, sizeof body, "PMTXTC,H,%lu,%d,%d,%.5f,%.6f,%c", + (unsigned long)tc_n_hse, (int)tc_hse_tmin, (int)tc_hse_tmax, + (double)(tc_hse_valid ? tc_hse_m[1] / tpp : 0), + (double)(tc_hse_valid ? tc_hse_m[2] / tpp : 0), tc_hse_valid ? 'V' : '-'); + else + nb = snprintf(body, sizeof body, "PMTXTC,L,%lu,%.4f,%.5f,%.6f,%c", + (unsigned long)tc_n_lse, + (double)(tc_lse_valid ? tc_lse_m[0] : 0), (double)(tc_lse_valid ? tc_lse_m[1] : 0), + (double)(tc_lse_valid ? tc_lse_m[2] : 0), tc_lse_valid ? 'V' : '-'); + if (nb < 0 || nb >= (int)sizeof body) { tc_dump_pending = 0; idx = 0; return; } + uint8_t cks = 0; + for (int i2 = 0; i2 < nb; i2++) cks ^= (uint8_t)body[i2]; + n = snprintf(dline, sizeof dline, "$%s*%02X\r\n", body, (unsigned)cks); + break; + } + } + if (n <= 0 || n >= (int)sizeof dline) { tc_dump_pending = 0; idx = 0; dn = -1; return; } + dn = n; + busy_ct = 0; + } + + __disable_irq(); // serialise against the ISR NMEA passthrough + uint8_t r = CDC_Copy_Transmit((uint8_t*)dline, (uint16_t)dn); + __enable_irq(); + if (r == USBD_BUSY) { // retry the SUBMIT only; the line stays built + if (++busy_ct > 5000) { tc_dump_pending = 0; idx = 0; dn = -1; } // host stopped reading + return; + } + dn = -1; + if (++idx > 8) { idx = 0; tc_dump_pending = 0; } +} + +// Main-loop entry point, called every pass. With every tc key at its default this reduces to +// four flag checks — no measurable cost, no behaviour change. +void tc_housekeeping(void){ + if (!tc_nom_load) tc_nom_load = SysTick->LOAD; // capture the nominal period once + + if (tc_reset_pending) { + memset(tc_bins, 0, sizeof tc_bins); + tc_hse_valid = tc_lse_valid = 0; + tc_n_hse = tc_n_lse = 0; + tc_e0_set = 0; tc_ema = 0; // new origin rebase with the next sample + tc_reset_pending = 0; + } + + if (tc_learn) { + tc_hse_learn(); + tc_lse_learn(); + static uint32_t last_fit = 0; + uint32_t now = (uint32_t)currentTime; + if (now - last_fit >= 300) { last_fit = now; tc_fit(); } // refit at most every 5 min + } + + // tc_steer_on in the gate: the governor owns DISENGAGE, so it must stay reachable even if + // the user turns every tc key off while steering is engaged mid-holdover — otherwise the + // tick ISR would keep applying a stale frozen correction forever. + if (tc_learn || tc_apply || tc_rtc || tc_steer_on || displayMode == MODE_TEMPCOMP) tc_governor(); + + tc_dump_step(); +} + +// tc_steer(): holdover rate steering (see tc_governor). Sets the length of the NEXT 1 ms +// period: LOAD writes take effect at the following reload, so distributing tc_rem longer +// periods per 1000 gives an average of base + rem/1000 extra ticks per ms — fractional-ppm +// rate control with three int32 ops. tc_steer_on is 0 unless tc_apply engaged in holdover, +// so the stock cost is one predicted-untaken branch per ms. +#define tc_steer() \ + if (tc_steer_on) { \ + tc_acc += tc_rem; \ + if (tc_acc >= 1000) { tc_acc -= 1000; SysTick->LOAD = (uint32_t)(tc_load_base + 1); } \ + else { SysTick->LOAD = (uint32_t)tc_load_base; } \ + } + #define timetick() \ + tc_steer(); \ millisec++; \ if (millisec>=10) { \ millisec=0; \ @@ -1844,6 +2403,7 @@ void SysTick_CountUp_P0(void) { } void SysTick_CountUp_NoUpdate(void) { + tc_steer(); // this handler inlines its own cascade: hook it too millisec++; if (millisec>=10) { millisec=0; @@ -2728,14 +3288,16 @@ int main(void) monitor_vbus(); - if (pps_ts_enabled) { + if (pps_ts_enabled || tc_learn || tc_apply || tc_rtc || displayMode == MODE_TEMPCOMP) { static uint32_t last_temp_read = 0; if ((uint32_t)currentTime - last_temp_read >= 4) { // refresh die temp every ~4 s last_temp_read = (uint32_t)currentTime; measure_temp(); } - if (pps_record_pending) emitPPSTimestamp(); // emit clears pending itself on success } + if (pps_ts_enabled && pps_record_pending) emitPPSTimestamp(); // emit clears pending itself on success + + tc_housekeeping(); // temp-comp learn/steer/dump; four flag checks when everything is off if (displayMode == MODE_VBAT) measure_vbat(); diff --git a/qspi/config.txt b/qspi/config.txt index b6a6983..5d1ee06 100644 --- a/qspi/config.txt +++ b/qspi/config.txt @@ -1,140 +1,168 @@ - -# Matrix refresh rate, in Hz. Min 1000, max 100000 -# This number is only a target, exact frequency will be a division of processor clock speed. -# Note the display is split into four matrices of 5 digits each. The number given here is the -# refresh rate of the display - the clock frequency of the matrix will be five times this number. -MATRIX_FREQUENCY = 20000 - - -# Use this timezone, leave blank to calculate automatically -# Must be an IANA timezone string, use e.g. Etc/GMT+5 for fixed offsets -# For UTC use Etc/UTC -#ZONE_OVERRIDE = America/New_York - - - - -## modes - -# ISO8601 standard, YYYY-MM-DD -MODE_ISO8601_STD=Enabled - -# ISO8601 ordinal (day of year) -MODE_ISO_Ordinal=disabled - -# ISO8601 week (year can be different around new year) -MODE_ISO_WEEK = disabled - -# Unix timestamp mode. The unix timestamp is always UTC. -MODE_UNIX = Disabled - -# Julian Date -MODE_JULIAN_DATE = disabled - -# Modified Julian Date (JD − 2400000.5) -# Note: the fractional part is approximate, and only updated once per second. -MODE_MODIFIED_JD = disabled - -# Display the UTC offset of the current local time -MODE_SHOW_OFFSET = enabled - -# Attempt to display tz name on the 7-segment display, this is usually fairly illegible -MODE_SHOW_TZ_NAME = enabled - -MODE_WEEKDAY = disabled -MODE_WEEKDA_DD = disabled -MODE_WDY_MM_DD = disabled - -# Turn off all LEDs. This mode can be useful if you want to reduce power consumption -# but keep the GPS module fully powered. -MODE_STANDBY=disabled - - -MODE_COUNTDOWN = off -COUNTDOWN_TO = 2023-05-06T23:00:00Z - - -# generic text display, can be used for testing -MODE_TEXT=off -TEXT=hello - - -# One of: slowfade, heartbeat, sawtooth, alt_sawtooth, toggle, solid -colon_mode=heartbeat - - -# Tolerance times, in seconds. -# When the clock loses its GPS fix, it progressively hides the last digits to represent the widening -# accuracy tolerance. These settings represent when to hide the last digits. -# If the TCXO is accurate to 1ppm, it would take 1000 seconds to drift by one millisecond. -Tolerance_time_1ms = 1000 -Tolerance_time_10ms = 10000 - -# The deciseconds digit is only disabled if the clock has been powered off, and the last RTC calibration -# was more than this many seconds ago -Tolerance_time_100ms = 100000 - -# For paranoia, set the 1ms tolerance to 1, which will disable the last digit as soon as GPS fix is lost. -# To turn off the tolerance feature, set all tolerances to 0. - - -# nonlinear brightness curve, five stops of input->output, use brightness-curve.htm for a GUI -# VTT9812FH with R11 = 470K -BS1 = 0,0 -BS2 = 131,365 -BS3 = 1076,1422 -BS4 = 2774,2665 -BS5 = 3849,4095 - -# shows input and output values of DAC/ADC -MODE_DEBUG_BRIGHTNESS = 0 - -# override auto brightness -#brightness=0.5 - -# show remainder of RTC calibration after 63 second period -MODE_DEBUG_RTC = 0 - -# list number of satellites in view (number, not signal strength) -mode_satview = 0 - -# show coin cell voltage -mode_vbat = 0 - -# output a $PMTXTS timing sentence over USB serial on each GPS PPS pulse, carrying the -# sub-millisecond phase measurement made at the edge, for host-side jitter/drift analysis. + +# Matrix refresh rate, in Hz. Min 1000, max 100000 +# This number is only a target, exact frequency will be a division of processor clock speed. +# Note the display is split into four matrices of 5 digits each. The number given here is the +# refresh rate of the display - the clock frequency of the matrix will be five times this number. +MATRIX_FREQUENCY = 20000 + + +# Use this timezone, leave blank to calculate automatically +# Must be an IANA timezone string, use e.g. Etc/GMT+5 for fixed offsets +# For UTC use Etc/UTC +#ZONE_OVERRIDE = America/New_York + + + + +## modes + +# ISO8601 standard, YYYY-MM-DD +MODE_ISO8601_STD=Enabled + +# ISO8601 ordinal (day of year) +MODE_ISO_Ordinal=disabled + +# ISO8601 week (year can be different around new year) +MODE_ISO_WEEK = disabled + +# Unix timestamp mode. The unix timestamp is always UTC. +MODE_UNIX = Disabled + +# Julian Date +MODE_JULIAN_DATE = disabled + +# Modified Julian Date (JD − 2400000.5) +# Note: the fractional part is approximate, and only updated once per second. +MODE_MODIFIED_JD = disabled + +# Display the UTC offset of the current local time +MODE_SHOW_OFFSET = enabled + +# Attempt to display tz name on the 7-segment display, this is usually fairly illegible +MODE_SHOW_TZ_NAME = enabled + +MODE_WEEKDAY = disabled +MODE_WEEKDA_DD = disabled +MODE_WDY_MM_DD = disabled + +# Turn off all LEDs. This mode can be useful if you want to reduce power consumption +# but keep the GPS module fully powered. +MODE_STANDBY=disabled + + +MODE_COUNTDOWN = off +COUNTDOWN_TO = 2023-05-06T23:00:00Z + + +# generic text display, can be used for testing +MODE_TEXT=off +TEXT=hello + + +# One of: slowfade, heartbeat, sawtooth, alt_sawtooth, toggle, solid +colon_mode=heartbeat + + +# Tolerance times, in seconds. +# When the clock loses its GPS fix, it progressively hides the last digits to represent the widening +# accuracy tolerance. These settings represent when to hide the last digits. +# If the TCXO is accurate to 1ppm, it would take 1000 seconds to drift by one millisecond. +Tolerance_time_1ms = 1000 +Tolerance_time_10ms = 10000 + +# The deciseconds digit is only disabled if the clock has been powered off, and the last RTC calibration +# was more than this many seconds ago +Tolerance_time_100ms = 100000 + +# For paranoia, set the 1ms tolerance to 1, which will disable the last digit as soon as GPS fix is lost. +# To turn off the tolerance feature, set all tolerances to 0. + + +# nonlinear brightness curve, five stops of input->output, use brightness-curve.htm for a GUI +# VTT9812FH with R11 = 470K +BS1 = 0,0 +BS2 = 131,365 +BS3 = 1076,1422 +BS4 = 2774,2665 +BS5 = 3849,4095 + +# shows input and output values of DAC/ADC +MODE_DEBUG_BRIGHTNESS = 0 + +# override auto brightness +#brightness=0.5 + +# show remainder of RTC calibration after 63 second period +MODE_DEBUG_RTC = 0 + +# list number of satellites in view (number, not signal strength) +mode_satview = 0 + +# show coin cell voltage +mode_vbat = 0 + +# output a $PMTXTS timing sentence over USB serial on each GPS PPS pulse, carrying the +# sub-millisecond phase measurement made at the edge, for host-side jitter/drift analysis. # When on, the sentence also carries a USB SOF-correlation tail (dwt_pps, sof_frame, dwt_sof) # that lets a host place the edge on its clock to ~microseconds despite USB delivery jitter; # a host that only needs the phase can ignore the extra fields. Enabling this turns on the USB # Start-Of-Frame interrupt (a cheap 1 kHz latch, below the display in priority). -pps = off - - -## astro modes (GPS-derived) -# These show on the date row while the live clock keeps running on the time row. -# They use the current position; with no GPS fix they fall back to fake_latitude/ -# fake_longitude below (handy indoors), or show "----" if neither is set. - -# Sunrise / sunset / solar noon (local time). Pages RISE -> SET -> SOL (see page_ms). -MODE_SUN = disabled - -# Sun azimuth & elevation right now, e.g. "AZ142EL38" (degrees; elevation may be negative). -MODE_SUN_AZEL = disabled - -# Moon: phase index 0-7 then illuminated %, e.g. "MOON 4 99". -# Index: 0 new, 1 waxing crescent, 2 first quarter, 3 waxing gibbous, -# 4 full, 5 waning gibbous, 6 last quarter, 7 waning crescent. -MODE_MOON = disabled - -# Maidenhead grid locator, e.g. "IO91xl". -MODE_GRID = disabled - -# Latitude / longitude in decimal degrees. Pages LAT -> LON (see page_ms). -MODE_LATLON = disabled - -# Dwell per sub-screen for the paged modes (MODE_SUN, MODE_LATLON), in milliseconds. -# Default 5500 (a subjectively-tuned cadence, found by feel); floored at 250. -page_ms = 5500 +pps = off + + +## temperature compensation (opt-in; all off = stock behaviour) +# While GPS-locked the clock can LEARN how each oscillator drifts with die temperature +# (tc_learn), then during a GPS outage steer the display timebase from that model +# (tc_apply) and/or keep the battery RTC trimmed for the next power-up (tc_rtc). +# Send "tc_dump = on" over USB serial to print the learned coefficients as lines you can +# paste below: pasted tc_hse_*/tc_lse_* values freeze the model (they override learning). +# "tc_reset = on" over serial clears the learned data. Both are ignored inside this file. +tc_learn = off +tc_apply = off +tc_rtc = off + +# show die temp / model offsets / sample count on the date row (pages like the astro modes) +mode_tempcomp = 0 + +# model centre (deg C) and safety limits; paste frozen coefficients from tc_dump below. +# HSE uses temperature DIFFERENCES only, so it has no 'a' term — just slope + curvature. +# Setting a coefficient to "nan" unfreezes it (learning takes over again). +# tc_t0 = 40 +# tc_hse_b = +# tc_hse_c = +# tc_lse_a = +# tc_lse_b = +# tc_lse_c = +# tc_engage_s = 2 (minimum 2) +# tc_max_ppm = 100 + + +## astro modes (GPS-derived) +# These show on the date row while the live clock keeps running on the time row. +# They use the current position; with no GPS fix they fall back to fake_latitude/ +# fake_longitude below (handy indoors), or show "----" if neither is set. + +# Sunrise / sunset / solar noon (local time). Pages RISE -> SET -> SOL (see page_ms). +MODE_SUN = disabled + +# Sun azimuth & elevation right now, e.g. "AZ142EL38" (degrees; elevation may be negative). +MODE_SUN_AZEL = disabled + +# Moon: phase index 0-7 then illuminated %, e.g. "MOON 4 99". +# Index: 0 new, 1 waxing crescent, 2 first quarter, 3 waxing gibbous, +# 4 full, 5 waning gibbous, 6 last quarter, 7 waning crescent. +MODE_MOON = disabled + +# Maidenhead grid locator, e.g. "IO91xl". +MODE_GRID = disabled + +# Latitude / longitude in decimal degrees. Pages LAT -> LON (see page_ms). +MODE_LATLON = disabled + +# Dwell per sub-screen for the paged modes (MODE_SUN, MODE_LATLON), in milliseconds. +# Default 5500 (a subjectively-tuned cadence, found by feel); floored at 250. +page_ms = 5500 + # Local Sidereal Time as a LIVE TICKING CLOCK on the time row (big digits). The date row # keeps the civil date, and the colons animate differently (alt_colon_mode below) so it # can never be mistaken for civil time. Sidereal runs 1.00273791x faster than civil, so the From 311e031ccf2b037eebc9a32dd28bdb85f6b1f939 Mon Sep 17 00:00:00 2001 From: Peter Lewis Date: Mon, 6 Jul 2026 20:12:20 +0100 Subject: [PATCH 14/37] Holdover significance-fade + persistent, evolving warm-start temp model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two coupled additions on top of the self-learning tempcomp, both opt-in and byte-identical to stock behaviour when off. significance_fade: during GPS-loss holdover, retire each sub-second digit by its remaining SIGNIFICANCE rather than by the fixed dash timeouts. A live 3-sigma time-interval-error bound U(tau) = k*sigma*tau (sigma = RSS of the calibration, the MEASURED temp-model residual, and aging terms) drives setPrecision's P3/P2/P1/P0 ladder: a digit dashes the moment U passes its place value — the same dash glyph as the Tolerance_time_* ladder, but driven by measured significance instead of fixed timeouts. digit_bright[] carries the per-digit intensity for a future smooth-dimming display change (and for emulators). tc_fit now also reports the weighted-RMS fit residual, mean-centred for HSE whose model origin is arbitrary. tc_seed (warm start): reload a previously-learned model -- the last tc_dump, persisted in config.txt by the host -- as an EVOLVING prior rather than a freeze. The clock is temperature-compensated from the first second and keeps refining: tc_fit_one returns the achieved model order and tc_fit preserves the seed until real data supports a fit at least as rich, then hands over. New keys tc_seed / tc_seed_lo / tc_seed_hi (documented in qspi/config.txt); tc_dump emits the coverage so a paste round-trips. Over serial the "tc_seed = on" line is the trigger -- it arms tc_seed_pending (a single-word ISR write) and tc_housekeeping applies the seed from the MAIN LOOP, serialized with tc_fit/tc_governor, after the whole paste has parsed. The learned state keeps its main-loop-only contract; the USB ISR never touches the model. Survived two adversarial review passes (the feature, then this port), with fixes re-verified in the emulator against this exact tree: boot-time tc_nom_load capture (ticks-per-ppm is never 0), origin-invariant HSE residual, main-loop-serialized serial seeding with a whole-paste trigger, tc_reset clears the held prior, significance_fade drives the dash ladder (no regression vs stock when enabled), and measure_temp's cadence gate includes the fade so die_temp_c is always real. Co-Authored-By: Claude Opus 4.8 --- mk4-time/Core/Src/main.c | 260 +++++++++++++++++++++++++++++++++++++-- qspi/config.txt | 12 +- 2 files changed, 263 insertions(+), 9 deletions(-) diff --git a/mk4-time/Core/Src/main.c b/mk4-time/Core/Src/main.c index cea7e57..75f15b1 100644 --- a/mk4-time/Core/Src/main.c +++ b/mk4-time/Core/Src/main.c @@ -250,6 +250,15 @@ volatile uint8_t pps_sof_valid = 0; // 1 once a real SOF anchor has been latc // Config-key scalars are written from the USB OTG ISR (parseConfigString) and read by the // main loop: volatile, matching the pps_ts_enabled precedent. volatile _Bool tc_learn = 0, tc_apply = 0, tc_rtc = 0; +// Holdover fade (opt-in). A sub-second digit whose accuracy can no longer be held — its time +// uncertainty during GPS-loss holdover has grown past that digit's place value — FADES to black by +// its remaining significance instead of dashing, overriding the fixed Tolerance_time_* ladder. +// digit_bright[] holds per-digit intensity 0..FADE_MAX for the [deciseconds, centiseconds, +// milliseconds, decimal-point] positions (FADE_MAX = fully lit, i.e. certainly significant). +volatile _Bool significance_fade = 0; +#define FADE_MAX 16 +uint8_t digit_bright[4] = { FADE_MAX, FADE_MAX, FADE_MAX, FADE_MAX }; +float holdover_u_us = 0.0f; // last computed 3σ time-interval-error bound U(τ), µs volatile int16_t tc_t0 = 40; // model centre temperature (°C) volatile uint16_t tc_engage_s = 2; // seconds of PPS absence before steering engages (min 2) volatile uint16_t tc_max_ppm = 100; // hard clamp on the applied correction magnitude @@ -286,6 +295,27 @@ _Bool tc_hse_valid = 0, tc_lse_valid = 0; int16_t tc_hse_tmin = 0, tc_hse_tmax = 0; // learned coverage: model is clamped to this range int16_t tc_lse_tmin = 0, tc_lse_tmax = 0; uint32_t tc_n_hse = 0, tc_n_lse = 0; // lifetime sample counts (display + dump) +// Weighted-RMS residual of each learned fit (model vs bin means): the model's OWN error, in its +// fit units (HSE: SysTick ticks/s; LSE: ppm). This is the holdover-fade uncertainty's σ_temp source +// — how far the temperature model actually is from the measured data, not a guess. +float tc_hse_resid = 0, tc_lse_resid = 0; + +// Warm-start (seed-and-evolve). A previously-learned model — the last tc_dump, written back into +// config.txt by the host (the firmware never writes the filesystem; the QSPI drive is host-owned) — +// is reloaded at boot as an evolving PRIOR rather than a hard freeze. The clock is temperature- +// compensated from the first second and keeps refining: tc_hse_prior/tc_lse_prior hold the model +// ORDER (1 const, 2 linear, 3 quadratic) currently carried by the seed, and tc_fit keeps that model +// until real learning supports a fit at least as rich, then hands over. tc_seed = off leaves the +// existing freeze path (tc_hse_b/… as asserted constants) untouched. +volatile _Bool tc_seed = 0; // config: warm-start from the seeded coefficients + evolve +volatile int16_t tc_seed_lo = 0, tc_seed_hi = 0; // seed coverage (die °C): bounds the prior — never extrapolated +uint8_t tc_hse_prior = 0, tc_lse_prior = 0; // seed model order still held (0 = handed over to real data) +_Bool tc_seed_done = 0; // one-shot: seed once per power-on (BSS-cleared at reset) +// Serial "tc_seed = on" arms this in the USB ISR; tc_housekeeping consumes it in the MAIN LOOP. +// The learned-state contract ("main-loop only") holds: the ISR only ever touches this one flag +// (plus the already-accepted single-word tc_cfg_*/tc_seed slots) — never the model itself. +volatile _Bool tc_seed_pending = 0; +static void tc_seed_apply(void); // defined by the tempcomp block; called after the config load // Display cache for MODE_TEMPCOMP. Written by the governor (main loop); read by sendDate, // which ALSO runs from the SysTick ISRs — each field is a single 32-bit (atomic) access, so @@ -551,7 +581,7 @@ void sendDate( _Bool now ){ } break; case MODE_TEMPCOMP: { - // Pages: die temp -> HSE model -> LSE model -> samples+state, astro_page_ms dwell each. + // Pages: die temp -> HSE model -> LSE model -> samples+state, 5.5 s dwell each. // Values are the governor's display cache (clamped so the row never overflows). Layout is // the RISE/SET style: label, separator space, a sign slot (space when positive), then the // digits — numbers align whether signed or not, and short values keep clear space at the @@ -1437,6 +1467,8 @@ void parseConfigString(char *key, char *value, _Bool from_serial) { tc_learn = truthy(value); // accumulate (die temp, ppm) samples while GPS-locked } else if (strcasecmp(key, "tc_apply") == 0) { tc_apply = truthy(value); // steer the SysTick timebase during GPS-loss holdover + } else if (strcasecmp(key, "significance_fade") == 0) { + significance_fade = truthy(value); // fade sub-second digits by significance in holdover, not dash } else if (strcasecmp(key, "tc_rtc") == 0) { tc_rtc = truthy(value); // additionally trim RTC->CALR while GPS is absent } else if (strcasecmp(key, "tc_t0") == 0) { @@ -1452,6 +1484,16 @@ void parseConfigString(char *key, char *value, _Bool from_serial) { } else if (strcasecmp(key, "tc_lse_a") == 0) { tc_parse_coeff(value, &tc_cfg_lse[0]); } else if (strcasecmp(key, "tc_lse_b") == 0) { tc_parse_coeff(value, &tc_cfg_lse[1]); } else if (strcasecmp(key, "tc_lse_c") == 0) { tc_parse_coeff(value, &tc_cfg_lse[2]); + } else if (strcasecmp(key, "tc_seed") == 0) { + tc_seed = truthy(value); // load the coefficients above as an EVOLVING prior, not a freeze + // Over serial, the "tc_seed = on" line is the TRIGGER: it arms a main-loop apply, so a paste + // seeds exactly once, after all its coefficient lines have parsed (send it last — the order + // tc_dump prints). ISR-safe by design: the apply itself always runs from tc_housekeeping. + if (from_serial && tc_seed) tc_seed_pending = 1; + } else if (strcasecmp(key, "tc_seed_lo") == 0) { + tc_seed_lo = (int16_t)atoi(value); // seed coverage low edge (die °C) — the prior is not extrapolated + } else if (strcasecmp(key, "tc_seed_hi") == 0) { + tc_seed_hi = (int16_t)atoi(value); } else if (strcasecmp(key, "tc_dump") == 0) { // Serial-only trigger: print the learned model as paste-ready config lines. A stray // tc_dump left in config.txt must not fire on every (re)load, hence the origin guard. @@ -1640,6 +1682,7 @@ void readConfigFile(void){ else requestMode=255; postConfigCleanup(); + tc_seed_apply(); // warm-start the tempco model from a persisted seed (tc_seed = on), else no-op } void calibrateRTC(void){ @@ -1986,7 +2029,9 @@ static _Bool tc_gauss3(float A[3][3], float y[3], float x[3]){ // pivot threshold yet overflow to Inf/NaN, and NaN must never reach the steering or display. static _Bool tc_fin3(const float m[3]){ return isfinite(m[0]) && isfinite(m[1]) && isfinite(m[2]); } -static _Bool tc_fit_one(_Bool lse, float scale, uint16_t n_quad, float m[3], +// Returns the ACHIEVED model order: 3 quadratic, 2 linear, 1 constant, 0 no fit. (The order lets the +// warm-start prior be preserved until real data supports a fit at least as rich — see tc_fit.) +static int tc_fit_one(_Bool lse, float scale, uint16_t n_quad, float m[3], int16_t *tmin_out, int16_t *tmax_out){ float S[5] = {0,0,0,0,0}, T[3] = {0,0,0}; float S0a = 0, T0a = 0; // all-samples weighted mean (constant fallback) @@ -2016,7 +2061,7 @@ static _Bool tc_fit_one(_Bool lse, float scale, uint16_t n_quad, float m[3], if (nb >= 3 && (tmax - tmin) >= 6) { // quadratic float A[3][3] = {{S[0],S[1],S[2]},{S[1],S[2],S[3]},{S[2],S[3],S[4]}}; float yv[3] = {T[0],T[1],T[2]}; - if (tc_gauss3(A, yv, m) && tc_fin3(m)) { *tmin_out = tmin; *tmax_out = tmax; return 1; } + if (tc_gauss3(A, yv, m) && tc_fin3(m)) { *tmin_out = tmin; *tmax_out = tmax; return 3; } } if (nb >= 2 && (tmax - tmin) >= 4) { // linear float det = S[0]*S[2] - S[1]*S[1]; @@ -2024,7 +2069,7 @@ static _Bool tc_fit_one(_Bool lse, float scale, uint16_t n_quad, float m[3], m[0] = (T[0]*S[2] - T[1]*S[1]) / det; m[1] = (S[0]*T[1] - S[1]*T[0]) / det; m[2] = 0; - if (tc_fin3(m)) { *tmin_out = tmin; *tmax_out = tmax; return 1; } + if (tc_fin3(m)) { *tmin_out = tmin; *tmax_out = tmax; return 2; } } } if (S0a >= (lse ? 8.0f : 60.0f)) { // constant: the dominant fixed offset @@ -2034,13 +2079,111 @@ static _Bool tc_fit_one(_Bool lse, float scale, uint16_t n_quad, float m[3], return 0; } +static float tc_poly(const float m[3], float x); // fwd: the residual pass evaluates the just-fit model + +// Weighted-RMS residual of a fitted model over every populated bin (not only the curve-eligible +// ones): sqrt( Σ n·(bin_mean − model(T))² / Σ n ), in the fit's units. Runs as a second pass, so +// it never perturbs the fit itself; 40 bins, at most once per 5 min alongside tc_fit(). +// `center` removes the weighted-mean of the residuals before the RMS — i.e. an ORIGIN-INVARIANT error. +// Needed for HSE: its model origin (m[0]) is arbitrary, so while a warm-start seed is held (m[0]=0) the +// real bins carry a different DC constant (the tc_e0 rebase); the raw offset would swamp the RMS and +// corrupt the holdover-fade σ_temp. Mean-centring measures only how well the SLOPE/CURVATURE match, +// which is all HSE cares about. LSE (absolute ppm, real m[0]) passes center=0 — its offset is real error. +static float tc_fit_resid(_Bool lse, float scale, const float m[3], _Bool center){ + float sw = 0, swr = 0, swr2 = 0; + for (int i = 0; i < 40; i++){ + uint16_t n = lse ? tc_bins[i].lse_n : tc_bins[i].hse_n; + if (!n) continue; + int32_t sum = lse ? tc_bins[i].lse_sum : tc_bins[i].hse_sum; + float y = ((float)sum / (float)n) * scale; + float x = ((float)(i * 2 - 8) + 0.5f) - (float)tc_t0; // bin centre − model origin + float r = y - tc_poly(m, x); + sw += (float)n; swr += (float)n * r; swr2 += (float)n * r * r; + } + if (!(sw > 0) || !isfinite(swr2)) return 0.0f; + if (center) swr2 -= swr * swr / sw; // Σn(r−r̄)² = Σn·r² − (Σn·r)²/Σn + return (swr2 > 0 && isfinite(swr2)) ? sqrtf(swr2 / sw) : 0.0f; +} + +// Refit both models from the bins. A warm-start prior (tc_*_prior != 0) is PRESERVED until real data +// supports a fit at least as rich as the seed's order — then real data takes over (prior cleared). +// With no prior held (the normal cold-learn case) this is the original behaviour: fit -> adopt/invalidate. +// The residual is always re-measured against whatever model is held; while a seed is still held with no +// real samples yet, tc_fit_resid returns 0 (no data) and the seed's CARRIED residual is kept. static void tc_fit(void){ - tc_hse_valid = tc_fit_one(0, 1.0f, 64, tc_hse_m, &tc_hse_tmin, &tc_hse_tmax); - tc_lse_valid = tc_fit_one(1, 1e6f/(32768.0f*63.0f), 4, tc_lse_m, &tc_lse_tmin, &tc_lse_tmax); + int16_t tmn, tmx; float m[3]; + + int oh = tc_fit_one(0, 1.0f, 64, m, &tmn, &tmx); + if (oh >= tc_hse_prior) { // real data at least as rich as the prior -> adopt it + tc_hse_valid = (oh > 0); + if (oh) { tc_hse_m[0]=m[0]; tc_hse_m[1]=m[1]; tc_hse_m[2]=m[2]; tc_hse_tmin=tmn; tc_hse_tmax=tmx; } + tc_hse_prior = 0; + } + if (tc_hse_valid) { float r = tc_fit_resid(0, 1.0f, tc_hse_m, 1); if (r > 0 || !tc_hse_prior) tc_hse_resid = r; } + else tc_hse_resid = 0; + + const float lse_scale = 1e6f/(32768.0f*63.0f); + int ol = tc_fit_one(1, lse_scale, 4, m, &tmn, &tmx); + if (ol >= tc_lse_prior) { + tc_lse_valid = (ol > 0); + if (ol) { tc_lse_m[0]=m[0]; tc_lse_m[1]=m[1]; tc_lse_m[2]=m[2]; tc_lse_tmin=tmn; tc_lse_tmax=tmx; } + tc_lse_prior = 0; + } + if (tc_lse_valid) { float r = tc_fit_resid(1, lse_scale, tc_lse_m, 0); if (r > 0 || !tc_lse_prior) tc_lse_resid = r; } + else tc_lse_resid = 0; } static float tc_poly(const float m[3], float x){ return m[0] + m[1]*x + m[2]*x*x; } +// Warm-start the tempco model from the seeded coefficients (tc_seed = on). Loads tc_hse_b/c and +// tc_lse_a/b/c — the last tc_dump, persisted in config.txt by the host — as the LIVE model so the +// clock is temperature-compensated from the first second, records the seed's order (kept by tc_fit +// until real data is at least as rich), and seeds a conservative residual so holdover-fade stays +// honest before real samples arrive. Clearing the frozen slots is what turns a freeze into an +// evolving prior. Runs once per boot; on a later live config reload it only re-clears the reparsed +// coefficients so the freeze path can't silently re-activate over the evolving model. +static void tc_seed_apply(void){ + if (!tc_seed) return; + if (tc_seed_done) { // live reload: keep evolving — don't re-freeze or re-seed + tc_cfg_hse[0]=tc_cfg_hse[1]=tc_cfg_hse[2]=NAN; + tc_cfg_lse[0]=tc_cfg_lse[1]=tc_cfg_lse[2]=NAN; + return; + } + _Bool have_hse = !isnan(tc_cfg_hse[1]) || !isnan(tc_cfg_hse[2]); + _Bool have_lse = !isnan(tc_cfg_lse[0]) || !isnan(tc_cfg_lse[1]) || !isnan(tc_cfg_lse[2]); + if (!have_hse && !have_lse) return; // seed enabled but no coefficients yet — wait for a reload + tc_seed_done = 1; + + int16_t lo = tc_seed_lo, hi = tc_seed_hi; + if (hi - lo < 4) { lo = (int16_t)(tc_t0 - 6); hi = (int16_t)(tc_t0 + 6); } // sane span if none given + // readConfigFile (hence this seed) runs at boot BEFORE the first tc_housekeeping, so tc_nom_load is + // still 0 and tc_tpp() would return 0 — which would silently zero the tick-domain HSE model. Capture + // the nominal SysTick period here (SystemClock_Config set it before the config load) — the same + // capture tc_housekeeping() also performs. + if (!tc_nom_load) tc_nom_load = SysTick->LOAD; + float tpp = (float)tc_tpp(); + + if (have_hse) { + tc_hse_m[0] = 0.0f; // arbitrary origin — steering uses temperature differences + tc_hse_m[1] = isnan(tc_cfg_hse[1]) ? 0.0f : tc_cfg_hse[1] * tpp; + tc_hse_m[2] = isnan(tc_cfg_hse[2]) ? 0.0f : tc_cfg_hse[2] * tpp; + tc_hse_tmin = lo; tc_hse_tmax = hi; tc_hse_valid = 1; + tc_hse_prior = (!isnan(tc_cfg_hse[2]) && tc_cfg_hse[2] != 0.0f) ? 3 : 2; + tc_hse_resid = 2.0f * tpp; // conservative (~2 ppm) until real data measures it + tc_cfg_hse[0] = tc_cfg_hse[1] = tc_cfg_hse[2] = NAN; // seed replaces freeze -> free to evolve + } + if (have_lse) { + tc_lse_m[0] = isnan(tc_cfg_lse[0]) ? 0.0f : tc_cfg_lse[0]; + tc_lse_m[1] = isnan(tc_cfg_lse[1]) ? 0.0f : tc_cfg_lse[1]; + tc_lse_m[2] = isnan(tc_cfg_lse[2]) ? 0.0f : tc_cfg_lse[2]; + tc_lse_tmin = lo; tc_lse_tmax = hi; tc_lse_valid = 1; + tc_lse_prior = (!isnan(tc_cfg_lse[2]) && tc_cfg_lse[2] != 0.0f) ? 3 + : (!isnan(tc_cfg_lse[1]) && tc_cfg_lse[1] != 0.0f) ? 2 : 1; + tc_lse_resid = 2.0f; + tc_cfg_lse[0] = tc_cfg_lse[1] = tc_cfg_lse[2] = NAN; + } +} + // LSE model (absolute ppm): non-NAN config a freezes it (the user asserted the values); // otherwise the learned fit, clamped to its observed temperature range (no extrapolation). // Config values are USB-ISR-written; snapshot each element once (single-word reads are atomic). @@ -2132,6 +2275,7 @@ static void tc_governor(void){ tc_disp_lse = dl; tc_disp_lse_ok = have_l; tc_disp_state = tc_steer_on ? 'A' : (!isnan(tc_cfg_hse[1]) || !isnan(tc_cfg_lse[0])) ? 'F' + : (tc_hse_prior || tc_lse_prior) ? 'S' // running on the warm-start seed (evolving) : (tc_learn && fresh < 5) ? 'L' : '-'; // --- HSE steering: engage only in holdover, after first-ever fix, with a usable model. @@ -2263,6 +2407,13 @@ static void tc_dump_step(void){ n = snprintf(dline, sizeof dline, "$%s*%02X\r\n", body, (unsigned)cks); break; } + case 9: { // seed coverage — with "tc_seed = on" the paste warm-starts + int lo = tc_hse_valid ? tc_hse_tmin : tc_lse_tmin; // (and keeps evolving) instead of freezing + int hi = tc_hse_valid ? tc_hse_tmax : tc_lse_tmax; + if (tc_lse_valid) { if (tc_lse_tmin < lo) lo = tc_lse_tmin; if (tc_lse_tmax > hi) hi = tc_lse_tmax; } + n = snprintf(dline, sizeof dline, "tc_seed_lo = %d\r\ntc_seed_hi = %d\r\n", lo, hi); + break; + } } if (n <= 0 || n >= (int)sizeof dline) { tc_dump_pending = 0; idx = 0; dn = -1; return; } dn = n; @@ -2277,17 +2428,73 @@ static void tc_dump_step(void){ return; } dn = -1; - if (++idx > 8) { idx = 0; tc_dump_pending = 0; } + if (++idx > 9) { idx = 0; tc_dump_pending = 0; } } // Main-loop entry point, called every pass. With every tc key at its default this reduces to // four flag checks — no measurable cost, no behaviour change. +// Holdover fade: from the residual 1σ time uncertainty during GPS-loss holdover, set each trailing +// sub-second digit's intensity by its remaining SIGNIFICANCE. U(τ) = k_σ·σ·τ (µs); σ = RSS of three +// independent ppm terms — how well the last cal pinned frequency, the MEASURED temp-model residual, +// and aging. A digit fades over its significance band and goes dark once U exceeds its place value. +static void computeHoldoverFade(void){ + uint32_t age = (uint32_t)currentTime - last_pps_time; // holdover seconds + + // σ_cal — frequency knowledge from the last RTC calibration, decaying with its age. + float cal_age = (float)((uint32_t)currentTime - (uint32_t)rtc_last_calibration); + float sigma_cal; + if (cal_age <= (float)CAL_PERIOD && tc_lse_valid) { + float cal_ppm = (float)debug_rtc_val * (1e6f / (32768.0f * (float)CAL_PERIOD)); + sigma_cal = fabsf(cal_ppm) + 0.05f * sqrtf(cal_age / (float)CAL_PERIOD); + } else { + sigma_cal = 0.02f * sqrtf(cal_age); // stale cal: random-walk bound + } + + // σ_temp — the MEASURED tempco-model residual (ppm), plus a penalty beyond the learned range. + float sigma_temp; + if (tc_hse_valid) { + float tpp = (float)tc_tpp(); + sigma_temp = (tpp > 0.0f) ? tc_hse_resid / tpp : 10.0f; // HSE residual (ticks/s) → ppm + int t = die_temp_c; + int over = t < tc_hse_tmin ? tc_hse_tmin - t : t > tc_hse_tmax ? t - tc_hse_tmax : 0; + if (over > 0) sigma_temp += 0.3f * (float)over; // unvalidated beyond coverage + } else { + sigma_temp = 10.0f; // no model yet — bare-crystal + } + + // σ_age — long-term oscillator aging (negligible over minutes/hours, kept for completeness). + float sigma_age = 0.1f * (float)age / 86400.0f; + + float sigma = sqrtf(sigma_cal*sigma_cal + sigma_temp*sigma_temp + sigma_age*sigma_age); + float U_us = 3.0f * sigma * (float)age; // k_σ = 3 ("certainly right") + holdover_u_us = U_us; // publish for read-back / display + + // Half place values (µs): 0.1 s, 0.01 s, 0.001 s. Fade band β. b_k = (h−U)/(β·h), clamped 0..1. + static const float h_us[3] = { 50000.0f, 5000.0f, 500.0f }; + const float beta = 0.4f; + for (int k = 0; k < 3; k++){ + float b = (h_us[k] - U_us) / (beta * h_us[k]); + if (b < 0.0f) b = 0.0f; else if (b > 1.0f) b = 1.0f; + digit_bright[k] = (uint8_t)(b * (float)FADE_MAX + 0.5f); + } + digit_bright[3] = digit_bright[0]; // the decimal point dies with the 0.1 s digit +} + void tc_housekeeping(void){ if (!tc_nom_load) tc_nom_load = SysTick->LOAD; // capture the nominal period once + // Serial warm-start, deferred out of the USB ISR: "tc_seed = on" armed the flag; apply here, + // serialized with tc_fit/tc_governor (the learned state stays main-loop-only). With the seed + // already applied, the same call is the freeze guard — it just re-NANs any tc_hse_*/tc_lse_* + // coefficients a later serial line reparsed, so the frozen path can't reactivate over the + // evolving model. + if (tc_seed_pending) { tc_seed_pending = 0; tc_seed_apply(); } + else if (tc_seed_done) tc_seed_apply(); + if (tc_reset_pending) { memset(tc_bins, 0, sizeof tc_bins); tc_hse_valid = tc_lse_valid = 0; + tc_hse_prior = tc_lse_prior = 0; // drop any held warm-start prior: reset is a cold restart tc_n_hse = tc_n_lse = 0; tc_e0_set = 0; tc_ema = 0; // new origin rebase with the next sample tc_reset_pending = 0; @@ -2306,6 +2513,12 @@ void tc_housekeeping(void){ // tick ISR would keep applying a stale frozen correction forever. if (tc_learn || tc_apply || tc_rtc || tc_steer_on || displayMode == MODE_TEMPCOMP) tc_governor(); + if (significance_fade) { // recompute the per-digit fade once per second while enabled + static uint32_t last_fade = 0; + uint32_t now = (uint32_t)currentTime; + if (now != last_fade) { last_fade = now; computeHoldoverFade(); } + } + tc_dump_step(); } @@ -2732,6 +2945,35 @@ void checkDelayedLoadRules(){ } void setPrecision(void){ + if (significance_fade && countMode == COUNT_NORMAL) { + // Holdover fade replaces the FIXED Tolerance_time_* dash ladder with a SIGNIFICANCE-driven one: + // computeHoldoverFade() sets each sub-second digit's intensity in digit_bright[] from the live + // time-interval-error bound, and a digit is DASHED the instant its significance reaches zero — + // honest on the real display. Digits still significant keep ticking (P3/P2/P1), so the emulator + // (and a future per-digit HW dimmer) can render the PARTIAL fade of the one on its way out. At + // lock all are FADE_MAX, so this reduces to a plain P3. digit_bright = [ds, cs, ms, dp]; + // buffer_c[3]/[2]/[1] = ms/cs/ds; the decimal point dies with the 0.1 s digit. + if (digit_bright[2]) { // ms still significant + buffer_c[0].high = 0b11001110 | cSegDP; + SetSysTick( &SysTick_CountUp_P3 ); + } else if (digit_bright[1]) { // ms dark, cs significant + buffer_c[3].low = 0b01000000; + buffer_c[0].high = 0b11001110 | cSegDP; + SetSysTick( &SysTick_CountUp_P2 ); + } else if (digit_bright[0]) { // ds only + buffer_c[3].low = 0b01000000; + buffer_c[2].low = 0b01000000; + buffer_c[0].high = 0b11001110 | cSegDP; + SetSysTick( &SysTick_CountUp_P1 ); + } else { // whole seconds + buffer_c[3].low = 0b01000000; + buffer_c[2].low = 0b01000000; + buffer_c[1].low = 0b01000000; + buffer_c[0].high = 0b11001110; + SetSysTick( &SysTick_CountUp_P0 ); + } + return; + } if (countMode == COUNT_NORMAL) { // situations not covered: @@ -3288,7 +3530,9 @@ int main(void) monitor_vbus(); - if (pps_ts_enabled || tc_learn || tc_apply || tc_rtc || displayMode == MODE_TEMPCOMP) { + // significance_fade is a die-temp consumer too: computeHoldoverFade charges an out-of-coverage + // penalty from die_temp_c, which would otherwise stay at its init 0 with every other flag off. + if (pps_ts_enabled || tc_learn || tc_apply || tc_rtc || significance_fade || displayMode == MODE_TEMPCOMP) { static uint32_t last_temp_read = 0; if ((uint32_t)currentTime - last_temp_read >= 4) { // refresh die temp every ~4 s last_temp_read = (uint32_t)currentTime; diff --git a/qspi/config.txt b/qspi/config.txt index 5d1ee06..1615a0e 100644 --- a/qspi/config.txt +++ b/qspi/config.txt @@ -121,7 +121,7 @@ tc_learn = off tc_apply = off tc_rtc = off -# show die temp / model offsets / sample count on the date row (pages like the astro modes) +# show die temp / model offsets / sample count on the date row (four pages, 5.5 s each) mode_tempcomp = 0 # model centre (deg C) and safety limits; paste frozen coefficients from tc_dump below. @@ -136,6 +136,16 @@ mode_tempcomp = 0 # tc_engage_s = 2 (minimum 2) # tc_max_ppm = 100 +# Warm start (seed-and-evolve): with tc_seed on, the pasted coefficients above load as an +# EVOLVING starting point instead of a freeze — compensated from the first second, refined as +# the clock learns, handed over once its own data is at least as rich. tc_dump also prints +# tc_seed_lo/hi (the model's temperature coverage) so a paste round-trips. Over serial, send +# the coefficients first and "tc_seed = on" last (the order tc_dump prints). +# tc_seed = on +# tc_seed_lo = +# tc_seed_hi = + + ## astro modes (GPS-derived) # These show on the date row while the live clock keeps running on the time row. From 78e0534d4ec6412544f8042c79e876afd5f18f51 Mon Sep 17 00:00:00 2001 From: Peter Lewis Date: Wed, 8 Jul 2026 00:27:14 +0100 Subject: [PATCH 15/37] Fix significance_fade boot flash: digit_bright defaults to 0 (dashed), not FADE_MAX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On a warm boot with significance_fade on, digit_bright[] defaulted to {FADE_MAX,...} ("certainly significant"), so setPrecision() at boot (and each PendSV second) showed the ms digit ticking — a ~1 s flash of numbers — until the first per-second computeHoldoverFade() saw the huge holdover age (last_pps_time=0, no PPS yet) and dashed them. Default to 0 (not significant): the sub-second digits start dashed and only light once computeHoldoverFade proves significance after lock (the same PendSV setPrecision path applies it, symmetric). Pre-existing PR #9 issue, not the SOF change. Builds clean. Co-Authored-By: Claude Opus 4.8 --- mk4-time/Core/Src/main.c | 6 +++++- qspi/output/fwt.bin | Bin 196608 -> 196608 bytes 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/mk4-time/Core/Src/main.c b/mk4-time/Core/Src/main.c index 75f15b1..bbc7687 100644 --- a/mk4-time/Core/Src/main.c +++ b/mk4-time/Core/Src/main.c @@ -255,9 +255,13 @@ volatile _Bool tc_learn = 0, tc_apply = 0, tc_rtc = 0; // its remaining significance instead of dashing, overriding the fixed Tolerance_time_* ladder. // digit_bright[] holds per-digit intensity 0..FADE_MAX for the [deciseconds, centiseconds, // milliseconds, decimal-point] positions (FADE_MAX = fully lit, i.e. certainly significant). +// Default 0 = NOT significant: at power-on nothing is disciplined yet (had_pps=0, holdover age is +// huge), so with significance_fade the sub-second digits must start DASHED and only light once the +// first computeHoldoverFade() proves significance — else setPrecision() at boot reads a stale FADE_MAX +// and flashes ticking numbers for ~1 s before the first per-second recompute dashes them. volatile _Bool significance_fade = 0; #define FADE_MAX 16 -uint8_t digit_bright[4] = { FADE_MAX, FADE_MAX, FADE_MAX, FADE_MAX }; +uint8_t digit_bright[4] = { 0, 0, 0, 0 }; float holdover_u_us = 0.0f; // last computed 3σ time-interval-error bound U(τ), µs volatile int16_t tc_t0 = 40; // model centre temperature (°C) volatile uint16_t tc_engage_s = 2; // seconds of PPS absence before steering engages (min 2) diff --git a/qspi/output/fwt.bin b/qspi/output/fwt.bin index ddcf72412d9761d21aa5caa680c79c5483264e93..7e43d8dabc19fefb6fddf33f0c060d8f5bb801f2 100755 GIT binary patch literal 196608 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

FD=qjWEkQWjUyzWiIsB23 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