Attaching and Controlling a 4in1 ESC with NanoESP32

Hello,

I am trying to control a 4in1 ESC (T-Motor F55A Pro II) using the arduino nano ESP32 to mediate signals from an RC Receiver (Radiolink 8 Channel R8EF) all powered by a 3s 5000 mAh battery. The 4in1 ESC is able to power only 2 of the 4 motors at once. The 2 that it powers is dependent on the order I attach() the motors to the digital pins of the nano (the last 2 I attach get powered). Is there a limitation in the ESP32Servo library or a limitation in the ESP32 itself that prevents it from sending the signals to all the motors? Code Below:

#include <ESP32Servo.h>
#include <ESP32PWM.h>

// ESCs
      Servo lrfESC;
      Servo rrfESC;
      Servo lffESC;
      Servo rffESC;
      const int amp = 100;

// RC RECEIVER
      const int channelPins[] = {2, 3, 4, 5, 6};  // Pins D2 to D6
      const int numChannels = 5;
      int LSUpDown = 0;
      int LSLeftRight = 0;
      int RSUpDown = 0;
      int RSLeftRight = 0;

void setup() {
  //Setup Serial
      Serial.begin(9600);

  //ESCs
      
      lffESC.attach(7);
      rffESC.attach(9);
      lrfESC.attach(8);
      rrfESC.attach(10);
        
}

void loop() {
  //Read RC Receiver
      int pulseWidth[numChannels];

      for (int i = 0; i < numChannels; i++) {
        pulseWidth[i] = pulseIn(channelPins[i], HIGH, 25000);  // Timeout set to 25 ms
      }

  //Send signals to ESCs
      LSUpDown = pulseWidth[2]; //Down 999, Up 1999
      LSLeftRight = pulseWidth[3]; //Left 999, Right 1999
      RSUpDown = pulseWidth[1]; //Down 1999, Up 999
      if(RSUpDown > 100) //Invert to be consistant
        {
          RSUpDown = 2998 - RSUpDown; //Down 999, Up 1999
        }
      RSLeftRight = pulseWidth[0]; //Left 999, Right 1999

if(pulseWidth[4]>1700) // Higher Setting, Air Mode
        { 
          lrfESC.writeMicroseconds(LSUpDown + map(RSLeftRight, 999, 1999, -amp, amp) + map(RSUpDown, 999, 1999, -amp, amp));
          rrfESC.writeMicroseconds(LSUpDown - map(RSLeftRight, 999, 1999, -amp, amp) + map(RSUpDown, 999, 1999, -amp, amp));
          lffESC.writeMicroseconds(LSUpDown + map(RSLeftRight, 999, 1999, -amp, amp) - map(RSUpDown, 999, 1999, -amp, amp));
          rffESC.writeMicroseconds(LSUpDown - map(RSLeftRight, 999, 1999, -amp, amp) - map(RSUpDown, 999, 1999, -amp, amp));
        }

        // Add a delay for stability (adjust as needed)
       delay(100);      
}

Welcome to the Arduino forums @bowlerme !
What core version and board are you using to compile the sketch?

We are aware of a compile time issue preventing ESP32Servo from working correctly on the Arduino Nano ESP32 board (as of core 2.0.11). :frowning: Please see here for more information.

Hello @lburelli !

I am using ESP32Servo version 0.13.0 (A few days ago I followed that linked forum post to get everything to compile, so thank you for that solution). I am using the Arduino Nano ESP32 (Code: ABX00092 / Barcode: 7630049204584) with the "Arduino ESP32 Boards" (by Arduino) board manager (version 2.0.11).

The code I posted compiles for me perfectly, my issue arises from only two motors of the four I have connected actually spinning. I can vary which two motors based on the order I attach() them as mentioned; which leads me to believe that the issue isn't soldering, the 4in1 ESC, or the motors. My guess is something in how this new ESP32 nano or how the ESP32Servo library handles sending writeMicroseconds() commands or attach() statements is the source of the issue.

The only other source of problems I could think of is that my 3S LiPo cannot provide enough power to the motors to drive all four (which I doubt as my previous iteration of this design, using an uno, Bluetooth receiver, a 3S LiPo, and 4 individual ESCs, was able to fly, see my video here if you are interested).

So, with that said, do you have any ideas I could try to see if I can make all the motors work?

Thank you for confirming (the fact that you didn't mention manually editing files made me question why the library worked at all! :slight_smile:).
That problem impacted mainly with the library's re-implementation of the Arduino APIs such as analogWrite(), and the Servo objects you are using should not be affected. However, there might still be some other issues with how PWM channels get mapped to pins; I haven't looked at the library with enough detail to be sure - I'll try to find some time to dig into it.
PS love the :red_car: :helicopter: project!

Okay, I did even more experimentation and I definitely think it's an issue with either the ESP32 or the ESP32Servo.h library. I added in all my driving code as well and only two of those work at once either (again, dependent on the order I use attach()). Switching between "Ground Mode" and "Air Mode" allows four (only two per mode) to get signals from the arduino.

The ESP32 datasheet says it has "2x UART communication", does that mean it can only communicate with 2 devices at once over serial? (This is way out of my realm of understanding) If so, is there any way around that (like alternating clock signals to each attached "Servo")?

Here is the longer code:

#include <ESP32Servo.h>
#include <ESP32PWM.h>
#include <Wire.h>

// ESCs
      Servo leftESC;
      Servo rightESC;
      Servo lrfESC;
      Servo rrfESC;
      Servo lffESC;
      Servo rffESC;
      Servo srv;
      const int amp = 100;

// RC RECEIVER
      const int channelPins[] = {2, 3, 4, 5, 6};  // Pins D2 to D6
      const int numChannels = 5;
      int LSUpDown = 0;
      int LSLeftRight = 0;
      int RSUpDown = 0;
      int RSLeftRight = 0;

// GYROSCOPE
      const uint8_t MPU_ADDR = 0x68; // I2C address of the MPU-6050. If AD0 pin is set to HIGH, the I2C address will be 0x69.

      int16_t accelerometer_x, accelerometer_y, accelerometer_z; // variables for accelerometer raw data
      int16_t gyro_x, gyro_y, gyro_z; // variables for gyro raw data
      int16_t temperature; // variables for temperature data

      char tmp_str[7]; // temporary variable used in convert function

      char* convert_int16_to_str(int16_t i) { // converts int16 to string. Moreover, resulting strings will have the same length in the debug monitor.
        sprintf(tmp_str, "%6d", i);
        return tmp_str;
      }

void setup() {
  //Setup Serial
      Serial.begin(9600);



  //GYROSCOPE
      //uint8_t twbrbackup = TWBR;
      uint8_t TWBR = 12;

      Wire.begin();
      Wire.beginTransmission(MPU_ADDR); // Begins a transmission to the I2C slave (GY-521 board)
      Wire.write(0x6B); // PWR_MGMT_1 register
      Wire.write(0); // set to zero (wakes up the MPU-6050)
      Wire.endTransmission(true);

  //ESCs
      srv.attach(13);
      leftESC.attach(12);
      rightESC.attach(11);
      
      lffESC.attach(7);
      rffESC.attach(9);
      lrfESC.attach(8);
      rrfESC.attach(10);
      

}

void loop() {
  
  // Get Gyroscope Info
      Wire.beginTransmission(MPU_ADDR);
      Wire.write(0x3B); // starting with register 0x3B (ACCEL_XOUT_H) [MPU-6000 and MPU-6050 Register Map and Descriptions Revision 4.2, p.40]
      Wire.endTransmission(false); // the parameter indicates that the Arduino will send a restart. As a result, the connection is kept active.
      Wire.requestFrom(MPU_ADDR, (size_t)7*2, true); // request a total of 7*2=14 registers
      
      // "Wire.read()<<8 | Wire.read();" means two registers are read and stored in the same variable
      accelerometer_x = Wire.read()<<8 | Wire.read(); // reading registers: 0x3B (ACCEL_XOUT_H) and 0x3C (ACCEL_XOUT_L)
      accelerometer_y = Wire.read()<<8 | Wire.read(); // reading registers: 0x3D (ACCEL_YOUT_H) and 0x3E (ACCEL_YOUT_L)
      accelerometer_z = Wire.read()<<8 | Wire.read(); // reading registers: 0x3F (ACCEL_ZOUT_H) and 0x40 (ACCEL_ZOUT_L)
      temperature = Wire.read()<<8 | Wire.read(); // reading registers: 0x41 (TEMP_OUT_H) and 0x42 (TEMP_OUT_L)
      gyro_x = Wire.read()<<8 | Wire.read(); // reading registers: 0x43 (GYRO_XOUT_H) and 0x44 (GYRO_XOUT_L)
      gyro_y = Wire.read()<<8 | Wire.read(); // reading registers: 0x45 (GYRO_YOUT_H) and 0x46 (GYRO_YOUT_L)
      gyro_z = Wire.read()<<8 | Wire.read(); // reading registers: 0x47 (GYRO_ZOUT_H) and 0x48 (GYRO_ZOUT_L)


  //Read RC Receiver
      int pulseWidth[numChannels];

      for (int i = 0; i < numChannels; i++) {
        pulseWidth[i] = pulseIn(channelPins[i], HIGH, 25000);  // Timeout set to 25 ms
      }

      Serial.println("PWM Signal Pulse Widths:");

      for (int i = 0; i < numChannels; i++) {
        Serial.print("Channel ");
        Serial.print(i + 1);
        Serial.print(": ");
        Serial.println(pulseWidth[i]);
      }

  //Print Gyro
      Serial.print("Gyro - X:");
      Serial.print(accelerometer_x);
      Serial.print(" Y:");
      Serial.print(accelerometer_y);
      Serial.print(" Z:");
      Serial.println(accelerometer_z);

  //Send signals to ESCs
      LSUpDown = pulseWidth[2]; //Down 999, Up 1999
      LSLeftRight = pulseWidth[3]; //Left 999, Right 1999
      RSUpDown = pulseWidth[1]; //Down 1999, Up 999
      if(RSUpDown > 100) //Invert to be consistant
        {
          RSUpDown = 2998 - RSUpDown; //Down 999, Up 1999
        }
      RSLeftRight = pulseWidth[0]; //Left 999, Right 1999

        

      if(pulseWidth[4]<1200 && pulseWidth[4]>100) // Lower Setting, Ground Mode
        {
           leftESC.writeMicroseconds(RSUpDown);
           rightESC.writeMicroseconds(RSUpDown);
          srv.write(map(RSLeftRight, 999, 1999, 0, 180));
          Serial.print(map(RSLeftRight, 999, 1999, 0, 180));
        }
      else if(pulseWidth[4]>1700) // Higher Setting, Air Mode
        { 
          lrfESC.writeMicroseconds(LSUpDown + map(RSLeftRight, 999, 1999, -amp, amp) + map(RSUpDown, 999, 1999, -amp, amp));
          rrfESC.writeMicroseconds(LSUpDown - map(RSLeftRight, 999, 1999, -amp, amp) + map(RSUpDown, 999, 1999, -amp, amp));
          lffESC.writeMicroseconds(LSUpDown + map(RSLeftRight, 999, 1999, -amp, amp) - map(RSUpDown, 999, 1999, -amp, amp));
          rffESC.writeMicroseconds(LSUpDown - map(RSLeftRight, 999, 1999, -amp, amp) - map(RSUpDown, 999, 1999, -amp, amp));
        }
      else // Off or Middle (kill all power)
        {

        }

        // Add a delay for stability (adjust as needed)
       delay(100);      
}

Ok, I found a workaround that is probably good enough. I alternate loop cycles assigning values to each "servo" (2 at a time), and detaching unused servos in the active clock cycle. The 4in1 ESC doesn't like that for startup, so I just assign the same startup values to the motors in pairs and then, once start-up is complete (or after the transition between modes), it detaches everything and returns to the "per cycle" attachment. Occasionally the motors sputter (likely due to the heavy increase in computation interrupting the continuous flow of signal), but it should be good enough.

If anyone finds a way to do it all in the same clock cycle, that would be preferred but the code below should work well enough for my project.

#include <ESP32Servo.h>
#include <ESP32PWM.h>
#include <Wire.h>

// ESCs
      Servo leftESC;
      Servo rightESC;
      Servo lrfESC;
      Servo rrfESC;
      Servo lffESC;
      Servo rffESC;
      Servo srv;
      const int amp = 100;

// Clock cycle
      int clk = 1;
      int prevVal = 0;

// RC RECEIVER
      const int channelPins[] = {2, 3, 4, 5, 6};  // Pins D2 to D6
      const int numChannels = 5;
      int LSUpDown = 0;
      int LSLeftRight = 0;
      int RSUpDown = 0;
      int RSLeftRight = 0;

// GYROSCOPE
      const uint8_t MPU_ADDR = 0x68; // I2C address of the MPU-6050. If AD0 pin is set to HIGH, the I2C address will be 0x69.

      int16_t accelerometer_x, accelerometer_y, accelerometer_z; // variables for accelerometer raw data
      int16_t gyro_x, gyro_y, gyro_z; // variables for gyro raw data
      int16_t temperature; // variables for temperature data

      char tmp_str[7]; // temporary variable used in convert function

      char* convert_int16_to_str(int16_t i) { // converts int16 to string. Moreover, resulting strings will have the same length in the debug monitor.
        sprintf(tmp_str, "%6d", i);
        return tmp_str;
      }

void setup() {
  //Setup Serial
      Serial.begin(115200);



  //GYROSCOPE
      //uint8_t twbrbackup = TWBR;
      uint8_t TWBR = 12;

      Wire.begin();
      Wire.beginTransmission(MPU_ADDR); // Begins a transmission to the I2C slave (GY-521 board)
      Wire.write(0x6B); // PWR_MGMT_1 register
      Wire.write(0); // set to zero (wakes up the MPU-6050)
      Wire.endTransmission(true);

  //ESCs
      srv.attach(13);
      leftESC.attach(12);
      rightESC.attach(11);
      
      lffESC.attach(7);
      rffESC.attach(9);
      lffESC.attach(8); //In setup, double attach for startup (2 channels at once only)
      rffESC.attach(10); //In setup, double attach for startup (2 channels at once only)
      

}

void loop() {
  
  // Get Gyroscope Info
      Wire.beginTransmission(MPU_ADDR);
      Wire.write(0x3B); // starting with register 0x3B (ACCEL_XOUT_H) [MPU-6000 and MPU-6050 Register Map and Descriptions Revision 4.2, p.40]
      Wire.endTransmission(false); // the parameter indicates that the Arduino will send a restart. As a result, the connection is kept active.
      Wire.requestFrom(MPU_ADDR, (size_t)7*2, true); // request a total of 7*2=14 registers
      
      // "Wire.read()<<8 | Wire.read();" means two registers are read and stored in the same variable
      accelerometer_x = Wire.read()<<8 | Wire.read(); // reading registers: 0x3B (ACCEL_XOUT_H) and 0x3C (ACCEL_XOUT_L)
      accelerometer_y = Wire.read()<<8 | Wire.read(); // reading registers: 0x3D (ACCEL_YOUT_H) and 0x3E (ACCEL_YOUT_L)
      accelerometer_z = Wire.read()<<8 | Wire.read(); // reading registers: 0x3F (ACCEL_ZOUT_H) and 0x40 (ACCEL_ZOUT_L)
      temperature = Wire.read()<<8 | Wire.read(); // reading registers: 0x41 (TEMP_OUT_H) and 0x42 (TEMP_OUT_L)
      gyro_x = Wire.read()<<8 | Wire.read(); // reading registers: 0x43 (GYRO_XOUT_H) and 0x44 (GYRO_XOUT_L)
      gyro_y = Wire.read()<<8 | Wire.read(); // reading registers: 0x45 (GYRO_YOUT_H) and 0x46 (GYRO_YOUT_L)
      gyro_z = Wire.read()<<8 | Wire.read(); // reading registers: 0x47 (GYRO_ZOUT_H) and 0x48 (GYRO_ZOUT_L)


  //Read RC Receiver
      int pulseWidth[numChannels];

      for (int i = 0; i < numChannels; i++) {
        pulseWidth[i] = pulseIn(channelPins[i], HIGH, 25000);  // Timeout set to 25 ms
      }

      Serial.println("PWM Signal Pulse Widths:");

      for (int i = 0; i < numChannels; i++) {
        Serial.print("Channel ");
        Serial.print(i + 1);
        Serial.print(": ");
        Serial.println(pulseWidth[i]);
      }

  //Print Gyro
      Serial.print("Gyro - X:");
      Serial.print(accelerometer_x);
      Serial.print(" Y:");
      Serial.print(accelerometer_y);
      Serial.print(" Z:");
      Serial.println(accelerometer_z);

  //Send signals to ESCs
      LSUpDown = pulseWidth[2]; //Down 999, Up 1999
      LSLeftRight = pulseWidth[3]; //Left 999, Right 1999
      RSUpDown = pulseWidth[1]; //Down 1999, Up 999
      if(RSUpDown > 100) //Invert to be consistant
        {
          RSUpDown = 2998 - RSUpDown; //Down 999, Up 1999
        }
      RSLeftRight = pulseWidth[0]; //Left 999, Right 1999

        

      if(pulseWidth[4]<1200 && pulseWidth[4]>100) // Lower Setting, Ground Mode
        {
          lrfESC.detach();
          lffESC.detach();
          rrfESC.detach();
          rffESC.detach();
          if(clk == 1)
          {
            srv.detach();
            leftESC.attach(12);
            rightESC.attach(11);

            leftESC.writeMicroseconds(RSUpDown);
            rightESC.writeMicroseconds(RSUpDown);
          } else
          {
            leftESC.detach();
            rightESC.detach();
            srv.attach(13);
            srv.write(map(RSLeftRight, 999, 1999, 0, 180));
            Serial.print(map(RSLeftRight, 999, 1999, 0, 180));
          }
        }
      else if(pulseWidth[4]>1700) // Higher Setting, Air Mode
        { 
          leftESC.detach();
          rightESC.detach();
          srv.detach();
          lrfESC.detach();
          lffESC.detach();
          rrfESC.detach();
          rffESC.detach();
          if(clk == 1)
          {

            lrfESC.attach(8);
            rrfESC.attach(10);
            lrfESC.writeMicroseconds(LSUpDown + map(RSLeftRight, 999, 1999, -amp, amp) + map(RSUpDown, 999, 1999, -amp, amp));
            rrfESC.writeMicroseconds(LSUpDown - map(RSLeftRight, 999, 1999, -amp, amp) + map(RSUpDown, 999, 1999, -amp, amp));
          } else
          {
            lffESC.attach(7);
            rffESC.attach(9);
            lffESC.writeMicroseconds(LSUpDown + map(RSLeftRight, 999, 1999, -amp, amp) - map(RSUpDown, 999, 1999, -amp, amp));
            rffESC.writeMicroseconds(LSUpDown - map(RSLeftRight, 999, 1999, -amp, amp) - map(RSUpDown, 999, 1999, -amp, amp));
          }
        }
      else // Off or Middle (kill all power)
        {
          if(pulseWidth[4] == 0)
          {
            //Do nothing, off
          }
          else if(prevVal > 1700) // Moving to Ground Mode
          {
            lrfESC.detach();
            lffESC.detach();
            rrfESC.detach();
            rffESC.detach();
            delay(100);

            srv.attach(13);
            leftESC.attach(12);
            rightESC.attach(11);
            delay(100);

          }
          else if(prevVal<1200 && prevVal>100) // Moving to Air Mode
          {
            leftESC.detach();
            rightESC.detach();
            srv.detach();
            delay(100);

            lffESC.attach(7);
            rffESC.attach(9);
            lffESC.attach(8); //In setup, double attach for startup (2 channels at once only)
            rffESC.attach(10); //In setup, double attach for startup (2 channels at once only)
            delay(100);
          }
        }

      prevVal = pulseWidth[4];
      clk = 3 - clk; // 2 becomes 1, 1 becomes 2     
}

Hello @bowlerme.
Arduino ESP32 core 2.0.12 is out, and it adds a new menu option to use a more library-compatible pin numbering scheme. Read all about it here and try it out!

Happy hacking! :hammer_and_wrench:

Hey, I had the same problem today. I found a library which lets you control all 4 of the ESC. It's called Deneyap Servo.