目录
1. 信号量简介
1.1 同步和互斥
1.1.1 同步
1.1.2 互斥
1.1.3 总结
1.2 分类
1.2.1 二值信号量
1.2.2 计数信号量
1.2.3 互斥信号量
1.2.4 递归信号量
2. 信号量控制块
3. 常用信号量API函数
3.1 创建信号量函数
3.1.1 创建二值信号量 xSemaphoreCreateBinary()
3.1.2 创建计数信号量 xSemaphoreCreateCounting()
3.1.3 信号量删除函数 vSemaphoreDelete()
3.2 信号量释放函数
3.2.1 普通释放函数 xSemaphoreGive()
3.2.2 中断释放函数 xSemaphoreGiveFromISR()
3.3 信号量获取函数
3.3.1 普通获取函数 xSemaphoreTake()
3.3.2 中断获取函数 xSemaphoreTakeFromISR()
4. 代码编写
4.1 二值信号量
4.1.1 应用任务创建
任务1函数:
接收任务函数:
发送任务函数:
相关宏定义:
4.1.2 开始任务的创建
4.1.3 主函数
4.1.4 完整代码
4.2 计数信号量
4.2.1 应用任务创建
任务1函数:
接收任务函数:
发送任务函数:
相关宏定义:
4.1.2 开始任务的创建
4.1.3 主函数
4.1.4 完整代码
1. 信号量简介
信号量(Semaphore)是一种实现任务间通信的机制,可以实现任务之间同步或临界资源的互斥访问,常用于协助一组相互竞争的任务来访问临界资源。在多任务系统中,各任务之间需要同步或互斥实现临界资源的保护,信号量功能可以为用户提供这方面的支持。
1.1 同步和互斥
1.1.1 同步
定义:同步是指在执行任务时,通过某种机制来协调不同任务的行为,确保它们以正确的顺序和条件进行。简单来说,就是使得一个任务的执行进度能够与另一个任务的状态保持一致。
例子:通俗点解释,如果你去包子店想买包子,但店里暂时没有包子,你就需要等待店主做包子。这种等待就是一种同步行为,确保你在包子准备好之前不能买到包子。
又例如,你需要进行传感器数据采集,一个采集数据的传感器任务,一个处理数据的任务,你想要处理数据,则需要等待传感器先去采用数据,这种等待的行为就称为同步行为。
1.1.2 互斥
定义:互斥是指确保在任何时刻只有一个任务能够访问共享资源。它防止了多个任务同时访问共享资源,从而避免数据冲突或资源争用的问题。
例子:厕所(临界资源或者说是共享资源),人上厕所(执行的任务)
为了保证资源的合理使用,互斥机制确保在任何时刻只有一个人可以使用厕所。当一个人正在使用厕所时,其他人必须等待。
1.1.3 总结
同步是一种更为复杂的互斥,而互斥是一种特殊的同步。
例子:还是以上厕所为例。
厕所(临界资源或者说是共享资源),甲、乙、丙、丁四个人上厕所(执行的任务)
同步:相当于我给你规定了上厕所的顺序,甲、乙、丙、丁(任务)排队上厕所(共享资源),是有顺序的,例如甲执行完才是乙,乙执行完才是丙,丙执行完才是丁,你需要按照我给你排的顺序上厕所。
互斥:也是甲、乙、丙、丁(任务)排队上厕所(共享资源),不过并没有顺序,互斥就保证你们四个人就只能一个人到厕所里。但是你们谁先上我不管,我就保证你们在同一个时间内,只有一个人在厕所里(使用共享资源)。
1.2 分类
1.2.1 二值信号量
定义:二值信号量是一种特殊的信号量,其内部状态只有两种:被释放(释放状态)或被占用(占用状态)。二值信号量既可以用于临界资源访问,也可用于同步功能。
特点:只有两种状态(0 或 1),类似于一个长度为1的队列。
用途:同步,可以用来实现任务间的事件通知。
互斥,用于保护临界区,但不支持优先级继承机制。
示例:一个任务用二值信号量通知另一个任务某个事件的发生。
xSemaphoreGive(xBinarySemaphore); // 释放信号量
xSemaphoreTake(xBinarySemaphore, portMAX_DELAY); // 等待信号量
1.2.2 计数信号量
定义:计数信号量是一种信号量,其内部状态可以是任意非负整数。 它可以被认为长度大于1的队列,信号量使用者依然不必关心存储在队列中的消息,只需要关心队列是否有消息即可。
特点:可以有多个状态值,表示可用的资源数量。
用途:资源计数,用于表示可用资源的数量。例如,有限数量的资源池。
任务同步,用于控制任务之间的协调。
示例:一个任务使用计数信号量来限制对共享资源的访问,例如限制最大并发线程数。
xSemaphoreGive(xCountingSemaphore); // 增加信号量计数
xSemaphoreTake(xCountingSemaphore, portMAX_DELAY); // 减少信号量计数
1.2.3 互斥信号量
定义:互斥信号量其实是特殊的二值信号量,由于其特有的优先级继承机制从而使他更适用于简单互锁,也就是保护临界资源。
特点:当一个低优先级的任务持有互斥信号量时,如果一个高优先级的任务请求该信号量,低优先级任务的优先级会临时提升,以避免优先级反转问题。
用途:专门用于保护临界区和同步任务。
示例:使用互斥信号量来保护一个共享资源,确保在任何时刻只有一个任务可以访问该资源。
xSemaphoreTake(xMutex, portMAX_DELAY); // 获取互斥信号量
// 访问共享资源
xSemaphoreGive(xMutex); // 释放互斥信号量
1.2.4 递归信号量
定义:递归信号量允许同一任务多次获取相同的信号量,而不需要释放每次获取时都释放。递归信号量的使用场景通常是需要递归调用的任务或函数。
特点:递归获取,同一任务可以多次获取递归信号量,每次获取时信号量计数会增加。
递归释放,需要和获取次数相同次数的释放才能真正释放信号量,使得其他任务可以获取它。
用途:适用于递归函数或需要在同一任务中多次获取信号量的场景。
示例:一个任务递归调用函数时,使用递归信号量保护临界区。
xSemaphoreTakeRecursive(xRecursiveSemaphore, portMAX_DELAY); // 递归获取信号量
// 递归函数调用
xSemaphoreGiveRecursive(xRecursiveSemaphore); // 递归释放信号量
2. 信号量控制块
/*
* 定义调度器使用的队列。
* 项目通过复制而非引用排队。请参见以下链接了解详细原因:http://www.freertos.org/Embedded-RTOS-Queues.html
*/
typedef struct QueueDefinition
{
int8_t *pcHead; /*< 指向队列存储区域的开始位置。 */
int8_t *pcTail; /*< 指向队列存储区域末尾的字节。一次分配比实际需要的多一个字节,以作为标记。 */
int8_t *pcWriteTo; /*< 指向存储区域中下一个可写的位置。 */
union /* 使用联合体是为了确保两个互斥的结构成员不会同时出现(避免浪费内存)。 */
{
int8_t *pcReadFrom; /*< 当结构体被用作队列时,指向最后一个读取项目的位置。 */
UBaseType_t uxRecursiveCallCount; /*< 当结构体用作互斥量时,维护递归获取互斥量的次数。 */
} u;
List_t xTasksWaitingToSend; /*< 阻塞等待向此队列发送项目的任务列表,按优先级排序。 */
List_t xTasksWaitingToReceive; /*< 阻塞等待从此队列接收项目的任务列表,按优先级排序。 */
volatile UBaseType_t uxMessagesWaiting; /*< 当前队列中项目的数量。 */
UBaseType_t uxLength; /*< 队列的长度,定义为队列可以容纳的项目数,而不是字节数。 */
UBaseType_t uxItemSize; /*< 队列将持有的每个项目的大小。 */
volatile int8_t cRxLock; /*< 存储在队列锁定时从队列中接收的项目数量(从队列中移除)。队列未锁定时设置为 queueUNLOCKED。 */
volatile int8_t cTxLock; /*< 存储在队列锁定时传输到队列的项目数量(添加到队列中)。队列未锁定时设置为 queueUNLOCKED。 */
#if( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
uint8_t ucStaticallyAllocated; /*< 如果队列的内存是静态分配的,设置为 pdTRUE,以确保不尝试释放内存。 */
#endif
#if ( configUSE_QUEUE_SETS == 1 )
struct QueueDefinition *pxQueueSetContainer; /*< 如果启用了队列集功能,指向包含此队列的队列集。 */
#endif
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxQueueNumber; /*< 队列编号,用于跟踪功能。 */
uint8_t ucQueueType; /*< 队列类型,用于跟踪功能。 */
#endif
} xQUEUE;
/* 旧的 xQUEUE 名称在上面维护,然后重新定义为新的 Queue_t 名称,以便支持旧的内核调试工具。 */
typedef xQUEUE Queue_t;
3. 常用信号量API函数
3.1 创建信号量函数
3.1.1 创建二值信号量 xSemaphoreCreateBinary()
#define xSemaphoreCreateBinary()
xQueueGenericCreate( ( UBaseType_t ) 1,
semSEMAPHORE_QUEUE_ITEM_LENGTH,
queueQUEUE_TYPE_BINARY_SEMAPHORE )
( UBaseType_t ) 1:这是传递给 xQueueGenericCreate 函数的第一个参数,表示队列(或信号量)可以容纳的最大项数。在创建二进制信号量时,这里设置为 1,因为二进制信号量只需要一个项目来表示“有信号”或“无信号”。
semSEMAPHORE_QUEUE_ITEM_LENGTH:这是第二个参数,指定队列项的长度,这里设置为 0。对于二进制信号量,这个值通常是sizeof(xSemaphoreHandle)或其他相应长度。它定义了队列中每个项的大小。在实际实现中,这个值通常是一个宏,确保与信号量的数据结构对齐。但是在二值信号量,我们不关注它的消息内容是什么,只关心它的里面有没有消息,因此这个值设为0。
queueQUEUE_TYPE_BINARY_SEMAPHORE:这是第三个参数,指定队列的类型。在这里,queueQUEUE_TYPE_BINARY_SEMAPHORE表示这是一个二进制信号量。这个宏在 FreeRTOS 的内部实现中定义了队列的具体类型,帮助系统正确管理不同类型的队列和信号量。
简单来说,就是创建一个没有消息存储空间的队列。
3.1.2 创建计数信号量 xSemaphoreCreateCounting()
函数原型 | SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount, UBaseType_tuxInitialCount); | |
功能 | 创建一个计数信号量。 | |
参数 | uxMaxCount | 计数信号量的最大值,当达到这个值的时候,信号量不能再被释放。 |
uxInitialCount | 创建计数信号量的初始值。 | |
返回值 | 如果创建成功则返回一个计数信号量句柄,用于访问创建的计数信号量。如果创建不成功则返回 NULL。 |
3.1.3 信号量删除函数 vSemaphoreDelete()
函数原型 | void vSemaphoreDelete( SemaphoreHandle_t xSemaphore ); | |
功能 | 删除一个信号量 | |
参数 | xSemaphore | 信号量句柄 |
返回值 | 无 |
3.2 信号量释放函数
3.2.1 普通释放函数 xSemaphoreGive()
#define xSemaphoreGive( xSemaphore )
xQQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ),
NULL,
semGIVE_BLOCK_TIME,
queueSEND_TO_BACK)
3.2.2 中断释放函数 xSemaphoreGiveFromISR()
#define xSemaphoreGiveFromISR( xSemaphore, pxHigherPriorityTaskWoken )
xQueueGiveFromISR((QueueHandle_t)( xSemaphore ),
(pxHigherPriorityTaskWoken ))
3.3 信号量获取函数
3.3.1 普通获取函数 xSemaphoreTake()
函数原型 | #define xSemaphoreTake( xSemaphore, xBlockTime) xQueueGenericReceive( (QueueHandle_t )(xSemaphore ), NULL, ( xBlockTime ), pdFALSE) | |
功能 | 获取一个信号量,可以是二值信号量、计数信号量、互斥量。 | |
参数 | xSemaphore | 信号量句柄。 |
xBlockTime | 等待信号量可用的最大超时时间,单位为tick(即系统节拍周期)如果宏INCLUDE vTaskSuspend定义为1且形参xTicksToWait设置为portMAX_DELAY,则任务将一直阻塞在该信号量上(即没有超时时间) | |
返回值 | 获取成功则返回pTRUE,在指定的超时时间中没有获取成功则返回 errQUEUE_EMPTY。 |
3.3.2 中断获取函数 xSemaphoreTakeFromISR()
函数原型 | xSemaphoreTakeFromISR(SemahoreHale_t xmar, signed BaseType_t pxHigherPriorityTaskWoken) | |
功能 | 在中断中获一个信号量(其实很少在中断中获取信号量)。可以是二值信号量、计数信号量。 | |
参数 | xSemaphore | 信号量句柄。 |
pxHigherPriority TaskWoken | 一个或者多个任务有可能阻塞在同一个信号量上,调用函数SemaphoreTakeFromISRO会唤醒阻塞在该信号量上优先级最高的信号量入队任务,如果被唤醒的任务的优先级大于或者等于被中断的任务的优先级,那么形参pxHigherPriorityTaskWoken就会被设置为pdTRUE.然后在中断退出前执行一次上下文切换,中断退出后则直接返回刚刚被唤醒的高优先级的任务。从FreeRTOS V7.3.0版本开始, pxHigherPriorityTaskWoken是一个可选的参数,可以设置为 NULL。 | |
返回值 | 获取成功则返回pdTRUE,没有获取成功则返回errQUEUE_EMPTY,没有获取成功是因为信号量不可用。 |
4. 代码编写
4.1 二值信号量
首先,找到一个工程,这里我们以动态任务为例:
基于STM32F103C8T6的FreeRTOS任务创建·动态任务资源-CSDN文库
4.1.1 应用任务创建
任务1函数:
实现LED灯的闪烁:
//任务1函数
void led1_task(void *pvParameters)
{
while(1)
{
LED1=0;
vTaskDelay(200);
LED1=1;
vTaskDelay(800);
}
}
接收任务函数:
用于接收消息变量,当接受到发送任务函数发送的BinarySem_Handle,串口打印BinarySem_Handle二值信号量获取成功!,并实现led2的翻转:
//接收任务函数
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;
}
}
这里的任务不需要再调用vTaskDelay();进行延时阻塞让出CPU的资源,因为在:
//获取二值信号量 xSemaphore,没获取到则一直等待
xReturn = xSemaphoreTake(BinarySem_Handle,/* 二值信号量句柄 */
portMAX_DELAY); /* 等待时间 */
portMAX_DELAY表示无限等待,直到有消息可接收,已经进行了阻塞。
发送任务函数:
用于发送消息,当按键PB1按下给出二值信号量BinarySem_Handle,当按键PB11按下给出二值信号量BinarySem_Handle:
//发送任务函数
void send_task(void *pvParameters)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
uint8_t KeyNum;
while(1)
{
KeyNum=Key_GetNum();
if(KeyNum==1)
{
xReturn = xSemaphoreGive( BinarySem_Handle );//给出二值信号量
if( xReturn == pdTRUE )
printf("BinarySem_Handle二值信号量释放成功,按键1!\r\n");
else
printf("BinarySem_Handle二值信号量释放失败,按键1!\r\n");
}
else if(KeyNum==2)
{
xReturn = xSemaphoreGive( BinarySem_Handle );//给出二值信号量
if( xReturn == pdTRUE )
printf("BinarySem_Handle二值信号量释放成功,按键2!\r\n");
else
printf("BinarySem_Handle二值信号量释放失败,按键2!\r\n");
}
vTaskDelay(20);
}
}
相关宏定义:
//任务优先级
#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);
4.1.2 开始任务的创建
之前讲过,开始任务的作用就是将,应用任务的三个任务完整的建立起来,通过调用临界区的API,规避中断带来的风险,创建完后删除自身,只进行应用任务的调度:
//开始任务任务函数
void start_task(void *pvParameters)
{
taskENTER_CRITICAL(); //进入临界区
/* 创建 BinarySem */
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(); //退出临界区
}
消息队列的创建以及开始任务相关宏定义:
//任务优先级
#define START_TASK_PRIO 1
//任务堆栈大小
#define START_STK_SIZE 128
//任务句柄
TaskHandle_t StartTask_Handler;
//任务函数
void start_task(void *pvParameters);
SemaphoreHandle_t BinarySem_Handle =NULL;
4.1.3 主函数
创建开始任务开始调用:
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(); //开启任务调度
}
4.1.4 完整代码
基于STM32F103C8T6的FreeRTOS的二值信号量资源-CSDN文库
4.2 计数信号量
4.2.1 应用任务创建
任务1函数:
实现LED灯的闪烁:
//任务1函数
void led1_task(void *pvParameters)
{
while(1)
{
LED1=0;
vTaskDelay(200);
LED1=1;
vTaskDelay(800);
}
}
接收任务函数:
用于接收消息变量,当按键1按下,释放一个空间(停车位):
//接收任务函数
void receive_task(void *pvParameters)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS,值为1 */
uint8_t KeyNum;
while(1)
{
KeyNum=Key_GetNum();
if(KeyNum==1)
{
xReturn = xSemaphoreTake( CountSem_Handle,0 );//获取计数信号量
if( xReturn == pdTRUE )
printf( "KEY1被按下,释放1个停车位。\r\n" );
else
printf( "KEY1被按下,但已无车位可以释放!\r\n" );
}
vTaskDelay(20);
}
}
发送任务函数:
用于发送消息,当按键2按下,申请占用一个空间(停车位):
//发送任务函数
void send_task(void *pvParameters)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
uint8_t KeyNum;
while(1)
{
KeyNum=Key_GetNum();
if(KeyNum==2)
{
xReturn = xSemaphoreGive( CountSem_Handle );//给出计数信号量
if( xReturn == pdTRUE )
printf("KEY_UP被按下,成功申请到停车位。\r\n");
else
printf("KEY_UP被按下,不好意思,现在停车场已满!\r\n");
}
vTaskDelay(20);
}
}
相关宏定义:
//任务优先级
#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);
4.1.2 开始任务的创建
之前讲过,开始任务的作用就是将,应用任务的三个任务完整的建立起来,通过调用临界区的API,规避中断带来的风险,创建完后删除自身,只进行应用任务的调度:
//开始任务任务函数
void start_task(void *pvParameters)
{
taskENTER_CRITICAL(); //进入临界区
/* 创建 CountSem */
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(); //退出临界区
}
消息队列的创建以及开始任务相关宏定义:
//任务优先级
#define START_TASK_PRIO 1
//任务堆栈大小
#define START_STK_SIZE 128
//任务句柄
TaskHandle_t StartTask_Handler;
//任务函数
void start_task(void *pvParameters);
SemaphoreHandle_t CountSem_Handle =NULL;
4.1.3 主函数
创建开始任务开始调用:
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(); //开启任务调度
}
4.1.4 完整代码
基于STM32F103C8T6的FreeRTOS的计数信号量资源-CSDN文库
FreeRTOS_时光の尘的博客-CSDN博客