Initial seemingly stable version of movement using the RTC COUNTER32 mode
This commit is contained in:
@@ -20,6 +20,9 @@ TINYUSB_CDC=1
|
|||||||
# Now we're all set to include gossamer's make rules.
|
# Now we're all set to include gossamer's make rules.
|
||||||
include $(GOSSAMER_PATH)/make.mk
|
include $(GOSSAMER_PATH)/make.mk
|
||||||
|
|
||||||
|
# Don't add gossamer's rtc.c since we are using our own rtc32.c
|
||||||
|
SRCS := $(filter-out $(GOSSAMER_PATH)/peripherals/rtc.c,$(SRCS))
|
||||||
|
|
||||||
CFLAGS+=-D_POSIX_C_SOURCE=200112L
|
CFLAGS+=-D_POSIX_C_SOURCE=200112L
|
||||||
|
|
||||||
define n
|
define n
|
||||||
@@ -136,6 +139,7 @@ INCLUDES += \
|
|||||||
-I./watch-library/hardware/watch \
|
-I./watch-library/hardware/watch \
|
||||||
|
|
||||||
SRCS += \
|
SRCS += \
|
||||||
|
./watch-library/hardware/watch/rtc32.c \
|
||||||
./watch-library/hardware/watch/watch.c \
|
./watch-library/hardware/watch/watch.c \
|
||||||
./watch-library/hardware/watch/watch_adc.c \
|
./watch-library/hardware/watch/watch_adc.c \
|
||||||
./watch-library/hardware/watch/watch_deepsleep.c \
|
./watch-library/hardware/watch/watch_deepsleep.c \
|
||||||
|
|||||||
+479
-256
@@ -2,6 +2,7 @@
|
|||||||
* MIT License
|
* MIT License
|
||||||
*
|
*
|
||||||
* Copyright (c) 2022 Joey Castillo
|
* Copyright (c) 2022 Joey Castillo
|
||||||
|
* Copyright (c) 2025 Alessandro Genova
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
@@ -59,7 +60,55 @@ void * watch_face_contexts[MOVEMENT_NUM_FACES];
|
|||||||
watch_date_time_t scheduled_tasks[MOVEMENT_NUM_FACES];
|
watch_date_time_t scheduled_tasks[MOVEMENT_NUM_FACES];
|
||||||
const int32_t movement_le_inactivity_deadlines[8] = {INT_MAX, 600, 3600, 7200, 21600, 43200, 86400, 604800};
|
const int32_t movement_le_inactivity_deadlines[8] = {INT_MAX, 600, 3600, 7200, 21600, 43200, 86400, 604800};
|
||||||
const int16_t movement_timeout_inactivity_deadlines[4] = {60, 120, 300, 1800};
|
const int16_t movement_timeout_inactivity_deadlines[4] = {60, 120, 300, 1800};
|
||||||
movement_event_t event;
|
|
||||||
|
typedef struct {
|
||||||
|
movement_event_type_t down_event;
|
||||||
|
watch_cb_t cb_longpress;
|
||||||
|
movement_timeout_index_t timeout_index;
|
||||||
|
volatile bool is_down;
|
||||||
|
volatile rtc_counter_t down_timestamp;
|
||||||
|
} movement_button_t;
|
||||||
|
|
||||||
|
/* Pieces of state that can be modified by the various interrupt callbacks.
|
||||||
|
The interrupt writes state changes here, and it will be acted upon on the next app_loop invokation.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
volatile uint32_t pending_events;
|
||||||
|
volatile bool turn_led_off;
|
||||||
|
volatile bool has_pending_sequence;
|
||||||
|
volatile bool enter_sleep_mode;
|
||||||
|
volatile bool exit_sleep_mode;
|
||||||
|
volatile bool is_sleeping;
|
||||||
|
volatile uint8_t subsecond;
|
||||||
|
volatile rtc_counter_t minute_counter;
|
||||||
|
volatile bool minute_alarm_fired;
|
||||||
|
volatile bool is_buzzing;
|
||||||
|
volatile uint8_t pending_sequence_priority;
|
||||||
|
|
||||||
|
// button tracking for long press
|
||||||
|
movement_button_t mode_button;
|
||||||
|
movement_button_t light_button;
|
||||||
|
movement_button_t alarm_button;
|
||||||
|
} movement_volatile_state_t;
|
||||||
|
|
||||||
|
movement_volatile_state_t movement_volatile_state;
|
||||||
|
|
||||||
|
// The last sequence that we have been asked to play while the watch was in deep sleep
|
||||||
|
static int8_t *_pending_sequence;
|
||||||
|
|
||||||
|
// The note sequence of the default alarm
|
||||||
|
int8_t alarm_tune[] = {
|
||||||
|
BUZZER_NOTE_C8, 4,
|
||||||
|
BUZZER_NOTE_REST, 4,
|
||||||
|
BUZZER_NOTE_C8, 4,
|
||||||
|
BUZZER_NOTE_REST, 4,
|
||||||
|
BUZZER_NOTE_C8, 4,
|
||||||
|
BUZZER_NOTE_REST, 4,
|
||||||
|
BUZZER_NOTE_C8, 6,
|
||||||
|
BUZZER_NOTE_REST, 18,
|
||||||
|
-8, 9,
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
int8_t _movement_dst_offset_cache[NUM_ZONE_NAMES] = {0};
|
int8_t _movement_dst_offset_cache[NUM_ZONE_NAMES] = {0};
|
||||||
#define TIMEZONE_DOES_NOT_OBSERVE (-127)
|
#define TIMEZONE_DOES_NOT_OBSERVE (-127)
|
||||||
@@ -68,9 +117,16 @@ void cb_mode_btn_interrupt(void);
|
|||||||
void cb_light_btn_interrupt(void);
|
void cb_light_btn_interrupt(void);
|
||||||
void cb_alarm_btn_interrupt(void);
|
void cb_alarm_btn_interrupt(void);
|
||||||
void cb_alarm_btn_extwake(void);
|
void cb_alarm_btn_extwake(void);
|
||||||
void cb_alarm_fired(void);
|
void cb_minute_alarm_fired(void);
|
||||||
void cb_fast_tick(void);
|
|
||||||
void cb_tick(void);
|
void cb_tick(void);
|
||||||
|
void cb_mode_btn_timeout_interrupt(void);
|
||||||
|
void cb_light_btn_timeout_interrupt(void);
|
||||||
|
void cb_alarm_btn_timeout_interrupt(void);
|
||||||
|
void cb_led_timeout_interrupt(void);
|
||||||
|
void cb_resign_timeout_interrupt(void);
|
||||||
|
void cb_sleep_timeout_interrupt(void);
|
||||||
|
void cb_buzzer_start(void);
|
||||||
|
void cb_buzzer_stop(void);
|
||||||
|
|
||||||
void cb_accelerometer_event(void);
|
void cb_accelerometer_event(void);
|
||||||
void cb_accelerometer_wake(void);
|
void cb_accelerometer_wake(void);
|
||||||
@@ -97,6 +153,21 @@ static udatetime_t _movement_convert_date_time_to_udate(watch_date_time_t date_t
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void _movement_set_top_of_minute_alarm() {
|
||||||
|
uint32_t counter = watch_rtc_get_counter();
|
||||||
|
watch_date_time_t date_time = watch_rtc_get_date_time();
|
||||||
|
uint32_t freq = watch_rtc_get_frequency();
|
||||||
|
|
||||||
|
// remove subsecond from counter
|
||||||
|
counter &= ~(freq - 1);
|
||||||
|
// counter at the next top of the minute
|
||||||
|
counter += (60 - date_time.unit.second) * freq;
|
||||||
|
|
||||||
|
movement_volatile_state.minute_counter = counter;
|
||||||
|
|
||||||
|
watch_rtc_register_comp_callback(cb_minute_alarm_fired, counter, MINUTE_TIMEOUT);
|
||||||
|
}
|
||||||
|
|
||||||
static bool _movement_update_dst_offset_cache(void) {
|
static bool _movement_update_dst_offset_cache(void) {
|
||||||
uzone_t local_zone;
|
uzone_t local_zone;
|
||||||
udatetime_t udate_time;
|
udatetime_t udate_time;
|
||||||
@@ -127,25 +198,27 @@ static bool _movement_update_dst_offset_cache(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static inline void _movement_reset_inactivity_countdown(void) {
|
static inline void _movement_reset_inactivity_countdown(void) {
|
||||||
movement_state.le_mode_ticks = movement_le_inactivity_deadlines[movement_state.settings.bit.le_interval];
|
rtc_counter_t counter = watch_rtc_get_counter();
|
||||||
movement_state.timeout_ticks = movement_timeout_inactivity_deadlines[movement_state.settings.bit.to_interval];
|
uint32_t freq = watch_rtc_get_frequency();
|
||||||
|
|
||||||
|
watch_rtc_register_comp_callback(
|
||||||
|
cb_resign_timeout_interrupt,
|
||||||
|
counter + movement_timeout_inactivity_deadlines[movement_state.settings.bit.to_interval] * freq,
|
||||||
|
RESIGN_TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
movement_volatile_state.enter_sleep_mode = false;
|
||||||
|
|
||||||
|
watch_rtc_register_comp_callback(
|
||||||
|
cb_sleep_timeout_interrupt,
|
||||||
|
counter + movement_le_inactivity_deadlines[movement_state.settings.bit.le_interval] * freq,
|
||||||
|
SLEEP_TIMEOUT
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void _movement_enable_fast_tick_if_needed(void) {
|
static inline void _movement_disable_inactivity_countdown(void) {
|
||||||
if (!movement_state.fast_tick_enabled) {
|
watch_rtc_disable_comp_callback(RESIGN_TIMEOUT);
|
||||||
movement_state.fast_ticks = 0;
|
watch_rtc_disable_comp_callback(SLEEP_TIMEOUT);
|
||||||
watch_rtc_register_periodic_callback(cb_fast_tick, 128);
|
|
||||||
movement_state.fast_tick_enabled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline void _movement_disable_fast_tick_if_possible(void) {
|
|
||||||
if ((movement_state.light_ticks == -1) &&
|
|
||||||
(movement_state.alarm_ticks == -1) &&
|
|
||||||
((movement_state.light_down_timestamp + movement_state.mode_down_timestamp + movement_state.alarm_down_timestamp) == 0)) {
|
|
||||||
movement_state.fast_tick_enabled = false;
|
|
||||||
watch_rtc_disable_periodic_callback(128);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void _movement_handle_top_of_minute(void) {
|
static void _movement_handle_top_of_minute(void) {
|
||||||
@@ -172,7 +245,6 @@ static void _movement_handle_top_of_minute(void) {
|
|||||||
// TODO: handle other advisory types
|
// TODO: handle other advisory types
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
movement_state.woke_from_alarm_handler = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void _movement_handle_scheduled_tasks(void) {
|
static void _movement_handle_scheduled_tasks(void) {
|
||||||
@@ -203,45 +275,59 @@ static void _movement_handle_scheduled_tasks(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void movement_request_tick_frequency(uint8_t freq) {
|
void movement_request_tick_frequency(uint8_t freq) {
|
||||||
// Movement uses the 128 Hz tick internally
|
|
||||||
if (freq == 128) return;
|
|
||||||
|
|
||||||
// Movement requires at least a 1 Hz tick.
|
// Movement requires at least a 1 Hz tick.
|
||||||
// If we are asked for an invalid frequency, default back to 1 Hz.
|
// If we are asked for an invalid frequency, default back to 1 Hz.
|
||||||
if (freq == 0 || __builtin_popcount(freq) != 1) freq = 1;
|
if (freq == 0 || __builtin_popcount(freq) != 1) freq = 1;
|
||||||
|
|
||||||
// disable all callbacks except the 128 Hz one
|
// disable all periodic callbacks
|
||||||
watch_rtc_disable_matching_periodic_callbacks(0xFE);
|
watch_rtc_disable_matching_periodic_callbacks(0xFF);
|
||||||
|
|
||||||
|
// this left-justifies the period in a 32-bit integer.
|
||||||
|
uint32_t tmp = (freq & 0xFF) << 24;
|
||||||
|
// now we can count the leading zeroes to get the value we need.
|
||||||
|
// 0x01 (1 Hz) will have 7 leading zeros for PER7. 0x80 (128 Hz) will have no leading zeroes for PER0.
|
||||||
|
uint8_t per_n = __builtin_clz(tmp);
|
||||||
|
|
||||||
movement_state.subsecond = 0;
|
|
||||||
movement_state.tick_frequency = freq;
|
movement_state.tick_frequency = freq;
|
||||||
|
movement_state.tick_pern = per_n;
|
||||||
|
|
||||||
watch_rtc_register_periodic_callback(cb_tick, freq);
|
watch_rtc_register_periodic_callback(cb_tick, freq);
|
||||||
}
|
}
|
||||||
|
|
||||||
void movement_illuminate_led(void) {
|
void movement_illuminate_led(void) {
|
||||||
if (movement_state.settings.bit.led_duration != 0b111) {
|
if (movement_state.settings.bit.led_duration != 0b111) {
|
||||||
|
movement_state.light_on = true;
|
||||||
watch_set_led_color_rgb(movement_state.settings.bit.led_red_color | movement_state.settings.bit.led_red_color << 4,
|
watch_set_led_color_rgb(movement_state.settings.bit.led_red_color | movement_state.settings.bit.led_red_color << 4,
|
||||||
movement_state.settings.bit.led_green_color | movement_state.settings.bit.led_green_color << 4,
|
movement_state.settings.bit.led_green_color | movement_state.settings.bit.led_green_color << 4,
|
||||||
movement_state.settings.bit.led_blue_color | movement_state.settings.bit.led_blue_color << 4);
|
movement_state.settings.bit.led_blue_color | movement_state.settings.bit.led_blue_color << 4);
|
||||||
if (movement_state.settings.bit.led_duration == 0) {
|
if (movement_state.settings.bit.led_duration == 0) {
|
||||||
movement_state.light_ticks = 1;
|
// Do nothing it'll be turned off on button release
|
||||||
} else {
|
} else {
|
||||||
movement_state.light_ticks = (movement_state.settings.bit.led_duration * 2 - 1) * 128;
|
// Set a timeout to turn off the light
|
||||||
|
rtc_counter_t counter = watch_rtc_get_counter();
|
||||||
|
uint32_t freq = watch_rtc_get_frequency();
|
||||||
|
watch_rtc_register_comp_callback(
|
||||||
|
cb_led_timeout_interrupt,
|
||||||
|
counter + (movement_state.settings.bit.led_duration * 2 - 1) * freq,
|
||||||
|
LED_TIMEOUT
|
||||||
|
);
|
||||||
}
|
}
|
||||||
_movement_enable_fast_tick_if_needed();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void movement_force_led_on(uint8_t red, uint8_t green, uint8_t blue) {
|
void movement_force_led_on(uint8_t red, uint8_t green, uint8_t blue) {
|
||||||
// this is hacky, we need a way for watch faces to set an arbitrary color and prevent Movement from turning it right back off.
|
// this is hacky, we need a way for watch faces to set an arbitrary color and prevent Movement from turning it right back off.
|
||||||
|
movement_state.light_on = true;
|
||||||
watch_set_led_color_rgb(red, green, blue);
|
watch_set_led_color_rgb(red, green, blue);
|
||||||
movement_state.light_ticks = 32767;
|
rtc_counter_t counter = watch_rtc_get_counter();
|
||||||
|
watch_rtc_register_comp_callback(cb_led_timeout_interrupt, counter + 32767, LED_TIMEOUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
void movement_force_led_off(void) {
|
void movement_force_led_off(void) {
|
||||||
|
movement_state.light_on = false;
|
||||||
|
// The led timeout probably already triggered, but still disable just in case we are switching off the light by other means
|
||||||
|
watch_rtc_disable_comp_callback(LED_TIMEOUT);
|
||||||
watch_set_led_off();
|
watch_set_led_off();
|
||||||
movement_state.light_ticks = -1;
|
|
||||||
_movement_disable_fast_tick_if_possible();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool movement_default_loop_handler(movement_event_t event) {
|
bool movement_default_loop_handler(movement_event_t event) {
|
||||||
@@ -315,63 +401,98 @@ void movement_cancel_background_task_for_face(uint8_t watch_face_index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void movement_request_sleep(void) {
|
void movement_request_sleep(void) {
|
||||||
/// FIXME: for #SecondMovement: This was a feature request to allow watch faces to request sleep.
|
movement_volatile_state.enter_sleep_mode = true;
|
||||||
/// Setting the ticks to 1 means the watch will sleep after the next tick. I'd like to say let's
|
|
||||||
/// set it to 0, have the watch face loop return false, and then we'll fall asleep immediately.
|
|
||||||
/// But could this lead to a race condition where the callback decrements to -1 before the loop?
|
|
||||||
/// This is the safest way but consider more testing here.
|
|
||||||
movement_state.le_mode_ticks = 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void movement_request_wake() {
|
void movement_request_wake() {
|
||||||
movement_state.needs_wake = true;
|
movement_volatile_state.exit_sleep_mode = true;
|
||||||
_movement_reset_inactivity_countdown();
|
_movement_reset_inactivity_countdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void end_buzzing() {
|
void cb_buzzer_start(void) {
|
||||||
movement_state.is_buzzing = false;
|
movement_volatile_state.is_buzzing = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void end_buzzing_and_disable_buzzer(void) {
|
void cb_buzzer_stop(void) {
|
||||||
end_buzzing();
|
movement_volatile_state.is_buzzing = false;
|
||||||
watch_disable_buzzer();
|
movement_volatile_state.pending_sequence_priority = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void movement_play_note(watch_buzzer_note_t note, uint16_t duration_ms) {
|
||||||
|
static int8_t single_note_sequence[3];
|
||||||
|
|
||||||
|
single_note_sequence[0] = note;
|
||||||
|
// 48 ticks per second for the tc0?
|
||||||
|
// Each tick is approximately 20ms
|
||||||
|
uint16_t duration = duration_ms / 20;
|
||||||
|
if (duration > 127) duration = 127;
|
||||||
|
single_note_sequence[1] = (int8_t)duration;
|
||||||
|
single_note_sequence[2] = 0;
|
||||||
|
|
||||||
|
movement_play_sequence(single_note_sequence, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void movement_play_signal(void) {
|
void movement_play_signal(void) {
|
||||||
void *maybe_disable_buzzer = end_buzzing_and_disable_buzzer;
|
movement_play_sequence(signal_tune, 1);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void movement_play_alarm(void) {
|
void movement_play_alarm(void) {
|
||||||
movement_play_alarm_beeps(5, BUZZER_NOTE_C8);
|
movement_play_sequence(alarm_tune, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
void movement_play_alarm_beeps(uint8_t rounds, watch_buzzer_note_t alarm_note) {
|
void movement_play_alarm_beeps(uint8_t rounds, watch_buzzer_note_t alarm_note) {
|
||||||
|
// Ugly but necessary to avoid breaking backward compatibility with some faces.
|
||||||
|
// Create an alarm tune on the fly with the specified note and repetition.
|
||||||
|
static int8_t custom_alarm_tune[19];
|
||||||
|
|
||||||
if (rounds == 0) rounds = 1;
|
if (rounds == 0) rounds = 1;
|
||||||
if (rounds > 20) rounds = 20;
|
if (rounds > 20) rounds = 20;
|
||||||
movement_request_wake();
|
|
||||||
movement_state.alarm_note = alarm_note;
|
for (uint8_t i = 0; i < 9; i++) {
|
||||||
// our tone is 0.375 seconds of beep and 0.625 of silence, repeated as given.
|
uint8_t note_idx = i * 2;
|
||||||
movement_state.alarm_ticks = 128 * rounds - 75;
|
uint8_t duration_idx = note_idx + 1;
|
||||||
_movement_enable_fast_tick_if_needed();
|
|
||||||
|
int8_t note = alarm_tune[note_idx];
|
||||||
|
int8_t duration = alarm_tune[duration_idx];
|
||||||
|
|
||||||
|
if (note == BUZZER_NOTE_C8) {
|
||||||
|
note = alarm_note;
|
||||||
|
} else if (note < 0) {
|
||||||
|
duration = rounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
custom_alarm_tune[note_idx] = note;
|
||||||
|
custom_alarm_tune[duration_idx] = duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
custom_alarm_tune[18] = 0;
|
||||||
|
|
||||||
|
movement_play_sequence(custom_alarm_tune, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
void movement_play_sequence(int8_t *note_sequence, uint8_t priority) {
|
||||||
|
// Priority is used to ensure that lower priority sequences don't cancel higher priority ones
|
||||||
|
// Priotity order: alarm(2) > signal(1) > note(0)
|
||||||
|
if (priority < movement_volatile_state.pending_sequence_priority) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
movement_volatile_state.pending_sequence_priority = priority;
|
||||||
|
|
||||||
|
// The tcc is off during sleep, we can't play immediately.
|
||||||
|
// Ask to wake up the watch.
|
||||||
|
if (movement_volatile_state.is_sleeping) {
|
||||||
|
_pending_sequence = note_sequence;
|
||||||
|
movement_volatile_state.has_pending_sequence = true;
|
||||||
|
movement_volatile_state.exit_sleep_mode = true;
|
||||||
|
} else {
|
||||||
|
watch_buzzer_play_sequence_with_volume(note_sequence, NULL, movement_button_volume());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t movement_claim_backup_register(void) {
|
uint8_t movement_claim_backup_register(void) {
|
||||||
if (movement_state.next_available_backup_register >= 8) return 0;
|
// We use backup register 7 in watch_rtc to keep track of the reference time
|
||||||
|
if (movement_state.next_available_backup_register >= 7) return 0;
|
||||||
return movement_state.next_available_backup_register++;
|
return movement_state.next_available_backup_register++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,18 +526,20 @@ watch_date_time_t movement_get_utc_date_time(void) {
|
|||||||
|
|
||||||
watch_date_time_t movement_get_date_time_in_zone(uint8_t zone_index) {
|
watch_date_time_t movement_get_date_time_in_zone(uint8_t zone_index) {
|
||||||
int32_t offset = movement_get_current_timezone_offset_for_zone(zone_index);
|
int32_t offset = movement_get_current_timezone_offset_for_zone(zone_index);
|
||||||
return watch_utility_date_time_convert_zone(watch_rtc_get_date_time(), 0, offset);
|
unix_timestamp_t timestamp = watch_rtc_get_unix_time();
|
||||||
|
return watch_utility_date_time_from_unix_time(timestamp, offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
watch_date_time_t movement_get_local_date_time(void) {
|
watch_date_time_t movement_get_local_date_time(void) {
|
||||||
watch_date_time_t date_time = watch_rtc_get_date_time();
|
unix_timestamp_t timestamp = watch_rtc_get_unix_time();
|
||||||
return watch_utility_date_time_convert_zone(date_time, 0, movement_get_current_timezone_offset());
|
return watch_utility_date_time_from_unix_time(timestamp, movement_get_current_timezone_offset());
|
||||||
}
|
}
|
||||||
|
|
||||||
void movement_set_local_date_time(watch_date_time_t date_time) {
|
void movement_set_utc_date_time(watch_date_time_t date_time) {
|
||||||
int32_t current_offset = movement_get_current_timezone_offset();
|
watch_rtc_set_date_time(date_time);
|
||||||
watch_date_time_t utc_date_time = watch_utility_date_time_convert_zone(date_time, current_offset, 0);
|
|
||||||
watch_rtc_set_date_time(utc_date_time);
|
// If the time was changed, the top of the minute alarm needs to be reset accordingly
|
||||||
|
_movement_set_top_of_minute_alarm();
|
||||||
|
|
||||||
// this may seem wasteful, but if the user's local time is in a zone that observes DST,
|
// this may seem wasteful, but if the user's local time is in a zone that observes DST,
|
||||||
// they may have just crossed a DST boundary, which means the next call to this function
|
// they may have just crossed a DST boundary, which means the next call to this function
|
||||||
@@ -424,6 +547,12 @@ void movement_set_local_date_time(watch_date_time_t date_time) {
|
|||||||
_movement_update_dst_offset_cache();
|
_movement_update_dst_offset_cache();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void movement_set_local_date_time(watch_date_time_t date_time) {
|
||||||
|
int32_t current_offset = movement_get_current_timezone_offset();
|
||||||
|
watch_date_time_t utc_date_time = watch_utility_date_time_convert_zone(date_time, current_offset, 0);
|
||||||
|
movement_set_utc_date_time(utc_date_time);
|
||||||
|
}
|
||||||
|
|
||||||
bool movement_button_should_sound(void) {
|
bool movement_button_should_sound(void) {
|
||||||
return movement_state.settings.bit.button_should_sound;
|
return movement_state.settings.bit.button_should_sound;
|
||||||
}
|
}
|
||||||
@@ -625,6 +754,38 @@ void app_init(void) {
|
|||||||
|
|
||||||
memset((void *)&movement_state, 0, sizeof(movement_state));
|
memset((void *)&movement_state, 0, sizeof(movement_state));
|
||||||
|
|
||||||
|
movement_volatile_state.pending_events = 0;
|
||||||
|
movement_volatile_state.turn_led_off = false;
|
||||||
|
|
||||||
|
movement_volatile_state.minute_alarm_fired = false;
|
||||||
|
movement_volatile_state.minute_counter = 0;
|
||||||
|
|
||||||
|
movement_volatile_state.enter_sleep_mode = false;
|
||||||
|
movement_volatile_state.exit_sleep_mode = false;
|
||||||
|
movement_volatile_state.has_pending_sequence = false;
|
||||||
|
movement_volatile_state.is_sleeping = false;
|
||||||
|
|
||||||
|
movement_volatile_state.is_buzzing = false;
|
||||||
|
movement_volatile_state.pending_sequence_priority = 0;
|
||||||
|
|
||||||
|
movement_volatile_state.mode_button.down_event = EVENT_MODE_BUTTON_DOWN;
|
||||||
|
movement_volatile_state.mode_button.is_down = false;
|
||||||
|
movement_volatile_state.mode_button.down_timestamp = 0;
|
||||||
|
movement_volatile_state.mode_button.timeout_index = MODE_BUTTON_TIMEOUT;
|
||||||
|
movement_volatile_state.mode_button.cb_longpress = cb_mode_btn_timeout_interrupt;
|
||||||
|
|
||||||
|
movement_volatile_state.light_button.down_event = EVENT_LIGHT_BUTTON_DOWN;
|
||||||
|
movement_volatile_state.light_button.is_down = false;
|
||||||
|
movement_volatile_state.light_button.down_timestamp = 0;
|
||||||
|
movement_volatile_state.light_button.timeout_index = LIGHT_BUTTON_TIMEOUT;
|
||||||
|
movement_volatile_state.light_button.cb_longpress = cb_light_btn_timeout_interrupt;
|
||||||
|
|
||||||
|
movement_volatile_state.alarm_button.down_event = EVENT_ALARM_BUTTON_DOWN;
|
||||||
|
movement_volatile_state.alarm_button.is_down = false;
|
||||||
|
movement_volatile_state.alarm_button.down_timestamp = 0;
|
||||||
|
movement_volatile_state.alarm_button.timeout_index = ALARM_BUTTON_TIMEOUT;
|
||||||
|
movement_volatile_state.alarm_button.cb_longpress = cb_alarm_btn_timeout_interrupt;
|
||||||
|
|
||||||
movement_state.has_thermistor = thermistor_driver_init();
|
movement_state.has_thermistor = thermistor_driver_init();
|
||||||
|
|
||||||
bool settings_file_exists = filesystem_file_exists("settings.u32");
|
bool settings_file_exists = filesystem_file_exists("settings.u32");
|
||||||
@@ -680,13 +841,19 @@ void app_init(void) {
|
|||||||
watch_rtc_set_date_time(date_time);
|
watch_rtc_set_date_time(date_time);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set up the 1 minute alarm (for background tasks and low power updates)
|
||||||
|
_movement_set_top_of_minute_alarm();
|
||||||
|
|
||||||
|
// register callbacks to be notified when buzzer starts/stops playing.
|
||||||
|
// this is so movement can be notified even when triggered by a face bypassing movement
|
||||||
|
watch_buzzer_register_global_callbacks(cb_buzzer_start, cb_buzzer_stop);
|
||||||
|
|
||||||
// populate the DST offset cache
|
// populate the DST offset cache
|
||||||
_movement_update_dst_offset_cache();
|
_movement_update_dst_offset_cache();
|
||||||
|
|
||||||
if (movement_state.accelerometer_motion_threshold == 0) movement_state.accelerometer_motion_threshold = 32;
|
if (movement_state.accelerometer_motion_threshold == 0) movement_state.accelerometer_motion_threshold = 32;
|
||||||
|
|
||||||
movement_state.light_ticks = -1;
|
movement_state.light_on = false;
|
||||||
movement_state.alarm_ticks = -1;
|
|
||||||
movement_state.next_available_backup_register = 2;
|
movement_state.next_available_backup_register = 2;
|
||||||
_movement_reset_inactivity_countdown();
|
_movement_reset_inactivity_countdown();
|
||||||
}
|
}
|
||||||
@@ -721,17 +888,12 @@ void app_setup(void) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// set up the 1 minute alarm (for background tasks and low power updates)
|
|
||||||
watch_date_time_t alarm_time;
|
|
||||||
alarm_time.reg = 0;
|
|
||||||
watch_rtc_register_alarm_callback(cb_alarm_fired, alarm_time, ALARM_MATCH_SS);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LCD autodetect uses the buttons as a a failsafe, so we should run it before we enable the button interrupts
|
// LCD autodetect uses the buttons as a a failsafe, so we should run it before we enable the button interrupts
|
||||||
watch_enable_display();
|
watch_enable_display();
|
||||||
|
|
||||||
if (movement_state.le_mode_ticks != -1) {
|
if (!movement_volatile_state.is_sleeping) {
|
||||||
watch_disable_extwake_interrupt(HAL_GPIO_BTN_ALARM_pin());
|
watch_disable_extwake_interrupt(HAL_GPIO_BTN_ALARM_pin());
|
||||||
|
|
||||||
watch_enable_external_interrupts();
|
watch_enable_external_interrupts();
|
||||||
@@ -808,149 +970,185 @@ void app_setup(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watch_faces[movement_state.current_face_idx].activate(watch_face_contexts[movement_state.current_face_idx]);
|
watch_faces[movement_state.current_face_idx].activate(watch_face_contexts[movement_state.current_face_idx]);
|
||||||
event.subsecond = 0;
|
movement_volatile_state.pending_events |= 1 << EVENT_ACTIVATE;
|
||||||
event.event_type = EVENT_ACTIVATE;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifndef MOVEMENT_LOW_ENERGY_MODE_FORBIDDEN
|
#ifndef MOVEMENT_LOW_ENERGY_MODE_FORBIDDEN
|
||||||
|
|
||||||
static void _sleep_mode_app_loop(void) {
|
static void _sleep_mode_app_loop(void) {
|
||||||
movement_state.needs_wake = false;
|
// as long as we are in low energy mode, we wake up here, update the screen, and go right back to sleep.
|
||||||
// as long as le_mode_ticks is -1 (i.e. we are in low energy mode), we wake up here, update the screen, and go right back to sleep.
|
while (movement_volatile_state.is_sleeping) {
|
||||||
while (movement_state.le_mode_ticks == -1) {
|
// if we need to wake immediately, do it!
|
||||||
// we also have to handle top-of-the-minute tasks here in the mini-runloop
|
if (movement_volatile_state.exit_sleep_mode) {
|
||||||
if (movement_state.woke_from_alarm_handler) _movement_handle_top_of_minute();
|
movement_volatile_state.exit_sleep_mode = false;
|
||||||
|
movement_volatile_state.is_sleeping = false;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we also have to handle top-of-the-minute tasks here in the mini-runloop
|
||||||
|
if (movement_volatile_state.minute_alarm_fired) {
|
||||||
|
movement_volatile_state.minute_alarm_fired = false;
|
||||||
|
_movement_handle_top_of_minute();
|
||||||
|
}
|
||||||
|
|
||||||
|
movement_event_t event;
|
||||||
event.event_type = EVENT_LOW_ENERGY_UPDATE;
|
event.event_type = EVENT_LOW_ENERGY_UPDATE;
|
||||||
|
event.subsecond = 0;
|
||||||
watch_faces[movement_state.current_face_idx].loop(event, watch_face_contexts[movement_state.current_face_idx]);
|
watch_faces[movement_state.current_face_idx].loop(event, watch_face_contexts[movement_state.current_face_idx]);
|
||||||
|
|
||||||
// if we need to wake immediately, do it!
|
// If any of the previous loops requested to wake up, do it!
|
||||||
if (movement_state.needs_wake) return;
|
if (movement_volatile_state.exit_sleep_mode) {
|
||||||
// otherwise enter sleep mode, and when the extwake handler is called, it will reset le_mode_ticks and force us out at the next loop.
|
movement_volatile_state.exit_sleep_mode = false;
|
||||||
else watch_enter_sleep_mode();
|
movement_volatile_state.is_sleeping = false;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise enter sleep mode, until either the top of the minute interrupt or extwake wakes us up.
|
||||||
|
watch_enter_sleep_mode();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool app_loop(void) {
|
static bool _switch_face(void) {
|
||||||
const watch_face_t *wf = &watch_faces[movement_state.current_face_idx];
|
const watch_face_t *wf = &watch_faces[movement_state.current_face_idx];
|
||||||
bool woke_up_for_buzzer = false;
|
|
||||||
|
|
||||||
if (movement_state.watch_face_changed) {
|
|
||||||
if (movement_state.settings.bit.button_should_sound) {
|
|
||||||
// low note for nonzero case, high note for return to watch_face 0
|
|
||||||
watch_buzzer_play_note_with_volume(movement_state.next_face_idx ? BUZZER_NOTE_C7 : BUZZER_NOTE_C8, 50, movement_state.settings.bit.button_volume);
|
|
||||||
}
|
|
||||||
wf->resign(watch_face_contexts[movement_state.current_face_idx]);
|
wf->resign(watch_face_contexts[movement_state.current_face_idx]);
|
||||||
movement_state.current_face_idx = movement_state.next_face_idx;
|
movement_state.current_face_idx = movement_state.next_face_idx;
|
||||||
// we have just updated the face idx, so we must recache the watch face pointer.
|
// we have just updated the face idx, so we must recache the watch face pointer.
|
||||||
wf = &watch_faces[movement_state.current_face_idx];
|
wf = &watch_faces[movement_state.current_face_idx];
|
||||||
watch_clear_display();
|
watch_clear_display();
|
||||||
movement_request_tick_frequency(1);
|
movement_request_tick_frequency(1);
|
||||||
|
|
||||||
|
if (movement_state.settings.bit.button_should_sound) {
|
||||||
|
// low note for nonzero case, high note for return to watch_face 0
|
||||||
|
movement_play_note(movement_state.next_face_idx ? BUZZER_NOTE_C7 : BUZZER_NOTE_C8, 50);
|
||||||
|
}
|
||||||
|
|
||||||
wf->activate(watch_face_contexts[movement_state.current_face_idx]);
|
wf->activate(watch_face_contexts[movement_state.current_face_idx]);
|
||||||
|
|
||||||
|
movement_event_t event;
|
||||||
event.subsecond = 0;
|
event.subsecond = 0;
|
||||||
event.event_type = EVENT_ACTIVATE;
|
event.event_type = EVENT_ACTIVATE;
|
||||||
movement_state.watch_face_changed = false;
|
movement_state.watch_face_changed = false;
|
||||||
|
bool can_sleep = wf->loop(event, watch_face_contexts[movement_state.current_face_idx]);
|
||||||
|
|
||||||
|
return can_sleep;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the LED should be off, turn it off
|
bool app_loop(void) {
|
||||||
if (movement_state.light_ticks == 0) {
|
const watch_face_t *wf = &watch_faces[movement_state.current_face_idx];
|
||||||
// unless the user is holding down the LIGHT button, in which case, give them more time.
|
|
||||||
if (HAL_GPIO_BTN_LIGHT_read()) {
|
|
||||||
movement_state.light_ticks = 1;
|
|
||||||
} else {
|
|
||||||
movement_force_led_off();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle top-of-minute tasks, if the alarm handler told us we need to
|
|
||||||
if (movement_state.woke_from_alarm_handler) _movement_handle_top_of_minute();
|
|
||||||
|
|
||||||
// if we have a scheduled background task, handle that here:
|
|
||||||
if (event.event_type == EVENT_TICK && movement_state.has_scheduled_background_task) _movement_handle_scheduled_tasks();
|
|
||||||
|
|
||||||
#ifndef MOVEMENT_LOW_ENERGY_MODE_FORBIDDEN
|
|
||||||
// if we have timed out of our low energy mode countdown, enter low energy mode.
|
|
||||||
if (movement_state.le_mode_ticks == 0) {
|
|
||||||
movement_state.le_mode_ticks = -1;
|
|
||||||
watch_register_extwake_callback(HAL_GPIO_BTN_ALARM_pin(), cb_alarm_btn_extwake, true);
|
|
||||||
event.event_type = EVENT_NONE;
|
|
||||||
event.subsecond = 0;
|
|
||||||
|
|
||||||
// _sleep_mode_app_loop takes over at this point and loops until le_mode_ticks is reset by the extwake handler,
|
|
||||||
// or wake is requested using the movement_request_wake function.
|
|
||||||
_sleep_mode_app_loop();
|
|
||||||
// as soon as _sleep_mode_app_loop returns, we prepare to reactivate
|
|
||||||
// ourselves, but first, we check to see if we woke up for the buzzer:
|
|
||||||
if (movement_state.is_buzzing) {
|
|
||||||
woke_up_for_buzzer = true;
|
|
||||||
}
|
|
||||||
event.event_type = EVENT_ACTIVATE;
|
|
||||||
// this is a hack tho: waking from sleep mode, app_setup does get called, but it happens before we have reset our ticks.
|
|
||||||
// need to figure out if there's a better heuristic for determining how we woke up.
|
|
||||||
app_setup();
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// default to being allowed to sleep by the face.
|
// default to being allowed to sleep by the face.
|
||||||
bool can_sleep = true;
|
bool can_sleep = true;
|
||||||
|
|
||||||
if (event.event_type) {
|
// Any events that have been added by the various interrupts in between app_loop invokations
|
||||||
event.subsecond = movement_state.subsecond;
|
uint32_t pending_events = movement_volatile_state.pending_events;
|
||||||
// the first trip through the loop overrides the can_sleep state
|
movement_volatile_state.pending_events = 0;
|
||||||
can_sleep = wf->loop(event, watch_face_contexts[movement_state.current_face_idx]);
|
|
||||||
|
|
||||||
// Keep light on if user is still interacting with the watch.
|
movement_event_t event;
|
||||||
if (movement_state.light_ticks > 0) {
|
event.event_type = EVENT_NONE;
|
||||||
switch (event.event_type) {
|
// Subsecond is determined by the TICK event, if concurrent events have happened,
|
||||||
case EVENT_LIGHT_BUTTON_DOWN:
|
// they will all have the same subsecond as they should to keep backward compatibility.
|
||||||
case EVENT_MODE_BUTTON_DOWN:
|
event.subsecond = movement_volatile_state.subsecond;
|
||||||
case EVENT_ALARM_BUTTON_DOWN:
|
|
||||||
|
// if the LED should be off, turn it off
|
||||||
|
if (movement_volatile_state.turn_led_off) {
|
||||||
|
// unless the user is holding down the LIGHT button, in which case, give them more time.
|
||||||
|
if (movement_volatile_state.light_button.is_down) {
|
||||||
|
} else {
|
||||||
|
movement_volatile_state.turn_led_off = false;
|
||||||
|
movement_force_led_off();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// actually play the note sequence we were asked to play while in deep sleep.
|
||||||
|
if (movement_volatile_state.has_pending_sequence) {
|
||||||
|
movement_volatile_state.has_pending_sequence = false;
|
||||||
|
watch_buzzer_play_sequence_with_volume(_pending_sequence, movement_request_sleep, movement_button_volume());
|
||||||
|
// When this sequence is done playing, movement_request_sleep is invoked and the watch will go,
|
||||||
|
// back to sleep (unless the user interacts with it in the meantime)
|
||||||
|
_pending_sequence = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle top-of-minute tasks, if the alarm handler told us we need to
|
||||||
|
if (movement_volatile_state.minute_alarm_fired) {
|
||||||
|
movement_volatile_state.minute_alarm_fired = false;
|
||||||
|
_movement_handle_top_of_minute();
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we have a scheduled background task, handle that here:
|
||||||
|
if (
|
||||||
|
(pending_events & (1 << EVENT_TICK))
|
||||||
|
&& event.subsecond == 0
|
||||||
|
&& movement_state.has_scheduled_background_task
|
||||||
|
) {
|
||||||
|
_movement_handle_scheduled_tasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delay auto light off if the user is still interacting with the watch.
|
||||||
|
if (movement_state.light_on) {
|
||||||
|
if (pending_events & (
|
||||||
|
(1 << EVENT_LIGHT_BUTTON_DOWN) |
|
||||||
|
(1 << EVENT_MODE_BUTTON_DOWN) |
|
||||||
|
(1 << EVENT_ALARM_BUTTON_DOWN)
|
||||||
|
)) {
|
||||||
movement_illuminate_led();
|
movement_illuminate_led();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
event.event_type = EVENT_NONE;
|
// Pop the EVENT_TIMEOUT out of the pending_events so it can be handled separately
|
||||||
|
bool resign_timeout = (pending_events & (1 << EVENT_TIMEOUT)) != 0;
|
||||||
|
if (resign_timeout) {
|
||||||
|
pending_events &= ~(1 << EVENT_TIMEOUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we have timed out of our timeout countdown, give the app a hint that they can resign.
|
// Consume all the pending events
|
||||||
if (movement_state.timeout_ticks == 0 && movement_state.current_face_idx != 0) {
|
movement_event_type_t event_type = 0;
|
||||||
movement_state.timeout_ticks = -1;
|
while (pending_events) {
|
||||||
|
if (pending_events & 1) {
|
||||||
|
event.event_type = event_type;
|
||||||
|
can_sleep = wf->loop(event, watch_face_contexts[movement_state.current_face_idx]) && can_sleep;
|
||||||
|
}
|
||||||
|
pending_events = pending_events >> 1;
|
||||||
|
event_type++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now handle the EVENT_TIMEOUT
|
||||||
|
if (resign_timeout && movement_state.current_face_idx != 0) {
|
||||||
event.event_type = EVENT_TIMEOUT;
|
event.event_type = EVENT_TIMEOUT;
|
||||||
event.subsecond = movement_state.subsecond;
|
can_sleep = wf->loop(event, watch_face_contexts[movement_state.current_face_idx]) && can_sleep;
|
||||||
// if we run through the loop again to time out, we need to reconsider whether or not we can sleep.
|
|
||||||
// if the first trip said true, but this trip said false, we need the false to override, thus
|
|
||||||
// we will be using boolean AND:
|
|
||||||
//
|
|
||||||
// first trip | can sleep | cannot sleep | can sleep | cannot sleep
|
|
||||||
// second trip | can sleep | cannot sleep | cannot sleep | can sleep
|
|
||||||
// && | can sleep | cannot sleep | cannot sleep | cannot sleep
|
|
||||||
bool can_sleep2 = wf->loop(event, watch_face_contexts[movement_state.current_face_idx]);
|
|
||||||
can_sleep = can_sleep && can_sleep2;
|
|
||||||
event.event_type = EVENT_NONE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now that we've handled all display update tasks, handle the alarm.
|
// The watch_face_changed flag might be set again by the face loop, so check it again
|
||||||
if (movement_state.alarm_ticks >= 0) {
|
if (movement_state.watch_face_changed) {
|
||||||
uint8_t buzzer_phase = (movement_state.alarm_ticks + 80) % 128;
|
can_sleep = _switch_face() && can_sleep;
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifndef MOVEMENT_LOW_ENERGY_MODE_FORBIDDEN
|
||||||
|
// if we have timed out of our low energy mode countdown, enter low energy mode.
|
||||||
|
if (movement_volatile_state.enter_sleep_mode && !movement_volatile_state.is_buzzing) {
|
||||||
|
movement_volatile_state.enter_sleep_mode = false;
|
||||||
|
movement_volatile_state.is_sleeping = true;
|
||||||
|
|
||||||
|
// No need to fire resign and sleep interrupts while in sleep mode
|
||||||
|
_movement_disable_inactivity_countdown();
|
||||||
|
|
||||||
|
watch_register_extwake_callback(HAL_GPIO_BTN_ALARM_pin(), cb_alarm_btn_extwake, true);
|
||||||
|
|
||||||
|
// _sleep_mode_app_loop takes over at this point and loops until exit_sleep_mode is set by the extwake handler,
|
||||||
|
// or wake is requested using the movement_request_wake function.
|
||||||
|
_sleep_mode_app_loop();
|
||||||
|
// as soon as _sleep_mode_app_loop returns, we prepare to reactivate
|
||||||
|
|
||||||
|
// // this is a hack tho: waking from sleep mode, app_setup does get called, but it happens before we have reset our ticks.
|
||||||
|
// // need to figure out if there's a better heuristic for determining how we woke up.
|
||||||
|
app_setup();
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#if __EMSCRIPTEN__
|
#if __EMSCRIPTEN__
|
||||||
shell_task();
|
shell_task();
|
||||||
@@ -961,19 +1159,6 @@ bool app_loop(void) {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
event.subsecond = 0;
|
|
||||||
|
|
||||||
// if the watch face changed, we can't sleep because we need to update the display.
|
|
||||||
if (movement_state.watch_face_changed) can_sleep = false;
|
|
||||||
|
|
||||||
// if we woke up for the buzzer, stay awake until it's finished.
|
|
||||||
if (woke_up_for_buzzer) {
|
|
||||||
while(watch_is_buzzer_or_led_enabled());
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the LED is on, we need to stay awake to keep the TCC running.
|
|
||||||
if (movement_state.light_ticks != -1) can_sleep = false;
|
|
||||||
|
|
||||||
// if we are plugged into USB, we can't sleep because we need to keep the serial shell running.
|
// if we are plugged into USB, we can't sleep because we need to keep the serial shell running.
|
||||||
if (usb_is_enabled()) {
|
if (usb_is_enabled()) {
|
||||||
yield();
|
yield();
|
||||||
@@ -983,114 +1168,152 @@ bool app_loop(void) {
|
|||||||
return can_sleep;
|
return can_sleep;
|
||||||
}
|
}
|
||||||
|
|
||||||
static movement_event_type_t _figure_out_button_event(bool pin_level, movement_event_type_t button_down_event_type, volatile uint16_t *down_timestamp) {
|
static movement_event_type_t _process_button_event(bool pin_level, movement_button_t* button) {
|
||||||
// force alarm off if the user pressed a button.
|
// This shouldn't happen normally
|
||||||
if (movement_state.alarm_ticks) movement_state.alarm_ticks = 0;
|
if (pin_level == button->is_down) {
|
||||||
|
return EVENT_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t counter = watch_rtc_get_counter();
|
||||||
|
|
||||||
|
button->is_down = pin_level;
|
||||||
|
|
||||||
if (pin_level) {
|
if (pin_level) {
|
||||||
// handle rising edge
|
// We schedule a timeout to fire the longpress event
|
||||||
_movement_enable_fast_tick_if_needed();
|
button->down_timestamp = counter;
|
||||||
*down_timestamp = movement_state.fast_ticks + 1;
|
watch_rtc_register_comp_callback(button->cb_longpress, counter + MOVEMENT_LONG_PRESS_TICKS, button->timeout_index);
|
||||||
return button_down_event_type;
|
// force alarm off if the user pressed a button.
|
||||||
|
watch_buzzer_abort_sequence();
|
||||||
|
return button->down_event;
|
||||||
} else {
|
} else {
|
||||||
// this line is hack but it handles the situation where the light button was held for more than 20 seconds.
|
// We cancel the timeout if it hasn't fired yet
|
||||||
// fast tick is disabled by then, and the LED would get stuck on since there's no one left decrementing light_ticks.
|
watch_rtc_disable_comp_callback(button->timeout_index);
|
||||||
if (movement_state.light_ticks == 1) movement_state.light_ticks = 0;
|
if ((counter - button->down_timestamp) >= MOVEMENT_LONG_PRESS_TICKS) {
|
||||||
// now that that's out of the way, handle falling edge
|
return button->down_event + 3;
|
||||||
uint16_t diff = movement_state.fast_ticks - *down_timestamp;
|
} else {
|
||||||
*down_timestamp = 0;
|
return button->down_event + 1;
|
||||||
_movement_disable_fast_tick_if_possible();
|
}
|
||||||
// any press over a half second is considered a long press. Fire the long-up event
|
|
||||||
if (diff > MOVEMENT_LONG_PRESS_TICKS) return button_down_event_type + 3;
|
|
||||||
else return button_down_event_type + 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void cb_light_btn_interrupt(void) {
|
void cb_light_btn_interrupt(void) {
|
||||||
bool pin_level = HAL_GPIO_BTN_LIGHT_read();
|
bool pin_level = HAL_GPIO_BTN_LIGHT_read();
|
||||||
|
|
||||||
|
movement_volatile_state.pending_events |= 1 << _process_button_event(pin_level, &movement_volatile_state.light_button);
|
||||||
|
|
||||||
_movement_reset_inactivity_countdown();
|
_movement_reset_inactivity_countdown();
|
||||||
event.event_type = _figure_out_button_event(pin_level, EVENT_LIGHT_BUTTON_DOWN, &movement_state.light_down_timestamp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void cb_mode_btn_interrupt(void) {
|
void cb_mode_btn_interrupt(void) {
|
||||||
bool pin_level = HAL_GPIO_BTN_MODE_read();
|
bool pin_level = HAL_GPIO_BTN_MODE_read();
|
||||||
|
|
||||||
|
movement_volatile_state.pending_events |= 1 << _process_button_event(pin_level, &movement_volatile_state.mode_button);
|
||||||
|
|
||||||
_movement_reset_inactivity_countdown();
|
_movement_reset_inactivity_countdown();
|
||||||
event.event_type = _figure_out_button_event(pin_level, EVENT_MODE_BUTTON_DOWN, &movement_state.mode_down_timestamp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void cb_alarm_btn_interrupt(void) {
|
void cb_alarm_btn_interrupt(void) {
|
||||||
bool pin_level = HAL_GPIO_BTN_ALARM_read();
|
bool pin_level = HAL_GPIO_BTN_ALARM_read();
|
||||||
|
|
||||||
|
movement_volatile_state.pending_events |= 1 << _process_button_event(pin_level, &movement_volatile_state.alarm_button);
|
||||||
|
|
||||||
_movement_reset_inactivity_countdown();
|
_movement_reset_inactivity_countdown();
|
||||||
event.event_type = _figure_out_button_event(pin_level, EVENT_ALARM_BUTTON_DOWN, &movement_state.alarm_down_timestamp);
|
}
|
||||||
|
|
||||||
|
static movement_event_type_t _process_button_longpress_timeout(movement_button_t* button) {
|
||||||
|
// Looks like all these checks are not needed for the longpress detection to work reliably.
|
||||||
|
// Keep the code around for now in case problems arise long-term.
|
||||||
|
|
||||||
|
// if (!button->is_down) {
|
||||||
|
// return EVENT_NONE;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// movement_event_type_t up_event = button->down_event + 1;
|
||||||
|
|
||||||
|
// if (movement_volatile_state.pending_events & 1 << up_event) {
|
||||||
|
// return EVENT_NONE;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// uint32_t counter = watch_rtc_get_counter();
|
||||||
|
// if ((counter - button->down_timestamp) < MOVEMENT_LONG_PRESS_TICKS) {
|
||||||
|
// return EVENT_NONE;
|
||||||
|
// }
|
||||||
|
|
||||||
|
movement_event_type_t longpress_event = button->down_event + 2;
|
||||||
|
|
||||||
|
return longpress_event;
|
||||||
|
}
|
||||||
|
|
||||||
|
void cb_light_btn_timeout_interrupt(void) {
|
||||||
|
movement_button_t* button = &movement_volatile_state.light_button;
|
||||||
|
|
||||||
|
movement_volatile_state.pending_events |= 1 << _process_button_longpress_timeout(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cb_mode_btn_timeout_interrupt(void) {
|
||||||
|
movement_button_t* button = &movement_volatile_state.mode_button;
|
||||||
|
|
||||||
|
movement_volatile_state.pending_events |= 1 << _process_button_longpress_timeout(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cb_alarm_btn_timeout_interrupt(void) {
|
||||||
|
movement_button_t* button = &movement_volatile_state.alarm_button;
|
||||||
|
|
||||||
|
movement_volatile_state.pending_events |= 1 << _process_button_longpress_timeout(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cb_led_timeout_interrupt(void) {
|
||||||
|
movement_volatile_state.turn_led_off = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void cb_resign_timeout_interrupt(void) {
|
||||||
|
movement_volatile_state.pending_events |= 1 << EVENT_TIMEOUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
void cb_sleep_timeout_interrupt(void) {
|
||||||
|
movement_request_sleep();
|
||||||
}
|
}
|
||||||
|
|
||||||
void cb_alarm_btn_extwake(void) {
|
void cb_alarm_btn_extwake(void) {
|
||||||
// wake up!
|
// wake up!
|
||||||
_movement_reset_inactivity_countdown();
|
movement_request_wake();
|
||||||
}
|
}
|
||||||
|
|
||||||
void cb_alarm_fired(void) {
|
void cb_minute_alarm_fired(void) {
|
||||||
|
movement_volatile_state.minute_alarm_fired = true;
|
||||||
|
|
||||||
#if __EMSCRIPTEN__
|
#if __EMSCRIPTEN__
|
||||||
_wake_up_simulator();
|
_wake_up_simulator();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
movement_state.woke_from_alarm_handler = true;
|
// Renew the alarm for a minute from the previous one (ensures no drift)
|
||||||
}
|
movement_volatile_state.minute_counter += watch_rtc_get_ticks_per_minute();
|
||||||
|
watch_rtc_register_comp_callback(cb_minute_alarm_fired, movement_volatile_state.minute_counter, MINUTE_TIMEOUT);
|
||||||
void cb_fast_tick(void) {
|
|
||||||
movement_state.fast_ticks++;
|
|
||||||
if (movement_state.light_ticks > 0) movement_state.light_ticks--;
|
|
||||||
if (movement_state.alarm_ticks > 0) movement_state.alarm_ticks--;
|
|
||||||
// check timestamps and auto-fire the long-press events
|
|
||||||
// Notice: is it possible that two or more buttons have an identical timestamp? In this case
|
|
||||||
// only one of these buttons would receive the long press event. Don't bother for now...
|
|
||||||
if (movement_state.light_down_timestamp > 0)
|
|
||||||
if (movement_state.fast_ticks - movement_state.light_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1)
|
|
||||||
event.event_type = EVENT_LIGHT_LONG_PRESS;
|
|
||||||
if (movement_state.mode_down_timestamp > 0)
|
|
||||||
if (movement_state.fast_ticks - movement_state.mode_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1)
|
|
||||||
event.event_type = EVENT_MODE_LONG_PRESS;
|
|
||||||
if (movement_state.alarm_down_timestamp > 0)
|
|
||||||
if (movement_state.fast_ticks - movement_state.alarm_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1)
|
|
||||||
event.event_type = EVENT_ALARM_LONG_PRESS;
|
|
||||||
// this is just a fail-safe; fast tick should be disabled as soon as the button is up, the LED times out, and/or the alarm finishes.
|
|
||||||
// but if for whatever reason it isn't, this forces the fast tick off after 20 seconds.
|
|
||||||
if (movement_state.fast_ticks >= 128 * 20) {
|
|
||||||
watch_rtc_disable_periodic_callback(128);
|
|
||||||
movement_state.fast_tick_enabled = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void cb_tick(void) {
|
void cb_tick(void) {
|
||||||
event.event_type = EVENT_TICK;
|
rtc_counter_t counter = watch_rtc_get_counter();
|
||||||
watch_date_time_t date_time = watch_rtc_get_date_time();
|
uint32_t freq = watch_rtc_get_frequency();
|
||||||
if (date_time.unit.second != movement_state.last_second) {
|
uint32_t subsecond_mask = freq - 1;
|
||||||
// TODO: can we consolidate these two ticks?
|
movement_volatile_state.pending_events |= 1 << EVENT_TICK;
|
||||||
if (movement_state.le_mode_ticks > 0) movement_state.le_mode_ticks--;
|
movement_volatile_state.subsecond = (counter & subsecond_mask) >> movement_state.tick_pern;
|
||||||
if (movement_state.timeout_ticks > 0) movement_state.timeout_ticks--;
|
|
||||||
|
|
||||||
movement_state.last_second = date_time.unit.second;
|
|
||||||
movement_state.subsecond = 0;
|
|
||||||
} else {
|
|
||||||
movement_state.subsecond++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void cb_accelerometer_event(void) {
|
void cb_accelerometer_event(void) {
|
||||||
uint8_t int_src = lis2dw_get_interrupt_source();
|
uint8_t int_src = lis2dw_get_interrupt_source();
|
||||||
|
|
||||||
if (int_src & LIS2DW_REG_ALL_INT_SRC_DOUBLE_TAP) {
|
if (int_src & LIS2DW_REG_ALL_INT_SRC_DOUBLE_TAP) {
|
||||||
event.event_type = EVENT_DOUBLE_TAP;
|
movement_volatile_state.pending_events |= 1 << EVENT_DOUBLE_TAP;
|
||||||
printf("Double tap!\n");
|
printf("Double tap!\n");
|
||||||
}
|
}
|
||||||
if (int_src & LIS2DW_REG_ALL_INT_SRC_SINGLE_TAP) {
|
if (int_src & LIS2DW_REG_ALL_INT_SRC_SINGLE_TAP) {
|
||||||
event.event_type = EVENT_SINGLE_TAP;
|
movement_volatile_state.pending_events |= 1 << EVENT_SINGLE_TAP;
|
||||||
printf("Single tap!\n");
|
printf("Single tap!\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void cb_accelerometer_wake(void) {
|
void cb_accelerometer_wake(void) {
|
||||||
event.event_type = EVENT_ACCELEROMETER_WAKE;
|
movement_volatile_state.pending_events |= 1 << EVENT_ACCELEROMETER_WAKE;
|
||||||
// also: wake up!
|
// also: wake up!
|
||||||
_movement_reset_inactivity_countdown();
|
_movement_reset_inactivity_countdown();
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-23
@@ -134,6 +134,17 @@ typedef enum {
|
|||||||
EVENT_DOUBLE_TAP, // Accelerometer detected a double tap. This event is not yet implemented.
|
EVENT_DOUBLE_TAP, // Accelerometer detected a double tap. This event is not yet implemented.
|
||||||
} movement_event_type_t;
|
} movement_event_type_t;
|
||||||
|
|
||||||
|
// Each different timeout type will use a different index when invoking watch_rtc_register_comp_callback
|
||||||
|
typedef enum {
|
||||||
|
LIGHT_BUTTON_TIMEOUT = 0, // Light button longpress timeout
|
||||||
|
MODE_BUTTON_TIMEOUT, // Mode button longpress timeout
|
||||||
|
ALARM_BUTTON_TIMEOUT, // Alarm button longpress timeout
|
||||||
|
LED_TIMEOUT, // LED off timeout
|
||||||
|
RESIGN_TIMEOUT, // Resign active face timeout
|
||||||
|
SLEEP_TIMEOUT, // Low-energy begin timeout
|
||||||
|
MINUTE_TIMEOUT, // Top of the Minute timeout
|
||||||
|
} movement_timeout_index_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint8_t event_type;
|
uint8_t event_type;
|
||||||
uint8_t subsecond;
|
uint8_t subsecond;
|
||||||
@@ -249,37 +260,16 @@ typedef struct {
|
|||||||
int16_t current_face_idx;
|
int16_t current_face_idx;
|
||||||
int16_t next_face_idx;
|
int16_t next_face_idx;
|
||||||
bool watch_face_changed;
|
bool watch_face_changed;
|
||||||
bool fast_tick_enabled;
|
|
||||||
int16_t fast_ticks;
|
|
||||||
|
|
||||||
// LED stuff
|
// LED stuff
|
||||||
int16_t light_ticks;
|
bool light_on;
|
||||||
|
|
||||||
// alarm stuff
|
|
||||||
int16_t alarm_ticks;
|
|
||||||
bool is_buzzing;
|
|
||||||
watch_buzzer_note_t alarm_note;
|
|
||||||
|
|
||||||
// button tracking for long press
|
|
||||||
uint16_t light_down_timestamp;
|
|
||||||
uint16_t mode_down_timestamp;
|
|
||||||
uint16_t alarm_down_timestamp;
|
|
||||||
|
|
||||||
// background task handling
|
// background task handling
|
||||||
bool woke_from_alarm_handler;
|
|
||||||
bool has_scheduled_background_task;
|
bool has_scheduled_background_task;
|
||||||
bool needs_wake;
|
|
||||||
|
|
||||||
// low energy mode countdown
|
|
||||||
int32_t le_mode_ticks;
|
|
||||||
|
|
||||||
// app resignation countdown (TODO: consolidate with LE countdown?)
|
|
||||||
int16_t timeout_ticks;
|
|
||||||
|
|
||||||
// stuff for subsecond tracking
|
// stuff for subsecond tracking
|
||||||
uint8_t tick_frequency;
|
uint8_t tick_frequency;
|
||||||
uint8_t last_second;
|
uint8_t tick_pern;
|
||||||
uint8_t subsecond;
|
|
||||||
|
|
||||||
// backup register stuff
|
// backup register stuff
|
||||||
uint8_t next_available_backup_register;
|
uint8_t next_available_backup_register;
|
||||||
@@ -324,9 +314,11 @@ void movement_cancel_background_task_for_face(uint8_t watch_face_index);
|
|||||||
void movement_request_sleep(void);
|
void movement_request_sleep(void);
|
||||||
void movement_request_wake(void);
|
void movement_request_wake(void);
|
||||||
|
|
||||||
|
void movement_play_note(watch_buzzer_note_t note, uint16_t duration_ms);
|
||||||
void movement_play_signal(void);
|
void movement_play_signal(void);
|
||||||
void movement_play_alarm(void);
|
void movement_play_alarm(void);
|
||||||
void movement_play_alarm_beeps(uint8_t rounds, watch_buzzer_note_t alarm_note);
|
void movement_play_alarm_beeps(uint8_t rounds, watch_buzzer_note_t alarm_note);
|
||||||
|
void movement_play_sequence(int8_t *note_sequence, uint8_t priority);
|
||||||
|
|
||||||
uint8_t movement_claim_backup_register(void);
|
uint8_t movement_claim_backup_register(void);
|
||||||
|
|
||||||
@@ -340,6 +332,7 @@ watch_date_time_t movement_get_utc_date_time(void);
|
|||||||
watch_date_time_t movement_get_local_date_time(void);
|
watch_date_time_t movement_get_local_date_time(void);
|
||||||
watch_date_time_t movement_get_date_time_in_zone(uint8_t zone_index);
|
watch_date_time_t movement_get_date_time_in_zone(uint8_t zone_index);
|
||||||
|
|
||||||
|
void movement_set_utc_date_time(watch_date_time_t date_time);
|
||||||
void movement_set_local_date_time(watch_date_time_t date_time);
|
void movement_set_local_date_time(watch_date_time_t date_time);
|
||||||
|
|
||||||
bool movement_button_should_sound(void);
|
bool movement_button_should_sound(void);
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2022 Joey Castillo
|
||||||
|
* Copyright (c) 2025 Alessandro Genova
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include "rtc32.h"
|
||||||
|
#include "sam.h"
|
||||||
|
|
||||||
|
rtc_cb_t _rtc_callback = NULL;
|
||||||
|
|
||||||
|
#if defined(_SAMD21_) || defined(_SAMD11_)
|
||||||
|
#define CTRLREG (RTC->MODE0.CTRL)
|
||||||
|
#define MODE_SETTING (RTC_MODE0_CTRL_MODE_COUNT32_Val) // Mode 0 Count32
|
||||||
|
#define PRESCALER_SETTING (RTC_MODE0_CTRL_PRESCALER_DIV8_Val)
|
||||||
|
#else
|
||||||
|
#define CTRLREG (RTC->MODE0.CTRLA)
|
||||||
|
#define MODE_SETTING (RTC_MODE0_CTRLA_MODE_COUNT32_Val) // Mode 0 Count32
|
||||||
|
#define PRESCALER_SETTING (RTC_MODE0_CTRLA_PRESCALER_DIV8_Val)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
bool rtc_is_enabled(void) {
|
||||||
|
return CTRLREG.bit.ENABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _rtc_sync(void) {
|
||||||
|
#if defined(_SAMD21_) || defined(_SAMD11_)
|
||||||
|
while (RTC->MODE0.STATUS.bit.SYNCBUSY);
|
||||||
|
#else
|
||||||
|
while (RTC->MODE0.SYNCBUSY.reg & RTC_MODE0_SYNCBUSY_MASK);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void rtc_init(void) {
|
||||||
|
#if defined(_SAMD21_) || defined(_SAMD11_)
|
||||||
|
// enable the RTC
|
||||||
|
PM->APBAMASK.reg |= PM_APBAMASK_RTC;
|
||||||
|
// clock RTC with GCLK3 (prescaled 1024 Hz output from the external crystal)
|
||||||
|
GCLK->CLKCTRL.reg = GCLK_CLKCTRL_GEN(3) | GCLK_CLKCTRL_ID(RTC_GCLK_ID) | GCLK_CLKCTRL_CLKEN;
|
||||||
|
#else
|
||||||
|
MCLK->APBAMASK.reg |= MCLK_APBAMASK_RTC;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// if (rtc_is_enabled()) return; // don't reset the RTC if it's already set up.
|
||||||
|
// Reset everything, once things are stabilized we can think about preserving some state
|
||||||
|
CTRLREG.bit.ENABLE = 0;
|
||||||
|
|
||||||
|
_rtc_sync();
|
||||||
|
CTRLREG.bit.SWRST = 1;
|
||||||
|
_rtc_sync();
|
||||||
|
|
||||||
|
CTRLREG.bit.MODE = MODE_SETTING;
|
||||||
|
CTRLREG.bit.PRESCALER = PRESCALER_SETTING;
|
||||||
|
|
||||||
|
#if defined(_SAML21_) || defined(_SAML22_) || defined(_SAMD51_)
|
||||||
|
CTRLREG.bit.COUNTSYNC = 1;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
RTC->MODE0.INTENSET.reg = RTC_MODE0_INTENSET_OVF;
|
||||||
|
}
|
||||||
|
|
||||||
|
void rtc_enable(void) {
|
||||||
|
if (rtc_is_enabled()) return;
|
||||||
|
CTRLREG.bit.ENABLE = 1;
|
||||||
|
_rtc_sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
void rtc_set_counter(rtc_counter_t counter) {
|
||||||
|
// // syncing before and after was found to increase reliability on Sensor Watch
|
||||||
|
_rtc_sync();
|
||||||
|
RTC->MODE0.COUNT.reg = counter;
|
||||||
|
_rtc_sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
rtc_counter_t rtc_get_counter(void) {
|
||||||
|
rtc_counter_t counter;
|
||||||
|
|
||||||
|
#if defined(_SAML21_) || defined(_SAML22_) || defined(_SAMD51_)
|
||||||
|
CTRLREG.bit.COUNTSYNC = 1;
|
||||||
|
#endif
|
||||||
|
_rtc_sync();
|
||||||
|
counter = RTC->MODE0.COUNT.reg;
|
||||||
|
|
||||||
|
return counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
void rtc_enable_compare_interrupt(uint32_t compare_time) {
|
||||||
|
RTC->MODE0.COMP[0].reg = compare_time;
|
||||||
|
_rtc_sync();
|
||||||
|
RTC->MODE0.INTENSET.reg = RTC_MODE0_INTENSET_CMP0;
|
||||||
|
// NVIC_ClearPendingIRQ(RTC_IRQn);
|
||||||
|
// NVIC_EnableIRQ(RTC_IRQn);
|
||||||
|
}
|
||||||
|
|
||||||
|
void rtc_configure_callback(rtc_cb_t callback) {
|
||||||
|
_rtc_callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
void rtc_disable_compare_interrupt(void){
|
||||||
|
RTC->MODE0.INTENCLR.reg = RTC_MODE0_INTENCLR_CMP0;
|
||||||
|
// NVIC_ClearPendingIRQ(RTC_IRQn);
|
||||||
|
// NVIC_DisableIRQ(RTC_IRQn);
|
||||||
|
}
|
||||||
|
|
||||||
|
void irq_handler_rtc(void);
|
||||||
|
|
||||||
|
void irq_handler_rtc(void) {
|
||||||
|
uint16_t int_cause = (uint16_t)RTC->MODE0.INTFLAG.reg;
|
||||||
|
RTC->MODE0.INTFLAG.reg = RTC_MODE0_INTFLAG_MASK;
|
||||||
|
(void)RTC->MODE0.INTFLAG.reg;
|
||||||
|
|
||||||
|
/* Invoke registered Callback function */
|
||||||
|
if (_rtc_callback != NULL) {
|
||||||
|
_rtc_callback(int_cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NVIC_ClearPendingIRQ(RTC_IRQn);
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
////< @file rtc32.h
|
||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020 Joey Castillo
|
||||||
|
* Copyright (c) 2025 Alessandro Genova
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @addtogroup rtc Real-Time Clock
|
||||||
|
* @brief Functions for configuring and using the Real-Time Clock peripheral.
|
||||||
|
* @details This is the rtc implementation for MODE0 (counter32)
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define RTC_REFERENCE_YEAR (2020)
|
||||||
|
|
||||||
|
typedef union {
|
||||||
|
struct {
|
||||||
|
uint32_t second : 6; // 0-59
|
||||||
|
uint32_t minute : 6; // 0-59
|
||||||
|
uint32_t hour : 5; // 0-23
|
||||||
|
uint32_t day : 5; // 1-31
|
||||||
|
uint32_t month : 4; // 1-12
|
||||||
|
uint32_t year : 6; // 0-63 (representing 2020-2083)
|
||||||
|
} unit;
|
||||||
|
uint32_t reg; // the bit-packed value as expected by the RTC peripheral's CLOCK register.
|
||||||
|
} rtc_date_time_t;
|
||||||
|
|
||||||
|
typedef enum rtc_alarm_match_t {
|
||||||
|
ALARM_MATCH_DISABLED = 0,
|
||||||
|
ALARM_MATCH_SS,
|
||||||
|
ALARM_MATCH_MMSS,
|
||||||
|
ALARM_MATCH_HHMMSS,
|
||||||
|
} rtc_alarm_match_t;
|
||||||
|
|
||||||
|
typedef uint32_t rtc_counter_t;
|
||||||
|
|
||||||
|
typedef void (*rtc_cb_t)(uint16_t intflag);
|
||||||
|
|
||||||
|
/** @brief Initializes the RTC.
|
||||||
|
* @details Configures the RTC for COUNT32 mode, with a 1 Hz
|
||||||
|
* tick derived from the 1024 Hz clock on GCLK3 (for SAM D devices)
|
||||||
|
* or OSC32KCTRL's most accurate 1024 Hz output (for SAM L devices).
|
||||||
|
*/
|
||||||
|
void rtc_init(void);
|
||||||
|
|
||||||
|
/** @brief Enables the RTC.
|
||||||
|
*/
|
||||||
|
void rtc_enable(void);
|
||||||
|
|
||||||
|
/** @brief Checks if the RTC is enabled.
|
||||||
|
* @return true if the RTC is enabled; false if not.
|
||||||
|
*/
|
||||||
|
bool rtc_is_enabled(void);
|
||||||
|
|
||||||
|
/** @brief Set the value of the counter register.
|
||||||
|
*/
|
||||||
|
void rtc_set_counter(rtc_counter_t counter);
|
||||||
|
|
||||||
|
/** @brief Returns the value of the counter register.
|
||||||
|
*/
|
||||||
|
rtc_counter_t rtc_get_counter(void);
|
||||||
|
|
||||||
|
/** @brief Configures the RTC alarm callback.
|
||||||
|
* @param callback The function to call when an RTC interrupt occurs. The callback
|
||||||
|
* will be passed a bitmask of the interrupt flags, the full contents
|
||||||
|
* of the RTC peripheral's INTFLAG register.
|
||||||
|
*/
|
||||||
|
void rtc_configure_callback(rtc_cb_t callback);
|
||||||
|
|
||||||
|
void rtc_enable_compare_interrupt(uint32_t compare_time);
|
||||||
|
void rtc_disable_compare_interrupt(void);
|
||||||
|
|
||||||
|
/** @} */
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
* MIT License
|
* MIT License
|
||||||
*
|
*
|
||||||
* Copyright (c) 2020 Joey Castillo
|
* Copyright (c) 2020 Joey Castillo
|
||||||
|
* Copyright (c) 2025 Alessandro Genova
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
@@ -23,11 +24,32 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
|
#include <limits.h>
|
||||||
|
|
||||||
#include "watch_rtc.h"
|
#include "watch_rtc.h"
|
||||||
#include "watch_private.h"
|
#include "watch_private.h"
|
||||||
|
#include "watch_utility.h"
|
||||||
|
|
||||||
|
static const uint32_t RTC_OSC_DIV = 10;
|
||||||
|
static const uint32_t RTC_OSC_HZ = 1 << RTC_OSC_DIV; // 2^10 = 1024
|
||||||
|
static const uint32_t RTC_PRESCALER_DIV = 3;
|
||||||
|
static const uint32_t RTC_CNT_HZ = RTC_OSC_HZ >> RTC_PRESCALER_DIV; // 1024 / 2^3 = 128
|
||||||
|
static const uint32_t RTC_CNT_DIV = RTC_OSC_DIV - RTC_PRESCALER_DIV; // 7
|
||||||
|
static const uint32_t RTC_CNT_TICKS_PER_MINUTE = RTC_CNT_HZ * 60;
|
||||||
|
static const uint32_t RTC_CNT_TICKS_PER_HOUR = RTC_CNT_TICKS_PER_MINUTE * 60;
|
||||||
|
|
||||||
|
static const int TB_BKUP_REG = 7;
|
||||||
|
|
||||||
|
#define WATCH_RTC_N_COMP_CB 8
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
volatile uint32_t counter;
|
||||||
|
volatile watch_cb_t callback;
|
||||||
|
volatile bool enabled;
|
||||||
|
} comp_cb_t;
|
||||||
|
|
||||||
watch_cb_t tick_callbacks[8];
|
watch_cb_t tick_callbacks[8];
|
||||||
|
comp_cb_t comp_callbacks[WATCH_RTC_N_COMP_CB];
|
||||||
watch_cb_t alarm_callback;
|
watch_cb_t alarm_callback;
|
||||||
watch_cb_t btn_alarm_callback;
|
watch_cb_t btn_alarm_callback;
|
||||||
watch_cb_t a2_callback;
|
watch_cb_t a2_callback;
|
||||||
@@ -46,14 +68,49 @@ void _watch_rtc_init(void) {
|
|||||||
#endif
|
#endif
|
||||||
rtc_enable();
|
rtc_enable();
|
||||||
rtc_configure_callback(watch_rtc_callback);
|
rtc_configure_callback(watch_rtc_callback);
|
||||||
|
|
||||||
|
for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) {
|
||||||
|
comp_callbacks[index].counter = 0;
|
||||||
|
comp_callbacks[index].callback = NULL;
|
||||||
|
comp_callbacks[index].enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
NVIC_ClearPendingIRQ(RTC_IRQn);
|
||||||
|
NVIC_EnableIRQ(RTC_IRQn);
|
||||||
}
|
}
|
||||||
|
|
||||||
void watch_rtc_set_date_time(rtc_date_time_t date_time) {
|
void watch_rtc_set_date_time(rtc_date_time_t date_time) {
|
||||||
rtc_set_date_time(date_time);
|
watch_rtc_set_unix_time(watch_utility_date_time_to_unix_time(date_time, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
rtc_date_time_t watch_rtc_get_date_time(void) {
|
rtc_date_time_t watch_rtc_get_date_time(void) {
|
||||||
return rtc_get_date_time();
|
return watch_utility_date_time_from_unix_time(watch_rtc_get_unix_time(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void watch_rtc_set_unix_time(unix_timestamp_t unix_time) {
|
||||||
|
// time_backup + counter / RTC_CNT_HZ = unix_time
|
||||||
|
rtc_counter_t counter = rtc_get_counter();
|
||||||
|
unix_timestamp_t tb = unix_time - (counter >> RTC_CNT_DIV);
|
||||||
|
watch_store_backup_data(tb, TB_BKUP_REG);
|
||||||
|
}
|
||||||
|
|
||||||
|
unix_timestamp_t watch_rtc_get_unix_time(void) {
|
||||||
|
// time_backup + counter / RTC_CNT_HZ = unix_time
|
||||||
|
rtc_counter_t counter = rtc_get_counter();
|
||||||
|
unix_timestamp_t tb = watch_get_backup_data(TB_BKUP_REG);
|
||||||
|
return tb + (counter >> RTC_CNT_DIV);
|
||||||
|
}
|
||||||
|
|
||||||
|
rtc_counter_t watch_rtc_get_counter(void) {
|
||||||
|
return rtc_get_counter();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t watch_rtc_get_frequency(void) {
|
||||||
|
return RTC_CNT_HZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t watch_rtc_get_ticks_per_minute(void) {
|
||||||
|
return RTC_CNT_TICKS_PER_MINUTE;
|
||||||
}
|
}
|
||||||
|
|
||||||
rtc_date_time_t watch_get_init_date_time(void) {
|
rtc_date_time_t watch_get_init_date_time(void) {
|
||||||
@@ -103,57 +160,98 @@ void watch_rtc_register_periodic_callback(watch_cb_t callback, uint8_t frequency
|
|||||||
// this also maps nicely to an index for our list of tick callbacks.
|
// this also maps nicely to an index for our list of tick callbacks.
|
||||||
tick_callbacks[per_n] = callback;
|
tick_callbacks[per_n] = callback;
|
||||||
|
|
||||||
NVIC_ClearPendingIRQ(RTC_IRQn);
|
// NVIC_ClearPendingIRQ(RTC_IRQn);
|
||||||
NVIC_EnableIRQ(RTC_IRQn);
|
// NVIC_EnableIRQ(RTC_IRQn);
|
||||||
RTC->MODE2.INTENSET.reg = 1 << per_n;
|
RTC->MODE0.INTENSET.reg = 1 << per_n;
|
||||||
}
|
}
|
||||||
|
|
||||||
void watch_rtc_disable_periodic_callback(uint8_t frequency) {
|
void watch_rtc_disable_periodic_callback(uint8_t frequency) {
|
||||||
if (__builtin_popcount(frequency) != 1) return;
|
if (__builtin_popcount(frequency) != 1) return;
|
||||||
uint8_t per_n = __builtin_clz((frequency & 0xFF) << 24);
|
uint8_t per_n = __builtin_clz((frequency & 0xFF) << 24);
|
||||||
RTC->MODE2.INTENCLR.reg = 1 << per_n;
|
RTC->MODE0.INTENCLR.reg = 1 << per_n;
|
||||||
}
|
}
|
||||||
|
|
||||||
void watch_rtc_disable_matching_periodic_callbacks(uint8_t mask) {
|
void watch_rtc_disable_matching_periodic_callbacks(uint8_t mask) {
|
||||||
RTC->MODE2.INTENCLR.reg = mask;
|
RTC->MODE0.INTENCLR.reg = mask;
|
||||||
}
|
}
|
||||||
|
|
||||||
void watch_rtc_disable_all_periodic_callbacks(void) {
|
void watch_rtc_disable_all_periodic_callbacks(void) {
|
||||||
watch_rtc_disable_matching_periodic_callbacks(0xFF);
|
watch_rtc_disable_matching_periodic_callbacks(0xFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
void watch_rtc_register_alarm_callback(watch_cb_t callback, rtc_date_time_t alarm_time, rtc_alarm_match_t mask) {
|
static void _watch_rtc_schedule_next_comp(void) {
|
||||||
RTC->MODE2.Mode2Alarm[0].ALARM.reg = alarm_time.reg;
|
rtc_disable_compare_interrupt();
|
||||||
RTC->MODE2.Mode2Alarm[0].MASK.reg = mask;
|
|
||||||
RTC->MODE2.INTENSET.reg = RTC_MODE2_INTENSET_ALARM0;
|
// The soonest we can schedule is the next tick
|
||||||
alarm_callback = callback;
|
rtc_counter_t curr_counter = watch_rtc_get_counter() + 1;
|
||||||
NVIC_ClearPendingIRQ(RTC_IRQn);
|
|
||||||
NVIC_EnableIRQ(RTC_IRQn);
|
bool schedule_any = false;
|
||||||
RTC->MODE2.INTENSET.reg = RTC_MODE2_INTENSET_ALARM0;
|
rtc_counter_t comp_counter;
|
||||||
|
rtc_counter_t min_diff = UINT_MAX;
|
||||||
|
|
||||||
|
for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) {
|
||||||
|
if (comp_callbacks[index].enabled) {
|
||||||
|
rtc_counter_t diff = comp_callbacks[index].counter - curr_counter;
|
||||||
|
if (diff <= min_diff) {
|
||||||
|
min_diff = diff;
|
||||||
|
comp_counter = comp_callbacks[index].counter;
|
||||||
|
schedule_any = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void watch_rtc_disable_alarm_callback(void) {
|
if (schedule_any) {
|
||||||
RTC->MODE2.INTENCLR.reg = RTC_MODE2_INTENCLR_ALARM0;
|
rtc_enable_compare_interrupt(comp_counter);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void watch_rtc_callback(uint16_t interrupt_status) {
|
void watch_rtc_register_comp_callback(watch_cb_t callback, rtc_counter_t counter, uint8_t index) {
|
||||||
uint16_t interrupt_enabled = RTC->MODE2.INTENSET.reg;
|
if (index >= WATCH_RTC_N_COMP_CB) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if ((interrupt_status & interrupt_enabled) & RTC_MODE2_INTFLAG_PER_Msk) {
|
rtc_disable_compare_interrupt();
|
||||||
|
|
||||||
|
comp_callbacks[index].counter = counter;
|
||||||
|
comp_callbacks[index].callback = callback;
|
||||||
|
comp_callbacks[index].enabled = true;
|
||||||
|
|
||||||
|
_watch_rtc_schedule_next_comp();
|
||||||
|
}
|
||||||
|
|
||||||
|
void watch_rtc_disable_comp_callback(uint8_t index) {
|
||||||
|
if (index >= WATCH_RTC_N_COMP_CB) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rtc_disable_compare_interrupt();
|
||||||
|
|
||||||
|
comp_callbacks[index].enabled = false;
|
||||||
|
|
||||||
|
_watch_rtc_schedule_next_comp();
|
||||||
|
}
|
||||||
|
|
||||||
|
void watch_rtc_callback(uint16_t interrupt_cause) {
|
||||||
|
// First read all relevant registers, to ensure no changes occurr during the callbacks
|
||||||
|
uint16_t interrupt_enabled = RTC->MODE0.INTENSET.reg;
|
||||||
|
rtc_counter_t comp_counter = RTC->MODE0.COMP[0].reg;
|
||||||
|
|
||||||
|
|
||||||
|
if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_PER_Msk) {
|
||||||
// handle the tick callback first, it's what we do the most.
|
// handle the tick callback first, it's what we do the most.
|
||||||
// start from PER7, the 1 Hz tick.
|
// start from PER7, the 1 Hz tick.
|
||||||
for(int8_t i = 7; i >= 0; i--) {
|
for(int8_t i = 7; i >= 0; i--) {
|
||||||
if ((interrupt_status & interrupt_enabled) & (1 << i)) {
|
if ((interrupt_cause & interrupt_enabled) & (1 << i)) {
|
||||||
if (tick_callbacks[i] != NULL) {
|
if (tick_callbacks[i] != NULL) {
|
||||||
tick_callbacks[i]();
|
tick_callbacks[i]();
|
||||||
}
|
}
|
||||||
RTC->MODE2.INTFLAG.reg = 1 << i;
|
|
||||||
// break; Uncertain if this fix is requried. We were discussing in discord. Might slightly increase power consumption.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if ((interrupt_status & interrupt_enabled) & RTC_MODE2_INTFLAG_TAMPER) {
|
}
|
||||||
|
|
||||||
|
if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_TAMPER) {
|
||||||
// handle the extwake interrupts next.
|
// handle the extwake interrupts next.
|
||||||
uint8_t reason = RTC->MODE2.TAMPID.reg;
|
uint8_t reason = RTC->MODE0.TAMPID.reg;
|
||||||
if (reason & RTC_TAMPID_TAMPID2) {
|
if (reason & RTC_TAMPID_TAMPID2) {
|
||||||
if (btn_alarm_callback != NULL) btn_alarm_callback();
|
if (btn_alarm_callback != NULL) btn_alarm_callback();
|
||||||
} else if (reason & RTC_TAMPID_TAMPID1) {
|
} else if (reason & RTC_TAMPID_TAMPID1) {
|
||||||
@@ -161,25 +259,37 @@ void watch_rtc_callback(uint16_t interrupt_status) {
|
|||||||
} else if (reason & RTC_TAMPID_TAMPID0) {
|
} else if (reason & RTC_TAMPID_TAMPID0) {
|
||||||
if (a4_callback != NULL) a4_callback();
|
if (a4_callback != NULL) a4_callback();
|
||||||
}
|
}
|
||||||
RTC->MODE2.TAMPID.reg = reason;
|
RTC->MODE0.TAMPID.reg = reason;
|
||||||
RTC->MODE2.INTFLAG.reg = RTC_MODE2_INTFLAG_TAMPER;
|
|
||||||
} else if ((interrupt_status & interrupt_enabled) & RTC_MODE2_INTFLAG_ALARM0) {
|
|
||||||
// finally handle the alarm.
|
|
||||||
if (alarm_callback != NULL) {
|
|
||||||
alarm_callback();
|
|
||||||
}
|
}
|
||||||
RTC->MODE2.INTFLAG.reg = RTC_MODE2_INTFLAG_ALARM0;
|
|
||||||
|
if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_CMP0) {
|
||||||
|
// The comp interrupt is generated one tick after the matched counter
|
||||||
|
// rtc_counter_t comp_counter = watch_rtc_get_counter() - 1;
|
||||||
|
|
||||||
|
for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) {
|
||||||
|
if (comp_callbacks[index].enabled && comp_counter == comp_callbacks[index].counter) {
|
||||||
|
comp_callbacks[index].enabled = false;
|
||||||
|
comp_callbacks[index].callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_watch_rtc_schedule_next_comp();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_OVF) {
|
||||||
|
// Handle the overflow of the counter. All we need to do is reset the reference time.
|
||||||
|
unix_timestamp_t tb = watch_get_backup_data(TB_BKUP_REG);
|
||||||
|
watch_store_backup_data(tb + (UINT_MAX >> RTC_CNT_DIV), TB_BKUP_REG);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void watch_rtc_enable(bool en) {
|
void watch_rtc_enable(bool en) {
|
||||||
// Writing it twice - as it's quite dangerous operation.
|
// Writing it twice - as it's quite dangerous operation.
|
||||||
// If write fails - we might hang with RTC off, which means no recovery possible
|
// If write fails - we might hang with RTC off, which means no recovery possible
|
||||||
while (RTC->MODE2.SYNCBUSY.reg);
|
while (RTC->MODE0.SYNCBUSY.reg);
|
||||||
RTC->MODE2.CTRLA.bit.ENABLE = en ? 1 : 0;
|
RTC->MODE0.CTRLA.bit.ENABLE = en ? 1 : 0;
|
||||||
while (RTC->MODE2.SYNCBUSY.reg);
|
while (RTC->MODE0.SYNCBUSY.reg);
|
||||||
RTC->MODE2.CTRLA.bit.ENABLE = en ? 1 : 0;
|
RTC->MODE0.CTRLA.bit.ENABLE = en ? 1 : 0;
|
||||||
while (RTC->MODE2.SYNCBUSY.reg);
|
while (RTC->MODE0.SYNCBUSY.reg);
|
||||||
}
|
}
|
||||||
|
|
||||||
void watch_rtc_freqcorr_write(int16_t value, int16_t sign) {
|
void watch_rtc_freqcorr_write(int16_t value, int16_t sign) {
|
||||||
@@ -188,8 +298,7 @@ void watch_rtc_freqcorr_write(int16_t value, int16_t sign) {
|
|||||||
data.bit.VALUE = value;
|
data.bit.VALUE = value;
|
||||||
data.bit.SIGN = sign;
|
data.bit.SIGN = sign;
|
||||||
|
|
||||||
RTC->MODE2.FREQCORR.reg = data.reg; // Setting correction in single write operation
|
RTC->MODE0.FREQCORR.reg = data.reg; // Setting correction in single write operation
|
||||||
|
|
||||||
// We do not sycnronize. We are not in a hurry
|
// We do not sycnronize. We are not in a hurry
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,9 +32,13 @@ void cb_watch_buzzer_seq(void);
|
|||||||
|
|
||||||
static uint16_t _seq_position;
|
static uint16_t _seq_position;
|
||||||
static int8_t _tone_ticks, _repeat_counter;
|
static int8_t _tone_ticks, _repeat_counter;
|
||||||
static bool _callback_running = false;
|
|
||||||
static int8_t *_sequence;
|
static int8_t *_sequence;
|
||||||
|
static uint8_t _volume;
|
||||||
static void (*_cb_finished)(void);
|
static void (*_cb_finished)(void);
|
||||||
|
static watch_cb_t _cb_start_global = NULL;
|
||||||
|
static watch_cb_t _cb_stop_global = NULL;
|
||||||
|
static volatile bool _led_is_active = false;
|
||||||
|
static volatile bool _buzzer_is_active = false;
|
||||||
|
|
||||||
static void _tcc_write_RUNSTDBY(bool value) {
|
static void _tcc_write_RUNSTDBY(bool value) {
|
||||||
// enables or disables RUNSTDBY of the tcc
|
// enables or disables RUNSTDBY of the tcc
|
||||||
@@ -46,13 +50,11 @@ static void _tcc_write_RUNSTDBY(bool value) {
|
|||||||
static inline void _tc0_start() {
|
static inline void _tc0_start() {
|
||||||
// start the TC0 timer
|
// start the TC0 timer
|
||||||
tc_enable(0);
|
tc_enable(0);
|
||||||
_callback_running = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void _tc0_stop() {
|
static inline void _tc0_stop() {
|
||||||
// stop the TC0 timer
|
// stop the TC0 timer
|
||||||
tc_disable(0);
|
tc_disable(0);
|
||||||
_callback_running = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void _tc0_initialize() {
|
static void _tc0_initialize() {
|
||||||
@@ -68,19 +70,32 @@ static void _tc0_initialize() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void)) {
|
void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void)) {
|
||||||
if (_callback_running) _tc0_stop();
|
watch_buzzer_play_sequence_with_volume(note_sequence, callback_on_end, WATCH_BUZZER_VOLUME_LOUD);
|
||||||
|
}
|
||||||
|
|
||||||
|
void watch_buzzer_play_sequence_with_volume(int8_t *note_sequence, void (*callback_on_end)(void), watch_buzzer_volume_t volume) {
|
||||||
|
// Abort any previous sequence
|
||||||
|
watch_buzzer_abort_sequence();
|
||||||
|
|
||||||
|
_buzzer_is_active = true;
|
||||||
|
|
||||||
|
if (_cb_start_global) {
|
||||||
|
_cb_start_global();
|
||||||
|
}
|
||||||
|
|
||||||
|
watch_enable_buzzer_and_leds();
|
||||||
|
|
||||||
watch_set_buzzer_off();
|
watch_set_buzzer_off();
|
||||||
_sequence = note_sequence;
|
_sequence = note_sequence;
|
||||||
_cb_finished = callback_on_end;
|
_cb_finished = callback_on_end;
|
||||||
|
_volume = volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25;
|
||||||
_seq_position = 0;
|
_seq_position = 0;
|
||||||
_tone_ticks = 0;
|
_tone_ticks = 0;
|
||||||
_repeat_counter = -1;
|
_repeat_counter = -1;
|
||||||
// prepare buzzer
|
// prepare buzzer
|
||||||
watch_enable_buzzer();
|
|
||||||
// setup TC0 timer
|
// setup TC0 timer
|
||||||
_tc0_initialize();
|
_tc0_initialize();
|
||||||
// TCC should run in standby mode
|
|
||||||
_tcc_write_RUNSTDBY(true);
|
|
||||||
// start the timer (for the 64 hz callback)
|
// start the timer (for the 64 hz callback)
|
||||||
_tc0_start();
|
_tc0_start();
|
||||||
}
|
}
|
||||||
@@ -110,7 +125,7 @@ void cb_watch_buzzer_seq(void) {
|
|||||||
// read note
|
// read note
|
||||||
watch_buzzer_note_t note = _sequence[_seq_position];
|
watch_buzzer_note_t note = _sequence[_seq_position];
|
||||||
if (note != BUZZER_NOTE_REST) {
|
if (note != BUZZER_NOTE_REST) {
|
||||||
watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], 25);
|
watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], _volume);
|
||||||
watch_set_buzzer_on();
|
watch_set_buzzer_on();
|
||||||
} else watch_set_buzzer_off();
|
} else watch_set_buzzer_off();
|
||||||
// set duration ticks and move to next tone
|
// set duration ticks and move to next tone
|
||||||
@@ -119,17 +134,37 @@ void cb_watch_buzzer_seq(void) {
|
|||||||
} else {
|
} else {
|
||||||
// end the sequence
|
// end the sequence
|
||||||
watch_buzzer_abort_sequence();
|
watch_buzzer_abort_sequence();
|
||||||
if (_cb_finished) _cb_finished();
|
|
||||||
}
|
}
|
||||||
} else _tone_ticks--;
|
} else _tone_ticks--;
|
||||||
}
|
}
|
||||||
|
|
||||||
void watch_buzzer_abort_sequence(void) {
|
void watch_buzzer_abort_sequence(void) {
|
||||||
// ends/aborts the sequence
|
// ends/aborts the sequence
|
||||||
if (_callback_running) _tc0_stop();
|
if (!_buzzer_is_active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_buzzer_is_active = false;
|
||||||
|
|
||||||
|
_tc0_stop();
|
||||||
|
|
||||||
watch_set_buzzer_off();
|
watch_set_buzzer_off();
|
||||||
// disable standby mode for TCC
|
|
||||||
_tcc_write_RUNSTDBY(false);
|
// disable TCC
|
||||||
|
watch_maybe_disable_buzzer_and_leds();
|
||||||
|
|
||||||
|
if (_cb_stop_global) {
|
||||||
|
_cb_stop_global();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_cb_finished) {
|
||||||
|
_cb_finished();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void watch_buzzer_register_global_callbacks(watch_cb_t cb_start, watch_cb_t cb_stop) {
|
||||||
|
_cb_stop_global = cb_start;
|
||||||
|
_cb_stop_global = cb_stop;
|
||||||
}
|
}
|
||||||
|
|
||||||
void irq_handler_tc0(void) {
|
void irq_handler_tc0(void) {
|
||||||
@@ -142,19 +177,41 @@ bool watch_is_buzzer_or_led_enabled(void){
|
|||||||
return tcc_is_enabled(0);
|
return tcc_is_enabled(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void watch_enable_buzzer(void) {
|
void watch_enable_buzzer_and_leds(void) {
|
||||||
if (!tcc_is_enabled(0)) {
|
if (!tcc_is_enabled(0)) {
|
||||||
|
// tcc_set_run_in_standby(0, true);
|
||||||
_watch_enable_tcc();
|
_watch_enable_tcc();
|
||||||
|
// TCC should run in standby mode
|
||||||
|
_tcc_write_RUNSTDBY(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void watch_disable_buzzer_and_leds(void) {
|
||||||
|
if (tcc_is_enabled(0)) {
|
||||||
|
_tcc_write_RUNSTDBY(false);
|
||||||
|
_watch_disable_tcc();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void watch_maybe_disable_buzzer_and_leds(void) {
|
||||||
|
if (_buzzer_is_active || _led_is_active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
watch_disable_buzzer_and_leds();
|
||||||
|
}
|
||||||
|
|
||||||
|
void watch_enable_buzzer(void) {
|
||||||
|
watch_enable_buzzer_and_leds();
|
||||||
|
}
|
||||||
|
|
||||||
void watch_set_buzzer_period_and_duty_cycle(uint32_t period, uint8_t duty) {
|
void watch_set_buzzer_period_and_duty_cycle(uint32_t period, uint8_t duty) {
|
||||||
tcc_set_period(0, period, true);
|
tcc_set_period(0, period, true);
|
||||||
tcc_set_cc(0, (WATCH_BUZZER_TCC_CHANNEL) % 4, period / (100 / duty), true);
|
tcc_set_cc(0, (WATCH_BUZZER_TCC_CHANNEL) % 4, period / (100 / duty), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void watch_disable_buzzer(void) {
|
void watch_disable_buzzer(void) {
|
||||||
_watch_disable_tcc();
|
watch_maybe_disable_buzzer_and_leds();
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void watch_set_buzzer_on(void) {
|
inline void watch_set_buzzer_on(void) {
|
||||||
@@ -172,14 +229,17 @@ void watch_buzzer_play_note(watch_buzzer_note_t note, uint16_t duration_ms) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void watch_buzzer_play_note_with_volume(watch_buzzer_note_t note, uint16_t duration_ms, watch_buzzer_volume_t volume) {
|
void watch_buzzer_play_note_with_volume(watch_buzzer_note_t note, uint16_t duration_ms, watch_buzzer_volume_t volume) {
|
||||||
if (note == BUZZER_NOTE_REST) {
|
static int8_t single_note_sequence[3];
|
||||||
watch_set_buzzer_off();
|
|
||||||
} else {
|
single_note_sequence[0] = note;
|
||||||
watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25);
|
// 48 ticks per second for the tc0?
|
||||||
watch_set_buzzer_on();
|
// Each tick is approximately 20ms
|
||||||
}
|
uint16_t duration = duration_ms / 20;
|
||||||
delay_ms(duration_ms);
|
if (duration > 127) duration = 127;
|
||||||
watch_set_buzzer_off();
|
single_note_sequence[1] = (int8_t)duration;
|
||||||
|
single_note_sequence[2] = 0;
|
||||||
|
|
||||||
|
watch_buzzer_play_sequence_with_volume(single_note_sequence, NULL, volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _watch_enable_tcc(void) {
|
void _watch_enable_tcc(void) {
|
||||||
@@ -263,7 +323,7 @@ void watch_enable_leds(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void watch_disable_leds(void) {
|
void watch_disable_leds(void) {
|
||||||
_watch_disable_tcc();
|
watch_maybe_disable_buzzer_and_leds();
|
||||||
}
|
}
|
||||||
|
|
||||||
void watch_set_led_color(uint8_t red, uint8_t green) {
|
void watch_set_led_color(uint8_t red, uint8_t green) {
|
||||||
@@ -275,6 +335,15 @@ void watch_set_led_color(uint8_t red, uint8_t green) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) {
|
void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) {
|
||||||
|
bool turning_on = (red | green | blue) != 0;
|
||||||
|
|
||||||
|
if (turning_on) {
|
||||||
|
_led_is_active = true;
|
||||||
|
watch_enable_buzzer_and_leds();
|
||||||
|
} else {
|
||||||
|
_led_is_active = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (tcc_is_enabled(0)) {
|
if (tcc_is_enabled(0)) {
|
||||||
uint32_t period = tcc_get_period(0);
|
uint32_t period = tcc_get_period(0);
|
||||||
tcc_set_cc(0, (WATCH_RED_TCC_CHANNEL) % 4, ((period * (uint32_t)red * 1000ull) / 255000ull), true);
|
tcc_set_cc(0, (WATCH_RED_TCC_CHANNEL) % 4, ((period * (uint32_t)red * 1000ull) / 255000ull), true);
|
||||||
@@ -289,6 +358,10 @@ void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) {
|
|||||||
(void) blue; // silence warning
|
(void) blue; // silence warning
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!turning_on) {
|
||||||
|
watch_maybe_disable_buzzer_and_leds();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void watch_set_led_red(void) {
|
void watch_set_led_red(void) {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
////< @file watch_rtc.h
|
////< @file watch_rtc.h
|
||||||
|
|
||||||
#include "watch.h"
|
#include "watch.h"
|
||||||
#include "rtc.h"
|
#include "rtc32.h"
|
||||||
|
|
||||||
/** @addtogroup rtc Real-Time Clock
|
/** @addtogroup rtc Real-Time Clock
|
||||||
* @brief This section covers functions related to the SAM L22's real-time clock peripheral, including
|
* @brief This section covers functions related to the SAM L22's real-time clock peripheral, including
|
||||||
@@ -42,17 +42,20 @@
|
|||||||
extern watch_cb_t btn_alarm_callback;
|
extern watch_cb_t btn_alarm_callback;
|
||||||
extern watch_cb_t a2_callback;
|
extern watch_cb_t a2_callback;
|
||||||
extern watch_cb_t a4_callback;
|
extern watch_cb_t a4_callback;
|
||||||
|
extern watch_cb_t comp_callback;
|
||||||
|
|
||||||
#define WATCH_RTC_REFERENCE_YEAR (2020)
|
#define WATCH_RTC_REFERENCE_YEAR (2020)
|
||||||
|
|
||||||
#define watch_date_time_t rtc_date_time_t
|
#define watch_date_time_t rtc_date_time_t
|
||||||
|
typedef rtc_counter_t watch_counter_t;
|
||||||
|
typedef uint32_t unix_timestamp_t;
|
||||||
|
|
||||||
/** @brief Called by main.c to check if the RTC is enabled.
|
/** @brief Called by main.c to check if the RTC is enabled.
|
||||||
* You may call this function, but outside of app_init, it should always return true.
|
* You may call this function, but outside of app_init, it should always return true.
|
||||||
*/
|
*/
|
||||||
bool _watch_rtc_is_enabled(void);
|
bool _watch_rtc_is_enabled(void);
|
||||||
|
|
||||||
/** @brief Sets the date and time.
|
/** @brief Sets the date and time. Calls watch_rtc_set_unix_time internally.
|
||||||
* @param date_time The date and time you wish to set, with a year value from 0-63 representing 2020-2083.
|
* @param date_time The date and time you wish to set, with a year value from 0-63 representing 2020-2083.
|
||||||
* @note The SAM L22 stores the year as six bits representing a value from 0 to 63. It treats this as a year
|
* @note The SAM L22 stores the year as six bits representing a value from 0 to 63. It treats this as a year
|
||||||
* offset from a reference year, which must be a leap year. Since 2020 was a leap year, and it allows
|
* offset from a reference year, which must be a leap year. Since 2020 was a leap year, and it allows
|
||||||
@@ -62,7 +65,7 @@ bool _watch_rtc_is_enabled(void);
|
|||||||
*/
|
*/
|
||||||
void watch_rtc_set_date_time(rtc_date_time_t date_time);
|
void watch_rtc_set_date_time(rtc_date_time_t date_time);
|
||||||
|
|
||||||
/** @brief Returns the date and time.
|
/** @brief Returns the date and time. Calls watch_rtc_get_unix_time internally.
|
||||||
* @return A rtc_date_time_t with the current date and time, with a year value from 0-63 representing 2020-2083.
|
* @return A rtc_date_time_t with the current date and time, with a year value from 0-63 representing 2020-2083.
|
||||||
* @see watch_rtc_set_date_time for notes about how the year is stored.
|
* @see watch_rtc_set_date_time for notes about how the year is stored.
|
||||||
*/
|
*/
|
||||||
@@ -73,26 +76,51 @@ rtc_date_time_t watch_rtc_get_date_time(void);
|
|||||||
*/
|
*/
|
||||||
rtc_date_time_t watch_get_init_date_time(void);
|
rtc_date_time_t watch_get_init_date_time(void);
|
||||||
|
|
||||||
/** @brief Registers an alarm callback that will be called when the RTC time matches the target time, as masked
|
/** @brief Set the current UTC date and time using a unix timestamp
|
||||||
* by the provided mask.
|
|
||||||
* @param callback The function you wish to have called when the alarm fires. If this value is NULL, the alarm
|
|
||||||
* interrupt will still be enabled, but no callback function will be called.
|
|
||||||
* @param alarm_time The time that you wish to match. The date is currently ignored.
|
|
||||||
* @param mask One of the values in rtc_alarm_match_t indicating which values to check.
|
|
||||||
* @details The alarm interrupt is a versatile tool for scheduling events in the future, especially since it can
|
|
||||||
* wake the device from all sleep modes. The key to its versatility is the mask parameter.
|
|
||||||
* Suppose we set an alarm for midnight, 00:00:00.
|
|
||||||
* * if mask is ALARM_MATCH_SS, the alarm will fire every minute when the clock ticks to seconds == 0.
|
|
||||||
* * with ALARM_MATCH_MMSS, the alarm will once an hour, at the top of each hour.
|
|
||||||
* * with ALARM_MATCH_HHMMSS, the alarm will fire at midnight every day.
|
|
||||||
* In theory the SAM L22's alarm function can match on days, months and even years, but I have not had
|
|
||||||
* success with this yet; as such, I am omitting these options for now.
|
|
||||||
*/
|
*/
|
||||||
void watch_rtc_register_alarm_callback(watch_cb_t callback, rtc_date_time_t alarm_time, rtc_alarm_match_t mask);
|
void watch_rtc_set_unix_time(unix_timestamp_t unix_time);
|
||||||
|
|
||||||
|
/** @brief Get the current UTC date and time using a unix timestamp
|
||||||
|
*/
|
||||||
|
unix_timestamp_t watch_rtc_get_unix_time(void);
|
||||||
|
|
||||||
|
/** @brief Get the current value of the internal hardware counter
|
||||||
|
* @details The counter starts at 0 and it increases at a 128Hz rate until it overflows and starts over.
|
||||||
|
* We never manually set the counter. Doing so allows us to calculate absolute elapsed and more.
|
||||||
|
* When the user sets the time, what is modified is the reference time (i.e. the date and time when
|
||||||
|
* the counter is 0).
|
||||||
|
*/
|
||||||
|
rtc_counter_t watch_rtc_get_counter(void);
|
||||||
|
|
||||||
|
/** @brief Get the RTC counter frequency.
|
||||||
|
*/
|
||||||
|
uint32_t watch_rtc_get_frequency(void);
|
||||||
|
|
||||||
|
/** @brief Get how many counter ticks are in one minute.
|
||||||
|
*/
|
||||||
|
uint32_t watch_rtc_get_ticks_per_minute(void);
|
||||||
|
|
||||||
|
/** @brief Registers a callback that will be called when the RTC counter matches the target counter.
|
||||||
|
* @param callback The function you wish to have called when the target counter is reached. If this value is NULL, the comp
|
||||||
|
* interrupt will still be enabled, but no callback function will be called.
|
||||||
|
* @param counter The time that you wish to match. The date is currently ignored.
|
||||||
|
* @param index We can have up to 8 active callbacks at a time. This parameter specifies which of the 8 callbacks should be set.
|
||||||
|
* @details The hardware RTC provides us with single interrupt that fires when the RTC counter matches a target counter COMP0.
|
||||||
|
* With a little bit of logic, we can provide multiple active compare callbacks. Every time a comp callback is
|
||||||
|
* registered/disabled/fired we iterate over all the active comp callbacks and set the hardware COMP0 counter
|
||||||
|
* to the next occurring one.
|
||||||
|
* With this very simple API, movement can implement one-shot timers to turn off the led and determine button longpresses
|
||||||
|
* as well as the inactivity timeouts for resigning and sleeping, as well as emulating the top of the minute alarm.
|
||||||
|
*/
|
||||||
|
void watch_rtc_register_comp_callback(watch_cb_t callback, rtc_counter_t counter, uint8_t index);
|
||||||
|
|
||||||
|
/** @brief Disables the specified comp callback.
|
||||||
|
*/
|
||||||
|
void watch_rtc_disable_comp_callback(uint8_t index);
|
||||||
|
|
||||||
/** @brief Disables the alarm callback.
|
/** @brief Disables the alarm callback.
|
||||||
*/
|
*/
|
||||||
void watch_rtc_disable_alarm_callback(void);
|
// void watch_rtc_disable_alarm_callback(void);
|
||||||
|
|
||||||
/** @brief Registers a "tick" callback that will be called once per second.
|
/** @brief Registers a "tick" callback that will be called once per second.
|
||||||
* @param callback The function you wish to have called when the clock ticks. If you pass in NULL, the tick
|
* @param callback The function you wish to have called when the clock ticks. If you pass in NULL, the tick
|
||||||
@@ -117,10 +145,6 @@ void watch_rtc_disable_tick_callback(void);
|
|||||||
* tick at 16 or 32 Hz to update the screen more quickly. Just remember that the more frequent the tick, the more
|
* tick at 16 or 32 Hz to update the screen more quickly. Just remember that the more frequent the tick, the more
|
||||||
* power your app will consume. Ideally you should enable the fast tick only when the user requires it (i.e. in
|
* power your app will consume. Ideally you should enable the fast tick only when the user requires it (i.e. in
|
||||||
* response to an input event), and move back to the slow tick after some time.
|
* response to an input event), and move back to the slow tick after some time.
|
||||||
*
|
|
||||||
* Also note that the RTC peripheral does not have sub-second resolution, so even if you set a 2 or 4 Hz interval,
|
|
||||||
* the system will not have any way of telling you where you are within a given second; watch_rtc_get_date_time
|
|
||||||
* will return the exact same timestamp until the second ticks over.
|
|
||||||
*/
|
*/
|
||||||
void watch_rtc_register_periodic_callback(watch_cb_t callback, uint8_t frequency);
|
void watch_rtc_register_periodic_callback(watch_cb_t callback, uint8_t frequency);
|
||||||
|
|
||||||
|
|||||||
@@ -170,8 +170,6 @@ void watch_set_buzzer_off(void);
|
|||||||
/** @brief Plays the given note for a set duration at the loudest possible volume.
|
/** @brief Plays the given note for a set duration at the loudest possible volume.
|
||||||
* @param note The note you wish to play, or BUZZER_NOTE_REST to disable output for the given duration.
|
* @param note The note you wish to play, or BUZZER_NOTE_REST to disable output for the given duration.
|
||||||
* @param duration_ms The duration of the note.
|
* @param duration_ms The duration of the note.
|
||||||
* @note Note that this will block your UI for the duration of the note's play time, and it will
|
|
||||||
* after this call, the buzzer period will be set to the period of this note.
|
|
||||||
*/
|
*/
|
||||||
void watch_buzzer_play_note(watch_buzzer_note_t note, uint16_t duration_ms);
|
void watch_buzzer_play_note(watch_buzzer_note_t note, uint16_t duration_ms);
|
||||||
|
|
||||||
@@ -179,8 +177,6 @@ void watch_buzzer_play_note(watch_buzzer_note_t note, uint16_t duration_ms);
|
|||||||
* @param note The note you wish to play, or BUZZER_NOTE_REST to disable output for the given duration.
|
* @param note The note you wish to play, or BUZZER_NOTE_REST to disable output for the given duration.
|
||||||
* @param duration_ms The duration of the note.
|
* @param duration_ms The duration of the note.
|
||||||
* @param volume either WATCH_BUZZER_VOLUME_SOFT or WATCH_BUZZER_VOLUME_LOUD
|
* @param volume either WATCH_BUZZER_VOLUME_SOFT or WATCH_BUZZER_VOLUME_LOUD
|
||||||
* @note This will block your UI for the duration of the note's play time, and after this call, the
|
|
||||||
* buzzer will stop sounding, but the TCC period will remain set to the period of this note.
|
|
||||||
*/
|
*/
|
||||||
void watch_buzzer_play_note_with_volume(watch_buzzer_note_t note, uint16_t duration_ms, watch_buzzer_volume_t volume);
|
void watch_buzzer_play_note_with_volume(watch_buzzer_note_t note, uint16_t duration_ms, watch_buzzer_volume_t volume);
|
||||||
|
|
||||||
@@ -202,10 +198,36 @@ extern const uint16_t NotePeriods[108];
|
|||||||
*/
|
*/
|
||||||
void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void));
|
void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void));
|
||||||
|
|
||||||
|
/** @brief Plays the given sequence of notes in a non-blocking way.
|
||||||
|
* @param note_sequence A pointer to the sequence of buzzer note & duration tuples, ending with a zero. A simple
|
||||||
|
* RLE logic is implemented: a negative number instead of a buzzer note means that the sequence
|
||||||
|
* is rewound by the given number of notes. The byte following a negative number determines the number
|
||||||
|
* of loops. I.e. if you want to repeat the last three notes of the sequence one time, you should provide
|
||||||
|
* the tuple -3, 1. The repeated notes must not contain any other repeat markers, or you will end up with
|
||||||
|
* an eternal loop.
|
||||||
|
* @param callback_on_end A pointer to a callback function to be invoked when the sequence has finished playing.
|
||||||
|
* @param volume either WATCH_BUZZER_VOLUME_SOFT or WATCH_BUZZER_VOLUME_LOUD
|
||||||
|
*/
|
||||||
|
void watch_buzzer_play_sequence_with_volume(int8_t *note_sequence, void (*callback_on_end)(void), watch_buzzer_volume_t volume);
|
||||||
|
|
||||||
/** @brief Aborts a playing sequence.
|
/** @brief Aborts a playing sequence.
|
||||||
*/
|
*/
|
||||||
void watch_buzzer_abort_sequence(void);
|
void watch_buzzer_abort_sequence(void);
|
||||||
|
|
||||||
|
void watch_buzzer_register_global_callbacks(watch_cb_t cb_start, watch_cb_t cb_stop);
|
||||||
|
|
||||||
|
/** @brief Enables the TCC peripheral, which drives the buzzer and the leds.
|
||||||
|
*/
|
||||||
|
void watch_enable_buzzer_and_leds(void);
|
||||||
|
|
||||||
|
/** @brief Disables the TCC peripheral that drives the buzzer and the leds.
|
||||||
|
*/
|
||||||
|
void watch_disable_buzzer_and_leds(void);
|
||||||
|
|
||||||
|
/** @brief Disables the TCC peripheral that drives the buzzer and the leds if neither is currently active
|
||||||
|
*/
|
||||||
|
void watch_maybe_disable_buzzer_and_leds(void);
|
||||||
|
|
||||||
#ifndef __EMSCRIPTEN__
|
#ifndef __EMSCRIPTEN__
|
||||||
void irq_handler_tc0(void);
|
void irq_handler_tc0(void);
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -278,6 +278,10 @@ watch_date_time_t watch_utility_date_time_convert_zone(watch_date_time_t date_ti
|
|||||||
return watch_utility_date_time_from_unix_time(timestamp, destination_utc_offset);
|
return watch_utility_date_time_from_unix_time(timestamp, destination_utc_offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint32_t watch_utility_unix_time_convert_zone(uint32_t timestamp, uint32_t origin_utc_offset, uint32_t destination_utc_offset) {
|
||||||
|
return timestamp - origin_utc_offset + destination_utc_offset;
|
||||||
|
}
|
||||||
|
|
||||||
watch_duration_t watch_utility_seconds_to_duration(uint32_t seconds) {
|
watch_duration_t watch_utility_seconds_to_duration(uint32_t seconds) {
|
||||||
watch_duration_t retval;
|
watch_duration_t retval;
|
||||||
|
|
||||||
|
|||||||
@@ -144,6 +144,16 @@ bool watch_utility_convert_to_12_hour(watch_date_time_t *date_time);
|
|||||||
*/
|
*/
|
||||||
watch_date_time_t watch_utility_date_time_convert_zone(watch_date_time_t date_time, uint32_t origin_utc_offset, uint32_t destination_utc_offset);
|
watch_date_time_t watch_utility_date_time_convert_zone(watch_date_time_t date_time, uint32_t origin_utc_offset, uint32_t destination_utc_offset);
|
||||||
|
|
||||||
|
/** @brief Converts a unix time from a given time zone to another time zone.
|
||||||
|
* @param timestamp The unix time that you wish to convert
|
||||||
|
* @param origin_utc_offset The number of seconds from UTC in the origin time zone
|
||||||
|
* @param destination_utc_offset The number of seconds from UTC in the destination time zone
|
||||||
|
* @return A unix time for the given UNIX timestamp and UTC offset.
|
||||||
|
* @note Adapted from MIT-licensed code from musl, Copyright © 2005-2014 Rich Felker, et al.:
|
||||||
|
* https://github.com/esmil/musl/blob/1cc81f5cb0df2b66a795ff0c26d7bbc4d16e13c6/src/time/__secs_to_tm.c
|
||||||
|
*/
|
||||||
|
uint32_t watch_utility_unix_time_convert_zone(uint32_t timestamp, uint32_t origin_utc_offset, uint32_t destination_utc_offset);
|
||||||
|
|
||||||
/** @brief Returns a temperature in degrees Celsius for a given thermistor voltage divider circuit.
|
/** @brief Returns a temperature in degrees Celsius for a given thermistor voltage divider circuit.
|
||||||
* @param value The raw analog reading from the thermistor pin (0-65535)
|
* @param value The raw analog reading from the thermistor pin (0-65535)
|
||||||
* @param highside True if the thermistor is connected to VCC and the series resistor is connected
|
* @param highside True if the thermistor is connected to VCC and the series resistor is connected
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
#include <limits.h>
|
||||||
|
|
||||||
#include "watch_rtc.h"
|
#include "watch_rtc.h"
|
||||||
#include "watch_main_loop.h"
|
#include "watch_main_loop.h"
|
||||||
@@ -29,8 +30,28 @@
|
|||||||
#include <emscripten.h>
|
#include <emscripten.h>
|
||||||
#include <emscripten/html5.h>
|
#include <emscripten/html5.h>
|
||||||
|
|
||||||
|
static const uint32_t RTC_CNT_HZ = 128;
|
||||||
|
static const uint32_t RTC_CNT_DIV = 7;
|
||||||
|
static const uint32_t RTC_CNT_TICKS_PER_MINUTE = RTC_CNT_HZ * 60;
|
||||||
|
static const uint32_t RTC_CNT_TICKS_PER_HOUR = RTC_CNT_TICKS_PER_MINUTE * 60;
|
||||||
|
|
||||||
|
static uint32_t counter_interval;
|
||||||
|
static uint32_t counter;
|
||||||
|
static uint32_t reference_timestamp;
|
||||||
|
|
||||||
|
#define WATCH_RTC_N_COMP_CB 8
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
volatile uint32_t counter;
|
||||||
|
volatile watch_cb_t callback;
|
||||||
|
volatile bool enabled;
|
||||||
|
} comp_cb_t;
|
||||||
|
|
||||||
static double time_offset = 0;
|
static double time_offset = 0;
|
||||||
static long tick_callbacks[8] = { -1, -1, -1, -1, -1, -1, -1, -1 };
|
watch_cb_t tick_callbacks[8];
|
||||||
|
comp_cb_t comp_callbacks[WATCH_RTC_N_COMP_CB];
|
||||||
|
|
||||||
|
static uint32_t scheduled_comp_counter;
|
||||||
|
|
||||||
static long alarm_interval_id = -1;
|
static long alarm_interval_id = -1;
|
||||||
static long alarm_timeout_id = -1;
|
static long alarm_timeout_id = -1;
|
||||||
@@ -40,41 +61,73 @@ watch_cb_t btn_alarm_callback;
|
|||||||
watch_cb_t a2_callback;
|
watch_cb_t a2_callback;
|
||||||
watch_cb_t a4_callback;
|
watch_cb_t a4_callback;
|
||||||
|
|
||||||
|
static void _watch_increase_counter(void *userData);
|
||||||
|
static void _watch_process_periodic_callbacks(void);
|
||||||
|
static void _watch_process_comp_callbacks(void);
|
||||||
|
|
||||||
bool _watch_rtc_is_enabled(void) {
|
bool _watch_rtc_is_enabled(void) {
|
||||||
return true;
|
return counter_interval;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _watch_rtc_init(void) {
|
void _watch_rtc_init(void) {
|
||||||
#if EMSCRIPTEN
|
for (uint8_t index = 0; index < 8; ++index) {
|
||||||
// Shifts the timezone so our local time is converted to UTC and set
|
tick_callbacks[index] = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) {
|
||||||
|
comp_callbacks[index].counter = 0;
|
||||||
|
comp_callbacks[index].callback = NULL;
|
||||||
|
comp_callbacks[index].enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduled_comp_counter = 0;
|
||||||
|
counter = 0;
|
||||||
|
counter_interval = 0;
|
||||||
|
|
||||||
|
watch_rtc_set_date_time(watch_get_init_date_time());
|
||||||
|
watch_rtc_enable(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void watch_rtc_set_date_time(rtc_date_time_t date_time) {
|
||||||
|
watch_rtc_set_unix_time(watch_utility_date_time_to_unix_time(date_time, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
rtc_date_time_t watch_rtc_get_date_time(void) {
|
||||||
|
return watch_utility_date_time_from_unix_time(watch_rtc_get_unix_time(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void watch_rtc_set_unix_time(unix_timestamp_t unix_time) {
|
||||||
|
// time_backup + counter / RTC_CNT_HZ = unix_time
|
||||||
|
rtc_counter_t counter = watch_rtc_get_counter();
|
||||||
|
reference_timestamp = unix_time - (counter >> RTC_CNT_DIV);
|
||||||
|
}
|
||||||
|
|
||||||
|
unix_timestamp_t watch_rtc_get_unix_time(void) {
|
||||||
|
// time_backup + counter / RTC_CNT_HZ = unix_time
|
||||||
|
rtc_counter_t counter = watch_rtc_get_counter();
|
||||||
|
return reference_timestamp + (counter >> RTC_CNT_DIV);
|
||||||
|
}
|
||||||
|
|
||||||
|
rtc_counter_t watch_rtc_get_counter(void) {
|
||||||
|
return counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t watch_rtc_get_frequency(void) {
|
||||||
|
return RTC_CNT_HZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t watch_rtc_get_ticks_per_minute(void) {
|
||||||
|
return RTC_CNT_TICKS_PER_MINUTE;
|
||||||
|
}
|
||||||
|
|
||||||
|
rtc_date_time_t watch_get_init_date_time(void) {
|
||||||
|
rtc_date_time_t date_time = {0};
|
||||||
|
|
||||||
int32_t time_zone_offset = EM_ASM_INT({
|
int32_t time_zone_offset = EM_ASM_INT({
|
||||||
return -new Date().getTimezoneOffset() * 60;
|
return new Date().getTimezoneOffset() * 60 * 1000; // ms
|
||||||
});
|
});
|
||||||
#endif
|
|
||||||
#ifdef BUILD_YEAR
|
|
||||||
watch_date_time_t date_time = watch_get_init_date_time();
|
|
||||||
#else
|
|
||||||
watch_date_time_t date_time = watch_rtc_get_date_time();
|
|
||||||
#endif
|
|
||||||
watch_rtc_set_date_time(watch_utility_date_time_convert_zone(date_time, time_zone_offset, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
void watch_rtc_set_date_time(watch_date_time_t date_time) {
|
date_time.reg = EM_ASM_INT({
|
||||||
time_offset = EM_ASM_DOUBLE({
|
|
||||||
const year = 2020 + (($0 >> 26) & 0x3f);
|
|
||||||
const month = ($0 >> 22) & 0xf;
|
|
||||||
const day = ($0 >> 17) & 0x1f;
|
|
||||||
const hour = ($0 >> 12) & 0x1f;
|
|
||||||
const minute = ($0 >> 6) & 0x3f;
|
|
||||||
const second = $0 & 0x3f;
|
|
||||||
const date = new Date(year, month - 1, day, hour, minute, second);
|
|
||||||
return date - Date.now();
|
|
||||||
}, date_time.reg);
|
|
||||||
}
|
|
||||||
|
|
||||||
watch_date_time_t watch_rtc_get_date_time(void) {
|
|
||||||
watch_date_time_t retval;
|
|
||||||
retval.reg = EM_ASM_INT({
|
|
||||||
const date = new Date(Date.now() + $0);
|
const date = new Date(Date.now() + $0);
|
||||||
return date.getSeconds() |
|
return date.getSeconds() |
|
||||||
(date.getMinutes() << 6) |
|
(date.getMinutes() << 6) |
|
||||||
@@ -82,27 +135,16 @@ watch_date_time_t watch_rtc_get_date_time(void) {
|
|||||||
(date.getDate() << 17) |
|
(date.getDate() << 17) |
|
||||||
((date.getMonth() + 1) << 22) |
|
((date.getMonth() + 1) << 22) |
|
||||||
((date.getFullYear() - 2020) << 26);
|
((date.getFullYear() - 2020) << 26);
|
||||||
}, time_offset);
|
}, time_zone_offset);
|
||||||
return retval;
|
|
||||||
}
|
|
||||||
|
|
||||||
rtc_date_time_t watch_get_init_date_time(void) {
|
|
||||||
rtc_date_time_t date_time = {0};
|
|
||||||
|
|
||||||
#ifdef BUILD_YEAR
|
#ifdef BUILD_YEAR
|
||||||
date_time.unit.year = BUILD_YEAR;
|
date_time.unit.year = BUILD_YEAR;
|
||||||
#else
|
|
||||||
date_time.unit.year = 5;
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef BUILD_MONTH
|
#ifdef BUILD_MONTH
|
||||||
date_time.unit.month = BUILD_MONTH;
|
date_time.unit.month = BUILD_MONTH;
|
||||||
#else
|
|
||||||
date_time.unit.month = 1;
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef BUILD_DAY
|
#ifdef BUILD_DAY
|
||||||
date_time.unit.day = BUILD_DAY;
|
date_time.unit.day = BUILD_DAY;
|
||||||
#else
|
|
||||||
date_time.unit.day = 1;
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef BUILD_HOUR
|
#ifdef BUILD_HOUR
|
||||||
date_time.unit.hour = BUILD_HOUR;
|
date_time.unit.hour = BUILD_HOUR;
|
||||||
@@ -122,12 +164,72 @@ void watch_rtc_disable_tick_callback(void) {
|
|||||||
watch_rtc_disable_periodic_callback(1);
|
watch_rtc_disable_periodic_callback(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void watch_invoke_periodic_callback(void *userData) {
|
static void _watch_increase_counter(void *userData) {
|
||||||
watch_cb_t callback = userData;
|
(void) userData;
|
||||||
callback();
|
|
||||||
|
counter += 1;
|
||||||
|
// Fire the periodic callbacks that match this counter
|
||||||
|
_watch_process_periodic_callbacks();
|
||||||
|
// Fire the comp callbacks that match this counter
|
||||||
|
_watch_process_comp_callbacks();
|
||||||
|
|
||||||
resume_main_loop();
|
resume_main_loop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void _watch_process_periodic_callbacks(void) {
|
||||||
|
/* It looks weird but it follows the way the hardware triggers periodic interrupts.
|
||||||
|
* For 128hz counter periodic interrupts fire at these tick values:
|
||||||
|
* 1Hz: 64
|
||||||
|
* 2Hz: 32, 96
|
||||||
|
* 4Hz: 16, 48, 80, 112
|
||||||
|
* 8Hz: 8, 24, 40, 56, 72, 88, 104, 120
|
||||||
|
* 16Hz: 4, 12, 20, ..., 124
|
||||||
|
* 32Hz: 2, 6, 10, ..., 126
|
||||||
|
* 64Hz: 1, 3, 5, ..., 127
|
||||||
|
* 128Hz: 0, 1, 2, ..., 127
|
||||||
|
*
|
||||||
|
* Which means that only one periodic interrupt can fire for a given counter value
|
||||||
|
* (except 128Hz which can always fire)
|
||||||
|
*/
|
||||||
|
|
||||||
|
uint32_t freq = watch_rtc_get_frequency();
|
||||||
|
uint32_t subsecond_mask = freq - 1;
|
||||||
|
uint32_t subseconds = counter & subsecond_mask;
|
||||||
|
|
||||||
|
// Find the firs non-zero bit in the counter, which can be used to determine the appropriate period (see table above).
|
||||||
|
uint8_t per_n = 0;
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < 7; i++) {
|
||||||
|
if (subseconds & (1 << i)) {
|
||||||
|
per_n = i + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tick_callbacks[per_n]) {
|
||||||
|
tick_callbacks[per_n]();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 128Hz is always a match
|
||||||
|
if (per_n != 0 && tick_callbacks[0]) {
|
||||||
|
tick_callbacks[0]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _watch_process_comp_callbacks(void) {
|
||||||
|
// In hardware the interrupt fires one tick after the matching counter
|
||||||
|
if (counter == (scheduled_comp_counter + 1)) {
|
||||||
|
for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) {
|
||||||
|
if (comp_callbacks[index].enabled && scheduled_comp_counter == comp_callbacks[index].counter) {
|
||||||
|
comp_callbacks[index].enabled = false;
|
||||||
|
comp_callbacks[index].callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_watch_rtc_schedule_next_comp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void watch_rtc_register_periodic_callback(watch_cb_t callback, uint8_t frequency) {
|
void watch_rtc_register_periodic_callback(watch_cb_t callback, uint8_t frequency) {
|
||||||
// we told them, it has to be a power of 2.
|
// we told them, it has to be a power of 2.
|
||||||
if (__builtin_popcount(frequency) != 1) return;
|
if (__builtin_popcount(frequency) != 1) return;
|
||||||
@@ -138,26 +240,19 @@ void watch_rtc_register_periodic_callback(watch_cb_t callback, uint8_t frequency
|
|||||||
// 0x01 (1 Hz) will have 7 leading zeros for PER7. 0xF0 (128 Hz) will have no leading zeroes for PER0.
|
// 0x01 (1 Hz) will have 7 leading zeros for PER7. 0xF0 (128 Hz) will have no leading zeroes for PER0.
|
||||||
uint8_t per_n = __builtin_clz(tmp);
|
uint8_t per_n = __builtin_clz(tmp);
|
||||||
|
|
||||||
double interval = 1000.0 / frequency; // in msec
|
tick_callbacks[per_n] = callback;
|
||||||
|
|
||||||
if (tick_callbacks[per_n] != -1) emscripten_clear_interval(tick_callbacks[per_n]);
|
|
||||||
tick_callbacks[per_n] = emscripten_set_interval(watch_invoke_periodic_callback, interval, (void *)callback);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void watch_rtc_disable_periodic_callback(uint8_t frequency) {
|
void watch_rtc_disable_periodic_callback(uint8_t frequency) {
|
||||||
if (__builtin_popcount(frequency) != 1) return;
|
if (__builtin_popcount(frequency) != 1) return;
|
||||||
uint8_t per_n = __builtin_clz((frequency & 0xFF) << 24);
|
uint8_t per_n = __builtin_clz((frequency & 0xFF) << 24);
|
||||||
if (tick_callbacks[per_n] != -1) {
|
tick_callbacks[per_n] = NULL;
|
||||||
emscripten_clear_interval(tick_callbacks[per_n]);
|
|
||||||
tick_callbacks[per_n] = -1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void watch_rtc_disable_matching_periodic_callbacks(uint8_t mask) {
|
void watch_rtc_disable_matching_periodic_callbacks(uint8_t mask) {
|
||||||
for (int i = 0; i < 8; i++) {
|
for (int i = 0; i < 8; i++) {
|
||||||
if (tick_callbacks[i] != -1 && (mask & (1 << i)) != 0) {
|
if (tick_callbacks[i] && (mask & (1 << i)) != 0) {
|
||||||
emscripten_clear_interval(tick_callbacks[i]);
|
tick_callbacks[i] = NULL;
|
||||||
tick_callbacks[i] = -1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,81 +261,70 @@ void watch_rtc_disable_all_periodic_callbacks(void) {
|
|||||||
watch_rtc_disable_matching_periodic_callbacks(0xFF);
|
watch_rtc_disable_matching_periodic_callbacks(0xFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void watch_invoke_alarm_interval_callback(void *userData) {
|
void watch_rtc_register_comp_callback(watch_cb_t callback, rtc_counter_t counter, uint8_t index) {
|
||||||
if (alarm_callback) alarm_callback();
|
if (index >= WATCH_RTC_N_COMP_CB) {
|
||||||
}
|
|
||||||
|
|
||||||
static void watch_invoke_alarm_callback(void *userData) {
|
|
||||||
if (alarm_callback) alarm_callback();
|
|
||||||
alarm_interval_id = emscripten_set_interval(watch_invoke_alarm_interval_callback, alarm_interval, NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
void watch_rtc_register_alarm_callback(watch_cb_t callback, watch_date_time_t alarm_time, rtc_alarm_match_t mask) {
|
|
||||||
watch_rtc_disable_alarm_callback();
|
|
||||||
|
|
||||||
switch (mask) {
|
|
||||||
case ALARM_MATCH_DISABLED:
|
|
||||||
return;
|
return;
|
||||||
case ALARM_MATCH_SS:
|
|
||||||
alarm_interval = 60 * 1000;
|
|
||||||
break;
|
|
||||||
case ALARM_MATCH_MMSS:
|
|
||||||
alarm_interval = 60 * 60 * 1000;
|
|
||||||
break;
|
|
||||||
case ALARM_MATCH_HHMMSS:
|
|
||||||
alarm_interval = 60 * 60 * 60 * 1000;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
double timeout = EM_ASM_DOUBLE({
|
comp_callbacks[index].counter = counter;
|
||||||
const now = Date.now();
|
comp_callbacks[index].callback = callback;
|
||||||
const date = new Date(now);
|
comp_callbacks[index].enabled = true;
|
||||||
|
|
||||||
const hour = ($0 >> 12) & 0x1f;
|
_watch_rtc_schedule_next_comp();
|
||||||
const minute = ($0 >> 6) & 0x3f;
|
|
||||||
const second = $0 & 0x3f;
|
|
||||||
|
|
||||||
if ($1 == 1) { // SS
|
|
||||||
if (second < date.getSeconds()) date.setMinutes(date.getMinutes() + 1);
|
|
||||||
date.setSeconds(second);
|
|
||||||
} else if ($1 == 2) { // MMSS
|
|
||||||
if (second < date.getSeconds()) date.setMinutes(date.getMinutes() + 1);
|
|
||||||
if (minute < date.getMinutes()) date.setHours(date.getHours() + 1);
|
|
||||||
date.setMinutes(minute, second);
|
|
||||||
} else if ($1 == 3) { // HHMMSS
|
|
||||||
if (second < date.getSeconds()) date.setMinutes(date.getMinutes() + 1);
|
|
||||||
if (minute < date.getMinutes()) date.setHours(date.getHours() + 1);
|
|
||||||
if (hour < date.getHours()) date.setDate(date.getDate() + 1);
|
|
||||||
date.setHours(hour, minute, second);
|
|
||||||
} else {
|
|
||||||
throw 'Invalid alarm match mask';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return date - now;
|
void watch_rtc_disable_comp_callback(uint8_t index) {
|
||||||
}, alarm_time.reg, mask);
|
if (index >= WATCH_RTC_N_COMP_CB) {
|
||||||
|
return;
|
||||||
alarm_callback = callback;
|
|
||||||
alarm_timeout_id = emscripten_set_timeout(watch_invoke_alarm_callback, timeout, NULL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void watch_rtc_disable_alarm_callback(void) {
|
comp_callbacks[index].enabled = false;
|
||||||
alarm_callback = NULL;
|
|
||||||
alarm_interval = 0;
|
|
||||||
|
|
||||||
if (alarm_timeout_id != -1) {
|
_watch_rtc_schedule_next_comp();
|
||||||
emscripten_clear_timeout(alarm_timeout_id);
|
|
||||||
alarm_timeout_id = -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (alarm_interval_id != -1) {
|
void _watch_rtc_schedule_next_comp(void) {
|
||||||
emscripten_clear_interval(alarm_interval_id);
|
scheduled_comp_counter = 0;
|
||||||
alarm_interval_id = -1;
|
|
||||||
|
// The soonest we can schedule is the next tick
|
||||||
|
rtc_counter_t curr_counter = watch_rtc_get_counter() + 1;
|
||||||
|
|
||||||
|
bool schedule_any = false;
|
||||||
|
rtc_counter_t comp_counter;
|
||||||
|
rtc_counter_t min_diff = UINT_MAX;
|
||||||
|
|
||||||
|
for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) {
|
||||||
|
// rtc_counter_t diff =
|
||||||
|
if (comp_callbacks[index].enabled) {
|
||||||
|
rtc_counter_t diff = comp_callbacks[index].counter - curr_counter;
|
||||||
|
if (diff <= min_diff) {
|
||||||
|
min_diff = diff;
|
||||||
|
comp_counter = comp_callbacks[index].counter;
|
||||||
|
schedule_any = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schedule_any) {
|
||||||
|
scheduled_comp_counter = comp_counter;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void watch_rtc_enable(bool en)
|
void watch_rtc_enable(bool en)
|
||||||
{
|
{
|
||||||
//Not simulated
|
// Nothing to do cases
|
||||||
|
if ((en && counter_interval) || (!en && !counter_interval)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (en) {
|
||||||
|
// Very bad way to keep time, but okay way to emulates the hardware.
|
||||||
|
double ms = 1000.0 / (double)RTC_CNT_HZ; // in msec
|
||||||
|
counter_interval = emscripten_set_interval(_watch_increase_counter, ms, NULL);
|
||||||
|
} else {
|
||||||
|
emscripten_clear_interval(counter_interval);
|
||||||
|
counter_interval = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void watch_rtc_freqcorr_write(int16_t value, int16_t sign)
|
void watch_rtc_freqcorr_write(int16_t value, int16_t sign)
|
||||||
|
|||||||
@@ -28,16 +28,20 @@
|
|||||||
#include <emscripten.h>
|
#include <emscripten.h>
|
||||||
#include <emscripten/html5.h>
|
#include <emscripten/html5.h>
|
||||||
|
|
||||||
static bool buzzer_enabled = false;
|
static volatile bool buzzer_enabled = false;
|
||||||
static uint32_t buzzer_period;
|
static uint32_t buzzer_period;
|
||||||
|
|
||||||
void cb_watch_buzzer_seq(void *userData);
|
void cb_watch_buzzer_seq(void *userData);
|
||||||
|
|
||||||
static uint16_t _seq_position;
|
static uint16_t _seq_position;
|
||||||
static int8_t _tone_ticks, _repeat_counter;
|
static int8_t _tone_ticks, _repeat_counter;
|
||||||
static long _em_interval_id = 0;
|
static volatile long _em_interval_id = 0;
|
||||||
static int8_t *_sequence;
|
static int8_t *_sequence;
|
||||||
|
static uint8_t _volume;
|
||||||
static void (*_cb_finished)(void);
|
static void (*_cb_finished)(void);
|
||||||
|
static watch_cb_t _cb_start_global = NULL;
|
||||||
|
static watch_cb_t _cb_stop_global = NULL;
|
||||||
|
static volatile bool _buzzer_is_active = false;
|
||||||
|
|
||||||
void _watch_enable_tcc(void) {}
|
void _watch_enable_tcc(void) {}
|
||||||
|
|
||||||
@@ -47,15 +51,27 @@ static inline void _em_interval_stop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void)) {
|
void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void)) {
|
||||||
if (_em_interval_id) _em_interval_stop();
|
watch_buzzer_play_sequence_with_volume(note_sequence, callback_on_end, WATCH_BUZZER_VOLUME_LOUD);
|
||||||
watch_set_buzzer_off();
|
}
|
||||||
|
|
||||||
|
void watch_buzzer_play_sequence_with_volume(int8_t *note_sequence, void (*callback_on_end)(void), watch_buzzer_volume_t volume) {
|
||||||
|
watch_buzzer_abort_sequence();
|
||||||
|
|
||||||
|
_buzzer_is_active = true;
|
||||||
|
|
||||||
|
if (_cb_start_global) {
|
||||||
|
_cb_start_global();
|
||||||
|
}
|
||||||
|
|
||||||
_sequence = note_sequence;
|
_sequence = note_sequence;
|
||||||
_cb_finished = callback_on_end;
|
_cb_finished = callback_on_end;
|
||||||
|
_volume = volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25;
|
||||||
_seq_position = 0;
|
_seq_position = 0;
|
||||||
_tone_ticks = 0;
|
_tone_ticks = 0;
|
||||||
_repeat_counter = -1;
|
_repeat_counter = -1;
|
||||||
// prepare buzzer
|
// prepare buzzer
|
||||||
watch_enable_buzzer();
|
watch_enable_buzzer();
|
||||||
|
watch_set_buzzer_off();
|
||||||
// initiate 64 hz callback
|
// initiate 64 hz callback
|
||||||
_em_interval_id = emscripten_set_interval(cb_watch_buzzer_seq, (double)(1000/64), (void *)NULL);
|
_em_interval_id = emscripten_set_interval(cb_watch_buzzer_seq, (double)(1000/64), (void *)NULL);
|
||||||
}
|
}
|
||||||
@@ -88,7 +104,7 @@ void cb_watch_buzzer_seq(void *userData) {
|
|||||||
if (note == BUZZER_NOTE_REST) {
|
if (note == BUZZER_NOTE_REST) {
|
||||||
watch_set_buzzer_off();
|
watch_set_buzzer_off();
|
||||||
} else {
|
} else {
|
||||||
watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], 25);
|
watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], _volume);
|
||||||
watch_set_buzzer_on();
|
watch_set_buzzer_on();
|
||||||
}
|
}
|
||||||
// set duration ticks and move to next tone
|
// set duration ticks and move to next tone
|
||||||
@@ -97,7 +113,6 @@ void cb_watch_buzzer_seq(void *userData) {
|
|||||||
} else {
|
} else {
|
||||||
// end the sequence
|
// end the sequence
|
||||||
watch_buzzer_abort_sequence();
|
watch_buzzer_abort_sequence();
|
||||||
if (_cb_finished) _cb_finished();
|
|
||||||
}
|
}
|
||||||
} else _tone_ticks--;
|
} else _tone_ticks--;
|
||||||
}
|
}
|
||||||
@@ -105,10 +120,32 @@ void cb_watch_buzzer_seq(void *userData) {
|
|||||||
void watch_buzzer_abort_sequence(void) {
|
void watch_buzzer_abort_sequence(void) {
|
||||||
// ends/aborts the sequence
|
// ends/aborts the sequence
|
||||||
if (_em_interval_id) _em_interval_stop();
|
if (_em_interval_id) _em_interval_stop();
|
||||||
|
|
||||||
watch_set_buzzer_off();
|
watch_set_buzzer_off();
|
||||||
|
watch_disable_buzzer();
|
||||||
|
|
||||||
|
if (!_buzzer_is_active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_buzzer_is_active = false;
|
||||||
|
|
||||||
|
if (_cb_stop_global) {
|
||||||
|
_cb_stop_global();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_cb_finished) {
|
||||||
|
_cb_finished();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void watch_buzzer_register_global_callbacks(watch_cb_t cb_start, watch_cb_t cb_stop) {
|
||||||
|
_cb_stop_global = cb_start;
|
||||||
|
_cb_stop_global = cb_stop;
|
||||||
}
|
}
|
||||||
|
|
||||||
void watch_enable_buzzer(void) {
|
void watch_enable_buzzer(void) {
|
||||||
|
watch_buzzer_abort_sequence();
|
||||||
buzzer_enabled = true;
|
buzzer_enabled = true;
|
||||||
buzzer_period = NotePeriods[BUZZER_NOTE_A4];
|
buzzer_period = NotePeriods[BUZZER_NOTE_A4];
|
||||||
|
|
||||||
@@ -175,15 +212,17 @@ void watch_buzzer_play_note(watch_buzzer_note_t note, uint16_t duration_ms) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void watch_buzzer_play_note_with_volume(watch_buzzer_note_t note, uint16_t duration_ms, watch_buzzer_volume_t volume) {
|
void watch_buzzer_play_note_with_volume(watch_buzzer_note_t note, uint16_t duration_ms, watch_buzzer_volume_t volume) {
|
||||||
if (note == BUZZER_NOTE_REST) {
|
static int8_t single_note_sequence[3];
|
||||||
watch_set_buzzer_off();
|
|
||||||
} else {
|
|
||||||
watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25);
|
|
||||||
watch_set_buzzer_on();
|
|
||||||
}
|
|
||||||
|
|
||||||
main_loop_sleep(duration_ms);
|
single_note_sequence[0] = note;
|
||||||
watch_set_buzzer_off();
|
// 64 ticks per second for the tc0?
|
||||||
|
// Each tick is approximately 15ms
|
||||||
|
uint16_t duration = duration_ms / 15;
|
||||||
|
if (duration > 127) duration = 127;
|
||||||
|
single_note_sequence[1] = (int8_t)duration;
|
||||||
|
single_note_sequence[2] = 0;
|
||||||
|
|
||||||
|
watch_buzzer_play_sequence_with_volume(single_note_sequence, NULL, volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
void watch_enable_leds(void) {}
|
void watch_enable_leds(void) {}
|
||||||
|
|||||||
Reference in New Issue
Block a user