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。