Hi, really stuck on this one. Dual propeller fan boat using drone arms, DJI Opto Escs and props. Electrically all works great.
I'm controlling this with Spektrum DX8 transmitter and AR6210 receiver. One radio channel into throttle, the other into aileron.
I'm using a Nano Every to intercept and adjust the radio signal and send corrected version to the escs, like a flight controller. Stopping and moving in a straight line works well, it's the turning that's got me bewildered. I have no gift for the math to taketh from one and giveth to the other. Kindly help my wretched soul if you would, please.
/*
Dual propeller fan boat
Throttle stick on Spektrum DX8 for both motors throttle
trim one or the other motor slower to turn left and right
TRAVEL %(us) LOW CENTER HIGH
100 1120 1520 1920
125 1020 1520 2020
150 920 1520 2120
at 2048 resolution (DSMX)
*/
#include <Servo.h>
int rMotorPinIn = 7;
int lMotorPinIn = 9;
Servo rSignalOut;
Servo lSignalOut;
int leftVal = 0; // raw throttle channel in
int rightVal = 0; // raw aileron channel in
int leftValOut = 0; // Arduino adjusted throttle channel
int rightValOut = 0; // Arduino adjusted aileron channel
int leftActualOut = 0; // value actually sent to ESC throttle channel
int rightActualOut = 0; // value actually sent to ESC aileron channel
// variable for Arduino, switch between read signal only mode
// and write ActualOut vals to test the dual brushless motors
boolean readOnlyModeSwitch;
void setup() {
Serial.begin(115200);
pinMode (rMotorPinIn, INPUT);
pinMode (lMotorPinIn, INPUT);
rSignalOut.attach(8);
lSignalOut.attach(10);
// write a signal so props stay stopped for now
lSignalOut.writeMicroseconds(1000);
rSignalOut.writeMicroseconds(1000);
// defaults to write mode, or normal intended usage mode
readOnlyModeSwitch = false;
Serial.println("waterMoccasin");
printMenu();
Serial.println("");
Serial.println("Beginning in write (adjusted) mode");
Serial.println("");
delay(3000); // wait a bit to read all that
// clear some lines on monitor screen
for (int i = 0; i < 5; i++) {
Serial.println();
}
}
void loop() {
if (Serial.available() > 0) {
char in = Serial.read();
if ((in == 'r') || (in == 'R')) {
readOnlyModeSwitch = true;
Serial.println("read mode");
Serial.println();
}
else if ((in == 'w') || (in == 'W')) {
readOnlyModeSwitch = false;
Serial.println("write mode");
Serial.println();
}
else if ((in == 'm') || (in == 'M')) {
printMenu();
delay(3000);
}
}
switch (readOnlyModeSwitch) {
case false: // write mode
leftVal = pulseIn(lMotorPinIn, 20000);
rightVal = pulseIn(rMotorPinIn, 20000);
// turn left, spin right faster relative to left
if ((rightVal > 1560)&& (leftVal > 1250)) {
rightVal = (leftVal + rightVal) / 2;
rightValOut = rightVal;
leftVal = leftVal - (rightVal / 2);
leftValOut = leftVal;
}
// turn right, spin left faster relative to right
else if ((rightVal < 1440)&& (leftVal > 1250)) {
leftVal = (leftVal + rightVal + 50) / 2;
leftValOut = leftVal;
rightVal = leftVal - (rightVal / 2);
rightValOut = rightVal;
}
// both motors straight line or standing still
else {
leftValOut = leftVal;
rightValOut = leftVal;
}
// write to setMotors function (do the thing)
setMotors();
// show me what ya got
printRealTimeData();
break;
case true: // read pwm only mode
leftVal = pulseIn(lMotorPinIn, 20000);
rightVal = pulseIn(rMotorPinIn, 20000);
printRealTimeData();
break;
}
}
void setMotors() {
// always send your best
leftActualOut = leftValOut;
lSignalOut.writeMicroseconds(leftActualOut);
rightActualOut = rightValOut;
rSignalOut.writeMicroseconds(rightActualOut);
}
void printRealTimeData() {
Serial.print("LVal: ");
Serial.print(leftVal);
Serial.print('\t');
Serial.print("RVal: ");
Serial.println(rightVal);
}
void printMenu() {
Serial.println("");
Serial.println("type r for read mode");
Serial.println("");
Serial.println("type w for write (adjusted) mode");
Serial.println("");
Serial.println("type m to repeat menu");
Serial.println("");
}
I've got some suggestions for improving and optimizing the code:
Use Meaningful Variable Names:
Rename variables and comments to make the code more self-explanatory. This makes it easier to understand the code's purpose and functionality.
Remove Magic Numbers:
Replace the magic numbers (e.g., 1560, 1440) with named constants or variables that represent these values. This makes the code more readable and maintainable.
Simplify Your Turning Logic:
The turning logic can be simplified. Instead of recalculating the motor values in each case, calculate the average throttle value and adjust it based on the turning direction.
Avoid Duplicate Code:
The code for reading PWM values and printing real-time data is repeated. You can move this common code outside the switch statement to reduce redundancy and improve maintainability.
Add Comments for Clarity:
Consider adding comments to explain the purpose of different sections of your code, especially the setup, loop, and functions.
Optimize Your Print Functions:
Printing data to the serial monitor can be resource-intensive. You can reduce the frequency of printing or optimize your print functions to avoid overloading the serial connection. For example, you might print data only at specific intervals or when certain conditions are met.
Here's the improved code:
#include <Servo.h>
int rMotorPinIn = 7;
int lMotorPinIn = 9;
Servo rSignalOut;
Servo lSignalOut;
int leftVal = 0;
int rightVal = 0;
int leftValOut = 0;
int rightValOut = 0;
void setup() {
Serial.begin(115200);
pinMode(rMotorPinIn, INPUT);
pinMode(lMotorPinIn, INPUT);
rSignalOut.attach(8);
lSignalOut.attach(10);
lSignalOut.writeMicroseconds(1000);
rSignalOut.writeMicroseconds(1000);
Serial.println("waterMoccasin");
printMenu();
Serial.println("Beginning in write (adjusted) mode");
for (int i = 0; i < 5; i++) {
Serial.println();
}
}
void loop() {
if (Serial.available() > 0) {
char in = Serial.read();
if (in == 'r' || in == 'R') {
readOnlyModeSwitch = true;
Serial.println("read mode");
Serial.println();
} else if (in == 'w' || in == 'W') {
readOnlyModeSwitch = false;
Serial.println("write mode");
Serial.println();
} else if (in == 'm' || in == 'M') {
printMenu();
delay(3000);
}
}
switch (readOnlyModeSwitch) {
case false: // write mode
readPWMValues();
updateMotorValues();
setMotors();
printRealTimeData();
break;
case true: // read pwm only mode
readPWMValues();
printRealTimeData();
break;
}
}
void readPWMValues() {
leftVal = pulseIn(lMotorPinIn, 20000);
rightVal = pulseIn(rMotorPinIn, 20000);
}
void updateMotorValues() {
int avgThrottle = (leftVal + rightVal) / 2;
if (rightVal > 1560 && leftVal > 1250) {
rightValOut = avgThrottle;
leftValOut = leftVal - (avgThrottle / 2);
} else if (rightVal < 1440 && leftVal > 1250) {
leftValOut = avgThrottle;
rightValOut = avgThrottle - (rightVal / 2);
} else {
leftValOut = avgThrottle;
rightValOut = avgThrottle;
}
}
void setMotors() {
lSignalOut.writeMicroseconds(leftValOut);
rSignalOut.writeMicroseconds(rightValOut);
}
void printRealTimeData() {
Serial.print("LVal: ");
Serial.print(leftVal);
Serial.print('\t');
Serial.print("RVal: ");
Serial.println(rightVal);
}
void printMenu() {
Serial.println("\ntype r for read mode\ntype w for write (adjusted) mode\ntype m to repeat menu\n");
}
These changes should make your code more readable, maintainable, and efficient.
No they can't reverse. Got it working by reassigning both channels in to the throttle stick out and mixing as needed with the other stick. Thanks for your response.
Thanks for all the suggestions, I ended up using a different transmitter since the one I was using had a broken throttle gimbal. Mixing the elevator and aileron just wasn't giving the result I wanted.
The code you posted looks cleaner, cheers for that. On the one I actually use, I take all the Serial printing out anyway since it's no good on the river to me. Final note: anyone else who wanted to use this in the future, I added back in the variable and declaration that somehow got left out in your version. Here's it added back in. Thanks for your help.
#include <Servo.h>
int rMotorPinIn = 7;
int lMotorPinIn = 9;
Servo rSignalOut;
Servo lSignalOut;
int leftVal = 0;
int rightVal = 0;
int leftValOut = 0;
int rightValOut = 0;
boolean readOnlyModeSwitch;
void setup() {
Serial.begin(115200);
pinMode(rMotorPinIn, INPUT);
pinMode(lMotorPinIn, INPUT);
readOnlyModeSwitch = false;
rSignalOut.attach(8);
lSignalOut.attach(10);
lSignalOut.writeMicroseconds(1000);
rSignalOut.writeMicroseconds(1000);
Serial.println("waterMoccasin");
printMenu();
Serial.println("Beginning in write (adjusted) mode");
for (int i = 0; i < 5; i++) {
Serial.println();
}
}
void loop() {
if (Serial.available() > 0) {
char in = Serial.read();
if (in == 'r' || in == 'R') {
readOnlyModeSwitch = true;
Serial.println("read mode");
Serial.println();
} else if (in == 'w' || in == 'W') {
readOnlyModeSwitch = false;
Serial.println("write mode");
Serial.println();
} else if (in == 'm' || in == 'M') {
printMenu();
delay(3000);
}
}
switch (readOnlyModeSwitch) {
case false: // write mode
readPWMValues();
updateMotorValues();
setMotors();
printRealTimeData();
break;
case true: // read pwm only mode
readPWMValues();
printRealTimeData();
break;
}
}
void readPWMValues() {
leftVal = pulseIn(lMotorPinIn, 20000);
rightVal = pulseIn(rMotorPinIn, 20000);
}
void updateMotorValues() {
int avgThrottle = (leftVal + rightVal) / 2;
if (rightVal > 1560 && leftVal > 1250) {
rightValOut = avgThrottle;
leftValOut = leftVal - (avgThrottle / 2);
} else if (rightVal < 1440 && leftVal > 1250) {
leftValOut = avgThrottle;
rightValOut = avgThrottle - (rightVal / 2);
} else {
leftValOut = avgThrottle;
rightValOut = avgThrottle;
}
}
void setMotors() {
lSignalOut.writeMicroseconds(leftValOut);
rSignalOut.writeMicroseconds(rightValOut);
}
void printRealTimeData() {
Serial.print("LVal: ");
Serial.print(leftVal);
Serial.print('\t');
Serial.print("RVal: ");
Serial.println(rightVal);
}
void printMenu() {
Serial.println("\ntype r for read mode\ntype w for write (adjusted) mode\ntype m to repeat menu\n");
}
Have been doing quite some testing recently to use (mainly) Nano's to control RC vehicles.
In case you're still looking for improvements to your project, happy to share my findings.
For instance, using pulseIn to read the receiver signal is not ideal (it blocks the processor from doing other things). Better to set up an ISR.
Where TH_L and TH_R are the throttle values sent to the ESCs and the strength of the diff is made adjustable with a pot meter, giving value 'factor' (between 0 and 1).
I'm just curious, would it have been possible to mix the throttle and aileron channels in the transmitter? Some radios have programmable mixers, I'm not familiar with your radio, though. (I'm a Futaba guy).
I had a flying setup that did exactly what you describe. I had two electric motors that had differential thrust (rudder channel) mixed with the throttle. It was a flying leaf blower, btw.
I also have used the Nano, have you tried the Nano Every? That's my goto these days, I find it much easier to use, but maybe I'm outdated since I really slowed down on Arduino projects in the last year.
Nano Every is still a great choice, interrupts on every pin, smalll footprint.
When using Arduinos to drive brushless (drone-) motors, I recently did a project using Arduino Nano ESP32 running a DShot implementation to drive BLHeli_S ESCs. Performance is spectacular: very linear, very controllable output and almost silent.