android 亮灯按钮,MTK6735(Android6.0)-按键灯的实现

一、按键灯的简介

最近调试一下按键灯,今天抽空顺便把的流程分析了一下。按键灯也是一种led,它的使用规则如命名一样,当按键按

下亮灯,如果一定时间不操作的话,一会会灭灯。其实这里的按键灯亮灭策略通常不是驱动来完成的,而是有用户空间来

控制。正如一句老话“驱动注重的不是策略,而是机制”;所以我们在编写驱动只需要完成led的点亮和熄灭功能即可。当

然在实际使用中我们会发现不是所有驱动都如此,有时候平台中也会添加一定的策略,这个本章不作分析。 调试按键灯,

当然首先从硬件开始。按键灯的原理图如下:

eab24ae7ba34a52748a0f11382eac5e2.png

67702dd29d2dc7c6f50aa5cf0485fa05.png

从原理图中我们发现,button-backlight是由两路ISINK控制的,ISNIK是一种类似于PWM的控制器,它可以发出类似PWM的信号,可以通过寄存器的配置 调整其占空比等参数,进入调节输出电流,从而控制led的亮度。

二、按键灯的驱动实现

1. 设备和驱动的匹配

//驱动文件中定义platform_driver

file:kernel-3.18/drivers/misc/mediatek/leds/leds_drv.c

#define USE_PINCTRL

#ifdef USE_PINCTRL

static const struct of_device_id leds_of_ids[] = {

{.compatible = "mediatek,leds-mt65xx",},

{}

};

#endif

static struct platform_driver mt65xx_leds_driver = {

.driver = {

.name = "leds-mt65xx",

.owner = THIS_MODULE,

#ifdef USE_PINCTRL

.of_match_table = leds_of_ids, //和dts中定义一致

#endif

},

.probe = mt65xx_leds_probe,

.remove = mt65xx_leds_remove,

/* .suspend = mt65xx_leds_suspend, */

.shutdown = mt65xx_leds_shutdown,

};//驱动模块的加载

static int __init mt65xx_leds_init(void)

{

ret = platform_driver_register(&mt65xx_leds_driver);

....

return ret;

}//dts中定义leds 的相关节点如:red,green,blue,keyboard-backlight,button-backlight等(后面会用到)

file:kernel-3.18/arch/arm/boot/dts/rlk6737m_35g_c_m0.dts

led0:led@0 {

compatible = "mediatek,red";

led_mode = <0>;

data = <1>;

pwm_config = <0 0 0 0 0>;

};

led1:led@1 {

compatible = "mediatek,green";

led_mode = <0>;

data = <1>;

pwm_config = <0 0 0 0 0>;

};

led2:led@2 {

compatible = "mediatek,blue";

led_mode = <0>;

data = <1>;

pwm_config = <0 0 0 0 0>;

};

led4:led@4 {

compatible = "mediatek,keyboard-backlight";

led_mode = <0>;

data = <1>;

pwm_config = <0 0 0 0 0>;

};

led5:led@5 {

compatible = "mediatek,button-backlight"; //这里着重分析按键灯button-backlight

led_mode = <3>;

data = <1>;

pwm_config = <0 0 0 0 0>;

};

led6:led@6 {

compatible = "mediatek,lcd-backlight";

led_mode = <5>;

data = <1>;

pwm_config = <0 0 0 0 0>;

};//dts中定义和platform_device相关的节点信息

file:kernel-3.18/arch/arm/boot/dts/mt6735m.dtsi

lightsystem: leds {

compatible = "mediatek,leds-mt65xx"; //这里的定义和上面platform_driver中定义的一致

};  上述dts中定义按键灯leds节点配置,内核起来后会解析dts生成相关的设备,并与驱动中的driver匹配,如果匹配成功就执行下面的probe

2.leds probe 函数实现

file:kernel-3.18/drivers/misc/mediatek/leds/leds_drv.c

static int mt65xx_leds_probe(struct platform_device *pdev)

{

int i;

int ret;/* rc; */

//进入probe后,会从dts中获取led节点的mode和data

struct cust_mt65xx_led *cust_led_list = mt_get_cust_led_list();

LEDS_DRV_DEBUG("%s\n", __func__);

get_div_array();

//MT65XX_LED_TYPE_TOTAL为改通用(common)驱动所支持灯的个数

for (i = 0; i < MT65XX_LED_TYPE_TOTAL; i++) {

//观察上面dts中mode如果mode为0(MT65XX_LED_MODE_NONE),则遍历下个元素

if (cust_led_list[i].mode == MT65XX_LED_MODE_NONE) {

g_leds_data[i] = NULL;

continue;

}

...

//将dts中配置的mode,name ,data 保存起来后面会用到

//通过观察上面的button-backlight 的配置,得出其mode为3,data为1,name为button-backlight

g_leds_data[i]->cust.mode = cust_led_list[i].mode;

g_leds_data[i]->cust.data = cust_led_list[i].data;

g_leds_data[i]->cust.name = cust_led_list[i].name;

g_leds_data[i]->cdev.name = cust_led_list[i].name;

g_leds_data[i]->cust.config_data = cust_led_list[i].config_data;/* bei add */

g_leds_data[i]->cdev.brightness_set = mt65xx_led_set; //设置led亮度的函数

//创建sys目录下的brightness等属性节点,提供给用户空间调用

ret = led_classdev_register(&pdev->dev, &g_leds_data[i]->cdev);

...

return 0;

...

}//后面会用到的一些结构的定义

file:kernel-3.18/drivers/misc/mediatek/leds/mt6735/leds_sw.h

enum mt65xx_led_type {

MT65XX_LED_TYPE_RED = 0,

MT65XX_LED_TYPE_GREEN,

MT65XX_LED_TYPE_BLUE,

MT65XX_LED_TYPE_JOGBALL,

MT65XX_LED_TYPE_KEYBOARD,

MT65XX_LED_TYPE_BUTTON,

MT65XX_LED_TYPE_LCD,

MT65XX_LED_TYPE_TOTAL,

};

/**

* led customization data structure

* name : must the same as lights HAL

* mode : control mode

* data :

* PWM: pwm number

* GPIO: gpio id

* PMIC: enum mt65xx_led_pmic

* CUST: custom set brightness function pointer

* config_data: pwm config data

*/

struct cust_mt65xx_led {

char *name;

enum mt65xx_led_mode mode;

long data;

struct PWM_config config_data;

};

/**

* led device node structure with mtk extentions

* cdev: common led device structure

* cust: customization data from device tree

* work: workqueue for specialfied led device

* level: brightness level

* delay_on: on time if led is blinking

* delay_off: off time if led is blinking

*/

struct mt65xx_led_data {

struct led_classdev cdev;

struct cust_mt65xx_led cust;

struct work_struct work;

int level;

int delay_on;

int delay_off;

};

file:kernel-3.18/include/linux/leds.h

enum led_brightness {

LED_OFF= 0,

LED_HALF= 127,

LED_FULL= 255,

};

struct led_classdev {

const char*name;

enum led_brightness brightness;

enum led_brightness max_brightness;

int flags;

...

/* Set LED brightness level */

/* Must not sleep, use a workqueue if needed */

void(*brightness_set)(struct led_classdev *led_cdev,

enum led_brightness brightness);

/* Get LED brightness level */

enum led_brightness (*brightness_get)(struct led_classdev *led_cdev);

.....

struct device*dev;

const struct attribute_group**groups;

struct list_head node;/* LED Device list */

...

};file:kernel-3.18/drivers/misc/mediatek/leds/mt6735/leds.c

char *leds_name[MT65XX_LED_TYPE_TOTAL] = {

"red",

"green",

"blue",

"jogball-backlight",

"keyboard-backlight",

"button-backlight",

"lcd-backlight",

}3.从dts中获取各种led的配置信息

struct cust_mt65xx_led *mt_get_cust_led_list(void)

{

struct cust_mt65xx_led *cust_led_list = get_cust_led_dtsi();

return cust_led_list;

}

struct cust_mt65xx_led *get_cust_led_dtsi(void)

{

struct device_node *led_node = NULL;

...

//MT65XX_LED_TYPE_TOTAL 为led数组长度,即可以支持led的个数

for (i = 0; i < MT65XX_LED_TYPE_TOTAL; i++) {

char node_name[32] = "mediatek,";

pled_dtsi[i].name = leds_name[i];

//使用"mediatek,button-backlight"寻找dtsi中定义的节点

led_node =

of_find_compatible_node(NULL, NULL,

strcat(node_name,

leds_name[i]));

if (!led_node) {

LEDS_DEBUG("Cannot find LED node from dts\n");

pled_dtsi[i].mode = 0;

pled_dtsi[i].data = -1;

} else {

isSupportDTS = true;

//读取led_mode值

ret =

of_property_read_u32(led_node, "led_mode",

&mode);

if (!ret) {

pled_dtsi[i].mode = mode;

LEDS_DEBUG

("The %s's led mode is : %d\n",

pled_dtsi[i].name,

pled_dtsi[i].mode);

}

//读取led的data值

ret =

of_property_read_u32(led_node, "data",

&data);

if (!ret) {

pled_dtsi[i].data = data;

LEDS_DEBUG

("The %s's led data is : %ld\n",

pled_dtsi[i].name,

pled_dtsi[i].data);

}

...

return pled_dtsi;

}4. 创建相关的设备节点

**

* led_classdev_register - register a new object of led_classdev class.

* @parent: The device to register.

* @led_cdev: the led_classdev structure for this device.

*/

int led_classdev_register(struct device *parent, struct led_classdev *led_cdev)

{

led_cdev->dev = device_create_with_groups(leds_class, parent, 0,

led_cdev, led_cdev->groups,

"%s", led_cdev->name);

...

return 0;

}//device_create_with_groups的实现

file:kernel-3.18/drivers/base/core.c

/**

* device_create_with_groups - creates a device and registers it with sysfs

* @class: pointer to the struct class that this device should be registered to

* @parent: pointer to the parent struct device of this new device, if any

* @devt: the dev_t for the char device to be added

* @drvdata: the data to be added to the device for callbacks

* @groups: NULL-terminated list of attribute groups to be created

* @fmt: string for the device's name

*

* This function can be used by char device classes. A struct device

* will be created in sysfs, registered to the specified class.

* Additional attributes specified in the groups parameter will also

* be created automatically.

*

* A "dev" file will be created, showing the dev_t for the device, if

* the dev_t is not 0,0.

* If a pointer to a parent struct device is passed in, the newly created

* struct device will be a child of that device in sysfs.

* The pointer to the struct device will be returned from the call.

* Any further sysfs files that might be required can be created using this

* pointer.

*

* Returns &struct device pointer on success, or ERR_PTR() on error.

*

* Note: the struct class passed to this function must have previously

* been created with a call to class_create().

*/

struct device *device_create_with_groups(struct class *class,

struct device *parent, dev_t devt,

void *drvdata,

const struct attribute_group **groups,

const char *fmt, ...)

{

va_list vargs;

struct device *dev;

va_start(vargs, fmt);

dev = device_create_groups_vargs(class, parent, devt, drvdata, groups,

fmt, vargs);

va_end(vargs);

return dev;

}//device_create_groups_vargs 的实现

static struct device *

device_create_groups_vargs(struct class *class, struct device *parent,

dev_t devt, void *drvdata,

const struct attribute_group **groups,

const char *fmt, va_list args)

{

struct device *dev = NULL;

int retval = -ENODEV;

if (class == NULL || IS_ERR(class))

goto error;

dev = kzalloc(sizeof(*dev), GFP_KERNEL);

if (!dev) {

retval = -ENOMEM;

goto error;

}

device_initialize(dev);

dev->devt = devt;

dev->class = class;

dev->parent = parent;

dev->groups = groups;

dev->release = device_create_release;

dev_set_drvdata(dev, drvdata);

retval = kobject_set_name_vargs(&dev->kobj, fmt, args);

if (retval)

goto error;

retval = device_add(dev);

if (retval)

goto error;

return dev;

error:

put_device(dev);

return ERR_PTR(retval);

}//device_add的实现

int device_add(struct device *dev)

{

...

/* first, register with generic layer. */

/* we require the name to be set before, and pass NULL */

error = kobject_add(&dev->kobj, dev->kobj.parent, NULL);

if (error)

goto Error;

...

error = device_add_attrs(dev);

...

}device_add_attrs的实现这里将会调用device_add_groups,class->dev_groups 作为参数呗传入,此时节点/sys/class/leds/xxx/brightness 已经被

创建 这里的xxx 对应驱动中的red,green,button_backlight,

lcd-backlight ... 已经创建

static int device_add_attrs(struct device *dev)

{

struct class *class = dev->class;

const struct device_type *type = dev->type;

int error;

//这里class->dev_groups先前已经在led-class.c的leds_init中被赋值 leds_class->dev_groups = led_groups;

if (class) {

error = device_add_groups(dev, class->dev_groups);

if (error)

return error;

}

...

return 0;

}5、属性节点的读写方法定义

file:kernel-3.18/drivers/leds/led-class.c

static int __init leds_init(void)

{

leds_class = class_create(THIS_MODULE, "leds"); //创建class对象

...

leds_class->dev_groups = led_groups; //传入brightness节点参数,led属性节点组赋值给leds_class

...

return 0;

}再看led_groups的定义如下:

static const struct attribute_group *led_groups[] = {

&led_group,

#ifdef CONFIG_LEDS_TRIGGERS

&led_trigger_group,

#endif

NULL,

};

static const struct attribute_group led_group = {

.attrs = led_class_attrs,

};

static struct attribute *led_class_attrs[] = {

&dev_attr_brightness.attr,

&dev_attr_max_brightness.attr,

NULL,

};当用户空间读取属性节点时候,会直接输入当前亮度值

static ssize_t brightness_show(struct device *dev,

struct device_attribute *attr, char *buf)

{

struct led_classdev *led_cdev = dev_get_drvdata(dev);

/* no lock needed for this */

led_update_brightness(led_cdev);

return sprintf(buf, "%u\n", led_cdev->brightness);

}

static ssize_t brightness_store(struct device *dev,

struct device_attribute *attr, const char *buf, size_t size)

{

struct led_classdev *led_cdev = dev_get_drvdata(dev);

unsigned long state;

ssize_t ret = -EINVAL;

ret = kstrtoul(buf, 10, &state); //将传入的字符串转换为十进制

if (ret)

return ret;

if (state == LED_OFF)

led_trigger_remove(led_cdev);

__led_set_brightness(led_cdev, state); //设置灯的亮度(亮灭)

return size;

}定义brightness属性的变量

static DEVICE_ATTR_RW(brightness);6.button-backlight 亮灯的实现

通过上面节点的 /sys/class/leds/button-backlight/brightness 写方法brightness_store的定义可知,当brightness节点被用户空间写入后,将触发

执行__led_set_brightness,我们可以通过用户空间传入的参数调节灯的亮度,这里这里的传入参数范围0~255

这里的__led_set_brightness如下定义:

file:kernel-3.18/drivers/leds/leds.h

static inline void __led_set_brightness(struct led_classdev *led_cdev,

enum led_brightness value)

{

if (value > led_cdev->max_brightness)

value = led_cdev->max_brightness; //对传入的值作越界处理

led_cdev->brightness = value;

if (!(led_cdev->flags & LED_SUSPENDED))

led_cdev->brightness_set(led_cdev, value);//执行亮灯操作

}这个函数最终会调用led_cdev->brightness_set,而 led_cdev->brightness_set在leds_drv.c 中已经被赋值过如下:

file:kernel-3.18/drivers/misc/mediatek/leds/leds_drv.c

g_leds_data[i]->cdev.brightness_set = mt65xx_led_set;

static void mt65xx_led_set(struct led_classdev *led_cdev,

enum led_brightness level)

{

struct mt65xx_led_data *led_data =

container_of(led_cdev, struct mt65xx_led_data, cdev);

...

mt_mt65xx_led_set(led_cdev, level);

...

}file:kernel-3.18/drivers/misc/mediatek/leds/mt6735/leds.c

void mt_mt65xx_led_set(struct led_classdev *led_cdev, enum led_brightness level)

{

struct mt65xx_led_data *led_data =

container_of(led_cdev, struct mt65xx_led_data, cdev);

/* unsigned long flags; */

/* spin_lock_irqsave(&leds_lock, flags); */

...

/* do something only when level is changed */

if (led_data->level != level) {

led_data->level = level;

if (strcmp(led_data->cust.name, "lcd-backlight") != 0) {

LEDS_DEBUG("Set NLED directly %d at time %lu\n",

led_data->level, jiffies);

schedule_work(&led_data->work);

} else {

if (level != 0

&& level * CONFIG_LIGHTNESS_MAPPING_VALUE < 255) {

level = 1;

} else {

level =

(level * CONFIG_LIGHTNESS_MAPPING_VALUE) /

255;

}

LEDS_DEBUG

("Set Backlight directly %d at time %lu, mapping level is %d\n",

led_data->level, jiffies, level);

if (MT65XX_LED_MODE_CUST_BLS_PWM == led_data->cust.mode) {

mt_mt65xx_led_set_cust(&led_data->cust,

((((1 <<

MT_LED_INTERNAL_LEVEL_BIT_CNT)

- 1) * level +

127) / 255));

} else {

//最终调用mt_mt65xx_led_set_cust

mt_mt65xx_led_set_cust(&led_data->cust, level);

}

}

}

}mt_mt65xx_led_set_cust的实现

file:kernel-3.18/drivers/misc/mediatek/leds/mt6735/leds.c

int mt_mt65xx_led_set_cust(struct cust_mt65xx_led *cust, int level)

{

struct nled_setting led_tmp_setting = { 0, 0, 0 };

int tmp_level = level;

static bool button_flag;

unsigned int BacklightLevelSupport =

Cust_GetBacklightLevelSupport_byPWM();

switch (cust->mode) {

case MT65XX_LED_MODE_PWM:

if (strcmp(cust->name, "lcd-backlight") == 0) {

bl_brightness_hal = level;

if (level == 0) {

mt_pwm_disable(cust->data,

cust->config_data.pmic_pad);

} else {

if (BacklightLevelSupport ==

BACKLIGHT_LEVEL_PWM_256_SUPPORT)

level = brightness_mapping(tmp_level);

else

level = brightness_mapto64(tmp_level);

mt_backlight_set_pwm(cust->data, level,

bl_div_hal,

&cust->config_data);

}

bl_duty_hal = level;

} else {

if (level == 0) {

led_tmp_setting.nled_mode = NLED_OFF;

mt_led_set_pwm(cust->data, &led_tmp_setting);

mt_pwm_disable(cust->data,

cust->config_data.pmic_pad);

} else {

led_tmp_setting.nled_mode = NLED_ON;

mt_led_set_pwm(cust->data, &led_tmp_setting);

}

}

return 1;

case MT65XX_LED_MODE_GPIO:

LEDS_DEBUG("brightness_set_cust:go GPIO mode!!!!!\n");

return ((cust_set_brightness) (cust->data)) (level);

//这里的MT65XX_LED_MODE_PMIC对应button-backlight的配置的mode=3

case MT65XX_LED_MODE_PMIC:

/* for button baclight used SINK channel, when set button ISINK,

don't do disable other ISINK channel */

//使用button-backlight的调用如下:

if ((strcmp(cust->name, "button-backlight") == 0)) {

if (button_flag == false) {

switch (cust->data) {

case MT65XX_LED_PMIC_NLED_ISINK0:

button_flag_isink0 = 1;

break;

case MT65XX_LED_PMIC_NLED_ISINK1:

button_flag_isink1 = 1;

break;

case MT65XX_LED_PMIC_NLED_ISINK2:

button_flag_isink2 = 1;

break;

case MT65XX_LED_PMIC_NLED_ISINK3:

button_flag_isink3 = 1;

break;

default:

break;

}

button_flag = true;

}

}

return mt_brightness_set_pmic(cust->data, level, bl_div_hal);

case MT65XX_LED_MODE_CUST_LCM:

if (strcmp(cust->name, "lcd-backlight") == 0)

bl_brightness_hal = level;

LEDS_DEBUG("brightness_set_cust:backlight control by LCM\n");

/* warning for this API revork */

return ((cust_brightness_set) (cust->data)) (level, bl_div_hal);

case MT65XX_LED_MODE_CUST_BLS_PWM:

if (strcmp(cust->name, "lcd-backlight") == 0)

bl_brightness_hal = level;

return ((cust_set_brightness) (cust->data)) (level);

case MT65XX_LED_MODE_NONE:

default:

break;

}

return -1;

}也就是说当用户对属性节点 /sys/class/leds/button-backlight/brightness 写入时最终调用mt_brightness_set_pmic函数,

mt_brightness_set_pmic的实现如下:

file:kernel-3.18/drivers/misc/mediatek/leds/mt6735/leds.c

int mt_brightness_set_pmic(enum mt65xx_led_pmic pmic_type, u32 level, u32 div)

{

static bool first_time = true;

LEDS_DEBUG("PMIC#%d:%d\n", pmic_type, level);

mutex_lock(&leds_pmic_mutex);

if (pmic_type == MT65XX_LED_PMIC_NLED_ISINK0) {

if ((button_flag_isink0 == 0) && (first_time == true)) {/* button

flag ==0, means this ISINK is not for button backlight */

if (button_flag_isink1 == 0)

pmic_set_register_value(PMIC_ISINK_CH1_EN, NLED_OFF);/* sw

workround for sync leds status */

if (button_flag_isink2 == 0)

pmic_set_register_value(PMIC_ISINK_CH2_EN,

NLED_OFF);

if (button_flag_isink3 == 0)

pmic_set_register_value(PMIC_ISINK_CH3_EN,

NLED_OFF);

first_time = false;

}

pmic_set_register_value(PMIC_RG_DRV_32K_CK_PDN, 0x0);/* Disable power down */

pmic_set_register_value(PMIC_RG_DRV_ISINK0_CK_PDN, 0);

pmic_set_register_value(PMIC_RG_DRV_ISINK0_CK_CKSEL, 0);

pmic_set_register_value(PMIC_ISINK_CH0_MODE, ISINK_PWM_MODE);

pmic_set_register_value(PMIC_ISINK_CH0_STEP, ISINK_3);/* 16mA */

pmic_set_register_value(PMIC_ISINK_DIM0_DUTY, 15);

pmic_set_register_value(PMIC_ISINK_DIM0_FSEL, ISINK_1KHZ);/* 1KHz */

if (level)

pmic_set_register_value(PMIC_ISINK_CH0_EN, NLED_ON);

else

pmic_set_register_value(PMIC_ISINK_CH0_EN, NLED_OFF);

mutex_unlock(&leds_pmic_mutex);

return 0;

} else if (pmic_type == MT65XX_LED_PMIC_NLED_ISINK1) {

if ((button_flag_isink1 == 0) && (first_time == true)) {/* button

flag ==0, means this ISINK is not for button backlight */

if (button_flag_isink0 == 0)

pmic_set_register_value(PMIC_ISINK_CH0_EN, NLED_OFF);/* sw

workround for sync leds status */

if (button_flag_isink2 == 0)

pmic_set_register_value(PMIC_ISINK_CH2_EN,

NLED_OFF);

if (button_flag_isink3 == 0)

pmic_set_register_value(PMIC_ISINK_CH3_EN,

NLED_OFF);

first_time = false;

}

pmic_set_register_value(PMIC_RG_DRV_32K_CK_PDN, 0x0);/* Disable power down */

pmic_set_register_value(PMIC_RG_DRV_ISINK1_CK_PDN, 0);

pmic_set_register_value(PMIC_RG_DRV_ISINK1_CK_CKSEL, 0);

pmic_set_register_value(PMIC_ISINK_CH1_MODE, ISINK_PWM_MODE);

pmic_set_register_value(PMIC_ISINK_CH1_STEP, ISINK_3);/* 16mA */

pmic_set_register_value(PMIC_ISINK_DIM1_DUTY, 15);

pmic_set_register_value(PMIC_ISINK_DIM1_FSEL, ISINK_1KHZ);/* 1KHz */

if (level)

pmic_set_register_value(PMIC_ISINK_CH1_EN, NLED_ON);

else

pmic_set_register_value(PMIC_ISINK_CH1_EN, NLED_OFF);

mutex_unlock(&leds_pmic_mutex);

return 0;

}

mutex_unlock(&leds_pmic_mutex);

return -1;

}上述pmic_set_register_value的操作就是对ISINK具体寄存器的操作,本文不作深入研究

三、总结 通过上述的分析,我们大致可以看出在mtk平台上leds系列的驱动流程大致如下,先在dts中定义各个led节点的配置,配 置如mode,name,data 预留给driver调用,然后创建common drver (通用驱动)对各个led统一管理,在通用驱动中各个不 同类型led 通过数组区分,common driver对各个led进行统一的设备注册,属性节点创建等。当然led的种类繁多还有充 电指示灯,呼吸灯等,在加上每种灯硬件配置不一样驱动实现方式也不同,这个需要另行分析了。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值