乐趣区

关于linux-kernel:高通8155-音频数据从HAL到DSP

从数据流的角度整顿下安卓平台音频数据从 HAL 层达到 DSP 这个流程。
以 MultiMedia22 –> QUIN_TDM_RX_0 播放为例。
次要关注 pcm 数据写到 dsp, 以及将前后端路由信息告知 dsp 两个点。

<!– more –>

[Platform: 高通 8155 gvmq Android 11]
[Kernel:msm-5.4]
代码可参考 codeaurora 按如下形式下载

repo init -u https://source.codeaurora.org/quic/la/platform/manifest.git -b release -m LA.AU.1.3.2.r2-02600-sa8155_gvmq.0.xml --depth 1
repo sync -c kernel/msm-5.4 platform/vendor/opensource/audio-kernel platform/hardware/qcom/audio platform/external/tinyalsa

浏览本文最好对 ALSA 或者 ASOC(ALSA System on Chip)有所理解,相干的文档可看下

<<Kernel Didr>>/Documentation/sound/soc/overview.rst

及该目录下的文档, 或者网上搜一下有一大堆材料。
简略的说,Linux ASOC 架构为了 XXX 目标,提出了一套这也牛逼那也高级还很省电 (DAPM) 的音频架构,当然也有全新的玩法和很多术语,从驱动角度来说,有三个局部比拟重要:

  • Codec 局部驱动
    codec 编解码芯片相干,比方其 Mixer, 管制,DAI 接口,A/D,D/ A 等,这部分要求仅为 codec 通用局部,不蕴含任何平台或者机器相干代码,以不便运行在任何架构和机器 (machine) 上。
  • Platform 局部驱动
    包含音频 DMA,数字音频接口(DAI)驱动程序(例如 I2S,AC97,PCM)和 DSP 驱动(高通有的文档把这 DSP 驱动独自拎进去,等同于 CPU 驱动),这部分也仅针对 soc cpu,不得蕴含特定板级相干代码,也就是说也是机器 (machine) 无关的。
  • Machine 局部驱动
    codec 和 platform 都是与机器无关的,它们是绝对独立的两个局部,那谁把他们黏合在一起呢?这个工作就落在了 machine 上,它形容和绑定 (dai link) 这些组件并实例化为声卡,蕴含有 codec 和 platform 特定相干代码。它能够解决任何机器特定的管制(GPIO, 中断, clocking, jacks, 电压等) 和机器级音频事件(例如,在播放开始时关上 speaker/hp 放大器)。

从数据流的角度来说,有两个概念比拟重要:

  • FE-DAI
    Front-End DAI, 前端,对用户空间可见,为 pcm 设施,可通过 mixer 操作路由到后端,与后端连贯上,可路由到多个后端 DAIs。
  • BE-DAI
    Back-End DAI, 后端,对用户空间不可见,可路由到多个前端 DAIs。

前端和后端的可路由形式会有个路由表,规定了哪些可式可连上。

提到 BE 和 FE DAI,不得不说的一个概念是 Dynamic PCM, 可看下文档 <<Kernel Didr>>/Documentation/sound/soc/dpcm.rst,下图也出自该文档,

  | Front End PCMs    |  SoC DSP  | Back End DAIs | Audio devices |
  
                      *************
  PCM0 <------------> *           * <----DAI0-----> Codec Headset
                      *           *
  PCM1 <------------> *           * <----DAI1-----> Codec Speakers
                      *   DSP     *
  PCM2 <------------> *           * <----DAI2-----> MODEM
                      *           *
  PCM3 <------------> *           * <----DAI3-----> BT
                      *           *
                      *           * <----DAI4-----> DMIC
                      *           *
                      *           * <----DAI5-----> FM
                      *************

如上图为智能手机音频示意图,其反对 Headset,Speakers,MODEM,蓝牙,数字麦克风,FM。其定义了 4 个前端 pcm 设施,6 个后端 DAIs。
每个前端 pcm 可把数据路由到 1 个或多个后端,例如 PCM0 数据可给 DAI3 BT,也可同时给到 DAI1 speaker 和 DAI3 BT。
多个前端也可同时路由到一个后端,例如 PCM0 PCM1 都把数据给 DAI0。
须要留神的是,后端 DAI 与外设通常为一一对应关系,即一个后端 DAI 代表了一个外设; 前端 pcm 和 HAL 层 use case 通常是一一对应的。

高通平台 adsp 驱动为了实现这些,软件上又分为 ASM ADM AFE

  • ASM
    流治理,能够简略了解为 FE 操作的一部分(FE 数据最终通过 q6asm 发送 apr 包形式和 dsp 交互),还包含对音频流的解决,如音效等。
  • ADM
    设施治理,也包含路由治理,即哪些流写到哪些设施,有设施层级的音频解决(如多个流混音后进行独特的音效解决)。
  • AFE
    可简略了解为 BE 的末端操作局部,名字获得让人纳闷。DSP 设施的操作,如 clock, pll 等。

HAL 层操作

8155 qcom audio HAL 挪到了 vendor/qcom/opensource/audio-hal/primary-hal, 不再位于 hardware 目录下了。
HAL 层有很多的逻辑解决,路由的使能敞开,还思考各种 use case, acdb 信息等,代码一大堆,对于咱们剖析数据流向来说让人头晕,好在咱们能够通过 tinymixer 和 tinyplay 命令行进行播放,通过看 tinyplay 播放流程可大大简化咱们剖析代码难度,不过在播放之前,咱们得用 tinymixer 进行通道的管制,让整个链路买通,能力写入数据。

这里我选取个不罕用的 USECASE_AUDIO_PLAYBACK_REAR_SEAT (rear-seat-playback) 来进行命令行操作。

通过查找代码其参考设计对应的前后端路由 path 如下,应用的 BE 是 QUIN_TDM_RX_0

vendor/qcom/opensource/audio-hal/primary-hal/configs/msmnile_au/mixer_paths_adp.xml
<path name="rear-seat-playback">
    <ctl name="QUIN_TDM_RX_0 Channels" value="Sixteen" />
    <ctl name="QUIN_TDM_RX_0 Audio Mixer MultiMedia22" value="1" />
</path>

该 use case 对应的 pcm 设施号为 54 (严格意义上说是 MultiMedia22 对应的 pcm 54)

msm8974/platform.h
#define REAR_SEAT_PCM_DEVICE 54

msm8974/platform.c
static int pcm_device_table[AUDIO_USECASE_MAX][2] = {
...// use case rear seat 对应的的 pcm 设施 54
    [USECASE_AUDIO_PLAYBACK_REAR_SEAT] = {REAR_SEAT_PCM_DEVICE,
                                          REAR_SEAT_PCM_DEVICE},

所以最终咱们可用命令行做如下操作进行播放

tinymix "QUIN_TDM_RX_0 Channels" "Six" # 设置 Channel 数
tinymix "QUIN_TDM_RX_0 Audio Mixer MultiMedia22" "1" # dpcm, 将前后端连起来
tinyplay /data/a.wav -D 0 -d 54 # 应用声卡 0, 第 54 个 pcm 设施播放

第一条命令设置下 channel 数,不是本文重点,疏忽;
第二条命令设置 dpcm 路由,将前后端连上;
第三条命令就是通过声卡 0 第 54 个设施播放了,其实就是通过 /dev/snd/pcmC0D54p 节点往内核写入数据。

tinyplay 播放流程挺简略的,整顿如下:

external/tinyalsa/tinyplay.c
main()
  + 参数解析
  + play_sample()
      + pcm_open()
      |   + snprintf(fn, sizeof(fn), "/dev/snd/pcmC%uD%u%c", card, device,
      |   |          flags & PCM_IN ? 'c' : 'p');
      |   + pcm->fd = open(fn, O_RDWR|O_NONBLOCK); // 关上 /dev/snd... 设施
      |   | 
      |   + ioctl(pcm->fd, SNDRV_PCM_IOCTL_HW_PARAMS, &params)
      |   + ioctl(pcm->fd, SNDRV_PCM_IOCTL_SW_PARAMS, &sparams)
      |
      + do {pcm_write() } while()
      |   + // pcm_write()
      |   + if (!pcm->running) {|   |   pcm_prepare(pcm); // ioctl(pcm->fd, SNDRV_PCM_IOCTL_PREPARE)
      |   |   ioctl(pcm->fd, SNDRV_PCM_IOCTL_WRITEI_FRAMES, &x)
      |   |   return 0;
      |   | }
      |   |
      |   + // 通过 ioctl 写数据
      +   + ioctl(pcm->fd, SNDRV_PCM_IOCTL_WRITEI_FRAMES, &x)

对本文来说次要也就关注三点:

  • pcm_open() 关上设施并设置软硬件参数;
  • pcm_prepare() 筹备;
  • pcm_write() 筹备好后写数据;

当然,这三个函数次要通过 ioctl()与内核交互。

直觉上,咱们跟着剖析 pcm_write()就能够晓得这个数据流向了,不过在剖析该函数之前,咱们得明确这个写到底是往内核的哪个设施写了?

pcm 设施信息查看

pcm 设施信息可用如下命令查看

# 查看 pcm 设施信息
$ cat /proc/asound/pcm
00-00: MultiMedia1 (*) :  : playback 1 : capture 1
00-01: MultiMedia2 (*) :  : playback 1 : capture 1
...
00-54: MultiMedia22 (*) :  : playback 1 : capture 1
...

如下面咱们例子的 pcm 54, id 名为 MultiMedia22, 反对 1 个播放 1 个录音。

PCM 设施的详细信息还能够通过如下命令查看

# 查看 pcm54 capture 信息
$ cat /proc/asound/card0/pcm54c/info
card: 0
device: 54
subdevice: 0
stream: CAPTURE
id: MultiMedia22 (*)
name: 
subname: subdevice #0
class: 0
subclass: 0
subdevices_count: 1
subdevices_avail: 1

在持续剖析往哪个设施写数据流程前,咱们得问问
54 在内核里对应的是哪个呢?MultiMedia22 为啥是 54, 而不是 55 或者 53 呢?

BE

pcm 设施创立

设施号 54 这个其实是 dai link(留神是 dai link 里而不是 dai 定义)里,刚好是排在第 54, 是声卡注册时依据 dai link 信息第 54 个注册上的设施,
所以如果有自已增加的 pcm 最好是增加到最初面,不然光改这些设施号对应关系都一堆。

kernel/msm-5.4/techpack/audio/asoc/sa8155.c
static struct snd_soc_dai_link msm_common_dai_links[] = {
    /* FrontEnd DAI Links */
    ...
    {.name = MSM_DAILINK_NAME(Media22), // dai_link 名,开展就是 "SA8155 Media22"
        .stream_name = "MultiMedia22",
        .dynamic = 1, // 可动静路由
#if IS_ENABLED(CONFIG_AUDIO_QGKI)
        .async_ops = ASYNC_DPCM_SND_SOC_PREPARE,
#endif /* CONFIG_AUDIO_QGKI */
        .dpcm_playback = 1, // 播放反对 dpcm
        .dpcm_capture = 1,
        .trigger = {SND_SOC_DPCM_TRIGGER_POST,
            SND_SOC_DPCM_TRIGGER_POST},
        .ignore_suspend = 1,
        .ignore_pmdown_time = 1,
        .id = MSM_FRONTEND_DAI_MULTIMEDIA22,
        SND_SOC_DAILINK_REG(multimedia22), // 宏,定义 cpu codec platform
    },

msm_dailink.h
SND_SOC_DAILINK_DEFS(multimedia22,
    // cpu 组件名,soc_bind_dai_link()绑定时告 of_node, dai_name 等匹配看下 snd_soc_find_dai()的 of_node, dai 相匹配,以及 msm_populate_dai_link_component_of_node()对 of_node 解决
    // 驱动在 msm-dai-fe.c
    DAILINK_COMP_ARRAY(COMP_CPU("MultiMedia22")),
    // codec 组件,因为动静 pcm,所以是 dummy 的
    DAILINK_COMP_ARRAY(COMP_CODEC("snd-soc-dummy", "snd-soc-dummy-dai")),
    // multimedia22 对应的平台组件,驱动在 msm-pcm-q6-v2.c
    DAILINK_COMP_ARRAY(COMP_PLATFORM("msm-pcm-dsp.0")));

咱们能够顺便看一眼其对应的 dai 定义, 也就定义了 playback/capture 的信息,如名字,反对的采样率,格局,反对的 channel
以及该 dai 的一些操作 ops 和 probe 函数

kernel/msm-5.4/techpack/audio/asoc/msm-dai-fe.c
static struct snd_soc_dai_driver msm_fe_dais[] = {
  ...
  {
    .playback = {
      .stream_name = "MultiMedia22 Playback",
      .aif_name = "MM_DL22",
      .rates = (SNDRV_PCM_RATE_8000_384000 |
          SNDRV_PCM_RATE_KNOT),
      .formats = (SNDRV_PCM_FMTBIT_S16_LE |
            SNDRV_PCM_FMTBIT_S24_LE |
            SNDRV_PCM_FMTBIT_S24_3LE |
            SNDRV_PCM_FMTBIT_S32_LE),
      .channels_min = 1,
      .channels_max = 32,
      .rate_min = 8000,
      .rate_max = 384000,
    },
    .capture = {
      .stream_name = "MultiMedia22 Capture",
      .aif_name = "MM_UL22",
      .rates = (SNDRV_PCM_RATE_8000_48000|
          SNDRV_PCM_RATE_KNOT),
      .formats = (SNDRV_PCM_FMTBIT_S16_LE |
            SNDRV_PCM_FMTBIT_S24_LE |
            SNDRV_PCM_FMTBIT_S24_3LE |
            SNDRV_PCM_FMTBIT_S32_LE),
      .channels_min = 1,
      .channels_max = 32,
      .rate_min =     8000,
      .rate_max =     48000,
    },
    .ops = &msm_fe_Multimedia_dai_ops, // 目前就只有 startup 办法
    .name = "MultiMedia22", // 与 dai link 里 cpu 组件名字雷同,匹配上
    .probe = fe_dai_probe,
  },

这些 dai links 会在 machine 驱动 probe 的时候,将 dai links 信息给声卡 card->dai_link,声卡注册的时候,会依据这些信息创立相应的 pcm 设施,

// machine 驱动 probe
msm_asoc_machine_probe() / sa8155.c
  + populate_snd_card_dailinks(&pdev->dev) // dai links 信息
  + msm_populate_dai_link_component_of_node() // 依据 dai link 赋值 of_node,如果找到那么 cpus->dai_name = NULL; platforms->name = NULL;
  + devm_snd_soc_register_card() // 注册声卡


static struct snd_soc_card *populate_snd_card_dailinks(struct device *dev)
{...
    if (!strcmp(match->data, "adp_star_codec")) {
        card = &snd_soc_card_auto_msm;
        ...
        memcpy(msm_auto_dai_links,
               msm_common_dai_links, // MultiMedia22 在这些 dai links 排第 54
               sizeof(msm_common_dai_links));
    ...
        dailink = msm_auto_dai_links;
    }
    ...     // dai link 给声卡的 dai_link
        card->dai_link = dailink;
        card->num_links = total_links;
    ...
}

声卡注册流程很长,尽管最近几个版本没大改变,但当前也可能会改,咱们次要关注下PCM 设施创立流程

这里简略列举下声卡注册流程,有趣味能够看看,具体的能够网上找些文章看看。

声卡注册流程
devm_snd_soc_register_card()
+ snd_soc_register_card()
  + snd_soc_bind_card()
    + snd_soc_instantiate_card()
      + for_each_card_links(card, dai_link) {|   soc_bind_dai_link() // 绑定 dai link
      |     + snd_soc_find_dai(dai_link->cpus); // cpus dai 匹配,先匹配 of_node
      |     |   + strcmp(..., dlc->dai_name) // 而后如果 dai_name 不为空,比拟组件驱动名字和 dai_link 中 cpu_dai_name
      |     + for_each_link_codecs(dai_link, i, codec) // codec dai 匹配
      |     + for_each_link_platforms(dai_link, i, platform) // platform dai 匹配
      |     |
      |     + soc_add_pcm_runtime() // 将 rtd->list 退出到 card->rtd_list 里,|        + rtd->num = card->num_rtd; // 设施号,该 num 即为咱们例子里的 54
      |        + card->num_rtd++; // 声卡的运行时例 +1
      + }
      + snd_card_register()
      | + snd_device_register_all()
      |   + list_for_each_entry(dev, &card->devices, list) {|   |   __snd_device_register()
      |   |     + dev->ops->dev_register(dev); // 遍历注册设施
      +   + }

下面的代码中咱们能够先关注下 rtd->num,即是咱们例子里的 pcm 设施号 54。

最终的设施注册是调用 dev->ops->dev_register(dev) 注册的,那么这个是哪个办法呢?
不同的设施有不同的注册办法,这些也简略列了下可能有用的,不便当前须要查看。

设施驱动文件 dev_register 办法
rawmidi.c snd_rawmidi_dev_register()
seq_device.c snd_seq_device_dev_register()
jack.c snd_jack_dev_register()
hwdep.c snd_hwdep_dev_register()
pcm.c snd_pcm_dev_register()
compress_offload.c snd_compress_dev_register()
timer.c snd_timer_dev_register()
control.c snd_ctl_dev_register()
ac97_codec.c snd_ac97_dev_register()

对于 pcm 设施来说,其定义和调用流程如下,可略过,间接到下一步 snd_pcm_dev_register()

# 流程
kernel/msm-5.4/sound/core/pcm.c
snd_soc_instantiate_card()
  for_each_card_rtds(card, rtd)
    soc_link_init(card, rtd);
      + soc_new_pcm()
          + snd_pcm_new()
              + _snd_pcm_new() // pcm 的两个流创立,并将 pcm 设施加到 card->devices list 里


# dev_register 定义
static int _snd_pcm_new(struct snd_card *card, const char *id, int device,
   int playback_count, int capture_count, bool internal,
   struct snd_pcm **rpcm)
{...
    static struct snd_device_ops ops = {
        .dev_free = snd_pcm_dev_free,
        .dev_register =    snd_pcm_dev_register,
        .dev_disconnect = snd_pcm_dev_disconnect,
    };
    ...
    // 播放 / 录音流及其子流的信息创立,目前 playback_count capture_count 都为 1,具体的可看下 soc_new_pcm()规定
    // 流信息赋值给 snd_pcm pcm->streams[stream];
    err = snd_pcm_new_stream(pcm, SNDRV_PCM_STREAM_PLAYBACK,
                 playback_count);
    ...
    err = snd_pcm_new_stream(pcm, SNDRV_PCM_STREAM_CAPTURE, capture_count);
};

int snd_pcm_new_stream(struct snd_pcm *pcm, int stream, int substream_count)
{...
    // 流名字,如咱们的例子,播放 pcmC0D54p,pcm->device 为设施号,如咱们例子的 54
    dev_set_name(&pstr->dev, "pcmC%iD%i%c", pcm->card->number, pcm->device,
             stream == SNDRV_PCM_STREAM_PLAYBACK ? 'p' : 'c');
    ... 子流信息,省略
    for (idx = 0, prev = NULL; idx < substream_count; idx++) {

_snd_pcm_new() 只是创立了播放 / 录音流及其子流信息 (如咱们的例子名字 pcmC0D54p),而后将 pcm 设施加到声卡 devices 列表里,并没有创立设施节点,
真正创立设施节点是 snd_pcm_dev_register(),

static int snd_pcm_dev_register(struct snd_device *device)
{...
    // cid 示意 SNDRV_PCM_STREAM_PLAYBACK SNDRV_PCM_STREAM_CAPTURE
    for (cidx = 0; cidx < 2; cidx++) {
        ...// 注册 pcm 设施
        /* register pcm */
        err = snd_register_device(devtype, pcm->card, pcm->device,
                      &snd_pcm_f_ops[cidx], pcm,
                      &pcm->streams[cidx].dev);
sound/core/sound.c
int snd_register_device(int type, struct snd_card *card, int dev,
            const struct file_operations *f_ops,
            void *private_data, struct device *device)
{...
    // 查找闲暇的 minor
    minor = snd_find_free_minor(type, card, dev);
    ...// 注册设施节点
    err = device_add(device);
    ...
}

snd_register_device()里通过调用 device_add()创立了设施节点,也即

/dev/snd/pcmC0D54p

之后,咱们就能够通过
pcm_write() –> ioctl(pcm->fd, SNDRV_PCM_IOCTL_WRITEI_FRAMES, &x)
往前端 PCM 设施写入数据了

PCM open

咱们晓得了往哪个设施写数据,直觉上应该持续剖析 pcm_write()看写流程,
不过个别 open 的时候会初始化一些重要的数据结构,所以这节把须要留神的点写写,也可跳过间接看写流程。

open 的流程按如下程序,可简略看下:

声卡
--> 播放流
  --> pcm 子流
    --> dpcm 前端 dai
      --> 后端所有组件关上
        --> 前端所有组件关上 (依照 fe dai, codec 组件,cpu 组件程序)

对应的代码

chrdev_open()
+ snd_open()
  + file->f_op->open() // snd_pcm_f_ops 见正文 1 
    + snd_pcm_playback_open()
      + snd_pcm_open()
        + snd_pcm_open_file()
          + snd_pcm_open_substream()
            + substream->ops->open // dpcm_fe_dai_open 见正文 2
              + dpcm_fe_dai_startup()
                + dpcm_be_dai_startup() // BE 组件关上
                | + soc_pcm_open() // 同下 fe 关上,省略
                |
                + soc_pcm_open() // 这里是 FE 组件关上
                  + soc_pcm_components_open()
                    + for_each_rtdcom(rtd, rtdcom) 
                        snd_soc_component_open(component, substream);
                        + component->driver->ops->open(substream) // fe dai, codec 组件,cpu 组件都关上

# 正文 1
// pcm 的播放录音 file_operations
const struct file_operations snd_pcm_f_ops[2] = {
  {
    .owner =    THIS_MODULE,
    .write =    snd_pcm_write,
    .write_iter =   snd_pcm_writev,
    .open =     snd_pcm_playback_open,
    .release =    snd_pcm_release,
    .llseek =   no_llseek,
    .poll =     snd_pcm_poll,
    .unlocked_ioctl = snd_pcm_ioctl,
    .compat_ioctl =   snd_pcm_ioctl_compat,
    .mmap =     snd_pcm_mmap,
    .fasync =   snd_pcm_fasync,
    .get_unmapped_area =  snd_pcm_get_unmapped_area,
  },
  {
    .owner =    THIS_MODULE,
    .read =     snd_pcm_read,
    .read_iter =    snd_pcm_readv,
    .open =     snd_pcm_capture_open,
    .release =    snd_pcm_release,
    .llseek =   no_llseek,
    .poll =     snd_pcm_poll,
    .unlocked_ioctl = snd_pcm_ioctl,
    .compat_ioctl =   snd_pcm_ioctl_compat,
    .mmap =     snd_pcm_mmap,
    .fasync =   snd_pcm_fasync,
    .get_unmapped_area =  snd_pcm_get_unmapped_area,
  }
};


# 正文 2 substream->ops

substream->ops 是在声卡注册时 soc_new_pcm() --> snd_pcm_set_ops(), 
依据运行时流 rtd 是否采纳动静 pcm 赋值的
soc_new_pcm()
  + snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &rtd->ops);
      + struct snd_pcm_str *stream = &pcm->streams[direction];
      | for (substream = stream->substream; substream != NULL; substream = substream->next)
      +   substream->ops = ops; // 也即 rtd->ops

其定义如下
/* create a new pcm */
int soc_new_pcm(struct snd_soc_pcm_runtime *rtd, int num)
{
  ...// 如果采纳 dynamic pcm,其办法
  /* ASoC PCM operations */
  if (rtd->dai_link->dynamic) {
    rtd->ops.open   = dpcm_fe_dai_open;
    rtd->ops.hw_params  = dpcm_fe_dai_hw_params;
    rtd->ops.prepare  = dpcm_fe_dai_prepare;
    rtd->ops.trigger  = dpcm_fe_dai_trigger;
    rtd->ops.hw_free  = dpcm_fe_dai_hw_free;
    rtd->ops.close    = dpcm_fe_dai_close;
    rtd->ops.pointer  = soc_pcm_pointer;
    rtd->ops.ioctl    = snd_soc_pcm_component_ioctl;
    ...
  } else {// 没有采纳 dpcm
    rtd->ops.open   = soc_pcm_open;
    rtd->ops.hw_params  = soc_pcm_hw_params;
    rtd->ops.prepare  = soc_pcm_prepare;
    rtd->ops.trigger  = soc_pcm_trigger;
    rtd->ops.hw_free  = soc_pcm_hw_free;
    rtd->ops.close    = soc_pcm_close;
    ...
  }


同时留神下 copy_user 赋值,前面会用到
for_each_rtdcom(rtd, rtdcom) {
  const struct snd_pcm_ops *ops = rtdcom->component->driver->ops;
  ....
  if (ops->copy_user)
    rtd->ops.copy_user  = snd_soc_pcm_component_copy_user;
  if (ops->page)
    rtd->ops.page   = snd_soc_pcm_component_page;
  if (ops->mmap)
    rtd->ops.mmap   = snd_soc_pcm_component_mmap;
}
留神点:
关注下 copy_user  = snd_soc_pcm_component_copy_user, 前面写数据时会用到,前面不再讲

对于 BE 的关上,前面的章节再看,
前端所有组件关上中,dai 定义里没有 open 操作,codec 组件是 dummy 的,所以咱们只看下 cpu 组件的 open。
对于咱们的例子,其驱动在 msm-pcm-q6-v2.c(对于 voip/voice/compress 或别的有本人的驱动,在此不扩大了)

其 open 函数 (msm_pcm_open()), 次要是通过 q6asm_audio_client_alloc() 进行 audio_client 的申请,
与 dsp 交互的信息根本都寄存在这外面,q6asm_audio_client_alloc() 次要进行了 session 申请和 session 注册。

msm_pcm_open() / msm-pcm-q6-v2.c
+ prtd->audio_client = q6asm_audio_client_alloc(|                       (app_cb)event_handler, prtd);
| + q6asm_audio_client_alloc() / kernel/msm-5.4/techpack/audio/dsp/q6asm.c
|   + n = q6asm_session_alloc(ac);
|   | + for (n = 1; n <= ASM_ACTIVE_STREAMS_ALLOWED; n++) {|   |       if (!(session[n].ac)) { // 查找闲暇的 session
|   |           session[n].ac = ac;q6
|   + ac->cb = cb; // 传入的 callback,事件回调解决
|   + rc = q6asm_session_register(ac);
+     +  apr_register("ADSP", "ASM",...)

对于 session 申请,其次要是从 session[ASM_ACTIVE_STREAMS_ALLOWED+1] 里找个闲暇的来用,容许 audio client 的 session 数是 [1, 15], 0 用于保留;另外咱们还看到有个 common_client, 其 id 为 ASM_CONTROL_SESSION,用于所有 session 内存映射校准。
一个 session 在 dsp 对应着的是 port 的概念, session 和 port 都有固定换算公式的,session 确定了,port 也确定了

kernel/msm-5.4/techpack/audio/include/dsp/q6asm-v2.h
/* Control session is used for mapping calibration memory */
  #define ASM_CONTROL_SESSION    (ASM_ACTIVE_STREAMS_ALLOWED + 1)

对于 session 注册,次要是调用 apr_register()进行信息注册,而后给 audio client 的 apr, 当有 apr 信息要解决的时候,通过 q6asm_callback 回调,进一步调用 audio client 回调解决

static int q6asm_session_register(struct audio_client *ac)
{
    ac->apr = apr_register("ADSP", "ASM",
            (apr_fn)q6asm_callback,
            ((ac->session) << 8 | 0x0001),
            ac);
    ...
    ac->apr2 = apr_register("ADSP", "ASM",
            (apr_fn)q6asm_callback,
            ((ac->session) << 8 | 0x0002),
            ac);
    ...
    // 运行时 session apr handle, rtac_asm_apr_data[session_id].apr_handle = handle;
    rtac_set_asm_handle(ac->session, ac->apr);

    pr_debug("%s: Registering the common port with APR\n", __func__);
    ac->mmap_apr = q6asm_mmap_apr_reg(); // 也是调用 apr_register

apr_register 在 apr_vm.c apr.c 里都有实现,apr_vm.c 用于 8155 hypervisor 计划, 也即一个芯片同时跑安卓 + 仪表 QNX 计划,能够看下单安卓的 apr.c。
apr_register 次要是在填充 apr_svc 信息,如 dest_id,client_id 等,除了确认 chanel 有没有关上,仿佛也没和 dsp 进行额定的信息替换,那 session 数据要往 dsp 哪个 port 写,是如何通知 dsp 的呢?咱们持续看看写流程吧。

apr (Asynchronous Packet Router), 用于和高通 dsp 进行交互,有本人的一套协定,简略的说无非就是包头加负载信息。

write 写数据到 dsp

用户空间写数据通过 pcm_write() –> ioctl(pcm->fd, SNDRV_PCM_IOCTL_WRITEI_FRAMES, &x)

其内核 alsa 层流程代码简略列举如下

kernel/msm-5.4/sound/core/pcm_native.c
snd_pcm_common_ioctl()
+ case SNDRV_PCM_IOCTL_WRITEI_FRAMES:
  case SNDRV_PCM_IOCTL_READI_FRAMES:
    + return snd_pcm_xferi_frames_ioctl(substream, arg);
      + copy_from_user(&xferi, _xferi, sizeof(xferi)) // frames 和 buf 地址信息
      + snd_pcm_lib_write(substream, xferi.buf, xferi.frames)
        + __snd_pcm_lib_xfer(substream, (void __force *)buf, true, frames, false);
          + writer(...transfer); // transfer 为 substream->ops->copy_user
            + interleaved_copy()
              + transfer(substream, 0, hwoff, data + off, frames);
                + substream->ops->copy_user()

对于咱们的例子, substream->ops->copy_user 定义在如下文件中

kernel/msm-5.4/techpack/audio/asoc/msm-pcm-q6-v2.c
static const struct snd_pcm_ops msm_pcm_ops = {
  .open           = msm_pcm_open,
  .copy_user  = msm_pcm_copy,

msm_pcm_copy() 依据读 / 写调用不同的函数,写就是 msm_pcm_playback_copy(), 其次要的流程为:

  • 查看是否有可用的 cpu_buf;
  • 将用户空间数据拷贝到 buffer 里,也即 audio client 的 port[dir]->buf[idx].data 里;
  • 通过 apr 发包给 dsp 通知其 session 对应的 port 信息和数据地址信息。
msm_pcm_copy()
+ msm_pcm_playback_copy()
  + while ((fbytes > 0) && (retries < MAX_PB_COPY_RETRIES)) {
  |   + data = q6asm_is_cpu_buf_avail(IN, prtd->audio_client, &size, // 是否有可用 cpu_buf
  |   + copy_from_user(bufptr, buf, xfer) // 从用户空间拷贝到 bufptr,bufptr = data;
  |   |
  |   + q6asm_write(prtd->audio_client, xfer,
  |   |   |               0, 0, NO_TIMESTAMP);
  |   |   + q6asm_add_hdr()
  |   |   | + __q6asm_add_hdr()
  |   |   |   + hdr->src_port = ((ac->session << 8) & 0xFF00) | (stream_id);
  |   |   |   + hdr->dest_port = ((ac->session << 8) & 0xFF00) | (stream_id);
  |   |   |
  |   |   + write.hdr.opcode = ASM_DATA_CMD_WRITE_V2;
  |   |   + write.buf_addr_lsw = lower_32_bits(ab->phys); // audio client 的以后 port 的地址,即无效数据的地址
  |   +   + apr_send_pkt(ac->apr, (uint32_t *) &write);
  + }
提醒:
1. 对于 cpu buf 申请可看 q6asm_audio_client_buf_alloc_contiguous(), 通过 msm_audio_ion_alloc()申请,这里还波及到和 dsp 地址映射 q6asm_memory_map_regions()
2.port[dir] dir 为 IN/OUT,是针对 dsp 来看的,播放就是 IN, 录音就是 OUT。

至此,pcm 数据写到 dsp 流程就完了, 也即前端流程实现,
数据发到 dsp 进行解决,这是个黑盒,有源码可剖析下流程,咱们能做的就是通过其接口,指定数据从哪个 BE 输入,接下来咱们看下后端相干的内容。

BE

在下面的剖析,咱们晓得了 pcm 流申请了闲暇的 session, 最终通过 apr 包将数据发给了 DSP,
可是 DSP 的硬件输入接口对 8155 平台来说有 5 组 TDM,每组 TDM 还有 RX0, RX1 等性能,那么咱们的 pcm 流数据如何通知 DSP 要往哪个 TDM 哪个性能输入的呢?

在揭晓答案前,咱们先看下 BE dai link 及相干定义(可跳过)。

dai link 及 定义

// dai link
static struct snd_soc_dai_link msm_common_be_dai_links[] = {
    /* Backend AFE DAI Links */
    ...
    {
        .name = LPASS_BE_QUIN_TDM_RX_0, // "QUIN_TDM_RX_0"
        .stream_name = "Quinary TDM0 Playback",
        .no_pcm = 1,
        .dpcm_playback = 1,
        .id = MSM_BACKEND_DAI_QUIN_TDM_RX_0,
        .be_hw_params_fixup = msm_tdm_be_hw_params_fixup,
        .ops = &sa8155_tdm_be_ops,
        .ignore_suspend = 1,
        .ignore_pmdown_time = 1,
        SND_SOC_DAILINK_REG(quin_tdm_rx_0),
    },

// quin_tdm_rx_0 定义
SND_SOC_DAILINK_DEFS(quin_tdm_rx_0,
    DAILINK_COMP_ARRAY(COMP_CPU("msm-dai-q6-tdm.36928")), // cpu 组件 msm-dai-q6-v2.c
    DAILINK_COMP_ARRAY(COMP_CODEC("msm-stub-codec.1", "msm-stub-rx")),
    DAILINK_COMP_ARRAY(COMP_PLATFORM("msm-pcm-routing"))); // platform 组件 msm-pcm-routing-v2.c

cpu 组件 ”msm-dai-q6-tdm.36928″ 36928,对应的是 AFE_PORT_ID_QUINARY_TDM_RX,也即 0x9040

// 36928 -> AFE_PORT_ID_QUINARY_TDM_RX
kernel/msm-5.4/techpack/audio/include/dsp/apr_audio-v2.h

/* Start of the range of port IDs for TDM devices. */
#define AFE_PORT_ID_TDM_PORT_RANGE_START    0x9000

#define AFE_PORT_ID_QUINARY_TDM_RX \
    (AFE_PORT_ID_TDM_PORT_RANGE_START + 0x40)

其仅有惟一的 dai, 即 COMP_CPU(“msm-dai-q6-tdm.36928”) 对应的 dai 是,

static struct snd_soc_dai_driver msm_dai_q6_tdm_dai[] = {...
    {
        .playback = {
            .stream_name = "Quinary TDM0 Playback",
            .aif_name = "QUIN_TDM_RX_0",
            .rates = SNDRV_PCM_RATE_48000 | SNDRV_PCM_RATE_8000 |
                SNDRV_PCM_RATE_16000 | SNDRV_PCM_RATE_48000 |
                SNDRV_PCM_RATE_176400 | SNDRV_PCM_RATE_352800,
            .formats = SNDRV_PCM_FMTBIT_S16_LE |
                   SNDRV_PCM_FMTBIT_S24_LE |
                   SNDRV_PCM_FMTBIT_S32_LE,
            .channels_min = 1,
            .channels_max = 16,
            .rate_min = 8000,
            .rate_max = 352800,
        },
        .name = "QUIN_TDM_RX_0",
        .ops = &msm_dai_q6_tdm_ops, // prepare hw_params set_tdm_slot set_sysclk 等办法
        .id = AFE_PORT_ID_QUINARY_TDM_RX,
        .probe = msm_dai_q6_dai_tdm_probe,
        .remove = msm_dai_q6_dai_tdm_remove,
    },

前后端连贯

下面插曲了一下后端 dai 定义的一些货色,当前也须要哪儿查代码。

回到 cpu 侧如何通知 dsp 哪个 pcm 流 (对应的 session) 要往哪个设施上写这个问题上来。

咱们 HAL 层操作 章节讲的用命令行播放,也只有
`
tinymix “QUIN_TDM_RX_0 Audio Mixer MultiMedia22” “1”
`

进行了前后端连贯操作,猜想这里会把信息通知 dsp, 是不是这么回事呢?咱们持续看看:

tinymix 其实是通过声卡 /dev/snd/controlC0 (0 示意声卡 0) 进行 control,

QUIN_TDM_RX_0 Audio Mixer 相干信息如下,

kernel/msm-5.4/techpack/audio/asoc/msm-pcm-routing-v2.c

static const struct snd_soc_dapm_widget msm_qdsp6_widgets_tdm[] = {...
    SND_SOC_DAPM_MIXER("QUIN_TDM_RX_0 Audio Mixer", SND_SOC_NOPM, 0, 0,
                quin_tdm_rx_0_mixer_controls,
                ARRAY_SIZE(quin_tdm_rx_0_mixer_controls)),

static const struct snd_kcontrol_new quin_tdm_rx_0_mixer_controls[] = {...
    SOC_DOUBLE_EXT("MultiMedia22", SND_SOC_NOPM,
    MSM_BACKEND_DAI_QUIN_TDM_RX_0, // be dai, shift_left, .shift
    // fe dai, shift_right, .rshift
    MSM_FRONTEND_DAI_MULTIMEDIA22, 1, 0, msm_routing_get_audio_mixer,
    msm_routing_put_audio_mixer),

也就是说设置 “QUIN_TDM_RX_0 Audio Mixer MultiMedia22” 时会调用 msm_routing_put_audio_mixer() 进行操作

static int msm_routing_put_audio_mixer(struct snd_kcontrol *kcontrol,
            struct snd_ctl_elem_value *ucontrol)
{...
    // 设置为 1
    if (ucontrol->value.integer.value[0] &&
       msm_pcm_routing_route_is_set(mc->shift, mc->rshift) == false) {
        // 路由解决   
        msm_pcm_routing_process_audio(mc->shift, mc->rshift, 1);
        // dapm 更新电源状态
        snd_soc_dapm_mixer_update_power(widget->dapm, kcontrol, 1,
            update);
    ...
}

msm_bedais 和 fe_dai_map 记录着前后端的信息,
msm_pcm_routing_process_audio() 里,如果后端 dai 处于 active 且前端流 id(即 audio_client 的 session)无效,
则会通过 adm_matrix_map()把 session 信息做为 apr 附载发给 DSP, DSP 收到信息后就晓得 pcm 流该往哪个设施写了。

reg -> be dai, val -> fe dai, set -> 0/1
msm_pcm_routing_process_audio(u16 reg, u16 val, int set)
+ if (set) {+ fdai = &fe_dai_map[val][session_type];
  | // 后端 dai active 且前端 session 不为 -1
  + if (msm_bedais[reg].active && fdai->strm_id !=
    |       INVALID_SESSION) {
    | // 设施关上
    + copp_idx = adm_open(port_id, ..., acdb_dev_id,
    | + // kernel/msm-5.4/techpack/audio/dsp/q6adm.c
    | + 省略... open_v8.hdr.dest_svc = APR_SVC_ADM;
    |
    | // 更新路由信息
    + msm_pcm_routing_build_matrix(val, ...);
    | + int port_id = get_port_id(msm_bedais[i].port_id);
    | + payload.port_id[num_copps] = port_id; // payload.port_id[]里即为后端
    | |
    | // ** fe_dai_map 里找到 strm_id, 即 pcm 流对应的 audio client 的 session **
    | + payload.session_id = fe_dai_map[fedai_id][sess_type].strm_id;
    | + adm_matrix_map(fedai_id, path_type, payload, perf_mode, passthr_mode);
    |   + // kernel/msm-5.4/techpack/audio/dsp/q6adm.c
    |   + route_set_opcode_matrix_id(&route, path, passthr_mode);
    |   |  + case ADM_PATH_PLAYBACK:
    |   |      route->hdr.opcode = ADM_CMD_MATRIX_MAP_ROUTINGS_V5; // 更新路由矩阵操作码
    |   |
    |   | session 更新到 matrix_map,做为 apr 包附载发过来
    |   + node->session_id = payload_map.session_id;
    |   + ret = apr_send_pkt(this_adm.apr, (uint32_t *)matrix_map);
    + }

咱们用 tinymix 连贯前后端的时候,没有进行任何的 pcm open/write 操作,所以 strm_id 都没调配,下面的代码仅是播放之后有路由更新才会执行。

那么咱们播放时首次在哪个阶段更新的路由信息呢?
答案是在 prepare 阶段。
可看下 adm_matrix_map()的 dump_stack():

dump_stack+0xb8/0x114
adm_matrix_map+0x58/0x5c4 [q6_dlkm]
msm_pcm_routing_reg_phy_stream+0x7c0/0x8f8 [platform_dlkm]
msm_pcm_playback_prepare+0x2ec/0x48c [platform_dlkm]
msm_pcm_prepare+0x20/0x3c [platform_dlkm]
snd_soc_component_prepare+0x44/0x80
soc_pcm_prepare+0xa0/0x28c
dpcm_fe_dai_prepare+0x110/0x2f4
snd_pcm_do_prepare+0x40/0xfc
snd_pcm_action_single.llvm.4985077898353288322+0x70/0x168
snd_pcm_common_ioctl+0x1030/0x1320
snd_pcm_ioctl_compat+0x234/0x3b4
__arm64_compat_sys_ioctl+0x10c/0x41c

另外呢在 snd_soc_dapm_mixer_update_power(), compress 播放设置 hw 参数阶段或者其余情景也会更新路由信息。

总结

对 HAL 层来说,播放要做的事就是,
首先设置路由,连贯前后端,
pcm open 时会从 session[]里找个 audio_session 闲暇的 session, 其对应者 pcm 前端流,
在 prepare 时会将前端 session 对应的后端路由信息发送给 DSP,
之后 pcm write 写数据时,前端通过 q6asm 把数据发给 DSP,DSP 会依据路由信息把数据往后端 port 输入。

退出移动版