In this tutorial we will check how to control a DC motor using an ESP32 and a PS4 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 check how to control a DC motor using an ESP32 and a PS4 controller. We will be using the Arduino core and this library.
For a detailed explanation on how to connect a PS4 controller to the ESP32 using the already mentioned library, please check here. Note that you will need to know the Bluetooth address stored on the controller and the procedure on how to obtain it can be found on the mentioned tutorial.
One important aspect that we need to account for is that we cannot directly power a DC motor using an ESP32 digital pin. The pins of the ESP32 cannot provide enough current and would get damaged if we attempted to do it.
Consequently, we will need to use an additional integrated circuit called ULN2803A. Naturally there are plenty of other options to control a DC motor from a microcontroller, but we will follow this approach.
We have already covered how to connect the ESP32 to an ULN2803A to control a DC motor on this tutorial. The tutorial contains a detailed explanation about the circuit and an example code for on / off control of the motor.
Below at figure 1 we can see a copy of the electric diagram covered in the tutorial. The ULN2803A will be providing the current to the motor and the ESP32 will be controlling if the motor is running or not.
So, when the GPIO of the ESP32 is at a digital high value (VCC), the ULN2803A will connect the motor to GND, making it run. When the GPIO is at a digital low value (GND), the motor will be disconnected from GND and, consequently, it will be stopped.

Note that the ULN2803A can be controlled with 3.3 V logic (as shown in the datasheet), which means it is compatible with the ESP32 digital pins.
In this tutorial we will not only see how to turn the motor on and off but also how to control its speed.
In terms of implementation, we will make the motor run at full speed when the user presses the cross button of the PS4 controller and we will make it stop when the user presses the square button.
For speed control, we will make use of the R2 button. Depending on how much pressure is being applied to the button, the motor speed will be set accordingly.
To be able to control the speed of the motor, we will need to use the LED PWM functionality supported by the ESP32. This feature was already covered in this tutorial. In short, we will be able to set the duty cycle of the digital signal that will be used to control the DC motor. Higher duty cycles will make the motor run faster.
You can ready here a very complete explanation on what is PWM and how it works.
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 PS4Controller.h library. Like already covered in other tutorials, this will make available the PS4 extern variable, which allows us to interact with a PS4 controller.
#include <PS4Controller.h>
Next we will define some global variables that will be used to parametrize the LED PWM functionality. We will start by defining the frequency of the signal. We will set it to 1000 Hz.
Then we will define which LED PWM channel to use. The ESP32 supports 16 independent channels, numbered from 0 to 15. We will be using channel 0.
We will also define a variable to specify the resolution of the duty channel. We will be using 8 bits, even though the ESP32 supports a maximum resolution of 16 bits. We will be choosing 8 bits because the value of pressure applied to the R2 button is also represented by 8 bits, which means we can use the values directly.
int freq = 1000;
int ledChannel = 0;
int resolution = 8;
We will also define an additional control variable that contains the previous pressure applied to the R2 button. We will set it to 0 and check why we need it later.
uint8_t previousVal = 0;
Moving on to the Arduino setup, the first thing we will do is initializing a serial connection. That way, we will be able to output some results from our program.
Serial.begin(115200);
After this we will use the previously defined variables to setup the LED PWM feature. We do this by calling the ledcSetup function and passing as input the following parameters, by the order they appear listed:
- LED PWM channel
- Signal frequency
- PWM resolution
ledcSetup(ledChannel, freq, resolution);
Note that we just did the setup of channel 0, but we haven’t specified yet which digital pin of the ESP32 will be outputting the signal. For that, we still need to call the ledcAttachPin function, passing as first input the number of the pin and as second the number of the channel.
I’ll be using pin 13. This should be the same pin connected to the ULN2803A integrated circuit in figure 1. You can use other digital pin if you prefer.
ledcAttachPin(13, ledChannel);
Next we will initialize the Bluetooth layer and set the ESP32 to be ready to receive the connection of a PS4 controller. We do this by calling the begin method on the PS4 extern variable, passing as input, as a string, the Bluetooth address stored on the controller.
Since this method returns a Boolean value indicating if the initialization procedure was successful or not, we will do an error checking.
if(!PS4.begin("01:01:01:01:01:01")){
Serial.println("Initializetion failed");
return;
};
Serial.println("Initialization finished.");
To finalize the Arduino setup, we will register a callback to handle the controller connection event and another to handle button presses.
This is done by calling the attachOnConnect and attach methods on the PS4 extern variable, respectively. We will check the implementation of the callback functions later.
The full Arduino setup can be seen below and it already contains the calls to register the callback functions.
void setup()
{
Serial.begin(115200);
ledcSetup(ledChannel, freq, resolution);
ledcAttachPin(13, ledChannel);
if(!PS4.begin("01:01:01:01:01:01")){
Serial.println("Initializetion failed");
return;
};
Serial.println("Initialization finished.");
PS4.attach(onEvent);
PS4.attachOnConnect(onConnection);
}
Since we are not going to make use of the Arduino main loop, we will delete the corresponding FreeRTOS task with a call to the vTaskDelete function, passing as input NULL.
void loop()
{
vTaskDelete(NULL);
}
Now we will analyze the implementation of the callback functions. We will start by the controller connection function, which we have named onConnection.
This function will simply call the isConnected method of the PS4 extern variable, to confirm a controller is connected, and print a message to the serial port.
void onConnection() {
if (PS4.isConnected()) {
Serial.println("Controller connected.");
}
}
Then we will see how to implement the onEvent callback function, which will be responsible for handling PS4 controller button clicks and control the DC motor accordingly.
We will start by handling the R2 button presses. The first thing we will do is obtaining the current analog value of the R2 button, which is a value between 0 and 255 (8 bits) that represents how much pressure is being applied to the button. 0 means it is not being pressed and 255 means it is being fully pressed.
To do so, we will navigate through some structs that are defined on this file. Our starting point will be the PS4 extern variable, which we will use to access its data member.
This member is a struct of type ps4_t. From this struct, we will access the analog field, which is another struct of type ps4_analog_t. We will then access the field called button, which is a struct of type ps4_analog_button_t.
This final struct allows us to access a field called r2, which contains the current analog value of this button.
uint8_t analogVal = PS4.data.analog.button.r2;
Since we just want to update the speed of the motor if the analog value of the button has changed, we will compare its value against the previousVal variable we have defined at the beginning of the program (this variable will be update later with the current analog value, after the conditional statements).
So, if the value has changed, then we will set the duty cycle of the signal to the current analog value of the R2 button. Since both are 8 bit values (they range between 0 and 255), we can directly pass the analog read from the controller to the function that will set the duty cycle.
To set the duty cycle we use the ledcWrite function, passing as first input the LED PWM channel and as second input the duty cycle.
if(previousVal != analogVal) {
ledcWrite(ledChannel, analogVal);
}
After this we need to update the previousVal variable to contain the current analog value obtained from the controller.
previousVal = analogVal;
Now that we have handled the R2 button functionality, we will take care of the cross and square buttons.
Note that, for simplicity, we won’t be covering corner cases where the user, for example, presses the cross and R2 buttons at the same time.
So, to check if the cross button was clicked, we will again navigate through some structs. We will start with the event attribute of the PS4 extern variable. This attribute is a struct of type ps4_event_t.
Then we will access the button_down field, which is another struct of type ps4_button_t. Finally we can access the fields of this struct to check if the corresponding button was pressed.
In our case we will be accessing the cross field to check if this PS4 controller button was pressed by the user. If so, we will call the ledcWrite function to set the motor to run at full speed. Recall that this means we need to set the duty cycle to its maximum allowed value: 255.
if(PS4.event.button_down.cross){
ledcWrite(ledChannel, 255);
}
We will do the same procedure to check if the square button was pressed. However, if it is pressed, we will set the duty cycle to 0, meaning the motor will stop.
if(PS4.event.button_down.square){
ledcWrite(ledChannel, 0);
}
The complete callback function to handle the button clicks can be seen below.
void onEvent(){
uint8_t analogVal = PS4.data.analog.button.r2;
if(previousVal != analogVal) {
ledcWrite(ledChannel, analogVal);
}
previousVal = analogVal;
if(PS4.event.button_down.cross){
ledcWrite(ledChannel, 255);
}
if(PS4.event.button_down.square){
ledcWrite(ledChannel, 0);
}
}
The complete code can be seen below.
#include <PS4Controller.h>
int freq = 1000;
int ledChannel = 0;
int resolution = 8;
uint8_t previousVal = 0;
void onEvent(){
uint8_t analogVal = PS4.data.analog.button.r2;
if(previousVal != analogVal) {
ledcWrite(ledChannel, analogVal);
}
previousVal = analogVal;
if(PS4.event.button_down.cross){
ledcWrite(ledChannel, 255);
}
if(PS4.event.button_down.square){
ledcWrite(ledChannel, 0);
}
}
void onConnection() {
if (PS4.isConnected()) {
Serial.println("Controller connected.");
}
}
void setup()
{
Serial.begin(115200);
ledcSetup(ledChannel, freq, resolution);
ledcAttachPin(13, ledChannel);
if(!PS4.begin("01:01:01:01:01:01")){
Serial.println("Initializetion failed");
return;
};
Serial.println("Initialization finished.");
PS4.attach(onEvent);
PS4.attachOnConnect(onConnection);
}
void loop()
{
vTaskDelete(NULL);
}
Testing the code
To test the whole system, start by connecting the ESP32, the ULN2803A and the DC motor, accordingly to the diagram shown in figure 1.
After that, upload the code to the ESP32 using the Arduino IDE. When the procedure is finished, open the IDE serial monitor. After the library initialization is done, you should get a message indicating it.
Then, connect the PS4 controller by clicking the PS button. You should get another message on the serial monitor indicating the controller is connected.
After that, start experimenting with the buttons. Clicking the cross button should make the motor run at full speed, and clicking the square button should make it stop. The R2 button should allow you to control the speed of the DC motor, accordingly to the amount of pressure you apply to the button.
The expected behavior can be seen at the video below.