colin@colino.net

2025/05/08 - 大约阅读10分钟

Apple II MouseCard IRQ 确实与 VBL 同步

这篇简短的技术文章旨在记录一些对大家来说可能并不明显的事情。最近,我向 Apple II 开发社区的同仁们提出了关于 Apple II MouseCard、它的中断以及它们与 Apple II 垂直消隐同步的问题。

关于该卡的文档指出,IRQ 与 VBL 同步:

我对此思考了很多,因为在 MAME 模拟器下,我的 Shufflepuck 游戏闪烁得很厉害,但在现实生活中,它的渲染却非常干净。当然,在 的现实生活中,我可以在 Apple //c 上进行测试,它并没有真正的 MouseCard,但具有兼容的鼠标固件,而且它是一台 PAL Apple,因此它以 50Hz 而非 60Hz 的频率运行。如果我想测试我的绘制速度是否足够快以适应 60Hz 的 Apple II,我必须浪费 3250 个周期才能开始绘制。

我如何浪费周期以确保我的游戏在 NTSC 机器上不会闪烁

因此,我无法进行我想要的精确测试,我不得不请好心人在他们的 NTSC Apple IIe 上进行多次测试,以确保我所做的事情是正确的。所有尝试过的人都告诉我它很好,没有闪烁,有些人还提供了视频(谢谢大家!)。但一些知识渊博的人告诉我,鼠标 IRQ 并没有,和/或无法与 VBL 同步,因为鼠标卡没有办法知道 VBL。充其量,它只能像“以相同的频率触发”那样“同步”。

最终,我在 MAME 的追踪器上提出了一个 issue,其中包含了我关于这个谜题的原理图:

我画了很好的图来解释自己

虽然 MAME 中的 bug 尚未修复,但 Robert Justice 和 R. Belmont(以及我自己的一些)对此进行的调查清楚地表明:

是的,Apple II MouseCard IRQ 与 VBL 信号精确同步,并与 VBL 信号同时触发。

首先,当程序员调用 MOUSE_INIT 时,鼠标固件会等待垂直消隐。在 Apple IIe 上,它通过观察 $C019RDVBL 来实现;在 Apple ][+ 上,由于此软开关不可用,它通过设置一个充满零的 HGR 页面,并在相关位置设置几个不同的字节,然后执行vapor lock来实现。它可能不太精确,相差几个周期,但足以确定光束在屏幕上的位置。

以下是从 IIe 上等待 VBL 的 AppleMouse 固件代码的摘录:

; Here we are in my code, preparing to call MOUSE_INIT.
A=00 X=C4 Y=40; N....IZ.; 6E90: ldx #$19   ; Load MOUSE_INIT entry point ID
A=00 X=19 Y=40; .....I..; 6E92: jsr $6eaf   ; Jump to firmware calling subroutine
A=00 X=19 Y=40; .....I..; 6EAF: ldy $c400, x ; Get the firmware jump target low byte
A=00 X=19 Y=BC; N....I..; 6EB2: sty $6eba   ; Patch our code to jump
A=00 X=19 Y=BC; N....I..; 6EB5: ldx #$c4   ; Load X and Y according to the
A=00 X=C4 Y=BC; N....I..; 6EB7: ldy #$40   ; mousecard specifications
A=00 X=C4 Y=40; .....I..; 6EB9: jmp $c4bc   ; Jump to mouse firmware INIT
; Now we enter the mouse firmware, and watch it wait for VBL start.
[...]
A=06 X=C4 Y=40; .....IZC; C426: lda RDVBL   ; The mouse firmware waits for start
A=80 X=C4 Y=40; N....I.C; C429: bmi $c426   ; of vertical blanking
[...]
A=00 X=C4 Y=40; .....IZC; C42B: lda RDVBL   ; Now it waits for start of beam drawing
A=80 X=C4 Y=40; N....I.C; C433: bpl $c430   ; on the screen
[...]
A=80 X=C4 Y=40; N....I.C; C430: lda RDVBL   ; And wait for start of VBL again
A=00 X=C4 Y=40; .....IZC; C433: bmi $c430   
[...]                     ; At that point, VBL just started

这部分解释了“AppleMouse 卡如何在没有该信号可用的情况下知道 VBL”:它像我们程序员一样,通过软件来实现。

现在是另一个问题:MouseCard 如何以与 VBL 相同的速率触发其 IRQ? NTSC Apple II 的帧速率为 59.92Hz,PAL Apple II 的帧速率为 50.32Hz

嗯,一旦 MouseCard 固件捕获到 VBL 的开始,它就会使用其附带的 6821 PIA 来设置其附带的 68705 CPU。在以下摘录中,我只保留了对 PIA 寄存器的读/写操作:

A=00 X=C4 Y=40; .....IZC; C470: lda $c082, y ; Read PIA 0x2
A=0C X=C4 Y=40; .....I.C; C478: sta $c082, y ; Write 0x0C to PIA 0x2
A=00 X=C4 Y=40; .....IZC; C416: lda $c082, y ; Read PIA 0x2
A=0C X=C4 Y=40; .....I.C; C41B: lda $c081, y ; Read PIA 0x1
A=00 X=C4 Y=40; .....IZC; C420: sta $c081, y ; Write 0x0C to PIA 0x1
A=FF X=C4 Y=40; N....I.C; C425: sta $c080, y ; Write 0xFF to PIA 0x0
A=FF X=C4 Y=40; N....I.C; C428: lda $c081, y ; Read PIA 0x1
A=04 X=C4 Y=40; .....I.C; C42D: sta $c081, y ; Write 0x04 to PIA 0x1
A=50 X=C4 Y=40; .....I.C; C431: sta $c080, y ; Write 0x50 to PIA 0x0
A=50 X=C4 Y=40; .....I.C; C434: lda $c082, y ; Read PIA 0x2
A=2C X=C4 Y=40; .....I.C; C439: sta $c082, y ; Write 0x2C to PIA 0x2
A=2C X=C4 Y=40; .....I.C; C43C: lda $c082, y ; Read PIA 0x2
A=2C X=C4 Y=40; .....I.C; C43F: bpl $c43c   ; Until high bit is set
[...]
A=2C X=C4 Y=40; .....I.C; C43C: lda $c082, y 
A=AC X=C4 Y=40; N....I.C; C43F: bpl $c43c   ; It is now
A=AC X=C4 Y=40; N....I.C; C441: and #$df   
A=8C X=C4 Y=40; N....I.C; C443: sta $c082, y ; Write 0x8C to PIA 0x2
A=80 X=C4 Y=40; N....I..; C470: lda $c082, y ; Read PIA 0x2
A=84 X=C4 Y=40; N....I..; C478: sta $c082, y ; Write 0x84 to PIA 0x2
A=00 X=C4 Y=40; .....IZ.; C470: lda $c082, y ; Read PIA
A=00 X=C4 Y=40; .....IZ.; C478: sta $c082, y ; Write 0x00 to PIA 0x2

基本上,发生的事情是,Apple II 主 CPU 运行鼠标固件代码,该代码命中 6821 PIA 寄存器,这使得 PIA 与卡上运行的 68705 进行通信。对我来说,它的细节太接近底层了,但重要的是 68705 在设置期间所做的事情。

68705 CPU 附带一个 2kB 的小程序,该小程序已在 MouseCard 上进行了编程。换句话说,MouseCard 上有两个固件:一个是主 CPU 运行的 (341-0270-c),另一个是 68705 运行的 (341-0269)。

68705 固件在设置期间执行以下操作:

A=00,X=00 083: lda  PORTA
A=90,X=00 085: bset 3, PORTC
A=90,X=00 087: brset 1, PORTC, $087
A=90,X=00 087: brset 1, PORTC, $087
A=90,X=00 08A: bclr 3, PORTC
A=90,X=00 08C: rts
A=90,X=00 3F9: sta  $59
[...]
A=24,X=24 5B2: lda  $59
A=90,X=24 5B4: anda #$01
A=00,X=24 5B6: tax
A=00,X=00 5B7: lda  $06C5,x
A=A2,X=00 5BA: sta  $52
A=A2,X=00 5BC: lda  $06C3,x
A=41,X=00 5BF: sta  $53
A=41,X=00 5C1: lda  $06C9,x
A=C7,X=00 5C4: sta  $50
A=C7,X=00 5C6: lda  $06C7,x
A=DF,X=00 5C9: sta  $51

我们稍后再讨论这些值。接下来要知道的是,68705 具有硬件(8 位)计时器功能,该功能在计时器达到 0 时会触发 IRQ。

让我们看看当计时器耗尽并中断 68705 时会发生什么:

67D bclr 7, TCR
67F dec  $4F   ; decrement a number
681 bne  $6BF   ; just rti if > 0
683 lda  TDR   ; Otherwise load Timer Data,
685 suba $50   ; Subtract the value at $50,
687 sta  TDR   ; Update Timer Data with that,
689 lda  $4F   ; and reinit the number at $4F
68B sbca $51   ; by subtracting the value at $51 from 0
68D inca      ; and adding one
68E sta  $4F
[...]
6B5 bclr 6, PORTB ; Finally, clear bit 6 of port B, which
[...]        ; triggers a main CPU interrupt - the VBL IRQ!
6BF rti

那么,这意味着什么?在每个 68705 CPU 周期中,计时器数据都会递减。当它达到 0 时,68705 会查看 $4F,如果它不为 0,则直接返回;但如果它为 0,它会将计时器数据重新初始化为小于 $FF,使该递减比其他递减短,并将完整递减的次数重置为 256 减去 $51 中的值。

现在来看 $50 和 $51 中的这些值,这些值来自固件中的 $06C9,x$06C7,x

      0 1 2 3 4 5 6 7 8 9 a b c d e f
000006c0 80 fc 8e 41 4e a2 4e df d9 c7 6e 00 00 00 00 00

当 X=0 时,$51 包含 $DF,$50 包含 $C7,当 X=1 时,$D9 / $6E。换句话说,计数器被编程为: ((256−0xDF)×256)+(256−0xC7)= 8505 NTSC 上的周期,((256−0xD9)×256)+(256−0x6E) = 10130 PAL 上的周期。

最后,当计时器数据在 68705 的 IRQ 期间被重新设置时,在 lda TDR / suba $50 / sta TDR 期间,我们必须记住,TDR 在 lda 期间和 suba 期间会继续递减。这些周期已计入幻数中。不知何故,有 10 个周期被考虑在内,即使 lda 和 suba 是 4 个周期,而 sta 是 5 个周期。我们认为,这与 LDA 返回的计时器数据递减了 3 个周期而不是 4 个周期有关。这是最后一个并非每个周期都精确计算在内的部分,但在这一点上,我们仍然可以从这些值中理解并确信 68705 被编程为每 8515 个周期 (NTSC) 或 10140 个周期 (PAL) 中断 6502。

鉴于 68705 的时钟频率为 2MHz,是 1MHz 6502 的两倍,这意味着每 17030 或 20280 个周期出现一次 6502 IRQ,这些值众所周知是两个垂直消隐之间的精确周期数。

总而言之:Apple II 技术说明“改变 VBL 中断率”中关于中断与 VBL 同步的说法是正确的。它是精确到周期的同步。它也会尽可能靠近 VBL 的开始时触发。

非常感谢那些在 MAME issue 上深入研究这个问题的人,对我来说这是一次非常有趣的深入探索。

最后,对于想要使用鼠标 IRQ 在 VBL 上同步的程序员来说:如果您希望它在 NTSC 和 PAL Apple II 计算机上都能工作,您将必须使用 TIME_DATA MouseCard 固件调用将其设置为正确的速率。为此,您需要知道您运行的是哪种计算机。您可以通过三种方式做到这一点:

我自己的代码使用 get_tv(),如果它得到 TV_OTHER,则询问用户

但请记住,IRQ 通过 IRQ 向量、ProDOS,然后是各种 IRQ 处理程序,一直到您的代码的漫长旅程意味着您的代码将在 VBL 发生后几百个周期才得到通知。因此,此方法有一个优点:在带有鼠标卡的 ][+ 上进行简单、干净的 VBL 同步,并且有两个不便之处:它需要 MouseCard,并消耗了您本来可以用来在屏幕上绘制的几百个周期。