1 | // SPDX-License-Identifier: GPL-2.0 |
2 | // |
3 | // Xilinx ASoC I2S audio support |
4 | // |
5 | // Copyright (C) 2018 Xilinx, Inc. |
6 | // |
7 | // Author: Praveen Vuppala <praveenv@xilinx.com> |
8 | // Author: Maruthi Srinivas Bayyavarapu <maruthis@xilinx.com> |
9 | |
10 | #include <linux/io.h> |
11 | #include <linux/module.h> |
12 | #include <linux/of.h> |
13 | #include <linux/of_platform.h> |
14 | #include <linux/platform_device.h> |
15 | #include <sound/pcm_params.h> |
16 | #include <sound/soc.h> |
17 | |
18 | #define DRV_NAME "xlnx_i2s" |
19 | |
20 | #define I2S_CORE_CTRL_OFFSET 0x08 |
21 | #define I2S_CORE_CTRL_32BIT_LRCLK BIT(3) |
22 | #define I2S_CORE_CTRL_ENABLE BIT(0) |
23 | #define I2S_I2STIM_OFFSET 0x20 |
24 | #define I2S_CH0_OFFSET 0x30 |
25 | #define I2S_I2STIM_VALID_MASK GENMASK(7, 0) |
26 | |
27 | struct xlnx_i2s_drv_data { |
28 | struct snd_soc_dai_driver dai_drv; |
29 | void __iomem *base; |
30 | unsigned int sysclk; |
31 | u32 data_width; |
32 | u32 channels; |
33 | bool is_32bit_lrclk; |
34 | struct snd_ratnum ratnum; |
35 | struct snd_pcm_hw_constraint_ratnums rate_constraints; |
36 | }; |
37 | |
38 | static int xlnx_i2s_set_sclkout_div(struct snd_soc_dai *cpu_dai, |
39 | int div_id, int div) |
40 | { |
41 | struct xlnx_i2s_drv_data *drv_data = snd_soc_dai_get_drvdata(dai: cpu_dai); |
42 | |
43 | if (!div || (div & ~I2S_I2STIM_VALID_MASK)) |
44 | return -EINVAL; |
45 | |
46 | drv_data->sysclk = 0; |
47 | |
48 | writel(val: div, addr: drv_data->base + I2S_I2STIM_OFFSET); |
49 | |
50 | return 0; |
51 | } |
52 | |
53 | static int xlnx_i2s_set_sysclk(struct snd_soc_dai *dai, |
54 | int clk_id, unsigned int freq, int dir) |
55 | { |
56 | struct xlnx_i2s_drv_data *drv_data = snd_soc_dai_get_drvdata(dai); |
57 | |
58 | drv_data->sysclk = freq; |
59 | if (freq) { |
60 | unsigned int bits_per_sample; |
61 | |
62 | if (drv_data->is_32bit_lrclk) |
63 | bits_per_sample = 32; |
64 | else |
65 | bits_per_sample = drv_data->data_width; |
66 | |
67 | drv_data->ratnum.num = freq / (bits_per_sample * drv_data->channels) / 2; |
68 | drv_data->ratnum.den_step = 1; |
69 | drv_data->ratnum.den_min = 1; |
70 | drv_data->ratnum.den_max = 255; |
71 | drv_data->rate_constraints.rats = &drv_data->ratnum; |
72 | drv_data->rate_constraints.nrats = 1; |
73 | } |
74 | return 0; |
75 | } |
76 | |
77 | static int xlnx_i2s_startup(struct snd_pcm_substream *substream, |
78 | struct snd_soc_dai *dai) |
79 | { |
80 | struct xlnx_i2s_drv_data *drv_data = snd_soc_dai_get_drvdata(dai); |
81 | |
82 | if (drv_data->sysclk) |
83 | return snd_pcm_hw_constraint_ratnums(runtime: substream->runtime, cond: 0, |
84 | SNDRV_PCM_HW_PARAM_RATE, |
85 | r: &drv_data->rate_constraints); |
86 | |
87 | return 0; |
88 | } |
89 | |
90 | static int xlnx_i2s_hw_params(struct snd_pcm_substream *substream, |
91 | struct snd_pcm_hw_params *params, |
92 | struct snd_soc_dai *i2s_dai) |
93 | { |
94 | u32 reg_off, chan_id; |
95 | struct xlnx_i2s_drv_data *drv_data = snd_soc_dai_get_drvdata(dai: i2s_dai); |
96 | |
97 | if (drv_data->sysclk) { |
98 | unsigned int bits_per_sample, sclk, sclk_div; |
99 | |
100 | if (drv_data->is_32bit_lrclk) |
101 | bits_per_sample = 32; |
102 | else |
103 | bits_per_sample = drv_data->data_width; |
104 | |
105 | sclk = params_rate(p: params) * bits_per_sample * params_channels(p: params); |
106 | sclk_div = drv_data->sysclk / sclk / 2; |
107 | |
108 | if ((drv_data->sysclk % sclk != 0) || |
109 | !sclk_div || (sclk_div & ~I2S_I2STIM_VALID_MASK)) { |
110 | dev_warn(i2s_dai->dev, "invalid SCLK divisor for sysclk %u and sclk %u\n" , |
111 | drv_data->sysclk, sclk); |
112 | return -EINVAL; |
113 | } |
114 | writel(val: sclk_div, addr: drv_data->base + I2S_I2STIM_OFFSET); |
115 | } |
116 | |
117 | chan_id = params_channels(p: params) / 2; |
118 | |
119 | while (chan_id > 0) { |
120 | reg_off = I2S_CH0_OFFSET + ((chan_id - 1) * 4); |
121 | writel(val: chan_id, addr: drv_data->base + reg_off); |
122 | chan_id--; |
123 | } |
124 | |
125 | return 0; |
126 | } |
127 | |
128 | static int xlnx_i2s_trigger(struct snd_pcm_substream *substream, int cmd, |
129 | struct snd_soc_dai *i2s_dai) |
130 | { |
131 | struct xlnx_i2s_drv_data *drv_data = snd_soc_dai_get_drvdata(dai: i2s_dai); |
132 | |
133 | switch (cmd) { |
134 | case SNDRV_PCM_TRIGGER_START: |
135 | case SNDRV_PCM_TRIGGER_RESUME: |
136 | case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: |
137 | writel(I2S_CORE_CTRL_ENABLE, addr: drv_data->base + I2S_CORE_CTRL_OFFSET); |
138 | break; |
139 | case SNDRV_PCM_TRIGGER_STOP: |
140 | case SNDRV_PCM_TRIGGER_SUSPEND: |
141 | case SNDRV_PCM_TRIGGER_PAUSE_PUSH: |
142 | writel(val: 0, addr: drv_data->base + I2S_CORE_CTRL_OFFSET); |
143 | break; |
144 | default: |
145 | return -EINVAL; |
146 | } |
147 | |
148 | return 0; |
149 | } |
150 | |
151 | static const struct snd_soc_dai_ops xlnx_i2s_dai_ops = { |
152 | .trigger = xlnx_i2s_trigger, |
153 | .set_sysclk = xlnx_i2s_set_sysclk, |
154 | .set_clkdiv = xlnx_i2s_set_sclkout_div, |
155 | .startup = xlnx_i2s_startup, |
156 | .hw_params = xlnx_i2s_hw_params |
157 | }; |
158 | |
159 | static const struct snd_soc_component_driver xlnx_i2s_component = { |
160 | .name = DRV_NAME, |
161 | .legacy_dai_naming = 1, |
162 | }; |
163 | |
164 | static const struct of_device_id xlnx_i2s_of_match[] = { |
165 | { .compatible = "xlnx,i2s-transmitter-1.0" , }, |
166 | { .compatible = "xlnx,i2s-receiver-1.0" , }, |
167 | {}, |
168 | }; |
169 | MODULE_DEVICE_TABLE(of, xlnx_i2s_of_match); |
170 | |
171 | static int xlnx_i2s_probe(struct platform_device *pdev) |
172 | { |
173 | struct xlnx_i2s_drv_data *drv_data; |
174 | int ret; |
175 | u32 format; |
176 | struct device *dev = &pdev->dev; |
177 | struct device_node *node = dev->of_node; |
178 | |
179 | drv_data = devm_kzalloc(dev: &pdev->dev, size: sizeof(*drv_data), GFP_KERNEL); |
180 | if (!drv_data) |
181 | return -ENOMEM; |
182 | |
183 | drv_data->base = devm_platform_ioremap_resource(pdev, index: 0); |
184 | if (IS_ERR(ptr: drv_data->base)) |
185 | return PTR_ERR(ptr: drv_data->base); |
186 | |
187 | ret = of_property_read_u32(np: node, propname: "xlnx,num-channels" , out_value: &drv_data->channels); |
188 | if (ret < 0) { |
189 | dev_err(dev, "cannot get supported channels\n" ); |
190 | return ret; |
191 | } |
192 | drv_data->channels *= 2; |
193 | |
194 | ret = of_property_read_u32(np: node, propname: "xlnx,dwidth" , out_value: &drv_data->data_width); |
195 | if (ret < 0) { |
196 | dev_err(dev, "cannot get data width\n" ); |
197 | return ret; |
198 | } |
199 | switch (drv_data->data_width) { |
200 | case 16: |
201 | format = SNDRV_PCM_FMTBIT_S16_LE; |
202 | break; |
203 | case 24: |
204 | format = SNDRV_PCM_FMTBIT_S24_LE; |
205 | break; |
206 | default: |
207 | return -EINVAL; |
208 | } |
209 | |
210 | if (of_device_is_compatible(device: node, "xlnx,i2s-transmitter-1.0" )) { |
211 | drv_data->dai_drv.name = "xlnx_i2s_playback" ; |
212 | drv_data->dai_drv.playback.stream_name = "Playback" ; |
213 | drv_data->dai_drv.playback.formats = format; |
214 | drv_data->dai_drv.playback.channels_min = drv_data->channels; |
215 | drv_data->dai_drv.playback.channels_max = drv_data->channels; |
216 | drv_data->dai_drv.playback.rates = SNDRV_PCM_RATE_8000_192000; |
217 | drv_data->dai_drv.ops = &xlnx_i2s_dai_ops; |
218 | } else if (of_device_is_compatible(device: node, "xlnx,i2s-receiver-1.0" )) { |
219 | drv_data->dai_drv.name = "xlnx_i2s_capture" ; |
220 | drv_data->dai_drv.capture.stream_name = "Capture" ; |
221 | drv_data->dai_drv.capture.formats = format; |
222 | drv_data->dai_drv.capture.channels_min = drv_data->channels; |
223 | drv_data->dai_drv.capture.channels_max = drv_data->channels; |
224 | drv_data->dai_drv.capture.rates = SNDRV_PCM_RATE_8000_192000; |
225 | drv_data->dai_drv.ops = &xlnx_i2s_dai_ops; |
226 | } else { |
227 | return -ENODEV; |
228 | } |
229 | drv_data->is_32bit_lrclk = readl(addr: drv_data->base + I2S_CORE_CTRL_OFFSET) & |
230 | I2S_CORE_CTRL_32BIT_LRCLK; |
231 | |
232 | dev_set_drvdata(dev: &pdev->dev, data: drv_data); |
233 | |
234 | ret = devm_snd_soc_register_component(dev: &pdev->dev, component_driver: &xlnx_i2s_component, |
235 | dai_drv: &drv_data->dai_drv, num_dai: 1); |
236 | if (ret) { |
237 | dev_err(&pdev->dev, "i2s component registration failed\n" ); |
238 | return ret; |
239 | } |
240 | |
241 | dev_info(&pdev->dev, "%s DAI registered\n" , drv_data->dai_drv.name); |
242 | |
243 | return ret; |
244 | } |
245 | |
246 | static struct platform_driver xlnx_i2s_aud_driver = { |
247 | .driver = { |
248 | .name = DRV_NAME, |
249 | .of_match_table = xlnx_i2s_of_match, |
250 | }, |
251 | .probe = xlnx_i2s_probe, |
252 | }; |
253 | |
254 | module_platform_driver(xlnx_i2s_aud_driver); |
255 | |
256 | MODULE_LICENSE("GPL v2" ); |
257 | MODULE_AUTHOR("Praveen Vuppala <praveenv@xilinx.com>" ); |
258 | MODULE_AUTHOR("Maruthi Srinivas Bayyavarapu <maruthis@xilinx.com>" ); |
259 | |