[OSDEV]编程高精度定时器(HPET)

时间:2020-12-02 08:51:47

高精度定时器HPET和I/O APIC一样,用的是内存映射,映射的地址保存在BIOS提供的ACPI表格中

我们首先来获取这个地址

获取HPET的I/O内存地址

先来看一下文档的30-31页:

[OSDEV]编程高精度定时器(HPET)

[OSDEV]编程高精度定时器(HPET)

关键就是那个表格,我们先把他写成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中

运行一下看看,如图:

[OSDEV]编程高精度定时器(HPET)

HPET寄存器

我们先来看一下寄存器的总体概况,先不详细讲解,等到以后用哪个的时候再说哪个

[OSDEV]编程高精度定时器(HPET)

获取HPET的周期

在设置之前我们当然要获取一下HPET计数器的周期,他在第一个寄存器上,名曰General Capability and ID Register,注意是Read-Only只读的

[OSDEV]编程高精度定时器(HPET)

高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计数器

唔.....说了这么久计数器的周期,但连计数器长什么样还不知道呢,赶紧来认识一下吧

[OSDEV]编程高精度定时器(HPET)

一眼看上去,这是所以寄存器中最简单的了吧,不但作用简单,结构也简单.....

我们先来试着读取一下低32位,返回0,为什么呢?

对了,我们还没有启动HPET呢,计数器当然是0了,先来启动HPET吧

启动HPET

启动HPET就要用到配置寄存器了,如下

[OSDEV]编程高精度定时器(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);
再来看一下输出

[OSDEV]编程高精度定时器(HPET)

对了,最上面的两个数0不是一样多的,可以数数....

这样的话我们就可以做安全检查了,如下

   if(!counter)
   {  
      printk("HPET didn't count,discard.\n");
      return -1;
   }

如果HPET不计数的话我们就放弃HPET,采用最原始的PIT.....

开启HPET中断

HPET的中断参见下表

[OSDEV]编程高精度定时器(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的配置寄存器

[OSDEV]编程高精度定时器(HPET)

这个偏移地址不是固定的,比如我们要用第一个定时器偏移就是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还要知道什么时间触发中断

[OSDEV]编程高精度定时器(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