BLE-NRF51822教程4-串口BLE解析

本讲逐行代码解析官方串口BLE例程demo

PS: 基于SDK5.1

 

主要分一下几个部分:

1 :Main函数的整体注释

2 :函数单独解析。

3 :接收串口数据并发送给对端设备

4 :接收手机数据并通过串口打印

 

Ps :第一和第二部分我在教程工程初始化流程中已经详细说明这里直接复制过来,做了一些修改以及添加了关于添加服务和添加特征值的讲解,如果之前看过可以直接看下 2函数单独解析中的 服务初始化后面添加的内容即可

一:main函数整体注释:

int main(void)

{

//初始化LED指示灯,用来指示广播和连接状态

leds_init();

//初始化软件定时器模块

timers_init();

//设置按键作为 DETECT signal 用来唤醒system off模式,具体参看数据手册power 章节

buttons_init();

//主要设置uart的引脚,波特率。接收,发送中断等。并开启uart模块中断

uart_init();

//协议栈初试化,设置时钟,demo里面设置为外部时钟。并且注册事件派发函数

    ble_stack_init();

//GAP一些参数的设置,设置设备名,设置PPCP(外围设备首选链接参数)。(手机连上某个蓝牙设备后可以从Generic Access Service中看到设置的这些参数)

    gap_params_init();

//服务初始化。添加uart的串口服务。主要提供两个特征值来供手机和板子以及电脑的通信

    services_init();

//设置广播数据以及扫描响应数据

    advertising_init();

//链接参数设置。主要设置什么时候发起更新链接参数请求以及间隔和最大尝试次数。

    conn_params_init();

//安全参数初始化。

    sec_params_init();

 

    simple_uart_putstring(START_STRING);

//设置广播类型,白名单,间隔,超时等特性。并开始广播。

    advertising_start();

 

 

for (;;)

{

//电源管理,调用arm0的指令__WFE();进入睡眠

power_manage();

    }

}

二:函数单独解析:

1 leds_init

static void leds_init(void)

{

    nrf_gpio_cfg_output(ADVERTISING_LED_PIN_NO);

    nrf_gpio_cfg_output(CONNECTED_LED_PIN_NO);

}

设置的PIN_CONFIG寄存器使能两个引脚的作为输出功能。用来当做指示灯指示广播和链接的状态。

2 timers_init

static void timers_init(void)

{

    // Initialize timer module

    APP_TIMER_INIT(APP_TIMER_PRESCALER, APP_TIMER_MAX_TIMERS,                                         APP_TIMER_OP_QUEUE_SIZE, false);

}

初始化软件定时器模块,该定时器模块并不是使用timer0-2来实现定时功能。而是使用51822中的RTC1 来软件模拟出定时器模块。RTC1使用32.768K时钟经过分频后是时钟来作为时钟源。所以该函数内部实现就是设置RTC1相关的寄存器和做一些初始化。其原理和timer 定时/计数器模块类似。具体细节参考芯片数据手册。

 

APP_TIMER_PRESCALER:设置分频系数。(以32.768K来分频)

APP_TIMER_MAX_TIMERS:设置可以创建的最大定时器个数

APP_TIMER_OP_QUEUE_SIZE:定时器操作队列,因为是用RTC模拟的软件定时器,因此内部                                                                是维护了一个软件定时器的操作队列

False:不使用调度,调度模块没有细看。51822关于调度的很多都是传False不使用调                      度。                  

3buttons_init

static void buttons_init(void)

{

nrf_gpio_cfg_sense_input(WAKEUP_BUTTON_PIN,

                             BUTTON_PULL,

                             NRF_GPIO_PIN_SENSE_LOW);   

}

这里的按键设置比较简单,主要通过PIN_CNF寄存器来设置一个IO口来作为来作为sensing mechanism机制的引脚。这里是设置了WAKEUP_BUTTON_PIN这个引脚来作为这个功能,设置成低电平时触发这个机制。而这个机制类似一个wakeup机制,当其被触发时会产生一个DETECT signal而这个信号会将cpu从system off模式中唤醒。

 

4  uart_init

static void uart_init(void)

{

simple_uart_config(RTS_PIN_NUMBER, TX_PIN_NUMBER, CTS_PIN_NUMBER, RX_PIN_NUMBER, HWFC);

 

    NRF_UART0->INTENSET = UART_INTENSET_RXDRDY_Enabled<<uart_intenset_rxdrdy_pos; </uart_intenset_rxdrdy_pos;<>

 

NVIC_SetPriority(UART0_IRQn, APP_IRQ_PRIORITY_LOW);

NVIC_EnableIRQ(UART0_IRQn);

    /**@snippet [UART Initialization] */

}

初始化uart设置输入输出引脚,是否关闭流控。一般使用官方例子的时候都要先将流控关掉,HWFC为False。然后打开uart的接收中断,打开uart模块的中断功能,以及设置优先级。         波特率在simple_uart_config中设置,该函数设置完引脚后使能uart,开启uart的接收和发送功能。

 

5   ble_stack_init

static void ble_stack_init(void)

{

    // Initialize SoftDevice.

    SOFTDEVICE_HANDLER_INIT(NRF_CLOCK_LFCLKSRC_XTAL_20_PPM, false);

 

    // Subscribe for BLE events.

    uint32_t err_code = softdevice_ble_evt_handler_set(ble_evt_dispatch);

    APP_ERROR_CHECK(err_code);

}

设置LFCLK(32.768K)的时钟源(协议栈需要使用),这里设置为外部晶振。False为不使用调度。softdevice_ble_evt_handler_set(ble_evt_dispatch);注册事件派发程序,基础1-协议栈概述说明过,当BLE收到广播,链接请求,对端设备数据等后底层处理完会上抛给上册app一个事件,这个事件的上抛过程是协议栈触发SWI中断,在中断内部将事件放入队列,然后调用app中的SWI中断。App中的SWI中断会get队列中的事件,并最终会调用注册的ble_evt_dispatch函数,这个函数再将事件发给各个服务以及模块的事件处理函数来处理各个服务及模块自己感兴趣的事件。相关原理基础1-协议栈概述视频教程中有说明。

 

6gap_params_init

设置必要的设备的GAP参数。

static void gap_params_init(void)

{

    uint32_t                err_code;

    ble_gap_conn_params_tgap_conn_params;

    ble_gap_conn_sec_mode_tsec_mode;

 

//设置设备名的写权限为普通模式,则手机扫描到设备连接上后可以在第一个服务Geneic Access Service(有的只显示UUID为1800)中改写Device name.(有的app可能本身未实现改写功能)

     BLE_GAP_CONN_SEC_MODE_SET_OPEN(&sec_mode);

//设置设备名,该设备名就是在手机app扫描蓝牙设备时显示的名字。

    err_code = sd_ble_gap_device_name_set(&sec_mode,(const uint8_t *) DEVICE_NAME,strlen(DEVICE_NAME));

    APP_ERROR_CHECK(err_code);

 

    memset(&gap_conn_params, 0, sizeof(gap_conn_params));

//设置外围设备连接首选参数。同device name一样,手机连上某个蓝牙设备后可以从Generic Access Service中看到设置的这些参数。这个参数主要是让中央设备在首次连接外设时可以读取他们以及时调整连接参数。或者当中央设备以后重连该外设,并且之前保留了这些参数那么就免去了连接后可能需要的修改连接参数的麻烦。

//当然,外围设备也可以之后通过sd_ble_gap_ppcp_get来获取之前设置的参数然后通过连接参数跟新请求函数向中央设备请求更改连接参数。

    gap_conn_params.min_conn_interval = MIN_CONN_INTERVAL;

    gap_conn_params.max_conn_interval = MAX_CONN_INTERVAL;

    gap_conn_params.slave_latency     = SLAVE_LATENCY;

    gap_conn_params.conn_sup_timeout  = CONN_SUP_TIMEOUT;

 

    err_code = sd_ble_gap_ppcp_set(&gap_conn_params);

    APP_ERROR_CHECK(err_code);

}

 

 

 

7 services_init

static void services_init(void)

{

    uint32_t         err_code;

ble_nus_init_tnus_init;

 

memset(&nus_init, 0, sizeof(nus_init));

        

//注册数据处理函数,这里处理的数据是收到手机发来的数据

// nus_data_handler就是将板子收到的数据通过串口打印到电脑上

//实现了手机->开发板->电脑方向的数据流传输。

nus_init.data_handler = nus_data_handler;

 

err_code =ble_nus_init(&m_nus, &nus_init);

    APP_ERROR_CHECK(err_code);

}

7.1 ble_nus_init该函数中实现添加服务以及添加特征值

uint32_t ble_nus_init(ble_nus_t * p_nus, constble_nus_init_t * p_nus_init)

{

uint32_t        err_code;

ble_uuid_tble_uuid;

//设置基准uuid

ble_uuid128_t   nus_base_uuid = {0x9E, 0xCA, 0xDC, 0x24, 0x0E, 0xE5, 0xA9,                                                    0xE0,0x93, 0xF3, 0xA3, 0xB5, 0x00, 0x00, 0x40, 0x6E};

 

if ((p_nus == NULL) || (p_nus_init == NULL))

    {

return NRF_ERROR_NULL;

    }

 

 // 初始化连接句柄,因为现在并未与手机连接所以先赋值无效。

   //赋值数据处理函数,就是上面刚提到的打印收到的手机数据

   //设置notify是否使能的标志量,该标志量在手机连上板子并且使能了具                               //有notfify的特征值时(这里是rx特征值后面会讲到),该标志会被设                                    //    置。这个标志量仅仅只是一个类似flag的作用,甚至可能并未被

            // 用到。

p_nus->conn_handle              = BLE_CONN_HANDLE_INVALID;

p_nus->data_handler             = p_nus_init->data_handler;

p_nus->is_notification_enabled      = false;

 

    // 因为是自己定义的uuid,所以需要调用该函数来赋值p_nus->uuid_type

            //该函数会将这个nus_base_uuid放到协议栈内部的表中

err_code = sd_ble_uuid_vs_add(&nus_base_uuid, &p_nus->uuid_type);

if (err_code != NRF_SUCCESS)

    {

returnerr_code;

    }

 

            //设置服务uuid以及uuid_type(就是上面调用的函数或得的)

ble_uuid.type = p_nus->uuid_type;

ble_uuid.uuid = BLE_UUID_NUS_SERVICE;

 

    // 到这里就添加服务到协议栈内部表中了

err_code = sd_ble_gatts_service_add(BLE_GATTS_SRVC_TYPE_PRIMARY,

&ble_uuid,

&p_nus->service_handle);

if (err_code != NRF_SUCCESS)

 {

            returnerr_code;

 }

 

    // 一个服务通常有几个特征值

            //这里在上面注册的服务中添加了两个特征值。

err_code = rx_char_add(p_nus, p_nus_init);

if (err_code != NRF_SUCCESS)

    {

returnerr_code;

    }

 

    // Add TX Characteristic.

err_code = tx_char_add(p_nus, p_nus_init);

if (err_code != NRF_SUCCESS)

    {

returnerr_code;

    }
return NRF_SUCCESS;

}


7.1.1  rx_char_add

这个特征用来将板子从串口收到的数据通过该特征值使用notify方式发送给手机

代码太长截图注释:

PS:后面标记写的有点问题。是设置读写不需要加密或MITM(其实就是设置安全模式和等级)

7.1.2tx_char_add

这个添加的特征值用来接收手机发送给板子的数据。

和Rx 特征值的设置基本一致,只是将notify 功能的设置去掉了改成了设置成可写。其他的代码基本是一样的。这里就补贴代码了。

 

8 advertising_init

广播参数的初始化

static void advertising_init(void)

{

    uint32_t      err_code;

    ble_advdata_tadvdata;
    ble_advdata_tscanrsp;

//该标志主要设置广播类型为有限可发现模式,并且设置不支持经典蓝牙

//相比于一般可发现模式的广播,有限可发现模式的广播平率更快,但是只能最多维持 //30s

    uint8_t       flags = BLE_GAP_ADV_FLAGS_LE_ONLY_LIMITED_DISC_MODE;

 

         //设置需要广播的uuid,就是上面主测的服务uuid

    ble_uuid_tadv_uuids[] = {{BLE_UUID_NUS_SERVICE, m_nus.uuid_type}};

 

 

         //这里设置广播的名字为全名,设置标志,就是上面提到的。

         //appearance为”外观”,他就是一个整形值,代表设备是一个手环,手机什么的。

    memset(&advdata, 0, sizeof(advdata));

    advdata.name_type               = BLE_ADVDATA_FULL_NAME;

    advdata.include_appearance      = false;

    advdata.flags.size              = sizeof(flags);

    advdata.flags.p_data            = &flags;

 

         //这里设置的是扫描响应数据。该数据在设备收到扫描请求的时候才会发出去。

         //有时候需要广播的数据可能太多,广播包中放不下,那么就可以放在扫描响应

         //数据中,这样对端设备便可以通过扫描请求来或得剩下的数据。

    memset(&scanrsp, 0, sizeof(scanrsp));

    scanrsp.uuids_complete.uuid_cnt = sizeof(adv_uuids) / sizeof(adv_uuids[0]);

    scanrsp.uuids_complete.p_uuids  =adv_uuids;

 

    err_code = ble_advdata_set(&advdata, &scanrsp);

    APP_ERROR_CHECK(err_code);

}

 

9 conn_params_init

设置连接参数

static void conn_params_init(void)

{

    uint32_t               err_code;

    ble_conn_params_init_tcp_init;

 

    memset(&cp_init, 0, sizeof(cp_init));

 

         //这里连接参数设置为NULL的原因是前面的gap_params_init函数中已经设置了连接       //参数并调用了sd_ble_gap_ppcp_set将参数设置到了协议栈中。所以这里既是不设置,

         //下面的ble_conn_params_init会自动判断是否为空,为空就调用提取函数,从协议栈

         //中提取之前注册的参数。

cp_init.p_conn_params                  = NULL;

//下面主要是设置一些连接参数更新的事件,以及更新周期和最大最大尝试更新次数。

//部分参数不好描述,视频中会说明。

    cp_init.first_conn_params_update_delay = FIRST_CONN_PARAMS_UPDATE_DELAY;

    cp_init.next_conn_params_update_delay  = NEXT_CONN_PARAMS_UPDATE_DELAY;

    cp_init.max_conn_params_update_count   = MAX_CONN_PARAMS_UPDATE_COUNT;

    cp_init.start_on_notify_cccd_handle    = BLE_GATT_HANDLE_INVALID;

    cp_init.disconnect_on_fail             = false;

    cp_init.evt_handler                    = on_conn_params_evt;

    cp_init.error_handler                  = conn_params_error_handler;

 

    err_code =ble_conn_params_init(&cp_init);

    APP_ERROR_CHECK(err_code);

}

10 sec_params_init

安全参数的初始化。主要设置

超时时间:比如配对过程中某一步的确认超过这个时间还未收到那么便是超时。APP会收到SD上抛的状态事件,状态为超时

Bond: 是否绑定。如果需要绑定,配对过程会有第三步的秘钥分发,然后app将秘钥存储在falsh这样下次就可以避免了下次重复配对的过程。

MITM: 是否需要中间人保护。

Io_caps:本设备的I/O能力。比如有显示屏,有键盘。

 

:当使能了MITM 并且两端设备一个有键盘,一个有显示屏时,配对过程中就会显示一个配对码,对端设备通过键盘再输入。

如果没有MITM保护配对过程中的信息是很容易被监听到的。但是如果有了MITM因为这个配对码信息是一端显示一端输入,并不会通过链路传输。因为除了两端设备不会有第三个设备知道。因此后续的链路加密就很难被破解。

OOB:与MITM类似,只是配对码不是通过键盘输入而是通过两端设备别的通信通道传输,比如NFC,当然前提是该通信链路是安全的。不如也没必要绕个弯而不直接用BLE来传输了。

后面就是设置加密秘钥的最大和最小值。加密秘钥的大小在7-16字节之间

 

配对的过程相对比较复杂,这里不做理论解释。后期需要的话会单独做一片配对的详细教程,群文件中有我上传了一个作为从机的配对历程也是基于uart,当主机在使能有第一个特征值的notify时便会触发配对,配对码是通过串口打印的。使用的随机产生的。当然也可以设置为静态的。

 

void sec_params_init(void)

{

    m_sec_params.timeout      = SEC_PARAM_TIMEOUT;

    m_sec_params.bond         = SEC_PARAM_BOND;

    m_sec_params.mitm         = SEC_PARAM_MITM;

    m_sec_params.io_caps      = SEC_PARAM_IO_CAPABILITIES;

    m_sec_params.oob          = SEC_PARAM_OOB; 

    m_sec_params.min_key_size = SEC_PARAM_MIN_KEY_SIZE;

    m_sec_params.max_key_size = SEC_PARAM_MAX_KEY_SIZE;

}

 

11 advertising_start

static void advertising_start(void)

{

    uint32_t             err_code;

    ble_gap_adv_params_t adv_params;

 

    memset(&adv_params, 0, sizeof(adv_params));

 

         //设置广播类型为通用广播.

        广播类型有四种:

通用广播:用途最广的广播方式。可以被扫描到,以及可以被连接

定向广播:用来快速建立和目标设备建立连接。报文中包含自己以及目标地址。

不可连接广播:只广播数据,不可以被扫描以及连接。

可发现广播;可以被扫描(回复扫描响应数据),不可以被连接。

 

adv_params.type        = BLE_GAP_ADV_TYPE_ADV_IND;

//如果广播方式为定向广播,这里添目标设备的地址

adv_params.p_peer_addr = NULL;         

//设置过滤规则。

//可设置为是否过滤掉非白名单中的扫描请以及非白名单中的连接请求或者两者都过滤。

    adv_params.fp          = BLE_GAP_ADV_FP_ANY;

//设置广播间隔和广播超时,超时时间到期如果设备还未连接那么app会收到协议栈上

//抛的广播超时时间。App可以做自己想做的处理,比如让设备进入睡眠。

adv_params.interval    = APP_ADV_INTERVAL;

    adv_params.timeout     = APP_ADV_TIMEOUT_IN_SECONDS;

//开启广播

    err_code = sd_ble_gap_adv_start(&adv_params);

    APP_ERROR_CHECK(err_code);

 

    nrf_gpio_pin_set(ADVERTISING_LED_PIN_NO);

}

 

三接收串口数据并发送给对端设备

上面介绍的整个初始化完成后,设备便进入睡眠模式,每当广播间隔到期会发送一次广播。直到有设备发来连接请求,当设备连接上手机后边继续处于睡眠状态等待”事件”的发生

 

先来分析电脑à开发板à手机方向的数据流

在main 函数的串口初始化程序uart_init的最后打开了串口的接收中断。

那么这个方向的数据流的起点就是在串口中断中收到电脑上发来的数据为起点

 

Uart中断函数在main函数上方


void UART0_IRQHandler(void)

{

    static uint8_t data_array[BLE_NUS_MAX_DATA_LEN];

    static uint8_t index = 0;

    uint32_t err_code;

         uint8_t temp;

        

         //取得电脑串口发过来的数据

    data_array[index] = simple_uart_get();

    index++;

         //判断串口发送给来的数据是否达到20的字节,或者是不是发送了字母’q’。如果满足

         //调用发送函数将收到的串口数据发送给手机。否则不发送等待知道满足条件。

    // (这里通常新手说手机收不到数据的原因,因为没输入达到20个字节)

    if ((data_array[index - 1] == 'q') || (index >= (BLE_NUS_MAX_DATA_LEN - 1)))

    {

        err_code = ble_nus_send_string(&m_nus, data_array, index + 1);

        if (err_code != NRF_ERROR_INVALID_STATE)

        {

            APP_ERROR_CHECK(err_code);

        }

//发送了数据后清零数组下标。以继续缓存后续的串口数据。

        index = 0;

    }

}

 


再来看看发送数据给手机的函数ble_nus_send_string

 

uint32_t ble_nus_send_string(ble_nus_t * p_nus, uint8_t * string, uint16_t length)

{

    ble_gatts_hvx_params_t hvx_params;

 

    if (p_nus == NULL)

    {

        return NRF_ERROR_NULL;

    }

//这里是检测参数是否正确。是否是已经连接上了手机 (只有连接后,conn_handle才会

   // 被赋值为有效值),检查手机是否使能了开发板的通知,因为开发板作为服务端向手机

         //发送数据时通过通知或指示两种方式,这两种方式都需要手机先使能开发板。

   if((p_nus->conn_handle==BLE_CONN_HANDLE_INVALID)||(!p_nus->is_notification_enabled))

    {

        return NRF_ERROR_INVALID_STATE;

    }

         //一次发送的长度不能超过限定值20

    if (length > BLE_NUS_MAX_DATA_LEN)

    {

        return NRF_ERROR_INVALID_PARAM;

    }

 

    memset(&hvx_params, 0, sizeof(hvx_params));

        

         //以为是通过Rx这个参数来发送数据给手机的,所以句柄要填rx的句柄

         //这个句柄是在上面的服务初始化函数中的添加特征值函数调用完毕后或得的(最后一     // 个参数为返回的句柄)

         //然后就是赋值要发送的数据,并且设置为notify方式

    hvx_params.handle = p_nus->rx_handles.value_handle;

    hvx_params.p_data = string;

    hvx_params.p_len  = &length;

    hvx_params.type   = BLE_GATT_HVX_NOTIFICATION;

//发送函数

    return sd_ble_gatts_hvx(p_nus->conn_handle, &hvx_params);

}

 

 

 

看到这里应该对电脑-》板子-》手机的数据流有一个认识。在讨论另一个方向的数据传输过程。我们先来看一个关于连接的问题。

我们调用sd_ble_gatts_hvx(p_nus->conn_handle, &hvx_params);发送数据给手机的时候,第二个参数是上面赋值的,那第一个参数这个连接句柄是怎么回事?在哪里设置过他?

连接句柄你可以看做是信道标志一样(实际数据接入地址),每两个连接的设备都会具有这个连接句柄。他们后续的通信都是通过这个连接句柄来进行(可以理解是信道标志,两个设备的通信标志必须一样,这代表他们是在同样的信道上通信才能正确进行通信)

 

上面我们说过,板子整个初始化流程走完后就是睡眠和广播等待手机连接。那么这个conn_handle就一定是手机发来连接,板子中协议栈处理完后上抛给app的连接事件中赋值的。从而记录下后续板子和手机通信的”信道”。 在 程序框架剖析  那一讲中介绍过,协议栈拋上来的事件结构体最终是由dispatch这个派发程序发给再发给各个服务的事件处理函数和模块的事件处理函数的。

 

static void ble_evt_dispatch(ble_evt_t * p_ble_evt)

{

         //将事件交给连接管理模块的事件处理函数

ble_conn_params_on_ble_evt(p_ble_evt);

//将事件交给uart服务的事件处理函数

ble_nus_on_ble_evt(&m_nus, p_ble_evt);

//处理一些一般的事件

    on_ble_evt(p_ble_evt);

}

再进入 uart服务的事件处理函数中看下发生连接时是如何记下 后续通信所用的连接句柄的

这里只截取部分相关代码




 

说完了连接句柄下面来说最后一个问题

手机-》板子—》电脑方向的数据处理过程。


四:接收手机数据并通过串口打印:

其实看完了上面关于连接句柄的记录。再来理解怎么收到手机的数据就容易了。

因为我们说过,手机发送数据过来也是一个事件!

既然都是事件,那么传递流程一定是一样的,只是在最后的处理上不同的事件不同的处理。

 

那么第一步一定是协议栈处理完收到的数据打包一个 “写事件” 然后上抛给app。其实就是上抛给dispatch。然后在由它继续分发事件

 

再进入函数内部:


再进入on_write函数内部一看究竟。


这里最终是调用了一个回调函数来数理最终的数据,那么这个回调函数是什么时候注册的。在 第二部分 函数单独解析的 services_init讲解中说明过。


再来看看注册的这个 nus_data_handler 到底干了什么

 



到这里手机->板子->电脑方向的数据流也理清了

整体的流程就是 手机发送数据给板子后,板子中低层的协议栈将收到的数据打包成一个写事件结构体,然后上抛给app,最终由app种的diapatch再分发给各个服务或模块的事件处理函数,而uart的事件处理函数收到写事件后判断是不是要打印到电脑上的”普通数据”,如果是就调用server_init中注册的回调函数。该回调函数最终将数据打印到电脑上


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值