1 | // SPDX-License-Identifier: GPL-2.0+ |
2 | // |
3 | // Tobermory audio support |
4 | // |
5 | // Copyright 2011 Wolfson Microelectronics |
6 | |
7 | #include <sound/soc.h> |
8 | #include <sound/soc-dapm.h> |
9 | #include <sound/jack.h> |
10 | #include <linux/gpio.h> |
11 | #include <linux/module.h> |
12 | |
13 | #include "../codecs/wm8962.h" |
14 | |
15 | static int sample_rate = 44100; |
16 | |
17 | static int tobermory_set_bias_level(struct snd_soc_card *card, |
18 | struct snd_soc_dapm_context *dapm, |
19 | enum snd_soc_bias_level level) |
20 | { |
21 | struct snd_soc_pcm_runtime *rtd; |
22 | struct snd_soc_dai *codec_dai; |
23 | int ret; |
24 | |
25 | rtd = snd_soc_get_pcm_runtime(card, dai_link: &card->dai_link[0]); |
26 | codec_dai = snd_soc_rtd_to_codec(rtd, 0); |
27 | |
28 | if (dapm->dev != codec_dai->dev) |
29 | return 0; |
30 | |
31 | switch (level) { |
32 | case SND_SOC_BIAS_PREPARE: |
33 | if (dapm->bias_level == SND_SOC_BIAS_STANDBY) { |
34 | ret = snd_soc_dai_set_pll(dai: codec_dai, WM8962_FLL, |
35 | WM8962_FLL_MCLK, freq_in: 32768, |
36 | freq_out: sample_rate * 512); |
37 | if (ret < 0) |
38 | pr_err("Failed to start FLL: %d\n" , ret); |
39 | |
40 | ret = snd_soc_dai_set_sysclk(dai: codec_dai, |
41 | WM8962_SYSCLK_FLL, |
42 | freq: sample_rate * 512, |
43 | SND_SOC_CLOCK_IN); |
44 | if (ret < 0) { |
45 | pr_err("Failed to set SYSCLK: %d\n" , ret); |
46 | snd_soc_dai_set_pll(dai: codec_dai, WM8962_FLL, |
47 | source: 0, freq_in: 0, freq_out: 0); |
48 | return ret; |
49 | } |
50 | } |
51 | break; |
52 | |
53 | default: |
54 | break; |
55 | } |
56 | |
57 | return 0; |
58 | } |
59 | |
60 | static int tobermory_set_bias_level_post(struct snd_soc_card *card, |
61 | struct snd_soc_dapm_context *dapm, |
62 | enum snd_soc_bias_level level) |
63 | { |
64 | struct snd_soc_pcm_runtime *rtd; |
65 | struct snd_soc_dai *codec_dai; |
66 | int ret; |
67 | |
68 | rtd = snd_soc_get_pcm_runtime(card, dai_link: &card->dai_link[0]); |
69 | codec_dai = snd_soc_rtd_to_codec(rtd, 0); |
70 | |
71 | if (dapm->dev != codec_dai->dev) |
72 | return 0; |
73 | |
74 | switch (level) { |
75 | case SND_SOC_BIAS_STANDBY: |
76 | ret = snd_soc_dai_set_sysclk(dai: codec_dai, WM8962_SYSCLK_MCLK, |
77 | freq: 32768, SND_SOC_CLOCK_IN); |
78 | if (ret < 0) { |
79 | pr_err("Failed to switch away from FLL: %d\n" , ret); |
80 | return ret; |
81 | } |
82 | |
83 | ret = snd_soc_dai_set_pll(dai: codec_dai, WM8962_FLL, |
84 | source: 0, freq_in: 0, freq_out: 0); |
85 | if (ret < 0) { |
86 | pr_err("Failed to stop FLL: %d\n" , ret); |
87 | return ret; |
88 | } |
89 | break; |
90 | |
91 | default: |
92 | break; |
93 | } |
94 | |
95 | dapm->bias_level = level; |
96 | |
97 | return 0; |
98 | } |
99 | |
100 | static int tobermory_hw_params(struct snd_pcm_substream *substream, |
101 | struct snd_pcm_hw_params *params) |
102 | { |
103 | sample_rate = params_rate(p: params); |
104 | |
105 | return 0; |
106 | } |
107 | |
108 | static const struct snd_soc_ops tobermory_ops = { |
109 | .hw_params = tobermory_hw_params, |
110 | }; |
111 | |
112 | SND_SOC_DAILINK_DEFS(cpu, |
113 | DAILINK_COMP_ARRAY(COMP_CPU("samsung-i2s.0" )), |
114 | DAILINK_COMP_ARRAY(COMP_CODEC("wm8962.1-001a" , "wm8962" )), |
115 | DAILINK_COMP_ARRAY(COMP_PLATFORM("samsung-i2s.0" ))); |
116 | |
117 | static struct snd_soc_dai_link tobermory_dai[] = { |
118 | { |
119 | .name = "CPU" , |
120 | .stream_name = "CPU" , |
121 | .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF |
122 | | SND_SOC_DAIFMT_CBM_CFM, |
123 | .ops = &tobermory_ops, |
124 | SND_SOC_DAILINK_REG(cpu), |
125 | }, |
126 | }; |
127 | |
128 | static const struct snd_kcontrol_new controls[] = { |
129 | SOC_DAPM_PIN_SWITCH("Main Speaker" ), |
130 | SOC_DAPM_PIN_SWITCH("DMIC" ), |
131 | }; |
132 | |
133 | static const struct snd_soc_dapm_widget widgets[] = { |
134 | SND_SOC_DAPM_HP("Headphone" , NULL), |
135 | SND_SOC_DAPM_MIC("Headset Mic" , NULL), |
136 | |
137 | SND_SOC_DAPM_MIC("DMIC" , NULL), |
138 | SND_SOC_DAPM_MIC("AMIC" , NULL), |
139 | |
140 | SND_SOC_DAPM_SPK("Main Speaker" , NULL), |
141 | }; |
142 | |
143 | static const struct snd_soc_dapm_route audio_paths[] = { |
144 | { "Headphone" , NULL, "HPOUTL" }, |
145 | { "Headphone" , NULL, "HPOUTR" }, |
146 | |
147 | { "Main Speaker" , NULL, "SPKOUTL" }, |
148 | { "Main Speaker" , NULL, "SPKOUTR" }, |
149 | |
150 | { "Headset Mic" , NULL, "MICBIAS" }, |
151 | { "IN4L" , NULL, "Headset Mic" }, |
152 | { "IN4R" , NULL, "Headset Mic" }, |
153 | |
154 | { "AMIC" , NULL, "MICBIAS" }, |
155 | { "IN1L" , NULL, "AMIC" }, |
156 | { "IN1R" , NULL, "AMIC" }, |
157 | |
158 | { "DMIC" , NULL, "MICBIAS" }, |
159 | { "DMICDAT" , NULL, "DMIC" }, |
160 | }; |
161 | |
162 | static struct snd_soc_jack tobermory_headset; |
163 | |
164 | /* Headset jack detection DAPM pins */ |
165 | static struct snd_soc_jack_pin tobermory_headset_pins[] = { |
166 | { |
167 | .pin = "Headset Mic" , |
168 | .mask = SND_JACK_MICROPHONE, |
169 | }, |
170 | { |
171 | .pin = "Headphone" , |
172 | .mask = SND_JACK_MICROPHONE, |
173 | }, |
174 | }; |
175 | |
176 | static int tobermory_late_probe(struct snd_soc_card *card) |
177 | { |
178 | struct snd_soc_pcm_runtime *rtd; |
179 | struct snd_soc_component *component; |
180 | struct snd_soc_dai *codec_dai; |
181 | int ret; |
182 | |
183 | rtd = snd_soc_get_pcm_runtime(card, dai_link: &card->dai_link[0]); |
184 | component = snd_soc_rtd_to_codec(rtd, 0)->component; |
185 | codec_dai = snd_soc_rtd_to_codec(rtd, 0); |
186 | |
187 | ret = snd_soc_dai_set_sysclk(dai: codec_dai, WM8962_SYSCLK_MCLK, |
188 | freq: 32768, SND_SOC_CLOCK_IN); |
189 | if (ret < 0) |
190 | return ret; |
191 | |
192 | ret = snd_soc_card_jack_new_pins(card, id: "Headset" , type: SND_JACK_HEADSET | |
193 | SND_JACK_BTN_0, jack: &tobermory_headset, |
194 | pins: tobermory_headset_pins, |
195 | ARRAY_SIZE(tobermory_headset_pins)); |
196 | if (ret) |
197 | return ret; |
198 | |
199 | wm8962_mic_detect(component, jack: &tobermory_headset); |
200 | |
201 | return 0; |
202 | } |
203 | |
204 | static struct snd_soc_card tobermory = { |
205 | .name = "Tobermory" , |
206 | .owner = THIS_MODULE, |
207 | .dai_link = tobermory_dai, |
208 | .num_links = ARRAY_SIZE(tobermory_dai), |
209 | |
210 | .set_bias_level = tobermory_set_bias_level, |
211 | .set_bias_level_post = tobermory_set_bias_level_post, |
212 | |
213 | .controls = controls, |
214 | .num_controls = ARRAY_SIZE(controls), |
215 | .dapm_widgets = widgets, |
216 | .num_dapm_widgets = ARRAY_SIZE(widgets), |
217 | .dapm_routes = audio_paths, |
218 | .num_dapm_routes = ARRAY_SIZE(audio_paths), |
219 | .fully_routed = true, |
220 | |
221 | .late_probe = tobermory_late_probe, |
222 | }; |
223 | |
224 | static int tobermory_probe(struct platform_device *pdev) |
225 | { |
226 | struct snd_soc_card *card = &tobermory; |
227 | int ret; |
228 | |
229 | card->dev = &pdev->dev; |
230 | |
231 | ret = devm_snd_soc_register_card(dev: &pdev->dev, card); |
232 | if (ret) |
233 | dev_err_probe(dev: &pdev->dev, err: ret, fmt: "snd_soc_register_card() failed\n" ); |
234 | |
235 | return ret; |
236 | } |
237 | |
238 | static struct platform_driver tobermory_driver = { |
239 | .driver = { |
240 | .name = "tobermory" , |
241 | .pm = &snd_soc_pm_ops, |
242 | }, |
243 | .probe = tobermory_probe, |
244 | }; |
245 | |
246 | module_platform_driver(tobermory_driver); |
247 | |
248 | MODULE_DESCRIPTION("Tobermory audio support" ); |
249 | MODULE_AUTHOR("Mark Brown <broonie@opensource.wolfsonmicro.com>" ); |
250 | MODULE_LICENSE("GPL" ); |
251 | MODULE_ALIAS("platform:tobermory" ); |
252 | |