USB声卡驱动(四)

前面记录了usb相关的东西。

现在记录一下alsa相关的东西。

将Audio Function 放在一个硬件电路板里面,将这个电路板称为一个声卡。通过各种各样的接口(可能是非usb接口),连接到需要这个Audio Function的设备上。

所以在alsa的世界里,将一个Audio Function抽象成一个card对象。Audio Function里面的各种Unit和Terminal,抽象成Device对象也称为Component对象。

因此,在alsa的世界里,就是创建各种card对象,device对象,以及card对象的各种回调,device对象的各种回调。

又因为,本来就是为了学习驱动所用,实在不想把所有的device都走一遍,所以,特选取PCM device作为本系列笔记的一个中心。然后依此展开

card,device的创建与销毁

card对象作为整个Audio Function的抽象,那么它管理着这个card下面的所有的device,如PCM,mixers,MIDI,synthesizer等。还负责电源状态的管理,热插拔的管理。card对象的类型为:

struct snd_card ;

为了创建一个card对象,调用alsa提供的如下函数:

int snd_card_new(struct device * parent, int idx, const char * xid, struct module * module, int extra_size, struct snd_card ** card_ret);

对于card对象来说,它可以有自己的私有数据,私有数据的分配,也由上面的函数进行分配,传入私有数据所占空间的大小即可,即上面函数的extra_size

有了card对象之后,就是device对象的创建了。为了更加容易区分,下面将会使用component这个术语。

为了创建一个component对象,需要调用下面的函数:

int snd_device_new(struct snd_card * card, enum snd_device_type type, void * device_data, struct snd_device_ops * ops);

注意,注意:card对象的私有数据,是有snd_card_new函数内部分配,但component的私有数据,却需要自己手动分配

在上面函数的第二个参数,表示的是component的类型,这种分类是为了card在释放device时,选择合适的时机。component还可以表示虚拟的逻辑体,此时这个参数可以传递SNDRV_DEV_LOWLEVEL

card对象只是代表了Audio Function。在驱动中,常常需要一个对象来代表这个整个硬件卡,比如这个硬件卡使用的IO资源,中断资源等。我们给这个对象取个名字,叫Chip对象。

chip对象的管理通常有两种方法

第一种,放入card对象的私有数据中。即通过snd_card_new函数分配对应空间,然后就可以通过下面的方式来访问:

struct mychip *chip = card->private_data;

第二种,分配一个虚拟的component对象来代表这个chip对象。

大致步骤如下:

调用snd_card_new之后,调用kzalloc分配chip对象。

struct snd_card *card;
struct mychip *chip;
err = snd_card_new(&pci->dev, index[dev], id[dev], THIS_MODULE,0, &card);
.....
chip = kzalloc(sizeof(*chip), GFP_KERNEL);

chip对象持有card对象的指针

struct mychip {struct snd_card *card;....
};chip->card = card;

然后创建一个component对象,并将chip对象作为component的数据。

static struct snd_device_ops ops = {.dev_free =        snd_mychip_dev_free,
};
....
snd_device_new(card, SNDRV_DEV_LOWLEVEL, chip, &ops);

这样chip对象就可以通过component对象来访问了。

当一切准备就绪,就可以将card对象,注册到alsa中了。注册调用的函数为:

int snd_card_register(struct snd_card * card);

只有这个函数成功调用之后,才能被外部正确的访问。如果调用失败,需要调用

int snd_card_free(struct snd_card * card)

来释放card对象。而,card对象里面的component对象不需要单独去释放,card对象管理着这些component的释放。

component对象被释放时,它的snd_device_ops接口的dev_free会被调用,因此,可以在这个地方,释放分配给component对象的数据,如上面的第二个方法中的chip对象,可以在此处释放。

如果设备支持热插拔,可以调用

int snd_card_free_when_closed(struct snd_card * card)

该函数,会等到所有设备都关闭之后,再释放card对象

PCM——一种特殊的component对象

PCM由playback和capture流组成。而每个流可以由多个子流组成。某些设备支持多个播放功能,比如emu10k1具有32位双通道的playback PCM。每次打开时,就有一个substream会被选中并被打开。

如果只有一个子流,那么一旦成功打开之后,第二次打开要么被阻塞,要么返回一个错误吗。不过这些细节已经由alsa的中间层处理好了。

PCM是一种特殊的component对象,但是我们不用调用snd_device_new函数来创建这个对象,因为alsa提供了一个简便的函数来创建它。当然这个简便函数内部会调用snd_device_new。函数原型如下:

int snd_pcm_new(struct snd_card * card, const char * id, int device, int playback_count, int capture_count, struct snd_pcm ** rpcm)

注意,上面的第四和第五参数,分别表示playback子流的个数和capture子流的个数。如果支持多个playback或者capture,则可以在此处,传递对应的个数。然后在open/close回调中正确的处理。

当需要知道是哪一个子流时,可以使用传递给回调接口的struct snd_pcm_substream 来获取对应的子流的编号。如:

struct snd_pcm_substream *substream;
int index = substream->number;

pcm对象创建之后,就需要设置pcm对象的操作回调,如下:

snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK,&snd_mychip_playback_ops);
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE,&snd_mychip_capture_ops);

操作的定义,通常像下面这样:

static struct snd_pcm_ops snd_mychip_playback_ops = {.open =        snd_mychip_pcm_open,.close =       snd_mychip_pcm_close,.ioctl =       snd_pcm_lib_ioctl,.hw_params =   snd_mychip_pcm_hw_params,.hw_free =     snd_mychip_pcm_hw_free,.prepare =     snd_mychip_pcm_prepare,.trigger =     snd_mychip_pcm_trigger,.pointer =     snd_mychip_pcm_pointer,
};

每个操作的具体意义,后面详述。

创建了PCM,并设置了操作之后。还会有其他的操作,比如预先分配点缓存,见后面的缓存管理一节,还可以给PCM添加点其他信息,用于标明这个硬件的能力和功能,见后面的硬件描述一节。

现在我们来看看PCM的销毁。正如上面说的一样,component的释放是有card对象来管理。此处的PCM也一样,因为她也是component。

但,当PCM对象创建自己的私有的数据时,为了释放它,需要给pcm的private_free回调设置对应的释放函数,并在这个函数中,进行释放。

PCM信息的实时获取

在pcm的运行过程中,想要事实的获取相应的信息,则可以通过snd_pcm_runtime对象来获取。

这个对象在PCM子流打开时,被分配并赋值给子流。可以通过如下代码访问:

substream->runtime

这个对象保存了大量信息,比如,hw_params,sw_params,buffer指针,内存映射对象,自旋锁等。

这个对象里面的大部分都是只读,唯有硬件描述,DMA缓存信息,私有数据是可以被我们修改的。

如果你使用标准的缓存分配函数int snd_pcm_lib_malloc_pages(struct snd_pcm_substream * substream, size_t size),你也不必设置DMA缓存信息。

硬件描述

在PCM打开时,可以为PCM设置一些硬件的基本信息。

硬件的基本信息由snd_pcm_hardware数据结构保存。Runtime对象则复制了这个数据结构,因此,在runtime中修改,并不会影响pcm的硬件信息。

比如,某个硬件,处在某个模式下,只有一个声道。此时无需再次创建一个snd_pcm_hardware对象,只需要在运行时,修改以前的snd_pcm_hardware对象即可。

通常情况下,硬件描述如下:

static struct snd_pcm_hardware snd_mychip_playback_hw = {.info = (SNDRV_PCM_INFO_MMAP |SNDRV_PCM_INFO_INTERLEAVED |SNDRV_PCM_INFO_BLOCK_TRANSFER |SNDRV_PCM_INFO_MMAP_VALID),.formats =          SNDRV_PCM_FMTBIT_S16_LE,.rates =            SNDRV_PCM_RATE_8000_48000,.rate_min =         8000,.rate_max =         48000,.channels_min =     2,.channels_max =     2,.buffer_bytes_max = 32768,.period_bytes_min = 4096,.period_bytes_max = 32768,.periods_min =      1,.periods_max =      1024,
};
  1. info:标明了PCM的类型和能力。至少需要指明,是否支持mmap,以及支持的是哪一种交错格式。当支持mmap时,添加SNDRV_PCM_INFO_MMAP 。如果支持交错格式,或者非交错格式,则分别增加SNDRV_PCM_INFO_INTERLEAVED 和SNDRV_PCM_INFO_NONINTERLEAVED 。如果都支持,则两个都增加。
    SNDRV_PCM_INFO_PAUSE ,SNDRV_PCM_INFO_RESUME,分别表示PCM支持“pause”,支持“suspend/resume”。当支持这些时,相应的PCM回调方法必须要支持。

  2. formats:支持的格式

  3. rates:支持的速录。如果支持连续的速率,需要传递CONTINUOUS位。如果支持的速率,没有被定义,需要增加KNOT位,然后手动设置硬件限制(见后文)

  4. rate_min,rate_max:最小,最大采样率

  5. channel_min和channe_max:最小,最大通道数

  6. buffer_bytes_max:最大buffer大小,单位字节。没有buffer_bytes_min,因为可以从最小周期大小和最小周期数计算得来。period_bytes_min,period_bytes_max分别邓毅了最小,最大周期大小。periods_min,period_max,定义了最小,最大周期数

注意:周期这个术语(period):它表示一次PCM中断产生的大小,该大小强烈依赖于硬件。通常,越小的周期大小中断越多。在capture中,这个大小,还决定了输入延迟。换句话说,整个buffer大小决定了输出延迟。

Runtime中经常使用的并不是硬件信息,而是来自于用户的配置信息,这些信息一部分来自于硬件参数,一部分来自于软件参数。需要注意的一件事是:在runtime中配置的buffer和period大小,是以帧作为单位的。它表示的是所有通道的一次采样。

1 frame = channels 乘以 samples-size

为了在帧和字节之间进行转换,定义了两个函数:

frames_to_bytes() ;
bytes_to_frames() ;

DMA缓存信息

dma缓存信息,由下面四个字段定义:

dma_area, dma_addr, dma_bytes , dma_private
  1. dma_area:buffer指针,可以使用memcpy函数来复制进/出
  2. dma_addr:buffer物理地址,仅当buffer是线性buffer时,这个值才会被指定
  3. dma_bytes:buffer大小,单位字节
  4. dma_private:给ALSA DMA 分配器使用的

如果使用了标准的缓存分配函数snd_pcm_lib_malloc_pages,则上面的这些字段,由ALSA自动设置好,并且不应该改变他们。换句话说,如果你想自己手动分配buffer,那么就需要在hw_params回调中,管理这些字段的值。

dma_bytes是强制需要的。

如果buffer被映射了,那么dma_area是必须的。如果驱动不支持mmap,这个字段则不是必须的。

dma_addr也是可选的

dam_private,也是可选的。

运行状态

运行状态可以通过runtime->status引用,它是一个指向 struct snd_pcm_mmap_status的指针。例如,你可以通过下面的代码,获取当前DMA硬件指针

runtime->status->hw_ptr

DMA应用指针可以通过runtime->control来引用,这个指针指向 struct snd_pcm_mmap_control。但是不推荐直接使用这个指针。

私有数据

Runtime也可以有自己的私有数据,它可以在open时创建,在close时销毁。注意,注意,这个是子流的私有数据,不是pcm的私有数据。通常情况下pcm的私有数据,指向了chip对象。

PCM的回调操作

通常,pcm的回调,成功返回0。否则返回一个负数。每个回调都会有一个参数:struct snd_pcm_substream 指针。为了获取chip对象,可以使用如下的代码:

struct mychip *chip = snd_pcm_substream_chip(substream);

上面的宏,直接读取substream->private_data,它是pcm->private_data的一个拷贝。

open

当pcm打开一个子流时,回调此函数。在这个函数里面,你至少需要初始化Runtime对象的hw。如下:

static int snd_xxx_open(struct snd_pcm_substream *substream)
{struct mychip *chip = snd_pcm_substream_chip(substream);struct snd_pcm_runtime *runtime = substream->runtime;runtime->hw = snd_mychip_playback_hw;return 0;
}

上面的snd_mychip_playback_hw就是预先定义的硬件描述。你还可以在这个地方为这个子流分配一个私有数据。

close

当pcm关闭一个子流时,回调此函数,上面open中创建的私有数据,需要在这个地方释放

ioctl

这是为pcm的ioctl调用而设置的回调。通常你可以直接将调用逻辑转交给通用的处理函数

snd_pcm_lib_ioctl()

hw_params

当应用程序设置硬件参数时,这个回调被调用。许多硬件设置都应该在这个回调中完成,包括buffer的分配。

初始化参数可以使用宏

params_xxx()

分配buffer,可以调用辅助函数

snd_pcm_lib_malloc_pages(substream, params_buffer_bytes(hw_params));

这个函数可用,有一个前提,那就是DMA缓冲已经被分配。详见缓冲类型小节

注意:这个回调和prepare回调,在每次初始化时,可以被调用多次。例如在OSS模拟中,每次通过ioctl改变参数时,这个回调都会被调用

因此,你需要非常小心,不要重复分配buffer。但是上面的辅助函数可以重复调用多次,因为,它会释放上一次分配的buffer。

另外需要注意的是,这个回调是非原子的。

hw_free

释放由hw_params回调分配的资源。例如释放snd_pcm_lib_malloc_pages分配的buffer。

snd_pcm_lib_free_pages(substream);

这个函数,总是在close回调之前被调用,同样的这个函数,也可能被多次调用。所以需要注意,不要多次释放已经释放的资源

prepare

当pcm准备好之后,回调这个函数。可以在此处设置格式类型,采样速率等等。

跟hw_params回调不同的是:每当从underrun状态恢复等,而调用snd_pcm_prepare时,prepare回调都会被调用。

注意,这个回调也是非原子的,可以在这个函数里面安全的使用调度相关的函数

在这个和下面的回调中,你可以使用runtime对象了。例如,获取当前的采样率,格式,通道数等等。

注意:该回调也会可能被多次调用

trigger

当pcm有start,stop,pause时,回调此函数。

对应的动作,则由该回调的第二个参数传进来。该回调,至少需要定义START和STOP动作。

如下:

switch (cmd) {case SNDRV_PCM_TRIGGER_START:/* do something to start the PCM engine */break;
case SNDRV_PCM_TRIGGER_STOP:/* do something to stop the PCM engine */break;
default:return -EINVAL;
}

当pcm支持pause操作时,PAUSE_PUSH和PAUSE_RELEASE动作,也必须在这个回调中支持。前者暂停pcm,后者重新开始pcm

当pcm支持suspend/resume操作时,不管是完全还是部分的suspend/resume,SUSPEND和RESUME动作,也必须在这个回调中支持。这些动作依电源状态而改变。显然,SUSPEND和RESUME分别表示挂起和重新开始pcm。通常这两个动作完全等同于STOP和START命令。

需要注意的是,这个回调是原子的。因此,不能在这里边进行休眠,trigger回调也应该尽可能的短,比如,仅激活DMA即可。其他的初始化工作应该在前面的hw_params和prepare回调中完成。

pointer

当alsa中间层,需要知道硬件buffer的当前位置时,这个回调被调用。位置的返回单位为帧,范围为0到buffer_size-1

这个回调通常是因为alsa中间层的buffer更新逻辑,而中断中调用的snd_pcm_period_elapsed函数会触发buffer的更新逻辑。

pcm的中间层更新位置,然后计算可用的空间,并唤醒等待的线程等。

这个函数也是原子的。

copy_user,copy_kernel,fill_silence

这些回调不是强制的,在大多数情况下,可以被省略。当硬件buffer不能使用常用的内存空间时,这些回调才会被使用。

某些声卡有自己的buffer,并且无法进行映射,此时,就不得不手动将数据从内存buffer传送到硬件buffer中。

另外,如果在物理和虚拟空间上,这些buffer都不是连续的,这些回调也可以被用上。

ack

该回调也不是强制性的。当appl_ptr在读写操作中被更新时,这个函数被回调。某些驱动需要跟踪内部buffer的appl_ptr,则可以使用这个回调

page

这个回调也是可选的。该回调主要是为了非连续buffer而用。见后面的缓存管理

注意:内存管理的部分细节描述,可以参考《usb声卡驱动(八)》

限制

如果你的声卡支持的是非标的采样率,你需要为这种条件设置一些限制。

例如,为了将采样率,限制在支持的值上,使用snd_pcm_hw_constraint_list().在open回调中,调用这个函数,如下:

static unsigned int rates[] ={4000, 10000, 22050, 44100};
static struct snd_pcm_hw_constraint_list constraints_rates = {.count = ARRAY_SIZE(rates),.list = rates,.mask = 0,
};static int snd_mychip_pcm_open(struct snd_pcm_substream *substream)
{int err;....err = snd_pcm_hw_constraint_list(substream->runtime, 0,SNDRV_PCM_HW_PARAM_RATE,&constraints_rates);if (err < 0)return err;....
}

alsa中可以有许多不同种类的限制。在asound/pcm.h文件中查看完整的列表。甚至可以定义自己的限制规则。例如,my_chip仅当S16_LE格式时,只有一个通道。然而,其他格式,可以有多个通道。此时,可以创建一个如下的规则:

static int hw_rule_channels_by_format(struct snd_pcm_hw_params *params,struct snd_pcm_hw_rule *rule)
{struct snd_interval *c = hw_param_interval(params,SNDRV_PCM_HW_PARAM_CHANNELS);struct snd_mask *f = hw_param_mask(params, SNDRV_PCM_HW_PARAM_FORMAT);struct snd_interval ch;snd_interval_any(&ch);if (f->bits[0] == SNDRV_PCM_FMTBIT_S16_LE) {ch.min = ch.max = 1;ch.integer = 1;return snd_interval_refine(c, &ch);}return 0;
}

然后调用函数,来添加规则

snd_pcm_hw_rule_add(substream->runtime, 0, SNDRV_PCM_HW_PARAM_CHANNELS,hw_rule_channels_by_format, NULL,SNDRV_PCM_HW_PARAM_FORMAT, -1);

当应用设置PCM格式时,这个规则函数就会被调用,它会根据规则改变通道数。但应用也可能在设置格式之前设置通道数,因此,还需要定义反向规则。

static int hw_rule_format_by_channels(struct snd_pcm_hw_params *params,struct snd_pcm_hw_rule *rule)
{struct snd_interval *c = hw_param_interval(params,SNDRV_PCM_HW_PARAM_CHANNELS);struct snd_mask *f = hw_param_mask(params, SNDRV_PCM_HW_PARAM_FORMAT);struct snd_mask fmt;snd_mask_any(&fmt);    /* Init the struct */if (c->min < 2) {fmt.bits[0] &= SNDRV_PCM_FMTBIT_S16_LE;return snd_mask_refine(f, &fmt);}return 0;
}

然后在open回调中,添加这个规则

snd_pcm_hw_rule_add(substream->runtime, 0, SNDRV_PCM_HW_PARAM_FORMAT,hw_rule_format_by_channels, NULL,SNDRV_PCM_HW_PARAM_CHANNELS, -1);

缓存管理

终于到了让人激动的内容了。

依赖总线和架构,alsa提供了几个分配buffer的函数。他们有相似的API,如果物理地址连续,可以通过snd_malloc_xxx_pages()函数进行分配。这里的xxx是总线类型。

snd_malloc_xxx_pages_fallback()函数,尝试分配指定的页,如果页不可用,则减少页的大小,直到足够的空间被分配。

释放页可以调用snd_free_xxx_pages()

通常,alsa在模块加载时,会分配一个较大的连续的物理,以供后续的使用,这叫做预分配。可以调用下面的函数,在PCM实例构造时,进行预分配

snd_pcm_lib_preallocate_pages_for_all(pcm, SNDRV_DMA_TYPE_DEV,snd_dma_pci_data(pci), size, max);

此处,size是预分配的大小。max是可以通过prealloc的proc文件进行更改的最大大小

第二个参数,和第三个参数,依赖于总线。在ISA总线下,则分别传递SNDRV_DMA_TYPE_DEV和snd_dma_isa_data()。

如果连续buffer不依赖于总线,则可以传递SNDRV_DMA_TYPE_CONTINUE和snd_dma_continuous_data(GFP_KERNEL).

如果是PCI的 scatter-gather buffer。则传递,SNDRV_DMA_TYPE_DEV_SG和snd_dma_pci_data(pci)

一旦buffer被预分配,则可以在hw_params回调中使用下面的函数

snd_pcm_lib_malloc_pages(substream, size);

外部硬件buffer

某些硬件有自己的buffer,且DMA无法进行传输。

要么,1)直接复制或者设置数据到外部buffer。要么2)构建一个内部buffer,再每次中断时,从这个内部buffer将数据复制或者设置到外部buffer。

如果外部buffer比较大时,第一种情况工作还OK。该方法不需要额外的buffer,因此比较高效。此时,只需要定义copy_user和copy_kernel回调来进行数据传输即可。定义fill_silence回调来进行playback但它有缺点,那就是无法进行映射。

第二种情况,可以支持mmap。

还有一些情况是,声卡使用了PCI 内存空间。此时,内存映射只在部分架构上可用,如intel架构。对于无法映射的架构,依然还需要定义copy_user,copy_kernel和fill_silence回调。

Vmalloc buffer

可能会使用由vmalloc()分配的buffer,例如,上面提到的内部buffer。由于分配的页不是连续的,因此需要设置page回调以获取每个偏移处的物理地址。

page回调的实现如下:

#include <linux/vmalloc.h>/* get the physical page pointer on the given offset */
static struct page *mychip_page(struct snd_pcm_substream *substream,unsigned long offset)
{void *pageptr = substream->runtime->dma_area + offset;return vmalloc_to_page(pageptr);
}

注意:部分内存的描述可以参考《usb声卡驱动(八)》

本篇关于alsa声卡驱动的引子到此为止。接下就是直接分析android的usb声卡驱动了。

USB声卡驱动(四):alsa概述相关推荐

  1. usb声卡驱动(六):usb声卡中的pcm打开和关闭

    usb声卡驱动(六) 前面记录了usb声卡驱动的注册过程. 下面,查看usb声卡里面pcm的打开和关闭,都做了什么工作. 一点基础前提 因为本系列文章的核心是,usb声卡驱动.所以并不会深入到alsa ...

  2. usb声卡驱动(一):USB描述符

    usb声卡驱动(一) 前面看了内核的启动,接下来就是驱动的学习. 正好手边有一个USB声卡,就准备以此为基础,进行usb声卡驱动的学习. 因此,在学些usb声卡之前,先看看usb驱动.然后再是alsa ...

  3. usb声卡驱动_iCON ProDrive第三代USB声卡驱动全新发布!

    2019年3月,iCON官方发布了一款全球首创--全新一代"ProDrive III"USB声卡驱动,iCON所有系列USB声卡(包括带声卡的MIDI键盘)已全面启用ProDriv ...

  4. USB声卡驱动(二):USB音频设备描述符

    USB声卡驱动(二)USB音频设备描述符 本篇笔记,分两部分,第一部分,是基本知识的记录.第二部分是一个实际的例子. 一.基本知识 一个音频设备(Audio Device)含有多个音频功能(Audio ...

  5. 万能声卡驱动(Alsa)的安装方法

    最近安装rh8.0,声卡是inter主板自带的AC'97声卡,没有linux驱动,经过一番折腾,终于搞定了,现在把经验分享给大家,祝linux下没有声音的朋友开心.         运行sndconf ...

  6. usb声卡驱动_来自MOTU的温馨提示:如果你的声卡在Windows系统下不稳定,你可以这样做!...

    武汉大学来自MOTU的温馨提示:如果你的声卡在Windows系统下不稳定,你可以这样做! 在我们日常使用外置声卡时,特别是USB接口的外置声卡,相信绝大多数朋友都遇到过声卡掉线.卡死.无故停止工作等各 ...

  7. Linux的声卡驱动中ALSA与OSS的区别和简单流程介

    在声卡的驱动中一种是OSS(开放声音系统),一种是ALSA(先进Linux声音架构).OSS是一个商业声卡驱动程序,需要花钱购买.一般我们现在使用的是ALSA的声音架构. Advanced Linux ...

  8. linux usb声卡驱动安装失败,声卡驱动安装出现错误?

    声卡驱动安装出现错误? 发布时间:2007-12-03 08:49:07来源:红联作者:jerrya 之前一打开什么音频视频,Amarok啊,Xine啊,Kaffeine啊就跟我玩崩溃 昨晚上按照一个 ...

  9. ALSA声卡驱动四之Control设备的创建

    Control接口 Control接口主要让用户空间的应用程序(alsa-lib)可以访问和控制音频codec芯片中的多路开关,滑动控件等.对于Mixer(混音)来说,Control接口显得尤为重要, ...

最新文章

  1. Traveller项目介绍
  2. iis 无法连接mysql_远程无法连接SQL2000及MySQL的原因和解决办法
  3. 使用jpmml-sparkml-executable生成PMML模型文件
  4. 《Python游戏编程快速上手》第十章TicTacToe
  5. 简述C# XML解析方法的特点及应用
  6. 主流mes厂商_工业软件:一文讲透国内外MES/MOM市场格局,主流厂商及其优势行业...
  7. Android 6.0权限问题
  8. OpenCv之图像形态学(笔记08)
  9. 解决 “Microsoft.Jet.Oledb.4.0 找不到提供者 或 未正确安装” 的方法
  10. 大腾讯的第一个开源项目「Tinker」
  11. I,P,B帧和PTS,DTS的关系,GOP相关
  12. python计算最大公约数函数_python如何分享解两数的最大公约数 python代码 最大公约和最小公倍数数计算?...
  13. jquery开发手册(详细全面)
  14. 基于Android的英文电子词典
  15. pe显示linux分区文件,找到了linux分区顺序错乱修复方法
  16. XDOJ 回文数 C语言
  17. JavaJUC基础知识梳理
  18. 【七夕节特刊】开源世界里的爱情保卫战
  19. PyQt5 基本语法(一):基类控件
  20. 字典遍历时不能修改字典元素

热门文章

  1. 菊花厂笔试面试备战(二)
  2. 雷军说芯片是手机科技的制高点,也是让小米成为伟大公司的核心技术
  3. 关于国庆节期间学习会员收益延期发放通知
  4. Highcharts实现饼图pie
  5. go五笔——基于Google在线五笔制作
  6. 联想官方正式版---Lenovo OEM Windows 7 Home Basic(EM)32BIT系统恢复盘
  7. Anti-Rootkit(ARK)内核级系统防护软件KsBinSword的设计与实现
  8. 从业3年45万年薪的AI训练师是如何养成的?
  9. RF入门:robotframwork的WEB功能测试—切换window窗口
  10. 常用获取日期相关方法