【LINUX驱动子系统】I2C子系统介绍

1. I2C的基本协议介绍

I2C 是很常见的一种总线协议, I2C 是 NXP 公司设计的, I2C 使用两条线在主控制器和从
机之间进行数据通信。一条是 SCL(串行时钟线),另外一条是 SDA(串行数据线),这两条数据
线需要接上拉电阻,总线空闲的时候 SCL 和 SDA 处于高电平。 I2C 总线标准模式下速度可以
达到 100Kb/S,快速模式下可以达到 400Kb/S。 、

I2C 是支持多从机的,也就是一个 I2C 控制器下可以挂多个 I2C 从设备,这些不同的 I2C
从设备有不同的器件地址,这样 I2C 主控制器就可以通过 I2C 设备的器件地址访问指定的 I2C
设备了。
在这里插入图片描述
SDA 和 SCL 这两根线必须要接一个上拉电阻,一般是 4.7K。其余的 I2C 从
器件都挂接到 SDA 和 SCL 这两根线上,这样就可以通过 SDA 和 SCL 这两根线来访问多个 I2C
设备。

  1. 起始位
    I2C 通信起始标志,通过这个起始位就可以告诉 I2C 从机,“我”要开始
    进行 I2C 通信了。在 SCL 为高电平的时候, SDA 出现下降沿就表示为起始位
    在这里插入图片描述
  2. 停止位
    停止位就是停止 I2C 通信的标志位,和起始位的功能相反。在 SCL 位高电平的时候, SDA
    出现上升沿就表示为停止位:

    在这里插入图片描述
  3. 数据传输
    I2C 总线在数据传输的时候要保证在 SCL 高电平期间, SDA 上的数据稳定,因此 SDA 上
    的数据变化只能在 SCL 低电平期间发生

    在这里插入图片描述
  4. 应答信号
    当 I2C 主机发送完 8 位数据以后会将 SDA 设置为输入状态,等待 I2C 从机应答,也就是
    等待 I2C 从机告诉主机它接收到数据了。应答信号是由从机发出的,主机需要提供应答信号所
    需的时钟,主机发送完 8 位数据以后紧跟着的一个时钟信号就是给应答信号使用的。从机通过
    将 SDA 拉低来表示发出应答信号,表示通信成功,否则表示通信失败
  5. I2C写时序
    在这里插入图片描述
    I2C 写时序,我们来看一下写时序的具体步骤:
    1)、开始信号。
    2)、发送 I2C 设备地址,每个 I2C 器件都有一个设备地址,通过发送具体的设备地址来决
    定访问哪个 I2C 器件。这是一个 8 位的数据,其中高 7 位是设备地址,最后 1 位是读写位,为
    1 的话表示这是一个读操作,为 0 的话表示这是一个写操作。

    3)、 I2C 器件地址后面跟着一个读写位,为 0 表示写操作,为 1 表示读操作。
    4)、从机发送的 ACK 应答信号。
    5)、重新发送开始信号。
    6)、发送要写写入数据的寄存器地址。
    7)、从机发送的 ACK 应答信号。
    8)、发送要写入寄存器的数据。
    9)、从机发送的 ACK 应答信号。
    10)、停止信号。
  6. I2C读时序
    在这里插入图片描述
    I2C 单字节读时序比写时序要复杂一点,读时序分为 4 大步,第一步是发送设备地址,第
    二步是发送要读取的寄存器地址,第三步重新发送设备地址,最后一步就是 I2C 从器件输出要
    读取的寄存器值,我们具体来看一下这步。
    1)、主机发送起始信号。
    2)、主机发送要读取的 I2C 从设备地址。
    3)、读写控制位,因为是向 I2C 从设备发送数据,因此是写信号。
    4)、从机发送的 ACK 应答信号。
    5)、重新发送 START 信号。
    6)、主机发送要读取的寄存器地址。
    7)、从机发送的 ACK 应答信号。
    8)、重新发送 START 信号。
    9)、重新发送要读取的 I2C 从设备地址。
    10)、读写控制位,这里是读信号,表示接下来是从 I2C 从设备里面读取数据。
    11)、从机发送的 ACK 应答信号。
    12)、从 I2C 器件里面读取到的数据。
    13)、主机发出 NO ACK 信号,表示读取完成,不需要从机再发送 ACK 信号了。
    14)、主机发出 STOP 信号,停止 I2C 通信。
  7. I2C多字节读写时序
    有时候我们需要读写多个字节,多字节读写时序和单字节的基本一致,只是在读写数据的
    时候可以连续发送多个自己的数据,其他的控制时序都是和单字节一样的。

参考文章:https://blog.csdn.net/qq_43858116/article/details/126031721

2. 整体框架介绍

在linux的i2c架构如下图
在这里插入图片描述
内核空间部分可以分为:i2c设备驱动、i2c核心以及i2c总线驱动

  • i2c核心:框架的实现;提供i2c总线驱动和设备驱动的注册、注销方法;i2c通信方法(algorithm)上层的,与具体适配器无关的代码以及探测设备、检测设备地址的上层代码等。这一部分的工作由内核开发者完成。
  • i2c总线驱动:具体控制器的实现;i2c总线驱动是对i2c硬件体系结构中适配器端的实现,说白了,就是怎么操作i2c模块工作。适配器可由CPU控制,甚至直接集成到cpu里面( algorithm driver
    adapter driver)
  • i2c设备:对i2c硬件体系结构中设备端的实现,比如说板上的EEPROM设备等。设备一般挂接在cpu控制的i2c适配器上,通过i2c适配器与cpu交换数据。( chip drivers, 包括多种类型,如RTC, EEPROM, I/O expander, hardware monitoring, sound, video等)
    名词解释
  • i2c-adapter(适配器):指的是CPU实际的I2C控制器(例如I2C0,I2C1);
  • i2c-device(设备):指的是I2C总线上的从设备(例如某片EEPROM,某个触摸屏);
  • i2c algorithm(算法、实现方法):这里指的是对i2c设备一套对应的通信方法。
    分层的好处:
    让工程师们各司其职,只关心自己应该实现的部分
    不需要为每一个i2c控制器编写所有从设备的控制代码,只需要分别完成n个控制器的控制接口,m个从设备的访问实现,即可实现任意的控制器访问任意的从设备(假设硬件连接支持)

2.1 I2C总线驱动

首先来看下I2C总线,其实linux中还有一个虚拟的总线,platform bus,建议可以先了解一下这个,platform 是虚拟出来的一条总线,目的是为了实现总线、设备、驱动框架,I2C也是同理的。对于 I2C 而言,不需要虚拟出一条总线,直接使用 I2C总线即可。 I2C 总线驱动重点是 I2C 适配器(也就是 SoC 的 I2C 接口控制器)驱动,这里要用到两个重要的数据结构: i2c_adapter 和 i2c_algorithm, I2C 子系统将 SoC 的 I2C 适配器(控制器)抽象成一个 i2c_adapter 结构体, i2c_adapter 结构体定义在 include/linux/i2c.h 文件中,结构体内容如下:

/*
 * i2c_adapter is the structure used to identify a physical i2c bus along
 * with the access algorithms necessary to access it.
 */
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	*/
	const struct i2c_lock_operations *lock_ops;
	struct rt_mutex bus_lock;
	struct rt_mutex mux_lock;

	int timeout;			/* in jiffies */
	int retries;
	struct device dev;		/* the adapter device */
	unsigned long locked_flags;	/* owned by the I2C core */
#define I2C_ALF_IS_SUSPENDED		0
#define I2C_ALF_SUSPEND_REPORTED	1

	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 irq_domain *host_notify_domain;
	struct regulator *bus_regulator;
};

i2c_algorithm 类型的指针变量 algo,对于一个 I2C 适配器,肯定要对外提供读写 API 函数,设备驱动程序可以使用这些 API 函数来完成读写操作。i2c_algorithm就是I2C 适配器与 IIC 设备进行通信的方法。

/**
 * struct i2c_algorithm - represent I2C transfer method
 * @master_xfer: Issue a set of i2c transactions to the given I2C adapter
 *   defined by the msgs array, with num messages available to transfer via
 *   the adapter specified by adap.
 * @master_xfer_atomic: same as @master_xfer. Yet, only using atomic context
 *   so e.g. PMICs can be accessed very late before shutdown. Optional.
 * @smbus_xfer: Issue smbus transactions to the given I2C adapter. If this
 *   is not present, then the bus layer will try and convert the SMBus calls
 *   into I2C transfers instead.
 * @smbus_xfer_atomic: same as @smbus_xfer. Yet, only using atomic context
 *   so e.g. PMICs can be accessed very late before shutdown. Optional.
 * @functionality: Return the flags that this algorithm/adapter pair supports
 *   from the ``I2C_FUNC_*`` flags.
 * @reg_slave: Register given client to I2C slave mode of this adapter
 * @unreg_slave: Unregister given client from I2C slave mode of this adapter
 *
 * The following structs are for those who like to implement new bus drivers:
 * i2c_algorithm is the interface to a class of hardware solutions which can
 * be addressed using the same bus algorithms - i.e. bit-banging or the PCF8584
 * to name two of the most common.
 *
 * The return codes from the ``master_xfer{_atomic}`` fields should indicate the
 * type of error code that occurred during the transfer, as documented in the
 * Kernel Documentation file Documentation/i2c/fault-codes.rst. Otherwise, the
 * number of messages executed should be returned.
 */
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 (*master_xfer_atomic)(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);
	int (*smbus_xfer_atomic)(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 *adap);

#if IS_ENABLED(CONFIG_I2C_SLAVE)
	int (*reg_slave)(struct i2c_client *client);
	int (*unreg_slave)(struct i2c_client *client);
#endif
};
  1. master_xfer 就是 I2C 适配器的传输函数,可以通过此函数来完成与 IIC 设备之间的通信。
  2. smbus_xfer 就是 SMBUS 总线的传输函数。 smbus 协议是从 I2C 协议的基础上发展而来的,他们之间有很大的相似度, SMBus 与 I2C 总线之间在时序特性上存在一些差别,应用于移动 PC 和桌面 PC 系统中的低速率通讯。
  3. I2C 总线驱动,或者说 I2C 适配器驱动的主要工作就是初始化 i2c_adapter 结构体变量,然后设置 i2c_algorithm 中的 master_xfer 函数。完成以后通过 i2c_add_numbered_adapter或i2c_add_adapter这两个函数向I2C子系统注册设置好的i2c_adapter,这两个函数的原型如下:
int i2c_add_adapter(struct i2c_adapter *adapter)
int i2c_add_numbered_adapter(struct i2c_adapter *adap)

这 两 个 函 数 的 区 别 在 于 i2c_add_adapter 会 动 态 分 配 一 个 总 线 编 号 , 而i2c_add_numbered_adapter 函数则指定一个静态的总线编号。函数参数和返回值含义如下:
adapter 或 adap:要添加到 Linux 内核中的 i2c_adapter,也就是 I2C 适配器。
返回值: 0,成功;负值,失败。
如果要删除 I2C 适配器的话使用i2c_del_adapter函数即可,函数原型如下:
void i2c_del_adapter(struct i2c_adapter * adap)
函数参数和返回值含义如下:
adap:要删除的 I2C 适配器。
返回值: 无。

2.2 I2C总线设备

I2C 设备驱动重点关注两个数据结构:i2c_clienti2c_driver,根据总线、设备和驱动模型,I2C 总线上一小节已经讲了。还剩下设备和驱动,i2c_client用于描述 I2C 总线下的设备,i2c_driver 则用于描述 I2C 总线下的设备驱动,类似于 platform 总线下的platform_deviceplatform_driver

2.2.1 i2c_client结构体

i2c_client结构体定义在include/linux/i2c.h文件中,内容如下:

/**
 * struct i2c_client - represent an I2C slave device
 * @flags: see I2C_CLIENT_* for possible flags
 * @addr: Address used on the I2C bus connected to the parent adapter.
 * @name: Indicates the type of the device, usually a chip name that's
 *	generic enough to hide second-sourcing and compatible revisions.
 * @adapter: manages the bus segment hosting this I2C device
 * @dev: Driver model device node for the slave.
 * @init_irq: IRQ that was set at initialization
 * @irq: indicates the IRQ generated by this device (if any)
 * @detected: member of an i2c_driver.clients list or i2c-core's
 *	userspace_devices list
 * @slave_cb: Callback when I2C slave mode of an adapter is used. The adapter
 *	calls it to pass on slave events to the slave driver.
 * @devres_group_id: id of the devres group that will be created for resources
 *	acquired when probing this device.
 *
 * An i2c_client identifies a single device (i.e. chip) connected to an
 * i2c bus. The behaviour exposed to Linux is defined by the driver
 * managing the device.
 */
struct i2c_client {
	unsigned short flags;		/* div., see below		*/
#define I2C_CLIENT_PEC		0x04	/* Use Packet Error Checking */
#define I2C_CLIENT_TEN		0x10	/* we have a ten bit chip address */
					/* Must equal I2C_M_TEN below */
#define I2C_CLIENT_SLAVE	0x20	/* we are the slave */
#define I2C_CLIENT_HOST_NOTIFY	0x40	/* We want to use I2C host notify */
#define I2C_CLIENT_WAKE		0x80	/* for board_info; true iff can wake */
#define I2C_CLIENT_SCCB		0x9000	/* Use Omnivision SCCB protocol */
					/* Must match I2C_M_STOP|IGNORE_NAK */

	unsigned short addr;		/* chip address - NOTE: 7bit	*/
					/* addresses are stored in the	*/
					/* _LOWER_ 7 bits		*/
	char name[I2C_NAME_SIZE];
	struct i2c_adapter *adapter;	/* the adapter we sit on	*/
	struct device dev;		/* the device structure		*/
	int init_irq;			/* irq set at initialization	*/
	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
	void *devres_group_id;		/* ID of probe devres group	*/
};

一个 I2C 设备对应一个 i2c_client 结构体变量, 系统每检测到一个 I2C 从设备就会给这个
设备分配一个 i2c_client。

2.2.2 i2c_driver结构体

i2c_driver 类似 platform_driver,是我们编写 I2C 设备驱动重点要处理的内容,i2c_driver
构体定义在 include/linux/i2c.h 文件中,内容如下:

/**
 * struct i2c_driver - represent an I2C device driver
 * @class: What kind of i2c device we instantiate (for detect)
 * @probe: Callback for device binding
 * @probe_new: Transitional callback for device binding - do not use
 * @remove: Callback for device unbinding
 * @shutdown: Callback for device shutdown
 * @alert: Alert callback, for example for the SMBus alert protocol
 * @command: Callback for bus-wide signaling (optional)
 * @driver: Device driver model driver
 * @id_table: List of I2C devices supported by this driver
 * @detect: Callback for device detection
 * @address_list: The I2C addresses to probe (for detect)
 * @clients: List of detected clients we created (for i2c-core use only)
 * @flags: A bitmask of flags defined in &enum i2c_driver_flags
 *
 * The driver.owner field should be set to the module owner of this driver.
 * The driver.name field should be set to the name of this driver.
 *
 * For automatic device detection, both @detect and @address_list must
 * be defined. @class should also be set, otherwise only devices forced
 * with module parameters will be created. The detect function must
 * fill at least the name field of the i2c_board_info structure it is
 * handed upon successful detection, and possibly also the flags field.
 *
 * If @detect is missing, the driver will still work fine for enumerated
 * devices. Detected devices simply won't be supported. This is expected
 * for the many I2C/SMBus devices which can't be detected reliably, and
 * the ones which can always be enumerated in practice.
 *
 * The i2c_client structure which is handed to the @detect callback is
 * not a real i2c_client. It is initialized just enough so that you can
 * call i2c_smbus_read_byte_data and friends on it. Don't do anything
 * else with it. In particular, calling dev_dbg and friends on it is
 * not allowed.
 */
struct i2c_driver {
	unsigned int class;

	union {
	/* Standard driver model interfaces */
		int (*probe)(struct i2c_client *client);
		/*
		 * Legacy callback that was part of a conversion of .probe().
		 * Today it has the same semantic as .probe(). Don't use for new
		 * code.
		 */
		int (*probe_new)(struct i2c_client *client);
	};
	void (*remove)(struct i2c_client *client);


	/* driver model interfaces that don't relate to enumeration  */
	void (*shutdown)(struct i2c_client *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").
	 * For the SMBus Host Notify protocol, the data corresponds to the
	 * 16-bit payload data reported by the slave device acting as master.
	 */
	void (*alert)(struct i2c_client *client, enum i2c_alert_protocol protocol,
		      unsigned int data);

	/* a ioctl like command that can be used to perform specific functions
	 * with the device.
	 */
	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 *client, struct i2c_board_info *info);
	const unsigned short *address_list;
	struct list_head clients;

	u32 flags;
};
  1. I2C 设备和驱动匹配成功以后probe函数就会执行,和 platform 驱动一样。
  2. device_driver 驱动结构体,如果使用设备树的话,需要设置 device_driver of_match_table 成员变量,也就是驱动的兼容(compatible)属性。
  3. id_table 是传统的、未使用设备树的设备匹配 ID 表。
    对于我们 I2C 设备驱动编写人来说,重点工作就是构建 i2c_driver,构建完成以后需要向I2C 子系统注册这个 i2c_driver i2c_driver 注册函数为 int i2c_register_driver,此函数原型如下:
int i2c_register_driver(struct module *owner, struct i2c_driver *driver)

函数参数和返回值含义如下:
owner: 一般为 THIS_MODULE。
driver:要注册的 i2c_driver。
返回值: 0,成功;负值,失败。
另外i2c_add_driver也常常用于注册i2c_driver, i2c_add_driver是一个宏,定义如下:

844 #define i2c_add_driver(driver) \
845 i2c_register_driver(THIS_MODULE, driver)

i2c_add_driver 就是对i2c_register_driver做了一个简单的封装,只有一个参数,就是要注册的 i2c_driver
注销 I2C 设备驱动的时候需要将前面注册的 i2c_driver 从 I2C 子系统中注销掉,需要用到i2c_del_driver 函数,此函数原型如下:

void i2c_del_driver(struct i2c_driver *driver)

函数参数和返回值含义如下:
driver:要注销的 i2c_driver。
返回值: 无。
i2c_driver 的注册示例代码如下:

1 /* i2c 驱动的 probe 函数 */
2 static int xxx_probe(struct i2c_client *client, const struct i2c_device_id *id)
3 {
4 		/* 函数具体程序 */
5 		return 0;
6 }
7 
8 /* i2c 驱动的 remove 函数 */
9 static int ap3216c_remove(struct i2c_client *client)
10 {
11 		/* 函数具体程序 */
12 		return 0;
13 }
14
15 /* 传统匹配方式 ID 列表 */
16 static const struct i2c_device_id xxx_id[] = {
17 		{"xxx", 0},
18 		{}
19 };
20
21 /* 设备树匹配列表 */
22 static const struct of_device_id xxx_of_match[] = {
23 		{ .compatible = "xxx" },
24 		{ /* Sentinel */ }
25 };
26
27 /* i2c 驱动结构体 */
28 static struct i2c_driver xxx_driver = {
29 		.probe = xxx_probe,
30 		.remove = xxx_remove,
31 		.driver = {
32 		.owner = THIS_MODULE,
33 		.name = "xxx",
34 		.of_match_table = xxx_of_match,
35 		},
36 		.id_table = xxx_id,
37 };
38
39 /* 驱动入口函数 */
40 static int __init xxx_init(void)
41 {
42 		int ret = 0;
43
44 		ret = i2c_add_driver(&xxx_driver);
45 		return ret;
46 }
47
48 /* 驱动出口函数 */
49 static void __exit xxx_exit(void)
50 {
51 		i2c_del_driver(&xxx_driver);
52 }
53
54 module_init(xxx_init);
55 module_exit(xxx_exit);

第 16~19 行, i2c_device_id,无设备树的时候匹配 ID 表。
第 22~25 行, of_device_id,设备树所使用的匹配表。
第 28~37 行, i2c_driver,当 I2C 设备I2C 驱动匹配成功以后 probe 函数就会执行,这些
platform 驱动一样, probe 函数里面基本就是标准的字符设备驱动那一套了。

2.3 I2C 设备和驱动匹配过程

I2C 设备和驱动的匹配过程是由 I2C 子系统核心层来完成的, drivers/i2c/i2c-core-base.c 就是 I2C 的核心部分, I2C 核心提供了一些与具体硬件无关的 API 函数,比如前面讲过的:

  1. i2c_adapter 注册/注销函数
int i2c_add_adapter(struct i2c_adapter *adapter)
int i2c_add_numbered_adapter(struct i2c_adapter *adap)
void i2c_del_adapter(struct i2c_adapter * adap)
  1. i2c_driver 注册/注销函数
int i2c_register_driver(struct module *owner, struct i2c_driver *driver)
int i2c_add_driver (struct i2c_driver *driver)
void i2c_del_driver(struct i2c_driver *driver)

设备和驱动的匹配过程也是由核心层完成的, I2C 总线的数据结构为 i2c_bus_type,定义在drivers/i2c/i2c-core-base.c 文件,i2c_bus_type内容如下:

492 struct bus_type i2c_bus_type = {
493 	.name = "i2c",
494 	.match = i2c_device_match,
495 	.probe = i2c_device_probe,
496 	.remove = i2c_device_remove,
497 	.shutdown = i2c_device_shutdown,
498 };

.match 就是 I2C 总线的设备和驱动匹配函数,在这里就是 i2c_device_match 这个函数,此函数内容如下:

93 static int i2c_device_match(struct device *dev, struct device_driver *drv)
94 {
95 		struct i2c_client *client = i2c_verify_client(dev);
96 		struct i2c_driver *driver;
97
98
99 		/* Attempt an OF style match */
100 	if (i2c_of_match_device(drv->of_match_table, client))
101 	return 1;
102
103 	/* Then ACPI style match */
104 	if (acpi_driver_match_device(dev, drv))
105 	return 1;
106
107 	driver = to_i2c_driver(drv);
108
109 	/* Finally an I2C match */
110 	if (i2c_match_id(driver->id_table, client))
111 		return 1;
112
113 	return 0;
114 }

第 100 行,i2c_of_match_device函数用于完成设备树中定义的设备与驱动匹配过程。比较
I2C 设备节点的 compatible 属性of_device_id 中的 compatible 属性是否相等,如果相当的话就
表示 I2C 设备和驱动匹配。
第 104 行,acpi_driver_match_device函数用于 ACPI 形式的匹配。
第 110 行,i2c_match_id函数用于传统的、无设备树的 I2C 设备和驱动匹配过程。比较 I2C设备名字和i2c_device_idname 字段是否相等,相等的话就说明 I2C 设备和驱动匹配成功。

参考文章:https://www.cnblogs.com/schips/p/linux-subsystem-i2c-0-about.html

  • 7
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值