| /* |
| * sound/soc/ambarella_i2s.c |
| * |
| * History: |
| * 2008/03/03 - [Eric Lee] created file |
| * 2008/04/16 - [Eric Lee] Removed the compiling warning |
| * 2009/01/22 - [Anthony Ginger] Port to 2.6.28 |
| * 2009/03/05 - [Cao Rongrong] Update from 2.6.22.10 |
| * 2009/06/10 - [Cao Rongrong] Port to 2.6.29 |
| * 2009/06/29 - [Cao Rongrong] Support more mclk and fs |
| * 2010/10/25 - [Cao Rongrong] Port to 2.6.36+ |
| * 2011/03/20 - [Cao Rongrong] Port to 2.6.38 |
| * |
| * Copyright (C) 2004-2009, Ambarella, Inc. |
| * |
| * 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, write to the Free Software |
| * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| * |
| */ |
| #include <linux/kernel.h> |
| #include <linux/module.h> |
| #include <linux/init.h> |
| #include <linux/slab.h> |
| #include <linux/delay.h> |
| #include <linux/dma-mapping.h> |
| |
| #include <sound/core.h> |
| #include <sound/pcm.h> |
| #include <sound/pcm_params.h> |
| #include <sound/soc.h> |
| |
| #include <mach/hardware.h> |
| #include <plat/audio.h> |
| |
| #include "ambarella_pcm.h" |
| #include "ambarella_i2s.h" |
| |
| /* |
| * The I2S ports are mux with the GPIOs. |
| * A3, A5, A5S and A6 support 5.1(6) channels, so we need to |
| * select the proper port to used and free the unused pins (GPIOs) |
| * for other usage. |
| * 1: 2 channels |
| * 2: 4 channels |
| * 3: 6 channels |
| */ |
| unsigned int used_port = 1; |
| module_param(used_port, uint, S_IRUGO); |
| MODULE_PARM_DESC(used_port, "Select the I2S port."); |
| |
| unsigned int default_sfreq = 0; |
| module_param(default_sfreq, uint, S_IRUGO); |
| MODULE_PARM_DESC(default_sfreq, "Default sfreq: 0. 44100, 1. 48000."); |
| |
| static DEFINE_MUTEX(clock_reg_mutex); |
| static int enable_ext_i2s = 1; |
| |
| static u32 DAI_Clock_Divide_Table[MAX_OVERSAMPLE_IDX_NUM][2] = { |
| { 1, 0 }, // 128xfs |
| { 3, 1 }, // 256xfs |
| { 5, 2 }, // 384xfs |
| { 7, 3 }, // 512xfs |
| { 11, 5 }, // 768xfs |
| { 15, 7 }, // 1024xfs |
| { 17, 8 }, // 1152xfs |
| { 23, 11 }, // 1536xfs |
| { 35, 17 } // 2304xfs |
| }; |
| |
| |
| /* FIXME HERE for PCM interface */ |
| static struct ambarella_pcm_dma_params ambarella_i2s_pcm_stereo_out = { |
| .name = "I2S PCM Stereo out", |
| .dev_addr = I2S_TX_LEFT_DATA_DMA_REG, |
| }; |
| |
| static struct ambarella_pcm_dma_params ambarella_i2s_pcm_stereo_in = { |
| .name = "I2S PCM Stereo in", |
| .dev_addr = I2S_RX_DATA_DMA_REG, |
| }; |
| |
| static inline void dai_tx_enable(void) |
| { |
| amba_setbitsl(I2S_INIT_REG, DAI_TX_EN); |
| } |
| |
| static inline void dai_rx_enable(void) |
| { |
| amba_setbitsl(I2S_INIT_REG, DAI_RX_EN); |
| } |
| |
| static inline void dai_tx_disable(void) |
| { |
| amba_clrbitsl(I2S_INIT_REG, DAI_TX_EN); |
| } |
| |
| static inline void dai_rx_disable(void) |
| { |
| amba_clrbitsl(I2S_INIT_REG, DAI_RX_EN); |
| } |
| |
| static inline void dai_fifo_rst(void) |
| { |
| amba_setbitsl(I2S_INIT_REG, DAI_FIFO_RST); |
| msleep(1); |
| if (amba_tstbitsl(I2S_INIT_REG, DAI_FIFO_RST)) { |
| printk("DAI_FIFO_RST fail!\n"); |
| } |
| } |
| |
| static int ambarella_i2s_hw_params(struct snd_pcm_substream *substream, |
| struct snd_pcm_hw_params *params, |
| struct snd_soc_dai *cpu_dai) |
| { |
| struct ambarella_pcm_dma_params *dma_data; |
| struct amb_i2s_priv *priv_data = snd_soc_dai_get_drvdata(cpu_dai); |
| u8 slots, word_pos, clksrc, mclk, oversample; |
| u8 rx_enabled = 0, tx_enabled = 0; |
| u32 clock_divider, clock_reg, channels; |
| |
| if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { |
| dma_data = &ambarella_i2s_pcm_stereo_out; |
| /* Disable tx/rx before initializing */ |
| dai_tx_disable(); |
| if(amba_tstbitsl(I2S_INIT_REG, 0x02)) { |
| rx_enabled = 1; |
| dai_rx_disable(); |
| } |
| if (priv_data->amb_i2s_intf.ms_mode == DAI_SLAVE) |
| amba_writel(I2S_TX_CTRL_REG, 0x08); |
| else |
| amba_writel(I2S_TX_CTRL_REG, 0x28); |
| amba_writel(I2S_TX_FIFO_LTH_REG, 0x10); |
| } else { |
| dma_data = &ambarella_i2s_pcm_stereo_in; |
| /* Disable tx/rx before initializing */ |
| dai_rx_disable(); |
| if(amba_tstbitsl(I2S_INIT_REG, 0x04)) { |
| tx_enabled = 1; |
| dai_tx_disable(); |
| } |
| if (priv_data->amb_i2s_intf.ms_mode == DAI_SLAVE) |
| amba_writel(I2S_RX_CTRL_REG, 0x00); |
| else |
| amba_writel(I2S_RX_CTRL_REG, 0x02); |
| amba_writel(I2S_RX_FIFO_GTH_REG, 0x40); |
| } |
| |
| snd_soc_dai_set_dma_data(cpu_dai, substream, dma_data); |
| |
| /* Set channels */ |
| channels = params_channels(params); |
| if (priv_data->controller_info->channel_select) |
| priv_data->controller_info->channel_select(channels); |
| priv_data->amb_i2s_intf.ch = channels; |
| |
| /* Set format */ |
| switch (params_format(params)) { |
| case SNDRV_PCM_FORMAT_S16_LE: |
| if (priv_data->amb_i2s_intf.mode == DAI_DSP_Mode) { |
| slots = channels - 1; |
| word_pos = 0x0f; |
| priv_data->amb_i2s_intf.slots = slots; |
| } else { |
| slots = 0; |
| word_pos = 0; |
| priv_data->amb_i2s_intf.slots = DAI_32slots; |
| } |
| priv_data->amb_i2s_intf.word_len = DAI_16bits; |
| priv_data->amb_i2s_intf.word_pos = word_pos; |
| priv_data->amb_i2s_intf.word_order = DAI_MSB_FIRST; |
| |
| amba_writel(I2S_MODE_REG, priv_data->amb_i2s_intf.mode); |
| amba_writel(I2S_WLEN_REG, 0x0f); |
| amba_writel(I2S_WPOS_REG, word_pos); |
| amba_writel(I2S_SLOT_REG, slots); |
| amba_writel(I2S_24BITMUX_MODE_REG, 0); |
| |
| break; |
| default: |
| return -EINVAL; |
| } |
| |
| switch (params_rate(params)) { |
| case 8000: |
| priv_data->amb_i2s_intf.sfreq = AUDIO_SF_48000; |
| break; |
| case 11025: |
| priv_data->amb_i2s_intf.sfreq = AUDIO_SF_11025; |
| break; |
| case 16000: |
| priv_data->amb_i2s_intf.sfreq = AUDIO_SF_16000; |
| break; |
| case 22050: |
| priv_data->amb_i2s_intf.sfreq = AUDIO_SF_22050; |
| break; |
| case 32000: |
| priv_data->amb_i2s_intf.sfreq = AUDIO_SF_32000; |
| break; |
| case 44100: |
| priv_data->amb_i2s_intf.sfreq = AUDIO_SF_44100; |
| break; |
| case 48000: |
| priv_data->amb_i2s_intf.sfreq = AUDIO_SF_48000; |
| break; |
| default: |
| return -EINVAL; |
| } |
| |
| /* Set clock */ |
| clksrc = priv_data->amb_i2s_intf.clksrc; |
| mclk = priv_data->amb_i2s_intf.mclk; |
| if (priv_data->controller_info->set_audio_pll) |
| priv_data->controller_info->set_audio_pll(clksrc, mclk); |
| |
| mutex_lock(&clock_reg_mutex); |
| clock_reg = amba_readl(I2S_CLOCK_REG); |
| oversample = priv_data->amb_i2s_intf.oversample; |
| clock_divider = DAI_Clock_Divide_Table[oversample][slots >> 6]; |
| clock_reg &= ~DAI_CLOCK_MASK; |
| clock_reg |= clock_divider; |
| if (priv_data->amb_i2s_intf.ms_mode == DAI_MASTER) |
| clock_reg |= I2S_CLK_MASTER_MODE; |
| else |
| clock_reg &= (~I2S_CLK_MASTER_MODE); |
| /* Disable output BCLK and LRCLK to disable external codec */ |
| if (enable_ext_i2s == 0) |
| clock_reg &= ~(I2S_CLK_WS_OUT_EN | I2S_CLK_BCLK_OUT_EN); |
| |
| amba_writel(I2S_CLOCK_REG, clock_reg); |
| mutex_unlock(&clock_reg_mutex); |
| |
| if(!amba_tstbitsl(I2S_INIT_REG, 0x6)) |
| dai_fifo_rst(); |
| |
| if(rx_enabled) |
| dai_rx_enable(); |
| if(tx_enabled) |
| dai_tx_enable(); |
| |
| msleep(1); |
| |
| /* Notify HDMI that the audio interface is changed */ |
| ambarella_audio_notify_transition(&priv_data->amb_i2s_intf, |
| AUDIO_NOTIFY_SETHWPARAMS); |
| |
| return 0; |
| } |
| |
| static int ambarella_i2s_prepare(struct snd_pcm_substream *substream, |
| struct snd_soc_dai *dai) |
| { |
| if(substream->stream == SNDRV_PCM_STREAM_CAPTURE) |
| dai_fifo_rst(); |
| return 0; |
| } |
| |
| static int ambarella_i2s_trigger(struct snd_pcm_substream *substream, int cmd, |
| struct snd_soc_dai *dai) |
| { |
| int ret = 0; |
| |
| switch (cmd) { |
| case SNDRV_PCM_TRIGGER_START: |
| case SNDRV_PCM_TRIGGER_RESUME: |
| case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: |
| if (substream->stream == SNDRV_PCM_STREAM_CAPTURE) { |
| dai_rx_enable(); |
| } else { |
| if(alsa_tx_enable_flag == 0) |
| dai_tx_enable(); |
| } |
| break; |
| case SNDRV_PCM_TRIGGER_STOP: |
| case SNDRV_PCM_TRIGGER_SUSPEND: |
| case SNDRV_PCM_TRIGGER_PAUSE_PUSH: |
| if (substream->stream == SNDRV_PCM_STREAM_CAPTURE) { |
| //dai_rx_disable(); |
| //Stop by DMA EOC |
| } else { |
| //dai_tx_disable(); |
| //Stop by DMA EOC |
| } |
| break; |
| default: |
| ret = -EINVAL; |
| break; |
| } |
| |
| return ret; |
| } |
| |
| /* |
| * Set Ambarella I2S DAI format |
| */ |
| static int ambarella_i2s_set_fmt(struct snd_soc_dai *cpu_dai, |
| unsigned int fmt) |
| { |
| struct amb_i2s_priv *priv_data = snd_soc_dai_get_drvdata(cpu_dai); |
| |
| switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) { |
| case SND_SOC_DAIFMT_LEFT_J: |
| priv_data->amb_i2s_intf.mode = DAI_leftJustified_Mode; |
| break; |
| case SND_SOC_DAIFMT_RIGHT_J: |
| priv_data->amb_i2s_intf.mode = DAI_rightJustified_Mode; |
| break; |
| case SND_SOC_DAIFMT_I2S: |
| priv_data->amb_i2s_intf.mode = DAI_I2S_Mode; |
| break; |
| case SND_SOC_DAIFMT_DSP_A: |
| priv_data->amb_i2s_intf.mode = DAI_DSP_Mode; |
| break; |
| default: |
| return -EINVAL; |
| } |
| |
| switch (fmt & SND_SOC_DAIFMT_MASTER_MASK) { |
| case SND_SOC_DAIFMT_CBS_CFS: |
| priv_data->amb_i2s_intf.ms_mode = DAI_MASTER; |
| break; |
| case SND_SOC_DAIFMT_CBM_CFM: |
| if (priv_data->amb_i2s_intf.mode != DAI_I2S_Mode) { |
| printk("DAI can't work in slave mode without standard I2S format!\n"); |
| return -EINVAL; |
| } |
| priv_data->amb_i2s_intf.ms_mode = DAI_SLAVE; |
| break; |
| default: |
| return -EINVAL; |
| } |
| |
| return 0; |
| } |
| |
| static int ambarella_i2s_set_sysclk(struct snd_soc_dai *cpu_dai, |
| int clk_id, unsigned int freq, int dir) |
| { |
| struct amb_i2s_priv *priv_data = snd_soc_dai_get_drvdata(cpu_dai); |
| |
| switch (clk_id) { |
| case AMBARELLA_CLKSRC_ONCHIP: |
| case AMBARELLA_CLKSRC_EXTERNAL: |
| priv_data->amb_i2s_intf.clksrc = clk_id; |
| priv_data->amb_i2s_intf.mclk = freq; |
| break; |
| default: |
| printk("CLK SOURCE (%d) is not supported yet\n", clk_id); |
| return -EINVAL; |
| } |
| |
| return 0; |
| } |
| |
| static int ambarella_i2s_set_clkdiv(struct snd_soc_dai *cpu_dai, |
| int div_id, int div) |
| { |
| struct amb_i2s_priv *priv_data = snd_soc_dai_get_drvdata(cpu_dai); |
| |
| switch (div_id) { |
| case AMBARELLA_CLKDIV_LRCLK: |
| priv_data->amb_i2s_intf.oversample = div; |
| break; |
| default: |
| return -EINVAL; |
| } |
| |
| return 0; |
| } |
| |
| static int external_i2s_get_status(struct snd_kcontrol *kcontrol, |
| struct snd_ctl_elem_value *ucontrol) |
| { |
| mutex_lock(&clock_reg_mutex); |
| ucontrol->value.integer.value[0] = enable_ext_i2s; |
| mutex_unlock(&clock_reg_mutex); |
| |
| return 0; |
| } |
| |
| static int external_i2s_set_status(struct snd_kcontrol *kcontrol, |
| struct snd_ctl_elem_value *ucontrol) |
| { |
| u32 clock_reg; |
| |
| mutex_lock(&clock_reg_mutex); |
| clock_reg = amba_readl(I2S_CLOCK_REG); |
| |
| enable_ext_i2s = ucontrol->value.integer.value[0]; |
| if (enable_ext_i2s) |
| clock_reg |= (I2S_CLK_WS_OUT_EN | I2S_CLK_BCLK_OUT_EN); |
| else |
| clock_reg &= ~(I2S_CLK_WS_OUT_EN | I2S_CLK_BCLK_OUT_EN); |
| |
| amba_writel(I2S_CLOCK_REG, clock_reg); |
| mutex_unlock(&clock_reg_mutex); |
| |
| return 1; |
| } |
| |
| static const char *i2s_status_str[] = {"Off", "On"}; |
| |
| static const struct soc_enum external_i2s_enum[] = { |
| SOC_ENUM_SINGLE_EXT(ARRAY_SIZE(i2s_status_str), i2s_status_str), |
| }; |
| |
| static const struct snd_kcontrol_new external_i2s_controls[] = { |
| SOC_ENUM_EXT("External I2S Switch", external_i2s_enum[0], |
| external_i2s_get_status, external_i2s_set_status), |
| }; |
| |
| int ambarella_i2s_add_controls(struct snd_soc_codec *codec) |
| { |
| return snd_soc_add_controls(codec, external_i2s_controls, |
| ARRAY_SIZE(external_i2s_controls)); |
| } |
| EXPORT_SYMBOL_GPL(ambarella_i2s_add_controls); |
| |
| |
| #ifdef CONFIG_PM |
| static int ambarella_i2s_dai_suspend(struct snd_soc_dai *dai) |
| { |
| struct amb_i2s_priv *priv_data = snd_soc_dai_get_drvdata(dai); |
| |
| priv_data->clock_reg = amba_readl(I2S_CLOCK_REG); |
| |
| return 0; |
| } |
| |
| |
| |
| static int ambarella_i2s_dai_resume(struct snd_soc_dai *dai) |
| { |
| struct amb_i2s_priv *priv_data = snd_soc_dai_get_drvdata(dai); |
| |
| amba_writel(I2S_CLOCK_REG, priv_data->clock_reg); |
| |
| return 0; |
| } |
| #else /* CONFIG_PM */ |
| #define ambarella_i2s_dai_suspend NULL |
| #define ambarella_i2s_dai_resume NULL |
| #endif /* CONFIG_PM */ |
| |
| |
| |
| static int ambarella_i2s_dai_probe(struct snd_soc_dai *dai) |
| { |
| u32 mclk, sfreq; |
| u32 clock_divider, clock_reg; |
| struct amb_i2s_priv *priv_data = snd_soc_dai_get_drvdata(dai); |
| |
| if (default_sfreq == 0) { |
| mclk = AudioCodec_11_2896M; |
| sfreq = AUDIO_SF_44100; |
| } else { |
| mclk = AudioCodec_12_288M; |
| sfreq = AUDIO_SF_48000; |
| } |
| |
| /* Disable internal codec (only for A2) except IPcam Board */ |
| if (strcmp(dai->card->name, "A2IPcam")) |
| clock_reg = 0x400; |
| |
| if (priv_data->controller_info->set_audio_pll) |
| priv_data->controller_info->set_audio_pll(AMBARELLA_CLKSRC_ONCHIP, mclk); |
| |
| /* Dai default smapling rate, polarity configuration. |
| * Note: Just be configured, actually BCLK and LRCLK will not |
| * output to outside at this time. */ |
| clock_divider = DAI_Clock_Divide_Table[AudioCodec_256xfs][DAI_32slots >> 6]; |
| clock_reg |= clock_divider | I2S_CLK_TX_PO_FALL; |
| amba_writel(I2S_CLOCK_REG, clock_reg); |
| |
| priv_data->amb_i2s_intf.mode = DAI_I2S_Mode; |
| priv_data->amb_i2s_intf.clksrc = AMBARELLA_CLKSRC_ONCHIP; |
| priv_data->amb_i2s_intf.ms_mode = DAI_MASTER; |
| priv_data->amb_i2s_intf.mclk = mclk; |
| priv_data->amb_i2s_intf.oversample = AudioCodec_256xfs; |
| priv_data->amb_i2s_intf.word_order = DAI_MSB_FIRST; |
| priv_data->amb_i2s_intf.sfreq = sfreq; |
| priv_data->amb_i2s_intf.word_len = DAI_16bits; |
| priv_data->amb_i2s_intf.word_pos = 0; |
| priv_data->amb_i2s_intf.slots = DAI_32slots; |
| priv_data->amb_i2s_intf.ch = 2; |
| |
| /* Notify HDMI that the audio interface is initialized */ |
| ambarella_audio_notify_transition(&priv_data->amb_i2s_intf, AUDIO_NOTIFY_INIT); |
| |
| return 0; |
| } |
| |
| static int ambarella_i2s_dai_remove(struct snd_soc_dai *dai) |
| { |
| struct amb_i2s_priv *priv_data = snd_soc_dai_get_drvdata(dai); |
| |
| /* Disable I2S clock output */ |
| amba_writel(I2S_CLOCK_REG, 0x0); |
| |
| /* Notify that the audio interface is removed */ |
| ambarella_audio_notify_transition(&priv_data->amb_i2s_intf, |
| AUDIO_NOTIFY_REMOVE); |
| |
| return 0; |
| } |
| |
| static struct snd_soc_dai_ops ambarella_i2s_dai_ops = { |
| .prepare = ambarella_i2s_prepare, |
| .trigger = ambarella_i2s_trigger, |
| .hw_params = ambarella_i2s_hw_params, |
| .set_fmt = ambarella_i2s_set_fmt, |
| .set_sysclk = ambarella_i2s_set_sysclk, |
| .set_clkdiv = ambarella_i2s_set_clkdiv, |
| }; |
| |
| static struct snd_soc_dai_driver ambarella_i2s_dai = { |
| .probe = ambarella_i2s_dai_probe, |
| .remove = ambarella_i2s_dai_remove, |
| .suspend = ambarella_i2s_dai_suspend, |
| .resume = ambarella_i2s_dai_resume, |
| .playback = { |
| .channels_min = 2, |
| .channels_max = 0, // initialized in ambarella_i2s_probe function |
| .rates = SNDRV_PCM_RATE_8000_48000, |
| .formats = SNDRV_PCM_FMTBIT_S16_LE, |
| }, |
| .capture = { |
| .channels_min = 2, |
| .channels_max = 0, // initialized in ambarella_i2s_probe function |
| .rates = SNDRV_PCM_RATE_8000_48000, |
| .formats = SNDRV_PCM_FMTBIT_S16_LE, |
| }, |
| .ops = &ambarella_i2s_dai_ops, |
| .symmetric_rates = 1, |
| }; |
| |
| static int __devinit ambarella_i2s_probe(struct platform_device *pdev) |
| { |
| struct amb_i2s_priv *priv_data; |
| |
| priv_data = kzalloc(sizeof(struct amb_i2s_priv), GFP_KERNEL); |
| if (priv_data == NULL) |
| return -ENOMEM; |
| |
| priv_data->controller_info = pdev->dev.platform_data; |
| /* aucodec_digitalio_on */ |
| switch(used_port) { |
| case 3: |
| ambarella_i2s_dai.playback.channels_max += 2; |
| ambarella_i2s_dai.capture.channels_max += 2; |
| if (priv_data->controller_info->aucodec_digitalio_2) |
| priv_data->controller_info->aucodec_digitalio_2(); |
| |
| case 2: |
| ambarella_i2s_dai.playback.channels_max += 2; |
| ambarella_i2s_dai.capture.channels_max += 2; |
| if (priv_data->controller_info->aucodec_digitalio_1) |
| priv_data->controller_info->aucodec_digitalio_1(); |
| |
| case 1: |
| ambarella_i2s_dai.playback.channels_max += 2; |
| ambarella_i2s_dai.capture.channels_max += 2; |
| if (priv_data->controller_info->aucodec_digitalio_0) |
| priv_data->controller_info->aucodec_digitalio_0(); |
| break; |
| |
| default: |
| printk("%s: Need to select proper I2S port.\n", __func__); |
| return -EINVAL; |
| } |
| |
| dev_set_drvdata(&pdev->dev, priv_data); |
| |
| return snd_soc_register_dai(&pdev->dev, &ambarella_i2s_dai); |
| } |
| |
| static int __devexit ambarella_i2s_remove(struct platform_device *pdev) |
| { |
| struct amb_i2s_priv *priv_data = dev_get_drvdata(&pdev->dev); |
| |
| snd_soc_unregister_dai(&pdev->dev); |
| kfree(priv_data); |
| |
| return 0; |
| } |
| |
| static struct platform_driver ambarella_i2s_driver = { |
| .probe = ambarella_i2s_probe, |
| .remove = __devexit_p(ambarella_i2s_remove), |
| |
| .driver = { |
| .name = "ambarella-i2s", |
| .owner = THIS_MODULE, |
| }, |
| }; |
| |
| |
| static int __init ambarella_i2s_init(void) |
| { |
| return platform_driver_register(&ambarella_i2s_driver); |
| } |
| module_init(ambarella_i2s_init); |
| |
| static void __exit ambarella_i2s_exit(void) |
| { |
| platform_driver_unregister(&ambarella_i2s_driver); |
| } |
| module_exit(ambarella_i2s_exit); |
| |
| MODULE_AUTHOR("Cao Rongrong <rrcao@ambarella.com>"); |
| MODULE_DESCRIPTION("Ambarella Soc I2S Interface"); |
| |
| MODULE_LICENSE("GPL"); |
| |