Nano / L298N motor control - PWM odd behaviour

Current Project: 4wd bluetooth control of mechanum wheels.

Hi all - the forums have been a great help (esp. serial input basics - Robin2!) and I have a working proof of concept prototype on the breadboard with code that compiles and controls the motors.

I'm hoping someone has some experience to share on motor behavior which is confusing me.

I'm getting a lot of 'whine' on the motors which stay stalled up until about 70/255 duty cycle after which they start spinning fast enough that it loses traction until I back off the power to 30-40/255. The 'whine' is apparent through all duty cycles but appears to lessen from 200/255 onwards (might just be drowned out by the cheap plastic gearboxes though...)

I'm using little tt motors so can live with the traction loss until the motors are upgraded but I've found that if I hook up one of these motors to a standalone motor control (not MCU based) I get no 'whine' and a very smooth startup from 10-15/255 duty cycle and smooth transitions right through to full power.

I don't know what chip the standalone motor controller uses as its a little black box but looking at the pcb is looks like a standard h-bridge with a pot, the mosfets are wrapped both sides in heatsink so no part number. I do know that it runs its PWM at 15kHz while the nano is 490Hz (except for the one I have on pin 6 which is 960Hz).

What I've read is that a high pwm takes out the whine at the expense of a decreased motor efficiency as its take more effort to saturate the controller ena input - but the motor starts up at a lower duty cycle and has smooth control seemingly with the higher frequency. from what I've read so far - the lower pwm frequency shouldn't affect the cut-in voltage of the motor, leading me to think I have a design (code or hardware) issue, I just have no idea of what it could be.

Before I start messing with nano's timer registers to adjust pwm - I'm hoping someone has some suggestions / advice.

code below, I'll try and append a schematic

Thanks for any suggestions - I'm relearning some very rusty coding skills so all feedback appreciated.

#include <SoftwareSerial.h>
SoftwareSerial SerialMotor(4, 3); // RX, TX



//Housekeeping timers
unsigned long startMillis;
unsigned long currentMillis;
const unsigned long period = 1000;  // # of millis wait to execute


static byte STX = 0x3C;         // start of frame marker
static byte ETX = 0x3E;         // end of frame marker
const byte numBytes = 32;	    // max size of data frame
byte receivedBytes[numBytes];	// global array to hold the received data
byte byteCount = 0;			    // number of bytes received in frame        
boolean newData = false;		// flag for new data received from controller





// Motor A block
#define motorAENA 9 // motor A ENA PIN
#define motorAIN1 14 // directional control pin
#define motorAIN2 15 // directional control pin 

// Motor B block
#define motorBENA 10 // motor A ENA PIN
#define motorBIN1 16 // directional control pin
#define motorBIN2 17 // directional control pin 

// Motor C block

#define motorCENA 11 // motor A ENA PIN
#define motorCIN1 18 // directional control pin
#define motorCIN2 19 // directional control pin 

// Motor D block
#define motorDENA 6 // motor A ENA PIN
#define motorDIN1 12 // directional control pin
#define motorDIN2 13 // directional control pin 





void recvBytesWithStartEndMarkers() {
    static boolean recvInProgress = false;
    static byte ndx = 0;
    byte startMarker = 0x3C;
    byte endMarker = 0x3E;
    byte rb;


    while (SerialMotor.available() > 0 && newData == false) {
        rb = SerialMotor.read();
        

        if (recvInProgress == true) {
            if (rb != endMarker) {
                receivedBytes[ndx] = rb;
                ndx++;
                if (ndx >= numBytes) {
                    ndx = numBytes - 1;
                }
            }
            else {
                receivedBytes[ndx] = '\0'; // terminate the string
                recvInProgress = false;
                byteCount = ndx;  // save the number for use when printing
                ndx = 0;
                newData = true;
            }
        }

        else if (rb == startMarker) {
            recvInProgress = true;
        }
    }
}

void setup()
{
    // Open serial communications and wait for port to open:
    Serial.begin(28800);


    Serial.println("Goodnight moon!");

    // set the data rate for the SoftwareSerial port
    SerialMotor.begin(28800);

    pinMode(motorAIN1, OUTPUT);
    pinMode(motorAIN2, OUTPUT);
    pinMode(motorBENA, OUTPUT);
    pinMode(motorBIN1, OUTPUT);
    pinMode(motorBIN2, OUTPUT);
    pinMode(motorCENA, OUTPUT);
    pinMode(motorCIN1, OUTPUT);
    pinMode(motorCIN2, OUTPUT);
    pinMode(motorDENA, OUTPUT);
    pinMode(motorDIN1, OUTPUT);
    pinMode(motorDIN2, OUTPUT);


}

void motorControl() {
    // frame structure HDR A1 A2 B1 B2 C1 C2 D1 D2 CS
    // motorN.1 = direction control
    // motorN.2 = speed control
    Serial.print("motorControl() called"); // debug code to serial monitor


    Serial.println();                                      // debug code to serial monitor
    Serial.print("frame structure into motorControl = ");
    for (int i = 0; i < byteCount; i++) {
        Serial.print(receivedBytes[i], HEX);
        Serial.print(" ");
    }
    Serial.println();


    switch (receivedBytes[1]) { // Motor A control
    case 0:
        Serial.println("case 0 - all brake on motor a"); // debug code to serial monitor
        digitalWrite(motorAIN1, HIGH);
        digitalWrite(motorAIN2, HIGH);
        analogWrite(motorAENA, 0);                       // future chnage  - benchtest current draw before ENA @ 255 for powered brake         
        return;
    case 1:
        digitalWrite(motorAIN1, LOW);
        digitalWrite(motorAIN2, HIGH);
        analogWrite(motorAENA, receivedBytes[2]);
        return;
    case 2:
        digitalWrite(motorAIN1, HIGH);
        digitalWrite(motorAIN2, LOW);
        analogWrite(motorAENA, receivedBytes[2]);
        return;
    }
    switch (receivedBytes[3]) { // Motor B control
    case 0:
        digitalWrite(motorBIN1, HIGH);
        digitalWrite(motorBIN2, HIGH);
        analogWrite(motorBENA, 0);
        return;
    case 1:
        digitalWrite(motorBIN1, LOW);
        digitalWrite(motorBIN2, HIGH);
        analogWrite(motorBENA, receivedBytes[4]);
        return;
    case 2:
        digitalWrite(motorBIN1, HIGH);
        digitalWrite(motorBIN2, LOW);
        analogWrite(motorBENA, receivedBytes[4]);
        return;
    }
    switch (receivedBytes[5]) { // Motor C control
    case 0:
        digitalWrite(motorCIN1, HIGH);
        digitalWrite(motorBIN2, HIGH);
        analogWrite(motorCENA, 0);
        return;
    case 1:
        digitalWrite(motorCIN1, LOW);
        digitalWrite(motorCIN2, HIGH);
        analogWrite(motorCENA, receivedBytes[6]);
        return;
    case 2:
        digitalWrite(motorBIN1, HIGH);
        digitalWrite(motorBIN2, LOW);
        analogWrite(motorCENA, receivedBytes[6]);
        return;
    }
    switch (receivedBytes[7]) { // Motor D control
    case 0:
        digitalWrite(motorDIN1, HIGH);
        digitalWrite(motorDIN2, HIGH);
        analogWrite(motorDENA, 0);
        return;
    case 1:
        digitalWrite(motorDIN1, LOW);
        digitalWrite(motorDIN2, HIGH);
        analogWrite(motorDENA, receivedBytes[8]);
        return;
    case 2:
        digitalWrite(motorDIN1, HIGH);
        digitalWrite(motorDIN2, LOW);
        analogWrite(motorDENA, receivedBytes[8]);
        return;
    }
    newData = false;										    // set newData to false after frame execution
    memset(receivedBytes, 0, numBytes);							// debug code - 0 write frame data after execution

}

boolean validateData() {
    Serial.print("RX command frame from controller ");  // debug code output received frame to serial monitor
    for (byte n = 0; n < byteCount; n++) {
        Serial.print(receivedBytes[n], HEX);
        Serial.print(' ');
    }

    byte checkSum = 0;							// initialize checksum at 0
    for (int i = 0; i < byteCount - 1; i++) {
        checkSum = checkSum + receivedBytes[i];
    }
    checkSum = checkSum % 10;

    if (checkSum == receivedBytes[byteCount - 1]) {
        Serial.print("Good checkSum = "); Serial.print(checkSum, HEX); // debug code to serial monitor

        byte checkFrame[] = { STX, checkSum, ETX };   // controlled expected ACK response frame
        SerialMotor.write(checkFrame, 3);

        Serial.print(" checkFrame = ");               // debug code to serial monitor
        for (int i = 0; i < byteCount - 1; i++) {     // debug code to serial monitor
            Serial.print(checkFrame[i], HEX);
            Serial.print(" ");
        }

        newData = false;
        return true;
    }
    else {
        Serial.print("Bad checkSum = "); Serial.print(checkSum, HEX); // debug code to serial monitor

        byte checkFrame[] = { STX, checkSum, ETX };   // ntf - future change, convert to resend request and match in controller code 
        SerialMotor.write(checkFrame, 3);
        Serial.print(" checkFrame = ");               // debug code to serial monitor
        for (int i = 0; i < byteCount - 1; i++) {     // debug code to serial monitor
            Serial.print(checkFrame[i], HEX);
            Serial.print(" ");
            newData = false;
            return false;
        }
    }

}

void parseData() {
    // read header (HDR) from input frame and action based on lookup table
    // HDR = 1 - motor control

    if (newData == true) {
        if (validateData() == true) {      // verify checksum and send frame ack if good
            switch (receivedBytes[0]) {
            case 1:
                motorControl();
                return;
            }
        }
    }
}

void loop() {
 

    recvBytesWithStartEndMarkers();
    if (newData == true) { parseData(); }

    /*
    currentMillis = millis();					  
    if (currentMillis - startMillis >= period) {  
	Serial.println("heartbeat");                    // debug code keepawake serialmon 
    startMillis = currentMillis;
    }
    */

Your L298N is taking a lot out of the input power, maybe 2vdc, so the "whining at 70" is because the power just is not "70" as expected. When you use one motor controller for one motor, you see the power drain of the four motors lessen. If you can try another motor controller module, try the TB6612FNG.

@xfpd Thanks - I'm still in the 'letting the magic smoke out' phase of learning so am trying to stick with cheap components (and I already a few L298N). I'll look at the TB6612FNG for the next version.

Measured output voltage on the L298N's is roughly consistent with duty cycle / 255 * (Vin - 2) which is leading me to think its a pwm frequency issue or (more likely) a design issue.

The L298N is a know heat source with the secondary purpose of driving TT motors. Try to make the output voltage of the L298N to just below the maximum input voltage of the motors. I think that is 6vdc for the TT motors.

Maybe the PWM is dropping out? If it is, could it be due to insufficient power?

@xfpd "known heat source with a secondary purpose of driving motors" ... made me laugh :slight_smile: have to keep the chips warm right?

I managed some time to test a few things out, I did connect to a 8v power supply for 6v at the motors but no change. I also tried swapping motors, MCUs and L298N's - no change.

What did change the L298N behaviour was pwm frequency - over 10kHz no whine and smoother startup. I had to use an ESP32 as the nano couldn't top 7,800Hz on the fast pwm channel with the prescaler set to 8.

The L298N is definitely obsolete for a reason. Changing the pwm frequency resolved one issue, but the startup behaviour is still puzzling, I understand that the duty cycle applied to the L298N needs to overcome the stall current of the tt motors, leaving the motor speed too high once its rolling. Could probably solve in the code by providing a brief burst of full duty pwm on startup followed by a more appropriate initial speed setting). Given the other pwm generator doesn't face those issues (and doesn't have any code) I'll start looking at spec sheets to find a better controller (starting with one you mentioned previously).

Thanks for the help.

Could you explain the logic behind feeding 5.15V into the Vin input on the Nano?

If it's like the clones I just checked, it has an AMS1117-5.0 on board.

The AMS1117 has a minimum drop out voltage of 1.0V @ 100mA.

I don't know what voltage the AMS1117-5.0 will be putting out with only 0.15V of headroom to work with, but I'd be moderately surprised if it was in the neighbourhood of 5.0V.

In addition to @van_der_decken comments
Why do you have 5.15V going to the ESP VP pin?

@van_der_decken Thanks for the feedback. You're right, I didn't read the data sheet well enough. Vin should be between 7V and 12V, with a minimum of 1.2V drop from the onboard regulator.

I measured off the 5v pin on the Nano and read 4.35V. I adjusted the supply to 6.2V and now read 4.95V off the 5V pin (doesn't increase for increasing Vin).

I'm sure the Nano is happier (I'm surprised it was running properly with the low voltage condition.

@jim-p - its actually going to the Vin - I mislabeled the connection on the drawing.