嵌入式Linux驱动学习 —— IIC

1 IIC

  IIC,串行总线,两条数据线:数据线SDA,时钟线SCL。多主机的半双工通信方式。
  传输波形示意图:
在这里插入图片描述

  信号类型:
  (1)空闲信号:SDA和SCL同时处于高电平。
  (2)起始信号:SCL为高,SDA由高到低的跳变。
  (3)结束信号:SCL为高,SDA由低到高的跳变。
  (4)响应信号:在第9个时钟接收方接收该字节成功,便会输出一个ACK应答信号,当SDA为高电平,表示为非应答信号NACK,当SDA为低电平,表示为有效应答信号ACK。

2 框架

(1)框架图
在这里插入图片描述
(2)结构体
  i2c_adapter:对应于物理上的一个适配器,对应的就是SOC上的I2C控制器
  i2c_algorithm:对应一套通信方法
  i2c_driver :对应一套驱动方法,其主要成员函数是 probe()、 remove()、 suspend()、 resume()等
  i2c_client:对应于真实的物理设备,每个 I2C 设备都需要一个 i2c_client 来描述
  一个i2c_adapter需要 i2c_algorithm 中提供的通信函数来控制适配器上产生特定的访问周期。i2c_driver 与 i2c_client 的关系是一对多。

3 源码分析

3.1 总线部分

  在drivers/i2c/busses/i2c-s3c2410.c中,初始化

static struct platform_driver s3c24xx_i2c_driver = {
	.probe		= s3c24xx_i2c_probe,
	.remove		= s3c24xx_i2c_remove,
	.id_table	= s3c24xx_driver_ids,
	.driver		= {
		.owner	= THIS_MODULE,
		.name	= "s3c-i2c",
		.pm	= S3C24XX_DEV_PM_OPS,
	},
};

static int __init i2c_adap_s3c_init(void)
{
	return platform_driver_register(&s3c24xx_i2c_driver);
}

  注册了平台驱动,当与设备匹配时,调用probe

static int s3c24xx_i2c_probe(struct platform_device *pdev)
{
	...

	i2c = kzalloc(sizeof(struct s3c24xx_i2c), GFP_KERNEL);//分配结构体
	if (!i2c) {
		dev_err(&pdev->dev, "no memory for state\n");
		return -ENOMEM;
	}

	strlcpy(i2c->adap.name, "s3c2410-i2c", sizeof(i2c->adap.name));
	i2c->adap.owner   = THIS_MODULE;
	i2c->adap.algo    = &s3c24xx_i2c_algorithm;//算法
	i2c->adap.retries = 2;
	i2c->adap.class   = I2C_CLASS_HWMON | I2C_CLASS_SPD;
	i2c->tx_setup     = 50;
	...
	ret = s3c24xx_i2c_init(i2c);//初始化相关寄存器
	if (ret != 0)
		goto err_iomap;

	...

	ret = request_irq(i2c->irq, s3c24xx_i2c_irq, IRQF_DISABLED,
			  dev_name(&pdev->dev), i2c);//中断
	...

	i2c->adap.nr = pdata->bus_num;

	ret = i2c_add_numbered_adapter(&i2c->adap);//添加适配器
	...
}

  i2c_add_numbered_adapter–>i2c_register_adapter,注册适配器

static int i2c_register_adapter(struct i2c_adapter *adap)
{
	...

	dev_set_name(&adap->dev, "i2c-%d", adap->nr);
	adap->dev.bus = &i2c_bus_type;//总线
	adap->dev.type = &i2c_adapter_type;
	res = device_register(&adap->dev);//注册设备
	...
	dummy = bus_for_each_drv(&i2c_bus_type, NULL, adap,
				 i2c_do_add_adapter);//遍历i2c_bus_type,进行匹配
	...
}

  遍历i2c_bus_type,匹配driver和adapter,调用i2c_do_add_adapter

static int i2c_do_add_adapter(struct device_driver *d, void *data)
{
	struct i2c_driver *driver = to_i2c_driver(d);
	struct i2c_adapter *adap = data;

	i2c_detect(adap, driver);//探测设备

	if (driver->attach_adapter) {
		driver->attach_adapter(adap);
	}
	return 0;
}

  看下如何探测设备

static int i2c_detect(struct i2c_adapter *adapter, struct i2c_driver *driver)
{
	const struct i2c_client_address_data *address_data;
	struct i2c_client *temp_client;
	int i, err = 0;
	int adap_id = i2c_adapter_id(adapter);//得到adap->nr

	...
	if (!(adapter->class & driver->class))//如果适配器dapter->class定义为不自动检测类型,函数返回
		goto exit_free;

	...
	for (i = 0; address_data->probe[i] != I2C_CLIENT_END; i += 2) {
		if (address_data->probe[i] == adap_id
		 || address_data->probe[i] == ANY_I2C_BUS) {
			temp_client->addr = address_data->probe[i + 1];
			err = i2c_detect_address(temp_client, -1, driver);
			if (err)
				goto exit_free;
		}
	}

	for (i = 0; address_data->normal_i2c[i] != I2C_CLIENT_END; i += 1) {
		int j, ignore;

		ignore = 0;
		...
		temp_client->addr = address_data->normal_i2c[i];
		err = i2c_detect_address(temp_client, -1, driver);
		if (err)
			goto exit_free;
	}

 exit_free:
	kfree(temp_client);
	return err;
}

  调用i2c_detect_address()函数进行设备地址检测

static int i2c_detect_address(struct i2c_client *temp_client, int kind,
			      struct i2c_driver *driver)
{
	...
	if (kind < 0) { //确认总线上挂接有该设备地址的I2C芯片
		if (i2c_smbus_xfer(adapter, addr, 0, 0, 0,
				   I2C_SMBUS_QUICK, NULL) < 0)
			return 0;

		if ((addr & ~0x0f) == 0x50)
			i2c_smbus_xfer(adapter, addr, 0, 0, 0,
				       I2C_SMBUS_QUICK, NULL);
	}

	memset(&info, 0, sizeof(struct i2c_board_info));
	info.addr = addr;
	err = driver->detect(temp_client, kind, &info);//调用i2c_driver->detect检测函数再次检测芯片具体是什么芯片
	...
		client = i2c_new_device(adapter, &info);//创建设备
		...
}

  i2c_smbus_xfer发送了数据

s32 i2c_smbus_xfer(struct i2c_adapter *adapter, u16 addr, unsigned short flags,
		   char read_write, u8 command, int protocol,
		   union i2c_smbus_data *data)
{
	...

	if (adapter->algo->smbus_xfer) { //如果适配器中指定了,则调用适配器中的
		...
	} else
		res = i2c_smbus_xfer_emulated(adapter,addr,flags,read_write,
					      command, protocol, data);

	return res;
}

  这里的adapter->algo适配器,在s3c24xx_i2c_probe中设置i2c->adap.algo = &s3c24xx_i2c_algorithm。

static const struct i2c_algorithm s3c24xx_i2c_algorithm = {
	.master_xfer		= s3c24xx_i2c_xfer,
	.functionality		= s3c24xx_i2c_func,
};

  没有smbus_xfer,所以调用i2c_smbus_xfer_emulated

static s32 i2c_smbus_xfer_emulated(struct i2c_adapter * adapter, u16 addr,
                                   unsigned short flags,
                                   char read_write, u8 command, int size,
                                   union i2c_smbus_data * data)
{
	unsigned char msgbuf0[I2C_SMBUS_BLOCK_MAX+3];
	unsigned char msgbuf1[I2C_SMBUS_BLOCK_MAX+2];
	int num = read_write == I2C_SMBUS_READ?2:1;
	struct i2c_msg msg[2] = { { addr, flags, 1, msgbuf0 },
	                          { addr, flags | I2C_M_RD, 0, msgbuf1 }
	                        };
	int i;
	u8 partial_pec = 0;
	int status;

	msgbuf0[0] = command;
	switch(size) {
	...
	case I2C_SMBUS_BYTE_DATA:
		if (read_write == I2C_SMBUS_READ)
			msg[1].len = 1;
		else {
			msg[0].len = 2;
			msgbuf0[1] = data->byte;
		}
		break;
	...
	}
	...
	status = i2c_transfer(adapter, msg, num);///将i2c_msg结构体的内容发送给I2C设备
	...
}

  构造了i2c_msg结构体,通过i2c_transfer发送

int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
{
	...
		orig_jiffies = jiffies;
		for (ret = 0, try = 0; try <= adap->retries; try++) {
			ret = adap->algo->master_xfer(adap, msgs, num);
			if (ret != -EAGAIN)
				break;
			if (time_after(jiffies, orig_jiffies + adap->timeout))
				break;
	...
}

  调用了adapter中algorithm的master_xfer,即s3c24xx_i2c_xfer,s3c24xx_i2c_xfer–>s3c24xx_i2c_doxfer–>s3c24xx_i2c_message_start

static void s3c24xx_i2c_message_start(struct s3c24xx_i2c *i2c,
				      struct i2c_msg *msg)
{
	unsigned int addr = (msg->addr & 0x7f) << 1;
	unsigned long stat;
	unsigned long iiccon;

	stat = 0;
	stat |=  S3C2410_IICSTAT_TXRXEN;

	if (msg->flags & I2C_M_RD) {
		stat |= S3C2410_IICSTAT_MASTER_RX;
		addr |= 1;
	} else
		stat |= S3C2410_IICSTAT_MASTER_TX;

	if (msg->flags & I2C_M_REV_DIR_ADDR)
		addr ^= 1;

	s3c24xx_i2c_enable_ack(i2c);

	iiccon = readl(i2c->regs + S3C2410_IICCON);
	writel(stat, i2c->regs + S3C2410_IICSTAT);

	dev_dbg(i2c->dev, "START: %08lx to IICSTAT, %02x to DS\n", stat, addr);
	writeb(addr, i2c->regs + S3C2410_IICDS);

	ndelay(i2c->tx_setup);

	dev_dbg(i2c->dev, "iiccon, %08lx\n", iiccon);
	writel(iiccon, i2c->regs + S3C2410_IICCON);

	stat |= S3C2410_IICSTAT_START;
	writel(stat, i2c->regs + S3C2410_IICSTAT);
}

  s3c24xx_i2c_message_start进行了硬件时序的操作。

3.2 设备驱动

3.2.1 设备

  在/Documentation/i2c/instantiating-devices中,介绍了实例化I2C设备的4种方法,最终都会调用i2c_new_device。

struct i2c_client *i2c_new_device(struct i2c_adapter *adap, struct i2c_board_info const *info)
{
	struct i2c_client	*client;
	int			status;

	client = kzalloc(sizeof *client, GFP_KERNEL);
	if (!client)
		return NULL;

	client->adapter = adap;

	client->dev.platform_data = info->platform_data;

	if (info->archdata)
		client->dev.archdata = *info->archdata;

	client->flags = info->flags;
	client->addr = info->addr;
	client->irq = info->irq;

	strlcpy(client->name, info->type, sizeof(client->name));

	/* Check for address business */
	status = i2c_check_addr(adap, client->addr);
	if (status)
		goto out_err;

	client->dev.parent = &client->adapter->dev;
	client->dev.bus = &i2c_bus_type;
	client->dev.type = &i2c_client_type;

	dev_set_name(&client->dev, "%d-%04x", i2c_adapter_id(adap),
		     client->addr);
	status = device_register(&client->dev);
	...
}

  传入的参数i2c_board_info描述I2C设备,在include/linux/i2c.h中

struct i2c_board_info {
	char		type[I2C_NAME_SIZE];
	unsigned short	flags;
	unsigned short	addr;
	void		*platform_data;
	struct dev_archdata	*archdata;
	int		irq;
};

#define I2C_BOARD_INFO(dev_type, dev_addr) \
	.type = dev_type, .addr = (dev_addr)   //type--类型名,addr--设备地址

  调用device_register注册设备,与之对应的注册设备会调用i2c_add_driver。

3.2.2 驱动

  i2c_add_driver–>i2c_register_driver

int i2c_register_driver(struct module *owner, struct i2c_driver *driver)
{
	int res;

	if (unlikely(WARN_ON(!i2c_bus_type.p)))
		return -EAGAIN;

	driver->driver.owner = owner;
	driver->driver.bus = &i2c_bus_type;//总线

	res = driver_register(&driver->driver);//注册驱动
	if (res)
		return res;

	INIT_LIST_HEAD(&driver->clients);
	mutex_lock(&core_lock);
	bus_for_each_dev(&i2c_bus_type, NULL, driver, __attach_adapter);//遍历i2c_bus_type,调用__attach_adapter
	mutex_unlock(&core_lock);

	return 0;
}

  __attach_adapter与i2c_do_add_adapter类似,都会调用i2c_detect。

3.3 实例化IIC设备

3.3.1 方法一

  在i2c-s3c2410.c中,注册适配器时,调用i2c_add_numbered_adapter–>i2c_register_adapter

static int i2c_register_adapter(struct i2c_adapter *adap)
{
	...
	if (adap->nr < __i2c_first_dynamic_bus_num)
		i2c_scan_static_board_info(adap);
	...
}

  __i2c_first_dynamic_bus_num的定义在driver/i2c/i2c_boardinfo.c中,初始化为0。
  如果执行了i2c_scan_static_board_info

static void i2c_scan_static_board_info(struct i2c_adapter *adapter)
{
	struct i2c_devinfo	*devinfo;

	down_read(&__i2c_board_lock);
	list_for_each_entry(devinfo, &__i2c_board_list, list) { //取出__i2c_board_list中每一项
		if (devinfo->busnum == adapter->nr
				&& !i2c_new_device(adapter,
						&devinfo->board_info))
			dev_err(&adapter->dev,
				"Can't create device at 0x%02x\n",
				devinfo->board_info.addr);
	}
	up_read(&__i2c_board_lock);
}

  取出__i2c_board_list每一项,如果devinfo->busnum == adapter->nr,调用i2c_new_device实例化。
  __i2c_board_list在哪里添加了:i2c_register_board_info

int __init i2c_register_board_info(int busnum,
	struct i2c_board_info const *info, unsigned len)
{
	int status;

	down_write(&__i2c_board_lock);

	if (busnum >= __i2c_first_dynamic_bus_num)
		__i2c_first_dynamic_bus_num = busnum + 1;//

	for (status = 0; len; len--, info++) {
		struct i2c_devinfo	*devinfo;

		devinfo = kzalloc(sizeof(*devinfo), GFP_KERNEL);
		if (!devinfo) {
			pr_debug("i2c-core: can't register boardinfo!\n");
			status = -ENOMEM;
			break;
		}

		devinfo->busnum = busnum;
		devinfo->board_info = *info;
		list_add_tail(&devinfo->list, &__i2c_board_list);//添加到链表中
	}

	up_write(&__i2c_board_lock);

	return status;
}

  以mach-mini2440.c中为例

static struct i2c_board_info mini2440_i2c_devs[] __initdata = {
	{
		I2C_BOARD_INFO("24c08", 0x50),
		.platform_data = &at24c08,
	},
};

static void __init mini2440_init(void)
{
	...
	i2c_register_board_info(0, mini2440_i2c_devs,
				ARRAY_SIZE(mini2440_i2c_devs));
	...
}

  从上述流程可以看出:这种方法必须在 i2c_register_adapter之前调用i2c_register_board_info注册,所以不适合动态加载insmod。

3.3.2 方法二

  与方法一类似,也构建i2c_board_info结构体,然后直接调用i2c_new_device或i2c_new_probed_device。

struct i2c_client *i2c_new_device(struct i2c_adapter *adap, struct i2c_board_info const *info)

  对于该函数第一个参数需要的i2c_adapter结构体,可以通过i2c_get_adapter()函数(传入参数0表示想要获得适配器0)得到。
  i2c_new_device : 认为设备肯定存在
  i2c_new_probed_device:对于"已经识别出来的设备"(probed_device),才会创建(“new”)

3.3.3 方法三

  从用户空间直接实例化I2C设备。例如,在用户空间执行

echo at24c08 0x50 > /sys/class/i2c-adapter/i2c-0/new_device

  内核会自动创建一个使用适配器0的设备名为“at24c08”,设备地址为0x50的I2C设备,相当于方法二中构建i2c_board_info结构体,然后直接调用i2c_new_device()函数。
  在用户空间执行

echo 0x50 > /sys/class/i2c-adapter/i2c-0/delete_device

  内核会调用i2c_unregister_device()函数删除这个设备地址为0x50的I2C设备。

3.3.4 方法四

  前面的3种方法都要事先确定适配器(I2C总线,I2C控制器)
  如果事先并不知道这个I2C设备在哪个适配器上,怎么办:去class表示的所有的适配器上查找
  如果适配器上一些I2C设备的地址是一样,怎么继续分辨它是哪一款:用detect函数
  参考drivers/hwmon/lm90.c。

4 AT24C08

  MINI2440开发板上使用的芯片AT24C08,8Kbit的EEPROM。
在这里插入图片描述
  读写数据时,要发送设备地址->读写地址->数据。
  24c08总共有1KB字节存储空间,这个空间分为4个块,每个块有16页,每页16字节(每块共256字节)。故一个存储单元的地址由器件地址(p0p1)+8位地址决定。
在这里插入图片描述

5 程序

  用第一种方法写at24c08测试程序。
  读写的操作方式采用Byte Write方式读,和Random Read方式写。
  其中Random Read方式需要对写入的地址进行确认。也就是读操作的时候,需要写一次,再读一次。
(1)修改内核
  修改arch/arm/mach-s3c24xx/mach-smdk2440.c。
  添加头文件

#include <linux/i2c.h>
#include <linux/i2c/at24.h>

  添加设备信息

static struct at24_platform_data at24_platdata = {
        .byte_len = 8192,//字节大小
        .page_size = 16,//页数大小
};
static struct i2c_board_info mini2440_i2c_devices[] = {
        {
                I2C_BOARD_INFO("24c08",0x50),//第一个参数是硬件名称,驱动的名字匹配不上的时候,会和这个匹配,第二个参数是IIC硬件地址。
                .platform_data = &at24_platdata,
        }
};

  在smdk2440_machine_init中添加

i2c_register_board_info(0,mini2440_i2c_devices,ARRAY_SIZE(mini2440_i2c_devices));

(1)驱动

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <asm/uaccess.h>
#include <linux/cdev.h>

static struct i2c_driver at24c08_driver;
static struct i2c_client *at24c08_client;
 
int major;	//主设备号
static struct cdev at24c08_cdev;
static struct class *at24c08_class;

static int i2c_read_byte(char *buf,int count)
{
	int ret=0;
	struct i2c_msg msg;
	msg.addr	= at24c08_client->addr;//0x05
	msg.flags	= 1;//1 代表读     0 代表写
	msg.len		= count;
	msg.buf		= buf;
	ret = i2c_transfer(at24c08_client->adapter,&msg, 1);
	if (ret < 0) {
		printk("i2c transfer failed!\n");
		return	-EINVAL;
	}
	return ret;
}
static int i2c_write_byte(char *buf,int count)
{
	int ret=0;
	struct i2c_msg msg;
	msg.addr	= at24c08_client->addr;//0x05
	msg.flags	= 0;	//写
	msg.len		= count;
	msg.buf		= buf;
	ret = i2c_transfer(at24c08_client->adapter,&msg,1);
	if (ret < 0) {
		printk("i2c transfer failed!\n");
		return	-EINVAL;
	}
	return ret;
}


static size_t at24c08_read(struct file *filep, char __user *buf, size_t size, 
	loff_t *ppos)
{
	int ret = 0;
	char *tmp;
	tmp = kmalloc(size,GFP_KERNEL);
	if(tmp==NULL){
		printk("malloc failed!\n");
		return -ENOMEM;
	}
	ret = i2c_read_byte(tmp,size);
	if(ret<0){
		printk("read byte failed!\n");
		ret = -EINVAL;
		goto err0;
	}
	
	ret = copy_to_user(buf,tmp,size);
	if(ret){
		printk("copy data failed!\n");
		ret =-EINVAL;
		goto err0;
	}
	kfree(tmp);
	return size;
    err0:
	    kfree(tmp);
	    return ret;		
}

static ssize_t at24c08_write(struct file *filep, const char __user *buf, size_t size,
			  loff_t *ppos)
{
	int ret = 0;
	char *tmp;
	tmp = kmalloc(size,GFP_KERNEL);
	if(tmp == NULL){
		printk("malloc failed!\n");
		return -ENOMEM;
		goto err0;
	}
	
	ret = copy_from_user(tmp, buf, size);
	if(ret){
		printk("copy data failed!\n");
		ret =-EFAULT;
		goto err0;
	}
	ret = i2c_write_byte(tmp, size);
	if(ret<0){
		printk("write byte failed!\n");
		ret = -EINVAL;
		goto err0;
	}
	
	kfree(tmp);
	return size;
	err0:
		kfree(tmp);
		return ret;
}

struct file_operations at24c08_fops = {
	.owner		=THIS_MODULE,
	.read   	=at24c08_read,
	.write  	=at24c08_write,
};

static int at24c08_probe(struct i2c_client *client,
		       const struct i2c_device_id *id)
{
	int err;
	dev_t devid;
	at24c08_client = client;	
 
 	printk("at24c08 probe !\n");
	/* 创建字符设备 */	
	devid = MKDEV(major, 0);	//从主设备号major,次设备号0得到dev_t类型
	if (major) 
	{
		err=register_chrdev_region(devid, 1, "at24c08");	//注册字符设备
	} 
	else 
	{
		err=alloc_chrdev_region(&devid, 0, 1, "at24c08");	//注册字符设备
		major = MAJOR(devid);	//从dev_t类型得到主设备
	}
	if(err < 0)
		return err;
	
	cdev_init(&at24c08_cdev, &at24c08_fops);
	cdev_add(&at24c08_cdev, devid, 1);
	
	at24c08_class = class_create(THIS_MODULE, "at24c08");
	device_create(at24c08_class, NULL, MKDEV(major, 0), NULL, "at24c08"); /* /dev/at24c08 */
 
	return err;
	
}
static int at24c08_remove(struct i2c_client *client)
{
	printk("at24c08_remove !\n");
	device_destroy(at24c08_class, MKDEV(major, 0));
	class_destroy(at24c08_class);
	cdev_del(&at24c08_cdev);
	unregister_chrdev_region(MKDEV(major, 0), 1);

	return 0;
	
}

struct i2c_device_id at24c08_table[]={
	{"24c08",0x50},
	{}
};

/*构建一个struct i2c_driver结构体*/
static struct i2c_driver at24c08_driver={
	.probe		= at24c08_probe,
	.remove		= at24c08_remove,
	.id_table   = at24c08_table,//记录此驱动服务于哪些设备
	.driver		= {
		.name	= "24c08",//
		.owner  = THIS_MODULE,
	},
};
static int __init at24c08_init(void)
{
	i2c_add_driver(&at24c08_driver);
	return 0;
}
static void __exit at24c08_exit(void)
{
	i2c_del_driver(&at24c08_driver);
}

module_init(at24c08_init);
module_exit(at24c08_exit);
MODULE_LICENSE("GPL");

()测试

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
/*
    ./i2c_test w data    
    ./i2c_test r 
*/
int main(int argc,char **argv)
{
   int fd;
   char register_addr = 0x78;//要写入的地址
   char wbuf[2];//写缓冲区
   char rbuf[2];//读缓冲区
	int size;
   //打开设备
   fd = open("/dev/at24c08", O_RDWR);
   if (fd < 0) 
   {
       perror("open error\n");
       exit(1);
   }
   if(strcmp(argv[1],"w") == 0)
   {//写操作    

       wbuf[0] = register_addr;
       wbuf[1] = atoi(argv[2]);
       
		/*向register_addr地址中写入数据,因为设备地址已经在板级信息中确定了,所以不需要通过ioctl设置设备地址*/
	   if( size= write(fd, wbuf, 2) != 2) 
       {
           perror("write error\n"); 
           exit(1);
       }
   } 
   else 
   {
		//读操作   Random Read
       if(write(fd, &register_addr, 1) != 1) //验证是否从register_addr地址读出
	   { 
           perror("write error\n"); 
       	   exit(1);
       }
       if(read(fd, &rbuf, 1) != 1) 
	   {
   		   perror("read error\n");
           exit(1);
       } 
       else 
       {
           printf("rbuf[0] = %d\n",rbuf[0]);
       }  
   }
   return 0;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值