Last active
November 19, 2023 15:04
-
-
Save rlcamp/b9f95a114fe3e3e24a420cb03b9a81a5 to your computer and use it in GitHub Desktop.
Blink without delay, but with the one_interval_has_elapsed() function in its own .c file, and with processor-specific sleep modes
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "one_interval_has_elapsed.h" | |
#if defined(__AVR__) | |
#include <avr/power.h> | |
#if 1 | |
/* avr using timer2 and SLEEP_MODE_PWR_SAVE, uses 400-800 uA at 3.3V depending on clock speed */ | |
#include <avr/io.h> | |
#include <avr/interrupt.h> | |
#include <avr/sleep.h> | |
#include <avr/power.h> | |
static uint8_t wakes = 0; | |
/* higher values of this allow longer intervals to be specified. todo: handle automatically */ | |
#define ISR_PER_WAKE 8U | |
ISR(TIMER2_COMPA_vect) { | |
/* todo: number of interrupts per wake should be chosen automatically */ | |
static uint8_t fastwake = 0; | |
fastwake++; | |
if (ISR_PER_WAKE == fastwake) { | |
fastwake = 0; | |
wakes++; | |
} | |
} | |
int one_interval_has_elapsed(const unsigned long interval_ms) { | |
static char initted = 0; | |
if (!initted) { | |
/* begin misc low-power stuff */ | |
/* disable adc */ | |
ADCSRA = 0; | |
/* disable timer0 */ | |
TCCR0B = 0; | |
power_all_disable(); | |
/* end misc low-power stuff */ | |
power_timer2_enable(); | |
/* is this necessary? */ | |
cli(); | |
TCCR2A = 0; | |
/* this code is terrible but doing it other ways results in more sram usage, which is at more of a premium than program memory */ | |
unsigned long compare_value; | |
const unsigned long interval_clocks = interval_ms * (F_CPU / 1000); | |
if ((compare_value = (interval_clocks + (0U * ISR_PER_WAKE)) / (1U * ISR_PER_WAKE)) <= UINT8_MAX) TCCR2B = 0b001; | |
else if ((compare_value = (interval_clocks + (4U * ISR_PER_WAKE)) / (8U * ISR_PER_WAKE)) <= UINT8_MAX) TCCR2B = 0b010; | |
else if ((compare_value = (interval_clocks + (16U * ISR_PER_WAKE)) / (32U * ISR_PER_WAKE)) <= UINT8_MAX) TCCR2B = 0b011; | |
else if ((compare_value = (interval_clocks + (32U * ISR_PER_WAKE)) / (64U * ISR_PER_WAKE)) <= UINT8_MAX) TCCR2B = 0b100; | |
else if ((compare_value = (interval_clocks + (64U * ISR_PER_WAKE)) / (128U * ISR_PER_WAKE)) <= UINT8_MAX) TCCR2B = 0b101; | |
else if ((compare_value = (interval_clocks + (128U * ISR_PER_WAKE)) / (256U * ISR_PER_WAKE)) <= UINT8_MAX) TCCR2B = 0b110; | |
else if ((compare_value = (interval_clocks + (512U * ISR_PER_WAKE)) / (1024U * ISR_PER_WAKE)) <= UINT8_MAX) TCCR2B = 0b111; | |
else return 1; /* crap */ | |
/* ctc mode */ | |
TCCR2A |= (1 << WGM21); | |
OCR2A = (uint8_t)compare_value; | |
TIFR2 = (1 << TOV2) | (1 << OCF2A) | (1 << OCF2B); | |
TIMSK2 = (1 << OCIE2A); | |
sei(); | |
initted = 1; | |
} | |
static uint8_t wakes_acknowledged = 0; | |
/* sleep until next interrupt */ | |
set_sleep_mode(SLEEP_MODE_PWR_SAVE); | |
cli(); | |
while (*(volatile uint8_t *)&wakes == wakes_acknowledged) { | |
sleep_enable(); | |
sei(); | |
sleep_cpu(); | |
sleep_disable(); | |
sei(); | |
cli(); | |
} | |
sei(); | |
wakes_acknowledged++; | |
return 1; | |
} | |
#else | |
/* avr using wdt, consumes around 4.5 uA at 3.3V */ | |
#include <avr/sleep.h> | |
#include <avr/wdt.h> | |
#include <avr/interrupt.h> | |
static uint8_t wakes = 0; | |
ISR(WDT_vect) { | |
wakes++; | |
} | |
int one_interval_has_elapsed(const unsigned long interval_ms) { | |
static char initted = 0; | |
if (!initted) { | |
MCUSR = 0; | |
const uint8_t wdtcsr_new_value = (1U << WDIE) | (interval_ms <= 24 ? 0 : | |
interval_ms <= 48 ? (1U << WDP0) : | |
interval_ms <= 96 ? (1U << WDP1) : | |
interval_ms <= 188 ? (1U << WDP1) | (1U << WDP0) : | |
interval_ms <= 375 ? (1U << WDP2) : | |
interval_ms <= 750 ? (1U << WDP2) | (1U << WDP0) : | |
interval_ms <= 1500 ? (1U << WDP2) | (1U << WDP1) : | |
interval_ms <= 3000 ? (1U << WDP2) | (1U << WDP1) | (1U << WDP0) : | |
interval_ms <= 6000 ? (1U << WDP3) : | |
(1U << WDP3) | (1U << WDP0) | |
); | |
/* timed sequence, must have the new value precomputed */ | |
WDTCSR = (1U << WDCE) | (1U << WDE); | |
WDTCSR = wdtcsr_new_value; | |
/* is this necessary */ | |
wdt_reset(); | |
} | |
static uint8_t wakes_acknowledged = 0; | |
cli(); | |
if (*(volatile uint8_t *)&wakes == wakes_acknowledged) { | |
/* timed sequence */ | |
set_sleep_mode (SLEEP_MODE_PWR_DOWN); | |
sleep_enable(); | |
/* turn off brown-out enable in software */ | |
MCUCR = (1U << BODS) | (1U << BODSE); | |
MCUCR = (1U << BODS); | |
sei(); | |
sleep_cpu(); | |
sleep_disable(); | |
cli(); | |
} | |
sei(); | |
wakes_acknowledged++; | |
return 1; | |
} | |
/* end avr wdt variant */ | |
#endif | |
/* end avr */ | |
#elif defined(__SAMD51__) | |
/* samd51, tested on feather m4 express, uses about 800 uA at 3.75V into Vbat */ | |
#if __has_include(<samd51.h>) | |
/* as invoked by Makefile, regardless of cmsis-atmel version */ | |
#include <samd51.h> | |
#else | |
#include <samd51/include/samd51.h> | |
#endif | |
static unsigned int wakes = 0; | |
void RTC_Handler(void) { | |
if (!RTC->MODE0.INTFLAG.bit.CMP0) return; | |
RTC->MODE0.INTFLAG.reg = RTC_MODE0_INTFLAG_CMP0; | |
wakes++; | |
__DSB(); | |
} | |
int one_interval_has_elapsed(const unsigned long interval_ms) { | |
static char initted = 0; | |
if (!initted) { | |
initted = 1; | |
/* begin misc low-power stuff */ | |
USB->DEVICE.CTRLA.bit.ENABLE = 0; | |
PM->STDBYCFG.bit.FASTWKUP = 0x0; | |
CoreDebug->DEMCR &= ~CoreDebug_DEMCR_TRCENA_Msk; | |
DWT->CTRL &= ~DWT_CTRL_CYCCNTENA_Msk; | |
/* end misc low-power stuff */ | |
PM->SLEEPCFG.bit.SLEEPMODE = PM_SLEEPCFG_SLEEPMODE_STANDBY_Val; | |
#ifdef CRYSTALLESS | |
OSC32KCTRL->OSCULP32K.bit.EN1K = 1; | |
OSC32KCTRL->RTCCTRL.reg = (OSC32KCTRL_RTCCTRL_Type) { .bit.RTCSEL = OSC32KCTRL_RTCCTRL_RTCSEL_ULP1K_Val }.reg; | |
#else | |
/* depends on PR #278 to avoid dependency on how the board was powered up / reset */ | |
OSC32KCTRL->XOSC32K.reg |= (OSC32KCTRL_XOSC32K_Type) { .bit.EN1K = 1, .bit.RUNSTDBY = 1 }.reg; | |
while (!OSC32KCTRL->STATUS.bit.XOSC32KRDY); | |
OSC32KCTRL->RTCCTRL.reg = (OSC32KCTRL_RTCCTRL_Type) { .bit.RTCSEL = OSC32KCTRL_RTCCTRL_RTCSEL_XOSC1K_Val }.reg; | |
#endif | |
RTC->MODE0.CTRLA.bit.ENABLE = 0; | |
while (RTC->MODE0.SYNCBUSY.bit.ENABLE); | |
RTC->MODE0.CTRLA.bit.SWRST = 1; | |
while (RTC->MODE0.SYNCBUSY.bit.SWRST); | |
RTC->MODE0.CTRLA.reg = (RTC_MODE0_CTRLA_Type) { .bit = { | |
.MODE = 0, | |
.MATCHCLR = 1, | |
.PRESCALER = RTC_MODE0_CTRLA_PRESCALER_DIV16_Val, | |
}}.reg; | |
while (RTC->MODE0.SYNCBUSY.reg); | |
const unsigned long period_ticks_minus_one = (interval_ms * 64 + 500) / 1000 - 1; | |
RTC->MODE0.COMP[0].reg = period_ticks_minus_one; | |
while (RTC->MODE0.SYNCBUSY.bit.COMP0); | |
RTC->MODE0.INTENSET.bit.CMP0 = 1; | |
NVIC_SetPriority(RTC_IRQn, 0x00); | |
NVIC_EnableIRQ(RTC_IRQn); | |
RTC->MODE0.CTRLA.bit.ENABLE = 1; | |
while (RTC->MODE0.SYNCBUSY.bit.ENABLE); | |
} | |
static unsigned int wakes_acknowledged = 0; | |
while (*(volatile unsigned int *)&wakes == wakes_acknowledged) { | |
__DSB(); | |
__WFE(); | |
} | |
wakes_acknowledged++; | |
return 1; | |
} | |
/* end samd51 */ | |
#elif defined(__SAMD21G18A__) || defined(__SAMD21E18A__) | |
/* samd21, tested on feather m0 and trinket m0, uses about 0.55 mA at 3.3V on the former */ | |
#if __has_include(<samd21.h>) | |
#include <samd21.h> | |
#else | |
#include <samd21/include/samd21.h> | |
#endif | |
static unsigned int wakes = 0; | |
void RTC_Handler(void) { | |
if (!RTC->MODE0.INTFLAG.bit.CMP0) return; | |
RTC->MODE0.INTFLAG.reg = (RTC_MODE0_INTFLAG_Type) { .bit.CMP0 = 1}.reg; | |
wakes++; | |
__DSB(); | |
} | |
/* copied verbatim from the workaround for chip errata 1.5.8, except that function is placed | |
in .datafunc, which results in it being collapsed into .data* by the linker script without | |
changing the attributes of .data itself, and long_call is used to prevent veneers */ | |
__attribute__((noinline, long_call, section(".datafunc"))) static void dsb_wfi_in_ram(void) { | |
__DSB(); | |
__WFI(); | |
// The following sequence ensures that the flash is ready before returning from the RAM code | |
#define CACHE_SIZE_IN_BYTES 64 | |
((volatile unsigned int *)FLASH_ADDR)[CACHE_SIZE_IN_BYTES / sizeof(unsigned int)]; //will read a word at FLASH_ADDR + 0x40 in this case | |
((volatile unsigned int *)FLASH_ADDR)[(CACHE_SIZE_IN_BYTES * 2) / sizeof(unsigned int)]; //will read a word at FLASH_ADDR + 0x80 in this case | |
} | |
int one_interval_has_elapsed(const unsigned long interval_ms) { | |
static char initted = 0; | |
if (!initted) { | |
initted = 1; | |
/* begin misc low-power stuff */ | |
USB->DEVICE.CTRLA.bit.ENABLE = 0; | |
/* end misc low-power stuff */ | |
PM->APBAMASK.bit.RTC_ = 1; | |
PM->APBBMASK.bit.PORT_ = 1; | |
#ifndef CRYSTALLESS | |
SYSCTRL->XOSC32K.reg = (SYSCTRL_XOSC32K_Type) { .bit = { | |
.RUNSTDBY = 1, | |
.EN32K = 1, | |
.XTALEN = 1, | |
.STARTUP = 6, | |
}}.reg; | |
SYSCTRL->XOSC32K.bit.ENABLE = 1; | |
while (!SYSCTRL->PCLKSR.bit.XOSC32KRDY); | |
#endif | |
GCLK->GENDIV.reg = (GCLK_GENDIV_Type) { .bit = { | |
.ID = 2, | |
.DIV = 4 /* with divsel = 1, this divides by 32 */ | |
}}.reg; | |
while (GCLK->STATUS.bit.SYNCBUSY); | |
GCLK->GENCTRL.reg = (GCLK_GENCTRL_Type) { .bit = { | |
.GENEN = 1, | |
#ifdef CRYSTALLESS | |
.SRC = GCLK_GENCTRL_SRC_OSCULP32K_Val, | |
#else | |
.SRC = GCLK_GENCTRL_SRC_XOSC32K_Val, | |
#endif | |
.ID = 2, | |
.DIVSEL = 1, | |
}}.reg; | |
while (GCLK->STATUS.bit.SYNCBUSY); | |
GCLK->CLKCTRL.reg = (GCLK_CLKCTRL_Type) { .bit = { | |
.CLKEN = 1, | |
.GEN = GCLK_CLKCTRL_GEN_GCLK2_Val, | |
.ID = GCLK_CLKCTRL_ID_RTC_Val | |
}}.reg; | |
while (GCLK->STATUS.bit.SYNCBUSY); | |
RTC->MODE0.CTRL.bit.ENABLE = 0; | |
while (RTC->MODE0.STATUS.bit.SYNCBUSY); | |
RTC->MODE0.CTRL.bit.SWRST = 1; | |
while (RTC->MODE0.STATUS.bit.SYNCBUSY); | |
RTC->MODE0.CTRL.reg = (RTC_MODE0_CTRL_Type) { .bit = { | |
.MODE = 0, | |
.MATCHCLR = 1, | |
.PRESCALER = RTC_MODE0_CTRL_PRESCALER_DIV16_Val, | |
}}.reg; | |
while (RTC->MODE0.STATUS.bit.SYNCBUSY); | |
const unsigned long period_ticks_minus_one = (interval_ms * 64 + 500) / 1000 - 1; | |
RTC->MODE0.COMP[0].reg = period_ticks_minus_one; | |
while (RTC->MODE0.STATUS.bit.SYNCBUSY); | |
RTC->MODE0.INTENSET.bit.CMP0 = 1; | |
NVIC_SetPriority(RTC_IRQn, 0x00); | |
NVIC_EnableIRQ(RTC_IRQn); | |
RTC->MODE0.CTRL.bit.ENABLE = 1; | |
while (RTC->MODE0.STATUS.bit.SYNCBUSY); | |
/* need this per chip errata 1.5.7 */ | |
SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk; | |
SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk; | |
} | |
static unsigned int wakes_acknowledged = 0; | |
/* samd21 implements wfe as nop so we have to do this garbage */ | |
__disable_irq(); | |
while (*(volatile unsigned int *)&wakes == wakes_acknowledged) { | |
/* need this indirection to ram per chip errata 1.5.8 */ | |
dsb_wfi_in_ram(); | |
__enable_irq(); | |
__disable_irq(); | |
} | |
__enable_irq(); | |
wakes_acknowledged++; | |
return 1; | |
} | |
/* end samd21 */ | |
#else | |
/* generic arduino code path, with sleep on arm */ | |
#include <Arduino.h> | |
int one_interval_has_elapsed(const unsigned long interval_ms) { | |
/* this logic puts the main thread in a low power state until it has been exactly one interval since the previous wakeup */ | |
static unsigned long prev = 0; | |
if (millis() - prev < interval_ms) { | |
#ifdef __arm__ | |
/* sleep until the next interrupt (which will probably be systick) */ | |
__DSB(); | |
__WFE(); | |
#endif | |
return 0; | |
} | |
prev += interval_ms; | |
return 1; | |
} | |
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#ifdef __cplusplus | |
extern "C" | |
#endif | |
int one_interval_has_elapsed(const unsigned long interval_milliseconds); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "one_interval_has_elapsed.h" | |
void setup() { | |
pinMode(LED_BUILTIN, OUTPUT); | |
} | |
void loop() { | |
/* only evaluate the rest of the main loop if it has been this many milliseconds since the previous time */ | |
if (!one_interval_has_elapsed(1000)) return; | |
static char state = 0; | |
digitalWrite(LED_BUILTIN, state); | |
state = !state; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment