FreeRTOS-信号量

FreeRTOS-信号量

  • 信号量其实就是队列的一种应用,信号量的各种操作都是在队列的基础上建立起来的。那么既然是在队列的基础上建立的,信号量一定具有和队列相同的属性。因此信号量也是为任务和任务、任务和中断之间通信做准备的,但是信号量一般用来进行资源管理和任务同步。因为信号量是一种共享资源,当它被创建之后,系统中所有任务和中断都能对信号量进行访问。同时也可以进行任务同步,即在一个任务(或中断)中告诉另一个任务它所等待的事件发生了,等到发生任务调度的时候,再切换到相应任务中,执行该事件发生的相关处理。FreeRTOS中的信号量有二值信号量、互斥信号量、计数信号量、递归互斥信号量,我们这里只讲述前三个。

相关API

  • 信号量使用的API函数最核心的就是xQueueGenericCreate()、xQueueGenericSend()、xQueueGenericReceive()这三个函数,看到这三个函数是不是有点眼熟?没错,这就是上一章中队列所使用的三个函数,只不过在信号量中对其做了一些封装而已,庆幸的是我们上一章已经着重分析过这些函数了,所以本章将不会再长篇大论的去分析这些源码了。下面来看一下信号量中的函数是如何对其进行封装的。代码定义如下。
#define semSEMAPHORE_QUEUE_ITEM_LENGTH		( ( uint8_t ) 0U )
#define xSemaphoreCreateBinary() xQueueGenericCreate( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE )

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

#define xSemaphoreTake( xSemaphore, xBlockTime )		xQueueGenericReceive( ( QueueHandle_t ) ( xSemaphore ), NULL, ( xBlockTime ), pdFALSE )

  • xSemaphoreCreateBinary()是创建二值信号量的函数,从定义可见,该函数调用了xQueueGenericCreate(),只不过定义的队列长度是1,队列项大小为0。那么这样问题来了,队列项大小为0的队列怎么入队和出队呢?这个无需着急,接下来我们会详细分析。
  • xSemaphoreGive() 是发送信号量,只要是要发送信号量,不管是二值信号量、计数信号量、互斥信号量还是递归互斥信号量都是调用的这个函数。该函数调用了xQueueGenericSend()函数,从中可见,这时候入队的队列项是NULL,相应的将不会占用空间,所以上面在创建信号量的时候为其分配的空间是0也就能解释一部分了,但是问题还没有解决,既然队列始终是空的,那么如何来判断是否有信号发送呢?其实是根据uxMessagesWaiting这个变量进行判断的,因为即使我们入队的是一个空队列项,那么如果说这是候满足uxMessagesWaiting<uxLength这个条件,依旧会进行入队操作,既然会进行入队操作,那么根据我们上一章讲述的,uxMessagesWaiting必然会+1,如果这时候我们规定uxLength=1,那么uxMessagesWaiting只能在0到1之间取值,所以就相当于该信号量只有0、1两种状态,所以叫二值信号量。如果我们规定的uxLength>1,那么uxMessagesWaiting最大计数值就为uxLength,所以这时候就是计数信号量。
  • xSemaphoreTake()是接收信号量,和发送信号量一样,任何类型的信号量接收都会调用这个函数。该函数调用了xQueueGenericReceive()函数,输入参数为信号量句柄和阻塞时间,最后一个输入参数为pdFALSE,设置接收后是否要删除信号量中的信号,这里设置为删除。同样我们可以看到,接收的信号量的时候传入的接收数组也是NULL,对照上面的发送信号量可以知道,接收信号量其实也是操作uxMessagesWaiting的,因为这部分代码上章节中没有分析,这里简要分析一下源码。
...
//非主要源码忽略
...
if( uxMessagesWaiting > ( UBaseType_t ) 0 )---1
			{

				pcOriginalReadPosition = pxQueue->u.pcReadFrom;

				prvCopyDataFromQueue( pxQueue, pvBuffer );---2

				if( xJustPeeking == pdFALSE )---3
				{
					traceQUEUE_RECEIVE( pxQueue );

					/* Actually removing data, not just peeking. */
					pxQueue->uxMessagesWaiting = uxMessagesWaiting - 1;----4
    		 ...
		     //互斥信号量相关的暂时忽略
		     ...
		     
					if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )----5
					{
						if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE )
						{
							queueYIELD_IF_USING_PREEMPTION();
						}
						else
						{
							mtCOVERAGE_TEST_MARKER();
						}
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}
				}
				else//xJustPeeking == pdTRUE    ----6
				{
					traceQUEUE_PEEK( pxQueue );

					pxQueue->u.pcReadFrom = pcOriginalReadPosition;

					if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
					{
						if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
						{
							/* The task waiting has a higher priority than this task. */
							queueYIELD_IF_USING_PREEMPTION();
						}
						else
						{
							mtCOVERAGE_TEST_MARKER();
						}
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}
				}

				taskEXIT_CRITICAL();
				return pdPASS;----7
			}
else----8
{
...
//这一部分就是设置阻塞时间
...
}
  1. 队列不为空
  2. 将读取队列项,注意读取的时候是不会自动进行uxMessagesWaiting -1操作的
  3. 判断是否读取后要移除队列项
  4. 因为读取后要移除队列项,那么队列中队列项的数量-1
  5. 执行移除队列项操作
  6. 如果读取后不溢出队列项,那么相应的队列中队列项的数量不变
  7. 如果队列是空的,那么就没有办法读取队列项,所以设置阻塞时间,详细的代码上章节已经分析过了,这里不再分析了
  • 从上面的分析中可以看出,当要进行信号量读取的时候,本质上还是根据uxMessagesWaiting来操作的。
  • 因为计数信号量和互斥信号量创建的封装过程要比二值信号量稍微复杂一点,所以没在上面分析,下面我们就来分析这两种信号量的创建。
#define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount ) xQueueCreateCountingSemaphore( ( uxMaxCount ), ( uxInitialCount ) )
	
	QueueHandle_t xQueueCreateCountingSemaphore( const UBaseType_t uxMaxCount, const UBaseType_t uxInitialCount )
	{
	QueueHandle_t xHandle;

		configASSERT( uxMaxCount != 0 );
		configASSERT( uxInitialCount <= uxMaxCount );

		xHandle = xQueueGenericCreate( uxMaxCount, queueSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_COUNTING_SEMAPHORE );

		if( xHandle != NULL )
		{
			( ( Queue_t * ) xHandle )->uxMessagesWaiting = uxInitialCount;

			traceCREATE_COUNTING_SEMAPHORE();
		}
		else
		{
			traceCREATE_COUNTING_SEMAPHORE_FAILED();
		}

		return xHandle;
	}
  • xSemaphoreCreateCounting()其实是调用了xQueueCreateCountingSemaphore()函数,输入参数为最大计数量和初始计数值。在xQueueCreateCountingSemaphore()中我们可以看出,其实最终创建计数信号量还是调用了xQueueGenericCreate()函数,只不过创建的队列长度为uxMaxCount,初始时候uxMessagesWaiting 为uxInitialCount而已,其他和创建二值信号量并没有什么本质区别。下面再分析互斥信号量创建过程。
  • 互斥信号量创建的时候调用的函数是xQueueCreateMutex()函数源码如下。
QueueHandle_t xQueueCreateMutex( const uint8_t ucQueueType )
	{
	Queue_t *pxNewQueue;
	const UBaseType_t uxMutexLength = ( UBaseType_t ) 1, uxMutexSize = ( UBaseType_t ) 0;

		pxNewQueue = ( Queue_t * ) xQueueGenericCreate( uxMutexLength, uxMutexSize, ucQueueType );
		prvInitialiseMutex( pxNewQueue );

		return pxNewQueue;
	}
  • 从上面可以看出,创建互斥信号量依旧调用了xQueueGenericCreate()函数,并且对垒的定义与二值信号量相似,队列长度均为1。但是后面紧接着调用了函数prvInitialiseMutex()来初始化互斥信号量。下面来看一下该函数定义。
static void prvInitialiseMutex( Queue_t *pxNewQueue )
	{
		if( pxNewQueue != NULL )
		{

			pxNewQueue->pxMutexHolder = NULL;----1
			pxNewQueue->uxQueueType = queueQUEUE_IS_MUTEX;----2

			/* In case this is a recursive mutex. */
			pxNewQueue->u.uxRecursiveCallCount = 0;----3

			traceCREATE_MUTEX( pxNewQueue );

			/* Start with the semaphore in the expected state. */
			( void ) xQueueGenericSend( pxNewQueue, NULL, ( TickType_t ) 0U, queueSEND_TO_BACK );----4
		}
		else
		{
			traceCREATE_MUTEX_FAILED();
		}
	}
  • 注意,在Queue_t 结构体中并没有发现pxMutexHolder 、uxQueueType 这两个成员,那么为什么这里会有这两个成员变量呢,这里我们在quue.c文件中找到其相关定义如下。
#define pxMutexHolder					pcTail
#define uxQueueType						pcHead
#define queueQUEUE_IS_MUTEX				NULL
  • 从上面定义可以看出,其实就是对Queue_t 结构体中的一些成员进行了一个重命名,这样增强代码可读性。
  1. 初始化互斥信号量队列为指针
  2. 初始化队列的类型为互斥信号量
  3. 重置互斥信号量回调次数为0
  4. 调用信号量发送函数,表示初始时互斥信号量是有效的

  • 到这里我们就将这些相关的API介绍完了,从中可以看出,信号量和队列是水乳交融,密不可分的,信号量其实就是队列的一种应用,下面我们来讲述一下信号量的如何来使用。

二值信号量

实验目标:在中断和任务中分别实现二值信号量的发送和接收,
在任务中采用按键实现信号量的发送,在另一个任务中等待接
收信号量。在中断中,PC发送给串口信息,串口接收到信息后
将信号量发送给任务。
  • 创建二值信号量代码如下
 Binary_Handler = xSemaphoreCreateBinary();	
	 if (Binary_Handler == NULL)
	 {
		 printf("Semaphore Binary Creat Failed!!!\r\n");
	 }
  • 创建按键任务,当按键按下时发送信号量。函数定义如下
void key_task(void* pvParameters)
{
    BaseType_t err;
	while(1)
	{
		if (Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON)
		{
			err = xSemaphoreGive(Binary_Handler);//发送新信号量
			if (err != pdTRUE)//判断信号量是否发送成功
			{
				printf("信号量发送失败!\r\n");
			}
		}
		vTaskDelay(10);
	}
}
  • 创建信号量接收函数,接收按键任务和中断发送的信号量。定义如下
void task2_task(void* pvParameters)
{
	u8 count = 0;
	BaseType_t err;
	while(1)
	{
		count++;//记录信号量接收次数
		if (Binary_Handler != NULL)//判断句柄有效性
		{
			err = xSemaphoreTake(Binary_Handler,portMAX_DELAY);//接收信号量
			if (err != pdTRUE)//判断是否接收成功
			{
				printf("接收信号量失败!!\r\n");
			}
			else//LED翻转 并且输出接收到的值
			{
				LED1 = ~LED1;
				printf("count = %d   Receive %s\r\n",count,USART_RX_BUF);
				memset(USART_RX_BUF,0,USART_REC_LEN);
				USART_RX_STA = 0;
			}
		}
		else
		{
			vTaskDelay(10);
		}

	}
}
  • 在串口中断服务函数中我们做以下更改。代码如下。
void USART1_IRQHandler(void)                	//串口1中断服务程序
{
	u8 Res;
	BaseType_t  HigherPriorityTaskWoken=pdFALSE;//用以判断是否需要进行任务切换
	BaseType_t 	err;
	
	
	if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)  //接收中断(接收到的数据必须是0x0d 0x0a结尾)
		{
		Res =USART_ReceiveData(USART1);	//读取接收到的数据
		
		if((USART_RX_STA&0x8000)==0)//接收未完成
			{
			if(USART_RX_STA&0x4000)//接收到了0x0d
				{
				if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始
				else USART_RX_STA|=0x8000;	//接收完成了 
				}
			else //还没收到0X0D
				{	
				if(Res==0x0d)USART_RX_STA|=0x4000;
				else
					{
					USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;
					USART_RX_STA++;
					if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//接收数据错误,重新开始接收	  
					}		 
				}
			}   		 
     } 
		
	if ( (Binary_Handler!=NULL) && (USART_RX_STA&0x8000) )//判断是否接收完成
	{
		err = xSemaphoreGiveFromISR(Binary_Handler,&HigherPriorityTaskWoken);//从中断中发送信号量
		if (err != pdTRUE)//判断是否出错
		{
			printf("Semaphore Give Failed!!!\r\n");
		}
		portYIELD_FROM_ISR(HigherPriorityTaskWoken);//根据HigherPriorityTaskWoken判断是否进行任务切换
		
	}

} 

优先级翻转

  • 上面使用二值信号量似乎没什么问题,但是在某些情况下,二值信号量可能会导致任务的优先级发生翻转,什么叫任务优先级翻转?假设我们这里有三个任务,分别为low_task、middle_task、high_task,且优先级依次升高,这里有一种可能,就是当高优先级的任务请求信号量进入阻塞状态,而低优先级任务一直控制着信号量不发出,这时,中等优先级的任务恰巧进入了准备态,所以中等优先级的任务就会打断低优先级任务,使得中等优先级的任务先执行,待其执行完毕后,再回到低优先级中,直到低优先级任务释放信号量后,高优先级的任务才能收到信号量而解除阻塞,这中间有一段时间导致中等优先级的任务优先级高于高优先级的任务优先级。如下图所示
    在这里插入图片描述
  • 接下来就用代码测试一下优先级翻转。首先定义三个任务,且优先级依次从小到达,定义如下
#define LOW_TASK_PRIO  2
#define LOW_STACK_SIZE 50
TaskHandle_t LowTask_Handler;
void low_task(void *pvParameters);

#define MIDDLE_TASK_PRIO  3
#define MIDDLE_STACK_SIZE 50
TaskHandle_t MiddleTask_Handler;
void middle_task(void *pvParameters);

#define HIGH_TASK_PRIO  4
#define HIGH_STACK_SIZE 50
TaskHandle_t HighTask_Handler;
void high_task(void *pvParameters);

void  low_task(void* pvParameters)
{
	long i;
	while(1)
	{
		printf("low task running\r\n");
		xSemaphoreTake(Binary_Handler,portMAX_DELAY);
		for (i=0; i<22222222; i++);
		xSemaphoreGive(Binary_Handler);
		printf("low task give semaphore\r\n");
		vTaskDelay(1000);
	}
}

void  middle_task(void* pvParameters)
{
	while(1)
	{
		printf("middle task running\r\n");
		vTaskDelay(1000);
	}
}

void  high_task(void* pvParameters)
{
	while(1)
	{
		printf("high task ask the semaphore\r\n");
		xSemaphoreTake(Binary_Handler,portMAX_DELAY);
		printf("high task running\r\n");
		xSemaphoreGive(Binary_Handler);
		vTaskDelay(1000);
	}
}
  • 接下来用在start_task任务中创建信号量和任务
void start_task(void *pvParameters)
{				

   Binary_Handler = xSemaphoreCreateBinary();	
	
	 if (Binary_Handler == NULL)
	 {
		 printf("Semaphore Binary Creat Failed!!!\r\n");
	 }
	  xSemaphoreGive(Binary_Handler);

	 xTaskCreate((TaskFunction_t )high_task,            //任务函数
                (const char*    )"high_task",          //任务名称
                (uint16_t       )HIGH_STACK_SIZE,        //任务堆栈大小
                (void*          )NULL,                  //传递给任务函数的参数
                (UBaseType_t    )HIGH_TASK_PRIO,       //任务优先级
                (TaskHandle_t*  )&HighTask_Handler);   //任务句柄 
	 
	 xTaskCreate((TaskFunction_t )middle_task,            //任务函数
                (const char*    )"middle_task",          //任务名称
                (uint16_t       )MIDDLE_STACK_SIZE,        //任务堆栈大小
                (void*          )NULL,                  //传递给任务函数的参数
                (UBaseType_t    )MIDDLE_TASK_PRIO,       //任务优先级
                (TaskHandle_t*  )&MiddleTask_Handler);   //任务句柄  
								
   xTaskCreate((TaskFunction_t )low_task,            //任务函数
                (const char*    )"low_task",          //任务名称
                (uint16_t       )LOW_STACK_SIZE,        //任务堆栈大小
                (void*          )NULL,                  //传递给任务函数的参数
                (UBaseType_t    )LOW_TASK_PRIO,       //任务优先级
                (TaskHandle_t*  )&LowTask_Handler);   //任务句柄  	 

	 vTaskDelete(StartTask_Handler);
}

  • 核心代码就这些,下面来看一下运行结果
    在这里插入图片描述
  • 从结果可以看出,其中有一段时间高优先级的任务一直在请求信号量而进入阻塞,这时中等优先级的任务会打断低优先级的任务而率先执行,高优先级的任务一直等待低优先级的任务释放信号量,相当于高优先级的任务优先级低了。

互斥信号量

  • 从上面可以看出,如果发生优先级翻转了,那么将不能保证高优先级的任务总是被率先执行,从而严重影响了系统的实时性,所以为了解决这个问题,FreeRTOS又提供了互斥信号量。互斥信号量相比于二值信号量,很大的改善了优先级翻转的问题,当高优先级任务等待低优先级的任务是释放信号量而进入阻塞状态时,会将低优先级任务的任务优先级提高到与高优先级一样,确保低优先级任务先执行,从而尽早释放信号量,释放信号量后,再将低优先级任务的优先级复原。这就是互斥信号量降低优先级翻转发生概率的原理。那么接下来看一下使用互斥信号量是否能有效地解决优先级翻转问题。
  • 同上面代码不变,这里仅将start_task中的信号量创建该为互斥信号量,代码如下。
void start_task(void *pvParameters)
{				

   Mutex_Handler = xSemaphoreCreateMutex();	
	
	 if (Mutex_Handler == NULL)
	 {
		 printf("Semaphore Binary Creat Failed!!!\r\n");
	 }
	  xSemaphoreGive(Mutex_Handler);

	 xTaskCreate((TaskFunction_t )high_task,            //任务函数
                (const char*    )"high_task",          //任务名称
                (uint16_t       )HIGH_STACK_SIZE,        //任务堆栈大小
                (void*          )NULL,                  //传递给任务函数的参数
                (UBaseType_t    )HIGH_TASK_PRIO,       //任务优先级
                (TaskHandle_t*  )&HighTask_Handler);   //任务句柄 
	 
	 xTaskCreate((TaskFunction_t )middle_task,            //任务函数
                (const char*    )"middle_task",          //任务名称
                (uint16_t       )MIDDLE_STACK_SIZE,        //任务堆栈大小
                (void*          )NULL,                  //传递给任务函数的参数
                (UBaseType_t    )MIDDLE_TASK_PRIO,       //任务优先级
                (TaskHandle_t*  )&MiddleTask_Handler);   //任务句柄  
								
   xTaskCreate((TaskFunction_t )low_task,            //任务函数
                (const char*    )"low_task",          //任务名称
                (uint16_t       )LOW_STACK_SIZE,        //任务堆栈大小
                (void*          )NULL,                  //传递给任务函数的参数
                (UBaseType_t    )LOW_TASK_PRIO,       //任务优先级
                (TaskHandle_t*  )&LowTask_Handler);   //任务句柄  	   	

	 vTaskDelete(StartTask_Handler);
}

  • 运行结果如下
    在这里插入图片描述
  • 从上图可以看出,使用互斥信号量能够解决优先级翻转问题,保证高优先级任务最先执行

计数信号量

  • 所谓计数型信号量,就是当有任务发送信号量的时候,uxMessagesWaiting +1(但必须满足uxMessagesWaiting <uxLength),当有任务请求接收信号量的时候,uxMessagesWaiting -1。这就是计数型信号量,但是要注意计数型信号量的计数值必须在0-uxLength之间。下面是计数型信号量的测试代码(通过按键发送信号量,另一个任务定时获取信号量)。

  • 按键发送信号量


void key_task(void* pvParameters)
{
	
	while(1)
	{
		if (Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON)
		{
			xSemaphoreGive(Count_Handler);
			//count = uxSemaphoreGetCount(Count_Handler);
			//printf("count=%ld\r\n",count);
		}
		
		if (Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON)
		{
		   xSemaphoreTake(Count_Handler,10);
		}
		vTaskDelay(10);
	}
}
  • 任务定时接收信号量
void task2_task(void* pvParameters)
{
	
	//BaseType_t err;
	while(1)
	{
		count = uxSemaphoreGetCount(Count_Handler);
		printf("count=%ld\r\n",count);
		if (Count_Handler != NULL)
		{
			if (count > 5)
			{
				LED1 = 0;
			}
			else
			{
				LED1 = 1;
			}
		}
		vTaskDelay(100);
	}
}
  • 具体代码就不详细说明了,用法比较简单。

  • 到这里就将FreeRTOS中的信号量讨论完了,主要讨论了二值信号量、互斥信号量和计数信号量,这几种信号量使用方法大同小异,其中互斥信号量能够很大的弥补二值信号量可能导致的优先级翻转问题。
  • 8
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
FreeRTOS中,计数信号量是一种同步机制,用于在多个任务之间传递和管理资源的访问。当一个任务需要访问某个资源时,它需要获取计数信号量,如果计数信号量的计数大于0,那么任务可以继续执行,计数信号量的计数减1。如果计数信号量的计数为0,则任务会被阻塞,直到有其他任务释放了计数信号量并将其计数增加。 要读取计数信号量的值,可以使用FreeRTOS提供的API函数xSemaphoreGetCount。这个函数返回计数信号量的当前计数值。需要注意的是,计数信号量的计数值仅仅表示资源的可用数量,并不保证是准确的,因为在多任务环境中,计数信号量的计数可能会在任务切换的过程中发生变化。 在使用计数信号量时,需要注意以下几个方面: . 在需要使用计数信号量的任务中,首先要创建一个计数信号量的句柄,使用xSemaphoreCreateCounting函数来创建,指定计数信号量的最大计数值和初始计数值。 2. 在任务中使用xSemaphoreTake函数获取计数信号量,如果计数信号量的计数大于0,任务可以继续执行,否则任务会被阻塞,直到有其他任务释放计数信号量。 3. 在其他任务中,可以使用xSemaphoreGive函数释放计数信号量,并将其计数增加。 综上所述,要读取FreeRTOS计数信号量的值,可以使用xSemaphoreGetCount函数。但需要注意计数信号量的计数值只是表示资源的可用数量,并不保证是准确的。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [FreeRTOS(4)----信号量](https://blog.csdn.net/weixin_63032791/article/details/130773792)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [FreeRTOS学习------信号量(实践)](https://blog.csdn.net/Freelifewe/article/details/64907349)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值