From b73934d13b461bed0b9b519159dfebe52ec00b56 Mon Sep 17 00:00:00 2001 From: Peter Lewis Date: Sat, 4 Jul 2026 09:57:27 +0100 Subject: [PATCH 01/12] 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/12] 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/12] =?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/12] 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/12] 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/12] 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/12] 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/12] 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/12] =?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/12] 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/12] 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/12] 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. */