// 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #define CIRRUS_LOGIC_CALIBRATION_EFI_NAME L"CirrusSmartAmpCalibrationData" #define CIRRUS_LOGIC_CALIBRATION_EFI_GUID \ EFI_GUID(0x02f9af02, 0x7734, 0x4233, 0xb4, 0x3d, 0x93, 0xfe, 0x5a, 0xa3, 0x5d, 0xb3) #define LENOVO_SPEAKER_ID_EFI_NAME L"SdwSpeaker" #define LENOVO_SPEAKER_ID_EFI_GUID \ EFI_GUID(0x48df970e, 0xe27f, 0x460a, 0xb5, 0x86, 0x77, 0x19, 0x80, 0x1d, 0x92, 0x82) #define HP_SPEAKER_ID_EFI_NAME L"HPSpeakerID" #define HP_SPEAKER_ID_EFI_GUID \ EFI_GUID(0xc49593a4, 0xd099, 0x419b, 0xa2, 0xc3, 0x67, 0xe9, 0x80, 0xe6, 0x1d, 0x1e) #define HP_CALIBRATION_EFI_NAME L"SmartAmpCalibrationData" #define HP_CALIBRATION_EFI_GUID \ EFI_GUID(0x53559579, 0x8753, 0x4f5c, 0x91, 0x30, 0xe8, 0x2a, 0xcf, 0xb8, 0xd8, 0x93) static const struct cs_amp_lib_cal_efivar { efi_char16_t *name; efi_guid_t *guid; } cs_amp_lib_cal_efivars[] = { { .name = HP_CALIBRATION_EFI_NAME, .guid = &HP_CALIBRATION_EFI_GUID, }, { .name = CIRRUS_LOGIC_CALIBRATION_EFI_NAME, .guid = &CIRRUS_LOGIC_CALIBRATION_EFI_GUID, }, }; #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); return time_in_100ns + UNIX_TIME_TO_WINDOWS_TIME_OFFSET; } static int cs_amp_write_cal_coeff(struct cs_dsp *dsp, const struct cirrus_amp_cal_controls *controls, const char *ctl_name, u32 val) { struct cs_dsp_coeff_ctl *cs_ctl; __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_FW_CS_DSP)) { mutex_lock(&dsp->pwr_lock); cs_ctl = cs_dsp_get_ctl(dsp, ctl_name, controls->mem_region, controls->alg_id); ret = cs_dsp_coeff_write_ctrl(cs_ctl, 0, &beval, sizeof(beval)); mutex_unlock(&dsp->pwr_lock); if (ret < 0) { dev_err(dsp->dev, "Failed to write to '%s': %d\n", ctl_name, ret); return ret; } return 0; } return -ENODEV; } static int cs_amp_read_cal_coeff(struct cs_dsp *dsp, const struct cirrus_amp_cal_controls *controls, const char *ctl_name, u32 *val) { struct cs_dsp_coeff_ctl *cs_ctl; __be32 beval; int ret; KUNIT_STATIC_STUB_REDIRECT(cs_amp_read_cal_coeff, dsp, controls, ctl_name, val); if (!IS_REACHABLE(CONFIG_FW_CS_DSP)) return -ENODEV; scoped_guard(mutex, &dsp->pwr_lock) { cs_ctl = cs_dsp_get_ctl(dsp, ctl_name, controls->mem_region, controls->alg_id); ret = cs_dsp_coeff_read_ctrl(cs_ctl, 0, &beval, sizeof(beval)); } if (ret < 0) { dev_err(dsp->dev, "Failed to write to '%s': %d\n", ctl_name, ret); return ret; } *val = be32_to_cpu(beval); return 0; } static int _cs_amp_write_cal_coeffs(struct cs_dsp *dsp, const struct cirrus_amp_cal_controls *controls, const struct cirrus_amp_cal_data *data) { int ret; dev_dbg(dsp->dev, "Calibration: Ambient=%#x, Status=%#x, CalR=%d\n", data->calAmbient, data->calStatus, data->calR); if (list_empty(&dsp->ctl_list)) { dev_info(dsp->dev, "Calibration disabled due to missing firmware controls\n"); return -ENOENT; } 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; } static int _cs_amp_read_cal_coeffs(struct cs_dsp *dsp, const struct cirrus_amp_cal_controls *controls, struct cirrus_amp_cal_data *data) { u64 time; u32 val; int ret; if (list_empty(&dsp->ctl_list)) { dev_info(dsp->dev, "Calibration disabled due to missing firmware controls\n"); return -ENOENT; } ret = cs_amp_read_cal_coeff(dsp, controls, controls->ambient, &val); if (ret) return ret; data->calAmbient = (s8)val; ret = cs_amp_read_cal_coeff(dsp, controls, controls->calr, &val); if (ret) return ret; data->calR = (u16)val; ret = cs_amp_read_cal_coeff(dsp, controls, controls->status, &val); if (ret) return ret; data->calStatus = (u8)val; /* Fill in timestamp */ time = cs_amp_time_now_in_windows_time(); data->calTime[0] = (u32)time; data->calTime[1] = (u32)(time >> 32); return 0; } /** * cs_amp_write_cal_coeffs - Write calibration data to firmware controls. * @dsp: Pointer to struct cs_dsp. * @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 cs_dsp *dsp, const struct cirrus_amp_cal_controls *controls, const struct cirrus_amp_cal_data *data) { if (IS_REACHABLE(CONFIG_FW_CS_DSP) || IS_ENABLED(CONFIG_SND_SOC_CS_AMP_LIB_TEST)) 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"); /** * cs_amp_read_cal_coeffs - Read calibration data from firmware controls. * @dsp: Pointer to struct cs_dsp. * @controls: Pointer to definition of firmware controls to be read. * @data: Pointer to calibration data where results will be written. * * Returns: 0 on success, else negative error value. */ int cs_amp_read_cal_coeffs(struct cs_dsp *dsp, const struct cirrus_amp_cal_controls *controls, struct cirrus_amp_cal_data *data) { if (IS_REACHABLE(CONFIG_FW_CS_DSP) || IS_ENABLED(CONFIG_SND_SOC_CS_AMP_LIB_TEST)) return _cs_amp_read_cal_coeffs(dsp, controls, data); else return -ENODEV; } EXPORT_SYMBOL_NS_GPL(cs_amp_read_cal_coeffs, "SND_SOC_CS_AMP_LIB"); /** * cs_amp_write_ambient_temp - write value to calibration ambient temperature * @dsp: Pointer to struct cs_dsp. * @controls: Pointer to definition of firmware controls to be read. * @temp: Temperature in degrees celcius. * * Returns: 0 on success, else negative error value. */ int cs_amp_write_ambient_temp(struct cs_dsp *dsp, const struct cirrus_amp_cal_controls *controls, u32 temp) { if (IS_REACHABLE(CONFIG_FW_CS_DSP) || IS_ENABLED(CONFIG_SND_SOC_CS_AMP_LIB_TEST)) return cs_amp_write_cal_coeff(dsp, controls, controls->ambient, temp); else return -ENODEV; } EXPORT_SYMBOL_NS_GPL(cs_amp_write_ambient_temp, "SND_SOC_CS_AMP_LIB"); static efi_status_t cs_amp_get_efi_variable(efi_char16_t *name, efi_guid_t *guid, u32 *returned_attr, unsigned long *size, void *buf) { u32 attr; if (!returned_attr) returned_attr = &attr; KUNIT_STATIC_STUB_REDIRECT(cs_amp_get_efi_variable, name, guid, returned_attr, size, buf); if (efi_rt_services_supported(EFI_RT_SUPPORTED_GET_VARIABLE)) return efi.get_variable(name, guid, returned_attr, size, buf); 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) { case EFI_SUCCESS: return 0; case EFI_NOT_FOUND: return -ENOENT; case EFI_BUFFER_TOO_SMALL: return -EFBIG; case EFI_WRITE_PROTECTED: case EFI_UNSUPPORTED: case EFI_ACCESS_DENIED: case EFI_SECURITY_VIOLATION: return -EACCES; default: return -EIO; } } 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; u8 *data; efi_status_t status; int i, ret; /* Find EFI variable and get size */ 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, attr, &data_size, NULL); if (status == EFI_BUFFER_TOO_SMALL) break; } 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); } /* 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_lib_cal_efivars[i].name, cs_amp_lib_cal_efivars[i].guid, attr, &data_size, data); if (status != EFI_SUCCESS) { ret = -EINVAL; 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) || struct_size(efi_data, data, efi_data->count) > data_size) { dev_err(dev, "EFI cal variable truncated\n"); ret = -EOVERFLOW; 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: kfree(data); dev_err(dev, "Failed to read calibration data from EFI: %d\n", ret); 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) { 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, NULL, NULL, NULL); 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 empty entries */ if (!efi_data->data[i].calTime[0] && !efi_data->data[i].calTime[1]) continue; /* 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) && (efi_data->data[amp_index].calTime[0] || efi_data->data[amp_index].calTime[1])) { 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; } 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. * @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) || 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"); /** * 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; u8 values[2]; }; static int cs_amp_get_efi_byte_spkid(struct device *dev, const struct cs_amp_spkid_efi *info) { efi_status_t status; unsigned long size; u8 spkid; int i, ret; size = sizeof(spkid); status = cs_amp_get_efi_variable(info->name, info->guid, NULL, &size, &spkid); ret = cs_amp_convert_efi_status(status); if (ret < 0) return ret; if (size == 0) return -ENOENT; for (i = 0; i < ARRAY_SIZE(info->values); i++) { if (info->values[i] == spkid) return i; } dev_err(dev, "EFI speaker ID bad value %#x\n", spkid); return -EINVAL; } static const struct cs_amp_spkid_efi cs_amp_spkid_byte_types[] = { { .name = LENOVO_SPEAKER_ID_EFI_NAME, .guid = &LENOVO_SPEAKER_ID_EFI_GUID, .values = { 0xd0, 0xd1 }, }, { .name = HP_SPEAKER_ID_EFI_NAME, .guid = &HP_SPEAKER_ID_EFI_GUID, .values = { 0x30, 0x31 }, }, }; /** * cs_amp_get_vendor_spkid - get a speaker ID from vendor-specific storage * @dev: pointer to struct device * * Known vendor-specific methods of speaker ID are checked and if one is * found its speaker ID value is returned. * * Return: >=0 is a valid speaker ID. -ENOENT if a vendor-specific method * was not found. -EACCES if the vendor-specific storage could not * be read. Other error values indicate that the data from the * vendor-specific storage was found but could not be understood. */ int cs_amp_get_vendor_spkid(struct device *dev) { int i, ret; if (!efi_rt_services_supported(EFI_RT_SUPPORTED_GET_VARIABLE) && !IS_ENABLED(CONFIG_SND_SOC_CS_AMP_LIB_TEST)) return -ENOENT; for (i = 0; i < ARRAY_SIZE(cs_amp_spkid_byte_types); i++) { ret = cs_amp_get_efi_byte_spkid(dev, &cs_amp_spkid_byte_types[i]); if (ret != -ENOENT) return ret; } return -ENOENT; } EXPORT_SYMBOL_NS_GPL(cs_amp_get_vendor_spkid, "SND_SOC_CS_AMP_LIB"); /** * cs_amp_create_debugfs - create a debugfs directory for a device * * @dev: pointer to struct device * * Creates a node under "cirrus_logic" in the root of the debugfs filesystem. * This is for Cirrus-specific debugfs functionality to be grouped in a * defined way, independently of the debugfs provided by ALSA/ASoC. * The general ALSA/ASoC debugfs may not be enabled, and does not necessarily * have a stable layout or naming convention. * * Return: Pointer to the dentry for the created directory, or -ENODEV. */ struct dentry *cs_amp_create_debugfs(struct device *dev) { struct dentry *dir; dir = debugfs_lookup("cirrus_logic", NULL); if (!dir) dir = debugfs_create_dir("cirrus_logic", NULL); return debugfs_create_dir(dev_name(dev), dir); } EXPORT_SYMBOL_NS_GPL(cs_amp_create_debugfs, "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, .set_efi_variable = cs_amp_set_efi_variable, .write_cal_coeff = cs_amp_write_cal_coeff, .read_cal_coeff = cs_amp_read_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 "); MODULE_LICENSE("GPL"); MODULE_IMPORT_NS("FW_CS_DSP");