《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核心概念 | aplay、arecord、amixer |
| 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年