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