ASoC: cs-amp-lib: Add function to write calibration to UEFI

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>
This commit is contained in:
Richard Fitzgerald
2025-10-21 11:50:19 +01:00
committed by Mark Brown
parent 959400caf5
commit 2b62e66626
2 changed files with 188 additions and 4 deletions

View 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);

View 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;