android系统移植之按键驱动篇

平台:MX53_QSB开发板

MX53_QSB开发板上一起有四个按键,分别为RESET,POWER,USER1,USER2。其中RESET为纯硬件复位按键,无须软件控制。POWER,USER1,USER2三个按键均需要程序控制。默认BSP包中将三个按钮全设置为上升和下降沿触发,当系统起来后,按下POWER键,进入睡眠状态,这时再按下POWER键唤醒时,系统系统被唤醒,但是一旦手松下,又触发了POWER键的中断,系统又睡下去了。在进入睡眠状态后,只有按USER1和USER2这两个键,才能正常唤醒。因此,这里有BUG需修复。

按键驱动有两个,一个为矩阵键盘驱动,路径为:

\drivers\input\keyboard\mxc_keyb.c

一个为GPIO接口的键盘驱动,路径为:

\drivers\input\keyboard\gpio_keys.c

前者用于多按键的情况,如果按键比较少,后者就可以了,一般情况下,android系统只需几个按键就可以了,所以大多数情况下,都是使用的gpio_keys.c。下面我们将详细分析该驱动的工作流程。

在module_init函数中,在总线上注册名为gpio-keys的驱动,这时将夫在总线上查找是否存在同名的设备。系统初始化时,mx53_loco.c中,mxc_board_init函数已调用了按键初始化函数loco_add_device_buttons(),同时pdev的数据结构体中定义了按键的相关信息如下:

#define GPIO_BUTTON(gpio_num, ev_code, act_low,descr, wake)   \

{                                                                         \

         .gpio           = gpio_num,                                    \

         .type           = EV_KEY,                                     \

         .code          = ev_code,                             \

         .active_low = act_low,                              \

         .desc           = "btn " descr,                                 \

         .wakeup               = wake,                                           \

}

 

static struct gpio_keys_button loco_buttons[] = {

         GPIO_BUTTON(MX53_nONKEY,KEY_POWER, 1, "power", 0),

         GPIO_BUTTON(USER_UI1,KEY_BACK, 1, "back", 0),

         GPIO_BUTTON(USER_UI2,KEY_HOME, 1, "home", 0),

};

 

static struct gpio_keys_platform_dataloco_button_data = {

         .buttons      = loco_buttons,

         .nbuttons    = ARRAY_SIZE(loco_buttons),

};

可见,结构体定义了三个GPIO,分别为power,back以及home。注意GPIO_BUTTON函数中的实参,第一个为对应的GPIO,纯硬件特性,第二个为按键的键值,在linux/input.h中定义:

#defineKEY_POWER         116

#defineKEY_HOME           102

#defineKEY_BACK            158

第三个参数为1,表明按下去为1,抬起为0;第四个参数为按键名称描述,无关紧要;第5个参数为wakeup,看名称好像与休眠唤醒有关,实际测试修改为1后,没有发现有什么异常。这几个参数是后续添加新的按键,或者更改按键功能的关键。

再回到gpio-keys.c中,找到同名设备后,探测函数gpio_keys_probe得到执行,调用input_allocate_device函数创建一个input设备,再通过一个for循环,调用gpio_keys_setup_key和input_set_capability函数,设置上表中列出的三个IO口的中断函数以及按键功能。后面用到了sysfs_create_group函数创建了基于sys系统的文件属性组,具体可以在?sys/devices/platform/gpio-keys目录下找到gpio_keys_attr_group结构体中attrs组对应的gpio_keys_attrs结构体中的几个属性文件,gpio_keys_attrs结构体描述如下:

static struct attribute *gpio_keys_attrs[] = {

         &dev_attr_keys.attr,

         &dev_attr_switches.attr,

         &dev_attr_disabled_keys.attr,

         &dev_attr_disabled_switches.attr,

         NULL,

};

dev_attr_disabled_keys和dev_attr_disabled_switches在前面做了如下声明:

static DEVICE_ATTR(disabled_keys, S_IWUSR |S_IRUGO,

                      gpio_keys_show_disabled_keys,

                      gpio_keys_store_disabled_keys);

static DEVICE_ATTR(disabled_switches, S_IWUSR |S_IRUGO,

                      gpio_keys_show_disabled_switches,

                      gpio_keys_store_disabled_switches);

在android系统终端,我们可以进入该路径查看是否存在,以及他们的文件属性如下:


注意上面四个文件的读写属性。

可见,这里留有在系统中操作按键的后门,具体以后再分析。接下来,调用input_register_device函数向输入子系统注册input_dev,结束探测函数初始化。

探测函数的关键点在gpio_keys_setup_key函数中,相关代码如下:

static int __devinit gpio_keys_setup_key(structplatform_device *pdev,

                                                struct gpio_button_data *bdata,

                                                struct gpio_keys_button *button)

{

         char *desc= button->desc ? button->desc : "gpio_keys";//从loco_buttons数组中获得按键的描述名称

         structdevice *dev = &pdev->dev;

         unsignedlong irqflags;

         intirq, error;

 

         //传入参数: 过期时间,回调函数,上下文

         //当计时器过期时,回调函数gpio_keys_timer将得到运行

         setup_timer(&bdata->timer,gpio_keys_timer, (unsigned long)bdata);//初始化计时器

         INIT_WORK(&bdata->work,gpio_keys_work_func);

 

         error= gpio_request(button->gpio, desc);//请求使用GPIO

         if(error < 0)

         {

                   dev_err(dev,"failed to request GPIO %d, error %d\n",button->gpio, error);

                   gotofail2;

         }

 

         error= gpio_direction_input(button->gpio);//设置指定的GPIO为输入模式

         if(error < 0)

         {

                   dev_err(dev,"failed to configure direction for GPIO %d, error %d\n",

                            button->gpio,error);

                   gotofail3;

         }

 

         irq =gpio_to_irq(button->gpio);//获得GPIO对应的中断号

         if (irq< 0)

         {

                   error= irq;

                   dev_err(dev,"Unable to get irq number for GPIO %d, error %d\n",button->gpio,error);

                   gotofail3;

         }

 

         irqflags= IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING;

         //irqflags= IRQF_TRIGGER_FALLING;//lqm changed.

         /*

          * If platform has specified that the buttoncan be disabled,

          * we don't want it to share the interruptline.

          */

         if(!button->can_disable)

                   irqflags|= IRQF_SHARED;

 

         error= request_irq(irq, gpio_keys_isr, irqflags, desc, bdata);//请求按键中断

     ……

}

该函数使用了带定时器延时的中断机制,用于按键去抖动。即中断的执行在定时器到来之后执行,这样能够有效的去除按键抖动。同时,中断使用工作队列的机制。

整个按键中断的工作调用有点复杂,下面逐步解析:

首先,上面函数中setup_timer函数初始化定时器,第二个实参为一个名为gpio_keys_timer的函数,一旦计时器过期,该函数将得到运行。setup_timer函数需和mod_timer函数配合使用,在按键中断的顶半部,即gpio_keys_isr函数,会判断debounce_interval是否为0,若为非0,则调用mod_timer函数延时debounce_interval ms,再触发定时器中断,即gpio_keys_timer函数得到执行。否则,直接调度工作队列,执行中断底半部。

回到gpio_keys_setup_key函数,在初始化完计时器后,再调用INIT_WORK函数初始化工作队列,初始化函数有一个实参函数gpio_keys_work_func,即一旦调度相应队列名,该函数将得到执行。

接下来是IO口的中断初始化,调用gpio_request函数申请相应的GPIO,调用gpio_direction_input函数将对应的GPIO设置为输入,gpio_to_irq函数通过指定GPIO口映射到指定的IRQ中断号,request_irq函数申请中断。

值得注意的是,三个按键通过request_irq申请中断时,共用了同一个中断函数gpio_keys_isr,那么程序是怎么判断具体是哪个按键触发的呢?

带着这个问题,我们将整个中断的执行过程疏理一遍:

第一步:硬件板按下按键,电平由高变低,中断被触发,中断函数gpio_keys_isr被调用。代码如下:

static irqreturn_t gpio_keys_isr(int irq, void*dev_id)

{

         structgpio_button_data *bdata = dev_id;

         structgpio_keys_button *button = bdata->button;

 

         BUG_ON(irq!= gpio_to_irq(button->gpio));

 

         if(button->debounce_interval)//产生按键中断后,用计时器延时button->debounce_interval ms之后,再执行按键处理

                  mod_timer(&bdata->timer,jiffies+ msecs_to_jiffies(button->debounce_interval));

         else

                   schedule_work(&bdata->work);

 

         returnIRQ_HANDLED;

}

中断函数会判断debounce_interval是否为0,debounce_interval代表计时器需要延时的时间,单位为毫秒。为了确定程序具体如何执行,我们首先需分析出它的值。以下是推理逻辑:

button->debounce_interval  à   找到button结构体来源

*bdata = dev_id&& *button = bdata->button  à  *button = dev_id->button

staticirqreturn_t gpio_keys_isr(int irq, void *dev_id);

error =request_irq(irq, gpio_keys_isr, irqflags, desc, bdata);

上面两个函数,gpio_keys_isr为request_irq的一个实参,根到request_irq代码中,最终会调用kernel\irq\manage.c中的request_threaded_irq函数,部分代码如下:

int request_threaded_irq(unsigned int irq,irq_handler_t handler,

                             irq_handler_t thread_fn, unsigned longirqflags,

                             const char *devname, void *dev_id)

{

     ……

         action= kzalloc(sizeof(struct irqaction), GFP_KERNEL);

         if(!action)

                   return-ENOMEM;

 

         action->handler= handler;

         action->thread_fn= thread_fn;

         action->flags= irqflags;

         action->name= devname;

         action->dev_id= dev_id;

 

         chip_bus_lock(irq,desc);

         retval= __setup_irq(irq, desc, action);

         chip_bus_sync_unlock(irq,desc);

 

         if(retval)

                   kfree(action);

 

#ifdef CONFIG_DEBUG_SHIRQ

         if(!retval && (irqflags & IRQF_SHARED)) {

                   /*

                    * It's a shared IRQ -- the driver ought to beprepared for it

                    * to happen immediately, so let's makesure....

                    * We disable the irq to make sure that a'real' IRQ doesn't

                    * run in parallel with our fake.

                    */

                   unsignedlong flags;

 

                   disable_irq(irq);

                   local_irq_save(flags);

 

                   handler(irq,dev_id);

 

                   local_irq_restore(flags);

                   enable_irq(irq);

         }

#endif

         returnretval;

}

继续根到__setup_irq函数,后面不继续分析了,最终会将request_irq中的两个实参irq和bdata赋给中断服务函数gpio_keys_isr中的两个实参,也就是说,*button =dev_id->button等价于*button=bdata->button。

在gpio_keys_probe函数中,bdata->button = button,*button = &pdata->buttons[i];可以推断出*button= pdata->buttons[i],而*pdata = pdev->dev.platform_data,那么*button= pdev->dev.platform_data-> buttons[i],也就是mx53_loco.c中的如下结构体数组中的一组:

static struct gpio_keys_button loco_buttons[] = {

         GPIO_BUTTON(MX53_nONKEY,KEY_POWER, 1, "power", 0),

         //GPIO_BUTTON(USER_UI1,KEY_BACK, 1, "back", 0),

         GPIO_BUTTON(USER_UI2,KEY_HOME, 1, "home", 0),

         GPIO_BUTTON(USER_UI1,KEY_RIGHT, 1, "right", 0),//test by lqm.

};

由此可以推断出,前面的button->debounce_interval即loco_buttons[i]-> debounce_interval。数组loco_buttons的结构体如下:

struct gpio_keys_button {

         /*Configuration parameters */

         intcode;               /* input event code(KEY_*, SW_*) */

         intgpio;

         intactive_low;

         char*desc;

         inttype;               /* input event type(EV_KEY, EV_SW) */

         intwakeup;                   /* configure thebutton as a wake-up source */

         intdebounce_interval;   /* debounce ticksinterval in msecs */

         boolcan_disable;

};

由于程序中并没有对debounce_interval赋值,因此默认debounce_interval为0。回到gpio_keys_isr函数,由于button->debounce_interval为0,那么计时器机制没有启动,直接执行调度函数schedule_work。

第二步:中断队列gpio_keys_work_func函数得到执行。它又会调用gpio_keys_report_event函数,代码如下:

static void gpio_keys_report_event(structgpio_button_data *bdata)

{

         structgpio_keys_button *button = bdata->button;

         structinput_dev *input = bdata->input;

         unsignedint type = button->type ?: EV_KEY;

         intstate = (gpio_get_value(button->gpio) ? 1 : 0) ^ button->active_low;//获得按键信息,同时与1异或?

 

         input_event(input,type, button->code, !!state);

         input_sync(input);//事件同步,它告知事件的接收者驱动已经发出了一个完整的报告

}

这里是中断底半部,变量state经gpio_get_value函数获得当前IO口的电平状态,再通过input_event函数将当前电平状态以及button->code上传给输入子系统。button->code即loco_buttons数组里面GPIO_BUTTON中的第二个参数,它定义了按键的作用。最后调用input_sync函数同事事件,结束一次按键的操作。

由于前面分析的debounce_interval值为0,因此执行流程比较简单,如果它不会0,将会启用计时器机制,流程会复杂一些。

默认三个按键的功能为开关机,主页和返回三个功能,如果我们需要修改对应按键的功能,是否修改上面的GPIO_BUTTON->ev_code就可以了呢?比如,我们需要将back键改为right键,做如下修改:

static struct gpio_keys_button loco_buttons[] = {

         GPIO_BUTTON(MX53_nONKEY,KEY_POWER, 1, "power", 0),

         //GPIO_BUTTON(USER_UI1,KEY_BACK, 1, "back", 0),

         GPIO_BUTTON(USER_UI2,KEY_HOME, 1, "home", 0),

         GPIO_BUTTON(USER_UI1,KEY_RIGHT, 1, "right", 0),//test by lqm.

};

编译内核,这样按键功能就改变了吗?答案是否定的。因为android并没有直接使用映射后的键值,而且对其再进行了一次映射,从内核标准键值到android所用键值的映射表定义在android文件系统的/system/usr/keylayout目录下。标准的映射文件为qwerty.kl,定义如下:

key 399  GRAVE

key 2     1

key 3     2

key 4     3

key 5     4

key 6     5

key 7     6

key 8     7

key 9     8

key 10    9

key 11    0

key 158  BACK              WAKE_DROPPED

key 230  SOFT_RIGHT        WAKE

key 60   SOFT_RIGHT        WAKE

key 107  ENDCALL           WAKE_DROPPED

key 62   ENDCALL           WAKE_DROPPED

key 229  MENU              WAKE_DROPPED

key 139  MENU              WAKE_DROPPED

key 59   MENU              WAKE_DROPPED

key 127  SEARCH            WAKE_DROPPED

key 217  SEARCH            WAKE_DROPPED

key 228  POUND

key 227  STAR

key 231  CALL              WAKE_DROPPED

key 61   CALL              WAKE_DROPPED

key 232  DPAD_CENTER       WAKE_DROPPED

key 108  DPAD_DOWN         WAKE_DROPPED

key 103  DPAD_UP           WAKE_DROPPED

key 102  HOME              WAKE

key 105  DPAD_LEFT         WAKE_DROPPED

key 106  DPAD_RIGHT        WAKE_DROPPED

key 115  VOLUME_UP

key 114  VOLUME_DOWN

key 116  POWER             WAKE

key 212  CAMERA

 

key 16    Q

key 17    W

key 18    E

key 19    R

key 20    T

key 21    Y

key 22    U

key 23    I

key 24    O

key 25    P

key 26   LEFT_BRACKET

key 27   RIGHT_BRACKET

key 43   BACKSLASH

 

key 30    A

key 31    S

key 32    D

key 33    F

key 34    G

key 35    H

key 36    J

key 37    K

key 38    L

key 39   SEMICOLON

key 40   APOSTROPHE

key 14   DEL

       

key 44    Z

key 45    X

key 46    C

key 47    V

key 48    B

key 49    N

key 50    M

key 51   COMMA

key 52   PERIOD

key 53   SLASH

key 28   ENTER

       

key 56   ALT_LEFT

key 100  ALT_RIGHT

key 42   SHIFT_LEFT

key 54   SHIFT_RIGHT

key 15   TAB

key 57   SPACE

key 150  EXPLORER

key 155  ENVELOPE       

 

key 12   MINUS

key 13   EQUALS

key 215   AT

android按键的处理是Window Manager负责,主要的映射转换实现在android源代码frameworks/base/libs/ui/EventHub.cpp此文件处理来自底层的所有输入事件,并根据来源对事件进行分类处理,对于按键事件,它首先记录驱动名称,再获取环境变量ANDROID_ROOT为系统路径,默认是/system,定义在android源代码/system/core/rootdir/init.rc文件中。然后查找路径为"系统路径/usr/keylayout/驱动名称.kl"的按键映射文件,如果不存在则默认用路径为"系统路径/usr/keylayout/qwerty.kl"。这个默认的按键映射文件,映射完成后再把经映射得到的android按键码值发给上层应用程序。所以我们可以在内核中定义多个按键设备,然后为每个设备设定不同的按键映射文件,不定义则会默认用qwerty.kl。

有了上面的分析,我们不难发现,上述更改是不可能有效果的,只能越改越不能用。相反,如果仅仅只需要更改某个按键的功能,根本就不用改内核,只需重新定义android系统的gpio-keys.kl即可。

进android系统后找到该文件,


可以发现,在在gpio-keys.kl和qwerty.kl两个文件,因此a ndroid系统会从gpio-keys.kl中查找键值进行映射。里面内容如下:

key 102  HOME              WAKE

key 158   BACK              WAKE

这不正是开发板上android的两个功能键吗?对于第三个POWER键,是用于开关机的,不属android功能键范畴,因此无须映射。

修改按键功能有两种方法:

方法一:直接修改gpio-keys.kl文件的内容,比如我们想将BACK键改成右键,我们只需做如下修改:

key 102  HOME              WAKE

key 106  DPAD_RIGHT       WAKE

注意上面的106以及按键名称,都是从qwerty.kl里面找的,千万不要在linux内核的input.h中查找。DPAD_RIGHT不能更改为RIGHT,否则android无法识别。

方法二:修改内核中mx53_loco.c中的数组如下:

static struct gpio_keys_button loco_buttons[] = {

         GPIO_BUTTON(MX53_nONKEY,KEY_POWER, 1, "power", 0),

         //GPIO_BUTTON(USER_UI1,KEY_BACK, 1, "back", 0),

         GPIO_BUTTON(USER_UI2,KEY_HOME, 1, "home", 0),

         GPIO_BUTTON(USER_UI1,KEY_RIGHT, 1, "right", 0),//test by lqm.

};

再修改上面的gpio-keys.kl文件,内容如下:

key 102  HOME              WAKE

key 106  DPAD_RIGHT       WAKE

方法二相比方法一,多了一个步骤,但是推荐采用方法二,这样更容易理解,也不易混淆。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
移动是IT发展未来 !嵌入式底层就是移动的发展未来 !如何在这个风云际会的时代,让自己积极的参与其中,作为程序员 ,嵌入式是无可避免的选择 !嵌入式底层驱动开发技术含量较高,掌握Android从应用开发,到系统移植,再到设备驱动开发的全套技术,无疑会极大的提升自己的职业竞争力和薪酬谢水平 ,本课程深入浅出,手敲全部实战项目代码,经历软硬件结合的嵌入式项目开发全部过程,而且课程中不仅仅讲解技术,更关注大家的职业生涯和发展,关注企业规模化工作中的模式。 1.课程研发环境 本课程包括JAVA应用、C语言驱动、NDK(应用调用驱动)等方面内容,课程涉及主要工具如下: 开发工具:Eclipse、Source Insight 交叉编译工具:arm-linux-gcc 4.5.1 其他工具:SecureCRT、Minitools、VMware等都会提供与项目匹配的安装程序,并且是破解版 2.内容简介 本教程共分五大部分内容,1 Android应用开发 2 Android系统移植 3 Cortex a8裸机接口开发 4 Android设备驱动开发 5 综合项目实战。第一部分课程从最基础的Android应用开发环境搭建开始,简单讲解了Android界面及事件处理之后,深入剖析Android Handler多线程机制,重点讲解Android NDK应用层与驱动的通信; 第二部分内容,先简单讲解Android系统移植相关原理,然后一步步手把手教大家如何进行Linux内核移植Android源码编译、以及Android到Cortex A8开发板的移植;第三部分内容,先教大家如何搭建裸机开发环境,然后带领大家一起阅读三星的芯片手册,并编写了LED、蜂鸣器、按键、中断、串口UART、实时时钟RTC、定时器PWM、模数转换ADC等裸机驱动;第四部分,讲解了Linux设备驱动开发环境搭建、内核开发相关理论,然后将裸机下的接口驱动移植Android环境下,并通过NDK和JAVA界面测试通过;第五部分,通过一个实战项目,综合应用各个模块的知识,为毕业设计 、项目研发和高新就业提供了很好的保障。 一、Android应用开发: 第1节:基于ARM Cortex-A8和Android 4.x的联动报警系统课程概述.zip 第2节:Android 4.x应用开发环境搭建.zip 第3节:Android程序结构.zip 第4节:Android界面布局.zip 第5节:Activity.zip 第6节:Android事件处理.zip 第7节:Android多线程.zip 第8节:Handler消息传递机制.zip 第9节:Android定时器.zip 第10节:Android NDK入门.zip 第11节:Android NDK深入理论讲解.zip 第12节:Android NDK深入实例演示.zip 第13节:Android NDK深入实例演示2.zip 第14节:Android NDK深入实例演示3.zip 二、Android系统移植: 第15节:Android移植之VMWare安装.zip 第16节:Android移植之Fedora安装.zip 第17节:Android移植之Fedora配置.zip 第18节:Android移植之Linux内核编译.zip 第19节:Android移植之Linux内核编译2.zip 第20节:Android移植Android文件系统编译.zip 第21节:linux补充之vi使用.zip 第22节:linux补充之shell命令.zip 第23节:Android移植之开发板真机测试.zip 三、Cortex-A8裸机开发: 第24节:Cortex-A8裸机开发环境搭建.zip 第25节:运行裸机程序的另外两种方式.zip 第26节:汇编点亮LED及代码分析.zip 第27节:关闭看门狗和调用C函数.zip 第28节:设置栈和C语言点亮LED.zip 第29节:控制icache.zip 第30节:控制蜂鸣器.zip 第31节:查询方式检测按键.zip 第32节:初始化系统时钟.zip 第33节:安装USB转串口驱动及串口工具.zip 第34节:Cortex-A8串口通信原理.zip 第35节:Cortex-A8串口通信实现.zip 第36节:Cortex-A8中断原理.zip 第37节:Cortex-A8中断实现.zip 第38节:Cortex-A8 PWM定时器原理.zip 第39节:Cortex-A8 PWM定时器实现.zip 第40节:Cortex-A8 RTC原理.zip 第41节:Cortex-A8 RTC实现.zip 第42节:Cortex-A8 ADC原理.zip 第43节:Cortex-A8 ADC实现.zip 四 Android 4.x设备驱动开发 第44节:Android 4.x设备驱动开发环境搭建.zip 第45节:Android 4.x设备驱动开发概述.zip 第46节:Android 4.x设备驱动开发HelloWorld演示.zip 第47节:Android 4.x字符设备驱动程序.zip 第48节:Android 4.x重要内核数据结构.zip 第49节:Android 4.x字符设备驱动程序示例.zip 第50节:另一种简单的字符设备驱动框架.zip 第51节:用Android NDK测试LED驱动.zip 第52节:Android的蜂鸣器驱动.zip 第53节:Android下查询方式的按键驱动.zip 第54节:Android下ADC驱动.zip 第55节:Android下RTC驱动.zip 第56节:Linux内核中断原理.zip 第57节:Android下PWM驱动.zip 五 、综合项目实战 第58节:项目实战之分析设计.zip 第59节:项目实战之音频报警.zip 第60节:项目实战之LED闪烁报警.zip 第61节:项目实战之蜂鸣器报警.zip 第62节:项目实战之ADC设置.zip 第63节:项目实战之ADC超标触发报警.zip 第64节:项目实战之ADC超标触发报警2.zip 第65节:项目实战之主界面功能.zip 第66节:项目实战之主界面功能2.zip 第67节:项目实战之RTC设置.zip 第68节:项目实战之RTC超时触发报警.zip 第69节:项目实战之按键触发报警.zip 第70节:项目实战之系统设置.zip 链接:http://pan.baidu.com/s/1jG1QpW6 密码:fnf3

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值