From 5ff4a88374986f3e74dd17f2c17b8532c88debb1 Mon Sep 17 00:00:00 2001 From: Joey Castillo Date: Sat, 18 Sep 2021 12:57:21 -0400 Subject: add a new deep sleep mode alongside backup mode --- apps/Sensor Watch Starter Project/app.c | 12 +++--- watch-library/main.c | 3 ++ watch-library/watch/watch_deepsleep.c | 76 ++++++++++++++++++++++++++++----- watch-library/watch/watch_deepsleep.h | 65 +++++++++++++++++++++------- watch-library/watch/watch_i2c.c | 5 +++ watch-library/watch/watch_i2c.h | 4 ++ watch-library/watch/watch_private.c | 9 ++-- 7 files changed, 137 insertions(+), 37 deletions(-) diff --git a/apps/Sensor Watch Starter Project/app.c b/apps/Sensor Watch Starter Project/app.c index 82ee26d5..d2ddd6a6 100644 --- a/apps/Sensor Watch Starter Project/app.c +++ b/apps/Sensor Watch Starter Project/app.c @@ -61,6 +61,9 @@ void app_wake_from_deep_sleep() { application_state.mode = (ApplicationMode)watch_get_backup_data(0); application_state.color = (LightColor)watch_get_backup_data(1); application_state.wake_count = (uint8_t)watch_get_backup_data(2) + 1; + + // wait a moment for the user's finger to be off the button + delay_ms(250); } /** @@ -160,10 +163,10 @@ bool app_loop() { watch_set_led_off(); // wait a moment for the user's finger to be off the button - delay_ms(1000); + delay_ms(250); // nap time :) - watch_enter_deep_sleep(); + watch_enter_deep_sleep(NULL); } return true; @@ -188,8 +191,5 @@ void cb_mode_pressed() { } void cb_alarm_pressed() { - // boo: http://ww1.microchip.com/downloads/en/DeviceDoc/SAM_L22_Family_Errata_DS80000782B.pdf - // Reference 15010. doesn't say it applies to PA02 but it seems it does? - // anyway can't deep sleep now :( - // application_state.enter_deep_sleep = true; + application_state.enter_deep_sleep = true; } diff --git a/watch-library/main.c b/watch-library/main.c index c9f24150..db602a34 100755 --- a/watch-library/main.c +++ b/watch-library/main.c @@ -47,6 +47,9 @@ int main(void) { } watch_disable_digital_input(VBUS_DET); + // initialize the delay driver before any user code is called. + delay_driver_init(); + // User code. Give the app a chance to initialize its data structures and state. app_init(); diff --git a/watch-library/watch/watch_deepsleep.c b/watch-library/watch/watch_deepsleep.c index ecc10b2b..e5494e79 100644 --- a/watch-library/watch/watch_deepsleep.c +++ b/watch-library/watch/watch_deepsleep.c @@ -103,21 +103,75 @@ uint32_t watch_get_backup_data(uint8_t reg) { return 0; } -void watch_enter_deep_sleep() { - // enable and configure the external wake interrupt, if not already set up. - if (btn_alarm_callback == NULL && a2_callback == NULL && a4_callback == NULL) { - gpio_set_pin_direction(BTN_ALARM, GPIO_DIRECTION_IN); - gpio_set_pin_pull_mode(BTN_ALARM, GPIO_PULL_DOWN); - gpio_set_pin_function(BTN_ALARM, PINMUX_PA02G_RTC_IN2); - _extwake_register_callback(&CALENDAR_0.device, extwake_callback); +void _watch_disable_all_pins_except_rtc() { + uint32_t config = RTC->MODE0.TAMPCTRL.reg; + uint32_t portb_pins_to_disable = 0xFFFFFFFF; + + // if there's an action set on RTC/IN[0], leave PB00 configured + if (config & RTC_TAMPCTRL_IN0ACT_Msk) portb_pins_to_disable &= 0xFFFFFFFE; + // same with RTC/IN[1] and PB02 + if (config & RTC_TAMPCTRL_IN1ACT_Msk) portb_pins_to_disable &= 0xFFFFFFFB; + + // port A: always keep PA02 configured as-is; that's our ALARM button. + gpio_set_port_direction(0, 0xFFFFFFFB, GPIO_DIRECTION_OFF); + // port B: disable all pins we didn't save above. + gpio_set_port_direction(1, portb_pins_to_disable, GPIO_DIRECTION_OFF); +} + +void _watch_disable_all_peripherals_except_slcd() { + _watch_disable_tcc(); + watch_disable_adc(); + watch_disable_external_interrupts(); + watch_disable_i2c(); + // TODO: replace this with a proper function when we remove the debug UART + SERCOM3->USART.CTRLA.reg &= ~SERCOM_USART_CTRLA_ENABLE; + MCLK->APBCMASK.reg &= ~MCLK_APBCMASK_SERCOM3; +} + +void watch_enter_deep_sleep(char *message) { + // configure the ALARM interrupt (the callback doesn't matter) + watch_register_extwake_callback(BTN_ALARM, NULL, true); + + if (message != NULL) { + watch_display_string(" ", 0); + watch_display_string(message, 0); + } else { + slcd_sync_deinit(&SEGMENT_LCD_0); + hri_mclk_clear_APBCMASK_SLCD_bit(SLCD); } - // disable SLCD + // disable all other peripherals + _watch_disable_all_peripherals_except_slcd(); + + // disable tick interrupt + watch_register_tick_callback(NULL); + + // disable brownout detector interrupt, which could inadvertently wake us up. + SUPC->INTENCLR.bit.BOD33DET = 1; + + // disable all pins + _watch_disable_all_pins_except_rtc(); + + // turn off RAM completely. + PM->STDBYCFG.bit.BBIASHS = 3; + + // enter standby (4); we basically hang out here until an interrupt forces us to reset. + sleep(4); + + NVIC_SystemReset(); +} + +void watch_enter_backup_mode() { + // this will not work on the current silicon revision, but I said in the documentation that we do it. + // so let's do it! + watch_register_extwake_callback(BTN_ALARM, NULL, true); + + watch_register_tick_callback(NULL); + _watch_disable_all_peripherals_except_slcd(); slcd_sync_deinit(&SEGMENT_LCD_0); hri_mclk_clear_APBCMASK_SLCD_bit(SLCD); + _watch_disable_all_pins_except_rtc(); - // TODO: disable other peripherals - - // go into backup sleep mode + // go into backup sleep mode (5). when we exit, the reset controller will take over. sleep(5); } diff --git a/watch-library/watch/watch_deepsleep.h b/watch-library/watch/watch_deepsleep.h index 14fe8115..3dadd663 100644 --- a/watch-library/watch/watch_deepsleep.h +++ b/watch-library/watch/watch_deepsleep.h @@ -33,7 +33,9 @@ * from deep sleep (aka BACKUP) mode. * @param pin Either pin BTN_ALARM, A2, or A4. These are the three external wake pins. If the pin * is BTN_ALARM, this function also enables an internal pull down on that pin. - * @param callback The callback to be called if this pin triggers outside of deep sleep mode. + * @param callback The callback to be called if this pin triggers outside of deep sleep mode. If + * this is NULL, no callback will be called even in normal mode, but the interrupt + * will still be enabled so that it can wake from deep sleep or backup mode. * @param level The level you wish to scan for: true for rising, false for falling. Note that you * cannot scan for both rising and falling edges like you can with the external interrupt * pins; with the external wake interrupt, you can only get one or the other. @@ -43,40 +45,73 @@ * WILL NOT be called, as the device is basically waking from reset at that point. * @warning As of the current SAM L22 silicon revision (rev B), the BTN_ALARM pin cannot wake the * device from BACKUP mode. You can still use this function to register a BTN_ALARM interrupt - * in normal or STANDBY mode, but to wake from BACKUP, you will need to use pin A2 or A4. + * in normal or deep sleep mode, but to wake from BACKUP, you will need to use pin A2 or A4. */ void watch_register_extwake_callback(uint8_t pin, ext_irq_cb_t callback, bool level); -/** @brief Stores data in one of the RTC's backup registers, which retain their data in deep sleep. +/** @brief Unregisters the interrupt on one of the EXTWAKE pins. This will prevent a value change on + * one of these pins from waking the device from deep sleep or BACKUP modes. + * @param pin Either pin BTN_ALARM, A2, or A4. If the pin is BTN_ALARM, this function DOES NOT disable + * the internal pull down on that pin. + */ +void watch_disable_extwake_interrupt(uint8_t pin, ext_irq_cb_t callback, bool level); + +/** @brief Stores data in one of the RTC's backup registers, which retain their data in the deep sleep + and backup modes. * @param data An unsigned 32 bit integer with the data you wish to store. * @param reg A register from 0-7. */ void watch_store_backup_data(uint32_t data, uint8_t reg); -/** @brief Gets 32 bits of data from the RTC's backup register, which retains its data in deep sleep. +/** @brief Gets 32 bits of data from the RTC's backup register. * @param reg A register from 0-7. * @return An unsigned 32 bit integer with the from the backup register. */ uint32_t watch_get_backup_data(uint8_t reg); +/** @brief Enters a deep sleep mode by disabling RAM retention and all peripherals except the RTC and + * (optionally) the LCD. You can wake from this mode by pressing the ALARM button. + * @param message Either NULL, or a string representing a message to display while in deep sleep mode. The + * message will be displayed at position 0, so you should pad out the beginning of the string + * with spaces if you wish for the message to appear on line 2, i.e. " SLEEP". If this + * parameter is NULL, the screen will be blanked, and this function will disable the SLCD + * peripheral for additional power savings. (also note that while the message will replace any + * text on the display, this function will not clear any indicators you have set. This is by + * design, in case you wish to leave an indicator lit in sleep mode.) + * @details This deep sleep mode is not the lowest power mode available (see watch_enter_backup_mode), but + * it has the benefit of being able to wake with a press of the ALARM button, and provides an option + * for displaying a message to the user when asleep. The only way to wake from this mode is by + * pressing the ALARM button, or receiving an interrupt on pin A2 or A4 of the nine-pin connector. + * (An alarm interrupt would also work, but this has not yet been implemented.) This function enables + * the ALARM button interrupt for you, but if you wish to wake from the A2 or A4 RTC interrupt, you + * must configure them by calling watch_register_extwake_callback. Note however that your callback + * will not be called in this case. + * Power consumption in deep sleep mode varies a bit with the battery voltage and the temperature, + * but at 3 V and ~25° C you can eoughly estimate: + * * ~12µA current draw with the LCD controller on (message != NULL) + * * ~6.5µA current draw with the LCD controller off (message == NULL) + * @note With RAM powered off, your application state will be cleared as soon as you call this function, and + * when the user wakes up the watch, your app will effectively be waking from reset. Your app's @ref + * app_wake_from_deep_sleep function will be called to give your app a chance to restore any state that + * you stored using @ref watch_store_backup_data. + */ +void watch_enter_deep_sleep(char *message); + /** @brief Enters the SAM L22's lowest-power mode, BACKUP. * @details This function does some housekeeping before entering BACKUP mode. It first disables all - * peripherals except for the RTC, and disables the tick interrupt (since that would wake) - * us up from deep sleep. It also sets an external wake source on the ALARM button, if one + * peripherals except for the RTC, and disables the tick interrupt (since that would wake + * us up from deep sleep). It also sets an external wake source on the ALARM button, if one * was not already set. If you wish to wake from another source, such as one of the external * wake interrupt pins on the 9-pin connector, set that up prior to calling this function. * @note If you have a callback set for an external wake interrupt, it will be called if triggered while * in ACTIVE, IDLE or STANDBY modes, but it *will not be called* when waking from BACKUP. * Waking from backup is effectively like waking from reset, except that your @ref * app_wake_from_deep_sleep function will be called. - * @warning In initial testing, it seems like the ALARM_BTN pin (PA02 RTC/IN2) cannot wake the device - * from deep sleep mode. There is an errata note (Reference: 15010, linked) that says that - * due to a silicon bug, PB01 cannot be used as RTC/IN2. It seems though that this bug may - * also affect PA02. As a result — and I'm very bummed about this — you cannot use deep sleep - * mode unless you set up an external wake interrupt using a device on the nine-pin connector - * (i.e. an accelerometer with an interrupt pin). Otherwise your only option for waking will - * be to unscrew the watch case and press the reset button on the back of the board. - * http://ww1.microchip.com/downloads/en/DeviceDoc/SAM_L22_Family_Errata_DS80000782B.pdf + * @warning On current revisions of the SAM L22 silicon, the ALARM_BTN pin (PA02 RTC/IN2) cannot wake + * the device from deep sleep mode. There is an errata note (Reference: 15010) that says that + * due to a silicon bug, RTC/IN2 is not functional in BACKUP. As a result, you should not call + * this function unless you have a device on the nine-pin connector with an external interrupt + * on pin A2 or A4 (i.e. an accelerometer with an interrupt pin). */ -void watch_enter_deep_sleep(); +void watch_enter_backup_mode(); /// @} diff --git a/watch-library/watch/watch_i2c.c b/watch-library/watch/watch_i2c.c index bded405b..385d9d08 100644 --- a/watch-library/watch/watch_i2c.c +++ b/watch-library/watch/watch_i2c.c @@ -30,6 +30,11 @@ void watch_enable_i2c() { i2c_m_sync_enable(&I2C_0); } +void watch_disable_i2c() { + i2c_m_sync_disable(&I2C_0); + hri_mclk_clear_APBCMASK_SERCOM1_bit(MCLK); +} + void watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length) { i2c_m_sync_set_periphaddr(&I2C_0, addr, I2C_M_SEVEN); io_write(I2C_0_io, buf, length); diff --git a/watch-library/watch/watch_i2c.h b/watch-library/watch/watch_i2c.h index e944bbab..7ac05c13 100644 --- a/watch-library/watch/watch_i2c.h +++ b/watch-library/watch/watch_i2c.h @@ -33,6 +33,10 @@ */ void watch_enable_i2c(); +/** @brief Disables the I2C peripheral. + */ +void watch_disable_i2c(); + /** @brief Sends a series of values to a device on the I2C bus. * @param addr The address of the device you wish to talk to. * @param buf A series of unsigned bytes; the data you wish to transmit. diff --git a/watch-library/watch/watch_private.c b/watch-library/watch/watch_private.c index 9eb39324..d3da5426 100644 --- a/watch-library/watch/watch_private.c +++ b/watch-library/watch/watch_private.c @@ -28,6 +28,9 @@ void _watch_init() { // disable the LED pin (it may have been enabled by the bootloader) watch_disable_digital_output(RED); + // RAM should be back-biased in STANDBY + PM->STDBYCFG.bit.BBIASHS = 1; + // Use switching regulator for lower power consumption. SUPC->VREG.bit.SEL = 1; while(!SUPC->STATUS.bit.VREGRDY); @@ -41,10 +44,9 @@ void _watch_init() { SUPC->BOD33.bit.ACTCFG = 1; // Enable sampling mode when active SUPC->BOD33.bit.RUNSTDBY = 1; // Enable sampling mode in standby SUPC->BOD33.bit.STDBYCFG = 1; // Run in standby - SUPC->BOD33.bit.RUNBKUP = 1; // Also run in backup mode + SUPC->BOD33.bit.RUNBKUP = 0; // Don't run in backup mode SUPC->BOD33.bit.PSEL = 0xB; // Check battery level every 4 seconds SUPC->BOD33.bit.LEVEL = 31; // Detect brownout at 2.5V (1.445V + level * 34mV) - SUPC->BOD33.bit.BKUPLEVEL = 31; // Detect same level in backup mode SUPC->BOD33.bit.ACTION = 0x2; // Generate an interrupt when BOD33 is triggered SUPC->BOD33.bit.HYST = 0; // Disable hysteresis while(!SUPC->STATUS.bit.B33SRDY); @@ -57,9 +59,6 @@ void _watch_init() { CALENDAR_0_init(); calendar_enable(&CALENDAR_0); - // Not sure if this belongs in every app -- is there a power impact? - delay_driver_init(); - // set up state btn_alarm_callback = NULL; a2_callback = NULL; -- cgit v1.2.3