Continue to Site

Welcome to our site!

Electro Tech is an online community (with over 170,000 members) who enjoy talking about and building electronic circuits, projects and gadgets. To participate you need to register. Registration is free. Click here to register now.

  • Welcome to our site! Electro Tech is an online community (with over 170,000 members) who enjoy talking about and building electronic circuits, projects and gadgets. To participate you need to register. Registration is free. Click here to register now.

Using a PIC to emulate a PSX controller

Status
Not open for further replies.

toodles

New Member
I'm attempting to make an arcade stick that supports multiple systems, and the next on the To-Do list is Playstation. I need it to emulate a PSX controller, (NOT read one, like most of the posts I've seen.) However, its being argumentative.

First off, does anyone know of any existing implementations? Surely someone has done this before me? I know Alex-RCPilot on this board was working on making a PSX gme pad and appeared to get very far, but he was using an AVR. There is an old page about it being done using a 68HC mcu, and after some creative googling, managed to finally find the source code and hex file for it. However I can find no mention anywhere of this being done in a PIC. I find that hard to believe.

Im having some difficulty getting it to work with the SPI module on a 18LF4550. If anyone knows of source from somewhere, please let me more. If none is found, I'll narrow my questions down and post them up.
 
Well, damn. I was hoping to gank other peoples code, so here's some PIC specific questions that are bugging me.

1. Does the SPI module (slave, Slave_Select enabled) have to have the SS line released between each byte? The waveforms in the data sheet for the 18f4550 don't show multiple bytes with SS pulled, only a single byte and then SS released, and a single byte interupted by SS being released, and resent when SS is pulled again.

2. What is the proper way to simulate an open collector output? I have been trying to simulate it by writing 0 to the LAT for the pin, and then toggling the TRIS pin, setting it to input when its supposed to be an open collector high, and setting output (which should turn low from the LAT being 0) when I need an open collector low. Do I need to write to the LAT every time I switch it to output, or can I write to LAT once, and then just switch the TRIS setting as needed?

Thanks for reading.
 
Wow this is frustrating. Below is the code Im using at the moment ; any suggestions on what I might be doing wrong would be appreciated.

Code:
/* includes */
#include <p18cxxx.h>
#include <delays.h>
#include <spi.h>
#include "upcb.h"  // contains the mLED_* defines.

#define ACK_Delay() {Delay10TCYx(0x28);}
#define ACK_Start() {LATCbits.LATC6=0;TRISCbits.TRISC6=0;}
#define ACK_End() {TRISCbits.TRISC6=0;}

#define SPI_Load(DATA) {do{  \
						SSPCON1bits.WCOL=0;\
						SSPBUF=DATA;\
     					}while(SSPCON1bits.WCOL);}  // if collision do it again
void main(void)
{
	unsigned char holder;
	ADCON1 |= 0x0F;                 // Default all pins to digital
   mInitAllLEDs(); /* set RA0 and RA1 as outputs */
   TRISAbits.TRISA5=1; //SS line - input
   TRISCbits.TRISC7=0; //SDO - output
   TRISBbits.TRISB1=1; //Clock - input
   TRISBbits.TRISB0=1; //SDI - input
   TRISCbits.TRISC6=0; //Line we're using for Ack
      
   mLED_1_Off(); /* sets LATA0 to 0 */
   mLED_2_Off(); /* sets LATA1 to 0 */

   	while(1)
   	{ 
		OpenSPI(SLV_SSON, MODE_10, SMPMID); 
		while(1)
		{
			SPI_Load(0xFF);
			while ( !SSPSTATbits.BF );      // wait until a cycle completes
			holder=SSPBUF;
			if (holder == 0x80)   // end first byte. check we got the expected 0x01/0x80
			{
				SPI_Load(0x82); //we did. load the next byte
				ACK_Start(); 	// and acknowledge
				ACK_Delay();
	  			ACK_End();
				while ( !SSPSTATbits.BF );    
				holder=SSPBUF; // end second byte. We should have sent the controller ID
							// and received a 0x42 querying status
				if (holder == 0x42)
				{
					//we did, so load next byte and acknowledge
					SPI_Load(0x5A); // byte to say 'here comes data'
					ACK_Start(); 
					ACK_Delay();
	  				ACK_End();
	  				mLED_1_Toggle();
	  				while ( !SSPSTATbits.BF );
	  				holder=SSPBUF; //end third byte. doesnt matter what we get but it 
	  							//has to be read to clear the buffer
	  				SPI_Load(0b11110111); //hold Up button
	  				ACK_Start(); 
					ACK_Delay();
	  				ACK_End();
	  				while ( !SSPSTATbits.BF ); //end fouth byte
	  				holder=SSPBUF; //end third byte. doesnt matter what we get but it 
	  							//has to be read to clear the buffer
	  				SPI_Load(0xFF); //no buttons pressed
	  				ACK_Start(); 
					ACK_Delay();
	  				ACK_End();
	  				while ( !SSPSTATbits.BF ); //end fifth byte
	  				holder=SSPBUF;
	  				//no need to ACK last byte.
	  			} // if (holder == 0x42)
	  		} // if (holder == 0x80)
			mLED_2_Toggle();
		}
	} //while(1)
}//main


/* Config settings */

#pragma config PLLDIV = 5 
#pragma config CPUDIV = OSC1_PLL2 
#pragma config USBDIV = 2 
#pragma config FOSC = HSPLL_HS 
#pragma config IESO = OFF 
#pragma config PWRT = OFF 
#pragma config BOR = ON 
#pragma config VREGEN = ON 
#pragma config WDT = OFF 
#pragma config WDTPS = 1 
#pragma config MCLRE = ON 
#pragma config LPT1OSC = OFF 
#pragma config PBADEN = OFF 
#pragma config CCP2MX = OFF
#pragma config STVREN = OFF 
#pragma config LVP = OFF 
#pragma config ICPRT = OFF 
#pragma config XINST = OFF 
#pragma config DEBUG = OFF 
#pragma config CP0 = OFF 
#pragma config CPB = OFF 
#pragma config WRT0 = OFF 
#pragma config WRTB = OFF 
#pragma config EBTR0 = OFF 
#pragma config EBTRB = OFF

If there is anything helpful Im leaving out, please say so.
 
What type of protocal does a PSX controller use? Does it require some kind of initialization from the console? If it doesn't (or if you know the initialization), maybe use the PIC to read the controller first to find out what happens when you press specific buttons (and pressures if it's a PS2 controller) and then get your PIC to imitate those outputs?

I dont know anything about the PSX controller. I can only offer suggestions.
 
Pages such as these:
http://www.gamesx.com/controldata/psxcont/psxcont.htm
**broken link removed**
describe the controller protocol far better than I could. There is no initialization of the controller, at least for the original digital controllers, just plug and respond to the serial commands as needed. There are commands sent from the console for the newer dualshock and dualshock 2 controllers, but these should be ignored by the digital controllers that dont support those features.

I know PSX controllers can be emulated by MCU's; the first link describes the project of someone who did it used a HC68 motorola mcu. After some hard google-fu, I managed to track down the hex and even the source code. The source shows a very simple SPI interface. If anyone wants to see it, let me know and I'll post it up.

____________________
I started from scratch and have tried to tackle this using bitbanging, but again I'm failing. The I had the first run writing values to the eeprom so I could get an idea what was going on, and it appears that the ATT (attention, pulled low to tell the controller that it's beginning to communicate with it, and released back to high when communication is done) is getting released before the second byte is fully transfered. The first byte IS being read properly. That's leading me to believe that the problem is with the ACK line (pulled low by the controller for a short while after each byte is transfered), prolly being too short or too long. The code you see below is an attempt to have the MCU figure out the best delay to hold the line down for; if the ATT line is released to high by the console unexpectedly, increment the delay time by 10 cycles. It cycles through without any value (10 instruction cycles through 2500 instruction cycles) without ever working properly.

The documents linked above give the time for ACK to be pulled down as 'at least one clock cycle', and 'at least 2us'. The clock signal stays high during this time, so I cant use it to count time. I *BELIEVE* that the PIC is running at 48MHz, at least that's my intention. If so, that's 12 MIPS, or 12 instructions per us. Running Delay10TCYx(3); should be the perfect amount of delay, but it is failing for all values from 1-250.

So I have one question outside of the general 'I dont have a clue what's wrong.' problem. With a 18LF4550 I/P with a 20Mhz crystal and 22pf caps, being powered by around 3.3v from the playstation, is the processor in fact running at 48MHz/12 MIPS with these config settings?
Code:
/* includes */
#include <p18cxxx.h>
#include <delays.h>
#include "upcb.h"  // contains the mLED_* defines and the EEpromPut function

#define ACK_Delay() {Delay10TCYx(0x3);}
#define ACK_Start() {LATCbits.LATC6=0;TRISCbits.TRISC6=0;}
#define ACK_End() {TRISCbits.TRISC6=0;}

#define SPI_Load(DATA) {do{  \
						SSPCON1bits.WCOL=0;\
						SSPBUF=DATA;\
     					}while(SSPCON1bits.WCOL);}  // if collision do it again
#define PSX_ATT PORTAbits.RA5
#define PSX_CLK PORTBbits.RB1
#define PSX_OUT LATCbits.LATC7
#define PSX_OUT_HIGH() {TRISCbits.TRISC7=1;}
#define PSX_OUT_LOW() {PSX_OUT=0;TRISCbits.TRISC7=0;}
#define PSX_IN  PORTBbits.RB0
     					
void PSX_Init(void);
void PSX_ACK_2(unsigned char delay);
unsigned char PSX_Xfer_Bit(unsigned char data);
unsigned char PSX_Xfer_Byte(unsigned char data);
void PSX_ACK(void);

void main(void)
{
	unsigned char holder, holder2, holder3, holder4, holder5, eeptr, tmpdelay;
	ADCON1 |= 0x0F;                 // Default all pins to digital
	holder=holder2=holder3=holder4=holder5=eeptr=0;
	tmpdelay=1;
	mInitAllLEDs(); /* set RA0 and RA1 as outputs */
	PSX_Init();
	while(1)
	{
		while(!PSX_ATT); //in case we got here from a unsynced attempt or unrecognized command, 
						// let's wait until the ACK is back up before waiting for it to go down
		while(PSX_ATT); //Wait for it to drop to begin
		if (0x01 == (holder=PSX_Xfer_Byte(0xFF))) // first byte should be 0x01 (since PSX_Xfer_byte knows PSX is LSB first, 
		{								// we can check for the actual sent values
			mLED_1_Toggle();
			PSX_ACK_2(tmpdelay); //send an acknowledgement
			
			if (0x42==(holder2=PSX_Xfer_Byte(0x41))) //second byte from us is the controller ID (0x41 being a digital)
			{							// byte we're hoping to receive is 0x42, for query status
				
				mLED_2_Toggle();
				PSX_ACK(); //send ack
				PSX_Xfer_Byte(0x5A); //send the 'here comes the data' byte. We dont care what it sends us
				if (PSX_ATT) continue;  //just in case we got dropped
				PSX_ACK();
				PSX_Xfer_Byte(0b11101111); //send the first byte which shows 'Up' is pressed
				if (PSX_ATT) continue;
				PSX_ACK();
				PSX_Xfer_Byte(0xFF); // all other buttons not pressed
									//No ACK'ing the last byte
				
			}
			else
			{
				if (holder2 == 0xAA)
				{
					if (tmpdelay >250)
					{
						mLED_2_On();
					}
					else
					{
						tmpdelay++;
					}
				}
			}
		}
		if (eeptr < 250)
		{
			if (holder2 == 0x42)
			{
				EEpromPut(eeptr, holder);
				eeptr++;
				EEpromPut(eeptr, holder2);
				eeptr++;
				EEpromPut(eeptr, tmpdelay);
				eeptr++;
			}			
		}
		else
		{ 
			mLED_2_On();
		}
		
	}
}//main

void PSX_Init(void)
{
	TRISAbits.TRISA5=1; //SS line - input
   	TRISCbits.TRISC7=0; //SDO - output
   	TRISBbits.TRISB1=1; //Clock - input
   	TRISBbits.TRISB0=1; //SDI - input
   	TRISCbits.TRISC6=0; //Line we're using for Ack
}
void PSX_ACK(void)
{
	ACK_Start(); 
	ACK_Delay();
	ACK_End();
}
void PSX_ACK_2(unsigned char delay)
{
	ACK_Start();
	Delay10TCYx(delay);
	ACK_End();
}
	
unsigned char PSX_Xfer_Byte(unsigned char data)
{
	unsigned char holder;
	holder=0;
	if (PSX_ATT) return (0xAA); //Check in case ATT is released back to high
	//PSX communication is LSB first
	if (PSX_Xfer_Bit(data & 0b00000001)) holder |= 0b00000001; if (PSX_ATT) return (0xAA);
	if (PSX_Xfer_Bit(data & 0b00000010)) holder |= 0b00000010; if (PSX_ATT) return (0xAA);
	if (PSX_Xfer_Bit(data & 0b00000100)) holder |= 0b00000100; if (PSX_ATT) return (0xAA);
	if (PSX_Xfer_Bit(data & 0b00001000)) holder |= 0b00001000; if (PSX_ATT) return (0xAA);
	if (PSX_Xfer_Bit(data & 0b00010000)) holder |= 0b00010000; if (PSX_ATT) return (0xAA);
	if (PSX_Xfer_Bit(data & 0b00100000)) holder |= 0b00100000; if (PSX_ATT) return (0xAA);
	if (PSX_Xfer_Bit(data & 0b01000000)) holder |= 0b01000000; if (PSX_ATT) return (0xAA);
	if (PSX_Xfer_Bit(data & 0b10000000)) holder |= 0b10000000; if (PSX_ATT) return (0xAA);	
	return(holder);
}
unsigned char PSX_Xfer_Bit(unsigned char data)
{
	while (PSX_CLK)  //Wait for clock to drop
	{
		if (PSX_ATT) return (0x00); //Check in case ATT is released back to high
	}
	// clock just dropped, so send the bit
	if (data)
	{
		PSX_OUT_HIGH();
	}
	else
	{
		PSX_OUT_LOW();
	}
	//wait for clock to raise back so we can read the data
	while (!PSX_CLK)
	{
		if (PSX_ATT) return (0x00);
	}
	//clock has raised, time to read the bit
	if (PSX_IN)
	{
		return(0xFF);
	}
	else
	{
		return(0x00);
	}
}
/* Config settings */

#pragma config PLLDIV = 5 
#pragma config CPUDIV = OSC1_PLL2 
#pragma config USBDIV = 2 
#pragma config FOSC = HSPLL_HS 
#pragma config IESO = OFF 
#pragma config PWRT = OFF 
#pragma config BOR = ON 
#pragma config VREGEN = ON 
#pragma config WDT = OFF 
#pragma config WDTPS = 1 
#pragma config MCLRE = ON 
#pragma config LPT1OSC = OFF 
#pragma config PBADEN = OFF 
#pragma config CCP2MX = OFF
#pragma config STVREN = OFF 
#pragma config LVP = OFF 
#pragma config ICPRT = OFF 
#pragma config XINST = OFF 
#pragma config DEBUG = OFF 
#pragma config CP0 = OFF 
#pragma config CPB = OFF 
#pragma config WRT0 = OFF 
#pragma config WRTB = OFF 
#pragma config EBTR0 = OFF 
#pragma config EBTRB = OFF

EDIT: DOH! Im a dumbass. ACK_Start and ACK_Edn both set the TRIS to 0. One should be 1. One moment while I edit and test.
Ninja edit 2: SUCCESS!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

Ken is jumping up just like he should. This is so awesome. The same bug was in the SPI version, so I should try it too here soon.
A little more tweaking is needed for what I need, but this proof of concept is a BIG BIG BIG help. If anyone wants to try, just take the bitbang code above and change:
#define ACK_End() {TRISCbits.TRISC6=0;}
to
#define ACK_End() {TRISCbits.TRISC6=1;}

WOOHOO!
 
Last edited:
and here's the working code using SPI. THe Stick_Left and functions used to return the status of the buttons should be pretty obvious, but they're just output pins defined in the upcb.h, and can be tweaked however one might like. I've just used this to play some Marvel vs Capcom 2 on my PS2 and it works great. Setting up to check for other commands and pretend to be controllers other than the original digital should be easy to implement.

Code:
#include <p18cxxx.h>
#include <delays.h>
#include <spi.h>
#include "upcb.h"

void PSX_ACK(void);
void PSX_Init_Pins(void);

void main (void)
{
	unsigned char holder;
	ADCON1 |= 0x0F;                 // Default all pins to digital
	mInitStick();
	mInitConsolePins();
	mInitButtons();
	mInitAllLEDs(); /* set RA0 and RA1 as outputs */
	PSX_Init_Pins();
	OpenSPI(SLV_SSON, MODE_10, SMPMID);
	while(1)
	{
		// Here we are at first byte. Load up dummy data to go
		SSPBUF=0xFF; 
		while ( !SSPSTATbits.BF ); 	// wait for a read cycle to complete
		if (SSPBUF == 0x80)  // 0x80 is 0x01 bit reverse; SPI uses MSB first, PSX used LSB first
		{					// so we have to reverse it for our comparisons
			// sweet, so the first byte was received correctly. 
			SSPBUF=0x82; // Load the next byte to send into the buffer
						// 0x82 is the bit reverse of 0x41, the controller ID of the 
						//original PSX digital pad. 
			PSX_ACK(); //acknowledge
			while ( !SSPSTATbits.BF ); 	// wait for a read cycle to complete
			// now we've read and sent the second byte. If its 0x42, the status query command,
			//then start sending the data. Otherwise, its an unsupported command we ignore.
			if (SSPBUF == 0x42) //0x42 is 0x42 reverse. Binary palindrome.
			{
				// correct command received so we're in sync. Time to send data.
				SSPBUF=0x5A; // sending the 'here comes the data byte' 0x5a, another palindrome
				PSX_ACK(); //acknowledge
				/* Now that the acknowledge is done, the PSX will start the clock again and shuffling
					the bits for the next byte transfer. Instead of going straight into waiting for it 
					to finish, let's use this time to construct the next byte we'll be sending. */
				holder=0xFF; //clear the byte out. 0 bits are pressed buttons, 
							// 1 bits are not pressed.
				/* Bit0 Bit1 Bit2 Bit3 Bit4 Bit5 Bit6 Bit7
     			   SLCT    1    1 STRT UP   RGHT DOWN LEFT  
     			   That's the layout for the PSX, But we'll be sending the byte backwards so:
     			   Bit7 Bit6 Bit5 Bit4 Bit3 Bit2 Bit1 Bit0       is how it really is
     			   SLCT    1    1 STRT UP   RGHT DOWN LEFT  		   */
     			if (!Stick_Select) holder &= 0b01111111;
     			if (!Stick_Start)  holder &= 0b11101111;
     			if (!Stick_Up)     holder &= 0b11110111;
     			if (!Stick_Right)  holder &= 0b11111011;
     			if (!Stick_Down)   holder &= 0b11111101;
     			if (!Stick_Left)   holder &= 0b11111110;
     			// all done building, wait for the third byte to finish
     			while ( !SSPSTATbits.BF );
     			// all done, load the contructed byte into the buffer
     			SSPBUF=holder;
     			PSX_ACK(); // finished so acknowledge
     			/* again, ACK is down, and the first contructed byte is being sent. 
     				let's construct the final byte with the time we have
     				Bit0 Bit1 Bit2 Bit3 Bit4 Bit5 Bit6 Bit7  - PSX style
     				L2   R2    L1  R1   /\   O    X    |_|
     				Bit7 Bit6 Bit5 Bit4 Bit3 Bit2 Bit1 Bit0  - our MSB style */
     			holder=0xFF;
     			if (!Stick_Roundhouse) holder &= 0b10111111; // map RH to R2
     			if (!Stick_Fierce)     holder &= 0b11101111; // map fierce to R1
     			if (!Stick_Strong)     holder &= 0b11110111; //strong to tri
     			if (!Stick_Forward)    holder &= 0b11111011; //forward to O
     			if (!Stick_Short)      holder &= 0b11111101; //short to X
     			if (!Stick_Jab)        holder &= 0b11111110; //jab to square
     			// all done building, wait for the fourth byte to finish
     			while ( !SSPSTATbits.BF );
     			// all done, load the contructed byte into the buffer
     			SSPBUF=holder;  //load fifth byte into buffer
     			PSX_ACK(); //ack receipt of fourth byte
     			while ( !SSPSTATbits.BF ); //wait for last byte to xfer
 			}
		}
			
	}
	
}

void PSX_Init_Pins(void)
{
	TRISAbits.TRISA5=1; //SS line - input
	TRISCbits.TRISC7=0; //SDO - output
	TRISBbits.TRISB1=1; //Clock - input
   	TRISBbits.TRISB0=1; //SDI - input
   	TRISCbits.TRISC6=0; //Line we're using for Ack
}

void PSX_ACK(void)
{
	LATCbits.LATC6=0; // make sure the pin will go low when turned to output
	TRISCbits.TRISC6=0; // make it an output to pull the ACK low
	Delay10TCYx(3); // Wait for 30 instruction cycles (about 2.5 usec)
	TRISCbits.TRISC6=1; // return the pin to an input high impedence
}
 
Did you ever get this to work? The communication is actually very simple. If you have a logic analyzer, or if you can rig one using a computer parallel port, then the easiest thing to do is record the pin states between a controller and PSX console.

A few years ago I found a site in German that had the data from a logic analyzer between the console and controller, but I no longer have the link. If you can find it it will be very helpful.

Dan East
 
Dan East said:
Did you ever get this to work? The communication is actually very simple. If you have a logic analyzer, or if you can rig one using a computer parallel port, then the easiest thing to do is record the pin states between a controller and PSX console.

A few years ago I found a site in German that had the data from a logic analyzer between the console and controller, but I no longer have the link. If you can find it it will be very helpful.

Dan East
toodles said:
and here's the working code using SPI.
:) Yeah, I got it to work using the code above. One tweak I did make later that helped (sometimes it would work in P2 slot but not P1) was to have all of the while loops waiting for ( !SSPSTATbits.BF ); to also check the ATT line to make sure it was low. Once I did that, it's been working perfectly for PS2 and PS1 games, both ports. I cant get it to work with adapters, but oh well. I have not tried adding in support for the extended DS1 and DS2 commands, but will eventually. Thanks though, but I gots it working. :)
 
nice job friend...you manage to figure that out by youself?.. wow.. i am sure your research will be very useful to others..
 
And a bunch more. Check out the project thread I started on the whole project

**broken link removed**

Eventually, I'll add in support to emulate a Dual Shock 1 and Dual Shock 2 properly, but there isn't much of a need since it's mainly for arcade fighting sticks, which are all digital.
 
Status
Not open for further replies.

Latest threads

New Articles From Microcontroller Tips

Back
Top