From eb9ec8659c42e34dc1b31e3ff591ac2bf842271f Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Sat, 26 Jul 2025 21:40:22 -0400 Subject: [PATCH] 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) {}