[PATCH 0/9] ALSA: cs35l56: Apply calibration from EFI
Factory calibration of the speakers stores the calibration information into an EFI variable.
This set of patches adds support for applying speaker calibration data from that EFI variable.
Patch #1 and #2 are bugfixes that are prerequisites.
The HDA patches #7 and #8 depend on the ASoC patches.
Patch #9 is a KUnit test for the new cs-amp-lib library.
Richard Fitzgerald (8): ASoC: wm_adsp: Fix missing mutex_lock in wm_adsp_write_ctl() ASoC: wm_adsp: Add wm_adsp_start() and wm_adsp_stop() ASoC: cs-amp-lib: Add helpers for factory calibration data ASoC: cs35l56: Add helper functions for amp calibration ASoC: cs35l56: Apply amp calibration from EFI data ALSA: hda: hda_cs_dsp_ctl: Add helper function to write calibration ALSA: hda: cs35l56: Apply amp calibration from EFI data ASoC: cs-amp-lib: Add KUnit test for calibration helpers
Simon Trimmer (1): ALSA: hda: hda_cs_dsp_ctl: Only call notify when a control has been added to a card
include/sound/cs-amp-lib.h | 66 +++ include/sound/cs35l56.h | 10 + sound/pci/hda/Kconfig | 2 + sound/pci/hda/cs35l56_hda.c | 36 +- sound/pci/hda/hda_cs_dsp_ctl.c | 52 ++- sound/pci/hda/hda_cs_dsp_ctl.h | 4 + sound/soc/codecs/Kconfig | 18 + sound/soc/codecs/Makefile | 4 + sound/soc/codecs/cs-amp-lib-test.c | 709 +++++++++++++++++++++++++++++ sound/soc/codecs/cs-amp-lib.c | 270 +++++++++++ sound/soc/codecs/cs35l56-sdw.c | 20 + sound/soc/codecs/cs35l56-shared.c | 83 ++++ sound/soc/codecs/cs35l56.c | 43 +- sound/soc/codecs/wm_adsp.c | 30 +- sound/soc/codecs/wm_adsp.h | 2 + 15 files changed, 1328 insertions(+), 21 deletions(-) create mode 100644 include/sound/cs-amp-lib.h create mode 100644 sound/soc/codecs/cs-amp-lib-test.c create mode 100644 sound/soc/codecs/cs-amp-lib.c
wm_adsp_write_ctl() must hold the pwr_lock mutex when calling cs_dsp_get_ctl().
This was previously partially fixed by commit 781118bc2fc1 ("ASoC: wm_adsp: Fix missing locking in wm_adsp_[read|write]_ctl()") but this only put locking around the call to cs_dsp_coeff_write_ctrl(), missing the call to cs_dsp_get_ctl().
Signed-off-by: Richard Fitzgerald rf@opensource.cirrus.com Fixes: 781118bc2fc1 ("ASoC: wm_adsp: Fix missing locking in wm_adsp_[read|write]_ctl()") --- sound/soc/codecs/wm_adsp.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/sound/soc/codecs/wm_adsp.c b/sound/soc/codecs/wm_adsp.c index 36ea0dcdc7ab..9cb9068c0462 100644 --- a/sound/soc/codecs/wm_adsp.c +++ b/sound/soc/codecs/wm_adsp.c @@ -683,11 +683,12 @@ static void wm_adsp_control_remove(struct cs_dsp_coeff_ctl *cs_ctl) int wm_adsp_write_ctl(struct wm_adsp *dsp, const char *name, int type, unsigned int alg, void *buf, size_t len) { - struct cs_dsp_coeff_ctl *cs_ctl = cs_dsp_get_ctl(&dsp->cs_dsp, name, type, alg); + struct cs_dsp_coeff_ctl *cs_ctl; struct wm_coeff_ctl *ctl; int ret;
mutex_lock(&dsp->cs_dsp.pwr_lock); + cs_ctl = cs_dsp_get_ctl(&dsp->cs_dsp, name, type, alg); ret = cs_dsp_coeff_write_ctrl(cs_ctl, 0, buf, len); mutex_unlock(&dsp->cs_dsp.pwr_lock);
From: Simon Trimmer simont@opensource.cirrus.com
There is a window of time where the cs_dsp_coeff_ctl has been created but has not been registered as a control with ALSA.
The hda_cs_dsp_write_ctl() helper function can be used update the cs_dsp coefficient before it has been registered, but it should only call snd_ctl_notify() once the control is associated with a card.
Signed-off-by: Simon Trimmer simont@opensource.cirrus.com Signed-off-by: Richard Fitzgerald rf@opensource.cirrus.com Fixes: e414b05e724f ("ALSA: hda: hda_cs_dsp_ctl: Add apis to write the controls directly") --- sound/pci/hda/hda_cs_dsp_ctl.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/sound/pci/hda/hda_cs_dsp_ctl.c b/sound/pci/hda/hda_cs_dsp_ctl.c index 463ca06036bf..0f5fdd44721c 100644 --- a/sound/pci/hda/hda_cs_dsp_ctl.c +++ b/sound/pci/hda/hda_cs_dsp_ctl.c @@ -224,9 +224,10 @@ int hda_cs_dsp_write_ctl(struct cs_dsp *dsp, const char *name, int type, if (ret == 0 || (cs_ctl->flags & WMFW_CTL_FLAG_SYS)) return 0;
+ /* Notify when a control has been added to a card */ ctl = cs_ctl->priv; - - snd_ctl_notify(ctl->card, SNDRV_CTL_EVENT_MASK_VALUE, &ctl->kctl->id); + if (ctl && ctl->kctl) + snd_ctl_notify(ctl->card, SNDRV_CTL_EVENT_MASK_VALUE, &ctl->kctl->id);
return 0; }
Separate the functionality of wm_adsp_event() into two exported functions wm_adsp_start() and wm_adsp_stop().
This allows the codec driver to start and stop the DSP outside of a DAPM widget.
Signed-off-by: Richard Fitzgerald rf@opensource.cirrus.com --- sound/soc/codecs/wm_adsp.c | 27 ++++++++++++++++++--------- sound/soc/codecs/wm_adsp.h | 2 ++ 2 files changed, 20 insertions(+), 9 deletions(-)
diff --git a/sound/soc/codecs/wm_adsp.c b/sound/soc/codecs/wm_adsp.c index 9cb9068c0462..7d5c096e06cd 100644 --- a/sound/soc/codecs/wm_adsp.c +++ b/sound/soc/codecs/wm_adsp.c @@ -1093,27 +1093,36 @@ static void wm_adsp_event_post_stop(struct cs_dsp *cs_dsp) dsp->fatal_error = false; }
+int wm_adsp_run(struct wm_adsp *dsp) +{ + flush_work(&dsp->boot_work); + + return cs_dsp_run(&dsp->cs_dsp); +} +EXPORT_SYMBOL_GPL(wm_adsp_run); + +void wm_adsp_stop(struct wm_adsp *dsp) +{ + cs_dsp_stop(&dsp->cs_dsp); +} +EXPORT_SYMBOL_GPL(wm_adsp_stop); + int wm_adsp_event(struct snd_soc_dapm_widget *w, struct snd_kcontrol *kcontrol, int event) { struct snd_soc_component *component = snd_soc_dapm_to_component(w->dapm); struct wm_adsp *dsps = snd_soc_component_get_drvdata(component); struct wm_adsp *dsp = &dsps[w->shift]; - int ret = 0;
switch (event) { case SND_SOC_DAPM_POST_PMU: - flush_work(&dsp->boot_work); - ret = cs_dsp_run(&dsp->cs_dsp); - break; + return wm_adsp_run(dsp); case SND_SOC_DAPM_PRE_PMD: - cs_dsp_stop(&dsp->cs_dsp); - break; + wm_adsp_stop(dsp); + return 0; default: - break; + return 0; } - - return ret; } EXPORT_SYMBOL_GPL(wm_adsp_event);
diff --git a/sound/soc/codecs/wm_adsp.h b/sound/soc/codecs/wm_adsp.h index 067d807a7ca8..e53dfcf1f78f 100644 --- a/sound/soc/codecs/wm_adsp.h +++ b/sound/soc/codecs/wm_adsp.h @@ -98,6 +98,8 @@ irqreturn_t wm_adsp2_bus_error(int irq, void *data); irqreturn_t wm_halo_bus_error(int irq, void *data); irqreturn_t wm_halo_wdt_expire(int irq, void *data);
+int wm_adsp_run(struct wm_adsp *dsp); +void wm_adsp_stop(struct wm_adsp *dsp); int wm_adsp_event(struct snd_soc_dapm_widget *w, struct snd_kcontrol *kcontrol, int event);
Create a new library for code that is used by multiple Cirrus Logic amps. This initially implements extracting amp calibration data from EFI and writing it to firmware controls.
During factory calibration of built-in speakers the firmware calibration constants are stored in an EFI file. The file contains an array of calibration constants for each of the speakers. cs_amp_get_calibration_data() searches for an entry matching the requested UID stamp, otherwise by array index. If the data is found in EFI the constants for that speaker are copied back to the caller.
If EFI is not enabled, the cs_amp_get_calibration_data() implementation will compile to simply return -ENOENT and the linker can drop the code.
The code to write calibration controls uses wm_adsp. Not all drivers use wm_adsp (notably, HDA drivers do not) so cs-amp-lib does not force building of wm_adsp. Instead, the code will compile away the call to wm_adsp_write_ctl() if wm_adsp is not reachable.
This strategy of conditional code allows cs-amp-lib to be shared by multiple drivers without forcing inclusion of other modules that might be unnecessary.
The calls to efi.get_variable() and wm_adsp_write_ctl() are in small wrapper functions. This is so that a KUNIT_STATIC_STUB_REDIRECT can be added in a future patch to redirect these calls to replacement functions for KUnit testing.
Signed-off-by: Richard Fitzgerald rf@opensource.cirrus.com --- include/sound/cs-amp-lib.h | 52 +++++++ sound/soc/codecs/Kconfig | 3 + sound/soc/codecs/Makefile | 2 + sound/soc/codecs/cs-amp-lib.c | 256 ++++++++++++++++++++++++++++++++++ 4 files changed, 313 insertions(+) create mode 100644 include/sound/cs-amp-lib.h create mode 100644 sound/soc/codecs/cs-amp-lib.c
diff --git a/include/sound/cs-amp-lib.h b/include/sound/cs-amp-lib.h new file mode 100644 index 000000000000..35f7bd848276 --- /dev/null +++ b/include/sound/cs-amp-lib.h @@ -0,0 +1,52 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Copyright (C) 2024 Cirrus Logic, Inc. and + * Cirrus Logic International Semiconductor Ltd. + */ + +#ifndef CS_AMP_LIB_H +#define CS_AMP_LIB_H + +#include <linux/efi.h> +#include <linux/types.h> + +struct wm_adsp; + +struct cirrus_amp_cal_data { + u32 calTarget[2]; + u32 calTime[2]; + s8 calAmbient; + u8 calStatus; + u16 calR; +} __packed; + +struct cirrus_amp_efi_data { + u32 size; + u32 count; + struct cirrus_amp_cal_data data[]; +} __packed; + +/** + * struct cirrus_amp_cal_controls - definition of firmware calibration controls + * @alg_id: ID of algorithm containing the controls. + * @mem_region: DSP memory region containing the controls. + * @ambient: Name of control for calAmbient value. + * @calr: Name of control for calR value. + * @status: Name of control for calStatus value. + * @checksum: Name of control for checksum value. + */ +struct cirrus_amp_cal_controls { + unsigned int alg_id; + int mem_region; + const char *ambient; + const char *calr; + const char *status; + const char *checksum; +}; + +int cs_amp_write_cal_coeffs(struct wm_adsp *dsp, + const struct cirrus_amp_cal_controls *controls, + const struct cirrus_amp_cal_data *data); +int cs_amp_get_efi_calibration_data(struct device *dev, u64 target_uid, int amp_index, + struct cirrus_amp_cal_data *out_data); +#endif /* CS_AMP_LIB_H */ diff --git a/sound/soc/codecs/Kconfig b/sound/soc/codecs/Kconfig index 027d9da85251..8356bd1256d8 100644 --- a/sound/soc/codecs/Kconfig +++ b/sound/soc/codecs/Kconfig @@ -729,6 +729,9 @@ config SND_SOC_CROS_EC_CODEC If you say yes here you will get support for the ChromeOS Embedded Controller's Audio Codec.
+config SND_SOC_CS_AMP_LIB + tristate + config SND_SOC_CS35L32 tristate "Cirrus Logic CS35L32 CODEC" depends on I2C diff --git a/sound/soc/codecs/Makefile b/sound/soc/codecs/Makefile index 4080646b2dd6..0fc40640e5d0 100644 --- a/sound/soc/codecs/Makefile +++ b/sound/soc/codecs/Makefile @@ -59,6 +59,7 @@ snd-soc-chv3-codec-objs := chv3-codec.o snd-soc-cpcap-objs := cpcap.o snd-soc-cq93vc-objs := cq93vc.o snd-soc-cros-ec-codec-objs := cros_ec_codec.o +snd-soc-cs-amp-lib-objs := cs-amp-lib.o snd-soc-cs35l32-objs := cs35l32.o snd-soc-cs35l33-objs := cs35l33.o snd-soc-cs35l34-objs := cs35l34.o @@ -452,6 +453,7 @@ obj-$(CONFIG_SND_SOC_CHV3_CODEC) += snd-soc-chv3-codec.o obj-$(CONFIG_SND_SOC_CQ0093VC) += snd-soc-cq93vc.o obj-$(CONFIG_SND_SOC_CPCAP) += snd-soc-cpcap.o obj-$(CONFIG_SND_SOC_CROS_EC_CODEC) += snd-soc-cros-ec-codec.o +obj-$(CONFIG_SND_SOC_CS_AMP_LIB) += snd-soc-cs-amp-lib.o obj-$(CONFIG_SND_SOC_CS35L32) += snd-soc-cs35l32.o obj-$(CONFIG_SND_SOC_CS35L33) += snd-soc-cs35l33.o obj-$(CONFIG_SND_SOC_CS35L34) += snd-soc-cs35l34.o diff --git a/sound/soc/codecs/cs-amp-lib.c b/sound/soc/codecs/cs-amp-lib.c new file mode 100644 index 000000000000..0e1249342a78 --- /dev/null +++ b/sound/soc/codecs/cs-amp-lib.c @@ -0,0 +1,256 @@ +// SPDX-License-Identifier: GPL-2.0-only +// +// Common code for Cirrus Logic Smart Amplifiers +// +// Copyright (C) 2024 Cirrus Logic, Inc. and +// Cirrus Logic International Semiconductor Ltd. + +#include <asm/byteorder.h> +#include <linux/dev_printk.h> +#include <linux/efi.h> +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/types.h> +#include <sound/cs-amp-lib.h> +#include "wm_adsp.h" + +#define CS_AMP_CAL_GUID \ + EFI_GUID(0x02f9af02, 0x7734, 0x4233, 0xb4, 0x3d, 0x93, 0xfe, 0x5a, 0xa3, 0x5d, 0xb3) + +#define CS_AMP_CAL_NAME L"CirrusSmartAmpCalibrationData" + +static int cs_amp_write_cal_coeff(struct wm_adsp *dsp, + const struct cirrus_amp_cal_controls *controls, + const char *ctl_name, u32 val) +{ + __be32 beval = cpu_to_be32(val); + int ret; + + if (IS_REACHABLE(CONFIG_SND_SOC_WM_ADSP)) { + ret = wm_adsp_write_ctl(dsp, ctl_name, controls->mem_region, + controls->alg_id, &beval, sizeof(beval)); + if (ret) + dev_err(dsp->cs_dsp.dev, "Failed to write to '%s': %d\n", ctl_name, ret); + + return ret; + } + + return -ENODEV; +} + +static int _cs_amp_write_cal_coeffs(struct wm_adsp *dsp, + const struct cirrus_amp_cal_controls *controls, + const struct cirrus_amp_cal_data *data) +{ + int ret; + + dev_dbg(dsp->cs_dsp.dev, "Calibration: Ambient=%#x, Status=%#x, CalR=%d\n", + data->calAmbient, data->calStatus, data->calR); + + ret = cs_amp_write_cal_coeff(dsp, controls, controls->ambient, data->calAmbient); + if (ret) + return ret; + + ret = cs_amp_write_cal_coeff(dsp, controls, controls->calr, data->calR); + if (ret) + return ret; + + ret = cs_amp_write_cal_coeff(dsp, controls, controls->status, data->calStatus); + if (ret) + return ret; + + ret = cs_amp_write_cal_coeff(dsp, controls, controls->checksum, data->calR + 1); + if (ret) + return ret; + + return 0; +} + +/** + * cs_amp_write_cal_coeffs - Write calibration data to firmware controls. + * @dsp: Pointer to struct wm_adsp. + * @controls: Pointer to definition of firmware controls to be written. + * @data: Pointer to calibration data. + * + * Returns: 0 on success, else negative error value. + */ +int cs_amp_write_cal_coeffs(struct wm_adsp *dsp, + const struct cirrus_amp_cal_controls *controls, + const struct cirrus_amp_cal_data *data) +{ + if (IS_REACHABLE(CONFIG_SND_SOC_WM_ADSP)) + return _cs_amp_write_cal_coeffs(dsp, controls, data); + else + return -ENODEV; +} +EXPORT_SYMBOL_NS_GPL(cs_amp_write_cal_coeffs, SND_SOC_CS_AMP_LIB); + +static efi_status_t cs_amp_get_efi_variable(efi_char16_t *name, + efi_guid_t *guid, + unsigned long *size, + void *buf) +{ + u32 attr; + + if (IS_ENABLED(CONFIG_EFI)) + return efi.get_variable(name, guid, &attr, size, buf); + + return EFI_NOT_FOUND; +} + +static struct cirrus_amp_efi_data *cs_amp_get_cal_efi_buffer(struct device *dev) +{ + struct cirrus_amp_efi_data *efi_data; + unsigned long data_size = 0; + u8 *data; + efi_status_t status; + int ret; + + /* Get real size of UEFI variable */ + status = cs_amp_get_efi_variable(CS_AMP_CAL_NAME, &CS_AMP_CAL_GUID, &data_size, NULL); + if (status != EFI_BUFFER_TOO_SMALL) + return ERR_PTR(-ENOENT); + + if (data_size < sizeof(*efi_data)) { + dev_err(dev, "EFI cal variable truncated\n"); + return ERR_PTR(-EOVERFLOW); + } + + /* Get variable contents into buffer */ + data = kmalloc(data_size, GFP_KERNEL); + if (!data) + return ERR_PTR(-ENOMEM); + + status = cs_amp_get_efi_variable(CS_AMP_CAL_NAME, &CS_AMP_CAL_GUID, &data_size, data); + if (status != EFI_SUCCESS) { + ret = efi_status_to_err(status); + goto err; + } + + efi_data = (struct cirrus_amp_efi_data *)data; + dev_dbg(dev, "Calibration: Size=%d, Amp Count=%d\n", efi_data->size, efi_data->count); + + if ((efi_data->count > 128) || + offsetof(struct cirrus_amp_efi_data, data[efi_data->count]) > data_size) { + dev_err(dev, "EFI cal variable truncated\n"); + ret = -EOVERFLOW; + goto err; + } + + return efi_data; + +err: + kfree(data); + dev_err(dev, "Failed to read calibration data from EFI: %d\n", ret); + + return ERR_PTR(ret); +} + +static u64 cs_amp_cal_target_u64(const struct cirrus_amp_cal_data *data) +{ + return ((u64)data->calTarget[1] << 32) | data->calTarget[0]; +} + +static int _cs_amp_get_efi_calibration_data(struct device *dev, u64 target_uid, int amp_index, + struct cirrus_amp_cal_data *out_data) +{ + struct cirrus_amp_efi_data *efi_data; + struct cirrus_amp_cal_data *cal = NULL; + int i, ret; + + efi_data = cs_amp_get_cal_efi_buffer(dev); + if (IS_ERR(efi_data)) + return PTR_ERR(efi_data); + + if (target_uid) { + for (i = 0; i < efi_data->count; ++i) { + u64 cal_target = cs_amp_cal_target_u64(&efi_data->data[i]); + + /* Skip entries with unpopulated silicon ID */ + if (cal_target == 0) + continue; + + if (cal_target == target_uid) { + cal = &efi_data->data[i]; + break; + } + } + } + + if (!cal && (amp_index >= 0) && (amp_index < efi_data->count)) { + u64 cal_target = cs_amp_cal_target_u64(&efi_data->data[amp_index]); + + /* + * Treat unpopulated cal_target as a wildcard. + * If target_uid != 0 we can only get here if cal_target == 0 + * or it didn't match any cal_target value. + * If target_uid == 0 it is a wildcard. + */ + if ((cal_target == 0) || (target_uid == 0)) + cal = &efi_data->data[amp_index]; + else + dev_warn(dev, "Calibration entry %d does not match silicon ID", amp_index); + } + + if (cal) { + memcpy(out_data, cal, sizeof(*out_data)); + ret = 0; + } else { + dev_warn(dev, "No calibration for silicon ID %#llx\n", target_uid); + ret = -ENOENT; + } + + kfree(efi_data); + + return ret; +} + +/** + * cs_amp_get_efi_calibration_data - get an entry from calibration data in EFI. + * @dev: struct device of the caller. + * @target_uid: UID to match, or zero to ignore UID matching. + * @amp_index: Entry index to use, or -1 to prevent lookup by index. + * @out_data: struct cirrus_amp_cal_data where the entry will be copied. + * + * This function can perform 3 types of lookup: + * + * (target_uid > 0, amp_index >= 0) + * UID search with fallback to using the array index. + * Search the calibration data for a non-zero calTarget that matches + * target_uid, and if found return that entry. Else, if the entry at + * [amp_index] has calTarget == 0, return that entry. Else fail. + * + * (target_uid > 0, amp_index < 0) + * UID search only. + * Search the calibration data for a non-zero calTarget that matches + * target_uid, and if found return that entry. Else fail. + * + * (target_uid == 0, amp_index >= 0) + * Array index fetch only. + * Return the entry at [amp_index]. + * + * An array lookup will be skipped if amp_index exceeds the number of + * entries in the calibration array, and in this case the return will + * be -ENOENT. An out-of-range amp_index does not prevent matching by + * target_uid - it has the same effect as passing amp_index < 0. + * + * If the EFI data is too short to be a valid entry, or the entry count + * in the EFI data overflows the actual length of the data, this function + * returns -EOVERFLOW. + * + * Return: 0 if the entry was found, -ENOENT if no entry was found, + * -EOVERFLOW if the EFI file is corrupt, else other error value. + */ +int cs_amp_get_efi_calibration_data(struct device *dev, u64 target_uid, int amp_index, + struct cirrus_amp_cal_data *out_data) +{ + if (IS_ENABLED(CONFIG_EFI)) + return _cs_amp_get_efi_calibration_data(dev, target_uid, amp_index, out_data); + else + return -ENOENT; +} +EXPORT_SYMBOL_NS_GPL(cs_amp_get_efi_calibration_data, SND_SOC_CS_AMP_LIB); + +MODULE_DESCRIPTION("Cirrus Logic amplifier library"); +MODULE_AUTHOR("Richard Fitzgerald rf@opensource.cirrus.com"); +MODULE_LICENSE("GPL");
Adds some helper functions and data for applying amp calibration.
1. cs35l56_read_silicon_uid() to get the silicon ID that is used to search for the correct calibration data entry.
2. Add the registers for the silicon ID to the readable registers.
3. cs35l56_get_calibration() wrapper around cs_amp_get_efi_calibration_data()
4. cs35l56_calibration_controls() table of the firmware controls for calibration data.
5. Added members to struct cs35l56_base to store the calibration data.
Signed-off-by: Richard Fitzgerald rf@opensource.cirrus.com --- include/sound/cs35l56.h | 10 ++++ sound/soc/codecs/Kconfig | 1 + sound/soc/codecs/cs35l56-shared.c | 83 +++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+)
diff --git a/include/sound/cs35l56.h b/include/sound/cs35l56.h index b24716ab2750..4014ed7097b3 100644 --- a/include/sound/cs35l56.h +++ b/include/sound/cs35l56.h @@ -12,6 +12,7 @@ #include <linux/firmware/cirrus/cs_dsp.h> #include <linux/regulator/consumer.h> #include <linux/regmap.h> +#include <sound/cs-amp-lib.h>
#define CS35L56_DEVID 0x0000000 #define CS35L56_REVID 0x0000004 @@ -23,6 +24,9 @@ #define CS35L56_BLOCK_ENABLES2 0x000201C #define CS35L56_REFCLK_INPUT 0x0002C04 #define CS35L56_GLOBAL_SAMPLE_RATE 0x0002C0C +#define CS35L56_OTP_MEM_53 0x00300D4 +#define CS35L56_OTP_MEM_54 0x00300D8 +#define CS35L56_OTP_MEM_55 0x00300DC #define CS35L56_ASP1_ENABLES1 0x0004800 #define CS35L56_ASP1_CONTROL1 0x0004804 #define CS35L56_ASP1_CONTROL2 0x0004808 @@ -262,6 +266,9 @@ struct cs35l56_base { bool fw_patched; bool secured; bool can_hibernate; + bool cal_data_valid; + s8 cal_index; + struct cirrus_amp_cal_data cal_data; struct gpio_desc *reset_gpio; };
@@ -269,6 +276,8 @@ extern struct regmap_config cs35l56_regmap_i2c; extern struct regmap_config cs35l56_regmap_spi; extern struct regmap_config cs35l56_regmap_sdw;
+extern const struct cirrus_amp_cal_controls cs35l56_calibration_controls; + extern const char * const cs35l56_tx_input_texts[CS35L56_NUM_INPUT_SRC]; extern const unsigned int cs35l56_tx_input_values[CS35L56_NUM_INPUT_SRC];
@@ -286,6 +295,7 @@ int cs35l56_is_fw_reload_needed(struct cs35l56_base *cs35l56_base); 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); int cs35l56_read_prot_status(struct cs35l56_base *cs35l56_base, bool *fw_missing, unsigned int *fw_version); int cs35l56_hw_init(struct cs35l56_base *cs35l56_base); diff --git a/sound/soc/codecs/Kconfig b/sound/soc/codecs/Kconfig index 8356bd1256d8..15f287784d8b 100644 --- a/sound/soc/codecs/Kconfig +++ b/sound/soc/codecs/Kconfig @@ -800,6 +800,7 @@ config SND_SOC_CS35L56 tristate
config SND_SOC_CS35L56_SHARED + select SND_SOC_CS_AMP_LIB tristate
config SND_SOC_CS35L56_I2C diff --git a/sound/soc/codecs/cs35l56-shared.c b/sound/soc/codecs/cs35l56-shared.c index cb4e83126b08..20b6dbd3fbab 100644 --- a/sound/soc/codecs/cs35l56-shared.c +++ b/sound/soc/codecs/cs35l56-shared.c @@ -5,10 +5,12 @@ // Copyright (C) 2023 Cirrus Logic, Inc. and // Cirrus Logic International Semiconductor Ltd.
+#include <linux/firmware/cirrus/wmfw.h> #include <linux/gpio/consumer.h> #include <linux/regmap.h> #include <linux/regulator/consumer.h> #include <linux/types.h> +#include <sound/cs-amp-lib.h>
#include "cs35l56.h"
@@ -36,6 +38,8 @@ int cs35l56_set_patch(struct cs35l56_base *cs35l56_base) EXPORT_SYMBOL_NS_GPL(cs35l56_set_patch, SND_SOC_CS35L56_SHARED);
static const struct reg_default cs35l56_reg_defaults[] = { + /* no defaults for OTP_MEM - first read populates cache */ + { CS35L56_ASP1_ENABLES1, 0x00000000 }, { CS35L56_ASP1_CONTROL1, 0x00000028 }, { CS35L56_ASP1_CONTROL2, 0x18180200 }, @@ -91,6 +95,9 @@ static bool cs35l56_readable_reg(struct device *dev, unsigned int reg) case CS35L56_BLOCK_ENABLES2: case CS35L56_REFCLK_INPUT: case CS35L56_GLOBAL_SAMPLE_RATE: + case CS35L56_OTP_MEM_53: + case CS35L56_OTP_MEM_54: + case CS35L56_OTP_MEM_55: case CS35L56_ASP1_ENABLES1: case CS35L56_ASP1_CONTROL1: case CS35L56_ASP1_CONTROL2: @@ -629,6 +636,81 @@ void cs35l56_init_cs_dsp(struct cs35l56_base *cs35l56_base, struct cs_dsp *cs_ds } EXPORT_SYMBOL_NS_GPL(cs35l56_init_cs_dsp, SND_SOC_CS35L56_SHARED);
+struct cs35l56_pte { + u8 x; + u8 wafer_id; + u8 pte[2]; + u8 lot[3]; + u8 y; + u8 unused[3]; + u8 dvs; +} __packed; +static_assert((sizeof(struct cs35l56_pte) % sizeof(u32)) == 0); + +static int cs35l56_read_silicon_uid(struct cs35l56_base *cs35l56_base, u64 *uid) +{ + struct cs35l56_pte pte; + u64 unique_id; + int ret; + + ret = regmap_raw_read(cs35l56_base->regmap, CS35L56_OTP_MEM_53, &pte, sizeof(pte)); + if (ret) { + dev_err(cs35l56_base->dev, "Failed to read OTP: %d\n", ret); + return ret; + } + + unique_id = pte.lot[2] | (pte.lot[1] << 8) | (pte.lot[0] << 16); + unique_id <<= 32; + unique_id |= pte.x | (pte.y << 8) | (pte.wafer_id << 16) | (pte.dvs << 24); + + dev_dbg(cs35l56_base->dev, "UniqueID = %#llx\n", unique_id); + + *uid = unique_id; + + return 0; +} + +/* Firmware calibration controls */ +const struct cirrus_amp_cal_controls cs35l56_calibration_controls = { + .alg_id = 0x9f210, + .mem_region = WMFW_ADSP2_YM, + .ambient = "CAL_AMBIENT", + .calr = "CAL_R", + .status = "CAL_STATUS", + .checksum = "CAL_CHECKSUM", +}; +EXPORT_SYMBOL_NS_GPL(cs35l56_calibration_controls, SND_SOC_CS35L56_SHARED); + +int cs35l56_get_calibration(struct cs35l56_base *cs35l56_base) +{ + u64 silicon_uid; + int ret; + + /* Driver can't apply calibration to a secured part, so skip */ + if (cs35l56_base->secured) + return 0; + + ret = cs35l56_read_silicon_uid(cs35l56_base, &silicon_uid); + if (ret < 0) + return ret; + + ret = cs_amp_get_efi_calibration_data(cs35l56_base->dev, silicon_uid, + cs35l56_base->cal_index, + &cs35l56_base->cal_data); + + /* Only return an error status if probe should be aborted */ + if ((ret == -ENOENT) || (ret == -EOVERFLOW)) + return 0; + + if (ret < 0) + return ret; + + cs35l56_base->cal_data_valid = true; + + return 0; +} +EXPORT_SYMBOL_NS_GPL(cs35l56_get_calibration, SND_SOC_CS35L56_SHARED); + int cs35l56_read_prot_status(struct cs35l56_base *cs35l56_base, bool *fw_missing, unsigned int *fw_version) { @@ -923,3 +1005,4 @@ MODULE_DESCRIPTION("ASoC CS35L56 Shared"); MODULE_AUTHOR("Richard Fitzgerald rf@opensource.cirrus.com"); MODULE_AUTHOR("Simon Trimmer simont@opensource.cirrus.com"); MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS(SND_SOC_CS_AMP_LIB);
If there are factory calibration settings in EFI, extract the settings and write them to the firmware calibration controls.
This must be done after any firmware or coefficients have been downloaded to the amp.
Signed-off-by: Richard Fitzgerald rf@opensource.cirrus.com --- sound/soc/codecs/cs35l56-sdw.c | 20 ++++++++++++++++ sound/soc/codecs/cs35l56.c | 43 +++++++++++++++++++++++++++++++--- 2 files changed, 60 insertions(+), 3 deletions(-)
diff --git a/sound/soc/codecs/cs35l56-sdw.c b/sound/soc/codecs/cs35l56-sdw.c index ab960a1c171e..eaa4c706f3a2 100644 --- a/sound/soc/codecs/cs35l56-sdw.c +++ b/sound/soc/codecs/cs35l56-sdw.c @@ -161,6 +161,20 @@ static const struct regmap_bus cs35l56_regmap_bus_sdw = { .val_format_endian_default = REGMAP_ENDIAN_BIG, };
+static int cs35l56_sdw_set_cal_index(struct cs35l56_private *cs35l56) +{ + int ret; + + /* SoundWire UniqueId is used to index the calibration array */ + ret = sdw_read_no_pm(cs35l56->sdw_peripheral, SDW_SCP_DEVID_0); + if (ret < 0) + return ret; + + cs35l56->base.cal_index = ret & 0xf; + + return 0; +} + static void cs35l56_sdw_init(struct sdw_slave *peripheral) { struct cs35l56_private *cs35l56 = dev_get_drvdata(&peripheral->dev); @@ -168,6 +182,12 @@ static void cs35l56_sdw_init(struct sdw_slave *peripheral)
pm_runtime_get_noresume(cs35l56->base.dev);
+ if (cs35l56->base.cal_index < 0) { + ret = cs35l56_sdw_set_cal_index(cs35l56); + if (ret < 0) + goto out; + } + regcache_cache_only(cs35l56->base.regmap, false);
ret = cs35l56_init(cs35l56); diff --git a/sound/soc/codecs/cs35l56.c b/sound/soc/codecs/cs35l56.c index 6dd0319bc843..73cc123db567 100644 --- a/sound/soc/codecs/cs35l56.c +++ b/sound/soc/codecs/cs35l56.c @@ -802,16 +802,44 @@ static struct snd_soc_dai_driver cs35l56_dai[] = { } };
+static int cs35l56_write_cal(struct cs35l56_private *cs35l56) +{ + int ret; + + if (cs35l56->base.secured || !cs35l56->base.cal_data_valid) + return -ENODATA; + + ret = wm_adsp_run(&cs35l56->dsp); + if (ret) + return ret; + + ret = cs_amp_write_cal_coeffs(&cs35l56->dsp, + &cs35l56_calibration_controls, + &cs35l56->base.cal_data); + + wm_adsp_stop(&cs35l56->dsp); + + if (ret == 0) + dev_info(cs35l56->base.dev, "Calibration applied\n"); + + return ret; +} + static void cs35l56_reinit_patch(struct cs35l56_private *cs35l56) { int ret;
/* Use wm_adsp to load and apply the firmware patch and coefficient files */ ret = wm_adsp_power_up(&cs35l56->dsp, true); - if (ret) + if (ret) { dev_dbg(cs35l56->base.dev, "%s: wm_adsp_power_up ret %d\n", __func__, ret); - else - cs35l56_mbox_send(&cs35l56->base, CS35L56_MBOX_CMD_AUDIO_REINIT); + return; + } + + cs35l56_write_cal(cs35l56); + + /* Always REINIT after applying patch or coefficients */ + cs35l56_mbox_send(&cs35l56->base, CS35L56_MBOX_CMD_AUDIO_REINIT); }
static void cs35l56_patch(struct cs35l56_private *cs35l56, bool firmware_missing) @@ -874,6 +902,9 @@ static void cs35l56_patch(struct cs35l56_private *cs35l56, bool firmware_missing CS35L56_FIRMWARE_MISSING); cs35l56->base.fw_patched = true;
+ if (cs35l56_write_cal(cs35l56) == 0) + cs35l56_mbox_send(&cs35l56->base, CS35L56_MBOX_CMD_AUDIO_REINIT); + err_unlock: mutex_unlock(&cs35l56->base.irq_lock); err: @@ -1356,6 +1387,7 @@ int cs35l56_common_probe(struct cs35l56_private *cs35l56)
init_completion(&cs35l56->init_completion); mutex_init(&cs35l56->base.irq_lock); + cs35l56->base.cal_index = -1; cs35l56->speaker_id = -ENOENT;
dev_set_drvdata(cs35l56->base.dev, cs35l56); @@ -1457,6 +1489,10 @@ int cs35l56_init(struct cs35l56_private *cs35l56) if (ret) return ret;
+ ret = cs35l56_get_calibration(&cs35l56->base); + if (ret) + return ret; + if (!cs35l56->base.reset_gpio) { dev_dbg(cs35l56->base.dev, "No reset gpio: using soft reset\n"); cs35l56->soft_resetting = true; @@ -1541,6 +1577,7 @@ EXPORT_NS_GPL_DEV_PM_OPS(cs35l56_pm_ops_i2c_spi, SND_SOC_CS35L56_CORE) = {
MODULE_DESCRIPTION("ASoC CS35L56 driver"); MODULE_IMPORT_NS(SND_SOC_CS35L56_SHARED); +MODULE_IMPORT_NS(SND_SOC_CS_AMP_LIB); MODULE_AUTHOR("Richard Fitzgerald rf@opensource.cirrus.com"); MODULE_AUTHOR("Simon Trimmer simont@opensource.cirrus.com"); MODULE_LICENSE("GPL");
Add hda_cs_dsp_write_cal_coeffs(), a helper function to write amp calibration data to firmware controls.
The calibration data is passed in a struct cirrus_amp_cal_data and a definition of the firmware algorithm controls in a struct cirrus_amp_cal_controls.
Signed-off-by: Richard Fitzgerald rf@opensource.cirrus.com --- sound/pci/hda/hda_cs_dsp_ctl.c | 47 ++++++++++++++++++++++++++++++++++ sound/pci/hda/hda_cs_dsp_ctl.h | 4 +++ 2 files changed, 51 insertions(+)
diff --git a/sound/pci/hda/hda_cs_dsp_ctl.c b/sound/pci/hda/hda_cs_dsp_ctl.c index 0f5fdd44721c..bdc21a117dd2 100644 --- a/sound/pci/hda/hda_cs_dsp_ctl.c +++ b/sound/pci/hda/hda_cs_dsp_ctl.c @@ -10,6 +10,7 @@ #include <sound/soc.h> #include <linux/firmware/cirrus/cs_dsp.h> #include <linux/firmware/cirrus/wmfw.h> +#include <sound/cs-amp-lib.h> #include "hda_cs_dsp_ctl.h"
#define ADSP_MAX_STD_CTRL_SIZE 512 @@ -247,6 +248,52 @@ int hda_cs_dsp_read_ctl(struct cs_dsp *dsp, const char *name, int type, } EXPORT_SYMBOL_NS_GPL(hda_cs_dsp_read_ctl, SND_HDA_CS_DSP_CONTROLS);
+static int hda_cs_dsp_write_cal_coeff(struct cs_dsp *cs_dsp, + const struct cirrus_amp_cal_controls *controls, + const char *ctl_name, unsigned int val) +{ + __be32 beval = cpu_to_be32(val); + int ret; + + ret = hda_cs_dsp_write_ctl(cs_dsp, ctl_name, controls->mem_region, + controls->alg_id, &beval, 4); + if (ret) { + dev_err(cs_dsp->dev, "Failed to write control %s: %d\n", ctl_name, ret); + return ret; + } + + return 0; +} + +int hda_cs_dsp_write_cal_coeffs(struct cs_dsp *cs_dsp, + const struct cirrus_amp_cal_controls *controls, + const struct cirrus_amp_cal_data *data) +{ + int ret; + + dev_dbg(cs_dsp->dev, "Calibration: Ambient=%#x, Status=%#x, R0=%d\n", + data->calAmbient, data->calStatus, data->calR); + + ret = hda_cs_dsp_write_cal_coeff(cs_dsp, controls, controls->ambient, data->calAmbient); + if (ret) + return ret; + + ret = hda_cs_dsp_write_cal_coeff(cs_dsp, controls, controls->calr, data->calR); + if (ret) + return ret; + + ret = hda_cs_dsp_write_cal_coeff(cs_dsp, controls, controls->status, data->calStatus); + if (ret) + return ret; + + ret = hda_cs_dsp_write_cal_coeff(cs_dsp, controls, controls->checksum, data->calR + 1); + if (ret) + return ret; + + return 0; +} +EXPORT_SYMBOL_NS_GPL(hda_cs_dsp_write_cal_coeffs, SND_HDA_CS_DSP_CONTROLS); + MODULE_DESCRIPTION("CS_DSP ALSA Control HDA Library"); MODULE_AUTHOR("Stefan Binding, sbinding@opensource.cirrus.com"); MODULE_LICENSE("GPL"); diff --git a/sound/pci/hda/hda_cs_dsp_ctl.h b/sound/pci/hda/hda_cs_dsp_ctl.h index 2cf93359c4f2..329531672005 100644 --- a/sound/pci/hda/hda_cs_dsp_ctl.h +++ b/sound/pci/hda/hda_cs_dsp_ctl.h @@ -10,6 +10,7 @@ #ifndef __HDA_CS_DSP_CTL_H__ #define __HDA_CS_DSP_CTL_H__
+#include <sound/cs-amp-lib.h> #include <sound/soc.h> #include <linux/firmware/cirrus/cs_dsp.h>
@@ -35,5 +36,8 @@ int hda_cs_dsp_write_ctl(struct cs_dsp *dsp, const char *name, int type, unsigned int alg, const void *buf, size_t len); int hda_cs_dsp_read_ctl(struct cs_dsp *dsp, const char *name, int type, unsigned int alg, void *buf, size_t len); +int hda_cs_dsp_write_cal_coeffs(struct cs_dsp *cs_dsp, + const struct cirrus_amp_cal_controls *controls, + const struct cirrus_amp_cal_data *data);
#endif /*__HDA_CS_DSP_CTL_H__*/
If there are factory calibration settings in EFI, extract the settings and write them to the firmware calibration controls.
Signed-off-by: Richard Fitzgerald rf@opensource.cirrus.com --- sound/pci/hda/Kconfig | 2 ++ sound/pci/hda/cs35l56_hda.c | 36 ++++++++++++++++++++++++++++++------ 2 files changed, 32 insertions(+), 6 deletions(-)
diff --git a/sound/pci/hda/Kconfig b/sound/pci/hda/Kconfig index 21a90b3c4cc7..6edfdaf98a74 100644 --- a/sound/pci/hda/Kconfig +++ b/sound/pci/hda/Kconfig @@ -162,6 +162,7 @@ config SND_HDA_SCODEC_CS35L56_I2C select SND_HDA_SCODEC_CS35L56 select SND_HDA_CIRRUS_SCODEC select SND_HDA_CS_DSP_CONTROLS + select SND_SOC_CS_AMP_LIB help Say Y or M here to include CS35L56 amplifier support with I2C control. @@ -177,6 +178,7 @@ config SND_HDA_SCODEC_CS35L56_SPI select SND_HDA_SCODEC_CS35L56 select SND_HDA_CIRRUS_SCODEC select SND_HDA_CS_DSP_CONTROLS + select SND_SOC_CS_AMP_LIB help Say Y or M here to include CS35L56 amplifier support with SPI control. diff --git a/sound/pci/hda/cs35l56_hda.c b/sound/pci/hda/cs35l56_hda.c index 75a14ba54fcd..47a225114f56 100644 --- a/sound/pci/hda/cs35l56_hda.c +++ b/sound/pci/hda/cs35l56_hda.c @@ -14,6 +14,7 @@ #include <linux/regmap.h> #include <linux/slab.h> #include <sound/core.h> +#include <sound/cs-amp-lib.h> #include <sound/hda_codec.h> #include <sound/tlv.h> #include "cirrus_scodec.h" @@ -547,6 +548,22 @@ static void cs35l56_hda_add_dsp_controls(struct cs35l56_hda *cs35l56) hda_cs_dsp_add_controls(&cs35l56->cs_dsp, &info); }
+static void cs35l56_hda_apply_calibration(struct cs35l56_hda *cs35l56) +{ + int ret; + + if (!cs35l56->base.cal_data_valid || cs35l56->base.secured) + return; + + ret = hda_cs_dsp_write_cal_coeffs(&cs35l56->cs_dsp, + &cs35l56_calibration_controls, + &cs35l56->base.cal_data); + if (ret < 0) + dev_warn(cs35l56->base.dev, "Failed to write calibration: %d\n", ret); + else + dev_info(cs35l56->base.dev, "Calibration applied\n"); +} + static int cs35l56_hda_fw_load(struct cs35l56_hda *cs35l56) { const struct firmware *coeff_firmware = NULL; @@ -618,12 +635,8 @@ static int cs35l56_hda_fw_load(struct cs35l56_hda *cs35l56) if (coeff_filename) dev_dbg(cs35l56->base.dev, "Loaded Coefficients: %s\n", coeff_filename);
- if (!firmware_missing) { - ret = cs35l56_mbox_send(&cs35l56->base, CS35L56_MBOX_CMD_AUDIO_REINIT); - if (ret) - goto err_powered_up; - } else if (wmfw_firmware || coeff_firmware) { - /* If we downloaded firmware, reset the device and wait for it to boot */ + /* If we downloaded firmware, reset the device and wait for it to boot */ + if (firmware_missing && (wmfw_firmware || coeff_firmware)) { cs35l56_system_reset(&cs35l56->base, false); regcache_mark_dirty(cs35l56->base.regmap); ret = cs35l56_wait_for_firmware_boot(&cs35l56->base); @@ -646,6 +659,11 @@ static int cs35l56_hda_fw_load(struct cs35l56_hda *cs35l56) if (ret) dev_dbg(cs35l56->base.dev, "%s: cs_dsp_run ret %d\n", __func__, ret);
+ cs35l56_hda_apply_calibration(cs35l56); + ret = cs35l56_mbox_send(&cs35l56->base, CS35L56_MBOX_CMD_AUDIO_REINIT); + if (ret) + cs_dsp_stop(&cs35l56->cs_dsp); + err_powered_up: if (!cs35l56->base.fw_patched) cs_dsp_power_down(&cs35l56->cs_dsp); @@ -953,6 +971,8 @@ int cs35l56_hda_common_probe(struct cs35l56_hda *cs35l56, int id) goto err; }
+ cs35l56->base.cal_index = cs35l56->index; + cs35l56_init_cs_dsp(&cs35l56->base, &cs35l56->cs_dsp); cs35l56->cs_dsp.client_ops = &cs35l56_hda_client_ops;
@@ -990,6 +1010,10 @@ int cs35l56_hda_common_probe(struct cs35l56_hda *cs35l56, int id) if (ret) goto err;
+ ret = cs35l56_get_calibration(&cs35l56->base); + if (ret) + goto err; + ret = cs_dsp_halo_init(&cs35l56->cs_dsp); if (ret) { dev_err_probe(cs35l56->base.dev, ret, "cs_dsp_halo_init failed\n");
Add a KUnit test for the cs-amp-lib library. This has test cases for cs_amp_get_efi_calibration_data() and cs_amp_write_cal_coeffs().
A KUNIT_STATIC_STUB_REDIRECT() has been added to cs_amp_get_efi_variable() and cs_amp_write_cal_coeff() so that the KUnit test can redirect these to test harness functions.
Signed-off-by: Richard Fitzgerald rf@opensource.cirrus.com --- include/sound/cs-amp-lib.h | 14 + sound/soc/codecs/Kconfig | 14 + sound/soc/codecs/Makefile | 2 + sound/soc/codecs/cs-amp-lib-test.c | 709 +++++++++++++++++++++++++++++ sound/soc/codecs/cs-amp-lib.c | 18 +- 5 files changed, 755 insertions(+), 2 deletions(-) create mode 100644 sound/soc/codecs/cs-amp-lib-test.c
diff --git a/include/sound/cs-amp-lib.h b/include/sound/cs-amp-lib.h index 35f7bd848276..c9055d833bd0 100644 --- a/include/sound/cs-amp-lib.h +++ b/include/sound/cs-amp-lib.h @@ -49,4 +49,18 @@ int cs_amp_write_cal_coeffs(struct wm_adsp *dsp, const struct cirrus_amp_cal_data *data); int cs_amp_get_efi_calibration_data(struct device *dev, u64 target_uid, int amp_index, struct cirrus_amp_cal_data *out_data); + +struct cs_amp_test_hooks { + efi_status_t (*get_efi_variable)(efi_char16_t *name, + efi_guid_t *guid, + unsigned long *size, + void *buf); + + int (*write_cal_coeff)(struct wm_adsp *dsp, + const struct cirrus_amp_cal_controls *controls, + const char *ctl_name, u32 val); +}; + +extern const struct cs_amp_test_hooks * const cs_amp_test_hooks; + #endif /* CS_AMP_LIB_H */ diff --git a/sound/soc/codecs/Kconfig b/sound/soc/codecs/Kconfig index 15f287784d8b..373d0f6478ae 100644 --- a/sound/soc/codecs/Kconfig +++ b/sound/soc/codecs/Kconfig @@ -732,6 +732,20 @@ config SND_SOC_CROS_EC_CODEC config SND_SOC_CS_AMP_LIB tristate
+config SND_SOC_CS_AMP_LIB_TEST + tristate "KUnit test for Cirrus Logic cs-amp-lib" + depends on KUNIT + depends on ACPI || COMPILE_TEST + default KUNIT_ALL_TESTS + select SND_SOC_CS_AMP_LIB + help + This builds KUnit tests for the Cirrus Logic common + amplifier library. + For more information on KUnit and unit tests in general, + please refer to the KUnit documentation in + Documentation/dev-tools/kunit/. + If in doubt, say "N". + config SND_SOC_CS35L32 tristate "Cirrus Logic CS35L32 CODEC" depends on I2C diff --git a/sound/soc/codecs/Makefile b/sound/soc/codecs/Makefile index 0fc40640e5d0..7c075539dc47 100644 --- a/sound/soc/codecs/Makefile +++ b/sound/soc/codecs/Makefile @@ -60,6 +60,7 @@ snd-soc-cpcap-objs := cpcap.o snd-soc-cq93vc-objs := cq93vc.o snd-soc-cros-ec-codec-objs := cros_ec_codec.o snd-soc-cs-amp-lib-objs := cs-amp-lib.o +snd-soc-cs-amp-lib-test-objs := cs-amp-lib-test.o snd-soc-cs35l32-objs := cs35l32.o snd-soc-cs35l33-objs := cs35l33.o snd-soc-cs35l34-objs := cs35l34.o @@ -454,6 +455,7 @@ obj-$(CONFIG_SND_SOC_CQ0093VC) += snd-soc-cq93vc.o obj-$(CONFIG_SND_SOC_CPCAP) += snd-soc-cpcap.o obj-$(CONFIG_SND_SOC_CROS_EC_CODEC) += snd-soc-cros-ec-codec.o obj-$(CONFIG_SND_SOC_CS_AMP_LIB) += snd-soc-cs-amp-lib.o +obj-$(CONFIG_SND_SOC_CS_AMP_LIB_TEST) += snd-soc-cs-amp-lib-test.o obj-$(CONFIG_SND_SOC_CS35L32) += snd-soc-cs35l32.o obj-$(CONFIG_SND_SOC_CS35L33) += snd-soc-cs35l33.o obj-$(CONFIG_SND_SOC_CS35L34) += snd-soc-cs35l34.o diff --git a/sound/soc/codecs/cs-amp-lib-test.c b/sound/soc/codecs/cs-amp-lib-test.c new file mode 100644 index 000000000000..6f9e5755e145 --- /dev/null +++ b/sound/soc/codecs/cs-amp-lib-test.c @@ -0,0 +1,709 @@ +// SPDX-License-Identifier: GPL-2.0-only +// +// KUnit test for the Cirrus common amplifier library. +// +// Copyright (C) 2024 Cirrus Logic, Inc. and +// Cirrus Logic International Semiconductor Ltd. + +#include <kunit/test.h> +#include <kunit/static_stub.h> +#include <linux/firmware/cirrus/wmfw.h> +#include <linux/gpio/driver.h> +#include <linux/list.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/random.h> +#include <sound/cs-amp-lib.h> +#include "wm_adsp.h" + +struct cs_amp_lib_test_priv { + struct platform_device amp_pdev; + + struct cirrus_amp_efi_data *cal_blob; + struct list_head ctl_write_list; +}; + +struct cs_amp_lib_test_ctl_write_entry { + struct list_head list; + unsigned int value; + char name[16]; +}; + +struct cs_amp_lib_test_param { + int num_amps; + int amp_index; +}; + +static void cs_amp_lib_test_init_dummy_cal_blob(struct kunit *test, int num_amps) +{ + struct cs_amp_lib_test_priv *priv = test->priv; + unsigned int blob_size; + + blob_size = offsetof(struct cirrus_amp_efi_data, data) + + sizeof(struct cirrus_amp_cal_data) * num_amps; + + priv->cal_blob = kunit_kzalloc(test, blob_size, GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, priv->cal_blob); + + priv->cal_blob->size = blob_size; + priv->cal_blob->count = num_amps; + + get_random_bytes(priv->cal_blob->data, sizeof(struct cirrus_amp_cal_data) * num_amps); +} + +static u64 cs_amp_lib_test_get_target_uid(struct kunit *test) +{ + struct cs_amp_lib_test_priv *priv = test->priv; + const struct cs_amp_lib_test_param *param = test->param_value; + u64 uid; + + uid = priv->cal_blob->data[param->amp_index].calTarget[1]; + uid <<= 32; + uid |= priv->cal_blob->data[param->amp_index].calTarget[0]; + + return uid; +} + +/* Redirected get_efi_variable to simulate that the file is too short */ +static efi_status_t cs_amp_lib_test_get_efi_variable_nohead(efi_char16_t *name, + efi_guid_t *guid, + unsigned long *size, + void *buf) +{ + if (!buf) { + *size = offsetof(struct cirrus_amp_efi_data, data) - 1; + return EFI_BUFFER_TOO_SMALL; + } + + return EFI_NOT_FOUND; +} + +/* Should return -EOVERFLOW if the header is larger than the EFI data */ +static void cs_amp_lib_test_cal_data_too_short_test(struct kunit *test) +{ + struct cs_amp_lib_test_priv *priv = test->priv; + struct cirrus_amp_cal_data result_data; + int ret; + + /* Redirect calls to get EFI data */ + kunit_activate_static_stub(test, + cs_amp_test_hooks->get_efi_variable, + cs_amp_lib_test_get_efi_variable_nohead); + + ret = cs_amp_get_efi_calibration_data(&priv->amp_pdev.dev, 0, 0, &result_data); + KUNIT_EXPECT_EQ(test, ret, -EOVERFLOW); + + kunit_deactivate_static_stub(test, cs_amp_test_hooks->get_efi_variable); +} + +/* Redirected get_efi_variable to simulate that the count is larger than the file */ +static efi_status_t cs_amp_lib_test_get_efi_variable_bad_count(efi_char16_t *name, + efi_guid_t *guid, + unsigned long *size, + void *buf) +{ + struct kunit *test = kunit_get_current_test(); + struct cs_amp_lib_test_priv *priv = test->priv; + + if (!buf) { + /* + * Return a size that is shorter than required for the + * declared number of entries. + */ + *size = priv->cal_blob->size - 1; + return EFI_BUFFER_TOO_SMALL; + } + + memcpy(buf, priv->cal_blob, priv->cal_blob->size - 1); + + return EFI_SUCCESS; +} + +/* Should return -EOVERFLOW if the entry count is larger than the EFI data */ +static void cs_amp_lib_test_cal_count_too_big_test(struct kunit *test) +{ + struct cs_amp_lib_test_priv *priv = test->priv; + struct cirrus_amp_cal_data result_data; + int ret; + + cs_amp_lib_test_init_dummy_cal_blob(test, 8); + + /* Redirect calls to get EFI data */ + kunit_activate_static_stub(test, + cs_amp_test_hooks->get_efi_variable, + cs_amp_lib_test_get_efi_variable_bad_count); + + ret = cs_amp_get_efi_calibration_data(&priv->amp_pdev.dev, 0, 0, &result_data); + KUNIT_EXPECT_EQ(test, ret, -EOVERFLOW); + + kunit_deactivate_static_stub(test, cs_amp_test_hooks->get_efi_variable); +} + +/* Redirected get_efi_variable to simulate that the variable not found */ +static efi_status_t cs_amp_lib_test_get_efi_variable_none(efi_char16_t *name, + efi_guid_t *guid, + unsigned long *size, + void *buf) +{ + return EFI_NOT_FOUND; +} + +/* If EFI doesn't contain a cal data variable the result should be -ENOENT */ +static void cs_amp_lib_test_no_cal_data_test(struct kunit *test) +{ + struct cs_amp_lib_test_priv *priv = test->priv; + struct cirrus_amp_cal_data result_data; + int ret; + + /* Redirect calls to get EFI data */ + kunit_activate_static_stub(test, + cs_amp_test_hooks->get_efi_variable, + cs_amp_lib_test_get_efi_variable_none); + + ret = cs_amp_get_efi_calibration_data(&priv->amp_pdev.dev, 0, 0, &result_data); + KUNIT_EXPECT_EQ(test, ret, -ENOENT); + + kunit_deactivate_static_stub(test, cs_amp_test_hooks->get_efi_variable); +} + +/* Redirected get_efi_variable to simulate reading a cal data blob */ +static efi_status_t cs_amp_lib_test_get_efi_variable(efi_char16_t *name, + efi_guid_t *guid, + unsigned long *size, + void *buf) +{ + static const efi_char16_t expected_name[] = L"CirrusSmartAmpCalibrationData"; + static const efi_guid_t expected_guid = + EFI_GUID(0x02f9af02, 0x7734, 0x4233, 0xb4, 0x3d, 0x93, 0xfe, 0x5a, 0xa3, 0x5d, 0xb3); + struct kunit *test = kunit_get_current_test(); + struct cs_amp_lib_test_priv *priv = test->priv; + + KUNIT_EXPECT_NOT_ERR_OR_NULL(test, name); + KUNIT_EXPECT_NOT_ERR_OR_NULL(test, guid); + KUNIT_EXPECT_NOT_ERR_OR_NULL(test, size); + + KUNIT_EXPECT_MEMEQ(test, name, expected_name, sizeof(expected_name)); + KUNIT_EXPECT_MEMEQ(test, guid, &expected_guid, sizeof(expected_guid)); + + if (!buf) { + *size = priv->cal_blob->size; + return EFI_BUFFER_TOO_SMALL; + } + + KUNIT_ASSERT_GE_MSG(test, ksize(buf), priv->cal_blob->size, "Buffer to small"); + + memcpy(buf, priv->cal_blob, priv->cal_blob->size); + + return EFI_SUCCESS; +} + +/* Get cal data block for a given amp, matched by target UID. */ +static void cs_amp_lib_test_get_efi_cal_by_uid_test(struct kunit *test) +{ + struct cs_amp_lib_test_priv *priv = test->priv; + const struct cs_amp_lib_test_param *param = test->param_value; + struct cirrus_amp_cal_data result_data; + u64 target_uid; + int ret; + + cs_amp_lib_test_init_dummy_cal_blob(test, param->num_amps); + + /* Redirect calls to get EFI data */ + kunit_activate_static_stub(test, + cs_amp_test_hooks->get_efi_variable, + cs_amp_lib_test_get_efi_variable); + + target_uid = cs_amp_lib_test_get_target_uid(test); + ret = cs_amp_get_efi_calibration_data(&priv->amp_pdev.dev, target_uid, -1, &result_data); + KUNIT_EXPECT_EQ(test, ret, 0); + + kunit_deactivate_static_stub(test, cs_amp_test_hooks->get_efi_variable); + + KUNIT_EXPECT_EQ(test, result_data.calTarget[0], target_uid & 0xFFFFFFFFULL); + KUNIT_EXPECT_EQ(test, result_data.calTarget[1], target_uid >> 32); + KUNIT_EXPECT_EQ(test, result_data.calTime[0], + priv->cal_blob->data[param->amp_index].calTime[0]); + KUNIT_EXPECT_EQ(test, result_data.calTime[1], + priv->cal_blob->data[param->amp_index].calTime[1]); + KUNIT_EXPECT_EQ(test, result_data.calAmbient, + priv->cal_blob->data[param->amp_index].calAmbient); + KUNIT_EXPECT_EQ(test, result_data.calStatus, + priv->cal_blob->data[param->amp_index].calStatus); + KUNIT_EXPECT_EQ(test, result_data.calR, + priv->cal_blob->data[param->amp_index].calR); +} + +/* Get cal data block for a given amp index without checking target UID. */ +static void cs_amp_lib_test_get_efi_cal_by_index_unchecked_test(struct kunit *test) +{ + struct cs_amp_lib_test_priv *priv = test->priv; + const struct cs_amp_lib_test_param *param = test->param_value; + struct cirrus_amp_cal_data result_data; + int ret; + + cs_amp_lib_test_init_dummy_cal_blob(test, param->num_amps); + + /* Redirect calls to get EFI data */ + kunit_activate_static_stub(test, + cs_amp_test_hooks->get_efi_variable, + cs_amp_lib_test_get_efi_variable); + + ret = cs_amp_get_efi_calibration_data(&priv->amp_pdev.dev, 0, + param->amp_index, &result_data); + KUNIT_EXPECT_EQ(test, ret, 0); + + kunit_deactivate_static_stub(test, cs_amp_test_hooks->get_efi_variable); + + KUNIT_EXPECT_EQ(test, result_data.calTime[0], + priv->cal_blob->data[param->amp_index].calTime[0]); + KUNIT_EXPECT_EQ(test, result_data.calTime[1], + priv->cal_blob->data[param->amp_index].calTime[1]); + KUNIT_EXPECT_EQ(test, result_data.calAmbient, + priv->cal_blob->data[param->amp_index].calAmbient); + KUNIT_EXPECT_EQ(test, result_data.calStatus, + priv->cal_blob->data[param->amp_index].calStatus); + KUNIT_EXPECT_EQ(test, result_data.calR, + priv->cal_blob->data[param->amp_index].calR); +} + +/* Get cal data block for a given amp index with checked target UID. */ +static void cs_amp_lib_test_get_efi_cal_by_index_checked_test(struct kunit *test) +{ + struct cs_amp_lib_test_priv *priv = test->priv; + const struct cs_amp_lib_test_param *param = test->param_value; + struct cirrus_amp_cal_data result_data; + u64 target_uid; + int ret; + + cs_amp_lib_test_init_dummy_cal_blob(test, param->num_amps); + + /* Redirect calls to get EFI data */ + kunit_activate_static_stub(test, + cs_amp_test_hooks->get_efi_variable, + cs_amp_lib_test_get_efi_variable); + + target_uid = cs_amp_lib_test_get_target_uid(test); + ret = cs_amp_get_efi_calibration_data(&priv->amp_pdev.dev, target_uid, + param->amp_index, &result_data); + KUNIT_EXPECT_EQ(test, ret, 0); + + kunit_deactivate_static_stub(test, cs_amp_test_hooks->get_efi_variable); + + KUNIT_EXPECT_EQ(test, result_data.calTime[0], + priv->cal_blob->data[param->amp_index].calTime[0]); + KUNIT_EXPECT_EQ(test, result_data.calTime[1], + priv->cal_blob->data[param->amp_index].calTime[1]); + KUNIT_EXPECT_EQ(test, result_data.calAmbient, + priv->cal_blob->data[param->amp_index].calAmbient); + KUNIT_EXPECT_EQ(test, result_data.calStatus, + priv->cal_blob->data[param->amp_index].calStatus); + KUNIT_EXPECT_EQ(test, result_data.calR, + priv->cal_blob->data[param->amp_index].calR); +} + +/* + * Get cal data block for a given amp index with checked target UID. + * The UID does not match so the result should be -ENOENT. + */ +static void cs_amp_lib_test_get_efi_cal_by_index_uid_mismatch_test(struct kunit *test) +{ + struct cs_amp_lib_test_priv *priv = test->priv; + const struct cs_amp_lib_test_param *param = test->param_value; + struct cirrus_amp_cal_data result_data; + u64 target_uid; + int ret; + + cs_amp_lib_test_init_dummy_cal_blob(test, param->num_amps); + + /* Redirect calls to get EFI data */ + kunit_activate_static_stub(test, + cs_amp_test_hooks->get_efi_variable, + cs_amp_lib_test_get_efi_variable); + + /* Get a target UID that won't match the entry */ + target_uid = ~cs_amp_lib_test_get_target_uid(test); + ret = cs_amp_get_efi_calibration_data(&priv->amp_pdev.dev, target_uid, + param->amp_index, &result_data); + KUNIT_EXPECT_EQ(test, ret, -ENOENT); + + kunit_deactivate_static_stub(test, cs_amp_test_hooks->get_efi_variable); +} + +/* + * Get cal data block for a given amp, where the cal data does not + * specify calTarget so the lookup falls back to using the index + */ +static void cs_amp_lib_test_get_efi_cal_by_index_fallback_test(struct kunit *test) +{ + struct cs_amp_lib_test_priv *priv = test->priv; + const struct cs_amp_lib_test_param *param = test->param_value; + struct cirrus_amp_cal_data result_data; + static const u64 bad_target_uid = 0xBADCA100BABABABAULL; + int i, ret; + + cs_amp_lib_test_init_dummy_cal_blob(test, param->num_amps); + + /* Make all the target values zero so they are ignored */ + for (i = 0; i < priv->cal_blob->count; ++i) { + priv->cal_blob->data[i].calTarget[0] = 0; + priv->cal_blob->data[i].calTarget[1] = 0; + } + + /* Redirect calls to get EFI data */ + kunit_activate_static_stub(test, + cs_amp_test_hooks->get_efi_variable, + cs_amp_lib_test_get_efi_variable); + + ret = cs_amp_get_efi_calibration_data(&priv->amp_pdev.dev, bad_target_uid, + param->amp_index, &result_data); + KUNIT_EXPECT_EQ(test, ret, 0); + + kunit_deactivate_static_stub(test, cs_amp_test_hooks->get_efi_variable); + + KUNIT_EXPECT_EQ(test, result_data.calTime[0], + priv->cal_blob->data[param->amp_index].calTime[0]); + KUNIT_EXPECT_EQ(test, result_data.calTime[1], + priv->cal_blob->data[param->amp_index].calTime[1]); + KUNIT_EXPECT_EQ(test, result_data.calAmbient, + priv->cal_blob->data[param->amp_index].calAmbient); + KUNIT_EXPECT_EQ(test, result_data.calStatus, + priv->cal_blob->data[param->amp_index].calStatus); + KUNIT_EXPECT_EQ(test, result_data.calR, + priv->cal_blob->data[param->amp_index].calR); +} + +/* + * If the target UID isn't present in the cal data, and there isn't an + * index to fall back do, the result should be -ENOENT. + */ +static void cs_amp_lib_test_get_efi_cal_uid_not_found_noindex_test(struct kunit *test) +{ + struct cs_amp_lib_test_priv *priv = test->priv; + struct cirrus_amp_cal_data result_data; + static const u64 bad_target_uid = 0xBADCA100BABABABAULL; + int i, ret; + + cs_amp_lib_test_init_dummy_cal_blob(test, 8); + + /* Make all the target values != bad_target_uid */ + for (i = 0; i < priv->cal_blob->count; ++i) { + priv->cal_blob->data[i].calTarget[0] &= ~(bad_target_uid & 0xFFFFFFFFULL); + priv->cal_blob->data[i].calTarget[1] &= ~(bad_target_uid >> 32); + } + + /* Redirect calls to get EFI data */ + kunit_activate_static_stub(test, + cs_amp_test_hooks->get_efi_variable, + cs_amp_lib_test_get_efi_variable); + + ret = cs_amp_get_efi_calibration_data(&priv->amp_pdev.dev, bad_target_uid, -1, + &result_data); + KUNIT_EXPECT_EQ(test, ret, -ENOENT); + + kunit_deactivate_static_stub(test, cs_amp_test_hooks->get_efi_variable); +} + +/* + * If the target UID isn't present in the cal data, and the index is + * out of range, the result should be -ENOENT. + */ +static void cs_amp_lib_test_get_efi_cal_uid_not_found_index_not_found_test(struct kunit *test) +{ + struct cs_amp_lib_test_priv *priv = test->priv; + struct cirrus_amp_cal_data result_data; + static const u64 bad_target_uid = 0xBADCA100BABABABAULL; + int i, ret; + + cs_amp_lib_test_init_dummy_cal_blob(test, 8); + + /* Make all the target values != bad_target_uid */ + for (i = 0; i < priv->cal_blob->count; ++i) { + priv->cal_blob->data[i].calTarget[0] &= ~(bad_target_uid & 0xFFFFFFFFULL); + priv->cal_blob->data[i].calTarget[1] &= ~(bad_target_uid >> 32); + } + + /* Redirect calls to get EFI data */ + kunit_activate_static_stub(test, + cs_amp_test_hooks->get_efi_variable, + cs_amp_lib_test_get_efi_variable); + + ret = cs_amp_get_efi_calibration_data(&priv->amp_pdev.dev, bad_target_uid, 99, + &result_data); + KUNIT_EXPECT_EQ(test, ret, -ENOENT); + + kunit_deactivate_static_stub(test, cs_amp_test_hooks->get_efi_variable); +} + +/* + * If the target UID isn't given, and the index is out of range, the + * result should be -ENOENT. + */ +static void cs_amp_lib_test_get_efi_cal_no_uid_index_not_found_test(struct kunit *test) +{ + struct cs_amp_lib_test_priv *priv = test->priv; + struct cirrus_amp_cal_data result_data; + int ret; + + cs_amp_lib_test_init_dummy_cal_blob(test, 8); + + /* Redirect calls to get EFI data */ + kunit_activate_static_stub(test, + cs_amp_test_hooks->get_efi_variable, + cs_amp_lib_test_get_efi_variable); + + ret = cs_amp_get_efi_calibration_data(&priv->amp_pdev.dev, 0, 99, &result_data); + KUNIT_EXPECT_EQ(test, ret, -ENOENT); + + kunit_deactivate_static_stub(test, cs_amp_test_hooks->get_efi_variable); +} + +/* If neither the target UID or the index is given the result should be -ENOENT. */ +static void cs_amp_lib_test_get_efi_cal_no_uid_no_index_test(struct kunit *test) +{ + struct cs_amp_lib_test_priv *priv = test->priv; + struct cirrus_amp_cal_data result_data; + int ret; + + cs_amp_lib_test_init_dummy_cal_blob(test, 8); + + /* Redirect calls to get EFI data */ + kunit_activate_static_stub(test, + cs_amp_test_hooks->get_efi_variable, + cs_amp_lib_test_get_efi_variable); + + ret = cs_amp_get_efi_calibration_data(&priv->amp_pdev.dev, 0, -1, &result_data); + KUNIT_EXPECT_EQ(test, ret, -ENOENT); + + kunit_deactivate_static_stub(test, cs_amp_test_hooks->get_efi_variable); +} + +/* + * If the UID is passed as 0 this must not match an entry with an + * unpopulated calTarget + */ +static void cs_amp_lib_test_get_efi_cal_zero_not_matched_test(struct kunit *test) +{ + struct cs_amp_lib_test_priv *priv = test->priv; + struct cirrus_amp_cal_data result_data; + int i, ret; + + cs_amp_lib_test_init_dummy_cal_blob(test, 8); + + /* Make all the target values zero so they are ignored */ + for (i = 0; i < priv->cal_blob->count; ++i) { + priv->cal_blob->data[i].calTarget[0] = 0; + priv->cal_blob->data[i].calTarget[1] = 0; + } + + /* Redirect calls to get EFI data */ + kunit_activate_static_stub(test, + cs_amp_test_hooks->get_efi_variable, + cs_amp_lib_test_get_efi_variable); + + ret = cs_amp_get_efi_calibration_data(&priv->amp_pdev.dev, 0, -1, &result_data); + KUNIT_EXPECT_EQ(test, ret, -ENOENT); + + kunit_deactivate_static_stub(test, cs_amp_test_hooks->get_efi_variable); +} + +static const struct cirrus_amp_cal_controls cs_amp_lib_test_calibration_controls = { + .alg_id = 0x9f210, + .mem_region = WMFW_ADSP2_YM, + .ambient = "CAL_AMBIENT", + .calr = "CAL_R", + .status = "CAL_STATUS", + .checksum = "CAL_CHECKSUM", +}; + +static int cs_amp_lib_test_write_cal_coeff(struct wm_adsp *dsp, + const struct cirrus_amp_cal_controls *controls, + const char *ctl_name, u32 val) +{ + struct kunit *test = kunit_get_current_test(); + struct cs_amp_lib_test_priv *priv = test->priv; + struct cs_amp_lib_test_ctl_write_entry *entry; + + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctl_name); + KUNIT_EXPECT_PTR_EQ(test, controls, &cs_amp_lib_test_calibration_controls); + + entry = kunit_kzalloc(test, sizeof(*entry), GFP_KERNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, entry); + + INIT_LIST_HEAD(&entry->list); + strscpy(entry->name, ctl_name, sizeof(entry->name)); + entry->value = val; + + list_add_tail(&entry->list, &priv->ctl_write_list); + + return 0; +} + +static void cs_amp_lib_test_write_cal_data_test(struct kunit *test) +{ + struct cs_amp_lib_test_priv *priv = test->priv; + struct cs_amp_lib_test_ctl_write_entry *entry; + struct cirrus_amp_cal_data data; + struct wm_adsp *dsp; + int ret; + + dsp = kunit_kzalloc(test, sizeof(*dsp), GFP_KERNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dsp); + dsp->cs_dsp.dev = &priv->amp_pdev.dev; + + get_random_bytes(&data, sizeof(data)); + + /* Redirect calls to write firmware controls */ + kunit_activate_static_stub(test, + cs_amp_test_hooks->write_cal_coeff, + cs_amp_lib_test_write_cal_coeff); + + ret = cs_amp_write_cal_coeffs(dsp, &cs_amp_lib_test_calibration_controls, &data); + KUNIT_EXPECT_EQ(test, ret, 0); + + kunit_deactivate_static_stub(test, cs_amp_test_hooks->write_cal_coeff); + + KUNIT_EXPECT_EQ(test, list_count_nodes(&priv->ctl_write_list), 4); + + /* Checksum control must be written last */ + entry = list_last_entry(&priv->ctl_write_list, typeof(*entry), list); + KUNIT_EXPECT_STREQ(test, entry->name, cs_amp_lib_test_calibration_controls.checksum); + KUNIT_EXPECT_EQ(test, entry->value, data.calR + 1); + list_del(&entry->list); + + entry = list_first_entry(&priv->ctl_write_list, typeof(*entry), list); + KUNIT_EXPECT_STREQ(test, entry->name, cs_amp_lib_test_calibration_controls.ambient); + KUNIT_EXPECT_EQ(test, entry->value, data.calAmbient); + list_del(&entry->list); + + entry = list_first_entry(&priv->ctl_write_list, typeof(*entry), list); + KUNIT_EXPECT_STREQ(test, entry->name, cs_amp_lib_test_calibration_controls.calr); + KUNIT_EXPECT_EQ(test, entry->value, data.calR); + list_del(&entry->list); + + entry = list_first_entry(&priv->ctl_write_list, typeof(*entry), list); + KUNIT_EXPECT_STREQ(test, entry->name, cs_amp_lib_test_calibration_controls.status); + KUNIT_EXPECT_EQ(test, entry->value, data.calStatus); +} + +static void cs_amp_lib_test_dev_release(struct device *dev) +{ +} + +static int cs_amp_lib_test_case_init(struct kunit *test) +{ + struct cs_amp_lib_test_priv *priv; + int ret; + + KUNIT_ASSERT_NOT_NULL(test, cs_amp_test_hooks); + + priv = kunit_kzalloc(test, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + test->priv = priv; + INIT_LIST_HEAD(&priv->ctl_write_list); + + /* Create dummy amp driver dev */ + priv->amp_pdev.name = "cs_amp_lib_test_drv"; + priv->amp_pdev.id = -1; + priv->amp_pdev.dev.release = cs_amp_lib_test_dev_release; + ret = platform_device_register(&priv->amp_pdev); + KUNIT_ASSERT_GE_MSG(test, ret, 0, "Failed to register amp platform device\n"); + + return 0; +} + +static void cs_amp_lib_test_case_exit(struct kunit *test) +{ + struct cs_amp_lib_test_priv *priv = test->priv; + + if (priv->amp_pdev.name) + platform_device_unregister(&priv->amp_pdev); +} + +static const struct cs_amp_lib_test_param cs_amp_lib_test_get_cal_param_cases[] = { + { .num_amps = 2, .amp_index = 0 }, + { .num_amps = 2, .amp_index = 1 }, + + { .num_amps = 3, .amp_index = 0 }, + { .num_amps = 3, .amp_index = 1 }, + { .num_amps = 3, .amp_index = 2 }, + + { .num_amps = 4, .amp_index = 0 }, + { .num_amps = 4, .amp_index = 1 }, + { .num_amps = 4, .amp_index = 2 }, + { .num_amps = 4, .amp_index = 3 }, + + { .num_amps = 5, .amp_index = 0 }, + { .num_amps = 5, .amp_index = 1 }, + { .num_amps = 5, .amp_index = 2 }, + { .num_amps = 5, .amp_index = 3 }, + { .num_amps = 5, .amp_index = 4 }, + + { .num_amps = 6, .amp_index = 0 }, + { .num_amps = 6, .amp_index = 1 }, + { .num_amps = 6, .amp_index = 2 }, + { .num_amps = 6, .amp_index = 3 }, + { .num_amps = 6, .amp_index = 4 }, + { .num_amps = 6, .amp_index = 5 }, + + { .num_amps = 8, .amp_index = 0 }, + { .num_amps = 8, .amp_index = 1 }, + { .num_amps = 8, .amp_index = 2 }, + { .num_amps = 8, .amp_index = 3 }, + { .num_amps = 8, .amp_index = 4 }, + { .num_amps = 8, .amp_index = 5 }, + { .num_amps = 8, .amp_index = 6 }, + { .num_amps = 8, .amp_index = 7 }, +}; + +static void cs_amp_lib_test_get_cal_param_desc(const struct cs_amp_lib_test_param *param, + char *desc) +{ + snprintf(desc, KUNIT_PARAM_DESC_SIZE, "num_amps:%d amp_index:%d", + param->num_amps, param->amp_index); +} + +KUNIT_ARRAY_PARAM(cs_amp_lib_test_get_cal, cs_amp_lib_test_get_cal_param_cases, + cs_amp_lib_test_get_cal_param_desc); + +static struct kunit_case cs_amp_lib_test_cases[] = { + /* Tests for getting calibration data from EFI */ + KUNIT_CASE(cs_amp_lib_test_cal_data_too_short_test), + KUNIT_CASE(cs_amp_lib_test_cal_count_too_big_test), + KUNIT_CASE(cs_amp_lib_test_no_cal_data_test), + KUNIT_CASE(cs_amp_lib_test_get_efi_cal_uid_not_found_noindex_test), + KUNIT_CASE(cs_amp_lib_test_get_efi_cal_uid_not_found_index_not_found_test), + KUNIT_CASE(cs_amp_lib_test_get_efi_cal_no_uid_index_not_found_test), + KUNIT_CASE(cs_amp_lib_test_get_efi_cal_no_uid_no_index_test), + KUNIT_CASE(cs_amp_lib_test_get_efi_cal_zero_not_matched_test), + KUNIT_CASE_PARAM(cs_amp_lib_test_get_efi_cal_by_uid_test, + cs_amp_lib_test_get_cal_gen_params), + KUNIT_CASE_PARAM(cs_amp_lib_test_get_efi_cal_by_index_unchecked_test, + cs_amp_lib_test_get_cal_gen_params), + KUNIT_CASE_PARAM(cs_amp_lib_test_get_efi_cal_by_index_checked_test, + cs_amp_lib_test_get_cal_gen_params), + KUNIT_CASE_PARAM(cs_amp_lib_test_get_efi_cal_by_index_uid_mismatch_test, + cs_amp_lib_test_get_cal_gen_params), + KUNIT_CASE_PARAM(cs_amp_lib_test_get_efi_cal_by_index_fallback_test, + cs_amp_lib_test_get_cal_gen_params), + + /* Tests for writing calibration data */ + KUNIT_CASE(cs_amp_lib_test_write_cal_data_test), + + { } /* terminator */ +}; + +static struct kunit_suite cs_amp_lib_test_suite = { + .name = "snd-soc-cs-amp-lib-test", + .init = cs_amp_lib_test_case_init, + .exit = cs_amp_lib_test_case_exit, + .test_cases = cs_amp_lib_test_cases, +}; + +kunit_test_suite(cs_amp_lib_test_suite); + +MODULE_IMPORT_NS(SND_SOC_CS_AMP_LIB); +MODULE_DESCRIPTION("KUnit test for Cirrus Logic amplifier library"); +MODULE_AUTHOR("Richard Fitzgerald rf@opensource.cirrus.com"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/codecs/cs-amp-lib.c b/sound/soc/codecs/cs-amp-lib.c index 0e1249342a78..a6ac96ecb176 100644 --- a/sound/soc/codecs/cs-amp-lib.c +++ b/sound/soc/codecs/cs-amp-lib.c @@ -6,6 +6,7 @@ // Cirrus Logic International Semiconductor Ltd.
#include <asm/byteorder.h> +#include <kunit/static_stub.h> #include <linux/dev_printk.h> #include <linux/efi.h> #include <linux/module.h> @@ -26,6 +27,8 @@ static int cs_amp_write_cal_coeff(struct wm_adsp *dsp, __be32 beval = cpu_to_be32(val); int ret;
+ KUNIT_STATIC_STUB_REDIRECT(cs_amp_write_cal_coeff, dsp, controls, ctl_name, val); + if (IS_REACHABLE(CONFIG_SND_SOC_WM_ADSP)) { ret = wm_adsp_write_ctl(dsp, ctl_name, controls->mem_region, controls->alg_id, &beval, sizeof(beval)); @@ -78,7 +81,7 @@ int cs_amp_write_cal_coeffs(struct wm_adsp *dsp, const struct cirrus_amp_cal_controls *controls, const struct cirrus_amp_cal_data *data) { - if (IS_REACHABLE(CONFIG_SND_SOC_WM_ADSP)) + if (IS_REACHABLE(CONFIG_SND_SOC_WM_ADSP) || IS_ENABLED(CONFIG_SND_SOC_CS_AMP_LIB_TEST)) return _cs_amp_write_cal_coeffs(dsp, controls, data); else return -ENODEV; @@ -92,6 +95,8 @@ static efi_status_t cs_amp_get_efi_variable(efi_char16_t *name, { u32 attr;
+ KUNIT_STATIC_STUB_REDIRECT(cs_amp_get_efi_variable, name, guid, size, buf); + if (IS_ENABLED(CONFIG_EFI)) return efi.get_variable(name, guid, &attr, size, buf);
@@ -244,13 +249,22 @@ static int _cs_amp_get_efi_calibration_data(struct device *dev, u64 target_uid, int cs_amp_get_efi_calibration_data(struct device *dev, u64 target_uid, int amp_index, struct cirrus_amp_cal_data *out_data) { - if (IS_ENABLED(CONFIG_EFI)) + if (IS_ENABLED(CONFIG_EFI) || IS_ENABLED(CONFIG_SND_SOC_CS_AMP_LIB_TEST)) return _cs_amp_get_efi_calibration_data(dev, target_uid, amp_index, out_data); else return -ENOENT; } EXPORT_SYMBOL_NS_GPL(cs_amp_get_efi_calibration_data, SND_SOC_CS_AMP_LIB);
+static const struct cs_amp_test_hooks cs_amp_test_hook_ptrs = { + .get_efi_variable = cs_amp_get_efi_variable, + .write_cal_coeff = cs_amp_write_cal_coeff, +}; + +const struct cs_amp_test_hooks * const cs_amp_test_hooks = + PTR_IF(IS_ENABLED(CONFIG_SND_SOC_CS_AMP_LIB_TEST), &cs_amp_test_hook_ptrs); +EXPORT_SYMBOL_NS_GPL(cs_amp_test_hooks, SND_SOC_CS_AMP_LIB); + MODULE_DESCRIPTION("Cirrus Logic amplifier library"); MODULE_AUTHOR("Richard Fitzgerald rf@opensource.cirrus.com"); MODULE_LICENSE("GPL");
Hi Richard,
kernel test robot noticed the following build errors:
[auto build test ERROR on broonie-sound/for-next] [also build test ERROR on tiwai-sound/for-next tiwai-sound/for-linus linus/master v6.8-rc5 next-20240221] [If your patch is applied to the wrong git tree, kindly drop us a note. And when submitting patch, we suggest to use '--base' as documented in https://git-scm.com/docs/git-format-patch#_base_tree_information]
url: https://github.com/intel-lab-lkp/linux/commits/Richard-Fitzgerald/ASoC-wm_ad... base: https://git.kernel.org/pub/scm/linux/kernel/git/broonie/sound.git for-next patch link: https://lore.kernel.org/r/20240221150507.1039979-10-rf%40opensource.cirrus.c... patch subject: [PATCH 9/9] ASoC: cs-amp-lib: Add KUnit test for calibration helpers config: hexagon-randconfig-r123-20240222 (https://download.01.org/0day-ci/archive/20240222/202402221544.okaeWC1f-lkp@i...) compiler: clang version 19.0.0git (https://github.com/llvm/llvm-project 36adfec155de366d722f2bac8ff9162289dcf06c) reproduce: (https://download.01.org/0day-ci/archive/20240222/202402221544.okaeWC1f-lkp@i...)
If you fix the issue in a separate patch/commit (i.e. not just a new version of the same patch/commit), kindly add following tags | Reported-by: kernel test robot yujie.liu@intel.com | Closes: https://lore.kernel.org/r/202402221544.okaeWC1f-lkp@intel.com/
All errors (new ones prefixed by >>):
ld.lld: error: undefined symbol: efi_status_to_err
referenced by cs-amp-lib.c sound/soc/codecs/cs-amp-lib.o:(cs_amp_get_efi_calibration_data) in archive vmlinux.a referenced by cs-amp-lib.c sound/soc/codecs/cs-amp-lib.o:(cs_amp_get_efi_calibration_data) in archive vmlinux.a
On Wed, 21 Feb 2024 16:04:58 +0100, Richard Fitzgerald wrote:
Factory calibration of the speakers stores the calibration information into an EFI variable.
This set of patches adds support for applying speaker calibration data from that EFI variable.
Patch #1 and #2 are bugfixes that are prerequisites.
The HDA patches #7 and #8 depend on the ASoC patches.
Patch #9 is a KUnit test for the new cs-amp-lib library.
Richard Fitzgerald (8): ASoC: wm_adsp: Fix missing mutex_lock in wm_adsp_write_ctl() ASoC: wm_adsp: Add wm_adsp_start() and wm_adsp_stop() ASoC: cs-amp-lib: Add helpers for factory calibration data ASoC: cs35l56: Add helper functions for amp calibration ASoC: cs35l56: Apply amp calibration from EFI data ALSA: hda: hda_cs_dsp_ctl: Add helper function to write calibration ALSA: hda: cs35l56: Apply amp calibration from EFI data ASoC: cs-amp-lib: Add KUnit test for calibration helpers
Simon Trimmer (1): ALSA: hda: hda_cs_dsp_ctl: Only call notify when a control has been added to a card
I guess it's better to be merged through ASoC tree. Feel free to my review tag
Reviewed-by: Takashi Iwai tiwai@suse.de
thanks,
Takashi
On 21/02/2024 15:04, Richard Fitzgerald wrote:
Factory calibration of the speakers stores the calibration information into an EFI variable.
This set of patches adds support for applying speaker calibration data from that EFI variable.
STOP! Don't merge.
I found a potential lockdep error in this code. Will send a V2.
On Wed, 21 Feb 2024 15:04:58 +0000, Richard Fitzgerald wrote:
Factory calibration of the speakers stores the calibration information into an EFI variable.
This set of patches adds support for applying speaker calibration data from that EFI variable.
Patch #1 and #2 are bugfixes that are prerequisites.
[...]
Applied to
https://git.kernel.org/pub/scm/linux/kernel/git/broonie/sound.git for-next
Thanks!
[1/9] ASoC: wm_adsp: Fix missing mutex_lock in wm_adsp_write_ctl() (no commit info) [2/9] ALSA: hda: hda_cs_dsp_ctl: Only call notify when a control has been added to a card (no commit info) [3/9] ASoC: wm_adsp: Add wm_adsp_start() and wm_adsp_stop() commit: 5519ac3a7164d5d1c31879bf5b0d279b58c8e88f [4/9] ASoC: cs-amp-lib: Add helpers for factory calibration data commit: 1cad8725f2b98965ed3658bc917090b30adb14fa [5/9] ASoC: cs35l56: Add helper functions for amp calibration commit: e1830f66f6c62d288d2c27a7ed18ab93caa0b253 [6/9] ASoC: cs35l56: Apply amp calibration from EFI data commit: 1326444e93c250ff99eba048f699313ba6acbf2f [7/9] ALSA: hda: hda_cs_dsp_ctl: Add helper function to write calibration (no commit info) [8/9] ALSA: hda: cs35l56: Apply amp calibration from EFI data commit: cfa43aaa7948be5a701ad4099588cf49d5a02708 [9/9] ASoC: cs-amp-lib: Add KUnit test for calibration helpers (no commit info)
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
participants (4)
-
kernel test robot
-
Mark Brown
-
Richard Fitzgerald
-
Takashi Iwai