在6502代码中实现高效的多重间接寻址

4

问题

我正在查看一个6502程序,它具有多个字节数组(与特定声音相对应的音效数据),这些数组具有不同的长度。目前,这涉及显式迭代第一个数组(如果排队),然后是第二个等等,并且每个声音都有一个单独的音量,延迟等变量集,因此代码设置为使用这些硬编码标签。

我想将其合并到循环中,索引这些额外的变量和音效数据。索引变量相当简单,可以使用索引寻址,但是索引音效数据需要更多的工作,我想知道是否在应用索引间接和间接索引寻址时遗漏了什么。

以下是我目前正在执行的自包含示例。如果可能的话,我希望缩紧LoadFromTable中的代码,最好使用XY寻址:

  .equ  Ptr0,  0x80
  .equ  Ptr1,  0x81

  .org  0xFE00

  .org  0x0000

Init:
  LDX #0xFF
  TXS

Main:
  LDX #0x00
  LDY #0x00
  JSR LoadFromTable
  ; A should be 'H',  0x48

  LDX #0x01
  LDY #0x00
  JSR LoadFromTable
  ; A should be 'B',  0x42

  LDX #0x02
  LDY #0x02
  JSR LoadFromTable
  ; A should be 'A',  0x41

  JMP Main

LoadFromTable:
  TXA           ; Double outer index to account for 16 bit pointers
  ASL           ;   "
  TAX           ;   "
  LDA Table,X   ; Load the low byte of the array into a pointer
  STA Ptr0      ;   "
  INX           ; Load the high byte of the array into the pointer
  LDA Table,X   ;   "
  STA Ptr1      ;   "
  LDA (Ptr0),Y  ; Load the character at the inner index into the array
  RTS

  .org  0x0040

Table:
  .word Item0
  .word Item1
  .word Item2

  .org  0x0080

Item0:
  .byte 'H', 'E', 'L', 'L', 'O', 0x00

Item1:
  .byte 'B', 'O', 'N', 'J', 'O', 'U', 'R', 0x00

Item2:
  .byte 'C', 'I', 'A', 'O', 0x00

  .org  0x00FA

  .word Init
  .word Init
  .word Init

实现

借鉴@NickWestgate的分离表格想法和@Michael指出的初始指针计算,我已经从以下代码中进行了改进:

PROCESS_MUSIC:
  ; ...
  BNE   MusDoB

MusChanA:
  ; ...
  LDA   MUSICA,X
  BNE   MusCmdToneA
  ; ...
  JMP   MusChanA

MusCmdToneA:
  ; ...
  BNE   MusNoteA
  ; ...

MusNoteA:
  ; ...
  LDA   MUSICA,X
  ; ...

MusDoB:
  ; ...
  BNE   MusDoDone

MusChanB:
  ; ...
  LDA   MUSICB,X
  BNE   MusCmdToneB
  ; ...
  JMP   MusChanB

MusCmdToneB:
  ; ...
  BNE   MusNoteB
  ; ...

MusNoteB:
  ; ...

MusDoDone:
  RTS

转化为更通用的子程序:

PROCESS_MUSIC:
  LDX #0x01

PerChannel:
  ; ...
  BNE EndPerChannel
  LDA MusicTableL,X
  STA tmp0
  LDA MusicTableH,X
  STA tmp1

MusChan:
  ; ...
  LDA (tmp0),Y
  BNE MusCmdTone
  ; ...
  BEQ MusChan

MusCmdTone:
  ; ...
  BNE MusNote
  ; ...

MusNote:
  ; ...
  LDA (tmp0),Y
  ; ...

EndPerChannel:
  DEX 
  BPL PerChannel
  RTS

增加以下表格:

MusicTableL:
    .byte <MUSICA
    .byte <MUSICB

MusicTableH:
    .byte >MUSICA
    .byte >MUSICB

这样做就不需要我之前一直使用的LoadFromTable函数了,而且整体看起来更加简洁。

3
多个音效会同时播放吗?如果不是,那么为每个样本加载设置Ptr0/1似乎非常低效(也就是说,只需要在切换到新的音效时才这么做)。 - Michael
根据我的理解,一次只会播放一个序列,并且应该完全播放。将指针对设置为加载的开始并进行递增可能更有效,但是那样你需要正确处理低位字节的回绕,这可能会更麻烦(我不认为INX/INY会设置进位标志)。 - msbit
@Michael 这是真的 :) - msbit
1
@Michael,看了你的原始评论后,我考虑将指针在循环调用站点之前提升到零页中,然后该函数就会退化为LDA(Ptr0),Y,因此它可以直接内联。 - msbit
1
https://retrocomputing.stackexchange.com/ - Erik Eidt
显示剩余3条评论
2个回答

3

以下是一些想法。其中之一是传递一个已经加倍的索引(即如果您可以安排好,或者可能在早期阶段就已经包含在累加器中)。

另一个想法是拆分地址表:

LoadFromTable:
  LDA TableL,X ; Load the low byte of the array into a pointer
  STA Ptr0      ;   "
  LDA TableH,X ; Load the high byte of the array into the pointer
  STA Ptr1      ;   "
  LDA (Ptr0),Y  ; Load the character at the inner index into the array
  RTS

TableL:
  .byte #<Item0
  .byte #<Item1
  .byte #<Item2

TableH:
  .byte #>Item0
  .byte #>Item1
  .byte #>Item2

如果您无法拆分表格,则可以通过执行以下操作来摆脱 INX:

  LDA Table,X   ; Load the low byte of the array into a pointer
  STA Ptr0      ;   "
  LDA Table+1,X ; Load the high byte of the array into the pointer
  STA Ptr1      ;   "

自修改代码可能很有用。生活在第零页将是一个因素:

  LDA Table,X   ; Load the low byte of the array into a pointer
  STA Load+1    ;   "
  LDA Table+1,X ; Load the high byte of the array into the pointer
  STA Load+2    ;   "
Load:
  LDA $FFFF,Y   ; Load the character at the inner index into the array

您还可以查看在存储指针时是否将 Y 添加到指针可以节省任何周期。这可能取决于使用的最常见路径(即是否通常不会 INC Ptr2/Load+2)。

这些很好。不幸的是,自修改示例在这种情况下不适用,因为代码存储在ROM中,但它非常巧妙。让我试着玩一下分割表,并看看是否可以在传递给函数之前预先计算偏移量。 - msbit
使用分割表格显著地清理了它。我可能会在问题中包含我的最终实现,供参考✌️ - msbit
如果你有足够的RAM,你可以将代码复制到RAM中并从那里运行它,这将允许你使用自修改的代码。 - puppydrum64
@puppydrum64:我刚刚给这个问题打上了平台标签,Atari 2600,它只有128字节的RAM。看起来有点紧张!;-) - Nick Westgate
1
你会惊讶于代码所占用的空间是多么少。但另一方面,数据表则需要大量的空间... - puppydrum64

1
如果你正在尝试在1 MHz 6502上生成实时音频,我已经使用每个样本40字节加上实际馈送DAC的时间来制作了四声音乐,而在我的情况下,这需要两个零页存储器来存储AUDV0和AUDV1寄存器。
关键是使用四个代码片段的“滚动”序列,形式如下:
; Carry must be clear on entry; will be clear on exit
; Acc, Y, and other flags ignored on entry; trashed on exit
; X register ignored and left alone
ldy phase1
lda (wave1a),y
ldy phase0
adc (wave0d),y
sta AUDV0
lda (newPhase0),y
sta phase0
ldy phase2
lda (wave2c),y
ldy phase3
adc (wave3d),y
sta AUDV1

这种方法使得在五个八度范围内产生四声部音乐成为可能,使用底部两个八度的半速和四分之一速率播放,但会占用40个字节的零页指针(几乎占据了2600上总RAM的三分之一!)。如果不需要选择播放速率,并且可以承受每个样本额外增加256个字节的填充,则可以将主要样本循环设置为以下内容:

 ldy outCounter
 clc
 bmi useSetB
useSetA:
 lda (wave0a),y
 adc (wave1a),y
 adc (wave2a),y
 adc (wave3a),y
 inc outCounter
 bne storeAndDone ; Will be 1-128
useSetB:
 lda (wave0b),y
 adc (wave1b),y
 adc (wave2b),y
 adc (wave3b),y
 inc outCounter
 nop ; Equalize time
storeAndDone:
 sta DACoutput

在设置outCounter的最高位和清除它之间的某个时刻,以及在它被清除和重新设置之间的另一个时刻,还需要运行另外两个代码片段。

; Run when outCounter reaches 128
 ldx #6 ; Counts by 2 for each voice
fixLp1:
 lda wave0b,x
 sta wave0a,x
 dex
 dex
 bpl fixLp1

; Run when outCounter reaches 128
 ldx #6
fixLp2:
 sec
 lda wave0a,x
 sbc top0,x
 lda wave0a+1,x
 sbc top0+1,x
 bcc notTopYet
 lda wave0a,x
 sbc length0,x
 sta wave0a,x
 lda wave0a+1,x
 sbc length0+1,x
 sta wave0a+1,x
 dex
 dex
 bpl fixLp2
 bmi done2
notTopYet:
 lda wave0a,x
 sta wave0a,x
 lda wave0a+1,x
 sta wave0a+1,x
 dex
 dex
 bpl fixLp2
done:

代码的后半部分可能会相当长,但只需要在每256个样本中运行一次,并且可以容纳任意循环部分。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接