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
+119 -8
View File
@@ -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];
+5 -4
View File
@@ -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
+18 -6
View File
@@ -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");
}