Skip to content

Instantly share code, notes, and snippets.

@rlcamp
Last active November 19, 2023 15:04
Show Gist options
  • Save rlcamp/b9f95a114fe3e3e24a420cb03b9a81a5 to your computer and use it in GitHub Desktop.
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
#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
#ifdef __cplusplus
extern "C"
#endif
int one_interval_has_elapsed(const unsigned long interval_milliseconds);
#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