From fee74e05c8cf659f62711d39bebe5a8943686196 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Wed, 17 Jun 2026 20:01:21 -0700 Subject: [PATCH] Add headset function control --- CMakeLists.txt | 6 +-- src/avclandrv.c | 127 +++++++++++++++++++++++++++++++++++++++++++++--- src/avclandrv.h | 9 ++-- src/sniffer.c | 24 ++++++--- 4 files changed, 145 insertions(+), 21 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c830ba5..12f1bf9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,9 +60,9 @@ set_property(CACHE USART_RXMODE PROPERTY STRINGS set(CMAKE_C_STANDARD 23) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) -set(CMAKE_C_FLAGS_RELEASE "-Wall -Wswitch-enum -Werror -Os") -set(CMAKE_C_FLAGS_RELWITHDEBINFO "-Wall -Wswitch-enum -Werror -O2 -save-temps -g -gdwarf-3 -gstrict-dwarf") -set(CMAKE_C_FLAGS_DEBUG "-Wall -Wswitch-enum -Werror -fanalyzer -O0 -save-temps -g -gdwarf-3 -gstrict-dwarf") +add_compile_options( + -Wall -Wswitch-enum -Werror + "$<$:-fanalyzer>") if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS "13") diff --git a/src/avclandrv.c b/src/avclandrv.c index f2aeeec..82f0adc 100644 --- a/src/avclandrv.c +++ b/src/avclandrv.c @@ -112,7 +112,7 @@ #define READING_NBITS GPIOR2 #define READING_PARITY GPIOR3 -#ifdef SOFTWARE_DEBUG +#ifndef NDEBUG #define TCB_CNTMODE TCB_CNTMODE_FRQPW_gc #else #define TCB_CNTMODE TCB_CNTMODE_PW_gc @@ -128,7 +128,7 @@ uint8_t *cd_Time_Sec; cd_modes CD_Mode; -#ifdef SOFTWARE_DEBUG +#ifndef NDEBUG volatile uint8_t pulse_count = 0; volatile uint16_t period = 0; #endif @@ -164,11 +164,32 @@ static const uint8_t cdloading_resp[] = {dev_CD_CHANGER, 0x01, 0x02}; -/* Disable serial and periodic interrupts during AVCLAN reads. - Not using cli() because AVCLAN reads depend on other interrupts. */ +// pending WO1 toggles (even); signed to avoid underflows from a stray OVF +static volatile int8_t mic_ntoggles = 0; + +// 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 +// (~500 ms) so consecutive presses stay distinct +constexpr uint16_t mic_press_ticks = (uint16_t)((F_CPU / 1024UL) / 10UL); +constexpr uint16_t mic_quiet_ticks = (uint16_t)((F_CPU / 1024UL) / 2UL); + +/* Disable non-read related interrupts (USART RX, PIT, TCA) during AVCLAN reads. + */ static inline void stopEvent() { RTC.PITINTCTRL &= ~(1 << RTC_PI_bp); USART0.CTRLA &= ~(1 << USART_RXCIE_bp); + + // WO1 toggles don't depend on OVF interrupt, but the OVF interrupt *DOES* + // count the toggles So, disabling the OVF interrupt alone is insufficient, + // we must also disable the timer + TCA0.SINGLE.INTCTRL &= ~TCA_SINGLE_OVF_bm; + + // Target pulse length is ~40-150ms, with interval between pulses of + // ~100-200ms + // 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; } // Re-enable serial and periodic interrupts. @@ -176,6 +197,13 @@ static inline void startEvent() { if (AVCLAN_isPlaying()) // Reenable PIT interrupt if currently playing RTC.PITINTCTRL |= (1 << RTC_PI_bp); USART0.CTRLA |= (1 << USART_RXCIE_bp); + // 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; + } } // clang-format off @@ -221,9 +249,66 @@ void AVCLAN_muteDevice(bool mute) { } } +#ifndef NDEBUG +// Toggle PB1 and return its new level. +bool AVCLAN_micToggle() { + // 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 AVCLAN_isMediaFunctioning() { 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 counts phases, stretches the final LOW phase into a quiet gap, and +// stops the timer on the last one. +ISR(TCA0_OVF_vect) { + TCA0.SINGLE.INTFLAGS = TCA_SINGLE_OVF_bm; + if (--mic_ntoggles == 1) { + // Stretch final phase to a ~500 ms idle-low so back-to-back presses stay + // distinct + TCA0.SINGLE.CMP0 = mic_quiet_ticks; + } else if (mic_ntoggles <= 0) { + mic_ntoggles = 0; // clamp to avoid perma-lockout in mic_pulse + TCA0.SINGLE.CTRLA &= ~TCA_SINGLE_ENABLE_bm; + } +} + +// Emulate a single play/pause button press on the source device. +void AVCLAN_micPlayPause() { mic_pulse(1); } + +// Emulate a skip-forward button press: H / L / H. +void AVCLAN_micSkip() { mic_pulse(3); } + // Sets CD_mode to play and resets timer count (so that the next interrupt is in // 1 sec) -void AVCLAN_startPlaying() { +static void AVCLAN_startPlaying() { + AVCLAN_micPlayPause(); CD_Mode = stPlay; ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { loop_until_bit_is_clear(RTC_PITSTATUS, RTC_CNTBUSY_bp); @@ -234,7 +319,8 @@ void AVCLAN_startPlaying() { // Sets CD_mode to play and resets timer count (so that the next interrupt is in // 1 sec) -void AVCLAN_stopPlaying() { +static void AVCLAN_stopPlaying() { + AVCLAN_micPlayPause(); CD_Mode = stStop; RTC.PITINTCTRL &= ~(1 << RTC_PI_bp); } @@ -256,6 +342,26 @@ void AVCLAN_init() { EVSYS.ASYNCCH0 = EVSYS_ASYNCCH0_AC2_OUT_gc; EVSYS.ASYNCUSER0 = EVSYS_ASYNCUSER0_ASYNCCH0_gc; // USER0 is TCB0 + // 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 + + // 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; + // TCB0 for read bit timing TCB0.CTRLB = TCB_CNTMODE; TCB0.INTCTRL = TCB_CAPT_bm; @@ -467,7 +573,7 @@ avclan_bit_t AVCLAN_sendbyte(const uint8_t *byte) { } ISR(TCB0_INT_vect) { -#ifdef SOFTWARE_DEBUG +#ifndef NDEBUG pulse_count++; period = TCB0.CNT; #endif @@ -1044,6 +1150,7 @@ response_t AVCLAN_handleframe(const AVCLAN_frame_t *in, AVCLAN_frame_t *out) { respond = r_Handled; break; case PACK3(dev_CMD_SW, dev_CD_CHANGER, Track_Seek_Up): + AVCLAN_micSkip(); cd_status.state = cd_SEEKING_TRACK; if (*cd_Track < 98) ++*cd_Track; @@ -1347,7 +1454,11 @@ void AVCLAN_normalizeState() { // } } -#ifdef SOFTWARE_DEBUG +#ifndef NDEBUG + // Only used immediately below + #define XSTR(x) #x + #define STR(x) XSTR(x) + uint16_t pulses[100]; uint16_t periods[100]; diff --git a/src/avclandrv.h b/src/avclandrv.h index e85f178..81ca7b8 100644 --- a/src/avclandrv.h +++ b/src/avclandrv.h @@ -222,11 +222,12 @@ void AVCLAN_incrementTime(); void AVCLAN_setTime(uint8_t mins, uint8_t secs); void AVCLAN_normalizeState(); -#ifdef SOFTWARE_DEBUG +#ifndef NDEBUG +bool AVCLAN_micToggle(); +void AVCLAN_micPlayPause(); +void AVCLAN_micSkip(); +bool AVCLAN_isMediaFunctioning(); void AVCLan_Measure(); #endif -#ifdef HARDWARE_DEBUG -void SetHighLow(); -#endif #endif // __AVCLANDRV_H diff --git a/src/sniffer.c b/src/sniffer.c index 3c9bf19..fae3080 100644 --- a/src/sniffer.c +++ b/src/sniffer.c @@ -249,7 +249,20 @@ int main() { } break; -#ifdef SOFTWARE_DEBUG +#ifndef NDEBUG + case 'g': AVCLAN_micToggle(); break; + case 'p': + RS232_Print("Play/pause begin\n"); + AVCLAN_micPlayPause(); + while (AVCLAN_isMediaFunctioning()) {} + RS232_Print("Play/pause end\n"); + break; + case 's': + RS232_Print("Skip begin\n"); + AVCLAN_micSkip(); + while (AVCLAN_isMediaFunctioning()) {} + RS232_Print("Skip end\n"); + break; case 'M': AVCLan_Measure(); break; #endif @@ -403,12 +416,11 @@ void print_help() { "k - Toggle character echo\n" "E - Beep\n" "P - Play\n" -#ifdef SOFTWARE_DEBUG +#ifndef NDEBUG + "g - Toggle MIC_CONTROL high/low\n" + "p - MIC play/pause pulse\n" + "s - MIC skip pulse\n" "M - Measure bit-timing (pulse-widths and periods)\n" -#endif -#ifdef HARDWARE_DEBUG - "1 - Hold High/low\n" - "E - Print line status\n" #endif "? - Print this message\n"); }