PCM data flow - 3 - ASoC codec driver
来源:互联网 发布:javascript教材 编辑:程序博客网 时间:2024/05/22 00:30
上一章提到codec_drv的几个组成部分,下面逐一介绍,基本是以内核文档Documentation/sound/alsa/soc/codec.txt中的内容为脉络来分析的。codec的作用,在概述中有说明,本章主要罗列下codec driver中重要的数据结构及注册流程。
Codec DAI and PCM configuration
codec_dai和pcm配置信息通过结构体snd_soc_dai_driver描述,包括dai的能力描述和操作接口,snd_soc_dai_driver最终会被注册到asoc-core中。
/* * Digital Audio Interface Driver. * * Describes the Digital Audio Interface in terms of its ALSA, DAI and AC97 * operations and capabilities. Codec and platform drivers will register this * structure for every DAI they have. * This structure covers the clocking, formating and ALSA operations for each * interface. */struct snd_soc_dai_driver { /* DAI description */ const char *name; unsigned int id; int ac97_control; /* DAI driver callbacks */ int (*probe)(struct snd_soc_dai *dai); int (*remove)(struct snd_soc_dai *dai); int (*suspend)(struct snd_soc_dai *dai); int (*resume)(struct snd_soc_dai *dai); /* ops */ const struct snd_soc_dai_ops *ops; /* DAI capabilities */ struct snd_soc_pcm_stream capture; struct snd_soc_pcm_stream playback; unsigned int symmetric_rates:1; /* probe ordering - for components with runtime dependencies */ int probe_order; int remove_order;};
· name:codec_dai的名称标识,machine中的dai_link通过codec_dai_name来匹配codec_dai;
· probe:codec_dai的probe函数,由snd_soc_instantiate_card回调;
· playback:回放能力描述信息,如所支持的声道数、采样率、音频格式;
· capture:录制能力描述信息,如所支持声道数、采样率、音频格式;
· ops:指向codec_dai的操作函数集,这些函数集非常重要,它定义了dai的时钟配置、格式配置、硬件参数配置等回调。
例子,wm8994有三个dai,这里只列其一:
static const struct snd_soc_dai_ops wm8994_aif1_dai_ops = { .set_sysclk = wm8994_set_dai_sysclk, .set_fmt = wm8994_set_dai_fmt, .hw_params = wm8994_hw_params, .shutdown = wm8994_aif_shutdown, .digital_mute = wm8994_aif_mute, .set_pll = wm8994_set_fll, .set_tristate = wm8994_set_tristate,};static struct snd_soc_dai_driver wm8994_dai[] = { { .name = "wm8994-aif1", .id = 1, .playback = { .stream_name = "AIF1 Playback", .channels_min = 1, .channels_max = 2, .rates = WM8994_RATES, .formats = WM8994_FORMATS, .sig_bits = 24, }, .capture = { .stream_name = "AIF1 Capture", .channels_min = 1, .channels_max = 2, .rates = WM8994_RATES, .formats = WM8994_FORMATS, .sig_bits = 24, }, .ops = &wm8994_aif1_dai_ops, }, // ...省略...
Codec control IO
移动设备的音频codec,其控制接口一般是I2C或SPI。codec的读写接口访问操作codec寄存器,当dapm触发或mixer控件发生改变时,需要调用到读写接口。
在snd_soc_codec_driver结构体中,有如下字段描述codec的控制接口:
/* codec IO */ unsigned int (*read)(struct snd_soc_codec *, unsigned int); int (*write)(struct snd_soc_codec *, unsigned int, unsigned int); int (*display_register)(struct snd_soc_codec *, char *, size_t, unsigned int); int (*volatile_register)(struct snd_soc_codec *, unsigned int); int (*readable_register)(struct snd_soc_codec *, unsigned int); int (*writable_register)(struct snd_soc_codec *, unsigned int); unsigned int reg_cache_size; short reg_cache_step; short reg_word_size; const void *reg_cache_default; short reg_access_size; const struct snd_soc_reg_access *reg_access_default; enum snd_soc_compress_type compress_type;
· read:读取codec寄存器接口;
· write:写入codec寄存器接口;
· volatile_register:判断指定的寄存器是否是volatile属性;假如是,则asoc-core读取它的时候不会只是读cache,会直接通过控制接口访问硬件IO;
· readable_register:判断指定的寄存器是否可读;
· reg_cache_default:codec寄存器的缺省值;
· reg_cache_size:codec缺省的寄存器值数组大小;
· reg_word_size:codec寄存器值宽度。
在Linux-3.4.5中,这里面很多信息都改用regmap描述了。asoc-core中判断是否用的是regmap,如果是,则调用regmap接口。见如下函数:
int snd_soc_update_bits(struct snd_soc_codec *codec, unsigned short reg, unsigned int mask, unsigned int value){ bool change; unsigned int old, new; int ret; if (codec->using_regmap) { // 当前使用regmap,调用regmap接口,其中codec->control_data是regmap私有数据 ret = regmap_update_bits_check(codec->control_data, reg, mask, value, &change); } else { // 非regmap,调用snd_soc_codec_driver定义的read/write回调 ret = snd_soc_read(codec, reg); if (ret < 0) return ret; old = ret; new = (old & ~mask) | (value & mask); change = old != new; if (change) ret = snd_soc_write(codec, reg, new); } if (ret < 0) return ret; return change;}
使用regmap,使得控制接口抽象化,codec_drv不用操心当前控制方式是什么;
regmap在线调试目录是/sys/kernel/debug/regmap;
关于wm8994的regmap描述,请自行查阅driver/mfd/wm8994-regmap.c。
Mixers and audio controls
音频控件多用于部件开关和音量的设定,音频控件可通过soc.h中的宏来定义。例如单一型控件:
#define SOC_SINGLE(xname, reg, shift, max, invert) \{ .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname, \ .info = snd_soc_info_volsw, .get = snd_soc_get_volsw,\ .put = snd_soc_put_volsw, \ .private_value = SOC_SINGLE_VALUE(reg, shift, max, invert) }
这种控件只有一个设置量,一般用于部件开关。宏定义的参数说明:
· xname:控件的名称标识;
· reg:控件对应的寄存器地址;
· shift:控件控制位在寄存器中的偏移;
· max:控件设置值范围,一般来说,如果控制位只有1位的话,那么max=1,因为设置值只有0和1;
· invert:设定值是否取反。
其他类型控件类似,不一一介绍了。
上述只是宏定义,音频控件真正的结构是snd_kcontrol_new:
struct snd_kcontrol_new { snd_ctl_elem_iface_t iface; /* interface identifier */ unsigned int device; /* device/client number */ unsigned int subdevice; /* subdevice (substream) number */ const unsigned char *name; /* ASCII name of item */ unsigned int index; /* index of item */ unsigned int access; /* access rights */ unsigned int count; /* count of same elements */ snd_kcontrol_info_t *info; snd_kcontrol_get_t *get; snd_kcontrol_put_t *put; union { snd_kcontrol_tlv_rw_t *c; const unsigned int *p; } tlv; unsigned long private_value;};
codec初始化时,通过snd_soc_add_codec_controls把所有定义好的音频控件注册到alsa-core中,上层可以通过tinymix、alsa_amixer等工具查看修改这些控件的设定。
音频控件的详细分析见dapm系列文章。
Codec audio operations
Codec的音频操作接口通过结构体snd_soc_dai_ops描述:
struct snd_soc_dai_ops { /* * DAI clocking configuration, all optional. * Called by soc_card drivers, normally in their hw_params. */ int (*set_sysclk)(struct snd_soc_dai *dai, int clk_id, unsigned int freq, int dir); int (*set_pll)(struct snd_soc_dai *dai, int pll_id, int source, unsigned int freq_in, unsigned int freq_out); int (*set_clkdiv)(struct snd_soc_dai *dai, int div_id, int div); /* * DAI format configuration * Called by soc_card drivers, normally in their hw_params. */ int (*set_fmt)(struct snd_soc_dai *dai, unsigned int fmt); int (*set_tdm_slot)(struct snd_soc_dai *dai, unsigned int tx_mask, unsigned int rx_mask, int slots, int slot_width); int (*set_channel_map)(struct snd_soc_dai *dai, unsigned int tx_num, unsigned int *tx_slot, unsigned int rx_num, unsigned int *rx_slot); int (*set_tristate)(struct snd_soc_dai *dai, int tristate); /* * DAI digital mute - optional. * Called by soc-core to minimise any pops. */ int (*digital_mute)(struct snd_soc_dai *dai, int mute); /* * ALSA PCM audio operations - all optional. * Called by soc-core during audio PCM operations. */ int (*startup)(struct snd_pcm_substream *, struct snd_soc_dai *); void (*shutdown)(struct snd_pcm_substream *, struct snd_soc_dai *); int (*hw_params)(struct snd_pcm_substream *, struct snd_pcm_hw_params *, struct snd_soc_dai *); int (*hw_free)(struct snd_pcm_substream *, struct snd_soc_dai *); int (*prepare)(struct snd_pcm_substream *, struct snd_soc_dai *); int (*trigger)(struct snd_pcm_substream *, int, struct snd_soc_dai *); /* * For hardware based FIFO caused delay reporting. * Optional. */ snd_pcm_sframes_t (*delay)(struct snd_pcm_substream *, struct snd_soc_dai *);};
注释比较详细的了,codec音频操作接口分为5大部分:时钟配置、格式配置、数字静音、PCM音频接口、FIFO延迟报告。着重说下时钟配置及格式配置接口:
· set_sysclk:codec_dai的系统时钟设置,当上层pcm_open时,需要回调该接口设置codec的系统时钟,codec才能正常工作;
· set_pll:Codec FLL设置,codec一般有个MCLK的输入时钟,回调该接口基于MCLK来设定codec FLL的时钟频率;得到正确的FLL时钟频率后,则codec的sysclk、bclk、lrclk均可从FLL分频出来(假设codec作为master);
· set_fmt:codec_dai的格式设置,相关格式见soc-dai.h定义;
· SND_SOC_DAIFMT_I2S——音频数据是I2S格式;
· SND_SOC_DAIFMT_DSP_A——音频数据是语音常用的PCM格式;
· SND_SOC_DAIFMT_CBM_CFM——codec作为master,BCLK和LRCLK由codec提供;
· SND_SOC_DAIFMT_CBS_CFS——codec作为 slave ,BCLK和LRCLK由soc platform提供;
· hw_params:codec_dai的硬件参数设置,根据上层设定的声道数、采样率、数据格式,来设置codec_dai相关寄存器。
static int goni_hifi_hw_params(struct snd_pcm_substream *substream, struct snd_pcm_hw_params *params){ struct snd_soc_pcm_runtime *rtd = substream->private_data; struct snd_soc_dai *codec_dai = rtd->codec_dai; struct snd_soc_dai *cpu_dai = rtd->cpu_dai; unsigned int pll_out = 24000000; // 这是MCLK的时钟频率,codec的源时钟 int ret = 0; /* set the cpu DAI configuration */ ret = snd_soc_dai_set_fmt(cpu_dai, SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBM_CFM); if (ret < 0) return ret; /* set codec DAI configuration */ ret = snd_soc_dai_set_fmt(codec_dai, SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBM_CFM); if (ret < 0) return ret; /* set the codec FLL */ ret = snd_soc_dai_set_pll(codec_dai, WM8994_FLL1, 0, pll_out, params_rate(params) * 256); if (ret < 0) return ret; /* set the codec system clock */ ret = snd_soc_dai_set_sysclk(codec_dai, WM8994_SYSCLK_FLL1, params_rate(params) * 256, SND_SOC_CLOCK_IN); if (ret < 0) return ret; return 0;}
其中snd_soc_dai_set_fmt()实际上会调用cpu_dai或codec_dai的set_fmt回调,
snd_soc_dai_set_pll()、snd_soc_dai_set_sysclk()类似。
· MCLK作为codec的源时钟,频率为24Mhz;
· 设置cpu_dai和codec_dai格式:I2S,BCLK和LRCLK由codec提供;
· 设置codec_dai的FLL1:时钟源是MCLK,时钟源频率是24Mhz,目的时钟频率是256fs;
· 设置codec_dai的系统时钟:时钟源是FLL1,系统时钟频率是256fs。
codec_dai的时钟设置非常重要,设置错误将会导致很多问题,典型如下:
· 无声-->> 检查codec的系统时钟、codec_dai的位时钟和帧时钟是否使能;
· 声音失真-->> 检查音频源数据的采样率是否和codec_dai的帧时钟一致;
· 声音断续 -->> 检查codec的系统时钟和位时钟、帧时钟是否同步,出现这种情况,可能是sysclk和BCLK/LRCLK不是由同一个时钟源分频出来导致的,取决于具体codec设计。DAPM description
概念:Dynamic Audio Power Management,动态音频电源管理,为移动Linux设备设计,使得音频系统任何时候都工作在最低功耗状态。
目的:使能必要的最少的部件,令音频系统正常工作。
原理:当音频路径发生改变(比如上层使用tinymix工具设置音频通路)时,或发生数据流事件(比如启动或停止播放)时,都会触发dapm去遍历所有邻近的音频部件,检查是否存在完整的音频路径(complete path:该部件必须往前能到达输入端点如DAC/Mic/Linein,往后能到达输出端点如ADC/HP/SPK),如果存在完整的音频路径,则该路径上面的所有部件都是需要上电的,其他部件则下电。
对于此的完整分析,见另一个系列的文章。
说说DAPM部件中最典型的mixer widget:Mixes several analog signals into a single analog signal. 它可以把几路模拟信号混合到一路输出。如WM8994的SPKMIXL:如图,SPKMIXL有5路输入,分别是:MIXINL、IN1LP、DAC1L、DAC2L、MIXEROUTL,因此这里可以构成5条路径。
· 1、如下5个控件控制SPKMIXL输入:
static const struct snd_kcontrol_new left_speaker_mixer[] = {SOC_DAPM_SINGLE("DAC2 Switch", WM8994_SPEAKER_MIXER, 9, 1, 0),SOC_DAPM_SINGLE("Input Switch", WM8994_SPEAKER_MIXER, 7, 1, 0),SOC_DAPM_SINGLE("IN1LP Switch", WM8994_SPEAKER_MIXER, 5, 1, 0),SOC_DAPM_SINGLE("Output Switch", WM8994_SPEAKER_MIXER, 3, 1, 0),SOC_DAPM_SINGLE("DAC1 Switch", WM8994_SPEAKER_MIXER, 1, 1, 0),};· 2、定义SPKMIXL的dapm widget:
SND_SOC_DAPM_MIXER_E("SPKL", WM8994_POWER_MANAGEMENT_3, 8, 0, left_speaker_mixer, ARRAY_SIZE(left_speaker_mixer), late_enable_ev, SND_SOC_DAPM_PRE_PMU),留意WM8994_POWER_MANAGEMENT_3寄存器的bit8正是控制SPKMIXL通断电的。
· 3、定义SPKMIXL相关路由:
static const struct snd_soc_dapm_route intercon[] = { // ... { "SPKL", "DAC1 Switch", "DAC1L" }, { "SPKL", "DAC2 Switch", "DAC2L" },
最终上层会看到两个控件:"SPKL DAC1 Switch","SPKL DAC2 Switch";前者用于SPKL选中DAC1L作为输入,后者用于SPKL选中DAC2L作为输入。
但控件"SPKL DAC1 Switch"或"SPKL DAC2 Switch"的打开,不代表能使得"SPKL"上电。只有当SPKL位于完整的音频路径中时,SPKL才会上电,WM8994_POWER_MANAGEMENT_3寄存器的bit8会被置为1。
Codec register
当platform_driver:
static struct platform_driver wm8994_codec_driver = { .driver = { .name = "wm8994-codec", .owner = THIS_MODULE, .pm = &wm8994_pm_ops, }, .probe = wm8994_probe, .remove = __devexit_p(wm8994_remove),};
与name ="wm8994-codec"的platform_device(该platform_device在driver/mfd/wm8994-core.c中创建并初始化完成)匹配后,立即回调wm8994_probe注册codec:
static int __devinit wm8994_probe(struct platform_device *pdev){ struct wm8994_priv *wm8994; wm8994 = devm_kzalloc(&pdev->dev, sizeof(struct wm8994_priv), GFP_KERNEL); if (wm8994 == NULL) return -ENOMEM; platform_set_drvdata(pdev, wm8994); wm8994->wm8994 = dev_get_drvdata(pdev->dev.parent); wm8994->pdata = dev_get_platdata(pdev->dev.parent); return snd_soc_register_codec(&pdev->dev, &soc_codec_dev_wm8994, wm8994_dai, ARRAY_SIZE(wm8994_dai));}
snd_soc_register_codec:将codec_driver和codec_dai_driver注册到asoc-core。
/** * snd_soc_register_codec - Register a codec with the ASoC core * * @codec: codec to register */int snd_soc_register_codec(struct device *dev, const struct snd_soc_codec_driver *codec_drv, struct snd_soc_dai_driver *dai_drv, int num_dai)创建一个snd_soc_codec实例,包含codec_drv(snd_soc_dai_driver)的相关信息,封装给asoc-core使用,相关代码段如下:
struct snd_soc_codec *codec; dev_dbg(dev, "codec register %s\n", dev_name(dev)); codec = kzalloc(sizeof(struct snd_soc_codec), GFP_KERNEL); if (codec == NULL) return -ENOMEM; /* create CODEC component name */ codec->name = fmt_single_name(dev, &codec->id); if (codec->name == NULL) { kfree(codec); return -ENOMEM; } // 初始化codec的寄存器缓存配置及读写接口 codec->write = codec_drv->write; codec->read = codec_drv->read; codec->volatile_register = codec_drv->volatile_register; codec->readable_register = codec_drv->readable_register; codec->writable_register = codec_drv->writable_register; codec->ignore_pmdown_time = codec_drv->ignore_pmdown_time; codec->dapm.bias_level = SND_SOC_BIAS_OFF; codec->dapm.dev = dev; codec->dapm.codec = codec; codec->dapm.seq_notifier = codec_drv->seq_notifier; codec->dapm.stream_event = codec_drv->stream_event; codec->dev = dev; codec->driver = codec_drv; codec->num_dai = num_dai; mutex_init(&codec->mutex);
把以上创建并初始化好的codec插入到codec_list链表上,Machine驱动初始化时会遍历该链表,以匹配并绑定dai_link上的codec:
list_add(&codec->list, &codec_list);
把codec_drv中的snd_soc_dai_driver(wm8994有3个dai,分别是aif1、aif2和aif3)注册到asoc-core:
/* register any DAIs */ if (num_dai) { ret = snd_soc_register_dais(dev, dai_drv, num_dai); if (ret < 0) goto fail; }
snd_soc_register_dais会把dai插入到dai_list链表中:list_add(&dai->list,&dai_list); Machine驱动初始化时会遍历该链表,以匹配并绑定dai_link上的codec_dai。
最后顺便提下dai_link中的codec和codec_dai的区别:codec指音频codec共有的部分,包括codec初始化函数、控制接口、寄存器缓存、控件列表、dapm部件列表、音频路由列表、偏置电压设置函数等描述信息;而codec_dai指codec上的音频接口驱动描述,各个接口的描述信息不一定都是一致的,所以各个音频接口都有它自身的驱动描述,包括音频接口的初始化函数、操作函数集、能力描述等。
我开始时认为:codec_dai从属于codec,dai_link没有必要同时声明codec和codec_dai,应该可以实现codec_dai就能找到它对应的父设备codec的方法。后来想到系统上如果有两个以上的codec,而恰好不同codec上的codec_dai有重名的话,此时就必须同时声明codec和codec_dai才能找到正确的音频接口了。
- PCM data flow - 3 - ASoC codec driver
- PCM data flow - part 3: ASoC codec driver
- PCM data flow - 4 - ASoC platform driver
- PCM data flow - 5 - ASoC machine driver
- PCM data flow - part 4: ASoC platform driver
- PCM data flow - part 5: ASoC machine driver
- PCM data flow - 2 - ASoC data structure
- PCM data flow - part 2: ASoC data structure
- ASoC之Codec Driver
- PCM data flow - 1 - Overview
- PCM data flow之一:概述
- PCM data flow之一:概述
- PCM data flow之六:Frames & Periods
- PCM data flow - part 1: Overview
- PCM data flow - part 6: Frames & Periods
- PCM data flow - 7 - Frame & Period
- PCM data flow之二:Frames and Periods
- ASoC架构中的Codec
- 图像处理之霍夫变换圆检测算法
- URL编码与解码
- 把PE映像文件从内存中DUMP到磁盘
- 算法学习资源
- 使用二维数组打印一个 10 行杨辉三角.
- PCM data flow - 3 - ASoC codec driver
- Setting Up A Build Environment on OS X 10.9 Mavericks
- (纯转载)一个真正符合中国国情的工作流设计参考(包括PHP实现)
- C语言里scanf函数的有关问题
- UID-09-PS-DM及画册
- Weka进行数据挖掘简介
- 一些简单的编程练习题
- fusioncharts入门系列3之柱状图与折线图
- cs文件编译方法