ASoC: nau8821: Fix IRQ handling and improve jack

Merge series from Cristian Ciocaltea <cristian.ciocaltea@collabora.com>:

This patch series addresses a set of issues in the Nuvoton NAU88L21
audio codec driver related to interrupt handling and jack hotplug
detection reliability.

The changes focus on:

* Eliminating race conditions between jack insertion and ejection events
* Ensuring interrupts are consistently and correctly cleared before
  unmasking
* Introducing a DMI-based quirk to bypass the jack debounce circuit on
  Valve Steam Deck, improving detection accuracy under stress
* Improving robustness of the IRQ handler by avoiding unnecessary
  blocking operations

The series has been tested on affected hardware to verify correct
behavior during repeated and rapid jack hotplug cycles.
This commit is contained in:
Mark Brown 2025-10-16 12:41:35 +01:00
commit f1a450f9e1
No known key found for this signature in database
GPG Key ID: 24D68B725D5487D0
2 changed files with 83 additions and 52 deletions

View File

@ -26,7 +26,8 @@
#include <sound/tlv.h>
#include "nau8821.h"
#define NAU8821_JD_ACTIVE_HIGH BIT(0)
#define NAU8821_QUIRK_JD_ACTIVE_HIGH BIT(0)
#define NAU8821_QUIRK_JD_DB_BYPASS BIT(1)
static int nau8821_quirk;
static int quirk_override = -1;
@ -1021,12 +1022,17 @@ static bool nau8821_is_jack_inserted(struct regmap *regmap)
return active_high == is_high;
}
static void nau8821_int_status_clear_all(struct regmap *regmap)
static void nau8821_irq_status_clear(struct regmap *regmap, int active_irq)
{
int active_irq, clear_irq, i;
int clear_irq, i;
/* Reset the intrruption status from rightmost bit if the corres-
* ponding irq event occurs.
if (active_irq) {
regmap_write(regmap, NAU8821_R11_INT_CLR_KEY_STATUS, active_irq);
return;
}
/* Reset the interruption status from rightmost bit if the
* corresponding irq event occurs.
*/
regmap_read(regmap, NAU8821_R10_IRQ_STATUS, &active_irq);
for (i = 0; i < NAU8821_REG_DATA_LEN; i++) {
@ -1052,20 +1058,24 @@ static void nau8821_eject_jack(struct nau8821 *nau8821)
snd_soc_component_disable_pin(component, "MICBIAS");
snd_soc_dapm_sync(dapm);
/* Clear all interruption status */
nau8821_int_status_clear_all(regmap);
/* Enable the insertion interruption, disable the ejection inter-
* ruption, and then bypass de-bounce circuit.
*/
/* Disable & mask both insertion & ejection IRQs */
regmap_update_bits(regmap, NAU8821_R12_INTERRUPT_DIS_CTRL,
NAU8821_IRQ_EJECT_DIS | NAU8821_IRQ_INSERT_DIS,
NAU8821_IRQ_EJECT_DIS);
/* Mask unneeded IRQs: 1 - disable, 0 - enable */
NAU8821_IRQ_INSERT_DIS | NAU8821_IRQ_EJECT_DIS,
NAU8821_IRQ_INSERT_DIS | NAU8821_IRQ_EJECT_DIS);
regmap_update_bits(regmap, NAU8821_R0F_INTERRUPT_MASK,
NAU8821_IRQ_EJECT_EN | NAU8821_IRQ_INSERT_EN,
NAU8821_IRQ_EJECT_EN);
NAU8821_IRQ_INSERT_EN | NAU8821_IRQ_EJECT_EN,
NAU8821_IRQ_INSERT_EN | NAU8821_IRQ_EJECT_EN);
/* Clear all interruption status */
nau8821_irq_status_clear(regmap, 0);
/* Enable & unmask the insertion IRQ */
regmap_update_bits(regmap, NAU8821_R12_INTERRUPT_DIS_CTRL,
NAU8821_IRQ_INSERT_DIS, 0);
regmap_update_bits(regmap, NAU8821_R0F_INTERRUPT_MASK,
NAU8821_IRQ_INSERT_EN, 0);
/* Bypass de-bounce circuit */
regmap_update_bits(regmap, NAU8821_R0D_JACK_DET_CTRL,
NAU8821_JACK_DET_DB_BYPASS, NAU8821_JACK_DET_DB_BYPASS);
@ -1089,22 +1099,17 @@ static void nau8821_eject_jack(struct nau8821 *nau8821)
NAU8821_IRQ_KEY_RELEASE_DIS |
NAU8821_IRQ_KEY_PRESS_DIS);
}
}
static void nau8821_jdet_work(struct work_struct *work)
{
struct nau8821 *nau8821 =
container_of(work, struct nau8821, jdet_work);
container_of(work, struct nau8821, jdet_work.work);
struct snd_soc_dapm_context *dapm = nau8821->dapm;
struct snd_soc_component *component = snd_soc_dapm_to_component(dapm);
struct regmap *regmap = nau8821->regmap;
int jack_status_reg, mic_detected, event = 0, event_mask = 0;
snd_soc_component_force_enable_pin(component, "MICBIAS");
snd_soc_dapm_sync(dapm);
msleep(20);
regmap_read(regmap, NAU8821_R58_I2C_DEVICE_ID, &jack_status_reg);
mic_detected = !(jack_status_reg & NAU8821_KEYDET);
if (mic_detected) {
@ -1137,6 +1142,7 @@ static void nau8821_jdet_work(struct work_struct *work)
snd_soc_component_disable_pin(component, "MICBIAS");
snd_soc_dapm_sync(dapm);
}
event_mask |= SND_JACK_HEADSET;
snd_soc_jack_report(nau8821->jack, event, event_mask);
}
@ -1146,6 +1152,15 @@ static void nau8821_setup_inserted_irq(struct nau8821 *nau8821)
{
struct regmap *regmap = nau8821->regmap;
/* Disable & mask insertion IRQ */
regmap_update_bits(regmap, NAU8821_R12_INTERRUPT_DIS_CTRL,
NAU8821_IRQ_INSERT_DIS, NAU8821_IRQ_INSERT_DIS);
regmap_update_bits(regmap, NAU8821_R0F_INTERRUPT_MASK,
NAU8821_IRQ_INSERT_EN, NAU8821_IRQ_INSERT_EN);
/* Clear insert IRQ status */
nau8821_irq_status_clear(regmap, NAU8821_JACK_INSERT_DETECTED);
/* Enable internal VCO needed for interruptions */
if (nau8821->dapm->bias_level < SND_SOC_BIAS_PREPARE)
nau8821_configure_sysclk(nau8821, NAU8821_CLK_INTERNAL, 0);
@ -1160,10 +1175,12 @@ static void nau8821_setup_inserted_irq(struct nau8821 *nau8821)
regmap_update_bits(regmap, NAU8821_R1D_I2S_PCM_CTRL2,
NAU8821_I2S_MS_MASK, NAU8821_I2S_MS_SLAVE);
/* Not bypass de-bounce circuit */
/* Do not bypass de-bounce circuit */
if (!(nau8821_quirk & NAU8821_QUIRK_JD_DB_BYPASS))
regmap_update_bits(regmap, NAU8821_R0D_JACK_DET_CTRL,
NAU8821_JACK_DET_DB_BYPASS, 0);
/* Unmask & enable the ejection IRQs */
regmap_update_bits(regmap, NAU8821_R0F_INTERRUPT_MASK,
NAU8821_IRQ_EJECT_EN, 0);
regmap_update_bits(regmap, NAU8821_R12_INTERRUPT_DIS_CTRL,
@ -1174,7 +1191,8 @@ static irqreturn_t nau8821_interrupt(int irq, void *data)
{
struct nau8821 *nau8821 = (struct nau8821 *)data;
struct regmap *regmap = nau8821->regmap;
int active_irq, clear_irq = 0, event = 0, event_mask = 0;
struct snd_soc_component *component;
int active_irq, event = 0, event_mask = 0;
if (regmap_read(regmap, NAU8821_R10_IRQ_STATUS, &active_irq)) {
dev_err(nau8821->dev, "failed to read irq status\n");
@ -1185,48 +1203,41 @@ static irqreturn_t nau8821_interrupt(int irq, void *data)
if ((active_irq & NAU8821_JACK_EJECT_IRQ_MASK) ==
NAU8821_JACK_EJECT_DETECTED) {
cancel_delayed_work_sync(&nau8821->jdet_work);
regmap_update_bits(regmap, NAU8821_R71_ANALOG_ADC_1,
NAU8821_MICDET_MASK, NAU8821_MICDET_DIS);
nau8821_eject_jack(nau8821);
event_mask |= SND_JACK_HEADSET;
clear_irq = NAU8821_JACK_EJECT_IRQ_MASK;
} else if (active_irq & NAU8821_KEY_SHORT_PRESS_IRQ) {
event |= NAU8821_BUTTON;
event_mask |= NAU8821_BUTTON;
clear_irq = NAU8821_KEY_SHORT_PRESS_IRQ;
nau8821_irq_status_clear(regmap, NAU8821_KEY_SHORT_PRESS_IRQ);
} else if (active_irq & NAU8821_KEY_RELEASE_IRQ) {
event_mask = NAU8821_BUTTON;
clear_irq = NAU8821_KEY_RELEASE_IRQ;
nau8821_irq_status_clear(regmap, NAU8821_KEY_RELEASE_IRQ);
} else if ((active_irq & NAU8821_JACK_INSERT_IRQ_MASK) ==
NAU8821_JACK_INSERT_DETECTED) {
cancel_delayed_work_sync(&nau8821->jdet_work);
regmap_update_bits(regmap, NAU8821_R71_ANALOG_ADC_1,
NAU8821_MICDET_MASK, NAU8821_MICDET_EN);
if (nau8821_is_jack_inserted(regmap)) {
/* detect microphone and jack type */
cancel_work_sync(&nau8821->jdet_work);
schedule_work(&nau8821->jdet_work);
/* Detect microphone and jack type */
component = snd_soc_dapm_to_component(nau8821->dapm);
snd_soc_component_force_enable_pin(component, "MICBIAS");
snd_soc_dapm_sync(nau8821->dapm);
schedule_delayed_work(&nau8821->jdet_work, msecs_to_jiffies(20));
/* Turn off insertion interruption at manual mode */
regmap_update_bits(regmap,
NAU8821_R12_INTERRUPT_DIS_CTRL,
NAU8821_IRQ_INSERT_DIS,
NAU8821_IRQ_INSERT_DIS);
regmap_update_bits(regmap,
NAU8821_R0F_INTERRUPT_MASK,
NAU8821_IRQ_INSERT_EN,
NAU8821_IRQ_INSERT_EN);
nau8821_setup_inserted_irq(nau8821);
} else {
dev_warn(nau8821->dev,
"Inserted IRQ fired but not connected\n");
nau8821_eject_jack(nau8821);
}
} else {
/* Clear the rightmost interrupt */
nau8821_irq_status_clear(regmap, active_irq);
}
if (!clear_irq)
clear_irq = active_irq;
/* clears the rightmost interruption */
regmap_write(regmap, NAU8821_R11_INT_CLR_KEY_STATUS, clear_irq);
if (event_mask)
snd_soc_jack_report(nau8821->jack, event, event_mask);
@ -1521,7 +1532,7 @@ static int nau8821_resume_setup(struct nau8821 *nau8821)
nau8821_configure_sysclk(nau8821, NAU8821_CLK_DIS, 0);
if (nau8821->irq) {
/* Clear all interruption status */
nau8821_int_status_clear_all(regmap);
nau8821_irq_status_clear(regmap, 0);
/* Enable both insertion and ejection interruptions, and then
* bypass de-bounce circuit.
@ -1651,7 +1662,8 @@ int nau8821_enable_jack_detect(struct snd_soc_component *component,
nau8821->jack = jack;
/* Initiate jack detection work queue */
INIT_WORK(&nau8821->jdet_work, nau8821_jdet_work);
INIT_DELAYED_WORK(&nau8821->jdet_work, nau8821_jdet_work);
ret = devm_request_threaded_irq(nau8821->dev, nau8821->irq, NULL,
nau8821_interrupt, IRQF_TRIGGER_LOW | IRQF_ONESHOT,
"nau8821", nau8821);
@ -1856,7 +1868,23 @@ static const struct dmi_system_id nau8821_quirk_table[] = {
DMI_MATCH(DMI_SYS_VENDOR, "Positivo Tecnologia SA"),
DMI_MATCH(DMI_BOARD_NAME, "CW14Q01P-V2"),
},
.driver_data = (void *)(NAU8821_JD_ACTIVE_HIGH),
.driver_data = (void *)(NAU8821_QUIRK_JD_ACTIVE_HIGH),
},
{
/* Valve Steam Deck LCD */
.matches = {
DMI_MATCH(DMI_SYS_VENDOR, "Valve"),
DMI_MATCH(DMI_PRODUCT_NAME, "Jupiter"),
},
.driver_data = (void *)(NAU8821_QUIRK_JD_DB_BYPASS),
},
{
/* Valve Steam Deck OLED */
.matches = {
DMI_MATCH(DMI_SYS_VENDOR, "Valve"),
DMI_MATCH(DMI_PRODUCT_NAME, "Galileo"),
},
.driver_data = (void *)(NAU8821_QUIRK_JD_DB_BYPASS),
},
{}
};
@ -1898,9 +1926,12 @@ static int nau8821_i2c_probe(struct i2c_client *i2c)
nau8821_check_quirks();
if (nau8821_quirk & NAU8821_JD_ACTIVE_HIGH)
if (nau8821_quirk & NAU8821_QUIRK_JD_ACTIVE_HIGH)
nau8821->jkdet_polarity = 0;
if (nau8821_quirk & NAU8821_QUIRK_JD_DB_BYPASS)
dev_dbg(dev, "Force bypassing jack detection debounce circuit\n");
nau8821_print_device_properties(nau8821);
nau8821_reset_chip(nau8821->regmap);

View File

@ -561,7 +561,7 @@ struct nau8821 {
struct regmap *regmap;
struct snd_soc_dapm_context *dapm;
struct snd_soc_jack *jack;
struct work_struct jdet_work;
struct delayed_work jdet_work;
int irq;
int clk_id;
int micbias_voltage;