// Copyright (C) 2026 Allen Hill // SPDX-License-Identifier: GPL-3.0-or-later #include #include #include #include #include "media_avr.h" // media_sync_during_mask (used by the bus guard) #include "hal/media.h" // F_CPU defined in timing_avr.h; the mic tick constants below are derived from // it (this hardware generation's TCA0/PB1 button-press implementation). #include "timing_avr.h" // pending WO1 toggles (even); signed to avoid underflows from a stray OVF static volatile int8_t mic_ntoggles = 0; // Target pulse length is ~40-150ms, with interval between pulses of // ~100-200ms // TCA0 period (CMP0/TOP) in ticks at F_CPU with the CLKSEL=DIV1024 prescaler. // A press phase is ~100 ms; the final LOW phase is stretched to // mic_refractory_period (~333 ms) so consecutive presses stay distinct static constexpr uint16_t mic_press_ticks = (uint16_t)((F_CPU / 1024UL) / 10UL); static constexpr uint16_t mic_refractory_period = (uint16_t)((F_CPU / 1024UL) / 3UL); // the longest AVCLAN frame duration is ~15ms static constexpr uint16_t close_thresh = (uint16_t)(mic_press_ticks * 15UL / 100UL); #ifndef NDEBUG // Toggle PB1 and return its new level. bool media_mic_toggle() { // Take manual control of PB1 (CMP1EN gives TCA0 control of WO1/PB1 level) TCA0.SINGLE.CTRLB &= ~TCA_SINGLE_CMP1EN_bm; VPORTB.OUT ^= PIN1_bm; return (VPORTB.OUT & PIN1_bm) != 0; } bool media_busy() { return mic_ntoggles != 0; } #endif // Begin a press waveform of `nphases` × 100 ms level segments. // - ~Immediately toggles high, alternates each phase (1 = single HIGH press, 3 // = skip H/L/H, etc). // - Halting the timer freezes WO1 at its last level; must run even number of // phases to ensure we return to low static void mic_pulse(uint8_t nphases) { ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { if (mic_ntoggles) // Skip if already pulsing return; // must be even to return to idle-low mic_ntoggles = (nphases & 0x01) ? nphases + 1 : nphases; TCA0.SINGLE.CTRLB |= TCA_SINGLE_CMP1EN_bm; // Reassert TCA control of WO1/PB1 TCA0.SINGLE.CTRLC = 0; // Reset WO1 level just in case TCA0.SINGLE.CNT = 0; TCA0.SINGLE.CMP0 = // TOP mic_press_ticks; // always restore default ~100 ms period TCA0.SINGLE.INTFLAGS = TCA_SINGLE_OVF_bm; // clear any stale flag TCA0.SINGLE.INTCTRL |= TCA_SINGLE_OVF_bm; TCA0.SINGLE.CTRLA |= TCA_SINGLE_ENABLE_bm; } } // OVF ISR body function: // - counts phases // - stretches the final LOW phase as a refractory period, to keep separate // pulse trains distinct // - stops the timer after the last phase static inline void mic_timer_isr_body(bool is_early) { if (--mic_ntoggles == 1) { // Stretch final phase to a ~333 ms idle-low so back-to-back presses stay // distinct // Invariant: mic_refractory_period > mic_press_ticks, so counter can never // silently wrap TCA0.SINGLE.CMP0 = mic_refractory_period; } else if (mic_ntoggles <= 0) { TCA0.SINGLE.CTRLA &= ~TCA_SINGLE_ENABLE_bm; TCA0.SINGLE.CTRLC = 0; // Timer must be disabled before (re)setting // CTRLC/WO1 level (§20.5.3) mic_ntoggles = 0; // clamp to avoid perma-lockout in mic_pulse } if (is_early) TCA0.SINGLE.CNT = 0; // OVF FLAG must be cleared via write. MUST BE PERFORMED LAST. // This function is called in two cases: // - ISR, triggered by actual OVF: OVF FLAG is set and needs clearing // - stopEvent, running ISR body ~early: OVF flag may be set while in // ATOMIC_BLOCK (either in stopEvent, or earlier in this function body). // Clear OVF FLAG as a precaution to prevent erroneous ISR runs TCA0.SINGLE.INTFLAGS = TCA_SINGLE_OVF_bm; } ISR(TCA0_OVF_vect) { mic_timer_isr_body(false); } // Emulate a transport-control button press on the source device. Each action // maps to a press-train of a given length on MIC_CONTROL. void media_action(enum MediaAction action) { switch (action) { case Play: case Pause: case Play_Pause: mic_pulse(1); break; // single press case Skip_Forward: case Track_Next: mic_pulse(3); break; // double-press case Skip_Backward: case Track_Prev: mic_pulse(5); break; // triple-press case Repeat: case Repeat_Single: case Shuffle: case Volume_Up: case Volume_Down: default: break; } } // Pre-emptively "overflow" and run the OVF ISR body early if a press is in // progress and likely to overflow within the masked window. This maintains: // - a rough absolute time for the pulse train // - peripheral WO1 toggles and mic_ntoggles kept in sync // - maximum frame duration is ~15ms, "early" OVF remains within acceptable // ranges for either high/low pulses // Caller (phy_guard_enter) guarantees interrupts are disabled. void media_sync_during_guard() { if (mic_ntoggles && TCA0.SINGLE.CNT >= (TCA0.SINGLE.CMP0 - close_thresh)) mic_timer_isr_body(true); } void media_init() { // PB1 needs to be set as an output for TCA0 to set the level PORTB.DIRSET = PIN1_bm; // Experimentally, a press should be ~100ms; multiple presses can be separated // by the same ~100ms (but separate pulse trains need more separation to // remain distinct) TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1024_gc; // In frequency (FRQ) mode, channel N compare match triggers "UPDATE" // When CMPnEN is set, TCA0 has control of the output level for the channel's // pin, and UPDATE toggles the level // Channel 1 controls WO1, which is mapped to PB1 TCA0.SINGLE.CTRLB = TCA_SINGLE_WGMODE_FRQ_gc | TCA_SINGLE_CMP1EN_bm; TCA0.SINGLE.CTRLC = 0; // Preset WO1 level low just to be sure mic_ntoggles = 0; // toggle WO1 ~immediately after each period start; should go low => high TCA0.SINGLE.CMP1 = 2; TCA0.SINGLE.INTFLAGS = TCA_SINGLE_OVF_bm; // Clear OVF flag just in case TCA0.SINGLE.INTCTRL = 0; }