main
John Lauer Rev 2: aim, status-dot lights, Adafruit 1317 battery, rev 2 cost, revisions, firmware 363a22f 1d ago
/*
 * leds.c : WS2812B-2020-V6 driver and light patterns
 *
 * Rev 2 chain: LED 0 = D4 STATUS dot, LEDs 1..4 = D5..D8 charge gauge
 * (25 / 50 / 75 / 100 %). Every light is a 1.5 mm round clear-resin pipe, so
 * the status dot shows everything rev 1's logo showed, at about half the drive.
 *
 * Why a bit-bang at 10 MHz (and not SPI / USART-SPI / CCL):
 *   LED_DIN is PB5. On the ATtiny1616 PB5 carries CLKOUT, AIN8, AC0 AINP1 and
 *   TCA0 WO2 (alternate) only: no SPI MOSI, no USART TxD, no CCL LUT output.
 *   So no serial peripheral can drive it; a cycle-counted loop it is. At the
 *   normal 5 MHz a 1.25 us bit is 6.25 cycles, too few to place both edges,
 *   and 20 MHz needs 4.5 V. 10 MHz (OSC20M / 2) is inside the 0-10 MHz
 *   @ 2.7 V speed grade, giving 13 cycles per bit:
 *
 *     WS2812B-2020-V6 spec       this loop @ 10 MHz
 *     T0H 220..380 ns            3 cycles = 300 ns
 *     T1H 580 ns..1 us           7 cycles = 700 ns
 *     T0L 580 ns..1 us           10 cycles = 1000 ns
 *     T1L 580 ns..1 us           6 cycles = 600 ns
 *     bit >= 1.25 us             13 cycles = 1.3 us
 *     RES >= 280 us              frames are >= 15 ms apart
 *
 *   T0L sits at the top of its range; WS2812-class parts only latch on a low
 *   longer than RES, so a longer low between bits (the byte fetch adds about
 *   0.6 us after each 8th bit) is harmless. The CPU clock is raised only for
 *   the ~200 us write with interrupts off, and only when the echo is idle;
 *   the echo event users are disconnected during the write so the TCBs never
 *   run on the doubled clock (a faucet pulse in that window just misses one
 *   echo, which no faucet treats as the hand leaving).
 *
 * Current cap: 12 mA per channel at 255, sum of all channel values limited to
 * LED_SUM_MAX (640 -> ~30 mA), sized for small dots and the 150 mAh cell.
 */
#include <avr/io.h>
#include <avr/interrupt.h>
#include "board.h"
#include "leds.h"
#include "echo.h"
#include "power.h"

static uint8_t px[LED_COUNT * 3];        /* G, R, B per LED (WS2812 order) */
static uint8_t last[LED_COUNT * 3];
static bool rail_on;
static uint8_t dark_frames;
static led_pattern_t pat;
static uint8_t pct;
static uint32_t t0;
static bool f_echo, f_blip;

static void ws_send(const uint8_t *p, uint8_t n)
{
    uint8_t b, bits;
    __asm__ __volatile__(
        "1:  ld   %[b], %a[p]+      \n\t"
        "    ldi  %[bits], 8        \n\t"
        "2:  sbi  %[port], %[pin]   \n\t"   /* c1  rising edge            */
        "    nop                    \n\t"   /* c2                         */
        "    sbrs %[b], 7           \n\t"   /* c3 (c3-c4 if bit is 1)     */
        "    cbi  %[port], %[pin]   \n\t"   /* c4  '0': low after 3 cyc   */
        "    lsl  %[b]              \n\t"   /* c5                         */
        "    nop                    \n\t"   /* c6                         */
        "    nop                    \n\t"   /* c7                         */
        "    cbi  %[port], %[pin]   \n\t"   /* c8  '1': low after 7 cyc   */
        "    nop                    \n\t"   /* c9                         */
        "    nop                    \n\t"   /* c10                        */
        "    dec  %[bits]           \n\t"   /* c11                        */
        "    brne 2b                \n\t"   /* c12-c13 -> next bit at c14 */
        "    dec  %[n]              \n\t"
        "    brne 1b                \n\t"
        : [b] "=&d" (b), [bits] "=&d" (bits), [p] "+e" (p), [n] "+r" (n)
        : [port] "I" (_SFR_IO_ADDR(VPORTB_OUT)), [pin] "I" (PIN_LED_DIN)
        : "memory");
}

void leds_init(void)
{
    PORTB.OUTSET = (1 << PIN_LED_EN_N);  /* rail off */
    PORTB.OUTCLR = (1 << PIN_LED_DIN);
    PORTB.DIRSET = (1 << PIN_LED_EN_N) | (1 << PIN_LED_DIN);
    rail_on = false;
    for (uint8_t i = 0; i < sizeof last; i++) last[i] = 0;
}

void leds_off(void)
{
    PORTB.OUTCLR = (1 << PIN_LED_DIN);   /* never drive data into an unpowered LED */
    PORTB.OUTSET = (1 << PIN_LED_EN_N);
    rail_on = false;
    pat = PAT_OFF;
    for (uint8_t i = 0; i < sizeof last; i++) last[i] = 0;
}

void leds_pattern(led_pattern_t p, uint8_t percent, uint32_t now)
{
    pat = p; pct = percent; t0 = now;
}

void leds_flags(bool echoing, bool blip) { f_echo = echoing; f_blip = blip; }
led_pattern_t leds_current(void) { return pat; }

static void set(uint8_t i, uint8_t r, uint8_t g, uint8_t b)
{
    px[3 * i] = g; px[3 * i + 1] = r; px[3 * i + 2] = b;
}

/* triangle 0..255 over 'period' ms, squared for a perceptual fade */
static uint8_t breath(uint32_t t, uint16_t period)
{
    uint32_t ph = t % period, half = period / 2;
    uint32_t tri = ph < half ? ph : period - ph;
    uint16_t lin = (uint16_t)(tri * 255u / half);
    return (uint8_t)((lin * lin) / 255u);
}

static uint8_t level_segments(uint8_t p) { return (uint8_t)((p + 12u) / 25u); }   /* 0..4 */

static void gauge(uint8_t lit, uint8_t fill_frac_255)
{
    for (uint8_t s = 0; s < 4; s++) {
        uint8_t k = s < lit ? 255 : 0;
        if (s == lit && fill_frac_255) k = fill_frac_255;
        if (pct <= 25) set(LED_GAUGE0 + s, (uint8_t)(28u * k / 255u), (uint8_t)(11u * k / 255u), 0);   /* amber */
        else set(LED_GAUGE0 + s, 0, (uint8_t)(28u * k / 255u), (uint8_t)(4u * k / 255u));             /* green */
    }
}

static void render(uint32_t now)
{
    uint32_t t = now - t0;
    for (uint8_t i = 0; i < sizeof px; i++) px[i] = 0;

    switch (pat) {
    case PAT_OFF:
        break;

    case PAT_GAUGE: {                    /* fill one segment per 375 ms, hold */
        uint8_t lit = level_segments(pct);
        if (!lit && pct) lit = 1;
        uint32_t step = t / 375u;
        if (step >= lit) gauge(lit, 0);
        else gauge((uint8_t)step, (uint8_t)((t % 375u) * 255u / 375u));
        uint8_t l = (uint8_t)(t < 3000 ? t * 5u / 3000u : 5);     /* status dot wakes up */
        set(LED_STATUS, (uint8_t)(l * 6 / 10), l, l);
        break;
    }

    case PAT_ARMED: {
        uint8_t l = (uint8_t)(2u + (breath(t, 4000) * 10u) / 255u);   /* 2..12 (rev 1 logo: 3..24) */
        if (f_echo) l = (uint8_t)(l + l / 2);                          /* echoing: a bit brighter */
        if (f_blip) l = ((t / 40u) & 1u) ? (uint8_t)(l * 2u) : 0;     /* re-arm: quick flicker */
        set(LED_STATUS, (uint8_t)(l * 6 / 10), l, l);                   /* soft cyan-white */
        break;
    }

    case PAT_DONE: {                     /* slow warm fade, 2.5 s */
        uint8_t k = t < 2500 ? (uint8_t)(255u - t * 255u / 2500u) : 0;
        k = (uint8_t)((k * k) / 255u);
        set(LED_STATUS, (uint8_t)(20u * k / 255u), (uint8_t)(15u * k / 255u), (uint8_t)(8u * k / 255u));
        break;
    }

    case PAT_LOW:                        /* red blink 4 Hz on the status dot and the first segment */
        if (((t / 250u) & 1u) == 0) {
            set(LED_STATUS, 24, 0, 0);
            set(LED_GAUGE0, 20, 0, 0);
        }
        break;

    case PAT_CHARGING: {
        uint8_t lit = (uint8_t)(pct / 25u);
        if (lit >= 4) gauge(4, 0);
        else {
            gauge(lit, 0);
            uint8_t k = breath(t, 1500);
            set(LED_GAUGE0 + lit, 0, (uint8_t)(3u + 25u * k / 255u), (uint8_t)(4u * k / 255u));
        }
        break;
    }

    case PAT_FULL:
        if (t < 5000) {
            for (uint8_t s = 0; s < 4; s++) set(LED_GAUGE0 + s, 0, 28, 0);
        } else {
            uint32_t w = (t - 5000) % 10000u;
            if (w < 600) {
                uint8_t k = breath(w, 600);
                for (uint8_t s = 0; s < 4; s++) set(LED_GAUGE0 + s, 0, (uint8_t)(8u * k / 255u), 0);
            }
        }
        break;

    case PAT_SHOW_CHARGE: {
        uint8_t lit = level_segments(pct);
        if (!lit && pct) lit = 1;
        gauge(lit, 0);
        break;
    }
    }

    /* current cap */
    uint16_t sum = 0;
    for (uint8_t i = 0; i < sizeof px; i++) sum += px[i];
    if (sum > LED_SUM_MAX)
        for (uint8_t i = 0; i < sizeof px; i++) px[i] = (uint8_t)(((uint32_t)px[i] * LED_SUM_MAX) / sum);
}

bool leds_frame(uint32_t now)
{
    render(now);

    bool any = false, changed = false;
    for (uint8_t i = 0; i < sizeof px; i++) {
        if (px[i]) any = true;
        if (px[i] != last[i]) changed = true;
    }

    if (!any) {
        if (rail_on && ++dark_frames >= 4) {        /* dark for a while: cut the rail */
            PORTB.OUTCLR = (1 << PIN_LED_DIN);
            PORTB.OUTSET = (1 << PIN_LED_EN_N);
            rail_on = false;
            for (uint8_t i = 0; i < sizeof last; i++) last[i] = 0;
        }
        if (!rail_on) return true;
    } else dark_frames = 0;

    if (!changed && rail_on) return true;

    if (!rail_on) {
        PORTB.OUTCLR = (1 << PIN_LED_EN_N) | (1 << PIN_LED_DIN);
        rail_on = true;
        delay_ms_busy(1);                           /* LED power-on reset */
    }

    if (!echo_quiet()) return false;                /* try again next frame */
    echo_hold(true);
    while (!echo_quiet() && !echo_blanking()) ;     /* a shot that started just now: <= 40 us */
    if (echo_blanking()) { echo_hold(false); return false; }

    cli();
    clock_fast();
    ws_send(px, sizeof px);
    clock_normal();
    sei();
    echo_hold(false);

    for (uint8_t i = 0; i < sizeof px; i++) last[i] = px[i];
    return true;
}