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

帅比萌擦擦* 提交于 2021-02-08 13:12:32

问题


I'm trying to write two bytes (color values) to the VRAM of my TI-84 Plus CE-T calculator, which uses the Zilog eZ80 CPU. The VRAM starts at 0xD40000 and is 0x25800 bytes long. The calculator has a built in syscall called MemSet, which fills a chunk of memory with one byte, but I want it to alternate between two different values and store these in memory. I tried using the following code:

#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

I want it to output 31 00 31 00 31 00... into memory starting at 0xD40000, but instead it seems to change only the first byte and jump to the end after doing so. Any ideas on how to fix this?


回答1:


The variation of tum_'s answer with faster-than-regular-dec bc zero test mechanism for looping.

    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

(BTW I never did eZ80 programming, and I didn't verify this in debugger, so this is kinda full of assumptions... actually thinking about it, isn't push on eZ80 32 bit? The the init of hl should be ld hl,$001F001F to set four bytes with single push, and the inner body of loop should have only two push hl)

(but I did ton of Z80 programming, so that's why I even bother with comment on this topic, even if I haven't seen eZ80 code ever before)

Edit: turns out the eZ80 push is 24 bit, i.e. the code above will produce incorrect result. It can be of course easily fixed (as the issue is implementation detail, not principal), like:

    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



回答2:


First of all, if you're going to move SP, you need to save and restore it. Second, you need to disable interrupts or else you'll have a race condition bug: if an interrupt triggers near the end of the copy, the stack will grow down into whatever is below it, which happens to be the 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 The eZ80 will cache any -IR/-DR suffix instruction; unlike the Z80, it doesn't reread the opcode from memory on each iteration. Consequently, instructions like LDIR can execute each iteration in just 2 bus cycles, one read and one write. The SP hack is therefore not only needlessly complicated, but actually slower. The SP hack still best left to more experienced programmers.

The eZ80 is very well pipelined and its performance is limited by its lack of any cache and 1-byte-wide bus. The only instruction that runs slower than the bus is MLT, a 2-bus-cycle instruction that needs 5 clock cycles. For every other instruction, just count the number of bytes in the opcode, and the number of read and write cycles, and you've got its execution time. It's a huge pity that in the TI-84+CE series, TI decided to pair the fast eZ80 with an SRAM that somehow needs four clock cycles for each read and write (at 48 MHz)! Yes, TI, a world leader in semiconductor design, managed to design a slow SRAM. Getting on-die SRAM to perform poorly is an engineering feat.

@harold has the right answer, though I prefer optimizing for size instead of speed outside of inner loops.

#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

On EFnet, #ez80-dev is a good place to ask questions. cemetech.net is also a good place.




回答3:


This does not work:

dec   bc
jr    z,j2

Only 8 bit dec and inc modify the flags. It could be fixed by properly detecting whether bc is zero.

Here is a different technique without manual looping:

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



回答4:


See Update 3 at the bottom.

In addition to @harold's answer: if there's a need for a faster alternative a well-known trick with PUSH can be used.

I'm not familiar with TI-84, the stack trick might be unacceptable on some systems or require interrupts to be disabled. And of course you are supposed to store/restore the SP before/after the above code.

Update 3: Removed my code snippet as it was incorrect for eZ80 anyway. However, thanks to the links provided by @DrDnar here is someone's -not mine! :)- attempt to push the performance to the limit (yes, I'm aware that filling with $55 is not the same as alternating between $31 and $00):

Code:

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 States !!! ;D (the classic LDIR takes about 537600 states)

Cemetech source There are more (allegedly faster) examples there.

This at least raises certain doubts regarding the claim that LDIR is the fastest option, so i would be interested in @DrDnar's comments.

Note: I'm not saying the claim is wrong as I'm not in the position to test any of this and see for myself. I've noticed that the author of the above code, although they mention "TI83PCE/TI84+CE" in the original post, perform the actual measurements on a TI83PCE only - and this might be important.

Also, the addresses and the size used in the code are not the same as in the OP's code, and the "8bpp mode" is mentioned, which again tells me very little but the OP does not mention any particular mode.

Update 4: The link provided by @iPhoenix contains loads of info on the TI-84+CE, including the LCD Controller details. This page explains, among other things, why '8bpp mode' has been specifically mentioned by the author of that code above:

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.

In other words - rather than filling the 153600 bytes of VRAM with 0x31,0x00 (16bpp colour, presumably), the OP could fill half of VRAM with a single byte value XY and configure (prior to the actual filling) the Color Palette so that the XY value maps to the desired 16bpp colour and thus achieve the same result.

With this approach any "inconveniences" of alternating between 31 and 00 just go away naturally.



来源:https://stackoverflow.com/questions/57483503/how-to-write-two-bytes-to-a-chunk-of-ram-repeatedly-in-z80-asm

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!