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;
}
reg | S3C2410_IISCON_PSCEN使能預分頻
下面我們來設定系統的時鐘的時鐘源
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的分析結束。