From eb9ec8659c42e34dc1b31e3ff591ac2bf842271f Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Sat, 26 Jul 2025 21:40:22 -0400 Subject: [PATCH 01/53] Initial seemingly stable version of movement using the RTC COUNTER32 mode --- Makefile | 4 + movement.c | 765 +++++++++++++-------- movement.h | 39 +- watch-library/hardware/watch/rtc32.c | 138 ++++ watch-library/hardware/watch/rtc32.h | 98 +++ watch-library/hardware/watch/watch_rtc.c | 187 +++-- watch-library/hardware/watch/watch_tcc.c | 119 +++- watch-library/shared/watch/watch_rtc.h | 68 +- watch-library/shared/watch/watch_tcc.h | 30 +- watch-library/shared/watch/watch_utility.c | 4 + watch-library/shared/watch/watch_utility.h | 10 + watch-library/simulator/watch/watch_rtc.c | 320 +++++---- watch-library/simulator/watch/watch_tcc.c | 67 +- 13 files changed, 1335 insertions(+), 514 deletions(-) create mode 100644 watch-library/hardware/watch/rtc32.c create mode 100644 watch-library/hardware/watch/rtc32.h diff --git a/Makefile b/Makefile index cae2b943..3c4436f3 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,9 @@ TINYUSB_CDC=1 # Now we're all set to include gossamer's make rules. include $(GOSSAMER_PATH)/make.mk +# Don't add gossamer's rtc.c since we are using our own rtc32.c +SRCS := $(filter-out $(GOSSAMER_PATH)/peripherals/rtc.c,$(SRCS)) + CFLAGS+=-D_POSIX_C_SOURCE=200112L define n @@ -136,6 +139,7 @@ INCLUDES += \ -I./watch-library/hardware/watch \ SRCS += \ + ./watch-library/hardware/watch/rtc32.c \ ./watch-library/hardware/watch/watch.c \ ./watch-library/hardware/watch/watch_adc.c \ ./watch-library/hardware/watch/watch_deepsleep.c \ diff --git a/movement.c b/movement.c index a37e8df2..adc01871 100644 --- a/movement.c +++ b/movement.c @@ -2,6 +2,7 @@ * MIT License * * Copyright (c) 2022 Joey Castillo + * 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 @@ -59,7 +60,55 @@ void * watch_face_contexts[MOVEMENT_NUM_FACES]; watch_date_time_t scheduled_tasks[MOVEMENT_NUM_FACES]; const int32_t movement_le_inactivity_deadlines[8] = {INT_MAX, 600, 3600, 7200, 21600, 43200, 86400, 604800}; const int16_t movement_timeout_inactivity_deadlines[4] = {60, 120, 300, 1800}; -movement_event_t event; + +typedef struct { + movement_event_type_t down_event; + watch_cb_t cb_longpress; + movement_timeout_index_t timeout_index; + volatile bool is_down; + volatile rtc_counter_t down_timestamp; +} movement_button_t; + +/* Pieces of state that can be modified by the various interrupt callbacks. + The interrupt writes state changes here, and it will be acted upon on the next app_loop invokation. +*/ +typedef struct { + volatile uint32_t pending_events; + volatile bool turn_led_off; + volatile bool has_pending_sequence; + volatile bool enter_sleep_mode; + volatile bool exit_sleep_mode; + volatile bool is_sleeping; + volatile uint8_t subsecond; + volatile rtc_counter_t minute_counter; + volatile bool minute_alarm_fired; + volatile bool is_buzzing; + volatile uint8_t pending_sequence_priority; + + // button tracking for long press + movement_button_t mode_button; + movement_button_t light_button; + movement_button_t alarm_button; +} movement_volatile_state_t; + +movement_volatile_state_t movement_volatile_state; + +// The last sequence that we have been asked to play while the watch was in deep sleep +static int8_t *_pending_sequence; + +// The note sequence of the default alarm +int8_t alarm_tune[] = { + BUZZER_NOTE_C8, 4, + BUZZER_NOTE_REST, 4, + BUZZER_NOTE_C8, 4, + BUZZER_NOTE_REST, 4, + BUZZER_NOTE_C8, 4, + BUZZER_NOTE_REST, 4, + BUZZER_NOTE_C8, 6, + BUZZER_NOTE_REST, 18, + -8, 9, + 0 +}; int8_t _movement_dst_offset_cache[NUM_ZONE_NAMES] = {0}; #define TIMEZONE_DOES_NOT_OBSERVE (-127) @@ -68,9 +117,16 @@ void cb_mode_btn_interrupt(void); void cb_light_btn_interrupt(void); void cb_alarm_btn_interrupt(void); void cb_alarm_btn_extwake(void); -void cb_alarm_fired(void); -void cb_fast_tick(void); +void cb_minute_alarm_fired(void); void cb_tick(void); +void cb_mode_btn_timeout_interrupt(void); +void cb_light_btn_timeout_interrupt(void); +void cb_alarm_btn_timeout_interrupt(void); +void cb_led_timeout_interrupt(void); +void cb_resign_timeout_interrupt(void); +void cb_sleep_timeout_interrupt(void); +void cb_buzzer_start(void); +void cb_buzzer_stop(void); void cb_accelerometer_event(void); void cb_accelerometer_wake(void); @@ -97,6 +153,21 @@ static udatetime_t _movement_convert_date_time_to_udate(watch_date_time_t date_t }; } +static void _movement_set_top_of_minute_alarm() { + uint32_t counter = watch_rtc_get_counter(); + watch_date_time_t date_time = watch_rtc_get_date_time(); + uint32_t freq = watch_rtc_get_frequency(); + + // remove subsecond from counter + counter &= ~(freq - 1); + // counter at the next top of the minute + counter += (60 - date_time.unit.second) * freq; + + movement_volatile_state.minute_counter = counter; + + watch_rtc_register_comp_callback(cb_minute_alarm_fired, counter, MINUTE_TIMEOUT); +} + static bool _movement_update_dst_offset_cache(void) { uzone_t local_zone; udatetime_t udate_time; @@ -127,25 +198,27 @@ static bool _movement_update_dst_offset_cache(void) { } static inline void _movement_reset_inactivity_countdown(void) { - movement_state.le_mode_ticks = movement_le_inactivity_deadlines[movement_state.settings.bit.le_interval]; - movement_state.timeout_ticks = movement_timeout_inactivity_deadlines[movement_state.settings.bit.to_interval]; + rtc_counter_t counter = watch_rtc_get_counter(); + uint32_t freq = watch_rtc_get_frequency(); + + watch_rtc_register_comp_callback( + cb_resign_timeout_interrupt, + counter + movement_timeout_inactivity_deadlines[movement_state.settings.bit.to_interval] * freq, + RESIGN_TIMEOUT + ); + + movement_volatile_state.enter_sleep_mode = false; + + watch_rtc_register_comp_callback( + cb_sleep_timeout_interrupt, + counter + movement_le_inactivity_deadlines[movement_state.settings.bit.le_interval] * freq, + SLEEP_TIMEOUT + ); } -static inline void _movement_enable_fast_tick_if_needed(void) { - if (!movement_state.fast_tick_enabled) { - movement_state.fast_ticks = 0; - watch_rtc_register_periodic_callback(cb_fast_tick, 128); - movement_state.fast_tick_enabled = true; - } -} - -static inline void _movement_disable_fast_tick_if_possible(void) { - if ((movement_state.light_ticks == -1) && - (movement_state.alarm_ticks == -1) && - ((movement_state.light_down_timestamp + movement_state.mode_down_timestamp + movement_state.alarm_down_timestamp) == 0)) { - movement_state.fast_tick_enabled = false; - watch_rtc_disable_periodic_callback(128); - } +static inline void _movement_disable_inactivity_countdown(void) { + watch_rtc_disable_comp_callback(RESIGN_TIMEOUT); + watch_rtc_disable_comp_callback(SLEEP_TIMEOUT); } static void _movement_handle_top_of_minute(void) { @@ -172,7 +245,6 @@ static void _movement_handle_top_of_minute(void) { // TODO: handle other advisory types } } - movement_state.woke_from_alarm_handler = false; } static void _movement_handle_scheduled_tasks(void) { @@ -203,45 +275,59 @@ static void _movement_handle_scheduled_tasks(void) { } void movement_request_tick_frequency(uint8_t freq) { - // Movement uses the 128 Hz tick internally - if (freq == 128) return; - // Movement requires at least a 1 Hz tick. // If we are asked for an invalid frequency, default back to 1 Hz. if (freq == 0 || __builtin_popcount(freq) != 1) freq = 1; - // disable all callbacks except the 128 Hz one - watch_rtc_disable_matching_periodic_callbacks(0xFE); + // disable all periodic callbacks + watch_rtc_disable_matching_periodic_callbacks(0xFF); + + // this left-justifies the period in a 32-bit integer. + uint32_t tmp = (freq & 0xFF) << 24; + // now we can count the leading zeroes to get the value we need. + // 0x01 (1 Hz) will have 7 leading zeros for PER7. 0x80 (128 Hz) will have no leading zeroes for PER0. + uint8_t per_n = __builtin_clz(tmp); - movement_state.subsecond = 0; movement_state.tick_frequency = freq; + movement_state.tick_pern = per_n; + watch_rtc_register_periodic_callback(cb_tick, freq); } void movement_illuminate_led(void) { if (movement_state.settings.bit.led_duration != 0b111) { + movement_state.light_on = true; watch_set_led_color_rgb(movement_state.settings.bit.led_red_color | movement_state.settings.bit.led_red_color << 4, movement_state.settings.bit.led_green_color | movement_state.settings.bit.led_green_color << 4, movement_state.settings.bit.led_blue_color | movement_state.settings.bit.led_blue_color << 4); if (movement_state.settings.bit.led_duration == 0) { - movement_state.light_ticks = 1; + // Do nothing it'll be turned off on button release } else { - movement_state.light_ticks = (movement_state.settings.bit.led_duration * 2 - 1) * 128; + // Set a timeout to turn off the light + rtc_counter_t counter = watch_rtc_get_counter(); + uint32_t freq = watch_rtc_get_frequency(); + watch_rtc_register_comp_callback( + cb_led_timeout_interrupt, + counter + (movement_state.settings.bit.led_duration * 2 - 1) * freq, + LED_TIMEOUT + ); } - _movement_enable_fast_tick_if_needed(); } } void movement_force_led_on(uint8_t red, uint8_t green, uint8_t blue) { // this is hacky, we need a way for watch faces to set an arbitrary color and prevent Movement from turning it right back off. + movement_state.light_on = true; watch_set_led_color_rgb(red, green, blue); - movement_state.light_ticks = 32767; + rtc_counter_t counter = watch_rtc_get_counter(); + watch_rtc_register_comp_callback(cb_led_timeout_interrupt, counter + 32767, LED_TIMEOUT); } void movement_force_led_off(void) { + movement_state.light_on = false; + // The led timeout probably already triggered, but still disable just in case we are switching off the light by other means + watch_rtc_disable_comp_callback(LED_TIMEOUT); watch_set_led_off(); - movement_state.light_ticks = -1; - _movement_disable_fast_tick_if_possible(); } bool movement_default_loop_handler(movement_event_t event) { @@ -315,63 +401,98 @@ void movement_cancel_background_task_for_face(uint8_t watch_face_index) { } void movement_request_sleep(void) { - /// FIXME: for #SecondMovement: This was a feature request to allow watch faces to request sleep. - /// Setting the ticks to 1 means the watch will sleep after the next tick. I'd like to say let's - /// set it to 0, have the watch face loop return false, and then we'll fall asleep immediately. - /// But could this lead to a race condition where the callback decrements to -1 before the loop? - /// This is the safest way but consider more testing here. - movement_state.le_mode_ticks = 1; + movement_volatile_state.enter_sleep_mode = true; } void movement_request_wake() { - movement_state.needs_wake = true; + movement_volatile_state.exit_sleep_mode = true; _movement_reset_inactivity_countdown(); } -static void end_buzzing() { - movement_state.is_buzzing = false; +void cb_buzzer_start(void) { + movement_volatile_state.is_buzzing = true; } -static void end_buzzing_and_disable_buzzer(void) { - end_buzzing(); - watch_disable_buzzer(); +void cb_buzzer_stop(void) { + movement_volatile_state.is_buzzing = false; + movement_volatile_state.pending_sequence_priority = 0; +} + +void movement_play_note(watch_buzzer_note_t note, uint16_t duration_ms) { + static int8_t single_note_sequence[3]; + + single_note_sequence[0] = note; + // 48 ticks per second for the tc0? + // Each tick is approximately 20ms + uint16_t duration = duration_ms / 20; + if (duration > 127) duration = 127; + single_note_sequence[1] = (int8_t)duration; + single_note_sequence[2] = 0; + + movement_play_sequence(single_note_sequence, 0); } void movement_play_signal(void) { - void *maybe_disable_buzzer = end_buzzing_and_disable_buzzer; - if (watch_is_buzzer_or_led_enabled()) { - maybe_disable_buzzer = end_buzzing; - } else { - watch_enable_buzzer(); - } - movement_state.is_buzzing = true; - watch_buzzer_play_sequence(signal_tune, maybe_disable_buzzer); - if (movement_state.le_mode_ticks == -1) { - // the watch is asleep. wake it up for "1" round through the main loop. - // the sleep_mode_app_loop will notice the is_buzzing and note that it - // only woke up to beep and then it will spinlock until the callback - // turns off the is_buzzing flag. - movement_state.needs_wake = true; - movement_state.le_mode_ticks = 1; - } + movement_play_sequence(signal_tune, 1); } void movement_play_alarm(void) { - movement_play_alarm_beeps(5, BUZZER_NOTE_C8); + movement_play_sequence(alarm_tune, 2); } void movement_play_alarm_beeps(uint8_t rounds, watch_buzzer_note_t alarm_note) { + // Ugly but necessary to avoid breaking backward compatibility with some faces. + // Create an alarm tune on the fly with the specified note and repetition. + static int8_t custom_alarm_tune[19]; + if (rounds == 0) rounds = 1; if (rounds > 20) rounds = 20; - movement_request_wake(); - movement_state.alarm_note = alarm_note; - // our tone is 0.375 seconds of beep and 0.625 of silence, repeated as given. - movement_state.alarm_ticks = 128 * rounds - 75; - _movement_enable_fast_tick_if_needed(); + + for (uint8_t i = 0; i < 9; i++) { + uint8_t note_idx = i * 2; + uint8_t duration_idx = note_idx + 1; + + int8_t note = alarm_tune[note_idx]; + int8_t duration = alarm_tune[duration_idx]; + + if (note == BUZZER_NOTE_C8) { + note = alarm_note; + } else if (note < 0) { + duration = rounds; + } + + custom_alarm_tune[note_idx] = note; + custom_alarm_tune[duration_idx] = duration; + } + + custom_alarm_tune[18] = 0; + + movement_play_sequence(custom_alarm_tune, 2); +} + +void movement_play_sequence(int8_t *note_sequence, uint8_t priority) { + // Priority is used to ensure that lower priority sequences don't cancel higher priority ones + // Priotity order: alarm(2) > signal(1) > note(0) + if (priority < movement_volatile_state.pending_sequence_priority) { + return; + } + + movement_volatile_state.pending_sequence_priority = priority; + + // The tcc is off during sleep, we can't play immediately. + // Ask to wake up the watch. + if (movement_volatile_state.is_sleeping) { + _pending_sequence = note_sequence; + movement_volatile_state.has_pending_sequence = true; + movement_volatile_state.exit_sleep_mode = true; + } else { + watch_buzzer_play_sequence_with_volume(note_sequence, NULL, movement_button_volume()); + } } uint8_t movement_claim_backup_register(void) { - if (movement_state.next_available_backup_register >= 8) return 0; + // We use backup register 7 in watch_rtc to keep track of the reference time + if (movement_state.next_available_backup_register >= 7) return 0; return movement_state.next_available_backup_register++; } @@ -405,18 +526,20 @@ watch_date_time_t movement_get_utc_date_time(void) { watch_date_time_t movement_get_date_time_in_zone(uint8_t zone_index) { int32_t offset = movement_get_current_timezone_offset_for_zone(zone_index); - return watch_utility_date_time_convert_zone(watch_rtc_get_date_time(), 0, offset); + unix_timestamp_t timestamp = watch_rtc_get_unix_time(); + return watch_utility_date_time_from_unix_time(timestamp, offset); } watch_date_time_t movement_get_local_date_time(void) { - watch_date_time_t date_time = watch_rtc_get_date_time(); - return watch_utility_date_time_convert_zone(date_time, 0, movement_get_current_timezone_offset()); + unix_timestamp_t timestamp = watch_rtc_get_unix_time(); + return watch_utility_date_time_from_unix_time(timestamp, movement_get_current_timezone_offset()); } -void movement_set_local_date_time(watch_date_time_t date_time) { - int32_t current_offset = movement_get_current_timezone_offset(); - watch_date_time_t utc_date_time = watch_utility_date_time_convert_zone(date_time, current_offset, 0); - watch_rtc_set_date_time(utc_date_time); +void movement_set_utc_date_time(watch_date_time_t date_time) { + watch_rtc_set_date_time(date_time); + + // If the time was changed, the top of the minute alarm needs to be reset accordingly + _movement_set_top_of_minute_alarm(); // this may seem wasteful, but if the user's local time is in a zone that observes DST, // they may have just crossed a DST boundary, which means the next call to this function @@ -424,6 +547,12 @@ void movement_set_local_date_time(watch_date_time_t date_time) { _movement_update_dst_offset_cache(); } +void movement_set_local_date_time(watch_date_time_t date_time) { + int32_t current_offset = movement_get_current_timezone_offset(); + watch_date_time_t utc_date_time = watch_utility_date_time_convert_zone(date_time, current_offset, 0); + movement_set_utc_date_time(utc_date_time); +} + bool movement_button_should_sound(void) { return movement_state.settings.bit.button_should_sound; } @@ -625,6 +754,38 @@ void app_init(void) { memset((void *)&movement_state, 0, sizeof(movement_state)); + movement_volatile_state.pending_events = 0; + movement_volatile_state.turn_led_off = false; + + movement_volatile_state.minute_alarm_fired = false; + movement_volatile_state.minute_counter = 0; + + movement_volatile_state.enter_sleep_mode = false; + movement_volatile_state.exit_sleep_mode = false; + movement_volatile_state.has_pending_sequence = false; + movement_volatile_state.is_sleeping = false; + + movement_volatile_state.is_buzzing = false; + movement_volatile_state.pending_sequence_priority = 0; + + movement_volatile_state.mode_button.down_event = EVENT_MODE_BUTTON_DOWN; + movement_volatile_state.mode_button.is_down = false; + movement_volatile_state.mode_button.down_timestamp = 0; + movement_volatile_state.mode_button.timeout_index = MODE_BUTTON_TIMEOUT; + movement_volatile_state.mode_button.cb_longpress = cb_mode_btn_timeout_interrupt; + + movement_volatile_state.light_button.down_event = EVENT_LIGHT_BUTTON_DOWN; + movement_volatile_state.light_button.is_down = false; + movement_volatile_state.light_button.down_timestamp = 0; + movement_volatile_state.light_button.timeout_index = LIGHT_BUTTON_TIMEOUT; + movement_volatile_state.light_button.cb_longpress = cb_light_btn_timeout_interrupt; + + movement_volatile_state.alarm_button.down_event = EVENT_ALARM_BUTTON_DOWN; + movement_volatile_state.alarm_button.is_down = false; + movement_volatile_state.alarm_button.down_timestamp = 0; + movement_volatile_state.alarm_button.timeout_index = ALARM_BUTTON_TIMEOUT; + movement_volatile_state.alarm_button.cb_longpress = cb_alarm_btn_timeout_interrupt; + movement_state.has_thermistor = thermistor_driver_init(); bool settings_file_exists = filesystem_file_exists("settings.u32"); @@ -680,13 +841,19 @@ void app_init(void) { watch_rtc_set_date_time(date_time); } + // set up the 1 minute alarm (for background tasks and low power updates) + _movement_set_top_of_minute_alarm(); + + // register callbacks to be notified when buzzer starts/stops playing. + // this is so movement can be notified even when triggered by a face bypassing movement + watch_buzzer_register_global_callbacks(cb_buzzer_start, cb_buzzer_stop); + // populate the DST offset cache _movement_update_dst_offset_cache(); if (movement_state.accelerometer_motion_threshold == 0) movement_state.accelerometer_motion_threshold = 32; - movement_state.light_ticks = -1; - movement_state.alarm_ticks = -1; + movement_state.light_on = false; movement_state.next_available_backup_register = 2; _movement_reset_inactivity_countdown(); } @@ -721,17 +888,12 @@ void app_setup(void) { } } #endif - - // set up the 1 minute alarm (for background tasks and low power updates) - watch_date_time_t alarm_time; - alarm_time.reg = 0; - watch_rtc_register_alarm_callback(cb_alarm_fired, alarm_time, ALARM_MATCH_SS); } // LCD autodetect uses the buttons as a a failsafe, so we should run it before we enable the button interrupts watch_enable_display(); - if (movement_state.le_mode_ticks != -1) { + if (!movement_volatile_state.is_sleeping) { watch_disable_extwake_interrupt(HAL_GPIO_BTN_ALARM_pin()); watch_enable_external_interrupts(); @@ -808,150 +970,186 @@ void app_setup(void) { } watch_faces[movement_state.current_face_idx].activate(watch_face_contexts[movement_state.current_face_idx]); - event.subsecond = 0; - event.event_type = EVENT_ACTIVATE; + movement_volatile_state.pending_events |= 1 << EVENT_ACTIVATE; } } #ifndef MOVEMENT_LOW_ENERGY_MODE_FORBIDDEN static void _sleep_mode_app_loop(void) { - movement_state.needs_wake = false; - // as long as le_mode_ticks is -1 (i.e. we are in low energy mode), we wake up here, update the screen, and go right back to sleep. - while (movement_state.le_mode_ticks == -1) { - // we also have to handle top-of-the-minute tasks here in the mini-runloop - if (movement_state.woke_from_alarm_handler) _movement_handle_top_of_minute(); + // as long as we are in low energy mode, we wake up here, update the screen, and go right back to sleep. + while (movement_volatile_state.is_sleeping) { + // if we need to wake immediately, do it! + if (movement_volatile_state.exit_sleep_mode) { + movement_volatile_state.exit_sleep_mode = false; + movement_volatile_state.is_sleeping = false; + return; + } + + // we also have to handle top-of-the-minute tasks here in the mini-runloop + if (movement_volatile_state.minute_alarm_fired) { + movement_volatile_state.minute_alarm_fired = false; + _movement_handle_top_of_minute(); + } + + movement_event_t event; event.event_type = EVENT_LOW_ENERGY_UPDATE; + event.subsecond = 0; watch_faces[movement_state.current_face_idx].loop(event, watch_face_contexts[movement_state.current_face_idx]); - // if we need to wake immediately, do it! - if (movement_state.needs_wake) return; - // otherwise enter sleep mode, and when the extwake handler is called, it will reset le_mode_ticks and force us out at the next loop. - else watch_enter_sleep_mode(); + // If any of the previous loops requested to wake up, do it! + if (movement_volatile_state.exit_sleep_mode) { + movement_volatile_state.exit_sleep_mode = false; + movement_volatile_state.is_sleeping = false; + + return; + } + + // otherwise enter sleep mode, until either the top of the minute interrupt or extwake wakes us up. + watch_enter_sleep_mode(); } } #endif +static bool _switch_face(void) { + const watch_face_t *wf = &watch_faces[movement_state.current_face_idx]; + + wf->resign(watch_face_contexts[movement_state.current_face_idx]); + movement_state.current_face_idx = movement_state.next_face_idx; + // we have just updated the face idx, so we must recache the watch face pointer. + wf = &watch_faces[movement_state.current_face_idx]; + watch_clear_display(); + movement_request_tick_frequency(1); + + if (movement_state.settings.bit.button_should_sound) { + // low note for nonzero case, high note for return to watch_face 0 + movement_play_note(movement_state.next_face_idx ? BUZZER_NOTE_C7 : BUZZER_NOTE_C8, 50); + } + + wf->activate(watch_face_contexts[movement_state.current_face_idx]); + + movement_event_t event; + event.subsecond = 0; + event.event_type = EVENT_ACTIVATE; + movement_state.watch_face_changed = false; + bool can_sleep = wf->loop(event, watch_face_contexts[movement_state.current_face_idx]); + + return can_sleep; +} + bool app_loop(void) { const watch_face_t *wf = &watch_faces[movement_state.current_face_idx]; - bool woke_up_for_buzzer = false; - - if (movement_state.watch_face_changed) { - if (movement_state.settings.bit.button_should_sound) { - // low note for nonzero case, high note for return to watch_face 0 - watch_buzzer_play_note_with_volume(movement_state.next_face_idx ? BUZZER_NOTE_C7 : BUZZER_NOTE_C8, 50, movement_state.settings.bit.button_volume); - } - wf->resign(watch_face_contexts[movement_state.current_face_idx]); - movement_state.current_face_idx = movement_state.next_face_idx; - // we have just updated the face idx, so we must recache the watch face pointer. - wf = &watch_faces[movement_state.current_face_idx]; - watch_clear_display(); - movement_request_tick_frequency(1); - wf->activate(watch_face_contexts[movement_state.current_face_idx]); - event.subsecond = 0; - event.event_type = EVENT_ACTIVATE; - movement_state.watch_face_changed = false; - } - - // if the LED should be off, turn it off - if (movement_state.light_ticks == 0) { - // unless the user is holding down the LIGHT button, in which case, give them more time. - if (HAL_GPIO_BTN_LIGHT_read()) { - movement_state.light_ticks = 1; - } else { - movement_force_led_off(); - } - } - - // handle top-of-minute tasks, if the alarm handler told us we need to - if (movement_state.woke_from_alarm_handler) _movement_handle_top_of_minute(); - - // if we have a scheduled background task, handle that here: - if (event.event_type == EVENT_TICK && movement_state.has_scheduled_background_task) _movement_handle_scheduled_tasks(); - -#ifndef MOVEMENT_LOW_ENERGY_MODE_FORBIDDEN - // if we have timed out of our low energy mode countdown, enter low energy mode. - if (movement_state.le_mode_ticks == 0) { - movement_state.le_mode_ticks = -1; - watch_register_extwake_callback(HAL_GPIO_BTN_ALARM_pin(), cb_alarm_btn_extwake, true); - event.event_type = EVENT_NONE; - event.subsecond = 0; - - // _sleep_mode_app_loop takes over at this point and loops until le_mode_ticks is reset by the extwake handler, - // or wake is requested using the movement_request_wake function. - _sleep_mode_app_loop(); - // as soon as _sleep_mode_app_loop returns, we prepare to reactivate - // ourselves, but first, we check to see if we woke up for the buzzer: - if (movement_state.is_buzzing) { - woke_up_for_buzzer = true; - } - event.event_type = EVENT_ACTIVATE; - // this is a hack tho: waking from sleep mode, app_setup does get called, but it happens before we have reset our ticks. - // need to figure out if there's a better heuristic for determining how we woke up. - app_setup(); - } -#endif // default to being allowed to sleep by the face. bool can_sleep = true; - if (event.event_type) { - event.subsecond = movement_state.subsecond; - // the first trip through the loop overrides the can_sleep state - can_sleep = wf->loop(event, watch_face_contexts[movement_state.current_face_idx]); + // Any events that have been added by the various interrupts in between app_loop invokations + uint32_t pending_events = movement_volatile_state.pending_events; + movement_volatile_state.pending_events = 0; - // Keep light on if user is still interacting with the watch. - if (movement_state.light_ticks > 0) { - switch (event.event_type) { - case EVENT_LIGHT_BUTTON_DOWN: - case EVENT_MODE_BUTTON_DOWN: - case EVENT_ALARM_BUTTON_DOWN: - movement_illuminate_led(); - } + movement_event_t event; + event.event_type = EVENT_NONE; + // Subsecond is determined by the TICK event, if concurrent events have happened, + // they will all have the same subsecond as they should to keep backward compatibility. + event.subsecond = movement_volatile_state.subsecond; + + // if the LED should be off, turn it off + if (movement_volatile_state.turn_led_off) { + // unless the user is holding down the LIGHT button, in which case, give them more time. + if (movement_volatile_state.light_button.is_down) { + } else { + movement_volatile_state.turn_led_off = false; + movement_force_led_off(); } + } - event.event_type = EVENT_NONE; + // actually play the note sequence we were asked to play while in deep sleep. + if (movement_volatile_state.has_pending_sequence) { + movement_volatile_state.has_pending_sequence = false; + watch_buzzer_play_sequence_with_volume(_pending_sequence, movement_request_sleep, movement_button_volume()); + // When this sequence is done playing, movement_request_sleep is invoked and the watch will go, + // back to sleep (unless the user interacts with it in the meantime) + _pending_sequence = NULL; } - // if we have timed out of our timeout countdown, give the app a hint that they can resign. - if (movement_state.timeout_ticks == 0 && movement_state.current_face_idx != 0) { - movement_state.timeout_ticks = -1; + // handle top-of-minute tasks, if the alarm handler told us we need to + if (movement_volatile_state.minute_alarm_fired) { + movement_volatile_state.minute_alarm_fired = false; + _movement_handle_top_of_minute(); + } + + // if we have a scheduled background task, handle that here: + if ( + (pending_events & (1 << EVENT_TICK)) + && event.subsecond == 0 + && movement_state.has_scheduled_background_task + ) { + _movement_handle_scheduled_tasks(); + } + + // Delay auto light off if the user is still interacting with the watch. + if (movement_state.light_on) { + if (pending_events & ( + (1 << EVENT_LIGHT_BUTTON_DOWN) | + (1 << EVENT_MODE_BUTTON_DOWN) | + (1 << EVENT_ALARM_BUTTON_DOWN) + )) { + movement_illuminate_led(); + } + } + + // Pop the EVENT_TIMEOUT out of the pending_events so it can be handled separately + bool resign_timeout = (pending_events & (1 << EVENT_TIMEOUT)) != 0; + if (resign_timeout) { + pending_events &= ~(1 << EVENT_TIMEOUT); + } + + // Consume all the pending events + movement_event_type_t event_type = 0; + while (pending_events) { + if (pending_events & 1) { + event.event_type = event_type; + can_sleep = wf->loop(event, watch_face_contexts[movement_state.current_face_idx]) && can_sleep; + } + pending_events = pending_events >> 1; + event_type++; + } + + // Now handle the EVENT_TIMEOUT + if (resign_timeout && movement_state.current_face_idx != 0) { event.event_type = EVENT_TIMEOUT; - event.subsecond = movement_state.subsecond; - // if we run through the loop again to time out, we need to reconsider whether or not we can sleep. - // if the first trip said true, but this trip said false, we need the false to override, thus - // we will be using boolean AND: - // - // first trip | can sleep | cannot sleep | can sleep | cannot sleep - // second trip | can sleep | cannot sleep | cannot sleep | can sleep - // && | can sleep | cannot sleep | cannot sleep | cannot sleep - bool can_sleep2 = wf->loop(event, watch_face_contexts[movement_state.current_face_idx]); - can_sleep = can_sleep && can_sleep2; - event.event_type = EVENT_NONE; + can_sleep = wf->loop(event, watch_face_contexts[movement_state.current_face_idx]) && can_sleep; } - // Now that we've handled all display update tasks, handle the alarm. - if (movement_state.alarm_ticks >= 0) { - uint8_t buzzer_phase = (movement_state.alarm_ticks + 80) % 128; - if(buzzer_phase == 127) { - // failsafe: buzzer could have been disabled in the meantime - if (!watch_is_buzzer_or_led_enabled()) watch_enable_buzzer(); - // play 4 beeps plus pause - for(uint8_t i = 0; i < 4; i++) { - // TODO: This method of playing the buzzer blocks the UI while it's beeping. - // It might be better to time it with the fast tick. - watch_buzzer_play_note(movement_state.alarm_note, (i != 3) ? 50 : 75); - if (i != 3) watch_buzzer_play_note(BUZZER_NOTE_REST, 50); - } - } - if (movement_state.alarm_ticks == 0) { - movement_state.alarm_ticks = -1; - _movement_disable_fast_tick_if_possible(); - } + // The watch_face_changed flag might be set again by the face loop, so check it again + if (movement_state.watch_face_changed) { + can_sleep = _switch_face() && can_sleep; } +#ifndef MOVEMENT_LOW_ENERGY_MODE_FORBIDDEN + // if we have timed out of our low energy mode countdown, enter low energy mode. + if (movement_volatile_state.enter_sleep_mode && !movement_volatile_state.is_buzzing) { + movement_volatile_state.enter_sleep_mode = false; + movement_volatile_state.is_sleeping = true; + + // No need to fire resign and sleep interrupts while in sleep mode + _movement_disable_inactivity_countdown(); + + watch_register_extwake_callback(HAL_GPIO_BTN_ALARM_pin(), cb_alarm_btn_extwake, true); + + // _sleep_mode_app_loop takes over at this point and loops until exit_sleep_mode is set by the extwake handler, + // or wake is requested using the movement_request_wake function. + _sleep_mode_app_loop(); + // as soon as _sleep_mode_app_loop returns, we prepare to reactivate + + // // this is a hack tho: waking from sleep mode, app_setup does get called, but it happens before we have reset our ticks. + // // need to figure out if there's a better heuristic for determining how we woke up. + app_setup(); + } +#endif + #if __EMSCRIPTEN__ shell_task(); #else @@ -961,19 +1159,6 @@ bool app_loop(void) { } #endif - event.subsecond = 0; - - // if the watch face changed, we can't sleep because we need to update the display. - if (movement_state.watch_face_changed) can_sleep = false; - - // if we woke up for the buzzer, stay awake until it's finished. - if (woke_up_for_buzzer) { - while(watch_is_buzzer_or_led_enabled()); - } - - // if the LED is on, we need to stay awake to keep the TCC running. - if (movement_state.light_ticks != -1) can_sleep = false; - // if we are plugged into USB, we can't sleep because we need to keep the serial shell running. if (usb_is_enabled()) { yield(); @@ -983,114 +1168,152 @@ bool app_loop(void) { return can_sleep; } -static movement_event_type_t _figure_out_button_event(bool pin_level, movement_event_type_t button_down_event_type, volatile uint16_t *down_timestamp) { - // force alarm off if the user pressed a button. - if (movement_state.alarm_ticks) movement_state.alarm_ticks = 0; +static movement_event_type_t _process_button_event(bool pin_level, movement_button_t* button) { + // This shouldn't happen normally + if (pin_level == button->is_down) { + return EVENT_NONE; + } + + uint32_t counter = watch_rtc_get_counter(); + + button->is_down = pin_level; if (pin_level) { - // handle rising edge - _movement_enable_fast_tick_if_needed(); - *down_timestamp = movement_state.fast_ticks + 1; - return button_down_event_type; + // We schedule a timeout to fire the longpress event + button->down_timestamp = counter; + watch_rtc_register_comp_callback(button->cb_longpress, counter + MOVEMENT_LONG_PRESS_TICKS, button->timeout_index); + // force alarm off if the user pressed a button. + watch_buzzer_abort_sequence(); + return button->down_event; } else { - // this line is hack but it handles the situation where the light button was held for more than 20 seconds. - // fast tick is disabled by then, and the LED would get stuck on since there's no one left decrementing light_ticks. - if (movement_state.light_ticks == 1) movement_state.light_ticks = 0; - // now that that's out of the way, handle falling edge - uint16_t diff = movement_state.fast_ticks - *down_timestamp; - *down_timestamp = 0; - _movement_disable_fast_tick_if_possible(); - // any press over a half second is considered a long press. Fire the long-up event - if (diff > MOVEMENT_LONG_PRESS_TICKS) return button_down_event_type + 3; - else return button_down_event_type + 1; + // We cancel the timeout if it hasn't fired yet + watch_rtc_disable_comp_callback(button->timeout_index); + if ((counter - button->down_timestamp) >= MOVEMENT_LONG_PRESS_TICKS) { + return button->down_event + 3; + } else { + return button->down_event + 1; + } } } void cb_light_btn_interrupt(void) { bool pin_level = HAL_GPIO_BTN_LIGHT_read(); + + movement_volatile_state.pending_events |= 1 << _process_button_event(pin_level, &movement_volatile_state.light_button); + _movement_reset_inactivity_countdown(); - event.event_type = _figure_out_button_event(pin_level, EVENT_LIGHT_BUTTON_DOWN, &movement_state.light_down_timestamp); } void cb_mode_btn_interrupt(void) { bool pin_level = HAL_GPIO_BTN_MODE_read(); + + movement_volatile_state.pending_events |= 1 << _process_button_event(pin_level, &movement_volatile_state.mode_button); + _movement_reset_inactivity_countdown(); - event.event_type = _figure_out_button_event(pin_level, EVENT_MODE_BUTTON_DOWN, &movement_state.mode_down_timestamp); } void cb_alarm_btn_interrupt(void) { bool pin_level = HAL_GPIO_BTN_ALARM_read(); + + movement_volatile_state.pending_events |= 1 << _process_button_event(pin_level, &movement_volatile_state.alarm_button); + _movement_reset_inactivity_countdown(); - event.event_type = _figure_out_button_event(pin_level, EVENT_ALARM_BUTTON_DOWN, &movement_state.alarm_down_timestamp); +} + +static movement_event_type_t _process_button_longpress_timeout(movement_button_t* button) { + // Looks like all these checks are not needed for the longpress detection to work reliably. + // Keep the code around for now in case problems arise long-term. + + // if (!button->is_down) { + // return EVENT_NONE; + // } + + // movement_event_type_t up_event = button->down_event + 1; + + // if (movement_volatile_state.pending_events & 1 << up_event) { + // return EVENT_NONE; + // } + + // uint32_t counter = watch_rtc_get_counter(); + // if ((counter - button->down_timestamp) < MOVEMENT_LONG_PRESS_TICKS) { + // return EVENT_NONE; + // } + + movement_event_type_t longpress_event = button->down_event + 2; + + return longpress_event; +} + +void cb_light_btn_timeout_interrupt(void) { + movement_button_t* button = &movement_volatile_state.light_button; + + movement_volatile_state.pending_events |= 1 << _process_button_longpress_timeout(button); +} + +void cb_mode_btn_timeout_interrupt(void) { + movement_button_t* button = &movement_volatile_state.mode_button; + + movement_volatile_state.pending_events |= 1 << _process_button_longpress_timeout(button); +} + +void cb_alarm_btn_timeout_interrupt(void) { + movement_button_t* button = &movement_volatile_state.alarm_button; + + movement_volatile_state.pending_events |= 1 << _process_button_longpress_timeout(button); +} + +void cb_led_timeout_interrupt(void) { + movement_volatile_state.turn_led_off = true; +} + +void cb_resign_timeout_interrupt(void) { + movement_volatile_state.pending_events |= 1 << EVENT_TIMEOUT; +} + +void cb_sleep_timeout_interrupt(void) { + movement_request_sleep(); } void cb_alarm_btn_extwake(void) { // wake up! - _movement_reset_inactivity_countdown(); + movement_request_wake(); } -void cb_alarm_fired(void) { +void cb_minute_alarm_fired(void) { + movement_volatile_state.minute_alarm_fired = true; + #if __EMSCRIPTEN__ _wake_up_simulator(); #endif - movement_state.woke_from_alarm_handler = true; -} - -void cb_fast_tick(void) { - movement_state.fast_ticks++; - if (movement_state.light_ticks > 0) movement_state.light_ticks--; - if (movement_state.alarm_ticks > 0) movement_state.alarm_ticks--; - // check timestamps and auto-fire the long-press events - // Notice: is it possible that two or more buttons have an identical timestamp? In this case - // only one of these buttons would receive the long press event. Don't bother for now... - if (movement_state.light_down_timestamp > 0) - if (movement_state.fast_ticks - movement_state.light_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1) - event.event_type = EVENT_LIGHT_LONG_PRESS; - if (movement_state.mode_down_timestamp > 0) - if (movement_state.fast_ticks - movement_state.mode_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1) - event.event_type = EVENT_MODE_LONG_PRESS; - if (movement_state.alarm_down_timestamp > 0) - if (movement_state.fast_ticks - movement_state.alarm_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1) - event.event_type = EVENT_ALARM_LONG_PRESS; - // this is just a fail-safe; fast tick should be disabled as soon as the button is up, the LED times out, and/or the alarm finishes. - // but if for whatever reason it isn't, this forces the fast tick off after 20 seconds. - if (movement_state.fast_ticks >= 128 * 20) { - watch_rtc_disable_periodic_callback(128); - movement_state.fast_tick_enabled = false; - } + // Renew the alarm for a minute from the previous one (ensures no drift) + movement_volatile_state.minute_counter += watch_rtc_get_ticks_per_minute(); + watch_rtc_register_comp_callback(cb_minute_alarm_fired, movement_volatile_state.minute_counter, MINUTE_TIMEOUT); } void cb_tick(void) { - event.event_type = EVENT_TICK; - watch_date_time_t date_time = watch_rtc_get_date_time(); - if (date_time.unit.second != movement_state.last_second) { - // TODO: can we consolidate these two ticks? - if (movement_state.le_mode_ticks > 0) movement_state.le_mode_ticks--; - if (movement_state.timeout_ticks > 0) movement_state.timeout_ticks--; - - movement_state.last_second = date_time.unit.second; - movement_state.subsecond = 0; - } else { - movement_state.subsecond++; - } + rtc_counter_t counter = watch_rtc_get_counter(); + uint32_t freq = watch_rtc_get_frequency(); + uint32_t subsecond_mask = freq - 1; + movement_volatile_state.pending_events |= 1 << EVENT_TICK; + movement_volatile_state.subsecond = (counter & subsecond_mask) >> movement_state.tick_pern; } void cb_accelerometer_event(void) { uint8_t int_src = lis2dw_get_interrupt_source(); if (int_src & LIS2DW_REG_ALL_INT_SRC_DOUBLE_TAP) { - event.event_type = EVENT_DOUBLE_TAP; + movement_volatile_state.pending_events |= 1 << EVENT_DOUBLE_TAP; printf("Double tap!\n"); } if (int_src & LIS2DW_REG_ALL_INT_SRC_SINGLE_TAP) { - event.event_type = EVENT_SINGLE_TAP; + movement_volatile_state.pending_events |= 1 << EVENT_SINGLE_TAP; printf("Single tap!\n"); } } void cb_accelerometer_wake(void) { - event.event_type = EVENT_ACCELEROMETER_WAKE; + movement_volatile_state.pending_events |= 1 << EVENT_ACCELEROMETER_WAKE; // also: wake up! _movement_reset_inactivity_countdown(); } diff --git a/movement.h b/movement.h index 56f67a49..0288c9c9 100644 --- a/movement.h +++ b/movement.h @@ -134,6 +134,17 @@ typedef enum { EVENT_DOUBLE_TAP, // Accelerometer detected a double tap. This event is not yet implemented. } movement_event_type_t; +// Each different timeout type will use a different index when invoking watch_rtc_register_comp_callback +typedef enum { + LIGHT_BUTTON_TIMEOUT = 0, // Light button longpress timeout + MODE_BUTTON_TIMEOUT, // Mode button longpress timeout + ALARM_BUTTON_TIMEOUT, // Alarm button longpress timeout + LED_TIMEOUT, // LED off timeout + RESIGN_TIMEOUT, // Resign active face timeout + SLEEP_TIMEOUT, // Low-energy begin timeout + MINUTE_TIMEOUT, // Top of the Minute timeout +} movement_timeout_index_t; + typedef struct { uint8_t event_type; uint8_t subsecond; @@ -249,37 +260,16 @@ typedef struct { int16_t current_face_idx; int16_t next_face_idx; bool watch_face_changed; - bool fast_tick_enabled; - int16_t fast_ticks; // LED stuff - int16_t light_ticks; - - // alarm stuff - int16_t alarm_ticks; - bool is_buzzing; - watch_buzzer_note_t alarm_note; - - // button tracking for long press - uint16_t light_down_timestamp; - uint16_t mode_down_timestamp; - uint16_t alarm_down_timestamp; + bool light_on; // background task handling - bool woke_from_alarm_handler; bool has_scheduled_background_task; - bool needs_wake; - - // low energy mode countdown - int32_t le_mode_ticks; - - // app resignation countdown (TODO: consolidate with LE countdown?) - int16_t timeout_ticks; // stuff for subsecond tracking uint8_t tick_frequency; - uint8_t last_second; - uint8_t subsecond; + uint8_t tick_pern; // backup register stuff uint8_t next_available_backup_register; @@ -324,9 +314,11 @@ void movement_cancel_background_task_for_face(uint8_t watch_face_index); void movement_request_sleep(void); void movement_request_wake(void); +void movement_play_note(watch_buzzer_note_t note, uint16_t duration_ms); void movement_play_signal(void); void movement_play_alarm(void); void movement_play_alarm_beeps(uint8_t rounds, watch_buzzer_note_t alarm_note); +void movement_play_sequence(int8_t *note_sequence, uint8_t priority); uint8_t movement_claim_backup_register(void); @@ -340,6 +332,7 @@ watch_date_time_t movement_get_utc_date_time(void); watch_date_time_t movement_get_local_date_time(void); watch_date_time_t movement_get_date_time_in_zone(uint8_t zone_index); +void movement_set_utc_date_time(watch_date_time_t date_time); void movement_set_local_date_time(watch_date_time_t date_time); bool movement_button_should_sound(void); diff --git a/watch-library/hardware/watch/rtc32.c b/watch-library/hardware/watch/rtc32.c new file mode 100644 index 00000000..934564bb --- /dev/null +++ b/watch-library/hardware/watch/rtc32.c @@ -0,0 +1,138 @@ +/* + * MIT License + * + * Copyright (c) 2022 Joey Castillo + * 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 + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include +#include "rtc32.h" +#include "sam.h" + +rtc_cb_t _rtc_callback = NULL; + +#if defined(_SAMD21_) || defined(_SAMD11_) +#define CTRLREG (RTC->MODE0.CTRL) +#define MODE_SETTING (RTC_MODE0_CTRL_MODE_COUNT32_Val) // Mode 0 Count32 +#define PRESCALER_SETTING (RTC_MODE0_CTRL_PRESCALER_DIV8_Val) +#else +#define CTRLREG (RTC->MODE0.CTRLA) +#define MODE_SETTING (RTC_MODE0_CTRLA_MODE_COUNT32_Val) // Mode 0 Count32 +#define PRESCALER_SETTING (RTC_MODE0_CTRLA_PRESCALER_DIV8_Val) +#endif + +bool rtc_is_enabled(void) { + return CTRLREG.bit.ENABLE; +} + +static void _rtc_sync(void) { +#if defined(_SAMD21_) || defined(_SAMD11_) + while (RTC->MODE0.STATUS.bit.SYNCBUSY); +#else + while (RTC->MODE0.SYNCBUSY.reg & RTC_MODE0_SYNCBUSY_MASK); +#endif +} + +void rtc_init(void) { +#if defined(_SAMD21_) || defined(_SAMD11_) + // enable the RTC + PM->APBAMASK.reg |= PM_APBAMASK_RTC; + // clock RTC with GCLK3 (prescaled 1024 Hz output from the external crystal) + GCLK->CLKCTRL.reg = GCLK_CLKCTRL_GEN(3) | GCLK_CLKCTRL_ID(RTC_GCLK_ID) | GCLK_CLKCTRL_CLKEN; +#else + MCLK->APBAMASK.reg |= MCLK_APBAMASK_RTC; +#endif + + // if (rtc_is_enabled()) return; // don't reset the RTC if it's already set up. + // Reset everything, once things are stabilized we can think about preserving some state + CTRLREG.bit.ENABLE = 0; + + _rtc_sync(); + CTRLREG.bit.SWRST = 1; + _rtc_sync(); + + CTRLREG.bit.MODE = MODE_SETTING; + CTRLREG.bit.PRESCALER = PRESCALER_SETTING; + +#if defined(_SAML21_) || defined(_SAML22_) || defined(_SAMD51_) + CTRLREG.bit.COUNTSYNC = 1; +#endif + + RTC->MODE0.INTENSET.reg = RTC_MODE0_INTENSET_OVF; +} + +void rtc_enable(void) { + if (rtc_is_enabled()) return; + CTRLREG.bit.ENABLE = 1; + _rtc_sync(); +} + +void rtc_set_counter(rtc_counter_t counter) { + // // syncing before and after was found to increase reliability on Sensor Watch + _rtc_sync(); + RTC->MODE0.COUNT.reg = counter; + _rtc_sync(); +} + +rtc_counter_t rtc_get_counter(void) { + rtc_counter_t counter; + +#if defined(_SAML21_) || defined(_SAML22_) || defined(_SAMD51_) + CTRLREG.bit.COUNTSYNC = 1; +#endif + _rtc_sync(); + counter = RTC->MODE0.COUNT.reg; + + return counter; +} + +void rtc_enable_compare_interrupt(uint32_t compare_time) { + RTC->MODE0.COMP[0].reg = compare_time; + _rtc_sync(); + RTC->MODE0.INTENSET.reg = RTC_MODE0_INTENSET_CMP0; + // NVIC_ClearPendingIRQ(RTC_IRQn); + // NVIC_EnableIRQ(RTC_IRQn); +} + +void rtc_configure_callback(rtc_cb_t callback) { + _rtc_callback = callback; +} + +void rtc_disable_compare_interrupt(void){ + RTC->MODE0.INTENCLR.reg = RTC_MODE0_INTENCLR_CMP0; + // NVIC_ClearPendingIRQ(RTC_IRQn); + // NVIC_DisableIRQ(RTC_IRQn); +} + +void irq_handler_rtc(void); + +void irq_handler_rtc(void) { + uint16_t int_cause = (uint16_t)RTC->MODE0.INTFLAG.reg; + RTC->MODE0.INTFLAG.reg = RTC_MODE0_INTFLAG_MASK; + (void)RTC->MODE0.INTFLAG.reg; + + /* Invoke registered Callback function */ + if (_rtc_callback != NULL) { + _rtc_callback(int_cause); + } + + // NVIC_ClearPendingIRQ(RTC_IRQn); +} diff --git a/watch-library/hardware/watch/rtc32.h b/watch-library/hardware/watch/rtc32.h new file mode 100644 index 00000000..52f20d68 --- /dev/null +++ b/watch-library/hardware/watch/rtc32.h @@ -0,0 +1,98 @@ +////< @file rtc32.h +/* + * MIT License + * + * Copyright (c) 2020 Joey Castillo + * 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 + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include +#include + +/** + * @addtogroup rtc Real-Time Clock + * @brief Functions for configuring and using the Real-Time Clock peripheral. + * @details This is the rtc implementation for MODE0 (counter32) + * @{ + */ + +#define RTC_REFERENCE_YEAR (2020) + +typedef union { + struct { + uint32_t second : 6; // 0-59 + uint32_t minute : 6; // 0-59 + uint32_t hour : 5; // 0-23 + uint32_t day : 5; // 1-31 + uint32_t month : 4; // 1-12 + uint32_t year : 6; // 0-63 (representing 2020-2083) + } unit; + uint32_t reg; // the bit-packed value as expected by the RTC peripheral's CLOCK register. +} rtc_date_time_t; + +typedef enum rtc_alarm_match_t { + ALARM_MATCH_DISABLED = 0, + ALARM_MATCH_SS, + ALARM_MATCH_MMSS, + ALARM_MATCH_HHMMSS, +} rtc_alarm_match_t; + +typedef uint32_t rtc_counter_t; + +typedef void (*rtc_cb_t)(uint16_t intflag); + +/** @brief Initializes the RTC. + * @details Configures the RTC for COUNT32 mode, with a 1 Hz + * tick derived from the 1024 Hz clock on GCLK3 (for SAM D devices) + * or OSC32KCTRL's most accurate 1024 Hz output (for SAM L devices). + */ +void rtc_init(void); + +/** @brief Enables the RTC. + */ +void rtc_enable(void); + +/** @brief Checks if the RTC is enabled. + * @return true if the RTC is enabled; false if not. + */ +bool rtc_is_enabled(void); + +/** @brief Set the value of the counter register. + */ +void rtc_set_counter(rtc_counter_t counter); + +/** @brief Returns the value of the counter register. + */ +rtc_counter_t rtc_get_counter(void); + +/** @brief Configures the RTC alarm callback. + * @param callback The function to call when an RTC interrupt occurs. The callback + * will be passed a bitmask of the interrupt flags, the full contents + * of the RTC peripheral's INTFLAG register. + */ +void rtc_configure_callback(rtc_cb_t callback); + +void rtc_enable_compare_interrupt(uint32_t compare_time); +void rtc_disable_compare_interrupt(void); + +/** @} */ diff --git a/watch-library/hardware/watch/watch_rtc.c b/watch-library/hardware/watch/watch_rtc.c index babae30d..61bc5d1f 100644 --- a/watch-library/hardware/watch/watch_rtc.c +++ b/watch-library/hardware/watch/watch_rtc.c @@ -2,6 +2,7 @@ * MIT License * * Copyright (c) 2020 Joey Castillo + * 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 @@ -23,11 +24,32 @@ */ #include +#include #include "watch_rtc.h" #include "watch_private.h" +#include "watch_utility.h" + +static const uint32_t RTC_OSC_DIV = 10; +static const uint32_t RTC_OSC_HZ = 1 << RTC_OSC_DIV; // 2^10 = 1024 +static const uint32_t RTC_PRESCALER_DIV = 3; +static const uint32_t RTC_CNT_HZ = RTC_OSC_HZ >> RTC_PRESCALER_DIV; // 1024 / 2^3 = 128 +static const uint32_t RTC_CNT_DIV = RTC_OSC_DIV - RTC_PRESCALER_DIV; // 7 +static const uint32_t RTC_CNT_TICKS_PER_MINUTE = RTC_CNT_HZ * 60; +static const uint32_t RTC_CNT_TICKS_PER_HOUR = RTC_CNT_TICKS_PER_MINUTE * 60; + +static const int TB_BKUP_REG = 7; + +#define WATCH_RTC_N_COMP_CB 8 + +typedef struct { + volatile uint32_t counter; + volatile watch_cb_t callback; + volatile bool enabled; +} comp_cb_t; watch_cb_t tick_callbacks[8]; +comp_cb_t comp_callbacks[WATCH_RTC_N_COMP_CB]; watch_cb_t alarm_callback; watch_cb_t btn_alarm_callback; watch_cb_t a2_callback; @@ -46,14 +68,49 @@ void _watch_rtc_init(void) { #endif rtc_enable(); rtc_configure_callback(watch_rtc_callback); + + for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) { + comp_callbacks[index].counter = 0; + comp_callbacks[index].callback = NULL; + comp_callbacks[index].enabled = false; + } + + NVIC_ClearPendingIRQ(RTC_IRQn); + NVIC_EnableIRQ(RTC_IRQn); } void watch_rtc_set_date_time(rtc_date_time_t date_time) { - rtc_set_date_time(date_time); + watch_rtc_set_unix_time(watch_utility_date_time_to_unix_time(date_time, 0)); } rtc_date_time_t watch_rtc_get_date_time(void) { - return rtc_get_date_time(); + return watch_utility_date_time_from_unix_time(watch_rtc_get_unix_time(), 0); +} + +void watch_rtc_set_unix_time(unix_timestamp_t unix_time) { + // time_backup + counter / RTC_CNT_HZ = unix_time + rtc_counter_t counter = rtc_get_counter(); + unix_timestamp_t tb = unix_time - (counter >> RTC_CNT_DIV); + watch_store_backup_data(tb, TB_BKUP_REG); +} + +unix_timestamp_t watch_rtc_get_unix_time(void) { + // time_backup + counter / RTC_CNT_HZ = unix_time + rtc_counter_t counter = rtc_get_counter(); + unix_timestamp_t tb = watch_get_backup_data(TB_BKUP_REG); + return tb + (counter >> RTC_CNT_DIV); +} + +rtc_counter_t watch_rtc_get_counter(void) { + return rtc_get_counter(); +} + +uint32_t watch_rtc_get_frequency(void) { + return RTC_CNT_HZ; +} + +uint32_t watch_rtc_get_ticks_per_minute(void) { + return RTC_CNT_TICKS_PER_MINUTE; } rtc_date_time_t watch_get_init_date_time(void) { @@ -103,57 +160,98 @@ void watch_rtc_register_periodic_callback(watch_cb_t callback, uint8_t frequency // this also maps nicely to an index for our list of tick callbacks. tick_callbacks[per_n] = callback; - NVIC_ClearPendingIRQ(RTC_IRQn); - NVIC_EnableIRQ(RTC_IRQn); - RTC->MODE2.INTENSET.reg = 1 << per_n; + // NVIC_ClearPendingIRQ(RTC_IRQn); + // NVIC_EnableIRQ(RTC_IRQn); + RTC->MODE0.INTENSET.reg = 1 << per_n; } void watch_rtc_disable_periodic_callback(uint8_t frequency) { if (__builtin_popcount(frequency) != 1) return; uint8_t per_n = __builtin_clz((frequency & 0xFF) << 24); - RTC->MODE2.INTENCLR.reg = 1 << per_n; + RTC->MODE0.INTENCLR.reg = 1 << per_n; } void watch_rtc_disable_matching_periodic_callbacks(uint8_t mask) { - RTC->MODE2.INTENCLR.reg = mask; + RTC->MODE0.INTENCLR.reg = mask; } void watch_rtc_disable_all_periodic_callbacks(void) { watch_rtc_disable_matching_periodic_callbacks(0xFF); } -void watch_rtc_register_alarm_callback(watch_cb_t callback, rtc_date_time_t alarm_time, rtc_alarm_match_t mask) { - RTC->MODE2.Mode2Alarm[0].ALARM.reg = alarm_time.reg; - RTC->MODE2.Mode2Alarm[0].MASK.reg = mask; - RTC->MODE2.INTENSET.reg = RTC_MODE2_INTENSET_ALARM0; - alarm_callback = callback; - NVIC_ClearPendingIRQ(RTC_IRQn); - NVIC_EnableIRQ(RTC_IRQn); - RTC->MODE2.INTENSET.reg = RTC_MODE2_INTENSET_ALARM0; +static void _watch_rtc_schedule_next_comp(void) { + rtc_disable_compare_interrupt(); + + // The soonest we can schedule is the next tick + rtc_counter_t curr_counter = watch_rtc_get_counter() + 1; + + bool schedule_any = false; + rtc_counter_t comp_counter; + rtc_counter_t min_diff = UINT_MAX; + + for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) { + if (comp_callbacks[index].enabled) { + rtc_counter_t diff = comp_callbacks[index].counter - curr_counter; + if (diff <= min_diff) { + min_diff = diff; + comp_counter = comp_callbacks[index].counter; + schedule_any = true; + } + } + } + + if (schedule_any) { + rtc_enable_compare_interrupt(comp_counter); + } } -void watch_rtc_disable_alarm_callback(void) { - RTC->MODE2.INTENCLR.reg = RTC_MODE2_INTENCLR_ALARM0; +void watch_rtc_register_comp_callback(watch_cb_t callback, rtc_counter_t counter, uint8_t index) { + if (index >= WATCH_RTC_N_COMP_CB) { + return; + } + + rtc_disable_compare_interrupt(); + + comp_callbacks[index].counter = counter; + comp_callbacks[index].callback = callback; + comp_callbacks[index].enabled = true; + + _watch_rtc_schedule_next_comp(); } -void watch_rtc_callback(uint16_t interrupt_status) { - uint16_t interrupt_enabled = RTC->MODE2.INTENSET.reg; +void watch_rtc_disable_comp_callback(uint8_t index) { + if (index >= WATCH_RTC_N_COMP_CB) { + return; + } - if ((interrupt_status & interrupt_enabled) & RTC_MODE2_INTFLAG_PER_Msk) { + rtc_disable_compare_interrupt(); + + comp_callbacks[index].enabled = false; + + _watch_rtc_schedule_next_comp(); +} + +void watch_rtc_callback(uint16_t interrupt_cause) { + // First read all relevant registers, to ensure no changes occurr during the callbacks + uint16_t interrupt_enabled = RTC->MODE0.INTENSET.reg; + rtc_counter_t comp_counter = RTC->MODE0.COMP[0].reg; + + + if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_PER_Msk) { // handle the tick callback first, it's what we do the most. // start from PER7, the 1 Hz tick. for(int8_t i = 7; i >= 0; i--) { - if ((interrupt_status & interrupt_enabled) & (1 << i)) { + if ((interrupt_cause & interrupt_enabled) & (1 << i)) { if (tick_callbacks[i] != NULL) { tick_callbacks[i](); } - RTC->MODE2.INTFLAG.reg = 1 << i; -// break; Uncertain if this fix is requried. We were discussing in discord. Might slightly increase power consumption. } } - } else if ((interrupt_status & interrupt_enabled) & RTC_MODE2_INTFLAG_TAMPER) { + } + + if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_TAMPER) { // handle the extwake interrupts next. - uint8_t reason = RTC->MODE2.TAMPID.reg; + uint8_t reason = RTC->MODE0.TAMPID.reg; if (reason & RTC_TAMPID_TAMPID2) { if (btn_alarm_callback != NULL) btn_alarm_callback(); } else if (reason & RTC_TAMPID_TAMPID1) { @@ -161,25 +259,37 @@ void watch_rtc_callback(uint16_t interrupt_status) { } else if (reason & RTC_TAMPID_TAMPID0) { if (a4_callback != NULL) a4_callback(); } - RTC->MODE2.TAMPID.reg = reason; - RTC->MODE2.INTFLAG.reg = RTC_MODE2_INTFLAG_TAMPER; - } else if ((interrupt_status & interrupt_enabled) & RTC_MODE2_INTFLAG_ALARM0) { - // finally handle the alarm. - if (alarm_callback != NULL) { - alarm_callback(); + RTC->MODE0.TAMPID.reg = reason; + } + + if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_CMP0) { + // The comp interrupt is generated one tick after the matched counter + // rtc_counter_t comp_counter = watch_rtc_get_counter() - 1; + + for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) { + if (comp_callbacks[index].enabled && comp_counter == comp_callbacks[index].counter) { + comp_callbacks[index].enabled = false; + comp_callbacks[index].callback(); + } } - RTC->MODE2.INTFLAG.reg = RTC_MODE2_INTFLAG_ALARM0; + _watch_rtc_schedule_next_comp(); + } + + if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_OVF) { + // Handle the overflow of the counter. All we need to do is reset the reference time. + unix_timestamp_t tb = watch_get_backup_data(TB_BKUP_REG); + watch_store_backup_data(tb + (UINT_MAX >> RTC_CNT_DIV), TB_BKUP_REG); } } void watch_rtc_enable(bool en) { // Writing it twice - as it's quite dangerous operation. // If write fails - we might hang with RTC off, which means no recovery possible - while (RTC->MODE2.SYNCBUSY.reg); - RTC->MODE2.CTRLA.bit.ENABLE = en ? 1 : 0; - while (RTC->MODE2.SYNCBUSY.reg); - RTC->MODE2.CTRLA.bit.ENABLE = en ? 1 : 0; - while (RTC->MODE2.SYNCBUSY.reg); + while (RTC->MODE0.SYNCBUSY.reg); + RTC->MODE0.CTRLA.bit.ENABLE = en ? 1 : 0; + while (RTC->MODE0.SYNCBUSY.reg); + RTC->MODE0.CTRLA.bit.ENABLE = en ? 1 : 0; + while (RTC->MODE0.SYNCBUSY.reg); } void watch_rtc_freqcorr_write(int16_t value, int16_t sign) { @@ -188,8 +298,7 @@ void watch_rtc_freqcorr_write(int16_t value, int16_t sign) { data.bit.VALUE = value; data.bit.SIGN = sign; - RTC->MODE2.FREQCORR.reg = data.reg; // Setting correction in single write operation + RTC->MODE0.FREQCORR.reg = data.reg; // Setting correction in single write operation // We do not sycnronize. We are not in a hurry } - diff --git a/watch-library/hardware/watch/watch_tcc.c b/watch-library/hardware/watch/watch_tcc.c index b5d50f94..06cee5d5 100644 --- a/watch-library/hardware/watch/watch_tcc.c +++ b/watch-library/hardware/watch/watch_tcc.c @@ -32,9 +32,13 @@ void cb_watch_buzzer_seq(void); static uint16_t _seq_position; static int8_t _tone_ticks, _repeat_counter; -static bool _callback_running = false; static int8_t *_sequence; +static uint8_t _volume; static void (*_cb_finished)(void); +static watch_cb_t _cb_start_global = NULL; +static watch_cb_t _cb_stop_global = NULL; +static volatile bool _led_is_active = false; +static volatile bool _buzzer_is_active = false; static void _tcc_write_RUNSTDBY(bool value) { // enables or disables RUNSTDBY of the tcc @@ -46,13 +50,11 @@ static void _tcc_write_RUNSTDBY(bool value) { static inline void _tc0_start() { // start the TC0 timer tc_enable(0); - _callback_running = true; } static inline void _tc0_stop() { // stop the TC0 timer tc_disable(0); - _callback_running = false; } static void _tc0_initialize() { @@ -68,19 +70,32 @@ static void _tc0_initialize() { } void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void)) { - if (_callback_running) _tc0_stop(); + watch_buzzer_play_sequence_with_volume(note_sequence, callback_on_end, WATCH_BUZZER_VOLUME_LOUD); +} + +void watch_buzzer_play_sequence_with_volume(int8_t *note_sequence, void (*callback_on_end)(void), watch_buzzer_volume_t volume) { + // Abort any previous sequence + watch_buzzer_abort_sequence(); + + _buzzer_is_active = true; + + if (_cb_start_global) { + _cb_start_global(); + } + + watch_enable_buzzer_and_leds(); + watch_set_buzzer_off(); _sequence = note_sequence; _cb_finished = callback_on_end; + _volume = volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25; _seq_position = 0; _tone_ticks = 0; _repeat_counter = -1; // prepare buzzer - watch_enable_buzzer(); + // setup TC0 timer _tc0_initialize(); - // TCC should run in standby mode - _tcc_write_RUNSTDBY(true); // start the timer (for the 64 hz callback) _tc0_start(); } @@ -110,7 +125,7 @@ void cb_watch_buzzer_seq(void) { // read note watch_buzzer_note_t note = _sequence[_seq_position]; if (note != BUZZER_NOTE_REST) { - watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], 25); + watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], _volume); watch_set_buzzer_on(); } else watch_set_buzzer_off(); // set duration ticks and move to next tone @@ -119,17 +134,37 @@ void cb_watch_buzzer_seq(void) { } else { // end the sequence watch_buzzer_abort_sequence(); - if (_cb_finished) _cb_finished(); } } else _tone_ticks--; } void watch_buzzer_abort_sequence(void) { // ends/aborts the sequence - if (_callback_running) _tc0_stop(); + if (!_buzzer_is_active) { + return; + } + + _buzzer_is_active = false; + + _tc0_stop(); + watch_set_buzzer_off(); - // disable standby mode for TCC - _tcc_write_RUNSTDBY(false); + + // disable TCC + watch_maybe_disable_buzzer_and_leds(); + + if (_cb_stop_global) { + _cb_stop_global(); + } + + if (_cb_finished) { + _cb_finished(); + } +} + +void watch_buzzer_register_global_callbacks(watch_cb_t cb_start, watch_cb_t cb_stop) { + _cb_stop_global = cb_start; + _cb_stop_global = cb_stop; } void irq_handler_tc0(void) { @@ -142,19 +177,41 @@ bool watch_is_buzzer_or_led_enabled(void){ return tcc_is_enabled(0); } -inline void watch_enable_buzzer(void) { +void watch_enable_buzzer_and_leds(void) { if (!tcc_is_enabled(0)) { + // tcc_set_run_in_standby(0, true); _watch_enable_tcc(); + // TCC should run in standby mode + _tcc_write_RUNSTDBY(true); } } +void watch_disable_buzzer_and_leds(void) { + if (tcc_is_enabled(0)) { + _tcc_write_RUNSTDBY(false); + _watch_disable_tcc(); + } +} + +void watch_maybe_disable_buzzer_and_leds(void) { + if (_buzzer_is_active || _led_is_active) { + return; + } + + watch_disable_buzzer_and_leds(); +} + +void watch_enable_buzzer(void) { + watch_enable_buzzer_and_leds(); +} + void watch_set_buzzer_period_and_duty_cycle(uint32_t period, uint8_t duty) { tcc_set_period(0, period, true); tcc_set_cc(0, (WATCH_BUZZER_TCC_CHANNEL) % 4, period / (100 / duty), true); } void watch_disable_buzzer(void) { - _watch_disable_tcc(); + watch_maybe_disable_buzzer_and_leds(); } inline void watch_set_buzzer_on(void) { @@ -172,14 +229,17 @@ void watch_buzzer_play_note(watch_buzzer_note_t note, uint16_t duration_ms) { } void watch_buzzer_play_note_with_volume(watch_buzzer_note_t note, uint16_t duration_ms, watch_buzzer_volume_t volume) { - if (note == BUZZER_NOTE_REST) { - watch_set_buzzer_off(); - } else { - watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25); - watch_set_buzzer_on(); - } - delay_ms(duration_ms); - watch_set_buzzer_off(); + static int8_t single_note_sequence[3]; + + single_note_sequence[0] = note; + // 48 ticks per second for the tc0? + // Each tick is approximately 20ms + uint16_t duration = duration_ms / 20; + if (duration > 127) duration = 127; + single_note_sequence[1] = (int8_t)duration; + single_note_sequence[2] = 0; + + watch_buzzer_play_sequence_with_volume(single_note_sequence, NULL, volume); } void _watch_enable_tcc(void) { @@ -263,7 +323,7 @@ void watch_enable_leds(void) { } void watch_disable_leds(void) { - _watch_disable_tcc(); + watch_maybe_disable_buzzer_and_leds(); } void watch_set_led_color(uint8_t red, uint8_t green) { @@ -275,6 +335,15 @@ void watch_set_led_color(uint8_t red, uint8_t green) { } void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) { + bool turning_on = (red | green | blue) != 0; + + if (turning_on) { + _led_is_active = true; + watch_enable_buzzer_and_leds(); + } else { + _led_is_active = false; + } + if (tcc_is_enabled(0)) { uint32_t period = tcc_get_period(0); tcc_set_cc(0, (WATCH_RED_TCC_CHANNEL) % 4, ((period * (uint32_t)red * 1000ull) / 255000ull), true); @@ -289,6 +358,10 @@ void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) { (void) blue; // silence warning #endif } + + if (!turning_on) { + watch_maybe_disable_buzzer_and_leds(); + } } void watch_set_led_red(void) { diff --git a/watch-library/shared/watch/watch_rtc.h b/watch-library/shared/watch/watch_rtc.h index a51fc826..e268ad28 100644 --- a/watch-library/shared/watch/watch_rtc.h +++ b/watch-library/shared/watch/watch_rtc.h @@ -27,7 +27,7 @@ ////< @file watch_rtc.h #include "watch.h" -#include "rtc.h" +#include "rtc32.h" /** @addtogroup rtc Real-Time Clock * @brief This section covers functions related to the SAM L22's real-time clock peripheral, including @@ -42,17 +42,20 @@ extern watch_cb_t btn_alarm_callback; extern watch_cb_t a2_callback; extern watch_cb_t a4_callback; +extern watch_cb_t comp_callback; #define WATCH_RTC_REFERENCE_YEAR (2020) #define watch_date_time_t rtc_date_time_t +typedef rtc_counter_t watch_counter_t; +typedef uint32_t unix_timestamp_t; /** @brief Called by main.c to check if the RTC is enabled. * You may call this function, but outside of app_init, it should always return true. */ bool _watch_rtc_is_enabled(void); -/** @brief Sets the date and time. +/** @brief Sets the date and time. Calls watch_rtc_set_unix_time internally. * @param date_time The date and time you wish to set, with a year value from 0-63 representing 2020-2083. * @note The SAM L22 stores the year as six bits representing a value from 0 to 63. It treats this as a year * offset from a reference year, which must be a leap year. Since 2020 was a leap year, and it allows @@ -62,7 +65,7 @@ bool _watch_rtc_is_enabled(void); */ void watch_rtc_set_date_time(rtc_date_time_t date_time); -/** @brief Returns the date and time. +/** @brief Returns the date and time. Calls watch_rtc_get_unix_time internally. * @return A rtc_date_time_t with the current date and time, with a year value from 0-63 representing 2020-2083. * @see watch_rtc_set_date_time for notes about how the year is stored. */ @@ -73,26 +76,51 @@ rtc_date_time_t watch_rtc_get_date_time(void); */ rtc_date_time_t watch_get_init_date_time(void); -/** @brief Registers an alarm callback that will be called when the RTC time matches the target time, as masked - * by the provided mask. - * @param callback The function you wish to have called when the alarm fires. If this value is NULL, the alarm +/** @brief Set the current UTC date and time using a unix timestamp + */ +void watch_rtc_set_unix_time(unix_timestamp_t unix_time); + +/** @brief Get the current UTC date and time using a unix timestamp + */ +unix_timestamp_t watch_rtc_get_unix_time(void); + +/** @brief Get the current value of the internal hardware counter + * @details The counter starts at 0 and it increases at a 128Hz rate until it overflows and starts over. + * We never manually set the counter. Doing so allows us to calculate absolute elapsed and more. + * When the user sets the time, what is modified is the reference time (i.e. the date and time when + * the counter is 0). + */ +rtc_counter_t watch_rtc_get_counter(void); + +/** @brief Get the RTC counter frequency. + */ +uint32_t watch_rtc_get_frequency(void); + +/** @brief Get how many counter ticks are in one minute. + */ +uint32_t watch_rtc_get_ticks_per_minute(void); + +/** @brief Registers a callback that will be called when the RTC counter matches the target counter. + * @param callback The function you wish to have called when the target counter is reached. If this value is NULL, the comp * interrupt will still be enabled, but no callback function will be called. - * @param alarm_time The time that you wish to match. The date is currently ignored. - * @param mask One of the values in rtc_alarm_match_t indicating which values to check. - * @details The alarm interrupt is a versatile tool for scheduling events in the future, especially since it can - * wake the device from all sleep modes. The key to its versatility is the mask parameter. - * Suppose we set an alarm for midnight, 00:00:00. - * * if mask is ALARM_MATCH_SS, the alarm will fire every minute when the clock ticks to seconds == 0. - * * with ALARM_MATCH_MMSS, the alarm will once an hour, at the top of each hour. - * * with ALARM_MATCH_HHMMSS, the alarm will fire at midnight every day. - * In theory the SAM L22's alarm function can match on days, months and even years, but I have not had - * success with this yet; as such, I am omitting these options for now. + * @param counter The time that you wish to match. The date is currently ignored. + * @param index We can have up to 8 active callbacks at a time. This parameter specifies which of the 8 callbacks should be set. + * @details The hardware RTC provides us with single interrupt that fires when the RTC counter matches a target counter COMP0. + * With a little bit of logic, we can provide multiple active compare callbacks. Every time a comp callback is + * registered/disabled/fired we iterate over all the active comp callbacks and set the hardware COMP0 counter + * to the next occurring one. + * With this very simple API, movement can implement one-shot timers to turn off the led and determine button longpresses + * as well as the inactivity timeouts for resigning and sleeping, as well as emulating the top of the minute alarm. */ -void watch_rtc_register_alarm_callback(watch_cb_t callback, rtc_date_time_t alarm_time, rtc_alarm_match_t mask); +void watch_rtc_register_comp_callback(watch_cb_t callback, rtc_counter_t counter, uint8_t index); + +/** @brief Disables the specified comp callback. + */ +void watch_rtc_disable_comp_callback(uint8_t index); /** @brief Disables the alarm callback. */ -void watch_rtc_disable_alarm_callback(void); +// void watch_rtc_disable_alarm_callback(void); /** @brief Registers a "tick" callback that will be called once per second. * @param callback The function you wish to have called when the clock ticks. If you pass in NULL, the tick @@ -117,10 +145,6 @@ void watch_rtc_disable_tick_callback(void); * tick at 16 or 32 Hz to update the screen more quickly. Just remember that the more frequent the tick, the more * power your app will consume. Ideally you should enable the fast tick only when the user requires it (i.e. in * response to an input event), and move back to the slow tick after some time. - * - * Also note that the RTC peripheral does not have sub-second resolution, so even if you set a 2 or 4 Hz interval, - * the system will not have any way of telling you where you are within a given second; watch_rtc_get_date_time - * will return the exact same timestamp until the second ticks over. */ void watch_rtc_register_periodic_callback(watch_cb_t callback, uint8_t frequency); diff --git a/watch-library/shared/watch/watch_tcc.h b/watch-library/shared/watch/watch_tcc.h index aff88f7f..99c56dcc 100644 --- a/watch-library/shared/watch/watch_tcc.h +++ b/watch-library/shared/watch/watch_tcc.h @@ -170,8 +170,6 @@ void watch_set_buzzer_off(void); /** @brief Plays the given note for a set duration at the loudest possible volume. * @param note The note you wish to play, or BUZZER_NOTE_REST to disable output for the given duration. * @param duration_ms The duration of the note. - * @note Note that this will block your UI for the duration of the note's play time, and it will - * after this call, the buzzer period will be set to the period of this note. */ void watch_buzzer_play_note(watch_buzzer_note_t note, uint16_t duration_ms); @@ -179,8 +177,6 @@ void watch_buzzer_play_note(watch_buzzer_note_t note, uint16_t duration_ms); * @param note The note you wish to play, or BUZZER_NOTE_REST to disable output for the given duration. * @param duration_ms The duration of the note. * @param volume either WATCH_BUZZER_VOLUME_SOFT or WATCH_BUZZER_VOLUME_LOUD - * @note This will block your UI for the duration of the note's play time, and after this call, the - * buzzer will stop sounding, but the TCC period will remain set to the period of this note. */ void watch_buzzer_play_note_with_volume(watch_buzzer_note_t note, uint16_t duration_ms, watch_buzzer_volume_t volume); @@ -202,10 +198,36 @@ extern const uint16_t NotePeriods[108]; */ void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void)); +/** @brief Plays the given sequence of notes in a non-blocking way. + * @param note_sequence A pointer to the sequence of buzzer note & duration tuples, ending with a zero. A simple + * RLE logic is implemented: a negative number instead of a buzzer note means that the sequence + * is rewound by the given number of notes. The byte following a negative number determines the number + * of loops. I.e. if you want to repeat the last three notes of the sequence one time, you should provide + * the tuple -3, 1. The repeated notes must not contain any other repeat markers, or you will end up with + * an eternal loop. + * @param callback_on_end A pointer to a callback function to be invoked when the sequence has finished playing. + * @param volume either WATCH_BUZZER_VOLUME_SOFT or WATCH_BUZZER_VOLUME_LOUD + */ +void watch_buzzer_play_sequence_with_volume(int8_t *note_sequence, void (*callback_on_end)(void), watch_buzzer_volume_t volume); + /** @brief Aborts a playing sequence. */ void watch_buzzer_abort_sequence(void); +void watch_buzzer_register_global_callbacks(watch_cb_t cb_start, watch_cb_t cb_stop); + +/** @brief Enables the TCC peripheral, which drives the buzzer and the leds. +*/ +void watch_enable_buzzer_and_leds(void); + +/** @brief Disables the TCC peripheral that drives the buzzer and the leds. + */ +void watch_disable_buzzer_and_leds(void); + +/** @brief Disables the TCC peripheral that drives the buzzer and the leds if neither is currently active + */ +void watch_maybe_disable_buzzer_and_leds(void); + #ifndef __EMSCRIPTEN__ void irq_handler_tc0(void); #endif diff --git a/watch-library/shared/watch/watch_utility.c b/watch-library/shared/watch/watch_utility.c index 7179aa72..d592fe56 100644 --- a/watch-library/shared/watch/watch_utility.c +++ b/watch-library/shared/watch/watch_utility.c @@ -278,6 +278,10 @@ watch_date_time_t watch_utility_date_time_convert_zone(watch_date_time_t date_ti return watch_utility_date_time_from_unix_time(timestamp, destination_utc_offset); } +uint32_t watch_utility_unix_time_convert_zone(uint32_t timestamp, uint32_t origin_utc_offset, uint32_t destination_utc_offset) { + return timestamp - origin_utc_offset + destination_utc_offset; +} + watch_duration_t watch_utility_seconds_to_duration(uint32_t seconds) { watch_duration_t retval; diff --git a/watch-library/shared/watch/watch_utility.h b/watch-library/shared/watch/watch_utility.h index 61c292b4..9b1b3a04 100644 --- a/watch-library/shared/watch/watch_utility.h +++ b/watch-library/shared/watch/watch_utility.h @@ -144,6 +144,16 @@ bool watch_utility_convert_to_12_hour(watch_date_time_t *date_time); */ watch_date_time_t watch_utility_date_time_convert_zone(watch_date_time_t date_time, uint32_t origin_utc_offset, uint32_t destination_utc_offset); +/** @brief Converts a unix time from a given time zone to another time zone. + * @param timestamp The unix time that you wish to convert + * @param origin_utc_offset The number of seconds from UTC in the origin time zone + * @param destination_utc_offset The number of seconds from UTC in the destination time zone + * @return A unix time for the given UNIX timestamp and UTC offset. + * @note Adapted from MIT-licensed code from musl, Copyright © 2005-2014 Rich Felker, et al.: + * https://github.com/esmil/musl/blob/1cc81f5cb0df2b66a795ff0c26d7bbc4d16e13c6/src/time/__secs_to_tm.c + */ +uint32_t watch_utility_unix_time_convert_zone(uint32_t timestamp, uint32_t origin_utc_offset, uint32_t destination_utc_offset); + /** @brief Returns a temperature in degrees Celsius for a given thermistor voltage divider circuit. * @param value The raw analog reading from the thermistor pin (0-65535) * @param highside True if the thermistor is connected to VCC and the series resistor is connected diff --git a/watch-library/simulator/watch/watch_rtc.c b/watch-library/simulator/watch/watch_rtc.c index 40487dc6..77463f74 100644 --- a/watch-library/simulator/watch/watch_rtc.c +++ b/watch-library/simulator/watch/watch_rtc.c @@ -21,6 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ +#include #include "watch_rtc.h" #include "watch_main_loop.h" @@ -29,8 +30,28 @@ #include #include +static const uint32_t RTC_CNT_HZ = 128; +static const uint32_t RTC_CNT_DIV = 7; +static const uint32_t RTC_CNT_TICKS_PER_MINUTE = RTC_CNT_HZ * 60; +static const uint32_t RTC_CNT_TICKS_PER_HOUR = RTC_CNT_TICKS_PER_MINUTE * 60; + +static uint32_t counter_interval; +static uint32_t counter; +static uint32_t reference_timestamp; + +#define WATCH_RTC_N_COMP_CB 8 + +typedef struct { + volatile uint32_t counter; + volatile watch_cb_t callback; + volatile bool enabled; +} comp_cb_t; + static double time_offset = 0; -static long tick_callbacks[8] = { -1, -1, -1, -1, -1, -1, -1, -1 }; +watch_cb_t tick_callbacks[8]; +comp_cb_t comp_callbacks[WATCH_RTC_N_COMP_CB]; + +static uint32_t scheduled_comp_counter; static long alarm_interval_id = -1; static long alarm_timeout_id = -1; @@ -40,41 +61,73 @@ watch_cb_t btn_alarm_callback; watch_cb_t a2_callback; watch_cb_t a4_callback; +static void _watch_increase_counter(void *userData); +static void _watch_process_periodic_callbacks(void); +static void _watch_process_comp_callbacks(void); + bool _watch_rtc_is_enabled(void) { - return true; + return counter_interval; } void _watch_rtc_init(void) { -#if EMSCRIPTEN - // Shifts the timezone so our local time is converted to UTC and set + for (uint8_t index = 0; index < 8; ++index) { + tick_callbacks[index] = NULL; + } + + for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) { + comp_callbacks[index].counter = 0; + comp_callbacks[index].callback = NULL; + comp_callbacks[index].enabled = false; + } + + scheduled_comp_counter = 0; + counter = 0; + counter_interval = 0; + + watch_rtc_set_date_time(watch_get_init_date_time()); + watch_rtc_enable(true); +} + +void watch_rtc_set_date_time(rtc_date_time_t date_time) { + watch_rtc_set_unix_time(watch_utility_date_time_to_unix_time(date_time, 0)); +} + +rtc_date_time_t watch_rtc_get_date_time(void) { + return watch_utility_date_time_from_unix_time(watch_rtc_get_unix_time(), 0); +} + +void watch_rtc_set_unix_time(unix_timestamp_t unix_time) { + // time_backup + counter / RTC_CNT_HZ = unix_time + rtc_counter_t counter = watch_rtc_get_counter(); + reference_timestamp = unix_time - (counter >> RTC_CNT_DIV); +} + +unix_timestamp_t watch_rtc_get_unix_time(void) { + // time_backup + counter / RTC_CNT_HZ = unix_time + rtc_counter_t counter = watch_rtc_get_counter(); + return reference_timestamp + (counter >> RTC_CNT_DIV); +} + +rtc_counter_t watch_rtc_get_counter(void) { + return counter; +} + +uint32_t watch_rtc_get_frequency(void) { + return RTC_CNT_HZ; +} + +uint32_t watch_rtc_get_ticks_per_minute(void) { + return RTC_CNT_TICKS_PER_MINUTE; +} + +rtc_date_time_t watch_get_init_date_time(void) { + rtc_date_time_t date_time = {0}; + int32_t time_zone_offset = EM_ASM_INT({ - return -new Date().getTimezoneOffset() * 60; + return new Date().getTimezoneOffset() * 60 * 1000; // ms }); -#endif -#ifdef BUILD_YEAR - watch_date_time_t date_time = watch_get_init_date_time(); -#else - watch_date_time_t date_time = watch_rtc_get_date_time(); -#endif - watch_rtc_set_date_time(watch_utility_date_time_convert_zone(date_time, time_zone_offset, 0)); -} -void watch_rtc_set_date_time(watch_date_time_t date_time) { - time_offset = EM_ASM_DOUBLE({ - const year = 2020 + (($0 >> 26) & 0x3f); - const month = ($0 >> 22) & 0xf; - const day = ($0 >> 17) & 0x1f; - const hour = ($0 >> 12) & 0x1f; - const minute = ($0 >> 6) & 0x3f; - const second = $0 & 0x3f; - const date = new Date(year, month - 1, day, hour, minute, second); - return date - Date.now(); - }, date_time.reg); -} - -watch_date_time_t watch_rtc_get_date_time(void) { - watch_date_time_t retval; - retval.reg = EM_ASM_INT({ + date_time.reg = EM_ASM_INT({ const date = new Date(Date.now() + $0); return date.getSeconds() | (date.getMinutes() << 6) | @@ -82,27 +135,16 @@ watch_date_time_t watch_rtc_get_date_time(void) { (date.getDate() << 17) | ((date.getMonth() + 1) << 22) | ((date.getFullYear() - 2020) << 26); - }, time_offset); - return retval; -} - -rtc_date_time_t watch_get_init_date_time(void) { - rtc_date_time_t date_time = {0}; + }, time_zone_offset); #ifdef BUILD_YEAR date_time.unit.year = BUILD_YEAR; -#else - date_time.unit.year = 5; #endif #ifdef BUILD_MONTH date_time.unit.month = BUILD_MONTH; -#else - date_time.unit.month = 1; #endif #ifdef BUILD_DAY date_time.unit.day = BUILD_DAY; -#else - date_time.unit.day = 1; #endif #ifdef BUILD_HOUR date_time.unit.hour = BUILD_HOUR; @@ -122,12 +164,72 @@ void watch_rtc_disable_tick_callback(void) { watch_rtc_disable_periodic_callback(1); } -static void watch_invoke_periodic_callback(void *userData) { - watch_cb_t callback = userData; - callback(); +static void _watch_increase_counter(void *userData) { + (void) userData; + + counter += 1; + // Fire the periodic callbacks that match this counter + _watch_process_periodic_callbacks(); + // Fire the comp callbacks that match this counter + _watch_process_comp_callbacks(); + resume_main_loop(); } +static void _watch_process_periodic_callbacks(void) { + /* It looks weird but it follows the way the hardware triggers periodic interrupts. + * For 128hz counter periodic interrupts fire at these tick values: + * 1Hz: 64 + * 2Hz: 32, 96 + * 4Hz: 16, 48, 80, 112 + * 8Hz: 8, 24, 40, 56, 72, 88, 104, 120 + * 16Hz: 4, 12, 20, ..., 124 + * 32Hz: 2, 6, 10, ..., 126 + * 64Hz: 1, 3, 5, ..., 127 + * 128Hz: 0, 1, 2, ..., 127 + * + * Which means that only one periodic interrupt can fire for a given counter value + * (except 128Hz which can always fire) + */ + + uint32_t freq = watch_rtc_get_frequency(); + uint32_t subsecond_mask = freq - 1; + uint32_t subseconds = counter & subsecond_mask; + + // Find the firs non-zero bit in the counter, which can be used to determine the appropriate period (see table above). + uint8_t per_n = 0; + + for (uint8_t i = 0; i < 7; i++) { + if (subseconds & (1 << i)) { + per_n = i + 1; + break; + } + } + + if (tick_callbacks[per_n]) { + tick_callbacks[per_n](); + } + + // 128Hz is always a match + if (per_n != 0 && tick_callbacks[0]) { + tick_callbacks[0](); + } +} + +static void _watch_process_comp_callbacks(void) { + // In hardware the interrupt fires one tick after the matching counter + if (counter == (scheduled_comp_counter + 1)) { + for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) { + if (comp_callbacks[index].enabled && scheduled_comp_counter == comp_callbacks[index].counter) { + comp_callbacks[index].enabled = false; + comp_callbacks[index].callback(); + } + } + + _watch_rtc_schedule_next_comp(); + } +} + void watch_rtc_register_periodic_callback(watch_cb_t callback, uint8_t frequency) { // we told them, it has to be a power of 2. if (__builtin_popcount(frequency) != 1) return; @@ -138,26 +240,19 @@ void watch_rtc_register_periodic_callback(watch_cb_t callback, uint8_t frequency // 0x01 (1 Hz) will have 7 leading zeros for PER7. 0xF0 (128 Hz) will have no leading zeroes for PER0. uint8_t per_n = __builtin_clz(tmp); - double interval = 1000.0 / frequency; // in msec - - if (tick_callbacks[per_n] != -1) emscripten_clear_interval(tick_callbacks[per_n]); - tick_callbacks[per_n] = emscripten_set_interval(watch_invoke_periodic_callback, interval, (void *)callback); + tick_callbacks[per_n] = callback; } void watch_rtc_disable_periodic_callback(uint8_t frequency) { if (__builtin_popcount(frequency) != 1) return; uint8_t per_n = __builtin_clz((frequency & 0xFF) << 24); - if (tick_callbacks[per_n] != -1) { - emscripten_clear_interval(tick_callbacks[per_n]); - tick_callbacks[per_n] = -1; - } + tick_callbacks[per_n] = NULL; } void watch_rtc_disable_matching_periodic_callbacks(uint8_t mask) { for (int i = 0; i < 8; i++) { - if (tick_callbacks[i] != -1 && (mask & (1 << i)) != 0) { - emscripten_clear_interval(tick_callbacks[i]); - tick_callbacks[i] = -1; + if (tick_callbacks[i] && (mask & (1 << i)) != 0) { + tick_callbacks[i] = NULL; } } } @@ -166,81 +261,70 @@ void watch_rtc_disable_all_periodic_callbacks(void) { watch_rtc_disable_matching_periodic_callbacks(0xFF); } -static void watch_invoke_alarm_interval_callback(void *userData) { - if (alarm_callback) alarm_callback(); -} - -static void watch_invoke_alarm_callback(void *userData) { - if (alarm_callback) alarm_callback(); - alarm_interval_id = emscripten_set_interval(watch_invoke_alarm_interval_callback, alarm_interval, NULL); -} - -void watch_rtc_register_alarm_callback(watch_cb_t callback, watch_date_time_t alarm_time, rtc_alarm_match_t mask) { - watch_rtc_disable_alarm_callback(); - - switch (mask) { - case ALARM_MATCH_DISABLED: - return; - case ALARM_MATCH_SS: - alarm_interval = 60 * 1000; - break; - case ALARM_MATCH_MMSS: - alarm_interval = 60 * 60 * 1000; - break; - case ALARM_MATCH_HHMMSS: - alarm_interval = 60 * 60 * 60 * 1000; - break; +void watch_rtc_register_comp_callback(watch_cb_t callback, rtc_counter_t counter, uint8_t index) { + if (index >= WATCH_RTC_N_COMP_CB) { + return; } - double timeout = EM_ASM_DOUBLE({ - const now = Date.now(); - const date = new Date(now); + comp_callbacks[index].counter = counter; + comp_callbacks[index].callback = callback; + comp_callbacks[index].enabled = true; - const hour = ($0 >> 12) & 0x1f; - const minute = ($0 >> 6) & 0x3f; - const second = $0 & 0x3f; + _watch_rtc_schedule_next_comp(); +} - if ($1 == 1) { // SS - if (second < date.getSeconds()) date.setMinutes(date.getMinutes() + 1); - date.setSeconds(second); - } else if ($1 == 2) { // MMSS - if (second < date.getSeconds()) date.setMinutes(date.getMinutes() + 1); - if (minute < date.getMinutes()) date.setHours(date.getHours() + 1); - date.setMinutes(minute, second); - } else if ($1 == 3) { // HHMMSS - if (second < date.getSeconds()) date.setMinutes(date.getMinutes() + 1); - if (minute < date.getMinutes()) date.setHours(date.getHours() + 1); - if (hour < date.getHours()) date.setDate(date.getDate() + 1); - date.setHours(hour, minute, second); - } else { - throw 'Invalid alarm match mask'; +void watch_rtc_disable_comp_callback(uint8_t index) { + if (index >= WATCH_RTC_N_COMP_CB) { + return; + } + + comp_callbacks[index].enabled = false; + + _watch_rtc_schedule_next_comp(); +} + +void _watch_rtc_schedule_next_comp(void) { + scheduled_comp_counter = 0; + + // The soonest we can schedule is the next tick + rtc_counter_t curr_counter = watch_rtc_get_counter() + 1; + + bool schedule_any = false; + rtc_counter_t comp_counter; + rtc_counter_t min_diff = UINT_MAX; + + for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) { + // rtc_counter_t diff = + if (comp_callbacks[index].enabled) { + rtc_counter_t diff = comp_callbacks[index].counter - curr_counter; + if (diff <= min_diff) { + min_diff = diff; + comp_counter = comp_callbacks[index].counter; + schedule_any = true; + } } - - return date - now; - }, alarm_time.reg, mask); - - alarm_callback = callback; - alarm_timeout_id = emscripten_set_timeout(watch_invoke_alarm_callback, timeout, NULL); -} - -void watch_rtc_disable_alarm_callback(void) { - alarm_callback = NULL; - alarm_interval = 0; - - if (alarm_timeout_id != -1) { - emscripten_clear_timeout(alarm_timeout_id); - alarm_timeout_id = -1; } - if (alarm_interval_id != -1) { - emscripten_clear_interval(alarm_interval_id); - alarm_interval_id = -1; + if (schedule_any) { + scheduled_comp_counter = comp_counter; } } void watch_rtc_enable(bool en) { - //Not simulated + // Nothing to do cases + if ((en && counter_interval) || (!en && !counter_interval)) { + return; + } + + if (en) { + // Very bad way to keep time, but okay way to emulates the hardware. + double ms = 1000.0 / (double)RTC_CNT_HZ; // in msec + counter_interval = emscripten_set_interval(_watch_increase_counter, ms, NULL); + } else { + emscripten_clear_interval(counter_interval); + counter_interval = 0; + } } void watch_rtc_freqcorr_write(int16_t value, int16_t sign) diff --git a/watch-library/simulator/watch/watch_tcc.c b/watch-library/simulator/watch/watch_tcc.c index 5466eebb..5e5dd013 100644 --- a/watch-library/simulator/watch/watch_tcc.c +++ b/watch-library/simulator/watch/watch_tcc.c @@ -28,16 +28,20 @@ #include #include -static bool buzzer_enabled = false; +static volatile bool buzzer_enabled = false; static uint32_t buzzer_period; void cb_watch_buzzer_seq(void *userData); static uint16_t _seq_position; static int8_t _tone_ticks, _repeat_counter; -static long _em_interval_id = 0; +static volatile long _em_interval_id = 0; static int8_t *_sequence; +static uint8_t _volume; static void (*_cb_finished)(void); +static watch_cb_t _cb_start_global = NULL; +static watch_cb_t _cb_stop_global = NULL; +static volatile bool _buzzer_is_active = false; void _watch_enable_tcc(void) {} @@ -47,15 +51,27 @@ static inline void _em_interval_stop() { } void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void)) { - if (_em_interval_id) _em_interval_stop(); - watch_set_buzzer_off(); + watch_buzzer_play_sequence_with_volume(note_sequence, callback_on_end, WATCH_BUZZER_VOLUME_LOUD); +} + +void watch_buzzer_play_sequence_with_volume(int8_t *note_sequence, void (*callback_on_end)(void), watch_buzzer_volume_t volume) { + watch_buzzer_abort_sequence(); + + _buzzer_is_active = true; + + if (_cb_start_global) { + _cb_start_global(); + } + _sequence = note_sequence; _cb_finished = callback_on_end; + _volume = volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25; _seq_position = 0; _tone_ticks = 0; _repeat_counter = -1; // prepare buzzer watch_enable_buzzer(); + watch_set_buzzer_off(); // initiate 64 hz callback _em_interval_id = emscripten_set_interval(cb_watch_buzzer_seq, (double)(1000/64), (void *)NULL); } @@ -88,7 +104,7 @@ void cb_watch_buzzer_seq(void *userData) { if (note == BUZZER_NOTE_REST) { watch_set_buzzer_off(); } else { - watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], 25); + watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], _volume); watch_set_buzzer_on(); } // set duration ticks and move to next tone @@ -97,7 +113,6 @@ void cb_watch_buzzer_seq(void *userData) { } else { // end the sequence watch_buzzer_abort_sequence(); - if (_cb_finished) _cb_finished(); } } else _tone_ticks--; } @@ -105,10 +120,32 @@ void cb_watch_buzzer_seq(void *userData) { void watch_buzzer_abort_sequence(void) { // ends/aborts the sequence if (_em_interval_id) _em_interval_stop(); + watch_set_buzzer_off(); + watch_disable_buzzer(); + + if (!_buzzer_is_active) { + return; + } + + _buzzer_is_active = false; + + if (_cb_stop_global) { + _cb_stop_global(); + } + + if (_cb_finished) { + _cb_finished(); + } +} + +void watch_buzzer_register_global_callbacks(watch_cb_t cb_start, watch_cb_t cb_stop) { + _cb_stop_global = cb_start; + _cb_stop_global = cb_stop; } void watch_enable_buzzer(void) { + watch_buzzer_abort_sequence(); buzzer_enabled = true; buzzer_period = NotePeriods[BUZZER_NOTE_A4]; @@ -175,15 +212,17 @@ void watch_buzzer_play_note(watch_buzzer_note_t note, uint16_t duration_ms) { } void watch_buzzer_play_note_with_volume(watch_buzzer_note_t note, uint16_t duration_ms, watch_buzzer_volume_t volume) { - if (note == BUZZER_NOTE_REST) { - watch_set_buzzer_off(); - } else { - watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25); - watch_set_buzzer_on(); - } + static int8_t single_note_sequence[3]; - main_loop_sleep(duration_ms); - watch_set_buzzer_off(); + single_note_sequence[0] = note; + // 64 ticks per second for the tc0? + // Each tick is approximately 15ms + uint16_t duration = duration_ms / 15; + if (duration > 127) duration = 127; + single_note_sequence[1] = (int8_t)duration; + single_note_sequence[2] = 0; + + watch_buzzer_play_sequence_with_volume(single_note_sequence, NULL, volume); } void watch_enable_leds(void) {} From e942f6768517085849daf4981d62c2c0ddcc9e9b Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Sat, 26 Jul 2025 21:42:31 -0400 Subject: [PATCH 02/53] Minor fixes to settings faces to work with the new rtc mode --- watch-faces/settings/finetune_face.c | 4 ++-- watch-faces/settings/set_time_face.c | 6 +---- watch-faces/settings/settings_face.c | 4 ++-- .../hardware/watch/watch_deepsleep.c | 22 +++++++++---------- .../{hardware => shared}/watch/rtc32.h | 0 watch-library/simulator/watch/watch_rtc.c | 3 ++- 6 files changed, 18 insertions(+), 21 deletions(-) rename watch-library/{hardware => shared}/watch/rtc32.h (100%) diff --git a/watch-faces/settings/finetune_face.c b/watch-faces/settings/finetune_face.c index 7567f9fc..4c77c4ea 100644 --- a/watch-faces/settings/finetune_face.c +++ b/watch-faces/settings/finetune_face.c @@ -106,7 +106,7 @@ static void finetune_adjust_subseconds(int delta) { watch_rtc_enable(false); delay_ms(delta); if (delta > 500) { - watch_date_time_t date_time = watch_rtc_get_date_time(); + watch_date_time_t date_time = movement_get_utc_date_time(); date_time.unit.second = (date_time.unit.second + 1) % 60; if (date_time.unit.second == 0) { // Overflow date_time.unit.minute = (date_time.unit.minute + 1) % 60; @@ -116,7 +116,7 @@ static void finetune_adjust_subseconds(int delta) { date_time.unit.day++; } } - watch_rtc_set_date_time(date_time); + movement_set_utc_date_time(date_time); } watch_rtc_enable(true); } diff --git a/watch-faces/settings/set_time_face.c b/watch-faces/settings/set_time_face.c index e273e61d..f52c7179 100644 --- a/watch-faces/settings/set_time_face.c +++ b/watch-faces/settings/set_time_face.c @@ -108,10 +108,6 @@ bool set_time_face_loop(movement_event_t event, void *context) { case EVENT_ALARM_LONG_UP: _abort_quick_ticks(); break; - case EVENT_MODE_BUTTON_UP: - _abort_quick_ticks(); - movement_move_to_next_face(); - return false; case EVENT_LIGHT_BUTTON_DOWN: current_page = (current_page + 1) % SET_TIME_FACE_NUM_SETTINGS; *((uint8_t *)context) = current_page; @@ -187,6 +183,6 @@ bool set_time_face_loop(movement_event_t event, void *context) { void set_time_face_resign(void *context) { (void) context; - watch_set_led_off(); movement_store_settings(); + movement_request_tick_frequency(1); } diff --git a/watch-faces/settings/settings_face.c b/watch-faces/settings/settings_face.c index 0894ebe5..44d70f1b 100644 --- a/watch-faces/settings/settings_face.c +++ b/watch-faces/settings/settings_face.c @@ -322,7 +322,7 @@ bool settings_face_loop(movement_event_t event, void *context) { case EVENT_MODE_BUTTON_UP: movement_force_led_off(); movement_move_to_next_face(); - return false; + return true; case EVENT_ALARM_BUTTON_UP: state->settings_screens[state->current_page].advance(); break; @@ -339,7 +339,7 @@ bool settings_face_loop(movement_event_t event, void *context) { movement_force_led_on(color.red | color.red << 4, color.green | color.green << 4, color.blue | color.blue << 4); - return false; + return true; } else { movement_force_led_off(); return true; diff --git a/watch-library/hardware/watch/watch_deepsleep.c b/watch-library/hardware/watch/watch_deepsleep.c index a0abe5eb..e90150ee 100644 --- a/watch-library/hardware/watch/watch_deepsleep.c +++ b/watch-library/hardware/watch/watch_deepsleep.c @@ -41,7 +41,7 @@ void sleep(const uint8_t mode) { } void watch_register_extwake_callback(uint8_t pin, watch_cb_t callback, bool level) { - uint32_t config = RTC->MODE2.TAMPCTRL.reg; + uint32_t config = RTC->MODE0.TAMPCTRL.reg; if (pin == HAL_GPIO_BTN_ALARM_pin()) { HAL_GPIO_BTN_ALARM_in(); @@ -71,22 +71,22 @@ void watch_register_extwake_callback(uint8_t pin, watch_cb_t callback, bool leve } // disable the RTC - RTC->MODE2.CTRLA.bit.ENABLE = 0; - while (RTC->MODE2.SYNCBUSY.bit.ENABLE); // wait for RTC to be disabled + RTC->MODE0.CTRLA.bit.ENABLE = 0; + while (RTC->MODE0.SYNCBUSY.bit.ENABLE); // wait for RTC to be disabled // update the configuration - RTC->MODE2.TAMPCTRL.reg = config; + RTC->MODE0.TAMPCTRL.reg = config; // re-enable the RTC - RTC->MODE2.CTRLA.bit.ENABLE = 1; + RTC->MODE0.CTRLA.bit.ENABLE = 1; NVIC_ClearPendingIRQ(RTC_IRQn); NVIC_EnableIRQ(RTC_IRQn); - RTC->MODE2.INTENSET.reg = RTC_MODE2_INTENSET_TAMPER; + RTC->MODE0.INTENSET.reg = RTC_MODE0_INTENSET_TAMPER; } void watch_disable_extwake_interrupt(uint8_t pin) { - uint32_t config = RTC->MODE2.TAMPCTRL.reg; + uint32_t config = RTC->MODE0.TAMPCTRL.reg; if (pin == HAL_GPIO_BTN_ALARM_pin()) { btn_alarm_callback = NULL; @@ -101,14 +101,14 @@ void watch_disable_extwake_interrupt(uint8_t pin) { } // disable the RTC - RTC->MODE2.CTRLA.bit.ENABLE = 0; - while (RTC->MODE2.SYNCBUSY.bit.ENABLE); // wait for RTC to be disabled + RTC->MODE0.CTRLA.bit.ENABLE = 0; + while (RTC->MODE0.SYNCBUSY.bit.ENABLE); // wait for RTC to be disabled // update the configuration - RTC->MODE2.TAMPCTRL.reg = config; + RTC->MODE0.TAMPCTRL.reg = config; // re-enable the RTC - RTC->MODE2.CTRLA.bit.ENABLE = 1; + RTC->MODE0.CTRLA.bit.ENABLE = 1; } void watch_store_backup_data(uint32_t data, uint8_t reg) { diff --git a/watch-library/hardware/watch/rtc32.h b/watch-library/shared/watch/rtc32.h similarity index 100% rename from watch-library/hardware/watch/rtc32.h rename to watch-library/shared/watch/rtc32.h diff --git a/watch-library/simulator/watch/watch_rtc.c b/watch-library/simulator/watch/watch_rtc.c index 77463f74..ee410c44 100644 --- a/watch-library/simulator/watch/watch_rtc.c +++ b/watch-library/simulator/watch/watch_rtc.c @@ -64,6 +64,7 @@ watch_cb_t a4_callback; static void _watch_increase_counter(void *userData); static void _watch_process_periodic_callbacks(void); static void _watch_process_comp_callbacks(void); +static void _watch_rtc_schedule_next_comp(void); bool _watch_rtc_is_enabled(void) { return counter_interval; @@ -283,7 +284,7 @@ void watch_rtc_disable_comp_callback(uint8_t index) { _watch_rtc_schedule_next_comp(); } -void _watch_rtc_schedule_next_comp(void) { +static void _watch_rtc_schedule_next_comp(void) { scheduled_comp_counter = 0; // The soonest we can schedule is the next tick From 6e23421df4c676b240012c88f5f1ae435bdbc6a3 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Tue, 5 Aug 2025 23:39:47 -0400 Subject: [PATCH 03/53] Fix a bug that was causing 10x power usage from boot until the first sound played --- movement.c | 3 --- watch-library/hardware/watch/watch_tcc.c | 4 +--- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/movement.c b/movement.c index adc01871..186aaeb5 100644 --- a/movement.c +++ b/movement.c @@ -960,9 +960,6 @@ void app_setup(void) { } #endif - watch_enable_buzzer(); - watch_enable_leds(); - movement_request_tick_frequency(1); for(uint8_t i = 0; i < MOVEMENT_NUM_FACES; i++) { diff --git a/watch-library/hardware/watch/watch_tcc.c b/watch-library/hardware/watch/watch_tcc.c index 06cee5d5..0e1fdd9d 100644 --- a/watch-library/hardware/watch/watch_tcc.c +++ b/watch-library/hardware/watch/watch_tcc.c @@ -317,9 +317,7 @@ void _watch_disable_tcc(void) { } void watch_enable_leds(void) { - if (!tcc_is_enabled(0)) { - _watch_enable_tcc(); - } + watch_enable_buzzer_and_leds(); } void watch_disable_leds(void) { From e2d13e076eb4f0e3893be2d5a95a74803223696d Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Sat, 2 Aug 2025 23:06:45 -0400 Subject: [PATCH 04/53] Optimize finetune and nanosec faces to work with rtc-counter32 --- movement.c | 21 +++++++++++++++------ movement.h | 2 ++ watch-faces/settings/finetune_face.c | 23 +++++++---------------- watch-faces/settings/finetune_face.h | 5 ----- watch-faces/settings/nanosec_face.c | 10 ++++------ 5 files changed, 28 insertions(+), 33 deletions(-) diff --git a/movement.c b/movement.c index 186aaeb5..70bd63b7 100644 --- a/movement.c +++ b/movement.c @@ -535,8 +535,22 @@ watch_date_time_t movement_get_local_date_time(void) { return watch_utility_date_time_from_unix_time(timestamp, movement_get_current_timezone_offset()); } +uint32_t movement_get_utc_timestamp(void) { + return watch_rtc_get_unix_time(); +} + void movement_set_utc_date_time(watch_date_time_t date_time) { - watch_rtc_set_date_time(date_time); + movement_set_utc_timestamp(watch_utility_date_time_to_unix_time(date_time, 0)); +} + +void movement_set_local_date_time(watch_date_time_t date_time) { + int32_t current_offset = movement_get_current_timezone_offset(); + watch_date_time_t utc_date_time = watch_utility_date_time_convert_zone(date_time, current_offset, 0); + movement_set_utc_date_time(utc_date_time); +} + +void movement_set_utc_timestamp(uint32_t timestamp) { + watch_rtc_set_unix_time(timestamp); // If the time was changed, the top of the minute alarm needs to be reset accordingly _movement_set_top_of_minute_alarm(); @@ -547,11 +561,6 @@ void movement_set_utc_date_time(watch_date_time_t date_time) { _movement_update_dst_offset_cache(); } -void movement_set_local_date_time(watch_date_time_t date_time) { - int32_t current_offset = movement_get_current_timezone_offset(); - watch_date_time_t utc_date_time = watch_utility_date_time_convert_zone(date_time, current_offset, 0); - movement_set_utc_date_time(utc_date_time); -} bool movement_button_should_sound(void) { return movement_state.settings.bit.button_should_sound; diff --git a/movement.h b/movement.h index 0288c9c9..c1ac756f 100644 --- a/movement.h +++ b/movement.h @@ -331,9 +331,11 @@ void movement_set_timezone_index(uint8_t value); watch_date_time_t movement_get_utc_date_time(void); watch_date_time_t movement_get_local_date_time(void); watch_date_time_t movement_get_date_time_in_zone(uint8_t zone_index); +uint32_t movement_get_utc_timestamp(void); void movement_set_utc_date_time(watch_date_time_t date_time); void movement_set_local_date_time(watch_date_time_t date_time); +void movement_set_utc_timestamp(uint32_t timestamp); bool movement_button_should_sound(void); void movement_set_button_should_sound(bool value); diff --git a/watch-faces/settings/finetune_face.c b/watch-faces/settings/finetune_face.c index 4c77c4ea..fe75d8fa 100644 --- a/watch-faces/settings/finetune_face.c +++ b/watch-faces/settings/finetune_face.c @@ -27,7 +27,6 @@ #include #include "finetune_face.h" #include "nanosec_face.h" -#include "watch_utility.h" #include "delay.h" extern nanosec_state_t nanosec_state; @@ -51,7 +50,7 @@ void finetune_face_activate(void *context) { } static float finetune_get_hours_passed(void) { - uint32_t current_time = watch_utility_date_time_to_unix_time(watch_rtc_get_date_time(), 0); + uint32_t current_time = movement_get_utc_timestamp(); return (current_time - nanosec_state.last_correction_time) / 3600.0f; } @@ -64,7 +63,7 @@ static void finetune_update_display(void) { if (finetune_page == 0) { watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "FTU", "FT"); - watch_date_time_t date_time = watch_rtc_get_date_time(); + watch_date_time_t date_time = movement_get_utc_date_time(); sprintf(buf, "%04d%02d", abs(total_adjustment), date_time.unit.second); watch_display_text(WATCH_POSITION_BOTTOM, buf); @@ -106,17 +105,9 @@ static void finetune_adjust_subseconds(int delta) { watch_rtc_enable(false); delay_ms(delta); if (delta > 500) { - watch_date_time_t date_time = movement_get_utc_date_time(); - date_time.unit.second = (date_time.unit.second + 1) % 60; - if (date_time.unit.second == 0) { // Overflow - date_time.unit.minute = (date_time.unit.minute + 1) % 60; - if (date_time.unit.minute == 0) { // Overflow - date_time.unit.hour = (date_time.unit.hour + 1) % 24; - if (date_time.unit.hour == 0) // Overflow - date_time.unit.day++; - } - } - movement_set_utc_date_time(date_time); + uint32_t timestamp = movement_get_utc_timestamp(); + timestamp += 1; + movement_set_utc_timestamp(timestamp); } watch_rtc_enable(true); } @@ -126,7 +117,7 @@ static void finetune_update_correction_time(void) { nanosec_state.freq_correction += roundf(nanosec_get_aging() * 100); // Remember when we last corrected time - nanosec_state.last_correction_time = watch_utility_date_time_to_unix_time(watch_rtc_get_date_time(), 0); + nanosec_state.last_correction_time = movement_get_utc_timestamp(); nanosec_save(); movement_move_to_face(0); // Go to main face after saving settings } @@ -146,7 +137,7 @@ bool finetune_face_loop(movement_event_t event, void *context) { // We flash green LED once per minute to measure clock error, when we are not on first screen if (finetune_page!=0) { watch_date_time_t date_time; - date_time = watch_rtc_get_date_time(); + date_time = movement_get_utc_date_time(); if (date_time.unit.second == 0) { watch_set_led_green(); #ifndef __EMSCRIPTEN__ diff --git a/watch-faces/settings/finetune_face.h b/watch-faces/settings/finetune_face.h index 905df760..76ae7e23 100644 --- a/watch-faces/settings/finetune_face.h +++ b/watch-faces/settings/finetune_face.h @@ -44,11 +44,6 @@ * worry about aging only on second/third years of watch calibration (if you * are really looking at less than 10 seconds per year of error). * - * Warning, do not use at the first second of a month, as you might stay at - * the same month and it will surprise you. Just wait 1 second...We are not - * fully replicating RTC timer behavior when RTC is off. - * Simulating months and years is... too much complexity. - * * For full usage instructions, please refer to the wiki: * https://www.sensorwatch.net/docs/watchfaces/nanosec/ */ diff --git a/watch-faces/settings/nanosec_face.c b/watch-faces/settings/nanosec_face.c index d4d7b9f5..f49b22ad 100644 --- a/watch-faces/settings/nanosec_face.c +++ b/watch-faces/settings/nanosec_face.c @@ -27,7 +27,6 @@ #include #include "nanosec_face.h" #include "filesystem.h" -#include "watch_utility.h" int16_t freq_correction_residual = 0; // Dithering 0.1ppm correction, does not need to be configured. int16_t freq_correction_previous = -30000; @@ -44,8 +43,7 @@ const float voltage_coefficient = 0.241666667 * dithering; // 10 * ppm/V. Nomina static void nanosec_init_profile(void) { nanosec_changed = true; nanosec_state.correction_cadence = 10; - watch_date_time_t date_time = watch_rtc_get_date_time(); - nanosec_state.last_correction_time = watch_utility_date_time_to_unix_time(date_time, 0); + nanosec_state.last_correction_time = movement_get_utc_timestamp(); // init data after changing profile - do that once per profile selection switch (nanosec_state.correction_profile) { @@ -265,8 +263,8 @@ static void nanosec_next_edit_screen(void) { float nanosec_get_aging() // Returns aging correction in ppm { - watch_date_time_t date_time = watch_rtc_get_date_time(); - float years = (watch_utility_date_time_to_unix_time(date_time, 0) - nanosec_state.last_correction_time) / 31536000.0f; // Years passed since finetune + uint32_t timestamp = movement_get_utc_timestamp(); + float years = (timestamp - nanosec_state.last_correction_time) / 31536000.0f; // Years passed since finetune return years*nanosec_state.aging_ppm_pa/100.0f; } @@ -377,7 +375,7 @@ movement_watch_face_advisory_t nanosec_face_advise(void *context) { // No need for background correction if we are on profile 0 - static hardware correction. if (nanosec_state.correction_profile != 0) { - watch_date_time_t date_time = watch_rtc_get_date_time(); + watch_date_time_t date_time = movement_get_utc_date_time(); retval.wants_background_task = date_time.unit.minute % nanosec_state.correction_cadence == 0; } From 7acc9cc4142acf025705c256de2105e9c65c25f8 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Sun, 3 Aug 2025 01:13:19 -0400 Subject: [PATCH 05/53] Align the top of the second with the 1Hz periodic interrupt --- movement.c | 68 +++++++++++++++-------- watch-library/hardware/watch/watch_rtc.c | 23 ++++++-- watch-library/simulator/watch/watch_rtc.c | 9 +-- 3 files changed, 68 insertions(+), 32 deletions(-) diff --git a/movement.c b/movement.c index 70bd63b7..4f49fb8a 100644 --- a/movement.c +++ b/movement.c @@ -155,17 +155,36 @@ static udatetime_t _movement_convert_date_time_to_udate(watch_date_time_t date_t static void _movement_set_top_of_minute_alarm() { uint32_t counter = watch_rtc_get_counter(); + uint32_t next_minute_counter; watch_date_time_t date_time = watch_rtc_get_date_time(); uint32_t freq = watch_rtc_get_frequency(); + uint32_t half_freq = freq >> 1; + uint32_t subsecond_mask = freq - 1; + uint32_t ticks_per_minute = watch_rtc_get_ticks_per_minute(); - // remove subsecond from counter - counter &= ~(freq - 1); + // get the counter at the last second tick + next_minute_counter = counter & (~subsecond_mask); + // add/subtract half second shift to sync up second tick with the 1Hz interrupt + next_minute_counter += (counter & subsecond_mask) >= half_freq ? half_freq : -half_freq; // counter at the next top of the minute - counter += (60 - date_time.unit.second) * freq; + next_minute_counter += (60 - date_time.unit.second) * freq; - movement_volatile_state.minute_counter = counter; + // Since the minute alarm is very important, double/triple check to make sure that it will fire. + // These are theoretical corner cases that probably can't even happen, but since we do a subtraction + // above I wanna be certain that we don't schedule the next alarm at a counter value just before the + // current counter, which would result in the alarm firing after more than one year. + // This should be robust to the counter overflow, and we should ever iterate once at most. + if (next_minute_counter == counter) { + next_minute_counter += ticks_per_minute; + } - watch_rtc_register_comp_callback(cb_minute_alarm_fired, counter, MINUTE_TIMEOUT); + while ((next_minute_counter - counter) > ticks_per_minute) { + next_minute_counter += ticks_per_minute; + } + + movement_volatile_state.minute_counter = next_minute_counter; + + watch_rtc_register_comp_callback(cb_minute_alarm_fired, next_minute_counter, MINUTE_TIMEOUT); } static bool _movement_update_dst_offset_cache(void) { @@ -850,9 +869,6 @@ void app_init(void) { watch_rtc_set_date_time(date_time); } - // set up the 1 minute alarm (for background tasks and low power updates) - _movement_set_top_of_minute_alarm(); - // register callbacks to be notified when buzzer starts/stops playing. // this is so movement can be notified even when triggered by a face bypassing movement watch_buzzer_register_global_callbacks(cb_buzzer_start, cb_buzzer_stop); @@ -865,6 +881,9 @@ void app_init(void) { movement_state.light_on = false; movement_state.next_available_backup_register = 2; _movement_reset_inactivity_countdown(); + + // set up the 1 minute alarm (for background tasks and low power updates) + _movement_set_top_of_minute_alarm(); } void app_wake_from_backup(void) { @@ -1069,21 +1088,6 @@ bool app_loop(void) { movement_volatile_state.turn_led_off = false; movement_force_led_off(); } - } - - // actually play the note sequence we were asked to play while in deep sleep. - if (movement_volatile_state.has_pending_sequence) { - movement_volatile_state.has_pending_sequence = false; - watch_buzzer_play_sequence_with_volume(_pending_sequence, movement_request_sleep, movement_button_volume()); - // When this sequence is done playing, movement_request_sleep is invoked and the watch will go, - // back to sleep (unless the user interacts with it in the meantime) - _pending_sequence = NULL; - } - - // handle top-of-minute tasks, if the alarm handler told us we need to - if (movement_volatile_state.minute_alarm_fired) { - movement_volatile_state.minute_alarm_fired = false; - _movement_handle_top_of_minute(); } // if we have a scheduled background task, handle that here: @@ -1123,6 +1127,12 @@ bool app_loop(void) { event_type++; } + // handle top-of-minute tasks, if the alarm handler told us we need to + if (movement_volatile_state.minute_alarm_fired) { + movement_volatile_state.minute_alarm_fired = false; + _movement_handle_top_of_minute(); + } + // Now handle the EVENT_TIMEOUT if (resign_timeout && movement_state.current_face_idx != 0) { event.event_type = EVENT_TIMEOUT; @@ -1153,6 +1163,15 @@ bool app_loop(void) { // // this is a hack tho: waking from sleep mode, app_setup does get called, but it happens before we have reset our ticks. // // need to figure out if there's a better heuristic for determining how we woke up. app_setup(); + + // If we woke up to play a note sequence, actually play the note sequence we were asked to play while in deep sleep. + if (movement_volatile_state.has_pending_sequence) { + movement_volatile_state.has_pending_sequence = false; + watch_buzzer_play_sequence_with_volume(_pending_sequence, movement_request_sleep, movement_button_volume()); + // When this sequence is done playing, movement_request_sleep is invoked and the watch will go, + // back to sleep (unless the user interacts with it in the meantime) + _pending_sequence = NULL; + } } #endif @@ -1300,9 +1319,10 @@ void cb_minute_alarm_fired(void) { void cb_tick(void) { rtc_counter_t counter = watch_rtc_get_counter(); uint32_t freq = watch_rtc_get_frequency(); + uint32_t half_freq = freq >> 1; uint32_t subsecond_mask = freq - 1; movement_volatile_state.pending_events |= 1 << EVENT_TICK; - movement_volatile_state.subsecond = (counter & subsecond_mask) >> movement_state.tick_pern; + movement_volatile_state.subsecond = ((counter + half_freq) & subsecond_mask) >> movement_state.tick_pern; } void cb_accelerometer_event(void) { diff --git a/watch-library/hardware/watch/watch_rtc.c b/watch-library/hardware/watch/watch_rtc.c index 61bc5d1f..f81f10ff 100644 --- a/watch-library/hardware/watch/watch_rtc.c +++ b/watch-library/hardware/watch/watch_rtc.c @@ -34,6 +34,7 @@ static const uint32_t RTC_OSC_DIV = 10; static const uint32_t RTC_OSC_HZ = 1 << RTC_OSC_DIV; // 2^10 = 1024 static const uint32_t RTC_PRESCALER_DIV = 3; static const uint32_t RTC_CNT_HZ = RTC_OSC_HZ >> RTC_PRESCALER_DIV; // 1024 / 2^3 = 128 +static const uint32_t RTC_CNT_SUBSECOND_MASK = RTC_CNT_HZ - 1; static const uint32_t RTC_CNT_DIV = RTC_OSC_DIV - RTC_PRESCALER_DIV; // 7 static const uint32_t RTC_CNT_TICKS_PER_MINUTE = RTC_CNT_HZ * 60; static const uint32_t RTC_CNT_TICKS_PER_HOUR = RTC_CNT_TICKS_PER_MINUTE * 60; @@ -88,17 +89,31 @@ rtc_date_time_t watch_rtc_get_date_time(void) { } void watch_rtc_set_unix_time(unix_timestamp_t unix_time) { - // time_backup + counter / RTC_CNT_HZ = unix_time + /* unix_time = time_backup + counter / RTC_CNT_HZ - 0.5 + * + * Because of the way the hardware is designed, the periodic interrupts fire at the subsecond tick values + * according to the table below (for a 128Hz counter). + * since the 1Hz periodic interrupt is the most important, we shift the conversion from counter to timestamp by 64 ticks, + * so that the second changes at the top of the 1Hz interrupt. Hence the 0.5 factor in the equation above. + * 1Hz: 64 + * 2Hz: 32, 96 + * 4Hz: 16, 48, 80, 112 + * 8Hz: 8, 24, 40, 56, 72, 88, 104, 120 + * 16Hz: 4, 12, 20, ..., 124 + * 32Hz: 2, 6, 10, ..., 126 + * 64Hz: 1, 3, 5, ..., 127 + * 128Hz: 0, 1, 2, ..., 127 + */ rtc_counter_t counter = rtc_get_counter(); - unix_timestamp_t tb = unix_time - (counter >> RTC_CNT_DIV); + unix_timestamp_t tb = unix_time - (counter >> RTC_CNT_DIV) - ((counter & RTC_CNT_SUBSECOND_MASK) >> (RTC_CNT_DIV - 1)) + 1; watch_store_backup_data(tb, TB_BKUP_REG); } unix_timestamp_t watch_rtc_get_unix_time(void) { - // time_backup + counter / RTC_CNT_HZ = unix_time + // unix_time = time_backup + counter / RTC_CNT_HZ - 0.5 rtc_counter_t counter = rtc_get_counter(); unix_timestamp_t tb = watch_get_backup_data(TB_BKUP_REG); - return tb + (counter >> RTC_CNT_DIV); + return tb + (counter >> RTC_CNT_DIV) + ((counter & RTC_CNT_SUBSECOND_MASK) >> (RTC_CNT_DIV - 1)) - 1; } rtc_counter_t watch_rtc_get_counter(void) { diff --git a/watch-library/simulator/watch/watch_rtc.c b/watch-library/simulator/watch/watch_rtc.c index ee410c44..b58b6bfa 100644 --- a/watch-library/simulator/watch/watch_rtc.c +++ b/watch-library/simulator/watch/watch_rtc.c @@ -31,6 +31,7 @@ #include static const uint32_t RTC_CNT_HZ = 128; +static const uint32_t RTC_CNT_SUBSECOND_MASK = RTC_CNT_HZ - 1; static const uint32_t RTC_CNT_DIV = 7; static const uint32_t RTC_CNT_TICKS_PER_MINUTE = RTC_CNT_HZ * 60; static const uint32_t RTC_CNT_TICKS_PER_HOUR = RTC_CNT_TICKS_PER_MINUTE * 60; @@ -98,15 +99,15 @@ rtc_date_time_t watch_rtc_get_date_time(void) { } void watch_rtc_set_unix_time(unix_timestamp_t unix_time) { - // time_backup + counter / RTC_CNT_HZ = unix_time + // unix_time = time_backup + counter / RTC_CNT_HZ - 0.5 rtc_counter_t counter = watch_rtc_get_counter(); - reference_timestamp = unix_time - (counter >> RTC_CNT_DIV); + reference_timestamp = unix_time - (counter >> RTC_CNT_DIV) - ((counter & RTC_CNT_SUBSECOND_MASK) >> (RTC_CNT_DIV - 1)) + 1; } unix_timestamp_t watch_rtc_get_unix_time(void) { - // time_backup + counter / RTC_CNT_HZ = unix_time + // unix_time = time_backup + counter / RTC_CNT_HZ - 0.5 rtc_counter_t counter = watch_rtc_get_counter(); - return reference_timestamp + (counter >> RTC_CNT_DIV); + return reference_timestamp + (counter >> RTC_CNT_DIV) + ((counter & RTC_CNT_SUBSECOND_MASK) >> (RTC_CNT_DIV - 1)) - 1; } rtc_counter_t watch_rtc_get_counter(void) { From a616ac6cc4865b8569a7f1bfb218638903532075 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Thu, 7 Aug 2025 23:34:13 -0400 Subject: [PATCH 06/53] Fix corner case that could cause the top of minute alarm to stop firing --- watch-library/hardware/watch/watch_rtc.c | 28 ++++++++++++++++------- watch-library/simulator/watch/watch_rtc.c | 11 +++++++-- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/watch-library/hardware/watch/watch_rtc.c b/watch-library/hardware/watch/watch_rtc.c index f81f10ff..1fee9917 100644 --- a/watch-library/hardware/watch/watch_rtc.c +++ b/watch-library/hardware/watch/watch_rtc.c @@ -49,6 +49,8 @@ typedef struct { volatile bool enabled; } comp_cb_t; +volatile uint32_t scheduled_comp_counter; + watch_cb_t tick_callbacks[8]; comp_cb_t comp_callbacks[WATCH_RTC_N_COMP_CB]; watch_cb_t alarm_callback; @@ -76,6 +78,8 @@ void _watch_rtc_init(void) { comp_callbacks[index].enabled = false; } + scheduled_comp_counter = 0; + NVIC_ClearPendingIRQ(RTC_IRQn); NVIC_EnableIRQ(RTC_IRQn); } @@ -195,10 +199,15 @@ void watch_rtc_disable_all_periodic_callbacks(void) { } static void _watch_rtc_schedule_next_comp(void) { - rtc_disable_compare_interrupt(); + rtc_counter_t curr_counter = watch_rtc_get_counter(); + // If there is already a pending comp interrupt for this very tick, let it fire + // And this function will be called again as soon as the interrupt fires. + if (curr_counter == scheduled_comp_counter) { + return; + } - // The soonest we can schedule is the next tick - rtc_counter_t curr_counter = watch_rtc_get_counter() + 1; + // Because of the hardware, the soonest we can schedule is the next tick + curr_counter +=1; bool schedule_any = false; rtc_counter_t comp_counter; @@ -216,7 +225,14 @@ static void _watch_rtc_schedule_next_comp(void) { } if (schedule_any) { - rtc_enable_compare_interrupt(comp_counter); + // If we are changing the comp counter at the front of the line + if (comp_counter != scheduled_comp_counter) { + scheduled_comp_counter = comp_counter; + rtc_enable_compare_interrupt(comp_counter); + } + } else { + scheduled_comp_counter = curr_counter - 2; + rtc_disable_compare_interrupt(); } } @@ -225,8 +241,6 @@ void watch_rtc_register_comp_callback(watch_cb_t callback, rtc_counter_t counter return; } - rtc_disable_compare_interrupt(); - comp_callbacks[index].counter = counter; comp_callbacks[index].callback = callback; comp_callbacks[index].enabled = true; @@ -239,8 +253,6 @@ void watch_rtc_disable_comp_callback(uint8_t index) { return; } - rtc_disable_compare_interrupt(); - comp_callbacks[index].enabled = false; _watch_rtc_schedule_next_comp(); diff --git a/watch-library/simulator/watch/watch_rtc.c b/watch-library/simulator/watch/watch_rtc.c index b58b6bfa..d9cfb9f2 100644 --- a/watch-library/simulator/watch/watch_rtc.c +++ b/watch-library/simulator/watch/watch_rtc.c @@ -286,10 +286,15 @@ void watch_rtc_disable_comp_callback(uint8_t index) { } static void _watch_rtc_schedule_next_comp(void) { - scheduled_comp_counter = 0; + rtc_counter_t curr_counter = watch_rtc_get_counter(); + // If there is already a pending comp interrupt for this very tick, let it fire + // And this function will be called again as soon as the interrupt fires. + if (curr_counter == scheduled_comp_counter) { + return; + } // The soonest we can schedule is the next tick - rtc_counter_t curr_counter = watch_rtc_get_counter() + 1; + curr_counter +=1; bool schedule_any = false; rtc_counter_t comp_counter; @@ -309,6 +314,8 @@ static void _watch_rtc_schedule_next_comp(void) { if (schedule_any) { scheduled_comp_counter = comp_counter; + } else { + scheduled_comp_counter = curr_counter - 2; } } From 9770ad4fe97d5e6a8b6fa83ec4f5b183a940fa36 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Fri, 8 Aug 2025 09:19:14 -0400 Subject: [PATCH 07/53] Fix more corner case that could cause the top of minute alarm to stop firing --- movement.c | 36 +++++++++++++---------- watch-library/hardware/watch/watch_rtc.c | 35 +++++++++++++++++----- watch-library/shared/watch/watch_rtc.h | 28 ++++++++++++++++++ watch-library/simulator/watch/watch_rtc.c | 27 +++++++++++++---- 4 files changed, 98 insertions(+), 28 deletions(-) diff --git a/movement.c b/movement.c index 4f49fb8a..7e4a23bf 100644 --- a/movement.c +++ b/movement.c @@ -220,7 +220,7 @@ static inline void _movement_reset_inactivity_countdown(void) { rtc_counter_t counter = watch_rtc_get_counter(); uint32_t freq = watch_rtc_get_frequency(); - watch_rtc_register_comp_callback( + watch_rtc_register_comp_callback_no_schedule( cb_resign_timeout_interrupt, counter + movement_timeout_inactivity_deadlines[movement_state.settings.bit.to_interval] * freq, RESIGN_TIMEOUT @@ -228,16 +228,19 @@ static inline void _movement_reset_inactivity_countdown(void) { movement_volatile_state.enter_sleep_mode = false; - watch_rtc_register_comp_callback( + watch_rtc_register_comp_callback_no_schedule( cb_sleep_timeout_interrupt, counter + movement_le_inactivity_deadlines[movement_state.settings.bit.le_interval] * freq, SLEEP_TIMEOUT ); + + watch_rtc_schedule_next_comp(); } static inline void _movement_disable_inactivity_countdown(void) { - watch_rtc_disable_comp_callback(RESIGN_TIMEOUT); - watch_rtc_disable_comp_callback(SLEEP_TIMEOUT); + watch_rtc_disable_comp_callback_no_schedule(RESIGN_TIMEOUT); + watch_rtc_disable_comp_callback_no_schedule(SLEEP_TIMEOUT); + watch_rtc_schedule_next_comp(); } static void _movement_handle_top_of_minute(void) { @@ -1194,9 +1197,11 @@ bool app_loop(void) { } static movement_event_type_t _process_button_event(bool pin_level, movement_button_t* button) { + movement_event_type_t event_type = EVENT_NONE; + // This shouldn't happen normally if (pin_level == button->is_down) { - return EVENT_NONE; + return event_type; } uint32_t counter = watch_rtc_get_counter(); @@ -1206,43 +1211,42 @@ static movement_event_type_t _process_button_event(bool pin_level, movement_butt if (pin_level) { // We schedule a timeout to fire the longpress event button->down_timestamp = counter; - watch_rtc_register_comp_callback(button->cb_longpress, counter + MOVEMENT_LONG_PRESS_TICKS, button->timeout_index); + watch_rtc_register_comp_callback_no_schedule(button->cb_longpress, counter + MOVEMENT_LONG_PRESS_TICKS, button->timeout_index); // force alarm off if the user pressed a button. watch_buzzer_abort_sequence(); - return button->down_event; + event_type = button->down_event; } else { // We cancel the timeout if it hasn't fired yet - watch_rtc_disable_comp_callback(button->timeout_index); + watch_rtc_disable_comp_callback_no_schedule(button->timeout_index); if ((counter - button->down_timestamp) >= MOVEMENT_LONG_PRESS_TICKS) { - return button->down_event + 3; + event_type = button->down_event + 3; } else { - return button->down_event + 1; + event_type = button->down_event + 1; } } + + // This will also schedule the comp callbacks above + _movement_reset_inactivity_countdown(); + + return event_type; } void cb_light_btn_interrupt(void) { bool pin_level = HAL_GPIO_BTN_LIGHT_read(); movement_volatile_state.pending_events |= 1 << _process_button_event(pin_level, &movement_volatile_state.light_button); - - _movement_reset_inactivity_countdown(); } void cb_mode_btn_interrupt(void) { bool pin_level = HAL_GPIO_BTN_MODE_read(); movement_volatile_state.pending_events |= 1 << _process_button_event(pin_level, &movement_volatile_state.mode_button); - - _movement_reset_inactivity_countdown(); } void cb_alarm_btn_interrupt(void) { bool pin_level = HAL_GPIO_BTN_ALARM_read(); movement_volatile_state.pending_events |= 1 << _process_button_event(pin_level, &movement_volatile_state.alarm_button); - - _movement_reset_inactivity_countdown(); } static movement_event_type_t _process_button_longpress_timeout(movement_button_t* button) { diff --git a/watch-library/hardware/watch/watch_rtc.c b/watch-library/hardware/watch/watch_rtc.c index 1fee9917..8d5255ad 100644 --- a/watch-library/hardware/watch/watch_rtc.c +++ b/watch-library/hardware/watch/watch_rtc.c @@ -198,7 +198,7 @@ void watch_rtc_disable_all_periodic_callbacks(void) { watch_rtc_disable_matching_periodic_callbacks(0xFF); } -static void _watch_rtc_schedule_next_comp(void) { +void watch_rtc_schedule_next_comp(void) { rtc_counter_t curr_counter = watch_rtc_get_counter(); // If there is already a pending comp interrupt for this very tick, let it fire // And this function will be called again as soon as the interrupt fires. @@ -245,7 +245,17 @@ void watch_rtc_register_comp_callback(watch_cb_t callback, rtc_counter_t counter comp_callbacks[index].callback = callback; comp_callbacks[index].enabled = true; - _watch_rtc_schedule_next_comp(); + watch_rtc_schedule_next_comp(); +} + +void watch_rtc_register_comp_callback_no_schedule(watch_cb_t callback, rtc_counter_t counter, uint8_t index) { + if (index >= WATCH_RTC_N_COMP_CB) { + return; + } + + comp_callbacks[index].counter = counter; + comp_callbacks[index].callback = callback; + comp_callbacks[index].enabled = true; } void watch_rtc_disable_comp_callback(uint8_t index) { @@ -255,13 +265,21 @@ void watch_rtc_disable_comp_callback(uint8_t index) { comp_callbacks[index].enabled = false; - _watch_rtc_schedule_next_comp(); + watch_rtc_schedule_next_comp(); +} + +void watch_rtc_disable_comp_callback_no_schedule(uint8_t index) { + if (index >= WATCH_RTC_N_COMP_CB) { + return; + } + + comp_callbacks[index].enabled = false; } void watch_rtc_callback(uint16_t interrupt_cause) { // First read all relevant registers, to ensure no changes occurr during the callbacks uint16_t interrupt_enabled = RTC->MODE0.INTENSET.reg; - rtc_counter_t comp_counter = RTC->MODE0.COMP[0].reg; + rtc_counter_t counter = watch_rtc_get_counter(); if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_PER_Msk) { @@ -291,15 +309,18 @@ void watch_rtc_callback(uint16_t interrupt_cause) { if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_CMP0) { // The comp interrupt is generated one tick after the matched counter - // rtc_counter_t comp_counter = watch_rtc_get_counter() - 1; + rtc_counter_t comp_counter = counter - 1; for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) { - if (comp_callbacks[index].enabled && comp_counter == comp_callbacks[index].counter) { + // Give it a little bit of wiggle room, if a comp callback is enabled and is just passed + if (comp_callbacks[index].enabled && + (comp_counter - comp_callbacks[index].counter) <= RTC_CNT_HZ + ) { comp_callbacks[index].enabled = false; comp_callbacks[index].callback(); } } - _watch_rtc_schedule_next_comp(); + watch_rtc_schedule_next_comp(); } if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_OVF) { diff --git a/watch-library/shared/watch/watch_rtc.h b/watch-library/shared/watch/watch_rtc.h index e268ad28..504c37f3 100644 --- a/watch-library/shared/watch/watch_rtc.h +++ b/watch-library/shared/watch/watch_rtc.h @@ -114,10 +114,38 @@ uint32_t watch_rtc_get_ticks_per_minute(void); */ void watch_rtc_register_comp_callback(watch_cb_t callback, rtc_counter_t counter, uint8_t index); +/** @brief Just like watch_rtc_register_comp_callback but doesn't actually schedule the callback + * + * Useful if you need register multiple callbacks at once, avoids multiple calls to the expensive watch_rtc_schedule_next_comp: + * Usage: + * watch_rtc_register_comp_callback_no_schedule(cb0, counter0, index0); + * watch_rtc_register_comp_callback_no_schedule(cb1, counter1, index1); + * watch_rtc_schedule_next_comp(); + */ +void watch_rtc_register_comp_callback_no_schedule(watch_cb_t callback, rtc_counter_t counter, uint8_t index); + /** @brief Disables the specified comp callback. */ void watch_rtc_disable_comp_callback(uint8_t index); +/** @brief Just like watch_rtc_disable_comp_callback but doesn't actually schedule the callback + * + * Useful if you need disable multiple callbacks at once, avoids multiple calls to the expensive watch_rtc_schedule_next_comp: + * Usage: + * watch_rtc_disable_comp_callback_no_schedule(index0); + * watch_rtc_disable_comp_callback_no_schedule(index1); + * watch_rtc_schedule_next_comp(); + */ +/** @brief Disables the specified comp callback. + */ +void watch_rtc_disable_comp_callback_no_schedule(uint8_t index); + +/** @brief Determines the first comp callback that should fire and schedule it with the RTC + * + * You would never need to call this manually, unless you used the 'no_schedule' functions above. + */ +void watch_rtc_schedule_next_comp(void); + /** @brief Disables the alarm callback. */ // void watch_rtc_disable_alarm_callback(void); diff --git a/watch-library/simulator/watch/watch_rtc.c b/watch-library/simulator/watch/watch_rtc.c index d9cfb9f2..1347d797 100644 --- a/watch-library/simulator/watch/watch_rtc.c +++ b/watch-library/simulator/watch/watch_rtc.c @@ -65,7 +65,6 @@ watch_cb_t a4_callback; static void _watch_increase_counter(void *userData); static void _watch_process_periodic_callbacks(void); static void _watch_process_comp_callbacks(void); -static void _watch_rtc_schedule_next_comp(void); bool _watch_rtc_is_enabled(void) { return counter_interval; @@ -228,7 +227,7 @@ static void _watch_process_comp_callbacks(void) { } } - _watch_rtc_schedule_next_comp(); + watch_rtc_schedule_next_comp(); } } @@ -272,7 +271,17 @@ void watch_rtc_register_comp_callback(watch_cb_t callback, rtc_counter_t counter comp_callbacks[index].callback = callback; comp_callbacks[index].enabled = true; - _watch_rtc_schedule_next_comp(); + watch_rtc_schedule_next_comp(); +} + +void watch_rtc_register_comp_callback_no_schedule(watch_cb_t callback, rtc_counter_t counter, uint8_t index) { + if (index >= WATCH_RTC_N_COMP_CB) { + return; + } + + comp_callbacks[index].counter = counter; + comp_callbacks[index].callback = callback; + comp_callbacks[index].enabled = true; } void watch_rtc_disable_comp_callback(uint8_t index) { @@ -282,10 +291,18 @@ void watch_rtc_disable_comp_callback(uint8_t index) { comp_callbacks[index].enabled = false; - _watch_rtc_schedule_next_comp(); + watch_rtc_schedule_next_comp(); } -static void _watch_rtc_schedule_next_comp(void) { +void watch_rtc_disable_comp_callback_no_schedule(uint8_t index) { + if (index >= WATCH_RTC_N_COMP_CB) { + return; + } + + comp_callbacks[index].enabled = false; +} + +void watch_rtc_schedule_next_comp(void) { rtc_counter_t curr_counter = watch_rtc_get_counter(); // If there is already a pending comp interrupt for this very tick, let it fire // And this function will be called again as soon as the interrupt fires. From a71967a0156a2f09f4eb4a706c9e5f6c345be4cb Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Sat, 9 Aug 2025 11:47:57 -0400 Subject: [PATCH 08/53] Perform as little work as possible in the interrupt callbacks --- movement.c | 111 +++++++++++++++++------ watch-library/hardware/watch/watch_rtc.c | 9 +- 2 files changed, 85 insertions(+), 35 deletions(-) diff --git a/movement.c b/movement.c index 7e4a23bf..49df5023 100644 --- a/movement.c +++ b/movement.c @@ -84,6 +84,7 @@ typedef struct { volatile bool minute_alarm_fired; volatile bool is_buzzing; volatile uint8_t pending_sequence_priority; + volatile bool schedule_next_comp; // button tracking for long press movement_button_t mode_button; @@ -184,7 +185,8 @@ static void _movement_set_top_of_minute_alarm() { movement_volatile_state.minute_counter = next_minute_counter; - watch_rtc_register_comp_callback(cb_minute_alarm_fired, next_minute_counter, MINUTE_TIMEOUT); + watch_rtc_register_comp_callback_no_schedule(cb_minute_alarm_fired, next_minute_counter, MINUTE_TIMEOUT); + movement_volatile_state.schedule_next_comp = true; } static bool _movement_update_dst_offset_cache(void) { @@ -234,13 +236,66 @@ static inline void _movement_reset_inactivity_countdown(void) { SLEEP_TIMEOUT ); - watch_rtc_schedule_next_comp(); + movement_volatile_state.schedule_next_comp = true; } static inline void _movement_disable_inactivity_countdown(void) { watch_rtc_disable_comp_callback_no_schedule(RESIGN_TIMEOUT); watch_rtc_disable_comp_callback_no_schedule(SLEEP_TIMEOUT); - watch_rtc_schedule_next_comp(); + movement_volatile_state.schedule_next_comp = true; +} + +static void _movement_renew_top_of_minute_alarm(void) { + // Renew the alarm for a minute from the previous one (ensures no drift) + movement_volatile_state.minute_counter += watch_rtc_get_ticks_per_minute(); + watch_rtc_register_comp_callback_no_schedule(cb_minute_alarm_fired, movement_volatile_state.minute_counter, MINUTE_TIMEOUT); + movement_volatile_state.schedule_next_comp = true; +} + +static void _movement_handle_button_presses(uint32_t pending_events) { + bool any_up = false; + bool any_down = false; + + movement_button_t* buttons[3] = { + &movement_volatile_state.mode_button, + &movement_volatile_state.light_button, + &movement_volatile_state.alarm_button + }; + + for (uint8_t i = 0; i < 3; i++) { + movement_button_t* button = buttons[i]; + + // If a button down occurred + if (pending_events & (1 << button->down_event)) { + watch_rtc_register_comp_callback_no_schedule(button->cb_longpress, button->down_timestamp + MOVEMENT_LONG_PRESS_TICKS, button->timeout_index); + any_down = true; + } + + // If a button up or button long up occurred + if (pending_events & ( + (1 << (button->down_event + 1)) | + (1 << (button->down_event + 3)) + )) { + // We cancel the timeout if it hasn't fired yet + watch_rtc_disable_comp_callback_no_schedule(button->timeout_index); + any_up = true; + } + } + + if (any_down) { + // force alarm off if the user pressed a button. + watch_buzzer_abort_sequence(); + + // Delay auto light off if the user is still interacting with the watch. + if (movement_state.light_on) { + movement_illuminate_led(); + } + } + + if (any_down || any_up) { + _movement_reset_inactivity_countdown(); + movement_volatile_state.schedule_next_comp = true; + } } static void _movement_handle_top_of_minute(void) { @@ -328,11 +383,12 @@ void movement_illuminate_led(void) { // Set a timeout to turn off the light rtc_counter_t counter = watch_rtc_get_counter(); uint32_t freq = watch_rtc_get_frequency(); - watch_rtc_register_comp_callback( + watch_rtc_register_comp_callback_no_schedule( cb_led_timeout_interrupt, counter + (movement_state.settings.bit.led_duration * 2 - 1) * freq, LED_TIMEOUT ); + movement_volatile_state.schedule_next_comp = true; } } } @@ -342,13 +398,15 @@ void movement_force_led_on(uint8_t red, uint8_t green, uint8_t blue) { movement_state.light_on = true; watch_set_led_color_rgb(red, green, blue); rtc_counter_t counter = watch_rtc_get_counter(); - watch_rtc_register_comp_callback(cb_led_timeout_interrupt, counter + 32767, LED_TIMEOUT); + watch_rtc_register_comp_callback_no_schedule(cb_led_timeout_interrupt, counter + 32767, LED_TIMEOUT); + movement_volatile_state.schedule_next_comp = true; } void movement_force_led_off(void) { movement_state.light_on = false; // The led timeout probably already triggered, but still disable just in case we are switching off the light by other means - watch_rtc_disable_comp_callback(LED_TIMEOUT); + watch_rtc_disable_comp_callback_no_schedule(LED_TIMEOUT); + movement_volatile_state.schedule_next_comp = true; watch_set_led_off(); } @@ -1018,6 +1076,7 @@ static void _sleep_mode_app_loop(void) { // we also have to handle top-of-the-minute tasks here in the mini-runloop if (movement_volatile_state.minute_alarm_fired) { movement_volatile_state.minute_alarm_fired = false; + _movement_renew_top_of_minute_alarm(); _movement_handle_top_of_minute(); } @@ -1034,6 +1093,12 @@ static void _sleep_mode_app_loop(void) { return; } + // If we have made changes to any of the RTC comp timers, schedule the next one in the queue + if (movement_volatile_state.schedule_next_comp) { + movement_volatile_state.schedule_next_comp = false; + watch_rtc_schedule_next_comp(); + } + // otherwise enter sleep mode, until either the top of the minute interrupt or extwake wakes us up. watch_enter_sleep_mode(); } @@ -1093,6 +1158,9 @@ bool app_loop(void) { } } + // handle any button up/down events that occurred, e.g. schedule longpress timeouts, reset inactivity, etc. + _movement_handle_button_presses(pending_events); + // if we have a scheduled background task, handle that here: if ( (pending_events & (1 << EVENT_TICK)) @@ -1102,17 +1170,6 @@ bool app_loop(void) { _movement_handle_scheduled_tasks(); } - // Delay auto light off if the user is still interacting with the watch. - if (movement_state.light_on) { - if (pending_events & ( - (1 << EVENT_LIGHT_BUTTON_DOWN) | - (1 << EVENT_MODE_BUTTON_DOWN) | - (1 << EVENT_ALARM_BUTTON_DOWN) - )) { - movement_illuminate_led(); - } - } - // Pop the EVENT_TIMEOUT out of the pending_events so it can be handled separately bool resign_timeout = (pending_events & (1 << EVENT_TIMEOUT)) != 0; if (resign_timeout) { @@ -1133,6 +1190,7 @@ bool app_loop(void) { // handle top-of-minute tasks, if the alarm handler told us we need to if (movement_volatile_state.minute_alarm_fired) { movement_volatile_state.minute_alarm_fired = false; + _movement_renew_top_of_minute_alarm(); _movement_handle_top_of_minute(); } @@ -1178,6 +1236,12 @@ bool app_loop(void) { } #endif + // If we have made changes to any of the RTC comp timers, schedule the next one in the queue + if (movement_volatile_state.schedule_next_comp) { + movement_volatile_state.schedule_next_comp = false; + watch_rtc_schedule_next_comp(); + } + #if __EMSCRIPTEN__ shell_task(); #else @@ -1209,15 +1273,9 @@ static movement_event_type_t _process_button_event(bool pin_level, movement_butt button->is_down = pin_level; if (pin_level) { - // We schedule a timeout to fire the longpress event button->down_timestamp = counter; - watch_rtc_register_comp_callback_no_schedule(button->cb_longpress, counter + MOVEMENT_LONG_PRESS_TICKS, button->timeout_index); - // force alarm off if the user pressed a button. - watch_buzzer_abort_sequence(); event_type = button->down_event; } else { - // We cancel the timeout if it hasn't fired yet - watch_rtc_disable_comp_callback_no_schedule(button->timeout_index); if ((counter - button->down_timestamp) >= MOVEMENT_LONG_PRESS_TICKS) { event_type = button->down_event + 3; } else { @@ -1225,9 +1283,6 @@ static movement_event_type_t _process_button_event(bool pin_level, movement_butt } } - // This will also schedule the comp callbacks above - _movement_reset_inactivity_countdown(); - return event_type; } @@ -1314,10 +1369,6 @@ void cb_minute_alarm_fired(void) { #if __EMSCRIPTEN__ _wake_up_simulator(); #endif - - // Renew the alarm for a minute from the previous one (ensures no drift) - movement_volatile_state.minute_counter += watch_rtc_get_ticks_per_minute(); - watch_rtc_register_comp_callback(cb_minute_alarm_fired, movement_volatile_state.minute_counter, MINUTE_TIMEOUT); } void cb_tick(void) { diff --git a/watch-library/hardware/watch/watch_rtc.c b/watch-library/hardware/watch/watch_rtc.c index 8d5255ad..8bdc1c50 100644 --- a/watch-library/hardware/watch/watch_rtc.c +++ b/watch-library/hardware/watch/watch_rtc.c @@ -207,7 +207,7 @@ void watch_rtc_schedule_next_comp(void) { } // Because of the hardware, the soonest we can schedule is the next tick - curr_counter +=1; + // curr_counter +=1; bool schedule_any = false; rtc_counter_t comp_counter; @@ -278,9 +278,8 @@ void watch_rtc_disable_comp_callback_no_schedule(uint8_t index) { void watch_rtc_callback(uint16_t interrupt_cause) { // First read all relevant registers, to ensure no changes occurr during the callbacks - uint16_t interrupt_enabled = RTC->MODE0.INTENSET.reg; rtc_counter_t counter = watch_rtc_get_counter(); - + uint16_t interrupt_enabled = (uint16_t)RTC->MODE0.INTENSET.reg; if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_PER_Msk) { // handle the tick callback first, it's what we do the most. @@ -309,12 +308,12 @@ void watch_rtc_callback(uint16_t interrupt_cause) { if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_CMP0) { // The comp interrupt is generated one tick after the matched counter - rtc_counter_t comp_counter = counter - 1; + // rtc_counter_t comp_counter = counter - 1; for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) { // Give it a little bit of wiggle room, if a comp callback is enabled and is just passed if (comp_callbacks[index].enabled && - (comp_counter - comp_callbacks[index].counter) <= RTC_CNT_HZ + (counter - comp_callbacks[index].counter) <= RTC_CNT_HZ ) { comp_callbacks[index].enabled = false; comp_callbacks[index].callback(); From 96682a513b18ac27874b05d60cd3ad771c890e70 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Sat, 9 Aug 2025 20:21:10 -0400 Subject: [PATCH 09/53] Undo some workarounds implemented to fix race conditions that are no longer needed --- watch-library/hardware/watch/watch_rtc.c | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/watch-library/hardware/watch/watch_rtc.c b/watch-library/hardware/watch/watch_rtc.c index 8bdc1c50..b3c61efb 100644 --- a/watch-library/hardware/watch/watch_rtc.c +++ b/watch-library/hardware/watch/watch_rtc.c @@ -207,7 +207,7 @@ void watch_rtc_schedule_next_comp(void) { } // Because of the hardware, the soonest we can schedule is the next tick - // curr_counter +=1; + curr_counter += 1; bool schedule_any = false; rtc_counter_t comp_counter; @@ -278,7 +278,7 @@ void watch_rtc_disable_comp_callback_no_schedule(uint8_t index) { void watch_rtc_callback(uint16_t interrupt_cause) { // First read all relevant registers, to ensure no changes occurr during the callbacks - rtc_counter_t counter = watch_rtc_get_counter(); + rtc_counter_t comp_counter = RTC->MODE0.COMP[0].reg; uint16_t interrupt_enabled = (uint16_t)RTC->MODE0.INTENSET.reg; if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_PER_Msk) { @@ -307,14 +307,8 @@ void watch_rtc_callback(uint16_t interrupt_cause) { } if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_CMP0) { - // The comp interrupt is generated one tick after the matched counter - // rtc_counter_t comp_counter = counter - 1; - for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) { - // Give it a little bit of wiggle room, if a comp callback is enabled and is just passed - if (comp_callbacks[index].enabled && - (counter - comp_callbacks[index].counter) <= RTC_CNT_HZ - ) { + if (comp_callbacks[index].enabled && comp_counter == comp_callbacks[index].counter) { comp_callbacks[index].enabled = false; comp_callbacks[index].callback(); } From 42fdad78d5c157f6c8e1c8191e088c56bed15860 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Tue, 12 Aug 2025 22:28:17 -0400 Subject: [PATCH 10/53] Add more theoretical checks to ensure we don't miss out on any comp timers Even without this "fix" the watch seems to be extremely stable and haven't seen any missed timers over several days test. So this might not actually be needed, but it should make things even safer. --- watch-library/hardware/watch/watch_rtc.c | 28 ++++++++++++++---------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/watch-library/hardware/watch/watch_rtc.c b/watch-library/hardware/watch/watch_rtc.c index b3c61efb..1b0b5f49 100644 --- a/watch-library/hardware/watch/watch_rtc.c +++ b/watch-library/hardware/watch/watch_rtc.c @@ -39,6 +39,8 @@ static const uint32_t RTC_CNT_DIV = RTC_OSC_DIV - RTC_PRESCALER_DIV; // 7 static const uint32_t RTC_CNT_TICKS_PER_MINUTE = RTC_CNT_HZ * 60; static const uint32_t RTC_CNT_TICKS_PER_HOUR = RTC_CNT_TICKS_PER_MINUTE * 60; +static const uint32_t RTC_COMP_GRACE_PERIOD = 4; + static const int TB_BKUP_REG = 7; #define WATCH_RTC_N_COMP_CB 8 @@ -200,14 +202,10 @@ void watch_rtc_disable_all_periodic_callbacks(void) { void watch_rtc_schedule_next_comp(void) { rtc_counter_t curr_counter = watch_rtc_get_counter(); - // If there is already a pending comp interrupt for this very tick, let it fire - // And this function will be called again as soon as the interrupt fires. - if (curr_counter == scheduled_comp_counter) { - return; - } - // Because of the hardware, the soonest we can schedule is the next tick - curr_counter += 1; + // We want to ensure we never miss any registered callbacks, + // so if a callback counter has just passed but didn't fire, give it a chance to fire. + rtc_counter_t lax_curr_counter = curr_counter - RTC_COMP_GRACE_PERIOD; bool schedule_any = false; rtc_counter_t comp_counter; @@ -215,7 +213,7 @@ void watch_rtc_schedule_next_comp(void) { for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) { if (comp_callbacks[index].enabled) { - rtc_counter_t diff = comp_callbacks[index].counter - curr_counter; + rtc_counter_t diff = comp_callbacks[index].counter - lax_curr_counter; if (diff <= min_diff) { min_diff = diff; comp_counter = comp_callbacks[index].counter; @@ -225,13 +223,17 @@ void watch_rtc_schedule_next_comp(void) { } if (schedule_any) { - // If we are changing the comp counter at the front of the line + // If we are changing the comp counter at the front of the line, don't schedule a comp interrupt for a counter that is too close to now if (comp_counter != scheduled_comp_counter) { + rtc_counter_t earliest_comp_counter = curr_counter + RTC_COMP_GRACE_PERIOD; + if ((earliest_comp_counter - lax_curr_counter) > (comp_counter - lax_curr_counter)) { + comp_counter = earliest_comp_counter; + } scheduled_comp_counter = comp_counter; rtc_enable_compare_interrupt(comp_counter); } } else { - scheduled_comp_counter = curr_counter - 2; + scheduled_comp_counter = lax_curr_counter - RTC_COMP_GRACE_PERIOD; rtc_disable_compare_interrupt(); } } @@ -278,7 +280,7 @@ void watch_rtc_disable_comp_callback_no_schedule(uint8_t index) { void watch_rtc_callback(uint16_t interrupt_cause) { // First read all relevant registers, to ensure no changes occurr during the callbacks - rtc_counter_t comp_counter = RTC->MODE0.COMP[0].reg; + rtc_counter_t curr_counter = watch_rtc_get_counter(); uint16_t interrupt_enabled = (uint16_t)RTC->MODE0.INTENSET.reg; if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_PER_Msk) { @@ -308,7 +310,9 @@ void watch_rtc_callback(uint16_t interrupt_cause) { if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_CMP0) { for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) { - if (comp_callbacks[index].enabled && comp_counter == comp_callbacks[index].counter) { + if (comp_callbacks[index].enabled && + (curr_counter - comp_callbacks[index].counter) < (RTC_COMP_GRACE_PERIOD * 4) + ) { comp_callbacks[index].enabled = false; comp_callbacks[index].callback(); } From a34a266903303fa4afaeb07c98c13396b7501b39 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Tue, 12 Aug 2025 22:25:47 -0400 Subject: [PATCH 11/53] Add a face to debug some counter32 metrics --- movement_config.h | 5 +- movement_faces.h | 1 + watch-faces.mk | 1 + watch-faces/demo/rtccount_face.c | 196 +++++++++++++++++++++++++++++++ watch-faces/demo/rtccount_face.h | 47 ++++++++ 5 files changed, 248 insertions(+), 2 deletions(-) create mode 100644 watch-faces/demo/rtccount_face.c create mode 100644 watch-faces/demo/rtccount_face.h diff --git a/movement_config.h b/movement_config.h index 0da8da43..f5f6b2b4 100644 --- a/movement_config.h +++ b/movement_config.h @@ -38,7 +38,8 @@ const watch_face_t watch_faces[] = { temperature_display_face, voltage_face, settings_face, - set_time_face + set_time_face, + rtccount_face, }; #define MOVEMENT_NUM_FACES (sizeof(watch_faces) / sizeof(watch_face_t)) @@ -49,7 +50,7 @@ const watch_face_t watch_faces[] = { * Some folks also like to use this to hide the preferences and time set faces from the normal rotation. * If you don't want any faces to be excluded, set this to 0 and a long Mode press will have no effect. */ -#define MOVEMENT_SECONDARY_FACE_INDEX (MOVEMENT_NUM_FACES - 4) +#define MOVEMENT_SECONDARY_FACE_INDEX (MOVEMENT_NUM_FACES - 5) /* Custom hourly chime tune. Check movement_custom_signal_tunes.h for options. */ #define SIGNAL_TUNE_DEFAULT diff --git a/movement_faces.h b/movement_faces.h index b75d5d7e..164dbb4c 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -79,4 +79,5 @@ #include "lander_face.h" #include "simon_face.h" #include "ping_face.h" +#include "rtccount_face.h" // New includes go above this line. diff --git a/watch-faces.mk b/watch-faces.mk index 5fd366a0..69ec4e9c 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -22,6 +22,7 @@ SRCS += \ ./watch-faces/demo/character_set_face.c \ ./watch-faces/demo/light_sensor_face.c \ ./watch-faces/demo/peek_memory_face.c \ + ./watch-faces/demo/rtccount_face.c \ ./watch-faces/sensor/accelerometer_status_face.c \ ./watch-faces/sensor/temperature_display_face.c \ ./watch-faces/sensor/temperature_logging_face.c \ diff --git a/watch-faces/demo/rtccount_face.c b/watch-faces/demo/rtccount_face.c new file mode 100644 index 00000000..f08cde5b --- /dev/null +++ b/watch-faces/demo/rtccount_face.c @@ -0,0 +1,196 @@ +/* + * MIT License + * + * Copyright (c) 2022 Joey Castillo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include +#include +#include "rtccount_face.h" +#include "watch.h" +#include "sam.h" +#include "watch_utility.h" +#include "watch_common_display.h" +#include "watch_rtc.h" + +typedef enum { + RTCCOUNT_STATUS_COUNTER = 0, + RTCCOUNT_STATUS_COUNTER_SUB, + RTCCOUNT_STATUS_MINUTES, + RTCCOUNT_STATUS_MINUTES_DIFF, + RTCCOUNT_STATUS_NUMBER +} rtccount_face_status_t; + +typedef struct { + rtccount_face_status_t status; + uint8_t frequency; + uint32_t n_top_of_minute; + uint32_t ref_timestamp; +} rtccount_state_t; + +static const uint32_t COUNTER_MASK = (1 << 19) - 1; + +static void _rtccount_face_draw(movement_event_t event, rtccount_state_t* state) { + uint32_t counter = watch_rtc_get_counter(); + + char buf[11] = " 000000\0"; + switch (state->status) { + case RTCCOUNT_STATUS_COUNTER: { + buf[0] = 'C'; + break; + } + + case RTCCOUNT_STATUS_COUNTER_SUB: { + buf[0] = 'S'; + break; + } + + case RTCCOUNT_STATUS_MINUTES: { + buf[0] = 'M'; + break; + } + + case RTCCOUNT_STATUS_MINUTES_DIFF: { + buf[0] = 'D'; + break; + } + + default: + break; + } + + watch_display_string(buf, 0); + + snprintf(buf, sizeof(buf), "%u", event.subsecond); + uint32_t len = strlen(buf); + watch_display_string(buf, 4 - len); + + switch (state->status) { + case RTCCOUNT_STATUS_COUNTER: { + snprintf(buf, sizeof(buf), "%lu", counter & COUNTER_MASK); + + size_t len = strlen(buf); + + watch_display_string(buf, 10 - len); + break; + } + + case RTCCOUNT_STATUS_COUNTER_SUB: { + snprintf(buf, sizeof(buf), "%lu", counter & 127); + + size_t len = strlen(buf); + + watch_display_string(buf, 10 - len); + break; + } + + case RTCCOUNT_STATUS_MINUTES: { + snprintf(buf, sizeof(buf), "%lu", state->n_top_of_minute & COUNTER_MASK); + + size_t len = strlen(buf); + + watch_display_string(buf, 10 - len); + break; + } + + case RTCCOUNT_STATUS_MINUTES_DIFF: { + uint32_t elapsed_minutes = (movement_get_utc_timestamp() - state->ref_timestamp) / 60; + + snprintf(buf, sizeof(buf), "%lu", (elapsed_minutes - state->n_top_of_minute) & COUNTER_MASK); + + size_t len = strlen(buf); + + watch_display_string(buf, 10 - len); + break; + } + + default: + break; + } +} + +void rtccount_face_setup(uint8_t watch_face_index, void ** context_ptr) { + (void) watch_face_index; + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(rtccount_state_t)); + memset(*context_ptr, 0, sizeof(rtccount_state_t)); + rtccount_state_t *state = (rtccount_state_t *) *context_ptr; + state->status = RTCCOUNT_STATUS_COUNTER; + state->frequency = 1; + state->n_top_of_minute = 0; + rtc_date_time_t datetime = movement_get_utc_date_time(); + state->ref_timestamp = movement_get_utc_timestamp() - datetime.unit.second; + } +} + +void rtccount_face_activate(void *context) { + rtccount_state_t* state = (rtccount_state_t*)context; + movement_request_tick_frequency(state->frequency); +} + +bool rtccount_face_loop(movement_event_t event, void *context) { + rtccount_state_t* state = (rtccount_state_t*)context; + + switch (event.event_type) { + case EVENT_BACKGROUND_TASK: + state->n_top_of_minute += 1; + break; + case EVENT_ALARM_BUTTON_UP: + if (state->frequency == 128) { + state->frequency = 1; + } else { + state->frequency *= 2; + } + + movement_request_tick_frequency(state->frequency); + break; + case EVENT_ALARM_LONG_PRESS: + state->n_top_of_minute = 0; + rtc_date_time_t datetime = movement_get_utc_date_time(); + state->ref_timestamp = movement_get_utc_timestamp() - datetime.unit.second; + break; + case EVENT_LIGHT_BUTTON_DOWN: + state->status = (state->status + 1) % RTCCOUNT_STATUS_NUMBER; + _rtccount_face_draw(event, state); + break; + case EVENT_ACTIVATE: + case EVENT_TICK: + _rtccount_face_draw(event, state); + break; + default: + movement_default_loop_handler(event); + break; + } + + return true; +} + +void rtccount_face_resign(void *context) { + (void) context; + movement_request_tick_frequency(1); +} + +movement_watch_face_advisory_t rtccount_face_advise(void *context) { + (void) context; + movement_watch_face_advisory_t retval = { 0 }; + retval.wants_background_task = true; + return retval; +} diff --git a/watch-faces/demo/rtccount_face.h b/watch-faces/demo/rtccount_face.h new file mode 100644 index 00000000..59f11929 --- /dev/null +++ b/watch-faces/demo/rtccount_face.h @@ -0,0 +1,47 @@ +/* + * MIT License + * + * Copyright (c) 2022 Joey Castillo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +/* + * RTCCOUNT FACE + * + * A test face to inspect some metrics of the rtc-counter32 mode. + */ + +#include "movement.h" + +void rtccount_face_setup(uint8_t watch_face_index, void ** context_ptr); +void rtccount_face_activate(void *context); +bool rtccount_face_loop(movement_event_t event, void *context); +void rtccount_face_resign(void *context); +movement_watch_face_advisory_t rtccount_face_advise(void *context); + +#define rtccount_face ((const watch_face_t){ \ + rtccount_face_setup, \ + rtccount_face_activate, \ + rtccount_face_loop, \ + rtccount_face_resign, \ + rtccount_face_advise, \ +}) From c37d40d086bf5d6d8694d6b94b30f9bb6dc8bda6 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Thu, 14 Aug 2025 21:40:02 -0400 Subject: [PATCH 12/53] Make the simulator play beeps a lot more reliably. --- watch-library/simulator/watch/watch_tcc.c | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/watch-library/simulator/watch/watch_tcc.c b/watch-library/simulator/watch/watch_tcc.c index 5e5dd013..c7b1182d 100644 --- a/watch-library/simulator/watch/watch_tcc.c +++ b/watch-library/simulator/watch/watch_tcc.c @@ -150,7 +150,11 @@ void watch_enable_buzzer(void) { buzzer_period = NotePeriods[BUZZER_NOTE_A4]; EM_ASM({ - Module['audioContext'] = new (window.AudioContext || window.webkitAudioContext)(); + // "It's recommended to create one AudioContext and reuse it instead of initializing a new one each time." + // https://developer.mozilla.org/en-US/docs/Web/API/AudioContext + if (!Module['audioContext']) { + Module['audioContext'] = new (window.AudioContext || window.webkitAudioContext)(); + } }); } @@ -163,13 +167,6 @@ void watch_set_buzzer_period_and_duty_cycle(uint32_t period, uint8_t duty_cycle) void watch_disable_buzzer(void) { buzzer_enabled = false; buzzer_period = NotePeriods[BUZZER_NOTE_A4]; - - EM_ASM({ - if (Module['audioContext']) { - Module['audioContext'].close(); - Module['audioContext'] = null; - } - }); } void watch_set_buzzer_on(void) { From 3caef587df7416108c04f9431a1bddf7f5b5ccd2 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Fri, 15 Aug 2025 00:53:23 -0400 Subject: [PATCH 13/53] Play an arbitrary stream on the buzzer without blocking --- watch-library/hardware/watch/watch_tcc.c | 70 ++++++++++++++++++++++- watch-library/shared/watch/watch_tcc.h | 49 ++++++++++++++++ watch-library/simulator/watch/watch_tcc.c | 68 +++++++++++++++++++++- 3 files changed, 183 insertions(+), 4 deletions(-) diff --git a/watch-library/hardware/watch/watch_tcc.c b/watch-library/hardware/watch/watch_tcc.c index 0e1fdd9d..8624ffcc 100644 --- a/watch-library/hardware/watch/watch_tcc.c +++ b/watch-library/hardware/watch/watch_tcc.c @@ -28,11 +28,15 @@ #include "tc.h" void _watch_enable_tcc(void); +static void (*_cb_tc0)(void) = NULL; void cb_watch_buzzer_seq(void); +void cb_watch_buzzer_raw_source(void); static uint16_t _seq_position; static int8_t _tone_ticks, _repeat_counter; static int8_t *_sequence; +static watch_buzzer_raw_source_t _raw_source; +static void* _userdata; static uint8_t _volume; static void (*_cb_finished)(void); static watch_cb_t _cb_start_global = NULL; @@ -94,6 +98,7 @@ void watch_buzzer_play_sequence_with_volume(int8_t *note_sequence, void (*callba _repeat_counter = -1; // prepare buzzer + _cb_tc0 = cb_watch_buzzer_seq; // setup TC0 timer _tc0_initialize(); // start the timer (for the 64 hz callback) @@ -138,6 +143,67 @@ void cb_watch_buzzer_seq(void) { } else _tone_ticks--; } +void watch_buzzer_play_raw_source(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end) { + watch_buzzer_play_raw_source_with_volume(raw_source, userdata, callback_on_end, WATCH_BUZZER_VOLUME_LOUD); +} + +void watch_buzzer_play_raw_source_with_volume(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end, watch_buzzer_volume_t volume) { + // Abort any previous sequence + watch_buzzer_abort_sequence(); + + _buzzer_is_active = true; + + if (_cb_start_global) { + _cb_start_global(); + } + + watch_enable_buzzer_and_leds(); + + watch_set_buzzer_off(); + _raw_source = raw_source; + _userdata = userdata; + _cb_finished = callback_on_end; + _volume = volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25; + _seq_position = 0; + _tone_ticks = 0; + // prepare buzzer + + _cb_tc0 = cb_watch_buzzer_raw_source; + // setup TC0 timer + _tc0_initialize(); + // start the timer (for the 64 hz callback) + _tc0_start(); +} + +void cb_watch_buzzer_raw_source(void) { + // callback for reading the note sequence + uint16_t period; + uint16_t duration; + bool done; + + if (_tone_ticks == 0) { + done = _raw_source(_seq_position, _userdata, &period, &duration); + + if (done) { + // end the sequence + watch_buzzer_abort_sequence(); + } else { + if (period == WATCH_BUZZER_PERIOD_REST) { + watch_set_buzzer_off(); + } else { + watch_set_buzzer_period_and_duty_cycle(period, _volume); + watch_set_buzzer_on(); + } + + // set duration ticks and move to next tone + _tone_ticks = duration; + _seq_position += 1; + } + } else { + _tone_ticks--; + } +} + void watch_buzzer_abort_sequence(void) { // ends/aborts the sequence if (!_buzzer_is_active) { @@ -169,7 +235,9 @@ void watch_buzzer_register_global_callbacks(watch_cb_t cb_start, watch_cb_t cb_s void irq_handler_tc0(void) { // interrupt handler for TC0 (globally!) - cb_watch_buzzer_seq(); + if (_cb_tc0) { + _cb_tc0(); + } TC0->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF; } diff --git a/watch-library/shared/watch/watch_tcc.h b/watch-library/shared/watch/watch_tcc.h index 99c56dcc..e87e53bf 100644 --- a/watch-library/shared/watch/watch_tcc.h +++ b/watch-library/shared/watch/watch_tcc.h @@ -126,6 +126,10 @@ typedef enum { BUZZER_NOTE_REST ///< no sound } watch_buzzer_note_t; +#define WATCH_BUZZER_PERIOD_REST 0 + +typedef bool (*watch_buzzer_raw_source_t)(uint16_t position, void* userdata, uint16_t* period, uint16_t* duration); + /** @brief Returns true if either the buzzer or the LED driver is enabled. * @details Both the buzzer and the LED use the TCC peripheral to drive their behavior. This function returns true if that * peripheral is enabled. You can use this function to determine whether you need to call the watch_enable_leds or @@ -210,6 +214,51 @@ void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(v */ void watch_buzzer_play_sequence_with_volume(int8_t *note_sequence, void (*callback_on_end)(void), watch_buzzer_volume_t volume); +/** @brief Plays the given raw buzzer source function in a non-blocking way. + * + * @details This function plays audio data generated by a raw source callback function, + * allowing for precise control over buzzer timing and frequency. The raw source + * function is called repeatedly to generate audio samples, each containing a + * period and duration for the buzzer tone. + * Useful for applications such as chirpy, so that they won't need to allocate a + * long note sequence, and we will also take care of all the timing logic. + * + * @param raw_source Pointer to the callback function that generates raw buzzer data. + * The function should take a position parameter and return true if + * more data is available, false if end of sequence is reached. + * Parameters: + * - position: Current position in the audio sequence (0-based) + * - userdata: User-provided data passed through to the callback + * - period: Pointer to store the period (in microseconds) for the tone + * - duration: Pointer to store the duration (in microseconds) for the tone + * @param userdata Pointer to user data that will be passed to the raw_source callback + * @param callback_on_end A pointer to a callback function to be invoked when the sequence has finished playing. + */ +void watch_buzzer_play_raw_source(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end); + +/** @brief Plays the given raw buzzer source function in a non-blocking way. + * + * @details This function plays audio data generated by a raw source callback function, + * allowing for precise control over buzzer timing and frequency. The raw source + * function is called repeatedly to generate audio samples, each containing a + * period and duration for the buzzer tone. + * Useful for applications such as chirpy, so that they won't need to allocate a + * long note sequence, and we will also take care of all the timing logic. + * + * @param raw_source Pointer to the callback function that generates raw buzzer data. + * The function should take a position parameter and return true if + * more data is available, false if end of sequence is reached. + * Parameters: + * - position: Current position in the audio sequence (0-based) + * - userdata: User-provided data passed through to the callback + * - period: Pointer to store the period (in microseconds) for the tone + * - duration: Pointer to store the duration (in microseconds) for the tone + * @param userdata Pointer to user data that will be passed to the raw_source callback + * @param callback_on_end A pointer to a callback function to be invoked when the sequence has finished playing. + * @param volume either WATCH_BUZZER_VOLUME_SOFT or WATCH_BUZZER_VOLUME_LOUD + */ +void watch_buzzer_play_raw_source_with_volume(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end, watch_buzzer_volume_t volume); + /** @brief Aborts a playing sequence. */ void watch_buzzer_abort_sequence(void); diff --git a/watch-library/simulator/watch/watch_tcc.c b/watch-library/simulator/watch/watch_tcc.c index c7b1182d..1ca4bbf0 100644 --- a/watch-library/simulator/watch/watch_tcc.c +++ b/watch-library/simulator/watch/watch_tcc.c @@ -32,11 +32,14 @@ static volatile bool buzzer_enabled = false; static uint32_t buzzer_period; void cb_watch_buzzer_seq(void *userData); +void cb_watch_buzzer_raw_source(void *userData); static uint16_t _seq_position; static int8_t _tone_ticks, _repeat_counter; static volatile long _em_interval_id = 0; static int8_t *_sequence; +static watch_buzzer_raw_source_t _raw_source; +static void* _userdata; static uint8_t _volume; static void (*_cb_finished)(void); static watch_cb_t _cb_start_global = NULL; @@ -57,6 +60,10 @@ void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(v void watch_buzzer_play_sequence_with_volume(int8_t *note_sequence, void (*callback_on_end)(void), watch_buzzer_volume_t volume) { watch_buzzer_abort_sequence(); + // prepare buzzer + watch_enable_buzzer(); + watch_set_buzzer_off(); + _buzzer_is_active = true; if (_cb_start_global) { @@ -69,9 +76,6 @@ void watch_buzzer_play_sequence_with_volume(int8_t *note_sequence, void (*callba _seq_position = 0; _tone_ticks = 0; _repeat_counter = -1; - // prepare buzzer - watch_enable_buzzer(); - watch_set_buzzer_off(); // initiate 64 hz callback _em_interval_id = emscripten_set_interval(cb_watch_buzzer_seq, (double)(1000/64), (void *)NULL); } @@ -117,6 +121,64 @@ void cb_watch_buzzer_seq(void *userData) { } else _tone_ticks--; } +void watch_buzzer_play_raw_source(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end) { + watch_buzzer_play_raw_source_with_volume(raw_source, userdata, callback_on_end, WATCH_BUZZER_VOLUME_LOUD); +} + +void watch_buzzer_play_raw_source_with_volume(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end, watch_buzzer_volume_t volume) { + watch_buzzer_abort_sequence(); + + // prepare buzzer + watch_enable_buzzer(); + watch_set_buzzer_off(); + + _buzzer_is_active = true; + + if (_cb_start_global) { + _cb_start_global(); + } + + _raw_source = raw_source; + _userdata = userdata; + _cb_finished = callback_on_end; + _volume = volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25; + _seq_position = 0; + _tone_ticks = 0; + + // initiate 64 hz callback + _em_interval_id = emscripten_set_interval(cb_watch_buzzer_raw_source, (double)(1000/64), (void *)NULL); +} + +void cb_watch_buzzer_raw_source(void *userData) { + // callback for reading the note sequence + (void) userData; + uint16_t period; + uint16_t duration; + bool done; + + if (_tone_ticks == 0) { + done = _raw_source(_seq_position, _userdata, &period, &duration); + + if (done) { + // end the sequence + watch_buzzer_abort_sequence(); + } else { + if (period == WATCH_BUZZER_PERIOD_REST) { + watch_set_buzzer_off(); + } else { + watch_set_buzzer_period_and_duty_cycle(period, _volume); + watch_set_buzzer_on(); + } + + // set duration ticks and move to next tone + _tone_ticks = duration; + _seq_position += 1; + } + } else { + _tone_ticks--; + } +} + void watch_buzzer_abort_sequence(void) { // ends/aborts the sequence if (_em_interval_id) _em_interval_stop(); From 557a785b1a091e5bf04b848b7909497b98f3cc57 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Fri, 15 Aug 2025 00:54:48 -0400 Subject: [PATCH 14/53] Fix chirpy to work with rtc-counter32 --- watch-faces/io/chirpy_demo_face.c | 167 ++++++++++-------------------- 1 file changed, 54 insertions(+), 113 deletions(-) diff --git a/watch-faces/io/chirpy_demo_face.c b/watch-faces/io/chirpy_demo_face.c index 673da201..3a7ff300 100644 --- a/watch-faces/io/chirpy_demo_face.c +++ b/watch-faces/io/chirpy_demo_face.c @@ -47,9 +47,6 @@ typedef struct { // Selected program chirpy_demo_program_t program; - // Helps us handle 1/64 ticks during transmission; including countdown timer - chirpy_tick_state_t tick_state; - // Used by chirpy encoder during transmission chirpy_encoder_state_t encoder_state; @@ -150,46 +147,10 @@ static void _cdf_update_lcd(chirpy_demo_state_t *state) { } } -static void _cdf_quit_chirping(chirpy_demo_state_t *state) { - state->mode = CDM_CHOOSE; - watch_set_buzzer_off(); - watch_clear_indicator(WATCH_INDICATOR_BELL); - movement_request_tick_frequency(1); -} - -static void _cdf_scale_tick(void *context) { - chirpy_demo_state_t *state = (chirpy_demo_state_t *)context; - chirpy_tick_state_t *tick_state = &state->tick_state; - - // Scale goes in 200Hz increments from 700 Hz to 12.3 kHz -> 58 steps - if (tick_state->seq_pos == 58) { - _cdf_quit_chirping(state); - return; - } - uint32_t freq = 700 + tick_state->seq_pos * 200; - uint32_t period = 1000000 / freq; - watch_set_buzzer_period_and_duty_cycle(period, 25); - watch_set_buzzer_on(); - ++tick_state->seq_pos; -} - -static void _cdf_data_tick(void *context) { - chirpy_demo_state_t *state = (chirpy_demo_state_t *)context; - - uint8_t tone = chirpy_get_next_tone(&state->encoder_state); - // Transmission over? - if (tone == 255) { - _cdf_quit_chirping(state); - return; - } - uint16_t period = chirpy_get_tone_period(tone); - watch_set_buzzer_period_and_duty_cycle(period, 25); - watch_set_buzzer_on(); -} - static uint8_t *curr_data_ptr; static uint16_t curr_data_ix; static uint16_t curr_data_len; +static chirpy_demo_state_t *curr_state; static uint8_t _cdf_get_next_byte(uint8_t *next_byte) { if (curr_data_ix == curr_data_len) @@ -199,59 +160,60 @@ static uint8_t _cdf_get_next_byte(uint8_t *next_byte) { return 1; } -static void _cdf_countdown_tick(void *context) { - chirpy_demo_state_t *state = (chirpy_demo_state_t *)context; - chirpy_tick_state_t *tick_state = &state->tick_state; - - // Countdown over: start actual broadcast - if (tick_state->seq_pos == 8 * 3) { - tick_state->tick_compare = 3; - tick_state->tick_count = -1; - tick_state->seq_pos = 0; - // We'll be chirping out a scale - if (false) { // state->program == CDP_CLEAR) { - tick_state->tick_fun = _cdf_scale_tick; - } - // We'll be chirping out data - else { - // Set up the encoder - chirpy_init_encoder(&state->encoder_state, _cdf_get_next_byte); - tick_state->tick_fun = _cdf_data_tick; - // Set up the data - curr_data_ix = 0; - if (state->program == CDP_INFO_SHORT) { - curr_data_ptr = short_data; - curr_data_len = short_data_len; - } else if (state->program == CDP_INFO_LONG) { - curr_data_ptr = long_data_str; - curr_data_len = strlen((const char *)long_data_str); - } else if (state->program == CDP_INFO_NANOSEC) { - curr_data_ptr = activity_buffer; - curr_data_len = activity_buffer_size; - } - } - return; +static void _cdf_on_chirping_done(void) { + if (curr_state) { + curr_state->mode = CDM_CHOOSE; } - // Sound or turn off buzzer - if ((tick_state->seq_pos % 8) == 0) { - watch_set_buzzer_period_and_duty_cycle(NotePeriods[BUZZER_NOTE_A5], 25); - watch_set_buzzer_on(); - } else if ((tick_state->seq_pos % 8) == 1) { - watch_set_buzzer_off(); - } - ++tick_state->seq_pos; + watch_clear_indicator(WATCH_INDICATOR_BELL); } -static void _cdm_setup_chirp(chirpy_demo_state_t *state) { - // We want frequent callbacks from now on - movement_request_tick_frequency(64); +static bool _cdm_raw_source_fn(uint16_t position, void* userdata, uint16_t* period, uint16_t* duration) { + // Beep countdown + if (position < 6) { + if (position % 2) { + *period = WATCH_BUZZER_PERIOD_REST; + *duration = 56; + } else { + *period = NotePeriods[BUZZER_NOTE_A5]; + *duration = 8; + } + return false; + } + + chirpy_demo_state_t *state = (chirpy_demo_state_t *)userdata; + + uint8_t tone = chirpy_get_next_tone(&state->encoder_state); + // Transmission over? + if (tone == 255) { + return true; + } + + *period = chirpy_get_tone_period(tone); + *duration = 3; + + return false; +} + +static void _cdm_start_transmission(chirpy_demo_state_t *state) { watch_set_indicator(WATCH_INDICATOR_BELL); state->mode = CDM_CHIRPING; - // Set up tick state; start with countdown - state->tick_state.tick_count = -1; - state->tick_state.tick_compare = 8; - state->tick_state.seq_pos = 0; - state->tick_state.tick_fun = _cdf_countdown_tick; + + // Set up the data + curr_state = state; + curr_data_ix = 0; + if (state->program == CDP_INFO_SHORT) { + curr_data_ptr = short_data; + curr_data_len = short_data_len; + } else if (state->program == CDP_INFO_LONG) { + curr_data_ptr = long_data_str; + curr_data_len = strlen((const char *)long_data_str); + } else if (state->program == CDP_INFO_NANOSEC) { + curr_data_ptr = activity_buffer; + curr_data_len = activity_buffer_size; + } + + chirpy_init_encoder(&state->encoder_state, _cdf_get_next_byte); + watch_buzzer_play_raw_source(_cdm_raw_source_fn, state, _cdf_on_chirping_done); } bool chirpy_demo_face_loop(movement_event_t event, void *context) { @@ -261,12 +223,7 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) { case EVENT_ACTIVATE: _cdf_update_lcd(state); break; - case EVENT_MODE_BUTTON_UP: - // Do not exit face while we're chirping - if (state->mode != CDM_CHIRPING) { - movement_move_to_next_face(); - } - break; + case EVENT_LIGHT_BUTTON_DOWN: case EVENT_LIGHT_BUTTON_UP: // We don't do light. break; @@ -286,10 +243,6 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) { state->program = CDP_CLEAR; _cdf_update_lcd(state); } - // If chirping: stoppit - else if (state->mode == CDM_CHIRPING) { - _cdf_quit_chirping(state); - } break; case EVENT_ALARM_LONG_PRESS: // If in choose mode: start chirping @@ -299,16 +252,7 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) { movement_force_led_off(); movement_move_to_next_face(); } else { - _cdm_setup_chirp(state); - } - } - break; - case EVENT_TICK: - if (state->mode == CDM_CHIRPING) { - ++state->tick_state.tick_count; - if (state->tick_state.tick_count == state->tick_state.tick_compare) { - state->tick_state.tick_count = 0; - state->tick_state.tick_fun(context); + _cdm_start_transmission(state); } } break; @@ -318,14 +262,11 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) { movement_move_to_face(0); } default: + movement_default_loop_handler(event); break; } - // Return true if the watch can enter standby mode. False needed when chirping. - if (state->mode == CDM_CHIRPING) - return false; - else - return true; + return true; } void chirpy_demo_face_resign(void *context) { From bf9d89106f6f0db838b90627185a0fd17bd5cd13 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Fri, 15 Aug 2025 21:34:29 -0400 Subject: [PATCH 15/53] Fix LED flickering while the buzzer is playing --- watch-library/hardware/watch/watch_tcc.c | 37 +++++++++++++++++------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/watch-library/hardware/watch/watch_tcc.c b/watch-library/hardware/watch/watch_tcc.c index 8624ffcc..fa81d074 100644 --- a/watch-library/hardware/watch/watch_tcc.c +++ b/watch-library/hardware/watch/watch_tcc.c @@ -43,6 +43,9 @@ static watch_cb_t _cb_start_global = NULL; static watch_cb_t _cb_stop_global = NULL; static volatile bool _led_is_active = false; static volatile bool _buzzer_is_active = false; +static volatile uint8_t _current_led_color[3] = {0, 0, 0}; + +static void _watch_set_led_duty_cycle(uint32_t period, uint8_t red, uint8_t green, uint8_t blue); static void _tcc_write_RUNSTDBY(bool value) { // enables or disables RUNSTDBY of the tcc @@ -276,6 +279,11 @@ void watch_enable_buzzer(void) { void watch_set_buzzer_period_and_duty_cycle(uint32_t period, uint8_t duty) { tcc_set_period(0, period, true); tcc_set_cc(0, (WATCH_BUZZER_TCC_CHANNEL) % 4, period / (100 / duty), true); + // The buzzer determines the period, which means that if the LED was active before it will flicker + // Update the LED duty cycle to match the new period required by the buzzer. + if (_led_is_active) { + _watch_set_led_duty_cycle(period, _current_led_color[0], _current_led_color[1], _current_led_color[2]); + } } void watch_disable_buzzer(void) { @@ -400,10 +408,27 @@ void watch_set_led_color(uint8_t red, uint8_t green) { #endif } +static void _watch_set_led_duty_cycle(uint32_t period, uint8_t red, uint8_t green, uint8_t blue) { + tcc_set_cc(0, (WATCH_RED_TCC_CHANNEL) % 4, ((period * (uint32_t)red * 1000ull) / 255000ull), true); +#ifdef WATCH_GREEN_TCC_CHANNEL + tcc_set_cc(0, (WATCH_GREEN_TCC_CHANNEL) % 4, ((period * (uint32_t)green * 1000ull) / 255000ull), true); +#else + (void) green; // silence warning +#endif +#ifdef WATCH_BLUE_TCC_CHANNEL + tcc_set_cc(0, (WATCH_BLUE_TCC_CHANNEL) % 4, ((period * (uint32_t)blue * 1000ull) / 255000ull), true); +#else + (void) blue; // silence warning +#endif +} + void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) { bool turning_on = (red | green | blue) != 0; if (turning_on) { + _current_led_color[0] = red; + _current_led_color[1] = green; + _current_led_color[2] = blue; _led_is_active = true; watch_enable_buzzer_and_leds(); } else { @@ -412,17 +437,7 @@ void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) { if (tcc_is_enabled(0)) { uint32_t period = tcc_get_period(0); - tcc_set_cc(0, (WATCH_RED_TCC_CHANNEL) % 4, ((period * (uint32_t)red * 1000ull) / 255000ull), true); -#ifdef WATCH_GREEN_TCC_CHANNEL - tcc_set_cc(0, (WATCH_GREEN_TCC_CHANNEL) % 4, ((period * (uint32_t)green * 1000ull) / 255000ull), true); -#else - (void) green; // silence warning -#endif -#ifdef WATCH_BLUE_TCC_CHANNEL - tcc_set_cc(0, (WATCH_BLUE_TCC_CHANNEL) % 4, ((period * (uint32_t)blue * 1000ull) / 255000ull), true); -#else - (void) blue; // silence warning -#endif + _watch_set_led_duty_cycle(period, red, green, blue); } if (!turning_on) { From 9d91ff3090bd6008afbd78261d824681998ff0f5 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Wed, 6 Aug 2025 21:29:44 -0400 Subject: [PATCH 16/53] Add an optional implementation of button debounce --- movement.c | 15 +++++++++++++++ movement_config.h | 6 ++++++ 2 files changed, 21 insertions(+) diff --git a/movement.c b/movement.c index 49df5023..aa90f034 100644 --- a/movement.c +++ b/movement.c @@ -67,6 +67,9 @@ typedef struct { movement_timeout_index_t timeout_index; volatile bool is_down; volatile rtc_counter_t down_timestamp; +#if MOVEMENT_DEBOUNCE_TICKS + volatile rtc_counter_t up_timestamp; +#endif } movement_button_t; /* Pieces of state that can be modified by the various interrupt callbacks. @@ -1270,12 +1273,24 @@ static movement_event_type_t _process_button_event(bool pin_level, movement_butt uint32_t counter = watch_rtc_get_counter(); +#if MOVEMENT_DEBOUNCE_TICKS + if ( + (counter - button->up_timestamp) <= MOVEMENT_DEBOUNCE_TICKS && + (counter - button->down_timestamp) <= MOVEMENT_DEBOUNCE_TICKS + ) { + return event_type; + } +#endif + button->is_down = pin_level; if (pin_level) { button->down_timestamp = counter; event_type = button->down_event; } else { +#if MOVEMENT_DEBOUNCE_TICKS + button->up_timestamp = counter; +#endif if ((counter - button->down_timestamp) >= MOVEMENT_LONG_PRESS_TICKS) { event_type = button->down_event + 3; } else { diff --git a/movement_config.h b/movement_config.h index f5f6b2b4..9633de87 100644 --- a/movement_config.h +++ b/movement_config.h @@ -101,4 +101,10 @@ const watch_face_t watch_faces[] = { */ #define MOVEMENT_DEFAULT_LED_DURATION 1 +/* Optionally debounce button presses (disable by default). + * A value of 4 is a good starting point if you have issues + * with multiple button presses firing. +*/ +#define MOVEMENT_DEBOUNCE_TICKS 0 + #endif // MOVEMENT_CONFIG_H_ From 6fe1b236a4854c0062930f52e27433e4242e9db4 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Fri, 15 Aug 2025 22:50:10 -0400 Subject: [PATCH 17/53] Minor improvement to movement_force_led_on logic --- movement.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/movement.c b/movement.c index aa90f034..f52050e0 100644 --- a/movement.c +++ b/movement.c @@ -400,8 +400,8 @@ void movement_force_led_on(uint8_t red, uint8_t green, uint8_t blue) { // this is hacky, we need a way for watch faces to set an arbitrary color and prevent Movement from turning it right back off. movement_state.light_on = true; watch_set_led_color_rgb(red, green, blue); - rtc_counter_t counter = watch_rtc_get_counter(); - watch_rtc_register_comp_callback_no_schedule(cb_led_timeout_interrupt, counter + 32767, LED_TIMEOUT); + // The led will stay on until movement_force_led_off is called, so disable the led timeout in case we were in the middle of it. + watch_rtc_disable_comp_callback_no_schedule(LED_TIMEOUT); movement_volatile_state.schedule_next_comp = true; } From 1b9624d042dc241ee08c9d290e8b199420183f93 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Sat, 26 Jul 2025 22:31:53 -0400 Subject: [PATCH 18/53] Make fast_stopwatch power efficiend by using the new counter32 --- .../complication/fast_stopwatch_face.c | 412 ++++++++---------- .../complication/fast_stopwatch_face.h | 11 +- 2 files changed, 193 insertions(+), 230 deletions(-) diff --git a/watch-faces/complication/fast_stopwatch_face.c b/watch-faces/complication/fast_stopwatch_face.c index ce998102..a4af13f3 100644 --- a/watch-faces/complication/fast_stopwatch_face.c +++ b/watch-faces/complication/fast_stopwatch_face.c @@ -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 -#include -#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); } diff --git a/watch-faces/complication/fast_stopwatch_face.h b/watch-faces/complication/fast_stopwatch_face.h index eac56651..d99f6085 100644 --- a/watch-faces/complication/fast_stopwatch_face.h +++ b/watch-faces/complication/fast_stopwatch_face.h @@ -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, \ From 04276c1999e3db14ee0aebc680cef38a1bab0ba9 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Sun, 17 Aug 2025 23:14:25 -0400 Subject: [PATCH 19/53] Improve watch_tcc to decouple leds and buzzer as much as allowed As a positive side effect, the led will stop emitting the faintiest of blinks when the buzzer starts playing. --- .../hardware/watch/watch_deepsleep.c | 3 +- watch-library/hardware/watch/watch_tcc.c | 127 ++++++++++-------- watch-library/shared/watch/watch_tcc.h | 46 +------ watch-library/simulator/watch/watch.c | 4 - watch-library/simulator/watch/watch_private.c | 2 - watch-library/simulator/watch/watch_tcc.c | 2 - 6 files changed, 76 insertions(+), 108 deletions(-) diff --git a/watch-library/hardware/watch/watch_deepsleep.c b/watch-library/hardware/watch/watch_deepsleep.c index e90150ee..20254df4 100644 --- a/watch-library/hardware/watch/watch_deepsleep.c +++ b/watch-library/hardware/watch/watch_deepsleep.c @@ -151,7 +151,8 @@ static void _watch_disable_all_pins_except_rtc(void) { } static void _watch_disable_all_peripherals_except_slcd(void) { - _watch_disable_tcc(); + watch_disable_leds(); + watch_disable_buzzer(); watch_disable_adc(); watch_disable_external_interrupts(); diff --git a/watch-library/hardware/watch/watch_tcc.c b/watch-library/hardware/watch/watch_tcc.c index fa81d074..cde3c0dd 100644 --- a/watch-library/hardware/watch/watch_tcc.c +++ b/watch-library/hardware/watch/watch_tcc.c @@ -27,10 +27,15 @@ #include "tcc.h" #include "tc.h" -void _watch_enable_tcc(void); +static void _watch_enable_tcc(void); +static void _watch_disable_tcc(void); +static void _watch_maybe_enable_tcc(void); +static void _watch_maybe_disable_tcc(void); +static void _watch_enable_led_pins(void); +static void _watch_disable_led_pins(void); static void (*_cb_tc0)(void) = NULL; -void cb_watch_buzzer_seq(void); -void cb_watch_buzzer_raw_source(void); +static void cb_watch_buzzer_seq(void); +static void cb_watch_buzzer_raw_source(void); static uint16_t _seq_position; static int8_t _tone_ticks, _repeat_counter; @@ -84,14 +89,11 @@ void watch_buzzer_play_sequence_with_volume(int8_t *note_sequence, void (*callba // Abort any previous sequence watch_buzzer_abort_sequence(); - _buzzer_is_active = true; - if (_cb_start_global) { _cb_start_global(); } - watch_enable_buzzer_and_leds(); - + watch_enable_buzzer(); watch_set_buzzer_off(); _sequence = note_sequence; _cb_finished = callback_on_end; @@ -154,13 +156,11 @@ void watch_buzzer_play_raw_source_with_volume(watch_buzzer_raw_source_t raw_sour // Abort any previous sequence watch_buzzer_abort_sequence(); - _buzzer_is_active = true; - if (_cb_start_global) { _cb_start_global(); } - watch_enable_buzzer_and_leds(); + watch_enable_buzzer(); watch_set_buzzer_off(); _raw_source = raw_source; @@ -213,14 +213,12 @@ void watch_buzzer_abort_sequence(void) { return; } - _buzzer_is_active = false; - _tc0_stop(); watch_set_buzzer_off(); // disable TCC - watch_maybe_disable_buzzer_and_leds(); + watch_disable_buzzer(); if (_cb_stop_global) { _cb_stop_global(); @@ -244,11 +242,11 @@ void irq_handler_tc0(void) { TC0->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF; } -bool watch_is_buzzer_or_led_enabled(void){ - return tcc_is_enabled(0); -} +void _watch_maybe_enable_tcc(void) { + if (!_buzzer_is_active && !_led_is_active) { + return; + } -void watch_enable_buzzer_and_leds(void) { if (!tcc_is_enabled(0)) { // tcc_set_run_in_standby(0, true); _watch_enable_tcc(); @@ -257,23 +255,26 @@ void watch_enable_buzzer_and_leds(void) { } } -void watch_disable_buzzer_and_leds(void) { +void _watch_maybe_disable_tcc(void) { + if (_buzzer_is_active || _led_is_active) { + return; + } + if (tcc_is_enabled(0)) { _tcc_write_RUNSTDBY(false); _watch_disable_tcc(); } } -void watch_maybe_disable_buzzer_and_leds(void) { - if (_buzzer_is_active || _led_is_active) { - return; - } - - watch_disable_buzzer_and_leds(); +void watch_enable_buzzer(void) { + _buzzer_is_active = true; + _watch_maybe_enable_tcc(); } -void watch_enable_buzzer(void) { - watch_enable_buzzer_and_leds(); +void watch_disable_buzzer(void) { + _buzzer_is_active = false; + watch_set_buzzer_off(); + _watch_maybe_disable_tcc(); } void watch_set_buzzer_period_and_duty_cycle(uint32_t period, uint8_t duty) { @@ -286,10 +287,6 @@ void watch_set_buzzer_period_and_duty_cycle(uint32_t period, uint8_t duty) { } } -void watch_disable_buzzer(void) { - watch_maybe_disable_buzzer_and_leds(); -} - inline void watch_set_buzzer_on(void) { HAL_GPIO_BUZZER_out(); HAL_GPIO_BUZZER_pmuxen(HAL_GPIO_PMUX_TCC_ALT); @@ -356,21 +353,6 @@ void _watch_enable_tcc(void) { tcc_set_cc(0, (WATCH_BLUE_TCC_CHANNEL) % 4, 0, false); #endif - // enable LED PWM pins (the LED driver assumes if the TCC is on, the pins are enabled) - HAL_GPIO_RED_pmuxen(HAL_GPIO_PMUX_TCC_ALT); - HAL_GPIO_RED_drvstr(1); - HAL_GPIO_RED_out(); -#ifdef WATCH_GREEN_TCC_CHANNEL - HAL_GPIO_GREEN_pmuxen(HAL_GPIO_PMUX_TCC_ALT); - HAL_GPIO_GREEN_drvstr(1); - HAL_GPIO_GREEN_out(); -#endif -#ifdef WATCH_BLUE_TCC_CHANNEL - HAL_GPIO_BLUE_pmuxen(HAL_GPIO_PMUX_TCC_ALT); - HAL_GPIO_BLUE_drvstr(1); - HAL_GPIO_BLUE_out(); -#endif - // Enable the TCC tcc_enable(0); } @@ -393,11 +375,45 @@ void _watch_disable_tcc(void) { } void watch_enable_leds(void) { - watch_enable_buzzer_and_leds(); + _led_is_active = true; + _watch_enable_led_pins(); + _watch_maybe_enable_tcc(); } void watch_disable_leds(void) { - watch_maybe_disable_buzzer_and_leds(); + _led_is_active = false; + _watch_disable_led_pins(); + _watch_maybe_disable_tcc(); +} + +void _watch_enable_led_pins(void) { + // enable LED PWM pins (the LED driver assumes if the TCC is on, the pins are enabled) + HAL_GPIO_RED_pmuxen(HAL_GPIO_PMUX_TCC_ALT); + HAL_GPIO_RED_drvstr(1); + HAL_GPIO_RED_out(); +#ifdef WATCH_GREEN_TCC_CHANNEL + HAL_GPIO_GREEN_pmuxen(HAL_GPIO_PMUX_TCC_ALT); + HAL_GPIO_GREEN_drvstr(1); + HAL_GPIO_GREEN_out(); +#endif +#ifdef WATCH_BLUE_TCC_CHANNEL + HAL_GPIO_BLUE_pmuxen(HAL_GPIO_PMUX_TCC_ALT); + HAL_GPIO_BLUE_drvstr(1); + HAL_GPIO_BLUE_out(); +#endif +} + +void _watch_disable_led_pins(void) { + HAL_GPIO_RED_pmuxdis(); + HAL_GPIO_RED_off(); +#ifdef WATCH_GREEN_TCC_CHANNEL + HAL_GPIO_GREEN_pmuxdis(); + HAL_GPIO_GREEN_off(); +#endif +#ifdef WATCH_BLUE_TCC_CHANNEL + HAL_GPIO_BLUE_pmuxdis(); + HAL_GPIO_BLUE_off(); +#endif } void watch_set_led_color(uint8_t red, uint8_t green) { @@ -429,19 +445,14 @@ void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) { _current_led_color[0] = red; _current_led_color[1] = green; _current_led_color[2] = blue; - _led_is_active = true; - watch_enable_buzzer_and_leds(); - } else { - _led_is_active = false; - } - - if (tcc_is_enabled(0)) { + watch_enable_leds(); uint32_t period = tcc_get_period(0); _watch_set_led_duty_cycle(period, red, green, blue); - } - - if (!turning_on) { - watch_maybe_disable_buzzer_and_leds(); + } else { + if (tcc_is_enabled(0)) { + _watch_set_led_duty_cycle(1, red, green, blue); + } + watch_disable_leds(); } } diff --git a/watch-library/shared/watch/watch_tcc.h b/watch-library/shared/watch/watch_tcc.h index e87e53bf..056844b3 100644 --- a/watch-library/shared/watch/watch_tcc.h +++ b/watch-library/shared/watch/watch_tcc.h @@ -130,13 +130,6 @@ typedef enum { typedef bool (*watch_buzzer_raw_source_t)(uint16_t position, void* userdata, uint16_t* period, uint16_t* duration); -/** @brief Returns true if either the buzzer or the LED driver is enabled. - * @details Both the buzzer and the LED use the TCC peripheral to drive their behavior. This function returns true if that - * peripheral is enabled. You can use this function to determine whether you need to call the watch_enable_leds or - * or watch_enable_buzzer functions before using these peripherals. - */ -bool watch_is_buzzer_or_led_enabled(void); - /** @addtogroup tcc Buzzer and LED Control (via the TCC peripheral) * @brief This section covers functions related to Timer Counter for Control peripheral, which drives the piezo buzzer * embedded in the F-91W's back plate as well as the LED that backlights the display. @@ -155,15 +148,13 @@ void watch_enable_buzzer(void); */ void watch_set_buzzer_period_and_duty_cycle(uint32_t period, uint8_t duty); -/** @brief Disables the TCC peripheral that drives the buzzer. - * @note If you are using PWM to set custom LED colors, this method will also disable the LED PWM driver, - * since the buzzer and LED both make use of the same peripheral to drive their PWM behavior. +/** @brief Disables the TCC peripheral that drives the buzzer (if LED not active). */ void watch_disable_buzzer(void); /** @brief Turns the buzzer output on. It will emit a continuous sound at the given frequency. - * @note The TCC peripheral that drives the buzzer does not run in standby mode; if you wish for buzzer - * output to continue, you should prevent your app from going to sleep. + * @note The TCC peripheral that drives the buzzer does run in standby mode; if you wish for buzzer + * output to continue, you don't need to prevent your app from going to sleep. */ void watch_set_buzzer_on(void); @@ -265,18 +256,6 @@ void watch_buzzer_abort_sequence(void); void watch_buzzer_register_global_callbacks(watch_cb_t cb_start, watch_cb_t cb_stop); -/** @brief Enables the TCC peripheral, which drives the buzzer and the leds. -*/ -void watch_enable_buzzer_and_leds(void); - -/** @brief Disables the TCC peripheral that drives the buzzer and the leds. - */ -void watch_disable_buzzer_and_leds(void); - -/** @brief Disables the TCC peripheral that drives the buzzer and the leds if neither is currently active - */ -void watch_maybe_disable_buzzer_and_leds(void); - #ifndef __EMSCRIPTEN__ void irq_handler_tc0(void); #endif @@ -294,26 +273,17 @@ void irq_handler_tc0(void); * so that watch_set_led_red sets the red LED, and watch_set_led_green sets the blue one. */ /// @{ -/** @brief Enables the bi-color LED. - * @note The TCC peripheral that drives the LEDs does not run in STANDBY mode — but the outputs do! This - * means that if you set either red, green or both LEDs to full power, they will shine even when - * your app is asleep. If, however, you set a custom color using watch_set_led_color, the color will - * not display correctly in STANDBY mode. You will need to keep your app running while the LED is on. +/** @brief Enables the TCC peripheral, which drives the LEDs. */ void watch_enable_leds(void); -/** @brief Disables the LEDs. - * @note This method will also disable the buzzer, since the buzzer and LED both make use of the same - * peripheral to drive their PWM behavior. +/** @brief Disables the TCC peripheral that drives the LEDs (if buzzer not active). */ void watch_disable_leds(void); /** @brief Sets the LED to a custom color by modulating each output's duty cycle. * @param red The red value from 0-255. * @param green The green value from 0-255. If your watch has a red/blue LED, this will be the blue value. - * @note If you are displaying a custom color, you will need to prevent your app from going to sleep - * while the LED is on; otherwise, the color will not display correctly. You can do this by - * returning false in your app_loop method. */ void watch_set_led_color(uint8_t red, uint8_t green); @@ -321,9 +291,6 @@ void watch_set_led_color(uint8_t red, uint8_t green); * @param red The red value from 0-255. * @param green The green value from 0-255. * @param blue The blue value from 0-255. - * @note If you are displaying a custom color, you will need to prevent your app from going to sleep - * while the LED is on; otherwise, the color will not display correctly. You can do this by - * returning false in your app_loop method. */ void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue); @@ -348,9 +315,6 @@ void watch_set_led_yellow(void); /** @brief Turns both the red and the green LEDs off. */ void watch_set_led_off(void); -/** @brief Disables the TCC peripheral. Should only be called internally. */ -void _watch_disable_tcc(void); - /// @brief An array of periods for all the notes on a piano, corresponding to the names in watch_buzzer_note_t. extern const uint16_t NotePeriods[108]; diff --git a/watch-library/simulator/watch/watch.c b/watch-library/simulator/watch/watch.c index 4e89f3d3..7584cd38 100644 --- a/watch-library/simulator/watch/watch.c +++ b/watch-library/simulator/watch/watch.c @@ -1,9 +1,5 @@ #include "watch.h" -bool watch_is_buzzer_or_led_enabled(void) { - return false; -} - bool watch_is_usb_enabled(void) { return true; } diff --git a/watch-library/simulator/watch/watch_private.c b/watch-library/simulator/watch/watch_private.c index 6f97490b..b94fbf12 100644 --- a/watch-library/simulator/watch/watch_private.c +++ b/watch-library/simulator/watch/watch_private.c @@ -51,8 +51,6 @@ int _gettimeofday(struct timeval *tv, void *tzvp) { return 0; } -void _watch_disable_tcc(void) {} - void _watch_enable_usb(void) {} void watch_disable_TRNG() {} diff --git a/watch-library/simulator/watch/watch_tcc.c b/watch-library/simulator/watch/watch_tcc.c index 1ca4bbf0..6cbf9822 100644 --- a/watch-library/simulator/watch/watch_tcc.c +++ b/watch-library/simulator/watch/watch_tcc.c @@ -46,8 +46,6 @@ static watch_cb_t _cb_start_global = NULL; static watch_cb_t _cb_stop_global = NULL; static volatile bool _buzzer_is_active = false; -void _watch_enable_tcc(void) {} - static inline void _em_interval_stop() { emscripten_clear_interval(_em_interval_id); _em_interval_id = 0; From cb05585c156efd3ff64cea0aeca844cc6c0cd02f Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Sun, 17 Aug 2025 23:35:15 -0400 Subject: [PATCH 20/53] Fix faces that relied on watch_buzzer_play_note being blocking --- .../complication/advanced_alarm_face.c | 22 ++++++++-------- watch-faces/complication/deadline_face.c | 24 ++++++++++++----- watch-faces/complication/simon_face.c | 26 +++++++------------ watch-faces/complication/tally_face.c | 22 +++++++++++----- 4 files changed, 53 insertions(+), 41 deletions(-) diff --git a/watch-faces/complication/advanced_alarm_face.c b/watch-faces/complication/advanced_alarm_face.c index 16bfdbce..4e5d9eb9 100644 --- a/watch-faces/complication/advanced_alarm_face.c +++ b/watch-faces/complication/advanced_alarm_face.c @@ -202,9 +202,16 @@ static void _alarm_update_alarm_enabled(alarm_state_t *state) { static void _alarm_play_short_beep(uint8_t pitch_idx) { // play a short double beep - watch_buzzer_play_note(_buzzer_notes[pitch_idx], 50); - watch_buzzer_play_note(BUZZER_NOTE_REST, 50); - watch_buzzer_play_note(_buzzer_notes[pitch_idx], 70); + static int8_t beep_sequence[] = { + 0, 4, + BUZZER_NOTE_REST, 4, + 0, 6, + 0 + }; + beep_sequence[0] = _buzzer_notes[pitch_idx]; + beep_sequence[4] = _buzzer_notes[pitch_idx]; + + movement_play_sequence(beep_sequence, 0); } static void _alarm_indicate_beep(alarm_state_t *state) { @@ -437,14 +444,7 @@ bool advanced_alarm_face_loop(movement_event_t event, void *context) { // play alarm if (state->alarm[state->alarm_playing_idx].beeps == 0) { // short beep - if (watch_is_buzzer_or_led_enabled()) { - _alarm_play_short_beep(state->alarm[state->alarm_playing_idx].pitch); - } else { - // enable, play beep and disable buzzer again - watch_enable_buzzer(); - _alarm_play_short_beep(state->alarm[state->alarm_playing_idx].pitch); - watch_disable_buzzer(); - } + _alarm_play_short_beep(state->alarm[state->alarm_playing_idx].pitch); } else { // regular alarm beeps movement_play_alarm_beeps((state->alarm[state->alarm_playing_idx].beeps == (ALARM_MAX_BEEP_ROUNDS - 1) ? 20 : state->alarm[state->alarm_playing_idx].beeps), diff --git a/watch-faces/complication/deadline_face.c b/watch-faces/complication/deadline_face.c index b145a618..ebad0e8b 100644 --- a/watch-faces/complication/deadline_face.c +++ b/watch-faces/complication/deadline_face.c @@ -82,26 +82,36 @@ static inline int _days_in_month(int16_t month, int16_t year) /* Play beep sound based on type */ static inline void _beep(beep_type_t beep_type) { + static int8_t beep_sequence[] = { + 0, 4, + 0, 6, + 0, 6, + 0 + }; + if (!movement_button_should_sound()) return; switch (beep_type) { case BEEP_BUTTON: - watch_buzzer_play_note_with_volume(BUZZER_NOTE_C7, 50, movement_button_volume()); + beep_sequence[0] = BUZZER_NOTE_C7; + beep_sequence[2] = 0; break; case BEEP_ENABLE: - watch_buzzer_play_note_with_volume(BUZZER_NOTE_G7, 50, movement_button_volume()); - watch_buzzer_play_note(BUZZER_NOTE_REST, 75); - watch_buzzer_play_note_with_volume(BUZZER_NOTE_C8, 50, movement_button_volume()); + beep_sequence[0] = BUZZER_NOTE_G7; + beep_sequence[2] = BUZZER_NOTE_REST; + beep_sequence[4] = BUZZER_NOTE_C8; break; case BEEP_DISABLE: - watch_buzzer_play_note_with_volume(BUZZER_NOTE_C8, 50, movement_button_volume()); - watch_buzzer_play_note(BUZZER_NOTE_REST, 75); - watch_buzzer_play_note_with_volume(BUZZER_NOTE_G7, 50, movement_button_volume()); + beep_sequence[0] = BUZZER_NOTE_C8; + beep_sequence[2] = BUZZER_NOTE_REST; + beep_sequence[4] = BUZZER_NOTE_G7; break; } + + movement_play_sequence(beep_sequence, 0); } /* Change tick frequency */ diff --git a/watch-faces/complication/simon_face.c b/watch-faces/complication/simon_face.c index 642b32a2..7b54f12f 100644 --- a/watch-faces/complication/simon_face.c +++ b/watch-faces/complication/simon_face.c @@ -119,30 +119,22 @@ static void _simon_play_note(SimonNote note, simon_state_t *state, bool skip_res switch (note) { case SIMON_LED_NOTE: if (!state->lightOff) watch_set_led_yellow(); - if (state->soundOff) - delay_ms(_delay_beep); - else - watch_buzzer_play_note(BUZZER_NOTE_D3, _delay_beep); + if (!state->soundOff) watch_buzzer_play_note(BUZZER_NOTE_D3, _delay_beep); + delay_ms(_delay_beep); break; case SIMON_MODE_NOTE: if (!state->lightOff) watch_set_led_red(); - if (state->soundOff) - delay_ms(_delay_beep); - else - watch_buzzer_play_note(BUZZER_NOTE_E4, _delay_beep); + if (!state->soundOff) watch_buzzer_play_note(BUZZER_NOTE_E4, _delay_beep); + delay_ms(_delay_beep); break; case SIMON_ALARM_NOTE: if (!state->lightOff) watch_set_led_green(); - if (state->soundOff) - delay_ms(_delay_beep); - else - watch_buzzer_play_note(BUZZER_NOTE_C3, _delay_beep); + if (!state->soundOff) watch_buzzer_play_note(BUZZER_NOTE_C3, _delay_beep); + delay_ms(_delay_beep); break; case SIMON_WRONG_NOTE: - if (state->soundOff) - delay_ms(800); - else - watch_buzzer_play_note(BUZZER_NOTE_A1, 800); + if (!state->soundOff) watch_buzzer_play_note(BUZZER_NOTE_A1, 800); + delay_ms(800); break; } watch_set_led_off(); @@ -150,7 +142,7 @@ static void _simon_play_note(SimonNote note, simon_state_t *state, bool skip_res if (note != SIMON_WRONG_NOTE) { _simon_clear_display(state); if (!skip_rest) { - watch_buzzer_play_note(BUZZER_NOTE_REST, (_delay_beep * 2)/3); + delay_ms((_delay_beep * 2)/3); } } } diff --git a/watch-faces/complication/tally_face.c b/watch-faces/complication/tally_face.c index deb5b878..ec217954 100644 --- a/watch-faces/complication/tally_face.c +++ b/watch-faces/complication/tally_face.c @@ -110,6 +110,12 @@ static bool tally_face_should_move_back(tally_state_t *state) { bool tally_face_loop(movement_event_t event, void *context) { tally_state_t *state = (tally_state_t *)context; static bool using_led = false; + static int8_t beep_sequence[] = { + 0, 2, + BUZZER_NOTE_REST, 3, + 0, 2, + 0 + }; if (using_led) { if(!HAL_GPIO_BTN_MODE_read() && !HAL_GPIO_BTN_LIGHT_read() && !HAL_GPIO_BTN_ALARM_read()) @@ -148,9 +154,11 @@ bool tally_face_loop(movement_event_t event, void *context) { state->tally_idx = _tally_default[state->tally_default_idx]; // reset tally index _init_val = true; //play a reset tune - if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_G6, 30); - if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_REST, 30); - if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_E6, 30); + if (movement_button_should_sound()) { + beep_sequence[0] = BUZZER_NOTE_G6; + beep_sequence[4] = BUZZER_NOTE_E6; + movement_play_sequence(beep_sequence, 0); + } print_tally(state, movement_button_should_sound()); } break; @@ -168,9 +176,11 @@ bool tally_face_loop(movement_event_t event, void *context) { if (TALLY_FACE_PRESETS_SIZE() > 1 && _init_val){ state->tally_default_idx = (state->tally_default_idx + 1) % TALLY_FACE_PRESETS_SIZE(); state->tally_idx = _tally_default[state->tally_default_idx]; - if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_E6, 30); - if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_REST, 30); - if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_G6, 30); + if (movement_button_should_sound()) { + beep_sequence[0] = BUZZER_NOTE_E6; + beep_sequence[4] = BUZZER_NOTE_G6; + movement_play_sequence(beep_sequence, 0); + } print_tally(state, movement_button_should_sound()); } else{ From f534e7c202f08c0b666c78dd2fd5295cbec81bcd Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Tue, 19 Aug 2025 00:00:51 -0400 Subject: [PATCH 21/53] Fix bug that was causing notes in a sequence to play too long --- movement.c | 16 ++++++++-------- watch-library/hardware/watch/watch_tcc.c | 12 ++++++------ watch-library/simulator/watch/watch_tcc.c | 8 ++++---- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/movement.c b/movement.c index f52050e0..d13146e9 100644 --- a/movement.c +++ b/movement.c @@ -103,13 +103,13 @@ static int8_t *_pending_sequence; // The note sequence of the default alarm int8_t alarm_tune[] = { BUZZER_NOTE_C8, 4, - BUZZER_NOTE_REST, 4, + BUZZER_NOTE_REST, 7, BUZZER_NOTE_C8, 4, - BUZZER_NOTE_REST, 4, + BUZZER_NOTE_REST, 7, BUZZER_NOTE_C8, 4, - BUZZER_NOTE_REST, 4, - BUZZER_NOTE_C8, 6, - BUZZER_NOTE_REST, 18, + BUZZER_NOTE_REST, 7, + BUZZER_NOTE_C8, 4, + BUZZER_NOTE_REST, 27, -8, 9, 0 }; @@ -505,9 +505,9 @@ void movement_play_note(watch_buzzer_note_t note, uint16_t duration_ms) { static int8_t single_note_sequence[3]; single_note_sequence[0] = note; - // 48 ticks per second for the tc0? - // Each tick is approximately 20ms - uint16_t duration = duration_ms / 20; + // 64 ticks per second for the tc0 + // Each tick is approximately 15ms + uint16_t duration = duration_ms / 15; if (duration > 127) duration = 127; single_note_sequence[1] = (int8_t)duration; single_note_sequence[2] = 0; diff --git a/watch-library/hardware/watch/watch_tcc.c b/watch-library/hardware/watch/watch_tcc.c index cde3c0dd..07f3126c 100644 --- a/watch-library/hardware/watch/watch_tcc.c +++ b/watch-library/hardware/watch/watch_tcc.c @@ -139,7 +139,7 @@ void cb_watch_buzzer_seq(void) { watch_set_buzzer_on(); } else watch_set_buzzer_off(); // set duration ticks and move to next tone - _tone_ticks = _sequence[_seq_position + 1]; + _tone_ticks = _sequence[_seq_position + 1] - 1; _seq_position += 2; } else { // end the sequence @@ -187,7 +187,7 @@ void cb_watch_buzzer_raw_source(void) { if (_tone_ticks == 0) { done = _raw_source(_seq_position, _userdata, &period, &duration); - if (done) { + if (done || duration == 0) { // end the sequence watch_buzzer_abort_sequence(); } else { @@ -199,7 +199,7 @@ void cb_watch_buzzer_raw_source(void) { } // set duration ticks and move to next tone - _tone_ticks = duration; + _tone_ticks = duration - 1; _seq_position += 1; } } else { @@ -305,9 +305,9 @@ void watch_buzzer_play_note_with_volume(watch_buzzer_note_t note, uint16_t durat static int8_t single_note_sequence[3]; single_note_sequence[0] = note; - // 48 ticks per second for the tc0? - // Each tick is approximately 20ms - uint16_t duration = duration_ms / 20; + // 64 ticks per second for the tc0 + // Each tick is approximately 15ms + uint16_t duration = duration_ms / 15; if (duration > 127) duration = 127; single_note_sequence[1] = (int8_t)duration; single_note_sequence[2] = 0; diff --git a/watch-library/simulator/watch/watch_tcc.c b/watch-library/simulator/watch/watch_tcc.c index 6cbf9822..e90b7613 100644 --- a/watch-library/simulator/watch/watch_tcc.c +++ b/watch-library/simulator/watch/watch_tcc.c @@ -110,7 +110,7 @@ void cb_watch_buzzer_seq(void *userData) { watch_set_buzzer_on(); } // set duration ticks and move to next tone - _tone_ticks = _sequence[_seq_position + 1]; + _tone_ticks = _sequence[_seq_position + 1] - 1; _seq_position += 2; } else { // end the sequence @@ -157,7 +157,7 @@ void cb_watch_buzzer_raw_source(void *userData) { if (_tone_ticks == 0) { done = _raw_source(_seq_position, _userdata, &period, &duration); - if (done) { + if (done || duration == 0) { // end the sequence watch_buzzer_abort_sequence(); } else { @@ -169,7 +169,7 @@ void cb_watch_buzzer_raw_source(void *userData) { } // set duration ticks and move to next tone - _tone_ticks = duration; + _tone_ticks = duration - 1; _seq_position += 1; } } else { @@ -272,7 +272,7 @@ void watch_buzzer_play_note_with_volume(watch_buzzer_note_t note, uint16_t durat static int8_t single_note_sequence[3]; single_note_sequence[0] = note; - // 64 ticks per second for the tc0? + // 64 ticks per second for the tc0 // Each tick is approximately 15ms uint16_t duration = duration_ms / 15; if (duration > 127) duration = 127; From 996d5d6679618e309a117437606bcec4e4365315 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Tue, 26 Aug 2025 21:44:00 -0400 Subject: [PATCH 22/53] Convert from timestamp to date_time once per second at most --- movement.c | 13 ++++++++++++- watch-library/hardware/watch/watch_rtc.c | 14 +++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/movement.c b/movement.c index d13146e9..bc8b12f9 100644 --- a/movement.c +++ b/movement.c @@ -614,8 +614,19 @@ watch_date_time_t movement_get_date_time_in_zone(uint8_t zone_index) { } watch_date_time_t movement_get_local_date_time(void) { + static struct { + unix_timestamp_t timestamp; + rtc_date_time_t datetime; + } cached_date_time = {.datetime.reg=0, .timestamp=0}; + unix_timestamp_t timestamp = watch_rtc_get_unix_time(); - return watch_utility_date_time_from_unix_time(timestamp, movement_get_current_timezone_offset()); + + if (timestamp != cached_date_time.timestamp) { + cached_date_time.timestamp = timestamp; + cached_date_time.datetime = watch_utility_date_time_from_unix_time(timestamp, movement_get_current_timezone_offset()); + } + + return cached_date_time.datetime; } uint32_t movement_get_utc_timestamp(void) { diff --git a/watch-library/hardware/watch/watch_rtc.c b/watch-library/hardware/watch/watch_rtc.c index 1b0b5f49..114c1ee4 100644 --- a/watch-library/hardware/watch/watch_rtc.c +++ b/watch-library/hardware/watch/watch_rtc.c @@ -91,7 +91,19 @@ void watch_rtc_set_date_time(rtc_date_time_t date_time) { } rtc_date_time_t watch_rtc_get_date_time(void) { - return watch_utility_date_time_from_unix_time(watch_rtc_get_unix_time(), 0); + static struct { + unix_timestamp_t timestamp; + rtc_date_time_t datetime; + } cached_date_time = {.datetime.reg=0, .timestamp=0}; + + unix_timestamp_t timestamp = watch_rtc_get_unix_time(); + + if (timestamp != cached_date_time.timestamp) { + cached_date_time.timestamp = timestamp; + cached_date_time.datetime = watch_utility_date_time_from_unix_time(timestamp, 0); + } + + return cached_date_time.datetime; } void watch_rtc_set_unix_time(unix_timestamp_t unix_time) { From 450100b90a3357c04dc28c11f5cfc8f69ca6ffef Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Tue, 26 Aug 2025 22:15:01 -0400 Subject: [PATCH 23/53] make movement_set_local_date_time slightly more efficient --- movement.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/movement.c b/movement.c index bc8b12f9..23eeb401 100644 --- a/movement.c +++ b/movement.c @@ -639,8 +639,7 @@ void movement_set_utc_date_time(watch_date_time_t date_time) { void movement_set_local_date_time(watch_date_time_t date_time) { int32_t current_offset = movement_get_current_timezone_offset(); - watch_date_time_t utc_date_time = watch_utility_date_time_convert_zone(date_time, current_offset, 0); - movement_set_utc_date_time(utc_date_time); + movement_set_utc_timestamp(watch_utility_date_time_to_unix_time(date_time, current_offset)); } void movement_set_utc_timestamp(uint32_t timestamp) { From 5293b7983c034044c7a07f21c2eab6439143e862 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Tue, 26 Aug 2025 22:23:14 -0400 Subject: [PATCH 24/53] Remove a few unnecessary date_time to timestamp conversions --- watch-faces/complication/interval_face.c | 3 +-- watch-faces/complication/totp_face.c | 3 +-- watch-faces/complication/totp_lfs_face.c | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/watch-faces/complication/interval_face.c b/watch-faces/complication/interval_face.c index 15593567..89f00b83 100644 --- a/watch-faces/complication/interval_face.c +++ b/watch-faces/complication/interval_face.c @@ -96,8 +96,7 @@ static inline void _inc_uint8(uint8_t *value, uint8_t step, uint8_t max) { static uint32_t _get_now_ts() { // returns the current date time as unix timestamp - watch_date_time_t now = watch_rtc_get_date_time(); - return watch_utility_date_time_to_unix_time(now, 0); + return movement_get_utc_timestamp(); } static inline void _button_beep() { diff --git a/watch-faces/complication/totp_face.c b/watch-faces/complication/totp_face.c index b45c62f6..666c3337 100644 --- a/watch-faces/complication/totp_face.c +++ b/watch-faces/complication/totp_face.c @@ -36,7 +36,6 @@ #include #include "totp_face.h" #include "watch.h" -#include "watch_utility.h" #include "TOTP.h" #include "base32.h" @@ -159,7 +158,7 @@ static void totp_generate_and_display(totp_state_t *totp_state) { } static inline uint32_t totp_compute_base_timestamp() { - return watch_utility_date_time_to_unix_time(movement_get_utc_date_time(), 0); + return movement_get_utc_timestamp(); } void totp_face_setup(uint8_t watch_face_index, void ** context_ptr) { diff --git a/watch-faces/complication/totp_lfs_face.c b/watch-faces/complication/totp_lfs_face.c index 8be3e28d..7d63bba4 100644 --- a/watch-faces/complication/totp_lfs_face.c +++ b/watch-faces/complication/totp_lfs_face.c @@ -30,7 +30,6 @@ #include "base32.h" #include "watch.h" -#include "watch_utility.h" #include "filesystem.h" #include "totp_lfs_face.h" @@ -253,7 +252,7 @@ void totp_lfs_face_activate(void *context) { } #endif - totp_state->timestamp = watch_utility_date_time_to_unix_time(movement_get_utc_date_time(), 0); + totp_state->timestamp = movement_get_utc_timestamp(); totp_face_set_record(totp_state, 0); } From 7d0f4d1a337f4e928d87cdf6ac8380a9ae82a070 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Tue, 26 Aug 2025 23:37:53 -0400 Subject: [PATCH 25/53] More efficient display draw for fast_stopwatch_face --- .../complication/fast_stopwatch_face.c | 54 ++++++++++++++----- .../complication/fast_stopwatch_face.h | 5 ++ 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/watch-faces/complication/fast_stopwatch_face.c b/watch-faces/complication/fast_stopwatch_face.c index a4af13f3..9baad1b7 100644 --- a/watch-faces/complication/fast_stopwatch_face.c +++ b/watch-faces/complication/fast_stopwatch_face.c @@ -25,6 +25,7 @@ #include #include +#include #include "fast_stopwatch_face.h" #include "watch.h" #include "watch_common_display.h" @@ -59,27 +60,52 @@ static inline void _button_beep() { // 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_elapsed(uint32_t ticks) { - ticks = ticks % TWENTY_FOUR_HOURS; - char buf[14]; +static void _display_elapsed(fast_stopwatch_state_t *state, uint32_t ticks) { + char buf[3]; uint8_t sec_100 = (ticks & 0x7F) * 100 / 128; + + watch_display_character_lp_seconds('0' + sec_100 / 10, 8); + watch_display_character_lp_seconds('0' + sec_100 % 10, 9); + uint32_t seconds = ticks >> 7; + + if (seconds == state->old_display.seconds) { + return; + } + + state->old_display.seconds = seconds; + + sprintf(buf, "%02lu", seconds % 60); + watch_display_text(WATCH_POSITION_MINUTES, buf); + uint32_t minutes = seconds / 60; - uint32_t hours = minutes / 60; + + if (minutes == state->old_display.minutes) { + return; + } + + state->old_display.minutes = minutes; + + sprintf(buf, "%02lu", minutes % 60); + watch_display_text(WATCH_POSITION_HOURS, buf); + + uint32_t hours = (minutes / 60) % 24; + + if (hours == state->old_display.hours) { + return; + } + + state->old_display.hours = hours; + if (hours) { - sprintf(buf, "%2u", hours); + sprintf(buf, "%2lu", hours); watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); } else { watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); } - - sprintf(buf, "%02lu%02lu%02u", (minutes % 60), (seconds % 60), sec_100); - watch_display_text(WATCH_POSITION_BOTTOM, buf); } static void _draw_indicators(fast_stopwatch_state_t *state, movement_event_t event, uint32_t elapsed) { @@ -255,6 +281,10 @@ void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr) { void fast_stopwatch_face_activate(void *context) { fast_stopwatch_state_t *state = (fast_stopwatch_state_t *) context; + // force full re-draw + state->old_display.seconds = UINT_MAX; + state->old_display.minutes = UINT_MAX; + state->old_display.hours = UINT_MAX; movement_request_tick_frequency(get_refresh_rate(state)); } @@ -270,7 +300,7 @@ bool fast_stopwatch_face_loop(movement_event_t event, void *context) { case EVENT_ACTIVATE: watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "STW", "ST"); _draw_indicators(state, event, elapsed); - _display_elapsed(elapsed); + _display_elapsed(state, elapsed); break; case EVENT_ALARM_BUTTON_DOWN: case EVENT_LIGHT_BUTTON_DOWN: @@ -278,7 +308,7 @@ bool fast_stopwatch_face_loop(movement_event_t event, void *context) { // Fall into the case below; case EVENT_TICK: _draw_indicators(state, event, elapsed); - _display_elapsed(elapsed); + _display_elapsed(state, elapsed); break; default: movement_default_loop_handler(event); diff --git a/watch-faces/complication/fast_stopwatch_face.h b/watch-faces/complication/fast_stopwatch_face.h index d99f6085..70c78bef 100644 --- a/watch-faces/complication/fast_stopwatch_face.h +++ b/watch-faces/complication/fast_stopwatch_face.h @@ -59,6 +59,11 @@ typedef struct { 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) + struct { + rtc_counter_t seconds; + rtc_counter_t minutes; + rtc_counter_t hours; + } old_display; // the digits currently being displayed on screen } fast_stopwatch_state_t; void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr); From edd3a5c3b45f3f524926141d0d2051c59b82c57a Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Tue, 26 Aug 2025 23:56:50 -0400 Subject: [PATCH 26/53] Add a slow display refresh mode to fast_stopwatch --- .../complication/fast_stopwatch_face.c | 43 ++++++++++++++----- .../complication/fast_stopwatch_face.h | 1 + 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/watch-faces/complication/fast_stopwatch_face.c b/watch-faces/complication/fast_stopwatch_face.c index 9baad1b7..63ef7176 100644 --- a/watch-faces/complication/fast_stopwatch_face.c +++ b/watch-faces/complication/fast_stopwatch_face.c @@ -31,6 +31,7 @@ #include "watch_common_display.h" #include "watch_utility.h" #include "watch_rtc.h" +#include "slcd.h" /* This watch face implements the original F-91W stopwatch functionality @@ -59,16 +60,23 @@ static inline void _button_beep() { // 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 uint8_t DISPLAY_RUNNING_RATE_SLOW = 2; /// @brief Display minutes, seconds and fractions derived from 128 Hz tick counter /// on the lcd. /// @param ticks static void _display_elapsed(fast_stopwatch_state_t *state, uint32_t ticks) { char buf[3]; - uint8_t sec_100 = (ticks & 0x7F) * 100 / 128; - watch_display_character_lp_seconds('0' + sec_100 / 10, 8); - watch_display_character_lp_seconds('0' + sec_100 % 10, 9); + if (state->slow_refresh && (state->status == SW_STATUS_RUNNING || state->status == SW_STATUS_IDLE)) { + watch_display_character_lp_seconds(' ', 8); + watch_display_character_lp_seconds(' ', 9); + } else { + uint8_t sec_100 = (ticks & 0x7F) * 100 / 128; + + watch_display_character_lp_seconds('0' + sec_100 / 10, 8); + watch_display_character_lp_seconds('0' + sec_100 % 10, 9); + } uint32_t seconds = ticks >> 7; @@ -157,7 +165,11 @@ static void _draw_indicators(fast_stopwatch_state_t *state, movement_event_t eve static uint8_t get_refresh_rate(fast_stopwatch_state_t *state) { switch (state->status) { case SW_STATUS_RUNNING: - return DISPLAY_RUNNING_RATE; + if (state->slow_refresh) { + return DISPLAY_RUNNING_RATE_SLOW; + } else { + return DISPLAY_RUNNING_RATE; + } case SW_STATUS_RUNNING_LAPPING: return 2; case SW_STATUS_STOPPED: @@ -174,7 +186,10 @@ static void state_transition(fast_stopwatch_state_t *state, rtc_counter_t counte case EVENT_ALARM_BUTTON_DOWN: state->status = SW_STATUS_RUNNING; state->start_counter = counter; - movement_request_tick_frequency(DISPLAY_RUNNING_RATE); + movement_request_tick_frequency(get_refresh_rate(state)); + return; + case EVENT_LIGHT_LONG_PRESS: + state->slow_refresh = !state->slow_refresh; return; default: return; @@ -185,12 +200,12 @@ static void state_transition(fast_stopwatch_state_t *state, rtc_counter_t counte case EVENT_ALARM_BUTTON_DOWN: state->status = SW_STATUS_STOPPED; state->stop_counter = counter; - movement_request_tick_frequency(1); + movement_request_tick_frequency(get_refresh_rate(state)); return; case EVENT_LIGHT_BUTTON_DOWN: state->status = SW_STATUS_RUNNING_LAPPING; state->lap_counter = counter; - movement_request_tick_frequency(2); + movement_request_tick_frequency(get_refresh_rate(state)); return; default: return; @@ -201,12 +216,17 @@ static void state_transition(fast_stopwatch_state_t *state, rtc_counter_t counte case EVENT_ALARM_BUTTON_DOWN: state->status = SW_STATUS_STOPPED_LAPPING; state->stop_counter = counter; - movement_request_tick_frequency(1); + movement_request_tick_frequency(get_refresh_rate(state)); return; case EVENT_LIGHT_BUTTON_DOWN: state->status = SW_STATUS_RUNNING; state->lap_counter = counter; - movement_request_tick_frequency(DISPLAY_RUNNING_RATE); + movement_request_tick_frequency(get_refresh_rate(state)); + return; + case EVENT_LIGHT_LONG_PRESS: + state->status = SW_STATUS_RUNNING; + state->slow_refresh = !state->slow_refresh; + movement_request_tick_frequency(get_refresh_rate(state)); return; default: return; @@ -218,7 +238,7 @@ static void state_transition(fast_stopwatch_state_t *state, rtc_counter_t counte 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); + movement_request_tick_frequency(get_refresh_rate(state)); return; case EVENT_LIGHT_BUTTON_DOWN: state->status = SW_STATUS_STOPPED; @@ -232,7 +252,7 @@ static void state_transition(fast_stopwatch_state_t *state, rtc_counter_t counte 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); + movement_request_tick_frequency(get_refresh_rate(state)); return; case EVENT_LIGHT_BUTTON_DOWN: state->status = SW_STATUS_IDLE; @@ -304,6 +324,7 @@ bool fast_stopwatch_face_loop(movement_event_t event, void *context) { break; case EVENT_ALARM_BUTTON_DOWN: case EVENT_LIGHT_BUTTON_DOWN: + case EVENT_LIGHT_LONG_PRESS: _button_beep(); // Fall into the case below; case EVENT_TICK: diff --git a/watch-faces/complication/fast_stopwatch_face.h b/watch-faces/complication/fast_stopwatch_face.h index 70c78bef..34cb4973 100644 --- a/watch-faces/complication/fast_stopwatch_face.h +++ b/watch-faces/complication/fast_stopwatch_face.h @@ -59,6 +59,7 @@ typedef struct { 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) + bool slow_refresh; // update the display slowly (same 128Hz timekeeping accuracy) struct { rtc_counter_t seconds; rtc_counter_t minutes; From 3ea2f9c58ab6cb0108f93cbbc64d493c42ded9bc Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Sat, 30 Aug 2025 23:46:34 -0400 Subject: [PATCH 27/53] Allow users to set independent buzzer volume for signal/alarm --- movement.c | 44 ++++++++++++++++--- movement.h | 18 +++++++- movement_config.h | 2 + watch-faces/settings/settings_face.c | 66 +++++++++++++++++++++++++++- watch-faces/settings/settings_face.h | 6 +++ 5 files changed, 127 insertions(+), 9 deletions(-) diff --git a/movement.c b/movement.c index 23eeb401..1bb0a4bb 100644 --- a/movement.c +++ b/movement.c @@ -157,6 +157,19 @@ static udatetime_t _movement_convert_date_time_to_udate(watch_date_time_t date_t }; } +static watch_buzzer_volume_t _movement_get_buzzer_volume(movement_buzzer_priority_t priority) { + switch (priority) { + case BUZZER_PRIORITY_BUTTON: + return movement_button_volume(); + case BUZZER_PRIORITY_SIGNAL: + return movement_signal_volume(); + case BUZZER_PRIORITY_ALARM: + return movement_alarm_volume(); + default: + return WATCH_BUZZER_VOLUME_LOUD; + } +} + static void _movement_set_top_of_minute_alarm() { uint32_t counter = watch_rtc_get_counter(); uint32_t next_minute_counter; @@ -512,15 +525,15 @@ void movement_play_note(watch_buzzer_note_t note, uint16_t duration_ms) { single_note_sequence[1] = (int8_t)duration; single_note_sequence[2] = 0; - movement_play_sequence(single_note_sequence, 0); + movement_play_sequence(single_note_sequence, BUZZER_PRIORITY_BUTTON); } void movement_play_signal(void) { - movement_play_sequence(signal_tune, 1); + movement_play_sequence(signal_tune, BUZZER_PRIORITY_SIGNAL); } void movement_play_alarm(void) { - movement_play_sequence(alarm_tune, 2); + movement_play_sequence(alarm_tune, BUZZER_PRIORITY_ALARM); } void movement_play_alarm_beeps(uint8_t rounds, watch_buzzer_note_t alarm_note) { @@ -550,10 +563,10 @@ void movement_play_alarm_beeps(uint8_t rounds, watch_buzzer_note_t alarm_note) { custom_alarm_tune[18] = 0; - movement_play_sequence(custom_alarm_tune, 2); + movement_play_sequence(custom_alarm_tune, BUZZER_PRIORITY_ALARM); } -void movement_play_sequence(int8_t *note_sequence, uint8_t priority) { +void movement_play_sequence(int8_t *note_sequence, movement_buzzer_priority_t priority) { // Priority is used to ensure that lower priority sequences don't cancel higher priority ones // Priotity order: alarm(2) > signal(1) > note(0) if (priority < movement_volatile_state.pending_sequence_priority) { @@ -569,7 +582,7 @@ void movement_play_sequence(int8_t *note_sequence, uint8_t priority) { movement_volatile_state.has_pending_sequence = true; movement_volatile_state.exit_sleep_mode = true; } else { - watch_buzzer_play_sequence_with_volume(note_sequence, NULL, movement_button_volume()); + watch_buzzer_play_sequence_with_volume(note_sequence, NULL, _movement_get_buzzer_volume(priority)); } } @@ -671,6 +684,21 @@ void movement_set_button_volume(watch_buzzer_volume_t value) { movement_state.settings.bit.button_volume = value; } +watch_buzzer_volume_t movement_signal_volume(void) { + return movement_state.signal_volume; +} +void movement_set_signal_volume(watch_buzzer_volume_t value) { + movement_state.signal_volume = value; +} + +watch_buzzer_volume_t movement_alarm_volume(void) { + return movement_state.alarm_volume; +} + +void movement_set_alarm_volume(watch_buzzer_volume_t value) { + movement_state.alarm_volume = value; +} + movement_clock_mode_t movement_clock_mode_24h(void) { return movement_state.settings.bit.clock_mode_24h ? MOVEMENT_CLOCK_MODE_24H : MOVEMENT_CLOCK_MODE_12H; } @@ -952,6 +980,8 @@ void app_init(void) { if (movement_state.accelerometer_motion_threshold == 0) movement_state.accelerometer_motion_threshold = 32; + movement_state.signal_volume = MOVEMENT_DEFAULT_SIGNAL_VOLUME; + movement_state.alarm_volume = MOVEMENT_DEFAULT_ALARM_VOLUME; movement_state.light_on = false; movement_state.next_available_backup_register = 2; _movement_reset_inactivity_countdown(); @@ -1241,7 +1271,7 @@ bool app_loop(void) { // If we woke up to play a note sequence, actually play the note sequence we were asked to play while in deep sleep. if (movement_volatile_state.has_pending_sequence) { movement_volatile_state.has_pending_sequence = false; - watch_buzzer_play_sequence_with_volume(_pending_sequence, movement_request_sleep, movement_button_volume()); + watch_buzzer_play_sequence_with_volume(_pending_sequence, movement_request_sleep, _movement_get_buzzer_volume(movement_volatile_state.pending_sequence_priority)); // When this sequence is done playing, movement_request_sleep is invoked and the watch will go, // back to sleep (unless the user interacts with it in the meantime) _pending_sequence = NULL; diff --git a/movement.h b/movement.h index c1ac756f..6e997d4a 100644 --- a/movement.h +++ b/movement.h @@ -145,6 +145,12 @@ typedef enum { MINUTE_TIMEOUT, // Top of the Minute timeout } movement_timeout_index_t; +typedef enum { + BUZZER_PRIORITY_BUTTON = 0, // Buzzer priority for button beeps (lowest priority). + BUZZER_PRIORITY_SIGNAL, // Buzzer priority for hourly chime (medium priority). + BUZZER_PRIORITY_ALARM, // Buzzer priority for hourly chime (highest priority). +} movement_buzzer_priority_t; + typedef struct { uint8_t event_type; uint8_t subsecond; @@ -286,6 +292,10 @@ typedef struct { lis2dw_data_rate_t accelerometer_background_rate; // threshold for considering the wearer is in motion uint8_t accelerometer_motion_threshold; + + // signal and alarm volumes + watch_buzzer_volume_t signal_volume; + watch_buzzer_volume_t alarm_volume; } movement_state_t; void movement_move_to_face(uint8_t watch_face_index); @@ -318,7 +328,7 @@ void movement_play_note(watch_buzzer_note_t note, uint16_t duration_ms); void movement_play_signal(void); void movement_play_alarm(void); void movement_play_alarm_beeps(uint8_t rounds, watch_buzzer_note_t alarm_note); -void movement_play_sequence(int8_t *note_sequence, uint8_t priority); +void movement_play_sequence(int8_t *note_sequence, movement_buzzer_priority_t priority); uint8_t movement_claim_backup_register(void); @@ -343,6 +353,12 @@ void movement_set_button_should_sound(bool value); watch_buzzer_volume_t movement_button_volume(void); void movement_set_button_volume(watch_buzzer_volume_t value); +watch_buzzer_volume_t movement_signal_volume(void); +void movement_set_signal_volume(watch_buzzer_volume_t value); + +watch_buzzer_volume_t movement_alarm_volume(void); +void movement_set_alarm_volume(watch_buzzer_volume_t value); + movement_clock_mode_t movement_clock_mode_24h(void); void movement_set_clock_mode_24h(movement_clock_mode_t value); diff --git a/movement_config.h b/movement_config.h index 9633de87..a20cd3c2 100644 --- a/movement_config.h +++ b/movement_config.h @@ -69,6 +69,8 @@ const watch_face_t watch_faces[] = { #define MOVEMENT_DEFAULT_BUTTON_SOUND true #define MOVEMENT_DEFAULT_BUTTON_VOLUME WATCH_BUZZER_VOLUME_SOFT +#define MOVEMENT_DEFAULT_SIGNAL_VOLUME WATCH_BUZZER_VOLUME_LOUD +#define MOVEMENT_DEFAULT_ALARM_VOLUME WATCH_BUZZER_VOLUME_LOUD /* Set the timeout before switching back to the main watch face * Valid values are: diff --git a/watch-faces/settings/settings_face.c b/watch-faces/settings/settings_face.c index 44d70f1b..73bdadc1 100644 --- a/watch-faces/settings/settings_face.c +++ b/watch-faces/settings/settings_face.c @@ -77,6 +77,64 @@ static void beep_setting_advance(void) { } } +static void signal_setting_display(uint8_t subsecond) { + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "SIG", "SI"); + watch_display_text(WATCH_POSITION_BOTTOM, "SIGNAL"); + if (subsecond % 2) { + if (movement_signal_volume() == WATCH_BUZZER_VOLUME_LOUD) { + // H for HIGH + watch_display_text(WATCH_POSITION_TOP_RIGHT, " H"); + } + else { + // L for LOW + watch_display_text(WATCH_POSITION_TOP_RIGHT, " L"); + } + } +} + +static void signal_setting_advance(void) { + if (movement_signal_volume() == WATCH_BUZZER_VOLUME_SOFT) { + // was soft. make it loud. + movement_set_signal_volume(WATCH_BUZZER_VOLUME_LOUD); + } else { + // was loud. make it soft. + movement_set_signal_volume(WATCH_BUZZER_VOLUME_SOFT); + } + + signal_setting_display(1); + movement_play_signal(); +} + + +static void alarm_setting_display(uint8_t subsecond) { + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "ALM", "AL"); + watch_display_text(WATCH_POSITION_BOTTOM, "ALARM "); + if (subsecond % 2) { + if (movement_alarm_volume() == WATCH_BUZZER_VOLUME_LOUD) { + // H for HIGH + watch_display_text(WATCH_POSITION_TOP_RIGHT, " H"); + } + else { + // L for LOW + watch_display_text(WATCH_POSITION_TOP_RIGHT, " L"); + } + } +} + +static void alarm_setting_advance(void) { + if (movement_alarm_volume() == WATCH_BUZZER_VOLUME_SOFT) { + // was soft. make it loud. + movement_set_alarm_volume(WATCH_BUZZER_VOLUME_LOUD); + } else { + // was loud. make it soft. + movement_set_alarm_volume(WATCH_BUZZER_VOLUME_SOFT); + + } + + alarm_setting_display(1); + movement_play_alarm(); +} + static void timeout_setting_display(uint8_t subsecond) { watch_display_text_with_fallback(WATCH_POSITION_TOP, "TMOUt", "TO"); if (subsecond % 2) { @@ -235,7 +293,7 @@ void settings_face_setup(uint8_t watch_face_index, void ** context_ptr) { settings_state_t *state = (settings_state_t *)*context_ptr; int8_t current_setting = 0; - state->num_settings = 5; // baseline, without LED settings + state->num_settings = 7; // baseline, without LED settings #ifdef BUILD_GIT_HASH state->num_settings++; #endif @@ -256,6 +314,12 @@ void settings_face_setup(uint8_t watch_face_index, void ** context_ptr) { state->settings_screens[current_setting].display = beep_setting_display; state->settings_screens[current_setting].advance = beep_setting_advance; current_setting++; + state->settings_screens[current_setting].display = signal_setting_display; + state->settings_screens[current_setting].advance = signal_setting_advance; + current_setting++; + state->settings_screens[current_setting].display = alarm_setting_display; + state->settings_screens[current_setting].advance = alarm_setting_advance; + current_setting++; state->settings_screens[current_setting].display = timeout_setting_display; state->settings_screens[current_setting].advance = timeout_setting_advance; current_setting++; diff --git a/watch-faces/settings/settings_face.h b/watch-faces/settings/settings_face.h index 49e96b98..fd9256ea 100644 --- a/watch-faces/settings/settings_face.h +++ b/watch-faces/settings/settings_face.h @@ -44,6 +44,12 @@ * This setting allows you to choose whether the Mode button should emit * a beep when pressed, and if so, how loud it should be. Options are * "Y" for yes and "N" for no. + * + * SI / SIG - Signal beep. + * This setting allows you to choose the hourly chime buzzer volume. + * + * AL / ALM - Alarm beep. + * This setting allows you to choose the hourly chime buzzer volume. * * TO / Tmout - Timeout. * Sets the time until screens that time out (like Settings and Time Set) From 961db3cc71a6e6eec77a34bfc23bf218b7335d15 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Fri, 12 Sep 2025 21:20:05 -0400 Subject: [PATCH 28/53] fix instant light not turning off after a long press --- movement.c | 1 + 1 file changed, 1 insertion(+) diff --git a/movement.c b/movement.c index 1bb0a4bb..1c9db617 100644 --- a/movement.c +++ b/movement.c @@ -435,6 +435,7 @@ bool movement_default_loop_handler(movement_event_t event) { movement_illuminate_led(); break; case EVENT_LIGHT_BUTTON_UP: + case EVENT_LIGHT_LONG_UP: if (movement_state.settings.bit.led_duration == 0) { movement_force_led_off(); } From e5ddbe3c10f2dfdd2ce11b5f74f97fb3758ad1d2 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Fri, 12 Sep 2025 21:26:02 -0400 Subject: [PATCH 29/53] immediately process events when coming out of deep sleep --- movement.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/movement.c b/movement.c index 1c9db617..14944bea 100644 --- a/movement.c +++ b/movement.c @@ -1277,6 +1277,10 @@ bool app_loop(void) { // back to sleep (unless the user interacts with it in the meantime) _pending_sequence = NULL; } + + // don't let the watch sleep when exiting deep sleep mode, + // so that app_loop will run again and process the events that may have fired. + can_sleep = false; } #endif From 13a516609716bb68ada905c1172b4b530ba17317 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Fri, 12 Sep 2025 21:41:59 -0400 Subject: [PATCH 30/53] use __builtin_ctz when processing pending events --- movement.c | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/movement.c b/movement.c index 14944bea..c1a5d087 100644 --- a/movement.c +++ b/movement.c @@ -1223,12 +1223,11 @@ bool app_loop(void) { // Consume all the pending events movement_event_type_t event_type = 0; while (pending_events) { - if (pending_events & 1) { - event.event_type = event_type; - can_sleep = wf->loop(event, watch_face_contexts[movement_state.current_face_idx]) && can_sleep; - } - pending_events = pending_events >> 1; - event_type++; + uint8_t next_event = __builtin_ctz(pending_events); + event.event_type = event_type + next_event; + can_sleep = wf->loop(event, watch_face_contexts[movement_state.current_face_idx]) && can_sleep; + pending_events = pending_events >> (next_event + 1); + event_type = event_type + next_event + 1; } // handle top-of-minute tasks, if the alarm handler told us we need to From 02faae3d25114300b7c96c5276d1e346804c1d92 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Mon, 15 Sep 2025 23:42:31 -0400 Subject: [PATCH 31/53] Events that follow a down event on the previous face should not be forwarded to the new face --- movement.c | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/movement.c b/movement.c index c1a5d087..34e5645e 100644 --- a/movement.c +++ b/movement.c @@ -61,6 +61,11 @@ watch_date_time_t scheduled_tasks[MOVEMENT_NUM_FACES]; const int32_t movement_le_inactivity_deadlines[8] = {INT_MAX, 600, 3600, 7200, 21600, 43200, 86400, 604800}; const int16_t movement_timeout_inactivity_deadlines[4] = {60, 120, 300, 1800}; +const uint32_t _movement_mode_button_events_mask = 0b1111 << EVENT_MODE_BUTTON_DOWN; +const uint32_t _movement_light_button_events_mask = 0b1111 << EVENT_LIGHT_BUTTON_DOWN; +const uint32_t _movement_alarm_button_events_mask = 0b1111 << EVENT_ALARM_BUTTON_DOWN; +const uint32_t _movement_button_events_mask = _movement_mode_button_events_mask | _movement_light_button_events_mask | _movement_alarm_button_events_mask; + typedef struct { movement_event_type_t down_event; watch_cb_t cb_longpress; @@ -93,6 +98,9 @@ typedef struct { movement_button_t mode_button; movement_button_t light_button; movement_button_t alarm_button; + + // button events that will not be passed to the current face loop, but will instead passed directly to the default loop handler. + volatile uint32_t passthrough_events; } movement_volatile_state_t; movement_volatile_state_t movement_volatile_state; @@ -278,6 +286,12 @@ static void _movement_handle_button_presses(uint32_t pending_events) { &movement_volatile_state.alarm_button }; + uint32_t button_events_masks[3] = { + _movement_mode_button_events_mask, + _movement_light_button_events_mask, + _movement_alarm_button_events_mask, + }; + for (uint8_t i = 0; i < 3; i++) { movement_button_t* button = buttons[i]; @@ -285,6 +299,8 @@ static void _movement_handle_button_presses(uint32_t pending_events) { if (pending_events & (1 << button->down_event)) { watch_rtc_register_comp_callback_no_schedule(button->cb_longpress, button->down_timestamp + MOVEMENT_LONG_PRESS_TICKS, button->timeout_index); any_down = true; + // this button's events will start getting passed to the face + movement_volatile_state.passthrough_events &= ~button_events_masks[i]; } // If a button up or button long up occurred @@ -1173,6 +1189,9 @@ static bool _switch_face(void) { movement_state.watch_face_changed = false; bool can_sleep = wf->loop(event, watch_face_contexts[movement_state.current_face_idx]); + // Button events that follow a down event that happened on the previous face should not be forwarded to the new face + movement_volatile_state.passthrough_events = _movement_button_events_mask; + return can_sleep; } @@ -1221,7 +1240,19 @@ bool app_loop(void) { } // Consume all the pending events + uint32_t passthrough_pending_events = pending_events & movement_volatile_state.passthrough_events; + pending_events = pending_events & ~movement_volatile_state.passthrough_events; + movement_event_type_t event_type = 0; + while (passthrough_pending_events) { + uint8_t next_event = __builtin_ctz(passthrough_pending_events); + event.event_type = event_type + next_event; + can_sleep = movement_default_loop_handler(event) && can_sleep; + passthrough_pending_events = passthrough_pending_events >> (next_event + 1); + event_type = event_type + next_event + 1; + } + + event_type = 0; while (pending_events) { uint8_t next_event = __builtin_ctz(pending_events); event.event_type = event_type + next_event; From dc6aebb4f8b5edf0dcb88edff64ab35c789ba7ad Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Sat, 18 Oct 2025 22:53:12 -0400 Subject: [PATCH 32/53] add fast_stopwatch_face to default, and remove rtccount_face --- movement_config.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/movement_config.h b/movement_config.h index a20cd3c2..2f48d8fd 100644 --- a/movement_config.h +++ b/movement_config.h @@ -32,14 +32,13 @@ const watch_face_t watch_faces[] = { world_clock_face, sunrise_sunset_face, moon_phase_face, - stopwatch_face, + fast_stopwatch_face, countdown_face, alarm_face, temperature_display_face, voltage_face, settings_face, set_time_face, - rtccount_face, }; #define MOVEMENT_NUM_FACES (sizeof(watch_faces) / sizeof(watch_face_t)) From d9b65ed5c8a1696abcd336bb5106396b3d942661 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Sat, 18 Oct 2025 23:23:43 -0400 Subject: [PATCH 33/53] ensure a button up event is always emitted --- movement.c | 45 +++++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/movement.c b/movement.c index 34e5645e..2c9cda21 100644 --- a/movement.c +++ b/movement.c @@ -1394,46 +1394,43 @@ void cb_alarm_btn_interrupt(void) { movement_volatile_state.pending_events |= 1 << _process_button_event(pin_level, &movement_volatile_state.alarm_button); } -static movement_event_type_t _process_button_longpress_timeout(movement_button_t* button) { - // Looks like all these checks are not needed for the longpress detection to work reliably. - // Keep the code around for now in case problems arise long-term. +static movement_event_type_t _process_button_longpress_timeout(bool pin_level, movement_button_t* button) { + if (!button->is_down) { + return EVENT_NONE; + } - // if (!button->is_down) { - // return EVENT_NONE; - // } - - // movement_event_type_t up_event = button->down_event + 1; - - // if (movement_volatile_state.pending_events & 1 << up_event) { - // return EVENT_NONE; - // } - - // uint32_t counter = watch_rtc_get_counter(); - // if ((counter - button->down_timestamp) < MOVEMENT_LONG_PRESS_TICKS) { - // return EVENT_NONE; - // } - - movement_event_type_t longpress_event = button->down_event + 2; - - return longpress_event; + if (pin_level) { + return button->down_event + 2; // event_longpress + } else { + // hypotetical corner case: if the timeout fired but the pin level is actually up, we may have missed/rejected the up event, so fire it here +#if MOVEMENT_DEBOUNCE_TICKS + // we're in a corner case, we don't know when the up actually happened. + button->up_timestamp = button->down_timestamp; +#endif + button->is_down = false; + return button->down_event + 1; // event_up + } } void cb_light_btn_timeout_interrupt(void) { + bool pin_level = HAL_GPIO_BTN_LIGHT_read(); movement_button_t* button = &movement_volatile_state.light_button; - movement_volatile_state.pending_events |= 1 << _process_button_longpress_timeout(button); + movement_volatile_state.pending_events |= 1 << _process_button_longpress_timeout(pin_level, button); } void cb_mode_btn_timeout_interrupt(void) { + bool pin_level = HAL_GPIO_BTN_MODE_read(); movement_button_t* button = &movement_volatile_state.mode_button; - movement_volatile_state.pending_events |= 1 << _process_button_longpress_timeout(button); + movement_volatile_state.pending_events |= 1 << _process_button_longpress_timeout(pin_level, button); } void cb_alarm_btn_timeout_interrupt(void) { + bool pin_level = HAL_GPIO_BTN_ALARM_read(); movement_button_t* button = &movement_volatile_state.alarm_button; - movement_volatile_state.pending_events |= 1 << _process_button_longpress_timeout(button); + movement_volatile_state.pending_events |= 1 << _process_button_longpress_timeout(pin_level, button); } void cb_led_timeout_interrupt(void) { From 24400c53bbabd661b093832c208fef8db1c26950 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Sun, 19 Oct 2025 21:11:33 -0400 Subject: [PATCH 34/53] fix slow default alarm tune --- movement.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/movement.c b/movement.c index 2c9cda21..000490c5 100644 --- a/movement.c +++ b/movement.c @@ -110,14 +110,14 @@ static int8_t *_pending_sequence; // The note sequence of the default alarm int8_t alarm_tune[] = { - BUZZER_NOTE_C8, 4, - BUZZER_NOTE_REST, 7, - BUZZER_NOTE_C8, 4, - BUZZER_NOTE_REST, 7, - BUZZER_NOTE_C8, 4, - BUZZER_NOTE_REST, 7, - BUZZER_NOTE_C8, 4, - BUZZER_NOTE_REST, 27, + BUZZER_NOTE_C8, 3, + BUZZER_NOTE_REST, 4, + BUZZER_NOTE_C8, 3, + BUZZER_NOTE_REST, 4, + BUZZER_NOTE_C8, 3, + BUZZER_NOTE_REST, 4, + BUZZER_NOTE_C8, 5, + BUZZER_NOTE_REST, 38, -8, 9, 0 }; From 7526b30b3d72e8af97ba68e517a639ef9f0b8af2 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Mon, 27 Oct 2025 00:22:14 -0400 Subject: [PATCH 35/53] talk to accelerometer in the app loop rather than interrupt routine --- movement.c | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/movement.c b/movement.c index 000490c5..f90aac93 100644 --- a/movement.c +++ b/movement.c @@ -93,6 +93,7 @@ typedef struct { volatile bool is_buzzing; volatile uint8_t pending_sequence_priority; volatile bool schedule_next_comp; + volatile bool has_pending_accelerometer; // button tracking for long press movement_button_t mode_button; @@ -276,6 +277,24 @@ static void _movement_renew_top_of_minute_alarm(void) { movement_volatile_state.schedule_next_comp = true; } +static uint32_t _movement_get_accelerometer_events() { + uint32_t accelerometer_events = 0; + + uint8_t int_src = lis2dw_get_interrupt_source(); + + if (int_src & LIS2DW_REG_ALL_INT_SRC_DOUBLE_TAP) { + accelerometer_events |= 1 << EVENT_DOUBLE_TAP; + printf("Double tap!\n"); + } + + if (int_src & LIS2DW_REG_ALL_INT_SRC_SINGLE_TAP) { + accelerometer_events |= 1 << EVENT_SINGLE_TAP; + printf("Single tap!\n"); + } + + return accelerometer_events; +} + static void _movement_handle_button_presses(uint32_t pending_events) { bool any_up = false; bool any_down = false; @@ -910,6 +929,7 @@ void app_init(void) { movement_volatile_state.enter_sleep_mode = false; movement_volatile_state.exit_sleep_mode = false; movement_volatile_state.has_pending_sequence = false; + movement_volatile_state.has_pending_accelerometer = false; movement_volatile_state.is_sleeping = false; movement_volatile_state.is_buzzing = false; @@ -1221,6 +1241,11 @@ bool app_loop(void) { } } + if (movement_volatile_state.has_pending_accelerometer) { + movement_volatile_state.has_pending_accelerometer = false; + pending_events |= _movement_get_accelerometer_events(); + } + // handle any button up/down events that occurred, e.g. schedule longpress timeouts, reset inactivity, etc. _movement_handle_button_presses(pending_events); @@ -1468,16 +1493,7 @@ void cb_tick(void) { } void cb_accelerometer_event(void) { - uint8_t int_src = lis2dw_get_interrupt_source(); - - if (int_src & LIS2DW_REG_ALL_INT_SRC_DOUBLE_TAP) { - movement_volatile_state.pending_events |= 1 << EVENT_DOUBLE_TAP; - printf("Double tap!\n"); - } - if (int_src & LIS2DW_REG_ALL_INT_SRC_SINGLE_TAP) { - movement_volatile_state.pending_events |= 1 << EVENT_SINGLE_TAP; - printf("Single tap!\n"); - } + movement_volatile_state.has_pending_accelerometer = true; } void cb_accelerometer_wake(void) { From c5fe350fa1ab16c03f304f21ee6d534ff2f5891e Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 16 Nov 2025 10:15:46 -0500 Subject: [PATCH 36/53] Explicitly set interrupt type to latched --- movement.c | 5 +++-- watch-library/shared/driver/lis2dw.c | 14 ++++++++++++++ watch-library/shared/driver/lis2dw.h | 4 ++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/movement.c b/movement.c index f90aac93..345dcb34 100644 --- a/movement.c +++ b/movement.c @@ -284,12 +284,12 @@ static uint32_t _movement_get_accelerometer_events() { if (int_src & LIS2DW_REG_ALL_INT_SRC_DOUBLE_TAP) { accelerometer_events |= 1 << EVENT_DOUBLE_TAP; - printf("Double tap!\n"); + printf("Double tap!\r\n"); } if (int_src & LIS2DW_REG_ALL_INT_SRC_SINGLE_TAP) { accelerometer_events |= 1 << EVENT_SINGLE_TAP; - printf("Single tap!\n"); + printf("Single tap!\r\n"); } return accelerometer_events; @@ -1118,6 +1118,7 @@ void app_setup(void) { watch_register_interrupt_callback(HAL_GPIO_A3_pin(), cb_accelerometer_event, INTERRUPT_TRIGGER_RISING); // Enable the interrupts... + lis2dw_latched_interrupts(); lis2dw_enable_interrupts(); // At first boot, this next line sets the accelerometer's sampling rate to 0, which is LIS2DW_DATA_RATE_POWERDOWN. diff --git a/watch-library/shared/driver/lis2dw.c b/watch-library/shared/driver/lis2dw.c index 1c89f244..6e3cb59a 100644 --- a/watch-library/shared/driver/lis2dw.c +++ b/watch-library/shared/driver/lis2dw.c @@ -425,6 +425,20 @@ void lis2dw_disable_interrupts(void) { #endif } +void lis2dw_pulsed_interrupts(void) { +#ifdef I2C_SERCOM + uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7); + watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7, configuration | LIS2DW_CTRL7_VAL_DRDY_PULSED); +#endif +} + +void lis2dw_latched_interrupts(void) { +#ifdef I2C_SERCOM + uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7); + watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7, configuration & ~LIS2DW_CTRL7_VAL_DRDY_PULSED); +#endif +} + lis2dw_wakeup_source_t lis2dw_get_wakeup_source() { #ifdef I2C_SERCOM return (lis2dw_wakeup_source_t) watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_SRC); diff --git a/watch-library/shared/driver/lis2dw.h b/watch-library/shared/driver/lis2dw.h index 3b017c65..2db57da1 100644 --- a/watch-library/shared/driver/lis2dw.h +++ b/watch-library/shared/driver/lis2dw.h @@ -375,6 +375,10 @@ void lis2dw_enable_interrupts(void); void lis2dw_disable_interrupts(void); +void lis2dw_pulsed_interrupts(void); + +void lis2dw_latched_interrupts(void); + lis2dw_interrupt_source_t lis2dw_get_interrupt_source(void); lis2dw_wakeup_source_t lis2dw_get_wakeup_source(void); From dd054efe8d6dc40a22dc7c7be26ba6eebbdca02b Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 16 Nov 2025 16:05:22 -0500 Subject: [PATCH 37/53] Added LIR latching on LIS2DW --- movement.c | 3 ++- watch-library/shared/driver/lis2dw.c | 31 ++++++++++++++++++++++++++-- watch-library/shared/driver/lis2dw.h | 14 +++++++++++-- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/movement.c b/movement.c index 345dcb34..a3a61233 100644 --- a/movement.c +++ b/movement.c @@ -816,6 +816,7 @@ bool movement_enable_tap_detection_if_available(void) { lis2dw_set_data_rate(LIS2DW_DATA_RATE_HP_400_HZ); lis2dw_set_mode(LIS2DW_MODE_LOW_POWER); lis2dw_enable_double_tap(); + lis2dw12_int_notification_set(LIS2DW12_INT_LATCHED); // Settling time (1 sample duration, i.e. 1/400Hz) delay_ms(3); @@ -836,6 +837,7 @@ bool movement_disable_tap_detection_if_available(void) { lis2dw_set_data_rate(movement_state.accelerometer_background_rate); lis2dw_set_mode(LIS2DW_MODE_LOW_POWER); lis2dw_disable_double_tap(); + lis2dw12_int_notification_set(LIS2DW12_INT_PULSED); // ...disable Z axis (not sure if this is needed, does this save power?)... lis2dw_configure_tap_threshold(0, 0, 0, 0); @@ -1118,7 +1120,6 @@ void app_setup(void) { watch_register_interrupt_callback(HAL_GPIO_A3_pin(), cb_accelerometer_event, INTERRUPT_TRIGGER_RISING); // Enable the interrupts... - lis2dw_latched_interrupts(); lis2dw_enable_interrupts(); // At first boot, this next line sets the accelerometer's sampling rate to 0, which is LIS2DW_DATA_RATE_POWERDOWN. diff --git a/watch-library/shared/driver/lis2dw.c b/watch-library/shared/driver/lis2dw.c index 6e3cb59a..339af08d 100644 --- a/watch-library/shared/driver/lis2dw.c +++ b/watch-library/shared/driver/lis2dw.c @@ -411,6 +411,33 @@ void lis2dw_configure_int2(uint8_t sources) { #endif } +void lis2dw12_int_notification_set(lis2dw12_lir_t val) { +#ifdef I2C_SERCOM + uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL3); + if (val == LIS2DW12_INT_LATCHED) { + configuration |= LIS2DW_CTRL3_VAL_LIR; + } else { + configuration &= ~LIS2DW_CTRL7_VAL_DRDY_PULSED; + } + watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL3, configuration); +#else + (void)val; +#endif +} + +lis2dw12_lir_t lis2dw12_int_notification_get(void) { +#ifdef I2C_SERCOM + uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL3); + if (configuration & LIS2DW12_INT_LATCHED) { + return LIS2DW12_INT_LATCHED; + } else { + return LIS2DW12_INT_PULSED; + } +#else + return LIS2DW12_INT_PULSED; +#endif +} + void lis2dw_enable_interrupts(void) { #ifdef I2C_SERCOM uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7); @@ -425,14 +452,14 @@ void lis2dw_disable_interrupts(void) { #endif } -void lis2dw_pulsed_interrupts(void) { +void lis2dw_pulsed_drdy_interrupts(void) { #ifdef I2C_SERCOM uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7); watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7, configuration | LIS2DW_CTRL7_VAL_DRDY_PULSED); #endif } -void lis2dw_latched_interrupts(void) { +void lis2dw_latched_drdy_interrupts(void) { #ifdef I2C_SERCOM uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7); watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7, configuration & ~LIS2DW_CTRL7_VAL_DRDY_PULSED); diff --git a/watch-library/shared/driver/lis2dw.h b/watch-library/shared/driver/lis2dw.h index 2db57da1..f7b0a801 100644 --- a/watch-library/shared/driver/lis2dw.h +++ b/watch-library/shared/driver/lis2dw.h @@ -92,6 +92,12 @@ typedef enum { LIS2DW_FILTER_HIGH_PASS = 1, } lis2dw_filter_t; +typedef enum +{ + LIS2DW12_INT_PULSED = 0, + LIS2DW12_INT_LATCHED = 1, +} lis2dw12_lir_t; + typedef enum { LIS2DW_RANGE_16_G = 0b11, // +/- 16g LIS2DW_RANGE_8_G = 0b10, // +/- 8g @@ -367,6 +373,10 @@ void lis2dw_configure_tap_threshold(uint8_t threshold_x, uint8_t threshold_y, ui void lis2dw_configure_tap_duration(uint8_t latency, uint8_t quiet, uint8_t shock); +void lis2dw12_int_notification_set(lis2dw12_lir_t val); + +lis2dw12_lir_t lis2dw12_int_notification_get(void); + void lis2dw_configure_int1(uint8_t sources); void lis2dw_configure_int2(uint8_t sources); @@ -375,9 +385,9 @@ void lis2dw_enable_interrupts(void); void lis2dw_disable_interrupts(void); -void lis2dw_pulsed_interrupts(void); +void lis2dw_pulsed_drdy_interrupts(void); -void lis2dw_latched_interrupts(void); +void lis2dw_latched_drdy_interrupts(void); lis2dw_interrupt_source_t lis2dw_get_interrupt_source(void); From 359bf0df692e4d77d08010b7edef87e864bec2b7 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 16 Nov 2025 16:06:08 -0500 Subject: [PATCH 38/53] Saw double-taps getting missed with latched LIR so moving back to pulsed --- movement.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/movement.c b/movement.c index a3a61233..361c8c89 100644 --- a/movement.c +++ b/movement.c @@ -816,7 +816,6 @@ bool movement_enable_tap_detection_if_available(void) { lis2dw_set_data_rate(LIS2DW_DATA_RATE_HP_400_HZ); lis2dw_set_mode(LIS2DW_MODE_LOW_POWER); lis2dw_enable_double_tap(); - lis2dw12_int_notification_set(LIS2DW12_INT_LATCHED); // Settling time (1 sample duration, i.e. 1/400Hz) delay_ms(3); @@ -837,7 +836,6 @@ bool movement_disable_tap_detection_if_available(void) { lis2dw_set_data_rate(movement_state.accelerometer_background_rate); lis2dw_set_mode(LIS2DW_MODE_LOW_POWER); lis2dw_disable_double_tap(); - lis2dw12_int_notification_set(LIS2DW12_INT_PULSED); // ...disable Z axis (not sure if this is needed, does this save power?)... lis2dw_configure_tap_threshold(0, 0, 0, 0); From 372992e2594b47bd36b3da7646b2b5cb6f6cc51b Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Wed, 19 Nov 2025 08:06:59 -0500 Subject: [PATCH 39/53] Added print for LIS2DW interrupt --- movement.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/movement.c b/movement.c index 361c8c89..70249d60 100644 --- a/movement.c +++ b/movement.c @@ -277,10 +277,20 @@ static void _movement_renew_top_of_minute_alarm(void) { movement_volatile_state.schedule_next_comp = true; } +#define PRINT_LIS_EVENTS false static uint32_t _movement_get_accelerometer_events() { uint32_t accelerometer_events = 0; uint8_t int_src = lis2dw_get_interrupt_source(); +#if PRINT_LIS_EVENTS + printf("_movement_get_accelerometer_events\r\n"); + if (int_src & LIS2DW_REG_ALL_INT_SRC_SLEEP_CHANGE_IA) printf("Sleep Change IA\r\n"); + if (int_src & LIS2DW_REG_ALL_INT_SRC_6D_IA) printf("6D IA\r\n"); + if (int_src & LIS2DW_REG_ALL_INT_SRC_SINGLE_TAP) printf("Single Tap\r\n"); + if (int_src & LIS2DW_REG_ALL_INT_SRC_DOUBLE_TAP) printf("Double Tap\r\n"); + if (int_src & LIS2DW_REG_ALL_INT_SRC_WU_IA) printf("Wake Up\r\n"); + if (int_src & LIS2DW_REG_ALL_INT_SRC_FF_IA) printf("Free Fall\r\n"); +#endif if (int_src & LIS2DW_REG_ALL_INT_SRC_DOUBLE_TAP) { accelerometer_events |= 1 << EVENT_DOUBLE_TAP; From 8d551d26af14dc1c81f060b5806e7a6ba82dd5f2 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Wed, 19 Nov 2025 08:08:01 -0500 Subject: [PATCH 40/53] Revert "Added print for LIS2DW interrupt" This reverts commit 0105fee2f176b38e51cd1da8118481b82bdb35ed. --- movement.c | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/movement.c b/movement.c index 70249d60..361c8c89 100644 --- a/movement.c +++ b/movement.c @@ -277,20 +277,10 @@ static void _movement_renew_top_of_minute_alarm(void) { movement_volatile_state.schedule_next_comp = true; } -#define PRINT_LIS_EVENTS false static uint32_t _movement_get_accelerometer_events() { uint32_t accelerometer_events = 0; uint8_t int_src = lis2dw_get_interrupt_source(); -#if PRINT_LIS_EVENTS - printf("_movement_get_accelerometer_events\r\n"); - if (int_src & LIS2DW_REG_ALL_INT_SRC_SLEEP_CHANGE_IA) printf("Sleep Change IA\r\n"); - if (int_src & LIS2DW_REG_ALL_INT_SRC_6D_IA) printf("6D IA\r\n"); - if (int_src & LIS2DW_REG_ALL_INT_SRC_SINGLE_TAP) printf("Single Tap\r\n"); - if (int_src & LIS2DW_REG_ALL_INT_SRC_DOUBLE_TAP) printf("Double Tap\r\n"); - if (int_src & LIS2DW_REG_ALL_INT_SRC_WU_IA) printf("Wake Up\r\n"); - if (int_src & LIS2DW_REG_ALL_INT_SRC_FF_IA) printf("Free Fall\r\n"); -#endif if (int_src & LIS2DW_REG_ALL_INT_SRC_DOUBLE_TAP) { accelerometer_events |= 1 << EVENT_DOUBLE_TAP; From b3c8f0f87a5bbb6bfd0485ca43a870614bbb51a4 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Fri, 21 Nov 2025 20:55:11 -0500 Subject: [PATCH 41/53] add missing copyright headers --- movement.h | 1 + watch-faces/demo/rtccount_face.c | 2 +- watch-faces/demo/rtccount_face.h | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/movement.h b/movement.h index 6e997d4a..94f9b83e 100644 --- a/movement.h +++ b/movement.h @@ -2,6 +2,7 @@ * MIT License * * Copyright (c) 2022 Joey Castillo + * 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 diff --git a/watch-faces/demo/rtccount_face.c b/watch-faces/demo/rtccount_face.c index f08cde5b..e9bd2104 100644 --- a/watch-faces/demo/rtccount_face.c +++ b/watch-faces/demo/rtccount_face.c @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2022 Joey Castillo + * 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 diff --git a/watch-faces/demo/rtccount_face.h b/watch-faces/demo/rtccount_face.h index 59f11929..a3478a2d 100644 --- a/watch-faces/demo/rtccount_face.h +++ b/watch-faces/demo/rtccount_face.h @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2022 Joey Castillo + * 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 From fe4541d4ee11e93b3767b16be1a27e9f78111073 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Fri, 21 Nov 2025 21:02:07 -0500 Subject: [PATCH 42/53] fix compilation warnings in rtccount_face --- watch-faces/demo/rtccount_face.c | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/watch-faces/demo/rtccount_face.c b/watch-faces/demo/rtccount_face.c index e9bd2104..72a3055a 100644 --- a/watch-faces/demo/rtccount_face.c +++ b/watch-faces/demo/rtccount_face.c @@ -48,6 +48,14 @@ typedef struct { static const uint32_t COUNTER_MASK = (1 << 19) - 1; +static void _rtccount_face_display_string(char* string, uint8_t pos) { + // watch_display_string is deprecated, but there is no alternative for this use-case + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wdeprecated-declarations" + watch_display_string(string, pos); + #pragma GCC diagnostic pop +} + static void _rtccount_face_draw(movement_event_t event, rtccount_state_t* state) { uint32_t counter = watch_rtc_get_counter(); @@ -77,48 +85,48 @@ static void _rtccount_face_draw(movement_event_t event, rtccount_state_t* state) break; } - watch_display_string(buf, 0); + _rtccount_face_display_string(buf, 0); snprintf(buf, sizeof(buf), "%u", event.subsecond); uint32_t len = strlen(buf); - watch_display_string(buf, 4 - len); + _rtccount_face_display_string(buf, 4 - len); switch (state->status) { case RTCCOUNT_STATUS_COUNTER: { - snprintf(buf, sizeof(buf), "%lu", counter & COUNTER_MASK); + snprintf(buf, sizeof(buf), "%u", counter & COUNTER_MASK); size_t len = strlen(buf); - watch_display_string(buf, 10 - len); + _rtccount_face_display_string(buf, 10 - len); break; } case RTCCOUNT_STATUS_COUNTER_SUB: { - snprintf(buf, sizeof(buf), "%lu", counter & 127); + snprintf(buf, sizeof(buf), "%u", counter & 127); size_t len = strlen(buf); - watch_display_string(buf, 10 - len); + _rtccount_face_display_string(buf, 10 - len); break; } case RTCCOUNT_STATUS_MINUTES: { - snprintf(buf, sizeof(buf), "%lu", state->n_top_of_minute & COUNTER_MASK); + snprintf(buf, sizeof(buf), "%u", state->n_top_of_minute & COUNTER_MASK); size_t len = strlen(buf); - watch_display_string(buf, 10 - len); + _rtccount_face_display_string(buf, 10 - len); break; } case RTCCOUNT_STATUS_MINUTES_DIFF: { uint32_t elapsed_minutes = (movement_get_utc_timestamp() - state->ref_timestamp) / 60; - snprintf(buf, sizeof(buf), "%lu", (elapsed_minutes - state->n_top_of_minute) & COUNTER_MASK); + snprintf(buf, sizeof(buf), "%u", (elapsed_minutes - state->n_top_of_minute) & COUNTER_MASK); size_t len = strlen(buf); - watch_display_string(buf, 10 - len); + _rtccount_face_display_string(buf, 10 - len); break; } From c6270444f0400a97f10232a18fa0ebdf32cadff1 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sat, 6 Sep 2025 11:45:46 -0400 Subject: [PATCH 43/53] Typo fix --- watch-faces/settings/settings_face.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watch-faces/settings/settings_face.h b/watch-faces/settings/settings_face.h index fd9256ea..b38e3d1a 100644 --- a/watch-faces/settings/settings_face.h +++ b/watch-faces/settings/settings_face.h @@ -49,7 +49,7 @@ * This setting allows you to choose the hourly chime buzzer volume. * * AL / ALM - Alarm beep. - * This setting allows you to choose the hourly chime buzzer volume. + * This setting allows you to choose the alarm buzzer volume. * * TO / Tmout - Timeout. * Sets the time until screens that time out (like Settings and Time Set) From c45fed8ea97818b4d291dab3ee57c9a1a4bd6fb9 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sat, 6 Sep 2025 11:49:00 -0400 Subject: [PATCH 44/53] Got rid of fallthrough warnings --- watch-faces/complication/fast_stopwatch_face.c | 2 +- watch-faces/io/chirpy_demo_face.c | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/watch-faces/complication/fast_stopwatch_face.c b/watch-faces/complication/fast_stopwatch_face.c index 63ef7176..70c1de96 100644 --- a/watch-faces/complication/fast_stopwatch_face.c +++ b/watch-faces/complication/fast_stopwatch_face.c @@ -326,7 +326,7 @@ bool fast_stopwatch_face_loop(movement_event_t event, void *context) { case EVENT_LIGHT_BUTTON_DOWN: case EVENT_LIGHT_LONG_PRESS: _button_beep(); - // Fall into the case below; + // fall through case EVENT_TICK: _draw_indicators(state, event, elapsed); _display_elapsed(state, elapsed); diff --git a/watch-faces/io/chirpy_demo_face.c b/watch-faces/io/chirpy_demo_face.c index 3a7ff300..f676e179 100644 --- a/watch-faces/io/chirpy_demo_face.c +++ b/watch-faces/io/chirpy_demo_face.c @@ -261,6 +261,7 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) { if (state->mode != CDM_CHIRPING) { movement_move_to_face(0); } + // fall through default: movement_default_loop_handler(event); break; From c6c97556ace998aaaa72cc18601e293347ea19d2 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Tue, 7 Oct 2025 07:23:26 -0400 Subject: [PATCH 45/53] Added FIFO timeout to LIS2DW --- watch-library/shared/driver/lis2dw.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/watch-library/shared/driver/lis2dw.c b/watch-library/shared/driver/lis2dw.c index 339af08d..fbf1ac75 100644 --- a/watch-library/shared/driver/lis2dw.c +++ b/watch-library/shared/driver/lis2dw.c @@ -277,6 +277,7 @@ inline void lis2dw_disable_fifo(void) { #endif } +#define FIFO_TIMEOUT 102 // This is timeout seconds * 128[RTC_CNT_HZ]. So 800ms is 102 bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data) { #ifdef I2C_SERCOM uint8_t temp = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_FIFO_SAMPLE); @@ -284,7 +285,11 @@ bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data) { fifo_data->count = temp & LIS2DW_FIFO_SAMPLE_COUNT; + rtc_counter_t timeout_counter = watch_rtc_get_counter() + FIFO_TIMEOUT; for(int i = 0; i < fifo_data->count; i++) { + if (watch_rtc_get_counter() > timeout_counter) { + break; + } fifo_data->readings[i] = lis2dw_get_raw_reading(); } From 19f789e428e489290f6e05f663e4b8e7b7a7fb48 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Tue, 7 Oct 2025 08:07:02 -0400 Subject: [PATCH 46/53] Made the timeout for lis2dw_read_fifo a variable --- .../sensor/accelerometer_data_acquisition_face.c | 4 ++-- watch-faces/sensor/lis2dw_monitor_face.c | 2 +- watch-library/shared/driver/lis2dw.c | 7 ++++--- watch-library/shared/driver/lis2dw.h | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/legacy/watch_faces/sensor/accelerometer_data_acquisition_face.c b/legacy/watch_faces/sensor/accelerometer_data_acquisition_face.c index 72584749..64a25a87 100644 --- a/legacy/watch_faces/sensor/accelerometer_data_acquisition_face.c +++ b/legacy/watch_faces/sensor/accelerometer_data_acquisition_face.c @@ -448,13 +448,13 @@ static void start_reading(accelerometer_data_acquisition_state_t *state) { state->records[state->pos++] = record; lis2dw_fifo_t fifo; - lis2dw_read_fifo(&fifo); // dump the fifo, this starts a fresh round of data in continue_reading + lis2dw_read_fifo(&fifo, 100); // dump the fifo, this starts a fresh round of data in continue_reading } static void continue_reading(accelerometer_data_acquisition_state_t *state) { printf("Continue reading\n"); lis2dw_fifo_t fifo; - lis2dw_read_fifo(&fifo); + lis2dw_read_fifo(&fifo, 100); fifo.count = min(fifo.count, 25); // hacky, but we need a consistent data rate; if we got a 26th data point, chuck it. uint8_t offset = 4 * (25 - fifo.count); // also hacky: we're sometimes short at the start. align to beginning of next second. diff --git a/watch-faces/sensor/lis2dw_monitor_face.c b/watch-faces/sensor/lis2dw_monitor_face.c index a717fe46..4459cf73 100644 --- a/watch-faces/sensor/lis2dw_monitor_face.c +++ b/watch-faces/sensor/lis2dw_monitor_face.c @@ -420,7 +420,7 @@ static void _monitor_update(lis2dw_monitor_state_t *state) lis2dw_fifo_t fifo; float x = 0, y = 0, z = 0; - lis2dw_read_fifo(&fifo); + lis2dw_read_fifo(&fifo, 25); if (fifo.count == 0) { return; } diff --git a/watch-library/shared/driver/lis2dw.c b/watch-library/shared/driver/lis2dw.c index fbf1ac75..6f77a94e 100644 --- a/watch-library/shared/driver/lis2dw.c +++ b/watch-library/shared/driver/lis2dw.c @@ -277,15 +277,15 @@ inline void lis2dw_disable_fifo(void) { #endif } -#define FIFO_TIMEOUT 102 // This is timeout seconds * 128[RTC_CNT_HZ]. So 800ms is 102 -bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data) { +bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data, uint32_t timeout) { + // timeout is in terms of 1/RTC_CNT_HZ seconds (likely 128 timeouts is one second) #ifdef I2C_SERCOM uint8_t temp = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_FIFO_SAMPLE); bool overrun = !!(temp & LIS2DW_FIFO_SAMPLE_OVERRUN); fifo_data->count = temp & LIS2DW_FIFO_SAMPLE_COUNT; - rtc_counter_t timeout_counter = watch_rtc_get_counter() + FIFO_TIMEOUT; + rtc_counter_t timeout_counter = watch_rtc_get_counter() + timeout; for(int i = 0; i < fifo_data->count; i++) { if (watch_rtc_get_counter() > timeout_counter) { break; @@ -296,6 +296,7 @@ bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data) { return overrun; #else (void) fifo_data; + (void) timeout; return false; #endif } diff --git a/watch-library/shared/driver/lis2dw.h b/watch-library/shared/driver/lis2dw.h index f7b0a801..d3a09dc4 100644 --- a/watch-library/shared/driver/lis2dw.h +++ b/watch-library/shared/driver/lis2dw.h @@ -345,7 +345,7 @@ void lis2dw_enable_fifo(void); void lis2dw_disable_fifo(void); -bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data); +bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data, uint32_t timeout); void lis2dw_clear_fifo(void); From 625e9209706650ca8ed2a2ab361c26a8ed1ff93c Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Tue, 7 Oct 2025 20:39:58 -0400 Subject: [PATCH 47/53] lis2dw_read_fifo(&fifo, 100 / DISPLAY_FREQUENCY)) --- watch-faces/sensor/lis2dw_monitor_face.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watch-faces/sensor/lis2dw_monitor_face.c b/watch-faces/sensor/lis2dw_monitor_face.c index 4459cf73..46cc15f3 100644 --- a/watch-faces/sensor/lis2dw_monitor_face.c +++ b/watch-faces/sensor/lis2dw_monitor_face.c @@ -420,7 +420,7 @@ static void _monitor_update(lis2dw_monitor_state_t *state) lis2dw_fifo_t fifo; float x = 0, y = 0, z = 0; - lis2dw_read_fifo(&fifo, 25); + lis2dw_read_fifo(&fifo, 100 / DISPLAY_FREQUENCY); if (fifo.count == 0) { return; } From 757f1a1de758524bedf3a6447c9015134d60149c Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Wed, 8 Oct 2025 17:48:34 -0400 Subject: [PATCH 48/53] defined LIS2DW_FIFO_TIMEOUT_SECOND --- .../watch_faces/sensor/accelerometer_data_acquisition_face.c | 4 ++-- watch-faces/sensor/lis2dw_monitor_face.c | 2 +- watch-library/shared/driver/lis2dw.h | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/legacy/watch_faces/sensor/accelerometer_data_acquisition_face.c b/legacy/watch_faces/sensor/accelerometer_data_acquisition_face.c index 64a25a87..968a7b81 100644 --- a/legacy/watch_faces/sensor/accelerometer_data_acquisition_face.c +++ b/legacy/watch_faces/sensor/accelerometer_data_acquisition_face.c @@ -448,13 +448,13 @@ static void start_reading(accelerometer_data_acquisition_state_t *state) { state->records[state->pos++] = record; lis2dw_fifo_t fifo; - lis2dw_read_fifo(&fifo, 100); // dump the fifo, this starts a fresh round of data in continue_reading + lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT_SECOND); // dump the fifo, this starts a fresh round of data in continue_reading } static void continue_reading(accelerometer_data_acquisition_state_t *state) { printf("Continue reading\n"); lis2dw_fifo_t fifo; - lis2dw_read_fifo(&fifo, 100); + lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT_SECOND); fifo.count = min(fifo.count, 25); // hacky, but we need a consistent data rate; if we got a 26th data point, chuck it. uint8_t offset = 4 * (25 - fifo.count); // also hacky: we're sometimes short at the start. align to beginning of next second. diff --git a/watch-faces/sensor/lis2dw_monitor_face.c b/watch-faces/sensor/lis2dw_monitor_face.c index 46cc15f3..ade0df55 100644 --- a/watch-faces/sensor/lis2dw_monitor_face.c +++ b/watch-faces/sensor/lis2dw_monitor_face.c @@ -420,7 +420,7 @@ static void _monitor_update(lis2dw_monitor_state_t *state) lis2dw_fifo_t fifo; float x = 0, y = 0, z = 0; - lis2dw_read_fifo(&fifo, 100 / DISPLAY_FREQUENCY); + lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT_SECOND / DISPLAY_FREQUENCY); if (fifo.count == 0) { return; } diff --git a/watch-library/shared/driver/lis2dw.h b/watch-library/shared/driver/lis2dw.h index d3a09dc4..f81c7750 100644 --- a/watch-library/shared/driver/lis2dw.h +++ b/watch-library/shared/driver/lis2dw.h @@ -301,6 +301,8 @@ typedef enum { #define LIS2DW_CTRL7_VAL_HP_REF_MODE 0b00000010 #define LIS2DW_CTRL7_VAL_LPASS_ON6D 0b00000001 +#define LIS2DW_FIFO_TIMEOUT_SECOND 100 + bool lis2dw_begin(void); uint8_t lis2dw_get_device_id(void); From e8750713f8e9034c8ff3b4d4b6fbe323c27154cb Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Thu, 13 Nov 2025 08:19:22 -0500 Subject: [PATCH 49/53] Renamed LIS2DW_FIFO_TIMEOUT_SECOND to LIS2DW_FIFO_TIMEOUT --- .../watch_faces/sensor/accelerometer_data_acquisition_face.c | 4 ++-- watch-faces/sensor/lis2dw_monitor_face.c | 2 +- watch-library/shared/driver/lis2dw.h | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/legacy/watch_faces/sensor/accelerometer_data_acquisition_face.c b/legacy/watch_faces/sensor/accelerometer_data_acquisition_face.c index 968a7b81..cb49e018 100644 --- a/legacy/watch_faces/sensor/accelerometer_data_acquisition_face.c +++ b/legacy/watch_faces/sensor/accelerometer_data_acquisition_face.c @@ -448,13 +448,13 @@ static void start_reading(accelerometer_data_acquisition_state_t *state) { state->records[state->pos++] = record; lis2dw_fifo_t fifo; - lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT_SECOND); // dump the fifo, this starts a fresh round of data in continue_reading + lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT); // dump the fifo, this starts a fresh round of data in continue_reading } static void continue_reading(accelerometer_data_acquisition_state_t *state) { printf("Continue reading\n"); lis2dw_fifo_t fifo; - lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT_SECOND); + lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT); fifo.count = min(fifo.count, 25); // hacky, but we need a consistent data rate; if we got a 26th data point, chuck it. uint8_t offset = 4 * (25 - fifo.count); // also hacky: we're sometimes short at the start. align to beginning of next second. diff --git a/watch-faces/sensor/lis2dw_monitor_face.c b/watch-faces/sensor/lis2dw_monitor_face.c index ade0df55..8afb7d3e 100644 --- a/watch-faces/sensor/lis2dw_monitor_face.c +++ b/watch-faces/sensor/lis2dw_monitor_face.c @@ -420,7 +420,7 @@ static void _monitor_update(lis2dw_monitor_state_t *state) lis2dw_fifo_t fifo; float x = 0, y = 0, z = 0; - lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT_SECOND / DISPLAY_FREQUENCY); + lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT / DISPLAY_FREQUENCY); if (fifo.count == 0) { return; } diff --git a/watch-library/shared/driver/lis2dw.h b/watch-library/shared/driver/lis2dw.h index f81c7750..fc5947e0 100644 --- a/watch-library/shared/driver/lis2dw.h +++ b/watch-library/shared/driver/lis2dw.h @@ -301,7 +301,7 @@ typedef enum { #define LIS2DW_CTRL7_VAL_HP_REF_MODE 0b00000010 #define LIS2DW_CTRL7_VAL_LPASS_ON6D 0b00000001 -#define LIS2DW_FIFO_TIMEOUT_SECOND 100 +#define LIS2DW_FIFO_TIMEOUT 100 // timeout is in terms of 1/RTC_CNT_HZ seconds (likely 128 timeouts is one second) bool lis2dw_begin(void); From 994c12cc30748c4443f360e5b982186a0e180226 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Tue, 7 Oct 2025 22:26:24 -0400 Subject: [PATCH 50/53] add REALLY_LONG_PRESS event after a button is held down for 1.5s --- movement.c | 50 ++++++++++++++++++++++++++++++++++++++++++-------- movement.h | 6 ++++++ 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/movement.c b/movement.c index 361c8c89..f71ee58f 100644 --- a/movement.c +++ b/movement.c @@ -24,6 +24,8 @@ */ #define MOVEMENT_LONG_PRESS_TICKS 64 +#define MOVEMENT_REALLY_LONG_PRESS_TICKS 192 +#define MOVEMENT_MAX_LONG_PRESS_TICKS 1280 // get a chance to check if a button held down over 10 seconds is a glitch #include #include @@ -61,9 +63,9 @@ watch_date_time_t scheduled_tasks[MOVEMENT_NUM_FACES]; const int32_t movement_le_inactivity_deadlines[8] = {INT_MAX, 600, 3600, 7200, 21600, 43200, 86400, 604800}; const int16_t movement_timeout_inactivity_deadlines[4] = {60, 120, 300, 1800}; -const uint32_t _movement_mode_button_events_mask = 0b1111 << EVENT_MODE_BUTTON_DOWN; -const uint32_t _movement_light_button_events_mask = 0b1111 << EVENT_LIGHT_BUTTON_DOWN; -const uint32_t _movement_alarm_button_events_mask = 0b1111 << EVENT_ALARM_BUTTON_DOWN; +const uint32_t _movement_mode_button_events_mask = 0b111111 << EVENT_MODE_BUTTON_DOWN; +const uint32_t _movement_light_button_events_mask = 0b111111 << EVENT_LIGHT_BUTTON_DOWN; +const uint32_t _movement_alarm_button_events_mask = 0b111111 << EVENT_ALARM_BUTTON_DOWN; const uint32_t _movement_button_events_mask = _movement_mode_button_events_mask | _movement_light_button_events_mask | _movement_alarm_button_events_mask; typedef struct { @@ -298,6 +300,7 @@ static uint32_t _movement_get_accelerometer_events() { static void _movement_handle_button_presses(uint32_t pending_events) { bool any_up = false; bool any_down = false; + bool any_long = false; movement_button_t* buttons[3] = { &movement_volatile_state.mode_button, @@ -322,10 +325,23 @@ static void _movement_handle_button_presses(uint32_t pending_events) { movement_volatile_state.passthrough_events &= ~button_events_masks[i]; } + // If a long press occurred + if (pending_events & (1 << button->down_event + 2)) { + watch_rtc_register_comp_callback_no_schedule(button->cb_longpress, button->down_timestamp + MOVEMENT_REALLY_LONG_PRESS_TICKS, button->timeout_index); + any_long = true; + } + + // If a really long press occurred + if (pending_events & (1 << button->down_event + 4)) { + watch_rtc_register_comp_callback_no_schedule(button->cb_longpress, button->down_timestamp + MOVEMENT_MAX_LONG_PRESS_TICKS, button->timeout_index); + any_long = true; + } + // If a button up or button long up occurred if (pending_events & ( (1 << (button->down_event + 1)) | - (1 << (button->down_event + 3)) + (1 << (button->down_event + 3)) | + (1 << (button->down_event + 5)) )) { // We cancel the timeout if it hasn't fired yet watch_rtc_disable_comp_callback_no_schedule(button->timeout_index); @@ -343,7 +359,7 @@ static void _movement_handle_button_presses(uint32_t pending_events) { } } - if (any_down || any_up) { + if (any_down || any_up || any_long) { _movement_reset_inactivity_countdown(); movement_volatile_state.schedule_next_comp = true; } @@ -1391,7 +1407,9 @@ static movement_event_type_t _process_button_event(bool pin_level, movement_butt #if MOVEMENT_DEBOUNCE_TICKS button->up_timestamp = counter; #endif - if ((counter - button->down_timestamp) >= MOVEMENT_LONG_PRESS_TICKS) { + if ((counter - button->down_timestamp) >= MOVEMENT_REALLY_LONG_PRESS_TICKS) { + event_type = button->down_event + 5; + } else if ((counter - button->down_timestamp) >= MOVEMENT_LONG_PRESS_TICKS) { event_type = button->down_event + 3; } else { event_type = button->down_event + 1; @@ -1424,8 +1442,18 @@ static movement_event_type_t _process_button_longpress_timeout(bool pin_level, m return EVENT_NONE; } + uint32_t counter = watch_rtc_get_counter(); + bool max_long_press = (counter - button->down_timestamp) >= MOVEMENT_MAX_LONG_PRESS_TICKS; + bool really_long_press = (counter - button->down_timestamp) >= MOVEMENT_REALLY_LONG_PRESS_TICKS; + if (pin_level) { - return button->down_event + 2; // event_longpress + if (max_long_press) { + return EVENT_NONE; // no further events left to emit + } else if (really_long_press) { + return button->down_event + 4; // event_really_longpress + } else { + return button->down_event + 2; // event_longpress + } } else { // hypotetical corner case: if the timeout fired but the pin level is actually up, we may have missed/rejected the up event, so fire it here #if MOVEMENT_DEBOUNCE_TICKS @@ -1433,7 +1461,13 @@ static movement_event_type_t _process_button_longpress_timeout(bool pin_level, m button->up_timestamp = button->down_timestamp; #endif button->is_down = false; - return button->down_event + 1; // event_up + if (max_long_press) { + return button->down_event + 5; // event_really_long_up + } else if (really_long_press) { + return button->down_event + 3; // event_long_up + } else { + return button->down_event + 1; // event_up + } } } diff --git a/movement.h b/movement.h index 94f9b83e..305a2e8c 100644 --- a/movement.h +++ b/movement.h @@ -121,14 +121,20 @@ typedef enum { EVENT_LIGHT_BUTTON_UP, // The light button was pressed for less than half a second, and released. EVENT_LIGHT_LONG_PRESS, // The light button was held for over half a second, but not yet released. EVENT_LIGHT_LONG_UP, // The light button was held for over half a second, and released. + EVENT_LIGHT_REALLY_LONG_PRESS, // The light button was held for more than 1.5 second, note yet released. + EVENT_LIGHT_REALLY_LONG_UP, // The light button was held for more than 1.5 second, and released. EVENT_MODE_BUTTON_DOWN, // The mode button has been pressed, but not yet released. EVENT_MODE_BUTTON_UP, // The mode button was pressed for less than half a second, and released. EVENT_MODE_LONG_PRESS, // The mode button was held for over half a second, but not yet released. EVENT_MODE_LONG_UP, // The mode button was held for over half a second, and released. NOTE: your watch face will resign immediately after receiving this event. + EVENT_MODE_REALLY_LONG_PRESS, // The mode button was held for more than 1.5 second, note yet released. + EVENT_MODE_REALLY_LONG_UP, // The mode button was held for more than 1.5 second, and released. EVENT_ALARM_BUTTON_DOWN, // The alarm button has been pressed, but not yet released. EVENT_ALARM_BUTTON_UP, // The alarm button was pressed for less than half a second, and released. EVENT_ALARM_LONG_PRESS, // The alarm button was held for over half a second, but not yet released. EVENT_ALARM_LONG_UP, // The alarm button was held for over half a second, and released. + EVENT_ALARM_REALLY_LONG_PRESS, // The alarm button was held for more than 1.5 second, note yet released. + EVENT_ALARM_REALLY_LONG_UP, // The alarm button was held for more than 1.5 second, and released. EVENT_ACCELEROMETER_WAKE, // The accelerometer has detected motion and woken up. EVENT_SINGLE_TAP, // Accelerometer detected a single tap. This event is not yet implemented. From 51ca839f3bcf5da62f933bd4356a1d67815605a4 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 23 Nov 2025 12:27:00 -0500 Subject: [PATCH 51/53] Removed all of the compiler warnings --- movement.c | 4 ++-- watch-faces/demo/rtccount_face.c | 8 ++++---- watch-library/hardware/watch/watch_rtc.c | 1 - watch-library/simulator/watch/watch_rtc.c | 1 - 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/movement.c b/movement.c index f71ee58f..bae267b3 100644 --- a/movement.c +++ b/movement.c @@ -326,13 +326,13 @@ static void _movement_handle_button_presses(uint32_t pending_events) { } // If a long press occurred - if (pending_events & (1 << button->down_event + 2)) { + if (pending_events & (1 << (button->down_event + 2))) { watch_rtc_register_comp_callback_no_schedule(button->cb_longpress, button->down_timestamp + MOVEMENT_REALLY_LONG_PRESS_TICKS, button->timeout_index); any_long = true; } // If a really long press occurred - if (pending_events & (1 << button->down_event + 4)) { + if (pending_events & (1 << (button->down_event + 4))) { watch_rtc_register_comp_callback_no_schedule(button->cb_longpress, button->down_timestamp + MOVEMENT_MAX_LONG_PRESS_TICKS, button->timeout_index); any_long = true; } diff --git a/watch-faces/demo/rtccount_face.c b/watch-faces/demo/rtccount_face.c index 72a3055a..6e7cb08c 100644 --- a/watch-faces/demo/rtccount_face.c +++ b/watch-faces/demo/rtccount_face.c @@ -93,7 +93,7 @@ static void _rtccount_face_draw(movement_event_t event, rtccount_state_t* state) switch (state->status) { case RTCCOUNT_STATUS_COUNTER: { - snprintf(buf, sizeof(buf), "%u", counter & COUNTER_MASK); + snprintf(buf, sizeof(buf), "%lu", counter & COUNTER_MASK); size_t len = strlen(buf); @@ -102,7 +102,7 @@ static void _rtccount_face_draw(movement_event_t event, rtccount_state_t* state) } case RTCCOUNT_STATUS_COUNTER_SUB: { - snprintf(buf, sizeof(buf), "%u", counter & 127); + snprintf(buf, sizeof(buf), "%lu", counter & 127); size_t len = strlen(buf); @@ -111,7 +111,7 @@ static void _rtccount_face_draw(movement_event_t event, rtccount_state_t* state) } case RTCCOUNT_STATUS_MINUTES: { - snprintf(buf, sizeof(buf), "%u", state->n_top_of_minute & COUNTER_MASK); + snprintf(buf, sizeof(buf), "%lu", state->n_top_of_minute & COUNTER_MASK); size_t len = strlen(buf); @@ -122,7 +122,7 @@ static void _rtccount_face_draw(movement_event_t event, rtccount_state_t* state) case RTCCOUNT_STATUS_MINUTES_DIFF: { uint32_t elapsed_minutes = (movement_get_utc_timestamp() - state->ref_timestamp) / 60; - snprintf(buf, sizeof(buf), "%u", (elapsed_minutes - state->n_top_of_minute) & COUNTER_MASK); + snprintf(buf, sizeof(buf), "%lu", (elapsed_minutes - state->n_top_of_minute) & COUNTER_MASK); size_t len = strlen(buf); diff --git a/watch-library/hardware/watch/watch_rtc.c b/watch-library/hardware/watch/watch_rtc.c index 114c1ee4..7cfa81c2 100644 --- a/watch-library/hardware/watch/watch_rtc.c +++ b/watch-library/hardware/watch/watch_rtc.c @@ -37,7 +37,6 @@ static const uint32_t RTC_CNT_HZ = RTC_OSC_HZ >> RTC_PRESCALER_DIV; // 1024 / 2^ static const uint32_t RTC_CNT_SUBSECOND_MASK = RTC_CNT_HZ - 1; static const uint32_t RTC_CNT_DIV = RTC_OSC_DIV - RTC_PRESCALER_DIV; // 7 static const uint32_t RTC_CNT_TICKS_PER_MINUTE = RTC_CNT_HZ * 60; -static const uint32_t RTC_CNT_TICKS_PER_HOUR = RTC_CNT_TICKS_PER_MINUTE * 60; static const uint32_t RTC_COMP_GRACE_PERIOD = 4; diff --git a/watch-library/simulator/watch/watch_rtc.c b/watch-library/simulator/watch/watch_rtc.c index 1347d797..4927ba14 100644 --- a/watch-library/simulator/watch/watch_rtc.c +++ b/watch-library/simulator/watch/watch_rtc.c @@ -34,7 +34,6 @@ static const uint32_t RTC_CNT_HZ = 128; static const uint32_t RTC_CNT_SUBSECOND_MASK = RTC_CNT_HZ - 1; static const uint32_t RTC_CNT_DIV = 7; static const uint32_t RTC_CNT_TICKS_PER_MINUTE = RTC_CNT_HZ * 60; -static const uint32_t RTC_CNT_TICKS_PER_HOUR = RTC_CNT_TICKS_PER_MINUTE * 60; static uint32_t counter_interval; static uint32_t counter; From e353d4760f951031723bc8e06d5a139b63c82c03 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Wed, 26 Nov 2025 13:23:42 -0500 Subject: [PATCH 52/53] disable REALLY_LONG_UP event for now --- movement.c | 16 +++++++++------- movement.h | 6 +++--- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/movement.c b/movement.c index bae267b3..f0fb9609 100644 --- a/movement.c +++ b/movement.c @@ -63,9 +63,9 @@ watch_date_time_t scheduled_tasks[MOVEMENT_NUM_FACES]; const int32_t movement_le_inactivity_deadlines[8] = {INT_MAX, 600, 3600, 7200, 21600, 43200, 86400, 604800}; const int16_t movement_timeout_inactivity_deadlines[4] = {60, 120, 300, 1800}; -const uint32_t _movement_mode_button_events_mask = 0b111111 << EVENT_MODE_BUTTON_DOWN; -const uint32_t _movement_light_button_events_mask = 0b111111 << EVENT_LIGHT_BUTTON_DOWN; -const uint32_t _movement_alarm_button_events_mask = 0b111111 << EVENT_ALARM_BUTTON_DOWN; +const uint32_t _movement_mode_button_events_mask = 0b11111 << EVENT_MODE_BUTTON_DOWN; +const uint32_t _movement_light_button_events_mask = 0b11111 << EVENT_LIGHT_BUTTON_DOWN; +const uint32_t _movement_alarm_button_events_mask = 0b11111 << EVENT_ALARM_BUTTON_DOWN; const uint32_t _movement_button_events_mask = _movement_mode_button_events_mask | _movement_light_button_events_mask | _movement_alarm_button_events_mask; typedef struct { @@ -340,8 +340,8 @@ static void _movement_handle_button_presses(uint32_t pending_events) { // If a button up or button long up occurred if (pending_events & ( (1 << (button->down_event + 1)) | - (1 << (button->down_event + 3)) | - (1 << (button->down_event + 5)) + (1 << (button->down_event + 3)) + // (1 << (button->down_event + 5)) )) { // We cancel the timeout if it hasn't fired yet watch_rtc_disable_comp_callback_no_schedule(button->timeout_index); @@ -1408,7 +1408,8 @@ static movement_event_type_t _process_button_event(bool pin_level, movement_butt button->up_timestamp = counter; #endif if ((counter - button->down_timestamp) >= MOVEMENT_REALLY_LONG_PRESS_TICKS) { - event_type = button->down_event + 5; + // event_type = button->down_event + 5; + event_type = button->down_event + 3; // TODO: swith to REALLY_LONG_UP } else if ((counter - button->down_timestamp) >= MOVEMENT_LONG_PRESS_TICKS) { event_type = button->down_event + 3; } else { @@ -1462,7 +1463,8 @@ static movement_event_type_t _process_button_longpress_timeout(bool pin_level, m #endif button->is_down = false; if (max_long_press) { - return button->down_event + 5; // event_really_long_up + // return button->down_event + 5; // event_really_long_up + return button->down_event + 3; // event_long_up TODO: use really_long_up } else if (really_long_press) { return button->down_event + 3; // event_long_up } else { diff --git a/movement.h b/movement.h index 305a2e8c..1bbd9638 100644 --- a/movement.h +++ b/movement.h @@ -122,19 +122,19 @@ typedef enum { EVENT_LIGHT_LONG_PRESS, // The light button was held for over half a second, but not yet released. EVENT_LIGHT_LONG_UP, // The light button was held for over half a second, and released. EVENT_LIGHT_REALLY_LONG_PRESS, // The light button was held for more than 1.5 second, note yet released. - EVENT_LIGHT_REALLY_LONG_UP, // The light button was held for more than 1.5 second, and released. + // EVENT_LIGHT_REALLY_LONG_UP, // The light button was held for more than 1.5 second, and released. EVENT_MODE_BUTTON_DOWN, // The mode button has been pressed, but not yet released. EVENT_MODE_BUTTON_UP, // The mode button was pressed for less than half a second, and released. EVENT_MODE_LONG_PRESS, // The mode button was held for over half a second, but not yet released. EVENT_MODE_LONG_UP, // The mode button was held for over half a second, and released. NOTE: your watch face will resign immediately after receiving this event. EVENT_MODE_REALLY_LONG_PRESS, // The mode button was held for more than 1.5 second, note yet released. - EVENT_MODE_REALLY_LONG_UP, // The mode button was held for more than 1.5 second, and released. + // EVENT_MODE_REALLY_LONG_UP, // The mode button was held for more than 1.5 second, and released. EVENT_ALARM_BUTTON_DOWN, // The alarm button has been pressed, but not yet released. EVENT_ALARM_BUTTON_UP, // The alarm button was pressed for less than half a second, and released. EVENT_ALARM_LONG_PRESS, // The alarm button was held for over half a second, but not yet released. EVENT_ALARM_LONG_UP, // The alarm button was held for over half a second, and released. EVENT_ALARM_REALLY_LONG_PRESS, // The alarm button was held for more than 1.5 second, note yet released. - EVENT_ALARM_REALLY_LONG_UP, // The alarm button was held for more than 1.5 second, and released. + // EVENT_ALARM_REALLY_LONG_UP, // The alarm button was held for more than 1.5 second, and released. EVENT_ACCELEROMETER_WAKE, // The accelerometer has detected motion and woken up. EVENT_SINGLE_TAP, // Accelerometer detected a single tap. This event is not yet implemented. From f6fad64eebe0b93b086c8bc0685fb049cef0e518 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Sun, 7 Dec 2025 21:09:39 -0500 Subject: [PATCH 53/53] don't force-reset the state of the RTC when rebooting --- watch-library/hardware/watch/rtc32.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/watch-library/hardware/watch/rtc32.c b/watch-library/hardware/watch/rtc32.c index 934564bb..d4ac9b27 100644 --- a/watch-library/hardware/watch/rtc32.c +++ b/watch-library/hardware/watch/rtc32.c @@ -61,9 +61,7 @@ void rtc_init(void) { MCLK->APBAMASK.reg |= MCLK_APBAMASK_RTC; #endif - // if (rtc_is_enabled()) return; // don't reset the RTC if it's already set up. - // Reset everything, once things are stabilized we can think about preserving some state - CTRLREG.bit.ENABLE = 0; + if (rtc_is_enabled()) return; // don't reset the RTC if it's already set up. _rtc_sync(); CTRLREG.bit.SWRST = 1;