FreeRTOS中的信号量实验

信号量是操作系统中重要的一部分,信号量一般用来进行资源管理和任务同 步,FreeRTOS 中信号量又分为二值信号量、计数型信号量、互斥信号量和递归 互斥信号量。不同的信号量其应用场景不同,但有些应用场景是可以互换着使用。 本章要实现的功能是:创建 3 个任务,分别为 LED、二值信号量释放、二值信号 量获取任务,LED 任务控制 DS0 指示灯闪烁,KEY_UP 和 KEY1 键控制二值信号量 释放,获取二值信号量成功,DS1 指示灯状态翻转,同时通过串口输出提示信息。 本章分为如下几部分内容: 7.1 信号量简介 7.2 常用信号量 API 函数 7.3 硬件设计 7.4 软件设计 7.5 实验现 象

7.1 信号量简介

信号量(Semaphore)是一种实现任务间通信的机制,可以实现任务之间同 步或临界资源的互斥访问,常用于协助一组相互竞争的任务来访问临界资源。在 多任务系统中,各任务之间需要同步或互斥实现临界资源的保护,信号量功能可 以为用户提供这方面的支持。 抽象的来讲,信号量是一个非负整数,所有获取它的任务都会将该整数减一 (获取它当然是为了使用资源),当该整数值为零时,所有试图获取它的任务都 将处于阻塞状态。通常一个信号量的计数值用于对应有效的资源数,表示剩下的 可被占用的互斥资源数。其值的含义分两种情况: ●0:表示没有积累下来的释放信号量操作,且有可能有在此信号量上阻塞 的任务。 ●正值,表示有一个或多个释放信号量操作。

7.1.1 二值信号量

二值信号量既可以用于临界资源访问也可以用于同步功能。 二值信号量和互斥信号量(以下使用互斥量表示互斥信号量)非常相似,但 是有一些细微差别:互斥量有优先级继承机制,二值信号量则没有这个机制。这 使得二值信号量更偏向应用于同步功能(任务与任务间的同步或任务和中断间同 步),而互斥量更偏向应用于临界资源的访问。 用作同步时,信号量在创建后应被置为空,任务 1 获取信号量而进入阻塞, 任务 2 在某种条件发生后,释放信号量,于是任务 1 获得信号量得以进入就绪 态,如果任务 1 的优先级是最高的,那么就会立即切换任务,从而达到了两个 任务间的同步。同样的,在中断服务函数中释放信号量,任务 1 也会得到信号 量,从而达到任务与中断间的同步。 还记得我们经常说的中断要快进快出吗,在裸机开发中我们经常是在中断中 做一个标记,然后在退出的时候进行轮询处理,这个就是类似我们使用信号量进 行同步的,当标记发生了,我们再做其他事情。在 FreeRTOS 中我们用信号量用 于同步,任务与任务的同步,中断与任务的同步,可以大大提高效率。 可以将二值信号量看作只有一个消息的队列,因此这个队列只能为空或满 (因此称为二值),我们在运用的时候只需要知道队列中是否有消息即可,而无 需关注消息是什么。

7.1.2 计数信号量

二进制信号量可以被认为是长度为 1 的队列,而计数信号量则可以被认为 长度大于 1 的队列,信号量使用者依然不必关心存储在队列中的消息,只需关 心队列是否有消息即可。 顾名思义,计数信号量肯定是用于计数的,在实际的使用中,我们常将计数 信号量用于事件计数与资源管理。每当某个事件发生时,任务或者中断将释放一 个信号量(信号量计数值加 1),当处理事件时(一般在任务中处理),处理任 务会取走该信号量(信号量计数值减 1),信号量的计数值则表示还有多少个事 件没被处理。此外,系统还有很多资源,我们也可以使用计数信号量进行资源管 理,信号量的计数值表示系统中可用的资源数目,任务必须先获取到信号量才能 获取资源访问权,当信号量的计数值为零时表示系统没有可用的资源,但是要注 意,在使用完资源的时候必须归还信号量,否则当计数值为 0 的时候任务就无 法访问该资源了。 计数型信号量允许多个任务对其进行操作,但限制了任务的数量。比如有一 个停车场,里面只有 100 个车位,那么能停的车只有 100 辆,也相当于我们的 信号量有 100 个,假如一开始停车场的车位还有 100 个,那么每进去一辆车就 要消耗一个停车位,车位的数量就要减一,对应的,我们的信号量在使用之后也 需要减一,当停车场停满了 100 辆车的时候,此时的停车位为 0,再来的车就 不能停进去了,否则将造成事故,也相当于我们的信号量为 0,后面的任务对这 个停车场资源的访问也无法进行,当有车从停车场离开的时候,车位又空余出来 了,那么,后面的车就能停进去了,我们信号量的操作也是一样的,当我们释放 了这个资源,后面的任务才能对这个资源进行访问。

7.1.3 互斥信号量

互斥信号量其实是特殊的二值信号量,由于其特有的优先级继承机制从而使 它更适用于简单互锁,也就是保护临界资源(什么是优先级继承在后续详细讲 解)。 用作互斥时,信号量创建后可用信号量个数应该是满的,任务在需要使用临 界资源时,(临界资源是指任何时刻只能被一个任务访问的资源),先获取互斥 信号量,使其变空,这样其他任务需要使用临界资源时就会因为无法获取信号量 而进入阻塞,从而保证了临界资源的安全。 在操作系统中,我们使用信号量的很多时候是为了给临界资源建立一个标 志,信号量表示了该临界资源被占用情况。这样,当一个任务在访问临界资源的 时候,就会先对这个资源信息进行查询,从而在了解资源被占用的情况之后,再 做处理,从而使得临界资源得到有效的保护。

7.1.4 递归信号量

递归信号量,见文知义,递归嘛,就是可以重复获取调用的,本来按照信号 量的特性,每获取一次可用信号量个数就会减少一个,但是递归则不然,对于已 经获取递归互斥量的任务可以重复获取该递归互斥量,该任务拥有递归信号量的 所有权。任务成功获取几次递归互斥量,就要返还几次,在此之前递归互斥量都 处于无效状态,其他任务无法获取,只有持有递归信号量的任务才能获取与释放。

7.1.5 二值信号量运作机制

创建信号量时,系统会为创建的信号量对象分配内存,并把可用信号量初始 化为用户自定义的个数,二值信号量的最大可用信号量个数为 1。 二值信号量获取,任何任务都可以从创建的二值信号量资源中获取一个二值 信号量,获取成功则返回正确,否则任务会根据用户指定的阻塞超时时间来等待 其它任务/中断释放信号量。在等待这段时间,系统将任务变成阻塞态,任务将 被挂到该信号量的阻塞等待列表中。 在二值信号量无效的时候,假如此时有任务获取该信号量的话,那么任务将 进入阻塞状态,具体如下。

假如某个时间中断/任务释放了信号量,那么,由于获取无效信号量而进入 阻塞态的任务将获得信号量并且恢复为就绪态,其过程具体如下图

7.1.7 计数信号量运作机制

计数信号量可以用于资源管理,允许多个任务获取信号量访问共享资源,但 会限制任务的最大数目。访问的任务数达到可支持的最大数目时,会阻塞其他试 图获取该信号量的任务,直到有任务释放了信号量。这就是计数型信号量的运作 机制,虽然计数信号量允许多个任务访问同一个资源,但是也有限定,比如某个 资源限定只能有 3 个任务访问,那么第 4 个任务访问的时候,会因为获取不到 信号量而进入阻塞,等到有任务(比如任务 1)释放掉该资源的时候,第 4 个 任务才能获取到信号量从而进行资源的访问,其运作的机制具体如下

7.2 常用信号量 API 函数

7.2.1 创建信号量函数

7.2.1.1 创建二值信号量 xSemaphoreCreateBinary() xSemaphoreCreateBinary()用于创建一个二值信号量,并返回一个句柄。其 实二值信号量和互斥量都共同使用一个类型 SemaphoreHandle_t 的句柄,该句 柄的原型是一个 void 型的指针。使用该函数创建的二值信号量是空的,在使用 函数 xSemaphoreTake()获取之前必须先调用函数 xSemaphoreGive()释放后才 可以获取。如果是使用老式的函数 vSemaphoreCreateBinary()创建的二值信号 量,则为 1,在使用之前不用先释放。要想使用该函数必须在 FreeRTOSConfig.h 中把宏 configSUPPORT_DYNAMIC_ALLOCATION 定义为 1,即开启动态内存分配。 其实该宏在 FreeRTOS.h 中默认定义为 1,即所有 FreeRTOS 的对象在创建的时 候都默认使用动态内存分配方案,xSemaphoreCreateBinary()函数原型具体代码 如下。

#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
#define xSemaphoreCreateBinary()
xQueueGenericCreate( ( UBaseType_t ) 1, (1)
semSEMAPHORE_QUEUE_ITEM_LENGTH, (2)
queueQUEUE_TYPE_BINARY_SEMAPHORE ) (3)
#endif

从这个函数原型我们就可以知道二值信号量的创建实际使用的函数就是 xQueueGenericCreate()函数,是不是很熟悉,这就是消息队列的创建使用的函 数,但是参数不一样,根据 xQueueGenericCreate()函数原型来讲解一下参数的 作用。 代码(1):uxQueueLength 为 1 表示创建的队列长度为 1,其实用作信号量 就表示信号量的最大可用个数,从前面的知识点我们就知道,二值信号量的非空 即满,长度为 1 不正是这样子的表示吗。 代码(2):semSEMAPHORE_QUEUE_ITEM_LENGTH 其实是一个宏定义,其值为 0, 见文知义,它表示创建的消息空间(队列项)大小为 0,因为这个所谓的“消息 队列”其实并不是用于存储消息的,而是被用作二值信号量,因为我们根本无需 关注消息内容是什么,只要知道有没有信号量就行了。 代码(3):ucQueueType 表示的是创建消息队列的类型,在 queue.h 中有定 义,具体代码如下,现在创建的是二值信号量,其类型就是 queueQUEUE_TYPE_BINARY_SEMAPHORE

可能很多人会问了,创建一个没有消息存储空间的队列,信号量用什么表 示?其实二值信号量的释放和获取都是通过操作队列结控制块构体成员 uxMessageWaiting 来实现的,它表示信号量中当前可用的信号量个数。在信号 量创建之后,变量 uxMessageWaiting 的值为 0,这说明当前信号量处于无效状 态,此时的信号量是无法被获取的,在获取信号之前,应先释放一个信号量。后 面讲到信号量释放和获取时还会详细介绍

7.2.1.2 创建计数信号量 xSemaphoreCreateCounting()

xSemaphoreCreateCounting()用于创建一个计数信号量。要想使用该函数必 须在 FreeRTOSConfig.h 中把宏 configSUPPORT_DYNAMIC_ALLOCATION 定义为 1,即开启动态内存分配。其实该宏在 FreeRTOS.h 中默认定义为 1,即所有 FreeRTOS 的对象在创建的时候都默认使用动态内存分配方案。 其实计数信号量跟二值信号量的创建过程都差不多,其实也是间接调用 xQueueGenericCreate()函数进行创建,xSemaphoreCreateCounting()函数说明 具体如下

7.2.2 信号量删除函数 vSemaphoreDelete()

vSemaphoreDelete()用于删除一个信号量,包括二值信号量,计数信号量, 互斥量和递归互斥量。如果有任务阻塞在该信号量上,那么不要删除该信号量。 该函数的具体如下。

7.2.3 信号量释放函 数

7.2.3.1 xSemaphoreGive()

xSemaphoreGive()是一个用于释放信号量的宏,真正的实现过程是调用消息 队列通用发送函数,xSemaphoreGive()函数原型具体代码如下。释放的信号量对 象必须是已经被创建的,可以用于二值信号量、计数信号量、互斥量的释放,但 不能释放由函数 xSemaphoreCreateRecursiveMutex()创建的递归互斥量。此外 该函数不能在中断中使用

#define xSemaphoreGive( xSemaphore )
xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ),
NULL,
semGIVE_BLOCK_TIME,
queueSEND_TO_BACK )

从该宏定义可以看出释放信号量实际上是一次入队操作,并且是不允许入队 阻塞,因为阻塞时间为 semGIVE_BLOCK_TIME,该宏的值为 0。 通过消息队列入队过程分析,我们可以将释放一个信号量的过程简化:如果 信号量未满,控制块结构体成员 uxMessageWaiting 就会加 1,然后判断是否有 阻塞的任务,如果有的话就会恢复阻塞的任务,然后返回成功信息(pdPASS); 如果信号量已满,则返回错误代码(err_QUEUE_FULL)

7.2.3.2 xSemaphoreGiveFromISR()

用于释放一个信号量,带中断保护。被释放的信号量可以是二进制信号量和 计数信号量。和普通版本的释放信号量 API 函数有些许不同,它不能释放互斥 量,这是因为互斥量不可以在中断中使用,互斥量的优先级继承机制只能在任务 中起作用,而在中断中毫无意义。带中断保护的信号量释放其实也是一个宏,真 正调用的函数是 xQueueGiveFromISR (),宏定义如下

#define xSemaphoreGiveFromISR( xSemaphore, pxHigherPriorityTaskWoken )
xQueueGiveFromISR( ( QueueHandle_t ) ( xSemaphore ),
( pxHigherPriorityTaskWoken ) 

7.2.4 信号量获取函 数

7.2.4.1 xSemaphoreTake()

xSemaphoreTake()函数用于获取信号量,不带中断保护。获取的信号量对象 可以是二值信号量、计数信号量和互斥量,但是递归互斥量并不能使用这个 API 函数获取。其实获取信号量是一个宏,真正调用的函数是 xQueueGenericReceive ()。该宏不能在中断使用,而是必须由具体中断保护功能的 xQueueReceiveFromISR()版本代替。该函数的具体说明如下。

7.2.4.2 xSemaphoreTakeFromISR()

xSemaphoreTakeFromISR()是函数 xSemaphoreTake()的中断版本,用于获取 信号量,是一个不带阻塞机制获取信号量的函数,获取对象必须由是已经创建的 信号量,信号量类型可以是二值信号量和计数信号量,它与 xSemaphoreTake() 函数不同,它不能用于获取互斥量,因为互斥量不可以在中断中使用,并且互斥 量特有的优先级继承机制只能在任务中起作用,而在中断中毫无意义。该函数的 具体说明如下。

整体代码

二值信号量整体代码

#include "system.h"
#include "SysTick.h"
#include "led.h"
#include "usart.h"
#include "FreeRTOS.h"
#include "task.h"
#include "key.h"
#include "semphr.h"


//任务优先级
#define START_TASK_PRIO        1
//任务堆栈大小    
#define START_STK_SIZE         128  
//任务句柄
TaskHandle_t StartTask_Handler;
//任务函数
void start_task(void *pvParameters);

//任务优先级
#define LED1_TASK_PRIO        2
//任务堆栈大小    
#define LED1_STK_SIZE         50  
//任务句柄
TaskHandle_t LED1Task_Handler;
//任务函数
void led1_task(void *pvParameters);

//任务优先级
#define RECEIVE_TASK_PRIO        3
//任务堆栈大小    
#define RECEIVE_STK_SIZE         50  
//任务句柄
TaskHandle_t ReceiveTask_Handler;
//任务函数
void receive_task(void *pvParameters);

//任务优先级
#define SEND_TASK_PRIO        4
//任务堆栈大小    
#define SEND_STK_SIZE         50  
//任务句柄
TaskHandle_t SendTask_Handler;
//任务函数
void send_task(void *pvParameters);


SemaphoreHandle_t BinarySem_Handle =NULL;


/*******************************************************************************
* 函 数 名         : main
* 函数功能           : 主函数
* 输    入         : 无
* 输    出         : 无
*******************************************************************************/
int main()
{
    SysTick_Init(72);
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4
    LED_Init();
    KEY_Init();
    USART1_Init(115200);
    printf("FreeRTOS二值信号量实验\r\n");
    printf("按下KEY_UP或者KEY1进行任务与任务间的同步\r\n");
    printf("Receive任务接收到消息在串口回显\r\n");
    //创建开始任务
    xTaskCreate((TaskFunction_t )start_task,            //任务函数
                (const char*    )"start_task",          //任务名称
                (uint16_t       )START_STK_SIZE,        //任务堆栈大小
                (void*          )NULL,                  //传递给任务函数的参数
                (UBaseType_t    )START_TASK_PRIO,       //任务优先级
                (TaskHandle_t*  )&StartTask_Handler);   //任务句柄              
    vTaskStartScheduler();          //开启任务调度
}

//开始任务任务函数
void start_task(void *pvParameters)
{
    taskENTER_CRITICAL();           //进入临界区
     
    /* 创建 二值信号量 */
    BinarySem_Handle = xSemaphoreCreateBinary();     
    
    //创建LED1任务
    xTaskCreate((TaskFunction_t )led1_task,     
                (const char*    )"led1_task",   
                (uint16_t       )LED1_STK_SIZE, 
                (void*          )NULL,
                (UBaseType_t    )LED1_TASK_PRIO,
                (TaskHandle_t*  )&LED1Task_Handler); 
                
    //创建接收任务
    xTaskCreate((TaskFunction_t )receive_task,     
                (const char*    )"receive_task",   
                (uint16_t       )RECEIVE_STK_SIZE, 
                (void*          )NULL,
                (UBaseType_t    )RECEIVE_TASK_PRIO,
                (TaskHandle_t*  )&ReceiveTask_Handler);

    //创建发送任务
    xTaskCreate((TaskFunction_t )send_task,     
                (const char*    )"send_task",   
                (uint16_t       )SEND_STK_SIZE, 
                (void*          )NULL,
                (UBaseType_t    )SEND_TASK_PRIO,
                (TaskHandle_t*  )&SendTask_Handler);
                
    vTaskDelete(StartTask_Handler); //删除开始任务
    taskEXIT_CRITICAL();            //退出临界区
} 

//LED1任务函数
void led1_task(void *pvParameters)
{
    while(1)
    {
        LED1=0;
        vTaskDelay(200);
        LED1=1;
        vTaskDelay(800);
    }
}

//接收任务函数
void receive_task(void *pvParameters)
{
    BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
    
    while(1)
    {
        //获取二值信号量 xSemaphore,没获取到则一直等待
        xReturn = xSemaphoreTake(BinarySem_Handle,/* 二值信号量句柄 */
                                portMAX_DELAY); /* 等待时间 */
        if(pdTRUE == xReturn)
            printf("BinarySem_Handle二值信号量获取成功!\n\n");
        LED2=!LED2;
    }
}

//发送任务函数
void send_task(void *pvParameters)
{
    BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
    u8 key=0;
    
    while(1)
    {
        key=KEY_Scan(0);
        if(key==KEY_UP_PRESS)
        {
            xReturn = xSemaphoreGive( BinarySem_Handle );//给出二值信号量
            if( xReturn == pdTRUE )
                printf("BinarySem_Handle二值信号量释放成功!\r\n");
            else
                printf("BinarySem_Handle二值信号量释放失败!\r\n");
        }
        else if(key==KEY1_PRESS)
        {
            xReturn = xSemaphoreGive( BinarySem_Handle );//给出二值信号量
            if( xReturn == pdTRUE )
                printf("BinarySem_Handle二值信号量释放成功!\r\n");
            else
                printf("BinarySem_Handle二值信号量释放失败!\r\n");
        }
        vTaskDelay(20);
    }
}

计数信号量整体代码

#include "system.h"
#include "SysTick.h"
#include "led.h"
#include "usart.h"
#include "FreeRTOS.h"
#include "task.h"
#include "key.h"
#include "semphr.h"


//任务优先级
#define START_TASK_PRIO        1
//任务堆栈大小    
#define START_STK_SIZE         128  
//任务句柄
TaskHandle_t StartTask_Handler;
//任务函数
void start_task(void *pvParameters);

//任务优先级
#define LED1_TASK_PRIO        2
//任务堆栈大小    
#define LED1_STK_SIZE         50  
//任务句柄
TaskHandle_t LED1Task_Handler;
//任务函数
void led1_task(void *pvParameters);

//任务优先级
#define RECEIVE_TASK_PRIO        3
//任务堆栈大小    
#define RECEIVE_STK_SIZE         50  
//任务句柄
TaskHandle_t ReceiveTask_Handler;
//任务函数
void receive_task(void *pvParameters);

//任务优先级
#define SEND_TASK_PRIO        4
//任务堆栈大小    
#define SEND_STK_SIZE         50  
//任务句柄
TaskHandle_t SendTask_Handler;
//任务函数
void send_task(void *pvParameters);


SemaphoreHandle_t CountSem_Handle =NULL;


/*******************************************************************************
* 函 数 名         : main
* 函数功能           : 主函数
* 输    入         : 无
* 输    出         : 无
*******************************************************************************/
int main()
{
    SysTick_Init(72);
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4
    LED_Init();
    KEY_Init();
    USART1_Init(115200);
    printf("FreeRTOS计数信号量实验\r\n");
    printf("车位默认值为5个,按下KEY_UP申请车位,按下KEY1释放车位\r\n");
    //创建开始任务
    xTaskCreate((TaskFunction_t )start_task,            //任务函数
                (const char*    )"start_task",          //任务名称
                (uint16_t       )START_STK_SIZE,        //任务堆栈大小
                (void*          )NULL,                  //传递给任务函数的参数
                (UBaseType_t    )START_TASK_PRIO,       //任务优先级
                (TaskHandle_t*  )&StartTask_Handler);   //任务句柄              
    vTaskStartScheduler();          //开启任务调度
}

//开始任务任务函数
void start_task(void *pvParameters)
{
    taskENTER_CRITICAL();           //进入临界区
     
    /* 创建 计数信号量 */
    CountSem_Handle = xSemaphoreCreateCounting(5,5); 
    
    //创建LED1任务
    xTaskCreate((TaskFunction_t )led1_task,     
                (const char*    )"led1_task",   
                (uint16_t       )LED1_STK_SIZE, 
                (void*          )NULL,
                (UBaseType_t    )LED1_TASK_PRIO,
                (TaskHandle_t*  )&LED1Task_Handler); 
                
    //创建接收任务
    xTaskCreate((TaskFunction_t )receive_task,     
                (const char*    )"receive_task",   
                (uint16_t       )RECEIVE_STK_SIZE, 
                (void*          )NULL,
                (UBaseType_t    )RECEIVE_TASK_PRIO,
                (TaskHandle_t*  )&ReceiveTask_Handler);

    //创建发送任务
    xTaskCreate((TaskFunction_t )send_task,     
                (const char*    )"send_task",   
                (uint16_t       )SEND_STK_SIZE, 
                (void*          )NULL,
                (UBaseType_t    )SEND_TASK_PRIO,
                (TaskHandle_t*  )&SendTask_Handler);
                
    vTaskDelete(StartTask_Handler); //删除开始任务
    taskEXIT_CRITICAL();            //退出临界区
} 

//LED1任务函数
void led1_task(void *pvParameters)
{
    while(1)
    {
        LED1=0;
        vTaskDelay(200);
        LED1=1;
        vTaskDelay(800);
    }
}

//接收任务函数
void receive_task(void *pvParameters)
{
    BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
    u8 key=0;
    
    while(1)
    {
        key=KEY_Scan(0);
        if(key==KEY1_PRESS)
        {
            xReturn = xSemaphoreTake( CountSem_Handle,0 );//获取计数信号量,0是阻塞时间
            if( xReturn == pdTRUE )
                printf( "KEY1被按下,释放1个停车位。\r\n" );
            else
                printf( "KEY1被按下,但已无车位可以释放!\r\n" );
        }
        vTaskDelay(20);
    }
}

//发送任务函数
void send_task(void *pvParameters)
{
    BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
    u8 key=0;
    
    while(1)
    {
        key=KEY_Scan(0);
        if(key==KEY_UP_PRESS)
        {
            xReturn = xSemaphoreGive( CountSem_Handle );//给出计数信号量
            if( xReturn == pdTRUE )
                printf("KEY_UP被按下,成功申请到停车位。\r\n");
            else
                printf("KEY_UP被按下,不好意思,现在停车场已满!\r\n");
        }
        vTaskDelay(20);
    }
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值