Assembly x86 (16bit):更准确的时间测量
Assembly x86 (16bit): More accurate time measurement
我正在使用 DOSBox 在 TASM 16 位中编程,这是今天的问题:
使用 DOS INT 21h/2Ch 我可以获得系统当前的百分之一秒。
这很好……直到它不是。
看,我正在寻找以毫秒为单位的至少半精确的时间测量,我确信这是可能的。
为什么,你问?看看 INT 15h/86h。
使用此中断,我可以将程序延迟 微秒 。
如果存在这样的精度,我相信获得毫秒将是在公园散步。
我的一些想法:使用每 1/1024 秒发生一次的 INT 70h,但我不知道如何监听中断,也不想要一个不能监听的计时系统除以 10.
这个问题现在已经打动了我,我在网上找不到已经存在的解决方案。
提前干杯
非常感谢 Peter Cordes 在评论中的回答,我现在 post 回答任何其他计划使用 30 年前的老式编译器的人。
粗略地说,您在 16 位 TASM 中可以获得的最佳时钟仍然不够准确。
幸运的是,在 TASM 中,您可以使用 .386
指令(如 here 所述)“解锁”32 位模式。
然后,您可以使用RDTSC
命令(读取时间戳计数器),但是有一个问题。它在TASM 中不存在。
它不存在的事实对我们没有任何意义,因为 TASM 中的所有命令(通常称为助记符)只是 OpCode 的替代品,它定义了 CPU 可以 运行 的每条指令。 =16=]
当 Intel Pentium CPU 发布时,包含了 RDTSC 的 OpCode,所以如果你有一个 CPU 从它开始......你很好。
现在,如果 TASM 中不存在 RDTSC 指令,我们如何 运行? (但在我们的 CPU 中)
在 TASM 中,有一条指令叫做 db
,通过它我们可以直接 运行 一个 OpCode。
正如所见 here,我们需要对 运行 RDTSC 做的是:db 0Fh, 31h
.
就是这样!您现在可以 运行 轻松执行此指令,并且您的程序仍将一团糟,但 timed 混乱!
在 16 位 PC 兼容 x86 系统中,PIT(可编程间隔定时器)使用 1.19318MHz 的时钟输入来递减 16 位计数器。每当计数器在 216 = 65536 增量后返回时,就会产生中断。 BIOS 提供的 ISR(中断服务例程)处理它然后递增软件计数器,频率为 1.19318MHz / 65536 ~= 18.2 Hz。
在DOS和其他实模式操作系统下,16位PIT计数器可以直接从相关端口以两个8位块读取,这个数据可以与软件维护的滴答计数器结合起来达到毫秒分辨率。基本上,一个人最终会使用一个 48 位滴答计数器,其中由 BIOS 维护的 32 位软件计数器构成最高有效位,而 16 位 PIT 计数器构成最低有效位。
由于数据并非一口气全部读出,因此存在竞争条件的风险,必须妥善处理。此外,一些 BIOS 用于将 PIT 编程为方波发生器而不是简单的速率计数器。虽然这不会干扰增加软件滴答的任务,但它会干扰 PIT 计数器寄存器与软件滴答的直接组合。这需要对 PIT 进行 一次性 初始化,以确保它在速率计数模式下运行。
下面是 16 位汇编代码,封装为 Turbo Pascal 单元,多年来我一直使用它来实现毫秒级精度的稳健计时。这里从滴答计数到毫秒的转换有点像黑盒子。我丢失了它的设计文档,现在无法即时快速重建它。我记得这个定点计算的抖动足够小,可以可靠地测量毫秒。 Turbo-Pascal 的调用约定要求在 DX:AX
寄存器对中返回一个 32 位整数结果。
UNIT Time; { Copyright (c) 1989-1993 Norbert Juffa }
INTERFACE
FUNCTION Clock: LONGINT; { same as VMS; time in milliseconds }
IMPLEMENTATION
FUNCTION Clock: LONGINT; ASSEMBLER;
ASM
PUSH DS { save caller's data segment }
MOV DS, Seg0040 { access ticker counter }
MOV BX, 6Ch { offset of ticker counter in segm.}
MOV DX, 43h { timer chip control port }
MOV AL, 4 { freeze timer 0 }
PUSHF { save caller's int flag setting }
CLI { make reading counter an atomic operation}
MOV DI, DS:[BX] { read BIOS ticker counter }
MOV CX, DS:[BX+2]
STI { enable update of ticker counter }
OUT DX, AL { latch timer 0 }
CLI { make reading counter an atomic operation}
MOV SI, DS:[BX] { read BIOS ticker counter }
MOV BX, DS:[BX+2]
IN AL, 40h { read latched timer 0 lo-byte }
MOV AH, AL { save lo-byte }
IN AL, 40h { read latched timer 0 hi-byte }
POPF { restore caller's int flag }
XCHG AL, AH { correct order of hi and lo }
CMP DI, SI { ticker counter updated ? }
JE @no_update { no }
OR AX, AX { update before timer freeze ? }
JNS @no_update { no }
MOV DI, SI { use second }
MOV CX, BX { ticker counter }
@no_update: NOT AX { counter counts down }
MOV BX, 36EDh { load multiplier }
MUL BX { W1 * M }
MOV SI, DX { save W1 * M (hi) }
MOV AX, BX { get M }
MUL DI { W2 * M }
XCHG BX, AX { AX = M, BX = W2 * M (lo) }
MOV DI, DX { DI = W2 * M (hi) }
ADD BX, SI { accumulate }
ADC DI, 0 { result }
XOR SI, SI { load zero }
MUL CX { W3 * M }
ADD AX, DI { accumulate }
ADC DX, SI { result in DX:AX:BX }
MOV DH, DL { move result }
MOV DL, AH { from DL:AX:BX }
MOV AH, AL { to }
MOV AL, BH { DX:AX:BH }
MOV DI, DX { save result }
MOV CX, AX { in DI:CX }
MOV AX, 25110 { calculate correction }
MUL DX { factor }
SUB CX, DX { subtract correction }
SBB DI, SI { factor }
XCHG AX, CX { result back }
MOV DX, DI { to DX:AX }
POP DS { restore caller's data segment }
END;
BEGIN
Port [] := ; { need rate generator, not square wave }
Port [] := 0; { generator as programmed by some BIOSes }
Port [] := 0; { for timer 0 }
END. { Time }
我正在使用 DOSBox 在 TASM 16 位中编程,这是今天的问题: 使用 DOS INT 21h/2Ch 我可以获得系统当前的百分之一秒。 这很好……直到它不是。
看,我正在寻找以毫秒为单位的至少半精确的时间测量,我确信这是可能的。
为什么,你问?看看 INT 15h/86h。 使用此中断,我可以将程序延迟 微秒 。 如果存在这样的精度,我相信获得毫秒将是在公园散步。
我的一些想法:使用每 1/1024 秒发生一次的 INT 70h,但我不知道如何监听中断,也不想要一个不能监听的计时系统除以 10.
这个问题现在已经打动了我,我在网上找不到已经存在的解决方案。
提前干杯
非常感谢 Peter Cordes 在评论中的回答,我现在 post 回答任何其他计划使用 30 年前的老式编译器的人。
粗略地说,您在 16 位 TASM 中可以获得的最佳时钟仍然不够准确。
幸运的是,在 TASM 中,您可以使用 .386
指令(如 here 所述)“解锁”32 位模式。
然后,您可以使用RDTSC
命令(读取时间戳计数器),但是有一个问题。它在TASM 中不存在。
它不存在的事实对我们没有任何意义,因为 TASM 中的所有命令(通常称为助记符)只是 OpCode 的替代品,它定义了 CPU 可以 运行 的每条指令。 =16=]
当 Intel Pentium CPU 发布时,包含了 RDTSC 的 OpCode,所以如果你有一个 CPU 从它开始......你很好。
现在,如果 TASM 中不存在 RDTSC 指令,我们如何 运行? (但在我们的 CPU 中)
在 TASM 中,有一条指令叫做 db
,通过它我们可以直接 运行 一个 OpCode。
正如所见 here,我们需要对 运行 RDTSC 做的是:db 0Fh, 31h
.
就是这样!您现在可以 运行 轻松执行此指令,并且您的程序仍将一团糟,但 timed 混乱!
在 16 位 PC 兼容 x86 系统中,PIT(可编程间隔定时器)使用 1.19318MHz 的时钟输入来递减 16 位计数器。每当计数器在 216 = 65536 增量后返回时,就会产生中断。 BIOS 提供的 ISR(中断服务例程)处理它然后递增软件计数器,频率为 1.19318MHz / 65536 ~= 18.2 Hz。
在DOS和其他实模式操作系统下,16位PIT计数器可以直接从相关端口以两个8位块读取,这个数据可以与软件维护的滴答计数器结合起来达到毫秒分辨率。基本上,一个人最终会使用一个 48 位滴答计数器,其中由 BIOS 维护的 32 位软件计数器构成最高有效位,而 16 位 PIT 计数器构成最低有效位。
由于数据并非一口气全部读出,因此存在竞争条件的风险,必须妥善处理。此外,一些 BIOS 用于将 PIT 编程为方波发生器而不是简单的速率计数器。虽然这不会干扰增加软件滴答的任务,但它会干扰 PIT 计数器寄存器与软件滴答的直接组合。这需要对 PIT 进行 一次性 初始化,以确保它在速率计数模式下运行。
下面是 16 位汇编代码,封装为 Turbo Pascal 单元,多年来我一直使用它来实现毫秒级精度的稳健计时。这里从滴答计数到毫秒的转换有点像黑盒子。我丢失了它的设计文档,现在无法即时快速重建它。我记得这个定点计算的抖动足够小,可以可靠地测量毫秒。 Turbo-Pascal 的调用约定要求在 DX:AX
寄存器对中返回一个 32 位整数结果。
UNIT Time; { Copyright (c) 1989-1993 Norbert Juffa }
INTERFACE
FUNCTION Clock: LONGINT; { same as VMS; time in milliseconds }
IMPLEMENTATION
FUNCTION Clock: LONGINT; ASSEMBLER;
ASM
PUSH DS { save caller's data segment }
MOV DS, Seg0040 { access ticker counter }
MOV BX, 6Ch { offset of ticker counter in segm.}
MOV DX, 43h { timer chip control port }
MOV AL, 4 { freeze timer 0 }
PUSHF { save caller's int flag setting }
CLI { make reading counter an atomic operation}
MOV DI, DS:[BX] { read BIOS ticker counter }
MOV CX, DS:[BX+2]
STI { enable update of ticker counter }
OUT DX, AL { latch timer 0 }
CLI { make reading counter an atomic operation}
MOV SI, DS:[BX] { read BIOS ticker counter }
MOV BX, DS:[BX+2]
IN AL, 40h { read latched timer 0 lo-byte }
MOV AH, AL { save lo-byte }
IN AL, 40h { read latched timer 0 hi-byte }
POPF { restore caller's int flag }
XCHG AL, AH { correct order of hi and lo }
CMP DI, SI { ticker counter updated ? }
JE @no_update { no }
OR AX, AX { update before timer freeze ? }
JNS @no_update { no }
MOV DI, SI { use second }
MOV CX, BX { ticker counter }
@no_update: NOT AX { counter counts down }
MOV BX, 36EDh { load multiplier }
MUL BX { W1 * M }
MOV SI, DX { save W1 * M (hi) }
MOV AX, BX { get M }
MUL DI { W2 * M }
XCHG BX, AX { AX = M, BX = W2 * M (lo) }
MOV DI, DX { DI = W2 * M (hi) }
ADD BX, SI { accumulate }
ADC DI, 0 { result }
XOR SI, SI { load zero }
MUL CX { W3 * M }
ADD AX, DI { accumulate }
ADC DX, SI { result in DX:AX:BX }
MOV DH, DL { move result }
MOV DL, AH { from DL:AX:BX }
MOV AH, AL { to }
MOV AL, BH { DX:AX:BH }
MOV DI, DX { save result }
MOV CX, AX { in DI:CX }
MOV AX, 25110 { calculate correction }
MUL DX { factor }
SUB CX, DX { subtract correction }
SBB DI, SI { factor }
XCHG AX, CX { result back }
MOV DX, DI { to DX:AX }
POP DS { restore caller's data segment }
END;
BEGIN
Port [] := ; { need rate generator, not square wave }
Port [] := 0; { generator as programmed by some BIOSes }
Port [] := 0; { for timer 0 }
END. { Time }