在例程中我选择动态内存分配方式,configSUPPORT_DYNAMIC_ALLOCATION为1,相较静态内存分配方式使用更简单,但不好的地方就是用户不能明确分配各个任务内存。在动态内存分配方式,FreeRTOS首先需要有一个分配好的RAM空间——堆。堆的大小通过configTOTAL_HEAP_SIZE来指定。在例程中分配的是1024个字,也就是2048字节。实际项目中可以在调试阶段借助xPortGetFreeHeapSize()函数来判断堆的剩余空间,以便合理定义堆的大小。
在动态分配方式下,当用户调用xTaskCreate()进行任务创建的时候,将自动在堆中分配任务堆栈和任务控制块,任务堆栈的大小在创建任务时由实参进行指定。同时,如队列、互斥信号量和计数型信号量等在创建后也需从堆中分配空间,堆的示意如下图。
在任务创建过程中,经pxPortInitialiseStack()函数完成堆栈初始化后的任务堆栈情况如下图,这张图也是任务切换时压栈和出栈所操作的内容,务必清晰。
堆栈初始化之后,我们看一下如何启动第一个任务,分析前必须了解如下基础知识。
1)程序指针PC(Program Counter):
dsPIC33的PC为24位可寻址4M× 24位的程序空间,这个空间均等地划分为用户程序空间(0x000000~0x7FFFFF)和配置(或测试)存储空间(0x800000~0xFFFFFF)。因此用户可以仅用PC<22:0>即可访问任何用户程序空间。
注意:堆栈初始化图中可以看到将任务函数的地址赋值给PC<15:0>,而PC<22:16>始终为0。这是因为dsPIC33系列MCU是16位的,虽然16位地址可以访问所有数据空间,但不能访问4M× 24位的程序空间,只能访问64K× 24位的程序空间。因此这里要提出跳转表的概念,当任务函数分配到大于64K× 24位的程序空间时编译器会链接出一个.handle的段,这个段中会定义一条GOTO指令,GOTO到实际的任务函数地址。所以才有堆栈初始化中PC<22:16>始终为0,而PC<15:0> = pxTaskCode的情况,因为若函数分配到了大于64K× 24位的程序空间,则pxTaskCode被链接为.handle段跳转表中的1条GOTO指令,借助该指令跳转到任务函数。
2)堆栈指针:
dsPIC33堆栈指针就是W15,大家清晰即可。
3)汇编指令:
为了真正的理解堆栈初始化及后续的任务切换过程,需要了解掌握如下的基本汇编语法。
同时xTaskCreate()进行任务创建的时候还干了一件事,就是通过调用函数prvAddNewTaskToReadyList() 将新建任务添加到就绪列表中,并且会找到这些新建任务中优先级最高的任务。这些任务创建后,任务调度器将通过xPortStartScheduler() 启动第一个任务,函数如下。
其中关键的portRESTORE_CONTEXT()的内容如下。首先看第一条指令,将所有任务中优先级最高任务的任务控制块TCB的第一个成员pxTopOfStack赋值给堆栈指针W15,接下来就比较简单,按照堆栈初始化的内容依次进行出栈,一直到SR寄存器出栈。
回过头我们继续看一下xPortStartScheduler(),其在执行完portRESTORE_CONTEXT() 后调用了一条汇编return指令,这个return指令的结果便是将启动的第一个任务的任务函数pxTaskCode的地址赋值给PC指针,之后根据任务函数pxTaskCode在flash中的存储位置,采用直接运行任务函数或通过跳转表GOTO间接运行任务函数。至此FreeRTOS的任务顺利完成了启动工作。