从数据流的角度整理下安卓平台音频数据从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, 可看下文档 <
,下图也出自该文档,
| 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输出。