目的:

  • 从驱动开发的角度大致了解一下 RK3399 Audio 功能。

环境:

  • NanoPC-T4 / Ubuntu-18.04 / Linux-4.4

目录:

1. 测试功能
2. 浏览硬件信息
3. 查看 driver 层
4. 应用层查看声卡信息

1. 测试功能

播放:

# 查看 playback 设备
$ aplay -l
**** List of PLAYBACK Hardware Devices ****
card 0: realtekrt5651co [realtek,rt5651-codec], device 0: ff880000.i2s-rt5651-aif1 rt5651-aif1-0 []
  Subdevices: 1/1
  Subdevice #0: subdevice #0

# 在 card 0, device 0 上播放 wav 文件
$ aplay -D hw:0,0 /root/Music/test.wav

录音:

# 查看 record 设备
$ arecord -l
**** List of CAPTURE Hardware Devices ****
card 0: realtekrt5651co [realtek,rt5651-codec], device 0: ff880000.i2s-rt5651-aif1 rt5651-aif1-0 []
  Subdevices: 1/1
  Subdevice #0: subdevice #0

# 从 card0, device 0 上录音
$ arecord -D hw:0,0 -f dat filename.wav

2. 浏览硬件信息

1) 查看原理图

关键点:

  • Audio Codec的型号:Realtek-ALC5651
  • 控制:I2C1
  • 数据:I2S0
  • 耳机插拔检测:PHONE_DET -> HP_DET_H -> GPIO4_D3_d
  • 录音:MIC_IN2P / MIC_IN2N

2) 查看 RK3399 的 datasheet

关键点:

3) 查看 RK3399 的 TRM

关键点: (Chapter 22 I2S/PCM Controller):

  • 概述 / Overview
    • I2S/PCM controllers 的 features

  • 框图 / Block Diagram

  • 功能描述 / Function description
    • master / slave
    • i2s 的3种 mode
    • pcm 的4种 mode

  • 寄存器描述 / Register Description,最重点的章节,深度定制时需完整阅读。
  • 引脚配置 / Interface description
  • 应用说明 / Application Notes

4) 查看 Audio Codec/Realtek-ALC5651 的 datasheet

关键点:

  • 2.Features
  • 4.Function Block and Mixer Path

    • Audio Mixer Path
    • Digital Mixer Path

  • 6.Pin Descriptions
    • Digital I/O Pins
    • Analog I/O Pins
  • 7.Function Description,最重点的章节,深度定制时需完整阅读。
  • 8.Registers List

3. 查看 driver 层

阅读下面的内容需要有 audio driver 相关的开发经验,不过我也会尽量给出必要的概念说明。

Soc Audio 简化模型

DAI 是什么?

Digital Audio Interface.
Provides audio data to the codec. Formats are usually AC97, I2S, PCM

ASoc 是什么?

ALSA System on Chip.
A Linux kernel subsystem created to provide better ALSA support for system-on-chip and portable audio codecs. It allows to reuse codec drivers across multiple architectures and provides an API to integrate them with the SoC audio interface.

ASoc 包括什么?

  • Platform drivers,提供了配置/使能 SoC audio interface (或称 CPU DAI) 的能力;
  • Codec drivers,提供了配置/使能 Codec 的能力;
  • Machine drivers,描述了应该如何控制 CPU DAI 和 Codec,使他们互相配合在一起工作;

3.1 查看 Machine driver

DT bindings:

arch/arm64/boot/dts/rockchip/rk3399-nanopi4-common.dtsi:

rt5651_card: rt5651-sound {
    status = "okay";
    compatible = "simple-audio-card";
    pinctrl-names = "default";
    pinctrl-0 = <&hp_det>;

    simple-audio-card,name = "realtek,rt5651-codec";
    simple-audio-card,format = "i2s";
    simple-audio-card,mclk-fs = <256>;
    simple-audio-card,hp-det-gpio = <&gpio4 28 GPIO_ACTIVE_HIGH>;

    simple-audio-card,widgets =
        "Microphone", "Mic Jack",
        "Headphone", "Headphone Jack";
    simple-audio-card,routing =
        "Mic Jack", "MICBIAS1",
        "IN1P", "Mic Jack",
        "Headphone Jack", "HPOL",
        "Headphone Jack", "HPOR";

    simple-audio-card,cpu {
        sound-dai = <&i2s0>;
    };
    simple-audio-card,codec {
        sound-dai = <&rt5651>;
    };
};

这里没有选择自己编写 Machine driver,而是采用了 simple-audio-card 这个通用的 Machine driver。

在 simple-audio-card 已经够用的情况下,建议优先使用 simple-audio-card 框架,代码会更加简洁一些。

相关文档和代码:

  • Documentation/devicetree/bindings/sound/simple-card.txt
  • Documentation/devicetree/bindings/sound/widgets.txt
  • Documentation/sound/alsa/soc/machine.txt
  • sound/soc/generic/simple-card.c

simple-card.c 做了什么?

虽然 simple-card.c 不是单板相关的东西,但还是有必要简单说明一下它的内容。

既然 simple-audio-card 是一个 Machine driver,Machine driver 最重要的事情是:构造并注册 struct snd_soc_card,可以认为一个 snd_soc_card 就代表着一个 soc 声卡:

static int asoc_simple_card_probe() {
    struct snd_soc_dai_link *dai_link;

    [...]
    
    /* Init snd_soc_card */
    priv->snd_card.owner = THIS_MODULE;
 priv->snd_card.dev = dev;
 dai_link = priv->dai_link;
 priv->snd_card.dai_link = dai_link;
 priv->snd_card.num_links = num_links;

    [...]

    /* 根据设备树的配置进一步初始化 snd_soc_card,
     * 包括 struct snd_soc_dai_link。
     */
    asoc_simple_card_parse_of(np, priv);

    /* Register snd_soc_card */
    devm_snd_soc_register_card(&pdev->dev, &priv->snd_card);
}

snd_soc_card 里有一个比较重要的成员变量 struct snd_soc_dai_link,snd_soc_dai_link 建立了 CPU DAI 和 Codec DAI 的连接 (link)。simple-card.c 会根据设备树里的配置对 snd_soc_dai_link 进行初始化。

后面就不再展开继续分析了,将关注点放在单板相关的部分。

分析设备树

1) 指定 platform & codec

simple-audio-card,cpu {
    sound-dai = <&i2s0>;
};
simple-audio-card,codec {
    sound-dai = <&rt5651>;
};

指明了:

  • platform driver 是 i2s0;
  • codec driver 是 rt5651;

2) 定义单板相关的 Widget

simple-audio-card,widgets =
    "Microphone", "Mic Jack",
    "Headphone", "Headphone Jack";

什么是 Widget?

  • 在 Asoc 驱动中,用 Widget 来描述一个声卡的功能部件
  • 参考文档:Documentation/sound/alsa/soc/dapm.txt

这里定义了 2 个 Widget:

  • Mic Jack,代表麦克风
  • Headphone Jack,代表 3.5 mm 耳机座

3) 设置单板相关的 Routing

simple-audio-card,routing =
  "Mic Jack", "MICBIAS1",
  "IN1P", "Mic Jack",
  "Headphone Jack", "HPOL",
  "Headphone Jack", "HPOR";

将 CPU DAI 和 Codec DAI 连接起来后,还需要设置 Codec 的 input 和 output 路径,对应的术语就是 Routing。

simple-audio-card,routing 的作用:

A list of the connections between audio components.
Each entry is a pair of strings, the first being the connection's sink, the second being the connection's source.

不过我认为设备树里的这些 Widget 和 Routing 都是没必要的,在 Codec drvier/rt5651.c 已经定义了足够让声卡正常工作的 Widget 和 Routing,有待考证。

3.2 查看 Platform driver

DT bindings:

arch/arm64/boot/dts/rockchip/rk3399.dtsi

i2s0: i2s@ff880000 {
  compatible = "rockchip,rk3399-i2s", "rockchip,rk3066-i2s";
  reg = <0x0 0xff880000 0x0 0x1000>;
  rockchip,grf = <&grf>;
  interrupts = <GIC_SPI 39 IRQ_TYPE_LEVEL_HIGH 0>;
  dmas = <&dmac_bus 0>, <&dmac_bus 1>;
  dma-names = "tx", "rx";
  clock-names = "i2s_clk", "i2s_hclk";
  clocks = <&cru SCLK_I2S0_8CH>, <&cru HCLK_I2S0_8CH>;
  resets = <&cru SRST_I2S0_8CH>, <&cru SRST_H_I2S0_8CH>;
  reset-names = "reset-m", "reset-h";
  pinctrl-names = "default";
  pinctrl-0 = <&i2s0_8ch_bus>;
  power-domains = <&power RK3399_PD_SDIOAUDIO>;
  status = "disabled";
};

相关文档和代码:

  • Documentation/devicetree/bindings/sound/rockchip-i2s.txt
  • sound/soc/rockchip/rockchip_i2s.c

rockchip_i2s.c 做了什么?

Asoc 里的 Platform driver 一般由 CPU 厂商负责编写,但是了解其内部实现有有利于我们宏观把握整个 ASoc 驱动框架。

rockchip_i2s.c 核心工作就是对外提供配置和使能 i2s 接口的能力,它最核心的工作如下。

1) 定义 1个 CPU DAI

static struct snd_soc_dai_driver rockchip_i2s_dai = {
 .probe = rockchip_i2s_dai_probe,
 .playback = {
  .stream_name = "Playback",
  .channels_min = 2,
  .channels_max = 8,
  .rates = SNDRV_PCM_RATE_8000_192000,
  ...
 },
 .capture = {
  .stream_name = "Capture",
  .channels_min = 2,
  .channels_max = 2,
  .rates = SNDRV_PCM_RATE_8000_192000,
  ...
 },
 .ops = &rockchip_i2s_dai_ops,
};

一个 snd_soc_dai_driver 就代表着一个 CPU DAI,该结构体提供了这个 CPU DAI的所有能力。

2) 定义CPU DAI 的操作集

static const struct snd_soc_dai_ops rockchip_i2s_dai_ops = {
 .hw_params = rockchip_i2s_hw_params,
 .set_sysclk = rockchip_i2s_set_sysclk,
 .set_fmt = rockchip_i2s_set_fmt,
 .trigger = rockchip_i2s_trigger,
};

这部分基本就是 i2s 最底层的硬件配置接口了,基本就是围绕着 clocking / format / channel / master-slave 等需求来操作寄存器。这些接口会被 Machine driver 所使用,以和 Codec 端进行配合,一般我们最关心的就是 clock 是否匹配,简化模型如下:

3) 注册 CPU DAI

static int rockchip_i2s_probe(struct platform_device *pdev)
{
    memcpy(soc_dai, &rockchip_i2s_dai, sizeof(*soc_dai));

    ret = devm_snd_soc_register_component(&pdev->dev,
              &rockchip_i2s_component,
              soc_dai, 1);
}

3.3 查看 Codec driver

DT bindings:

&i2c1 {
	status = "okay";
	i2c-scl-rising-time-ns = <150>;
	i2c-scl-falling-time-ns = <30>;
	clock-frequency = <200000>;

	rt5651: rt5651@1a {
		#sound-dai-cells = <0>;
		compatible = "rockchip,rt5651";
		reg = <0x1a>;
		clocks = <&cru SCLK_I2S_8CH_OUT>;
		clock-names = "mclk";
		pinctrl-names = "default";
		pinctrl-0 = <&i2s_8ch_mclk>;
		status = "okay";
	};
};

相关代码和文档:

  • sound/soc/codecs/rt5651.c
  • Documentation/sound/alsa/soc/dapm.txt

rt5651.c 里比较关键的点

Audio Codec 的驱动代码都是由 Codec 厂商提供的,了解其内部实现有利于我们根据自己的需求进行定制。一般 Audio Codec里会有如下的关键信息用于表征整个的 Codec 的内部构造。

1) 定义一堆的 snd_kcontrol_new

/* Digital Mixer */
static const struct snd_kcontrol_new rt5651_snd_controls[] = {
 /* Headphone Output Volume */
 SOC_DOUBLE_TLV("HP Playback Volume", RT5651_HP_VOL,
  RT5651_L_VOL_SFT, RT5651_R_VOL_SFT, 39, 1, out_vol_tlv),
 /* OUTPUT Control */
 SOC_DOUBLE_TLV("OUT Playback Volume", RT5651_LOUT_CTRL1,
  RT5651_L_VOL_SFT, RT5651_R_VOL_SFT, 39, 1, out_vol_tlv),
  ...
}

static const struct snd_kcontrol_new rt5616_sto1_adc_l_mix[] = {
 SOC_DAPM_SINGLE("ADC1 Switch", RT5616_STO1_ADC_MIXER,
   RT5616_M_STO1_ADC_L1_SFT, 1, 1),
};
...

snd_kcontrol_new 是 构造 snd_kcontrol 的原材料。

snd_kcontrol(简称 kcontrol ) 是 Audio Codec 里的一个配置项,一般对应着寄存器里的某个字段。

2) 定义一堆的 Widget

static const struct snd_soc_dapm_widget rt5616_dapm_widgets[] = {
 SND_SOC_DAPM_SUPPLY("PLL1", RT5616_PWR_ANLG2,
       RT5616_PWR_PLL_BIT, 0, NULL, 0),
  ...

 SND_SOC_DAPM_MIXER("Stereo1 ADC MIXL", SND_SOC_NOPM, 0, 0,
      rt5616_sto1_adc_l_mix,
      ARRAY_SIZE(rt5616_sto1_adc_l_mix)),
  ...

Widget 是 Audio Codec 里的功能部件,看下面这个示意图会比较容易理解:

Widget 的类型包括:

 o Mixer      - Mixes several analog signals into a single analog signal.
 o Mux        - An analog switch that outputs only one of many inputs.
 o PGA        - A programmable gain amplifier or attenuation widget.
 o ADC        - Analog to Digital Converter
 o DAC        - Digital to Analog Converter
 o Switch     - An analog switch
 o Input      - A codec input pin
 o Output     - A codec output pin
 o Headphone  - Headphone (and optional Jack)
 o Mic        - Mic (and optional Jack)
 o Line       - Line Input/Output (and optional Jack)
 o Speaker    - Speaker
 o Supply     - Power or clock supply widget used by other widgets.
 o Regulator  - External regulator that supplies power to audio components.
 o Clock      - External clock that supplies clock to audio components.
 o AIF IN     - Audio Interface Input (with TDM slot mask).
 o AIF OUT    - Audio Interface Output (with TDM slot mask).
 o Siggen     - Signal Generator.
 o DAI IN     - Digital Audio Interface Input.
 o DAI OUT    - Digital Audio Interface Output.
 o DAI Link   - DAI Link between two DAI structures */
 o Pre        - Special PRE widget (exec before all others)
 o Post       - Special POST widget (exec after all others)

Widget 可以和某个 kcontrol 绑定在一起,典型的就是 mixer/mux widget。

3) 定义一个描述 Audio Codec 内部 Routing 的结构体: snd_soc_dapm_route

static const struct snd_soc_dapm_route rt5616_dapm_routes[] = {
 {"IN1P", NULL, "LDO"},
 {"IN2P", NULL, "LDO"},
  ...
  {"LOUT L Playback", "Switch", "LOUT MIX"},
 {"LOUT R Playback", "Switch", "LOUT MIX"},

这里的 Route 有点类似网络中的路由表,路由表中的每一项定义了一段路径。将多个路由器里的某个路径都连接在一起后,就形成一个完整的音频播放 / 录制路径。

  • 第1个参数是目的地;
  • 第2个参数是会用到的 kcontrol,可以为 NULL;
  • 第3个成员是来源;

4) 用一个结构体来汇总上面的所有Codec 描述信息:snd_soc_codec_driver

static struct snd_soc_codec_driver soc_codec_dev_rt5651 = {
 .probe = rt5651_probe,
 .suspend = rt5651_suspend,
 .resume = rt5651_resume,
 .set_bias_level = rt5651_set_bias_level,
 .idle_bias_off = true,
 .controls = rt5651_snd_controls,
 .num_controls = ARRAY_SIZE(rt5651_snd_controls),
 .dapm_widgets = rt5651_dapm_widgets,
 .num_dapm_widgets = ARRAY_SIZE(rt5651_dapm_widgets),
 .dapm_routes = rt5651_dapm_routes,
 .num_dapm_routes = ARRAY_SIZE(rt5651_dapm_routes),
};

snd_soc_codec_driver 就代表了一个 Codec driver。

5) 注册 codec driver: snd_soc_register_codec()

static int rt5651_i2c_probe() {
  ...
  ret = snd_soc_register_codec(&i2c->dev, &soc_codec_dev_rt5651,
    rt5651_dai, ARRAY_SIZE(rt5651_dai));
}

将 codec driver 注册进系统后,系统就有能力动态地判断是否应该使能 Audio Codec 内部的就某个 Path,只有当 Path 上的各个 Route 是连接的并且有应用程序在使用声卡,才需要真正地给 Audio Codec 上电。

rt5651_dai 是 Codec 端的 DAI,它向 Machine driver 提供配置 Codec 的能力:

static const struct snd_soc_dai_ops rt5651_aif_dai_ops = {
 .hw_params = rt5651_hw_params,
 .set_fmt = rt5651_set_dai_fmt,
 .set_sysclk = rt5651_set_dai_sysclk,
 .set_pll = rt5651_set_dai_pll,
};

static struct snd_soc_dai_driver rt5651_dai[] = {
 {
  .name = "rt5651-aif1",
  .id = RT5651_AIF1,
  .playback = {
   .stream_name = "AIF1 Playback",
   ...
  },
  .capture = {
   .stream_name = "AIF1 Capture",
   ...
  },
  .ops = &rt5651_aif_dai_ops,
 },
  ...

到此 Machine driver 就有了协调控制 Platform 端和 Codec 端的能力了。

4. 应用层查看声卡信息

查看所有的 DAI:

$ cat /sys/kernel/debug/asoc/dais  
i2s-hifi
i2s-hifi
ff870000.spdif
ff8a0000.i2s
ff880000.i2s      // cpu dai
dit-hifi
rt5651-aif2
rt5651-aif1       // codec dai
snd-soc-dummy-dai

查看 Audio Codec 的寄存器:

$ cat /sys/kernel/debug/regmap/1-001a/registers 
000: 0000
002: 8888
003: c8c8
005: 0000
00d: 0200
...

查看 Widget 的状态:

$ cat /sys/devices/platform/rt5651-sound/ff880000.i2s-rt5651-aif1/dapm_widget
I2S1 ASRC: Off
I2S2 ASRC: Off
STO1 DAC ASRC: Off
STO2 DAC ASRC: Off
ADC ASRC: Off
...

查看和配置 Kcontrol:

$ tinymix --help
usage: tinymix [options] <command>
options:
 -h, --help        : prints this help message and exits
 -v, --version     : prints this version of tinymix and exits
 -D, --card NUMBER : specifies the card number of the mixer
commands:
 get NAME|ID       : prints the values of a control
 set NAME|ID VALUE : sets the value of a control
 controls          : lists controls of the mixer
 contents          : lists controls of the mixer and their contents

5. 参考

思考技术,也思考人生

要学习技术,更要学习如何生活

你和我各有一个苹果,如果我们交换苹果的话,我们还是只有一个苹果。但当你和我各有一个想法,我们交换想法的话,我们就都有两个想法了。

对 嵌入式系统 (Linux、RTOS、OpenWrt、Android) 和 开源软件 感兴趣,关注公众号:嵌入式Hacker

觉得文章对你有价值,不妨点个 在看和赞

Logo

更多推荐