从数据流的角度整顿下安卓平台音频数据从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 1repo 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 54msm8974/platform.cstatic 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.cmain() + 参数解析 + 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/pcm00-00: MultiMedia1 (*) : : playback 1 : capture 100-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/infocard: 0device: 54subdevice: 0stream: CAPTUREid: MultiMedia22 (*)name: subname: subdevice #0class: 0subclass: 0subdevices_count: 1subdevices_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.cstatic 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.hSND_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.cstatic 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驱动probemsm_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.csnd_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.cint 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_operationsconst 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.csnd_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.cstatic 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 linkstatic 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_RXkernel/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.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/1msm_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/0x114adm_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/0x80soc_pcm_prepare+0xa0/0x28cdpcm_fe_dai_prepare+0x110/0x2f4snd_pcm_do_prepare+0x40/0xfcsnd_pcm_action_single.llvm.4985077898353288322+0x70/0x168snd_pcm_common_ioctl+0x1030/0x1320snd_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输入。