Maker.io main logo

ESP32 PlayStation Controller

151

2024-06-18 | By Adafruit Industries

License: See Original Project Wifi ESP32

Courtesy of Adafruit

Guide by John Park

Overview

controller_1

 

The original PlayStation controller is great, but it's wired for use with ‎a PlayStation. This guide shows how to cut the cord and convert it to ‎a wireless Bluetooth gamepad for your computer gaming needs.‎

An Adafruit ItsyBitsy ESP32 and Arduino software make it all possible, ‎and a LiPo battery and built-in charger keep it powered.‎

Parts

PlayStation Controller

Use an original Sony PlayStation controller model SCPH-1080 -- the ‎kind before the dual analog sticks or rumble motors were added. ‎These can be had for around $10 at a retro gaming store or online ‎auction.‎

play_1a

Arduino IDE Setup

You need to install the right USB-to-serial driver for your chip in ‎addition to the Arduino IDE. If you are unsure which is the right one, ‎install both!‎

Install Arduino IDE

The first thing you will need to do is to download the latest release of ‎the Arduino IDE. You will need to be using version 1.8 or higher for ‎this guide.‎

Arduino IDE Download

Install CP2104 / CP2102N USB Driver

The USB-to-Serial converter that talks to the ESP32 chip itself will ‎need a driver on your computer's operating system. The driver is ‎available for Mac and Windows. It is already built into Linux.‎

Click here to download the CP2104 USB Driver

Install CH9102 / CH34X USB Driver

Newer ESP32 boards have a different USB-to-serial converter that ‎talks to the chip itself and will need a driver on your computer's ‎operating system. The driver is available for Mac and Windows. It is ‎already built into Linux.‎

If you would like more detail, check out the guide on installing these ‎drivers.‎

Click here to download the Windows driver

Click here to download the Mac driver

Install ESP32 Board Support Package

After you have downloaded and installed the latest version of ‎Arduino IDE, you will need to start the IDE and navigate ‎to the Preferences menu. You can access it from the File menu ‎in Windows or Linux, or the Arduino menu on OS X.‎

file_2

A dialog will pop up just like the one shown below.‎

dialog_3

We will be adding a URL to the new Additional Boards Manager ‎URLs option. The list of URLs is comma separated, and you will only ‎have to add each URL once. New Adafruit boards and updates to ‎existing boards will automatically be picked up by the Board ‎Manager each time it is opened. The URLs point to index files that ‎the Board Manager uses to build the list of available & installed ‎boards.‎

To find the most up to date list of URLs you can add, you can visit the ‎list of third party board URLs on the Arduino IDE wiki. We will only ‎need to add one URL to the IDE in this example, but you can add ‎multiple URLS by separating them with commas. Copy and paste ‎the link below into the Additional Boards Manager URLs option in ‎the Arduino IDE preferences.‎

https://raw.githubusercontent.com/espressif/arduino-esp32/gh-‎pages/package_esp32_dev_index.json

list_4

If you have multiple boards you want to support, say ESP8266 and ‎Adafruit, have both URLs in the text box separated by a comma (,)‎

Once done click OK to save the new preference settings.‎

The next step is to actually install the Board Support Package (BSP). ‎Go to the Tools → Board → Board Manager submenu. A dialog should ‎come up with various BSPs. Search for esp32.

boardsmanager_5

Click the Install button and wait for it to finish. Once it is finished, ‎you can close the dialog.‎

In the Tools → Board submenu you should see ESP32 Arduino and in ‎that dropdown, it should contain the ESP32 boards along with all the ‎latest ESP32 boards.‎

Look for the board called Adafruit ItsyBitsy ESP32.‎

board_6

The upload speed can be changed: faster speed makes uploads take ‎less time but sometimes can cause upload issues. 921600 should ‎work fine, but if you're having issues, you can drop down lower.‎

Controller Circuit

circuit_7

The PlayStation controller PCB uses conductive pads that short to ‎ground when the buttons are pressed. Using a continuity tester, I ‎diagrammed these traces, and the copper test points we will use to ‎wire the buttons to the microcontroller.‎

tester_8

diagrams_10

You can see from the above diagrams that we'll wire up most of the ‎PlayStation controller buttons to GPIO pins on the ItsyBitsy ESP32. ‎The R2 trigger button will be wired to the Reset button, and the L2 is ‎left unused. (You could choose to wire it and adjust the Arduino ‎sketch if you like.)‎

Code the Controller

Copy the example below and paste it into the Arduino IDE.‎

You must change the ssid and password in the example code to your ‎WiFi SSID and password before uploading this to your board. This is ‎only necessary if you plan to use the over-the-air (OTA) update ‎feature, otherwise you can leave these alone.‎

You can also change the sleepSeconds value if you want your ‎controller to go into deep sleep sooner or later than the default 30 ‎seconds.‎

code_11

Once you've made these changes, upload the code to your ItsyBitsy ‎ESP32.‎

Download File

Copy Code
// SPDX-FileCopyrightText: 2024 John Park for Adafruit Industries
//
// SPDX-License-Identifier: MIT
/*
* Feather ESP32 Bluetooth LE gamepad https://github.com/lemmingDev/ESP32-BLE-Gamepad
* Deep sleep with wake on START button press
* https://randomnerdtutorials.com/esp32-deep-sleep-arduino-ide-wake-up-sources/

* OTA WiFi uploads
* https://docs.espressif.com/projects/arduino-esp32/en/latest/ota_web_update.html
* Sketch > Compile binary, then http://esp32.local/?userid=admin&pwd=admin
* pick compiled .bin, upload.
*/

#include <Arduino.h>
#include <BleGamepad.h>
#include <Adafruit_NeoPixel.h>

#include <esp_wifi.h>
#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <Update.h>

bool web_ota = false;

int sleepSeconds = 30; // how long is it inactive before going to sleep

const char* host = "esp32";
const char* ssid = "xxxxxxx"; // your WiFi SSID here
const char* password = "xxxxxxxx"; // your WiFi password here
WebServer server(80);

/*
* Login page
*/

const char* loginIndex =
"<form name='loginForm'>"
"<table width='20%' bgcolor='A09F9F' align='center'>"
"<tr>"
"<td colspan=2>"
"<center><font size=4><b>ESP32 Login Page</b></font></center>"
"<br>"
"</td>"
"<br>"
"<br>"
"</tr>"
"<tr>"
"<td>Username:</td>"
"<td><input type='text' size=25 name='userid'><br></td>"
"</tr>"
"<br>"
"<br>"
"<tr>"
"<td>Password:</td>"
"<td><input type='Password' size=25 name='pwd'><br></td>"
"<br>"
"<br>"
"</tr>"
"<tr>"
"<td><input type='submit' onclick='check(this.form)' value='Login'></td>"
"</tr>"
"</table>"
"</form>"
"<script>"
"function check(form)"
"{"
"if(form.userid.value=='admin' && form.pwd.value=='admin')"
"{"
"window.open('/serverIndex')"
"}"
"else"
"{"
" alert('Error Password or Username')/*displays error message*/"
"}"
"}"
"</script>";

/*
* Server Index Page
*/

const char* serverIndex =
"<script src='https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js'></script>"
"<form method='POST' action='#' enctype='multipart/form-data' id='upload_form'>"
"<input type='file' name='update'>"
"<input type='submit' value='Update'>"
"</form>"
"<div id='prg'>progress: 0%</div>"
"<script>"
"$('form').submit(function(e){"
"e.preventDefault();"
"var form = $('#upload_form')[0];"
"var data = new FormData(form);"
" $.ajax({"
"url: '/update',"
"type: 'POST',"
"data: data,"
"contentType: false,"
"processData:false,"
"xhr: function() {"
"var xhr = new window.XMLHttpRequest();"
"xhr.upload.addEventListener('progress', function(evt) {"
"if (evt.lengthComputable) {"
"var per = evt.loaded / evt.total;"
"$('#prg').html('progress: ' + Math.round(per*100) + '%');"
"}"
"}, false);"
"return xhr;"
"},"
"success:function(d, s) {"
"console.log('success!')"
"},"
"error: function (a, b, c) {"
"}"
"});"
"});"
"</script>";

////////////////////////////////////// GAMEPAD
#define numOfButtons 12
// sleep wake button definition (also update line in setup(): 'esp_sleep_enable_ext0_wakeup(GPIO_NUM_4,0);')
#define BUTTON_PIN_BITMASK 0x10 // start button on RTC GPIO pin 4 which is 0x10 (2^4 in hex)
// RTC_DATA_ATTR int bootCount = 0;

BleGamepad bleGamepad("ItsyController", "Adafruit", 100); // name, manufacturer, batt level to start
byte previousButtonStates[numOfButtons];
byte currentButtonStates[numOfButtons];

// ItsyBitsy EPS32: 13, 12, 14, 33, 32, 7, 5, 27, 15, 20, 8, 22, 21, 19, 36, 37, 38, 4, 26, 25
// RTC IO: 13, 12, 14, 33, 32, 27, 15, 36, 37, 38, 4, 26, 25
// pins that act funny: 5, 37, 22
byte buttonPins[numOfButtons] = { 13, 12, 14, 33, 32, 7, 27, 15, 21, 19, 4, 26 }; // ItsyBitsy
byte physicalButtons[numOfButtons] = { 1, 2, 4, 5, 7, 8, 15, 16, 13, 14, 12, 11 }; // controller assignments
// b0, b1, b3, b4, b6, b7, b14, b15, b12, b13, b10, b11
// gampad: O/b0, X/b1, ^/b3, []]/b4, l_trig/b6, r_trig/b7, up/b14 , down/b15 , left/b12 , right/b13, select/b11, start/b10

int last_button_press = millis();
int sleepTime = (sleepSeconds * 1000);

Adafruit_NeoPixel pixel(1, 0, NEO_GRB + NEO_KHZ800); // Itsy on-board NeoPixel

void setup()
{
Serial.begin(115200);
delay(500);

//Print the wakeup reason for ESP32
// print_wakeup_reason();
esp_sleep_enable_ext0_wakeup(GPIO_NUM_4,0); //1 = High, 0 = Low

for (byte currentPinIndex = 0; currentPinIndex < numOfButtons; currentPinIndex++)
{
pinMode(buttonPins[currentPinIndex], INPUT_PULLUP);
previousButtonStates[currentPinIndex] = HIGH;
currentButtonStates[currentPinIndex] = HIGH;
}

bleGamepad.begin();
delay(100);
pixel.begin();
pixel.clear();

if (web_ota) {

// Connect to WiFi network
WiFi.begin(ssid, password);
Serial.println("");

// Wait for connection for 20 seconds, then move on
unsigned long startTime = millis(); // Get the current time
while (!(WiFi.status() == WL_CONNECTED) && ((millis() - startTime) < 2000)) {
delay(500);
Serial.print(".");
}

if (WiFi.status() == WL_CONNECTED) {

Serial.println("");
Serial.print("Connected to ");
Serial.println(ssid);
Serial.print("IP address: ");
Serial.println(WiFi.localIP());

/*use mdns for host name resolution*/
if (!MDNS.begin(host)) { //http://esp32.local
Serial.println("Error setting up MDNS responder!");
while (1) {
delay(1000);
}
}
Serial.println("mDNS responder started");
/*return index page which is stored in serverIndex */
server.on("/", HTTP_GET, []() {
server.sendHeader("Connection", "close");
server.send(200, "text/html", loginIndex);
});
server.on("/serverIndex", HTTP_GET, []() {
server.sendHeader("Connection", "close");
server.send(200, "text/html", serverIndex);
});
/*handling uploading firmware file */
server.on("/update", HTTP_POST, []() {
server.sendHeader("Connection", "close");
server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK");
ESP.restart();
}, []() {
HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
Serial.printf("Update: %s\n", upload.filename.c_str());
if (!Update.begin(UPDATE_SIZE_UNKNOWN)) { //start with max available size
Update.printError(Serial);
}
} else if (upload.status == UPLOAD_FILE_WRITE) {
/* flashing firmware to ESP*/
if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
Update.printError(Serial);
}
} else if (upload.status == UPLOAD_FILE_END) {
if (Update.end(true)) { //true to set the size to the current progress
Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize);
} else {
Update.printError(Serial);
}
}
});
server.begin();
}
else {
Serial.println("");
Serial.println("WiFi connection timed out, you may need to update SSID/password. Moving on now.");
}
}
}

void loop()
{
if (web_ota) {
server.handleClient();
delay(1);
}

if (bleGamepad.isConnected())
{
pixel.setPixelColor(0, 0x000033);
pixel.show();

for (byte currentIndex = 0; currentIndex < numOfButtons; currentIndex++)
{
currentButtonStates[currentIndex] = digitalRead(buttonPins[currentIndex]);

if (currentButtonStates[currentIndex] != previousButtonStates[currentIndex])
{
last_button_press = millis(); // update last_button_press for sleep timing

if (currentButtonStates[currentIndex] == LOW)
{
bleGamepad.press(physicalButtons[currentIndex]);
}
else
{
bleGamepad.release(physicalButtons[currentIndex]);
}
}
}

if (currentButtonStates != previousButtonStates)
{
for (byte currentIndex = 0; currentIndex < numOfButtons; currentIndex++)
{
previousButtonStates[currentIndex] = currentButtonStates[currentIndex];
}

bleGamepad.sendReport();
}
if (millis() - last_button_press > sleepTime) {
server.stop();
delay(300);
esp_wifi_stop();
delay(300);
esp_deep_sleep_start();
}
}
}

‎View on GitHub

Gamepad

You'll use the BleGamepad library to create the gamepad object:‎

BleGamepad bleGamepad("ItsyController", "Adafruit", 100); // name, manufacturer, batt ‎level to start

You can pick your own string for the name, so in this case ‎‎"ItsyController" will show up on your computer or other device during ‎pairing. The battery level feature is not used in this project.‎

Deep Sleep

Deep sleep mode saves on battery consumption by turning off the ‎ESP32's processor, WiFi and Bluetooth radios, while keeping the ‎Ultra Low Power co-processor (ULP) active, checking for the wake-‎up call. ‎

Wake up sources can include a timer, cap touch pin, and external ‎sources, a.k.a. buttons. We'll use the PlayStation's start button as our ‎wake-up source.‎

The way the code works is to keep track of when buttons are pressed ‎and if none has been touched for thirty seconds (or ‎whatever sleepTime you pick), the server and WiFi radio are stopped, ‎and then esp_deep_sleep_start(); is called.‎

Download File

Copy Code
if (millis() - last_button_press > sleepTime) {
server.stop();
delay(300);
esp_wifi_stop();
delay(300);
esp_deep_sleep_start();
}

Wake Up

While in deep sleep, the ULP can keep an eye on any of the real time ‎clock (RTC) GPIO pins. On the ItsyBitsy ESP32 this is any of the ‎following pins: 13, 12, 14, 33, 32, 27, 15, 36, 37, 38, 4, 26, 25.‎

We'll use pin 4, which is the one the start button is connected to:‎

esp_sleep_enable_ext0_wakeup(GPIO_NUM_4,0);‎

The '0' indicates that wake up will happen when the indicated pin 4 ‎goes low.‎

Web Update

If you want to update the code after you've closed up your controller, ‎you can use the over-the-air (OTA) web update. ‎

Point your browser at the ItsyBitsy's server at http://esp32.local and ‎login with:‎

  • username = admin
  • password = admin

Follow the info in this guide on compiling and uploading your ‎firmware.‎

Convert the Controller

These are the main steps you'll do for the conversion:‎

  • open up the controller
  • remove wired connector cable and the original IC
  • solder wires from the button test points to the ItsyBitsy ESP32
  • cut a little bit of plastic to fit the USB connector for charging
  • optionally, drill a couple of holes to see the status LEDs

Open the Controller

Unscrew the eight screws with a Philips #1 screwdriver and set them ‎in a safe place.‎

Open the controller by lifting up the back shell.‎

Remove the main PCB and shoulder button boards, then set the ‎case and rubber button pads aside.‎

open_12

open_13

open_14

open_15

LED Holes

You can drill a small hole if you like in the back of the case so you ‎can see the indicator NeoPixel LED.‎

A hole in the inner side of the right grip in the back of the case can ‎be used to see the charger LED status.‎

You could optionally get fancy and fashion some light pipes from ‎clear plastic. The best bet is usually to scavenge light pipes from old, ‎broken hardware.‎

holes_16

holes_17

holes_18

holes_19

holes_20

holes_21

Desolder Cable

Flip the PCB over and desolder the six pins that connect the cable to ‎the PCB. It's helpful to use some extra flux or fresh solder, heat up ‎each joint and then use a solder sucker to, well, suck up the solder.‎

You can also desolder and remove the large electrolytic capacitor to ‎free up some space.‎

We'll use these holes for routing wires, so it's good to get them nice ‎and free of solder.‎

cable_22

cable_23

cable_24

cable_25

IC Removal

This is also a good time to desolder and remove the original IC ‎controller chip from the PCB.‎

You can see from these pictures that I had already wired the buttons ‎pads when I realized the IC needed to be removed, otherwise I was ‎seeing spurious button presses occur as the IC was being ‎accidentally powered in unintended ways.‎

ic_26

ic_27

Wiring

This is the most fiddly part. Wiring the buttons and ground from the ‎PCB to the ItsyBitsy GPIO pins.‎

Cut a six-inch length of the rainbow hookup wires, strip one end, ‎solder to the appropriate solder point, and then route it through a ‎PCB hole to the other side where you'll feed the wire through the ‎matched ItsyBitsy GPIO pad.‎

I ran all of the wires through the pads as shown before cutting, ‎stripping, and soldering them to the ItsyBitsy in order to get the ‎routing and lengths right.‎

Consult the wiring diagram and PCB overlay shown below.‎

wiring_28

wiring_29

wiring_30

wiring_31

wiringcircuit_32

wiringdiagram_33

wiringcircuit_34

Shoulder Buttons

Here are some detailed photos of the shoulder button wiring. I used ‎Kapton tape to dress these wires as shown.‎

Note the R2 trigger button wiring from the extension PCB to the ‎ItsyBitsy reset pin.‎

buttons_35

buttons_36

buttons_37

buttons_38

buttons_39

LiPo Charger

Prep the charger by soldering 6" lengths of wire as shown. Once the ‎controller is partly assembled in the shell we'll solder the other ends ‎to the BAT, G, and USB pins on the ItsyBitsy.‎

charger_40

charger_41

charger_42

charger_43

charger_44

USB Power Extension

You'll add a USB-C jack to the top of the controller to charge the ‎battery.‎

Solder wires from USB jack breakout G to ground on the ItsyBitsy, ‎and breakout V to USB pin on the ItsyBitsy.‎

Trim some plastic from the case to accommodate the breakout jack.‎

extension_45

extension_46

extension_47

extension_48

extension_49

extension_50

Attach USB Breakout

Use a bit of CA glue to affix the USB breakout to the controller PCB.‎

You can add some tape to insulate the exposed contacts from ‎accidental shorts.‎

attach_51

attach_52

The USB port is for charging only, not data. You'll be able to use over-‎the-air (OTA) firmware updates over WiFi should you ever want to ‎adjust the code once the controller is closed back up.‎

Close the Controller

Carefully close the shell and screw it back together with the eight ‎Philips screws.‎

Your controller is ready for play! See the next page in the guide for ‎details on pairing and use.‎

close_53

close_54

close_55

close_56

Charge It

Plug in a USB C cable to charge.‎

charge_57

Use the Controller

Tap the Start button on the controller to wake it up -- there is no ‎on/off switch, but that's OK, the battery should last about six months ‎between charges thanks to deep sleep mode.‎

Pair it with your computer or mobile device in the Bluetooth settings.‎

Use a gamepad tester, such ‎as https://hardwaretester.com/gamepad to check the buttons are all ‎working as expected.‎

In your game or emulator, map the controls however you like.‎

Have fun!‎

map_58

gamepad_59

games_60

 

Power Profiling

power_61

In order to estimate battery life for both active use and deep sleep, ‎the Nordic PPK came in very handy! It provides power to the ‎ItsyBitsy ESP32 and can very accurately measure the current used.‎

  • Nordic nRF-PPK2 - Power Profiler Kit II

To profile your project, follow this guide page.‎

Active Current Draw

In active use with the BLE connection and buttons being pressed, ‎the controller draws ~123mA, you can see this in the selected ‎average in the Nordic PPK graph below.‎

This translates to about three hours of play from the 350mAh LiPo ‎battery.‎

play_62

Deep Sleep Draw

After 30 seconds of inactivity, the controller is programmed to go ‎into deep sleep. Here it draws an impressively low current, about ‎‎0.077mA. This translates to an impressive six months of deep sleep ‎time!‎

draw_63

batterylife_64

Initial Not-So-Deep Sleep

When I first profiled the power, I was seeing pretty high power ‎consumption during what should have been deep sleep -- around ‎‎1.8mA.‎

After many attempts at cutting out code sections and scouring ‎the documentation, I discovered I needed to not only send ‎the esp_deep_sleep_start() command, but also explicitly turn off the WiFi ‎radio with esp_wifi_stop().‎

With this magical combo in place the ItsyBitsy fell into a wonderful ‎slumber, sipping a mere 0.077mA as it slept 😴.

sleep_65

Mfr Part # 5889
ADAFRUIT ITSYBITSY ESP32 - PCB A
Adafruit Industries LLC
R259,08
View More Details
16MM ILLUMINATED PUSHBUTTON - GR
Mfr Part # 1443
16MM ILLUMINATED PUSHBUTTON - GR
Adafruit Industries LLC
Mfr Part # 5180
SIMPLE USB C SOCKET BREAKOUT
Adafruit Industries LLC
Mfr Part # 4730
HOOK-UP 30AWG MULTIPLE 918.6'
Adafruit Industries LLC
R120,44
View More Details
Add all DigiKey Parts to Cart
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.