如何在 Z80 asm 中将两个字节重复写入一块 RAM

How to write two bytes to a chunk of RAM repeatedly in Z80 asm

我正在尝试将两个字节(颜色值)写入我的 TI-84 Plus CE-T 计算器的 VRAM,该计算器使用 Zilog eZ80 CPU。 VRAM 从 0xD40000 开始,长度为 0x25800 字节。计算器有一个名为 MemSet 的内置系统调用,它用一个字节填充一大块内存,但我希望它在两个不同的值之间交替并将它们存储在内存中。我尝试使用以下代码:

#include "includes\ti84pce.inc"

    .assume ADL=1
    .org userMem-2
    .db tExtTok,tAsm84CeCmp

    call  _homeup
    call  _ClrScrnFull
    ld    hl,13893632     ; = D40000, vram start
    ld    bc,153600       ; = 025800, count/vram length
j1:
    ld    (hl),31         ; set first byte
    inc   hl
    dec   bc
    jr    z,j2            ; jump to end if count==0
    ld    (hl),0          ; set second byte
    inc   hl
    dec   bc
    jr    z,j2            ; jump to end if count==0
    jp    j1              ; loop
j2:
    call  _GetKey
    call  _ClrScrnFull
    ret

我希望它将 31 00 31 00 31 00... 输出到从 0xD40000 开始的内存中,但它似乎只更改了第一个字节并在这样做之后跳转到末尾。关于如何解决此问题的任何想法?

这不起作用:

dec   bc
jr    z,j2

只有 8 位 decinc 修改标志。可以通过正确检测 bc 是否为零来修复。

这是一种无需手动循环的不同技术:

ld    hl,$D40000
ld    (hl),31
inc   hl
ld    (hl),0
dec   hl
ld    de,$D40002
ld    bc,800 - 2
ldir

请参阅底部的 更新 3

除了@harold 的回答:如果需要更快的替代方案,可以使用众所周知的 PUSH 技巧。

我不熟悉 TI-84,堆栈技巧在某些系统上可能是不可接受的,或者需要禁用中断。当然你应该 store/restore SP before/after 上面的代码。

更新 3: 删除了我的代码片段,因为它对 eZ80 来说是不正确的。 但是,感谢@DrDnar 提供的 links 这里是某人的 - 不是我的! :)- 尝试将性能推向极限(是的,我知道填充 55 美元与在 31 美元和 00 美元之间交替 不同):

Code:

FastClr:
        ld      de,5555      ; will write byte 85 (= blue color)
        or      a
        sbc     hl,hl
        ld      b,217
        di
        add     hl,sp           ; saves SP in HL
        ld      sp,vram+76818   ; for best optimisation , we'll write 18 extra bytes
ClrLp:  .fill 118,$d5           ;       = 118 * "PUSH DE"
        djnz    ClrLp           ; during 217 times
        ld      sp,hl           ; restore SP
        ei

16+4+8+8+4+4+16+217*(118*10+13)-5+4+4=258944 States !!! ;D (the classic LDIR takes about 537600 states)

Cemetech source 那里有更多(据说更快)的例子。

这至少对 LDIR 是最快选项的说法提出了一些疑问,所以我会对@DrDnar 的评论感兴趣。

注意:我并不是说这个说法是错误的,因为我无法测试其中的任何一个并亲眼看看。我注意到上面代码的作者,尽管他们在原始 post 中提到了 "TI83PCE/TI84+CE",但仅在 TI83PCE 上执行实际测量 - 这可能很重要。

此外,代码中使用的地址和大小与 OP 代码中使用的地址和大小不同,并且提到了“8bpp 模式”,这再次告诉我很少,但 OP 没有提到任何特定模式.

更新 4:@iPhoenix 提供的 link 包含大量关于 TI-84+CE 的信息,包括 LCD Controller details。 此页面解释了为什么上面代码的作者特别提到“8bpp 模式”:

When the LCD is in 8bpp mode, data written to VRAM will act as an 8-bit index to the LCD's 256x16-bit Color Palette. Note that the colour palette must be initialized prior to setting this mode or you will receive unexpected results. (See LCDPalette register - 0x200 for information on the Color Palette). This will effectively halve the amount of VRAM required to store a full resolution 320x240 image (76800 bytes vs 153600 bytes). The extra 76800 bytes of VRAM could be used to double buffer or for temporary data storage. Note that the TIOS will not be usable in this mode, it expects 16bpp 5:6:5 mode at all times.

换句话说 - 而不是用 0x31,0x00(大概是 16bpp 颜色)填充 153600 字节的 VRAM,OP 可以用单个字节值填充 half VRAM XY 并配置(在实际填充之前)调色板,以便 XY 值映射到所需的 16bpp 颜色,从而获得相同的结果。

通过这种方法,任何在 31 和 00 之间交替的 "inconveniences" 都会自然消失。

tum_ 的答案变体,具有比常规更快的dec bc 零循环测试机制。

    LD   SP,$D65800    ; <end of VRAM>: 0xD40000+0x25800
    LD   BC,[=10=]4B      ; 0x4B many times (in C) the 256x inner loop (B=0)
        ; that results into 0x4B00 repeats of loop, which when 8 bytes per loop
        ; are set makes the total 0x25800 bytes (VRAM size)
        ; (if you would unroll it for more than 8 bytes, it will be a bit more
        ; tricky to calculate the initial BC to get correct amount of looping)
        ; (not that much tricky, just a tiny bit)
    LD   HL,31         ; H <- 0, L <- 31
.L1
    PUSH HL            ; (SP – 2) <- L, (SP – 1) <- H, SP <- SP - 2
    PUSH HL            ; set 8 bytes in each iteration
    PUSH HL
    PUSH HL
    DJNZ .L1           ; loop by B value (in this example it starts as 0 => 256x loop)
    DEC  C             ; loop by C ("outer" counter)
    JR   NZ,.L1        ; btw JP is faster than JR on original Z80, but not on eZ80
.END

(顺便说一句,我从来没有做过 eZ80 编程,我也没有在调试器中验证这一点,所以这有点充满假设......实际上考虑一下,不是 push 在 eZ80 32 位上? hl 的init 应该是ld hl,[=15=]1F001F 来设置四个字节与单个push,并且循环内部应该只有两个push hl)

(但我做了 ton 的 Z80 编程,所以这就是为什么我什至不厌其烦地评论这个话题,即使我以前从未见过 eZ80 代码)

编辑:事实证明 eZ80 推送是 24 位的,即上面的代码会产生不正确的结果。它当然可以很容易地修复(因为问题是实现细节,而不是原则),比如:

    LD   SP,$D65800    ; <end of VRAM>: 0xD40000+0x25800
    LD   BC,[=11=]14      ; 0x14 many times (in C) the 256x inner loop (B=0)
        ; that results into 0x1400 repeats of loop, which with 30 bytes per
        ; loop set makes the total 0x25800 bytes (VRAM size)
    LD   HL,F001F    ; will set bytes 31,  0, 31
    LD   DE,[=11=]1F00    ; will set bytes  0, 31,  0
.L1
    PUSH DE
    PUSH HL
        ; here SP = SP-6, and 6 bytes 31, 0, 31, 0, 31, 0 were set
    PUSH DE
    PUSH HL
    PUSH DE
    PUSH HL
    PUSH DE
    PUSH HL
    PUSH DE
    PUSH HL            ; unrolled 5 times to set 30 bytes in total
    DJNZ .L1           ; loop by B value (in this example it starts as 0 => 256x loop)
    DEC  C             ; loop by C ("outer" counter)
    JR   NZ,.L1

首先,如果你要移动SP,你需要保存和恢复它。其次,您需要禁用中断,否则您将遇到竞争条件错误:如果在副本末尾附近触发中断,堆栈将向下增长到它下面的任何内容,这恰好是 VAT。

; Index registers are actually fast on the eZ80
    ld   ix, 0
    add  ix, sp
    di
; Do some hack using SP here
    ld   sp, ix
    ei

@Ped7g eZ80会缓存任何-IR/-DR后缀的指令;与 Z80 不同,它不会在每次迭代时从内存中重新读取操作码。因此,像 LDIR 这样的指令可以在 2 个总线周期内执行每次迭代,一次读取和一次写入。 因此,SP hack 不仅不必要地复杂,而且实际上更慢。SP hack 最好留给更有经验的程序员。

eZ80 的流水线处理得很好,但它的性能因缺少任何高速缓存和 1 字节宽的总线而受到限制。唯一比总线运行速度慢的指令是 MLT,这是一个需要 5 个时钟周期的 2 总线周期指令。对于其他每条指令,只需计算操作码中的字节数,以及读写周期数,就可以得到它的执行时间。非常遗憾的是,在 TI-84+CE 系列中,TI 决定将快速的 eZ80 与 SRAM 配对,每次读写(48 MHz)不知何故需要 四个 个时钟周期!是的,半导体设计领域的全球领导者 TI 成功设计了 slow SRAM。让片上 SRAM 性能不佳 是一项工程壮举。

@harold 有正确的答案,尽管我更喜欢优化大小而不是内部循环之外的速度。

#include "includes\ti84pce.inc"

    .assume ADL=1
    .org userMem-2
    .db tExtTok,tAsm84CeCmp

    call  _homeup
    call  _ClrScrnFull
; Initialize registers
    ld    hl, vRam
    ld    bc, lcdWidth * lcdHeight * 2 - 2
    push  hl
    pop   de
; Write initial 2-byte value
    ld    (hl), 31
    inc   hl
    ld    (hl), 0
    inc   hl
    ex    de, hl
; Copy everything all at once.  Interrupts may trigger while this instruction is processing.
    ldir
    call  _GetKey
    call  _ClrScrnFull
    ret

在 EFnet 上,#ez80-dev 是提问的好地方。 cemetech.net也是个好地方