void PC_DOSReturn (void)
{
PC_ExitFlag = TRUE; /* Indicate we are returning to DOS */
longjmp(PC_JumpBuf, 1); /* Jump back to saved environment */
}
static jmp_buf PC_JumpBuf;
如果调用这个函数,那么就跳回,执行如下代码
void PC_SetTickRate (INT16U freq)
{
#if OS_CRITICAL_METHOD == 3 /* Allocate storage for CPU status register */
OS_CPU_SR cpu_sr;
#endif
INT16U count;
if (freq == 18) { /* See if we need to restore the DOS frequency */
count = 0;
} else if (freq > 0) {
/* Compute 8254 counts for desired frequency and ... */
/* ... round to nearest count */
count = (INT16U)(((INT32U)2386360L / freq + 1) >> 1);
} else {
count = 0;
}
OS_ENTER_CRITICAL();
outp(TICK_T0_8254_CWR, TICK_T0_8254_CTR0_MODE3); /* Load the 8254 with desired frequency */
outp(TICK_T0_8254_CTR0, count & 0xFF); /* Low byte */
outp(TICK_T0_8254_CTR0, (count >> 8) & 0xFF); /* High byte */
OS_EXIT_CRITICAL();
}
函数PC_SetTickRate()允许用户为 ?C /OS-II定义频率,以改变钟节拍的速率。在DOS下,每秒产生18.20648次时钟节拍,或每隔54.925ms一次。这是因为82C54定时器芯片没有初始化,而使用默认值65,535的结果。如果初始化为58,659,那么时钟节拍的速率就会精确地为20.000Hz。笔者决定将时钟节拍设得更快一些,用的是200Hz(实际是上是 199.9966Hz)。注意OS_CPU_A.ASM中的OSTickISR()函数将会每11个时钟节拍调用一次DOS中的时钟节拍处理,这是为了保证在DOS下时钟的准确性。如果用户希望将时钟节拍的速度设置为20HZ,就必须这样做。在返回DOS以前,要调用PC_SetTickRate(),并设置18为目标频率,PC_SetTickRate()就会知道用户要设置为18.2Hz,并且会正确设置82C54。
首先我们来了解一下PC机的时钟是如何工作的,PC机采用一块8253定时器芯片计算系统时钟的脉冲,若干个系统时钟周期转换成一个脉冲,这些脉冲序列可以用以计时,也可以送入计算机的扬声器产生特定频率的声音。8253定时器芯片独立于CPU运行,它可以象实时时钟那样,CPU的工作状态对它没有任何影响。
8253芯片有三个独立的信道,每个信道的功能各不相同,三个信道的功能如下:信道0:为系统时钟所用,在启动时由BIOS置入初值,每秒钟约发出18.2个脉冲,脉冲的计数值存放在BIOS数据区的0040:006c存储单元中(注意,这个单元的内容对我们非常有用!),信道0的输出脉冲作为申请定时器中断的请求信号,还用于磁盘的某些定时操作,如果改变了信道0的计数值,必须确保在CPU每次访问磁盘以前恢复原来的读数,否则将使磁盘读写产生错误。信道1:用于控制计算机的动态RAM刷新速率,一般情况下不要去改变它。信道2:连接计算机的扬声器,产生单一的方波信号控制扬声器发声。8253定时器芯片的每一个信道含有3个寄存器,CPU通过访问3个端口(信道0为40h,信道1为41h,信道2为42h)来访问各个端口的3个寄存器,8253每个端口有6种工作模式,当信道0用于定时或信道2用于定时或发声时,一般用模式3。在模式3下,计数值被置入锁存器后立即复制到计数器,计数器在每次系统时钟到来时减1,减至0后一方面马上从锁存器中重新读取计数值,另一方面向CPU发出一个中断请求(INT 1CH中断,很有用),如此循环在输出线上高低电平的时间各占计数时间的一半,从而产生方波输出。
对8253定时器芯片编程是通过命令端口寄存器43h来实现,它决定选用的信道、工作模式、送入锁存器的计数值是一字节还是两字节、是二进制码还是BCD码等工作参数,端口43h各位的组合形式如下:
位0____若为1则采用二进制表示,否则用BCD码表示计数值。
位3-1____工作模式号,其值(0-5)对应6种模式。
位5-4____操作的类型:00:把计数值送入锁存器;
01:读写高字节;
10:读写低字节;
11:先读写高字节,再读写低字节;
位7-6____决定选用的信道号,其值为0-2。
对8253芯片编程的三个步骤:
1.设置命令端口43h,
2.向端口发送一个工作状态字节,
3.确定定时器的工作方式;
若是信道2,给端口61h的第0位和第1位置数,启动时钟信号,当第1位置1时,信道2驱动扬声器,置0时用于定时操作;将一个字的计数值,按先低字节后高字节的顺序送入信道的I/O端口寄存器(信道0为40h,信道1为41h,信道2为42h)。当第3个步骤完成后被编程的信道马上在新的状态下开始工作。由于8253的三个信道都独立于CPU运行,所以在程序结束以前要恢复各信道的正常状态值。
在游戏中我们只使用信道0,怎样对其进行编程呢?首先得确定放入锁存器的16位的计数值:
16位的计数值=1.19318MB/希望的频率其中1.19318MB是系统振荡器的频率。
由上式可知,计数器所能产生的值是18.2Hz-1.19318MHz,这已经足够了,可以满足我们游戏的要求。
以下是对8253定时器芯片信道0编程的函数:
#define T60HZ 0x4dae
#define T50HZ 0x5d37
#define T40HZ 0x7468
#define T30HZ 0x965c
#define T20HZ 0xe90b
#define T18HZ 0xffff
#define LOW_BYTE(n) (n&0x00ff)
#define HI_BYTE(n) ((n>>8)&0x00ff)
int far *clk=(int far *)0x0000046c;
1.改变时间定时器值的函数:
void ChangTime(unsigned cnt)
{
outportb(0x43,0x3c);
outportb(0x40,LOW_BYTE(cnt));
outportb(0x40,HI_BYTE(cnt));
}
2.延时函数:
void Delay(int d)
{
int tm=*clk;
while(*clk-tm }
上面定义的指针*clk指向BIOS数据区的0040:006c存储单元,该单元中存放着定时器的计数值,我们可以根据该单元的内容计算差值来达到延时的目的。
需要注意的是,由于改变定时器计数值的操作会与保护模式下的代码发生冲突,所以不能在WINDOWS的DOS仿真环境下改变定时器,必须在纯DOS环境里使用。改变了定时器值以后,程序结束以前必须恢复原来的值,如果不恢复原来的18.2次的计数值,在读写磁盘操作时将引起读写错误甚至死机。
我们将时钟设置成每秒60HZ这样调用函数:ChangTime(T60HZ);
每个PC机中都有一个PIT,以通过IRQ0产生周期性的时钟中断信号。当前使用最普遍的是Intel 8254 PIT芯片,它的I/O端口地址是0x40~0x43。
Intel 8254 PIT有3个计时通道,每个通道都有其不同的用途:
(1) 通道0用来负责更新系统时钟。每当一个时钟滴答过去时,它就会通过IRQ0向系统产生一次时钟中断。
(2) 通道1通常用于控制DMAC对RAM的刷新。
(3) 通道2被连接到PC机的扬声器,以产生方波信号。
每个通道都有一个向下减小的计数器,8254 PIT的输入时钟信号的频率是1193181HZ,也即一秒钟输入1193181个clock-cycle。每输入一个clock-cycle其时间通道的计数器就向下减1,一直减到0值。因此对于通道0而言,当他的计数器减到0时,PIT就向系统产生一次时钟中断,表示一个时钟滴答已经过去了。当各通道的计数器减到0时,我们就说该通道处于“Terminal count”状态。
通道计数器的最大值是10000h,所对应的时钟中断频率是1193181/(65536)=18.2HZ,也就是说,此时一秒钟之内将产生18.2次时钟中断。
7.1.2.1 PIT的I/O端口
在i386平台上,8254芯片的各寄存器的I/O端口地址如下:
Port Description
40h Channel 0 counter(read/write)
41h Channel 1 counter(read/write)
42h Channel 2 counter(read/write)
43h PIT control word(write only)
其中,由于通道0、1、2的计数器是一个16位寄存器,而相应的端口却都是8位的,因此读写通道计数器必须进行进行两次I/O端口读写操作,分别对应于计数器的高字节和低字节,至于是先读写高字节再读写低字节,还是先读写低字节再读写高字节,则由PIT的控制寄存器来决定。8254 PIT的控制寄存器的格式如下:
(1)bit[7:6]——Select Counter,选择对那个计数器进行操作。“00”表示选择Counter 0,“01”表示选择Counter 1,“10”表示选择Counter 2,“11”表示Read-Back Command(仅对于8254,对于8253无效)。
(2)bit[5:4]——Read/Write/Latch格式位。“00”表示锁存(Latch)当前计数器的值;“01”只读写计数器的高字节(MSB);“10”只读写计数器的低字节(LSB);“11”表示先读写计数器的LSB,再读写MSB。
(3)bit[3:1]——Mode bits,控制各通道的工作模式。“000”对应Mode 0;“001”对应Mode 1;“010”对应Mode 2;“011”对应Mode 3;“100”对应Mode 4;“101”对应Mode 5。
(4)bit[0]——控制计数器的存储模式。0表示以二进制格式存储,1表示计数器中的值以BCD格式存储。
7.1.2.2 PIT通道的工作模式
PIT各通道可以工作在下列6种模式下:
1. Mode 0:当通道处于“Terminal count”状态时产生中断信号。
2. Mode 1:Hardware retriggerable one-shot。
3. Mode 2:Rate Generator。这种模式典型地被用来产生实时时钟中断。此时通道的信号输出管脚OUT初始时被设置为高电平,并以此持续到计数器的值减到1。然后在接下来的这个clock-cycle期间,OUT管脚将变为低电平,直到计数器的值减到0。当计数器的值被自动地重新加载后,OUT管脚又变成高电平,然后重复上述过程。通道0通常工作在这个模式下。
4. Mode 3:方波信号发生器。
5. Mode 4:Software triggered strobe。
6. Mode 5:Hardware triggered strobe。
7.1.2.3 锁存计数器(Latch Counter)
当控制寄存器中的bit[5:4]设置成0时,将把当前通道的计数器值锁存。此时通过I/O端口可以读到一个稳定的计数器值,因为计数器表面上已经停止向下计数(PIT芯片内部并没有停止向下计数)。NOTE!一旦发出了锁存命令,就要马上读计数器的值。
7.1.3 时间戳记数器TSC
从Pentium开始,所有的Intel 80x86 CPU就都又包含一个64位的时间戳记数器(TSC)的寄存器。该寄存器实际上是一个不断增加的计数器,它在CPU的每个时钟信号到来时加1(也即每一个clock-cycle输入CPU时,该计数器的值就加1)。
汇编指令rdtsc可以用于读取TSC的值。利用CPU的TSC,操作系统通常可以得到更为精准的时间度量。假如clock-cycle的频率是400MHZ,那么TSC就将每2.5纳秒增加一次。