main
John Lauer Rev 2: two IR emitters (850 nm at 8.2 ohm, 940 nm at 4.7 ohm), battery 40 / 74 / 170 days, new resistor pages 795559a 1d ago
/*
 * power.c : clocks, RTC time base, sleep, pins, battery, ADC, watchdog, EEPROM
 *
 * Power states (currents from calcs.json battery_life, typical, 25 C):
 *   SLEEP    power-down, PIT off, wake on PA2 (accel INT1) or PB2 (VBUS) edges.
 *            MCU ~0.1 uA, LIS2DW12 LP1 12.5 Hz wake-up ~1 uA, total with
 *            LDO, charger leakage, the always-on 9.4 Mohm divider (0.42 uA)
 *            and pack protection ~4.7 uA.
 *   AWAKE    (settle, charging display) STANDBY between 64 Hz PIT ticks,
 *            OSC20M off in standby, accel 50 Hz.
 *   SESSION  STANDBY with OSC20M kept on by the TCBs (RUNSTDBY), receiver
 *            powered (1.1 mA), ~2.9 mA total average incl. echo pulses (rev 2:
 *            850 nm at 8.2 ohm, one 940 nm at 4.7 ohm, 0.41 mA) and
 *            the breathing status dot. IDLE only during the ~0.2 ms blanking
 *            after each echo (TCA0 does not run in standby).
 */
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/cpufunc.h>
#include <util/atomic.h>
#include <util/delay.h>
#include "board.h"
#include "power.h"

/* ------------------------------------------------------------------ clock */
void clock_init(void)
{
    _PROTECTED_WRITE(CLKCTRL.MCLKCTRLA, CLKCTRL_CLKSEL_OSC20M_gc);
    _PROTECTED_WRITE(CLKCTRL.MCLKCTRLB, CLKCTRL_PDIV_4X_gc | CLKCTRL_PEN_bm);   /* 5 MHz */
}

/* 10 MHz is inside the 0-10 MHz @ 2.7-5.5 V speed grade (datasheet 1.0
 * "Speed Grades"); 20 MHz would need 4.5 V. The prescaler switch is
 * immediate and glitch free. */
void clock_fast(void)   { _PROTECTED_WRITE(CLKCTRL.MCLKCTRLB, CLKCTRL_PDIV_2X_gc | CLKCTRL_PEN_bm); }
void clock_normal(void) { _PROTECTED_WRITE(CLKCTRL.MCLKCTRLB, CLKCTRL_PDIV_4X_gc | CLKCTRL_PEN_bm); }

void delay_ms_busy(uint16_t ms)
{
    while (ms--) _delay_ms(1);
}

/* -------------------------------------------------------------- time base */
static volatile uint16_t rtc_hi;
static volatile uint8_t pit_ticks;

void time_init(void)
{
    while (RTC.STATUS) ;
    RTC.CLKSEL = RTC_CLKSEL_INT32K_gc;         /* OSCULP32K */
    RTC.PER = 0xFFFF;
    RTC.INTCTRL = RTC_OVF_bm;
    RTC.CTRLA = RTC_PRESCALER_DIV32_gc | RTC_RUNSTDBY_bm | RTC_RTCEN_bm;   /* 1024 Hz */
    time_resume();
}

void time_resume(void)
{
    while (RTC.PITSTATUS) ;
    RTC.PITINTCTRL = RTC_PI_bm;
    RTC.PITCTRLA = RTC_PERIOD_CYC512_gc | RTC_PITEN_bm;   /* 32768 / 512 = 64 Hz */
}

void time_suspend(void)
{
    while (RTC.PITSTATUS) ;
    RTC.PITCTRLA = 0;                          /* the PIT would wake power-down */
}

uint32_t time_ticks(void)
{
    uint16_t hi, lo;
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
        hi = rtc_hi;
        lo = RTC.CNT;
        if ((RTC.INTFLAGS & RTC_OVF_bm) && lo < 0x8000u) hi++;   /* overflow not yet serviced */
    }
    return ((uint32_t)hi << 16) | lo;
}

uint32_t time_ms(void)
{
    return (time_ticks() * 125u) >> 7;         /* * 1000 / 1024, fine for 9 h */
}

bool tick_take(void)
{
    bool t = false;
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
        if (pit_ticks) { pit_ticks--; t = true; }
    }
    return t;
}

bool tick_pending(void) { return pit_ticks != 0; }

ISR(RTC_CNT_vect)
{
    RTC.INTFLAGS = RTC_OVF_bm;
    rtc_hi++;
}

ISR(RTC_PIT_vect)
{
    RTC.PITINTFLAGS = RTC_PI_bm;
    if (pit_ticks < 8) pit_ticks++;
}

/* ------------------------------------------------------------------ sleep */
void sleep_now(sleep_mode_t m)
{
    uint8_t s = m == SLEEP_POWERDOWN_MODE ? SLPCTRL_SMODE_PDOWN_gc
              : m == SLEEP_STANDBY_MODE ? SLPCTRL_SMODE_STDBY_gc : SLPCTRL_SMODE_IDLE_gc;
    SLPCTRL.CTRLA = s | SLPCTRL_SEN_bm;
    sei();                                     /* sei; sleep: no interrupt slips in between */
    __asm__ __volatile__("sleep");
    SLPCTRL.CTRLA = 0;
}

/* ------------------------------------------------------------------- pins */
static volatile bool f_accel, f_vbus;

void pins_init(void)
{
    /* PORTA: PA1 CHG_STAT in, PA2 ACC_INT1 in, PA3/PA5 gates (inputs + 100k
     * pull-downs until armed), PA4 RX_PWR out low, PA6/PA7 analog */
    PORTA.OUTCLR = (1 << PIN_RX_PWR) | (1 << PIN_IR850_G) | (1 << PIN_IR940_G);
    PORTA.DIR = (1 << PIN_RX_PWR);
    PORTA.PIN1CTRL = 0;
    PORTA.PIN6CTRL = PORT_ISC_INPUT_DISABLE_gc;   /* VBAT_SENSE is analog (<= 2.1 V) */
    PORTA.PIN7CTRL = PORT_ISC_INPUT_DISABLE_gc;   /* RX_SIG analog */

    /* PORTB: PB4 high (LED rail off), PB5 low (no back-powering), PB3 unused
     * (unconnected since the divider became always-on) with its pull-up */
    PORTB.OUT = (1 << PIN_LED_EN_N);
    PORTB.DIR = (1 << PIN_LED_EN_N) | (1 << PIN_LED_DIN);
    PORTB.PIN3CTRL = PORT_PULLUPEN_bm;

    /* PORTC: PC2 EVOUT2 out low, PC1/PC3 unused with pull-ups, PC0 ACC_INT2 in */
    PORTC.OUT = 0;
    PORTC.DIR = (1 << PIN_ECHO_EVT);
    PORTC.PIN1CTRL = PORT_PULLUPEN_bm;
    PORTC.PIN3CTRL = PORT_PULLUPEN_bm;
    PORTC.PIN0CTRL = 0;
}

/* PA2 and PB2 are "fully asynchronous" pins (Px2/Px6): edge sensing works in
 * power-down without a clock. The other pins could only wake on both edges or
 * level, so these two nets are on the right pins. */
void wake_arm(void)
{
    PORTA.PIN2CTRL = PORT_ISC_RISING_gc;
    PORTB.PIN2CTRL = PORT_ISC_BOTHEDGES_gc;
}

bool wake_take_accel(void)
{
    bool f;
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { f = f_accel; f_accel = false; }
    return f;
}

bool wake_take_vbus(void)
{
    bool f;
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { f = f_vbus; f_vbus = false; }
    return f;
}

ISR(PORTA_PORT_vect)
{
    uint8_t f = PORTA.INTFLAGS;
    PORTA.INTFLAGS = f;
    if (f & (1 << PIN_ACC_INT1)) f_accel = true;
}

ISR(PORTB_PORT_vect)
{
    uint8_t f = PORTB.INTFLAGS;
    PORTB.INTFLAGS = f;
    if (f & (1 << PIN_VBUS_SENSE)) f_vbus = true;
}

/* -------------------------------------------------------- battery, charger */
bool vbus_present(void)    { return PORTB.IN & (1 << PIN_VBUS_SENSE); }
bool charge_complete(void) { return vbus_present() && (PORTA.IN & (1 << PIN_CHG_STAT)); }

/* VBAT_SENSE is VBAT / 2 from an always-on 4.7M / 4.7M divider (0.45 uA),
 * never above 2.1 V, with C6 = 100 nF on the tap. The source impedance is
 * 2.35 Mohm (tau 235 ms), far too high to charge the ADC sample capacitor
 * through, so C6 is what the ADC samples: one sample takes a few pF from
 * 100 nF (< 0.1 mV) and C6 recovers between readings. The reading uses the
 * longest sample time (SAMPCTRL = 31), 16 accumulated conversions, and the
 * average of 4 such reads. Pin leakage flows through 2.35 Mohm too: 1 nA
 * typical is 2.4 mV at the pin (4.7 mV at VBAT), small next to the 3.45 V
 * stop threshold, but worth a one-point check against a meter per board. */
uint16_t vbat_mv(void)
{
    uint32_t acc = 0;
    for (uint8_t i = 0; i < 4; i++) acc += adc_read_mv(ADC_MUXPOS_AIN6_gc, true, 4, 31);
    return (uint16_t)((acc * 2u + 2u) / 4u);
}

/* resting LiPo curve (approximate, 1S), mV -> % */
static const uint16_t curve_mv[] = { 3450, 3620, 3700, 3750, 3790, 3820, 3870, 3920, 4000, 4100, 4200 };
static const uint8_t  curve_pc[] = {    0,    7,   16,   26,   37,   47,   58,   68,   79,   90,  100 };

uint8_t vbat_percent(uint16_t mv)
{
    if (mv <= curve_mv[0]) return 0;
    for (uint8_t i = 1; i < sizeof curve_mv / sizeof curve_mv[0]; i++) {
        if (mv <= curve_mv[i]) {
            uint16_t d = curve_mv[i] - curve_mv[i - 1];
            return (uint8_t)(curve_pc[i - 1] +
                             ((uint32_t)(mv - curve_mv[i - 1]) * (curve_pc[i] - curve_pc[i - 1]) + d / 2) / d);
        }
    }
    return 100;
}

/* -------------------------------------------------------------------- ADC */
uint16_t adc_read_mv(uint8_t muxpos, bool ref_2v5, uint8_t nsamp_log2, uint8_t sampctrl)
{
    VREF.CTRLA = (uint8_t)((VREF.CTRLA & ~VREF_ADC0REFSEL_gm) |
                           (ref_2v5 ? VREF_ADC0REFSEL_2V5_gc : VREF_ADC0REFSEL_1V1_gc));
    ADC0.CTRLA = 0;
    ADC0.CTRLB = nsamp_log2;                                 /* SAMPNUM: 2^n accumulated */
    ADC0.CTRLC = ADC_SAMPCAP_bm | ADC_REFSEL_INTREF_gc | ADC_PRESC_DIV8_gc;   /* 625 kHz */
    ADC0.CTRLD = ADC_INITDLY_DLY32_gc;                       /* reference settle */
    ADC0.SAMPCTRL = sampctrl;                                /* extra sample cycles (0..31) */
    ADC0.MUXPOS = muxpos;
    ADC0.INTFLAGS = ADC_RESRDY_bm;
    ADC0.CTRLA = ADC_ENABLE_bm;
    ADC0.COMMAND = ADC_STCONV_bm;
    while (!(ADC0.INTFLAGS & ADC_RESRDY_bm)) ;
    uint16_t r = ADC0.RES >> nsamp_log2;
    ADC0.CTRLA = 0;
    return (uint16_t)(((uint32_t)r * (ref_2v5 ? 2500u : 1100u)) >> 10);
}

/* --------------------------------------------------------------- watchdog */
void wdt_on(void)
{
    while (WDT.STATUS & WDT_SYNCBUSY_bm) ;
    _PROTECTED_WRITE(WDT.CTRLA, WDT_PERIOD_1KCLK_gc);       /* ~1 s */
}

void wdt_off(void)
{
    while (WDT.STATUS & WDT_SYNCBUSY_bm) ;
    _PROTECTED_WRITE(WDT.CTRLA, 0);
}

void wdt_kick(void) { __builtin_avr_wdr(); }

/* ----------------------------------------------------------------- EEPROM */
void eeprom_read_bytes(uint8_t addr, void *dst, uint8_t n)
{
    const volatile uint8_t *e = (const volatile uint8_t *)(MAPPED_EEPROM_START + addr);
    uint8_t *d = (uint8_t *)dst;
    while (NVMCTRL.STATUS & NVMCTRL_EEBUSY_bm) ;
    while (n--) *d++ = *e++;
}

/* Page buffer write through the memory map, then erase+write the page.
 * The caller keeps the block inside one 32-byte page. */
void eeprom_write_bytes(uint8_t addr, const void *src, uint8_t n)
{
    volatile uint8_t *e = (volatile uint8_t *)(MAPPED_EEPROM_START + addr);
    const uint8_t *s = (const uint8_t *)src;
    while (NVMCTRL.STATUS & NVMCTRL_EEBUSY_bm) ;
    _PROTECTED_WRITE_SPM(NVMCTRL.CTRLA, NVMCTRL_CMD_PAGEBUFCLR_gc);
    while (n--) *e++ = *s++;
    _PROTECTED_WRITE_SPM(NVMCTRL.CTRLA, NVMCTRL_CMD_PAGEERASEWRITE_gc);
    while (NVMCTRL.STATUS & NVMCTRL_EEBUSY_bm) ;
}