carla与g29联合调试(二)

9 篇文章 0 订阅

前言:

对于力反馈的源码解析。

一、工作空间分析

工作空间如下图所示:

config文件中是对相关参数的设定;

launch文件是将yaml文件和执行文件融合启动;

msg文件是自定义消息类型;

script文件是python版本的ros执行文件,主要是为了发送控制旋转和力度的控制指令与carla的联合调试就是在这里实现的;

src里为核心内容通过输入输出信号控制g29

二、src解析

其他文件基础内容,只对于核心内容解析分析。

2.1 头文件

#include <ros/ros.h>
#include <linux/input.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <fcntl.h>
#include <math.h>

#include "g29_force_feedback/ForceFeedback.h"

其中:

#include <linux/input.h>

用于Linux系统中读取输入设备的信息。它提供了访问Linux内核输入子系统的接口,使开发者能够获取和处理用户输入事件,如按键、鼠标、触摸屏等。该头文件定义了一系列结构体和常量,用于描述输入设备的特性和事件。开发者可以使用这些结构体和函数来监听和处理输入事件,实现各种应用程序,例如图形界面、游戏、交互式应用等。主要用来与g29接口之间的输入输出。

#include <sys/ioctl.h>

用于进行输入输出控制操作。它提供了对设备的ioctl函数的声明,可以用来发送控制命令给设备驱动程序,实现一些特殊的操作。ioctl函数是一种特殊的系统调用,用于对已打开的设备文件进行控制。通过使用不同的参数和命令,可以实现对设备的配置、查询设备状态、发送控制命令等功能。在这个头文件中,定义了一些常量和结构体,用于ioctl函数中命令和参数的传递。程序员可以根据需要使用这些常量和结构体,调用ioctl函数来进行相应的设备控制操作。

#include "g29_force_feedback/ForceFeedback.h"

自定义消息类型

2.2 main函数

int main(int argc, char **argv)
{
    ros::init(argc, argv, "g29_force_feedback_node");
    G29ForceFeedback g29_ff;
    ros::spin();
    return(0);
}

核心在类G29ForceFeedback的实现中。

2.3 G29ForceFeedback声明

class G29ForceFeedback
{
private:
    ros::Subscriber sub_target;
    ros::Timer timer;
    float m_pub_rate;

    // variables from fouce feedback API
    int m_device_handle;
    int m_axis_code = ABS_X;
    int m_axis_min;
    int m_axis_max;
    struct ff_effect m_effect;

    // device config
    std::string m_device_name;
    double m_max_force;
    double m_min_force;

    // motion config 0:PID force, 1:constant force
    double m_Kp;
    double m_Ki;
    double m_Kd;
    double m_offset;

    // target and current state of the wheel
    bool m_pid_mode;
    double m_target_angle;
    double m_target_force;
    double m_current_angle;

public:
    G29ForceFeedback();

private:
    void targetCallback(const g29_force_feedback::ForceFeedback &in_target);
    void timerCallback(const ros::TimerEvent&);
    int testBit(int bit, unsigned char *array);
    void initFfDevice();
    void updateFfDevice();
};

逐行分析:

    ros::Subscriber sub_target;
    ros::Timer timer;
    float m_pub_rate;

一个订阅,一个计时,然后一个发布的速率。

    // variables from fouce feedback API
    int m_device_handle;
    int m_axis_code = ABS_X;
    int m_axis_min;
    int m_axis_max;
    struct ff_effect m_effect;

力反馈变量的相关设置,其中

m_device_handle:一个整数变量,表示力反馈设备的句柄或标识符。

m_axis_code:一个整数变量,表示力反馈设备的轴码,指示要应用力反馈效果的轴。

m_axis_min 和 m_axis_max:两个整数变量,表示力反馈设备的轴的最小值和最大值,用于标定力反馈效果的范围。

m_effect:一个ff_effect结构体变量,用于描述和控制力反馈效果。

而ABS_X为Linux输入子系统中定义的一个常量,用于表示力反馈设备的 X 轴。

ff_effect是Linux中的一个结构体,定义在<linux/input.h>头文件中。它用于描述和控制力反馈(force feedback)效果。

    // device config
    std::string m_device_name;
    double m_max_force;
    double m_min_force;

设备相关的设施,设备名称,最大力反馈、最小力反馈。

    // motion config 0:PID force, 1:constant force
    double m_Kp;
    double m_Ki;
    double m_Kd;
    double m_offset;

对于PID的设定。

    // target and current state of the wheel
    bool m_pid_mode;
    double m_target_angle;
    double m_target_force;
    double m_current_angle;

判断是否使用pid控制和目标角度、力反馈和当前的角度。

    void targetCallback(const g29_force_feedback::ForceFeedback &in_target);
    void timerCallback(const ros::TimerEvent&);
    int testBit(int bit, unsigned char *array);
    void initFfDevice();
    void updateFfDevice();

一些成员函数。

2.4 G29ForceFeedback实现

2.4.1 构造函数

G29ForceFeedback::G29ForceFeedback():
    m_device_name("/dev/input/event19"),
    m_Kp(0.1),
    m_Ki(0.0),
    m_Kd(0.0),
    m_offset(0.01),
    m_max_force(1.0),
    m_min_force(0.2),
    m_pub_rate(0.1),
    m_pid_mode(0)
{
    ros::NodeHandle n;
    sub_target = n.subscribe("/ff_target", 1, &G29ForceFeedback::targetCallback, this);

    n.getParam("device_name", m_device_name);
    n.getParam("Kp", m_Kp);
    n.getParam("Ki", m_Ki);
    n.getParam("Kd", m_Kd);
    n.getParam("offset", m_offset);
    n.getParam("max_force", m_max_force);
    n.getParam("min_force", m_min_force);
    n.getParam("pub_rate", m_pub_rate);

    initFfDevice();

    ros::Duration(1).sleep();
    timer = n.createTimer(ros::Duration(m_pub_rate), &G29ForceFeedback::timerCallback, this);
}

逐句分析:

    m_device_name("/dev/input/event19"),

默认使用event19接口

    m_Kp(0.1),
    m_Ki(0.0),
    m_Kd(0.0),
    m_offset(0.01),
    m_max_force(1.0),
    m_min_force(0.2),
    m_pub_rate(0.1),
    m_pid_mode(0)

相关参数设置。

    ros::NodeHandle n;
    sub_target = n.subscribe("/ff_target", 1, &G29ForceFeedback::targetCallback, this);

ros节点初始化,及订阅初始化,订阅topic为“/ff_target”,这个topic就是carla端要发送的topic。

    n.getParam("device_name", m_device_name);
    n.getParam("Kp", m_Kp);
    n.getParam("Ki", m_Ki);
    n.getParam("Kd", m_Kd);
    n.getParam("offset", m_offset);
    n.getParam("max_force", m_max_force);
    n.getParam("min_force", m_min_force);
    n.getParam("pub_rate", m_pub_rate);

参数服务器的设定,直接使用config里面的yaml文件设施,主要是为了方便调参。

    ros::Duration(1).sleep();
    timer = n.createTimer(ros::Duration(m_pub_rate), &G29ForceFeedback::timerCallback, this);

使用createTimer可以创建一个计时器对象,并指定它的触发周期。每次都使用回调函数timerCallback。


2.4.2 timerCallback函数

// update input event with timer callback
void G29ForceFeedback::timerCallback(const ros::TimerEvent&)
{
    updateFfDevice();
}

注意参数是const ros::TimerEvent&,这里有所疑问,以前从未遇见过。大概是形参可以直接省略?

// update input event with writing information to the event file
void G29ForceFeedback::updateFfDevice()
{
    struct input_event event;
    static float diff_i = 0.0, diff = 0.0;
    double diff_d, force, buf;

    // if you wanna use I control, let's avoid integral value exploding
    // static int count = 0;
    // count ++;
    // if (force > 0.3 || count > 10)
    // {
        //     diff_i = 0.0;
        //     count = 0;
        // }

    // calcurate values for PID control
    buf = diff;
    diff = m_target_angle - m_current_angle;
    diff_i += diff;
    diff_d = diff - buf;

    if (m_pid_mode)
    {
        force = fabs(m_Kp * diff + m_Ki * diff_i + m_Kd * diff_d) * ((diff > 0.0) ? 1.0 : -1.0);

        // if wheel angle reached to the target
        if (fabs(diff) < m_offset)
        {
            force = 0.0;
        }
        else
        {
            // force less than 0.2 cannot turn the wheel
            force = (force > 0.0) ? std::max(force, m_min_force) : std::min(force, -m_min_force);
            // set max force for safety
            force = (force > 0.0) ? std::min(force, m_max_force) : std::max(force, -m_max_force);
        }
    }
    else
    {
        force = fabs(m_target_force) * ((diff > 0.0) ? 1.0 : -1.0);

        // if wheel angle reached to the target
        if (fabs(diff) < m_offset)
        {
            force = 0.0;
        }
    }

    // for safety
    force = (force > 0.0) ? std::min(force, m_max_force) : std::max(force, -m_max_force);

    // start effect
    m_effect.u.constant.level = (short)(force * 32767.0);
    m_effect.direction = 0xC000;
    m_effect.u.constant.envelope.attack_level = (short)(force * 32767.0);
    m_effect.u.constant.envelope.fade_level = (short)(force * 32767.0);

    if (ioctl(m_device_handle, EVIOCSFF, &m_effect) < 0)
    {
        std::cout << "failed to upload m_effect" << std::endl;
    }

    // get current state
    while (read(m_device_handle, &event, sizeof(event)) == sizeof(event))
    {
        if (event.type == EV_ABS && event.code == m_axis_code)
        {
            m_current_angle = (event.value - (m_axis_max + m_axis_min) * 0.5) * 2 / (m_axis_max - m_axis_min);
        }
    }
}

逐句分析:

    struct input_event event;
    static float diff_i = 0.0, diff = 0.0;
    double diff_d, force, buf;

input_event 是一个结构体类型,它是 Linux 内核中用于表示输入设备事件的结构体,

定义了两个静态变量diff_i和diff。

diff_d、force和buf是一些参数,是为了使用PID调参,主要是为了调节目标角度和当前角度之间的PID控制。

    // calcurate values for PID control
    buf = diff;
    diff = m_target_angle - m_current_angle;
    diff_i += diff;
    diff_d = diff - buf;

简单描述下pid的过程,

diff是全局变量,其中buf用于存储上一次获得的diff数据,

然后用目标角度减去当前角度来计算出新的diff,

diff_i是diff的积分值,用于累计差值以实现积分控制,

diff_d是微分值,用于计算差值的变化率。

下面是pid控制的过程:

    if (m_pid_mode)
    {
        force = fabs(m_Kp * diff + m_Ki * diff_i + m_Kd * diff_d) * ((diff > 0.0) ? 1.0 : -1.0);

        // if wheel angle reached to the target
        if (fabs(diff) < m_offset)
        {
            force = 0.0;
        }
        else
        {
            // force less than 0.2 cannot turn the wheel
            force = (force > 0.0) ? std::max(force, m_min_force) : std::min(force, -m_min_force);
            // set max force for safety
            force = (force > 0.0) ? std::min(force, m_max_force) : std::max(force, -m_max_force);
        }
    }
    else
    {
        force = fabs(m_target_force) * ((diff > 0.0) ? 1.0 : -1.0);

        // if wheel angle reached to the target
        if (fabs(diff) < m_offset)
        {
            force = 0.0;
        }
    }

其中fabs函数的作用是计算绝对值;

m_Kp在参数服务器下表示的是Kp值;

m_Ki在参数服务器下表示的是Ki值;

m_Kd在参数服务器下表示的是Kd值;

然后分别乘上相应的参数,这行代码的目的是计算出根据PID控制器输出的力或转矩的大小,并且根据diff的正负情况来确定方向。

后面的if和else结构里面是说如果diff小于m_offset的值(设定为0.01)说明到了指定target_angle就将force设置为0。

后面的else就是设定安全范围,在设置的最小和最大值之间。

最后的else是设定不适用Pid控制时,当达到设定的值0.01以下直接置零,其他不操作。

    // start effect
    m_effect.u.constant.level = (short)(force * 32767.0);
    m_effect.direction = 0xC000;
    m_effect.u.constant.envelope.attack_level = (short)(force * 32767.0);
    m_effect.u.constant.envelope.fade_level = (short)(force * 32767.0);

这里用到底了使用ff_effect生成的对象,m_effect。

ff_effect时Linux中使用的力反馈结构,这四行是使用的控制参数,具体含义不太理解。

在网上找到的关于ff_effect的定义:

  • type:表示力反馈效果的类型,例如振动、冲击等。
  • id:用于标识力反馈效果的唯一 ID。
  • direction:表示力的方向,通常使用角度或向量来表示。
  • trigger:定义触发力反馈效果的触发器。
  • replay:设置力反馈效果的重播次数和时间间隔。
  • u:联合体(union),用于存储具体力反馈效果的各种属性和参数。
    if (ioctl(m_device_handle, EVIOCSFF, &m_effect) < 0)
    {
        std::cout << "failed to upload m_effect" << std::endl;
    }

其中ioctl函数是头文件#include <sys/ioctl.h>带入的。

它的作用是将上一步获得的ff_effect对象的相关参数传递给指定的设备,

m_device_handle表示要控制的设备名字,就是可以在参数服务器中设置的,

EVIOCSFF是请求码,表示具体的操作,这里是将m_effect结构体上传到设备,

然后如果返回值小于0表示失败。就会输出"failed to upload m_effect"。

    // get current state
    while (read(m_device_handle, &event, sizeof(event)) == sizeof(event))
    {
        if (event.type == EV_ABS && event.code == m_axis_code)
        {
            m_current_angle = (event.value - (m_axis_max + m_axis_min) * 0.5) * 2 / (m_axis_max - m_axis_min);
        }
    }

使用read函数读取控制设备的状态,并存储在event变量中。

如果event.type == EV_ABS && event.code == m_axis_code符合条件,就将m_current_angle赋值。主要作用是是获取设备上特定轴的当前角度,并将其存储在 m_current_angle变量中。

这里给出在网上找到的关于Linux控制符的相关资料:

链接:Linux输入子系统:事件的编码 -- event-codes.txt_ev_key_DroidPhone的博客-CSDN博客

 Event types:types对应于一个相同逻辑输入结构的一组Codes。每个type都有一组可用的codes用于产生输入事件。每个type可用的codes的详细信息请参考Codes一节的内容。

* EV_SYN:

  - 用于事件间的分割标志。事件可能按时间或空间进行分割,就像在多点触摸协议中的例子。

 * EV_KEY:

  - 用来描述键盘,按键或者类似键盘设备的状态变化。

 * EV_REL:

  - 用来描述相对坐标轴上数值的变化,例如:鼠标向左方移动了5个单位。

 * EV_ABS:

  -用来描述相对坐标轴上数值的变化,例如:描述触摸屏上坐标的值。

 * EV_MSC:

  - 当不能匹配现有的类型时,使用该类型进行描述。

 * EV_SW:

  - 用来描述具备两种状态的输入开关。

 * EV_LED:

  - 用于控制设备上的LED灯的开和关。

 * EV_SND:

  - 用来给设备输出提示声音。

 * EV_REP:

  -用于可以自动重复的设备(autorepeating)。

 * EV_FF:

  - 用来给输入设备发送强制回馈命令。(震动?)

 * EV_PWR:

  - 特别用于电源开关的输入。.

 * EV_FF_STATUS:

  - 用于接收设备的强制反馈状态。

2.4.3 targetCallback函数

// get target information of wheel control from ros message
void G29ForceFeedback::targetCallback(const g29_force_feedback::ForceFeedback &in_target)
{
    m_pid_mode = in_target.pid_mode;
    m_target_angle = in_target.angle;
    m_target_force = in_target.force;
}

使用过ros来接收控制指令,包括是否开启pid、目标角度和控制的力度,其实逻辑是有冲突的。如果选择开启pid,那么force会强制设置为1,这是force就没有用了。

2.4.4 initFfDevice函数

// initialize force feedback device
void G29ForceFeedback::initFfDevice()
{
    // setup device
    unsigned char key_bits[1+KEY_MAX/8/sizeof(unsigned char)];
    unsigned char abs_bits[1+ABS_MAX/8/sizeof(unsigned char)];
    unsigned char ff_bits[1+FF_MAX/8/sizeof(unsigned char)];

    struct input_event event;
    struct input_absinfo abs_info;

    m_device_handle = open(m_device_name.c_str(), O_RDWR|O_NONBLOCK);
    if (m_device_handle < 0)
    {
        std::cout << "ERROR: cannot open device : "<< m_device_name << std::endl;
        exit(1);
    }else{std::cout << "device opened" << std::endl;}

    // which axes has the device?
    memset(abs_bits, 0, sizeof(abs_bits));
    if (ioctl(m_device_handle, EVIOCGBIT(EV_ABS, sizeof(abs_bits)), abs_bits) < 0)
    {
        std::cout << "ERROR: cannot get abs bits" << std::endl;
        exit(1);
    }

    // get some information about force feedback
    memset(ff_bits, 0, sizeof(ff_bits));
    if (ioctl(m_device_handle, EVIOCGBIT(EV_FF, sizeof(ff_bits)), ff_bits) < 0)
    {
        std::cout << "ERROR: cannot get ff bits" << std::endl;
        exit(1);
    }

    // get axis value range
    if (ioctl(m_device_handle, EVIOCGABS(m_axis_code), &abs_info) < 0)
    {
        std::cout << "ERROR: cannot get axis range" << std::endl;
        exit(1);
    }
    m_axis_max = abs_info.maximum;
    m_axis_min = abs_info.minimum;
    if (m_axis_min >= m_axis_max)
    {
        std::cout << "ERROR: axis range has bad value" << std::endl;
        exit(1);
    }

    // check force feedback is supported?
    if(!testBit(FF_CONSTANT, ff_bits))
    {
        std::cout << "ERROR: force feedback is not supported" << std::endl;
        exit(1);
    }else{std::cout << "force feedback supported" << std::endl;}

    // auto centering off
    memset(&event, 0, sizeof(event));
    event.type = EV_FF;
    event.code = FF_AUTOCENTER;
    event.value = 0;
    if (write(m_device_handle, &event, sizeof(event)) != sizeof(event))
    {
        std::cout << "failed to disable auto centering" << std::endl;
        exit(1);
    }

    // initialize constant foce m_effect
    memset(&m_effect, 0, sizeof(m_effect));
    m_effect.type = FF_CONSTANT;
    m_effect.id = -1;
    m_effect.trigger.button = 0;
    m_effect.trigger.interval = 0;
    m_effect.replay.length = 0xffff;
    m_effect.replay.delay = 0;
    m_effect.u.constant.level = 0;
    m_effect.direction = 0xC000;
    m_effect.u.constant.envelope.attack_length = 0;
    m_effect.u.constant.envelope.attack_level = 0;
    m_effect.u.constant.envelope.fade_length = 0;
    m_effect.u.constant.envelope.fade_level = 0;

    if (ioctl(m_device_handle, EVIOCSFF, &m_effect) < 0)
    {
        std::cout << "failed to upload m_effect" << std::endl;
        exit(1);
    }

    // start m_effect
    memset(&event, 0, sizeof(event));
    event.type = EV_FF;
    event.code = m_effect.id;
    event.value = 1;
    if (write(m_device_handle, &event, sizeof(event)) != sizeof(event))
    {
        std::cout << "failed to start event" << std::endl;
        exit(1);
    }
}

初始化函数。

    // setup device
    unsigned char key_bits[1+KEY_MAX/8/sizeof(unsigned char)];
    unsigned char abs_bits[1+ABS_MAX/8/sizeof(unsigned char)];
    unsigned char ff_bits[1+FF_MAX/8/sizeof(unsigned char)];

这是Iinux中相关参数的驱动设置,其中

key_bits数组表示按键的数量,

abs_bits数组表示绝对值事件的数量,

ff_bits数组是设备支持的最大力反馈效果数。

具体见这篇博文:

https://www.cnblogs.com/lifexy/p/7553861.html

    struct input_event event;
    struct input_absinfo abs_info;

两个输入的结构。

    m_device_handle = open(m_device_name.c_str(), O_RDWR|O_NONBLOCK);
    if (m_device_handle < 0)
    {
        std::cout << "ERROR: cannot open device : "<< m_device_name << std::endl;
        exit(1);
    }else{std::cout << "device opened" << std::endl;}

使用open打开函数,打开m_device_name的c语言str类型,并使用O_RDWR|O_NONBLOCK表示打开读写和非堵塞行驶,如果成功打开就m_device_handle大于0,否则小于0。

    // which axes has the device?
    memset(abs_bits, 0, sizeof(abs_bits));
    if (ioctl(m_device_handle, EVIOCGBIT(EV_ABS, sizeof(abs_bits)), abs_bits) < 0)
    {
        std::cout << "ERROR: cannot get abs bits" << std::endl;
        exit(1);
    }

使用memset函数将abs_bits数组全部置为0,

之后使用ioctl函数将获取EV_ABS表示绝对值事件,并使用某种宏将它读取到的东西存放在了abs_bits数组中。如果ioctl小于0就表示示未读取成功。

    // get some information about force feedback
    memset(ff_bits, 0, sizeof(ff_bits));
    if (ioctl(m_device_handle, EVIOCGBIT(EV_FF, sizeof(ff_bits)), ff_bits) < 0)
    {
        std::cout << "ERROR: cannot get ff bits" << std::endl;
        exit(1);
    }

和上面的同理。

    // get axis value range
    if (ioctl(m_device_handle, EVIOCGABS(m_axis_code), &abs_info) < 0)
    {
        std::cout << "ERROR: cannot get axis range" << std::endl;
        exit(1);
    }
    m_axis_max = abs_info.maximum;
    m_axis_min = abs_info.minimum;
    if (m_axis_min >= m_axis_max)
    {
        std::cout << "ERROR: axis range has bad value" << std::endl;
        exit(1);
    }

判断能否获得轴的信息。

    // check force feedback is supported?
    if(!testBit(FF_CONSTANT, ff_bits))
    {
        std::cout << "ERROR: force feedback is not supported" << std::endl;
        exit(1);
    }else{std::cout << "force feedback supported" << std::endl;}

测试是否支持力反馈。

    // auto centering off
    memset(&event, 0, sizeof(event));
    event.type = EV_FF;
    event.code = FF_AUTOCENTER;
    event.value = 0;
    if (write(m_device_handle, &event, sizeof(event)) != sizeof(event))
    {
        std::cout << "failed to disable auto centering" << std::endl;
        exit(1);
    }

将event初始化置为0,

并将相关参数初始化,

EV_FF表示是力反馈,

FF_AUTOCENTER表示是自动居中控制的代码,

value将它置零就是关闭这个功能。

    // initialize constant foce m_effect
    memset(&m_effect, 0, sizeof(m_effect));
    m_effect.type = FF_CONSTANT;
    m_effect.id = -1;
    m_effect.trigger.button = 0;
    m_effect.trigger.interval = 0;
    m_effect.replay.length = 0xffff;
    m_effect.replay.delay = 0;
    m_effect.u.constant.level = 0;
    m_effect.direction = 0xC000;
    m_effect.u.constant.envelope.attack_length = 0;
    m_effect.u.constant.envelope.attack_level = 0;
    m_effect.u.constant.envelope.fade_length = 0;
    m_effect.u.constant.envelope.fade_level = 0;

一些参数设置,应该是linux中设定力反馈的参数。

    if (ioctl(m_device_handle, EVIOCSFF, &m_effect) < 0)
    {
        std::cout << "failed to upload m_effect" << std::endl;
        exit(1);
    }

    // start m_effect
    memset(&event, 0, sizeof(event));
    event.type = EV_FF;
    event.code = m_effect.id;
    event.value = 1;
    if (write(m_device_handle, &event, sizeof(event)) != sizeof(event))
    {
        std::cout << "failed to start event" << std::endl;
        exit(1);
    }

开始力反馈。

2.4.5 testBit函数

int G29ForceFeedback::testBit(int bit, unsigned char *array)
{
    return ((array[bit / (sizeof(unsigned char) * 8)] >> (bit % (sizeof(unsigned char) * 8))) & 1);
}

三、总结

对于Linux中相关指令的调用需要很熟悉,特别是输入输出相关的内容。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值