码迷,mamicode.com
首页 > 其他好文 > 详细

20150429 S3C实现DMA驱动程序编写

时间:2015-04-29 16:27:57      阅读:298      评论:0      收藏:0      [点我收藏+]

标签:

20150429 S3C实现DMA驱动程序编写

2015-04-29 Lover雪儿


IMX257上只有SDMA,SDMADMA的功能更加强大,但是为了学习的目的,如果直接学习SDMA,可能会不能消化,

所以,此处,我们从简单到复杂,S3C2440DMA驱动程序开始学习,等学懂它之后,我们再进军IMX257SDMA.


.一个简单的程序框架

1.定义一些指针

 1 static int major = 0;
 2 
 3 #define MEM_CPY_NO_DMA  0
 4 #define MEM_CPY_DMA     1
 5 
 6 static char *src;
 7 static u32 src_phys;
 8 
 9 static char *dst;
10 static u32 dst_phys;
11 
12 static struct class *cls;
13 
14 #define BUF_SIZE  (512*1024)

上面代码中主要定义了主设备号,目的地址,源地址等.

 

2.定义字符设备的file_operation结构体

 1 static int s3c_dma_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
 2 {
 3     switch (cmd)
 4     {
 5         case MEM_CPY_NO_DMA :
 6         {
 7             break;
 8         }
 9         case MEM_CPY_DMA :
10         {
11             break;
12         }
13     }
14     return 0;
15 }
16 
17 static struct file_operations dma_fops = {
18     .owner  = THIS_MODULE,
19     .ioctl  = s3c_dma_ioctl,
20 };

case,分别定义CPU拷贝数据和DMA拷贝数据两种情况.

 

3.入口函数

 1 static int s3c_dma_init(void)
 2 {
 3     /* 分配SRC, DST对应的缓冲区 */
 4     src = dma_alloc_writecombine(NULL, BUF_SIZE, &src_phys, GFP_KERNEL);
 5     if (NULL == src)
 6     {
 7         printk("can‘t alloc buffer for src\n");
 8         return -ENOMEM;
 9     }
10     
11     dst = dma_alloc_writecombine(NULL, BUF_SIZE, &dst_phys, GFP_KERNEL);
12     if (NULL == dst)
13     {
14         dma_free_writecombine(NULL, BUF_SIZE, src, src_phys);
15         printk("can‘t alloc buffer for dst\n");
16         return -ENOMEM;
17     }
18 
19     major = register_chrdev(0, "s3c_dma", &dma_fops);
20 
21     /* 为了自动创建设备节点 */
22     cls = class_create(THIS_MODULE, "s3c_dma");
23     class_device_create(cls, NULL, MKDEV(major, 0), NULL, "dma"); /* /dev/dma */
24         
25     return 0;
26 }

上面代码中:首先分配了源.目的地址的内存缓冲区,接着就是申请字符设备,自动创建字符设备节点.

 

4.出口函数

1 static void s3c_dma_exit(void)
2 {
3     class_device_destroy(cls, MKDEV(major, 0));
4     class_destroy(cls);
5     unregister_chrdev(major, "s3c_dma");
6     dma_free_writecombine(NULL, BUF_SIZE, src, src_phys);
7     dma_free_writecombine(NULL, BUF_SIZE, dst, dst_phys);    
8 } 

销毁字符设备的类,卸载字符设备,接着就是释放前面申请的源.目的地址的内存.

附上驱动代码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/uaccess.h>
 8 #include <asm/irq.h>
 9 #include <asm/io.h>
10 #include <asm/arch/regs-gpio.h>
11 #include <asm/hardware.h>
12 #include <linux/poll.h>
13 
14 static int major = 0;
15 
16 #define MEM_CPY_NO_DMA  0
17 #define MEM_CPY_DMA     1
18 
19 static char *src;
20 static u32 src_phys;
21 
22 static char *dst;
23 static u32 dst_phys;
24 
25 static struct class *cls;
26 
27 #define BUF_SIZE  (512*1024)
28 
29 static int s3c_dma_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
30 {
31     switch (cmd)
32     {
33         case MEM_CPY_NO_DMA :
34         {
35             break;
36         }
37 
38         case MEM_CPY_DMA :
39         {
40             break;
41         }
42     }
43 
44     return 0;
45 }
46 
47 static struct file_operations dma_fops = {
48     .owner  = THIS_MODULE,
49     .ioctl  = s3c_dma_ioctl,
50 };
51 
52 static int s3c_dma_init(void)
53 {
54     /* 分配SRC, DST对应的缓冲区 */
55     src = dma_alloc_writecombine(NULL, BUF_SIZE, &src_phys, GFP_KERNEL);
56     if (NULL == src)
57     {
58         printk("can‘t alloc buffer for src\n");
59         return -ENOMEM;
60     }
61     
62     dst = dma_alloc_writecombine(NULL, BUF_SIZE, &dst_phys, GFP_KERNEL);
63     if (NULL == dst)
64     {
65         dma_free_writecombine(NULL, BUF_SIZE, src, src_phys);
66         printk("can‘t alloc buffer for dst\n");
67         return -ENOMEM;
68     }
69 
70     major = register_chrdev(0, "s3c_dma", &dma_fops);
71 
72     /* 为了自动创建设备节点 */
73     cls = class_create(THIS_MODULE, "s3c_dma");
74     class_device_create(cls, NULL, MKDEV(major, 0), NULL, "dma"); /* /dev/dma */
75         
76     return 0;
77 }
78 
79 static void s3c_dma_exit(void)
80 {
81     class_device_destroy(cls, MKDEV(major, 0));
82     class_destroy(cls);
83     unregister_chrdev(major, "s3c_dma");
84     dma_free_writecombine(NULL, BUF_SIZE, src, src_phys);
85     dma_free_writecombine(NULL, BUF_SIZE, dst, dst_phys);    
86 }
87 
88 module_init(s3c_dma_init);
89 module_exit(s3c_dma_exit);
90 
91 MODULE_LICENSE("GPL");
dma_1.c

 

.增加不使用DMA的内存拷贝功能.

 1 static int s3c_dma_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
 2 {
 3     int i;
 4     memset(src, 0xAA, BUF_SIZE);
 5     memset(dst, 0x55, BUF_SIZE);
 6     
 7     switch (cmd)
 8     {
 9         case MEM_CPY_NO_DMA :
10         {
11             for (i = 0; i < BUF_SIZE; i++)
12                 dst[i] = src[i];
13             if (memcmp(src, dst, BUF_SIZE) == 0)
14             {
15                 printk("MEM_CPY_NO_DMA OK\n");
16             }
17             else
18             {
19                 printk("MEM_CPY_DMA ERROR\n");
20             }
21             break;
22         }
23         case MEM_CPY_DMA :
24         {
25             break;
26         }
27     }
28 
29     return 0;
30 }

如程序所示,ioctl函数中不适用DMA的增加内存拷贝功能.

附上驱动程序2

技术分享
  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/uaccess.h>
  8 #include <asm/irq.h>
  9 #include <asm/io.h>
 10 #include <asm/arch/regs-gpio.h>
 11 #include <asm/hardware.h>
 12 #include <linux/poll.h>
 13 
 14 static int major = 0;
 15 
 16 #define MEM_CPY_NO_DMA  0
 17 #define MEM_CPY_DMA     1
 18 
 19 static char *src;
 20 static u32 src_phys;
 21 
 22 static char *dst;
 23 static u32 dst_phys;
 24 
 25 static struct class *cls;
 26 
 27 #define BUF_SIZE  (512*1024)
 28 
 29 static int s3c_dma_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
 30 {
 31     int i;
 32 
 33     memset(src, 0xAA, BUF_SIZE);
 34     memset(dst, 0x55, BUF_SIZE);
 35     
 36     switch (cmd)
 37     {
 38         case MEM_CPY_NO_DMA :
 39         {
 40             for (i = 0; i < BUF_SIZE; i++)
 41                 dst[i] = src[i];
 42             if (memcmp(src, dst, BUF_SIZE) == 0)
 43             {
 44                 printk("MEM_CPY_NO_DMA OK\n");
 45             }
 46             else
 47             {
 48                 printk("MEM_CPY_DMA ERROR\n");
 49             }
 50             break;
 51         }
 52 
 53         case MEM_CPY_DMA :
 54         {
 55             break;
 56         }
 57     }
 58 
 59     return 0;
 60 }
 61 
 62 static struct file_operations dma_fops = {
 63     .owner  = THIS_MODULE,
 64     .ioctl  = s3c_dma_ioctl,
 65 };
 66 
 67 static int s3c_dma_init(void)
 68 {
 69     /* 分配SRC, DST对应的缓冲区 */
 70     src = dma_alloc_writecombine(NULL, BUF_SIZE, &src_phys, GFP_KERNEL);
 71     if (NULL == src)
 72     {
 73         printk("can‘t alloc buffer for src\n");
 74         return -ENOMEM;
 75     }
 76     
 77     dst = dma_alloc_writecombine(NULL, BUF_SIZE, &dst_phys, GFP_KERNEL);
 78     if (NULL == dst)
 79     {
 80         dma_free_writecombine(NULL, BUF_SIZE, src, src_phys);
 81         printk("can‘t alloc buffer for dst\n");
 82         return -ENOMEM;
 83     }
 84 
 85     major = register_chrdev(0, "s3c_dma", &dma_fops);
 86 
 87     /* 为了自动创建设备节点 */
 88     cls = class_create(THIS_MODULE, "s3c_dma");
 89     class_device_create(cls, NULL, MKDEV(major, 0), NULL, "dma"); /* /dev/dma */
 90         
 91     return 0;
 92 }
 93 
 94 static void s3c_dma_exit(void)
 95 {
 96     class_device_destroy(cls, MKDEV(major, 0));
 97     class_destroy(cls);
 98     unregister_chrdev(major, "s3c_dma");
 99     dma_free_writecombine(NULL, BUF_SIZE, src, src_phys);
100     dma_free_writecombine(NULL, BUF_SIZE, dst, dst_phys);    
101 }
102 
103 module_init(s3c_dma_init);
104 module_exit(s3c_dma_exit);
105 
106 MODULE_LICENSE("GPL");
dma_2.c

附上测试程序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 nodma
 9  * ./dma_test dma
10  */
11 #define MEM_CPY_NO_DMA  0
12 #define MEM_CPY_DMA     1
13 
14 void print_usage(char *name)
15 {
16     printf("Usage:\n");
17     printf("%s <nodma | dma>\n", name);
18 }
19 
20 
21 int main(int argc, char **argv)
22 {
23     int fd;
24     
25      if (argc != 2)
26     {
27         print_usage(argv[0]);
28         return -1;
29     }
30 
31     fd = open("/dev/dma", O_RDWR);
32     if (fd < 0)
33     {
34         printf("can‘t open /dev/dma\n");
35         return -1;
36     }
37 
38     if (strcmp(argv[1], "nodma") == 0)
39     {
40         while (1)
41         {
42             ioctl(fd, MEM_CPY_NO_DMA);
43         }
44     }
45     else if (strcmp(argv[1], "dma") == 0)
46     {
47         while (1)
48         {
49             ioctl(fd, MEM_CPY_DMA);
50         }
51     }
52     else
53     {
54         print_usage(argv[0]);
55         return -1;
56     }
57     return 0;     
58 }
dma_test.c

 

.映射s3cDMA内存地址

测试程序时,不知道大家有没有发现,当我们运行test测试程序时,我们的拷贝进程占用了全部的CPU,以至于我们想运行一条命令都要等好久才能实现,

为了解决它,我们就引入了DMA,接下来,我们就真正开始DMA的编程

 1 #define DMA0_BASE_ADDR  0x4B000000
 2 #define DMA1_BASE_ADDR  0x4B000040
 3 #define DMA2_BASE_ADDR  0x4B000080
 4 #define DMA3_BASE_ADDR  0x4B0000C0
 5 
 6 struct s3c_dma_regs {
 7     unsigned long disrc;
 8     unsigned long disrcc;
 9     unsigned long didst;
10     unsigned long didstc;
11     unsigned long dcon;
12     unsigned long dstat;
13     unsigned long dcsrc;
14     unsigned long dcdst;
15     unsigned long dmasktrig;
16 };

如程序中所示:首先定义DMA的四个通道的基地址,接着定义一个s3cdma的寄存器结构体.

分别再入口函数中映射DMA的内存IO端口,此处我们使用DMA0,当然如果要使用其他的DMA,类似.

在入口函数中映射:

dma_regs = ioremap(DMA0_BASE_ADDR, sizeof(struct s3c_dma_regs));

在出口函数中解除DMA映射.

iounmap(dma_regs);


.注册DMA中断

前面我们已经映好DMA的内存地址了.

接下来,要想DMA工作,还需要对齐进行注册DMA中断和DMA配置.

在入口函数中注册DMA中断,再出口函数中释放DMA中断

在入口函数中注册中断:

1 if (request_irq(IRQ_DMA3, s3c_dma_irq, 0, "s3c_dma", 1))
2     {
3         printk("can‘t request_irq for DMA\n");
4         return -EBUSY;
5     }

在出口函数中释放中断:

free_irq(IRQ_DMA3, 1);

当用户程序中定义为 MEM_CPY_DMA 使用DMA传输时,便会在ioctl函数中进入case MEM_CPY_DMA :.

接着我们便在其中配置号DMA的源.目的参数,接着便启动传输,CPU则进入休眠状态,让出CPU供其他进程使用.DMA拷贝完成时,便会触发中断,从而唤醒.

ioctl函数的部分程序如下面所示:

 1         case MEM_CPY_DMA :
 2         {
 3             ev_dma = 0;
 4             
 5             /* 把源,目的,长度告诉DMA */
 6             dma_regs->disrc      = src_phys;        /* 源的物理地址 */
 7             dma_regs->disrcc     = (0<<1) | (0<<0); /* 源位于AHB总线, 源地址递增 */
 8             dma_regs->didst      = dst_phys;        /* 目的的物理地址 */
 9             dma_regs->didstc     = (0<<2) | (0<<1) | (0<<0); /* 目的位于AHB总线, 目的地址递增 */
10             dma_regs->dcon       = (1<<30)|(1<<29)|(0<<28)|(1<<27)|(0<<23)|(0<<20)|(BUF_SIZE<<0);  /* 使能中断,单个传输,软件触发, */
11 
12             /* 启动DMA */
13             dma_regs->dmasktrig  = (1<<1) | (1<<0);
14 
15             /* 如何知道DMA什么时候完成? */
16             /* 休眠 */
17             wait_event_interruptible(dma_waitq, ev_dma);
18 
19             if (memcmp(src, dst, BUF_SIZE) == 0)
20             {
21                 printk("MEM_CPY_DMA OK\n");
22             }
23             else
24             {
25                 printk("MEM_CPY_DMA ERROR\n");
26             }
27             
28             break;
29         }

中断服务程序:

1 static irqreturn_t s3c_dma_irq(int irq, void *devid)
2 {
3     /* 唤醒 */
4     ev_dma = 1;
5     wake_up_interruptible(&dma_waitq);   /* 唤醒休眠的进程 */
6     return IRQ_HANDLED;
7 }

附上驱动程序3:

技术分享
  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/uaccess.h>
  8 #include <asm/irq.h>
  9 #include <asm/io.h>
 10 #include <asm/arch/regs-gpio.h>
 11 #include <asm/hardware.h>
 12 #include <linux/poll.h>
 13 #include <linux/dma-mapping.h>
 14 
 15 #define MEM_CPY_NO_DMA  0
 16 #define MEM_CPY_DMA     1
 17 
 18 #define BUF_SIZE  (512*1024)
 19 
 20 #define DMA0_BASE_ADDR  0x4B000000
 21 #define DMA1_BASE_ADDR  0x4B000040
 22 #define DMA2_BASE_ADDR  0x4B000080
 23 #define DMA3_BASE_ADDR  0x4B0000C0
 24 
 25 struct s3c_dma_regs {
 26     unsigned long disrc;
 27     unsigned long disrcc;
 28     unsigned long didst;
 29     unsigned long didstc;
 30     unsigned long dcon;
 31     unsigned long dstat;
 32     unsigned long dcsrc;
 33     unsigned long dcdst;
 34     unsigned long dmasktrig;
 35 };
 36 
 37 
 38 static int major = 0;
 39 
 40 static char *src;
 41 static u32 src_phys;
 42 
 43 static char *dst;
 44 static u32 dst_phys;
 45 
 46 static struct class *cls;
 47 
 48 static volatile struct s3c_dma_regs *dma_regs;
 49 
 50 static DECLARE_WAIT_QUEUE_HEAD(dma_waitq);
 51 /* 中断事件标志, 中断服务程序将它置1,ioctl将它清0 */
 52 static volatile int ev_dma = 0;
 53 
 54 static int s3c_dma_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
 55 {
 56     int i;
 57 
 58     memset(src, 0xAA, BUF_SIZE);
 59     memset(dst, 0x55, BUF_SIZE);
 60     
 61     switch (cmd)
 62     {
 63         case MEM_CPY_NO_DMA :
 64         {
 65             for (i = 0; i < BUF_SIZE; i++)
 66                 dst[i] = src[i];
 67             if (memcmp(src, dst, BUF_SIZE) == 0)
 68             {
 69                 printk("MEM_CPY_NO_DMA OK\n");
 70             }
 71             else
 72             {
 73                 printk("MEM_CPY_DMA ERROR\n");
 74             }
 75             break;
 76         }
 77 
 78         case MEM_CPY_DMA :
 79         {
 80             ev_dma = 0;
 81             
 82             /* 把源,目的,长度告诉DMA */
 83             dma_regs->disrc      = src_phys;        /* 源的物理地址 */
 84             dma_regs->disrcc     = (0<<1) | (0<<0); /* 源位于AHB总线, 源地址递增 */
 85             dma_regs->didst      = dst_phys;        /* 目的的物理地址 */
 86             dma_regs->didstc     = (0<<2) | (0<<1) | (0<<0); /* 目的位于AHB总线, 目的地址递增 */
 87             dma_regs->dcon       = (1<<30)|(1<<29)|(0<<28)|(1<<27)|(0<<23)|(0<<20)|(BUF_SIZE<<0);  /* 使能中断,单个传输,软件触发, */
 88 
 89             /* 启动DMA */
 90             dma_regs->dmasktrig  = (1<<1) | (1<<0);
 91 
 92             /* 如何知道DMA什么时候完成? */
 93             /* 休眠 */
 94             wait_event_interruptible(dma_waitq, ev_dma);
 95 
 96             if (memcmp(src, dst, BUF_SIZE) == 0)
 97             {
 98                 printk("MEM_CPY_DMA OK\n");
 99             }
100             else
101             {
102                 printk("MEM_CPY_DMA ERROR\n");
103             }
104             
105             break;
106         }
107     }
108 
109     return 0;
110 }
111 
112 static struct file_operations dma_fops = {
113     .owner  = THIS_MODULE,
114     .ioctl  = s3c_dma_ioctl,
115 };
116 
117 static irqreturn_t s3c_dma_irq(int irq, void *devid)
118 {
119     /* 唤醒 */
120     ev_dma = 1;
121     wake_up_interruptible(&dma_waitq);   /* 唤醒休眠的进程 */
122     return IRQ_HANDLED;
123 }
124 
125 static int s3c_dma_init(void)
126 {
127     if (request_irq(IRQ_DMA3, s3c_dma_irq, 0, "s3c_dma", 1))
128     {
129         printk("can‘t request_irq for DMA\n");
130         return -EBUSY;
131     }
132     
133     /* 分配SRC, DST对应的缓冲区 */
134     src = dma_alloc_writecombine(NULL, BUF_SIZE, &src_phys, GFP_KERNEL);
135     if (NULL == src)
136     {
137         printk("can‘t alloc buffer for src\n");
138         free_irq(IRQ_DMA3, 1);
139         return -ENOMEM;
140     }
141     
142     dst = dma_alloc_writecombine(NULL, BUF_SIZE, &dst_phys, GFP_KERNEL);
143     if (NULL == dst)
144     {
145         free_irq(IRQ_DMA3, 1);
146         dma_free_writecombine(NULL, BUF_SIZE, src, src_phys);
147         printk("can‘t alloc buffer for dst\n");
148         return -ENOMEM;
149     }
150 
151     major = register_chrdev(0, "s3c_dma", &dma_fops);
152 
153     /* 为了自动创建设备节点 */
154     cls = class_create(THIS_MODULE, "s3c_dma");
155     class_device_create(cls, NULL, MKDEV(major, 0), NULL, "dma"); /* /dev/dma */
156 
157     dma_regs = ioremap(DMA3_BASE_ADDR, sizeof(struct s3c_dma_regs));
158         
159     return 0;
160 }
161 
162 static void s3c_dma_exit(void)
163 {
164     iounmap(dma_regs);
165     class_device_destroy(cls, MKDEV(major, 0));
166     class_destroy(cls);
167     unregister_chrdev(major, "s3c_dma");
168     dma_free_writecombine(NULL, BUF_SIZE, src, src_phys);
169     dma_free_writecombine(NULL, BUF_SIZE, dst, dst_phys);    
170     free_irq(IRQ_DMA3, 1);
171 }
172 
173 module_init(s3c_dma_init);
174 module_exit(s3c_dma_exit);
175 
176 MODULE_LICENSE("GPL");
dma_3.c

附上测试程序3:

技术分享
 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 nodma
 9  * ./dma_test dma
10  */
11 #define MEM_CPY_NO_DMA  0
12 #define MEM_CPY_DMA     1
13 
14 void print_usage(char *name)
15 {
16     printf("Usage:\n");
17     printf("%s <nodma | dma>\n", name);
18 }
19 
20 
21 int main(int argc, char **argv)
22 {
23     int fd;
24     
25      if (argc != 2)
26     {
27         print_usage(argv[0]);
28         return -1;
29     }
30 
31     fd = open("/dev/dma", O_RDWR);
32     if (fd < 0)
33     {
34         printf("can‘t open /dev/dma\n");
35         return -1;
36     }
37 
38     if (strcmp(argv[1], "nodma") == 0)
39     {
40         while (1)
41         {
42             ioctl(fd, MEM_CPY_NO_DMA);
43         }
44     }
45     else if (strcmp(argv[1], "dma") == 0)
46     {
47         while (1)
48         {
49             ioctl(fd, MEM_CPY_DMA);
50         }
51     }
52     else
53     {
54         print_usage(argv[0]);
55         return -1;
56     }
57     return 0;     
58 }
dma_test.c

 

.总结DMA工作流程.

简单来说,我觉得DMA编程总共分为配置和运行两个步骤.

配置:

注册中断

分配供模拟拷贝数据的源.目的地址的内存空间.

注册字符设备

映射ioremapDMAIO空间

运行:

应用程序调用ioctl(fd,MEM_CPY_DMA),进入case语句

case语句中配置号DMA传输数据所需要的源,目的,大小,便启动DMA传输.

启动DMA传输后,程序进入可中断的休眠,让出CPU

一旦DMA传输完毕,便会触发中断,再中断中唤醒,然后打印MEM_CPY_DMA OK告诉应用程序DMA传输成功了.

 

20150429 S3C实现DMA驱动程序编写

标签:

原文地址:http://www.cnblogs.com/lihaiyan/p/4466041.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!