Programmable Interval Timer

来自osdev
(重定向自PIT
跳到导航 跳到搜索

可编程间隔定时器PIT-Programmable Interval Timer)芯片(Intel 8253/8254)基本上由一个振荡器(oscillator)、一个预分频器(prescaler)和3个独立的分频器(frequency dividers)组成。 (译者注:Timer一词可以翻译为计时器或定时器,译者以后打算尽量翻译为”定时器“。Timer的核心功能是周期性的发出Tick信号。) 每个分频器都有一个输出,用于允许定时器控制外部电路(例如IRQ 0)。

振荡器(Oscillator)

PIT芯片使用的振荡器运行频率(大约)为1.193182 MHz。 究其原因,我们需要回顾一下历史(回到20世纪70年代的后半段)……

最初PC使用单个 “基础振荡器” 来产生14.31818 MHz的频率,因为该频率当时通常用于电视电路中。 将该基频除以3得到CPU使用的4.77272666 MHz的频率,再除以4得到CGA视频控制器使用的3.579545 MHz的频率。 通过将这些信号逻辑与(AND)在一起,创建了等于基频除以12的频率。 该频率为1.1931816666 MHz (其中6666部分是无限不循环小数)。 当时,这是一种降低成本的绝妙方法,由于14.31818 MHz振荡器因批量生产较为廉价,从中获得其它频率比使用几个振荡器更便宜。 在现代计算机中,电子设备的成本要低得多,CPU和视频的运行频率也要高得多,PIT的存在让人想起了那段“美好的旧日时光”。

分频器(Frequency Dividers)

分频器的基本原理是分割一个频率以获得较慢的频率。 这通常是通过使用计数器(counter)来完成的。 来自输入频率的每个“脉冲”都会导致计数器减小,当计数器达到零时,输出端会产生一个脉冲,计数器会复位。 例如,如果输入信号为200 Hz,并且计数器每次被重置值为10,则输出频率将为200/10,即20Hz。

PIT只有16位用作分频器,可以表示从0到65535的值。 由于频率不存在除以0,许多实现使用0来表示值65536(或在BCD模式下编程时为10000)。

PIT芯片有三个可编程的独立分频器(称为3个独立通道),其中的“复位计数器”的值由软件(操作系统)设置。 软件还指定当计数器在每个单独通道上达到零时要采取的操作。 通过这种方式,每个通道可以在几种“模式(Mode)”中使用 —— 例如,作为分频器(其中计数自动周期重置)或作为“one-shot”定时器(计数不自动重置)。

每个PIT通道也有一个“门极输入(gate input)”引脚,可以用来控制输入信号(1.19 MHz的)是否到达通道。 对于PIT通道0和1,相关的门极输入引脚未连接到任何东西。 PIT通道2门由IO端口0x61地址的位0控制。

PIT定时器精度

PIT定时器的精度取决于所用振荡器的质量,通常精确到每天1.73秒以内。 造成这种不准确的原因有很多,但正因为如此,将时间或频率指定为5或6位数以上没有多大意义。

输出

通道0直接连接到IRQ0,因此最好仅将其用于产生中断的目的。 通道1不可用,甚至可能不存在。 通道2连接到PC扬声器,但可以用于其它目的,而不产生扬声器声音的音调。

Channel 0

PIT通道0的输出连接到PIC芯片,以便生成“IRQ 0”。 通常,在引导期间,BIOS将通道0设置为计数65535或0(转换为65536),输出频率为18.2065 Hz(或每54.9254毫秒一个IRQ)。 通道0可能是最有用的PIT通道,因为它是唯一连接到IRQ的通道。 它可用于以你选择的频率(只要高于18 Hz,达到分频器最大能力)生成无穷连续的“定时器信号”,或在可编程短延迟(小于18分之一秒,原因同前)后生成单次CPU中断(在“one shot”模式下)。

选择下面的一种操作模式时,请记住IRQ0是由通道0输出电压的上升沿产生的,这一点很有用 (即从 “低” 到 “高” 的过渡)。

Channel 1

PIT通道1的输出曾经与DMA控制器的通道0结合使用,用于刷新DRAM(动态随机存取存储器)或RAM。 通常,RAM中的每个位都由一个电容器组成,该电容器具有代表该位状态的微小电荷,但是 (由于电荷泄漏) 这些电容器需要定期 “刷新”,以便它们不会忘记其状态。

在以后的机器上,DRAM刷新由专用硬件完成,不再使用PIT(和DMA控制器)。 在现代计算机上实现PIT功能的被集成到了大规模集成电路中,PIT通道1不再可用,并且可能根本未实现。

Channel 2

PIT通道2的输出连接到PC扬声器,因此输出的频率决定了扬声器产生声音的频率。 这是唯一可以通过软件(通过I/O端口0x61的位0)控制门极输入的通道,也是唯一可以通过软件(通过I/O端口0x61的位5)读取其输出(高电压或低电压)的通道。 有关如何对PC扬声器进行编程的详细信息,请参见 这里

I/O 端口

PIT芯片使用以下I/O端口:

I/O 端口     用途
0x40         Channel 0数据端口(可读/可写)
0x41         Channel 1数据端口(可读/可写)
0x42         Channel 2数据端口(可读/可写)
0x43         Mode/Command register - 模式/命令寄存器(只写,忽略读)

每个8位数据端口是相同的,用于设置计数器的16位”reload value“或读取通道的16位”current count“ (稍后会详细介绍)。 不应混淆PIT通道的”current count“和”reload value“。 通常,当”current count“达到零时,PIT通道的输出会改变,并且”current count“会重新加载”reload value“,但情况并不总是如此。 ”current count“和”reload value“的使用方式以及它们包含的内容取决于PIT通道配置为使用哪种模式。

I/O地址0x43处的模式/命令寄存器(Mode/Command register)包含以下内容:

Bit位         用途
6 和 7      选择通道-Select channe :
                0 0 = Channel 0
                0 1 = Channel 1
                1 0 = Channel 2
                1 1 = Read-back命令(仅限8254)
4 和 5      访问模式-Access mode :
                0 0 = Latch count value command
                0 1 = 访问模式: 仅lobyte
                1 0 = 访问模式:仅限hibyte
                1 1 = 访问模式:lobyte/hibyte
1至3    操作模式 - Operating mode:
                0 0 0 = 模式 0(终端计数中断-interrupt on terminal count)
                0 0 1 = 模式1 (硬件可重新触发单次 - hardware re-triggerable one-shot)
                0 1 0 = 模式2 (速率发生器 - rate generator)
                0 1 1 = 模式3(方波发生器 - square wave generator)
                1 0 0 = 模式4 (软件触发选通 - software triggered strobe)
                1 0 1 = 模式5 (硬件触发选通 - hardware triggered strobe)
                1 1 0 = 模式2(速率发生器,与010b相同)
                1 1 1 = 模式3(方波发生器,与011b相同)
0            BCD/二进制模式: 0 = 16位二进制,1 = 四数字BCD

“Select Channel”位选择正在配置的通道,并且无论其它位或正在执行的操作类型如何,每次写入“模式/命令寄存器”时都必须始终有效。 在旧的8253芯片上不支持 “read back” (两个位设置),但在除PS/2之外的所有AT和以后的计算机上都应该支持 “read back” (即任何不过时的东西都将支持它)。 “read back”命令将在后面讨论。

“Access Mode”位告诉PIT你希望对所选通道使用的访问模式,并向CTC指定[#Counter Latch Command|Counter Latch Command]]。 每次写入“模式/命令寄存器”时,这些位必须有效。 对于 “read back” 命令,这些位都有不同的含义。 对于其余的组合,这些位指定将读取和写入相关PIT通道数据端口的顺序数据。 因为数据端口(data port)是8位I/O端口,而涉及的值都是16位,所以PIT芯片需要知道每次读取或写入data port需要的高还是低字节。 对于 “lobyte only”,仅向/从data port读取或写入计数器值的最低8位。 对于“hibyte only”,仅读取或写入计数器值的最高8位。 对于“LOBYTE/Hibyte”模式,16位总是成对传输,最低的8位紧随其后的是最高的8位(这两个8位传输都是到相同IO端口的,顺序地 - 单word传输将不起作用)。

“Operating Mode” 位指定所选PIT通道应以哪种模式操作。 对于“read back”命令和“counter latch”命令,这些位具有不同的含义(参见下面与这些命令对应的信息)。 有6种不同的操作模式。 每种操作模式将在稍后单独讨论。

“BCD/Binary”位确定PIT通道是以二进制模式还是以BCD模式(其中计数器的每4位代表一个十进制数字,计数器保存0000到9999之间的值)运行。80x86个人电脑只使用二进制模式(BCD模式实现较为丑陋,且限制了可能的计数/频率范围)。 虽然应该仍然可以使用BCD模式,但在一些“兼容”的芯片上可能无法正常工作。 对于 “read back” 命令和 “counter latch” 命令,此位具有不同的含义 (请参阅下面与这些命令相对应的信息)。

操作模式

虽然每种操作模式的行为不同,但有些事情对所有操作模式都是通用的。这包括:

Initial Output State-初始输出状态
每次写入“模式/命令寄存器”时,所选PIT通道中的所有内部逻辑都会重置,并且输出立即进入其初始状态 (取决于模式)。
Changing Reload Value - 正在更改重新加载值
可以随时将新的“reload value”写入PIT通道的数据端口。 操作模式决定了这将产生的确切效果。
Current Counter - 当前值计数器
Current Counter总是递减或重置为输入信号(1.193182兆赫)的下降沿上的“reload value”。
Current Counter Reload - 当前计数器重新加载
在current count在重新加载时减少的模式中,current count不会在与重新加载相同的输入时钟脉冲上减少 - 它在下一次输入时钟脉冲上开始递减。

模式0 - 终端计数中断模式(Interrupt On Terminal Count)

对于该模式,当写入模式/命令寄存器时,输出信号变低,PIT等待软件设置重新加载寄存器,开始倒计时。 重新加载寄存器设置后,current count将设置为输入信号(1.193182兆赫)下一个下降沿的reload value。 输入信号的后续下降沿将减小current count (如果栅极输入在输入信号的前一个上升沿为高)。

current count从1减至0时,输出变高并保持高,直到写入另一个模式/命令寄存器或再次设置重新加载寄存器。 current count将绕回到0xFFFF(或在BCD模式下为0x9999),并继续递减,直到设置“模式/命令寄存器”或“重新加载寄存器”,但这不会影响输出引脚状态。

可以随时更改“reload value”。 在“lobyte/hibyte”访问模式下,当“reload value”的第一个字节被设置时,计数将停止。 一旦完全设置了“reload value”(在任何访问模式下),(1.193182兆赫)输入信号的下一个下降沿将导致新的“reload value”被复制到current count中,并且倒计时将从新值开始继续。

注意: 尽管此模式的名称具有误导性,但它仅在通道0上生成中断。

模式1 – 硬件可重新触发一次触发模式(Hardware Re-triggerable One-shot)

该模式类似于上面的模式0,但是在检测到门极输入的上升沿之前,计数不会开始。 因此,它不适用于PIT通道0或1(其中栅极输入不能更改)。

写入模式/命令寄存器时,输出信号变高,PIT等待软件设置重新加载寄存器。 重新加载寄存器设置后,PIT将等待门极输入的下一个上升沿。 一旦发生这种情况,输出信号将变得低电平,并且current count将被设置为 (1.193182 MHz) 输入信号的下一个下降沿的reload value。 随后输入信号的下降沿将减少current count。

current count从1减至0时,输出变为高电平,并保持高电平,直到写入另一个”模式/命令寄存器“或重新设置”重新加载寄存器“。 current count将绕回到0xFFFF (或在BCD模式下为0 x9999),并继续递减,直到”模式/命令寄存器“或”重载寄存器“被设置,但是这不会影响输出引脚状态。

如果在此过程中门极输入信号变低,则不会产生任何影响。 但是,如果门极输入再次变为高电平,将导致current count在输入信号的下一个下降沿从重新加载寄存器重新加载,并再次重新启动计数(与第一次开始计数时相同)。

reload value可以随时更改,但是新值不会影响current count,直到重新加载current count (在门极输入的下一个上升沿)。 因此,如果要执行此操作,请在修改reload value后,清除并重置IO端口0x61的位0。

模式2 - 速率发生器模式(Rate Generator)

此模式用作分频器。

写入模式/命令寄存器时,输出信号变高,PIT等待软件设置重新加载寄存器。 重新加载寄存器设置后,current count将设置为输入信号(1.193182兆赫)下一个下降沿的reload value。 输入信号的后续下降沿将减小current count (如果栅极输入在输入信号的前一个上升沿为高)。

当current count从2减至1时,输出变低,在(1.193182 MHz)输入信号的下一个下降沿,它将再次变高,current count将设置为reload value,计数将继续。

如果门极输入变为低电平,则计数停止,输出立即变为高电平。 一旦栅极输入返回高,输入信号上的下一个下降沿将导致current count设置为reload value,并且操作将继续。

reload value可以随时更改,但是新值不会影响current count,直到重新加载current count(当current count从两个减少到一个,或者门极输入先低后高)。 发生这种情况时,计数将继续使用新的reload value。

此模式必须 使用“reload value” (或除数)。

该模式产生的高输出信号在一个输入信号周期(0.8381 uS)内降低,速度太快,无法对PC扬声器产生影响(参见模式3)。 因此,模式2对于利用PIT通道2产生声音是无用的。

通常,操作系统和BIOS对PIT通道0使用模式3 (参见下文) 来生成IRQ 0定时器tick,但有些则使用模式2来获得频率精度 (频率 = 1193182 / reload_value Hz)。

模式3 – 方波发生器模式(Square Wave Generator)

对于模式3,PIT通道像模式2一样作为分频器运行,但是输出信号被馈送到内部“触发器”以产生方波(而不是短脉冲)。 触发器每次其输入状态 (或PIT通道分频器的输出) 改变时都会改变其输出状态。 这会导致实际输出改变状态的频率降低一半,因此为了补偿这一点,在输入信号的每个下降沿上,current count减少两次(而不是一次),并且current count设置为reload value的频率提高两倍。

写入“模式/命令寄存器”时,输出信号变为高电平,PIT等待软件设置重新加载寄存器。 在重新加载寄存器被设置之后,current count将被设置为 (1.193182 MHz) 输入信号的下一个下降沿上的reload value。 输入信号的后续下降沿将使current count减少两次(如果输入信号的前一上升沿上的栅极输入为高)。

注: 在正常情况下,50% 写入模式/命令寄存器的时间,输出状态将是低的。 然后输出将变高,这将立即产生(可能是虚假的)IRQ0。 另外50%的时间,输出将已经很高,并且不会生成IRQ0。

对于偶数reload value,当current count从2递减到0时,触发器的输出改变状态; current count将重置为reload value,计数将继续。

对于奇数reload value,current count始终设置为比reload value小1。 如果当current count从2递减到0时触发器的输出为低,则其行为将与等效的偶数reload value相同。 然而,如果触发器的输出为高,则重新加载将延迟一个输入信号周期(0.8381 uS),这将导致“高”脉冲稍长,占空比将不精确为50%。 由于reload value无论如何都会向下舍入为最接近的偶数,因此建议仅使用偶数reload value(这意味着你应该在将值发送到端口之前掩蔽该值)。

注意: 对模式3中的reload value的此偶数值限制将可能的输出频率的数量减少了一半。 如果你希望能够将IRQ0的频率控制到更高的程度,那么可以考虑使用模式2代替通道0。

在通道2上,如果栅极输入变为低电平,则计数停止,输出立即变为高电平。 一旦门极输入返回高,输入信号上的下一个下降沿将导致current count设置为reload value,并且操作将继续 (输出保持高)。

可随时更改reload value,但新值不会影响current count,直到重新加载current count(当其从2降至0,或门极输入先低后高)。 发生这种情况时,计数将继续使用新的reload value。

此模式必须 “不” 使用一个的reload value (或除数)。

模式4 – 软件触发选通模式(Software Triggered Strobe)

模式4用作可重新触发的延迟,并在current count达到零时产生脉冲。

当写入模式/命令寄存器时,输出信号变高,PIT等待通过软件设置重载寄存器。 设置重新加载寄存器后,current count将设置为(1.193182 MHz)输入信号下一个下降沿的reload value。 输入信号的后续下降沿将递减current count(如果门极输入在输入信号的前一上升沿为高)。

当current count从1减至0时,输出在输入信号的一个周期内变低 (0.8381 uS)。 current count将环绕至0xFFFF(或BCD模式下的0x9999),并继续递减,直到设置模式/命令寄存器或重新加载寄存器,但这不会影响输出状态。

如果门极输入变为低电平,计数将停止,但输出不会受到影响,并且current count不会重置为reload value。

可以随时更改reload value。 当设置了新值(lobyte/hibyte访问模式的两个字节)时,它将被加载到(1.193182 MHz)输入信号下一个下降沿的current count中,计数将继续使用新的reload value。

模式5 - 硬件触发选通模式(Hardware Triggered Strobe)

模式5与模式4类似,不同之处在于它等待门极输入的上升沿触发 (或重新触发) 延迟周期 (类似于模式1)。 因此,它不适用于PIT通道0或1(其中门极输入无法更改)。

写入模式/命令寄存器时,输出信号变为高电平,PIT等待软件设置重新加载寄存器。 重新加载寄存器设置后,PIT将等待门极输入的下一个上升沿。 一旦发生这种情况,current count将设置为(1.193182 MHz)输入信号下一个下降沿的reload value。 输入信号的后续下降沿将递减current count。

当current count从1减至0时,输出在输入信号的一个周期内变低 (0.8381 uS)。 current count将环绕至0xFFFF(或BCD模式下的0x9999),并继续递减,直到设置模式/命令寄存器或重新加载寄存器,但这不会影响输出状态。

如果门极输入信号在此过程中变为低电平,则不会产生任何影响。 但是,如果门极输入再次变高,将导致current count在输入信号的下一个下降沿从重新加载寄存器重新加载,并再次重新启动计数 (与首次开始计数时相同)。

reload value可以随时更改,但新值不会影响current count,直到重新加载current count(在栅极输入的下一个上升沿)。 发生这种情况时,计数将继续使用新的reload value。

Counter Latch Command

为了防止current count被更新,可以使用latch命令 “闩锁” PIT通道。 为此,将值CC000000(二进制)发送到模式/命令寄存器(I/O端口0x43),其中“CC”对应于通道号。 发送锁存命令后,current count被复制到内部“锁存寄存器”中,然后可通过与所选通道对应的数据端口(I/O端口0x40至0x42)读取该寄存器。 保持在锁存寄存器中的值保持不变,直到它被完全读取,或者直到写入新的模式/命令寄存器。

latch命令的主要优点是,它允许读取current count的两个字节,而不会出现不一致的情况。 例如,如果你没有使用LATCH命令,则在读取低位字节之后、读取高位字节之前,current count可能会从0x0200减少到0x01FF,因此软件会认为计数器是0x0100,而不是0x0200(或0x01FF)。

虽然锁存器命令不应该影响current count,但是在发送锁存器命令的一些 (旧的/不可靠的) 主板上,可能会导致输入信号的周期偶尔被错过,这将导致current count比它应该的时间晚0.8381ms被递减。 如果你正在发送锁存命令,这常常会导致精度问题(但是如果你需要发送锁存命令,那么你可能希望考虑重新设计代码)。

Read Back Command

read back命令是发送到模式/命令寄存器(I/O端口0x43)的特殊命令。 旧的8253芯片不支持 “read back”,但除PS/2外,所有AT和更高版本的计算机都应支持 “read back” (即任何不过时的东西都会支持它)。

对于read back命令,模式/命令寄存器使用以下格式:

Bit位         用途
7 and 6      read back命令时必须设置1
5            Latch count flag (0 = latch count, 1 = don't latch count)
4            Latch status flag (0 = latch status, 1 = don't latch status)
3            Read back timer channel 2 (1 = yes, 0 = no)
2            Read back timer channel 1 (1 = yes, 0 = no)
1            Read back timer channel 0 (1 = yes, 0 = no)
0           保留 (应清除0)

注:请小心第4位和第5位-它们是否表达是颠倒的。

read back命令的位1到3选择哪些PIT通道受到影响,并允许同时选择多个通道。

如果位5清除,则使用位1至3选择的任何/所有PIT通道将其current count复制到锁存寄存器中 (类似于发送LATCH命令,不同之处在于它使用一个命令可用于多个通道)。

如果位4是清除的,则对于以位1至3选择的任何/所有PIT通道,对应数据端口的下一次读取将返回一个状态字节 (如下所述)。

Read Back Status 字节

发送位4清零的回读命令后,读取每个选定通道的数据端口将返回以下格式的状态值:

Bit位        用途
7            Output pin state
6            Null count flags
4 and 5      Access mode :
                0 0 = Latch count value command
                0 1 = Access mode: lobyte only
                1 0 = Access mode: hibyte only
                1 1 = Access mode: lobyte/hibyte
1 to 3       Operating mode :
                0 0 0 = Mode 0 (interrupt on terminal count)
                0 0 1 = Mode 1 (hardware re-triggerable one-shot)
                0 1 0 = Mode 2 (rate generator)
                0 1 1 = Mode 3 (square wave generator)
                1 0 0 = Mode 4 (software triggered strobe)
                1 0 1 = Mode 5 (hardware triggered strobe)
                1 1 0 = Mode 2 (rate generator, same as 010b)
                1 1 1 = Mode 3 (square wave generator, same as 011b)
0            BCD/Binary mode: 0 = 16-bit binary, 1 = four-digit BCD

底部六位返回上次初始化通道时编程到模式/命令寄存器中的值。

位7表示发出回读命令时PIT通道输出引脚的状态。

位6表示在将新编程的重载入值加载到current count(如果设置)之前,新编程的除数值是否已加载到current count中(如果清除),或者通道是否仍在等待触发信号或current count向下计数到零。 在初始化模式/命令寄存器或写入新的reload value时设置此位,并在将reload value复制到current count时清除。

读取 Current Count

要使用“仅限lobyte”或“仅限hibyte”访问模式读取current count,只需执行“in al,0x40”(用于PIT通道0)即可。 对于高于4.7 kHz的频率,最容易的方法是将reload value的高字节设置为0,然后使用“仅限大堂字节”访问模式以最大限度地减少麻烦。

对于 “lobyte/hibyte” 访问模式,你需要发送latch命令 (如上所述),以避免获得错误的结果。 如果任何其它代码可以在发送闩锁命令后但在读取最高8位之前尝试设置PIT通道的reload value或读取其current count,则必须阻止它。 禁用中断对单CPU计算机有效。 例如,要读取PIT通道0的计数,你可以使用以下内容:

unsigned read_pit_count(void) {
	unsigned count = 0;
	
	// 禁用中断
	cli();
	
	// al = 第6位和第7位中的通道,其余位清零
	outb(0x43,0b0000000);
	
	count = inb(0x40);		// Low byte
	count |= inb(0x40)<<8;		// High byte
	
	return count;
}

设置 Reload Value

要设置reload value,只需将值发送到相应的数据端口。 对于“lobyte only”或“hibyte only”访问模式,这仅使用单条“out 0x40,al”(用于PIT通道0)。

对于 “lobyte/hibyte” 访问模式,你需要发送低8位,然后发送高8位。 发送最低8位后,必须防止其它代码设置PIT通道的reload value或读取其current count。 禁用中断对单CPU计算机有效。 例如:

void set_pit_count(unsigned count) {
	// Disable interrupts
	cli();
	
	// Set low byte
	outb(0x40,count&0xFF);		// Low byte
	outb(0x40,(count&0xFF00)>>8);	// High byte
	return;
}

需要注意的是,reload value为零可以用来指定65536的除数。 这就是BIOS获得低至18.2065 Hz的IRQ0频率的原因。

PIT Channel 0 示例代码

下面的示例代码是为NASM编写的,但尚未经过测试。

其想法是提供单个例程来初始化任意(可能)频率的PIT通道0,并使用IRQ 0精确跟踪自PIT配置以来的实时(毫秒)。

为了准确起见,初始化代码将计算要添加到每个IRQ的 “系统定时器tick” 的整毫秒数,以及避免漂移的 “毫秒的分数” 。 这可能很重要,例如,如果PIT设置为700 Hz,IRQ之间的计算结果(大约)为1.42857 ms,因此只跟踪整个毫秒将导致巨大的不准确。

希望大家都熟悉定点数学。 例如,我将使用 “32.32” 表示法,如果高32位值等于0x00000001并且低32位值等于0x80000000,则组合值将是1.5。 同样,分数0.75用0xC000000表示,0.125用0x20000000表示,0.12345用0x1F9A6B50表示。

首先,下面的代码包含此示例使用的所有数据。 假定 “.bss” 部分用零填充。

section .bss
system_timer_fractions:  resd 1          ; 自定时器初始化以来1毫秒的分数
system_timer_ms:         resd 1          ; 自定时器初始化以来的整毫秒数
IRQ0_fractions:          resd 1          ; IRQ之间1ms的分数
IRQ0_ms:                 resd 1          ; IRQ之间的整数毫秒数
IRQ0_frequency:          resd 1          ; PIT的实际频率
PIT_reload_value:        resw 1          ; 当前PIT重新装载值
section .text

接下来是IRQ 0的处理程序。 它相当简单(它所做的全部工作就是将264位定点值相加,并向PIC芯片发送EOI)。

IRQ0_handler:
	push eax
	push ebx
 
	mov eax, [IRQ0_fractions]
	mov ebx, [IRQ0_ms]                    ; eax.ebx = IRQs之间的时间量
	add [system_timer_fractions], eax     ; 更新系统定时器刻度分数
	adc [system_timer_ms], ebx            ;更新系统定时器tick毫秒
 
	mov al, 0x20
	out 0x20, al                          ; 将EOI发送到PIC

	pop ebx
	pop eax
	iretd

现在是棘手的一点——初始化例程。 PIT不能产生一些频率。 例如,如果你想要8000Hz,那么你可以选择8007.93Hz或7954.544Hz。 在这种情况下,以下代码将找到最接近的可能频率。 一旦它计算了最接近的可能频率,它将反转计算以找到所选的实际频率(四舍五入为最接近的整数,仅用于显示目的)。

为了获得一些额外的精度,我还使用 “3579545/3” 而不是1193182Hz。 由于硬件不准确,这基本上是没有意义的(我只是喜欢正确)。

 ; 输入
 ; ebx   期望的PIT频率,单位为Hz
 
 init_PIT:
    pushad
 
    ; 检查一下
 
    mov eax,0x10000                   ;EAX=最慢频率的reload value(65536)
    cmp ebx,18                        ;请求的频率是否太低?
    jbe .gotReloadValue               ; 是,使用尽可能低的频率
 
    mov eax,1                         ; ax = 最快频率的reload value (1)
    cmp ebx,1193181                   ;请求的频率是否过高?
    jae .gotReloadValue               ; 是,使用尽可能快的频率
 
    ; Calculate the reload value
 
    mov eax,3579545
    mov edx,0                         ;edx:eax = 3579545
    div ebx                           ;eax = 3579545 / frequency, edx = remainder
    cmp edx,3579545 / 2               ; 余数的是否超过一半?
    jb .l1                            ; 否,向下舍入
    inc eax                           ; 是的,向上舍入
 .l1:
    mov ebx,3
    mov edx,0                         ;edx:eax = 3579545 * 256 / frequency
    div ebx                           ;eax = (3579545 * 256 / 3 * 256) / frequency
    cmp edx,3 / 2                     ;余数的是否超过一半?
    jb .l2                            ; 否,向下舍入
    inc eax                           ; 是,向上舍入
 .l2:
 
 
 ; 存储reload value并计算实际频率
 
 .gotReloadValue:
    push eax                          ;存储reload_值以备以后使用
    mov [PIT_reload_value],ax         ;存储reload value以备以后使用
    mov ebx,eax                       ;ebx = reload value
 
    mov eax,3579545
    mov edx,0                         ;edx:eax = 3579545
    div ebx                           ;eax = 3579545 / reload_value, edx = remainder
    cmp edx,3579545 / 2               ;余数的是否超过一半?
    jb .l3                            ; 否,向下舍入
    inc eax                           ; 是,向上舍入
 .l3:
    mov ebx,3
    mov edx,0                         ;edx:eax = 3579545 / reload_value
    div ebx                           ;eax = (3579545 / 3) / frequency
    cmp edx,3 / 2                     ; 余数的是否超过一半?
    jb .l4                            ; 否,向下舍入
    inc eax                           ; 是,向上舍入
 .l4:
    mov [IRQ0_frequency],eax          ;存储实际频率,以便以后显示
 
 
 ; 计算32.32定点IRQ之间的时间量
 ;
 ; 注: 基本公式为:
 ;           time in ms = reload_value / (3579545 / 3) * 1000
 ;       可以通过以下方式重新排列:
 ;           time in ms = reload_value * 3000 / 3579545
 ;           time in ms = reload_value * 3000 / 3579545 * (2^42)/(2^42)
 ;           time in ms = reload_value * 3000 * (2^42) / 3579545 / (2^42)
 ;           time in ms * 2^32 = reload_value * 3000 * (2^42) / 3579545 / (2^42) * (2^32)
 ;           time in ms * 2^32 = reload_value * 3000 * (2^42) / 3579545 / (2^10)
 
    pop ebx                           ;ebx = reload_value
    mov eax,0xDBB3A062                ;eax = 3000 * (2^42) / 3579545
    mul ebx                           ;edx:eax = reload_value * 3000 * (2^42) / 3579545
    shrd eax,edx,10
    shr edx,10                        ;edx:eax = reload_value * 3000 * (2^42) / 3579545 / (2^10)
 
    mov [IRQ0_ms],edx                 ;IRMS之间的整组
    mov [IRQ0_fractions],eax          ;设置IRQ之间1毫秒的分数
 
 
 ; 对PIT通道进行编程
 
    pushfd
    cli                               ;禁用的中断(以防万一)
 
    mov al,00110100b                  ;channel 0, lobyte/hibyte, rate generator
    out 0x43, al
 
    mov ax,[PIT_reload_value]         ;ax = 16 bit reload value
    out 0x40,al                       ;设置PIT reload value的低位字节
    mov al,ah                         ;ax = high 8 bits of reload value
    out 0x40,al                       ; 设置PIT reload value的高字节
 
    popfd
 
    popad
    ret

注意:你还需要为IRQ 0安装IDT entry-中断描述符表条目,并在PIC芯片(或I/O APIC)中取消屏蔽它。

当然,将PIT配置为固定值会更容易,但是这样不是更乐趣吗?:-)

使用定时器IRQ

使用IRQ实现 休眠(sleep)

PIT每 “n” 毫秒生成一个硬件中断,使你可以创建一个简单的定时器。 从包含延迟的全局变量x开始:

 section .data
 CountDown: dd 0

接下来,每次调用定时器中断时,递减此变量,直到存储0。

 section .text
 global TimerIRQ
 TimerIRQ:
     push eax
     mov eax, [CountDown]
     or eax, or eax ; 与0进行比较的快速方法
     jz TimerDone
     mov eax, [CountDown]
     dec eax
     mov [CountDown], eax
 TimerDone:
     pop eax
     iretd

最后,创建一个等待时间间隔的函数 sleep,以毫秒为单位。

 [GLOBAL sleep]
 sleep:
     push ebp
     mov ebp, esp
     push eax
     mov eax, [ebp + 8] ; eax具有唯一参数的值
     mov [CountDown], eax
 SleepLoop:
     cli ; 测试中不可被中断
     mov eax, [CountDown]
     or eax, eax
     jz SleepDone
     sti
     nop ; nop几次这样中断就可以处理了
     nop
     nop
     nop
     nop
     nop
     jmp SleepLoop
 SleepDone:
     sti
     pop eax
     pop ebp
     ret

在多任务系统中,请考虑使用这些倒计时变量的链表或数组。 如果你的多任务系统支持进程间通信,你还可以将信号量/交换存储在两个进程可以通话的地方,让中断在定时器完成后向等待进程发送消息, 让等待进程阻止所有执行,直到消息出现:

#define COUNTDOWN_DONE_MSG 1
struct TimerBlock {
    EXCHANGE e;
    uint32_t CountDown;
} timerblocks[20];

void TimerIRQ(void) /* called from Assembly */
{
    uint8_t i;
    
    for (i = 0; i < 20; i++)
        if (timerblocks[i].CountDown > 0) {
            timerblocks[i].CountDown--;
            if (timerblocks[i].CountDown == 0)
                SendMessage(timerblocks[i].e, COUNTDOWN_DONE_MESSAGE);
        }
}

void Sleep(uint32_t delay)
{
    struct TimerBlock *t;

    if ((t = findTimerBlock()) == nil)
        return;
    t->CountDown = delay;
    WaitForMessageFrom(t->e = getCrntExch());
}

在文档中,请注意定时器的间隔。 例如,如果定时器间隔为每tick 10毫秒,则程序员可以编写

Sleep(100);

休眠一秒钟。

使用IRQ进行抢占式多任务

定时器IRQ也可用于执行抢先多任务处理。 要给当前正在运行的任务一些运行时间,请设置一个阈值,例如3次tick周期。 使用与前面类似的全局变量,但从0开始递增,当该变量达到3时,切换任务。 你怎么做取决于你自己。

另见

文章

论坛主题

外部链接

de:Programmable_Interval_Timer