Linux驱动开发之i2c框架讲解到例程

这篇具有很好参考价值的文章主要介绍了Linux驱动开发之i2c框架讲解到例程。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

        本篇章在rk3399平台上,基于设备树的i2c驱动开发。i2c直接使用硬件i2c总线,体系结构分为3部分:I2C 核心、I2C 总线驱动和I2C 设备驱动。I2C 核心(i2c-core.c)提供了I2C 总线驱动和设备驱动的注册、注销方法等。我们主要了解Linux中i2c的基本框架,分为i2c主机驱动开发i2c设备驱动开发。主机驱动一般由芯片原厂开发,通常需要我们做的就是针对具体某个设备的设备驱动开发,硬件设备信息通过设备树描述。

1. i2c主机驱动框架

1.1 结构体描述

        i2c适配器驱动开发中,要用到两个重要的数据结构: i2c_adapteri2c_algorithm,结构体定义在 include/linux/i2c.h文件中。

i2c_adapter结构体中主要关注const struct i2c_algorithm *algo和struct device dev;dev对应具体i2c设备,查询设备树的对应节点。

i2c_algorithm结构体对外提供读写API函数;master_xfer就是 I2C适配器的传输函数,可以通过此函数来完成与 IIC设备之间的通信。smbus_xfer就是 SMBUS(系统管理)总线的传输函数。

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 */
	void *algo_data;

	/* data fields that are valid for all devices	*/
	struct rt_mutex bus_lock;

	int timeout;			/* in jiffies */
	int retries;
	struct device dev;		/* the adapter device */

	int nr;
	char name[48];
	struct completion dev_released;

	struct mutex userspace_clients_lock;
	struct list_head userspace_clients;

	struct i2c_bus_recovery_info *bus_recovery_info;
	const struct i2c_adapter_quirks *quirks;
};


struct i2c_algorithm {
	/* If an adapter algorithm can't do I2C-level access, set master_xfer
	   to NULL. If an adapter algorithm can do SMBus access, set
	   smbus_xfer. If set to NULL, the SMBus protocol is simulated
	   using common I2C messages */
	/* master_xfer should return the number of messages successfully
	   processed, or a negative value on error */
	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);

	/* To determine what the adapter supports */
	u32 (*functionality) (struct i2c_adapter *);

#if IS_ENABLED(CONFIG_I2C_SLAVE)
	int (*reg_slave)(struct i2c_client *client);
	int (*unreg_slave)(struct i2c_client *client);
#endif
};

1.2 相关函数

注册函数:填充好 i2c_adapter结构体变量和设置完 i2c_algorithm中的 master_xfer函数后,需要向系统注册适配器驱动,函数原型如下(都可以注册,二选一):

int i2c_add_adapter(struct i2c_adapter *adapter)        //使用动态的总线号

int i2c_add_numbered_adapter(struct i2c_adapter *adap)    //使用静态的总线号

返回值: 0,成功;负值,失败。

注销函数:如果要删除 I2C适配器的话使用 i2c_del_adapter函数即可,函数原型如下:

void i2c_del_adapter(struct i2c_adapter * adap)

1.3 浅析i2c适配器驱动源码

        在内核中我们要怎么去查找到源码文件呢?可以通过设备树i2c节点中compatible字符串查找。例如i2c1中的"rockchip,rk3399-i2c",Linux内核中全局搜索该字符串可找到适配器驱动文件为

i2c-rk3x.c 。

Linux驱动开发之i2c框架讲解到例程

Linux驱动开发之i2c框架讲解到例程

        解析rk3x_i2c_probe函数:of_match_node查找节点,后面填充adapter结构体变量,platform_get_resource获取节点IORESOURCE_MEM属性资源,devm_ioremap_resource对寄存器基地址进行内存映射,platform_get_irq、devm_request_irq获取并申请中断,rk3x_i2c_adapt_div设置i2c时钟,最后是进行注册i2c_add_adapter。

static int rk3x_i2c_probe(struct platform_device *pdev)
{
	struct device_node *np = pdev->dev.of_node;
	const struct of_device_id *match;
	struct rk3x_i2c *i2c;
	struct resource *mem;
	int ret = 0;
	int bus_nr;
	u32 value;
	int irq;
	unsigned long clk_rate;

	i2c = devm_kzalloc(&pdev->dev, sizeof(struct rk3x_i2c), GFP_KERNEL);
	if (!i2c)
		return -ENOMEM;

	match = of_match_node(rk3x_i2c_match, np);
	i2c->soc_data = (struct rk3x_i2c_soc_data *)match->data;

	/* use common interface to get I2C timing properties */
	i2c_parse_fw_timings(&pdev->dev, &i2c->t, true);

	strlcpy(i2c->adap.name, "rk3x-i2c", sizeof(i2c->adap.name));
	i2c->adap.owner = THIS_MODULE;
	i2c->adap.algo = &rk3x_i2c_algorithm;
	i2c->adap.retries = 3;
	i2c->adap.dev.of_node = np;
	i2c->adap.algo_data = i2c;
	i2c->adap.dev.parent = &pdev->dev;

	i2c->dev = &pdev->dev;

	spin_lock_init(&i2c->lock);
	init_waitqueue_head(&i2c->wait);

	i2c->i2c_restart_nb.notifier_call = rk3x_i2c_restart_notify;
	i2c->i2c_restart_nb.priority = 128;
	ret = register_i2c_restart_handler(&i2c->i2c_restart_nb);
	if (ret) {
		dev_err(&pdev->dev, "failed to setup i2c restart handler.\n");
		return ret;
	}

	mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	i2c->regs = devm_ioremap_resource(&pdev->dev, mem);
	if (IS_ERR(i2c->regs))
		return PTR_ERR(i2c->regs);

	/* Try to set the I2C adapter number from dt */
	bus_nr = of_alias_get_id(np, "i2c");

	/*
	 * Switch to new interface if the SoC also offers the old one.
	 * The control bit is located in the GRF register space.
	 */
	if (i2c->soc_data->grf_offset >= 0) {
		struct regmap *grf;

		grf = syscon_regmap_lookup_by_phandle(np, "rockchip,grf");
		if (IS_ERR(grf)) {
			dev_err(&pdev->dev,
				"rk3x-i2c needs 'rockchip,grf' property\n");
			return PTR_ERR(grf);
		}

		if (bus_nr < 0) {
			dev_err(&pdev->dev, "rk3x-i2c needs i2cX alias");
			return -EINVAL;
		}

		/* 27+i: write mask, 11+i: value */
		value = BIT(27 + bus_nr) | BIT(11 + bus_nr);

		ret = regmap_write(grf, i2c->soc_data->grf_offset, value);
		if (ret != 0) {
			dev_err(i2c->dev, "Could not write to GRF: %d\n", ret);
			return ret;
		}
	}

	/* IRQ setup */
	irq = platform_get_irq(pdev, 0);
	if (irq < 0) {
		dev_err(&pdev->dev, "cannot find rk3x IRQ\n");
		return irq;
	}

	ret = devm_request_irq(&pdev->dev, irq, rk3x_i2c_irq,
			       0, dev_name(&pdev->dev), i2c);
	if (ret < 0) {
		dev_err(&pdev->dev, "cannot request IRQ\n");
		return ret;
	}

	platform_set_drvdata(pdev, i2c);

	if (i2c->soc_data->calc_timings == rk3x_i2c_v0_calc_timings) {
		/* Only one clock to use for bus clock and peripheral clock */
		i2c->clk = devm_clk_get(&pdev->dev, NULL);
		i2c->pclk = i2c->clk;
	} else {
		i2c->clk = devm_clk_get(&pdev->dev, "i2c");
		i2c->pclk = devm_clk_get(&pdev->dev, "pclk");
	}

	if (IS_ERR(i2c->clk)) {
		ret = PTR_ERR(i2c->clk);
		if (ret != -EPROBE_DEFER)
			dev_err(&pdev->dev, "Can't get bus clk: %d\n", ret);
		return ret;
	}
	if (IS_ERR(i2c->pclk)) {
		ret = PTR_ERR(i2c->pclk);
		if (ret != -EPROBE_DEFER)
			dev_err(&pdev->dev, "Can't get periph clk: %d\n", ret);
		return ret;
	}

	ret = clk_prepare(i2c->clk);
	if (ret < 0) {
		dev_err(&pdev->dev, "Can't prepare bus clk: %d\n", ret);
		return ret;
	}
	ret = clk_prepare(i2c->pclk);
	if (ret < 0) {
		dev_err(&pdev->dev, "Can't prepare periph clock: %d\n", ret);
		goto err_clk;
	}

	i2c->clk_rate_nb.notifier_call = rk3x_i2c_clk_notifier_cb;
	ret = clk_notifier_register(i2c->clk, &i2c->clk_rate_nb);
	if (ret != 0) {
		dev_err(&pdev->dev, "Unable to register clock notifier\n");
		goto err_pclk;
	}

	clk_rate = clk_get_rate(i2c->clk);
	rk3x_i2c_adapt_div(i2c, clk_rate);

	ret = i2c_add_adapter(&i2c->adap);
	if (ret < 0) {
		dev_err(&pdev->dev, "Could not register adapter\n");
		goto err_clk_notifier;
	}

	dev_info(&pdev->dev, "Initialized RK3xxx I2C bus at %p\n", i2c->regs);

	return 0;

err_clk_notifier:
	clk_notifier_unregister(i2c->clk, &i2c->clk_rate_nb);
err_pclk:
	clk_unprepare(i2c->pclk);
err_clk:
	clk_unprepare(i2c->clk);
	return ret;
}

2. i2c设备驱动开发

2.1 设备驱动结构体描述

        i2c设备驱动重点关注两个数据结构: i2c_clienti2c_driver。 i2c_client描述设备信息,一个设备对应一个i2c_client变量,i2c_driver类似platform_driver,描述驱动方法。

        如果使用设备树的话,需要设置 i2c_driver中的device_driver的of_match_table成员变量,跟设备树的 (compatible)属性对应。

struct i2c_client {
	unsigned short flags;		/* div., see below		*/
	unsigned short addr;		/* i2c芯片地址(低7位)	*/

	char name[I2C_NAME_SIZE];
	struct i2c_adapter *adapter;	/* 指向i2c适配器	*/
	struct device dev;		/* the device structure		*/
	int irq;			/* irq issued by device		*/
	struct list_head detected;
#if IS_ENABLED(CONFIG_I2C_SLAVE)
	i2c_slave_cb_t slave_cb;	/* callback for slave mode	*/
#endif
};


struct i2c_driver {
	unsigned int class;

	int (*attach_adapter)(struct i2c_adapter *) __deprecated;

	/* Standard driver model interfaces */
	int (*probe)(struct i2c_client *, const struct i2c_device_id *);
	int (*remove)(struct i2c_client *);

	/* driver model interfaces that don't relate to enumeration  */
	void (*shutdown)(struct i2c_client *);

	/* Alert callback, for example for the SMBus alert protocol.
	 * The format and meaning of the data value depends on the protocol.
	 * For the SMBus alert protocol, there is a single bit of data passed
	 * as the alert response's low bit ("event flag").
	 */
	void (*alert)(struct i2c_client *, unsigned int data);


	int (*command)(struct i2c_client *client, unsigned int cmd, void *arg);

	struct device_driver driver;
	const struct i2c_device_id *id_table;

	/* Device detection callback for automatic device creation */
	int (*detect)(struct i2c_client *, struct i2c_board_info *);
	const unsigned short *address_list;
	struct list_head clients;
};

2.2  相关函数

注册函数:i2c_driver注册函数为 i2c_register_driver,此函数原型如下:

int i2c_register_driver(struct module *owner, struct i2c_driver *driver) 

owner 一般为 THIS_MODULE。
driver:要注册的 i2c_driver。
返回值: 0,成功;负值,失败。

#define i2c_add_driver(driver)   i2c_register_driver(THIS_MODULE, driver)

注销函数

void i2c_del_driver(struct i2c_driver *driver)

2.3  设备数据收发

        对I2C 设备寄存器进行读写操作用到 i2c_transfer 函数,i2c_transfer 函数最终会调用I2C 适配器中i2c_algorithm 里面的master_xfer 函数。

Linux驱动开发之i2c框架讲解到例程

 i2c_msg结构体如下,flags设置为I2C_M_RD则为读操作,设置为 0 则为写操作。

Linux驱动开发之i2c框架讲解到例程

 还有两个API 函数分别用于I2C 数据的收发操作,这两个函数最终都会调用i2c_transfer。

I2C数据发送函数原型如下:

Linux驱动开发之i2c框架讲解到例程

 I2C数据接收函数原型如下:

Linux驱动开发之i2c框架讲解到例程

3. I2C设备驱动编写

3.1  硬件设备基本信息获取

        这里使用的是迅为7寸的LVDS屏,查看硬件原理图可知使用的是 i2c1;

Linux驱动开发之i2c框架讲解到例程

        查看触摸IC的data sheep可知,可以操作的寄存器;

Linux驱动开发之i2c框架讲解到例程

Linux驱动开发之i2c框架讲解到例程

         设备树下的设备信息描述如下:挂载在 i2c1 节点下,compatible 为"edt,ft5x0x_ts";          设备访问地址为0x38;

&i2c1 {
        status = "okay";
        i2c-scl-rising-time-ns = <140>;
        i2c-scl-falling-time-ns = <30>;

........

        ft5x06@38 {
                compatible = "edt,ft5x0x_ts"; 
                reg = <0x38>;
                touch-gpio = <&gpio1 20 IRQ_TYPE_EDGE_RISING>;
                interrupt-parent = <&gpio1>;
                interrupts = <20 IRQ_TYPE_LEVEL_LOW>;
		        pinctrl-names = "default";
                pinctrl-0 = <&gt911_gpio>;
                reset-gpios = <&gpio1 9 GPIO_ACTIVE_LOW>;
#if defined(LCD_TYPE_9_7)
		touch_type = <0>;       /*0:9.7, 1: 7.0*/
#elif defined(LCD_TYPE_7_0)
		touch_type = <1>;
#elif defined(LCD_TYPE_MIPI_7_0_NEW)|| defined(LCD_TYPE_MIPI_7_0_OLD)
		touch_type = <1>;
#endif
        };

3.2  设备驱动编写

        在kernel/driver下搜索“edt,ft5x0x_ts”字符串,找到对应的 touch 驱动文件,文件名为tf5x06_ts.c,设备驱动的编写可以参考下该文件,但由于我们刚开始不熟悉该 IC ,所以只需要简单化驱动程序,重在熟悉基本开发框架。

        寄存器的读写函数需要填写 i2c_msg 结构体变量,addr为设备地址,传入值为0x38;       flag设为0表示写操作,设为1表示读操作;设备与驱动匹配成功后,调用probe函数,在probe函数里仅做设备寄存器读写操作;在 i2c_driver 结构体变量中,of_match_table用于与设备树匹配,id_table用于传统、无设备树下的匹配。

static struct i2c_client *ft5x06_client;

//读寄存器函数
static int ft5x06_read_reg(uint8_t reg_addr)
{
	uint8_t data;
	struct i2c_msg msgs[] = {
		[0] = {
			.addr = ft5x06_client->addr,
			.flags = 0,   //写
			.len = sizeof(reg_addr),
			.buf = &reg_addr,
		},

		[1] = {
			.addr = ft5x06_client->addr,
			.flags = 1,  //读
			.len = sizeof(data),
			.buf = &data,
		},
	};
	i2c_transfer(ft5x06_client->adapter, msgs, 2);
	return data;
}
//写寄存器函数
static void ft5x06_write_reg(uint8_t reg_addr, uint8_t data, uint8_t len)
{
	uint8_t buff[64];
	struct i2c_msg msgs;
	buff[0] = reg_addr;
	memcpy(&buff[1], &data, len);

	msgs.addr = ft5x06_client->addr,
	msgs.flags = 0,
	msgs.len = len + 1,		//addr+data
	msgs.buf = buff,

	i2c_transfer(ft5x06_client->adapter, &msgs, 1);
}

static int i2c_touch_irq_probe(struct i2c_client *i2c_client, const struct i2c_device_id *id)
{
    int ret = 0;
	printk("i2c_touch_irq_probe\n");
    ft5x06_client = i2c_client;
	ft5x06_write_reg(0x80, 0x20, 1);	//写入值0x20到0x80寄存器
	ret = ft5x06_read_reg(0x80);		//读出0x80寄存器的值
	printk("0x80 reg value is %X\n", ret);
	ret = ft5x06_read_reg(0x01);		//读出0x01寄存器的值
	printk("0x01 reg value is %X\n", ret);
    return 0;
}

static  int i2c_touch_remove(struct i2c_client *i2c_client)
{
	printk("i2c_touch_remove \n");
	return 0;
}

static const struct i2c_device_id ft5x0x_id[] = {
	{"ft5x0x_ts", 0},  
	{}
};

const struct of_device_id of_match_table_test[] = {
    {.compatible = "edt,ft5x0x_ts"},
    {},
};

static struct i2c_driver i2c_touch_driver =
{
    .probe = i2c_touch_irq_probe,
	.remove = i2c_touch_remove,
    .driver = {
        .owner = THIS_MODULE,
        .name = "i2c_touch_test",
        .of_match_table = of_match_table_test 
    },
	.id_table = ft5x0x_id,
};
static int i2c_test_init(void)
{
    i2c_add_driver(&i2c_touch_driver);
    printk("i2c_test_init \n");
    return 0;
}
static void i2c_test_exit(void)
{
    printk("i2c_test_exit \n");
    i2c_del_driver(&i2c_touch_driver);
}

MODULE_LICENSE("GPL");
module_init(i2c_test_init);
module_exit(i2c_test_exit);

3.3  修改kernel配置

        设备驱动文件编写好后,还需要配置下kernel,把之前系统用的 ft5x06 驱动屏蔽掉,

在 kernel/ 下输入“make menuconfig”,找到文件位置选择不编译即可;配置好后查看 .config 文件是否修改完成。

Linux驱动开发之i2c框架讲解到例程

Linux驱动开发之i2c框架讲解到例程

Linux驱动开发之i2c框架讲解到例程

Linux驱动开发之i2c框架讲解到例程

Linux驱动开发之i2c框架讲解到例程

3.4  烧录测试

        在 i2c 设备文件中,有个 1-0038 (i2c1-0x38)就是我们需要找的节点;

Linux驱动开发之i2c框架讲解到例程

         加载运行驱动文件,在probe函数中打印读取的结果,验证完成。

Linux驱动开发之i2c框架讲解到例程文章来源地址https://www.toymoban.com/news/detail-433290.html

到了这里,关于Linux驱动开发之i2c框架讲解到例程的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 详解AT24CXX驱动开发(linux platform tree - i2c应用)

    目录 概述 1 认识AT24Cxx 1.1 AT24CXX的特性 1.2 AT24CXX描述 1.2.1 引脚 1.2.2 容量描述 1.2.3 设备地址 1.3 操作时序 1.3.1 写单个字节时序 1.3.2 写page字节时序 1.3.3 读取当前数据时序 1.3.4 随机读取数据 1.3.5 连续读取多个数据 2 驱动开发 2.1 硬件接口 2.2 代码实现 2.2.1 查看设备信息 2.2.2 编写

    2024年02月22日
    浏览(43)
  • I2C知识大全系列四 —— I2C驱动之Linux下的I2C

    Linux 系统定义了 I2C 驱动体系结构。在 Linux 系统中, I2C 驱动由三部分组成,即 I2C 核心 、 I2C 总线驱动 和 I2C 设备驱动 。这三部分相互协作,形成了非常通用、可适应性很强的 I2C 框架。 I2C核心 I2C 核心提供了 I2C 总线驱动 和 设备驱动 的 注册 、 注销方法 , I2C 通信方法

    2024年02月07日
    浏览(56)
  • Linux I2C 驱动实验

    目录 一、Linux I2C 驱动简介 1、I2C 总线驱动 2、I2C 设备驱动 1、 i2c_client 结构体 2、 i2c_driver 结构体 二、硬件分析 三、设备树编写 1、pinctrl_i2c1 2、在 i2c1 节点追加 ap3216c 子节点 3、验证 四、 代码编写 1、makefile 2、ap3216c.h  3、ap3216c.c ①、头文件 ②、驱动出入口  ③、 i2c驱动

    2024年02月08日
    浏览(52)
  • 【IMX6ULL驱动开发学习】10.Linux I2C驱动实战:AT24C02驱动设计流程

    前情回顾:【IMX6ULL驱动开发学习】09.Linux之I2C框架简介和驱动程序模板_阿龙还在写代码的博客-CSDN博客 目录 一、修改设备树(设备树用来指定引脚资源) 二、编写驱动 2.1 i2c_drv_read 2.2 i2c_drv_write 2.3 完整驱动程序 三、上机测试 放在哪个I2C控制器下面 AT24C02的I2C设备地址(查

    2024年02月11日
    浏览(48)
  • QEMU学习(五):I2C设备仿真及驱动开发

            I2C 是很常用的一个串行通信接口,用于连接各种外设、传感器等器件, 本章我们来学习一下如何在QEMU里仿真I2C设备及 Linux 下开发 I2C 接口器件驱动。 下面是标准的设备添加结构,我们使用的是常见的at_24c系列设备来做I2C的通信,详细代码请看qemuhwnvrameeprom_

    2024年02月08日
    浏览(103)
  • Linux I2C驱动分析2 - 通过设备树添加设备

    一. I2C通过设备树添加设备         通过设备可以向I2C总线添加I2C设备,设备树举例如下:         设备树中在I2C总线下添加了一个oled0.98设备,oled在I2C总线中的地址为0x3c。可以在/sys/bus/platform下看到这个设备。 二. I2C驱动代码 三. I2C应用代码         以上的代码功能是首

    2024年02月14日
    浏览(44)
  • 用OLED屏幕播放视频(2): 为OLED屏幕开发I2C驱动

    下面的系列文章记录了如何使用一块linux开发扳和一块OLED屏幕实现视频的播放: 项目介绍 为OLED屏幕开发I2C驱动 使用cuda编程加速视频处理 这是此系列文章的第2篇, 主要总结和记录一个I2C从设备的驱动, 在linux内核中如何实现, 如何给用户态的程序暴露合适的接口, 让用户态有机

    2024年02月09日
    浏览(44)
  • RK3568平台入门到精通系列讲解之UBOOT开发篇(I2C操作)

    uboot中i2c读写有2种方式,一种使用uboot驱动模型,通过宏CONFIG_DM_I2C定义,另一种是传统方式,通过宏CONFIG_SYS_I2C定义。 在uboot命令行中,通过定义宏CONFIG_CMD_I2C,可以打开i2c cmd 子系统。输入i2c查看 usage。 举例:读取i2c地址为0x20的外设芯片,从第0个寄存器开始读,共读16个寄

    2024年02月02日
    浏览(54)
  • Clion开发STM32之I2C驱动(参考RT-Thread)

    本章是根据RT-Thread源码中的I2C组件进行抽离,主要不习惯用RT-Thread 然后在结合at24cxx模块补充测试 也为了方便移植和独立于框架的限制。 操作gpio部分 头文件 源码 头文件 源文件

    2024年02月10日
    浏览(43)
  • <Linux开发> linux开发工具-之-I2C TOOLS工具使用

    <Linux开发> linux开发工具-之-I2C TOOLS工具使用 <Android开发> Android开发工具- 之-I2C TOOLS工具使用 <Linux开发>驱动开发 -之- Linux I2C 驱动 在笔者的另一篇文章 <Android开发> Android开发工具- 之-I2C TOOLS工具使用讲解过,如何在android上使用I2C TOOLS工具。本文主要是分析如何在

    2024年02月16日
    浏览(50)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包