2.4 替换lora_gateway 官方库底层spi驱动

本节提示
         所需环境:ubuntu 16 、 18
             工具:VScode、openwrt交叉编译工具
           源码包:https://github.com/Lora-net/lora_gateway 
                  https://github.com/TheThingsNetwork/packet_forwarder

2.4 替换LoRaWan 官方库底层spi驱动
在进入本章之前建议先自行学习这位博文:https://blog.csdn.net/iotisan/article/details/72633960
2.4.1 认识源码
通过源码分析和其他博主分析:我们这次的主要对:/lora_gateway/libloragw/src/loragw_spi.native.c 进行修改

#include <stdint.h>        /* C99 types */
#include <stdio.h>        /* printf fprintf */
#include <stdlib.h>        /* malloc free */
#include <unistd.h>        /* lseek, close */
#include <fcntl.h>        /* open */
#include <string.h>        /* memset */

#include <sys/ioctl.h>
#include <linux/spi/spidev.h>

#include "loragw_spi.h"
#include "loragw_hal.h"

/* -------------------------------------------------------------------------- */
/* --- PRIVATE MACROS ------------------------------------------------------- */

#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
#if DEBUG_SPI == 1
    #define DEBUG_MSG(str)                fprintf(stderr, str)
    #define DEBUG_PRINTF(fmt, args...)    fprintf(stderr,"%s:%d: "fmt, __FUNCTION__, __LINE__, args)
    #define CHECK_NULL(a)                if(a==NULL){fprintf(stderr,"%s:%d: ERROR: NULL POINTER AS ARGUMENT\n", __FUNCTION__, __LINE__);return LGW_SPI_ERROR;}
#else
    #define DEBUG_MSG(str)
    #define DEBUG_PRINTF(fmt, args...)
    #define CHECK_NULL(a)                if(a==NULL){return LGW_SPI_ERROR;}
#endif

/* -------------------------------------------------------------------------- */
/* --- PRIVATE CONSTANTS ---------------------------------------------------- */

#define READ_ACCESS     0x00
#define WRITE_ACCESS    0x80
#define SPI_SPEED       8000000
#define SPI_DEV_PATH    "/dev/spidev0.0"
//#define SPI_DEV_PATH    "/dev/spidev32766.0"

/* -------------------------------------------------------------------------- */
/* --- PUBLIC FUNCTIONS DEFINITION ------------------------------------------ */

/* SPI initialization and configuration */
int lgw_spi_open(void **spi_target_ptr) {
    int *spi_device = NULL;
    int dev;
    int a=0, b=0;
    int i;

    /* check input variables */
    CHECK_NULL(spi_target_ptr); /* cannot be null, must point on a void pointer (*spi_target_ptr can be null) */

    /* allocate memory for the device descriptor */
    spi_device = malloc(sizeof(int));
    if (spi_device == NULL) {
        DEBUG_MSG("ERROR: MALLOC FAIL\n");
        return LGW_SPI_ERROR;
    }

    /* open SPI device */
    dev = open(SPI_DEV_PATH, O_RDWR);
    if (dev < 0) {
        DEBUG_PRINTF("ERROR: failed to open SPI device %s\n", SPI_DEV_PATH);
        return LGW_SPI_ERROR;
    }

    /* setting SPI mode to 'mode 0' */
    i = SPI_MODE_0;
    a = ioctl(dev, SPI_IOC_WR_MODE, &i);
    b = ioctl(dev, SPI_IOC_RD_MODE, &i);
    if ((a < 0) || (b < 0)) {
        DEBUG_MSG("ERROR: SPI PORT FAIL TO SET IN MODE 0\n");
        close(dev);
        free(spi_device);
        return LGW_SPI_ERROR;
    }

    /* setting SPI max clk (in Hz) */
    i = SPI_SPEED;
    a = ioctl(dev, SPI_IOC_WR_MAX_SPEED_HZ, &i);
    b = ioctl(dev, SPI_IOC_RD_MAX_SPEED_HZ, &i);
    if ((a < 0) || (b < 0)) {
        DEBUG_MSG("ERROR: SPI PORT FAIL TO SET MAX SPEED\n");
        close(dev);
        free(spi_device);
        return LGW_SPI_ERROR;
    }

    /* setting SPI to MSB first */
    i = 0;
    a = ioctl(dev, SPI_IOC_WR_LSB_FIRST, &i);
    b = ioctl(dev, SPI_IOC_RD_LSB_FIRST, &i);
    if ((a < 0) || (b < 0)) {
        DEBUG_MSG("ERROR: SPI PORT FAIL TO SET MSB FIRST\n");
        close(dev);
        free(spi_device);
        return LGW_SPI_ERROR;
    }

    /* setting SPI to 8 bits per word */
    i = 0;
    a = ioctl(dev, SPI_IOC_WR_BITS_PER_WORD, &i);
    b = ioctl(dev, SPI_IOC_RD_BITS_PER_WORD, &i);
    if ((a < 0) || (b < 0)) {
        DEBUG_MSG("ERROR: SPI PORT FAIL TO SET 8 BITS-PER-WORD\n");
        close(dev);
        return LGW_SPI_ERROR;
    }

    *spi_device = dev;
    *spi_target_ptr = (void *)spi_device;
    DEBUG_MSG("Note: SPI port opened and configured ok\n");
    return LGW_SPI_SUCCESS;
}

/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

/* SPI release */
int lgw_spi_close(void *spi_target) {
    int spi_device;
    int a;

    /* check input variables */
    CHECK_NULL(spi_target);

    /* close file & deallocate file descriptor */
    spi_device = *(int *)spi_target; /* must check that spi_target is not null beforehand */
    a = close(spi_device);
    free(spi_target);

    /* determine return code */
    if (a < 0) {
        DEBUG_MSG("ERROR: SPI PORT FAILED TO CLOSE\n");
        return LGW_SPI_ERROR;
    } else {
        DEBUG_MSG("Note: SPI port closed\n");
        return LGW_SPI_SUCCESS;
    }
}

/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

/* Simple write */
int lgw_spi_w(void *spi_target, uint8_t spi_mux_mode, uint8_t spi_mux_target, uint8_t address, uint8_t data) {
    int spi_device;
    uint8_t out_buf[3];
    uint8_t command_size;
    struct spi_ioc_transfer k;
    int a;

    /* check input variables */
    CHECK_NULL(spi_target);
    if ((address & 0x80) != 0) {
        DEBUG_MSG("WARNING: SPI address > 127\n");
    }

    spi_device = *(int *)spi_target; /* must check that spi_target is not null beforehand */

    /* prepare frame to be sent */
    if (spi_mux_mode == LGW_SPI_MUX_MODE1) {
        out_buf[0] = spi_mux_target;
        out_buf[1] = WRITE_ACCESS | (address & 0x7F);
        out_buf[2] = data;
        command_size = 3;
    } else {
        out_buf[0] = WRITE_ACCESS | (address & 0x7F);
        out_buf[1] = data;
        command_size = 2;
    }

    /* I/O transaction */
    memset(&k, 0, sizeof(k)); /* clear k */
    k.tx_buf = (unsigned long) out_buf;
    k.len = command_size;
    k.speed_hz = SPI_SPEED;
    k.cs_change = 0;
    k.bits_per_word = 8;
    a = ioctl(spi_device, SPI_IOC_MESSAGE(1), &k);

    /* determine return code */
    if (a != (int)k.len) {
        DEBUG_MSG("ERROR: SPI WRITE FAILURE\n");
        return LGW_SPI_ERROR;
    } else {
        DEBUG_MSG("Note: SPI write success\n");
        return LGW_SPI_SUCCESS;
    }
}

/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

/* Simple read */
int lgw_spi_r(void *spi_target, uint8_t spi_mux_mode, uint8_t spi_mux_target, uint8_t address, uint8_t *data) {
    int spi_device;
    uint8_t out_buf[3];
    uint8_t command_size;
    uint8_t in_buf[ARRAY_SIZE(out_buf)];
    struct spi_ioc_transfer k;
    int a;

    /* check input variables */
    CHECK_NULL(spi_target);
    if ((address & 0x80) != 0) {
        DEBUG_MSG("WARNING: SPI address > 127\n");
    }
    CHECK_NULL(data);

    spi_device = *(int *)spi_target; /* must check that spi_target is not null beforehand */

    /* prepare frame to be sent */
    if (spi_mux_mode == LGW_SPI_MUX_MODE1) {
        out_buf[0] = spi_mux_target;
        out_buf[1] = READ_ACCESS | (address & 0x7F);
        out_buf[2] = 0x00;
        command_size = 3;
    } else {
        out_buf[0] = READ_ACCESS | (address & 0x7F);
        out_buf[1] = 0x00;
        command_size = 2;
    }

    /* I/O transaction */
    memset(&k, 0, sizeof(k)); /* clear k */
    k.tx_buf = (unsigned long) out_buf;
    k.rx_buf = (unsigned long) in_buf;
    k.len = command_size;
    k.cs_change = 0;
    a = ioctl(spi_device, SPI_IOC_MESSAGE(1), &k);

    /* determine return code */
    if (a != (int)k.len) {
        DEBUG_MSG("ERROR: SPI READ FAILURE\n");
        return LGW_SPI_ERROR;
    } else {
        DEBUG_MSG("Note: SPI read success\n");
        *data = in_buf[command_size - 1];
        return LGW_SPI_SUCCESS;
    }
}

/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

/* Burst (multiple-byte) write */
int lgw_spi_wb(void *spi_target, uint8_t spi_mux_mode, uint8_t spi_mux_target, uint8_t address, uint8_t *data, uint16_t size) {
    int spi_device;
    uint8_t command[2];
    uint8_t command_size;
    struct spi_ioc_transfer k[2];
    int size_to_do, chunk_size, offset;
    int byte_transfered = 0;
    int i;

    /* check input parameters */
    CHECK_NULL(spi_target);
    if ((address & 0x80) != 0) {
        DEBUG_MSG("WARNING: SPI address > 127\n");
    }
    CHECK_NULL(data);
    if (size == 0) {
        DEBUG_MSG("ERROR: BURST OF NULL LENGTH\n");
        return LGW_SPI_ERROR;
    }

    spi_device = *(int *)spi_target; /* must check that spi_target is not null beforehand */

    /* prepare command byte */
    if (spi_mux_mode == LGW_SPI_MUX_MODE1) {
        command[0] = spi_mux_target;
        command[1] = WRITE_ACCESS | (address & 0x7F);
        command_size = 2;
    } else {
        command[0] = WRITE_ACCESS | (address & 0x7F);
        command_size = 1;
    }
    size_to_do = size;

    /* I/O transaction */
    memset(&k, 0, sizeof(k)); /* clear k */
    k[0].tx_buf = (unsigned long) &command[0];
    k[0].len = command_size;
    k[0].cs_change = 0;
    k[1].cs_change = 0;
    for (i=0; size_to_do > 0; ++i) {
        chunk_size = (size_to_do < LGW_BURST_CHUNK) ? size_to_do : LGW_BURST_CHUNK;
        offset = i * LGW_BURST_CHUNK;
        k[1].tx_buf = (unsigned long)(data + offset);
        k[1].len = chunk_size;
        byte_transfered += (ioctl(spi_device, SPI_IOC_MESSAGE(2), &k) - k[0].len );
        DEBUG_PRINTF("BURST WRITE: to trans %d # chunk %d # transferred %d \n", size_to_do, chunk_size, byte_transfered);
        size_to_do -= chunk_size; /* subtract the quantity of data already transferred */
    }

    /* determine return code */
    if (byte_transfered != size) {
        DEBUG_MSG("ERROR: SPI BURST WRITE FAILURE\n");
        return LGW_SPI_ERROR;
    } else {
        DEBUG_MSG("Note: SPI burst write success\n");
        return LGW_SPI_SUCCESS;
    }
}

/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

/* Burst (multiple-byte) read */
int lgw_spi_rb(void *spi_target, uint8_t spi_mux_mode, uint8_t spi_mux_target, uint8_t address, uint8_t *data, uint16_t size) {
    int spi_device;
    uint8_t command[2];
    uint8_t command_size;
    struct spi_ioc_transfer k[2];
    int size_to_do, chunk_size, offset;
    int byte_transfered = 0;
    int i;

    /* check input parameters */
    CHECK_NULL(spi_target);
    if ((address & 0x80) != 0) {
        DEBUG_MSG("WARNING: SPI address > 127\n");
    }
    CHECK_NULL(data);
    if (size == 0) {
        DEBUG_MSG("ERROR: BURST OF NULL LENGTH\n");
        return LGW_SPI_ERROR;
    }

    spi_device = *(int *)spi_target; /* must check that spi_target is not null beforehand */

    /* prepare command byte */
    if (spi_mux_mode == LGW_SPI_MUX_MODE1) {
        command[0] = spi_mux_target;
        command[1] = READ_ACCESS | (address & 0x7F);
        command_size = 2;
    } else {
        command[0] = READ_ACCESS | (address & 0x7F);
        command_size = 1;
    }
    size_to_do = size;

    /* I/O transaction */
    memset(&k, 0, sizeof(k)); /* clear k */
    k[0].tx_buf = (unsigned long) &command[0];
    k[0].len = command_size;
    k[0].cs_change = 0;
    k[1].cs_change = 0;
    for (i=0; size_to_do > 0; ++i) {
        chunk_size = (size_to_do < LGW_BURST_CHUNK) ? size_to_do : LGW_BURST_CHUNK;
        offset = i * LGW_BURST_CHUNK;
        k[1].rx_buf = (unsigned long)(data + offset);
        k[1].len = chunk_size;
        byte_transfered += (ioctl(spi_device, SPI_IOC_MESSAGE(2), &k) - k[0].len );
        DEBUG_PRINTF("BURST READ: to trans %d # chunk %d # transferred %d \n", size_to_do, chunk_size, byte_transfered);
        size_to_do -= chunk_size;  /* subtract the quantity of data already transferred */
    }

    /* determine return code */
    if (byte_transfered != size) {
        DEBUG_MSG("ERROR: SPI BURST READ FAILURE\n");
        return LGW_SPI_ERROR;
    } else {
        DEBUG_MSG("Note: SPI burst read success\n");
        return LGW_SPI_SUCCESS;
    }
}

2.4.2 找出我们需要修改的函数
熟悉linux的小伙伴,从以上源码不难看出,调用硬件spi,所需要的函数为:

int lgw_spi_open(void **spi_target_ptr) 
int lgw_spi_close(void *spi_target)
int lgw_spi_w(void *spi_target, uint8_t spi_mux_mode, uint8_t spi_mux_target, uint8_t address, uint8_t data)
int lgw_spi_r(void *spi_target, uint8_t spi_mux_mode, uint8_t spi_mux_target, uint8_t address, uint8_t *data) 
int lgw_spi_wb(void *spi_target, uint8_t spi_mux_mode, uint8_t spi_mux_target, uint8_t address, uint8_t *data, uint16_t size)
int lgw_spi_rb(void *spi_target, uint8_t spi_mux_mode, uint8_t spi_mux_target, uint8_t address, uint8_t *data, uint16_t size)

**2.4.3修改int lgw_spi_open(void spi_target_ptr)
这个函数主要作用是:打开和设置硬件SPI 并且返回设备描述对象。在上一环节中,我们自写了SPI驱动并且已经设置好了我们所需的通信模式,因此只需将模拟SPI初始化函数写入即可。同时需要注意*spi_target_ptr = (void *)spi_device
在lora_reg.c有对spi_target_ptr的调用,主要作用是描述SPI设备。函数内容如下:

/* Concentrator connect */
int lgw_connect(bool spi_only, uint32_t tx_notch_freq) {
    int spi_stat = LGW_SPI_SUCCESS;
    uint8_t u = 0;
    int x;

    /* check SPI link status */
    if (lgw_spi_target != NULL) {
        DEBUG_MSG("WARNING: concentrator was already connected\n");
        lgw_spi_close(lgw_spi_target);
    }

    /* open the SPI link */
    spi_stat = lgw_spi_open(&lgw_spi_target);
    if (spi_stat != LGW_SPI_SUCCESS) {
        DEBUG_MSG("ERROR CONNECTING CONCENTRATOR\n");
        return LGW_REG_ERROR;
    }

    if (spi_only == false ) {
        /* Detect if the gateway has an FPGA with SPI mux header support */
        /* First, we assume there is an FPGA, and try to read its version */
        spi_stat = lgw_spi_r(lgw_spi_target, LGW_SPI_MUX_MODE1, LGW_SPI_MUX_TARGET_FPGA, loregs[LGW_VERSION].addr, &u);
        if (spi_stat != LGW_SPI_SUCCESS) {
            DEBUG_MSG("ERROR READING VERSION REGISTER\n");
            return LGW_REG_ERROR;
        }
        if (check_fpga_version(u) != true) {
            /* We failed to read expected FPGA version, so let's assume there is no FPGA */
            DEBUG_PRINTF("INFO: no FPGA detected or version not supported (v%u)\n", u);
            lgw_spi_mux_mode = LGW_SPI_MUX_MODE0;
        } else {
            DEBUG_PRINTF("INFO: detected FPGA with SPI mux header (v%u)\n", u);
            lgw_spi_mux_mode = LGW_SPI_MUX_MODE1;
            /* FPGA Soft Reset */
            lgw_spi_w(lgw_spi_target, lgw_spi_mux_mode, LGW_SPI_MUX_TARGET_FPGA, 0, 1);
            lgw_spi_w(lgw_spi_target, lgw_spi_mux_mode, LGW_SPI_MUX_TARGET_FPGA, 0, 0);
            /* FPGA configure */
            x = lgw_fpga_configure(tx_notch_freq);
            if (x != LGW_REG_SUCCESS) {
                DEBUG_MSG("ERROR CONFIGURING FPGA\n");
                return LGW_REG_ERROR;
            }
        }

在int lgw_connect(bool spi_only, uint32_t tx_notch_freq) 函数中对spi_target_ptr进行了检空操作,然后作为SPI 设备描述符传给了lgw_spi_w
。到此得出结论,同时保证不破坏上级函数的调用因此对*spi_target_ptr赋以非空值代码如下:

int lgw_spi_open(void **spi_target_ptr) {
*spi_target_ptr=“hello”;
spi_simulate_io_init();/模拟SPI初始化函数
return 0;
}

2.4.4 int lgw_spi_close(void spi_target)
SPI 关闭函数,在先前驱动程序中有自动关闭函数,因此在这里已经没有实质的功能,只需返回一个成功标志即可。

int lgw_spi_close(void *spi_target) {
    int spi_device;
    int a;
    /* check input variables */
    CHECK_NULL(spi_target);

    /* close file & deallocate file descriptor */
    spi_device = *(int *)spi_target; /* must check that spi_target is not null beforehand */
    a = close(spi_device);
    free(spi_target);
    /* determine return code */
    if (a < 0) {
        DEBUG_MSG("ERROR: SPI PORT FAILED TO CLOSE\n");
        return LGW_SPI_ERROR;
    } else {
        DEBUG_MSG("Note: SPI port closed\n");
        return LGW_SPI_SUCCESS;
    }
}

修改代码如下:

int lgw_spi_close(void *spi_target) {
        return LGW_SPI_SUCCESS;
}

(3)修改:

int lgw_spi_w(void *spi_target, uint8_t spi_mux_mode, uint8_t spi_mux_target, uint8_t address, uint8_t data)
int lgw_spi_r(void *spi_target, uint8_t spi_mux_mode, uint8_t spi_mux_target, uint8_t address, uint8_t *data) 
int lgw_spi_wb(void *spi_target, uint8_t spi_mux_mode, uint8_t spi_mux_target, uint8_t address, uint8_t *data, uint16_t size)
int lgw_spi_rb(void *spi_target, uint8_t spi_mux_mode, uint8_t spi_mux_target, uint8_t address, uint8_t *data, uint16_t size)

对于这几个函数的修改,只需要替换为前一章的读写函数即可,这里不做过多的解说。
2.4.5 lora_gateway功能测试
(1)添加交叉编译工具:
打开lora_gateway主目录Makefile

ARCH ?=
CROSS_COMPILE ?=
export
### general build targets
all:
	$(MAKE) all -e -C libloragw
	$(MAKE) all -e -C util_pkt_logger
	$(MAKE) all -e -C util_spi_stress
	$(MAKE) all -e -C util_tx_test
	$(MAKE) all -e -C util_lbt_test
	$(MAKE) all -e -C util_tx_continuous
	$(MAKE) all -e -C util_spectral_scan
clean:
	$(MAKE) clean -e -C libloragw
	$(MAKE) clean -e -C util_pkt_logger
	$(MAKE) clean -e -C util_spi_stress
	$(MAKE) clean -e -C util_tx_test
	$(MAKE) clean -e -C util_lbt_test
	$(MAKE) clean -e -C util_tx_continuous
	$(MAKE) clean -e -C util_spectral_scan
### EOF

修改ARCH CROSS_COMPILE

ARCH =mpis
CROSS_COMPILE =你的openwrt交叉编译工具存放路径

(2)编译
打开终端进入lora_gateway目录输入:

make 

(3)测试
将步骤二无报错编译后的可执行文件整体打包至,MT7628中或者只复制loragw_reg可执行文件。

说明:上层设置寄存器多调用loragw_reg.c中的函数,所以只要loragw_reg验证通过,整个程序就可以完美运行

测试结果如下:
在这里插入图片描述

2.5.5 整装测试

在这个环节我们需要源码:https://github.com/TheThingsNetwork/packet_forwarder 输入命令:

git clone https://github.com/TheThingsNetwork/packet_forwarder

在测试环节我们不要做过多修改,如果使用的到具体项目时,也比较容易修改,设计的代码量不是很大。接下来编译这个网关数据转发器。
同样进入主目录Makefile 增加添加编译工具,可参考本页:2.4.5 lora_gateway功能测试 设置。然后终端进入目录执行:make即可。

注意:在编译过程中可能会报qsort_r()未定义;如果只是测试,可先将其屏蔽。

将编译以后的可执行文件整体复制或者单独复制lora_pkt_fwd可执行文件到MT7628执行:

chmod 777 lora_pkt_fwd
./lora_pkt_fwd

使用终端发送数据得到结果(由于实验的时候openwrt系统没有进行时间校准,所以显示的时间不正确):
在这里插入图片描述
至此sx1301的开源程序已经成功移植到mt7628 openwrt平台。后续将对离散的开源程序进行整合,特别是网关转发程序,很多内容对一般场景并不适用。

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
lora_pkt_fwd是一个LoRaWAN数据包转发程序,用于将LoRaWAN设备(如传感器或节点)的数据包转发到LoRa网关,并进一步传输到网络服务器。它处理物理层和数据链路层的通信,实现了数据包的接收和发送功能。 lora_pkt_fwd的主要功能包括: 1. 接收功能:lora_pkt_fwd负责接收来自LoRaWAN设备的数据包。它监听指定的频率和数据速率,等待设备发送数据。一旦接收到数据包,它将对其进行解码,并提取出有效负载数据。 2. 发送功能:lora_pkt_fwd可将解码后的数据包转发到已配置的LoRa网关。它通过与网关建立连接发送数据包,然后等待网关将数据包发送到网络服务器。通过此过程,它实现了从设备到网络服务器的数据传输。 3. 协议支持:lora_pkt_fwd支持多种LoRaWAN协议版本,如LoRaWAN 1.0.2和LoRaWAN 1.1。它能够根据所需的协议配置自身的行为,以便与所连接的设备和网络兼容。 4. 配置管理:lora_pkt_fwd提供了可编辑的配置文件,可以根据需要自定义参数。用户可以配置通信频率、数据速率、网络服务器地址和端口等信息。这使得它可以适应不同的LoRa网关和网络环境。 总而言之,lora_pkt_fwd是一个功能强大的LoRaWAN数据包转发程序,能够接收、解码和发送数据包,从设备到网络服务器进行可靠的数据传输。它提供了灵活的配置选项,以适应各种LoRaWAN协议和网络设置。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值