1 | // SPDX-License-Identifier: GPL-2.0 |
2 | // |
3 | // Freescale P1022DS ALSA SoC Machine driver |
4 | // |
5 | // Author: Timur Tabi <timur@freescale.com> |
6 | // |
7 | // Copyright 2010 Freescale Semiconductor, Inc. |
8 | |
9 | #include <linux/module.h> |
10 | #include <linux/fsl/guts.h> |
11 | #include <linux/interrupt.h> |
12 | #include <linux/of.h> |
13 | #include <linux/of_address.h> |
14 | #include <linux/slab.h> |
15 | #include <sound/soc.h> |
16 | |
17 | #include "fsl_dma.h" |
18 | #include "fsl_ssi.h" |
19 | #include "fsl_utils.h" |
20 | |
21 | /* P1022-specific PMUXCR and DMUXCR bit definitions */ |
22 | |
23 | #define CCSR_GUTS_PMUXCR_UART0_I2C1_MASK 0x0001c000 |
24 | #define CCSR_GUTS_PMUXCR_UART0_I2C1_UART0_SSI 0x00010000 |
25 | #define CCSR_GUTS_PMUXCR_UART0_I2C1_SSI 0x00018000 |
26 | |
27 | #define CCSR_GUTS_PMUXCR_SSI_DMA_TDM_MASK 0x00000c00 |
28 | #define CCSR_GUTS_PMUXCR_SSI_DMA_TDM_SSI 0x00000000 |
29 | |
30 | #define CCSR_GUTS_DMUXCR_PAD 1 /* DMA controller/channel set to pad */ |
31 | #define CCSR_GUTS_DMUXCR_SSI 2 /* DMA controller/channel set to SSI */ |
32 | |
33 | /* |
34 | * Set the DMACR register in the GUTS |
35 | * |
36 | * The DMACR register determines the source of initiated transfers for each |
37 | * channel on each DMA controller. Rather than have a bunch of repetitive |
38 | * macros for the bit patterns, we just have a function that calculates |
39 | * them. |
40 | * |
41 | * guts: Pointer to GUTS structure |
42 | * co: The DMA controller (0 or 1) |
43 | * ch: The channel on the DMA controller (0, 1, 2, or 3) |
44 | * device: The device to set as the target (CCSR_GUTS_DMUXCR_xxx) |
45 | */ |
46 | static inline void guts_set_dmuxcr(struct ccsr_guts __iomem *guts, |
47 | unsigned int co, unsigned int ch, unsigned int device) |
48 | { |
49 | unsigned int shift = 16 + (8 * (1 - co) + 2 * (3 - ch)); |
50 | |
51 | clrsetbits_be32(&guts->dmuxcr, 3 << shift, device << shift); |
52 | } |
53 | |
54 | /* There's only one global utilities register */ |
55 | static phys_addr_t guts_phys; |
56 | |
57 | /** |
58 | * machine_data: machine-specific ASoC device data |
59 | * |
60 | * This structure contains data for a single sound platform device on an |
61 | * P1022 DS. Some of the data is taken from the device tree. |
62 | */ |
63 | struct machine_data { |
64 | struct snd_soc_dai_link dai[2]; |
65 | struct snd_soc_card card; |
66 | unsigned int dai_format; |
67 | unsigned int codec_clk_direction; |
68 | unsigned int cpu_clk_direction; |
69 | unsigned int clk_frequency; |
70 | unsigned int ssi_id; /* 0 = SSI1, 1 = SSI2, etc */ |
71 | unsigned int dma_id[2]; /* 0 = DMA1, 1 = DMA2, etc */ |
72 | unsigned int dma_channel_id[2]; /* 0 = ch 0, 1 = ch 1, etc*/ |
73 | char platform_name[2][DAI_NAME_SIZE]; /* One for each DMA channel */ |
74 | }; |
75 | |
76 | /** |
77 | * p1022_ds_machine_probe: initialize the board |
78 | * |
79 | * This function is used to initialize the board-specific hardware. |
80 | * |
81 | * Here we program the DMACR and PMUXCR registers. |
82 | */ |
83 | static int p1022_ds_machine_probe(struct snd_soc_card *card) |
84 | { |
85 | struct machine_data *mdata = |
86 | container_of(card, struct machine_data, card); |
87 | struct ccsr_guts __iomem *guts; |
88 | |
89 | guts = ioremap(offset: guts_phys, size: sizeof(struct ccsr_guts)); |
90 | if (!guts) { |
91 | dev_err(card->dev, "could not map global utilities\n" ); |
92 | return -ENOMEM; |
93 | } |
94 | |
95 | /* Enable SSI Tx signal */ |
96 | clrsetbits_be32(&guts->pmuxcr, CCSR_GUTS_PMUXCR_UART0_I2C1_MASK, |
97 | CCSR_GUTS_PMUXCR_UART0_I2C1_UART0_SSI); |
98 | |
99 | /* Enable SSI Rx signal */ |
100 | clrsetbits_be32(&guts->pmuxcr, CCSR_GUTS_PMUXCR_SSI_DMA_TDM_MASK, |
101 | CCSR_GUTS_PMUXCR_SSI_DMA_TDM_SSI); |
102 | |
103 | /* Enable DMA Channel for SSI */ |
104 | guts_set_dmuxcr(guts, co: mdata->dma_id[0], ch: mdata->dma_channel_id[0], |
105 | CCSR_GUTS_DMUXCR_SSI); |
106 | |
107 | guts_set_dmuxcr(guts, co: mdata->dma_id[1], ch: mdata->dma_channel_id[1], |
108 | CCSR_GUTS_DMUXCR_SSI); |
109 | |
110 | iounmap(addr: guts); |
111 | |
112 | return 0; |
113 | } |
114 | |
115 | /** |
116 | * p1022_ds_startup: program the board with various hardware parameters |
117 | * |
118 | * This function takes board-specific information, like clock frequencies |
119 | * and serial data formats, and passes that information to the codec and |
120 | * transport drivers. |
121 | */ |
122 | static int p1022_ds_startup(struct snd_pcm_substream *substream) |
123 | { |
124 | struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream); |
125 | struct machine_data *mdata = |
126 | container_of(rtd->card, struct machine_data, card); |
127 | struct device *dev = rtd->card->dev; |
128 | int ret = 0; |
129 | |
130 | /* Tell the codec driver what the serial protocol is. */ |
131 | ret = snd_soc_dai_set_fmt(snd_soc_rtd_to_codec(rtd, 0), fmt: mdata->dai_format); |
132 | if (ret < 0) { |
133 | dev_err(dev, "could not set codec driver audio format\n" ); |
134 | return ret; |
135 | } |
136 | |
137 | /* |
138 | * Tell the codec driver what the MCLK frequency is, and whether it's |
139 | * a slave or master. |
140 | */ |
141 | ret = snd_soc_dai_set_sysclk(snd_soc_rtd_to_codec(rtd, 0), clk_id: 0, freq: mdata->clk_frequency, |
142 | dir: mdata->codec_clk_direction); |
143 | if (ret < 0) { |
144 | dev_err(dev, "could not set codec driver clock params\n" ); |
145 | return ret; |
146 | } |
147 | |
148 | return 0; |
149 | } |
150 | |
151 | /** |
152 | * p1022_ds_machine_remove: Remove the sound device |
153 | * |
154 | * This function is called to remove the sound device for one SSI. We |
155 | * de-program the DMACR and PMUXCR register. |
156 | */ |
157 | static int p1022_ds_machine_remove(struct snd_soc_card *card) |
158 | { |
159 | struct machine_data *mdata = |
160 | container_of(card, struct machine_data, card); |
161 | struct ccsr_guts __iomem *guts; |
162 | |
163 | guts = ioremap(offset: guts_phys, size: sizeof(struct ccsr_guts)); |
164 | if (!guts) { |
165 | dev_err(card->dev, "could not map global utilities\n" ); |
166 | return -ENOMEM; |
167 | } |
168 | |
169 | /* Restore the signal routing */ |
170 | clrbits32(&guts->pmuxcr, CCSR_GUTS_PMUXCR_UART0_I2C1_MASK); |
171 | clrbits32(&guts->pmuxcr, CCSR_GUTS_PMUXCR_SSI_DMA_TDM_MASK); |
172 | guts_set_dmuxcr(guts, co: mdata->dma_id[0], ch: mdata->dma_channel_id[0], device: 0); |
173 | guts_set_dmuxcr(guts, co: mdata->dma_id[1], ch: mdata->dma_channel_id[1], device: 0); |
174 | |
175 | iounmap(addr: guts); |
176 | |
177 | return 0; |
178 | } |
179 | |
180 | /** |
181 | * p1022_ds_ops: ASoC machine driver operations |
182 | */ |
183 | static const struct snd_soc_ops p1022_ds_ops = { |
184 | .startup = p1022_ds_startup, |
185 | }; |
186 | |
187 | /** |
188 | * p1022_ds_probe: platform probe function for the machine driver |
189 | * |
190 | * Although this is a machine driver, the SSI node is the "master" node with |
191 | * respect to audio hardware connections. Therefore, we create a new ASoC |
192 | * device for each new SSI node that has a codec attached. |
193 | */ |
194 | static int p1022_ds_probe(struct platform_device *pdev) |
195 | { |
196 | struct device *dev = pdev->dev.parent; |
197 | /* ssi_pdev is the platform device for the SSI node that probed us */ |
198 | struct platform_device *ssi_pdev = to_platform_device(dev); |
199 | struct device_node *np = ssi_pdev->dev.of_node; |
200 | struct device_node *codec_np = NULL; |
201 | struct machine_data *mdata; |
202 | struct snd_soc_dai_link_component *comp; |
203 | int ret; |
204 | const char *sprop; |
205 | const u32 *iprop; |
206 | |
207 | /* Find the codec node for this SSI. */ |
208 | codec_np = of_parse_phandle(np, phandle_name: "codec-handle" , index: 0); |
209 | if (!codec_np) { |
210 | dev_err(dev, "could not find codec node\n" ); |
211 | return -EINVAL; |
212 | } |
213 | |
214 | mdata = kzalloc(size: sizeof(struct machine_data), GFP_KERNEL); |
215 | if (!mdata) { |
216 | ret = -ENOMEM; |
217 | goto error_put; |
218 | } |
219 | |
220 | comp = devm_kzalloc(dev: &pdev->dev, size: 6 * sizeof(*comp), GFP_KERNEL); |
221 | if (!comp) { |
222 | ret = -ENOMEM; |
223 | goto error_put; |
224 | } |
225 | |
226 | mdata->dai[0].cpus = &comp[0]; |
227 | mdata->dai[0].codecs = &comp[1]; |
228 | mdata->dai[0].platforms = &comp[2]; |
229 | |
230 | mdata->dai[0].num_cpus = 1; |
231 | mdata->dai[0].num_codecs = 1; |
232 | mdata->dai[0].num_platforms = 1; |
233 | |
234 | mdata->dai[1].cpus = &comp[3]; |
235 | mdata->dai[1].codecs = &comp[4]; |
236 | mdata->dai[1].platforms = &comp[5]; |
237 | |
238 | mdata->dai[1].num_cpus = 1; |
239 | mdata->dai[1].num_codecs = 1; |
240 | mdata->dai[1].num_platforms = 1; |
241 | |
242 | |
243 | mdata->dai[0].cpus->dai_name = dev_name(dev: &ssi_pdev->dev); |
244 | mdata->dai[0].ops = &p1022_ds_ops; |
245 | |
246 | /* ASoC core can match codec with device node */ |
247 | mdata->dai[0].codecs->of_node = codec_np; |
248 | |
249 | /* We register two DAIs per SSI, one for playback and the other for |
250 | * capture. We support codecs that have separate DAIs for both playback |
251 | * and capture. |
252 | */ |
253 | memcpy(&mdata->dai[1], &mdata->dai[0], sizeof(struct snd_soc_dai_link)); |
254 | |
255 | /* The DAI names from the codec (snd_soc_dai_driver.name) */ |
256 | mdata->dai[0].codecs->dai_name = "wm8776-hifi-playback" ; |
257 | mdata->dai[1].codecs->dai_name = "wm8776-hifi-capture" ; |
258 | |
259 | /* Get the device ID */ |
260 | iprop = of_get_property(node: np, name: "cell-index" , NULL); |
261 | if (!iprop) { |
262 | dev_err(&pdev->dev, "cell-index property not found\n" ); |
263 | ret = -EINVAL; |
264 | goto error; |
265 | } |
266 | mdata->ssi_id = be32_to_cpup(p: iprop); |
267 | |
268 | /* Get the serial format and clock direction. */ |
269 | sprop = of_get_property(node: np, name: "fsl,mode" , NULL); |
270 | if (!sprop) { |
271 | dev_err(&pdev->dev, "fsl,mode property not found\n" ); |
272 | ret = -EINVAL; |
273 | goto error; |
274 | } |
275 | |
276 | if (strcasecmp(s1: sprop, s2: "i2s-slave" ) == 0) { |
277 | mdata->dai_format = SND_SOC_DAIFMT_NB_NF | |
278 | SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_CBP_CFP; |
279 | mdata->codec_clk_direction = SND_SOC_CLOCK_OUT; |
280 | mdata->cpu_clk_direction = SND_SOC_CLOCK_IN; |
281 | |
282 | /* In i2s-slave mode, the codec has its own clock source, so we |
283 | * need to get the frequency from the device tree and pass it to |
284 | * the codec driver. |
285 | */ |
286 | iprop = of_get_property(node: codec_np, name: "clock-frequency" , NULL); |
287 | if (!iprop || !*iprop) { |
288 | dev_err(&pdev->dev, "codec bus-frequency " |
289 | "property is missing or invalid\n" ); |
290 | ret = -EINVAL; |
291 | goto error; |
292 | } |
293 | mdata->clk_frequency = be32_to_cpup(p: iprop); |
294 | } else if (strcasecmp(s1: sprop, s2: "i2s-master" ) == 0) { |
295 | mdata->dai_format = SND_SOC_DAIFMT_NB_NF | |
296 | SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_CBC_CFC; |
297 | mdata->codec_clk_direction = SND_SOC_CLOCK_IN; |
298 | mdata->cpu_clk_direction = SND_SOC_CLOCK_OUT; |
299 | } else if (strcasecmp(s1: sprop, s2: "lj-slave" ) == 0) { |
300 | mdata->dai_format = SND_SOC_DAIFMT_NB_NF | |
301 | SND_SOC_DAIFMT_LEFT_J | SND_SOC_DAIFMT_CBP_CFP; |
302 | mdata->codec_clk_direction = SND_SOC_CLOCK_OUT; |
303 | mdata->cpu_clk_direction = SND_SOC_CLOCK_IN; |
304 | } else if (strcasecmp(s1: sprop, s2: "lj-master" ) == 0) { |
305 | mdata->dai_format = SND_SOC_DAIFMT_NB_NF | |
306 | SND_SOC_DAIFMT_LEFT_J | SND_SOC_DAIFMT_CBC_CFC; |
307 | mdata->codec_clk_direction = SND_SOC_CLOCK_IN; |
308 | mdata->cpu_clk_direction = SND_SOC_CLOCK_OUT; |
309 | } else if (strcasecmp(s1: sprop, s2: "rj-slave" ) == 0) { |
310 | mdata->dai_format = SND_SOC_DAIFMT_NB_NF | |
311 | SND_SOC_DAIFMT_RIGHT_J | SND_SOC_DAIFMT_CBP_CFP; |
312 | mdata->codec_clk_direction = SND_SOC_CLOCK_OUT; |
313 | mdata->cpu_clk_direction = SND_SOC_CLOCK_IN; |
314 | } else if (strcasecmp(s1: sprop, s2: "rj-master" ) == 0) { |
315 | mdata->dai_format = SND_SOC_DAIFMT_NB_NF | |
316 | SND_SOC_DAIFMT_RIGHT_J | SND_SOC_DAIFMT_CBC_CFC; |
317 | mdata->codec_clk_direction = SND_SOC_CLOCK_IN; |
318 | mdata->cpu_clk_direction = SND_SOC_CLOCK_OUT; |
319 | } else if (strcasecmp(s1: sprop, s2: "ac97-slave" ) == 0) { |
320 | mdata->dai_format = SND_SOC_DAIFMT_NB_NF | |
321 | SND_SOC_DAIFMT_AC97 | SND_SOC_DAIFMT_CBP_CFP; |
322 | mdata->codec_clk_direction = SND_SOC_CLOCK_OUT; |
323 | mdata->cpu_clk_direction = SND_SOC_CLOCK_IN; |
324 | } else if (strcasecmp(s1: sprop, s2: "ac97-master" ) == 0) { |
325 | mdata->dai_format = SND_SOC_DAIFMT_NB_NF | |
326 | SND_SOC_DAIFMT_AC97 | SND_SOC_DAIFMT_CBC_CFC; |
327 | mdata->codec_clk_direction = SND_SOC_CLOCK_IN; |
328 | mdata->cpu_clk_direction = SND_SOC_CLOCK_OUT; |
329 | } else { |
330 | dev_err(&pdev->dev, |
331 | "unrecognized fsl,mode property '%s'\n" , sprop); |
332 | ret = -EINVAL; |
333 | goto error; |
334 | } |
335 | |
336 | if (!mdata->clk_frequency) { |
337 | dev_err(&pdev->dev, "unknown clock frequency\n" ); |
338 | ret = -EINVAL; |
339 | goto error; |
340 | } |
341 | |
342 | /* Find the playback DMA channel to use. */ |
343 | mdata->dai[0].platforms->name = mdata->platform_name[0]; |
344 | ret = fsl_asoc_get_dma_channel(ssi_np: np, name: "fsl,playback-dma" , dai: &mdata->dai[0], |
345 | dma_channel_id: &mdata->dma_channel_id[0], |
346 | dma_id: &mdata->dma_id[0]); |
347 | if (ret) { |
348 | dev_err(&pdev->dev, "missing/invalid playback DMA phandle\n" ); |
349 | goto error; |
350 | } |
351 | |
352 | /* Find the capture DMA channel to use. */ |
353 | mdata->dai[1].platforms->name = mdata->platform_name[1]; |
354 | ret = fsl_asoc_get_dma_channel(ssi_np: np, name: "fsl,capture-dma" , dai: &mdata->dai[1], |
355 | dma_channel_id: &mdata->dma_channel_id[1], |
356 | dma_id: &mdata->dma_id[1]); |
357 | if (ret) { |
358 | dev_err(&pdev->dev, "missing/invalid capture DMA phandle\n" ); |
359 | goto error; |
360 | } |
361 | |
362 | /* Initialize our DAI data structure. */ |
363 | mdata->dai[0].stream_name = "playback" ; |
364 | mdata->dai[1].stream_name = "capture" ; |
365 | mdata->dai[0].name = mdata->dai[0].stream_name; |
366 | mdata->dai[1].name = mdata->dai[1].stream_name; |
367 | |
368 | mdata->card.probe = p1022_ds_machine_probe; |
369 | mdata->card.remove = p1022_ds_machine_remove; |
370 | mdata->card.name = pdev->name; /* The platform driver name */ |
371 | mdata->card.owner = THIS_MODULE; |
372 | mdata->card.dev = &pdev->dev; |
373 | mdata->card.num_links = 2; |
374 | mdata->card.dai_link = mdata->dai; |
375 | |
376 | /* Register with ASoC */ |
377 | ret = snd_soc_register_card(card: &mdata->card); |
378 | if (ret) { |
379 | dev_err(&pdev->dev, "could not register card\n" ); |
380 | goto error; |
381 | } |
382 | |
383 | of_node_put(node: codec_np); |
384 | |
385 | return 0; |
386 | |
387 | error: |
388 | kfree(objp: mdata); |
389 | error_put: |
390 | of_node_put(node: codec_np); |
391 | return ret; |
392 | } |
393 | |
394 | /** |
395 | * p1022_ds_remove: remove the platform device |
396 | * |
397 | * This function is called when the platform device is removed. |
398 | */ |
399 | static void p1022_ds_remove(struct platform_device *pdev) |
400 | { |
401 | struct snd_soc_card *card = platform_get_drvdata(pdev); |
402 | struct machine_data *mdata = |
403 | container_of(card, struct machine_data, card); |
404 | |
405 | snd_soc_unregister_card(card); |
406 | kfree(objp: mdata); |
407 | } |
408 | |
409 | static struct platform_driver p1022_ds_driver = { |
410 | .probe = p1022_ds_probe, |
411 | .remove_new = p1022_ds_remove, |
412 | .driver = { |
413 | /* |
414 | * The name must match 'compatible' property in the device tree, |
415 | * in lowercase letters. |
416 | */ |
417 | .name = "snd-soc-p1022ds" , |
418 | }, |
419 | }; |
420 | |
421 | /** |
422 | * p1022_ds_init: machine driver initialization. |
423 | * |
424 | * This function is called when this module is loaded. |
425 | */ |
426 | static int __init p1022_ds_init(void) |
427 | { |
428 | struct device_node *guts_np; |
429 | struct resource res; |
430 | |
431 | /* Get the physical address of the global utilities registers */ |
432 | guts_np = of_find_compatible_node(NULL, NULL, compat: "fsl,p1022-guts" ); |
433 | if (of_address_to_resource(dev: guts_np, index: 0, r: &res)) { |
434 | pr_err("snd-soc-p1022ds: missing/invalid global utils node\n" ); |
435 | of_node_put(node: guts_np); |
436 | return -EINVAL; |
437 | } |
438 | guts_phys = res.start; |
439 | of_node_put(node: guts_np); |
440 | |
441 | return platform_driver_register(&p1022_ds_driver); |
442 | } |
443 | |
444 | /** |
445 | * p1022_ds_exit: machine driver exit |
446 | * |
447 | * This function is called when this driver is unloaded. |
448 | */ |
449 | static void __exit p1022_ds_exit(void) |
450 | { |
451 | platform_driver_unregister(&p1022_ds_driver); |
452 | } |
453 | |
454 | module_init(p1022_ds_init); |
455 | module_exit(p1022_ds_exit); |
456 | |
457 | MODULE_AUTHOR("Timur Tabi <timur@freescale.com>" ); |
458 | MODULE_DESCRIPTION("Freescale P1022 DS ALSA SoC machine driver" ); |
459 | MODULE_LICENSE("GPL v2" ); |
460 | |