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
Studio: project overview, how it works, lights, charging, battery life, firmware source, dashboard screenshots; licenseā¦
c2eaf61
3d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
/*
* echo.c : hardware IR transponder
*
* Signal path (no CPU in the loop):
*
* RX_SIG (PA7) --> AC0 AINP0
* DAC0 (VREF 1.1 V) --> AC0 negative input (MUXNEG = DAC)
* AC0 OUT --> EVSYS ASYNCCH0 --+--> ASYNCUSER0 (TCB0 capture/start)
* +--> ASYNCUSER11 (TCB1 capture/start)
* +--> ASYNCUSER10 (EVOUT2 = PC2, ECHO_EVT probe)
* TCB0 single-shot, ASYNC = 1: WO (PA5) goes high on the event edge itself,
* TCB1 likewise on PA3. Both count CCMP cycles of CLK_PER (5 MHz) then drop.
*
* Datasheet facts this relies on (ATtiny1614/16/17 DS40002204A):
* - TCB single-shot with CTRLB.ASYNC = 1: "the output is high when the event
* arrives", counter starts 2 to 3 CLK_TCB later. Echo latency is therefore
* the comparator delay plus the event routing, well under 1 us.
* - Single-shot ignores events while counting, so our own echo (which our
* photodiode also sees) cannot retrigger during the echo. After the echo
* the event users are disconnected for ECHO_BLANK_US.
* - Writing TOP to CNT before enabling stops the single-shot firing at enable.
* - Sleep Mode Activity table: in STANDBY only RTC, ADC, TCBn (with RUNSTDBY)
* keep CLK_PER, and the wake sources include "TCBn Capture interrupt" but
* NOT the AC interrupt. So pulses are timestamped from the TCB0 CAPT
* interrupt (end of capture or end of echo), and AC0 interrupts are unused.
* - TCA0 has no RUNSTDBY on the 1-series, so the blanking timer (TCA0) only
* runs in IDLE: main.c sleeps in IDLE while echo_blanking() is true
* (about 0.2 ms per faucet pulse) and in STANDBY otherwise.
* - PA5 is also AC0 OUT: AC0.CTRLA.OUTEN must stay 0.
* - The 1-series has no "DACREF" (that is the 2-series / AVR Dx name); the
* equivalent here is DAC0 with VREF.CTRLA.DAC0REFSEL, output buffer off.
* DAC0 OUT would be PA6 (VBAT_SENSE): OUTEN stays 0 too.
*/
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/atomic.h>
#include "board.h"
#include "echo.h"
#include "power.h"
#define CH0 EVSYS_ASYNCUSER0_ASYNCCH0_gc /* 0x03 = async channel 0, same code for every user */
#define TCA_TICK_NS 3200u /* CLK_PER / 16 at 5 MHz */
#define US_TO_TCA(us) ((uint16_t)(((uint32_t)(us) * 1000u + TCA_TICK_NS - 1) / TCA_TICK_NS))
#define QLEN 16
static volatile echo_pulse_t q[QLEN];
static volatile uint8_t q_head, q_tail;
static volatile bool listening;
static volatile bool blanking;
static volatile bool held;
static uint8_t width_us = ECHO_DEFAULT_US;
static volatile uint16_t blank_ticks;
static volatile uint32_t duty_win_t;
static volatile uint16_t duty_win_us;
static void push(uint32_t t, uint16_t w)
{
uint8_t n = (uint8_t)((q_head + 1) & (QLEN - 1));
if (n == q_tail) return; /* full: drop (main loop is late) */
q[q_head].t = t;
q[q_head].width_cyc = w;
q_head = n;
}
bool echo_pop(echo_pulse_t *p)
{
bool ok = false;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
if (q_tail != q_head) {
p->t = q[q_tail].t;
p->width_cyc = q[q_tail].width_cyc;
q_tail = (uint8_t)((q_tail + 1) & (QLEN - 1));
ok = true;
}
}
return ok;
}
static void users_connect(bool on)
{
EVSYS.ASYNCUSER0 = on ? CH0 : 0; /* TCB0 */
EVSYS.ASYNCUSER11 = on ? CH0 : 0; /* TCB1 */
}
void echo_init(void)
{
/* gates low with the pins as inputs: the 100k pull-downs hold the FETs off */
PORTA.OUTCLR = (1 << PIN_IR850_G) | (1 << PIN_IR940_G) | (1 << PIN_RX_PWR);
PORTA.DIRCLR = (1 << PIN_IR850_G) | (1 << PIN_IR940_G);
PORTA.DIRSET = (1 << PIN_RX_PWR);
PORTA.PIN7CTRL = PORT_ISC_INPUT_DISABLE_gc; /* analog input */
TCB0.CTRLA = 0; TCB1.CTRLA = 0;
AC0.CTRLA = 0; DAC0.CTRLA = 0;
users_connect(false);
}
void echo_rx_on(void)
{
PORTA.OUTSET = (1 << PIN_RX_PWR); /* TLV9062 + RX_REF bias */
VREF.CTRLA = (uint8_t)((VREF.CTRLA & ~VREF_DAC0REFSEL_gm) | VREF_DAC0REFSEL_1V1_gc);
VREF.CTRLB |= VREF_DAC0REFEN_bm;
DAC0.DATA = (uint8_t)(((uint32_t)(RX_REF_MV + RX_THRESH_OVER_MV) * 256u) / 1100u);
DAC0.CTRLA = DAC_ENABLE_bm | DAC_RUNSTDBY_bm; /* OUTEN = 0: internal only */
AC0.MUXCTRLA = AC_MUXPOS_PIN0_gc | AC_MUXNEG_DAC_gc;
AC0.INTCTRL = 0;
AC0.CTRLA = AC_ENABLE_bm | AC_HYSMODE_25mV_gc | AC_RUNSTDBY_bm; /* fast mode, OUTEN = 0 */
EVSYS.ASYNCCH0 = EVSYS_ASYNCCH0_AC0_OUT_gc;
EVSYS.ASYNCUSER10 = EVSYS_ASYNCUSER10_ASYNCCH0_gc; /* EVOUT2 */
PORTC.OUTCLR = (1 << PIN_ECHO_EVT);
PORTC.DIRSET = (1 << PIN_ECHO_EVT);
PORTMUX.CTRLA |= PORTMUX_EVOUT2_bm;
held = false;
}
void echo_rx_off(void)
{
cli();
users_connect(false);
TCB0.CTRLA = 0; TCB0.INTCTRL = 0; TCB0.CTRLB = 0;
TCB1.CTRLA = 0; TCB1.CTRLB = 0;
TCA0.SINGLE.CTRLA = 0; TCA0.SINGLE.INTCTRL = 0;
blanking = false;
listening = false;
sei();
PORTA.OUTCLR = (1 << PIN_IR850_G) | (1 << PIN_IR940_G);
PORTA.DIRCLR = (1 << PIN_IR850_G) | (1 << PIN_IR940_G);
PORTMUX.CTRLA &= (uint8_t)~PORTMUX_EVOUT2_bm;
EVSYS.ASYNCUSER10 = 0;
EVSYS.ASYNCCH0 = 0;
PORTC.OUTCLR = (1 << PIN_ECHO_EVT);
AC0.CTRLA = 0;
DAC0.CTRLA = 0;
VREF.CTRLB &= (uint8_t)~VREF_DAC0REFEN_bm;
PORTA.OUTCLR = (1 << PIN_RX_PWR);
q_head = q_tail = 0;
}
/*
* Threshold from the receiver's own noise floor: 16 ADC readings of RX_SIG
* (1.1 V reference), take the 75th percentile (a stray faucet pulse in the
* sample set cannot drag it up), add 60 mV, clamp.
*/
uint16_t echo_calibrate(void)
{
uint16_t v[16];
for (uint8_t i = 0; i < 16; i++) {
v[i] = adc_read_mv(ADC_MUXPOS_AIN7_gc, false, 0, 2);
delay_ms_busy(1);
}
for (uint8_t i = 1; i < 16; i++) {
uint16_t x = v[i]; int8_t j = (int8_t)i - 1;
while (j >= 0 && v[j] > x) { v[j + 1] = v[j]; j--; }
v[j + 1] = x;
}
uint16_t th = (uint16_t)(v[12] + RX_THRESH_OVER_MV);
if (th < RX_THRESH_MIN_MV) th = RX_THRESH_MIN_MV;
if (th > RX_THRESH_MAX_MV) th = RX_THRESH_MAX_MV;
DAC0.DATA = (uint8_t)(((uint32_t)th * 256u) / 1100u); /* 4.3 mV per step */
return th;
}
/* Listen: TCB0 in pulse-width capture (EDGE = 0: rising edge clears and
* restarts, falling edge captures and interrupts). Emitter pins are inputs, so
* nothing is emitted. */
void echo_listen_start(void)
{
cli();
users_connect(false);
TCB1.CTRLA = 0;
TCB0.CTRLA = 0;
PORTA.DIRCLR = (1 << PIN_IR850_G) | (1 << PIN_IR940_G);
TCB0.CTRLB = TCB_CNTMODE_PW_gc;
TCB0.EVCTRL = TCB_CAPTEI_bm;
TCB0.INTFLAGS = TCB_CAPT_bm;
TCB0.INTCTRL = TCB_CAPT_bm;
TCB0.CTRLA = TCB_CLKSEL_CLKDIV1_gc | TCB_RUNSTDBY_bm | TCB_ENABLE_bm;
listening = true;
q_head = q_tail = 0;
users_connect(true);
sei();
}
static void tcb_single(TCB_t *t, uint16_t top, bool irq)
{
t->CTRLA = 0;
t->CTRLB = TCB_ASYNC_bm | TCB_CCMPEN_bm | TCB_CNTMODE_SINGLE_gc;
t->CCMP = top;
t->CNT = top; /* no shot at enable (datasheet 21.3.3.1.7) */
t->EVCTRL = TCB_CAPTEI_bm; /* EDGE = 0: start on the rising edge */
t->INTFLAGS = TCB_CAPT_bm;
t->INTCTRL = irq ? TCB_CAPT_bm : 0;
t->CTRLA = TCB_CLKSEL_CLKDIV1_gc | TCB_RUNSTDBY_bm | TCB_ENABLE_bm;
}
void echo_arm(uint8_t us)
{
if (us < ECHO_MIN_US) us = ECHO_MIN_US;
if (us > ECHO_MAX_US) us = ECHO_MAX_US;
width_us = us;
/* output is high from the event (async) to CNT == CCMP plus 2..3 cycles */
uint16_t top = (uint16_t)(us * (F_CPU / 1000000UL) - 2u);
blank_ticks = US_TO_TCA(ECHO_BLANK_US);
cli();
users_connect(false);
listening = false;
PORTA.OUTCLR = (1 << PIN_IR850_G) | (1 << PIN_IR940_G);
tcb_single(&TCB0, top, true); /* TCB0 CAPT = end of echo: timestamp + blanking */
tcb_single(&TCB1, top, false);
TCA0.SINGLE.CTRLA = 0;
TCA0.SINGLE.CTRLB = 0; /* normal mode */
TCA0.SINGLE.INTFLAGS = TCA_SINGLE_OVF_bm;
TCA0.SINGLE.INTCTRL = TCA_SINGLE_OVF_bm;
blanking = false;
q_head = q_tail = 0;
duty_win_t = time_ticks();
duty_win_us = 0;
users_connect(!held);
sei();
}
/* The TCBs keep running on every faucet pulse (that is what timestamps them),
* the pin direction decides whether an emitter actually fires: an input pin
* leaves the gate on its 100k pull-down. */
void echo_set(bool enabled, uint8_t emitters)
{
uint8_t on = 0;
if (enabled) {
if (emitters & EMIT_850) on |= (1 << PIN_IR850_G);
if (emitters & EMIT_940) on |= (1 << PIN_IR940_G);
}
PORTA.DIRCLR = (uint8_t)(((1 << PIN_IR850_G) | (1 << PIN_IR940_G)) & ~on);
PORTA.DIRSET = on;
}
bool echo_blanking(void) { return blanking; }
bool echo_quiet(void)
{
return !blanking && !(TCB0.STATUS & TCB_RUN_bm) && !(TCB1.STATUS & TCB_RUN_bm);
}
void echo_hold(bool hold)
{
cli();
held = hold;
if (hold) users_connect(false);
else if (!blanking && (TCB0.CTRLA & TCB_ENABLE_bm)) users_connect(true);
sei();
}
uint8_t echo_width_us(void) { return width_us; }
/* end of a capture (listen) or end of an echo (armed) */
ISR(TCB0_INT_vect)
{
uint32_t now = time_ticks();
if (listening) {
push(now, TCB0.CCMP); /* reading CCMP clears CAPT in capture modes */
TCB0.INTFLAGS = TCB_CAPT_bm;
return;
}
TCB0.INTFLAGS = TCB_CAPT_bm;
users_connect(false); /* blank: our own echo must not retrigger */
push(now, 0);
/* duty guard: at most ECHO_DUTY_MAX_US of echo per 10 ms window (2 %) */
uint16_t bt = blank_ticks;
if ((uint32_t)(now - duty_win_t) >= (ECHO_DUTY_WINDOW_MS * TICKS_PER_S) / 1000u) {
duty_win_t = now;
duty_win_us = 0;
}
duty_win_us += width_us;
if (duty_win_us + width_us > ECHO_DUTY_MAX_US) bt = US_TO_TCA(ECHO_DUTY_WINDOW_MS * 1000u);
TCA0.SINGLE.CNT = 0;
TCA0.SINGLE.PER = bt;
TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV16_gc | TCA_SINGLE_ENABLE_bm;
blanking = true;
}
/* blanking over: reconnect, but never while the comparator is still high
* (reconnecting on a high level would look like a fresh rising edge) */
ISR(TCA0_OVF_vect)
{
TCA0.SINGLE.INTFLAGS = TCA_SINGLE_OVF_bm;
if (AC0.STATUS & AC_STATE_bm) {
TCA0.SINGLE.CNT = 0;
TCA0.SINGLE.PER = US_TO_TCA(50);
return;
}
TCA0.SINGLE.CTRLA = 0;
blanking = false;
if (!held) users_connect(true);
}