高精度定时器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计数器的周期,他在第一个寄存器上,名曰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,果然是高精度.....
唔.....说了这么久计数器的周期,但连计数器长什么样还不知道呢,赶紧来认识一下吧
一眼看上去,这是所以寄存器中最简单的了吧,不但作用简单,结构也简单.....
我们先来试着读取一下低32位,返回0,为什么呢?
对了,我们还没有启动HPET呢,计数器当然是0了,先来启动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;
}
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的中断参见下表
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会被自动禁用,也不会发出任何中断
这部分比较复杂,有许多许多的标记,如果我们先把每一个标记都弄懂的话大家肯定都没有耐心了,我们不妨参照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个也没有问题的....
HPET规范:http://www.intel.com/content/dam/www/public/us/en/documents/technical-specifications/software-developers-hpet-spec-1-0a.pdf
IA-PC HPET (High Precision Event Timers) Specification