自己动手写最简单的Android驱动---LED驱动的编写

安卓驱动开发学习--举例--调试 android 驱动开发 调试 adb 阅读详情

转载注明出处,作者:K_Linux_Man, 薛凯 山东中医药大学,给文章内容引入个人毕业设计。

 开发平台:farsight s5pc100-a

内核:linux2.6.29

环境搭配:有博文介绍

开发环境:Ubuntu 、Eclipse

首先强调一下要点:

1.编写android驱动时,首先先要完成linux驱动,因为android驱动其实是在linux驱动基础之上完成了HAL层(硬件抽象层),如果想要测试的话,自己也要编写java程序来测试你的驱动。

2.android的根文件系统是eclair_2.1版本。我会上传做好的根文件系统提供大家。这里要说的是,android底层内核还是linux的内核,只是进行了一些裁剪。做好的linux内核镜像,这个我也会上传给大家。android自己做了一套根文件系统,这才是android自己做的东西。android事实上只是做了一套根文件系统罢了。

 

假设linux驱动大家都已经做好了。我板子上有四个灯,通过ioctl控制四个灯,给定不同的参数,点亮不同的灯。

linux驱动代码因平台不同而有所不同,这就不黏代码了。

这是我测试linux驱动编写的驱动,代码如下:

 

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#define LED_ON _IO ('k',1)
#define LED_OFF _IO ('k',2)
int main()
{
	int i = 0;
	int dev_fd;
	dev_fd = open("/dev/led",O_RDWR);
	if ( dev_fd == -1 ) {
		printf("Cann't open file /dev/led\n");
		exit(1);
	}
	while(1)
	{
	 ioctl(dev_fd,LED_ON,1);
	 sleep(1);
	 ioctl(dev_fd,LED_OFF,1);
	 sleep(1);
	 ioctl(dev_fd,LED_ON,2);
	 sleep(1);
	 ioctl(dev_fd,LED_OFF,2);
	 sleep(1);
	 ioctl(dev_fd,LED_ON,3);
	 sleep(1);
	 ioctl(dev_fd,LED_OFF,3);
	 sleep(1);
	 ioctl(dev_fd,LED_ON,4);
	 sleep(1);
	 ioctl(dev_fd,LED_OFF,4);
	 sleep(1);

	}
	return 0;
}

 

下面开始把linux驱动封装成android驱动。

首先介绍一下android驱动用到的三个重要的结构体,

struct hw_module_t;

struct hw_device_t;

struct hw_module_methods_t;

android源码里面结构体的声明

typedef struct hw_module_t {

uint 32_t   tag;

uint16_t    version_major;

uint16_t    version_minor;

const char *id;

const char *name;

const char *author;

const hw_module_methods_t  *methods;

void* dso;

uint32_t reserved[32-7];

} hw_module_t;


 

typedef struct hw_device_t {

uint32_t tag;

uint32_t version;

struct hw_module_t* module;

uint32_t reserved[12];

int (*close) (struct hw_device_t  *device);

}hw_device_t;


 

typedef struct hw_module_methods_t {

 int (*open) (const struct hw_module_t *module, const char *id,

                      struct hw_device_t **device);

} hw_module_methods_t;


我们经常会用到这三个结构体。

 

android驱动目录结构:

led

   |--- hal

   |       |----jni

   |               |----- Android.mk

   |               |----com_farsgiht_server_ledServer.cpp

   |       |----stub

   |                 |---- include

   |                 |             |-----led.h

   |                 |-----module

   |                               |-----Android.mk

   |                               |-----led.c

   |--- linux_drv

首先我们要编写一个stub(代理),代理的意思是,针对你所特有的设备,你找一个代理人就可以帮你完成,它是操作linux驱动的第一层。

编写头文件,名字led.h

代码如下;

#include <hardware/hardware.h>
#include <fcntl.h>
#include <errno.h>
#include <cutils/log.h>
#include <cutils/atomic.h>


#define LED_HARDWARE_MODULE_ID "led"


struct led_module_t {
	struct hw_module_t common;
};

struct led_control_device_t {
	struct hw_device_t common;

	int (*set_on) (struct led_control_device_t *dev, int arg);
	int (*set_off)(struct led_control_device_t *dev, int arg);
};


struct led_control_context_t {
	struct led_control_device_t device;
};


struct hw_module_t  sturct hw_device_t  这两个结构体不能直接使用,所以进行了一下封装(继承)。

led_module_t 继承 hw_module_t

led_control_device_t 继承 hw_device_t

led_control_context_t 继承 led_control_device_t

在led_control_device_t 结构体有函数指针的声明,因为后面代码中会给这些函数指针赋值

 

编写led.c

代码如下:

#define LOG_TAG "LedStub"
#include <hardware/hardware.h>
#include <fcntl.h>
#include <errno.h>
#include <cutils/log.h>
#include <cutils/atomic.h>
#include <sys/ioctl.h>
#include "../include/led.h"


#define LED_ON 	_IO ('k',1)
#define LED_OFF 	_IO ('k',2)

int fd;

static int led_set_on(struct led_control_device_t *dev, int arg)
{
	LOGI("led_set_on");
	ioctl(fd, LED_ON, arg);
	return 0;
}

static int led_set_off(struct led_control_device_t *dev, int arg)
{
	LOGI("led_set_off");
	ioctl(fd, LED_OFF, arg);
	return 0;
}

static int led_device_close(struct hw_device_t *device)
{
	struct led_control_context_t *context = (struct led_control_context_t *)device;
	if(context) free(context);
	close(fd);
	return 0;
}


static int led_device_open(const struct hw_module_t *module, const char *name, 
	struct hw_device_t **device)
{
	struct led_control_context_t *context;
	LOGD("led_device_open");
	context = (struct led_control_context_t *)malloc(sizeof(*context));
	memset(context, 0, sizeof(*context));

	context->device.common.tag = HARDWARE_DEVICE_TAG;
	context->device.common.version = 0;
	context->device.common.module= module;
	context->device.common.close = led_device_close;

	context->device.set_on = led_set_on;
	context->device.set_off = led_set_off;
	
	*device = (struct hw_device_t *)&(context->device);

	if((fd = open("/dev/led",O_RDWR)) == -1)
	{
		LOGI("ERROR: open");
	}else {
		LOGI("open led device ok\n");
	}

	return 0;
}

static struct hw_module_methods_t led_module_methods = {
open:led_device_open
};


const struct led_module_t HAL_MODULE_INFO_SYM = {
common:{
tag: HARDWARE_MODULE_TAG,
	  version_major:1,
	  version_minor:0,
	  id:LED_HARDWARE_MODULE_ID,
	  name:"led_stub",
	  author:"K_Linux_Man",
	  methods: &led_module_methods,
		 },
};


首先先看 struct led_module_t HAL_MODULE_INFO_SYM。这个结构体的名字必须是这个名字,否则系统无法找到led_module_t这个结构体。

然后对led_module_t 里的成员hw_module_t结构体赋值。最关键的为id和methods两个成员的赋值,id必须要赋值,因为后面有个函数要找到hw_module_t就是通过id号去找的。 methods被赋值之后,上层的jni才能去调用。

接着看methods 结构体里的成员就一个,open函数指针,对这个函数指针进行了赋值,赋了led_device_open函数,这个函数实现的主要就是分配led_control_context_t结构体空间,并对成员进行赋值。注意hw_device_t 里的成员module、close必须赋值。

函数指针赋值:

context->device.set_on = led_set_on;

context->device.set_off = led_set_off;

下面这句话的用意是,传进来的device指针赋予新的值,只要调用这个函数,传进来的二级指针所指向的一级指针就有值了(二级指针改变了一级指针的指向,你可以看我写的 int*p 和 int **p 博文)。

*device = (struct hw_device_t *)&(context->device);

接着就是打开设备文件,得到fd

led_set_on();里面调用ioctl;

led_set_off();里面调用ioctl;

 

接下来写jni了。。com_farsight_server_ledServer.cpp文件

文件代码:

#define LOG_TAG "ledService"

#include "utils/Log.h"
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>
#include <jni.h>
#include "../stub/include/led.h"


static led_control_device_t *sLedDevice = NULL;


static jint led_set_on(JNIEnv *env, jobject thiz, jint arg) 
{
	if(sLedDevice) {
		LOGI("led_set_on");
		sLedDevice->set_on(sLedDevice, (int)arg);
	}else {
		LOGI("sLedDevice is NULL");
	};
	return 0;
}

static jint led_set_off(JNIEnv *env, jobject thiz, jint arg)
{
	if(sLedDevice) {
		LOGI("led_set_off");
		sLedDevice->set_off(sLedDevice, (int)arg);
	}else {
		LOGI("sLedDevice is null");
	}
	return 0;
}



static inline int led_control_open(const struct hw_module_t *module,
	struct led_control_device_t **device)
{
	LOGI("led_control_open");
	return module->methods->open(module, LED_HARDWARE_MODULE_ID,
		(struct hw_device_t **)device);
}


static jint led_init(JNIEnv *env, jclass clazz)
{
	led_module_t const *module;
	LOGI("led_init");

	if(hw_get_module(LED_HARDWARE_MODULE_ID, (const hw_module_t **)&module) == 0) {
		LOGI("get Module OK");
		if (led_control_open(&module->common, &sLedDevice) != 0) {
			LOGI("led_init error");
			return -1;
		}
	}
		LOGI("led_init success");
		return 0;
		
}


static const JNINativeMethod gMethods[] = {
	{"_init",			"()Z",			(void *)led_init},
	{"_set_on",			"(I)I",			(void *)led_set_on},
	{"_set_off",		"(I)I",			(void *)led_set_off},
};

static int registerMethods(JNIEnv *env) {
	static const char * const kClassName = 
		"com/farsight/server/ledService";
	jclass clazz;
	clazz = env->FindClass(kClassName);
	if(clazz == NULL) {
		LOGE("Can't find class %s\n", kClassName);
		return -1;
	}

	if(env->RegisterNatives(clazz, gMethods, 
			sizeof(gMethods)/sizeof(gMethods[0])) !=  JNI_OK) 
	{
		LOGE("failed registering methods for %s\n", kClassName);
		return -1;
	}

	return 0;
}


jint JNI_OnLoad(JavaVM *vm, void *reserved) {
	JNIEnv *env = NULL;
	jint result = -1;
	LOGI("JNI_onLoad");

	if(vm->GetEnv((void **)&env, JNI_VERSION_1_4) != JNI_OK) {
		LOGE("ERROR: jni_onload()\n");
		goto fail;
	}

	assert(env != NULL);
	if(registerMethods(env) != 0) {
		LOGE("ERROR: registerMethod()\n");
		goto fail;
	}

	result = JNI_VERSION_1_4;

fail:
	return result;
}

在jni里首先加载jni库文件的时候先要调用JNI_OnLoad函数,通过系统函数GetEnv让env指针获得有效的值。然后接着调用registerMethods函数,这个函数是自己定义一个函数。

static const char * const kClassName = "com/farsight/server/ledService"; 类名与Eclipse下开发对应的包一致。不过点换成了下划线。

然后找到对应的类,接着就是向系统注册Native函数(Native Interface即本地接口函数),函数列表gMethods里 _init是上层framework去加载库时候调用的,当上层调用_init时,与之对应调用的函数就是led_init, ()Z的意思是函数led_init参数为空,返回为空。这里其实就是做了一个函数的映射,上层用的java函数,在这里与之对应成c 函数。

同理,其余的_set_on _set_off就不必赘述。

在调用led_init()函数时,系统是如何找到与之对应的stub的呢(也就是如何找到hw_module_t结构体的呢)?主要的函数就是hw_get_module这个函数是通过第一个参数ID号,找到系统里已经存在的与之对应id号的stub(即led_module_t HAL_MODULE_INFO_SYM 结构体变量),第二个参数就传进去的二级指针,让module获取有效的值,

接着调用 led_control_open,这个函数是内联函数,函数里面接着调用了HAL_MODULE_INFO_SYM 里的methods,methods里就一个成员open,其实呢就是调用了led.c(stub)的led_device_open函数,sLedDevice指针是一个全局变量,经过这个函数的调用,sLedDevice就获得了hw_deive_t的地址(sLedDevice指向了hw_device_t)。

本来一个指针没有值,但是通过传进去二级指针,就能让原来为空的指针获得有效的值,你可以参考我写的博文 int*p和 int **p,对你们理解二级指针改变一级指针指向有帮助。既然在jni层能够获得stub里的hw_module_t 和 hw_device_t,那么去调用stub里的函数也就不是问题了。

 

接下来就是去实现framework层了,framew层里的service去调用jni的。framework层里的service是在eclipse下开发的。

文件名:ledService.java

代码:

package com.farsight.server;

import android.util.Log;

public class ledService {
	static {
		Log.i("ledService", "Load Native service LIB");
		System.loadLibrary("led_runtime");
	}
	public ledService() {
		Log.i ( "Java Service" , "do init Native Call" );
		_init ();
	}
	public boolean set_on(int arg) {
		if(0 == _set_on(arg)) {
			return true;
		}else {
			return false;
		}
	}
	
	public boolean set_off(int arg) {
		if(0 == _set_off(arg)) {
			return true;
		}else {
			return false;
		}
	}
	
	private static native boolean _init();
	private static native int _set_on(int arg);
	private static native int _set_off(int arg); 
}


 

private static native boolean _init();

private static native int _set_on(int arg);

private static native int _set_off(int arg);

这里的三个函数,就是在jni里声明的native interface接口函数。

当声明一个ledService 的对象时,static里的函数库会加载,默认的路径就是去加载/system/lib下与之对应的库,强调一点就是,led_runtime省去了前面的lib和后缀.so。

这样,我们去调用jni的时候就能成功,否则会失败。

 

其余的就是在应用程序里声明一个ledService对象,然后调用对象里的set_on 和 set_off 就可以了。可以自己写一个应用程序去测试一下。

下面是我的一个项目的截图:

因为设计到M0开发板,所以会有温湿度以及RFID卡的截图。

 

 源码下载地址:http://download.youkuaiyun.com/detail/k_linux_man/3865567

Android根文件系统、内核zIamge下载;http://download.youkuaiyun.com/detail/k_linux_man/3865826

基于“xxx” Androidx平台的驱动及系统开发 之 触摸板篇 触摸IC 驱动适配及遇到的问题并解决 详情 阅读详情

相关推荐

Android驱动程序开发实例精讲-0_Android系统HAL驱动开发经典案例详解(基于Android4.0)

通过学习一个LED点灯的简单功能,掌握Linux驱动程序与HAL硬件抽象层之间的调用方法,同时掌握JNI层的编写思想,学会使用Eclipse编写Android应用程序,深入体会Android HAL架构。本章内容主要参考文献:《Android深度探索(卷1) HAL与驱动开发》、《TQ210开发板Android_HAL_LED_V1.2.pdf》

zbl_learn的博客 2789

android驱动开发从零到一

驱动c文件放在编译好的安卓源码里,放在其本身有的驱动一起的位置,所有驱动一起编译并构建镜像

icedrunker的博客 1万+

android平台驱动开发(一)

最简单的hello world 驱动

墨染天玑的博客 2855

Android底层技术:Linux驱动框架与开发

于此,将框架(Framework)和设计模式(Design Pattern)应用于Linux驱动开发,说明了如何以面向对象、设计模式和框架概念来看待Linux驱动程序的架构。其直接的益处就是:让我们能基于一致的设计理念来结合Android HAL与Linux两层的驱动开发,规划出整体和谐Android-based软硬整合产品架构。

STM32F4三重ADC+DMA双缓冲实战:用CubeMX搞定同步采样,告别数据覆盖烦恼

本文详细介绍了STM32F4三重ADC与DMA双缓冲技术的实战应用,通过CubeMX配置实现同步采样,解决数据覆盖问题。内容涵盖硬件架构解析、CubeMX工程配置、双缓冲实现技巧及调试优化方法,适用于电机控制、电力监测等需要高精度多通道信号采集的场景。

weixin_30627341的博客 357

windows10 + wsl2安装docker

一、安装wsl https://docs.microsoft.com/zh-cn/windows/wsl/install-win10 1.1以管理员身份运行powershell 1.2执行如下命令 dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart 1.3重启电脑 1.4 重启电脑后,如果需要安装wsl2, 则安装官方文档继续后续操作,因我...

happytree001的博客 1567

自己动手写linux内核,自己动手写最简单的Android驱动---LED驱动的编写

开发平台:farsight s5pc100-a内核:linux2.6.29环境搭配:有博文介绍开发环境:Ubuntu 、Eclipse首先强调一下要点:1.编写Android驱动时,首先先要完成linux驱动,因为android驱动其实是在linux驱动基础之上完成了HAL层(硬件抽象层),如果想要测试的话,自己也要编写java程序来测试你的驱动。2.android的根文件系统是eclair_2....

weixin_29157361的博客 221

国产PLC实战:用汇川Easy320搞定TCP通信,手把手教你网口转串口控制设备

本文详细解析了如何使用汇川Easy320 PLC实现TCP通信与串口控制,解决工业自动化中设备间通信协议差异问题。通过实战案例和代码示例,展示了Easy320在协议转换、数据解析和串口转发中的高效应用,帮助工程师快速掌握网口转串口控制技术。

weixin_31774841的博客 497

自己动手写最简单的Android驱动---LED驱动的编写【转】

本文转载自:http://blog.youkuaiyun.com/k_linux_man/article/details/7023824 转载注明出处,作者:K_Linux_Man, 薛凯 山东中医药大学,给文章内容引入个人毕业设计。 开发平台:farsight s5pc100-a 内核:linux2.6.29 环境搭配:有博文介绍 开发环境:Ubuntu 、Eclipse 首先强调一下要点: 1.编写A...

zzb2760715357的博客 134

2.4G跳频算法 2.4G切信道算法 (简例示意nRF52832)

2.4G跳频算法 nRF52832示例 2.4G自适应信道算法

Peter.Min 955

风控IV、AUC、KS python计算代码

df_temp:数据集,Y:是否逾期标签,feas_list:需要计算IV的 变量。2、 按月和不同Y 下的 IV计算。

暮雨听轩的专栏 1617

c语言安卓驱动开发,android驱动学习1-驱动开发流程(Android.mk)

QQ:971586331软件环境:操作系统:windows 10IDE版本:Android Studio 3.4.2JAVA版本:jdk-8u221-windows-x64NDK版本:android-ndk-r20-windows-x86_64Kernel版本:linux 3.0开发板android版本:android 4.0.3硬件环境:开发板:itop-4412精英版本文内容:本文描述了如何...

weixin_39529128的博客 975

工商银行软件开发中心数据安全监测溯源防护体系建设成果及展望

结合业界先进溯源理念和我行管理要求,依托大数据、分布式搜索分析引擎等技术,构建PB级海量数据加工分析和小时级快速核验、检索、溯源手段(如图6所示),支撑6种客户敏感信息关键要素的单一或组合溯源,形成行之有效的数据安全溯源流程,配套落地自动化快速泄露样本真实性核验和灵活高效的溯源能力,通过数据相关属性的自动化分析,实现追踪泄露源头、明确泄露路径的目标,为有效应对数据安全事件,降低和消除风险与不良影响,开展事后追责处置工作提供体系化解决方案。提高数据排查的自动化程度和排查效率,降低人工成本。

银行信息系统应用架构详解 221

android驱动学习1-驱动开发流程(Android.mk)

QQ:971586331 软件环境: 操作系统:windows 10 IDE版本:Android Studio 3.4.2 JAVA版本:jdk-8u221-windows-x64 NDK版本:android-ndk-r20-windows-x86_64 Kernel版本:linux 3.0 开发板android版本:android 4.0.3 硬件环境: 开发板:itop-44...

sy84436446的博客 6128

韩版传奇 2 源码分析与 Unity 重制(一)服务端 TCP 状态管理

该专题将会分析 LOMCN 基于韩版传奇 2,使用 .NET 重写的传奇源码(服务端 + 客户端),分析数据交互、状态管理和客户端渲染等技术,此外笔者还会分享将客户端部分移植到 Unity 和服务端用现代编程语言重写的全过程。本文重点介绍了服务端的启动链路、网络初始化和连接处理这三个过程,并简要分析的游戏循环的实现与数据包处理,在下一篇文章中,我们会从客户端角度分析从游戏启动、登录、游戏开始和基础游戏交互的全流程。

东方猫的博客 2496

【超分辨率(Super-Resolution)】关于【超分辨率重建】专栏的相关说明,包含专栏简介、专栏亮点、适配人群、相关说明、阅读顺序、超分理解、实现流程、研究方向、论文代码数据集汇总等

本专栏研究领域为【超分辨率重建】,涵盖图像超分、视频超分,实时超分,4K修复等方面。主要内容包括主流算法模型的论文精读、论文复现、毕业设计、涨点手段、调参技巧、论文写作、应用落地等方面。算法模型从SRCNN开始更新至今,一般是一篇论文精读对应一篇论文复现。论文精读详解理论,归化繁为简,归纳核心,积累词句,培养阅读论文和论文写作能力。论文复现依托Pytorch代码,实现完整的模型训练流程,总结调参方法,记录碰到的bug,论文插图可视化,培养读写代码能力、做实验的能力、以及应用落地能力。

主要更新底层视觉(去噪、超分等)相关的科研内容,形式为【论文精读】+【论文复现】 3万+

Android 从硬件到应用:一步一步向上爬 1 -- 从零编写底层硬件驱动程序

硬件平台:TI AM335X Starter Kit 开发源码:TI-Android-ICS-4.0.3-DevKit-EVM-SK-3.0.1.bin 主机系统:Ubuntu 10.04        这次写《Android 从硬件到应用》是想尝试从底层的最简单的GPIO硬件驱动开始,一步一步的向上走,经过硬件抽象层HAL、JNI方法等,最终编写出APP,达到硬件调用的目的,期间会增加一些

Marvin_wu 9080

怀旧版电脑GBC模拟器

再现原版GBC掌机的界面,电脑运行连游戏界面都一样大小,很怀旧,很真实,(英文版)强烈推荐,很小的文件,很难才找到的。

从LED硬件控制流程认识Android架构!

层次概述APP层在Android Studio进行开发的人,调Java包的API,就是调API框架层Java API的提供者,它提供了被称为“服务”的东西,就是应用层你调某个API,我帮你做操作并给你返回值思考这里需要思考一个问题,例如硬件只有一个,那如果多个应用程序都要控制它,怎么办?这就要提到“服务”这个东西,就是安卓系统做主,它去建立一个进程访问硬件(上服务),然后应用层的进程要控制硬件,都给系统服务(下服务)发请求,“系统服务”会建立队列让它们排队使用这个硬件服务(

weixin_62622990的博客 1200

Outlook Express 6 (OE6) for Win7/Win8/Win10

英文版 Outlook Express6.0, 兼容Win7/Win10/WinXP,解压就可用。

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

第 1 篇 入坑不亏!瑞芯微 RK 平台 + 安卓驱动开发,小白全维度扫盲

本系列是国内少有的纯小白友好、全流程闭环的瑞芯微 RK 平台安卓驱动开发教程,全程遵循「扫盲→筑基→环境→原理→实战→进阶→项目落地」的渐进式路径,绝对不跳步、不搞天书式教学。

爱片闲串,爱钓鱼,爱中医,爱分享 1172

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

上一篇: Farsight(华清远见) s5pc100-a(A8) 开发板Android驱动开发环境搭建
下一篇: Android jni GetFieldID 和 GetMethodID 函数的说明
karry_vcent
博客等级 码龄15年 93粉丝 45原创
评论 17
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值