[RFC PATCH 17/23] ASoC: qcom: audioreach: add live speaker temperature (T0) readback

From: Srinivas Kandagatla

Date: Fri Oct 23 2026 - 13:30:52 EST


A calibration needs each speaker's die temperature when R0 is measured,
so that the R0/T0 pair given back to the DSP is consistent.

Let the machine driver register the speakers' thermal zones in VI
channel order with audioreach_sp_register_speaker_map(), and expose
their temperatures in Q6 format as a read-only "Speaker Protection T0"
control. Record T0 with R0 when a calibration completes. A slot that
cannot be read reports 0.

Signed-off-by: Srinivas Kandagatla <srinivas.kandagatla@xxxxxxxxxxxxxxxx>
---
sound/soc/qcom/qdsp6/audioreach.c | 65 ++++++++++++++++++++++++++++++-
sound/soc/qcom/qdsp6/audioreach.h | 5 +++
sound/soc/qcom/qdsp6/q6apm-dai.c | 7 ++++
sound/soc/qcom/qdsp6/topology.c | 41 +++++++++++++++++++
4 files changed, 116 insertions(+), 2 deletions(-)

diff --git a/sound/soc/qcom/qdsp6/audioreach.c b/sound/soc/qcom/qdsp6/audioreach.c
index d027c763eb25..f05033cc3f7d 100644
--- a/sound/soc/qcom/qdsp6/audioreach.c
+++ b/sound/soc/qcom/qdsp6/audioreach.c
@@ -5,6 +5,7 @@
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/soc/qcom/apr.h>
+#include <linux/thermal.h>
#include <sound/soc.h>
#include <sound/soc-dai.h>
#include <sound/pcm.h>
@@ -41,6 +42,13 @@ struct sp_vi_r0t0_cfg {
static struct sp_vi_r0t0_cfg sp_vi_r0t0_cfg;
static DEFINE_MUTEX(sp_vi_r0t0_cfg_lock);

+/* Speakers' thermal zones in VI channel order, set by the machine driver */
+static struct {
+ struct thermal_zone_device *tz[MAX_SP_VI_SPEAKERS];
+ unsigned int num;
+} sp_speaker_map;
+static DEFINE_MUTEX(sp_speaker_map_lock);
+
/* SubGraph Config */
struct apm_sub_graph_data {
struct apm_sub_graph_cfg sub_graph_cfg;
@@ -1564,6 +1572,50 @@ void audioreach_get_sp_vi_r0t0(long *vals, unsigned int count)
}
}

+int audioreach_sp_register_speaker_map(struct thermal_zone_device * const *tzs,
+ unsigned int num)
+{
+ unsigned int i;
+
+ if (num > MAX_SP_VI_SPEAKERS)
+ return -EINVAL;
+
+ guard(mutex)(&sp_speaker_map_lock);
+
+ for (i = 0; i < num; i++)
+ sp_speaker_map.tz[i] = tzs[i];
+ for (; i < MAX_SP_VI_SPEAKERS; i++)
+ sp_speaker_map.tz[i] = NULL;
+ sp_speaker_map.num = num;
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(audioreach_sp_register_speaker_map);
+
+/* Read each speaker's T0 in Q6 (degC * 64), 0 if its zone cannot be read */
+int audioreach_get_sp_vi_t0(long *vals, unsigned int count)
+{
+ unsigned int i, n;
+
+ guard(mutex)(&sp_speaker_map_lock);
+
+ n = min(count, sp_speaker_map.num);
+ for (i = 0; i < n; i++) {
+ int mdeg;
+
+ if (sp_speaker_map.tz[i] &&
+ !thermal_zone_get_temp(sp_speaker_map.tz[i], &mdeg))
+ vals[i] = ((s64)mdeg * 64) / 1000;
+ else
+ vals[i] = 0;
+ }
+ for (; i < count; i++)
+ vals[i] = 0;
+
+ return n;
+}
+EXPORT_SYMBOL_GPL(audioreach_get_sp_vi_t0);
+
int audioreach_set_sp_vi_r0t0(struct device *dev, const long *vals, unsigned int count)
{
struct sp_vi_r0t0_cfg new = {};
@@ -1629,6 +1681,7 @@ void audioreach_vi_calibration_event(struct device *dev,
const struct event_id_vi_per_spkr_calibration *cali,
u32 num_ch)
{
+ long t0_live[MAX_SP_VI_SPEAKERS];
bool decisive = true;
bool success = true;
unsigned int i;
@@ -1681,12 +1734,20 @@ void audioreach_vi_calibration_event(struct device *dev,
i, r0_int, r0_frac, sp_vi_r0.r0_cali_q24[i]);
}

- /* Only a run where every speaker succeeded gives a usable R0/T0 set */
+ /*
+ * Only a run where every speaker succeeded gives a usable R0/T0 set.
+ * The DSP reports only R0, so pair it with the speakers' last T0
+ * reading; a zone that cannot be read gives T0 = 0.
+ */
if (success) {
+ audioreach_get_sp_vi_t0(t0_live, num_ch);
+
guard(mutex)(&sp_vi_r0t0_cfg_lock);
sp_vi_r0t0_cfg.num_ch = num_ch;
- for (i = 0; i < num_ch; i++)
+ for (i = 0; i < num_ch; i++) {
sp_vi_r0t0_cfg.r0_cali_q24[i] = cali->cali_param[i].r0_cali_q24;
+ sp_vi_r0t0_cfg.t0_cali_q6[i] = (s16)t0_live[i];
+ }
}

dev_info(dev, "VI calibration complete: %s\n", success ? "success" : "failed");
diff --git a/sound/soc/qcom/qdsp6/audioreach.h b/sound/soc/qcom/qdsp6/audioreach.h
index 67a9d3c66fd4..7345b6d15f58 100644
--- a/sound/soc/qcom/qdsp6/audioreach.h
+++ b/sound/soc/qcom/qdsp6/audioreach.h
@@ -8,6 +8,7 @@
#include <sound/soc.h>
struct q6apm;
struct q6apm_graph;
+struct thermal_zone_device;

/* Module IDs */
#define MODULE_ID_WR_SHARED_MEM_EP 0x07001000
@@ -1137,6 +1138,10 @@ u32 audioreach_get_sp_operation_mode(void);
void audioreach_set_sp_operation_mode(u32 mode);
void audioreach_get_sp_vi_r0t0(long *vals, unsigned int count);
int audioreach_set_sp_vi_r0t0(struct device *dev, const long *vals, unsigned int count);
+int audioreach_sp_register_speaker_map(struct thermal_zone_device * const *tzs,
+ unsigned int num);
+int audioreach_get_sp_vi_t0(long *vals, unsigned int count);
+int audioreach_add_sp_telemetry_controls(struct snd_soc_component *component);
void audioreach_vi_calibration_event(struct device *dev,
const struct event_id_vi_per_spkr_calibration *cali,
u32 num_ch);
diff --git a/sound/soc/qcom/qdsp6/q6apm-dai.c b/sound/soc/qcom/qdsp6/q6apm-dai.c
index 9002acf0e0cd..19c91d0bede4 100644
--- a/sound/soc/qcom/qdsp6/q6apm-dai.c
+++ b/sound/soc/qcom/qdsp6/q6apm-dai.c
@@ -20,6 +20,7 @@
#include <sound/soc-dapm.h>
#include <asm/div64.h>
#include <asm/dma.h>
+#include "audioreach.h"
#include "q6apm.h"

#define DRV_NAME "q6apm-dai"
@@ -1173,8 +1174,14 @@ static const struct snd_compress_ops q6apm_dai_compress_ops = {
.copy = q6apm_compr_copy,
};

+static int q6apm_dai_component_probe(struct snd_soc_component *component)
+{
+ return audioreach_add_sp_telemetry_controls(component);
+}
+
static const struct snd_soc_component_driver q6apm_fe_dai_component = {
.name = DRV_NAME,
+ .probe = q6apm_dai_component_probe,
.open = q6apm_dai_open,
.close = q6apm_dai_close,
.prepare = q6apm_dai_prepare,
diff --git a/sound/soc/qcom/qdsp6/topology.c b/sound/soc/qcom/qdsp6/topology.c
index 6316d1ab0675..8bc681a633d8 100644
--- a/sound/soc/qcom/qdsp6/topology.c
+++ b/sound/soc/qcom/qdsp6/topology.c
@@ -1530,6 +1530,47 @@ static int audioreach_put_sp_r0t0(struct snd_kcontrol *kcontrol,
return 1;
}

+/* Live T0 per speaker in Q6 (degC * 64), 0 if it cannot be read */
+static int audioreach_info_sp_t0(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = MAX_SP_VI_SPEAKERS;
+ uinfo->value.integer.min = S32_MIN;
+ uinfo->value.integer.max = S32_MAX;
+
+ return 0;
+}
+
+static int audioreach_get_sp_t0(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ audioreach_get_sp_vi_t0(ucontrol->value.integer.value,
+ MAX_SP_VI_SPEAKERS);
+
+ return 0;
+}
+
+static const struct snd_kcontrol_new audioreach_sp_telemetry_controls[] = {
+ {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Speaker Protection T0",
+ .access = SNDRV_CTL_ELEM_ACCESS_READ |
+ SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+ .info = audioreach_info_sp_t0,
+ .get = audioreach_get_sp_t0,
+ },
+};
+
+/* Kept apart from the topology SP controls, so FEs have them without it */
+int audioreach_add_sp_telemetry_controls(struct snd_soc_component *component)
+{
+ return snd_soc_add_component_controls(component,
+ audioreach_sp_telemetry_controls,
+ ARRAY_SIZE(audioreach_sp_telemetry_controls));
+}
+EXPORT_SYMBOL_GPL(audioreach_add_sp_telemetry_controls);
+
static int audioreach_control_load_mix(struct snd_soc_component *scomp,
struct snd_ar_control *scontrol,
struct snd_kcontrol_new *kc,
--
2.47.3