嵌入式操作系统学习(1) 在IAR平台下移植FreeRTOS与µC/OS-III到cortex-m3芯片
以前工作的时候使用过嵌入式操作系统,但也仅仅是仿照别人的代码建立任务,使用一些信号量和消息队列,并没有怎么深入了解操作系统的内部机制原理,所以打算在接下来结合源代码来学习嵌入式操作系统的实现原理,目前开源的嵌入式操作系统比较多,选择了当前比较主流的两款系统来对照学习。正好手头有一块以前做无线的时候在淘宝买来的2538的板子,它的MCU也是Cortex-M3内核,所以先把系统移植到2538上再来研究。
1.启动代码
Cortex-M3芯片在烧写程序时会把代码下载到flash里,与一些高端的arm芯片不同,并不是把所有的代码都拷到内存再去执行,而是在flash里一条一条地读取指令到ram里执行。
嵌入式程序在启动时都有一段启动代码,通常是用汇编写的,而且不同的芯片启动代码通常是不一样的,不同的原因是它们的外设不一样,从而中断向量表是不一样的,而中断向量表是启动时所必须的。
在理解启动代码之前,首先要知道icf文件,每款芯片都有对应的icf文件。我们知道M3芯片都有flash和ram作为存储设备,在datasheet里会有一章Memory Map来说明内存地址的映射关系,但这些编译器是不知道的,所以需要通过icf文件把这些地址定义出来,告诉编译器,rom、ram、堆栈等地址的范围,并导出对应的符号,以后编写启动代码时就用这些符号来代替实际地址。
有了icf文件后,编译器就清楚地知道芯片的内存分布结构了,编译链接后为各函数和全局变量变量分配地址,这时可以通过.map文件来查看地址分布。IAR编译器自身有一个默认的启动文件cstartup.s,会引导程序进入中断向量表,我们发现这个文件里并没有中断向量表,中断向量表实际上是在用户编写的启动文件里,如果没有定义中断向量表,则会进入IAR自带的库里的中断向量表,最后直接跳转到main函数。
中断向量表的名字叫__vector_table,这是规定好了的,不能更改,定义格式如下
DATA
__vector_table
DCD sfe(CSTACK)
DCD Reset_Handler
DCD NMI_Handler
DCD HardFault_Handler
DCD MemManage_Handler
DCD BusFault_Handler
DCD UsageFault_Handler
...
进入中断向量表后,第一个地址跳转到堆栈的末尾,因为在M3中堆栈的地址是从下往上生长,之后就进入Reset_Handler中断去执行复位操作,在里面会调用__iar_program_start去完成一些全局变量的初始化工作,并把程序引导到main()函数。__iar_program_start是用汇编语言写的,在IAR自带的启动文件里有定义的。在《EWARM_DevelopmentGuide.ENU.pdf》的p51~p53详细的说明了整个启动的过程,下面4张图非常直观的描述了从进入中断向量表开始到main函数的过程
中断向量表的起始地址为.intvec,由icf文件定义,也有些芯片可能在flash的起始地址,即一上电就进入中断向量表,而2538是定义在0x00200000即rom的起始地址,IAR自身的启动文件可能还在前面定义了一些指令,但是最后总归会进入中断向量表。
芯片厂家提供的例子中都会有一个启动文件,而这个启动文件最重要的事情就是定义中断向量表,可以用汇编写,当然也可以用c语言写,2538的启动代码就是用c语言写的,在一个名叫startup_iar.c的文件里,中断向量表定义如下:
__root void (* const __vector_table[])(void) @ ".intvec" =
{
(void (*)(void))&STACK_TOP, // 0 The initial stack pointer
ResetISR, // 1 The reset handler
NmiISR, // The NMI handler
FaultISR, // The hard fault handler
IntDefaultHandler, // 4 The MPU fault handler
IntDefaultHandler, // 5 The bus fault handler
IntDefaultHandler, // 6 The usage fault handler
0, // 7 Reserved
0, // 8 Reserved
0, // 9 Reserved
0, // 10 Reserved
IntDefaultHandler, // 11 SVCall handler
IntDefaultHandler, // 12 Debug monitor handler
0, // 13 Reserved
PendSVIntHandler, // 14 The PendSV handler
SysTickIntHandler, // 15 The SysTick handler
GPIOAIntHandler, // 16 GPIO Port A
GPIOBIntHandler, // 17 GPIO Port B
GPIOCIntHandler, // 18 GPIO Port C
GPIODIntHandler, // 19 GPIO Port D
0, // 20 none
UART0IntHandler, // 21 UART0 Rx and Tx
UART1IntHandler, // 22 UART1 Rx and Tx
SSI0IntHandler, // 23 SSI0 Rx and Tx
I2CIntHandler, // 24 I2C Master and Slave
......
注意中断向量表里函数的顺序是不能修改的,如GPIO、UART中断等,这些中断的偏移地址都是在datasheet里定义好的,不同的芯片都是不一样的,中断来了之后会根据中断向量表的起始地址找到对应的中断入口。当然函数的名字是可以修改的,改了之后你中断函数的名字要与中断向量表里的名字相同,这些函数都被声明了weak类型,没有定义也不会报错,如果定义了链接程序将会链接到它。
2.FreeRTOS的移植
虽然启动代码的内容有些难以理解,但是只要理解了启动代码和中断向量表的概念,那么任何操作系统的移植都是一样的,而且非常简单。
在M3中和操作系统相关的2个中断SVCall和PendSV要挂接到操作系统提供的中断函数里,在FreeRTOS里,用portasm.s里的vPortSVCHandler和xPortPendSVHandler这两个函数替换中断向量表里的第11号和第14号中断。
另外每个操作系统都需要一个系统时钟来作为系统任务调度时的时间基准节拍,所以需要把15号中断SysTickIntHandler替换成FreeRTOS的xPortSysTickHandler。在CC2538的系统定时器的例子里面,为了控制系统时钟的中断发生时间,需要通过SysTickPeriodSet()来设置定时器周期,但是看了一下FreeRTOS提供的STM32的例子里似乎没有用到STM32的SysTick_SetReload()来设置系统时钟的周期?
上面这个疑惑主要是没搞清楚芯片的组成结构,来看一下下面这张图
整个M3芯片,M3内核是由ARM公司来设计,而其他外设都是由芯片制造商设计如TI、ST公司等等,一般通用定时器等外设也都是由芯片制造商设计,不同的芯片定时器也都是不一样的。但是,系统定时器属于M3内核里面的外设,所有M3内核的芯片,他们系统定时器的地址都是相同的。所以只要告诉FreeRTOS系统时钟的频率和每秒定时器中断的发生频率(在FreeRTOSConfig.h里设置,这里设置周期为1ms)
define configCPU_CLOCK_HZ ( ( unsigned long ) 32000000 )//时钟频率
define configTICK_RATE_HZ ( ( portTickType ) 1000 )//每秒发生中断的次数
FreeRTOS就会自己装载定时器发生中断所需需要的计数值,不需要我们再去设定
/* Configure SysTick to interrupt at the requested rate. */
portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );
定时器的寄存器定义如下:
#define portNVIC_SYSTICK_CTRL_REG ( * ( ( volatile uint32_t * ) 0xe000e010 ) )
#define portNVIC_SYSTICK_LOAD_REG ( * ( ( volatile uint32_t * ) 0xe000e014 ) )
可能我们发现系统用到的这3个中断优先级也没有让我们配置,因为中断管理器NVIC也是属于内核里的东西,所以FreeRTOS已经帮我们配好了。
接下来就是堆内存管理的问题了,在\FreeRTOS\Source\portable\MemMang文件夹下有5个内存管理的文件,heap_1.c、heap_2.c、heap_4.c都是先获取一段连续的数组空间用作堆内存,通过configTOTAL_HEAP_SIZE宏定义设置堆的大小,然后这段内存由FreeRTOS自行管理,而heap_3.c使用的是标准库提供的malloc函数,如果使用这个需要修改icf文件增加堆内存的空间,cc2538默认为4K太小了,heap_5.c允许分配的空间不连续,这5个文件移植时任选一个。
上面都改好了之后,把RreeRTOS的代码搬到应用工程里就可以了,要用到的文件为\FreeRTOS\Source目录下的除portable文件夹外的所有文件,\FreeRTOS\Source\portable\IAR\ARM_CM3目录下的所有文件,\FreeRTOS\Source\portable\MemMang的heap_4.c文件,然后在工程里把用到的头文件路径添加一下就可以了,要注意的是portasm.s是汇编语言,要在Assembler选项卡里添加头文件路径
3.µC/OS-III的移植
移植好了FreeRTOS后,移植µC/OS-III基本上是类似的,步骤如下:
1.在中断向量表中把SysTickIntHandler和PendSVIntHandler这2个中断替换成OS_CPU_SysTickHandler和OS_CPU_PendSVHandler,在µC/OS中不使用SVCall中断
2.µC/OS-III需要移植者为CPU_TS_TmrRd根据移植的芯片提供一个更高精度的时基,主要用于任务利用率、信号量、邮箱、事件标志组的时间测量,没有也并不影响系统运行,所以这里为了防止编译错误,定义一个空函数,想用的话在定时器里加个时间戳。
CPU_TS_TMR CPU_TS_TmrRd(void)
{
return 0;
}
void CPU_TS_TmrInit()
{
return;
}
3.把源码目录UCOSIII的3个子目录uC-CPU、uc-LIB、uCOS-III中的所有源文件加入的工程里,另外从STM32的demo工程里的APP文件夹下把app.c、app_cfg.h、includes.h和os_type.h这几个文件加入到工程里,头文件是µC/OS-III源码编译所必须的,而app.c里有系统初始化的代码和新建任务的例子
4.最后就是设置系统定时器中断周期了,另外在app.c把平台相关的BSP_Init(); 去掉
//这句话去掉
//BSP_Init(); /* Initialize BSP functions */
CPU_Init(); /* Initialize the uC/CPU services */
//获取系统时钟频率,不同的硬件接口不一样
//freq = BSP_CPU_ClkFreq();
freq = SysCtrlClockGet(); /* Determine SysTick reference freq. */
cnts = freq / (CPU_INT32U)OSCfg_TickRate_Hz; /* Determine nbr SysTick increments */
//设置系统定时器周期为1ms
OS_CPU_SysTickInit(cnts); /* Init uC/OS periodic time src (SysTick). */
以上4个移植步骤也很简单,但是µC/OS-III提供的例子里源码独立性较差,对于硬件的平台的耦合性较强,还用到了CMSIS,对于有经验的开发者能很容易的剥离出来,但对于初学者可能会对移植中碰到的一些问题感到茫然,相反FreeRTOS的源码移植时就保持了很强的独立性,只需修改配置头文件里的几个宏即可。