From 9031b415030a316ec4ca513185e2d2c0fbb894c4 Mon Sep 17 00:00:00 2001 From: Jerome Brunet Date: Thu, 30 Mar 2017 12:17:27 +0200 Subject: [PATCH] ASoC: meson: add initial i2s dai support Add support for the i2s dai found on Amlogic Meson SoC family. With this initial implementation, only playback is supported. Capture will be part of furture work. Signed-off-by: Jerome Brunet --- sound/soc/meson/Kconfig | 2 +- sound/soc/meson/Makefile | 4 +- sound/soc/meson/i2s-dai.c | 465 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 469 insertions(+), 2 deletions(-) create mode 100644 sound/soc/meson/i2s-dai.c diff --git a/sound/soc/meson/Kconfig b/sound/soc/meson/Kconfig index 6e030b5..5904e9e 100644 --- a/sound/soc/meson/Kconfig +++ b/sound/soc/meson/Kconfig @@ -78,5 +78,5 @@ config SND_SOC_MESON_I2S tristate "Meson i2s interface" depends on SND_SOC_MESON help - Say Y or M if you want to add support for i2s dma driver for Amlogic + Say Y or M if you want to add support for i2s driver for Amlogic Meson SoCs. diff --git a/sound/soc/meson/Makefile b/sound/soc/meson/Makefile index 5796007..b8641f9 100644 --- a/sound/soc/meson/Makefile +++ b/sound/soc/meson/Makefile @@ -22,6 +22,8 @@ obj-$(CONFIG_SND_MESON_AXG_SPDIFOUT) += snd-soc-meson-axg-spdifout.o snd-soc-meson-audio-core-objs := audio-core.o snd-soc-meson-aiu-i2s-dma-objs := aiu-i2s-dma.o +snd-soc-meson-i2s-dai-objs := i2s-dai.o obj-$(CONFIG_SND_SOC_MESON) += snd-soc-meson-audio-core.o -obj-$(CONFIG_SND_SOC_MESON_I2S) += snd-soc-meson-aiu-i2s-dma.o \ No newline at end of file +obj-$(CONFIG_SND_SOC_MESON_I2S) += snd-soc-meson-aiu-i2s-dma.o +obj-$(CONFIG_SND_SOC_MESON_I2S) += snd-soc-meson-i2s-dai.o \ No newline at end of file diff --git a/sound/soc/meson/i2s-dai.c b/sound/soc/meson/i2s-dai.c new file mode 100644 index 0000000..1008af8 --- /dev/null +++ b/sound/soc/meson/i2s-dai.c @@ -0,0 +1,465 @@ +/* + * Copyright (C) 2017 BayLibre, SAS + * Author: Jerome Brunet + * Copyright (C) 2017 Amlogic, Inc. All rights reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "aiu-regs.h" +#include "audio-core.h" + +#define DRV_NAME "meson-i2s-dai" + +struct meson_i2s_dai { + struct meson_audio_core_data *core; + struct clk *mclk; + struct clk *bclks; + struct clk *iface; + struct clk *fast; + bool bclks_idle; +}; + +#define AIU_CLK_CTRL_I2S_DIV_EN BIT(0) +#define AIU_CLK_CTRL_I2S_DIV_MASK GENMASK(3, 2) +#define AIU_CLK_CTRL_AOCLK_POLARITY_MASK BIT(6) +#define AIU_CLK_CTRL_AOCLK_POLARITY_NORMAL (0 << 6) +#define AIU_CLK_CTRL_AOCLK_POLARITY_INVERTED (1 << 6) +#define AIU_CLK_CTRL_ALRCLK_POLARITY_MASK BIT(7) +#define AIU_CLK_CTRL_ALRCLK_POLARITY_NORMAL (0 << 7) +#define AIU_CLK_CTRL_ALRCLK_POLARITY_INVERTED (1 << 7) +#define AIU_CLK_CTRL_ALRCLK_SKEW_MASK GENMASK(9, 8) +#define AIU_CLK_CTRL_ALRCLK_LEFT_J (0 << 8) +#define AIU_CLK_CTRL_ALRCLK_I2S (1 << 8) +#define AIU_CLK_CTRL_ALRCLK_RIGHT_J (2 << 8) +#define AIU_CLK_CTRL_MORE_I2S_DIV_MASK GENMASK(5, 0) +#define AIU_CLK_CTRL_MORE_I2S_DIV(div) (((div) - 1) << 0) +#define AIU_CODEC_DAC_LRCLK_CTRL_DIV_MASK GENMASK(11, 0) +#define AIU_CODEC_DAC_LRCLK_CTRL_DIV(div) (((div) - 1) << 0) +#define AIU_I2S_DAC_CFG_PAYLOAD_SIZE_MASK GENMASK(1, 0) +#define AIU_I2S_DAC_CFG_AOCLK_32 (0 << 0) +#define AIU_I2S_DAC_CFG_AOCLK_48 (2 << 0) +#define AIU_I2S_DAC_CFG_AOCLK_64 (3 << 0) +#define AIU_I2S_MISC_HOLD_EN BIT(2) +#define AIU_I2S_SOURCE_DESC_MODE_8CH BIT(0) +#define AIU_I2S_SOURCE_DESC_MODE_24BIT BIT(5) +#define AIU_I2S_SOURCE_DESC_MODE_32BIT BIT(9) +#define AIU_I2S_SOURCE_DESC_MODE_SPLIT BIT(11) + +static void __hold(struct meson_i2s_dai *priv, bool enable) +{ + regmap_update_bits(priv->core->aiu, AIU_I2S_MISC, + AIU_I2S_MISC_HOLD_EN, + enable ? AIU_I2S_MISC_HOLD_EN : 0); +} + +static void __divider_enable(struct meson_i2s_dai *priv, bool enable) +{ + regmap_update_bits(priv->core->aiu, AIU_CLK_CTRL, + AIU_CLK_CTRL_I2S_DIV_EN, + enable ? AIU_CLK_CTRL_I2S_DIV_EN : 0); +} + +static void __playback_start(struct meson_i2s_dai *priv) +{ + __divider_enable(priv, true); + __hold(priv, false); +} + +static void __playback_stop(struct meson_i2s_dai *priv, bool clk_force) +{ + __hold(priv, true); + /* Disable the bit clks if necessary */ + if (clk_force || !priv->bclks_idle) + __divider_enable(priv, false); +} + +static int meson_i2s_dai_trigger(struct snd_pcm_substream *substream, int cmd, + struct snd_soc_dai *dai) +{ + struct meson_i2s_dai *priv = snd_soc_dai_get_drvdata(dai); + bool clk_force_stop = false; + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + case SNDRV_PCM_TRIGGER_RESUME: + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + __playback_start(priv); + return 0; + + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_SUSPEND: + clk_force_stop = true; + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + __playback_stop(priv, clk_force_stop); + return 0; + + default: + return -EINVAL; + } +} + +static int __bclks_set_rate(struct meson_i2s_dai *priv, unsigned int srate, + unsigned int width) +{ + unsigned int fs; + + /* Get the oversampling factor */ + fs = DIV_ROUND_CLOSEST(clk_get_rate(priv->mclk), srate); + + /* + * This DAI is usually connected to the dw-hdmi which does not support + * bclk being 32 * lrclk or 48 * lrclk + * Restrict to blck = 64 * lrclk + */ + if (fs % 64) + return -EINVAL; + + /* Set the divider between lrclk and bclk */ + regmap_update_bits(priv->core->aiu, AIU_I2S_DAC_CFG, + AIU_I2S_DAC_CFG_PAYLOAD_SIZE_MASK, + AIU_I2S_DAC_CFG_AOCLK_64); + + regmap_update_bits(priv->core->aiu, AIU_CODEC_DAC_LRCLK_CTRL, + AIU_CODEC_DAC_LRCLK_CTRL_DIV_MASK, + AIU_CODEC_DAC_LRCLK_CTRL_DIV(64)); + + /* Use CLK_MORE for the i2s divider */ + regmap_update_bits(priv->core->aiu, AIU_CLK_CTRL, + AIU_CLK_CTRL_I2S_DIV_MASK, + 0); + + regmap_update_bits(priv->core->aiu, AIU_CLK_CTRL_MORE, + AIU_CLK_CTRL_MORE_I2S_DIV_MASK, + AIU_CLK_CTRL_MORE_I2S_DIV(fs / 64)); + + return 0; +} + +static int __setup_desc(struct meson_i2s_dai *priv, unsigned int width, + unsigned int channels) +{ + u32 desc = 0; + + switch (width) { + case 24: + /* + * For some reason, 24 bits wide audio don't play well + * if the 32 bits mode is not set + */ + desc |= (AIU_I2S_SOURCE_DESC_MODE_24BIT | + AIU_I2S_SOURCE_DESC_MODE_32BIT); + break; + case 16: + break; + + default: + return -EINVAL; + } + + switch (channels) { + case 2: /* Nothing to do */ + break; + case 8: + /* TODO: Still requires testing ... */ + desc |= AIU_I2S_SOURCE_DESC_MODE_8CH; + break; + default: + return -EINVAL; + } + + regmap_update_bits(priv->core->aiu, AIU_I2S_SOURCE_DESC, + AIU_I2S_SOURCE_DESC_MODE_8CH | + AIU_I2S_SOURCE_DESC_MODE_24BIT | + AIU_I2S_SOURCE_DESC_MODE_32BIT, + desc); + + return 0; +} + +static int meson_i2s_dai_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai) +{ + struct meson_i2s_dai *priv = snd_soc_dai_get_drvdata(dai); + unsigned int width = params_width(params); + unsigned int channels = params_channels(params); + unsigned int rate = params_rate(params); + int ret; + + ret = __setup_desc(priv, width, channels); + if (ret) { + dev_err(dai->dev, "Unable set to set i2s description\n"); + return ret; + } + + ret = __bclks_set_rate(priv, rate, width); + if (ret) { + dev_err(dai->dev, "Unable set to the i2s clock rates\n"); + return ret; + } + + return 0; +} + +static int meson_i2s_dai_set_fmt(struct snd_soc_dai *dai, unsigned int fmt) +{ + struct meson_i2s_dai *priv = snd_soc_dai_get_drvdata(dai); + u32 val; + + if ((fmt & SND_SOC_DAIFMT_MASTER_MASK) != SND_SOC_DAIFMT_CBS_CFS) + return -EINVAL; + + /* DAI output mode */ + switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) { + case SND_SOC_DAIFMT_I2S: + val = AIU_CLK_CTRL_ALRCLK_I2S; + break; + case SND_SOC_DAIFMT_LEFT_J: + val = AIU_CLK_CTRL_ALRCLK_LEFT_J; + break; + case SND_SOC_DAIFMT_RIGHT_J: + val = AIU_CLK_CTRL_ALRCLK_RIGHT_J; + break; + default: + return -EINVAL; + } + + regmap_update_bits(priv->core->aiu, AIU_CLK_CTRL, + AIU_CLK_CTRL_ALRCLK_SKEW_MASK, + val); + + /* DAI clock polarity */ + switch (fmt & SND_SOC_DAIFMT_INV_MASK) { + case SND_SOC_DAIFMT_IB_IF: + /* Invert both clocks */ + val = AIU_CLK_CTRL_ALRCLK_POLARITY_INVERTED | + AIU_CLK_CTRL_AOCLK_POLARITY_INVERTED; + break; + case SND_SOC_DAIFMT_IB_NF: + /* Invert bit clock */ + val = AIU_CLK_CTRL_ALRCLK_POLARITY_NORMAL | + AIU_CLK_CTRL_AOCLK_POLARITY_INVERTED; + break; + case SND_SOC_DAIFMT_NB_IF: + /* Invert frame clock */ + val = AIU_CLK_CTRL_ALRCLK_POLARITY_INVERTED | + AIU_CLK_CTRL_AOCLK_POLARITY_NORMAL; + break; + case SND_SOC_DAIFMT_NB_NF: + /* Normal clocks */ + val = AIU_CLK_CTRL_ALRCLK_POLARITY_NORMAL | + AIU_CLK_CTRL_AOCLK_POLARITY_NORMAL; + break; + default: + return -EINVAL; + } + + regmap_update_bits(priv->core->aiu, AIU_CLK_CTRL, + AIU_CLK_CTRL_ALRCLK_POLARITY_MASK | + AIU_CLK_CTRL_AOCLK_POLARITY_MASK, + val); + + switch (fmt & SND_SOC_DAIFMT_CLOCK_MASK) { + case SND_SOC_DAIFMT_CONT: + priv->bclks_idle = true; + break; + case SND_SOC_DAIFMT_GATED: + priv->bclks_idle = false; + break; + default: + return -EINVAL; + } + + return 0; +} + +static int meson_i2s_dai_set_sysclk(struct snd_soc_dai *dai, int clk_id, + unsigned int freq, int dir) +{ + struct meson_i2s_dai *priv = snd_soc_dai_get_drvdata(dai); + int ret; + + if (WARN_ON(clk_id != 0)) + return -EINVAL; + + if (dir == SND_SOC_CLOCK_IN) + return 0; + + ret = clk_set_rate(priv->mclk, freq); + if (ret) { + dev_err(dai->dev, "Failed to set sysclk to %uHz", freq); + return ret; + } + + return 0; +} + +static int meson_i2s_dai_startup(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + struct meson_i2s_dai *priv = snd_soc_dai_get_drvdata(dai); + int ret; + + /* Power up the i2s fast domain - can't write the registers w/o it */ + ret = clk_prepare_enable(priv->fast); + if (ret) + goto out_clk_fast; + + /* Make sure nothing gets out of the DAI yet */ + __hold(priv, true); + + /* I2S encoder needs the mixer interface gate */ + ret = clk_prepare_enable(priv->iface); + if (ret) + goto out_clk_iface; + + /* Enable the i2s master clock */ + ret = clk_prepare_enable(priv->mclk); + if (ret) + goto out_mclk; + + /* Enable the bit clock gate */ + ret = clk_prepare_enable(priv->bclks); + if (ret) + goto out_bclks; + + /* Make sure the interface expect a memory layout we can work with */ + regmap_update_bits(priv->core->aiu, AIU_I2S_SOURCE_DESC, + AIU_I2S_SOURCE_DESC_MODE_SPLIT, + AIU_I2S_SOURCE_DESC_MODE_SPLIT); + + return 0; + +out_bclks: + clk_disable_unprepare(priv->mclk); +out_mclk: + clk_disable_unprepare(priv->iface); +out_clk_iface: + clk_disable_unprepare(priv->fast); +out_clk_fast: + return ret; +} + +static void meson_i2s_dai_shutdown(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + struct meson_i2s_dai *priv = snd_soc_dai_get_drvdata(dai); + + clk_disable_unprepare(priv->bclks); + clk_disable_unprepare(priv->mclk); + clk_disable_unprepare(priv->iface); + clk_disable_unprepare(priv->fast); +} + +static const struct snd_soc_dai_ops meson_i2s_dai_ops = { + .startup = meson_i2s_dai_startup, + .shutdown = meson_i2s_dai_shutdown, + .trigger = meson_i2s_dai_trigger, + .hw_params = meson_i2s_dai_hw_params, + .set_fmt = meson_i2s_dai_set_fmt, + .set_sysclk = meson_i2s_dai_set_sysclk, +}; + +static struct snd_soc_dai_driver meson_i2s_dai = { + .playback = { + .stream_name = "Playback", + .channels_min = 2, + .channels_max = 8, + .rates = SNDRV_PCM_RATE_8000_192000, + .formats = (SNDRV_PCM_FMTBIT_S16_LE | + SNDRV_PCM_FMTBIT_S24_LE) + }, + .ops = &meson_i2s_dai_ops, +}; + +static const struct snd_soc_component_driver meson_i2s_dai_component = { + .name = DRV_NAME, +}; + +static int meson_i2s_dai_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct meson_i2s_dai *priv; + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + platform_set_drvdata(pdev, priv); + priv->core = dev_get_drvdata(dev->parent); + + priv->fast = devm_clk_get(dev, "fast"); + if (IS_ERR(priv->fast)) { + if (PTR_ERR(priv->fast) != -EPROBE_DEFER) + dev_err(dev, "Can't get the i2s fast domain clock\n"); + return PTR_ERR(priv->fast); + } + + priv->iface = devm_clk_get(dev, "iface"); + if (IS_ERR(priv->iface)) { + if (PTR_ERR(priv->iface) != -EPROBE_DEFER) + dev_err(dev, "Can't get i2s dai clock gate\n"); + return PTR_ERR(priv->iface); + } + + priv->bclks = devm_clk_get(dev, "bclks"); + if (IS_ERR(priv->bclks)) { + if (PTR_ERR(priv->bclks) != -EPROBE_DEFER) + dev_err(dev, "Can't get bit clocks gate\n"); + return PTR_ERR(priv->bclks); + } + + priv->mclk = devm_clk_get(dev, "mclk"); + if (IS_ERR(priv->mclk)) { + if (PTR_ERR(priv->mclk) != -EPROBE_DEFER) + dev_err(dev, "failed to get the i2s master clock\n"); + return PTR_ERR(priv->mclk); + } + + return devm_snd_soc_register_component(dev, &meson_i2s_dai_component, + &meson_i2s_dai, 1); +} + +static const struct of_device_id meson_i2s_dai_of_match[] = { + { .compatible = "amlogic,meson-i2s-dai", }, + { .compatible = "amlogic,meson-gxbb-i2s-dai", }, + { .compatible = "amlogic,meson-gxl-i2s-dai", }, + {} +}; +MODULE_DEVICE_TABLE(of, meson_i2s_dai_of_match); + +static struct platform_driver meson_i2s_dai_pdrv = { + .probe = meson_i2s_dai_probe, + .driver = { + .name = DRV_NAME, + .of_match_table = meson_i2s_dai_of_match, + }, +}; +module_platform_driver(meson_i2s_dai_pdrv); + +MODULE_DESCRIPTION("Meson i2s DAI ASoC Driver"); +MODULE_AUTHOR("Jerome Brunet "); +MODULE_LICENSE("GPL v2");