高通8155 音频数据从HAL到DSP

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

[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)有所了解,相关的文档可看下

<>/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, 可看下文档 <>/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

    
    

该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, ¶ms)
      |   + 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输出。

你可能感兴趣的:(linux-kernel音频)