您的位置:首页 > 运维架构 > Linux

ARM-Linux下ZLG7290 I2C驱动程序的编写

2011-04-02 10:37 302 查看
项目中用到ZLG7260按键数码显示芯片与at91sam9260系列ARM9处理器连接,ZLG7290通过I2C接口与ARM9的TWI I2C(PA23:SDA数据线 ;PA24:SCL时钟线)接口相连连接,另外还有一个中断信号引脚(INT)与ARM的一个GPIO引脚(PB30)连接,具体连接电路由于公司需要技术保密不贴出来了。本周通过学习Linux I2C体系架构,完成这一驱动程序,下面将一些开发心得贴出来和大家分享。


首先来看什么是I2C
I2C (Inter-Integrated Circuit)总线是一种由PHILIPS公司开发的两线式串行总线,用于连接微控制器及其外围设备。I2C总线最主要的优点是其简单性和有效性。由于接口直接在组件之上,因此I2C总线占用的空间非常小,减少了电路板的空间和芯片管脚的数目,降低了互联本钱。I2C总线最初为音频和视频设备开发,现已应用于各种服务与治理场合,来实现配置或把握组件的功能状态,如电源、系统风扇、系统温度等参数,增加了系统的安全性,方便了治理。
I2C总线是由数据线SDA和时钟SCL构成的串行总线,可发送和接收数据,每个器件都有一个惟一的地址识别。I2C 规程运用主/从双向通讯。器件发送数据到总线上,则定义为发送器,器件接收数据则定义为接收器。主器件和从器件都可以工作于接收和发送状态。总线必须由主器件(通常为微控制器)控制,主器件产生串行时钟(SCL)控制总线的传输方向,并产生起始和停止条件。SDA线上的数据状态仅在SCL为低电平的期间才能改变,SCL为高电平的期间,SDA状态的改变被用来表示起始和停止条件。
I2C总线在传送数据过程中共有三种类型信号,它们分别是:开始信号、结束信号和应答信号。开始信号:SCL为高电平时,SDA由高电平向低电平跳变,开始传送数据。结束信号:SCL为低电平时,SDA由低电平向高电平跳变,结束传送数据。应答信号:接收数据的IC在接收到8bit数据后,向发送数据的IC发出特定的低电平脉冲,表示已收到数据。CPU向受控单元发出一个信号后,等待受控单元发出一个应答信号,CPU接收到应答信号后,根据实际情况作出是否继续传递信号的判定。若未收到应答信号,由判定为受控单元出现故障。
再来看Linux内核提供的I2C构架





内核中i2c相关代码可以分为三个层次:
1. i2c框架:i2c.h和i2c-core.c为i2c框架的主体,提供了核心数据结构的定义、i2c适配器驱动和设备驱动的注册、注销管理,i2c通信方法上层的、与具体适配器无关的代码、检测设备地址的上层代码等;i2c-dev.c用于创建i2c适配器的/dev/i2c/%d设备节点,提供i2c设备访问方法等。
2. i2c总线适配器驱动:定义描述具体i2c总线适配器的i2c_adapter数据结构、实现在具体i2c适配器上的i2c总线通信方法,并由i2c_algorithm数据结构进行描述。
3 i2c设备驱动:定义描述具体设备的i2c_client和可能的私有数据结构、借助i2c框架的i2c_probe函数实现注册设备的attach_adapter方法、提供设备可能使用的地址范围、以及设备地址检测成功后创建i2c_client数据结构的回调函数。

具体到Linux内核的对应文件来看:
在Linux驱动的i2c文件夹下有algos,busses,chips三个文件夹,另外还有i2c-core.c和i2c-dev.c两个文件。其中i2c-core.c文件实现了I2C core框架,是Linux内核用来维护和治理的I2C的核心部分,其中维护了两个静态的LiST,分别记录系统中的I2C driver结构和I2C adapter结构。I2C core提供接口函数,对应一个I2C adatper。I2C driver和I2C client初始化时在I2C core中进行注册,以及退出时进行注销。同时还提供了I2C总线读写访问的一般接口,主要应用在I2C设备驱动中。
  Busses文件夹下的i2c-at91.c文件实现了at91sam9260下I2C总线适配器驱动,定义描述了具体的I2C总线适配器的i2c_adapter数据结构,实现比较底层的对I2C总线访问的具体方法。I2C adapter 构造一个对I2C core层接口的数据结构,并通过接口函数向I2C core注册一个控制器。I2C adapter主要实现对I2C总线访问的算法,iic_xfer() 函数就是I2C adapter底层对I2C总线读写方法的实现。同时I2C adpter 中还实现了对I2C控制器中断的处理函数。
  i2c-dev.c文件中实现了I2C driver,提供了一个通用的I2C设备的驱动程序,实现了字符类型设备的访问接口,实现了对用户应用层的接口,提供用户程序访问I2C设备的接口,包括实现open,release,read,write以及最重要的ioctl等标准文件操纵的接口函数。我们可以通过open函数打开 I2C的设备文件,通过ioctl函数设定要访问从设备的地址,然后就可以通过 read和write函数完成对I2C设备的读写操纵。
  通过I2C driver提供的通用方法可以访问任何一个I2C的设备,但是其中实现的read,write及ioctl等功能完全是基于一般设备的实现,所有的操纵数据都是基于字节流,没有明确的格式和意义。为了更方便和有效地使用I2C设备,我们可以为一个具体的I2C设备开发特定的I2C设备驱动程序,在驱动中完成对特定的数据格式的解释以及实现一些专用的功能。


下面以ZLG7290 I2C驱动为例,说明如何开发具体的Linux I2C设备驱动程序
程序分为三个部分,I2C相关部分,中断接口部分和字符设备接口部分。
首先是I2C相关部分:
1、自定义一个zlg7290结构体,封装了I2C Client和字符设备描述cdev。

1、自定义一个zlg7290结构体,封装了I2C Client和字符设备描述cdev。
struct zlg7290
{
struct cdev cdev;
struct class_device *class_dev;
struct i2c_client client;

};
2、定义I2C driver结构,
static struct i2c_driver zlg7290_driver = {
.driver = {
.name   = "zlg7290",
},
.id             = I2C_DRIVERID_ZLG7290,
.attach_adapter = zlg7290_attach_adapter,
.detach_client  = zlg7290_detach_client,
};
3、定义ZLG7290设备地址范围
#define ADDR_ZLG7290 0x38  /* ZLG7290 address 0x38*/

static unsigned short normal_i2c[] = { ADDR_ZLG7290, I2C_CLIENT_END };

I2C_CLIENT_INSMOD_1(zlg7290);
其中normal_i2c数组指明设备可能使用的地址。另外还可以指定normal_i2c¬_range[]、probe[]、probe_range、ignore[]、ignore_range、force[]等数组,其含义详见“writing-clients”文档。然后,由I2C_CLIENT_INSMOD宏声明该模块导出的与地址区间有关的可变参数,并创建静态变量addr_data
4、定义方法zlg7290_attach_adapter:
static int zlg7290_attach_adapter(struct i2c_adapter *adapter)
{
return i2c_probe(adapter, &addr_data, zlg7290_detect);
}
该函数在ZLG7290设备驱动程序模块的初始化时调用,针对系统上每个i2c总线都被调用一次,尝试认领该总线上所有使用这个驱动的i2c设备。另外,这个函数只需直接调用在i2c-core.c文件中实现的i2c_probe函数即可,传递的第三个参数为由驱动程序模块提供的回调函数ZLG7290_detect_client。
5、在ltc3445.c文件中定义回调函数ZLG7290_detect_client
static int zlg7290_detect(struct i2c_adapter *adapter, int address, int kind)
{
struct i2c_client *client;
struct zlg7290  *ctr_zlg7290;
int ret = 0;

if (!(ctr_zlg7290 = kmalloc(sizeof(struct zlg7290), GFP_KERNEL)))
return -ENOMEM;

client = &ctr_zlg7290->client;
i2c_set_clientdata(client, ctr_zlg7290);
client->addr = address;
client->adapter = adapter;
client->driver = &zlg7290_driver;
client->flags = 0;

strlcpy(client->name, "zlg7290", I2C_NAME_SIZE);

if ((ret = i2c_attach_client(client)))
goto exit_kfree;

cdev_init(&zlg7290->cdev, &zlg7290_fops);
eep->cdev.owner = THIS_MODULE;
ret = cdev_add(&ctr_zlg7290->cdev, MKDEV(MAJOR(zlg7290_devt), zlg7290_minor), 1);
if (ret < 0) {
dev_err(&client->dev, "register char device failed/n");
goto exit_detach;
}

ctr_zlg7290->->class_dev = class_device_create(zlg7290_class, NULL,
MKDEV(MAJOR(zlg7290_devt),
zlg7290_minor),
&client->dev, "zlg7290%d",
zlg7290_minor);
if (IS_ERR(ctr_zlg7290->class_dev)) {
dev_err(&client->dev, "can not create class device/n");
ret = PTR_ERR(ctr_zlg7290->class_dev);
goto exit_cdev_del;
}

dev_info(&client->dev, "device at /dev/zlg7290%d (%d:%d)/n",
zlg7290_minor, MAJOR(zlg7290_devt), zlg7290_minor);

zlg7290_minor++;

return 0;

exit_cdev_del:
cdev_del(&ctr_zlg7290->cdev);
exit_detach:
i2c_detach_client(client);
exit_kfree:
kfree(ctr_zlg7290);
return ret;
}
在检测到设备时调用该函数,传递的第一个参数为当前i2c适配器的i2c_adapter数据结构的地址,第二个参数为设备地址。该函数创建设备的i2c_client数据结构并直接调用i2c-core.c中实现的i2c_attach_client函数向i2c适配器数据结构i2c_adapter.clients指针数组注册,最后调用与具体i2c设备相关的代码初始化设备,并完成i2c_client中私有数据结构的初始化。
6、定义zlg7290_detach_client方法,在断开设备是调用该函数:
static int zlg7290_detach_client(struct i2c_client *client)
{
int ret;

ret = i2c_detach_client(client);
if (ret) return ret;
kfree(i2c_get_clientdata(client));

return 0;
}


其次是字符设备接口部分
这一部分和普通的字符设备驱动类似,提供用户空间调用的接口,在read,write等函数中调用底层的I2C函数完成数据传输。

/*****************Character device File operations application interface part begin********/
static int zlg7290_hw_write(struct  zlg7290 *ctr_zlg7290,  int len, size_t *retlen, char *buf)
{
struct i2c_client *client = &ctr_zlg7290->client;
unsigned char tbuf[2]; /*tbuf[0] is register address , tbuf[1] is the value  to write in */
unsigned short i;
tbuf[0] = buf[0];
tbuf[1] = buf[1];

if (i2c_master_send(client, tbuf, 2) != 2)
{
dev_err(&client->dev, "i2c write error/n");
return -EIO;
}

return 0;
}

static int zlg7290_hw_read(struct zlg7290 *ctr_zlg7290 , int len, size_t *retlen, char *buf)
{
struct i2c_client *client = &ctr_zlg7290 ->client;
struct i2c_msg msg[] =
{
{ client->addr, 0, 2, buf },   /*the buf contains register address*/
{ client->addr, I2C_M_RD, len, buf },/*the buf contains register value*/
};

if ((i2c_transfer(client->adapter, msg, 2)) != 2)
{
dev_err(&client->dev, "i2c read error/n");
return -EIO;
}

*retlen = len;

return 0;
}
#endif
/* -------------------------------------------------------------------------- */

static int zlg7290_open(struct inode *inode, struct file *file)
{
int err;

err = request_irq(zlg7290_IRQ->irq,zlg7290INT_handler,zlg7290_IRQ->flags,zlg7290_IRQ->name,(void*)&key_press);
if (err)
{
free_irq(zlg7290_IRQ->irq,(void*)&key_press);
return  -EBUSY;
}

struct zlg7290  *ctr_zlg7290 = container_of(inode->i_cdev, struct zlg7290, cdev);

file->private_data = ctr_zlg7290;

return 0;
}

static int zlg7290_release(struct inode *inode, struct file *file)
{
file->private_data = NULL;

free_irq(zlg7290_IRQ->irq,(void*)&key_press);

return 0;
}

static ssize_t zlg7290_writereg(struct file *file, const char __user *buf,
size_t count, loff_t *fpos)
{
struct zlg7290  *ctr_zlg7290 = file->private_data;
char *kbuf;
size_t retlen = 0;
int ret = 0;

if (!count) return 0;

kbuf = kmalloc(count, GFP_KERNEL);
if (kbuf == NULL)
return -ENOMEM;

/*(Cuntianrui Warning)the *buf is buf[2] in user mode , buf[0] is register address and buf[1] is register value*/
if (copy_from_user(kbuf,buf,count))
{
kfree(kbuf);
return -EFAULT;
}

ret = zlg7290_hw_write(ctr_zlg7290, count, &retlen, kbuf);
if (ret)
{
kfree(kbuf);
return ret;
}

kfree(kbuf);
return retlen;
}

static ssize_t zlg7290_readreg(struct file *file, char __user *buf, size_t count, loff_t *fpos)
{

struct zlg7290 *ctr_zlg7290 = file->private_data;
size_t retlen = 0;
int ret = 0;
char *kbuf;

if (!count) return 0;

/*if key_press = 0 , sleep here*/
wait_event_interruptible(keyRead_waitq, key_press);
/*running to this line , key_press is 1*/
key_press = 0;

kbuf = kmalloc(count, GFP_KERNEL);
if (kbuf == NULL)
return -ENOMEM;
/*(Cuntianrui Warning)buf is in user mode,and need put register address in it before calling read function
The function return register content in buf */
if (copy_from_user(kbuf, buf, 1))
return -EFAULT;

ret = zlg7290_hw_read(ctr_zlg7290, count,  &retlen, kbuf);
if (ret) {
kfree(kbuf);
return ret;
}

if (copy_to_user(buf,kbuf,count))
{
kfree(kbuf);
return -EFAULT;
}
kfree(kbuf);
return retlen;
}

static struct file_operations zlg7290_fops =
{
.owner    = THIS_MODULE,
.read     = zlg7290_readreg,
.write    = zlg7290_writereg,
.open     = zlg7290_open,
.release  = zlg7290_release,
};


最后是中断处理部分

该部分和我在《ARMLinux GPIO中断程序》一文中所讲类似,不在赘述,仅贴出代码。

struct gpio_irq_desc
{
int irq;
unsigned long flags;
char *name;
};

static struct gpio_irq_desc zlg7290_IRQ={AT91_PIN_PB30,AT91_AIC_SRCTYPE_LOW,"zlg7290irq"};

static irqreturn_t zlg7290INT_handler(int irq,void *dev_id)
{
key_press =1;
wake_up_interruptible(&keyRead_waitq);
return IRQ_RETVAL(IRQ_HANDLED);
}


文章结束贴出完整代码

/*
ZLG7290 Support for Atmel AT91sam9260

Copyright (C) Mar.29.2011 CunTianrui Changsha Ruiwei Co.Ltd
Email:cskywit@163.com

*/

#include <linux/module.h>
#include <linux/version.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/pci.h>
#include <linux/types.h>
#include <linux/delay.h>
#include <linux/i2c.h>
#include <linux/init.h>
#include <linux/clk.h>
#include <linux/platform_device.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/poll.h>
#include <linux/sched.h>
#include <linux/jiffies.h>
#include <linux/interrupt.h>

#include <asm/io.h>
#include <asm/arch/at91_twi.h>
#include <asm/arch/board.h>
#include <asm/arch/cpu.h>
#include <asm/arch/gpio.h>
#include <asm/uaccess.h>

#include <asm/arch/at91_pio.h>
#include <asm/arch/at91_aic.h>
#include <asm/arch/at91_pmc.h>

#include "zlg7290.h"

#define ADDR_ZLG7290 0x38 /* ZLG7290 address 0x38*/

static unsigned short normal_i2c[] = { ADDR_ZLG7290, I2C_CLIENT_END };

I2C_CLIENT_INSMOD_1(zlg7290);

/*zlg7290 INT definition*/

static DECLARE_WAIT_QUEUE_HEAD(keyRead_waitq);//INT button half
static volatile int key_press =0;

static void zlg7290INT_init(void)
{
at91_set_gpio_input(AT91_PIN_PB30, 1);
at91_set_deglitch(AT91_PIN_PB30, 1);
at91_sys_write(1 + PIO_IDR, 1<<30);
at91_sys_write(1 + PIO_IER, (~(1<<30)));
at91_sys_write(AT91_PMC_PCER, 1 << 3);
}

struct gpio_irq_desc { int irq; unsigned long flags; char *name; }; static struct gpio_irq_desc zlg7290_IRQ={AT91_PIN_PB30,AT91_AIC_SRCTYPE_LOW,"zlg7290irq"}; static irqreturn_t zlg7290INT_handler(int irq,void *dev_id) { key_press =1; wake_up_interruptible(&keyRead_waitq); return IRQ_RETVAL(IRQ_HANDLED); }
/*zlg7290 I2C device definition*/
static dev_t zlg7290_devt;
static int zlg7290_minor = 0;
static struct class *zlg7290_class;

/*zlg7290 device struct*/
struct zlg7290
{
struct cdev cdev;
struct class_device *class_dev;
struct i2c_client client;

};

/*****************Character device File operations application interface part begin********/ static int zlg7290_hw_write(struct zlg7290 *ctr_zlg7290, int len, size_t *retlen, char *buf) { struct i2c_client *client = &ctr_zlg7290->client; unsigned char tbuf[2]; /*tbuf[0] is register address , tbuf[1] is the value to write in */ unsigned short i; tbuf[0] = buf[0]; tbuf[1] = buf[1]; if (i2c_master_send(client, tbuf, 2) != 2) { dev_err(&client->dev, "i2c write error/n"); return -EIO; } return 0; } static int zlg7290_hw_read(struct zlg7290 *ctr_zlg7290 , int len, size_t *retlen, char *buf) { struct i2c_client *client = &ctr_zlg7290 ->client; struct i2c_msg msg[] = { { client->addr, 0, 2, buf }, /*the buf contains register address*/ { client->addr, I2C_M_RD, len, buf },/*the buf contains register value*/ }; if ((i2c_transfer(client->adapter, msg, 2)) != 2) { dev_err(&client->dev, "i2c read error/n"); return -EIO; } *retlen = len; return 0; } #endif /* -------------------------------------------------------------------------- */ static int zlg7290_open(struct inode *inode, struct file *file) { int err; err = request_irq(zlg7290_IRQ->irq,zlg7290INT_handler,zlg7290_IRQ->flags,zlg7290_IRQ->name,(void*)&key_press); if (err) { free_irq(zlg7290_IRQ->irq,(void*)&key_press); return -EBUSY; } struct zlg7290 *ctr_zlg7290 = container_of(inode->i_cdev, struct zlg7290, cdev); file->private_data = ctr_zlg7290; return 0; } static int zlg7290_release(struct inode *inode, struct file *file) { file->private_data = NULL; free_irq(zlg7290_IRQ->irq,(void*)&key_press); return 0; } static ssize_t zlg7290_writereg(struct file *file, const char __user *buf, size_t count, loff_t *fpos) { struct zlg7290 *ctr_zlg7290 = file->private_data; char *kbuf; size_t retlen = 0; int ret = 0; if (!count) return 0; kbuf = kmalloc(count, GFP_KERNEL); if (kbuf == NULL) return -ENOMEM; /*(Cuntianrui Warning)the *buf is buf[2] in user mode , buf[0] is register address and buf[1] is register value*/ if (copy_from_user(kbuf,buf,count)) { kfree(kbuf); return -EFAULT; } ret = zlg7290_hw_write(ctr_zlg7290, count, &retlen, kbuf); if (ret) { kfree(kbuf); return ret; } kfree(kbuf); return retlen; } static ssize_t zlg7290_readreg(struct file *file, char __user *buf, size_t count, loff_t *fpos) { struct zlg7290 *ctr_zlg7290 = file->private_data; size_t retlen = 0; int ret = 0; char *kbuf; if (!count) return 0; /*if key_press = 0 , sleep here*/ wait_event_interruptible(keyRead_waitq, key_press); /*running to this line , key_press is 1*/ key_press = 0; kbuf = kmalloc(count, GFP_KERNEL); if (kbuf == NULL) return -ENOMEM; /*(Cuntianrui Warning)buf is in user mode,and need put register address in it before calling read function The function return register content in buf */ if (copy_from_user(kbuf, buf, 1)) return -EFAULT; ret = zlg7290_hw_read(ctr_zlg7290, count, &retlen, kbuf); if (ret) { kfree(kbuf); return ret; } if (copy_to_user(buf,kbuf,count)) { kfree(kbuf); return -EFAULT; } kfree(kbuf); return retlen; } static struct file_operations zlg7290_fops = { .owner = THIS_MODULE, .read = zlg7290_readreg, .write = zlg7290_writereg, .open = zlg7290_open, .release = zlg7290_release, };/*****************Character device File operations application interface part End********/

/*****************ZLG7290 I2C driver interface part Begin**************************/

static int zlg7290_attach_adapter(struct i2c_adapter *adapter);
static int zlg7290_detect(struct i2c_adapter *adapter,int address,int kind);
static int zlg7290_detach_client(struct i2c_client *client);

/* this is the driver that will be inserted */
static struct i2c_driver zlg7290_driver = {
.driver = {
.name = "zlg7290",
},
.id = I2C_DRIVERID_ZLG7290,
.attach_adapter = zlg7290_attach_adapter,
.detach_client = zlg7290_detach_client,
};

static int zlg7290_attach_adapter(struct i2c_adapter *adapter)
{
return i2c_probe(adapter, &addr_data, zlg7290_detect);
}

static int zlg7290_detect(struct i2c_adapter *adapter, int address, int kind)
{
struct i2c_client *client;
struct zlg7290 *ctr_zlg7290;
int ret = 0;

if (!(ctr_zlg7290 = kmalloc(sizeof(struct zlg7290), GFP_KERNEL)))
return -ENOMEM;

client = &ctr_zlg7290->client;
i2c_set_clientdata(client, ctr_zlg7290);
client->addr = address;
client->adapter = adapter;
client->driver = &zlg7290_driver;
client->flags = 0;

strlcpy(client->name, "zlg7290", I2C_NAME_SIZE);

if ((ret = i2c_attach_client(client)))
goto exit_kfree;

cdev_init(&zlg7290->cdev, &zlg7290_fops);
eep->cdev.owner = THIS_MODULE;
ret = cdev_add(&ctr_zlg7290->cdev, MKDEV(MAJOR(zlg7290_devt), zlg7290_minor), 1);
if (ret < 0) {
dev_err(&client->dev, "register char device failed/n");
goto exit_detach;
}

ctr_zlg7290->->class_dev = class_device_create(zlg7290_class, NULL,
MKDEV(MAJOR(zlg7290_devt),
zlg7290_minor),
&client->dev, "zlg7290%d",
zlg7290_minor);
if (IS_ERR(ctr_zlg7290->class_dev)) {
dev_err(&client->dev, "can not create class device/n");
ret = PTR_ERR(ctr_zlg7290->class_dev);
goto exit_cdev_del;
}

dev_info(&client->dev, "device at /dev/zlg7290%d (%d:%d)/n",
zlg7290_minor, MAJOR(zlg7290_devt), zlg7290_minor);

zlg7290_minor++;

return 0;

exit_cdev_del:
cdev_del(&ctr_zlg7290->cdev);
exit_detach:
i2c_detach_client(client);
exit_kfree:
kfree(ctr_zlg7290);
return ret;
}

static int zlg7290_detach_client(struct i2c_client *client)
{
int ret;

ret = i2c_detach_client(client);
if (ret) return ret;
kfree(i2c_get_clientdata(client));

return 0;
}

static int __init zlg7290_init(void)
{
int ret;

i2c_eeprom_class = class_create(THIS_MODULE, "zlg7290");
if (IS_ERR(zlg7290_class)) return PTR_ERR(zlg7290_class);
ret = alloc_chrdev_region(&zlg7290_devt, 0, 5, "zlg7290");
if (ret < 0)
{
printk(KERN_ERR "failed to allocate char dev region/n");
class_destroy(zlg7290_class);
return ret;
}
zlg7290INT_init();

return i2c_add_driver(&zlg7290_driver);
}

static void __exit zlg7290_exit(void)
{
class_destroy(zlg7290_class);
unregister_chrdev_region(zlg7290_devt, 5);
i2c_del_driver(&zlg7290_driver);
}

/*****************ZLG7290 I2C driver interface part END**************************/

MODULE_AUTHOR("CunTianrui <cskywit@163.com>");
MODULE_DESCRIPTION("zlg7290 driver");
MODULE_LICENSE("GPL");

module_init(zlg7290_init);
module_exit(zlg7290_exit);
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: