FreertosPowerPC
目录
PowerPC
常用寄存器
异常处理
系统初始化和启动
链接和存储分布
链接脚本分析
init_table和zero_table
启动代码和RAM分布
从main()到第一个Task执行
Task创建和Heap初始化
创建Queue
Task执行
内存占用分析
总结
中断
中断和堆栈
外部中断过程
系统调用过程
关于抢占
PowerPC
CPU不仅仅是一种计算资源,它为操作系统的实现提供了基本逻辑机制和物理支持。比如CPU可以提供特权态和用户态,以及迁移方法,为现代操作系统内核和用户程序权限的分离提供了基础。因此,想要深入理解Freertos的运行机制,需要了解相应的CPU架构。我们的项目中采用了NXP MPC57XX,即PowerPC架构。
PowerPC是IBM推出的精简指令集计算机。这里简单介绍一下对于CPU来说非常重要的两种资源—寄存器和异常处理。
常用寄存器
PowerPC 的应用级寄存器分为三类:通用寄存器(GPR)、浮点寄存器(FPR])和专用寄存器(SPR)。
需要特别注意的寄存器如下:
1,GPR中的R1寄存器在我们的系统中充当栈指针寄存器SP。
2,MSR CPU状态寄存器。
3,SPR 给出处理器核心内部资源的状态并对其进行控制
- 指令地址寄存器(Instruction Address Register,IAR),即PC寄存器。
它是当前指令的地址。IAR 主要是由调试器使用,显示将要被执行的下一条指令。*5746C芯片手册中没有该寄存器,需要调查与SRR0寄存器的关系。 - 链接寄存器(Link Register,LR)
这个寄存器存放的是函数调用结束处的返回地址。某些转移指令可以自动加载 LR 到转移之后的指令。每个转移指令编码中都有一个 LK 位。如果 LK 为 1,转移指令就会将程序计数器移为 LR 中的地址。而且,条件转移指令bclr
转移到 LR 中的值。 - 定点异常寄存器(Fixed-Point Exception Register,XER)
这个寄存器存放整数运算操作的进位以及溢出信息。它还存放某些整数运算操作的进位输入以及加载和存储指令(lswx
和stswx
)中传输的字节数。 - 计数寄存器(Count Register,CTR)
这个寄存器中存放了一个循环计数器,会随特定转移操作而递减。条件转移指令bcctr
转移到 CTR 中的值。 - 条件寄存器(Condition Register,CR)
详情请参见如下链接:
IBM Developer 正在整合其语言站点组合。 – IBM Developer
异常处理
操作系统的实现依赖于CPU所能提供的中断和异常处理机制。
PowerPC CPU提供了几种基本的中断类型(参见5746C芯片手册63章Exception小节),对于我们来说,比较常见的有Machine Check,External Input,System Call。
- Machine Check由CPU检测到某些系统错误时产生,比如非法访问,bus error。
- External Input就是我们所说的外部中断,我们系统中所配的CAN, Watch Dog, DMA, Uart,硬件定时器等中断都属于这种中断,实际上,Freertos的心跳也是外部中断,使用的是硬件定时器。
- System Call中断是软件产生中断的机制,一般在系统调用时使用,用来从用户态进入内核态,使用内核所提供的功能。Freertos的目前只有三个系统调用使用了System Call:vPortYield,xPortRaisePrivilege,vPortLowerPrivilege。从这里可以看出,Freertos很多的API并不是在内核上下文中执行,而是在调用Task本身的上下文中。
所以,可以把我们开发过程中遇到的中断大致分为两类。PowerPC CPU中断和外部中断。他们之间的关系是:外部中断是PowerPC CPU中断的一种。由此,我们也可以在我们的系统里找到两个中断向量表。
Power PC的CPU中断的处理函数定义在interrupt_vectors.S(core_exceptions_table)文件中。我们平常所配的外部中断,其中断向量表定义在startup_MPC5746C.S(intc_vector_table)文件中。
外部中断到来时,CPU先根据core_exceptions_table找到相应的处理函数,在该汇编函数中再通过查找中断向量表intc_vector_table,找到具体的处理函数,也就是一般我们定义的CAN,DMA,KL15等中断的处理函数。详细过程请参见第三章。
系统初始化和启动
本节的最终目的是理解RAM中的数据分布,和系统中各寄存器的作用和更新时机,以及Freertos中关键变量的作用和使用方法。
链接和存储分布
首先介绍一下在这一过程中涉及到的几个重点概念:GNU 链接脚本,init_table,zero_table,启动代码。
其中,链接脚本为linker_flash.ld文件,而init_table,zero_table,启动代码都存在于startup_MPC5746C.S文件中。
工程编译阶段,链接器会根据链接脚本的配置扫描所有源文件并进行如下动作
- 计算各SECTION在Flash和RAM中的位置。
- 根据上面的计算结果,对init_table、zero_table和其他一些记录SECTION地址的变量赋值。init_table和zero_table记录了启动时,需要拷贝到RAM中的SECTION在Flash和RAM的开始/结束地址。启动时,启动代码会根据这两个table将Flash中部分内容加载到RAM中。
- 指定启动代码的入口位置。
在我们的系统中,init_table主要指定了data SECTION(已初始化全局变量)和外部中断向量表在Flash和RAM中的地址。zero_table指定了bss SECTION,即未初始化全局变量在Flash和RAM中的地址。
由此可见,text SECTION,即代码段没有copy到RAM里面,所以我们的系统中,所有的代码都在Flash中执行。
链接脚本分析
GNU LD脚本简介:
GNU LD 脚本学习笔记 - windtail - 博客园
主要包含m_text, m_data两个地址段。这两个段规定了各数据结构在Flash和RAM中的地址,其中m_text段规定了Flash中的数据分布,m_data段规定了RAM中的数据分布
Flash |
说明 |
是否需要copy到RAM中 |
---|---|---|
cpu0_reset_vector | ||
cpu2_reset_vector | ||
startup | 记录启动代码在Flash中的位置。 | 否 |
core_exceptions_table | PowerPC CPU异常向量表,外部中断是CPU异常中的一项(IVOR4)。 | 否 |
intc_vector_table | 外部中断向量表 | 是 |
text | 代码 | 否 |
init_table | 记录了data段和外部中断向量表在Flash和RAM中的位置。在启动代码中会利用该table将这两部分内容从Flash拷贝到RAM指定位置。 | 否 |
zero_table | 记录了bss段在Flash和RAM中的位置。在启动代码中会利用该table将RAM未初始化全局变量清0 | 否 |
interrupts_ram | 规定外部中断像量表在RAM中的位置 | 启动代码根据init_table,将intc_vector_table拷贝到这里 |
data | 规定已初始化全局变量在Flash和RAM中的位置 | 是,启动代码根据init_table进行拷贝。 |
bss | 规定未初始化全局变量在Flash和RAM中的位置 | 不拷贝,根据zero_table,将RAM相应位置初始化为0 |
stack |
规定系统堆栈在Flash和RAM中的位置。 这部分堆栈用于中断处理程序和Freertos内核,不同于Task和malloc所用堆栈。 |
不拷贝,根据本段初始化系统堆栈和堆栈指针。 |
init_table和zero_table
init_table和zero_table的定义如下。其实很多段是空的,需要主要关注的为加粗部分。这些变量的赋值都可以在链接脚本中找到。
/* Init table */
.section .init_table, "a"
.long 5
.long __VECTOR_TABLE
.long __VECTOR_RAM
.long __VECTOR_TABLE_COPY_END
.long __CUSTOM_ROM
.long __CUSTOM_RAM
.long __CUSTOM_END
.long __DATA_ROM
.long __DATA_RAM
.long __DATA_END
.long __SDATA_ROM
.long __SDATA_RAM
.long __SDATA_END
.long __CODE_ROM
.long __CODE_RAM
.long __CODE_END
/* Zero table */
.section .zero_table, "a"
.long 2
.long __SBSS_START
.long __SBSS_END
.long __BSS_START
.long __BSS_END
启动代码和RAM分布
启动代码的位置由链接脚本指定。linker_flash.ld中将_start符号指定为启动位置:ENTRY(_start)。_start符号位于startup_MPC5746C.S中。
我们跟踪一下它所进行的动作。
- 关闭中断。wrteei 0
- 初始化通用和专用寄存器
- 关闭Watch Dog
- 配置系统栈指针,将R1寄存器赋值为链接脚本中的__SP_INIT
- 系统初始化:SystemInit()
- 通过MC_ME寄存器配置并使能CPU2
- 为当前core配置外部中断控制器
- 配置PowerPC CPU中断向量表,即core_exceptions_table/VTABLE。
- 配置CPU的DMA访问权限。
- data,bss,中断向量表初始化:init_data_bss()
- 从链接脚本定义的符号__COPY_TABLE和__ZERO_TABLE取出init_table和zero_table的地址。
- 利用init_table将外部中断向量表和已初始化全局变量copy到RAM中
- 将当前CPU的外部中断向量表赋值为intc_vector_table。 *s_vectors[coreId] = (uint32_t)__VECTOR_RAM; s_vectors存的是寄存器INTC_IACKR的地址,该寄存器保存了外部中断控制器的中断向量表基地址。
- 利用zero_table将RAM中未初始化全局变量的所在区域都赋值为0.
- 使能中断:wrteei 1
跳转main函数执行。e_bl main
此时内存分布大致如下图:
图1.1
从main()到第一个Task执行
*我们的main函数中首先使能了一些驱动,特别是在wvdTskConfig_Init中会使能某些中断,调用各Task的API。这样做可能会有问题,因为此时Freertos调度尚未开始,而中断处理函数或Task的API中可能会使用一些敏感的Freertos API,造成系统异常。
这是一个待修改的课题。我们这里的讨论先忽略这一部分。
除此之外,我们main函数里进行的第一个动作是调用xTaskCreate创建Task。xTaskCreate中需要调用pvPortMalloc为Task分配TCB结构和堆栈,而第一次调用pvPortMalloc会初始化堆,所以我们要同步分析一下堆的初始化和分配策略。
Task创建和Heap初始化
我们系统采用的堆分配策略为heap4.c,可参见如下链接:轻量级操作系统FreeRTOS的内存操作机制(三) - 吴跃前 - 博客园
首先要说明的是Freertos的堆是一个全局变量,应当位于bss段并且在启动代码中被初始化为全0。并且,这个堆要和链接脚本中的stack Section做区别,那个多为中断使用。其定义如下。
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
configTOTAL_HEAP_SIZE目前在我们的系统中定义为:FreeRTOSConfig.h:#define configTOTAL_HEAP_SIZE ( ( size_t ) 81920 )。约80K。
pvPortMalloc的调用过程如下。
- 判断是否为第一次malloc,如果是,则调用prvHeapInit()初始化堆。
- 根据ucHeap的地址和大小初始化空闲Block链表xStart和pxEnd。此时只有一个空闲Block。
- 赋值xMinimumEverFreeBytesRemaining,用来记录历史最小堆空间。
- 赋值xFreeBytesRemaining,用来记录剩余的堆空间大小。
- xBlockAllocatedBi
- 如果申请的空间小于xFreeBytesRemaining,则开始通过xStart和pxEnd查找第一符合要求的Block。如果能找到则分配空间。
- 更新xFreeBytesRemaining和xMinimumEverFreeBytesRemaining
xTaskCreate创建Task的过程如下:
- 判断堆栈是向上增长还是向下增长,我们的系统里面现在配置为向下增长:portmacro.h: #define portSTACK_GROWTH ( -1 )
- 调用pvPortMalloc为Task分配栈空间。
- 调用pvPortMalloc为Task的TCB结构分配空间
- 初始化Task: prvInitialiseNewTask(),主要是初始化TCB结构并构造第一个栈帧。
- 记录栈地址,优先级,Name等等各项属性到TCP结构
- 设置State和Event列表*需要继续调查。
- 调用pxPortInitialiseStack()构建第一个栈帧,让Task看起来像已经执行过。这时候,函数指针pxTaskCode会赋给栈中LR寄存器和SRR0(saved address of offending instruction)寄存器对应的地址,这样当Task开始执行时,将堆栈弹出,就会从我们写的第一行代码开始执行。
- 将Task加入Ready列表:prvAddNewTaskToReadyList()
- 首先进入临界区,防止在更新Task list时被中断打断,中断中也有可能更新task list. taskENTER_CRITICAL()
- 如果没有当前任务,则将其设为当前任务pxCurrentTCB = pxNewTCB;如果已经有Task了,则比较两个Task的优先级,将pxCurrentTCB赋值为高优先级Task的TCB结构。
- 如果这是第一个Task则调用prvInitialiseTaskLists来初始化Task list。(使用全局变量uxCurrentNumberOfTasks来判断是否是第一个Task)该函数中会初始化各优先级的Readylist,同时初始化DelayedTaskList。*各List的详细作用后续调查。
- 调用prvAddTaskToReadyList将该Task追加至所在优先级的Ready list的末尾。该过程中也会更新全局变量uxTopReadyPriority为当前最高就绪Task优先级。
- 退出临界区。
- 如果调度已经开始,并且该Task是优先级最高的Task则调入执行:taskYIELD_IF_USING_PREEMPTION。不过我们的系统里不存在这种情况,我们是先创建所有Task,后开始调度。
*待更新:此时内存分布
创建Queue
xQueueGenericCreate
- 计算Queue数据存储占用空间,uxQueueLength * uxItemSize。
- 调用pvPortMalloc为Queue管理结构Queue_t和数据存储分配空间。所以Queue是位于堆中的。
- prvInitialiseNewQueue():初始化Queue_t结构。并通过Queue_t的xTasksWaitingToSend判断是否有Task正在写入该Queue,如果是的话则调用queueYIELD_IF_USING_PREEMPTION进行调度,该函数会调用xPortSyscall,调用号为0。
通过se_sc命令发起SystemCall中断,系统调用号存于r3寄存器
在interrupt_vectors.S 文件里的core_exceptions_table/VTABLE中断向量表中,查找并进入SysemCall的中断处理函数,名字也是xPortSyscall。
该函数中发现需要调用vPortYield,该函数中会对当前Task进行压栈,切换到系统堆栈并最终跳转vTaskSwitchContext。详见第三章系统调用过程分析。
*待更新:此时内存分布
Task执行
上面的工作完成后,下一步是开始调度。
vTaskStartScheduler()
- 创建idle Task。*待完善
- 创建Timer Task。*待完善
- 关中断
- 初始化系统心跳数为0,xTickCount = ( TickType_t ) 0U
- 设置系统心跳中断,开始调度。xPortStartScheduler():
- 通过INT_SYS_InstallHandler安装中断处理函数vPortTickISR()。该函数会通过__VECTOR_RAM变量找到ram中的外部中断向量表,并修改对应的函数指针。
- 使能系统心跳中断,设置中断优先级,设置心跳间隔。
- 运行第一个Task: vPortStartFirstTask()
- 因为这里要切换到Task中执行,切换堆栈。所以第一步保存系统堆栈指针到全局变量pxSystemStackPointer。即把当前R1寄存器的保存到pxSystemStackPointer。
- 通过全局变量pxCurrentTCB找到当前Task的TCB结构。这个变量前面创建Task的时候已经被赋值为我们创建的最高优先级的Task。见xTaskCreate的5.b小节。
根据该TCB结构找到该Task的堆栈指针,并赋值给R1寄存器。 - portRESTORE_CONTEXT:利用R1寄存器弹出堆栈。这时候,LR,R1,SRR,CTR等等寄存器都被更新,做好了进入Task执行的准备。还记得,Task的第一个堆栈是在xTaskCreate的4.c小节特意构造的,所以下一步就是进入Task函数的第一条指令进行执行。
- 利用se_rfi指令切换上下文。进入Task执行。
从此Task就开始执行了。Freertos的调度和抢占也就开始了。最常见的调度就是在上面第五步设置的定周期系统心跳中断。我们看一下这个中断里干了什么。
vPortTickISR()
当系统心跳(此为硬件定时器中断,时外部中断的一种,该中断触发时的系统堆栈切换在第三章介绍)超时的时候,vPortTickISR()会被调用:
- 关闭中断:vPortMaskInterrupts->wrteei 0 *
- xTaskIncrementTick:递增系统心跳计数xTickCount,并判断是否需要调度。
- 如果心跳计数大于xNextTaskUnblockTime,则遍历pxDelayedTaskList中是否有定时超时的Task。如果没有,则更新xNextTaskUnblockTime为最近的要超时的Task的超时时间。
如果有的话,就将找到的这些Task移出Block List,移出Event List,移入Ready List。如果找到的Task的优先级中,有大于当前Task(pxCurrentTCB)优先级的Task。则判断为需要调度。 - 如果当前优先级的Ready List大于1,也判断为需要调度。因为Freetros中,相同优先级的Task采用时间片轮转,当前Task既然已经运行了一个时间片,就要调另一个相同优先级的Task来执行。
- 返回是否需要调度标志位。
- 如果心跳计数大于xNextTaskUnblockTime,则遍历pxDelayedTaskList中是否有定时超时的Task。如果没有,则更新xNextTaskUnblockTime为最近的要超时的Task的超时时间。
- prvPortTimerReset()
- 如果第二步判断是需要调度,则通过vTaskSwitchContext切换Task。
- 通过全局变量uxSchedulerSuspended判断当前是否已停止调度,如果是的话则赋值xYieldPending = pdTRUE,如果不是则进行如下操作
- 设置xYieldPending = pdFALSE,更新Task统计信息,调用taskCHECK_FOR_STACK_OVERFLOW检查栈溢出。
- 调用taskSELECT_HIGHEST_PRIORITY_TASK挑选下一个要执行的Task。
- 通过全局变量uxTopReadyPriority获取当前最高就绪Task优先级。
- 查找该优先级ReadyTaskList,获取该列表第一个元素的TCB结构,并赋值给pxCurrentTCB。
- 至此,下一个时间片要执行的Task已经选定好。vPortTickISR结束后会返回到外部中断处理函数vPortISRHandler中,该函数会调用portPOP_TASK将pxCurrentTCB中保存的堆栈指针更新到r1寄存器,然后调用portRESTORE_CONTEXT将r1所指向的堆栈加载到CPU各寄存器中。这样,vPortISRHandler返回后CPU就会执行pxCurrentTCB所指向的Task。详情见第三章外部中断过程。
内存占用分析
总结
中断
中断和堆栈
外部中断使用的是系统堆栈,即图1.1的System Stack。System Stack的大小和地址在编译时根据链接脚本linker_flash.ld确定。具体的来说,栈底的地址会存放于符号__SP_INIT中。
系统启动时,startup_MPC5746C.S中的启动代码会将__SP_INIT赋值到r1寄存器。在这之后函数调用开始进行。
从启动代码到Task开始调用之前,所有的函数调用都是用的这个堆栈。 第二章Task执行小节的第6步讲到,vTaskStartScheduler会调用vPortStartFirstTask函数,该函数会将系统运行到此时的r1寄存器的值保存到全局变量 pxSystemStackPointer中,之后CPU转到Task的堆栈中执行。之后外部中断触发时,cpu会根据CPU异常向量表core_exceptions_table跳转到vPortISRHandler中执行,该函数会调用portLOAD_SYSTEM_STACK_POINTER将之前保存在pxSystemStackPointer中的值加载到r1寄存器,之后才会跳转具体的中断服务函数。
所以,我们所配置的外部中断都跑在System Stack里面。同时,中断结束时,全局变量 pxSystemStackPointer用来保存System Stack的栈指针。
外部中断过程
第一章说过我们系统里的中断大致可分为两个层次,CPU中断和外部中断,外部中断时CPU中断的一种,即0x40 External Input(图3.1),详见芯片手册63.8节。
还记得在SystemInit()函数中将core_exceptions_table配置到CPU的IVPR寄存器中的(第二章启动代码和RAM分布小结第5步),所以当0x40即外部中断到来时,CPU将查找core_exceptions_table并跳转到函数vPortISRHandler(图3.2)中执行。
图3.1
图3.2
我们来详细分析一下这个过程:
- CPU收到0x40中断,通过IVPR寄存器保存的中断向量表core_exceptions_table找到并跳转外部中断处理函数vPortISRHandler。 e_b vPortISRHandler
- vPortISRHandler函数中:
- 调用portSAVE_CONTEXT,利用r1寄存器将当前的CPU各寄存器保存到当前的堆栈,即当前Task的堆栈。
- 调用portPUSH_TASK,将当前的r1寄存器的值保存到pxCurrentTCB中,即当前Task的TCB结构中。
- 从pxSystemStackPointer读出System Stack的堆栈指针赋给r1寄存器,切换到系统堆栈。
- 利用INTC_IACKR寄存器将外部中断向量表基地址和中断向量存入r3寄存器。INTC_IACKR寄存器中的中断向量表基地址是在启动代码的init_data_bss()函数中赋值为intc_vector_table(__VECTOR_RAM)的。见第二章启动代码和RAM分布小结第6步.
- 利用r3寄存器将将外部中断服务函数地址存入LR寄存器。
- 跳转至中断服务函数中执行。 se_blrl
- 中断服务函数ISR返回后返回vPortISRHandler继续执行。epilogue:
- 保存当前堆栈指针r1到全局变量pxSystemStackPointer中。
- 调用portPOP_TASK从pxCurrentTCB读出当前Task的堆栈指针并赋值到r1寄存器。
- 调用portRESTORE_CONTEXT,该函数会根据r1寄存器将当前Task的堆栈弹出到CPU的各寄存器。这样CPU就会切换到Task执行。由此可见,单纯的中断不会引起系统调度和抢占,只有在中断服务函数中存在改变pxCurrentTCB的动作,才会引起抢占,例如第二章讲的系统心跳中断服务函数vPortTickISR()中做的那样。
- 切换上下文,返回Task执行。se_rfi
- s
系统调用过程(TBD)
关于抢占(TBD)
FreertosPowerPC相关推荐
最新文章
- PCL点云数据 滤波降噪
- 关于python的一些好的书籍推荐-如果只能推荐3本关于python的书,你会推荐哪3本?...
- Hadoop分布式环境下的数据抽样
- 单继承模式下的JAVA和C++
- C++程序设计方法3:类中的静态成员
- innodb redo buffer的认识
- 数位DP算法概述及习题
- matlab 中文注释乱码问题解决
- 基于51单片机的指纹考勤系统
- 修改页面变成灰色代码修改方法
- 绩效考核方法有哪些?这四种你知道几个?
- 了解JavaScript的Flow、认识Flow及其简单用法
- 如何从TI官网下载芯片的AltiumDesigner原理图文件和封装文件
- 互联网公司招聘--网易--网易云音乐程序员--2017年笔试题
- 如何练习插画?插画应该如何构图?
- 利用Wireless Repeater(无线中继模式)扩大你的无线网络
- 阿里开发手册-MySQL规约
- cf聊天室,cf聊天室下载
- ModBusTcp协议(一)
- 如何让自己一直成为一个 Python 菜鸡儿?