usb声卡驱动(六):usb声卡中的pcm打开和关闭
usb声卡驱动(六)
前面记录了usb声卡驱动的注册过程。
下面,查看usb声卡里面pcm的打开和关闭,都做了什么工作。
一点基础前提
因为本系列文章的核心是,usb声卡驱动。所以并不会深入到alsa内部的细节。但是在进行pcm的打开和关闭之前。需要知道一些alsa内部的一些东西。
在《usb声卡驱动(四)》和《usb声卡驱动(五)》中提到了一个函数:
int snd_card_register(struct snd_card * card);
该函数,需要知道如下的一个简单逻辑。
- 它遍历所有的component,比如前面提到的pcm
- 然后调用这些component的dev_register回调。
- 在这些回调中,会有各个component专用的一些设置。比如调用device_create创建对应的驱动文件等。这样应用就可以通过文件系统来使用它了。
好了,仅仅知道这些就可以了。下面,看看pcm的打开。
pcm的打开
在《usb声卡驱动(四)》中,提及到,每个pcm可以有多个substream.而每次open时,就是open一个substream出来。而substream的个数,由下面函数的第四,第五参数指定。
int snd_pcm_new(struct snd_card * card, const char * id, int device, int playback_count, int capture_count, struct snd_pcm ** rpcm)
当有多个substream时,每次open,就打开一个substream,在使用过程中,通过substream的index来区分
当只有一个substream时,第一次open,就打开一个substream。如果重复打开,则依赖文件是否要阻塞,如果阻塞,则open调用不会返回。如果非阻塞,则返回一个错误值。
在usb声卡驱动中,只有一个substream,想一想为什么?
代码如下(详情见《usb声卡驱动(五)》):
//啦啦啦,终于找到,我们信息苦苦期待的创建pcm对象啦err = snd_pcm_new(chip->card, "USB Audio", chip->pcm_devs,stream == SNDRV_PCM_STREAM_PLAYBACK ? 1 : 0,stream == SNDRV_PCM_STREAM_PLAYBACK ? 0 : 1,&pcm);
对于usb声卡驱动而言,在open时,substream直接由参数,传递进来。那么substrea是什么时候创建的呢?
因为本笔记不是对alsa的解析,所以只给出alsa内部的简单逻辑,等有空了,再来详细记录一下alsa内部的世界吧。
- 当调用snd_pcm_new函数创建pcm实例时,内部还会根据传递的playback(capture)的个数,来创建对应个数的substream。
- 这些substream通过内部的next,形成一个链表
- 同一个方向(playback或者capture)的substream。由一个包装类snd_pcm_str包装。因此,会有两个包装类(playback或者capture),他们组成一个snd_pcm_str的数组。
- pcm实例(一个特殊的component)持有上面的snd_pcm_str数组。
- 当需要open的时候,则分配一个已经创建好的substream即可。
alsa针对pcm的open的内部逻辑,几乎就变成了上面的逆序了。简述如下:
- 首先得到pcm实例,和方向信息
- 根据方向信息,从snd_pcm_str的数组中,选择对应的snd_pcm_str对象。
- 从snd_pcm_str对象的substream获取substream的链表。然后判断合适的substream返回即可。
- 调用这个substream的open回调,即《usb声卡驱动(五)》中谈到的snd_usb_playback_ops或者snd_usb_capture_ops中的open回调
接下来,就是snd_usb_playback_ops或者snd_usb_capture_ops中的open回调了.从《usb声卡驱动(四)》可以知道,这个open回调里面,几乎就是对runtime对象的hw字段进行初始化。
如下:
static int snd_usb_playback_open(struct snd_pcm_substream *substream)
{return snd_usb_pcm_open(substream, SNDRV_PCM_STREAM_PLAYBACK);
}static int snd_usb_pcm_open(struct snd_pcm_substream *substream, int direction)
{struct snd_usb_stream *as = snd_pcm_substream_chip(substream);struct snd_pcm_runtime *runtime = substream->runtime;struct snd_usb_substream *subs = &as->substream[direction];subs->interface = -1;subs->altset_idx = 0;//预定义好的硬件描述runtime->hw = snd_usb_hardware;runtime->private_data = subs;subs->pcm_substream = substream;/* runtime PM is also done there *//* initialize DSD/DOP context */subs->dsd_dop.byte_idx = 0;subs->dsd_dop.channel = 0;subs->dsd_dop.marker = 1;return setup_hw_info(runtime, subs);
}
注意,上面的hw赋值,snd_usb_hardware的值如下:
static struct snd_pcm_hardware snd_usb_hardware =
{//表示硬件支持的功能和特性.info = SNDRV_PCM_INFO_MMAP |SNDRV_PCM_INFO_MMAP_VALID |SNDRV_PCM_INFO_BATCH |SNDRV_PCM_INFO_INTERLEAVED |SNDRV_PCM_INFO_BLOCK_TRANSFER |SNDRV_PCM_INFO_PAUSE,//最大buffer.buffer_bytes_max = 1024 * 1024,//最小period.period_bytes_min = 64,//最大period.period_bytes_max = 512 * 1024,//最小/大period数.periods_min = 2,.periods_max = 1024,
};
这个结构体,表示的是usb声卡支持的硬件功能和特性。
上面的函数内容,没有什么可以赘述的,进入setup_hw_info函数,如下:
static int setup_hw_info(struct snd_pcm_runtime *runtime, struct snd_usb_substream *subs)
{struct audioformat *fp;unsigned int pt, ptmin;int param_period_time_if_needed;int err;//赋值格式runtime->hw.formats = subs->formats;//初始化各种值runtime->hw.rate_min = 0x7fffffff;runtime->hw.rate_max = 0;runtime->hw.channels_min = 256;runtime->hw.channels_max = 0;runtime->hw.rates = 0;ptmin = UINT_MAX;/* check min/max rates and channels *///检查各种最小和最大值list_for_each_entry(fp, &subs->fmt_list, list) {runtime->hw.rates |= fp->rates;if (runtime->hw.rate_min > fp->rate_min)runtime->hw.rate_min = fp->rate_min;if (runtime->hw.rate_max < fp->rate_max)runtime->hw.rate_max = fp->rate_max;if (runtime->hw.channels_min > fp->channels)runtime->hw.channels_min = fp->channels;if (runtime->hw.channels_max < fp->channels)runtime->hw.channels_max = fp->channels;if (fp->fmt_type == UAC_FORMAT_TYPE_II && fp->frame_size > 0) {/* FIXME: there might be more than one audio formats... */runtime->hw.period_bytes_min = runtime->hw.period_bytes_max =fp->frame_size;}pt = 125 * (1 << fp->datainterval);//计算出来的周期间隔,单位微妙ptmin = min(ptmin, pt);}//电源管理相关,暂时跳过err = snd_usb_autoresume(subs->stream->chip);if (err < 0)return err;//该值表示,是否需要周期间隔的调整。全速设备不需要,因为它固定param_period_time_if_needed = SNDRV_PCM_HW_PARAM_PERIOD_TIME;//全速设备,间隔时间固定为1000usif (subs->speed == USB_SPEED_FULL)/* full speed devices have fixed data packet interval */ptmin = 1000;if (ptmin == 1000)/* if period time doesn't go below 1 ms, no rules needed */param_period_time_if_needed = -1;//设置,周期间隔的最小值,和最大值snd_pcm_hw_constraint_minmax(runtime, SNDRV_PCM_HW_PARAM_PERIOD_TIME,ptmin, UINT_MAX);//下面一些列都是在添加规则,当修改对应的参数时,对应的回调函数将会被触发if ((err = snd_pcm_hw_rule_add(runtime, 0, SNDRV_PCM_HW_PARAM_RATE,hw_rule_rate, subs,SNDRV_PCM_HW_PARAM_FORMAT,SNDRV_PCM_HW_PARAM_CHANNELS,param_period_time_if_needed,-1)) < 0)goto rep_err;if ((err = snd_pcm_hw_rule_add(runtime, 0, SNDRV_PCM_HW_PARAM_CHANNELS,hw_rule_channels, subs,SNDRV_PCM_HW_PARAM_FORMAT,SNDRV_PCM_HW_PARAM_RATE,param_period_time_if_needed,-1)) < 0)goto rep_err;if ((err = snd_pcm_hw_rule_add(runtime, 0, SNDRV_PCM_HW_PARAM_FORMAT,hw_rule_format, subs,SNDRV_PCM_HW_PARAM_RATE,SNDRV_PCM_HW_PARAM_CHANNELS,param_period_time_if_needed,-1)) < 0)goto rep_err;if (param_period_time_if_needed >= 0) {err = snd_pcm_hw_rule_add(runtime, 0,SNDRV_PCM_HW_PARAM_PERIOD_TIME,hw_rule_period_time, subs,SNDRV_PCM_HW_PARAM_FORMAT,SNDRV_PCM_HW_PARAM_CHANNELS,SNDRV_PCM_HW_PARAM_RATE,-1);if (err < 0)goto rep_err;}//判断是否都是一些常规的采样率。见《usb声卡驱动(四)》if ((err = snd_usb_pcm_check_knot(runtime, subs)) < 0)goto rep_err;return 0;rep_err://电源管理相关,进入suspend状态snd_usb_autosuspend(subs->stream->chip);return err;
}
依然是对hw里面的字段就行赋值。没有什么可记录的,一些关键位置见上面的注释
上面是playback的open,接下来看看capture的open,如下:
static int snd_usb_capture_open(struct snd_pcm_substream *substream)
{return snd_usb_pcm_open(substream, SNDRV_PCM_STREAM_CAPTURE);
}
他们调用到了同一个snd_usb_pcm_open函数中,也是对hw的各种初始化。
虽然上面的函数没有什么可记录,但需要注意一下几点:
- 周期间隔的计算,即在《usb声卡驱动(三)》中谈到的“2的(bInterval-1)次方乘以125us”
- 根据不同的条件,hw里面的值可能会不同,此时使用了《usb声卡驱动(四)》的限制规则。举例如下:
if ((err = snd_pcm_hw_rule_add(runtime, 0, SNDRV_PCM_HW_PARAM_RATE,hw_rule_rate, subs,SNDRV_PCM_HW_PARAM_FORMAT,SNDRV_PCM_HW_PARAM_CHANNELS,param_period_time_if_needed,-1)) < 0)
如果应用修改格式,通道,还有周期时间,那么hw_rule_rate函数就会被触发,
它会根据各种要求,进行必要的设置。内部又是对hw字段的各种赋值,也没啥好记录的。
总结就是:open里面是对runtime的hw的正确赋值。
pcm的关闭
有了前面的打开逻辑,现在直接进入substream的close回调中。如下:
static int snd_usb_playback_close(struct snd_pcm_substream *substream)
{return snd_usb_pcm_close(substream, SNDRV_PCM_STREAM_PLAYBACK);
}static int snd_usb_capture_close(struct snd_pcm_substream *substream)
{return snd_usb_pcm_close(substream, SNDRV_PCM_STREAM_CAPTURE);
}static int snd_usb_pcm_close(struct snd_pcm_substream *substream, int direction)
{struct snd_usb_stream *as = snd_pcm_substream_chip(substream);struct snd_usb_substream *subs = &as->substream[direction];//停止usb端点。分别停止同步端点和数据端点//至于usb端点如何停止,详见后面关于端点操作的详细介绍stop_endpoints(subs, true);if (!as->chip->shutdown && subs->interface >= 0) {//切换接口的设置为0,因为设置0,表示了0带宽。用于释放usb资源usb_set_interface(subs->dev, subs->interface, 0);subs->interface = -1;}subs->pcm_substream = NULL;//当pcm关闭时,电源状态进入一种挂起的状态,暂且不表snd_usb_autosuspend(subs->stream->chip);return 0;
}
上面的函数,会去调用stop_endpoint函数,如下:
static void stop_endpoints(struct snd_usb_substream *subs, bool wait)
{//snd_usb_endpoint_stop停止给定的端点,主要的操作就是将端点的使用计数//减一,如果使用计数变成0,则unlink所有激活的urbif (test_and_clear_bit(SUBSTREAM_FLAG_SYNC_EP_STARTED, &subs->flags))snd_usb_endpoint_stop(subs->sync_endpoint);if (test_and_clear_bit(SUBSTREAM_FLAG_DATA_EP_STARTED, &subs->flags))snd_usb_endpoint_stop(subs->data_endpoint);if (wait) {//表示等待对应的端点中所有已经提交的队列被处理完成。snd_usb_endpoint_sync_pending_stop(subs->sync_endpoint);snd_usb_endpoint_sync_pending_stop(subs->data_endpoint);}
}
这个函数,根据需要,分别停止,同步端点和数据端点,并等到真正的端点关闭。关于同步端点和数据端点,可见《usb声卡驱动(二)》
各个端点的操作函数的语义,见上面的注释
hw参数设置
在open了一个pcm,并正确的返回了一个流之后。常常通过IOCTL,获取这个pcm相关的硬件信息,然后应用选择不同的配置并设置硬件。
在设置里面,分成了两种:hw,和sw。前者表示硬件参数,后者表示软件参数。
先来看看hw的数据结构用下面表示
struct snd_pcm_hw_params {unsigned int flags;struct snd_mask masks[SNDRV_PCM_HW_PARAM_LAST_MASK - SNDRV_PCM_HW_PARAM_FIRST_MASK + 1];struct snd_mask mres[5]; /* reserved masks */struct snd_interval intervals[SNDRV_PCM_HW_PARAM_LAST_INTERVAL -SNDRV_PCM_HW_PARAM_FIRST_INTERVAL + 1];struct snd_interval ires[9]; /* reserved intervals */unsigned int rmask; /* W: requested masks */unsigned int cmask; /* R: changed masks */unsigned int info; /* R: Info flags for returned setup */unsigned int msbits; /* R: used most significant bits */unsigned int rate_num; /* R: rate numerator */unsigned int rate_den; /* R: rate denominator */snd_pcm_uframes_t fifo_size; /* R: chip FIFO size in frames */unsigned char reserved[64]; /* reserved for future */
};
- flags:目前不知为何用,从源码上面看,跟限制规则有关
- masks:用于描述access、format、subformat这几个参数
- intervals:用于描述内核定义的所有interval参数.
- rmask:用户空间想要修改的参数bit集合。
- cmask:用户空间成功修改的参数bit集合。
- info: 表示硬件的一些参数,跟runtime的info一致,形如:SNDRV_PCM_INFO_XXX
- msbits: 有效位
- rate_num:采样率的分子
- rate_den:采样率的分母
- fifo_size:某些硬件的fifo大小
当应用通过ioctl,传递SNDRV_PCM_IOCTL_HW_PARAMS命令时,驱动根据snd_pcm_hw_params和限制规则(open的时候添加的规则)来决定硬件的一些特性。
当设置硬件参数时,pcm的hw_params回调会被触发.根据《usb声卡驱动(四)》可以知道应该在这个回调中,做一些硬件设置,包括buffer的分配。
接下来看看usb声卡驱动的这个回调,如下:
static int snd_usb_hw_params(struct snd_pcm_substream *substream,struct snd_pcm_hw_params *hw_params)
{struct snd_usb_substream *subs = substream->runtime->private_data;struct audioformat *fmt;int ret;//调动alsa提供的buffer分配函数//注意不要自己分配函数,因为该回调可能会被调用多次,如果自己修改需要注意内存的泄漏//同时,分配的内存需要在hw_free回调中进行释放ret = snd_pcm_lib_alloc_vmalloc_buffer(substream,params_buffer_bytes(hw_params));if (ret < 0)return ret;//设置各种硬件参数subs->pcm_format = params_format(hw_params);subs->period_bytes = params_period_bytes(hw_params);subs->period_frames = params_period_size(hw_params);subs->buffer_periods = params_periods(hw_params);subs->channels = params_channels(hw_params);subs->cur_rate = params_rate(hw_params);fmt = find_format(subs);if (!fmt) {dev_dbg(&subs->dev->dev,"cannot set format: format = %#x, rate = %d, channels = %d\n",subs->pcm_format, subs->cur_rate, subs->channels);return -EINVAL;}down_read(&subs->stream->chip->shutdown_rwsem);//如果正处于shutdown状态则返回,否则继续设置参数if (subs->stream->chip->shutdown)ret = -ENODEV;elseret = set_format(subs, fmt);up_read(&subs->stream->chip->shutdown_rwsem);if (ret < 0)return ret;//need_setup_ep表示要配置usb使用的端点//每一个端点在使用之前,都需要先设置参数,而每一次的参数修改,都需要重新配置usb端点subs->interface = fmt->iface;subs->altset_idx = fmt->altset_idx;subs->need_setup_ep = true;return 0;
}
上面的函数,都是对subs的赋值。其中需要注意的是snd_pcm_lib_alloc_vmalloc_buffer函数。
它分配这个substream的buffer。它的内容如下:
#define snd_pcm_lib_alloc_vmalloc_buffer(subs, size) \_snd_pcm_lib_alloc_vmalloc_buffer \(subs, size, GFP_KERNEL | __GFP_HIGHMEM | __GFP_ZERO)int _snd_pcm_lib_alloc_vmalloc_buffer(struct snd_pcm_substream *substream,size_t size, gfp_t gfp_flags)
{struct snd_pcm_runtime *runtime;if (PCM_RUNTIME_CHECK(substream))return -EINVAL;runtime = substream->runtime;//重复调用的情况下,释放上一次分配的内存if (runtime->dma_area) {if (runtime->dma_bytes >= size)return 0; /* already large enough */vfree(runtime->dma_area);}//将分配的内存起始地址保存在dma_area,后面有用处runtime->dma_area = __vmalloc(size, gfp_flags, PAGE_KERNEL);if (!runtime->dma_area)return -ENOMEM;//分配的内存的大小runtime->dma_bytes = size;return 1;
}
这个函数调用__vmalloc函数,在常规内存,或者高端内存中分配buffer,注意,这里面没有DMA区域。
分配完成之后,将地址,赋值给runtime的dma_area.
虽然,这里的名字叫dma_area,但它并没有用到dma内存。
总结:hw_params回调,进行进一步的初始化,并分配buffer
sw参数设置
设置完了硬件参数以后,接下来可以设置软件参数了。来看看软件参数的数据结构
struct snd_pcm_sw_params {int tstamp_mode; /* timestamp mode */unsigned int period_step;unsigned int sleep_min; /* min ticks to sleep */snd_pcm_uframes_t avail_min; /* min avail frames for wakeup */snd_pcm_uframes_t xfer_align; /* obsolete: xfer size need to be a multiple */snd_pcm_uframes_t start_threshold; /* min hw_avail frames for automatic start */snd_pcm_uframes_t stop_threshold; /* min avail frames for automatic stop */snd_pcm_uframes_t silence_threshold; /* min distance from noise for silence filling */snd_pcm_uframes_t silence_size; /* silence block size */snd_pcm_uframes_t boundary; /* pointers wrap point */unsigned int proto; /* protocol version */unsigned int tstamp_type; /* timestamp type (req. proto >= 2.0.12) */unsigned char reserved[56]; /* reserved for future */
};
- tstamp_mode:值为下面
enum {SNDRV_PCM_TSTAMP_NONE = 0,SNDRV_PCM_TSTAMP_ENABLE,SNDRV_PCM_TSTAMP_LAST = SNDRV_PCM_TSTAMP_ENABLE,
};
表示是否启用时间戳
2. period_step:未见其用处,不知道有何用,查看源码也没弄明白,暂时先放放
3. sleep_min:未见其用处
4. avail_min:最小可用帧。到达这个之后,唤醒等待中的线程
5. xfer_align:已过时,也未见其用处
6. start_threshold:开始运行的阈值。
7. stop_threshold:开始停止的阈值
8. silence_threshold:xrun状态下,使用静默数据填充的阈值
9. silence_size:silence数据块大小
10. boundary:buffer的边界
11. proto:protocol 版本
12. tstamp_type:时间戳类型,值为
enum {SNDRV_PCM_TSTAMP_TYPE_GETTIMEOFDAY = 0, /* gettimeofday equivalent */SNDRV_PCM_TSTAMP_TYPE_MONOTONIC, /* posix_clock_monotonic equivalent */SNDRV_PCM_TSTAMP_TYPE_MONOTONIC_RAW, /* monotonic_raw (no NTP) */SNDRV_PCM_TSTAMP_TYPE_LAST = SNDRV_PCM_TSTAMP_TYPE_MONOTONIC_RAW,
};
表示,runtime里面时间戳的模式。各个模式的定义,见上面的注释
应用程序使用ioctl,传递SNDRV_PCM_IOCTL_SW_PARAMS命令,将用户空间的参数,设置进驱动。驱动将这些值,设置到对应的substream的runtime里面。
这个设置并没有对应的回调会被触发。
参数相关的释放
在上面的参数设置中。可以看到有两个主要方向:
- 修改runtime中的各种各样的值
- 分配buffer
runtime中的值随,runtime的释放而释放。runtime的产生在open时刻,因此,runtime的释放,应该在release时刻,注意并不是close时刻。runtime是由alsa分配的,相应的释放也由alsa管理,我们不讨论其细节。
相应的buffer分配在hw_params时刻;那么buffer的释放就应该在hw_free时刻。这个buffer由usb声卡驱动分配,现在看一下它的释放细节,如下:
static int snd_usb_hw_free(struct snd_pcm_substream *substream)
{struct snd_usb_substream *subs = substream->runtime->private_data;subs->cur_audiofmt = NULL;subs->cur_rate = 0;subs->period_bytes = 0;down_read(&subs->stream->chip->shutdown_rwsem);if (!subs->stream->chip->shutdown) {//端点的引用计数减一。为0时,unlink所有的urbstop_endpoints(subs, true);snd_usb_endpoint_deactivate(subs->sync_endpoint);snd_usb_endpoint_deactivate(subs->data_endpoint);}up_read(&subs->stream->chip->shutdown_rwsem);//释放hw_params中分配的内存return snd_pcm_lib_free_vmalloc_buffer(substream);
}
主要的资源释放在snd_pcm_lib_free_vmalloc_buffer,即释放由snd_pcm_lib_alloc_vmalloc_buffer分配的buffer
至此,已经翻阅完了,pcm设备的参数设置和释放
接下来,应该进入一个关键的部分。以playback为例,先查看prepare。然后再查看开始,暂停,停止的操作。同时查看,端点的操作,以及数据传输情况。
下篇见
usb声卡驱动(六):usb声卡中的pcm打开和关闭相关推荐
- 七,USB设备驱动 - 分析USB储存驱动程序
前面学习了USB驱动的一些基础概念与重要的数据结构,那么究竟如何编写一个USB 驱动程序呢?编写与一个USB设备驱动程序的方法和其他总线驱动方式类似,驱动程序把驱动程序对象注册到USB子系统中,稍后再 ...
- USB 3G驱动和USB HOST驱动加载
********************************LoongEmbedded******************************** 作者:LoongEmbedded(kandi ...
- python中文件的打开与关闭_python中的文件打开与关闭操作命令介绍
python中的文件打开与关闭操作命令介绍 1.文件打开与关闭 在python,使用open函数,可以打开一个已经存在的文件,或者创建一个新文件 open(文件名,访问模式). f = open('t ...
- linux usb声卡驱动安装失败,声卡驱动安装出现错误?
声卡驱动安装出现错误? 发布时间:2007-12-03 08:49:07来源:红联作者:jerrya 之前一打开什么音频视频,Amarok啊,Xine啊,Kaffeine啊就跟我玩崩溃 昨晚上按照一个 ...
- Linux下的USB总线驱动(04)——USB键盘驱动 usbkbd.c
原文链接地址:http://www.linuxidc.com/Linux/2012-12/76197p9.htm 跟USB鼠标类型一样,USB键盘也属于HID类型,代码在/dirver/hid/usb ...
- linux内核添加usb键盘驱动,配置USB外设 - linux-2.6.32在mini2440开发板上移植_Linux编程_Linux公社-Linux系统门户网站...
linux-2.6.32在mini2440开发板上移植 配置USB外设 [日期:2013-04-08] 来源:Linux社区 作者:ssdsafsdsd [字体:大 中 小] 编者:因为LINUX内核 ...
- Linux下的USB总线驱动(03)——USB鼠标驱动 usbmouse.c
USB鼠标驱动 usbmouse.c 原文链接:http://www.linuxidc.com/Linux/2012-12/76197p7.htm drivers/hid/usbhid/usbmous ...
- linux系统查看usb转串口驱动,Linux usb转串口驱动
Linux USB转串口驱动程序: /* * USB Serial Converter driver */ #include #include #include #include #include # ...
- C语言中文件的打开和关闭
1.文件指针 文件在使用时都会在内存中开辟一个相应的文件信息区域来存放信息,这个信息保存在结构体变量中的. 标题 2.文件的打开和关闭 fopen打开文件 fclose关闭文件 #includ ...
最新文章
- Wiener Filter
- FPP(彩包)、COEM(简包)、MOLP(license授) 介绍
- Matlab符号运算总结
- 【CF#505B】Mr. Kitayuta's Colorful Graph (并查集或Floyd或BFS)
- python闹钟界面程序_「Python编程」自由管理时间之编写一个小闹钟起床
- 局域网内两台9303 的管理vlan mac地址冲突解决方法
- Jenkins的配置(rpm red hat方式)
- typescript之prototype
- Axure share 二三事
- 国内图像处理相关期刊
- openjudge 7920 统计单词数
- openEuler Summit | 江大勇:凝聚创新力量 逐梦数字时代星辰大海
- 最新zotero与obsidian笔记联动教程(可代替citations和mdnotes)
- 李珣同款爱心特效代码,加DIY教程,快拿去送给你喜欢的那个ta吧。
- Gradle下载超时
- 【数学】n次方差公式及证明方法
- 锤子,剪刀,布游戏的研究
- ernieSAGE:PGL at TextGraphs 2020 Shared Task: Explanation Regeneration using Language and Graph Lear
- PROFIBUS总线光纤模块在矿场设备的应用案例
- Pyke 简介 (4) :Pyke 是什么?
热门文章
- java基础应用程序超市收银_超市收银程序(JAVA课程设计 2011)
- 圆形体癣是什么样子的图片_身上长圆圈癣怎么办?
- 手机APP开发之MIT Appinventor详细实战教程(十三),云服务器的数据遇到的问题和解决的方法,以及网页客户端的详细使用方法,WIFI通信,数据获取在one net平台的相关应用
- 七大设计原则之接口隔离原则应用
- 苹果怎么取消微信订阅服务器,教程:取消微信免密支付授权功能设置
- 简单整系数滤波器去除心电信号的基线漂移
- 4.预测模型,马尔可夫链
- python 3.7如何安装pyqt4
- 智能合约--如何实现可升级的智能合约
- Debezium的MySQL连接器的工作原理