[置顶] APIC Timer

前言

(由于之前的blog已经关闭了,所以将此文章迁移至这里,并非转载)

之前已经大体的写过APIC的一些内容,这次是写一些APIC定时器的内容,当然,也是翻译了一些来自OSDev的资料(不要问我为什么不翻译Intel手册,其实都一样,Intel手册里面写的太长了,有时候不一定要把所有东西看完才能使用里面的内容)。如果喜欢看原文,可以点APIC timer – OSDev Wiki.链接。有任何疑问,可以e-mail联系,[email protected]。(其实msn也行)

使用APIC Timer的最大好处是每个cpu内核都有一个定时器(避免了定时器资源的抢占问题,你要是抢走了一个cpu核心,你就肯定抢走了一个定时器),这里的cpu内核是指和核心数,把超线程技术的那个内核也算进去了。相反的我们之前使用的PIT(Programmable Interval Timer)就不这样,PIT是共用的一个。因为这样,我们不需要考虑任何资源管理问题,我们能够很轻松的使用它(老外说话有时候很..)。但是APIC有一个比较麻烦的是,他的精度和CPU的频率有关(这里的频率指的是CPU的外频,现在主流的就是200MHz,但是有点飘,这个我在最后解释)。而PIT使用的是一个标准的频率(1193182Hz)。如果要使用APICTimer,那么你必须自己知道每秒钟能触发多少个中断。

APIC Timer的模式

APIC定时器一般包含2-3种定时器模式,前两种(周期触发periodic和一次性触发one-shot)一般被现在所有的Local APIC所支持。但是第三种(TSC-Deadline模式)目前只在最新的CPU里面支持(话说我手上拿的CPU貌似都支持)。

周期触发模式(Periodic Mode)

周期触发模式中,程序设置一个”初始计数“寄存器(Initial Count),同时Local APIC会将这个数复制到”当前计数“寄存器(Current Count)。Local APIC会将这个数(当前计数)递减,直到减到0为止,这时候将触发一个IRQ(可以理解为触发一次中断),与此同时将当前计数恢复到初始计数寄存器的值,然后周而复始的执行上述逻辑。可以使用这种方法通过Local APIC实现定时按照一定时间间隔触发中断的功能。”当前计数“寄存器中的值的递减速度依赖于当前CPU的主频除以一个叫做”分频寄存器“(“Divide Configuration Register”)的值(换个思路来说就是,多少个tick减少1)。

举例来说,对于一个2.4GHz的CPU使用的CPU总线频率是800MHz(大家说的外频800MHz),”分频寄存器“(“Divide Configuration Register”)设置的是“除四”(”divide by 4“),并且初始计数(initial count)设置到123456。那么结果就是当前计数(current count)每200M分之1秒减1,大约617.28us触发一个irq(一次中断),也就是1620.01Hz。

一次性触发模式(One-Shot Mode)

对于一次性触发模式,Local APIC中的“当前计数”寄存器的减少方式和周期触发模式一样,也是当“当前计数“寄存器的值到0的时候触发一次定时器IRQ(中断)。但是它不会自动恢复到初始计数。这样,软件必须每次都要写“初始计数”寄存器一个值来让其再一次的计时并触发IRQ。这种模式的有点事,软件可以精确地控制定时器的IRQ的发生时间。例如,系统可以根据进程的优先级来设置任务切换(一些任务使用较短的CPU时间,一些任务使用较长的CPU时间),并且不会产生任何不必要的IRQs(这句话我也不太清楚什么意思,不过大约就是可以精确地控制切换进程的时间到时IRQ产生,因为进程切换也耽误时间)。一些系统通过为计数器付值的方法去实现通用的高精度计时器服务。换个例子来说就是,有3个任务需要分别在1234ns、333ns、4444ns的时候去做,这样的话,就设定定时器显示333ns,到时中断发生时执行任务二,同时设定定时器901ns,到时中断发生时执行任务一,同时在设定定时器441111ns,定时后执行任务三(原文的英语的例子我是不理解为什么要写那么折腾了,我就简单的按上面举例了)。

缺点是,一次性触发可能会影响实时性(因为在设置的时候会耽误一些,导致的不确定性),并且还需要为了避免竞争,维护新的计数值和老的计数值。

TSC-Deadline Mode(不是我不翻译,我是真不会翻译)

TSC-Deadline模式同上述两种模式有很大的区别。触发方式发生了区别,上述两种方式是基于当前计数减少到0触发的,而TSC-Deadline模式是软件设置了一个”deadline“,然后当CPU的时间戳大于或等于”deadline“的时候触发一次IRQ。

尽管存在如上的差异,软件可能会将它用于替代一次性触发模式,相比于一次性触发模式,这样可以得到更高的精度(因为时间戳的生成时CPU的时钟,而不是CPU总线频率),并且它更容易处理资源竞争(最后这句话我真不太理解)。

使用APIC Timer

使能APIC Timer

首先,应该通过写MSR寄存器使能Local APIC硬件。

其次,配置一个用于定时器的中断,并且软件开启APIC。

最后,配置APIC Timer的中断向量以及操作模式。

具体操作方法,参考Inter开发手册Vol3A Chapter 9。

初始化步骤

这里使用的方式是使用CPU总线频率作为基准源, 有很多种方法能够完成这部分所讲的内容,并且每种方法都不一样。例如:Real Time Clock,TimeStamp Counter,PIT or even polling CMOS registers。这里要讲的内容仍然要用到PIT,因为他很简单。按照如下步骤执行:

  1. 重置APIC到一个已知的状态
  2. 使能APIC Timer
  3. 重置APIC Timer的计数器
  4. 设置另外的一个已知时钟源
  5. 获取APIC TImer的计数器的当前值
  6. 以秒为基准校准一下
  7. 设置APIC Timer计数器的分频
  8. 设置接受APIC Timer中断触发

APIC Timer可以被设置为经过若干个tick以后触发一次中断,这里设置的地方叫做”分频数“(divide value)。这意味着可以通过将这一数值乘以APIC Timer的计数来获得当前CPU总线的频率。这里的分频数可以设置最小值是1最大值是128,具体的请参考Intel编程手册Vol3A Chapter9.5.4。(注,网上有人说在Bochs上设置分频数为1不好用,于是他设置了16)。

实现

在开始前,我们先来定义一系列常量和函数。

apic        = the linear address where you have mapped the APIC registers

APIC_APICID = 20h
APIC_APICVER    = 30h
APIC_TASKPRIOR  = 80h
APIC_EOI    = 0B0h
APIC_LDR    = 0D0h
APIC_DFR    = 0E0h
APIC_SPURIOUS   = 0F0h
APIC_ESR    = 280h
APIC_ICRL   = 300h
APIC_ICRH   = 310h
APIC_LVT_TMR    = 320h
APIC_LVT_PERF   = 340h
APIC_LVT_LINT0  = 350h
APIC_LVT_LINT1  = 360h
APIC_LVT_ERR    = 370h
APIC_TMRINITCNT = 380h
APIC_TMRCURRCNT = 390h
APIC_TMRDIV = 3E0h
APIC_LAST   = 38Fh
APIC_DISABLE    = 10000h
APIC_SW_ENABLE  = 100h
APIC_CPUFOCUS   = 200h
APIC_NMI    = (4<<8)
TMR_PERIODIC    = 20000h
TMR_BASEDIV = (1<<20)

        ;Interrupt Service Routines
isr_dummytmr:   mov         dword [apic+APIC_EOI], 0
        iret
isr_spurious:   iret
        ;function to set a specific interrupt gate in IDT
        ;al=interrupt
        ;ebx=isr entry point
writegate:  ...
        ret

同样的也需要配置一些IDT项,并且需要设置一个用于处理中断的中断门和处理函数。这里是:

interrupt 32:timer, IRQ0
interrupt 39 : spurious irq, IRQ7

如果有需要,那就直接改代码就好了。

ASM代码示例

提供一种asm的代码示例,大家看看就好了,汇编都差不多也就两种语法,intel还有at&a我是都被祸害习惯了。

        ;you should read MSR, get APIC base and map to "apic"
        ;you should have used lidt properly

        ;set up isrs
        mov         al, 32
        mov         ebx, isr_dummytmr
        call            writegate
        mov         al, 39
        mov         ebx, isr_spurious
        call            writegate

        ;initialize LAPIC to a well known state
        mov         dword [apic+APIC_DFR], 0FFFFFFFFh
        mov         eax, dword [apic+APIC_LDR]
        and         eax, 00FFFFFFh
        or          al, 1
        mov         dword [apic+APIC_LDR], eax
        mov         dword [apic+APIC_LVT_TMR], APIC_DISABLE
        mov         dword [apic+APIC_LVT_PERF], APIC_NMI
        mov         dword [apic+APIC_LVT_LINT0], APIC_DISABLE
        mov         dword [apic+APIC_LVT_LINT1], APIC_DISABLE
        mov         dword [apic+APIC_TASKPRIOR], 0
        ;okay, now we can enable APIC
        ;global enable
        mov         ecx, 1bh
        rdmsr
        bts         eax, 11
        wrmsr
        ;software enable, map spurious interrupt to dummy isr
        mov         dword [apic+APIC_SPURIOUS], 39+APIC_SW_ENABLE
        ;map APIC timer to an interrupt, and by that enable it in one-shot mode
        mov         dword [apic+APIC_LVT_TMR], 32
        ;set up divide value to 16
        mov         dword [apic+APIC_TMRDIV], 03h

        ;ebx=0xFFFFFFFF;
        xor         ebx, ebx
        dec         ebx

        ;initialize PIT Ch 2 in one-shot mode
        ;waiting 1 sec could slow down boot time considerably,
        ;so we'll wait 1/100 sec, and multiply the counted ticks
        mov         dx, 61h
        in          al, dx
        and         al, 0fdh
        or          al, 1
        out         dx, al
        mov         al, 10110010b
        out         43h, al
        ;1193180/100 Hz = 11931 = 2e9bh
        mov         al, 9bh     ;LSB
        out         42h, al
        in          al, 60h     ;short delay
        mov         al, 2eh     ;MSB
        out         42h, al
        ;reset PIT one-shot counter (start counting)
        in          al, dx
        and         al, 0feh
        out         dx, al      ;gate low
        or          al, 1
        out         dx, al      ;gate high
        ;reset APIC timer (set counter to -1)
        mov         dword [apic+APIC_TMRINITCNT], ebx
        ;now wait until PIT counter reaches zero
@@:     in          al, dx
        and         al, 20h
        jz          @b
        ;stop APIC timer
        mov         dword [apic+APIC_LVT_TMR], APIC_DISABLE
        ;now do the math...
        xor         eax, eax
        xor         ebx, ebx
        dec         eax
        ;get current counter value
        mov         ebx, dword [apic+APIC_TMRCURRCNT]
        ;it is counted down from -1, make it positive
        sub         eax, ebx
        inc         eax
        ;we used divide value different than 1, so now we have to multiply the result by 16
        shl         eax, 4      ;*16
        xor         edx, edx
        ;moreover, PIT did not wait a whole sec, only a fraction, so multiply by that too
        mov         ebx, 100    ;*PITHz
        mul         ebx
    ;-----edx:eax now holds the CPU bus frequency-----
        ;now calculate timer counter value of your choice
        ;this means that tasks will be preempted 1000 times in a second. 100 is popular too.
        mov         ebx, 1000
        xor         edx, edx
        div         ebx
        ;again, we did not use divide value of 1
        shr         eax, 4      ;/16
        ;sanity check, min 16
        cmp         eax, 010h
        jae         @f
        mov         eax, 010h
        ;now eax holds appropriate number of ticks, use it as APIC timer counter initializer
@@:     mov         dword [apic+APIC_TMRINITCNT], eax
        ;finally re-enable timer in periodic mode
        mov         dword [apic+APIC_LVT_TMR], 32 or TMR_PERIODIC
        ;setting divide value register again not needed by the manuals
        ;although I have found buggy hardware that required it
        mov         dword [apic+APIC_TMRDIV], 03h

C语言代码示例

void apic_timer_init(uint32 quantum){
    uint32 tmp, cpubusfreq;

    //set up isrs
    writegate(32,isr_dummytmr);
    writegate(39,isr_spurious);

    //initialize LAPIC to a well known state
    (uint32*)(apic+APIC_DFR)=0xFFFFFFFF;
    (uint32*)(apic+APIC_LDR)=((uint32*)(apic+APIC_LDR)&0x00FFFFFF)|1);
    (uint32*)(apic+APIC_LVT_TMR)=APIC_DISABLE;
    (uint32*)(apic+APIC_LVT_PERF)=APIC_NMI;
    (uint32*)(apic+APIC_LVT_LINT0)=APIC_DISABLE;
    (uint32*)(apic+APIC_LVT_LINT1)=APIC_DISABLE;
    (uint32*)(apic+APIC_TASKPRIOR)=0;

    //okay, now we can enable APIC
    //global enable
    cpuSetAPICBase(cpuGetAPICBase());
    //software enable, map spurious interrupt to dummy isr
    (uint32*)(apic+APIC_SPURIOUS)=39|APIV_SW_ENABLE;
    //map APIC timer to an interrupt, and by that enable it in one-shot mode
    (uint32*)(apic+APIC_LVT_TMR)=32;
    //set up divide value to 16
    (uint32*)(apic+APIC_TMRDIV)=0x03;

    //initialize PIT Ch 2 in one-shot mode
    //waiting 1 sec could slow down boot time considerably,
    //so we'll wait 1/100 sec, and multiply the counted ticks
    outb(0x61,inb(0x61)&0xFD)|1);
    outb(0x43,0xB2);
    //1193180/100 Hz = 11931 = 2e9bh
    outb(0x42,0x9B);    //LSB
    in(0x60);   //short delay
    outb(0x42,0x2E);    //MSB

    //reset PIT one-shot counter (start counting)
    (uint8)tmp=inb(0x61)&0xFE;
    outb(0x61,(uint8)tmp);      //gate low
    outb(0x61,(uint8)tmp|1);        //gate high
    //reset APIC timer (set counter to -1)
    (uint32*)(apic+APIC_TMRINITCNT)=0xFFFFFFFF;

    //now wait until PIT counter reaches zero
    while(!(inb(0x61)&0x20));

    //stop APIC timer
    (uint32*)(apic+APIC_LVT_TMR)=APIC_DISABLE;

    //now do the math...
    cpubusfreq=((0xFFFFFFFF-(uint32*)(apic+APIC_TMRINITCNT))+1)*16*100;
    tmp=cpubusfreq/quantum/16;

    //sanity check, now tmp holds appropriate number of ticks, use it as APIC timer counter initializer
    (uint32*)(apic+APIC_TMRINITCNT)=(tmp<16?16:tmp);
    //finally re-enable timer in periodic mode
    (uint32*)(apic+APIC_LVT_TMR)=32|TMR_PERIODIC;
    //setting divide value register again not needed by the manuals
    //although I have found buggy hardware that required it
    (uint32*)(apic+APIC_TMRDIV)=0x03;
}

其他已知问题

在最开始的时候我说过CPU总线频率不固定的这件事情,主要是在实测过程中会发生一些偏差,应该不算是漂移。大约在199.90~200.10MHz之间飘。这点同硬件的哥们研究过,后来得出的结果是一个叫做”自动跳频”技术造成的,好像可以关了这个就好了,具体我也没试验。只是希望别给大家带来不必要的麻烦。

你可能感兴趣的:(定时器,嵌入式,英特尔,APIC,APIC-Timer)