Programmable Interval Timer
可编程间隔定时器(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时,切换任务。 你怎么做取决于你自己。
另见
文章
论坛主题
外部链接
- Programmable Interval Timer on Wikipedia
- The PIT: A System Clock on osdever