Merge pull request #65 from alesgenova/rtc-counter32

Make movement use the hardware RTC in COUNTER32 mode and other improvements
This commit is contained in:
Joey Castillo
2026-01-03 09:37:59 +08:00
committed by GitHub
41 changed files with 2730 additions and 1055 deletions

View File

@ -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 \

View File

@ -448,13 +448,13 @@ static void start_reading(accelerometer_data_acquisition_state_t *state) {
state->records[state->pos++] = record; state->records[state->pos++] = record;
lis2dw_fifo_t fifo; lis2dw_fifo_t fifo;
lis2dw_read_fifo(&fifo); // dump the fifo, this starts a fresh round of data in continue_reading lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT); // dump the fifo, this starts a fresh round of data in continue_reading
} }
static void continue_reading(accelerometer_data_acquisition_state_t *state) { static void continue_reading(accelerometer_data_acquisition_state_t *state) {
printf("Continue reading\n"); printf("Continue reading\n");
lis2dw_fifo_t fifo; lis2dw_fifo_t fifo;
lis2dw_read_fifo(&fifo); lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT);
fifo.count = min(fifo.count, 25); // hacky, but we need a consistent data rate; if we got a 26th data point, chuck it. fifo.count = min(fifo.count, 25); // hacky, but we need a consistent data rate; if we got a 26th data point, chuck it.
uint8_t offset = 4 * (25 - fifo.count); // also hacky: we're sometimes short at the start. align to beginning of next second. uint8_t offset = 4 * (25 - fifo.count); // also hacky: we're sometimes short at the start. align to beginning of next second.

1003
movement.c

File diff suppressed because it is too large Load Diff

View File

@ -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
@ -120,20 +121,43 @@ typedef enum {
EVENT_LIGHT_BUTTON_UP, // The light button was pressed for less than half a second, and released. EVENT_LIGHT_BUTTON_UP, // The light button was pressed for less than half a second, and released.
EVENT_LIGHT_LONG_PRESS, // The light button was held for over half a second, but not yet released. EVENT_LIGHT_LONG_PRESS, // The light button was held for over half a second, but not yet released.
EVENT_LIGHT_LONG_UP, // The light button was held for over half a second, and released. EVENT_LIGHT_LONG_UP, // The light button was held for over half a second, and released.
EVENT_LIGHT_REALLY_LONG_PRESS, // The light button was held for more than 1.5 second, note yet released.
// EVENT_LIGHT_REALLY_LONG_UP, // The light button was held for more than 1.5 second, and released.
EVENT_MODE_BUTTON_DOWN, // The mode button has been pressed, but not yet released. EVENT_MODE_BUTTON_DOWN, // The mode button has been pressed, but not yet released.
EVENT_MODE_BUTTON_UP, // The mode button was pressed for less than half a second, and released. EVENT_MODE_BUTTON_UP, // The mode button was pressed for less than half a second, and released.
EVENT_MODE_LONG_PRESS, // The mode button was held for over half a second, but not yet released. EVENT_MODE_LONG_PRESS, // The mode button was held for over half a second, but not yet released.
EVENT_MODE_LONG_UP, // The mode button was held for over half a second, and released. NOTE: your watch face will resign immediately after receiving this event. EVENT_MODE_LONG_UP, // The mode button was held for over half a second, and released. NOTE: your watch face will resign immediately after receiving this event.
EVENT_MODE_REALLY_LONG_PRESS, // The mode button was held for more than 1.5 second, note yet released.
// EVENT_MODE_REALLY_LONG_UP, // The mode button was held for more than 1.5 second, and released.
EVENT_ALARM_BUTTON_DOWN, // The alarm button has been pressed, but not yet released. EVENT_ALARM_BUTTON_DOWN, // The alarm button has been pressed, but not yet released.
EVENT_ALARM_BUTTON_UP, // The alarm button was pressed for less than half a second, and released. EVENT_ALARM_BUTTON_UP, // The alarm button was pressed for less than half a second, and released.
EVENT_ALARM_LONG_PRESS, // The alarm button was held for over half a second, but not yet released. EVENT_ALARM_LONG_PRESS, // The alarm button was held for over half a second, but not yet released.
EVENT_ALARM_LONG_UP, // The alarm button was held for over half a second, and released. EVENT_ALARM_LONG_UP, // The alarm button was held for over half a second, and released.
EVENT_ALARM_REALLY_LONG_PRESS, // The alarm button was held for more than 1.5 second, note yet released.
// EVENT_ALARM_REALLY_LONG_UP, // The alarm button was held for more than 1.5 second, and released.
EVENT_ACCELEROMETER_WAKE, // The accelerometer has detected motion and woken up. EVENT_ACCELEROMETER_WAKE, // The accelerometer has detected motion and woken up.
EVENT_SINGLE_TAP, // Accelerometer detected a single tap. This event is not yet implemented. EVENT_SINGLE_TAP, // Accelerometer detected a single tap. This event is not yet implemented.
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 enum {
BUZZER_PRIORITY_BUTTON = 0, // Buzzer priority for button beeps (lowest priority).
BUZZER_PRIORITY_SIGNAL, // Buzzer priority for hourly chime (medium priority).
BUZZER_PRIORITY_ALARM, // Buzzer priority for hourly chime (highest priority).
} movement_buzzer_priority_t;
typedef struct { typedef struct {
uint8_t event_type; uint8_t event_type;
uint8_t subsecond; uint8_t subsecond;
@ -249,37 +273,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;
@ -296,6 +299,10 @@ typedef struct {
lis2dw_data_rate_t accelerometer_background_rate; lis2dw_data_rate_t accelerometer_background_rate;
// threshold for considering the wearer is in motion // threshold for considering the wearer is in motion
uint8_t accelerometer_motion_threshold; uint8_t accelerometer_motion_threshold;
// signal and alarm volumes
watch_buzzer_volume_t signal_volume;
watch_buzzer_volume_t alarm_volume;
} movement_state_t; } movement_state_t;
void movement_move_to_face(uint8_t watch_face_index); void movement_move_to_face(uint8_t watch_face_index);
@ -324,9 +331,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, movement_buzzer_priority_t priority);
uint8_t movement_claim_backup_register(void); uint8_t movement_claim_backup_register(void);
@ -339,8 +348,11 @@ void movement_set_timezone_index(uint8_t value);
watch_date_time_t movement_get_utc_date_time(void); watch_date_time_t movement_get_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);
uint32_t movement_get_utc_timestamp(void);
void movement_set_utc_date_time(watch_date_time_t date_time);
void movement_set_local_date_time(watch_date_time_t date_time); void movement_set_local_date_time(watch_date_time_t date_time);
void movement_set_utc_timestamp(uint32_t timestamp);
bool movement_button_should_sound(void); bool movement_button_should_sound(void);
void movement_set_button_should_sound(bool value); void movement_set_button_should_sound(bool value);
@ -348,6 +360,12 @@ void movement_set_button_should_sound(bool value);
watch_buzzer_volume_t movement_button_volume(void); watch_buzzer_volume_t movement_button_volume(void);
void movement_set_button_volume(watch_buzzer_volume_t value); void movement_set_button_volume(watch_buzzer_volume_t value);
watch_buzzer_volume_t movement_signal_volume(void);
void movement_set_signal_volume(watch_buzzer_volume_t value);
watch_buzzer_volume_t movement_alarm_volume(void);
void movement_set_alarm_volume(watch_buzzer_volume_t value);
movement_clock_mode_t movement_clock_mode_24h(void); movement_clock_mode_t movement_clock_mode_24h(void);
void movement_set_clock_mode_24h(movement_clock_mode_t value); void movement_set_clock_mode_24h(movement_clock_mode_t value);

View File

@ -32,13 +32,13 @@ const watch_face_t watch_faces[] = {
world_clock_face, world_clock_face,
sunrise_sunset_face, sunrise_sunset_face,
moon_phase_face, moon_phase_face,
stopwatch_face, fast_stopwatch_face,
countdown_face, countdown_face,
alarm_face, alarm_face,
temperature_display_face, temperature_display_face,
voltage_face, voltage_face,
settings_face, settings_face,
set_time_face set_time_face,
}; };
#define MOVEMENT_NUM_FACES (sizeof(watch_faces) / sizeof(watch_face_t)) #define MOVEMENT_NUM_FACES (sizeof(watch_faces) / sizeof(watch_face_t))
@ -49,7 +49,7 @@ const watch_face_t watch_faces[] = {
* Some folks also like to use this to hide the preferences and time set faces from the normal rotation. * Some folks also like to use this to hide the preferences and time set faces from the normal rotation.
* If you don't want any faces to be excluded, set this to 0 and a long Mode press will have no effect. * If you don't want any faces to be excluded, set this to 0 and a long Mode press will have no effect.
*/ */
#define MOVEMENT_SECONDARY_FACE_INDEX (MOVEMENT_NUM_FACES - 4) #define MOVEMENT_SECONDARY_FACE_INDEX (MOVEMENT_NUM_FACES - 5)
/* Custom hourly chime tune. Check movement_custom_signal_tunes.h for options. */ /* Custom hourly chime tune. Check movement_custom_signal_tunes.h for options. */
#define SIGNAL_TUNE_DEFAULT #define SIGNAL_TUNE_DEFAULT
@ -68,6 +68,8 @@ const watch_face_t watch_faces[] = {
#define MOVEMENT_DEFAULT_BUTTON_SOUND true #define MOVEMENT_DEFAULT_BUTTON_SOUND true
#define MOVEMENT_DEFAULT_BUTTON_VOLUME WATCH_BUZZER_VOLUME_SOFT #define MOVEMENT_DEFAULT_BUTTON_VOLUME WATCH_BUZZER_VOLUME_SOFT
#define MOVEMENT_DEFAULT_SIGNAL_VOLUME WATCH_BUZZER_VOLUME_LOUD
#define MOVEMENT_DEFAULT_ALARM_VOLUME WATCH_BUZZER_VOLUME_LOUD
/* Set the timeout before switching back to the main watch face /* Set the timeout before switching back to the main watch face
* Valid values are: * Valid values are:
@ -100,4 +102,10 @@ const watch_face_t watch_faces[] = {
*/ */
#define MOVEMENT_DEFAULT_LED_DURATION 1 #define MOVEMENT_DEFAULT_LED_DURATION 1
/* Optionally debounce button presses (disable by default).
* A value of 4 is a good starting point if you have issues
* with multiple button presses firing.
*/
#define MOVEMENT_DEBOUNCE_TICKS 0
#endif // MOVEMENT_CONFIG_H_ #endif // MOVEMENT_CONFIG_H_

View File

@ -79,4 +79,5 @@
#include "lander_face.h" #include "lander_face.h"
#include "simon_face.h" #include "simon_face.h"
#include "ping_face.h" #include "ping_face.h"
#include "rtccount_face.h"
// New includes go above this line. // New includes go above this line.

View File

@ -22,6 +22,7 @@ SRCS += \
./watch-faces/demo/character_set_face.c \ ./watch-faces/demo/character_set_face.c \
./watch-faces/demo/light_sensor_face.c \ ./watch-faces/demo/light_sensor_face.c \
./watch-faces/demo/peek_memory_face.c \ ./watch-faces/demo/peek_memory_face.c \
./watch-faces/demo/rtccount_face.c \
./watch-faces/sensor/accelerometer_status_face.c \ ./watch-faces/sensor/accelerometer_status_face.c \
./watch-faces/sensor/temperature_display_face.c \ ./watch-faces/sensor/temperature_display_face.c \
./watch-faces/sensor/temperature_logging_face.c \ ./watch-faces/sensor/temperature_logging_face.c \

View File

@ -202,9 +202,16 @@ static void _alarm_update_alarm_enabled(alarm_state_t *state) {
static void _alarm_play_short_beep(uint8_t pitch_idx) { static void _alarm_play_short_beep(uint8_t pitch_idx) {
// play a short double beep // play a short double beep
watch_buzzer_play_note(_buzzer_notes[pitch_idx], 50); static int8_t beep_sequence[] = {
watch_buzzer_play_note(BUZZER_NOTE_REST, 50); 0, 4,
watch_buzzer_play_note(_buzzer_notes[pitch_idx], 70); BUZZER_NOTE_REST, 4,
0, 6,
0
};
beep_sequence[0] = _buzzer_notes[pitch_idx];
beep_sequence[4] = _buzzer_notes[pitch_idx];
movement_play_sequence(beep_sequence, 0);
} }
static void _alarm_indicate_beep(alarm_state_t *state) { static void _alarm_indicate_beep(alarm_state_t *state) {
@ -437,14 +444,7 @@ bool advanced_alarm_face_loop(movement_event_t event, void *context) {
// play alarm // play alarm
if (state->alarm[state->alarm_playing_idx].beeps == 0) { if (state->alarm[state->alarm_playing_idx].beeps == 0) {
// short beep // short beep
if (watch_is_buzzer_or_led_enabled()) { _alarm_play_short_beep(state->alarm[state->alarm_playing_idx].pitch);
_alarm_play_short_beep(state->alarm[state->alarm_playing_idx].pitch);
} else {
// enable, play beep and disable buzzer again
watch_enable_buzzer();
_alarm_play_short_beep(state->alarm[state->alarm_playing_idx].pitch);
watch_disable_buzzer();
}
} else { } else {
// regular alarm beeps // regular alarm beeps
movement_play_alarm_beeps((state->alarm[state->alarm_playing_idx].beeps == (ALARM_MAX_BEEP_ROUNDS - 1) ? 20 : state->alarm[state->alarm_playing_idx].beeps), movement_play_alarm_beeps((state->alarm[state->alarm_playing_idx].beeps == (ALARM_MAX_BEEP_ROUNDS - 1) ? 20 : state->alarm[state->alarm_playing_idx].beeps),

View File

@ -82,26 +82,36 @@ static inline int _days_in_month(int16_t month, int16_t year)
/* Play beep sound based on type */ /* Play beep sound based on type */
static inline void _beep(beep_type_t beep_type) static inline void _beep(beep_type_t beep_type)
{ {
static int8_t beep_sequence[] = {
0, 4,
0, 6,
0, 6,
0
};
if (!movement_button_should_sound()) if (!movement_button_should_sound())
return; return;
switch (beep_type) { switch (beep_type) {
case BEEP_BUTTON: case BEEP_BUTTON:
watch_buzzer_play_note_with_volume(BUZZER_NOTE_C7, 50, movement_button_volume()); beep_sequence[0] = BUZZER_NOTE_C7;
beep_sequence[2] = 0;
break; break;
case BEEP_ENABLE: case BEEP_ENABLE:
watch_buzzer_play_note_with_volume(BUZZER_NOTE_G7, 50, movement_button_volume()); beep_sequence[0] = BUZZER_NOTE_G7;
watch_buzzer_play_note(BUZZER_NOTE_REST, 75); beep_sequence[2] = BUZZER_NOTE_REST;
watch_buzzer_play_note_with_volume(BUZZER_NOTE_C8, 50, movement_button_volume()); beep_sequence[4] = BUZZER_NOTE_C8;
break; break;
case BEEP_DISABLE: case BEEP_DISABLE:
watch_buzzer_play_note_with_volume(BUZZER_NOTE_C8, 50, movement_button_volume()); beep_sequence[0] = BUZZER_NOTE_C8;
watch_buzzer_play_note(BUZZER_NOTE_REST, 75); beep_sequence[2] = BUZZER_NOTE_REST;
watch_buzzer_play_note_with_volume(BUZZER_NOTE_G7, 50, movement_button_volume()); beep_sequence[4] = BUZZER_NOTE_G7;
break; break;
} }
movement_play_sequence(beep_sequence, 0);
} }
/* Change tick frequency */ /* Change tick frequency */

View File

@ -2,6 +2,7 @@
* MIT License * MIT License
* *
* Copyright (c) 2022 Andreas Nebinger * Copyright (c) 2022 Andreas Nebinger
* 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
@ -24,11 +25,13 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <limits.h>
#include "fast_stopwatch_face.h" #include "fast_stopwatch_face.h"
#include "watch.h" #include "watch.h"
#include "watch_common_display.h" #include "watch_common_display.h"
#include "watch_utility.h" #include "watch_utility.h"
#include "watch_rtc.h" #include "watch_rtc.h"
#include "slcd.h"
/* /*
This watch face implements the original F-91W stopwatch functionality This watch face implements the original F-91W stopwatch functionality
@ -40,173 +43,247 @@
turns on on each button press or it doesn't. turns on on each button press or it doesn't.
*/ */
#if __EMSCRIPTEN__ // Loosely implement the watch as a state machine
#include <emscripten.h> typedef enum {
#include <emscripten/html5.h> SW_STATUS_IDLE = 0,
#else SW_STATUS_RUNNING,
#include "tc.h" SW_STATUS_RUNNING_LAPPING,
#endif SW_STATUS_STOPPED,
SW_STATUS_STOPPED_LAPPING
// distant future for background task: January 1, 2083 } stopwatch_status_t;
static const watch_date_time_t distant_future = {
.unit = {0, 0, 0, 1, 1, 63}
};
static uint32_t _ticks;
static uint32_t _lap_ticks;
static uint8_t _blink_ticks;
static uint32_t _old_seconds;
static uint8_t _old_minutes;
static uint8_t _hours;
static bool _colon;
static bool _is_running;
#if __EMSCRIPTEN__
static long _em_interval_id = 0;
void em_cb_handler(void *userData) {
// interrupt handler for emscripten 128 Hz callbacks
(void) userData;
_ticks++;
}
static void _cb_initialize() { }
static inline void _cb_stop() {
emscripten_clear_interval(_em_interval_id);
_em_interval_id = 0;
_is_running = false;
}
static inline void _cb_start() {
// initiate 128 hz callback
_em_interval_id = emscripten_set_interval(em_cb_handler, (double)(1000/128), (void *)NULL);
}
#else
static inline void _cb_start() {
// start the TC1 timer
tc_enable(1);
_is_running = true;
}
static inline void _cb_stop() {
// stop the TC1 timer
tc_disable(1);
_is_running = false;
}
static void _cb_initialize() {
tc_init(1, GENERIC_CLOCK_3, TC_PRESCALER_DIV4);
tc_set_counter_mode(1, TC_COUNTER_MODE_8BIT);
tc_set_run_in_standby(1, true);
_cb_stop();
tc_count8_set_period(1, 1); // 1024 Hz divided by 4 divided by 2 results in a 128 Hz interrupt
/// FIXME: #SecondMovement, we need a gossamer wrapper for interrupts.
TC1->COUNT8.INTENSET.bit.OVF = 1;
NVIC_ClearPendingIRQ(TC1_IRQn);
NVIC_EnableIRQ (TC1_IRQn);
}
void irq_handler_tc1(void);
void irq_handler_tc1(void) {
// interrupt handler for TC1 (globally!)
_ticks++;
TC1->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF;
}
#endif
static inline void _button_beep() { static inline void _button_beep() {
// play a beep as confirmation for a button press (if applicable) // play a beep as confirmation for a button press (if applicable)
if (movement_button_should_sound()) watch_buzzer_play_note_with_volume(BUZZER_NOTE_C7, 50, movement_button_volume()); if (movement_button_should_sound()) watch_buzzer_play_note_with_volume(BUZZER_NOTE_C7, 50, movement_button_volume());
} }
// How quickly should the elapsing time be displayed?
// This is just for looks, timekeeping is always accurate to 128Hz
static const uint8_t DISPLAY_RUNNING_RATE = 32;
static const uint8_t DISPLAY_RUNNING_RATE_SLOW = 2;
/// @brief Display minutes, seconds and fractions derived from 128 Hz tick counter /// @brief Display minutes, seconds and fractions derived from 128 Hz tick counter
/// on the lcd. /// on the lcd.
/// @param ticks /// @param ticks
static void _display_ticks(uint32_t ticks) { static void _display_elapsed(fast_stopwatch_state_t *state, uint32_t ticks) {
char buf[14]; char buf[3];
uint8_t sec_100 = (ticks & 0x7F) * 100 / 128;
if (state->slow_refresh && (state->status == SW_STATUS_RUNNING || state->status == SW_STATUS_IDLE)) {
watch_display_character_lp_seconds(' ', 8);
watch_display_character_lp_seconds(' ', 9);
} else {
uint8_t sec_100 = (ticks & 0x7F) * 100 / 128;
watch_display_character_lp_seconds('0' + sec_100 / 10, 8);
watch_display_character_lp_seconds('0' + sec_100 % 10, 9);
}
uint32_t seconds = ticks >> 7; uint32_t seconds = ticks >> 7;
if (seconds == state->old_display.seconds) {
return;
}
state->old_display.seconds = seconds;
sprintf(buf, "%02lu", seconds % 60);
watch_display_text(WATCH_POSITION_MINUTES, buf);
uint32_t minutes = seconds / 60; uint32_t minutes = seconds / 60;
if (_hours) {
sprintf(buf, "%2u", _hours); if (minutes == state->old_display.minutes) {
return;
}
state->old_display.minutes = minutes;
sprintf(buf, "%02lu", minutes % 60);
watch_display_text(WATCH_POSITION_HOURS, buf);
uint32_t hours = (minutes / 60) % 24;
if (hours == state->old_display.hours) {
return;
}
state->old_display.hours = hours;
if (hours) {
sprintf(buf, "%2lu", hours);
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
} else { } else {
watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
} }
sprintf(buf, "%02lu%02lu%02u", minutes, (seconds % 60), sec_100);
watch_display_text(WATCH_POSITION_BOTTOM, buf);
} }
/// @brief Displays the current stopwatch time on the LCD (more optimized than _display_ticks()) static void _draw_indicators(fast_stopwatch_state_t *state, movement_event_t event, uint32_t elapsed) {
static void _draw() { uint8_t subsecond;
if (_lap_ticks == 0) { bool tock;
char buf[14];
uint8_t sec_100 = (_ticks & 0x7F) * 100 / 128; switch (state->status) {
if (_is_running) { case SW_STATUS_RUNNING:
uint32_t seconds = _ticks >> 7; subsecond = elapsed & 127;
if (seconds != _old_seconds) { tock = subsecond >= 64;
// seconds have changed
_old_seconds = seconds; watch_clear_indicator(WATCH_INDICATOR_LAP);
uint8_t minutes = seconds / 60; if (tock) {
seconds %= 60; watch_clear_colon();
if (minutes != _old_minutes) {
// minutes have changed, draw everything
_old_minutes = minutes;
minutes %= 60;
if (_hours) {
// with hour indicator
sprintf(buf, "%2u", _hours);
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
} else {
// no hour indicator
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
}
sprintf(buf, "%02u%02lu%02u", minutes, seconds, sec_100);
watch_display_text(WATCH_POSITION_BOTTOM, buf);
} else {
// just draw seconds
sprintf(buf, "%02lu", seconds);
// note that we're drawing the seconds in the "minutes" position, since this
// watch face uses the "seconds" position for hundredths of seconds
watch_display_text(WATCH_POSITION_MINUTES, buf);
watch_display_character_lp_seconds('0' + sec_100 / 10, 8);
watch_display_character_lp_seconds('0' + sec_100 % 10, 9);
}
} else { } else {
// only draw 100ths of seconds watch_set_colon();
watch_display_character_lp_seconds('0' + sec_100 / 10, 8);
watch_display_character_lp_seconds('0' + sec_100 % 10, 9);
} }
} else {
_display_ticks(_ticks); return;
}
} case SW_STATUS_RUNNING_LAPPING:
if (_is_running) { tock = event.subsecond > 0;
// blink the colon every half second
uint8_t blink_ticks = ((_ticks >> 6) & 1); if (tock) {
if (blink_ticks != _blink_ticks) { watch_clear_indicator(WATCH_INDICATOR_LAP);
_blink_ticks = blink_ticks; watch_clear_colon();
_colon = !_colon; } else {
if (_colon) watch_set_colon(); watch_set_indicator(WATCH_INDICATOR_LAP);
else watch_clear_colon(); watch_set_colon();
} }
return;
case SW_STATUS_STOPPED_LAPPING:
watch_set_indicator(WATCH_INDICATOR_LAP);
watch_set_colon();
return;
case SW_STATUS_STOPPED:
case SW_STATUS_IDLE:
default:
watch_clear_indicator(WATCH_INDICATOR_LAP);
watch_set_colon();
return;
} }
} }
static inline void _update_lap_indicator() { static uint8_t get_refresh_rate(fast_stopwatch_state_t *state) {
if (_lap_ticks) watch_set_indicator(WATCH_INDICATOR_LAP); switch (state->status) {
else watch_clear_indicator(WATCH_INDICATOR_LAP); case SW_STATUS_RUNNING:
if (state->slow_refresh) {
return DISPLAY_RUNNING_RATE_SLOW;
} else {
return DISPLAY_RUNNING_RATE;
}
case SW_STATUS_RUNNING_LAPPING:
return 2;
case SW_STATUS_STOPPED:
case SW_STATUS_IDLE:
default:
return 1;
}
} }
static inline void _set_colon() { static void state_transition(fast_stopwatch_state_t *state, rtc_counter_t counter, movement_event_type_t event_type) {
watch_set_colon(); switch (state->status) {
_colon = true; case SW_STATUS_IDLE:
switch (event_type) {
case EVENT_ALARM_BUTTON_DOWN:
state->status = SW_STATUS_RUNNING;
state->start_counter = counter;
movement_request_tick_frequency(get_refresh_rate(state));
return;
case EVENT_LIGHT_LONG_PRESS:
state->slow_refresh = !state->slow_refresh;
return;
default:
return;
}
case SW_STATUS_RUNNING:
switch (event_type) {
case EVENT_ALARM_BUTTON_DOWN:
state->status = SW_STATUS_STOPPED;
state->stop_counter = counter;
movement_request_tick_frequency(get_refresh_rate(state));
return;
case EVENT_LIGHT_BUTTON_DOWN:
state->status = SW_STATUS_RUNNING_LAPPING;
state->lap_counter = counter;
movement_request_tick_frequency(get_refresh_rate(state));
return;
default:
return;
}
case SW_STATUS_RUNNING_LAPPING:
switch (event_type) {
case EVENT_ALARM_BUTTON_DOWN:
state->status = SW_STATUS_STOPPED_LAPPING;
state->stop_counter = counter;
movement_request_tick_frequency(get_refresh_rate(state));
return;
case EVENT_LIGHT_BUTTON_DOWN:
state->status = SW_STATUS_RUNNING;
state->lap_counter = counter;
movement_request_tick_frequency(get_refresh_rate(state));
return;
case EVENT_LIGHT_LONG_PRESS:
state->status = SW_STATUS_RUNNING;
state->slow_refresh = !state->slow_refresh;
movement_request_tick_frequency(get_refresh_rate(state));
return;
default:
return;
}
case SW_STATUS_STOPPED_LAPPING:
switch (event_type) {
case EVENT_ALARM_BUTTON_DOWN:
state->status = SW_STATUS_RUNNING_LAPPING;
state->start_counter = counter - state->stop_counter + state->start_counter;
state->lap_counter = counter - state->stop_counter + state->lap_counter;
movement_request_tick_frequency(get_refresh_rate(state));
return;
case EVENT_LIGHT_BUTTON_DOWN:
state->status = SW_STATUS_STOPPED;
return;
default:
return;
}
case SW_STATUS_STOPPED:
switch (event_type) {
case EVENT_ALARM_BUTTON_DOWN:
state->status = SW_STATUS_RUNNING;
state->start_counter = counter - state->stop_counter + state->start_counter;
movement_request_tick_frequency(get_refresh_rate(state));
return;
case EVENT_LIGHT_BUTTON_DOWN:
state->status = SW_STATUS_IDLE;
return;
default:
return;
}
default:
return;
}
}
static uint32_t elapsed_time(fast_stopwatch_state_t *state, rtc_counter_t counter) {
switch (state->status) {
case SW_STATUS_IDLE:
return 0;
case SW_STATUS_RUNNING:
return counter - state->start_counter;
case SW_STATUS_RUNNING_LAPPING:
case SW_STATUS_STOPPED_LAPPING:
return state->lap_counter - state->start_counter;
case SW_STATUS_STOPPED:
return state->stop_counter - state->start_counter;
default:
return 0;
}
} }
void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr) { void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr) {
@ -215,114 +292,54 @@ void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr) {
*context_ptr = malloc(sizeof(fast_stopwatch_state_t)); *context_ptr = malloc(sizeof(fast_stopwatch_state_t));
memset(*context_ptr, 0, sizeof(fast_stopwatch_state_t)); memset(*context_ptr, 0, sizeof(fast_stopwatch_state_t));
fast_stopwatch_state_t *state = (fast_stopwatch_state_t *)*context_ptr; fast_stopwatch_state_t *state = (fast_stopwatch_state_t *)*context_ptr;
_ticks = _lap_ticks = _blink_ticks = _old_minutes = _old_seconds = _hours = 0; state->start_counter = 0;
_is_running = _colon = false; state->stop_counter = 0;
state->light_on_button = true; state->lap_counter = 0;
} state->status = SW_STATUS_IDLE;
if (!_is_running) {
// prepare the 128 Hz callback source
_cb_initialize();
} }
} }
void fast_stopwatch_face_activate(void *context) { void fast_stopwatch_face_activate(void *context) {
(void) context; fast_stopwatch_state_t *state = (fast_stopwatch_state_t *) context;
if (_is_running) { // force full re-draw
// The background task will keep the watch from entering low energy mode while the stopwatch is on screen. state->old_display.seconds = UINT_MAX;
movement_schedule_background_task(distant_future); state->old_display.minutes = UINT_MAX;
} state->old_display.hours = UINT_MAX;
movement_request_tick_frequency(get_refresh_rate(state));
} }
bool fast_stopwatch_face_loop(movement_event_t event, void *context) { bool fast_stopwatch_face_loop(movement_event_t event, void *context) {
fast_stopwatch_state_t *state = (fast_stopwatch_state_t *)context; fast_stopwatch_state_t *state = (fast_stopwatch_state_t *)context;
// handle overflow of fast ticks rtc_counter_t counter = watch_rtc_get_counter();
while (_ticks >= (128 * 60 * 60)) {
_ticks -= (128 * 60 * 60); state_transition(state, counter, event.event_type);
_hours++; rtc_counter_t elapsed = elapsed_time(state, counter);
if (_hours >= 24) _hours -= 24;
// initiate a re-draw
_old_minutes = 59;
}
switch (event.event_type) { switch (event.event_type) {
case EVENT_ACTIVATE: case EVENT_ACTIVATE:
_set_colon();
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "STW", "ST"); watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "STW", "ST");
_update_lap_indicator(); _draw_indicators(state, event, elapsed);
if (_is_running) movement_request_tick_frequency(16); _display_elapsed(state, elapsed);
_display_ticks(_lap_ticks ? _lap_ticks : _ticks);
break;
case EVENT_TICK:
_draw();
break;
case EVENT_LIGHT_LONG_PRESS:
// kind od hidden feature: long press toggles light on or off
state->light_on_button = !state->light_on_button;
if (state->light_on_button) movement_illuminate_led();
else watch_set_led_off();
break; break;
case EVENT_ALARM_BUTTON_DOWN: case EVENT_ALARM_BUTTON_DOWN:
_is_running = !_is_running;
if (_is_running) {
// start or continue stopwatch
movement_request_tick_frequency(16);
// register 128 hz callback for time measuring
_cb_start();
// schedule the keepalive task when running
movement_schedule_background_task(distant_future);
} else {
// stop the stopwatch
_cb_stop();
movement_request_tick_frequency(1);
_set_colon();
// cancel the keepalive task
movement_cancel_background_task();
}
_draw();
_button_beep();
break;
case EVENT_LIGHT_BUTTON_DOWN: case EVENT_LIGHT_BUTTON_DOWN:
if (state->light_on_button) movement_illuminate_led(); case EVENT_LIGHT_LONG_PRESS:
if (_is_running) { _button_beep();
if (_lap_ticks) { // fall through
// clear lap and continue running case EVENT_TICK:
_lap_ticks = 0; _draw_indicators(state, event, elapsed);
movement_request_tick_frequency(16); _display_elapsed(state, elapsed);
} else {
// set lap ticks and stop updating the display
_lap_ticks = _ticks;
movement_request_tick_frequency(2);
_set_colon();
}
} else {
if (_lap_ticks) {
// clear lap and show running stopwatch
_lap_ticks = 0;
} else if (_ticks) {
// reset stopwatch
_ticks = _lap_ticks = _blink_ticks = _old_minutes = _old_seconds = _hours = 0;
_button_beep();
}
}
_display_ticks(_ticks);
_update_lap_indicator();
break;
case EVENT_TIMEOUT:
if (!_is_running) movement_move_to_face(0);
break;
case EVENT_LOW_ENERGY_UPDATE:
_draw();
break; break;
default: default:
movement_default_loop_handler(event); movement_default_loop_handler(event);
break; break;
} }
return true; return true;
} }
void fast_stopwatch_face_resign(void *context) { void fast_stopwatch_face_resign(void *context) {
(void) context; (void) context;
// cancel the keepalive task movement_request_tick_frequency(1);
movement_cancel_background_task();
} }

View File

@ -55,7 +55,16 @@
#include "movement.h" #include "movement.h"
typedef struct { typedef struct {
bool light_on_button; // determines whether the light button actually triggers the led rtc_counter_t start_counter; // rtc counter when the stopwatch was started
rtc_counter_t lap_counter; // rtc counter when the stopwatch was lapped
rtc_counter_t stop_counter; // rtc counter when the stopwatch was stopped
uint8_t status; // the status the stopwatch is in (idle, running, stopped)
bool slow_refresh; // update the display slowly (same 128Hz timekeeping accuracy)
struct {
rtc_counter_t seconds;
rtc_counter_t minutes;
rtc_counter_t hours;
} old_display; // the digits currently being displayed on screen
} fast_stopwatch_state_t; } fast_stopwatch_state_t;
void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr); void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr);
@ -63,12 +72,6 @@ void fast_stopwatch_face_activate(void *context);
bool fast_stopwatch_face_loop(movement_event_t event, void *context); bool fast_stopwatch_face_loop(movement_event_t event, void *context);
void fast_stopwatch_face_resign(void *context); void fast_stopwatch_face_resign(void *context);
#if __EMSCRIPTEN__
void em_cb_handler(void *userData);
#else
void TC2_Handler(void);
#endif
#define fast_stopwatch_face ((const watch_face_t){ \ #define fast_stopwatch_face ((const watch_face_t){ \
fast_stopwatch_face_setup, \ fast_stopwatch_face_setup, \
fast_stopwatch_face_activate, \ fast_stopwatch_face_activate, \

View File

@ -96,8 +96,7 @@ static inline void _inc_uint8(uint8_t *value, uint8_t step, uint8_t max) {
static uint32_t _get_now_ts() { static uint32_t _get_now_ts() {
// returns the current date time as unix timestamp // returns the current date time as unix timestamp
watch_date_time_t now = watch_rtc_get_date_time(); return movement_get_utc_timestamp();
return watch_utility_date_time_to_unix_time(now, 0);
} }
static inline void _button_beep() { static inline void _button_beep() {

View File

@ -119,30 +119,22 @@ static void _simon_play_note(SimonNote note, simon_state_t *state, bool skip_res
switch (note) { switch (note) {
case SIMON_LED_NOTE: case SIMON_LED_NOTE:
if (!state->lightOff) watch_set_led_yellow(); if (!state->lightOff) watch_set_led_yellow();
if (state->soundOff) if (!state->soundOff) watch_buzzer_play_note(BUZZER_NOTE_D3, _delay_beep);
delay_ms(_delay_beep); delay_ms(_delay_beep);
else
watch_buzzer_play_note(BUZZER_NOTE_D3, _delay_beep);
break; break;
case SIMON_MODE_NOTE: case SIMON_MODE_NOTE:
if (!state->lightOff) watch_set_led_red(); if (!state->lightOff) watch_set_led_red();
if (state->soundOff) if (!state->soundOff) watch_buzzer_play_note(BUZZER_NOTE_E4, _delay_beep);
delay_ms(_delay_beep); delay_ms(_delay_beep);
else
watch_buzzer_play_note(BUZZER_NOTE_E4, _delay_beep);
break; break;
case SIMON_ALARM_NOTE: case SIMON_ALARM_NOTE:
if (!state->lightOff) watch_set_led_green(); if (!state->lightOff) watch_set_led_green();
if (state->soundOff) if (!state->soundOff) watch_buzzer_play_note(BUZZER_NOTE_C3, _delay_beep);
delay_ms(_delay_beep); delay_ms(_delay_beep);
else
watch_buzzer_play_note(BUZZER_NOTE_C3, _delay_beep);
break; break;
case SIMON_WRONG_NOTE: case SIMON_WRONG_NOTE:
if (state->soundOff) if (!state->soundOff) watch_buzzer_play_note(BUZZER_NOTE_A1, 800);
delay_ms(800); delay_ms(800);
else
watch_buzzer_play_note(BUZZER_NOTE_A1, 800);
break; break;
} }
watch_set_led_off(); watch_set_led_off();
@ -150,7 +142,7 @@ static void _simon_play_note(SimonNote note, simon_state_t *state, bool skip_res
if (note != SIMON_WRONG_NOTE) { if (note != SIMON_WRONG_NOTE) {
_simon_clear_display(state); _simon_clear_display(state);
if (!skip_rest) { if (!skip_rest) {
watch_buzzer_play_note(BUZZER_NOTE_REST, (_delay_beep * 2)/3); delay_ms((_delay_beep * 2)/3);
} }
} }
} }

View File

@ -110,6 +110,12 @@ static bool tally_face_should_move_back(tally_state_t *state) {
bool tally_face_loop(movement_event_t event, void *context) { bool tally_face_loop(movement_event_t event, void *context) {
tally_state_t *state = (tally_state_t *)context; tally_state_t *state = (tally_state_t *)context;
static bool using_led = false; static bool using_led = false;
static int8_t beep_sequence[] = {
0, 2,
BUZZER_NOTE_REST, 3,
0, 2,
0
};
if (using_led) { if (using_led) {
if(!HAL_GPIO_BTN_MODE_read() && !HAL_GPIO_BTN_LIGHT_read() && !HAL_GPIO_BTN_ALARM_read()) if(!HAL_GPIO_BTN_MODE_read() && !HAL_GPIO_BTN_LIGHT_read() && !HAL_GPIO_BTN_ALARM_read())
@ -148,9 +154,11 @@ bool tally_face_loop(movement_event_t event, void *context) {
state->tally_idx = _tally_default[state->tally_default_idx]; // reset tally index state->tally_idx = _tally_default[state->tally_default_idx]; // reset tally index
_init_val = true; _init_val = true;
//play a reset tune //play a reset tune
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_G6, 30); if (movement_button_should_sound()) {
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_REST, 30); beep_sequence[0] = BUZZER_NOTE_G6;
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_E6, 30); beep_sequence[4] = BUZZER_NOTE_E6;
movement_play_sequence(beep_sequence, 0);
}
print_tally(state, movement_button_should_sound()); print_tally(state, movement_button_should_sound());
} }
break; break;
@ -168,9 +176,11 @@ bool tally_face_loop(movement_event_t event, void *context) {
if (TALLY_FACE_PRESETS_SIZE() > 1 && _init_val){ if (TALLY_FACE_PRESETS_SIZE() > 1 && _init_val){
state->tally_default_idx = (state->tally_default_idx + 1) % TALLY_FACE_PRESETS_SIZE(); state->tally_default_idx = (state->tally_default_idx + 1) % TALLY_FACE_PRESETS_SIZE();
state->tally_idx = _tally_default[state->tally_default_idx]; state->tally_idx = _tally_default[state->tally_default_idx];
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_E6, 30); if (movement_button_should_sound()) {
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_REST, 30); beep_sequence[0] = BUZZER_NOTE_E6;
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_G6, 30); beep_sequence[4] = BUZZER_NOTE_G6;
movement_play_sequence(beep_sequence, 0);
}
print_tally(state, movement_button_should_sound()); print_tally(state, movement_button_should_sound());
} }
else{ else{

View File

@ -36,7 +36,6 @@
#include <string.h> #include <string.h>
#include "totp_face.h" #include "totp_face.h"
#include "watch.h" #include "watch.h"
#include "watch_utility.h"
#include "TOTP.h" #include "TOTP.h"
#include "base32.h" #include "base32.h"
@ -159,7 +158,7 @@ static void totp_generate_and_display(totp_state_t *totp_state) {
} }
static inline uint32_t totp_compute_base_timestamp() { static inline uint32_t totp_compute_base_timestamp() {
return watch_utility_date_time_to_unix_time(movement_get_utc_date_time(), 0); return movement_get_utc_timestamp();
} }
void totp_face_setup(uint8_t watch_face_index, void ** context_ptr) { void totp_face_setup(uint8_t watch_face_index, void ** context_ptr) {

View File

@ -30,7 +30,6 @@
#include "base32.h" #include "base32.h"
#include "watch.h" #include "watch.h"
#include "watch_utility.h"
#include "filesystem.h" #include "filesystem.h"
#include "totp_lfs_face.h" #include "totp_lfs_face.h"
@ -253,7 +252,7 @@ void totp_lfs_face_activate(void *context) {
} }
#endif #endif
totp_state->timestamp = watch_utility_date_time_to_unix_time(movement_get_utc_date_time(), 0); totp_state->timestamp = movement_get_utc_timestamp();
totp_face_set_record(totp_state, 0); totp_face_set_record(totp_state, 0);
} }

View File

@ -0,0 +1,204 @@
/*
* MIT License
*
* 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 <stdlib.h>
#include <string.h>
#include "rtccount_face.h"
#include "watch.h"
#include "sam.h"
#include "watch_utility.h"
#include "watch_common_display.h"
#include "watch_rtc.h"
typedef enum {
RTCCOUNT_STATUS_COUNTER = 0,
RTCCOUNT_STATUS_COUNTER_SUB,
RTCCOUNT_STATUS_MINUTES,
RTCCOUNT_STATUS_MINUTES_DIFF,
RTCCOUNT_STATUS_NUMBER
} rtccount_face_status_t;
typedef struct {
rtccount_face_status_t status;
uint8_t frequency;
uint32_t n_top_of_minute;
uint32_t ref_timestamp;
} rtccount_state_t;
static const uint32_t COUNTER_MASK = (1 << 19) - 1;
static void _rtccount_face_display_string(char* string, uint8_t pos) {
// watch_display_string is deprecated, but there is no alternative for this use-case
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
watch_display_string(string, pos);
#pragma GCC diagnostic pop
}
static void _rtccount_face_draw(movement_event_t event, rtccount_state_t* state) {
uint32_t counter = watch_rtc_get_counter();
char buf[11] = " 000000\0";
switch (state->status) {
case RTCCOUNT_STATUS_COUNTER: {
buf[0] = 'C';
break;
}
case RTCCOUNT_STATUS_COUNTER_SUB: {
buf[0] = 'S';
break;
}
case RTCCOUNT_STATUS_MINUTES: {
buf[0] = 'M';
break;
}
case RTCCOUNT_STATUS_MINUTES_DIFF: {
buf[0] = 'D';
break;
}
default:
break;
}
_rtccount_face_display_string(buf, 0);
snprintf(buf, sizeof(buf), "%u", event.subsecond);
uint32_t len = strlen(buf);
_rtccount_face_display_string(buf, 4 - len);
switch (state->status) {
case RTCCOUNT_STATUS_COUNTER: {
snprintf(buf, sizeof(buf), "%lu", counter & COUNTER_MASK);
size_t len = strlen(buf);
_rtccount_face_display_string(buf, 10 - len);
break;
}
case RTCCOUNT_STATUS_COUNTER_SUB: {
snprintf(buf, sizeof(buf), "%lu", counter & 127);
size_t len = strlen(buf);
_rtccount_face_display_string(buf, 10 - len);
break;
}
case RTCCOUNT_STATUS_MINUTES: {
snprintf(buf, sizeof(buf), "%lu", state->n_top_of_minute & COUNTER_MASK);
size_t len = strlen(buf);
_rtccount_face_display_string(buf, 10 - len);
break;
}
case RTCCOUNT_STATUS_MINUTES_DIFF: {
uint32_t elapsed_minutes = (movement_get_utc_timestamp() - state->ref_timestamp) / 60;
snprintf(buf, sizeof(buf), "%lu", (elapsed_minutes - state->n_top_of_minute) & COUNTER_MASK);
size_t len = strlen(buf);
_rtccount_face_display_string(buf, 10 - len);
break;
}
default:
break;
}
}
void rtccount_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(rtccount_state_t));
memset(*context_ptr, 0, sizeof(rtccount_state_t));
rtccount_state_t *state = (rtccount_state_t *) *context_ptr;
state->status = RTCCOUNT_STATUS_COUNTER;
state->frequency = 1;
state->n_top_of_minute = 0;
rtc_date_time_t datetime = movement_get_utc_date_time();
state->ref_timestamp = movement_get_utc_timestamp() - datetime.unit.second;
}
}
void rtccount_face_activate(void *context) {
rtccount_state_t* state = (rtccount_state_t*)context;
movement_request_tick_frequency(state->frequency);
}
bool rtccount_face_loop(movement_event_t event, void *context) {
rtccount_state_t* state = (rtccount_state_t*)context;
switch (event.event_type) {
case EVENT_BACKGROUND_TASK:
state->n_top_of_minute += 1;
break;
case EVENT_ALARM_BUTTON_UP:
if (state->frequency == 128) {
state->frequency = 1;
} else {
state->frequency *= 2;
}
movement_request_tick_frequency(state->frequency);
break;
case EVENT_ALARM_LONG_PRESS:
state->n_top_of_minute = 0;
rtc_date_time_t datetime = movement_get_utc_date_time();
state->ref_timestamp = movement_get_utc_timestamp() - datetime.unit.second;
break;
case EVENT_LIGHT_BUTTON_DOWN:
state->status = (state->status + 1) % RTCCOUNT_STATUS_NUMBER;
_rtccount_face_draw(event, state);
break;
case EVENT_ACTIVATE:
case EVENT_TICK:
_rtccount_face_draw(event, state);
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
void rtccount_face_resign(void *context) {
(void) context;
movement_request_tick_frequency(1);
}
movement_watch_face_advisory_t rtccount_face_advise(void *context) {
(void) context;
movement_watch_face_advisory_t retval = { 0 };
retval.wants_background_task = true;
return retval;
}

View File

@ -0,0 +1,47 @@
/*
* MIT License
*
* 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
/*
* RTCCOUNT FACE
*
* A test face to inspect some metrics of the rtc-counter32 mode.
*/
#include "movement.h"
void rtccount_face_setup(uint8_t watch_face_index, void ** context_ptr);
void rtccount_face_activate(void *context);
bool rtccount_face_loop(movement_event_t event, void *context);
void rtccount_face_resign(void *context);
movement_watch_face_advisory_t rtccount_face_advise(void *context);
#define rtccount_face ((const watch_face_t){ \
rtccount_face_setup, \
rtccount_face_activate, \
rtccount_face_loop, \
rtccount_face_resign, \
rtccount_face_advise, \
})

View File

@ -47,9 +47,6 @@ typedef struct {
// Selected program // Selected program
chirpy_demo_program_t program; chirpy_demo_program_t program;
// Helps us handle 1/64 ticks during transmission; including countdown timer
chirpy_tick_state_t tick_state;
// Used by chirpy encoder during transmission // Used by chirpy encoder during transmission
chirpy_encoder_state_t encoder_state; chirpy_encoder_state_t encoder_state;
@ -150,46 +147,10 @@ static void _cdf_update_lcd(chirpy_demo_state_t *state) {
} }
} }
static void _cdf_quit_chirping(chirpy_demo_state_t *state) {
state->mode = CDM_CHOOSE;
watch_set_buzzer_off();
watch_clear_indicator(WATCH_INDICATOR_BELL);
movement_request_tick_frequency(1);
}
static void _cdf_scale_tick(void *context) {
chirpy_demo_state_t *state = (chirpy_demo_state_t *)context;
chirpy_tick_state_t *tick_state = &state->tick_state;
// Scale goes in 200Hz increments from 700 Hz to 12.3 kHz -> 58 steps
if (tick_state->seq_pos == 58) {
_cdf_quit_chirping(state);
return;
}
uint32_t freq = 700 + tick_state->seq_pos * 200;
uint32_t period = 1000000 / freq;
watch_set_buzzer_period_and_duty_cycle(period, 25);
watch_set_buzzer_on();
++tick_state->seq_pos;
}
static void _cdf_data_tick(void *context) {
chirpy_demo_state_t *state = (chirpy_demo_state_t *)context;
uint8_t tone = chirpy_get_next_tone(&state->encoder_state);
// Transmission over?
if (tone == 255) {
_cdf_quit_chirping(state);
return;
}
uint16_t period = chirpy_get_tone_period(tone);
watch_set_buzzer_period_and_duty_cycle(period, 25);
watch_set_buzzer_on();
}
static uint8_t *curr_data_ptr; static uint8_t *curr_data_ptr;
static uint16_t curr_data_ix; static uint16_t curr_data_ix;
static uint16_t curr_data_len; static uint16_t curr_data_len;
static chirpy_demo_state_t *curr_state;
static uint8_t _cdf_get_next_byte(uint8_t *next_byte) { static uint8_t _cdf_get_next_byte(uint8_t *next_byte) {
if (curr_data_ix == curr_data_len) if (curr_data_ix == curr_data_len)
@ -199,59 +160,60 @@ static uint8_t _cdf_get_next_byte(uint8_t *next_byte) {
return 1; return 1;
} }
static void _cdf_countdown_tick(void *context) { static void _cdf_on_chirping_done(void) {
chirpy_demo_state_t *state = (chirpy_demo_state_t *)context; if (curr_state) {
chirpy_tick_state_t *tick_state = &state->tick_state; curr_state->mode = CDM_CHOOSE;
// Countdown over: start actual broadcast
if (tick_state->seq_pos == 8 * 3) {
tick_state->tick_compare = 3;
tick_state->tick_count = -1;
tick_state->seq_pos = 0;
// We'll be chirping out a scale
if (false) { // state->program == CDP_CLEAR) {
tick_state->tick_fun = _cdf_scale_tick;
}
// We'll be chirping out data
else {
// Set up the encoder
chirpy_init_encoder(&state->encoder_state, _cdf_get_next_byte);
tick_state->tick_fun = _cdf_data_tick;
// Set up the data
curr_data_ix = 0;
if (state->program == CDP_INFO_SHORT) {
curr_data_ptr = short_data;
curr_data_len = short_data_len;
} else if (state->program == CDP_INFO_LONG) {
curr_data_ptr = long_data_str;
curr_data_len = strlen((const char *)long_data_str);
} else if (state->program == CDP_INFO_NANOSEC) {
curr_data_ptr = activity_buffer;
curr_data_len = activity_buffer_size;
}
}
return;
} }
// Sound or turn off buzzer watch_clear_indicator(WATCH_INDICATOR_BELL);
if ((tick_state->seq_pos % 8) == 0) {
watch_set_buzzer_period_and_duty_cycle(NotePeriods[BUZZER_NOTE_A5], 25);
watch_set_buzzer_on();
} else if ((tick_state->seq_pos % 8) == 1) {
watch_set_buzzer_off();
}
++tick_state->seq_pos;
} }
static void _cdm_setup_chirp(chirpy_demo_state_t *state) { static bool _cdm_raw_source_fn(uint16_t position, void* userdata, uint16_t* period, uint16_t* duration) {
// We want frequent callbacks from now on // Beep countdown
movement_request_tick_frequency(64); if (position < 6) {
if (position % 2) {
*period = WATCH_BUZZER_PERIOD_REST;
*duration = 56;
} else {
*period = NotePeriods[BUZZER_NOTE_A5];
*duration = 8;
}
return false;
}
chirpy_demo_state_t *state = (chirpy_demo_state_t *)userdata;
uint8_t tone = chirpy_get_next_tone(&state->encoder_state);
// Transmission over?
if (tone == 255) {
return true;
}
*period = chirpy_get_tone_period(tone);
*duration = 3;
return false;
}
static void _cdm_start_transmission(chirpy_demo_state_t *state) {
watch_set_indicator(WATCH_INDICATOR_BELL); watch_set_indicator(WATCH_INDICATOR_BELL);
state->mode = CDM_CHIRPING; state->mode = CDM_CHIRPING;
// Set up tick state; start with countdown
state->tick_state.tick_count = -1; // Set up the data
state->tick_state.tick_compare = 8; curr_state = state;
state->tick_state.seq_pos = 0; curr_data_ix = 0;
state->tick_state.tick_fun = _cdf_countdown_tick; if (state->program == CDP_INFO_SHORT) {
curr_data_ptr = short_data;
curr_data_len = short_data_len;
} else if (state->program == CDP_INFO_LONG) {
curr_data_ptr = long_data_str;
curr_data_len = strlen((const char *)long_data_str);
} else if (state->program == CDP_INFO_NANOSEC) {
curr_data_ptr = activity_buffer;
curr_data_len = activity_buffer_size;
}
chirpy_init_encoder(&state->encoder_state, _cdf_get_next_byte);
watch_buzzer_play_raw_source(_cdm_raw_source_fn, state, _cdf_on_chirping_done);
} }
bool chirpy_demo_face_loop(movement_event_t event, void *context) { bool chirpy_demo_face_loop(movement_event_t event, void *context) {
@ -261,12 +223,7 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) {
case EVENT_ACTIVATE: case EVENT_ACTIVATE:
_cdf_update_lcd(state); _cdf_update_lcd(state);
break; break;
case EVENT_MODE_BUTTON_UP: case EVENT_LIGHT_BUTTON_DOWN:
// Do not exit face while we're chirping
if (state->mode != CDM_CHIRPING) {
movement_move_to_next_face();
}
break;
case EVENT_LIGHT_BUTTON_UP: case EVENT_LIGHT_BUTTON_UP:
// We don't do light. // We don't do light.
break; break;
@ -286,10 +243,6 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) {
state->program = CDP_CLEAR; state->program = CDP_CLEAR;
_cdf_update_lcd(state); _cdf_update_lcd(state);
} }
// If chirping: stoppit
else if (state->mode == CDM_CHIRPING) {
_cdf_quit_chirping(state);
}
break; break;
case EVENT_ALARM_LONG_PRESS: case EVENT_ALARM_LONG_PRESS:
// If in choose mode: start chirping // If in choose mode: start chirping
@ -299,16 +252,7 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) {
movement_force_led_off(); movement_force_led_off();
movement_move_to_next_face(); movement_move_to_next_face();
} else { } else {
_cdm_setup_chirp(state); _cdm_start_transmission(state);
}
}
break;
case EVENT_TICK:
if (state->mode == CDM_CHIRPING) {
++state->tick_state.tick_count;
if (state->tick_state.tick_count == state->tick_state.tick_compare) {
state->tick_state.tick_count = 0;
state->tick_state.tick_fun(context);
} }
} }
break; break;
@ -317,15 +261,13 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) {
if (state->mode != CDM_CHIRPING) { if (state->mode != CDM_CHIRPING) {
movement_move_to_face(0); movement_move_to_face(0);
} }
// fall through
default: default:
movement_default_loop_handler(event);
break; break;
} }
// Return true if the watch can enter standby mode. False needed when chirping. return true;
if (state->mode == CDM_CHIRPING)
return false;
else
return true;
} }
void chirpy_demo_face_resign(void *context) { void chirpy_demo_face_resign(void *context) {

View File

@ -420,7 +420,7 @@ static void _monitor_update(lis2dw_monitor_state_t *state)
lis2dw_fifo_t fifo; lis2dw_fifo_t fifo;
float x = 0, y = 0, z = 0; float x = 0, y = 0, z = 0;
lis2dw_read_fifo(&fifo); lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT / DISPLAY_FREQUENCY);
if (fifo.count == 0) { if (fifo.count == 0) {
return; return;
} }

View File

@ -27,7 +27,6 @@
#include <math.h> #include <math.h>
#include "finetune_face.h" #include "finetune_face.h"
#include "nanosec_face.h" #include "nanosec_face.h"
#include "watch_utility.h"
#include "delay.h" #include "delay.h"
extern nanosec_state_t nanosec_state; extern nanosec_state_t nanosec_state;
@ -51,7 +50,7 @@ void finetune_face_activate(void *context) {
} }
static float finetune_get_hours_passed(void) { static float finetune_get_hours_passed(void) {
uint32_t current_time = watch_utility_date_time_to_unix_time(watch_rtc_get_date_time(), 0); uint32_t current_time = movement_get_utc_timestamp();
return (current_time - nanosec_state.last_correction_time) / 3600.0f; return (current_time - nanosec_state.last_correction_time) / 3600.0f;
} }
@ -64,7 +63,7 @@ static void finetune_update_display(void) {
if (finetune_page == 0) { if (finetune_page == 0) {
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "FTU", "FT"); watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "FTU", "FT");
watch_date_time_t date_time = watch_rtc_get_date_time(); watch_date_time_t date_time = movement_get_utc_date_time();
sprintf(buf, "%04d%02d", abs(total_adjustment), date_time.unit.second); sprintf(buf, "%04d%02d", abs(total_adjustment), date_time.unit.second);
watch_display_text(WATCH_POSITION_BOTTOM, buf); watch_display_text(WATCH_POSITION_BOTTOM, buf);
@ -106,17 +105,9 @@ static void finetune_adjust_subseconds(int delta) {
watch_rtc_enable(false); watch_rtc_enable(false);
delay_ms(delta); delay_ms(delta);
if (delta > 500) { if (delta > 500) {
watch_date_time_t date_time = watch_rtc_get_date_time(); uint32_t timestamp = movement_get_utc_timestamp();
date_time.unit.second = (date_time.unit.second + 1) % 60; timestamp += 1;
if (date_time.unit.second == 0) { // Overflow movement_set_utc_timestamp(timestamp);
date_time.unit.minute = (date_time.unit.minute + 1) % 60;
if (date_time.unit.minute == 0) { // Overflow
date_time.unit.hour = (date_time.unit.hour + 1) % 24;
if (date_time.unit.hour == 0) // Overflow
date_time.unit.day++;
}
}
watch_rtc_set_date_time(date_time);
} }
watch_rtc_enable(true); watch_rtc_enable(true);
} }
@ -126,7 +117,7 @@ static void finetune_update_correction_time(void) {
nanosec_state.freq_correction += roundf(nanosec_get_aging() * 100); nanosec_state.freq_correction += roundf(nanosec_get_aging() * 100);
// Remember when we last corrected time // Remember when we last corrected time
nanosec_state.last_correction_time = watch_utility_date_time_to_unix_time(watch_rtc_get_date_time(), 0); nanosec_state.last_correction_time = movement_get_utc_timestamp();
nanosec_save(); nanosec_save();
movement_move_to_face(0); // Go to main face after saving settings movement_move_to_face(0); // Go to main face after saving settings
} }
@ -146,7 +137,7 @@ bool finetune_face_loop(movement_event_t event, void *context) {
// We flash green LED once per minute to measure clock error, when we are not on first screen // We flash green LED once per minute to measure clock error, when we are not on first screen
if (finetune_page!=0) { if (finetune_page!=0) {
watch_date_time_t date_time; watch_date_time_t date_time;
date_time = watch_rtc_get_date_time(); date_time = movement_get_utc_date_time();
if (date_time.unit.second == 0) { if (date_time.unit.second == 0) {
watch_set_led_green(); watch_set_led_green();
#ifndef __EMSCRIPTEN__ #ifndef __EMSCRIPTEN__

View File

@ -44,11 +44,6 @@
* worry about aging only on second/third years of watch calibration (if you * worry about aging only on second/third years of watch calibration (if you
* are really looking at less than 10 seconds per year of error). * are really looking at less than 10 seconds per year of error).
* *
* Warning, do not use at the first second of a month, as you might stay at
* the same month and it will surprise you. Just wait 1 second...We are not
* fully replicating RTC timer behavior when RTC is off.
* Simulating months and years is... too much complexity.
*
* For full usage instructions, please refer to the wiki: * For full usage instructions, please refer to the wiki:
* https://www.sensorwatch.net/docs/watchfaces/nanosec/ * https://www.sensorwatch.net/docs/watchfaces/nanosec/
*/ */

View File

@ -27,7 +27,6 @@
#include <math.h> #include <math.h>
#include "nanosec_face.h" #include "nanosec_face.h"
#include "filesystem.h" #include "filesystem.h"
#include "watch_utility.h"
int16_t freq_correction_residual = 0; // Dithering 0.1ppm correction, does not need to be configured. int16_t freq_correction_residual = 0; // Dithering 0.1ppm correction, does not need to be configured.
int16_t freq_correction_previous = -30000; int16_t freq_correction_previous = -30000;
@ -44,8 +43,7 @@ const float voltage_coefficient = 0.241666667 * dithering; // 10 * ppm/V. Nomina
static void nanosec_init_profile(void) { static void nanosec_init_profile(void) {
nanosec_changed = true; nanosec_changed = true;
nanosec_state.correction_cadence = 10; nanosec_state.correction_cadence = 10;
watch_date_time_t date_time = watch_rtc_get_date_time(); nanosec_state.last_correction_time = movement_get_utc_timestamp();
nanosec_state.last_correction_time = watch_utility_date_time_to_unix_time(date_time, 0);
// init data after changing profile - do that once per profile selection // init data after changing profile - do that once per profile selection
switch (nanosec_state.correction_profile) { switch (nanosec_state.correction_profile) {
@ -265,8 +263,8 @@ static void nanosec_next_edit_screen(void) {
float nanosec_get_aging() // Returns aging correction in ppm float nanosec_get_aging() // Returns aging correction in ppm
{ {
watch_date_time_t date_time = watch_rtc_get_date_time(); uint32_t timestamp = movement_get_utc_timestamp();
float years = (watch_utility_date_time_to_unix_time(date_time, 0) - nanosec_state.last_correction_time) / 31536000.0f; // Years passed since finetune float years = (timestamp - nanosec_state.last_correction_time) / 31536000.0f; // Years passed since finetune
return years*nanosec_state.aging_ppm_pa/100.0f; return years*nanosec_state.aging_ppm_pa/100.0f;
} }
@ -377,7 +375,7 @@ movement_watch_face_advisory_t nanosec_face_advise(void *context) {
// No need for background correction if we are on profile 0 - static hardware correction. // No need for background correction if we are on profile 0 - static hardware correction.
if (nanosec_state.correction_profile != 0) { if (nanosec_state.correction_profile != 0) {
watch_date_time_t date_time = watch_rtc_get_date_time(); watch_date_time_t date_time = movement_get_utc_date_time();
retval.wants_background_task = date_time.unit.minute % nanosec_state.correction_cadence == 0; retval.wants_background_task = date_time.unit.minute % nanosec_state.correction_cadence == 0;
} }

View File

@ -108,10 +108,6 @@ bool set_time_face_loop(movement_event_t event, void *context) {
case EVENT_ALARM_LONG_UP: case EVENT_ALARM_LONG_UP:
_abort_quick_ticks(); _abort_quick_ticks();
break; break;
case EVENT_MODE_BUTTON_UP:
_abort_quick_ticks();
movement_move_to_next_face();
return false;
case EVENT_LIGHT_BUTTON_DOWN: case EVENT_LIGHT_BUTTON_DOWN:
current_page = (current_page + 1) % SET_TIME_FACE_NUM_SETTINGS; current_page = (current_page + 1) % SET_TIME_FACE_NUM_SETTINGS;
*((uint8_t *)context) = current_page; *((uint8_t *)context) = current_page;
@ -187,6 +183,6 @@ bool set_time_face_loop(movement_event_t event, void *context) {
void set_time_face_resign(void *context) { void set_time_face_resign(void *context) {
(void) context; (void) context;
watch_set_led_off();
movement_store_settings(); movement_store_settings();
movement_request_tick_frequency(1);
} }

View File

@ -77,6 +77,64 @@ static void beep_setting_advance(void) {
} }
} }
static void signal_setting_display(uint8_t subsecond) {
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "SIG", "SI");
watch_display_text(WATCH_POSITION_BOTTOM, "SIGNAL");
if (subsecond % 2) {
if (movement_signal_volume() == WATCH_BUZZER_VOLUME_LOUD) {
// H for HIGH
watch_display_text(WATCH_POSITION_TOP_RIGHT, " H");
}
else {
// L for LOW
watch_display_text(WATCH_POSITION_TOP_RIGHT, " L");
}
}
}
static void signal_setting_advance(void) {
if (movement_signal_volume() == WATCH_BUZZER_VOLUME_SOFT) {
// was soft. make it loud.
movement_set_signal_volume(WATCH_BUZZER_VOLUME_LOUD);
} else {
// was loud. make it soft.
movement_set_signal_volume(WATCH_BUZZER_VOLUME_SOFT);
}
signal_setting_display(1);
movement_play_signal();
}
static void alarm_setting_display(uint8_t subsecond) {
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "ALM", "AL");
watch_display_text(WATCH_POSITION_BOTTOM, "ALARM ");
if (subsecond % 2) {
if (movement_alarm_volume() == WATCH_BUZZER_VOLUME_LOUD) {
// H for HIGH
watch_display_text(WATCH_POSITION_TOP_RIGHT, " H");
}
else {
// L for LOW
watch_display_text(WATCH_POSITION_TOP_RIGHT, " L");
}
}
}
static void alarm_setting_advance(void) {
if (movement_alarm_volume() == WATCH_BUZZER_VOLUME_SOFT) {
// was soft. make it loud.
movement_set_alarm_volume(WATCH_BUZZER_VOLUME_LOUD);
} else {
// was loud. make it soft.
movement_set_alarm_volume(WATCH_BUZZER_VOLUME_SOFT);
}
alarm_setting_display(1);
movement_play_alarm();
}
static void timeout_setting_display(uint8_t subsecond) { static void timeout_setting_display(uint8_t subsecond) {
watch_display_text_with_fallback(WATCH_POSITION_TOP, "TMOUt", "TO"); watch_display_text_with_fallback(WATCH_POSITION_TOP, "TMOUt", "TO");
if (subsecond % 2) { if (subsecond % 2) {
@ -235,7 +293,7 @@ void settings_face_setup(uint8_t watch_face_index, void ** context_ptr) {
settings_state_t *state = (settings_state_t *)*context_ptr; settings_state_t *state = (settings_state_t *)*context_ptr;
int8_t current_setting = 0; int8_t current_setting = 0;
state->num_settings = 5; // baseline, without LED settings state->num_settings = 7; // baseline, without LED settings
#ifdef BUILD_GIT_HASH #ifdef BUILD_GIT_HASH
state->num_settings++; state->num_settings++;
#endif #endif
@ -256,6 +314,12 @@ void settings_face_setup(uint8_t watch_face_index, void ** context_ptr) {
state->settings_screens[current_setting].display = beep_setting_display; state->settings_screens[current_setting].display = beep_setting_display;
state->settings_screens[current_setting].advance = beep_setting_advance; state->settings_screens[current_setting].advance = beep_setting_advance;
current_setting++; current_setting++;
state->settings_screens[current_setting].display = signal_setting_display;
state->settings_screens[current_setting].advance = signal_setting_advance;
current_setting++;
state->settings_screens[current_setting].display = alarm_setting_display;
state->settings_screens[current_setting].advance = alarm_setting_advance;
current_setting++;
state->settings_screens[current_setting].display = timeout_setting_display; state->settings_screens[current_setting].display = timeout_setting_display;
state->settings_screens[current_setting].advance = timeout_setting_advance; state->settings_screens[current_setting].advance = timeout_setting_advance;
current_setting++; current_setting++;
@ -322,7 +386,7 @@ bool settings_face_loop(movement_event_t event, void *context) {
case EVENT_MODE_BUTTON_UP: case EVENT_MODE_BUTTON_UP:
movement_force_led_off(); movement_force_led_off();
movement_move_to_next_face(); movement_move_to_next_face();
return false; return true;
case EVENT_ALARM_BUTTON_UP: case EVENT_ALARM_BUTTON_UP:
state->settings_screens[state->current_page].advance(); state->settings_screens[state->current_page].advance();
break; break;
@ -339,7 +403,7 @@ bool settings_face_loop(movement_event_t event, void *context) {
movement_force_led_on(color.red | color.red << 4, movement_force_led_on(color.red | color.red << 4,
color.green | color.green << 4, color.green | color.green << 4,
color.blue | color.blue << 4); color.blue | color.blue << 4);
return false; return true;
} else { } else {
movement_force_led_off(); movement_force_led_off();
return true; return true;

View File

@ -44,6 +44,12 @@
* This setting allows you to choose whether the Mode button should emit * This setting allows you to choose whether the Mode button should emit
* a beep when pressed, and if so, how loud it should be. Options are * a beep when pressed, and if so, how loud it should be. Options are
* "Y" for yes and "N" for no. * "Y" for yes and "N" for no.
*
* SI / SIG - Signal beep.
* This setting allows you to choose the hourly chime buzzer volume.
*
* AL / ALM - Alarm beep.
* This setting allows you to choose the alarm buzzer volume.
* *
* TO / Tmout - Timeout. * TO / Tmout - Timeout.
* Sets the time until screens that time out (like Settings and Time Set) * Sets the time until screens that time out (like Settings and Time Set)

View File

@ -0,0 +1,136 @@
/*
* 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.
_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);
}

View File

@ -41,7 +41,7 @@ void sleep(const uint8_t mode) {
} }
void watch_register_extwake_callback(uint8_t pin, watch_cb_t callback, bool level) { void watch_register_extwake_callback(uint8_t pin, watch_cb_t callback, bool level) {
uint32_t config = RTC->MODE2.TAMPCTRL.reg; uint32_t config = RTC->MODE0.TAMPCTRL.reg;
if (pin == HAL_GPIO_BTN_ALARM_pin()) { if (pin == HAL_GPIO_BTN_ALARM_pin()) {
HAL_GPIO_BTN_ALARM_in(); HAL_GPIO_BTN_ALARM_in();
@ -71,22 +71,22 @@ void watch_register_extwake_callback(uint8_t pin, watch_cb_t callback, bool leve
} }
// disable the RTC // disable the RTC
RTC->MODE2.CTRLA.bit.ENABLE = 0; RTC->MODE0.CTRLA.bit.ENABLE = 0;
while (RTC->MODE2.SYNCBUSY.bit.ENABLE); // wait for RTC to be disabled while (RTC->MODE0.SYNCBUSY.bit.ENABLE); // wait for RTC to be disabled
// update the configuration // update the configuration
RTC->MODE2.TAMPCTRL.reg = config; RTC->MODE0.TAMPCTRL.reg = config;
// re-enable the RTC // re-enable the RTC
RTC->MODE2.CTRLA.bit.ENABLE = 1; RTC->MODE0.CTRLA.bit.ENABLE = 1;
NVIC_ClearPendingIRQ(RTC_IRQn); NVIC_ClearPendingIRQ(RTC_IRQn);
NVIC_EnableIRQ(RTC_IRQn); NVIC_EnableIRQ(RTC_IRQn);
RTC->MODE2.INTENSET.reg = RTC_MODE2_INTENSET_TAMPER; RTC->MODE0.INTENSET.reg = RTC_MODE0_INTENSET_TAMPER;
} }
void watch_disable_extwake_interrupt(uint8_t pin) { void watch_disable_extwake_interrupt(uint8_t pin) {
uint32_t config = RTC->MODE2.TAMPCTRL.reg; uint32_t config = RTC->MODE0.TAMPCTRL.reg;
if (pin == HAL_GPIO_BTN_ALARM_pin()) { if (pin == HAL_GPIO_BTN_ALARM_pin()) {
btn_alarm_callback = NULL; btn_alarm_callback = NULL;
@ -101,14 +101,14 @@ void watch_disable_extwake_interrupt(uint8_t pin) {
} }
// disable the RTC // disable the RTC
RTC->MODE2.CTRLA.bit.ENABLE = 0; RTC->MODE0.CTRLA.bit.ENABLE = 0;
while (RTC->MODE2.SYNCBUSY.bit.ENABLE); // wait for RTC to be disabled while (RTC->MODE0.SYNCBUSY.bit.ENABLE); // wait for RTC to be disabled
// update the configuration // update the configuration
RTC->MODE2.TAMPCTRL.reg = config; RTC->MODE0.TAMPCTRL.reg = config;
// re-enable the RTC // re-enable the RTC
RTC->MODE2.CTRLA.bit.ENABLE = 1; RTC->MODE0.CTRLA.bit.ENABLE = 1;
} }
void watch_store_backup_data(uint32_t data, uint8_t reg) { void watch_store_backup_data(uint32_t data, uint8_t reg) {
@ -151,7 +151,8 @@ static void _watch_disable_all_pins_except_rtc(void) {
} }
static void _watch_disable_all_peripherals_except_slcd(void) { static void _watch_disable_all_peripherals_except_slcd(void) {
_watch_disable_tcc(); watch_disable_leds();
watch_disable_buzzer();
watch_disable_adc(); watch_disable_adc();
watch_disable_external_interrupts(); watch_disable_external_interrupts();

View File

@ -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,36 @@
*/ */
#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_SUBSECOND_MASK = RTC_CNT_HZ - 1;
static const uint32_t RTC_CNT_DIV = RTC_OSC_DIV - RTC_PRESCALER_DIV; // 7
static const uint32_t RTC_CNT_TICKS_PER_MINUTE = RTC_CNT_HZ * 60;
static const uint32_t RTC_COMP_GRACE_PERIOD = 4;
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;
volatile uint32_t scheduled_comp_counter;
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 +72,77 @@ 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;
}
scheduled_comp_counter = 0;
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(); static struct {
unix_timestamp_t timestamp;
rtc_date_time_t datetime;
} cached_date_time = {.datetime.reg=0, .timestamp=0};
unix_timestamp_t timestamp = watch_rtc_get_unix_time();
if (timestamp != cached_date_time.timestamp) {
cached_date_time.timestamp = timestamp;
cached_date_time.datetime = watch_utility_date_time_from_unix_time(timestamp, 0);
}
return cached_date_time.datetime;
}
void watch_rtc_set_unix_time(unix_timestamp_t unix_time) {
/* unix_time = time_backup + counter / RTC_CNT_HZ - 0.5
*
* Because of the way the hardware is designed, the periodic interrupts fire at the subsecond tick values
* according to the table below (for a 128Hz counter).
* since the 1Hz periodic interrupt is the most important, we shift the conversion from counter to timestamp by 64 ticks,
* so that the second changes at the top of the 1Hz interrupt. Hence the 0.5 factor in the equation above.
* 1Hz: 64
* 2Hz: 32, 96
* 4Hz: 16, 48, 80, 112
* 8Hz: 8, 24, 40, 56, 72, 88, 104, 120
* 16Hz: 4, 12, 20, ..., 124
* 32Hz: 2, 6, 10, ..., 126
* 64Hz: 1, 3, 5, ..., 127
* 128Hz: 0, 1, 2, ..., 127
*/
rtc_counter_t counter = rtc_get_counter();
unix_timestamp_t tb = unix_time - (counter >> RTC_CNT_DIV) - ((counter & RTC_CNT_SUBSECOND_MASK) >> (RTC_CNT_DIV - 1)) + 1;
watch_store_backup_data(tb, TB_BKUP_REG);
}
unix_timestamp_t watch_rtc_get_unix_time(void) {
// unix_time = time_backup + counter / RTC_CNT_HZ - 0.5
rtc_counter_t counter = rtc_get_counter();
unix_timestamp_t tb = watch_get_backup_data(TB_BKUP_REG);
return tb + (counter >> RTC_CNT_DIV) + ((counter & RTC_CNT_SUBSECOND_MASK) >> (RTC_CNT_DIV - 1)) - 1;
}
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 +192,123 @@ 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) { void watch_rtc_schedule_next_comp(void) {
RTC->MODE2.Mode2Alarm[0].ALARM.reg = alarm_time.reg; rtc_counter_t curr_counter = watch_rtc_get_counter();
RTC->MODE2.Mode2Alarm[0].MASK.reg = mask;
RTC->MODE2.INTENSET.reg = RTC_MODE2_INTENSET_ALARM0; // We want to ensure we never miss any registered callbacks,
alarm_callback = callback; // so if a callback counter has just passed but didn't fire, give it a chance to fire.
NVIC_ClearPendingIRQ(RTC_IRQn); rtc_counter_t lax_curr_counter = curr_counter - RTC_COMP_GRACE_PERIOD;
NVIC_EnableIRQ(RTC_IRQn);
RTC->MODE2.INTENSET.reg = RTC_MODE2_INTENSET_ALARM0; bool schedule_any = false;
rtc_counter_t comp_counter;
rtc_counter_t min_diff = UINT_MAX;
for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) {
if (comp_callbacks[index].enabled) {
rtc_counter_t diff = comp_callbacks[index].counter - lax_curr_counter;
if (diff <= min_diff) {
min_diff = diff;
comp_counter = comp_callbacks[index].counter;
schedule_any = true;
}
}
}
if (schedule_any) {
// If we are changing the comp counter at the front of the line, don't schedule a comp interrupt for a counter that is too close to now
if (comp_counter != scheduled_comp_counter) {
rtc_counter_t earliest_comp_counter = curr_counter + RTC_COMP_GRACE_PERIOD;
if ((earliest_comp_counter - lax_curr_counter) > (comp_counter - lax_curr_counter)) {
comp_counter = earliest_comp_counter;
}
scheduled_comp_counter = comp_counter;
rtc_enable_compare_interrupt(comp_counter);
}
} else {
scheduled_comp_counter = lax_curr_counter - RTC_COMP_GRACE_PERIOD;
rtc_disable_compare_interrupt();
}
} }
void watch_rtc_disable_alarm_callback(void) { void watch_rtc_register_comp_callback(watch_cb_t callback, rtc_counter_t counter, uint8_t index) {
RTC->MODE2.INTENCLR.reg = RTC_MODE2_INTENCLR_ALARM0; if (index >= WATCH_RTC_N_COMP_CB) {
return;
}
comp_callbacks[index].counter = counter;
comp_callbacks[index].callback = callback;
comp_callbacks[index].enabled = true;
watch_rtc_schedule_next_comp();
} }
void watch_rtc_callback(uint16_t interrupt_status) { void watch_rtc_register_comp_callback_no_schedule(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) { comp_callbacks[index].counter = counter;
comp_callbacks[index].callback = callback;
comp_callbacks[index].enabled = true;
}
void watch_rtc_disable_comp_callback(uint8_t index) {
if (index >= WATCH_RTC_N_COMP_CB) {
return;
}
comp_callbacks[index].enabled = false;
watch_rtc_schedule_next_comp();
}
void watch_rtc_disable_comp_callback_no_schedule(uint8_t index) {
if (index >= WATCH_RTC_N_COMP_CB) {
return;
}
comp_callbacks[index].enabled = false;
}
void watch_rtc_callback(uint16_t interrupt_cause) {
// First read all relevant registers, to ensure no changes occurr during the callbacks
rtc_counter_t curr_counter = watch_rtc_get_counter();
uint16_t interrupt_enabled = (uint16_t)RTC->MODE0.INTENSET.reg;
if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_PER_Msk) {
// 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 +316,36 @@ 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 ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_CMP0) {
if (alarm_callback != NULL) { for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) {
alarm_callback(); if (comp_callbacks[index].enabled &&
(curr_counter - comp_callbacks[index].counter) < (RTC_COMP_GRACE_PERIOD * 4)
) {
comp_callbacks[index].enabled = false;
comp_callbacks[index].callback();
}
} }
RTC->MODE2.INTFLAG.reg = RTC_MODE2_INTFLAG_ALARM0; watch_rtc_schedule_next_comp();
}
if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_OVF) {
// Handle the overflow of the counter. All we need to do is reset the reference time.
unix_timestamp_t tb = watch_get_backup_data(TB_BKUP_REG);
watch_store_backup_data(tb + (UINT_MAX >> RTC_CNT_DIV), TB_BKUP_REG);
} }
} }
void watch_rtc_enable(bool en) { 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 +354,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
} }

View File

@ -27,14 +27,30 @@
#include "tcc.h" #include "tcc.h"
#include "tc.h" #include "tc.h"
void _watch_enable_tcc(void); static void _watch_enable_tcc(void);
void cb_watch_buzzer_seq(void); static void _watch_disable_tcc(void);
static void _watch_maybe_enable_tcc(void);
static void _watch_maybe_disable_tcc(void);
static void _watch_enable_led_pins(void);
static void _watch_disable_led_pins(void);
static void (*_cb_tc0)(void) = NULL;
static void cb_watch_buzzer_seq(void);
static void cb_watch_buzzer_raw_source(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 watch_buzzer_raw_source_t _raw_source;
static void* _userdata;
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 volatile uint8_t _current_led_color[3] = {0, 0, 0};
static void _watch_set_led_duty_cycle(uint32_t period, uint8_t red, uint8_t green, uint8_t blue);
static void _tcc_write_RUNSTDBY(bool value) { static void _tcc_write_RUNSTDBY(bool value) {
// enables or disables RUNSTDBY of the tcc // enables or disables RUNSTDBY of the tcc
@ -46,13 +62,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 +82,30 @@ 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();
if (_cb_start_global) {
_cb_start_global();
}
watch_enable_buzzer();
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();
_cb_tc0 = cb_watch_buzzer_seq;
// 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,51 +135,156 @@ 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
_tone_ticks = _sequence[_seq_position + 1]; _tone_ticks = _sequence[_seq_position + 1] - 1;
_seq_position += 2; _seq_position += 2;
} 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_play_raw_source(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end) {
watch_buzzer_play_raw_source_with_volume(raw_source, userdata, callback_on_end, WATCH_BUZZER_VOLUME_LOUD);
}
void watch_buzzer_play_raw_source_with_volume(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end, watch_buzzer_volume_t volume) {
// Abort any previous sequence
watch_buzzer_abort_sequence();
if (_cb_start_global) {
_cb_start_global();
}
watch_enable_buzzer();
watch_set_buzzer_off();
_raw_source = raw_source;
_userdata = userdata;
_cb_finished = callback_on_end;
_volume = volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25;
_seq_position = 0;
_tone_ticks = 0;
// prepare buzzer
_cb_tc0 = cb_watch_buzzer_raw_source;
// setup TC0 timer
_tc0_initialize();
// start the timer (for the 64 hz callback)
_tc0_start();
}
void cb_watch_buzzer_raw_source(void) {
// callback for reading the note sequence
uint16_t period;
uint16_t duration;
bool done;
if (_tone_ticks == 0) {
done = _raw_source(_seq_position, _userdata, &period, &duration);
if (done || duration == 0) {
// end the sequence
watch_buzzer_abort_sequence();
} else {
if (period == WATCH_BUZZER_PERIOD_REST) {
watch_set_buzzer_off();
} else {
watch_set_buzzer_period_and_duty_cycle(period, _volume);
watch_set_buzzer_on();
}
// set duration ticks and move to next tone
_tone_ticks = duration - 1;
_seq_position += 1;
}
} 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;
}
_tc0_stop();
watch_set_buzzer_off(); watch_set_buzzer_off();
// disable standby mode for TCC
_tcc_write_RUNSTDBY(false); // disable TCC
watch_disable_buzzer();
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) {
// interrupt handler for TC0 (globally!) // interrupt handler for TC0 (globally!)
cb_watch_buzzer_seq(); if (_cb_tc0) {
_cb_tc0();
}
TC0->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF; TC0->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF;
} }
bool watch_is_buzzer_or_led_enabled(void){ void _watch_maybe_enable_tcc(void) {
return tcc_is_enabled(0); if (!_buzzer_is_active && !_led_is_active) {
return;
}
if (!tcc_is_enabled(0)) {
// tcc_set_run_in_standby(0, true);
_watch_enable_tcc();
// TCC should run in standby mode
_tcc_write_RUNSTDBY(true);
}
} }
inline void watch_enable_buzzer(void) { void _watch_maybe_disable_tcc(void) {
if (!tcc_is_enabled(0)) { if (_buzzer_is_active || _led_is_active) {
_watch_enable_tcc(); return;
} }
if (tcc_is_enabled(0)) {
_tcc_write_RUNSTDBY(false);
_watch_disable_tcc();
}
}
void watch_enable_buzzer(void) {
_buzzer_is_active = true;
_watch_maybe_enable_tcc();
}
void watch_disable_buzzer(void) {
_buzzer_is_active = false;
watch_set_buzzer_off();
_watch_maybe_disable_tcc();
} }
void watch_set_buzzer_period_and_duty_cycle(uint32_t period, uint8_t duty) { 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);
} // The buzzer determines the period, which means that if the LED was active before it will flicker
// Update the LED duty cycle to match the new period required by the buzzer.
void watch_disable_buzzer(void) { if (_led_is_active) {
_watch_disable_tcc(); _watch_set_led_duty_cycle(period, _current_led_color[0], _current_led_color[1], _current_led_color[2]);
}
} }
inline void watch_set_buzzer_on(void) { inline void watch_set_buzzer_on(void) {
@ -172,14 +302,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); // 64 ticks per second for the tc0
watch_set_buzzer_on(); // Each tick is approximately 15ms
} uint16_t duration = duration_ms / 15;
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) {
@ -220,21 +353,6 @@ void _watch_enable_tcc(void) {
tcc_set_cc(0, (WATCH_BLUE_TCC_CHANNEL) % 4, 0, false); tcc_set_cc(0, (WATCH_BLUE_TCC_CHANNEL) % 4, 0, false);
#endif #endif
// enable LED PWM pins (the LED driver assumes if the TCC is on, the pins are enabled)
HAL_GPIO_RED_pmuxen(HAL_GPIO_PMUX_TCC_ALT);
HAL_GPIO_RED_drvstr(1);
HAL_GPIO_RED_out();
#ifdef WATCH_GREEN_TCC_CHANNEL
HAL_GPIO_GREEN_pmuxen(HAL_GPIO_PMUX_TCC_ALT);
HAL_GPIO_GREEN_drvstr(1);
HAL_GPIO_GREEN_out();
#endif
#ifdef WATCH_BLUE_TCC_CHANNEL
HAL_GPIO_BLUE_pmuxen(HAL_GPIO_PMUX_TCC_ALT);
HAL_GPIO_BLUE_drvstr(1);
HAL_GPIO_BLUE_out();
#endif
// Enable the TCC // Enable the TCC
tcc_enable(0); tcc_enable(0);
} }
@ -257,13 +375,45 @@ void _watch_disable_tcc(void) {
} }
void watch_enable_leds(void) { void watch_enable_leds(void) {
if (!tcc_is_enabled(0)) { _led_is_active = true;
_watch_enable_tcc(); _watch_enable_led_pins();
} _watch_maybe_enable_tcc();
} }
void watch_disable_leds(void) { void watch_disable_leds(void) {
_watch_disable_tcc(); _led_is_active = false;
_watch_disable_led_pins();
_watch_maybe_disable_tcc();
}
void _watch_enable_led_pins(void) {
// enable LED PWM pins (the LED driver assumes if the TCC is on, the pins are enabled)
HAL_GPIO_RED_pmuxen(HAL_GPIO_PMUX_TCC_ALT);
HAL_GPIO_RED_drvstr(1);
HAL_GPIO_RED_out();
#ifdef WATCH_GREEN_TCC_CHANNEL
HAL_GPIO_GREEN_pmuxen(HAL_GPIO_PMUX_TCC_ALT);
HAL_GPIO_GREEN_drvstr(1);
HAL_GPIO_GREEN_out();
#endif
#ifdef WATCH_BLUE_TCC_CHANNEL
HAL_GPIO_BLUE_pmuxen(HAL_GPIO_PMUX_TCC_ALT);
HAL_GPIO_BLUE_drvstr(1);
HAL_GPIO_BLUE_out();
#endif
}
void _watch_disable_led_pins(void) {
HAL_GPIO_RED_pmuxdis();
HAL_GPIO_RED_off();
#ifdef WATCH_GREEN_TCC_CHANNEL
HAL_GPIO_GREEN_pmuxdis();
HAL_GPIO_GREEN_off();
#endif
#ifdef WATCH_BLUE_TCC_CHANNEL
HAL_GPIO_BLUE_pmuxdis();
HAL_GPIO_BLUE_off();
#endif
} }
void watch_set_led_color(uint8_t red, uint8_t green) { void watch_set_led_color(uint8_t red, uint8_t green) {
@ -274,20 +424,35 @@ void watch_set_led_color(uint8_t red, uint8_t green) {
#endif #endif
} }
void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) { static void _watch_set_led_duty_cycle(uint32_t period, uint8_t red, uint8_t green, uint8_t blue) {
if (tcc_is_enabled(0)) { tcc_set_cc(0, (WATCH_RED_TCC_CHANNEL) % 4, ((period * (uint32_t)red * 1000ull) / 255000ull), true);
uint32_t period = tcc_get_period(0);
tcc_set_cc(0, (WATCH_RED_TCC_CHANNEL) % 4, ((period * (uint32_t)red * 1000ull) / 255000ull), true);
#ifdef WATCH_GREEN_TCC_CHANNEL #ifdef WATCH_GREEN_TCC_CHANNEL
tcc_set_cc(0, (WATCH_GREEN_TCC_CHANNEL) % 4, ((period * (uint32_t)green * 1000ull) / 255000ull), true); tcc_set_cc(0, (WATCH_GREEN_TCC_CHANNEL) % 4, ((period * (uint32_t)green * 1000ull) / 255000ull), true);
#else #else
(void) green; // silence warning (void) green; // silence warning
#endif #endif
#ifdef WATCH_BLUE_TCC_CHANNEL #ifdef WATCH_BLUE_TCC_CHANNEL
tcc_set_cc(0, (WATCH_BLUE_TCC_CHANNEL) % 4, ((period * (uint32_t)blue * 1000ull) / 255000ull), true); tcc_set_cc(0, (WATCH_BLUE_TCC_CHANNEL) % 4, ((period * (uint32_t)blue * 1000ull) / 255000ull), true);
#else #else
(void) blue; // silence warning (void) blue; // silence warning
#endif #endif
}
void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) {
bool turning_on = (red | green | blue) != 0;
if (turning_on) {
_current_led_color[0] = red;
_current_led_color[1] = green;
_current_led_color[2] = blue;
watch_enable_leds();
uint32_t period = tcc_get_period(0);
_watch_set_led_duty_cycle(period, red, green, blue);
} else {
if (tcc_is_enabled(0)) {
_watch_set_led_duty_cycle(1, red, green, blue);
}
watch_disable_leds();
} }
} }

View File

@ -277,20 +277,26 @@ inline void lis2dw_disable_fifo(void) {
#endif #endif
} }
bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data) { bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data, uint32_t timeout) {
// timeout is in terms of 1/RTC_CNT_HZ seconds (likely 128 timeouts is one second)
#ifdef I2C_SERCOM #ifdef I2C_SERCOM
uint8_t temp = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_FIFO_SAMPLE); uint8_t temp = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_FIFO_SAMPLE);
bool overrun = !!(temp & LIS2DW_FIFO_SAMPLE_OVERRUN); bool overrun = !!(temp & LIS2DW_FIFO_SAMPLE_OVERRUN);
fifo_data->count = temp & LIS2DW_FIFO_SAMPLE_COUNT; fifo_data->count = temp & LIS2DW_FIFO_SAMPLE_COUNT;
rtc_counter_t timeout_counter = watch_rtc_get_counter() + timeout;
for(int i = 0; i < fifo_data->count; i++) { for(int i = 0; i < fifo_data->count; i++) {
if (watch_rtc_get_counter() > timeout_counter) {
break;
}
fifo_data->readings[i] = lis2dw_get_raw_reading(); fifo_data->readings[i] = lis2dw_get_raw_reading();
} }
return overrun; return overrun;
#else #else
(void) fifo_data; (void) fifo_data;
(void) timeout;
return false; return false;
#endif #endif
} }
@ -411,6 +417,33 @@ void lis2dw_configure_int2(uint8_t sources) {
#endif #endif
} }
void lis2dw12_int_notification_set(lis2dw12_lir_t val) {
#ifdef I2C_SERCOM
uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL3);
if (val == LIS2DW12_INT_LATCHED) {
configuration |= LIS2DW_CTRL3_VAL_LIR;
} else {
configuration &= ~LIS2DW_CTRL7_VAL_DRDY_PULSED;
}
watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL3, configuration);
#else
(void)val;
#endif
}
lis2dw12_lir_t lis2dw12_int_notification_get(void) {
#ifdef I2C_SERCOM
uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL3);
if (configuration & LIS2DW12_INT_LATCHED) {
return LIS2DW12_INT_LATCHED;
} else {
return LIS2DW12_INT_PULSED;
}
#else
return LIS2DW12_INT_PULSED;
#endif
}
void lis2dw_enable_interrupts(void) { void lis2dw_enable_interrupts(void) {
#ifdef I2C_SERCOM #ifdef I2C_SERCOM
uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7); uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7);
@ -425,6 +458,20 @@ void lis2dw_disable_interrupts(void) {
#endif #endif
} }
void lis2dw_pulsed_drdy_interrupts(void) {
#ifdef I2C_SERCOM
uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7);
watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7, configuration | LIS2DW_CTRL7_VAL_DRDY_PULSED);
#endif
}
void lis2dw_latched_drdy_interrupts(void) {
#ifdef I2C_SERCOM
uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7);
watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7, configuration & ~LIS2DW_CTRL7_VAL_DRDY_PULSED);
#endif
}
lis2dw_wakeup_source_t lis2dw_get_wakeup_source() { lis2dw_wakeup_source_t lis2dw_get_wakeup_source() {
#ifdef I2C_SERCOM #ifdef I2C_SERCOM
return (lis2dw_wakeup_source_t) watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_SRC); return (lis2dw_wakeup_source_t) watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_SRC);

View File

@ -92,6 +92,12 @@ typedef enum {
LIS2DW_FILTER_HIGH_PASS = 1, LIS2DW_FILTER_HIGH_PASS = 1,
} lis2dw_filter_t; } lis2dw_filter_t;
typedef enum
{
LIS2DW12_INT_PULSED = 0,
LIS2DW12_INT_LATCHED = 1,
} lis2dw12_lir_t;
typedef enum { typedef enum {
LIS2DW_RANGE_16_G = 0b11, // +/- 16g LIS2DW_RANGE_16_G = 0b11, // +/- 16g
LIS2DW_RANGE_8_G = 0b10, // +/- 8g LIS2DW_RANGE_8_G = 0b10, // +/- 8g
@ -295,6 +301,8 @@ typedef enum {
#define LIS2DW_CTRL7_VAL_HP_REF_MODE 0b00000010 #define LIS2DW_CTRL7_VAL_HP_REF_MODE 0b00000010
#define LIS2DW_CTRL7_VAL_LPASS_ON6D 0b00000001 #define LIS2DW_CTRL7_VAL_LPASS_ON6D 0b00000001
#define LIS2DW_FIFO_TIMEOUT 100 // timeout is in terms of 1/RTC_CNT_HZ seconds (likely 128 timeouts is one second)
bool lis2dw_begin(void); bool lis2dw_begin(void);
uint8_t lis2dw_get_device_id(void); uint8_t lis2dw_get_device_id(void);
@ -339,7 +347,7 @@ void lis2dw_enable_fifo(void);
void lis2dw_disable_fifo(void); void lis2dw_disable_fifo(void);
bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data); bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data, uint32_t timeout);
void lis2dw_clear_fifo(void); void lis2dw_clear_fifo(void);
@ -367,6 +375,10 @@ void lis2dw_configure_tap_threshold(uint8_t threshold_x, uint8_t threshold_y, ui
void lis2dw_configure_tap_duration(uint8_t latency, uint8_t quiet, uint8_t shock); void lis2dw_configure_tap_duration(uint8_t latency, uint8_t quiet, uint8_t shock);
void lis2dw12_int_notification_set(lis2dw12_lir_t val);
lis2dw12_lir_t lis2dw12_int_notification_get(void);
void lis2dw_configure_int1(uint8_t sources); void lis2dw_configure_int1(uint8_t sources);
void lis2dw_configure_int2(uint8_t sources); void lis2dw_configure_int2(uint8_t sources);
@ -375,6 +387,10 @@ void lis2dw_enable_interrupts(void);
void lis2dw_disable_interrupts(void); void lis2dw_disable_interrupts(void);
void lis2dw_pulsed_drdy_interrupts(void);
void lis2dw_latched_drdy_interrupts(void);
lis2dw_interrupt_source_t lis2dw_get_interrupt_source(void); lis2dw_interrupt_source_t lis2dw_get_interrupt_source(void);
lis2dw_wakeup_source_t lis2dw_get_wakeup_source(void); lis2dw_wakeup_source_t lis2dw_get_wakeup_source(void);

View File

@ -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);
/** @} */

View File

@ -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,79 @@ 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 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. * 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 counter 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. * @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 alarm interrupt is a versatile tool for scheduling events in the future, especially since it can * @details The hardware RTC provides us with single interrupt that fires when the RTC counter matches a target counter COMP0.
* wake the device from all sleep modes. The key to its versatility is the mask parameter. * With a little bit of logic, we can provide multiple active compare callbacks. Every time a comp callback is
* Suppose we set an alarm for midnight, 00:00:00. * registered/disabled/fired we iterate over all the active comp callbacks and set the hardware COMP0 counter
* * if mask is ALARM_MATCH_SS, the alarm will fire every minute when the clock ticks to seconds == 0. * to the next occurring one.
* * with ALARM_MATCH_MMSS, the alarm will once an hour, at the top of each hour. * With this very simple API, movement can implement one-shot timers to turn off the led and determine button longpresses
* * with ALARM_MATCH_HHMMSS, the alarm will fire at midnight every day. * as well as the inactivity timeouts for resigning and sleeping, as well as emulating the top of the minute alarm.
* 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_register_comp_callback(watch_cb_t callback, rtc_counter_t counter, uint8_t index);
/** @brief Just like watch_rtc_register_comp_callback but doesn't actually schedule the callback
*
* Useful if you need register multiple callbacks at once, avoids multiple calls to the expensive watch_rtc_schedule_next_comp:
* Usage:
* watch_rtc_register_comp_callback_no_schedule(cb0, counter0, index0);
* watch_rtc_register_comp_callback_no_schedule(cb1, counter1, index1);
* watch_rtc_schedule_next_comp();
*/
void watch_rtc_register_comp_callback_no_schedule(watch_cb_t callback, rtc_counter_t counter, uint8_t index);
/** @brief Disables the specified comp callback.
*/
void watch_rtc_disable_comp_callback(uint8_t index);
/** @brief Just like watch_rtc_disable_comp_callback but doesn't actually schedule the callback
*
* Useful if you need disable multiple callbacks at once, avoids multiple calls to the expensive watch_rtc_schedule_next_comp:
* Usage:
* watch_rtc_disable_comp_callback_no_schedule(index0);
* watch_rtc_disable_comp_callback_no_schedule(index1);
* watch_rtc_schedule_next_comp();
*/
/** @brief Disables the specified comp callback.
*/
void watch_rtc_disable_comp_callback_no_schedule(uint8_t index);
/** @brief Determines the first comp callback that should fire and schedule it with the RTC
*
* You would never need to call this manually, unless you used the 'no_schedule' functions above.
*/
void watch_rtc_schedule_next_comp(void);
/** @brief Disables the alarm callback. /** @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 +173,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);

View File

@ -126,12 +126,9 @@ typedef enum {
BUZZER_NOTE_REST ///< no sound BUZZER_NOTE_REST ///< no sound
} watch_buzzer_note_t; } watch_buzzer_note_t;
/** @brief Returns true if either the buzzer or the LED driver is enabled. #define WATCH_BUZZER_PERIOD_REST 0
* @details Both the buzzer and the LED use the TCC peripheral to drive their behavior. This function returns true if that
* peripheral is enabled. You can use this function to determine whether you need to call the watch_enable_leds or typedef bool (*watch_buzzer_raw_source_t)(uint16_t position, void* userdata, uint16_t* period, uint16_t* duration);
* or watch_enable_buzzer functions before using these peripherals.
*/
bool watch_is_buzzer_or_led_enabled(void);
/** @addtogroup tcc Buzzer and LED Control (via the TCC peripheral) /** @addtogroup tcc Buzzer and LED Control (via the TCC peripheral)
* @brief This section covers functions related to Timer Counter for Control peripheral, which drives the piezo buzzer * @brief This section covers functions related to Timer Counter for Control peripheral, which drives the piezo buzzer
@ -151,15 +148,13 @@ void watch_enable_buzzer(void);
*/ */
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);
/** @brief Disables the TCC peripheral that drives the buzzer. /** @brief Disables the TCC peripheral that drives the buzzer (if LED not active).
* @note If you are using PWM to set custom LED colors, this method will also disable the LED PWM driver,
* since the buzzer and LED both make use of the same peripheral to drive their PWM behavior.
*/ */
void watch_disable_buzzer(void); void watch_disable_buzzer(void);
/** @brief Turns the buzzer output on. It will emit a continuous sound at the given frequency. /** @brief Turns the buzzer output on. It will emit a continuous sound at the given frequency.
* @note The TCC peripheral that drives the buzzer does not run in standby mode; if you wish for buzzer * @note The TCC peripheral that drives the buzzer does run in standby mode; if you wish for buzzer
* output to continue, you should prevent your app from going to sleep. * output to continue, you don't need to prevent your app from going to sleep.
*/ */
void watch_set_buzzer_on(void); void watch_set_buzzer_on(void);
@ -170,8 +165,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 +172,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 +193,69 @@ 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 Plays the given raw buzzer source function in a non-blocking way.
*
* @details This function plays audio data generated by a raw source callback function,
* allowing for precise control over buzzer timing and frequency. The raw source
* function is called repeatedly to generate audio samples, each containing a
* period and duration for the buzzer tone.
* Useful for applications such as chirpy, so that they won't need to allocate a
* long note sequence, and we will also take care of all the timing logic.
*
* @param raw_source Pointer to the callback function that generates raw buzzer data.
* The function should take a position parameter and return true if
* more data is available, false if end of sequence is reached.
* Parameters:
* - position: Current position in the audio sequence (0-based)
* - userdata: User-provided data passed through to the callback
* - period: Pointer to store the period (in microseconds) for the tone
* - duration: Pointer to store the duration (in microseconds) for the tone
* @param userdata Pointer to user data that will be passed to the raw_source callback
* @param callback_on_end A pointer to a callback function to be invoked when the sequence has finished playing.
*/
void watch_buzzer_play_raw_source(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end);
/** @brief Plays the given raw buzzer source function in a non-blocking way.
*
* @details This function plays audio data generated by a raw source callback function,
* allowing for precise control over buzzer timing and frequency. The raw source
* function is called repeatedly to generate audio samples, each containing a
* period and duration for the buzzer tone.
* Useful for applications such as chirpy, so that they won't need to allocate a
* long note sequence, and we will also take care of all the timing logic.
*
* @param raw_source Pointer to the callback function that generates raw buzzer data.
* The function should take a position parameter and return true if
* more data is available, false if end of sequence is reached.
* Parameters:
* - position: Current position in the audio sequence (0-based)
* - userdata: User-provided data passed through to the callback
* - period: Pointer to store the period (in microseconds) for the tone
* - duration: Pointer to store the duration (in microseconds) for the tone
* @param userdata Pointer to user data that will be passed to the raw_source callback
* @param callback_on_end A pointer to a callback function to be invoked when the sequence has finished playing.
* @param volume either WATCH_BUZZER_VOLUME_SOFT or WATCH_BUZZER_VOLUME_LOUD
*/
void watch_buzzer_play_raw_source_with_volume(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end, watch_buzzer_volume_t volume);
/** @brief Aborts a playing sequence. /** @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);
#ifndef __EMSCRIPTEN__ #ifndef __EMSCRIPTEN__
void irq_handler_tc0(void); void irq_handler_tc0(void);
#endif #endif
@ -223,26 +273,17 @@ void irq_handler_tc0(void);
* so that watch_set_led_red sets the red LED, and watch_set_led_green sets the blue one. * so that watch_set_led_red sets the red LED, and watch_set_led_green sets the blue one.
*/ */
/// @{ /// @{
/** @brief Enables the bi-color LED. /** @brief Enables the TCC peripheral, which drives the LEDs.
* @note The TCC peripheral that drives the LEDs does not run in STANDBY mode — but the outputs do! This
* means that if you set either red, green or both LEDs to full power, they will shine even when
* your app is asleep. If, however, you set a custom color using watch_set_led_color, the color will
* not display correctly in STANDBY mode. You will need to keep your app running while the LED is on.
*/ */
void watch_enable_leds(void); void watch_enable_leds(void);
/** @brief Disables the LEDs. /** @brief Disables the TCC peripheral that drives the LEDs (if buzzer not active).
* @note This method will also disable the buzzer, since the buzzer and LED both make use of the same
* peripheral to drive their PWM behavior.
*/ */
void watch_disable_leds(void); void watch_disable_leds(void);
/** @brief Sets the LED to a custom color by modulating each output's duty cycle. /** @brief Sets the LED to a custom color by modulating each output's duty cycle.
* @param red The red value from 0-255. * @param red The red value from 0-255.
* @param green The green value from 0-255. If your watch has a red/blue LED, this will be the blue value. * @param green The green value from 0-255. If your watch has a red/blue LED, this will be the blue value.
* @note If you are displaying a custom color, you will need to prevent your app from going to sleep
* while the LED is on; otherwise, the color will not display correctly. You can do this by
* returning false in your app_loop method.
*/ */
void watch_set_led_color(uint8_t red, uint8_t green); void watch_set_led_color(uint8_t red, uint8_t green);
@ -250,9 +291,6 @@ void watch_set_led_color(uint8_t red, uint8_t green);
* @param red The red value from 0-255. * @param red The red value from 0-255.
* @param green The green value from 0-255. * @param green The green value from 0-255.
* @param blue The blue value from 0-255. * @param blue The blue value from 0-255.
* @note If you are displaying a custom color, you will need to prevent your app from going to sleep
* while the LED is on; otherwise, the color will not display correctly. You can do this by
* returning false in your app_loop method.
*/ */
void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue); void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue);
@ -277,9 +315,6 @@ void watch_set_led_yellow(void);
/** @brief Turns both the red and the green LEDs off. */ /** @brief Turns both the red and the green LEDs off. */
void watch_set_led_off(void); void watch_set_led_off(void);
/** @brief Disables the TCC peripheral. Should only be called internally. */
void _watch_disable_tcc(void);
/// @brief An array of periods for all the notes on a piano, corresponding to the names in watch_buzzer_note_t. /// @brief An array of periods for all the notes on a piano, corresponding to the names in watch_buzzer_note_t.
extern const uint16_t NotePeriods[108]; extern const uint16_t NotePeriods[108];

View File

@ -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;

View File

@ -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

View File

@ -1,9 +1,5 @@
#include "watch.h" #include "watch.h"
bool watch_is_buzzer_or_led_enabled(void) {
return false;
}
bool watch_is_usb_enabled(void) { bool watch_is_usb_enabled(void) {
return true; return true;
} }

View File

@ -51,8 +51,6 @@ int _gettimeofday(struct timeval *tv, void *tzvp) {
return 0; return 0;
} }
void _watch_disable_tcc(void) {}
void _watch_enable_usb(void) {} void _watch_enable_usb(void) {}
void watch_disable_TRNG() {} void watch_disable_TRNG() {}

View File

@ -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_SUBSECOND_MASK = RTC_CNT_HZ - 1;
static const uint32_t RTC_CNT_DIV = 7;
static const uint32_t RTC_CNT_TICKS_PER_MINUTE = RTC_CNT_HZ * 60;
static 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) {
// unix_time = time_backup + counter / RTC_CNT_HZ - 0.5
rtc_counter_t counter = watch_rtc_get_counter();
reference_timestamp = unix_time - (counter >> RTC_CNT_DIV) - ((counter & RTC_CNT_SUBSECOND_MASK) >> (RTC_CNT_DIV - 1)) + 1;
}
unix_timestamp_t watch_rtc_get_unix_time(void) {
// unix_time = time_backup + counter / RTC_CNT_HZ - 0.5
rtc_counter_t counter = watch_rtc_get_counter();
return reference_timestamp + (counter >> RTC_CNT_DIV) + ((counter & RTC_CNT_SUBSECOND_MASK) >> (RTC_CNT_DIV - 1)) - 1;
}
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,95 @@ 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) {
} return;
static void watch_invoke_alarm_callback(void *userData) {
if (alarm_callback) alarm_callback();
alarm_interval_id = emscripten_set_interval(watch_invoke_alarm_interval_callback, alarm_interval, NULL);
}
void watch_rtc_register_alarm_callback(watch_cb_t callback, watch_date_time_t alarm_time, rtc_alarm_match_t mask) {
watch_rtc_disable_alarm_callback();
switch (mask) {
case ALARM_MATCH_DISABLED:
return;
case ALARM_MATCH_SS:
alarm_interval = 60 * 1000;
break;
case ALARM_MATCH_MMSS:
alarm_interval = 60 * 60 * 1000;
break;
case ALARM_MATCH_HHMMSS:
alarm_interval = 60 * 60 * 60 * 1000;
break;
} }
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 void watch_rtc_register_comp_callback_no_schedule(watch_cb_t callback, rtc_counter_t counter, uint8_t index) {
if (second < date.getSeconds()) date.setMinutes(date.getMinutes() + 1); if (index >= WATCH_RTC_N_COMP_CB) {
date.setSeconds(second); return;
} else if ($1 == 2) { // MMSS }
if (second < date.getSeconds()) date.setMinutes(date.getMinutes() + 1);
if (minute < date.getMinutes()) date.setHours(date.getHours() + 1); comp_callbacks[index].counter = counter;
date.setMinutes(minute, second); comp_callbacks[index].callback = callback;
} else if ($1 == 3) { // HHMMSS comp_callbacks[index].enabled = true;
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); void watch_rtc_disable_comp_callback(uint8_t index) {
date.setHours(hour, minute, second); if (index >= WATCH_RTC_N_COMP_CB) {
} else { return;
throw 'Invalid alarm match mask'; }
comp_callbacks[index].enabled = false;
watch_rtc_schedule_next_comp();
}
void watch_rtc_disable_comp_callback_no_schedule(uint8_t index) {
if (index >= WATCH_RTC_N_COMP_CB) {
return;
}
comp_callbacks[index].enabled = false;
}
void watch_rtc_schedule_next_comp(void) {
rtc_counter_t curr_counter = watch_rtc_get_counter();
// If there is already a pending comp interrupt for this very tick, let it fire
// And this function will be called again as soon as the interrupt fires.
if (curr_counter == scheduled_comp_counter) {
return;
}
// The soonest we can schedule is the next tick
curr_counter +=1;
bool schedule_any = false;
rtc_counter_t comp_counter;
rtc_counter_t min_diff = UINT_MAX;
for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) {
// rtc_counter_t diff =
if (comp_callbacks[index].enabled) {
rtc_counter_t diff = comp_callbacks[index].counter - curr_counter;
if (diff <= min_diff) {
min_diff = diff;
comp_counter = comp_callbacks[index].counter;
schedule_any = true;
}
} }
return date - now;
}, alarm_time.reg, mask);
alarm_callback = callback;
alarm_timeout_id = emscripten_set_timeout(watch_invoke_alarm_callback, timeout, NULL);
}
void watch_rtc_disable_alarm_callback(void) {
alarm_callback = NULL;
alarm_interval = 0;
if (alarm_timeout_id != -1) {
emscripten_clear_timeout(alarm_timeout_id);
alarm_timeout_id = -1;
} }
if (alarm_interval_id != -1) { if (schedule_any) {
emscripten_clear_interval(alarm_interval_id); scheduled_comp_counter = comp_counter;
alarm_interval_id = -1; } else {
scheduled_comp_counter = curr_counter - 2;
} }
} }
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)

View File

@ -28,18 +28,23 @@
#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);
void cb_watch_buzzer_raw_source(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 watch_buzzer_raw_source_t _raw_source;
static void* _userdata;
static uint8_t _volume;
static void (*_cb_finished)(void); static void (*_cb_finished)(void);
static watch_cb_t _cb_start_global = NULL;
void _watch_enable_tcc(void) {} static watch_cb_t _cb_stop_global = NULL;
static volatile bool _buzzer_is_active = false;
static inline void _em_interval_stop() { static inline void _em_interval_stop() {
emscripten_clear_interval(_em_interval_id); emscripten_clear_interval(_em_interval_id);
@ -47,15 +52,28 @@ 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);
}
void watch_buzzer_play_sequence_with_volume(int8_t *note_sequence, void (*callback_on_end)(void), watch_buzzer_volume_t volume) {
watch_buzzer_abort_sequence();
// prepare buzzer
watch_enable_buzzer();
watch_set_buzzer_off(); watch_set_buzzer_off();
_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
watch_enable_buzzer();
// 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,32 +106,115 @@ 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
_tone_ticks = _sequence[_seq_position + 1]; _tone_ticks = _sequence[_seq_position + 1] - 1;
_seq_position += 2; _seq_position += 2;
} 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_play_raw_source(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end) {
watch_buzzer_play_raw_source_with_volume(raw_source, userdata, callback_on_end, WATCH_BUZZER_VOLUME_LOUD);
}
void watch_buzzer_play_raw_source_with_volume(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end, watch_buzzer_volume_t volume) {
watch_buzzer_abort_sequence();
// prepare buzzer
watch_enable_buzzer();
watch_set_buzzer_off();
_buzzer_is_active = true;
if (_cb_start_global) {
_cb_start_global();
}
_raw_source = raw_source;
_userdata = userdata;
_cb_finished = callback_on_end;
_volume = volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25;
_seq_position = 0;
_tone_ticks = 0;
// initiate 64 hz callback
_em_interval_id = emscripten_set_interval(cb_watch_buzzer_raw_source, (double)(1000/64), (void *)NULL);
}
void cb_watch_buzzer_raw_source(void *userData) {
// callback for reading the note sequence
(void) userData;
uint16_t period;
uint16_t duration;
bool done;
if (_tone_ticks == 0) {
done = _raw_source(_seq_position, _userdata, &period, &duration);
if (done || duration == 0) {
// end the sequence
watch_buzzer_abort_sequence();
} else {
if (period == WATCH_BUZZER_PERIOD_REST) {
watch_set_buzzer_off();
} else {
watch_set_buzzer_period_and_duty_cycle(period, _volume);
watch_set_buzzer_on();
}
// set duration ticks and move to next tone
_tone_ticks = duration - 1;
_seq_position += 1;
}
} else {
_tone_ticks--;
}
}
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];
EM_ASM({ EM_ASM({
Module['audioContext'] = new (window.AudioContext || window.webkitAudioContext)(); // "It's recommended to create one AudioContext and reuse it instead of initializing a new one each time."
// https://developer.mozilla.org/en-US/docs/Web/API/AudioContext
if (!Module['audioContext']) {
Module['audioContext'] = new (window.AudioContext || window.webkitAudioContext)();
}
}); });
} }
@ -126,13 +227,6 @@ void watch_set_buzzer_period_and_duty_cycle(uint32_t period, uint8_t duty_cycle)
void watch_disable_buzzer(void) { void watch_disable_buzzer(void) {
buzzer_enabled = false; buzzer_enabled = false;
buzzer_period = NotePeriods[BUZZER_NOTE_A4]; buzzer_period = NotePeriods[BUZZER_NOTE_A4];
EM_ASM({
if (Module['audioContext']) {
Module['audioContext'].close();
Module['audioContext'] = null;
}
});
} }
void watch_set_buzzer_on(void) { void watch_set_buzzer_on(void) {
@ -175,15 +269,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) {}