Skeetendo

’Cause all games were better on the GBC

You are not logged in.

#1 2013-03-15 23:29:50

Miksy91
Member
Registered: 2010-10-16
Post 1,566/2,311

VBlank interrupt and VRAM

So the issue is like this:

There are several situations in my hack where I want to access Video RAM ($8000-$8FFF) to write data there. I'm mostly doing this by a "copy paste" loop routine but the problem with this kind of an approach on its own is the fact that I'm not taking in account whether VRAM is accessable at the moment or not. And I kinda felt like I want this thing to work with every kind of an emulating system, not just VBA which ignores this VRAM accessability.

So I did a little search and ran into the following information.

http://www.legendarypokemon.net/gmbspec wrote:

FF46
   Name     - DMA
   Contents - DMA Transfer and Start Address (W)

   The DMA Transfer (40*28 bit) from internal ROM or RAM
   ($0000-$F19F) to the OAM (address $FE00-$FE9F) can be
   performed. It takes 152 microseconds for the transfer.

   40*28 bit = #140  or #$8C.  As you can see, it only
   transfers $8C bytes of data. OAM data is $A0 bytes
   long, from $0-$9F.

   But if you examine the OAM data you see that 4 bits are
   not in use.
   
   40*32 bit = #$A0, but since 4 bits for each OAM is not
   used it's 40*28 bit.

   It transfers all the OAM data to OAM RAM.

   The DMA transfer start address can be designated every
   $100 from address $0000-$F100. That means $0000, $0100,
   $0200, $0300….

    As can be seen by looking at register $FF41 Sprite RAM
   ($FE00 - $FE9F) is not always available. A simple routine
   that many games use to write data to Sprite memory is shown
   below. Since it copies data to the sprite RAM at the appro-
   priate times it removes that responsibility from the main
   program. This routine is usually put into high RAM ($FF80 -
   $FFFE) and called from a VBlank Interrupt:

   Example program:

VBlank:
      push af        <- Save A reg & flags
      ld a,BASE_ADRS <- transfer data from BASE_ADRS
      ld ($ff46),a   <- put A into DMA registers
      ld a,28h       <- loop length
Wait:                <- We need to wait 152.5 microseconds.
      dec a          <-  4 cycles - decrease A by 1
      jr nz,Wait     <- 12 cycles - branch if Not Zero to Wait
      pop af         <- Restore A reg & flags
      reti           <- Return from interrupt

    The most likely reason that it is put into high RAM is so
   that the BASE_ADRS may be changed dynamically by writing a
   new value directly into RAM. This would serve to keep this
   interrupt routine to an absolute minimum execution-time wise.

I can't even tell if this has anything to do with the situation I'm. Simply because this one is talking about OAM, not VRAM but I've heard others saying VRAM also "working in the same way".

So according to this, we have a similar routine to this one stored in the High RAM which I found in $FF80. And by looking at how it's accessed during the game, I simply searched for "call $FF80" instructions and found several of them, starting at in the beginning of the rom (address $150+).
And not long after that, I checked whether crystal disassembly has something to say about this and, should have known, the whole thing was actually very well documented and I found it easily.

https://github.com/kanzure/pokecrystal/ … vblank.asm

The routine is the same in silver as it is in crystal (expect for different ram addresses, and "jr c .doneframeaction" missing after "call DMATransfer").

So in silver (u) rom, we've got:


00:01A9 (corresponds to "; bg map buffer has priority")

call 1458 ; call UpdateBGMapBuffer
jr c, 01C2 ; jr c, .doneframeaction
call 0BDF ; call UpdatePalsIfCGB
jr c, 01C2 ; jr c, .doneframeaction
call 14BB ; call DMATransfer
call 15D0
....

So I'd assume the routine starting at x14BB holds the answers to "where we write?" and "how much data?".

Well as I'm typing this right now, I'm following the routine at the same time. And, I get the feeling this routine is used to load hl with one of the "common" addresses (for palettes and such) or one pulled from the ram (specifically, hl is loaded with the values in FFD8-FFD9). So if I could use this for my own purpose, I'd most likely want to use a routine that first modifies the values in these two addresses.

After this is done, a loop starts which copies values from de into addresses in hl.
This thing is used to store 20 bytes, 6 times in a row. But after each 20 bytes are copied, the next set of 20 bytes is transfered 0xD bytes ahead (with add hl, bc (bc = 0008)).


But this is pretty much all I understand about this, at least for now. So any ideas, how would I go about using the right VBlank type (type 00) so I could access this routine that is used to write data in ram? Am I on the right path or thinking something in a totally wrong way?

I feel like I learned something while doing this, and on the other hand, I feel like I got nothing out of it. The whole thing still makes no sense, and I've got no idea how should I attempt to create a code of this kind.

load hl, (copy from)
ld de, (copy to)
ld bc, (amount)
call @wait ; wait for VRAM accessibility for certain xx amount of time
call @execute ; execute code until VRAM is inaccessable again
jr nz "2 commands backwards"
ret

But yeah, all help would be appreciated.

Offline

#2 2013-03-16 03:14:20

Tauwasser
Member
Registered: 2010-10-16
Post 355/448

Re: VBlank interrupt and VRAM

There's already a routine available for that kind of stuff: 0x0DFE

That routine will transfer gfx to VRAM while in Vblank. It will transfer in multiples of 0x08 tiles until your desired number is reached.
You cannot use the standard DMA for anything but OAM. If you want to go CGB-only, then you can use the new DMA around FF5x.

You can use 0x0E72 if you're not sure what mode you're in (or you need to execute the same general-purpose code in different modes). It will call 0x0DFE when the screen is turned on, else 0xDCD.

0x0DCD: a:hl = source; de = destination; bc = number of bytes,
0x0DFE: b:de = source; hl = destination; c = number of tiles (i.e. number of bytes / 0x10)
0x0E72: b:de = source; hl = destination; c = number of tiles (i.e. number of bytes / 0x10)

EDIT: Oh yeah, 0xDFE uses the update service. If you turn that off, you will obviously loop forever. Just saying.

FFD6 - FFD6 = Screen Update Service
00 = off
01 = transfer from backup at C3A0 (Tiles) 06x0D to cur. screen
02 = transfer from backup atCCD9 (Pals)  "  " "
03 = transfer from backup at C3A0 (Tiles) 06x0D to window
04 = transfer from backup at C3A0 CCD9 (Pals) " " "

FFD7 - FFD7 = Current part of screen/window that is being written
00 = top 06x0D
01 = mid 06x0D
02 = bot 06x0D

cYa,

Tauwasser

Last edited by Tauwasser (2013-03-16 03:16:25)

Offline

#3 2013-03-16 03:36:08

comet
Member
Registered: 2012-04-09
Post 162/675

Re: VBlank interrupt and VRAM

At the time I didn't understand what the modes were actually for so I didn't comment them very well. Looking at it now it's really obvious that saving uses the barebones mode 1, and the battle scene and menus have their own modes. It should be easy to track down which is which by breaking on writes to $ff9e (Crystal).

Last edited by comet (2013-03-16 03:38:18)

Offline

#4 2013-03-16 06:59:30

Miksy91
Member
Registered: 2010-10-16
Post 1,567/2,311

Re: VBlank interrupt and VRAM

Thanks a bunch for the answers!

Say, Tauwasser, there are still certain things that confuse me but other than that, I do understand what you're talking about here.
So, I followed the routines you explained above and I think I get most of it.

But basically, I should call 0E72 to check the bit 7 in LCD Control (FF40). And related to that, I can write data straight to VRAM if the screen is turned off, right? And this thing here is accomplished by jumping to 0DCD later in the code.

But in case VRAM is not accessable, pc is set to 0DFE instead and "It will transfer in multiples of 0x08 tiles until your desired number is reached.". But related to this, I'm not catching what the Screen Update Service is about.
This doesn't seem to have anything to do with where I'm writing to but instead, for "avoiding errors" caused by the process I make, I think.

Well, dunno. But I can simply make this thing work by setting the register values like this (b:de = source; hl = destination; c = number of tiles / 0x10) and calling 0E72, correct?

Also, how does the code at 032E actually work? I found this part corresponding it in GB CPU Manual:

Main;

halt
nop
ld a, (VBlankFlag)
or a
jr z, Main
....


Vblank;

push af
push bc
push de
push hl
call SpriteDma ; Do sprite updates
ld a, 01
ld (VblankFlag), a
pop hl
pop de
pop bc
pop af
reti

It's just that during that code, "nothing is written anywhere". Or so I see it because to me, it simply looks like a small routine used for waiting interrupts to occur, checking what the value in CEEA is and starting to wait for another interrupt to occur. This is obviously a wrong notion because if it was like that, it would be an infinite loop because the value in CEEA seems to stay the same all the time.

Last edited by Miksy91 (2013-03-16 07:02:11)

Offline

#5 2013-03-16 15:33:39

Tauwasser
Member
Registered: 2010-10-16
Post 356/448

Re: VBlank interrupt and VRAM

You got most of it right.

Miksy91 wrote:

Well, dunno. But I can simply make this thing work by setting the register values like this (b:de = source; hl = destination; c = number of tiles / 0x10) and calling 0E72, correct?

c = number of tiles - or number of bytes / 0x10. It just depends on how you think about your data. 1 tile has 0x10 bytes.

Miksy91 wrote:

But basically, I should call 0E72 to check the bit 7 in LCD Control (FF40). And related to that, I can write data straight to VRAM if the screen is turned off, right? And this thing here is accomplished by jumping to 0DCD later in the code.

0x0E72 will check for you:

@0x0E72:
if (screen_is_on == true) then jump 0x0DFE
transform input arguments from 0xDFE-style to 0xDCD-style:
b:de = source; hl = destination; c = number of tiles => a:hl = source; de = destination; bc = number of bytes
jump 0x0DCD.
Miksy91 wrote:

Also, how does the code at 032E actually work? [...]
It's just that during that code, "nothing is written anywhere". Or so I see it because to me, it simply looks like a small routine used for waiting interrupts to occur, checking what the value in CEEA is and starting to wait for another interrupt to occur. This is obviously a wrong notion because if it was like that, it would be an infinite loop because the value in CEEA seems to stay the same all the time.

@150: vblank routine
push af
push bc
push de
push hl
ld a, [$FF00 + $A0]
and a, $07
ld e, a
ld d, $00
ld hl, @0170
add hl, de
add hl, de
ldi a, [hl]
ld h, [hl]
ld l, a
ld de, @0168
push de
jp [hl]
@0168:
call $1EFA
pop hl
pop de
pop bc
pop af
reti
@0170:
dw @0180
dw @01F4
dw @02B0
dw @02C4
dw @0255
dw @0278
dw @0180
dw @0180
@180: common vblank routine
ld hl, $FF9D
inc [hl]                 // increment vblank counter
ld a, [$FF00 + $04]
ld b, a
ld a, [$FF00 + $E3]
adc a, b
ld [$FF00 + $E3], a // random number generator
ld a, [$FF00 + $04]
ld b, a
ld a, [$FF00 + $E4]
sbc a, b
ld [$FF00 + $E4], a // random number generator
ld a, [$FF00 + $9F]
ld [$D155], a         // current RB backup
ld a, [$FF00 + $D1]
ld [$FF00 + $43], a
ld a, [$FF00 + $D2]
ld [$FF00 + $42], a // scroll register x/y transfer
ld a, [$FF00 + $D4]
ld [$FF00 + $4A], a
ld a, [$FF00 + $D3]
ld [$FF00 + $4B], a // window position x/y transfer
call $1458             // update tiles in direction player is moving on map
jr c, @01C2
call $0BDF             // cgb palette refresh
jr c, @01C2
call $14BB             // screen update service
call $15D0             // 2bpp tile transfer service
call $1579             // 1bpp tile transfer service
call $162B             // tileset animations
call $1642             // bg blanking service (write 0x60 to area around 12x14 active visible area)
@01C2:
ld a, [$FF00 + $DA] // OAM update switch
and a, a
jr nz, @01CA
call $FF80             // transfer OAM data
@01CA:
xor a, a
ld [$CEEA], a         // vblank flag is reset <====
ld a, [$CEE8]         // some software timers
and a, a
jr z, @01D8
dec a
ld [$CEE8], a
@01D8:
ld a, [$CEE9]
and a, a
jr z, @01E2
dec a
ld [$CEE9], a
@01E2:
call $08E6              // update button presses
ld a, $3A
rst $10
call $405C              // call 3A:405C: music
ld a, [$D155]
rst $10                  // restore original RB
ld a, [$FF00 + $9A]
ld [$FF00 + $E5], a // update second stable counter
ret
@1F4: PKMN fight vblank routine
ld a, [$FF00 + $9F]
ld [$D155], a         // current RB backup
ld a, [$FF00 + $D1]
ld [$FF00 + $43], a
ld a, [$FF00 + $D2]
ld [$FF00 + $42], a // scroll register x/y transfer
call @023E            // DMG/CGB color refresh
jr c, @020F
call $14BB             // screen update service
call $15D0             // 2bpp tile transfer service
call $FF80             // transfer OAM data
@020F:
ld a, [$FF00 + $C8]
or a, a
jr z, @0219
ld c, a
ld a, [$C700]
ld [$FF00 + c], a    // hw register update
@0219:
xor a, a
ld [$CEEA], a         // vblank flag is reset <====
ld a, [$FF00 + $0F]
ld b, a
xor a, a
ld [$FF00 + $0F], a
ld a, $02
ld [$FF00 + $FF], a
ld a, b
and a, $08
or a, $02
ld [$FF00 + $0F], a  // interrupt logic
ei
ld a, $3A
rst $10
call $405C              // call 3A:405C: music
ld a, [$D155]
rst $10                  // restore original RB
ld a, $1F
ld [$FF00 + $FF], a
ret

@023E: // DMG/CGB color refresh
ld a, [$FF00 + $E8]
and a, a
jp nz, $0BE3           // CGB color refresh
ld a, [$CF43]
ld [$FF00 + $47], a
ld a, [$CF44]
ld [$FF00 + $48], a
ld a, [$CF45]
ld [$FF00 + $49], a
and a, a
ret
@255: SIO announce vblank routine?
ld a, [$FF00 + $9F]
ld [$D155], a         // current RB backup
call $14BB             // screen update service
call $15D0             // 2bpp tile transfer service
call $FF80             // transfer OAM data
call $08E6              // update button presses
xor a, a
ld [$CEEA], a         // vblank flag is reset <====
call $1EBF             // SIO announce
ld a, $3A
rst $10
call $405C              // call 3A:405C: music
ld a, [$D155]
rst $10                  // restore original RB
ret
@278: ??? vblank routine
ld a, [$FF00 + $9F]
ld [$D155], a         // current RB backup
ld a, [$FF00 + $D1]
ld [$FF00 + $43], a // scroll register x transfer
call $0BDF             // cgb palette refresh
jr c, @028C
call $14BB             // screen update service
call $15D0             // 2bpp tile transfer service
@028C:
xor a, a
ld [$CEEA], a         // vblank flag is reset <====
call $08E6              // update button presses
xor a, a
ld [$FF00 + $0F], a
ld a, $02
ld [$FF00 + $FF], a
ld [$FF00 + $0F], a  // interrupt logic
ei
ld a, $3A
rst $10
call $405C              // call 3A:405C: music
ld a, [$D155]
rst $10                  // restore original RB
di
xor a, a
ld [$FF00 + $0F], a
ld a, $1F
ld [$FF00 + $FF], a
ret
@2B0: music-only vblank routine
ld a, [$FF00 + $9F]
ld [$D155], a         // current RB backup
ld a, $3A
rst $10
call $405C              // call 3A:405C: music
ld a, [$D155]
rst $10                  // restore original RB
xor a, a
ld [$CEEA], a         // vblank flag is reset <====
ret
@2C4: ??? vblank routine
ld a, [$FF00 + $9D]
inc a
ld [$FF00 + $9D], a // increment vblank counter
ld a, [$FF00 + $04]
ld b, a
ld a, [$FF00 + $E3]
adc a, b
ld [$FF00 + $E3], a // random number generator
ld a, [$FF00 + $04]
ld b, a
ld a, [$FF00 + $E4]
sbc a, b
ld [$FF00 + $E4], a // random number generator
call $08E6              // update button presses
ld a, [$FF00 + $9F]
ld [$D155], a         // current RB backup
ld a, [$FF00 + $D1]
ld [$FF00 + $43], a
ld a, [$FF00 + $D2]
ld [$FF00 + $42], a // scroll register x/y transfer
ld a, [$FF00 + $D4]
ld [$FF00 + $4A], a
ld a, [$FF00 + $D3]
ld [$FF00 + $4B], a // window position x/y transfer
call $14BB             // screen update service
call $1458             // update tiles in direction player is moving on map
call $15D0             // 2bpp tile transfer service
call $1579             // 1bpp tile transfer service
call $162B             // tileset animations
call $FF80             // transfer OAM data
xor a, a
ld [$CEEA], a         // vblank flag is reset <====
ld a, [$CEE9]         // software timer
and a, a
jr z, @0311
dec a
ld [$CEE9], a
@0311:
xor a, a
ld [$FF00 + $0F], a
ld a, $02
ld [$FF00 + $FF], a
ld [$FF00 + $0F], a  // interrupt logic
ei
ld a, $3A
rst $10
call $405C              // call 3A:405C: music
ld a, [$D155]
rst $10                  // restore original RB
di
xor a, a
ld [$FF00 + $0F], a
ld a, $1F
ld [$FF00 + $FF], a
ret

The routine @180 is the one you will mostly have to deal with. All the others are for special applications where regular screen updates aren't too important or handled by the user routine itself instead of the vblank routine.
As you see CEEA will be zero after vblank has occurred in all cases. The screen update service is called in most common routines as well.

cYa,

Tauwasser

Offline

#6 2013-03-16 17:34:15

Miksy91
Member
Registered: 2010-10-16
Post 1,568/2,311

Re: VBlank interrupt and VRAM

Okay, I got the thing working (props to you for that!) but still don't understand why.

For starting out, how do we access 0x150 ? Is program counter set to 0150 by default if the processor is not doing anything else? I'd assume not.
Also, what does "halt" do? Wait for "interrupts" to occur and execute data at 0x150 when they do ? If so, that would explain how the routine at 0x32E is related to this one. Still not even familiar if this sentence here makes any sense, it's kinda difficult to be talking about things you don't even understand in your own language in a foreign one.

But anyway, these are pretty much all the things that confuse me about this.

Last edited by Miksy91 (2013-03-16 17:34:39)

Offline

#7 2013-03-16 17:56:21

Tauwasser
Member
Registered: 2010-10-16
Post 358/448

Re: VBlank interrupt and VRAM

Oh, that's your confusion.

No, the halt command will halt the execution of any code. [Actually, there is a bug: halt will execute the following command twice if an interrupt is pending while halt is being executed. However, the PC will only be incremented one byte - which will destroy 16-bit opcodes. This is why halt should be followed by one or more nops.].

Now, halt halts the CPU to save power until an interrupt occurs. Interrupts are executed at specific locations called interrupt vectors. On the GBC, the interrupt vectors are as follows; priority descending:

0040       Vertical Blank Interrupt Start Address
0048       LCDC Status Interrupt Start Address
0050       Timer Overflow Interrupt Start Address
0058       Serial Transfer Completion Interrupt Start Address
0060       High-to-Low of P10-P13 Interrupt Start Address

So when 0x032E is executed, it will halt the cpu, i.e. wait for any interrupt. When a vblank interrupt occurs, the CPU will start processing the Interrupt Service Routine (ISR) code at 0x0040, which is a jp 0x0150. This sets the vblank flag, so once the interrupt returns, the routine at 0x032E can check if a vblank interrupt woke it or if it was some kind of different interrupt:

pc   instruction   [CEEA]
032e  ld a, 01      ??
0330  ld (CEEA), a  01
0333  halt          01
0333  halt          01
0333  halt          01
0333  halt          01
0333  halt          01
-- timer interrupt --
0050  reti          01
0335  ld a,(CEEA)   01
-- vblank interrupt --
0040  jp 150        01
...   ...           01
01CA  xor a,a       01
01CB  ld (CEEA), a  00
...   ...
016F  reti          00
0338  and a, a      00
0339  jr nz, 0333   00
-- too bad, we missed vblank by two cycles! --
0333  halt          00
0333  halt          00
0333  halt          00
-- any interrupt --
...   reti          00
0335  ld a,(CEEA)   00
0338  and a, a      00
0339  jr nz, 0333   00
033B  ret
-- we waited for at least one vblank --

This is what one trace of the running code might look like :)

Notice that man modern CPUs/µC have a VIC - a Vectored Interrupt Controller, where the vectors are not fixed like for GBC. Also, the GBC will only serve the most recent interrupt. However, modern hardware knows multiple systems: IRQs and FIQs: IRQs are handled in the order in which they appear and can be interrupted by FIQs, which may only be served once or may also be queued. However, there are also hierarchical interrupts, which generally allow for interrupts interrupting other interrupt service routines. Also, when two interrupts are generated at exactly the same clock cycle, there is usually a hierarchy in place to determine which interrupt is "more important" and gets served (at all or first).
You should always be aware of the platform you're dealing with, because it's pretty easy to create a live-lock due to unforeseen situations.

cYa,

Tauwasser

Last edited by Tauwasser (2013-03-16 18:06:28)

Offline

#8 2013-03-17 07:50:04

Miksy91
Member
Registered: 2010-10-16
Post 1,569/2,311

Re: VBlank interrupt and VRAM

Okay, I've got it somehow. This thing now also explains why I've noticed bgb acting in an "odd" way during debugging sequences, starting to execute data at those interrupt addresses although the code was never told to "call" anything from there.

Well, this thing still ain't that clear to me but I think I know everything I need for now. After all, I can move forward with what I've got at the moment and it shouldn't be too hard figuring this stuff out later, especially since I started to take computer science courses at the uni recently.

Offline

Board footer

Powered by FluxBB