HPET
- 本页不是对HPET的完整描述,只是一个轻量级的介绍。 如果你需要本文未涵盖的任何信息,请参考HPET规范。
HPET或高精度事件定时器(High Precision Event Timer),是英特尔和微软设计的一款硬件,用于取代较旧的 PIT 和 RTC。 它由(通常为64位)主计数器(递增计数)以及3到32位或64位宽的比较器组成。 HPET使用内存映射IO进行编程,使用ACPI可以找到HPET的基地址。
使用ACPI检测HPET
那个HPET规范定义了一个ACPI 2.0表,用于检测系统中存在的HPET的存在、地址和功能。 如果该表不存在,你应该假设没有HPET,并且应该回退到PIT或APIC Timer。
struct address_structure
{
uint8_t address_space_id; // 0 - system memory, 1 - system I/O
uint8_t register_bit_width;
uint8_t register_bit_offset;
uint8_t reserved;
uint64_t address;
} __attribute__((packed));
struct description_table_header
{
char signature[4]; // 'HPET' in case of HPET table
uint32_t length;
uint8_t revision;
uint8_t checksum;
char oemid[6];
uint64_t oem_tableid;
uint32_t oem_revision;
uint32_t creator_id;
uint32_t creator_revision;
} __attribute__((packed));
struct hpet : public description_table_header
{
uint8_t hardware_rev_id;
uint8_t comparator_count:5;
uint8_t counter_size:1;
uint8_t reserved:1;
uint8_t legacy_replacement:1;
uint16_t pci_vendor_id;
address_structure address;
uint8_t hpet_number;
uint16_t minimum_tick;
uint8_t page_protection;
} __attribute__((packed));
HPET-定时器(timer)vs比较器(comparators)
定时器中只有一个递增计数主计数器,但中断生成由*比较器*处理。 有3到32个比较器,确切的数量由上面hpet
结构中的compasator_count
字段表示。 请记住,你必须初始化主计数器和所有比较器。 此外,比较器中断的路由以及允许的路由是独立的,因此你必须为每个中断单独检测和设置它。 正文中进一步提供了有关此过程的更多信息。
HPET操作模式
HPET提供两种操作模式: 单次(one-shot,规范中也称为“非周期”)和周期模式。
One-shot模式
在非周期模式下,操作系统用主计数器的值对定时器的一个比较器寄存器进行编程,以触发中断。 如果定时器设置为32位模式,则计数器回绕(wraps around)时也会产生中断。 比较器寄存器的值永远不会被硬件写入,你可以随时对其进行写入和读取,因此你可以更改主计数器中将生成中断的值。
HPET中的每个比较器都必须支持非周期模式。
周期模式
周期模式比非周期模式更棘手。 对于周期模式,类似于一次触发模式,写入一个值,在该值处,将向比较器寄存器生成中断。 但是,当产生中断时,硬件将将比较器寄存器中的值增加最后一个写入它的值! 这是HPET的主要计数器向上计数的结果。
因此,如果我们设置定时器时,主计数器的值是12345,并且我们将12456写入比较器(即,从现在起,中断将触发111个时间单位),当中断触发时,12456将“添加”到比较器寄存器,因此它将变为24912,即从第一个中断开始的12456个时间单位。 有两种技术可以处理此问题;这两种技术都将在本文的后面部分介绍。
不需要 比较器来支持此模式; 初始化比较器时,你必须检测到此功能。 本文将进一步提供有关这方面的更多信息。
中断例程(Interrupt routing)
HPET支持三种中断映射选项: “遗留替换(legacy replacement)” 选项、标准选项和FSB选项。
“旧版替换”映射
在此映射中,HPET的定时器(比较器)#0替换PIT中断,而定时器#1替换RTC的中断(换句话说,PIC和RTC将不再导致中断)。 虽然HPET规范提供了下表描述了此映射中定时器 #0和 #1的路由,但建议至少检查ACPI表中IRQ0和IRQ8到I/O APIC的路由例程(routing)。
定时器 | PIC映射 | IOAPIC映射 |
---|---|---|
0 | IRQ0 | IRQ2 |
1 | IRQ8 | IRQ8 |
N | 根据IRQ路由字段 | 根据IRQ路由字段 |
标准映射
在标准映射中,每个定时器都有自己的中断路由控制。 通过读取定时器的能力寄存器可以找到允许的I/O APIC输入。
FSB映射
这种映射几乎与PCI的消息信号中断相同。 定义如何配置FSB中断的“定时器N FSB中断路由寄存器”可在规范中找到。 使用定时器配置寄存器中的 “Tn_FSB_EN_CNF” 字段启用FSB中断。 本文将不再进一步讨论这种映射模式。
HPET寄存器
下表和字段描述也可以在规范中找到。 “偏移量”指与hpet
struct的address
字段中定义的地址的偏移量。 下表跳过规范中定义的保留寄存器。
偏移 | 寄存器 | 类型 |
---|---|---|
0x000 - 0x007 | General Capabilities and ID Register | Read only |
0x010 - 0x017 | General Configuration Register | Read/write |
0x020 - 0x027 | General Interrupt Status Register | Read/write clear |
0x0F0 - 0x0F7 | Main Counter Value Register | Read/write |
(0x100 + 0x20 * N) - (0x107 + 0x20 * N) | Timer N Configuration and Capability Register | Read/write |
(0x108 + 0x20 * N) - (0x10F + 0x20 * N) | Timer N Comparator Value Register | Read/write |
(0x110 + 0x20 * N) - (0x117 + 0x20 * N) | Timer N FSB Interrupt Route Register | Read/write |
General Capabilities and ID Register
位 | 名称 | 描述 |
---|---|---|
63:32 | COUNTER_CLK_PERIOD | 主计数器计时周期,单位为飞秒(10^-15s)。 不得为零,必须小于或等于0x05F5E100或100纳秒。 |
31-16 | 供应商ID | 此字段的解释应与PCI的供应商ID类似。 |
15 | LEG_RT_CAP | 如果此位为1,则HPET能够使用 “遗留替换” 映射。 |
14 | 保留 | - |
13 | COUNT_SIZE_CAP | 该位为1时,HPET主计数器可以在64位模式下运行。 |
12:8 | NUM_TIM_CAP | 定时器的数量为-1。 |
7:0 | REV_ID
表示实现的是哪个版本的功能,不能为0 |
General Configuration Register
位 | 名称 | 描述 |
---|---|---|
63:2 | 保留 | - |
1 | LEG_RT_CNF | 0-“旧版替换”映射已禁用
1-启用“传统替换”映射 |
0 | ENABLE_CNF | 整体(Overall)启用。
0-主计数器停止,定时器中断被禁用 1-主计数器正在运行,如果启用,则允许定时器中断 |
General Interrupt Status Register
位 | 名称 | 描述 |
---|---|---|
63:32 | 保留 | - |
n | Tn_INT_STS | 该功能取决于定时器#n使用的是边缘触发模式还是电平触发模式。
对于电平触发:,默认值为0。 当相应的定时器中断处于活动状态时,将设置此位为1。 如果设置了1,软件可以通过向该位写入1来清除它。 写入0不起作用。 对于边缘触发: 应忽略此位。 它始终设置为0。 |
Main Counter Value Register
该寄存器的位63:0称为MAIN_COUNTER_VAL。 仅当计数器暂停 (ENABLE_CNF = 0) 时,才应执行对该寄存器的写入操作。 Reads将返回主计数器的当前值。32位计数器对于较高的32位总是返回0。 如果在64位计数器上执行32位读取,请参考规范中的2.4.7,了解如何安全执行该操作的说明。 建议在仅32位软件上使用32位计数器。
Timer N Configuration and Capability Register
位 | 名称 | 描述 |
---|---|---|
63:32 | Tn_INT_ROUTE_CAP | 定时器n中断路由功能。 如果在该字段中设置了位X,则意味着该定时器可以映射到I/O APIC的IRQX线。 |
31:16 | Reserved | - |
15 | Tn_FSB_INT_DEL_CAP | 如果此只读位为1,则此定时器支持FSB中断映射。 |
14 | Tn_FSB_EN_CNF | 如果该位设置为1,该定时器将使用FSB中断映射。 |
13:9 | Tn_INT_ROUTE_CNF | 表示I/O APIC路由。可以使用TN_INT_ROUTE_CAP确定允许值。如果写入非法值,则从此字段读回的值将与写入的值不匹配。 |
8 | Tn_32MODE_CNF | 对于64位定时器,如果设置了此字段,则定时器将被强制以32位模式工作。否则没有效果。 |
7 | Reserved | - |
6 | Tn_VAL_SET_CNF | 此字段用于允许软件直接设置周期定时器的累加器。详细解释将在本文中进一步提供。 |
5 | Tn_SIZE_CAP | 如果该只读位设置为1,则定时器的大小为64位。否则,它是32位的。 |
4 | Tn_PER_INT_CAP | 如果此只读位设置为1,则此定时器支持周期性模式。 |
3 | Tn_TYPE_CNF | 如果Tn_PER_INT_CAP为1,则向该字段写入1将启用周期定时器,写入0将启用非周期模式。否则,该位将被忽略,读取它将始终返回0。 |
2 | Tn_INT_ENB_CNF | 将此位设置为1可启用中断触发。即使此位为0,此定时器仍将设置TN_INT_STS。 |
1 | Tn_INT_TYPE_CNF | 0 - this timer generates edge-triggered interrupts.
1-此定时器生成电平触发的中断。当产生中断时,设置Tn_INT_STS。如果在清除该位之前发生另一个中断,则该中断将保持活动状态。 |
0 | Reserved | - |
Timer N Comparator Value Register
位63:0(或31:0,如果定时器在32位模式下工作)用于与主计数器进行比较,以检查是否应生成中断。
初始化
以下是初始化主计数器和比较器以接收中断所需执行的过程。
一般初始化:
1. 在‘HPET’ACPI表中查找HPET基地址。
2. 计算HPET频率 (f = 10 ^ 15/周期)。
3.保存最小刻度(来自ACPI表或配置寄存器)。
4. 初始化比较器。
5. 设置ENABLE_CNF位
定时器N初始化:
1.确定定时器N是否具有周期性功能,保存该信息以避免每次重新读取。
2.确定当前定时器的允许中断路由,并为其分配一个中断。
我只在实际使用定时器时才启用它们,所以这里没有比较器的“真正”初始化。
请记住,允许的中断例程 可能跑飞。 也就是说,你可能还需要使用一些ISA中断-或者至少能够在某一点上明确使用它们。 上次我检查VirtualBox允许的HPET映射时,它允许将每个定时器路由到系统上存在的32个I/O APIC输入中的“任意”。 知道硬件有多有漏洞,我不会太惊讶,如果有一台带有HPET的PC声称允许输入#31,而实际上只有24个I/O APIC输入。 为定时器选择中断路由时请注意这一点。
使用定时器
单次(One-shot)模式
要启用一次性模式:
// "time" is time in femtoseconds from now to interrupt
if (time < COUNTER_CLK_PERIOD)
{
time = adjust_time(time);
}
write_register_64(timer_configuration(n), (ioapic_input << 9) | (1 << 2));
write_register_64(timer_comparator(n), read_register(main_counter) + time);
我希望上面的代码是显而易见的。 如果不是,请分析上面使用的寄存器中特定字段的含义。
周期模式
要启用周期模式,请执行以下操作:
// "time" is time in femtoseconds from now to interrupt
if (time < COUNTER_CLK_PERIOD)
{
time = adjust_time(time);
}
write_register_64(timer_configuration(n), (ioapic_input << 9) | (1 << 2) | (1 << 3) | (1 << 6));
write_register_64(timer_comparator(n), read_register(main_counter) + time);
write_register_64(timer_comparator(n), time);
这段代码需要更多注释。
位2同上,中断使能。 第3位也很简单——1表示周期定时器。 但我们也设置了第6位。 为什么?
让我们看看HPET规范的引用内容:
Timer n Value Set: [...] Software uses this read/write bit only for timers that have been set to periodic mode. By writing this bit to a 1, the software is then allowed to directly set a periodic timer’s accumulator.
软件不必将此位写回0(自动清0)。 如果定时器设置为非周期性模式,则软件不应在此位位置写入1。
这意味着下一次写入定时器N比较器寄存器将具有通常的含义,而“第二次”下一次写入将直接写入累加器。 我举得这里的措辞本可以更好。