373 lines
14 KiB
C++
373 lines
14 KiB
C++
/*
|
||
* ╔═══════════════════════════════════════════════════════════╗
|
||
* ║ ZK-Glasses — ATtiny85 IR LED Controller Firmware ║
|
||
* ╠═══════════════════════════════════════════════════════════╣
|
||
* ║ Board: ATtiny85 (ATTinyCore, 8 MHz internal) ║
|
||
* ║ Programmer: USBasp / Arduino as ISP ║
|
||
* ║ License: MIT ║
|
||
* ╚═══════════════════════════════════════════════════════════╝
|
||
*
|
||
* Pin Assignment (DIP-8):
|
||
* ┌────────────────────────────┐
|
||
* │ ATtiny85 │
|
||
* │ (RST) PB5 ─┤1 8├─ VCC │
|
||
* │ (ADC) PB3 ─┤2 7├─ PB2 (BTN)
|
||
* │ (OC1B)PB4 ─┤3 6├─ PB1 (LED_R) OC0B
|
||
* │ GND ─┤4 5├─ PB0 (LED_L) OC0A
|
||
* └────────────────────────────┘
|
||
*
|
||
* PB0 - LED group LEFT (8 IR LEDs via N-MOSFET gate)
|
||
* PB1 - LED group RIGHT (8 IR LEDs via N-MOSFET gate)
|
||
* PB2 - Tactile button (internal pull-up, active LOW)
|
||
* PB3 - Status LED (green, current-limited)
|
||
* PB4 - Battery voltage sense (ADC2, voltage divider)
|
||
*
|
||
* Modes:
|
||
* 0 OFF — LEDs off, MCU in low-power idle
|
||
* 1 CONSTANT — Full brightness, steady
|
||
* 2 PULSE_30HZ — 30 Hz square wave (half power draw)
|
||
* 3 PULSE_60HZ — 60 Hz, matches common camera fps
|
||
* 4 RANDOM_FLICKER — Pseudorandom on/off, defeats adaptive filters
|
||
* 5 STEALTH — 25% duty constant (very low visibility)
|
||
*
|
||
* Controls:
|
||
* Short press (<500ms) — Cycle to next mode
|
||
* Long press (>800ms) — Cycle brightness (25→50→75→100%)
|
||
* Double press (<300ms) — Instant OFF
|
||
*
|
||
* Features:
|
||
* - Auto-off after 2 hours (configurable)
|
||
* - Low battery warning: status LED blinks when Vcc < 3.3V
|
||
* - All timing via Timer0 (no delay() in main loop)
|
||
*/
|
||
|
||
#include <avr/io.h>
|
||
#include <avr/interrupt.h>
|
||
#include <avr/sleep.h>
|
||
#include <avr/wdt.h>
|
||
|
||
// ── Pin Definitions ──────────────────────────────────────────
|
||
#define PIN_LED_L PB0 // Left IR LED group (OC0A)
|
||
#define PIN_LED_R PB1 // Right IR LED group (OC0B)
|
||
#define PIN_BTN PB2 // Button input
|
||
#define PIN_STATUS PB3 // Status LED
|
||
#define PIN_VBAT PB4 // Battery ADC (ADC2)
|
||
|
||
// ── Configuration ────────────────────────────────────────────
|
||
#define NUM_MODES 6
|
||
#define DEBOUNCE_MS 40
|
||
#define SHORT_PRESS_MAX 500 // ms — below this = short press
|
||
#define LONG_PRESS_MIN 800 // ms — above this = long press
|
||
#define DOUBLE_PRESS_MAX 300 // ms — gap for double-press detection
|
||
#define AUTO_OFF_MS 7200000UL // 2 hours in ms
|
||
#define LOW_BATT_ADC 175 // ~3.3V via voltage divider (3.3/5*1024*R2/(R1+R2))
|
||
#define BATT_CHECK_INTERVAL 10000 // Check battery every 10s
|
||
|
||
// ── Brightness Levels (PWM duty 0–255) ───────────────────────
|
||
const uint8_t BRIGHTNESS_LEVELS[] = { 64, 128, 192, 255 };
|
||
#define NUM_BRIGHTNESS 4
|
||
|
||
// ── Mode Enumeration ─────────────────────────────────────────
|
||
enum Mode : uint8_t {
|
||
MODE_OFF = 0,
|
||
MODE_CONSTANT = 1,
|
||
MODE_PULSE_30HZ = 2,
|
||
MODE_PULSE_60HZ = 3,
|
||
MODE_RANDOM = 4,
|
||
MODE_STEALTH = 5
|
||
};
|
||
|
||
// ── State ────────────────────────────────────────────────────
|
||
volatile uint8_t g_mode = MODE_OFF;
|
||
volatile uint8_t g_brightness_idx = 3; // Start at full
|
||
volatile uint32_t g_millis = 0;
|
||
volatile bool g_btn_pressed = false;
|
||
|
||
uint32_t g_mode_start_ms = 0; // For auto-off timer
|
||
uint32_t g_last_batt_check = 0;
|
||
bool g_low_battery = false;
|
||
uint16_t g_lfsr = 0xACE1; // LFSR seed for random mode
|
||
|
||
// ── Millisecond Timer (Timer0 overflow at 8MHz/64/256 ≈ 488Hz) ──
|
||
// We'll use Timer1 for millis since Timer0 is used for PWM
|
||
// Actually, let's use a simple millis implementation with Timer0
|
||
// Timer0 is set up for Fast PWM on OC0A/OC0B
|
||
|
||
// We'll track time using Timer1 overflow
|
||
ISR(TIMER1_OVF_vect) {
|
||
// Timer1 at 8MHz/64, 8-bit overflow = every 2.048ms
|
||
// We'll count overflows for rough millisecond tracking
|
||
g_millis += 2;
|
||
}
|
||
|
||
// ── LFSR Pseudorandom (16-bit Galois) ────────────────────────
|
||
uint16_t lfsr_next() {
|
||
uint16_t bit = ((g_lfsr >> 0) ^ (g_lfsr >> 2) ^
|
||
(g_lfsr >> 3) ^ (g_lfsr >> 5)) & 1u;
|
||
g_lfsr = (g_lfsr >> 1) | (bit << 15);
|
||
return g_lfsr;
|
||
}
|
||
|
||
// ── ADC Read (blocking, 10-bit) ──────────────────────────────
|
||
uint16_t read_adc(uint8_t channel) {
|
||
ADMUX = channel; // Vcc reference, selected channel
|
||
ADCSRA = (1 << ADEN) | (1 << ADSC) | // Enable, start conversion
|
||
(1 << ADPS2) | (1 << ADPS1); // Prescaler /64
|
||
while (ADCSRA & (1 << ADSC)); // Wait for completion
|
||
uint16_t result = ADC;
|
||
ADCSRA &= ~(1 << ADEN); // Disable ADC to save power
|
||
return result;
|
||
}
|
||
|
||
// ── Set LED PWM Duty Cycle ───────────────────────────────────
|
||
void set_leds(uint8_t duty) {
|
||
OCR0A = duty; // Left group
|
||
OCR0B = duty; // Right group
|
||
}
|
||
|
||
// ── Status LED Control ───────────────────────────────────────
|
||
void status_led(bool on) {
|
||
if (on) PORTB |= (1 << PIN_STATUS);
|
||
else PORTB &= ~(1 << PIN_STATUS);
|
||
}
|
||
|
||
// ── Button Reading (debounced) ───────────────────────────────
|
||
bool button_is_down() {
|
||
return !(PINB & (1 << PIN_BTN)); // Active LOW
|
||
}
|
||
|
||
// ── Setup ────────────────────────────────────────────────────
|
||
void setup() {
|
||
// --- GPIO ---
|
||
DDRB = (1 << PIN_LED_L) | (1 << PIN_LED_R) | (1 << PIN_STATUS);
|
||
PORTB = (1 << PIN_BTN); // Pull-up on button
|
||
|
||
// --- Timer0: Fast PWM on OC0A (PB0) and OC0B (PB1) ---
|
||
// Mode 3 (Fast PWM, TOP=0xFF), prescaler /1 for ~31.4 kHz base
|
||
// Non-inverting output on both channels
|
||
TCCR0A = (1 << COM0A1) | (1 << COM0B1) |
|
||
(1 << WGM01) | (1 << WGM00);
|
||
TCCR0B = (1 << CS00); // No prescaler → 8MHz/256 = 31.25kHz
|
||
OCR0A = 0;
|
||
OCR0B = 0;
|
||
|
||
// --- Timer1: Millis tracking ---
|
||
// CTC mode, prescaler /64 → overflow at 8MHz/64/256 ≈ 488 Hz
|
||
TCCR1 = (1 << CS12) | (1 << CS11) | (1 << CS10); // /64
|
||
TIMSK |= (1 << TOIE1); // Overflow interrupt
|
||
|
||
sei(); // Enable global interrupts
|
||
|
||
// Startup flash
|
||
status_led(true);
|
||
_delay_ms(200);
|
||
status_led(false);
|
||
}
|
||
|
||
// ── Process Button Input ─────────────────────────────────────
|
||
void process_button() {
|
||
static uint32_t press_start = 0;
|
||
static uint32_t last_release = 0;
|
||
static bool was_pressed = false;
|
||
static bool awaiting_double = false;
|
||
static uint8_t press_count = 0;
|
||
|
||
bool pressed = button_is_down();
|
||
uint32_t now = g_millis;
|
||
|
||
if (pressed && !was_pressed) {
|
||
// --- Button just pressed ---
|
||
press_start = now;
|
||
was_pressed = true;
|
||
}
|
||
else if (!pressed && was_pressed) {
|
||
// --- Button just released ---
|
||
uint32_t duration = now - press_start;
|
||
was_pressed = false;
|
||
|
||
if (duration > LONG_PRESS_MIN) {
|
||
// Long press → cycle brightness
|
||
g_brightness_idx = (g_brightness_idx + 1) % NUM_BRIGHTNESS;
|
||
// Flash status LED to indicate level
|
||
for (uint8_t i = 0; i <= g_brightness_idx; i++) {
|
||
status_led(true);
|
||
_delay_ms(80);
|
||
status_led(false);
|
||
_delay_ms(80);
|
||
}
|
||
}
|
||
else if (duration < SHORT_PRESS_MAX) {
|
||
// Short press — check for double press
|
||
if (awaiting_double && (now - last_release < DOUBLE_PRESS_MAX)) {
|
||
// Double press → OFF
|
||
g_mode = MODE_OFF;
|
||
set_leds(0);
|
||
awaiting_double = false;
|
||
press_count = 0;
|
||
status_led(true);
|
||
_delay_ms(50);
|
||
status_led(false);
|
||
return;
|
||
}
|
||
awaiting_double = true;
|
||
last_release = now;
|
||
press_count++;
|
||
}
|
||
}
|
||
|
||
// Finalize single press after double-press window expires
|
||
if (awaiting_double && !pressed &&
|
||
(now - last_release > DOUBLE_PRESS_MAX)) {
|
||
// Single short press → next mode
|
||
g_mode = (g_mode + 1) % NUM_MODES;
|
||
g_mode_start_ms = now;
|
||
awaiting_double = false;
|
||
press_count = 0;
|
||
|
||
// Quick status flash for mode feedback
|
||
for (uint8_t i = 0; i <= g_mode; i++) {
|
||
status_led(true);
|
||
_delay_ms(40);
|
||
status_led(false);
|
||
_delay_ms(40);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Battery Check ────────────────────────────────────────────
|
||
void check_battery() {
|
||
uint16_t adc_val = read_adc(2); // ADC2 = PB4
|
||
g_low_battery = (adc_val < LOW_BATT_ADC);
|
||
}
|
||
|
||
// ── Main Loop ────────────────────────────────────────────────
|
||
void loop() {
|
||
uint32_t now = g_millis;
|
||
|
||
// --- Button handling ---
|
||
process_button();
|
||
|
||
// --- Auto-off timer ---
|
||
if (g_mode != MODE_OFF &&
|
||
(now - g_mode_start_ms > AUTO_OFF_MS)) {
|
||
g_mode = MODE_OFF;
|
||
set_leds(0);
|
||
}
|
||
|
||
// --- Battery check (periodic) ---
|
||
if (now - g_last_batt_check > BATT_CHECK_INTERVAL) {
|
||
g_last_batt_check = now;
|
||
check_battery();
|
||
}
|
||
|
||
// --- Low battery warning ---
|
||
if (g_low_battery && g_mode != MODE_OFF) {
|
||
// Blink status LED every 2 seconds
|
||
status_led((now / 500) % 4 == 0);
|
||
}
|
||
|
||
// --- LED Mode Execution ---
|
||
uint8_t duty = BRIGHTNESS_LEVELS[g_brightness_idx];
|
||
|
||
switch (g_mode) {
|
||
|
||
case MODE_OFF:
|
||
set_leds(0);
|
||
status_led(false);
|
||
// Could enter sleep mode here for power savings
|
||
break;
|
||
|
||
case MODE_CONSTANT:
|
||
set_leds(duty);
|
||
break;
|
||
|
||
case MODE_PULSE_30HZ:
|
||
// 30 Hz = 33.3ms period, 16.7ms on / 16.7ms off
|
||
if ((now % 33) < 17)
|
||
set_leds(duty);
|
||
else
|
||
set_leds(0);
|
||
break;
|
||
|
||
case MODE_PULSE_60HZ:
|
||
// 60 Hz = 16.7ms period, 8.3ms on / 8.3ms off
|
||
if ((now % 17) < 9)
|
||
set_leds(duty);
|
||
else
|
||
set_leds(0);
|
||
break;
|
||
|
||
case MODE_RANDOM:
|
||
// Change state every 5–25ms (pseudorandom)
|
||
{
|
||
static uint32_t next_change = 0;
|
||
if (now >= next_change) {
|
||
uint16_t rnd = lfsr_next();
|
||
// Random on/off
|
||
if (rnd & 1)
|
||
set_leds(duty);
|
||
else
|
||
set_leds(0);
|
||
// Random interval 5–25ms
|
||
next_change = now + 5 + (rnd % 21);
|
||
}
|
||
}
|
||
break;
|
||
|
||
case MODE_STEALTH:
|
||
// 25% of selected brightness, constant
|
||
set_leds(duty / 4);
|
||
break;
|
||
}
|
||
|
||
_delay_ms(1); // ~1 kHz loop rate
|
||
}
|
||
|
||
/*
|
||
* ── Build & Flash Notes ──────────────────────────────────────
|
||
*
|
||
* Arduino IDE Setup:
|
||
* 1. Install ATTinyCore via Board Manager
|
||
* URL: http://drazzy.com/package_drazzy.com_index.json
|
||
* 2. Board: "ATtiny25/45/85 (No bootloader)"
|
||
* 3. Chip: ATtiny85
|
||
* 4. Clock: 8 MHz (internal)
|
||
* 5. Programmer: USBasp (or "Arduino as ISP")
|
||
* 6. Burn Bootloader first (sets fuses)
|
||
* 7. Upload with programmer (Ctrl+Shift+U)
|
||
*
|
||
* PlatformIO (alternative):
|
||
* [env:attiny85]
|
||
* platform = atmelavr
|
||
* board = attiny85
|
||
* framework = arduino
|
||
* board_build.f_cpu = 8000000L
|
||
* upload_protocol = usbasp
|
||
*
|
||
* Power Consumption (estimated):
|
||
* Mode | Avg Draw | Runtime (500mAh)
|
||
* ──────────────┼───────────┼─────────────────
|
||
* OFF | ~5 µA | years
|
||
* CONSTANT 100% | ~1.6 A | ~18 min
|
||
* PULSE 30Hz | ~800 mA | ~37 min
|
||
* PULSE 60Hz | ~800 mA | ~37 min
|
||
* RANDOM | ~600 mA | ~50 min
|
||
* STEALTH | ~400 mA | ~75 min
|
||
*
|
||
* (16 LEDs × 100mA = 1.6A max; MCU + driver overhead ~5mA)
|
||
*
|
||
* Wiring Quick Reference:
|
||
*
|
||
* ATtiny85 Pin 5 (PB0) ──→ 1kΩ ──→ Q1 Gate (IRLML6344)
|
||
* Q1 Drain ──→ LED Group L (8× parallel)
|
||
* Q1 Source ──→ GND
|
||
*
|
||
* ATtiny85 Pin 6 (PB1) ──→ 1kΩ ──→ Q2 Gate (IRLML6344)
|
||
* Q2 Drain ──→ LED Group R (8× parallel)
|
||
* Q2 Source ──→ GND
|
||
*
|
||
* ATtiny85 Pin 7 (PB2) ──→ Button ──→ GND (internal pull-up)
|
||
* ATtiny85 Pin 2 (PB3) ──→ 330Ω ──→ Status LED ──→ GND
|
||
* ATtiny85 Pin 3 (PB4) ──→ Voltage divider (100k/100k) ──→ VBAT
|
||
*/
|