Do not use LD A,I to read interrupt enabled state

Page 1/4
| 2 | 3 | 4

By Grauw

Ascended (10767)

Grauw's picture

27-12-2015, 05:15

So, you think you’re smart and use this code pattern to restore the interrupt state after disabling it:

    ld a,i
    di
    ; do stuff that does not modify p/v flag
    ret po
    ei
    ret

Bad idea! Because, quote Z80 user manual:

Z80 user manual wrote:

LD A,I
P/V contains contents of IFF2
If an interrupt occurs during execution of this instruction, the Parity flag contains a 0.

In other words, if an interrupt occurs in the wrong place, the interrupt status will be restored incorrectly.

This caused a rare random hang in VGMPlay, which recently became much more reproducible because I now generate a lot more interrupts in my development version. Fortunately this behaviour is also emulated well, so I was able to find the cause relatively quickly.

Even the MSX2 BIOS developers used this pattern for CALSLT / CALLF, and as a result also introduced this rare interrupt restoration status bug. Thus, you must also not rely on CALSLT / CALLF preserving the interrupt state, despite it saying so (on MSX1, I believe it always disables them anyway).

I’m left with asking myself: why, Zilog, why!

Anyone know of a way to reliably determine the interrupt state? I can’t think of any.

Login or register to post comments

By Grauw

Ascended (10767)

Grauw's picture

27-12-2015, 05:38

Note, the R800 deviates from the Z80 in this regard, it does not exhibit this behaviour.

Also, I forgot to mention, the same applies to LD A,R.

By RetroTechie

Paragon (1563)

RetroTechie's picture

27-12-2015, 07:25

Double-check?

    ld a,i
    jp pe,go_on     ; IFF was 1 (interrupts enabled)

; if P/V read "0", try a 2nd time
    ld a,i

go_on:
    di
    ; do stuff that does not modify p/v flag
    ret po
    ei
    ret

From a theoretical p.o.v., still not 100% airtight. But from a practical p.o.v., ultra-reliable I think. Especially when VDP is the only source of interrupts as is usually the case.
But okay, for the ultra-paranoid: LOL!

    ld a,i
    jp pe,go_on
    ld a,i
    jp pe,go_on
    ld a,i
go_on:
    di
    ; do stuff that does not modify p/v flag
    ret po
    ei
    ret

And figure out what would need to happen to have interrupts disabled on entry, but still take the EI-exit.

By ARTRAG

Enlighted (6935)

ARTRAG's picture

27-12-2015, 08:00

Push af
Pop af

By RetroTechie

Paragon (1563)

RetroTechie's picture

27-12-2015, 08:29

Yeah... I think that's implied by

    ; do stuff that does not modify p/v flag

The problem here is how to be sure that DI/EI state really makes it into the P/V flag. Apparently, a simple "ld a,i" is not 100% reliable for that purpose.

By Daemos

Prophet (2060)

Daemos's picture

27-12-2015, 12:45

nvm

By Prodatron

Paragon (1843)

Prodatron's picture

27-12-2015, 13:51

RetroTechie wrote:
    ld a,i
    jp pe,go_on
    ld a,i
    jp pe,go_on
    ld a,i
go_on:
    di
    ; do stuff that does not modify p/v flag
    ret po
    ei
    ret

This wouldn't solve the problem, but even increase it. You lose, as soon as the IRQ occures in front of the DI command, and it doesn't matter if you put multiple JP PE before -> this would even increase the chance, that an IRQ occures at the wrong place.
The only chance I see here is to patch the #38 vector. This would set a flag, which overrides the P/V flag check in the routine.

By ARTRAG

Enlighted (6935)

ARTRAG's picture

27-12-2015, 14:28

Why not something like this?

Ld a,i
Di
Push af
;  do what you like

Pop af
Ret po
Ei
Ret

By ARTRAG

Enlighted (6935)

ARTRAG's picture

27-12-2015, 14:37

Never mind the problem occurs while executing ld a,I
So no solution exists based on z80 capabilities

By Prodatron

Paragon (1843)

Prodatron's picture

27-12-2015, 15:00

ARTRAG wrote:

So no solution exists based on z80 capabilities

When patching #38 you have a solution, but it's a complex one, so maybe it makes more sense to handle this manually.

By kanima

Master (194)

kanima's picture

27-12-2015, 15:13

I found a similar discussion at the following link. It's not clear to me whether they ever got the forcing of PE/PO correct, so I've adapted it slightly with code that should work correctly.

   ; Push a zero byte to the stack and pop it
   xor a
   push af
   pop af
   ld a,i
   jp pe,interrupts_checked
   ; See if an interrupt triggered. If so, the byte on the stack (or better said,
   ; below the stack) will not be 0 anymore UNLESS this code is executing 
   ; from $0000-$00FF area!!!
   dec sp
   dec sp
   pop af
   or a
   jr z,clear_or_set_pv_flag
   ; Our zero byte has been overwritten, which means an interrupt must have
   ; occurred. Set A to $7F so the increase will set parity even
   ld a,$7F
clear_or_set_pv_flag:
   inc a
interrupts_checked:
   ; PE = interrupts enabled
   ; PO = interrupts disabled

The high byte of the return address put on the stack when an interrupt occurs overwrites our zero byte. If an interrupt occurs during our LD A,I instruction, then it will be the address of that LD A,I instruction that will be the return address (or maybe of the instruction right after it?). In order for our code to find out whether an interrupt occurred during the LD A,I instruction, the high byte of the address of that instruction must NOT be zero. In other words, make sure the above code snippet is always located at address $0100 or higher, NEVER between $0000-$00ff.

Page 1/4
| 2 | 3 | 4