Asoc框架是linux内核采用的一种音频子系统框架。他在alsa框架的基础上又封装了一层。实际上就把asoc移植看成alsa移植即可。(虽然2个我都不太懂,初学asoc驱动)
在对实际文件分析后我把alsa框架需要修改的主要为如下几类:
SMDK_xxxx.c文件 --> Machine
I2C子系统驱动 -->codec
I2S控制器驱动
其中I2S控制器驱动和I2C子系统驱动由内核已经给出。I2S驱动主要是设置I2S相关的寄存器,便于ASOC其他部分(框架理解不是很透彻,这里可以看成PCM数据发送)发送数据。
而I2C子系统驱动则负责配置WM8960的寄存器。SMDK.c文件主要是负责将asoc各个部分通过一个结构体描述连接起来。他们都是由asoc更上层的结构进行调用的。
对于我们移植来说,主要是关心SMDK_xxxx.c这样的Machine部分的文件。如果内核没有的话,就需要自己动手写了,我这里是没有的,所以我去找了个S3c2451的WM8960的Machine文件,接下来说下修改步骤。
想要正确播放出声音,第一步就需要修改machine中的dai结构体:
下面只说下主要的成员:
Cpu_dai_name 对应i2s控制器驱动
Codec_dai_name 对应于内核提供的codec文件中的同名dai 图3 给出
Platform_name 用于数据传输的平台名称 (我猜的 I2SDMA?)
Codec_name 对应于控制codec的 i2c驱动
ops 主要成员是.hw_params变量,当通过alsa-util测试播放的时候,应用会通过 这个函数传入一些控制参数来设置i2s和wm8960。 通过 图2 划线的2个参数就能对应到codec和i2s各自的控制函数当中。分别进行设置。参数则通过查看他们对应的函数就能得出。
图2
图3
修改后若使用默认的设置无法播放,则需要检查设置让关键的寄存器值变正确。
IIS设置主要要设置时钟源选I2SCLK,通过EPLL产生时钟,设置可以查看芯片手册中的i2s时钟路线图,然后就是bfs,rfs,psr参数,通常使用bfs=32 rfs=256,因为wm8960中默认设置还会将传入的频率/256和/32,所以为了保证频率正确就这样设置,这两个可以通过应用传参得到。而psr就需要我们通过计算得到了,比如44100hz采样率就是5 采样率是22050时就是10。这是通过公式算出来的。(psr是i2s预分频寄存器)
N + 1 = (67.7Mhz) / (256 * 44.1Khz) = 5.99
N + 1 = (67.7Mhz) / (256 * 22.05Khz) = 11
我们只需要让这些值真确写入寄存器就好了。设置通过i2s控制器驱动中的i2s_set_clkdiv函数,当然这些只是设置,并没有实际写入到寄存器,真正写入到寄存器的是config_setup函数,里面调用
set_bfs(i2s, bfs);
set_rfs(i2s, rfs);
写入寄存器,psr在最后进行设置。
if (!(i2s->quirks & QUIRK_NO_MUXPSR)) {
psr =m_psr;// i2s->rclk_srcrate / i2s->frmclk / rfs;
printk("i2s %s setup psr:%d \r\n",__func__,psr);
writel(((psr ) << 8) | PSR_PSREN, i2s->addr + I2SPSR);
dev_dbg(&i2s->pdev->dev,
"RCLK_SRC=%luHz PSR=%u, RCLK=%dfs, BCLK=%dfs\n",
i2s->rclk_srcrate, psr, rfs, bfs);
}
可以看到我用m_psr替换掉了原始的psr计算,这是因为,我在machine中已经计算好了psr的值,传入设置好的时候,不用计算直接写入即可。
并且,i2s驱动中的set_clkdiv可能会少 rfs和psr的设置,加入即可。
(这些修改的部分是通过machine中的.hw_params函数设置的)
以下代码是I2S.c中的代码,添加了2和3两个选项(这是通过调试得到的缺少的部分)。
#define DIV_RCLK 2
#define DIV_PSR 3
static int i2s_set_clkdiv(struct snd_soc_dai *dai,
int div_id, int div)
{
struct i2s_dai *i2s = to_info(dai);
struct i2s_dai *other = i2s->pri_dai ? : i2s->sec_dai;
switch (div_id) {
case SAMSUNG_I2S_DIV_BCLK:
if ((any_active(i2s) && div && (get_bfs(i2s) != div))
|| (other && other->bfs && (other->bfs != div))) {
dev_err(&i2s->pdev->dev,
"%s:%d Other DAI busy\n", __func__, __LINE__);
return -EAGAIN;
}
printk(KERN_ERR"bfs:%d\r\n",div);
i2s->bfs = div;
break;
case DIV_RCLK:
if ((any_active(i2s) && div && (get_rfs(i2s) != div))
|| (other && other->rfs && (other->rfs != div))) {
dev_err(&i2s->pdev->dev,
"%s:%d Other DAI busy\n", __func__, __LINE__);
return -EAGAIN;
}
printk(KERN_ERR"rfs:%d\r\n",div);
i2s->rfs = div;
break;
case DIV_PSR:
{
m_psr=div;
}break;
default:
dev_err(&i2s->pdev->dev,
"Invalid clock divider(%d)\n", div_id);
return -EINVAL;
}
return 0;
}
这里还需要注意一个函数就是i2s_txctrl ,这个函数控制着i2s的输出,在函数的末尾可以看到设置IISCON和IISMOD寄存器,如果不出声音可以在此跟踪一下这两个寄存器的状态。
正确的状态不知道的话可以先写下裸机wm8960驱动以后再来验证,裸机驱动wm8960的代码网上还是比较多的。
我这里改成了con |1, 原因可以通过看手册和跟踪寄存器得出。
到此I2S部分和machine部分就修改完了。还要注意一点,如果iis设置全对,还是没有声音,这时就要考虑codec驱动的初始化问题了,以wm8960.c为例,我是在代码中的wm8960_probe函数中看到了wm8960的一些寄存器初始化函数调用,一些关键的设置并不正确,于是修改为如下:
static int wm8960_probe(struct snd_soc_codec *codec)
{
struct wm8960_priv *wm8960 = snd_soc_codec_get_drvdata(codec);
struct wm8960_data *pdata = dev_get_platdata(codec->dev);
int ret;
wm8960->set_bias_level = wm8960_set_bias_level_out3;
if (!pdata) {
dev_warn(codec->dev, "No platform data supplied\n");
} else {
if (pdata->capless)
wm8960->set_bias_level = wm8960_set_bias_level_capless;
}
ret = snd_soc_codec_set_cache_io(codec, 7, 9, SND_SOC_REGMAP);
if (ret < 0) {
dev_err(codec->dev, "Failed to set cache I/O: %d\n", ret);
return ret;
}
ret = wm8960_reset(codec);
if (ret < 0) {
dev_err(codec->dev, "Failed to issue reset\n");
return ret;
}
wm8960->set_bias_level(codec, SND_SOC_BIAS_STANDBY);
/* Latch the update bits */
/*
snd_soc_update_bits(codec, WM8960_LINVOL, 0x100, 0x100);
snd_soc_update_bits(codec, WM8960_RINVOL, 0x100, 0x100);
snd_soc_update_bits(codec, WM8960_LADC, 0x100, 0x100);
snd_soc_update_bits(codec, WM8960_RADC, 0x100, 0x100);
snd_soc_update_bits(codec, WM8960_LDAC, 0x100, 0x100);
snd_soc_update_bits(codec, WM8960_RDAC, 0x100, 0x100);
snd_soc_update_bits(codec, WM8960_LOUT1, 0x100, 0x100);
snd_soc_update_bits(codec, WM8960_ROUT1, 0x100, 0x100);
snd_soc_update_bits(codec, WM8960_LOUT2, 0x100, 0x100);
snd_soc_update_bits(codec, WM8960_ROUT2, 0x100, 0x100);
*/
//power
snd_soc_update_bits(codec, WM8960_POWER1,0x1c0,0x1c0);
snd_soc_update_bits(codec, WM8960_POWER2,0x1f8 ,0x1f8);
snd_soc_update_bits(codec, WM8960_POWER3,0x00c ,0x00c);
// clock CLKSEL = 0 : no PLL -> SYSCLK using MCLK
snd_soc_update_bits(codec, 0x4,0, 0x0);
// no mute ok
snd_soc_update_bits(codec, 0x5,0, 0x0); // set no mute
//snd_soc_update_bits(codec, 0x5, 0x08);// set mute
// audio interface
snd_soc_update_bits(codec, 0x7,0x2, 0x2);// 00 = 16bits, 10 = IIS format
// volume +6db ok
snd_soc_update_bits(codec, 0x2,0x1ff, 0x60 | 0x100);// WM8960_LOUT1
snd_soc_update_bits(codec, 0x3,0x1ff, 0x60 | 0x100);// WM8960_ROUT1
snd_soc_update_bits(codec, 0xa,0x1ff, 0xFF | 0x100);// Left DAC volume
snd_soc_update_bits(codec, 0xb,0x1ff, 0xFF | 0x100);// Right DAC volume
// mixer control
snd_soc_update_bits(codec, 0x22,0x180, 1<<8 | 1<<7); // Left output mixer control
snd_soc_update_bits(codec, 0x25,0x180, 1<<8 | 1<<7); // Right output mixer control
printk(KERN_ERR"wm8960 init done!\r\n");
snd_soc_add_codec_controls(codec, wm8960_snd_controls,
ARRAY_SIZE(wm8960_snd_controls));
wm8960_add_widgets(codec);
return 0;
}
注释掉的是原来的,wm8960寄存器地址在wm8960.h中都有定义的,我为了省事直接把裸机代码拿过来用了,效果是一样的。主要设置了电源,时钟,音量和声音输出,这些缺1不可(测试过)。
这样修改完后声音应该能放出来了。
如果出声音了,速率不对,偏快或偏慢,那是因为EPLL时钟设置不对,IIS需要67.7M的时钟频率才能正确播放。,当然80M的频率播放也蛮有意思的,音调被改变,一样的音乐,不一样的感受。^_^
smdk_8960.c是我手动添加的,还需要修改Makefile和Kconfig文件,才能被配置和编译。
首先需要在smdk_s5pv210.c中的device __init数值,确定iis0和i2c0设备在数组中。(我的wm8960 用的是iis0和i2c0),将wm8960作为i2c子设备添加到 i2c0中,配置内核支持i2c,asoc然后启动后确定i2c0,wm8960,asoc正确被加载(看输出),i2s0会在wm8960匹配成功后会出现如下类似输出:
soc-audio soc-audio: wm8960-hifi <-> samsung-i2s.0 mapping ok
最终成功时会有如下输出:
ALSA device list:
#0: SMDK-I2S
也就是成功注册了一个声卡设备。
在/dev目录下会出现这些设备节点:
controlC0 --> 用于声卡的控制,例如通道选择,混音,麦克风的控制等
pcmC0D0c --> 用于录音的pcm设备
pcmC0D0p --> 用于播放的pcm设备
timer --> 定时器
最后通过alsa-util调用alsa-lib库 播放wav文件 如果需要播放MP3还需要libmad(MP3解码库)的支持,直接让alsa-lib使用libmad库我还没找到方法,但可以通过自己写一个程序,调用libmad和alsa-lib来播放MP3,代码网上可以搜到。
MP3文件:
可以看到,播放MP3和WAV的驱动输出基本是一样的,最原始的音频数据是WAV,播放MP3只是通过软解码转换成WAV文件流而已。
对于这些带框架的驱动学习,如果遇到问题不知道出在哪里可以先去调试一下裸机代码,确定寄存器的正确配置后再来调驱动框架会有事半功倍的效果。^_^