2.字符设备驱动------按键中断及休眠

硬件相关配置:

*S2      eint0   GPF0
*S3      eint2   GPF2
*S4      eint11  GPG3
*S5      eint19  GPG11

 

先看测试程序中的main函数(thirdtest.c)

 1 int main(int argc, char **argv)
 2 {
 3     int fd;
 4     unsigned char key_vals[3];
 5     
 6     /*,open返回整型变量,若值等于-1,说明打开文件出现错误,
 7      *如果为大于0的值,那么这个值代表的就是文件描述符fd*/
 8     fd = open("/dev/buttons", O_RDWR);    /*控制符:O_RDWR 读、写打开*/
 9     if(fd < 0)
10     {
11         printf("can not open!\n");
12     }
13 
14     while(1)
15     {
16         read(fd, &key_val, 1);
17         printf("key_val = 0x%x\n", key_val);
18     }
19     return 0;
20 }

(所有的操作都是以open函数来开始,它用来获取fd,然后后期的其他操作全部控制fd来完成对硬件设备的实际操作)

应用程序打开设备,调用系统调用open时,操作系统会将文件系统对应设备文件的inode中的file_operations安装进用户进程的task_struct中的file_struct,最终会调用到调用具体文件的file_operations底层操作函数 drv_open , 在这个函数里面干什么呢?

  注册中断: 

    (1)向irq_des[irq]结构中的action链表中链入了用户注册的中断处理函数

    (2)设置中断的触发方式为边沿触发

    (3)使能中断

1 static int drv_open(struct inode *inode, struct file *file)
2 {
3     /* 配置GPF0,2,GPG3,11为中断引脚 */
4     request_irq(IRQ_EINT0,  buttons_irq, IRQT_BOTHEDGE, "S2", 1);
5     request_irq(IRQ_EINT2,  buttons_irq, IRQT_BOTHEDGE, "S3", 1);
6     request_irq(IRQ_EINT11, buttons_irq, IRQT_BOTHEDGE, "S4", 1);
7     request_irq(IRQ_EINT19, buttons_irq, IRQT_BOTHEDGE, "S5", 1);    
8     return 0;
9 }

现在来看 request_irq(IRQ_EINT0, buttons_irq, IRQT_BOTHEDGE, "S2", 1);

1 int request_irq(unsigned int irq, irq_handler_t handler,
2         unsigned long irqflags, const char *devname, void *dev_id)
3     
4 irq:中断号
5 handler:处理函数
6 irqflags:上升沿触发,下降沿触发,边沿触发等。指定了快速中断或中断共享等中断处理属性.
7 *devname:中断名字。通常是设备驱动程序的名称。改值用在 /proc/interrupt 系统 (虚拟)
8 文件上,或内核发生中断错误时使用。
9 dev_id 可作为共享中断时的中断区别参数,也可以用来指定中断服务函数需要参考的数据地址。也用于卸载action

  1.确定中断号,可以查看这个函数的调用 s3c24xx_init_irq 很明显的看到能够使用的宏在 include/asm-arm/arch/irqs.h中定义

1 #define IRQ_EINT0      S3C2410_IRQ(0)        /*对应宏为 16 */
2 #define IRQ_EINT2      S3C2410_IRQ(2)
3 #define IRQ_EINT11     S3C2410_IRQ(39)
4 #define IRQ_EINT19     S3C2410_IRQ(47)

  2.中断标志irqflags,同样在 s3c24xx_init_irq 找到相关的 set_irq_chip,找到对应的chip,深入分析下set_type函数,最后在include/asm-arm/irq.h 发现双边沿触发的宏。

1 ......
2 #define __IRQT_FALEDGE    IRQ_TYPE_EDGE_FALLING 
3 #define __IRQT_RISEDGE    IRQ_TYPE_EDGE_RISING    
4   .....
5 #define IRQT_BOTHEDGE    (__IRQT_RISEDGE|__IRQT_FALEDGE)    
1 #define IRQ_TYPE_EDGE_RISING    0x00000001    /* Edge rising type */
2 #define IRQ_TYPE_EDGE_FALLING    0x00000002    /* Edge falling type */

  3.char *devname中断名随便取名

    分别取名为“s2, s3, s3, s4”

  4.dev_id可用作释放中断函数中删除action的标识,这里可以先写作1

  5. handler中断处理函数

1 /*功能打印中断号*/
2 static irqreturn_t buttons_irq(int irq, void *dev_id)
3 {
4     printk("irq%d\r\n",irq);
5     return IRQ_HANDLED;
6 }

  

  关闭中断:这里需要增加释放删除action链表的函数

1 int drv_close(struct inode *inode, struct file *file)
2 {
3     free_irq(IRQ_EINT0, 1);
4     free_irq(IRQ_EINT2, 1);
5     free_irq(IRQ_EINT11,1);
6     free_irq(IRQ_EINT19,1);
7     return 0;
8 }

  回到main函数,open配置中断之后,得到正确的文件描述符fd, 进入while循环,执行read函数,最终会执行到底层操作函数drv_read

1 static ssize_t drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
2 {
  /*此处并不实现其他功能*/
3 return 0; 4 }

  最后,测试按键触发中断,打印设备号。

 

完整程序:

 1 #include <sys/types.h>
 2 #include <sys/stat.h>
 3 #include <fcntl.h>
 4 #include <stdio.h>
 5 
 6 /* thirddrvtest on
 7   */
 8 int main(int argc, char **argv)
 9 {
10     int fd;
11     unsigned char key_vals[3];
12     
13     /*,open返回的是一个整型变量,如果这个值等于-1,说明打开文件出现错误,
14      *如果为大于0的值,那么这个值代表的就是文件描述符fd*/
15     fd = open("/dev/buttons", O_RDWR);    /*O_RDWR 读、写打开*/
16     if(fd < 0)
17     {
18         printf("can not open!\n");
19     }
20 
21     while(1)
22     {
23         read(fd, &key_val, 1);
24         printf("key_val = 0x%x\n", key_val);
25     }
26     return 0;
27 }
drv_test.c
 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/interrupt.h>
13 
14 
15 
16 volatile unsigned long *gpfcon;
17 volatile unsigned long *gpfdat;
18 volatile unsigned long *gpgcon;
19 volatile unsigned long *gpgdat;
20 
21 
22 static struct class *drv_class;
23 static struct class_device    *drv_class_dev;
24 
25 static irqreturn_t buttons_irq(int irq, void *dev_id)
26 {
27     printk("irq%d\r\n",irq);
28     return IRQ_HANDLED;
29 }
30 
31 static int drv_open(struct inode *inode, struct file *file)
32 {
33     /* 配置GPF0,2为输入引脚 */
34     /* 配置GPG3,11为输入引脚 */
35     request_irq(IRQ_EINT0,  buttons_irq, IRQT_BOTHEDGE, "S2", 1);
36     request_irq(IRQ_EINT2,  buttons_irq, IRQT_BOTHEDGE, "S3", 1);
37     request_irq(IRQ_EINT11, buttons_irq, IRQT_BOTHEDGE, "S4", 1);
38     request_irq(IRQ_EINT19, buttons_irq, IRQT_BOTHEDGE, "S5", 1);    
39     return 0;
40 }
41 
42 int drv_close(struct inode *inode, struct file *file)
43 {
44     free_irq(IRQ_EINT0, 1);
45     free_irq(IRQ_EINT2, 1);
46     free_irq(IRQ_EINT11,1);
47     free_irq(IRQ_EINT19,1);
48     return 0;
49 }
50 
51 
52 static ssize_t drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
53 {
54     //int minor =  MINOR(file->f_dentry->d_inode->i_rdev);
55     //printk("drv_write=%d\n",minor);
56     return 0;
57 }
58 
59 static ssize_t drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
60 {
61     return 0;
62 }
63 
64 
65 static struct file_operations drv_fops = {
66     .owner  =   THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
67     .open   =   drv_open,     
68     .write    =    drv_write,
69     .read    =    drv_read,     
70     .release =  drv_close,  
71 };
72 
73 static int major;
74 static int drv_init(void)
75 {
76     int minor=0;
77     major=register_chrdev(0, "drv", &drv_fops); // 注册, 告诉内核
78     drv_class = class_create(THIS_MODULE, "drv");
79     drv_class_dev = class_device_create(drv_class, NULL, MKDEV(major, 0), NULL, "xyz%d", minor);
80 
81     gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
82     gpfdat = gpfcon + 1;
83     gpgcon = (volatile unsigned long *)ioremap(0x56000060, 16);
84     gpgdat = gpgcon + 1;
85     return 0;
86 }
87 
88 static void drv_exit(void)
89 {
90     unregister_chrdev(major, "drv"); // 卸载
91     class_device_unregister(drv_class_dev);
92     class_destroy(drv_class);
93     iounmap(gpfcon);
94     iounmap(gpgcon);
95 }
96 
97 module_init(drv_init);
98 module_exit(drv_exit);
99 MODULE_LICENSE("GPL");
drv.c

 

改进1如何区分是不同的按键,按下还是松开,并读出按键值?

  要读出引脚的电平,在裸机学习中,可知,可以通过读取对应寄存器,进行移位操作来获取引脚状态

 1         /* 读GPF0,2 */
 2     regval = *gpfdat;
 3     key_vals[0] = (regval & (1<<0)) ? 1 : 0;
 4     key_vals[1] = (regval & (1<<2)) ? 1 : 0;
 5     
 6 
 7     /* 读GPG3,11 */
 8     regval = *gpgdat;
 9     key_vals[2] = (regval & (1<<3)) ? 1 : 0;
10     key_vals[3] = (regval & (1<<11)) ? 1 : 0;        
寄存器移位读取引脚状态

  幸运的是在Linux内部有系统函数中已经有一个这样功能的函数了,直接拿来用就可以了。

  输入系统定义的引脚 pin 对应的宏 S3C24XX_GPIO_BASE(pin), 即可读出引脚电平

1 unsigned int s3c2410_gpio_getpin(unsigned int pin)
2 {
3     void __iomem *base = S3C24XX_GPIO_BASE(pin);
4     unsigned long offs = S3C2410_GPIO_OFFSET(pin);
5 
6     return __raw_readl(base + 0x04) & (1<< offs);
7 }
s3c2410_gpio_getpin(unsigned int pin)

 

为了区分不同的按键,我们给每个按键赋一个键值,同时构造一个结构体数组,来存放这些按键对应的键值

  1.先构造一个结构体 pin_desc

1 struct pin_desc{
2     unsigned int pin;  
3     unsigned int key_val;
4 };

  2.再构造构造一个结构体数组pin_desc[4]

1  /*键值: 按下时,0x01, 0x02, 0x03, 0x04*/
2  /*键值: 松开时,0x81, 0x82, 0x83, 0x84*/
3 struct pin_desc pins_desc[4] = {
4    /*   pin      ,key_val */
5     {S3C2410_GPF0, 0x01},
6     {S3C2410_GPF2, 0x02},
7     {S3C2410_GPG3, 0x03},
8     {S3C2410_GPG11,0x04},
9 };

OK, 那怎么使用这个数组呢?(将引脚的键值,通过注册函数的dev_id 注册进去)

 1 static int third_drv_open(struct inode *inode, struct file *file)
 2 {
 3     //GPF0 GPF2  GPG3 GPG11,使用虚拟地址
 4     /*设置GPF0,2  GPG3,11为中断引脚*/
 5     request_irq(IRQ_EINT0,  buttons_irq, IRQT_BOTHEDGE, "s2",  &pins_desc[0]);
 6     request_irq(IRQ_EINT2,  buttons_irq, IRQT_BOTHEDGE, "s3",  &pins_desc[1]);
 7     request_irq(IRQ_EINT11, buttons_irq, IRQT_BOTHEDGE, "s4",  &pins_desc[2]);
 8     request_irq(IRQ_EINT19, buttons_irq, IRQT_BOTHEDGE, "s5",  &pins_desc[3]);
 9     return 0; 
10 }
third_drv_open
1 int third_drv_close(struct inode *inode, struct file *file)
2 {
3     free_irq(IRQ_EINT0,  &pins_desc[0]);
4     free_irq(IRQ_EINT2,  &pins_desc[1]);
5     free_irq(IRQ_EINT11, &pins_desc[2]);
6     free_irq(IRQ_EINT19, &pins_desc[3]);
7     return 0;
8 }
third_drv_close

引脚的键值已经配置好了,那怎么读出单个引脚状态值所对应的键值呢?

 通过读取 s3c2410_gpio_getpin 

 1 /*确定按键值*/
 2 static irqreturn_t buttons_irq(int irq, void *dev_id)
 3 {
 4     /*在中断处理函数中如何使用pins_desc[4] ?*/
 5     struct pin_desc *pin = (struct pin_desc *)dev_id;
 6     unsigned int pinval;
 7     
 8     /*系统函数,可读出单个引脚状态值,高低电平*/
 9     pinval = s3c2410_gpio_getpin(pin_desc->pin);  /* 1 / 0 */
10 
11     /*确定按键值*/
12     if (pinval)
13     {
14         /* 为1--松开 */
15         key_val = 0x80 | pin_desc->key_val;
16         
17     }
18     else
19     {
20         /* 0--按下 */
21         key_val = pin_desc->key_val;
25     
26     return IRQ_HANDLED;
27 }

  注册号中断引脚之后,发生中断,引脚及键值的地址--&pins_desc[0],就被传入中断处理函数

  如何使用pins_desc[i] 来将引脚传递给 s3c2410_gpio_getpin(unsigned int pin)

  构造结构体指针,利用由request初始化的 dev_id 指针传递参数,第5-9行,通过指向pin_desc的指针,把pin传递给它。

改进2:

  我们使用中断的目的,就是为了在中断发生时,才去读操作,避免像查询一样一直read,从而占据大量的CPU。

  休眠读取:

程序设计目的:App去读取按键值,如果有按键中断触发(键值有改变)则打印,否则休眠.

 

如上框图所示:

  在main函数中,进入while(1)死循环之后,执行read操作,

  若按键值更新,则读取键值

  若未更新,则进入休眠并等待更新,更新后,唤醒进程。

如何设置休眠机制?

  驱动程序中需要设计休眠,中断发生来唤醒首先定义一个等待队列,下述是一个宏

1 //生成一个等待队列头wait_queue_head_t,名字为name
2 DECLARE_WAIT_QUEUE_HEAD(name) 
3 
4 // 定义一个名为`button_waitq`的队列
5 static DECLARE_WAIT_QUEUE_HEAD(button_waitq);

  休眠函数如下,condition=0才休眠,定义在include/linux/wait.h

1 #define wait_event_interruptible(wq, condition)                \
2 ({                                    \
3     int __ret = 0;                            \
4     if (!(condition))                        \
5         __wait_event_interruptible(wq, condition, __ret);    \
6     __ret;                                \
7 })

  唤醒也是一个宏,参数是等待队列,放置在中断函数处理中,定义在include/linux/wait.h

1 wake_up_interruptible(&button_waitq);   /* 唤醒休眠的进程 */

  在read函数中设置

 1 ssize_t third_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
 2 {    
 3   /*size 在此即为sizeof(key_val), 例如:0x01,一个字节大小*/
 4     if(size != 1)
 5     return -EINVAL;
 6 
 7     /*如果没有按键动作,ev_press = 0, 将进程挂在队列里面等待,休眠, 不会执行下面的函数*/
 8     wait_event_interruptible(button_waitq, ev_press);
 9     //谁来唤醒进程?-> 中断发生的时候,就唤醒进程
10     
11     /*/*如果有按键动作,ev_press = 1,会执行到此函数,并返回键值,将变量清零*/*/
12     copy_to_user(buf, &key_val, 1);
13     ev_press = 0;  /*清零,为下一次操作赋初值*/
14     return 1;
15 }

  在third_drv_open中设置

 1 /*确定按键值*/
 2 static irqreturn_t buttons_irq(int irq, void *dev_id)
 3 {
 4     /*在中断处理函数中如何使用pins_desc[4] */
 5     struct pin_desc *pin = (struct pin_desc *)dev_id;
 6     unsigned int pinval;
 7     
 8     /*系统函数,可读出单个引脚状态值,高低电平*/
 9     pinval = s3c2410_gpio_getpin(pin_desc->pin);
10 
11     /*确定按键值*/
12     if (pinval)
13     {
14         /* 为1--松开 */
15         key_val = 0x80 | pin_desc->key_val;
16         
17     }
18     else
19     {
20         /* 0--按下 */
21         key_val = pin_desc->key_val;
22     }
23     ev_press = 1;    /*表示中断发生了*/
24     wake_up_interruptible(&button_waitq);    /*去队列button_waitq里面唤醒休眠的进程*/
25     
26     return IRQ_HANDLED;
27 }

 改进后的完整代码如下:

 1 // app.c
 2 #include <sys/types.h>
 3 #include <sys/stat.h>
 4 #include <fcntl.h>
 5 #include <stdio.h>
 6 #include <unistd.h>
 7 
 8 int main(int argc, char **argv)
 9 {
10     int fd;
11     unsigned char key_val;
12     fd = open("/dev/xyz0", O_RDWR);
13     if (fd < 0)
14     {
15         printf("can't open!\n");
16     }
17     while (1)
18     {
19         read(fd, &key_val, 1);
20         printf("key_val = 0x%x\n", key_val);
21     }
22     return 0;
23 }
forth_drvtest.c

 

  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/interrupt.h>
 13 
 14 volatile unsigned long *gpfcon;
 15 volatile unsigned long *gpfdat;
 16 volatile unsigned long *gpgcon;
 17 volatile unsigned long *gpgdat;
 18 
 19 static struct class *drv_class;
 20 static struct class_device    *drv_class_dev;
 21 
 22 // 定义一个名为`button_waitq`的队列
 23 static DECLARE_WAIT_QUEUE_HEAD(button_waitq);
 24 // flag=1 means irq happened and need to update
 25 int flag=0;
 26 
 27 struct pin_desc{
 28     unsigned int pin;
 29     unsigned int key_val;
 30 };
 31 
 32 /* 键值: 按下时, 0x01, 0x02, 0x03, 0x04 */
 33 /* 键值: 松开时, 0x81, 0x82, 0x83, 0x84 */
 34 static unsigned char key_val;
 35 
 36 struct pin_desc pins_desc[4] = {
 37     {S3C2410_GPF0, 0x01},
 38     {S3C2410_GPF2, 0x02},
 39     {S3C2410_GPG3, 0x03},
 40     {S3C2410_GPG11, 0x04},
 41 };
 42 
 43 static irqreturn_t buttons_irq(int irq, void *dev_id)
 44 {
 45     printk("irq%d\r\n",irq);
 46 
 47     struct pin_desc * pindesc = (struct pin_desc *)dev_id;
 48     unsigned int pinval;
 49     
 50     pinval = s3c2410_gpio_getpin(pindesc->pin);
 51 
 52     if (pinval)
 53     {
 54         /* 松开 */
 55         key_val = 0x80 | pindesc->key_val;
 56     }
 57     else
 58     {
 59         /* 按下 */
 60         key_val = pindesc->key_val;
 61     }
 62 
 63     wake_up_interruptible(&button_waitq);   /* 唤醒休眠的进程 */
 64     flag=1;
 65 
 66     return IRQ_RETVAL(IRQ_HANDLED);
 67 }
 68 
 69 static int drv_open(struct inode *inode, struct file *file)
 70 {
 71     /* 配置GPF0,2为输入引脚 */
 72     /* 配置GPG3,11为输入引脚 */
 73     request_irq(IRQ_EINT0,  buttons_irq, IRQT_BOTHEDGE, "S2", &pins_desc[0]);
 74     request_irq(IRQ_EINT2,  buttons_irq, IRQT_BOTHEDGE, "S3", &pins_desc[1]);
 75     request_irq(IRQ_EINT11, buttons_irq, IRQT_BOTHEDGE, "S4", &pins_desc[2]);
 76     request_irq(IRQ_EINT19, buttons_irq, IRQT_BOTHEDGE, "S5", &pins_desc[3]);    
 77     return 0;
 78 }
 79 
 80 int drv_close(struct inode *inode, struct file *file)
 81 {
 82     free_irq(IRQ_EINT0, &pins_desc[0]);
 83     free_irq(IRQ_EINT2, &pins_desc[1]);
 84     free_irq(IRQ_EINT11,&pins_desc[2]);
 85     free_irq(IRQ_EINT19,&pins_desc[3]);
 86     return 0;
 87 }
 88 
 89 
 90 static ssize_t drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
 91 {
 92     //int minor =  MINOR(file->f_dentry->d_inode->i_rdev);
 93     //printk("drv_write=%d\n",minor);
 94     return 0;
 95 }
 96 
 97 static ssize_t drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
 98 {
 99     if (size != 1)
100     return -EINVAL;
101 
102     /* 如果没有按键动作, 休眠 */
103     wait_event_interruptible(button_waitq, flag);
104 
105     /* 如果有按键动作, 返回键值 */
106     copy_to_user(buf, &key_val, 1);
107     flag = 0;
108     
109     return 1;
110 }
111 
112 
113 static struct file_operations drv_fops = {
114     .owner  =   THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
115     .open   =   drv_open,     
116     .write    =    drv_write,
117     .read    =    drv_read,     
118     .release =  drv_close,  
119 };
120 
121 static int major;
122 static int drv_init(void)
123 {
124     int minor=0;
125     major=register_chrdev(0, "drv", &drv_fops); // 注册, 告诉内核
126     drv_class = class_create(THIS_MODULE, "drv");
127     drv_class_dev = class_device_create(drv_class, NULL, MKDEV(major, 0), NULL, "xyz%d", minor);
128 
129     gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
130     gpfdat = gpfcon + 1;
131     gpgcon = (volatile unsigned long *)ioremap(0x56000060, 16);
132     gpgdat = gpgcon + 1;
133     return 0;
134 }
135 
136 static void drv_exit(void)
137 {
138     unregister_chrdev(major, "drv"); // 卸载
139     class_device_unregister(drv_class_dev);
140     class_destroy(drv_class);
141     iounmap(gpfcon);
142     iounmap(gpgcon);
143 }
144 
145 module_init(drv_init);
146 module_exit(drv_exit);
147 MODULE_AUTHOR("xxx");
148 MODULE_VERSION("0.1.0");
149 MODULE_DESCRIPTION("S3C2410/S3C2440 LED Driver");
150 MODULE_LICENSE("GPL");
forth_drv.c

 

测试

测试运行./text /dev/xyz0 & 后台运行

# ./test /dev/xyz0 &
# irq55
key_val = 0x3
irq55
key_val = 0x83
irq18
key_val = 0x2
irq18
key_val = 0x82
irq16
key_val = 0x1
irq16
key_val = 0x81
irq63
key_val = 0x4
irq63
key_val = 0x84

 

使用top查看占用

  Load average: 0.00 0.01 0.00
  PID  PPID USER     STAT   VSZ %MEM %CPU COMMAND
  782   770 0        R     3096   5%   0% top
  770     1 0        S     3096   5%   0% -sh
  781   770 0        S     1312   2%   0% ./test /dev/xyz0

 

使用ps查看任务为S状态 休眠状态

# ps
  PID  Uid        VSZ Stat Command
    1 0          3092 S   init
  781 0          1312 S   ./test /dev/xyz0
  783 0          3096 R   ps

 

posted @ 2018-12-11 20:50  朱果果  阅读(585)  评论(0编辑  收藏  举报