1 | // SPDX-License-Identifier: GPL-2.0-only |
2 | /* |
3 | * wm8728.c -- WM8728 ALSA SoC Audio driver |
4 | * |
5 | * Copyright 2008 Wolfson Microelectronics plc |
6 | * |
7 | * Author: Mark Brown <broonie@opensource.wolfsonmicro.com> |
8 | */ |
9 | |
10 | #include <linux/mod_devicetable.h> |
11 | #include <linux/module.h> |
12 | #include <linux/moduleparam.h> |
13 | #include <linux/init.h> |
14 | #include <linux/delay.h> |
15 | #include <linux/pm.h> |
16 | #include <linux/i2c.h> |
17 | #include <linux/platform_device.h> |
18 | #include <linux/regmap.h> |
19 | #include <linux/spi/spi.h> |
20 | #include <linux/slab.h> |
21 | #include <sound/core.h> |
22 | #include <sound/pcm.h> |
23 | #include <sound/pcm_params.h> |
24 | #include <sound/soc.h> |
25 | #include <sound/initval.h> |
26 | #include <sound/tlv.h> |
27 | |
28 | #include "wm8728.h" |
29 | |
30 | /* |
31 | * We can't read the WM8728 register space so we cache them instead. |
32 | * Note that the defaults here aren't the physical defaults, we latch |
33 | * the volume update bits, mute the output and enable infinite zero |
34 | * detect. |
35 | */ |
36 | static const struct reg_default wm8728_reg_defaults[] = { |
37 | { 0, 0x1ff }, |
38 | { 1, 0x1ff }, |
39 | { 2, 0x001 }, |
40 | { 3, 0x100 }, |
41 | }; |
42 | |
43 | /* codec private data */ |
44 | struct wm8728_priv { |
45 | struct regmap *regmap; |
46 | }; |
47 | |
48 | static const DECLARE_TLV_DB_SCALE(wm8728_tlv, -12750, 50, 1); |
49 | |
50 | static const struct snd_kcontrol_new wm8728_snd_controls[] = { |
51 | |
52 | SOC_DOUBLE_R_TLV("Digital Playback Volume" , WM8728_DACLVOL, WM8728_DACRVOL, |
53 | 0, 255, 0, wm8728_tlv), |
54 | |
55 | SOC_SINGLE("Deemphasis" , WM8728_DACCTL, 1, 1, 0), |
56 | }; |
57 | |
58 | /* |
59 | * DAPM controls. |
60 | */ |
61 | static const struct snd_soc_dapm_widget wm8728_dapm_widgets[] = { |
62 | SND_SOC_DAPM_DAC("DAC" , "HiFi Playback" , SND_SOC_NOPM, 0, 0), |
63 | SND_SOC_DAPM_OUTPUT("VOUTL" ), |
64 | SND_SOC_DAPM_OUTPUT("VOUTR" ), |
65 | }; |
66 | |
67 | static const struct snd_soc_dapm_route wm8728_intercon[] = { |
68 | {"VOUTL" , NULL, "DAC" }, |
69 | {"VOUTR" , NULL, "DAC" }, |
70 | }; |
71 | |
72 | static int wm8728_mute(struct snd_soc_dai *dai, int mute, int direction) |
73 | { |
74 | struct snd_soc_component *component = dai->component; |
75 | u16 mute_reg = snd_soc_component_read(component, WM8728_DACCTL); |
76 | |
77 | if (mute) |
78 | snd_soc_component_write(component, WM8728_DACCTL, val: mute_reg | 1); |
79 | else |
80 | snd_soc_component_write(component, WM8728_DACCTL, val: mute_reg & ~1); |
81 | |
82 | return 0; |
83 | } |
84 | |
85 | static int wm8728_hw_params(struct snd_pcm_substream *substream, |
86 | struct snd_pcm_hw_params *params, |
87 | struct snd_soc_dai *dai) |
88 | { |
89 | struct snd_soc_component *component = dai->component; |
90 | u16 dac = snd_soc_component_read(component, WM8728_DACCTL); |
91 | |
92 | dac &= ~0x18; |
93 | |
94 | switch (params_width(p: params)) { |
95 | case 16: |
96 | break; |
97 | case 20: |
98 | dac |= 0x10; |
99 | break; |
100 | case 24: |
101 | dac |= 0x08; |
102 | break; |
103 | default: |
104 | return -EINVAL; |
105 | } |
106 | |
107 | snd_soc_component_write(component, WM8728_DACCTL, val: dac); |
108 | |
109 | return 0; |
110 | } |
111 | |
112 | static int wm8728_set_dai_fmt(struct snd_soc_dai *codec_dai, |
113 | unsigned int fmt) |
114 | { |
115 | struct snd_soc_component *component = codec_dai->component; |
116 | u16 iface = snd_soc_component_read(component, WM8728_IFCTL); |
117 | |
118 | /* Currently only I2S is supported by the driver, though the |
119 | * hardware is more flexible. |
120 | */ |
121 | switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) { |
122 | case SND_SOC_DAIFMT_I2S: |
123 | iface |= 1; |
124 | break; |
125 | default: |
126 | return -EINVAL; |
127 | } |
128 | |
129 | /* The hardware only support full slave mode */ |
130 | switch (fmt & SND_SOC_DAIFMT_MASTER_MASK) { |
131 | case SND_SOC_DAIFMT_CBS_CFS: |
132 | break; |
133 | default: |
134 | return -EINVAL; |
135 | } |
136 | |
137 | switch (fmt & SND_SOC_DAIFMT_INV_MASK) { |
138 | case SND_SOC_DAIFMT_NB_NF: |
139 | iface &= ~0x22; |
140 | break; |
141 | case SND_SOC_DAIFMT_IB_NF: |
142 | iface |= 0x20; |
143 | iface &= ~0x02; |
144 | break; |
145 | case SND_SOC_DAIFMT_NB_IF: |
146 | iface |= 0x02; |
147 | iface &= ~0x20; |
148 | break; |
149 | case SND_SOC_DAIFMT_IB_IF: |
150 | iface |= 0x22; |
151 | break; |
152 | default: |
153 | return -EINVAL; |
154 | } |
155 | |
156 | snd_soc_component_write(component, WM8728_IFCTL, val: iface); |
157 | return 0; |
158 | } |
159 | |
160 | static int wm8728_set_bias_level(struct snd_soc_component *component, |
161 | enum snd_soc_bias_level level) |
162 | { |
163 | struct wm8728_priv *wm8728 = snd_soc_component_get_drvdata(c: component); |
164 | u16 reg; |
165 | |
166 | switch (level) { |
167 | case SND_SOC_BIAS_ON: |
168 | case SND_SOC_BIAS_PREPARE: |
169 | case SND_SOC_BIAS_STANDBY: |
170 | if (snd_soc_component_get_bias_level(component) == SND_SOC_BIAS_OFF) { |
171 | /* Power everything up... */ |
172 | reg = snd_soc_component_read(component, WM8728_DACCTL); |
173 | snd_soc_component_write(component, WM8728_DACCTL, val: reg & ~0x4); |
174 | |
175 | /* ..then sync in the register cache. */ |
176 | regcache_sync(map: wm8728->regmap); |
177 | } |
178 | break; |
179 | |
180 | case SND_SOC_BIAS_OFF: |
181 | reg = snd_soc_component_read(component, WM8728_DACCTL); |
182 | snd_soc_component_write(component, WM8728_DACCTL, val: reg | 0x4); |
183 | break; |
184 | } |
185 | return 0; |
186 | } |
187 | |
188 | #define WM8728_RATES (SNDRV_PCM_RATE_8000_192000) |
189 | |
190 | #define WM8728_FORMATS (SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S20_3LE |\ |
191 | SNDRV_PCM_FMTBIT_S24_LE) |
192 | |
193 | static const struct snd_soc_dai_ops wm8728_dai_ops = { |
194 | .hw_params = wm8728_hw_params, |
195 | .mute_stream = wm8728_mute, |
196 | .set_fmt = wm8728_set_dai_fmt, |
197 | .no_capture_mute = 1, |
198 | }; |
199 | |
200 | static struct snd_soc_dai_driver wm8728_dai = { |
201 | .name = "wm8728-hifi" , |
202 | .playback = { |
203 | .stream_name = "Playback" , |
204 | .channels_min = 2, |
205 | .channels_max = 2, |
206 | .rates = WM8728_RATES, |
207 | .formats = WM8728_FORMATS, |
208 | }, |
209 | .ops = &wm8728_dai_ops, |
210 | }; |
211 | |
212 | static const struct snd_soc_component_driver soc_component_dev_wm8728 = { |
213 | .set_bias_level = wm8728_set_bias_level, |
214 | .controls = wm8728_snd_controls, |
215 | .num_controls = ARRAY_SIZE(wm8728_snd_controls), |
216 | .dapm_widgets = wm8728_dapm_widgets, |
217 | .num_dapm_widgets = ARRAY_SIZE(wm8728_dapm_widgets), |
218 | .dapm_routes = wm8728_intercon, |
219 | .num_dapm_routes = ARRAY_SIZE(wm8728_intercon), |
220 | .suspend_bias_off = 1, |
221 | .idle_bias_on = 1, |
222 | .use_pmdown_time = 1, |
223 | .endianness = 1, |
224 | }; |
225 | |
226 | static const struct of_device_id wm8728_of_match[] = { |
227 | { .compatible = "wlf,wm8728" , }, |
228 | { } |
229 | }; |
230 | MODULE_DEVICE_TABLE(of, wm8728_of_match); |
231 | |
232 | static const struct regmap_config wm8728_regmap = { |
233 | .reg_bits = 7, |
234 | .val_bits = 9, |
235 | .max_register = WM8728_IFCTL, |
236 | |
237 | .reg_defaults = wm8728_reg_defaults, |
238 | .num_reg_defaults = ARRAY_SIZE(wm8728_reg_defaults), |
239 | .cache_type = REGCACHE_MAPLE, |
240 | }; |
241 | |
242 | #if defined(CONFIG_SPI_MASTER) |
243 | static int wm8728_spi_probe(struct spi_device *spi) |
244 | { |
245 | struct wm8728_priv *wm8728; |
246 | int ret; |
247 | |
248 | wm8728 = devm_kzalloc(dev: &spi->dev, size: sizeof(struct wm8728_priv), |
249 | GFP_KERNEL); |
250 | if (wm8728 == NULL) |
251 | return -ENOMEM; |
252 | |
253 | wm8728->regmap = devm_regmap_init_spi(spi, &wm8728_regmap); |
254 | if (IS_ERR(ptr: wm8728->regmap)) |
255 | return PTR_ERR(ptr: wm8728->regmap); |
256 | |
257 | spi_set_drvdata(spi, data: wm8728); |
258 | |
259 | ret = devm_snd_soc_register_component(dev: &spi->dev, |
260 | component_driver: &soc_component_dev_wm8728, dai_drv: &wm8728_dai, num_dai: 1); |
261 | |
262 | return ret; |
263 | } |
264 | |
265 | static struct spi_driver wm8728_spi_driver = { |
266 | .driver = { |
267 | .name = "wm8728" , |
268 | .of_match_table = wm8728_of_match, |
269 | }, |
270 | .probe = wm8728_spi_probe, |
271 | }; |
272 | #endif /* CONFIG_SPI_MASTER */ |
273 | |
274 | #if IS_ENABLED(CONFIG_I2C) |
275 | static int wm8728_i2c_probe(struct i2c_client *i2c) |
276 | { |
277 | struct wm8728_priv *wm8728; |
278 | int ret; |
279 | |
280 | wm8728 = devm_kzalloc(dev: &i2c->dev, size: sizeof(struct wm8728_priv), |
281 | GFP_KERNEL); |
282 | if (wm8728 == NULL) |
283 | return -ENOMEM; |
284 | |
285 | wm8728->regmap = devm_regmap_init_i2c(i2c, &wm8728_regmap); |
286 | if (IS_ERR(ptr: wm8728->regmap)) |
287 | return PTR_ERR(ptr: wm8728->regmap); |
288 | |
289 | i2c_set_clientdata(client: i2c, data: wm8728); |
290 | |
291 | ret = devm_snd_soc_register_component(dev: &i2c->dev, |
292 | component_driver: &soc_component_dev_wm8728, dai_drv: &wm8728_dai, num_dai: 1); |
293 | |
294 | return ret; |
295 | } |
296 | |
297 | static const struct i2c_device_id wm8728_i2c_id[] = { |
298 | { "wm8728" , 0 }, |
299 | { } |
300 | }; |
301 | MODULE_DEVICE_TABLE(i2c, wm8728_i2c_id); |
302 | |
303 | static struct i2c_driver wm8728_i2c_driver = { |
304 | .driver = { |
305 | .name = "wm8728" , |
306 | .of_match_table = wm8728_of_match, |
307 | }, |
308 | .probe = wm8728_i2c_probe, |
309 | .id_table = wm8728_i2c_id, |
310 | }; |
311 | #endif |
312 | |
313 | static int __init wm8728_modinit(void) |
314 | { |
315 | int ret = 0; |
316 | #if IS_ENABLED(CONFIG_I2C) |
317 | ret = i2c_add_driver(&wm8728_i2c_driver); |
318 | if (ret != 0) { |
319 | printk(KERN_ERR "Failed to register wm8728 I2C driver: %d\n" , |
320 | ret); |
321 | } |
322 | #endif |
323 | #if defined(CONFIG_SPI_MASTER) |
324 | ret = spi_register_driver(&wm8728_spi_driver); |
325 | if (ret != 0) { |
326 | printk(KERN_ERR "Failed to register wm8728 SPI driver: %d\n" , |
327 | ret); |
328 | } |
329 | #endif |
330 | return ret; |
331 | } |
332 | module_init(wm8728_modinit); |
333 | |
334 | static void __exit wm8728_exit(void) |
335 | { |
336 | #if IS_ENABLED(CONFIG_I2C) |
337 | i2c_del_driver(driver: &wm8728_i2c_driver); |
338 | #endif |
339 | #if defined(CONFIG_SPI_MASTER) |
340 | spi_unregister_driver(sdrv: &wm8728_spi_driver); |
341 | #endif |
342 | } |
343 | module_exit(wm8728_exit); |
344 | |
345 | MODULE_DESCRIPTION("ASoC WM8728 driver" ); |
346 | MODULE_AUTHOR("Mark Brown <broonie@opensource.wolfsonmicro.com>" ); |
347 | MODULE_LICENSE("GPL" ); |
348 | |