一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

linux IIC驅(qū)動(dòng)開發(fā)(詳)

 enchen008 2014-04-16

  I2C總線是有Philips公司開發(fā)的,它是一種比較簡(jiǎn)單的總線,接線簡(jiǎn)單:只有兩根線數(shù)據(jù)線(SCL)和時(shí)鐘線(SDA),控制簡(jiǎn)單。所以一些封裝較小的器件多使用I2C總線,常見的使用I2C總線的設(shè)備有EEPROM、rtc及一些傳感器。這里我們介紹下基于linux的I2C設(shè)備驅(qū)動(dòng)的編寫。

I2C設(shè)備驅(qū)動(dòng)的編寫有多種方式:

一種是直接操作CPU的I2C控制器,正對(duì)于某一個(gè)設(shè)備寫一個(gè)字符驅(qū)動(dòng),這種驅(qū)動(dòng)相對(duì)來(lái)說(shuō)比較直接,不需要太依賴于內(nèi)核相關(guān)配置,但是這類設(shè)備驅(qū)動(dòng)依賴CPU,可移植性較差。

一種是基于linux內(nèi)核I2C子系統(tǒng)完成設(shè)備驅(qū)動(dòng)的編寫,一般內(nèi)核會(huì)繼承相關(guān)CPU的控制器驅(qū)動(dòng)即使沒(méi)有也可以通過(guò)技術(shù)支持可以獲得,所以我們只需要使用linux下I2C子系統(tǒng)提供的相關(guān)接口來(lái)構(gòu)建我們的設(shè)備驅(qū)動(dòng)就行了。這樣我們的設(shè)備驅(qū)動(dòng)并不依賴于某一個(gè)特定的CPU,可移植性較好。
在寫驅(qū)動(dòng)之前我們先了解下I2C總線中幾個(gè)比較重要的概念:

1、 地址

I2C總線上可以連接多個(gè)相同或不同的設(shè)備,總線怎么樣才能知道數(shù)據(jù)應(yīng)該發(fā)送到那個(gè)設(shè)備呢,這里需要一個(gè)地址來(lái)唯一的標(biāo)識(shí)一個(gè)設(shè)備。I2C設(shè)備地址有7位地址和10位地址,那么這個(gè)地址是怎么來(lái)的呢,其實(shí)這個(gè)地址我們可以通過(guò)相關(guān)的芯片手冊(cè)獲得,這里通過(guò)一個(gè)EEPROM和一個(gè)溫度傳感器來(lái)說(shuō)明。
EEPROM(AT24C02/04/08/16)芯片手冊(cè)上有如下說(shuō)明:

再結(jié)合原理圖

在通過(guò)芯片手冊(cè)我們可以知道EEPROM的地址的前四位為1010,通過(guò)原理圖A0/A1/及NC的狀態(tài)我們可以知道后三位為000,這樣我們就知道這個(gè)EEPROM在I2C總線上的地址為7’b1010000。

同樣我們可以通過(guò)如下內(nèi)容知道溫度傳感器的地址為7’b1001000

芯片手冊(cè):

原理圖:

2、 時(shí)序

不同的I2C設(shè)備有不同的時(shí)序,我們也可以說(shuō)是不同的協(xié)議,我們需要了解一些時(shí)序相關(guān)的東西,我們發(fā)送數(shù)據(jù)是什么時(shí)候開始什么時(shí)候結(jié)束,怎么發(fā)送都由這個(gè)時(shí)序決定。

開始/停止

完整時(shí)序

現(xiàn)在的CPU多數(shù)都有I2C控制器,我們不需要太關(guān)心具體時(shí)序的實(shí)現(xiàn),這些都由控制器去完成,并且內(nèi)核已經(jīng)集成多數(shù)CPU的I2C控制器驅(qū)動(dòng),我們寫設(shè)備驅(qū)動(dòng)就是按照I2C子系統(tǒng)的要求,為它提供需要的數(shù)據(jù)即可。

I2C子系統(tǒng)下設(shè)備驅(qū)動(dòng)有兩種模式,一種是用戶模式設(shè)備驅(qū)動(dòng)這種驅(qū)動(dòng)依賴I2C子系統(tǒng)中的i2c-dev這個(gè)驅(qū)動(dòng),我們需要在應(yīng)用程序去封裝數(shù)據(jù),這需要應(yīng)用程序的開發(fā)人員具備相當(dāng)?shù)挠布A(chǔ),另外一種是普通的設(shè)備驅(qū)動(dòng)。分別看下這兩種方法的具體實(shí)現(xiàn)過(guò)程。

用戶模式驅(qū)動(dòng)實(shí)現(xiàn):

相關(guān)結(jié)構(gòu)體:

struct i2c_msg {
                __u16 addr; /* slave address */
                __u16 flags;
                #define I2C_M_TEN 0x0010 /* this is a ten bit chip address */
                #define I2C_M_RD 0x0001 /* read data, from slave to master */
                __u16 len; /* msg length */
                __u8 *buf; /* pointer to msg data */
        };
                struct i2c_rdwr_ioctl_data {
                struct i2c_msg *msgs; /* pointers to i2c_msgs */
                __u32 nmsgs; /* number of i2c_msgs */
        };

上面就是我們向底層傳遞的結(jié)構(gòu),我們需要把我們的時(shí)序封裝成這樣的結(jié)構(gòu)然后傳遞下去就行了。

AT24c04時(shí)序

轉(zhuǎn)化為消息結(jié)構(gòu)為:
        e2prom_data.nmsgs=2;
        (e2prom_data.msgs[0]).len=1; //e2prom 目標(biāo)數(shù)據(jù)的地址
        (e2prom_data.msgs[0]).addr=0x50; // e2prom 設(shè)備地址
        (e2prom_data.msgs[0]).flags=0; //write
        (e2prom_data.msgs[0]).buf=(unsigned char*)malloc(2);
        (e2prom_data.msgs[0]).buf[0]=0x0; //e2prom數(shù)據(jù)地址
        (e2prom_data.msgs[1]).len=1; //讀出的數(shù)據(jù)
        (e2prom_data.msgs[1]).addr=0x50; // e2prom 設(shè)備地址
        (e2prom_data.msgs[1]).flags=I2C_M_RD; //read
        (e2prom_data.msgs[1]).buf=(unsigned char*)malloc(1);//存放返回值的地址。
        (e2prom_data.msgs[1]).buf[0]=0; //初始化讀緩沖

這里我們封裝了兩個(gè)消息,在這個(gè)時(shí)序中操作模式改變了,所以我們必須封裝為兩個(gè)時(shí)序,如果操作模式不變封裝一個(gè)消息就可以了比如如下時(shí)序:

  e2prom_data.nmsgs=1;
        (e2prom_data.msgs[0]).len=1; //e2prom 目標(biāo)數(shù)據(jù)的地址
        (e2prom_data.msgs[0]).addr=0x50; // e2prom 設(shè)備地址
        (e2prom_data.msgs[0]).flags=0; //write
        (e2prom_data.msgs[0]).buf=(unsigned char*)malloc(1);
        (e2prom_data.msgs[0]).buf[0]=0x0; //e2prom數(shù)據(jù)地址

接著我們可以看看別的設(shè)備的時(shí)序大家可以發(fā)現(xiàn)大同小異!

我們把剛才封裝的消息通過(guò)ioctl發(fā)下去就能夠完成數(shù)據(jù)的讀寫了。例程如下:
         #include <stdio.h>
         #include <linux/types.h>
         #include <stdlib.h>
         #include <fcntl.h>
         #include <unistd.h>
         #include <sys/types.h>
         #include <sys/ioctl.h>
         #include <errno.h>
         #include <linux/i2c.h>
         #include <linux/i2c-dev.h>

int main()
{
        int fd,ret;
        struct i2c_rdwr_ioctl_data e2prom_data;
        fd=open("/dev/i2c-0",O_RDWR);
                if(fd<0)
                {
                        perror("open error");
                }
        e2prom_data.nmsgs=2; 
        e2prom_data.msgs=(struct i2c_msg*)malloc(e2prom_data.nmsgs*sizeof(struct i2c_msg));
        if(!e2prom_data.msgs)
        {
                perror("malloc error");
                exit(1);
        }
        ioctl(fd,I2C_TIMEOUT,1);/*超時(shí)時(shí)間*/
        ioctl(fd,I2C_RETRIES,2);/*重復(fù)次數(shù)*/
        sleep(1);

        e2prom_data.nmsgs=2;
        (e2prom_data.msgs[0]).len=1; //e2prom 目標(biāo)數(shù)據(jù)的地址
        (e2prom_data.msgs[0]).addr=0x48; // e2prom 設(shè)備地址
        (e2prom_data.msgs[0]).flags=0; //write
        (e2prom_data.msgs[0]).buf=(unsigned char*)malloc(2);
        (e2prom_data.msgs[0]).buf[0]=0x0; //e2prom數(shù)據(jù)地址
        (e2prom_data.msgs[1]).len=2; //讀出的數(shù)據(jù)
        (e2prom_data.msgs[1]).addr=0x48; // e2prom 設(shè)備地址
        (e2prom_data.msgs[1]).flags=I2C_M_RD;//read
        (e2prom_data.msgs[1]).buf=(unsigned char*)malloc(2);//存放返回值的地址。
        (e2prom_data.msgs[1]).buf[0]=0; //初始化讀緩沖
        (e2prom_data.msgs[1]).buf[1]=0; //初始化讀緩沖
        ret=ioctl(fd,I2C_RDWR,(unsigned long)&e2prom_data);
        if(ret<0)
        {
                perror("ioctl error2");
        }
        printf("%x",(e2prom_data.msgs[1]).buf[0]);
        printf("%x\n",(e2prom_data.msgs[1]).buf[1]);

        close(fd);
        return 0;
}

前面我們說(shuō)了如何I2C用戶模式驅(qū)動(dòng),這種驅(qū)動(dòng)基于I2C子系統(tǒng),但是他對(duì)于應(yīng)用程序開發(fā)人員的要求較高,需要應(yīng)用程序開發(fā)人員了解硬件的一些東西,比如時(shí)序,地址等等,而多數(shù)時(shí)候應(yīng)用程序開發(fā)人員是按照操作文件的方法操作設(shè)備,所以我們更希望用一些更簡(jiǎn)單的接口去訪問(wèn)。也就是我們今天的內(nèi)容——基于I2C子系統(tǒng)的字符驅(qū)動(dòng)。

I2C子系統(tǒng)的代碼分為三部分如圖:

Host:主機(jī)控制器驅(qū)動(dòng)

Device:設(shè)備驅(qū)動(dòng)代碼

Core: 核心代碼,提供設(shè)備與控制器的接口

一、主機(jī)控制器驅(qū)動(dòng)

Linux下主機(jī)控制器驅(qū)動(dòng),大多數(shù)是BSP提供的,這里不多說(shuō),簡(jiǎn)單說(shuō)下它主要干的活。I2C主機(jī)控制器在內(nèi)核里又叫適配器,用結(jié)構(gòu)i2c_adapter描述。

struct i2c_adapter {
        struct module *owner;
        unsigned int class;        /* classes to allow probing for */
        const struct i2c_algorithm *algo;        /* the algorithm to access the bus */
        ……
        };

struct i2c_algorithm 提供設(shè)備訪問(wèn)控制器的接口,定義如下:

struct i2c_algorithm {
        int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs,int num);
        int (*smbus_xfer) (struct i2c_adapter *adap, u16 addr,unsigned short flags, char read_write,u8 command, int size, union i2c_smbus_data *data);
        u32 (*functionality) (struct i2c_adapter *);
        };

其中master_xfer就是我們給設(shè)備端提供的接口,這部分內(nèi)容按照芯片手冊(cè)中寄存器的操作實(shí)現(xiàn)數(shù)據(jù)的收發(fā)。

最終我們將i2c_adapter注冊(cè)到系統(tǒng)中,使用如下函數(shù):
        int i2c_add_numbered_adapter(struct i2c_adapter *);

二、核心代碼

這部分就不說(shuō)了,剛才我們介紹的函數(shù)全部是核心代碼提供,它主要是提供標(biāo)準(zhǔn)的統(tǒng)一的接口。

三、設(shè)備代碼

基于I2C的字符驅(qū)動(dòng)的編寫首先我們需要了解幾個(gè)特定的結(jié)構(gòu)。

1、i2c_bus_type

i2c總線結(jié)構(gòu)定義了一些總線相關(guān)的方法,這里我們關(guān)系的是i2c_driver和i2c_client的配備規(guī)則,為什么匹配呢,i2c_client攜帶硬件信息,而i2c_driver只負(fù)責(zé)操作設(shè)備而不管操作的是那個(gè)設(shè)備它需要的硬件信息有i2c_client提供,所以需要i2c_client和i2c_driver協(xié)同操作,而一個(gè)系統(tǒng)中i2c_driver和i2c_client都可能有多個(gè),如何得到自己的另一半就是我所說(shuō)的匹配規(guī)則,i2c_bus_type的匹配規(guī)則定義如下:

struct bus_type i2c_bus_type = {
        .name = "i2c",
        .match = i2c_device_match,
        .probe = i2c_device_probe,
        .remove = i2c_device_remove,
        .shutdown = i2c_device_shutdown,
        .pm = &i2c_device_pm_ops,
        };

static int i2c_device_match(struct device *dev, struct device_driver *drv)
        {
                struct i2c_client *client = i2c_verify_client(dev);
                struct i2c_driver *driver;
                if (!client)
                        return 0;
                /* Attempt an OF style match */
                if (of_driver_match_device(dev, drv))
                        return 1;
                driver = to_i2c_driver(drv);
                /* match on an id table if there is one */
                if (driver->id_table)
                        return i2c_match_id(driver->id_table, client) != NULL;
                return 0;
        }

我們發(fā)現(xiàn)i2c總線的匹配規(guī)則是id或name兩種,id優(yōu)先級(jí)高。

2、板級(jí)結(jié)構(gòu):

struct i2c_board_info {
        char type[I2C_NAME_SIZE];        //芯片類型,其實(shí)也就是名字,用來(lái)匹配
        unsigned short flags;        //標(biāo)志位,一些特定的標(biāo)志
        unsigned short addr;        //地址,從設(shè)備地址,不包括讀寫位
        void *platform_data;        //用來(lái)傳遞一些私有數(shù)據(jù)
        struct dev_archdata *archdata;        //同上
        struct device_node *of_node;
        int        irq;
        };

板子上沒(méi)有一個(gè)I2C的設(shè)備,我們就要注冊(cè)一個(gè)這樣的結(jié)構(gòu)體,到內(nèi)核里邊,這部分代碼一般添加在平臺(tái)代碼里邊,注冊(cè)函數(shù)如下:

i2c_register_board_info(int busnum, struct i2c_board_info const *info,unsigned n);
        busnum 現(xiàn)在很多CPU有多條I2C總線,這個(gè)參數(shù)表示第幾條總線
        info 是一個(gè)結(jié)構(gòu)體數(shù)據(jù),表示我們要注冊(cè)的I2C設(shè)備
        n 表示我們注冊(cè)了幾個(gè)I2C設(shè)備

通過(guò)上面函數(shù)就能把設(shè)備注冊(cè)到系統(tǒng)中。結(jié)構(gòu)如圖:

3、i2c_client

這個(gè)結(jié)構(gòu)我們不需要操作,是操作系統(tǒng)即核心代碼自動(dòng)完成,這個(gè)過(guò)程其實(shí)是在注冊(cè)i2c_adapter的時(shí)候完成的。即在函數(shù)i2c_add_numbered_adapter中完成,最終i2c_client攜帶者i2c_board_info和i2c_adapter的信息。

4、i2c_driver

這部分代碼主要負(fù)責(zé)注冊(cè)i2c_driver和匹配相應(yīng)的i2c_client。I2c_driver定義如下:

struct i2c_driver {
        int (*probe)(struct i2c_client *, const struct i2c_device_id *);
        int (*remove)(struct i2c_client *);
        struct device_driver driver;
        const struct i2c_device_id *id_table;
        ……
        };

注冊(cè)函數(shù)如下:
        int i2c_add_driver(struct i2c_driver *driver);

這個(gè)函數(shù)負(fù)責(zé)注冊(cè)i2c_driver并匹配i2c_client,當(dāng)匹配到了對(duì)于的i2c_client,probe函數(shù)被執(zhí)行,并且i2c_client被以參數(shù)的形式傳遞過(guò)來(lái)。我們可以通過(guò)i2c_client提供的硬件信息和操作接口操作我們想要的設(shè)備。

5、數(shù)據(jù)傳輸

數(shù)據(jù)傳輸結(jié)構(gòu):

struct i2c_msg {
        __u16 addr; /* slave address */
        __u16 flags;
        #define I2C_M_TEN         0x0010 /* this is a ten bit chip address */
        #define I2C_M_RD        0x0001 /* read data, from slave to master */
        #define I2C_M_NOSTART        0x4000 /* if I2C_FUNC_PROTOCOL_MANGLING */
        #define I2C_M_REV_DIR_ADDR        0x2000 /* if I2C_FUNC_PROTOCOL_MANGLING */
        #define I2C_M_IGNORE_NAK        0x1000 /* if I2C_FUNC_PROTOCOL_MANGLING */
        #define I2C_M_NO_RD_ACK        0x0800 /* if I2C_FUNC_PROTOCOL_MANGLING */
        #define I2C_M_RECV_LEN        0x0400 /* length will be first received byte */
        __u16 len;        /* msg length */
        __u8 *buf;        /* pointer to msg data */
        };

消息的封裝與上節(jié)用戶模式驅(qū)動(dòng)相似,封裝好消息使用如下函數(shù)提交給核心代碼,最終通過(guò)控制器驅(qū)動(dòng)發(fā)送給具體的設(shè)備。

int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);
        adap 適配器,由client->adapter獲得。
        msgs 消息
        num 消息個(gè)數(shù)

通過(guò)上面內(nèi)容我們就可以構(gòu)建我們的基于linux下i2c子系統(tǒng)的設(shè)備驅(qū)動(dòng)了,例程如下:

平臺(tái)代碼添加:

static struct i2c_board_info i2c_devs0[] __initdata = {
                {I2C_BOARD_INFO("lm75", 0x48),},
        };
        i2c_register_board_info(0, i2c_devs0, ARRAY_SIZE(i2c_devs0));

驅(qū)動(dòng)代碼:

#include < linux/module.h>
        #include < linux/kernel.h>
        #include < linux/init.h>
        #include < linux/fs.h>
        #include < linux/cdev.h>
        #include < linux/i2c.h>
        #include < linux/slab.h>
        #include < asm/uaccess.h>
        MODULE_LICENSE ("GPL");
        #define LM75_REG_CONF         0x01
        static const u8 LM75_REG_TEMP[3] = {
        0x00,         /* input */
        0x03,         /* max */
        0x02,         /* hyst */
        };
        struct lm75_data 
        {
                u16 temp[3]; /* Register values,
                0 = input
                1 = max
                2 = hyst */
        };
        static int lm75_major = 250;
        static int lm75_minor = 0;
        static int number_of_devices = 1;
        static dev_t devno = 0;
        static struct cdev cdev;
        static struct i2c_client *new_client;
        struct lm75_data *data;
        static int lm75_read_value(struct i2c_client *client)
        {
                struct i2c_msg msgs[2];
                int status;
                char buf1[2];
                char buf2[2];
                msgs[0].len = 1;
                msgs[0].addr = client->addr; // lm75 設(shè)備地址
                msgs[0].flags = 0;//write
                msgs[0].buf = buf1;
                msgs[0].buf[0] = LM75_REG_TEMP[0];
                msgs[1].len = 2;//讀出的數(shù)據(jù)
                msgs[1].addr = client->addr;// lm75 設(shè)備地址 
                msgs[1].flags = I2C_M_RD;//read
                msgs[1].buf = buf2;//存放返回值的地址。
                status = i2c_transfer(client->adapter, msgs, 2);
                if(status < 0)
                        return status;
                printk("1 = %2x %2x\n", buf2[0], buf2[1]);
                return (buf2[0] << 8) | buf2[1];
        }
        static ssize_t lm75_read(struct file *file, char __user *buff, size_t count, loff_t *offset) 
        {
                int status;
                status = lm75_read_value(new_client);
                if(status < 0)
                        {
                                return status;
                        }
                printk("status = %x\n", status);
                if(copy_to_user(buff, (char *)&status, sizeof(status)))
                        return -EFAULT;
                return 0;
                }
        static int lm75_open(struct inode *inode, struct file *file)
        {
                return 0;
        }
        static int lm75_release(struct inode *inode, struct file *file)
        {
                return 0;
        }
        static struct file_operations lm75_fops = {
                .owner = THIS_MODULE,
                .read = lm75_read,
                .open = lm75_open,
                .release = lm75_release,
        };
        static int lm75_probe(struct i2c_client *client, const struct i2c_device_id *id)
        {
                int ret = 0;
                new_client = client;
                devno = MKDEV(lm75_major, lm75_minor);
                ret = register_chrdev_region(devno, number_of_devices, "lm75");
                if(ret)
                        {
                                printk("failed to register device number\n");
                                goto err_register_chrdev_region;
                        }
                cdev_init(&cdev, &lm75_fops);
                cdev.owner = THIS_MODULE;
                ret = cdev_add(&cdev, devno, number_of_devices);
                if(ret)
                        {
                                printk("failed to add device\n");
                                goto err_cdev_add;
                        }
                return 0;
                err_cdev_add:
                unregister_chrdev_region(devno, number_of_devices);
                err_register_chrdev_region:
                kfree(data);
                return ret;
        }
        static int lm75_remove(struct i2c_client *client)
        {
                cdev_del(&cdev);
                unregister_chrdev_region(devno, number_of_devices);
                return 0;
        }
        enum lm75_type {        /* keep sorted in alphabetical order */
                lm75,
                lm75a,
        };
        static const struct i2c_device_id lm75_ids[] = {
                { "lm75", lm75, },
                { "lm75a", lm75a, },
                { /* LIST END */ }
        };
        static struct i2c_driver lm75_driver = {
                .driver = {
                        .name = "lm75",
                        },
                .probe = lm75_probe,
                .remove = lm75_remove,
                .id_table = lm75_ids,
        };
        static int __init s5pc100_lm75_init(void)
        {
                return i2c_add_driver(&lm75_driver);
        }
        static void __exit s5pc100_lm75_exit(void)
        {
                i2c_del_driver(&lm75_driver);
        }
        module_init(s5pc100_lm75_init);
        module_exit(s5pc100_lm75_exit); 

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多

    国产又大又硬又粗又湿| 国产色一区二区三区精品视频| 欧美一区二区口爆吞精| 精品国产品国语在线不卡| 国产午夜福利片在线观看| 殴美女美女大码性淫生活在线播放| 福利视频一区二区三区| 欧美黑人在线一区二区| 丝袜av一区二区三区四区五区| 好吊色欧美一区二区三区顽频| 国产精品超碰在线观看| 国产又粗又长又大的视频| 欧美一区二区日韩一区二区| 在线观看日韩欧美综合黄片| 日韩特级黄片免费在线观看| 久久国产亚洲精品赲碰热| 粗暴蹂躏中文一区二区三区| 福利视频一区二区三区| 国产精品欧美激情在线播放| 麻豆欧美精品国产综合久久| 果冻传媒精选麻豆白晶晶| 六月丁香六月综合缴情| 精品女同一区二区三区| 日本精品中文字幕在线视频| 粉嫩内射av一区二区| 日韩无套内射免费精品| 深夜视频在线观看免费你懂 | 日本和亚洲的香蕉视频| 丝袜诱惑一区二区三区| 精品欧美一区二区三久久| 麻豆精品在线一区二区三区| 国产不卡视频一区在线| 中文字幕亚洲人妻在线视频| 99久久国产精品免费| 欧美成人黄色一级视频| 欧美人妻一区二区三区| 日本道播放一区二区三区| 国产av精品高清一区二区三区| 亚洲丁香婷婷久久一区| 欧美小黄片在线一级观看| 99视频精品免费视频|