project
Ghost Hand
Public Unreviewedby John Lauer
A small battery gadget that keeps an automatic sink faucet running while you use the sink: it learns the faucet's timeout, re-arms with the shortest blip, and stops after 10 minutes or when picked up.
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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
/*
* 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) ;
}