Maker.io main logo

ESP32 PlayStation Controller

210

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
¥2,447
View More Details
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
¥1,137
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.