diff --git a/make.mk b/make.mk index 955ea3102..09a862f90 100644 --- a/make.mk +++ b/make.mk @@ -121,6 +121,7 @@ SRCS += \ $(TOP)/watch-library/hardware/watch/watch_storage.c \ $(TOP)/watch-library/hardware/watch/watch_deepsleep.c \ $(TOP)/watch-library/hardware/watch/watch_private.c \ + $(TOP)/watch-library/hardware/watch/watch_hpt.c \ $(TOP)/watch-library/hardware/watch/watch.c \ $(TOP)/watch-library/hardware/hal/src/hal_atomic.c \ $(TOP)/watch-library/hardware/hal/src/hal_delay.c \ @@ -195,6 +196,7 @@ SRCS += \ $(TOP)/watch-library/simulator/watch/watch_storage.c \ $(TOP)/watch-library/simulator/watch/watch_deepsleep.c \ $(TOP)/watch-library/simulator/watch/watch_private.c \ + $(TOP)/watch-library/simulator/watch/watch_hpt.c \ $(TOP)/watch-library/simulator/watch/watch.c \ $(TOP)/watch-library/shared/driver/thermistor_driver.c \ $(TOP)/watch-library/shared/driver/opt3001.c \ diff --git a/movement/make/Makefile b/movement/make/Makefile index 42dfc644d..2d2a93797 100644 --- a/movement/make/Makefile +++ b/movement/make/Makefile @@ -87,7 +87,6 @@ SRCS += \ ../watch_faces/complication/ratemeter_face.c \ ../watch_faces/complication/interval_face.c \ ../watch_faces/complication/rpn_calculator_alt_face.c \ - ../watch_faces/complication/stock_stopwatch_face.c \ ../watch_faces/complication/tachymeter_face.c \ ../watch_faces/settings/nanosec_face.c \ ../watch_faces/settings/finetune_face.c \ @@ -126,6 +125,11 @@ SRCS += \ ../watch_faces/clock/minute_repeater_decimal_face.c \ ../watch_faces/complication/tuning_tones_face.c \ ../watch_faces/complication/kitchen_conversions_face.c \ + ../watch_faces/complication/hpt_lapsplit_chrono_face.c \ + ../watch_faces/clock/hpt_led_test_face.c \ + ../watch_faces/complication/dual_timer_face.c \ + ../watch_faces/complication/hpt_countdown_face.c \ + ../watch_faces/complication/stock_stopwatch_face.c \ # New watch faces go above this line. # Leave this line at the bottom of the file; it has all the targets for making your project. diff --git a/movement/movement.c b/movement/movement.c index d780a2f37..d77676c55 100644 --- a/movement/movement.c +++ b/movement/movement.c @@ -31,6 +31,8 @@ #include #include #include "watch.h" +#include "watch_utility.h" +#include "watch_hpt.h" #include "filesystem.h" #include "movement.h" @@ -76,6 +78,62 @@ movement_state_t movement_state; void * watch_face_contexts[MOVEMENT_NUM_FACES]; watch_date_time scheduled_tasks[MOVEMENT_NUM_FACES]; + + +// --- High-Precision Timer stuff --- + +// the number of available HPT request lines +// (things other than faces can use the HPT) +#define HPT_NUM_REQUESTS (MOVEMENT_NUM_FACES +1) + +// buzzer implementation using HPT +#define MOVEMENT_BUZZER_HPT (MOVEMENT_NUM_FACES) +void _movement_play_next_buzzer_note(void); + +// the number of bytes used to store the active HPT requests +#define HPT_REQUESTS_SIZE (1 + ((HPT_NUM_REQUESTS-1)/8)) + +// Bit-field used to keep track of faces that have requested the use of the HPT +volatile uint8_t hpt_requests[HPT_REQUESTS_SIZE]; + +/** + * Sets the bit indicating that the given face wants to keep the HPT enabled. +*/ +static inline void hpt_set_request(uint8_t face_idx) { + uint8_t request_flag_idx = face_idx / 8; + uint8_t request_flag_bit = face_idx % 8; + + hpt_requests[request_flag_idx] |= (1 << request_flag_bit); +} + +/** + * Clears the bit indicating that the given face wants to keep the HPT enabled. +*/ +static inline void hpt_clr_request(uint8_t face_idx) { + uint8_t request_flag_idx = face_idx / 8; + uint8_t request_flag_bit = face_idx % 8; + + hpt_requests[request_flag_idx] &= ~(1 << request_flag_bit); +} + +/** + * Returns true if there are any open HPT enable requests. +*/ +static inline bool hpt_any_requests(void) { + for(uint8_t request_idx = 0; request_idx < HPT_REQUESTS_SIZE; ++request_idx) { + if(hpt_requests[request_idx] != 0) return true; + } + return false; +} + +// timestamps at which watch faces should have a HPT event triggered. UINT64_MAX for no callback +volatile uint64_t hpt_scheduled_events[HPT_NUM_REQUESTS]; + +// the number of times the high-precision timer has overflowed +volatile uint8_t hpt_overflows = 0; + +// --- End HPT stuff --- + 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; @@ -134,6 +192,7 @@ void cb_alarm_btn_extwake(void); void cb_alarm_fired(void); void cb_fast_tick(void); void cb_tick(void); +void cb_hpt(HPT_CALLBACK_CAUSE cause); static inline void _movement_reset_inactivity_countdown(void) { movement_state.le_mode_ticks = movement_le_inactivity_deadlines[movement_state.settings.bit.le_interval]; @@ -150,7 +209,6 @@ static inline void _movement_enable_fast_tick_if_needed(void) { 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); @@ -177,13 +235,16 @@ static void _movement_handle_scheduled_tasks(void) { if (scheduled_tasks[i].reg) { if (scheduled_tasks[i].reg == date_time.reg) { scheduled_tasks[i].reg = 0; - movement_event_t background_event = { EVENT_BACKGROUND_TASK, 0 }; + movement_event_t background_event = {EVENT_BACKGROUND_TASK, 0}; watch_faces[i].loop(background_event, &movement_state.settings, watch_face_contexts[i]); // check if loop scheduled a new task - if (scheduled_tasks[i].reg) { + if (scheduled_tasks[i].reg) + { num_active_tasks++; } - } else { + } + else + { num_active_tasks++; } } @@ -293,34 +354,20 @@ void movement_request_wake() { _movement_reset_inactivity_countdown(); } -void end_buzzing() { - movement_state.is_buzzing = false; -} - -void end_buzzing_and_disable_buzzer(void) { - end_buzzing(); - watch_disable_buzzer(); -} - 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, NULL); } +// Default alarm signal. Two beeps. +int8_t alarm_sequence[] = { + BUZZER_NOTE_C8, 1, + BUZZER_NOTE_REST, 1, + BUZZER_NOTE_C8, 1, + BUZZER_NOTE_REST, 5, + -4, 1, // <- loops_idx + 0, +}; + void movement_play_alarm(void) { movement_play_alarm_beeps(5, BUZZER_NOTE_C8); } @@ -328,11 +375,14 @@ void movement_play_alarm(void) { void movement_play_alarm_beeps(uint8_t rounds, BuzzerNote alarm_note) { 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(); + + // modify alarm sequence to match desired note and repeats + alarm_sequence[0] = alarm_note; // first tone + alarm_sequence[4] = alarm_note; // second tone + alarm_sequence[9] = (rounds-1); // repeat count + + // play sequence, each note is 1/8 of a second long + movement_play_sequence_speed(alarm_sequence, NULL, 1024/8); } uint8_t movement_claim_backup_register(void) { @@ -357,7 +407,6 @@ void app_init(void) { movement_state.settings.bit.le_interval = 2; movement_state.settings.bit.led_duration = 1; movement_state.light_ticks = -1; - movement_state.alarm_ticks = -1; movement_state.next_available_backup_register = 4; _movement_reset_inactivity_countdown(); @@ -386,6 +435,8 @@ void app_setup(void) { static bool is_first_launch = true; if (is_first_launch) { + is_first_launch = false; + #ifdef MOVEMENT_CUSTOM_BOOT_COMMANDS MOVEMENT_CUSTOM_BOOT_COMMANDS() #endif @@ -393,8 +444,15 @@ void app_setup(void) { for(uint8_t i = 0; i < MOVEMENT_NUM_FACES; i++) { watch_face_contexts[i] = NULL; scheduled_tasks[i].reg = 0; - is_first_launch = false; + hpt_scheduled_events[i] = UINT64_MAX; } + + for(uint8_t i=0; i < HPT_REQUESTS_SIZE; ++i) { + hpt_requests[i] = 0; + } + + hpt_overflows = 0; + watch_hpt_init(&cb_hpt); // set up the 1 minute alarm (for background tasks and low power updates) watch_date_time alarm_time; @@ -451,11 +509,10 @@ static void _sleep_mode_app_loop(void) { 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(movement_state.next_face_idx ? BUZZER_NOTE_C7 : BUZZER_NOTE_C8, 50); + movement_play_note(movement_state.next_face_idx ? BUZZER_NOTE_C7 : BUZZER_NOTE_C8, 50); } wf->resign(&movement_state.settings, watch_face_contexts[movement_state.current_face_idx]); movement_state.current_face_idx = movement_state.next_face_idx; @@ -498,10 +555,7 @@ bool app_loop(void) { // 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; - } + // ourselves 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. @@ -541,26 +595,6 @@ bool app_loop(void) { } } - // 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(); - } - } - // if we are plugged into USB, handle the file browser tasks if (watch_is_usb_enabled()) { char line[256] = {0}; @@ -592,11 +626,6 @@ bool app_loop(void) { // 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; @@ -605,7 +634,9 @@ bool app_loop(void) { static movement_event_type_t _figure_out_button_event(bool pin_level, movement_event_type_t button_down_event_type, uint16_t *down_timestamp) { // force alarm off if the user pressed a button. - if (movement_state.alarm_ticks) movement_state.alarm_ticks = 0; + if(pin_level) { + movement_silence_buzzer(); + } if (pin_level) { // handle rising edge @@ -656,7 +687,6 @@ void cb_alarm_fired(void) { 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... @@ -691,3 +721,328 @@ void cb_tick(void) { movement_state.subsecond++; } } + +/** Figures out the next scheduled event for the HPT to wake up for. + * Must be called whenever: + * - a new event is scheduled + * - an old event is deleted + * - an event is triggered + * - the timer overflows + */ +static void _movement_hpt_schedule_next_event() +{ + uint64_t next = UINT64_MAX; + for (uint8_t req_idx = 0; req_idx < HPT_NUM_REQUESTS; ++req_idx) + { + uint64_t event = hpt_scheduled_events[req_idx]; + if (event < next) + { + next = event; + } + } + + // if an event is scheduled and the timer is currently tracking this overflow cycle + if ((next != UINT64_MAX) && ((next >> 32) == hpt_overflows)) + { + uint32_t low_part = next; + watch_hpt_schedule_callback(low_part); + } + else + { + watch_hpt_disable_scheduled_callback(); + } +} + +void movement_hpt_request(void) +{ + movement_hpt_request_face(movement_state.current_face_idx); +} +void movement_hpt_request_face(uint8_t face_idx) +{ + bool hpt_was_enabled = hpt_any_requests(); + hpt_set_request(face_idx); + + if (!hpt_was_enabled) + { + watch_hpt_enable(); + } +} + +void movement_hpt_release(void) +{ + movement_hpt_release_face(movement_state.current_face_idx); +} +void movement_hpt_release_face(uint8_t face_idx) +{ + // cancel this face's background task if one was scheduled + hpt_scheduled_events[face_idx] = UINT64_MAX; + + // release the request this face had on the HPT + hpt_clr_request(face_idx); + if (!hpt_any_requests()) { + watch_hpt_disable(); + } +} + +// Default callback handler for HPT interrupts +void cb_hpt(HPT_CALLBACK_CAUSE cause) +{ + // This single interrupt vector must handle the overflow case and the match compare case + if (cause.overflow) + { + hpt_overflows++; + } + + // We must also take great care here, as the timer will continue ticking in the background if we take too long to service a request + + // Execute this in a loop because it's possible for a face to schedule a new background event while handling its current one + // And it's possible that they schedule it in the past for some reason. + bool canSleep = true; + + while (true) + { + bool eventTriggered = false; + + // TODO: What happens if an overflow occurs while we're inside this ISR? + + uint64_t now = movement_hpt_get(); + + // iterate over faces and execute any callbacks for faces that have scheduled them + for (uint8_t face_idx = 0; face_idx < HPT_NUM_REQUESTS; ++face_idx) + { + uint64_t face_time = hpt_scheduled_events[face_idx]; + if (face_time <= now) + { + // clear the scheduled event and allow the face to schedule a new one + hpt_scheduled_events[face_idx] = UINT64_MAX; + + if (face_idx < MOVEMENT_NUM_FACES) + { + // invoke the face + watch_face_t face = watch_faces[face_idx]; + movement_event_t event; + event.event_type = EVENT_HPT; + event.subsecond = 0; + + canSleep &= face.loop(event, &(movement_state.settings), watch_face_contexts[face_idx]); + } + else + { + if(face_idx == MOVEMENT_BUZZER_HPT) { + _movement_play_next_buzzer_note(); + } + } + + eventTriggered = true; + } + } + // keep doing this until no more scheduled events need to be processed + if (!eventTriggered) + { + break; + } + } + + _movement_hpt_schedule_next_event(); + if (canSleep) + { + // TODO put cpu to sleep? + } +} + +void movement_hpt_schedule(uint64_t timestamp) +{ + movement_hpt_schedule_face(timestamp, movement_state.current_face_idx); +} +void movement_hpt_schedule_face(uint64_t timestamp, uint8_t face_idx) +{ + hpt_scheduled_events[face_idx] = timestamp; + _movement_hpt_schedule_next_event(); +} + +void movement_hpt_cancel(void) { + movement_hpt_cancel_face(movement_state.current_face_idx); +} + +void movement_hpt_cancel_face(uint8_t face_idx) { + movement_hpt_schedule_face(UINT64_MAX, face_idx); +} + +uint64_t movement_hpt_get() +{ + uint64_t time; + while (true) + { + // the time we started this whole thing + uint32_t start = watch_hpt_get(); + + // create a timestamp by combining overflow count + time = (((uint64_t)hpt_overflows) << 32) | start; + + // check to see if an overflow occurred while we were doing all that + uint32_t end = watch_hpt_get(); + if (end >= start) + { + // everything is as we expect + break; + } + else + { + // an overflow has occurred, do it all again + } + } + + return time; +} + +uint64_t movement_hpt_get_fast() { + // don't bother checking for overflows or synchronizing the timer + return (((uint64_t)hpt_overflows) << 32) | watch_hpt_get_fast(); +} + +// New buzzer sequence logic implemented using HPT instead of TC3 in watch_buzzer.c + +// a pointer to the next note to play in the sequence +volatile int8_t *buzzer_sequence = 0; +// if a negative number is encountered in the buzzer sequence, this is the number of times it should be repeated +// If this value is zero, this is the first time the loop sequence was encountered, so set it to repeat and begin repeating +// if this value is greater than one, decrement it and repeat again +// if this value is exactly one, decrement it and do not repeat +volatile uint8_t buzzer_sequence_repeats = 0; + +// length of a single buzzer note. Default is 1/8th of a second +volatile uint16_t buzzer_note_length = 128; + +// the HPT timestamp that the current note is scheduled to stop playing at. +volatile uint64_t buzzer_note_end_ts = 0; + +void (*buzzer_sequence_end_callback)(void) = NULL; + +void movement_silence_buzzer(void) { + watch_set_buzzer_off(); + buzzer_sequence = NULL; + buzzer_sequence_repeats = 0; + movement_hpt_cancel_face(MOVEMENT_BUZZER_HPT); + movement_hpt_release_face(MOVEMENT_BUZZER_HPT); + + if(buzzer_sequence_end_callback) { + buzzer_sequence_end_callback(); + } + buzzer_sequence_end_callback = NULL; +} + +void _movement_play_next_buzzer_note(void) { + // if the buzzer sequence is null, stop + if(buzzer_sequence == 0) { + movement_silence_buzzer(); + return; + } + + int8_t nextNote = buzzer_sequence[0]; + if(nextNote == BUZZER_NOTE_END) { + movement_silence_buzzer(); + return; + } + + int8_t duration = buzzer_sequence[1]; + if(duration < 0) { + // error case, durations must always be greater than or equal to zero + movement_silence_buzzer(); + return; + } + + // check for jumps + if(nextNote < 0) { + if(duration == 0) { + // special case,if they do zero repeats, just ignore this one and play the next note. + // makes it easier for faces that want to do variable repeats + + buzzer_sequence += 2; + + } else if (duration < 0) { + // skip over the next section entirely + buzzer_sequence += (-nextNote * 2); + buzzer_sequence_repeats = 0; + } else if(buzzer_sequence_repeats == 0) { + // first time we encountered this repeat, set it up + // skip backward the given number of notes + buzzer_sequence += (nextNote * 2); + // set up the number of repeats + buzzer_sequence_repeats = duration; + } else if(buzzer_sequence_repeats == 1) { + // last time we should repeat, continue onward + // skip over the repeat marker to the next note + buzzer_sequence += 2; + buzzer_sequence_repeats = 0; + } else { + // mark a repeat and continue on again + // skip backwards the given number of notes + buzzer_sequence += (nextNote * 2); + // mark off a repeat + buzzer_sequence_repeats--; + } + + // previous conditional block should have moved to the next note by now + _movement_play_next_buzzer_note(); + } + else + { + // note is not an end marker or a loop marker, so play it + + // skip ahead to next note regardless + buzzer_sequence += 2; + + if (duration == 0) + { + // special case: skip the note + + _movement_play_next_buzzer_note(); + } + else + { + // set up the buzzer + if (nextNote == BUZZER_NOTE_REST) + { + watch_set_buzzer_off(); + } + else + { + watch_set_buzzer_period(NotePeriods[nextNote - 1]); + watch_set_buzzer_on(); + } + + // schedule the next note change + buzzer_note_end_ts += ((uint64_t)buzzer_note_length) * duration; + + movement_hpt_schedule_face(buzzer_note_end_ts, MOVEMENT_BUZZER_HPT); + } + } +} + +void movement_play_sequence_speed(int8_t *note_sequence, void (*callback_on_end)(void), uint16_t note_duration) { + buzzer_sequence = note_sequence; + buzzer_sequence_end_callback = callback_on_end; + + // some reasonable minimums + if(note_duration < 16) note_duration = 16; + buzzer_note_length = note_duration; + + // start up the HPT + movement_hpt_request_face(MOVEMENT_BUZZER_HPT); + + // set the "current" note to have ended right now so the next note knows when to end + buzzer_note_end_ts = movement_hpt_get(); + + _movement_play_next_buzzer_note(); +} + +void movement_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void)) { + // default note speed was in 1/64ths of a second, as far as I can tell. + movement_play_sequence_speed(note_sequence, callback_on_end, 1024/64); +} + +int8_t buzzer_single_note_sequence[] = {0,1,0}; +void movement_play_note(BuzzerNote note, uint16_t duration_ms) { + buzzer_single_note_sequence[0] = note; + movement_play_sequence_speed(buzzer_single_note_sequence, NULL, (duration_ms * 1024) / 1000); +} \ No newline at end of file diff --git a/movement/movement.h b/movement/movement.h index 1dabfbc5b..773026824 100644 --- a/movement/movement.h +++ b/movement/movement.h @@ -120,6 +120,7 @@ typedef enum { EVENT_ALARM_BUTTON_UP, // The alarm button was pressed for less than half a second, and released. EVENT_ALARM_LONG_PRESS, // The alarm button was held for over half a second, but not yet released. EVENT_ALARM_LONG_UP, // The alarm button was held for over half a second, and released. + EVENT_HPT, // The high-precision timer has reached or surpassed the timestamp requested by your face using movement_hpt_schedule. Your face may not be in the foreground. } movement_event_type_t; typedef struct { @@ -174,16 +175,16 @@ typedef void (*watch_face_activate)(movement_settings_t *settings, void *context * to override this behavior (e.g. your user interface requires all three buttons), your watch face MUST * call the movement_move_to_next_face function in response to the EVENT_MODE_LONG_PRESS event. If you * fail to do this, the user will become stuck on your watch face. - * @param event A struct containing information about the event, including its type. @see movement_event_type_t + * @param event A struct containing information about the event, including its type. `movement_event_type_t` * for a list of all possible event types. - * @param settings A pointer to the global Movement settings. @see watch_face_setup. - * @param context A pointer to your application's context. @see watch_face_setup. + * @param settings A pointer to the global Movement settings. See `watch_face_setup`. + * @param context A pointer to your application's context. See `watch_face_setup`. * @return true if your watch face is prepared for the system to enter STANDBY mode; false to keep the system awake. * You should almost always return true. * Note that this return value has no effect if your loop function has called movement_move_to_next_face * or movement_move_to_face; in that case, your watch face will resign immediately, and the next watch * face will make the decision on entering standby mode. - * @note There are two event types that require some extra thought: + * @note There are three event types that require some extra thought: The EVENT_LOW_ENERGY_UPDATE event type is a special case. If you are in the foreground when the watch goes into low energy mode, you will receive this tick once a minute (at the top of the minute) so that you can update the screen. Great! But! When you receive this event, all pins and peripherals other than @@ -194,7 +195,9 @@ typedef void (*watch_face_activate)(movement_settings_t *settings, void *context **Your watch face MUST NOT wake up peripherals in response to a low power tick.** The purpose of this mode is to consume as little energy as possible during the (potentially long) intervals when it's unlikely the user is wearing or looking at the watch. - EVENT_BACKGROUND_TASK is also a special case. @see watch_face_wants_background_task for details. + EVENT_BACKGROUND_TASK is also a special case. watch_face_wants_background_task for details. + EVENT_HPT is similar to EVENT_BACKGROUND_TASK, but it is triggered by the HPT system, rather than the + RTC. See `movement_hpt_schedule` for details. */ typedef bool (*watch_face_loop)(movement_event_t event, movement_settings_t *settings, void *context); @@ -253,11 +256,6 @@ typedef struct { // LED stuff int16_t light_ticks; - // alarm stuff - int16_t alarm_ticks; - bool is_buzzing; - BuzzerNote alarm_note; - // button tracking for long press uint16_t light_down_timestamp; uint16_t mode_down_timestamp; @@ -306,10 +304,211 @@ void movement_cancel_background_task_for_face(uint8_t watch_face_index); void movement_request_wake(void); +// Buzzer operations, now handled by the HPT + +/** + * Plays the hourly signal chime. + */ void movement_play_signal(void); + +/** + * Plays the default alarm signal + */ void movement_play_alarm(void); + +/** + * Plays an alarm signal consisting of two short beeps once a second at + * the given tone for the given number of rounds. + * @param rounds the number of times to repeat the alarm beeps + * @param alarm_note the frequence of beep to play +*/ void movement_play_alarm_beeps(uint8_t rounds, BuzzerNote alarm_note); +/** + * Plays the given note for the given number of milliseconds + * + * Note that unlike `watch_buzzer_play_note`, this function does *not* block the CPU while playing. To play a sequence of notes, use `movement_play_sequence`. + * + * @param note the freqency of the note to play + * @param duration_ms the number of milliseconds to hold the note for + */ +void movement_play_note(BuzzerNote note, uint16_t duration_ms); + +/** + * Plays a melody on the buzzer. The note sequence must follow the pattern described below: + * + * Each note in the sequence is represented using a pair of bytes. + * + * If the first byte is a BuzzerNote, the second byte is interpreted as the number of durations to hold the note for. If the duration is zero, the note is skipped. If the duration is negative, this is considered an error and the sequence is aborted + * + * If the first byte is negative, it is interpreted as a "jump marker", and indicates the number of notes in the sequence to jump over. + * - If the second byte is positive, the jump marker is interpreted as a "repeat" command, and the second byte indicates the number of times the previous section should be repeated. + * - If the second byte is negative, the jump marker is interpreted as a "skip" command, and the first byte indicates the number of following notes to skip. (I.e., a value of -4 would skip the next four elements in the sequence) + * - If the second byte is zero, the jump marker is ignored. + * + * It is critical that repeated sections do not include other repeat markers, or the sequence will never stop playing. + * + * Jump examples: + * - (-2, 3) => repeat the previous two notes three times + * - (-4, -1) => skip over the next four notes + * - (-3, 0) => ignore this marker and play the next element in the sequence + * + * The sequence MUST end with a null terminator (NULL, 0 or BUZZER_NOTE_END). + * + * See `movement_custom_signal_tunes.h` for some example sequences. + * + * If non-null, the given callback function will be invoked when the sequence + * is finished playing. + * + * The default duration of a note is 1/64th of a second, or about 16 + * milliseconds. To use a different note length, use + * `movement_play_sequence_speed`. + * + * @param note_sequence a pointer to the first note in the sequence. See detailed description for an explanation of the expected structure of a sequence + * @param callback_on_end a pointer to a method that should be invoked when the sequence finishes playing. May be null if no callback is required +*/ +void movement_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void)); + +/** + * Like `movement_play_sequence` except you may specify the length of a note. + * + * @param note_sequence a pointer to the first note in the sequence. See `movement_play_sequence` for more details + * @param callback_on_end a pointer to a method that should be invoked when the sequence finishes playing. May be null if no callback is required + * @param note_duration the standard length of a note, in 1/1024ths of a second. The minimum note_duration is 16 ticks. +*/ +void movement_play_sequence_speed(int8_t *note_sequence, void (*callback_on_end)(void), uint16_t note_duration); + +/** + * Silences any notes or sequences playing on the buzzer. + */ +void movement_silence_buzzer(void); + uint8_t movement_claim_backup_register(void); +/** + * The High Precision Timer (HPT) is an on-demand timer running at 1024hz. + * It runs independently of the real time clock and can be used to measure + * durations and schedule future tasks. The HPT is not affected by changes to + * the RTC clock time. While enabled, it continues to run in standby mode and + * may be used to trigger events while the watch is asleep. + * + * The HPT is disabled when not needed to conserve power. Before using it to get a + * timestamp, a watch face must activate it using `movement_hpt_request`. If + * not already running, this will enable and start the timer module. While the + * timer is enabled, the face may retrieve the current timestamp using + * `movement_hpt_get`, or schedule a background event using + * `movement_hpt_schedule`. When a face no longer needs to use the timestamp or + * scheduled event provided by the HPT it MUST call `movement_hpt_release`. + * If no other face has an outstanding request for the HPT, the peripheral may + * be disabled. + * + * Unlike the timestamp provided by the RTC, the HPT timestamp may not be + * modified by the user. However, as it is not running all the time, the only + * guarantee to be made about the HPT timestamp is that between the time your + * face calls `movement_hpt_request` until it calls `movement_hpt_release`, + * the value returned from `movement_hpt_get` will increment upwards at 1024hz. + * Outside of the request/release window, the timestamp value may change + * unpredictably. + * + * Faces may schedule an EVENT_HPT event to occur by calling + * `movement_hpt_schedule` and passing in a timestamp for the event to occur. + * The face must call `movement_hpt_request` before scheduling the event, and + * must not call `movement_hpt_release` until after the event has occurred. + * Note that when your face receives the EVENT_HPT event, it may be running in + * the background. In this case, you may need to use the "_face" variant of + * the HPT methods to specify that it is your face being called. Remember to + * call `movement_hpt_release` after receiving your EVENT_HPT event if you do + * not need the HPT anymore. + */ + +/** + * Enables the HPT for the active face. This method must be called before using + * `movement_hpt_get` or `movement_hpt_schedule`. The HPT will remain running + * in the background until it is released using `movement_hpt_release` + */ +void movement_hpt_request(void); + +/** + * A variant of "movement_hpt_request" that can be used when your face is not + * running in the foreground. + * + * The index number of your face is provided in the setup method when your face is first invoked. + * + * @param face_idx the index number of the face to request use of the HPT for + */ +void movement_hpt_request_face(uint8_t face_idx); + +/** + * Disables the HPT if no other faces are using it. + * + * This method should be called when your face no longer needs to use the reference timestamp provided by the HPT. + * + * Any future events scheduled using `movement_hpt_schedule` will be cancelled. + */ +void movement_hpt_release(void); + +/** + * A variant of "movement_hpt_release" that can be used when your face is + * not running in the foreground. + * + * The index number of your face is provided in the setup method when your face is first invoked. + * + * @param face_idx the number of the face to release the HPT request for + */ +void movement_hpt_release_face(uint8_t face_idx); + +/** + * Schedules a future EVENT_HPT event to occur on or after the given HPT timestamp. + * + * The Movement framework will do its best to trigger the event as close to the + * timestamp as possible, but it may be delayed slightly if multiple faces or events are + * scheduled to occur on the same timestamp. + * + * Try to avoid scheduling an event for a timestamp less than or equal to the current HPT time. In theory, the event would be invoked immediately, but this has not been exhaustively tested. + * + * @param timestamp the future timestamp at which an EVENT_HPT event should be generated for this face + */ +void movement_hpt_schedule(uint64_t timestamp); + +/** + * A variant of "movement_hpt_schedule" that can be used when your face is not + * running in the foreground. +*/ +void movement_hpt_schedule_face(uint64_t timestamp, uint8_t face_idx); + +/** + * Cancels any upcoming EVENT_HPT events for the current face, if any. + */ +void movement_hpt_cancel(void); +/** + * Cancels any upcoming EVENT_HPT events for the specified face + */ +void movement_hpt_cancel_face(uint8_t face_idx); + +/** + * Returns the current timestamp of the high-precision timer, in 1/1024ths of a + * second. + * + * Before using this timestamp, your face must request that the HPT be + * activated using "movement_hpt_request". + * + * This method synchronizes state between the CPU and timer peripheral used to maintain the HPT timestamp, and may take a few milliseconds of active CPU time to execute. If you do not need an exact HPT timestamp, consider using `movement_hpt_get_fast`. + */ +uint64_t movement_hpt_get(void); + +/** + * Returns the current timestamp of the high-precision timer, in 1/1024ths of a + * second. + * + * The timestamp returned from this method is not suitable for control or scheduling purposes; + * it is not properly synchronized with the timer peripheral and it does not + * perform double-checking for timer overflows. However, it may be suitable for + * non-critical timestamp purposes, such as showing the current time of a + * running stopwatch. + * + * Before using this timestamp, your face must request that the HPT be + * activated using "movement_hpt_request". + */ +uint64_t movement_hpt_get_fast(void); + #endif // MOVEMENT_H_ diff --git a/movement/movement_config.h b/movement/movement_config.h index 067ca44b2..0f0faa2b8 100644 --- a/movement/movement_config.h +++ b/movement/movement_config.h @@ -28,11 +28,11 @@ #include "movement_faces.h" const watch_face_t watch_faces[] = { - simple_clock_face, - world_clock_face, - sunrise_sunset_face, - moon_phase_face, - stopwatch_face, + stock_stopwatch_face, + hpt_led_test_face, + hpt_lapsplit_chrono_face, + hpt_countdown_face, + // dual_timer_face, preferences_face, set_time_face, thermistor_readout_face, diff --git a/movement/movement_faces.h b/movement/movement_faces.h index 7feb0f408..5b9f5f976 100644 --- a/movement/movement_faces.h +++ b/movement/movement_faces.h @@ -62,7 +62,6 @@ #include "ratemeter_face.h" #include "rpn_calculator_alt_face.h" #include "weeknumber_clock_face.h" -#include "stock_stopwatch_face.h" #include "tachymeter_face.h" #include "nanosec_face.h" #include "finetune_face.h" @@ -103,6 +102,10 @@ #include "minute_repeater_decimal_face.h" #include "tuning_tones_face.h" #include "kitchen_conversions_face.h" +#include "hpt_lapsplit_chrono_face.h" +#include "hpt_led_test_face.h" +#include "hpt_countdown_face.h" +#include "stock_stopwatch_face.h" // New includes go above this line. #endif // MOVEMENT_FACES_H_ diff --git a/movement/watch_faces/clock/hpt_led_test_face.c b/movement/watch_faces/clock/hpt_led_test_face.c new file mode 100644 index 000000000..3d0f7b23c --- /dev/null +++ b/movement/watch_faces/clock/hpt_led_test_face.c @@ -0,0 +1,175 @@ +/* + * MIT License + * + * Copyright (c) 2024 <#author_name#> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include +#include +#include "hpt_led_test_face.h" +#include + +const int8_t METROID_SIGNAL[] = { + BUZZER_NOTE_F6, + 1, + BUZZER_NOTE_A6SHARP_B6FLAT, + 1, + BUZZER_NOTE_C7, + 1, + BUZZER_NOTE_D7, + 1, + BUZZER_NOTE_E7, + 1, + BUZZER_NOTE_C7, + 1, + BUZZER_NOTE_G6, + 1, + BUZZER_NOTE_C7, + 1, + BUZZER_NOTE_F7, + 1, + BUZZER_NOTE_D7, + 1, + BUZZER_NOTE_A6SHARP_B6FLAT, + 1, + BUZZER_NOTE_G6, + 1, + BUZZER_NOTE_A6, + 4, + BUZZER_NOTE_END, +}; + +void hpt_led_test_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void **context_ptr) +{ + (void)settings; + (void)watch_face_index; + if (*context_ptr == NULL) + { + *context_ptr = malloc(sizeof(hpt_led_test_state_t)); + memset(*context_ptr, 0, sizeof(hpt_led_test_state_t)); + // Do any one-time tasks in here; the inside of this conditional happens only at boot. + hpt_led_test_state_t *state = (hpt_led_test_state_t *)context_ptr; + state->face_idx = watch_face_index; + state->running = false; + } + // Do any pin or peripheral setup here; this will be called whenever the watch wakes from deep sleep. +} + +void hpt_led_test_face_activate(movement_settings_t *settings, void *context) +{ + (void)settings; + hpt_led_test_state_t *state = (hpt_led_test_state_t *)context; + + watch_enable_leds(); + + movement_request_tick_frequency(8); + // movement_hpt_schedule_face(movement_hpt_get() + 2048, state->face_idx); + + // Handle any tasks related to your watch face coming on screen. +} + +void render_hpt_time(uint32_t timestamp) +{ + uint8_t high_high_nibble = timestamp >> 28; + uint8_t high_low_nibble = (timestamp >> 24) & 0xF; + + uint32_t lower_value = timestamp & 0xFFFFFF; + + char buf[11]; + sprintf(buf, "%x %x%06x", high_high_nibble, high_low_nibble, lower_value); + watch_display_string(buf, 0); +} + +bool hpt_led_test_face_loop(movement_event_t event, movement_settings_t *settings, void *context) +{ + hpt_led_test_state_t *state = (hpt_led_test_state_t *)context; + + uint8_t tick; + + switch (event.event_type) + { + case EVENT_ACTIVATE: + // Show your initial UI here. + break; + case EVENT_LIGHT_BUTTON_UP: + //movement_play_sequence_speed(METROID_SIGNAL, NULL, 100); + movement_play_alarm_beeps(10, BUZZER_NOTE_C8); + break; + case EVENT_TICK: + // If needed, update your display here. + render_hpt_time(movement_hpt_get_fast()); + + if (((movement_hpt_get_fast() / 512) % 2) == 0) + { + watch_set_led_green(); + } + else + { + watch_set_led_red(); + } + + break; + case EVENT_ALARM_BUTTON_UP: + if (state->running) + { + state->running = false; + movement_hpt_release(); + } + else + { + state->running = true; + movement_hpt_request(); + } + break; + case EVENT_TIMEOUT: + // Your watch face will receive this event after a period of inactivity. If it makes sense to resign, + // you may uncomment this line to move back to the first watch face in the list: + // movement_move_to_face(0); + break; + case EVENT_LOW_ENERGY_UPDATE: + // If you did not resign in EVENT_TIMEOUT, you can use this event to update the display once a minute. + // Avoid displaying fast-updating values like seconds, since the display won't update again for 60 seconds. + // You should also consider starting the tick animation, to show the wearer that this is sleep mode: + // watch_start_tick_animation(500); + break; + default: + // Movement's default loop handler will step in for any cases you don't handle above: + // * EVENT_LIGHT_BUTTON_DOWN lights the LED + // * EVENT_MODE_BUTTON_UP moves to the next watch face in the list + // * EVENT_MODE_LONG_PRESS returns to the first watch face (or skips to the secondary watch face, if configured) + // You can override any of these behaviors by adding a case for these events to this switch statement. + return movement_default_loop_handler(event, settings); + } + + // return true if the watch can enter standby mode. Generally speaking, you should always return true. + // Exceptions: + // * If you are displaying a color using the low-level watch_set_led_color function, you should return false. + // * If you are sounding the buzzer using the low-level watch_set_buzzer_on function, you should return false. + // Note that if you are driving the LED or buzzer using Movement functions like movement_illuminate_led or + // movement_play_alarm, you can still return true. This guidance only applies to the low-level watch_ functions. + return true; +} + +void hpt_led_test_face_resign(movement_settings_t *settings, void *context) +{ + (void)settings; + hpt_led_test_state_t *state = (hpt_led_test_state_t *)context; +} diff --git a/movement/watch_faces/clock/hpt_led_test_face.h b/movement/watch_faces/clock/hpt_led_test_face.h new file mode 100644 index 000000000..44da486a6 --- /dev/null +++ b/movement/watch_faces/clock/hpt_led_test_face.h @@ -0,0 +1,57 @@ +/* + * MIT License + * + * Copyright (c) 2024 <#author_name#> + * + * 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. + */ + +#ifndef HPT_LED_TEST_FACE_H_ +#define HPT_LED_TEST_FACE_H_ + +#include "movement.h" + +/* + * A DESCRIPTION OF YOUR WATCH FACE + * + * and a description of how use it + * + */ + +typedef struct { + // Anything you need to keep track of, put it here! + uint8_t face_idx; + bool running; +} hpt_led_test_state_t; + +void hpt_led_test_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void hpt_led_test_face_activate(movement_settings_t *settings, void *context); +bool hpt_led_test_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void hpt_led_test_face_resign(movement_settings_t *settings, void *context); + +#define hpt_led_test_face ((const watch_face_t){ \ + hpt_led_test_face_setup, \ + hpt_led_test_face_activate, \ + hpt_led_test_face_loop, \ + hpt_led_test_face_resign, \ + NULL, \ +}) + +#endif // HPT_LED_TEST_FACE_H_ + diff --git a/movement/watch_faces/complication/counter_face.c b/movement/watch_faces/complication/counter_face.c index 69ca1f738..8b259847f 100644 --- a/movement/watch_faces/complication/counter_face.c +++ b/movement/watch_faces/complication/counter_face.c @@ -53,7 +53,7 @@ bool counter_face_loop(movement_event_t event, movement_settings_t *settings, vo switch (event.event_type) { case EVENT_ALARM_BUTTON_UP: - watch_buzzer_abort_sequence(); //abort running buzzer sequence when counting fast + movement_silence_buzzer(); //abort running buzzer sequence when counting fast state->counter_idx++; // increment counter index if (state->counter_idx>99) { //0-99 state->counter_idx=0;//reset counter index @@ -64,7 +64,7 @@ bool counter_face_loop(movement_event_t event, movement_settings_t *settings, vo } break; case EVENT_LIGHT_LONG_PRESS: - watch_buzzer_abort_sequence(); + movement_silence_buzzer(); state->beep_on = !state->beep_on; if (state->beep_on) { watch_set_indicator(WATCH_INDICATOR_SIGNAL); @@ -132,7 +132,7 @@ void beep_counter(counter_state_t *state) { i++; sound_seq[i] = high_count-1; } - watch_buzzer_play_sequence((int8_t *)sound_seq, NULL); + movement_play_sequence((int8_t *)sound_seq, NULL); } diff --git a/movement/watch_faces/complication/dual_timer_face.c b/movement/watch_faces/complication/dual_timer_face.c index f98c35b4b..65b53e148 100644 --- a/movement/watch_faces/complication/dual_timer_face.c +++ b/movement/watch_faces/complication/dual_timer_face.c @@ -26,122 +26,36 @@ #include #include #include "dual_timer_face.h" -#include "watch.h" -#include "watch_utility.h" -#include "watch_rtc.h" - -/* - * IMPORTANT: This watch face uses the same TC2 callback counter as the Stock Stopwatch - * watch-face. It works through calling a global handler function. The two watch-faces - * therefore can't coexist within the same firmware. If you want to compile this watch-face - * then you need to remove the line <../watch_faces/complication/stock_stopwatch_face.c \> - * from the Makefile. - */ +#include "movement.h" // FROM stock_stopwatch_face.c //////////////////////////////////////////////// // Copyright (c) 2022 Andreas Nebinger -#if __EMSCRIPTEN__ -#include -#include -#else -#include "../../../watch-library/hardware/include/saml22j18a.h" -#include "../../../watch-library/hardware/include/component/tc.h" -#include "../../../watch-library/hardware/hri/hri_tc_l22.h" -#endif - -static const watch_date_time distant_future = {.unit = {0, 0, 0, 1, 1, 63}}; -static bool _is_running; -static uint32_t _ticks; - -#if __EMSCRIPTEN__ - -static long _em_interval_id = 0; - -void em_dual_timer_cb_handler(void *userData) { - // interrupt handler for emscripten 128 Hz callbacks - (void) userData; - _ticks++; -} - -static void _dual_timer_cb_initialize() { } - -static inline void _dual_timer_cb_stop() { - emscripten_clear_interval(_em_interval_id); - _em_interval_id = 0; - _is_running = false; -} - -static inline void _dual_timer_cb_start() { - // initiate 128 hz callback - _em_interval_id = emscripten_set_interval(em_dual_timer_cb_handler, (double)(1000/128), (void *)NULL); -} - -#else - -static inline void _dual_timer_cb_start() { - // start the TC2 timer - hri_tc_set_CTRLA_ENABLE_bit(TC2); - _is_running = true; -} - -static inline void _dual_timer_cb_stop() { - // stop the TC2 timer - hri_tc_clear_CTRLA_ENABLE_bit(TC2); - _is_running = false; -} - -static void _dual_timer_cb_initialize() { - // setup and initialize TC2 for a 64 Hz interrupt - hri_mclk_set_APBCMASK_TC2_bit(MCLK); - hri_gclk_write_PCHCTRL_reg(GCLK, TC2_GCLK_ID, GCLK_PCHCTRL_GEN_GCLK3 | GCLK_PCHCTRL_CHEN); - _dual_timer_cb_stop(); - hri_tc_write_CTRLA_reg(TC2, TC_CTRLA_SWRST); - hri_tc_wait_for_sync(TC2, TC_SYNCBUSY_SWRST); - hri_tc_write_CTRLA_reg(TC2, TC_CTRLA_PRESCALER_DIV64 | // 32 Khz divided by 64 divided by 4 results in a 128 Hz interrupt - TC_CTRLA_MODE_COUNT8 | - TC_CTRLA_RUNSTDBY); - hri_tccount8_write_PER_reg(TC2, 3); - hri_tc_set_INTEN_OVF_bit(TC2); - NVIC_ClearPendingIRQ(TC2_IRQn); - NVIC_EnableIRQ (TC2_IRQn); -} - -// you need to take stock_stopwatch.c out of the Makefile or this will create a conflict -// you have to choose between one of the stopwatches - void TC2_Handler(void) { - // interrupt handler for TC2 (globally!) - _ticks++; - TC2->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF; -} - -#endif +// max time displayed is 99 days, 23 hours, 59 minutes, 59 seconds, 99 centiseconds +const uint64_t MAX_TIME = (9900LL/1024LL) + 1024LL * (59LL + 59LL*60LL + 23LL*60LL*60LL + 99LL*24LL*60LL*60LL); // STATIC FUNCTIONS /////////////////////////////////////////////////////////// /** @brief converts tick counts to duration struct for time display + * + * ticks = 1/1024ths of a second */ -static dual_timer_duration_t ticks_to_duration(uint32_t ticks) { +static dual_timer_duration_t ticks_to_duration(uint64_t ticks) { dual_timer_duration_t duration; - uint8_t hours = 0; - uint8_t days = 0; - // count hours and days - while (ticks >= (128 * 60 * 60)) { - ticks -= (128 * 60 * 60); - hours++; - if (hours >= 24) { - hours -= 24; - days++; - } - } + // limit timers to 99d23h59m59d99c + if(ticks > MAX_TIME) ticks = MAX_TIME; - // convert minutes, seconds, centiseconds - duration.centiseconds = (ticks & 0x7F) * 100 / 128; - duration.seconds = (ticks >> 7) % 60; - duration.minutes = (ticks >> 7) / 60; - duration.hours = hours; - duration.days = days; + uint16_t thousandths = ticks % 1024; + ticks /= 1024; // ticks = seconds now + duration.centiseconds = (thousandths * 100) / 1024; + duration.seconds = ticks % 60; + ticks /= 60; // ticks = minutes now + duration.minutes = ticks % 60; + ticks /= 60; // ticks = hours now + duration.hours = ticks % 24; + ticks /= 24; // ticks = days now + duration.days = ticks % 100; return duration; } @@ -152,19 +66,9 @@ static dual_timer_duration_t ticks_to_duration(uint32_t ticks) { */ static void start_timer(dual_timer_state_t *state, bool timer) { // if it is not running yet, run it - if ( !_is_running ) { - _is_running = true; - movement_request_tick_frequency(16); - state->start_ticks[timer] = 0; - state->stop_ticks[timer] = 0; - _ticks = 0; - _dual_timer_cb_start(); - movement_schedule_background_task(distant_future); - } else { - // if another timer is already running save the current tick - state->start_ticks[timer] = _ticks; - state->stop_ticks[timer] = _ticks; - } + movement_hpt_request(); + movement_request_tick_frequency(16); + state->start_ticks[timer] = movement_hpt_get(); state->running[timer] = true; } @@ -174,15 +78,14 @@ static void start_timer(dual_timer_state_t *state, bool timer) { */ static void stop_timer(dual_timer_state_t *state, bool timer) { // stop timer and save duration - state->stop_ticks[timer] = _ticks; + state->stop_ticks[timer] = movement_hpt_get(); state->duration[timer] = ticks_to_duration(state->stop_ticks[timer] - state->start_ticks[timer]); state->running[timer] = false; - // if the other timer is not running, stop callback - if ( state->running[!timer] == false ) { - _is_running = false; - _dual_timer_cb_stop(); + + // if neither timer is running, release hpt + if(!(state->running[0] || state->running[1])) { movement_request_tick_frequency(1); - movement_cancel_background_task(); + movement_hpt_release(); } } @@ -197,9 +100,10 @@ static void dual_timer_display(dual_timer_state_t *state) { char buf[11]; char oi[3]; // get the current time count of the selected counter - dual_timer_duration_t timer = state->running[state->show] ? ticks_to_duration(state->stop_ticks[state->show] - state->start_ticks[state->show]) : state->duration[state->show]; - // get the current time count of the other counter - dual_timer_duration_t other = ticks_to_duration(state->stop_ticks[!state->show] - state->start_ticks[!state->show]); + uint64_t now = movement_hpt_get(); + + dual_timer_duration_t timer = state->running[state->show] ? ticks_to_duration(now - state->start_ticks[state->show]) : state->duration[state->show]; + dual_timer_duration_t other = state->running[!state->show] ? ticks_to_duration(now - state->start_ticks[!state->show]) : state->duration[!state->show]; if ( timer.days > 0 ) sprintf(buf, "%02u%02u%02u", timer.days, timer.hours, timer.minutes); @@ -213,11 +117,11 @@ static void dual_timer_display(dual_timer_state_t *state) { watch_display_string(state->show ? "B" : "A", 0); // indicate whether other counter is running - watch_display_string(state->running[!state->show] && (_ticks % 100) < 50 ? "+" : " ", 1); + watch_display_string(state->running[!state->show] && (now % 1024) < 512 ? "+" : " ", 1); // indicate for how long the other counter has been running sprintf(oi, "%2u", other.days > 0 ? other.days : (other.hours > 0 ? other.hours : (other.minutes > 0 ? other.minutes : (other.seconds > 0 ? other.seconds : other.centiseconds)))); - watch_display_string( (state->stop_ticks[!state->show] - state->start_ticks[!state->show]) > 0 ? oi : " ", 2); + watch_display_string( state->running[!state->show] ? oi : " ", 2); // blink colon when running if ( timer.centiseconds > 50 || !state->running[state->show] ) watch_set_colon(); @@ -232,35 +136,23 @@ void dual_timer_face_setup(movement_settings_t *settings, uint8_t watch_face_ind if (*context_ptr == NULL) { *context_ptr = malloc(sizeof(dual_timer_state_t)); memset(*context_ptr, 0, sizeof(dual_timer_state_t)); - _ticks = 0; - } - if (!_is_running) { - _dual_timer_cb_initialize(); } } void dual_timer_face_activate(movement_settings_t *settings, void *context) { (void) settings; (void) context; - if (_is_running) { - movement_schedule_background_task(distant_future); - } } bool dual_timer_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { dual_timer_state_t *state = (dual_timer_state_t *)context; - // timers stop at 99:23:59:59:99 - if ( (_ticks - state->start_ticks[0]) >= 1105919999 ) - stop_timer(state, 0); - - if ( (_ticks - state->start_ticks[1]) >= 1105919999 ) - stop_timer(state, 1); + bool is_running = (state->running[0] || state->running[1]); switch (event.event_type) { case EVENT_ACTIVATE: watch_set_colon(); - if (_is_running) { + if (is_running) { movement_request_tick_frequency(16); if ( state->running[0] ) state->show = 0; @@ -272,12 +164,7 @@ bool dual_timer_face_loop(movement_event_t event, movement_settings_t *settings, } break; case EVENT_TICK: - if ( _is_running ) { - // update stop ticks - if ( state->running[0] ) - state->stop_ticks[0] = _ticks; - if ( state->running[1] ) - state->stop_ticks[1] = _ticks; + if ( is_running ) { dual_timer_display(state); } break; @@ -313,7 +200,7 @@ bool dual_timer_face_loop(movement_event_t event, movement_settings_t *settings, break; case EVENT_TIMEOUT: // go back to - if (!_is_running) movement_move_to_face(0); + if (!is_running) movement_move_to_face(0); break; case EVENT_LOW_ENERGY_UPDATE: dual_timer_display(state); @@ -330,5 +217,6 @@ void dual_timer_face_resign(movement_settings_t *settings, void *context) { (void) context; movement_cancel_background_task(); // handle any cleanup before your watch face goes off-screen. + movement_request_tick_frequency(1); } diff --git a/movement/watch_faces/complication/dual_timer_face.h b/movement/watch_faces/complication/dual_timer_face.h index d1ac79359..a629254bc 100644 --- a/movement/watch_faces/complication/dual_timer_face.h +++ b/movement/watch_faces/complication/dual_timer_face.h @@ -59,12 +59,6 @@ * button to move to the next watch face is disabled to be able to use it to toggle between * the timers. In this case LONG PRESSING MODE will move to the next face instead of moving * back to the default watch face. - * - * IMPORTANT: This watch face uses the same TC2 callback counter as the Stock Stopwatch - * watch-face. It works through calling a global handler function. The two watch-faces - * therefore can't coexist within the same firmware. If you want to compile this watch-face - * then you need to remove the line <../watch_faces/complication/stock_stopwatch_face.c \> - * from the Makefile. */ #include "movement.h" @@ -78,8 +72,8 @@ typedef struct { } dual_timer_duration_t; typedef struct { - uint32_t start_ticks[2]; - uint32_t stop_ticks[2]; + uint64_t start_ticks[2]; + uint64_t stop_ticks[2]; dual_timer_duration_t duration[2]; bool running[2]; bool show; @@ -90,12 +84,6 @@ void dual_timer_face_activate(movement_settings_t *settings, void *context); bool dual_timer_face_loop(movement_event_t event, movement_settings_t *settings, void *context); void dual_timer_face_resign(movement_settings_t *settings, void *context); -#if __EMSCRIPTEN__ -void em_dual_timer_cb_handler(void *userData); -#else -void TC2_Handler(void); -#endif - #define dual_timer_face ((const watch_face_t){ \ dual_timer_face_setup, \ dual_timer_face_activate, \ diff --git a/movement/watch_faces/complication/hpt_countdown_face.c b/movement/watch_faces/complication/hpt_countdown_face.c new file mode 100644 index 000000000..85f933c43 --- /dev/null +++ b/movement/watch_faces/complication/hpt_countdown_face.c @@ -0,0 +1,429 @@ +/* + * MIT License + * + * Copyright (c) 2024 <#author_name#> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include +#include +#include +#include "hpt_countdown_face.h" + +#define LCD_UPDATE_RATE_WHILE_RUNNING 8 + +static void render(hpt_countdown_state_t *state, bool blink) +{ + // always draw auto-repeat indicator, but not count + if (state->auto_repeat) + { + watch_set_indicator(WATCH_INDICATOR_LAP); + } + else + { + watch_clear_indicator(WATCH_INDICATOR_LAP); + } + + char buf[9]; + if (state->setting_mode != 0) + { + // draw the set time and repeat mode + sprintf(buf, " %02d%02d%02d", + state->set_hours, + state->set_minutes, + state->set_seconds); + watch_display_string(buf, 2); + if (blink) + { + // clear the setting being configured + // should map to the digit being configured + watch_display_string(" ", 10 - (state->setting_mode)); + } + } + else + { + // draw repeat count + if (state->auto_repeat) + { + sprintf(buf, "%2d", state->repeat_count); + watch_display_string(buf, 2); + } + else + { + watch_display_string(" ", 2); + } + + int64_t timeLeft; // 1024hz ticks + + if (state->running) + { + // calculate time left until target time + int64_t now = movement_hpt_get_fast(); + int64_t signed_target = state->target; + timeLeft = signed_target - now; + } + else + { + timeLeft = state->paused_ms_left; + } + + // TODO: need to do better with the division here + // - it looks bad when you start the timer and it immediately shows the time-1 + // - it sits on the number zero for a full second before it triggers the beeps + // - and in "normal" mode, it sits on the zero for a second before it starts showing negative numbers + // Would rather it stay on the configured time for a full second when you start it and start beeping immediately when the timer hits zero + + bool negative = timeLeft < 0; + if (negative) + timeLeft = -timeLeft; + // we do some clamping + if (timeLeft >= 30 * 60 * 60 * 1024) + timeLeft = (30 * 60 * 60 * 1024 - 1); + + // throw away ms part + + timeLeft /= 1024; + + uint8_t seconds = timeLeft % 60; + timeLeft /= 60; + uint8_t minutes = timeLeft % 60; + uint8_t hours = timeLeft / 60; + + if (minutes == 0 && hours == 0) + { + sprintf(buf, " %3d", negative ? -seconds : seconds); + watch_display_string(buf, 4); + watch_clear_colon(); + } + else if (hours == 0) + { + sprintf(buf, " %3d%02d", negative ? -minutes : minutes, seconds); + watch_display_string(buf, 4); + watch_clear_colon(); + } + else + { + watch_set_colon(); + if (state->auto_repeat) + { + // don't overwrite lap counter + // also, should never be negative + sprintf(buf, "%2d%02d%02d", hours, minutes, seconds); + watch_display_string(buf, 4); + } + else + { + // allow overwrite of lap counter with negative sign + sprintf(buf, "%3d%02d%02d", negative ? -hours : hours, minutes, seconds); + watch_display_string(buf, 3); + } + } + } +} + +static void reset_timer(hpt_countdown_state_t *state) +{ + // make sure time is always paused first! + + state->paused_ms_left = ((state->set_hours * 60 * 60) + + (state->set_minutes * 60) + + (state->set_seconds)) * + 1024; + state->repeat_count = 0; +} + +// starts a paused timer +static void start_timer(hpt_countdown_state_t *state) +{ + movement_hpt_request_face(state->watch_face_index); + uint64_t now = movement_hpt_get(); + state->running = true; + + movement_request_tick_frequency(LCD_UPDATE_RATE_WHILE_RUNNING); + + // reset the target based on the number of seconds left when the timer was paused + state->target = now + state->paused_ms_left; + + // if the target was in the past, don't schedule a new reset, the time has already expired. + if (state->target >= now) + { + movement_hpt_schedule_face(state->target, state->watch_face_index); + } +} + +// restarts a currently running timer for another lap +static void restart_timer(hpt_countdown_state_t *state) +{ + state->repeat_count = (state->repeat_count + 1) % 40; + uint64_t duration = ((state->set_hours * 60 * 60) + + (state->set_minutes * 60) + + (state->set_seconds)) * + 1024; + state->target += duration; + movement_hpt_schedule_face(state->target, state->watch_face_index); +} + +static void trigger_timer(hpt_countdown_state_t *state) +{ + movement_play_alarm(); + + // timer will start counting up, so we can cancel the alarm but leave the HPT running + movement_hpt_cancel_face(state->watch_face_index); + + // if auto repeat is enabled, restart_timer will schedule another event for us. + if (state->auto_repeat) + { + restart_timer(state); + } +} + +static void pause_timer(hpt_countdown_state_t *state) +{ + state->running = false; + // record time left between now and the target + state->paused_ms_left = state->target - movement_hpt_get(); + + movement_request_tick_frequency(1); + + // can cancel the scheduled event and stop HPT + movement_hpt_cancel_face(state->watch_face_index); + movement_hpt_release_face(state->watch_face_index); +} + +void hpt_countdown_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void **context_ptr) +{ + (void)settings; + (void)watch_face_index; + if (*context_ptr == NULL) + { + *context_ptr = malloc(sizeof(hpt_countdown_state_t)); + memset(*context_ptr, 0, sizeof(hpt_countdown_state_t)); + // Do any one-time tasks in here; the inside of this conditional happens only at boot. + + hpt_countdown_state_t *state = *context_ptr; + state->auto_repeat = false; + state->set_hours = 0; + state->set_minutes = 0; + state->set_seconds = 10; + state->repeat_count = 0; + state->watch_face_index = watch_face_index; + state->setting_mode = 0; + + reset_timer(state); + } + // Do any pin or peripheral setup here; this will be called whenever the watch wakes from deep sleep. +} + +void hpt_countdown_face_activate(movement_settings_t *settings, void *context) +{ + (void)settings; + hpt_countdown_state_t *state = (hpt_countdown_state_t *)context; + + // reset setting mode + state->setting_mode = 0; + watch_display_string("TR", 0); + + // if the timer is running, use a higher tick rate to update the display more responsively, even though the background timer is not tied to the tick rate anymore + if (state->running) + { + movement_request_tick_frequency(LCD_UPDATE_RATE_WHILE_RUNNING); + } +} + +static void increment_setting(hpt_countdown_state_t *state) +{ + switch (state->setting_mode) + { + case 1: // seconds ones + if (state->set_seconds % 10 == 9) + { + state->set_seconds -= 9; + } + else + { + state->set_seconds += 1; + } + break; + case 2: // seconds tens + state->set_seconds = (state->set_seconds + 10) % 60; + break; + case 3: // minutes ones + if (state->set_minutes % 10 == 9) + { + state->set_minutes -= 9; + } + else + { + state->set_minutes += 1; + } + break; + case 4: // minutes tens + state->set_minutes = (state->set_minutes + 10) % 60; + break; + case 5: // hours ones + if (state->set_hours % 10 == 9) + { + state->set_hours -= 9; + } + else + { + state->set_hours += 1; + } + break; + case 6: // hours tens + state->set_hours = (state->set_hours + 10) % 30; + break; + default: + break; + } +} + +bool hpt_countdown_face_loop(movement_event_t event, movement_settings_t *settings, void *context) +{ + hpt_countdown_state_t *state = (hpt_countdown_state_t *)context; + + bool blink = !!(event.subsecond & 1); + + switch (event.event_type) + { + case EVENT_ACTIVATE: + // Show your initial UI here. + render(state, false); + break; + case EVENT_TICK: + render(state, blink); + break; + case EVENT_LIGHT_BUTTON_DOWN: + // swallow this because light behavior is different here. + break; + case EVENT_LIGHT_BUTTON_UP: + // reset time and toggle repeat mode + if (state->setting_mode != 0) + { + // exit setting mode + reset_timer(state); + } + else if (!(state->running)) + { + // if the timer is already reset, enter setting mode + int64_t expected_ticks_left = ((state->set_hours * 60 * 60) + + (state->set_minutes * 60) + + (state->set_seconds)) * + 1024; + + if (state->paused_ms_left == expected_ticks_left) + { + // timer is already reset, enter settings mode + state->setting_mode = 1; + } + else + { + // reset timer + reset_timer(state); + } + } + else + { + // nothing really to do if they press this while the timer is running I think + } + render(state, false); + break; + case EVENT_LIGHT_LONG_PRESS: + // toggle auto-repeat mode + state->auto_repeat = !(state->auto_repeat); + // if timer is currently in overflow mode, restart a new lap immediately + if (state->running && (state->target <= movement_hpt_get())) + { + restart_timer(state); + } + render(state, false); + break; + case EVENT_ALARM_BUTTON_DOWN: + if (state->setting_mode == 0) + { + if (state->running) + { + pause_timer(state); + } + else + { + start_timer(state); + } + } + else + { + // increment setting item + increment_setting(state); + } + render(state, false); + break; + case EVENT_TIMEOUT: + if (!(state->running)) + { + movement_move_to_face(0); + } + break; + case EVENT_MODE_BUTTON_UP: + // if in setting mode, loop through settings, otherwise, regular mode behavior + if (state->setting_mode != 0) + { + state->setting_mode = (state->setting_mode + 1); + if (state->setting_mode > 6) + { + // loop back to 1 + state->setting_mode = 1; + } + render(state, true); + } + else + { + movement_move_to_next_face(); + } + break; + case EVENT_HPT: + // timer went off + trigger_timer(state); + break; + default: + // Movement's default loop handler will step in for any cases you don't handle above: + // * EVENT_LIGHT_BUTTON_DOWN lights the LED + // * EVENT_MODE_BUTTON_UP moves to the next watch face in the list + // * EVENT_MODE_LONG_PRESS returns to the first watch face (or skips to the secondary watch face, if configured) + // You can override any of these behaviors by adding a case for these events to this switch statement. + return movement_default_loop_handler(event, settings); + } + + // return true if the watch can enter standby mode. Generally speaking, you should always return true. + // Exceptions: + // * If you are displaying a color using the low-level watch_set_led_color function, you should return false. + // * If you are sounding the buzzer using the low-level watch_set_buzzer_on function, you should return false. + // Note that if you are driving the LED or buzzer using Movement functions like movement_illuminate_led or + // movement_play_alarm, you can still return true. This guidance only applies to the low-level watch_ functions. + return true; +} + +void hpt_countdown_face_resign(movement_settings_t *settings, void *context) +{ + (void)settings; + (void)context; + + // handle any cleanup before your watch face goes off-screen. + movement_request_tick_frequency(1); +} diff --git a/movement/watch_faces/complication/hpt_countdown_face.h b/movement/watch_faces/complication/hpt_countdown_face.h new file mode 100644 index 000000000..dc9e98c3b --- /dev/null +++ b/movement/watch_faces/complication/hpt_countdown_face.h @@ -0,0 +1,103 @@ +/* + * MIT License + * + * Copyright (c) 2024 <#author_name#> + * + * 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. + */ + +#ifndef HPT_COUNTDOWN_FACE_H_ +#define HPT_COUNTDOWN_FACE_H_ + +#include "movement.h" + +/* + * hpt_countdown_face: A countdown timer implemented using the high-precision timer + * + * Counts down up to 29h59m59s. Auto-repeat feature. + * ALARM: Starts or pauses countdown. In setting mode, increments current digit + * LIGHT: Short press while timer is reset to enter/exit setting mode. Short press while timer is paused to reset. Long press at any time to toggle auto-repeat + * MODE: In setting mode, moves between digits. + * + * When auto-repeat mode is enabled, "LAP" is shown on display with the number of repeats triggered in the top right. When reaching target time, countdown will be reset automatically. + * + * When not in auto-repeat mode, display will start counting *up* and show a minus sign until paused and reset. + */ + +typedef struct { + // configured number of hours, up to 29 i guess + uint8_t set_hours :5; + // configured number of minutes, up to 59 + uint8_t set_minutes :6; + // configured number of seconds up to 59 + uint8_t set_seconds :6; + + // 17 bits + + bool auto_repeat :1; + bool running :1; + + // 19 bits + + /** + * 0 = not setting anything + * 1 = seconds-ones + * 2= seconds-tens + * 3=minutes-ones + * 4=minutes-tens + * 5=hours-ones + * 6=hours-tens + * 7=unused + */ + uint8_t setting_mode :3; + uint8_t repeat_count :5; + + // 27 bits + + uint8_t _padding :5; + + // 32 bits (aligned) + + // the target timestamp we are counting down to + uint64_t target; + + // while paused, the number of milliseconds (really, 1024hz time ticks) remaining in the countdown + // signed in case the timer was paused after it has expired. + int64_t paused_ms_left; + + // the ID of this face, for background task management + uint8_t watch_face_index; + +} hpt_countdown_state_t; + +void hpt_countdown_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void hpt_countdown_face_activate(movement_settings_t *settings, void *context); +bool hpt_countdown_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void hpt_countdown_face_resign(movement_settings_t *settings, void *context); + +#define hpt_countdown_face ((const watch_face_t){ \ + hpt_countdown_face_setup, \ + hpt_countdown_face_activate, \ + hpt_countdown_face_loop, \ + hpt_countdown_face_resign, \ + NULL, \ +}) + +#endif // HPT_COUNTDOWN_FACE_H_ + diff --git a/movement/watch_faces/complication/hpt_lapsplit_chrono_face.c b/movement/watch_faces/complication/hpt_lapsplit_chrono_face.c new file mode 100644 index 000000000..8095c0dd3 --- /dev/null +++ b/movement/watch_faces/complication/hpt_lapsplit_chrono_face.c @@ -0,0 +1,283 @@ +/* + * MIT License + * + * Copyright (c) 2024 <#author_name#> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include +#include +#include "hpt_lapsplit_chrono_face.h" +#include +#include + +// frequency rate of underlying timer (high precision timer) +#define LCF_SUBSECOND_RATE 1024 + +#define LCF_DISPLAY_UPDATE_RATE 16 + +static void render(hpt_lapsplit_chrono_state_t *context, bool lowEnergyUpdate) +{ + // show CR in the DOW index + // DAY numerals show hours duration + // show LAP if in lap mode, not if in split mode + // rest is pretty obvious + + uint64_t runningTime; + if (context->running == LCF_RUN_RUNNING) + { + // use "fast" get here because we don't need a truly accurate timestamp while the timer is running. + runningTime = movement_hpt_get_fast() - context->startTs; + } + else + { + runningTime = context->pausedTs; + } + uint64_t showTime = context->display == LCF_DISPLAY_SPLIT ? context->splitTs : runningTime; + + uint8_t time_hundreths = (((uint16_t)(showTime % LCF_SUBSECOND_RATE)) * 100) / LCF_SUBSECOND_RATE; + + uint32_t d = showTime / LCF_SUBSECOND_RATE; + uint8_t time_seconds = d % 60; + d /= 60; + uint8_t time_minutes = d % 60; + d /= 60; + uint8_t time_hours = d > 40 ? 40 : d; + + char buf[7]; + + if (!lowEnergyUpdate) + { + sprintf(buf, "%02d%02d%02d", time_minutes, time_seconds, time_hundreths); + } + else + { + // since we only update once a minute in LE mode, only display the minutes + sprintf(buf, "%02d--LE", time_minutes); + } + watch_display_string(buf, 4); + + // always show the colon if we're paused + if (context->running == LCF_RUN_STOPPED) + { + watch_set_colon(); + } + else + { + // otherwise, blink the colon once every second + if ((runningTime % LCF_SUBSECOND_RATE) < (LCF_SUBSECOND_RATE / 2)) + { + watch_set_colon(); + } + else + { + watch_clear_colon(); + } + } + + if (context->mode == LCF_MODE_LAP) + { + // display lap count in lap mode + watch_set_indicator(WATCH_INDICATOR_LAP); + sprintf(buf, "%2d", context->laps); + watch_display_string(buf, 2); + } + else + { + // display hour count in date digits for as long as possible + + watch_clear_indicator(WATCH_INDICATOR_LAP); + if (time_hours == 0) + { + watch_display_string(" ", 2); + } + else if (time_hours > 39) + { + // keep timing, but I guess show an error up here. + watch_display_string(" E", 2); + } + else + { + sprintf(buf, "%2d", time_hours); + watch_display_string(buf, 2); + } + } +} + +static void splitButton(hpt_lapsplit_chrono_state_t *state) +{ + if (state->display == LCF_DISPLAY_SPLIT) + { + // if the split duration is being displayed, clear it when you press "light" again, but don't change anything else + state->display = LCF_DISPLAY_TIME; + return; + } + + if (state->running == LCF_RUN_STOPPED) + { + // If the timer is paused, but it is showing a non-zero time, reset the time back to zero + if (state->pausedTs != 0 || state->laps != 0) + { + state->pausedTs = 0; + state->laps = 0; + } + else + { + // if already reset to zero, toggle lap/split mode + state->mode = state->mode == LCF_MODE_LAP ? LCF_MODE_SPLIT : LCF_MODE_LAP; + } + } + else + { + // record split duration + uint64_t now = movement_hpt_get(); + state->splitTs = now - state->startTs; + + // display split instead of current time + state->display = LCF_DISPLAY_SPLIT; + + if (state->mode == LCF_MODE_LAP) + { + // reset start time to current timestamp to start a new lap + state->startTs = now; + if (state->laps == 39) + { + state->laps = 0; + } + else + { + state->laps = state->laps + 1; + } + } + } +} + +static void startStopButton(hpt_lapsplit_chrono_state_t *state) +{ + if (state->running == LCF_RUN_RUNNING) + { + // if running, stop the timer and record its duration + uint64_t now = movement_hpt_get(); + state->running = LCF_RUN_STOPPED; + state->pausedTs = now - state->startTs; + movement_hpt_release(); + + // slow display back down because the time is paused + movement_request_tick_frequency(1); + } + else + { + // restart the timer + movement_hpt_request(); + uint64_t now = movement_hpt_get(); + state->running = LCF_RUN_RUNNING; + state->startTs = now - state->pausedTs; + + // increase display rate so it looks like the timer is running + movement_request_tick_frequency(LCF_DISPLAY_UPDATE_RATE); + } +} + +void hpt_lapsplit_chrono_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void **context_ptr) +{ + (void)settings; + (void)watch_face_index; + if (*context_ptr == NULL) + { + *context_ptr = malloc(sizeof(hpt_lapsplit_chrono_state_t)); + memset(*context_ptr, 0, sizeof(hpt_lapsplit_chrono_state_t)); + } +} + +void hpt_lapsplit_chrono_face_activate(movement_settings_t *settings, void *context) +{ + (void)settings; + hpt_lapsplit_chrono_state_t *state = (hpt_lapsplit_chrono_state_t *)context; + // always show the running time when switching to this face + state->display = LCF_DISPLAY_TIME; + + // if the timer is running, show a higher update rate + if(state->running == LCF_RUN_RUNNING) { + movement_request_tick_frequency(LCF_DISPLAY_UPDATE_RATE); + } else { + movement_request_tick_frequency(1); + } + + watch_display_string("CH", 0); +} + +bool hpt_lapsplit_chrono_face_loop(movement_event_t event, movement_settings_t *settings, void *context) +{ + hpt_lapsplit_chrono_state_t *state = (hpt_lapsplit_chrono_state_t *)context; + + switch (event.event_type) + { + case EVENT_LIGHT_BUTTON_DOWN: + splitButton(state); + render(state, false); + break; + // swallow the long press to avoid toggling light settings here in a confusing way + case EVENT_LIGHT_LONG_PRESS: + break; + case EVENT_ALARM_BUTTON_DOWN: + startStopButton(state); + render(state, false); + break; + case EVENT_LOW_ENERGY_UPDATE: + render(state, true); + break; + case EVENT_ACTIVATE: + case EVENT_TICK: + render(state, false); + break; + case EVENT_TIMEOUT: + // only timeout if the chrono is not running + if (state->running == LCF_RUN_STOPPED) + { + movement_move_to_face(0); + } + break; + default: + // Movement's default loop handler will step in for any cases you don't handle above: + // * EVENT_LIGHT_BUTTON_DOWN lights the LED + // * EVENT_MODE_BUTTON_UP moves to the next watch face in the list + // * EVENT_MODE_LONG_PRESS returns to the first watch face (or skips to the secondary watch face, if configured) + // You can override any of these behaviors by adding a case for these events to this switch statement. + return movement_default_loop_handler(event, settings); + } + + // return true if the watch can enter standby mode. Generally speaking, you should always return true. + // Exceptions: + // * If you are displaying a color using the low-level watch_set_led_color function, you should return false. + // * If you are sounding the buzzer using the low-level watch_set_buzzer_on function, you should return false. + // Note that if you are driving the LED or buzzer using Movement functions like movement_illuminate_led or + // movement_play_alarm, you can still return true. This guidance only applies to the low-level watch_ functions. + return true; +} + +void hpt_lapsplit_chrono_face_resign(movement_settings_t *settings, void *context) +{ + (void)settings; + (void)context; + + // handle any cleanup before your watch face goes off-screen. + // reset tick frequency + movement_request_tick_frequency(1); +} diff --git a/movement/watch_faces/complication/hpt_lapsplit_chrono_face.h b/movement/watch_faces/complication/hpt_lapsplit_chrono_face.h new file mode 100644 index 000000000..cdf63d949 --- /dev/null +++ b/movement/watch_faces/complication/hpt_lapsplit_chrono_face.h @@ -0,0 +1,117 @@ +/* + * MIT License + * + * Copyright (c) 2024 Zach Miller + * + * 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. + */ + +#ifndef HPT_LAPSPLIT_CHRONO_FACE_H_ +#define HPT_LAPSPLIT_CHRONO_FACE_H_ + +#include "movement.h" + +/* + * A lap/split chronograph accurate to thousandths of a second (though only hundreths are displayed). + * + * Display: + * The chronograph face will display CH in the day-of-week digits to indicate the mode. ("CHronograph") + * The chronograph time will be displayed in the primary digits in MM:SS CC format. If the time exceeds 1 hour, + * the number of hours will be displayed in the top right corner (up to 24 hours). The colon in the time display will flash while + * the chronograph is running. If the chronograph is in "lap" mode, the word "LAP" will be displayed, otherwise, + * the chronograph is in "split" mode. + * + * In Lap mode, the number of recorded laps will be displayed in the top right corner of the display, up to 39 laps + * before restarting from zero. In split mode, the upper right corner of the display will show hours elapsed, up to 39. + * If more than 39 hours have elapsed in split mode, the letter E will show, indicating an Excessive amount of time has elapsed. + * + * Buttons: + * - LIGHT: Lap+Split/Reset - Pressing this while the chronograph is running will display a lap/split time while the chronograph + * continues to run in the background (This is indicated by the flashing colon). In lap mode, the + * chronograph will be restated from zero. Press this again while a lap/split time is being + * displayed to return to the running time. Press this while the chronograph is paused to reset to zero. + * Press this while the chronograph is stopped and reading zero will toggle between "lap" and "split" modes + * - ALARM: Start/Stop - Press this to start or pause the chronograph. + * + * Two-finisher operation: While the chronograph is "split" mode and running, press [LIGHT] when the first + * finisher crosses the line to display their split time. The chronograph will continue to run in the background. + * Press [ALARM] when the second finisher crosses the line to stop the chronograph. The display will continue to + * show the time of the first finisher. Press [LIGHT] to show the finish time of the second competitor. Press [LIGHT] + * again to reset the chronograph, or [ALARM] to start the timer again. + * + * If the chronograph is stopped, the display will time out after the configured time and return to the main screen + */ + +// For some reason, when I wrote these, I thought that in C, zero was true and nonzero was false (probably because of bash scripts) +// So I did a lot of explicit checking for zero and nonzero instead of using bools +// Probably should have just used bools. + +#define LCF_MODE_LAP 1 +#define LCF_MODE_SPLIT 0 + +#define LCF_RUN_RUNNING 1 +#define LCF_RUN_STOPPED 0 + +// show the time based on "splitTs" +#define LCF_DISPLAY_SPLIT 1 +// show the time based on the time elapsed since "startTs" +#define LCF_DISPLAY_TIME 0 + +typedef struct +{ + /** LCF_MODE */ + uint8_t mode : 1; + + /** LCF_DISPLAY */ + uint8_t display : 1; + + /** LCF_RUN */ + uint8_t running : 1; + + uint8_t _padding1 : 5; // align to byte boundaries + + // up to 39 laps, then reset + uint8_t laps : 6; + + uint8_t _padding2 :2; + + // when running, start timestamp is the zero index from which the timer is running from + uint64_t startTs; + + // duration recorded time when chronograph is paused, in 1/1024ths of a second + uint64_t pausedTs; + + // duration of lap/split time + uint64_t splitTs; +} hpt_lapsplit_chrono_state_t; + +void hpt_lapsplit_chrono_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void **context_ptr); +void hpt_lapsplit_chrono_face_activate(movement_settings_t *settings, void *context); +bool hpt_lapsplit_chrono_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void hpt_lapsplit_chrono_face_resign(movement_settings_t *settings, void *context); + +#define hpt_lapsplit_chrono_face ((const watch_face_t){ \ + hpt_lapsplit_chrono_face_setup, \ + hpt_lapsplit_chrono_face_activate, \ + hpt_lapsplit_chrono_face_loop, \ + hpt_lapsplit_chrono_face_resign, \ + NULL, \ +}) + +#endif // HPT_LAPSPLIT_CHRONO_FACE_H_ diff --git a/movement/watch_faces/complication/interval_face.c b/movement/watch_faces/complication/interval_face.c index f4983236f..b433f05f6 100644 --- a/movement/watch_faces/complication/interval_face.c +++ b/movement/watch_faces/complication/interval_face.c @@ -334,7 +334,7 @@ static void _set_next_timestamp(interval_face_state_t *state) { watch_date_time target_dt = watch_utility_date_time_from_unix_time(_target_ts, 0); movement_schedule_background_task_for_face(state->face_idx, target_dt); // play sound - watch_buzzer_play_sequence(sound_seq, NULL); + movement_play_sequence(sound_seq, NULL); } static inline bool _is_timer_empty(interval_timer_setting_t *timer) { @@ -608,7 +608,7 @@ bool interval_face_loop(movement_event_t event, movement_settings_t *settings, v state->face_state = interval_state_waiting; _init_timer_info(state); _face_draw(state, event.subsecond); - watch_buzzer_play_sequence((int8_t *)_sound_seq_finish, NULL); + movement_play_sequence((int8_t *)_sound_seq_finish, NULL); } break; case EVENT_TIMEOUT: diff --git a/movement/watch_faces/complication/invaders_face.c b/movement/watch_faces/complication/invaders_face.c index c3b13c680..b2e63b73a 100644 --- a/movement/watch_faces/complication/invaders_face.c +++ b/movement/watch_faces/complication/invaders_face.c @@ -98,7 +98,7 @@ static inline void _resume_buttons() { /// @brief play a sound sequence if the game is in beepy mode static inline void _play_sequence(invaders_state_t *state, int8_t *sequence) { - if (state->sound_on) watch_buzzer_play_sequence((int8_t *)sequence, NULL); + if (state->sound_on) movement_play_sequence((int8_t *)sequence, NULL); } /// @brief draw the remaining defense lines @@ -140,7 +140,7 @@ static void _game_over(invaders_state_t *state) { _current_state = invaders_state_game_over; movement_request_tick_frequency(1); _signals.suspend_buttons = true; - if (state->sound_on) watch_buzzer_play_sequence((int8_t *)_sound_seq_game_over, _resume_buttons); + if (state->sound_on) movement_play_sequence((int8_t *)_sound_seq_game_over, _resume_buttons); // save current score to highscore, if applicable if (_score > state->highscore) state->highscore = _score; } diff --git a/movement/watch_faces/complication/kitchen_conversions_face.c b/movement/watch_faces/complication/kitchen_conversions_face.c index c19e75540..80af77ec6 100644 --- a/movement/watch_faces/complication/kitchen_conversions_face.c +++ b/movement/watch_faces/complication/kitchen_conversions_face.c @@ -258,7 +258,7 @@ static void display(kitchen_conversions_state_t *state, movement_settings_t *set watch_display_string("Err", 5); if (settings->bit.button_should_sound) - watch_buzzer_play_sequence(calc_fail_seq, NULL); + movement_play_sequence(calc_fail_seq, NULL); } else { @@ -278,7 +278,7 @@ static void display(kitchen_conversions_state_t *state, movement_settings_t *set } if (settings->bit.button_should_sound) - watch_buzzer_play_sequence(calc_success_seq, NULL); + movement_play_sequence(calc_success_seq, NULL); } watch_display_string("=", 1); } diff --git a/movement/watch_faces/complication/sailing_face.c b/movement/watch_faces/complication/sailing_face.c index a6c13fe86..524e8d29f 100644 --- a/movement/watch_faces/complication/sailing_face.c +++ b/movement/watch_faces/complication/sailing_face.c @@ -158,7 +158,7 @@ static void ring(sailing_state_t *state, movement_settings_t *settings) { movement_cancel_background_task(); if (beepflag + 1 == beepseconds_size) { //equivalent to (beepflag + 1 == sizeof(beepseconds) / sizeof(int)) but without needing to divide here => quicker if (alarmflag != 0){ - watch_buzzer_play_sequence(long_beep, NULL); + movement_play_sequence(long_beep, NULL); } movement_cancel_background_task(); counting(state); @@ -171,14 +171,14 @@ static void ring(sailing_state_t *state, movement_settings_t *settings) { for (int i = 0; i < 5; i++) { if (beepseconds[beepflag] == 60 * state->minutes[i]) { if (alarmflag > 1) { - watch_buzzer_play_sequence((int8_t *)double_beep, NULL); + movement_play_sequence((int8_t *)double_beep, NULL); } ringflag = true; } } if (!ringflag) { if (alarmflag == 3) { - watch_buzzer_play_sequence((int8_t *)single_beep, NULL); + movement_play_sequence((int8_t *)single_beep, NULL); } } ringflag = false; @@ -197,7 +197,7 @@ static void start(sailing_state_t *state, movement_settings_t *settings) {//gets state->now_ts = watch_utility_date_time_to_unix_time(now, get_tz_offset(settings)); state->target_ts = state->now_ts; if (alarmflag != 0){ - watch_buzzer_play_sequence(long_beep, NULL); + movement_play_sequence(long_beep, NULL); } counting(state); return; diff --git a/movement/watch_faces/complication/stock_stopwatch_face.c b/movement/watch_faces/complication/stock_stopwatch_face.c index 4a9608d91..971c939c2 100644 --- a/movement/watch_faces/complication/stock_stopwatch_face.c +++ b/movement/watch_faces/complication/stock_stopwatch_face.c @@ -42,18 +42,13 @@ #if __EMSCRIPTEN__ #include #include -#else -#include "../../../watch-library/hardware/include/saml22j18a.h" -#include "../../../watch-library/hardware/include/component/tc.h" -#include "../../../watch-library/hardware/hri/hri_tc_l22.h" #endif -// distant future for background task: January 1, 2083 -static const watch_date_time distant_future = { - .unit = {0, 0, 0, 1, 1, 63} -}; +// the HPT timestamp the stopwatch was most recently started +static uint64_t _hpt_start_timestamp; -static uint32_t _ticks; +// an accumulated ticks value that is updated when the stopwatch is paused +static uint32_t _paused_ticks; static uint32_t _lap_ticks; static uint8_t _blink_ticks; static uint32_t _old_seconds; @@ -62,70 +57,17 @@ static uint8_t _hours; static bool _colon; static bool _is_running; -#if __EMSCRIPTEN__ - -static long _em_interval_id = 0; - -void em_cb_handler(void *userData) { - // interrupt handler for emscripten 128 Hz callbacks - (void) userData; - _ticks++; -} - -static void _cb_initialize() { } - -static inline void _cb_stop() { - emscripten_clear_interval(_em_interval_id); - _em_interval_id = 0; - _is_running = false; -} - -static inline void _cb_start() { - // initiate 128 hz callback - _em_interval_id = emscripten_set_interval(em_cb_handler, (double)(1000/128), (void *)NULL); -} - -#else - -static inline void _cb_start() { - // start the TC2 timer - hri_tc_set_CTRLA_ENABLE_bit(TC2); - _is_running = true; -} - -static inline void _cb_stop() { - // stop the TC2 timer - hri_tc_clear_CTRLA_ENABLE_bit(TC2); - _is_running = false; -} - -static void _cb_initialize() { - // setup and initialize TC2 for a 64 Hz interrupt - hri_mclk_set_APBCMASK_TC2_bit(MCLK); - hri_gclk_write_PCHCTRL_reg(GCLK, TC2_GCLK_ID, GCLK_PCHCTRL_GEN_GCLK3 | GCLK_PCHCTRL_CHEN); - _cb_stop(); - hri_tc_write_CTRLA_reg(TC2, TC_CTRLA_SWRST); - hri_tc_wait_for_sync(TC2, TC_SYNCBUSY_SWRST); - hri_tc_write_CTRLA_reg(TC2, TC_CTRLA_PRESCALER_DIV64 | // 32 Khz divided by 64 divided by 4 results in a 128 Hz interrupt - TC_CTRLA_MODE_COUNT8 | - TC_CTRLA_RUNSTDBY); - hri_tccount8_write_PER_reg(TC2, 3); - hri_tc_set_INTEN_OVF_bit(TC2); - NVIC_ClearPendingIRQ(TC2_IRQn); - NVIC_EnableIRQ (TC2_IRQn); -} - -void TC2_Handler(void) { - // interrupt handler for TC2 (globally!) - _ticks++; - TC2->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF; +static inline uint32_t _get_running_ticks(void) { + uint32_t ticks = _paused_ticks; + if(_is_running) { + ticks += (movement_hpt_get() - _hpt_start_timestamp)/8; + } + return ticks; } -#endif - static inline void _button_beep(movement_settings_t *settings) { // play a beep as confirmation for a button press (if applicable) - if (settings->bit.button_should_sound) watch_buzzer_play_note(BUZZER_NOTE_C7, 50); + if (settings->bit.button_should_sound) movement_play_note(BUZZER_NOTE_C7, 50); } /// @brief Display minutes, seconds and fractions derived from 128 Hz tick counter @@ -145,6 +87,8 @@ static void _display_ticks(uint32_t ticks) { /// @brief Displays the current stopwatch time on the LCD (more optimized than _display_ticks()) static void _draw() { + + uint32_t _ticks = _get_running_ticks(); if (_lap_ticks == 0) { char buf[14]; uint8_t sec_100 = (_ticks & 0x7F) * 100 / 128; @@ -209,36 +153,24 @@ void stock_stopwatch_face_setup(movement_settings_t *settings, uint8_t watch_fac *context_ptr = malloc(sizeof(stock_stopwatch_state_t)); memset(*context_ptr, 0, sizeof(stock_stopwatch_state_t)); stock_stopwatch_state_t *state = (stock_stopwatch_state_t *)*context_ptr; - _ticks = _lap_ticks = _blink_ticks = _old_minutes = _old_seconds = _hours = 0; + _paused_ticks = _lap_ticks = _blink_ticks = _old_minutes = _old_seconds = _hours = 0; _is_running = _colon = false; state->light_on_button = true; } - if (!_is_running) { - // prepare the 128 Hz callback source - _cb_initialize(); - } } void stock_stopwatch_face_activate(movement_settings_t *settings, void *context) { (void) settings; (void) context; - if (_is_running) { - // The background task will keep the watch from entering low energy mode while the stopwatch is on screen. - movement_schedule_background_task(distant_future); - } + } + + bool stock_stopwatch_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { stock_stopwatch_state_t *state = (stock_stopwatch_state_t *)context; - // handle overflow of fast ticks - while (_ticks >= (128 * 60 * 60)) { - _ticks -= (128 * 60 * 60); - _hours++; - if (_hours >= 24) _hours -= 24; - // initiate a re-draw - _old_minutes = 59; - } + uint32_t _ticks = _get_running_ticks(); switch (event.event_type) { case EVENT_ACTIVATE: @@ -262,17 +194,14 @@ bool stock_stopwatch_face_loop(movement_event_t event, movement_settings_t *sett if (_is_running) { // start or continue stopwatch movement_request_tick_frequency(16); - // register 128 hz callback for time measuring - _cb_start(); - // schedule the keepalive task when running - movement_schedule_background_task(distant_future); + movement_hpt_request(); + _hpt_start_timestamp = movement_hpt_get(); } else { // stop the stopwatch - _cb_stop(); movement_request_tick_frequency(1); _set_colon(); - // cancel the keepalive task - movement_cancel_background_task(); + _paused_ticks += (movement_hpt_get() - _hpt_start_timestamp)/8; + movement_hpt_release(); } _draw(); _button_beep(settings); @@ -294,9 +223,9 @@ bool stock_stopwatch_face_loop(movement_event_t event, movement_settings_t *sett if (_lap_ticks) { // clear lap and show running stopwatch _lap_ticks = 0; - } else if (_ticks) { + } else if (_paused_ticks) { // reset stopwatch - _ticks = _lap_ticks = _blink_ticks = _old_minutes = _old_seconds = _hours = 0; + _paused_ticks = _lap_ticks = _blink_ticks = _old_minutes = _old_seconds = _hours = 0; _button_beep(settings); } } diff --git a/movement/watch_faces/complication/timer_face.c b/movement/watch_faces/complication/timer_face.c index 29392d694..f13cd1fb7 100644 --- a/movement/watch_faces/complication/timer_face.c +++ b/movement/watch_faces/complication/timer_face.c @@ -43,7 +43,7 @@ static inline int32_t _get_tz_offset(movement_settings_t *settings) { static void _signal_callback() { if (_beeps_to_play) { _beeps_to_play--; - watch_buzzer_play_sequence((int8_t *)_sound_seq_beep, _signal_callback); + movement_play_sequence((int8_t *)_sound_seq_beep, _signal_callback); } } @@ -62,7 +62,7 @@ static void _start(timer_state_t *state, movement_settings_t *settings, bool wit state->mode = running; movement_schedule_background_task_for_face(state->watch_face_index, target_dt); watch_set_indicator(WATCH_INDICATOR_BELL); - if (with_beep) watch_buzzer_play_sequence((int8_t *)_sound_seq_start, NULL); + if (with_beep) movement_play_sequence((int8_t *)_sound_seq_start, NULL); } static void _draw(timer_state_t *state, uint8_t subsecond) { @@ -304,7 +304,7 @@ bool timer_face_loop(movement_event_t event, movement_settings_t *settings, void case EVENT_BACKGROUND_TASK: // play the alarm _beeps_to_play = 4; - watch_buzzer_play_sequence((int8_t *)_sound_seq_beep, _signal_callback); + movement_play_sequence((int8_t *)_sound_seq_beep, _signal_callback); _reset(state); if (state->timers[state->current_timer].unit.repeat) _start(state, settings, false); break; diff --git a/watch-library/hardware/watch/watch_buzzer.c b/watch-library/hardware/watch/watch_buzzer.c index 2dce8d23a..91403d04d 100644 --- a/watch-library/hardware/watch/watch_buzzer.c +++ b/watch-library/hardware/watch/watch_buzzer.c @@ -28,121 +28,6 @@ #include "../../../watch-library/hardware/include/component/tc.h" #include "../../../watch-library/hardware/hri/hri_tc_l22.h" -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 void (*_cb_finished)(void); - -static void _tcc_write_RUNSTDBY(bool value) { - // enables or disables RUNSTDBY of the tcc - hri_tcc_clear_CTRLA_ENABLE_bit(TCC0); - hri_tcc_write_CTRLA_RUNSTDBY_bit(TCC0, value); - hri_tcc_set_CTRLA_ENABLE_bit(TCC0); - hri_tcc_wait_for_sync(TCC0, TCC_SYNCBUSY_ENABLE); -} - -static inline void _tc3_start() { - // start the TC3 timer - hri_tc_set_CTRLA_ENABLE_bit(TC3); - _callback_running = true; -} - -static inline void _tc3_stop() { - // stop the TC3 timer - hri_tc_clear_CTRLA_ENABLE_bit(TC3); - hri_tc_wait_for_sync(TC3, TC_SYNCBUSY_ENABLE); - _callback_running = false; -} - -static void _tc3_initialize() { - // setup and initialize TC3 for a 64 Hz interrupt - hri_mclk_set_APBCMASK_TC3_bit(MCLK); - hri_gclk_write_PCHCTRL_reg(GCLK, TC3_GCLK_ID, GCLK_PCHCTRL_GEN_GCLK3 | GCLK_PCHCTRL_CHEN); - _tc3_stop(); - hri_tc_write_CTRLA_reg(TC3, TC_CTRLA_SWRST); - hri_tc_wait_for_sync(TC3, TC_SYNCBUSY_SWRST); - hri_tc_write_CTRLA_reg(TC3, TC_CTRLA_PRESCALER_DIV64 | - TC_CTRLA_MODE_COUNT8 | - TC_CTRLA_RUNSTDBY); - hri_tccount8_write_PER_reg(TC3, 7); // 32 Khz divided by 64 divided by 8 equals 64 Hz - hri_tc_set_INTEN_OVF_bit(TC3); - NVIC_ClearPendingIRQ(TC3_IRQn); - NVIC_EnableIRQ (TC3_IRQn); -} - -void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void)) { - if (_callback_running) _tc3_stop(); - watch_set_buzzer_off(); - _sequence = note_sequence; - _cb_finished = callback_on_end; - _seq_position = 0; - _tone_ticks = 0; - _repeat_counter = -1; - // prepare buzzer - watch_enable_buzzer(); - // setup TC3 timer - _tc3_initialize(); - // TCC should run in standby mode - _tcc_write_RUNSTDBY(true); - // start the timer (for the 64 hz callback) - _tc3_start(); -} - -void cb_watch_buzzer_seq(void) { - // callback for reading the note sequence - if (_tone_ticks == 0) { - if (_sequence[_seq_position] < 0 && _sequence[_seq_position + 1]) { - // repeat indicator found - if (_repeat_counter == -1) { - // first encounter: load repeat counter - _repeat_counter = _sequence[_seq_position + 1]; - } else _repeat_counter--; - if (_repeat_counter > 0) - // rewind - if (_seq_position > _sequence[_seq_position] * -2) - _seq_position += _sequence[_seq_position] * 2; - else - _seq_position = 0; - else { - // continue - _seq_position += 2; - _repeat_counter = -1; - } - } - if (_sequence[_seq_position] && _sequence[_seq_position + 1]) { - // read note - BuzzerNote note = _sequence[_seq_position]; - if (note != BUZZER_NOTE_REST) { - watch_set_buzzer_period(NotePeriods[note]); - watch_set_buzzer_on(); - } else watch_set_buzzer_off(); - // set duration ticks and move to next tone - _tone_ticks = _sequence[_seq_position + 1]; - _seq_position += 2; - } 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) _tc3_stop(); - watch_set_buzzer_off(); - // disable standby mode for TCC - _tcc_write_RUNSTDBY(false); -} - -void TC3_Handler(void) { - // interrupt handler vor TC3 (globally!) - cb_watch_buzzer_seq(); - TC3->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF; -} inline void watch_enable_buzzer(void) { if (!hri_tcc_get_CTRLA_reg(TCC0, TCC_CTRLA_ENABLE)) { @@ -173,7 +58,7 @@ void watch_buzzer_play_note(BuzzerNote note, uint16_t duration_ms) { if (note == BUZZER_NOTE_REST) { watch_set_buzzer_off(); } else { - watch_set_buzzer_period(NotePeriods[note]); + watch_set_buzzer_period(NotePeriods[note-1]); watch_set_buzzer_on(); } delay_ms(duration_ms); diff --git a/watch-library/hardware/watch/watch_hpt.c b/watch-library/hardware/watch/watch_hpt.c new file mode 100644 index 000000000..d332e6386 --- /dev/null +++ b/watch-library/hardware/watch/watch_hpt.c @@ -0,0 +1,221 @@ +#include "watch_hpt.h" +#include "parts.h" + +// user HPT callback +void (*hpt_isr_callback)(HPT_CALLBACK_CAUSE) = NULL; + +// actual HPT ISR +void TC2_Handler(void) +{ + // check flags + HPT_CALLBACK_CAUSE cause; + cause.overflow = TC2->COUNT32.INTFLAG.bit.OVF != 0; // TC2.INTFLAG.OVF + cause.compare_match = TC2->COUNT32.INTFLAG.bit.MC0 != 0; // TC2.INTFLAG.MC0 + // clear interrupt flags + // TC2.INTFLAG = 0xFF + // silly that you have to write ones these flags to clear them + TC2->COUNT32.INTFLAG.reg = 0xFF; + + if (hpt_isr_callback) + { + (*hpt_isr_callback)(cause); + } +} + +void watch_hpt_init(void (*callback_function)(HPT_CALLBACK_CAUSE)) +{ + hpt_isr_callback = callback_function; + + // set up clock generator for TC2 at 1024hz + + // Let's use gen 2 for this + + GCLK_CRITICAL_SECTION_ENTER(); + + // Setup generator 2 for 1024 output + // GENCTRL2: + // SRC = 0x4 - external 32k oscillator + // DIV = 4 - 32k/ (2^(4+1)) = 1k + // DIVSEL = 1 - divide clock by 2^(DIV+1), not just by DIV. (of course, 32 could easily fit in DIV, but it's a power of two, why not) + // RUNSTDBY = 0 - this only seems needed if it's powering a pin, which it is not. + // OE = 0 - no output + // OOV = 0 - i don't think this matters + // IDC = 0 - don't think we need to worry about duty cycle + // enable it too + GCLK->GENCTRL[2].reg = + GCLK_GENCTRL_SRC_XOSC32K | + GCLK_GENCTRL_DIV(4) | + GCLK_GENCTRL_DIVSEL | + GCLK_GENCTRL_GENEN; + + // Configure generator 2 as the source for TC2 + // TC2/3 is peripheral 24 + // PCHCTRL24: + // WRTLOCK = 0 // don't write lock it (maybe do write lock it?) + // CHEN = 1 // do enable the channel mapping + // GEN = 2 -- select generator 2 we just configured + + GCLK->PCHCTRL[24].reg = + GCLK_PCHCTRL_CHEN | + GCLK_PCHCTRL_GEN_GCLK2; + + GCLK_CRITICAL_SECTION_LEAVE(); + + + // Configure TC2 to count up to MAX and generate appropriate interrupts, but don't turn it on + + // I don't really know if these critical sections are important, but they don't do anything by default, so why not? + TC_CRITICAL_SECTION_ENTER(); + + // reset TC2 + TC2->COUNT32.CTRLA.bit.SWRST = 1; + // wait for reset to complete + while (TC2->COUNT32.SYNCBUSY.bit.SWRST) + ; + + // Set up CTRLA: + // COPEN0 = 0 - not doing any captures + // COPEN1 = 0 + // CAPTEN0 = 0 - CC0 is our main compare channel not a capture channel + // CAPTEN1 = 0 + // ALOCK = ? - we should figure out what this is for, maybe this will help with interrupt handling + // PRESCALER = 0 - input clock is already 1024hz + // ONDEMAND = 1 - only request clock active when timer is running. This is fine if this is the only peripheral using our clock generator + // RUNSTDBY = 1 - we *do* want the timer to continue running in standby, so it may be used to wake up the cpu and perform tasks + // PRESCSYNC = 0 - we are not using the prescaler anyway, so this doesn't matter + // MODE = 2 - 32-bit mode + // ENABLE = 0 don't enable it just yet + TC2->COUNT32.CTRLA.reg = + TC_CTRLA_ONDEMAND | + TC_CTRLA_RUNSTDBY | + TC_CTRLA_MODE_COUNT32; + + // Don't bother synchronizing it here. it will be synchronized when the timer is enabled. + + // Clear the count just to be safe + TC2->COUNT32.COUNT.bit.COUNT = 0; + + // synchronizing this might not be necessary + while (TC2->COUNT32.SYNCBUSY.bit.COUNT) + ; + + // enable TC2 interrupt + // Disable IRQ temporarily while setting up interrupt flags + NVIC_DisableIRQ(TC2_IRQn); + + // TC2.INTENSET: enabling interrupts + + // Always enable overflow interrupt + TC2->COUNT32.INTENSET.bit.OVF = 1; + + // Disable compare match interrupt initially. Will be enabled later if necessary + TC2->COUNT32.INTENCLR.bit.MC0 = 1; + + // Clear any pending interrupt flags + TC2->COUNT32.INTFLAG.reg = TC_INTFLAG_MC0 | TC_INTFLAG_OVF; + + // Enable timer IRQ in NVIC + NVIC_ClearPendingIRQ(TC2_IRQn); + NVIC_EnableIRQ(TC2_IRQn); + + TC_CRITICAL_SECTION_LEAVE(); +} + +void watch_hpt_enable(void) +{ + // movement should be keeping track of whether this timer is enabled or not, so it's fine to just naïvely enable it here without checking to see if it was previously enabled. + + // start timer + TC_CRITICAL_SECTION_ENTER(); + TC2->COUNT32.CTRLA.bit.ENABLE = 1; + // wait for timer to be enabled + while (TC2->COUNT32.SYNCBUSY.bit.ENABLE) + ; + TC_CRITICAL_SECTION_LEAVE(); +} + +void watch_hpt_disable(void) +{ + // stop timer + TC_CRITICAL_SECTION_ENTER(); + TC2->COUNT32.CTRLA.bit.ENABLE = 0; + // wait for timer to be disabled (i mean, maybe? why bother waiting if nobody needs it anymore) + while (TC2->COUNT32.SYNCBUSY.bit.ENABLE) + ; + TC_CRITICAL_SECTION_LEAVE(); +} + +void watch_hpt_schedule_callback(uint32_t timestamp) +{ + TC_CRITICAL_SECTION_ENTER(); + + // cleare MC0 interrupt status + TC2->COUNT32.INTFLAG.reg = TC_INTFLAG_MC0; + + // set compare value + TC2->COUNT32.CC[0].reg = timestamp; + // wait for counter value to be synchronized + while (TC2->COUNT32.SYNCBUSY.bit.CC0) + ; + + // enable MC0 interrupt + TC2->COUNT32.INTENSET.bit.MC0 = 1; + TC_CRITICAL_SECTION_LEAVE(); +} + +void watch_hpt_disable_scheduled_callback(void) +{ + // disable interrupt + TC_CRITICAL_SECTION_ENTER(); + TC2->COUNT32.INTENCLR.bit.MC0 = 1; // disable match/compare 0 interrupt + TC_CRITICAL_SECTION_LEAVE(); +} + +// There is a lot of busy-waiting in here that involves synchronizing stuff between the main CPU and the timer +// For example, when reading from the timer, the count must be synchronized first by executing a "READSYNC" command. +// This command gets executed on the next TC clock cycle, which is running at 1024hz, meaning the CPU might be spinning +// for up to a millisecond just to read the value from the counter. + +// This is somewhat unfortunate, because interrupt triggering is much faster than that. I was having trouble with the +// ISR on compare events because I'd schedule an event at like, t=1000, but when I called watch_hpt_get() from inside +// the ISR, I would get some lower value like t=996. + +// After adding the appropriate busy-wait checks for synchronization, it started coming back correct. If I scheduled a +// callback at T=1000, I could call watch_hpt_get() inside the ISR and it would return T=1005. A later timestamp is +// fine, but does this mean that the CPU was just spinning for 5 milliseconds waiting for the timer? + +// There's gotta be a faster technique for synchronizing the timer and CPU. + +uint32_t watch_hpt_get(void) +{ + // synchronize a read of the count value + TC_CRITICAL_SECTION_ENTER(); + TC2->COUNT32.CTRLBSET.reg = TC_CTRLBSET_CMD_READSYNC; + + // wait for command to be executed. CMD is cleared by the timer when the command is received + while (TC2->COUNT32.CTRLBSET.bit.CMD) + ; + + // wait for CTRLB to be synchronized to the timer + // this might not be necessary, since we just waited for CMD + while (TC2->COUNT32.SYNCBUSY.bit.CTRLB) + ; + + // wait for count to be synchronized + while (TC2->COUNT32.SYNCBUSY.bit.COUNT) + ; + + // finally safe to read count + uint32_t count = TC2->COUNT32.COUNT.bit.COUNT; + TC_CRITICAL_SECTION_LEAVE(); + return count; +} + +uint32_t watch_hpt_get_fast(void) +{ + // quick and dirty timer read, not suitable for scheduling purposes + TC_CRITICAL_SECTION_ENTER(); + uint32_t count = TC2->COUNT32.COUNT.bit.COUNT; + TC_CRITICAL_SECTION_LEAVE(); + return count; +} \ No newline at end of file diff --git a/watch-library/hardware/watch/watch_private.c b/watch-library/hardware/watch/watch_private.c index cd607b8e8..2bd5f3dec 100644 --- a/watch-library/hardware/watch/watch_private.c +++ b/watch-library/hardware/watch/watch_private.c @@ -147,6 +147,10 @@ void _watch_enable_tcc(void) { hri_tcc_write_CC_reg(TCC0, WATCH_BUZZER_TCC_CHANNEL, 0); hri_tcc_write_CC_reg(TCC0, WATCH_RED_TCC_CHANNEL, 0); hri_tcc_write_CC_reg(TCC0, WATCH_GREEN_TCC_CHANNEL, 0); + + // Always run TCC in standby + hri_tcc_write_CTRLA_RUNSTDBY_bit(TCC0, true); + // Enable the TCC hri_tcc_set_CTRLA_ENABLE_bit(TCC0); hri_tcc_wait_for_sync(TCC0, TCC_SYNCBUSY_ENABLE); diff --git a/watch-library/shared/watch/watch_buzzer.h b/watch-library/shared/watch/watch_buzzer.h index 4c39475c2..736300368 100644 --- a/watch-library/shared/watch/watch_buzzer.h +++ b/watch-library/shared/watch/watch_buzzer.h @@ -59,6 +59,7 @@ void watch_set_buzzer_off(void); /// @brief 87 notes for use with watch_buzzer_play_note typedef enum BuzzerNote { + BUZZER_NOTE_END = 0, BUZZER_NOTE_A1, ///< 55.00 Hz BUZZER_NOTE_A1SHARP_B1FLAT, ///< 58.27 Hz BUZZER_NOTE_B1, ///< 61.74 Hz @@ -158,7 +159,7 @@ typedef enum BuzzerNote { void watch_buzzer_play_note(BuzzerNote note, uint16_t duration_ms); /// @brief An array of periods for all the notes on a piano, corresponding to the names in BuzzerNote. -extern const uint16_t NotePeriods[108]; +extern const uint16_t NotePeriods[87]; /** @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 @@ -173,13 +174,13 @@ extern const uint16_t NotePeriods[108]; * zero byte, which is used here as the end-of-sequence marker. But hey, a frequency that low cannot be * played properly by the watch's buzzer, anyway. */ -void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void)); +//void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void)); uint16_t sequence_length(int8_t *sequence); /** @brief Aborts a playing sequence. */ -void watch_buzzer_abort_sequence(void); +//void watch_buzzer_abort_sequence(void); #ifndef __EMSCRIPTEN__ void TC3_Handler(void); diff --git a/watch-library/shared/watch/watch_hpt.h b/watch-library/shared/watch/watch_hpt.h new file mode 100644 index 000000000..fb4f38fb6 --- /dev/null +++ b/watch-library/shared/watch/watch_hpt.h @@ -0,0 +1,79 @@ +#ifndef WATCH_HPT_H__ +#define WATCH_HPT_H__ + +#include +#include + +/** + * watch_hpt provides low-level access to the high-precision timer. + * + * These methods are not intended to be used by watch faces. See the + * "movement_hpt_*" faces in movement.h instead. +*/ + +/** + * Describes the reasons the HPT callback is being invoked. More than one flag may be set. +*/ +typedef struct { + + /** + * The callback is being invoked because the count is greater than or equal to to the scheduled timestamp + */ + bool compare_match :1; + + /** + * The callback is being invoked because the counter overflowed and reset to zero. + */ + bool overflow :1; + + // not used + uint8_t _padding :6; +} HPT_CALLBACK_CAUSE; + + + +/** + * Performs one-time setup of the peripherals used by the high-precision timer. + * + * Does not start the timer. + * + * @param callback_function an interrupt handler that will be invoked when the timer hits a scheduled timestamp or overflows. +*/ +void watch_hpt_init(void (*callback_function)(HPT_CALLBACK_CAUSE cause)); + +/** + * Enables and starts the high-precision timer. The timestamp *may* be reset to zero if the timer was not already running. + */ +void watch_hpt_enable(void); + +/** + * Stops the high-precision timer and powers it down. + */ +void watch_hpt_disable(void); + +/** + * Returns the current counter value of the high-precision timer. + */ +uint32_t watch_hpt_get(void); + +/** + * Returns the current timestamp of the high-precision timer, without synchronization. + * + * The timestamp returned by this method is not suitable for scheduling purposes or other complex logic, but it may be good enough for non-critical purposes, such as showing the current time of a running stopwatch. +*/ +uint32_t watch_hpt_get_fast(void); + +/** + * Sets the timestamp at which the previously registered callback should be invoked. Note that this will be called every time the counter value reaches this value, including after an overflow occurs. +*/ +void watch_hpt_schedule_callback(uint32_t timestamp); + +/** + * Disables any previously scheduled callback. +*/ +void watch_hpt_disable_scheduled_callback(void); + +// TC2 Interrupt Handler (internal) +void TC2_Handler(void); + +#endif \ No newline at end of file diff --git a/watch-library/shared/watch/watch_private_buzzer.c b/watch-library/shared/watch/watch_private_buzzer.c index def54a469..dc2888754 100644 --- a/watch-library/shared/watch/watch_private_buzzer.c +++ b/watch-library/shared/watch/watch_private_buzzer.c @@ -23,6 +23,12 @@ */ #include "driver_init.h" +#include "watch_private_buzzer.h" + +// note: the buzzer uses a 1 MHz clock. these values were determined by dividing 1,000,000 by the target frequency. +// i.e. for a 440 Hz tone (A4 on the piano), 1MHz/440Hz = 2273 +const uint16_t NotePeriods[87] = {18182,17161,16197,15288,14430,13620,12857,12134,11453,10811,10204,9631,9091,8581,8099,7645,7216,6811,6428,6068,5727,5405,5102,4816,4545,4290,4050,3822,3608,3405,3214,3034,2863,2703,2551,2408,2273,2145,2025,1911,1804,1703,1607,1517,1432,1351,1276,1204,1136,1073,1012,956,902,851,804,758,716,676,638,602,568,536,506,478,451,426,402,379,358,338,319,301,284,268,253,239,225,213,201,190,179,169,159,150,142,134,127}; + uint16_t sequence_length(int8_t *sequence) { uint16_t result = 0; diff --git a/watch-library/shared/watch/watch_private_buzzer.h b/watch-library/shared/watch/watch_private_buzzer.h index 3017bbb54..8c57fee5d 100644 --- a/watch-library/shared/watch/watch_private_buzzer.h +++ b/watch-library/shared/watch/watch_private_buzzer.h @@ -24,9 +24,6 @@ #ifndef _WATCH_PRIVATE_BUZZER_H_INCLUDED #define _WATCH_PRIVATE_BUZZER_H_INCLUDED -// note: the buzzer uses a 1 MHz clock. these values were determined by dividing 1,000,000 by the target frequency. -// i.e. for a 440 Hz tone (A4 on the piano), 1MHz/440Hz = 2273 -const uint16_t NotePeriods[108] = {18182,17161,16197,15288,14430,13620,12857,12134,11453,10811,10204,9631,9091,8581,8099,7645,7216,6811,6428,6068,5727,5405,5102,4816,4545,4290,4050,3822,3608,3405,3214,3034,2863,2703,2551,2408,2273,2145,2025,1911,1804,1703,1607,1517,1432,1351,1276,1204,1136,1073,1012,956,902,851,804,758,716,676,638,602,568,536,506,478,451,426,402,379,358,338,319,301,284,268,253,239,225,213,201,190,179,169,159,150,142,134,127}; uint16_t sequence_length(int8_t *sequence); diff --git a/watch-library/simulator/watch/watch_buzzer.c b/watch-library/simulator/watch/watch_buzzer.c index 7ccb8545b..e3ef47c99 100644 --- a/watch-library/simulator/watch/watch_buzzer.c +++ b/watch-library/simulator/watch/watch_buzzer.c @@ -45,68 +45,6 @@ static inline void _em_interval_stop() { _em_interval_id = 0; } -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(); - _sequence = note_sequence; - _cb_finished = callback_on_end; - _seq_position = 0; - _tone_ticks = 0; - _repeat_counter = -1; - // prepare buzzer - watch_enable_buzzer(); - // initiate 64 hz callback - _em_interval_id = emscripten_set_interval(cb_watch_buzzer_seq, (double)(1000/64), (void *)NULL); -} - -void cb_watch_buzzer_seq(void *userData) { - // callback for reading the note sequence - (void) userData; - if (_tone_ticks == 0) { - if (_sequence[_seq_position] < 0 && _sequence[_seq_position + 1]) { - // repeat indicator found - if (_repeat_counter == -1) { - // first encounter: load repeat counter - _repeat_counter = _sequence[_seq_position + 1]; - } else _repeat_counter--; - if (_repeat_counter > 0) - // rewind - if (_seq_position > _sequence[_seq_position] * -2) - _seq_position += _sequence[_seq_position] * 2; - else - _seq_position = 0; - else { - // continue - _seq_position += 2; - _repeat_counter = -1; - } - } - if (_sequence[_seq_position] && _sequence[_seq_position + 1]) { - // read note - BuzzerNote note = _sequence[_seq_position]; - if (note == BUZZER_NOTE_REST) { - watch_set_buzzer_off(); - } else { - watch_set_buzzer_period(NotePeriods[note]); - watch_set_buzzer_on(); - } - // set duration ticks and move to next tone - _tone_ticks = _sequence[_seq_position + 1]; - _seq_position += 2; - } 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 (_em_interval_id) _em_interval_stop(); - watch_set_buzzer_off(); -} - void watch_enable_buzzer(void) { buzzer_enabled = true; buzzer_period = NotePeriods[BUZZER_NOTE_A4]; @@ -172,7 +110,7 @@ void watch_buzzer_play_note(BuzzerNote note, uint16_t duration_ms) { if (note == BUZZER_NOTE_REST) { watch_set_buzzer_off(); } else { - watch_set_buzzer_period(NotePeriods[note]); + watch_set_buzzer_period(NotePeriods[note-1]); watch_set_buzzer_on(); } diff --git a/watch-library/simulator/watch/watch_hpt.c b/watch-library/simulator/watch/watch_hpt.c new file mode 100644 index 000000000..f83529397 --- /dev/null +++ b/watch-library/simulator/watch/watch_hpt.c @@ -0,0 +1,211 @@ +#include +#include +#include "watch_hpt.h" +#include + +// the performance.now timestamp when the timer was enabled +volatile double simhpt_enabled_timestamp = 0; +// the number of counts previously accumulated from other instances of running +volatile uint32_t simhpt_paused_count = 0; +// whether the callback needs to be invoked +volatile bool simhpt_callback_enabled; +// the timestamp the callback needs to be invoked at +volatile uint32_t simhpt_callback_timestamp; +volatile bool simhpt_running = false; +void (*simhpt_callback_function)(HPT_CALLBACK_CAUSE cause) = 0; + +long simhpt_overflow_timeout_handle = 0; +long simhpt_compare_timeout_handle = 0; + +const double OVERFLOW_MSECS = (double)(UINT32_MAX) * (1024.0 / 1000.0); // this might be backwards, but it's close enough who cares + +//#define HPT_DEBUG + +static void cb_overflow(void *_unused) +{ +#ifdef HPT_DEBUG + printf("hpt-isr-overflow cause=%s\r\n", (char *)_unused); +#endif + // fire off an interrupt + if (simhpt_callback_function) + { + HPT_CALLBACK_CAUSE cause; + cause.compare_match = false; + cause.overflow = true; + cause._padding = 0; + (*simhpt_callback_function)(cause); + } + // schedule another overflow + simhpt_overflow_timeout_handle = emscripten_set_timeout(&cb_overflow, OVERFLOW_MSECS, "repeat"); +} + +volatile bool cb_compare_updated_timeout = false; + +static void cb_compare(void *_unused) +{ +#ifdef HPT_DEBUG + printf("hpt-isr-compare\r\n"); +#endif + + // keep track of whether the callback function set a new callback time or cleared it + // if they did not, we need to automatically trigger another one when the timer overflows + cb_compare_updated_timeout = false; + // fire off an interrupt + if (simhpt_callback_function) + { + HPT_CALLBACK_CAUSE cause; + cause.compare_match = true; + cause.overflow = false; + cause._padding = 0; + (*simhpt_callback_function)(cause); + } + + // they did not set a new timeout or clear it as a result of this callback, so set a new one automatically + if (cb_compare_updated_timeout == false) + { + simhpt_compare_timeout_handle = emscripten_set_timeout(&cb_compare, OVERFLOW_MSECS, 0); + } +} + +void watch_hpt_init(void (*callback_function)(HPT_CALLBACK_CAUSE cause)) +{ + simhpt_callback_function = callback_function; + simhpt_paused_count = 0; + simhpt_running = false; + simhpt_enabled_timestamp = 0; + simhpt_overflow_timeout_handle = 0; + simhpt_callback_enabled = false; + simhpt_callback_timestamp = 0; + simhpt_compare_timeout_handle = 0; +} + +void watch_hpt_enable(void) +{ + if (!simhpt_running) + { +#ifdef HPT_DEBUG + printf("enabling hpt\r\n"); +#endif + simhpt_enabled_timestamp = emscripten_performance_now(); + simhpt_running = true; + + uint32_t timer_value = simhpt_paused_count; +#ifdef HPT_DEBUG + printf("stored ticks: %d\r\n", timer_value); +#endif + + // I am at my wits end here. I cannot figure out why emscripten invokes this callback method almost immediately + // if I use a fake value for msec_until_next_overflow of "UINT32_MAX", it seems to work + // If i use the real value it's supposed to be, which is like, 99% of UINT32_MAX, it does not. + // It must be a bug with how emscripten or javascript handles extremely long timeout values. + // I think definitely, because JS uses a signed 32-bit integer for the delay, meaning it will do weird shit with these large values. + // + // We'll have to work around that limitation by chaining timeouts together and counting manually + // + // For now, just never send overflow events when running in the simulator. Still good for like, 49 days. + // + // uint32_t ticks_until_next_overflow = UINT32_MAX - timer_value; + // double msec_until_next_overflow_actual = ((double)ticks_until_next_overflow) / 1.024; + // double msec_until_next_overflow = OVERFLOW_MSECS / 2.0; + // // always set an overflow timeout + // // double msec_until_overflow = (double)(UINT32_MAX - timer_value) / 1.024; + // printf("msec until overflow fake: %f\r\n", msec_until_next_overflow); + // printf("msec until overflow calc: %f\r\n", msec_until_next_overflow_actual); + // simhpt_overflow_timeout_handle = emscripten_set_timeout(&cb_overflow, 10.0*60.0*1000.0, "enable"); + + // see if/when we need to in voke the callback timeout + if (simhpt_callback_enabled) + { + double sec_until_callback = (double)(simhpt_callback_timestamp - timer_value) / 1024.0; + if (sec_until_callback < 0) + { + // roll it forward one overflow amount + sec_until_callback += ((double)UINT32_MAX) / 1024.0; + } + simhpt_compare_timeout_handle = emscripten_set_timeout(&cb_compare, sec_until_callback * 1000.0, 0); + } + } +} +void watch_hpt_disable(void) +{ + if (simhpt_running) + { +#ifdef HPT_DEBUG + printf("hpt disabled\r\n"); +#endif + if (simhpt_overflow_timeout_handle) + { + emscripten_clear_timeout(simhpt_overflow_timeout_handle); + } + if (simhpt_compare_timeout_handle) + { + emscripten_clear_timeout(simhpt_compare_timeout_handle); + } + + double msec_active_for = emscripten_performance_now() - simhpt_enabled_timestamp; + double ticks_active_for = msec_active_for * 1.024; + simhpt_paused_count += ticks_active_for; + simhpt_running = false; + } +} + +uint32_t watch_hpt_get(void) +{ + if (simhpt_running) + { + double msec = emscripten_performance_now() - simhpt_enabled_timestamp; + double ticks = msec * 1.024; + uint64_t accumulated_ticks = ((uint64_t)ticks) + simhpt_paused_count; +#ifdef HPT_DEBUG + printf("hpt-get-running: %d\r\n", accumulated_ticks); +#endif + return accumulated_ticks; + } + else + { +#ifdef HPT_DEBUG + printf("hpt-get-paused: %d\r\n", simhpt_paused_count); +#endif + return simhpt_paused_count; + } +} + +uint32_t watch_hpt_get_fast(void) +{ + return watch_hpt_get(); +} +void watch_hpt_schedule_callback(uint32_t timestamp) +{ + #ifdef HPT_DEBUG + printf("hpt-schedule: %" PRIu32 "\r\n", timestamp); + #endif + + cb_compare_updated_timeout = true; + + uint32_t current_time = watch_hpt_get(); + double seconds_until_callback = 0.0; + if (timestamp > current_time) + { + // no problem, schedule as normal + seconds_until_callback = ((double)(timestamp - current_time)) / 1024.0; + } + else + { + // compare will still occur, but it will be next time around + seconds_until_callback = ((double)(timestamp - current_time) + (OVERFLOW_MSECS / 1000.0)) / 1024.0; + } + + // save these values to they can pause and resume the timer and the callbacks will be re-scheduled. + simhpt_callback_enabled = true; + simhpt_callback_timestamp = timestamp; + + emscripten_clear_timeout(simhpt_compare_timeout_handle); + emscripten_set_timeout(&cb_compare, seconds_until_callback * 1000.0, 0); +} +void watch_hpt_disable_scheduled_callback(void) +{ + cb_compare_updated_timeout = true; + + simhpt_callback_enabled = false; + emscripten_clear_timeout(simhpt_compare_timeout_handle); +}