FreeRTOS基础学习(四)- 信号量

目录

一.信号量基础概念

二. 二值信号量(Binary Semaphore)

1.基础概念

2.二值信号量相关的常用函数

(1)二值信号量的创建 :xSemaphoreCreateBinary()

(2)二值信号量的释放 :xSemaphoreGive()

(3)二值信号量的获取 :xSemaphoreTake()

(4)中断中释放二值信号量 :xSemaphoreGiveFromISR()

(5)删除信号量:vSemaphoreDelete()

(6)综合示例

三. 计数信号量(Counting Semaphore)

1.核心原理

2.常用函数

(1)创建计数信号量 xSemaphoreCreateCounting

(2)获取计数信号量 xSemaphoreTake

(3)释放计数信号量 xSemaphoreGive

四. 互斥信号量(Mutex Semaphore)

1. 基本概念

2.常用函数

(1)创建互斥信号量 xSemaphoreCreateMutex()

(2)释放互斥信号量 xSemaphoreGive()

(3)获取互斥信号量xSemaphoreTake()

(4)综合示例

五.总结

1.三种信号量总结

2.通用函数总结(适用于多种信号量)

3.使用场景举例对比


一.信号量基础概念

信号量是一种任务间同步机制,用于协调多个任务或中断对共享资源的访问,防止资源冲突。它就像一个“通行证”,拿到信号量 = 拿到通行证,可以进入访问资源;如果没拿到,那就等待通行证释放。

信号量底层其实是使用 FreeRTOS 的队列(Queue)机制来实现的,我们可以把它理解为一种 “特殊配置的队列”,但是信号量本身不承载任何“有效数据”,它只表示一种状态(比如“我来了”“你可以动了”)。所以信号量不需要传输数据,只需要传达“信号”就行了;

FreeRTOS提供了3种主要类型的信号量:二值信号量、计数信号量、互斥信号量,下面具体展开了解;

二. 二值信号量(Binary Semaphore)
1.基础概念

它就像一个“标志位”或“开关”,只能是 0(无信号) 或 1(有信号)。就像一个盒子,只要有人放了东西(信号)(xSemaphoreGive()),它就是非空状态,但是有人把盒子里的东西拿走了(xSemaphoreTake()),这个盒子就是空的状态,所以这个盒子只有“空”或“非空”两个状态,代表这个房间能不能进。这个牌子就是“二值信号量”。二值信号量通常用于任务间同步,或者中断唤醒任务。

2.二值信号量相关的常用函数
(1)二值信号量的创建 :xSemaphoreCreateBinary()

初始状态为“空”,如果任务创建后要立刻执行一次,需要手动Give一次(xSemaphoreGive() ),否则一开始就会卡住;

如果信号量完全依赖于中断或者其他任务来Give,那无需手动初始化;

  • 函数原型

SemaphoreHandle_t xSemaphoreCreateBinary(void);
  • 返回值:

    • 成功:返回一个信号量句柄(handle);

    • 失败:返回 NULL(堆内存不足等)。

  • 示例代码

SemaphoreHandle_t xSemaphore;
xSemaphore = xSemaphoreCreateBinary();//返回一个指向信号量对象的指针,成功时返回非 NULL,失败时返回 NULL

//现在假设任务创建成功,需要立刻执行一次,则需要手动give一次:
if (xSemaphore != NULL) {
    xSemaphoreGive(xSemaphore); // 给一个初始信号量
}
(2)二值信号量的释放 :xSemaphoreGive()

释放一个信号量,通常用来通知其他任务或同步。

  • 函数原型:

BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore); 
//参数1 xSemaphore:信号量句柄;
  • 返回值

    • pdTRUE:成功释放;

    • pdFALSE:失败(比如队列满了);

  • 示例代码

if (xSemaphoreTake(xSemaphore, pdMS_TO_TICKS(1000))) 
{ 
// 成功拿到信号量,执行操作 
} else 
{ 
// 超时了,信号量没人给 
}
(3)二值信号量的获取 :xSemaphoreTake()
  • 函数原型

BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);
//参数1 xSemaphore:信号量句柄;
//参数2 xTicksToWait:最大等待时间,单位是tick
  • 返回值

    • pdTRUE:成功获取;

    • pdFALSE:超时未获取。

  • 示例代码

if (xSemaphoreTake(xSemaphore, pdMS_TO_TICKS(1000))) {
    // 成功拿到信号量,执行操作
} else {
    // 超时了,信号量没人给
}

二值型号量创建、释放与获取粗略的用法顺序大概是以下:

//1.创建二值信号量
SemaphoreHandle_t xSemaphore = xSemaphoreCreateBinary();

//2.获取信号量(拿“通行证”),如果成功获取信号量,任务立即获取成功;
//如果没获取成功,任务就会阻塞(挂起自己)等待直到可用或超时;
xSemaphoreTake(xSemaphore, TickType_t xTicksToWait);

//3.释放信号量(给“通行证”),通常是另一个任务或中断服务程序调用,表示资源/事件已准备好;
xSemaphoreGive(xSemaphore);

完整示例:

//任务:实现任务B每3秒通知一次任务A,任务A收到通知后立即执行自己的工作;

SemaphoreHandle_t xSemaphore;

void TaskA(void *pvParameters) {
    while (1) {
        if (xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdTRUE) {
            //xSemaphoreTake拿走一个信号;
            printf("Task A: Received signal! Start working...\n");
        }//portMAX_DELAY 表示无限等下去;
    }
}

void TaskB(void *pvParameters) {
    while (1) {
        vTaskDelay(pdMS_TO_TICKS(3000));  // 等 3 秒
        xSemaphoreGive(xSemaphore);       // 给一个信号,从而通知 Task A
        printf("Task B: Sent signal to Task A\n");
    }
}

void app_main() {
    xSemaphore = xSemaphoreCreateBinary();//二值信号量的创建
    xTaskCreate(TaskA, "TaskA", 2048, NULL, 2, NULL);//任务A创建
    xTaskCreate(TaskB, "TaskB", 2048, NULL, 2, NULL);//任务B创建
}
  • Q&A:give了之后需要手动重置信号量为空吗?

    • 二值信号量的使用流程是:Give → Take

    • xSemaphoreGive():让信号量“可用”;

    • xSemaphoreTake():消耗信号量 ;

    • 以上这2个函数是配套使用的,给了信号量之后,要有任务调用 xSemaphoreTake() 来“消费”它;一旦被 Take,信号量状态从有→ 空,接下来就必须等待下一次 Give 才能再次使用;

  • Q&A:是谁要获取信号量?又是谁要释放信号量呢?

    • 原则就是谁想要干活就要先“获取”(take)信号量;谁觉得别人可以开始干活了,就“释放”(give)信号量。

(4)中断中释放二值信号量 :xSemaphoreGiveFromISR()

由于中断里不能用 xSemaphoreGive(),所以这个是专门为**中断服务程序(ISR)**设计的版本,用来从中断中释放信号量。

xSemaphoreGiveFromISR() 是非阻塞的,在中断里不能等待!

xSemaphoreGiveFromISR()必须配合 portYIELD_FROM_ISR() 使用,确保中断退出时能切换到高优先级任务。

另外,需要注意,中断的本质是为了快速响应事件,比如按钮按下、串口收到数据,中断服务程序(ISR)执行时间必须非常短,所以与中断相关函数中只有Give函数,不能有等待接收的函数,所以,与中断有关的函数中,这里仅有xSemaphoreGiveFromISR(),不可以在中断中使用xSemaphoreTake()

如果你在中断中等待信号量(比如 xSemaphoreTake()),那么中断将会阻塞,而阻塞中断是非常严重的设计错误,可能会导致系统卡死或错过其他中断。

  • 函数原型:

BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken);
//参数1 xSemaphore:要给的信号量句柄
//参数2 pxHigherPriorityTaskWoken:用来通知 RTOS:是否需要在中断退出后立即进行一次任务切换;
//如果信号量触发导致某个任务解除阻塞,且解除阻塞的这个某任务优先级高于当前系统正在运行的任务,
//则 xSemaphoreGiveFromISR() 会将 pxHigherPriorityTaskWoken 设置为 pdTRUE,
//表示如果 xSemaphoreGiveFromISR() 将此值设置为 pdTRUE,则应在退出中断之前请求上下文切换,执行任务优先级更高的这个某任务;
  • 返回值:

    • pdTRUE:成功释放;

    • pdFALSE:释放信号量失败。

  • 示例代码

// 声明信号量
SemaphoreHandle_t xSemaphore;//句柄

// 中断服务程序(如 GPIO 中断)
void IRAM_ATTR gpio_isr_handler(void* arg)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    // 从中断中 Give 信号量
    xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken);

    // 如果唤醒了高优先级任务,要求立即切换
    if (xHigherPriorityTaskWoken == pdTRUE) {
        portYIELD_FROM_ISR();
    }
}
(5)删除信号量:vSemaphoreDelete()

释放信号量占用的系统资源。当信号量不再需要时,应调用此函数进行清理,以防内存泄漏。

  • 函数原型:

void vSemaphoreDelete(SemaphoreHandle_t xSemaphore);
//参数 xSemaphore:创建二值信号量时的句柄,相当于信号量的“身份证”或“控制指针”。
//无返回值(void 类型);
  • 使用场景

    1.动态创建的信号量(xSemaphoreCreateBinary() 或 xSemaphoreCreateMutex() 等)在不再使用时,应该手动删除;

   2.比如某些临时功能、初始化流程中用完一次信号量后,及时释放资源;

  3.【注意】静态信号量(使用 xSemaphoreCreateBinaryStatic() 创建的)不应使用这个函数删除。

  • 示例代码

SemaphoreHandle_t xSemaphore;

void some_task(void *pvParameters)
{
    // 创建信号量
    xSemaphore = xSemaphoreCreateBinary();
    if (xSemaphore == NULL) {
        printf("信号量创建失败!\n");
        vTaskDelete(NULL);
    }

    // 给一次信号量,让任务可以继续执行
    xSemaphoreGive(xSemaphore);

    // 使用信号量
    if (xSemaphoreTake(xSemaphore, pdMS_TO_TICKS(1000))) {
        printf("任务成功获取信号量\n");
    }

    // 使用完后删除
    vSemaphoreDelete(xSemaphore);

    // 删除任务
    vTaskDelete(NULL);
}
(6)综合示例
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "driver/gpio.h"
#include "esp_log.h"

#define BUTTON_GPIO 0 // 假设按键接在 GPIO0

SemaphoreHandle_t xSemaphore = NULL;

// 中断服务函数
void IRAM_ATTR gpio_isr_handler(void* arg)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    // 给信号量,唤醒任务
    xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken);

    // 若需要上下文切换
    if (xHigherPriorityTaskWoken == pdTRUE) {
        portYIELD_FROM_ISR();
    }
}

// 任务函数:等待按键信号量,执行操作
void sensor_task(void *arg)
{
    while (1)
    {
        // 等待信号量,有效期为无限等待
        if (xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdTRUE)
        {
            // 模拟读取传感器
            ESP_LOGI("SENSOR", "读取传感器中...");
            vTaskDelay(pdMS_TO_TICKS(500)); // 模拟耗时

            // 模拟上传数据
            ESP_LOGI("SENSOR", "上传数据中...");
            vTaskDelay(pdMS_TO_TICKS(500));

            ESP_LOGI("SENSOR", "操作完成,等待下一次中断触发...");
        }
    }
}

void app_main(void)
{
    // 创建二值信号量
    xSemaphore = xSemaphoreCreateBinary();
    if (xSemaphore == NULL)
    {
        ESP_LOGE("MAIN", "创建信号量失败!");
        return;
    }

    // 配置 GPIO 作为输入(上拉),设置中断
    gpio_config_t io_conf = {
        .pin_bit_mask = 1ULL << BUTTON_GPIO,
        .mode = GPIO_MODE_INPUT,
        .pull_up_en = GPIO_PULLUP_ENABLE,
        .intr_type = GPIO_INTR_NEGEDGE // 下降沿触发
    };
    gpio_config(&io_conf);

    // 安装中断服务并注册
    gpio_install_isr_service(0);
    gpio_isr_handler_add(BUTTON_GPIO, gpio_isr_handler, NULL);

    // 创建任务
    xTaskCreate(sensor_task, "SensorTask", 2048, NULL, 10, NULL);

    // 手动 give 一次,首次运行后可以立即进入任务(可选)
    xSemaphoreGive(xSemaphore);
}
三. 计数信号量(Counting Semaphore)

计数信号量是一种可以计数的信号量,用于管理“多个资源”的使用权限,或者“统计”多个事件的发生次数。它不像二值信号量只能是 0 或 1,而是可以有 多个“许可”数量。它可用于计数资源的使用情况,比如管理多个相同资源(如 N 个串口缓冲区、连接数、设备通道等)的访问。

1.核心原理
  • 初始化时设置一个“可用资源数量”(最大计数值)。

  • 每次任务调用 xSemaphoreTake() 时,计数值减 1。

  • 每次调用 xSemaphoreGive() 时,计数值加 1。

  • 当计数值为 0 时,再 Take() 的任务将被阻塞。

所以,从本质上看,它和二值信号量一样,底层是用队列实现的

2.常用函数
(1)创建计数信号量 xSemaphoreCreateCounting
  • 函数原型

SemaphoreHandle_t xSemaphoreCreateCounting(
    UBaseType_t uxMaxCount,     // 信号量最大计数值
    UBaseType_t uxInitialCount  // 初始可用资源数量
);
  • 示例

SemaphoreHandle_t xSemaphore;

void app_main() {
    // 创建最大为5、初始为2的计数信号量
    xSemaphore = xSemaphoreCreateCounting(5, 2);

    if (xSemaphore == NULL) {
        printf("信号量创建失败!\n");
    }
}
(2)获取计数信号量 xSemaphoreTake
  • 从计数信号量中“获取”一个资源,也就是将信号量计数减1。
  • 如果当前信号量计数大于0,函数立即成功返回,表示成功获取资源。

  • 如果信号量计数为0,表示当前没有可用资源,调用该函数的任务会阻塞(等待)直到信号量计数变为大于0(即有资源释放),或者超时。

(3)释放计数信号量 xSemaphoreGive

释放一个资源,即使信号量计数加1。释放资源后,系统会检查是否有阻塞任务等待该信号量,如果有,唤醒优先级最高的任务。

  • 示例

//有3台打印机共享给多个打印任务使用,每个任务打印时需要占用一台打印机;
//打印完成后释放打印机供其他任务使用。打印机资源有限,因此必须用计数信号量来管理访问。
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include <stdio.h>

#define PRINT_JOB_COUNT 5

// 创建计数信号量,代表3台打印机资源
SemaphoreHandle_t xPrinterSemaphore;

void print_task(void* arg) {
    int task_id = (int)arg;
    for (int i = 0; i < PRINT_JOB_COUNT; i++) {
        printf("Task %d: Waiting for printer...\n", task_id);

        // 请求一个打印机,等待最多1000 tick
        if (xSemaphoreTake(xPrinterSemaphore, pdMS_TO_TICKS(1000)) == pdTRUE) {
            printf("Task %d: Got a printer, printing job %d\n", task_id, i + 1);

            // 模拟打印时间
            vTaskDelay(pdMS_TO_TICKS(500));

            printf("Task %d: Finished printing job %d, releasing printer\n", task_id, i + 1);
            
            // 释放打印机资源
            xSemaphoreGive(xPrinterSemaphore);
        } else {
            printf("Task %d: Timeout waiting for printer for job %d\n", task_id, i + 1);
        }

        // 等待一段时间后准备下一次打印
        vTaskDelay(pdMS_TO_TICKS(200));
    }

    vTaskDelete(NULL); // 任务完成后自杀
}

void app_main(void) {
    // 初始化计数信号量,3个打印机资源,初始可用3个
    xPrinterSemaphore = xSemaphoreCreateCounting(3, 3);
    if (xPrinterSemaphore == NULL) {
        printf("Failed to create printer semaphore\n");
        return;
    }

    // 创建多个打印任务,模拟多个任务共享3台打印机
    for (int i = 1; i <= 5; i++) {
        char task_name[16];
        snprintf(task_name, sizeof(task_name), "PrintTask%d", i);
        xTaskCreate(print_task, task_name, 2048, (void*)i, 2, NULL);
    }
}

(4)中断中释放计数信号量 :xSemaphoreGiveFromISR()

释放信号量计数+1,同时可以通知系统是否需要从ISR直接切换任务

// 创建计数信号量,最大计数5,初始资源数5
SemaphoreHandle_t xSemaphore = xSemaphoreCreateCounting(5, 5);

void task(void* arg) {
    while(1) {
        // 请求一个资源,等待最多100个tick
        if (xSemaphoreTake(xSemaphore, 100)) {
            // 成功获取资源,执行任务代码
            do_something();
            
            // 任务结束释放资源
            xSemaphoreGive(xSemaphore);
        } else {
            // 超时没获得资源,处理超时逻辑
            handle_timeout();
        }
    }
}

// 中断服务函数释放资源示例
void IRAM_ATTR isr_handler(void* arg) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken);
    if(xHigherPriorityTaskWoken) {
        portYIELD_FROM_ISR();
    }
}
四. 互斥信号量(Mutex Semaphore)
1. 基本概念
  • 互斥信号量是一种特殊的二值信号量,设计目的是防止多个任务同时访问共享资源(如共享变量、外设等)。

  • 它提供了优先级继承机制,用来解决任务优先级反转问题(Priority Inversion)。

  • 互斥信号量必须由同一个任务“拿(take)”和“放(give)”,否则会产生错误。

2.常用函数
(1)创建互斥信号量 xSemaphoreCreateMutex()
  • 函数原型

SemaphoreHandle_t xSemaphoreCreateMutex(void);//无参数
  • 返回值

  • 成功:返回互斥信号量句柄(SemaphoreHandle_t 类型)

  • 失败:返回 NULL(比如内存不足)

  • 示例

SemaphoreHandle_t xMutex;

void app_main() {
    xMutex = xSemaphoreCreateMutex();//无参数
    if (xMutex == NULL) {
        printf("互斥信号量创建失败!\n");
    } else {
        printf("互斥信号量创建成功!\n");
    }
}
(2)释放互斥信号量 xSemaphoreGive()
  • 函数原型

BaseType_t xSemaphoreGive(SemaphoreHandle_t xMutex);
//参数: xMutex:互斥信号量句柄
  • 返回值

  • pdTRUE:释放成功

  • pdFALSE:释放失败(常见于没有先 take 就 give)

  • 示例

// 假设当前任务已经成功 xSemaphoreTake(xMutex)
xSemaphoreGive(xMutex);
  • 必须是当前获取了该 mutex 的任务才能释放,否则行为未定义;

  • 释放后,可能唤醒等待该 mutex 的任务;

(3)获取互斥信号量xSemaphoreTake()
  • 函数原型

BaseType_t xSemaphoreTake(SemaphoreHandle_t xMutex, TickType_t xTicksToWait);
//参数1: xMutex:互斥信号量句柄(通过 xSemaphoreCreateMutex() 获得)
//参数2: xTicksToWait:最大等待时间(单位为 Tick)   0:不等待,立即返回  ;portMAX_DELAY:无限等待直到可用
  • 返回值

  • pdTRUE:成功获取互斥信号量

  • pdFALSE:超时未获取

  • 示例

if (xSemaphoreTake(xMutex, pdMS_TO_TICKS(100))) {
    // 成功获取互斥量
    printf("进入临界区\n");

    // 访问受保护资源...

    xSemaphoreGive(xMutex); // 使用完毕,记得释放
} else {
    printf("获取互斥量超时,无法进入临界区\n");
}
(4)综合示例
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"

SemaphoreHandle_t xMutex;

void low_priority_task(void *param)
{
    xSemaphoreTake(xMutex, portMAX_DELAY);
    printf("低优先级任务 A 获取了互斥量,开始占用资源...\n");

    // 模拟长时间占用资源
    for (int i = 0; i < 5; i++) {
        printf("A 正在使用资源 (%d/5)...\n", i+1);
        vTaskDelay(pdMS_TO_TICKS(1000));
    }

    printf("A 释放互斥量,资源释放完毕\n");
    xSemaphoreGive(xMutex);

    vTaskDelete(NULL);
}

void medium_priority_task(void *param)
{
    while (1) {
        printf("中等优先级任务 C 正在运行(打断其他任务)...\n");
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

void high_priority_task(void *param)
{
    // 等待一段时间让低优先级任务先运行并获取 mutex
    vTaskDelay(pdMS_TO_TICKS(1000));

    printf("高优先级任务 B 尝试获取互斥量...\n");
    xSemaphoreTake(xMutex, portMAX_DELAY);
    printf("高优先级任务 B 成功获取了互斥量,执行完成!\n");
    xSemaphoreGive(xMutex);

    vTaskDelete(NULL);
}

void app_main(void)
{
    xMutex = xSemaphoreCreateMutex();

    if (xMutex == NULL) {
        printf("创建互斥量失败!\n");
        return;
    }

    // 创建三个不同优先级的任务
    xTaskCreate(low_priority_task, "Task A (Low)", 2048, NULL, 3, NULL);
    xTaskCreate(medium_priority_task, "Task C (Medium)", 2048, NULL, 4, NULL);
    xTaskCreate(high_priority_task, "Task B (High)", 2048, NULL, 5, NULL);
}

五.总结
1.三种信号量总结

信号量类型

描述

特点

适用场景

二值信号量

Binary Semaphore

只有两个状态:“满”(1) 或 “空”(0)

- 不记录数量

- 类似事件通知

- 支持中断中释放

- 初始值为空

- 任务同步

- 中断通知任务

- 实现简单的“事件到达”

计数信号量

Counting Semaphore

信号量可以有多个“资源单位”

- 支持计数(如5个资源)

- 支持中断中释放- 初始值由你设置

- 多资源池管理(如多个缓冲区)

- 控制并发数

- 网络连接池

互斥信号量(互斥量)

Mutex Semaphore

只能被“获得它的任务”释放

- 带优先级继承机制

- 同一任务重复获取会阻塞

(递归 mutex 除外)

- 保护临界资源

- 防止“优先级反转”

- 用于串口、共享变量、IO口保护等

2.通用函数总结(适用于多种信号量)

函数名

说明

使用说明

xSemaphoreCreateBinary()

创建二值信号量

信号量初始为空

xSemaphoreCreateCounting(max, init)

创建计数信号量

可设置最大和初始计数值

xSemaphoreCreateMutex()

创建互斥信号量

含优先级继承

xSemaphoreTake(xSemaphore, timeout)

获取信号量(减1)

适用于所有类型(互斥量也一样)

xSemaphoreGive(xSemaphore)

释放信号量(加1)

所有类型通用(互斥量需自己释放)

xSemaphoreGiveFromISR(xSemaphore, *pxHigherPriorityTaskWoken)

在中断中释放信号量

仅适用于二值或计数信号量

vSemaphoreDelete(xSemaphore)

删除信号量

所有类型都适用

3.使用场景举例对比

应用场景

推荐使用

中断通知任务

二值信号量(GiveFromISR)

任务之间同步事件

二值信号量

管理多个连接/资源

 

计数信号量

控制并发执行上限

计数信号量

保护共享变量/设备

互斥信号量

防止优先级反转

互斥信号量(具备继承机制)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值