天天看點

Linux下的I2S驅動學習

轉載請注明出處:http://blog.csdn.net/gotowu/article/details/46329809

1、I2S概述

既然要學習I2S,就要想、首先知道他是幹什麼用的。

I2S(Inter—IC Sound)總線, 又稱 內建電路内置音頻總線,是飛利浦公司為數字音頻裝置之間的音頻資料傳輸而制定的一種總線标準,該總線專責于音頻裝置之間的資料傳輸,廣泛應用于各種多媒體系統。它采用了沿獨立的導線傳輸時鐘與資料信号的設計,通過将資料和時鐘信号分離,避免了因時差誘發的失真,為使用者節省了購買抵抗音頻抖動的專業裝置的費用。

2、I2S的總線規範

1.串行時鐘SCLK,也叫位時鐘(BCLK),即對應數字音頻的每一位資料,SCLK都有1個脈沖。SCLK的頻率=2×采樣頻率×采樣位數。

2. 幀時鐘LRCK,(也稱WS),用于切換左右聲道的資料。LRCK為“1”表示正在傳輸的是右聲道的資料,為“0”則表示正在傳輸的是左聲道的資料。LRCK的頻率等于采樣頻率。

3.串行資料SDATA,就是用二進制補碼表示的音頻資料。

3、I2S有4線,包括串行資料輸入IISDI,串行資料輸出IISDO,左右通道選擇IISLRCK,和穿行位時鐘IISCLK。生成IISLRCK和IISCLK的裝置是主裝置。

I2S驅動是作為接口驅動,供linux音頻驅動使用的,是以它的代碼中,必然要有音頻驅動的一些東西。分析的時候适當的結合一下音頻驅動就好看了。

下面來看看I2S驅動

使用平台裝置注冊IIS驅動。

module_platform_driver(s3c24xx_iis_driver);

module_platform_driver()宏的作用就是定義指定名稱的平台裝置驅動注冊函數和平台裝置驅動登出函數,并且在函數體内分别通過platform_driver_register()函數和platform_driver_unregister()函數注冊和登出該平台裝置驅動。

跟蹤到s3c24xx_iis_driver,發現主要是做了兩個事,一個是probe函數,還有一個就是remove。

static struct platform_driver s3c24xx_iis_driver = {
.probe  = s3c24xx_iis_dev_probe,
.remove = __devexit_p(s3c24xx_iis_dev_remove),
.driver = {
.name = "s3c24xx-iis",
.owner = THIS_MODULE,
},
};           

在s3c24xx_iis_dev_probe中,通過snd_soc_register_dai注冊一個ASOC核心的DAI接口,DAI是數字音頻接口。

static __devinit int s3c24xx_iis_dev_probe(struct platform_device *pdev)
{
return snd_soc_register_dai(&pdev->dev, &s3c24xx_i2s_dai);
}           

s3c24xx_iis_dev_remove與s3c24xx_iis_dev_probe相反,不在贅述。

snd_soc_register_dais函數顯示為每個snd_soc_dai執行個體配置設定記憶體,确定dai的名字,用snd_soc_dai_driver執行個體的字段對它進行必要初始化,最後把該dai連結到全局連結清單dai_list中。

dai驅動通常對應cpu的一個或幾個I2S/PCM接口,與snd_soc_platform一樣,dai驅動也是實作為一個platform driver,實作一個dai驅動大緻可以分為以下幾個步驟:

1.定義一個snd_soc_dai_driver結構的執行個體;

2.在對應的platform_driver中的probe回調中通過API:snd_soc_register_dai或者snd_soc_register_dais,注冊snd_soc_dai執行個體;

3.實作snd_soc_dai_driver結構中的probe、suspend等回調;

4.實作snd_soc_dai_driver結構中的snd_soc_dai_ops字段中的回調函數;

snd_soc_register_dai  

定義一個snd_soc_dai_driver結構

static struct snd_soc_dai_driver s3c24xx_i2s_dai = {
.probe = s3c24xx_i2s_probe,
.suspend = s3c24xx_i2s_suspend,
.resume = s3c24xx_i2s_resume,
.playback = {                          //播放
.channels_min = 2,
.channels_max = 2,
.rates = S3C24XX_I2S_RATES,
.formats = SNDRV_PCM_FMTBIT_S8 | SNDRV_PCM_FMTBIT_S16_LE,},
.capture = {                         //錄音
.channels_min = 2,
.channels_max = 2,
.rates = S3C24XX_I2S_RATES,
.formats = SNDRV_PCM_FMTBIT_S8 | SNDRV_PCM_FMTBIT_S16_LE,},
.ops = &s3c24xx_i2s_dai_ops,
};           

主要包括s3c24xx_i2s_probe,和 &s3c24xx_i2s_dai_ops,

對s3c24xx_i2s_probe進行填充

static int s3c24xx_i2s_probe(struct snd_soc_dai *dai)
{
pr_debug("Entered %s\n", __func__);
 
s3c24xx_i2s.regs = ioremap(S3C2410_PA_IIS, 0x100);
if (s3c24xx_i2s.regs == NULL)
return -ENXIO;
 
s3c24xx_i2s.iis_clk = clk_get(dai->dev, "iis");
if (IS_ERR(s3c24xx_i2s.iis_clk)) {
pr_err("failed to get iis_clock\n");
iounmap(s3c24xx_i2s.regs);
return PTR_ERR(s3c24xx_i2s.iis_clk);
}
clk_enable(s3c24xx_i2s.iis_clk);
 
/* 配置I2S的管腳在正确的模式 */
s3c2410_gpio_cfgpin(S3C2410_GPE0, S3C2410_GPE0_I2SLRCK);
s3c2410_gpio_cfgpin(S3C2410_GPE1, S3C2410_GPE1_I2SSCLK);
s3c2410_gpio_cfgpin(S3C2410_GPE2, S3C2410_GPE2_CDCLK);
s3c2410_gpio_cfgpin(S3C2410_GPE3, S3C2410_GPE3_I2SSDI);
s3c2410_gpio_cfgpin(S3C2410_GPE4, S3C2410_GPE4_I2SSDO);
 
writel(S3C2410_IISCON_IISEN, s3c24xx_i2s.regs + S3C2410_IISCON);
 
s3c24xx_snd_txctrl(0);
s3c24xx_snd_rxctrl(0);
 
return 0;
}           

對于ioremap,Ioremap宏定義在asm/io.h内。要通路s3c2410平台上的I2S寄存器, 檢視datasheet 知道IIS實體位址為0x55000000,我們把它定義為宏S3C2410_PA_IIS,如下:

#define S3C2410_PA_IIS    (0x55000000)

若要在核心空間(iis驅動)中通路這段I/O寄存器(IIS)資源需要先建立到核心位址空間的映射:

s3c24xx_i2s.regs = ioremap(S3C2410_PA_IIS, 0x100);

建立好了之後,我們就可以通過readl(s3c24xx_i2s.regs)或writel(value, s3c24xx_i2s.regs)等IO接口函數去通路它。

再看s3c24xx_i2s_dai_ops

static const struct snd_soc_dai_ops s3c24xx_i2s_dai_ops = {
.trigger= s3c24xx_i2s_trigger,            //觸發
.hw_params= s3c24xx_i2s_hw_params,    //設定硬體參數
.set_fmt= s3c24xx_i2s_set_fmt,           //設定dai的格式
.set_clkdiv= s3c24xx_i2s_set_clkdiv,     //設定分頻系數
.set_sysclk= s3c24xx_i2s_set_sysclk,    //設定dai的主時鐘
};           

首先來看看I2S的觸發,有六種情況

static int s3c24xx_i2s_trigger(struct snd_pcm_substream *substream, int cmd,
       struct snd_soc_dai *dai)
{
int ret = 0;
struct s3c_dma_params *dma_data =
snd_soc_dai_get_dma_data(dai, substream);  //DMA擷取到的資料是播放的還是錄音
 
pr_debug("Entered %s\n", __func__);
 
switch (cmd) {
case SNDRV_PCM_TRIGGER_START: //此處case後無語句,則即便cmd為SNDRV_PCM_TRIGGER_START
case SNDRV_PCM_TRIGGER_RESUME: //依然會一直執行後面的語句,直到出現break
case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
if (!s3c24xx_snd_is_clkmaster()) {
ret = s3c24xx_snd_lrsync();
if (ret)
goto exit_err;
}
 
if (substream->stream == SNDRV_PCM_STREAM_CAPTURE)
s3c24xx_snd_rxctrl(1);         //錄音
else
s3c24xx_snd_txctrl(1);         //播放
 
s3c2410_dma_ctrl(dma_data->channel, S3C2410_DMAOP_STARTED); //改變信道的狀态
break;
case SNDRV_PCM_TRIGGER_STOP:
case SNDRV_PCM_TRIGGER_SUSPEND:
case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
if (substream->stream == SNDRV_PCM_STREAM_CAPTURE)
s3c24xx_snd_rxctrl(0);
else
s3c24xx_snd_txctrl(0);
break;
default:
ret = -EINVAL;
break;
}
 
exit_err:
return ret;
}           

在看s3c24xx_i2s_hw_params

static int s3c24xx_i2s_hw_params(struct snd_pcm_substream *substream,
 struct snd_pcm_hw_params *params,
 struct snd_soc_dai *dai)
{
struct snd_soc_pcm_runtime *rtd = substream->private_data;
struct s3c_dma_params *dma_data;
u32 iismod;
 
pr_debug("Entered %s\n", __func__);
 
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
dma_data = &s3c24xx_i2s_pcm_stereo_out;
else
dma_data = &s3c24xx_i2s_pcm_stereo_in;
 
snd_soc_dai_set_dma_data(rtd->cpu_dai, substream, dma_data);  
 
/* Working copies of register */
iismod = readl(s3c24xx_i2s.regs + S3C2410_IISMOD);
pr_debug("hw_params r: IISMOD: %x\n", iismod);
 
switch (params_format(params)) {
case SNDRV_PCM_FORMAT_S8:
iismod &= ~S3C2410_IISMOD_16BIT;
dma_data->dma_size = 1;
break;
case SNDRV_PCM_FORMAT_S16_LE:
iismod |= S3C2410_IISMOD_16BIT;
dma_data->dma_size = 2;
break;
default:
return -EINVAL;
}           

snd_soc_dai_set_dma_data(rtd->cpu_dai, substream, dma_data);通過(substream->stream == SNDRV_PCM_STREAM_PLAYBACK)進行判斷是錄音還是播放。

通過switch (params_format(params))進行判斷是選擇8位還是16位的傳輸通道,并設定相應的IISMOD和dma寬度。

設定傳輸的格式和主從模式的選擇。

static int s3c24xx_i2s_set_fmt(struct snd_soc_dai *cpu_dai,
unsigned int fmt)
{
u32 iismod;
 
pr_debug("Entered %s\n", __func__);
 
iismod = readl(s3c24xx_i2s.regs + S3C2410_IISMOD);
pr_debug("hw_params r: IISMOD: %x \n", iismod);
 
switch (fmt & SND_SOC_DAIFMT_MASTER_MASK) {
case SND_SOC_DAIFMT_CBM_CFM:                             //設定主or從模式當IISMOD的
iismod |= S3C2410_IISMOD_SLAVE;                   //第8位為0時是master mode
break;                                                                 //為1時是slave mode
case SND_SOC_DAIFMT_CBS_CFS:
iismod &= ~S3C2410_IISMOD_SLAVE;
break;
default:
return -EINVAL;
}
 
switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) {
case SND_SOC_DAIFMT_LEFT_J:
iismod |= S3C2410_IISMOD_MSB;        //#define S3C2410_IISMOD_MSB(1 << 4)               
break;                                                   //IISMOD的第4位為1時是 MSB (Left)-justified format
case SND_SOC_DAIFMT_I2S:                        //IISMOD的第4位為0時是IIS compatible format
iismod &= ~S3C2410_IISMOD_MSB;
break;
default:
return -EINVAL;
}
 
writel(iismod, s3c24xx_i2s.regs + S3C2410_IISMOD);    //将IISMOD的狀态寫入IISMOD的位址
pr_debug("hw_params w: IISMOD: %x \n", iismod);
return 0;
}           

下面讓我們再來看看I2S對時鐘分頻的操作

首先主裝置時鐘頻率MCLK=PCLK/預分頻值,I2SLRCK頻率=主裝置時鐘頻率/CODECLK,

CODECLK的采樣頻率類型為256fs和384fs。

串行為采用頻率BCLK類型有16/32/48fs,可以通過設定串行位數和CODECLK采樣頻率完成。

串行位時鐘頻率=CODECLK的采用類型/串行資料位數

static int s3c24xx_i2s_set_clkdiv(struct snd_soc_dai *cpu_dai,
int div_id, int div)
{
u32 reg;
 
pr_debug("Entered %s\n", __func__);
 
switch (div_id) {
case S3C24XX_DIV_BCLK:                          //串行時鐘頻率
reg = readl(s3c24xx_i2s.regs + S3C2410_IISMOD) & ~S3C2410_IISMOD_FS_MASK;
writel(reg | div, s3c24xx_i2s.regs + S3C2410_IISMOD);
break;
case S3C24XX_DIV_MCLK:                           //主時鐘頻率
reg = readl(s3c24xx_i2s.regs + S3C2410_IISMOD) & ~(S3C2410_IISMOD_384FS);
writel(reg | div, s3c24xx_i2s.regs + S3C2410_IISMOD);
break;
case S3C24XX_DIV_PRESCALER:                //預分頻值設定
writel(div, s3c24xx_i2s.regs + S3C2410_IISPSR);
reg = readl(s3c24xx_i2s.regs + S3C2410_IISCON);
writel(reg | S3C2410_IISCON_PSCEN, s3c24xx_i2s.regs + S3C2410_IISCON);
break;
default:
return -EINVAL;
}
 
return 0;
}           
Linux下的I2S驅動學習

reg | S3C2410_IISCON_PSCEN使能預分頻

Linux下的I2S驅動學習

下面我們來設定系統的時鐘的時鐘源

static int s3c24xx_i2s_set_sysclk(struct snd_soc_dai *cpu_dai,
int clk_id, unsigned int freq, int dir)
{
u32 iismod = readl(s3c24xx_i2s.regs + S3C2410_IISMOD);    //讀取IISMOD的狀态
 
pr_debug("Entered %s\n", __func__);
 
iismod &= ~S3C2440_IISMOD_MPLL;          //初始化選擇PCLK 外設時鐘
 
switch (clk_id) {
case S3C24XX_CLKSRC_PCLK:  //如果clk_id為0,則使用PCLK
break;
case S3C24XX_CLKSRC_MPLL:  //如果clk_id為1,則選擇MPLL
iismod |= S3C2440_IISMOD_MPLL;
break;
default:
return -EINVAL;
}
 
writel(iismod, s3c24xx_i2s.regs + S3C2410_IISMOD);
return 0;
}           

這裡要解釋一下MPLL(phase locked loop),S3C2440有兩個PLL(phase locked loop)一個是MPLL,一個是UPLL。MPLL用于CPU及其他外圍器件,UPLL用于USB。從S3C2440的DATASHEET裡可以看到,S3C2440最大支援400MHz的主頻,但是這并不意味着一定工作在400MHz下面,可以通過設定MPLL, UPLL寄存器來設定CPU的工作頻率。 

到此 s3c24xx_i2s_dai_ops的分析結束。

繼續閱讀