一、使用DMA的优点及DMA支持的请求源(请求源是启动DMA传输的事件,可以认为是触发。它可以是软件,也可以是中断,或者外部事件)

1、DMA优点是其进行数据传输时不需要CPU的干涉,可以大大提高CPU的工作效率。

2、DMA在大容量数据传输中非常重要,比如图像数据传输,SD卡数据传输,USB数据传输等。

3、S3C2440有四个DMA,每个DMA支持的工作方式基本相同,但支持的DMA请求源可能略有不同。如下为四个DMA通道分别支持的DMA请求源:

Ch0:    nXDREQ0,      UART0,      SDI,            Timer,         USB EP1

Ch1:    nXDREQ1,      UART1,      I2SSDI,       SPI0,          USB EP2

Ch2:    I2SSDO,          I2SSDI,      SDI,            Timer,         USB EP3

Ch3:    UART1,           SDI,            SPI1,          Timer,         USB EP4

那么怎么使用DMA呢,S3C2440内部集成了DMA控制器,我们只需要简单的配置一下寄存器就可以实现DMA的传输了。

二、DMA配置步骤及要点: (参考点击打开链接

1. 数据从哪里来,到哪里去?

使用DMA首先我们要知道数据的流向,DISRCx寄存器是DMA初始源寄存器,存放了数据的源地址。DIDSTx是DMA的初始目的寄存器,存放数据的目的地址。

2. 数据走的什么总线?地址是否是固定的?

我们还要知道源与目的数据存储设备在什么总线上(AHB系统总线,一般是高速的比如内存,APB外围总线,低速的比如SD,UART;具体走什么总线可以在datasheet上查到);以及数据传输结束以后起始地址还原到发送前的起始地址呢,还是在现在的末尾+1做为新的起始地址。这些设置在DISRCCx与DIDSTCx两个寄存器里面配置。

3. 数据以什么方式传输?源与目的是什么设备?要不要自动重载?

需要确定数据的传输方式有请求还是握手,根据上面的总线确定与什么时钟同步(HCLK,PCLK),是单元传输还是突发传输,是以字节传输还是字传输,是否重载。是单服务(只发送一次)还是多服务(不停循环发送),以及数据的传送大小。选择源与目的设备。最后还要确定中断是不是传输结束发生(CURR_TC记数是不是0)。这些都在DCONx中设置。

4. 怎么开始传输DMA和停止DMA,这些在DMASKTRIG中设置。

DMA(Direct Memory Access)

即直接存储器访问, DMA 传输方式无需 CPU 直接控制传输,通过硬件为 RAM 、I/O 设备开辟一条直接传送数据的通路,能使 CPU 的效率大为提高。

学了这么多驱动,不难推出DMA的编写套路:

  • 1)注册DMA中断,分配缓冲区
  • 2)注册字符设备,并提供文件操作集合fops
  • -> 2.1)file_operations里设置DMA硬件相关操作,来启动DMA

由于我们是用字符设备的测试方法测试的,而本例子只是用两个地址之间的拷贝来演示DMA的作用,所以采用字符设备方式编写

1.驱动编写之前,先来讲如何分配释放缓冲区、DMA相关寄存器介绍、使用DMA中断

1.1在linux中,分配释放DMA缓冲区,只能使用以下几个函数

1) 

  1. /*该函数只禁止cache缓冲,保持写缓冲区,也就是对注册的物理区写入数据,也会更新到对应的虚拟缓存区上*/
  2. void *dma_alloc_writecombine(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp);
  3. //分配DMA缓存区
  4. //返回值为:申请到的DMA缓冲区的虚拟地址,若为NULL,表示分配失败,需要释放,避免内存泄漏
  5. //参数如下:
  6.   //*dev:指针,这里填0,表示这个申请的缓冲区里没有内容
  7.   //size:分配的地址大小(字节单位)
  8.   //*handle:申请到的物理起始地址
  9.   //gfp:分配出来的内存参数,标志定义在<linux/gfp.h>,常用标志如下:
  10.     //GFP_ATOMIC 用来从中断处理和进程上下文之外的其他代码中分配内存. 从不睡眠.
  11.     //GFP_KERNEL 内核内存的正常分配. 可能睡眠.
  12.       //GFP_USER 用来为用户空间页来分配内存; 它可能睡眠.

2)

  1. /*该函数禁止cache缓存以及禁止写入缓冲区*/
  2. void * dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp);
  3. //分配DMA缓存区,返回值和参数和上面的函数一直

 3)

  1. dma_free_writecombine(struct device *dev, size_t size, void *cpu_addr, dma_addr_t handle); //释放DMA缓存,与dma_alloc_writecombine()对应
  2. //size:释放长度
  3. //cpu_addr:虚拟地址,
  4. //handle:物理地址

 4)

  1. dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t handle) //释放DMA缓存,与dma_alloc_coherent ()对应
  2. //size:释放长度//cpu_addr:虚拟地址,
  3. //handle:物理地址

(PS: dma_free_writecombine()其实就是dma_free_conherent(),只不过是用了#define重命名而已。)

而我们之前用的内存分配kmalloc()函数,是不能用在DMA上,因为分配出来的内存可能在物理地址上是不连续的.

1.2 那么2440开发板如何来启动DMA,先来看2440的DMA寄存器

(PS:实际这些DMA相关的寄存器,在linux内核中三星已封装好了,可以直接调用,不过非常麻烦,还不如直接设置寄存器,可以参考:http://blog.csdn.net/mirkerson/article/details/6632273)

1.2.1 2440支持4个通道的DMA控制器

其中4个通道的DMA外设请求源,如下图所示(通过DCONn寄存器的[26:24]来设置)

(PS:如果请求源是系统总线上的,就只需要设置DCONn寄存器的[23]=0即可)

1.2.2 且每个通道都可以处理以下4种情况:

1) 源和目标都在系统总线上(比如:两个物理内存地址) 2) 当目标在外设总线上时,源在系统总线上(外设指:串口,定时器,I2C,I2S等) 3) 当目标在系统总线上时,源在外设总线上 4) 源和目标都在外设总线上

1.2.3 DMA有两种工作模式(通过DCONn寄存器的[28]来设置)

查询模式:

当DMA请求XnXDREQ为低电平时,则DMA会一直传输数据,直到DMA请求拉高,才停止

握手模式:

当DMA请求XnXDREQ有下降沿触发时,则DMA会传输一次数据

1.2.4 DMA有两种传输模式(通过DCONn寄存器的[31]来设置)

单元传输:

指传输过程中,每执行一次,则读1次,写1次.(如上图所示)

突发4传输:

指传输过程中,每执行一次,则读4次,然后写4次(如下图所示)

1.2.5 2440中的DMA寄存器如下图所示:

共有4个通道的寄存器,且每个通道的寄存器内容都一致,所以我们以DMA通道0为例:

1)DISRC0初始源寄存器 

[30:0] : 存放DMA源的基地址

2)DISRCC0初始源控制寄存器

[1] : 源位置选择,0:源在系统总线上,                       1:源在外设总线上

[0] : 源地址选择,0:传输时源地址自动增加,            1:源地址固定

3)DIDST0初始目标寄存器

[30:0] : 设置DMA目的的基地址

4)DIDSTC0初始目标控制寄存器

[2]  : 中断时间选择,       0:当DMA传输计数=0,立即发生中断       1:执行完自动加载后再发送中断(也就是计数为0,然后重新加载计数值)

[1] : 目的位置选择,         0:目的在系统总线上,                         1:目的在外设总线上

[0] : 目的地址选择,         0:传输时目的地址自动增加,            1:目的地址固定

5)DCON0控制寄存器

[31] : 工作模式选择,   0:查询模式                  1:握手模式      (当源处于外设时,尽量选择握手模式)

[30] : 中断请求(DREQ)/中断回应(DACK)的同步时钟选择,        0:PCLK同步     1:HCLK同步

(PS:如果有设备在HCLK上,该位应当设为1,比如:(SDRAM)内存数组, 反之当这些设备在PCLK上,应当设为0,比如:ADC,IIS,I2C,UART)

[29] : DMA传输计数中断使能/禁止      0:禁止中断                                1:当传输完成后,产生中断

[28] : 传输模式选择,         0:单元传输                            1:突发4传输

[27] : 传输服务模式

0:单服务模式,比如:有2个DMA请求,它们会被顺序执行一次(单元传输/突发4传输)后停止,然后直到有下一次DMA请求,再重新开始另一次循环。

1:全服务模式,指该DMA若有请求,则会占用DMA总线,一直传输,期间若有其它DMA请求,只有等待传输计数TC为0,才会执行其它DMA请求

[26:24] : DMA外设请求源选择

[23]     : 软件/硬件请求源选择      0:软件请求            1:硬件请求(还需要设置[26:24]来选择外设源)

[22]     : 重新加载开关选项             为0即可

[21:20] : 传输数据大小    为00(8位)即可

[19:0]   : 设置DMA传输的计数TC

6)DSTAT0状态寄存器

[21:20] :      DMA状态             00:空闲           01:忙

[19:0]   : 传输计数当前值CURR_TC            为0表示传输结束

7)DCSRC0当前源寄存器

[30:0]  : 存放DMA当前的源基地址

8)DCDST0当前目标寄存器

[30:0]  : 存放DMA当前的目的基地址

9)DMASKTRIG0触发屏蔽寄存器

[2]   : 停止STOP            该位写1,立刻停止DMA当前的传输

[1]   : DMA通道使能         0:关闭DMA的通道0(禁止DMA请求)            1:开启DMA的通道0(开启DMA请求)

[0]   : 软件请求触发      1:表示启动一次软件请求DMA,只有DCONn[23]=0和DMASKTRIGn[1]=1才有效,DMA传输时,该位自动清0

1.3接下来就开始讲linux注册DMA中断

首先,DMA的每个通道只能有一个源- >目的,所以输入命令 cat /proc/interrupts ,找到DMA3中断未被使用

所以在linux中使用:

  1. request_irq(IRQ_DMA3, s3c_dma_irq, NULL, "s3c_dma", 1);// s3c_dma_irq:中断服务函数,这里注册DMA3中断服务函数
  2. //NULL:中断产生类型, 不需要,所以填NULL
  3. //1:表示中断时,传入中断函数的参数,本节不需要所以填1,切记不能填0,否则注册失败

2.接下来,我们便来写一个DMA的字符设备驱动

步骤如下:

  • 1) 注册DMA中断,分配两个DMA缓冲区(源、目的)
  • 2) 注册字符设备,并提供文件操作集合fops
  • -> 2.1) 通过ioctl的cmd来判断是使用DMA启动两个地址之间的拷贝,还是直接两个地址之间的拷贝
  • -> 2.2)若是DMA启动,则设置DMA的相关硬件,并启动DMA传输

2.1 所以,驱动代码如下所示:

  1. #include <linux/module.h>
  2. #include <linux/kernel.h>
  3. #include <linux/fs.h>
  4. #include <linux/init.h>
  5. #include <linux/delay.h>
  6. #include <linux/irq.h>
  7. #include <asm/irq.h>
  8. #include <asm/arch/regs-gpio.h>
  9. #include <asm/hardware.h>
  10. #include <asm/uaccess.h>
  11. #include <asm/io.h>
  12. #include <linux/dma-mapping.h>
  13.  
  14. #define S3C_DMA_SIZE 512*1024 //DMA传输长度 512KB
  15.  
  16. #define NORMAL_COPY 0 //两个地址之间的正常拷贝
  17. #define DMA_COPY 1 //两个地址之间的DMA拷贝
  18.  
  19. /*函数声明*/
  20. static DECLARE_WAIT_QUEUE_HEAD(s3c_dma_queue); //声明等待队列
  21. static int s3c_dma_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long flags);
  22.  
  23. /*
  24. * 定义中断事件标志
  25. * 0:进入等待队列 1:退出等待队列
  26. */
  27. static int s3c_dma_even=0;
  28.  
  29. static unsigned char *source_virt; //源虚拟地址
  30. static unsigned int source_phys; //源物理地址
  31.  
  32. static unsigned char *dest_virt; //目的虚拟地址
  33. static unsigned int dest_phys; //目的虚拟地址
  34.  
  35. /*DMA3寄存器*/
  36. struct S3c_dma3_regs{
  37. unsigned int disrc3 ; //0x4b0000c0
  38. unsigned int disrcc3 ;
  39. unsigned int didst3 ;
  40. unsigned int didstc3 ;
  41. unsigned int dcon3 ;
  42. unsigned int dstat3 ;
  43. unsigned int dcsrc3 ;
  44. unsigned int dcdst3 ;
  45. unsigned int dmasktrig3; //0x4b0000e0
  46. };
  47.  
  48. static volatile struct S3c_dma3_regs *s3c_dma3_regs;
  49.  
  50. /*字符设备操作*/
  51. static struct file_operations s3c_dma_fops={
  52. .owner = THIS_MODULE,
  53. .ioctl = s3c_dma_ioctl,
  54. };
  55.  
  56. /*中断服务函数*/
  57. static irqreturn_t s3c_dma_irq (int irq, void *dev_id)
  58. {
  59. s3c_dma_even=1; //退出等待队列
  60. wake_up_interruptible(&s3c_dma_queue); //唤醒 中断
  61. return IRQ_HANDLED;
  62. }
  63.  
  64. /*ioctl函数*/
  65. static int s3c_dma_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long flags)
  66. {
  67. int i;
  68. memset(source_virt, 0xAA, S3C_DMA_SIZE);
  69. memset(dest_virt, 0x55, S3C_DMA_SIZE);
  70.  
  71. switch(cmd)
  72. {
  73. case NORMAL_COPY: //正常拷贝
  74.  
  75. for(i=0;i<S3C_DMA_SIZE;i++)
  76. dest_virt[i] = source_virt[i];
  77.  
  78. if(memcmp(dest_virt, source_virt, S3C_DMA_SIZE)==0)
  79. {
  80. printk("NORMAL_COPY OK\n");
  81. return 0;
  82. }
  83. else
  84. {
  85. printk("NORMAL_COPY ERROR\n");
  86. return -EAGAIN;
  87. }
  88.  
  89. case DMA_COPY: //DMA拷贝
  90.  
  91. s3c_dma_even=0; //进入等待队列
  92.  
  93. /*设置DMA寄存器,启动一次DMA传输 */
  94. /* 源的物理地址 */
  95. s3c_dma3_regs->disrc3 = source_phys;
  96. /* 源位于AHB总线, 源地址递增 */
  97. s3c_dma3_regs->disrcc3 = (0<<1) | (0<<0);
  98. /* 目的的物理地址 */
  99. s3c_dma3_regs->didst3 = dest_phys;
  100. /* 目的位于AHB总线, 目的地址递增 */
  101. s3c_dma3_regs->didstc3 = (0<<2) | (0<<1) | (0<<0);
  102. /* 使能中断,单个传输,软件触发, */
  103. s3c_dma3_regs->dcon3=(1<<30)|(1<<29)|(0<<28)|(1<<27)|(0<<23)|(0<<20)|(S3C_DMA_SIZE<<0);
  104. //启动一次DMA传输
  105. s3c_dma3_regs->dmasktrig3 = (1<<1) | (1<<0);
  106.  
  107. wait_event_interruptible(s3c_dma_queue, s3c_dma_even); //进入睡眠,等待DMA传输中断到来才退出
  108.  
  109. if(memcmp(dest_virt, source_virt, S3C_DMA_SIZE)==0)
  110. {
  111. printk("DMA_COPY OK\n");
  112. return 0;
  113. }
  114. else
  115. {
  116. printk("DMA_COPY ERROR\n");
  117. return -EAGAIN;
  118. }
  119.  
  120. break;
  121. }
  122. return 0;
  123. }
  124.  
  125. static unsigned int major;
  126. static struct class *cls;
  127. static int s3c_dma_init(void)
  128. {
  129. /*1.1 注册DMA3 中断 */
  130. if(request_irq(IRQ_DMA3, s3c_dma_irq,NULL, "s3c_dma",1))
  131. {
  132. printk("Can't request_irq \"IRQ_DMA3\"!!!\n ");
  133. return -EBUSY;
  134. }
  135.  
  136. /*1.2 分配两个DMA缓冲区(源、目的)*/
  137. source_virt=dma_alloc_writecombine(NULL,S3C_DMA_SIZE, &source_phys, GFP_KERNEL);
  138. if(source_virt==NULL)
  139. {
  140. printk("Can't dma_alloc \n ");
  141. return -ENOMEM;
  142. }
  143.  
  144. dest_virt=dma_alloc_writecombine(NULL,S3C_DMA_SIZE, &dest_phys, GFP_KERNEL);
  145. if(dest_virt==NULL)
  146. {
  147. printk("Can't dma_alloc \n ");
  148. return -ENOMEM;
  149. }
  150.  
  151. /*2.注册字符设备,并提供文件操作集合fops*/
  152. major=register_chrdev(0, "s3c_dma",&s3c_dma_fops);
  153. cls= class_create(THIS_MODULE, "s3c_dma");
  154. class_device_create(cls, NULL,MKDEV(major,0), NULL, "s3c_dma");
  155.  
  156. s3c_dma3_regs=ioremap(0x4b0000c0, sizeof(struct S3c_dma3_regs));
  157.  
  158. return 0;
  159. }
  160.  
  161. static void s3c_dma_exit(void)
  162. {
  163. iounmap(s3c_dma3_regs);
  164.  
  165. class_device_destroy(cls, MKDEV(major,0));
  166. class_destroy(cls);
  167.  
  168. dma_free_writecombine(NULL, S3C_DMA_SIZE, dest_virt, dest_phys);
  169. dma_free_writecombine(NULL, S3C_DMA_SIZE, source_virt, source_phys);
  170.  
  171. free_irq(IRQ_DMA3, 1);
  172.  
  173. }
  174. module_init(s3c_dma_init);
  175. module_exit(s3c_dma_exit);
  176. MODULE_LICENSE("GPL");

2.2 应用测试程序如下所示:

  1. #include <stdio.h>
  2. #include <sys/types.h>
  3. #include <sys/stat.h>
  4. #include <fcntl.h>
  5. #include <sys/ioctl.h>
  6. #include <string.h>
  7.  
  8. /* ./dma_test NORMAL
  9. * ./dma_test DMA
  10. */
  11. #define NORMAL_COPY 0 //两个地址之间的正常拷贝
  12. #define DMA_COPY 1 //两个地址之间的DMA拷贝
  13.  
  14. void print_usage(char *name)
  15. {
  16. printf("Usage:\n");
  17. printf("%s <NORMAL | DMA>\n", name);
  18. }
  19.  
  20. int main(int argc, char **argv)
  21. {
  22. int fd,i=30;
  23.  
  24. if (argc != 2)
  25. {
  26. print_usage(argv[0]);
  27. return -1;
  28. }
  29.  
  30. fd = open("/dev/s3c_dma", O_RDWR);
  31. if (fd < 0)
  32. {
  33. printf("can't open /dev/s3c_dma\n");
  34. return -1;
  35. }
  36.  
  37. if (strcmp(argv[1], "NORMAL") == 0)
  38. {
  39. while (i--)                //调用驱动的ioctl(),30次
  40. {
  41. ioctl(fd, NORMAL_COPY);
  42. }
  43. }
  44. else if (strcmp(argv[1], "DMA") == 0)
  45. {
  46. while (i--)                //调用驱动的ioctl(),30次        
  47. {
  48. ioctl(fd, DMA_COPY);
  49. }
  50. }
  51. else
  52. {
  53. print_usage(argv[0]);
  54. return -1;
  55. }
  56. return 0;
  57. }

3.测试运行

输入 ./dma_test NORMAL & ,使用CPU正常拷贝,可以发现占用了大部分资源,输入 ls 无反应:

输入./dma_test DMA & ,使用DMA拷贝,输入 ls 立马有反应,从而释放了CPU的压力:

22、DMA驱动程序框架的更多相关文章

  1. tty驱动程序框架

    tty驱动程序框架 一.TTY概念解析 在Linux系统中,终端是一类字符型设备,它包括多种类型,通常使用tty来简称各种类型的终端设备. 1.1串口终端(/dev/ttyS*) 串口终端是使用计算机 ...

  2. 18.tty驱动程序框架

    tty驱动程序框架 一.TTY概念解析 在Linux系统中,终端是一类字符型设备,它包括多种类型,通常使用tty来简称各种类型的终端设备. 1.1串口终端(/dev/ttyS*) 串口终端是使用计算机 ...

  3. 嵌入式Linux驱动学习之路(九)Linux系统调用、驱动程序框架

    应用程序通过open  read  write close 等函数来操作计算机硬件.类似是一个接口. 当应用程序调用这些接口程序时,计算机是如何进入内核的呢?这是经过了系统调用. 实际上当调用接口函数 ...

  4. 2.5 USB摄像头驱动程序框架

    学习目标:根据vivi驱动架构和linux-2.6.31/linux-2.6.31.14/drivers/media/video/uvc/Uvc_driver.c驱动源码,分析usb摄像头驱动程序框架 ...

  5. 10. LCD驱动程序 ——框架分析

    引言: 由LCD的硬件原理及操作(可参看韦哥博客:第017课 LCD原理详解及裸机程序分析) 我们知道只要LCD控制器的相关寄存器正确配置好,就可以在LCD面板上显示framebuffer中的内容. ...

  6. DMA驱动框架

    框架入口源文件:dma.c (可根据入口源文件,再按着框架到内核走一遍) 内核版本:linux_2.6.22.6    硬件平台:JZ2440 以下是驱动框架: 以下是驱动代码  dma.c : #i ...

  7. linux驱动程序框架基础

    ============================      指引     ============================= 第一节是最基础的驱动程序: 第二节是/dev应用层接口的使 ...

  8. Linux驱动之USB总线驱动程序框架简析

    通用串行总线(USB)是主机和外围设备之间的一种连接.USB总线规范有1.1版和2.0版,当然现在已经有了3.0版本.USB1.1支持两种传输速度:低速为1.5Mbps,高速为12Mbps.USB2. ...

  9. 9、LCD驱动程序框架

    linux-3.4.2\drivers\video\S3C2410fb.c(内核自带驱动程序) fbmem.c是LCD驱动程序顶层框架文件,是一个通用的文件,在初始化init函数中会注册一个字符设备, ...

随机推荐

  1. J2msi 自己制作的把exe打成安装包简易GUI程序

    因为wix是用命令行执行的,操作比较麻烦,所以自己写了个wix生成安装包的图形操作程序, 原理很简单,主要用java 来执行 wix 的heat .candle 和 light 命令 ,最后生成安装文 ...

  2. HDU 2017 Multi-University Training Contest - Team 4 1009 1011

    Questionnaire Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 524288/524288 K (Java/Others)T ...

  3. STANDBY REDO LOG

    SRL Introduce 从">ORACLE9i开始,出现了Standby Redo Logs(SRL),9.1开始只有">physical standby支持SRL ...

  4. AIX设备四种状态

    AIX设备四种状态: Undefined(未定义):表示系统无法识别该设备,也就是系统无法找到该设备.(驱动无法识别) Defined(定义):表示系统可以识别设备,信息保存在ODM,但是系统不能使用 ...

  5. C/C++(基础-常量,类型转换)

    字符 char ascII码表 #include<stdio.h> int main() { unsigned char ch; for(ch = 0;ch < 128;ch++) ...

  6. BZOJ3994: [SDOI2015]约数个数和(莫比乌斯反演)

    Description  设d(x)为x的约数个数,给定N.M,求     Input 输入文件包含多组测试数据. 第一行,一个整数T,表示测试数据的组数. 接下来的T行,每行两个整数N.M. Out ...

  7. JMS基础知识

    JMS规范: jms的基本构件:  连接工厂(connectionFactory):客户用来创建连接的对象.比如:activeMQ提供的ActiveMQConnectionFactory. 连接(co ...

  8. Codeforces 718C. Sasha and Array(线段树)

    传送门 解题思路: 这道题给了我们一个崭新的角度来看线段树. 我们常常使用的线段树是维护区间的函数的. 这里呢,提示我们线段树其实还可以维护递推. 美好的矩阵递推性质支持了这一功能. 或者说,对于递推 ...

  9. BZOJ2754: [SCOI2012]喵星球上的点名(AC自动机/后缀自动机)

    Description a180285幸运地被选做了地球到喵星球的留学生.他发现喵星人在上课前的点名现象非常有趣.   假设课堂上有N个喵星人,每个喵星人的名字由姓和名构成.喵星球上的老师会选择M个串 ...

  10. vim学习4

    分频 参考 http://coolshell.cn/articles/1679.htm