freeRtos学习之内核剖析

此文章参考了朱工的博客,真的很佩服朱工。

更为详细的讲解可以查看朱工的博客

开始对内核进行剖析,对于FreeRTOS内核来说,列表就是它最基础的部分。
个人认为内核最核心部分就是TCB控制块和列表 
一.列表和列表项 
列表被FreeRTOS调度器使用,用于跟踪任务,处于就绪、挂起、延时的任务,都会被挂接到各自的列表中。具体是怎么挂接到列表中的,下面会进行说明。
 FreeRTOS列表使用指针指向列表项。一个列表(list)下面可能有很多个列表项(list item),每个列表项都有一个指针指向列表,类似于一个双向链表。


 
两个重要的结构体:
列表结构体包括:1.用于检测列表项数据是否完整(同样需要打开对应的宏才能使用)2.该列表挂接的列表项数目,0表示列表 为空。3.用于遍历列表的指针
4.列表项 (用于标记列表结束)
 
列表项结构体包括1. 用于检查列表项数据是否完整(需要在projdefs.h中将对应的宏打开) 2.列表项值(通常是一个被跟踪任务的优先级或者调度事件的计数器值)3.指向上一个列表项 4.指向下一个列表项 5.指向任务TCB 6.指向列表项的列表
 
开始进行操作:想操作列表和列表项第一个工作肯定是先把他们初始化咯
初始化列表(将用于标记列表结束的列表项成员插入到列表中)
 
初始化后的列表如下:
 
初始化列表项:
 
做的工作就是确保列表项不在任何列表中(还是相对蛮好理解的)
 


初始化完成后,就是将列表项插入到列表中了
函数可以将pxNewListItem(函数的参数)指向的列表项插入到pxList指向的列表中,列表项在列表的位置由pxNewListItem->xItemValue(此值为列表项结构体中的列白项值 通常为任务优先级挥着计数器值)决定,按照降序排列。
插入函数的内部操作无非就是通过指针来实现的,完成后更新指针的指向,完成插入操作
例:将一个列表项为30的插入到列表中
 
如图可以看出列表中已经有了一个列表项为30的列表项了,然后再插入一个列表项为40的一个。如下图所示:
 
上面例子是将列表项插入到列表的指定位置(位置与列表项值相关)。还有一个将列百项插入到列表末端的API函数,如下所示:
 






二.从任务创建来解析内核
    
上图为任务创建的API函数,传入指点的值就可以创建一个任务了,但是他只是一个宏定义真正调用的函数时xTaskGenericCreate()。
当你创建任务的时候,函数就会创建一个TCB任务控制块,TCB任务块用于存储任务的状态信息,包括任务运行时的环境。
同样任务控制块也有一个很大的机构体如下所示:
  


这个结构体很大,也说明他很重要,是内核的主要部分,我觉得他更像是一个交通枢纽,通过他来得到和传递一些信息到指定的位置。






TCB任务块中主要包括:任务的状态列表项和事件列表项,任务的优先级,指向任务堆栈起始位置的指针,任务描述的数组,保存临界区嵌套深度的变量




调度器就是通过把任务TCB中的状态列表项xStateListItem和事件列表项xEventListItem被FreeRTOS调度器使用,用于跟踪任务,处于就绪、挂起、延时的任务,都会被挂接到各自的列表中。  (在task.c中,定义了一些静态列表变量,其中有就绪、阻塞、挂起列表,例如当某个任务处于就绪态时,调度器就将这个任务TCB的xStateListItem列表项挂接到就绪列表。)


通俗的说这个过程就是:
被创建的任务状态变了—》告诉自己的那个TCB块(列表项给相应的值)--》
相应的列表项挂接到对应的列表中(调用插入函数)。
用户程序调用任务创建函数,任务创建同时创建TCB,TCB监控你的状态,TCB状态对应相应的列表项,相应的列表项插入到对应的列表中。系统查询,例如调度器查询就绪列表中哪个优先级最高,然后执行对应的函数。


下面详细说一下任务创建后做的一些处理:
1.任务创建调用xTaskGenericCreate()
2. xTaskGenericCreate()创建任务,采用动态分配内存,调用函数prvAllocateTCBAndStack()创建任务堆栈和任务TCB
3.初始化任务TCB必要的字段
调用函数prvInitialiseTCBVariables()初始化任务TCB必要的字段。在调用创建任务API函数xTaskCreate()时,参数pcName(任务描述)、uxPriority(任务优先级)都会被写入任务TCB相应的字段,TCB字段中的xStateListItem和xEventListItem列表项也会被初始化,初始化后的列表项如图2-1所示。在图2-1中,列表项xEventListItem的成员列表项值xItemValue被初始为4,这是因为我在应用中设置的最大优先级数目(configMAX_PRIORITIES)为5,而xEventListItem. xItemValue等于configMAX_PRIORITIES减去任务A的优先级(为1),即5-1=4。这一点很重要,在这里xItemValue不是直接保存任务优先级,而是保存优先级的补数,这意味着xItemValue的值越大,对应的任务优先级越小。FreeRTOS内核使用vListInsert函数将事件列表项插入到
一个列表,这个函数根据xItemValue的值的大小顺序来进行插入操作。使用宏listGET_OWNER_OF_HEAD_ENTRY获得列表中的第一个列表项的xTiemValue值总是最小,也就是优先级最高的任务!
此外,TCB其它的一些字段也被初始化,比如临界区嵌套次数、运行时间计数器、任务通知值、任务通知状态等,
4.初始化任务堆栈
 调用函数pxPortInitialiseStack()初始化任务堆栈,并将最新的栈顶指针赋值给任务TCB的pxTopOfStack字段。
 调用函数pxPortInitialiseStack()后,相当于执行了一次系统节拍时钟中断:将一些重要寄存器入栈。虽然任务还没开始执行,也并没有中断发生,但看上去就像寄存器已经被入栈了,并且部分堆栈值被修改成了我们需要的已知值。对于不同的硬件架构,入栈的寄存器也不相同,所以我们看到这个函数是由移植层提供的。
5.进入临界区
6.当前任务数量加1
  在tasks.c中 ,定义了一些静态私有变量,用来跟踪任务的数量或者状态等等,其中变量uxCurrentNumberOfTasks表示当前任务的总数量,每创建一个任务,这个变量都会增加1
7.为第一次运行做准备
  如果这是第一个任务(uxCurrentNumberOfTasks等于1),则调用函数prvInitialiseTaskLists()初始化任务列表。FreeRTOS使用列表来跟踪任务,在tasks.c中,定义了静态类型的列表变量。   现在这些列表都要进行初始化,会调用API函数vListInitialise()初始化列表
8.更新当前正在运行的任务TCb指针
tasks.c中定义了一个任务TCB指针型变量:
      PRIVILEGED_DATA TCB_t * volatile pxCurrentTCB= NULL;
      这是一个全局变量,在tasks.c中只定义了这一个全局变量。这个变量用来指向当前正在运行的任务TCB,我们需要多了解一下这个变量。FreeRTOS的核心是确保处于优先级最高的就绪任务获得CPU运行权。在下一章讲述任务切换时会知道,任务切换就是找到优先级最高的就绪任务,而找出的这个最高优先级任务的TCB,就被赋给变量pxCurrentTCB。
      如果调度器还没有准备好(程序刚开始运行时,可能会先创建几个任务,之后才会启动调度器),并且新创建的任务优先级大于变量pxCurrentTCB指向的任务优先级,则设置pxCurrentTCB指向当前新创建的任务TCB(确保pxCurrentTCB指向优先级最高的就绪任务)
9.将新创建的任务加入就绪列表数组
      调用prvAddTaskToReadyList(pxNewTCB)将创建的任务TCB加入到就绪列表数组中,任务的优先级确定了加入到就绪列表数组的哪个下标。比如我们新创建的任务优先级为1,则这个任务被加入到列表pxReadyTasksLists[1]中
 


10.退出临界区
11.执行上下文切换
如果上面的步骤都正确执行,并且调度器也开始工作,则判断当前任务的优先级是否大于新创建的任务优先级。如果新创建的任务优先级更高,则调用taskYIELD_IF_USING_PREEMPTION()强制进行一次上下文切换,切换后,新创建的任务将获得CPU控制权,精简后的代码如下所示。
三.任务创建完了,接下来启动内核,当然在启动后,内核肯定也做了很多的处理。


任务创建完成后,静态变量指针pxCurrentTCB指向优先级最高的就绪任务。但此时任务并不能运行,因为接下来还有关键的一步:启动FreeRTOS调度器。
      调度器是FreeRTOS操作系统的核心,主要负责任务切换,即找出最高优先级的就绪任务,并使之获得CPU运行权。调度器并非自动运行的,需要人为启动它。
      API函数vTaskStartScheduler()用于启动调度器,它会创建一个空闲任务、初始化一些静态变量,最主要的,它会初始化系统节拍定时器并设置好相应的中断,然后启动第一个任务。


这个API函数首先创建一个空闲任务,空闲任务使用最低优先级(0级),空闲任务的任务句柄存放在静态变量xIdleTaskHandle中,可以调用API函数xTaskGetIdleTaskHandle()获得空闲任务句柄。
      如果任务创建成功,则关闭中断(调度器启动结束时会再次使能中断的),初始化一些静态变量,然后调用函数xPortStartScheduler()来启动系统节拍定时器并启动第一个任务。因为设置系统节拍定时器涉及到硬件特性,因此函数xPortStartScheduler()由移植层提供,不同的硬件架构,这个函数的代码也不相同。
在Cortex-M3架构中,FreeRTOS为了任务启动和任务切换使用了三个异常:SVC、PendSV和SysTick。SVC(系统服务调用)用于任务启动,有些操作系统不允许应用程序直接访问硬件,而是通过提供一些系统服务函数,通过SVC来调用;PendSV(可挂起系统调用)用于完成任务切换,它的最大特性是如果当前有优先级比它高的中断在运行,PendSV会推迟执行,直到高优先级中断执行完毕;SysTick用于产生系统节拍时钟,提供一个时间片,如果多个任务共享同一个优先级,则每次SysTick中断,下一个任务将获得一个时间片。关于详细的SVC、PendSV异常描述,推荐《Cortex-M3权威指南》一书的“异常”部分。
      这里将PendSV和SysTick异常优先级设置为最低,这样任务切换不会打断某个中断服务程序,中断服务程序也不会被延迟,这样简化了设计,有利于系统稳定。
      接下来调用函数vPortSetupTimerInterrupt()设置SysTick定时器中断周期并使能定时器运行这个函数比较简单,就是设置SysTick硬件的相应寄存器。
      再接下来有一个关键的函数是prvStartFirstTask(),


四,启动了内核,下面就是任务的切换部分了
FreeRTOS有两种方法触发任务切换:
1.执行系统调用,比如普通任务可以使用taskYIELD()强制任务切换,中断服务程序中使用portYIELD_FROM_ISR()强制任务切换


2.系统节拍时钟中断(异常中断)
对于第二种任务切换方法,在系统节拍时钟中断服务函数中,首先会更新tick计数器的值、查看是否有任务解除阻塞,如果有任务解除阻塞的话,则使能PandSV中断。PendSV中断的产生是通过代码:portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT实现的,它向中断状态寄存器bit28位写入1,将PendSV中断设置为挂起状态,等到优先级高于PendSV的中断执行完成后,PendSV中断服务程序将被执行,进行任务切换工作。
 
先强调图1-1中的几个术语,首先是“主堆栈指针MSP”和“进程堆栈指针PSP”。对于Cortex-M3硬件,当系统复位后,默认使用MSP指针。MSP指针用于操作系统内核以及处理异常(也就是说中断服务程序中默认强制使用MSP指针,这是硬件自动设置的)。任务(进程)使用PSP指针,操作系统负责从MSP指针切换到PSP指针。这个过程在《FreeRTOS高级篇3---启动调度器》一文的最后部分中进行了讲解:在SVC中断服务程序中启动第一个任务,当从SVC中断服务退出前,通过向r14寄存器最后4位按位或上0x0D,使得硬件在退出时使用进程堆栈指针PSP完成出栈操作并返回后进入线程模式、返回Thumb状态。
      其次,“堆栈”和“任务堆栈”也值得强调一下。每个任务都有自己的“任务堆栈”,在任务创建时会创建指定大小的任务堆栈,这是任务能够独立运行的前提条件之一。在任务中定义的局部变量,会优先使用寄存器,寄存器不够时就使用任务堆栈的空间。如果在任务中调用其它函数,则调用前的保存信息也存到任务堆栈中去。根据任务代码来估算任务堆栈的大小是件十分重要的技能。前面也说了,Cortex-M3硬件有两个堆栈指针,操作系统内核以及异常处理程序中使用MSP指针,所以它们也需要一个堆栈空间,我们称之为“堆栈”,这个堆栈空间和任务堆栈空间在物理上是绝对不可以重叠的,图1-2展示了一个编译好的程序可能的RAM分配情况(堆栈向下生长)。
 


 
五,任务之间的通信
任务之间通讯方式有很多,队列啊,二值信号量啊,互斥信号量啊,其实都是一样的,类似于数组和变量的关系。
 队列是FreeRTOS主要的任务间通讯方式。可以在任务与任务间、中断和任务间传送信息。发送到队列的消息是通过拷贝实现的,这意味着队列存储的数据是原数据,而不是原数据的引用。同样它也有一个数据结构:所有的队列API都是围绕他的结构体展开的。
创建过程:
 
初始化后的队列内存如下图:
 
 


入队的流程:
 


 
 当任务将数据入队时,如果队列未满或者以覆盖式入队,情况是最简单的,调用函数prvCopyDataToQueue()将要入队的数据拷贝到队列。这个函数处理三种入队情况,第一种是队列项大小为0时(即队列结构体成员uxItemSize为0,比如二进制信号量和计数信号量),不进行数据拷贝工作,而是将队列项计数器加1(即队列结构体成员uxMessagesWaiting++);第二种情况是从队列尾入队时,则将数据拷贝到指针pxQueue->pcWriteTo指向的地方、更新指针指向的位置、队列项计数器加1;第三种情况是从队列首入队时,则将数据拷贝到指针pxQueue->u.pcReadFrom指向的地方、更新指针指向的位置、队列项计数器加1。如果是覆盖式入队,还会调整队列项计数器的值。
      完成数据入队操作后,还要检查是否有任务因为等待出队而阻塞,因为这次数据入队,队列至少有一个队列项,如果有阻塞任务,则阻塞的最高优先级任务可以解除阻塞了。
      因等待出队而阻塞的任务会将任务的事件列表项(即任务TCB结构体成员xEventListItem,我们在《FreeRTOS高级篇2---FreeRTOS任务创建分析》一文中讲到过事件列表项,它是任务TCB的一个结构体成员)挂接到队列的等待出队列表上(即队列结构体成员xTasksWaitingToReceive)。现在,因为要解除任务阻塞,我们需要将任务的事件列表项从队列的等待出队队列上删除,并且将任务移动到就绪列表中。这一切,都是调用函数xTaskRemoveFromEventList()实现的。
      之后,如果解除阻塞的任务优先级比当前任务优先级更高,则触发一个PendSV中断,等退出临界区后,进行上下文切换。入队任务完成。
      上面讨论了最理想的情况,过程也简洁明了,但如果任务入队时,队列满并且不允许覆盖入队,则情况会变得复杂起来。
      在这种情况下,先看一个简单分支:阻塞时间为0的情况。设置阻塞时间为0意味着当队列满时,函数立即返回,返回一个错误代码,表示队列满。
      如果阻塞时间不为0,则本任务会因为等待入队而进入阻塞。在将任务设置为阻塞的过程中,是不希望有其它任务和中断操作这个队列的事件列表的(队列结构体成员xTasksWaitingToReceive列表和xTasksWaitingToSend列表),因为操作队列事件列表可能引起其它任务解除阻塞,这可能会发生优先级翻转。比如任务A的优先级低于本任务,但是在本任务进入阻塞的过程中,任务A却因为其它原因解除阻塞了,这显然是要绝对禁止的。因此FreeRTOS使用挂起调度器来简单粗暴的禁止其它任务操作队列,因为挂起调度器意味着任务不能切换并且不准调用可能引起任务切换的API函数。
      但挂起调度器并不会禁止中断,中断服务函数仍然可以操作队列事件列表,可能会解除任务阻塞、可能会进行上下文切换,这是不允许的。于是,解决办法是不但挂起调度器,还要给队列上锁!
      队列结构体中有两个成员跟队列上锁有关:xRxLock和xTxLock。这两个成员变量为queueUNLOCKED(宏,定义为-1)时,表示队列未上锁;当这两个成员变量为queueLOCKED_UNMODIFIED(宏,定义为0)时,表示队列上锁。
      给队列上锁是调用宏prvLockQueue()实现的,代码很简单,将队列结构体成员xRxLock和xTxLock都设置为queueLOCKED_UNMODIFIED。
      我们看一下给队列上锁是如何起作用的。当中断服务程序操作队列并且导致阻塞的任务解除阻塞时,会首先判断该队列是否上锁,如果没有上锁,则解除被阻塞的任务,还会根据需要设置上下文切换请求标志;如果队列已经上锁,则不会解除被阻塞的任务,取而代之的是,将xRxLock或xTxLock加1,表示队列上锁期间出队或入队的数目,也表示有任务可以解除阻塞了。这部分代码在带中断保护的入队和出队API函数中,后面我们会讲到,这里先有个印象。
      有将队列上锁操作,就会有解除队列锁操作。函数prvUnlockQueue()用于解除队列锁,将可以解除阻塞的任务插入到就绪列表,解除任务的最大数量由xRxLock和xTxLock指定。
      经过一系列的逻辑判断,发现本任务还是要进入阻塞状态,则调用函数vTaskPlaceOnEventList()来实现。这个函数将揭示任务因等待特定事件而进入阻塞的详细步骤,其实非常简单,只有两步:第一步,将任务的事件列表项(任务TCB结构体成员xEventListItem)插入到队列的等待入队列表(队列结构体成员xTasksWaitingToSend)中;第二步,将任务的状态列表项(任务TCB结构体成员xStateListItem)从就绪列表中删除,然后插入到延时列表中,任务的最大延时时间放入xStateListItem. xItemValue中,每次系统节拍定时器中断服务函数中,都会检查这个值,检测任务是否超时。
      当任务成功阻塞在等待入队操作后,当前任务就没有必要再占用CPU了,所以接下来解除队列锁、恢复调度器、进行任务切换,下一个处于最高优先级的就绪任务就会被运行了。


 

  • 14
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: FreeRTOS是一款免费的实时操作系统内核,适用于嵌入式系统的开发项目。该内核实现与应用开发实战指南pdf是一本详细介绍FreeRTOS内核实现和应用开发的书籍。 该书首先对FreeRTOS内核进行了详细介绍,包括内核的组件、任务管理、调度器、定时器等方面。在介绍中,作者比较详细地讲解了FreeRTOS内核的工作原理,并通过源代码的分析,形象地展现了其内部实现过程。 接下来,书籍着重讲解了如何应用FreeRTOS来实现嵌入式系统的项目开发。作者通过详细的案例说明,让读者能够更好地掌握FreeRTOS的应用技能。在案例中,介绍的涉及多种应用,例如中断处理、串口通信、LED、温度传感器等等。通过这些应用案例的学习,读者能够更好地理解FreeRTOS在实际项目中的应用,并能够独立设计和开发嵌入式系统项目。 除此之外,书籍还介绍了FreeRTOS针对嵌入式系统的一些高级特性,例如内存管理、IPC机制、任务优先级等等。这些特性的介绍,让读者能够更深刻地了解FreeRTOS的设计思想和应用方法。 总体来说,该书籍对FreeRTOS内核的实现和应用开发都进行了详尽的介绍。通过学习该书籍,读者可以很好地掌握FreeRTOS的开发方法,使其能够独立开发嵌入式系统。 ### 回答2: FreeRTOS是一款非常流行的嵌入式操作系统,广泛应用于很多不同领域的嵌入式系统中。《FreeRTOS内核实现与应用开发实战指南》是一本专门介绍FreeRTOS的书籍,对其内核实现以及应用开发提供了非常好的指导和实践经验。 本书首先介绍了操作系统的基本概念和原理,然后深入解析了FreeRTOS的架构、调度器、任务管理、内存管理等核心模块的实现原理。读者不仅可以了解FreeRTOS在底层如何运作,还可以了解其设计理念、特点和优势。 同时,本书还提供了很多实战经验,包括如何使用FreeRTOS进行常见的任务处理、通信、同步等操作,如何进行系统调试和性能优化等。读者可以通过实际的项目案例来深入理解FreeRTOS的应用开发。 《FreeRTOS内核实现与应用开发实战指南》不仅适合嵌入式开发工程师、系统架构师、嵌入式软件设计师等专业人士,也可以作为学习嵌入式操作系统的入门读物。无论是对于FreeRTOS的初学者还是有一定经验的开发人员,本书都提供了非常有价值的参考和实践指南。 ### 回答3: FreeRTOS是一个适用于嵌入式系统的开源实时操作系统。本书《FreeRTOS内核实现与应用开发实战指南》是作者胡宝清著作的一本技术书籍,主要介绍了FreeRTOS操作系统的相关实现和应用开发方法。书中具体内容包括简介、任务调度、任务间通信、内存管理、中断处理、时间管理和应用示例等方面。 通过本书,读者能够深入了解FreeRTOS内核实现和应用开发的实战指南,掌握了相关的调试技巧和调优方法,使得在实际应用开发中更加顺利。其中,作者重点讲解了任务调度、任务间通信、内存管理和中断处理等核心内容,这对于初学者来说尤为重要,能够让读者更好地理解FreeRTOS操作系统的底层原理。 另外,本书还提供了大量的应用示例,包括LED、键盘、独立按键、定时器和串口应用等,这些示例不仅能够让读者快速入门,同时也可以为读者带来更多的启发和想法。最后,本书还提供了相关的代码,可以让读者更好地学习和实践。 总的来说,本书是一本非常实用的FreeRTOS技术书籍,不仅适合嵌入式系统相关从业者,也适合对于FreeRTOS感兴趣的普通读者进行学习和参考。通过学习本书,读者能够更好地理解FreeRTOS内核实现和应用开发的实战指南,为嵌入式系统开发和应用提供更全面的技术支持。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值