Hello all! Thank you for taking the time to read my post. I consider myself an arduino user of intermediate knowledge, but, when it comes to SPI, I have little experience, so I'd like to ask others to help point me in the right direction.
I want to control a Playstation 2 with an ESP32. I have found some codes from a few years back that are capable of this, but were written with the Atmega328P and it's AVR architecture in mind. For this project, however, I really need it to be an ESP32 as it has all of the features that I need.
However, as you can probably already imagine, I get compile errors (below) because the "ISR (SPI_STC_vect)" interrupt doesn't work with the ESP32 because it directly addresses the registers, which are of course, different. So, I have to convert the older AVR based code to an ESP32 based one. My initial thought (about 3 hours of research ago) was that it shouldn't be too hard. SPI is still SPI, I just need to use the proper register lingo and viola, error-free compilation.
So here's my question! Is it that simple? Or is AVR so different that this isn't even reasonable? Is it more likely that the entire code will need to be rewritten?
My understanding of why they wanted to directly use the registers is because the 328P was only 8HZ (I believe they had to use 3.3V arduino variations), so the digital write wasn't fast enough. Addressing the registers directily allowed for significantly faster digitalwrite speeds. However, using an ESP32, surely it's quick enough to not need to use the registers directly, right? Or is my understanding of the inner workings of digital write not right?
I'll post the code below. If anyone can give me any suggestions or tips, I'd really appreciate it. Thanks again,
Andy
#include <SPI.h>
#define ackPin 9
#define triPin 7
#define sqPin 6
#define oPin 5
#define exPin 4
/*
General Process
1. Set up MCU as slave - check
2. Set up MCU for interrupts, letting the ps2 handle SS and clock rate - check
3. Collect data regarding the controller state
4. When an interrupt occurs, transfer this data to the Ps2, byte by byte
Transfer Data Process:
1. Have 1 array (data.response) that can hold all the needed data to transfer.
2. For each command we recieve from the ps2, we set up the array for the needed information and the needed response length
3. Transfer it one byte at time. (NOTE: One byte per interrupt)
4. Except for a few special cases, we don't have to worry about another command from the ps2 until the transfer is complete.
5. Reset our index for the next transfer.
*/
struct DATA { //Main data structure
byte response[20];
volatile size_t count;
volatile size_t sendAck;
size_t responseLength;
bool configState;
bool aboutToConfig;
bool aboutToSwitchBetweenAnalogDigital;
bool secondPass;
bool modeLocked;
bool isDigital;
bool isAnalog;
bool ssState;
bool oldSsState;
bool FourcSecondPass;
bool aboutToSetMotorStates;
bool smallMotorOn;
long lastChange;
} data = {{0x41, 0x5A}, 0, 0, 5, false, false, false, false, false, true, false, false, false, false, false, false, 0};
void SlaveInit(void) {
/*
This function sets up the arduino in the correct configureation to communicate with the PS2.
*/
pinMode(MOSI, INPUT); //pin 11
pinMode(MISO, OUTPUT); //pin 12
pinMode(SCK, INPUT); //pin 13
pinMode(SS, INPUT); //pin 10
SPCR |= bit (SPE); //Init as slave
SPCR |= 0x2C; //Configure for LSB & correct SPI_Mode
pinMode(ackPin, OUTPUT);
digitalWrite(ackPin, LOW); //Because of external circuitry (open drain) need to flip this so it works appropriately
pinMode(triPin, INPUT_PULLUP);
pinMode(sqPin, INPUT_PULLUP);
pinMode(oPin, INPUT_PULLUP);
pinMode(exPin, INPUT_PULLUP);
SPCR |= _BV(SPIE); //Attach Interrupts
}
void setup() {
SlaveInit();
}
/*
This function reads the states of the pins and returns the correct byte describing the state of the controller
*/
byte getControllerStateFirstByte() {
byte data = 0xFF; //Buttons are ative low
byte pinState = PIND; //digitalRead() is too slow, we need to read the pins directly from the register
//The following are examples of reading the pins and correctly setting up the data to be transferred.
/*
if (!(pinState & 0b00001000)) {
data &= ~0x10; // up
}
if (!(pinState & 00000010)) {
data &= ~0x40; //down
}
*/
return data;
}
byte getControllerStateSecondByte() {
/*
This function reads the states of the pins and
sets up the byte describing the state of the controller appropriately
*/
byte pinState = PIND;
byte data = 0xFF;
//The following are examples of reading the pins and correctly setting up the data to be transferred.
/*
if (!(pinState & 0b10000000)) {
data &= ~0x10; //triangle
}
if (!(pinState & 0b01000000)) {
data &= ~0x40; // X
}
if (!(pinState & 0b00100000)) {
data &= ~0x20; // O
}
if (!(pinState & 0b00010000)) {
data &= ~0x80; //Square
}
*/
return data; //Buttons are active low in the bus, so we invert all the bits
}
// SPI interrupt routine
ISR (SPI_STC_vect) {
//By the time we reach this routine, we have already transferred the first byte i.e CMD: 0x01, Data: 0xFF, aka the first byte of the header has already been sent
byte cmd = SPDR; //Read incomming command from ps2
switch (cmd) { //Main logic handling of the cmds of the ps2
//See http://store.curiousinventor.com/guides/PS2/ for more information about these commands.
case 0x41:
data.responseLength = 9;
if (data.configState && data.isAnalog) {
data.response[0] = 0x79;
data.response[1] = 0x5a;
data.response[2] = 0x00;
data.response[3] = 0x00;
data.response[4] = 0x03;
data.response[5] = 0x00;
data.response[6] = 0x00;
data.response[7] = 0x5A; //last byte is always 0x5A
} else if (data.configState && data.isDigital) {
data.response[0] = 0x41;
data.response[1] = 0x5A;
data.response[2] = 0xFF;
data.response[3] = 0xFF;
data.response[4] = 0x00;
data.response[5] = 0x00;
data.response[6] = 0x00;
data.response[7] = 0x5A; //last byte is always 0x5A
}
break;
case 0x42:
if (data.isDigital) {
data.responseLength = 5;
data.response[0] = 0x41;
}
else if (data.isAnalog) {
data.responseLength = 9;
data.response[0] = 0x79;
data.response[4] = 0x7F;
data.response[5] = 0x7F;
data.response[6] = 0x7F;
data.response[7] = 0x7F;
data.response[8] = 0x7F;
}
/*
TODO: handle rumble here
*/
data.response[1] = 0x5A;
data.response[2] = getControllerStateFirstByte();
data.response[3] = getControllerStateSecondByte(); //This is the 5th byte
break;
case 0x43: // enter/exit config mode
data.aboutToConfig = true;
data.responseLength = 9;
if (data.isDigital) {
data.responseLength = 5;
data.response[0] = 0x41;
}
else if (data.isAnalog) {
data.response[0] = 0x79; //analog mode
if (!(data.configState)) {
data.response[4] = 0x7F; //analog sticks
data.response[5] = 0x7F;
data.response[6] = 0x7F;
data.response[7] = 0x7F;
}
}
if (!(data.configState)) {
data.response[2] = getControllerStateFirstByte();
data.response[3] = getControllerStateSecondByte();
//No vibration motor control necessary
}
break;
case 0x44:
data.responseLength = 9;
if (data.configState) { //Only works if we are in config mode
data.aboutToSwitchBetweenAnalogDigital = true;
data.response[0] = 0xF3; //Stating that we are indeed in config mode
data.response[1] = 0x5A;
}
break;
case 0x45:
data.responseLength = 9;
// if (data.configState) { //For some reason this causes glitches though all documentation says we should be in config mode
data.response[0] = 0xF3;
data.response[1] = 0x5A;
data.response[2] = 0x03;
data.response[3] = 0x02;
data.response[4] = 0x00; //0x01 if LED is on, possibly a way to switch between analog & digtial?
data.response[5] = 0x02;
data.response[6] = 0x01;
data.response[7] = 0x00;
// }
break;
case 0x46:
data.responseLength = 9;
if (data.configState && data.secondPass == false) { //Only works if we are in config mode
data.response[0] = 0xF3;
data.response[1] = 0x5A;
data.response[3] = 0x00;
data.response[4] = 0x00;
data.response[5] = 0x02;
data.response[6] = 0x00;
data.response[7] = 0x0A;
data.secondPass = true;
}
break;
case 0x47:
data.responseLength = 9;
if (data.configState) {
data.response[0] = 0xF3;
data.response[1] = 0x5A;
data.response[3] = 0x00;
data.response[4] = 0x02;
data.response[5] = 0x00;
data.response[6] = 0x00;
data.response[7] = 0x0A;
}
break;
case 0x4C:
data.responseLength = 9;
if (data.configState && data.FourcSecondPass == false) {
data.response[0] = 0xF3;
data.response[1] = 0x5A;
data.response[2] = 0x00;
data.response[3] = 0x00;
data.response[4] = 0x00;
data.response[5] = 0x04;
data.response[6] = 0x00;
data.response[7] = 0x00;
data.FourcSecondPass = true;
}
break;
case 0x4D:
//Not implemented
break;
case 0x4F:
//Not implemented
break;
case 0x00: //Special commands after recieving a specific command
if (data.count == 2) { //This jumps over the end of the header where the cmd would be 0x00
if (data.aboutToConfig) {
data.configState = false;
data.response[0] = 0x41;
data.aboutToConfig = false;
}
else if (data.aboutToSwitchBetweenAnalogDigital && data.modeLocked == false) {
data.isDigital = true;
data.isAnalog = false;
data.response[0] = 0x41;
data.aboutToSwitchBetweenAnalogDigital = false;
}
else if (data.aboutToSetMotorStates) {
data.smallMotorOn = true;
}
}
break;
case 0x01: //Special commands after recieving a specific command
if (data.count == 2) { //this jumps over the header where cmd would be 0x01, may not need this
if (data.aboutToConfig) {
data.configState = true;
data.response[0] = 0xF3;
data.aboutToConfig = false;
}
else if (data.aboutToSwitchBetweenAnalogDigital && data.modeLocked == false) {
data.isDigital = false;
data.isAnalog = true;
data.response[0] = 0xF3;
data.aboutToSwitchBetweenAnalogDigital = false;
}
else if (data.configState && data.secondPass) { //Only works if we are in config mode
data.response[0] = 0xF3;
data.response[1] = 0x5A;
data.response[3] = 0x00;
data.response[4] = 0x00;
data.response[5] = 0x00;
data.response[6] = 0x00;
data.response[7] = 0x14;
}
else if (data.configState && data.FourcSecondPass) { //Only works if we are in config mode
data.response[0] = 0xF3;
data.response[1] = 0x5A;
data.response[3] = 0x00;
data.response[4] = 0x00;
data.response[5] = 0x06;
data.response[6] = 0x00;
data.response[7] = 0x00;
}
}
break;
case 0x03: //Lock Analog/Digital
data.modeLocked = true;
data.response[4] = 0x00;
data.response[5] = 0x00;
data.response[6] = 0x00;
data.response[7] = 0x00;
data.aboutToSwitchBetweenAnalogDigital = false;
break;
}// end switch
SPDR = data.response[data.count]; //Put the data into the registers to be transferred during this interuupt
if (data.count < (data.responseLength - 1)) { //only increase our counter until we are at the end of the needed response
data.count++;
}
data.sendAck = 1;
}
void loop() {
//Send ack after each bit transfer
if (data.sendAck) {
digitalWrite(ackPin, HIGH); //Again in opposite configuration due to external open drain circuitry
delayMicroseconds(4);
digitalWrite(ackPin, LOW);
data.sendAck = 0;
}
//Debouce the SS line
/*
We debounce this line so that our count is reset appropriately when we aren't communicating
*/
data.ssState = digitalRead(SS);
if (data.ssState == HIGH) { //Reset where we are when we aren't communicating
if (millis() - data.lastChange > 10) { //State is valid
data.count = 0;
data.responseLength = 0;
}
}
if (data.ssState != data.oldSsState) {
data.lastChange = millis();
}
data.oldSsState = data.ssState;
} //end loop()
Error code when compiling for an ESP32-dowdq6:
Arduino: 1.8.15 (Windows 10), Board: "ESP32 Dev Module, Disabled, Disabled, Default 4MB with spiffs (1.2MB APP/1.5MB SPIFFS), 240MHz (WiFi/BT), QIO, 80MHz, 16MB (128Mb), 921600, Core 1, Core 1, None, Disabled, Disabled"
Ps2ControllerEmulator_for_T-display:162:5: error: expected constructor, destructor, or type conversion before '(' token
162 | ISR (SPI_STC_vect) {
| ^
D:\Arduino\Ps2ControllerEmulator_for_T-display\Ps2ControllerEmulator_for_T-display.ino: In function 'void SlaveInit()':
Ps2ControllerEmulator_for_T-display:67:3: error: 'SPCR' was not declared in this scope
67 | SPCR |= bit (SPE); //Init as slave
| ^~~~
In file included from sketch\Ps2ControllerEmulator_for_T-display.ino.cpp:1:
Ps2ControllerEmulator_for_T-display:67:16: error: 'SPE' was not declared in this scope; did you mean 'SPI'?
67 | SPCR |= bit (SPE); //Init as slave
| ^~~
C:\Users\mazeb\AppData\Local\Arduino15\packages\esp32\hardware\esp32\3.0.7\cores\esp32/Arduino.h:110:25: note: in definition of macro 'bit'
110 | #define bit(b) (1UL << (b))
| ^
Ps2ControllerEmulator_for_T-display:78:15: error: 'SPIE' was not declared in this scope; did you mean 'SPI'?
78 | SPCR |= _BV(SPIE); //Attach Interrupts
| ^~~~
C:\Users\mazeb\AppData\Local\Arduino15\packages\esp32\hardware\esp32\3.0.7\cores\esp32/Arduino.h:111:25: note: in definition of macro '_BV'
111 | #define _BV(b) (1UL << (b))
| ^
D:\Arduino\Ps2ControllerEmulator_for_T-display\Ps2ControllerEmulator_for_T-display.ino: In function 'byte getControllerStateFirstByte()':
Ps2ControllerEmulator_for_T-display:97:19: error: 'PIND' was not declared in this scope
97 | byte pinState = PIND; //digitalRead() is too slow, we need to read the pins directly from the register
| ^~~~
D:\Arduino\Ps2ControllerEmulator_for_T-display\Ps2ControllerEmulator_for_T-display.ino: In function 'byte getControllerStateSecondByte()':
Ps2ControllerEmulator_for_T-display:124:19: error: 'PIND' was not declared in this scope
124 | byte pinState = PIND;
| ^~~~
D:\Arduino\Ps2ControllerEmulator_for_T-display\Ps2ControllerEmulator_for_T-display.ino: At global scope:
Ps2ControllerEmulator_for_T-display:162:5: error: expected constructor, destructor, or type conversion before '(' token
162 | ISR (SPI_STC_vect) {
| ^
exit status 1
expected constructor, destructor, or type conversion before '(' token
This report would have more information with
"Show verbose output during compilation"
option enabled in File -> Preferences.