Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Inc/stm32f0xx_it.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ void PendSV_Handler(void);
void SysTick_Handler(void);
void DMA1_CH1_IRQHandler(void);
void I2C1_IRQHandler(void);
void TIM1_BRK_UP_TRG_COM_IRQHandler(void);
void EXTI0_1_IRQHandler(void);
/* USER CODE BEGIN EFP */

Expand Down
25 changes: 14 additions & 11 deletions Inc/ups_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* Core Principles:
* 1. Single Source of Truth: One authoritative state structure
* 2. Explicit State Machine: All behavior driven by documented state transitions
* 3. Canonical Scheduler: TIM1 10ms interrupt serves as single timebase
* 3. Canonical Scheduler: 10 ms tick (derived from SysTick) serves as single timebase
* 4. ISR Safety: No flash writes in ISRs; minimal work in ISRs
* 5. Data Coherence: Multi-byte registers read from single atomic snapshot
*
Expand Down Expand Up @@ -135,10 +135,10 @@ extern "C" {


/* Timing Constants (in 10ms ticks unless otherwise noted)
* Canonical scheduler: TIM1 tick period is fixed at 10ms (see Core Principles). Do not change
* TIM1 period without updating TICK_PERIOD_MS and all derived constants. All other "10ms tick"
* Canonical scheduler: 10 ms tick period is fixed (see Core Principles). Do not change
* the scheduler tick period without updating TICK_PERIOD_MS and all derived constants. All other "10ms tick"
* comments in this file refer to this definition. */
#define TICK_PERIOD_MS 10 /* TIM1 tick period in milliseconds */
#define TICK_PERIOD_MS 10 /* canonical 10 ms tick period in milliseconds */
/* Derived tick constants - self-consistent with TICK_PERIOD_MS */
#define TICKS_PER_100MS (100 / TICK_PERIOD_MS) /* Ticks per 100ms */
#define TICKS_PER_500MS (500 / TICK_PERIOD_MS) /* Ticks per 500ms (ADC trigger) */
Expand Down Expand Up @@ -342,7 +342,7 @@ typedef struct {
charger_state_t charger_state;
learning_mode_t learning_mode; /* Derived from charger_state; never set directly */

/* State timing (in TIM1 ticks). Invariant: set on every transition into that state (including restart start, charger PRESENT/ABSENT, power RPI_ON/RPI_OFF). */
/* State timing (in canonical 10 ms ticks). Invariant: set on every transition into that state (including restart start, charger PRESENT/ABSENT, power RPI_ON/RPI_OFF). */
uint32_t power_state_entry_ticks;
uint32_t charger_state_entry_ticks;

Expand Down Expand Up @@ -383,7 +383,7 @@ typedef struct {
*/
typedef struct {
button_state_t state;
uint32_t press_start_tick; /* TIM1 tick when press first detected */
uint32_t press_start_tick; /* canonical 10 ms tick when press first detected */
uint16_t hold_ticks; /* Duration held (increments while button pressed).
* Saturates at >BUTTON_LONG_PRESS_TICKS to avoid re-fire. */
uint8_t long_press_fired; /* Flag: 1 = long press action already triggered (one-shot guarantee) */
Expand Down Expand Up @@ -468,7 +468,7 @@ typedef struct {
uint16_t version; /* 0x28-0x29: Firmware version */

/* Snapshot timing */
uint32_t snapshot_tick; /* TIM1 tick when snapshot was taken */
uint32_t snapshot_tick; /* canonical 10 ms tick when snapshot was taken */
uint32_t last_true_vbat_sample_tick; /* Tick when last true-VBAT was captured.
* Must remain uint32_t; do not reintroduce 8-bit tick fields for staleness. */
uint16_t last_true_vbat_mv; /* Cached true-VBAT (charger not influencing) for percent calc. */
Expand Down Expand Up @@ -585,9 +585,9 @@ STATIC_ASSERT(sizeof(flash_persistent_data_t) <= FLASH_PAGE_SIZE,
/*===========================================================================*/

/**
* @brief Scheduler Flags - Set by TIM1 ISR, cleared by main loop (canonical scheduler)
* @brief Scheduler Flags - Set by canonical 10 ms tick (generated from SysTick), cleared by main loop (canonical scheduler)
*
* TIM1 ISR sets tick_10ms every 10ms, tick_100ms every 100ms, tick_500ms every 500ms.
* Canonical 10 ms tick sets tick_10ms every 10ms, tick_100ms every 100ms, tick_500ms every 500ms.
* Main loop derives tick_1s from tick_100ms (10 pulses = 1 second) and clears all flags after processing.
* tick_counter is monotonic (never cleared); use it for precise timing deltas when needed.
*/
Expand All @@ -599,6 +599,9 @@ typedef struct {
volatile uint32_t tick_counter; /* Free-running 10ms counter (monotonic, never cleared) */
} scheduler_flags_t;

/** Called from SysTick every 10 ms (flag-only; do not add heavy logic). Declared here for stm32f0xx_it.c. */
void Scheduler_ISR_Tick10ms(void);

/*===========================================================================*/
/* I2C PENDING WRITE STRUCTURE */
/*===========================================================================*/
Expand Down Expand Up @@ -863,9 +866,9 @@ void Charger_UpdatePhysicalPresence(const authoritative_state_t *state,
uint8_t Charger_IsInfluencingVBAT(const system_state_t *state);
/* Returns 1 if charger_state == CHARGER_STATE_PRESENT, 0 otherwise */

/* True-VBAT freshness check - uses TIM1 ticks (10ms resolution) */
/* True-VBAT freshness check - uses canonical 10 ms tick (10ms resolution) */
/* Contract: Compares now_ticks against last_true_vbat_sample_tick and TRUE_VBAT_MAX_AGE_TICKS.
* MUST NOT use SysTick milliseconds.
* MUST use canonical 10 ms tick for freshness; MUST NOT use SysTick milliseconds.
* Wraparound: (now_ticks - last_true_vbat_sample_tick) MUST be computed as unsigned (uint32_t)
* so that modulo-2^32 subtraction yields correct age when tick_counter has wrapped. */
uint8_t IsTrueVbatSampleFresh(uint32_t now_ticks, uint32_t last_true_vbat_sample_tick);
Expand Down
54 changes: 18 additions & 36 deletions Src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ void CheckPowerOnConditions(void);
/* Raw ADC buffer; DMA fills, main loop processes into state */
__IO uint16_t aADCxConvertedData[ADC_CONVERTED_DATA_BUFFER_SIZE];

/* Timing/button helpers (TIM1/EXTI). Authoritative state is state/sys_state only;
/* Timing/button helpers (canonical 10 ms tick/EXTI). Authoritative state is state/sys_state only;
* no shadow globals (uXXXVolt, counters, mode flags) remain as authoritative. */
__IO uint8_t sKeyFlag = 0; /* Button activity (EXTI edge) */
__IO uint16_t sIPIdleTicks = 0;
Expand Down Expand Up @@ -80,14 +80,14 @@ volatile uint8_t active_reg_image = 0;
volatile uint8_t adc_ready = 0;
volatile uint32_t adc_sample_seq = 0;

/* Canonical scheduler - TIM1 sets flags only; main loop runs all tasks.
/* Canonical scheduler - canonical 10 ms tick (generated from SysTick) sets flags only; main loop runs all tasks.
* Flag race: small chance of losing an event if ISR sets a flag between main's check and clear.
* For coarse 100ms/500ms tasks usually acceptable; for never-miss semantics consider
* counters (increment in ISR) or copy+clear in one short critical section in main. */
static scheduler_flags_t sched_flags;
/* Main loop derives tick_1s from tick_100ms (10 pulses = 1s) */
static uint8_t tick_100ms_count = 0;
/* Downcounters for 100ms/500ms flags (avoid modulo in ISR) */
/* Downcounters for 100ms/500ms flags (avoid modulo in ISR). ISR-only: do not modify from main. */
static uint8_t ticks_until_100ms = TICKS_PER_100MS;
static uint8_t ticks_until_500ms = TICKS_PER_500MS;

Expand Down Expand Up @@ -465,7 +465,7 @@ static void UpdateDerivedState(void)

void Snapshot_Update(void)
{
state.snapshot_tick = sched_flags.tick_counter; /* canonical TIM1 tick */
state.snapshot_tick = sched_flags.tick_counter; /* canonical 10 ms tick */
uint8_t inactive = (uint8_t)(1u - active_reg_image);
StateToRegisterBuffer(&state, &sys_state, reg_image[inactive]);
active_reg_image = inactive;
Expand Down Expand Up @@ -862,7 +862,7 @@ int main(void)

while (1)
{
/* Canonical scheduler - run tasks from TIM1 flags, then clear flags */
/* Canonical scheduler - run tasks from scheduler flags, then clear flags */
if (sched_flags.tick_10ms)
{
Scheduler_Tick10ms();
Expand Down Expand Up @@ -1844,24 +1844,21 @@ void DMA1_CH1_IRQHandler(void)
}
}

/* TIM1 ISR is flag-only. All timing logic runs in main loop. */
void TIM1_BRK_UP_TRG_COM_IRQHandler(void)
/* Scheduler 10 ms tick: flag-only work, called from SysTick every 10th tick.
* On Cortex-M0, aligned 32-bit write to tick_counter is atomic; main reads it for deltas. */
void Scheduler_ISR_Tick10ms(void)
{
if (LL_TIM_IsActiveFlag_UPDATE(TIM1) == 1)
sched_flags.tick_counter++;
sched_flags.tick_10ms = 1;
if (--ticks_until_100ms == 0u)
{
LL_TIM_ClearFlag_UPDATE(TIM1);
sched_flags.tick_counter++;
sched_flags.tick_10ms = 1;
if (--ticks_until_100ms == 0u)
{
ticks_until_100ms = TICKS_PER_100MS;
sched_flags.tick_100ms = 1;
}
if (--ticks_until_500ms == 0u)
{
ticks_until_500ms = TICKS_PER_500MS;
sched_flags.tick_500ms = 1;
}
ticks_until_100ms = TICKS_PER_100MS;
sched_flags.tick_100ms = 1;
}
if (--ticks_until_500ms == 0u)
{
ticks_until_500ms = TICKS_PER_500MS;
sched_flags.tick_500ms = 1;
}
}

Expand Down Expand Up @@ -2376,21 +2373,6 @@ static void MX_ADC_Init(void)
};
LL_ADC_Enable(ADC1);
LL_ADC_REG_StartConversion(ADC1);

LL_APB1_GRP2_EnableClock(LL_APB1_GRP2_PERIPH_TIM1);

LL_TIM_SetPrescaler(TIM1, __LL_TIM_CALC_PSC(SystemCoreClock, 10000));
/* Set the frequency to 100 Hz. */
LL_TIM_SetAutoReload(
TIM1, __LL_TIM_CALC_ARR(SystemCoreClock, LL_TIM_GetPrescaler(TIM1), 100));
LL_TIM_EnableIT_UPDATE(TIM1);

NVIC_SetPriority(TIM1_BRK_UP_TRG_COM_IRQn, 0);
NVIC_EnableIRQ(TIM1_BRK_UP_TRG_COM_IRQn);

LL_TIM_EnableCounter(TIM1);

LL_TIM_GenerateEvent_UPDATE(TIM1);
}

/**
Expand Down
9 changes: 8 additions & 1 deletion Src/stm32f0xx_it.c
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "stm32f0xx.h"
#include "ups_state.h"
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
Expand Down Expand Up @@ -139,7 +140,13 @@ void PendSV_Handler(void)
void SysTick_Handler(void)
{
/* USER CODE BEGIN SysTick_IRQn 0 */

static uint8_t div10 = 0;
div10++;
if (div10 >= 10)
{
div10 = 0;
Scheduler_ISR_Tick10ms();
}
/* USER CODE END SysTick_IRQn 0 */

/* USER CODE BEGIN SysTick_IRQn 1 */
Expand Down
3 changes: 2 additions & 1 deletion documents/UPSPlus_Behavior_Spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ It is intended as the source of truth for feature development and future changes

## 3. Timing Model

- Canonical scheduler tick: **10ms** (TIM1 ISR sets flags only).
- Canonical scheduler tick: **10ms** (derived from SysTick; ISR sets flags only).
- Derived tick rates:
- 100ms heartbeat
- 500ms ADC trigger
Expand Down Expand Up @@ -558,6 +558,7 @@ To boot the application after OTA programming:

## 16. Change Impact Map

- If the scheduler timebase (10 ms) source changes (e.g. SysTick vs TIM), update Section 3 and all comments referring to the interrupt source.
- If ADC cadence changes, revisit: charger stability counters, protection sample count, window duration.
- If register map changes, revisit: Factory Testing ABI, test scripts, and external tools.
- If snapshot frequency changes, revisit: I2C coherence assumptions and staleness guarantees.
Expand Down
Loading