[PATCH 03/11] ASoC: cs35l56: Add common code for factory calibration

Richard Fitzgerald
2025-10-16T10:43:06Z

Add core code to support factory calibration. This can be used by both
the ASoC and HDA drivers.

This code consists of implementations of sysfs handlers for three
sysfs files used to start factory calibration and read the results.

Sysfs is used here so that access will be restricted. These are mainly
intended to be used by manufacturers and in service centres after a
repair. It is not something that a normal user should ever touch.

Calibration affects the matching of the amp hardware to the external
speakers. If not done correctly (either accidentally or maliciously) it can
cause the speakers to be under-protected. The PC manufacturer's tunings can
limit the allowed range of a calibration result but some speakers have very
wide manufacturing tolerances, so the allowed range could be wide.

This is not a full implementation of sysfs files. There are some
requirements to synchronize with the rest of the amp driver, and the way
this is done is significantly different between ASoC and HDA. Therefore
cs35l56-shared.c provides the main part of the sysfs file handlers, but the
files themselves are defined in the ASoC and HDA drivers with suitable
handling before calling into this shared code.

The cal_data sysfs allows the calibration to be read and also for a
previous calibration to be written (for systems where the storage is not
something directly accessible to drivers, such as on filesystems). As a
precaution cal_data can only be written if the driver does not already have
valid calibration. Code outside the kernel should treat the content of
cal_data as an opaque blob, so the struct definition is not exported as a
user API.

Signed-off-by: Richard Fitzgerald <rf@opensource.cirrus.com>
---
include/sound/cs35l56.h | 18 +++
sound/soc/codecs/Kconfig | 3 +
sound/soc/codecs/cs35l56-shared.c | 253 +++++++++++++++++++++++++++++-
3 files changed, 269 insertions(+), 5 deletions(-)

diff --git a/include/sound/cs35l56.h b/include/sound/cs35l56.h
index ec9b1072d6be..4ed738615021 100644
--- a/include/sound/cs35l56.h
+++ b/include/sound/cs35l56.h
@@ -62,6 +62,8 @@
#define CS35L56_IRQ1_MASK_8 0x000E0AC
#define CS35L56_IRQ1_MASK_18 0x000E0D4
#define CS35L56_IRQ1_MASK_20 0x000E0DC
+#define CS35L56_MIXER_NGATE_CH1_CFG 0x0010004
+#define CS35L56_MIXER_NGATE_CH2_CFG 0x0010008
#define CS35L56_DSP_MBOX_1_RAW 0x0011000
#define CS35L56_DSP_VIRTUAL1_MBOX_1 0x0011020
#define CS35L56_DSP_VIRTUAL1_MBOX_2 0x0011024
@@ -177,6 +179,9 @@
/* IRQ1_EINT_8 */
#define CS35L56_TEMP_ERR_EINT1_MASK 0x80000000

+/* MIXER_NGATE_CHn_CFG */
+#define CS35L56_AUX_NGATE_CHn_EN 0x00000001
+
/* Mixer input sources */
#define CS35L56_INPUT_SRC_NONE 0x00
#define CS35L56_INPUT_SRC_ASP1RX1 0x08
@@ -243,6 +248,7 @@
#define CS35L56_MBOX_CMD_AUDIO_PLAY 0x0B000001
#define CS35L56_MBOX_CMD_AUDIO_PAUSE 0x0B000002
#define CS35L56_MBOX_CMD_AUDIO_REINIT 0x0B000003
+#define CS35L56_MBOX_CMD_AUDIO_CALIBRATION 0x0B000006
#define CS35L56_MBOX_CMD_HIBERNATE_NOW 0x02000001
#define CS35L56_MBOX_CMD_WAKEUP 0x02000002
#define CS35L56_MBOX_CMD_PREVENT_AUTO_HIBERNATE 0x02000003
@@ -264,6 +270,9 @@
#define CS35L56_RESET_PULSE_MIN_US 1100
#define CS35L56_WAKE_HOLD_TIME_US 1000

+#define CS35L56_CALIBRATION_POLL_US (100 * USEC_PER_MSEC)
+#define CS35L56_CALIBRATION_TIMEOUT_US (5 * USEC_PER_SEC)
+
#define CS35L56_SDW1_PLAYBACK_PORT 1
#define CS35L56_SDW1_CAPTURE_PORT 3

@@ -294,6 +303,7 @@ struct cs35l56_fw_reg {
struct cs35l56_base {
struct device *dev;
struct regmap *regmap;
+ struct cs_dsp *dsp;
int irq;
struct mutex irq_lock;
u8 type;
@@ -359,6 +369,14 @@ int cs35l56_runtime_suspend_common(struct cs35l56_base *cs35l56_base);
int cs35l56_runtime_resume_common(struct cs35l56_base *cs35l56_base, bool is_soundwire);
void cs35l56_init_cs_dsp(struct cs35l56_base *cs35l56_base, struct cs_dsp *cs_dsp);
int cs35l56_get_calibration(struct cs35l56_base *cs35l56_base);
+ssize_t cs35l56_calibrate_sysfs_store(struct cs35l56_base *cs35l56_base,
+ const char *buf, size_t count);
+ssize_t cs35l56_cal_ambient_sysfs_store(struct cs35l56_base *cs35l56_base,
+ const char *buf, size_t count);
+ssize_t cs35l56_cal_data_sysfs_read(struct cs35l56_base *cs35l56_base,
+ char *buf, loff_t pos, size_t count);
+ssize_t cs35l56_cal_data_sysfs_write(struct cs35l56_base *cs35l56_base,
+ const char *buf, loff_t pos, size_t count);
int cs35l56_read_prot_status(struct cs35l56_base *cs35l56_base,
bool *fw_missing, unsigned int *fw_version);
void cs35l56_log_tuning(struct cs35l56_base *cs35l56_base, struct cs_dsp *cs_dsp);
diff --git a/sound/soc/codecs/Kconfig b/sound/soc/codecs/Kconfig
index 160c07699a8b..5917bf5a72f8 100644
--- a/sound/soc/codecs/Kconfig
+++ b/sound/soc/codecs/Kconfig
@@ -896,6 +896,9 @@ config SND_SOC_CS35L56_SDW
help
Enable support for Cirrus Logic CS35L56 boosted amplifier with SoundWire control

+config SND_SOC_CS35L56_CAL_SYSFS_COMMON
+ bool
+
config SND_SOC_CS40L50
tristate "Cirrus Logic CS40L50 CODEC"
depends on MFD_CS40L50_CORE
diff --git a/sound/soc/codecs/cs35l56-shared.c b/sound/soc/codecs/cs35l56-shared.c
index 1ecfc38d8eb4..dc6e49e3421a 100644
--- a/sound/soc/codecs/cs35l56-shared.c
+++ b/sound/soc/codecs/cs35l56-shared.c
@@ -6,11 +6,15 @@
// Cirrus Logic International Semiconductor Ltd.

#include <linux/array_size.h>
+#include <linux/cleanup.h>
#include <linux/firmware/cirrus/wmfw.h>
#include <linux/gpio/consumer.h>
#include <linux/regmap.h>
#include <linux/regulator/consumer.h>
#include <linux/spi/spi.h>
+#include <linux/stddef.h>
+#include <linux/string.h>
+#include <linux/string_choices.h>
#include <linux/types.h>
#include <sound/cs-amp-lib.h>

@@ -206,6 +210,8 @@ static bool cs35l56_readable_reg(struct device *dev, unsigned int reg)
case CS35L56_IRQ1_MASK_8:
case CS35L56_IRQ1_MASK_18:
case CS35L56_IRQ1_MASK_20:
+ case CS35L56_MIXER_NGATE_CH1_CFG:
+ case CS35L56_MIXER_NGATE_CH2_CFG:
case CS35L56_DSP_VIRTUAL1_MBOX_1:
case CS35L56_DSP_VIRTUAL1_MBOX_2:
case CS35L56_DSP_VIRTUAL1_MBOX_3:
@@ -263,6 +269,8 @@ static bool cs35l56_common_volatile_reg(unsigned int reg)
case CS35L56_IRQ1_EINT_1 ... CS35L56_IRQ1_EINT_8:
case CS35L56_IRQ1_EINT_18:
case CS35L56_IRQ1_EINT_20:
+ case CS35L56_MIXER_NGATE_CH1_CFG:
+ case CS35L56_MIXER_NGATE_CH2_CFG:
case CS35L56_DSP_VIRTUAL1_MBOX_1:
case CS35L56_DSP_VIRTUAL1_MBOX_2:
case CS35L56_DSP_VIRTUAL1_MBOX_3:
@@ -724,15 +732,11 @@ static void cs35l56_issue_wake_event(struct cs35l56_base *cs35l56_base)
cs35l56_wait_control_port_ready();
}

-int cs35l56_runtime_suspend_common(struct cs35l56_base *cs35l56_base)
+static int cs35l56_wait_for_ps3(struct cs35l56_base *cs35l56_base)
{
unsigned int val;
int ret;

- if (!cs35l56_base->init_done)
- return 0;
-
- /* Firmware must have entered a power-save state */
ret = regmap_read_poll_timeout(cs35l56_base->regmap,
cs35l56_base->fw_reg->transducer_actual_ps,
val, (val >= CS35L56_PS3),
@@ -741,6 +745,17 @@ int cs35l56_runtime_suspend_common(struct cs35l56_base *cs35l56_base)
if (ret)
dev_warn(cs35l56_base->dev, "PS3 wait failed: %d\n", ret);

+ return ret;
+}
+
+int cs35l56_runtime_suspend_common(struct cs35l56_base *cs35l56_base)
+{
+ if (!cs35l56_base->init_done)
+ return 0;
+
+ /* Firmware must have entered a power-save state */
+ cs35l56_wait_for_ps3(cs35l56_base);
+
/* Clear BOOT_DONE so it can be used to detect a reboot */
regmap_write(cs35l56_base->regmap, CS35L56_IRQ1_EINT_4, CS35L56_OTP_BOOT_DONE_MASK);

@@ -839,6 +854,8 @@ void cs35l56_init_cs_dsp(struct cs35l56_base *cs35l56_base, struct cs_dsp *cs_ds
cs_dsp->mem = cs35l56_dsp1_regions;
cs_dsp->num_mems = ARRAY_SIZE(cs35l56_dsp1_regions);
cs_dsp->no_core_startstop = true;
+
+ cs35l56_base->dsp = cs_dsp;
}
EXPORT_SYMBOL_NS_GPL(cs35l56_init_cs_dsp, "SND_SOC_CS35L56_SHARED");

@@ -942,6 +959,232 @@ int cs35l56_get_calibration(struct cs35l56_base *cs35l56_base)
}
EXPORT_SYMBOL_NS_GPL(cs35l56_get_calibration, "SND_SOC_CS35L56_SHARED");

+static int cs35l56_stash_calibration(struct cs35l56_base *cs35l56_base,
+ const struct cirrus_amp_cal_data *data)
+{
+
+ /* Ignore if it is empty */
+ if (!data->calTime[0] && !data->calTime[1])
+ return -ENODATA;
+
+ if (cs_amp_cal_target_u64(data) != cs35l56_base->silicon_uid) {
+ dev_err(cs35l56_base->dev, "cal_data not for this silicon ID\n");
+ return -EINVAL;
+ }
+
+ cs35l56_base->cal_data = *data;
+ cs35l56_base->cal_data_valid = true;
+
+ return 0;
+}
+
+static int cs35l56_perform_calibration(struct cs35l56_base *cs35l56_base)
+{
+ const struct cirrus_amp_cal_controls *calibration_controls =
+ cs35l56_base->calibration_controls;
+ struct cs_dsp *dsp = cs35l56_base->dsp;
+ struct cirrus_amp_cal_data cal_data;
+ struct cs_dsp_coeff_ctl *ctl;
+ bool ngate_ch1_was_enabled = false;
+ bool ngate_ch2_was_enabled = false;
+ int cali_norm_en_alg_id, cali_norm_en_mem;
+ int ret;
+ __be32 val;
+
+ if (cs35l56_base->silicon_uid == 0) {
+ dev_err(cs35l56_base->dev, "Cannot calibrate: no silicon UID\n");
+ return -ENXIO;
+ }
+
+ switch (cs35l56_base->type) {
+ case 0x54:
+ case 0x56:
+ case 0x57:
+ if (cs35l56_base->rev < 0xb2) {
+ cali_norm_en_alg_id = 0x9f22f;
+ cali_norm_en_mem = WMFW_ADSP2_YM;
+ } else {
+ cali_norm_en_alg_id = 0x9f210;
+ cali_norm_en_mem = WMFW_ADSP2_XM;
+ }
+ break;
+ default:
+ cali_norm_en_alg_id = 0xbf210;
+ cali_norm_en_mem = WMFW_ADSP2_XM;
+ break;
+ }
+
+ ret = pm_runtime_resume_and_get(cs35l56_base->dev);
+ if (ret)
+ return ret;
+
+ ret = cs35l56_wait_for_ps3(cs35l56_base);
+ if (ret)
+ goto err_pm_put;
+
+ regmap_update_bits_check(cs35l56_base->regmap, CS35L56_MIXER_NGATE_CH1_CFG,
+ CS35L56_AUX_NGATE_CHn_EN, 0, &ngate_ch1_was_enabled);
+ regmap_update_bits_check(cs35l56_base->regmap, CS35L56_MIXER_NGATE_CH2_CFG,
+ CS35L56_AUX_NGATE_CHn_EN, 0, &ngate_ch2_was_enabled);
+
+ scoped_guard(mutex, &dsp->pwr_lock) {
+ ctl = cs_dsp_get_ctl(dsp,
+ calibration_controls->status,
+ calibration_controls->mem_region,
+ calibration_controls->alg_id);
+ if (!ctl) {
+ dev_err(cs35l56_base->dev, "Could not get %s control\n",
+ calibration_controls->status);
+ ret = -ENXIO;
+ goto err;
+ }
+
+ val = cpu_to_be32(0);
+ ret = cs_dsp_coeff_write_ctrl(cs_dsp_get_ctl(dsp,
+ "CALI_NORM_EN",
+ cali_norm_en_mem,
+ cali_norm_en_alg_id),
+ 0, &val, sizeof(val));
+ if (ret < 0) {
+ dev_err(cs35l56_base->dev, "Could not write %s: %d\n", "CALI_NORM_EN", ret);
+ goto err;
+ }
+
+ ret = cs35l56_mbox_send(cs35l56_base, CS35L56_MBOX_CMD_AUDIO_CALIBRATION);
+ if (ret)
+ goto err;
+
+ if (read_poll_timeout(cs_dsp_coeff_read_ctrl, ret,
+ (val == cpu_to_be32(1)),
+ CS35L56_CALIBRATION_POLL_US,
+ CS35L56_CALIBRATION_TIMEOUT_US,
+ true,
+ ctl, 0, &val, sizeof(val))) {
+ dev_err(cs35l56_base->dev, "Calibration timed out (CAL_STATUS: %u)\n",
+ be32_to_cpu(val));
+ ret = -ETIMEDOUT;
+ goto err;
+ }
+ }
+
+ cs35l56_base->cal_data_valid = false;
+ memset(&cal_data, 0, sizeof(cal_data));
+ ret = cs_amp_read_cal_coeffs(dsp, calibration_controls, &cal_data);
+ if (ret)
+ goto err;
+
+ dev_info(cs35l56_base->dev, "Cal status:%d calR:%d ambient:%d\n",
+ cal_data.calStatus, cal_data.calR, cal_data.calAmbient);
+
+ cal_data.calTarget[0] = (u32)cs35l56_base->silicon_uid;
+ cal_data.calTarget[1] = (u32)(cs35l56_base->silicon_uid >> 32);
+ cs35l56_base->cal_data = cal_data;
+ cs35l56_base->cal_data_valid = true;
+
+ ret = 0;
+
+err:
+ if (ngate_ch1_was_enabled) {
+ regmap_set_bits(cs35l56_base->regmap, CS35L56_MIXER_NGATE_CH1_CFG,
+ CS35L56_AUX_NGATE_CHn_EN);
+ }
+ if (ngate_ch2_was_enabled) {
+ regmap_set_bits(cs35l56_base->regmap, CS35L56_MIXER_NGATE_CH2_CFG,
+ CS35L56_AUX_NGATE_CHn_EN);
+ }
+err_pm_put:
+ pm_runtime_put(cs35l56_base->dev);
+
+ return ret;
+}
+
+ssize_t cs35l56_calibrate_sysfs_store(struct cs35l56_base *cs35l56_base,
+ const char *buf, size_t count)
+{
+ static const char * const options[] = { "factory" };
+ int ret;
+
+ if (!IS_ENABLED(CONFIG_SND_SOC_CS35L56_CAL_SYSFS_COMMON))
+ return -ENXIO;
+
+ switch (sysfs_match_string(options, buf)) {
+ case 0:
+ ret = cs35l56_perform_calibration(cs35l56_base);
+ if (ret < 0)
+ return ret;
+ break;
+ default:
+ return -ENXIO;
+ }
+
+ return count;
+}
+EXPORT_SYMBOL_NS_GPL(cs35l56_calibrate_sysfs_store, "SND_SOC_CS35L56_SHARED");
+
+ssize_t cs35l56_cal_ambient_sysfs_store(struct cs35l56_base *cs35l56_base,
+ const char *buf, size_t count)
+{
+ unsigned long val;
+ int ret;
+
+ if (!IS_ENABLED(CONFIG_SND_SOC_CS35L56_CAL_SYSFS_COMMON))
+ return -ENXIO;
+
+ ret = pm_runtime_resume_and_get(cs35l56_base->dev);
+ if (ret)
+ return ret;
+
+ ret = kstrtoul(buf, 10, &val);
+ if (ret) {
+ ret = -ENXIO;
+ goto out;
+ }
+
+ ret = cs_amp_write_ambient_temp(cs35l56_base->dsp, cs35l56_base->calibration_controls, val);
+out:
+ pm_runtime_put(cs35l56_base->dev);
+
+ if (ret < 0)
+ return ret;
+
+ return count;
+}
+EXPORT_SYMBOL_NS_GPL(cs35l56_cal_ambient_sysfs_store, "SND_SOC_CS35L56_SHARED");
+
+ssize_t cs35l56_cal_data_sysfs_read(struct cs35l56_base *cs35l56_base,
+ char *buf, loff_t pos, size_t count)
+{
+ if (!IS_ENABLED(CONFIG_SND_SOC_CS35L56_CAL_SYSFS_COMMON))
+ return -ENXIO;
+
+ if (!cs35l56_base->cal_data_valid)
+ return 0;
+
+ return memory_read_from_buffer(buf, count, &pos, &cs35l56_base->cal_data,
+ sizeof(cs35l56_base->cal_data));
+}
+EXPORT_SYMBOL_NS_GPL(cs35l56_cal_data_sysfs_read, "SND_SOC_CS35L56_SHARED");
+
+ssize_t cs35l56_cal_data_sysfs_write(struct cs35l56_base *cs35l56_base,
+ const char *buf, loff_t pos, size_t count)
+{
+ const struct cirrus_amp_cal_data *cal_data = (const void *)buf;
+ int ret;
+
+ if (!IS_ENABLED(CONFIG_SND_SOC_CS35L56_CAL_SYSFS_COMMON))
+ return -ENXIO;
+
+ if (count != sizeof(*cal_data))
+ return -EMSGSIZE;
+
+ ret = cs35l56_stash_calibration(cs35l56_base, cal_data);
+ if (ret)
+ return ret;
+
+ return count;
+}
+EXPORT_SYMBOL_NS_GPL(cs35l56_cal_data_sysfs_write, "SND_SOC_CS35L56_SHARED");
+
int cs35l56_read_prot_status(struct cs35l56_base *cs35l56_base,
bool *fw_missing, unsigned int *fw_version)
{
--
2.47.3