From 81d04bf6302937004db17e4bcee69733644cbcdf Mon Sep 17 00:00:00 2001 From: Zach Miller Date: Tue, 12 Mar 2024 18:51:45 -0400 Subject: [PATCH 01/15] HPT: basic implementation without low-level support --- movement/movement.c | 88 +++++++++++++++++++++++- movement/movement.h | 83 ++++++++++++++++++++++ watch-library/hardware/watch/watch_hpt.h | 41 +++++++++++ 3 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 watch-library/hardware/watch/watch_hpt.h diff --git a/movement/movement.c b/movement/movement.c index d780a2f37..24859e920 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,15 @@ movement_state_t movement_state; void * watch_face_contexts[MOVEMENT_NUM_FACES]; watch_date_time scheduled_tasks[MOVEMENT_NUM_FACES]; + +// bit flags indicating which watch face wants the HPT enabled. +// make sure this number width is larger than the number of faces +#define HPT_REQUESTS_T uint16_t +HPT_REQUESTS_T hpt_enable_requests; + +// timestamps at which watch faces should have a HPT event triggered. UINT32_MAX for no callback +uint32_t hpt_scheduled_events[MOVEMENT_NUM_FACES]; + const int32_t movement_le_inactivity_deadlines[8] = {INT_MAX, 600, 3600, 7200, 21600, 43200, 86400, 604800}; const int16_t movement_timeout_inactivity_deadlines[4] = {60, 120, 300, 1800}; movement_event_t event; @@ -393,8 +404,10 @@ 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] = UINT32_MAX; } + hpt_enable_requests = 0; + is_first_launch = false; // set up the 1 minute alarm (for background tasks and low power updates) watch_date_time alarm_time; @@ -691,3 +704,76 @@ void cb_tick(void) { movement_state.subsecond++; } } + +void movement_hpt_request() { + movement_hpt_request_face(movement_state.current_face_idx); +} +void movement_hpt_request_face(uint8_t face_idx) { + HPT_REQUESTS_T old_hpt_requests = hpt_enable_requests; + hpt_enable_requests |= (1 << face_idx); + if(old_hpt_requests != hpt_enable_requests) { + watch_hpt_enable(); + } +} + +void movement_hpt_relenquish() { + movement_hpt_relenquish_face(movement_state.current_face_idx); +} +void movement_hpt_relenquish_face(uint8_t face_idx) { + // cancel this face's background task if one was scheduled + hpt_scheduled_events[face_idx] = UINT32_MAX; + hpt_enable_requests = ~(~hpt_enable_requests | (1 << face_idx)); + if(hpt_enable_requests == 0) { + watch_hpt_disable(); + } +} + +void cb_hpt() { + // execute callbacks for any faces that have background tasks scheduled + + bool task_triggered = false; + // loop through faces like this because it may be possible for a face to schedule *another* background task for itself to call, or it may be possible that by the time a face has finished handling its own background task, another face's task is ready to be triggered. + uint32_t next_scheduled_event; + do { + next_scheduled_event = UINT32_MAX; + for(uint8_t face_idx = 0; face_idx < MOVEMENT_NUM_FACES; ++face_idx) { + uint32_t face_scheduled_timestamp = hpt_scheduled_events[face_idx]; + if(face_scheduled_timestamp <= watch_hpt_get()) { + hpt_scheduled_events[face_idx] = UINT32_MAX; + // TODO trigger face with EVENT_HPT + task_triggered = true; + } else { + if(face_scheduled_timestamp < next_scheduled_event) { + next_scheduled_event = face_scheduled_timestamp; + } + } + } + } while(task_triggered); + + if(next_scheduled_event != UINT32_MAX) { + // another face has a task scheduled. + watch_hpt_register_callback(next_scheduled_event, &cb_hpt); + } +} + +void movement_hpt_schedule(uint32_t timestamp) { + movement_hpt_schedule_face(timestamp, movement_state.current_face_idx); +} +void movement_hpt_schedule_face(uint32_t timestamp, uint8_t face_idx) { + hpt_scheduled_events[face_idx] = timestamp; + uint32_t min_cb_timestamp = UINT32_MAX; + for(uint8_t idx = 0; idx < MOVEMENT_NUM_FACES; ++idx) { + uint32_t face_timestamp = hpt_scheduled_events[idx]; + if(min_cb_timestamp > face_timestamp) { + min_cb_timestamp = face_timestamp; + } + } + + if(min_cb_timestamp < UINT32_MAX) { + watch_hpt_register_callback(min_cb_timestamp, &cb_hpt); + } +} + +inline uint32_t movement_hpt_get() { + return watch_hpt_get(); +} \ No newline at end of file diff --git a/movement/movement.h b/movement/movement.h index 1dabfbc5b..d458b13e6 100644 --- a/movement/movement.h +++ b/movement/movement.h @@ -312,4 +312,87 @@ void movement_play_alarm_beeps(uint8_t rounds, BuzzerNote alarm_note); uint8_t movement_claim_backup_register(void); +/** + * The High Precision Timer (HPT) is a 32-bit counter ticking upward at 128hz. + * 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 wake the watch up from sleep. + * + * The HPT is disabled by default 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_relenquish". + * If no other face has an outstanding request for the HPT, it will be disabled + * to conserve power. + * + * Watch faces may not modify the value of the HPT counter in any way. The only + * guarantee to be made about the HPT timestamp is that between the time your + * face calls "movement_hpt_request" until the moment it calls + * "movement_hpt_relenquish", the value returned from "movement_hpt_get" will + * increment upwards at 128 hz. Outside of that 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_relenquish" 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 will need to use the "_face" variant of + * the HPT methods to specify that it is your face being called. +*/ + +/** + * 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_relenquish" +*/ +void movement_hpt_request(); +/** + * A variant of "movement_hpt_request" that can be used when your face is not + * running in the foreground. +*/ +void movement_hpt_request_face(uint8_t face_idx); + +/** + * Disables the HPT if no other faces are using it. This method must be called + * when your face no longer needs the timestamp provided by + * "movement_hpt_get" or has no scheduled background tasks, in order to save + * power. +*/ +void movement_hpt_relenquish(); +/** + * A variant of "movement_hpt_relenquish" that can be used when your face is + * not running in the foreground. +*/ +void movement_hpt_relenquish_face(uint8_t face_idx); + +/** + * Schedules a future EVENT_HPT event to occur on or after the given timestamp. + * The Movement framework will do its best to trigger the event as close to the + * timestamp as possible, but it may be delayed if multiple faces or events are + * scheduled to occur on the same timestamp. + * + * Faces should avoid scheduling background events too close to or before the + * current timestamp, or the event may be missed. +*/ +void movement_hpt_schedule(uint32_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(uint32_t timestamp, uint8_t face_idx); + +/** + * Returns the current timestamp of the high-precision timer, in 1/128ths of a + * second. + * + * Before using this timestamp, your face must request that the HPT be + * activated using "movement_hpt_request". +*/ +uint32_t movement_hpt_get(); + #endif // MOVEMENT_H_ diff --git a/watch-library/hardware/watch/watch_hpt.h b/watch-library/hardware/watch/watch_hpt.h new file mode 100644 index 000000000..d77fa26f4 --- /dev/null +++ b/watch-library/hardware/watch/watch_hpt.h @@ -0,0 +1,41 @@ +#ifndef WATCH_HPT_H__ +#define WATCH_HPT_H__ + +#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. +*/ + +/** + * Enables the high-precision timer and resets its value to zero +*/ +void watch_hpt_enable(); + +/** + * Stops the high-precision timer and powers it down. +*/ +void watch_hpt_disable(); + +/** + * Returns the current timetamp of the high-precision timer. +*/ +uint32_t watch_hpt_get(); + +/** + * Registers the given callback function to be invoked at the given timestamp value. + * + * Replaces any previously registered callback function. +*/ +void watch_hpt_register_callback(uint32_t timestamp, void (*callback_function)()); + +/** + * Registers the given callback function to be invoked when the high-precision timer overflows. +*/ +void watch_hpt_register_overflow(void (*callback_function)()); + + +#endif \ No newline at end of file From 092a375f3c0342d837a6496fd835615427a97d37 Mon Sep 17 00:00:00 2001 From: Zach Miller Date: Wed, 13 Mar 2024 16:19:28 -0400 Subject: [PATCH 02/15] HPT: chrono face using high-precision timer --- .../complication/hpt_lapsplit_chrono_face.c | 278 ++++++++++++++++++ .../complication/hpt_lapsplit_chrono_face.h | 123 ++++++++ 2 files changed, 401 insertions(+) create mode 100644 movement/watch_faces/complication/hpt_lapsplit_chrono_face.c create mode 100644 movement/watch_faces/complication/hpt_lapsplit_chrono_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..a203bd3ce --- /dev/null +++ b/movement/watch_faces/complication/hpt_lapsplit_chrono_face.c @@ -0,0 +1,278 @@ +/* + * 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" + +// frequency rate of underlying timer (high precision timer) +#define LCF_SUBSECOND_RATE 128 +#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 + + uint32_t runningTime; + if (context->running == LCF_RUN_RUNNING) + { + runningTime = movement_hpt_get() - context->startTs; + } + else + { + runningTime = context->pausedTs; + } + uint32_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 + { + sprintf(buf, "%02d--LE", time_minutes); + } + watch_display_string(buf, 4); + + if (context->running == LCF_RUN_STOPPED) + { + watch_set_colon(); + } + else + { + if ((runningTime % LCF_SUBSECOND_RATE) < (LCF_SUBSECOND_RATE / 2)) + { + watch_set_colon(); + } + else + { + watch_clear_colon(); + } + } + + if (context->mode == LCF_MODE_LAP) + { + watch_set_indicator(WATCH_INDICATOR_LAP); + sprintf(buf, "%2d", context->laps); + watch_display_string(buf, 2); + } + else + { + watch_clear_indicator(WATCH_INDICATOR_LAP); + if (time_hours == 0) + { + watch_display_string(" ", 2); + } + else if (time_hours > 39) + { + watch_display_string(" E", 2); + } + else + { + sprintf(buf, "%2d", time_hours); + watch_display_string(buf, 2); + } + } +} + +static void startButton(hpt_lapsplit_chrono_state_t *state) +{ + if (state->display == LCF_DISPLAY_SPLIT) + { + // if the duration is being displayed, clear it when you press "light" again + state->display = LCF_DISPLAY_TIME; + return; + } + + if (state->running == LCF_RUN_STOPPED) + { + // // reset timestamp based on held duration and start timer + // state->running = LCF_RUN_RUNNING; + // state->startTs = resetBackwards(now, state->duration); + // state->duration = 0; + // state->display = LCF_DISPLAY_TIME; + + // reset timer to zero + if (state->pausedTs != 0 || state->laps != 0) + { + state->pausedTs = 0; + state->laps = 0; + } + else + { + // if already reset, toggle lap/split mode + state->mode = state->mode == LCF_MODE_LAP ? LCF_MODE_SPLIT : LCF_MODE_LAP; + } + } + else + { + // record duration and show + uint32_t now = movement_hpt_get(); + state->splitTs = now - state->startTs; + 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 stopButton(hpt_lapsplit_chrono_state_t *state) +{ + if (state->running == LCF_RUN_RUNNING) + { + // if running stop the timer and record its duration + uint32_t now = movement_hpt_get(); + state->running = LCF_RUN_STOPPED; + state->pausedTs = now - state->startTs; + movement_hpt_relenquish(); + } + else + { + // restart the timer + movement_hpt_request(); + uint32_t now = movement_hpt_get(); + state->running = LCF_RUN_RUNNING; + state->startTs = now - state->pausedTs; + } +} + +void 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)); + // Do any one-time tasks in here; the inside of this conditional happens only at boot. + } + // Do any pin or peripheral setup here; this will be called whenever the watch wakes from deep sleep. +} + +void 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; + + // always run this face at a high tick frequency so we can capture sub-second button presses for more accurate time + movement_request_tick_frequency(LCF_DISPLAY_UPDATE_RATE); + + watch_display_string("CH", 0); +} + +bool 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; + + // handle_button_lights(event, settings, wdt_now); + + // only use the subsecond info from "tick" events. Subseconds from buttons or other event types seem unreliable + // (Maybe they're actually 128hz subseconds?) + + uint32_t now = movement_hpt_get(); + + switch (event.event_type) + { + case EVENT_LIGHT_BUTTON_DOWN: + startButton(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: + stopButton(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: + 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 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..485bec0de --- /dev/null +++ b/movement/watch_faces/complication/hpt_lapsplit_chrono_face.h @@ -0,0 +1,123 @@ +/* + * 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_LAPSPLIT_CHRONO_FACE_H_ +#define HPT_LAPSPLIT_CHRONO_FACE_H_ + +#include "movement.h" + +/* + * A lap/split chronograph accurate to approximately hundreths of a second (technically 1/64 of a second). + * + * Display: + * The chronograph face will display CH in the day-of-week digits to indicate the mode. + * The chronograph time will be displayed in the primary digits in MM:SS HH 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 + * + * Caveats: + * - The chronograph makes frequent use of "unix time" to store timestamps. As this build uses a 32-bit number + * for unix timestamps, it may not work correctly after 2038. + * - As far as I can tell, this watch does not handle leap seconds, but if it did, that could also mess with the + * behavior of the chronograph + * - The display shows hundreths of a second, but the actual resolution is only 1/64ths of a second. If Movement + * let me use the full 128hz tick rate, then we could actually get accuracy down to the hundreths. + */ + +#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 "duration" +#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; + + // 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 + uint32_t startTs; + + // duration recorded time when chronograph is paused, in 1/128ths of a second + uint32_t pausedTs : 32; + + + // duration of lap/split time + // no actual reason for this to be :31 other than it matches pausedTs being :31. + uint32_t splitTs : 32; +} 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_ From 757957ef585f9639ccb52ffbb2a1d5cfec92d0c8 Mon Sep 17 00:00:00 2001 From: Zach Miller Date: Wed, 13 Mar 2024 16:38:09 -0400 Subject: [PATCH 03/15] HPT: add stuff to makefiles and update watch_hpt.h a bit --- make.mk | 1 + movement/make/Makefile | 1 + movement/movement.c | 49 ++++++++++++------- movement/movement.h | 6 +-- movement/movement_faces.h | 1 + .../complication/hpt_lapsplit_chrono_face.c | 8 +-- .../complication/hpt_lapsplit_chrono_face.h | 2 + watch-library/hardware/watch/watch_hpt.h | 40 ++++++++++++--- 8 files changed, 76 insertions(+), 32 deletions(-) diff --git a/make.mk b/make.mk index 955ea3102..757587924 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 \ diff --git a/movement/make/Makefile b/movement/make/Makefile index 42dfc644d..deef427ea 100644 --- a/movement/make/Makefile +++ b/movement/make/Makefile @@ -126,6 +126,7 @@ 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 \ # 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 24859e920..b1d7483c2 100644 --- a/movement/movement.c +++ b/movement/movement.c @@ -705,7 +705,7 @@ void cb_tick(void) { } } -void movement_hpt_request() { +void movement_hpt_request(void) { movement_hpt_request_face(movement_state.current_face_idx); } void movement_hpt_request_face(uint8_t face_idx) { @@ -716,7 +716,7 @@ void movement_hpt_request_face(uint8_t face_idx) { } } -void movement_hpt_relenquish() { +void movement_hpt_relenquish(void) { movement_hpt_relenquish_face(movement_state.current_face_idx); } void movement_hpt_relenquish_face(uint8_t face_idx) { @@ -728,31 +728,44 @@ void movement_hpt_relenquish_face(uint8_t face_idx) { } } -void cb_hpt() { +const HPT_CALLBACK_CAUSE M_HPT_TRIGGERS = { false, true, false, 0}; + +void cb_hpt(HPT_CALLBACK_CAUSE cause) +{ // execute callbacks for any faces that have background tasks scheduled bool task_triggered = false; // loop through faces like this because it may be possible for a face to schedule *another* background task for itself to call, or it may be possible that by the time a face has finished handling its own background task, another face's task is ready to be triggered. uint32_t next_scheduled_event; - do { + do + { next_scheduled_event = UINT32_MAX; - for(uint8_t face_idx = 0; face_idx < MOVEMENT_NUM_FACES; ++face_idx) { - uint32_t face_scheduled_timestamp = hpt_scheduled_events[face_idx]; - if(face_scheduled_timestamp <= watch_hpt_get()) { - hpt_scheduled_events[face_idx] = UINT32_MAX; - // TODO trigger face with EVENT_HPT - task_triggered = true; - } else { - if(face_scheduled_timestamp < next_scheduled_event) { - next_scheduled_event = face_scheduled_timestamp; + for (uint8_t face_idx = 0; face_idx < MOVEMENT_NUM_FACES; ++face_idx) + { + uint32_t face_scheduled_timestamp = hpt_scheduled_events[face_idx]; + if (face_scheduled_timestamp <= watch_hpt_get()) + { + hpt_scheduled_events[face_idx] = UINT32_MAX; + // TODO trigger face with EVENT_HPT + task_triggered = true; + } + else + { + if (face_scheduled_timestamp < next_scheduled_event) + { + next_scheduled_event = face_scheduled_timestamp; + } } } - } - } while(task_triggered); + } while (task_triggered); - if(next_scheduled_event != UINT32_MAX) { + if (next_scheduled_event != UINT32_MAX) + { // another face has a task scheduled. - watch_hpt_register_callback(next_scheduled_event, &cb_hpt); + watch_hpt_register_callback(next_scheduled_event, &cb_hpt, M_HPT_TRIGGERS); + } else { + // disable callback + watch_hpt_register_callback(0,0,M_HPT_TRIGGERS); } } @@ -770,7 +783,7 @@ void movement_hpt_schedule_face(uint32_t timestamp, uint8_t face_idx) { } if(min_cb_timestamp < UINT32_MAX) { - watch_hpt_register_callback(min_cb_timestamp, &cb_hpt); + watch_hpt_register_callback(min_cb_timestamp, &cb_hpt, M_HPT_TRIGGERS); } } diff --git a/movement/movement.h b/movement/movement.h index d458b13e6..df7e8066a 100644 --- a/movement/movement.h +++ b/movement/movement.h @@ -350,7 +350,7 @@ uint8_t movement_claim_backup_register(void); * "movement_hpt_get" or "movement_hpt_schedule". The HPT will remain running * in the background until it is released using "movement_hpt_relenquish" */ -void movement_hpt_request(); +void movement_hpt_request(void); /** * A variant of "movement_hpt_request" that can be used when your face is not * running in the foreground. @@ -363,7 +363,7 @@ void movement_hpt_request_face(uint8_t face_idx); * "movement_hpt_get" or has no scheduled background tasks, in order to save * power. */ -void movement_hpt_relenquish(); +void movement_hpt_relenquish(void); /** * A variant of "movement_hpt_relenquish" that can be used when your face is * not running in the foreground. @@ -393,6 +393,6 @@ void movement_hpt_schedule_face(uint32_t timestamp, uint8_t face_idx); * Before using this timestamp, your face must request that the HPT be * activated using "movement_hpt_request". */ -uint32_t movement_hpt_get(); +uint32_t movement_hpt_get(void); #endif // MOVEMENT_H_ diff --git a/movement/movement_faces.h b/movement/movement_faces.h index 7feb0f408..2b5bd954b 100644 --- a/movement/movement_faces.h +++ b/movement/movement_faces.h @@ -103,6 +103,7 @@ #include "minute_repeater_decimal_face.h" #include "tuning_tones_face.h" #include "kitchen_conversions_face.h" +#include "hpt_lapsplit_chrono_face.h" // New includes go above this line. #endif // MOVEMENT_FACES_H_ diff --git a/movement/watch_faces/complication/hpt_lapsplit_chrono_face.c b/movement/watch_faces/complication/hpt_lapsplit_chrono_face.c index a203bd3ce..f25171e7f 100644 --- a/movement/watch_faces/complication/hpt_lapsplit_chrono_face.c +++ b/movement/watch_faces/complication/hpt_lapsplit_chrono_face.c @@ -182,7 +182,7 @@ static void stopButton(hpt_lapsplit_chrono_state_t *state) } } -void lapsplit_chrono_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void **context_ptr) +void hpt_lapsplit_chrono_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void **context_ptr) { (void)settings; (void)watch_face_index; @@ -195,7 +195,7 @@ void lapsplit_chrono_face_setup(movement_settings_t *settings, uint8_t watch_fac // Do any pin or peripheral setup here; this will be called whenever the watch wakes from deep sleep. } -void lapsplit_chrono_face_activate(movement_settings_t *settings, void *context) +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; @@ -208,7 +208,7 @@ void lapsplit_chrono_face_activate(movement_settings_t *settings, void *context) watch_display_string("CH", 0); } -bool lapsplit_chrono_face_loop(movement_event_t event, movement_settings_t *settings, void *context) +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; @@ -267,7 +267,7 @@ bool lapsplit_chrono_face_loop(movement_event_t event, movement_settings_t *sett return true; } -void lapsplit_chrono_face_resign(movement_settings_t *settings, void *context) +void hpt_lapsplit_chrono_face_resign(movement_settings_t *settings, void *context) { (void)settings; (void)context; diff --git a/movement/watch_faces/complication/hpt_lapsplit_chrono_face.h b/movement/watch_faces/complication/hpt_lapsplit_chrono_face.h index 485bec0de..bf3e6c191 100644 --- a/movement/watch_faces/complication/hpt_lapsplit_chrono_face.h +++ b/movement/watch_faces/complication/hpt_lapsplit_chrono_face.h @@ -27,6 +27,8 @@ #include "movement.h" +// #include + /* * A lap/split chronograph accurate to approximately hundreths of a second (technically 1/64 of a second). * diff --git a/watch-library/hardware/watch/watch_hpt.h b/watch-library/hardware/watch/watch_hpt.h index d77fa26f4..cb111b6df 100644 --- a/watch-library/hardware/watch/watch_hpt.h +++ b/watch-library/hardware/watch/watch_hpt.h @@ -3,6 +3,27 @@ #include +/** + * Defines the reasons the HPT callback is being invoked. More than one flag may be set. +*/ +typedef struct { + /** + * The callback is being invoked because of an error in the comparison (?) + */ + bool cmp_error :1; + /** + * The callback is being invoked because the count is equal to the scheduled timestamp + */ + bool compare_match :1; + /** + * The callbac is being invoked because the counter overflowed and reset to zero. + */ + bool overflow :1; + + // not used + uint8_t _padding :5; +} HPT_CALLBACK_CAUSE; + /** * watch_hpt provides low-level access to the high-precision timer. * @@ -10,6 +31,13 @@ * "movement_hpt_*" faces in movement.h instead. */ +/** + * Performs one-time setup of the peripherals used by the high-precision timer. + * + * Does not enable the timer. +*/ +void watch_hpt_init(); + /** * Enables the high-precision timer and resets its value to zero */ @@ -28,14 +56,12 @@ uint32_t watch_hpt_get(); /** * Registers the given callback function to be invoked at the given timestamp value. * + * - timestamp: the counter value at which the callback should be triggered. + * - callback_function: a pointer to a callback function that should be invoked when a HPT event occurs. Pass null (0) to disable callbacks + * - enabled_triggers: a set of flags indicating which counter events should trigger the callback + * * Replaces any previously registered callback function. */ -void watch_hpt_register_callback(uint32_t timestamp, void (*callback_function)()); - -/** - * Registers the given callback function to be invoked when the high-precision timer overflows. -*/ -void watch_hpt_register_overflow(void (*callback_function)()); - +void watch_hpt_register_callback(uint32_t timestamp, void (*callback_function)(HPT_CALLBACK_CAUSE cause), HPT_CALLBACK_CAUSE enabled_triggers); #endif \ No newline at end of file From 0d51c44d1c5b5d4dad2039759938243ffe8145cd Mon Sep 17 00:00:00 2001 From: Zach Miller Date: Thu, 14 Mar 2024 12:17:38 -0400 Subject: [PATCH 04/15] HPT: switch to 1024hz timekeeping TODO: - Scheduling next HPT event - Carefully handling overflows - Low level timer stuff --- movement/movement.c | 115 +++++++++--------- movement/movement.h | 17 ++- .../complication/hpt_lapsplit_chrono_face.c | 20 +-- .../complication/hpt_lapsplit_chrono_face.h | 20 +-- watch-library/hardware/watch/watch_hpt.h | 21 ++-- 5 files changed, 88 insertions(+), 105 deletions(-) diff --git a/movement/movement.c b/movement/movement.c index b1d7483c2..8ffb3b89a 100644 --- a/movement/movement.c +++ b/movement/movement.c @@ -58,6 +58,10 @@ #include "movement_custom_signal_tunes.h" +// macros that are not width-dependent +#define SET_BIT(value, bit) value |= (1 << bit) +#define CLR_BIT(value, bit) value = (~(~(value) | (1 << bit))) + // Default to no secondary face behaviour. #ifndef MOVEMENT_SECONDARY_FACE_INDEX #define MOVEMENT_SECONDARY_FACE_INDEX 0 @@ -79,13 +83,18 @@ movement_state_t movement_state; void * watch_face_contexts[MOVEMENT_NUM_FACES]; watch_date_time scheduled_tasks[MOVEMENT_NUM_FACES]; +// HPT stuff + // bit flags indicating which watch face wants the HPT enabled. -// make sure this number width is larger than the number of faces +// TODO make sure this number width is larger than the number of faces #define HPT_REQUESTS_T uint16_t HPT_REQUESTS_T hpt_enable_requests; -// timestamps at which watch faces should have a HPT event triggered. UINT32_MAX for no callback -uint32_t hpt_scheduled_events[MOVEMENT_NUM_FACES]; +// timestamps at which watch faces should have a HPT event triggered. UINT64_MAX for no callback +uint64_t hpt_scheduled_events[MOVEMENT_NUM_FACES]; + +// the number of times the high-precision timer has overflowed +uint8_t hpt_overflows = 0; 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}; @@ -404,7 +413,7 @@ void app_setup(void) { for(uint8_t i = 0; i < MOVEMENT_NUM_FACES; i++) { watch_face_contexts[i] = NULL; scheduled_tasks[i].reg = 0; - hpt_scheduled_events[i] = UINT32_MAX; + hpt_scheduled_events[i] = UINT64_MAX; } hpt_enable_requests = 0; is_first_launch = false; @@ -705,13 +714,37 @@ void cb_tick(void) { } } +/** 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 < MOVEMENT_NUM_FACES; ++req_idx) { + uint64_t event = hpt_scheduled_events[req_idx]; + if(event < next) { + event = next; + } + } + + // if an event is scheduled and the timer is currently tracking this overflow cycle + if(next != UINT64_MAX && (next >> 32) == hpt_overflows) { + watch_hpt_schedule_callback((uint32_t)(next & UINT32_MAX)); + } 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) { HPT_REQUESTS_T old_hpt_requests = hpt_enable_requests; - hpt_enable_requests |= (1 << face_idx); - if(old_hpt_requests != hpt_enable_requests) { + SET_BIT(hpt_enable_requests, face_idx); + if(old_hpt_requests == 0) { watch_hpt_enable(); } } @@ -721,72 +754,40 @@ void movement_hpt_relenquish(void) { } void movement_hpt_relenquish_face(uint8_t face_idx) { // cancel this face's background task if one was scheduled - hpt_scheduled_events[face_idx] = UINT32_MAX; - hpt_enable_requests = ~(~hpt_enable_requests | (1 << face_idx)); + hpt_scheduled_events[face_idx] = UINT64_MAX; + + // release the request this face had on the HPT + CLR_BIT(hpt_enable_requests, face_idx); if(hpt_enable_requests == 0) { watch_hpt_disable(); } } -const HPT_CALLBACK_CAUSE M_HPT_TRIGGERS = { false, true, false, 0}; - void cb_hpt(HPT_CALLBACK_CAUSE cause) { + if(cause.overflow) { + hpt_overflows++; + } + uint64_t now = movement_hpt_get(); + // execute callbacks for any faces that have background tasks scheduled - bool task_triggered = false; - // loop through faces like this because it may be possible for a face to schedule *another* background task for itself to call, or it may be possible that by the time a face has finished handling its own background task, another face's task is ready to be triggered. - uint32_t next_scheduled_event; - do - { - next_scheduled_event = UINT32_MAX; - for (uint8_t face_idx = 0; face_idx < MOVEMENT_NUM_FACES; ++face_idx) - { - uint32_t face_scheduled_timestamp = hpt_scheduled_events[face_idx]; - if (face_scheduled_timestamp <= watch_hpt_get()) - { - hpt_scheduled_events[face_idx] = UINT32_MAX; - // TODO trigger face with EVENT_HPT - task_triggered = true; - } - else - { - if (face_scheduled_timestamp < next_scheduled_event) - { - next_scheduled_event = face_scheduled_timestamp; - } - } - } - } while (task_triggered); + // TODO redo this to handle 64-bit counts etc - if (next_scheduled_event != UINT32_MAX) - { - // another face has a task scheduled. - watch_hpt_register_callback(next_scheduled_event, &cb_hpt, M_HPT_TRIGGERS); - } else { - // disable callback - watch_hpt_register_callback(0,0,M_HPT_TRIGGERS); - } + _movement_hpt_schedule_next_event(); } -void movement_hpt_schedule(uint32_t timestamp) { +void movement_hpt_schedule(uint64_t timestamp) { movement_hpt_schedule_face(timestamp, movement_state.current_face_idx); } -void movement_hpt_schedule_face(uint32_t timestamp, uint8_t face_idx) { +void movement_hpt_schedule_face(uint64_t timestamp, uint8_t face_idx) { hpt_scheduled_events[face_idx] = timestamp; - uint32_t min_cb_timestamp = UINT32_MAX; - for(uint8_t idx = 0; idx < MOVEMENT_NUM_FACES; ++idx) { - uint32_t face_timestamp = hpt_scheduled_events[idx]; - if(min_cb_timestamp > face_timestamp) { - min_cb_timestamp = face_timestamp; - } - } - - if(min_cb_timestamp < UINT32_MAX) { - watch_hpt_register_callback(min_cb_timestamp, &cb_hpt, M_HPT_TRIGGERS); - } + _movement_hpt_schedule_next_event(); } -inline uint32_t movement_hpt_get() { - return watch_hpt_get(); +inline uint64_t movement_hpt_get() { + uint64_t timestamp = hpt_overflows; + timestamp <<= 32; + timestamp |= (uint64_t)watch_hpt_get(); + return timestamp; } \ No newline at end of file diff --git a/movement/movement.h b/movement/movement.h index df7e8066a..82857c187 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 { @@ -313,7 +314,7 @@ void movement_play_alarm_beeps(uint8_t rounds, BuzzerNote alarm_note); uint8_t movement_claim_backup_register(void); /** - * The High Precision Timer (HPT) is a 32-bit counter ticking upward at 128hz. + * 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 @@ -333,7 +334,7 @@ uint8_t movement_claim_backup_register(void); * guarantee to be made about the HPT timestamp is that between the time your * face calls "movement_hpt_request" until the moment it calls * "movement_hpt_relenquish", the value returned from "movement_hpt_get" will - * increment upwards at 128 hz. Outside of that window, the timestamp value may + * increment upwards at 1024hz. Outside of that window, the timestamp value may * change unpredictably. * * Faces may schedule an EVENT_HPT event to occur by calling @@ -375,24 +376,22 @@ void movement_hpt_relenquish_face(uint8_t face_idx); * The Movement framework will do its best to trigger the event as close to the * timestamp as possible, but it may be delayed if multiple faces or events are * scheduled to occur on the same timestamp. - * - * Faces should avoid scheduling background events too close to or before the - * current timestamp, or the event may be missed. */ -void movement_hpt_schedule(uint32_t timestamp); +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(uint32_t timestamp, uint8_t face_idx); +void movement_hpt_schedule_face(uint64_t timestamp, uint8_t face_idx); /** - * Returns the current timestamp of the high-precision timer, in 1/128ths of a + * 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". */ -uint32_t movement_hpt_get(void); +uint64_t movement_hpt_get(void); #endif // MOVEMENT_H_ diff --git a/movement/watch_faces/complication/hpt_lapsplit_chrono_face.c b/movement/watch_faces/complication/hpt_lapsplit_chrono_face.c index f25171e7f..65cca2951 100644 --- a/movement/watch_faces/complication/hpt_lapsplit_chrono_face.c +++ b/movement/watch_faces/complication/hpt_lapsplit_chrono_face.c @@ -27,7 +27,7 @@ #include "hpt_lapsplit_chrono_face.h" // frequency rate of underlying timer (high precision timer) -#define LCF_SUBSECOND_RATE 128 +#define LCF_SUBSECOND_RATE 1024 #define LCF_DISPLAY_UPDATE_RATE 16 static void render(hpt_lapsplit_chrono_state_t *context, bool lowEnergyUpdate) @@ -37,7 +37,7 @@ static void render(hpt_lapsplit_chrono_state_t *context, bool lowEnergyUpdate) // show LAP if in lap mode, not if in split mode // rest is pretty obvious - uint32_t runningTime; + uint64_t runningTime; if (context->running == LCF_RUN_RUNNING) { runningTime = movement_hpt_get() - context->startTs; @@ -46,7 +46,7 @@ static void render(hpt_lapsplit_chrono_state_t *context, bool lowEnergyUpdate) { runningTime = context->pausedTs; } - uint32_t showTime = context->display == LCF_DISPLAY_SPLIT ? context->splitTs : runningTime; + 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; @@ -142,7 +142,7 @@ static void startButton(hpt_lapsplit_chrono_state_t *state) else { // record duration and show - uint32_t now = movement_hpt_get(); + uint64_t now = movement_hpt_get(); state->splitTs = now - state->startTs; state->display = LCF_DISPLAY_SPLIT; @@ -167,7 +167,7 @@ static void stopButton(hpt_lapsplit_chrono_state_t *state) if (state->running == LCF_RUN_RUNNING) { // if running stop the timer and record its duration - uint32_t now = movement_hpt_get(); + uint64_t now = movement_hpt_get(); state->running = LCF_RUN_STOPPED; state->pausedTs = now - state->startTs; movement_hpt_relenquish(); @@ -176,7 +176,7 @@ static void stopButton(hpt_lapsplit_chrono_state_t *state) { // restart the timer movement_hpt_request(); - uint32_t now = movement_hpt_get(); + uint64_t now = movement_hpt_get(); state->running = LCF_RUN_RUNNING; state->startTs = now - state->pausedTs; } @@ -210,16 +210,8 @@ void hpt_lapsplit_chrono_face_activate(movement_settings_t *settings, void *cont 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; - // handle_button_lights(event, settings, wdt_now); - - // only use the subsecond info from "tick" events. Subseconds from buttons or other event types seem unreliable - // (Maybe they're actually 128hz subseconds?) - - uint32_t now = movement_hpt_get(); - switch (event.event_type) { case EVENT_LIGHT_BUTTON_DOWN: diff --git a/movement/watch_faces/complication/hpt_lapsplit_chrono_face.h b/movement/watch_faces/complication/hpt_lapsplit_chrono_face.h index bf3e6c191..ee301af54 100644 --- a/movement/watch_faces/complication/hpt_lapsplit_chrono_face.h +++ b/movement/watch_faces/complication/hpt_lapsplit_chrono_face.h @@ -30,7 +30,7 @@ // #include /* - * A lap/split chronograph accurate to approximately hundreths of a second (technically 1/64 of a second). + * 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. @@ -58,14 +58,6 @@ * 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 - * - * Caveats: - * - The chronograph makes frequent use of "unix time" to store timestamps. As this build uses a 32-bit number - * for unix timestamps, it may not work correctly after 2038. - * - As far as I can tell, this watch does not handle leap seconds, but if it did, that could also mess with the - * behavior of the chronograph - * - The display shows hundreths of a second, but the actual resolution is only 1/64ths of a second. If Movement - * let me use the full 128hz tick rate, then we could actually get accuracy down to the hundreths. */ #define LCF_MODE_LAP 1 @@ -98,15 +90,13 @@ typedef struct uint8_t _padding2 :2; // when running, start timestamp is the zero index from which the timer is running from - uint32_t startTs; - - // duration recorded time when chronograph is paused, in 1/128ths of a second - uint32_t pausedTs : 32; + uint64_t startTs; + // duration recorded time when chronograph is paused, in 1/1024ths of a second + uint64_t pausedTs; // duration of lap/split time - // no actual reason for this to be :31 other than it matches pausedTs being :31. - uint32_t splitTs : 32; + 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); diff --git a/watch-library/hardware/watch/watch_hpt.h b/watch-library/hardware/watch/watch_hpt.h index cb111b6df..4406a5888 100644 --- a/watch-library/hardware/watch/watch_hpt.h +++ b/watch-library/hardware/watch/watch_hpt.h @@ -34,12 +34,14 @@ typedef struct { /** * Performs one-time setup of the peripherals used by the high-precision timer. * + * - callback_function: a function that should be invoked when the timer overflows or reaches a scheduled event. + * * Does not enable the timer. */ -void watch_hpt_init(); +void watch_hpt_init(void (*callback_function)(HPT_CALLBACK_CAUSE cause)); /** - * Enables the high-precision timer and resets its value to zero + * Enables the high-precision timer */ void watch_hpt_enable(); @@ -54,14 +56,13 @@ void watch_hpt_disable(); uint32_t watch_hpt_get(); /** - * Registers the given callback function to be invoked at the given timestamp value. - * - * - timestamp: the counter value at which the callback should be triggered. - * - callback_function: a pointer to a callback function that should be invoked when a HPT event occurs. Pass null (0) to disable callbacks - * - enabled_triggers: a set of flags indicating which counter events should trigger the callback - * - * Replaces any previously registered callback function. + * 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 the scheduled callback. */ -void watch_hpt_register_callback(uint32_t timestamp, void (*callback_function)(HPT_CALLBACK_CAUSE cause), HPT_CALLBACK_CAUSE enabled_triggers); +void watch_hpt_disable_scheduled_callback(); #endif \ No newline at end of file From d6f9e992c9f296af0c53cb34f00f1a131ad2c5be Mon Sep 17 00:00:00 2001 From: Zach Miller Date: Fri, 15 Mar 2024 15:04:23 -0400 Subject: [PATCH 05/15] HPT: more complete implementation in movement.c and some psuedocode for watch_hpt.c --- movement/movement.c | 117 ++++++++++++++---- watch-library/hardware/watch/watch_hpt.c | 107 ++++++++++++++++ .../{hardware => shared}/watch/watch_hpt.h | 16 ++- 3 files changed, 209 insertions(+), 31 deletions(-) create mode 100644 watch-library/hardware/watch/watch_hpt.c rename watch-library/{hardware => shared}/watch/watch_hpt.h (84%) diff --git a/movement/movement.c b/movement/movement.c index 8ffb3b89a..508a9db81 100644 --- a/movement/movement.c +++ b/movement/movement.c @@ -721,73 +721,146 @@ void cb_tick(void) { * - an event is triggered * - the timer overflows */ -static void _movement_hpt_schedule_next_event() { +static void _movement_hpt_schedule_next_event() +{ uint64_t next = UINT64_MAX; - for(uint8_t req_idx = 0; req_idx < MOVEMENT_NUM_FACES; ++req_idx) { + for (uint8_t req_idx = 0; req_idx < MOVEMENT_NUM_FACES; ++req_idx) + { uint64_t event = hpt_scheduled_events[req_idx]; - if(event < next) { + if (event < next) + { event = next; } } // if an event is scheduled and the timer is currently tracking this overflow cycle - if(next != UINT64_MAX && (next >> 32) == hpt_overflows) { + if (next != UINT64_MAX && (next >> 32) == hpt_overflows) + { watch_hpt_schedule_callback((uint32_t)(next & UINT32_MAX)); - } else { + } + else + { watch_hpt_disable_scheduled_callback(); } } -void movement_hpt_request(void) { +void movement_hpt_request(void) +{ movement_hpt_request_face(movement_state.current_face_idx); } -void movement_hpt_request_face(uint8_t face_idx) { +void movement_hpt_request_face(uint8_t face_idx) +{ HPT_REQUESTS_T old_hpt_requests = hpt_enable_requests; SET_BIT(hpt_enable_requests, face_idx); - if(old_hpt_requests == 0) { + if (old_hpt_requests == 0) + { watch_hpt_enable(); } } -void movement_hpt_relenquish(void) { +void movement_hpt_relenquish(void) +{ movement_hpt_relenquish_face(movement_state.current_face_idx); } -void movement_hpt_relenquish_face(uint8_t face_idx) { +void movement_hpt_relenquish_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 CLR_BIT(hpt_enable_requests, face_idx); - if(hpt_enable_requests == 0) { + if (hpt_enable_requests == 0) + { watch_hpt_disable(); } } void cb_hpt(HPT_CALLBACK_CAUSE cause) { - if(cause.overflow) { + // This single interrupt vector must handle the overflow case and the match compare case + if (cause.overflow) + { hpt_overflows++; + // exit early if this is just an overflow + if (!(cause.compare_match)) + return; } - uint64_t now = movement_hpt_get(); - // execute callbacks for any faces that have background tasks scheduled + // 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 eventTriggered = false; + bool canSleep = true; + do + { + 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 < MOVEMENT_NUM_FACES; ++face_idx) + { + if (hpt_scheduled_events[face_idx] <= now) + { + // clear the scheduled event and allow the face to schedule a new one + hpt_scheduled_events[face_idx] = UINT64_MAX; - // TODO redo this to handle 64-bit counts etc + // 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]); + + eventTriggered = true; + } + } + // keep doing this until no more scheduled events need to be processed + } while (eventTriggered); _movement_hpt_schedule_next_event(); + if (canSleep) + { + // TODO put cpu to sleep? + } } -void movement_hpt_schedule(uint64_t timestamp) { +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) { +void movement_hpt_schedule_face(uint64_t timestamp, uint8_t face_idx) +{ hpt_scheduled_events[face_idx] = timestamp; _movement_hpt_schedule_next_event(); } -inline uint64_t movement_hpt_get() { - uint64_t timestamp = hpt_overflows; - timestamp <<= 32; - timestamp |= (uint64_t)watch_hpt_get(); - return timestamp; +inline uint64_t movement_hpt_get() +{ + // TODO: take care when combining this count - hpt may overflow in the middle here. + + 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; } \ No newline at end of file diff --git a/watch-library/hardware/watch/watch_hpt.c b/watch-library/hardware/watch/watch_hpt.c new file mode 100644 index 000000000..333403866 --- /dev/null +++ b/watch-library/hardware/watch/watch_hpt.c @@ -0,0 +1,107 @@ + +#include "watch_hpt.h" + + + +void (*hpt_isr_callback)(HPT_CALLBACK_CAUSE) = 0; + +void __TC2_ISR() { + // check flags + HPT_CALLBACK_CAUSE cause; + cause.overflow = false; // TC2.INTFLAG.OVF + cause.compare_match = false; // TC2.INTFLAG.MC0 + // clear interrupt flags + // TC2.INTFLAG = 0xFF + if(hpt_isr_callback != 0) { + (*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 + + // Setup generator 2 for 1024 output + // GENCTRL2: + // SRC = 0x4 - external 32k oscillator + // DIV = 5 - 32k/ (2^5) = 1k + // DIVSEL = 1 - divide clock by 2^DIV, 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 + // GENEN = 0 - don't enable it just yet (actually it might be okay to leave it enabled all the time. Hopefully there is no warm-up time) + + // 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 + + // Configure TC2 to count up to MAX and generate appropriate interrupts, but don't turn it on + + // TC2.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.COUNT = 0 // just clear it to be safe + // TC2.CC0 = 0 + + // TC2.INTENSET: enabling interrupts + // OVF = 1 - always enable overflow interrupt +} + +void watch_hpt_enable(void) { + // enable clock generator + // GENCTRL2.GENEN = 1 + + // start timer + // TC2.ENABLE = 1 + + // Don't wait for enable here, it can be waited for on other operations that need it + +} + +void watch_hpt_disable(void) { + // stop timer + // TC2.ENABLE = 0 + + // stop clock generator + // GENCTRL2.GENEN = 0 +} + +void watch_hpt_schedule_callback(uint32_t timestamp) { + // set compare channel + // TC2.CC0 = timestamp + // enable interrupt + // TC2.INTENSET.MC0 = 1 +} + +void watch_hpt_disable_scheduled_callback(void) { + // disable interrupt + // TC2.INTENCLR.MC0 = 1 +} + +uint32_t watch_hpt_get(void) { + // synchronize a read of the count value + // TC2.CTRLBSET.CMD = 0x4 - force READSYNC + + // wait for synchronization to complete. Also wait for timer to be enabled if it was *just* turned on + // while(TC2.SYNCBUSY.COUNT == 1 || TC2.SYNCBUSY.ENABLE == 1); + + // return TC2.COUNT; + return 0; +} \ No newline at end of file diff --git a/watch-library/hardware/watch/watch_hpt.h b/watch-library/shared/watch/watch_hpt.h similarity index 84% rename from watch-library/hardware/watch/watch_hpt.h rename to watch-library/shared/watch/watch_hpt.h index 4406a5888..1243caeae 100644 --- a/watch-library/hardware/watch/watch_hpt.h +++ b/watch-library/shared/watch/watch_hpt.h @@ -2,15 +2,13 @@ #define WATCH_HPT_H__ #include +#include /** * Defines the reasons the HPT callback is being invoked. More than one flag may be set. */ typedef struct { - /** - * The callback is being invoked because of an error in the comparison (?) - */ - bool cmp_error :1; + /** * The callback is being invoked because the count is equal to the scheduled timestamp */ @@ -21,7 +19,7 @@ typedef struct { bool overflow :1; // not used - uint8_t _padding :5; + uint8_t _padding :6; } HPT_CALLBACK_CAUSE; /** @@ -43,17 +41,17 @@ void watch_hpt_init(void (*callback_function)(HPT_CALLBACK_CAUSE cause)); /** * Enables the high-precision timer */ -void watch_hpt_enable(); +void watch_hpt_enable(void); /** * Stops the high-precision timer and powers it down. */ -void watch_hpt_disable(); +void watch_hpt_disable(void); /** * Returns the current timetamp of the high-precision timer. */ -uint32_t watch_hpt_get(); +uint32_t watch_hpt_get(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. @@ -63,6 +61,6 @@ void watch_hpt_schedule_callback(uint32_t timestamp); /** * Disables the scheduled callback. */ -void watch_hpt_disable_scheduled_callback(); +void watch_hpt_disable_scheduled_callback(void); #endif \ No newline at end of file From ae5b46617d79807b45ea8d241981d80799fedb36 Mon Sep 17 00:00:00 2001 From: Zach Miller Date: Sat, 16 Mar 2024 15:02:27 -0400 Subject: [PATCH 06/15] HPT: now working on hardware Still some questions left in the code, and I had to disable the stock stopwatch and dual timer faces. Once I am sure this works properly, I'll update those faces to use it. --- movement/make/Makefile | 2 +- movement/movement.c | 52 ++- movement/movement_config.h | 6 +- movement/movement_faces.h | 2 +- .../watch_faces/clock/hpt_led_test_face.c | 140 ++++++++ .../watch_faces/clock/hpt_led_test_face.h | 57 +++ .../complication/dual_timer_face.c | 10 +- .../complication/dual_timer_face.h | 2 +- .../complication/stock_stopwatch_face.c | 324 ------------------ .../complication/stock_stopwatch_face.h | 80 ----- watch-library/hardware/watch/watch_hpt.c | 150 ++++++-- watch-library/shared/watch/watch_hpt.h | 2 + 12 files changed, 364 insertions(+), 463 deletions(-) create mode 100644 movement/watch_faces/clock/hpt_led_test_face.c create mode 100644 movement/watch_faces/clock/hpt_led_test_face.h delete mode 100644 movement/watch_faces/complication/stock_stopwatch_face.c delete mode 100644 movement/watch_faces/complication/stock_stopwatch_face.h diff --git a/movement/make/Makefile b/movement/make/Makefile index deef427ea..a1600280a 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 \ @@ -127,6 +126,7 @@ SRCS += \ ../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 \ # 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 508a9db81..5bdb6d2bd 100644 --- a/movement/movement.c +++ b/movement/movement.c @@ -88,13 +88,13 @@ watch_date_time scheduled_tasks[MOVEMENT_NUM_FACES]; // bit flags indicating which watch face wants the HPT enabled. // TODO make sure this number width is larger than the number of faces #define HPT_REQUESTS_T uint16_t -HPT_REQUESTS_T hpt_enable_requests; +volatile HPT_REQUESTS_T hpt_enable_requests; // timestamps at which watch faces should have a HPT event triggered. UINT64_MAX for no callback -uint64_t hpt_scheduled_events[MOVEMENT_NUM_FACES]; +volatile uint64_t hpt_scheduled_events[MOVEMENT_NUM_FACES]; // the number of times the high-precision timer has overflowed -uint8_t hpt_overflows = 0; +volatile uint8_t hpt_overflows = 0; 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}; @@ -154,6 +154,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]; @@ -197,13 +198,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++; } } @@ -416,7 +420,9 @@ void app_setup(void) { hpt_scheduled_events[i] = UINT64_MAX; } hpt_enable_requests = 0; + hpt_overflows = 0; is_first_launch = false; + watch_hpt_init(&cb_hpt); // set up the 1 minute alarm (for background tasks and low power updates) watch_date_time alarm_time; @@ -729,14 +735,15 @@ static void _movement_hpt_schedule_next_event() uint64_t event = hpt_scheduled_events[req_idx]; if (event < next) { - 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) + if ((next != UINT64_MAX) && ((next >> 32) == hpt_overflows)) { - watch_hpt_schedule_callback((uint32_t)(next & UINT32_MAX)); + uint32_t low_part = next; + watch_hpt_schedule_callback(low_part); } else { @@ -752,6 +759,7 @@ void movement_hpt_request_face(uint8_t face_idx) { HPT_REQUESTS_T old_hpt_requests = hpt_enable_requests; SET_BIT(hpt_enable_requests, face_idx); + if (old_hpt_requests == 0) { watch_hpt_enable(); @@ -781,26 +789,30 @@ void cb_hpt(HPT_CALLBACK_CAUSE cause) if (cause.overflow) { hpt_overflows++; - // exit early if this is just an overflow - if (!(cause.compare_match)) - return; } // 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 eventTriggered = false; bool canSleep = true; - do + + 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 < MOVEMENT_NUM_FACES; ++face_idx) { - if (hpt_scheduled_events[face_idx] <= now) + uint64_t face_time = hpt_scheduled_events[face_idx]; + //printf("face: %d, ts: %" PRIu64 "\r\n", face_idx, face_time); + if (face_time <= now) { + watch_set_led_yellow(); // clear the scheduled event and allow the face to schedule a new one hpt_scheduled_events[face_idx] = UINT64_MAX; @@ -817,7 +829,11 @@ void cb_hpt(HPT_CALLBACK_CAUSE cause) } } // keep doing this until no more scheduled events need to be processed - } while (eventTriggered); + if (!eventTriggered) + { + break; + } + } _movement_hpt_schedule_next_event(); if (canSleep) @@ -836,10 +852,8 @@ void movement_hpt_schedule_face(uint64_t timestamp, uint8_t face_idx) _movement_hpt_schedule_next_event(); } -inline uint64_t movement_hpt_get() +uint64_t movement_hpt_get() { - // TODO: take care when combining this count - hpt may overflow in the middle here. - uint64_t time; while (true) { diff --git a/movement/movement_config.h b/movement/movement_config.h index 067ca44b2..038963f31 100644 --- a/movement/movement_config.h +++ b/movement/movement_config.h @@ -28,11 +28,7 @@ #include "movement_faces.h" const watch_face_t watch_faces[] = { - simple_clock_face, - world_clock_face, - sunrise_sunset_face, - moon_phase_face, - stopwatch_face, + hpt_led_test_face, preferences_face, set_time_face, thermistor_readout_face, diff --git a/movement/movement_faces.h b/movement/movement_faces.h index 2b5bd954b..fa6cf4a8c 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" @@ -104,6 +103,7 @@ #include "tuning_tones_face.h" #include "kitchen_conversions_face.h" #include "hpt_lapsplit_chrono_face.h" +#include "hpt_led_test_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..5531679b3 --- /dev/null +++ b/movement/watch_faces/clock/hpt_led_test_face.c @@ -0,0 +1,140 @@ +/* + * 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 + +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->leds_off = 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_hpt_request_face(state->face_idx); + + movement_request_tick_frequency(2); + //movement_hpt_schedule_face(movement_hpt_get() + 2048, state->face_idx); + + + // Handle any tasks related to your watch face coming on screen. +} + +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_DOWN: + state->leds_off = true; + + movement_hpt_schedule(movement_hpt_get() + 2048); + break; + case EVENT_TICK: + //If needed, update your display here. + printf("ft:%" PRIu64 "\r\n", movement_hpt_get()); + + if (state->leds_off == true) + { + watch_set_led_off(); + } + else + { + if (((movement_hpt_get() / 512) % 2) == 0) + { + watch_set_led_green(); + } + else + { + watch_set_led_red(); + } + } + break; + case EVENT_HPT: + state->leds_off = false; + printf("fe:%" PRIu64 "\r\n", movement_hpt_get()); + break; + case EVENT_ALARM_BUTTON_UP: + // Just in case you have need for another button. + 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; + + // handle any cleanup before your watch face goes off-screen. + movement_hpt_relenquish_face(state->face_idx); +} 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..88e092156 --- /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 leds_off; +} 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/dual_timer_face.c b/movement/watch_faces/complication/dual_timer_face.c index f98c35b4b..03124d75e 100644 --- a/movement/watch_faces/complication/dual_timer_face.c +++ b/movement/watch_faces/complication/dual_timer_face.c @@ -109,11 +109,11 @@ static void _dual_timer_cb_initialize() { // 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; -} +// void TC2_Handler(void) { +// // interrupt handler for TC2 (globally!) +// _ticks++; +// TC2->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF; +// } #endif diff --git a/movement/watch_faces/complication/dual_timer_face.h b/movement/watch_faces/complication/dual_timer_face.h index d1ac79359..8ae198701 100644 --- a/movement/watch_faces/complication/dual_timer_face.h +++ b/movement/watch_faces/complication/dual_timer_face.h @@ -93,7 +93,7 @@ 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); +//void TC2_Handler(void); #endif #define dual_timer_face ((const watch_face_t){ \ diff --git a/movement/watch_faces/complication/stock_stopwatch_face.c b/movement/watch_faces/complication/stock_stopwatch_face.c deleted file mode 100644 index 4a9608d91..000000000 --- a/movement/watch_faces/complication/stock_stopwatch_face.c +++ /dev/null @@ -1,324 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2022 Andreas Nebinger - * - * 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 "stock_stopwatch_face.h" -#include "watch.h" -#include "watch_utility.h" -#include "watch_rtc.h" - -/* - This watch face implements the original F-91W stopwatch functionality - including counting hundredths of seconds and lap timing. There are two - improvements compared to the functionality of the original: - 1. When reaching 59:59 the counter does not simply jump back to zero, - but keeps track of hours in the upper right hand corner. (Up to 24h) - 2. Long pressing the light button toggles the led behaviour: it either - turns on on each button press or it doesn't. -*/ - -#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} -}; - -static uint32_t _ticks; -static uint32_t _lap_ticks; -static uint8_t _blink_ticks; -static uint32_t _old_seconds; -static uint8_t _old_minutes; -static uint8_t _hours; -static bool _colon; -static bool _is_running; - -#if __EMSCRIPTEN__ - -static long _em_interval_id = 0; - -void em_cb_handler(void *userData) { - // interrupt handler for emscripten 128 Hz callbacks - (void) userData; - _ticks++; -} - -static void _cb_initialize() { } - -static inline void _cb_stop() { - emscripten_clear_interval(_em_interval_id); - _em_interval_id = 0; - _is_running = false; -} - -static inline void _cb_start() { - // initiate 128 hz callback - _em_interval_id = emscripten_set_interval(em_cb_handler, (double)(1000/128), (void *)NULL); -} - -#else - -static inline void _cb_start() { - // start the 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; -} - -#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); -} - -/// @brief Display minutes, seconds and fractions derived from 128 Hz tick counter -/// on the lcd. -/// @param ticks -static void _display_ticks(uint32_t ticks) { - char buf[14]; - uint8_t sec_100 = (ticks & 0x7F) * 100 / 128; - uint32_t seconds = ticks >> 7; - uint32_t minutes = seconds / 60; - if (_hours) - sprintf(buf, "%2u%02lu%02lu%02u", _hours, minutes, (seconds % 60), sec_100); - else - sprintf(buf, " %02lu%02lu%02u", minutes, (seconds % 60), sec_100); - watch_display_string(buf, 2); -} - -/// @brief Displays the current stopwatch time on the LCD (more optimized than _display_ticks()) -static void _draw() { - if (_lap_ticks == 0) { - char buf[14]; - uint8_t sec_100 = (_ticks & 0x7F) * 100 / 128; - if (_is_running) { - uint32_t seconds = _ticks >> 7; - if (seconds != _old_seconds) { - // seconds have changed - _old_seconds = seconds; - uint8_t minutes = seconds / 60; - seconds %= 60; - if (minutes != _old_minutes) { - // minutes have changed, draw everything - _old_minutes = minutes; - minutes %= 60; - if (_hours) - // with hour indicator - sprintf(buf, "%2u%02u%02lu%02u", _hours, minutes, seconds, sec_100); - else - // no hour indicator - sprintf(buf, " %02u%02lu%02u", minutes, seconds, sec_100); - watch_display_string(buf, 2); - } else { - // just draw seconds - sprintf(buf, "%02lu%02u", seconds, sec_100); - watch_display_string(buf, 6); - } - } else { - // only draw 100ths of seconds - sprintf(buf, "%02u", sec_100); - watch_display_string(buf, 8); - } - } else { - _display_ticks(_ticks); - } - } - if (_is_running) { - // blink the colon every half second - uint8_t blink_ticks = ((_ticks >> 6) & 1); - if (blink_ticks != _blink_ticks) { - _blink_ticks = blink_ticks; - _colon = !_colon; - if (_colon) watch_set_colon(); - else watch_clear_colon(); - } - } -} - -static inline void _update_lap_indicator() { - if (_lap_ticks) watch_set_indicator(WATCH_INDICATOR_LAP); - else watch_clear_indicator(WATCH_INDICATOR_LAP); -} - -static inline void _set_colon() { - watch_set_colon(); - _colon = true; -} - -void stock_stopwatch_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(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; - _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; - } - - switch (event.event_type) { - case EVENT_ACTIVATE: - _set_colon(); - watch_display_string("ST ", 0); - _update_lap_indicator(); - if (_is_running) movement_request_tick_frequency(16); - _display_ticks(_lap_ticks ? _lap_ticks : _ticks); - break; - case EVENT_TICK: - _draw(); - break; - case EVENT_LIGHT_LONG_PRESS: - // kind od hidden feature: long press toggles light on or off - state->light_on_button = !state->light_on_button; - if (state->light_on_button) movement_illuminate_led(); - else watch_set_led_off(); - break; - case EVENT_ALARM_BUTTON_DOWN: - _is_running = !_is_running; - if (_is_running) { - // start or continue stopwatch - movement_request_tick_frequency(16); - // register 128 hz callback for time measuring - _cb_start(); - // schedule the keepalive task when running - movement_schedule_background_task(distant_future); - } else { - // stop the stopwatch - _cb_stop(); - movement_request_tick_frequency(1); - _set_colon(); - // cancel the keepalive task - movement_cancel_background_task(); - } - _draw(); - _button_beep(settings); - break; - case EVENT_LIGHT_BUTTON_DOWN: - if (state->light_on_button) movement_illuminate_led(); - if (_is_running) { - if (_lap_ticks) { - // clear lap and continue running - _lap_ticks = 0; - movement_request_tick_frequency(16); - } else { - // set lap ticks and stop updating the display - _lap_ticks = _ticks; - movement_request_tick_frequency(2); - _set_colon(); - } - } else { - if (_lap_ticks) { - // clear lap and show running stopwatch - _lap_ticks = 0; - } else if (_ticks) { - // reset stopwatch - _ticks = _lap_ticks = _blink_ticks = _old_minutes = _old_seconds = _hours = 0; - _button_beep(settings); - } - } - _display_ticks(_ticks); - _update_lap_indicator(); - break; - case EVENT_TIMEOUT: - if (!_is_running) movement_move_to_face(0); - break; - case EVENT_LOW_ENERGY_UPDATE: - _draw(); - break; - default: - movement_default_loop_handler(event, settings); - break; - } - return true; -} - -void stock_stopwatch_face_resign(movement_settings_t *settings, void *context) { - (void) settings; - (void) context; - // cancel the keepalive task - movement_cancel_background_task(); -} diff --git a/movement/watch_faces/complication/stock_stopwatch_face.h b/movement/watch_faces/complication/stock_stopwatch_face.h deleted file mode 100644 index 6796a8499..000000000 --- a/movement/watch_faces/complication/stock_stopwatch_face.h +++ /dev/null @@ -1,80 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2022 Andreas Nebinger - * - * 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 STOCK_STOPWATCH_FACE_H_ -#define STOCK_STOPWATCH_FACE_H_ - -/* - * STOCK STOPWATCH face - * - * The Stock Stopwatch face implements the original F-91W stopwatch - * functionality, including counting hundredths of seconds and lap timing. - * - * Use the ALARM button to start and stop the stopwatch. - * Press the LIGHT button while the stopwatch is running to view the lap time. - * (The stopwatch continues running in the background, indicated by a blinking colon.) - * Press the LIGHT button again to switch back to the running stopwatch. - * Press the LIGHT button when the timekeeping is stopped to reset the stopwatch. - * - * There are two improvements compared to the original F-91W: - * o When the stopwatch reaches 59:59, the counter does not simply jump back - * to zero but keeps track of hours in the upper right-hand corner - * (up to 24 hours). - * o Long-press the light button to toggle the LED behavior. - * It either turns on with each button press or remains off. - * - * NOTE: - * This watch face relies heavily on static vars in stock_stopwatch.c. - * The disadvantage is that you cannot use more than one instance of this - * watch face on your custom firmware - but then again, who would want that? - * The advantage is that accessing vars is more direct and faster, and we - * can save some precious cpu cycles. :-) - */ - -#include "movement.h" - -typedef struct { - bool light_on_button; // determines whether the light button actually triggers the led -} stock_stopwatch_state_t; - -void stock_stopwatch_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); -void stock_stopwatch_face_activate(movement_settings_t *settings, void *context); -bool stock_stopwatch_face_loop(movement_event_t event, movement_settings_t *settings, void *context); -void stock_stopwatch_face_resign(movement_settings_t *settings, void *context); - -#if __EMSCRIPTEN__ -void em_cb_handler(void *userData); -#else -void TC2_Handler(void); -#endif - -#define stock_stopwatch_face ((const watch_face_t){ \ - stock_stopwatch_face_setup, \ - stock_stopwatch_face_activate, \ - stock_stopwatch_face_loop, \ - stock_stopwatch_face_resign, \ - NULL, \ -}) - -#endif // STOCK_STOPWATCH_FACE_H_ diff --git a/watch-library/hardware/watch/watch_hpt.c b/watch-library/hardware/watch/watch_hpt.c index 333403866..59a576b06 100644 --- a/watch-library/hardware/watch/watch_hpt.c +++ b/watch-library/hardware/watch/watch_hpt.c @@ -1,39 +1,55 @@ #include "watch_hpt.h" +#include "parts.h" - +// user HPT callback void (*hpt_isr_callback)(HPT_CALLBACK_CAUSE) = 0; -void __TC2_ISR() { + +// actual HPT callback +void TC2_Handler(void) +{ // check flags HPT_CALLBACK_CAUSE cause; - cause.overflow = false; // TC2.INTFLAG.OVF - cause.compare_match = false; // TC2.INTFLAG.MC0 + 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 - if(hpt_isr_callback != 0) { + // silly that you have to write ones these flags to clear them + TC2->COUNT32.INTFLAG.reg = 0xFF; + + if (hpt_isr_callback != 0) + { (*hpt_isr_callback)(cause); } } -void watch_hpt_init(void (*callback_function)(HPT_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 = 5 - 32k/ (2^5) = 1k - // DIVSEL = 1 - divide clock by 2^DIV, not just by DIV. (of course, 32 could easily fit in DIV, but it's a power of two, why not) + // 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 - // GENEN = 0 - don't enable it just yet (actually it might be okay to leave it enabled all the time. Hopefully there is no warm-up time) + // 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 @@ -42,6 +58,12 @@ void watch_hpt_init(void (*callback_function)(HPT_CALLBACK_CAUSE)) { // 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 // TC2.CTRLA: @@ -56,52 +78,126 @@ void watch_hpt_init(void (*callback_function)(HPT_CALLBACK_CAUSE)) { // 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 + TC_CRITICAL_SECTION_ENTER(); + + // reset counter + TC2->COUNT32.CTRLA.bit.SWRST = 1; + while(TC2->COUNT32.SYNCBUSY.bit.SWRST != 0); + + TC2->COUNT32.CTRLA.reg = + TC_CTRLA_ONDEMAND | + TC_CTRLA_RUNSTDBY | + TC_CTRLA_MODE_COUNT32; // TC2.COUNT = 0 // just clear it to be safe // TC2.CC0 = 0 + TC2->COUNT32.COUNT.bit.COUNT = 0; + while(TC2->COUNT32.SYNCBUSY.bit.COUNT != 0); + // TC2->COUNT32.CC[0].bit.CC = UINT32_MAX -1; + // while(TC2->COUNT32.SYNCBUSY.bit.CC0 != 0); + + // enable TC2 interrupt + + NVIC_DisableIRQ(TC2_IRQn); + // TC2.INTENSET: enabling interrupts // OVF = 1 - always enable overflow interrupt -} + // disable compare match interrupt to start + TC2->COUNT32.INTENCLR.bit.MC0 = 1; + TC2->COUNT32.INTENSET.bit.OVF = 1; + + TC2->COUNT32.INTFLAG.reg = 0xFF; -void watch_hpt_enable(void) { - // enable clock generator - // GENCTRL2.GENEN = 1 + NVIC_ClearPendingIRQ(TC2_IRQn); + NVIC_EnableIRQ(TC2_IRQn); - // start timer - // TC2.ENABLE = 1 + TC_CRITICAL_SECTION_LEAVE(); - // Don't wait for enable here, it can be waited for on other operations that need it +} +void watch_hpt_enable(void) +{ + // start timer + // TC2.ENABLE = 1 + TC_CRITICAL_SECTION_ENTER(); + TC2->COUNT32.CTRLA.bit.ENABLE = 1; + while (TC2->COUNT32.SYNCBUSY.bit.ENABLE != 0) + ; + TC_CRITICAL_SECTION_LEAVE(); } -void watch_hpt_disable(void) { +void watch_hpt_disable(void) +{ // stop timer // TC2.ENABLE = 0 - - // stop clock generator - // GENCTRL2.GENEN = 0 + TC_CRITICAL_SECTION_ENTER(); + TC2->COUNT32.CTRLA.bit.ENABLE = 0; + while (TC2->COUNT32.SYNCBUSY.bit.ENABLE != 0) + asm(""); + TC_CRITICAL_SECTION_LEAVE(); } -void watch_hpt_schedule_callback(uint32_t timestamp) { +void watch_hpt_schedule_callback(uint32_t timestamp) +{ // set compare channel // TC2.CC0 = timestamp // enable interrupt // TC2.INTENSET.MC0 = 1 + + TC_CRITICAL_SECTION_ENTER(); + TC2->COUNT32.CC[0].reg = timestamp; + while(TC2->COUNT32.SYNCBUSY.bit.CC0) + asm(""); + TC2->COUNT32.INTFLAG.reg = 0xFF; + + TC2->COUNT32.INTENSET.bit.MC0 = 1; + TC_CRITICAL_SECTION_LEAVE(); } -void watch_hpt_disable_scheduled_callback(void) { +void watch_hpt_disable_scheduled_callback(void) +{ // disable interrupt // TC2.INTENCLR.MC0 = 1 + TC_CRITICAL_SECTION_ENTER(); + TC2->COUNT32.INTENCLR.bit.MC0 = 1; + TC_CRITICAL_SECTION_LEAVE(); } -uint32_t watch_hpt_get(void) { +// 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 // TC2.CTRLBSET.CMD = 0x4 - force READSYNC + TC_CRITICAL_SECTION_ENTER(); + TC2->COUNT32.CTRLBSET.reg = TC_CTRLBSET_CMD_READSYNC; + + // wait for command to be executed + while(TC2->COUNT32.CTRLBSET.bit.CMD); + + // wait for sync to occur? + while (TC2->COUNT32.SYNCBUSY.bit.CTRLB); + // this might not be necessary? - // wait for synchronization to complete. Also wait for timer to be enabled if it was *just* turned on - // while(TC2.SYNCBUSY.COUNT == 1 || TC2.SYNCBUSY.ENABLE == 1); + // wait for count to be synchronized + while (TC2->COUNT32.SYNCBUSY.bit.COUNT); - // return TC2.COUNT; - return 0; + // finally safe to read count + 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/shared/watch/watch_hpt.h b/watch-library/shared/watch/watch_hpt.h index 1243caeae..e2d915cd3 100644 --- a/watch-library/shared/watch/watch_hpt.h +++ b/watch-library/shared/watch/watch_hpt.h @@ -63,4 +63,6 @@ void watch_hpt_schedule_callback(uint32_t timestamp); */ void watch_hpt_disable_scheduled_callback(void); +void TC2_Handler(void); + #endif \ No newline at end of file From eb966e40f94df2a654cd1617b1ba2ca0e5f984fa Mon Sep 17 00:00:00 2001 From: Zach Miller Date: Sat, 16 Mar 2024 20:04:10 -0400 Subject: [PATCH 07/15] HPT: Simulator support (mostly) and some fixes --- make.mk | 1 + movement/movement.c | 14 +- movement/movement.h | 14 +- movement/movement_config.h | 1 + .../watch_faces/clock/hpt_led_test_face.c | 19 +- .../complication/hpt_lapsplit_chrono_face.c | 18 +- watch-library/simulator/watch/watch_hpt.c | 189 ++++++++++++++++++ 7 files changed, 235 insertions(+), 21 deletions(-) create mode 100644 watch-library/simulator/watch/watch_hpt.c diff --git a/make.mk b/make.mk index 757587924..09a862f90 100644 --- a/make.mk +++ b/make.mk @@ -196,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/movement.c b/movement/movement.c index 5bdb6d2bd..72c8db727 100644 --- a/movement/movement.c +++ b/movement/movement.c @@ -24,6 +24,8 @@ #define MOVEMENT_LONG_PRESS_TICKS 64 +#define HPT_DEBUG + #include #include #include @@ -83,7 +85,7 @@ movement_state_t movement_state; void * watch_face_contexts[MOVEMENT_NUM_FACES]; watch_date_time scheduled_tasks[MOVEMENT_NUM_FACES]; -// HPT stuff +// high-precision timer stuff // bit flags indicating which watch face wants the HPT enabled. // TODO make sure this number width is larger than the number of faces @@ -766,11 +768,11 @@ void movement_hpt_request_face(uint8_t face_idx) } } -void movement_hpt_relenquish(void) +void movement_hpt_release(void) { - movement_hpt_relenquish_face(movement_state.current_face_idx); + movement_hpt_release_face(movement_state.current_face_idx); } -void movement_hpt_relenquish_face(uint8_t 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; @@ -863,6 +865,10 @@ uint64_t movement_hpt_get() // create a timestamp by combining overflow count time = (((uint64_t)hpt_overflows) << 32) | start; + #ifdef HPT_DEBUG + printf("movement-hpt-get: start=%d hpt_overflows=%d time=%" PRIu64, start, hpt_overflows, time); + #endif + // check to see if an overflow occurred while we were doing all that uint32_t end = watch_hpt_get(); if (end >= start) diff --git a/movement/movement.h b/movement/movement.h index 82857c187..1fd142f3c 100644 --- a/movement/movement.h +++ b/movement/movement.h @@ -326,21 +326,21 @@ uint8_t movement_claim_backup_register(void); * 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_relenquish". + * scheduled event provided by the HPT it MUST call "movement_hpt_release". * If no other face has an outstanding request for the HPT, it will be disabled * to conserve power. * * Watch faces may not modify the value of the HPT counter in any way. The only * guarantee to be made about the HPT timestamp is that between the time your * face calls "movement_hpt_request" until the moment it calls - * "movement_hpt_relenquish", the value returned from "movement_hpt_get" will + * "movement_hpt_release", the value returned from "movement_hpt_get" will * increment upwards at 1024hz. Outside of that 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_relenquish" until after the event has occurred. + * 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 will need to use the "_face" variant of * the HPT methods to specify that it is your face being called. @@ -349,7 +349,7 @@ uint8_t movement_claim_backup_register(void); /** * 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_relenquish" + * in the background until it is released using "movement_hpt_release" */ void movement_hpt_request(void); /** @@ -364,12 +364,12 @@ void movement_hpt_request_face(uint8_t face_idx); * "movement_hpt_get" or has no scheduled background tasks, in order to save * power. */ -void movement_hpt_relenquish(void); +void movement_hpt_release(void); /** - * A variant of "movement_hpt_relenquish" that can be used when your face is + * A variant of "movement_hpt_release" that can be used when your face is * not running in the foreground. */ -void movement_hpt_relenquish_face(uint8_t face_idx); +void movement_hpt_release_face(uint8_t face_idx); /** * Schedules a future EVENT_HPT event to occur on or after the given timestamp. diff --git a/movement/movement_config.h b/movement/movement_config.h index 038963f31..eabe1aeec 100644 --- a/movement/movement_config.h +++ b/movement/movement_config.h @@ -29,6 +29,7 @@ const watch_face_t watch_faces[] = { hpt_led_test_face, + hpt_lapsplit_chrono_face, preferences_face, set_time_face, thermistor_readout_face, diff --git a/movement/watch_faces/clock/hpt_led_test_face.c b/movement/watch_faces/clock/hpt_led_test_face.c index 5531679b3..9ccf0de23 100644 --- a/movement/watch_faces/clock/hpt_led_test_face.c +++ b/movement/watch_faces/clock/hpt_led_test_face.c @@ -51,13 +51,24 @@ void hpt_led_test_face_activate(movement_settings_t *settings, void *context) watch_enable_leds(); movement_hpt_request_face(state->face_idx); - movement_request_tick_frequency(2); + 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; @@ -76,7 +87,7 @@ bool hpt_led_test_face_loop(movement_event_t event, movement_settings_t *setting break; case EVENT_TICK: //If needed, update your display here. - printf("ft:%" PRIu64 "\r\n", movement_hpt_get()); + render_hpt_time(movement_hpt_get()); if (state->leds_off == true) { @@ -96,7 +107,7 @@ bool hpt_led_test_face_loop(movement_event_t event, movement_settings_t *setting break; case EVENT_HPT: state->leds_off = false; - printf("fe:%" PRIu64 "\r\n", movement_hpt_get()); + //printf("fe:%" PRIu64 "\r\n", movement_hpt_get()); break; case EVENT_ALARM_BUTTON_UP: // Just in case you have need for another button. @@ -136,5 +147,5 @@ void hpt_led_test_face_resign(movement_settings_t *settings, void *context) hpt_led_test_state_t *state = (hpt_led_test_state_t *)context; // handle any cleanup before your watch face goes off-screen. - movement_hpt_relenquish_face(state->face_idx); + movement_hpt_release_face(state->face_idx); } diff --git a/movement/watch_faces/complication/hpt_lapsplit_chrono_face.c b/movement/watch_faces/complication/hpt_lapsplit_chrono_face.c index 65cca2951..ecc8d6e77 100644 --- a/movement/watch_faces/complication/hpt_lapsplit_chrono_face.c +++ b/movement/watch_faces/complication/hpt_lapsplit_chrono_face.c @@ -25,6 +25,8 @@ #include #include #include "hpt_lapsplit_chrono_face.h" +#include +#include // frequency rate of underlying timer (high precision timer) #define LCF_SUBSECOND_RATE 1024 @@ -110,7 +112,7 @@ static void render(hpt_lapsplit_chrono_state_t *context, bool lowEnergyUpdate) } } -static void startButton(hpt_lapsplit_chrono_state_t *state) +static void splitButton(hpt_lapsplit_chrono_state_t *state) { if (state->display == LCF_DISPLAY_SPLIT) { @@ -162,7 +164,7 @@ static void startButton(hpt_lapsplit_chrono_state_t *state) } } -static void stopButton(hpt_lapsplit_chrono_state_t *state) +static void startStopButton(hpt_lapsplit_chrono_state_t *state) { if (state->running == LCF_RUN_RUNNING) { @@ -170,7 +172,9 @@ static void stopButton(hpt_lapsplit_chrono_state_t *state) uint64_t now = movement_hpt_get(); state->running = LCF_RUN_STOPPED; state->pausedTs = now - state->startTs; - movement_hpt_relenquish(); + movement_hpt_release(); + + //printf("ch-stopped: now=%" PRIu64 " pausedTs=%" PRIu64 " startTs=%" PRIu64 "\r\n", now, state->pausedTs, state->startTs); } else { @@ -179,6 +183,8 @@ static void stopButton(hpt_lapsplit_chrono_state_t *state) uint64_t now = movement_hpt_get(); state->running = LCF_RUN_RUNNING; state->startTs = now - state->pausedTs; + + //printf("ch-started: now=%" PRIu64 " pausedTs=%" PRIu64 " startTs=%" PRIu64 "\r\n", now, state->pausedTs, state->startTs); } } @@ -215,14 +221,14 @@ bool hpt_lapsplit_chrono_face_loop(movement_event_t event, movement_settings_t * switch (event.event_type) { case EVENT_LIGHT_BUTTON_DOWN: - startButton(state); + 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: - stopButton(state); + startStopButton(state); render(state, false); break; @@ -262,7 +268,7 @@ bool hpt_lapsplit_chrono_face_loop(movement_event_t event, movement_settings_t * void hpt_lapsplit_chrono_face_resign(movement_settings_t *settings, void *context) { (void)settings; - (void)context; + (void)context; // handle any cleanup before your watch face goes off-screen. // reset tick frequency diff --git a/watch-library/simulator/watch/watch_hpt.c b/watch-library/simulator/watch/watch_hpt.c new file mode 100644 index 000000000..0b8a9e88f --- /dev/null +++ b/watch-library/simulator/watch/watch_hpt.c @@ -0,0 +1,189 @@ +#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 + +static void cb_overflow(void *_unused) +{ + printf("hpt-isr-overflow cause=%s\r\n", (char*)_unused); + + // 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) +{ + printf("hpt-isr-compare\r\n"); + + // 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 = false; + cause.overflow = true; + 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) + { + printf("enabling hpt\r\n"); + simhpt_enabled_timestamp = emscripten_performance_now(); + simhpt_running = true; + + uint32_t timer_value = simhpt_paused_count; + printf("stored ticks: %d\r\n", timer_value); + + // 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) + { + printf("hpt disabled\r\n"); + 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; + + printf("hpt-get-running: %d\r\n", accumulated_ticks); + return accumulated_ticks; + } + else + { + + printf("hpt-get-paused: %d\r\n", simhpt_paused_count); + return simhpt_paused_count; + } +} +void watch_hpt_schedule_callback(uint32_t timestamp) +{ + 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); +} From bc284acbb7db618024ac40e564d56cac27a66038 Mon Sep 17 00:00:00 2001 From: Zach Miller Date: Sun, 17 Mar 2024 13:06:20 -0400 Subject: [PATCH 08/15] HPT: Update dual timer face to use HPT --- movement/make/Makefile | 1 + movement/movement.c | 2 +- movement/movement_config.h | 3 +- .../complication/dual_timer_face.c | 186 ++++-------------- .../complication/dual_timer_face.h | 16 +- watch-library/hardware/watch/watch_hpt.c | 9 +- watch-library/simulator/watch/watch_hpt.c | 37 ++-- 7 files changed, 71 insertions(+), 183 deletions(-) diff --git a/movement/make/Makefile b/movement/make/Makefile index a1600280a..4b29aa6ec 100644 --- a/movement/make/Makefile +++ b/movement/make/Makefile @@ -127,6 +127,7 @@ SRCS += \ ../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 \ # 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 72c8db727..c1df6fc61 100644 --- a/movement/movement.c +++ b/movement/movement.c @@ -24,7 +24,7 @@ #define MOVEMENT_LONG_PRESS_TICKS 64 -#define HPT_DEBUG +//#define HPT_DEBUG #include #include diff --git a/movement/movement_config.h b/movement/movement_config.h index eabe1aeec..e17e9cf2b 100644 --- a/movement/movement_config.h +++ b/movement/movement_config.h @@ -28,8 +28,9 @@ #include "movement_faces.h" const watch_face_t watch_faces[] = { - hpt_led_test_face, + //hpt_led_test_face, hpt_lapsplit_chrono_face, + dual_timer_face, preferences_face, set_time_face, thermistor_readout_face, diff --git a/movement/watch_faces/complication/dual_timer_face.c b/movement/watch_faces/complication/dual_timer_face.c index 03124d75e..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 8ae198701..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/watch-library/hardware/watch/watch_hpt.c b/watch-library/hardware/watch/watch_hpt.c index 59a576b06..443431d81 100644 --- a/watch-library/hardware/watch/watch_hpt.c +++ b/watch-library/hardware/watch/watch_hpt.c @@ -6,8 +6,7 @@ // user HPT callback void (*hpt_isr_callback)(HPT_CALLBACK_CAUSE) = 0; - -// actual HPT callback +// actual HPT ISR void TC2_Handler(void) { // check flags @@ -133,8 +132,7 @@ void watch_hpt_disable(void) // TC2.ENABLE = 0 TC_CRITICAL_SECTION_ENTER(); TC2->COUNT32.CTRLA.bit.ENABLE = 0; - while (TC2->COUNT32.SYNCBUSY.bit.ENABLE != 0) - asm(""); + while (TC2->COUNT32.SYNCBUSY.bit.ENABLE != 0); TC_CRITICAL_SECTION_LEAVE(); } @@ -147,8 +145,7 @@ void watch_hpt_schedule_callback(uint32_t timestamp) TC_CRITICAL_SECTION_ENTER(); TC2->COUNT32.CC[0].reg = timestamp; - while(TC2->COUNT32.SYNCBUSY.bit.CC0) - asm(""); + while(TC2->COUNT32.SYNCBUSY.bit.CC0); TC2->COUNT32.INTFLAG.reg = 0xFF; TC2->COUNT32.INTENSET.bit.MC0 = 1; diff --git a/watch-library/simulator/watch/watch_hpt.c b/watch-library/simulator/watch/watch_hpt.c index 0b8a9e88f..678fb708a 100644 --- a/watch-library/simulator/watch/watch_hpt.c +++ b/watch-library/simulator/watch/watch_hpt.c @@ -19,10 +19,13 @@ 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) { - printf("hpt-isr-overflow cause=%s\r\n", (char*)_unused); - +#ifdef HPT_DEBUG + printf("hpt-isr-overflow cause=%s\r\n", (char *)_unused); +#endif // fire off an interrupt if (simhpt_callback_function) { @@ -40,7 +43,9 @@ volatile bool cb_compare_updated_timeout = false; static void cb_compare(void *_unused) { - printf("hpt-isr-compare\r\n"); +#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 @@ -78,12 +83,16 @@ 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 @@ -97,7 +106,7 @@ void watch_hpt_enable(void) // // 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; + // 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); @@ -121,7 +130,9 @@ 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); @@ -142,17 +153,19 @@ 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; - - printf("hpt-get-running: %d\r\n", accumulated_ticks); - return accumulated_ticks; + 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 { - - printf("hpt-get-paused: %d\r\n", simhpt_paused_count); +#ifdef HPT_DEBUG + printf("hpt-get-paused: %d\r\n", simhpt_paused_count); +#endif return simhpt_paused_count; } } From aeee9a402915cfbfdd49bd32063f5b8ac162aef2 Mon Sep 17 00:00:00 2001 From: Zach Miller Date: Mon, 18 Mar 2024 12:25:11 -0400 Subject: [PATCH 09/15] HPT: add countdown face and some simulator fixes --- movement/make/Makefile | 1 + movement/movement.c | 10 +- movement/movement.h | 9 + movement/movement_config.h | 3 +- movement/movement_faces.h | 1 + .../complication/hpt_countdown_face.c | 418 ++++++++++++++++++ .../complication/hpt_countdown_face.h | 104 +++++ watch-library/simulator/watch/watch_hpt.c | 4 +- 8 files changed, 546 insertions(+), 4 deletions(-) create mode 100644 movement/watch_faces/complication/hpt_countdown_face.c create mode 100644 movement/watch_faces/complication/hpt_countdown_face.h diff --git a/movement/make/Makefile b/movement/make/Makefile index 4b29aa6ec..454875843 100644 --- a/movement/make/Makefile +++ b/movement/make/Makefile @@ -128,6 +128,7 @@ SRCS += \ ../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 \ # 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 c1df6fc61..1d4bdf402 100644 --- a/movement/movement.c +++ b/movement/movement.c @@ -814,7 +814,7 @@ void cb_hpt(HPT_CALLBACK_CAUSE cause) //printf("face: %d, ts: %" PRIu64 "\r\n", face_idx, face_time); if (face_time <= now) { - watch_set_led_yellow(); + //watch_set_led_yellow(); // clear the scheduled event and allow the face to schedule a new one hpt_scheduled_events[face_idx] = UINT64_MAX; @@ -854,6 +854,14 @@ void movement_hpt_schedule_face(uint64_t timestamp, uint8_t face_idx) _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; diff --git a/movement/movement.h b/movement/movement.h index 1fd142f3c..47463f11d 100644 --- a/movement/movement.h +++ b/movement/movement.h @@ -385,6 +385,15 @@ void movement_hpt_schedule(uint64_t timestamp); */ void movement_hpt_schedule_face(uint64_t timestamp, uint8_t face_idx); +/** + * Cancels any upcoming HPT events for the current face, if any. +*/ +void movement_hpt_cancel(void); +/** + * Cancels any upcoming 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. diff --git a/movement/movement_config.h b/movement/movement_config.h index e17e9cf2b..650f6d152 100644 --- a/movement/movement_config.h +++ b/movement/movement_config.h @@ -30,7 +30,8 @@ const watch_face_t watch_faces[] = { //hpt_led_test_face, hpt_lapsplit_chrono_face, - dual_timer_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 fa6cf4a8c..7e181f3a3 100644 --- a/movement/movement_faces.h +++ b/movement/movement_faces.h @@ -104,6 +104,7 @@ #include "kitchen_conversions_face.h" #include "hpt_lapsplit_chrono_face.h" #include "hpt_led_test_face.h" +#include "hpt_countdown_face.h" // New includes go above this line. #endif // MOVEMENT_FACES_H_ 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..3c810a0a4 --- /dev/null +++ b/movement/watch_faces/complication/hpt_countdown_face.c @@ -0,0 +1,418 @@ +/* + * 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" + +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); + } + + // for debugging + // if(state->last_subsecond == 1) { + // watch_set_indicator(WATCH_INDICATOR_PM); + // } else { + // watch_clear_indicator(WATCH_INDICATOR_PM); + // } + + 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(); + int64_t signed_target = state->target; + timeLeft = signed_target - now; + printf("t: now=%" PRId64 " target=%" PRId64 " left=%" PRId64 "\r\n", now, signed_target, timeLeft); + } + else + { + timeLeft = state->paused_ms_left; + printf("t: paused left=%" PRId64 "\r\n", timeLeft); + } + + + 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 to call pause_timer first! + + state->paused_ms_left = ((state->set_hours * 60 * 60) + + (state->set_minutes * 60) + + (state->set_seconds)) * 1024; + state->running = false; + state->repeat_count = 0; + state->setting_mode = 0; +} + +// starts or restarts 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; + // reset the target based on the number of seconds left when the timer was paused + state->target = now + state->paused_ms_left; + 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 (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(); + + // 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_face)); + 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; + + 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(8); + } +} + +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); + // TODO: 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..008155601 --- /dev/null +++ b/movement/watch_faces/complication/hpt_countdown_face.h @@ -0,0 +1,104 @@ +/* + * 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 + + // 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 :18; + + // 35 bits + + bool auto_repeat :1; + bool running :1; + + // 37 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; + + + // 43 bits + uint8_t padding :5; + + // 48 bits + + // the target timestamp we are counting down to + uint64_t target; + + 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/watch-library/simulator/watch/watch_hpt.c b/watch-library/simulator/watch/watch_hpt.c index 678fb708a..823c815e8 100644 --- a/watch-library/simulator/watch/watch_hpt.c +++ b/watch-library/simulator/watch/watch_hpt.c @@ -54,8 +54,8 @@ static void cb_compare(void *_unused) if (simhpt_callback_function) { HPT_CALLBACK_CAUSE cause; - cause.compare_match = false; - cause.overflow = true; + cause.compare_match = true; + cause.overflow = false; cause._padding = 0; (*simhpt_callback_function)(cause); } From b727cc2661090a7f14b41767786f817aded1c8ba Mon Sep 17 00:00:00 2001 From: Zach Miller Date: Mon, 18 Mar 2024 16:03:40 -0400 Subject: [PATCH 10/15] HPT: general cleanup - Removed some debugging cruft and commented out code - HPT requests are stored using a byte array instead of a single variable (allows for more than 64 HPT requests) - Added "fast" mode to counter reads that skips the synchronization step - Fixes and improvements to HPT timing faces. --- movement/movement.c | 89 +++++++++++++------ movement/movement.h | 15 ++++ .../complication/hpt_countdown_face.c | 67 ++++++++------ .../complication/hpt_lapsplit_chrono_face.c | 39 ++++---- .../complication/hpt_lapsplit_chrono_face.h | 6 +- watch-library/hardware/watch/watch_hpt.c | 44 +++++---- watch-library/shared/watch/watch_hpt.h | 7 ++ watch-library/simulator/watch/watch_hpt.c | 5 ++ 8 files changed, 178 insertions(+), 94 deletions(-) diff --git a/movement/movement.c b/movement/movement.c index 1d4bdf402..b881c9f53 100644 --- a/movement/movement.c +++ b/movement/movement.c @@ -24,8 +24,6 @@ #define MOVEMENT_LONG_PRESS_TICKS 64 -//#define HPT_DEBUG - #include #include #include @@ -60,10 +58,6 @@ #include "movement_custom_signal_tunes.h" -// macros that are not width-dependent -#define SET_BIT(value, bit) value |= (1 << bit) -#define CLR_BIT(value, bit) value = (~(~(value) | (1 << bit))) - // Default to no secondary face behaviour. #ifndef MOVEMENT_SECONDARY_FACE_INDEX #define MOVEMENT_SECONDARY_FACE_INDEX 0 @@ -85,19 +79,57 @@ movement_state_t movement_state; void * watch_face_contexts[MOVEMENT_NUM_FACES]; watch_date_time scheduled_tasks[MOVEMENT_NUM_FACES]; -// high-precision timer stuff -// bit flags indicating which watch face wants the HPT enabled. -// TODO make sure this number width is larger than the number of faces -#define HPT_REQUESTS_T uint16_t -volatile HPT_REQUESTS_T hpt_enable_requests; +// --- 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 + +// 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[MOVEMENT_NUM_FACES]; +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; @@ -412,6 +444,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 @@ -421,9 +455,12 @@ void app_setup(void) { scheduled_tasks[i].reg = 0; hpt_scheduled_events[i] = UINT64_MAX; } - hpt_enable_requests = 0; + + for(uint8_t i=0; i < HPT_REQUESTS_SIZE; ++i) { + hpt_requests[i] = 0; + } + hpt_overflows = 0; - is_first_launch = false; watch_hpt_init(&cb_hpt); // set up the 1 minute alarm (for background tasks and low power updates) @@ -759,10 +796,10 @@ void movement_hpt_request(void) } void movement_hpt_request_face(uint8_t face_idx) { - HPT_REQUESTS_T old_hpt_requests = hpt_enable_requests; - SET_BIT(hpt_enable_requests, face_idx); + bool hpt_was_enabled = hpt_any_requests(); + hpt_set_request(face_idx); - if (old_hpt_requests == 0) + if (!hpt_was_enabled) { watch_hpt_enable(); } @@ -778,13 +815,13 @@ void movement_hpt_release_face(uint8_t face_idx) hpt_scheduled_events[face_idx] = UINT64_MAX; // release the request this face had on the HPT - CLR_BIT(hpt_enable_requests, face_idx); - if (hpt_enable_requests == 0) - { + 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 @@ -811,16 +848,13 @@ void cb_hpt(HPT_CALLBACK_CAUSE cause) for (uint8_t face_idx = 0; face_idx < MOVEMENT_NUM_FACES; ++face_idx) { uint64_t face_time = hpt_scheduled_events[face_idx]; - //printf("face: %d, ts: %" PRIu64 "\r\n", face_idx, face_time); if (face_time <= now) { - //watch_set_led_yellow(); // clear the scheduled event and allow the face to schedule a new one hpt_scheduled_events[face_idx] = UINT64_MAX; // invoke the face watch_face_t face = watch_faces[face_idx]; - movement_event_t event; event.event_type = EVENT_HPT; event.subsecond = 0; @@ -873,10 +907,6 @@ uint64_t movement_hpt_get() // create a timestamp by combining overflow count time = (((uint64_t)hpt_overflows) << 32) | start; - #ifdef HPT_DEBUG - printf("movement-hpt-get: start=%d hpt_overflows=%d time=%" PRIu64, start, hpt_overflows, time); - #endif - // check to see if an overflow occurred while we were doing all that uint32_t end = watch_hpt_get(); if (end >= start) @@ -891,4 +921,9 @@ uint64_t movement_hpt_get() } 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(); } \ No newline at end of file diff --git a/movement/movement.h b/movement/movement.h index 47463f11d..ada29bf6d 100644 --- a/movement/movement.h +++ b/movement/movement.h @@ -403,4 +403,19 @@ void movement_hpt_cancel_face(uint8_t face_idx); */ 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 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/watch_faces/complication/hpt_countdown_face.c b/movement/watch_faces/complication/hpt_countdown_face.c index 3c810a0a4..2f9cd990c 100644 --- a/movement/watch_faces/complication/hpt_countdown_face.c +++ b/movement/watch_faces/complication/hpt_countdown_face.c @@ -27,9 +27,10 @@ #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) { @@ -40,13 +41,6 @@ static void render(hpt_countdown_state_t *state, bool blink) watch_clear_indicator(WATCH_INDICATOR_LAP); } - // for debugging - // if(state->last_subsecond == 1) { - // watch_set_indicator(WATCH_INDICATOR_PM); - // } else { - // watch_clear_indicator(WATCH_INDICATOR_PM); - // } - char buf[9]; if (state->setting_mode != 0) { @@ -81,17 +75,20 @@ static void render(hpt_countdown_state_t *state, bool blink) if (state->running) { // calculate time left until target time - int64_t now = movement_hpt_get(); + int64_t now = movement_hpt_get_fast(); int64_t signed_target = state->target; timeLeft = signed_target - now; - printf("t: now=%" PRId64 " target=%" PRId64 " left=%" PRId64 "\r\n", now, signed_target, timeLeft); } else { timeLeft = state->paused_ms_left; - printf("t: paused left=%" PRId64 "\r\n", timeLeft); } + // 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) @@ -101,6 +98,7 @@ static void render(hpt_countdown_state_t *state, bool blink) timeLeft = (30 * 60 * 60 * 1024 - 1); // throw away ms part + timeLeft /= 1024; uint8_t seconds = timeLeft % 60; @@ -142,25 +140,32 @@ static void render(hpt_countdown_state_t *state, bool blink) static void reset_timer(hpt_countdown_state_t *state) { - // make sure to call pause_timer first! + // 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->running = false; + (state->set_minutes * 60) + + (state->set_seconds)) * + 1024; state->repeat_count = 0; - state->setting_mode = 0; } -// starts or restarts a paused timer +// 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; - movement_hpt_schedule_face(state->target, state->watch_face_index); + + // 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 @@ -168,8 +173,9 @@ 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->set_minutes * 60) + + (state->set_seconds)) * + 1024; state->target += duration; movement_hpt_schedule_face(state->target, state->watch_face_index); } @@ -181,11 +187,11 @@ static void trigger_timer(hpt_countdown_state_t *state) // 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) @@ -194,6 +200,8 @@ static void pause_timer(hpt_countdown_state_t *state) // 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); @@ -205,7 +213,7 @@ void hpt_countdown_face_setup(movement_settings_t *settings, uint8_t watch_face_ (void)watch_face_index; if (*context_ptr == NULL) { - *context_ptr = malloc(sizeof(hpt_countdown_face)); + *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. @@ -216,6 +224,7 @@ void hpt_countdown_face_setup(movement_settings_t *settings, uint8_t watch_face_ state->set_seconds = 10; state->repeat_count = 0; state->watch_face_index = watch_face_index; + state->setting_mode = 0; reset_timer(state); } @@ -232,8 +241,9 @@ void hpt_countdown_face_activate(movement_settings_t *settings, void *context) 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(8); + if (state->running) + { + movement_request_tick_frequency(LCD_UPDATE_RATE_WHILE_RUNNING); } } @@ -289,7 +299,7 @@ bool hpt_countdown_face_loop(movement_event_t event, movement_settings_t *settin { hpt_countdown_state_t *state = (hpt_countdown_state_t *)context; - bool blink = event.subsecond == 1; + bool blink = !!(event.subsecond & 1); switch (event.event_type) { @@ -314,9 +324,10 @@ bool hpt_countdown_face_loop(movement_event_t event, movement_settings_t *settin { // 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; - + (state->set_minutes * 60) + + (state->set_seconds)) * + 1024; + if (state->paused_ms_left == expected_ticks_left) { // timer is already reset, enter settings mode diff --git a/movement/watch_faces/complication/hpt_lapsplit_chrono_face.c b/movement/watch_faces/complication/hpt_lapsplit_chrono_face.c index ecc8d6e77..315f16096 100644 --- a/movement/watch_faces/complication/hpt_lapsplit_chrono_face.c +++ b/movement/watch_faces/complication/hpt_lapsplit_chrono_face.c @@ -30,6 +30,7 @@ // 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) @@ -42,7 +43,8 @@ static void render(hpt_lapsplit_chrono_state_t *context, bool lowEnergyUpdate) uint64_t runningTime; if (context->running == LCF_RUN_RUNNING) { - runningTime = movement_hpt_get() - context->startTs; + // 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 { @@ -123,13 +125,7 @@ static void splitButton(hpt_lapsplit_chrono_state_t *state) if (state->running == LCF_RUN_STOPPED) { - // // reset timestamp based on held duration and start timer - // state->running = LCF_RUN_RUNNING; - // state->startTs = resetBackwards(now, state->duration); - // state->duration = 0; - // state->display = LCF_DISPLAY_TIME; - - // reset timer to zero + // 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; @@ -137,15 +133,17 @@ static void splitButton(hpt_lapsplit_chrono_state_t *state) } else { - // if already reset, toggle lap/split mode + // if already reset to zero, toggle lap/split mode state->mode = state->mode == LCF_MODE_LAP ? LCF_MODE_SPLIT : LCF_MODE_LAP; } } else { - // record duration and show + // 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) @@ -168,13 +166,14 @@ static void startStopButton(hpt_lapsplit_chrono_state_t *state) { if (state->running == LCF_RUN_RUNNING) { - // if running stop the timer and record its duration + // 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(); - //printf("ch-stopped: now=%" PRIu64 " pausedTs=%" PRIu64 " startTs=%" PRIu64 "\r\n", now, state->pausedTs, state->startTs); + // slow display back down because the time is paused + movement_request_tick_frequency(1); } else { @@ -184,7 +183,8 @@ static void startStopButton(hpt_lapsplit_chrono_state_t *state) state->running = LCF_RUN_RUNNING; state->startTs = now - state->pausedTs; - //printf("ch-started: now=%" PRIu64 " pausedTs=%" PRIu64 " startTs=%" PRIu64 "\r\n", now, state->pausedTs, state->startTs); + // increase display rate so it looks like the timer is running + movement_request_tick_frequency(LCF_DISPLAY_UPDATE_RATE); } } @@ -196,9 +196,7 @@ void hpt_lapsplit_chrono_face_setup(movement_settings_t *settings, uint8_t watch { *context_ptr = malloc(sizeof(hpt_lapsplit_chrono_state_t)); memset(*context_ptr, 0, sizeof(hpt_lapsplit_chrono_state_t)); - // Do any one-time tasks in here; the inside of this conditional happens only at boot. } - // Do any pin or peripheral setup here; this will be called whenever the watch wakes from deep sleep. } void hpt_lapsplit_chrono_face_activate(movement_settings_t *settings, void *context) @@ -208,8 +206,12 @@ void hpt_lapsplit_chrono_face_activate(movement_settings_t *settings, void *cont // always show the running time when switching to this face state->display = LCF_DISPLAY_TIME; - // always run this face at a high tick frequency so we can capture sub-second button presses for more accurate time - movement_request_tick_frequency(LCF_DISPLAY_UPDATE_RATE); + // 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); } @@ -231,15 +233,12 @@ bool hpt_lapsplit_chrono_face_loop(movement_event_t event, movement_settings_t * 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: if (state->running == LCF_RUN_STOPPED) diff --git a/movement/watch_faces/complication/hpt_lapsplit_chrono_face.h b/movement/watch_faces/complication/hpt_lapsplit_chrono_face.h index ee301af54..73882bc64 100644 --- a/movement/watch_faces/complication/hpt_lapsplit_chrono_face.h +++ b/movement/watch_faces/complication/hpt_lapsplit_chrono_face.h @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2024 <#author_name#> + * 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 @@ -27,8 +27,6 @@ #include "movement.h" -// #include - /* * A lap/split chronograph accurate to thousandths of a second (though only hundreths are displayed). * @@ -66,7 +64,7 @@ #define LCF_RUN_RUNNING 1 #define LCF_RUN_STOPPED 0 -// show the time based on "duration" +// 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 diff --git a/watch-library/hardware/watch/watch_hpt.c b/watch-library/hardware/watch/watch_hpt.c index 443431d81..c526152eb 100644 --- a/watch-library/hardware/watch/watch_hpt.c +++ b/watch-library/hardware/watch/watch_hpt.c @@ -81,7 +81,8 @@ void watch_hpt_init(void (*callback_function)(HPT_CALLBACK_CAUSE)) // reset counter TC2->COUNT32.CTRLA.bit.SWRST = 1; - while(TC2->COUNT32.SYNCBUSY.bit.SWRST != 0); + while (TC2->COUNT32.SYNCBUSY.bit.SWRST) + ; TC2->COUNT32.CTRLA.reg = TC_CTRLA_ONDEMAND | @@ -91,28 +92,27 @@ void watch_hpt_init(void (*callback_function)(HPT_CALLBACK_CAUSE)) // TC2.COUNT = 0 // just clear it to be safe // TC2.CC0 = 0 TC2->COUNT32.COUNT.bit.COUNT = 0; - while(TC2->COUNT32.SYNCBUSY.bit.COUNT != 0); - // TC2->COUNT32.CC[0].bit.CC = UINT32_MAX -1; - // while(TC2->COUNT32.SYNCBUSY.bit.CC0 != 0); + while (TC2->COUNT32.SYNCBUSY.bit.COUNT) + ; + // TC2->COUNT32.CC[0].bit.CC = UINT32_MAX -1; + // while(TC2->COUNT32.SYNCBUSY.bit.CC0 != 0); // enable TC2 interrupt NVIC_DisableIRQ(TC2_IRQn); - // TC2.INTENSET: enabling interrupts // OVF = 1 - always enable overflow interrupt // disable compare match interrupt to start TC2->COUNT32.INTENCLR.bit.MC0 = 1; TC2->COUNT32.INTENSET.bit.OVF = 1; - + TC2->COUNT32.INTFLAG.reg = 0xFF; NVIC_ClearPendingIRQ(TC2_IRQn); NVIC_EnableIRQ(TC2_IRQn); TC_CRITICAL_SECTION_LEAVE(); - } void watch_hpt_enable(void) @@ -121,7 +121,7 @@ void watch_hpt_enable(void) // TC2.ENABLE = 1 TC_CRITICAL_SECTION_ENTER(); TC2->COUNT32.CTRLA.bit.ENABLE = 1; - while (TC2->COUNT32.SYNCBUSY.bit.ENABLE != 0) + while (TC2->COUNT32.SYNCBUSY.bit.ENABLE) ; TC_CRITICAL_SECTION_LEAVE(); } @@ -132,7 +132,8 @@ void watch_hpt_disable(void) // TC2.ENABLE = 0 TC_CRITICAL_SECTION_ENTER(); TC2->COUNT32.CTRLA.bit.ENABLE = 0; - while (TC2->COUNT32.SYNCBUSY.bit.ENABLE != 0); + while (TC2->COUNT32.SYNCBUSY.bit.ENABLE) + ; TC_CRITICAL_SECTION_LEAVE(); } @@ -145,7 +146,8 @@ void watch_hpt_schedule_callback(uint32_t timestamp) TC_CRITICAL_SECTION_ENTER(); TC2->COUNT32.CC[0].reg = timestamp; - while(TC2->COUNT32.SYNCBUSY.bit.CC0); + while (TC2->COUNT32.SYNCBUSY.bit.CC0) + ; TC2->COUNT32.INTFLAG.reg = 0xFF; TC2->COUNT32.INTENSET.bit.MC0 = 1; @@ -171,7 +173,7 @@ void watch_hpt_disable_scheduled_callback(void) // 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 +// 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. @@ -184,17 +186,29 @@ uint32_t watch_hpt_get(void) TC2->COUNT32.CTRLBSET.reg = TC_CTRLBSET_CMD_READSYNC; // wait for command to be executed - while(TC2->COUNT32.CTRLBSET.bit.CMD); + while (TC2->COUNT32.CTRLBSET.bit.CMD) + ; // wait for sync to occur? - while (TC2->COUNT32.SYNCBUSY.bit.CTRLB); - // this might not be necessary? + while (TC2->COUNT32.SYNCBUSY.bit.CTRLB) + ; + // this might not be necessary? // wait for count to be synchronized - while (TC2->COUNT32.SYNCBUSY.bit.COUNT); + 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 + 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/shared/watch/watch_hpt.h b/watch-library/shared/watch/watch_hpt.h index e2d915cd3..125f7d0fb 100644 --- a/watch-library/shared/watch/watch_hpt.h +++ b/watch-library/shared/watch/watch_hpt.h @@ -53,6 +53,13 @@ void watch_hpt_disable(void); */ 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. */ diff --git a/watch-library/simulator/watch/watch_hpt.c b/watch-library/simulator/watch/watch_hpt.c index 823c815e8..992c0c90e 100644 --- a/watch-library/simulator/watch/watch_hpt.c +++ b/watch-library/simulator/watch/watch_hpt.c @@ -169,6 +169,11 @@ uint32_t watch_hpt_get(void) return simhpt_paused_count; } } + +uint32_t watch_hpt_get_fast(void) +{ + return watch_hpt_get(); +} void watch_hpt_schedule_callback(uint32_t timestamp) { cb_compare_updated_timeout = true; From 2cf79bce520f85c9de09e375edeea8463dc39105 Mon Sep 17 00:00:00 2001 From: Zach Miller Date: Tue, 19 Mar 2024 15:15:20 -0400 Subject: [PATCH 11/15] HPT: Refactor buzzer sequences to use HPT instead of TC3 Also refactors movement.c to use HPT buzzing for alarms, signals, and other short beeps. Adds another feature to buzzer sequences: skip aheads. (Not used by anything yet, but repeater faces might need it. Also enables TCC run in standby always. TCC is enabled on-demand anyway. Might as well let it run in standby. Eventually, I would like to remove `watch_buzzer_play_note` because it uses delay_ms, but a lot of faces use it right now and they can be refactored later. --- movement/movement.c | 272 +++++++++++++----- movement/movement.h | 52 +++- movement/movement_config.h | 2 +- .../watch_faces/clock/hpt_led_test_face.c | 86 ++++-- .../watch_faces/clock/hpt_led_test_face.h | 2 +- .../watch_faces/complication/counter_face.c | 6 +- .../watch_faces/complication/interval_face.c | 4 +- .../watch_faces/complication/invaders_face.c | 4 +- .../complication/kitchen_conversions_face.c | 4 +- .../watch_faces/complication/sailing_face.c | 8 +- .../watch_faces/complication/timer_face.c | 6 +- watch-library/hardware/watch/watch_buzzer.c | 117 +------- watch-library/hardware/watch/watch_private.c | 4 + watch-library/shared/watch/watch_buzzer.h | 5 +- watch-library/simulator/watch/watch_buzzer.c | 64 +---- watch-library/simulator/watch/watch_hpt.c | 6 +- 16 files changed, 332 insertions(+), 310 deletions(-) diff --git a/movement/movement.c b/movement/movement.c index b881c9f53..2b85b0e40 100644 --- a/movement/movement.c +++ b/movement/movement.c @@ -84,7 +84,11 @@ watch_date_time scheduled_tasks[MOVEMENT_NUM_FACES]; // the number of available HPT request lines // (things other than faces can use the HPT) -#define HPT_NUM_REQUESTS MOVEMENT_NUM_FACES +#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)) @@ -205,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); @@ -351,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); } @@ -386,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) { @@ -415,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(); @@ -518,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; @@ -565,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. @@ -608,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}; @@ -659,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; @@ -672,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 @@ -723,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... @@ -769,7 +732,7 @@ void cb_tick(void) { static void _movement_hpt_schedule_next_event() { uint64_t next = UINT64_MAX; - for (uint8_t req_idx = 0; req_idx < MOVEMENT_NUM_FACES; ++req_idx) + for (uint8_t req_idx = 0; req_idx < HPT_NUM_REQUESTS; ++req_idx) { uint64_t event = hpt_scheduled_events[req_idx]; if (event < next) @@ -845,7 +808,7 @@ void cb_hpt(HPT_CALLBACK_CAUSE cause) 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 < MOVEMENT_NUM_FACES; ++face_idx) + 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) @@ -853,13 +816,22 @@ void cb_hpt(HPT_CALLBACK_CAUSE cause) // clear the scheduled event and allow the face to schedule a new one hpt_scheduled_events[face_idx] = UINT64_MAX; - // invoke the face - watch_face_t face = watch_faces[face_idx]; - movement_event_t event; - event.event_type = EVENT_HPT; - event.subsecond = 0; + 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]); + 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; } @@ -926,4 +898,156 @@ uint64_t movement_hpt_get() 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) { + printf("bc\r\n"); + + // 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; + } + + printf("bz: %d %d\r\n", nextNote, duration); + + // 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; + printf("bt: %" PRIu64 "\r\n", buzzer_note_end_ts); + + 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 ada29bf6d..a85b4c24c 100644 --- a/movement/movement.h +++ b/movement/movement.h @@ -254,11 +254,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; @@ -307,10 +302,55 @@ void movement_cancel_background_task_for_face(uint8_t watch_face_index); void movement_request_wake(void); +// Buzzer operation, 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 at the given tone for the given number of rounds +*/ void movement_play_alarm_beeps(uint8_t rounds, BuzzerNote alarm_note); +/** + * Plays the given note for the given number of milliseconds. + * + * Note that this does *not* block the face while running. To play a sequence of notes, use "movement_play_sequence" +*/ +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 element in the sequence is a pair of bytes. + * - If the first byte is a BuzzerNote, the second byte is interpreted as the duration of the note + * - 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 indicates the number of notes in the sequence that should be skipped + * If the second byte is zero, the jump marker is ignored. + * For example, to repeat the last four notes 3 additional times, the values of the two bytes would be -4, 3. + * + * The sequence MUST end with a null terminator (NULL, 0 or BUZZER_NOTE_END). + * See `movement_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 "length" of a note is 1/64th of a second, or about 16 milliseconds. To use a different note length, use `movement_play_sequence_speed`. +*/ +void movement_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void)); +void movement_play_sequence_speed(int8_t *note_sequence, void (*callback_on_end)(void), uint16_t note_duration); + +/** + * Silences any note sequences playing on the buzzer +*/ +void movement_silence_buzzer(void); + uint8_t movement_claim_backup_register(void); /** @@ -418,4 +458,6 @@ uint64_t movement_hpt_get(void); */ uint64_t movement_hpt_get_fast(void); + + #endif // MOVEMENT_H_ diff --git a/movement/movement_config.h b/movement/movement_config.h index 650f6d152..e3f2a9918 100644 --- a/movement/movement_config.h +++ b/movement/movement_config.h @@ -28,7 +28,7 @@ #include "movement_faces.h" const watch_face_t watch_faces[] = { - //hpt_led_test_face, + hpt_led_test_face, hpt_lapsplit_chrono_face, hpt_countdown_face, // dual_timer_face, diff --git a/movement/watch_faces/clock/hpt_led_test_face.c b/movement/watch_faces/clock/hpt_led_test_face.c index 9ccf0de23..3d0f7b23c 100644 --- a/movement/watch_faces/clock/hpt_led_test_face.c +++ b/movement/watch_faces/clock/hpt_led_test_face.c @@ -27,6 +27,36 @@ #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; @@ -38,7 +68,7 @@ void hpt_led_test_face_setup(movement_settings_t *settings, uint8_t watch_face_i // 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->leds_off = false; + state->running = false; } // Do any pin or peripheral setup here; this will be called whenever the watch wakes from deep sleep. } @@ -49,16 +79,15 @@ void hpt_led_test_face_activate(movement_settings_t *settings, void *context) hpt_led_test_state_t *state = (hpt_led_test_state_t *)context; watch_enable_leds(); - movement_hpt_request_face(state->face_idx); movement_request_tick_frequency(8); - //movement_hpt_schedule_face(movement_hpt_get() + 2048, state->face_idx); - + // 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) { +void render_hpt_time(uint32_t timestamp) +{ uint8_t high_high_nibble = timestamp >> 28; uint8_t high_low_nibble = (timestamp >> 24) & 0xF; @@ -66,7 +95,7 @@ void render_hpt_time(uint32_t timestamp) { char buf[11]; sprintf(buf, "%x %x%06x", high_high_nibble, high_low_nibble, lower_value); - watch_display_string(buf,0); + watch_display_string(buf, 0); } bool hpt_led_test_face_loop(movement_event_t event, movement_settings_t *settings, void *context) @@ -80,37 +109,35 @@ bool hpt_led_test_face_loop(movement_event_t event, movement_settings_t *setting case EVENT_ACTIVATE: // Show your initial UI here. break; - case EVENT_LIGHT_BUTTON_DOWN: - state->leds_off = true; - - movement_hpt_schedule(movement_hpt_get() + 2048); + 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()); + // If needed, update your display here. + render_hpt_time(movement_hpt_get_fast()); - if (state->leds_off == true) + if (((movement_hpt_get_fast() / 512) % 2) == 0) { - watch_set_led_off(); + watch_set_led_green(); } else { - if (((movement_hpt_get() / 512) % 2) == 0) - { - watch_set_led_green(); - } - else - { - watch_set_led_red(); - } + watch_set_led_red(); } - break; - case EVENT_HPT: - state->leds_off = false; - //printf("fe:%" PRIu64 "\r\n", movement_hpt_get()); + break; case EVENT_ALARM_BUTTON_UP: - // Just in case you have need for another button. + 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, @@ -144,8 +171,5 @@ bool hpt_led_test_face_loop(movement_event_t event, movement_settings_t *setting 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; - - // handle any cleanup before your watch face goes off-screen. - movement_hpt_release_face(state->face_idx); + 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 index 88e092156..44da486a6 100644 --- a/movement/watch_faces/clock/hpt_led_test_face.h +++ b/movement/watch_faces/clock/hpt_led_test_face.h @@ -37,7 +37,7 @@ typedef struct { // Anything you need to keep track of, put it here! uint8_t face_idx; - bool leds_off; + 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); 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/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/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_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..23be1ec91 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 @@ -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/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 index 992c0c90e..f83529397 100644 --- a/watch-library/simulator/watch/watch_hpt.c +++ b/watch-library/simulator/watch/watch_hpt.c @@ -19,7 +19,7 @@ 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 +//#define HPT_DEBUG static void cb_overflow(void *_unused) { @@ -176,6 +176,10 @@ uint32_t watch_hpt_get_fast(void) } 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(); From aa79719c6b7d85b875d5a53a56e2b11ae3e8656e Mon Sep 17 00:00:00 2001 From: Zach Miller Date: Tue, 19 Mar 2024 17:19:48 -0400 Subject: [PATCH 12/15] HPT: Some general clean-up and better documentation --- movement/movement.h | 185 +++++++++++------- .../complication/hpt_countdown_face.c | 2 +- .../complication/hpt_lapsplit_chrono_face.c | 10 +- .../complication/hpt_lapsplit_chrono_face.h | 10 +- watch-library/hardware/watch/watch_hpt.c | 81 ++++---- watch-library/shared/watch/watch_buzzer.h | 2 +- watch-library/shared/watch/watch_hpt.h | 42 ++-- .../shared/watch/watch_private_buzzer.c | 6 + .../shared/watch/watch_private_buzzer.h | 3 - 9 files changed, 209 insertions(+), 132 deletions(-) diff --git a/movement/movement.h b/movement/movement.h index a85b4c24c..773026824 100644 --- a/movement/movement.h +++ b/movement/movement.h @@ -175,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 @@ -195,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); @@ -302,53 +304,83 @@ void movement_cancel_background_task_for_face(uint8_t watch_face_index); void movement_request_wake(void); -// Buzzer operation, now handled by the HPT +// Buzzer operations, now handled by the HPT /** - * Plays the hourly signal chime -*/ + * 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 at the given tone for the given number of rounds + * 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. + * Plays the given note for the given number of milliseconds * - * Note that this does *not* block the face while running. To play a sequence of notes, use "movement_play_sequence" -*/ + * 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 element in the sequence is a pair of bytes. - * - If the first byte is a BuzzerNote, the second byte is interpreted as the duration of the note - * - 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 indicates the number of notes in the sequence that should be skipped - * If the second byte is zero, the jump marker is ignored. - * For example, to repeat the last four notes 3 additional times, the values of the two bytes would be -4, 3. + * 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_signal_tunes.h` for some example sequences. * - * If non-null, the given callback function will be invoked when the sequence is finished playing. + * See `movement_custom_signal_tunes.h` for some example sequences. * - * The default "length" of a note is 1/64th of a second, or about 16 milliseconds. To use a different note length, use `movement_play_sequence_speed`. + * 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)); -void movement_play_sequence_speed(int8_t *note_sequence, void (*callback_on_end)(void), uint16_t note_duration); /** - * Silences any note sequences playing on the buzzer + * 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); @@ -358,65 +390,84 @@ uint8_t movement_claim_backup_register(void); * 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 wake the watch up from sleep. + * may be used to trigger events while the watch is asleep. * - * The HPT is disabled by default to conserve power. Before using it to get a - * timestamp, a watch face must activate it using "movement_hpt_request". If + * 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, it will be disabled - * to conserve power. + * `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. * - * Watch faces may not modify the value of the HPT counter in any way. The only + * 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 the moment it calls - * "movement_hpt_release", the value returned from "movement_hpt_get" will - * increment upwards at 1024hz. Outside of that window, the timestamp value may - * change unpredictably. + * 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. + * `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 will need to use the "_face" variant of - * the HPT methods to specify that it is your face being called. -*/ + * 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" -*/ + * `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 must be called - * when your face no longer needs the timestamp provided by - * "movement_hpt_get" or has no scheduled background tasks, in order to save - * power. -*/ + * 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 timestamp. + * 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 if multiple faces or events are + * 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); /** @@ -426,12 +477,12 @@ void movement_hpt_schedule(uint64_t timestamp); void movement_hpt_schedule_face(uint64_t timestamp, uint8_t face_idx); /** - * Cancels any upcoming HPT events for the current face, if any. -*/ + * Cancels any upcoming EVENT_HPT events for the current face, if any. + */ void movement_hpt_cancel(void); /** - * Cancels any upcoming HPT events for the specified face -*/ + * Cancels any upcoming EVENT_HPT events for the specified face + */ void movement_hpt_cancel_face(uint8_t face_idx); /** @@ -440,24 +491,24 @@ void movement_hpt_cancel_face(uint8_t face_idx); * * 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 purposes; - * it is not properly synchronized with the timer peripheral, and it does not + * 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/watch_faces/complication/hpt_countdown_face.c b/movement/watch_faces/complication/hpt_countdown_face.c index 2f9cd990c..85f933c43 100644 --- a/movement/watch_faces/complication/hpt_countdown_face.c +++ b/movement/watch_faces/complication/hpt_countdown_face.c @@ -348,7 +348,7 @@ bool hpt_countdown_face_loop(movement_event_t event, movement_settings_t *settin case EVENT_LIGHT_LONG_PRESS: // toggle auto-repeat mode state->auto_repeat = !(state->auto_repeat); - // TODO: if timer is currently in overflow mode, restart a new lap immediately + // if timer is currently in overflow mode, restart a new lap immediately if (state->running && (state->target <= movement_hpt_get())) { restart_timer(state); diff --git a/movement/watch_faces/complication/hpt_lapsplit_chrono_face.c b/movement/watch_faces/complication/hpt_lapsplit_chrono_face.c index 315f16096..8095c0dd3 100644 --- a/movement/watch_faces/complication/hpt_lapsplit_chrono_face.c +++ b/movement/watch_faces/complication/hpt_lapsplit_chrono_face.c @@ -69,16 +69,19 @@ static void render(hpt_lapsplit_chrono_state_t *context, bool lowEnergyUpdate) } 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(); @@ -91,12 +94,15 @@ static void render(hpt_lapsplit_chrono_state_t *context, bool lowEnergyUpdate) 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) { @@ -104,6 +110,7 @@ static void render(hpt_lapsplit_chrono_state_t *context, bool lowEnergyUpdate) } else if (time_hours > 39) { + // keep timing, but I guess show an error up here. watch_display_string(" E", 2); } else @@ -118,7 +125,7 @@ static void splitButton(hpt_lapsplit_chrono_state_t *state) { if (state->display == LCF_DISPLAY_SPLIT) { - // if the duration is being displayed, clear it when you press "light" again + // 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; } @@ -241,6 +248,7 @@ bool hpt_lapsplit_chrono_face_loop(movement_event_t event, movement_settings_t * 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); diff --git a/movement/watch_faces/complication/hpt_lapsplit_chrono_face.h b/movement/watch_faces/complication/hpt_lapsplit_chrono_face.h index 73882bc64..cdf63d949 100644 --- a/movement/watch_faces/complication/hpt_lapsplit_chrono_face.h +++ b/movement/watch_faces/complication/hpt_lapsplit_chrono_face.h @@ -31,8 +31,8 @@ * 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. - * The chronograph time will be displayed in the primary digits in MM:SS HH format. If the time exceeds 1 hour, + * 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. @@ -58,6 +58,10 @@ * 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 @@ -80,7 +84,7 @@ typedef struct /** LCF_RUN */ uint8_t running : 1; - uint8_t _padding1 :5; + uint8_t _padding1 : 5; // align to byte boundaries // up to 39 laps, then reset uint8_t laps : 6; diff --git a/watch-library/hardware/watch/watch_hpt.c b/watch-library/hardware/watch/watch_hpt.c index c526152eb..d332e6386 100644 --- a/watch-library/hardware/watch/watch_hpt.c +++ b/watch-library/hardware/watch/watch_hpt.c @@ -1,10 +1,8 @@ - #include "watch_hpt.h" - #include "parts.h" // user HPT callback -void (*hpt_isr_callback)(HPT_CALLBACK_CAUSE) = 0; +void (*hpt_isr_callback)(HPT_CALLBACK_CAUSE) = NULL; // actual HPT ISR void TC2_Handler(void) @@ -18,7 +16,7 @@ void TC2_Handler(void) // silly that you have to write ones these flags to clear them TC2->COUNT32.INTFLAG.reg = 0xFF; - if (hpt_isr_callback != 0) + if (hpt_isr_callback) { (*hpt_isr_callback)(cause); } @@ -63,9 +61,19 @@ void watch_hpt_init(void (*callback_function)(HPT_CALLBACK_CAUSE)) GCLK_CRITICAL_SECTION_LEAVE(); + // Configure TC2 to count up to MAX and generate appropriate interrupts, but don't turn it on - // TC2.CTRLA: + // 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 @@ -73,42 +81,40 @@ void watch_hpt_init(void (*callback_function)(HPT_CALLBACK_CAUSE)) // 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 + // 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 - TC_CRITICAL_SECTION_ENTER(); - - // reset counter - TC2->COUNT32.CTRLA.bit.SWRST = 1; - while (TC2->COUNT32.SYNCBUSY.bit.SWRST) - ; - TC2->COUNT32.CTRLA.reg = TC_CTRLA_ONDEMAND | TC_CTRLA_RUNSTDBY | TC_CTRLA_MODE_COUNT32; - // TC2.COUNT = 0 // just clear it to be safe - // TC2.CC0 = 0 + // 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) ; - // TC2->COUNT32.CC[0].bit.CC = UINT32_MAX -1; - // while(TC2->COUNT32.SYNCBUSY.bit.CC0 != 0); // enable TC2 interrupt - + // Disable IRQ temporarily while setting up interrupt flags NVIC_DisableIRQ(TC2_IRQn); // TC2.INTENSET: enabling interrupts - // OVF = 1 - always enable overflow interrupt - // disable compare match interrupt to start - TC2->COUNT32.INTENCLR.bit.MC0 = 1; + + // Always enable overflow interrupt TC2->COUNT32.INTENSET.bit.OVF = 1; - TC2->COUNT32.INTFLAG.reg = 0xFF; + // 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); @@ -117,10 +123,12 @@ void watch_hpt_init(void (*callback_function)(HPT_CALLBACK_CAUSE)) 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 - // TC2.ENABLE = 1 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(); @@ -129,9 +137,9 @@ void watch_hpt_enable(void) void watch_hpt_disable(void) { // stop timer - // TC2.ENABLE = 0 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(); @@ -139,17 +147,18 @@ void watch_hpt_disable(void) void watch_hpt_schedule_callback(uint32_t timestamp) { - // set compare channel - // TC2.CC0 = timestamp - // enable interrupt - // TC2.INTENSET.MC0 = 1 - 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) ; - TC2->COUNT32.INTFLAG.reg = 0xFF; + // enable MC0 interrupt TC2->COUNT32.INTENSET.bit.MC0 = 1; TC_CRITICAL_SECTION_LEAVE(); } @@ -157,9 +166,8 @@ void watch_hpt_schedule_callback(uint32_t timestamp) void watch_hpt_disable_scheduled_callback(void) { // disable interrupt - // TC2.INTENCLR.MC0 = 1 TC_CRITICAL_SECTION_ENTER(); - TC2->COUNT32.INTENCLR.bit.MC0 = 1; + TC2->COUNT32.INTENCLR.bit.MC0 = 1; // disable match/compare 0 interrupt TC_CRITICAL_SECTION_LEAVE(); } @@ -181,18 +189,17 @@ void watch_hpt_disable_scheduled_callback(void) uint32_t watch_hpt_get(void) { // synchronize a read of the count value - // TC2.CTRLBSET.CMD = 0x4 - force READSYNC TC_CRITICAL_SECTION_ENTER(); TC2->COUNT32.CTRLBSET.reg = TC_CTRLBSET_CMD_READSYNC; - // wait for command to be executed + // 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 sync to occur? + // 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) ; - // this might not be necessary? // wait for count to be synchronized while (TC2->COUNT32.SYNCBUSY.bit.COUNT) @@ -206,7 +213,7 @@ uint32_t watch_hpt_get(void) uint32_t watch_hpt_get_fast(void) { - // quick and dirty timer read, not suitable for scheduling + // 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(); diff --git a/watch-library/shared/watch/watch_buzzer.h b/watch-library/shared/watch/watch_buzzer.h index 23be1ec91..736300368 100644 --- a/watch-library/shared/watch/watch_buzzer.h +++ b/watch-library/shared/watch/watch_buzzer.h @@ -159,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 diff --git a/watch-library/shared/watch/watch_hpt.h b/watch-library/shared/watch/watch_hpt.h index 125f7d0fb..fb4f38fb6 100644 --- a/watch-library/shared/watch/watch_hpt.h +++ b/watch-library/shared/watch/watch_hpt.h @@ -5,52 +5,55 @@ #include /** - * Defines the reasons the HPT callback is being invoked. More than one flag may be set. + * 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 equal to the scheduled timestamp - */ + * The callback is being invoked because the count is greater than or equal to to the scheduled timestamp + */ bool compare_match :1; + /** - * The callbac is being invoked because the counter overflowed and reset to zero. - */ + * 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; -/** - * 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. -*/ + /** * Performs one-time setup of the peripherals used by the high-precision timer. * - * - callback_function: a function that should be invoked when the timer overflows or reaches a scheduled event. + * Does not start the timer. * - * Does not enable 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 the high-precision timer -*/ + * 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 timetamp of the high-precision timer. -*/ + * Returns the current counter value of the high-precision timer. + */ uint32_t watch_hpt_get(void); /** @@ -66,10 +69,11 @@ uint32_t watch_hpt_get_fast(void); void watch_hpt_schedule_callback(uint32_t timestamp); /** - * Disables the scheduled callback. + * 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); From 728b3e3459cd466873aa5ea5142a7d0d2da2aa5f Mon Sep 17 00:00:00 2001 From: Zach Miller Date: Tue, 19 Mar 2024 18:11:13 -0400 Subject: [PATCH 13/15] HPT: Restore and refactor stock_stopwatch_face to use HPT --- movement/make/Makefile | 1 + movement/movement_config.h | 1 + movement/movement_faces.h | 1 + .../complication/stock_stopwatch_face.c | 253 ++++++++++++++++++ .../complication/stock_stopwatch_face.h | 80 ++++++ 5 files changed, 336 insertions(+) create mode 100644 movement/watch_faces/complication/stock_stopwatch_face.c create mode 100644 movement/watch_faces/complication/stock_stopwatch_face.h diff --git a/movement/make/Makefile b/movement/make/Makefile index 454875843..2d2a93797 100644 --- a/movement/make/Makefile +++ b/movement/make/Makefile @@ -129,6 +129,7 @@ SRCS += \ ../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_config.h b/movement/movement_config.h index e3f2a9918..0f0faa2b8 100644 --- a/movement/movement_config.h +++ b/movement/movement_config.h @@ -28,6 +28,7 @@ #include "movement_faces.h" const watch_face_t watch_faces[] = { + stock_stopwatch_face, hpt_led_test_face, hpt_lapsplit_chrono_face, hpt_countdown_face, diff --git a/movement/movement_faces.h b/movement/movement_faces.h index 7e181f3a3..5b9f5f976 100644 --- a/movement/movement_faces.h +++ b/movement/movement_faces.h @@ -105,6 +105,7 @@ #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/complication/stock_stopwatch_face.c b/movement/watch_faces/complication/stock_stopwatch_face.c new file mode 100644 index 000000000..971c939c2 --- /dev/null +++ b/movement/watch_faces/complication/stock_stopwatch_face.c @@ -0,0 +1,253 @@ +/* + * MIT License + * + * Copyright (c) 2022 Andreas Nebinger + * + * 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 "stock_stopwatch_face.h" +#include "watch.h" +#include "watch_utility.h" +#include "watch_rtc.h" + +/* + This watch face implements the original F-91W stopwatch functionality + including counting hundredths of seconds and lap timing. There are two + improvements compared to the functionality of the original: + 1. When reaching 59:59 the counter does not simply jump back to zero, + but keeps track of hours in the upper right hand corner. (Up to 24h) + 2. Long pressing the light button toggles the led behaviour: it either + turns on on each button press or it doesn't. +*/ + +#if __EMSCRIPTEN__ +#include +#include +#endif + +// the HPT timestamp the stopwatch was most recently started +static uint64_t _hpt_start_timestamp; + +// 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; +static uint8_t _old_minutes; +static uint8_t _hours; +static bool _colon; +static bool _is_running; + +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; +} + +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) movement_play_note(BUZZER_NOTE_C7, 50); +} + +/// @brief Display minutes, seconds and fractions derived from 128 Hz tick counter +/// on the lcd. +/// @param ticks +static void _display_ticks(uint32_t ticks) { + char buf[14]; + uint8_t sec_100 = (ticks & 0x7F) * 100 / 128; + uint32_t seconds = ticks >> 7; + uint32_t minutes = seconds / 60; + if (_hours) + sprintf(buf, "%2u%02lu%02lu%02u", _hours, minutes, (seconds % 60), sec_100); + else + sprintf(buf, " %02lu%02lu%02u", minutes, (seconds % 60), sec_100); + watch_display_string(buf, 2); +} + +/// @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; + if (_is_running) { + uint32_t seconds = _ticks >> 7; + if (seconds != _old_seconds) { + // seconds have changed + _old_seconds = seconds; + uint8_t minutes = seconds / 60; + seconds %= 60; + if (minutes != _old_minutes) { + // minutes have changed, draw everything + _old_minutes = minutes; + minutes %= 60; + if (_hours) + // with hour indicator + sprintf(buf, "%2u%02u%02lu%02u", _hours, minutes, seconds, sec_100); + else + // no hour indicator + sprintf(buf, " %02u%02lu%02u", minutes, seconds, sec_100); + watch_display_string(buf, 2); + } else { + // just draw seconds + sprintf(buf, "%02lu%02u", seconds, sec_100); + watch_display_string(buf, 6); + } + } else { + // only draw 100ths of seconds + sprintf(buf, "%02u", sec_100); + watch_display_string(buf, 8); + } + } else { + _display_ticks(_ticks); + } + } + if (_is_running) { + // blink the colon every half second + uint8_t blink_ticks = ((_ticks >> 6) & 1); + if (blink_ticks != _blink_ticks) { + _blink_ticks = blink_ticks; + _colon = !_colon; + if (_colon) watch_set_colon(); + else watch_clear_colon(); + } + } +} + +static inline void _update_lap_indicator() { + if (_lap_ticks) watch_set_indicator(WATCH_INDICATOR_LAP); + else watch_clear_indicator(WATCH_INDICATOR_LAP); +} + +static inline void _set_colon() { + watch_set_colon(); + _colon = true; +} + +void stock_stopwatch_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(stock_stopwatch_state_t)); + memset(*context_ptr, 0, sizeof(stock_stopwatch_state_t)); + stock_stopwatch_state_t *state = (stock_stopwatch_state_t *)*context_ptr; + _paused_ticks = _lap_ticks = _blink_ticks = _old_minutes = _old_seconds = _hours = 0; + _is_running = _colon = false; + state->light_on_button = true; + } +} + +void stock_stopwatch_face_activate(movement_settings_t *settings, void *context) { + (void) settings; + (void) context; + +} + + + +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; + + uint32_t _ticks = _get_running_ticks(); + + switch (event.event_type) { + case EVENT_ACTIVATE: + _set_colon(); + watch_display_string("ST ", 0); + _update_lap_indicator(); + if (_is_running) movement_request_tick_frequency(16); + _display_ticks(_lap_ticks ? _lap_ticks : _ticks); + break; + case EVENT_TICK: + _draw(); + break; + case EVENT_LIGHT_LONG_PRESS: + // kind od hidden feature: long press toggles light on or off + state->light_on_button = !state->light_on_button; + if (state->light_on_button) movement_illuminate_led(); + else watch_set_led_off(); + break; + case EVENT_ALARM_BUTTON_DOWN: + _is_running = !_is_running; + if (_is_running) { + // start or continue stopwatch + movement_request_tick_frequency(16); + movement_hpt_request(); + _hpt_start_timestamp = movement_hpt_get(); + } else { + // stop the stopwatch + movement_request_tick_frequency(1); + _set_colon(); + _paused_ticks += (movement_hpt_get() - _hpt_start_timestamp)/8; + movement_hpt_release(); + } + _draw(); + _button_beep(settings); + break; + case EVENT_LIGHT_BUTTON_DOWN: + if (state->light_on_button) movement_illuminate_led(); + if (_is_running) { + if (_lap_ticks) { + // clear lap and continue running + _lap_ticks = 0; + movement_request_tick_frequency(16); + } else { + // set lap ticks and stop updating the display + _lap_ticks = _ticks; + movement_request_tick_frequency(2); + _set_colon(); + } + } else { + if (_lap_ticks) { + // clear lap and show running stopwatch + _lap_ticks = 0; + } else if (_paused_ticks) { + // reset stopwatch + _paused_ticks = _lap_ticks = _blink_ticks = _old_minutes = _old_seconds = _hours = 0; + _button_beep(settings); + } + } + _display_ticks(_ticks); + _update_lap_indicator(); + break; + case EVENT_TIMEOUT: + if (!_is_running) movement_move_to_face(0); + break; + case EVENT_LOW_ENERGY_UPDATE: + _draw(); + break; + default: + movement_default_loop_handler(event, settings); + break; + } + return true; +} + +void stock_stopwatch_face_resign(movement_settings_t *settings, void *context) { + (void) settings; + (void) context; + // cancel the keepalive task + movement_cancel_background_task(); +} diff --git a/movement/watch_faces/complication/stock_stopwatch_face.h b/movement/watch_faces/complication/stock_stopwatch_face.h new file mode 100644 index 000000000..6796a8499 --- /dev/null +++ b/movement/watch_faces/complication/stock_stopwatch_face.h @@ -0,0 +1,80 @@ +/* + * MIT License + * + * Copyright (c) 2022 Andreas Nebinger + * + * 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 STOCK_STOPWATCH_FACE_H_ +#define STOCK_STOPWATCH_FACE_H_ + +/* + * STOCK STOPWATCH face + * + * The Stock Stopwatch face implements the original F-91W stopwatch + * functionality, including counting hundredths of seconds and lap timing. + * + * Use the ALARM button to start and stop the stopwatch. + * Press the LIGHT button while the stopwatch is running to view the lap time. + * (The stopwatch continues running in the background, indicated by a blinking colon.) + * Press the LIGHT button again to switch back to the running stopwatch. + * Press the LIGHT button when the timekeeping is stopped to reset the stopwatch. + * + * There are two improvements compared to the original F-91W: + * o When the stopwatch reaches 59:59, the counter does not simply jump back + * to zero but keeps track of hours in the upper right-hand corner + * (up to 24 hours). + * o Long-press the light button to toggle the LED behavior. + * It either turns on with each button press or remains off. + * + * NOTE: + * This watch face relies heavily on static vars in stock_stopwatch.c. + * The disadvantage is that you cannot use more than one instance of this + * watch face on your custom firmware - but then again, who would want that? + * The advantage is that accessing vars is more direct and faster, and we + * can save some precious cpu cycles. :-) + */ + +#include "movement.h" + +typedef struct { + bool light_on_button; // determines whether the light button actually triggers the led +} stock_stopwatch_state_t; + +void stock_stopwatch_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void stock_stopwatch_face_activate(movement_settings_t *settings, void *context); +bool stock_stopwatch_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void stock_stopwatch_face_resign(movement_settings_t *settings, void *context); + +#if __EMSCRIPTEN__ +void em_cb_handler(void *userData); +#else +void TC2_Handler(void); +#endif + +#define stock_stopwatch_face ((const watch_face_t){ \ + stock_stopwatch_face_setup, \ + stock_stopwatch_face_activate, \ + stock_stopwatch_face_loop, \ + stock_stopwatch_face_resign, \ + NULL, \ +}) + +#endif // STOCK_STOPWATCH_FACE_H_ From 6721c009e182ff0dbbccbf81b73a776f4bf7065c Mon Sep 17 00:00:00 2001 From: Zach Miller Date: Tue, 19 Mar 2024 18:23:04 -0400 Subject: [PATCH 14/15] HPT: minor cleanup of some debug statements --- movement/movement.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/movement/movement.c b/movement/movement.c index 2b85b0e40..d77676c55 100644 --- a/movement/movement.c +++ b/movement/movement.c @@ -932,8 +932,6 @@ void movement_silence_buzzer(void) { } void _movement_play_next_buzzer_note(void) { - printf("bc\r\n"); - // if the buzzer sequence is null, stop if(buzzer_sequence == 0) { movement_silence_buzzer(); @@ -953,8 +951,6 @@ void _movement_play_next_buzzer_note(void) { return; } - printf("bz: %d %d\r\n", nextNote, duration); - // check for jumps if(nextNote < 0) { if(duration == 0) { @@ -1017,7 +1013,6 @@ void _movement_play_next_buzzer_note(void) { // schedule the next note change buzzer_note_end_ts += ((uint64_t)buzzer_note_length) * duration; - printf("bt: %" PRIu64 "\r\n", buzzer_note_end_ts); movement_hpt_schedule_face(buzzer_note_end_ts, MOVEMENT_BUZZER_HPT); } From 3b752d0d49aee0394d44e71c1033fb9661ee33e8 Mon Sep 17 00:00:00 2001 From: Zach Miller Date: Tue, 19 Mar 2024 18:31:14 -0400 Subject: [PATCH 15/15] HPT: small fix to countdown face state --- .../complication/hpt_countdown_face.h | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/movement/watch_faces/complication/hpt_countdown_face.h b/movement/watch_faces/complication/hpt_countdown_face.h index 008155601..dc9e98c3b 100644 --- a/movement/watch_faces/complication/hpt_countdown_face.h +++ b/movement/watch_faces/complication/hpt_countdown_face.h @@ -50,16 +50,10 @@ typedef struct { // 17 bits - // 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 :18; - - // 35 bits - bool auto_repeat :1; bool running :1; - // 37 bits + // 19 bits /** * 0 = not setting anything @@ -74,15 +68,20 @@ typedef struct { uint8_t setting_mode :3; uint8_t repeat_count :5; + // 27 bits - // 43 bits - uint8_t padding :5; + uint8_t _padding :5; - // 48 bits + // 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;