main
John Lauer Studio: project overview, how it works, lights, charging, battery life, firmware source, dashboard screenshots; license… c2eaf61 3d ago
/*
 * 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);
}