]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
ASoC: cs-amp-lib: Add function to write calibration to UEFI
authorRichard Fitzgerald <rf@opensource.cirrus.com>
Tue, 21 Oct 2025 10:50:19 +0000 (11:50 +0100)
committerMark Brown <broonie@kernel.org>
Mon, 27 Oct 2025 14:07:48 +0000 (14:07 +0000)
Add cs_amp_set_efi_calibration_data() to write an amp calibration
blob to UEFI calibration variable.

The UEFI variable will be updated or created as necessary.

- If a Vendor-specific variable exists it will be updated,
  else if the Cirrus variable exists it will be update
  else the Cirrus variable will be created.

Some collateral changes are required:

- cs_amp_convert_efi_status() now specifically handles
  EFI_WRITE_PROTECTED error.

- cs_amp_get_cal_efi_buffer() can optionally return the name,
  guid and attr of the variable it found.

- cs_amp_get_cal_efi_buffer() will update the 'size' field of
  the returned data blob if it is zero. The BIOS could have
  pre-allocated the UEFI variable as zero-filled

Signed-off-by: Richard Fitzgerald <rf@opensource.cirrus.com>
Reviewed-by: Takashi Iwai <tiwai@suse.de>
Link: https://patch.msgid.link/20251021105022.1013685-9-rf@opensource.cirrus.com
Signed-off-by: Mark Brown <broonie@kernel.org>
include/sound/cs-amp-lib.h
sound/soc/codecs/cs-amp-lib.c

index 2e5616a5e1f706d7e99b0b534cbf4b3fbd654d76..240bc53a93075efe45921edadc3424589c43f9c7 100644 (file)
@@ -55,6 +55,8 @@ int cs_amp_write_ambient_temp(struct cs_dsp *dsp,
                              u32 temp);
 int cs_amp_get_efi_calibration_data(struct device *dev, u64 target_uid, int amp_index,
                                    struct cirrus_amp_cal_data *out_data);
+int cs_amp_set_efi_calibration_data(struct device *dev, int amp_index, int num_amps,
+                                   const struct cirrus_amp_cal_data *in_data);
 int cs_amp_get_vendor_spkid(struct device *dev);
 struct dentry *cs_amp_create_debugfs(struct device *dev);
 
index c5791cbeb5b813fc7ec1554e56489bf4458ff50e..7038574e3f4b62145ca558719387d9877f34939d 100644 (file)
@@ -13,6 +13,7 @@
 #include <linux/firmware/cirrus/cs_dsp.h>
 #include <linux/math64.h>
 #include <linux/module.h>
+#include <linux/mutex.h>
 #include <linux/overflow.h>
 #include <linux/slab.h>
 #include <linux/timekeeping.h>
@@ -49,9 +50,16 @@ static const struct cs_amp_lib_cal_efivar {
        },
 };
 
+#define CS_AMP_CAL_DEFAULT_EFI_ATTR                    \
+               (EFI_VARIABLE_NON_VOLATILE |            \
+                EFI_VARIABLE_BOOTSERVICE_ACCESS |      \
+                EFI_VARIABLE_RUNTIME_ACCESS)
+
 /* Offset from Unix time to Windows time (100ns since 1 Jan 1601) */
 #define UNIX_TIME_TO_WINDOWS_TIME_OFFSET       116444736000000000ULL
 
+static DEFINE_MUTEX(cs_amp_efi_cal_write_lock);
+
 static u64 cs_amp_time_now_in_windows_time(void)
 {
        u64 time_in_100ns = div_u64(ktime_get_real_ns(), 100);
@@ -263,6 +271,20 @@ static efi_status_t cs_amp_get_efi_variable(efi_char16_t *name,
        return EFI_NOT_FOUND;
 }
 
+static efi_status_t cs_amp_set_efi_variable(efi_char16_t *name,
+                                           efi_guid_t *guid,
+                                           u32 attr,
+                                           unsigned long size,
+                                           void *buf)
+{
+       KUNIT_STATIC_STUB_REDIRECT(cs_amp_set_efi_variable, name, guid, attr, size, buf);
+
+       if (!efi_rt_services_supported(EFI_RT_SUPPORTED_SET_VARIABLE))
+               return EFI_NOT_FOUND;
+
+       return efi.set_variable(name, guid, attr, size, buf);
+}
+
 static int cs_amp_convert_efi_status(efi_status_t status)
 {
        switch (status) {
@@ -272,6 +294,7 @@ static int cs_amp_convert_efi_status(efi_status_t status)
                return -ENOENT;
        case EFI_BUFFER_TOO_SMALL:
                return -EFBIG;
+       case EFI_WRITE_PROTECTED:
        case EFI_UNSUPPORTED:
        case EFI_ACCESS_DENIED:
        case EFI_SECURITY_VIOLATION:
@@ -281,7 +304,10 @@ static int cs_amp_convert_efi_status(efi_status_t status)
        }
 }
 
-static struct cirrus_amp_efi_data *cs_amp_get_cal_efi_buffer(struct device *dev)
+static struct cirrus_amp_efi_data *cs_amp_get_cal_efi_buffer(struct device *dev,
+                                                            efi_char16_t **name,
+                                                            efi_guid_t **guid,
+                                                            u32 *attr)
 {
        struct cirrus_amp_efi_data *efi_data;
        unsigned long data_size = 0;
@@ -293,7 +319,7 @@ static struct cirrus_amp_efi_data *cs_amp_get_cal_efi_buffer(struct device *dev)
        for (i = 0; i < ARRAY_SIZE(cs_amp_lib_cal_efivars); i++) {
                status = cs_amp_get_efi_variable(cs_amp_lib_cal_efivars[i].name,
                                                 cs_amp_lib_cal_efivars[i].guid,
-                                                NULL, &data_size, NULL);
+                                                attr, &data_size, NULL);
                if (status == EFI_BUFFER_TOO_SMALL)
                        break;
        }
@@ -301,6 +327,12 @@ static struct cirrus_amp_efi_data *cs_amp_get_cal_efi_buffer(struct device *dev)
        if (status != EFI_BUFFER_TOO_SMALL)
                return ERR_PTR(-ENOENT);
 
+       if (name)
+               *name = cs_amp_lib_cal_efivars[i].name;
+
+       if (guid)
+               *guid = cs_amp_lib_cal_efivars[i].guid;
+
        if (data_size < sizeof(*efi_data)) {
                dev_err(dev, "EFI cal variable truncated\n");
                return ERR_PTR(-EOVERFLOW);
@@ -313,7 +345,7 @@ static struct cirrus_amp_efi_data *cs_amp_get_cal_efi_buffer(struct device *dev)
 
        status = cs_amp_get_efi_variable(cs_amp_lib_cal_efivars[i].name,
                                         cs_amp_lib_cal_efivars[i].guid,
-                                        NULL, &data_size, data);
+                                        attr, &data_size, data);
        if (status != EFI_SUCCESS) {
                ret = -EINVAL;
                goto err;
@@ -329,6 +361,10 @@ static struct cirrus_amp_efi_data *cs_amp_get_cal_efi_buffer(struct device *dev)
                goto err;
        }
 
+       /* This could be zero-filled space pre-allocated by the BIOS */
+       if (efi_data->size == 0)
+               efi_data->size = data_size;
+
        return efi_data;
 
 err:
@@ -338,6 +374,20 @@ err:
        return ERR_PTR(ret);
 }
 
+static int cs_amp_set_cal_efi_buffer(struct device *dev,
+                                    efi_char16_t *name,
+                                    efi_guid_t *guid,
+                                    u32 attr,
+                                    struct cirrus_amp_efi_data *data)
+{
+       efi_status_t status;
+
+       status = cs_amp_set_efi_variable(name, guid, attr,
+                                        struct_size(data, data, data->count), data);
+
+       return cs_amp_convert_efi_status(status);
+}
+
 static int _cs_amp_get_efi_calibration_data(struct device *dev, u64 target_uid, int amp_index,
                                            struct cirrus_amp_cal_data *out_data)
 {
@@ -345,7 +395,7 @@ static int _cs_amp_get_efi_calibration_data(struct device *dev, u64 target_uid,
        struct cirrus_amp_cal_data *cal = NULL;
        int i, ret;
 
-       efi_data = cs_amp_get_cal_efi_buffer(dev);
+       efi_data = cs_amp_get_cal_efi_buffer(dev, NULL, NULL, NULL);
        if (IS_ERR(efi_data))
                return PTR_ERR(efi_data);
 
@@ -397,6 +447,98 @@ static int _cs_amp_get_efi_calibration_data(struct device *dev, u64 target_uid,
        return ret;
 }
 
+static int _cs_amp_set_efi_calibration_data(struct device *dev, int amp_index, int num_amps,
+                                           const struct cirrus_amp_cal_data *in_data)
+{
+       u64 cal_target = cs_amp_cal_target_u64(in_data);
+       unsigned long num_entries;
+       struct cirrus_amp_efi_data *data __free(kfree) = NULL;
+       efi_char16_t *name = CIRRUS_LOGIC_CALIBRATION_EFI_NAME;
+       efi_guid_t *guid = &CIRRUS_LOGIC_CALIBRATION_EFI_GUID;
+       u32 attr = CS_AMP_CAL_DEFAULT_EFI_ATTR;
+       int i, ret;
+
+       if (cal_target == 0)
+               return -EINVAL;
+
+       data = cs_amp_get_cal_efi_buffer(dev, &name, &guid, &attr);
+       ret = PTR_ERR_OR_ZERO(data);
+       if (ret == -ENOENT) {
+               data = NULL;
+               goto alloc_new;
+       } else if (ret) {
+               return ret;
+       }
+
+       /*
+        * If the EFI variable is just zero-filled reserved space the count
+        * must be set.
+        */
+       if (data->count == 0)
+               data->count = (data->size - sizeof(data)) / sizeof(data->data[0]);
+
+       if (amp_index < 0) {
+               /* Is there already a slot for this target? */
+               for (amp_index = 0; amp_index < data->count; amp_index++) {
+                       if (cs_amp_cal_target_u64(&data->data[amp_index]) == cal_target)
+                               break;
+               }
+
+               /* Else find an empty slot */
+               if (amp_index >= data->count) {
+                       for (amp_index = 0; amp_index < data->count; amp_index++) {
+                               if ((data->data[amp_index].calTime[0] == 0) &&
+                                   (data->data[amp_index].calTime[1] == 0))
+                                       break;
+                       }
+               }
+       } else {
+               /*
+                * If the index is forced there could be another active
+                * slot with the same calTarget. So deduplicate.
+                */
+               for (i = 0; i < data->count; i++) {
+                       if (i == amp_index)
+                               continue;
+
+                       if ((data->data[i].calTime[0] == 0) && (data->data[i].calTime[1] == 0))
+                               continue;
+
+                       if (cs_amp_cal_target_u64(&data->data[i]) == cal_target)
+                               memset(data->data[i].calTime, 0, sizeof(data->data[i].calTime));
+               }
+       }
+
+alloc_new:
+       if (amp_index < 0)
+               amp_index = 0;
+
+       num_entries = max(num_amps, amp_index + 1);
+       if (!data || (data->count < num_entries)) {
+               struct cirrus_amp_efi_data *old_data __free(kfree) = no_free_ptr(data);
+               unsigned int new_data_size = struct_size(data, data, num_entries);
+
+               data = kzalloc(new_data_size, GFP_KERNEL);
+               if (!data)
+                       return -ENOMEM;
+
+               if (old_data)
+                       memcpy(data, old_data, struct_size(old_data, data, old_data->count));
+
+               data->count = num_entries;
+               data->size = new_data_size;
+       }
+
+       data->data[amp_index] = *in_data;
+       ret = cs_amp_set_cal_efi_buffer(dev, name, guid, attr, data);
+       if (ret) {
+               dev_err(dev, "Failed writing calibration to EFI: %d\n", ret);
+               return ret;
+       }
+
+       return 0;
+}
+
 /**
  * cs_amp_get_efi_calibration_data - get an entry from calibration data in EFI.
  * @dev:       struct device of the caller.
@@ -443,6 +585,46 @@ int cs_amp_get_efi_calibration_data(struct device *dev, u64 target_uid, int amp_
 }
 EXPORT_SYMBOL_NS_GPL(cs_amp_get_efi_calibration_data, "SND_SOC_CS_AMP_LIB");
 
+/**
+ * cs_amp_set_efi_calibration_data - write a calibration data entry to EFI.
+ * @dev:       struct device of the caller.
+ * @amp_index: Entry index to use, or -1 to use any available slot.
+ * @num_amps:  Maximum number of amps to reserve slots for, or -1 to ignore.
+ * @in_data:   struct cirrus_amp_cal_data entry to be written to EFI.
+ *
+ * If a Vendor-specific variable exists it will be updated,
+ * else if the Cirrus variable exists it will be updated
+ * else the Cirrus variable will be created.
+ *
+ * If amp_index >= 0 the data will be placed in this entry of the calibration
+ * data array, overwriting what was in that entry. Any other entries with the
+ * same calTarget will be marked empty.
+ *
+ * If amp_index < 0 and in_data->calTarget matches any existing entry, that
+ * entry will be overwritten. Else the first available free entry will be used,
+ * extending the size of the EFI variable if there are no free entries.
+ *
+ * If num_amps > 0 the EFI variable will be sized to contain at least this
+ * many calibration entries, with any new entries marked empty.
+ *
+ * Return: 0 if the write was successful, -EFBIG if space could not be made in
+ *        the EFI file to add the entry, -EACCES if it was not possible to
+ *        read or write the EFI variable.
+ */
+int cs_amp_set_efi_calibration_data(struct device *dev, int amp_index, int num_amps,
+                                   const struct cirrus_amp_cal_data *in_data)
+{
+       if (IS_ENABLED(CONFIG_EFI) || IS_ENABLED(CONFIG_SND_SOC_CS_AMP_LIB_TEST)) {
+               scoped_guard(mutex, &cs_amp_efi_cal_write_lock) {
+                       return _cs_amp_set_efi_calibration_data(dev, amp_index,
+                                                               num_amps, in_data);
+               }
+       }
+
+       return -ENOENT;
+}
+EXPORT_SYMBOL_NS_GPL(cs_amp_set_efi_calibration_data, "SND_SOC_CS_AMP_LIB");
+
 struct cs_amp_spkid_efi {
        efi_char16_t *name;
        efi_guid_t *guid;