One of the most important aspects when writing software for a game console is taking input from a controller, as otherwise what's the point of a game where the player can't take control of it? Unless its some demoscene stuff you intend to write for the console.
This chapter goes over how to initialize and handle controller input in your PS1 program. This is something that hasn't really been explained very well in many tutorials that I've seen in my personal experience, so I'll try my best to explain how controller input is handled on the PS1 in great detail.
Compatible with PSn00bSDK: Yes
There are a number of ways to obtain input from the controller on the PS1; direct hardware access (complicated), through BIOS InitPAD()/StartPAD() (simpler, but has some limitations), though PadInit()/PadRead() (even simpler, but only works with digital pads), and through various controller related libraries such as libtap, libgun and libpad provided in the PsyQ/Programmers' Tool SDK.
The most common method people tend to use is PadInit() and PadRead(), and whilst it is the most simplest to use it is not exactly the most ideal method for a proper game project. For one it only works with digital pads and Dual-shock pads with analog turned off. Secondly, there's no way to determine if a pad is disconnected or not and rumble features on Dual-shock pads cannot be accessed with it. It is, however, ideal for prototype test programs.
Another method which is a little more ideal is to use InitPAD() and StartPAD() which are provided by the BIOS ROM of the console. This method is not only capable of determining if a controller is attached or not but it also works with a wider variety of controllers including multi-taps. The only controllers that it doesn't really support properly are both the Namco and Konami lightguns as they need to be polled in a specific way for it to work, which is not supported by these functions. Lightguns are beyond the scope for this chapter anyway. It still cannot access rumble features of Dual-analog and Dual-shock controllers however.
For compatibility with PSn00bSDK, only the BIOS InitPAD() and StartPAD() functions will be covered in this chapter. In the future, I will look into making a follow-up that uses libpad or a similar equivalent library for PSn00bSDK that would also cover accessing rumble features of Dual-analog and Dual-shock pads.
The first step is to define an array of two 34 byte elements. This array will be used as a buffer for storing incoming controller data which requires at least about 34 bytes per controller port. It may sound a bit excessive for storing input data, but this is necessary in case a multitap is connected to one of the ports, as a multitap will return up to 34 bytes from all four controllers connected to the tap.
u_char padbuff[2][34];
Next is to call InitPAD() to define the pad array to the BIOS controller subsystem. libpad still uses the same buffer format that InitPAD() would return data to, so upgrading from the BIOS pad routines to libpad should be pretty trivial.
InitPAD( padbuff[0], 34, padbuff[1], 34 );
Before you start controller polling, it may be a good idea to set the first two elements of both arrays with 0xff, so that your program won't process faulty input when the array is parsed with zero filled data. You'll figure out why later in this chapter.
padbuff[0][0] = padbuff[0][1] = 0xff; padbuff[1][0] = padbuff[1][1] = 0xff;
Then start controller polling with StartPAD().
StartPAD();
After calling StartPAD() and you have a tty console in your development setup (or the message window in no$psx), you may see the message "VSync: Timeout" pop-up when your program reaches a VSync call. This happens because StartPAD() annoyingly re-enables automatic VSync interrupt acknowledge in the PS1's kernel, preventing the VSync() function from detecting a VSync interrupt from occuring. Fortunately, it has a timeout, of which it will disable the automatic VSync acknowledge so it can detect VSync interrupts again. This can happen on both Programmer's Tool and PSn00bSDK.
In PSn00bSDK, you can avoid this message by calling ChangeClearPAD(1) after StartPAD(), but this function is only really exposed in PSn00bSDK.
It is generally unnecessary to stop controller polling using StopPAD(), plus it may cause Dual-shock controllers to reset their controller mode back to digital mode. If you're using BIOS memory card functions later down the line, stopping pads will also stop memory cards as well. Using StopPAD() is only really required when transferring execution to a child PS-EXE or another PS-EXE altogether.
After calling StartPAD(), the
Because the PS1's BIOS controller handler depends on VSync interrupts to poll the controller ports, you will not be able to receive further inputs when interrupts are disabled, but this isn't really something to worry about yet at this point in this tutorial series.
Now, onto the pad buffer data. The first byte stored in the
if( padbuff[0][0] == 0 ) { // controller on port 1 connected }
The following byte is the controller ID, denoting both the controller type and the number of bytes the controller has sent to the console. The controller type is stored in the upper 4 bits of the ID byte while the data byte is at the lower 4 bits.
padtype = padbuff[0][1]>>4; // get the controller type padlen = padbuff[0][1]&0xF; // get the data length (normally not needed)
The following table lists controller type values that are relevant to this chapter. Lightgun IDs are omitted as you won't be able to poll them properly anyway.
Controller | Type Value |
---|---|
Mouse | 0x1 |
Namco NegCon | 0x2 |
Digital pad or Dual-shock in digital mode (no light) | 0x4 |
Analog Stick or Dual-analog in green mode | 0x5 |
Dual-shock in analog mode or Dual-analog in red mode | 0x7 |
Namco JogCon | 0xE |
The two bytes following the ID byte are usually the controller's digital button states as a 16-bit integer, with each bit representing the state of one button. Oddly, the bit states are inverted, where a bit value of 1 means released and a value of 0 means pressed.
The following table lists which bit of the 16-bit word corresponds to each button of a standard digital pad or Dual-shock controller. Both controllers share the same bit positions.
Bit | Button |
---|---|
0 | Select |
1 | L3 (Dual-shock only) |
2 | R3 (Dual-shock only) |
3 | Start |
4 | Up |
5 | Right |
6 | Down |
7 | Left |
8 | L2 |
9 | R2 |
10 | L1 |
11 | R1 |
12 | Triangle |
13 | Circle |
14 | Cross |
15 | Square |
It may be a good idea to define a list of constants for each button so
you can specify which bit to test with more coherent names. In PSn00bSDK,
these are already defined in
#define PAD_SELECT 1 #define PAD_L3 2 #define PAD_R3 4 #define PAD_START 8 #define PAD_UP 16 #define PAD_RIGHT 32 #define PAD_DOWN 64 #define PAD_LEFT 128 #define PAD_L2 256 #define PAD_R2 512 #define PAD_L1 1024 #define PAD_R1 2048 #define PAD_TRIANGLE 4096 #define PAD_CIRCLE 8192 #define PAD_CROSS 16384 #define PAD_SQUARE 32768
To test if a button is pressed, simply mask the 16-bit integer against the bit value of the button you want to test using an AND (&) operator. Because the pressed state of a button is zero, you'll have to follow it up with a NOT (!) operator as well.
u_short button; ... button = *((u_short*)(padbuff[0]+2)); // is cross pressed? if( !( button & PAD_CROSS ) ) { // do something when cross is pressed }
If you only need to support regular controllers (digital, analog stick,
Dual-analog, Dual-shock), a simplle struct like this should suffice for
all the four controller types and with analog input on controllers with
such features. In PSn00bSDK, the following struct is already defined in
typedef struct _PADTYPE { unsigned char stat; unsigned char len:4; unsigned char type:4; unsigned short btn; unsigned char rs_x,rs_y; unsigned char ls_x,ls_y; } PADTYPE;
If the connected controller doesn't feature any analog inputs, the
elements following
When parsing analog stick inputs, remember that the coordinates when the stick is at its center position is not always 128, due to deadzones and other factors that affect the potentiometers. Its recommended to implement a simple threshold to make sure your code does not register false inputs (at least to the player) when the stick is placed at its center position.
For more information about controller input data, you may want to check the Controllers Chapter in nocash's PSX specs document.
As an exercise for this chapter, we're going to make the textured sprite
from the last chapter move using the controller. Begin by adding the button
definitions, structs and
Next, place the InitPAD() and StartPAD() calls at the end
of your init() function, so pads get initialized alongside the
graphics. If you're using PSn00bSDK, you can add ChangeClearPAD(1)
after StartPAD() to avoid the
Next, define two variables named pos_x and pos_y of type int at the start of the main() function, preferably before the line that defines the TILE variable. These will be for storing the X,Y coordinates for the textured sprite. Following that, define a variable pad of type PADTYPE, this will be used for reading controller inputs more easily.
Before the while loop, set both pos_x and pos_y to 48 to make sure they don't contain a random undefined value. Within the loop, add the following code that parses the controller and performs actions according to controller inputs.
pad = (PADTYPE*)padbuff[0]; // Only parse inputs when a controller is connected if( pad->stat == 0 ) { // Only parse when a digital pad, // dual-analog and dual-shock is connected if( ( pad->type == 0x4 ) || ( pad->type == 0x5 ) || ( pad->type == 0x7 ) ) { if( !(pad->btn&PAD_UP) ) // test UP { pos_y--; } else if( !(pad->btn&PAD_UP) ) // test DOWN { pos_y++; } if( !(pad->btn&PAD_LEFT) ) // test LEFT { pos_x--; } else if( !(pad->btn&PAD_RIGHT) ) // test RIGHT { pos_x++; } } }
Now, go to the bit of code that sorts the textured sprite, and modify the setXY0 macro call to use X,Y coordinates from pos_x and pos_y.
setXY0(sprt, pos_x, pos_y);
If you followed the instructions properly, the source code should look like this:
#include <sys/types.h> // This provides typedefs needed by libgte.h and libgpu.h #include <stdio.h> // Not necessary but include it anyway #include <libetc.h> // Includes some functions that controls the display #include <libgte.h> // GTE header, not really used but libgpu.h depends on it #include <libgpu.h> // GPU library header #include <libapi.h> // API header, has InitPAD() and StartPAD() defs #define OTLEN 8 // Ordering table length (recommended to set as a define // so it can be changed easily) DISPENV disp[2]; // Display/drawing buffer parameters DRAWENV draw[2]; int db = 0; // PSn00bSDK requires having all u_long types replaced with // u_int, as u_long in modern GCC that PSn00bSDK uses defines it as a 64-bit integer. u_long ot[2][OTLEN]; // Ordering table length char pribuff[2][32768]; // Primitive buffer char *nextpri; // Next primitive pointer int tim_mode; // TIM image parameters RECT tim_prect,tim_crect; int tim_uoffs,tim_voffs; // Pad stuff (omit when using PSn00bSDK) #define PAD_SELECT 1 #define PAD_L3 2 #define PAD_R3 4 #define PAD_START 8 #define PAD_UP 16 #define PAD_RIGHT 32 #define PAD_DOWN 64 #define PAD_LEFT 128 #define PAD_L2 256 #define PAD_R2 512 #define PAD_L1 1024 #define PAD_R1 2048 #define PAD_TRIANGLE 4096 #define PAD_CIRCLE 8192 #define PAD_CROSS 16384 #define PAD_SQUARE 32768 typedef struct _PADTYPE { unsigned char stat; unsigned char len:4; unsigned char type:4; unsigned short btn; unsigned char rs_x,rs_y; unsigned char ls_x,ls_y; } PADTYPE; // pad buffer arrays u_char padbuff[2][34]; void display() { DrawSync(0); // Wait for any graphics processing to finish VSync(0); // Wait for vertical retrace PutDispEnv(&disp[db]); // Apply the DISPENV/DRAWENVs PutDrawEnv(&draw[db]); SetDispMask(1); // Enable the display DrawOTag(ot[db]+OTLEN-1); // Draw the ordering table db = !db; // Swap buffers on every pass (alternates between 1 and 0) nextpri = pribuff[db]; // Reset next primitive pointer } // Texture upload function void LoadTexture(u_long *tim, TIM_IMAGE *tparam) { // Read TIM parameters (PsyQ) OpenTIM(tim); ReadTIM(tparam); // Read TIM parameters (PSn00bSDK) //GetTimInfo(tim, tparam); // Upload pixel data to framebuffer LoadImage(tparam->prect, (u_long*)tparam->paddr); DrawSync(0); // Upload CLUT to framebuffer if present if( tparam->mode & 0x8 ) { LoadImage(tparam->crect, (u_long*)tparam->caddr); DrawSync(0); } } void loadstuff(void) { TIM_IMAGE my_image; // TIM image parameters extern u_long tim_my_image[]; // Load the texture LoadTexture(tim_my_image, &my_image); // Copy the TIM coordinates tim_prect = *my_image.prect; tim_crect = *my_image.crect; tim_mode = my_image.mode; // Calculate U,V offset for TIMs that are not page aligned tim_uoffs = (tim_prect.x%64)<<(2-(tim_mode&0x3)); tim_voffs = (tim_prect.y&0xff); } // To make main look tidy, init stuff has to be moved here void init(void) { // Reset graphics ResetGraph(0); // First buffer SetDefDispEnv(&disp[0], 0, 0, 320, 240); SetDefDrawEnv(&draw[0], 0, 240, 320, 240); // Second buffer SetDefDispEnv(&disp[1], 0, 240, 320, 240); SetDefDrawEnv(&draw[1], 0, 0, 320, 240); draw[0].isbg = 1; // Enable clear setRGB0(&draw[0], 63, 0, 127); // Set clear color (dark purple) draw[1].isbg = 1; setRGB0(&draw[1], 63, 0, 127); nextpri = pribuff[0]; // Set initial primitive pointer address // load textures and possibly other stuff loadstuff(); // set tpage of lone texture as initial tpage draw[0].tpage = getTPage( tim_mode&0x3, 0, tim_prect.x, tim_prect.y ); draw[1].tpage = getTPage( tim_mode&0x3, 0, tim_prect.x, tim_prect.y ); // apply initial drawing environment PutDrawEnv(&draw[!db]); // Initialize the pads InitPAD( padbuff[0], 34, padbuff[1], 34 ); // Begin polling StartPAD(); // To avoid VSync Timeout error, may not be defined in PsyQ ChangeClearPAD( 1 ); } int main() { int pos_x,pos_y; PADTYPE *pad; TILE *tile; // Pointer for TILE SPRT *sprt; // Pointer for SPRT // Init stuff init(); pos_x = pos_y = 48; while(1) { // Parse controller input pad = (PADTYPE*)padbuff[0]; // Only parse inputs when a controller is connected if( pad->stat == 0 ) { // Only parse when a digital pad, // dual-analog and dual-shock is connected if( ( pad->type == 0x4 ) || ( pad->type == 0x5 ) || ( pad->type == 0x7 ) ) { if( !(pad->btn&PAD_UP) ) // test UP { pos_y--; } else if( !(pad->btn&PAD_DOWN) ) // test DOWN { pos_y++; } if( !(pad->btn&PAD_LEFT) ) // test LEFT { pos_x--; } else if( !(pad->btn&PAD_RIGHT) ) // test RIGHT { pos_x++; } } } ClearOTagR(ot[db], OTLEN); // Clear ordering table // Sort textured sprite sprt = (SPRT*)nextpri; setSprt(sprt); // Initialize the primitive (very important) setXY0(sprt, pos_x, pos_y); // Position the sprite at (48,48) setWH(sprt, 64, 64); // Set size to 64x64 pixels setUV0(sprt, // Set UV coordinates tim_uoffs, tim_voffs); setClut(sprt, // Set CLUT coordinates to sprite tim_crect.x, tim_crect.y); setRGB0(sprt, // Set primitive color 128, 128, 128); addPrim(ot[db], sprt); // Sort primitive to OT nextpri += sizeof(SPRT); // Advance next primitive address // Sort untextured tile primitive from the last tutorial tile = (TILE*)nextpri; // Cast next primitive setTile(tile); // Initialize the primitive (very important) setXY0(tile, 32, 32); // Set primitive (x,y) position setWH(tile, 64, 64); // Set primitive size setRGB0(tile, 255, 255, 0); // Set color yellow addPrim(ot[db], tile); // Add primitive to the ordering table nextpri += sizeof(TILE); // Advance the next primitive pointer // Update the display display(); } return 0; }
Compile, execute and you should get a textured sprite that you can move around with the directional pad of the controller.
Hopefully, this chapter should teach you about getting controller input more than using PadInit() and PadStart(). The only downside with this method is that you can't control the vibration motors of either Dual-analog or Dual-shock controllers, or enabling analog mode on Dual-shock controllers in software. Hopefully, this will be covered in a future chapter of this tutorial series.
In the next chapter, we'll be looking into handling analog inputs and a glimpse of fixed-point integer math for performing fractional calculations without using floats.
Previous | Back to Index | Next |