#include #include #include #include #include #include #include #include #define FACE1_PIN PA0 #define FACE2_PIN PA1 #define WING1_PIN PA2 #define WING2_PIN PA3 #define WING3_PIN PA4 #define ENV_PIN PA5 #define RX_PIN PB0 #define TX_PIN PB1 // OC0A on ATtiny43U #define SW1_PIN PB3 #define LED_FACE1_MASK (1u << FACE1_PIN) #define LED_FACE2_MASK (1u << FACE2_PIN) #define LED_WING1_MASK (1u << WING1_PIN) #define LED_WING2_MASK (1u << WING2_PIN) #define LED_WING3_MASK (1u << WING3_PIN) #define LED_ALL_MASK (LED_FACE1_MASK | LED_FACE2_MASK | LED_WING1_MASK | LED_WING2_MASK | LED_WING3_MASK) #define LED_PWM_STEPS 16u #define LED_PWM_SLOT_DELAY_LOOPS 1100u #define MOSI_PIN PB4 #define MISO_PIN PB5 #define OSCCAL_INPUT_FREQ 32768UL #define OSCCAL_FREQ_CYCLES 40UL #define OSCCAL_MAX_DELTA 32u #define OSCCAL_TARGET_TICKS ((uint16_t)((F_CPU * OSCCAL_FREQ_CYCLES + OSCCAL_INPUT_FREQ / 2UL) / OSCCAL_INPUT_FREQ)) #define OSCCAL_TOLERANCE_TICKS ((uint16_t)(1u + (OSCCAL_TARGET_TICKS - 1u) / 100u)) #define RX_DEFAULT_ON_THRESHOLD 16u #define RX_DEFAULT_OFF_THRESHOLD 8u #define RX_SAMPLES_PER_BIT 4u #define RX_SYMBOL_ON_MIN_SAMPLES 3u #define TX_PREAMBLE_BITS 4u #define TX_SYNC_WORD 0xAAu #define TX_SYNC_WORD_BITS 8u #define OOK_PAYLOAD_BYTES 1u #define OOK_NIBBLES_PER_BYTE 2u #define OOK_CODEWORD_BITS 8u #define TX_FRAME_BITS (TX_PREAMBLE_BITS + 1u + TX_SYNC_WORD_BITS + \ (OOK_PAYLOAD_BYTES * OOK_NIBBLES_PER_BYTE * OOK_CODEWORD_BITS)) #define TX_FRAME_BYTES ((TX_FRAME_BITS + 7u) / 8u) #define WDT_TX_REQUEST_TICKS 240u // ~60 s at a ~250 ms WDT interval #define WDT_SEEN_IDS_MAX_AGE_TICKS 60u // ~15 s at a ~250 ms WDT interval #define WDT_SW1_LONG_PRESS_TICKS 8u // ~2 s at a ~250 ms WDT interval #define WDT_ENV_STUCK_HIGH_TICKS 8u // ~2 s at a ~250 ms WDT interval #define WDT_ENV_STUCK_FLASH_TICKS 16u // ~4 s at a ~250 ms WDT interval #define SELF_TX_DELAY_MASK 0x07u // 1-8 WDT ticks, ~= 0.25-2.0 s #define SEEN_DEVICE_BITMAP_BYTES 32u #define H84_CODEWORD_XOR_MASK 0x03u // this causes 0x00 and 0xFF to be rejected typedef struct { uint8_t ain1_above_vref : 1; uint8_t tx_request : 1; uint8_t tx_ook_done : 1; uint8_t seen_ids_timeout_request : 1; uint8_t sw1_long_press_request : 1; } isr_event_flags_t; typedef struct { uint8_t rx_envelope_on : 1; uint8_t rx_prev_envelope_on : 1; uint8_t rx_packet_ready : 1; uint8_t rx_packet_failed : 1; uint8_t tx_running : 1; uint8_t led_active : 1; } local_flags_t; _Static_assert(sizeof(isr_event_flags_t) == 1u, "isr_event_flags_t must pack to 1 byte"); _Static_assert(sizeof(local_flags_t) == 1u, "local_flags_t must pack to 1 byte"); _Static_assert(TX_FRAME_BITS <= 255u, "tx_bits_remaining is uint8_t; TX_FRAME_BITS must be <= 255"); _Static_assert(TX_FRAME_BITS == 29u, "start_tx_ook_byte packs the current fixed OOK frame layout"); _Static_assert(TX_SYNC_WORD == 0xAAu, "start_tx_ook_byte packs the current sync word"); static volatile isr_event_flags_t isr_events; static volatile uint8_t sw1_last_level; static volatile uint8_t rx_running; static volatile uint8_t tx_ook_running; static local_flags_t local_flags; static volatile uint8_t rx_t0_count_1ms; static volatile uint8_t rx_sample_seq; enum rx_decode_state { RX_STATE_SYNC = 0, RX_STATE_SYNC_WORD, RX_STATE_FEC }; static uint8_t my_id; static uint8_t rx_state; static uint8_t rx_samples_in_bit; static uint8_t rx_on_samples_in_bit; static uint8_t rx_fec_codeword_count; static uint8_t rx_word; static uint8_t rx_word_bit_count; static uint8_t rx_payload_byte; static uint8_t rx_dynamic_on_threshold; static uint8_t rx_dynamic_off_threshold; static uint8_t rx_level_low; static uint8_t rx_level_high; static volatile uint8_t rx_packet_data; static uint8_t rx_poll_last_sample_seq; static volatile uint8_t tx_frame[TX_FRAME_BYTES]; static volatile uint8_t tx_bits_remaining; static volatile uint8_t tx_samples_in_bit; static volatile uint8_t tx_wdt_ticks_remaining; static volatile uint8_t seen_ids_wdt_ticks_remaining; static volatile uint8_t sw1_hold_wdt_ticks; static volatile uint8_t sw1_long_press_armed; static volatile uint8_t env_high_wdt_ticks; static volatile uint8_t env_stuck_flash_wdt_ticks; static uint8_t seen_device_ids[SEEN_DEVICE_BITMAP_BYTES]; extern uint16_t osccal_loop(void); extern void signal_done(void); // Adapted from avr-osccal's BSD-2-Clause AVR053 calibration routine. static uint16_t osccal_calibrate_range(uint8_t osccal_min, uint8_t osccal_max) { uint8_t step; uint8_t osccal; uint8_t curr; uint16_t ticks; uint16_t deviation; uint16_t min_deviation; uint8_t best; step = (uint8_t)((osccal_max - osccal_min + 1u) / 2u); osccal = (uint8_t)(osccal_min + step); wdt_reset(); for (;;) { curr = OSCCAL; while (curr != osccal) { uint8_t diff = (uint8_t)(osccal - curr); if (curr > osccal) { diff = (uint8_t)(-diff); } if (diff > OSCCAL_MAX_DELTA) { if (curr > osccal) { curr = (uint8_t)(curr - OSCCAL_MAX_DELTA); } else { curr = (uint8_t)(curr + OSCCAL_MAX_DELTA); } } else { curr = osccal; } OSCCAL = curr; } ticks = osccal_loop(); if (ticks == OSCCAL_TARGET_TICKS) { return 0u; } step = (uint8_t)(step / 2u); if (step == 0u) { break; } if (ticks < OSCCAL_TARGET_TICKS) { osccal = (uint8_t)(osccal + step); } else { osccal = (uint8_t)(osccal - step); } } if (osccal > (uint8_t)(osccal_min + 2u)) { osccal_min = (uint8_t)(osccal - 2u); } if (osccal < (uint8_t)(osccal_max - 2u)) { osccal_max = (uint8_t)(osccal + 2u); } best = osccal; min_deviation = 0xffffu; wdt_reset(); osccal = osccal_min; for (;;) { OSCCAL = osccal; ticks = osccal_loop(); if (ticks == OSCCAL_TARGET_TICKS) { return 0u; } if (ticks < OSCCAL_TARGET_TICKS) { deviation = (uint16_t)(OSCCAL_TARGET_TICKS - ticks); } else { deviation = (uint16_t)(ticks - OSCCAL_TARGET_TICKS); } if (deviation <= min_deviation) { min_deviation = deviation; best = osccal; } if (osccal == osccal_max) { break; } ++osccal; } OSCCAL = best; return min_deviation; } static uint8_t osccal_boot_requested(void) { DDRB &= (uint8_t)~(1u << MOSI_PIN); PORTB |= (1u << MOSI_PIN); _delay_loop_1(16u); return ((PINB & (1u << MOSI_PIN)) == 0u) ? 1u : 0u; } static void osccal_mode_run(void) __attribute__((noreturn)); static void osccal_mode_run(void) { uint16_t min_deviation; uint8_t range1_osccal; uint16_t range2_min_deviation; cli(); // Match the normal firmware clock before measuring F_CPU cycles. clock_prescale_set(clock_div_2); DDRB |= (1u << MISO_PIN); PORTB |= (1u << MOSI_PIN); PORTB |= (1u << MISO_PIN); while ((PINB & (1u << MOSI_PIN)) != 0u) { wdt_reset(); } while ((PINB & (1u << MOSI_PIN)) == 0u) { wdt_reset(); } min_deviation = osccal_calibrate_range(0u, 0x7fu); range1_osccal = OSCCAL; range2_min_deviation = osccal_calibrate_range(0x80u, 0xffu); if (range2_min_deviation < min_deviation) { min_deviation = range2_min_deviation; } else { OSCCAL = range1_osccal; } if (min_deviation > OSCCAL_TOLERANCE_TICKS) { PINB |= (1u << MISO_PIN); } else { wdt_reset(); eeprom_update_byte((uint8_t *)0x00, OSCCAL); wdt_reset(); signal_done(); } for (;;) { wdt_reset(); } } static void leds_apply_on_mask(uint8_t on_mask) { PORTA &= (uint8_t)~LED_ALL_MASK; DDRA = (uint8_t)((DDRA & (uint8_t)~LED_ALL_MASK) | (on_mask & LED_ALL_MASK)); } static void leds_off(void) { PORTA &= (uint8_t)~LED_ALL_MASK; DDRA &= (uint8_t)~LED_ALL_MASK; } static void leds_init(void) { leds_off(); } static uint8_t led_pwm_level(uint8_t brightness) { return (uint8_t)((brightness + 15u) >> 4); } static void leds_show_pwm( uint8_t face1, uint8_t face2, uint8_t wing1, uint8_t wing2, uint8_t wing3) { const uint8_t face1_level = led_pwm_level(face1); const uint8_t face2_level = led_pwm_level(face2); const uint8_t wing1_level = led_pwm_level(wing1); const uint8_t wing2_level = led_pwm_level(wing2); const uint8_t wing3_level = led_pwm_level(wing3); for (uint8_t slot = 0u; slot < LED_PWM_STEPS; ++slot) { uint8_t on_mask = 0u; if (slot < face1_level) { on_mask |= LED_FACE1_MASK; } if (slot < face2_level) { on_mask |= LED_FACE2_MASK; } if (slot < wing1_level) { on_mask |= LED_WING1_MASK; } if (slot < wing2_level) { on_mask |= LED_WING2_MASK; } if (slot < wing3_level) { on_mask |= LED_WING3_MASK; } leds_apply_on_mask(on_mask); _delay_loop_2(LED_PWM_SLOT_DELAY_LOOPS); } } static const uint8_t hamming84_encode_table[16] PROGMEM = { 0x03u, 0x48u, 0xa9u, 0xe2u, 0x9au, 0xd1u, 0x30u, 0x7bu, 0x84u, 0xcfu, 0x2eu, 0x65u, 0x1du, 0x56u, 0xb7u, 0xfcu }; static uint8_t hamming84_encode_nibble(uint8_t nibble) { return pgm_read_byte(&hamming84_encode_table[nibble & 0x0fu]); } // Returns 0=uncorrectable, 1=valid or corrected. static uint8_t hamming84_decode_codeword(uint8_t codeword, uint8_t *nibble) { codeword ^= H84_CODEWORD_XOR_MASK; const uint8_t b1 = (uint8_t)((codeword >> 0) & 0x01u); const uint8_t b2 = (uint8_t)((codeword >> 1) & 0x01u); const uint8_t b3 = (uint8_t)((codeword >> 2) & 0x01u); const uint8_t b4 = (uint8_t)((codeword >> 3) & 0x01u); const uint8_t b5 = (uint8_t)((codeword >> 4) & 0x01u); const uint8_t b6 = (uint8_t)((codeword >> 5) & 0x01u); const uint8_t b7 = (uint8_t)((codeword >> 6) & 0x01u); const uint8_t b8 = (uint8_t)((codeword >> 7) & 0x01u); const uint8_t s1 = (uint8_t)(b1 ^ b3 ^ b5 ^ b7); const uint8_t s2 = (uint8_t)(b2 ^ b3 ^ b6 ^ b7); const uint8_t s4 = (uint8_t)(b4 ^ b5 ^ b6 ^ b7); const uint8_t syndrome = (uint8_t)((s4 << 2) | (s2 << 1) | s1); const uint8_t overall_parity = (uint8_t)(b1 ^ b2 ^ b3 ^ b4 ^ b5 ^ b6 ^ b7 ^ b8); if (syndrome != 0u) { if (overall_parity == 0u) { return 0u; // Double-bit error detected. } codeword ^= (uint8_t)(1u << (syndrome - 1u)); } *nibble = 0; *nibble |= (uint8_t)(((codeword >> 2) & 0x01u) << 3); *nibble |= (uint8_t)(((codeword >> 4) & 0x01u) << 2); *nibble |= (uint8_t)(((codeword >> 5) & 0x01u) << 1); *nibble |= (uint8_t)(((codeword >> 6) & 0x01u) << 0); return 1u; } static void rx_reset_decoder(void) { rx_state = RX_STATE_SYNC; rx_samples_in_bit = 0; rx_on_samples_in_bit = 0; rx_fec_codeword_count = 0; rx_word = 0u; rx_word_bit_count = 0u; rx_payload_byte = 0; local_flags.rx_envelope_on = 0u; local_flags.rx_prev_envelope_on = 0u; } static void rx_fail_packet(void) { local_flags.rx_packet_failed = 1u; rx_reset_decoder(); } static void rx_update_thresholds(uint8_t t0_count) { if (t0_count > rx_level_high) { rx_level_high = t0_count; } else if (rx_level_high > rx_level_low) { --rx_level_high; } if (t0_count < rx_level_low) { rx_level_low = t0_count; } else if (rx_level_low < rx_level_high) { ++rx_level_low; } if (rx_level_high <= rx_level_low) { rx_dynamic_on_threshold = RX_DEFAULT_ON_THRESHOLD; rx_dynamic_off_threshold = RX_DEFAULT_OFF_THRESHOLD; return; } const uint8_t span = (uint8_t)(rx_level_high - rx_level_low); if (span < 6u) { rx_dynamic_on_threshold = RX_DEFAULT_ON_THRESHOLD; rx_dynamic_off_threshold = RX_DEFAULT_OFF_THRESHOLD; return; } // Exact integer thirds without pulling in libgcc mul/div helpers. uint8_t span_third = 0u; uint8_t rem = span; while (rem >= 12u) { rem = (uint8_t)(rem - 12u); span_third = (uint8_t)(span_third + 4u); } while (rem >= 3u) { rem = (uint8_t)(rem - 3u); ++span_third; } const uint8_t span_two_thirds = (uint8_t)((uint8_t)(span_third << 1) + (rem == 2u)); rx_dynamic_off_threshold = (uint8_t)(rx_level_low + span_third); rx_dynamic_on_threshold = (uint8_t)(rx_level_low + span_two_thirds); if (rx_dynamic_on_threshold <= (uint8_t)(rx_dynamic_off_threshold + 1u)) { rx_dynamic_on_threshold = (uint8_t)(rx_dynamic_off_threshold + 2u); } } static void rx_handle_bit(uint8_t bit) { switch (rx_state) { case RX_STATE_SYNC: if (bit != 0u) { if (++rx_word_bit_count >= TX_PREAMBLE_BITS) { rx_fail_packet(); } } else { rx_state = RX_STATE_SYNC_WORD; rx_word = 0u; rx_word_bit_count = 0u; } break; case RX_STATE_SYNC_WORD: rx_word = (uint8_t)((rx_word << 1) | bit); if (++rx_word_bit_count >= TX_SYNC_WORD_BITS) { if (rx_word != TX_SYNC_WORD) { rx_fail_packet(); break; } rx_state = RX_STATE_FEC; rx_fec_codeword_count = 0; rx_word = 0u; rx_word_bit_count = 0u; rx_payload_byte = 0; } break; case RX_STATE_FEC: rx_word = (uint8_t)((rx_word << 1) | bit); if (++rx_word_bit_count >= OOK_CODEWORD_BITS) { uint8_t nibble = 0; const uint8_t status = hamming84_decode_codeword(rx_word, &nibble); if (status == 0u) { rx_fail_packet(); break; } if ((rx_fec_codeword_count & 0x01u) == 0u) { rx_payload_byte = (uint8_t)(nibble << 4); } else { rx_payload_byte |= nibble; } ++rx_fec_codeword_count; rx_word = 0u; rx_word_bit_count = 0u; if (rx_fec_codeword_count >= (OOK_PAYLOAD_BYTES * OOK_NIBBLES_PER_BYTE)) { rx_packet_data = rx_payload_byte; local_flags.rx_packet_ready = 1u; rx_reset_decoder(); } } break; default: rx_reset_decoder(); break; } } static void rx_process_sample(uint8_t t0_count) { rx_update_thresholds(t0_count); if (local_flags.rx_envelope_on) { if (t0_count <= rx_dynamic_off_threshold) { local_flags.rx_envelope_on = 0u; } } else if (t0_count >= rx_dynamic_on_threshold) { local_flags.rx_envelope_on = 1u; } // Timing recovery: nudge symbol phase only on OFF->ON transitions. if (local_flags.rx_envelope_on && !local_flags.rx_prev_envelope_on) { if (rx_samples_in_bit <= 1u) { rx_samples_in_bit = 0; rx_on_samples_in_bit = 0; } else if (rx_samples_in_bit >= (RX_SAMPLES_PER_BIT - 1u)) { rx_samples_in_bit = (RX_SAMPLES_PER_BIT - 1u); } } local_flags.rx_prev_envelope_on = local_flags.rx_envelope_on; if (local_flags.rx_envelope_on) { ++rx_on_samples_in_bit; } ++rx_samples_in_bit; if (rx_samples_in_bit >= RX_SAMPLES_PER_BIT) { const uint8_t bit = (rx_on_samples_in_bit >= RX_SYMBOL_ON_MIN_SAMPLES) ? 1u : 0u; rx_handle_bit(bit); rx_samples_in_bit = 0; rx_on_samples_in_bit = 0; } } static void rx_decoder_init(void) { rx_dynamic_on_threshold = RX_DEFAULT_ON_THRESHOLD; rx_dynamic_off_threshold = RX_DEFAULT_OFF_THRESHOLD; rx_level_low = 0; rx_level_high = RX_DEFAULT_ON_THRESHOLD; local_flags.rx_packet_ready = 0u; local_flags.rx_packet_failed = 0u; rx_packet_data = 0; rx_reset_decoder(); } static void timer1_compa_enable_1ms(void) { // Timer1 CTC mode, TOP=OCR1A. TCNT1 = 0; TCCR1A = (1 << WGM11); TCCR1B = (1 << CS11) | (1 << CS10); // prescaler=64 OCR1A = 62; // ~1 ms at 4 MHz, prescaler 64 TIFR1 = (1 << OCF1A); // clear pending compare flag TIMSK1 |= (1 << OCIE1A); } static void timer1_compa_disable(void) { TIMSK1 &= ~(1 << OCIE1A); TCCR1A = 0; TCCR1B = 0; TIFR1 = (1 << OCF1A); // clear pending compare flag } static void ain1_comp_interrupt_enable(void) { ACSR |= (1 << ACI); // clear pending comparator flag ACSR |= (1 << ACIE); } static void ain1_comp_interrupt_disable(void) { ACSR &= ~(1 << ACIE); ACSR |= (1 << ACI); // clear pending comparator flag } static void sw1_falling_interrupt_init(void) { sw1_last_level = ((PINB & (1 << SW1_PIN)) != 0u) ? 1u : 0u; isr_events.tx_request = 0u; GIFR = (1 << PCIF1); // clear pending pin-change flag (PCINT[15:8]) PCMSK1 |= (1 << PCINT11); // enable SW1_PIN pin-change source GIMSK |= (1 << PCIE1); // enable pin-change interrupts for PCINT[15:8] } static void watchdog_interrupt_init(void) { const uint8_t sreg = SREG; cli(); MCUSR &= ~(1 << WDRF); tx_wdt_ticks_remaining = WDT_TX_REQUEST_TICKS; seen_ids_wdt_ticks_remaining = 0u; sw1_hold_wdt_ticks = 0u; sw1_long_press_armed = (sw1_last_level == 0u) ? 1u : 0u; env_high_wdt_ticks = 0u; env_stuck_flash_wdt_ticks = 0u; isr_events.tx_request = 0u; isr_events.tx_ook_done = 0u; isr_events.ain1_above_vref = 0u; isr_events.seen_ids_timeout_request = 0u; isr_events.sw1_long_press_request = 0u; // Timed sequence: update WDT to interrupt-only mode at ~250 ms interval. WDTCSR = (1 << WDCE) | (1 << WDE); WDTCSR = (1 << WDIE) | (1 << WDP2); SREG = sreg; } static void tx_carrier_on(void) { TCCR0A = (1 << WGM01) | (1 << COM0A0); } static void tx_carrier_off(void) { TCCR0A = (1 << WGM01); PORTB &= ~(1 << TX_PIN); } ISR(TIM1_COMPA_vect) { if (rx_running) { const uint8_t t0_delta = TCNT0; TCNT0 = 0; rx_t0_count_1ms = t0_delta; ++rx_sample_seq; } if (tx_ook_running) { const uint8_t bit_pos = (uint8_t)(TX_FRAME_BITS - tx_bits_remaining); const uint8_t byte_index = (uint8_t)(bit_pos >> 3); const uint8_t bit_index = (uint8_t)(7u - (bit_pos & 0x07u)); const uint8_t bit = (uint8_t)((tx_frame[byte_index] >> bit_index) & 0x01u); if (bit) { tx_carrier_on(); } else { tx_carrier_off(); } ++tx_samples_in_bit; if (tx_samples_in_bit >= RX_SAMPLES_PER_BIT) { tx_samples_in_bit = 0; --tx_bits_remaining; if (tx_bits_remaining == 0u) { tx_carrier_off(); timer1_compa_disable(); tx_ook_running = 0u; isr_events.tx_ook_done = 1u; } } } } static void timer0_ctc_init(uint8_t top) { // OC0A (TX) output enable DDRB |= (1 << TX_PIN); // CTC mode: WGM02:0 = 2 => WGM01=1, TOP=OCR0A. // Toggle OC0A on compare match for a hardware square wave. TCCR0A = (1 << WGM01) | (1 << COM0A0); TCCR0B = (1 << CS00); // prescaler=1 OCR0A = top; } static void start_rx(void) { // T0 is RX_PIN on ATtiny43U. Count external edges on T0 using Timer0 clock source. DDRB &= ~(1 << RX_PIN); PORTB &= ~(1 << RX_PIN); // Timer0 normal mode as external event counter. TCCR0A = 0; TCCR0B = 0; TCNT0 = 0; rx_t0_count_1ms = 0; rx_sample_seq = 0; rx_poll_last_sample_seq = 0; rx_decoder_init(); rx_running = 1u; // External clock source on T0 pin, rising edge. TCCR0B = (1 << CS02) | (1 << CS01) | (1 << CS00); timer1_compa_enable_1ms(); } static void stop_rx(void) { rx_running = 0u; TCCR0A = 0; TCCR0B = 0; TCNT0 = 0; timer1_compa_disable(); } static void start_tx() { // f_out = F_CPU / (2 * N * (OCR0A + 1)) when OC0A toggles in CTC mode. // 40 kHz @ 4 MHz, N=1 => OCR0A = 49. timer0_ctc_init(49); } static void clear_tx_ook_done_event(void) { const uint8_t sreg = SREG; cli(); isr_events.tx_ook_done = 0u; SREG = sreg; } static void start_tx_ook_byte(uint8_t data) { const uint8_t codeword_hi = hamming84_encode_nibble((uint8_t)(data >> 4)); const uint8_t codeword_lo = hamming84_encode_nibble((uint8_t)(data & 0x0fu)); tx_frame[0] = 0xf5u; tx_frame[1] = (uint8_t)(0x50u | (codeword_hi >> 5)); tx_frame[2] = (uint8_t)((codeword_hi << 3) | (codeword_lo >> 5)); tx_frame[3] = (uint8_t)(codeword_lo << 3); tx_bits_remaining = TX_FRAME_BITS; tx_samples_in_bit = 0; clear_tx_ook_done_event(); tx_ook_running = 1u; start_tx(); tx_carrier_on(); timer1_compa_enable_1ms(); } static void stop_tx() { // Stop Timer0 and disconnect OC0A from TX. TCCR0A = 0; TCCR0B = 0; PORTB &= ~(1 << TX_PIN); DDRB &= ~(1 << TX_PIN); } ISR(ANA_COMP_vect) { // With ACBG=1, comparator compares VBG (positive input) vs AIN1 (negative input). // AIN1 rising above VBG causes a high->low transition on ACO (falling edge). isr_events.ain1_above_vref = 1u; } ISR(PCINT1_vect) { const uint8_t sw1_level = ((PINB & (1 << SW1_PIN)) != 0u) ? 1u : 0u; if ((sw1_last_level != 0u) && (sw1_level == 0u)) { sw1_hold_wdt_ticks = 0u; sw1_long_press_armed = 1u; isr_events.tx_request = 1u; } else if (sw1_level != 0u) { sw1_hold_wdt_ticks = 0u; sw1_long_press_armed = 0u; } sw1_last_level = sw1_level; } ISR(WDT_vect) { if (tx_wdt_ticks_remaining > 0u) { --tx_wdt_ticks_remaining; } if (tx_wdt_ticks_remaining == 0u) { tx_wdt_ticks_remaining = WDT_TX_REQUEST_TICKS; isr_events.tx_request = 1u; } if (seen_ids_wdt_ticks_remaining > 0u) { --seen_ids_wdt_ticks_remaining; if (seen_ids_wdt_ticks_remaining == 0u) { isr_events.seen_ids_timeout_request = 1u; } } if (sw1_long_press_armed && (sw1_last_level == 0u)) { if (sw1_hold_wdt_ticks < WDT_SW1_LONG_PRESS_TICKS) { ++sw1_hold_wdt_ticks; } if (sw1_hold_wdt_ticks >= WDT_SW1_LONG_PRESS_TICKS) { sw1_long_press_armed = 0u; isr_events.sw1_long_press_request = 1u; } } // With ACBG=1, ACO is low when AIN1/ENV is above the bandgap reference. if ((ACSR & (1 << ACO)) == 0u) { if (env_high_wdt_ticks < WDT_ENV_STUCK_HIGH_TICKS) { ++env_high_wdt_ticks; } if ((env_high_wdt_ticks >= WDT_ENV_STUCK_HIGH_TICKS) && (env_stuck_flash_wdt_ticks == 0u)) { env_stuck_flash_wdt_ticks = WDT_ENV_STUCK_FLASH_TICKS; } } else { env_high_wdt_ticks = 0u; env_stuck_flash_wdt_ticks = 0u; } if (env_stuck_flash_wdt_ticks > 0u) { --env_stuck_flash_wdt_ticks; } } static void poll_rx_decoder(void) { uint8_t sample_seq; uint8_t t0_count; const uint8_t sreg = SREG; cli(); sample_seq = rx_sample_seq; if (sample_seq == rx_poll_last_sample_seq) { SREG = sreg; return; } t0_count = rx_t0_count_1ms; rx_poll_last_sample_seq = sample_seq; SREG = sreg; rx_process_sample(t0_count); } typedef struct { uint8_t tx_triggered : 1; uint8_t seen_ids_timeout_triggered : 1; uint8_t sw1_long_press_triggered : 1; } loop_events_t; static loop_events_t poll_loop_events(void) { loop_events_t events = {0u, 0u, 0u}; const uint8_t sreg = SREG; cli(); if (isr_events.tx_request) { isr_events.tx_request = 0u; events.tx_triggered = 1u; } if (isr_events.seen_ids_timeout_request) { isr_events.seen_ids_timeout_request = 0u; events.seen_ids_timeout_triggered = 1u; } if (isr_events.sw1_long_press_request) { isr_events.sw1_long_press_request = 0u; events.sw1_long_press_triggered = 1u; } SREG = sreg; return events; } static void clear_ain1_event_flag(void) { const uint8_t sreg = SREG; cli(); isr_events.ain1_above_vref = 0u; SREG = sreg; } static void clear_seen_device_ids(void) { for (uint8_t i = 0u; i < SEEN_DEVICE_BITMAP_BYTES; ++i) { seen_device_ids[i] = 0u; } } static uint8_t count_seen_device_ids(void) { uint8_t count = 0u; for (uint8_t i = 0u; i < SEEN_DEVICE_BITMAP_BYTES; ++i) { for (uint8_t b = seen_device_ids[i]; b != 0u; b = (uint8_t)(b >> 1)) { count = (uint8_t)(count + (uint8_t)(b & 1u)); } } return count; } static uint8_t triwave8(uint8_t phase) { if ((phase & 0x80u) != 0u) { return (uint8_t)((uint8_t)(255u - phase) << 1); } return (uint8_t)(phase << 1); } static uint16_t build_lightshow_seed() { const uint8_t self_byte_index = (uint8_t)(my_id >> 3); const uint8_t self_bit_mask = (uint8_t)(1u << (my_id & 0x07u)); uint16_t seed = 0x6D2Bu; seen_device_ids[self_byte_index] |= self_bit_mask; for (uint8_t i = 0u; i < SEEN_DEVICE_BITMAP_BYTES; ++i) { seed ^= seen_device_ids[i]; seed = (uint16_t)((seed << 5) | (seed >> 11)); seed ^= (uint16_t)((uint16_t)i << 8); seed = (uint16_t)(seed + 0x3Du); } if (seed == 0u) { seed = 0xA35Fu; } return seed; } static uint8_t lightshow_rand8(uint16_t *state) { uint16_t x = *state; x ^= (uint16_t)(x << 7); x ^= (uint16_t)(x >> 9); x ^= (uint16_t)(x << 8); *state = x; return (uint8_t)(x ^ (x >> 8)); } static void run_lightshow(uint8_t seen_count, uint16_t seed) { uint8_t tier = 0u; uint8_t speed = 3u; uint16_t total_quanta = 250u; uint16_t rng = seed; uint8_t phase = (uint8_t)seed; const uint8_t style = (uint8_t)(seed & 0x07u); if (seen_count <= 1u) { tier = 0u; total_quanta = 250u; // ~5 s speed = 3u; } else if (seen_count <= 3u) { tier = 1u; total_quanta = 400u; // ~8 s speed = 5u; } else if (seen_count <= 10u) { tier = 2u; total_quanta = 600u; // ~12 s speed = 7u; } else { tier = 3u; total_quanta = 800u; // ~16 s speed = 10u; } while (total_quanta > 0u) { const uint8_t rnd = lightshow_rand8(&rng); uint8_t face1; uint8_t face2; uint8_t wing1; uint8_t wing2; uint8_t wing3; uint8_t wing_shift = 2u; const uint8_t wing_floor = (uint8_t)(tier << 3); phase = (uint8_t)(phase + speed + (rnd & tier)); face1 = (uint8_t)((triwave8(phase) >> 1) + 24u); face2 = (uint8_t)((triwave8((uint8_t)(phase + 128u)) >> 1) + 24u); if (tier == 0u) { wing1 = 0u; wing2 = 0u; wing3 = 0u; if ((style & 0x06u) == 0x02u) { face2 = face1; } else if ((style & 0x06u) == 0x04u) { if ((phase & 0x40u) == 0u) { face1 = 255u; face2 = 16u; } else { face1 = 16u; face2 = 255u; } } else if ((style & 0x06u) == 0x06u) { if ((phase & 0x18u) == 0u) { face1 = 255u; face2 = 255u; } else { face1 = 8u; face2 = 8u; } } if ((phase & 0x3fu) < 8u) { const uint8_t wink = triwave8((uint8_t)(phase << 2)); if ((seed & 0x03u) == 0u) { wing1 = wink; } else if ((seed & 0x03u) == 1u) { wing2 = wink; } else { wing3 = wink; } } } else { if (tier >= 3u) { wing_shift = 1u; } if ((style & 0x06u) == 0x06u) { uint8_t scan = (uint8_t)((phase >> 5) & 0x03u); if (scan == 3u) { scan = 1u; } wing1 = wing_floor; wing2 = wing_floor; wing3 = wing_floor; if (scan == 0u) { wing1 = 255u; } else if (scan == 1u) { wing2 = 255u; } else { wing3 = 255u; } } else if (style == 3u) { const uint8_t pulse = triwave8((uint8_t)(phase << 1)); face1 = pulse; face2 = pulse; wing1 = pulse; wing2 = pulse; wing3 = pulse; } else if (style == 4u) { if ((phase & 0x40u) == 0u) { wing1 = 255u; wing2 = wing_floor; wing3 = 255u; face1 = 255u; face2 = 16u; } else { wing1 = wing_floor; wing2 = 255u; wing3 = wing_floor; face1 = 16u; face2 = 255u; } } else { uint8_t wing_gap = 85u; if ((style & 0x01u) != 0u) { wing_gap = 128u; } wing1 = (uint8_t)((triwave8(phase) >> wing_shift) + wing_floor); wing2 = (uint8_t)((triwave8((uint8_t)(phase + wing_gap)) >> wing_shift) + wing_floor); wing3 = (uint8_t)((triwave8((uint8_t)(phase + wing_gap + wing_gap)) >> wing_shift) + wing_floor); } if ((style >= 2u) && (style != 4u)) { face2 = face1; } } if ((tier >= 3u) && ((rnd & 0x0fu) == 0u)) { face1 = 255u; face2 = 255u; } leds_show_pwm(face1, face2, wing1, wing2, wing3); --total_quanta; } } static void note_seen_device_id(uint8_t device_id) { const uint8_t byte_index = (uint8_t)(device_id >> 3); const uint8_t bit_mask = (uint8_t)(1u << (device_id & 0x07u)); if ((seen_device_ids[byte_index] & bit_mask) != 0u) { return; } seen_device_ids[byte_index] |= bit_mask; const uint8_t sreg = SREG; cli(); seen_ids_wdt_ticks_remaining = WDT_SEEN_IDS_MAX_AGE_TICKS; isr_events.seen_ids_timeout_request = 0u; SREG = sreg; } static void schedule_delayed_self_tx(void) { const uint8_t delay_wdt_ticks = (uint8_t)(my_id & SELF_TX_DELAY_MASK); const uint8_t sreg = SREG; cli(); tx_wdt_ticks_remaining = (uint8_t)(delay_wdt_ticks + 1u); isr_events.tx_request = 0u; SREG = sreg; } static void act_on_seen_devices(void) { const uint8_t seen_count = count_seen_device_ids(); const uint16_t seed = build_lightshow_seed(); if (rx_running) { stop_rx(); } ain1_comp_interrupt_disable(); local_flags.led_active = 1u; const uint8_t sreg = SREG; cli(); seen_ids_wdt_ticks_remaining = 0u; isr_events.seen_ids_timeout_request = 0u; isr_events.ain1_above_vref = 0u; SREG = sreg; run_lightshow(seen_count, seed); leds_off(); local_flags.led_active = 0u; clear_seen_device_ids(); if (!local_flags.tx_running) { ain1_comp_interrupt_enable(); } } static uint8_t handle_sw1_long_press_event(uint8_t sw1_long_press_triggered) { if (sw1_long_press_triggered == 0u) { return 0u; } if (local_flags.tx_running) { const uint8_t sreg = SREG; cli(); isr_events.sw1_long_press_request = 1u; SREG = sreg; return 1u; } if (rx_running) { stop_rx(); } ain1_comp_interrupt_disable(); local_flags.led_active = 1u; clear_ain1_event_flag(); run_lightshow(10, my_id); leds_off(); local_flags.led_active = 0u; ain1_comp_interrupt_enable(); return 1u; } static void handle_seen_ids_timeout_event(uint8_t seen_ids_timeout_triggered) { if (seen_ids_timeout_triggered == 0u) { return; } act_on_seen_devices(); } static void handle_tx_done_event(void) { const uint8_t sreg = SREG; cli(); if (!(local_flags.tx_running && isr_events.tx_ook_done)) { SREG = sreg; return; } isr_events.tx_ook_done = 0u; SREG = sreg; stop_tx(); local_flags.tx_running = 0u; clear_ain1_event_flag(); if (!local_flags.led_active) { ain1_comp_interrupt_enable(); } } static void handle_tx_request_event(uint8_t tx_triggered) { if ((tx_triggered == 0u) || local_flags.tx_running) { return; } if (rx_running) { stop_rx(); } ain1_comp_interrupt_disable(); clear_ain1_event_flag(); start_tx_ook_byte(my_id); local_flags.tx_running = 1u; } static void maybe_start_rx_from_ain1(void) { uint8_t ain1_triggered = 0u; const uint8_t sreg = SREG; if (local_flags.tx_running || local_flags.led_active || rx_running) { return; } cli(); if (isr_events.ain1_above_vref) { isr_events.ain1_above_vref = 0u; ain1_triggered = 1u; } SREG = sreg; if (ain1_triggered) { start_rx(); } } static void handle_rx_packet_if_ready(void) { if (local_flags.tx_running || local_flags.led_active || !rx_running) { return; } poll_rx_decoder(); if (local_flags.rx_packet_failed) { local_flags.rx_packet_failed = 0u; stop_rx(); clear_ain1_event_flag(); return; } if (!local_flags.rx_packet_ready) { return; } const uint8_t nearby_device_id = rx_packet_data; local_flags.rx_packet_ready = 0u; stop_rx(); clear_ain1_event_flag(); note_seen_device_id(nearby_device_id); schedule_delayed_self_tx(); } static void handle_env_stuck_flash(void) { const uint8_t flash_ticks = env_stuck_flash_wdt_ticks; if ((flash_ticks != 0u) && ((flash_ticks & 0x01u) != 0u)) { leds_apply_on_mask(LED_WING2_MASK); } else { leds_off(); } } static void sleep_if_idle(void) { cli(); const uint8_t tx_done_pending = (uint8_t)(local_flags.tx_running && isr_events.tx_ook_done); const uint8_t ain1_start_pending = (uint8_t)(!local_flags.tx_running && !rx_running && isr_events.ain1_above_vref); const uint8_t rx_sample_pending = (uint8_t)( !local_flags.tx_running && rx_running && (rx_sample_seq != rx_poll_last_sample_seq)); if (!isr_events.tx_request && !isr_events.seen_ids_timeout_request && !isr_events.sw1_long_press_request && !tx_done_pending && !ain1_start_pending && !rx_sample_pending) { // Keep comparator and pin-change wake sources fully available. set_sleep_mode(SLEEP_MODE_IDLE); sleep_enable(); sei(); sleep_cpu(); sleep_disable(); } else { sei(); } } int main(void) { if (osccal_boot_requested() != 0u) { osccal_mode_run(); } // If we have a stored OSCCAL in EEPROM, use it const uint8_t osccal = eeprom_read_byte((const uint8_t *)0x00); if (osccal != 0xff) { OSCCAL = osccal; } // F_CPU = 4 MHz clock_prescale_set(clock_div_2); // Load our ID my_id = eeprom_read_byte((const uint8_t *)0x01); // Configure LED banks as open-collector outputs, defaulting off/floating. leds_init(); // Configure AIN1 as analog comparator input. DDRA &= ~(1 << ENV_PIN); PORTA &= ~(1 << ENV_PIN); DIDR0 |= (1 << AIN1D); // Configure TX request as pulled-up digital input. DDRB &= ~(1 << SW1_PIN); PORTB |= (1 << SW1_PIN); sw1_falling_interrupt_init(); watchdog_interrupt_init(); // Comparator setup: // - Positive input = internal bandgap reference (ACBG=1) // - Negative input = AIN1 pin // - Interrupt on falling edge (ACIS1:0 = 10), which corresponds to AIN1 > VBG // when using bandgap on the positive input. ACSR = (1 << ACBG) | (1 << ACIS1); ain1_comp_interrupt_enable(); sei(); local_flags.tx_running = 0u; local_flags.led_active = 0u; for (;;) { const loop_events_t events = poll_loop_events(); handle_tx_done_event(); const uint8_t sw1_long_press_handled = handle_sw1_long_press_event(events.sw1_long_press_triggered); if (!sw1_long_press_handled) { handle_tx_request_event(events.tx_triggered); } handle_seen_ids_timeout_event(events.seen_ids_timeout_triggered); maybe_start_rx_from_ain1(); handle_rx_packet_if_ready(); handle_env_stuck_flash(); sleep_if_idle(); } }