当前位置: 首页 > news >正文

高通音频数据从HAL到DSP

概述

参考高通平台8155

从数据流的角度整理下安卓平台音频数据从HAL层到达DSP这个流程;

以 MultiMedia22 --> QUIN_TDM_RX_0 播放为例;

主要关注pcm数据写到dsp, 以及将前后端路由信息告知dsp两个点。

<!-- more -->

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

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

阅读本文最好对ALSA或者ASOC(ALSA System on Chip)有所了解,相关的文档可看下

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

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

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

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

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

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

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

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

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

高通平台adsp驱动为了实现这些,软件上又分为 ASM(q6asm.c) ADM(q6adm.c) AFE(q6afe.c)

  • 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.c
static int pcm_device_table[AUDIO_USECASE_MAX][2] = {
...// use case rear seat对应的的pcm设备54[USECASE_AUDIO_PLAYBACK_REAR_SEAT] = {REAR_SEAT_PCM_DEVICE,REAR_SEAT_PCM_DEVICE},

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

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

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

tinyplay播放流程挺简单的,整理如下:

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

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

  • pcm_open() 打开设备并设置软硬件参数;
  • pcm_prepare() 准备;
  • pcm_write() 准备好后写数据;

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

直觉上,我们跟着分析pcm_write()就可以知道这个数据流向了,不过在分析该函数之前,我们得明确这个写究竟是往内核的哪个设备写了?

pcm 设备信息查看

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

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

如上面我们例子的pcm 54, id名为 MultiMedia22, 支持1个播放1个录音。

PCM设备的详细信息还可以通过如下命令查看

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

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

FE

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 */...{// dai_link名,展开就是"SA8155 Media22".name = MSM_DAILINK_NAME(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,// 宏,定义cpu codec platformSND_SOC_DAILINK_REG(multimedia22),},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.cDAILINK_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.cDAILINK_COMP_ARRAY(COMP_PLATFORM("msm-pcm-dsp.0")));
Tips:
sa8155.c 属于asoc的machine部分

我们可以顺便看一眼其对应的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信息| // 根据dai link赋值of_node,如果找到那么 cpus->dai_name和platforms->name为NULL;+ msm_populate_dai_link_component_of_node()+ 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排第54sizeof(msm_common_dai_links));...dailink = msm_auto_dai_links;}...     // dai link给声卡的dai_linkcard->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匹配,|     |   + snd_soc_is_matching_component(dlc, component) // 先匹配of_node|     |   | // 然后如果dai_name不为空,比较组件驱动名字和dai_link中cpu_dai_name|     |   + strcmp(..., dlc->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.csnd_rawmidi_dev_register()
seq_device.csnd_seq_device_dev_register()
jack.csnd_jack_dev_register()
hwdep.csnd_hwdep_dev_register()
pcm.csnd_pcm_dev_register()
compress_offload.csnd_compress_dev_register()
timer.csnd_timer_dev_register()
control.csnd_ctl_dev_register()
ac97_codec.csnd_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为设备号,如我们例子的54dev_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_CAPTUREfor (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)
{...// 查找空闲的minorminor = 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()| // fe dai, codec组件,cpu组件都打开+ for_each_rtdcom(rtd, rtdcom)snd_soc_component_open(component, substream);+ component->driver->ops->open(substream)# 注释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 { // 没有采用dpcmrtd->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的打开,并未定义open函数(其是在prepare时进行afe的start操作),
前端所有组件打开中,platform组件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)) {|   | // 是否有可用cpu_buf|   + data = q6asm_is_cpu_buf_avail(IN, prtd->audio_client, &size,|   |  // 从用户空间拷贝到bufptr, bufptr = data;|   + copy_from_user(bufptr, buf, xfer)|   ||   + 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;|   |   | // audio client的当前port buf的地址,即有效数据的地址|   |   + write.buf_addr_lsw = lower_32_bits(ab->phys);|   +   + 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,// cpu组件 msm-dai-q6-v2.cDAILINK_COMP_ARRAY(COMP_CPU("msm-dai-q6-tdm.36928")),DAILINK_COMP_ARRAY(COMP_CODEC("msm-stub-codec.1", "msm-stub-rx")),// platform组件 msm-pcm-routing-v2.cDAILINK_COMP_ARRAY(COMP_PLATFORM("msm-pcm-routing")));

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",// prepare hw_params set_tdm_slot set_sysclk 等方法.ops = &msm_dai_q6_tdm_ops, .id = AFE_PORT_ID_QUINARY_TDM_RX,.probe = msm_dai_q6_dai_tdm_probe,.remove = msm_dai_q6_dai_tdm_remove,},

前后端连接

上面插曲了一下后端dai定义的一些东西,以后也需要哪儿查代码。其操作函数ops里的几个方法, 通过 q6afe.c 里的接口与DSP交互。

回到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, .rshiftMSM_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)
{...// 设置为1if (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[MSM_BACKEND_DAI_MAX] 和 fe_dai_mapMSM_FRONTEND_DAI_MAX 记录着前后端的信息,
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输出。

相关文章:

高通音频数据从HAL到DSP

概述 参考高通平台8155 从数据流的角度整理下安卓平台音频数据从HAL层到达DSP这个流程; 以 MultiMedia22 --> QUIN_TDM_RX_0 播放为例; 主要关注pcm数据写到dsp, 以及将前后端路由信息告知dsp两个点。 <!-- more --> [Platform:高通 8155 gvmq Android 11] [Ker…...

第六天 开始Unity Shader的学习之Unity中的基础光照之漫反射光照模型

Unity Shader的学习笔记 第六天 开始Unity Shader的学习之Unity中的基础光照之漫反射光照模型 文章目录 Unity Shader的学习笔记前言一、漫反射光照模型1.逐像素光照① 更改v2f② 传递法线信息给片元着色器③ 片元着色器计算漫反射光照模型 二.半兰伯特模型总结 前言 提示&am…...

【RabbitMQ】队列模型

1.概述 RabbitMQ作为消息队列&#xff0c;有6种队列模型&#xff0c;分别在不同的场景进行使用&#xff0c;分别是Hello World&#xff0c;Work queues&#xff0c;Publish/Subscribe&#xff0c;Routing&#xff0c;Topics&#xff0c;RPC。 下面就分别对几个模型进行讲述。…...

【Java设计模式】第3章 软件设计七大原则

3-1 本章导航 学习开辟原则(基础原则)依赖倒置原则单一职责原则接口隔离原则迪米特法则(最少知道原则)里氏替换原则合成复用原则(组合复用原则)核心思想: 设计原则需结合实际场景平衡,避免过度设计。设计模式中可能部分遵循原则,需灵活取舍。3-2 开闭原则讲解 定义 软…...

Axure中继器(Repeater): 列表展示

文章目录 引言I 中继器说明中继器的作用中继器的结构中继器例子II 中继器基础应用:列表展示表头制作列表内容表头中的列与中继器的列绑定填充数据内容引言 中继器是Axure RP 7.0推出的新功能,用于快速设计一些复杂的交互界面(制作“高保真”的动态原型)。 I 中继器说明 中…...

mybatis的第五天学习笔记

12. 动态SQL 12.1 动态SQL概述 新增内容&#xff1a; 动态SQL执行流程 MyBatis如何解析动态SQLSQL语句构建过程参数绑定机制 新增示例 // 动态条件查询接口示例 List<User> searchUsers(Param("name") String name,Param("age") Integer age,Para…...

LeetCode 941 有效的山脉数组

算法探索&#xff1a;如何精准判断有效山脉数组 在计算机科学领域&#xff0c;算法和数据结构堪称基石&#xff0c;它们不仅是解决复杂问题的有力工具&#xff0c;更是衡量程序员技术水平的重要指标。数组作为最基础、应用最广泛的数据结构之一&#xff0c;围绕它衍生出了大量…...

java设计模式-单例模式

单例模式 1、饿汉式(静态常量) Slf4j public class SingletonTest01 {public static void main(String[] args) {Singleton singleton Singleton.getInstance();Singleton singleton2 Singleton.getInstance();log.info("比对结果&#xff1a;{}",singletonsingl…...

对抗Prompt工程:构建AI安全护栏的攻防实践

大语言模型的开放性与自然语言交互特性使其面临前所未有的Prompt工程攻击威胁。本文通过分析2021-2023年间157个真实越狱案例&#xff0c;揭示语义混淆、上下文劫持、多模态组合三重攻击路径的技术原理&#xff0c;提出融合动态意图拓扑分析&#xff08;DITA&#xff09;、对抗…...

CentOS 环境下 MySQL 数据库全部备份的操作指南

最近阿里云个人服务到期&#xff0c;因为是很久之前买的测试机器&#xff0c;配置较低&#xff0c;上面运行的有技术博客 和以往的测试项目&#xff0c;所以准备放弃掉。 需要备份下上面的表结构和数据、以及代码仓库。 下面是一个完整的 CentOS 环境下 MySQL 数据库全部备份…...

回溯算法补充leetcode

1. 组合 leetcode题目链接&#xff1a;77. 组合 给定两个整数 n 和 k&#xff0c;返回范围 [1, n] 中所有可能的 k 个数的组合。 你可以按 任何顺序 返回答案。 示例 1&#xff1a; 输入&#xff1a;n 4, k 2 输出&#xff1a; [[2,4],[3,4],[2,3],[1,2],[1,3],[1,4], ] 示…...

利用 AI 实现雷池 WAF 自动化运维

欢迎加入雷池社区&#xff1a;雷池 WAF | 下一代 Web 应用防火墙 | 免费使用 已经升级到 8.4.0 的兄弟们应该会发现雷池又多了一些 AI 能力&#xff0c;8.4.0 更新公告。 感谢 Web2GPT 为雷池提供的 AI 能力支持。 主要变化 右下角多了一个 AI 小助手 按钮右上角多了一个 连…...

【嵌入式面试】

1、如果中断函数中有耗时较长的内容&#xff0c;会导致以下问题&#xff0c;如何解决&#xff1f; 对系统实时性的影响 阻塞低优先级中断&#xff1a;中断函数执行时间过长&#xff0c;会阻塞其他低优先级中断的响应。例如&#xff0c;如果一个高优先级中断处理程序中包含耗时…...

【Hadoop入门】Hadoop生态之HDFS

1 HDFS核心设计原理 HDFS&#xff08;Hadoop Distributed File System&#xff09;是专为大规模数据存储设计的分布式文件系统&#xff0c;其核心设计基于以下原则&#xff1a; 数据分块与分布式存储&#xff1a; 分块机制&#xff1a;文件被切分为固定大小的数据块&#xff08…...

试剂SYBR 14核酸染料在染色时的操作步骤(说明)

化学试剂的基本内容||试剂参数 ---中文名&#xff1a;SYBR 14核酸染料 ---英文名&#xff1a;SYBR 14 Nucleic Acid Stain ---浓度&#xff1a;通常以5mM的DMSO储存液形式提供。 ---吸收波长&#xff1a;488nm ---发射波长&#xff1a;518nm ---出厂商&#xff1a;西安强…...

Spring Boot 国际化配置项详解

Spring Boot 国际化配置项详解 1. 核心配置项分类 将配置项分为以下类别&#xff0c;便于快速定位&#xff1a; 1.1 消息源配置&#xff08;MessageSource 相关&#xff09; 控制属性文件的加载、编码、缓存等行为。 配置项作用默认值示例说明spring.messages.basename指定属…...

Python之禅:深入理解Python设计哲学

Python之禅(The Zen of Python)是Python语言的核心设计哲学&#xff0c;由Python创始人Guido van Rossum和Tim Peters共同制定。理解Python之禅不仅能帮助我们写出更"Pythonic"的代码&#xff0c;还能深入把握Python语言的设计理念。 Python之禅的由来 Python之禅最…...

Rancher 全面介绍

目录 Rancher 全面介绍1. **Rancher 的定义与核心功能**2. **Rancher 的应用场景**3. **Rancher 的生态系统**4. **Rancher 的优势**5. **总结** Rancher 全面介绍 1. Rancher 的定义与核心功能 Rancher 是一个开源的企业级多集群 Kubernetes 管理平台&#xff0c;旨在简化容…...

Docker常用命令

镜像命令 搜索镜像 docker search nginx 拉取镜像 docker pull nginx&#xff0c;默认拉取最新镜像 docker pull nginx:1.25.3&#xff0c;拉取指定版本 查看镜像 docker images 删除镜像 docker rmi nginx:1.25.3 docker rmi -f $(docker images -aq)&#xff0c;删除全…...

项目中如何防止超卖

什么是超卖&#xff1f;假如只剩下一个库存&#xff0c;却被多个订单买到了&#xff0c;简单理解就是库存不够了还能正常下单。 方案1&#xff1a;数据库行级锁 1. 实体类 Data TableName("product") public class Product {TableId(type IdType.AUTO)private Lon…...

龙虎榜——20250408

行情如下 根据2025年4月8日的龙虎榜的行业分析如下&#xff1a; 一、农业种植与乡村振兴 • 政策催化&#xff1a;推进种业自主创新、农机装备升级等目标&#xff0c;叠加中美关税反制逻辑。 • 市场表现&#xff1a; • 农业种植&#xff1a;种子类企业因国产替代预期受资…...

快速上手Vue3国际化 (i18n)

文章目录 一、背景介绍二、页面效果三、使用步骤四、代码1.src/App.vue2.src/main.js3.src/locales/index.js4.src/views/login/_request.js5.src/locales/en.json6.src/locales/zh.json7.SystemParam.vue8.I18NController.java9.DataServiceConfigValue.java10.ConfigValue.ja…...

Mistral OCR:重新定义文档理解的下一代 OCR 技术

引言 在数字化时代,文档处理和理解是企业、科研机构以及个人工作流程中的重要环节。然而,传统的光学字符识别(OCR)技术往往难以应对复杂文档中的多语言、多模态内容。近日,法国 AI 明星创企 Mistral AI 推出了一款名为 Mistral OCR 的光学字符识别 API,以其卓越的性能和…...

前端面试核心知识点整理:从 JavaScript 到 Vue 全解析

一、JavaScript 异步编程核心:Promise 与 async/await 1. Promise 深度解析 定义:Promise 是处理异步操作的对象,代表一个异步操作的最终状态(成功 / 失败)。三种状态: pending(进行中):初始状态,异步操作未完成。fulfilled(已成功):异步操作成功,调用 resolve …...

npm fund 命令的作用

运行别人的项目遇到这个问题&#xff1a; npm fund 命令的作用 npm fund 是 npm 提供的命令&#xff0c;用于显示项目依赖中哪些包需要资金支持。这些信息来自包的 package.json 中定义的 funding 字段&#xff0c;目的是帮助开发者了解如何支持开源维护者。 典型场景示例 假…...

LeetCode344反转字符串

思路&#xff1a; 交换即可 void reverseString(char* s, int sSize) {int jsSize-1;for(int i0;i<sSize/2;i){int tmps[i];s[i]s[j];s[j]tmp;j--;} }...

[Python] 企业内部应用接入钉钉登录,端内免登录+浏览器授权登录

[Python] 为企业网站应用接入钉钉鉴权&#xff0c;实现钉钉客户端内自动免登授权&#xff0c;浏览器中手动钉钉授权登录两种逻辑。 操作步骤 企业内部获得 开发者权限&#xff0c;没有的话先申请。 访问 钉钉开放平台-应用开发 创建一个 企业内部应用-钉钉应用。 打开应用…...

设计模式-单例设计模式

目录 什么是单例设计模式&#xff1f; 为什么要使用单例模式&#xff1f; 资源方面 数据一致方面 系统性能方面 代码维护方面 如何设计单例类&#xff1f; 在说模式之前&#xff0c;我们需要先知道怎么设计才可以让一个类只能有一个实例化对象呢&#xff1f; 饿汉模式…...

Nextjs15 实战 - React Notes CURD 实现

本专栏内容均可在Github&#xff1a;notes_04 找到 完整项目使用技术栈&#xff1a; Nextjs15 MySQL Redis Auth Prisma i18n strapi Docker vercel 一、本节目标 本篇我们来实现右侧笔记CURD部分。 一、效果 当点击 New 按钮的时候进入编辑界面&#xff1a; 当点击…...

【KWDB 创作者计划】架构设计与AIoT场景实践

产品定位与核心价值主张 架构设计与技术实现 分布式架构设计 多模存储引擎实现 云边端协同机制 核心技术创新解析 就地计算技术 自适应时序引擎 混合事务处理 性能优化技术体系 高效存储机制 查询加速策略 资源管理与隔离 行业解决方案与典型应用 工业物联网平台…...

DeepSeek底层揭秘——《推理时Scaling方法》技术对比浅析

4月初&#xff0c;DeepSeek 提交到 arXiv 上的最新论文正在 AI 社区逐渐升温。 笔者尝试对比了“关于推理时Scaling”与现有技术&#xff0c;粗浅分析如下&#xff1a; 与LoRA的对比 区别&#xff1a; 应用场景&#xff1a;LoRA是一种参数高效微调方法&#xff0c;主要用于在…...

Spring MVC与Spring Boot文件上传配置差异对比及文件上传关键类详细说明与对比

一、Spring MVC与Spring Boot文件上传配置差异对比 1. 配置方式差异 框架配置方式依赖管理自动配置Spring MVC需手动配置MultipartResolver&#xff08;如StandardServletMultipartResolver&#xff09;需自行引入commons-fileupload等依赖无&#xff0c;默认不启用文件上传支…...

Linux网络配置与测试

目录 一.与网络配置相关的命令 1.1ifconfig命令 1.1.1作用 1.1.2网络接口的信息 接口信息的组成 1.1.3显示所有网卡包括没有启动的网卡 1.1.4查看指定网络接口 1.1.5开启或关闭网卡 1.1.6设置临时虚拟网卡 1.1.7网络通讯情况 ​编辑 1.1.8临时修改网卡属性 1.2hos…...

游戏赛季和数据处理

问题 游戏从无赛季到赛季机制会涉及哪些问题&#xff1a; 如何改动&#xff0c;增加赛季机制&#xff0c;涉及要修改的代码量最少如何改动&#xff0c;账号、角色部分数据继承问题&#xff0c;涉及要修改的代码量最少账号下角色的永久服共享或是永久服独立&#xff0c;需要做…...

京东店铺托管7*16小时全时护航

内容概要 京东店铺托管服务的*716小时全时护航模式&#xff0c;相当于给商家配了个全年无休的"运营管家"。专业团队每天从早7点到晚11点实时盯着运营数据和商品排名&#xff0c;连半夜流量波动都能通过智能系统秒级预警。这种全天候服务可不是单纯拼人力——系统自动…...

HTTP的Keep-Alive是什么?TCP 的 Keepalive 和 HTTP 的 Keep-Alive 是一个东西吗?

HTTP的Keep-Alive&#xff1a; HTTP Keep-Alive 是一种机制&#xff0c;允许客户端和服务器在单个 TCP 连接 上发送多个 HTTP 请求 和 响应&#xff0c;而不是每次请求和响应后都关闭连接。它的主要目的是提高性能&#xff0c;减少连接的开销&#xff0c;优化通信效率。 工作…...

使用scoop一键下载jdk和实现版本切换

安装 在 PowerShell 中输入下面内容&#xff0c;保证允许本地脚本的执行&#xff1a; set-executionpolicy remotesigned -scope currentuser然后执行下面的命令安装 Scoop&#xff1a; iwr -useb get.scoop.sh | iex国内用户可以使用镜像源安装&#xff1a;powershell iwr -us…...

PPIO × UI-TARS:用自然语言操控电脑,AI Agent 的极致体验

Manus的爆火预示着AI 正在从单纯的文本生成和图像识别迈向更复杂的交互场景。字节跳动近期推出的开源项目 UI-TARS Desktop 为我们展示了一种全新的可能性&#xff1a;能够通过自然语言理解和处理来控制计算机界面。这款工具代表了人工智能与人机交互领域的重大突破&#xff0c…...

PG:incorrect prev-link

目录 WAL日志中"incorrect prev-link"错误解决方案错误原因分析解决步骤典型修复案例 WAL日志中"incorrect prev-link"错误解决方案 错误原因分析 WAL日志的prev-link字段用于确保日志记录的连续性。当出现incorrect prev-link 2/754ECB0 at 2/8000028错…...

SQL Server 数据库邮件配置失败:SMTP 连接与权限问题

问题现象&#xff1a; 配置数据库邮件时&#xff0c;发送测试邮件失败&#xff0c;提示 “邮件无法发送到 SMTP 服务器&#xff0c;操作超时”&#xff08;错误 14661&#xff09;或 “服务器拒绝发件人地址”&#xff08;错误 15009&#xff09;。 快速诊断 检查数据库邮件配置…...

深入浅出动态规划:从基础到蓝桥杯实战(Java版)

引言&#xff1a;为什么你需要掌握动态规划&#xff1f; 动态规划&#xff08;DP&#xff09;是算法竞赛和面试中的常客&#xff0c;不仅能大幅提升解题效率&#xff08;时间复杂度通常为O(n)或O(n)&#xff09;[4]&#xff0c;更是解决复杂优化问题的利器。统计显示&#xff…...

获取cookie的chrome插件:Get cookies.txt LOCALLY

接上一篇&#xff0c;在下载视频的时候需要网站的cookie&#xff0c;下面介绍一款可以获取网站cookie的chrome插件 https://chromewebstore.google.com/detail/get-cookiestxt-locally/cclelndahbckbenkjhflpdbgdldlbecc?utm_sourceitem-share-cb 备注需要科学上网 【使用方…...

opencv无法设置禁用RGB转换问题

树莓派连接摄像头,摄像头输出格式为YUYV(YUV422)。 通过执行 v4l2-ctl --list-formats --device/dev/video0 可以看的具体的摄像头的数据格式。 使用opencv获取视频流&#xff0c;通过cap.set(cv2.CAP_PROP_CONVERT_RGB, 0)设置禁用自动转换RGB格式&#xff0c;但是打印输出…...

Ansible:roles角色

文章目录 Roles角色Ansible Roles目录编排Roles各目录作用创建 roleplaybook调用角色调用角色方法1&#xff1a;调用角色方法2&#xff1a;调用角色方法3&#xff1a; roles 中 tags 使用实战案例 Roles角色 角色是ansible自1.2版本引入的新特性&#xff0c;用于层次性、结构化…...

SAP系统采购信息记录失效

问题&#xff1a;采购信息记录失效 现象&#xff1a;最初主数据导入完成之后&#xff0c;单元测试的时采购信息记录是有效的&#xff0c;中间经过配置的变化&#xff0c;集成测试初期发现采购信息记录全部失效。 原因&#xff1a; 单元测试时发现采购订单里面的条件类型…...

JavaWeb 课堂笔记 —— 04 Ajax

本系列为笔者学习JavaWeb的课堂笔记&#xff0c;视频资源为B站黑马程序员出品的《黑马程序员JavaWeb开发教程&#xff0c;实现javaweb企业开发全流程&#xff08;涵盖SpringMyBatisSpringMVCSpringBoot等&#xff09;》&#xff0c;章节分布参考视频教程&#xff0c;为同样学习…...

Pandas 库

Pandas 是一个开源的数据分析和数据处理库&#xff0c;它是基于 Python 编程语言的。 Pandas 提供了易于使用的数据结构和数据分析工具&#xff0c;特别适用于处理结构化数据&#xff0c;如表格型数据 Pandas 是数据科学和分析领域中常用的工具之一&#xff0c;它使得用户能够…...

4.8学习总结

完成摆动序列的算法题&#xff08;比较难&#xff0c;想不出方法&#xff09; 学习了HashMap,TreeMap 的源码&#xff08;看完一遍对其理解没有太清楚&#xff0c;还需再多刷几遍理解源码及其底层逻辑的概念&#xff09; 学习了可变参数和Collections工具类...

C语言之九九乘法表

一、代码展示 二、运行结果 三、代码分析 首先->是外层循环是小于等于9的 然后->是内层循环是小于等于外层循环的 最后->就是\n让九九乘法表的格式更加美观(当然 电脑不同 有可能%2d 也有可能%3d) 四、与以下素数题目逻辑相似 五、运行结果...

【Linux操作系统】:信号

Linux操作系统下的信号 一、引言 首先我们可以简单理解一下信号的概念&#xff0c;信号&#xff0c;顾名思义&#xff0c;就是我们操作系统发送给进程的消息。举个简单的例子&#xff0c;我们在写C/C程序的时候&#xff0c;当执行a / 0类似的操作的时候&#xff0c;程序直接就挂…...