|
μC/OS II针对TMS320C32的移植
摘 要: 嵌入式OS-mC/OS-II是为数不多的几个源代码公开的嵌入式操作系统之一,对它的学习、分析能够给我们带来很多概念上的了解和深入。另外mC/OS-II可以移植到很多CPU芯片,本文介绍了把mC/OS-II移值到TI公司浮点DSP芯片的过程的一些细节问题。
关键词: 嵌入式操作系统;mC/OS-II; DSP;移植
嵌入式操作系统mC/OS II是一个可移植可裁剪、占先式多任务OS。大部分源代码用ANSI C语言编写,只有很少的一部分用汇编语言编写,使移植工作简化。
下面介绍如何将mC/OS-II移植到TI的浮点DSP芯片TMS320C32上。
TMS320C32 DSP芯片介绍
TMS320C3x是TI公司的浮点DSP芯片。TMS320C32是其中的一款,能工作在60MHz的时钟频率下,指令运行速度达到60 MFLOPS,是性价比很高的浮点处理器,有着广泛的应用。
TMS320C32芯片的组成:浮点CPU、512字节RAM、2个DMA通道、1个串口、2个定时器、固化引导程序,另外还有如下的通用寄存器:8个40bit的寄存器(R0~R7),可以用来存放32bit的整数,同时也可以用来存放40bit的扩展精度的浮点数;8个32bit的辅助寄存器(AR0~AR7),它们的主要作用是存储地址、参与各种模式的寻址等,当然也可以作为一般的寄存器来使用;状态寄存器ST(含全局中断使能位)、堆栈寄存器SP、中断标志寄存器IF、中断使能寄存器IE、I/O标志寄存器IOF、数据页指针寄存器DP(每页容量为64K)、索引寄存器IR0、IR1、块寄存器BK、重复执行寄存器RS(块起始地址)、RE(块终止地址)、RC(重复次数)。
要实现mC/OS-II向TMS320C32的移植,需要C3x的C编译器支持,否则无从下手。我们使用的是TI公司的C编译器Coder Composer V4.10.36。这个C编译器允许嵌入行汇编,另外还具有强大的优化C编译的功能。
移植中所需修改的文件
和CPU相关的文件主要有四个:C语言文件OS_CPU_C32.C、INCLUDES.H头文件、头文件OS_CPU_C32.H和汇编文件OS_CPU_C32.ASM,我们的主要移植工作就是针对这些文件做一些变动。
OS_CPU_C32.H
OS_CPU_C32.H包括typedef、#define定义的CPU相关信息如下:
#ifndef __OS_CPU_H__
#define __OS_CPU_H__
typedef unsigned char BOOLEAN;/*布尔量*/
typedef unsigned char INT8U;/* 8位无符号数*/
typedef signed char INT8S; /* 8位有符号数*/
typedef unsigned int INT16U;/* 16位无符号数*/
typedef signed int INT16S; /* 16位有符号数*/
typedef unsigned long INT32U;/* 32位无符号数*/
typedef signed long INT32S; /* 32位有符号数*/
typedef float FP32;/* 32位单精度浮点数 */
typedef long double FP40; /*40位扩展精度浮点数*/
typedef unsigned int OS_STK; /*堆栈入口宽度位32位*/
#define OS_STK_GROWTH 0 /*堆栈由低地址向高地址增长*/
#define OS_CRITICAL_METHOD 1
#if OS_CRITICAL_METHOD == 1 /*方法一*/
#define OS_ENTER_CRITICAL() asm (" AND 0DFFFH,ST ") /*关全局中断,进入临界区*/
#define OS_EXIT_CRITICAL() asm (" OR 02000H,ST ") /*开全局中断,退出临界区*/
#endif
#if OS_CRITICAL_METHOD == 2 /*方法二*/
/*保存中断禁止状态到堆栈,关全局中断,进入临界区*/
#define OS_ENTER_CRITICAL() {
asm(" PUSH ST");
asm(" AND 0DFFFH,ST"); }
#define OS_EXIT_CRITICAL() asm("POP ST") /* 恢复中断禁止状态*/
#endif
#define OS_TASK_SW() asm("TRAP 27") /*用于任务切换的软中断*/
数据类型
由于不同的处理器有不同的字长,所以mC/OS-II的移植包括了一系列的数据类型的定义,以确保其可移植性。这里我们定义一些C32以及Code Composer都能识别、处理的数据类型。
C32本质上只有4种数据类型:32位的无符号整数:0_4294967295;32位的有符号整数:-2147483648_2147483647;32位的浮点单精度浮点数:5.877472e-39_3.4028235e38;40位的扩展进度浮点数5.87747175e-39_3.4028236684e38;我们上面定义的8、16位数实际上都是32位的。另外C32中,堆栈都是按32位数据类型进行操作的,所以堆栈数据类型OS_STK申明为32位无符号整数;
代码的临界区
mC/OS-II在进入系统临界代码区之前要关中断,避免临界区代码受多任务或中断服务程序的破坏,等到临界区代码执行完毕之后,该怎么处理呢?有两种方案可以供选择:1)不管关中断前中断使能情况是什么样子,一律开中断;2)恢复关中断前中断使能情况,从一定程度上保证任务执行环境的完整性。
C32中,状态寄存器ST的第13位是全局中断使能位GIE把该位置0,那么不管什么中断都不去被响应,直到临界区代码执行完毕为止。(注:C32没有不可屏蔽的中断NMI,对于别的芯片来说,如果有NMI的话,处理办法就是在这个中断服务程序ISR中对ST中的GIE位进行判断,如果置0,那么这个ISR简单响应一下这个中断,大部分处理工作放到GIE置1后马上去执行)。宏OS_ENTER_CRITICAL()把GIE位置0而关闭所有中断。
堆栈增长方向
C32处理器的堆栈是由低地址向高地址递增,所以OS_STK_GROWTH应该设置为1;
进入任务切换函数OS_TASK_SW()的定义
mC/OS-II中,进入任务切换是用函数OS_TASK_SW()来实现的。这个函数通过软中断模拟了一次中断过程,在这个中断服务程序ISR中实现任务的切换,切换的具体实现在介绍任务切换函数OSCtxSw()时详细阐述。C32共有28个软中断可供使用,通过执行汇编指令 TRAP 0、TRAP 1……TRAP27来产生软中断,也称为TRAP陷阱调用。这里,我们选择编号为27的软中断作为进入任务切换的中断:
#define OS_TASK_SW() asm("TRAP 27")
还要注意的一点是这个中断服务程序的入口必须指向函数OSCtxSw()。
INCLUDES.H文件
INCLUDES.H是主要的头文件,在大多数.C文件的开始都包含INCLUDES.H文件。不同处理器、不同编译器、不同库文件,需要修改INCLUDES.H文件,删除不使用的头文件,添加自己的头文件。而且,头文件之间有包含关系、条件编译的,一定要排好他们之间的先后顺序。,INCLUDES.H文件修改如下:
#ifndef __INCLUDES_H__
#define __INCLUDES_H__
#include "OS_CFG.H"
#include "OS_CPU.H"
#include "mCOS_II.H"
#include "C32.H"
#endif
其中C32.H文件包含了4个头文件:
#include "Timerdef.H"
#include "SerialPort.H"
#include "Dma.H"
#include "Bus.H"
分别对C32的定时器、串口、DMA通道、总线编程用到的数据结构进行定义。
OS_CPU_C32.ASM文件
本来,这个汇编文件里面要实现4个函数:多任务启动函数中调用的OSStartHighRdy()、中断任务切换函数OSIntCtxSw()、任务切换函数OSCtxSw()、时钟节拍服务函数OSTickISR();但是这里只实现后两个函数。前两个函数在OS_CPU_C32.C中实现。
任务切换函数:OSCtxSw()
该函数由任务切换函数OS_TASK_SW()进入,与中断程序中调用的OSIntCtxSw()不同。mC/OS-II中,如果任务执行了某个函数,其结果改变了当前任务的状态(如OSTaskSuspend()、OSTimeDly())、或者是改变了别的任务的状态(OSTaskResume()、OSTimeDlyResume())都要引起新的任务调度:OSSched();在任务调度函数找出新任务将其控制块地址放到OSTCBHigRdy后,执行OS_TASK_SW()。任务切换流程:1)硬件进入中断处理:全局中断使能位置0、返回地址压栈。2)寄存器值压入当前任务堆栈;3)修改当前任务控制块指针OSTCBCur和当前任务优先级OSPrioCur;4)恢复任务堆栈中的值到寄存器中;5)执行当前任务,由RETI指令完成。
时钟节拍函数:OSTickISR()
mC/OS-II中,时钟节拍中断是一个非常重要的中断,因为整个操作系统的活动都受到它的激励。
OSTickISR()的执行流程:1)硬件进入中断处理,同上;2)保护上下文环境;3)调用OSIntEnter(),记录中断嵌套层数;4)调用OSTimeTick(),检查处理各个任务的延时,并根据情况修改就绪任务表;5)调用OSIntExit(),检查就绪任务表,看是否有比当前任务优先级更高的任务就绪,如果有,则进行调度;如果没有,OSIntExit()返回并恢复2)所保存的上下文环境,并执行RETI回到被中断的那个任务里继续运行;如果有,那么OSIntExit()就不返回到这里,具体的情况后面介绍OSIntExit()时具体阐述。
OS_CPU_C32.C文件
这个文件里,主要实现3个函数:堆栈初始化函数OSTaskInit()、中断任务切换函数OSIntCtxSw()、多任务启动函数中调用的OSStartHighRdy(),另外还有5个扩展外挂函数:
void OSTaskCreateHook(OS_TCB ptcb){} /*任务创建扩展外挂函数*/
void OSTaskSwHook(void){} /*任务切换扩展外挂函数*/
void OSTaskDelHook(OS_TCB *ptcb){} /*任务删除扩展外挂函数*/
void OSTaskStatHook(void){} /*统计任务扩展外挂函数*/
void OSTimeTickHook(void){} /*时钟节拍创建扩展外挂函数*/
这几个函数我们这里都处理为空函数,而且还可以通过在文件OS_CFG.H中设置OS_CPU_HOOKS_EN为0而不使用这些函数。我们主要来讨论前三个函数:
堆栈初始化函数OSTaskInit()
堆栈初始化函数OSTaskInit()是由任务创建函数OSTaskCreate()或OSTaskCreateExt()来调用,用来初始化任务堆栈。初始化后的堆栈保存着任务第一次执行时的上下文环境,它和中断后的堆栈神似!这个函数最关键的两个参数就是任务的起始地址void(* task)(void *pd)和任务使用堆栈的栈顶指针void *ptos;需要注意的是,任务第一次执行时的某些全局寄存器的值有特殊要求:1)状态寄存器ST的初始值必须保证中断全局使能位GIE为1,从而保证时钟节拍中断不会长时间被屏蔽,这里我选择初值为0x2000;2)页指针寄存器DP的初始值:如果你选择Small-Memory模式进行编译时,那么它的初始值应该和建立C环境时对DP的初始化值一样;否则就不要对这个DP寄存器进行任何保护处理,这样也可以的,不过这样的话,别的函数也就要做相应的改动了;如果选择了Large-Memory模式的话,那么这个值的初始化就可以不进行了,因为编译系统在编译时会自动插入更新DP的指令的;
中断任务级切换函数OSIntCtxSw()
mC/OS-II中,中断的产生可能会引起任务的切换,在中断服务程序的最后会调用OSIntExit()检查任务就绪状态。如果需要进行任务切换,将调用OSIntCtxSw()。所以OSIntCtxSw()又称为中断级的任务切换函数。需要注意的是,任何中断服务程序ISR前面都要像时钟节拍函数:OSTickISR()流程的第2步那样保存上下文环境。OSIntCtxSw()和OSCtxSw()的后半部分几乎相同,不同之处是:对当前任务的堆栈指针进行调整!其代码如下:
asm(" SUBI 5,SP ");
asm(" LDI @_OSTCBCur,AR0 ");
asm(" STI SP,*AR0 ");
这里我们把堆栈指针SP减去5,就是调整的结果。
下面我们来分析一下这个“5”是怎么得来的(我们针对时钟节拍中断ISR来进行说明):
任务调用函数OSIntExit()之前时,当前任务堆栈的情况如图1所示;调用OSIntExit函数后,当前任务堆栈的情况如图2所示,为什么会这样呢?我们可以看看OSIntExit()函数编译后的汇编文件就明白了,这个函数入口的地方有如下几条语句:
_OSIntExit:
push fp
ldiu sp,fp
push ar4
然后该函数又调用中断切换函数OSIntCtxSw(),这时当前任务堆栈的的情况如图3所示;OSIntCtxSw()编译后的汇编文件入口的地方有如下几条语句:
_OSIntCtxSw:
push fp
由此可见,当别的任务需要调度时,当前任务需要把自己的堆栈指针SP调整到调用OSIntExit()之前的值,从图上可以看出只要把当前任务的堆栈指针的值减去“5”便可。
多任务启动函数中调用的OSStartHighRdy()
这个函数只在多任务启动函数中调用一次,主要用来把多任务启动时优先级最高的就绪任务的上下文环境从堆栈恢复过来。mC/OS-II系统启动时至少创建了一个任务__空闲任务,实际应用当然还要创建别的用户任务。OSStart()首先从这些任务中查找出优先级最高的就绪任务,并把它的任务控制块的地址赋给OSTCBHighRdy,然后调用OSStartHighRdy()来运行OSTCBHighRdy指向的那个任务控制块所对应的任务。其流程是:1)该函数的返回地址压入堆栈,注意,这里提到的堆栈是mC/OS-II系统使用的堆栈,而与其他任何任务使用的堆栈没有任何关系,而且这个地址压不压栈意义已经不大,因为不可能再从返回这里返回回去;2)变量OSRunning赋值为True,标志多任务已经启动;3)从任务初始化过的堆栈中恢复上下文环境,代码和上面的雷同;4)执行RETI指令运行这个任务;
两点补充说明
编译器的编译选项
在移植过程中,除了要熟悉mC/OS-II内核原理和目标芯片之外,还要熟悉C编译器提供编译选项的使用。使用不当,会给移植带来大麻烦。上面在介绍堆栈初始化函数也提及到有关DP寄存器的处理问题上,就是涉及到编译选项的选择:是选用Small-Memory还是Large-Memory模式,这个取决于编译选项-mb的打开和关闭。
中断函数的编写
在分析函数OSIntCtxSw()时,我们提到分析的结果是针对时钟中断服务函数得出的。大家应该注意到这个函数是汇编语言写的,那么我们写别的中断处理程序时,如果用C语言来写的话,要不要注意些什么呢?回答是肯定的。因为编译器在处理C语言写的中断服务函数时会作出一些特殊的处理:在这个ISR入口处插入压栈指令,把部分全局寄存器的值压入堆栈,具体哪些,因函数的不同而有所不同。这就干扰了我们保存上下文环境的工作,如果不进行处理,任务调度时会出现难以想象的问题。解决这个问题的办法就是要让C编译器认为这个中断服务函数是一般的函数,那么它就不会在函数入口处插入一系列的压栈指令。C编译器Code Composer规定,凡是函数名为c_intnm(其中n、m是小于9正整数)的函数都是中断函数,在编译这些函数时都作出特殊的处理,为此,我们避免为中断处理函数取这样的名字就可以了。
但是,编译在处理一般函数时还是要做一定的处理的,譬如:
push fp
ldiu sp,fp
push ar4
为此,我们仔细分析编译器编译生成的汇编代码,并对堆栈作相应的调整。然后在函数结束的地方嵌入asm(" RETI");语句结束。这样中断程序就可以正确执行。
它的框架如下:
void Int0ISR(void)
{
asm(" SUBI N,SP") /*这个N的值要看具体的程序来定,一般等于1或2*/ |
|