diff --git a/mk4-date/Core/Src/main.c b/mk4-date/Core/Src/main.c
index 5190e7f..c526627 100644
--- a/mk4-date/Core/Src/main.c
+++ b/mk4-date/Core/Src/main.c
@@ -1,1269 +1,1372 @@
-/* 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"
-
-/* Private includes ----------------------------------------------------------*/
-/* USER CODE BEGIN Includes */
-#include
-#include
-#include
-/* 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 */
-
-#define byteswap32(x) \
- ( ((x & 0xff000000) >> 24) | ((x & 0x00ff0000) >> 8) \
- | ((x & 0x0000ff00) << 8) | ((x & 0x000000ff) << 24))
-
-/* USER CODE END PM */
-
-/* Private variables ---------------------------------------------------------*/
-
-/* USER CODE BEGIN PV */
-
-
-const uint8_t lut_7seg[] = {
- 0,
- 64, // !
- 0b00100010,// "
- 64, // #
- 64, // $
- 64, // %
- 64, // &
- 0b00000010,// '
- 0b00111001,// (
- 0b00001111,// )
- 64, // *
- 64, // +
- 64, // ,
- 0b01000000,// -
- 64, // .
- 0b01010010,// /
- 0b00111111,// 0
- 0b00000110,// 1
- 0b01011011,// 2
- 0b01001111,// 3
- 0b01100110,// 4
- 0b01101101,// 5
- 0b01111101,// 6
- 0b00000111,// 7
- 0b01111111,// 8
- 0b01101111,// 9
- 64, // :
- 64, // ;
- 64, // <
- 64, // =
- 64, // >
- 64, // ?
- 64, // @
- 0b01110111,// A
- 0b01111100,// B
- 0b00111001,// C
- 0b01011110,// D
- 0b01111001,// E
- 0b01110001,// F
- 0b00111101,// G
- 0b01110100,// H
- 0b00000110,// I
- 0b00011110,// J
- 0b01110101,// K
- 0b00111000,// L
- 0b00010101,// M
- 0b01010100,// N
- 0b00111111,// O
- 0b01110011,// P
- 0b01100111,// Q
- 0b01010000,// R
- 0b01101101,// S
- 0b01111000,// T
- 0b00111110,// U
- 0b01100010,// V
- 0b00101010,// W
- 0b01110110,// X
- 0b01101110,// Y
- 0b01011011,// Z
- 0b00111001,// [
- 0b01100100,// \ //
- 0b00001111,// ]
- 0b00100011,// ^
- 0b00001000,// _
- 0b00100000,// `
- 0b01110111,// a
- 0b01111100,// b
- 0b01011000,// c
- 0b01011110,// d
- 0b01111001,// e
- 0b01110001,// f
- 0b00111101,// g
- 0b01110100,// h
- 0b00000100,// i
- 0b00011110,// j
- 0b01110101,// k
- 0b00111000,// l
- 0b01010101,// m
- 0b01010100,// n
- 0b01011100,// o
- 0b01110011,// p
- 0b01100111,// q
- 0b01010000,// r
- 0b01101101,// s
- 0b01111000,// t
- 0b00011100,// u
- 0b01100010,// v
- 0b01101010,// w
- 0b01110110,// x
- 0b01101110,// y
- 0b01011011,// z
- 0b00111001,// {
- 64, // |
- 0b00001111,// }
- 64, // ~
-};
-
-const uint8_t lut_7seg_inv[] = {
- 0,
- 64, // !
- 0b00010100,// "
- 64, // #
- 64, // $
- 64, // %
- 64, // &
- 0b00010000,// '
- 0b00001111,// (
- 0b00111001,// )
- 64, // *
- 64, // +
- 64, // ,
- 0b01000000,// -
- 64, // .
- 0b01010010,// /
- 0b00111111,// 0
- 0b00110000,// 1
- 0b01011011,// 2
- 0b01111001,// 3
- 0b01110100,// 4
- 0b01101101,// 5
- 0b01101111,// 6
- 0b00111000,// 7
- 0b01111111,// 8
- 0b01111101,// 9
- 64, // :
- 64, // ;
- 64, // <
- 64, // =
- 64, // >
- 64, // ?
- 64, // @
- 0b01111110,// A
- 0b01100111,// B
- 0b00001111,// C
- 0b01110011,// D
- 0b01001111,// E
- 0b01001110,// F
- 0b00101111,// G
- 0b01100110,// H
- 0b00110000,// I
- 0b00110011,// J
- 0b01101110,// K
- 0b00000111,// L
- 0b00101010,// M
- 0b01100010,// N
- 0b00111111,// O
- 0b01011110,// P
- 0b01111100,// Q
- 0b01000010,// R
- 0b01101101,// S
- 0b01000111,// T
- 0b00110111,// U
- 0b01010100,// V
- 0b00010101,// W
- 0b01110110,// X
- 0b01110101,// Y
- 0b01011011,// Z
- 0b00001111,// [
- 0b01100100,// \ //
- 0b00111001,// ]
- 0b00011100,// ^
- 0b00000001,// _
- 0b00000100,// `
- 0b01111110,// a
- 0b01100111,// b
- 0b01000011,// c
- 0b01110011,// d
- 0b01001111,// e
- 0b01001110,// f
- 0b00101111,// g
- 0b01100110,// h
- 0b00100000,// i
- 0b00110011,// j
- 0b01101110,// k
- 0b00000111,// l
- 0b01101010,// m
- 0b01100010,// n
- 0b01100011,// o
- 0b01011110,// p
- 0b01111100,// q
- 0b01000010,// r
- 0b01101101,// s
- 0b01000111,// t
- 0b00100011,// u
- 0b01010100,// v
- 0b01010101,// w
- 0b01110110,// x
- 0b01110101,// y
- 0b01011011,// z
- 0b00001111,// {
- 64, // |
- 0b00111001,// }
- 64, // ~
-};
-
-#define CMD_LOAD_TEXT 0x90
-#define CMD_SET_FREQUENCY 0x91
-#define CMD_RELOAD_TEXT 0x92
-#define CMD_SET_SCROLL_SPEED 0x93
-
-#define CMD_SHOW_CRC 0x9D
-#define CMD_REPORT_CRC 0x9E
-#define CMD_START_BOOTLOADER 0x9F
-
-#define CMD_SET_FREQUENCY_B2 0xA1
-#define CMD_SET_FREQUENCY_B3 0xA2
-
-// 32e6/5/50 = 128000 Hz
-// on -O0, ARR_MIN 66 => 95522.388Hz
-#define ARR_MIN 49
-#define ARR_MAX 6399
-// 32e6/5/6400 = 1000Hz
-
-uint16_t pre_buffer_a[5] ={0};
-uint16_t pre_buffer_b[5] ={0};
-uint8_t status = 0;
-
-uint16_t buffer_a[5] ={0};
-uint16_t buffer_b[5] ={0};
-uint8_t buffer_idx=0;
-
-#define MAX_TEXT_LEN 32
-uint8_t text[MAX_TEXT_LEN] ={0};
-uint8_t text_idx=0;
-uint8_t dp_pos=0;
-
-
-uint32_t target_freq=0;
-
-const uint16_t cathodes_a[5]={
- 0b1001100000000010,
- 0b1001100000000001,
- 0b1001000000000011,
- 0b1000100000000011,
- 0b0001100000000011
-};
-const uint16_t cathodes_b[5]={
- 0b1111000000000000,
- 0b1110100000000000,
- 0b1101100000000000,
- 0b1011100000000000,
- 0b0111100000000000
-};
-
-uint8_t inverted=0;
-uint8_t b1_held =0;
-uint8_t b2_held =0;
-
-/* USER CODE END PV */
-
-/* Private function prototypes -----------------------------------------------*/
-void SystemClock_Config(void);
-static void MX_GPIO_Init(void);
-static void MX_TIM2_Init(void);
-static void MX_USART2_UART_Init(void);
-static void MX_TIM21_Init(void);
-/* USER CODE BEGIN PFP */
-
-/* USER CODE END PFP */
-
-/* Private user code ---------------------------------------------------------*/
-/* USER CODE BEGIN 0 */
-
-__attribute__((naked,noreturn))
-void triggerBootloader(void){
-
-
- LL_TIM_DisableIT_UPDATE(TIM2);
- LL_TIM_DisableCounter(TIM2);
- LL_TIM_DeInit(TIM2);
-
- LL_TIM_DisableIT_UPDATE(TIM21);
- LL_TIM_DisableCounter(TIM21);
- LL_TIM_DeInit(TIM21);
-
- LL_USART_Disable(USART2);
- LL_USART_DeInit(USART2);
-
- GPIOA->ODR=0;
- GPIOB->ODR=0;
-
- LL_GPIO_DeInit(GPIOA);
- LL_GPIO_DeInit(GPIOB);
-
- LL_APB2_GRP1_ForceReset(LL_APB2_GRP1_PERIPH_SYSCFG);
- LL_APB1_GRP1_ForceReset(LL_APB1_GRP1_PERIPH_ALL);
- LL_APB2_GRP1_ReleaseReset(LL_APB2_GRP1_PERIPH_SYSCFG);
- LL_APB1_GRP1_ReleaseReset(LL_APB1_GRP1_PERIPH_ALL);
-
- LL_RCC_DeInit();
- SysTick->CTRL = 0;
- SysTick->LOAD = 0;
- SysTick->VAL = 0;
-
-#define SYSMEM 0x1FF00000
-
- __set_MSP(*(volatile uint32_t*) SYSMEM);
- ((void (*)(void)) (*((volatile uint32_t *)(SYSMEM + 4))))();
-
- __builtin_unreachable();
-}
-
-
-void setDigitPre(uint8_t digit, uint8_t val){
- if (val<32 || val>127) val=32;
-
- if (inverted == 1) {
- if (digit>=5) {
- pre_buffer_b[9-digit] = (lut_7seg_inv[val-32]<<4) | cathodes_b[9-digit];
- } else {
- pre_buffer_a[9-digit-5] = (lut_7seg_inv[val-32]<<4) | cathodes_a[9-digit-5];
- }
- } else {
- if (digit>=5) {
- pre_buffer_a[digit-5] = (lut_7seg[val-32]<<4) | cathodes_a[digit-5];
- } else {
- pre_buffer_b[digit] = (lut_7seg[val-32]<<4) | cathodes_b[digit];
- }
- }
-}
-void setDigitDirect(uint8_t digit, uint8_t val){
- if (val<32 || val>127) val=32;
-
- if (inverted == 1) {
- if (digit>=5) {
- buffer_b[9-digit] = (lut_7seg_inv[val-32]<<4) | cathodes_b[9-digit];
- } else {
- buffer_a[9-digit-5] = (lut_7seg_inv[val-32]<<4) | cathodes_a[9-digit-5];
- }
- } else {
- if (digit>=5) {
- buffer_a[digit-5] = (lut_7seg[val-32]<<4) | cathodes_a[digit-5];
- } else {
- buffer_b[digit] = (lut_7seg[val-32]<<4) | cathodes_b[digit];
- }
- }
-}
-
-// Main display matrix routine
-void TIM2_IRQHandler(void)
-{
- if (TIM2->SR & TIM_SR_UIF){
-
- GPIOA->ODR = buffer_a[buffer_idx];
- GPIOB->ODR = buffer_b[buffer_idx];
-
- buffer_idx ++;
- if (buffer_idx>=5) buffer_idx=0;
-
- TIM2->SR = ~TIM_DIER_UIE;
- return;
- }
-}
-
-void TIM21_IRQHandler(void){
- if (TIM21->SR & TIM_SR_UIF){
-
- uint8_t i = (LL_GPIO_ReadInputPort(GPIOC) & 1)?1:0;
-
- //if (i!=inverted)...
-
- inverted=i;
-
- TIM21->SR = ~TIM_DIER_UIE;
-
- //if (!latched) return; //don't intervene while waiting for latch
- //if (inverted)...
-
-#define btn_debounce 2
-#define btn_delay 42
-#define btn_repeat 10
-
-
- if ((LL_GPIO_ReadInputPort(GPIOB) & LL_GPIO_PIN_3)==0) {
- if (++b1_held == btn_debounce || b1_held == btn_delay) {
- if ( (USART2->ISR & USART_ISR_TXE) && b2_held==0 ) {
- USART2->TDR = 0x91 + inverted;
- }
- if (b1_held==btn_delay) b1_held -= btn_repeat;
- }
- } else b1_held=0;
- if ((LL_GPIO_ReadInputPort(GPIOC) & LL_GPIO_PIN_13)==0) {
- if (++b2_held == btn_debounce || b2_held == btn_delay) {
- if ( (USART2->ISR & USART_ISR_TXE) && b1_held==0) {
- USART2->TDR = 0x92 - inverted;
- }
- if (b2_held==btn_delay) b2_held -= btn_repeat;
- }
- } else b2_held=0;
-
- if (b1_held > btn_delay-btn_repeat && b2_held > btn_delay-btn_repeat){
- if ( (USART2->ISR & USART_ISR_TXE)) {
- // If triggering a reset the bootloader expects the line to be empty
- // Don't resend the command until buttons are released
- if (b1_heldTDR = 0x93;
- // Wipe our display too
- buffer_b[0] = 0;
- buffer_b[1] = 0;
- buffer_b[2] = 0;
- buffer_b[3] = 0;
- buffer_b[4] = 0;
- buffer_a[0] = 0;
- buffer_a[1] = 0;
- buffer_a[2] = 0;
- buffer_a[3] = 0;
- buffer_a[4] = 0;
- }
- b1_held = b2_held = btn_delay+1;
- }
- }
- }
-}
-
-static inline void setFrequency(void){
-
- if (target_freq<1 || target_freq>100000) return;
-
- uint32_t arr = round(6400000.0 / (float)target_freq) -1.0;
- if (arr > ARR_MAX) arr = ARR_MAX;
- if (arr < ARR_MIN) arr = ARR_MIN;
- TIM2->ARR= arr;
-}
-
-static inline void latchDisplay(void){
- buffer_b[0] = pre_buffer_b[0];
- buffer_b[1] = pre_buffer_b[1];
- buffer_b[2] = pre_buffer_b[2];
- buffer_b[3] = pre_buffer_b[3];
- buffer_b[4] = pre_buffer_b[4];
- buffer_a[0] = pre_buffer_a[0];
- buffer_a[1] = pre_buffer_a[1];
- buffer_a[2] = pre_buffer_a[2];
- buffer_a[3] = pre_buffer_a[3];
- buffer_a[4] = pre_buffer_a[4];
-
- if (!dp_pos) return;
-
- if (inverted){
- if (dp_pos>=5) {
- buffer_b[9-dp_pos] |=1 | cathodes_b[9-dp_pos];
- } else {
- buffer_a[9-dp_pos-5] |=(1<<14) | cathodes_a[9-dp_pos-5];
- }
- } else {
- if (dp_pos>5) {
- buffer_a[dp_pos-6] |=1<<14;
- } else {
- buffer_b[dp_pos-1] |=1;
- }
- }
-}
-static inline uint8_t waitForByte(void){
- while( !( USART2->ISR & USART_ISR_RXNE ) ) {};
- return USART2->RDR;
-}
-
-void transmitBlocking(uint8_t * c, size_t n){
- while (n--){
- while( !( USART2->ISR & USART_ISR_TXE ) ) {};
- USART2->TDR = *c++;
- }
-}
-
-static inline void waitForLatch(void){
- LL_USART_DisableDirectionRx(USART2);
- LL_GPIO_SetPinMode( GPIOA, LL_GPIO_PIN_3, LL_GPIO_MODE_INPUT );
-
- while (GPIOA->IDR & LL_GPIO_PIN_3) {}
-
- latchDisplay();
-
- // The latch byte is 0xFE with even parity, so as soon as the line returns high we can re-enable uart
- while (!(GPIOA->IDR & LL_GPIO_PIN_3)) {}
-
- LL_GPIO_SetPinMode( GPIOA, LL_GPIO_PIN_3, LL_GPIO_MODE_ALTERNATE );
- LL_USART_EnableDirectionRx(USART2);
-}
-
-static inline void parseByte(uint8_t x){
-
- if (x & 0x80) { // command byte
- status = x;
- switch (x) {
- case CMD_SHOW_CRC:
- case CMD_LOAD_TEXT:
- text_idx=0;
- dp_pos=0;
- memset(text, 0, MAX_TEXT_LEN);
-
- pre_buffer_b[0]=0;
- pre_buffer_b[1]=0;
- pre_buffer_b[2]=0;
- pre_buffer_b[3]=0;
- pre_buffer_b[4]=0;
- pre_buffer_a[0]=0;
- pre_buffer_a[1]=0;
- pre_buffer_a[2]=0;
- pre_buffer_a[3]=0;
- pre_buffer_a[4]=0;
-
- break;
- case CMD_RELOAD_TEXT:
- latchDisplay();
- break;
- case CMD_SET_SCROLL_SPEED:
- break;
-
- case CMD_SET_FREQUENCY:
- case CMD_SET_FREQUENCY_B2:
- case CMD_SET_FREQUENCY_B3:
- target_freq=0;
- break;
-
- case CMD_REPORT_CRC:
- transmitBlocking( (uint8_t*)0x8007ffc, 4);
- break;
-
- case CMD_START_BOOTLOADER:
- triggerBootloader();
- break;
-
- default:
- status=0;
- }
-
- if (x==CMD_SHOW_CRC) {
- uint32_t* crc = (uint32_t*)0x8007ffc;
- sprintf(text, "d %08lx", byteswap32(crc[0]));
- for (text_idx=0; text_idx<10; text_idx++)
- setDigitPre(text_idx, text[text_idx]);
- }
-
- return;
- }
-
- // Process data
- switch(status){
-
- case CMD_SHOW_CRC:
- case CMD_LOAD_TEXT:
- if (x=='\n' || x==0) {
- waitForLatch();
- return;
- }
- if (x=='.') {
- dp_pos = text_idx;
- return;
- }
- if(text_idx > MAX_TEXT_LEN) return;
-
- if (text_idx < 10) setDigitPre(text_idx, x);
- text[text_idx++] = x;
- return;
-
- case CMD_SET_SCROLL_SPEED:
- return;
-
- case CMD_SET_FREQUENCY:
- status=CMD_SET_FREQUENCY_B2;
- target_freq |= x<<14;
- return;
- case CMD_SET_FREQUENCY_B2:
- status=CMD_SET_FREQUENCY_B3;
- target_freq |= x<<7;
- return;
- case CMD_SET_FREQUENCY_B3:
- status=0;
- target_freq |= x;
- setFrequency();
- return;
- }
-
-
-}
-
-
-/* USER CODE END 0 */
-
-/**
- * @brief The application entry point.
- * @retval int
- */
-int main(void)
-{
- /* USER CODE BEGIN 1 */
-
- /* USER CODE END 1 */
-
- /* MCU Configuration--------------------------------------------------------*/
-
- /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
-
- LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_SYSCFG);
- LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_PWR);
-
- /* System interrupt init*/
-
- /* USER CODE BEGIN Init */
-
- /* USER CODE END Init */
-
- /* Configure the system clock */
- SystemClock_Config();
-
- /* USER CODE BEGIN SysInit */
-
- /* USER CODE END SysInit */
-
- /* Initialize all configured peripherals */
- MX_GPIO_Init();
- MX_TIM2_Init();
- MX_USART2_UART_Init();
- MX_TIM21_Init();
- /* USER CODE BEGIN 2 */
-
-
- // (HAL_TIM_Base_Start_IT(&htim2)
-
-
- LL_TIM_EnableIT_UPDATE(TIM2); //TIM2->DIER |= TIM_DIER_UIE;
- LL_TIM_EnableCounter(TIM2); //TIM2->CR1 |= TIM_CR1_CEN;
-
- LL_TIM_EnableIT_UPDATE(TIM21);
- LL_TIM_EnableCounter(TIM21);
-
-// setDigitDirect(0, 'l');
-// setDigitDirect(1, 'o');
-// setDigitDirect(2, 'l');
-// setDigitDirect(3, 'o');
-// setDigitDirect(4, 'l');
-// setDigitDirect(5, 'o');
-// setDigitDirect(6, 'l');
-// setDigitDirect(7, 'o');
-// setDigitDirect(8, 'l');
-// setDigitDirect(9, 'o');
-
-
- //buffer_a[2] |= 1<<14;
- //buffer_b[2] |= 1;
-
-
- /* USER CODE END 2 */
-
- /* Infinite loop */
- /* USER CODE BEGIN WHILE */
- while (1)
- {
-
- uint8_t x = waitForByte();
- parseByte(x);
-
-
- /* USER CODE END WHILE */
-
- /* USER CODE BEGIN 3 */
- }
- /* USER CODE END 3 */
-}
-
-/**
- * @brief System Clock Configuration
- * @retval None
- */
-void SystemClock_Config(void)
-{
- LL_FLASH_SetLatency(LL_FLASH_LATENCY_1);
-
- if(LL_FLASH_GetLatency() != LL_FLASH_LATENCY_1)
- {
- Error_Handler();
- }
- LL_PWR_SetRegulVoltageScaling(LL_PWR_REGU_VOLTAGE_SCALE1);
- LL_RCC_HSI_Enable();
-
- /* Wait till HSI is ready */
- while(LL_RCC_HSI_IsReady() != 1)
- {
-
- }
- LL_RCC_HSI_SetCalibTrimming(16);
- LL_RCC_PLL_ConfigDomain_SYS(LL_RCC_PLLSOURCE_HSI, LL_RCC_PLL_MUL_4, LL_RCC_PLL_DIV_2);
- LL_RCC_PLL_Enable();
-
- /* Wait till PLL is ready */
- while(LL_RCC_PLL_IsReady() != 1)
- {
-
- }
- LL_RCC_SetAHBPrescaler(LL_RCC_SYSCLK_DIV_1);
- LL_RCC_SetAPB1Prescaler(LL_RCC_APB1_DIV_1);
- LL_RCC_SetAPB2Prescaler(LL_RCC_APB2_DIV_1);
- LL_RCC_SetSysClkSource(LL_RCC_SYS_CLKSOURCE_PLL);
-
- /* Wait till System clock is ready */
- while(LL_RCC_GetSysClkSource() != LL_RCC_SYS_CLKSOURCE_STATUS_PLL)
- {
-
- }
-
- LL_Init1msTick(32000000);
-
- LL_SetSystemCoreClock(32000000);
- LL_RCC_SetUSARTClockSource(LL_RCC_USART2_CLKSOURCE_PCLK1);
-}
-
-/**
- * @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 */
-
- LL_TIM_InitTypeDef TIM_InitStruct = {0};
-
- /* Peripheral clock enable */
- LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_TIM2);
-
- /* TIM2 interrupt Init */
- NVIC_SetPriority(TIM2_IRQn, 0);
- NVIC_EnableIRQ(TIM2_IRQn);
-
- /* USER CODE BEGIN TIM2_Init 1 */
-
- /* USER CODE END TIM2_Init 1 */
- TIM_InitStruct.Prescaler = 0;
- TIM_InitStruct.CounterMode = LL_TIM_COUNTERMODE_UP;
- TIM_InitStruct.Autoreload = 319;
- TIM_InitStruct.ClockDivision = LL_TIM_CLOCKDIVISION_DIV1;
- LL_TIM_Init(TIM2, &TIM_InitStruct);
- LL_TIM_EnableARRPreload(TIM2);
- LL_TIM_SetClockSource(TIM2, LL_TIM_CLOCKSOURCE_INTERNAL);
- LL_TIM_SetTriggerOutput(TIM2, LL_TIM_TRGO_RESET);
- LL_TIM_DisableMasterSlaveMode(TIM2);
- /* USER CODE BEGIN TIM2_Init 2 */
-
- /* USER CODE END TIM2_Init 2 */
-
-}
-
-/**
- * @brief TIM21 Initialization Function
- * @param None
- * @retval None
- */
-static void MX_TIM21_Init(void)
-{
-
- /* USER CODE BEGIN TIM21_Init 0 */
-
- /* USER CODE END TIM21_Init 0 */
-
- LL_TIM_InitTypeDef TIM_InitStruct = {0};
-
- /* Peripheral clock enable */
- LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_TIM21);
-
- /* TIM21 interrupt Init */
- NVIC_SetPriority(TIM21_IRQn, 0);
- NVIC_EnableIRQ(TIM21_IRQn);
-
- /* USER CODE BEGIN TIM21_Init 1 */
-
- /* USER CODE END TIM21_Init 1 */
- TIM_InitStruct.Prescaler = 31;
- TIM_InitStruct.CounterMode = LL_TIM_COUNTERMODE_UP;
- TIM_InitStruct.Autoreload = 19999;
- TIM_InitStruct.ClockDivision = LL_TIM_CLOCKDIVISION_DIV1;
- LL_TIM_Init(TIM21, &TIM_InitStruct);
- LL_TIM_DisableARRPreload(TIM21);
- LL_TIM_SetClockSource(TIM21, LL_TIM_CLOCKSOURCE_INTERNAL);
- LL_TIM_SetTriggerOutput(TIM21, LL_TIM_TRGO_RESET);
- LL_TIM_DisableMasterSlaveMode(TIM21);
- /* USER CODE BEGIN TIM21_Init 2 */
-
- /* USER CODE END TIM21_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 */
-
- LL_USART_InitTypeDef USART_InitStruct = {0};
-
- LL_GPIO_InitTypeDef GPIO_InitStruct = {0};
-
- /* Peripheral clock enable */
- LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_USART2);
-
- LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOA);
- /**USART2 GPIO Configuration
- PA2 ------> USART2_TX
- PA3 ------> USART2_RX
- */
- GPIO_InitStruct.Pin = LL_GPIO_PIN_2;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_ALTERNATE;
- GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
- GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
- GPIO_InitStruct.Alternate = LL_GPIO_AF_4;
- LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
-
- GPIO_InitStruct.Pin = LL_GPIO_PIN_3;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_ALTERNATE;
- GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
- GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
- GPIO_InitStruct.Alternate = LL_GPIO_AF_4;
- LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
-
- /* USER CODE BEGIN USART2_Init 1 */
-
- // Disable overrun detection, for two reasons
- // 1. The command structure should always sort itself out
- // 2. It makes interactive debugging the assembled clock a lot easier
- USART2->CR3 = USART_CR3_OVRDIS_Msk;
-
- /* USER CODE END USART2_Init 1 */
- USART_InitStruct.BaudRate = 115200;
- USART_InitStruct.DataWidth = LL_USART_DATAWIDTH_9B;
- USART_InitStruct.StopBits = LL_USART_STOPBITS_1;
- USART_InitStruct.Parity = LL_USART_PARITY_EVEN;
- USART_InitStruct.TransferDirection = LL_USART_DIRECTION_TX_RX;
- USART_InitStruct.HardwareFlowControl = LL_USART_HWCONTROL_NONE;
- USART_InitStruct.OverSampling = LL_USART_OVERSAMPLING_16;
- LL_USART_Init(USART2, &USART_InitStruct);
- LL_USART_ConfigAsyncMode(USART2);
- LL_USART_Enable(USART2);
- /* USER CODE BEGIN USART2_Init 2 */
-
- /* USER CODE END USART2_Init 2 */
-
-}
-
-/**
- * @brief GPIO Initialization Function
- * @param None
- * @retval None
- */
-static void MX_GPIO_Init(void)
-{
- LL_GPIO_InitTypeDef GPIO_InitStruct = {0};
-
- /* GPIO Ports Clock Enable */
- LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOC);
- LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOA);
- LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOB);
-
- /**/
- LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_0);
-
- /**/
- LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_1);
-
- /**/
- LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_4);
-
- /**/
- LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_5);
-
- /**/
- LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_6);
-
- /**/
- LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_7);
-
- /**/
- LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_0);
-
- /**/
- LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_1);
-
- /**/
- LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_10);
-
- /**/
- LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_11);
-
- /**/
- LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_12);
-
- /**/
- LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_13);
-
- /**/
- LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_14);
-
- /**/
- LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_15);
-
- /**/
- LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_8);
-
- /**/
- LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_9);
-
- /**/
- LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_10);
-
- /**/
- LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_11);
-
- /**/
- LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_12);
-
- /**/
- LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_15);
-
- /**/
- LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_4);
-
- /**/
- LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_5);
-
- /**/
- LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_6);
-
- /**/
- LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_7);
-
- /**/
- LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_8);
-
- /**/
- LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_9);
-
- /**/
- GPIO_InitStruct.Pin = LL_GPIO_PIN_0;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_INPUT;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_UP;
- LL_GPIO_Init(GPIOC, &GPIO_InitStruct);
-
- /**/
- GPIO_InitStruct.Pin = LL_GPIO_PIN_13;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_INPUT;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_UP;
- LL_GPIO_Init(GPIOC, &GPIO_InitStruct);
-
- /**/
- GPIO_InitStruct.Pin = LL_GPIO_PIN_0;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
- GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
- GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
- LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
-
- /**/
- GPIO_InitStruct.Pin = LL_GPIO_PIN_1;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
- GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
- GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
- LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
-
- /**/
- GPIO_InitStruct.Pin = LL_GPIO_PIN_4;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
- GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
- GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
- LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
-
- /**/
- GPIO_InitStruct.Pin = LL_GPIO_PIN_5;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
- GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
- GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
- LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
-
- /**/
- GPIO_InitStruct.Pin = LL_GPIO_PIN_6;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
- GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
- GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
- LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
-
- /**/
- GPIO_InitStruct.Pin = LL_GPIO_PIN_7;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
- GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
- GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
- LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
-
- /**/
- GPIO_InitStruct.Pin = LL_GPIO_PIN_0;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
- GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
- GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
- LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
-
- /**/
- GPIO_InitStruct.Pin = LL_GPIO_PIN_1;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
- GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
- GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
- LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
-
- /**/
- GPIO_InitStruct.Pin = LL_GPIO_PIN_10;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
- GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
- GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
- LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
-
- /**/
- GPIO_InitStruct.Pin = LL_GPIO_PIN_11;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
- GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
- GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
- LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
-
- /**/
- GPIO_InitStruct.Pin = LL_GPIO_PIN_12;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
- GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
- GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
- LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
-
- /**/
- GPIO_InitStruct.Pin = LL_GPIO_PIN_13;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
- GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
- GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
- LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
-
- /**/
- GPIO_InitStruct.Pin = LL_GPIO_PIN_14;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
- GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
- GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
- LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
-
- /**/
- GPIO_InitStruct.Pin = LL_GPIO_PIN_15;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
- GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
- GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
- LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
-
- /**/
- GPIO_InitStruct.Pin = LL_GPIO_PIN_8;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
- GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
- GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
- LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
-
- /**/
- GPIO_InitStruct.Pin = LL_GPIO_PIN_9;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
- GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
- GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
- LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
-
- /**/
- GPIO_InitStruct.Pin = LL_GPIO_PIN_10;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
- GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
- GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
- LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
-
- /**/
- GPIO_InitStruct.Pin = LL_GPIO_PIN_11;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
- GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
- GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
- LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
-
- /**/
- GPIO_InitStruct.Pin = LL_GPIO_PIN_12;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
- GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
- GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
- LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
-
- /**/
- GPIO_InitStruct.Pin = LL_GPIO_PIN_15;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
- GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
- GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
- LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
-
- /**/
- GPIO_InitStruct.Pin = LL_GPIO_PIN_3;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_INPUT;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_UP;
- LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
-
- /**/
- GPIO_InitStruct.Pin = LL_GPIO_PIN_4;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
- GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
- GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
- LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
-
- /**/
- GPIO_InitStruct.Pin = LL_GPIO_PIN_5;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
- GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
- GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
- LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
-
- /**/
- GPIO_InitStruct.Pin = LL_GPIO_PIN_6;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
- GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
- GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
- LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
-
- /**/
- GPIO_InitStruct.Pin = LL_GPIO_PIN_7;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
- GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
- GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
- LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
-
- /**/
- GPIO_InitStruct.Pin = LL_GPIO_PIN_8;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
- GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
- GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
- LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
-
- /**/
- GPIO_InitStruct.Pin = LL_GPIO_PIN_9;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
- GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
- GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
- LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
-
-#define DISABLE_SWCLK
-
-#ifdef DISABLE_SWCLK
-
- if ( (LL_GPIO_ReadInputPort(GPIOC) & LL_GPIO_PIN_13)!=0 && ((LL_GPIO_ReadInputPort(GPIOB) & LL_GPIO_PIN_3)!=0) ){
- GPIO_InitStruct.Pin = LL_GPIO_PIN_14;
- GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
- GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
- GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
- LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
- }
-#endif
-}
-
-/* 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 */
-
- /* 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****/
+/* 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"
+
+/* Private includes ----------------------------------------------------------*/
+/* USER CODE BEGIN Includes */
+#include
+#include
+#include
+/* 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 */
+
+#define byteswap32(x) \
+ ( ((x & 0xff000000) >> 24) | ((x & 0x00ff0000) >> 8) \
+ | ((x & 0x0000ff00) << 8) | ((x & 0x000000ff) << 24))
+
+/* USER CODE END PM */
+
+/* Private variables ---------------------------------------------------------*/
+
+/* USER CODE BEGIN PV */
+
+
+const uint8_t lut_7seg[] = {
+ 0,
+ 64, // !
+ 0b00100010,// "
+ 64, // #
+ 64, // $
+ 64, // %
+ 64, // &
+ 0b00000010,// '
+ 0b00111001,// (
+ 0b00001111,// )
+ 64, // *
+ 64, // +
+ 64, // ,
+ 0b01000000,// -
+ 64, // .
+ 0b01010010,// /
+ 0b00111111,// 0
+ 0b00000110,// 1
+ 0b01011011,// 2
+ 0b01001111,// 3
+ 0b01100110,// 4
+ 0b01101101,// 5
+ 0b01111101,// 6
+ 0b00000111,// 7
+ 0b01111111,// 8
+ 0b01101111,// 9
+ 64, // :
+ 64, // ;
+ 64, // <
+ 64, // =
+ 64, // >
+ 64, // ?
+ 64, // @
+ 0b01110111,// A
+ 0b01111100,// B
+ 0b00111001,// C
+ 0b01011110,// D
+ 0b01111001,// E
+ 0b01110001,// F
+ 0b00111101,// G
+ 0b01110100,// H
+ 0b00000110,// I
+ 0b00011110,// J
+ 0b01110101,// K
+ 0b00111000,// L
+ 0b00010101,// M
+ 0b01010100,// N
+ 0b00111111,// O
+ 0b01110011,// P
+ 0b01100111,// Q
+ 0b01010000,// R
+ 0b01101101,// S
+ 0b01111000,// T
+ 0b00111110,// U
+ 0b01100010,// V
+ 0b00101010,// W
+ 0b01110110,// X
+ 0b01101110,// Y
+ 0b01011011,// Z
+ 0b00111001,// [
+ 0b01100100,// \ //
+ 0b00001111,// ]
+ 0b00100011,// ^
+ 0b00001000,// _
+ 0b00100000,// `
+ 0b01110111,// a
+ 0b01111100,// b
+ 0b01011000,// c
+ 0b01011110,// d
+ 0b01111001,// e
+ 0b01110001,// f
+ 0b00111101,// g
+ 0b01110100,// h
+ 0b00000100,// i
+ 0b00011110,// j
+ 0b01110101,// k
+ 0b00111000,// l
+ 0b01010101,// m
+ 0b01010100,// n
+ 0b01011100,// o
+ 0b01110011,// p
+ 0b01100111,// q
+ 0b01010000,// r
+ 0b01101101,// s
+ 0b01111000,// t
+ 0b00011100,// u
+ 0b01100010,// v
+ 0b01101010,// w
+ 0b01110110,// x
+ 0b01101110,// y
+ 0b01011011,// z
+ 0b00111001,// {
+ 64, // |
+ 0b00001111,// }
+ 64, // ~
+};
+
+const uint8_t lut_7seg_inv[] = {
+ 0,
+ 64, // !
+ 0b00010100,// "
+ 64, // #
+ 64, // $
+ 64, // %
+ 64, // &
+ 0b00010000,// '
+ 0b00001111,// (
+ 0b00111001,// )
+ 64, // *
+ 64, // +
+ 64, // ,
+ 0b01000000,// -
+ 64, // .
+ 0b01010010,// /
+ 0b00111111,// 0
+ 0b00110000,// 1
+ 0b01011011,// 2
+ 0b01111001,// 3
+ 0b01110100,// 4
+ 0b01101101,// 5
+ 0b01101111,// 6
+ 0b00111000,// 7
+ 0b01111111,// 8
+ 0b01111101,// 9
+ 64, // :
+ 64, // ;
+ 64, // <
+ 64, // =
+ 64, // >
+ 64, // ?
+ 64, // @
+ 0b01111110,// A
+ 0b01100111,// B
+ 0b00001111,// C
+ 0b01110011,// D
+ 0b01001111,// E
+ 0b01001110,// F
+ 0b00101111,// G
+ 0b01100110,// H
+ 0b00110000,// I
+ 0b00110011,// J
+ 0b01101110,// K
+ 0b00000111,// L
+ 0b00101010,// M
+ 0b01100010,// N
+ 0b00111111,// O
+ 0b01011110,// P
+ 0b01111100,// Q
+ 0b01000010,// R
+ 0b01101101,// S
+ 0b01000111,// T
+ 0b00110111,// U
+ 0b01010100,// V
+ 0b00010101,// W
+ 0b01110110,// X
+ 0b01110101,// Y
+ 0b01011011,// Z
+ 0b00001111,// [
+ 0b01100100,// \ //
+ 0b00111001,// ]
+ 0b00011100,// ^
+ 0b00000001,// _
+ 0b00000100,// `
+ 0b01111110,// a
+ 0b01100111,// b
+ 0b01000011,// c
+ 0b01110011,// d
+ 0b01001111,// e
+ 0b01001110,// f
+ 0b00101111,// g
+ 0b01100110,// h
+ 0b00100000,// i
+ 0b00110011,// j
+ 0b01101110,// k
+ 0b00000111,// l
+ 0b01101010,// m
+ 0b01100010,// n
+ 0b01100011,// o
+ 0b01011110,// p
+ 0b01111100,// q
+ 0b01000010,// r
+ 0b01101101,// s
+ 0b01000111,// t
+ 0b00100011,// u
+ 0b01010100,// v
+ 0b01010101,// w
+ 0b01110110,// x
+ 0b01110101,// y
+ 0b01011011,// z
+ 0b00001111,// {
+ 64, // |
+ 0b00111001,// }
+ 64, // ~
+};
+
+#define CMD_LOAD_TEXT 0x90
+#define CMD_SET_FREQUENCY 0x91
+#define CMD_RELOAD_TEXT 0x92
+#define CMD_SET_SCROLL_SPEED 0x93
+#define CMD_SET_SEG_BALANCE 0x94 // + 9 data bytes: duty (of 16) for 0..8 lit segments
+
+#define CMD_SHOW_CRC 0x9D
+#define CMD_REPORT_CRC 0x9E
+#define CMD_START_BOOTLOADER 0x9F
+
+#define CMD_SET_FREQUENCY_B2 0xA1
+#define CMD_SET_FREQUENCY_B3 0xA2
+
+// 32e6/5/50 = 128000 Hz
+// on -O0, ARR_MIN 66 => 95522.388Hz
+#define ARR_MIN 49
+#define ARR_MAX 6399
+// 32e6/5/6400 = 1000Hz
+
+uint16_t pre_buffer_a[5] ={0};
+uint16_t pre_buffer_b[5] ={0};
+uint8_t status = 0;
+
+uint16_t buffer_a[5] ={0};
+uint16_t buffer_b[5] ={0};
+uint8_t buffer_idx=0;
+
+#define MAX_TEXT_LEN 32
+uint8_t text[MAX_TEXT_LEN] ={0};
+uint8_t text_idx=0;
+uint8_t dp_pos=0;
+
+
+uint32_t target_freq=0;
+
+const uint16_t cathodes_a[5]={
+ 0b1001100000000010,
+ 0b1001100000000001,
+ 0b1001000000000011,
+ 0b1000100000000011,
+ 0b0001100000000011
+};
+const uint16_t cathodes_b[5]={
+ 0b1111000000000000,
+ 0b1110100000000000,
+ 0b1101100000000000,
+ 0b1011100000000000,
+ 0b0111100000000000
+};
+
+// --- Per-segment brightness balance (received from the time board) ---------------------------
+// The shared LED rail makes digits with FEWER lit segments glow brighter (per-digit return-path
+// drop). The time board computes the compensation and forwards a 9-entry duty table over the
+// UART (CMD_SET_SEG_BALANCE + 9 data bytes: lit cycles of 16 for a digit with 0..8 lit segments);
+// this board just applies it: the scan ISR runs a D-cycle dither where each digit is lit in its
+// table share of cycles. D adapts to the commanded scan rate so the dither never strobes
+// (>= ~200 Hz), and the identity table (all 16, the default) is bit-identical to stock.
+// Segment bits per port (cathode selects and all else pass through unmasked, verbatim):
+// port A: segments bits 4..10 + DP bit 14 (cathodes_a use bits 15,12,11,1,0)
+// port B: segments bits 4..10 + DP bit 0 (cathodes_b use bits 15..11)
+#define SEGBAL_MASK_A 0x47F0u
+#define SEGBAL_MASK_B 0x07F1u
+uint8_t segbal_table[9] = {16,16,16,16,16,16,16,16,16};
+uint8_t segbal_stage[9]; // RX staging — committed atomically on the 9th byte
+uint8_t segbal_stage_idx = 0;
+uint8_t segbal_duty_a[5] = {16,16,16,16,16}; // lit cycles (of 16) per column, per port
+uint8_t segbal_duty_b[5] = {16,16,16,16,16};
+uint8_t segbal_cycle = 0; // advances once per 5-column sweep, wraps at the depth
+
+// rank(c) for D=16 (bit-reversed c); for D=8/4 shift right by 1/2. A digit is lit on the cycles
+// whose rank falls in ITS OWN window of size s: (rank + phase) mod D < s, phase fixed per digit
+// (port x column). Two properties, both hardware-proven the hard way:
+// - NESTED: when a duty steps (segment count changed, duty table re-forwarded) the lit set
+// gains/loses exactly one cycle instead of reshuffling — a reshuffling spread (e.g.
+// (c*s) % D < s, the first ship) visibly re-phases the light at each step.
+// - DECORRELATED: distinct phases keep the digits' lit cycles spread across the period. With
+// one shared window (no rotation — the second ship) every digit lit the same low-rank cycles:
+// high-rank cycles went ALL-dark (whole-row 200 Hz comb), the shared rail saw a sawtooth that
+// stepped with the time board's content, and the row blipped at 1 Hz with per-digit brightness
+// off calibration.
+// A rotated rank window bit-reverses to a van-der-Corput run, so spacing stays near-even at
+// every s. Identity (duty 16 = s = D) lights every cycle regardless of phase = stock-identical.
+static const uint8_t SEGBAL_REV16[16] = {0,8,4,12,2,10,6,14,1,9,5,13,3,11,7,15};
+static const uint8_t SEGBAL_PH_A[5] = {0, 6, 12, 2, 8}; // idx*3 mod 16, idx = col*2 + port —
+static const uint8_t SEGBAL_PH_B[5] = {3, 9, 15, 5, 11}; // all 10 digit phases distinct
+
+static uint8_t segbal_pop(uint16_t v){
+ uint8_t n = 0;
+ while (v) { n += v & 1u; v >>= 1; }
+ return n;
+}
+// Recompute the per-column duties from the CURRENT buffer words. Called wherever the buffers
+// change (latch, direct writes, wipe) — cheap, and the ISR itself never does the counting.
+static void segbalRecompute(void){
+ for (uint8_t i = 0; i < 5; i++) {
+ uint8_t na = segbal_pop(buffer_a[i] & SEGBAL_MASK_A);
+ uint8_t nb = segbal_pop(buffer_b[i] & SEGBAL_MASK_B);
+ segbal_duty_a[i] = segbal_table[na > 8 ? 8 : na];
+ segbal_duty_b[i] = segbal_table[nb > 8 ? 8 : nb];
+ }
+}
+// Dither depth for the commanded scan rate (TIM2 clock 6.4 MHz; setFrequency retunes ARR from
+// 1..100 kHz): deepest of {16,8,4} keeping the dither >= ~200 Hz, else 1 = balancing off.
+static uint8_t segbal_depth(void){
+ uint32_t step = 6400000u / ((uint32_t)TIM2->ARR + 1u);
+ if (step >= 16000u) return 16u;
+ if (step >= 8000u) return 8u;
+ if (step >= 4000u) return 4u;
+ return 1u;
+}
+
+uint8_t inverted=0;
+uint8_t b1_held =0;
+uint8_t b2_held =0;
+
+/* USER CODE END PV */
+
+/* Private function prototypes -----------------------------------------------*/
+void SystemClock_Config(void);
+static void MX_GPIO_Init(void);
+static void MX_TIM2_Init(void);
+static void MX_USART2_UART_Init(void);
+static void MX_TIM21_Init(void);
+/* USER CODE BEGIN PFP */
+
+/* USER CODE END PFP */
+
+/* Private user code ---------------------------------------------------------*/
+/* USER CODE BEGIN 0 */
+
+__attribute__((naked,noreturn))
+void triggerBootloader(void){
+
+
+ LL_TIM_DisableIT_UPDATE(TIM2);
+ LL_TIM_DisableCounter(TIM2);
+ LL_TIM_DeInit(TIM2);
+
+ LL_TIM_DisableIT_UPDATE(TIM21);
+ LL_TIM_DisableCounter(TIM21);
+ LL_TIM_DeInit(TIM21);
+
+ LL_USART_Disable(USART2);
+ LL_USART_DeInit(USART2);
+
+ GPIOA->ODR=0;
+ GPIOB->ODR=0;
+
+ LL_GPIO_DeInit(GPIOA);
+ LL_GPIO_DeInit(GPIOB);
+
+ LL_APB2_GRP1_ForceReset(LL_APB2_GRP1_PERIPH_SYSCFG);
+ LL_APB1_GRP1_ForceReset(LL_APB1_GRP1_PERIPH_ALL);
+ LL_APB2_GRP1_ReleaseReset(LL_APB2_GRP1_PERIPH_SYSCFG);
+ LL_APB1_GRP1_ReleaseReset(LL_APB1_GRP1_PERIPH_ALL);
+
+ LL_RCC_DeInit();
+ SysTick->CTRL = 0;
+ SysTick->LOAD = 0;
+ SysTick->VAL = 0;
+
+#define SYSMEM 0x1FF00000
+
+ __set_MSP(*(volatile uint32_t*) SYSMEM);
+ ((void (*)(void)) (*((volatile uint32_t *)(SYSMEM + 4))))();
+
+ __builtin_unreachable();
+}
+
+
+void setDigitPre(uint8_t digit, uint8_t val){
+ if (val<32 || val>127) val=32;
+
+ if (inverted == 1) {
+ if (digit>=5) {
+ pre_buffer_b[9-digit] = (lut_7seg_inv[val-32]<<4) | cathodes_b[9-digit];
+ } else {
+ pre_buffer_a[9-digit-5] = (lut_7seg_inv[val-32]<<4) | cathodes_a[9-digit-5];
+ }
+ } else {
+ if (digit>=5) {
+ pre_buffer_a[digit-5] = (lut_7seg[val-32]<<4) | cathodes_a[digit-5];
+ } else {
+ pre_buffer_b[digit] = (lut_7seg[val-32]<<4) | cathodes_b[digit];
+ }
+ }
+}
+void setDigitDirect(uint8_t digit, uint8_t val){
+ if (val<32 || val>127) val=32;
+ // (duties refreshed at the end — this writes the live buffers directly)
+
+ if (inverted == 1) {
+ if (digit>=5) {
+ buffer_b[9-digit] = (lut_7seg_inv[val-32]<<4) | cathodes_b[9-digit];
+ } else {
+ buffer_a[9-digit-5] = (lut_7seg_inv[val-32]<<4) | cathodes_a[9-digit-5];
+ }
+ } else {
+ if (digit>=5) {
+ buffer_a[digit-5] = (lut_7seg[val-32]<<4) | cathodes_a[digit-5];
+ } else {
+ buffer_b[digit] = (lut_7seg[val-32]<<4) | cathodes_b[digit];
+ }
+ }
+ segbalRecompute();
+}
+
+// Main display matrix routine. seg-balance: each digit is lit in duty/16 of the dither cycles —
+// rescaled to the depth D for this scan rate — with its cathode-select bits present in EVERY
+// cycle. The identity table gives duty 16 = always lit = bit-identical to the stock scan.
+void TIM2_IRQHandler(void)
+{
+ if (TIM2->SR & TIM_SR_UIF){
+
+ uint16_t wa = buffer_a[buffer_idx];
+ uint16_t wb = buffer_b[buffer_idx];
+ uint8_t D = segbal_depth();
+ // duty on the 0..16 scale -> lit cycles of D (D=16 exact; lit digits keep >= 1 cycle)
+ uint8_t sa = (uint8_t)(((uint16_t)segbal_duty_a[buffer_idx] * D + 8u) >> 4);
+ uint8_t sb = (uint8_t)(((uint16_t)segbal_duty_b[buffer_idx] * D + 8u) >> 4);
+ if (segbal_duty_a[buffer_idx] && !sa) sa = 1;
+ if (segbal_duty_b[buffer_idx] && !sb) sb = 1;
+ uint8_t r = (uint8_t)(SEGBAL_REV16[segbal_cycle % D] >> ((D == 16u) ? 0 : (D == 8u) ? 1 : 2));
+ if (((uint8_t)(r + SEGBAL_PH_A[buffer_idx]) & (D - 1u)) >= sa) wa &= (uint16_t)~SEGBAL_MASK_A;
+ if (((uint8_t)(r + SEGBAL_PH_B[buffer_idx]) & (D - 1u)) >= sb) wb &= (uint16_t)~SEGBAL_MASK_B;
+
+ GPIOA->ODR = wa;
+ GPIOB->ODR = wb;
+
+ buffer_idx ++;
+ if (buffer_idx>=5) { buffer_idx=0; segbal_cycle = (uint8_t)((segbal_cycle + 1) & 15); }
+
+ TIM2->SR = ~TIM_DIER_UIE;
+ return;
+ }
+}
+
+void TIM21_IRQHandler(void){
+ if (TIM21->SR & TIM_SR_UIF){
+
+ uint8_t i = (LL_GPIO_ReadInputPort(GPIOC) & 1)?1:0;
+
+ //if (i!=inverted)...
+
+ inverted=i;
+
+ TIM21->SR = ~TIM_DIER_UIE;
+
+ //if (!latched) return; //don't intervene while waiting for latch
+ //if (inverted)...
+
+#define btn_debounce 2
+#define btn_delay 42
+#define btn_repeat 10
+
+
+ if ((LL_GPIO_ReadInputPort(GPIOB) & LL_GPIO_PIN_3)==0) {
+ if (++b1_held == btn_debounce || b1_held == btn_delay) {
+ if ( (USART2->ISR & USART_ISR_TXE) && b2_held==0 ) {
+ USART2->TDR = 0x91 + inverted;
+ }
+ if (b1_held==btn_delay) b1_held -= btn_repeat;
+ }
+ } else b1_held=0;
+ if ((LL_GPIO_ReadInputPort(GPIOC) & LL_GPIO_PIN_13)==0) {
+ if (++b2_held == btn_debounce || b2_held == btn_delay) {
+ if ( (USART2->ISR & USART_ISR_TXE) && b1_held==0) {
+ USART2->TDR = 0x92 - inverted;
+ }
+ if (b2_held==btn_delay) b2_held -= btn_repeat;
+ }
+ } else b2_held=0;
+
+ if (b1_held > btn_delay-btn_repeat && b2_held > btn_delay-btn_repeat){
+ if ( (USART2->ISR & USART_ISR_TXE)) {
+ // If triggering a reset the bootloader expects the line to be empty
+ // Don't resend the command until buttons are released
+ if (b1_heldTDR = 0x93;
+ // Wipe our display too
+ buffer_b[0] = 0;
+ buffer_b[1] = 0;
+ buffer_b[2] = 0;
+ buffer_b[3] = 0;
+ buffer_b[4] = 0;
+ buffer_a[0] = 0;
+ buffer_a[1] = 0;
+ buffer_a[2] = 0;
+ buffer_a[3] = 0;
+ buffer_a[4] = 0;
+ segbalRecompute();
+ }
+ b1_held = b2_held = btn_delay+1;
+ }
+ }
+ }
+}
+
+static inline void setFrequency(void){
+
+ if (target_freq<1 || target_freq>100000) return;
+
+ uint32_t arr = round(6400000.0 / (float)target_freq) -1.0;
+ if (arr > ARR_MAX) arr = ARR_MAX;
+ if (arr < ARR_MIN) arr = ARR_MIN;
+ TIM2->ARR= arr;
+}
+
+static inline void latchDisplay(void){
+ buffer_b[0] = pre_buffer_b[0];
+ buffer_b[1] = pre_buffer_b[1];
+ buffer_b[2] = pre_buffer_b[2];
+ buffer_b[3] = pre_buffer_b[3];
+ buffer_b[4] = pre_buffer_b[4];
+ buffer_a[0] = pre_buffer_a[0];
+ buffer_a[1] = pre_buffer_a[1];
+ buffer_a[2] = pre_buffer_a[2];
+ 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) {
+ buffer_b[9-dp_pos] |=1 | cathodes_b[9-dp_pos];
+ } else {
+ buffer_a[9-dp_pos-5] |=(1<<14) | cathodes_a[9-dp_pos-5];
+ }
+ } else {
+ if (dp_pos>5) {
+ buffer_a[dp_pos-6] |=1<<14;
+ } else {
+ buffer_b[dp_pos-1] |=1;
+ }
+ }
+}
+// NOTE: every path that changes buffer_a/buffer_b refreshes the balance duties — the DP OR above
+// included, which is why the recompute lives in the callers right after latchDisplay()/writes.
+static inline uint8_t waitForByte(void){
+ while( !( USART2->ISR & USART_ISR_RXNE ) ) {};
+ return USART2->RDR;
+}
+
+void transmitBlocking(uint8_t * c, size_t n){
+ while (n--){
+ while( !( USART2->ISR & USART_ISR_TXE ) ) {};
+ USART2->TDR = *c++;
+ }
+}
+
+static inline void waitForLatch(void){
+ LL_USART_DisableDirectionRx(USART2);
+ LL_GPIO_SetPinMode( GPIOA, LL_GPIO_PIN_3, LL_GPIO_MODE_INPUT );
+
+ while (GPIOA->IDR & LL_GPIO_PIN_3) {}
+
+ latchDisplay();
+ segbalRecompute();
+
+ // The latch byte is 0xFE with even parity, so as soon as the line returns high we can re-enable uart
+ while (!(GPIOA->IDR & LL_GPIO_PIN_3)) {}
+
+ LL_GPIO_SetPinMode( GPIOA, LL_GPIO_PIN_3, LL_GPIO_MODE_ALTERNATE );
+ LL_USART_EnableDirectionRx(USART2);
+}
+
+static inline void parseByte(uint8_t x){
+
+ if (x & 0x80) { // command byte
+ status = x;
+ switch (x) {
+ case CMD_SHOW_CRC:
+ case CMD_LOAD_TEXT:
+ text_idx=0;
+ dp_pos=0;
+ memset(text, 0, MAX_TEXT_LEN);
+
+ pre_buffer_b[0]=0;
+ pre_buffer_b[1]=0;
+ pre_buffer_b[2]=0;
+ pre_buffer_b[3]=0;
+ pre_buffer_b[4]=0;
+ pre_buffer_a[0]=0;
+ pre_buffer_a[1]=0;
+ pre_buffer_a[2]=0;
+ pre_buffer_a[3]=0;
+ pre_buffer_a[4]=0;
+
+ break;
+ case CMD_RELOAD_TEXT:
+ latchDisplay();
+ segbalRecompute();
+ break;
+ case CMD_SET_SEG_BALANCE:
+ segbal_stage_idx = 0;
+ break;
+ case CMD_SET_SCROLL_SPEED:
+ break;
+
+ case CMD_SET_FREQUENCY:
+ case CMD_SET_FREQUENCY_B2:
+ case CMD_SET_FREQUENCY_B3:
+ target_freq=0;
+ break;
+
+ case CMD_REPORT_CRC:
+ transmitBlocking( (uint8_t*)0x8007ffc, 4);
+ break;
+
+ case CMD_START_BOOTLOADER:
+ triggerBootloader();
+ break;
+
+ default:
+ status=0;
+ }
+
+ if (x==CMD_SHOW_CRC) {
+ uint32_t* crc = (uint32_t*)0x8007ffc;
+ sprintf(text, "d %08lx", byteswap32(crc[0]));
+ for (text_idx=0; text_idx<10; text_idx++)
+ setDigitPre(text_idx, text[text_idx]);
+ }
+
+ return;
+ }
+
+ // Process data
+ switch(status){
+
+ case CMD_SHOW_CRC:
+ case CMD_LOAD_TEXT:
+ if (x=='\n' || x==0) {
+ waitForLatch();
+ return;
+ }
+ if (x=='.') {
+ dp_pos = text_idx;
+ 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;
+ return;
+
+ case CMD_SET_SCROLL_SPEED:
+ return;
+
+ case CMD_SET_SEG_BALANCE:
+ // 9 duty bytes (0..16, for 0..8 lit segments), committed atomically on the last one — an
+ // interrupted frame (any command byte resets `status`) leaves the previous table intact.
+ if (segbal_stage_idx < 9) segbal_stage[segbal_stage_idx++] = (x > 16) ? 16 : x;
+ if (segbal_stage_idx == 9) {
+ for (uint8_t i = 0; i < 9; i++) segbal_table[i] = segbal_stage[i];
+ segbalRecompute();
+ status = 0;
+ }
+ return;
+
+ case CMD_SET_FREQUENCY:
+ status=CMD_SET_FREQUENCY_B2;
+ target_freq |= x<<14;
+ return;
+ case CMD_SET_FREQUENCY_B2:
+ status=CMD_SET_FREQUENCY_B3;
+ target_freq |= x<<7;
+ return;
+ case CMD_SET_FREQUENCY_B3:
+ status=0;
+ target_freq |= x;
+ setFrequency();
+ return;
+ }
+
+
+}
+
+
+/* USER CODE END 0 */
+
+/**
+ * @brief The application entry point.
+ * @retval int
+ */
+int main(void)
+{
+ /* USER CODE BEGIN 1 */
+
+ /* USER CODE END 1 */
+
+ /* MCU Configuration--------------------------------------------------------*/
+
+ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
+
+ LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_SYSCFG);
+ LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_PWR);
+
+ /* System interrupt init*/
+
+ /* USER CODE BEGIN Init */
+
+ /* USER CODE END Init */
+
+ /* Configure the system clock */
+ SystemClock_Config();
+
+ /* USER CODE BEGIN SysInit */
+
+ /* USER CODE END SysInit */
+
+ /* Initialize all configured peripherals */
+ MX_GPIO_Init();
+ MX_TIM2_Init();
+ MX_USART2_UART_Init();
+ MX_TIM21_Init();
+ /* USER CODE BEGIN 2 */
+
+
+ // (HAL_TIM_Base_Start_IT(&htim2)
+
+
+ LL_TIM_EnableIT_UPDATE(TIM2); //TIM2->DIER |= TIM_DIER_UIE;
+ LL_TIM_EnableCounter(TIM2); //TIM2->CR1 |= TIM_CR1_CEN;
+
+ LL_TIM_EnableIT_UPDATE(TIM21);
+ LL_TIM_EnableCounter(TIM21);
+
+// setDigitDirect(0, 'l');
+// setDigitDirect(1, 'o');
+// setDigitDirect(2, 'l');
+// setDigitDirect(3, 'o');
+// setDigitDirect(4, 'l');
+// setDigitDirect(5, 'o');
+// setDigitDirect(6, 'l');
+// setDigitDirect(7, 'o');
+// setDigitDirect(8, 'l');
+// setDigitDirect(9, 'o');
+
+
+ //buffer_a[2] |= 1<<14;
+ //buffer_b[2] |= 1;
+
+
+ /* USER CODE END 2 */
+
+ /* Infinite loop */
+ /* USER CODE BEGIN WHILE */
+ while (1)
+ {
+
+ uint8_t x = waitForByte();
+ parseByte(x);
+
+
+ /* USER CODE END WHILE */
+
+ /* USER CODE BEGIN 3 */
+ }
+ /* USER CODE END 3 */
+}
+
+/**
+ * @brief System Clock Configuration
+ * @retval None
+ */
+void SystemClock_Config(void)
+{
+ LL_FLASH_SetLatency(LL_FLASH_LATENCY_1);
+
+ if(LL_FLASH_GetLatency() != LL_FLASH_LATENCY_1)
+ {
+ Error_Handler();
+ }
+ LL_PWR_SetRegulVoltageScaling(LL_PWR_REGU_VOLTAGE_SCALE1);
+ LL_RCC_HSI_Enable();
+
+ /* Wait till HSI is ready */
+ while(LL_RCC_HSI_IsReady() != 1)
+ {
+
+ }
+ LL_RCC_HSI_SetCalibTrimming(16);
+ LL_RCC_PLL_ConfigDomain_SYS(LL_RCC_PLLSOURCE_HSI, LL_RCC_PLL_MUL_4, LL_RCC_PLL_DIV_2);
+ LL_RCC_PLL_Enable();
+
+ /* Wait till PLL is ready */
+ while(LL_RCC_PLL_IsReady() != 1)
+ {
+
+ }
+ LL_RCC_SetAHBPrescaler(LL_RCC_SYSCLK_DIV_1);
+ LL_RCC_SetAPB1Prescaler(LL_RCC_APB1_DIV_1);
+ LL_RCC_SetAPB2Prescaler(LL_RCC_APB2_DIV_1);
+ LL_RCC_SetSysClkSource(LL_RCC_SYS_CLKSOURCE_PLL);
+
+ /* Wait till System clock is ready */
+ while(LL_RCC_GetSysClkSource() != LL_RCC_SYS_CLKSOURCE_STATUS_PLL)
+ {
+
+ }
+
+ LL_Init1msTick(32000000);
+
+ LL_SetSystemCoreClock(32000000);
+ LL_RCC_SetUSARTClockSource(LL_RCC_USART2_CLKSOURCE_PCLK1);
+}
+
+/**
+ * @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 */
+
+ LL_TIM_InitTypeDef TIM_InitStruct = {0};
+
+ /* Peripheral clock enable */
+ LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_TIM2);
+
+ /* TIM2 interrupt Init */
+ NVIC_SetPriority(TIM2_IRQn, 0);
+ NVIC_EnableIRQ(TIM2_IRQn);
+
+ /* USER CODE BEGIN TIM2_Init 1 */
+
+ /* USER CODE END TIM2_Init 1 */
+ TIM_InitStruct.Prescaler = 0;
+ TIM_InitStruct.CounterMode = LL_TIM_COUNTERMODE_UP;
+ TIM_InitStruct.Autoreload = 319;
+ TIM_InitStruct.ClockDivision = LL_TIM_CLOCKDIVISION_DIV1;
+ LL_TIM_Init(TIM2, &TIM_InitStruct);
+ LL_TIM_EnableARRPreload(TIM2);
+ LL_TIM_SetClockSource(TIM2, LL_TIM_CLOCKSOURCE_INTERNAL);
+ LL_TIM_SetTriggerOutput(TIM2, LL_TIM_TRGO_RESET);
+ LL_TIM_DisableMasterSlaveMode(TIM2);
+ /* USER CODE BEGIN TIM2_Init 2 */
+
+ /* USER CODE END TIM2_Init 2 */
+
+}
+
+/**
+ * @brief TIM21 Initialization Function
+ * @param None
+ * @retval None
+ */
+static void MX_TIM21_Init(void)
+{
+
+ /* USER CODE BEGIN TIM21_Init 0 */
+
+ /* USER CODE END TIM21_Init 0 */
+
+ LL_TIM_InitTypeDef TIM_InitStruct = {0};
+
+ /* Peripheral clock enable */
+ LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_TIM21);
+
+ /* TIM21 interrupt Init */
+ NVIC_SetPriority(TIM21_IRQn, 0);
+ NVIC_EnableIRQ(TIM21_IRQn);
+
+ /* USER CODE BEGIN TIM21_Init 1 */
+
+ /* USER CODE END TIM21_Init 1 */
+ TIM_InitStruct.Prescaler = 31;
+ TIM_InitStruct.CounterMode = LL_TIM_COUNTERMODE_UP;
+ TIM_InitStruct.Autoreload = 19999;
+ TIM_InitStruct.ClockDivision = LL_TIM_CLOCKDIVISION_DIV1;
+ LL_TIM_Init(TIM21, &TIM_InitStruct);
+ LL_TIM_DisableARRPreload(TIM21);
+ LL_TIM_SetClockSource(TIM21, LL_TIM_CLOCKSOURCE_INTERNAL);
+ LL_TIM_SetTriggerOutput(TIM21, LL_TIM_TRGO_RESET);
+ LL_TIM_DisableMasterSlaveMode(TIM21);
+ /* USER CODE BEGIN TIM21_Init 2 */
+
+ /* USER CODE END TIM21_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 */
+
+ LL_USART_InitTypeDef USART_InitStruct = {0};
+
+ LL_GPIO_InitTypeDef GPIO_InitStruct = {0};
+
+ /* Peripheral clock enable */
+ LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_USART2);
+
+ LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOA);
+ /**USART2 GPIO Configuration
+ PA2 ------> USART2_TX
+ PA3 ------> USART2_RX
+ */
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_2;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_ALTERNATE;
+ GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
+ GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
+ GPIO_InitStruct.Alternate = LL_GPIO_AF_4;
+ LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
+
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_3;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_ALTERNATE;
+ GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
+ GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
+ GPIO_InitStruct.Alternate = LL_GPIO_AF_4;
+ LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
+
+ /* USER CODE BEGIN USART2_Init 1 */
+
+ // Disable overrun detection, for two reasons
+ // 1. The command structure should always sort itself out
+ // 2. It makes interactive debugging the assembled clock a lot easier
+ USART2->CR3 = USART_CR3_OVRDIS_Msk;
+
+ /* USER CODE END USART2_Init 1 */
+ USART_InitStruct.BaudRate = 115200;
+ USART_InitStruct.DataWidth = LL_USART_DATAWIDTH_9B;
+ USART_InitStruct.StopBits = LL_USART_STOPBITS_1;
+ USART_InitStruct.Parity = LL_USART_PARITY_EVEN;
+ USART_InitStruct.TransferDirection = LL_USART_DIRECTION_TX_RX;
+ USART_InitStruct.HardwareFlowControl = LL_USART_HWCONTROL_NONE;
+ USART_InitStruct.OverSampling = LL_USART_OVERSAMPLING_16;
+ LL_USART_Init(USART2, &USART_InitStruct);
+ LL_USART_ConfigAsyncMode(USART2);
+ LL_USART_Enable(USART2);
+ /* USER CODE BEGIN USART2_Init 2 */
+
+ /* USER CODE END USART2_Init 2 */
+
+}
+
+/**
+ * @brief GPIO Initialization Function
+ * @param None
+ * @retval None
+ */
+static void MX_GPIO_Init(void)
+{
+ LL_GPIO_InitTypeDef GPIO_InitStruct = {0};
+
+ /* GPIO Ports Clock Enable */
+ LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOC);
+ LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOA);
+ LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOB);
+
+ /**/
+ LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_0);
+
+ /**/
+ LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_1);
+
+ /**/
+ LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_4);
+
+ /**/
+ LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_5);
+
+ /**/
+ LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_6);
+
+ /**/
+ LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_7);
+
+ /**/
+ LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_0);
+
+ /**/
+ LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_1);
+
+ /**/
+ LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_10);
+
+ /**/
+ LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_11);
+
+ /**/
+ LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_12);
+
+ /**/
+ LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_13);
+
+ /**/
+ LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_14);
+
+ /**/
+ LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_15);
+
+ /**/
+ LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_8);
+
+ /**/
+ LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_9);
+
+ /**/
+ LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_10);
+
+ /**/
+ LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_11);
+
+ /**/
+ LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_12);
+
+ /**/
+ LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_15);
+
+ /**/
+ LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_4);
+
+ /**/
+ LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_5);
+
+ /**/
+ LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_6);
+
+ /**/
+ LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_7);
+
+ /**/
+ LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_8);
+
+ /**/
+ LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_9);
+
+ /**/
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_0;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_INPUT;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_UP;
+ LL_GPIO_Init(GPIOC, &GPIO_InitStruct);
+
+ /**/
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_13;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_INPUT;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_UP;
+ LL_GPIO_Init(GPIOC, &GPIO_InitStruct);
+
+ /**/
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_0;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
+ GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
+ GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
+ LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
+
+ /**/
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_1;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
+ GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
+ GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
+ LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
+
+ /**/
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_4;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
+ GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
+ GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
+ LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
+
+ /**/
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_5;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
+ GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
+ GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
+ LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
+
+ /**/
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_6;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
+ GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
+ GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
+ LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
+
+ /**/
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_7;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
+ GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
+ GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
+ LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
+
+ /**/
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_0;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
+ GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
+ GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
+ LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
+
+ /**/
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_1;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
+ GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
+ GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
+ LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
+
+ /**/
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_10;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
+ GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
+ GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
+ LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
+
+ /**/
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_11;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
+ GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
+ GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
+ LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
+
+ /**/
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_12;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
+ GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
+ GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
+ LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
+
+ /**/
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_13;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
+ GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
+ GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
+ LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
+
+ /**/
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_14;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
+ GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
+ GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
+ LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
+
+ /**/
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_15;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
+ GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
+ GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
+ LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
+
+ /**/
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_8;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
+ GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
+ GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
+ LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
+
+ /**/
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_9;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
+ GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
+ GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
+ LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
+
+ /**/
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_10;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
+ GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
+ GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
+ LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
+
+ /**/
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_11;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
+ GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
+ GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
+ LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
+
+ /**/
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_12;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
+ GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
+ GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
+ LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
+
+ /**/
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_15;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
+ GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
+ GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
+ LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
+
+ /**/
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_3;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_INPUT;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_UP;
+ LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
+
+ /**/
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_4;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
+ GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
+ GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
+ LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
+
+ /**/
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_5;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
+ GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
+ GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
+ LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
+
+ /**/
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_6;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
+ GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
+ GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
+ LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
+
+ /**/
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_7;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
+ GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
+ GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
+ LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
+
+ /**/
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_8;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
+ GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
+ GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
+ LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
+
+ /**/
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_9;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
+ GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
+ GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
+ LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
+
+#define DISABLE_SWCLK
+
+#ifdef DISABLE_SWCLK
+
+ if ( (LL_GPIO_ReadInputPort(GPIOC) & LL_GPIO_PIN_13)!=0 && ((LL_GPIO_ReadInputPort(GPIOB) & LL_GPIO_PIN_3)!=0) ){
+ GPIO_InitStruct.Pin = LL_GPIO_PIN_14;
+ GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
+ GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
+ GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
+ GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
+ LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
+ }
+#endif
+}
+
+/* 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 */
+
+ /* 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****/
diff --git a/mk4-date/Core/Src/version.c b/mk4-date/Core/Src/version.c
index 03c750c..214b8e0 100644
--- a/mk4-date/Core/Src/version.c
+++ b/mk4-date/Core/Src/version.c
@@ -1,4 +1,4 @@
// in pre-build steps: rm -f Core/Src/version.o
-#define VERSION_STRING "Version 0.0.1 "
+#define VERSION_STRING "Version 0.0.2 "
#include "../../../version.h"
diff --git a/mk4-time/Core/Inc/astro.h b/mk4-time/Core/Inc/astro.h
new file mode 100644
index 0000000..417b4cc
--- /dev/null
+++ b/mk4-time/Core/Inc/astro.h
@@ -0,0 +1,55 @@
+/*
+ * 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);
+
+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]);
+
+/* 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 539aba0..62378aa 100644
--- a/mk4-time/Core/Inc/main.h
+++ b/mk4-time/Core/Inc/main.h
@@ -1,285 +1,318 @@
-/* USER CODE BEGIN Header */
-/**
- ******************************************************************************
- * @file : main.h
- * @brief : Header for main.c file.
- * This file contains the common defines of the application.
- ******************************************************************************
- * @attention
- *
- * © Copyright (c) 2020 STMicroelectronics.
- * All rights reserved.
- *
- * This software component is licensed by ST under BSD 3-Clause license,
- * the "License"; You may not use this file except in compliance with the
- * License. You may obtain a copy of the License at:
- * opensource.org/licenses/BSD-3-Clause
- *
- ******************************************************************************
- */
-/* USER CODE END Header */
-
-/* Define to prevent recursive inclusion -------------------------------------*/
-#ifndef __MAIN_H
-#define __MAIN_H
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-/* Includes ------------------------------------------------------------------*/
-#include "stm32l4xx_hal.h"
-#include "stm32l4xx_ll_lptim.h"
-#include "stm32l4xx_ll_bus.h"
-#include "stm32l4xx_ll_cortex.h"
-#include "stm32l4xx_ll_rcc.h"
-#include "stm32l4xx_ll_system.h"
-#include "stm32l4xx_ll_utils.h"
-#include "stm32l4xx_ll_pwr.h"
-#include "stm32l4xx_ll_gpio.h"
-#include "stm32l4xx_ll_dma.h"
-
-#include "stm32l4xx_ll_exti.h"
-
-/* Private includes ----------------------------------------------------------*/
-/* USER CODE BEGIN Includes */
-
-/* USER CODE END Includes */
-
-/* Exported types ------------------------------------------------------------*/
-/* USER CODE BEGIN ET */
-
-typedef struct {
- uint8_t tenYears;
- uint8_t years;
- uint8_t tenMonths;
- uint8_t months;
- uint8_t tenDays;
- uint8_t days;
-
- uint8_t tenHours;
- uint8_t hours;
- uint8_t tenMinutes;
- uint8_t minutes;
- uint8_t tenSeconds;
- uint8_t seconds;
-} bcdStamp_t;
-
-typedef struct {
- uint8_t low;
- uint8_t high;
-} buffer_c_t;
-
-extern buffer_c_t buffer_c[];
-
-extern uint16_t buffer_b[];
-
-extern _Bool delayedReadConfigFile;
-extern _Bool delayedCheckOnEject;
-
-extern _Bool waitingForLatch;
-extern _Bool resendDate;
-
-/* USER CODE END ET */
-
-/* Exported constants --------------------------------------------------------*/
-/* USER CODE BEGIN EC */
-
-#define RULES_FILENAME "/TZRULES.BIN"
-#define MAP_FILENAME "/TZMAP.BIN"
-#define CONFIG_FILENAME "/CONFIG.TXT"
-
-#define cSegDP 0b00010000
-
-#define cSegDecode0 0b00111111
-#define cSegDecode1 0b00000110
-#define cSegDecode2 0b01011011
-#define cSegDecode3 0b01001111
-#define cSegDecode4 0b01100110
-#define cSegDecode5 0b01101101
-#define cSegDecode6 0b01111101
-#define cSegDecode7 0b00000111
-#define cSegDecode8 0b01111111
-#define cSegDecode9 0b01101111
-
-#define bSegDecode0 0b0011111100
-#define bSegDecode1 0b0000011000
-#define bSegDecode2 0b0101101100
-#define bSegDecode3 0b0100111100
-#define bSegDecode4 0b0110011000
-#define bSegDecode5 0b0110110100
-#define bSegDecode6 0b0111110100
-#define bSegDecode7 0b0000011100
-#define bSegDecode8 0b0111111100
-#define bSegDecode9 0b0110111100
-
-#define bCat0 0b1111000000000000
-#define bCat1 0b1110001000000000
-#define bCat2 0b1101001000000000
-#define bCat3 0b1011001000000000
-#define bCat4 0b0111001000000000
-
-
-// ADC timer is 1000Hz, interrupt at TC
-// DAC timer is 100Hz, interrupt at HT and TC
-#define DAC_BUFFER_SIZE 20
-#define ADC_BUFFER_SIZE 50
-
-// NMEA 0183 messages have a max length of 82 characters
-#define NMEA_BUF_SIZE 90
-
-#define CMD_LOAD_TEXT 0x90
-#define CMD_SET_FREQUENCY 0x91
-#define CMD_RELOAD_TEXT 0x92
-#define CMD_SHOW_CRC 0x9D
-
-//#define NONCOMPLIANT_DATE_MODES
-
-enum {
- MODE_ISO8601_STD =0,
- MODE_ISO_ORDINAL,
- MODE_ISO_WEEK,
- MODE_UNIX,
- MODE_JULIAN_DATE,
- MODE_MODIFIED_JD,
- MODE_SHOW_OFFSET,
- MODE_SHOW_TZ_NAME,
- MODE_WEEKDAY,
- MODE_WEEKDA_DD,
- MODE_WDY_MM_DD,
- MODE_STANDBY,
- MODE_COUNTDOWN,
- MODE_SATVIEW,
- MODE_DEBUG_BRIGHTNESS,
- MODE_DEBUG_RTC,
- MODE_TEXT,
- MODE_FIRMWARE_CRC_T,
- MODE_FIRMWARE_CRC_D,
- MODE_VBAT,
- MODE_DISPLAYTEST,
- MODE_TTFF,
-#ifdef NONCOMPLIANT_DATE_MODES
- MODE_DDMMYYYY,
-#endif
-
- NUM_DISPLAY_MODES
-};
-
-enum {
- COUNT_NORMAL =0,
- COUNT_HIDDEN,
- COUNT_DOWN
-};
-
-enum {
- COLON_MODE_SLOWFADE = 0,
- COLON_MODE_HEARTBEAT,
- COLON_MODE_1PPS_SAWTOOTH,
- COLON_MODE_ALT_SAWTOOTH,
- COLON_MODE_TOGGLE,
- COLON_MODE_SOLID
-};
-
-enum {
- NMEA_ALL=0,
- NMEA_RMC,
- NMEA_NONE
-};
-
-enum {
- RULES_OK=0,
- RULES_STR_ERR,
- RULES_NO_FILE,
- RULES_HEADER_ERR,
- RULES_VERSION_UNKNOWN,
- RULES_CATEGORY_UNKNOWN,
- RULES_ZONE_UNKNOWN
-};
-
-enum {
- SV_GPS_L1=0,
- SV_GPS_UNKNOWN,
- SV_GLONASS_L1,
- SV_GLONASS_UNKNOWN,
- SV_GALILEO_E1,
- SV_GALILEO_UNKNOWN,
- SV_BEIDOU_B1,
- SV_BEIDOU_UNKNOWN,
- SV_COUNT
-};
-/* USER CODE END EC */
-
-/* Exported macro ------------------------------------------------------------*/
-/* USER CODE BEGIN EM */
-
-#define byteswap32(x) \
- ( ((x & 0xff000000) >> 24) | ((x & 0x00ff0000) >> 8) \
- | ((x & 0x0000ff00) << 8) | ((x & 0x000000ff) << 24))
-
-/* USER CODE END EM */
-
-void HAL_TIM_MspPostInit(TIM_HandleTypeDef *htim);
-
-/* Exported functions prototypes ---------------------------------------------*/
-void Error_Handler(void);
-
-/* USER CODE BEGIN EFP */
-void decodeRMC(void);
-void decodeGSV(uint8_t rec);
-void setDisplayPWM(uint32_t bright);
-void write_rtc(void);
-void displayOff(void);
-void button1pressed(void);
-void button2pressed(void);
-void buttonsBothHeld(void);
-void setPrecision(void);
-void sendDate( _Bool now );
-void generateDACbuffer(uint16_t * buf);
-void PPS(void);
-void PPS_NoUpdate(void);
-void PPS_Countdown(void);
-void rxConfigString(char c);
-void monitor_vbus(void);
-
-#define latchSegments() \
- buffer_c[0].low = next7seg.c; \
- buffer_b[0] = next7seg.b[0]; \
- buffer_b[1] = next7seg.b[1]; \
- buffer_b[2] = next7seg.b[2]; \
- buffer_b[3] = next7seg.b[3]; \
- buffer_b[4] = next7seg.b[4];
-
-#define triggerPendSV() \
- SCB->ICSR = SCB_ICSR_PENDSVSET_Msk;
-
-#define sendLatch() \
- huart2.Instance->TDR = 0xFE;
-
-#define loadNextTimestamp() \
- latchSegments() \
- sendLatch() \
- waitingForLatch=0;\
- triggerPendSV()
-
-extern uint32_t __VECTORS_FLASH[];
-extern uint32_t __VECTORS_RAM[];
-#define SetSysTick(x) __VECTORS_RAM[ 16 + SysTick_IRQn ] = (uint32_t)x
-#define SetPPS(x) __VECTORS_RAM[ 16 + EXTI9_5_IRQn ] = (uint32_t)x
-
-#define SetVector(x,y) __VECTORS_RAM[ 16 + x ] = (uint32_t)y
-#define GetVector(x) ((void (*)(void)) __VECTORS_RAM[ 16 + x ]
-
-/* USER CODE END EFP */
-
-/* Private defines -----------------------------------------------------------*/
-/* USER CODE BEGIN Private defines */
-
-/* USER CODE END Private defines */
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif /* __MAIN_H */
-
-/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/
+/* USER CODE BEGIN Header */
+/**
+ ******************************************************************************
+ * @file : main.h
+ * @brief : Header for main.c file.
+ * This file contains the common defines of the application.
+ ******************************************************************************
+ * @attention
+ *
+ * © Copyright (c) 2020 STMicroelectronics.
+ * All rights reserved.
+ *
+ * This software component is licensed by ST under BSD 3-Clause license,
+ * the "License"; You may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ * opensource.org/licenses/BSD-3-Clause
+ *
+ ******************************************************************************
+ */
+/* USER CODE END Header */
+
+/* Define to prevent recursive inclusion -------------------------------------*/
+#ifndef __MAIN_H
+#define __MAIN_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Includes ------------------------------------------------------------------*/
+#include "stm32l4xx_hal.h"
+#include "stm32l4xx_ll_lptim.h"
+#include "stm32l4xx_ll_bus.h"
+#include "stm32l4xx_ll_cortex.h"
+#include "stm32l4xx_ll_rcc.h"
+#include "stm32l4xx_ll_system.h"
+#include "stm32l4xx_ll_utils.h"
+#include "stm32l4xx_ll_pwr.h"
+#include "stm32l4xx_ll_gpio.h"
+#include "stm32l4xx_ll_dma.h"
+
+#include "stm32l4xx_ll_exti.h"
+
+/* Private includes ----------------------------------------------------------*/
+/* USER CODE BEGIN Includes */
+
+/* USER CODE END Includes */
+
+/* Exported types ------------------------------------------------------------*/
+/* USER CODE BEGIN ET */
+
+typedef struct {
+ uint8_t tenYears;
+ uint8_t years;
+ uint8_t tenMonths;
+ uint8_t months;
+ uint8_t tenDays;
+ uint8_t days;
+
+ uint8_t tenHours;
+ uint8_t hours;
+ uint8_t tenMinutes;
+ uint8_t minutes;
+ uint8_t tenSeconds;
+ uint8_t seconds;
+} bcdStamp_t;
+
+typedef struct {
+ uint8_t low;
+ uint8_t high;
+} buffer_c_t;
+
+extern buffer_c_t buffer_c[];
+
+extern uint16_t buffer_b[];
+
+extern _Bool delayedReadConfigFile;
+extern _Bool delayedCheckOnEject;
+extern volatile uint8_t fatfs_busy;
+
+extern _Bool waitingForLatch;
+extern _Bool resendDate;
+
+/* USER CODE END ET */
+
+/* Exported constants --------------------------------------------------------*/
+/* USER CODE BEGIN EC */
+
+#define RULES_FILENAME "/TZRULES.BIN"
+#define STARS_FILENAME "/STARS.BIN"
+#define MAP_FILENAME "/TZMAP.BIN"
+#define CONFIG_FILENAME "/CONFIG.TXT"
+
+#define cSegDP 0b00010000
+
+#define cSegDecode0 0b00111111
+#define cSegDecode1 0b00000110
+#define cSegDecode2 0b01011011
+#define cSegDecode3 0b01001111
+#define cSegDecode4 0b01100110
+#define cSegDecode5 0b01101101
+#define cSegDecode6 0b01111101
+#define cSegDecode7 0b00000111
+#define cSegDecode8 0b01111111
+#define cSegDecode9 0b01101111
+
+#define bSegDecode0 0b0011111100
+#define bSegDecode1 0b0000011000
+#define bSegDecode2 0b0101101100
+#define bSegDecode3 0b0100111100
+#define bSegDecode4 0b0110011000
+#define bSegDecode5 0b0110110100
+#define bSegDecode6 0b0111110100
+#define bSegDecode7 0b0000011100
+#define bSegDecode8 0b0111111100
+#define bSegDecode9 0b0110111100
+
+#define bCat0 0b1111000000000000
+#define bCat1 0b1110001000000000
+#define bCat2 0b1101001000000000
+#define bCat3 0b1011001000000000
+#define bCat4 0b0111001000000000
+
+
+// ADC timer is 1000Hz, interrupt at TC
+// DAC timer is 100Hz, interrupt at HT and TC
+#define DAC_BUFFER_SIZE 20
+#define ADC_BUFFER_SIZE 50
+
+// NMEA 0183 messages have a max length of 82 characters; the extended $PMTXTS (with the SOF-
+// correlation tail: dwt_pps, sof_frame, dwt_sof) runs ~110, so this sizes the tx/rx buffers for it.
+#define NMEA_BUF_SIZE 128
+
+#define CMD_LOAD_TEXT 0x90
+#define CMD_SET_FREQUENCY 0x91
+#define CMD_RELOAD_TEXT 0x92
+#define CMD_SHOW_CRC 0x9D
+
+//#define NONCOMPLIANT_DATE_MODES
+
+enum {
+ MODE_ISO8601_STD =0,
+ MODE_ISO_ORDINAL,
+ MODE_ISO_WEEK,
+ MODE_UNIX,
+ MODE_JULIAN_DATE,
+ MODE_MODIFIED_JD,
+ MODE_SHOW_OFFSET,
+ MODE_SHOW_TZ_NAME,
+ MODE_WEEKDAY,
+ MODE_WEEKDA_DD,
+ MODE_WDY_MM_DD,
+ MODE_STANDBY,
+ MODE_COUNTDOWN,
+ MODE_SATVIEW,
+ MODE_DEBUG_BRIGHTNESS,
+ MODE_DEBUG_RTC,
+ MODE_TEXT,
+ MODE_FIRMWARE_CRC_T,
+ MODE_FIRMWARE_CRC_D,
+ MODE_VBAT,
+ MODE_DISPLAYTEST,
+ MODE_TTFF,
+#ifdef NONCOMPLIANT_DATE_MODES
+ MODE_DDMMYYYY,
+#endif
+
+ // Astro pack — GPS-derived astronomy read-outs. SATVIEW-style: the payload
+ // shows on the 10-char date row while the live clock keeps running on the
+ // time row. Enabled individually via the MODE_* config keys, like any mode.
+ MODE_SUN, // sunrise / sunset / solar noon (local), auto-paged
+ MODE_SUN_AZEL, // sun azimuth & elevation, now
+ MODE_MOON, // moon phase index + illuminated %
+ MODE_GRID, // Maidenhead grid locator
+ MODE_LATLON, // latitude / longitude, auto-paged
+
+ // Temperature-compensation diagnostics: die temp / model offsets / sample count
+ // paged on the date row (satview pattern). Values come from the tempcomp module.
+ MODE_TEMPCOMP,
+
+ // 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,
+
+ // Live Allan deviation of the free-running crystal, sigma_y(tau) paged across octave
+ // taus (1,2,4,...,1024 s) on the date row. Date-row diagnostic (satview pattern); the
+ // time row keeps live GPS time. Enabled via the MODE_ADEV config key like any mode.
+ MODE_ADEV,
+
+ // Bright-star meridian-transit predictor: the soonest bright stars to cross the local
+ // meridian (culminate), paged as " " countdowns on the date row. Uses the
+ // GPS fix + local_sidereal_time (transit when LST == RA). Gated by the MODE_STAR key.
+ MODE_STAR,
+
+ NUM_DISPLAY_MODES
+};
+
+enum {
+ COUNT_NORMAL =0,
+ COUNT_HIDDEN,
+ COUNT_DOWN,
+ COUNT_ALT // time row driven by the alternate timebase (MODE_LST / MODE_SOLAR)
+};
+
+enum {
+ COLON_MODE_SLOWFADE = 0,
+ COLON_MODE_HEARTBEAT,
+ COLON_MODE_1PPS_SAWTOOTH,
+ COLON_MODE_ALT_SAWTOOTH,
+ COLON_MODE_TOGGLE,
+ COLON_MODE_SOLID
+};
+
+enum {
+ NMEA_ALL=0,
+ NMEA_RMC,
+ NMEA_NONE
+};
+
+enum {
+ RULES_OK=0,
+ RULES_STR_ERR,
+ RULES_NO_FILE,
+ RULES_HEADER_ERR,
+ RULES_VERSION_UNKNOWN,
+ RULES_CATEGORY_UNKNOWN,
+ RULES_ZONE_UNKNOWN
+};
+
+enum {
+ SV_GPS_L1=0,
+ SV_GPS_UNKNOWN,
+ SV_GLONASS_L1,
+ SV_GLONASS_UNKNOWN,
+ SV_GALILEO_E1,
+ SV_GALILEO_UNKNOWN,
+ SV_BEIDOU_B1,
+ SV_BEIDOU_UNKNOWN,
+ SV_COUNT
+};
+/* USER CODE END EC */
+
+/* Exported macro ------------------------------------------------------------*/
+/* USER CODE BEGIN EM */
+
+#define byteswap32(x) \
+ ( ((x & 0xff000000) >> 24) | ((x & 0x00ff0000) >> 8) \
+ | ((x & 0x0000ff00) << 8) | ((x & 0x000000ff) << 24))
+
+/* USER CODE END EM */
+
+void HAL_TIM_MspPostInit(TIM_HandleTypeDef *htim);
+
+/* Exported functions prototypes ---------------------------------------------*/
+void Error_Handler(void);
+
+/* USER CODE BEGIN EFP */
+void decodeRMC(void);
+void decodeGSV(uint8_t rec);
+void setDisplayPWM(uint32_t bright);
+void write_rtc(void);
+void displayOff(void);
+void button1pressed(void);
+void button2pressed(void);
+void buttonsBothHeld(void);
+void setPrecision(void);
+void sendDate( _Bool now );
+void generateDACbuffer(uint16_t * buf);
+void PPS(void);
+void PPS_NoUpdate(void);
+void PPS_Countdown(void);
+void rxConfigString(char c);
+void monitor_vbus(void);
+
+#define latchSegments() \
+ buffer_c[0].low = next7seg.c; \
+ buffer_b[0] = next7seg.b[0]; \
+ buffer_b[1] = next7seg.b[1]; \
+ buffer_b[2] = next7seg.b[2]; \
+ buffer_b[3] = next7seg.b[3]; \
+ buffer_b[4] = next7seg.b[4];
+
+#define triggerPendSV() \
+ SCB->ICSR = SCB_ICSR_PENDSVSET_Msk;
+
+#define sendLatch() \
+ huart2.Instance->TDR = 0xFE;
+
+#define loadNextTimestamp() \
+ latchSegments() \
+ sendLatch() \
+ waitingForLatch=0;\
+ triggerPendSV()
+
+extern uint32_t __VECTORS_FLASH[];
+extern uint32_t __VECTORS_RAM[];
+#define SetSysTick(x) __VECTORS_RAM[ 16 + SysTick_IRQn ] = (uint32_t)x
+#define SetPPS(x) __VECTORS_RAM[ 16 + EXTI9_5_IRQn ] = (uint32_t)x
+
+#define SetVector(x,y) __VECTORS_RAM[ 16 + x ] = (uint32_t)y
+#define GetVector(x) ((void (*)(void)) __VECTORS_RAM[ 16 + x ]
+
+/* USER CODE END EFP */
+
+/* Private defines -----------------------------------------------------------*/
+/* USER CODE BEGIN Private defines */
+
+/* USER CODE END Private defines */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* __MAIN_H */
+
+/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/
diff --git a/mk4-time/Core/Src/astro.c b/mk4-time/Core/Src/astro.c
new file mode 100644
index 0000000..9a7aa04
--- /dev/null
+++ b/mk4-time/Core/Src/astro.c
@@ -0,0 +1,189 @@
+/*
+ * 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; }
+
+/* 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). */
+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 = gmst_hours(n); /* hours */
+ 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;
+}
+
+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;
+ 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 */
+}
+
+/* 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) {
+ /* 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/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 2a35026..537719b 100644
--- a/mk4-time/Core/Src/main.c
+++ b/mk4-time/Core/Src/main.c
@@ -1,2985 +1,5309 @@
-/* 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"
-/* 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;
-#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;
-
-#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
-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;
- }
- 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, "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 (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);}
-
-// PPS rising edge
-void PPS(void)
-{
- 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)
-{
- 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)
-{
- 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 );
-}
-
-#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;
-}
-
-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 */
-
- 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 (displayMode == MODE_VBAT)
- measure_vbat();
-
-
- /* 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****/
+/* 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);
+static uint8_t adev_disp_noct(void); // Allan-deviation display accessors (engine defined far below;
+static float adev_disp_sigma(uint8_t k); // sendDate() lives above the engine, so read via these)
+static volatile uint32_t adev_last_ms; // uwTick of the last accepted sample (staleness/HOLD tell)
+/* 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;
+// 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 /
+// 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;
+_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;
+_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 colon_alt_mode
+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;
+
+// --- 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 —
+// 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
+
+// --- Temperature compensation (opt-in) ------------------------------------------------------
+// Learns ppm-vs-die-temperature for both oscillators while GPS-locked (tc_learn), then during
+// GPS-loss holdover steers the SysTick timebase from the HSE model (tc_apply) and optionally
+// trims RTC->CALR from the LSE model (tc_rtc) so the battery RTC hands over better time across
+// a power loss. "tc_dump = on" over serial prints the learned coefficients as ready-to-paste
+// config lines; non-NAN tc_hse_a/tc_lse_a in config freeze the model (config overrides learning).
+// All defaults off: with none of the keys set, behaviour is identical to stock.
+// Config-key scalars are written from the USB OTG ISR (parseConfigString) and read by the
+// main loop: volatile, matching the pps_ts_enabled precedent.
+volatile _Bool tc_learn = 0, tc_apply = 0, tc_rtc = 0;
+// Holdover fade (opt-in). A sub-second digit whose accuracy can no longer be held — its time
+// uncertainty during GPS-loss holdover has grown past that digit's place value — FADES to black by
+// its remaining significance instead of dashing, overriding the fixed Tolerance_time_* ladder.
+// digit_bright[] holds per-digit intensity 0..FADE_MAX for the [deciseconds, centiseconds,
+// milliseconds, decimal-point] positions (FADE_MAX = fully lit, i.e. certainly significant).
+// Default 0 = NOT significant: at power-on nothing is disciplined yet (had_pps=0, holdover age is
+// huge), so with significance_fade the sub-second digits must start DASHED and only light once the
+// first computeHoldoverFade() proves significance — else setPrecision() at boot reads a stale FADE_MAX
+// and flashes ticking numbers for ~1 s before the first per-second recompute dashes them.
+volatile _Bool significance_fade = 0;
+#define FADE_MAX 16
+uint8_t digit_bright[4] = { 0, 0, 0, 0 };
+float holdover_u_us = 0.0f; // last computed 3σ time-interval-error bound U(τ), µs
+volatile int16_t tc_t0 = 40; // model centre temperature (°C)
+volatile uint16_t tc_engage_s = 2; // seconds of PPS absence before steering engages (min 2)
+volatile uint16_t tc_max_ppm = 100; // hard clamp on the applied correction magnitude
+// Frozen coefficients (ppm units at tc_t0). Elements are single-word (atomic) reads/writes;
+// consumers snapshot each element once. HSE has NO 'a': its learned origin is arbitrary and
+// steering uses temperature differences only, so freezing needs just b (and optionally c).
+float tc_cfg_hse[3] = {NAN, NAN, NAN}; // [0] unused, [1] ppm/°C, [2] ppm/°C²
+float tc_cfg_lse[3] = {NAN, NAN, NAN}; // absolute: ppm, ppm/°C, ppm/°C²
+volatile _Bool tc_dump_pending = 0; // set by the serial parser, serviced in the main loop
+volatile _Bool tc_reset_pending = 0;
+volatile _Bool adev_dump_pending = 0; // "adev_dump = on" over serial -> emit one $PMADEV sentence
+volatile _Bool hdev_dump_pending = 0; // "hdev_dump = on" over serial -> emit one $PMHDEV sentence (Hadamard)
+volatile _Bool star_dump_pending = 0; // "star_dump = on" over serial -> emit one $PMSTAR sentence
+
+// Validated coefficient parse: garbage/'----'/empty leaves the value untouched (a pasted-back
+// commented dump line must not freeze 0.0); an explicit "nan" parses and UNFREEZES the slot.
+static void tc_parse_coeff(const char *v, float *out){
+ char *end;
+ float f = strtof(v, &end);
+ if (end != v) *out = f;
+}
+
+// Steering handoff, governor (main loop) -> tick ISR. base/rem are written together under
+// IRQ-off; the ISR Bresenham distributes `rem` one-tick-longer periods per 1000 ms so the
+// average period is (tc_load_base+1) + rem/1000 ticks — fractional-ppm rate steering.
+volatile uint8_t tc_steer_on = 0;
+volatile int32_t tc_load_base = 0; // SysTick->LOAD for the shorter of the two periods
+volatile int32_t tc_rem = 0; // extra-tick remainder, always in [0,1000)
+volatile int32_t tc_acc = 0; // Bresenham accumulator (ISR-owned)
+
+// Learned state (main-loop only). 2 °C bins spanning die temp -8..71 °C; sums are bounded by
+// the halving-at-32768 aging rule (max |sum| ~ 6400*32768 < 2^31), so int32 cannot overflow.
+struct tc_bin { int32_t hse_sum, lse_sum; uint16_t hse_n, lse_n; };
+struct tc_bin tc_bins[40];
+float tc_hse_m[3], tc_lse_m[3]; // learned models (ppm at powers of T - tc_t0)
+_Bool tc_hse_valid = 0, tc_lse_valid = 0;
+int16_t tc_hse_tmin = 0, tc_hse_tmax = 0; // learned coverage: model is clamped to this range
+int16_t tc_lse_tmin = 0, tc_lse_tmax = 0;
+uint32_t tc_n_hse = 0, tc_n_lse = 0; // lifetime sample counts (display + dump)
+// Weighted-RMS residual of each learned fit (model vs bin means): the model's OWN error, in its
+// fit units (HSE: SysTick ticks/s; LSE: ppm). This is the holdover-fade uncertainty's σ_temp source
+// — how far the temperature model actually is from the measured data, not a guess.
+float tc_hse_resid = 0, tc_lse_resid = 0;
+
+// Warm-start (seed-and-evolve). A previously-learned model — the last tc_dump, written back into
+// config.txt by the host (the firmware never writes the filesystem; the QSPI drive is host-owned) —
+// is reloaded at boot as an evolving PRIOR rather than a hard freeze. The clock is temperature-
+// compensated from the first second and keeps refining: tc_hse_prior/tc_lse_prior hold the model
+// ORDER (1 const, 2 linear, 3 quadratic) currently carried by the seed, and tc_fit keeps that model
+// until real learning supports a fit at least as rich, then hands over. tc_seed = off leaves the
+// existing freeze path (tc_hse_b/… as asserted constants) untouched.
+volatile _Bool tc_seed = 0; // config: warm-start from the seeded coefficients + evolve
+volatile int16_t tc_seed_lo = 0, tc_seed_hi = 0; // seed coverage (die °C): bounds the prior — never extrapolated
+uint8_t tc_hse_prior = 0, tc_lse_prior = 0; // seed model order still held (0 = handed over to real data)
+_Bool tc_seed_done = 0; // one-shot: seed once per power-on (BSS-cleared at reset)
+// Serial "tc_seed = on" arms this in the USB ISR; tc_housekeeping consumes it in the MAIN LOOP.
+// The learned-state contract ("main-loop only") holds: the ISR only ever touches this one flag
+// (plus the already-accepted single-word tc_cfg_*/tc_seed slots) — never the model itself.
+volatile _Bool tc_seed_pending = 0;
+static void tc_seed_apply(void); // defined by the tempcomp block; called after the config load
+
+// Display cache for MODE_TEMPCOMP. Written by the governor (main loop); read by sendDate,
+// which ALSO runs from the SysTick ISRs — each field is a single 32-bit (atomic) access, so
+// the worst case is a one-repaint-stale value pairing, never a torn read.
+float tc_disp_hse = 0, tc_disp_lse = 0;
+_Bool tc_disp_hse_ok = 0, tc_disp_lse_ok = 0;
+char tc_disp_state = '-'; // A applying · F frozen (config) · L learning · - idle
+
+#define CHECK_CONFIG_MTIME
+
+struct {
+#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;
+ uint16_t page_ms; // paged modes (SUN/LATLON): sub-screen dwell, ms
+ _Bool modes_enabled[NUM_DISPLAY_MODES];
+
+} config = {0};
+
+struct {
+ float in;
+ float out;
+} brightnessCurve[] = {
+ // Measured VTT9812FH (R11 = 470K) response, baked in so the clock needs no config.txt BS lines.
+ // These are the same five stops as "BSn = in,out" (out is inverted, 4095-out, exactly as
+ // parseBrightness does); a config.txt BS line still overrides its stop at load.
+ {0, 4095-0}, // BS1 = 0,0
+ {131, 4095-365}, // BS2 = 131,365
+ {1076, 4095-1422}, // BS3 = 1076,1422
+ {2774, 4095-2665}, // BS4 = 2774,2665
+ {3849, 4095-4095}, // BS5 = 3849,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;
+}
+// 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);
+ 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();
+}
+
+// ---- Bright-star meridian-transit predictor (MODE_STAR) ----------------------------------------
+// A star crosses the local meridian (upper culmination — its highest point in the sky) exactly when
+// the Local Sidereal Time equals the star's right ascension. From the GPS fix + local_sidereal_time
+// we compute, for each catalogue star, the seconds until its next transit and the altitude it will
+// reach (90 - |latitude - declination|), then cache the soonest few for a paged " "
+// countdown on the date row. J2000 catalogue positions are precessed to date (first-order IAU) so
+// the timing stays good to the shown minute for decades. Compute is main-loop only (double trig).
+//
+// The catalogue is loaded at boot from /STARS.BIN on the CLOCK drive (the QSPI flash volume;
+// generate-stars.py, HYG v4). There is deliberately NO baked-in fallback: without a valid file the
+// mode honestly shows nothing ("STAr ----") rather than quietly substituting lookalike data. The
+// transit *maths* is verified in the emulator.
+#define STAR_MAX 128u // RAM cap on the loaded catalogue (the STARS.BIN file is clamped to this)
+#define STAR_SHOW 8u // cache the soonest 8 upcoming transits
+#define STAR_SIDSEC_PER_HR 3590.1704 // solar seconds the meridian takes to sweep one hour of RA
+typedef struct { char nm[4]; uint32_t epoch; int8_t alt; char dir; } star_entry_t; // dir: culminates due (S)outh / (N)orth
+static star_entry_t star_cache[STAR_SHOW];
+static volatile uint8_t star_ncache;
+
+// The live catalogue in RAM: J2000 RA hours / Dec degrees + proper motion (mas/yr; mu_alpha* incl.
+// cos-dec), plus the cached APPARENT place of date (ra_now/dec_now, refreshed daily) the transit
+// math consumes. Loaded from the card or the baked default.
+static struct { char nm[4]; float ra; float dec; int16_t pmra, pmdec; float ra_now, dec_now; } star_buf[STAR_MAX];
+static uint16_t star_count = 0;
+static uint32_t star_apparent_at = 0; // currentTime of the last apparent-place refresh (0 = never)
+volatile float star_max_mag = 6.0f; // config "star_max_mag": only load stars brighter than this (file is mag-sorted -> early-stop). Default 6 = the whole file.
+
+// Load /STARS.BIN into star_buf, filtered to star_max_mag. The file is magnitude-sorted, so we stop at
+// the first star past the cut. Hardened like loadRules (PR#7): validate magic + record length, clamp
+// the count BEFORE writing, byte-check every read, sanity-check RA/Dec, scrub name bytes. No file (or
+// an invalid one) leaves the catalogue EMPTY — MODE_STAR requires STARS.BIN on the CLOCK drive.
+static void loadStars(void){
+ star_count = 0; star_apparent_at = 0;
+ FIL file;
+ _Bool file_ok = 0;
+ if (f_open(&file, STARS_FILENAME, FA_READ) == FR_OK){
+ unsigned int rc; uint8_t hdr[16];
+ uint16_t rl = 0;
+ if (f_read(&file, hdr, 16, &rc) == FR_OK && rc == 16 && memcmp(hdr, "MST1", 4) == 0
+ && ((rl = (uint16_t)(hdr[6] | (hdr[7]<<8))) == 10u || rl == 14u) // v1 (no PM) or v2 (+pmra/pmdec i16)
+ && (uint16_t)(hdr[8] | (hdr[9]<<8)) == 100u){ // mag_scale we decode against (mag*100); reject a file written to a different scale
+ file_ok = 1;
+ uint16_t count = (uint16_t)(hdr[4] | (hdr[5]<<8));
+ float mc = star_max_mag * 100.0f; // saturate: a huge star_max_mag means "load all", never wrap negative (out-of-range float->int16 is UB)
+ int16_t magcut = mc > 32767.0f ? 32767 : (mc < -32768.0f ? -32768 : (int16_t)mc);
+ for (uint16_t k = 0; k < count && star_count < STAR_MAX; k++){
+ uint8_t rec[14];
+ if (f_read(&file, rec, rl, &rc) != FR_OK || rc != rl) break; // torn read -> keep what we have
+ int16_t mag = (int16_t)(rec[4] | (rec[5]<<8));
+ if (mag > magcut) continue; // filter per-record (don't trust the file to be mag-sorted); loop still bounded by EOF + STAR_MAX
+ float ra = (float)(uint16_t)(rec[0] | (rec[1]<<8)) / 65536.0f * 24.0f;
+ float dec = (float)( int16_t)(rec[2] | (rec[3]<<8)) / 100.0f;
+ if (ra < 0.0f || ra >= 24.0f || dec < -90.0f || dec > 90.0f) continue; // reject garbage
+ for (int b = 0; b < 4; b++){ // sanitize: name bytes go raw to the date-board UART — never forward control/high-bit bytes from a hostile file
+ uint8_t c = rec[6 + b];
+ star_buf[star_count].nm[b] = (c < 0x20 || c > 0x7E) ? ' ' : (char)c;
+ }
+ star_buf[star_count].ra = ra; star_buf[star_count].dec = dec;
+ star_buf[star_count].pmra = (rl == 14u) ? (int16_t)(rec[10] | (rec[11]<<8)) : 0;
+ star_buf[star_count].pmdec = (rl == 14u) ? (int16_t)(rec[12] | (rec[13]<<8)) : 0;
+ star_count++;
+ }
+ }
+ f_close(&file);
+ }
+ (void)file_ok; // no fallback by design: absent/invalid file -> star_count 0 -> "STAr ----"
+}
+
+// J2000 -> apparent place of date: linear proper motion, then RIGOROUS IAU-1976 precession (the
+// zeta/z/theta rotation). The previous first-order formula carried a tan(dec) term that diverges
+// near the pole — Polaris's transit countdown was ~5 minutes wrong by 2026 and growing. The exact
+// rotation has no singularity; it costs ~6 double-trig per star, so it runs at LOW cadence (daily —
+// precession moves ~0.14 arcsec/day) and the per-second star_update just consumes the cache.
+static void star_refresh_apparent(void){
+ const double D2R = 0.017453292519943295;
+ double yrs = ((double)currentTime - 946728000.0) / 31557600.0; // Julian years since J2000.0
+ double T = yrs / 100.0; // Julian centuries
+ double zeta = (2306.2181*T + 0.30188*T*T + 0.017998*T*T*T) * (D2R / 3600.0);
+ double zz = (2306.2181*T + 1.09468*T*T + 0.018203*T*T*T) * (D2R / 3600.0);
+ double theta = (2004.3109*T - 0.42665*T*T - 0.041833*T*T*T) * (D2R / 3600.0);
+ double st = sin(theta), ct = cos(theta);
+ for (uint16_t s = 0; s < star_count; s++){
+ double d0 = (double)star_buf[s].dec;
+ double cd = cos(d0 * D2R); if (cd < 1e-6) cd = 1e-6; // pole guard for the mu/cos(dec) term
+ double a0 = (double)star_buf[s].ra
+ + ((double)star_buf[s].pmra / cd) * yrs / (3600000.0 * 15.0); // mas/yr (mu_alpha*) -> hours
+ d0 += (double)star_buf[s].pmdec * yrs / 3600000.0; // mas/yr -> degrees
+ double ar = a0 * 15.0 * D2R + zeta, dr = d0 * D2R;
+ double ca = cos(ar), sa = sin(ar), cdd = cos(dr), sd = sin(dr);
+ double A = cdd * sa;
+ double B = ct * cdd * ca - st * sd;
+ double C = st * cdd * ca + ct * sd;
+ double a_now = (atan2(A, B) + zz) / (D2R * 15.0); // hours
+ a_now = fmod(a_now, 24.0); if (a_now < 0.0) a_now += 24.0;
+ if (C > 1.0) C = 1.0;
+ if (C < -1.0) C = -1.0;
+ star_buf[s].ra_now = (float)a_now;
+ star_buf[s].dec_now = (float)(asin(C) / D2R);
+ }
+ star_apparent_at = (uint32_t)currentTime;
+}
+
+static void star_update(void){
+ float lat = latitude, lon = longitude; // one snapshot of the fix
+ if (!astro_pos_ok(lat, lon)) { star_ncache = 0; return; }
+ if (star_apparent_at == 0 || (uint32_t)((uint32_t)currentTime - star_apparent_at) > 86400u)
+ star_refresh_apparent(); // daily is plenty (~0.14 arcsec/day)
+ double lst = local_sidereal_time((double)currentTime, (double)lon); // hours [0,24)
+
+ // Single pass: keep the soonest STAR_SHOW visible stars in a small array sorted ascending by
+ // sidereal-hours-to-transit. No per-star scratch (star_count can be the whole SD catalogue), so the
+ // stack stays bounded regardless of catalogue size.
+ struct { float dt; int8_t alt; char nm[4]; char dir; } top[STAR_SHOW]; uint8_t n = 0;
+ for (uint16_t s = 0; s < star_count; s++) {
+ double a_now = (double)star_buf[s].ra_now, d_now = (double)star_buf[s].dec_now; // apparent of date (cached)
+ double diff = (double)lat - d_now; // sign = which horizon it culminates over
+ double alt = 90.0 - fabs(diff); // upper-transit altitude (geometric)
+ if (alt <= -0.57) continue; // horizon gate WITH refraction: ~34' lifts a grazer into view
+ double dt = fmod(a_now - lst, 24.0); if (dt < 0.0) dt += 24.0; // sidereal hours to transit
+ float dtf = (float)dt;
+ if (n < STAR_SHOW || dtf < top[n-1].dt) { // insertion-sort into the top-N
+ uint8_t pos = (n < STAR_SHOW) ? n : (uint8_t)(STAR_SHOW - 1);
+ if (n < STAR_SHOW) n++;
+ while (pos > 0 && top[pos-1].dt > dtf) { top[pos] = top[pos-1]; pos--; }
+ top[pos].dt = dtf; memcpy(top[pos].nm, star_buf[s].nm, 4);
+ top[pos].alt = (int8_t)(alt >= 0.0 ? alt + 0.5 : 0.0); // refraction-band grazers read alt 0
+ top[pos].dir = (diff >= 0.0) ? 'S' : 'N'; // dec below latitude -> due south, else due north
+ }
+ }
+ __disable_irq();
+ for (uint8_t i = 0; i < n; i++) {
+ memcpy(star_cache[i].nm, top[i].nm, 4);
+ star_cache[i].epoch = (uint32_t)currentTime + (uint32_t)((double)top[i].dt * STAR_SIDSEC_PER_HR + 0.5);
+ star_cache[i].alt = top[i].alt;
+ star_cache[i].dir = top[i].dir;
+ }
+ star_ncache = n;
+ __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_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';
+ 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_TEMPCOMP: {
+ // Pages: die temp -> HSE model -> LSE model -> samples+state, page_ms dwell each.
+ // Values are the governor's display cache (clamped so the row never overflows). Layout is
+ // the RISE/SET style: label, separator space, a sign slot (space when positive), then the
+ // digits — numbers align whether signed or not, and short values keep clear space at the
+ // row's end beside the time row: "tC 32C" / "HSE -0.25" / "rtC 18.68" / "n 159 L".
+ int tcp = (int)((uwTick / page_ms()) % 4);
+ char num[12];
+ if (tcp == 0) {
+ int t2 = (int)die_temp_c;
+ i = sprintf((char*)&uart2_tx_buffer[1], "tC %c%dC", t2 < 0 ? '-' : ' ', t2 < 0 ? -t2 : t2);
+ } else if (tcp == 1 || tcp == 2) {
+ _Bool ok = (tcp == 1) ? tc_disp_hse_ok : tc_disp_lse_ok;
+ float v = (tcp == 1) ? tc_disp_hse : tc_disp_lse;
+ if (!ok) i = sprintf((char*)&uart2_tx_buffer[1], "%s ----", (tcp == 1) ? "HSE" : "rtC");
+ else {
+ sprintf(num, "%.2f", (double)(v < 0 ? -v : v));
+ i = sprintf((char*)&uart2_tx_buffer[1], "%s %c%s", (tcp == 1) ? "HSE" : "rtC", v < 0 ? '-' : ' ', num);
+ }
+ } else {
+ unsigned long ns = tc_n_hse > 999999UL ? 999999UL : tc_n_hse;
+ i = sprintf((char*)&uart2_tx_buffer[1], "n%6lu %c", ns, tc_disp_state);
+ }
+ break;
+ }
+ case MODE_ADEV: {
+ // sigma_y(tau) paged one octave per page_ms dwell; tau = 2^page seconds. Until the ring has
+ // enough contiguous 1 s samples for even tau=1 (needs 3), show a filling marker. The layout is
+ // a compact scientific that always fits the 10-char row: "s ", mantissa dropped as
+ // the tau label grows ("1s 3.2e-11" / "64s 3e-11" / "1024s3e-11"). Fractional-frequency values.
+ uint8_t noct = adev_disp_noct();
+ if (noct == 0) {
+ i = sprintf((char*)&uart2_tx_buffer[1], "Adev ----"); // filling, or PPS not locked yet
+ } else if ((uint32_t)(uwTick - adev_last_ms) > 2500u) {
+ i = sprintf((char*)&uart2_tx_buffer[1], "Adev HOLd"); // holdover: no fresh samples — the curve would be a frozen replay
+ } else {
+ uint32_t k = (uwTick / page_ms()) % noct;
+ uint32_t tau = 1u << k;
+ double s = (double)adev_disp_sigma((uint8_t)k);
+ char sig[12];
+ sprintf(sig, (tau < 10) ? "%.1e" : "%.0e", s); // room for a mantissa digit only when tau is short
+ i = sprintf((char*)&uart2_tx_buffer[1], "%lu %s", // NO unit-s ('s' == the '5' glyph: "1024s3e-11" read as 10245...) and ALWAYS a separator
+ (unsigned long)tau, sig);
+ }
+ break;
+ }
+ case MODE_STAR: {
+ // Soonest bright-star meridian transits, paged one per page_ms dwell: " " counting
+ // down to culmination. The countdown ticks every second (recomputed here from the cached transit
+ // epoch); star_update() re-sorts the list in the main loop. "STAr ----" with no GPS fix.
+ if (star_ncache == 0) { i = sprintf((char*)&uart2_tx_buffer[1], "STAr ----"); break; }
+ // The payoff moment: when the soonest star reaches culmination, latch its name and hold a "NOW"
+ // tell for 8 s — otherwise the next star_update() re-sort rolls it off the list the second it
+ // happens and the event is invisible.
+ static char star_now_nm[4];
+ static uint32_t star_now_until;
+ if ((long)star_cache[0].epoch - (long)currentTime <= 1L){
+ memcpy(star_now_nm, star_cache[0].nm, 4);
+ star_now_until = (uint32_t)currentTime + 8u;
+ }
+ if (star_now_until && (uint32_t)currentTime < star_now_until){
+ i = sprintf((char*)&uart2_tx_buffer[1], "%-4.4s NOW", star_now_nm);
+ break;
+ }
+ uint32_t p = (uwTick / page_ms()) % star_ncache;
+ long rem = (long)star_cache[p].epoch - (long)currentTime; // seconds to transit
+ if (rem < 0) rem = 0;
+ // §6: countdown as bare space-separated values (no dash). Under an hour it reads minutes seconds
+ // (" 1 35"); an hour or more switches to hours-'h'-minutes (" 2h15") so a far
+ // transit can't masquerade as 2 min 15 s and the field still fits the row.
+ if (rem < 3600)
+ i = sprintf((char*)&uart2_tx_buffer[1], "%-4.4s %2ld %02ld", star_cache[p].nm, rem / 60, rem % 60);
+ else
+ i = sprintf((char*)&uart2_tx_buffer[1], "%-4.4s %2ldh%02ld", star_cache[p].nm, rem / 3600, (rem / 60) % 60);
+ 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);
+ // 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;
+ }
+ 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 = (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 ";
+ 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 = (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;
+ 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.
+// --- 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;
+ 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 );
+ // 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);
+ }
+ }
+
+}
+
+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 (++r 80
+
+void setDisplayPWM(uint32_t bright){
+ display_scan_len = (uint16_t)bright;
+ HAL_DMA_Abort(&hdma_tim1_up);
+ HAL_DMA_Abort(&hdma_tim7_up);
+ HAL_DMA_Start(&hdma_tim1_up, (uint32_t)buffer_b, (uint32_t)&GPIOB->ODR, 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);
+}
+
+// --- Per-segment brightness balance (seg_balance) --------------------------------------------
+// The display is voltage-driven (buffer chips + 10R per segment; the DAC sets the rail), so all
+// lit segments of a digit share the drop across that digit's common return path: FEWER lit
+// segments leaves more voltage per LED, so '-', '1' and '7' glow visibly brighter than '8' —
+// worst at low brightness where the rail sits just above the LED forward voltage. There is no
+// per-digit analog knob, but there is headroom in the scan: buffer_b/buffer_c are sized [80] and
+// the DMA is circular, so the 5-step scan can be replayed as 16 cycles of 5 slots. Slots 0..4
+// stay the live "master" slots every existing writer already targets; segbal_poll() (main loop,
+// at most 1 kHz) mirrors them into slots 5..79 with each digit lit in s of its 16 cycles,
+// s = 16 - strength * (16 - 2 * popcount(segments)) / 100
+// so at full strength s = 2N and every lit segment averages the same duty-per-segment product:
+// (1/N) * (2N/16) = 1/8, uniform across the display (an 8-segment '8.' keeps 16/16 — sparse
+// digits dim DOWN to its level, dense digits are untouched). The cycle mask picks each digit's s
+// lit cycles as a NESTED rank window: rank = bit-reversed k rotated by a fixed per-digit phase
+// (cycle 0 — the master slot — pinned at rank 0, so masters are never rewritten and count toward
+// the duty exactly). Two properties are load-bearing, and each was a hardware-visible artefact
+// when missing:
+// - NESTED (s+1's lit set = s's plus exactly one cycle): s changes whenever a digit's segment
+// count does — every second boundary at least — and a spread that RESHUFFLES on s±1 (e.g.
+// (k*s) % 16 < s, the first ship) re-phases the whole column's light at that instant: a
+// once-per-second step in the ghost bleed of unlit segments + a beat on the sub-second digits.
+// - DECORRELATED (distinct phase per digit): un-rotated nested ranks light every digit on the
+// SAME low-rank cycles, so the shared rail sees a sawtooth (all-on cycles vs sparse cycles,
+// per-cycle load variance ~14 vs ~1.4 rotated in sim) whose shape steps with the time content
+// — the whole DATE row blipped at 1 Hz and per-digit brightness left calibration (the K curve
+// was eyeballed against decorrelated patterns).
+// Positions stay near-evenly spread at every s (a rotated rank window bit-reverses to a van-der-
+// Corput run; worst case s = 1 is the master alone, unchanged from stock 5-slot timing).
+//
+// Above 100 the map turns into a power law, s = 16*(N/8)^(strength/100) — continuous at 100
+// (gamma 1 = the linear map). Near the LED forward-voltage knee (low rail) segment current is
+// exponential in voltage, so the real bloom ratio between a '-' and an '8.' can exceed what any
+// LINEAR duty map can counter; hardware A/B showed 100 under-correcting at low brightness. The
+// exponent steepens the curve: at 200, a 1-segment digit runs 1/16 duty against the 8-segment
+// digit's 16/16. Tune live over serial against the actual display.
+// Off (0, the default) leaves the stock 5-slot scan and all timing untouched.
+//
+// The bloom is rail-dependent, and NOT monotonically: hardware calibration (eyeball-matched at
+// four rail levels) found a valley — strong compensation needed at the dim end (the LED knee,
+// where current is exponential in voltage), moderate again at full rail (maximum-current IR drop
+// in the shared return), mild in between. "seg_balance = on" applies that measured curve,
+// interpolated by the live rail (dac_target, 4095 = dimmest); it is the whole intended interface.
+// A numeric value (2..300) instead applies a fixed manual strength for experiments.
+// seg_balance = 0/off (the default) keeps the stock scan and stock timing untouched.
+volatile uint16_t seg_balance = 0; // 0 = off (stock) · 1/on = AUTO (calibrated curve) · 2..300 = fixed manual strength
+
+#define SEGBAL_BSEG_MASK 0x01FCu // GPIOB word: bits 2..8 are segments; everything else
+ // (bCat column selects etc.) passes through unmasked
+
+// rank(k) for D=16 (bit-reversed k). For D=8/4 shift right by 1/2 — dropping the low bits of the
+// reversal is exactly the 3-/2-bit reversal, so one table serves every depth.
+static const uint8_t SEGBAL_REV16[16] = {0,8,4,12,2,10,6,14,1,9,5,13,3,11,7,15};
+// Per-digit phases for the mirror ranks (col x bank; DP rides its digit's phase so its cycles
+// stay a subset of the digit's). Rank 0 = the master slot is pinned; the phase rotates ranks
+// 1..D-1 among themselves (mod D-1), so every digit still lights the master and its mirrors sit
+// in a distinct rank window. Values are spread over the 15-ring (idx*4 mod 15, all distinct).
+static const uint8_t SEGBAL_PH_B[5] = {0, 4, 8, 12, 1};
+static const uint8_t SEGBAL_PH_C[5] = {5, 9, 13, 2, 6};
+
+// Mirror freshness across main-loop stalls. The sub-second MASTERS are written from the SysTick
+// ISR (SysTick_CountUp_*), so the stock 5-slot scan never lags the main loop — but the mirrors
+// carry (D-1)/D of the light when balancing, and a refill that lives only in the main loop goes
+// stale for the length of any long pass (the once-per-second PendSV display prep, housekeeping,
+// a flash commit): hardware showed the sub-second digits freezing once per second, present since
+// the first segbal ship and independent of the dither order. segbal_poll() therefore EXPORTS the
+// per-column lit-cycle bitmaps, and every SysTick tick re-copies the live master values through
+// them (segbal_isr_refresh), pinning mirror VALUE freshness to the same 1 ms the masters get.
+// The bitmaps may lag a main-loop pass behind a glyph change — a duty lag of one pass,
+// imperceptible where a frozen digit was not.
+static volatile uint16_t segbal_lit_b[5], segbal_lit_c[5], segbal_lit_dp[5]; // bit k = cycle k lit
+static volatile uint8_t segbal_mirror_live = 0; // ISR refresh armed (D-cycle scan up, bitmaps valid)
+
+// Lit cycles (of 16) for a digit with n lit segments at effective strength `eff` (0..300).
+// n <= 8 always (7 segments + DP). Returns 16 (always lit) .. 1 (floor for any lit digit).
+static uint32_t segbal_duty(uint32_t n, uint32_t eff){
+ if (n == 0) return 0;
+ if (eff <= 100) // linear blend: 100 -> s = 2n exactly
+ return 16u - eff * (16u - 2u*n) / 100u;
+ float s = 16.0f * powf((float)n / 8.0f, (float)eff / 100.0f); // power law, gamma > 1
+ uint32_t si = (uint32_t)(s + 0.5f);
+ return si < 1u ? 1u : (si > 16u ? 16u : si);
+}
+
+// Effective strength for THIS refill. AUTO (seg_balance = 1/on) follows the hardware calibration:
+// a full eyeballed sweep across the whole rail on a production Mk IV (2026-07-11) landed the even
+// point on a clean EXPONENTIAL — the round-number rails fell on a x3-per-half-brightness geometric
+// progression (rail brightest -> 10, mid -> 30, dimmest -> 90), i.e. K = 10 * 9^(dac/4095). That is
+// exactly the LED knee: near the bottom of the rail, segment current is exponential in forward
+// voltage, so a sparse digit pulls away exponentially fast and needs exponentially more duty haircut.
+// 4095 is the 12-bit DAC full-scale code (dac_target maxes there, matching the original firmware); the
+// intermediate breakpoints below stay on clean powers of two and K evaluates to the measured anchors.
+// Sampled to a 9-point LUT (cheaper than a per-refill powf, and exponentials interpolate linearly to
+// well under 1 K). A numeric value (2..300) instead applies a fixed manual strength for experiments.
+// seg_balance = 0/off (the default) keeps the stock scan and stock timing untouched.
+static const uint16_t SEGBAL_AUTO_DAC[9] = { 0, 512, 1024, 1536, 2048, 2560, 3072, 3584, 4095 };
+static const uint16_t SEGBAL_AUTO_K[9] = { 10, 13, 17, 23, 30, 39, 52, 68, 90 }; // 10*9^(dac/4095)
+
+static uint32_t segbal_strength(void){
+ if (seg_balance != 1) return seg_balance; // manual fixed strength, or 0 = off
+ int32_t d = (int32_t)dac_target;
+ if (d <= SEGBAL_AUTO_DAC[0]) return SEGBAL_AUTO_K[0];
+ for (uint32_t i = 1; i < 9; i++) {
+ if (d <= (int32_t)SEGBAL_AUTO_DAC[i]) {
+ int32_t d0 = SEGBAL_AUTO_DAC[i-1], d1 = SEGBAL_AUTO_DAC[i];
+ int32_t k0 = SEGBAL_AUTO_K[i-1], k1 = SEGBAL_AUTO_K[i];
+ return (uint32_t)(k0 + (k1 - k0) * (d - d0) / (d1 - d0));
+ }
+ }
+ return SEGBAL_AUTO_K[8];
+}
+
+// Dither depth (cycles per column) for the CURRENT scan rate. display_frequency is a user config
+// (1..100 kHz) that retunes TIM1, and a fixed 16-cycle dither at a slow scan would visibly strobe:
+// the dither repeats at step/(5*D) Hz, so pick the deepest D of {16,8,4} that stays >= ~200 Hz,
+// and return 0 (balancing unavailable) below ~4 kHz steps. Stock rate (~62 kHz steps) gives D=16.
+static uint32_t segbal_depth(void){
+ uint32_t step = 16000000u / (TIM1->ARR + 1u); // TIM1 clock 16 MHz (see setDisplayFreq)
+ if (step >= 16000u) return 16u;
+ if (step >= 8000u) return 8u;
+ if (step >= 4000u) return 4u;
+ return 0u;
+}
+
+// Forward the duty table (ALWAYS on the 0..16 scale — the date board rescales to its own depth)
+// to the DATE BOARD over the shared UART, so both rows equalise together from the one config key.
+// Sent from the main loop only when the UART is idle and the date board is not inside its latch
+// window (its RX is disabled there — bytes would be lost). The date board commits the 9 bytes
+// atomically, so an interrupted frame is harmless; a periodic re-send makes delivery eventual.
+#define CMD_SET_SEG_BALANCE 0x94
+static void segbal_forward(uint32_t eff){
+ static uint32_t eff_sent = 0xFFFFFFFFu;
+ static uint16_t resend = 0;
+ static uint8_t tx[10];
+ if (eff == eff_sent && ++resend < 2048u) return; // re-assert every ~2 s (lost-frame heal)
+ if (waitingForLatch || huart2.gState != HAL_UART_STATE_READY) return; // retry a later poll
+ tx[0] = CMD_SET_SEG_BALANCE;
+ for (uint32_t n = 0; n <= 8u; n++) tx[1 + n] = (uint8_t)segbal_duty(n, eff);
+ if (HAL_UART_Transmit_DMA(&huart2, tx, 10) == HAL_OK) { eff_sent = eff; resend = 0; }
+}
+
+void segbal_poll(void){
+ static uint16_t last_ms = 0xFFFF;
+
+ if (displayMode == MODE_STANDBY) { segbal_mirror_live = 0; return; } // display is off — never (re)start its DMA here
+
+ uint32_t D = segbal_depth();
+ uint32_t eff = (seg_balance && D) ? segbal_strength() : 0;
+ segbal_forward(eff); // keep the date row in step (0 = identity/off)
+
+ // Gradual significance fade — the per-digit dimmer PR #9 anticipated. While any sub-second
+ // digit is mid-fade, run the mirror even with seg_balance off (identity duty) so digit_bright
+ // (0..FADE_MAX, recomputed each second from the live U(τ)) can scale each digit's cycles: the
+ // real display renders the partial fade the emulator always could. Columns c1/c2/c3 = ds/cs/ms;
+ // the decimal point (c0.high, cSegDP) follows digit_bright[3]. Big digits never fade.
+ uint32_t fading = significance_fade && D && countMode == COUNT_NORMAL &&
+ (digit_bright[0] < FADE_MAX || digit_bright[1] < FADE_MAX ||
+ digit_bright[2] < FADE_MAX || digit_bright[3] < FADE_MAX);
+
+ if (!eff && !fading) {
+ segbal_mirror_live = 0; // stop the ISR refresh before dropping the scan
+ if (display_scan_len != 5) setDisplayPWM(5); // live-disable: back to the stock scan
+ return;
+ }
+
+ // Refill at most once per ms: the masters change at most that fast (the sub-second digits
+ // exactly that fast), and the mirror may lag a main-loop pass behind them harmlessly.
+ uint16_t ms = (uint16_t)decisec*100 + (uint16_t)centisec*10 + millisec;
+ uint16_t want_len = (uint16_t)(5u * D);
+ if (ms == last_ms && display_scan_len == want_len) return;
+ last_ms = ms;
+
+ uint32_t revsh = (D == 16u) ? 0u : (D == 8u) ? 1u : 2u; // SEGBAL_REV16 shift for this depth
+ for (uint32_t col = 0; col < 5; col++) {
+ uint16_t mb = buffer_b[col];
+ uint8_t ml = buffer_c[col].low, mh = buffer_c[col].high;
+ uint32_t nb = (uint32_t)__builtin_popcount(mb & SEGBAL_BSEG_MASK);
+ // The GPIOC digit is 7 segments in .low plus its decimal point in .high (cSegDP). The REST of
+ // .high is that bank's one-cold column select + enables (set once in SysInit) — addressing,
+ // not LEDs — and must survive in every mirror slot, exactly like GPIOB's bCat bits.
+ uint32_t nc = (uint32_t)__builtin_popcount(ml) + ((mh >> 4) & 1u);
+ // duty on the 0..16 scale, rescaled to this depth (D=16 is exact; lit digits keep >= 1 cycle)
+ uint32_t sb = (segbal_duty(nb, eff) * D + 8u) / 16u; if (nb && !sb) sb = 1u;
+ uint32_t sc = (segbal_duty(nc, eff) * D + 8u) / 16u; if (nc && !sc) sc = 1u;
+ uint32_t sdp = sc; // the DP rides its digit's cycles...
+ if (fading) {
+ if (col >= 1 && col <= 3) { // ds/cs/ms: scale by live significance
+ uint32_t f = digit_bright[col - 1];
+ sc = (sc * f + 8u) / 16u;
+ if (f && ml && !sc) sc = 1u; // mid-fade digits never fully dark...
+ if (!f) sc = 0u; // ...but zero significance is zero
+ sdp = sc;
+ } else if (col == 0) { // ...except the seconds column's DP,
+ uint32_t f = digit_bright[3]; // which fades with the 0.1 s digit
+ sdp = (sc * f + 8u) / 16u;
+ if (f && !sdp) sdp = 1u;
+ if (!f) sdp = 0u;
+ }
+ }
+ uint16_t cat = mb & (uint16_t)~SEGBAL_BSEG_MASK;
+ uint8_t csel = mh & (uint8_t)~cSegDP; // GPIOC column select + enables
+ uint32_t ring = D - 1u; // mirror ranks 1..D-1 rotate mod D-1
+ uint32_t pb = SEGBAL_PH_B[col] % ring;
+ uint32_t pc = SEGBAL_PH_C[col] % ring;
+ uint16_t bm_b = 1u, bm_c = 1u, bm_dp = 1u; // cycle 0 = the master, always lit
+ for (uint32_t k = 1; k < D; k++) {
+ uint32_t i = col + 5u*k;
+ uint32_t r = (uint32_t)(SEGBAL_REV16[k] >> revsh); // 1..D-1 for k >= 1
+ uint32_t rb = r - 1u + pb; if (rb >= ring) rb -= ring;
+ uint32_t rc = r - 1u + pc; if (rc >= ring) rc -= ring;
+ uint32_t lb = (rb + 1u < sb), lc = (rc + 1u < sc), ldp = (rc + 1u < sdp);
+ bm_b |= (uint16_t)(lb << k); bm_c |= (uint16_t)(lc << k); bm_dp |= (uint16_t)(ldp << k);
+ buffer_b[i] = lb ? mb : cat; // column select stays in every slot
+ buffer_c[i].low = lc ? ml : 0;
+ buffer_c[i].high = csel | (ldp ? (mh & cSegDP) : 0);
+ }
+ segbal_lit_b[col] = bm_b; segbal_lit_c[col] = bm_c; segbal_lit_dp[col] = bm_dp;
+ }
+ if (display_scan_len != want_len) setDisplayPWM(want_len); // extend to the D-cycle scan
+ segbal_mirror_live = 1; // bitmaps valid — arm the ISR refresh
+}
+
+// SysTick-side mirror refresh: re-copy the live master values through the exported bitmaps every
+// millisecond, so the mirrors can never go staler than the masters (see the bitmap block above).
+// Cost with D=16: 5 x 15 slot writes, ~10 us at 80 MHz — 1% of one SysTick period, only while
+// the D-cycle scan is up. Bitmaps and masters are each written whole (halfword stores), so the
+// worst race with the main-loop refill is one slot showing one frame of the other's value.
+void segbal_isr_refresh(void){
+ if (!segbal_mirror_live || display_scan_len <= 5) return;
+ uint32_t D = (uint32_t)display_scan_len / 5u;
+ for (uint32_t col = 0; col < 5; col++) {
+ uint16_t mb = buffer_b[col];
+ uint8_t ml = buffer_c[col].low, mh = buffer_c[col].high;
+ uint16_t cat = mb & (uint16_t)~SEGBAL_BSEG_MASK;
+ uint8_t csel = mh & (uint8_t)~cSegDP;
+ uint16_t lb = segbal_lit_b[col], lc = segbal_lit_c[col], ldp = segbal_lit_dp[col];
+ for (uint32_t k = 1; k < D; k++) {
+ uint32_t i = col + 5u*k;
+ buffer_b[i] = ((lb >> k) & 1u) ? mb : cat;
+ buffer_c[i].low = ((lc >> k) & 1u) ? ml : 0;
+ buffer_c[i].high = csel | (((ldp >> k) & 1u) ? (mh & cSegDP) : 0);
+ }
+ }
+}
+
+// ---- $PMLOOP: main-loop latency diagnostic (`loop_diag = on` over serial or config) -----------
+// Once per second, emit the WORST gap (ms of uwTick) between consecutive profiler marks and the
+// tag of the section that produced it: $PMLOOP,,. Marks bracket the main loop's
+// sections; PendSV stamps tag 15 when it preempts, so the once-per-second display prep shows up
+// under its own name. Emission is lossy on a busy USB endpoint by design (1 Hz diagnostic).
+// Tags: 1 menu . 2 balance/colon . 3 tz-lookup . 4 delayed-housekeeping . 5 vbus/temp .
+// 6 pps-emit/tempcomp/dumps . 7 vbat/astro . 8 mode-pages/star . 9 alt/loop-tail . 15 PendSV
+volatile uint8_t loop_diag = 0;
+volatile uint8_t pmloop_lasttag = 0;
+static uint32_t pmloop_last = 0, pmloop_max = 0, pmloop_win = 0;
+static uint8_t pmloop_maxtag = 0;
+#define LP_MARK(n) do { uint32_t t_ = uwTick, g_ = t_ - pmloop_last; \
+ if (g_ > pmloop_max) { pmloop_max = g_; pmloop_maxtag = pmloop_lasttag; } \
+ pmloop_last = t_; pmloop_lasttag = (n); } while (0)
+
+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()
+
+// Colon brightness tracking. The colons are TIM2 PWM (buffer_colons_L/R), NOT part of the segment
+// scan and NOT coupled to dac_target — so out of the box they hold their animation brightness while
+// the digits dim around them, blazing at low rail. colon_balance ties the colon PWM to the rail:
+// 0/off (default) = stock (colons at full animation brightness)
+// 1/on = AUTO: scale the colon PWM by the calibrated rail curve (see colon_scale_for)
+// 2..256 = fixed manual scale (of 256) for eyeball calibration across the rail
+// The scale is folded into the animation buffer by loadColonAnimation and re-applied by
+// colon_balance_poll() from the main loop when the rail moves — never from the DMA/scan ISR.
+volatile uint16_t colon_balance = 0;
+static uint16_t colonScale = 256; // applied fixed-point scale, 256 = full animation brightness
+static volatile uint8_t colonForce = 0; // config set colon_balance — apply once even if within the AUTO hysteresis
+
+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;
+ }
+
+ // Track the rail: fold the current colon brightness into the freshly-loaded animation (256 = full,
+ // so colon_balance = off is exact identity). The DMA reads this buffer live — refilling it in place
+ // re-brightens the colons without a restart or a phase jump.
+ for (int k = 0; k < 200; k++) {
+ buffer_colons_L[k] = (uint16_t)(((uint32_t)buffer_colons_L[k] * colonScale) >> 8);
+ buffer_colons_R[k] = (uint16_t)(((uint32_t)buffer_colons_R[k] * colonScale) >> 8);
+ }
+}
+
+// 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();
+ }
+}
+
+// AUTO colon scale vs rail (dac_target 0 = brightest .. 4095 = dimmest). A full eyeballed sweep on a
+// production Mk IV (2026-07-11) landed a clean straight line: the colons want full animation scale at
+// the bright half of the rail and taper linearly to a dim floor at the darkest, so the separators stay
+// present but recessed while the digits blaze at low rail. Two anchors define it (both baked here and
+// overridable per-hardware from config.txt, exactly like the BS brightness curve):
+// colon_full_at — the dac at/below which colons run at full scale (256). Default 2048 (mid rail).
+// colon_floor — the minimum scale, reached at the dimmest rail (dac 4095). Default 20.
+// The line runs from (colon_full_at, 256) toward (4095, 0), clamped at the top to 256 and the bottom to
+// colon_floor: scale = clamp( 256*(4095-dac) / (4095-colon_full_at), colon_floor, 256 ). With the
+// defaults this is ~ (4095-dac)/8 and passes through the measured points (dac 3276->102, 2867->153).
+volatile int32_t colon_full_at = 2048; // baked default; config "colon_full_at = N" overrides
+volatile int32_t colon_floor = 20; // baked default; config "colon_floor = N" overrides
+static uint16_t colon_scale_for(int32_t d){
+ int32_t span = 4095 - colon_full_at; if (span < 1) span = 1; // guard div-by-zero / inverted anchor
+ int32_t s = 256 * (4095 - d) / span;
+ if (s > 256) s = 256;
+ if (s < colon_floor) s = colon_floor;
+ if (s < 0) s = 0;
+ return (uint16_t)s;
+}
+// Re-mirror the colon scale into the animation buffer when the live rail (or the config) moves it.
+// Main-loop only (loadColonAnimation touches 400 samples); throttled so it reloads on real change.
+void colon_balance_poll(void){
+ if (displayMode == MODE_STANDBY) return; // colons off (displayOff stops TIM2) — match segbal_poll's guard
+ uint16_t want = (colon_balance == 0) ? 256
+ : (colon_balance == 1) ? colon_scale_for((int32_t)dac_target)
+ : (colon_balance > 256) ? 256 : colon_balance;
+ int diff = (int)want - (int)colonScale; if (diff < 0) diff = -diff;
+ if (diff >= 4 || colonForce) { // hysteresis throttles AUTO drift; colonForce lands an explicit set
+ colonForce = 0;
+ colonScale = want;
+ // loadColonAnimation() also runs in the USART2 ISR (button -> nextMode -> applyColonForMode); mask
+ // JUST that IRQ so a button press can't re-enter mid-refill and tear buffer_colons. A few us, only
+ // on a real brightness step, and PPS (EXTI9_5) + GPS (USART1) are untouched.
+ NVIC_DisableIRQ(USART2_IRQn);
+ loadColonAnimation(); // refills buffer_colons at the new scale; DMA picks it up, no restart
+ NVIC_EnableIRQ(USART2_IRQn);
+ }
+}
+
+_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 4095
+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;
+
+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;
+}
+
+void parseConfigString(char *key, char *value, _Bool from_serial) {
+
+ 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, "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) {
+ 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) {
+
+ colonModeCivil = parseColonName(value);
+
+ } else if (strcasecmp(key, "colon_alt_mode") == 0) {
+
+ colonModeAlt = parseColonName(value); // shared by MODE_LST and MODE_SOLAR ("COLONALT" in the menu)
+ colonAltExplicit = 1;
+
+ } 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 (strcasecmp(key, "MODE_TEMPCOMP") == 0) {
+ set_mode_enabled(MODE_TEMPCOMP, value);
+ } 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, "MODE_ADEV") == 0) {
+ set_mode_enabled(MODE_ADEV, value);
+ } else if (strcasecmp(key, "MODE_STAR") == 0) {
+ set_mode_enabled(MODE_STAR, value);
+ } else if (strcasecmp(key, "tc_learn") == 0) {
+ tc_learn = truthy(value); // accumulate (die temp, ppm) samples while GPS-locked
+ } else if (strcasecmp(key, "tc_apply") == 0) {
+ tc_apply = truthy(value); // steer the SysTick timebase during GPS-loss holdover
+ } else if (strcasecmp(key, "significance_fade") == 0) {
+ significance_fade = truthy(value); // fade sub-second digits by significance in holdover, not dash
+ } else if (strcasecmp(key, "seg_balance") == 0) {
+ // Equalise per-segment brightness by duty (see segbal_poll). "on" (or 1) = AUTO, the
+ // calibrated strength-vs-rail curve — the intended setting. A numeric 2..300 applies a
+ // fixed manual strength (<=100 linear blend, >100 power-law) for tuning experiments.
+ int v = truthy(value) ? 1 : atoi(value);
+ seg_balance = (uint16_t)(v < 0 ? 0 : (v > 300 ? 300 : v));
+ } else if (strcasecmp(key, "colon_balance") == 0) {
+ // Dim the colons with the rail (see colon_balance_poll). "on"/1 = AUTO curve; a numeric 2..256
+ // pins a fixed scale (of 256) for eyeball calibration. The main-loop poll re-applies it.
+ int v = truthy(value) ? 1 : (falsey(value) ? 0 : atoi(value));
+ colon_balance = (uint16_t)(v < 0 ? 0 : (v > 256 ? 256 : v));
+ colonForce = 1; // land an explicit sweep step even if within the hysteresis band
+ } else if (strcasecmp(key, "colon_full_at") == 0) {
+ // Per-hardware anchor: the dac (0..4095) at/below which AUTO colons run at full scale. Overrides
+ // the baked default. Kept below full-scale so 4095-colon_full_at stays a positive span.
+ int v = atoi(value); colon_full_at = v < 0 ? 0 : (v > 4000 ? 4000 : v);
+ colonForce = 1;
+ } else if (strcasecmp(key, "colon_floor") == 0) {
+ // Per-hardware anchor: the AUTO colon scale (of 256) at the dimmest rail. Overrides the baked default.
+ int v = atoi(value); colon_floor = v < 0 ? 0 : (v > 256 ? 256 : v);
+ colonForce = 1;
+ } else if (strcasecmp(key, "tc_rtc") == 0) {
+ tc_rtc = truthy(value); // additionally trim RTC->CALR while GPS is absent
+ } else if (strcasecmp(key, "tc_t0") == 0) {
+ int v = atoi(value); tc_t0 = v < -30 ? -30 : (v > 80 ? 80 : v);
+ } else if (strcasecmp(key, "tc_engage_s") == 0) {
+ // Floor of 2: currentTime pre-increments at the modelled .900 mark, so "fresh" reads 1
+ // for the last 100 ms of every LOCKED second — a floor of 1 would engage during lock.
+ int v = atoi(value); tc_engage_s = v < 2 ? 2 : (v > 3600 ? 3600 : v);
+ } else if (strcasecmp(key, "tc_max_ppm") == 0) {
+ int v = atoi(value); tc_max_ppm = v < 1 ? 1 : (v > 200 ? 200 : v);
+ } else if (strcasecmp(key, "tc_hse_b") == 0) { tc_parse_coeff(value, &tc_cfg_hse[1]);
+ } else if (strcasecmp(key, "tc_hse_c") == 0) { tc_parse_coeff(value, &tc_cfg_hse[2]);
+ } else if (strcasecmp(key, "tc_lse_a") == 0) { tc_parse_coeff(value, &tc_cfg_lse[0]);
+ } else if (strcasecmp(key, "tc_lse_b") == 0) { tc_parse_coeff(value, &tc_cfg_lse[1]);
+ } else if (strcasecmp(key, "tc_lse_c") == 0) { tc_parse_coeff(value, &tc_cfg_lse[2]);
+ } else if (strcasecmp(key, "tc_seed") == 0) {
+ tc_seed = truthy(value); // load the coefficients above as an EVOLVING prior, not a freeze
+ // Over serial, the "tc_seed = on" line is the TRIGGER: it arms a main-loop apply, so a paste
+ // seeds exactly once, after all its coefficient lines have parsed (send it last — the order
+ // tc_dump prints). ISR-safe by design: the apply itself always runs from tc_housekeeping.
+ if (from_serial && tc_seed) tc_seed_pending = 1;
+ } else if (strcasecmp(key, "tc_seed_lo") == 0) {
+ tc_seed_lo = (int16_t)atoi(value); // seed coverage low edge (die °C) — the prior is not extrapolated
+ } else if (strcasecmp(key, "tc_seed_hi") == 0) {
+ tc_seed_hi = (int16_t)atoi(value);
+ } else if (strcasecmp(key, "tc_dump") == 0) {
+ // Serial-only trigger: print the learned model as paste-ready config lines. A stray
+ // tc_dump left in config.txt must not fire on every (re)load, hence the origin guard.
+ if (from_serial && truthy(value)) tc_dump_pending = 1;
+ } else if (strcasecmp(key, "adev_dump") == 0) {
+ if (from_serial && truthy(value)) adev_dump_pending = 1; // serial-only: emit one $PMADEV sentence
+ } else if (strcasecmp(key, "hdev_dump") == 0) {
+ if (from_serial && truthy(value)) hdev_dump_pending = 1; // serial-only: emit one $PMHDEV sentence (drift-immune Hadamard)
+ } else if (strcasecmp(key, "star_dump") == 0) {
+ if (from_serial && truthy(value)) star_dump_pending = 1; // serial-only: emit one $PMSTAR sentence
+ } else if (strcasecmp(key, "star_max_mag") == 0) {
+ float smm = (float)atof(value); // trim the transit catalogue to stars brighter than this (applied at boot in loadStars)
+ if (isfinite(smm)) star_max_mag = smm; // atof("nan") is a real NaN -> the float->int16 magcut cast would be UB
+ } else if (strcasecmp(key, "loop_diag") == 0) {
+ loop_diag = truthy(value) ? 1 : 0; // 1 Hz $PMLOOP main-loop latency diagnostic
+ } else if (strcasecmp(key, "tc_reset") == 0) {
+ if (from_serial && truthy(value)) tc_reset_pending = 1; // serial-only, same guard
+
+ } else if (key[0]=='B' && key[1]=='S' && key[3]==0) { //BS1, BS2, etc
+ if (!key[2] || key[2]<'1' || key[2]>'0'+sizeof(brightnessCurve)/sizeof(brightnessCurve[0])) return;
+
+ 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){
+ // 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;
+ for (uint8_t i=0; i=2)) {
+ parseConfigString(key, value, 1); // serial origin: tc_dump/tc_reset may fire
+ // rxConfigString runs in the USB OTG ISR; postConfigCleanup() calls nextMode()
+ // and sendDate(), which are non-reentrant against the SysTick repaint. Defer it
+ // to the main loop so it runs in thread context, like the file-config path does.
+ delayedPostConfigCleanup=1;
+ }
+ 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
+ // 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;
+ }
+#endif
+
+ config.tolerance_1ms = 1000;
+ config.tolerance_10ms = 10000;
+ config.tolerance_100ms = 100000;
+ config.zone_override = 0;
+ config.brightness_override = -1.0;
+ colonModeCivil = 0;
+ colonModeAlt = COLON_MODE_ALT_SAWTOOTH;
+ colonAltExplicit = 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.
+
+// ---- Live Allan deviation of the FREE-RUNNING HSE (MODE_ADEV) ----------------------------------
+// The honest oscillator-stability signal is the free-running DWT phase, NOT the SysTick residual:
+// capturePPS() re-pins SysTick every second (an infinite-gain phase reset), so the residual's ADEV
+// rolls off artificially past tau=1 s and measures the discipline loop, not the crystal. DWT->CYCCNT
+// is never re-pinned; its per-second delta minus the 80 MHz core count is the bare fractional-
+// frequency error. We integrate that to cumulative phase (int32 ticks, 12.5 ns) in a RAM2 ring and
+// compute the overlapping Allan deviation (IEEE-1139) on demand. A host can reconstruct the
+// disciplined-output curve from $PMTXTS. Verified vs a double-precision reference in the emulator.
+#define ADEV_N 4096u // 1 Hz phase samples (16 KB of RAM2); tau to 1024 s @ N-2m=2048 overlaps
+#define ADEV_OCT 11u // octave taus 1,2,4,...,1024 s
+#define ADEV_FCPU 80000000 // core ticks per true GPS second
+#ifdef __EMSCRIPTEN__
+static int32_t adev_x[ADEV_N]; // emu: plain static (no RAM2 section)
+#else
+__attribute__((section(".ram2"))) static int32_t adev_x[ADEV_N]; // RAM2 @ 0x10000000, off the CRC path
+#endif
+static uint32_t adev_prev_dwt, adev_prev_epoch;
+// Cumulative phase, ticks. UNSIGNED on purpose: with a static ppm-scale offset the phase ramps
+// monotonically and an int32 accumulator hits signed-overflow UB after ~311 days of continuous lock
+// at 1 ppm. Unsigned wrap is defined; the ring stores the (int32_t) view and the second difference
+// is computed back in unsigned, so everything stays exact modulo 2^32 (true |d| << 2^31 always).
+static uint32_t adev_phase_u;
+static uint16_t adev_widx, adev_valid;
+static uint8_t adev_have_prev;
+static float adev_sigma_cache[ADEV_OCT];
+static volatile uint8_t adev_noct;
+static volatile uint32_t adev_last_ms; // uwTick of the last accepted sample: the display's staleness tell
+
+static void adev_reset(void){
+ memset((void*)adev_x, 0, sizeof adev_x); // RAM2 is NOT zeroed at boot — clear explicitly
+ adev_phase_u=0; adev_widx=0; adev_valid=0; adev_have_prev=0; adev_noct=0;
+ for (uint32_t i=0;i restart at 0
+ adev_phase_u=0; adev_x[0]=0; adev_widx=1; adev_valid=1;
+ adev_noct=0; // honest NOW, not at the next page flip
+ }
+ adev_prev_dwt=dwt; adev_prev_epoch=epoch; adev_have_prev=1;
+}
+// Overlapping Allan deviation for averaging factor m (tau = m s), read time-ordered across the ring.
+static float adev_sigma_for_m(uint32_t m){
+ uint32_t N=adev_valid;
+ if (N < 2u*m+1u) return 0.0f;
+ uint16_t base=(adev_valid fractional frequency (dimensionless)
+}
+// Recompute the octave cache (thread context). Publish adev_noct under IRQ mask (torn-read guard).
+// PUBLICATION gate: valid >= 4m (>= 2m overlapping triplets), stricter than the mathematical minimum
+// 2m+1 — at exactly 2m+1 an octave is a SINGLE second-difference shown at full display authority
+// (~100% error bars). adev_sigma_for_m itself keeps the mathematical gate for the test/oracle path.
+static void adev_reduce(void){
+ uint8_t noct=0;
+ for (uint8_t k=0;kCYCCNT; \
+ 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; \
+ adev_push_dwt(pps_cap.dwt_pps, pps_cap.epoch); /* free-running Allan-deviation sample */ \
+ } 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.
+// 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;
+ 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
+
+ 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)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];
+
+ 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;
+}
+
+// ==================== Temperature compensation (opt-in; state near pps_cap) ====================
+// Everything below runs in the MAIN LOOP only (float allowed, calibrateRTC precedent). The tick
+// ISRs see just three precomputed int32s via the tc_steer_* handoff in the timetick() hook.
+
+static uint32_t tc_nom_load = 0; // SysTick->LOAD captured before any steering (80 MHz: 79999)
+
+// Ticks per ppm, derived from the captured nominal period so no core-clock assumption is baked
+// in: one second is (LOAD+1)*1000 ticks, so 1 ppm = (LOAD+1)/1000 ticks (80 at 80 MHz).
+// Verified against the live unit: $PMTXTS reports load=79999 (10 MHz TCXO -> PLL -> 80 MHz).
+static int32_t tc_tpp(void){ return (int32_t)((tc_nom_load + 1) / 1000); }
+
+static int tc_bin_i(int t){ int i = (t + 8) / 2; return i < 0 ? 0 : (i > 39 ? 39 : i); }
+
+// One HSE sample per GPS-locked second. The PPS ISRs re-zero the ms cascade at every edge
+// (SysTick->VAL reload + counter reset), so each capture is already a SELF-CONTAINED one-second
+// accumulation: pos = const + tpp·ppm(T), where const is a fixed capture/reload offset and
+// tpp = ticks per ppm. Measured on the live unit: pos = 79925.8 ± 0.67 ticks (~8 ns RMS) — the
+// constant dominates and is unknowable from lock data alone, so the model is learned in ticks
+// with an ARBITRARY ORIGIN, rebased to the first accepted sample to keep bin sums small. Its
+// differences over temperature are exact, and holdover steering only ever applies
+// model(T_now) − model(T_at_loss), from which the origin cancels. (An earlier draft differenced
+// consecutive captures — but the per-edge cascade reset makes that identically ~0; verified on
+// hardware: dpos = 0.05 ± 1.1 ticks.)
+static int32_t tc_e0 = 0; // origin rebase: first accepted sample
+static _Bool tc_e0_set = 0;
+static int32_t tc_ema = 0; // slow tracker for the glitch gate
+static void tc_hse_learn(void){
+ static uint32_t last_seq = 0;
+ static uint8_t warm = 0;
+
+ __disable_irq(); // tear-free copy (emitPPSTimestamp pattern)
+ uint32_t seq = pps_cap.seq;
+ uint16_t subms = pps_cap.subms;
+ uint32_t st = pps_cap.systick;
+ int16_t temp = pps_cap.temp;
+ uint8_t flags = pps_cap.flags;
+ __enable_irq();
+
+ if (seq == last_seq) return; // no new edge since last pass
+ _Bool contiguous = (seq == last_seq + 1);
+ last_seq = seq;
+
+ if ((flags & 0x3) != 0x3 || subms > 999) { warm = 0; return; }
+ if (!contiguous) { warm = 0; return; } // edges were missed: settle again
+ if (warm < 10) { warm++; return; } // settle after (re)acquisition
+
+ int32_t half = (int32_t)(tc_nom_load + 1) * 500; // half a second in ticks
+ int32_t e = (int32_t)subms * (int32_t)(tc_nom_load + 1)
+ + (int32_t)tc_nom_load - (int32_t)st; // this second's accumulation (+ const)
+ if (e > half) e -= 2 * half; // fold the origin into ±half a second
+ if (e < -half) e += 2 * half;
+
+ int32_t tpp = tc_tpp(); // ticks per ppm (80 at 80 MHz)
+ if (!tc_e0_set) { tc_e0 = e; tc_ema = 0; tc_e0_set = 1; }
+ e -= tc_e0; // arbitrary-origin rebase (keeps sums int32-safe)
+ if (e - tc_ema > 100 * tpp || e - tc_ema < -100 * tpp) return; // >100 ppm step: glitch
+ if (e > 30000 || e < -30000) return; // hard cap so 32768·|e| can never overflow int32
+ tc_ema += (e - tc_ema) / 16;
+
+ struct tc_bin *b = &tc_bins[tc_bin_i(temp)];
+ if (b->hse_n >= 8) { // per-bin outlier gate: 10 ppm off the mean
+ int32_t d = e - b->hse_sum / (int32_t)b->hse_n;
+ if (d > 10 * tpp || d < -10 * tpp) return;
+ }
+ if (b->hse_n >= 32768) { b->hse_sum /= 2; b->hse_n /= 2; } // overflow-proof aging
+ b->hse_sum += e; b->hse_n++;
+ tc_n_hse++;
+}
+
+// One LSE sample per successful RTC calibration: calibrateRTC only advances the BKP31R stamp
+// on an in-range 63 s measurement, so watching the stamp inherits its validity gate for free.
+static void tc_lse_learn(void){
+ static uint32_t seen = 0;
+ uint32_t cal = rtc_last_calibration;
+ if (cal == seen) return;
+ _Bool first = (seen == 0);
+ seen = cal;
+ if (first) return; // boot-time stamp, not a fresh measurement
+ int32_t v = debug_rtc_val; // raw LSE cycle error over CAL_PERIOD (63 s)
+ if (v > 1000 || v < -1000) return;
+ struct tc_bin *b = &tc_bins[tc_bin_i(die_temp_c)];
+ if (b->lse_n >= 32768) { b->lse_sum /= 2; b->lse_n /= 2; }
+ b->lse_sum += v; b->lse_n++;
+ tc_n_lse++;
+}
+
+// Solve A·x = y for a 3x3 symmetric system by Gaussian elimination with partial pivoting.
+static _Bool tc_gauss3(float A[3][3], float y[3], float x[3]){
+ int p[3] = {0, 1, 2};
+ for (int c = 0; c < 3; c++){
+ int best = c;
+ for (int r = c + 1; r < 3; r++)
+ if (fabsf(A[p[r]][c]) > fabsf(A[p[best]][c])) best = r;
+ int t = p[c]; p[c] = p[best]; p[best] = t;
+ if (fabsf(A[p[c]][c]) < 1e-9f) return 0;
+ for (int r = c + 1; r < 3; r++){
+ float f = A[p[r]][c] / A[p[c]][c];
+ for (int k = c; k < 3; k++) A[p[r]][k] -= f * A[p[c]][k];
+ y[p[r]] -= f * y[p[c]];
+ }
+ }
+ for (int c = 2; c >= 0; c--){
+ float s = y[p[c]];
+ for (int k = c + 1; k < 3; k++) s -= A[p[c]][k] * x[k];
+ x[c] = s / A[p[c]][c];
+ }
+ return 1;
+}
+
+// Weighted least-squares fit of y(T) = a + b·x + c·x², x = T - tc_t0, over bin means.
+// Falls back quadratic -> linear -> constant as temperature coverage thins. `scale` converts
+// bin units (HSE: 1.0 — model stays in ticks, arbitrary origin; LSE: raw 63 s cal cycles → ppm).
+// A fit is only accepted if every coefficient is finite: a near-singular system can pass the
+// pivot threshold yet overflow to Inf/NaN, and NaN must never reach the steering or display.
+static _Bool tc_fin3(const float m[3]){ return isfinite(m[0]) && isfinite(m[1]) && isfinite(m[2]); }
+
+// Returns the ACHIEVED model order: 3 quadratic, 2 linear, 1 constant, 0 no fit. (The order lets the
+// warm-start prior be preserved until real data supports a fit at least as rich — see tc_fit.)
+static int tc_fit_one(_Bool lse, float scale, uint16_t n_quad, float m[3],
+ int16_t *tmin_out, int16_t *tmax_out){
+ float S[5] = {0,0,0,0,0}, T[3] = {0,0,0};
+ float S0a = 0, T0a = 0; // all-samples weighted mean (constant fallback)
+ int nb = 0, tmin = 127, tmax = -128;
+ int tmin_a = 127, tmax_a = -128;
+
+ for (int i = 0; i < 40; i++){
+ uint16_t n = lse ? tc_bins[i].lse_n : tc_bins[i].hse_n;
+ if (!n) continue;
+ int32_t sum = lse ? tc_bins[i].lse_sum : tc_bins[i].hse_sum;
+ int t = i * 2 - 8; // bin low edge; bin holds {t, t+1}
+ float y = ((float)sum / (float)n) * scale;
+ float w = (float)n;
+ S0a += w; T0a += w * y;
+ if (t < tmin_a) tmin_a = t;
+ if (t > tmax_a) tmax_a = t;
+ if (n < n_quad) continue; // curve terms only from well-filled bins
+ float x = ((float)t + 0.5f) - (float)tc_t0; // true bin centre: t + 0.5
+ nb++;
+ if (t < tmin) tmin = t;
+ if (t > tmax) tmax = t;
+ S[0] += w; S[1] += w*x; S[2] += w*x*x;
+ S[3] += w*x*x*x; S[4] += w*x*x*x*x;
+ T[0] += w*y; T[1] += w*x*y; T[2] += w*x*x*y;
+ }
+
+ if (nb >= 3 && (tmax - tmin) >= 6) { // quadratic
+ float A[3][3] = {{S[0],S[1],S[2]},{S[1],S[2],S[3]},{S[2],S[3],S[4]}};
+ float yv[3] = {T[0],T[1],T[2]};
+ if (tc_gauss3(A, yv, m) && tc_fin3(m)) { *tmin_out = tmin; *tmax_out = tmax; return 3; }
+ }
+ if (nb >= 2 && (tmax - tmin) >= 4) { // linear
+ float det = S[0]*S[2] - S[1]*S[1];
+ if (fabsf(det) > 1e-9f){
+ m[0] = (T[0]*S[2] - T[1]*S[1]) / det;
+ m[1] = (S[0]*T[1] - S[1]*T[0]) / det;
+ m[2] = 0;
+ if (tc_fin3(m)) { *tmin_out = tmin; *tmax_out = tmax; return 2; }
+ }
+ }
+ if (S0a >= (lse ? 8.0f : 60.0f)) { // constant: the dominant fixed offset
+ m[0] = T0a / S0a; m[1] = 0; m[2] = 0;
+ if (tc_fin3(m)) { *tmin_out = tmin_a; *tmax_out = tmax_a; return 1; }
+ }
+ return 0;
+}
+
+static float tc_poly(const float m[3], float x); // fwd: the residual pass evaluates the just-fit model
+
+// Weighted-RMS residual of a fitted model over every populated bin (not only the curve-eligible
+// ones): sqrt( Σ n·(bin_mean − model(T))² / Σ n ), in the fit's units. Runs as a second pass, so
+// it never perturbs the fit itself; 40 bins, at most once per 5 min alongside tc_fit().
+// `center` removes the weighted-mean of the residuals before the RMS — i.e. an ORIGIN-INVARIANT error.
+// Needed for HSE: its model origin (m[0]) is arbitrary, so while a warm-start seed is held (m[0]=0) the
+// real bins carry a different DC constant (the tc_e0 rebase); the raw offset would swamp the RMS and
+// corrupt the holdover-fade σ_temp. Mean-centring measures only how well the SLOPE/CURVATURE match,
+// which is all HSE cares about. LSE (absolute ppm, real m[0]) passes center=0 — its offset is real error.
+static float tc_fit_resid(_Bool lse, float scale, const float m[3], _Bool center){
+ float sw = 0, swr = 0, swr2 = 0;
+ for (int i = 0; i < 40; i++){
+ uint16_t n = lse ? tc_bins[i].lse_n : tc_bins[i].hse_n;
+ if (!n) continue;
+ int32_t sum = lse ? tc_bins[i].lse_sum : tc_bins[i].hse_sum;
+ float y = ((float)sum / (float)n) * scale;
+ float x = ((float)(i * 2 - 8) + 0.5f) - (float)tc_t0; // bin centre − model origin
+ float r = y - tc_poly(m, x);
+ sw += (float)n; swr += (float)n * r; swr2 += (float)n * r * r;
+ }
+ if (!(sw > 0) || !isfinite(swr2)) return 0.0f;
+ if (center) swr2 -= swr * swr / sw; // Σn(r−r̄)² = Σn·r² − (Σn·r)²/Σn
+ return (swr2 > 0 && isfinite(swr2)) ? sqrtf(swr2 / sw) : 0.0f;
+}
+
+// Refit both models from the bins. A warm-start prior (tc_*_prior != 0) is PRESERVED until real data
+// supports a fit at least as rich as the seed's order — then real data takes over (prior cleared).
+// With no prior held (the normal cold-learn case) this is the original behaviour: fit -> adopt/invalidate.
+// The residual is always re-measured against whatever model is held; while a seed is still held with no
+// real samples yet, tc_fit_resid returns 0 (no data) and the seed's CARRIED residual is kept.
+static void tc_fit(void){
+ int16_t tmn, tmx; float m[3];
+
+ int oh = tc_fit_one(0, 1.0f, 64, m, &tmn, &tmx);
+ if (oh >= tc_hse_prior) { // real data at least as rich as the prior -> adopt it
+ tc_hse_valid = (oh > 0);
+ if (oh) { tc_hse_m[0]=m[0]; tc_hse_m[1]=m[1]; tc_hse_m[2]=m[2]; tc_hse_tmin=tmn; tc_hse_tmax=tmx; }
+ tc_hse_prior = 0;
+ }
+ if (tc_hse_valid) { float r = tc_fit_resid(0, 1.0f, tc_hse_m, 1); if (r > 0 || !tc_hse_prior) tc_hse_resid = r; }
+ else tc_hse_resid = 0;
+
+ const float lse_scale = 1e6f/(32768.0f*63.0f);
+ int ol = tc_fit_one(1, lse_scale, 4, m, &tmn, &tmx);
+ if (ol >= tc_lse_prior) {
+ tc_lse_valid = (ol > 0);
+ if (ol) { tc_lse_m[0]=m[0]; tc_lse_m[1]=m[1]; tc_lse_m[2]=m[2]; tc_lse_tmin=tmn; tc_lse_tmax=tmx; }
+ tc_lse_prior = 0;
+ }
+ if (tc_lse_valid) { float r = tc_fit_resid(1, lse_scale, tc_lse_m, 0); if (r > 0 || !tc_lse_prior) tc_lse_resid = r; }
+ else tc_lse_resid = 0;
+}
+
+static float tc_poly(const float m[3], float x){ return m[0] + m[1]*x + m[2]*x*x; }
+
+// Warm-start the tempco model from the seeded coefficients (tc_seed = on). Loads tc_hse_b/c and
+// tc_lse_a/b/c — the last tc_dump, persisted in config.txt by the host — as the LIVE model so the
+// clock is temperature-compensated from the first second, records the seed's order (kept by tc_fit
+// until real data is at least as rich), and seeds a conservative residual so holdover-fade stays
+// honest before real samples arrive. Clearing the frozen slots is what turns a freeze into an
+// evolving prior. Runs once per boot; on a later live config reload it only re-clears the reparsed
+// coefficients so the freeze path can't silently re-activate over the evolving model.
+static void tc_seed_apply(void){
+ if (!tc_seed) return;
+ if (tc_seed_done) { // live reload: keep evolving — don't re-freeze or re-seed
+ tc_cfg_hse[0]=tc_cfg_hse[1]=tc_cfg_hse[2]=NAN;
+ tc_cfg_lse[0]=tc_cfg_lse[1]=tc_cfg_lse[2]=NAN;
+ return;
+ }
+ _Bool have_hse = !isnan(tc_cfg_hse[1]) || !isnan(tc_cfg_hse[2]);
+ _Bool have_lse = !isnan(tc_cfg_lse[0]) || !isnan(tc_cfg_lse[1]) || !isnan(tc_cfg_lse[2]);
+ if (!have_hse && !have_lse) return; // seed enabled but no coefficients yet — wait for a reload
+ tc_seed_done = 1;
+
+ int16_t lo = tc_seed_lo, hi = tc_seed_hi;
+ if (hi - lo < 4) { lo = (int16_t)(tc_t0 - 6); hi = (int16_t)(tc_t0 + 6); } // sane span if none given
+ // readConfigFile (hence this seed) runs at boot BEFORE the first tc_housekeeping, so tc_nom_load is
+ // still 0 and tc_tpp() would return 0 — which would silently zero the tick-domain HSE model. Capture
+ // the nominal SysTick period here (SystemClock_Config set it before the config load) — the same
+ // capture tc_housekeeping() also performs.
+ if (!tc_nom_load) tc_nom_load = SysTick->LOAD;
+ float tpp = (float)tc_tpp();
+
+ if (have_hse) {
+ tc_hse_m[0] = 0.0f; // arbitrary origin — steering uses temperature differences
+ tc_hse_m[1] = isnan(tc_cfg_hse[1]) ? 0.0f : tc_cfg_hse[1] * tpp;
+ tc_hse_m[2] = isnan(tc_cfg_hse[2]) ? 0.0f : tc_cfg_hse[2] * tpp;
+ tc_hse_tmin = lo; tc_hse_tmax = hi; tc_hse_valid = 1;
+ tc_hse_prior = (!isnan(tc_cfg_hse[2]) && tc_cfg_hse[2] != 0.0f) ? 3 : 2;
+ tc_hse_resid = 2.0f * tpp; // conservative (~2 ppm) until real data measures it
+ tc_cfg_hse[0] = tc_cfg_hse[1] = tc_cfg_hse[2] = NAN; // seed replaces freeze -> free to evolve
+ }
+ if (have_lse) {
+ tc_lse_m[0] = isnan(tc_cfg_lse[0]) ? 0.0f : tc_cfg_lse[0];
+ tc_lse_m[1] = isnan(tc_cfg_lse[1]) ? 0.0f : tc_cfg_lse[1];
+ tc_lse_m[2] = isnan(tc_cfg_lse[2]) ? 0.0f : tc_cfg_lse[2];
+ tc_lse_tmin = lo; tc_lse_tmax = hi; tc_lse_valid = 1;
+ tc_lse_prior = (!isnan(tc_cfg_lse[2]) && tc_cfg_lse[2] != 0.0f) ? 3
+ : (!isnan(tc_cfg_lse[1]) && tc_cfg_lse[1] != 0.0f) ? 2 : 1;
+ tc_lse_resid = 2.0f;
+ tc_cfg_lse[0] = tc_cfg_lse[1] = tc_cfg_lse[2] = NAN;
+ }
+}
+
+// LSE model (absolute ppm): non-NAN config a freezes it (the user asserted the values);
+// otherwise the learned fit, clamped to its observed temperature range (no extrapolation).
+// Config values are USB-ISR-written; snapshot each element once (single-word reads are atomic).
+static _Bool tc_model_lse(int t, float *ppm){
+ float a = tc_cfg_lse[0], b = tc_cfg_lse[1], c = tc_cfg_lse[2];
+ if (!isnan(a)) {
+ if (isnan(b)) b = 0;
+ if (isnan(c)) c = 0;
+ float x = (float)t - (float)tc_t0;
+ *ppm = a + b*x + c*x*x;
+ return isfinite(*ppm);
+ }
+ if (!tc_lse_valid) return 0;
+ if (t < tc_lse_tmin) t = tc_lse_tmin;
+ if (t > tc_lse_tmax) t = tc_lse_tmax;
+ *ppm = tc_poly(tc_lse_m, (float)t - (float)tc_t0);
+ return 1;
+}
+
+// HSE steering delta in TICKS between two temperatures. The learned model's origin is
+// arbitrary (see tc_hse_learn), so only differences are meaningful — which is exactly what
+// holdover needs: at GPS loss the display is phase-true, and the error that then accrues is
+// the temperature-driven CHANGE of the oscillator, model(T_now) − model(T_loss). Frozen config
+// coefficients are in ppm; a cancels in the difference, so only tc_hse_b/c are required.
+static _Bool tc_hse_delta(int t_now, int t_ref, int32_t *dticks){
+ float b = tc_cfg_hse[1], c = tc_cfg_hse[2];
+ if (!isnan(b)) { // frozen: b (and optionally c) from config
+ if (isnan(c)) c = 0;
+ float x1 = (float)t_now - (float)tc_t0, x0 = (float)t_ref - (float)tc_t0;
+ float dppm = (b*x1 + c*x1*x1) - (b*x0 + c*x0*x0);
+ if (!isfinite(dppm)) return 0;
+ *dticks = (int32_t)lroundf(dppm * (float)tc_tpp());
+ return 1;
+ }
+ if (!tc_hse_valid) return 0;
+ if (t_now < tc_hse_tmin) t_now = tc_hse_tmin; // no extrapolation past learned coverage
+ if (t_now > tc_hse_tmax) t_now = tc_hse_tmax;
+ if (t_ref < tc_hse_tmin) t_ref = tc_hse_tmin;
+ if (t_ref > tc_hse_tmax) t_ref = tc_hse_tmax;
+ float d = tc_poly(tc_hse_m, (float)t_now - (float)tc_t0)
+ - tc_poly(tc_hse_m, (float)t_ref - (float)tc_t0);
+ if (!isfinite(d)) return 0;
+ *dticks = (int32_t)lroundf(d); // learned model is already in ticks
+ return 1;
+}
+
+// Once-per-second control: evaluate the models at the current die temperature, refresh the
+// display cache, engage/disengage SysTick steering, and (optionally) trim RTC->CALR.
+static void tc_governor(void){
+ static uint32_t last_run = 0;
+ static int32_t applied_E = 0; // ticks/second currently steered
+ static _Bool was_on = 0;
+ static int16_t t_loss = 0; // die temp captured when steering engaged
+ static int32_t last_steps = 0x7FFF; // last CALR trim written (sentinel: none)
+ static uint32_t last_calr = 0;
+
+ // Snapshot the two ISR-written time variables together: currentTime increments at the
+ // modelled .900 mark while last_pps_time updates at the edge, and reading them separately
+ // can interleave with both ISRs and yield a wrapped-huge "fresh" that spuriously engages.
+ __disable_irq();
+ uint32_t now = (uint32_t)currentTime;
+ uint32_t lpps = last_pps_time;
+ __enable_irq();
+ if (now == last_run) return;
+ last_run = now;
+
+ uint32_t fresh = now - lpps; // seconds since the last PPS edge
+ if (fresh > 0x80000000u) fresh = 0; // interleaved-read underflow: treat as fresh
+
+ int t = die_temp_c;
+ float lp = 0;
+ _Bool have_l = tc_model_lse(t, &lp);
+ int32_t tpp = tc_tpp();
+
+ // Would-be steering delta at the current temperatures (also feeds the display)
+ int32_t dt_now = 0;
+ _Bool have_h = tc_hse_delta(t, was_on ? t_loss : t, &dt_now);
+
+ // display cache (clamped so "HSE -99.99" never exceeds the 10-char row).
+ // HSE page shows the ACTIVE steering correction in ppm (0.00 while locked — the PPS
+ // discipline owns the phase then); LSE page shows the absolute model ppm.
+ float dh = was_on ? (float)applied_E / (float)tpp : 0.0f;
+ float dl = lp;
+ if (dh > 99.99f) dh = 99.99f;
+ if (dh < -99.99f) dh = -99.99f;
+ if (dl > 99.99f) dl = 99.99f;
+ if (dl < -99.99f) dl = -99.99f;
+ tc_disp_hse = dh; tc_disp_hse_ok = have_h;
+ tc_disp_lse = dl; tc_disp_lse_ok = have_l;
+ tc_disp_state = tc_steer_on ? 'A'
+ : (!isnan(tc_cfg_hse[1]) || !isnan(tc_cfg_lse[0])) ? 'F'
+ : (tc_hse_prior || tc_lse_prior) ? 'S' // running on the warm-start seed (evolving)
+ : (tc_learn && fresh < 5) ? 'L' : '-';
+
+ // --- HSE steering: engage only in holdover, after first-ever fix, with a usable model.
+ // The correction is the temperature-driven CHANGE since GPS loss (origin cancels; see
+ // tc_hse_delta). At the loss instant the delta is 0 by construction and grows only as the
+ // die temperature moves, so engage is glitch-free and re-lock needs no unwinding beyond
+ // the LOAD restore (the per-edge phase snap owns lock).
+ if (tc_apply && had_pps && fresh >= tc_engage_s) {
+ if (!was_on) t_loss = (int16_t)t; // remember the temperature we lost GPS at
+ int32_t target = 0;
+ if (tc_hse_delta(t, t_loss, &target)) {
+ int32_t lim = (int32_t)tc_max_ppm * tpp;
+ if (target > lim) target = lim;
+ if (target < -lim) target = -lim;
+ if (!was_on) applied_E = target; // 0 at engage by construction...
+ else { // ...then gentle slew (temp-quantisation steps)
+ int32_t slew = tpp / 4; // 0.25 ppm per second
+ int32_t d = target - applied_E;
+ if (d > slew) d = slew;
+ if (d < -slew) d = -slew;
+ applied_E += d;
+ }
+ int32_t base = applied_E >= 0 ? applied_E / 1000 : -((-applied_E + 999) / 1000);
+ int32_t rem = applied_E - base * 1000; // floor-division remainder, always [0,1000)
+ __disable_irq();
+ tc_load_base = (int32_t)tc_nom_load + base;
+ tc_rem = rem;
+ tc_steer_on = 1;
+ __enable_irq();
+ was_on = 1;
+ } else if (was_on) { // model became unusable mid-holdover
+ tc_steer_on = 0;
+ SysTick->LOAD = tc_nom_load;
+ tc_acc = 0;
+ applied_E = 0;
+ was_on = 0;
+ }
+ } else if (was_on) {
+ tc_steer_on = 0; // flag first: the ISR stops writing LOAD...
+ SysTick->LOAD = tc_nom_load; // ...then restore the nominal period
+ tc_acc = 0;
+ applied_E = 0;
+ was_on = 0;
+ }
+
+ // --- LSE -> RTC->CALR trim: power-loss insurance only (display time is HSE-driven) ---
+ // calibrateRTC() owns CALR while locked (it runs from the PPS ISRs, which are silent now);
+ // the first successful calibration after re-lock re-measures and overwrites this trim.
+ // While PPS is fresh, forget our last write: calibrateRTC has since replaced CALR, so an
+ // equal-valued model trim in the NEXT outage must not be skipped by the != guard.
+ if (fresh <= 63) last_steps = 0x7FFF;
+ if (tc_rtc && have_l && fresh > 63 && now - last_calr >= 60) {
+ int32_t steps = (int32_t)lroundf(lp * (1048576.0f / 1000000.0f)); // ppm -> CALM steps
+ if (steps > 255) steps = 255;
+ if (steps < -255) steps = -255;
+ if (steps != last_steps && !(RTC->ISR & RTC_ISR_RECALPF)) {
+ // IRQ-off around the WPR unlock/write/relock triplet: PendSV's write_rtc() (runs each
+ // second in holdover) does its own WPR sequence, and a preemption between our key
+ // writes and the CALR store would leave the store silently ignored.
+ __disable_irq();
+ __HAL_RTC_WRITEPROTECTION_DISABLE(&hrtc);
+ RTC->CALR = 0x100 + steps; // same midpoint convention as calibrateRTC
+ __HAL_RTC_WRITEPROTECTION_ENABLE(&hrtc);
+ __enable_irq();
+ last_steps = steps;
+ last_calr = now; // note: BKP31R deliberately NOT updated
+ }
+ }
+}
+
+// "tc_dump = on" over serial: emit the learned model as ready-to-paste config.txt lines plus
+// two checksummed $PMTXTC sentences (H and L — split so each fits NMEA_BUF_SIZE). One line per
+// main-loop pass; each line is FORMATTED ONCE and only the CDC submit is retried on BUSY (float
+// snprintf must not re-run thousands of times against the ISR's own float sprintf — newlib-nano
+// shares one _reent). A stuck host aborts the dump after a bounded number of BUSY passes.
+// HSE coefficients are printed in ppm/°C (per-degree slope b and curvature c, converted from
+// the tick-domain model); the HSE 'a' term has an arbitrary instrument origin and is neither
+// printed nor needed — steering uses temperature DIFFERENCES only (see tc_hse_delta).
+static void tc_dump_step(void){
+ static uint8_t idx = 0;
+ static int dn = -1; // formatted length; -1 = line not built yet
+ static uint16_t busy_ct = 0;
+ static char dline[NMEA_BUF_SIZE];
+ if (!tc_dump_pending) return;
+ if (hUsbDeviceFS.dev_state != USBD_STATE_CONFIGURED) { tc_dump_pending = 0; idx = 0; dn = -1; return; }
+
+ if (dn < 0) { // build the current line exactly once
+ float tpp = (float)tc_tpp();
+ int n = 0;
+ switch (idx) {
+ case 0:
+ n = snprintf(dline, sizeof dline, "# tempcomp: hse n=%lu lse n=%lu, die %d..%d C, state %c\r\n",
+ (unsigned long)tc_n_hse, (unsigned long)tc_n_lse,
+ (int)tc_hse_tmin, (int)tc_hse_tmax, tc_disp_state);
+ break;
+ case 1: n = snprintf(dline, sizeof dline, "tc_t0 = %d\r\n", (int)tc_t0); break;
+ case 2: // HSE slope, ppm/degC (origin-free)
+ if (tc_hse_valid) n = snprintf(dline, sizeof dline, "tc_hse_b = %.5f\r\n", (double)(tc_hse_m[1] / tpp));
+ else n = snprintf(dline, sizeof dline, "# tc_hse_b = ----\r\n");
+ break;
+ case 3: // HSE curvature, ppm/degC^2
+ if (tc_hse_valid) n = snprintf(dline, sizeof dline, "tc_hse_c = %.6f\r\n", (double)(tc_hse_m[2] / tpp));
+ else n = snprintf(dline, sizeof dline, "# tc_hse_c = ----\r\n");
+ break;
+ case 4: case 5: case 6: { // LSE a/b/c, absolute ppm at tc_t0
+ static const char nm[3] = {'a','b','c'};
+ static const char *fm[3] = {"tc_lse_%c = %.4f\r\n", "tc_lse_%c = %.5f\r\n", "tc_lse_%c = %.6f\r\n"};
+ int k = idx - 4;
+ if (tc_lse_valid) n = snprintf(dline, sizeof dline, fm[k], nm[k], (double)tc_lse_m[k]);
+ else n = snprintf(dline, sizeof dline, "# tc_lse_%c = ----\r\n", nm[k]);
+ break;
+ }
+ case 7: case 8: { // machine-parsable pair for the web app
+ char body[72];
+ int nb;
+ if (idx == 7)
+ nb = snprintf(body, sizeof body, "PMTXTC,H,%lu,%d,%d,%.5f,%.6f,%c",
+ (unsigned long)tc_n_hse, (int)tc_hse_tmin, (int)tc_hse_tmax,
+ (double)(tc_hse_valid ? tc_hse_m[1] / tpp : 0),
+ (double)(tc_hse_valid ? tc_hse_m[2] / tpp : 0), tc_hse_valid ? 'V' : '-');
+ else
+ nb = snprintf(body, sizeof body, "PMTXTC,L,%lu,%.4f,%.5f,%.6f,%c",
+ (unsigned long)tc_n_lse,
+ (double)(tc_lse_valid ? tc_lse_m[0] : 0), (double)(tc_lse_valid ? tc_lse_m[1] : 0),
+ (double)(tc_lse_valid ? tc_lse_m[2] : 0), tc_lse_valid ? 'V' : '-');
+ if (nb < 0 || nb >= (int)sizeof body) { tc_dump_pending = 0; idx = 0; return; }
+ uint8_t cks = 0;
+ for (int i2 = 0; i2 < nb; i2++) cks ^= (uint8_t)body[i2];
+ n = snprintf(dline, sizeof dline, "$%s*%02X\r\n", body, (unsigned)cks);
+ break;
+ }
+ case 9: { // seed coverage — with "tc_seed = on" the paste warm-starts
+ int lo = tc_hse_valid ? tc_hse_tmin : tc_lse_tmin; // (and keeps evolving) instead of freezing
+ int hi = tc_hse_valid ? tc_hse_tmax : tc_lse_tmax;
+ if (tc_lse_valid) { if (tc_lse_tmin < lo) lo = tc_lse_tmin; if (tc_lse_tmax > hi) hi = tc_lse_tmax; }
+ n = snprintf(dline, sizeof dline, "tc_seed_lo = %d\r\ntc_seed_hi = %d\r\n", lo, hi);
+ break;
+ }
+ }
+ if (n <= 0 || n >= (int)sizeof dline) { tc_dump_pending = 0; idx = 0; dn = -1; return; }
+ dn = n;
+ busy_ct = 0;
+ }
+
+ __disable_irq(); // serialise against the ISR NMEA passthrough
+ uint8_t r = CDC_Copy_Transmit((uint8_t*)dline, (uint16_t)dn);
+ __enable_irq();
+ if (r == USBD_BUSY) { // retry the SUBMIT only; the line stays built
+ if (++busy_ct > 5000) { tc_dump_pending = 0; idx = 0; dn = -1; } // host stopped reading
+ return;
+ }
+ dn = -1;
+ if (++idx > 9) { idx = 0; tc_dump_pending = 0; }
+}
+
+// "adev_dump = on" over serial: emit the whole live Allan-deviation curve as ONE checksummed
+// sentence — $PMADEV,,,,..,*CC — with sigma_k = sigma_y(tau)
+// at tau = 2^k s (fractional frequency, %.2e). valid = contiguous phase samples behind the estimate
+// (confidence). Fresh cache computed here (thread context) so the dump works in any display mode.
+// Serviced from the main loop; retries only the CDC submit on USBD_BUSY, like tc_dump_step/emitPPS.
+static void adev_dump_step(void){
+ static int dn = -1; // formatted length; -1 = not built yet
+ static uint16_t busy_ct = 0;
+ static char dline[160]; // epoch+tau0 header + 11 octaves outgrow NMEA_BUF_SIZE
+ if (!adev_dump_pending) return;
+ if (hUsbDeviceFS.dev_state != USBD_STATE_CONFIGURED) { adev_dump_pending = 0; dn = -1; return; }
+
+ if (dn < 0) {
+ adev_reduce(); // fresh octave cache (never in an ISR)
+ uint8_t noct = adev_noct;
+ uint16_t valid = adev_valid;
+ char body[128];
+ // Self-describing for machine consumers: epoch (unix s) + tau0 (s) lead the sentence, so tau_k =
+ // tau0 * 2^k needs no out-of-band spec and stale sentences are detectable.
+ int nb = snprintf(body, sizeof body, "PMADEV,%lu,1,%u,%u",
+ (unsigned long)(uint32_t)currentTime, (unsigned)valid, (unsigned)noct);
+ if (nb < 0 || nb >= (int)sizeof body) { adev_dump_pending = 0; return; }
+ for (uint8_t k = 0; k < noct; k++) {
+ int t = snprintf(body + nb, sizeof body - nb, ",%.2e", (double)adev_sigma_cache[k]);
+ if (t < 0 || t >= (int)(sizeof body - nb)) { adev_dump_pending = 0; return; }
+ nb += t;
+ }
+ uint8_t cks = 0;
+ for (int i = 0; i < nb; i++) cks ^= (uint8_t)body[i];
+ int n = snprintf(dline, sizeof dline, "$%s*%02X\r\n", body, (unsigned)cks);
+ if (n <= 0 || n >= (int)sizeof dline) { adev_dump_pending = 0; return; }
+ dn = n; busy_ct = 0;
+ }
+
+ __disable_irq(); // serialise against the ISR NMEA passthrough
+ uint8_t r = CDC_Copy_Transmit((uint8_t*)dline, (uint16_t)dn);
+ __enable_irq();
+ if (r == USBD_BUSY) { // retry the SUBMIT only; the line stays built
+ if (++busy_ct > 5000) { adev_dump_pending = 0; dn = -1; }
+ return;
+ }
+ dn = -1; adev_dump_pending = 0;
+}
+
+// "hdev_dump = on": one $PMHDEV sentence — the Hadamard twin of $PMADEV (same shape: epoch, tau0,
+// valid, noct, sigmas), computed on demand from the shared phase ring. Same CDC/BUSY-retry contract.
+static void hdev_dump_step(void){
+ static int dn = -1;
+ static uint16_t busy_ct = 0;
+ static char dline[160];
+ if (!hdev_dump_pending) return;
+ if (hUsbDeviceFS.dev_state != USBD_STATE_CONFIGURED) { hdev_dump_pending = 0; dn = -1; return; }
+
+ if (dn < 0) {
+ uint8_t noct = hdev_noct();
+ uint16_t valid = adev_valid;
+ char body[128];
+ int nb = snprintf(body, sizeof body, "PMHDEV,%lu,1,%u,%u",
+ (unsigned long)(uint32_t)currentTime, (unsigned)valid, (unsigned)noct);
+ if (nb < 0 || nb >= (int)sizeof body) { hdev_dump_pending = 0; return; }
+ for (uint8_t k = 0; k < noct; k++) {
+ int t = snprintf(body + nb, sizeof body - nb, ",%.2e", (double)hdev_sigma_for_m(1u<= (int)(sizeof body - nb)) { hdev_dump_pending = 0; return; }
+ nb += t;
+ }
+ uint8_t cks = 0;
+ for (int i = 0; i < nb; i++) cks ^= (uint8_t)body[i];
+ int n = snprintf(dline, sizeof dline, "$%s*%02X\r\n", body, (unsigned)cks);
+ if (n <= 0 || n >= (int)sizeof dline) { hdev_dump_pending = 0; return; }
+ dn = n; busy_ct = 0;
+ }
+
+ __disable_irq();
+ uint8_t r = CDC_Copy_Transmit((uint8_t*)dline, (uint16_t)dn);
+ __enable_irq();
+ if (r == USBD_BUSY) {
+ if (++busy_ct > 5000) { hdev_dump_pending = 0; dn = -1; }
+ return;
+ }
+ dn = -1; hdev_dump_pending = 0;
+}
+
+// "star_dump = on" over serial: the current soonest-transit list as ONE checksummed sentence —
+// $PMSTAR,,,,,..*CC. Fresh list computed here so it works
+// in any display mode. Same CDC/BUSY-retry contract as adev_dump_step/emitPPSTimestamp.
+static void star_dump_step(void){
+ static int dn = -1;
+ static uint16_t busy_ct = 0;
+ static char dline[192]; // 8 entries x ",NAME,SSSSS,AA,D" + header outgrow NMEA_BUF_SIZE
+ if (!star_dump_pending) return;
+ if (hUsbDeviceFS.dev_state != USBD_STATE_CONFIGURED) { star_dump_pending = 0; dn = -1; return; }
+
+ if (dn < 0) {
+ star_update(); // fresh transit list (never in an ISR)
+ uint8_t n = star_ncache;
+ char body[176];
+ int nb = snprintf(body, sizeof body, "PMSTAR,%u", (unsigned)n);
+ if (nb < 0 || nb >= (int)sizeof body) { star_dump_pending = 0; return; }
+ for (uint8_t k = 0; k < n; k++) {
+ long rem = (long)star_cache[k].epoch - (long)currentTime; if (rem < 0) rem = 0;
+ int t = snprintf(body + nb, sizeof body - nb, ",%.4s,%ld,%d,%c",
+ star_cache[k].nm, rem, (int)star_cache[k].alt, star_cache[k].dir);
+ if (t < 0 || t >= (int)(sizeof body - nb)) { star_dump_pending = 0; return; }
+ nb += t;
+ }
+ uint8_t cks = 0;
+ for (int i = 0; i < nb; i++) cks ^= (uint8_t)body[i];
+ int nn = snprintf(dline, sizeof dline, "$%s*%02X\r\n", body, (unsigned)cks);
+ if (nn <= 0 || nn >= (int)sizeof dline) { star_dump_pending = 0; return; }
+ dn = nn; busy_ct = 0;
+ }
+
+ __disable_irq();
+ uint8_t r = CDC_Copy_Transmit((uint8_t*)dline, (uint16_t)dn);
+ __enable_irq();
+ if (r == USBD_BUSY) { if (++busy_ct > 5000) { star_dump_pending = 0; dn = -1; } return; }
+ dn = -1; star_dump_pending = 0;
+}
+
+// Main-loop entry point, called every pass. With every tc key at its default this reduces to
+// four flag checks — no measurable cost, no behaviour change.
+// Holdover fade: from the residual 1σ time uncertainty during GPS-loss holdover, set each trailing
+// sub-second digit's intensity by its remaining SIGNIFICANCE. U(τ) = k_σ·σ·τ (µs); σ = RSS of three
+// independent ppm terms — how well the last cal pinned frequency, the MEASURED temp-model residual,
+// and aging. A digit fades over its significance band and goes dark once U exceeds its place value.
+static void computeHoldoverFade(void){
+ uint32_t age = (uint32_t)currentTime - last_pps_time; // holdover seconds
+
+ // σ_cal — frequency knowledge from the last RTC calibration, decaying with its age.
+ float cal_age = (float)((uint32_t)currentTime - (uint32_t)rtc_last_calibration);
+ float sigma_cal;
+ if (cal_age <= (float)CAL_PERIOD && tc_lse_valid) {
+ float cal_ppm = (float)debug_rtc_val * (1e6f / (32768.0f * (float)CAL_PERIOD));
+ sigma_cal = fabsf(cal_ppm) + 0.05f * sqrtf(cal_age / (float)CAL_PERIOD);
+ } else {
+ sigma_cal = 0.02f * sqrtf(cal_age); // stale cal: random-walk bound
+ }
+
+ // σ_temp — the MEASURED tempco-model residual (ppm), plus a penalty beyond the learned range.
+ float sigma_temp;
+ if (tc_hse_valid) {
+ float tpp = (float)tc_tpp();
+ sigma_temp = (tpp > 0.0f) ? tc_hse_resid / tpp : 10.0f; // HSE residual (ticks/s) → ppm
+ int t = die_temp_c;
+ int over = t < tc_hse_tmin ? tc_hse_tmin - t : t > tc_hse_tmax ? t - tc_hse_tmax : 0;
+ if (over > 0) sigma_temp += 0.3f * (float)over; // unvalidated beyond coverage
+ } else {
+ sigma_temp = 10.0f; // no model yet — bare-crystal
+ }
+
+ // σ_age — long-term oscillator aging (negligible over minutes/hours, kept for completeness).
+ float sigma_age = 0.1f * (float)age / 86400.0f;
+
+ float sigma = sqrtf(sigma_cal*sigma_cal + sigma_temp*sigma_temp + sigma_age*sigma_age);
+ float U_us = 3.0f * sigma * (float)age; // k_σ = 3 ("certainly right")
+ holdover_u_us = U_us; // publish for read-back / display
+
+ // Half place values (µs): 0.1 s, 0.01 s, 0.001 s. Fade band β. b_k = (h−U)/(β·h), clamped 0..1.
+ static const float h_us[3] = { 50000.0f, 5000.0f, 500.0f };
+ const float beta = 0.4f;
+ for (int k = 0; k < 3; k++){
+ float b = (h_us[k] - U_us) / (beta * h_us[k]);
+ if (b < 0.0f) b = 0.0f; else if (b > 1.0f) b = 1.0f;
+ digit_bright[k] = (uint8_t)(b * (float)FADE_MAX + 0.5f);
+ }
+ digit_bright[3] = digit_bright[0]; // the decimal point dies with the 0.1 s digit
+}
+
+void tc_housekeeping(void){
+ if (!tc_nom_load) tc_nom_load = SysTick->LOAD; // capture the nominal period once
+
+ // Serial warm-start, deferred out of the USB ISR: "tc_seed = on" armed the flag; apply here,
+ // serialized with tc_fit/tc_governor (the learned state stays main-loop-only). With the seed
+ // already applied, the same call is the freeze guard — it just re-NANs any tc_hse_*/tc_lse_*
+ // coefficients a later serial line reparsed, so the frozen path can't reactivate over the
+ // evolving model.
+ if (tc_seed_pending) { tc_seed_pending = 0; tc_seed_apply(); }
+ else if (tc_seed_done) tc_seed_apply();
+
+ if (tc_reset_pending) {
+ memset(tc_bins, 0, sizeof tc_bins);
+ tc_hse_valid = tc_lse_valid = 0;
+ tc_hse_prior = tc_lse_prior = 0; // drop any held warm-start prior: reset is a cold restart
+ tc_n_hse = tc_n_lse = 0;
+ tc_e0_set = 0; tc_ema = 0; // new origin rebase with the next sample
+ tc_reset_pending = 0;
+ }
+
+ if (tc_learn) {
+ tc_hse_learn();
+ tc_lse_learn();
+ static uint32_t last_fit = 0;
+ uint32_t now = (uint32_t)currentTime;
+ if (now - last_fit >= 300) { last_fit = now; tc_fit(); } // refit at most every 5 min
+ }
+
+ // tc_steer_on in the gate: the governor owns DISENGAGE, so it must stay reachable even if
+ // the user turns every tc key off while steering is engaged mid-holdover — otherwise the
+ // tick ISR would keep applying a stale frozen correction forever.
+ if (tc_learn || tc_apply || tc_rtc || tc_steer_on || displayMode == MODE_TEMPCOMP) tc_governor();
+
+ if (significance_fade) { // recompute the per-digit fade once per second while enabled
+ static uint32_t last_fade = 0;
+ uint32_t now = (uint32_t)currentTime;
+ if (now != last_fade) { last_fade = now; computeHoldoverFade(); }
+ }
+
+ tc_dump_step();
+}
+
+// tc_steer(): holdover rate steering (see tc_governor). Sets the length of the NEXT 1 ms
+// period: LOAD writes take effect at the following reload, so distributing tc_rem longer
+// periods per 1000 gives an average of base + rem/1000 extra ticks per ms — fractional-ppm
+// rate control with three int32 ops. tc_steer_on is 0 unless tc_apply engaged in holdover,
+// so the stock cost is one predicted-untaken branch per ms.
+#define tc_steer() \
+ if (tc_steer_on) { \
+ tc_acc += tc_rem; \
+ if (tc_acc >= 1000) { tc_acc -= 1000; SysTick->LOAD = (uint32_t)(tc_load_base + 1); } \
+ else { SysTick->LOAD = (uint32_t)tc_load_base; } \
+ }
+
+#define timetick() \
+ tc_steer(); \
+ millisec++; \
+ if (millisec>=10) { \
+ millisec=0; \
+ 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];
+
+ segbal_isr_refresh(); // mirrors track the masters at ISR freshness (seg_balance)
+
+ 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];
+
+ segbal_isr_refresh();
+
+ 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];
+
+ segbal_isr_refresh();
+
+ HAL_IncTick();
+
+ if (decisec==9 && centisec==0 && millisec==0){
+ currentTime++;
+ setNextTimestamp( currentTime );
+ sendDate(0);
+ }
+}
+
+void SysTick_CountUp_P0(void) {
+
+ timetick()
+
+ segbal_isr_refresh();
+
+ HAL_IncTick();
+
+ if (decisec==9 && centisec==0 && millisec==0){
+ currentTime++;
+ setNextTimestamp( currentTime );
+ sendDate(0);
+ }
+}
+
+void SysTick_CountUp_NoUpdate(void) {
+ tc_steer(); // this handler inlines its own cascade: hook it too
+ 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();
+ }
+ }
+ }
+
+ segbal_isr_refresh(); // masters are main-loop-drawn here (TEXT etc.) — keep mirrors no staler
+
+ 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];
+
+ segbal_isr_refresh();
+
+ 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];
+
+ segbal_isr_refresh();
+
+ 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];
+
+ segbal_isr_refresh();
+
+ 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];
+
+ segbal_isr_refresh();
+
+ HAL_IncTick();
+
+ if (decisec==9 && centisec==0 && millisec==0){
+ currentTime++;
+ setNextCountdown( currentTime );
+ sendDate(0);
+ }
+}
+
+// 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];
+
+ segbal_isr_refresh();
+
+ 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];
+
+ segbal_isr_refresh();
+
+ HAL_IncTick();
+
+ if (decisec==9 && centisec==0 && millisec==0){
+ alt_prep_next();
+ }
+}
+
+void SysTick_Alt_P1(void) {
+ timetick()
+
+ buffer_c[1].low=cLut[decisec];
+
+ segbal_isr_refresh();
+
+ HAL_IncTick();
+
+ if (decisec==9 && centisec==0 && millisec==0){
+ alt_prep_next();
+ }
+}
+
+void SysTick_Alt_P0(void) {
+ timetick()
+
+ segbal_isr_refresh();
+
+ HAL_IncTick();
+
+ if (decisec==9 && centisec==0 && millisec==0){
+ alt_prep_next();
+ }
+}
+
+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 sizeof rules[0] || numEntries > MAX_RULES) {
+ f_close(&file);
+ return RULES_HEADER_ERR;
+ }
+
+ int i;
+ for (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)
+ || 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);
+ }
+
+ 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 (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) {
+ countMode = COUNT_NORMAL;
+ setPrecision();
+ SetPPS( &PPS );
+ TIM2->CCR1 = 0;
+ TIM2->CCR2 = 0;
+ latchSegments();
+ }
+ }
+ applyColonForMode(); // idempotent: alt colon on entry, civil colon on exit
+ 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;
+ adev_reset(); // clear the (un-zeroed) RAM2 Allan-deviation ring before the first PPS sample
+
+ 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();
+ loadStars(); // scan /STARS.BIN into the transit catalogue (star_max_mag is known now); falls back to the baked bright set
+
+ 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)
+ {
+ LP_MARK(1); // $PMLOOP section marks — see the pmloop block above segbal_isr_refresh
+ if (loop_diag && (uint32_t)(uwTick - pmloop_win) >= 1000u) {
+ pmloop_win = uwTick;
+ char pl[40];
+ int pn = sprintf(pl, "$PMLOOP,%lu,%u\r\n", (unsigned long)pmloop_max, (unsigned)pmloop_maxtag);
+ __disable_irq(); CDC_Copy_Transmit((uint8_t*)pl, (uint16_t)pn); __enable_irq(); // lossy: diag
+ pmloop_max = 0;
+ }
+ LP_MARK(2);
+ segbal_poll(); // per-segment brightness balance (seg_balance) — refills the mirror slots, ≤1 kHz
+ colon_balance_poll(); // dim the colons with the rail (colon_balance) — reloads the anim buffer on change
+
+ LP_MARK(3);
+ // 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
+ && (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) {
+#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
+ fatfs_busy=0;
+ }
+
+ LP_MARK(4);
+ if (delayedCheckOnEject) firmwareCheckOnEject();
+
+ if (delayedPostConfigCleanup) {
+ delayedPostConfigCleanup=0;
+ postConfigCleanup();
+ }
+
+ fatfs_busy=1; // FATFS_remount + readConfigFile + checkDelayedLoadRules touch FATFS
+ if (delayedReadConfigFile) {
+ FATFS_remount();
+ readConfigFile();
+ delayedReadConfigFile=0;
+ }
+
+ checkDelayedLoadRules();
+ fatfs_busy=0;
+
+ if (delayedDisplayFreq) setDisplayFreq(delayedDisplayFreq);
+
+ LP_MARK(5);
+ monitor_vbus();
+
+ // significance_fade is a die-temp consumer too: computeHoldoverFade charges an out-of-coverage
+ // penalty from die_temp_c, which would otherwise stay at its init 0 with every other flag off.
+ if (pps_ts_enabled || tc_learn || tc_apply || tc_rtc || significance_fade || displayMode == MODE_TEMPCOMP) {
+ static uint32_t last_temp_read = 0;
+ if ((uint32_t)currentTime - last_temp_read >= 4) { // refresh die temp every ~4 s
+ last_temp_read = (uint32_t)currentTime;
+ measure_temp();
+ }
+ }
+ LP_MARK(6);
+ if (pps_ts_enabled && pps_record_pending) emitPPSTimestamp(); // emit clears pending itself on success
+
+ tc_housekeeping(); // temp-comp learn/steer/dump; four flag checks when everything is off
+ adev_dump_step(); // one-shot $PMADEV emit when adev_dump was set over serial (else 1 flag check)
+ hdev_dump_step(); // one-shot $PMHDEV (Hadamard) twin
+ star_dump_step(); // one-shot $PMSTAR emit when star_dump was set over serial (else 1 flag check)
+
+ LP_MARK(7);
+ 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();
+ // 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.
+ LP_MARK(8);
+ 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); }
+ }
+ }
+
+ // MODE_TEMPCOMP pages on the same dwell: repaint on the page flip (same guard as above)
+ if (displayMode == MODE_TEMPCOMP) {
+ static uint32_t tc_last_pg = 0;
+ uint32_t pg = uwTick / page_ms();
+ if (pg != tc_last_pg && decisec != 9) { tc_last_pg = pg; sendDate(1); }
+ }
+
+ // MODE_ADEV pages the octave taus on the same dwell. Recompute the octave cache on each flip
+ // (thread context, ~sub-ms) so the shown sigma tracks the growing series, then repaint. The
+ // 0xFFFFFFFF sentinel forces a reduce+paint on first entry rather than waiting a full dwell.
+ if (displayMode == MODE_ADEV) {
+ static uint32_t adev_last_pg = 0xFFFFFFFFu;
+ uint32_t pg = uwTick / page_ms();
+ if (pg != adev_last_pg && decisec != 9) { adev_last_pg = pg; adev_display_update(); sendDate(1); }
+ }
+
+ // MODE_STAR: recompute the soonest-transit list once a second (the countdown ticks off the cached
+ // epoch every 1 Hz sendDate; re-sorting keeps the order fresh as stars culminate), repaint on flip.
+ if (displayMode == MODE_STAR) {
+ static uint32_t star_last_sec = 0xFFFFFFFFu, star_last_pg = 0xFFFFFFFFu;
+ if ((uint32_t)currentTime != star_last_sec) { star_last_sec = (uint32_t)currentTime; star_update(); }
+ uint32_t pg = uwTick / page_ms();
+ if (pg != star_last_pg && decisec != 9) { star_last_pg = pg; sendDate(1); }
+ }
+
+ // MODE_LST / MODE_SOLAR: stage the next civil boundary's alternate reading
+ // (thread-context doubles; no-op in every other mode)
+ LP_MARK(9);
+ alt_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****/
diff --git a/mk4-time/Core/Src/stm32l4xx_it.c b/mk4-time/Core/Src/stm32l4xx_it.c
index d836d32..879d4c5 100644
--- a/mk4-time/Core/Src/stm32l4xx_it.c
+++ b/mk4-time/Core/Src/stm32l4xx_it.c
@@ -202,6 +202,8 @@ void DebugMon_Handler(void)
void PendSV_Handler(void)
{
/* USER CODE BEGIN PendSV_IRQn 0 */
+ extern volatile uint8_t pmloop_lasttag;
+ pmloop_lasttag = 15; // $PMLOOP attribution: the .900 display prep preempted the main loop
// Writing to the RTC is normally very fast, but if something goes wrong
// the HAL functions will fail to time out if it's running with the same
diff --git a/mk4-time/STM32L476RGTX_FLASH.ld b/mk4-time/STM32L476RGTX_FLASH.ld
index 2ec19be..ef1f728 100644
--- a/mk4-time/STM32L476RGTX_FLASH.ld
+++ b/mk4-time/STM32L476RGTX_FLASH.ld
@@ -172,6 +172,17 @@ SECTIONS
__bss_end__ = _ebss;
} >RAM
+ /* RAM2 (0x10000000, 32 KB) — NOLOAD: startup neither copies nor zeroes it, so RAM2 users clear
+ themselves. Outside the app-CRC/reflash region. Home of the free-running Allan-deviation ring. */
+ .ram2 (NOLOAD) :
+ {
+ . = ALIGN(8);
+ _sram2 = .;
+ *(.ram2) *(.ram2*)
+ . = ALIGN(8);
+ _eram2 = .;
+ } >RAM2
+
/* User_heap_stack section, used to check that there is enough "RAM" Ram type memory left */
._user_heap_stack :
{
diff --git a/mk4-time/STM32L476RGTX_NoBootloader.ld b/mk4-time/STM32L476RGTX_NoBootloader.ld
index 66b7c3c..962678e 100644
--- a/mk4-time/STM32L476RGTX_NoBootloader.ld
+++ b/mk4-time/STM32L476RGTX_NoBootloader.ld
@@ -172,6 +172,17 @@ SECTIONS
__bss_end__ = _ebss;
} >RAM
+ /* RAM2 (0x10000000, 32 KB) — NOLOAD: startup neither copies nor zeroes it, so RAM2 users clear
+ themselves. Outside the app-CRC/reflash region. Home of the free-running Allan-deviation ring. */
+ .ram2 (NOLOAD) :
+ {
+ . = ALIGN(8);
+ _sram2 = .;
+ *(.ram2) *(.ram2*)
+ . = ALIGN(8);
+ _eram2 = .;
+ } >RAM2
+
/* User_heap_stack section, used to check that there is enough "RAM" Ram type memory left */
._user_heap_stack :
{
diff --git a/mk4-time/USB_DEVICE/App/usbd_cdc_if.c b/mk4-time/USB_DEVICE/App/usbd_cdc_if.c
index 2fbbe07..4a11a2b 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;
}
@@ -304,12 +307,26 @@ uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len)
uint8_t CDC_Copy_Transmit(uint8_t* nmea, uint16_t Len)
{
- static uint8_t txbuf[NMEA_BUF_SIZE];
+ /* Sized for the LONGEST sentence we emit, not NMEA_BUF_SIZE: a mature $PMADEV (epoch + tau0 +
+ valid + 11 octaves) is ~133 bytes. At NMEA_BUF_SIZE (128) the Len guard below returned
+ USBD_FAIL, which adev_dump_step treats as done — so once the Allan record grew past 10
+ octaves the dump was SILENTLY dropped on hardware (the emulator bypasses CDC and never saw
+ it). Keep this >= the largest snprintf buffer any *_dump_step builds into. */
+ static uint8_t txbuf[192];
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);
diff --git a/mk4-time/USB_DEVICE/Target/usbd_conf.c b/mk4-time/USB_DEVICE/Target/usbd_conf.c
index ab89fbb..bc8ce5a 100644
--- a/mk4-time/USB_DEVICE/Target/usbd_conf.c
+++ b/mk4-time/USB_DEVICE/Target/usbd_conf.c
@@ -204,6 +204,27 @@ 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. 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;
+ 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);
}
@@ -361,7 +382,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;
diff --git a/mk4-time/test/test_astro.c b/mk4-time/test/test_astro.c
new file mode 100644
index 0000000..3c85600
--- /dev/null
+++ b/mk4-time/test/test_astro.c
@@ -0,0 +1,140 @@
+/* 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("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. */
+ 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/.gitignore b/qspi/.gitignore
new file mode 100644
index 0000000..7782d3b
--- /dev/null
+++ b/qspi/.gitignore
@@ -0,0 +1 @@
+hyg_v41.csv
diff --git a/qspi/config.txt b/qspi/config.txt
index 3606e81..e5b84fe 100644
--- a/qspi/config.txt
+++ b/qspi/config.txt
@@ -1,102 +1,223 @@
-
-# Matrix refresh rate, in Hz. Min 1000, max 100000
-# This number is only a target, exact frequency will be a division of processor clock speed.
-# Note the display is split into four matrices of 5 digits each. The number given here is the
-# refresh rate of the display - the clock frequency of the matrix will be five times this number.
-MATRIX_FREQUENCY = 20000
-
-
-# Use this timezone, leave blank to calculate automatically
-# Must be an IANA timezone string, use e.g. Etc/GMT+5 for fixed offsets
-# For UTC use Etc/UTC
-#ZONE_OVERRIDE = America/New_York
-
-
-
-
-## modes
-
-# ISO8601 standard, YYYY-MM-DD
-MODE_ISO8601_STD=Enabled
-
-# ISO8601 ordinal (day of year)
-MODE_ISO_Ordinal=disabled
-
-# ISO8601 week (year can be different around new year)
-MODE_ISO_WEEK = disabled
-
-# Unix timestamp mode. The unix timestamp is always UTC.
-MODE_UNIX = Disabled
-
-# Julian Date
-MODE_JULIAN_DATE = disabled
-
-# Modified Julian Date (JD − 2400000.5)
-# Note: the fractional part is approximate, and only updated once per second.
-MODE_MODIFIED_JD = disabled
-
-# Display the UTC offset of the current local time
-MODE_SHOW_OFFSET = enabled
-
-# Attempt to display tz name on the 7-segment display, this is usually fairly illegible
-MODE_SHOW_TZ_NAME = enabled
-
-MODE_WEEKDAY = disabled
-MODE_WEEKDA_DD = disabled
-MODE_WDY_MM_DD = disabled
-
-# Turn off all LEDs. This mode can be useful if you want to reduce power consumption
-# but keep the GPS module fully powered.
-MODE_STANDBY=disabled
-
-
-MODE_COUNTDOWN = off
-COUNTDOWN_TO = 2023-05-06T23:00:00Z
-
-
-# generic text display, can be used for testing
-MODE_TEXT=off
-TEXT=hello
-
-
-# One of: slowfade, heartbeat, sawtooth, alt_sawtooth, toggle, solid
-colon_mode=heartbeat
-
-
-# Tolerance times, in seconds.
-# When the clock loses its GPS fix, it progressively hides the last digits to represent the widening
-# accuracy tolerance. These settings represent when to hide the last digits.
-# If the TCXO is accurate to 1ppm, it would take 1000 seconds to drift by one millisecond.
-Tolerance_time_1ms = 1000
-Tolerance_time_10ms = 10000
-
-# The deciseconds digit is only disabled if the clock has been powered off, and the last RTC calibration
-# was more than this many seconds ago
-Tolerance_time_100ms = 100000
-
-# For paranoia, set the 1ms tolerance to 1, which will disable the last digit as soon as GPS fix is lost.
-# To turn off the tolerance feature, set all tolerances to 0.
-
-
-# nonlinear brightness curve, five stops of input->output, use brightness-curve.htm for a GUI
-# VTT9812FH with R11 = 470K
-BS1 = 0,0
-BS2 = 131,365
-BS3 = 1076,1422
-BS4 = 2774,2665
-BS5 = 3849,4095
-
-# shows input and output values of DAC/ADC
-MODE_DEBUG_BRIGHTNESS = 0
-
-# override auto brightness
-#brightness=0.5
-
-# show remainder of RTC calibration after 63 second period
-MODE_DEBUG_RTC = 0
-
-# list number of satellites in view (number, not signal strength)
-mode_satview = 0
-
-# show coin cell voltage
-mode_vbat = 0
+
+# Matrix refresh rate, in Hz. Min 1000, max 100000
+# This number is only a target, exact frequency will be a division of processor clock speed.
+# Note the display is split into four matrices of 5 digits each. The number given here is the
+# refresh rate of the display - the clock frequency of the matrix will be five times this number.
+MATRIX_FREQUENCY = 20000
+
+
+# Use this timezone, leave blank to calculate automatically
+# Must be an IANA timezone string, use e.g. Etc/GMT+5 for fixed offsets
+# For UTC use Etc/UTC
+#ZONE_OVERRIDE = America/New_York
+
+
+
+
+## modes
+
+# ISO8601 standard, YYYY-MM-DD
+MODE_ISO8601_STD=Enabled
+
+# ISO8601 ordinal (day of year)
+MODE_ISO_Ordinal=disabled
+
+# ISO8601 week (year can be different around new year)
+MODE_ISO_WEEK = disabled
+
+# Unix timestamp mode. The unix timestamp is always UTC.
+MODE_UNIX = Disabled
+
+# Julian Date
+MODE_JULIAN_DATE = disabled
+
+# Modified Julian Date (JD − 2400000.5)
+# Note: the fractional part is approximate, and only updated once per second.
+MODE_MODIFIED_JD = disabled
+
+# Display the UTC offset of the current local time
+MODE_SHOW_OFFSET = enabled
+
+# Attempt to display tz name on the 7-segment display, this is usually fairly illegible
+MODE_SHOW_TZ_NAME = enabled
+
+MODE_WEEKDAY = disabled
+MODE_WEEKDA_DD = disabled
+MODE_WDY_MM_DD = disabled
+
+# Turn off all LEDs. This mode can be useful if you want to reduce power consumption
+# but keep the GPS module fully powered.
+MODE_STANDBY=disabled
+
+
+MODE_COUNTDOWN = off
+COUNTDOWN_TO = 2023-05-06T23:00:00Z
+
+
+# generic text display, can be used for testing
+MODE_TEXT=off
+TEXT=hello
+
+
+# One of: slowfade, heartbeat, sawtooth, alt_sawtooth, toggle, solid
+colon_mode=heartbeat
+
+
+# Tolerance times, in seconds.
+# When the clock loses its GPS fix, it progressively hides the last digits to represent the widening
+# accuracy tolerance. These settings represent when to hide the last digits.
+# If the TCXO is accurate to 1ppm, it would take 1000 seconds to drift by one millisecond.
+Tolerance_time_1ms = 1000
+Tolerance_time_10ms = 10000
+
+# The deciseconds digit is only disabled if the clock has been powered off, and the last RTC calibration
+# was more than this many seconds ago
+Tolerance_time_100ms = 100000
+
+# For paranoia, set the 1ms tolerance to 1, which will disable the last digit as soon as GPS fix is lost.
+# To turn off the tolerance feature, set all tolerances to 0.
+
+
+# nonlinear brightness curve, five stops of input->output, use brightness-curve.htm for a GUI.
+# The measured VTT9812FH (R11 = 470K) curve below is now the compiled-in firmware default, so these
+# lines are only needed to OVERRIDE it (e.g. a different photodiode). Uncomment and edit to change a stop.
+# VTT9812FH with R11 = 470K
+#BS1 = 0,0
+#BS2 = 131,365
+#BS3 = 1076,1422
+#BS4 = 2774,2665
+#BS5 = 3849,4095
+
+# shows input and output values of DAC/ADC
+MODE_DEBUG_BRIGHTNESS = 0
+
+# override auto brightness
+#brightness=0.5
+
+# show remainder of RTC calibration after 63 second period
+MODE_DEBUG_RTC = 0
+
+# list number of satellites in view (number, not signal strength)
+mode_satview = 0
+
+# show coin cell voltage
+mode_vbat = 0
+
+# output a $PMTXTS timing sentence over USB serial on each GPS PPS pulse, carrying the
+# sub-millisecond phase measurement made at the edge, for host-side jitter/drift analysis.
+# When on, the sentence also carries a USB SOF-correlation tail (dwt_pps, sof_frame, dwt_sof)
+# that lets a host place the edge on its clock to ~microseconds despite USB delivery jitter;
+# a host that only needs the phase can ignore the extra fields. Enabling this turns on the USB
+# Start-Of-Frame interrupt (a cheap 1 kHz latch, below the display in priority).
+pps = off
+
+
+## temperature compensation (opt-in; all off = stock behaviour)
+# While GPS-locked the clock can LEARN how each oscillator drifts with die temperature
+# (tc_learn), then during a GPS outage steer the display timebase from that model
+# (tc_apply) and/or keep the battery RTC trimmed for the next power-up (tc_rtc).
+# Send "tc_dump = on" over USB serial to print the learned coefficients as lines you can
+# paste below: pasted tc_hse_*/tc_lse_* values freeze the model (they override learning).
+# "tc_reset = on" over serial clears the learned data. Both are ignored inside this file.
+tc_learn = off
+tc_apply = off
+tc_rtc = off
+
+# show die temp / model offsets / sample count on the date row (four pages, 5.5 s each)
+mode_tempcomp = 0
+
+# model centre (deg C) and safety limits; paste frozen coefficients from tc_dump below.
+# HSE uses temperature DIFFERENCES only, so it has no 'a' term — just slope + curvature.
+# Setting a coefficient to "nan" unfreezes it (learning takes over again).
+# tc_t0 = 40
+# tc_hse_b =
+# tc_hse_c =
+# tc_lse_a =
+# tc_lse_b =
+# tc_lse_c =
+# tc_engage_s = 2 (minimum 2)
+# tc_max_ppm = 100
+
+# Warm start (seed-and-evolve): with tc_seed on, the pasted coefficients above load as an
+# EVOLVING starting point instead of a freeze — compensated from the first second, refined as
+# the clock learns, handed over once its own data is at least as rich. tc_dump also prints
+# tc_seed_lo/hi (the model's temperature coverage) so a paste round-trips. Over serial, send
+# the coefficients first and "tc_seed = on" last (the order tc_dump prints).
+# tc_seed = on
+# tc_seed_lo =
+# tc_seed_hi =
+
+
+
+## astro modes (GPS-derived)
+# These show on the date row while the live clock keeps running on the time row.
+# They use the current position; with no GPS fix they fall back to fake_latitude/
+# fake_longitude below (handy indoors), or show "----" if neither is set.
+
+# Sunrise / sunset / solar noon (local time). Pages RISE -> SET -> SOL (see page_ms).
+MODE_SUN = disabled
+
+# Sun azimuth & elevation right now, e.g. "AZ142EL38" (degrees; elevation may be negative).
+MODE_SUN_AZEL = disabled
+
+# Moon: phase index 0-7 then illuminated %, e.g. "MOON 4 99".
+# Index: 0 new, 1 waxing crescent, 2 first quarter, 3 waxing gibbous,
+# 4 full, 5 waning gibbous, 6 last quarter, 7 waning crescent.
+MODE_MOON = disabled
+
+# Maidenhead grid locator, e.g. "IO91xl".
+MODE_GRID = disabled
+
+# Latitude / longitude in decimal degrees. Pages LAT -> LON (see page_ms).
+MODE_LATLON = disabled
+
+# Dwell per sub-screen for the paged modes (MODE_SUN, MODE_LATLON), in milliseconds.
+# Default 5500 (a subjectively-tuned cadence, found by feel); floored at 250.
+page_ms = 5500
+
+# Bright-star meridian-transit predictor. Paged countdowns to the soonest bright stars crossing
+# your local meridian (culminating), e.g. "SIR 0:45" = Sirius transits in 45 min. Needs a position
+# (GPS fix or fake_longitude); shows "STAr ----" without one. J2000 catalogue precessed to date.
+# (Companion serial command: "star_dump = on" prints the whole list as $PMSTAR.)
+MODE_STAR = disabled
+
+# 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
+
+# Live Allan deviation of the FREE-RUNNING crystal (its true stability, undisciplined). Pages
+# sigma_y(tau) across octave averaging times on the date row: "1s 3.2e-11", "64s 3e-11", ... up to
+# tau=1024 s, while the time row keeps live GPS time. Shows "Adev ----" until it has a few contiguous
+# locked seconds. The honest crystal signal (DWT phase, not the disciplined output).
+# (Companion serial command: "adev_dump = on" prints the whole curve as $PMADEV.)
+MODE_ADEV = 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).
+#fake_latitude = 51.48
+#fake_longitude = -0.01
+
+## display balance
+
+# Equalise per-segment brightness by duty, so a "1" isn't brighter than an "8" (current sharing).
+# "on"/1 = the calibrated auto curve (recommended). A number 2..300 pins a fixed manual strength.
+seg_balance = on
+
+# Dim the colons in step with the main display brightness (they are on their own PWM rail and
+# otherwise stay bright as the digits dim down). "on"/1 = the auto curve; a number 2..256 pins a
+# fixed scale of 256. Leave off until the digit brightness levels are dialled in, then switch on.
+#colon_balance = on
diff --git a/qspi/generate-stars.py b/qspi/generate-stars.py
new file mode 100644
index 0000000..26a2a5d
--- /dev/null
+++ b/qspi/generate-stars.py
@@ -0,0 +1,136 @@
+#!/usr/bin/env python3
+"""
+generate-stars.py -> output/stars.bin
+
+Build the bright-star transit catalogue the clock reads from the QSPI/SD card, mirroring
+generate-tzrules.py. Source is the HYG database v4 (CC0), decimal J2000 RA (hours) / Dec (degrees) —
+the same units the firmware wants. We keep the naked-eye "stars people actually recognise" (default
+mag <= 2.5, ~90 stars), magnitude-sorted so the firmware can early-stop at a `star_max_mag` config
+knob. Names are 4-char, uppercase (the mk4-date 7-seg font is uppercase-only and has a glyph for every
+letter; I/O/S/Z read as 1/0/5/2 and K/M/Q/V/W/X are rough approximations — flagged, not excluded).
+
+FILE FORMAT (little-endian):
+ header 16 B: magic "MST1" (4) | count u16 | recordLength u16 (=14) | mag_scale u16 (=100) | 6 B zero
+ record 14 B: ra u16 (= round(ra_hours/24 * 65536)) -> firmware: ra_h = ra/65536*24
+ dec i16 (= round(dec_deg * 100)) -> firmware: dec = dec/100
+ mag i16 (= round(mag * 100), load-filter) -> dropped from RAM after the cut
+ nm char[4] (uppercase, space-padded, unique)
+ pmra i16 (mas/yr, mu_alpha* incl. cos-dec) -> proper motion (alpha Cen drifts ~14 s
+ pmdec i16 (mas/yr) of transit time by 2028 without it)
+ (The firmware also accepts legacy 10-byte records — proper motion treated as zero.)
+"""
+import csv, os, struct, sys, urllib.request
+
+HYG_URL = "https://raw.githubusercontent.com/astronexus/HYG-Database/main/hyg/CURRENT/hygdata_v41.csv"
+HERE = os.path.dirname(os.path.abspath(__file__))
+CACHE = os.path.join(HERE, "hyg_v41.csv") # gitignored build cache
+OUT = os.path.join(HERE, "output", "stars.bin")
+MAG_CUT = float(os.environ.get("STAR_MAG_CUT", "2.5"))
+MAGIC = b"MST1"
+REC_LEN = 14
+MAG_SCALE = 100
+
+# --- 7-seg legibility (mk4-date lut_7seg is uppercase-only; every letter renders) ---
+AMBIG = set("IOSZ") # render but look like 1 0 5 2
+ROUGH = set("KMQVWX") # present but rough 7-seg approximations
+
+def fetch_hyg():
+ if not os.path.exists(CACHE):
+ sys.stderr.write(f"downloading HYG v4.1 -> {CACHE} ...\n")
+ urllib.request.urlretrieve(HYG_URL, CACHE)
+ return CACHE
+
+def clean_name(s):
+ """Uppercase, keep A-Z0-9 only."""
+ return "".join(c for c in s.upper() if c.isalnum())
+
+# Curated overrides (review-driven, famous-stars scope):
+# - Mirach's natural stem "MIRA" IMPERSONATES Mira (omicron Ceti, itself famous and not on the card).
+# - Markab (alpha Peg, Great Square corner) deserves "MARK"; obscure Markeb (kappa Vel) sorted one
+# place earlier by magnitude and used to steal it, leaving Markab an unrecognizable "MARB".
+NAME_OVERRIDE = { 'Mirach': 'MRCH', 'Markab': 'MARK', 'Markeb': 'MARB' }
+# Asterism-completing showpieces admitted ABOVE the magnitude cut (famous-only scope intact):
+# Megrez (mag 3.3) is the 7th star of the Big Dipper — without it the most-pointed-at northern
+# asterism is forever one star short.
+ALWAYS_INCLUDE = { 'Megrez' }
+
+def abbrev(proper, bayer, con, used):
+ """A unique 4-char uppercase name. Curated override first; else prefer the proper name; fall back to Bayer(greek)+con."""
+ if proper in NAME_OVERRIDE:
+ nm = NAME_OVERRIDE[proper]
+ if nm in used:
+ raise RuntimeError(f"override collision: {proper} -> {nm}")
+ return nm
+ GREEK = { # HYG 3-letter Bayer prefix -> single display letter
+ 'Alp':'A','Bet':'B','Gam':'G','Del':'D','Eps':'E','Zet':'Z','Eta':'H','The':'T','Iot':'I',
+ 'Kap':'K','Lam':'L','Mu':'M','Nu':'N','Xi':'X','Omi':'O','Pi':'P','Rho':'R','Sig':'S',
+ 'Tau':'U','Ups':'U','Phi':'F','Chi':'C','Psi':'Y','Ome':'O'}
+ cands = []
+ if proper:
+ base = clean_name(proper.split()[0]) # first word, e.g. "Rigil Kentaurus" -> RIGIL
+ if len(base) >= 2:
+ cands.append(base[:4])
+ cands.append((base[:3] + base[-1]) if len(base) > 4 else base[:4])
+ cands.append(base[:2] + base[-2:])
+ if bayer and con:
+ g = GREEK.get(bayer.split('-')[0])
+ if g:
+ cands.append((g + clean_name(con))[:4])
+ for c in cands:
+ c = (c + " ")[:4].strip()
+ if c and c not in used:
+ return c
+ # last resort: proper/bayer stem + a disambiguating digit
+ stem = (cands[0] if cands else "STR")[:3]
+ for d in "23456789":
+ c = (stem + d)[:4]
+ if c not in used:
+ return c
+ raise RuntimeError(f"cannot uniquely name {proper or bayer!r}")
+
+def build():
+ rows = [r for r in csv.DictReader(open(fetch_hyg())) if r['id'] != '0' and r['mag']]
+ # drop secondary components of multiple-star systems (comp != 1): they duplicate the primary's
+ # position, so they'd transit at the same instant (e.g. Capella's mag-0.96 component, or Toliman
+ # = alpha Cen B sitting on Rigil Kentaurus). Keep only the primary / single stars.
+ rows = [r for r in rows if (not r['comp']) or r['comp'] == '1']
+ stars = [r for r in rows if float(r['mag']) <= MAG_CUT or r['proper'] in ALWAYS_INCLUDE]
+ stars.sort(key=lambda r: float(r['mag'])) # brightest first -> firmware early-stop by mag
+
+ used, out, report = set(), [], []
+ for r in stars:
+ nm = abbrev(r['proper'], r['bayer'], r['con'], used)
+ used.add(nm)
+ ra_h = float(r['ra']) % 24.0
+ ra_u = round(ra_h / 24.0 * 65536.0) & 0xFFFF
+ dec_i = max(-9000, min(9000, round(float(r['dec']) * 100)))
+ mag_i = round(float(r['mag']) * MAG_SCALE)
+ pmra = max(-32768, min(32767, round(float(r['pmra'] or 0)))) # mas/yr (HYG: mu_alpha*)
+ pmdec = max(-32768, min(32767, round(float(r['pmdec'] or 0))))
+ out.append((ra_u, dec_i, mag_i, nm, pmra, pmdec))
+ flags = ''.join(sorted(set(nm) & (AMBIG | ROUGH)))
+ report.append((r['proper'] or ('*' + (r['bayer'] or '')), nm, float(r['mag']), flags))
+
+ os.makedirs(os.path.dirname(OUT), exist_ok=True)
+ with open(OUT, "wb") as f:
+ f.write(MAGIC + struct.pack("') if fl else ''}")
+
+if __name__ == "__main__":
+ build()
diff --git a/qspi/output/fwd.bin b/qspi/output/fwd.bin
index 552e706..6cf98f0 100755
Binary files a/qspi/output/fwd.bin and b/qspi/output/fwd.bin differ
diff --git a/qspi/output/fwt.bin b/qspi/output/fwt.bin
index ddcf724..bd78778 100755
Binary files a/qspi/output/fwt.bin and b/qspi/output/fwt.bin differ
diff --git a/qspi/output/stars.bin b/qspi/output/stars.bin
new file mode 100644
index 0000000..febaa34
Binary files /dev/null and b/qspi/output/stars.bin differ