Android字符设备驱动及应用层从jni控制GPIO实战

本文主要讲述从实际项目中一个GPIO口控制一个加密芯片上下电的功能,提供动态库给客户,并有Android应用层apk调用.so库文件的例子,希望能为大家字符设备驱动以及jni开发入门带来帮助!

以下描述参考摘录了别人的话:http://koliy.iteye.com/blog/1424304

android应用层要访问驱动,一般有三种方法。 
1.应用层 ---> framwork层JNI ---> 驱动c 
2.应用层 ---> framwork层JNI ---> 硬件抽象层HAL ----> 驱动c 

3.应用层-->驱动c(读写字符设备)


三种方法,各有各的好处,第1种,方便与驱动调试实验,只要编译好ko文件和libxxx.so文件,放入系统中编译好就可以立即调试了。 

第2种JNI方法有些改变和增加了HAl,对驱动有了封装,简单来讲,硬件驱动程序一方面分布在Linux内核中,另一方面分布在用户空间的硬件抽象层中。不要误会android把驱动分成两半了,其实android只是在驱动的调用接口上在增加一层对应接口功能来让framwork层JNI来调用。比如,驱动有read接口,HAl层就是封装read接口,做一个 hal_read接口,里面调用read接口,然后把hal_read接口给JNI调用。 

明白了吗?这方面有人讲得非常好,有兴趣的可以点击: 
http://blog.csdn.NET/luoshengyang/article/details/6575988 
好处就是对对厂家来说能把商业秘密隐藏起来,我们做驱动实验的话,操作会极其复杂,不过对理解android整个系统都是极其用用的,因为它从下到上涉及到了android系统的硬件驱动层,硬件抽象层,运行时库和应用程序框架层等等。 

第3种涉及到Android上层读写设备节点,由于我们现在要讲的GPIO口是一个字符设备驱动,也会创建节点,所以可以通过此方法配置

这里由于客户需要我们的*.so库目前只讲第1种方法的实现:在此之前,请大家了解下JNI的编程方法,JNI是一种为Java和C、C++之间能互相访问所提供的编程接口(自行百度了解)

下面详细讲解开发过程(android系统大同小异),主要分为三部分:1.GPIO口字符设备驱动的实现,2.jni环境搭建及代码编写,3.应用层调用jni代码的实现

GPIO口字符设备驱动的实现
关于GPIO口字符设备驱动我们要做如下步骤:
1  找到原理图对应的GPIO口并配置它为输出管脚
gpio口配置(不同平台配置不一样)但目的是一样的 设置GPIO口输出,并默认低电平,我们接的是57管脚
<&range 56 1 0x1500>
<&range 57 1 0x1500>   代表57管脚做为gpio口使用,并默认低电平    
这个io口寄存器地址:0xe46002e4


但是调试过程中发现
#define gpio_lp 57 
gpio_request(gpio_lp,"pos_pwr");
gpio_set_value(gpio_lp,1);

GPIO调用request报错,导致GPIO不能用但是换个GPIO口后换个gpio口就不报错了,

这种原因是由于intel的特殊性:gpio在vmm的地方被调用,在kernel下就不能操作它(一般平台这样操作没问题的)

后续,想到了另外一种方法

void __iomem *ldo_mmio_base = ioremap(0xe46002e4, 4);

iowrite32(0x1700, ldo_mmio_base);   //1700代表设置寄存器(0xe46002e4)为GPIO口,输出  为高

iowrite32(0x1500, ldo_mmio_base);//1500代表设置寄存器(0xe46002e4)为GPIO口,输出 为底  

实现了IO口的控制

1.2  源代码我们放到linux-3.10/drivers/char下面让系统生成设备节点:/dev/mypos
   
 linux-3.10依据自己的kernel名字不同而不同

linux-3.10/drivers/char/lp6252_switch.c    

[objc]   view plain  copy
  1. #include <linux/module.h>               /* For module specific items */    
  2. #include <linux/moduleparam.h>          /* For new moduleparam's */    
  3. #include <linux/types.h>                /* For standard types (like size_t) */    
  4. #include <linux/errno.h>                /* For the -ENODEV/... values */    
  5. #include <linux/kernel.h>               /* For printk/panic/... */    
  6. #include <linux/fs.h>                   /* For file operations */^M    
  7. #include <linux/ioport.h>               /* For io-port access */    
  8. #include <linux/platform_device.h>      /* For platform_driver framework */    
  9. #include <linux/init.h>                 /* For __init/__exit/... */    
  10. #include <linux/uaccess.h>              /* For copy_to_user/put_user/... */    
  11. #include <linux/io.h>                   /* For inb/outb/... */    
  12. #include <linux/gpio.h>    
  13. #include <linux/device.h>    
  14. #include <linux/cdev.h>    
  15. #include <linux/slab.h>               /*kamlloc */    
  16. //#include <asm-generic/ioctl.h>    
  17.      
  18.  //ioctl   
  19. #define CMD_FLAG  'i'    
  20. #define POS_PWR_ON      _IOR(CMD_FLAG,0x00000001,__u32)      
  21. #define POS_PWR_OFF     _IOR(CMD_FLAG,0x00000000,__u32)   
  22. #define gpio_lp         57   
  23.     
  24. static int  major =0;    
  25. static struct classclass *pos_class;    
  26. struct cdev_pos {    
  27.     struct cdev cdev;    
  28. };     
  29. struct cdev_pos *pos_dev;    
  30.     
  31. static int pos_ioctl(struct file* filp,unsigned int cmd,unsigned long argv)    
  32. {    
  33.     printk(KERN_INFO "entry kernel.... \n");    
  34.     printk(KERN_INFO "%d\n", POS_PWR_ON);  
  35.     <span style="color:#ff0000;">void __iomem *ldo_mmio_base = ioremap(0xe46002e44);</span>  
  36.     
  37.     switch(cmd)    
  38.     {    
  39.         case POS_PWR_ON:    
  40.         {    
  41. #if 0  
  42.             gpio_set_value(gpio_lp,1);  //   
  43.             printk(KERN_INFO "POS on\n");   
  44. #endif  
  45.             iowrite32(0x1700, ldo_mmio_base)  
  46.             break;    
  47.         }    
  48.         case POS_PWR_OFF:    
  49.         {    
  50. #if 0  
  51.             gpio_set_value(gpio_lp,0);  
  52.             printk(KERN_INFO "POS off \n");  
  53. #endif</span>  
  54.             iowrite32(0x1500, ldo_mmio_base);  
  55.             break;    
  56.         }    
  57.         default:    
  58.             return -EINVAL;    
  59.     }    
  60.     return 0;    
  61. }    
  62.     
  63.     
  64. //open    
  65. static int pos_open(struct inode* i_node,struct file* filp)    
  66. {    
  67.     printk(KERN_INFO "taosong open init.... \n");    
  68.     int err;   
  69. #if 0     
  70.     err = gpio_request(gpio_lp,"pos_pwr");  
  71.     if(err<0)    
  72.     {    
  73.         printk(KERN_INFO "gpio request faile \n");    
  74.         return err;    
  75.     }    
  76.     gpio_direction_output(gpio_lp,1);  
  77.  #endif  
  78.     return 0;    
  79. }    
  80.     
  81. //close    
  82. static void pos_close(struct inode* i_node,struct file* filp)    
  83. {    
  84. printk(KERN_INFO "taosong close init \n");  
  85. #if 0    
  86.     gpio_free(gpio_lp);   
  87. #endif  
  88.     return ;    
  89. }    
  90.     
  91. /* file operations */    
  92. struct file_operations fops={    
  93.     .owner  = THIS_MODULE,    
  94.     .open   = pos_open,    
  95.     .unlocked_ioctl = pos_ioctl,   
  96.     .release= pos_close,    
  97. };    
  98.     
  99. static int __init pos_init(void)    
  100. {    
  101. printk(KERN_INFO "init .... \n");    
  102.     dev_t dev_no;    
  103.     int result,err;    
  104.     err = alloc_chrdev_region(&dev_no,0,1,"my_pos"); //dynamic request device number    
  105.     if(err<0)    
  106.     {    
  107.         printk(KERN_INFO "ERROR\n");    
  108.         return err;    
  109.     }    
  110.     major = MAJOR(dev_no);    
  111.     pos_dev = kmalloc(sizeof(struct cdev_pos),GFP_KERNEL);    
  112.     if(!pos_dev)    
  113.     {    
  114.         result = -ENOMEM;    
  115.         goto fail_malloc;    
  116.     }    
  117.     memset(pos_dev,0,sizeof(pos_dev));    
  118.         
  119.     cdev_init(&pos_dev->cdev,&fops);     
  120.     pos_dev->cdev.owner = THIS_MODULE;    
  121.     result = cdev_add(&pos_dev->cdev,dev_no,1);     
  122.     if(result <0)    
  123.     {   printk(KERN_INFO "error\n");    
  124.         goto fail_add;    
  125.     }    
  126.     pos_class = class_create(THIS_MODULE,"mypos");  //in sys/class create sysfs file    
  127.     device_create(pos_class,NULL,MKDEV(major,0),NULL,"mypos"); //dynamic create device file  /dev/mypos    
  128.     return 0;    
  129. fail_add:    
  130.     kfree(pos_dev);    
  131. fail_malloc:    
  132.     unregister_chrdev_region(dev_no,1);    
  133.     return result;    
  134.     
  135. }    
  136.     
  137. static void __exit pos_exit(void)    
  138. {    
  139.     dev_t dev_no=MKDEV(major,0);    
  140.     
  141.     unregister_chrdev_region(dev_no,1);    
  142.     cdev_del(&pos_dev->cdev);    
  143.     kfree(pos_dev);    
  144.     device_destroy(pos_class,dev_no);    
  145.     class_destroy(pos_class);    
  146.       printk(KERN_INFO "exit........ \n");    
  147. }    
  148. module_init(pos_init);    
  149. module_exit(pos_exit);    
  150. MODULE_AUTHOR("*@*.com");    
  151. MODULE_DESCRIPTION("control_pos_power");    
  152. MODULE_LICENSE("GPL");   


要让此代码生效编译进去:

在相应的makefile改过来

diff --Git a/drivers/char/Makefile b/drivers/char/Makefile
index e562ed5..98e871f 100644
--- a/drivers/char/Makefile
+++ b/drivers/char/Makefile
@@ -2,6 +2,7 @@
 # Makefile for the kernel character device drivers.
 #
 
+obj-y                          += lp6252_switch.o
 obj-y                          += mem.o random.o
 obj-$(CONFIG_TTY_PRINTK)       += ttyprintk.o
 obj-y                          += misc.o


这样加入lp6252_switch.c并修改Makefile后固件生成就会在 /dev/ 下生成节点   /dev/mypos

要让这个节点让别人可读写,还必须修改节点的系统所有者以及他的权限,这个步骤我们一版在 init.rc中进行

我的代码是system/core/rootdir/init.rc

diff --git a/core/rootdir/init.rc b/core/rootdir/init.rc
index fc5d73f..b3095c0 100644
--- a/core/rootdir/init.rc
+++ b/core/rootdir/init.rc
@@ -308,6 +308,8 @@ on boot
 
     chmod 0777 /dev/ttyS1
     chmod 0777 /sys/devices/l68ie_switch/chip_switch
+    chown system system /dev/mypos
+    chmod 0766 /dev/mypos
 
     chown system system /sys/devices/system/cpu/cpufreq/interactive/timer_rate
     chmod 0660 /sys/devices/system/cpu/cpufreq/interactive/timer_rate


至此一个可用的字符设备节点 /dev/mypos 已经生成并能供给上层可读写的权限



二.jni环境搭建及代码编写

2.1 NDK本地开发环境搭建

这里主要介绍在eclipse上搭建NDK开发环境。

以前做Android的项目要用到NDK就必须要下载NDK,下载安装Cygwin(模拟Linux环境用的),下载CDT(Eclipse C/C++开发插件),还要配置编译器,环境变量...

麻烦到不想说了,本人在网上查了一下资料,发现了一个超级快配置NDK的办法。

Step1:到Android官网下载Android的开发工具ADT(Android Development Tool的缩写),该工具集成了最新的ADT和NDK插件以及Eclipse,还有一个最新版本SDK。解压之后就可以用了,非常爽!

ADT插件:管理Android SDK和相关的开发工具的    

NDK插件:用于开发Android NDK的插件,ADT版本在20以上,就能安装NDK插件,另外NDK集成了CDT插件

也可以在线更新ADT、NDK插件,不过速度超级慢...所以果断在网上下载集成开发工具ADT,下载链接见:http://developer.android.com/sdk/index.html   本地百度云地址链接:http://pan.baidu.com/s/1slcVhxF 密码:ucf9

Step2:到Android官网下载最新的NDK,注:NDK版本在r7(本文使用android-ndk-r9b)以上之后就集成了Cygwin,而且还是十分精简版。比起下载Cygwin要方便多啦!

下载链接见:http://developer.android.com/tools/sdk/ndk/index.html  

android-ndk-r9b 百度云地链接:http://pan.baidu.com/s/1hrPGtKC 密码:od7o

Step3打开Eclipse,点Window->Preferences->Android->NDK,设置NDK路径,例如我的是E:\qf项目20160603\s600\android-ndk-r9b-windows-x86\android-ndk-r9b



Step4:新建一个Android工程,在工程上右键点击Android Tools->Add Native Support...,然后给我们的.so文件取个名字,例如:poscontrol 

     

这时候工程就会多一个jni的文件夹,jni下有Android.mk和poscontrol.cpp(我这里改成.c文件了)文件。Android.mk是NDK工程的Makefile,poscontrol.cpp就是NDK的源文件。


Step5:右键项目工程点击Run as  就会生成libposcontrol.so


Step6:完成了,然后运行。运行之前先编译NDK,然后在编译JAVA代码。编译也许会遇到Unable to launch cygpath. Is Cygwin on the path?错误,解决办法如下:

1.工程右键,点Properties->C/C++ Build的Building Settings中去掉Use default build command,然后输入${NDKROOT}/ndk-build.cmd


2.在C/C++ Build中点击Environment,点Add...添加环境变量NDKROOT,值为NDK的根目录


3.再编译,问题就解决啦!


2.2 jni代码编写
poscontrol.c
[objc]   view plain  copy
  1. #include<stdio.h>  
  2. #include<stdlib.h>  
  3. #include<fcntl.h>  
  4. #include<errno.h>  
  5. #include<unistd.h>  
  6. #include<sys/ioctl.h>  
  7. #include<jni.h>  // 一定要包含此文件  
  8. #include<string.h>  
  9. #include<sys/types.h>  
  10. #include<sys/stat.h>  
  11. #include <android/log.h>  
  12. //驱动里的命令码.  
  13. #define CMD_FLAG 'i'  
  14. #define LED_ON      _IOR(CMD_FLAG,0x00000001,__u32)  
  15. #define LED_OFF     _IOR(CMD_FLAG,0x00000000,__u32)  
  16.   
  17. #define DEVICE_NAME "/dev/mypos"  
  18. int fd;  
  19.   
  20.   
  21. static const charchar *TAG="012";  
  22. #define LOGI(fmt, args...) __android_log_print(ANDROID_LOG_INFO,  TAG, fmt, ##args)  
  23. #define LOGD(fmt, args...) __android_log_print(ANDROID_LOG_DEBUG, TAG, fmt, ##args)  
  24. #define LOGE(fmt, args...) __android_log_print(ANDROID_LOG_ERROR, TAG, fmt, ##args)  
  25.   
  26. /* * Class:     Linuxc 
  27. * Method:    openled 
  28. * Signature: ()I 
  29. */  
  30.   
  31.  JNIEXPORT void JNICALL Java_com_idean_s600_pay_manage_IntelPosPowerManager_posPowerOn(JNIEnv* env, jclass mc)  
  32. {  
  33.   LOGI("POWER ON BY QINFENG");  
  34.   LOGI("LED_ON:%d   LED_OFF:%d",LED_ON,LED_OFF);  
  35.   fd=open(DEVICE_NAME,O_RDWR);  
  36.   if(fd<0)  
  37.       {  
  38.             LOGI("don't open dev");  
  39.         }  
  40.         else  
  41.             {  
  42.             ioctl(fd,LED_ON,NULL) ;  
  43.             LOGI("open success");  
  44.             }  
  45.   
  46.   
  47. }  
  48.   
  49. /* * Class:     Linuxc 
  50. * Method:    clsoeled 
  51. * Signature: ()V 
  52. */  
  53. JNIEXPORT void JNICALL Java_com_idean_s600_pay_manage_IntelPosPowerManager_posPowerOff(JNIEnv* env, jclass mc)  
  54. {  
  55.     LOGI("POWER Off BY QINFENG");  
  56.     ioctl(fd,LED_OFF,NULL) ;  
  57.     close(fd);  
  58.   
  59. }  
关于驱动接口的说明:
[objc]   view plain  copy
  1. #define DEVICE_NAME "/dev/mypos"  
[objc]   view plain  copy
  1. 这个使我们驱动生成的节点,我们后面通过  
[objc]   view plain  copy
  1. fd=open(DEVICE_NAME,O_RDWR);  
[objc]   view plain  copy
  1. ioctl(fd,LED_ON,NULL) ;  
调用节点并通过底层相对应的CMD ( LED_ON OR LED_OFF )控制节点的高低电平(加密芯片的上电和下电)

主要提供两个接口给上层调用:
[objc]   view plain  copy
  1. JNICALL Java_com_idean_s600_pay_manage_IntelPosPowerManager_posPowerOn  
[objc]   view plain  copy
  1. Java_com_idean_s600_pay_manage_IntelPosPowerManager_posPowerOff  
根据JNI的规则:
[objc]   view plain  copy
  1. com_idean_s600_pay_manage_IntelPosPowerManager  
     包名:package com.idean.s600.pay.manage;
类名: IntelPosPowerManager
而posPowerOn  posPowerOff  为上层要调用的本地接口

在这里特别说明下,JNI的格式规范要注意的地方: 
1.函数声明的格式: 
  因JNI会把 '_' 转换成' . ' 所以在类名和函数接口中不要出现' _ ',以免应用层调用不到JNI接口,这方面对初学者来说极其重要,所以用eclipse生成的android类文件,最好改下类名。不了解对实验的热情打击比较重。 
2.JNI函数分本地方法和静态方法。 
  本地方法: 
        public native int jni();  // 不带static 声明. 
  对应的 JNI 函数中参数的定义有改动: 
        Java_xx_xx_LedControl_jni(JNIEnv*env, jobject obj) 
  静态方法: 
         public static native int jni();  // 带static 声明. 
  对应的 JNI 函数中参数的定义有改动: 
        Java_xx_xx_LedControl_jni(JNIEnv*env, jclass cls)
 
注意 jobject 和jclass的变动。 

Android.mk代码如下:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_LDLIBS    := -lm -llog 
LOCAL_MODULE    := poscontrol
LOCAL_SRC_FILES := poscontrol.c
include $(BUILD_SHARED_LIBRARY)

注意 LOCAL_MODULE    :poscontrl 代表生成的是libposcontrol.so   apk上层调用的库文件必须一致

回到 (ndk目录路径)/JNI/(应用工程文件)/  路径下   (个人理解可以本地cmd输入命令编译)
输入命令 : ../../ndk-build 

会生成 libs 和obj 2个文件。 libposcontrol.so文件放在 libs /armeabi/ 下 

至此,JNI编译成功,连接上层关于GPIO的字符驱动节点和上层可调用的SO库文件


三.应用层调用jni代码的实现(apk编写)

关于这节主要贴一下源代码:

IntelPosPowerManager.java

[java]   view plain  copy
  1. package com.idean.s600.pay.manage;  
  2.   
  3.   
  4. import android.media.AudioManager;  
  5. import android.os.Bundle;  
  6. import android.app.Activity;  
  7. import android.content.Intent;  
  8. import android.util.Log;  
  9. import android.view.Menu;  
  10. import android.view.View;  
  11. import android.view.View.OnClickListener;  
  12. import android.widget.Button;  
  13.   
  14. public class IntelPosPowerManager extends Activity {  
  15.   
  16.       
  17.     private Button power_on;    
  18.     private Button power_off;  
  19.     private OnClickListener mylistener;    
  20.       
  21.     @Override  
  22.     protected void onCreate(Bundle savedInstanceState) {  
  23.         super.onCreate(savedInstanceState);  
  24.         setContentView(R.layout.activity_intel_pos_power_manager);  
  25.         power_on=  (Button)findViewById(R.id.power_on);  
  26.         power_off= (Button)findViewById(R.id.power_off);  
  27.   
  28.          //成功点击事件  
  29.         power_on.setOnClickListener(new View.OnClickListener() {  
  30.                 @Override  
  31.                 public void onClick(View v) {  
  32.                     // TODO Auto-generated method stub  
  33.                      Log.d("012""power_on by android\n");    
  34.                         posPowerOn();    
  35.   
  36.                         }  
  37.             });  
  38.         power_off.setOnClickListener(new View.OnClickListener() {  
  39.             @Override  
  40.             public void onClick(View v) {  
  41.                 // TODO Auto-generated method stub  
  42.                  Log.d("012""power_off by android\n");    
  43.                     posPowerOff();    
  44.   
  45.                     }  
  46.         });  
  47.          
  48.           
  49.       
  50.     }  
  51.    
  52.       
  53.     protected void onDestroy(){    
  54.         super.onDestroy();    
  55.     }    
  56.   
  57.   
  58.           
  59.   
  60.     @Override  
  61.     public boolean onCreateOptionsMenu(Menu menu) {  
  62.         // Inflate the menu; this adds items to the action bar if it is present.  
  63.         getMenuInflater()  
  64.                 .inflate(R.menu.activity_intel_pos_power_manager, menu);  
  65.         return true;  
  66.     }  
  67.        
  68.        static {    
  69.             try{    
  70.                 Log.i("012","try to load poscontrol.so");    
  71.                 System.loadLibrary("poscontrol");      
  72.         //加载本地库,也就是JNI生成的libxxx.so文件,下面再说。    
  73.             }catch (UnsatisfiedLinkError ule){    
  74.                 Log.e("012","WARNING: Could not load poscontrol.so");    
  75.             }    
  76.         }    
  77.         /** 
  78.          * 控制金融芯片上电 
  79.          */  
  80.         public native static void posPowerOn();  
  81.   
  82.         /** 
  83.          * 控制金融芯片下电 
  84.          */  
  85.         public native static void posPowerOff();  
  86.   
  87. }  
注意的点:
[java]   view plain  copy
  1. System.loadLibrary("poscontrol");  poscontrol名字要和.so库对应起来  
[java]   view plain  copy
  1. 本地调用方法:        
[java]   view plain  copy
  1. 控制金融芯片上电  
  2.         public native static void posPowerOn();  
  3. 控制金融芯片下电  
  4.         public native static void posPowerOff();  

附录xml文件布局:

activity_intel_pos_power_manager.xml

[html]   view plain  copy
  1. <?xml version="1.0" encoding="utf-8"?>    
  2. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    
  3.     android:orientation="vertical"    
  4.     android:layout_width="fill_parent"    
  5.     android:layout_height="fill_parent"    
  6.     >    
  7. <TextView      
  8.     android:id="@+id/position"    
  9.     android:layout_centerInParent="true"    
  10.     android:layout_width="wrap_content"     
  11.     android:layout_height="wrap_content"     
  12.     android:textSize="25sp"    
  13.     android:textColor="#ff0000"  
  14.     android:text=" power control "    
  15.     />    
  16. <Button     
  17.     android:id="@+id/power_on"    
  18.     android:layout_width="wrap_content"    
  19.     android:layout_height="wrap_content"    
  20.     android:textSize="18sp"    
  21.     android:text="power_on"    
  22.     android:layout_toLeftOf="@+id/position"    
  23.     android:layout_centerHorizontal="true"    
  24.     android:layout_alignTop="@+id/position"    
  25.     />    
  26. <Button     
  27.     android:id="@+id/power_off"    
  28.     android:layout_width="wrap_content"    
  29.     android:layout_height="wrap_content"    
  30.     android:textSize="18sp"    
  31.     android:text="power_off"    
  32.     android:layout_toRightOf="@+id/position"    
  33.     android:layout_alignTop="@+id/position"    
  34.     />    
  35. </RelativeLayout>    


至此,从底层GPIO字符设备驱动 到 JNI 库文件实现,到上层apk调用JNI本地接口功能全部实现


apk下载地址:http://download.csdn.net/detail/qf0727/9665118


通过按钮 power_on 和 power_off 就能够控制GPIO口的高低电平了

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值