《Linux 设备驱动开发详解:基于最新的 Linux 4.0 内核》 第 17 章 Linux 音频设备驱动

《Linux 设备驱动开发详解:基于最新的 Linux 4.0 内核》

第 17 章 Linux 音频设备驱动

参考:宋宝华 著,机械工业出版社,2015年版


17.1 ALSA 体系结构

17.1.1 ALSA 简介

ALSA(Advanced Linux Sound Architecture,高级 Linux 声音架构)是 Linux 内核中的音频子系统,自 Linux 2.6 起取代了旧的 OSS(Open Sound System)成为标准音频框架。

复制代码
ALSA 的主要特性:
  ✓ 支持多种音频硬件(声卡、编解码器、数字音频接口)
  ✓ 支持多通道音频(立体声、5.1、7.1 等)
  ✓ 支持全双工(同时录音和播放)
  ✓ 支持 MIDI(Musical Instrument Digital Interface)
  ✓ 支持混音器(Mixer)控制
  ✓ 提供用户空间 API(alsa-lib)
  ✓ 向后兼容 OSS API

17.1.2 ALSA 体系结构层次

复制代码
ALSA 完整体系结构:

┌─────────────────────────────────────────────────────────────┐
│                      用户空间                                │
│  ┌──────────────────────────────────────────────────────┐   │
│  │              应用程序                                 │   │
│  │  aplay / arecord / audacity / VLC / PulseAudio       │   │
│  └──────────────────────────┬─────────────────────────┘   │
│  ┌──────────────────────────▼─────────────────────────┐   │
│  │              alsa-lib(用户空间库)                   │   │
│  │  提供 PCM、Mixer、MIDI 等高层 API                    │   │
│  │  /usr/lib/libasound.so                              │   │
│  └──────────────────────────┬─────────────────────────┘   │
└──────────────────────────────┼──────────────────────────────┘
                               │ 系统调用(/dev/snd/pcmC0D0p 等)
┌──────────────────────────────▼──────────────────────────────┐
│                      内核空间                                │
│  ┌──────────────────────────────────────────────────────┐   │
│  │              ALSA 核心层(sound/core/)               │   │
│  │  PCM 核心 / Mixer 核心 / MIDI 核心 / Timer 核心       │   │
│  └──────────────────────────┬─────────────────────────┘   │
│  ┌──────────────────────────▼─────────────────────────┐   │
│  │              ASoC 层(sound/soc/)                   │   │
│  │  Machine 驱动 / Platform 驱动 / Codec 驱动           │   │
│  └──────────────────────────┬─────────────────────────┘   │
│  ┌──────────────────────────▼─────────────────────────┐   │
│  │              硬件驱动层                              │   │
│  │  I2S 控制器 / AC97 控制器 / USB 音频 / HDMI 音频     │   │
│  └──────────────────────────┬─────────────────────────┘   │
└──────────────────────────────┼──────────────────────────────┘
                               │ 操作硬件寄存器
┌──────────────────────────────▼──────────────────────────────┐
│                      硬件层                                  │
│  音频编解码器(WM8960/ES8316 等)+ I2S 控制器 + 扬声器/麦克风│
└─────────────────────────────────────────────────────────────┘

17.1.3 ALSA 设备文件

bash 复制代码
# ALSA 在 /dev/snd/ 下创建设备文件
ls /dev/snd/
# controlC0   ← 声卡0的控制设备(混音器控制)
# pcmC0D0c    ← 声卡0,设备0,录音(capture)
# pcmC0D0p    ← 声卡0,设备0,播放(playback)
# pcmC0D1p    ← 声卡0,设备1,播放
# seq         ← MIDI 音序器
# timer       ← 定时器

# 命名规则:
# C0 = Card 0(声卡0)
# D0 = Device 0(设备0)
# p  = playback(播放)
# c  = capture(录音)

# 查看声卡信息
cat /proc/asound/cards
#  0 [ALSA           ]: bcm2835_alsa - bcm2835 ALSA
#                       bcm2835 ALSA

cat /proc/asound/pcm
#  00-00: bcm2835 ALSA : bcm2835 ALSA : playback 8

# 使用 aplay 播放音频
aplay -D hw:0,0 test.wav

# 使用 arecord 录音
arecord -D hw:0,0 -f S16_LE -r 44100 -c 2 output.wav

# 查看混音器控制
amixer -c 0 contents

17.1.4 ALSA 核心概念

复制代码
ALSA 核心概念:

声卡(Card):
  对应一个音频硬件设备
  每个声卡有唯一编号(0, 1, 2...)
  通过 snd_card_new() 创建

PCM(Pulse Code Modulation,脉冲编码调制):
  数字音频的基本格式
  参数:采样率(44100Hz)、位深(16bit)、通道数(2=立体声)
  分为播放(Playback)和录音(Capture)两个方向

混音器(Mixer):
  控制音量、静音、音频路由等
  通过 ALSA 控制接口(Control Interface)实现

MIDI:
  Musical Instrument Digital Interface
  数字乐器通信协议

Period 和 Buffer:
  Buffer:DMA 缓冲区总大小
  Period:每次 DMA 中断传输的数据量
  Buffer = Period × Period_count
  例:Buffer=4096字节,Period=1024字节,Period_count=4

17.2 ALSA 驱动的组成

17.2.1 ALSA 驱动的核心数据结构

c 复制代码
#include <sound/core.h>
#include <sound/pcm.h>
#include <sound/control.h>

/*
 * snd_card:声卡结构体
 * 每个音频设备对应一个 snd_card
 */
struct snd_card {
    int             number;         /* 声卡编号 */
    char            id[16];         /* 声卡 ID(字符串)*/
    char            driver[16];     /* 驱动名称 */
    char            shortname[32];  /* 短名称 */
    char            longname[80];   /* 长名称 */

    struct list_head devices;       /* 设备链表 */
    struct list_head controls;      /* 控制链表 */

    struct device   *dev;           /* 关联的设备 */
    /* ... */
};

/*
 * snd_pcm:PCM 设备
 * 每个声卡可以有多个 PCM 设备
 */
struct snd_pcm {
    struct snd_card *card;          /* 所属声卡 */
    int              device;        /* PCM 设备编号 */
    char             name[80];      /* PCM 设备名称 */

    struct snd_pcm_str streams[2];  /* 播放和录音流 */
    /* [SNDRV_PCM_STREAM_PLAYBACK] 和 [SNDRV_PCM_STREAM_CAPTURE] */
};

/*
 * snd_pcm_ops:PCM 操作函数集
 * 驱动必须实现这些函数
 */
struct snd_pcm_ops {
    int (*open)(struct snd_pcm_substream *substream);
    int (*close)(struct snd_pcm_substream *substream);
    int (*ioctl)(struct snd_pcm_substream *substream,
                 unsigned int cmd, void *arg);
    int (*hw_params)(struct snd_pcm_substream *substream,
                     struct snd_pcm_hw_params *params);
    int (*hw_free)(struct snd_pcm_substream *substream);
    int (*prepare)(struct snd_pcm_substream *substream);
    int (*trigger)(struct snd_pcm_substream *substream, int cmd);
    snd_pcm_uframes_t (*pointer)(struct snd_pcm_substream *substream);
    int (*copy)(struct snd_pcm_substream *substream, int channel,
                snd_pcm_uframes_t pos, void __user *buf,
                snd_pcm_uframes_t count);
    int (*mmap)(struct snd_pcm_substream *substream,
                struct vm_area_struct *vma);
};

17.2.2 ALSA 驱动的注册流程

复制代码
ALSA 驱动注册流程:

module_init()
    ↓
1. snd_card_new()          ← 创建声卡
    ↓
2. 创建 PCM 设备:
   snd_pcm_new()           ← 创建 PCM 设备
   snd_pcm_set_ops()       ← 设置 PCM 操作函数
    ↓
3. 创建混音器控制:
   snd_ctl_add()           ← 添加控制元素
    ↓
4. 初始化硬件
    ↓
5. snd_card_register()     ← 注册声卡(此后设备可用)

module_exit()
    ↓
snd_card_free()            ← 释放声卡(自动注销所有子设备)

17.2.3 简单的 ALSA 虚拟声卡驱动

c 复制代码
/*
 * virtual_sound.c ------ 简单的 ALSA 虚拟声卡驱动
 * 实现一个虚拟声卡,数据写入后直接丢弃(类似 /dev/null)
 * 用于理解 ALSA 驱动框架
 */

#include <linux/module.h>
#include <linux/init.h>
#include <linux/platform_device.h>
#include <sound/core.h>
#include <sound/pcm.h>
#include <sound/pcm_params.h>
#include <sound/control.h>
#include <sound/initval.h>

#define CARD_NAME    "VirtualSound"
#define PCM_NAME     "Virtual PCM"

/* 驱动私有数据 */
struct virtual_sound {
    struct snd_card     *card;
    struct snd_pcm      *pcm;
    struct timer_list    timer;     /* 模拟 DMA 完成的定时器 */
    spinlock_t           lock;
    struct snd_pcm_substream *substream;
    unsigned int         buf_pos;   /* 当前缓冲区位置 */
    int                  running;   /* 是否正在播放 */
};

/* ── PCM 硬件参数约束 ──────────────────────────────────────── */

static const struct snd_pcm_hardware virtual_pcm_hw = {
    .info           = SNDRV_PCM_INFO_MMAP |
                      SNDRV_PCM_INFO_MMAP_VALID |
                      SNDRV_PCM_INFO_INTERLEAVED |
                      SNDRV_PCM_INFO_BLOCK_TRANSFER,
    .formats        = SNDRV_PCM_FMTBIT_S16_LE |
                      SNDRV_PCM_FMTBIT_S24_LE |
                      SNDRV_PCM_FMTBIT_S32_LE,
    .rates          = SNDRV_PCM_RATE_8000_192000,
    .rate_min       = 8000,
    .rate_max       = 192000,
    .channels_min   = 1,
    .channels_max   = 2,
    .buffer_bytes_max = 32768,      /* 最大缓冲区:32KB */
    .period_bytes_min = 4096,       /* 最小 Period:4KB */
    .period_bytes_max = 16384,      /* 最大 Period:16KB */
    .periods_min    = 2,
    .periods_max    = 8,
};

/* ── 定时器回调(模拟 DMA 完成中断)──────────────────────── */

static void virtual_timer_callback(struct timer_list *t)
{
    struct virtual_sound *vs = from_timer(vs, t, timer);
    struct snd_pcm_substream *substream = vs->substream;
    struct snd_pcm_runtime *runtime;
    unsigned long flags;
    unsigned int period_bytes;

    if (!substream)
        return;

    runtime = substream->runtime;
    spin_lock_irqsave(&vs->lock, flags);

    if (!vs->running) {
        spin_unlock_irqrestore(&vs->lock, flags);
        return;
    }

    /* 更新缓冲区位置(模拟数据消耗)*/
    period_bytes = snd_pcm_lib_period_bytes(substream);
    vs->buf_pos += period_bytes;
    if (vs->buf_pos >= snd_pcm_lib_buffer_bytes(substream))
        vs->buf_pos = 0;

    spin_unlock_irqrestore(&vs->lock, flags);

    /* 通知 ALSA 核心:一个 Period 的数据已处理完 */
    snd_pcm_period_elapsed(substream);

    /* 重新设置定时器(模拟下一次 DMA 完成)*/
    if (vs->running) {
        unsigned int period_ms = (period_bytes * 1000) /
                                  (runtime->rate *
                                   runtime->channels *
                                   snd_pcm_format_physical_width(runtime->format) / 8);
        mod_timer(&vs->timer, jiffies + msecs_to_jiffies(period_ms));
    }
}

/* ── PCM 操作函数 ──────────────────────────────────────────── */

static int virtual_pcm_open(struct snd_pcm_substream *substream)
{
    struct virtual_sound *vs = snd_pcm_substream_chip(substream);

    substream->runtime->hw = virtual_pcm_hw;
    vs->substream = substream;

    pr_info("virtual_sound: PCM 打开(%s)\n",
            substream->stream == SNDRV_PCM_STREAM_PLAYBACK ?
            "播放" : "录音");
    return 0;
}

static int virtual_pcm_close(struct snd_pcm_substream *substream)
{
    struct virtual_sound *vs = snd_pcm_substream_chip(substream);

    vs->substream = NULL;
    pr_info("virtual_sound: PCM 关闭\n");
    return 0;
}

static int virtual_pcm_hw_params(struct snd_pcm_substream *substream,
                                  struct snd_pcm_hw_params *params)
{
    /*
     * 分配 DMA 缓冲区
     * snd_pcm_lib_malloc_pages:分配 PCM 缓冲区
     */
    return snd_pcm_lib_malloc_pages(substream,
                                     params_buffer_bytes(params));
}

static int virtual_pcm_hw_free(struct snd_pcm_substream *substream)
{
    return snd_pcm_lib_free_pages(substream);
}

static int virtual_pcm_prepare(struct snd_pcm_substream *substream)
{
    struct virtual_sound *vs = snd_pcm_substream_chip(substream);

    vs->buf_pos = 0;
    pr_info("virtual_sound: PCM 准备,采样率=%u,通道=%u,格式=%u\n",
            substream->runtime->rate,
            substream->runtime->channels,
            substream->runtime->format);
    return 0;
}

static int virtual_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
    struct virtual_sound *vs = snd_pcm_substream_chip(substream);
    struct snd_pcm_runtime *runtime = substream->runtime;

    switch (cmd) {
    case SNDRV_PCM_TRIGGER_START:
    case SNDRV_PCM_TRIGGER_RESUME:
        /* 开始播放/录音 */
        vs->running = 1;
        /* 启动定时器模拟 DMA */
        {
            unsigned int period_bytes = snd_pcm_lib_period_bytes(substream);
            unsigned int period_ms = (period_bytes * 1000) /
                                      (runtime->rate * runtime->channels *
                                       snd_pcm_format_physical_width(runtime->format) / 8);
            mod_timer(&vs->timer, jiffies + msecs_to_jiffies(period_ms));
        }
        pr_info("virtual_sound: 开始播放\n");
        break;

    case SNDRV_PCM_TRIGGER_STOP:
    case SNDRV_PCM_TRIGGER_SUSPEND:
        /* 停止播放/录音 */
        vs->running = 0;
        del_timer(&vs->timer);
        pr_info("virtual_sound: 停止播放\n");
        break;

    default:
        return -EINVAL;
    }

    return 0;
}

static snd_pcm_uframes_t virtual_pcm_pointer(struct snd_pcm_substream *substream)
{
    struct virtual_sound *vs = snd_pcm_substream_chip(substream);

    /* 返回当前播放/录音位置(以帧为单位)*/
    return bytes_to_frames(substream->runtime, vs->buf_pos);
}

static const struct snd_pcm_ops virtual_pcm_ops = {
    .open      = virtual_pcm_open,
    .close     = virtual_pcm_close,
    .ioctl     = snd_pcm_lib_ioctl,
    .hw_params = virtual_pcm_hw_params,
    .hw_free   = virtual_pcm_hw_free,
    .prepare   = virtual_pcm_prepare,
    .trigger   = virtual_pcm_trigger,
    .pointer   = virtual_pcm_pointer,
};

/* ── 混音器控制 ──────────────────────────────────────────────── */

static int volume_info(struct snd_kcontrol *kcontrol,
                        struct snd_ctl_elem_info *uinfo)
{
    uinfo->type  = SNDRV_CTL_ELEM_TYPE_INTEGER;
    uinfo->count = 2;  /* 左右声道 */
    uinfo->value.integer.min = 0;
    uinfo->value.integer.max = 100;
    return 0;
}

static int volume_get(struct snd_kcontrol *kcontrol,
                       struct snd_ctl_elem_value *ucontrol)
{
    ucontrol->value.integer.value[0] = 80;  /* 左声道音量 */
    ucontrol->value.integer.value[1] = 80;  /* 右声道音量 */
    return 0;
}

static int volume_put(struct snd_kcontrol *kcontrol,
                       struct snd_ctl_elem_value *ucontrol)
{
    int left  = ucontrol->value.integer.value[0];
    int right = ucontrol->value.integer.value[1];
    pr_info("virtual_sound: 设置音量 L=%d R=%d\n", left, right);
    return 1;  /* 返回1表示值已改变 */
}

static const struct snd_kcontrol_new volume_control = {
    .iface  = SNDRV_CTL_ELEM_IFACE_MIXER,
    .name   = "Master Playback Volume",
    .index  = 0,
    .info   = volume_info,
    .get    = volume_get,
    .put    = volume_put,
};

/* ── 模块加载函数 ──────────────────────────────────────────── */

static struct virtual_sound *g_vs;

static int __init virtual_sound_init(void)
{
    struct snd_card *card;
    struct snd_pcm *pcm;
    struct virtual_sound *vs;
    int ret;

    /* 1. 创建声卡 */
    ret = snd_card_new(NULL,          /* 父设备 */
                        -1,            /* 声卡编号(-1=自动)*/
                        "VirtualSound",/* ID */
                        THIS_MODULE,
                        sizeof(struct virtual_sound),
                        &card);
    if (ret < 0) {
        pr_err("virtual_sound: 创建声卡失败\n");
        return ret;
    }

    /* 获取私有数据 */
    vs = card->private_data;
    vs->card = card;
    spin_lock_init(&vs->lock);
    timer_setup(&vs->timer, virtual_timer_callback, 0);

    /* 设置声卡信息 */
    strcpy(card->driver,    "VirtualSound");
    strcpy(card->shortname, "Virtual Sound Card");
    strcpy(card->longname,  "Virtual Sound Card for Testing");

    /* 2. 创建 PCM 设备 */
    ret = snd_pcm_new(card,
                       PCM_NAME,   /* PCM 名称 */
                       0,          /* PCM 设备编号 */
                       1,          /* 播放子流数量 */
                       1,          /* 录音子流数量 */
                       &pcm);
    if (ret < 0) {
        pr_err("virtual_sound: 创建 PCM 设备失败\n");
        goto err_card;
    }

    vs->pcm = pcm;
    pcm->private_data = vs;
    strcpy(pcm->name, PCM_NAME);

    /* 设置 PCM 操作函数 */
    snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &virtual_pcm_ops);
    snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE,  &virtual_pcm_ops);

    /* 预分配 DMA 缓冲区 */
    snd_pcm_lib_preallocate_pages_for_all(pcm,
                                           SNDRV_DMA_TYPE_CONTINUOUS,
                                           snd_dma_continuous_data(GFP_KERNEL),
                                           32768, 32768);

    /* 3. 添加混音器控制 */
    ret = snd_ctl_add(card, snd_ctl_new1(&volume_control, vs));
    if (ret < 0) {
        pr_err("virtual_sound: 添加混音器控制失败\n");
        goto err_card;
    }

    /* 4. 注册声卡 */
    ret = snd_card_register(card);
    if (ret < 0) {
        pr_err("virtual_sound: 注册声卡失败\n");
        goto err_card;
    }

    g_vs = vs;
    pr_info("virtual_sound: 声卡注册成功,编号 %d\n", card->number);
    return 0;

err_card:
    snd_card_free(card);
    return ret;
}

static void __exit virtual_sound_exit(void)
{
    if (g_vs) {
        del_timer_sync(&g_vs->timer);
        snd_card_free(g_vs->card);
    }
    pr_info("virtual_sound: 驱动已卸载\n");
}

module_init(virtual_sound_init);
module_exit(virtual_sound_exit);

MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("ALSA 虚拟声卡驱动");

17.3 ASoC 架构

17.3.1 ASoC 的背景

ASoC(ALSA System on Chip)是专为嵌入式系统设计的 ALSA 扩展框架,解决了传统 ALSA 驱动在嵌入式场景中的问题:

复制代码
传统 ALSA 驱动的问题(嵌入式场景):

问题一:代码重复
  每款开发板都需要重写整个音频驱动
  即使使用相同的 Codec 芯片,也需要重复实现

问题二:耦合度高
  Codec 驱动与 SoC I2S 控制器驱动紧密耦合
  更换 Codec 或 SoC 需要大量修改

问题三:电源管理差
  没有统一的音频电源管理机制
  无法精细控制各组件的电源状态

ASoC 的解决方案:
  将音频驱动分为三个独立的组件:
  1. Codec 驱动:针对音频编解码器芯片(与 SoC 无关)
  2. Platform 驱动:针对 SoC 的 I2S/PCM 控制器(与 Codec 无关)
  3. Machine 驱动:将 Codec 和 Platform 连接起来(板级配置)

17.3.2 ASoC 架构图

复制代码
ASoC 架构:

┌─────────────────────────────────────────────────────────────┐
│                      用户空间                                │
│  aplay / arecord / PulseAudio / Android AudioFlinger        │
└──────────────────────────┬──────────────────────────────────┘
                           │
┌──────────────────────────▼──────────────────────────────────┐
│                   ALSA 核心层                                │
└──────────────────────────┬──────────────────────────────────┘
                           │
┌──────────────────────────▼──────────────────────────────────┐
│                   ASoC 核心层(sound/soc/soc-core.c)        │
│  负责 Machine/Platform/Codec 的注册和绑定                    │
└──────┬───────────────────┼───────────────────┬──────────────┘
       │                   │                   │
┌──────▼──────┐   ┌────────▼────────┐   ┌──────▼──────┐
│  Machine    │   │    Platform     │   │    Codec    │
│  驱动       │   │    驱动         │   │    驱动     │
│             │   │                 │   │             │
│ 板级配置    │   │ I2S/PCM 控制器  │   │ WM8960      │
│ DAI 链接    │   │ DMA 控制器      │   │ ES8316      │
│ 音频路由    │   │ SoC 相关        │   │ ALC5651     │
└──────┬──────┘   └────────┬────────┘   └──────┬──────┘
       │                   │                   │
       └───────────────────┼───────────────────┘
                           │
┌──────────────────────────▼──────────────────────────────────┐
│                      硬件层                                  │
│  I2S 总线 ←→ Codec 芯片 ←→ 扬声器/麦克风                   │
│  I2C/SPI 总线 ←→ Codec 控制接口                             │
└─────────────────────────────────────────────────────────────┘

17.3.3 ASoC 核心概念

复制代码
ASoC 核心概念:

DAI(Digital Audio Interface,数字音频接口):
  SoC 与 Codec 之间的数字音频连接
  常见类型:I2S、PCM、TDM、AC97、S/PDIF

DAI Link(DAI 链接):
  描述 SoC 的 DAI 与 Codec 的 DAI 之间的连接关系
  在 Machine 驱动中定义

DAPM(Dynamic Audio Power Management,动态音频电源管理):
  根据音频路由自动管理各组件的电源状态
  只有实际使用的音频路径才会上电
  减少功耗

Widget(音频组件):
  DAPM 中的基本单元
  类型:DAC、ADC、Mixer、MUX、PGA、HP、SPK 等
  通过 Route 连接形成音频路径

Route(音频路由):
  描述 Widget 之间的连接关系
  {目标Widget, 控制开关, 源Widget}

17.4 ASoC Codec 驱动

17.4.1 Codec 驱动的作用

Codec(编解码器)驱动负责控制音频编解码器芯片,实现:

  • 模拟信号与数字信号的转换(ADC/DAC)
  • 音量控制、静音、EQ 等音频处理
  • 音频路由(麦克风→ADC、DAC→扬声器等)

17.4.2 Codec 驱动的核心结构

c 复制代码
#include <sound/soc.h>

/*
 * snd_soc_codec_driver:Codec 驱动
 */
struct snd_soc_codec_driver {
    /* 探测和移除 */
    int (*probe)(struct snd_soc_codec *codec);
    int (*remove)(struct snd_soc_codec *codec);

    /* 挂起和恢复 */
    int (*suspend)(struct snd_soc_codec *codec);
    int (*resume)(struct snd_soc_codec *codec);

    /* 寄存器 I/O */
    unsigned int (*read)(struct snd_soc_codec *codec, unsigned int reg);
    int (*write)(struct snd_soc_codec *codec, unsigned int reg,
                 unsigned int val);

    /* 控制(混音器)*/
    const struct snd_kcontrol_new *controls;
    int num_controls;

    /* DAPM Widget */
    const struct snd_soc_dapm_widget *dapm_widgets;
    int num_dapm_widgets;

    /* DAPM Route */
    const struct snd_soc_dapm_route *dapm_routes;
    int num_dapm_routes;
};

/*
 * snd_soc_dai_driver:DAI 驱动
 * 描述 Codec 的数字音频接口
 */
struct snd_soc_dai_driver {
    const char *name;           /* DAI 名称 */

    /* 播放能力 */
    struct snd_soc_pcm_stream playback;

    /* 录音能力 */
    struct snd_soc_pcm_stream capture;

    /* DAI 操作函数 */
    const struct snd_soc_dai_ops *ops;
};

/*
 * snd_soc_dai_ops:DAI 操作函数集
 */
struct snd_soc_dai_ops {
    /* 设置 DAI 格式(I2S/PCM/TDM 等)*/
    int (*set_fmt)(struct snd_soc_dai *dai, unsigned int fmt);

    /* 设置采样率和位深 */
    int (*hw_params)(struct snd_soc_pcm_runtime *rtd,
                     struct snd_pcm_hw_params *params,
                     struct snd_soc_dai *dai);

    /* 设置时钟 */
    int (*set_sysclk)(struct snd_soc_dai *dai, int clk_id,
                      unsigned int freq, int dir);

    /* 设置 PLL */
    int (*set_pll)(struct snd_soc_dai *dai, int pll_id, int source,
                   unsigned int freq_in, unsigned int freq_out);

    /* 数字静音 */
    int (*digital_mute)(struct snd_soc_dai *dai, int mute);

    /* 触发(开始/停止)*/
    int (*trigger)(struct snd_pcm_substream *substream, int cmd,
                   struct snd_soc_dai *dai);
};

17.4.3 完整的 Codec 驱动案例(WM8960)

c 复制代码
/*
 * wm8960_simple.c ------ WM8960 音频 Codec 驱动(简化版)
 *
 * WM8960 是 Wolfson(现 Cirrus Logic)的立体声音频 Codec:
 * - 支持 I2S/PCM 数字音频接口
 * - 内置耳机放大器和扬声器放大器
 * - 通过 I2C 控制接口配置
 * - 支持 8kHz~48kHz 采样率
 */

#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/regmap.h>
#include <sound/soc.h>
#include <sound/soc-dapm.h>
#include <sound/tlv.h>

/* WM8960 寄存器定义 */
#define WM8960_LINVOL    0x00   /* 左输入音量 */
#define WM8960_RINVOL    0x01   /* 右输入音量 */
#define WM8960_LOUT1     0x02   /* LOUT1 音量(耳机左)*/
#define WM8960_ROUT1     0x03   /* ROUT1 音量(耳机右)*/
#define WM8960_CLOCK1    0x04   /* 时钟1 */
#define WM8960_DACCTL1   0x05   /* DAC 控制1 */
#define WM8960_DACCTL2   0x06   /* DAC 控制2 */
#define WM8960_IFACE1    0x07   /* 音频接口1 */
#define WM8960_CLOCK2    0x08   /* 时钟2 */
#define WM8960_IFACE2    0x09   /* 音频接口2 */
#define WM8960_LDAC      0x0A   /* 左 DAC 音量 */
#define WM8960_RDAC      0x0B   /* 右 DAC 音量 */
#define WM8960_RESET     0x0F   /* 复位 */
#define WM8960_3D        0x10   /* 3D 增强 */
#define WM8960_ALC1      0x11   /* ALC1 */
#define WM8960_ALC2      0x12   /* ALC2 */
#define WM8960_ALC3      0x13   /* ALC3 */
#define WM8960_NOISEG    0x14   /* 噪声门 */
#define WM8960_LADC      0x15   /* 左 ADC 音量 */
#define WM8960_RADC      0x16   /* 右 ADC 音量 */
#define WM8960_ADDCTL1   0x17   /* 附加控制1 */
#define WM8960_ADDCTL2   0x18   /* 附加控制2 */
#define WM8960_POWER1    0x19   /* 电源管理1 */
#define WM8960_POWER2    0x1A   /* 电源管理2 */
#define WM8960_ADDCTL3   0x1B   /* 附加控制3 */
#define WM8960_APOP1     0x1C   /* 防爆音控制1 */
#define WM8960_APOP2     0x1D   /* 防爆音控制2 */
#define WM8960_LINPATH   0x20   /* 左输入路径 */
#define WM8960_RINPATH   0x21   /* 右输入路径 */
#define WM8960_LOUTMIX   0x22   /* 左输出混音 */
#define WM8960_ROUTMIX   0x25   /* 右输出混音 */
#define WM8960_MONOMIX1  0x26   /* 单声道混音1 */
#define WM8960_MONOMIX2  0x27   /* 单声道混音2 */
#define WM8960_LOUT2     0x28   /* LOUT2 音量(扬声器左)*/
#define WM8960_ROUT2     0x29   /* ROUT2 音量(扬声器右)*/
#define WM8960_MONO      0x2A   /* 单声道输出 */
#define WM8960_INBMIX1   0x2B   /* 输入 Boost 混音1 */
#define WM8960_INBMIX2   0x2C   /* 输入 Boost 混音2 */

/* WM8960 寄存器默认值 */
static const struct reg_default wm8960_reg_defaults[] = {
    { WM8960_LINVOL,   0x0097 },
    { WM8960_RINVOL,   0x0097 },
    { WM8960_LOUT1,    0x00FF },
    { WM8960_ROUT1,    0x00FF },
    { WM8960_CLOCK1,   0x0000 },
    { WM8960_DACCTL1,  0x0008 },
    { WM8960_IFACE1,   0x0002 },
    /* ... */
};

/* regmap 配置 */
static const struct regmap_config wm8960_regmap = {
    .reg_bits  = 7,
    .val_bits  = 9,
    .max_register = WM8960_INBMIX2,
    .reg_defaults = wm8960_reg_defaults,
    .num_reg_defaults = ARRAY_SIZE(wm8960_reg_defaults),
    .cache_type = REGCACHE_RBTREE,
};

/* 驱动私有数据 */
struct wm8960_priv {
    struct regmap   *regmap;
    int              sysclk;
    int              clk_id;
};

/* ── 音量控制(TLV)──────────────────────────────────────────── */

/* 定义 TLV(Tagged Length Value)音量范围 */
/* 耳机音量:-73dB ~ +6dB,步进 1dB */
static const DECLARE_TLV_DB_SCALE(out_tlv, -7300, 100, 0);

/* DAC 数字音量:-127dB ~ 0dB,步进 0.5dB */
static const DECLARE_TLV_DB_SCALE(dac_tlv, -12700, 50, 1);

/* ADC 数字音量:-97dB ~ +30dB,步进 0.5dB */
static const DECLARE_TLV_DB_SCALE(adc_tlv, -9700, 50, 0);

/* ── 混音器控制 ──────────────────────────────────────────────── */

static const struct snd_kcontrol_new wm8960_snd_controls[] = {
    /* 耳机音量控制 */
    SOC_DOUBLE_R_TLV("Headphone Playback Volume",
                      WM8960_LOUT1, WM8960_ROUT1,
                      0, 127, 0, out_tlv),

    /* 扬声器音量控制 */
    SOC_DOUBLE_R_TLV("Speaker Playback Volume",
                      WM8960_LOUT2, WM8960_ROUT2,
                      0, 127, 0, out_tlv),

    /* DAC 数字音量 */
    SOC_DOUBLE_R_TLV("DAC Playback Volume",
                      WM8960_LDAC, WM8960_RDAC,
                      0, 255, 0, dac_tlv),

    /* ADC 数字音量 */
    SOC_DOUBLE_R_TLV("Capture Volume",
                      WM8960_LADC, WM8960_RADC,
                      0, 255, 0, adc_tlv),

    /* 麦克风 Boost */
    SOC_DOUBLE_R("Mic Boost Volume",
                  WM8960_LINPATH, WM8960_RINPATH,
                  4, 3, 0),

    /* 3D 增强 */
    SOC_SINGLE("3D Switch", WM8960_3D, 0, 1, 0),
    SOC_SINGLE("3D Volume", WM8960_3D, 1, 15, 0),
};

/* ── DAPM Widget ──────────────────────────────────────────────── */

static const struct snd_soc_dapm_widget wm8960_dapm_widgets[] = {
    /* 输入 */
    SND_SOC_DAPM_INPUT("LINPUT1"),
    SND_SOC_DAPM_INPUT("RINPUT1"),
    SND_SOC_DAPM_INPUT("LINPUT2"),
    SND_SOC_DAPM_INPUT("RINPUT2"),
    SND_SOC_DAPM_INPUT("LINPUT3"),
    SND_SOC_DAPM_INPUT("RINPUT3"),

    /* 麦克风偏置 */
    SND_SOC_DAPM_MICBIAS("MICB", WM8960_POWER1, 1, 0),

    /* ADC */
    SND_SOC_DAPM_ADC("Left ADC",  "Capture", WM8960_POWER1, 3, 0),
    SND_SOC_DAPM_ADC("Right ADC", "Capture", WM8960_POWER1, 2, 0),

    /* DAC */
    SND_SOC_DAPM_DAC("Left DAC",  "Playback", WM8960_POWER2, 8, 0),
    SND_SOC_DAPM_DAC("Right DAC", "Playback", WM8960_POWER2, 7, 0),

    /* 输出混音器 */
    SND_SOC_DAPM_MIXER("Left Output Mixer",  WM8960_POWER3, 3, 0,
                        NULL, 0),
    SND_SOC_DAPM_MIXER("Right Output Mixer", WM8960_POWER3, 2, 0,
                        NULL, 0),

    /* 耳机放大器 */
    SND_SOC_DAPM_HP("HP_L", NULL),
    SND_SOC_DAPM_HP("HP_R", NULL),

    /* 扬声器放大器 */
    SND_SOC_DAPM_SPK("SPK_L", NULL),
    SND_SOC_DAPM_SPK("SPK_R", NULL),

    /* 输出 */
    SND_SOC_DAPM_OUTPUT("HP_L"),
    SND_SOC_DAPM_OUTPUT("HP_R"),
    SND_SOC_DAPM_OUTPUT("SPK_L"),
    SND_SOC_DAPM_OUTPUT("SPK_R"),
};

/* ── DAPM Route ──────────────────────────────────────────────── */

static const struct snd_soc_dapm_route wm8960_dapm_routes[] = {
    /* 输入路径:麦克风 → ADC */
    { "Left ADC",  NULL, "LINPUT1" },
    { "Right ADC", NULL, "RINPUT1" },

    /* 播放路径:DAC → 输出混音器 → 耳机/扬声器 */
    { "Left Output Mixer",  NULL, "Left DAC"  },
    { "Right Output Mixer", NULL, "Right DAC" },

    { "HP_L", NULL, "Left Output Mixer"  },
    { "HP_R", NULL, "Right Output Mixer" },

    { "SPK_L", NULL, "Left Output Mixer"  },
    { "SPK_R", NULL, "Right Output Mixer" },
};

/* ── DAI 操作函数 ──────────────────────────────────────────────── */

static int wm8960_set_dai_fmt(struct snd_soc_dai *codec_dai,
                               unsigned int fmt)
{
    struct snd_soc_codec *codec = codec_dai->codec;
    struct wm8960_priv *wm8960 = snd_soc_codec_get_drvdata(codec);
    u16 iface = 0;

    /* 设置主从模式 */
    switch (fmt & SND_SOC_DAIFMT_MASTER_MASK) {
    case SND_SOC_DAIFMT_CBM_CFM:  /* Codec 作为主设备 */
        iface |= 0x0040;
        break;
    case SND_SOC_DAIFMT_CBS_CFS:  /* Codec 作为从设备 */
        break;
    default:
        return -EINVAL;
    }

    /* 设置音频格式 */
    switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) {
    case SND_SOC_DAIFMT_I2S:      /* I2S 格式 */
        iface |= 0x0002;
        break;
    case SND_SOC_DAIFMT_RIGHT_J:  /* 右对齐 */
        break;
    case SND_SOC_DAIFMT_LEFT_J:   /* 左对齐 */
        iface |= 0x0001;
        break;
    default:
        return -EINVAL;
    }

    /* 写入接口寄存器 */
    snd_soc_write(codec, WM8960_IFACE1, iface);
    return 0;
}

static int wm8960_hw_params(struct snd_pcm_substream *substream,
                             struct snd_pcm_hw_params *params,
                             struct snd_soc_dai *dai)
{
    struct snd_soc_codec *codec = dai->codec;
    u16 iface = snd_soc_read(codec, WM8960_IFACE1) & 0xFFF3;

    /* 设置位深 */
    switch (params_width(params)) {
    case 16:
        break;
    case 20:
        iface |= 0x0004;
        break;
    case 24:
        iface |= 0x0008;
        break;
    case 32:
        iface |= 0x000C;
        break;
    }

    snd_soc_write(codec, WM8960_IFACE1, iface);
    return 0;
}

static int wm8960_set_sysclk(struct snd_soc_dai *dai, int clk_id,
                              unsigned int freq, int dir)
{
    struct snd_soc_codec *codec = dai->codec;
    struct wm8960_priv *wm8960 = snd_soc_codec_get_drvdata(codec);

    wm8960->sysclk = freq;
    wm8960->clk_id = clk_id;
    return 0;
}

static const struct snd_soc_dai_ops wm8960_dai_ops = {
    .set_fmt    = wm8960_set_dai_fmt,
    .hw_params  = wm8960_hw_params,
    .set_sysclk = wm8960_set_sysclk,
};

/* ── DAI 驱动 ──────────────────────────────────────────────────── */

static struct snd_soc_dai_driver wm8960_dai = {
    .name = "wm8960-hifi",
    .playback = {
        .stream_name  = "Playback",
        .channels_min = 1,
        .channels_max = 2,
        .rates        = SNDRV_PCM_RATE_8000_48000,
        .formats      = SNDRV_PCM_FMTBIT_S16_LE |
                        SNDRV_PCM_FMTBIT_S20_3LE |
                        SNDRV_PCM_FMTBIT_S24_LE,
    },
    .capture = {
        .stream_name  = "Capture",
        .channels_min = 1,
        .channels_max = 2,
        .rates        = SNDRV_PCM_RATE_8000_48000,
        .formats      = SNDRV_PCM_FMTBIT_S16_LE |
                        SNDRV_PCM_FMTBIT_S20_3LE |
                        SNDRV_PCM_FMTBIT_S24_LE,
    },
    .ops = &wm8960_dai_ops,
};

/* ── Codec 驱动 probe ──────────────────────────────────────────── */

static int wm8960_probe(struct snd_soc_codec *codec)
{
    struct wm8960_priv *wm8960 = snd_soc_codec_get_drvdata(codec);

    /* 复位 Codec */
    snd_soc_write(codec, WM8960_RESET, 0);

    /* 使能 VREF 和 VMID(必须先使能)*/
    snd_soc_write(codec, WM8960_POWER1, 0x00C0);
    msleep(100);

    /* 使能 DAC 和 ADC */
    snd_soc_write(codec, WM8960_POWER2, 0x01E0);

    /* 配置默认音量 */
    snd_soc_write(codec, WM8960_LDAC, 0x00FF);  /* 0dB */
    snd_soc_write(codec, WM8960_RDAC, 0x01FF);  /* 0dB,同步更新 */

    /* 配置耳机音量 */
    snd_soc_write(codec, WM8960_LOUT1, 0x006F);  /* -10dB */
    snd_soc_write(codec, WM8960_ROUT1, 0x016F);  /* -10dB,同步更新 */

    dev_info(codec->dev, "WM8960 初始化成功\n");
    return 0;
}

/* ── Codec 驱动结构体 ──────────────────────────────────────────── */

static struct snd_soc_codec_driver soc_codec_dev_wm8960 = {
    .probe          = wm8960_probe,
    .controls       = wm8960_snd_controls,
    .num_controls   = ARRAY_SIZE(wm8960_snd_controls),
    .dapm_widgets   = wm8960_dapm_widgets,
    .num_dapm_widgets = ARRAY_SIZE(wm8960_dapm_widgets),
    .dapm_routes    = wm8960_dapm_routes,
    .num_dapm_routes = ARRAY_SIZE(wm8960_dapm_routes),
};

/* ── I2C 驱动 ──────────────────────────────────────────────────── */

static int wm8960_i2c_probe(struct i2c_client *i2c,
                             const struct i2c_device_id *id)
{
    struct wm8960_priv *wm8960;
    int ret;

    wm8960 = devm_kzalloc(&i2c->dev, sizeof(*wm8960), GFP_KERNEL);
    if (!wm8960)
        return -ENOMEM;

    /* 初始化 regmap */
    wm8960->regmap = devm_regmap_init_i2c(i2c, &wm8960_regmap);
    if (IS_ERR(wm8960->regmap))
        return PTR_ERR(wm8960->regmap);

    i2c_set_clientdata(i2c, wm8960);

    /* 注册 Codec 驱动 */
    ret = snd_soc_register_codec(&i2c->dev,
                                  &soc_codec_dev_wm8960,
                                  &wm8960_dai, 1);
    if (ret)
        dev_err(&i2c->dev, "注册 Codec 失败:%d\n", ret);

    return ret;
}

static int wm8960_i2c_remove(struct i2c_client *client)
{
    snd_soc_unregister_codec(&client->dev);
    return 0;
}

static const struct i2c_device_id wm8960_i2c_id[] = {
    { "wm8960", 0 },
    {}
};
MODULE_DEVICE_TABLE(i2c, wm8960_i2c_id);

static const struct of_device_id wm8960_of_match[] = {
    { .compatible = "wlf,wm8960" },
    {}
};
MODULE_DEVICE_TABLE(of, wm8960_of_match);

static struct i2c_driver wm8960_i2c_driver = {
    .driver = {
        .name           = "wm8960",
        .of_match_table = wm8960_of_match,
    },
    .probe    = wm8960_i2c_probe,
    .remove   = wm8960_i2c_remove,
    .id_table = wm8960_i2c_id,
};
module_i2c_driver(wm8960_i2c_driver);

MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("WM8960 音频 Codec 驱动");

17.5 ASoC 平台驱动

17.5.1 Platform 驱动的作用

Platform 驱动负责控制 SoC 内置的 I2S 控制器DMA 控制器,实现音频数据的 DMA 传输:

复制代码
Platform 驱动的职责:

1. I2S 控制器驱动
   - 配置 I2S 时序(主从模式、数据格式、时钟)
   - 控制 I2S 数据传输

2. DMA 驱动
   - 配置 DMA 通道(源地址、目标地址、传输大小)
   - 启动/停止 DMA 传输
   - 处理 DMA 完成中断

3. PCM 操作
   - 分配 DMA 缓冲区
   - 实现 trigger(开始/停止)
   - 实现 pointer(当前位置)

17.5.2 Platform 驱动框架

c 复制代码
/*
 * i2s_platform.c ------ I2S Platform 驱动框架
 */

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/dmaengine.h>
#include <sound/soc.h>
#include <sound/dmaengine_pcm.h>

/* I2S 控制器寄存器 */
#define I2S_CTRL_REG    0x00
#define I2S_CLK_REG     0x04
#define I2S_TXFIFO_REG  0x08
#define I2S_RXFIFO_REG  0x0C
#define I2S_STATUS_REG  0x10

/* I2S 控制寄存器位 */
#define I2S_CTRL_EN     BIT(0)   /* 使能 I2S */
#define I2S_CTRL_TXEN   BIT(1)   /* 使能发送 */
#define I2S_CTRL_RXEN   BIT(2)   /* 使能接收 */
#define I2S_CTRL_MASTER BIT(3)   /* 主设备模式 */

/* Platform 驱动私有数据 */
struct my_i2s_dev {
    void __iomem            *base;
    struct clk              *clk;
    int                      irq;
    struct dma_chan          *tx_chan;   /* 发送 DMA 通道 */
    struct dma_chan          *rx_chan;   /* 接收 DMA 通道 */
    dma_addr_t               fifo_addr; /* FIFO 物理地址 */
};

/* ── DAI 操作函数 ──────────────────────────────────────────────── */

static int my_i2s_set_fmt(struct snd_soc_dai *dai, unsigned int fmt)
{
    struct my_i2s_dev *i2s = snd_soc_dai_get_drvdata(dai);
    u32 ctrl = readl(i2s->base + I2S_CTRL_REG);

    /* 设置主从模式 */
    switch (fmt & SND_SOC_DAIFMT_MASTER_MASK) {
    case SND_SOC_DAIFMT_CBM_CFM:  /* I2S 作为从设备 */
        ctrl &= ~I2S_CTRL_MASTER;
        break;
    case SND_SOC_DAIFMT_CBS_CFS:  /* I2S 作为主设备 */
        ctrl |= I2S_CTRL_MASTER;
        break;
    }

    writel(ctrl, i2s->base + I2S_CTRL_REG);
    return 0;
}

static int my_i2s_hw_params(struct snd_pcm_substream *substream,
                             struct snd_pcm_hw_params *params,
                             struct snd_soc_dai *dai)
{
    struct my_i2s_dev *i2s = snd_soc_dai_get_drvdata(dai);
    unsigned int rate = params_rate(params);
    unsigned int clk_rate;

    /* 根据采样率配置时钟分频 */
    clk_rate = clk_get_rate(i2s->clk);
    writel(clk_rate / (rate * 64), i2s->base + I2S_CLK_REG);

    return 0;
}

static int my_i2s_trigger(struct snd_pcm_substream *substream,
                           int cmd, struct snd_soc_dai *dai)
{
    struct my_i2s_dev *i2s = snd_soc_dai_get_drvdata(dai);
    u32 ctrl = readl(i2s->base + I2S_CTRL_REG);

    switch (cmd) {
    case SNDRV_PCM_TRIGGER_START:
    case SNDRV_PCM_TRIGGER_RESUME:
        if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
            ctrl |= I2S_CTRL_TXEN;
        else
            ctrl |= I2S_CTRL_RXEN;
        ctrl |= I2S_CTRL_EN;
        break;

    case SNDRV_PCM_TRIGGER_STOP:
    case SNDRV_PCM_TRIGGER_SUSPEND:
        if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
            ctrl &= ~I2S_CTRL_TXEN;
        else
            ctrl &= ~I2S_CTRL_RXEN;
        break;
    }

    writel(ctrl, i2s->base + I2S_CTRL_REG);
    return 0;
}

static const struct snd_soc_dai_ops my_i2s_dai_ops = {
    .set_fmt   = my_i2s_set_fmt,
    .hw_params = my_i2s_hw_params,
    .trigger   = my_i2s_trigger,
};

/* ── DAI 驱动 ──────────────────────────────────────────────────── */

static struct snd_soc_dai_driver my_i2s_dai = {
    .name = "my-i2s",
    .playback = {
        .stream_name  = "Playback",
        .channels_min = 1,
        .channels_max = 2,
        .rates        = SNDRV_PCM_RATE_8000_192000,
        .formats      = SNDRV_PCM_FMTBIT_S16_LE |
                        SNDRV_PCM_FMTBIT_S24_LE |
                        SNDRV_PCM_FMTBIT_S32_LE,
    },
    .capture = {
        .stream_name  = "Capture",
        .channels_min = 1,
        .channels_max = 2,
        .rates        = SNDRV_PCM_RATE_8000_192000,
        .formats      = SNDRV_PCM_FMTBIT_S16_LE |
                        SNDRV_PCM_FMTBIT_S24_LE |
                        SNDRV_PCM_FMTBIT_S32_LE,
    },
    .ops = &my_i2s_dai_ops,
};

/* ── DMA PCM 配置 ──────────────────────────────────────────────── */

static const struct snd_dmaengine_pcm_config my_dmaengine_pcm_config = {
    .prepare_slave_config = snd_dmaengine_pcm_prepare_slave_config,
};

/* ── Platform 驱动 ──────────────────────────────────────────────── */

static const struct snd_soc_component_driver my_i2s_component = {
    .name = "my-i2s",
};

static int my_i2s_probe(struct platform_device *pdev)
{
    struct my_i2s_dev *i2s;
    struct resource *res;
    int ret;

    i2s = devm_kzalloc(&pdev->dev, sizeof(*i2s), GFP_KERNEL);
    if (!i2s)
        return -ENOMEM;

    /* 获取寄存器资源 */
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    i2s->base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(i2s->base))
        return PTR_ERR(i2s->base);

    /* FIFO 物理地址(用于 DMA)*/
    i2s->fifo_addr = res->start + I2S_TXFIFO_REG;

    /* 获取时钟 */
    i2s->clk = devm_clk_get(&pdev->dev, "i2s_clk");
    if (IS_ERR(i2s->clk))
        return PTR_ERR(i2s->clk);
    clk_prepare_enable(i2s->clk);

    /* 申请 DMA 通道 */
    i2s->tx_chan = dma_request_slave_channel(&pdev->dev, "tx");
    i2s->rx_chan = dma_request_slave_channel(&pdev->dev, "rx");

    dev_set_drvdata(&pdev->dev, i2s);

    /* 注册 DAI 驱动 */
    ret = devm_snd_soc_register_component(&pdev->dev,
                                           &my_i2s_component,
                                           &my_i2s_dai, 1);
    if (ret)
        return ret;

    /* 注册 DMA PCM */
    ret = devm_snd_dmaengine_pcm_register(&pdev->dev,
                                           &my_dmaengine_pcm_config, 0);
    if (ret)
        return ret;

    dev_info(&pdev->dev, "I2S Platform 驱动注册成功\n");
    return 0;
}

static const struct of_device_id my_i2s_of_match[] = {
    { .compatible = "myvendor,my-i2s" },
    {}
};

static struct platform_driver my_i2s_driver = {
    .probe  = my_i2s_probe,
    .driver = {
        .name           = "my-i2s",
        .of_match_table = my_i2s_of_match,
    },
};
module_platform_driver(my_i2s_driver);

MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("My I2S Platform 驱动");

17.6 ASoC Machine 驱动

17.6.1 Machine 驱动的作用

Machine 驱动是 ASoC 的胶水层,负责将 Codec 驱动和 Platform 驱动连接起来,描述具体开发板的音频硬件配置:

复制代码
Machine 驱动的职责:

1. 定义 DAI Link(连接 Platform DAI 和 Codec DAI)
2. 配置音频路由(哪个麦克风连接到哪个输入)
3. 处理板级特定操作(耳机检测、扬声器使能 GPIO 等)
4. 配置时钟(为 Codec 提供 MCLK)
5. 注册声卡

17.6.2 完整的 Machine 驱动案例

c 复制代码
/*
 * my_board_audio.c ------ 开发板音频 Machine 驱动
 *
 * 硬件配置:
 * - SoC:i.MX6ULL
 * - Codec:WM8960(通过 I2C 控制,I2S 传输音频数据)
 * - 连接:SoC I2S1 ←→ WM8960 I2S
 * - 耳机检测:GPIO1_IO05(低电平表示耳机插入)
 * - 扬声器使能:GPIO1_IO06(高电平使能)
 */

#include <linux/module.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <sound/soc.h>
#include <sound/jack.h>

/* 耳机插拔检测 */
static struct snd_soc_jack headphone_jack;
static struct snd_soc_jack_pin headphone_jack_pins[] = {
    {
        .pin  = "Headphone Jack",
        .mask = SND_JACK_HEADPHONE,
    },
};

/* Machine 驱动私有数据 */
struct my_board_audio {
    int hp_det_gpio;    /* 耳机检测 GPIO */
    int spk_en_gpio;    /* 扬声器使能 GPIO */
};

/* ── DAI 初始化(每次打开音频流时调用)──────────────────────── */

static int my_board_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 sample_rate = params_rate(params);
    unsigned int mclk;
    int ret;

    /*
     * 为 WM8960 提供 MCLK
     * WM8960 要求 MCLK = 采样率 × 256
     */
    mclk = sample_rate * 256;

    /* 设置 CPU DAI(I2S 控制器)的系统时钟 */
    ret = snd_soc_dai_set_sysclk(cpu_dai, 0, mclk, SND_SOC_CLOCK_OUT);
    if (ret) {
        pr_err("my_board: 设置 CPU DAI 时钟失败:%d\n", ret);
        return ret;
    }

    /* 设置 Codec DAI(WM8960)的系统时钟 */
    ret = snd_soc_dai_set_sysclk(codec_dai, 0, mclk, SND_SOC_CLOCK_IN);
    if (ret) {
        pr_err("my_board: 设置 Codec DAI 时钟失败:%d\n", ret);
        return ret;
    }

    return 0;
}

/* ── 声卡初始化(声卡注册时调用一次)──────────────────────────── */

static int my_board_init(struct snd_soc_pcm_runtime *rtd)
{
    struct snd_soc_codec *codec = rtd->codec;
    struct snd_soc_dapm_context *dapm = &codec->dapm;
    int ret;

    /* 注册耳机插拔检测 */
    ret = snd_soc_card_jack_new(rtd->card, "Headphone Jack",
                                 SND_JACK_HEADPHONE,
                                 &headphone_jack,
                                 headphone_jack_pins,
                                 ARRAY_SIZE(headphone_jack_pins));
    if (ret)
        return ret;

    /* 设置 DAPM 端点状态 */
    /* 告诉 DAPM 哪些端点是连接的(不连接的端点会被断电)*/
    snd_soc_dapm_enable_pin(dapm, "HP_L");
    snd_soc_dapm_enable_pin(dapm, "HP_R");
    snd_soc_dapm_enable_pin(dapm, "SPK_L");
    snd_soc_dapm_enable_pin(dapm, "SPK_R");
    snd_soc_dapm_enable_pin(dapm, "LINPUT1");
    snd_soc_dapm_enable_pin(dapm, "RINPUT1");

    /* 禁用未使用的端点 */
    snd_soc_dapm_nc_pin(dapm, "LINPUT2");
    snd_soc_dapm_nc_pin(dapm, "RINPUT2");
    snd_soc_dapm_nc_pin(dapm, "LINPUT3");
    snd_soc_dapm_nc_pin(dapm, "RINPUT3");

    snd_soc_dapm_sync(dapm);

    return 0;
}

/* ── Machine 驱动操作函数 ──────────────────────────────────────── */

static const struct snd_soc_ops my_board_ops = {
    .hw_params = my_board_hw_params,
};

/* ── DAI Link(连接 Platform 和 Codec)──────────────────────────── */

static struct snd_soc_dai_link my_board_dai_links[] = {
    {
        .name           = "WM8960 HiFi",
        .stream_name    = "WM8960 HiFi",

        /* CPU(Platform)端 DAI */
        .cpu_dai_name   = "my-i2s",        /* Platform DAI 名称 */
        .platform_name  = "my-i2s",        /* Platform 驱动名称 */

        /* Codec 端 DAI */
        .codec_dai_name = "wm8960-hifi",   /* Codec DAI 名称 */
        .codec_name     = "wm8960.1-001a", /* Codec 设备名称(I2C总线1,地址0x1a)*/

        /* 音频格式:I2S,Codec 作为从设备 */
        .dai_fmt        = SND_SOC_DAIFMT_I2S |
                          SND_SOC_DAIFMT_NB_NF |
                          SND_SOC_DAIFMT_CBS_CFS,

        .init           = my_board_init,
        .ops            = &my_board_ops,
    },
};

/* ── 声卡级 DAPM Widget(板级)──────────────────────────────────── */

static const struct snd_soc_dapm_widget my_board_dapm_widgets[] = {
    SND_SOC_DAPM_HP("Headphone Jack", NULL),
    SND_SOC_DAPM_SPK("Speaker", NULL),
    SND_SOC_DAPM_MIC("Microphone", NULL),
};

/* ── 声卡级 DAPM Route ──────────────────────────────────────────── */

static const struct snd_soc_dapm_route my_board_dapm_routes[] = {
    /* 耳机连接 */
    { "Headphone Jack", NULL, "HP_L" },
    { "Headphone Jack", NULL, "HP_R" },

    /* 扬声器连接 */
    { "Speaker", NULL, "SPK_L" },
    { "Speaker", NULL, "SPK_R" },

    /* 麦克风连接 */
    { "LINPUT1", NULL, "Microphone" },
    { "RINPUT1", NULL, "Microphone" },
    { "Microphone", NULL, "MICB" },
};

/* ── 声卡结构体 ──────────────────────────────────────────────────── */

static struct snd_soc_card my_board_card = {
    .name           = "my-board-audio",
    .owner          = THIS_MODULE,
    .dai_link       = my_board_dai_links,
    .num_links      = ARRAY_SIZE(my_board_dai_links),
    .dapm_widgets   = my_board_dapm_widgets,
    .num_dapm_widgets = ARRAY_SIZE(my_board_dapm_widgets),
    .dapm_routes    = my_board_dapm_routes,
    .num_dapm_routes = ARRAY_SIZE(my_board_dapm_routes),
};

/* ── probe 函数 ──────────────────────────────────────────────────── */

static int my_board_audio_probe(struct platform_device *pdev)
{
    struct my_board_audio *priv;
    struct device_node *np = pdev->dev.of_node;
    int ret;

    priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    /* 从设备树获取 GPIO */
    priv->hp_det_gpio = of_get_named_gpio(np, "hp-det-gpio", 0);
    priv->spk_en_gpio = of_get_named_gpio(np, "spk-en-gpio", 0);

    /* 申请扬声器使能 GPIO */
    if (gpio_is_valid(priv->spk_en_gpio)) {
        devm_gpio_request_one(&pdev->dev, priv->spk_en_gpio,
                               GPIOF_OUT_INIT_LOW, "spk-en");
    }

    /* 设置声卡的父设备 */
    my_board_card.dev = &pdev->dev;

    /* 注册声卡 */
    ret = devm_snd_soc_register_card(&pdev->dev, &my_board_card);
    if (ret) {
        dev_err(&pdev->dev, "注册声卡失败:%d\n", ret);
        return ret;
    }

    dev_info(&pdev->dev, "音频 Machine 驱动加载成功\n");
    return 0;
}

static const struct of_device_id my_board_audio_of_match[] = {
    { .compatible = "myvendor,my-board-audio" },
    {}
};
MODULE_DEVICE_TABLE(of, my_board_audio_of_match);

static struct platform_driver my_board_audio_driver = {
    .probe  = my_board_audio_probe,
    .driver = {
        .name           = "my-board-audio",
        .of_match_table = my_board_audio_of_match,
    },
};
module_platform_driver(my_board_audio_driver);

MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("My Board 音频 Machine 驱动");

17.6.3 Machine 驱动的设备树配置

dts 复制代码
/* 设备树中的音频配置 */
/ {
    /* Machine 驱动节点 */
    sound {
        compatible = "myvendor,my-board-audio";

        /* 耳机检测和扬声器使能 GPIO */
        hp-det-gpio = <&gpio1 5 GPIO_ACTIVE_LOW>;
        spk-en-gpio = <&gpio1 6 GPIO_ACTIVE_HIGH>;
    };

    /* I2S 控制器(Platform 驱动)*/
    i2s1: i2s@02028000 {
        compatible = "myvendor,my-i2s";
        reg = <0x02028000 0x4000>;
        interrupts = <0 30 IRQ_TYPE_LEVEL_HIGH>;
        clocks = <&clks IMX6UL_CLK_SAI1>,
                 <&clks IMX6UL_CLK_SAI1_IPG>;
        clock-names = "i2s_clk", "i2s_ipg";
        dmas = <&sdma 20 24 0>, <&sdma 21 24 0>;
        dma-names = "tx", "rx";
        status = "okay";
    };
};

/* I2C 总线上的 WM8960 Codec */
&i2c1 {
    wm8960: codec@1a {
        compatible = "wlf,wm8960";
        reg = <0x1a>;
        clocks = <&clks IMX6UL_CLK_SAI1>;
        clock-names = "mclk";
        wlf,shared-lrclk;
    };
};

17.6.4 音频驱动的测试

bash 复制代码
# 查看声卡信息
cat /proc/asound/cards
#  0 [myboardaudio   ]: my-board-audio - my-board-audio
#                       my-board-audio

# 查看 PCM 设备
cat /proc/asound/pcm
#  00-00: WM8960 HiFi wm8960-hifi-0 : WM8960 HiFi : playback 1 : capture 1

# 查看混音器控制
amixer -c 0 contents
# numid=1,iface=MIXER,name='Master Playback Volume'
#   ; type=INTEGER,access=rw------,values=2,min=0,max=127,step=0
#   : values=79,79
# numid=2,iface=MIXER,name='Headphone Playback Volume'
#   ; type=INTEGER,access=rw------,values=2,min=0,max=127,step=0
#   : values=111,111

# 设置音量
amixer -c 0 set 'Headphone Playback Volume' 80%

# 播放音频
aplay -D hw:0,0 -f S16_LE -r 44100 -c 2 test.wav

# 录音
arecord -D hw:0,0 -f S16_LE -r 44100 -c 2 -d 5 record.wav

# 查看 DAPM 状态
cat /sys/kernel/debug/asoc/my-board-audio/dapm_pop_time
cat /sys/kernel/debug/asoc/my-board-audio/wm8960.1-001a/dapm

# 使用 speaker-test 测试
speaker-test -c 2 -r 44100 -t sine -f 1000

本章小结

章节 核心知识点 关键 API
17.1 ALSA体系结构 ALSA特性;完整层次架构图(用户空间→alsa-lib→ALSA核心→ASoC→硬件);/dev/snd设备文件命名规则;PCM/混音器/MIDI/Period/Buffer核心概念 aplayarecordamixer
17.2 ALSA驱动组成 snd_card/snd_pcm/snd_pcm_ops核心结构体;注册流程(snd_card_new→snd_pcm_new→snd_card_register);完整虚拟声卡驱动(含定时器模拟DMA/PCM操作/混音器控制) snd_card_new()snd_pcm_new()snd_pcm_period_elapsed()
17.3 ASoC架构 传统ALSA问题;ASoC三组件(Codec/Platform/Machine);完整架构图;DAI/DAI Link/DAPM/Widget/Route核心概念 概念理解
17.4 ASoC Codec驱动 snd_soc_codec_driver/snd_soc_dai_driver/snd_soc_dai_ops;TLV音量控制;DAPM Widget(ADC/DAC/HP/SPK);DAPM Route;完整WM8960驱动(含regmap/混音器/DAPM/I2C注册) snd_soc_register_codec()SOC_DOUBLE_R_TLV()SND_SOC_DAPM_DAC()
17.5 ASoC平台驱动 Platform驱动职责(I2S控制器+DMA);DAI操作函数(set_fmt/hw_params/trigger);DMA PCM配置;devm_snd_dmaengine_pcm_register devm_snd_soc_register_component()devm_snd_dmaengine_pcm_register()
17.6 ASoC Machine驱动 Machine驱动作用(胶水层);DAI Link定义(cpu_dai/codec_dai/dai_fmt);声卡级DAPM Widget和Route;耳机检测;完整Machine驱动(含设备树配置和测试命令) devm_snd_soc_register_card()snd_soc_card_jack_new()

ASoC 驱动开发要点

复制代码
1. 三驱动分工明确
   Codec 驱动:只关心 Codec 芯片,与 SoC 无关
   Platform 驱动:只关心 I2S 控制器和 DMA,与 Codec 无关
   Machine 驱动:板级配置,连接 Codec 和 Platform

2. DAPM 的正确使用
   定义所有 Widget(输入/输出/ADC/DAC/混音器等)
   定义 Route(Widget 之间的连接关系)
   使用 nc_pin 禁用未连接的端点

3. 时钟配置
   Machine 驱动的 hw_params 中配置 MCLK
   确保 Codec 的 MCLK = 采样率 × 256(或其他倍数)

4. DAI 格式
   dai_fmt 必须 CPU 和 Codec 一致
   主从模式:通常 SoC 作为主设备(CBS_CFS)

5. 调试工具
   /proc/asound/cards    ← 声卡信息
   /proc/asound/pcm      ← PCM 设备
   amixer                ← 混音器控制
   /sys/kernel/debug/asoc/ ← DAPM 状态

参考文献:宋宝华《Linux设备驱动开发详解:基于最新的Linux 4.0内核》,机械工业出版社,2015年