* [RFC PATCH 2/2] ASoC: cs42l42: support Apple headset remotes on CS42L83
2026-09-17 7:23 [RFC PATCH 0/2] ASoC: CS42L83 Apple headset remote support Ryan Murray
2026-09-17 7:23 ` [RFC PATCH 1/2] ASoC: cs42l42: handle jack interrupt I/O and resume errors Ryan Murray
@ 2026-09-17 7:23 ` Ryan Murray
1 sibling, 0 replies; 4+ messages in thread
From: Ryan Murray @ 2026-09-17 7:23 UTC (permalink / raw)
To: patches, linux-sound
Cc: david.rhodes, rf, lgirdwood, broonie, perex, tiwai,
james.schulman, povik+lin, asahi, linux-kernel
Apple wired headset volume buttons use a microphone-bias signalling
protocol instead of the resistor levels handled by the analogue detector.
The CS42L83 can identify these remotes and decode their button indications
without an audio stream or an external serial clock.
After CTIA or OMTP detection, perform a bounded identification exchange on
CS42L83. Wait for the receive acknowledgment rather than the earlier
transmit-complete indication, which can expose an incomplete identifier.
Validate all three identifier symbols before enabling remote reception.
Restore the analogue detector on timeout, a short, an invalid identifier,
or a register-access error.
Preserve the receiver's raw volume indications before applying the
analogue interrupt masks. Report volume indications as press/release
pairs and use short-detect transitions for the centre button. Discard
pre-identification button state and restore the saved detector settings
on unplug, redetection and suspend under the existing IRQ mutex.
Select this capability from the CS42L83 device identity. No board-specific
property, clock provider, work item or extra power reference is needed.
The CS42L42 continues to use its analogue detector.
The undocumented CS42L83 sequence was independently implemented from
register-level reverse engineering and validated on a J700 with a B0
codec and Apple wired earbuds. Other boards and codec revisions have not
been hardware-tested. The machine driver must expose the appropriate
SND_JACK_BTN capabilities and key mappings.
Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
---
sound/soc/codecs/cs42l42.c | 263 ++++++++++++++++++++++++++++++++++++-
sound/soc/codecs/cs42l42.h | 15 +++
2 files changed, 276 insertions(+), 2 deletions(-)
diff --git a/sound/soc/codecs/cs42l42.c b/sound/soc/codecs/cs42l42.c
index 2d1b45ea1..c510ceae8 100644
--- a/sound/soc/codecs/cs42l42.c
+++ b/sound/soc/codecs/cs42l42.c
@@ -15,6 +15,7 @@
#include <linux/types.h>
#include <linux/init.h>
#include <linux/delay.h>
+#include <linux/jiffies.h>
#include <linux/regmap.h>
#include <linux/slab.h>
#include <linux/acpi.h>
@@ -1257,6 +1258,240 @@ static void cs42l42_manual_hs_type_detect(struct cs42l42_private *cs42l42)
(CS42L42_HSDET_COMP2_LVL_DEFAULT << CS42L42_HSDET_COMP2_LVL_SHIFT));
}
+/*
+ * CS42L83 receives Apple headset remote data over the microphone-bias line.
+ * The receiver operates on the internal oscillator, including while audio
+ * is idle. These control values and status bits are specific to CS42L83.
+ */
+#define CS42L83_REMOTE_TX_START BIT(7)
+#define CS42L83_REMOTE_RX_ACK BIT(3)
+#define CS42L83_REMOTE_BUTTON_IRQ BIT(5)
+#define CS42L83_REMOTE_BUTTONS GENMASK(4, 0)
+#define CS42L83_REMOTE_VOLUME_DOWN BIT(0)
+#define CS42L83_REMOTE_VOLUME_UP BIT(1)
+
+/* Complete receiver control values; these are not CS42L42 detect modes. */
+#define CS42L83_REMOTE_BIAS_RESET 0x03
+#define CS42L83_REMOTE_BIAS_IDENTIFY 0x9e
+#define CS42L83_REMOTE_BIAS_BUTTONS 0x5f
+#define CS42L83_REMOTE_RX_CONFIG 0x8f
+#define CS42L83_REMOTE_LEVEL_IDENTIFY 0x36
+#define CS42L83_REMOTE_LEVEL_BUTTONS 0x3c
+
+static const unsigned int cs42l83_remote_regs[] = {
+ [CS42L83_REMOTE_SAVED_HSBIAS_SC_AUTOCTL] = CS42L42_HSBIAS_SC_AUTOCTL,
+ [CS42L83_REMOTE_SAVED_WAKE_CTL] = CS42L42_WAKE_CTL,
+ [CS42L83_REMOTE_SAVED_MISC_DET_CTL] = CS42L42_MISC_DET_CTL,
+ [CS42L83_REMOTE_SAVED_MIC_DET_CTL1] = CS42L42_MIC_DET_CTL1,
+ [CS42L83_REMOTE_SAVED_MIC_DET_CTL2] = CS42L42_MIC_DET_CTL2,
+ [CS42L83_REMOTE_SAVED_DET_INT1_MASK] = CS42L42_DET_INT1_MASK,
+ [CS42L83_REMOTE_SAVED_DET_INT2_MASK] = CS42L42_DET_INT2_MASK,
+ [CS42L83_REMOTE_SAVED_HS_BIAS_CTL] = CS42L42_HS_BIAS_CTL,
+ [CS42L83_REMOTE_SAVED_HSDET_CTL2] = CS42L42_HSDET_CTL2,
+};
+
+static void cs42l83_remote_restore(struct cs42l42_private *cs42l42)
+{
+ int i, ret;
+
+ for (i = 0; i < ARRAY_SIZE(cs42l83_remote_regs); i++) {
+ ret = regmap_write(cs42l42->regmap, cs42l83_remote_regs[i],
+ cs42l42->remote_saved[i]);
+ if (ret)
+ dev_warn(cs42l42->dev, "Failed to restore headset detection register %#x: %d\n",
+ cs42l83_remote_regs[i], ret);
+ }
+ cs42l42->remote_active = false;
+}
+
+/*
+ * Hold the bias reference during a bias-mode change and allow it to settle
+ * before starting transmission. Apply the TX bit only after the bias bits.
+ */
+static int cs42l83_remote_set_detect(struct cs42l42_private *cs42l42, unsigned int value)
+{
+ unsigned int old;
+ bool changed;
+ int ret;
+
+ ret = regmap_read(cs42l42->regmap, CS42L42_MISC_DET_CTL, &old);
+ if (ret)
+ return ret;
+ changed = (old & CS42L42_HSBIAS_CTL_MASK) != (value & CS42L42_HSBIAS_CTL_MASK);
+ if (changed) {
+ /* CS42L83 bias transitions require clearing this undocumented field. */
+ ret = regmap_update_bits(cs42l42->regmap, CS42L42_HS_BIAS_CTL, GENMASK(3, 2), 0);
+ if (ret)
+ return ret;
+ ret = regmap_update_bits(cs42l42->regmap, CS42L42_HSDET_CTL2,
+ CS42L42_HSBIAS_REF_MASK, CS42L42_HSBIAS_REF_MASK);
+ if (ret)
+ return ret;
+ }
+ ret = regmap_update_bits(cs42l42->regmap, CS42L42_MISC_DET_CTL,
+ value & CS42L83_REMOTE_TX_START ? 0x7f : 0xff, value);
+ if (ret)
+ return ret;
+ if (!!(old & BIT(2)) != !!(value & BIT(2)))
+ usleep_range(10000, 11000);
+ if (value & CS42L83_REMOTE_TX_START) {
+ ret = regmap_write(cs42l42->regmap, CS42L42_MISC_DET_CTL, value);
+ if (ret)
+ return ret;
+ }
+ if (changed)
+ return regmap_update_bits(cs42l42->regmap, CS42L42_HSDET_CTL2,
+ CS42L42_HSBIAS_REF_MASK, 0);
+ return 0;
+}
+
+/* Called with irq_lock held; every attempt is bounded and has analogue fallback. */
+static void cs42l83_remote_start(struct cs42l42_private *cs42l42)
+{
+ static const struct reg_sequence identify[] = {
+ { CS42L42_DET_INT2_MASK, 0xff },
+ { CS42L42_HSBIAS_SC_AUTOCTL, 0x03 },
+ { CS42L42_WAKE_CTL, CS42L42_M_HP_WAKE_MASK | CS42L42_M_MIC_WAKE_MASK },
+ { CS42L42_MIC_DET_CTL2, CS42L83_REMOTE_RX_CONFIG },
+ };
+ unsigned int int1, int2, status1, status2, symbols, symbol[3];
+ unsigned long deadline;
+ int i, ret;
+
+ if (cs42l42->devid != CS42L83_CHIP_ID ||
+ (cs42l42->hs_type != CS42L42_PLUG_CTIA &&
+ cs42l42->hs_type != CS42L42_PLUG_OMTP))
+ return;
+
+ for (i = 0; i < ARRAY_SIZE(cs42l83_remote_regs); i++) {
+ ret = regmap_read(cs42l42->regmap, cs42l83_remote_regs[i],
+ &cs42l42->remote_saved[i]);
+ if (ret)
+ return;
+ }
+
+ ret = regmap_multi_reg_write(cs42l42->regmap, identify, ARRAY_SIZE(identify));
+ if (ret)
+ goto fallback;
+ /* Reset the bias before applying the identification threshold. */
+ ret = cs42l83_remote_set_detect(cs42l42, CS42L83_REMOTE_BIAS_RESET);
+ if (ret)
+ goto fallback;
+ usleep_range(10000, 11000);
+ ret = regmap_update_bits(cs42l42->regmap, CS42L42_MIC_DET_CTL1,
+ CS42L42_HS_DET_LEVEL_MASK, CS42L83_REMOTE_LEVEL_IDENTIFY);
+ if (ret)
+ goto fallback;
+ ret = regmap_read(cs42l42->regmap, CS42L42_DET_INT_STATUS1, &int1);
+ if (ret)
+ goto fallback;
+ ret = regmap_read(cs42l42->regmap, CS42L42_DET_INT_STATUS2, &int2);
+ if (ret)
+ goto fallback;
+ /* Start identification after the controlled bias rise. */
+ ret = cs42l83_remote_set_detect(cs42l42, CS42L83_REMOTE_BIAS_IDENTIFY);
+ if (ret)
+ goto fallback;
+
+ /*
+ * Bit 4 signals TX completion, before the remote reply is ready. Wait
+ * for RX acknowledgment (bit 3), with a bounded analogue fallback.
+ */
+ msleep(35);
+ deadline = jiffies + msecs_to_jiffies(500);
+ do {
+ ret = regmap_read(cs42l42->regmap, CS42L42_DET_INT_STATUS2, &int2);
+ if (ret)
+ goto fallback;
+ if (int2 & CS42L83_REMOTE_RX_ACK)
+ break;
+ usleep_range(5000, 6000);
+ } while (time_before(jiffies, deadline));
+ if (!(int2 & CS42L83_REMOTE_RX_ACK)) {
+ ret = -ETIMEDOUT;
+ goto fallback;
+ }
+ ret = regmap_read(cs42l42->regmap, CS42L42_DET_INT_STATUS1, &int1);
+ if (ret)
+ goto fallback;
+ ret = regmap_read(cs42l42->regmap, CS42L42_DET_STATUS1, &status1);
+ if (ret)
+ goto fallback;
+ ret = regmap_read(cs42l42->regmap, CS42L42_DET_STATUS2, &status2);
+ if (ret)
+ goto fallback;
+ symbols = (status2 >> 4) | ((status1 & 0x1f) << 4);
+ for (i = 0; i < ARRAY_SIZE(symbol); i++)
+ symbol[i] = (symbols >> (i * 3)) & 7;
+ /* A short or an invalid identifier must retain the analogue detector. */
+ if (status2 & CS42L42_SHORT_TRUE_MASK) {
+ ret = -ENODEV;
+ goto fallback;
+ }
+ for (i = 0; i < ARRAY_SIZE(symbol); i++) {
+ if (symbol[i] < 1 || symbol[i] > 4) {
+ ret = -ENODEV;
+ goto fallback;
+ }
+ }
+
+ /* Use the remote-button threshold only after a valid identification. */
+ ret = regmap_update_bits(cs42l42->regmap, CS42L42_MIC_DET_CTL1,
+ CS42L42_HS_DET_LEVEL_MASK, CS42L83_REMOTE_LEVEL_BUTTONS);
+ if (ret)
+ goto fallback;
+
+ /* The receiver now decodes remote button pulses autonomously. */
+ ret = cs42l83_remote_set_detect(cs42l42, CS42L83_REMOTE_BIAS_BUTTONS);
+ if (ret)
+ goto fallback;
+ msleep(35);
+ ret = regmap_write(cs42l42->regmap, CS42L42_HSBIAS_SC_AUTOCTL,
+ (cs42l42->hs_bias_sense_en << CS42L42_HSBIAS_SENSE_EN_SHIFT) | 3);
+ if (ret)
+ goto fallback;
+ ret = regmap_write(cs42l42->regmap, CS42L42_DET_INT1_MASK,
+ cs42l42->remote_saved[CS42L83_REMOTE_SAVED_DET_INT1_MASK] &
+ ~(cs42l42->hs_bias_sense_en << CS42L42_HSBIAS_SENSE_SHIFT));
+ if (ret)
+ goto fallback;
+ ret = regmap_write(cs42l42->regmap, CS42L42_DET_INT2_MASK,
+ 0xff & ~(CS42L83_REMOTE_BUTTON_IRQ |
+ CS42L42_M_SHORT_DET_MASK | CS42L42_M_SHORT_RLS_MASK));
+ if (ret)
+ goto fallback;
+ cs42l42->remote_active = true;
+ dev_dbg(cs42l42->dev, "Apple headset remote enabled\n");
+ return;
+
+fallback:
+ dev_dbg(cs42l42->dev, "Headset remote identification failed: %d\n", ret);
+ cs42l83_remote_restore(cs42l42);
+}
+
+static void cs42l83_remote_buttons(struct cs42l42_private *cs42l42,
+ unsigned int int1, unsigned int int2)
+{
+ unsigned int buttons = 0;
+
+ /* Volume indications are pulses, without a separate release event. */
+ if (int1 & CS42L83_REMOTE_BUTTONS) {
+ if (int1 & CS42L83_REMOTE_VOLUME_DOWN)
+ buttons |= SND_JACK_BTN_2;
+ if (int1 & CS42L83_REMOTE_VOLUME_UP)
+ buttons |= SND_JACK_BTN_1;
+ if (buttons)
+ snd_soc_jack_report(cs42l42->jack, buttons,
+ SND_JACK_BTN_1 | SND_JACK_BTN_2);
+ snd_soc_jack_report(cs42l42->jack, 0,
+ SND_JACK_BTN_1 | SND_JACK_BTN_2);
+ }
+ if (int2 & CS42L42_M_SHORT_DET_MASK)
+ snd_soc_jack_report(cs42l42->jack, SND_JACK_BTN_0, SND_JACK_BTN_0);
+ if (int2 & CS42L42_M_SHORT_RLS_MASK)
+ snd_soc_jack_report(cs42l42->jack, 0, SND_JACK_BTN_0);
+}
+
static void cs42l42_process_hs_type_detect(struct cs42l42_private *cs42l42)
{
unsigned int hs_det_status;
@@ -1398,6 +1633,9 @@ static void cs42l42_process_hs_type_detect(struct cs42l42_private *cs42l42)
static void cs42l42_init_hs_type_detect(struct cs42l42_private *cs42l42)
{
+ if (cs42l42->remote_active)
+ cs42l83_remote_restore(cs42l42);
+
/* Mask tip sense interrupts */
regmap_update_bits(cs42l42->regmap,
CS42L42_TSRS_PLUG_INT_MASK,
@@ -1485,6 +1723,9 @@ static void cs42l42_init_hs_type_detect(struct cs42l42_private *cs42l42)
static void cs42l42_cancel_hs_type_detect(struct cs42l42_private *cs42l42)
{
+ if (cs42l42->remote_active)
+ cs42l83_remote_restore(cs42l42);
+
/* Mask button detect interrupts */
regmap_update_bits(cs42l42->regmap,
CS42L42_DET_INT2_MASK,
@@ -1664,6 +1905,7 @@ irqreturn_t cs42l42_irq_thread(int irq, void *data)
struct cs42l42_private *cs42l42 = (struct cs42l42_private *)data;
unsigned int stickies[12];
unsigned int masks[12];
+ unsigned int raw_remote_buttons = 0;
unsigned int current_plug_status;
unsigned int current_button_status;
unsigned int i;
@@ -1686,8 +1928,11 @@ irqreturn_t cs42l42_irq_thread(int irq, void *data)
&masks[i]);
if (ret)
goto out_error;
- stickies[i] = stickies[i] & (~masks[i]) &
- irq_params_table[i].mask;
+ if (cs42l42->remote_active && i == 6)
+ raw_remote_buttons = stickies[i] & CS42L83_REMOTE_BUTTONS;
+ stickies[i] &= ~masks[i];
+ if (!(cs42l42->remote_active && i == 7))
+ stickies[i] &= irq_params_table[i].mask;
}
/* Read tip sense status before handling type detect */
@@ -1709,6 +1954,13 @@ irqreturn_t cs42l42_irq_thread(int irq, void *data)
if ((~masks[5]) & irq_params_table[5].mask) {
if (stickies[5] & CS42L42_HSDET_AUTO_DONE_MASK) {
cs42l42_process_hs_type_detect(cs42l42);
+ if (cs42l42->devid == CS42L83_CHIP_ID) {
+ cs42l83_remote_start(cs42l42);
+ /* Do not interpret pre-identification events as keys. */
+ current_button_status = 0;
+ stickies[7] = 0;
+ raw_remote_buttons = 0;
+ }
switch (cs42l42->hs_type) {
case CS42L42_PLUG_CTIA:
case CS42L42_PLUG_OMTP:
@@ -1759,6 +2011,11 @@ irqreturn_t cs42l42_irq_thread(int irq, void *data)
}
}
+ if (cs42l42->remote_active && cs42l42->plug_state == CS42L42_TS_PLUG) {
+ cs42l83_remote_buttons(cs42l42, raw_remote_buttons, stickies[7]);
+ return IRQ_HANDLED;
+ }
+
/* Check button detect status */
if (cs42l42->plug_state == CS42L42_TS_PLUG && ((~masks[7]) & irq_params_table[7].mask)) {
if (!(current_button_status &
@@ -2169,6 +2426,8 @@ int cs42l42_suspend(struct device *dev)
* is shared.
*/
scoped_guard(mutex, &cs42l42->irq_lock) {
+ if (cs42l42->remote_active)
+ cs42l83_remote_restore(cs42l42);
cs42l42->suspended = true;
/* Save register values that will be overwritten by shutdown sequence */
diff --git a/sound/soc/codecs/cs42l42.h b/sound/soc/codecs/cs42l42.h
index 3d85ebc59..1065d8cd6 100644
--- a/sound/soc/codecs/cs42l42.h
+++ b/sound/soc/codecs/cs42l42.h
@@ -24,6 +24,19 @@
#include <sound/soc-component.h>
#include <sound/soc-dai.h>
+enum cs42l83_remote_saved_reg {
+ CS42L83_REMOTE_SAVED_HSBIAS_SC_AUTOCTL,
+ CS42L83_REMOTE_SAVED_WAKE_CTL,
+ CS42L83_REMOTE_SAVED_MISC_DET_CTL,
+ CS42L83_REMOTE_SAVED_MIC_DET_CTL1,
+ CS42L83_REMOTE_SAVED_MIC_DET_CTL2,
+ CS42L83_REMOTE_SAVED_DET_INT1_MASK,
+ CS42L83_REMOTE_SAVED_DET_INT2_MASK,
+ CS42L83_REMOTE_SAVED_HS_BIAS_CTL,
+ CS42L83_REMOTE_SAVED_HSDET_CTL2,
+ CS42L83_REMOTE_NUM_SAVED_REGS,
+};
+
struct cs42l42_private {
struct regmap *regmap;
struct device *dev;
@@ -51,6 +64,8 @@ struct cs42l42_private {
u8 hs_bias_ramp_time;
u8 hs_bias_sense_en;
u8 stream_use;
+ bool remote_active;
+ unsigned int remote_saved[CS42L83_REMOTE_NUM_SAVED_REGS];
bool hp_adc_up_pending;
bool suspended;
bool sdw_waiting_first_unattach;
^ permalink raw reply [flat|nested] 4+ messages in thread