我是靠谱客的博主 善良蜜蜂,这篇文章主要介绍APIC Timer前言APIC Timer的模式使用APIC Timer,现在分享给大家,希望可以做个参考。

前言

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

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

实现

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

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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我是都被祸害习惯了。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
;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语言代码示例

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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 Timer前言APIC Timer的模式使用APIC Timer的全部内容,更多相关APIC内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(75)

评论列表共有 0 条评论

立即
投稿
返回
顶部