[RFC PATCH 18/23] ASoC: qcom: audioreach: add speaker protection calibration state control

From: Srinivas Kandagatla

Date: Fri Oct 23 2026 - 13:24:50 EST


Add a read-only "Speaker Protection Calibration State" control with
each speaker's last calibration state, so userspace can follow a run.

Signed-off-by: Srinivas Kandagatla <srinivas.kandagatla@xxxxxxxxxxxxxxxx>
---
sound/soc/qcom/qdsp6/audioreach.c | 51 +++++++++++++++++++++++++++++++
sound/soc/qcom/qdsp6/audioreach.h | 2 ++
sound/soc/qcom/qdsp6/q6apm.c | 15 ++++++++-
sound/soc/qcom/qdsp6/topology.c | 18 +++++++++++
4 files changed, 85 insertions(+), 1 deletion(-)

diff --git a/sound/soc/qcom/qdsp6/audioreach.c b/sound/soc/qcom/qdsp6/audioreach.c
index f05033cc3f7d..3eba0a9a0372 100644
--- a/sound/soc/qcom/qdsp6/audioreach.c
+++ b/sound/soc/qcom/qdsp6/audioreach.c
@@ -21,6 +21,7 @@ enum sp_vi_cali_state {
SP_VI_CALI_RUNNING, /* Events arriving, none decisive yet */
SP_VI_CALI_FAILED, /* Run finished, no usable R0 */
SP_VI_CALI_SUCCESS, /* Run finished, @r0_cali_q24 valid */
+ SP_VI_CALI_ABORTED, /* Graph stopped before the run finished */
};

struct sp_vi_r0_result {
@@ -28,6 +29,8 @@ struct sp_vi_r0_result {
u32 num_ch;
s32 r0_cali_q24[MAX_SP_VI_SPEAKERS];
u32 seen_states[MAX_SP_VI_SPEAKERS];
+ /* Last calibration state per speaker (enum vi_calibration_state) */
+ u32 last_state[MAX_SP_VI_SPEAKERS];
};

static struct sp_vi_r0_result sp_vi_r0;
@@ -1616,6 +1619,21 @@ int audioreach_get_sp_vi_t0(long *vals, unsigned int count)
}
EXPORT_SYMBOL_GPL(audioreach_get_sp_vi_t0);

+/* Latest calibration state per speaker, 0 before any event */
+int audioreach_get_sp_vi_cal_state(long *vals, unsigned int count)
+{
+ unsigned int i;
+
+ guard(mutex)(&sp_vi_r0_lock);
+
+ count = min(count, MAX_SP_VI_SPEAKERS);
+ for (i = 0; i < count; i++)
+ vals[i] = sp_vi_r0.last_state[i];
+
+ return count;
+}
+EXPORT_SYMBOL_GPL(audioreach_get_sp_vi_cal_state);
+
int audioreach_set_sp_vi_r0t0(struct device *dev, const long *vals, unsigned int count)
{
struct sp_vi_r0t0_cfg new = {};
@@ -1695,6 +1713,8 @@ void audioreach_vi_calibration_event(struct device *dev,
for (i = 0; i < num_ch; i++) {
u32 state = cali->cali_param[i].state;

+ sp_vi_r0.last_state[i] = state;
+
if (sp_vi_cali_state_is_failure(state))
success = false;
else if (!sp_vi_cali_state_is_terminal(state))
@@ -1753,6 +1773,37 @@ void audioreach_vi_calibration_event(struct device *dev,
dev_info(dev, "VI calibration complete: %s\n", success ? "success" : "failed");
}

+/* A run cut short never reports a final state, close it out */
+void audioreach_vi_calibration_abort(struct device *dev)
+{
+ unsigned int i;
+
+ guard(mutex)(&sp_vi_r0_lock);
+
+ if (sp_vi_r0.state != SP_VI_CALI_RUNNING)
+ return;
+
+ sp_vi_r0.state = SP_VI_CALI_ABORTED;
+
+ for (i = 0; i < MAX_SP_VI_SPEAKERS; i++) {
+ u32 state = sp_vi_r0.last_state[i];
+
+ /* Slot never reported: not part of this run */
+ if (!sp_vi_r0.seen_states[i])
+ continue;
+
+ if (sp_vi_cali_state_is_terminal(state))
+ continue;
+
+ dev_info(dev, "VI calibration channel %u: aborted while %s\n",
+ i, sp_vi_cali_state_name(state));
+ sp_vi_r0.last_state[i] = VI_CALIBRATION_STATE_INACTIVE;
+ }
+
+ dev_info(dev, "VI calibration aborted: graph stopped before the DSP finished\n");
+}
+EXPORT_SYMBOL_GPL(audioreach_vi_calibration_abort);
+
static int audioreach_speaker_protection(struct q6apm_graph *graph,
const struct audioreach_module *module)
{
diff --git a/sound/soc/qcom/qdsp6/audioreach.h b/sound/soc/qcom/qdsp6/audioreach.h
index 7345b6d15f58..6a08f52b7786 100644
--- a/sound/soc/qcom/qdsp6/audioreach.h
+++ b/sound/soc/qcom/qdsp6/audioreach.h
@@ -1141,10 +1141,12 @@ int audioreach_set_sp_vi_r0t0(struct device *dev, const long *vals, unsigned int
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_get_sp_vi_cal_state(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);
+void audioreach_vi_calibration_abort(struct device *dev);
int audioreach_compr_set_param(struct q6apm_graph *graph,
const struct audioreach_module_config *mcfg);
int audioreach_setup_push_pull(struct q6apm_graph *graph, phys_addr_t bphys,
diff --git a/sound/soc/qcom/qdsp6/q6apm.c b/sound/soc/qcom/qdsp6/q6apm.c
index a4502741d87f..41daa02febf4 100644
--- a/sound/soc/qcom/qdsp6/q6apm.c
+++ b/sound/soc/qcom/qdsp6/q6apm.c
@@ -813,10 +813,19 @@ struct q6apm_graph *q6apm_graph_open(struct device *dev, q6apm_cb cb,
}
EXPORT_SYMBOL_GPL(q6apm_graph_open);

+static void q6apm_graph_abort_vi_calibration(struct q6apm_graph *graph)
+{
+ if (q6apm_find_module_by_mid(graph, MODULE_ID_SPEAKER_PROTECTION_VI))
+ audioreach_vi_calibration_abort(graph->dev);
+}
+
int q6apm_graph_close(struct q6apm_graph *graph)
{
struct audioreach_graph *ar_graph = graph->ar_graph;

+ /* Covers a graph closed without ever being started, e.g. on prepare failure */
+ q6apm_graph_abort_vi_calibration(graph);
+
graph->ar_graph = NULL;
kref_put(&ar_graph->refcount, q6apm_put_audioreach_graph);
gpr_free_port(graph->port);
@@ -852,6 +861,7 @@ EXPORT_SYMBOL_GPL(q6apm_graph_start);
int q6apm_graph_stop(struct q6apm_graph *graph)
{
struct audioreach_graph *ar_graph = graph->ar_graph;
+ int ret;

if (ar_graph->start_count == 0)
return 0;
@@ -859,7 +869,10 @@ int q6apm_graph_stop(struct q6apm_graph *graph)
if (--ar_graph->start_count > 0)
return 0;

- return audioreach_graph_mgmt_cmd(ar_graph, APM_CMD_GRAPH_STOP);
+ ret = audioreach_graph_mgmt_cmd(ar_graph, APM_CMD_GRAPH_STOP);
+ q6apm_graph_abort_vi_calibration(graph);
+
+ return ret;
}
EXPORT_SYMBOL_GPL(q6apm_graph_stop);

diff --git a/sound/soc/qcom/qdsp6/topology.c b/sound/soc/qcom/qdsp6/topology.c
index 8bc681a633d8..75381d9a1435 100644
--- a/sound/soc/qcom/qdsp6/topology.c
+++ b/sound/soc/qcom/qdsp6/topology.c
@@ -1551,6 +1551,15 @@ static int audioreach_get_sp_t0(struct snd_kcontrol *kcontrol,
return 0;
}

+static int audioreach_get_sp_cal_state(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ audioreach_get_sp_vi_cal_state(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,
@@ -1560,6 +1569,15 @@ static const struct snd_kcontrol_new audioreach_sp_telemetry_controls[] = {
.info = audioreach_info_sp_t0,
.get = audioreach_get_sp_t0,
},
+ {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Speaker Protection Calibration State",
+ .access = SNDRV_CTL_ELEM_ACCESS_READ |
+ SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+ /* Last calibration state per speaker (enum vi_calibration_state) */
+ .info = audioreach_info_sp_t0,
+ .get = audioreach_get_sp_cal_state,
+ },
};

/* Kept apart from the topology SP controls, so FEs have them without it */
--
2.47.3