linux IIO驱动框架

        工业IO(Industrial I/O)是专用于ADC和DAC的内核子系统,加速度计、陀螺仪、电流电压测量芯片、光传感器、压力传感器等都属于IIO系列设备。

        IIO模型采用设备和通道架构。其中设备属于芯片本身,通道则表示设备的单个采集线,设备可能有若干个通道。例如加速度计就有3个通道,每个轴(X、Y和Z)都有一个通道。

        IIO设备和用户空间交互有两种方式:

  • /sys/bus/iio/iio:deviceX/,代表传感器及其通道
  • /dev/iio:deviceX,字符设备,用于输出设备事件和数据缓冲区

        设备驱动程序使用IIO内核提供的功能和API来管理设备,并向IIO内核报告处理情况;IIO内核通过sysfs和字符设备将底层机制抽象到用户空间。

一、IIO数据结构

        IIO设备在内核中表示为struct iio_dev{},并由struct iio_info{}结构描述。IIO通道则由struc iio_chan_spec{}表示。

1. 设备结构iio_dev

结构体struct iio_dev{}的定义:

struct iio_dev {
    [...]
    int modes; 
    /* 支持不同的模式:
    INDIO_DIRECT_MODE: 设备提供/sysfs类型的接口
    INDIO_BUFFER_TRIGGERED: 设备支持硬件触发,当使用iio_triggered_buffer_setup()时函数设置缓冲区时,该模式会自动添加到设备;
    INDIO_BUFFER_HARDWARE: 该设备支持硬件缓冲区;
    INDIO_ALL_BUFFER_MODES: 上述两种模式的组合
    */
    struct device dev;
    int scan_bytes; // 捕捉并提供给缓冲区的字节数。
    const ulong *available_scan_mask;
    /*
        用来标志那个通道可以启用缓冲,例如三通道设备允许掩码0x7(0b111)和0(0b000),这意味着可以不启用和全部启动,例如不能只启用X和Y,
    static const ulong my_scan_masks[] = {0x7, 0x0};
    indio_dev->available_scan_masks = my_scan_masks;
    */
    const ulong *active_scan_mask;
    /* 已启用的通道掩码,只有这些通道的数据才被推入buffer,例如对于8通道的ADC转换器,如果仅启用第一(0)、第三(2)和最后一个(7)通道,则位掩码我0b10000101(0x85),active_scan_mask设置为0x85,然后驱动程序可以使用for_each_set_bit宏遍历每个设备位,获取数据并放入缓冲区中。
    */ 
    bool scan_timestamp; // 是否将捕获时间推入缓冲区,缓冲区作为最后一个元素推入buffer,时间戳是8字节长。
    struct iio_trigger *trig; // 当前设备的触发器
    struct iio_poll_fun *pollfunc; // 在接收的触发器上运行的函数
    struct iio_chan_spec *channels;  // 设备通道
    int num_channels;
    const char *name;
    const struct iio_info *info;  // 驱动程序的回调和常量信息
    const struct iio_buffer_setup_ops *setup_ops; // 用户启用、禁用缓冲区之前和之后的一组函数,如果未指定则使用iio内核默认的函数iio_triggered_buffer_setup_ops
    struct cdev chrdev;
};

// 其中结构体struct iio_buffer_setup_ops定义如下:
struct iio_buffer_setup_ops {
    int (*preenable)(struct iio_dev *); // enable缓冲之前的回调函数
    int (*postenable)(struct iio_dev *); // enable缓冲之后的回调函数
    int (*predisable)(struct iio_dev *); // disable缓冲之前的回调函数
    int (*postdisable)(struct iio_dev *); // disable缓冲之后的回调函数
    int (*validate_scan_mask)(struct iio_dev *indio_dev, const ulong *scan_mask);
};

用于结构体struct iio_dev{}的操作函数:

// iio_dev分配函数
struct iio_dev *iio_device_alloc(int sizeof_priv);
struct iio_dev *devm_iio_device_alloc(int sizeof_priv); // devres版本的alloc函数

void iio_device_free(struct iio_dev);

// iio_dev的注册和注销
int iio_device_register(struct iio_dev *indio_dev);
void iio_device_unregister(struct iio_dev *indio_dev);

// 分配函数使用示例,在驱动程序的probe函数中
struct iio_dev *indio_dev;
struct my_private_data *data;
indio_dev = iio_device_alloc(sizeof(*data));

data = iio_priv(indio_dev);

[...]// 其他配置
iio_device_register(indio_dev);

        iio_info{}用于声明iio内核使用的读取写入通道属性值的钩子函数:

struct iio_info {
    struct module *driver_module;
    const struct attribute_group *attrs;
    int (*read_raw)(struct iio_dev *indio_dev,
                struct iio_chan_spec const *chan,
                int *val, int *val2, long mask); // 用户读取sysfs设备时的会调用函数
    int (*write_raw)(struct iio_dev *indio_dev,
                struct iio_chan_spec const *chan,
                int val, int val2, long mask);

    [...] // 其他一些不关注的成员
};

2. 通道结构iio_chan_spec

        通道代表单条采集线。

struct iio_chan_spec {
    /*type指通道的测量类型,电压测量:IIO_VOLTAGE,
        其他如IIOL_LIGHT(光测量), IIO_ACCEL(加速度计)*/
    enum iio_chan_type type;
    int channel; // 当indexed=1时,指定通道index
    int channel2; // 当modified=1时,指定通道修饰符
    ulong address; //
    int scan_index; // 当使用缓冲区触发时,用于标识通道值在缓冲区record中的index;
    struct {
        char sign;
        u8 realbits;
        u8 storagebits;
        u8 shift;
        u8 repeat;
        enum iio_endian endianness;
    } scan_type; // 当使用缓冲区触发时,用于标识通道值在缓冲区record中的格式;
    /*
    通道在sysfs中属性文件以掩码形式指定,下面每个掩码均代表一个sysfs属性文件。
    */
    long info_mask_separate; // 对应的属性文件专属于此通道
    long info_mask_shared_by_type; // 对应的属性文件为相同类型的所有通道共享,导出的信息也由同一类型的通道共享;
    long info_mask_shared_by_dir;  // 对应的属性文件为相同方向的所有通道共享,导出的信息也由同一类型的通道共享;
    long info_mask_shared_by_all;  // 对应的属性文件为所有通道共享,导出的信息也由同一类型的通道共享;
    const struct iio_event_spec *event_spec;
    uint num_event_spec;
    const struct iio_chan_spec_ext_info *ext_info;
    const char *extend_name;
    const char *datasheet_name;
    uint modified:1; 
    /* 指出修饰符是否应用于此通道属性名称,例如IIO_MOD_X,IIO_MOD_Y, 
       IIO_MOD_Z是围绕xyz轴方向的修饰符,可用修饰符在enum iio_modifier中定义。
        修饰符仅影响sysfs目录中的通道属性名称,而不是值
    */
    uint indexed:1; // 指出通道属性名称是否具有索引
    uint output:1;
    uint differential:1;
};

// 其中info_mask_xxx的值对应到一个enum类型:enum struct iio_chan_info_enum
enum struct iio_chan_info_enum {
    IIO_CHAN_INFO_RAW = 0,
    IIO_CHAN_INFO_PROCESSED,
    IIO_CHAN_INFO_SCALE,
    IIO_CHAN_INFO_OFFSET,
    [...],
    IIO_CHAN_INFO_SAMP_FREQ,
    IIO_CHAN_INFO_FREQUENCY,
    IIO_CHAN_INFO_PHASE,
    IIO_CHAN_INFO_HARDWAREGAIN,
    IIO_CHAN_INFO_HYSTERESIS,
    [...],
};
// 对应命名字符为
static const char * const iio_chan_info_postfix[] = {
    [IIO_CHAN_INFO_RAW] = "raw",
    [IIO_CHAN_INFO_PROCESSED] = "input",
    [IIO_CHAN_INFO_SCALE] = "scale",
    [IIO_CHAN_INFO_CALIBBIAS] = "calibbias",
    [...],
    [IIO_CHAN_INFO_FREQ] = "sampling_frequency",
    [IIO_CHAN_INFO_FREQUENCY] = "frequency",
    [...],
};



        通道属性命名有IIO内核按照如下的方式自动生成:{direction}_{type}_{index}_{modifier}_{info_mask},

// sysfs文件名称格式:{direction}_{type}_{index}_{modifier}_{info_mask}
//其中direction对应于属性方向
static const char * const iio_direction [] = {
    [0] = "in",
    [1] = "out",
};

//其中type对应于通道的类型,取值与字符数组:
static const char * const iio_chan_type_name_spec [] = {
    [IIO_VOLTAGE] = "voltage",
    [IIO_CURRENT] = "current",
    [IIO_POWER] = "power",
    [IIO_ACCEL] = "accel",
    [...],
    [IIO_UVINDEX] = "uvindex",
    [IIO_ELECTRICALCONDUCTIVITY] = "electricalconductivity",
    [IIO_COUNT] = "count",
    [IIO_INDEX] = "index",
    [IIO_GRAVITY] = "gravity",
};

// 其中{index}取决于.indexed和.channel的值
// 其中{modifier}取决于.modified和.channel2的值,该值根据数组iio_modifier_names值进行替换
static const char *const iio_modifier_names[] = {
    [IIO_MOD_X] = "x",
    [IIO_MOD_Y] = "y",
    [IIO_MOD_Z] = "z",
    [IIO_MOD_X_AND_Y] = "x&y",
    [IIO_MOD_X_AND_Z] = "x&z",
    [IIO_MOD_Y_AND_Z] = "y&Z",
    [...],
    [IIO_MOD_CO2] = "co2",
    [IIO_MOD_VOC] = "voc",
};

// 其中info_mask和介绍iio_chan_spec结构是指定的名称相同

下面是chan_spec对应通道的方式,

//当每个通道有多个数据时,需要使用索引和修饰符识别

// 只有一个通道时示例如下:
static const struct iio_chan_spec adc_channels[] =  {
    {.type = IIO_VOLTAGE,
    .info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
    },
}
// 得出属性文件
/sys/bus/iio/iio:deviceX/in_voltage_raw

// 有3个通道时的示例如下:
static const struct  iio_chan_type adc_channels[] = {
{
    .type = IIO_VOLTAGE,
    .indexed = 1,
    .channel = 0,
    .info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
},
{
    .type = IIO_VOLTAGE,
    .indexed = 1,
    .channel = 1,
    .info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
},
{
    .type = IIO_VOLTAGE,
    .indexed = 1,
    .channel = 2,
    .info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
},
}
// 得出属性文件
/sys/bus/iio/iio:deviceX/in_voltage0_raw
/sys/bus/iio/iio:deviceX/in_voltage1_raw
/sys/bus/iio/iio:deviceX/in_voltage2_raw

// 假如光传感器有两个通道,一个用于红外光,另一个用于红外和可见光,此时需使用修饰符
static const struct iio_chan_spec my_light_channels[] = {
{
    .type = IIO_INDENSITY,
    .modified = 1,
    .channel2 = IIO_MOD_LIGHT_IR,
    .info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
    .info_mask_shared = BIT(IIO_CHAN_INFO_SAMP_FREQ),
},
{
    .type = IIO_INDENSITY,
    .modified = 1,
    .channel2 = IIO_MOD_LIGHT_BOTH,
    .info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
    .info_mask_shared = BIT(IIO_CHAN_INFO_SAMP_FREQ),
},
{
    .type = IIO_LIGHT,
    .info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED),
    .info_mask_shared = BIT(IIO_CHAN_INFO_SAMP_FREQ),
},
}
// 对应sysfs属性文件
/sys/bus/iio/iio:deviceX/in_intensity_ir_raw  // 测量IR强度的通道
/sys/bus/iio/iio:deviceX/in_intensity_both_raw  // 测量红外和可见光的通道
/sys/bus/iio/iio:deviceX/in_illuminance_input // 用于处理数据
/sys/bus/iio/iio:deviceX/sampling_frequency // 用于采样频率,全部通道共享


二、IIO触发缓冲区支持

        IIO设备驱动程序和触发器完全无关。触发器可以初始化一个或多个设备上的数据捕获,这些触发器用于填充缓冲区、作为字符设备提供给用户空间。

在linux内核中有如下触发器,可以用于和IIO设备关联:

  • iio-trig-interrupt: 这为使用IRQ作为IIO触发器提供支持。对应内核选项CONFIG_IIO_INTERRUPT_TRIGGER
  • iio-trig-hrtimer: 提供基于时钟频率的IIO触发器,使用HRT作为中断源(从内核4.5开始)。内核选项为CONFIG_IIO_HRTIMER_TRIG
  • iio-trig-sysfs:允许使用sysfs向触发数据捕获。内核选项CONFIG_IIO_SYSFS_TRIGGER
  • iio-trig-bfin-timer:允许使用blackfin定时器作为IIO触发器

1. IIO驱动程序支持缓冲区

        IIO驱动程序可以声明任意数量的触发器,也可以选择哪些通道的数据推入缓冲区中。

        IIO设备提供触发缓冲区支持时,必须设置iio_dev.pollfunc,触发器触发时执行它,它负责通过indo_dev->active_scan_mask查找启用的通道,检索其数据,并使用iio_push_to_buffers_with_timestamp函数将他们提供给indio_dev->buffer。IIO内核提供了一组函数来设置触发缓冲区。

下面是IIO驱动程序使用触发器的流程:

// 1. 如果需要填写iio_buffer_setup_ops{}结构
const struct iio_buffer_setup_ops sensor_buffer_setup_ops = {
.preenable = my_sensor_buffer_preenable,
.postenable = my_sensor_buffer_postenable,
.predisable = my_sensor_buffer_predisable,
.postdisable = my_sensor_buffer_postdisable,
};

// 2. 编写与触发器关联的上半部。在绝大多数情况下,必须提供捕获的时间戳。
irqreturn_t sensor_iio_pollfunc(int irq, void *p) {
    struct iio_poll_func *pf = p;
    pf->timestamp = iio_get_ns((struct indio_dev*)pf->indio_dev);
    return IRQ_WAKE_THREAD;
}

// 3. 编写触发下半部,它将从每个通道读取数据,并把他们加入缓冲区中。
irqreturn_t sensor_trigger_handler(int irq, void *p) {
    struct iio_poll_func *pf = p;
    struct indio_dev* iio_dev = pf->indio_dev;
    u16 buf[8];
    int bit,i=0;

    // 使用锁保护缓冲区,mutex_lock(&my_mutex);
    // 3.1 获取每个通道的数据
    for_each_set_bit(bit, iio_dev->active_scan_mask, iio_dev->masklength) {
        buf[i++] = sensor_get_data(bit); // 获取某个通道的数据,具体和芯片实现相关
    }
    /*
        3.2 如果iio_dev.scan_timestamp = true, 则捕获时间戳将被推送和存储
        在将其推送到设备缓冲区之前,它作为示例缓冲区对象的最后一个元素,占8个字节
    */
    iio_push_to_buffers_with_timestamp(iio_dev, buf, pf->timestamp);
    // 打开锁,mutex_unlock(&my_mutex);

    // 3.3 通知触发
    iio_trigger_notify_done(iio_dev->trig);
}

// 4. 在probe函数中,必须首先设置缓冲区,再使用iio_device_register()注册设备
iio_triggered_buffer_setup(indio_dev, sensor_iio_pollfunc,
                            sensor_trigger_handler, sensor_buffer_setup_ops);
// 其中函数iio_triggered_buffer_setup也将为设备提供INDIO_DIRECT_MODE功能,当触发器指定到设备时,并不知道什么时候会被触发。
// 在支持缓冲区捕获机制时,应防止在sysfs上的数据捕获(有read_raw钩子执行)与缓冲区捕获冲突,在对应的钩子函数read_raw中应该用iio_buffer_enabled()判断是否已启动了缓冲区捕获,如下所示:
static int my_read_raw(struct iio_dev *indio_dev,
                        const struct iio_chan_spec *chan,
                        int *val, int *val2, long mask) {
    [...]
    switch(mask) {
    case IIO_CHAN_INFO_RAW:
        if (iio_buffer_enabled(iio_dev)) return -EBUSY;
    }
    [...]
}

2. IIO触发器和用户空间sysfs

        可以在用户空间设置IIO设备对应的触发器。sysfs中有个位置与触发器相关:

  • /sys/bus/iio/devices/triggerY/,一旦IIO触发器在IIO内核中注册并且对应于索引Y的触发器,就会创建该目录,该目录中有一个属性文件name;
  • /sys/bus/iio/devices/iio:deviceX,如果设备支持触发缓冲区,则会自动创建目录/sys/bus/iio/devices/iio:deviceX/trigger/*; 

i). sysfs触发器接口

        sysfs触发器对应的sysfs目录为/sys/bus/iio/devices/iio_sysfs_trigger/,用于sysfs触发器管理,该目录下有两个文件add_trigger和remove_trigger。

a). 创建触发器trigger,示例:

        echo 2 > /sys/bus/iio/devices/iio_sysfs_trigger/add_trigger

        会创建触发器sysfstrig2,创建目录/sys/bus/iio/devices/trigger2/,在该目录下文件name,其值为“sysfstrig2”;在该目录下还有文件trigger_now,用于开始触发捕获,并把数据推入他们各自的缓冲区。

b). 删除触发器 对应sysfs文件remove_trigger

        echo 2 > /sys/bus/iio/devices/iio_sysfs_trigger/add_trigger

c). 把触发器绑定给特定的IIO设备

        要把IIO设备与触发器绑定,需要将触发器名称写入IIO设备的sysfs文件/sys/bus/iio/devices/iio:deviceX/trigger/current_trigger中,示例如下:

        echo "sysfstrig2" > /sys/bus/iio/devices/iio:deviceX/trigger/current_trigger

解除绑定:

        echo "" > /sys/bus/iio/devices/iio:deviceX/trigger/current_trigger

ii). 中断触发器使用

下面是中断触发器的示例:

static struct resource iio_irq_trigger_resources[] = {
[0] = {
    .start = IRQ_NR_FOR_YOUR_IRQ,
    .flags = IORESOURCE_IRQ | IORESOURCE_IRQ_LOWEDGE,
},
};

static struct platform_device iio_irq_trigger = {
    .name = "iio_interrupt_trigger",
    .num_resources = 1,
    .reource = iio_irq_trigger_resources,
};

platform_device_register(&&iio_irq_trigger);

// 声明为IRQ触发器,将导致加载IRQ触发器独立模块。IRQ触发器名称的格式为irqtrigX,其中X为对应于刚传递的虚拟IRQ, 查看cat /sys/bus/iio/devices/trigger0/name

// 下面是在设备树节点声明IRQ触发器接口:
mylabel : my_trigger@0 {
    compatible = "iio_interrupt_trigger";
    interrupt-parent = <&gpio4>;
    interrupts = <30 0x0>;
};

iii). hrtimer触发器接口

        hrtimer触发器依赖于configfs文件系统。

mkdir /config

mount -t configfs none /config

        模块iio-trig-hrtimer会自动在/config/目录下创建/config/iio,用户可以在目录/config/iio/triggers/hrtimer/目录下创建hrtimer触发器:

mkdir /config/iio/triggers/hrtimer/my_trigger_name/

删除Hrtimer触发器:

rmdir /config/iio/triggers/hrtimer/my_trigger_name/

每个hrtimer触发器在该触发器目录中都包含单个sampling_frequency属性。

3. IIO缓冲区中的数据

        IIO缓冲区提供连续的数据捕获,一次可以同时读取多个数据通道。可通过字符设备文件/dev/iio:deviceX从用户空间访问缓冲区。

i). IIO缓冲区的sysfs接口

        IIO缓冲区在目录/sys/bus/iio/devices/iio:deviceX/buffer/下有一些属性:

length:缓冲区可存储的数据取样总量。这是缓冲区包含的扫描数量。

enable:激活缓冲区捕获,启动缓冲区捕获。

watermark:这是一个正数,指定阻塞读取应该等待的扫描元素数量。

ii). iio缓冲区设置

        被读取并推入缓冲区的数据称为扫描元素。扫描元素的配置可以通过/sys/bus/iio/devices/iio:deviceX/scan_elements/目录访问。其中包括如下属性:

  • en:实际上是属性名称的后缀,用于启动通道。仅当其值不为0时,触发的捕捉将包含此通道的采样。每个通道对应一个文件,例如:in_voltage0_en、in_volatge1_en等
  • type:描述扫描元素数据在缓冲区内的存储,因此描述从用户空间读取到它的形式。文件名示例为in_voltage0_type等。其值的格式是[be|le]:[s|u]bits/storage_bitsXrepeat[>>shift],其中[s|u]指出是无符号还是有符号数,其中bits是指值的位数,其中storage_bits是指存储的位数,其中shift是指取值之前移动的位数(一般是storage_bits - bits),其中repeat是指位重复存储数量(重复数位0或1时,重复值省略)。
type值示例:
$ cat /sys/bus/iio/devices/iio:device0/scan_elements/in_accel_y_type
le:s12/16>>4

这解释为小端有符号数,16位长度,在屏蔽12位有效数之前需要右移4位。
在struct iio_chan_spec{}中负责表示缓冲区元素格式的是scan_type{}
struct iio_chan_spec {
    [...];
    struct {
        char sign;
        u8 realbits;
        u8 storagebits;
        u8 shift;
        u8 repeat;
        enum iio_endian endianness;
    } scan_type;
    [...];
};

// 下面是scan_type的示例:
static struct iio_chan_spec accel_channels[] = {
{
    .type = IIO_ACCEL,
    .modified = 1,
    .channel2 = IIO_MOD_X,
    .scan_index = 0,
    .scan_type = {
        .sign = 's',
        .realbits = 12,
        .storagebits = 16,
        .shift = 4,
        .endianness = IIO_LE,
    },
},
/* 类似于X轴,Y轴和Z轴都是scan_index分别是1和2
*/
};

   

三、IIO数据访问

1、单次捕获

        单次数据捕获通过sysfs接口完成,在iio内核中后端会调用struct iio_info{}.read_raw()和write_raw()回调函数。例如对于具有两个通道的温度传感器:一个用于测量环境温度,一个用于测量热电偶温度:

# cd /sys/bus/iio/devices/iio:device0/
# cat in_voltage3_raw
6646
# cat in_voltage_scale
0.305175781

// 将刻度乘以原始值即获得处理后的值

2. 缓冲区数据访问

        要使触发采集正常工作,首先在驱动程序中实现触发器的支持。然后要从用户空间sysfs目录创建触发器并进行分配,启用ADC通道,设置缓冲区的大小并启用它。

// 1). 创建触发器
echo 0 > /sys/devices/sysfs_iio_trigger/add_trigger
// 得到触发器,对应木/sys/bus/iio/devices/trigger0/,名称为systrig0

// 2). 绑定iio设备和触发器systrig0
echo systrig0 > /sys/bus/iio/devices/iio:device0/trigger/current_trigger

// 3). 启动一些扫描元素
echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_voltage4_en
echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_voltage5_en
echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_voltage6_en
echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_voltage7_en

// 4). 设置缓冲区大小,这里是指缓冲区中record的数量
echo 100 > /sys/bus/iio/devices/iio:device0/buffer/length

// 5). 启用缓冲区
echo 1 > /sys/bus/iio/devices/iio:device0/buffer/enable

// 6). 启动触发器
echo 1 > /sys/bus/iio/devices/trigger0/trigger_now

// 7). 查看缓冲区中的数据
cat "/dev/iio:device0" | xxd -

// 停止缓冲区和分离触发器
echo 1 > /sys/bus/iio/devices/iio:device0/buffer/enable
echo "" > /sys/bus/iio/devices/iio:device0/trigger/current_trigger

四、IIO工具

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值