zoukankan      html  css  js  c++  java
  • 平台总线 —— 平台总线驱动模型

    目录

      1、为什么会有平台总线?

      2、平台总线三要素

      3、平台总线编程接口

      4、编写能在多平台下使用的led驱动

    1、为什么会有平台总线?

     1     用于平台升级:三星: 2410, 2440, 6410, s5pc100  s5pv210  4412
     2         硬件平台升级的时候,部分的模块的控制方式,基本上是类似的
     3         但是模块的地址是不一样
     4 
     5         gpio控制逻辑: 1, 配置gpio的输入输出功能: gpxxconf
     6                       2, 给gpio的数据寄存器设置高低电平: gpxxdata
     7                     逻辑操作基本上是一样的
     8                     但是地址不一样
     9         
    10         uart控制:1,设置8n1(数据位、奇偶校验位、停止位),115200, no AFC(流控)
    11                     UCON,ULCON, UMODOEN, UDIV
    12                 
    13                 逻辑基本上是一样的
    14                 但是地址不一样
    15 
    16 问题:
    17     当soc升级的时候, 对于相似的设备驱动,需要编写很多次(如果不用平台总线)
    18     但是会有大部分重复代码
    19 
    20 解决:引入平台总线    
    21         device(中断/地址)和driver(操作逻辑) 分离
    22     在升级的时候,只需要修改device中信息即可(中断/地址)
    23     实现一个driver代码能够驱动多个平台相似的模块,并且修改的代码量很少

    2、平台总线三要素 —— platform_bus、device、driver·

      platform会存在/sys/bus/里面

     如下图所示, platform目录下会有两个文件,分别就是platform设备和platform驱动

     

           device设备

      挂接在platform总线下的设备, 使用结构体platform_device描述

        driver驱动

      挂接在platform总线下,是个与某种设备相对于的驱动, 使用结构体platform_driver描述

        platform总线

      是个全局变量,为platform_bus_type,属于虚拟设备总线,通过这个总线将设备和驱动联系起来,属于Linux中bus的一种。

      以下依次介绍其数据结构:

      1)device

     1 struct platform_device {
     2     const char  * name;  //设备名称,要与platform_driver的name一样,这样总线才能匹配成功
     3     u32             id;  //插入总线下相同name的设备编号(一个驱动可以有多个设备),如果只有一个设备填-1
     4     struct device   dev; //具体的device结构体,继承了device父类
     5                          //成员platform_data可以给平台driver提供各种数据(比如:GPIO引脚等等)
     6     u32    num_resources;//资源数目
     7     struct resource    * resource;//资源描述,用来描述io,内存等
     8 };
     9 
    10 //资源文件 ,定义在includelinuxioport.h
    11 struct resource {
    12     resource_size_t start;  //起始资源,如果是地址的话,必须是物理地址
    13     resource_size_t end;    //结束资源,如果是地址的话,必须是物理地址
    14     const char *name;       //资源名
    15     unsigned long flags;    //资源类型,可以是io/irq/mem等
    16     struct resource *parent, *sibling, *child;    //链表结构,可以构成链表
    17 };
    18 // type
    19 #define IORESOURCE_IO         0x00000100    /* Resource type */
    20 #define IORESOURCE_MEM        0x00000200
    21 #define IORESOURCE_IRQ        0x00000400
    22 #define IORESOURCE_DMA        0x00000800

    注册和注销

    1 int  platform_device_register(struct platform_device * pdev);
    2 void  platform_device_unregister(struct platform_device * pdev)

    2)driver

     1 struct platform_driver {
     2         int (*probe)(struct platform_device *); //匹配成功之后被调用的函数
     3         int (*remove)(struct platform_device *);//device移除的时候调用的函数
     4         struct device_driver driver; //继承了driver父类
     5                             |
     6                             const char        *name;
     7         const struct platform_device_id *id_table; //如果driver支持多个平台,在列表中写出来
     8 }

        注册与注销

    1  int platform_driver_register(struct platform_driver *drv);
    2 void platform_driver_unregister(struct platform_driver *drv);

    3)platform_bus

     1 struct bus_type platform_bus_type = {              
     2 .name             = "platform",         //设备名称
     3 .dev_attrs        = platform_dev_attrs, //设备属性、含获取sys文件名,该总线会放在/sys/bus下
     4 .match            = platform_match,     //匹配设备和驱动,匹配成功就调用driver的.probe函数
     5 .uevent           = platform_uevent,    //消息传递,比如热插拔操作
     6 .suspend          = platform_suspend,   //电源管理的低功耗挂起
     7 .suspend_late     = platform_suspend_late,  
     8 .resume_early     = platform_resume_early,
     9 .resume           = platform_resume,   //恢复
    10 };
    11 

      匹配方法:match

     1  static int platform_match(struct device *dev, struct device_driver *drv)
     2 {
     3 //1,优先匹配pdriver中的id_table,里面包含了支持不同的平台的名字
     4 //2,直接匹配driver中名字和device中名字
     5   
     6     struct platform_device *pdev = to_platform_device(dev);
     7     struct platform_driver *pdrv = to_platform_driver(drv);
     8 
     9     if (pdrv->id_table)  // 如果pdrv中有idtable,平台列表名字和pdev中的名字
    10         return platform_match_id(pdrv->id_table, pdev) != NULL; 
    11 
    12     /* fall-back to driver name match */
    13     return (strcmp(pdev->name, drv->name) == 0);
    14 
    15 }    

      如何实现?

    在pdev与pdrv指向的device、driver结构体中,各自都有一个成员 -- 父类结构体,实际上向总线注册的是这个父类结构体指针。通过container_of,可以通过成员找到包含该成员的整个结构体。

    1 #define to_platform_device(x)   container_of((x), struct platform_device, dev)
    3 #define to_platform_driver(drv) (container_of((drv), struct platform_driver, driver))

    3、平台总线编程接口

      1) pdev 注册和注销

    1 int platform_device_register(struct platform_device * pdev);
    2 void  platform_device_unregister(struct platform_device * pdev);

      2)pdrv注册与注销

    1 int platform_device_register(struct platform_device * pdev);
    2 void  platform_device_unregister(struct platform_device * pdev);

      3)获取资源数据(对资源的定义可以参考内核/arch/arm/mach-xxx.c文件)

    1 int platform_get_irq(struct platform_device * dev,unsigned int num);                                        
    2 struct resource * platform_get_resource_byname(struct platform_device * dev,unsigned int type,const char * name);

     

     4、编写能在多平台下使用的LED驱动

       1)注册一个platform_device,定义资源:地址和中断

    1 struct resource {
    2     resource_size_t start;// 开始
    3     resource_size_t end;  //结束
    4     const char *name;     //描述,自定义
    5     unsigned long flags; //区分当前资源描述的是中断(IORESOURCE_IRQ)还是内存(IORESOURCE_MEM)
    6     struct resource *parent, *sibling, *child;
    7 };

      2)注册一个platform_driver,实现操作失败的代码

     1    注册完毕,同时如果和pdev匹配成功,自动调用probe方法:
     2          probe方法: 对硬件进行操作
     3               a,注册设备号,并且注册fops--为用户提供一个设备标示,同时提供文件操作io接口
     4               b,创建设备节点
     5               c,初始化硬件
     6                       ioremap(地址);  //地址从pdev需要获取
     7                       readl/writle();
     8               d,实现各种io接口: xxx_open, xxx_read, ..
     9 

     

     1 获取资源的方式:        
     2  //获取资源
     3  struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num)
     4 {
     5     int i;
     6 
     7     for (i = 0; i < dev->num_resources; i++) {
     8         struct resource *r = &dev->resource[i];
     9 
    10         if (type == resource_type(r) && num-- == 0)
    11             return r;
    12     }
    13     return NULL;
    14 }
    15 // 参数1: 从哪个pdev中获取资源
    16 // 参数2:  资源类型
    17 // 参数3: 表示获取同种资源的第几个(0,1,2,3.....)
    18 //返回值:返回一个指针,指向想获取的资源项

     示例:编写平台驱动,实现最基本的匹配

     1 #include <linux/init.h>
     2 #include <linux/module.h>
     3 #include <linux/ioport.h>
     4 #include <plat/irqs.h>
     5 #include <linux/platform_device.h>
     6 
     7 
     8 static int led_pdrv_probe(struct platform_device * pdev)
     9 {
    10     printk("--------------%s-------------
    ",__FUNCTION__);
    11     return 0;
    12 }
    13 
    14 
    15 static int led_pdrv_remove(struct platform_device * pdev)
    16 {
    17 
    18     return 0;
    19 }
    20 
    21 
    22 //id_table:平台的id列表,包含支出不同平台的名字
    23 const struct platform_device_id led_id_table[] = {
    24     {"exynos_4412_led", 0x4444}, 
    25     {"s3c2410_led",    0x2244},
    26     {"s5pv210_led",    0x3344},
    27 };
    28 
    29 struct platform_driver led_pdrv = {
    30     .probe  = led_pdrv_probe,
    31     .remove = led_pdrv_remove,
    32     .driver = {  
    33         .name = "samsung_led_drv",
    34         //可以用作匹配
    35         // /sys/bus/platform/drivers/samsung_led_drv
    36     },
    37     .id_table = led_id_table,
    38 
    39 };
    40 
    41 
    42 static int __init plat_led_drv_init(void)
    43 {
    44     printk("---------------%s---------------
    ",__FUNCTION__);
    45     //注册一个平台驱动
    46     return platform_driver_register(&led_pdrv);
    47 
    48 }
    49 
    50 static void __exit plat_led_drv_exit(void)
    51 {
    52 
    53     platform_driver_unregister(&led_pdrv);
    54 }
    55 
    56 
    57 
    58 module_init(plat_led_drv_init);
    59 module_exit(plat_led_drv_exit);
    60 
    61 MODULE_LICENSE("GPL");
    plat_led_pdrv.c
     1 #include <linux/init.h>
     2 #include <linux/module.h>
     3 #include <linux/ioport.h>
     4 #include <plat/irqs.h>
     5 #include <linux/platform_device.h>
     6 
     7 #define GPIO_REG_BASE 0X11400000
     8 
     9 #define GPF3_CON GPIO_REG_BASE + 0x01E0
    10 #define GPF3_SIZE 24   //定义6个(一组)寄存器的连续空间
    11 
    12 #define GPX1_CON GPIO_REG_BASE + 0x01E0
    13 #define GPX1_SIZE 24   //定义6个(一组)寄存器的连续空间
    14 
    15 
    16 //一个设备可能有多个资源
    17 struct resource led_res[] = {
    18     [0] = {
    19         .start = GPF3_CON,
    20         .end   = GPF3_CON + GPF3_SIZE -1,
    21         .flags = IORESOURCE_MEM,
    22     },
    23     
    24     [1] = {
    25         .start = GPX1_CON,
    26         .end   = GPX1_CON + GPX1_SIZE -1,
    27         .flags = IORESOURCE_MEM,
    28     },
    29 
    30     //有些设备也有中断资源,仅说明中断资源使用
    31     [2] = {  
    32         .start = 4,   //#define IRQ_EINT4      S3C2410_IRQ(36)       /* 52 */
    33         .end   = 4,   //中断没有连续地址概念,开始与结束都是中断号
    34         .flags = IORESOURCE_IRQ, 
    35     },
    36 
    37 };
    38 
    39 
    40 
    41 struct platform_device led_pdev = {
    42     .name = "exynos_4412_led",  //用于匹配的设备名称  /sys/bus/platform/devices
    43     .id   = -1,    //表示只有一个设备
    44     .num_resources = ARRAY_SIZE(led_res), //资源数量,ARRAY_SIZE()函数:获取数量
    45     .resource      = led_res,
    46     
    47 
    48 };
    49 
    50 
    51 
    52 static int __init plat_led_dev_init(void)
    53 {
    54     //注册一个平台设备
    55     return platform_device_register(&led_pdev);
    56 
    57 }
    58 
    59 static void __exit plat_led_dev_exit(void)
    60 {
    61 
    62     platform_device_unregister(&led_pdev);
    63 }
    64 
    65 module_init(plat_led_dev_init);
    66 module_exit(plat_led_dev_exit);
    67 
    68 MODULE_LICENSE("GPL");
    plat_led_pdev.c

       测试:

     进一步完善:在驱动程序中,实现了probe方法,获取硬件资源,完成led设备初始化,实现了上层调用的IO接口。实现了应用程序对硬件的控制。

      1 #include <linux/init.h>
      2 #include <linux/module.h>
      3 #include <linux/ioport.h>
      4 #include <plat/irqs.h>
      5 #include <linux/platform_device.h>
      6 #include <linux/of.h>
      7 #include <linux/of_irq.h>
      8 #include <linux/interrupt.h>
      9 #include <linux/slab.h>
     10 #include <linux/fs.h>
     11 #include <linux/device.h>
     12 #include <linux/kdev_t.h>
     13 #include <linux/err.h>
     14 #include <asm/io.h>
     15 #include <asm/uaccess.h>
     16 
     17 
     18 
     19 //设计一个全局变量
     20 struct led_dev{
     21     int dev_major;
     22     struct class *cls;
     23     struct device *dev;   //这个dev是用来创建设备文件的,不同与注册到bus的dev
     24     struct resource *res; //获取到的某类资源
     25     void*  reg_base;  //映射后的虚拟地址
     26 };
     27 
     28 struct led_dev *samsung_led;
     29 
     30 ssize_t led_pdrv_write(struct file *filp, const char __user * buf, size_t count, loff_t *fops)
     31 {
     32     int val;
     33     int ret; 
     34     
     35     ret = copy_from_user(&val, buf, count);
     36     if(ret > 0)
     37     {
     38         printk("copy_from_user failed
    ");
     39         return -EFAULT;
     40     }
     41 
     42     if(val){
     43 
     44         writel(readl(samsung_led->reg_base + 4) |  (0x3<<4) , samsung_led->reg_base+4);
     45     
     46     }else{
     47 
     48         writel(readl(samsung_led->reg_base + 4) & ~(0x3<<4) , samsung_led->reg_base+4);
     49     
     50     }
     51 
     52     return 0;
     53     
     54 }
     55 
     56 int led_pdrv_open(struct inode *inode, struct file *filp)
     57 {
     58     printk("--------------%s-------------
    ",__FUNCTION__);
     59     //open相关的初始化也可以在open中完成,或在probe
     60     return 0;
     61 }
     62 
     63 int led_pdrv_close(struct inode *inode, struct file *filp)
     64 {
     65     printk("--------------%s-------------
    ",__FUNCTION__);
     66     return 0;
     67 }
     68 
     69 
     70 
     71 //给上层应用提供文件IO接口
     72 const struct file_operations led_fops = {
     73     .open    = led_pdrv_open,
     74     .write   = led_pdrv_write,
     75     .release = led_pdrv_close,
     76 
     77 }; 
     78 
     79 static int led_pdrv_probe(struct platform_device * pdev)
     80 {
     81     printk("--------------%s-------------
    ",__FUNCTION__);
     82     
     83     samsung_led = kzalloc(sizeof(struct led_dev), GFP_KERNEL);
     84     if(samsung_led == NULL)
     85     {
     86         printk("kzalloc failed
    ");
     87         return -ENOMEM;
     88     }
     89     
     90     
     91     /*    对硬件进行操作
     92      *    a,注册设备号,并且注册fops--为用户提供一个设备标示,同时提供文件操作io接口
     93      *    b,创建设备节点
     94      *    c,初始化硬件
     95      *         ioremap(地址);  //地址从pdev需要获取
     96      *         readl/writle();
     97      *    d,实现各种io接口: xxx_open, xxx_read, ..
     98      */
     99 
    100     //a. 注册设备号,动态
    101     samsung_led->dev_major = register_chrdev(0, "led_drv", &led_fops);
    102 
    103     //b. 创建设备节点
    104     samsung_led->cls = class_create(THIS_MODULE, "led_new_cls");
    105     samsung_led->dev = device_create(samsung_led->cls, NULL, MKDEV(samsung_led->dev_major,0), NULL, "led0");
    106 
    107     //c. 初始化硬件
    108 
    109      //获取资源 
    110      //参数1:从哪个pdev中获取资源
    111      //参数2:指定资源类型
    112      //参数3:表示获取同种资源的第几个,(0,1,2,...)
    113     samsung_led->res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    114     int irqno = platform_get_irq(pdev, 0);
    115         //等同于platform_get_resource(pdev, IORESOURCE_IRQ, 0);
    116     printk("-------get irqno = %d-------
    ", irqno);
    117         
    118      //大小 = end - start + 1; 加1是因为地址从0读起,eg: 7~0 = 7-0+1
    119      //resource_size : samsung_led->res->end - samsung_led->res->start + 1 
    120     samsung_led->reg_base = ioremap(samsung_led->res->start,resource_size(samsung_led->res));
    121 
    122     //对寄存器进行配置 -- 输出功能
    123     writel((readl(samsung_led->reg_base) & ~(0xff<<16)) | (0x11<<16), samsung_led->reg_base);
    124     
    125     
    126     return 0;
    127 }
    128 
    129 //与probe是一对儿,做了与probe相反的事
    130 static int led_pdrv_remove(struct platform_device * pdev)
    131 {
    132     iounmap(samsung_led->reg_base);
    133     class_destroy(samsung_led->cls);
    134     device_destroy(samsung_led->cls, MKDEV(samsung_led->dev_major,0));
    135     unregister_chrdev(samsung_led->dev_major, "led_drv");
    136     kfree(samsung_led);
    137     return 0;
    138 }
    139 
    140 
    141 //id_table:平台的id列表,包含支出不同平台的名字
    142 const struct platform_device_id led_id_table[] = {
    143     {"exynos_4412_led", 0x4444}, 
    144     {"s3c2410_led",    0x2244},
    145     {"s5pv210_led",    0x3344},
    146 };
    147 
    148 struct platform_driver led_pdrv = {
    149     .probe  = led_pdrv_probe,
    150     .remove = led_pdrv_remove,
    151     .driver = {  
    152         .name = "samsung_led_drv",
    153         //可以用作匹配
    154         //创建/sys/bus/platform/drivers/samsung_led_drv
    155     },
    156     .id_table = led_id_table,
    157 
    158 };
    159 
    160 
    161 static int __init plat_led_drv_init(void)
    162 {
    163     printk("---------------%s---------------
    ",__FUNCTION__);
    164     //注册一个平台驱动
    165     return platform_driver_register(&led_pdrv);
    166 
    167 }
    168 
    169 static void __exit plat_led_drv_exit(void)
    170 {
    171 
    172     platform_driver_unregister(&led_pdrv);
    173 }
    174 
    175 
    176 module_init(plat_led_drv_init);
    177 module_exit(plat_led_drv_exit);
    178 
    179 MODULE_LICENSE("GPL");
    plat_led_pdrv.c
     1 #include <linux/init.h>
     2 #include <linux/module.h>
     3 #include <linux/ioport.h>
     4 #include <plat/irqs.h>
     5 #include <linux/platform_device.h>
     6 
     7 #define GPIO_REG_BASE 0X11400000
     8 
     9 #define GPF3_CON GPIO_REG_BASE + 0x01E0
    10 #define GPF3_SIZE 24   //定义6个(一组)寄存器的连续空间
    11 
    12 #define GPX1_CON GPIO_REG_BASE + 0x01E0
    13 #define GPX1_SIZE 24   //定义6个(一组)寄存器的连续空间
    14 
    15 
    16 //一个设备可能有多个资源
    17 struct resource led_res[] = {
    18     [0] = {
    19         .start = GPF3_CON,
    20         .end   = GPF3_CON + GPF3_SIZE -1,
    21         .flags = IORESOURCE_MEM,
    22     },
    23     
    24     [1] = {
    25         .start = GPX1_CON,
    26         .end   = GPX1_CON + GPX1_SIZE -1,
    27         .flags = IORESOURCE_MEM,
    28     },
    29 
    30     //有些设备也有中断资源,仅说明中断资源使用
    31     [2] = {  
    32         .start = 4,   //#define IRQ_EINT4      S3C2410_IRQ(36)       /* 52 */
    33         .end   = 4,   //中断没有连续地址概念,开始与结束都是中断号
    34         .flags = IORESOURCE_IRQ, 
    35     },
    36 
    37 };
    38 
    39 
    40 
    41 struct platform_device led_pdev = {
    42     .name = "exynos_4412_led",  //用于匹配的设备名称  /sys/bus/platform/devices
    43     .id   = -1,    //表示只有一个设备
    44     .num_resources = ARRAY_SIZE(led_res), //资源数量,ARRAY_SIZE()函数:获取数量
    45     .resource      = led_res,
    46     
    47 
    48 };
    49 
    50 
    51 
    52 static int __init plat_led_dev_init(void)
    53 {
    54     //注册一个平台设备
    55     return platform_device_register(&led_pdev);
    56 
    57 }
    58 
    59 static void __exit plat_led_dev_exit(void)
    60 {
    61 
    62     platform_device_unregister(&led_pdev);
    63 }
    64 
    65 module_init(plat_led_dev_init);
    66 module_exit(plat_led_dev_exit);
    67 
    68 MODULE_LICENSE("GPL");
    plat_led_pdev.c
     1 #include <stdio.h>
     2 #include <stdlib.h>
     3 #include <string.h>
     4 #include <sys/types.h>
     5 #include <sys/stat.h>
     6 #include <fcntl.h>
     7 #include <unistd.h>
     8 
     9 
    10 
    11 int main(int argc, char * argv[])
    12 {
    13     int fd;
    14 
    15     int on = 0;
    16     
    17     fd = open("/dev/led0", O_RDWR);
    18     if(fd < 0)
    19     {
    20         perror("open
    ");
    21         exit(1);
    22     }
    23 
    24     while(1)
    25     {
    26         on = 0;
    27         write(fd, &on, 4);
    28         sleep(1);
    29 
    30         on = 1;
    31         write(fd, &on, 4);
    32         sleep(1);
    33     }
    34     
    35 
    36     return 0;
    37 }
    led_test.c
     1 ROOTFS_DIR = /home/linux/source/rootfs#根文件系统路径
     2 
     3 APP_NAME = led_test
     4 MODULE_NAME  = plat_led_pdev
     5 MODULE_NAME1 = plat_led_pdrv
     6 
     7 CROSS_COMPILE = /home/linux/toolchains/gcc-4.6.4/bin/arm-none-linux-gnueabi-
     8 CC = $(CROSS_COMPILE)gcc
     9 
    10 ifeq ($(KERNELRELEASE),)
    11 
    12 KERNEL_DIR = /home/linux/kernel/linux-3.14-fs4412          #编译过的内核源码的路径
    13 CUR_DIR = $(shell pwd)     #当前路径
    14 
    15 all:
    16     make -C $(KERNEL_DIR) M=$(CUR_DIR) modules  #把当前路径编成modules
    17     $(CC) $(APP_NAME).c -o $(APP_NAME)
    18     @#make -C 进入到内核路径
    19     @#M 指定当前路径(模块位置)
    20 
    21 clean:
    22     make -C $(KERNEL_DIR) M=$(CUR_DIR) clean
    23 
    24 install:
    25     sudo cp -raf *.ko $(APP_NAME) $(ROOTFS_DIR)/drv_module     #把当前的所有.ko文件考到根文件系统的drv_module目录
    26 
    27 else
    28 
    29 obj-m += $(MODULE_NAME).o    #指定内核要把哪个文件编译成ko
    30 obj-m += $(MODULE_NAME1).o
    31 
    32 endif
    Makefile

    测试结果:

     在运行led_test后,可以看见led闪烁,查看platform总线下的devices,drivers目录,也分别注册

    了设备与驱动

     

     小结:

      平台设备驱动模型,主要是用于平台升级的,可以在平台升级时,主要修改设备相关文件即可,对于设备驱动,尽可能重用。

      平台设备驱动来源于设备驱动模型,只是平台总线由系统实现了而已。

      平台设备代码里面主要实现:将设备注册到总线,填充设备相关的硬件资源,例如内存资源,中断资源;

      在平台驱动里,最先做的也是将驱动注册到总线,然后对硬件进行一系列初始化,那么硬件信息从哪里来?在设备驱动注册到总线后,总线会将设备的platform_device中的成指针dev传给设备驱动,设备驱动通过成员就可获取到结构体的数据,访问到设备的资源,从而进行硬件相关操作,这些操作都可以在探测函数中完成。此外,设备驱动还要为应用层提供操作设备的接口fops。简言之:注册、硬件初始化、获取设备资源,实现上层接口,注销。

  • 相关阅读:
    this关键字
    方法重载和方法重写
    构造方法
    Java数据类型以及变量的定义
    Java 语言概述
    Java Socket通信
    类图
    JAVA语言编程注意事项
    Scanner--控制台输入
    java运算符
  • 原文地址:https://www.cnblogs.com/y4247464/p/12406182.html
Copyright © 2011-2022 走看看