Win32编程串口超时结构体的一般性设置

本文详细介绍了Win32串口通信中的超时设置原理及不同配置模式的效果,包括非阻塞、无限阻塞与定时阻塞模式,并通过实验对比了各种配置参数对数据接收的影响。

  Win32串口结构体有5个成员变量:读数据拼接超时、读数据总超时常数项、读数据总超时一次项、写数据总超时常数项、写数据总超时一次项。

  拼接超时即是指两个字节之间不大于这个毫秒数,底层即将这两个字节视为同一组报文。仅读数据有拼接超时。这个参数通常都是正值,不应设置为0或负数。

  总超时常数项是指调用函数开始时进行计时,如果没有收到新数据,则等待这个时间。

  总超时一次项是每接收一个字节,则在常数项的基础上增加一份时间。

  一般我们把写超时都设置为0,读数据总超时一次项也设置为0,只使用读数据拼接超时和读数据总超时常数项。

  这两个超时参数可以配置成简单数据拼接模式、全阻塞拼接模式、定时阻塞拼接模式。模式与是否设置了OVERLAPPED无关。

非阻塞模式:

  有一个特殊的组合:-1,0,0,0,0,也就是数据拼接超时为-1,其它4个参数全为0。这是一个特殊的配置,效果是非阻塞接收。非阻塞模式下底层的数据拼接不起作用。在Win32的串口编程中会说到一个MAXDWORD,也就是0xffffffff,和-1是同一个值,不用纠结叫什么名字。这个模式适合新手学习使用,可以在一个线程中处理多个串口通信,适合那些不擅长多线程的开发者(多线程里面有很多坑,如果没把握的话还是宁可不要乱用)。当然,一个线程处理所有串口也可以节省内存,性能上不会差很多,就是CPU占用变高一点点。

无限阻塞模式:

  无限阻塞模式的组合是:1,0,0,0,0,与非阻塞模式的区别在于数据拼接超时变成正数。这里用的是1,其实也可以换一个拼接超时的值,以增强数据拼接的功能。使用1是我个人的SDK中对通信工具代码的要求——要尽可能弱化底层超时的作用。这后面有一套复杂的机制来保证数据的完整性和响应性。如果不想搞那么麻烦,就用个大点的数字,牺牲掉一点响应性来保证数据的完整性也是可以的。这个模式适合开发从机线程。

定时阻塞模式:

  有限阻塞模式的组合是:1,1000,0,0,0,这个组合是最大阻塞1000ms,这个根据需要来写,不要照抄,有数据时立即返回。读数据总超时常数项就是限制等待的最长时间。同样的,如果不想搞一大堆代码来同时保证数据完整性和响应性,还是把数据拼接超时设置一个大点的数字。这个模式适合开发主机线程。

串口读超时设置试验

   以下按拼接超时、常数项、一次项的顺序命名配置参数。发送的方式为隔100ms发送一个'a',发送20个时延迟2000ms。

1, 0, 0

  几乎没有延时,每传输一个字节就立即返回这个字节。没有收到传输的字节时就保持等待。命令行显示如下:

send: a
a
send: a
a

0, 1, 0

  几乎没有阻塞,接收线程一直在循环,如果没有收到数据就返回null(实际上是Win32的ReadFile返回长度为0,在Java中我把它转换成了null),如果有收到数据就返回这个字节。命令行显示如下:

null
null
send: a
a
null
null

0, 0, 1

  几乎不按套路出牌,接收线程无视数据发送的时机等待,突然返回一串东西。命令行显示如下:

send: a
send: a
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
send: a
send: a

  这个数字会影响一次接收到的字符串长度,但规律很复杂。一次项的数字越大,接收的长度倾向于变得更长。字节发送越频繁,这个长度也倾向于变得更长。

0, 1000, 1

  在上一节的基础上增加了一个1s的延迟。收到的东西多了一点,没什么结构性的变化。

0, 1000, 0

  调用receive后固定一个时间后返回。等价于sleep一秒后调用无阻塞的receive。命令行显示如下:

send: a
send: a
send: a
send: a
send: a
send: a
send: a
send: a
send: a
aaaaaaaaa
send: a
send: a
send: a
send: a
send: a
send: a
send: a
send: a
send: a
aaaaaaaaa
send: a
a
null
send: a
send: a

200, 0, 0

  接收到数据后200ms内无数据后返回。如果没有接收到数据,则等待。命令行显示如下:

send: a
send: a
send: a
send: a
send: a
send: a
send: a
send: a
send: a
send: a
send: a
send: a
send: a
send: a
send: a
send: a
send: a
send: a
send: a
send: a
aaaaaaaaaaaaaaaaaaaa

200, 1000, 0

  接收到数据后200ms内无数据返回,如果有数据但离函数调用的时间超过了1000ms也会返回。命令行显示如下:

send: a
send: a
send: a
send: a
send: a
send: a
send: a
send: a
send: a
aaaaaaaaa
send: a
send: a
send: a
send: a
send: a
send: a
send: a
send: a
send: a
send: a
aaaaaaaaaa
null
send: a
a
send: a

  从以上试验可以看出第一个参数控制的是对报文连续性的判断,第二个参数控制的是函数强制返回的时间,第三个参数则是每接收一个字节就按比例延长函数强制返回的时间。返回的逻辑可以写作:

if(当前时间 - 最后一个字节接收的时间 > 数据拼接超时
        || 当前时间 - 第一个字节接收的时间 > 读数据总超时常数项 + 读数据总超时一次项 * 已接收的字节数 * 一个未知的系数)
    break;

  非阻塞有一个特殊的配置组合,只有一种模式,配置不是唯一的,但效果都一样,没有可以深入的地方。下文介绍的是阻塞模式下的一些配置方法。

  如果想要快速反应,则数据拼接超时就要尽可能短,也就是应为1(非阻塞配置下为-1,那个是特殊情况)。此时总超时实质上有很大的概率不起作用,多数报文都是破碎的,需要在应用层处理数据拼接。

  如果不需要快速反应,希望充分发挥系统底层的数据拼接能力,则数据拼接超时应在合理的范围选择尽可能大的值,读数据总超时常数项要够大,读数据总超时一次项可以取1,或不怎么大的数字。一次项对后期的影响很大,不要取太大的数值。

  可以给这三个参数换个叫法,依次为:引力常数、耐性常数、希望系数。拼接超时其实就像是碎片之间的引力大小,设置越高则碎片越容易聚合。读数据总超时常数项就是接收函数的初始耐心值,它越大则这个函数越有耐心可以在没有数据的情况下等下去。读数据总超时一次项就像是接收函数在成功后对后面还有更多数据的希望,就像炒股的人越涨越买一样,收到的数据越多,它越会继续等下去。

  引力是客观的,如果碎片吸不到一起,则必然破碎。耐性和希望是主观的,它的作用建立在碎片能够吸到一起,也就是客观存在决定主观意识。另外耐性和希望只是决定了数据的量涨到多少可以抛出,而引力决定了数据的断崖。比方说如果股价突然跳楼了,那么散户就有大概率会割肉,而如果数据碎片确定吸不到一起了,这个函数也会立即像散户割肉一样抛出已经持有的数据,不会再留有任何的耐心和希望。

附上一段Win32 Java代码示范:

@Override
	public SerialPortMessage receive(long blockTimeout) {
		if (!open()) {
			return null;
		}
		try {
			if (blockTimeout == 0) {
				// 非阻塞
				if (!setTimeout(m_handle,
						-1,
						0,
						0,
						0,
						0)) {
					throw new IOException("receive com" + m_comPort + " failed. last error at setTimeout: " + getLastErrorCode());
				}
			} else if (blockTimeout < 0) {
				// 无限阻塞
				if (!setTimeout(m_handle,
						1,
						0,
						0,
						0,
						0)) {
					throw new IOException("receive com" + m_comPort + " failed. last error at setTimeout: " + getLastErrorCode());
				}
			} else {
				// 定时阻塞
				if (!setTimeout(m_handle,
						1,
						blockTimeout,
						0,
						0,
						0)) {
					throw new IOException("receive com" + m_comPort + " failed. last error at setTimeout: " + getLastErrorCode());
				}
			}
			SerialPortMessage ret = read();
			if (!setTimeout(m_handle, 1, 0, 0, 0, 0)) {
				throw new IOException("receive com" + m_comPort + " failed. last error at setTimeout: " + getLastErrorCode());
			}
			return ret;
		} catch (Throwable e) {
			getOnCatch().accept(e);
			close();
		}
		return null;
	}

附上JNI代码:

pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_JNI.h

/* DO NOT EDIT THIS FILE - it is machine generated */
#include "JNIInclude/jni.h"
/* Header for class pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_JNI */

#ifndef _Included_pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_JNI
#define _Included_pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_JNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_JNI
 * Method:    getLastError
 * Signature: ()J
 */
JNIEXPORT jlong JNICALL Java_pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_1JNI_getLastError
  (JNIEnv *, jclass);

/*
 * Class:     pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_JNI
 * Method:    createFile
 * Signature: ([BZ)J
 */
JNIEXPORT jlong JNICALL Java_pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_1JNI_createFile
  (JNIEnv *, jclass, jbyteArray, jboolean);

/*
 * Class:     pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_JNI
 * Method:    closeHandle
 * Signature: (J)Z
 */
JNIEXPORT jboolean JNICALL Java_pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_1JNI_closeHandle
  (JNIEnv *, jclass, jlong);

/*
 * Class:     pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_JNI
 * Method:    setupComm
 * Signature: (JJJ)Z
 */
JNIEXPORT jboolean JNICALL Java_pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_1JNI_setupComm
  (JNIEnv *, jclass, jlong, jlong, jlong);

/*
 * Class:     pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_JNI
 * Method:    setCommTimeouts
 * Signature: (JJJJJJ)Z
 */
JNIEXPORT jboolean JNICALL Java_pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_1JNI_setCommTimeouts
  (JNIEnv *, jclass, jlong, jlong, jlong, jlong, jlong, jlong);

/*
 * Class:     pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_JNI
 * Method:    setCommState
 * Signature: (JJJJJ)Z
 */
JNIEXPORT jboolean JNICALL Java_pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_1JNI_setCommState
  (JNIEnv *, jclass, jlong, jlong, jlong, jlong, jlong);

/*
 * Class:     pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_JNI
 * Method:    purgeComm
 * Signature: (J)Z
 */
JNIEXPORT jboolean JNICALL Java_pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_1JNI_purgeComm
  (JNIEnv *, jclass, jlong);

/*
 * Class:     pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_JNI
 * Method:    readFile
 * Signature: (J[B)I
 */
JNIEXPORT jint JNICALL Java_pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_1JNI_readFile
  (JNIEnv *, jclass, jlong, jbyteArray);

/*
 * Class:     pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_JNI
 * Method:    writeFile
 * Signature: (J[BI)I
 */
JNIEXPORT jint JNICALL Java_pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_1JNI_writeFile
  (JNIEnv *, jclass, jlong, jbyteArray, jint);

#ifdef __cplusplus
}
#endif
#endif

 pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_JNI.cpp

#include <malloc.h>
#include <windows.h>
#include "pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_JNI.h"

#ifdef __cplusplus
extern "C" {
#endif
	/*
	 * Class:     pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_JNI
	 * Method:    getLastError
	 * Signature: ()J
	 */
	JNIEXPORT jlong JNICALL Java_pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_1JNI_getLastError
	(JNIEnv*, jclass) {
		return (unsigned long long) GetLastError();
	}

	/*
	 * Class:     pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_JNI
	 * Method:    createFile
	 * Signature: ([BZ)J
	 */
	JNIEXPORT jlong JNICALL Java_pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_1JNI_createFile
	(JNIEnv *jEnv, jclass jCls, jbyteArray jPortName, jboolean isOverlapped) {
		int len = jEnv->GetArrayLength(jPortName);
		char *buf = (char*) malloc(len);
		jEnv->GetByteArrayRegion(jPortName, 0, len, (jbyte*) buf);
		HANDLE handle = CreateFile(buf, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING,
				isOverlapped ? (FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED) : 0, NULL);
		free(buf);
		return (jlong) handle;
	}

	/*
	 * Class:     pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_JNI
	 * Method:    closeHandle
	 * Signature: (J)Z
	 */
	JNIEXPORT jboolean JNICALL Java_pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_1JNI_closeHandle
	(JNIEnv *jEnv, jclass jCls, jlong handle) {
		return CloseHandle((HANDLE) handle);
	}

	/*
	 * Class:     pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_JNI
	 * Method:    setupComm
	 * Signature: (JJJ)Z
	 */
	JNIEXPORT jboolean JNICALL Java_pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_1JNI_setupComm
	(JNIEnv *jEnv, jclass jCls, jlong handle, jlong inBuffer, jlong outBuffer) {
		return SetupComm((HANDLE) handle, inBuffer, outBuffer);
	}

	/*
	 * Class:     pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_JNI
	 * Method:    setCommTimeouts
	 * Signature: (JJJJJJ)Z
	 */
	JNIEXPORT jboolean JNICALL Java_pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_1JNI_setCommTimeouts
	(JNIEnv *jEnv, jclass jCls, jlong handle, jlong ReadIntervalTimeout, jlong ReadTotalTimeoutConstant,
			jlong ReadTotalTimeoutMultiplier, jlong WriteTotalTimeoutConstant, jlong WriteTotalTimeoutMultiplier) {
		COMMTIMEOUTS timeouts;
		timeouts.ReadIntervalTimeout = ReadIntervalTimeout;
		timeouts.ReadTotalTimeoutConstant = ReadTotalTimeoutConstant;
		timeouts.ReadTotalTimeoutMultiplier = ReadTotalTimeoutMultiplier;
		timeouts.WriteTotalTimeoutConstant = WriteTotalTimeoutConstant;
		timeouts.WriteTotalTimeoutMultiplier = WriteTotalTimeoutMultiplier;
		return SetCommTimeouts((HANDLE) handle, &timeouts);
	}

	/*
	 * Class:     pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_JNI
	 * Method:    setCommState
	 * Signature: (JJJJJ)Z
	 */
	JNIEXPORT jboolean JNICALL Java_pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_1JNI_setCommState
	(JNIEnv *jEnv, jclass jCls, jlong handle, jlong BaudRate, jlong ByteSize, jlong Parity, jlong StopBits) {
		DCB dcb;
		GetCommState((HANDLE) handle, &dcb);
		dcb.BaudRate = BaudRate;
		dcb.ByteSize = ByteSize;
		dcb.Parity = Parity;
		dcb.fBinary = TRUE;
		dcb.fParity = TRUE;
		dcb.StopBits = StopBits;
		return SetCommState((HANDLE) handle, &dcb);
	}

	/*
	 * Class:     pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_JNI
	 * Method:    purgeComm
	 * Signature: (J)Z
	 */
	JNIEXPORT jboolean JNICALL Java_pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_1JNI_purgeComm
	(JNIEnv *jEnv, jclass jCls, jlong handle) {
		return PurgeComm((HANDLE) handle, PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | PURGE_RXCLEAR);
	}

	/*
	 * Class:     pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_JNI
	 * Method:    readFile
	 * Signature: (J[B)I
	 */
	JNIEXPORT jint JNICALL Java_pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_1JNI_readFile
	(JNIEnv *jEnv, jclass jCls, jlong handle, jbyteArray jBuffer) {
		int bufLen = jEnv->GetArrayLength(jBuffer);
		char *buf = (char*) malloc(bufLen);
		//jEnv->GetByteArrayRegion(jBuffer, 0, bufLen, (jbyte*)buf);
		DWORD recvLen;
		DWORD dwErrorFlags;
		COMSTAT ComStat;
		OVERLAPPED ov;
		ClearCommError((HANDLE) handle, &dwErrorFlags, &ComStat);
		memset(&ov, 0, sizeof(ov));
		if (!ReadFile((HANDLE) handle, buf, bufLen, &recvLen, &ov)) {
			if (GetLastError() == ERROR_IO_PENDING) {
				GetOverlappedResult((HANDLE) handle, &ov, &recvLen, TRUE);
				jEnv->SetByteArrayRegion(jBuffer, 0, recvLen, (jbyte*) buf);
			} else {
				recvLen = -1;
			}
		} else {
			jEnv->SetByteArrayRegion(jBuffer, 0, recvLen, (jbyte*) buf);
		}
		free(buf);
		return recvLen;
	}

	/*
	 * Class:     pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_JNI
	 * Method:    writeFile
	 * Signature: (J[BI)I
	 */
	JNIEXPORT jint JNICALL Java_pers_laserpen_util_communication_serialPort_NativeSerialPortUtils_1JNI_writeFile
	(JNIEnv *jEnv, jclass jCls, jlong handle, jbyteArray jBuffer, jint length) {
		int bufLen = jEnv->GetArrayLength(jBuffer);
		char *buf = (char*) malloc(bufLen);
		jEnv->GetByteArrayRegion(jBuffer, 0, bufLen, (jbyte*) buf);
		DWORD sendLen;
		DWORD dwErrorFlags;
		COMSTAT ComStat;
		OVERLAPPED ov;
		ClearCommError((HANDLE) handle, &dwErrorFlags, &ComStat);
		memset(&ov, 0, sizeof(ov));
		if (!WriteFile((HANDLE) handle, buf, length, &sendLen, &ov)) {
			if (GetLastError() == ERROR_IO_PENDING) {
				GetOverlappedResult((HANDLE) handle, &ov, &sendLen, TRUE);
			} else {
				sendLen = -1;
			}
		}
		//jEnv->SetByteArrayRegion(jBuffer, 0, recvLen, (jbyte*)buf);
		free(buf);
		return sendLen;
	}

#ifdef __cplusplus
}
#endif

JNI的Java部分:

package pers.laserpen.util.communication.serialPort;

/**
 * javac -h 需要像编译class一样把所有引用的java文件都编译,为了防止引用java文件,把jni部分单独放在一个类中。
 * 
 * @author Laserpen
 */
abstract class NativeSerialPortUtils_JNI {
	native static final long getLastError();

	native static final long createFile(byte[] portName, boolean isOverlapped);

	native static final boolean closeHandle(long handle);

	native static final boolean setupComm(long handle, long inBuffer, long outBuffer);

	native static final boolean setCommTimeouts(long handle, long readIntervalTimeout, long readTotalTimeoutConstant,
			long readTotalTimeoutMultiplier, long writeTotalTimeoutConstant, long writeTotalTimeoutMultiplier);

	native static final boolean setCommState(long handle, long baudRate, long byteSize, long parity, long stopBits);

	native static final boolean purgeComm(long handle);

	native static final int readFile(long handle, byte[] buffer);

	native static final int writeFile(long handle, byte[] buffer, int length);
}
package pers.laserpen.util.communication.serialPort;

import pers.laserpen.util.string.ENCODING;

public abstract class NativeSerialPortUtils extends NativeSerialPortUtils_JNI {

	public static final long getLastErrorCode() {
		return getLastError();
	}

	public static final long openCom(int com, boolean isOverlapped) {
		String portStr = "\\\\.\\COM" + com + "\0";// C和C++无法识别byte[]的长度,需要添加一个0字符。
		return createFile(portStr.getBytes(ENCODING.US_ASCII()), isOverlapped);
	}

	public static final boolean closeCom(long handle) {
		return closeHandle(handle);
	}

	public static boolean setBuffer(long handle, int inBuffer, int outBuffer) {
		return setupComm(handle, inBuffer, outBuffer);
	}

	public static boolean setSimpleTimeout(long handle) {
		return setCommTimeouts(handle, -1, 0, 0, 0, 0);
	}

	public static boolean setTimeout(long handle, long readIntervalTimeout, long readTotalTimeoutConstant,
			long readTotalTimeoutMultiplier, long writeTotalTimeoutConstant, long writeTotalTimeoutMultiplier) {
		return setCommTimeouts(handle, readIntervalTimeout, readTotalTimeoutConstant, readTotalTimeoutMultiplier,
				writeTotalTimeoutConstant, writeTotalTimeoutMultiplier);
	}

	public static final boolean setState(long handle, long baudRate, ByteSize byteSize, Parity parity,
			StopBits stopBits) {
		return setCommState(handle, baudRate, byteSize.getFlag(), parity.getFlag(), stopBits.getFlag());
	}

	public static final boolean ready(long handle) {
		return purgeComm(handle);
	}

	public static final int read(long handle, byte[] buffer) {
		return readFile(handle, buffer);
	}

	public static final int write(long handle, byte[] buffer, int length) {
		return writeFile(handle, buffer, length);
	}

}

OVERLAPPED有什么作用?

  对我而言,OVERLAPPED已经没有作用了。上层封装使这个标志存在与否都表现出一样的特性。不过如果没有上层封装,OVERLAPPED可以把阻塞的通信变成非阻塞的流程。关键在于ReadFile。如果设置成非阻塞,ReadFile会返回当时得到的所有数据,这有可能是一些碎片。而使用OVERLAPPED设置超时结构体为阻塞,则ReadFile仍然是非阻塞的。使用ReadFile可以得到当前的状态,而不是当前的缓存字节。那么在合适的状态下取出缓存字节,就更有可能得到一个整体。

  我仍然建议做一个上层封装。因为即使能够一定程度上保证数据完整性,并使ReadFile非阻塞,OVERLAPPED仍然存在尾部超时,尾部超时是Win32 API层面上无法解决的。

为什么使用malloc和free?

  JNI代码用于Win32 API的基本操作,它只需要编写一次,因此即使出现内存溢出,故障也是有限的,总能把故障清理干净。而它的性能会影响所有用到它的应用。使用malloc和free可以达到尽可能高的性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值