[PATCH 0/5] ASoC/SoundWire: fix race condition on system suspend
This patchset fixes a race condition when a SoundWire codec signals an 'Alert' status during a system suspend, which can happen when e.g. a jack detection event is detected and handled in a workqueue scheduled after the link is suspended.
When this happens in pm_runtime suspend, we already use pm_runtime_get_resume() to force the link to remain active, or resume immediately, but during a system suspend we don't have this option: we can't prevent the entire system from suspending just because of a jack detection or codec-specific event.
This patch suggests instead disabling the implementation-defined or class-defined IRQs at the start of the suspend, along with an additional flag to protect against an interrupt thread/workqueue being scheduled after the interrupts are disabled.
One could argue that the SoundWire core could have handled this case, which is a fair objection. I chose to keep the interrupt handling codec-specific since not all codecs can actually generate interrupts, and what they do during these interrupts is also quite custom - see e.g. the difference between the RT711 and RT711-SDCA versions. In the latter case the class-defined interrupts are cleared by the codec driver, not by the core.
This patchset implies changes to both ASoC and SoundWire, with a minimal dependency on a sdw_update() helper and its _no_pm version, both of which should have been added a long time ago for read-modify-write operations.
This race condition was present in all previous kernel versions but was only identified in late May in automated tests. We added Fixes tag to help linux-stable backports, but there's no real point in back-porting to earlier than 5.10 due to other missing dependencies in the upstream code.
Pierre-Louis Bossart (5): soundwire: export sdw_update() and sdw_update_no_pm() ASoC: rt700-sdw: fix race condition on system suspend ASoC: rt711-sdw: fix race condition on system suspend ASoC: rt5682-sdw: fix race condition on system suspend ASoC: rt711-sdca-sdw: fix race condition on system suspend
drivers/soundwire/bus.c | 17 +++++++++++- drivers/soundwire/bus.h | 13 --------- include/linux/soundwire/sdw.h | 3 ++ sound/soc/codecs/rt5682-sdw.c | 38 +++++++++++++++++++++++-- sound/soc/codecs/rt5682.h | 2 ++ sound/soc/codecs/rt700-sdw.c | 34 +++++++++++++++++++++-- sound/soc/codecs/rt700.c | 4 +++ sound/soc/codecs/rt700.h | 2 ++ sound/soc/codecs/rt711-sdca-sdw.c | 46 +++++++++++++++++++++++++++++-- sound/soc/codecs/rt711-sdca.c | 4 +++ sound/soc/codecs/rt711-sdca.h | 2 ++ sound/soc/codecs/rt711-sdw.c | 34 +++++++++++++++++++++-- sound/soc/codecs/rt711.c | 4 +++ sound/soc/codecs/rt711.h | 2 ++ 14 files changed, 183 insertions(+), 22 deletions(-)
base-commit: bf28c803f2f4b14716df168e98f6ede4ba955866
We currently export sdw_read() and sdw_write() but the sdw_update() and sdw_update_no_pm() are currently available only to the bus code. This was missed in an earlier contribution.
Export both functions so that codec drivers can perform read-modify-write operations without duplicating the code.
Fixes: b04c975e654c ('soundwire: bus: use sdw_update_no_pm when initializing a device') Signed-off-by: Pierre-Louis Bossart pierre-louis.bossart@linux.intel.com Reviewed-by: Bard Liao bard.liao@intel.com Reviewed-by: Péter Ujfalusi peter.ujfalusi@linux.intel.com --- drivers/soundwire/bus.c | 17 ++++++++++++++++- drivers/soundwire/bus.h | 13 ------------- include/linux/soundwire/sdw.h | 3 +++ 3 files changed, 19 insertions(+), 14 deletions(-)
diff --git a/drivers/soundwire/bus.c b/drivers/soundwire/bus.c index a9e0aa72654d..5d5b0bd59ae3 100644 --- a/drivers/soundwire/bus.c +++ b/drivers/soundwire/bus.c @@ -492,7 +492,7 @@ int sdw_read_no_pm(struct sdw_slave *slave, u32 addr) } EXPORT_SYMBOL(sdw_read_no_pm);
-static int sdw_update_no_pm(struct sdw_slave *slave, u32 addr, u8 mask, u8 val) +int sdw_update_no_pm(struct sdw_slave *slave, u32 addr, u8 mask, u8 val) { int tmp;
@@ -503,6 +503,21 @@ static int sdw_update_no_pm(struct sdw_slave *slave, u32 addr, u8 mask, u8 val) tmp = (tmp & ~mask) | val; return sdw_write_no_pm(slave, addr, tmp); } +EXPORT_SYMBOL(sdw_update_no_pm); + +/* Read-Modify-Write Slave register */ +int sdw_update(struct sdw_slave *slave, u32 addr, u8 mask, u8 val) +{ + int tmp; + + tmp = sdw_read(slave, addr); + if (tmp < 0) + return tmp; + + tmp = (tmp & ~mask) | val; + return sdw_write(slave, addr, tmp); +} +EXPORT_SYMBOL(sdw_update);
/** * sdw_nread() - Read "n" contiguous SDW Slave registers diff --git a/drivers/soundwire/bus.h b/drivers/soundwire/bus.h index 40354469860a..7631ef5e71fb 100644 --- a/drivers/soundwire/bus.h +++ b/drivers/soundwire/bus.h @@ -201,19 +201,6 @@ static inline void sdw_fill_port_params(struct sdw_port_params *params, params->data_mode = data_mode; }
-/* Read-Modify-Write Slave register */ -static inline int sdw_update(struct sdw_slave *slave, u32 addr, u8 mask, u8 val) -{ - int tmp; - - tmp = sdw_read(slave, addr); - if (tmp < 0) - return tmp; - - tmp = (tmp & ~mask) | val; - return sdw_write(slave, addr, tmp); -} - /* broadcast read/write for tests */ int sdw_bread_no_pm_unlocked(struct sdw_bus *bus, u16 dev_num, u32 addr); int sdw_bwrite_no_pm_unlocked(struct sdw_bus *bus, u16 dev_num, u32 addr, u8 value); diff --git a/include/linux/soundwire/sdw.h b/include/linux/soundwire/sdw.h index ced07f8fde87..de9802a24e7e 100644 --- a/include/linux/soundwire/sdw.h +++ b/include/linux/soundwire/sdw.h @@ -1041,6 +1041,9 @@ int sdw_write_no_pm(struct sdw_slave *slave, u32 addr, u8 value); int sdw_read_no_pm(struct sdw_slave *slave, u32 addr); int sdw_nread(struct sdw_slave *slave, u32 addr, size_t count, u8 *val); int sdw_nwrite(struct sdw_slave *slave, u32 addr, size_t count, u8 *val); +int sdw_update(struct sdw_slave *slave, u32 addr, u8 mask, u8 val); +int sdw_update_no_pm(struct sdw_slave *slave, u32 addr, u8 mask, u8 val); + int sdw_compare_devid(struct sdw_slave *slave, struct sdw_slave_id id); void sdw_extract_slave_id(struct sdw_bus *bus, u64 addr, struct sdw_slave_id *id);
On 14-06-21, 13:08, Pierre-Louis Bossart wrote:
We currently export sdw_read() and sdw_write() but the sdw_update() and sdw_update_no_pm() are currently available only to the bus code. This was missed in an earlier contribution.
Export both functions so that codec drivers can perform read-modify-write operations without duplicating the code.
Acked-By: Vinod Koul vkoul@kernel.org
On Mon, 14 Jun 2021 13:08:11 -0500, Pierre-Louis Bossart wrote:
We currently export sdw_read() and sdw_write() but the sdw_update() and sdw_update_no_pm() are currently available only to the bus code. This was missed in an earlier contribution.
Export both functions so that codec drivers can perform read-modify-write operations without duplicating the code.
Applied to
https://git.kernel.org/pub/scm/linux/kernel/git/broonie/sound.git for-next
Thanks!
[1/5] soundwire: export sdw_update() and sdw_update_no_pm() commit: d38ebaf2c88442a830d402fa7805ddbb60c4cd0c [2/5] ASoC: rt700-sdw: fix race condition on system suspend commit: 60888ef827e354d7a3611288d86629e5f1824613 [3/5] ASoC: rt711-sdw: fix race condition on system suspend commit: 18236370a098428d7639686daa36584d0d363c9e [4/5] ASoC: rt5682-sdw: fix race condition on system suspend commit: 14f4946d55d335692462f6fa4eb4ace0bf6ad1d9 [5/5] ASoC: rt711-sdca-sdw: fix race condition on system suspend commit: d2bf75f4f6b277c35eb887859139df7c2d390b87
All being well this means that it will be integrated into the linux-next tree (usually sometime in the next 24 hours) and sent to Linus during the next merge window (or sooner if it is a bug fix), however if problems are discovered then the patch may be dropped or reverted.
You may get further e-mails resulting from automated or manual testing and review of the tree, please engage with people reporting problems and send followup patches addressing any issues that are reported if needed.
If any updates are required or you are submitting further changes they should be sent as incremental updates against current git, existing patches will not be replaced.
Please add any relevant lists and maintainers to the CCs when replying to this mail.
Thanks, Mark
In previous commits we cancelled deferred work, but there is still a window of time where a new interrupt could result in new deferred work executed after the link is disabled, leading to an IO error.
This patch uses an 'disable_irq_lock' mutex to prevent new interrupts from happening after the start of the system suspend. The choice of a mutex v. a spinlock is mainly due to the time required to clear interrupts, which requires a command to be transmitted by the SoundWire host IP and acknowledged with an interrupt. The 'interrupt_callback' routine is also not meant to be called from an interrupt context.
An additional 'disable_irq' flag prevents race conditions where the status changes before the interrupts are disabled, but the workqueue handling status changes is scheduled after the completion of the system suspend. On resume the interrupts are re-enabled already by the io_init routine so we only clear the flag.
BugLink: https://github.com/thesofproject/linux/issues/2943 Fixes: 5f2df2a4583b ('ASoC: rt700: wait for the delayed work to finish when the system suspends') Signed-off-by: Pierre-Louis Bossart pierre-louis.bossart@linux.intel.com Reviewed-by: Bard Liao bard.liao@intel.com Reviewed-by: Péter Ujfalusi peter.ujfalusi@linux.intel.com --- sound/soc/codecs/rt700-sdw.c | 34 ++++++++++++++++++++++++++++++++-- sound/soc/codecs/rt700.c | 4 ++++ sound/soc/codecs/rt700.h | 2 ++ 3 files changed, 38 insertions(+), 2 deletions(-)
diff --git a/sound/soc/codecs/rt700-sdw.c b/sound/soc/codecs/rt700-sdw.c index d1d9c0f455b4..bda594899664 100644 --- a/sound/soc/codecs/rt700-sdw.c +++ b/sound/soc/codecs/rt700-sdw.c @@ -418,10 +418,12 @@ static int rt700_interrupt_callback(struct sdw_slave *slave, dev_dbg(&slave->dev, "%s control_port_stat=%x", __func__, status->control_port);
- if (status->control_port & 0x4) { + mutex_lock(&rt700->disable_irq_lock); + if (status->control_port & 0x4 && !rt700->disable_irq) { mod_delayed_work(system_power_efficient_wq, &rt700->jack_detect_work, msecs_to_jiffies(250)); } + mutex_unlock(&rt700->disable_irq_lock);
return 0; } @@ -490,6 +492,34 @@ static int __maybe_unused rt700_dev_suspend(struct device *dev) return 0; }
+static int __maybe_unused rt700_dev_system_suspend(struct device *dev) +{ + struct sdw_slave *slave = dev_to_sdw_dev(dev); + struct rt700_priv *rt700 = dev_get_drvdata(dev); + int ret; + + if (!rt700->hw_init) + return 0; + + /* + * prevent new interrupts from being handled after the + * deferred work completes and before the parent disables + * interrupts on the link + */ + mutex_lock(&rt700->disable_irq_lock); + rt700->disable_irq = true; + ret = sdw_update_no_pm(slave, SDW_SCP_INTMASK1, + SDW_SCP_INT1_IMPL_DEF, 0); + mutex_unlock(&rt700->disable_irq_lock); + + if (ret < 0) { + /* log but don't prevent suspend from happening */ + dev_dbg(&slave->dev, "%s: could not disable imp-def interrupts\n:", __func__); + } + + return rt700_dev_suspend(dev); +} + #define RT700_PROBE_TIMEOUT 5000
static int __maybe_unused rt700_dev_resume(struct device *dev) @@ -521,7 +551,7 @@ static int __maybe_unused rt700_dev_resume(struct device *dev) }
static const struct dev_pm_ops rt700_pm = { - SET_SYSTEM_SLEEP_PM_OPS(rt700_dev_suspend, rt700_dev_resume) + SET_SYSTEM_SLEEP_PM_OPS(rt700_dev_system_suspend, rt700_dev_resume) SET_RUNTIME_PM_OPS(rt700_dev_suspend, rt700_dev_resume, NULL) };
diff --git a/sound/soc/codecs/rt700.c b/sound/soc/codecs/rt700.c index 01af9d9dd3ca..921382724f9c 100644 --- a/sound/soc/codecs/rt700.c +++ b/sound/soc/codecs/rt700.c @@ -1112,6 +1112,8 @@ int rt700_init(struct device *dev, struct regmap *sdw_regmap, rt700->sdw_regmap = sdw_regmap; rt700->regmap = regmap;
+ mutex_init(&rt700->disable_irq_lock); + /* * Mark hw_init to false * HW init will be performed when device reports present @@ -1133,6 +1135,8 @@ int rt700_io_init(struct device *dev, struct sdw_slave *slave) { struct rt700_priv *rt700 = dev_get_drvdata(dev);
+ rt700->disable_irq = false; + if (rt700->hw_init) return 0;
diff --git a/sound/soc/codecs/rt700.h b/sound/soc/codecs/rt700.h index 794ee2e29051..bed9d1de6d5b 100644 --- a/sound/soc/codecs/rt700.h +++ b/sound/soc/codecs/rt700.h @@ -23,6 +23,8 @@ struct rt700_priv { struct delayed_work jack_detect_work; struct delayed_work jack_btn_check_work; int jack_type; + struct mutex disable_irq_lock; /* imp-def irq lock protection */ + bool disable_irq; };
struct sdw_stream_data {
In previous commits we cancelled deferred work, but there is still a window of time where a new interrupt could result in new deferred work executed after the link is disabled, leading to an IO error. While we did not see this IO error on RT711-based platforms, the code pattern is similar to the RT700 case where the IO error was noted, so the fix is added for consistency.
This patch uses an 'disable_irq_lock' mutex to prevent new interrupts from happening after the start of the system suspend. The choice of a mutex v. a spinlock is mainly due to the time required to clear interrupts, which requires a command to be transmitted by the SoundWire host IP and acknowledged with an interrupt. The 'interrupt_callback' routine is also not meant to be called from an interrupt context.
An additional 'disable_irq' flag prevents race conditions where the status changes before the interrupts are disabled, but the workqueue handling status changes is scheduled after the completion of the system suspend. On resume the interrupts are re-enabled already by the io_init routine so we only clear the flag.
BugLink: https://github.com/thesofproject/linux/issues/2943 Fixes: 501ef013390b ('ASoC: rt711: wait for the delayed work to finish when the system suspends') Signed-off-by: Pierre-Louis Bossart pierre-louis.bossart@linux.intel.com Reviewed-by: Bard Liao bard.liao@intel.com Reviewed-by: Péter Ujfalusi peter.ujfalusi@linux.intel.com --- sound/soc/codecs/rt711-sdw.c | 34 ++++++++++++++++++++++++++++++++-- sound/soc/codecs/rt711.c | 4 ++++ sound/soc/codecs/rt711.h | 2 ++ 3 files changed, 38 insertions(+), 2 deletions(-)
diff --git a/sound/soc/codecs/rt711-sdw.c b/sound/soc/codecs/rt711-sdw.c index 15299084429f..bda2cc9439c9 100644 --- a/sound/soc/codecs/rt711-sdw.c +++ b/sound/soc/codecs/rt711-sdw.c @@ -423,10 +423,12 @@ static int rt711_interrupt_callback(struct sdw_slave *slave, dev_dbg(&slave->dev, "%s control_port_stat=%x", __func__, status->control_port);
- if (status->control_port & 0x4) { + mutex_lock(&rt711->disable_irq_lock); + if (status->control_port & 0x4 && !rt711->disable_irq) { mod_delayed_work(system_power_efficient_wq, &rt711->jack_detect_work, msecs_to_jiffies(250)); } + mutex_unlock(&rt711->disable_irq_lock);
return 0; } @@ -493,6 +495,34 @@ static int __maybe_unused rt711_dev_suspend(struct device *dev) return 0; }
+static int __maybe_unused rt711_dev_system_suspend(struct device *dev) +{ + struct rt711_priv *rt711 = dev_get_drvdata(dev); + struct sdw_slave *slave = dev_to_sdw_dev(dev); + int ret; + + if (!rt711->hw_init) + return 0; + + /* + * prevent new interrupts from being handled after the + * deferred work completes and before the parent disables + * interrupts on the link + */ + mutex_lock(&rt711->disable_irq_lock); + rt711->disable_irq = true; + ret = sdw_update_no_pm(slave, SDW_SCP_INTMASK1, + SDW_SCP_INT1_IMPL_DEF, 0); + mutex_unlock(&rt711->disable_irq_lock); + + if (ret < 0) { + /* log but don't prevent suspend from happening */ + dev_dbg(&slave->dev, "%s: could not disable imp-def interrupts\n:", __func__); + } + + return rt711_dev_suspend(dev); +} + #define RT711_PROBE_TIMEOUT 5000
static int __maybe_unused rt711_dev_resume(struct device *dev) @@ -524,7 +554,7 @@ static int __maybe_unused rt711_dev_resume(struct device *dev) }
static const struct dev_pm_ops rt711_pm = { - SET_SYSTEM_SLEEP_PM_OPS(rt711_dev_suspend, rt711_dev_resume) + SET_SYSTEM_SLEEP_PM_OPS(rt711_dev_system_suspend, rt711_dev_resume) SET_RUNTIME_PM_OPS(rt711_dev_suspend, rt711_dev_resume, NULL) };
diff --git a/sound/soc/codecs/rt711.c b/sound/soc/codecs/rt711.c index 9f5b2dc16c54..4dbfa7b8680e 100644 --- a/sound/soc/codecs/rt711.c +++ b/sound/soc/codecs/rt711.c @@ -1166,6 +1166,8 @@ int rt711_init(struct device *dev, struct regmap *sdw_regmap, rt711->sdw_regmap = sdw_regmap; rt711->regmap = regmap;
+ mutex_init(&rt711->disable_irq_lock); + /* * Mark hw_init to false * HW init will be performed when device reports present @@ -1190,6 +1192,8 @@ int rt711_io_init(struct device *dev, struct sdw_slave *slave) { struct rt711_priv *rt711 = dev_get_drvdata(dev);
+ rt711->disable_irq = false; + if (rt711->hw_init) return 0;
diff --git a/sound/soc/codecs/rt711.h b/sound/soc/codecs/rt711.h index ca0f581feec7..2af467631435 100644 --- a/sound/soc/codecs/rt711.h +++ b/sound/soc/codecs/rt711.h @@ -25,6 +25,8 @@ struct rt711_priv { struct work_struct calibration_work; struct mutex calibrate_mutex; /* for headset calibration */ int jack_type, jd_src; + struct mutex disable_irq_lock; /* imp-def irq lock protection */ + bool disable_irq; };
struct sdw_stream_data {
In the initial driver we cancelled deferred work, but there is still a window of time where a new interrupt could result in new deferred work executed after the link is disabled, leading to an IO error. While we did not see this IO error on RT5682-based platforms, the code pattern is similar to the RT700 case where the IO error was noted, so the fix is added for consistency.
This patch uses an 'disable_irq_lock' mutex to prevent new interrupts from happening after the start of the system suspend. The choice of a mutex v. a spinlock is mainly due to the time required to clear interrupts, which requires a command to be transmitted by the SoundWire host IP and acknowledged with an interrupt. The 'interrupt_callback' routine is also not meant to be called from an interrupt context.
An additional 'disable_irq' flag prevents race conditions where the status changes before the interrupts are disabled, but the workqueue handling status changes is scheduled after the completion of the system suspend. On resume the interrupts are re-enabled already by the io_init routine so we only clear the flag.
The Fixes tag points to a 5.10 commit, there's no need to propagate this change to earlier upstream versions.
BugLink: https://github.com/thesofproject/linux/issues/2943 Fixes: 4a55000722d7 ('ASoC: codecs: rt*.c: remove useless pointer cast') Signed-off-by: Pierre-Louis Bossart pierre-louis.bossart@linux.intel.com Reviewed-by: Bard Liao bard.liao@intel.com Reviewed-by: Péter Ujfalusi peter.ujfalusi@linux.intel.com --- sound/soc/codecs/rt5682-sdw.c | 38 +++++++++++++++++++++++++++++++++-- sound/soc/codecs/rt5682.h | 2 ++ 2 files changed, 38 insertions(+), 2 deletions(-)
diff --git a/sound/soc/codecs/rt5682-sdw.c b/sound/soc/codecs/rt5682-sdw.c index 54873730bec5..31a4f286043e 100644 --- a/sound/soc/codecs/rt5682-sdw.c +++ b/sound/soc/codecs/rt5682-sdw.c @@ -344,6 +344,8 @@ static int rt5682_sdw_init(struct device *dev, struct regmap *regmap, rt5682->sdw_regmap = regmap; rt5682->is_sdw = true;
+ mutex_init(&rt5682->disable_irq_lock); + rt5682->regmap = devm_regmap_init(dev, NULL, dev, &rt5682_sdw_indirect_regmap); if (IS_ERR(rt5682->regmap)) { @@ -378,6 +380,8 @@ static int rt5682_io_init(struct device *dev, struct sdw_slave *slave) int ret = 0, loop = 10; unsigned int val;
+ rt5682->disable_irq = false; + if (rt5682->hw_init) return 0;
@@ -679,10 +683,12 @@ static int rt5682_interrupt_callback(struct sdw_slave *slave, dev_dbg(&slave->dev, "%s control_port_stat=%x", __func__, status->control_port);
- if (status->control_port & 0x4) { + mutex_lock(&rt5682->disable_irq_lock); + if (status->control_port & 0x4 && !rt5682->disable_irq) { mod_delayed_work(system_power_efficient_wq, &rt5682->jack_detect_work, msecs_to_jiffies(rt5682->irq_work_delay_time)); } + mutex_unlock(&rt5682->disable_irq_lock);
return 0; } @@ -740,6 +746,34 @@ static int __maybe_unused rt5682_dev_suspend(struct device *dev) return 0; }
+static int __maybe_unused rt5682_dev_system_suspend(struct device *dev) +{ + struct rt5682_priv *rt5682 = dev_get_drvdata(dev); + struct sdw_slave *slave = dev_to_sdw_dev(dev); + int ret; + + if (!rt5682->hw_init) + return 0; + + /* + * prevent new interrupts from being handled after the + * deferred work completes and before the parent disables + * interrupts on the link + */ + mutex_lock(&rt5682->disable_irq_lock); + rt5682->disable_irq = true; + ret = sdw_update_no_pm(slave, SDW_SCP_INTMASK1, + SDW_SCP_INT1_IMPL_DEF, 0); + mutex_unlock(&rt5682->disable_irq_lock); + + if (ret < 0) { + /* log but don't prevent suspend from happening */ + dev_dbg(&slave->dev, "%s: could not disable imp-def interrupts\n:", __func__); + } + + return rt5682_dev_suspend(dev); +} + static int __maybe_unused rt5682_dev_resume(struct device *dev) { struct sdw_slave *slave = dev_to_sdw_dev(dev); @@ -768,7 +802,7 @@ static int __maybe_unused rt5682_dev_resume(struct device *dev) }
static const struct dev_pm_ops rt5682_pm = { - SET_SYSTEM_SLEEP_PM_OPS(rt5682_dev_suspend, rt5682_dev_resume) + SET_SYSTEM_SLEEP_PM_OPS(rt5682_dev_system_suspend, rt5682_dev_resume) SET_RUNTIME_PM_OPS(rt5682_dev_suspend, rt5682_dev_resume, NULL) };
diff --git a/sound/soc/codecs/rt5682.h b/sound/soc/codecs/rt5682.h index 74ff66767016..b59221048ebf 100644 --- a/sound/soc/codecs/rt5682.h +++ b/sound/soc/codecs/rt5682.h @@ -1415,6 +1415,8 @@ struct rt5682_priv { struct regulator_bulk_data supplies[RT5682_NUM_SUPPLIES]; struct delayed_work jack_detect_work; struct delayed_work jd_check_work; + struct mutex disable_irq_lock; /* imp-def irq lock protection */ + bool disable_irq; struct mutex calibrate_mutex; struct sdw_slave *slave; enum sdw_slave_status status;
In the initial driver we cancelled deferred work, but there is still a window of time where a new interrupt could result in new deferred work executed after the link is disabled, leading to an IO error. While we did not see this IO error on RT711-sdca-based platforms, the code pattern is similar to the RT700 case where the IO error was noted, so the fix is added for consistency.
This patch uses an 'disable_irq_lock' mutex to prevent new interrupts from happening after the start of the system suspend. The choice of a mutex v. a spinlock is mainly due to the time required to clear interrupts, which requires a command to be transmitted by the SoundWire host IP and acknowledged with an interrupt. The 'interrupt_callback' routine is also not meant to be called from an interrupt context.
An additional 'disable_irq' flag prevents race conditions where the status changes before the interrupts are disabled, but the workqueue handling status changes is scheduled after the completion of the system suspend. On resume the interrupts are re-enabled already by the io_init routine so we only clear the flag.
The code is slightly different from the other codecs since the interrupt callback deals with the SDCA interrupts, leading to a much larger section that's protected by the mutex. The SoundWire interrupt scheme requires a read after clearing a status, it's not clear from the specifications what would happen if SDCA interrupts are disabled in the middle of the sequence, so the entire interrupt status read/write is kept as is, even if in the end we discard the information.
BugLink: https://github.com/thesofproject/linux/issues/2943 Fixes: 7ad4d237e7c4 ('ASoC: rt711-sdca: Add RT711 SDCA vendor-specific driver') Signed-off-by: Pierre-Louis Bossart pierre-louis.bossart@linux.intel.com Reviewed-by: Bard Liao bard.liao@intel.com Reviewed-by: Péter Ujfalusi peter.ujfalusi@linux.intel.com --- sound/soc/codecs/rt711-sdca-sdw.c | 46 +++++++++++++++++++++++++++++-- sound/soc/codecs/rt711-sdca.c | 4 +++ sound/soc/codecs/rt711-sdca.h | 2 ++ 3 files changed, 50 insertions(+), 2 deletions(-)
diff --git a/sound/soc/codecs/rt711-sdca-sdw.c b/sound/soc/codecs/rt711-sdca-sdw.c index 03cd3e0142f9..aaf5af153d3f 100644 --- a/sound/soc/codecs/rt711-sdca-sdw.c +++ b/sound/soc/codecs/rt711-sdca-sdw.c @@ -256,6 +256,15 @@ static int rt711_sdca_interrupt_callback(struct sdw_slave *slave, scp_sdca_stat2 = rt711->scp_sdca_stat2; }
+ /* + * The critical section below intentionally protects a rather large piece of code. + * We don't want to allow the system suspend to disable an interrupt while we are + * processing it, which could be problematic given the quirky SoundWire interrupt + * scheme. We do want however to prevent new workqueues from being scheduled if + * the disable_irq flag was set during system suspend. + */ + mutex_lock(&rt711->disable_irq_lock); + ret = sdw_read_no_pm(rt711->slave, SDW_SCP_SDCA_INT1); if (ret < 0) goto io_error; @@ -314,13 +323,16 @@ static int rt711_sdca_interrupt_callback(struct sdw_slave *slave, "%s scp_sdca_stat1=0x%x, scp_sdca_stat2=0x%x\n", __func__, rt711->scp_sdca_stat1, rt711->scp_sdca_stat2);
- if (status->sdca_cascade) + if (status->sdca_cascade && !rt711->disable_irq) mod_delayed_work(system_power_efficient_wq, &rt711->jack_detect_work, msecs_to_jiffies(30));
+ mutex_unlock(&rt711->disable_irq_lock); + return 0;
io_error: + mutex_unlock(&rt711->disable_irq_lock); pr_err_ratelimited("IO error in %s, ret %d\n", __func__, ret); return ret; } @@ -382,6 +394,36 @@ static int __maybe_unused rt711_sdca_dev_suspend(struct device *dev) return 0; }
+static int __maybe_unused rt711_sdca_dev_system_suspend(struct device *dev) +{ + struct rt711_sdca_priv *rt711_sdca = dev_get_drvdata(dev); + struct sdw_slave *slave = dev_to_sdw_dev(dev); + int ret1, ret2; + + if (!rt711_sdca->hw_init) + return 0; + + /* + * prevent new interrupts from being handled after the + * deferred work completes and before the parent disables + * interrupts on the link + */ + mutex_lock(&rt711_sdca->disable_irq_lock); + rt711_sdca->disable_irq = true; + ret1 = sdw_update_no_pm(slave, SDW_SCP_SDCA_INTMASK1, + SDW_SCP_SDCA_INTMASK_SDCA_0, 0); + ret2 = sdw_update_no_pm(slave, SDW_SCP_SDCA_INTMASK2, + SDW_SCP_SDCA_INTMASK_SDCA_8, 0); + mutex_unlock(&rt711_sdca->disable_irq_lock); + + if (ret1 < 0 || ret2 < 0) { + /* log but don't prevent suspend from happening */ + dev_dbg(&slave->dev, "%s: could not disable SDCA interrupts\n:", __func__); + } + + return rt711_sdca_dev_suspend(dev); +} + #define RT711_PROBE_TIMEOUT 5000
static int __maybe_unused rt711_sdca_dev_resume(struct device *dev) @@ -413,7 +455,7 @@ static int __maybe_unused rt711_sdca_dev_resume(struct device *dev) }
static const struct dev_pm_ops rt711_sdca_pm = { - SET_SYSTEM_SLEEP_PM_OPS(rt711_sdca_dev_suspend, rt711_sdca_dev_resume) + SET_SYSTEM_SLEEP_PM_OPS(rt711_sdca_dev_system_suspend, rt711_sdca_dev_resume) SET_RUNTIME_PM_OPS(rt711_sdca_dev_suspend, rt711_sdca_dev_resume, NULL) };
diff --git a/sound/soc/codecs/rt711-sdca.c b/sound/soc/codecs/rt711-sdca.c index 0b0c230dcf71..2e992589f1e4 100644 --- a/sound/soc/codecs/rt711-sdca.c +++ b/sound/soc/codecs/rt711-sdca.c @@ -1411,6 +1411,8 @@ int rt711_sdca_init(struct device *dev, struct regmap *regmap, rt711->regmap = regmap; rt711->mbq_regmap = mbq_regmap;
+ mutex_init(&rt711->disable_irq_lock); + /* * Mark hw_init to false * HW init will be performed when device reports present @@ -1494,6 +1496,8 @@ int rt711_sdca_io_init(struct device *dev, struct sdw_slave *slave) int ret = 0; unsigned int val;
+ rt711->disable_irq = false; + if (rt711->hw_init) return 0;
diff --git a/sound/soc/codecs/rt711-sdca.h b/sound/soc/codecs/rt711-sdca.h index 43ae82b7fdb3..498ca687c47b 100644 --- a/sound/soc/codecs/rt711-sdca.h +++ b/sound/soc/codecs/rt711-sdca.h @@ -27,6 +27,8 @@ struct rt711_sdca_priv { struct delayed_work jack_detect_work; struct delayed_work jack_btn_check_work; struct mutex calibrate_mutex; /* for headset calibration */ + struct mutex disable_irq_lock; /* SDCA irq lock protection */ + bool disable_irq; int jack_type, jd_src; unsigned int scp_sdca_stat1, scp_sdca_stat2; int hw_ver;
participants (3)
-
Mark Brown
-
Pierre-Louis Bossart
-
Vinod Koul