mirror of
https://github.com/joeycastillo/second-movement.git
synced 2026-05-03 05:06:12 +00:00
Make fast_stopwatch power efficiend by using the new counter32
This commit is contained in:
@ -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);
|
||||
}
|
||||
|
||||
@ -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, \
|
||||
|
||||
Reference in New Issue
Block a user