17.5 Linux ASoC音频设备驱动
17.5.1 ASoC(ALSA System on Chip) 驱动的组成
ASoC(ALSA System on Chip)是 ALSA 在 SoC 方面的发展和演变,ASoC在本质上仍然属于ALSA,但是在 ALSA 架构基础上对 CPU 相关的代码和 Codec 相关的代码进行分离。其原因是,采用传统 ALSA 架构的情况下,同一型号的 Codec 工作于不同的 CPU 时,需要不同的驱动,这不符合代码重用的要求。
对于目前嵌入式系统上的声卡驱动开发,建议读者尽量采用 ASoC 框架,ASoC 主要由 3 部分组成。
(1)Codec 驱动。这一部分只关心 Codec 本身,与 CPU 平台相关的特性不由此部分操作。
(2)平台驱动。这一部分只关心 CPU 本身,不关心 Codec。主要处理两个问题:DMA 引擎和 SoC 集成的 PCM、I 2 S 或 AC 97 数字接口控制。
(3)板驱动(称为 machine 驱动)。这一部分将平台驱动和 Codec 驱动绑定在一起,描述板一级的硬件特征。
在以上 3 部分中,1 和 2 基本都可以仍然是通用的驱动,Codec 驱动认为自己可以连接任意 CPU,而 CPU 的 I 2 S、PCM 或 AC 97 接口对应的平台驱动则认为自己可以连接任意符合其接口类型的 Codec,只有 (3)是不通用的,由特定的电路板上具体的 CPU 和 Codec 确定,因此板驱动很像一个插座,上面插上了 Codec 和平台这两个插头。
在以上三部分之上的是 ASoC 核心层,由内核源代码中的 sound/soc/soc-core.c 实现,查看其源代码发现它完全是一个传统的 ALSA 驱动。因此,对于基于 ASoC 架构的声卡驱动,alsa-lib以及 ALSA 的一系列 utility 仍然是可用的。而ASoC 的用户编程方法也与 ALSA 完全一致。
内核源代码的 Documentation/sound/alsa/soc/目录包含ASoC 相关的文档。
17.5.2 ASoC Codec 驱动
在 ASoC 架构下,Codec 驱动负责如下工作。
(1)Codec DAI(Digital Audio Interfaces)和 PCM (脉冲编码调制)配置,由结构体 snd_soc_ dai(如代码清单 17.28)来描述,形容 playback、capture 的属性以及 DAI 接口的操作。
代码清单 17.28 DAI 结构体 snd_soc_dai 定义
include/sound/soc-dai.h
/*
* Digital Audio Interface runtime data.
*
* Holds runtime data for a DAI.
*/
struct snd_soc_dai {
/* DAI 的描述 */
int id;
struct device *dev;
void *ac97_pdata; /* platform_data for the ac97 codec */
/* driver ops */
struct snd_soc_dai_driver *driver;
/* DAI 运行时信息 */
unsigned int capture_active:1; /* stream is in use */
unsigned int playback_active:1; /* stream is in use */
unsigned int symmetric_rates:1;
unsigned int symmetric_channels:1;
unsigned int symmetric_samplebits:1;
unsigned int active;
unsigned char probed:1;
struct snd_soc_dapm_widget *playback_widget;
struct snd_soc_dapm_widget *capture_widget;
/* DAI DMA data */
void *playback_dma_data;
void *capture_dma_data;
/* Symmetry data - only valid if symmetry is being enforced */
unsigned int rate;
unsigned int channels;
unsigned int sample_bits;
/* parent platform/codec */
struct snd_soc_platform *platform;
struct snd_soc_codec *codec;
struct snd_soc_component *component;
/* CODEC TDM slot masks and params (for fixup) */
unsigned int tx_mask;
unsigned int rx_mask;
struct snd_soc_card *card;
struct list_head list;
};
(2)Codec IO 操作、动态音频电源管理以及时钟、PLL 等控制。
代码清单 17.28 中的 snd_soc_codec 结构体是对 Codec 本身 I/O 控制以及动态音频电源管理(Dynamic Audio Power Management,DAPM)的描述。描述 I 2 C、SPI 或 AC 97 如何读写 Codec 寄存器并容纳 DAPM 链表,其定义如代码清单 17.29。
代码清单 17.29 snd_soc_codec 结构体定义
include/sound/soc.h
/* SoC Audio Codec */
struct snd_soc_codec {
char *name;
struct module *owner;
struct mutex mutex;
/* callbacks */
int (*dapm_event)(struct snd_soc_codec *codec, int event);
/* runtime */
struct snd_card *card;
struct snd_ac97 *ac97; /* for ad-hoc ac97 devices */
unsigned int active;
unsigned int pcm_devs;
void *private_data;
/* codec IO */
void *control_data; /* codec control (i2c/3wire) data */
unsigned int (*read)(struct snd_soc_codec *, unsigned int);
int (*write)(struct snd_soc_codec *, unsigned int, unsigned int);
hw_write_t hw_write;
hw_read_t hw_read;
void *reg_cache;
short reg_cache_size;
short reg_cache_step;
/* dapm */
struct list_head dapm_widgets;
struct list_head dapm_paths;
unsigned int dapm_state;
unsigned int suspend_dapm_state;
struct delayed_work delayed_work;
/* codec DAI's */
struct snd_soc_codec_dai *dai;
unsigned int num_dai;
};
snd_soc_dai_ops 则描述该 Codec 的时钟、PLL 以及格式设置,其定义如代码清单 17.30。
代码清单 17.30 snd_soc_dai_ops 结构体定义
include/sound/soc-dai.h
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);
int (*set_bclk_ratio)(struct snd_soc_dai *dai, unsigned int ratio);
/*
* 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 (*xlate_tdm_slot_mask)(unsigned int slots,
unsigned int *tx_mask, unsigned int *rx_mask);
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);
int (*get_channel_map)(struct snd_soc_dai *dai,
unsigned int *tx_num, unsigned int *tx_slot,
unsigned int *rx_num, unsigned int *rx_slot);
/*
* DAI digital mute - optional.
* Called by soc-core to minimise any pops.
*/
int (*digital_mute)(struct snd_soc_dai *dai, int mute);
int (*mute_stream)(struct snd_soc_dai *dai, int mute, int stream);
/*
* 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 *);
/*
* NOTE: Commands passed to the trigger function are not necessarily
* compatible with the current state of the dai. For example this
* sequence of commands is possible: START STOP STOP.
* So do not unconditionally use refcounting functions in the trigger
* function, e.g. clk_enable/disable.
*/
int (*trigger)(struct snd_pcm_substream *, int,
struct snd_soc_dai *);
int (*bespoke_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 *);
};
(3)Codec 的 mixer 控制。
ASoC 中定义了一组宏来描述 Codec 的 mixer 控制,这组宏可以将 mixer 名和对应的寄存器进行绑定,主要包括:
include/sound/soc.h
SOC_SINGLE(xname, reg, shift, mask, invert)
SOC_DOUBLE(xname, reg, shift_left, shift_right, mask, invert)
SOC_ENUM_SINGLE(xreg, xshift, xmask, xtexts)
例如,对于宏 SOC_SINGLE,参数 xname 是 mixer 的名字(如“Playback Volume”),reg是控制该 mixer 的寄存器,shift 对应寄存器内的位,mask 是进行操作时的屏蔽位,invert 表明是否倒序或翻转。
(4)Codec 音频操作。
在 ASoC 驱动的 Codec 部分,也需要关心音频流开始采集或播放时的一些动作,如hw_params()、hw_free()、prepare()、trigger()这些操作,不过与原始 ALSA 不同的是,在 Codec驱动的这些函数中,不关心 CPU 端,而只关心 Codec 本身,由结构体 snd_soc_ops 描述,如代码清单 17.31 所示。
代码清单 17.31 snd_soc_ops 结构体定义
include/sound/soc.h
/* SoC audio ops */
struct snd_soc_ops {
int (*startup)(struct snd_pcm_substream *);
void (*shutdown)(struct snd_pcm_substream *);
int (*hw_params)(struct snd_pcm_substream *, struct snd_pcm_hw_params *);
int (*hw_free)(struct snd_pcm_substream *);
int (*prepare)(struct snd_pcm_substream *);
int (*trigger)(struct snd_pcm_substream *, int);
};
17.5.3 ASoC 平台驱动
首先,在 ASoC 平台驱动部分,同样存在着 Codec 驱动中的 snd_soc_dai、snd_soc_dai_ops、snd_soc_ops 这 3 个结构体的实例用于描述 DAI 和 DAI 上的操作,不同的是,在平台驱动中,它们只描述 CPU 相关的部分。除此之外,在 ASoC 平台驱动中,必须实现完整的DMA 驱动,即传统 ALSA 的 snd_pcm_ops 结构体成员函数 trigger()、pointer()等。因此 ASoC 平台驱动由 DAI 和 DMA 两部分组成,如代码清单 17.32 所示。(i2s数字音频数据传输总线)
代码清单 17.32 ASoC 平台驱动的组成
/* DAI 部分 */
static int xxx_i2s_set_dai_fmt(struct snd_soc_dai *cpu_dai, unsigned int fmt)
{
...
}
static int xxx_i2s_startup(struct snd_pcm_substream *substream)
{
...
}
static int xxx_i2s_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params)
{
...
}
static void xxx_i2s_shutdown(struct snd_pcm_substream *substream)
{
...
}
static int xxx_i2s_probe(struct platform_device *pdev, struct snd_soc_dai *dai)
{
...
}
static void xxx_i2s_remove(struct platform_device *pdev, struct snd_soc_dai *dai)
{
...
}
static int xxx_i2s_suspend(struct platform_device *dev, struct snd_soc_dai *dai)
{
...
}
static int xxx_i2s_resume(struct platform_device *pdev, struct snd_soc_dai *dai)
{
...
}
struct snd_soc_dai xxx_i2s_dai = {
.name = "xxx-i2s",
.id = 0,
.type = SND_SOC_DAI_I2S,
.probe = xxx_i2s_probe,
.remove = xxx_i2s_remove,
.suspend = xxx_i2s_suspend,
.resume = xxx_i2s_resume,
.playback = {
.channels_min = 1,
.channels_max = 2,
.rates = XXX_I2S_RATES,
.formats = XXX_I2S_FORMATS,
},
.capture = {.channels_min = 1,
.channels_max = 2,
.rates = XXX_I2S_RATES,
.formats = XXX_I2S_FORMATS,
},
.ops = {.startup = xxx_i2s_startup,
.shutdown = xxx_i2s_shutdown,
.hw_params = xxx_i2s_hw_params,
},
.dai_ops = {.set_fmt = xxx_i2s_set_dai_fmt,
},
};
/* DMA 部分 */
static void bf5xx_dma_irq(void *data)
{
struct snd_pcm_substream *pcm = data;
snd_pcm_period_elapsed(pcm);
}
static const struct snd_pcm_hardware xxx_pcm_hardware = {
...
};
static int xxx_pcm_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params)
{
...
snd_pcm_lib_malloc_pages(substream, size);
return 0;
}
static int xxx_pcm_hw_free(struct snd_pcm_substream *substream)
{
snd_pcm_lib_free_pages(substream);
return 0;
}
static int xxx_pcm_prepare(struct snd_pcm_substream *substream)
{
...
}
static int xxx_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
...
}
static snd_pcm_uframes_t xxx_pcm_pointer(struct snd_pcm_substream *substream)
{
...
}
static int xxx_pcm_open(struct snd_pcm_substream *substream)
{
...
}
static int xxx_pcm_mmap(struct snd_pcm_substream *substream, struct vm_area_struct *vma)
{
...
}
struct snd_pcm_ops xxx_pcm_i2s_ops = {
.open = xxx_pcm_open,
.ioctl = snd_pcm_lib_ioctl,
.hw_params = xxx_pcm_hw_params,
.hw_free = xxx_pcm_hw_free,
.prepare = xxx_pcm_prepare,
.trigger = xxx_pcm_trigger,
.pointer = xxx_pcm_pointer,
.mmap = xxx_pcm_mmap,
};
17.5.4 ASoC 板驱动
ASoC 板驱动直接与板对应,对于一块确定的电路板,其 SoC 和 Codec 都是确定的,因此板驱动将 ASoC Codec 驱动和 CPU 端的平台驱动进行绑定,这个绑定用数据结构 snd_soc_dai_link描述,其定义如代码清单 17.33 所示。
代码清单 17.33 snd_soc_dai_link 结构体
include/sound/soc.h
struct snd_soc_dai_link {
char *name; /* Codec name */
char *stream_name; /* Stream name */
/* DAI */
struct snd_soc_dai *codec_dai;
struct snd_soc_dai *cpu_dai;
/* 板流操作 */
struct snd_soc_ops *ops;
/* codec/machine 特定的初始化 */
int (*init)(struct snd_soc_codec *codec);
/* DAI pcm */
struct snd_pcm *pcm;
};
除此之外,板驱动还关心一些板特定的硬件操作,也存在一个 snd_soc_ops 的实例。
在板驱动的模块初始化函数中,会通过 platform_device_add()注册一个名为“soc-audio”的platform 设备,因为 soc-core.c 注册了一个名为“soc-audio”的 platform 驱动,因此,在板驱动中注册“soc-audio”设备会引起两者的匹配,从而引发一系列的初始化操作。尤其值得一提的是,“soc-audio”设备的私有数据需要为一个 snd_soc_device 的结构体实体,因此一个板驱动典型的模块加载函数将形如代码清单 17.34。代码清单 17.34 ASoC 板驱动模块加载函数及其访问的数据结构
static struct snd_soc_dai_link cpux_codecy_dai = {
.name = "codecy",
.stream_name = "CODECY",
.cpu_dai = &cpux_i2s_dai,
.codec_dai = &codecy_dai,
.ops = &cpux_codecy_ops,
};
static struct snd_soc_machine cpux_codecy = {
.name = "cpux_codecy",
.probe = cpux_probe,
.dai_link = &cpux_codecy_dai,
.num_links = 1,
};
static struct snd_soc_device cpux_codecy_snd_devdata = {
.machine = &cpux_codecy,
.platform = &cpux_i2s_soc_platform,
.codec_dev = &soc_codec_dev_codecy,
};
static struct platform_device *cpux_codecy_snd_device;
static int __init cpux_codecy_init(void)
{
int ret;
cpux_codecy_snd_device = platform_device_alloc("soc-audio", -1);
if (!cpux_codecy_snd_device)
return -ENOMEM;
platform_set_drvdata(cpux_codecy_snd_device, &cpux_codecy_snd_devdata);
cpux_codecy_snd_devdata.dev = &cpux_codecy_snd_device->dev;
ret = platform_device_add(cpux_codecy_snd_device);
if (ret)
platform_device_put(cpux_codecy_snd_device);
return ret;
}
module_init(cpux_codecy_init);
分析:
ASoC 驱动的 Codec、平台和板驱动是 3 个独立的内核模块,在板驱动中,对 ASoC Codec 设备、ASoC平台设备实例的访问都通过被ASoC Codec驱动或ASoC平台驱动导出的全局变量执行,这使得 ASoC 难以同时支持两个以上的 Codec。