第一部分 裸机系统和RTOS系统
1、裸机:不带任何操作系统,又称为前后台系统,前台系统指的中断函数,后台系统指的大循环。
缺点: ①:实时性差,轮流执行;
②:delay,空等待,CPU不执行其他代码
③:结构臃肿,实习功能都放在无限循环
2、RTOS:实时操作系统
优点:①:分而治之,实现功能划分为多个任务
②:延时函数:任务调度
③:抢占式:高优先级任务抢占低优先级任务
④:任务堆栈:每个任务都有自己的栈空间
注意:1.中断可以打断任意任务 2.任务可以同等优先级
3、任务调度
抢占式调度:主要针对优先级不同的任务,每个任务都有一个优先级,优先级高的任务可以抢占优先级低的任务。
时间片调度:主要针对优先级相同的任务,当多个任务的优先级相同时,任务调度器会在每一次系统时钟节拍到的时候切换任务。
第二部分 任务定义
1、任务
根据功能的不同,把裸机系统分割为⼀个个独⽴的⽆线循环且⽆法返回的函数。我们把这种函数称 之为任务。
注意:
1.输入参数为void*,这样保证可以给任务传递需要的值;
2.任务没有返回参数
在FreeRTOS中,为了简化任务的管理和调度,尽可能地减少了内核的复杂性和资源占用。任务的执行是一个无限循环(infinite loop),任务内部的逻辑通过不断循环执行来实现。当任务执行结束时,其实际的返回值并不会被使用,因为任务并不会像函数一样被调用者获取其返回值。简化任务的设计也有助于提高系统的实时性和效率,因为不需要额外的堆栈空间来保存任务的返回值,也不需要额外的处理来管理返回值的传递和处理。
3.无限循环
(1)连续性:死循环使得任务能够持续执行,不会在任务完成一次执行后立即终止。这需要不断地执行某项任务或监控某个状态的任务。
(2)周期性调度: 实时操作系统通常会使用调度器来管理任务的执行,根据任务的优先级和调度算法来决定任务的执行顺序。通过死循环,任务可以周期性地执行,并根据调度器的调度策略来确保各个任务的执行顺序和时间片分配。 (3)资源管理: 死循环使得任务能够在执行过程中管理自己的资源,如分配和释放内存、访问外设等。任务可以根据需要重复执行特定的操作,而不必担心执行完一次后就被系统终止。
2、任务栈![](https://i-blog.csdnimg.cn/blog_migrate/2ab18503cb5f57cc354798d7278f8177.png)
首先我们知道所有函数开辟是需要栈来存放临时变量和他调用结束后返回的地址,所以每个任务都需要一个任务栈来存放他任务结束后需要返回的地址或被某中断打断后的保护现场。
每个任务都有自己的栈空间,用来保护每个任务的现场。函数在中断、任务调度、自己调用函数等情况下都需要保护自己的现场,就需要自己的任务栈。
3、系统栈
系统栈中保存中断。每个systick中断都会使用到系统栈。
疑点1:任务栈为什么会在.bss段:
-
内存管理:将任务栈分配到.bss段可以帮助在编译时确定任务栈的大小,并在程序加载时根据需要动态分配内存,从而更好地管理内存资源。
-
未初始化数据: 任务栈在任务创建时通常是未初始化的,因为任务的执行会在创建后立即开始。.bss段用于存储未初始化的全局或静态变量,因此将任务栈分配到.bss段可以保持一致性
-
疑点2:系统栈和任务栈为什么不在一块
-
栈大小的灵活配置:不同的任务可能需要不同大小的栈空间,根据任务的需求分配独立的任务栈可以更好地控制栈大小。
-
避免系统栈溢出影响任务:如果任务共享系统栈,当某个任务的栈溢出时,可能会影响整个系统的稳定性,导致系统崩溃。通过使用独立的任务栈,可以限制栈溢出对系统的影响,提高系统的稳定性和可靠性。
-
任务切换效率:独立的任务栈可以简化任务切换过程,使任务切换更加高效。每个任务的栈指针可以轻松地保存和恢复,而不会涉及到系统栈的复杂操作。
4、任务控制块
类似进程中的进程控制块PCB,为了方便管理任务,任务栈,任务列表,那么引入一个结构体TCB来控制他们三者。
typedef struct tskTaskControlBlock
{
/*< 任务栈的栈顶指针 */
volatile uint32_t *pxTopOfStack;
/*< 任务列表的节点指针,后续可以通过这个挂在任务列表上 */
ListItem_t xStateListItem;
/*< 事件列表节点指针,这个暂时先不用管,后续会讲 */
ListItem_t xEventListItem;
/*< 任务优先级 */
unsigned long uxPriority;
int xTicksToDelay;
/*< 任务栈起始地址,也就是我们定义的数组起始地址 */
int *pxStack;
/*< 任务名称 */
char pcTaskName[ configMAX_TASK_NAME_LEN ];
}tskTCB;
typedef tskTCB TCB_t;
有了栈顶指针和任务栈起始指针就可以遍历整个任务栈,提高效率
5、状态转换![](https://i-blog.csdnimg.cn/blog_migrate/b37969275df52f07209bf62fdc28b575.png)
5.1列表
typedef struct xLIST
{
listFIRST_LIST_INTEGRITY_CHECK_VALUE //用来检查链表完整性
configLIST_VOLATILE UBaseType_t uxNumberOfItems; //记录列表中列表项的数量
ListItem_t * configLIST_VOLATILE pxIndex; //指向列表项,用于遍历列表
MiniListItem_t xListEnd; //这是来表示列表结束用的,指向迷你列表项
listSECOND_LIST_INTEGRITY_CHECK_VALUE //用来检查链表完整性
} List_t;
5.2列表项
struct xLIST_ITEM
{
listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE //用来检查列表完整性
configLIST_VOLATILE TickType_t xItemValue; //存储列表项的值
struct xLIST_ITEM * configLIST_VOLATILE pxNext; //指向下一个列表项
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; //指向前一个列表项
void * pvOwner;//记录列表项被谁拥有,这里是看被哪个任务拥有
void * configLIST_VOLATILE pvContainer;//还是记录列表项被谁拥有,但是这里是看被哪个列表拥有
listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE //用来检查列表完整性
};
typedef struct xLIST_ITEM ListItem_t;
这样理解更容易,其实就是一个大的双向循环链表。
目前我个人的理解就是:就绪链表中有很多优先级的链表,某个链表中的结点项都是相同优先级。所以在相同优先级这里用的是时间片轮转。
第三部分 创建任务
一、动态创建任务
task.c 721行
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
const char * const pcName,
const configSTACK_DEPTH_TYPE usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask )
1.TaskFunction_t pvTaskCode:任务函数的指针。这个参数是一个函数指针,指向一个任务的实际执行代码。任务函数的原型必须符合 TaskFunction_t 类型,即 void func(void *pvParameters),其中 pvParameters 是一个指向和任务相关的参数的指针。
void fun(void*arg);//示例
2.const char * const pcName:任务的名称。这个参数是一个字符串常量,用于标识任务。
3.const configSTACK_DEPTH_TYPE usStackDepth:任务栈的大小。这个参数表示任务栈的深度、空间或者大小,以字节为单位。在创建任务时需要提供一个足够大的堆栈以确保任务可以运行。
4.void * const pvParameters:任务参数的指针。这个参数是任务的实际参数,它的类型可以是 void 指针或者其他任意类型的指针。在任务执行时,可以通过此参数来传递任务需要的参数。
5.UBaseType_t uxPriority:任务的优先级。这个参数用于指定任务的优先级,数值越大表示优先级越高。优先级的取值范围根据量化数值的位数而定,一般来说,取值范围是 0-31,在 FreeRTOS 中,0 号优先级最低,而 31 号优先级最高。
6.TaskHandle_t * const pxCreatedTask:用于返回创建的任务的句柄。创建任务成功后,系统将返回一个指向该任务的任务句柄,其实就是任务的任务堆栈,此参数就是保存这个任务句柄,其他API函数可能会使用到这个句柄。
TaskHandle_t xHandleTask1;//任务句柄
//任务执行函数
void Task1(void * param)
{
while (1)
{
printf("1");
}
}
//任务执行函数
xTaskCreate(Task1, "Task1", 100, NULL, 1, &xHandleTask1);//创建一个任务
二、静态创建任务
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 )
PRIVILEGED_FUNCTION;
通过静态创建方式,可以使用编译器的内存分配器来分配任务所需的内存空间,从而避免了动态分配内存的开销和潜在的内存泄漏问题。静态创建方式还可以在一些对内存使用有限制的场合下有效地管理系统资源,提高系统的稳定性和性能。
三、二者区别
静态创建任务是在编译时为任务分配内存,这意味着任务在运行时之前已经预分配了足够的内存。这种方法不需要在运行时使用动态内存分配函数,因此更加可靠和节省内存。在静态方式下,任务可以使用xTaskCreateStatic()函数创建。
动态创建任务是在运行时通过动态内存分配函数分配任务内存。这种分配方式可以更灵活地适应不同大小和数量的任务,并且支持删除或重新创建任务。然而,在动态方式下,程序需要在运行时使用动态内存分配函数,这可能会导致内存泄漏和堆碎片等问题。在动态方式下,任务需要使用xTaskCreate()函数创建。
需要注意的是,静态方式创建任务需要事先知道任务所需的内存大小,以及将任务的堆栈和控制块明确地分配给该任务。如果任务使用的内存超出了分配的内存,则可能会发生严重错误,例如内存泄漏或严重的崩溃。在动态方式下,内存分配在运行时动态进行,因此可以更好地处理任务所需的内存。
第四部分 相关API
1、删除任务
void vTaskDelete( TaskHandle_t xTaskToDelete )
2、任务状态
2.1 用于使当前任务暂停一段时间之后再执行,在等待期间FreeRTOS会尝试进行其他任务调度
void vTaskDelay( const TickType_t xTicksToDelay )
2.2 使任务等待到特定的时间点才重新变为就绪状态。
BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement )
上述区别:
vTaskDelay函数和vTaskDelayUntil函数都用于在FreeRTOS中实现任务的时间延迟,但它们的方式不同。
vTaskDelay函数通过传递一个相对延迟的节拍数来工作。任务会阻塞指定的节拍数,然后继续执行。这意味着vTaskDelay的延迟时间是相对于当前任务的执行时间而言的,实际的延迟时间可能会受到任务切换和系统负载的影响。因此,无法保证精确的延迟时间,可能会有一定的误差。
vTaskDelayUntil函数通过传递一个绝对时间点(以节拍数表示)来工作。任务会等待直到当前时间达到或超过传递的绝对时间点,然后继续执行。这意味着vTaskDelayUntil提供了更精确的延迟控制,可以实现准确的定时任务。您可以根据需要计算下一个执行时间点,并将其传递给vTaskDelayUntil函数,任务将在该时间点进行阻塞,确保精确的延迟时间。
vTaskDelay用于相对延迟,而vTaskDelayUntil用于绝对时间点延迟,使得在实现定时任务时更加方便和精确。
3.开启任务调度
void vTaskStartScheduler( void )
第五部分 任务调度![](https://i-blog.csdnimg.cn/blog_migrate/966e262809467b80e4981c5b7c695a40.png)
pxReadyTasksLists:这是一个数组,包含了多个链表,其数量等于configMAX_PRIORITIES,它用于存储处于就绪状态的任务。每个链表对应一个优先级,因此,数组中的每个元素存储了同一优先级的就绪任务。当任务准备好运行时,它将被添加到适当优先级的链表中,以等待被调度器选中执行。
xDelayedTaskList1 和 xDelayedTaskList2:这两个链表用于存储被延时挂起的任务。通常,xDelayedTaskList1 包含所有未溢出的延时任务,而 xDelayedTaskList2 用于存储延时已经溢出的任务。这种设计允许 FreeRTOS 处理不同时间范围内的延时任务。延时任务在指定的时间段内不会被执行,而是在延时到期后再被移到就绪链表。
pxDelayedTaskList 和 pxOverflowDelayedTaskList:这两个指针变量用于指向当前使用的延时任务链表。通常,pxDelayedTaskList 指向 xDelayedTaskList1 或 xDelayedTaskList2 中的一个,具体取决于当前的延时情况。这些链表用于存储不同时间范围内的延时任务。
xPendingReadyList:这个链表用于存储在调度器被挂起时已经准备好运行的任务。当调度器处于挂起状态时,如果有任务变为就绪状态,它们将被添加到这个链表中。当调度器被恢复时,这些任务将被移动到适当的 pxReadyTasksLists 中,以等待被调度执行。
第六部分 中断
1、临界段
1.定义 执行中不能被打断的代码段,最常见的就是对全局变量的操作。
2.面临打断:①外部中断②系统时钟(任务切换)
3.临界段保护:在代码执行过程中,将可以打断它的中断关闭,执行完再打开中断。
2、用于中断屏蔽的特殊寄存器
1)PRIMASK寄存器:此中断屏蔽寄存器⽤于禁⽌除NMI 和 HardFalut外的所有异常和中断。
2)FAUKTMASK寄存器:当他置1时,除了NMI外其余所有的中断都被屏蔽掉
3)BASEPRI寄存器:当它设为某个值后,所有优先级号⼤于等于此值的中断都被屏蔽掉。如果被设为 0,则不关闭任务中断。
3、关中断函数
①不带返回值的(不保护中断)
void vPortRaiseBASEPRI( void )
②带返回值的 (中断保护) 可嵌套
uint32_t ulPortRaiseBASEPRI( void )
返回值的关中断函数,可以⽤在中断中使⽤,因为保存了原先的 “basepir”寄存器的值。返回值作 为开中断的参数,将“basepir”寄存器的值设置为原来的样⼦。
4、开中断函数
①不带中断保护:vPortSetBASEPRI( 0 ) ----------->void vPortRaiseBASEPRI( void )
②带中断保护:vPortSetBASEPRI( x ): ------------>uint32_t ulPortRaiseBASEPRI( void )
5、进入和退出临界段
进⼊和退出临界段对应中断的关闭和打开。
基于前⾯开关中断的两种类型:带中断保护 和 不带中断保护。
我们的进⼊临界段函数有:
①带中断保护的 portSET_INTERRUPT_MASK_FROM_ISR();
②不带中断保护的 portENTER_CRITICAL();
我们的退出临界段函数有:
①带中断保护的 portCLEAR_INTERRUPT_MASK_FROM_ISR();
②不带中断保护的 portEXIT_CRITICAL()
5、临界段代码的应用
/* 在中断场合,临界段可以嵌套 */ { uint32_t ulReturn; /* 进入临界段,临界段可以嵌套 */ ulReturn = taskENTER_CRITICAL_FROM_ISR(); /* 临界段代码 */ /* 退出临界段 */ taskEXIT_CRITICAL_FROM_ISR( ulReturn ); } /* 在非中断场合,临界段不能嵌套 */ { /* 进入临界段 */ taskENTER_CRITICAL(); /* 临界段代码 */ /* 退出临界段*/ taskEXIT_CRITICAL(); }
一上是我目前对FreeRTOS的前面理解,对于中断这里还有很深的东西,需要花一定的时间去消化,在后续我会进一步深入,如果上述有什么问题也会及时修改,并做以补充。