RK3568休眠按键唤醒驱动(低功耗)

该文章已生成可运行项目,

RK3568休眠按键唤醒驱动(低功耗)


1. 数据结构定义

1.1 gpio_keys_button — 按键属性结构体

  • 描述单个 GPIO 按键属性,包括按键代码、触发方式、唤醒能力、去抖时间等。

1.2 gpio_keys_platform_data — 平台数据

  • 包含所有按键数组,按键数目,是否支持重复按压等。

1.3 gpio_button_data — 按键运行时数据

  • 关联单个按键的 GPIO 描述符、输入设备、按键码、工作队列等信息。

1.4 gpio_keys_drvdata — 驱动私有数据

  • 包含平台数据、输入设备、按键映射表及所有按键数据数组。

2. 核心函数详解

2.1 gpio_keys_gpio_work_func — 按键事件工作处理函数

  • 读取 GPIO 值,触发输入事件,支持去抖;
  • 处理完后释放唤醒锁。

2.2 gpio_keys_gpio_isr — 中断服务函数

  • 按键中断触发,保持唤醒锁,调度延迟工作处理按键事件。

2.3 gpio_keys_get_data_from_devtree — 解析设备树数据

  • 从设备树获取按键节点数量及每个按键属性;
  • 支持读取 labelcodewakeupdebounce_intervalpress_type

2.4 gpio_keys_setup_key — 按键初始化设置

  • 获取 GPIO 描述符;
  • 计算 GPIO 号、分组,打印信息;
  • 获取中断号,注册中断处理函数;
  • 初始化输入设备能力。

2.5 gpio_keys_probe — 驱动 probe 函数

  • 驱动入口,调用设备树解析,分配驱动私有数据;
  • 分配输入设备,初始化按键,注册输入设备;
  • 支持唤醒初始化。

3. 电源管理相关

  • gpio_keys_button_enable_wakeup / gpio_keys_enable_wakeup:启用按键唤醒中断;
  • gpio_keys_button_disable_wakeup / gpio_keys_disable_wakeup:禁用按键唤醒中断;
  • gpio_keys_suspend / gpio_keys_resume:系统挂起和恢复时调用,管理唤醒功能。

4. 驱动注册与模块入口

  • 定义设备树匹配表,匹配 compatible = "my-keys"
  • 平台驱动结构体绑定 probe 和电源管理操作;
  • 驱动初始化注册和退出注销;
  • 使用 late_initcall_sync 延迟初始化驱动;
  • 声明模块 GPL 许可证。

5.完整驱动解析

#include <linux/module.h>
#include <linux/types.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/sched.h>
#include <linux/pm.h>
#include <linux/slab.h>
#include <linux/sysctl.h>
#include <linux/proc_fs.h>
#include <linux/delay.h>
#include <linux/platform_device.h>
#include <linux/input.h>
#include <linux/workqueue.h>
#include <linux/gpio.h>
#include <linux/gpio/consumer.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <linux/spinlock.h>
#include <dt-bindings/input/gpio-keys.h>
#include <linux/device.h>

// 定义打印信息的宏,方便调试时输出信息,格式统一,前缀为 "mytips: "
#define mytips(str, ...) printk("mytips: " str, ##__VA_ARGS__)

/* 
 * 按键的描述结构体,保存单个按键的相关信息 
 */
struct gpio_keys_button {
    unsigned int code;             // 按键对应的输入事件代码,如 KEY_VOLUMEUP
    int active_low;                // 按键是否为低电平有效,1表示低电平有效
    const char *label;             // 按键的标签名称
    unsigned int type;             // 输入事件类型,一般为 EV_KEY
    int wakeup;                   // 是否支持唤醒系统,1支持,0不支持
    int debounce_interval;         // 按键去抖延迟,单位毫秒
    int value;                     // 当前按键值(内部使用)
    /*unsigned int trigger;*/       // 触发类型,注释掉未使用
    unsigned int press_type;       // 按压类型,0为短按,>0为长按,单位秒
};

/* 
 * 平台数据结构,包含所有按键的信息 
 */
struct gpio_keys_platform_data {
    const struct gpio_keys_button *buttons;  // 按键数组指针
    int nbuttons;                            // 按键数量
    unsigned int rep:1;                      // 是否支持重复按键事件(autorepeat)
    const char *label;                       // 平台设备标签
};

/* 
 * 按键运行时数据结构,保存按键相关的工作队列、GPIO描述符等 
 */
struct gpio_button_data {
    const struct gpio_keys_button *button;  // 指向对应的按键描述结构体
    struct input_dev *input;                 // 输入设备指针
    struct gpio_desc *gpiod;                 // GPIO描述符,用于读取GPIO电平
    unsigned short *code;                    // 指向按键码映射的指针
    struct delayed_work work;                // 延迟工作,用于去抖处理
    unsigned int press;                      // 按键当前状态,1按下,0松开
    unsigned int irq;                        // GPIO对应的中断号
};

/* 
 * 驱动私有数据结构,包含平台数据、输入设备及所有按键数据 
 */
struct gpio_keys_drvdata {
    const struct gpio_keys_platform_data *pdata;  // 平台数据指针
    struct input_dev *input;                        // 输入设备指针
    unsigned short *keymap;                         // 按键码数组
    struct gpio_button_data data[];                 // 按键数据数组,长度为按键数
};

/* 函数声明,驱动内部使用 */
static int gpio_keys_enable_wakeup(struct gpio_keys_drvdata *ddata);
static int gpio_keys_button_enable_wakeup(struct gpio_button_data *bdata);
static void gpio_keys_gpio_work_func(struct work_struct *work);
static irqreturn_t gpio_keys_gpio_isr(int irq, void *dev_id);
static struct gpio_keys_platform_data* gpio_keys_get_data_from_devtree(struct device *dev);
static int gpio_keys_setup_key(struct platform_device *pdev, struct input_dev *input,
                struct gpio_keys_drvdata *ddata, const struct gpio_keys_button *button,
                int idx, struct fwnode_handle *child);
static int gpio_keys_probe(struct platform_device *pdev);

/**
 * gpio_keys_gpio_work_func - 按键事件的延迟工作处理函数
 * @work: 延迟工作结构体指针
 *
 * 读取GPIO值,生成按键输入事件,并同步输入设备状态。
 * 处理完成后释放系统唤醒锁。
 */
static void gpio_keys_gpio_work_func(struct work_struct *work){
    // 通过容器宏获取 gpio_button_data 结构体
	struct gpio_button_data *bdata =
        container_of(work, struct gpio_button_data, work.work);

	struct input_dev *input = bdata->input;  // 输入设备指针
	int val;

	// 读取GPIO电平,支持睡眠状态下调用
	val = gpiod_get_value_cansleep(bdata->gpiod);
	if (val < 0) {
		mytips("err get gpio val: %d\n", val);
		return;
	}

	// 发送按键事件,EV_KEY事件,值为val转换为布尔值(0或1)
	input_event(input, EV_KEY, *bdata->code, !!val);
	input_sync(input);  // 同步输入设备,确保事件发送

	bdata->press = !!val;  // 记录按键当前状态

	// 如果按键支持唤醒,释放唤醒锁
	if (bdata->button->wakeup)
		pm_relax(bdata->input->dev.parent);
}

/**
 * gpio_keys_gpio_isr - GPIO中断处理函数
 * @irq: 中断号
 * @dev_id: 设备ID指针,指向 gpio_button_data 结构体
 *
 * 中断触发时调用,主要是启动延迟工作处理按键事件,并管理系统唤醒锁。
 */
static irqreturn_t gpio_keys_gpio_isr(int irq, void *dev_id){
	struct gpio_button_data *bdata = dev_id;

	// 如果该按键支持唤醒,则保持唤醒锁,防止系统睡眠
	if(bdata->button->wakeup) 
		pm_stay_awake(bdata->input->dev.parent);

	// 重新调度延迟工作,延迟时间为去抖时间加上按键按下时的长按时长(单位转换为jiffies)
	mod_delayed_work(system_wq,
             &bdata->work,
             msecs_to_jiffies(bdata->button->debounce_interval + !bdata->press * bdata->button->press_type * 1000));

	return IRQ_HANDLED;  // 中断已处理
}

/**
 * gpio_keys_get_data_from_devtree - 解析设备树,获取平台数据
 * @dev: 设备结构体指针
 *
 * 从设备树节点读取子节点数量及每个按键的属性,填充平台数据结构。
 */
static struct gpio_keys_platform_data*
gpio_keys_get_data_from_devtree(struct device *dev){
	int nbuttons = 0;
	struct gpio_keys_platform_data *pdata;
	struct gpio_keys_button *button;
	struct fwnode_handle *child;

	// 获取设备子节点数量,即按键数量
	nbuttons = device_get_child_node_count(dev);
	if(!nbuttons){
		mytips("no keys dev\n");
		return ERR_PTR(-ENODEV);
	}
	mytips("button number: %d\n", nbuttons);

	// 分配平台数据结构,包含按键数组空间
	pdata = devm_kzalloc(dev, sizeof(*pdata) + nbuttons * sizeof(*button), GFP_KERNEL);
	if(!pdata){
		mytips("data alloc failed\n");
		return ERR_PTR(-ENOMEM);
	}

	// 按键数组紧跟平台数据结构后面
	button = (struct gpio_keys_button*)(pdata + 1);

	pdata->buttons = button;
	pdata->nbuttons = nbuttons;

	// 读取平台设备标签
	device_property_read_string(dev, "label", &pdata->label);

	// 读取是否支持自动重复(autorepeat)
	pdata->rep = device_property_read_bool(dev, "autorepeat");

	// 遍历子节点,读取每个按键的属性
	device_for_each_child_node(dev, child){
		fwnode_property_read_string(child, "label", &button->label);  // 读取标签

		button->type = EV_KEY;  // 按键类型固定为EV_KEY

		// 读取按键代码,失败则使用默认值1
		if(fwnode_property_read_u32(child, "code", &button->code)){
			mytips("use default code : 1");
			button->code = 1;
		}
		mytips("code = %u\n", button->code);

		// 读取是否支持唤醒
		button->wakeup = fwnode_property_read_bool(child, "wakeup");
		mytips("wakeup=%d\n", button->wakeup);

		// 读取去抖时间,失败则默认10ms
		if(fwnode_property_read_u32(child, "debounce_interval", &button->debounce_interval)){
			button->debounce_interval = 10;
		}
		mytips("debounce interval=%d\n", button->debounce_interval);

		// 读取按压类型,失败则默认0(短按)
		if(fwnode_property_read_u32(child, "press_type", &button->press_type)){
			button->press_type = 0;
		}

		button ++;  // 指向下一个按键
	}

	return pdata;
}

/**
 * gpio_keys_setup_key - 初始化单个按键
 * @pdev: 平台设备指针
 * @input: 输入设备指针
 * @ddata: 驱动私有数据指针
 * @button: 按键描述结构体指针
 * @idx: 按键索引
 * @child: 设备树子节点句柄
 *
 * 获取GPIO、IRQ,初始化延迟工作,设置输入设备能力,申请中断等。
 */
static int gpio_keys_setup_key(struct platform_device *pdev,
                struct input_dev *input,
                struct gpio_keys_drvdata *ddata,
                const struct gpio_keys_button *button,
                int idx,
                struct fwnode_handle *child){

	const char *label = button->label ? button->label : "my_keys";  // 标签名称,默认my_keys
	struct device *dev = &pdev->dev;
	struct gpio_button_data *bdata = &ddata->data[idx];  // 当前按键的数据结构体
	irq_handler_t isr;
	unsigned long irqflags;
	int gpio = -1, bank = -1, group = -1;
	int irq;
	int error;
	bool active_low;
	char gpioname[10];

	bdata->input = input;
	bdata->button = button;

	// 获取GPIO描述符,方向为输入
	bdata->gpiod = devm_fwnode_get_gpiod_from_child(dev, NULL, child, GPIOD_IN, label);
	if(IS_ERR(bdata->gpiod)){
		mytips("failed to get gpio, errnum:%ld\n", PTR_ERR(bdata->gpiod));
		return PTR_ERR(bdata->gpiod);
	}

	gpio = desc_to_gpio(bdata->gpiod);  // 获取GPIO编号
	
	// 计算GPIO分组及银行编号,用于打印
	group = gpio / 32;
	bank = (gpio - (group * 32)) / 8;
	sprintf(gpioname, "GPIO%d%c%d", bank, 'A' + bank, gpio - group * 32 - bank * 8);

	mytips("gpio %d : %s\n", gpio, gpioname);

	active_low = gpiod_is_active_low(bdata->gpiod);  // 判断是否低电平有效
	mytips("active low : %d\n", active_low);
	
	irq = gpiod_to_irq(bdata->gpiod);  // 获取GPIO对应的中断号
	if(irq < 0){
		mytips("err get irq for gpio %s\n", gpioname);
		return irq;
	}
	bdata->irq = irq;
	mytips("irq %d\n attach %s\n", irq, gpioname);

	// 初始化延迟工作,绑定工作函数
	INIT_DELAYED_WORK(&bdata->work, gpio_keys_gpio_work_func);
	
	bdata->press = 0;  // 初始化按键状态为未按下

	// 关联keymap数组中对应位置的按键码
	bdata->code = &ddata->keymap[idx];
	*bdata->code = button->code;

	// 设置输入设备能力,支持按键事件和对应按键码
	input_set_capability(input, EV_KEY, *bdata->code);

	// 注册中断处理函数,支持上升沿和下降沿触发
	isr = gpio_keys_gpio_isr;
	irqflags = IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING;
	error = devm_request_any_context_irq(dev, bdata->irq, isr, irqflags, label, bdata);
	if(error < 0) {
		mytips("request irq %d failed\n", bdata->irq);
		return error;
	}
	
	return 0;
}

/**
 * gpio_keys_probe - 驱动的probe函数,初始化设备
 * @pdev: 平台设备指针
 *
 * 1. 解析设备树获取按键平台数据;
 * 2. 分配驱动私有数据结构和输入设备;
 * 3. 初始化每个按键;
 * 4. 注册输入设备;
 * 5. 设置设备唤醒功能(如支持)。
 */
static int gpio_keys_probe(struct platform_device *pdev){
	struct device *dev = &pdev->dev;
	const struct gpio_keys_platform_data *pdata; 
	struct fwnode_handle *child = NULL;
	struct gpio_keys_drvdata *ddata;
	struct input_dev *input;
	size_t size;
	int i, error, wakeup = 0;
	
	// 从设备树解析平台数据
	pdata = gpio_keys_get_data_from_devtree(dev);
	if(IS_ERR(pdata))
		return PTR_ERR(pdata);

	// 计算分配驱动私有数据的大小,包含按键数据数组
	size = sizeof(struct gpio_keys_drvdata) + 
			pdata->nbuttons * sizeof(struct gpio_button_data);
	ddata = devm_kzalloc(dev, size, GFP_KERNEL);
	if(!ddata) {
		mytips("failed to allocate ddata\n");
		return -ENOMEM;
	}

	// 分配按键码数组
	ddata->keymap = devm_kcalloc(dev, pdata->nbuttons, sizeof(ddata->keymap[0]), GFP_KERNEL);
	if(!ddata->keymap)
		return -ENOMEM;

	// 分配输入设备
	input = devm_input_allocate_device(dev);
	if(!input) {
		mytips("failed to allocate input dev\n");
		return -ENOMEM;
	}

	ddata->pdata = pdata;
	ddata->input = input;
	
	input->name = pdev->name;          // 输入设备名称,使用平台设备名
	input->dev.parent = dev;           // 设备父设备指针

	input->keycode = ddata->keymap;   // 按键码映射
	input->keycodesize = sizeof(ddata->keymap[0]);
	input->keycodemax = pdata->nbuttons;

	// 如果支持重复按键事件,则设置EV_REP位
	if(pdata->rep)
		__set_bit(EV_REP, input->evbit);

	// 逐个初始化按键
	for(i = 0; i < pdata->nbuttons; i ++) {
		const struct gpio_keys_button *button = &pdata->buttons[i];

		child = device_get_next_child_node(dev, child);
		if(!child) {
			mytips("no child device node\n");
			return -EINVAL;
		}

		error = gpio_keys_setup_key(pdev, input, ddata, button, i, child);
		if(error) {
			fwnode_handle_put(child);
			return error;
		}

		// 判断是否存在支持唤醒的按键
		if(button->wakeup)
			wakeup = 1;
	}
	fwnode_handle_put(child);

	// 注册输入设备
	error = input_register_device(input);
	if(error) {
		mytips("unable to register input dev\n");
		return error;
	}

	// 保存驱动私有数据指针
	platform_set_drvdata(pdev, ddata);
    input_set_drvdata(input, ddata);

	// 初始化设备唤醒功能
	if(wakeup){
		error = device_init_wakeup(dev, wakeup);
		mytips("init wakeup,ret = %d\n", error);
		// gpio_keys_enable_wakeup(ddata); // 这里注释掉,可根据需求打开
	}

	return 0;
}

/**
 * gpio_keys_button_enable_wakeup - 使能单个按键的中断唤醒功能
 * @bdata: 按键数据结构体指针
 *
 * 使能中断唤醒并设置中断触发类型为双边沿。
 */
static int
gpio_keys_button_enable_wakeup(struct gpio_button_data *bdata)
{
    int error;

    // 使能中断唤醒
    error = enable_irq_wake(bdata->irq);
    if (error) {
        mytips("failed setup wakeup source IRQ: %d by err: %d\n",
            bdata->irq, error);
        return error;
    }

    // 设置中断触发类型为上升沿和下降沿触发
    error = irq_set_irq_type(bdata->irq, IRQ_TYPE_EDGE_RISING | IRQ_TYPE_EDGE_FALLING);
    if (error) {
        mytips("failed to set wakeup trigger for IRQ %d: %d\n", bdata->irq, error);
        disable_irq_wake(bdata->irq);  // 失败则撤销唤醒使能
        return error;
    }

    return 0;
}

/**
 * gpio_keys_enable_wakeup - 使能所有支持唤醒的按键
 * @ddata: 驱动私有数据指针
 *
 * 遍历所有按键,调用 gpio_keys_button_enable_wakeup 使能唤醒。
 */
static int 
gpio_keys_enable_wakeup(struct gpio_keys_drvdata *ddata)
{
    struct gpio_button_data *bdata;
    int error;
    int i;

    for (i = 0; i < ddata->pdata->nbuttons; i++) {
        bdata = &ddata->data[i];
        if (bdata->button->wakeup) {
            error = gpio_keys_button_enable_wakeup(bdata);
            if (error)
                return error;
        }
    }

    return 0;
}

/**
 * gpio_keys_button_disable_wakeup - 禁用单个按键的中断唤醒
 * @bdata: 按键数据结构体指针
 */
static void __maybe_unused
gpio_keys_button_disable_wakeup(struct gpio_button_data *bdata)
{
    int error;

    error = disable_irq_wake(bdata->irq);
    if (error)
        mytips("failed to disable wakeup src IRQ %d: %d\n", bdata->irq, error);
}

/**
 * gpio_keys_disable_wakeup - 禁用所有支持唤醒的按键
 * @ddata: 驱动私有数据指针
 */
static void __maybe_unused
gpio_keys_disable_wakeup(struct gpio_keys_drvdata *ddata)
{
    struct gpio_button_data *bdata;
    int i;

    for (i = 0; i < ddata->pdata->nbuttons; i++) {
        bdata = &ddata->data[i];
        if (irqd_is_wakeup_set(irq_get_irq_data(bdata->irq)))
            gpio_keys_button_disable_wakeup(bdata);
    }
}

/**
 * gpio_keys_suspend - 设备挂起回调
 * @dev: 设备指针
 *
 * 挂起时使能唤醒功能(如支持)。
 */
static int __maybe_unused gpio_keys_suspend(struct device *dev)
{
    struct gpio_keys_drvdata *ddata = dev_get_drvdata(dev);
    int error;

    if (device_may_wakeup(dev)) {
        error = gpio_keys_enable_wakeup(ddata);
        if (error)
            return error;
    }
    return 0;
}

/**
 * gpio_keys_resume - 设备恢复回调
 * @dev: 设备指针
 *
 * 恢复时禁用唤醒功能。
 */
static int __maybe_unused gpio_keys_resume(struct device *dev)
{
    struct gpio_keys_drvdata *ddata = dev_get_drvdata(dev);

    if (device_may_wakeup(dev)) {
        gpio_keys_disable_wakeup(ddata);
    }

    return 0;
}

// 定义简单的电源管理操作结构体,绑定挂起和恢复回调函数
static SIMPLE_DEV_PM_OPS(gpio_keys_pm_ops, gpio_keys_suspend, gpio_keys_resume);

// 设备树匹配表,匹配 compatible 字段为 "my-keys"
static const struct of_device_id gpio_keys_of_match[] = {
    { .compatible = "my-keys", },
    { },
};

MODULE_DEVICE_TABLE(of, gpio_keys_of_match);

// 平台驱动结构体定义
static struct platform_driver gpio_keys_device_driver = {
    .probe      = gpio_keys_probe,      // probe 函数入口
    .driver     = {
        .name   = "my-keys",            // 驱动名称
        .of_match_table = gpio_keys_of_match,  // 设备树匹配表
		.pm = &gpio_keys_pm_ops,        // 电源管理操作
    }
};

// 驱动模块初始化函数
static int __init gpio_keys_init(void)
{
    return platform_driver_register(&gpio_keys_device_driver);
}

// 驱动模块退出函数
static void __exit gpio_keys_exit(void)
{
    platform_driver_unregister(&gpio_keys_device_driver);
}

// 延迟初始化模块,保证所有子系统初始化完成后再加载
late_initcall_sync(gpio_keys_init);
module_exit(gpio_keys_exit);

MODULE_LICENSE("GPL");  // 声明GPL许可,保证开源兼容性

本文章已经生成可运行项目
### RK356X 芯片休眠唤醒实现方法 #### 1. 休眠唤醒功能概述 休眠唤醒是指系统在低功耗模式下保存当前状态,并能够在特定条件下恢复到正常工作状态的功能。对于嵌入式系统而言,这一特性至关重要,因为它不仅有助于节省能源,还能提升用户体验[^1]。 #### 2. 硬件设计指南 为了确保RK356X平台上的设备可以稳定地进入和退出休眠状态,硬件设计师需注意电源管理单元的设计,确保其能响应外部中断请求(如按下Power按钮),并通过GPIO或其他接口向处理器发送唤醒信号。此外,还需考虑时钟源的选择与配置,以支持最低限度的操作频率,从而减少能耗。 #### 3. 软件开发指南 针对Rockchip平台的休眠唤醒功能实现,主要涉及以下几个方面: - **驱动程序编写**:需要为具体的外设(例如Wi-Fi模块RTL8821CS)定制化开发相应的驱动代码,以便于正确处理不同类型的睡眠指令以及后续的重新初始化过程。 - **系统配置调整**:修改内核参数或BIOS设置来优化系统的待机行为;这可能涉及到更改默认超时时长、启用/禁用某些服务等操作。 - **应用程序层的支持**:确保高层应用能够感知并适当地应对即将发生的挂起事件,比如提前释放资源或者保存未完成的工作进度。 ```c // 示例:注册一个用于监听电源按键事件的回调函数 static int power_key_event(struct input_handle *handle, unsigned int type, unsigned int code, int value) { if (type == EV_KEY && code == KEY_POWER && value == 0) { // 当检测到松开电源键时触发唤醒逻辑 pm_wakeup_request(); // 发送唤醒请求给PMU } return 0; } ``` #### 4. 调试与优化 当遇到类似于“RK3568 RTL8821CS Wi-Fi模块在一分钟后的唤醒过程中无法找到任何可用接入点”的情况时,可以从多个角度入手解决问题: - 检查固件版本是否最新; - 验证无线网卡是否已成功加载必要的微码; - 排除其他可能导致干扰的因素,如蓝牙共存问题; - 尝试增加延迟时间让WiFi模组有更多的时间去扫描周围的SSID列表[^2]。 另外,关于较长时间才亮屏的现象,则可能是由于某个组件在恢复期间消耗了大量的CPU周期所致。可以通过`dmesg | grep wakeup`命令获取详细的日志记录,进而定位具体原因所在[^3]。 #### 5. 常见问题与解决方案 面对诸如“息屏休眠后电流超出预期范围”的挑战,建议采取以下措施来进行诊断: - 审视整个电路板布局是否存在不必要的漏电路径; - 对比官方给出的标准值判断实际测量数据是否有异常波动; - 如果发现是由于进入了更深级别的省电模式所引起的,则应适当放宽阈值限制或是缩短过渡期长度,以此达到平衡性能与续航之间的关系[^4]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值