| /* |
| * ASoC Driver for the Coral Edge TPU dev board |
| * |
| * Author: June Tate-Gans <jtgans@google.com> |
| * Copyright 2018 |
| * |
| * This program is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU General Public License |
| * version 2 as published by the Free Software Foundation. |
| * |
| * This program is distributed in the hope that it will be useful, but |
| * WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * General Public License for more details. |
| */ |
| |
| #include <linux/module.h> |
| #include <linux/platform_device.h> |
| #include <linux/clk.h> |
| #include <linux/i2c.h> |
| |
| #include <sound/core.h> |
| #include <sound/jack.h> |
| #include <sound/pcm.h> |
| #include <sound/pcm_params.h> |
| #include <sound/soc.h> |
| |
| #include "../codecs/rt5645.h" |
| #include "../fsl/fsl_sai.h" |
| |
| #define PLATFORM_CLOCK 2457600 |
| static unsigned long codec_clock = PLATFORM_CLOCK; |
| |
| static struct snd_soc_jack headset_jack; |
| |
| static struct snd_soc_jack_pin headset_jack_pin = { |
| .pin = "Headphone Jack", |
| .mask = 0xFFFFF, |
| .invert = 0 |
| }; |
| |
| static int card_init(struct snd_soc_pcm_runtime *rtd) { |
| int ret; |
| |
| rt5645_sel_asrc_clk_src(rtd->codec, |
| RT5645_DA_STEREO_FILTER | |
| RT5645_AD_STEREO_FILTER | |
| RT5645_DA_MONO_L_FILTER | |
| RT5645_DA_MONO_R_FILTER, |
| RT5645_CLK_SEL_I2S1_ASRC); |
| |
| ret = snd_soc_dai_set_sysclk(rtd->codec_dai, RT5645_SCLK_S_MCLK, |
| codec_clock, SND_SOC_CLOCK_IN); |
| if (ret < 0) { |
| dev_err(rtd->card->dev, "can't set sysclk: %d\n", ret); |
| return ret; |
| } |
| |
| ret = snd_soc_card_jack_new(rtd->card, "Headphone Jack", |
| SND_JACK_HEADSET, |
| &headset_jack, &headset_jack_pin, 1); |
| if (ret < 0) { |
| dev_err(rtd->card->dev, "can't add headphone jack: %d\n", ret); |
| return ret; |
| } |
| |
| return rt5645_set_jack_detect(rtd->codec, &headset_jack, NULL, NULL); |
| } |
| |
| static int hw_params( |
| struct snd_pcm_substream *substream, |
| struct snd_pcm_hw_params *params) { |
| int ret = 0; |
| struct snd_soc_pcm_runtime *rtd = substream->private_data; |
| unsigned int freq = params_rate(params) * 512; |
| |
| /* set codec PLL source to the 24.576MHz (MCLK) platform clock */ |
| ret = snd_soc_dai_set_pll(rtd->codec_dai, 0, RT5645_PLL1_S_MCLK, |
| codec_clock, freq); |
| if (ret < 0) { |
| dev_err(rtd->dev, "can't set codec pll: %d\n", ret); |
| return ret; |
| } |
| |
| ret = snd_soc_dai_set_sysclk(rtd->codec_dai, RT5645_SCLK_S_PLL1, freq, |
| SND_SOC_CLOCK_IN); |
| if (ret < 0) { |
| dev_err(rtd->dev, "can't set codec sysclk in: %d\n", ret); |
| return ret; |
| } |
| |
| ret = snd_soc_dai_set_sysclk(rtd->codec_dai, RT5645_SCLK_S_PLL1, freq, |
| SND_SOC_CLOCK_OUT); |
| if (ret < 0) { |
| dev_err(rtd->dev, "can't set codec sysclk out: %d\n", ret); |
| return ret; |
| } |
| |
| ret = snd_soc_dai_set_sysclk(rtd->cpu_dai, FSL_SAI_CLK_MAST1, freq, |
| SND_SOC_CLOCK_OUT); |
| if (ret < 0) { |
| dev_err(rtd->dev, "can't set cpu sysclk out: %d\n", ret); |
| return ret; |
| } |
| |
| return ret; |
| } |
| |
| static struct snd_soc_ops ops = { |
| .hw_params = hw_params, |
| }; |
| |
| static struct snd_soc_dai_link card_dai[] = { |
| { |
| .name = "rt5645", |
| .stream_name = "Coral Edge TPU HiFi", |
| .codec_dai_name = "rt5645-aif1", |
| .dai_fmt = SND_SOC_DAIFMT_I2S | |
| SND_SOC_DAIFMT_NB_NF | |
| SND_SOC_DAIFMT_CBS_CFS, |
| .ops = &ops, |
| .init = card_init |
| }, |
| }; |
| |
| static const struct snd_soc_dapm_widget card_widgets[] = { |
| SND_SOC_DAPM_HP("Headphone Jack", NULL), |
| SND_SOC_DAPM_SPK("Speaker", NULL), |
| SND_SOC_DAPM_MIC("Internal Mic", NULL), |
| SND_SOC_DAPM_MIC("Headphone Mic", NULL), |
| }; |
| |
| static const struct snd_soc_dapm_route audio_routes[] = { |
| {"AIF1 Capture", NULL, "CPU-Capture"}, |
| {"micbias1", NULL, "Headphone Mic"}, |
| {"IN1P", NULL, "micbias1"}, |
| {"Speaker", NULL, "SPOL"}, |
| {"Speaker", NULL, "SPOR"}, |
| {"DMIC L1", NULL, "Internal Mic"}, |
| {"DMIC R1", NULL, "Internal Mic"}, |
| {"Headphone Jack", NULL, "HPOR"}, |
| {"Headphone Jack", NULL, "HPOL"}, |
| }; |
| |
| static const struct snd_kcontrol_new card_controls[] = { |
| SOC_DAPM_PIN_SWITCH("Headphone Jack"), |
| SOC_DAPM_PIN_SWITCH("Speaker"), |
| SOC_DAPM_PIN_SWITCH("Internal Mic"), |
| SOC_DAPM_PIN_SWITCH("Headphone Mic"), |
| }; |
| |
| static struct snd_soc_card snd_edgetpu_card = { |
| .name = "snd-edgetpu-card", |
| .owner = THIS_MODULE, |
| .dai_link = card_dai, |
| .num_links = ARRAY_SIZE(card_dai), |
| .dapm_routes = audio_routes, |
| .num_dapm_routes = ARRAY_SIZE(audio_routes), |
| .dapm_widgets = card_widgets, |
| .num_dapm_widgets = ARRAY_SIZE(card_widgets), |
| .controls = card_controls, |
| .num_controls = ARRAY_SIZE(card_controls), |
| .fully_routed = true, |
| }; |
| |
| static int probe(struct platform_device *pdev) { |
| int ret = 0; |
| struct snd_soc_dai_link *dai = &card_dai[0]; |
| struct snd_soc_card *card = &snd_edgetpu_card; |
| struct device *dev = &pdev->dev; |
| struct i2c_client *codec_dev; |
| struct device_node *i2s_node; |
| struct clk *codec_clk; |
| |
| if (!dev) { |
| printk(KERN_ERR "edgetpu-audio-card: no device for this platform_device?!\n"); |
| return -EINVAL; |
| } |
| |
| card->dev = dev; |
| |
| if (!dev->of_node) { |
| dev_err(dev, "this device requires a devicetree node!\n"); |
| return -EINVAL; |
| } |
| |
| dai->codec_name = NULL; |
| dai->codec_of_node = of_parse_phandle(dev->of_node, "audio-codec", 0); |
| |
| if (!dai->codec_of_node) { |
| dev_err(dev, "can't parse codec node\n"); |
| return -EINVAL; |
| } |
| |
| codec_dev = of_find_i2c_device_by_node(dai->codec_of_node); |
| |
| if (!codec_dev) { |
| dev_err(dev, "can't find codec device!\n"); |
| return -EINVAL; |
| } |
| |
| codec_clk = clk_get(&codec_dev->dev, NULL); |
| if (IS_ERR(codec_clk)) { |
| dev_warn(dev, "can't find clock -- using defaults!\n"); |
| } else { |
| codec_clock = clk_get_rate(codec_clk); |
| clk_put(codec_clk); |
| dev_info(dev, "clock set to %ld\n", codec_clock); |
| } |
| |
| i2s_node = of_parse_phandle(dev->of_node, "audio-cpu", 0); |
| |
| if (!i2s_node) { |
| dev_err(dev, "can't parse cpu node\n"); |
| return -EINVAL; |
| } |
| |
| dai->cpu_dai_name = NULL; |
| dai->cpu_of_node = i2s_node; |
| dai->platform_name = NULL; |
| dai->platform_of_node = i2s_node; |
| |
| ret = snd_soc_of_parse_card_name(card, "google,model"); |
| if (ret < 0) { |
| dev_err(dev, "can't parse card name: %d\n", ret); |
| return ret; |
| } |
| |
| ret = devm_snd_soc_register_card(dev, card); |
| if (ret < 0) { |
| dev_err(dev, "can't register card: %d\n", ret); |
| return ret; |
| } |
| |
| return 0; |
| } |
| |
| static const struct of_device_id of_match[] = { |
| { |
| .compatible = "google,edgetpu-audio-card", |
| }, |
| {}, |
| }; |
| MODULE_DEVICE_TABLE(of, of_match); |
| |
| static struct platform_driver card_driver = { |
| .driver = { |
| .name = "edgetpu-audio-card", |
| .owner = THIS_MODULE, |
| .of_match_table = of_match, |
| }, |
| .probe = probe, |
| }; |
| module_platform_driver(card_driver); |
| |
| MODULE_AUTHOR("June Tate-Gans <jtgans@google.com>"); |
| MODULE_DESCRIPTION("ASoC Driver for Coral Edge TPU"); |
| MODULE_LICENSE("GPL v2"); |