目录

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文件中。

工程编译阶段,链接器会根据链接脚本的配置扫描所有源文件并进行如下动作

  1. 计算各SECTION在Flash和RAM中的位置。
  2. 根据上面的计算结果,对init_table、zero_table和其他一些记录SECTION地址的变量赋值。init_table和zero_table记录了启动时,需要拷贝到RAM中的SECTION在Flash和RAM的开始/结束地址。启动时,启动代码会根据这两个table将Flash中部分内容加载到RAM中。
  3. 指定启动代码的入口位置。

在我们的系统中,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中。

我们跟踪一下它所进行的动作。

  1. 关闭中断。wrteei 0
  2. 初始化通用和专用寄存器
  3. 关闭Watch Dog
  4. 配置系统栈指针,将R1寄存器赋值为链接脚本中的__SP_INIT
  5. 系统初始化:SystemInit()
    1. 通过MC_ME寄存器配置并使能CPU2
    2. 为当前core配置外部中断控制器
    3. 配置PowerPC CPU中断向量表,即core_exceptions_table/VTABLE。
    4. 配置CPU的DMA访问权限。
  6. data,bss,中断向量表初始化:init_data_bss()
    1. 从链接脚本定义的符号__COPY_TABLE和__ZERO_TABLE取出init_table和zero_table的地址。
    2. 利用init_table将外部中断向量表和已初始化全局变量copy到RAM中
    3. 将当前CPU的外部中断向量表赋值为intc_vector_table。    *s_vectors[coreId] = (uint32_t)__VECTOR_RAM; s_vectors存的是寄存器INTC_IACKR的地址,该寄存器保存了外部中断控制器的中断向量表基地址。
    4. 利用zero_table将RAM中未初始化全局变量的所在区域都赋值为0.
  7. 使能中断:wrteei 1
  8. 跳转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的调用过程如下。

  1. 判断是否为第一次malloc,如果是,则调用prvHeapInit()初始化堆。

    1. 根据ucHeap的地址和大小初始化空闲Block链表xStart和pxEnd。此时只有一个空闲Block。
    2. 赋值xMinimumEverFreeBytesRemaining,用来记录历史最小堆空间。
    3. 赋值xFreeBytesRemaining,用来记录剩余的堆空间大小。
    4. xBlockAllocatedBi
  2. 如果申请的空间小于xFreeBytesRemaining,则开始通过xStart和pxEnd查找第一符合要求的Block。如果能找到则分配空间。
  3. 更新xFreeBytesRemaining和xMinimumEverFreeBytesRemaining

xTaskCreate创建Task的过程如下:

  1. 判断堆栈是向上增长还是向下增长,我们的系统里面现在配置为向下增长:portmacro.h: #define portSTACK_GROWTH            ( -1 )
  2. 调用pvPortMalloc为Task分配栈空间。
  3. 调用pvPortMalloc为Task的TCB结构分配空间
  4. 初始化Task: prvInitialiseNewTask(),主要是初始化TCB结构并构造第一个栈帧。
    1. 记录栈地址,优先级,Name等等各项属性到TCP结构
    2. 设置State和Event列表*需要继续调查。
    3. 调用pxPortInitialiseStack()构建第一个栈帧,让Task看起来像已经执行过。这时候,函数指针pxTaskCode会赋给栈中LR寄存器和SRR0(saved address of offending instruction)寄存器对应的地址,这样当Task开始执行时,将堆栈弹出,就会从我们写的第一行代码开始执行。

  5. 将Task加入Ready列表:prvAddNewTaskToReadyList()
    1. 首先进入临界区,防止在更新Task list时被中断打断,中断中也有可能更新task list. taskENTER_CRITICAL()
    2. 如果没有当前任务,则将其设为当前任务pxCurrentTCB = pxNewTCB;如果已经有Task了,则比较两个Task的优先级,将pxCurrentTCB赋值为高优先级Task的TCB结构。
    3. 如果这是第一个Task则调用prvInitialiseTaskLists来初始化Task list。(使用全局变量uxCurrentNumberOfTasks来判断是否是第一个Task)该函数中会初始化各优先级的Readylist,同时初始化DelayedTaskList。*各List的详细作用后续调查。                                                                                               
    4. 调用prvAddTaskToReadyList将该Task追加至所在优先级的Ready list的末尾。该过程中也会更新全局变量uxTopReadyPriority为当前最高就绪Task优先级。
    5. 退出临界区。
    6. 如果调度已经开始,并且该Task是优先级最高的Task则调入执行:taskYIELD_IF_USING_PREEMPTION。不过我们的系统里不存在这种情况,我们是先创建所有Task,后开始调度。

*待更新:此时内存分布

创建Queue

xQueueGenericCreate

  1. 计算Queue数据存储占用空间,uxQueueLength * uxItemSize。
  2. 调用pvPortMalloc为Queue管理结构Queue_t和数据存储分配空间。所以Queue是位于堆中的。
  3. 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()

  1. 创建idle Task。*待完善
  2. 创建Timer Task。*待完善
  3. 关中断
  4. 初始化系统心跳数为0,xTickCount = ( TickType_t ) 0U
  5. 设置系统心跳中断,开始调度。xPortStartScheduler():
    1. 通过INT_SYS_InstallHandler安装中断处理函数vPortTickISR()。该函数会通过__VECTOR_RAM变量找到ram中的外部中断向量表,并修改对应的函数指针。
    2. 使能系统心跳中断,设置中断优先级,设置心跳间隔。
  6. 运行第一个Task: vPortStartFirstTask()
    1. 因为这里要切换到Task中执行,切换堆栈。所以第一步保存系统堆栈指针到全局变量pxSystemStackPointer。即把当前R1寄存器的保存到pxSystemStackPointer。
    2. 通过全局变量pxCurrentTCB找到当前Task的TCB结构。这个变量前面创建Task的时候已经被赋值为我们创建的最高优先级的Task。见xTaskCreate的5.b小节。
      根据该TCB结构找到该Task的堆栈指针,并赋值给R1寄存器。
    3. portRESTORE_CONTEXT:利用R1寄存器弹出堆栈。这时候,LR,R1,SRR,CTR等等寄存器都被更新,做好了进入Task执行的准备。还记得,Task的第一个堆栈是在xTaskCreate的4.c小节特意构造的,所以下一步就是进入Task函数的第一条指令进行执行。
    4. 利用se_rfi指令切换上下文。进入Task执行。

从此Task就开始执行了。Freertos的调度和抢占也就开始了。最常见的调度就是在上面第五步设置的定周期系统心跳中断。我们看一下这个中断里干了什么。

vPortTickISR()

当系统心跳(此为硬件定时器中断,时外部中断的一种,该中断触发时的系统堆栈切换在第三章介绍)超时的时候,vPortTickISR()会被调用:

  1. 关闭中断:vPortMaskInterrupts->wrteei  0  *
  2. xTaskIncrementTick:递增系统心跳计数xTickCount,并判断是否需要调度。
    1. 如果心跳计数大于xNextTaskUnblockTime,则遍历pxDelayedTaskList中是否有定时超时的Task。如果没有,则更新xNextTaskUnblockTime为最近的要超时的Task的超时时间。
      如果有的话,就将找到的这些Task移出Block List,移出Event List,移入Ready List。如果找到的Task的优先级中,有大于当前Task(pxCurrentTCB)优先级的Task。则判断为需要调度。
    2. 如果当前优先级的Ready List大于1,也判断为需要调度。因为Freetros中,相同优先级的Task采用时间片轮转,当前Task既然已经运行了一个时间片,就要调另一个相同优先级的Task来执行。
    3. 返回是否需要调度标志位。
  3. prvPortTimerReset()
  4. 如果第二步判断是需要调度,则通过vTaskSwitchContext切换Task。
    1. 通过全局变量uxSchedulerSuspended判断当前是否已停止调度,如果是的话则赋值xYieldPending = pdTRUE,如果不是则进行如下操作
    2. 设置xYieldPending = pdFALSE,更新Task统计信息,调用taskCHECK_FOR_STACK_OVERFLOW检查栈溢出。
    3. 调用taskSELECT_HIGHEST_PRIORITY_TASK挑选下一个要执行的Task。
      1. 通过全局变量uxTopReadyPriority获取当前最高就绪Task优先级。
      2. 查找该优先级ReadyTaskList,获取该列表第一个元素的TCB结构,并赋值给pxCurrentTCB。
  5. 至此,下一个时间片要执行的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

我们来详细分析一下这个过程:

  1. CPU收到0x40中断,通过IVPR寄存器保存的中断向量表core_exceptions_table找到并跳转外部中断处理函数vPortISRHandler。   e_b vPortISRHandler
  2. vPortISRHandler函数中:
    1. 调用portSAVE_CONTEXT,利用r1寄存器将当前的CPU各寄存器保存到当前的堆栈,即当前Task的堆栈。
    2. 调用portPUSH_TASK,将当前的r1寄存器的值保存到pxCurrentTCB中,即当前Task的TCB结构中。
    3. 从pxSystemStackPointer读出System Stack的堆栈指针赋给r1寄存器,切换到系统堆栈
    4. 利用INTC_IACKR寄存器将外部中断向量表基地址和中断向量存入r3寄存器。INTC_IACKR寄存器中的中断向量表基地址是在启动代码的init_data_bss()函数中赋值为intc_vector_table(__VECTOR_RAM)的。见第二章启动代码和RAM分布小结第6步.
    5. 利用r3寄存器将将外部中断服务函数地址存入LR寄存器。
    6. 跳转至中断服务函数中执行。 se_blrl
    7. 中断服务函数ISR返回后返回vPortISRHandler继续执行。epilogue:
    8. 保存当前堆栈指针r1到全局变量pxSystemStackPointer中。
    9. 调用portPOP_TASK从pxCurrentTCB读出当前Task的堆栈指针并赋值到r1寄存器。
    10. 调用portRESTORE_CONTEXT,该函数会根据r1寄存器将当前Task的堆栈弹出到CPU的各寄存器。这样CPU就会切换到Task执行。由此可见,单纯的中断不会引起系统调度和抢占,只有在中断服务函数中存在改变pxCurrentTCB的动作,才会引起抢占,例如第二章讲的系统心跳中断服务函数vPortTickISR()中做的那样。
    11. 切换上下文,返回Task执行。se_rfi
  3. s

系统调用过程(TBD)

关于抢占(TBD)

FreertosPowerPC相关推荐

最新文章

  1. PCL点云数据 滤波降噪
  2. 关于python的一些好的书籍推荐-如果只能推荐3本关于python的书,你会推荐哪3本?...
  3. Hadoop分布式环境下的数据抽样
  4. 单继承模式下的JAVA和C++
  5. C++程序设计方法3:类中的静态成员
  6. innodb redo buffer的认识
  7. 数位DP算法概述及习题
  8. matlab 中文注释乱码问题解决
  9. 基于51单片机的指纹考勤系统
  10. 修改页面变成灰色代码修改方法
  11. 绩效考核方法有哪些?这四种你知道几个?
  12. 了解JavaScript的Flow、认识Flow及其简单用法
  13. 如何从TI官网下载芯片的AltiumDesigner原理图文件和封装文件
  14. 互联网公司招聘--网易--网易云音乐程序员--2017年笔试题
  15. 如何练习插画?插画应该如何构图?
  16. 利用Wireless Repeater(无线中继模式)扩大你的无线网络
  17. 阿里开发手册-MySQL规约
  18. cf聊天室,cf聊天室下载
  19. ModBusTcp协议(一)
  20. 如何让自己一直成为一个 Python 菜鸡儿?

热门文章

  1. Vue动态引入JS文件
  2. python open读写文件
  3. 信息论通识课程:建立过渡性的模型时应遵循最大熵原理(过犹不及)
  4. ESP32入门基础之ble spp client 和 ble spp server 的学习理解
  5. UVA - 11600 Masud Rana
  6. hive解绑邮箱_荣耀战棋如何注册HiVE账号 荣耀战棋打不开登陆不了怎么办
  7. 玩转 Commander.js —— 你也是命令行大师
  8. 带有CSS剪切路径的粗略头像
  9. 软件测试周刊(第87期):天下就没有偶然,那不过是化了妆的、戴了面具的必然。
  10. js 数组删除元素,并获得真实长度