首先介绍HID:
HID是Human Interface Devices的缩写.翻译成中文即为人机交互设备.这里的人机交互设备是一个宏观上面的概念,任何设备,只要符合HID spec,都可以称之为HID设备

在make menuconfig中,选中USB Human Interface Device(full HID) support。则所有USB HID都会被驱动,其中包括USB Mouse。
在drivers/hid/usbhid/Kconfig看到这项对应的为: CONFIG_USB_HID
又在drivers/hid/usbhid/Makefile中看到: obj-$(CONFIG_USB_HID) += usbhid.o

换句话说,如果选中built-in.则 obj-y中加入usbhid.o

如果将这一项选中为M(module),则obj-m中加入usbhid.o

也就是说:如果选中USB Human Interface Device(full HID) support为built-in.则usbhid.o会被built-in。
再在drivers/hid/usbhid/.usbhid.o.cmd中看到usbhid.o其实是由:hid-core.o,hid-quirks.o,hiddev.o组成的。
也就是说:当在Menuconfig中选中那一项后,这三个.o都会被built-in.

下一篇从driver角度学习。

在drivers/hid/usbhid/hid-core.c中,有如下语句:
module_init(hid_init);
表明当hid-usb.o(hid-core.o等三个组成)添加入kernel core时,会调用hid_init.

1. hid_init分析:
hid_init首先调用usbhid_quirks_init();
1.1. usbhid_quirks_init() 解析:
其实就是查找insmod 时给的pid,vid参数在quirks列表中是否有,如果有,就替换。没有就创建。

1.2. hiddev_init();
此function只有在选中CONFIG_USB_HIDDEV才会真正做事。
也就是说:只有在配置kernel时选中下面条目才有效.
config USB_HIDDEV
bool "/dev/hiddev raw HID device support"
它只是简单的注册一个USB设备。但这个设备在USB 硬件插入时什么都不作。

1.3 usb_register(&hid_driver);
注册一个USB driver.

从这个driver的id_table来看:

.match_flags = USB_DEVICE_ID_MATCH_INT_CLASS

表明匹配的是:Interface class

.bInterfaceClass = USB_INTERFACE_CLASS_HID

表明Interface Class为HID设备,则会被唤醒。

2. HID USB设备被插入时的状况:
分析hid_driver->probe
第一个参数为USB Core传过来的USB设备Interface。第二个参数为本driver的id_table.

2.1 usb_hid_configure(intf);

首先查看quirks.使用usbhid_lookup_quirk()从静态和动态quirks list中查是否此device包含在其中。

Sam看HID driver是以mouse为线索,

interface->desc.bInterfaceSubClass=USB_CLASS_HID

interface->desc.bInterfaceSubClass == USB_INTERFACE_SUBCLASS_BOOT

interface->desc.bInterfaceProtocol == USB_INTERFACE_PROTOCOL_MOUSE

所以:quirks |= HID_QUIRK_NOGET;

背景知识一:模块参数:
当使用insmod或modprobe安装模块时,可以通过模块参数给模块传递一些数值。这增加了模块的灵活性。但在使用模块参数之前,必须要让这些参数对insmod可见,则可以使用如下方式,让insmod知道参数名:
module_param_named(name, value, type, perm)
name是参数的名称(insmod时使用)
value是参数在模块中对应的变量
type是参数的类型
perm是参数的权限(其实就是/sys/module/[module_name]/parameters的权限)

例如:
int disk_size = 1024;
module_param_named(size, disk_size, int, S_IRUGO);
则给模块加上名称为"size"的参数,如果在加载模块是使用
insmod thismodule size=100,
那么在模块代码中disk_size的值就是100。相反,如果加载模块时没有指定参数,那么模块代码中disk_size的值仍是默认的1024。
注意,所有模块参数,都应该给定一个默认值。

MODULE_PARM_DESC(),对模块参数的描述。

背景知识二:模块宏:

MODULE_AUTHOR();标明模块拥有者

MODULE_DESCRIPTION(); module描述

MODULE_LICENSE(); module license.如果没有,insmod时会警告

1. 解读hid device probe程序:

static int hid_probe(struct usb_interface *intf, const struct usb_device_id *id)
{
 struct hid_device *hid;
 char path[64];
 int i;
 char *c;

dbg_hid("HID probe called for ifnum %d\n",
   intf->altsetting->desc.bInterfaceNumber);

if (!(hid = usb_hid_configure(intf)))
  return -ENODEV;

usbhid_init_reports(hid);
 hid_dump_device(hid);
 if (hid->quirks & HID_QUIRK_RESET_LEDS)
  usbhid_set_leds(hid);

if (!hidinput_connect(hid))
  hid->claimed |= HID_CLAIMED_INPUT;
 if (!hiddev_connect(hid))
  hid->claimed |= HID_CLAIMED_HIDDEV;

usb_set_intfdata(intf, hid);

if (!hid->claimed) {
  printk ("HID device not claimed by input or hiddev\n");
  hid_disconnect(intf);
  return -ENODEV;
 }

if ((hid->claimed & HID_CLAIMED_INPUT))
  hid_ff_init(hid);

if (hid->quirks & HID_QUIRK_SONY_PS3_CONTROLLER)
  hid_fixup_sony_ps3_controller(interface_to_usbdev(intf),
   intf->cur_altsetting->desc.bInterfaceNumber);

printk(KERN_INFO);

if (hid->claimed & HID_CLAIMED_INPUT)
  printk("input");
 if (hid->claimed == (HID_CLAIMED_INPUT | HID_CLAIMED_HIDDEV))
  printk(",");
 if (hid->claimed & HID_CLAIMED_HIDDEV)
  printk("hiddev%d", hid->minor);

c = "Device";
 for (i = 0; i < hid->maxcollection; i++) {
  if (hid->collection[i].type == HID_COLLECTION_APPLICATION &&
      (hid->collection[i].usage & HID_USAGE_PAGE) == HID_UP_GENDESK &&
      (hid->collection[i].usage & 0xffff) < ARRAY_SIZE(hid_types)) {
   c = hid_types[hid->collection[i].usage & 0xffff];
   break;
  }
 }

usb_make_path(interface_to_usbdev(intf), path, 63);

printk(": USB HID v%x.%02x %s [%s] on %s\n",
  hid->version >> 8, hid->version & 0xff, c, hid->name, path);

return 0;
}

1.1: usb_hid_configure(intf)解读:

从字面来看,它是指配置hid。

static struct hid_device *usb_hid_configure(struct usb_interface *intf)
{
 struct usb_host_interface *interface = intf->cur_altsetting;
 struct usb_device *dev = interface_to_usbdev (intf);
 struct hid_descriptor *hdesc;
 struct hid_device *hid;
 u32 quirks = 0;
 unsigned rsize = 0;
 char *rdesc;
 int n, len, insize = 0;
 struct usbhid_device *usbhid;

//得到对应vid,pid的quriks.如果没有,则返回0
 quirks = usbhid_lookup_quirk(le16_to_cpu(dev->descriptor.idVendor),
   le16_to_cpu(dev->descriptor.idProduct));

//如果为boot设备且为keyboard或mouse.则quirks=HID_QUIRK_NOGET

if (interface->desc.bInterfaceSubClass == USB_INTERFACE_SUBCLASS_BOOT) {
  if (interface->desc.bInterfaceProtocol == USB_INTERFACE_PROTOCOL_KEYBOARD ||
   interface->desc.bInterfaceProtocol == USB_INTERFACE_PROTOCOL_MOUSE)
    quirks |= HID_QUIRK_NOGET;
 }

//如果quirks显示要忽略,则退出probe

if (quirks & HID_QUIRK_IGNORE)
  return NULL;

//HID_QUIRK_IGNORE_MOUSE表示如果为mouse,则忽略。

if ((quirks & HID_QUIRK_IGNORE_MOUSE) &&
  (interface->desc.bInterfaceProtocol == USB_INTERFACE_PROTOCOL_MOUSE))
   return NULL;

//如果interface扩展描述符中没有类型为HID_DT_HID条目,或者interface包含的endpoint数目为0,又或者interface endpoint中扩展描述符中没有类型为HID_DT_HID的条目。则退出。
 if (usb_get_extra_descriptor(interface, HID_DT_HID, &hdesc) &&
     (!interface->desc.bNumEndpoints ||
      usb_get_extra_descriptor(&interface->endpoint[0], HID_DT_HID, &hdesc))) {
  dbg_hid("class descriptor not present\n");
  return NULL;
 }

//得到描述符长度:

for (n = 0; n < hdesc->bNumDescriptors; n++)
  if (hdesc->desc[n].bDescriptorType == HID_DT_REPORT)
   rsize = le16_to_cpu(hdesc->desc[n].wDescriptorLength);

//如果描述符长度不对,则退出

if (!rsize || rsize > HID_MAX_DESCRIPTOR_SIZE) {
  dbg_hid("weird size of report descriptor (%u)\n", rsize);
  return NULL;
 }

//创建此长度内存空间

if (!(rdesc = kmalloc(rsize, GFP_KERNEL))) {
  dbg_hid("couldn't allocate rdesc memory\n");
  return NULL;
 }

//向dev的endpoint发送HID_REQ_SET_IDLE request.

hid_set_idle(dev, interface->desc.bInterfaceNumber, 0, 0);

//取得report description的详细信息,放到rdesc中。

if ((n = hid_get_class_descriptor(dev, interface->desc.bInterfaceNumber, HID_DT_REPORT, rdesc, rsize)) < 0) {
  dbg_hid("reading report descriptor failed\n");
  kfree(rdesc);
  return NULL;
 }

usbhid_fixup_report_descriptor(le16_to_cpu(dev->descriptor.idVendor),
   le16_to_cpu(dev->descriptor.idProduct), rdesc,
   rsize, rdesc_quirks_param);

dbg_hid("report descriptor (size %u, read %d) = ", rsize, n);
 for (n = 0; n < rsize; n++)
  dbg_hid_line(" %02x", (unsigned char) rdesc[n]);
 dbg_hid_line("\n");

//解析report。并建立hid_device.返回给hid.

if (!(hid = hid_parse_report(rdesc, n))) {
  dbg_hid("parsing report descriptor failed\n");
  kfree(rdesc);
  return NULL;
 }

kfree(rdesc);
 hid->quirks = quirks;

if (!(usbhid = kzalloc(sizeof(struct usbhid_device), GFP_KERNEL)))
  goto fail_no_usbhid;

hid->driver_data = usbhid;
 usbhid->hid = hid;

usbhid->bufsize = HID_MIN_BUFFER_SIZE;
 hid_find_max_report(hid, HID_INPUT_REPORT, &usbhid->bufsize);
 hid_find_max_report(hid, HID_OUTPUT_REPORT, &usbhid->bufsize);
 hid_find_max_report(hid, HID_FEATURE_REPORT, &usbhid->bufsize);

if (usbhid->bufsize > HID_MAX_BUFFER_SIZE)
  usbhid->bufsize = HID_MAX_BUFFER_SIZE;

hid_find_max_report(hid, HID_INPUT_REPORT, &insize);

if (insize > HID_MAX_BUFFER_SIZE)
  insize = HID_MAX_BUFFER_SIZE;

if (hid_alloc_buffers(dev, hid)) {
  hid_free_buffers(dev, hid);
  goto fail;
 }

for (n = 0; n < interface->desc.bNumEndpoints; n++) {

struct usb_endpoint_descriptor *endpoint;
  int pipe;
  int interval;

endpoint = &interface->endpoint[n].desc;
  if ((endpoint->bmAttributes & 3) != 3)  
   continue;

interval = endpoint->bInterval;

if (hid->collection->usage == HID_GD_MOUSE && hid_mousepoll_interval > 0)
   interval = hid_mousepoll_interval;

if (usb_endpoint_dir_in(endpoint)) {
   if (usbhid->urbin)
    continue;
   if (!(usbhid->urbin = usb_alloc_urb(0, GFP_KERNEL)))
    goto fail;
   pipe = usb_rcvintpipe(dev, endpoint->bEndpointAddress);
   usb_fill_int_urb(usbhid->urbin, dev, pipe, usbhid->inbuf, insize,
      hid_irq_in, hid, interval);
   usbhid->urbin->transfer_dma = usbhid->inbuf_dma;
   usbhid->urbin->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
  } else {
   if (usbhid->urbout)
    continue;
   if (!(usbhid->urbout = usb_alloc_urb(0, GFP_KERNEL)))
    goto fail;
   pipe = usb_sndintpipe(dev, endpoint->bEndpointAddress);
   usb_fill_int_urb(usbhid->urbout, dev, pipe, usbhid->outbuf, 0,
      hid_irq_out, hid, interval);
   usbhid->urbout->transfer_dma = usbhid->outbuf_dma;
   usbhid->urbout->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
  }
 }

if (!usbhid->urbin) {
  err_hid("couldn't find an input interrupt endpoint");
  goto fail;
 }

init_waitqueue_head(&hid->wait);

INIT_WORK(&usbhid->reset_work, hid_reset);
 setup_timer(&usbhid->io_retry, hid_retry_timeout, (unsigned long) hid);

spin_lock_init(&usbhid->inlock);
 spin_lock_init(&usbhid->outlock);
 spin_lock_init(&usbhid->ctrllock);

hid->version = le16_to_cpu(hdesc->bcdHID);
 hid->country = hdesc->bCountryCode;
 hid->dev = &intf->dev;
 usbhid->intf = intf;
 usbhid->ifnum = interface->desc.bInterfaceNumber;

hid->name[0] = 0;

if (dev->manufacturer)
  strlcpy(hid->name, dev->manufacturer, sizeof(hid->name));

if (dev->product) {
  if (dev->manufacturer)
   strlcat(hid->name, " ", sizeof(hid->name));
  strlcat(hid->name, dev->product, sizeof(hid->name));
 }

if (!strlen(hid->name))
  snprintf(hid->name, sizeof(hid->name), "HID %04x:%04x",
    le16_to_cpu(dev->descriptor.idVendor),
    le16_to_cpu(dev->descriptor.idProduct));

hid->bus = BUS_USB;
 hid->vendor = le16_to_cpu(dev->descriptor.idVendor);
 hid->product = le16_to_cpu(dev->descriptor.idProduct);

usb_make_path(dev, hid->phys, sizeof(hid->phys));
 strlcat(hid->phys, "/input", sizeof(hid->phys));
 len = strlen(hid->phys);
 if (len < sizeof(hid->phys) - 1)
  snprintf(hid->phys + len, sizeof(hid->phys) - len,
    "%d", intf->altsetting[0].desc.bInterfaceNumber);

if (usb_string(dev, dev->descriptor.iSerialNumber, hid->uniq, 64) <= 0)
  hid->uniq[0] = 0;

usbhid->urbctrl = usb_alloc_urb(0, GFP_KERNEL);
 if (!usbhid->urbctrl)
  goto fail;

usb_fill_control_urb(usbhid->urbctrl, dev, 0, (void *) usbhid->cr,
        usbhid->ctrlbuf, 1, hid_ctrl, hid);
 usbhid->urbctrl->setup_dma = usbhid->cr_dma;
 usbhid->urbctrl->transfer_dma = usbhid->ctrlbuf_dma;
 usbhid->urbctrl->transfer_flags |= (URB_NO_TRANSFER_DMA_MAP | URB_NO_SETUP_DMA_MAP);
 hid->hidinput_input_event = usb_hidinput_input_event;
 hid->hid_open = usbhid_open;
 hid->hid_close = usbhid_close;
#ifdef CONFIG_USB_HIDDEV
 hid->hiddev_hid_event = hiddev_hid_event;
 hid->hiddev_report_event = hiddev_report_event;
#endif
 return hid;

fail:
 usb_free_urb(usbhid->urbin);
 usb_free_urb(usbhid->urbout);
 usb_free_urb(usbhid->urbctrl);
 hid_free_buffers(dev, hid);
 kfree(usbhid);
fail_no_usbhid:
 hid_free_device(hid);

return NULL;
}

1.1.1: usbhid_lookup_quirk(pid,vid)解析:

分别使用usbhid_exists_dquirk(pid,vid),usbhid_exists_squirk(pid,vid)来查看动态和静态的quirks-list.如果有,则返回此quirks.没有,则返回0。

1.1.2:usb_get_extra_descriptor(descriptor, type, &hdesc)解析:

它调用__usb_get_extra_descriptor()来解析参数一(interface或endpoint 描述符)中的扩展描述符区--extra。看是否有类型为参数二(type)的条目,然后把地址交给参数三。

Sam未经证明的猜测:HID设备中,interface描述符中包含的这个扩展描述符。其中存放的都是HID信息,以hid_descriptor结构存放。

对于HID设备来说,在interface description之后会附加一个hid description, hid description中的最后部份包含有Report description或者Physical Descriptors的长度.

hid_descriptor->usb_descriptor_header->bDescriptorType为HID_DT_HID

后面描述符(包括它自己)hid_descriptor->hid_class_descriptor->bDescriptorType 为HID_DT_REPORT

1.1.3:hid_set_idle(struct usb_device *dev, int ifnum, int report, int idle):

它简单的调用usb_control_msg() 发送或接收一个usb 控制消息。

具体到这个例子中:

usb_control_msg(dev, usb_sndctrlpipe(dev, 0),
  HID_REQ_SET_IDLE, USB_TYPE_CLASS | USB_RECIP_INTERFACE, (idle << 8) | report,
  ifnum, NULL, 0, USB_CTRL_SET_TIMEOUT);

usb_sndctrlpipe(dev, 0):指出将dev中endpoint 0 设为发送control管道。

HID_REQ_SET_IDLE:此控制消息的Request。

USB_TYPE_CLASS | USB_RECIP_INTERFACE:请求类型.

调用成功才会返回。

1.1.4: hid_get_class_descriptor(dev, interface->desc.bInterfaceNumber, HID_DT_REPORT, rdesc, rsize)

也是通过调用usb_control_msg() 发送或接收一个usb 控制消息。

发送USB_REQ_GET_DESCRIPTOR,将得到的 report descriptor放到rdesc中。

1.1.5: hid_parse_report(__u8 *start, unsigned size)

参数一:通过usb_control_msg发送request(USB_REQ_GET_DESCRIPTOR) 从设备得到的report descriptor.

参数二:是此report Descriptor的长度。

1).首先创建hid_device类型设备。

2). 将参数一中数据保存在hid_device->rdesc中。长度保存在hid_device->rsize中。

3). 使用fetch_item(__u8 *start, __u8 *end, struct hid_item *item)得到report description的下一项数据。放到item中。

4). 根据不同的item.type.分别调用

hid_parser_main,
  hid_parser_global,
  hid_parser_local,
  hid_parser_reserved

来处理。

读USB HID driver时,看到quirks这部分内容。因为之前在看USB部分代码时,常看到类似的内容,但对它的语义理解并不清楚,只是笼统地认为是一个需要修正的东西。现在稍微研究一下。

一:quirks简介:

quirks: 怪癖的意思,也就是说它某种特性与通常的USB设备不相同。

Sam之前是在USB部分看到quirks:

在~/drivers/usb/core/quirks.c中,有个usb_quirk_list列表,它其实就是一个黑名单,描述了某个设备有何种问题。例如:

{ USB_DEVICE(0x03f0, 0x4002), .driver_info = USB_QUIRK_NO_AUTOSUSPEND },

表明:vid=0x03f0,pid=0x4002的设备(Hewlett-Packard PhotoSmart 720 ),不能自动suspend.

这个列表是不断扩展的,不断添加有问题的设备上来。

则判断一个设备是不是在这张黑名单上,然后如果是的,就判断它具体是属于哪种问题,

二:HID quirks:

Sam在看USB HID driver时,在modules insmod时,会调用hid_init()。它又会调用usbhid_quirks_init()

如果在insmod modules时,使用modules参数添加了quirks,格式为:quirks=pid:vid:quirks

则将此项内容添加或替换在动态建立的dquirks_list中。换句话说,也就是当modules的提供商知道自己的设备会有什么异常时,可以这样使用。

注意:此处仅仅是将某种PID,VID的quirks添加进动态列表而已。

有动态黑名单,就有静态黑名单。静态黑名单在~/drivers/hid/usbhid/hid-quirks.c中--hid_blacklist。它描绘了已知所有的quirks.

三:如何使用HID quirks:

在hid 设备probe时,会调用usbhid_lookup_quirk(),它则分别调用usbhid_exists_dquirk(动态)——和usbhid_exists_squirk(静态) 在静态黑名单和动态黑名单中寻找有没有对应的vid和pid设备。如果有,则将quirks纪录在hid->quirks中。

Linux下USB HID device driver研究相关推荐

  1. 嵌入式linux查看usb设备驱动程序,嵌入式Linux下USB驱动程序的设计

    嵌入式Linux下USB驱动程序的设计 usb概念:  USB(Universal Serial Bus)即通用串行总线,是一种全新的双向同步传输的支持热插拔的数据传输总线,其目的是为了提供一种兼容不 ...

  2. linux下usb驱动编写

    linux下usb驱动编写(内核2.4)--2.6与此接口有区别2006-09-15 14:57我们知道了在Linux下如何去使用一些最常见的USB设备.但对于做系统设计的程序员来说,这是远远不够的, ...

  3. usb linux 内核,Linux下USB内核之学习笔记

    Linux下USB子系统软件结构为 USB 内核(USB驱动,USBD )处于系统的中心,对于它进行研究是能够进行USB驱动开发(包括客户驱动和主机驱动)的第一步.它为客户端驱动和主机控制器驱动提供了 ...

  4. linux下usb设备节点名不固定,解决Linux下USB设备节点ttyUSB名不固定的问题,生成固定USB转串口设备节点...

    解决Linux下USB设备节点ttyUSB名不固定的问题,生成固定USB转串口设备节点 2018-09-19 http://blog.sina.com.cn/s/blog_8b58097f0102wx ...

  5. usb设备检测linux,Linux下USB设备检测全教程(转)

    Linux下USB设备检测全教程(转)[@more@] USB设备检测也是通过/proc目录下的USB文件系统进行的.为了使一个USB设备能够正常工作,必须要现在系统中插入USB桥接器模块.在检测开始 ...

  6. linux下usb转串口驱动分析

    linux下usb转串口驱动分析 分类: linux driver 2012-06-08 15:11 456人阅读 评论(0) 收藏 举报 linux struct interface returni ...

  7. Linux下USB suspend/resume源码分析【转】

    转自:http://blog.csdn.net/aaronychen/article/details/3928479 Linux下USB suspend/resume源码分析 Author:aaron ...

  8. linux配置usb主从_一种Linux下USB设备主从切换的实现

    龙源期刊网 http://www.qikan.com.cn 一种 Linux 下 USB 设备主从切换的实现 作者:侯景昆 来源:<电子技术与软件工程> 2013 年第 22 期 摘 要 ...

  9. linux下 usb 和pci设备的reset

    linux下 usb 和pci设备的reset 1. 什么是设备的reset 设备的寄存器 设备的结构体 usb device reset pci device reset pci 设备的functi ...

最新文章

  1. volatile关键字对
  2. 二叉搜索树的最近祖先
  3. 50%的次日留存率,没有评论留言功能的same是如何做到的?
  4. XCode上传成功后,商店里构建版本显示正在处理中
  5. Python基础——PyCharm版本——第九章、MySQL操作(核心4)
  6. 为什么我开发的SAP UI5应用里的group by 功能不工作?
  7. 多中心容灾实践:如何实现真正的异地多活?
  8. 微信JS SDK开放,前端开发者“鸡冻”了!
  9. java 模块化基础
  10. dc持久内存与mysql_为什么持久性内存会改变你的世界!
  11. Docker应用容器引擎_简介---Docker工作笔记003
  12. Laravel Event模块分析
  13. 卸载华为系统wifi服务器,如何安装随行WiFi驱动及如何卸载驱动
  14. 详解DFT的scan(边界扫描)
  15. 软银没有中国,孙正义失去一切
  16. 宽带未能和路由器连接服务器,连接路由器却上不了网怎么办
  17. 培训机构让Github的含金量降低了?
  18. “区块链”技术在传统行业中的应用
  19. WinIo64驱动级别的键盘模拟(java)
  20. 排列宝石问题C++实现

热门文章

  1. “亚马逊抄袭”引热议,拼多多、淘宝、京东该怎么玩?
  2. 三个月自学自动化测试,鬼知道我经历了什么?薪资从4.5K到11K
  3. 鸿蒙5G的营业执照,华为继“鸿蒙”商标后注册“5G”商标
  4. javafx2舞台和场景_Oracle推动JavaFX 2,Stephen Chin和Jim Weaver入伍
  5. phpMailer在本地可以发送邮件,服务器上不行
  6. 【转】我都30岁了,零基础想转行去学编程,靠谱吗?
  7. FPGA实现俄罗斯方块(二)
  8. 【无人机】模拟一群配备向下摄像头的移动空中代理覆盖平面区域(Matlab代码实现)
  9. 视频去水印软件哪个好用?
  10. echartsdemo