8259可编程中断控制器即PIC,是构成x86体系架构的最重要的芯片之一,没有它,处理器就无法接收到外部设备产生的中断请求信号。8259可以有效管理周边外设产生的中断信号,并将该信号输出给CPU...

    v0.0.4的项目地址:

    github.com地址:https://github.com/zenglong/zenglOX (只包含git提交上来的源代码)
   
    Dropbox地址:点此进入Dropbox网盘  (该版本位于zenglOX_v0.0.4的文件夹,该文件夹里的zip压缩包为源代码)

    sourceforge地址:https://sourceforge.net/projects/zenglox/files  (该版本位于zenglOX_v0.0.4的文件夹,该文件夹里的zip压缩包为源代码)

    另外再附加一个英特尔英文手册的共享链接地址:
    http://pan.baidu.com/share/link?shareid=2345340326&uk=940392313 (里面有APIC等的相关介绍)

    有关zenglOX的编译及gdb调试的方法,请参考zenglOX v0.0.1里的文章。

IRQ(中断请求):

    要了解外部设备IRQ中断请求的方式,就必须先了解PIC,在 http://wiki.osdev.org/PIC 该链接对应的文章里有PIC的完整英文介绍,下面就对该篇文章里涉及到的内容做个简单的描述。

    8259可编程中断控制器即PIC,是构成x86体系架构的最重要的芯片之一,没有它,处理器就无法接收到外部设备产生的中断请求信号。8259芯片可以有效管理周边外设产生的中断信号,并将该信号输出给CPU,CPU收到不同的中断信号时就会调用对应的处理例程来进行处理。

    在比较新的计算机系统里,8259 PIC已经被APIC(高级的可编程中断控制器)取代,不过APIC是兼容PIC的,所以下面用于设置PIC的代码同样可以运行在APIC的系统里,APIC被广泛用于多核心,多处理器的系统,它可以在多个处理器之间发送中断信号,让多处理器能够协调工作,这点在旧的8259 PIC里是无法实现的,有关APIC的详情可以参考 http://wiki.osdev.org/APIC 或者参考英特尔英文手册的第2194页(即手册的第三卷的第十章)。下面只讲解和8259 PIC有关的内容。

    在一开始早期的IBM PC和XT的机子上,只使用了一个单一的8259 PIC芯片,如下图所示:


图1

    上图里显示8259 PIC芯片里提供了0到7一共8个引脚,可以连接8个外设,即该芯片可以接受8个IRQ中断请求信号,0到7的引脚,每个引脚都对应有一个中断向量号,该向量号是在PIC初始化时由程序来设定的,主板刚运行启动时,最开始是由主板里的BIOS程式来进行设定,通常情况下,BIOS会将PIC里的向量号依次设置为8到15(十六进制为0x08到0x0F),即0号引脚对应的向量号为0x08,7号引脚对应的向量号为0x0F,当0号引脚收到外部设备的中断请求时,会将0x08的中断向量号传给CPU,CPU就会根据0x08的中断号,查询中断表,再定位跳转到对应的处理程式去处理该中断。

    后来的IBM PC/AT机子里,在原来PIC芯片基础上,又级联了一个PIC芯片,如下图所示:
 


图2

    这样原来单一的PIC只能处理8个中断请求,而现在就可以处理15个中断请求,主芯片的2号引脚与Slave PIC芯片的out输出引脚级联在一起,占用掉了一个IRQ中断引脚。另外还有一点需要注意的是,由于2号引脚用于级联Slave PIC芯片了,所以原来接在2号引脚上的设备现在接在IRQ9上(IRQ9的位置下面有说明),在软件上只需增加IRQ9的中断服务程序,由它再调用IRQ2的中断服务程序,就可以和原有系统保持兼容,这里需要额外说明的是:Master PIC的0到7号引脚对应的中断请求为IRQ0到IRQ7,而Slave PIC的0到7号引脚对应的中断请求为IRQ8到IRQ15 (所以这里提到的IRQ9就对应Slave PIC的1号引脚)。

    主从芯片的工作原理大致为:当PIC的某个IRQ引脚收到外设的信号时,会先检查该中断信号对应的通道是否被掩码位屏蔽,如果没有被屏蔽,又没有其他中断请求在等待处理中,那么Master PIC就会通过out引脚向CPU的中断线发送信号,如果是Slave PIC则先由out引脚向Master PIC的IRQ2引脚发送信号,再由Master PIC向CPU发送信号,CPU在接收到信号后,会先向Master PIC和Slave PIC进行查询,判断是哪个芯片发出的中断,再通知对应的芯片将中断号传过来,PIC收到通知后,就会将软件(BIOS或操作系统)初始化时设定的中断向量号通过相关总线传给CPU,CPU在收到中断向量号后,如果是在保护模式下,CPU就会根据中断向量号,查询操作系统设置的IDT(中断描述符表),并从该表里找到对应的中断处理程式的地址,再跳转到该地址去执行,有关IDT的设置和原理,可以参考上一篇zenglOX v0.0.3 初始化GDT和IDT的文章。

    下面介绍下如何对8259 PIC芯片进行编程:

    每个PIC芯片(包括master主芯片与slave从芯片)都有一个命令端口和一个数据端口,如下表所示:

端口名称 对应的I/O端口号
Master PIC - Command 
主芯片命令端口
0x0020
Master PIC - Data 
主芯片数据端口
0x0021
Slave PIC - Command 
从芯片命令端口
0x00A0
Slave PIC - Data 
从芯片数据端口
0x00A1

    在没有向命令端口发出命令时,可以通过数据端口将8259 PIC芯片的中断掩码给读取出来。

    前面我们提到,在一开始,Master PIC的中断向量号被依次设置为8到15,但是这段向量号在IDT里已经被CPU预定义的中断和异常给占用掉了(可以参考上一篇文章的相关说明),所以在zenglOX里需要重新设置PIC的中断向量号,要设置中断向量号,就必须对主从PIC芯片进行重新初始化,所以在zlox_descriptor_tables.c里的zlox_init_idt函数里就增加了以下代码:

// Initialise the interrupt descriptor table.
static ZLOX_VOID zlox_init_idt()
{   
...............................................
...............................................

    // Remap the irq table.
    // ICW1: starts the initialization sequence (in cascade mode)
    zlox_outb(0x20, 0x11);
    zlox_outb(0xA0, 0x11);
    // ICW2: Master PIC vector offset
    zlox_outb(0x21, 0x20);
    // ICW2: Slave PIC vector offset
    zlox_outb(0xA1, 0x28);
    // ICW3: tell Master PIC that there is a slave PIC at IRQ2 (0000 0100)
    zlox_outb(0x21, 0x04);
    // ICW3: tell Slave PIC its cascade identity (0000 0010)
    zlox_outb(0xA1, 0x02);
    // ICW4: 8086/88 (MCS-80/85) mode
    zlox_outb(0x21, 0x01);
    // ICW4: 8086/88 (MCS-80/85) mode
    zlox_outb(0xA1, 0x01);
    // masks.
    zlox_outb(0x21, 0x0);
    // masks.
    zlox_outb(0xA1, 0x0);

...............................................
...............................................

    zlox_idt_set_gate(ZLOX_IRQ0, (ZLOX_UINT32)_zlox_irq_0, 0x08, 0x8E);
    zlox_idt_set_gate(ZLOX_IRQ1, (ZLOX_UINT32)_zlox_irq_1, 0x08, 0x8E);
    zlox_idt_set_gate(ZLOX_IRQ2, (ZLOX_UINT32)_zlox_irq_2, 0x08, 0x8E);
    zlox_idt_set_gate(ZLOX_IRQ3, (ZLOX_UINT32)_zlox_irq_3, 0x08, 0x8E);
    zlox_idt_set_gate(ZLOX_IRQ4, (ZLOX_UINT32)_zlox_irq_4, 0x08, 0x8E);
    zlox_idt_set_gate(ZLOX_IRQ5, (ZLOX_UINT32)_zlox_irq_5, 0x08, 0x8E);
    zlox_idt_set_gate(ZLOX_IRQ6, (ZLOX_UINT32)_zlox_irq_6, 0x08, 0x8E);
    zlox_idt_set_gate(ZLOX_IRQ7, (ZLOX_UINT32)_zlox_irq_7, 0x08, 0x8E);
    zlox_idt_set_gate(ZLOX_IRQ8, (ZLOX_UINT32)_zlox_irq_8, 0x08, 0x8E);
    zlox_idt_set_gate(ZLOX_IRQ9, (ZLOX_UINT32)_zlox_irq_9, 0x08, 0x8E);
    zlox_idt_set_gate(ZLOX_IRQ10, (ZLOX_UINT32)_zlox_irq_10, 0x08, 0x8E);
    zlox_idt_set_gate(ZLOX_IRQ11, (ZLOX_UINT32)_zlox_irq_11, 0x08, 0x8E);
    zlox_idt_set_gate(ZLOX_IRQ12, (ZLOX_UINT32)_zlox_irq_12, 0x08, 0x8E);
    zlox_idt_set_gate(ZLOX_IRQ13, (ZLOX_UINT32)_zlox_irq_13, 0x08, 0x8E);
    zlox_idt_set_gate(ZLOX_IRQ14, (ZLOX_UINT32)_zlox_irq_14, 0x08, 0x8E);
    zlox_idt_set_gate(ZLOX_IRQ15, (ZLOX_UINT32)_zlox_irq_15, 0x08, 0x8E);

...............................................
...............................................
}


			

    在进入保护模式后,要初始化PIC,就需要给两个PIC分别传递初始化命令(命令代码为0x11),所以上面代码里,一开始就分别向0x20和0xA0即两个PIC的命令端口输出了0x11的命令代码,PIC在收到0x11的初始化命令(ICW1)后,就会在对应的数据端口依次等待3个额外的"initialisation words(初始化字)",这些初始化字的作用如下:
  • 设置起始中断向量号 (ICW2)
  • 设置主从芯片的级联方式 (ICW3)
  • 设置PIC芯片的工作模式等额外信息 (ICW4)
    上面代码通过zlox_outb(0x21, 0x20);函数将Master PIC芯片的起始中断向量号设置为0x20,即Master PIC的0号引脚(IRQ0)对应中断号为0x20,通过zlox_outb(0xA1, 0x28);函数将Slave PIC的起始中断向量号设置为0x28,即Slave PIC的0号引脚(IRQ8)对应的中断号为0x28。这里需要注意的是,起始中断向量号不可以随便设置,起始中断向量号必须能被8整除,上面的0x20即十进制32能被8整除,0x28即十进制40也能被8整除。

    接着代码使用zlox_outb(0x21, 0x04);函数告诉Master PIC芯片,在2号引脚(IRQ2)上级联了一个Slave PIC芯片,函数的第二个参数0x04对应二进制为 0000 0100,位2上被设置为1,代表2号引脚上级联了从芯片。然后zlox_outb(0xA1, 0x02);函数用于告诉Slave PIC,它的级联标识为0x02 。

    在设置完主从芯片的ICW2与ICW3初始化字后,代码通过zlox_outb(0x21, 0x01);和zlox_outb(0xA1, 0x01);函数将主从芯片设置为8086/88模式。

    最后由zlox_outb(0x21, 0x0);与zlox_outb(0xA1, 0x0);函数来将主从芯片的屏蔽掩码设置为0,表示不屏蔽外部设备的中断。

    在设置好PIC芯片的中断向量号后,就只需按照上一篇文章里IDT相关部分的做法,依次编写和设置IRQ0到IRQ15(对应向量号32到47)的中断处理程序即可,如上面的zlox_init_idt里就调用zlox_idt_set_gate(ZLOX_IRQ0, (ZLOX_UINT32)_zlox_irq_0, 0x08, 0x8E);函数将ZLOX_IRQ0的中断处理程序设置为_zlox_irq_0,该函数是zlox_interrupt.s里设置的汇编函数:

................................
................................

.macro _ZLOX_INT_IRQ irqNum intNum
.global _zlox_irq_\irqNum
_zlox_irq_\irqNum:
	cli
	pushl $0
	pushl $\intNum
	jmp _zlox_irq_common_stub
.endm

................................
................................

_ZLOX_INT_IRQ	0,	32
_ZLOX_INT_IRQ	1,	33
_ZLOX_INT_IRQ	2,	34
_ZLOX_INT_IRQ	3,	35
_ZLOX_INT_IRQ	4,	36
_ZLOX_INT_IRQ	5,	37
_ZLOX_INT_IRQ	6,	38
_ZLOX_INT_IRQ	7,	39
_ZLOX_INT_IRQ	8,	40
_ZLOX_INT_IRQ	9,	41
_ZLOX_INT_IRQ	10,	42
_ZLOX_INT_IRQ	11,	43
_ZLOX_INT_IRQ	12,	44
_ZLOX_INT_IRQ	13,	45
_ZLOX_INT_IRQ	14,	46
_ZLOX_INT_IRQ	15,	47

................................
................................

# This is our common IRQ stub. It saves the processor state, sets
# up for kernel mode segments, calls the C-level fault handler,
# and finally restores the stack frame.
_zlox_irq_common_stub:
	pusha		# Pushes edi,esi,ebp,esp,ebx,edx,ecx,eax

	movw %ds,%ax	# Lower 16-bits of eax = ds.
	pushl %eax	# save the data segment descriptor

	movw $0x10,%ax	# load the kernel data segment descriptor
	movw %ax,%ds
	movw %ax,%es
	movw %ax,%fs
	movw %ax,%gs

	call zlox_irq_handler	# in zlox_isr.c

	pop %ebx		# reload the original data segment descriptor
	movw %bx,%ds
	movw %bx,%es
	movw %bx,%fs
	movw %bx,%gs

	popa		# Pops edi,esi,ebp...
	addl $8,%esp	# Cleans up the pushed error code and pushed ISR number
	sti
	iret		# pops 5 things at once: CS, EIP, EFLAGS, SS, and ESP


    由于_zlox_irq_开头的中断处理程序比较多,所以也采用上一篇里提到的宏方法来批量进行设置,这些IRQ中断请求处理程序都会jmp _zlox_irq_common_stub跳转到_zlox_irq_common_stub标签处执行,在该标签里的代码接着会通过call zlox_irq_handler指令,调用进入zlox_isr.c里的zlox_irq_handler函数:

ZLOX_ISR_CALLBACK interrupt_callbacks[256];

..........................................
..........................................

// This gets called from our ASM interrupt handler stub.
ZLOX_VOID zlox_irq_handler(ZLOX_ISR_REGISTERS regs)
{
    // Send an EOI (end of interrupt) signal to the PICs.
    // If this interrupt involved the slave.
    if (regs.int_no >= ZLOX_IRQ8)
    {
        // Send reset signal to slave.
        zlox_outb(0xA0, 0x20);
    }
	// Send reset signal to master. (As well as slave, if necessary).
        zlox_outb(0x20, 0x20);
	
	//zlox_monitor_write("zenglox recieved irq: ");
	//zlox_monitor_write_dec(regs.int_no);
	//zlox_monitor_put('\n');

    if (interrupt_callbacks[regs.int_no] != 0)
    {
        ZLOX_ISR_CALLBACK callback = interrupt_callbacks[regs.int_no];
        callback(regs);
    }
}


    在IRQ中断请求处理结束时,需要向PIC芯片发送EOI(end of interrupt中断结束命令),EOI对应的命令代码为0x20,另外,如果是Slave PIC芯片接收到的中断信号,需要向Slave PIC和Master PIC都发送EOI命令,因为Slave PIC是通过Master PIC向CPU发送中断信号的,如果只是Master PIC芯片接收到的中断信号,则只需向Master PIC芯片发送EOI命令即可。所以上面代码先通过if (regs.int_no >= ZLOX_IRQ8)判断中断号是否大于等于ZLOX_IRQ8(对应的宏值为40),如果大于等于ZLOX_IRQ8则说明当前的中断请求是由Slave PIC接收到的,则先通过zlox_outb(0xA0, 0x20);函数向Slave PIC发送EOI命令,再通过zlox_outb(0x20, 0x20);函数向Master PIC发送EOI命令。

    接着再通过if (interrupt_callbacks[regs.int_no] != 0)函数判断是否注册了该中断对应的callback回调处理函数,如果注册了,则调用对应的处理函数,interrupt_callbacks是开头注册的一个数组,该数组里存放了各种中断处理函数的函数指针。

PIT(可编程间隔定时器):

    PIT的详细英文说明可以参考 http://wiki.osdev.org/Programmable_Interval_Timer 这篇文章,下面做个简单的讲解。

    PIT(Programmable Interval Timer 可编程间隔定时器)芯片也就是8253或8254芯片,这些芯片基本上是由一个oscillator(振荡器),一个prescaler(预分频器)以及三个独立的frequency dividers(分频器)组成,每个frequency dividers分频器都有一个输出引脚用于控制其他设备,主要是向其他设备输出一些时钟信号,例如,其中一个分频器输出的信号可以用来控制系统的时钟。

    为了能更好的说明8253芯片的作用,先来看下它的外观图以及内部结构图。

    8253芯片的外观图如下:


图3

    8253芯片的内部结构图如下:
 

图4

    上面的图片来源于:http://en.wikipedia.org/wiki/Intel_8253 ,其中Counter 0,Counter 1,Counter 2代表三个独立的frequency dividers分频器,因为分频器的作用就是将输入引脚(即上图里Clock 0之类的以Clock开头的引脚)传过来的高频率的信号在内部Counter计数器的作用下,在输出引脚(即上图里Out 0之类的以Out开头的引脚)输出所需的低频率的信号,例如,假设输入引脚的输入信号的频率是200Hz(即每秒在输入引脚收到200个脉冲),当分频器内部的Counter计数器的初始值为10时,每次输入端收到一个脉冲,Counter计数器的值就会递减,当减到0时,分频器就会在Out输出引脚产生一个输出脉冲,同时,Counter计数器的值又会被重置为10,然后循环之前的模式继续递减,这样每10个输入脉冲会产生一个输出脉冲,所以200Hz的输入频率,就可以得到200/10即20Hz的输出频率。

    Counter 0对应的分频器,它的Out输出引脚连接的是上面提到过的8259 PIC主芯片的IRQ0引脚,这样当Out引脚输出脉冲时,PIC中断控制器的IRQ0引脚就会收到信号,然后PIC就会向CPU发出中断信号,CPU就会查询并跳转到IRQ0的中断请求处理程序去执行,通过编写IRQ0的中断请求处理程序,就可以设置系统时钟的滴答数,并可以通过系统时钟来控制多任务之间的切换(在以后的版本里会实现多任务)。

    Counter 1对应的分频器在早期的机子里被用于控制DRAM或RAM内存的刷新频率,在后来的机子里,DRAM内存使用专用硬件来控制刷新,所以该分频器就不再被使用了。

    Counter 2对应的分频器用于连接PC的扬声器,有关PC扬声器的编程方式可以参考 http://wiki.osdev.org/PC_Speaker 链接里的文章。

    上面分频器的Clock输入引脚的脉冲信号来源于前面提到的oscillator振荡器,oscillator振荡器提供给8253 PIT芯片的频率大概是1.193182 MHz,至于为什么是1.193182 MHz,就得先回顾下历史(大概是20世纪70年代中后期):

    早期的PC电脑只使用一个单一的"base oscillator(基振荡器)"产生一个14.31818 MHz的频率,该频率在当时被广泛用于电视电路。基振荡器的频率除以3(通过分频)可以得到一个4.77272666 MHz的频率,该频率当时被提供给CPU使用,基振荡器的频率除以4可以得到一个3.579545 MHz的频率用于CGA视频控制器,将得到的这两个频率进行逻辑与,就得到了1.1931816666... MHz(6666...是6的无限循环)的频率,该频率相当于基振荡器频率14.31818 MHz除以12 ,这样就得到了8253 PIT的输入频率了。由于14.31818 MHz的基振荡器比较便宜适合量产,所以使用一个基振荡器来得到其他各种频率的方式比单独使用几个不同频率的振荡器要经济得多。

    到了后来,电子装备的成本越来越便宜,CPU和视频控制器的频率也远远高于早期的工作频率,而PIT的1.193182 MHz(估计值)则遗留了下来。

    所以对PIT进行编程,其实最主要的就是通过I/O端口来设置Counter计数器的初始值或者叫reset重置值,在设置好计数器的值后,分频器就会将输入的1.193182 MHz频率除以计数器的值,来得到一个需要的输出频率,在zlox_time.c文件的zlox_init_timer函数里,就对PIT的Counter 0计数器进行了设置:

/* timer.c Initialises the PIT, and handles clock updates. */

#include "zlox_time.h"
#include "zlox_isr.h"
#include "zlox_monitor.h"

ZLOX_UINT32 tick = 0;

static ZLOX_VOID zlox_timer_callback(ZLOX_ISR_REGISTERS regs)
{
    tick++;
    zlox_monitor_write("zenglOX Tick: ");
    zlox_monitor_write_dec(tick);
    zlox_monitor_write("\n");
}

ZLOX_VOID zlox_init_timer(ZLOX_UINT32 frequency)
{
    ZLOX_UINT32 divisor;
    ZLOX_UINT8 l;
    ZLOX_UINT8 h;
    // Firstly, register our timer callback.
    zlox_register_interrupt_callback(ZLOX_IRQ0,&zlox_timer_callback);

    // The value we send to the PIT is the value to divide it's input clock
    // (1193180 Hz) by, to get our required frequency. Important to note is
    // that the divisor must be small enough to fit into 16-bits.
    divisor = 1193180 / frequency;

    // Send the command byte.
    zlox_outb(0x43, 0x36);

    // Divisor has to be sent byte-wise, so split here into upper/lower bytes.
    l = (ZLOX_UINT8)(divisor & 0xFF);
    h = (ZLOX_UINT8)( (divisor>>8) & 0xFF);

    // Send the frequency divisor.
    zlox_outb(0x40, l);
    zlox_outb(0x40, h);
}


    上面的zlox_init_timer函数里先用zlox_register_interrupt_callback函数将IRQ0的中断请求处理函数注册为zlox_timer_callback,这样当IRQ0收到PIT定时器输出的时钟信号时,就可以调用到zlox_timer_callback函数,在该函数里会将系统时钟的tick滴答数加一,然后通过zlox_monitor_write函数向显示器屏幕输出当前的tick时钟滴答值,这些打印输出只是在该版本里用于调试用的,在后面的版本里会被移除。

    接着,代码通过divisor = 1193180 / frequency;用1193180 Hz(估计值)即输入频率除以frequency自己要设置的时钟频率,就可以得到divisor分频器的计数器值了,不过这里需要注意的是:由于PIT计数器只有16位,所以要确保divisor计数器值在16位的范围内即0到65535,当为0时,很多PIT芯片的实现里会将0当成65536的值来看待。

    得到了计数器值后,就需要将该值通过I/O端口设置到PIT里,下面先来看下PIT编程时需要用到的I/O端口。

    PIT芯片使用了如下几个I/O端口:

I/O 端口地址 Usage 用法
0x40 Channel 0 data port (read/write)
0号分频器的数据端口(可读数据或写数据)
0x41 Channel 1 data port (read/write)
1号分频器的数据端口(可读数据或写数据)
0x42 Channel 2 data port (read/write)
2号分频器的数据端口(可读数据或写数据)
0x43  Mode/Command register (write only, a read is ignored)
命令控制寄存器(只能写数据,读操作会被忽略)

    0x43对应命令控制寄存器,通过该端口可以设置需要对哪个分频器进行操作,以及设置该分频器的工作模式,上面介绍过的计数器递减到0后,又会自动重置为初始值的这种循环计数的定时方式只是最常见的一种工作模式,它还有很多别的工作模式,传给0x43端口的命令字节的各二进制位含义如下:

Bits         Usage
 6 and 7      Select channel :
                 0 0 = Channel 0
                 0 1 = Channel 1
                 1 0 = Channel 2
                 1 1 = Read-back command (8254 only)
 4 and 5      Access mode :
                 0 0 = Latch count value command
                 0 1 = Access mode: lobyte only
                 1 0 = Access mode: hibyte only
                 1 1 = Access mode: lobyte/hibyte
 1 to 3       Operating mode :
                 0 0 0 = Mode 0 (interrupt on terminal count)
                 0 0 1 = Mode 1 (hardware re-triggerable one-shot)
                 0 1 0 = Mode 2 (rate generator)
                 0 1 1 = Mode 3 (square wave generator)
                 1 0 0 = Mode 4 (software triggered strobe)
                 1 0 1 = Mode 5 (hardware triggered strobe)
                 1 1 0 = Mode 2 (rate generator, same as 010b)
                 1 1 1 = Mode 3 (square wave generator, same as 011b)
 0            BCD/Binary mode: 0 = 16-bit binary, 1 = four-digit BCD


    之前的zlox_init_timer函数里通过zlox_outb(0x43, 0x36);向0x43传递了0x36的命令字节,0x36的二进制格式为 0011 0110 (从高位到低位的顺序),其中位0为0,表示要设置的计数器值为16位二进制格式,位1到位3为110(这里是从低位到高位的顺序),说明Operating mode工作模式是Mode 2即最常见的频率发生器,前面提到过,它会将计数器循环递减(减到0后又重置为初始值),从而可以将输入信号分频为一个所需的更低频率的信号进行输出。位4到位5为11,说明Access mode为lobyte/hibyte,即稍候,先输入分频器的数据端口里的字节将被假定为计数器的低字节部分,再次输入数据端口里的字节为计数器的高字节部分,从而可以将16位的计数器值分两次进行设置。最后的位6和位7为00,说明需要设置的分频器为Channel 0即0号分频器,该分频器连接了IRQ0。(其他的Operating mode模式的工作原理,请参考 http://wiki.osdev.org/Programmable_Interval_Timer 链接里的文章)

    在zlox_outb(0x43, 0x36);设置了命令字节后,就可以通过zlox_outb(0x40, l);和zlox_outb(0x40, h);来依次设置计数器值的低字节和高字节。

    在zlox_kernel.c文件的zlox_kernel_main内核主入口函数里,就通过调用上面的zlox_init_timer函数来设置zenglOX系统时钟的频率:

/*zlox_kernel.c Defines the C-code kernel entry point, calls initialisation routines.*/

#include "zlox_monitor.h"
#include "zlox_descriptor_tables.h"
#include "zlox_time.h"

//zenglOX kernel main entry
ZLOX_SINT32 zlox_kernel_main(ZLOX_VOID * mboot_ptr)
{
	// init gdt and idt
	zlox_init_descriptor_tables();	

	// Initialise the screen (by clearing it)
	zlox_monitor_clear();
	 // Write out a sample string
	zlox_monitor_write("hello world!\nwelcome to zenglOX v0.0.4!\n");

	asm volatile("int $0x3");
        asm volatile("int $0x4");
	
	asm volatile("sti");
	zlox_init_timer(50);

	return (ZLOX_SINT32)mboot_ptr;
}


    上面代码里zlox_init_timer的参数为50,表示时钟频率为50Hz(每秒钟产生50次时间中断),相当于每20ms会增加一次系统时钟的tick滴答值。

    在bochs里,由于软件模拟的原因,不能完全按照50Hz来进行输出,所以bochs里每次增加tick的周期会超过20ms,如果在Virtual Box里运行zenglOX.iso则结果会和设置的50Hz很接近,bochs里的截图如下:


图5

    OK,就到这里,休息,休息一下 o(∩_∩)o~~
 
上下篇

下一篇: zenglOX v0.0.5 分页

上一篇: zenglOX v0.0.3 初始化GDT和IDT

相关文章

zenglOX v1.0.0 shell(命令行程式及各种小工具)

zenglOX v0.0.2 VGA输出显示字符串

zenglOX v2.0.0 E1000系列网卡驱动, PCI驱动, PS/2控制器驱动, 以太网,ARP,IP,UDP,DHCP,ICMP协议, dhcp,arp,ipconf,ping,lspci命令行程式

zenglOX v1.3.0 动态链接库, 固定位置的内核栈, double fault(双误异常检测内核栈溢出)

zenglOX v1.2.0 ISO 9660文件系统

zenglOX v3.2.0 USB v1.1, UHCI, USB KeyBoard, USB Mouse