Platform Specific Series
In this series we'll take a look at hardware related tasks Lesson
P1 - Joystick Reading on the N64 lets look at reading from the joystick on the N64 |
N64_JoystickExample.asm |
Reading the joystick
We're going to read in from the Joystick ports. We'll load in 64 bytes from the serial controller - there are 8 bytes which will be returned from each of the four joysticks. In the screenshot shown UDLR have been pressed on all four controllers Each controller returns the button states and analog data |
|
The N64 controllers are connected via the so called 'PIF' chip
(Peripheral Interface?) The base of the PIF registers is at address 0xBFC007C0 To initialize things we first write 0x8 to the status register at 0xBFC007FC |
|
We're going to use the serial interface (SI) to send data to
the PIF, This is basically a DMA copy, which sends a block of data. We need to prepare a 96 byte block of data to send to the PIF to initialize things. We also define 8 bytes to get back the results when we do a read! |
|
OK we need to use the SI to Send the INIT code to the PIF The SI registers are at 0xA4800000+ We need to calculate our source address (PIF_Init) - but we need the true 'hardware address' we can get this by ANDing with 0x1FFFFFFF We send the source address to 0xA4800000 (SI_DRAM_ADDR_REG) We want to transfer to the PIF ram at 0xBFC007C0 (PIF_RAM_START) - we also AND this with 0x1FFFFFFF We write the destination to the write register at 0xA4800010 (SI_PIF_ADDR_WR64B_REG) - this write also starts the transfer |
|
OK Let's actually get the keypresses from the joypads! This time we use the SI to READ from the PIF. We need to calculate our destination address (PIF_JoyState) - but we need the true 'hardware address' we can get this by ANDing with 0x1FFFFFFF We send the destination address to 0xA4800000 (SI_DRAM_ADDR_REG) - this is the same as the address for writing This time we want to transfer FROM to the PIF ram at 0xBFC007C0 (PIF_RAM_START) - we also AND this with 0x1FFFFFFF We write the source to the read register 0xA4800004 (SI_PIF_ADDR_RD64B_REG) - this write also starts the transfer |
|
As a test we dump with our memdump sub There are 8 bytes returned, the first 4 are status, the next 4 are directions and buttons in the format: %ABZSUDLR --LRUDLR XXXXXXXX YYYYYYYY AB - A/B Buttons ZLR - Shoulder buttons / Triggers UDLR - Digital Directions UDLR - C Pad XXXXXXXX YYYYYYYY - Analog stick S - Start |
The SI makes getting the directions easy! We're defining 'PIF_JoyState' in our ROM, but it's changed during the program, that's because the ROM is actually copied to RAM at execution time by the firmware, so it's actually changeable at run time! |
Lesson
P2 - Joystick Reading on the PSX lets look at reading from the joystick on the Playstation |
PSX_JoystickExample.asm |
Reading the joystick via the Bios
The easiest way to read in the joypad is via the Bios functions. Here we're pressing the buttons on Joypad 1, and showing the results to the screen. In theory Joypad 2 should be shown as well, but it seems maybe there's an issue with our emulator which stops them working. |
||||||||||||||||||||||||||||
We're going to execute the bios functions at address 0xB0 (the so
called 'B functions') We need to call this with a function number in T1 - Here we use 0x12 which is 'Init Pad' This takes 4 parameters in A0-A3 A0 is the address of the buffer for joypad 1, A1 is the size (Should be 0x22) A2 is the address of the buffer for joypad 2, A3 is the size (Should be 0x22) |
||||||||||||||||||||||||||||
Once we have Initialized the joypad we can start the read process. We use Bios function 0x13 'Startpad' - which takes no parameters. This will automatically fill the buffer with the joypad data. |
||||||||||||||||||||||||||||
Here is a crude test program, to read in the raw joypad data and
show it to the screen! The direction buttons have the following bits
|
Reading the joystick Directly!
If we're feeling masochistic we can read from the hardware
directly! We'll need to use various hardware ports to control the hardware. 0x1F801040 JOY_DATA 0x1F801044 JOY_STAT 0x1F801048 JOY_MODE 0x1F80104A JOY_CTRL 0x1F80104E JOY_BAUD |
|
We point T1 to the base address of the controller ports. First we reset the control port, via 0x1F80104A We have a small delay to ensure the hardware has time to process the change. |
|
Next we set the Baud rate and mode We then select a joystick via the CTRL port. |
|
We send and receive a total of 9 bytes from the joypad We need to send two init bytes to the control port... 0x01 and 0x42 We use JoyWait to wait for a reply, and MonitorJoy to show the returned data and status flags. |
|
JoyWait will read in from the status port, and wait for bit 1 to
equal one, this implies a response from the joypad. MonitorJoy is for our testing, it shows the returned data in A0, and status byte in A1 to the screen. |
|
We can now read back the data bytes. We repeatedly send a 0x0 byte, and read back the response, this will be a set of digital buttons or an analog axis. |
What Is NativeSprite?
Nativesprite allows us to use platform specific sprite
capabilities, to form 'objects' (grids of sprites which can be
used in the same way on all systems. This allows us to write a multiplatform program, but gain the benefits of the hardware capabilities. Nativesprite splits the job of drawing sprites into three parts. |
|
Part 1 is
multiplatform, it is a list of NativeSprite objects with X,Y
co-ordinates (in logical units - Pairs of Pixels) Our game can change the co-ordinates and sprite object pointers to change in-game graphics in a multiplatform way. With ChibiVM, the 16 bit pointer is a 'numbered entry' in the AddressRemapTable. This is to allow 16 bit ChibiVM to address the 32 bit address space of the MIPS |
|
Part 2 is platform
specific, and defines the sprite pattern grid, This defines how the object is made up of hardware sprite patterns. It also offers platform specific functions, like sprite scaling or palettes where available. On systems where no Hardware sprites are available, XOR software sprites are used (made up of 8x8 tiles)... XOR is used because it means we don't need to worry about redrawing the background. On these XOR systems, we define a 16 bit 'pattern number' for each 8x8 pixel 'tile', which is used to calculate an offset within the bitmap pattern data. Part 3 is also platform specific. This is the bitmap pattern data used to draw the sprite, in the format of the hardware sprites or screen memory. |
We need to define some memory for our variables. nativespriteaddr is the address of the tile pattern bitmap data nativespriteactivebuffer is the settings of the current sprites which have been drawn to the screen, we need this to 'remember' what sprites are there, so we can remove them if they move or change. Because we're using XOR, we just 'redraw' a sprite in it's old position to remove it. On the PSX we cannot access VRAM directly so we must transfer an 8x8 block FROM vram to regular RAM, XOR it and copy it back to VRAM, we use NativeSprite_XORbuffer for this 'temporary store |
|
NativeSpr_Init
will prepare the system for drawing sprites. On hardware sprite systems, we transfer patterns to VRAM On the XOR sprite systems we just remember the address we were passed, which contains the bitmap 'tile pattern' data. |
|
NativeSpr_DrawArray
draws the array of sprite objects defined in Part1 The Co-ordinates of the sprite (in pairs of pixels) and the pointer to the sprite object (part 2) There are two versions: NativeSpr_DrawArrayReiKou is used with ChibiVM, and uses the AddressRemapTable to convert a 16 bit pointer to 32 bits. NativeSpr_DrawArray uses a 32 bit pointer... If you're using NativeSprite on it's own, this is the one you would use. On XOR based systems nativespriteactivebuffer keeps a copy of the positions and sprite objects drawn to the screen, this is used to remove a sprite (VIA XOR - simply redrawing it in the same position) if the position or object changes. We use NativeSpr_DrawOne to draw the sprite to the screen. |
|
NativeSpr_TestOne
Checks the 'ActiveBuffer' to see if the XOR sprite is already
drawn to the screen at this position If it is it doesn't need redrawing If it has moved or changed, we remove the old sprite (by XOR drawing it in the same position) then draw the new sprite. |
|
Here is the correct Object definition
for the XOR systems The Width and Height in patterns is defined in the first two words. On these XOR systems, we define a 32 bit 'pattern number' for each 8x8 pixel 'tile', which is used to calculate an offset within the bitmap pattern data. |
|
NativeSpr_drawone
will load the parameters of the sprite object to draw loading the
X,Y position into S1,S4,and the object definition address into S5 |
|
NativeSpr_DrawExtra
will draw object S5 at position S1,S4 We load in the Width and height of the object (in 8x8 pattern tiles) We load in the pattern number, and see if it's 0xFFFFFFFF (Which is an 'empty pattern' We convert the X,Y co-ordinate from 'Logical units' (pairs of pixels making a centered virtual screen of 128,96 from co-ordinate 64,80) to a screen co-ordinate in pairs of pixels We then calculate the source pattern address for the tile patterns. On these systems each pattern is 128 bytes. |
|
On the Playstation we can't access VRAM directly. We send commands to 0x1F8001810 to get the video hardware to work for us. We have to use command 0xC0 'Get Image from Framebuffer' to transfer an 8x8 block into RAM (NativeSprite_XORbuffer) We then XOR the source pattern into the buffer. Finally we write it back to screen with command 0xA0 'Send image to Framebuffer' |
|
We repeat for the next tile on the line, and the
other lines that make up this 'spriteobject' drawnative_overrow Handles when part of our spriteobject has gone off the right of the screen. drawnativeskipx Handles when part of the spriteobject is off the left of the screen, or contains empty patterns |
|
NativeSpr_HideAll
will remove all the hardware sprites from the screen, and should
be used for things like title screens. It should also be used
before redrawing the background with XOR sprites. With XOR sprites, we walk through the ActiveSpriteBuffer, redrawing each sprite to the screen, which effectively removes it (as we're using XOR) |
What Is NativeSprite?
Nativesprite allows us to use platform specific sprite
capabilities, to form 'objects' (grids of sprites which can be
used in the same way on all systems. This allows us to write a multiplatform program, but gain the benefits of the hardware capabilities. Nativesprite splits the job of drawing sprites into three parts. |
|
Part 1 is
multiplatform, it is a list of NativeSprite objects with X,Y
co-ordinates (in logical units - Pairs of Pixels) Our game can change the co-ordinates and sprite object pointers to change in-game graphics in a multiplatform way. With ChibiVM, the 16 bit pointer is a 'numbered entry' in the AddressRemapTable. This is to allow 16 bit ChibiVM to address the 32 bit address space of the MIPS |
|
Part 2 is platform
specific, and defines the sprite pattern grid, This defines how the object is made up of hardware sprite patterns. It also offers platform specific functions, like sprite scaling or palettes where available. On systems where no Hardware sprites are available, XOR software sprites are used (made up of 8x8 tiles)... XOR is used because it means we don't need to worry about redrawing the background. On these XOR systems, we define a 16 bit 'pattern number' for each 8x8 pixel 'tile', which is used to calculate an offset within the bitmap pattern data. Part 3 is also platform specific. This is the bitmap pattern data used to draw the sprite, in the format of the hardware sprites or screen memory. |
We need to define some memory for our variables. nativespriteaddr is the address of the tile pattern bitmap data nativespriteactivebuffer is the settings of the current sprites which have been drawn to the screen, we need this to 'remember' what sprites are there, so we can remove them if they move or change. Because we're using XOR, we just 'redraw' a sprite in it's old position to remove it. |
|
NativeSpr_Init
will prepare the system for drawing sprites. On hardware sprite systems, we transfer patterns to VRAM On the XOR sprite systems we just remember the address we were passed, which contains the bitmap 'tile pattern' data. |
|
NativeSpr_DrawArray
draws the array of sprite objects defined in Part1 The Co-ordinates of the sprite (in pairs of pixels) and the pointer to the sprite object (part 2) There are two versions: NativeSpr_DrawArrayReiKou is used with ChibiVM, and uses the AddressRemapTable to convert a 16 bit pointer to 32 bits. NativeSpr_DrawArray uses a 32 bit pointer... If you're using NativeSprite on it's own, this is the one you would use. On XOR based systems nativespriteactivebuffer keeps a copy of the positions and sprite objects drawn to the screen, this is used to remove a sprite (VIA XOR - simply redrawing it in the same position) if the position or object changes. We use NativeSpr_DrawOne to draw the sprite to the screen. |
|
NativeSpr_TestOne
Checks the 'ActiveBuffer' to see if the XOR sprite is already
drawn to the screen at this position If it is it doesn't need redrawing If it has moved or changed, we remove the old sprite (by XOR drawing it in the same position) then draw the new sprite. |
|
Here is the correct Object definition
for the XOR systems The Width and Height in patterns is defined in the first two words. On these XOR systems, we define a 32 bit 'pattern number' for each 8x8 pixel 'tile', which is used to calculate an offset within the bitmap pattern data. |
|
NativeSpr_drawone
will load the parameters of the sprite object to draw loading the
X,Y position into S1,S4,and the object definition address into S5 |
|
NativeSpr_DrawExtra
will draw object S5 at position S1,S4 We load in the Width and height of the object (in 8x8 pattern tiles) We load in the pattern number, and see if it's 0xFFFFFFFF (Which is an 'empty pattern' We convert the X,Y co-ordinate from 'Logical units' (pairs of pixels making a centered virtual screen of 128,96 from co-ordinate 64,80) to a screen co-ordinate in pairs of pixels |
|
Next we calculate the VRAM Destination. Each line on the N64 screen is 320 pixels, and each pixel is 2 bytes. We also calculate the source pattern address for the tile patterns. On these systems each pattern is 128 bytes. |
|
We read 4 bytes (2 pixels) from the source pattern,
xor them with the screen contents, and write back to the screen. We do this 4 times to write a full line, then move down a line (640 bytes - the 16 we just wrote) |
|
We repeat for the next tile on the line, and the
other lines that make up this 'spriteobject' drawnative_overrow Handles when part of our spriteobject has gone off the right of the screen. drawnativeskipx Handles when part of the spriteobject is off the left of the screen, or contains empty patterns |
|
NativeSpr_HideAll
will remove all the hardware sprites from the screen, and should
be used for things like title screens. It should also be used
before redrawing the background with XOR sprites. With XOR sprites, we walk through the ActiveSpriteBuffer, redrawing each sprite to the screen, which effectively removes it (as we're using XOR) |
What Is Multiplatform Bitmap?
Multiplaform bitmap allows us to draw graphics in a common way,
whatever the capabilities of the system! It's intended for low speed drawing or real-time calculated drawing, like drawing title screens, or graphs, or even making a sprite editor or other graphics package! There are 4 functions provided: mpbitmap_setpixel - Set a pixel to a color (0-15) mpbitmap_getpixel - Reads a pixel from the screen mpbitmap_settile - Set an 8x8 pixel tile block (in bitplane format, 1-4 bitplanes) mpbitmap_gettile - Get an 8x8 pixel tile block from the screen (in bitplane format, 1-4 bitplanes) |
Pixel Commands
mpbitmap_setpixel draws a pixel to
the screen The color to set is specified in S0 The Y position in pixels is specified in S5 The Low byte of the X position is specified in S2 The High byte of the X position is specified in S6 (The X position is split into two registers to maintain functional compatibility with the Z80 Version!) To write a pixel to the screen we need to use video command '0xA0' - send image to framebuffer We use the GP0 command port 0x1F801810 to define the command, XY pos, and image size The 'image' in this case is a single 1x1 pixel!!! We convert our numbered pixel (0-15) into the native screen format (%-BBBBBGGGGGRRRRR) We then send it to the screen in the low 16 bits of the word written to the command port |
|
The mpbitmap_getpixel
function is almost the same, we use command '0xC0' to read back a
byte from the screen. We use mpbitmap_getpixel_FindColor to convert the 16 bits read from the screen (in %-BBBBBGGGGGRRRRR format) into a color number (0-15) ... we do this for compatibility with the other systems (like the Z80 and 6502, were even 16 colors are a luxury!) |
|
mpbitmap_getpixel_FindColor will
take a 16 bit value read from the screen, and convert it to a
numbered color using the 'PSX_Palette' We seek entries in the palette, until we find a match, or go through all 16 colors. The 16 color limit is to maintain compatibility with the other systems Multiplatform Bitmap supports. |
Tile Commands
The 'mpbitmap_settile'
command will take an 8x8 block of data in bitplane format, and draw
it to the screen. Up to 4 bitplanes are supported, meaning 16 colors, 32 bytes for the full tile. We start by selecting the VRAM destination using GPU command 0xA0. Next we load in the bitplanes we've been provided with, The source data may be less than 4 bitpanes, so we load any others from the default 'MpBitmap_TileTints' |
|
To calculate the color number we take the most
significant bit from each bitplane byte. We use this as a lookup in our palette, giving us a 16 bit screen-format color. We want to write this to the screen, but we need two pixels of data before we do, so we build them up into a3, until we have a pair, then send them to the screen |
|
mpbitmap_gettile will
read pixel data back from the screen, returning 1-4 bitplanes as
required. We read data back from the screen using the 0xC0 command. Each read must load 2 pixels, so we do one read every two pixels. We load in 8 pixels, convert the colors, and shift the bitplane bits into 4 bytes. |
|
We store the data back to address S6, writing back as
many bitplanes as we were asked to (originally specified by S0 - now
in A3) |
What Is Multiplatform Bitmap?
Multiplaform bitmap allows us to draw graphics in a common way,
whatever the capabilities of the system! It's intended for low speed drawing or real-time calculated drawing, like drawing title screens, or graphs, or even making a sprite editor or other graphics package! There are 4 functions provided: mpbitmap_setpixel - Set a pixel to a color (0-15) mpbitmap_getpixel - Reads a pixel from the screen mpbitmap_settile - Set an 8x8 pixel tile block (in bitplane format, 1-4 bitplanes) mpbitmap_gettile - Get an 8x8 pixel tile block from the screen (in bitplane format, 1-4 bitplanes) |
Pixel Commands
We need to calculate VRAM destinations. Screen memory starts at 0xA0100000, Each pixel is 2 bytes in the format %RRRRRGGGGGBBBBBB- Each line is 320 pixels. We have two routines to do this mpbitmap_GetScreenPos works in pixels mpbitmap_GetScreenPosTile works in 8x8 tile blocks they also load A0 with the address of the palette (N64_Palette) |
|
mpbitmap_setpixel
draws a pixel to the screen The color to set is specified in S0 The Y position in pixels is specified in S5 The Low byte of the X position is specified in S2 The High byte of the X position is specified in S6 (The X position is split into two registers to maintain functional compatibility with the Z80 Version!) We convert our numbered pixel (0-15) into the native screen format %RRRRRGGGGGBBBBBB- using our palette lookup We then write it to the screen. |
|
The mpbitmap_getpixel
function works in reverse. We load the pixel from the screen, We then use mpbitmap_getpixel_FindColor to convert the 16 bits read from the screen (in %RRRRRGGGGGBBBBBB- format) into a color number (0-15) ... We do this for compatibility with the other systems (like the Z80 and 6502, were even 16 colors are a luxury!) |
|
mpbitmap_getpixel_FindColor
will take a 16 bit value read from the screen, and convert it to a
numbered color using the 'N64_Palette' We seek entries in the palette, until we find a match, or go through all 16 colors. The 16 color limit is to maintain compatibility with the other systems Multiplatform Bitmap supports. |
Tile Commands
The 'mpbitmap_settile'
command will take an 8x8 block of data in bitplane format, and draw
it to the screen. Up to 4 bitplanes are supported, meaning 16 colors, 32 bytes for the full tile. We start by calculating the VRAM destination. Next we load in the bitplanes we've been provided with, The source data may be less than 4 bitpanes, so we load any others from the default 'MpBitmap_TileTints' |
|
To calculate the color number we take the most
significant bit from each bitplane byte. We use this as a lookup in our palette, giving us a 16 bit screen-format color. We write the 16 bit value to the screen (1 pixel). At the end of each line we add 320*2 to the VRAM position, as each line is 320 pixels, and 2 bytes per pixel |
|
mpbitmap_gettile will read pixel data back from the
screen, returning 1-4 bitplanes as required. We read data back from the screen, converting it via mpbitmap_getpixel_FindColor We load in 8 pixels, and shift the bitplane bits into 4 bytes. |
|
We store the data back to address S6, writing back as
many bitplanes as we were asked to (originally specified by S0 - now
in A3) |