1 | // SPDX-License-Identifier: GPL-2.0-only |
2 | // |
3 | // Common code for Cirrus Logic Smart Amplifiers |
4 | // |
5 | // Copyright (C) 2024 Cirrus Logic, Inc. and |
6 | // Cirrus Logic International Semiconductor Ltd. |
7 | |
8 | #include <asm/byteorder.h> |
9 | #include <kunit/static_stub.h> |
10 | #include <linux/dev_printk.h> |
11 | #include <linux/efi.h> |
12 | #include <linux/firmware/cirrus/cs_dsp.h> |
13 | #include <linux/module.h> |
14 | #include <linux/slab.h> |
15 | #include <linux/types.h> |
16 | #include <sound/cs-amp-lib.h> |
17 | |
18 | #define CS_AMP_CAL_GUID \ |
19 | EFI_GUID(0x02f9af02, 0x7734, 0x4233, 0xb4, 0x3d, 0x93, 0xfe, 0x5a, 0xa3, 0x5d, 0xb3) |
20 | |
21 | #define CS_AMP_CAL_NAME L"CirrusSmartAmpCalibrationData" |
22 | |
23 | static int cs_amp_write_cal_coeff(struct cs_dsp *dsp, |
24 | const struct cirrus_amp_cal_controls *controls, |
25 | const char *ctl_name, u32 val) |
26 | { |
27 | struct cs_dsp_coeff_ctl *cs_ctl; |
28 | __be32 beval = cpu_to_be32(val); |
29 | int ret; |
30 | |
31 | KUNIT_STATIC_STUB_REDIRECT(cs_amp_write_cal_coeff, dsp, controls, ctl_name, val); |
32 | |
33 | if (IS_REACHABLE(CONFIG_FW_CS_DSP)) { |
34 | mutex_lock(&dsp->pwr_lock); |
35 | cs_ctl = cs_dsp_get_ctl(dsp, name: ctl_name, type: controls->mem_region, alg: controls->alg_id); |
36 | ret = cs_dsp_coeff_write_ctrl(ctl: cs_ctl, off: 0, buf: &beval, len: sizeof(beval)); |
37 | mutex_unlock(lock: &dsp->pwr_lock); |
38 | |
39 | if (ret < 0) { |
40 | dev_err(dsp->dev, "Failed to write to '%s': %d\n" , ctl_name, ret); |
41 | return ret; |
42 | } |
43 | |
44 | return 0; |
45 | } |
46 | |
47 | return -ENODEV; |
48 | } |
49 | |
50 | static int _cs_amp_write_cal_coeffs(struct cs_dsp *dsp, |
51 | const struct cirrus_amp_cal_controls *controls, |
52 | const struct cirrus_amp_cal_data *data) |
53 | { |
54 | int ret; |
55 | |
56 | dev_dbg(dsp->dev, "Calibration: Ambient=%#x, Status=%#x, CalR=%d\n" , |
57 | data->calAmbient, data->calStatus, data->calR); |
58 | |
59 | if (list_empty(head: &dsp->ctl_list)) { |
60 | dev_info(dsp->dev, "Calibration disabled due to missing firmware controls\n" ); |
61 | return -ENOENT; |
62 | } |
63 | |
64 | ret = cs_amp_write_cal_coeff(dsp, controls, ctl_name: controls->ambient, val: data->calAmbient); |
65 | if (ret) |
66 | return ret; |
67 | |
68 | ret = cs_amp_write_cal_coeff(dsp, controls, ctl_name: controls->calr, val: data->calR); |
69 | if (ret) |
70 | return ret; |
71 | |
72 | ret = cs_amp_write_cal_coeff(dsp, controls, ctl_name: controls->status, val: data->calStatus); |
73 | if (ret) |
74 | return ret; |
75 | |
76 | ret = cs_amp_write_cal_coeff(dsp, controls, ctl_name: controls->checksum, val: data->calR + 1); |
77 | if (ret) |
78 | return ret; |
79 | |
80 | return 0; |
81 | } |
82 | |
83 | /** |
84 | * cs_amp_write_cal_coeffs - Write calibration data to firmware controls. |
85 | * @dsp: Pointer to struct cs_dsp. |
86 | * @controls: Pointer to definition of firmware controls to be written. |
87 | * @data: Pointer to calibration data. |
88 | * |
89 | * Returns: 0 on success, else negative error value. |
90 | */ |
91 | int cs_amp_write_cal_coeffs(struct cs_dsp *dsp, |
92 | const struct cirrus_amp_cal_controls *controls, |
93 | const struct cirrus_amp_cal_data *data) |
94 | { |
95 | if (IS_REACHABLE(CONFIG_FW_CS_DSP) || IS_ENABLED(CONFIG_SND_SOC_CS_AMP_LIB_TEST)) |
96 | return _cs_amp_write_cal_coeffs(dsp, controls, data); |
97 | else |
98 | return -ENODEV; |
99 | } |
100 | EXPORT_SYMBOL_NS_GPL(cs_amp_write_cal_coeffs, SND_SOC_CS_AMP_LIB); |
101 | |
102 | static efi_status_t cs_amp_get_efi_variable(efi_char16_t *name, |
103 | efi_guid_t *guid, |
104 | unsigned long *size, |
105 | void *buf) |
106 | { |
107 | u32 attr; |
108 | |
109 | KUNIT_STATIC_STUB_REDIRECT(cs_amp_get_efi_variable, name, guid, size, buf); |
110 | |
111 | if (IS_ENABLED(CONFIG_EFI)) |
112 | return efi.get_variable(name, guid, &attr, size, buf); |
113 | |
114 | return EFI_NOT_FOUND; |
115 | } |
116 | |
117 | static struct cirrus_amp_efi_data *cs_amp_get_cal_efi_buffer(struct device *dev) |
118 | { |
119 | struct cirrus_amp_efi_data *efi_data; |
120 | unsigned long data_size = 0; |
121 | u8 *data; |
122 | efi_status_t status; |
123 | int ret; |
124 | |
125 | /* Get real size of UEFI variable */ |
126 | status = cs_amp_get_efi_variable(CS_AMP_CAL_NAME, guid: &CS_AMP_CAL_GUID, size: &data_size, NULL); |
127 | if (status != EFI_BUFFER_TOO_SMALL) |
128 | return ERR_PTR(error: -ENOENT); |
129 | |
130 | if (data_size < sizeof(*efi_data)) { |
131 | dev_err(dev, "EFI cal variable truncated\n" ); |
132 | return ERR_PTR(error: -EOVERFLOW); |
133 | } |
134 | |
135 | /* Get variable contents into buffer */ |
136 | data = kmalloc(size: data_size, GFP_KERNEL); |
137 | if (!data) |
138 | return ERR_PTR(error: -ENOMEM); |
139 | |
140 | status = cs_amp_get_efi_variable(CS_AMP_CAL_NAME, guid: &CS_AMP_CAL_GUID, size: &data_size, buf: data); |
141 | if (status != EFI_SUCCESS) { |
142 | ret = -EINVAL; |
143 | goto err; |
144 | } |
145 | |
146 | efi_data = (struct cirrus_amp_efi_data *)data; |
147 | dev_dbg(dev, "Calibration: Size=%d, Amp Count=%d\n" , efi_data->size, efi_data->count); |
148 | |
149 | if ((efi_data->count > 128) || |
150 | offsetof(struct cirrus_amp_efi_data, data[efi_data->count]) > data_size) { |
151 | dev_err(dev, "EFI cal variable truncated\n" ); |
152 | ret = -EOVERFLOW; |
153 | goto err; |
154 | } |
155 | |
156 | return efi_data; |
157 | |
158 | err: |
159 | kfree(objp: data); |
160 | dev_err(dev, "Failed to read calibration data from EFI: %d\n" , ret); |
161 | |
162 | return ERR_PTR(error: ret); |
163 | } |
164 | |
165 | static u64 cs_amp_cal_target_u64(const struct cirrus_amp_cal_data *data) |
166 | { |
167 | return ((u64)data->calTarget[1] << 32) | data->calTarget[0]; |
168 | } |
169 | |
170 | static int _cs_amp_get_efi_calibration_data(struct device *dev, u64 target_uid, int amp_index, |
171 | struct cirrus_amp_cal_data *out_data) |
172 | { |
173 | struct cirrus_amp_efi_data *efi_data; |
174 | struct cirrus_amp_cal_data *cal = NULL; |
175 | int i, ret; |
176 | |
177 | efi_data = cs_amp_get_cal_efi_buffer(dev); |
178 | if (IS_ERR(ptr: efi_data)) |
179 | return PTR_ERR(ptr: efi_data); |
180 | |
181 | if (target_uid) { |
182 | for (i = 0; i < efi_data->count; ++i) { |
183 | u64 cal_target = cs_amp_cal_target_u64(data: &efi_data->data[i]); |
184 | |
185 | /* Skip entries with unpopulated silicon ID */ |
186 | if (cal_target == 0) |
187 | continue; |
188 | |
189 | if (cal_target == target_uid) { |
190 | cal = &efi_data->data[i]; |
191 | break; |
192 | } |
193 | } |
194 | } |
195 | |
196 | if (!cal && (amp_index >= 0) && (amp_index < efi_data->count)) { |
197 | u64 cal_target = cs_amp_cal_target_u64(data: &efi_data->data[amp_index]); |
198 | |
199 | /* |
200 | * Treat unpopulated cal_target as a wildcard. |
201 | * If target_uid != 0 we can only get here if cal_target == 0 |
202 | * or it didn't match any cal_target value. |
203 | * If target_uid == 0 it is a wildcard. |
204 | */ |
205 | if ((cal_target == 0) || (target_uid == 0)) |
206 | cal = &efi_data->data[amp_index]; |
207 | else |
208 | dev_warn(dev, "Calibration entry %d does not match silicon ID" , amp_index); |
209 | } |
210 | |
211 | if (cal) { |
212 | memcpy(out_data, cal, sizeof(*out_data)); |
213 | ret = 0; |
214 | } else { |
215 | dev_warn(dev, "No calibration for silicon ID %#llx\n" , target_uid); |
216 | ret = -ENOENT; |
217 | } |
218 | |
219 | kfree(objp: efi_data); |
220 | |
221 | return ret; |
222 | } |
223 | |
224 | /** |
225 | * cs_amp_get_efi_calibration_data - get an entry from calibration data in EFI. |
226 | * @dev: struct device of the caller. |
227 | * @target_uid: UID to match, or zero to ignore UID matching. |
228 | * @amp_index: Entry index to use, or -1 to prevent lookup by index. |
229 | * @out_data: struct cirrus_amp_cal_data where the entry will be copied. |
230 | * |
231 | * This function can perform 3 types of lookup: |
232 | * |
233 | * (target_uid > 0, amp_index >= 0) |
234 | * UID search with fallback to using the array index. |
235 | * Search the calibration data for a non-zero calTarget that matches |
236 | * target_uid, and if found return that entry. Else, if the entry at |
237 | * [amp_index] has calTarget == 0, return that entry. Else fail. |
238 | * |
239 | * (target_uid > 0, amp_index < 0) |
240 | * UID search only. |
241 | * Search the calibration data for a non-zero calTarget that matches |
242 | * target_uid, and if found return that entry. Else fail. |
243 | * |
244 | * (target_uid == 0, amp_index >= 0) |
245 | * Array index fetch only. |
246 | * Return the entry at [amp_index]. |
247 | * |
248 | * An array lookup will be skipped if amp_index exceeds the number of |
249 | * entries in the calibration array, and in this case the return will |
250 | * be -ENOENT. An out-of-range amp_index does not prevent matching by |
251 | * target_uid - it has the same effect as passing amp_index < 0. |
252 | * |
253 | * If the EFI data is too short to be a valid entry, or the entry count |
254 | * in the EFI data overflows the actual length of the data, this function |
255 | * returns -EOVERFLOW. |
256 | * |
257 | * Return: 0 if the entry was found, -ENOENT if no entry was found, |
258 | * -EOVERFLOW if the EFI file is corrupt, else other error value. |
259 | */ |
260 | int cs_amp_get_efi_calibration_data(struct device *dev, u64 target_uid, int amp_index, |
261 | struct cirrus_amp_cal_data *out_data) |
262 | { |
263 | if (IS_ENABLED(CONFIG_EFI) || IS_ENABLED(CONFIG_SND_SOC_CS_AMP_LIB_TEST)) |
264 | return _cs_amp_get_efi_calibration_data(dev, target_uid, amp_index, out_data); |
265 | else |
266 | return -ENOENT; |
267 | } |
268 | EXPORT_SYMBOL_NS_GPL(cs_amp_get_efi_calibration_data, SND_SOC_CS_AMP_LIB); |
269 | |
270 | static const struct cs_amp_test_hooks cs_amp_test_hook_ptrs = { |
271 | .get_efi_variable = cs_amp_get_efi_variable, |
272 | .write_cal_coeff = cs_amp_write_cal_coeff, |
273 | }; |
274 | |
275 | const struct cs_amp_test_hooks * const cs_amp_test_hooks = |
276 | PTR_IF(IS_ENABLED(CONFIG_SND_SOC_CS_AMP_LIB_TEST), &cs_amp_test_hook_ptrs); |
277 | EXPORT_SYMBOL_NS_GPL(cs_amp_test_hooks, SND_SOC_CS_AMP_LIB); |
278 | |
279 | MODULE_DESCRIPTION("Cirrus Logic amplifier library" ); |
280 | MODULE_AUTHOR("Richard Fitzgerald <rf@opensource.cirrus.com>" ); |
281 | MODULE_LICENSE("GPL" ); |
282 | MODULE_IMPORT_NS(FW_CS_DSP); |
283 | |