【问题标题】:How to write two bytes to a chunk of RAM repeatedly in Z80 asm如何在 Z80 asm 中重复将两个字节写入一块 RAM
【发布时间】:2019-12-20 08:24:33
【问题描述】:

我正在尝试将两个字节(颜色值)写入我的 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

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

【问题讨论】:

  • 宽于 1 字节的模式的等效 C 函数是“wmemset”,它采用wchar(宽字符)参数。不过,如果您的计算器缺少该功能,我不会感到惊讶。不过,其他平台也有;例如glibc 有一个针对各种目标的 asm 优化 wmemset。
  • 您真的应该考虑使用fasmg 进行汇编编程。它要优越得多,并且实际上得到了支持。这里有更多信息:github.com/CE-Programming/documentation

标签: assembly texas-instruments z80


【解决方案1】:

这不起作用:

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,$25800 - 2
ldir

【讨论】:

    【解决方案2】:

    请参阅底部的Update 3

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

    我不熟悉 TI-84,堆栈技巧在某些系统上可能不可接受,或者需要禁用中断。当然,您应该在上述代码之前/之后存储/恢复 SP。

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

    代码:

    FastClr:
            ld      de,$555555      ; 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 个状态!!! ;D( 经典的 LDIR 大约需要 537600 个状态)

    Cemetech source 那里有更多(据称更快)示例。

    这至少引起了对 LDIR 是最快选择的说法的某些怀疑,所以我会对@DrDnar 的 cmets 感兴趣。

    注意:我并不是说该声明是错误的,因为我无法测试其中的任何内容并亲自查看。我注意到上述代码的作者虽然在原帖中提到了“TI83PCE/TI84+CE”,但仅在 TI83PCE 上执行实际测量 - 这可能很重要。

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

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

    当 LCD 处于 8bpp 模式时,写入 VRAM 的数据将充当 8 位索引 到 LCD 的 256x16 位调色板。注意调色板 必须在设置此模式之前进行初始化,否则您将收到 意想不到的结果。 (有关信息,请参见 LCDPalette 寄存器 - 0x200 在调色板上)。这将有效地将 VRAM 的数量减半 存储全分辨率 320x240 图像(76800 字节 vs 153600 字节)。额外的 76800 字节 VRAM 可用于翻倍 缓冲区或用于临时数据存储。请注意,TIOS 不会 可在此模式下使用,它始终期望 16bpp 5:6:5 模式。

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

    通过这种方法,任何在 31 和 00 之间交替的“不便”都会自然而然地消失。

    【讨论】:

    • 这有点有趣,你通过用push 替换常规写入来获得性能,然后你这样做dec bc 事情......如果你真的想要性能,你应该展开push hl 至少几次,例如 4x,即 8 个字节,所以你需要 0x4B00 重复,这些可以像初始化部分:ld bc,$004B 和循环体:j1: 4x push hldjnz j1dec c@987654333 @
    • @Ped7g 当然,我们的目标是展示一个例子。速度与大小,循环展开技术非常明显。随意发布您的答案,让我们看看您实现了每个字节有多少 T 状态:)(我懒得破译评论中的未格式化代码)。
    • 好的,我把它作为完整答案发布了,虽然它只是你的变体.. 我也注意到你使用 JP 与 JR 优化,但那是 Z80 的东西,而不是 eZ80。
    • 83PCE 运行与 84+CE 相同的处理器,硬件基本相同,除了一些测试导致(和一些额外的小事情)。见wikiti.brandonw.net/…
    • Wiki article 说:“2016 年,针对欧洲教育市场发布了 TI-84 Plus CE-T。与 CE 型号的唯一显着区别是增加了一个 LED 闪烁,同时计算器处于按下测试模式。”
    【解决方案3】:

    tum_ 答案的变化与快于常规-dec bc 循环的零测试机制。

        LD   SP,$D65800    ; <end of VRAM>: 0xD40000+0x25800
        LD   BC,$004B      ; 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 应该是ld hl,$001F001F 设置四个字节与单个push,循环的内部应该只有两个push hl)

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

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

        LD   SP,$D65800    ; <end of VRAM>: 0xD40000+0x25800
        LD   BC,$0014      ; 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,$1F001F    ; will set bytes 31,  0, 31
        LD   DE,$001F00    ; 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
    

    【讨论】:

    • eZ80 上的 HL、DE、BC、SP(因此 push)似乎是 24 位的(在 ADL 模式下,请参阅 OP 的 .assume ADL=1)。是 - 3 个字节。最不寻常的))
    • +1 更新答案。似乎 eZ80 上的 LDIR 是每字节 2 个周期,而 PUSH(在 ADL 模式下)是 4 个周期,每字节提供 4/3 个周期。与 LDIR 为 21T 而 PUSH 为 5.5T(每字节)的 z80 相比,增益较小。
    • @melbok 你添加了sp 保存吗?就像在数据部分的某处保留 24 位内存空间(不确定您的汇编语法是什么,可能是 OldSp: ds 3 ?),并且在代码部分之前的 ld sp,... 执行首先 ld (OldSp),sp 并在填充 ld sp,(OldSp) 之后进行恢复回来了。 (就像你明白什么是sp 以及它是如何“滥用”它的“通常”目的在这个内存填充物中?)(可能不是,因为你确实在它之后添加了call _GetKey :) ...在@987654338 之前@你需要已经恢复了sp,否则你正在覆盖VRAM下的内存,如果有任何可写的话)
    • @melbok 是的,call,更重要的是,ret 指令隐式使用 SP。 ret 从堆栈中获取返回地址,因此除非您恢复堆栈指针的正确值,否则您最终会遇到麻烦。我确实在答案的末尾添加了免责声明。
    • @tum_ 如果他刚开始组装,再多的免责声明也无济于事,有这么多新的东西和细节,我可以想象这会让人不知所措……还有组装的新手,如果他们以前做过一些高级语言的编程,常常没有意识到汇编所需的精度水平,只是跳过一两个词,甚至整个免责声明,因为“不可能那么重要,对吧?”... :D ...所以我们俩都有点讨厌不提供完整的工作代码,包括sp保存...然后OP甚至没有指定汇编程序(语法):)
    【解决方案4】:

    首先,如果你要移动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 配对,该 SRAM 每次读写都需要 四个 时钟周期(48 MHz)!是的,半导体设计领域的世界领先者 TI 成功地设计了一个 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也是个好地方。

    【讨论】:

    • 感谢您提供有关 eZ80 的详细信息,这对我来说很有趣(我在 ZX Spectrum 上做 Z80,以后再也没有接近 eZ80)。
    • 干杯,确实是一个非常好的答案。该特定硬件上的慢速 SRAM 信息是新的,并且肯定需要重新考虑整个方法。附带说明:Retrocomputing 也是解决此类问题的好资源。
    • @DrDnar:我在“答案”中添加了“更新 3”。请您发表评论好吗?
    • 你是对的,出于某种奇怪的原因,我在那里考虑了 LDIR。但是仍然需要禁用中断,因为接近副本末尾的中断会破坏 VAT。
    • @Ped7g 和 DrDnar:如果它是一个非常大的填充,可能值得使用 push 来处理大部分内容(启用中断),但停止短 128 或 256 个字节(或任何最大中断堆栈消耗是)并用单独的安全循环填充其余部分。在主循环之外花费更多的代码大小和一些时间,所以只有在加速与第二好的填充循环支付额外开销的情况下才值得。当然,对于可以异步读取并显示“垃圾”或其他异步 DMA 情况的视频 RAM,这可能不合适。 (IDK 任何关于 TI 计算器或 Z80 的很多信息,但我很想被打断。)
    猜你喜欢
    • 2020-07-18
    • 2013-01-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-11-13
    相关资源
    最近更新 更多