Make fast_stopwatch power efficiend by using the new counter32

This commit is contained in:
Alessandro Genova
2025-07-26 22:31:53 -04:00
parent 6fe1b236a4
commit 1b9624d042
2 changed files with 193 additions and 230 deletions

View File

@ -2,6 +2,7 @@
* MIT License
*
* Copyright (c) 2022 Andreas Nebinger
* Copyright (c) 2025 Alessandro Genova
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@ -40,173 +41,203 @@
turns on on each button press or it doesn't.
*/
#if __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/html5.h>
#else
#include "tc.h"
#endif
// distant future for background task: January 1, 2083
static const watch_date_time_t distant_future = {
.unit = {0, 0, 0, 1, 1, 63}
};
static uint32_t _ticks;
static uint32_t _lap_ticks;
static uint8_t _blink_ticks;
static uint32_t _old_seconds;
static uint8_t _old_minutes;
static uint8_t _hours;
static bool _colon;
static bool _is_running;
#if __EMSCRIPTEN__
static long _em_interval_id = 0;
void em_cb_handler(void *userData) {
// interrupt handler for emscripten 128 Hz callbacks
(void) userData;
_ticks++;
}
static void _cb_initialize() { }
static inline void _cb_stop() {
emscripten_clear_interval(_em_interval_id);
_em_interval_id = 0;
_is_running = false;
}
static inline void _cb_start() {
// initiate 128 hz callback
_em_interval_id = emscripten_set_interval(em_cb_handler, (double)(1000/128), (void *)NULL);
}
#else
static inline void _cb_start() {
// start the TC1 timer
tc_enable(1);
_is_running = true;
}
static inline void _cb_stop() {
// stop the TC1 timer
tc_disable(1);
_is_running = false;
}
static void _cb_initialize() {
tc_init(1, GENERIC_CLOCK_3, TC_PRESCALER_DIV4);
tc_set_counter_mode(1, TC_COUNTER_MODE_8BIT);
tc_set_run_in_standby(1, true);
_cb_stop();
tc_count8_set_period(1, 1); // 1024 Hz divided by 4 divided by 2 results in a 128 Hz interrupt
/// FIXME: #SecondMovement, we need a gossamer wrapper for interrupts.
TC1->COUNT8.INTENSET.bit.OVF = 1;
NVIC_ClearPendingIRQ(TC1_IRQn);
NVIC_EnableIRQ (TC1_IRQn);
}
void irq_handler_tc1(void);
void irq_handler_tc1(void) {
// interrupt handler for TC1 (globally!)
_ticks++;
TC1->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF;
}
#endif
// Loosely implement the watch as a state machine
typedef enum {
SW_STATUS_IDLE = 0,
SW_STATUS_RUNNING,
SW_STATUS_RUNNING_LAPPING,
SW_STATUS_STOPPED,
SW_STATUS_STOPPED_LAPPING
} stopwatch_status_t;
static inline void _button_beep() {
// play a beep as confirmation for a button press (if applicable)
if (movement_button_should_sound()) watch_buzzer_play_note_with_volume(BUZZER_NOTE_C7, 50, movement_button_volume());
}
// How quickly should the elapsing time be displayed?
// This is just for looks, timekeeping is always accurate to 128Hz
static const uint8_t DISPLAY_RUNNING_RATE = 32;
static const uint32_t TWENTY_FOUR_HOURS = 24 * 60 * 60 * 128;
/// @brief Display minutes, seconds and fractions derived from 128 Hz tick counter
/// on the lcd.
/// @param ticks
static void _display_ticks(uint32_t ticks) {
static void _display_elapsed(uint32_t ticks) {
ticks = ticks % TWENTY_FOUR_HOURS;
char buf[14];
uint8_t sec_100 = (ticks & 0x7F) * 100 / 128;
uint32_t seconds = ticks >> 7;
uint32_t minutes = seconds / 60;
if (_hours) {
sprintf(buf, "%2u", _hours);
uint32_t hours = minutes / 60;
if (hours) {
sprintf(buf, "%2u", hours);
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
} else {
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
}
sprintf(buf, "%02lu%02lu%02u", minutes, (seconds % 60), sec_100);
sprintf(buf, "%02lu%02lu%02u", (minutes % 60), (seconds % 60), sec_100);
watch_display_text(WATCH_POSITION_BOTTOM, buf);
}
/// @brief Displays the current stopwatch time on the LCD (more optimized than _display_ticks())
static void _draw() {
if (_lap_ticks == 0) {
char buf[14];
uint8_t sec_100 = (_ticks & 0x7F) * 100 / 128;
if (_is_running) {
uint32_t seconds = _ticks >> 7;
if (seconds != _old_seconds) {
// seconds have changed
_old_seconds = seconds;
uint8_t minutes = seconds / 60;
seconds %= 60;
if (minutes != _old_minutes) {
// minutes have changed, draw everything
_old_minutes = minutes;
minutes %= 60;
if (_hours) {
// with hour indicator
sprintf(buf, "%2u", _hours);
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
} else {
// no hour indicator
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
}
sprintf(buf, "%02u%02lu%02u", minutes, seconds, sec_100);
watch_display_text(WATCH_POSITION_BOTTOM, buf);
} else {
// just draw seconds
sprintf(buf, "%02lu", seconds);
// note that we're drawing the seconds in the "minutes" position, since this
// watch face uses the "seconds" position for hundredths of seconds
watch_display_text(WATCH_POSITION_MINUTES, buf);
watch_display_character_lp_seconds('0' + sec_100 / 10, 8);
watch_display_character_lp_seconds('0' + sec_100 % 10, 9);
}
static void _draw_indicators(fast_stopwatch_state_t *state, movement_event_t event, uint32_t elapsed) {
uint8_t subsecond;
bool tock;
switch (state->status) {
case SW_STATUS_RUNNING:
subsecond = elapsed & 127;
tock = subsecond >= 64;
watch_clear_indicator(WATCH_INDICATOR_LAP);
if (tock) {
watch_clear_colon();
} else {
// only draw 100ths of seconds
watch_display_character_lp_seconds('0' + sec_100 / 10, 8);
watch_display_character_lp_seconds('0' + sec_100 % 10, 9);
watch_set_colon();
}
} else {
_display_ticks(_ticks);
}
}
if (_is_running) {
// blink the colon every half second
uint8_t blink_ticks = ((_ticks >> 6) & 1);
if (blink_ticks != _blink_ticks) {
_blink_ticks = blink_ticks;
_colon = !_colon;
if (_colon) watch_set_colon();
else watch_clear_colon();
}
return;
case SW_STATUS_RUNNING_LAPPING:
tock = event.subsecond > 0;
if (tock) {
watch_clear_indicator(WATCH_INDICATOR_LAP);
watch_clear_colon();
} else {
watch_set_indicator(WATCH_INDICATOR_LAP);
watch_set_colon();
}
return;
case SW_STATUS_STOPPED_LAPPING:
watch_set_indicator(WATCH_INDICATOR_LAP);
watch_set_colon();
return;
case SW_STATUS_STOPPED:
case SW_STATUS_IDLE:
default:
watch_clear_indicator(WATCH_INDICATOR_LAP);
watch_set_colon();
return;
}
}
static inline void _update_lap_indicator() {
if (_lap_ticks) watch_set_indicator(WATCH_INDICATOR_LAP);
else watch_clear_indicator(WATCH_INDICATOR_LAP);
static uint8_t get_refresh_rate(fast_stopwatch_state_t *state) {
switch (state->status) {
case SW_STATUS_RUNNING:
return DISPLAY_RUNNING_RATE;
case SW_STATUS_RUNNING_LAPPING:
return 2;
case SW_STATUS_STOPPED:
case SW_STATUS_IDLE:
default:
return 1;
}
}
static inline void _set_colon() {
watch_set_colon();
_colon = true;
static void state_transition(fast_stopwatch_state_t *state, rtc_counter_t counter, movement_event_type_t event_type) {
switch (state->status) {
case SW_STATUS_IDLE:
switch (event_type) {
case EVENT_ALARM_BUTTON_DOWN:
state->status = SW_STATUS_RUNNING;
state->start_counter = counter;
movement_request_tick_frequency(DISPLAY_RUNNING_RATE);
return;
default:
return;
}
case SW_STATUS_RUNNING:
switch (event_type) {
case EVENT_ALARM_BUTTON_DOWN:
state->status = SW_STATUS_STOPPED;
state->stop_counter = counter;
movement_request_tick_frequency(1);
return;
case EVENT_LIGHT_BUTTON_DOWN:
state->status = SW_STATUS_RUNNING_LAPPING;
state->lap_counter = counter;
movement_request_tick_frequency(2);
return;
default:
return;
}
case SW_STATUS_RUNNING_LAPPING:
switch (event_type) {
case EVENT_ALARM_BUTTON_DOWN:
state->status = SW_STATUS_STOPPED_LAPPING;
state->stop_counter = counter;
movement_request_tick_frequency(1);
return;
case EVENT_LIGHT_BUTTON_DOWN:
state->status = SW_STATUS_RUNNING;
state->lap_counter = counter;
movement_request_tick_frequency(DISPLAY_RUNNING_RATE);
return;
default:
return;
}
case SW_STATUS_STOPPED_LAPPING:
switch (event_type) {
case EVENT_ALARM_BUTTON_DOWN:
state->status = SW_STATUS_RUNNING_LAPPING;
state->start_counter = counter - state->stop_counter + state->start_counter;
state->lap_counter = counter - state->stop_counter + state->lap_counter;
movement_request_tick_frequency(2);
return;
case EVENT_LIGHT_BUTTON_DOWN:
state->status = SW_STATUS_STOPPED;
return;
default:
return;
}
case SW_STATUS_STOPPED:
switch (event_type) {
case EVENT_ALARM_BUTTON_DOWN:
state->status = SW_STATUS_RUNNING;
state->start_counter = counter - state->stop_counter + state->start_counter;
movement_request_tick_frequency(DISPLAY_RUNNING_RATE);
return;
case EVENT_LIGHT_BUTTON_DOWN:
state->status = SW_STATUS_IDLE;
return;
default:
return;
}
default:
return;
}
}
static uint32_t elapsed_time(fast_stopwatch_state_t *state, rtc_counter_t counter) {
switch (state->status) {
case SW_STATUS_IDLE:
return 0;
case SW_STATUS_RUNNING:
return counter - state->start_counter;
case SW_STATUS_RUNNING_LAPPING:
case SW_STATUS_STOPPED_LAPPING:
return state->lap_counter - state->start_counter;
case SW_STATUS_STOPPED:
return state->stop_counter - state->start_counter;
default:
return 0;
}
}
void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr) {
@ -215,114 +246,49 @@ void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr) {
*context_ptr = malloc(sizeof(fast_stopwatch_state_t));
memset(*context_ptr, 0, sizeof(fast_stopwatch_state_t));
fast_stopwatch_state_t *state = (fast_stopwatch_state_t *)*context_ptr;
_ticks = _lap_ticks = _blink_ticks = _old_minutes = _old_seconds = _hours = 0;
_is_running = _colon = false;
state->light_on_button = true;
}
if (!_is_running) {
// prepare the 128 Hz callback source
_cb_initialize();
state->start_counter = 0;
state->stop_counter = 0;
state->lap_counter = 0;
state->status = SW_STATUS_IDLE;
}
}
void fast_stopwatch_face_activate(void *context) {
(void) context;
if (_is_running) {
// The background task will keep the watch from entering low energy mode while the stopwatch is on screen.
movement_schedule_background_task(distant_future);
}
fast_stopwatch_state_t *state = (fast_stopwatch_state_t *) context;
movement_request_tick_frequency(get_refresh_rate(state));
}
bool fast_stopwatch_face_loop(movement_event_t event, void *context) {
fast_stopwatch_state_t *state = (fast_stopwatch_state_t *)context;
// handle overflow of fast ticks
while (_ticks >= (128 * 60 * 60)) {
_ticks -= (128 * 60 * 60);
_hours++;
if (_hours >= 24) _hours -= 24;
// initiate a re-draw
_old_minutes = 59;
}
rtc_counter_t counter = watch_rtc_get_counter();
state_transition(state, counter, event.event_type);
rtc_counter_t elapsed = elapsed_time(state, counter);
switch (event.event_type) {
case EVENT_ACTIVATE:
_set_colon();
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "STW", "ST");
_update_lap_indicator();
if (_is_running) movement_request_tick_frequency(16);
_display_ticks(_lap_ticks ? _lap_ticks : _ticks);
break;
case EVENT_TICK:
_draw();
break;
case EVENT_LIGHT_LONG_PRESS:
// kind od hidden feature: long press toggles light on or off
state->light_on_button = !state->light_on_button;
if (state->light_on_button) movement_illuminate_led();
else watch_set_led_off();
_draw_indicators(state, event, elapsed);
_display_elapsed(elapsed);
break;
case EVENT_ALARM_BUTTON_DOWN:
_is_running = !_is_running;
if (_is_running) {
// start or continue stopwatch
movement_request_tick_frequency(16);
// register 128 hz callback for time measuring
_cb_start();
// schedule the keepalive task when running
movement_schedule_background_task(distant_future);
} else {
// stop the stopwatch
_cb_stop();
movement_request_tick_frequency(1);
_set_colon();
// cancel the keepalive task
movement_cancel_background_task();
}
_draw();
_button_beep();
break;
case EVENT_LIGHT_BUTTON_DOWN:
if (state->light_on_button) movement_illuminate_led();
if (_is_running) {
if (_lap_ticks) {
// clear lap and continue running
_lap_ticks = 0;
movement_request_tick_frequency(16);
} else {
// set lap ticks and stop updating the display
_lap_ticks = _ticks;
movement_request_tick_frequency(2);
_set_colon();
}
} else {
if (_lap_ticks) {
// clear lap and show running stopwatch
_lap_ticks = 0;
} else if (_ticks) {
// reset stopwatch
_ticks = _lap_ticks = _blink_ticks = _old_minutes = _old_seconds = _hours = 0;
_button_beep();
}
}
_display_ticks(_ticks);
_update_lap_indicator();
break;
case EVENT_TIMEOUT:
if (!_is_running) movement_move_to_face(0);
break;
case EVENT_LOW_ENERGY_UPDATE:
_draw();
_button_beep();
// Fall into the case below;
case EVENT_TICK:
_draw_indicators(state, event, elapsed);
_display_elapsed(elapsed);
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
void fast_stopwatch_face_resign(void *context) {
(void) context;
// cancel the keepalive task
movement_cancel_background_task();
movement_request_tick_frequency(1);
}

View File

@ -55,7 +55,10 @@
#include "movement.h"
typedef struct {
bool light_on_button; // determines whether the light button actually triggers the led
rtc_counter_t start_counter; // rtc counter when the stopwatch was started
rtc_counter_t lap_counter; // rtc counter when the stopwatch was lapped
rtc_counter_t stop_counter; // rtc counter when the stopwatch was stopped
uint8_t status; // the status the stopwatch is in (idle, running, stopped)
} fast_stopwatch_state_t;
void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr);
@ -63,12 +66,6 @@ void fast_stopwatch_face_activate(void *context);
bool fast_stopwatch_face_loop(movement_event_t event, void *context);
void fast_stopwatch_face_resign(void *context);
#if __EMSCRIPTEN__
void em_cb_handler(void *userData);
#else
void TC2_Handler(void);
#endif
#define fast_stopwatch_face ((const watch_face_t){ \
fast_stopwatch_face_setup, \
fast_stopwatch_face_activate, \