高通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 54msm8974/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设备播放

【高通8155 音频数据从HAL到DSP】第一条命令设置下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->opssubstream->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 = https://www.it610.com/article/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_START0x9000#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.cstatic 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输出。

    推荐阅读