FreeRTOS入门知识(初识RTOS)(二)
文章目录
- 摘要
- 一、浅识FreeRTOS快速入门课程
- 1.2、FerrRTOS 与裸机区别
- 1.2.4 动态静态创建任务---理解任务的句柄意义
- 1.2.5 进一步分析任务调度
- 将上面这段话进行总结:
- 调度器的作用:
- 开发者的职能或者是应用FreeRTOS角度:
- 宏观角度(需要我基于什么内容实现的):
- 微观角度(RTOS 内核职责):
- 删除任务
- 1.2.6 创建任务中参数的理解
摘要
持续更新中
一、浅识FreeRTOS快速入门课程
1.2、FerrRTOS 与裸机区别
1.2.4 动态静态创建任务—理解任务的句柄意义
xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);xTaskCreateStatic(Task3Function, "Task3", 100, NULL, 1, xTask3Stack, &xTask3TCB);
传入一个句柄,相当于是一个数据类型, 用这个数据类型进行存储一些东西,也就是说我需要一个大的数据类型,然后里面什么都可以干,这样我就很方便进行操作了。
可以看到都是使用这个句柄里面的一些内容,进行如何创建任务和如何调度任务,相当于是基于这个结构体进行控制这个任务。这就是句柄的作用。深层次的阅读源码,可以明显看出都是在慢慢补充这个句柄里面的内容,等填充完毕,就意味着任务就可以进行很合理的调度。
typedef struct tskTaskControlBlock /* The old naming convention is used to prevent breaking kernel aware debuggers. */{volatile StackType_t * pxTopOfStack; /*< Points to the location of the last item placed on the tasks stack. THIS MUST BE THE FIRST MEMBER OF THE TCB STRUCT. */#if ( portUSING_MPU_WRAPPERS == 1 )xMPU_SETTINGS xMPUSettings; /*< The MPU settings are defined as part of the port layer. THIS MUST BE THE SECOND MEMBER OF THE TCB STRUCT. */#endifListItem_t xStateListItem; /*< The list that the state list item of a task is reference from denotes the state of that task (Ready, Blocked, Suspended ). */ListItem_t xEventListItem; /*< Used to reference a task from an event list. */UBaseType_t uxPriority; /*< The priority of the task. 0 is the lowest priority. */StackType_t * pxStack; /*< Points to the start of the stack. */char pcTaskName[ configMAX_TASK_NAME_LEN ]; /*< Descriptive name given to the task when created. Facilitates debugging only. */ /*lint !e971 Unqualified char types are allowed for strings and single characters only. */#if ( ( portSTACK_GROWTH > 0 ) || ( configRECORD_STACK_HIGH_ADDRESS == 1 ) )StackType_t * pxEndOfStack; /*< Points to the highest valid address for the stack. */#endif#if ( portCRITICAL_NESTING_IN_TCB == 1 )UBaseType_t uxCriticalNesting; /*< Holds the critical section nesting depth for ports that do not maintain their own count in the port layer. */#endif#if ( configUSE_TRACE_FACILITY == 1 )UBaseType_t uxTCBNumber; /*< Stores a number that increments each time a TCB is created. It allows debuggers to determine when a task has been deleted and then recreated. */UBaseType_t uxTaskNumber; /*< Stores a number specifically for use by third party trace code. */#endif#if ( configUSE_MUTEXES == 1 )UBaseType_t uxBasePriority; /*< The priority last assigned to the task - used by the priority inheritance mechanism. */UBaseType_t uxMutexesHeld;#endif#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; /*< Stores the amount of time the task has spent in the Running state. */#endif#if ( configUSE_NEWLIB_REENTRANT == 1 )/* Allocate a Newlib reent structure that is specific to this task.* Note Newlib support has been included by popular demand, but is not* used by the FreeRTOS maintainers themselves. FreeRTOS is not* responsible for resulting newlib operation. User must be familiar with* newlib and must provide system-wide implementations of the necessary* stubs. Be warned that (at the time of writing) the current newlib design* implements a system-wide malloc() that must be provided with locks.** See the third party link http://www.nadler.com/embedded/newlibAndFreeRTOS.html* for additional information. */struct _reent xNewLib_reent;#endif#if ( configUSE_TASK_NOTIFICATIONS == 1 )volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];#endif/* See the comments in FreeRTOS.h with the definition of* tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE. */#if ( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 ) /*lint !e731 !e9029 Macro has been consolidated for readability reasons. */uint8_t ucStaticallyAllocated; /*< Set to pdTRUE if the task is a statically allocated to ensure no attempt is made to free the memory. */#endif#if ( INCLUDE_xTaskAbortDelay == 1 )uint8_t ucDelayAborted;#endif#if ( configUSE_POSIX_ERRNO == 1 )int iTaskErrno;#endif} tskTCB;
int main( void ){TaskHandle_t xHandleTask1;#ifdef DEBUGdebug();#endifprvSetupHardware();printf("Hello, world!\r\n");xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);xTaskCreateStatic(Task3Function, "Task3", 100, NULL, 1, xTask3Stack, &xTask3TCB);/* Start the scheduler. */vTaskStartScheduler();/* Will only get here if there was not enough heap space to create theidle task. */return 0;}
这个就相当于是这个函数如何使用的结构体,所以每个函数都是需要一个句柄的,就是如何控制这个函数。
这个句柄的作用可理解为是供外部控制的,因为我们传入是一个地址,也就是我们在main函数中创建的这个控制体,我们传进入的这个句柄的地址,那么这样就可以任务创建过程中任务的状态、优先级、堆栈等核心信息,给存到这个里面,这样就可以实现外部管理,例如:
-
挂起/恢复任务(
vTaskSuspend
/vTaskResume
) -
删除任务(
vTaskDelete
) -
修改优先级(
vTaskPrioritySet
) -
发送任务通知(
xTaskNotifyGive
)
如果没有传入句柄也是不会受到影响的,因为他仅失去外部管理能力,但任务自身的执行不受影响。并且即使句柄参数为 NULL
,FreeRTOS 仍会分配TCB和堆栈,并将任务加入调度器队列,任务会按优先级正常调度运行。
可以看出句柄是外部控制的核心。
静态的就需要提前提供好内存,提前确定好TCB结构体,并且这个栈就是一个空闲的内存,通过定义数组实现。
1.2.5 进一步分析任务调度
在任务调度实验中,优先级都一样的情况下,是顺序执行的。
int main( void )
{#ifdef DEBUGdebug();
#endifprvSetupHardware();printf("Hello, world!\r\n");xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);xTaskCreateStatic(Task3Function, "Task3", 100, NULL, 1, xTask3Stack, &xTask3TCB);/* Start the scheduler. */vTaskStartScheduler();/* Will only get here if there was not enough heap space to create theidle task. */return 0;
}
从主函数这段代码,再次印证了和裸机的区别,而执行的区别正是调度器产生的,所以在学习FreeRTOS的时候一定要区别于裸机开发的思想。这是很关键的,并且每一个任务都是要死循环的。
并且可以看出,单核的就是一次只能执行一个任务,然后只要我切换的足够快,就可以认为是一直执行,有种自欺欺人的感觉,所以还是没必要这么理解。就理解成调度式执行或抢占式执行我觉得最好。
而只是进行了优先级修改,可以发现任务2和任务3根本没有机会执行,这是因为在任务1的内部根本没有被阻塞或者改变状态,所以调度器会一直执行他,这也是我们内部写的简单的原因,这里其实会引入一个思考,也就是在每一个任务在实现的时候应该怎么实现状态的切换,简单的可以通过延时函数实现,接下来会有讲解,但是复杂的呐?这个地方也是FreeRTOS的核心。
通过一个简单的任务里面删除任务和自杀,其实就应该明白了,这里面全是调度器的功劳,也就是说我们写的代码完全是在一个系统里面运行,并且我们是没有考虑如何调度的,如果不深究,我们相当于是调用API去实现某些功能,完成调度,在应用层使用它。个人理解在FreeRTOS开发过程中,有点像是在Windows操作系统进行桌面化操作一样,我需要的时候我就把窗口给调出来,如果不需要的时候我就把窗口给缩小,如果我想删除就直接删除。而FreeRTOS开发就是相当于是我们在开发过程中自己要规划好每一个需要执行的任务,怎么执行,什么时候执行,又与其他任务相关性是什么,在宏观上我们要自己把握,但是在微观上是FreeRTOS内部实现的。
将上面这段话进行总结:
调度器的作用:
-
FreeRTOS(或任何RTOS)的核心价值在于它内置的调度器。开发者负责创建任务(窗口)、定义任务要做什么(窗口里的内容),但 “何时运行哪个任务”这个关键决策是由调度器自动完成的。应用程序员无需(也不应该直接干预)具体调度算法(如优先级调度、轮转调度)的执行时刻。
-
就像在 Windows 上,你决定打开什么程序(创建任务/窗口),聚焦哪个窗口(手动选择前台任务),最小化窗口(挂起任务),但你不用关心操作系统底层是如何安排 CPU 时间片给各个进程/线程的(调度器的微观实现)。
所以说再开发的时候我们需要更加的专注于如何去用,或者说是业务层面理解,实现我们需要的业务亦或者我们想要实现的算法。
开发者的职能或者是应用FreeRTOS角度:
-
使用 FreeRTOS 开发,确实就像在一个“迷你操作系统”上开发应用。你通过 FreeRTOS 提供的 API (
xTaskCreate
,vTaskDelay
,vTaskDelete
,xQueueSend
,xSemaphoreTake
等) 来实现:- 任务管理:创建、删除、挂起、恢复任务。(相当于打开、关闭、最小化、还原窗口)。
- 资源管理:获取/释放共享资源(互斥锁、信号量)。(相当于窗口间协调资源访问)。
- 任务同步与通信:通过队列、事件组等机制进行任务间的协调和数据传递。(相当于窗口间拖放、复制粘贴或共享数据)。
- 时间管理:延时(
vTaskDelay
)、定时器。(相当于定时提醒或后台任务)。
-
开发者的主要工作是利用这些 API 构建应用逻辑,组合出所需的功能。
如何熟练的使用FreeRTOS开发相关的业务逻辑是很重要的,也是工作用需要的技能,并且最重要的是一些调试技巧也是很重要的。因为FreeRTOS相较于裸机开发的复杂性,有一个重要的调试思路是很重要的相较于裸机开发。
宏观角度(需要我基于什么内容实现的):
-
RTOS 开发区别于裸机和通用操作系统开发的关键点。你需要像整个“小宇宙”的系统设计师一样思考:
-
任务划分 (Task Partitioning):如何将复杂的应用功能合理地分解成若干个独立(或半独立)的任务(线程)?哪些功能是并发的?
-
任务行为定义:每个任务具体做什么?它的执行逻辑是什么?
-
优先级设定 (Priority Assignment):每个任务有多紧急?高优先级任务是否可以抢占低优先级任务?这决定了在资源冲突时谁先执行。(这是最影响系统实时响应性的设计决策!)
-
同步与通信机制 (Synchronization & Communication):任务之间如何协调以避免竞争冲突?如何安全地传递信息和数据?选择队列、信号量、互斥锁、事件组等哪种机制最合适?
-
资源使用规划:任务需要多少栈空间(Stack)?共享的资源有哪些?如何保护?
-
实时性要求 (Real-time Constraints):哪些任务需要在确定的时间点或时间段内完成?
-
-
需要站在整体系统架构的角度做这些宏观决策。 FreeRTOS 提供了强大的工具(API),但如何合理、高效、安全地组装这些工具来完成你的“作品”(嵌入式应用),完全取决于你的设计。
微观角度(RTOS 内核职责):
一旦你设计好宏观方案(定义了任务、优先级、同步通信等),并通过 API 将信息告诉 FreeRTOS(调用 xTaskCreate
等),FreeRTOS 内核(主要是调度器)就会在底层精密地自动执行:
-
维护任务状态(就绪、运行、阻塞、挂起)。
-
在任务切换点(如调用阻塞API
vTaskDelay
,xQueueReceive
等,或系统时钟滴答tick interrupt
到来)时,根据预设的优先级策略(通常是固定优先级抢占调度)选择下一个要运行的任务。 -
执行任务上下文切换 (Context Switching):保存当前任务的 CPU 寄存器状态(现场),恢复下一个任务的现场,然后跳转到下一个任务开始执行。这个过程对应用开发者是透明的。
-
管理就绪队列和延迟列表等内部数据结构。
注意是自动执行!!!!! 对于内核的机制个人理解,还是应该掌握的,这对于在分析某些Bug的时候是有很大的帮助的,如果只是调用API,还是有一定的局限性。
删除任务
在实际应用中,直接删除任务(尤其是不在任务入口函数结尾的自删除)需要非常谨慎:
-
任务删除时 不会自动回收其堆栈内存!需要手动调用
vTaskDelete()
删除其他任务(如在其他任务或空闲任务中删除)后,由空闲任务(idle task
)来回收其内存(前提是启用了configUSE_TRACE_FACILITY
或configUSE_IDLE_HOOK
并在空闲任务中调用vPortFree()
类似的机制,或者更常见的,在创建任务时分配动态内存的情况下,调用vTaskDelete()
会触发heap
的free
)。简单删除可能导致内存泄漏。 -
任务内部申请的动态内存、持有的信号量、锁定的互斥量等资源,需要任务在自删除前妥善清理释放,否则会造成资源泄露或死锁。
-
通常建议由一个“清理”任务或任务在被显式请求删除时再执行删除操作(包括释放自身资源),而不是随意自删除。
对于任务的删除,还没有那么深刻的理解,暂时就先这样。这个地方很容易造成BUG,因为会造成资源泄露或死锁。
1.2.6 创建任务中参数的理解
xTaskCreate(TaskGenericFunction, "Task4", 100, (void *)4, 1, NULL);xTaskCreate(TaskGenericFunction, "Task5", 100, (void *)5, 1, NULL);
void TaskGenericFunction(void * param)
{int val = (int)param;while (1){printf("%d", val);}
}
可以看出在参数位置我们传入的(void *)4
,而在我们的实际任务中,我们其实需要多少是一个整数,但是我们的创建任务的函数参数只能是指针,因此我们必须在创建的时候进行强制转换成指针,然后在传进去以后,在将他强制转换成我们需要的整数。为什么这样?问就是为了兼容性。
需要注意的是若传递的值超出指针范围(如大于 0xFFFFFFFF
),可能引发错误。
同样如果需要的就是指针参数
void task_function(void *param) {// 将 void* 强制转换为 int*,再解引用获取值int value = *(int*)param;printf("Value: %d\n", value);
}int main() {int num = 42;// 传递整数的地址(转换为 void*)xTaskCreate(task_function, "Task", 1024, (void*)&num, 1, NULL);
}
我们就需要传递参数,并且同样需要先强制转换为(void*)&num
,这也是工作中胡哥最初教我认识指针的时候告诉我的关于指针的一些用法。然后我们传递进去以后,此时一定要谨记,在将其强制转换为我们需要的指针类型,不然就会出错。这是因为:在 C 语言中,void*
是一种通用指针类型,可以指向任意数据类型的内存地址。但它不能直接解引用**(如 *void_ptr
是非法的),因为编译器不知道数据类型和内存布局。这个原因我在之前文章已经分析过了。
这是关于栈空间分布的初步理解,我们暂时只需要知道在我们分布的栈空间前面还有一个是存入一些信息的,例如我们需要存入我们分配的栈空间的大小是多少,包括TCB1也是如此,在其前面也是有一个空间存相关大小的,具体我们可以先不用理解具体什么,后续有的是机会深入理解,只需要大致知道是什么就行。其中长度就是为了在使用free的时候知道我们这个释放的空间是多少。
并且接下来模拟一下,栈溢出的错误,
void Task1Function(void * param)
{volatile char buf[500];int i;while (1){task1flagrun = 1;task2flagrun = 0;task3flagrun = 0;printf("1");for ( i = 0; i < 500; i++)buf[i] = 0;}
}xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);
申请100×4的字节空间,但是我们用的是500个字节,肯定会导致溢出。
直接跳转到了HardFault_Handler
,程序崩溃,所以一定是注意栈溢出的危险。
后续会单独开章节讨论这两个问题,这两个问题很重要,是深入FreeRTOS的关键。
在分配的时候如何确定栈空间大小?
函数如何使用栈空间?
如果觉得我的内容对您有帮助,希望不要吝啬您的赞和关注,您的赞和关注是我更新优质内容的最大动力。
专栏介绍
《嵌入式通信协议解析专栏》
《PID算法专栏》
《C语言指针专栏》
《单片机嵌入式软件相关知识》
《FreeRTOS源码理解专栏》
《嵌入式软件分层架构的设计原理与实践验证》
文章源码获取方式:
如果您对本文的源码感兴趣,欢迎在评论区留下您的邮箱地址。我会在空闲时间整理相关代码,并通过邮件发送给您。由于个人时间有限,发送可能会有一定延迟,请您耐心等待。同时,建议您在评论时注明具体的需求或问题,以便我更好地为您提供针对性的帮助。
【版权声明】
本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议。这意味着您可以自由地共享(复制、分发)和改编(修改、转换)本文内容,但必须遵守以下条件:
署名:您必须注明原作者(即本文博主)的姓名,并提供指向原文的链接。
相同方式共享:如果您基于本文创作了新的内容,必须使用相同的 CC 4.0 BY-SA 协议进行发布。
感谢您的理解与支持!如果您有任何疑问或需要进一步协助,请随时在评论区留言,笔者一定知无不言,言无不尽。