作者有话要说
对于这个越来约浮躁的社会,什么都要钱,特别是网上那些垃圾教程,越听越模糊,那行吧,我直接就从 FreeRTOS 的 API函数 学起,管你这么多底层内容的,以后再说吧!!!(对不起!还是讲了不少底层知识。。。)
我写的只是简略描述一下这些 API ,然后直接教你怎么调用,但是具体的 API 函数参考官网的 --> FreeRTOS API 引用
CubeMX 配置
这个是大佬写的配置,感觉可以的,就放在这里了,配置我就不写了
STM32CubeMX学习笔记(28)——FreeRTOS实时操作系统使用(任务管理)_thread id identifies the thread._Leung_ManWah的博客-CSDN博客
初期配置
虽然在 cubemx 配置的都是这个文件,但是可以打开了解一下,也可以直接跳过。
先找到 FreeRTOS.c 这个文件,在头位置找到 FreeRTOS.h 文件跳转过去,往下翻一翻就可以看到 调用了 FreeRTOSConfig.h 文件,可以过来看看,里面写了啥,至于为什么要看,那当然是,省的老是打开 CubeMX 配置生成来去,又卡又慢的。
对于这部分内容想详细了解的可以看下官网(有中文还是挺香的)。
FreeRTOS - The Free RTOS configuration constants and configuration options - FREE Open Source RTOS for small real time embedded systems
注意事项
- 使用 STM32CubeMX 代码生成,在 STM32Cube 固件中,通过 ARM 提供的通用 CMSIS-OS( cmsis_os.h 和 cmsis_os.c 文件,这两个文件把给 FreeRTOS 封装了一层,调用这其中的各类函数,和直接调用 FreeRTOS 的函数没有区别 ) 封装层,将 FreeRTOS 用作实时操作系统。也就是说在一套代码里有着两套标准,在阅读源码时需要注意区分。我主要讲解的是 FreeRTOS 的函数,配合 CubeMX 使用,而尽量少用 CMSIS-OS 封装层。
- 等待补充。。。
任务创建
这一章的创建任务可以由 CubeMX 生成,任务删除可以看看。
任务创建由两种方法,一种是动态创建,一种是静态创建。
这一章就介绍上面两个 创建任务函数,加一个 删除任务的函数
动态创建
函数介绍
对于 动态创建 的 函数如下:
BaseType_t xTaskCreate( TaskFunction_t pvTaskCode,
const char * const pcName,
configSTACK_DEPTH_TYPE usStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *pxCreatedTask
);
参数分别是:
- 指向任务入口函数的指针(pvTaskCode):这里要传入的参数是一个自定义的函数,这个任务应该是要符合规则,就是符合 带有一个参数(这个参数是一个 void * 类型,就是可以传入任意类型参数) 的 无返回值 的函数。
- 任务名字(pcName)。
- 任务堆栈大小(usStackDepth):以 size_t(这里的 size_t 是 4 个字节(32位系统中的 int 的大小)) 为
最小步进长度
的堆栈大小。- 假如传入 128,则堆栈大小是 ( 128 * sizeof( size_t ) ) Byte。
- 任务的传入形参(pvParameters),在任务内部可以获取这个形参的值(貌似用的比较少)。
- 任务优先级(uxPriority),有这几个优先级可以选择:
- 这里虽然写了 负的优先级,但是会在创建任务的时候改到 0 值以上( CMSIS-OS 封装层 改值后传入 FreeRTOS 的创建任务中)。
- 这里定义优先级的范围可以是 4 到 32,必须有至少 4 种优先级
- 不知到为什么,我在 cubemx 调成了32的优先级最大值,而在代码中还是只有这几种优先级可以选择,但是我翻看源码的时候,里面虽然支持 0 到 31的优先级,这里却只会枚举定义这么几个。。(现在知道了,是因为 cmsis_os .c/.h 文件是 ARM 提供的通用 CMSIS-OS 封装层,CubeMX 生成不改这个,改的是 FreeRTOSConfig.h 文件,也就是优先级支持了,可以直接传入数字进去,不用管那几个枚举值 )
- 这里虽然写了 负的优先级,但是会在创建任务的时候改到 0 值以上( CMSIS-OS 封装层 改值后传入 FreeRTOS 的创建任务中)。
- 任务句柄(pxCreatedTask),类型是void * 类型,指针指向的是一个
tskTCB
(任务控制块)(保存任务的数据结构体) 结构体,里面存储的是这个任务的
返回值是:用于判断创建是否成功的,当值为 pdPASS 时创建成功,当值为 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY 时创建失败
函数使用
TaskHandle_t myTask01Handle; // hal 库生产的是 osThreadId myTask01Handle; 两者没有区别 osThreadId 就是对 TaskHandle_t起了别名
// 任务调度函数
void StartTask01(void const * argument)
{
for(;;)
{
printf("I'm Task01.\r\n");
vTaskDelay(10); // 自动进入阻塞状态,等待
}
}
void main(void)
{
// 动态的创建一个任务啦,参数可以看前面的解释
if(xTaskCreate((TaskFunction_t)StartTask01, "myTask01", 128, NULL, (UBaseType_t)osPriorityIdle, myTask01Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask01 OK!\r\n");
}
vTaskStartScheduler(); /* 启动任务,开启调度 */
/* 永远不应该到达这里,因为现在控制权已经被调度器接管了 */
while(1);
}
静态创建
一般比较少用这个,不是重点。
函数介绍
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,
const char * const pcName,
const uint32_t ulStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
StackType_t * const puxStackBuffer,
StaticTask_t * const pxTaskBuffer );
参数分别是:
- 任务函数起始地址(pxTaskCode),和动态创建描述一样。
- 任务名字(pcName)
- 任务堆栈大小(ulStackDepth),用于指定传入的
puxStackBuffer
参数的 buf 的大小,最小步进大小是 sizeof( StackType_t ) 。- 假如传入 128,则堆栈大小是 ( 128 * sizeof( StackType_t ) ) Byte,此时
puxStackBuffer
的数组必须有 128 的索引。
- 假如传入 128,则堆栈大小是 ( 128 * sizeof( StackType_t ) ) Byte,此时
- 任务的传入形参(pvParameters),和动态创建描述一样。
- 任务优先级(uxPriority),和动态创建描述一样。
- 必须指向至少具有
ulStackDepth
索引的StackType_t
数组(请参阅上面的ulStackDepth
参数),该数组用作任务的堆栈,因此必须是永久性
的(全局,或静态)。 - 是一个 StaticTask_t * 变量,在函数内部被强转成 TCB_t * 变量,然后获取各种 TCB_t(保存任务的数据结构体) 内容,最后给函数当返回值。
返回值是:返回一个TCB_t
指针(这个tcb结构体保存的内容和 pxTaskBuffer 参数是相同的),若是为空,则创建失败。
函数使用
// 一定要放在全局的位置,使栈空间永久存在
StackType_t myTask02Buff[128];
StaticTask_t myTask02Handle;
// 任务调度函数
void StartTask02(void const * argument)
{
for(;;)
{
printf("I'm Task02.\r\n");
vTaskDelay(10); // 自动进入阻塞状态,等待
}
}
void main(void)
{
// 静态的创建一个任务啦,参数可以看前面的解释
if(xTaskCreateStatic((TaskFunction_t)StartTask02, "myTask02", 128, NULL, (UBaseType_t)osPriorityIdle, myTask02Buff, &myTask02Handle) != NULL)
{
printf("Create StartTask02 OK!\r\n");
}
vTaskStartScheduler(); /* 启动任务,开启调度 */
/* 永远不应该到达这里,因为现在控制权已经被调度器接管了 */
while(1);
}
删除任务
被删除的任务将从所有的就绪、阻塞、挂起和事件的列表中移除。
空闲任务的责任是要将分配给已删除任务的内存释放掉。
注意:只有内核为任务分配的内存空间才会在任务被删除后自动回收,任务自己占用的内存或资源需要由应用程序自己显式地释放。
函数介绍
void vTaskDelete( TaskHandle_t xTask );
参数:
- xTask ,待删除的任务的句柄。传递 NULL 将导致调用他的任务被删除。
- 这里注意一下,若是传入的参数是 NULL ,则会删除调用他的任务,这个怎么实现的?
- 因为在 tasks.c 中有一个全局 TCB 指针,会指向当前任务 TCB,
- 在 vTaskDelete 函数中调用了 prvGetTCBFromHandle 宏来判断该传入TCB参数是否为空,若是为空,则获取当前正在执行的任务的 TCB ,否则返回传入的 TCB 指针。
该函数没有返回值。
函数使用
// 一定要放在全局的位置,使栈空间永久存在
TaskHandle_t myTask01Handle; // hal 库生产的是 osThreadId myTask01Handle; 两者没有区别 osThreadId 就是对 TaskHandle_t起了别名
StackType_t myTask02Buff[128];
StaticTask_t myTask02Handle;
// 任务调度函数
void StartTask01(void const * argument)
{
for(;;)
{
printf("I'm Task01.\r\n");
vTaskDelay(10); // 自动进入阻塞状态,等待
}
}
// 任务调度函数
void StartTask02(void const * argument)
{
uint8_t i = 0;
for(;;)
{
printf("I'm Task02.\r\n");
vTaskDelay(10); // 自动进入阻塞状态,等待
// 当 i > 20 时,释放当前线程 ( StartTask02 )
if(i++ > 20)
{
vTaskDelete(NULL);
}
}
}
void main(void)
{
// 动态的创建一个任务啦,参数可以看前面的解释
if(xTaskCreate((TaskFunction_t)StartTask01, "myTask01", 128, NULL, (UBaseType_t)osPriorityIdle, myTask01Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask01 OK!\r\n");
}
// 静态的创建一个任务啦,参数可以看前面的解释
if(xTaskCreateStatic((TaskFunction_t)StartTask02, "myTask02", 128, NULL, (UBaseType_t)osPriorityIdle, myTask02Buff, &myTask02Handle) != NULL)
{
printf("Create StartTask02 OK!\r\n");
}
vTaskStartScheduler(); /* 启动任务,开启调度 */
/* 永远不应该到达这里,因为现在控制权已经被调度器接管了 */
while(1);
}
对于任务创建的扩展知识
对于CubeMX生成的任务创建函数解读
任务的创建包含两个部分,第一部分是一个宏函数( 动态时为 osThreadDef )(静态时为 osThreadStaticDef),用于打包各个参数,给到一个新创建的 osThreadDef_t 类型变量,该变量名为,os_thread_def_##name,(在宏中,## 号用于连接两边。假如名字是123,则变量名为,os_thread_def_123)。
对于任务创建的第二部分是一个函数(osThreadCreate),在函数内部直接调用本章的任务创建函数,来创建任务。第一个参数是,第一部分的宏创建的 osThreadDef_t 类型变量;第二个参数是,创建函数的第 4 个参数(pvParameters,任务的传入参数)。
TCB结构体是什么?
这一小节参考: FreeRTOS解析:TCB_t结构体及重要变量说明(Task-1)_stacktype结构体_Nrush的博客-CSDN博客
TCB_t 的全称为 Task Control Block,也就是任务控制块,这个结构体包含了一个任务所有的信息,它的定义以及相关变量的解释如下
/*
* 任务控制块。为每个任务分配一个任务控制块(TCB),
* 并存储任务状态信息,包括指向任务上下文(任务的运行时环境,包括寄存器值)的指针
*/
typedef struct tskTaskControlBlock
{
// 这里栈顶指针必须位于TCB第一项是为了便于上下文切换操作,详见 xPortPendSVHandler 中任务切换的操作。
volatile StackType_t *pxTopOfStack;
#if ( portUSING_MPU_WRAPPERS == 1 )
// MPU设置被定义为端口层的一部分。这必须是TCB结构的第二个成员。
xMPU_SETTINGS xMPUSettings;
#endif
// 表示任务状态,不同的状态会挂接在不同的状态链表下(就绪、阻塞、挂起)。
ListItem_t xStateListItem;
// 事件链表项,会挂接到不同事件链表下
ListItem_t xEventListItem;
// 任务优先级,数值越大优先级越高
UBaseType_t uxPriority;
// 指向堆栈起始位置,这只是单纯的一个分配空间的地址,可以用来检测堆栈是否溢出
StackType_t *pxStack;
// 任务名
char pcTaskName[ configMAX_TASK_NAME_LEN ];
// 指向栈尾,可以用来检测堆栈是否溢出
#if ( ( portSTACK_GROWTH > 0 ) || ( configRECORD_STACK_HIGH_ADDRESS == 1 ) )
StackType_t *pxEndOfStack;
#endif
// 记录临界段的嵌套层数
#if ( portCRITICAL_NESTING_IN_TCB == 1 )
UBaseType_t uxCriticalNesting;
#endif
// 跟踪调试用的变量
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxTCBNumber;
UBaseType_t uxTaskNumber;
#endif
// 任务优先级被临时提高时,保存任务原本的优先级
#if ( configUSE_MUTEXES == 1 )
UBaseType_t uxBasePriority;
UBaseType_t uxMutexesHeld;
#endif
// 任务的一个标签值,可以由用户自定义它的意义,例如可以传入一个函数指针可以用来做 Hook 函数调用
#if ( configUSE_APPLICATION_TASK_TAG == 1 )
TaskHookFunction_t pxTaskTag;
#endif
// 任务的线程本地存储指针,可以理解为这个任务私有的存储空间
#if( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 )
void *pvThreadLocalStoragePointers[ configNUM_THREAD_LOCAL_STORAGE_POINTERS ];
#endif
// 运行时间变量
#if( configGENERATE_RUN_TIME_STATS == 1 )
uint32_t ulRunTimeCounter;
#endif
// 支持NEWLIB的一个变量
#if ( configUSE_NEWLIB_REENTRANT == 1 )
struct _reent xNewLib_reent;
#endif
// 任务通知功能需要用到的变量
#if( configUSE_TASK_NOTIFICATIONS == 1 )
// 任务通知的值
volatile uint32_t ulNotifiedValue;
// 任务通知的状态
volatile uint8_t ucNotifyState;
#endif
// 用来标记这个任务的栈是不是静态分配的
#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
uint8_t ucStaticallyAllocated;
#endif
// 延时是否被打断
#if( INCLUDE_xTaskAbortDelay == 1 )
uint8_t ucDelayAborted;
#endif
} tskTCB;
/* 起一个别名 */
typedef tskTCB TCB_t;
创建时的 TCB 获取
对于 任务相关的 API 由一个在 task. h 中 TaskHandle_t 实际就是一个 void * 类型用于接收所有类型的指针,在创建任务时不管调用 xTaskCreate 还是 xTaskCreateStatic 都会获得一个 TaskHandle_t 类型的值,我在追溯到最后时,看见实际这个 TaskHandle_t 指针获取的是一个 TCB_t 的结构体。
拿动态创建任务函数举例:
开始传入的一个 pxCreatedTask 一般是一个空指针,用于接收在函数内部 TCB指针,里面包含任务的信息。
这里把两个参数传入了 prvInitialiseNewTask 函数中。
在 prvInitialiseNewTask 函数的最末尾把 TCB结构体的值,给到了 pxCreatedTask ,函数返回时,这个参数已经指向了创建的新任务的 TCB 结构体。
任务控制
vTaskDelay
- 这个函数用于任务延时的
- 是非阻塞延时,当任务调用他时,只有调用他的任务进入阻塞状态,此时会执行其他任务。
- 当延时结束时,延时事件到来,会将被阻塞的任务切换回就绪状态。
- 进入就绪状态不代表可以马上执行该任务,还要等待轮到她进入
- 将 INCLUDE_vTaskDelay 定义为 1,此函数才可用。
函数介绍
void vTaskDelay( const TickType_t xTicksToDelay );
参数:
- xTicksToDelay ,调用任务应阻塞的
tick
周期数。
函数使用
// 一定要放在全局的位置,使栈空间永久存在
TaskHandle_t myTask01Handle; // hal 库生产的是 osThreadId myTask01Handle; 两者没有区别 osThreadId 就是对 TaskHandle_t 起了别名
TaskHandle_t myTask02Handle;
// 任务调度函数
void StartTask01(void const * argument)
{
for(;;)
{
printf("I'm Task01.\r\n");
vTaskDelay(10); // 自动进入阻塞状态,等待
}
}
// 任务调度函数
void StartTask02(void const * argument)
{
uint8_t i = 0;
for(;;)
{
printf("I'm Task02.\r\n");
vTaskDelay(10); // 自动进入阻塞状态,等待
// 当 i > 20 时,释放当前线程 ( StartTask02 )
if(i++ > 20)
{
vTaskDelete(NULL);
}
}
}
void main(void)
{
// 动态的创建一个任务啦,参数可以看前面的解释
if(xTaskCreate((TaskFunction_t)StartTask01, "myTask01", 128, NULL, (UBaseType_t)osPriorityIdle, myTask01Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask01 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask02, "myTask02", 128, NULL, (UBaseType_t)osPriorityIdle, myTask02Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask02 OK!\r\n");
}
vTaskStartScheduler(); /* 启动任务,开启调度 */
/* 永远不应该到达这里,因为现在控制权已经被调度器接管了 */
while(1);
}
vTaskDelayUntil
-
INCLUDE_vTaskDelayUntil 必须被定义为 1 才能使用此函数。
-
此函数与 vTaskDelay() 在一个重要的方面有所不同: 传入的时间参数是相对于 vTaskDelay() 被调用的时间, 而 vTaskDelayUntil() 会指定任务希望取消阻塞的 绝对 时间。
-
具体来说:
- vTaskDelay 函数是通过将 当前任务挂起一段时间来实现延迟 的,而在这段时间内,任务不会执行任何操作。这意味着,如果在任务挂起期间发生了某些事件,例如中断或其他高优先级任务的执行,那么任务将会 “丢失执行”,即任务的执行时间会相应地延迟。因此,vTaskDelay 不能保证任务的执行时间是精确的。
- 相比之下,vTaskDelayUntil 函数是 通过计算时间差来实现延迟 的,它可以精确地控制任务的执行时间。使用 vTaskDelayUntil 的任务可以在指定的时间内暂停执行,然后在预期的时间内恢复执行。这种方式可以保证任务在预期的时间内执行,从而提高系统的实时性和性能。因此,使用 vTaskDelayUntil 的任务不会 “丢失执行”。
-
使用说明:
- 在使用 vTaskDelayUntil 函数时,我们需要首先初始化 pxPreviousWakeTime 变量,然后在函数中传入该变量的地址,以及需要等待的时间间隔 xTimeIncrement。
- 函数会自动计算任务需要等待多长时间,然后阻塞任务直到该时间到达,然后任务会被自动唤醒。这种方式可以精确控制任务的执行时间,从而提高系统的实时性和性能。
函数介绍
void vTaskDelayUntil( TickType_t *pxPreviousWakeTime,
const TickType_t xTimeIncrement );
参数介绍:
- pxPreviousWakeTime,
- 指向一个 TickType_t 类型变量的指针,用于保存任务最后一次解除阻塞的时间。
- 这个变量需要在第一次使用前用当前时间进行初始化,可以通过调用 xTaskGetTickCount 函数获取当前时间。
- 在 vTaskDelayUntil 函数内部,此变量会自动更新,以便在下一次调用时使用。
- xTimeIncrement,周期时间段,即任务需要等待的时间。
- 该任务将在 (*pxPreviousWakeTime + xTimeIncrement) 时间解除阻塞。
- 如果任务需要以固定的间隔期执行,可以使用相同的 xTimeIncrement 参数值调用 vTaskDelayUntil 函数。
函数使用
TaskHandle_t myTask01Handle; // hal 库生产的是 osThreadId myTask01Handle; 两者没有区别 osThreadId 就是对 TaskHandle_t 起了别名
TaskHandle_t myTask02Handle;
TaskHandle_t myTask03Handle;
// 任务调度函数
void StartTask01(void const * argument)
{
for(;;)
{
printf("I'm Task01.\r\n");
vTaskDelay( 10 ); // 自动进入阻塞状态,等待
}
}
// 任务调度函数
void StartTask02(void const * argument)
{
// 创建一个
TickType_t xLastWakeTime = xTaskGetTickCount();
for(;;)
{
printf("I'm Task02.\r\n");
// 绝对延时
vTaskDelayUntil( &xLastWakeTime, 50 );
}
}
// 任务调度函数
void StartTask03(void const * argument)
{
for(;;)
{
printf("I'm Task03.\r\n");
// 绝对延时
vTaskDelay( 10 );
}
}
void main(void)
{
// 动态的创建一个任务啦,参数可以看前面的解释
if(xTaskCreate((TaskFunction_t)StartTask01, "myTask01", 128, NULL, (UBaseType_t)osPriorityIdle, myTask01Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask01 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask02, "myTask02", 128, NULL, (UBaseType_t)osPriorityIdle, myTask02Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask02 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask03, "myTask03", 128, NULL, (UBaseType_t)osPriorityIdle, myTask03Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask03 OK!\r\n");
}
vTaskStartScheduler(); /* 启动任务,开启调度 */
/* 永远不应该到达这里,因为现在控制权已经被调度器接管了 */
while(1);
}