2024年4月2日发(作者:)

1 VC下时钟的获得

《嵌入式实时操作系统uC/OS-II》这本书已经安排了大量篇幅来专门讲解uC/OS-II的移

植:第13章移植uC/OS-II,第14章uC/OS-II在80x86上的移植,第15章uC/OS-II在带有硬

件浮点运算单元的80x86上的移植。所以本文只是重点讲解移植到VC下和其他处理器上的不

同地方,更详细的介绍读者可以参考《嵌入式实时操作系统uC/OS-II》这本书。和所有其他的

移植一样,本文所做的移植也只需要修改uC/OS-II处理器相关代码,一共包括3个文件:

OS_CPU.H,OS_CPU_,OS_CPU_C.C。考虑到VC可以嵌入汇编代码,并不需要专门

的汇编代码文件,所以OS_CPU_是多余的,最终只有OS_CPU.H和OS_CPU_C.C两

个文件。所以这两个文件成了移植的关键,首先要解决的问题就是时钟“滴答”的获得。

移植到BC下的uC/OS-II是通过修改DOS下的硬件时钟中断来得到时钟滴答的,VC下时

钟滴答从哪里来呢?这是移植uC/OS-II到VC下第一个要考虑的问题。在windows的保护模式

下不能像DOS下面那么容易,直接通过一个函数调用就能够修改中断。windows下要修改中

断涉及到驱动程序,这样就加大了移植的困难度与复杂度,但好处是只有真正硬件时钟的“滴

答”才能够保证uC/OS-II的实时性。另外一种解决方法是采用windows下的软件定时器,通

过定时器来产生模拟时钟“滴答”。考虑到本移植只是为了教学和学习,并没有应用到对实时

性要求高的产品,所以最终决定采用软件定时器来模拟时钟中断。Windows下软件定时器种类

很多,下面分别简要介绍一下这些定时器:

er()函数

有windows下编程经验的最先想到的应该是SetTimer这个API函数,但本文采用的移植

程序是基于控制台的,也就是说最开始建立VC工程的时候选择的是创建win32 console

application,控制台下的程序是没有消息循环的,所以要使用SetTimer函数则必须再创建一个

线程来专门处理消息循环,这样一来事情就复杂了,而且这个函数定时精度非常不高。所以这

种方法不是特别合适。

tEvent()函数

这个函数很简单,不需要消息循环,定时精度为ms级,主要应用在多媒体定时方面,能

够在非常精确的时间间隔内完成一个事件、函数或过程的调用。函数原型:MMRESULT

timeSetEvent(UINT uDelay, UINT uResolution,LPTIMECALLBACK lpTimeProc,WORD

dwUser,UINT fuEvent ),可以通过调用timeSetEvent()函数,将需要周期性执行的任务定义

在LpTimeProc回调函数中,从而完成所需处理的事件。调用这个函数后会增加一个线程,时

间一到则在这个线程中调用回调函数,对于主线程来说,非常类似外部中断调用,我们需要的

正是这样的效果,所以本文最终选择这个函数来产生时钟“滴答”。

erformanceFrequency()和 QueryPerformanceCounter()函数

这两个函数可以实现更高精度的定时,误差不超过1微秒,进行定时之前,先调用

QueryPerformanceFrequency()函数获得机器内部定时器的时钟频率, 然后在需要严格定时的事

件发生之前和发生之后分别调用QueryPerformanceCounter()函数,利用两次获得的计数之差及

时钟频率,计算出事件经 历的精确时间。可见,这两个函数主要是应用在计算时间方面,并

没有设置回调函数机制,如果我们要使用这个函数的话,则需要首先创建一个线程,然后在这

个线程中计算时间调用我们要定时处理的函数,等于需要手动实现定时函数回调机制,远比

timeSetEvent()函数来得复杂。

当然还有更多的定时器函数,这里不一一介绍,读者可以自行参考相关书籍。本文选择的

是timeSetEvent函数,调用这个函数后uC/OS-II就已经开始它的脉搏了。

2 模拟时钟中断的产生

中断指的是中止当前的事务,处理别的更要紧的事情。我们通过软件定时器来模拟产生

uC/OS-II的时钟中断,但timeSetEvent()函数调用定时回调函数是和主线程同时被windows操

作系统调度的,并没有起到中断的作用。所以在调用定时回调函数的时候必须停止主线程的运

行,退出回调函数则恢复主线程的运行,自然这些事情可以都放在定时回调函数,也就是

uC/OS-II的时钟中断处理函数中完成。Windows下要挂起一个线程的运行,首先要得到这个线

程的句柄,然后调用SuspendThread(hangdler)和ResumeThread(handler)就可以挂起和继

续执行线程。如图1所示。

主线程

OSTickISR运行

定时器线程

OSTickISR退出

主线程调用

timesetEvent函数后

定时器线程启动

时间到调用OSTickISR

时钟中断处理函数,此

函数挂起主线程

恢复主线

程运行

图1 模拟中断

在我们的程序中刚开始只有一个线程,调用timeSetEvent则产生了定时器线程,定时器线

程会按我们预定的时间周期调用uC/OS-II的中断处理函数OSTickISR,如果uC/OS-II没有屏

蔽中断,OSTickISR则挂起主线程,然后才做它改做的事情:调度任务执行。

在main函数的的OSInit()前,我们加入了一个VCInit()函数,主要初始化VC环境,包括

获得主线程的句柄,设置上下文环境标志位,特别要注意的是,句柄的获得是要通过伪句柄转

换的,如程序清单 L1所示

程序清单 L1 初始化VC环境

void main (void)

{

VCInit(); //在此加入我们的VCInit函数

OSInit(); // 初始化uC/OS-II

// 安装uC/OS-II的任务切换向量

// 创建用户起始任务(为了方便讨论,这里以TaskStart()作为起始任务)

OSStart(); // 开始多任务调度

}

HANDLE mainhandle;

//增加的全局变量:主线程句柄

CONTEXT Context; //增加的全局变量:主线程切换上下文

void VCInit(void)

{

HANDLE cp,ct;

tFlags = CONTEXT_CONTROL;

cp = GetCurrentProcess(); //得到当前进程句柄

Ct = GetCurrentThread(); //得到当前线程伪句柄

DuplicateHandle(cp, ct, cp, &mainhandle, 0, TRUE, 2); // 伪句柄转换,得到线程真句柄

}

时钟中断处理子程序OSTickISR将在调用timeSetEvent后,被定时器线程周期调用,其代

码如程序清单 L2所示

程序清单 L2 时钟节拍ISR

Void CALLBACK OSTickISR(unsigned int a,unsigned int b,unsigned long c,unsigned long

d,unsigned long e)

{

if(!FlagEn) //通过一个全局变量表示是否屏蔽中断

return; //如果当前中断被屏蔽则返回

SuspendThread(mainhandle); //中止主线程的运行,模拟中断产生.但没有保存寄存器

}

GetThreadContext(mainhandle, &Context); //得到主线程上下文,为切换任务做准备

OSIntNesting++;

if (OSIntNesting == 1) {

OSTCBCur->OSTCBStkPtr = (OS_STK *); //保存当前esp

//ucos内部定时

//由于不能使用中断返回指令,所以此函数是要返回的

}

OSTimeTick();

OSIntExit();

ResumeThread(mainhandle); //模拟中断返回,主线程得以继续执行

开关中断我们通过设置一个全局变量来解决,代码如下:

#define OS_ENTER_CRITICAL() FlagEn=0 //禁止定时器 调度

#define OS_EXIT_CRITICAL() FlagEn=1 //容许定时器调度

因此在中断处理程序中首先判断当前是否容许中断,如果容许才挂起主线程,模拟中断产

生,完成进一步的工作。

3 任务切换

任务切换,其实做的是任务的上下文切换,在其他CPU上非常容易分辨出任务的上下文,

一般就是CPU上的相应寄存器,那么在VC下呢?从简单考虑,我们选择了不带浮点运算的上

下文环境,因此任务的上下文和uC/OS-II在80x86上移植的上下文很相近,不同点只是段寄

存器不用保存,因为在VC下任务其实只是在同一个线程中切换,而且在保护模式下段寄存器

的概念已变,其值在同一个线程中是不会变的。任务切换压栈时如图2所示

低端地址

EDI

ESI

EBP

ESP

模拟pushad

EBX

EDX

ECX

EAX

子程序是从当前的

模拟pushfd

PSW

esp+4处取得的

OFF task

保存任务切换地址

传入参数,所以要

0

空出4个字节

OFF pdata

任务参数地址

高端地址

图2 任务切换压栈后状态

手动任务切换代码如程序清单L3所示,主要涉及VC中嵌入汇编的编写,请读者自行分析

程序清单L3 手动任务切换

void OSCtxSw(void)

{

_asm{

lea eax, nextstart ;任务切换回来后从nextstart开始

push eax

pushfd ;标志寄存器的值

pushad ;保存EAX -- EDI

mov ebx, [OSTCBCur]

mov [ebx], esp ;把堆栈入口的地址保存到当前TCB结构中

}

OSTaskSwHook();

OSTCBCur = OSTCBHighRdy;

OSPrioCur = OSPrioHighRdy;

_asm{

mov ebx, [OSTCBCur]

mov esp, [ebx] ;得到OSTCBHighRdy的esp

popad ;恢复所有通用寄存器,共8个

popfd ;恢复标志寄存器

ret ;跳转到指定任务运行

}

nextstart: //任务切换回来的运行地址

return;

}

至此,其他VC下的移植方法与普通的移植无异,不必再深究下去,请读者自行分析