FreeRTOS教程7 事件组

1、准备材料

正点原子stm32f407探索者开发板V2.4

STM32CubeMX软件(Version 6.10.0

Keil µVision5 IDE(MDK-Arm

野火DAP仿真器

XCOM V2.6串口助手

2、学习目标

本文主要学习 FreeRTOS 事件组的相关知识,包括事件组概述、事件组特征、创建事件组、操作事件组、删除事件组等知识

3、前提知识

3.1、什么是事件组?

事件组(event group)也是FreeRTOS中另外一种进程间通信技术,事件组适用于多个事件触发一个或多个任务运行,可以实现事件的广播,还可以实现多个任务的同步运行,如下所述

  • 事件组允许任务等待一个或多个事件的组合
  • 事件组会解除所有等待同一事件的任务的阻塞状态

3.1、事件组特征

3.1.1、事件组、事件标志和事件位

事件 “标志” 是一个布尔值(1 或 0),用于指示事件是否发生,事件 “组” 是一组事件标志,事件标志只能为 1 或 0 ,允许事件标志的状态存储在单个位中,并且事件组中所有事件标志的状态存储在单个变量中

事件组中每个事件标志的状态由 EventBits_t 类型变量中的单个位表示。因此,事件标志也称为事件 “位” ,如果 EventBits_t 变量中的某个位设置为 1 ,则该位表示的事件已发生,否则如果 EventBits_t 变量中的某个位设置为 0 ,则该位表示的事件尚未发生

如下图所示显示了各个事件标志如何映射到 EventBits_t 类型变量中的各个位 (注释1)

3.1.2、EventBits_t 数据类型

一个事件组对象有一个变量类型为 EventBits_t 的内部变量用于存储事件标志位,该变量可以设置为 16 位或 32 位,具体由参数 configUSE_16_BIT_TICKS 所决定,当参数设置为 1 时,那么每个事件组包含 8 个可用的事件位(包括 8 个保留位),否则设置为 0 时,每个事件组包含 24 个可用的事件位(包括 8 个保留位)

3.1.3、多个任务访问

事件组本身就是对象,任何知道其存在的任务或 ISR 都可以访问它们。任意数量的任务可以在同一事件组中设置位,并且任意数量的任务可以从同一事件组中读取位

3.2、创建事件组

一个事件组在使用之前必须先创建,如下所示为使用动态/静态内存分配创建一个事件组的 API 函数

/**
  * @brief  动态分配内存创建事件组函数
  * @retval 返回成功创建的事件组的句柄,返回NULL表示因内存空间不足创建失败
  */
EventGroupHandle_t xEventGroupCreate(void);

/**
  * @brief  静态分配内存创建事件组函数
  * @param  pxEventGroupBuffer:指向StaticEventGroup_t类型的变量,该变量用于存储事件组数据结构体
  * @retval 返回成功创建的事件组的句柄,返回NULL表示因pxEventGroupBuffer空间不足创建失败
  */
EventGroupHandle_t xEventGroupCreateStatic(
								StaticEventGroup_t *pxEventGroupBuffer);

3.3、操作事件组

FreeRTOS 提供了两组 API 来对事件组的某些位进行置位和清零两种操作,具体如下所示

/**
  * @brief  将事件组某些位置位
  * @param  xEventGroup:要设置位的事件组
  * @param  uxBitsToSet:指定要在事件组中设置的一个或多个位的按位值,例如设置为0x09表示置位3 和位0
  * @retval 调用 xEventGroupSetBits()返回时事件组的值
  */
EventBits_t xEventGroupSetBits(EventGroupHandle_t xEventGroup,
							   const EventBits_t uxBitsToSet);

/**
  * @brief  将事件组某些位清零
  * @param  xEventGroup:要在其中清除位的事件组
  * @param  uxBitsToSet:表示要在事件组中清除一个或多个位的按位值
  * @retval 返回清除指定位之前的事件组的值
  */
EventBits_t xEventGroupClearBits(EventGroupHandle_t xEventGroup,
								 const EventBits_t uxBitsToClear);

/**
  * @brief  上述两个函数的中断安全版本
  * @param  pxHigherPriorityTaskWoken:用于通知应用程序编写者是否应该执行上下文切换
  * @retval 消息已发送到RTOS守护进程任务,则返回pdPASS,否则将返回pdFAIL
  */
BaseType_t xEventGroupSetBitsFromISR(EventGroupHandle_t xEventGroup,
									 const EventBits_t uxBitsToSet,
									 BaseType_t *pxHigherPriorityTaskWoken);

BaseType_t xEventGroupClearBitsFromISR(EventGroupHandle_t xEventGroup,
									   const EventBits_t uxBitsToClear);

/*example1: 将事件组 EventGroup_Test 的位 1 和 3 置位*/
EventBits_t return_value;
return_value = xEventGroupSetBits(EventGroup_Test, 0x0A);

/*example2: 将事件组 EventGroup_Test 的位 0 和 2 清零*/
EventBits_t return_value;
return_value = xEventGroupClearBits(EventGroup_Test, 0x05);

同时 FreeRTOS 也提供了查询事件组当前值的 API 函数,具体如下所示

/**
  * @brief  读取事件组的当前值
  * @param  xEventGroup:正在查询的事件组
  * @retval 返回事件组当前的值
  */
EventBits_t xEventGroupGetBits(EventGroupHandle_t xEventGroup);

/**
  * @brief  上述函数的中断安全版本
  */
EventBits_t xEventGroupGetBitsFromISR(EventGroupHandle_t xEventGroup);

3.4、xEventGroupWaitBits() API 函数

FreeRTOS 关于事件组提出了等待事件组和事件组同步两个比较重要的 API 函数,分别对应两种不同的使用场景,等待事件组主要用于使用事件组进行事件的管理,而另外一主要用于使用事件组进行任务间的同步,接下来主要详细介绍两个函数的具体用法

xEventGroupWaitBits() API 函数允许任务读取事件组的值,并且可以选择在阻塞状态下等待事件组中的一个或多个事件位被设置(如果事件位尚未设置),如下所示为其具体的函数声明

/**
  * @brief  等待事件组中多个事件位表示的事件成立
  * @param  xEventGroup:所操作事件组的句柄
  * @param  uxBitsToWaitFor:所等待事件位的掩码,例如设置为0x05表示等待第0位和/或第2位
  * @param  xClearOnExit:pdTRUE表示事件组条件成立退出阻塞状态时将掩码指定的所有位清零;pdFALSE表示事件组条件成立退出阻塞状态时不将掩码指定的所有位清零
  * @param  xWaitForAllBits:pdTRUE表示等待掩码中所有事件位都置1,条件才算成立(逻辑与);pdFALSE表示等待掩码中所有事件位中一个置1,条件就成立(逻辑或)
  * @param  xTicksToWait:任务进入阻塞状态等待时间成立的超时节拍数
  * @retval 返回事件位等待完成设置或阻塞时间过期时的事件组值
  */
EventBits_t xEventGroupWaitBits(const EventGroupHandle_t xEventGroup,
								const EventBits_t uxBitsToWaitFor,
								const BaseType_t xClearOnExit,
								const BaseType_t xWaitForAllBits,
								TickType_t xTicksToWait);

3.4.1、uxBitsToWaitForxWaitForAllBits 参数

调度程序用来确定任务是否进入阻塞状态以及任务何时离开阻塞状态的条件称为 “解除阻塞条件” 。解锁条件由 uxBitsToWaitForxWaitForAllBits 参数值的组合指定:

  • uxBitsToWaitFor 指定要测试事件组中的哪些事件位
  • xWaitForAllBits 指定是使用按位 OR 测试还是按位 AND 测试

如果调用 xEventGroupWaitBits() 时满足解锁条件,任务将不会进入阻塞状态,下表提供了导致任务进入阻塞状态或退出阻塞状态的条件示例。表中列出的值仅显示事件组和 uxBitsToWaitFor 值的最低有效的四个二进制位,其他位均假定为零

现有事件组值 uxBitsToWaitFor xWaitForAllBits 导致的结果
0000 0101 pdFALSE 由于事件组中的位 0 或位 2 均未设置,调用任务将进入阻塞状态,并且当事件组中的位 0 或位 2 被设置时,调用任务将离开阻塞状态
0100 0101 pdTRUE 调用任务将进入阻塞状态,因为事件组中的位 0 和位 2 未同时设置,并且当事件组中的位 0 和位 2 均设置时,调用任务将离开阻塞状态
0100 0110 pdFALSE 调用任务不会进入阻塞状态,因为 xWaitForAllBits 为 pdFALSE,并且 uxBitsToWaitFor 指定的两个位之一已在事件组中设置
0100 0110 pdTRUE 调用任务将进入阻塞状态,因为 xWaitForAllBits 为pdTRUE,并且事件组中仅已设置 uxBitsToWaitFor 指定的两个位之一。 当事件组中的位 2 和位 3 均被设置时,任务将离开阻塞状态

3.4.2、xClearOnExit 参数

调用任务使用 uxBitsToWaitFor 参数指定要测试的位,并且调用任务可能需要在满足其解锁条件后将这些位清零。可以使用 xEventGroupClearBits() API 函数清除事件位,但使用该函数手动清除事件位将导致应用程序代码中出现竞争条件

因此提供 xClearOnExit 参数就是为了避免这些潜在的竞争条件。如果 xClearOnExit 设置为 pdTRUE,则事件位的测试和清除对于调用任务来说是一个原子操作(不能被其他任务或中断中断),简单来说就是如果 xClearOnExit 设置为 pdTRUE,则调用任务退出后会将事件组所有位清零,否则不清零

如果 xEventGroupWaitBits() 由于满足调用任务的解锁条件而返回,则返回值是满足调用任务的解锁条件时事件组的值(如果 xClearOnExit 为 pdTRUE,则在自动清除任何位之前),在这种情况下,返回值也将满足解锁条件。如果 xEventGroupWaitBits() 因为 xTicksToWait 参数指定的退出阻塞时间到期而返回,则返回值为退出阻塞时间到期时事件组的值,在这种情况下,返回值将不满足解锁条件

3.5、xEventGroupSync() API 函数

提供 xEventGroupSync() 是为了允许两个或多个任务使用事件组来相互同步。该函数允许任务设置事件组中的一个或多个事件位,然后等待同一事件组中指定的事件位组合被设置

如下所示为 xEventGroupSync() API 函数的具体声明

/**
  * @brief  事件组同步
  * @param  uxBitsToSet:设置和测试位的事件组
  * @param  uxBitsToWaitFor:指定事件组中要测试的一个或多个事件位的按位值
  * @param  xTicksToWait:任务进入阻塞状态等待时间成立的超时节拍数
  * @retval 返回函数退出时事件组的值
  */
EventBits_t xEventGroupSync(EventGroupHandle_t xEventGroup,
							const EventBits_t uxBitsToSet,
							const EventBits_t uxBitsToWaitFor,
							TickType_t xTicksToWait);

3.5.1、函数返回值

xEventGroupSync() 函数返回函数退出时事件组的值,可能有以下两种情况

  1. xEventGroupSync() 函数的 uxBitsToWaitFor 参数指定了调用任务的解锁条件,如果该函数由于满足解锁条件而返回,则 uxBitsToWaitFor 指定的事件位将在 xEventGroupSync() 返回之前清回零,并且在自动清为零之前会将事件组的值作为函数返回值返回

  2. 如果 xEventGroupSync() 由于 xTicksToWait 参数指定的阻塞时间到期而返回,则返回值为阻塞时间到期时事件组的值,在这种情况下,返回值将不满足调用任务的解锁条件

3.5.2、应用举例

举个简单的例子就容易理解:

假设目前有两个任务,分别为 TASK1 和 TASK2 ,如果 TASK1 被执行过程中因为延时等原因先于 TASK2 调用了 xEventGroupSync() 函数,参数 uxBitsToSet 被设置为 0x01(0000 0001),参数 uxBitsToWaitFor 被设置为 0x05(0000 0101),则 TASK1 执行到该函数时会将事件组中位 0 的值置 1 ,然后进入阻塞状态,等待位 2 和位 0 同时被置 1 ;

如果 TASK2 与 TASK1 一样,只不过落后于 TASK1 执行 xEventGroupSync() 函数,并且参数 uxBitsToSet 被设置为 0x04(0000 0100),当 TASK2 执行该函数时会将事件组中位 2 的值置 1 ,此时满足解锁条件,所以 TASK2 不会进入阻塞状态,同时 TASK1 也满足解锁条件,从阻塞状态中退出,这时候假设任务优先级一致,则 TASK1 和 TASK2 会同时从同步点开始运行后续的程序代码,从而达到同步的目的

3.5、删除事件组

/**
  * @brief  删除事件组
  * @param  xEventGroup:要删除事件组的句柄
  * @retval None
  */
void vEventGroupDelete(EventGroupHandle_t xEventGroup);

4、实验一:使用事件组进行事件管理

4.1、实验目标

Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf 手册第 “8.3” 小节最后介绍了关于事件组的事件管理示例 22 ,这里我们来复现一下

  1. 创建一个用于演示本实验的事件组 xEventGroup
  2. 创建一个负责将事件组 xEventGroup 位 0 和位 1 置位的任务 Task_SetBits
  3. 启动 RTC 1s 周期唤醒,在 RTC 周期唤醒回调函数中负责将事件组 xEventGroup 位 0 置位
  4. 创建一个负责等待事件组位 0 或位 1 或位 2 满足条件的任务 Task_ReadBits

4.2、CubeMX相关配置

首先读者应按照 "FreeRTOS教程1 基础知识 "章节配置一个可以正常编译通过的 FreeRTOS 空工程,然后在此空工程的基础上增加本实验所提出的要求

本实验需要初始化 USART1 作为输出信息渠道,具体配置步骤请阅读 “STM32CubeMX教程9 USART/UART 异步通信” ,如下图所示

本实验需要配置 RTC 周期唤醒中断,具体配置步骤和参数介绍读者可阅读”STM32CubeMX教程10 RTC 实时时钟 - 周期唤醒、闹钟A/B事件和备份寄存器“实验,此处不再赘述,这里参数、时钟配置如下图所示

由于需要在 RTC 周期唤醒中断中使用 FreeRTOS 的 API 函数,因此 RTC 周期唤醒中断的优先级应该设置在 15~5 之间,此处设置为 7 ,具体如下图所示

单击 Middleware and Software Packs/FREERTOS,在 Configuration 中单击 Tasks and Queues 选项卡,双击默认任务按任务 Task_SetBits 修改其参数,然后增加另外一个 Task_ReadBits 任务,具体如下图所示

然后在 Configuration 中单击 Events 选项卡,单击右下角的 Add 按钮增加一个事件组 xEventGroup ,具体如下图所示

配置 Clock Configuration 和 Project Manager 两个页面,接下来直接单击 GENERATE CODE 按钮生成工程代码即可

4.3、添加其他必要代码

按照 “STM32CubeMX教程9 USART/UART 异步通信” 实验 “6、串口printf重定向” 小节增加串口 printf 重定向代码,具体不再赘述

首先应该在 freertos.c 中添加信号量的头文件和定义需要用到的事件组位的宏定义,如下所述

/*freertos.c中添加头文件*/
#include "stdio.h"
#include "event_groups.h"

/*事件组位宏定义*/
#define mainFIRST_TASK_BIT 	( 1UL << 0UL ) /* 事件组位 0 */
#define mainSECOND_TASK_BIT ( 1UL << 1UL ) /* 事件组位 1 */
#define mainISR_BIT         ( 1UL << 2UL ) /* 事件组位 2 */

然后在该文件中重新实现周期唤醒回调函数,该函数用于 1s 周期将事件组 xEventGroup 的位 2 置 1 ,具体如下所示

/*周期唤醒回调函数*/
void HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc)
{
	/* 输出信息提示 */
	printf("Bit setting ISR -\t about to set bit 2.\r\n");
	/* 从中断中设置事件组位 2 为 1 */
	BaseType_t xHigherPriorityTaskWoken = pdFALSE;
	xEventGroupSetBitsFromISR(xEventGroupHandle, mainISR_BIT, &xHigherPriorityTaskWoken);
	portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

最后仍然在该文件中实现任务 Task_SetBits 和任务 Task_ReadBits 两个任务函数体即可,具体如下所示

/*事件组置位任务*/
void AppTask_SetBits(void *argument)
{
  /* USER CODE BEGIN AppTask_SetBits */
	/* 400ms延时变量 */
	const TickType_t xDelay400ms = pdMS_TO_TICKS(400UL);
	/* Infinite loop */
	for(;;)
	{
		/* 在下次循环开始之前短延时 */
		vTaskDelay(xDelay400ms);
		/* 输出事件组位 0 被置位任务置 1 信息 */
		printf("Bit setting task -\t about to set bit 0.\r\n");
		xEventGroupSetBits(xEventGroupHandle, mainFIRST_TASK_BIT);
		/* 在置位下一位之前短延时 */
		vTaskDelay(xDelay400ms);
		/* 输出事件组位 1 被置位任务置 1 信息 */
		printf("Bit setting task -\t about to set bit 1.\r\n");
		xEventGroupSetBits(xEventGroupHandle, mainSECOND_TASK_BIT);
	}
	/* USER CODE END AppTask_SetBits */
}

/*事件组读取任务*/
void AppTask_ReadBits(void *argument)
{
	/* USER CODE BEGIN AppTask_ReadBits */
	/* 创建事件组 */
	EventBits_t xEventGroupValue;
	/* 设置要测试的位 */
	const EventBits_t xBitsToWaitFor = (mainFIRST_TASK_BIT |
										mainSECOND_TASK_BIT | 
										mainISR_BIT);
	/* Infinite loop */
	for(;;)
	{
	xEventGroupValue = xEventGroupWaitBits( 
							/* 被读的事件组 */
							xEventGroupHandle,
							/* 要测试的位 */
							xBitsToWaitFor,
							/* 阻塞条件满足退出时清除所有事件位 */
							pdTRUE,
							/* 不等待所有位. */
							pdFALSE,
							/* 永远等待,不会超时 */
							portMAX_DELAY);
		/* 位 0 被置 1 */
		if((xEventGroupValue & mainFIRST_TASK_BIT) != 0)
		{
			printf("Bit reading task -\t Event bit 0 was set\r\n");
		}
		/* 位 1 被置 1 */
		if((xEventGroupValue & mainSECOND_TASK_BIT ) != 0 )
		{
			printf("Bit reading task -\t Event bit 1 was set\r\n");
		}
		/* 位 2 被置 1 */
		if((xEventGroupValue & mainISR_BIT ) != 0 )
		{
			printf("Bit reading task -\t Event bit 2 was set\r\n");
		}
	}
	/* USER CODE END AppTask_ReadBits */
}

4.4、烧录验证

烧录程序,在 xEventGroupWaitBits() 函数 xWaitForAllBits 参数设置为 pdFALSE 的情况下串口产生的输出信息如下图所示

从图中可可以看出,因为对 xEventGroupWaitBits() 的调用中的 xWaitForAllBits 参数设置为 pdFALSE, 每次设置任何事件位时,从事件组读取的任务都会离开阻塞状态并立即执行

4.5、测试 xWaitForAllBits 参数

将任务 AppTask_ReadBits() 调用的 xEventGroupWaitBits() 函数 xWaitForAllBits 参数设置为 pdTRUE,表示需要等待所有事件组测试位满足才能离开阻塞状态,这种情况下串口产生的输出如下图所示

在上图中可以看出,由于 xWaitForAllBits 参数设置为 pdTRUE,从事件组读取的任务仅在所有三个事件位均置 1 后才可以离开阻塞状态

5、实验二:使用事件组进行任务同步

5.1、实验目标

  1. 创建一个用于演示本实验的事件组 xEventGroup
  2. 创建三个任务通过延时模拟不同时间到达任务同步点

5.2、CubeMX相关配置

首先读者应按照 "FreeRTOS教程1 基础知识" 章节配置一个可以正常编译通过的 FreeRTOS 空工程,然后在此空工程的基础上增加本实验所提出的要求

本实验需要初始化 USART1 作为输出信息渠道,具体配置步骤请阅读 “STM32CubeMX教程9 USART/UART 异步通信” ,如下图所示

单击 Middleware and Software Packs/FREERTOS,在 Configuration 中单击 Tasks and Queues 选项卡,双击默认任务修改其参数,具体如下图所示

然后在 Configuration 中单击 Events 选项卡,单击右下角的 Add 按钮增加一个事件组 xEventGroup ,具体如下图所示

配置 Clock Configuration 和 Project Manager 两个页面,接下来直接单击 GENERATE CODE 按钮生成工程代码即可

5.3、添加其他必要代码

按照 “STM32CubeMX教程9 USART/UART 异步通信” 实验 “6、串口printf重定向” 小节增加串口 printf 重定向代码,具体不再赘述

首先应该在 freertos.c 中添加信号量的头文件和定义需要用到的事件组位的宏定义,如下所述

/*头文件*/
#include "stdio.h"
#include "stdlib.h"
#include "event_groups.h"

/*事件组位宏定义*/
#define mainFIRST_TASK_BIT 	( 1UL << 0UL ) /* 事件组位 0 */
#define mainSECOND_TASK_BIT ( 1UL << 1UL ) /* 事件组位 1 */
#define mainTHIRD_TASK_BIT  ( 1UL << 2UL ) /* 事件组位 2 */

修改 MX_FREERTOS_Init() 函数,将默认生成的创建一个任务程序注释掉,然后利用一个任务回调函数通过不同的参数创建三个不同的任务,部分注释已经删除,具体如下所示

void MX_FREERTOS_Init(void) {
  /* Create the thread(s) */
  /* creation of Task_Syncing */
  //Task_SyncingHandle = osThreadNew(AppTask_Syncing, NULL, &Task_Syncing_attributes);

  /* USER CODE BEGIN RTOS_THREADS */
  /* add threads, ... */
	xTaskCreate(AppTask_Syncing, "Task 1", 1000, (void*)mainFIRST_TASK_BIT, 24, NULL);
	xTaskCreate(AppTask_Syncing, "Task 2", 1000, (void*)mainSECOND_TASK_BIT, 24, NULL);
	xTaskCreate(AppTask_Syncing, "Task 3", 1000, (void*)mainTHIRD_TASK_BIT, 24, NULL);
  /* USER CODE END RTOS_THREADS */

  /* Create the event(s) */
  /* creation of xEventGroup */
  xEventGroupHandle = osEventFlagsNew(&xEventGroup_attributes);

  /* USER CODE BEGIN RTOS_EVENTS */
  /* add events, ... */
  /* USER CODE END RTOS_EVENTS */
}

最后实现任务入口函数 AppTask_Syncing() 的函数体即可,具体如下所述

/*事件组同步任务函数*/
void AppTask_Syncing(void *argument)
{
	/* USER CODE BEGIN AppTask_Syncing */
	/* 创建两个延时用于合成随机延时时间 */
	const TickType_t xMaxDelay = pdMS_TO_TICKS(4000UL);
	const TickType_t xMinDelay = pdMS_TO_TICKS(200UL);
	/* 延时时间 */
	TickType_t xDelayTime;
	/* 任务要设置的事件组的位 */
	EventBits_t uxThisTasksSyncBit;
	/* 任务要等待的事件组的所有位 */
	const EventBits_t uxAllSyncBits = ( mainFIRST_TASK_BIT |
										mainSECOND_TASK_BIT |
										mainTHIRD_TASK_BIT );
	
	uxThisTasksSyncBit = ( EventBits_t )argument;
	/* Infinite loop */
	for(;;)
	{
		/* 合成随机延时时间,模拟三个任务不同时间到达同步点 */
		xDelayTime = (rand() % xMaxDelay) + xMinDelay;
		vTaskDelay(xDelayTime);
		printf("%s reached sync point\r\n", pcTaskGetTaskName(NULL));
		xEventGroupSync(/* 被读的事件组 */
										xEventGroupHandle,
										/* 测试的位 */
										uxThisTasksSyncBit,
										/* 需要等待的所有位 */
										uxAllSyncBits,
										/* 永远等待,不会超时 */
										portMAX_DELAY);
		/* 任务会同时退出同步点,串口输出需要时间,所以通过临界段保护串口输出 */
		taskENTER_CRITICAL();
		printf("%s exited sync point\r\n", pcTaskGetTaskName(NULL));
		taskEXIT_CRITICAL();
	}
	/* USER CODE END AppTask_Syncing */
}

5.4、烧录验证

烧录程序,打开串口助手,通过串口助手输出的信息可以发现,三个任务在不同的(伪随机)时间到达任务点,但是当其中最后一个任务到达同步点之后,三个任务会同时退出同步点,具体的串口输出信息如下图所示

6、注释详解

注释1:图片来源于 Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf

参考资料

STM32Cube高效开发教程(基础篇)

Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf

热门相关:逍遥小书生   99次出墙:老公,情难自禁   我的末世基地车   学霸,你女朋友掉了   美漫之大冬兵