高精度定时器HPET和I/O APIC一样,用的是内存映射,映射的地址保存在BIOS提供的ACPI表格中
我们首先来获取这个地址
获取HPET的I/O内存地址
先来看一下文档的30-31页:
关键就是那个表格,我们先把他写成C语言的形式
(注意:部分内容在上一篇中已经提过,不再重复了,参见http://blog.csdn.net/goodqt/article/details/15337067)
typedef struct ACPIHeaderHpet{ ACPIHeader header; u32 eventTimerBlockID; ACPIAddressFormat baseAddress; u8 hpetNumber; u16 minTickInPeriodicMode; u8 attribute; } __attribute__ ((packed)) ACPIHeaderHpet;
typedef struct ACPIAddressFormat{ u8 addressSpaceID; u8 registerBitWidth; u8 registerBitOffset; u8 reserved; u64 address; } __attribute__ ((packed)) ACPIAddressFormat;对照上面的表格看吧
有了这个,下一步就很好写了
static int parseDT(ACPIHeader *dt) { u32 signature = dt->signature; char signatureString[5]; memcpy((void *)signatureString,(const void *)&signature,4); signatureString[4] = '\0'; printk("Found device %s from ACPI.\n",signatureString); if(signature == *(u32 *)"APIC") parseApic((ACPIHeaderApic *)dt); else if(signature = *(u32 *)"HPET") parseHpet((ACPIHeaderHpet *)dt); return 0; }找到HPET的表格
static int parseHpet(ACPIHeaderHpet *hpet) { char temp[20]; temp[0] = '0'; temp[1] = 'x'; pointer address = (pointer)hpet->baseAddress.address; hpetAddress = (u8 *)pa2va(address); itoa(address,temp + 2,0x10,16,'0',1); printk("\nFound HPET:Address => %s.\n",temp); return 0; }这样我们就获得了HPET的地址,并将其储存在了hpetAddress中
运行一下看看,如图:
HPET寄存器
我们先来看一下寄存器的总体概况,先不详细讲解,等到以后用哪个的时候再说哪个
获取HPET的周期
在设置之前我们当然要获取一下HPET计数器的周期,他在第一个寄存器上,名曰General Capability and ID Register,注意是Read-Only只读的
高32位就是HPET计数器的周期.它以呼秒(femptosecond,1呼秒=10^(-15)秒)为单位指示多长时间HPET计数器自增一次(最大是10^8呼秒,约合100纳秒)
int initHpet(void) { hpetAddress = getHpetAddress(); if(!hpetAddress) return -1; u32 period = hpetIn(HPET_PERIOD_REG); /*#define HPET_PERIOD_REG 0x4*/ printk("HPET Period:%d\n",period); return 0; }
QEMU上打印的是10000000呼秒,约合10纳秒,也就是每隔10纳秒HPET计数器就会自增一次
再次强调一下,这个是Read-Only只读的,不能写数据来改变HPET计数器的周期
现在我们获得了HPET计数器的周期,只不过这是用的呼秒,如果能够换算成每秒计数器走多少可能会比较易懂(毕竟呼秒这个单位我们太不常用了,顶多也就说到毫秒)
现在就来试试看:
/*......*/ u32 period = hpetIn(HPET_PERIOD_REG); unsigned long long hpetFreq = FSEC_PER_SEC; hpetFreq = hpetFreq / period; printk("HPET Period:%d,",period); { char temp[20]; itoa(hpetFreq,temp,10,0,0,1); printk("Freq: %s\n",temp); } /*.......*/打印出来的是每秒增加10^8,果然是高精度.....
HPET计数器
唔.....说了这么久计数器的周期,但连计数器长什么样还不知道呢,赶紧来认识一下吧
一眼看上去,这是所以寄存器中最简单的了吧,不但作用简单,结构也简单.....
我们先来试着读取一下低32位,返回0,为什么呢?
对了,我们还没有启动HPET呢,计数器当然是0了,先来启动HPET吧
启动HPET
启动HPET就要用到配置寄存器了,如下
(PS:目测Intel粗心了,Offset居然写着0,查一下我们的第一个表格知道这个寄存器的偏移明明是0x10.....)
我们来解释一下这个寄存器.第一位上是控制计数器的开关,第二位是控制是否产生中断的开关,我们现在主要用第一位
写个enableHpet函数,顺便把disbaleHpet和restartHpet的函数也写了(这样我们就调用restartHpet好了,防止某些"好心"的BIOS自动帮你打开了HPET)
static inline int enableHpet(void) { u32 conf = hpetIn(HPET_CONF_REG); conf |= HPET_CONF_ENABLE; hpetOut(HPET_CONF_REG,conf); return 0; } static inline int disableHpet(void) { u32 conf = hpetIn(HPET_CONF_REG); conf &= ~HPET_CONF_ENABLE; hpetOut(HPET_CONF_REG,conf); return 0; } static inline int restartHpet(void) { disableHpet(); hpetOut(HPET_COUNTER_REG_LOW,0x0); hpetOut(HPET_COUNTER_REG_HIGH,0x0); enableHpet(); return 0; }终于可以改写我们的函数来打印出走动的counter的数值了,不过还有一个问题,由于计数器有64位,我们必须分两次来读,万一在第一次读完后更新了怎么办,我们举个例子
假如现在第三十二位是0xffffffff,高32位是0x00000000,我们将0xffffffff读了出来,正在这时,计数器更新了,产生进位,下面我就不用再说了吧.....
所以我们还是写一个hpetReadCounter(事实上我们应该再写一个hpetWriteCounter,偷懒先不写了)
static inline u64 hpetReadCounter(void) { u64 counter; disableHpet(); counter = hpetIn(HPET_COUNTER_REG_LOW); counter |= ((u64)hpetIn(HPET_COUNTER_REG_HIGH)) << 32; enableHpet(); return counter; }
打印出新的Counter
printk("Restart HPET and clear counter....\n"); restartHpet(); for(volatile int i = 0;i < 100000;++i) ; u64 counter = hpetReadCounter(); itoa(counter,temp,0x10,16,'0',1); printk("We waited a minute,now counter is 0x%s\n",temp);再来看一下输出
对了,最上面的两个数0不是一样多的,可以数数....
这样的话我们就可以做安全检查了,如下
if(!counter) { printk("HPET didn't count,discard.\n"); return -1; }
如果HPET不计数的话我们就放弃HPET,采用最原始的PIT.....
开启HPET中断
HPET的中断参见下表
Timer0触发的IRQ号分8259A和(I/O)APIC.....
我使用的是APIC,为了使效果更明显,首先关闭其他所有中断,只打开IRQ2,并将这个IRQ的处理函数打印一行字"Interrupt!"就返回
现在运行一下,不出意外的话HPET的中断还没启用,你应该看到PIT触发的中断一行行打印......(PIT在APIC下也触发IRQ2)
然后我们让HPET的中断启动
static inline int enableHpetInterrupt(void) { u32 conf = hpetIn(HPET_CONF_REG); conf |= HPET_CONF_ENABLE_INT; hpetOut(HPET_CONF_REG,conf); return 0; }然后主函数中调用一下,再次运行,不出意外你应该发现什么也不输出了,因为我们启动了HPET的中断但没有配置,HPET不会发出任何中断,在这种情况下PIT会被自动禁用,也不会发出任何中断
配置HPET的Timer
这部分比较复杂,有许多许多的标记,如果我们先把每一个标记都弄懂的话大家肯定都没有耐心了,我们不妨参照Linux的源代码,只说几个用到的标记
先看Timer的配置寄存器
这个偏移地址不是固定的,比如我们要用第一个定时器偏移就是0x100 + (1 - 1)*0x20=0x100,第二个则是0x100 + (2 - 1)*0x20=0x120,以此类推
这么多标记我们只说几个就能使我们的代码运行的很好
1.TN_INT_EN_INT 顾名思义,设置这个定时器是否启用
2.TN_TYPE_CNF 决定这个计时器是周期的还是一次性的,另外下一个位TN_PER_INT_CAP(是Read-Only)表示此为是否可用
其他位有兴趣的可以自己查文档解决
但是只有这些信息还不够,HPET还要知道什么时间触发中断
这个寄存器就简单多了,只有一个信息,对于周期性的定时器,这个寄存器决定多长时间触发一次中断,对于一次性的定时器,这个寄存器决定什么时候触发中断
好了,现在我们来编写hpetStartTimer
static inline int hpetStartTimer(int index,int periodic,u32 time) { u32 conf = hpetIn(HPET_TIMER_CONF_REG(index)); if(periodic) { if(conf & HPET_TIMER_CONF_PER_CAP) /*判断这个Timer是否支持周期性.*/ conf |= HPET_TIMER_CONF_PERIODIC; else return -1; } conf |= HPET_TIMER_CONF_ENABLE; hpetOut(HPET_TIMER_CONF_REG(index),conf); hpetOut(HPET_TIMER_CMP_REG(index),time); return 0; }然后我们调用这个函数
if(hpetStartTimer(0,1,0x2ffffff)) { disableHpet(); return -1; }
这里我们选择了如果不支持周期性就直接退出,其实没有必要这样做,我们只要在每次中断执行时修改寄存器,使其延迟指定的时间再次发生中断,就可以模拟周期性
另外实际使用时还要通过我们一开始得到的freq计算这里的参数u32 time,这部分内容就不再涉及了....
现在再次运行,会发现以不大的速度不断打印"Interrupt!",通过调节参数time,就可以轻松的改变其打印速度
试了下,QEMU环境下每秒最多产生大约100个中断,VirtualBox或者其他比较好的模拟器、真实的机器可以产生更多,1000个也没有问题的....
参考资料
IA-PC HPET (High Precision Event Timers) Specification