嵌入式操作系统uc/os ii是一个可移植可裁剪可固化的、占先式多任务的微os。它的源代码结构清晰、明了,注释详尽、易懂,组织有条理,而且大部分源代码都是用大多数编译器都支持的ansi c语言写的,只有很少的一部分与微处理器相关的代码是用汇编语言写的。这给我们学习和理解内核代码带来了很大的方便,也使得移值工作得到最大程度的简化。
下面介绍如何将uc/os-ii移值到ti的浮点dsp芯片tms320c32上;(注:基于tms320c32的移值也适合于tms320c3x系列dsp芯片)。
一、tms320c32 dsp芯片介绍
tms320c3x是ti公司的第三代数字信号处理芯片(简称dsp芯片),也是其第一代浮点dsp芯片。tms320c32是其中的一款,能工作在60mhz的时钟频率下,指令运行速度达到60 mflops,是一款性价比很高的浮点处理器,在工业控制及其其他领域有着广泛的应用。
tms320c32芯片由如下几个模块组成:浮点cpu、512字节的片上ram、2个dma通道、1个串口、2个定时器、片上固化的引导程序等。另外c32内部还有如下的一些全局的、通用的寄存器:1)8个40-bit的寄存器(r0~r7),可以用来存放32-bit的整数,同时也可以用来存放40-bit的扩展精度的浮点数;2)8个32-bit的辅助寄存器(ar0~ar7),它们的主要作用是存储地址、参与各种模式的寻址等,当然也可以作为一般的寄存器来使用;3)状态寄存器st(含有全局中断使能位global interrupt-enable)、堆栈寄存器sp、中断标志寄存器if、中断使能寄存器ie、i/o标志寄存器iof、数据页指针寄存器dp(每页容量为64k,跨也操作时必须更新dp寄存器)、索引寄存器ir0、ir1(用来参与复杂的寻址)、块大小寄存器bk(循环寻址时指定数据块大小的寄存器)、重复执行寄存器rs(重复块起始地址)、re(重复块终止地址)、rc(重复次数);这些寄存器都是全局的,可以看成c语言中各种类型的变量,用作不同的用途;
要实现uc/os-ii向tms320c32的移值,需要c3x的c编译器支持,否则无从下手。我们使用的是ti公司的c编译器coder composer v4.10.36。这个c编译器允许嵌入行汇编,另外还具有强大的优化c编译的功能。
二、移值中所需修改的文件
和cpu相关的文件主要有三个:c语言文件os_cpu_c32.c、头文件os_cpu_c32.h和汇编文件os_cpu_c32.asm,我们的主要移值工作就是针对这三个文件做一些变动。
1.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`) /*用于任务切换的软中断*/
(1)数据类型
由于不同的处理器有不同的字长,所以uc/os-ii的移值包括了一系列的数据类型的定义,以确保其可移值性。这里我们定义一些c32以及code composer都能识别、处理的一些数据类型。
type size range
minimum maximum
char,signed char 32-bit -2147483648(-231) 2147483647(231-1)
unsigned char 32-bit 0 4294967295(232-1)
short 32-bit -2147483648(-231) 2147483647(231-1)
unsigned short 32-bit 0 4294967295(232-1)
int,signed int 32-bit -2147483648(-231) 2147483647(231-1)
unsigned int 32-bit 0 4294967295(232-1)
long,signed long 32-bit -2147483648(-231) 2147483647(231-1)
unsigned long 32-bit 0 4294967295(232-1)
float 32-bit 5.877472e-39 3.4028235e38
double 32-bit 5.877472e-39 3.4028235e38
long double 40-bit 5.87747175e-39 3.4028236684e38
tms320c32的数据类型(表i)
从上表我们可以看出,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位无符号整数;
(2)代码的临界区
uc/os-ii在进入系统临界代码区之前要关中断,避免临界区代码受多任务或中断服务程序的破坏,等到临界区代码执行完毕之后,该怎么处理呢?有两种方案可以供选择:1)不管关中断前中断使能情况是什么样子的,一律开中断;2)恢复关中断前中断使能情况,从一定程度上保证任务执行环境的完整性。这两种方案分别与方法一和方法二相对应。
c32中,状态寄存器st的第13位是全局中断使能位gie(global interrupt-enable),把该位置0,那么不管什么中断都不去被响应,直到临界区代码执行完毕为止。(注:c32没有不可屏蔽的中断nmi,对于别的芯片来说,如果有nmi的话,处理办法就是在这个中断服务程序isr中对st中的gie位进行判断,如果置0,那么这个isr简单响应一下这个中断,大部分处理工作放到gie置1后马上去执行)。宏os_enter_critical()把gie位置0而关闭所有中断。
(3)堆栈增长方向
c32处理器的堆栈是由低地址向高地址递增,所以os_stk_growth应该设置为1;
(4)进入任务切换函数os_task_sw()的定义
uc/os-ii中,进入任务切换是用函数os_task_sw()来实现的。这个函数通过软中断模拟了一次中断过程,在这个中断服务程序isr中实现任务的切换,切换的具体实现在介绍任务切换函数osctxsw()时详细阐述。c32共有28个软中断可供使用,其编号为0~27,通过执行汇编指令 trap 0、trap 1、……、trap27来产生软中断,也称为trap陷阱调用。这里,我们选择编号为27的软中断作为进入任务切换的中断,是由下面的语句完成这个定义的:
#define os_task_sw() asm(` trap 27`)
还要注意的一点这个中断服务程序的入口必须指向函数osctxsw()。
2.includes.h文件
includes.h是主要的头文件,在大都后缀名为.c的文件的开始都包含includes.h文件,使得我们的程序变得简洁、可读性强。对于不同的处理器、不同的编译器以及提供的不同的库文件,我们需要对includes.h文件进行修改,删除不使用的头文件,添加自己的头文件。而且如果头文件之间有包含关系、条件编译关联的,一定要排好他们之间的先后顺序。我们为c32编写的移值实例都放在一个目录下面,includes.h文件修改如下:
#ifndef __includes_h__
#define __includes_h__
#include `os_cfg.h`
#include `os_cpu.h`
#include `ucos_ii.h`
#include `c32.h`
#endif
其中c32.h文件包含了4个头文件:
#include `timerdef.h`
#include `serialport.h`
#include `dma.h`
#include `bus.h`
这四个文件分别对c32的定时器、串口、dma通道、总线编程时要用到的数据结构进行定义;可以说,这也是与cpu相关的一个头文件吧。
3.os_cpu_c32.asm文件
本来,这个汇编文件里面要实现4个函数:多任务启动函数中调用的osstarthighrdy()、中断任务切换函数osintctxsw()、任务切换函数osctxsw()、时钟节拍服务函数ostickisr();但是我这里只实现后面两个函数。前两个函数我们在os_cpu_c32.c中实现,顺便借此提及一点有关堆栈调整的知识。
(1)任务切换函数:osctxsw()
该函数是由于执行进入任务切换函数os_task_sw()而进入的,它是一个任务级的切换函数,不同与中断程序中调用的切换函数osintctxsw()。uc/os-ii中,如果任务执行了某个函数,而这个函数的执行结果是:或者改变当前任务的状态(执行了挂起任务函数ostasksuspend()、任务延时函数ostimedly())、或者是改变了别的任务的状态(恢复任务函数ostaskresume()、任务延时结束函数ostimedlyresume())都要引起新的任务调度:ossched();而任务调度函数查找出需要调度执行的任务的控制块地址放到ostcbhigrdy,然后执行进入任务切换函数os_task_sw()执行软中断,继而进行任务切换。任务切换流程:1)硬件进行进入中断处理工作:全局中断使能位置0、返回地址压栈。需要注意的是:有些cpu进入中断时会把一些全局的寄存器也压栈,但是这里,c32没有这么做,它只把返回地址压入当前任务的堆栈;2)保护上下文环境变量:把全局的寄存器中的值压入当前任务堆栈,这个是由一系列的push、pushf指令完成;3)修改当前任务控制块指针ostcbcur和当前任务优先级ospriocur;4)恢复当前任务的上下文环境:把堆栈中的值弹到全局寄存器中,这个是由一系列的pop、popf指令完成;5)开始执行当前任务,由一条指令完成:reti;
上下文环境切换的部分代码。
(2)时钟节拍函数:ostickisr()
uc/os-ii中,时钟节拍中断是一个非常重要的中断,因为整个操作系统的活动都受到它的激励。系统利用时钟中断来维持任务的延时、等待以及切换等操作,以保证多有任务都能平等的得到cpu的拥有权。可以说,它是整个os的脉搏。
ostickisr()的执行流程:1)硬件进行进入中断处理工作,同上;2)保护上下文环境,同上,有一点不同的就是当前任务的堆栈指针还没有保存到任务控制块相应的域中去;3)调用执行osintenter(),记录中断嵌套层数;4)调用ostimetick(),检查处理各个任务的延时,并根据情况修改就绪任务表;5)调用中断任务切换函数osintexit(),检查就绪任务表,看是否由比当前任务优先级更高的任务就绪,如果有,则进行调度;这里要提及的是,3)~5)三步执行时对环境的影响不波及到其他任何一个任务的环境,也就是3)~5)三步所形成的新的环境,不管系统进行不进行任务调度,必须全部舍弃而别别的任务的执行环境所覆盖。所以,如果没有比当前任务优先级更高的任务就绪,osintexit()返回并恢复2)所保存的上下文环境,并执行reti回到被中断的那个任务里继续运行;如果有,那么osintexit()就不返回到这里,具体的情况后面介绍osintexit()时具体阐述。
4.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而不使用这些函数。我们主要来讨论前三个函数:
(1)堆栈初始化函数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的指令的;
(2)中断任务级切换函数osintctxsw()
uc/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()之前时,当前任务堆栈的情况如图a)所示;调用osintexit函数后,当前任务堆栈的情况如图b)所示,为什么会这样呢?我们可以看看osintexit()函数编译后的汇编文件就明白了,这个函数入口的地方有如下几条语句:
_osintexit:
push fp
ldiu sp,fp
push ar4
然后该函数又调用中断切换函数osintctxsw(),这时当前任务堆栈的的情况如图c)所示;为什么会这样呢?我们看看osintctxsw()编译后的汇编文件就明白了,这个函数入口的地方有如下几条语句:
_osintctxsw:
push fp
由此可见,当别的任务需要调度时,当前任务需要把自己的堆栈指针sp调整到调用osintexit()之前的值,从图上可以看出只要把当前任务的堆栈指针的值减去“5”便可。
(3)多任务启动函数中调用的osstarthighrdy()
这个函数只在多任务启动函数中调用一次,主要用来把多任务启动时优先级最高的就绪任务的上下文环境从堆栈恢复过来。uc/os-ii系统启动时至少创建了一个任务――空闲任务,实际应用当然还要创建别的用户任务。osstart()首先从这些任务中查找出优先级最高的就绪任务,并把它的任务控制块的地址赋给ostcbhighrdy,然后调用osstarthighrdy()来运行ostcbhighrdy指向的那个任务控制块所对应的任务。其流程是:1)该函数的返回地址压入堆栈,注意,这里提到的堆栈是uc/os-ii系统使用的堆栈,而与其他任何任务使用的堆栈没有任何关系,而且这个地址压不压栈意义已经不大,因为不可能再从返回这里返回回去;2)变量osrunning赋值为true,标志多任务已经启动;3)从任务初始化过的堆栈中恢复上下文环境,代码和上面的雷同;4)执行reti指令运行这个任务;
三、两点补充说明
1.编译器的编译选项
在移值过程中,除了要熟悉uc/os-ii内核原理和目标芯片之外,熟悉使用相应的c编译器也非常重要。通常c编译器都提供一些编译选项,在移值的过程中,要注意这些选项的使用。使用不当,会给移值带来难移想象的麻烦。上面在介绍堆栈初始化函数也提及到有关dp寄存器的处理问题上,就是涉及到编译选项的选择:是选用small-memory还是large-memory模式,这个取决于编译选项-mb的打开和关闭。
2.中断函数的编写
在分析函数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*/
…………
asm(` reti`) /*用自己的返回指令返回*/
}
总结:在移值和运行的uc/os-ii过程中,也许还会有新的问题出现,遇到问题时要仔细分析,分析堆栈的使用、中断的影响,分析编译器生成的汇编代码,就可以解决这些问题,从而实现uc/os-ii的可*运行。
参考文献
1.《uc/os-ii——源码公开的实时嵌入式操作系统》 邵贝贝译 中国电力出版社 2001
2.tms320c3x/4x optimizing c compiler user’s guide texas instruments
3.《单片机与嵌入式系统应用》no.12 何立民编著