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(;;);