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
16static char codec_name[SND_ACPI_I2C_ID_LEN];
17
18struct loongson_card_data {
19	struct snd_soc_card snd_card;
20	unsigned int mclk_fs;
21};
22
23static 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(rtd->card);
30	int ret, mclk;
31
32	if (ls_card->mclk_fs) {
33		mclk = ls_card->mclk_fs * params_rate(params);
34		ret = snd_soc_dai_set_sysclk(cpu_dai, 0, 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(codec_dai, 0, 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
51static const struct snd_soc_ops loongson_ops = {
52	.hw_params = loongson_card_hw_params,
53};
54
55SND_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
60static 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
71static 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, "cpu", 0, &args);
84	if (ret || !is_acpi_device_node(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(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, "codec", 0, &args);
98	if (ret || !is_acpi_device_node(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(codec_name, sizeof(codec_name), "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(card->dev, "codec-dai-name",
108				    &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
115static 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(dev->of_node, "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(dev->of_node, "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(cpu, NULL, loongson_dai_links[i].cpus, 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(codec, NULL, loongson_dai_links[i].codecs, 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(cpu);
149	of_node_put(codec);
150
151	return 0;
152
153err:
154	of_node_put(cpu);
155	of_node_put(codec);
156	return ret;
157}
158
159static 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(&pdev->dev, 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, ls_priv);
176
177	ret = device_property_read_string(&pdev->dev, "model", &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(&pdev->dev, "mclk-fs", &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(&pdev->dev))
189		ret = loongson_card_parse_acpi(ls_priv);
190	else
191		ret = loongson_card_parse_of(ls_priv);
192	if (ret < 0)
193		return ret;
194
195	ret = devm_snd_soc_register_card(&pdev->dev, card);
196
197	return ret;
198}
199
200static const struct of_device_id loongson_asoc_dt_ids[] = {
201	{ .compatible = "loongson,ls-audio-card" },
202	{ /* sentinel */ },
203};
204MODULE_DEVICE_TABLE(of, loongson_asoc_dt_ids);
205
206static 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};
214module_platform_driver(loongson_audio_driver);
215
216MODULE_DESCRIPTION("Loongson ASoc Sound Card driver");
217MODULE_AUTHOR("Loongson Technology Corporation Limited");
218MODULE_LICENSE("GPL");
219