LaC's n64 hardware dox... v0.5 (public release) --------------- release notes --------------- This little doc is my attempt to make the n64 coding scene a little better I hope. It is a compilation of stuff i've been working on for some months... a result of alot of reversing of several games compiled with libultra, and some help from certain people. It is mainly for people who wish to code without using a developement library (like libultra). It is specfically for doing intros,trainers, etc... to attach to roms. It is very simplistic, and assumes you have some knowledge already. In other words this doc isnt really meant for people just starting. You should probably have read the libultra docs and be familiar with the n64 hardware... and the purpose of things like the AI,PI,SI,VI etc... I suppose some emulation author could find use of this too. Some people will probably be mad I released this doc, but I guess since nothing is really happening in the n64 scene it might get things moving? Especially from people that really want to hack. Also I have noticed some really crappy and inaccurate info being released which basically looks like stuff ripped from czn intros or something and contains no real knowledge. Yes all you freedom of information people... you want info... hack it yourself. Or read this doc, then hack some more ;) Also I can't guarantee the 100% accuracy of any of this document. Also I liked to be greeted if you are using my dox or my source code in your work. NOTE: SOME symbols in the follow text will reference symbols #define'd in RCP.H or r4300.h so make sure you look there when you are confused. Yes I know t hey are part of the standard devkit and some people dont have it... but i'm sure you can find these files if you really need to. -VI (video interface) Accessing the video on the n64 is very easy (like most things in this doc) * initialization: The video hardware is initialized by simply writing all the necessary values to the vi regs. I'm only going to discuss one mode here, but u can easily find the values for other modes by just printing out reg values to the screen after initing your favorite mode with libultra. You can also alter values of course to make your own modes. That I will not discuss here. The one i'm discussing is the simple 320x240 RGBA 16bit non-antialiasing mode. The base address of the VI regs are mapped at 0xa4400000, so to simply write a value to a reg in r4300 asm would be like this: ;;this is just an example but it happens to be the write to the ;;VI_H_WIDTH_REG defined in RCP.H lui at,0xa440 ;at=0xa4400000 li t0,0x140 ;t0=0x140 sw t0,0x8(at) ;write t0 to reg at $at+0x8 in C: IO_WRITE(VI_H_WIDTH_REG,0x140); //IO_WRITE and IO_READ are in r4300.h Ok to initialize this mode here are the values to write for each reg: -> (VI_CONTROL_REG, 0x0000320e) -> (VI_DRAM_ADDR_REG,0) -> (VI_H_WIDTH_REG,0x140) -> (VI_V_INTR_REG,0x2) -> (VI_V_CURRENT_LINE_REG,0x0) -> (VI_TIMING_REG,0x03e52239) -> (VI_V_SYNC_REG,0x0000020d) -> (VI_H_SYNC_REG,0x00000c15) -> (VI_H_SYNC_LEAP_REG,0x0c150c15) -> (VI_H_VIDEO_REG,0x006c02ec) -> (VI_V_VIDEO_REG,0x002501ff) -> (VI_V_BURST_REG,0x000e0204) -> (VI_X_SCALE_REG,0x200); -> (VI_Y_SCALE_REG,0x01000400) All the values are pretty obvious and dont need much explaining. And with a little hacking you can come up with your own modes. If you've managed to get this far you know that the video screen is just showing zebra stripes or some garbage. The reason for this is you have not set the frame buffer for this video mode. This is what osViSwapBuffer() is for (if you have used libultra). to set your frame buffer simply write the rdram address of the buffer to VI_DRAM_ADDR_REG or just change the init code to have it set it up. Now you can simply write your graphics to the buffer and they'll be updated. NOTE: There is ALOT of other things you can tweek, change, by messing with the VI regs... mess around... figure it out. Also note that there can be cache problems with the video if you are using a virtual address like 0x80200000 in your VI_DRAM_ADDR_REG, try using the physical address 0xa0200000 instead or make sure you are writing back and invalidatiing the cache lines. if you are confused about physical and virtual addresses or how the cache works, read the r4300 man... and if you still don't understand it totally, join the club. * end init * vertical retrace wait: pseudo code: while ( VI_CURRENT_REG != 512 ) {wait} // I got this value from bpoint * end retrace wait -AI (audio interface) The audio interface is probably the easiest thing to do. * initialization: None needed. Altho I suppose you can include setting the sound frequency and/or enabling the AI_CONTROL_REG as initialization (step #4 below) * end init * setting frequency: 1. Set the dac rate write to AI_DACRATE_REG this value: (VI_NTSC_CLOCK/freq)-1 ^could be pal 2. Set the bitrate (4bit, 8bit, or 16bit) write to the AI_BITRATE_REG the value: bitrate-1 * end frequency * sending sound buffer 1. Before sending a buffer, its a good idea to make sure one isnt already being played. Simply read AI_STATUS_REG then AND it with AI_STATUS_FIFO_FULL. if the result is true... then wait. 2. Write the 64bit aligned address of the sound buffer to AI_DRAM_ADDR_REG 3. Write the length of the buffer be played to AI_LEN_REG NOTE: this length must be multiple of 8, no larger than 262144 bytes. 4. Write AI_CONTROL_DMA_ON to the AI_CONTROL_REG (only needed once) * end sound buffer NOTE: If you have read the libultra manuals you will notice when it discusses the AI routines (osAi.man) it says the AI regs are double buffered. This is important to realize that you can send one buffer while another is playing. Also note that the AI_LEN_REG counts down to 0 as the current buffer is being played. This can be useful to tell how much of the buffer is left. -PI (peripheral interface) * init pif: ( you should do this before you do anything! ) 1. Simply write 0x8 to PIF_RAM_START+0x3c * end init pif * PI DMA transfer 1. Wait for previous dma transfer to finish (see next explanation) 2. Write the physical dram address of the dma transfer to PI_DRAM_ADDR_REG NOTE: To convert from a virtual address to physical, simply AND the address with 0x1fffffff. 3. Write the physical cart address to PI_CART_ADDR_REG. 4. Write the length-1 of the dma transfer to PI_WR_LEN_REG this is from cart->rdram change this to RD ^ for the other way also note you must write a 0x2 to PI_STATUS_REG in order to write to the cart space (0xb0000000) NOTE: The cart addr must be 2 byte aligned, and the rdram addres must 8-byte aligned. Once again make sure you write back the cache lines and invalidate the cache lines if needed, or you will run into trouble. * end PI DMA transfer * PI DMA wait 1. Read PI_STATUS_REG then AND it with 0x3, if its true... then wait until it is not true. NOTE: Look at RCP.H for more information on the PI_STATUS_REG and the PI in general. * end PI DMA wait -reading and writing the new sram chip (DS1) - Sram is mapped at 0xa8000000. The trick is that you cannot write to it directly, you must us the PI. Actually it is possible to write to it directly, but I dont know how because it needs to be timed carefully. Its a little tricky which requires writing some values into some PI regs to initialize the PI correctly for the type of transfer protocol the sram needs for successful data transfer. * Init the PI for sram 1. write 0x05 to the PI_BSD_DOM2_LAT_REG. 2. write 0x0c to the PI_BSD_DOM2_PWD_REG. 3. write 0x0d to the PI_BSD_DOM2_PGS_REG. 4. write 0x02 to the PI_BSD_DOM2_RLS_REG. * End init PI for sram Now you should be able to use the PI to transfer between rdram and sram. (refer to the dox above concerning PI, but replace the ROM address with sram address 0xa8000000). -SI (serial interface) The SI is very similar to the PI for obvious reasons. It is used mainly for accessing the joyport and pifram... which will be dicussed in the next section. * SI DMA transfer 1. Wait for previous dma transfer to finish (see next explanation) 2. Write the physical dram address of the dma transfer to SI_DRAM_ADDR_REG 3. Write PIF_RAM_START to the SI_PIF_ADDR_RD64B_REG or the SI_PIF_ADDR_WR64B_REG, depending on what you wish to do (read or write). This will cause a 64B read or write between pif_ram and rdram. NOTE: The SI addr must be 2 byte aligned, and the rdram addres must 8-byte aligned. Once again make sure you write back the cache lines and invalidate the cache lines if needed, or you will run into trouble. * end SI DMA tranfer * SI DMA wait 1. Read SI_STATUS_REG then AND it with 0x3, if its true... then wait until it is not true. NOTE: Look at RCP.H for more information on the SI_STATUS_REG and the SI in general. * end SI DMA wait -PIF Usage- (controller reading/detection) If you have done research and peeked at the RCP.h file you should already know some things about the pif. The SI is used to send commands to the pif ram that tell the pif what to do. The SI is also used to read the results of those commands back. You can tell the pif to do alot of stuff. for instance... reading joysticks, reading mempacks, detecting joysticks, detecting mempacks, activating the rumblepack, detecting the rumble pack, reading cartridge eeprom... etc. In this version of the document I will only cover reading joysticks and detection. Below is a very brief and not so detailed view of pif command structure and an example of using them to perform some operations. first this is how pif ram is setup: [64byte block] at 0xbfc007c0 (1fc007c0) { command data recv channel 1 - 00 00 00 00 : 00 00 00 00 - 8 bytes channel 2 - 00 00 00 00 : 00 00 00 00 - 8 bytes channel 3 - 00 00 00 00 : 00 00 00 00 - 8 bytes channel 4 - 00 00 00 00 : 00 00 00 00 - 8 bytes channel 5 - 00 00 00 00 : 00 00 00 00 - 8 bytes 00 00 00 00 : 00 00 00 00 - 8 bytes (dummy data) 00 00 00 00 : 00 00 00 00 - 8 bytes (dummy data) 00 00 00 00 : 00 00 00 00 - 8 bytes (dummy data) } ^^pif status control byte This is how you should visualize it for operations I describe below. For other stuff it is setup differently... but in all cases it is just 64 bytes. Each channel can contain a command in the first 4 bytes (the left column). Each command has a structure like so: byte 1 = 0xff for new command | 0xfe for end of commands byte 2 = number of bytes to send byte 3 = number of bytes to recieve byte 4 = Command Type Command Types: 00 = get status 01 = read button values 02 = read from mempack 03 = write to mempack ff = reset controller 04 = read eeprom 05 = write eeprom Here is an example on how to build a command for reading a joystick: * Init the joysticks for reading send the pif command block to pif_ram using the SI DMA -----------------------------++------------------------------ such a block to read 4 joys: || such a block to read 1 joy: [64byte block] || [64byte block] { command data || { joy1 ff010401 - ffffffff || joy1 ff010401 - ffffffff joy2 ff010401 - ffffffff || 00000000 - ffffffff joy3 ff010401 - ffffffff || 00000000 - ffffffff joy4 ff010401 - ffffffff || 00000000 - ffffffff fe000000 - 00000000 || fe000000 - 00000000 00000000 - 00000000 || 00000000 - 00000000 00000000 - 00000000 || 00000000 - 00000000 00000000 - 00000001 || 00000000 - 00000001 } || } ----------------------------++------------------------------ after sending this the joystick values will now be updated in pif RAM NOTE: make sure you put the ffffffff in the data column, otherwise it will cause errors. ff010401 is the command that reads the joystick values. | ff is basically a flag for a new command. 01 says we are going to send 1 byte (the command type). 04 says we are going to read 4 bytes (into the data column) 01 is the command type (read button values). You will notice the 5th channel command is fe, this command signals the end of the command block. The 00000001 tells the pif there is a new command block to be processed. Without this the command block will not be executed. * end Init joysticks * Read Joysticks The joy values can be read from the spaces marked by 0xFFFFFFFF in the block above. Of course you must first DMA from pif ram back to rdram. Or you can just read the data directly by making a pointer to 0xbfc007c0 (start of the pif_ram), although I would not recommend that method. Here would be a sufficient C code to read in a controller's values: void siReadJoy(int cont,OSContPad *p) { unsigned char pif_block[64]; si_DMA_from_pif (pif_block); memcpy (p,pif_block+((cont*8)+4),4); } The OSContPad structure is in the libultra header file OS.H * end Read Joysticks * Detecting if Joysticks are connected This is very easy and can be done after you send any command to read or write something to the controllers. Whenever you try and execute a command on a channel and that device on the channel (like a joystick) is not present the pif will write an error value to the command column that the error occured in. For instance... lets say you did the example above and you tried to read controller values. Well if you tried to read the controller values for all four joystick channels you will notice that if you don't have a joystick physically plugged in to the port(s) you are reading from, then no values will appear. Well I think this is an obvious result. But also notice that the pif will put an error value into the upper 4 bits of the 3rd byte in the command column. The Error values are as follows: 0 - no error, operation successful. 8 - error, device not present for specified command. 4 - error, unable to send/recieve the number bytes for command type. This would be an example of the result of trying to read 4 controllers (like in above example) and only a joystick in port 3 is connected: -----------------------------------+ [64byte block] read from pif ram | { command data | joy1 ff018401 - ffffffff <--- 8 is the error code for device joy2 ff018401 - ffffffff | not present. joy3 ff010401 - 00000000 <--- read was successful on this joy4 ff018401 - ffffffff | channel, no buttons being pressed fe000000 - 00000000 | 00000000 - 00000000 | 00000000 - 00000000 | 00000000 - 00000000 | } | -----------------------------------+ This would be an example of the result of trying to read 5 bytes for the read joystick command: (all 4 joysticks are connected) -----------------------------------+ [64byte block] sent to pif ram | { command data | joy1 ff010501 - ffffffff <--- joy2 ff010501 - ffffffff <--- note we tried to read 5 instead joy3 ff010501 - ffffffff <--- of 4. The device only allows you joy4 ff010501 - ffffffff <--- to read 4 bytes with that command fe000000 - 00000000 | 00000000 - 00000000 | 00000000 - 00000000 | 00000000 - 00000001 | } | -----------------------------------+ -----------------------------------+ [64byte block] read from pif ram | { command data | joy1 ff014501 - 00000000 <--- (note that no buttons are being joy2 ff014501 - 00000000 <--- pressed on any controller) joy3 ff014501 - 00000000 <--- notice the 4. It is the error joy4 ff014501 - 00000000 <--- code for send/recieve. fe000000 - 00000000 | 00000000 - 00000000 | 00000000 - 00000000 | 00000000 - 00000000 | } | ----------------------------------+ NOTE: Even though we tried to read an extra byte for the buttons values the button values will still appear... but the error code will still be generated because there is only 4 bytes to be read, not 5. * end Detecting if Joysticks are connected * Getting controller status ff010300 is the command used to get the controller status. | ff is basically a flag for a new command. 01 says we are going to send 1 byte (the command type). 03 says we are going to read 3 bytes (into the data column) 00 is the command type (get controller status). Here is and example of reading the status from 4 controllers Only the first two controllers are actually plugged in. There is a pack in the 1st controller and there is no pack in the second controller. -----------------------------------+ [64byte block] sent to pif ram | { command data | joy1 ff010300 - ffffffff | joy2 ff010300 - ffffffff | joy3 ff010300 - ffffffff | joy4 ff010300 - ffffffff | fe000000 - 00000000 | 00000000 - 00000000 | 00000000 - 00000000 | 00000000 - 00000001 | } | -----------------------------------+ -----------------------------------+ [64byte block] read from pif ram | { command data | joy1 ff010300 - 050001ff <--- notice only 3 bytes were read joy2 ff010300 - 050002ff <--- that is why the last byte is joy3 ff018300 - ffffffff | still ff joy4 ff018300 - ffffffff | fe000000 - 00000000 | 00000000 - 00000000 | 00000000 - 00000000 | 00000000 - 00000000 | } | ----------------------------------+ The first two bytes in the data column is the controller type. I'm not exactly sure what use this is... do steering wheels have a different controller type? ;) I dunno. The 3rd byte is useful. Its for detecting if there is something plugged into the mempack slot on the controller. 1 = pack present 2 = nothing plugged in * end Getting controller status * reading/writing cart eeprom COMING NEXT RELEASE * end reading/writing cart eeprom * reading/writing mempack eeprom COMING NEXT RELEASE * end reading/writing mempack eeprom -------- Future -------- I know this document isnt much as it stands but I plan on adding some rsp info into it and of course any other info I currently havent included as time permits. Also all my source code for the stuff in this doc might get released. but right now everything is meant to build with SN's assembler and linker. i wish to recode it so it compiles with a freeware assembler... so once I do that I will release source... or maybe before ;) __-----------------------------------------------__ greets to people who helped me with some stuff! __-----------------------------------------------__ nagra, bpoint, hartec, jovis, wild_fire, datawiz Questions & Comments: about anything except where to get a devkit or libultra or roms or header file or whatever. In other words if you have a question about stuff in this doc and you are fairly intelligent, or you have a question about how to implement things in your n64 program or emulator... contact LaC on IRC efnet in #n64dev or if you must: EMAIL: LaC@dextrose.com -EOF-