Properly fix headset functions when there's bus activity

This commit is contained in:
Allen Hill
2026-06-20 20:28:53 -07:00
parent 59b8bd0a54
commit 355b01964f
+44 -29
View File
@@ -134,11 +134,18 @@ static volatile uint16_t pulsewidth;
// pending WO1 toggles (even); signed to avoid underflows from a stray OVF // pending WO1 toggles (even); signed to avoid underflows from a stray OVF
static volatile int8_t mic_ntoggles = 0; 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. // 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_quiet_ticks // A press phase is ~100 ms; the final LOW phase is stretched to
// (~500 ms) so consecutive presses stay distinct // 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_press_ticks = (uint16_t)((F_CPU / 1024UL) / 10UL);
static constexpr uint16_t mic_quiet_ticks = (uint16_t)((F_CPU / 1024UL) / 2UL); 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 #ifndef NDEBUG
// Toggle PB1 and return its new level. // Toggle PB1 and return its new level.
@@ -176,20 +183,38 @@ static void mic_pulse(uint8_t nphases) {
} }
} }
// OVF ISR counts phases, stretches the final LOW phase into a quiet gap, and // OVF ISR body function:
// stops the timer on the last one. // - counts phases
ISR(TCA0_OVF_vect) { // - stretches the final LOW phase as a refractory period, to keep separate
TCA0.SINGLE.INTFLAGS = TCA_SINGLE_OVF_bm; // pulse trains distinct
// - stops the timer after the last phase
static inline void mic_timer_isr_body(bool is_early) {
if (--mic_ntoggles == 1) { if (--mic_ntoggles == 1) {
// Stretch final phase to a ~500 ms idle-low so back-to-back presses stay // Stretch final phase to a ~333 ms idle-low so back-to-back presses stay
// distinct // distinct
TCA0.SINGLE.CMP0 = mic_quiet_ticks; // Invariant: mic_refractory_period > mic_press_ticks, so counter can never
// silently wrap
TCA0.SINGLE.CMP0 = mic_refractory_period;
} else if (mic_ntoggles <= 0) { } else if (mic_ntoggles <= 0) {
mic_ntoggles = 0; // clamp to avoid perma-lockout in mic_pulse
TCA0.SINGLE.CTRLA &= ~TCA_SINGLE_ENABLE_bm; 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 single play/pause button press on the source device. // Emulate a single play/pause button press on the source device.
void AVCLAN_micPlayPause() { mic_pulse(1); } void AVCLAN_micPlayPause() { mic_pulse(1); }
@@ -203,18 +228,14 @@ static inline void stopEvent() {
RTC.INTCTRL &= ~RTC_OVF_bm; RTC.INTCTRL &= ~RTC_OVF_bm;
USART0.CTRLA &= ~USART_RXCIE_bm; USART0.CTRLA &= ~USART_RXCIE_bm;
// WO1 toggles don't depend on OVF interrupt, but the OVF interrupt *DOES* // Pre-emptively "overflow" and run ISR early if expected/possible to
// count the toggles // overflow within this frame. This maintains:
// So, disabling the OVF interrupt alone is insufficient, we must also // - a rough absolute time for the pulse train
// disable the timer // - peripheral WO1 toggles and mic_ntoggles kept in sync
TCA0.SINGLE.INTCTRL &= ~TCA_SINGLE_OVF_bm; // - maximum frame duration is ~15ms, "early" OVF remains within acceptable
// ranges for either high/low pulses
// Target pulse length is ~40-150ms, with interval between pulses of if (mic_ntoggles && TCA0.SINGLE.CNT >= (TCA0.SINGLE.CMP0 - close_thresh))
// ~100-200ms mic_timer_isr_body(true);
// The longest AVCLAN frame duration is ~15ms, so stretching either phase
// (high/low) won't exceed the allowable ranges for pulses (high) or
// intervals (low)
TCA0.SINGLE.CTRLA &= ~TCA_SINGLE_ENABLE_bm;
} }
} }
@@ -224,13 +245,6 @@ static inline void startEvent() {
if (AVCLAN_isPlaying()) // Reenable status interrupt if currently playing if (AVCLAN_isPlaying()) // Reenable status interrupt if currently playing
RTC.INTCTRL |= RTC_OVF_bm; RTC.INTCTRL |= RTC_OVF_bm;
USART0.CTRLA |= USART_RXCIE_bm; USART0.CTRLA |= USART_RXCIE_bm;
// Resume/re-arm mic-press timer only while a press is in progress.
// Enable before unmasking so a pending final-phase OVF lands after
// re-enable and the ISR's own ENABLE clear wins (no spurious extra period).
if (mic_ntoggles) {
TCA0.SINGLE.CTRLA |= TCA_SINGLE_ENABLE_bm;
TCA0.SINGLE.INTCTRL |= TCA_SINGLE_OVF_bm;
}
} }
} }
@@ -350,6 +364,7 @@ void AVCLAN_init() {
// Channel 1 controls WO1, which is mapped to PB1 // Channel 1 controls WO1, which is mapped to PB1
TCA0.SINGLE.CTRLB = TCA_SINGLE_WGMODE_FRQ_gc | TCA_SINGLE_CMP1EN_bm; TCA0.SINGLE.CTRLB = TCA_SINGLE_WGMODE_FRQ_gc | TCA_SINGLE_CMP1EN_bm;
TCA0.SINGLE.CTRLC = 0; // Preset WO1 level low just to be sure 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 // toggle WO1 ~immediately after each period start; should go low => high
TCA0.SINGLE.CMP1 = 2; TCA0.SINGLE.CMP1 = 2;