请稍侯

MQX机制分析——启动流程

21 February 2014

1 启动代码在哪儿

  接着上一个hello world的工程,我们点开hello_twrk60n512工程中的hello.c,发现并没有我们平时所看到的的裸奔程序不太一样,怎么连个main函数都找不到,更别说系统启动代码了,不要急,首先我们先要找到链接文件。链接文件就是用来指明链接各个目标文件的规则了,工程下面不同的源文件会生成不同的目标文件,链接的过程会把这些链接文件链接成最终的可执行文件,链接文件里面会定义一些segment(段),表明芯片存储范围的分配,rom、ram、中断向量表等等的地址范围,以及不同的目标文件的相同段如何合并以及他们的存放位置(MQX的lcf文件在后面会单独详细介绍)。浏览了下..\Freescale_MQX_4_0\lib\twrk60n512.cw10\debug\bsp目录下的intflash.lcf链接文件,发现没有定义程序的入口,以前ld格式的链接文件里面是有指定程序入口的,可是在lcf文件怎么没找到?再想想其他办法,在开发环境的链接器的选项也有指定程序入口的,我们右击工程,选择呢properties,点到c/c++ Build标签下的Link下面,发现还是没有指定程序入口,怎么办?继续想,对了,我们可以找到中断向量表,因为中断向量表的第二项存的是复位向量的地址,根据cortex m3/4的特性,cpu复位时,会从中断向量表的起始地址(在之前这篇文章有讲过,这里的起始地址不一定是0x0000_0000)取出堆栈指针SP,从下一个32位的空间处取出复位中断入口向量。目标有了,我们现在要找到定义中断向量表的文件,终于!!!我们发现在..\Freescale_MQX_4_0\mqx\source\bsp\twrk60n512文件夹目录下的vectors.c中的中断向量表定义的中断向量表:

__attribute__((section(".vectors_rom"))) const vector_entry __vector_table[256] __attribute__((used)) = 
{
    (vector_entry)__BOOT_STACK_ADDRESS,
    BOOT_START,         /* 0x01  0x00000004   -   ivINT_Initial_Program_Counter */
    DEFAULT_VECTOR,     /* 0x02  0x00000008   -   ivINT_NMI                     */
    DEFAULT_VECTOR,     /* 0x03  0x0000000C   -   ivINT_Hard_Fault              */
    DEFAULT_VECTOR,     /* 0x04  0x00000010   -   ivINT_Mem_Manage_Fault        */
    DEFAULT_VECTOR,     /* 0x05  0x00000014   -   ivINT_Bus_Fault               */
    DEFAULT_VECTOR,     /* 0x06  0x00000018   -   ivINT_Usage_Fault             */
    0,                  /* 0x07  0x0000001C   -   ivINT_Reserved7               */
    0,                  /* 0x08  0x00000020   -   ivINT_Reserved8               */
    0,                  /* 0x09  0x00000024   -   ivINT_Reserved9               */
    0,                  /* 0x0A  0x00000028   -   ivINT_Reserved10              */
    _svc_handler,       /* 0x0B  0x0000002C   -   ivINT_SVCall                  */
    DEFAULT_VECTOR,     /* 0x0C  0x00000030   -   ivINT_DebugMonitor            */
    0,                  /* 0x0D  0x00000034   -   ivINT_Reserved13              */
    _pend_svc,          /* 0x0E  0x00000038   -   ivINT_PendableSrvReq          */
    DEFAULT_VECTOR,     /* 0x0F  0x0000003C   -   ivINT_SysTick                 */
    /* Cortex external interrupt vectors                                        */
    DEFAULT_VECTOR,     /* 0x10  0x00000040   -   ivINT_DMA0                    */
    DEFAULT_VECTOR,     /* 0x11  0x00000044   -   ivINT_DMA1                    */
    DEFAULT_VECTOR,     /* 0x12  0x00000048   -   ivINT_DMA2                    */
...

  __attribute__((section(".vectors_rom")))这句话的意思是在表明装载编译生成目标文件的时候将它放在.vectors_rom这个段中。__attribute__是gcc的一个很大的特色,它可以用来设置函数属性、变量属性和类型属性,感兴趣的可以查看gcc相关的资料。vector_entry是之前定义的一个函数指针类型:typedef void (*vector_entry)(void);__attribute__((used))是 ARM 编译器支持的 GNU 编译器扩展,告诉编译器在目标文件中保留它,并且按照声明的顺序在目标文件中排列(这边我也不是很明白=。=!,没看懂啥意思)。反正这个类型定义就是定义了一个const类型的数组,存放的数据是函数指针类型,编译完之后按顺序存放在目标文件的.vectors_rom段中。我们将注意力集中到第二项BOOT_START,这个名字看起来有点唬人,看起来像是程序入口,查看它的定义,发现是个宏定义:#define BOOT_START __boot。不过看起来好像快接近正确的结果了,找啊找,终于在..\Freescale_MQX_4_0\mqx\source\psp\cortex_m\core\M4目录下的boot.S找到了这个函数,没错了,这就是我们要找的程序启动的入口了(后来想起来,为啥不直接debug到程序入口处呢,一下子就找到了,省的这么费事)。一个程序入口也找了这么久,喝口水,压压惊,咱继续看下去。

  首先分析__boot这个函数,程序中已经删除了不必要的一些预处理命令。

ASM_PUBLIC_BEGIN(__boot)
ASM_PUBLIC_FUNC(__boot)
ASM_LABEL(__boot)
        //清中断使能寄存器和中断挂起寄存器
        ldr r0, =0xe000e180     //中断使能寄存器
        ldr r1, =0xe000e280     //中断挂起寄存器
        ldr r2, =0xffffffff     //要向寄存器中写的数
        mov r3, #8              //总共8组寄存器

ASM_LABEL(_boot_loop)
        cbz r3, _boot_loop_end
        str r2, [r0], #4        //将0xffffffff写入到r0里面存放的地址中,然后r0+4
        str r2, [r1], #4        //将0xffffffff写入到r1里面存放的地址中,然后r0+4
        sub r3, r3, #1          //循环8次
        b _boot_loop
ASM_LABEL(_boot_loop_end)
        //将MSP赋值给PSP
        mrs r0, MSP
        msr PSP, r0
        //CONTROL[1]写1,即使用PSP
        mrs r0, CONTROL
        orr r0, r0, #2
        msr CONTROL, r0
        isb #15//清空processor的流水线,确保在 ISB 指令完成后,才从高速缓存或内存中提取位于该指令后的所有其他指令。
     //调用EWL(Embedded Warrior Library)库里面的startup函数
        ASM_EXTERN(__thumb_startup)
        b ASM_PREFIX(__thumb_startup)
 ASM_PUBLIC_END(__boot)

  注释都已经写的很清楚了,这个函数所做的操作就是清中断使能寄存器和中断挂起寄存器、切换PSP(进程堆栈指针)和调用startup函数,这个函数是在EWL库中的,我们可以在{InstallPath\MCU\ARM_EABI_Support\ewl\EWL_Runtime\Runtime_ARM\Source\startup.c里面找到,在_thumb_startup函数中会跳转到main函数开始执行。找到main函数(..\FreescaleMQX_4_0\mqx\source\bsp\twrk60n512),发现main函数真短,就三句代码:

int main(void)
{
   extern const MQX_INITIALIZATION_STRUCT MQX_init_struct;
   _mqx( (MQX_INITIALIZATION_STRUCT_PTR) &MQX_init_struct );
   return 0;
}

  声明了一个初始化结构体(这个结构体我们不急着分析,在启动过程中会带着讲它),接着就调用_mqx()函数了,所以,这个_mqx()函数就是MQX的启动函数了,重头戏才刚刚开始。


  在介绍这个函数之前,首先要讲到一个不得不提的概念,就是kernel data(内核数据区),这个东西会伴随着我们整个的源码分析过程,几乎所有的函数都会使用到这个东西,那么这个kernel data究竟是什么呢?内核数据区在内存中的表现形式为一段内存,在代码中的表现形式为一个包含了若干内核相关信息的结构体kernel_data_struct,里面的成员变量用来表明MQX的状态和动态变量的,该结构体的成员数不是固定的,其中有些变量是根据用户的配置决定是否编译,所以内核数据区的大小不是固定的。在执行MQX初始化函数_mqx()时,我们向其传递一个名为MQX_INITIALIZATION_STRUCT的结构体作为参数,这个结构体包含了初始化MQX的基本信息如处理器个数,中断堆栈大小等等,其中的成员变量BSP_DEFAULT_START_OF_KERNEL_MEMORY代表了kernel data所在内存的起始地址,该变量具体数值是在链接的时候定下来的(在lcf文件中有对它赋值),这个起始地址也就是内核数据区kernel_data_struct的起始地址。


  执行_mqx()函数的过程中,程序会对这块内存区域赋值,也就是对kernel_data_struct其中的成员变量进行赋值。这块内存应该是被访问的最频繁的,因为以后所有涉及到与内核相关的操作都要从这块内存先要获得内核状态的相关信息。以轻量级事件为例,当创建一个事件时,首先要调用_GET_KERNEL_DATA(kernel_data)函数获取内核信息,找到KERNEL_DATA_STRUCT结构中LWEVENTS这个成员,它代表了存储轻量级事件的队列的头。遍历这个队列,查看当前创建的轻量级事件在不在这个队列中,如果已经存在的话就返回错误,如果没有就将它压入内核数据中的轻量级事件队列中。

  这里的_GET_KERNEL_DATA是一个宏定义:

//_mqx_kernel_data 定义
KERNEL_ACCESS struct kernel_data_struct _PTR_ _mqx_kernel_data = (pointer)-1;
//_GET_KERNEL_DATA 宏定义
#define _GET_KERNEL_DATA(x)     x = _mqx_kernel_data
//_SET_KERNEL_DATA 宏定义
#define _SET_KERNEL_DATA(x)     _mqx_kernel_data = (struct kernel_data_struct _PTR_)(x)

  _mqx_kernel_data就是内核数据区变量,当我需要需要获取内核数据的时候我就将它复制给我定义的变量就好了,如果我要设置它,就将我需要赋的值给这个变量,一般会在初始化_mqx的时候会给它赋值,之后只需要读取内核数据就可以了。接下来开始看看_mqx()函数做了什么。

2 启动代码分析

_mqx_uint _mqx(register MQX_INITIALIZATION_STRUCT_PTR mqx_init)
{  
    KERNEL_DATA_STRUCT_PTR kernel_data;
    TASK_TEMPLATE_STRUCT_PTR template_ptr;
    TD_STRUCT_PTR td_ptr;
    _mqx_uint result;
    pointer stack_ptr;
    pointer sys_td_stack_ptr;
    uchar_ptr sys_stack_base_ptr;
    //将初始化结构体中的START_OF_KERNEL_MEMORY起始地址进行16字节向高地址对齐
    kernel_data = (KERNEL_DATA_STRUCT_PTR) _ALIGN_ADDR_TO_HIGHER_MEM(mqx_init->START_OF_KERNEL_MEMORY);
    //将对其完的地址用来设置_mqx_kernel_data
    _SET_KERNEL_DATA(kernel_data);
    //下面的操作时为了强制链接器包含下面的符号,所以当优化等级很高的时候使用变量地址,避免优化成直接常量分配,接下来又会被清0(这边不太理解,调试的时候看汇编代码总觉得给同一个变量赋值两次没有意义)
    *(volatile pointer*) kernel_data = (pointer) & _mqx_version_number;
    *(volatile pointer*) kernel_data = (pointer) & _mqx_vendor;
    //初始化内核数据区为0
    _mem_zero((pointer) kernel_data, (_mem_size) sizeof(KERNEL_DATA_STRUCT));
    //赋值初始化结构体到内核数据区
    kernel_data->INIT = *mqx_init;
    kernel_data->INIT.START_OF_KERNEL_MEMORY = (pointer) kernel_data;
    kernel_data->INIT.END_OF_KERNEL_MEMORY = (pointer) _ALIGN_ADDR_TO_LOWER_MEM(kernel_data->INIT.END_OF_KERNEL_MEMORY);
    //初始化内核数据区数据结构
    _mqx_init_kernel_data_internal();
    //初始化轻量级存储资源管理,创建系统缺省内存池
    result = _mem_init_internal();

    // 将中断栈空间登记在内核数据区中,判断是否定义了中断栈的地址
    if (kernel_data->INIT.INTERRUPT_STACK_LOCATION) {
        stack_ptr = kernel_data->INIT.INTERRUPT_STACK_LOCATION;
        result = kernel_data->INIT.INTERRUPT_STACK_SIZE;
    }
    else {
        //没有定义就使用指定的最小栈大小
        if ( kernel_data->INIT.INTERRUPT_STACK_SIZE < PSP_MINSTACKSIZE ) {
            kernel_data->INIT.INTERRUPT_STACK_SIZE = PSP_MINSTACKSIZE;
        } 

        //为什么只要加上PSP_STACK_ALIGNMENT + 1而不需要&(~PSP_STACK_ALIGNMENT)
        result = kernel_data->INIT.INTERRUPT_STACK_SIZE + PSP_STACK_ALIGNMENT + 1;
        result = kernel_data->INIT.INTERRUPT_STACK_SIZE;
        stack_ptr = _mem_alloc_system((_mem_size)result);// 分配内存空间
        _mem_set_type(stack_ptr, MEM_TYPE_INTERRUPT_STACK);// 设定存储区类型
    } 
    _task_fill_stack_internal((_mqx_uint_ptr)stack_ptr, result);
    // 获得经过对齐的中断栈的指针
    kernel_data->INTERRUPT_STACK_PTR = _GET_STACK_BASE(stack_ptr, result);

    //为系统任务描述符设置栈,防止空闲任务被异常阻塞或者空间任务没有被使用
    result = PSP_MINSTACKSIZE;
    sys_td_stack_ptr = _mem_alloc_system((_mem_size) result);
    _mem_set_type(sys_td_stack_ptr, MEM_TYPE_SYSTEM_STACK);
    
    sys_stack_base_ptr = (uchar_ptr) _GET_STACK_BASE(sys_td_stack_ptr, result);
    td_ptr = SYSTEM_TD_PTR(kernel_data);
    td_ptr->STACK_PTR = (pointer)(sys_stack_base_ptr - sizeof(PSP_STACK_START_STRUCT));
    td_ptr->STACK_BASE = sys_stack_base_ptr;
    //16字节对齐
    td_ptr->STACK_LIMIT = _GET_STACK_LIMIT(sys_td_stack_ptr, result);
    _mqx_system_stack = td_ptr->STACK_PTR;

    //初始化就绪队列
    result = _psp_init_readyqs();
    //创建创建组件的轻量级信号量
    _lwsem_create((LWSEM_STRUCT_PTR)&kernel_data->COMPONENT_CREATE_LWSEM, 1);
    //创建任务创建销毁的轻量级信号量
    _lwsem_create((LWSEM_STRUCT_PTR) & kernel_data->TASK_CREATE_LWSEM, 1);
    //使能定时器和其他设备
    result = _bsp_enable_card();

    //创建空闲任务
    td_ptr = _task_init_internal(
                    (TASK_TEMPLATE_STRUCT_PTR)&kernel_data->IDLE_TASK_TEMPLATE,
                    kernel_data->ACTIVE_PTR->TASK_ID, MQX_IDLE_TASK_PARAMETER, TRUE, NULL, 0);
    //将空闲任务置于就绪
    _task_ready_internal(td_ptr);
    //查找自启动任务并且创建
    template_ptr = kernel_data->INIT.TASK_TEMPLATE_LIST;
    while (template_ptr->TASK_TEMPLATE_INDEX) {
        if (template_ptr->TASK_ATTRIBUTES & MQX_AUTO_START_TASK) {
            td_ptr = _task_init_internal(template_ptr, kernel_data->ACTIVE_PTR->TASK_ID,
                            template_ptr->CREATION_PARAMETER, FALSE, NULL, 0);
            _task_ready_internal(td_ptr);
        } 
        ++template_ptr;
    } 
    //开始调度,再也不会返回这里
    _sched_start_internal();  
    //这里返回值只是为了满足lint的要求
    return MQX_OK;  

}  

  上面的源码我已经做了很大的精简,将没有定义的配置和错误检查都删去了,便于观看。我们通过查看上面的代码看以看到在启动的过程中所做的一些工作:

   - 设置kernel data地址并且进行初始化。  
   - 创建轻量级内存池,在kernel data中登记中断栈、系统任务栈。  
   - 初始化就绪队列  
   - 创建任务相关信号量  
   - 初始化中断向量表、定时器、启动时钟及其他设备  
   - 创建空闲任务、查找自启动任务并且将它们就绪  
   - 开始执行调度 

  在整个过程中,代码涉及到很多的内存对齐操作,比如在设置kernel data的起始地址的时候以及在获取栈的基址的时候,字节对齐的作用不仅是便于cpu快速访问,同时合理的利用字节对齐可以有效地节省存储空间。对齐的操作其实很简单:

(地址 + (对其值-1)) & (~(对其值-1))

  下面对每一步进行分析:

2.1 设置kernel data地址并且进行初始化

  第一步就是设置内核数据区了,因为接下来所有的操作都会和kernel data相关,首先我们从初始化结构体(由参数传入)中获得了kernel data的起始地址,将这个地址进行16字节向高地址对齐之后,调用_SET_KERNEL_DATA将处理完的起始地址赋值给_mqx_kernel_data。接着两句代码,每一句都懂,可是放在一起出现在这里就看不懂了:

    *(volatile pointer*) kernel_data = (pointer) & _mqx_version_number;
    *(volatile pointer*) kernel_data = (pointer) & _mqx_vendor;

  注释的意思貌似是和调TAD(Task Aware Debugging)有关,使用变量地址是为了避免优化等级太高时直接优化为常量分配,不过还是不明白。下面将内核数据区清0,将初始化结构体中相关的信息写入到kernel data中,然后调用_mqx_init_\kernel_data_internal函数初始化kernel data的其他成员变量;设置禁止和使能的优先级;设置系统任务相关属性(系统任务永远不会运行,但是它的任务描述符用来描述初始化和内存分配时的错误码,任务描述符以后会讲到);初始化轻量级信号量队列、延时队列;遍历任务模板,找出其中最低的优先级,从而确定空闲任务的优先级,并且设置空闲任务相关属性;初始化任务描述符队列。

2.2 登记中断栈、系统任务栈

  这一步查看有没有定义中断栈的位置大小,没有就是用默认的最小设置,接着申请空间,有一点要注意,栈底的地址不是申请时返回的起始地址,而是起始地址加上栈大小再进行字节对齐之后的地址。系统任务栈同理。

2.3 初始化就绪队列

  就绪任务队列是任务调度机构中最重要的任务队列之一,就绪队列的创建是调用_psp_init_readyqs函数实现的,之前我们已经获得了任务模板中任务的最低优先级,需要创建的就绪任务队列的数目为最低优先级+2。为什么是最低优先级+2呢?举个例子就明白了,比如说现在遍历完任务模板之后得到任务最低优先级为5,空闲任务的优先级为最低优先级加1也就是5+1=6,所以总共要创建0、1、2...6这7(5+2)个就绪队列。创建就绪队列时只是创建了队列头结点,就绪队列为双向链表,当队列为空时,头节点中的队列首任务指针和队列尾任务指针都指向头节点自己。N+2个就绪任务队列头节点之间也以链表形式连接在一起。就绪任务队列的组织形式如下图所示。 1
  各优先级的就绪任务队列头节点在存储系统中是连续存放的,当需要查询当前非空的最高优先级的就绪任务队列时,将从优先级为0(值越小优先级越高)的就绪任务队列向优先级低的就绪任务队列依次查询,以空闲任务就绪队列为结束。MQX的空闲任务是对一个变量的递增运算,没有对其它系统资源的争用,因此只会被高优先级的任务抢占,而不会被阻塞。空闲任务在不运行的情况下,总是就绪的。
  在系统运行时,允许动态调整任务的优先级,即可将任务挂在对应优先级的就绪任务队列中,但不能将任务调整到未创建头节点的就绪任务队列中。

2.4 创建任务相关信号量

  创建创建组件的轻量级信号量和任务创建销毁的轻量级信号量,这两个信号量的数目都是1。创建组件的轻量级信号量是在MQX创建组件的时候使用的,创建之前先将该信号量减1,那么其他等待该信号量的任务(创建其他组件的任务)就会被放在该信号量的等待队列中,知道这个需要创建的信号量被创建完之后释放了这个信号量,其他任务才可以创建其他组件。任务创建销毁的轻量级信号量也是同样的原理了。信号量涉及到任务间的同步,之后会讲到它的具体原理。

2.5 初始化中断向量表、定时器、启动时钟及其他设备

  这一步主要是调用_bsp_enable_card函数完成的,在其中有调用若干其他相关函数:

2.5.1 初始化中断向量表

  由_psp_int_init函数完成,MQX的中断向量表是动态管理的,这里的动态主要有2层含义:

  1 向量表的位置是动态的,通过_int_set_vector_table函数修改VTOR寄存器(向量表偏移寄存器,之前的文章有介绍过);
  2 中断处理函数可以动态的改变,这意味着同一个中断可以在不同情况下绑定不同的中断函数对同一个中断做出不同的响应;

  MQX中断服务例程分为内核ISR和用户ISR两个相对独立部分。内核ISR用于实现硬件中断到用户ISR的映射,一般通过汇编语言实现,以确保MQX对中断事件的快速响应。内核ISR程序函数_int_kernel_isr,在“..\mqx\psp\dispatch.S”文件中。用户ISR通常由用户使用 C语言编写,实现特定的功能。

  动态中断向量表结构按HASH结构组织,如下所示。动态中断向量表表项以8个向量为一组(按缺省值分组)管理。系统初始化时根据给定的中断向量的数量除以8来确定动态中断向量表的组数,如251个中断可以分成251/8为32个组。MQX系统初始化时调用了一个malloc函数为这32个组头(指针数组,每个指针为4字节,而不是20字节)申请一块连续的地址空间,所以组头节点之间可以通过偏移一个队头节点大小的空间进行寻址;而组内的各个表项节点地址不连续,每个链表项节点是独立的动态申请;通过链表项内的“NEXT”字段指针串成链表。MQX初始化时将每个组头节点的内容都初始设为NULL。向量表表项内容由用户在安装中断服务例程时,调用_int_install_isr函数填充。 1

2.5.2 初始化定时器systick

  MQX的时间管理是由硬件定时器systick完成的,SysTick定时器被捆绑在NVIC(嵌套向量中断控制器)中,有效位数是24位,采用减1计数的方式工作,当减1计数到0,可产生SysTick异常(中断),SysTick中断处理函数对对系统的时间进行计时更新;从延时队列中移出到期的任务,并加入到就绪队列中;判断RR调度的任务时间片是否耗尽,对时间片耗尽的任务移到同一优先级就绪队列的队尾等工作。与之前的MQX4.0.0不同,在MQX4.0.2中定义了一个全局的结构体:HWTIMER类型的变量systimer作为systcik的句柄,所有的systick的操作就是对这个句柄的操作。初始化定时器包括:

1 调用hwtimer_init来实现初始化,绑定底层的相关操作如初始化打开关闭函数;设置systick的中断优先级为2;  
2 调用hwtimer_set_freq设置systick时钟源为核心时钟并且systick的周期为0.5ms;  
3 调用hwtimer_callback_reg来绑定systick的中断处理函数_time_notify_kernel;  
4 调用hwtimer_start启动定时器systick。  

2.5.3 配置系统时钟

  系统时钟的配置是使用的PE生成的代码,与无操作系统类似,将内核时钟设置到96MHz。

2.5.4 安装其他设备

  根据user_config.h中的配置。,调用该模块的设备安装函数。比如说在配置中使用的串口ttye,那么就会调用_kuart_polled_install函数来安装串口ttye

 #if BSPCFG_ENABLE_TTYE
    _kuart_polled_install("ttye:", &_bsp_sci4_init, _bsp_sci4_init.QUEUE_SIZE);
#endif

2.6 创建空闲任务、查找自启动任务并且将它们就绪

  在之前已经设置了空闲任务相关属性,现在只需要调用_task_init_internal函数就可以创建空闲任务了。接着遍历任务模板列表,找出其中任务的属性为自启动的,同样调用_task_init_internal函数创建,并且调用_task_ready_internal函数使他们就绪。   

2.7 开始执行调度

  从执行_sched_start_internal这个函数MQX便开始执行调度了,程序永远不可能再回到这里了。