天天看点

eCos启动过程详解,基于Cortex-M架构

eCos是开源免版税的抢占式实时操作系统。其最大亮点是可配置,与其配套的图形化配置工具提供组件管理、选项配置、自动化单元测试等。eCos核心组件包括硬件抽象层(HAL)、设备驱动(IO)、实时内核(两种调度算法可选)、线程安全的C库、POSIX兼容层、文件系统(FAT、JFFS2、ROMFS)、协议栈(lwIP、OpenBSD、FreeBSD)、图形系统(Nano-X)等,同时支持第三方扩展组件。

Cortex-M基础

Cortex-M将执行模式分成handler模式和thread模式,进入异常或中断处理则进入handler模式,其他情况则为thread模式。Cortex-M有两个运行级别,分别为特权级和用户级(非特权级),handler模式总是运行在特权级,而thread模式可以运行在特权级也可以运行在用户级,这通过CONTROL特殊寄存器控制。Cortex-M的堆栈寄存器SP对应两个物理寄存器MSP和PSP,MSP为主堆栈,PSP为进程堆栈,handler模式总是使用MSP作为堆栈,thread模式可以选择使用MSP或PSP作为堆栈,同样通过CONTROL特殊寄存器控制。

复位后,Cortex-M进入thread模式、特权级、使用MSP堆栈,并从向量表项0处取SP寄存器值,从向量表项1处取PC寄存器值,然后从PC寄存器值处开始执行,一般情况下默认向量表存储在地址0x00000000处,不同的变种其默认向量表可能不同。

向量表

Cortex-M复位后首先从默认向量表处读取SP初始值和PC初始值。为了让Cortex-M复位后立即有可用的复位向量,必须将向量表存储在ROM中,然后在初始化过程可选地将向量表地址映射到其它区域。eCos为Cortex-M准备的向量表主要是存储在RAM中,因为eCos需要支持修改向量表项,但是仍然要为复位动作准备存储在ROM中的向量表,ROM中的向量表仅有两项,分别为SP初始值和PC初始值,刚好满足Cortex-M复位的需求。

// hal/cortexm/arch/<version>/src/vectors.S:79
        .section        ".vectors","ax"
        .global         hal_vsr_table
hal_vsr_table_init:
        .long           hal_startup_stack 
        .long           hal_reset_vsr      

(2)通过.section伪指令将向量表存储在.vectors节,.vectors会在target.ld链接控制文件中特殊对待,将其定位到指定的地址,一般情况下为0x00000000,这是Cortex-M复位后的向量表起始地址。

(5)向量表项0为堆栈地址,Cortex-M的堆栈为递减的满栈,因此这里的值应当为堆栈最高地址+1,例如堆栈空间分配在0x20001000~0x20001FFF,那么这里应当为0x20002000。hal_startup_stack符号是在target.ld中定义的。

(6)向量表项1为复位向量,存储初始化函数地址,这里为hal_reset_vsr,是在hal_misc.c中定义的C函数。

伪入口

在调试过程中,不会产生复位来初始化SP和PC,为了能够配合调试软件模拟复位过程的SP和PC寄存器的初始化,eCos提供了伪入口,伪入口将初始化SP,并跳转到初始化函数hal_reset_vsr。

// hal/cortexm/arch/<version>/src/vectors.S:100
        .type   reset_vector, %function
reset_vector:
        ldr     sp,=hal_startup_stack
        b       hal_reset_vsr      

(2)reset_vector是个普通函数,不需要定位到特殊位置,target.ld将reset_vector设置为入口地址,调试器加载程序后会将PC指向reset_vector所在的地址。

(4)模拟复位时读取向量0初始化SP的过程。

(5)模拟复位时跳转到向量1所指函数的过程,这里是hal_reset_vsr。

复位向量hal_reset_vsr

整个初始化过程主要由hal_reset_vsr函数完成,hal_reset_vsr函数的基本流程:调用平台相同的系统初始化、初始化异常向量表、更改运行模式、初始化数据区、初始化中断向量表、调用变种初始化函数、调用平台初始化函数、调用全局静态对象构造函数、调用cyg_start进入用户代码。

调用平台相同的系统初始化

// hal/cortexm/arch/<version>/src/hal_misc.c:131
void hal_reset_vsr( void )
{
    hal_system_init();      

(4)hal_system_init函数通常由平台层HAL提供,主要进行基础设施的初始化,例如时钟、外扩存储器、IO配置等,后续的初始化过程将依赖于这些基础设施,因此必须最先被初始化,平台层HAL还提供非基础设置的初始化函数hal_platform_init,hal_platform_init函数将在初始化的末尾调用。

初始化异常向量表

// hal/cortexm/arch/<version>/src/hal_misc.c:157
    for( i = 2; i < 15; i++ )
        hal_vsr_table[i] = (CYG_ADDRESS)hal_default_exception_vsr;

    hal_vsr_table[CYGNUM_HAL_VECTOR_SERVICE] = (CYG_ADDRESS)hal_default_svc_vsr;
    hal_vsr_table[CYGNUM_HAL_VECTOR_PENDSV] = (CYG_ADDRESS)hal_pendable_svc_vsr;

    for( i = CYGNUM_HAL_VECTOR_SYS_TICK ;
         i < CYGNUM_HAL_VSR_MAX;
         i++ )
        hal_vsr_table[i] = (CYG_ADDRESS)hal_default_interrupt_vsr;

    HAL_WRITE_UINT32( CYGARC_REG_NVIC_BASE+CYGARC_REG_NVIC_VTOR,
                      CYGARC_REG_NVIC_VTOR_TBLOFF(0)|
                      CYGARC_REG_NVIC_VTOR_TBLBASE_SRAM );      

(2)初始化异常向量表不包括堆栈指针和复位向量,因为这两项仅在系统复位时需要,而系统复位后默认从ROM中读取,因此这里不需要初始化,此外向量15为SysTick对应的向量,eCos将SysTick作为中断处理而不是异常,这是非常合理的。

(3)初始化成默认的异常处理函数hal_default_exception_vsr。

(5,6)SVC和PendSV使用专门的异常处理函数hal_default_svc_vsr和hal_pendable_svc_vsr。

(8)从SysTick开始的向量作为中断处理,默认处理函数为hal_default_interrupt_vsr。

(13)将初始化完成的向量表地址写入NVIC的VTOR寄存器,从这一刻起,Cortex-M将从hal_vsr_table读取异常向量。

更改运行模式

Cortex-M复位后,进入特权级thread模式,使用MSP堆栈。eCos希望初始化过程以及线程使用PSP,而把MSP留给异常或中断处理单独使用。因此接下来eCos需要进行一次运行模式的改变,将当前使用的SP由MSP更改为PSP,并且关闭中断,eCos初始化过程是要求关中断的。

// hal/cortexm/arch/<version>/src/hal_misc.c:201
    hal_vsr_table[CYGNUM_HAL_VECTOR_SERVICE] = (CYG_ADDRESS)hal_switch_state_vsr;
    __asm__ volatile( "swi 0" );
    hal_vsr_table[CYGNUM_HAL_VECTOR_SERVICE] = (CYG_ADDRESS)hal_default_svc_vsr;      

(2)将SVC异常向量更改为hal_switch_state_vsr,这是汇编实现的异常处理函数,后面再解释。

(3)插入swi指令,swi是系统调用指令,执行该指令将产生SVC异常,上一行代码刚刚SVC异常处理向量更改为hal_switch_state_vsr,因此执行swi指令后将会通过异常处理机制跳转到hal_switch_state_vsr执行。

(4)恢复SVC向量。

// hal/cortexm/arch/<version>/src/vectors.S:130
        .type   hal_switch_state_vsr, %function
hal_switch_state_vsr:

        mov     r0,#CYGNUM_HAL_CORTEXM_PRIORITY_MAX
        msr     basepri,r0

        mov     r0,#2                   // Set CONTROL register to 2
        msr     control,r0
        isb                             // Insert a barrier

        mov     r0,sp
        msr     psp,r0                  // Copy SP to PSP

        orr     lr,#0xD                 // Adjust return link
        bx      lr                      // Return to init code on PSP now      

(2)hal_switch_state_vsr是个异常处理函数,在Cortex-M中异常处理函数跟普通函数没有区别,因为Cortex-M的异常处理机制会模拟C函数调用过程来调用异常处理函数,看起来就像是调用了一个普通函数。

(6)首先将最高优先级写入basepri寄存器,这起到关中断的作用,因为所有优先级低于等于basepri的中断都被屏蔽了,在Cortex-M中优先级越高其对应的数值越小,因此从数值上讲,所有优先级数值大于等于basepri值的中断都被屏蔽,但是basepri不会屏蔽HardFault、NMI,因此初始化过程不可能会产生外部中断(包括SysTick异常)但是可能会产生异常,这也是需要在切换运行状态之前首先初始化向量表的原因。eCos没有使用primask和faultmask寄存器来关中断,因为eCos希望即使是在关中断的情况下也能处理异常。

(9)将control寄存器设置为2,即:选择PSP作为thread模式指针,thread运行在特权级上。

(10)插入isb指令,清洗流水线,确保前面的所有指令都执行完成后再执行后续的指令,这保证刚刚设置起作用后在执行后续的指令。

(13)将MSP的值复制给PSP,这个函数是通过异常机制调用的,处理异常时使用handler模式,handler模式总是使用MSP作为堆栈指针,也就是说这里的SP实际上是映射到MSP的,SP的值就是MSP的值,至此,PSP的值与MSP的值一模一样,因此从MSP变更到PSP不会有任何问题。

(16)从异常状态返回,因为lr的低4位被修改成0xD,因此返回的过程从PSP中处栈,回到thread模式后使用PSP作为堆栈寄存器。

疑问:为什么要通过SVC系统调用异常来调用这个函数,如果按照普通函数的形式调用会有什么问题吗?

初始化数据区

更改模式后,继续回到hal_misc.c,接下来的代码是在特权级thread模式,使用PSP的情况下执行的。初始化数据过程将初始化数据从ROM拷贝到RAM,并将初始化为0的区域清除为0。初始化数据区的内容包括定义时带有初始值的全局变量或静态变量,不包括全局或静态的C++对象实例,全局或静态C++对象实例的初始化见后文。

// hal/cortexm/arch/<version>/src/hal_misc.c:211
    {
        register cyg_uint32 *p, *q;
        for( p = &__ram_data_start, q = &__rom_data_start;
             p < &__ram_data_end;
             p++, q++ )
            *p = *q;
    }

    {
        register cyg_uint32 *p, *q;
        for( p = &__sram_data_start, q = &__srom_data_start;
             p < &__sram_data_end;
             p++, q++ )
            *p = *q;
    }

    {
        register cyg_uint32 *p;
        for( p = &__bss_start; p < &__bss_end; p++ )
            *p = 0;
    }      

(4)初始化RAM中.data段,初始化数据被存储在ROM中,因此将ROM中的初始化数据拷贝到RAM即可,这里RAM可能是芯片内部SRAM的一部分,也有可能是外部扩展的SRAM或DRAM,根据系统配置不同而不同。

(12)初始化SRAM中的.data段,初始化数据被存储在ROM中,因此将ROM中的初始化数据拷贝到RAM即可,SRAM指的是芯片内部的SRAM。

(20)将.bss段的数据清零,凡是存储在.bss段的数据,要么其定义时的初始值被赋值为0,要么没有定义初始值,这两种情况下,均将其清零。

疑问:.data被分成内部RAM和外部RAM两种情况,为什么.bss字段没有考虑两部分呢?

初始化中断向量表

// hal/cortexm/arch/<version>/src/hal_misc.c:240
    {
        register int i;

        HAL_WRITE_UINT32( CYGARC_REG_NVIC_BASE+CYGARC_REG_NVIC_SHPR0, 0x00000000 );
        HAL_WRITE_UINT32( CYGARC_REG_NVIC_BASE+CYGARC_REG_NVIC_SHPR1, 0xFF000000 );
        HAL_WRITE_UINT32( CYGARC_REG_NVIC_BASE+CYGARC_REG_NVIC_SHPR2, 0x00FF0000 );

        hal_interrupt_handlers[CYGNUM_HAL_INTERRUPT_SYS_TICK] = (CYG_ADDRESS)hal_default_isr;

        for( i = 1; i < CYGNUM_HAL_ISR_COUNT; i++ )
        {
            hal_interrupt_handlers[i] = (CYG_ADDRESS)hal_default_isr;
            HAL_WRITE_UINT8( CYGARC_REG_NVIC_BASE+CYGARC_REG_NVIC_PR(i-CYGNUM_HAL_INTERRUPT_EXTERNAL), 0x80 );
        }
    }      

(5)将存储器管理fault异常、总线fault异常、用法fault异常的优先级设置为0,在eCos中,中断的优先级至少大于0,因此这三个异常比任何中断的优先级都高,这也是合理的,异常比中断更要紧。优先级寄存器是8位的,这里使用32位方式写,一次写4个优先级。

(6)将SVC异常优先级设置为0xFF,这是Cortex-M的最低优先级,也就是说SVC异常的优先级低于任何中断优先级。

(7)将PendSVC异常优先级设置为0xFF,SysTick异常优先级设置为0。

(9)设置SysTick的中断服务函数为默认中断服务函数hal_default_isr,上文提到过,eCos将Cortex-M中16个系统异常中的SysTick异常作为中断处理,而且是第0号中断。

(13)将其余的中断服务函数均设置成默认中断服务函数hal_default_isr。

(14)设置中断优先级初始值为0x80,这个优先级高于SVC异常和PendSVC异常的优先级,低于Fault异常的优先级。但SysTick的优先级被设置为0。

疑问:SysTick是否应该和其它中断一致对待,设置其默认优先级为0x80?

使能异常

接下来使能用法fault异常、总线fault异常和存储器管理fault异常,这让eCos有机会捕获异常并通知调试器或内核。在fault异常屏蔽的情况,如果发生异常将会上访成硬件fault异常。

// hal/cortexm/arch/<version>/src/hal_misc.c:278
    HAL_READ_UINT32( base+CYGARC_REG_NVIC_SHCSR, shcsr );
    shcsr |= CYGARC_REG_NVIC_SHCSR_USGFAULTENA;
    shcsr |= CYGARC_REG_NVIC_SHCSR_BUSFAULTENA;
    shcsr |= CYGARC_REG_NVIC_SHCSR_MEMFAULTENA;
    HAL_WRITE_UINT32( base+CYGARC_REG_NVIC_SHCSR, shcsr );      

变体初始化

同样是基于Cortex-M,不同的芯片系列会有不同的外设,每个系列有其对应的变体HAL层,通过hal_variant_init初始化变体层HAL。

// hal/cortexm/arch/<version>/src/hal_misc.c:287
    hal_variant_init();      

平台初始化

相同的芯片还被应用于不同的目标机,每个目标机都有其对应的平台HAL层,通过hal_platform_init初始化平台层HAL,平台层初始化可以包括部分外围器件的初始化。

// hal/cortexm/arch/<version>/src/hal_misc.c:288
    hal_platform_init();      

初始化滴答定时器

滴答定时器是操作系统的脉搏,这里初始化滴答定时器,需要注意的是,这里进行的初始化工作进行设置合适的寄存器值,然后让滴答定时器自由运行,并不会产生中断,滴答定时器的中断向量安装和中断使能通过内核的时钟模块完成。

// hal/cortexm/arch/<version>/src/hal_misc.c:291
    HAL_CLOCK_INITIALIZE( CYGNUM_HAL_RTC_PERIOD );      

初始化C++全局或静态对象

// hal/cortexm/arch/<version>/src/hal_misc.c:307
    cyg_hal_invoke_constructors();      

进入用户代码

// hal/cortexm/arch/<version>/src/hal_misc.c:307
    cyg_start();
    for(;;);