1 | // SPDX-License-Identifier: GPL-2.0 |
2 | // |
3 | // Loongson ASoC Audio Machine driver |
4 | // |
5 | // Copyright (C) 2023 Loongson Technology Corporation Limited |
6 | // Author: Yingkun Meng <mengyingkun@loongson.cn> |
7 | // |
8 | |
9 | #include <linux/module.h> |
10 | #include <sound/soc.h> |
11 | #include <sound/soc-acpi.h> |
12 | #include <linux/acpi.h> |
13 | #include <linux/pci.h> |
14 | #include <sound/pcm_params.h> |
15 | |
16 | static char codec_name[SND_ACPI_I2C_ID_LEN]; |
17 | |
18 | struct loongson_card_data { |
19 | struct snd_soc_card snd_card; |
20 | unsigned int mclk_fs; |
21 | }; |
22 | |
23 | static int loongson_card_hw_params(struct snd_pcm_substream *substream, |
24 | struct snd_pcm_hw_params *params) |
25 | { |
26 | struct snd_soc_pcm_runtime *rtd = substream->private_data; |
27 | struct snd_soc_dai *cpu_dai = snd_soc_rtd_to_cpu(rtd, 0); |
28 | struct snd_soc_dai *codec_dai = snd_soc_rtd_to_codec(rtd, 0); |
29 | struct loongson_card_data *ls_card = snd_soc_card_get_drvdata(card: rtd->card); |
30 | int ret, mclk; |
31 | |
32 | if (ls_card->mclk_fs) { |
33 | mclk = ls_card->mclk_fs * params_rate(p: params); |
34 | ret = snd_soc_dai_set_sysclk(dai: cpu_dai, clk_id: 0, freq: mclk, |
35 | SND_SOC_CLOCK_OUT); |
36 | if (ret < 0) { |
37 | dev_err(codec_dai->dev, "cpu_dai clock not set\n" ); |
38 | return ret; |
39 | } |
40 | |
41 | ret = snd_soc_dai_set_sysclk(dai: codec_dai, clk_id: 0, freq: mclk, |
42 | SND_SOC_CLOCK_IN); |
43 | if (ret < 0) { |
44 | dev_err(codec_dai->dev, "codec_dai clock not set\n" ); |
45 | return ret; |
46 | } |
47 | } |
48 | return 0; |
49 | } |
50 | |
51 | static const struct snd_soc_ops loongson_ops = { |
52 | .hw_params = loongson_card_hw_params, |
53 | }; |
54 | |
55 | SND_SOC_DAILINK_DEFS(analog, |
56 | DAILINK_COMP_ARRAY(COMP_CPU("loongson-i2s" )), |
57 | DAILINK_COMP_ARRAY(COMP_EMPTY()), |
58 | DAILINK_COMP_ARRAY(COMP_EMPTY())); |
59 | |
60 | static struct snd_soc_dai_link loongson_dai_links[] = { |
61 | { |
62 | .name = "Loongson Audio Port" , |
63 | .stream_name = "Loongson Audio" , |
64 | .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_IB_NF |
65 | | SND_SOC_DAIFMT_CBC_CFC, |
66 | SND_SOC_DAILINK_REG(analog), |
67 | .ops = &loongson_ops, |
68 | }, |
69 | }; |
70 | |
71 | static int loongson_card_parse_acpi(struct loongson_card_data *data) |
72 | { |
73 | struct snd_soc_card *card = &data->snd_card; |
74 | struct fwnode_handle *fwnode = card->dev->fwnode; |
75 | struct fwnode_reference_args args; |
76 | const char *codec_dai_name; |
77 | struct acpi_device *adev; |
78 | struct device *phy_dev; |
79 | int ret, i; |
80 | |
81 | /* fixup platform name based on reference node */ |
82 | memset(&args, 0, sizeof(args)); |
83 | ret = acpi_node_get_property_reference(fwnode, name: "cpu" , index: 0, args: &args); |
84 | if (ret || !is_acpi_device_node(fwnode: args.fwnode)) { |
85 | dev_err(card->dev, "No matching phy in ACPI table\n" ); |
86 | return ret ?: -ENOENT; |
87 | } |
88 | adev = to_acpi_device_node(args.fwnode); |
89 | phy_dev = acpi_get_first_physical_node(adev); |
90 | if (!phy_dev) |
91 | return -EPROBE_DEFER; |
92 | for (i = 0; i < card->num_links; i++) |
93 | loongson_dai_links[i].platforms->name = dev_name(dev: phy_dev); |
94 | |
95 | /* fixup codec name based on reference node */ |
96 | memset(&args, 0, sizeof(args)); |
97 | ret = acpi_node_get_property_reference(fwnode, name: "codec" , index: 0, args: &args); |
98 | if (ret || !is_acpi_device_node(fwnode: args.fwnode)) { |
99 | dev_err(card->dev, "No matching phy in ACPI table\n" ); |
100 | return ret ?: -ENOENT; |
101 | } |
102 | adev = to_acpi_device_node(args.fwnode); |
103 | snprintf(buf: codec_name, size: sizeof(codec_name), fmt: "i2c-%s" , acpi_dev_name(adev)); |
104 | for (i = 0; i < card->num_links; i++) |
105 | loongson_dai_links[i].codecs->name = codec_name; |
106 | |
107 | device_property_read_string(dev: card->dev, propname: "codec-dai-name" , |
108 | val: &codec_dai_name); |
109 | for (i = 0; i < card->num_links; i++) |
110 | loongson_dai_links[i].codecs->dai_name = codec_dai_name; |
111 | |
112 | return 0; |
113 | } |
114 | |
115 | static int loongson_card_parse_of(struct loongson_card_data *data) |
116 | { |
117 | struct device_node *cpu, *codec; |
118 | struct snd_soc_card *card = &data->snd_card; |
119 | struct device *dev = card->dev; |
120 | int ret, i; |
121 | |
122 | cpu = of_get_child_by_name(node: dev->of_node, name: "cpu" ); |
123 | if (!cpu) { |
124 | dev_err(dev, "platform property missing or invalid\n" ); |
125 | return -EINVAL; |
126 | } |
127 | codec = of_get_child_by_name(node: dev->of_node, name: "codec" ); |
128 | if (!codec) { |
129 | dev_err(dev, "audio-codec property missing or invalid\n" ); |
130 | ret = -EINVAL; |
131 | goto err; |
132 | } |
133 | |
134 | for (i = 0; i < card->num_links; i++) { |
135 | ret = snd_soc_of_get_dlc(of_node: cpu, NULL, dlc: loongson_dai_links[i].cpus, index: 0); |
136 | if (ret < 0) { |
137 | dev_err(dev, "getting cpu dlc error (%d)\n" , ret); |
138 | goto err; |
139 | } |
140 | |
141 | ret = snd_soc_of_get_dlc(of_node: codec, NULL, dlc: loongson_dai_links[i].codecs, index: 0); |
142 | if (ret < 0) { |
143 | dev_err(dev, "getting codec dlc error (%d)\n" , ret); |
144 | goto err; |
145 | } |
146 | } |
147 | |
148 | of_node_put(node: cpu); |
149 | of_node_put(node: codec); |
150 | |
151 | return 0; |
152 | |
153 | err: |
154 | of_node_put(node: cpu); |
155 | of_node_put(node: codec); |
156 | return ret; |
157 | } |
158 | |
159 | static int loongson_asoc_card_probe(struct platform_device *pdev) |
160 | { |
161 | struct loongson_card_data *ls_priv; |
162 | struct snd_soc_card *card; |
163 | int ret; |
164 | |
165 | ls_priv = devm_kzalloc(dev: &pdev->dev, size: sizeof(*ls_priv), GFP_KERNEL); |
166 | if (!ls_priv) |
167 | return -ENOMEM; |
168 | |
169 | card = &ls_priv->snd_card; |
170 | |
171 | card->dev = &pdev->dev; |
172 | card->owner = THIS_MODULE; |
173 | card->dai_link = loongson_dai_links; |
174 | card->num_links = ARRAY_SIZE(loongson_dai_links); |
175 | snd_soc_card_set_drvdata(card, data: ls_priv); |
176 | |
177 | ret = device_property_read_string(dev: &pdev->dev, propname: "model" , val: &card->name); |
178 | if (ret) { |
179 | dev_err(&pdev->dev, "Error parsing card name: %d\n" , ret); |
180 | return ret; |
181 | } |
182 | ret = device_property_read_u32(dev: &pdev->dev, propname: "mclk-fs" , val: &ls_priv->mclk_fs); |
183 | if (ret) { |
184 | dev_err(&pdev->dev, "Error parsing mclk-fs: %d\n" , ret); |
185 | return ret; |
186 | } |
187 | |
188 | if (has_acpi_companion(dev: &pdev->dev)) |
189 | ret = loongson_card_parse_acpi(data: ls_priv); |
190 | else |
191 | ret = loongson_card_parse_of(data: ls_priv); |
192 | if (ret < 0) |
193 | return ret; |
194 | |
195 | ret = devm_snd_soc_register_card(dev: &pdev->dev, card); |
196 | |
197 | return ret; |
198 | } |
199 | |
200 | static const struct of_device_id loongson_asoc_dt_ids[] = { |
201 | { .compatible = "loongson,ls-audio-card" }, |
202 | { /* sentinel */ }, |
203 | }; |
204 | MODULE_DEVICE_TABLE(of, loongson_asoc_dt_ids); |
205 | |
206 | static struct platform_driver loongson_audio_driver = { |
207 | .probe = loongson_asoc_card_probe, |
208 | .driver = { |
209 | .name = "loongson-asoc-card" , |
210 | .pm = &snd_soc_pm_ops, |
211 | .of_match_table = loongson_asoc_dt_ids, |
212 | }, |
213 | }; |
214 | module_platform_driver(loongson_audio_driver); |
215 | |
216 | MODULE_DESCRIPTION("Loongson ASoc Sound Card driver" ); |
217 | MODULE_AUTHOR("Loongson Technology Corporation Limited" ); |
218 | MODULE_LICENSE("GPL" ); |
219 | |