Add headset function control

This commit is contained in:
Allen Hill
2026-06-17 20:01:21 -07:00
parent e8082fa249
commit fee74e05c8
4 changed files with 145 additions and 21 deletions
+3 -3
View File
@@ -60,9 +60,9 @@ set_property(CACHE USART_RXMODE PROPERTY STRINGS
set(CMAKE_C_STANDARD 23) set(CMAKE_C_STANDARD 23)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_C_FLAGS_RELEASE "-Wall -Wswitch-enum -Werror -Os") add_compile_options(
set(CMAKE_C_FLAGS_RELWITHDEBINFO "-Wall -Wswitch-enum -Werror -O2 -save-temps -g -gdwarf-3 -gstrict-dwarf") -Wall -Wswitch-enum -Werror
set(CMAKE_C_FLAGS_DEBUG "-Wall -Wswitch-enum -Werror -fanalyzer -O0 -save-temps -g -gdwarf-3 -gstrict-dwarf") "$<$<CONFIG:Debug>:-fanalyzer>")
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS "13") if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS "13")
+119 -8
View File
@@ -112,7 +112,7 @@
#define READING_NBITS GPIOR2 #define READING_NBITS GPIOR2
#define READING_PARITY GPIOR3 #define READING_PARITY GPIOR3
#ifdef SOFTWARE_DEBUG #ifndef NDEBUG
#define TCB_CNTMODE TCB_CNTMODE_FRQPW_gc #define TCB_CNTMODE TCB_CNTMODE_FRQPW_gc
#else #else
#define TCB_CNTMODE TCB_CNTMODE_PW_gc #define TCB_CNTMODE TCB_CNTMODE_PW_gc
@@ -128,7 +128,7 @@ uint8_t *cd_Time_Sec;
cd_modes CD_Mode; cd_modes CD_Mode;
#ifdef SOFTWARE_DEBUG #ifndef NDEBUG
volatile uint8_t pulse_count = 0; volatile uint8_t pulse_count = 0;
volatile uint16_t period = 0; volatile uint16_t period = 0;
#endif #endif
@@ -164,11 +164,32 @@ static const uint8_t cdloading_resp[] = {dev_CD_CHANGER,
0x01, 0x01,
0x02}; 0x02};
/* Disable serial and periodic interrupts during AVCLAN reads. // pending WO1 toggles (even); signed to avoid underflows from a stray OVF
Not using cli() because AVCLAN reads depend on other interrupts. */ 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() { static inline void stopEvent() {
RTC.PITINTCTRL &= ~(1 << RTC_PI_bp); RTC.PITINTCTRL &= ~(1 << RTC_PI_bp);
USART0.CTRLA &= ~(1 << USART_RXCIE_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. // Re-enable serial and periodic interrupts.
@@ -176,6 +197,13 @@ static inline void startEvent() {
if (AVCLAN_isPlaying()) // Reenable PIT interrupt if currently playing if (AVCLAN_isPlaying()) // Reenable PIT interrupt if currently playing
RTC.PITINTCTRL |= (1 << RTC_PI_bp); RTC.PITINTCTRL |= (1 << RTC_PI_bp);
USART0.CTRLA |= (1 << USART_RXCIE_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 // 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 // Sets CD_mode to play and resets timer count (so that the next interrupt is in
// 1 sec) // 1 sec)
void AVCLAN_startPlaying() { static void AVCLAN_startPlaying() {
AVCLAN_micPlayPause();
CD_Mode = stPlay; CD_Mode = stPlay;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
loop_until_bit_is_clear(RTC_PITSTATUS, RTC_CNTBUSY_bp); 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 // Sets CD_mode to play and resets timer count (so that the next interrupt is in
// 1 sec) // 1 sec)
void AVCLAN_stopPlaying() { static void AVCLAN_stopPlaying() {
AVCLAN_micPlayPause();
CD_Mode = stStop; CD_Mode = stStop;
RTC.PITINTCTRL &= ~(1 << RTC_PI_bp); RTC.PITINTCTRL &= ~(1 << RTC_PI_bp);
} }
@@ -256,6 +342,26 @@ void AVCLAN_init() {
EVSYS.ASYNCCH0 = EVSYS_ASYNCCH0_AC2_OUT_gc; EVSYS.ASYNCCH0 = EVSYS_ASYNCCH0_AC2_OUT_gc;
EVSYS.ASYNCUSER0 = EVSYS_ASYNCUSER0_ASYNCCH0_gc; // USER0 is TCB0 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 for read bit timing
TCB0.CTRLB = TCB_CNTMODE; TCB0.CTRLB = TCB_CNTMODE;
TCB0.INTCTRL = TCB_CAPT_bm; TCB0.INTCTRL = TCB_CAPT_bm;
@@ -467,7 +573,7 @@ avclan_bit_t AVCLAN_sendbyte(const uint8_t *byte) {
} }
ISR(TCB0_INT_vect) { ISR(TCB0_INT_vect) {
#ifdef SOFTWARE_DEBUG #ifndef NDEBUG
pulse_count++; pulse_count++;
period = TCB0.CNT; period = TCB0.CNT;
#endif #endif
@@ -1044,6 +1150,7 @@ response_t AVCLAN_handleframe(const AVCLAN_frame_t *in, AVCLAN_frame_t *out) {
respond = r_Handled; respond = r_Handled;
break; break;
case PACK3(dev_CMD_SW, dev_CD_CHANGER, Track_Seek_Up): case PACK3(dev_CMD_SW, dev_CD_CHANGER, Track_Seek_Up):
AVCLAN_micSkip();
cd_status.state = cd_SEEKING_TRACK; cd_status.state = cd_SEEKING_TRACK;
if (*cd_Track < 98) if (*cd_Track < 98)
++*cd_Track; ++*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 pulses[100];
uint16_t periods[100]; uint16_t periods[100];
+5 -4
View File
@@ -222,11 +222,12 @@ void AVCLAN_incrementTime();
void AVCLAN_setTime(uint8_t mins, uint8_t secs); void AVCLAN_setTime(uint8_t mins, uint8_t secs);
void AVCLAN_normalizeState(); void AVCLAN_normalizeState();
#ifdef SOFTWARE_DEBUG #ifndef NDEBUG
bool AVCLAN_micToggle();
void AVCLAN_micPlayPause();
void AVCLAN_micSkip();
bool AVCLAN_isMediaFunctioning();
void AVCLan_Measure(); void AVCLan_Measure();
#endif #endif
#ifdef HARDWARE_DEBUG
void SetHighLow();
#endif
#endif // __AVCLANDRV_H #endif // __AVCLANDRV_H
+18 -6
View File
@@ -249,7 +249,20 @@ int main() {
} }
break; 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; case 'M': AVCLan_Measure(); break;
#endif #endif
@@ -403,12 +416,11 @@ void print_help() {
"k - Toggle character echo\n" "k - Toggle character echo\n"
"E - Beep\n" "E - Beep\n"
"P - Play\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" "M - Measure bit-timing (pulse-widths and periods)\n"
#endif
#ifdef HARDWARE_DEBUG
"1 - Hold High/low\n"
"E - Print line status\n"
#endif #endif
"? - Print this message\n"); "? - Print this message\n");
} }