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
}