ESP32 PS3 Controller: Controlling a DC motor

In this tutorial we will learn how to control a DC motor using the ESP32 and a PS3 controller. We will be using the Arduino core and this library. The tests from this tutorial were done using a DFRobot’s ESP32 module integrated in a ESP32 development board.

Introduction

In this tutorial we will learn how to control a DC motor using the ESP32 and a PS3 controller. We will be using the Arduino core and this library.

For a similar tutorial but using a PS4 controller, please check here.

For an introductory tutorial on how to connect a PS3 controller to an ESP32 please check here.

In our implementation, we will be able to control if the motor is off or on at full speed by clicking the square and cross buttons, respectively.

We will also be able to control the speed of the motor by using the R2 button. We will be reading the analog value of the pressure applied to the button to set the speed accordingly, which will allow us to vary between off (button not pressed) and full speed (button fully pressed).

It’s very important to take in consideration that we cannot directly power a DC motor from a digital output pin of the ESP32, since it cannot provide the required current and it would get damaged.

Thus, we will need to use an additional integrated circuit called ULN2803A, which is able to provide the needed current and can also be controlled from a digital pin of a microcontroller such as the ESP32.

For a more detailed explanation about the ULN2803A please check here. For a basic explanation on how to control a DC motor (on / off) using an ESP32 and the ULN2803A, please check this tutorial.

The tutorial contains the electric diagram on how to connect the ESP32 to the integrated circuit and how to power it. Below at figure 1 is a copy of the schematic from the mentioned tutorial.

Schematic for controlling a DC motor with the ESP32, using a ULN2803A integrated circuit
Figure 1 – Electric diagram for controlling a DC motor with the ESP32 and a ULN2803A IC.

In sum, the ULN2803A works as a switch. It will turn on / off the connection of the DC motor to GND, depending on the state of the input pin labeled in the image as In 1.

So, when the GPIO of the ESP32 is at a digital high value (VCC), the ULN2803A will connect the DC motor to GND, and thus the motor will be on at full speed. When the GPIO is at a digital low value (GND), the motor will be disconnected from GND, and thus it will be off. Intermediate voltage values will allow to control the speed of the motor.

One important thing to mention is that the ULN2803A integrated circuit can be controlled by 3.3 V inputs and provide higher voltages. This allows us to control a 5 V DC motor with the ESP32, such as illustrated in figure 1.

Note that, in order to be able to control the speed of the motor, we will need to use the PWM functionalities of the ESP32. That way, we will be able to control the interval of time the digital signal that is controlling the ULN2803A is on and off, which allows to control the speed of the motor. You can read more about PWM here.

If the signal is fully on (duty cycle of 100%) the motor will be running at full speed. If the sinal is fully off (duty cycle of 0%), the motor will be stopped. Any duty cycles between these values will correspond to different running speeds of the motor.

So, we will be using the LED PWM features of the ESP32, already covered in this tutorial.

The tests from this tutorial were done using a DFRobot’s ESP32 module integrated in a ESP32 development board.

The code

We will start by including the Ps3Controller.h library, which will make available the Ps3 extern variable. We can use it to interact with the controller.

#include <Ps3Controller.h>

Followed by that we are going to define some variables that we will later use to setup the LED PWM feature.

The first one will be the frequency of the PWM signal that will be used to control the motor. We will be using a frequency of 1000 Hz.

The second one will be the LED PWM channel to be used. The LED PWM of the ESP32 is composed of 16 independent channels that have configurable duty cycles and wave periods. We will be using channel 0 (channels are numbered from 0 to 15).

Finally we will set the accuracy of the duty cycle to have a resolution of 8 bits. However, the channels support a maximum resolution of 16 bit.

Note that the use of 8 bits resolution allows to directly pass the value of the pressure applied to the R2 controller button, which also has a resolution of 8 bits.

int freq = 1000;
int ledChannel = 0;
int resolution = 8;

After this we will move to the Arduino setup function, where we will start by opening a serial connection.

Serial.begin(115200);

Then we will take care of the setup of the LED PWM functionality. To do it, we will call the ledcSetup function, passing as first input the led channel, as second the frequency and as third the resolution. Naturally we will be using the global variables we have defined before.

ledcSetup(ledChannel, freq, resolution);

It’s important to take in consideration that, by calling the previous function, we have just set the PWM channel that will be used, not the digital pin that will output the signal.

To do so we need to call the ledcAttachPin function, passing as first input the number of the digital pin that will output the signal and as second input the number of the PWM channel. I’ll be using pin 13 but you can use other if you prefer.

ledcAttachPin(13, ledChannel);

Once we finish the LED PWM setup we will take care of the initialization needed for the PS3 controller library to work. So, we will call the begin method on the Ps3 extern variable, passing as input a string with the Bluetooth address stored on the PS3 controller.

You can consult this tutorial if you haven’t yet obtained this address for your controller. It contains a detailed explanation on how to do it and the software needed. The tutorial covers the procedure using a PS4 controller but it is the same for a PS3 controller.

Note that this method call will take care of initializing the ESP32 Bluetooth layer along with all the services needed to receive an incoming PS3 controller connection.

Since the begin method returns a Boolean indicating if the initialization was successful or not, we will use it for an error checking.

if (!Ps3.begin("yourDeviceAddress")) {
    Serial.println("Initialization failed.");
    return;
}

Serial.println("Initialization finished.");

To finalize the Arduino setup function we will register some handling functions to take care of the event that is generated when a controller connects and also when a button is pressed.

We will call the attach method on the Ps3 extern variable to register the event handling function that will process buttons pressed. We will call it onEvent and check its implementation later.

Ps3.attach(onEvent);

Then we will call the attachOnConnect method to register the event handling function that will be triggered when a controller connects to the ESP32. We will call it onConnection and also analyze its implementation later.

Ps3.attachOnConnect(onConnection);

The full Arduino setup function can be seen below.

void setup() {

  Serial.begin(115200);

  ledcSetup(ledChannel, freq, resolution);
  ledcAttachPin(13, ledChannel);

  if (!Ps3.begin("yourDeviceAddress")) {
    Serial.println("Initialization failed.");
    return;
  }

  Serial.println("Initialization finished.");

  Ps3.attach(onEvent);
  Ps3.attachOnConnect(onConnection);

}

Since our implementation will be event based, we won’t need to do anything in the Arduino main loop. Thus, we will simply delete the corresponding FreeRTOS task by calling the vTaskDelete function and passing as input the value NULL, which will make the calling task delete itself.

The full main loop can be seen below.

void loop() {
  vTaskDelete(NULL);
}

We will now analyze the implementation of our callback functions. Note that both of them need to follow a predefined signature: they must return void and receive no arguments.

We will start by the simpler one: the controller connected event handling function (called onConnection). In short, this function will just confirm that the controller is connected by checking the output of the isConnected method and print a message to the serial port.

void onConnection() {
  
  if (Ps3.isConnected()) {
    Serial.println("Controller connected.");
  }
}

The other function, called onEvent, will be responsible for handling the button clicked events and act accordingly, making our DC motor run or stop.

The first thing we will do is checking if the analog value of the R2 button has changed. To do so, we will need to navigate through a couple of data structures, as we will see.

We will start by access the event attribute of the Ps3 extern variable. This attribute is a struct of type ps3_event_t.

Then we will access to the analog_changed element of the previous struct, which is another struct of type ps3_analog_t. On this struct, we will now access to the button element, which is another struct of type ps3_analog_button_t.

Finally, on this struct, we can check if the analog value of the R2 button has changed by accessing the r2 element.

if (Ps3.event.analog_changed.button.r2) {
    // set motor to run at a given speed
}

Inside the previous conditional block we will now read the actual analog button pressure value. To do this, we will again navigate some structs.

So, we start by the data attribute of the Ps3 extern variable. This attribute is a struct of type ps3_t. On this struct, we will access the element called analog, which is a struct of type ps3_analog_t.

Then we will access to the element button, which is another struct of type ps3_analog_button_t. Finally, like before, we access the element called r2 which, this time, contains the current analog value of the button.

uint8_t analogVal = Ps3.data.analog.button.r2;

To use this value as the duty cycle of the signal that will be used to control the motor, we just need to call the ledcWrite function, passing as first input the number of the channel and as second input the duty cycle.

Recall from before that we have a resolution of the PWM duty cycle of 8 bits, which mean we can specify a value of 0 (stopped) to 255 (full speed). Remember that we have also mentioned that the analog value of the pressure applied to a button of the controller is represented by 8 bits, which means it will fall in the same interval between 0 and 255.

The full conditional block with the ledcWrite function call can be seen below.

if (Ps3.event.analog_changed.button.r2) {

    uint8_t analogVal = Ps3.data.analog.button.r2;
    ledcWrite(ledChannel, analogVal);
}

We will now handle the cross and square button, which will be used to put the motor to run at full speed and to stop it, respectively. The implementation will be similar, except that we will access slightly different structs.

Note that in this case we won’t be considering the pressure applied to both of these buttons (although the library supports obtaining it, as seen here). Instead, we will just check if the buttons are pressed or not and act accordingly.

So, starting by the cross button, we will once again access the event attribute of the Ps3 extern variable.

Then we will access the button_down field of the previous struct. This is a struct of type ps3_button_t. Then, we can simply access the button we want to check if it was pressed.

In our case, we will check the field called cross, which basically indicates if the cross button was pressed or not. If it was, we will set the motor to run at full speed by writing a duty cycle value of 255.

if(Ps3.event.button_down.cross){
    ledcWrite(ledChannel, 255);
}

The implementation for the square button is similar, except that we will set a duty cycle of 0, thus stopping the motor.

if(Ps3.event.button_down.square){
     ledcWrite(ledChannel, 0);
}

The full event handling function can be seen below, containing the 3 conditional blocks that will allow to control the motor.

Take in consideration that we are accessing a lot of different structs, which might be interesting to analyze if you want to use other buttons and features of the controller. The structs definition can be seen here.

Note also that we are not explicitly handling the corner cases where the motor is already running, for example, by pressing the cross button, and then the user presses R2. Naturally, the code below can be extended to handle such cases. Nonetheless, for this tutorial, things were kept simpler.

void onEvent(){
  
  if (Ps3.event.analog_changed.button.r2) {

    uint8_t analogVal = Ps3.data.analog.button.r2;
    ledcWrite(ledChannel, analogVal);
  }

  if(Ps3.event.button_down.cross){
    ledcWrite(ledChannel, 255);
  }
  
  if(Ps3.event.button_down.square){
     ledcWrite(ledChannel, 0);
  }
  
}

The final complete code can be seen below.

#include <Ps3Controller.h>

int freq = 1000;
int ledChannel = 0;
int resolution = 8;

void onEvent(){
  
  if (Ps3.event.analog_changed.button.r2) {

    uint8_t analogVal = Ps3.data.analog.button.r2;
    ledcWrite(ledChannel, analogVal);
  }

  if(Ps3.event.button_down.cross){
    ledcWrite(ledChannel, 255);
  }
  
  if(Ps3.event.button_down.square){
     ledcWrite(ledChannel, 0);
  }
  
}

void onConnection() {
  
  if (Ps3.isConnected()) {
    Serial.println("Controller connected.");
  }
}

void setup() {

  Serial.begin(115200);

  ledcSetup(ledChannel, freq, resolution);
  ledcAttachPin(13, ledChannel);

  if (!Ps3.begin("yourDeviceAddress")) {
    Serial.println("Initialization failed.");
    return;
  }

  Serial.println("Initialization finished.");

  Ps3.attach(onEvent);
  Ps3.attachOnConnect(onConnection);

}

void loop() {
  vTaskDelete(NULL);
}

Testing the code

To test the whole system, the first thing we need to do is connecting the ESP32, the ULN2803A and the DC motor accordingly to the schematic shown in figure 1. Make sure to only power on the whole system after everything is connected.

Once finished, compile and upload the code from the previous section to your ESP32. After the procedure finishes, open the Arduino IDE serial monitor.

When the Bluetooth layer is initialized, you should get a message indicating such.

After that, you can connect the PS3 controller by pressing the PS button. Once you do it, the controller should connect to the ESP32 and a message should get printed to the serial monitor, like illustrated in figure 2.

Output of the program after the initialization and the connection of the PS3 controller.
Figure 2 – Output of the program after the initialization and the connection of the PS3 controller.

Finally, you can start testing the system by clicking the buttons. If you click the cross button, the DC motor should start spinning at full speed. If you click the square button, it should stop.

If you use the R2 button, the velocity of the DC motor should vary accordingly to the pressure applied to the button.

The video below demonstrates the expected behavior.

Leave a Reply

%d bloggers like this: