Serial Communication problem: Python-Arduino for controlling a brushless motor

Hello! I am controlling a brushless motor and trying to send the control signals from PC (Python) to Arduino via serial communication. The Arduino will send back the IMU reading. However, I'm facing a problem: the moment I run the Python code, the motor goes out of control (max. voltage).

  1. Where is the problem exactly?
  2. Are there possible delays in the communication? How to fix them?

Any kind of help will be very much appreciated. Attached codes below:

Arduino code:

////interrupt////////////////////////////////////////////////////////////////////////////////////////////
#include <avr/interrupt.h>
const uint8_t SOFT_SERIAL_INT = INT0; // use INT0 as the interrupt source


// MPU6050 data///////////////////////////////////////////////////////////////////////////////////////////

//MPU 6050 Libraries
#include <Wire.h>
#include <I2Cdev.h>
#include <MPU6050.h>
MPU6050 mpu;


// Raw data
int16_t ax, ay, az; 
int16_t gx, gy, gz; 
// Converted data 
float axConv; 
float ayConv; 
float azConv; 
float gyConv;
// Computations
float thetax_rad; 
float thetaz_rad; 
float go;
float theta_avg_rad; 
float theta_avg_deg;


/**** difference between sensed and actual angles *****////////////////////////////////////////////////////
float offset= 0.01;
//Variables for the low pass filter
float F_theta_old=0;
float F_theta_new=0;
float F_theta_deg;
int F_theta_new_deg;


/**** Conversion *****//////////////////////////////////////////////////////////////////////////////////////
float Deg_to_Rad = 3.141592654/180; 
float Rad_to_Deg = 180/3.141592654;


//Brushless motor library 
#include<Servo.h>
#define ESC_PIN 2 //PWM Connected to PIN 2 
Servo esc;

int theta;


/////////////////////////////////////////////////////////////////////////////////////////////////////////

void setup() 
{
  Serial.begin(115200);
  
  attachInterrupt(digitalPinToInterrupt(2), serialEvent, CHANGE);
  
  //MPU 5060 Setup
  Wire.begin(); 
  mpu.initialize();
  esc.attach(ESC_PIN, 1000, 2000); //calibrating the motor for min and max rpm esc.write(180);
  delay(5000);
  esc.write(0);
  delay(2000);
  esc.write(10); esc.writeMicroseconds(1000); //min PWM


 delay(5000); //give the setup some delay
}

void loop() 
{

//nothing, just waits for an interrupt command :)
  ReadIMU();
  delay(50);
}

void serialEvent() //to read any command 
{
  EIFR |= (1 << INTF0); // set the interrupt flag
  if (Serial.available()>=2) 
  {
    byte bytes[2];
    Serial.readBytes(bytes, 2);
    Serial.read(); // discard newline character
    int16_t command = bytes[0];
    int16_t pwm = bytes[1];

    /* for debugging
    byte packet[2];
    packet[0] = command;
    packet[1] = value;
    Serial.write(packet, sizeof(packet));
    */

    if (command == 1) //send IMU values 
    {
      ReadIMU(); 
      Serial.write(F_theta_new_deg);

    } 
    else if (command == 2) 
    {
      WriteToMotor(pwm);
      Serial.write(F_theta_new_deg);
    }
  
    }
  }

void ReadIMU() 
{
// MPU readings, mapping, and related computations
mpu.getMotion6(&ax, &ay, &az, &gx, &gy, &gz); 
axConv = ax/16384.0;
ayConv = ay/16384.0;
azConv = az/16384.0;
gyConv = gy/131.0;
go = sqrt(pow(axConv,2) + pow(ayConv,2) + pow(azConv,2));
thetax_rad = acos(axConv/go);
thetaz_rad = asin(azConv/go);
theta_avg_rad = (thetaz_rad+thetax_rad)/2 + offset; 
//theta_avg_deg = theta_avg_rad*Rad_to_Deg; 

// Low pass filter
F_theta_new= (0.7)*F_theta_old +(0.3)*theta_avg_rad; 
F_theta_new_deg= F_theta_new*Rad_to_Deg;

F_theta_old = F_theta_new;
}

void WriteToMotor(int pwm) //pwm is sent as an argument 
{
int mappedPWM = map(pwm, 0, 255, 1000, 1450);
esc.writeMicroseconds(mappedPWM);
}


Python code:

import serial
import threading
import time
import struct
import csv
import math
import time
import matplotlib.pyplot as plt


# Establish connection with Arduino
ser = serial.Serial('/dev/tty.usbmodem14201', 115200)

# Check if the connection is open
if ser.isOpen():
    print("Connection established!")
else:
    print("Connection failed!")

# Fixed step
step_delay = 100  # millis
# For all cases,
# Number of steps required = interval / step_delay

# For 10 seconds period
interval_10 = 10000  # 10 seconds
steps_10 = interval_10 // step_delay

# For 5 seconds interval
interval_5 = 5000
steps_5 = interval_5 // step_delay

# For 5 seconds interval
interval_15 = 15000
steps_15 = interval_15 // step_delay

pwm = 0
pwm_values = []


def serialCommunication(newPWM, oldPWM=None):
    if newPWM != oldPWM:
        oldPWM = newPWM
        # send PWM & read Theta
        mapped_PWM = int((newPWM - 1000)*255/450)
        ser.write(struct.pack('BB', 2, mapped_PWM))
    else:
        # just read Theta
        ser.write(struct.pack('BB', 1, 0))

    # Wait for the message to be sent
    time.sleep(0.05)

    # Read from the serial port
    data = ser.read()
    # Print the received data
    decimal_val = ord(data)
    print("PWM signal sent: " + str(mapped_PWM) + " " + str(int(newPWM)))
    print("Theta value received: " + str(decimal_val))

    # Open the file and write data to it
    with open('profile2data.csv', mode='a', newline='') as file:
        writer = csv.writer(file)
        writer.writerow([newPWM, decimal_val])


def Pwm_signal():
    global pwm, pwm_values

    with open('profile2data.csv', mode='w', newline='') as file:
        writer = csv.writer(file)
        writer.writerow(['PWM', 'Theta'])

    # Sin wave, for 15 seconds and frequency= 0.25 Hz, fluctuating between 1200 and 1300
    freq = 0.25  # Hz
    period = 1.0 / freq  # seconds
    amplitude = (1300 - 1200) // 2
    offset = 1200 + amplitude

    for i in range(steps_15 + 1):
        rad = 2 * math.pi * freq * i * step_delay / 1000.0
        pwm = int(amplitude * math.sin(rad)) + offset
        serialCommunication(pwm)
        time.sleep(step_delay / 1000.0)

    # Step to 1400
    pwm = 1400
    for i in range(steps_5+1):
        serialCommunication(pwm)
        time.sleep(step_delay / 1000.0)

    # Step to 1300
    pwm = 1300
    for i in range(steps_5+1):
        serialCommunication(pwm)
        time.sleep(step_delay / 1000.0)

    # Step to 1200
    pwm = 1200
    for i in range(steps_5+1):
        serialCommunication(pwm)
        time.sleep(step_delay / 1000.0)

    # Linear ramp from 1200 to 1300 for 5 seconds
    for i in range(steps_5 + 1):
        pwm = int((i / steps_5) * (1300 - 1200) + 1200)
        serialCommunication(pwm)
        time.sleep(step_delay / 1000.0)

    # Linear ramp from 1300 to 1200 for 5 seconds
    for i in range(steps_5, -1, -1):
        pwm = int((i / steps_5) * (1300 - 1200) + 1200)
        serialCommunication(pwm)
        time.sleep(step_delay / 1000.0)

    # Linear ramp from 1200 to 1400 for 5 seconds
    for i in range(steps_5 + 1):
        pwm = int((i / steps_5) * (1400 - 1200) + 1200)
        serialCommunication(pwm)
        time.sleep(step_delay / 1000.0)

    # Ramp from 1400 to 1050 for 10 seconds
    for i in range(steps_10, -1, -1):
        pwm = int((i / steps_10) * (1050 - 1400) + 1400)
        serialCommunication(pwm)
        time.sleep(step_delay / 1000.0)

    # Set the motor to its minimum value
    pwm = 1050
    serialCommunication(pwm)

# Add a delay to allow the Arduino to reset
time.sleep(12)

# create two threads
thread_1 = threading.Thread(target=Pwm_signal)

# start the threads
thread_1.start()

# wait for the threads to finish
thread_1.join()

              

what type of Arduino do you have? opening the serial port will reboot some arduinos like the UNO

and second guessing how long it will take for the answer to arrive is a recipe for failure. you need to read when there is something to read

1 Like

Hello Jackson! Thanks for your comment.

Yes, it's an UNO. Do you have any other recommendations?

I use what I documented here when I need to communicate between my Mac (Python3) and my arduino

it's in French but google translate will help you out and check out the code, it uses queues to handle the communication both ways and with the keyboard as well

Sometimes I might send a couple of synch bytes

Arduino

void setup()
{
  Serial.begin(115200);
  
  attachInterrupt(digitalPinToInterrupt(2), serialEvent, CHANGE);
  
  //MPU 5060 Setup
  Wire.begin(); 
  mpu.initialize();
  esc.attach(ESC_PIN, 1000, 2000); //calibrating the motor for min and max rpm esc.write(180);
  delay(5000);
  esc.write(0);
  delay(2000);
  esc.write(10); esc.writeMicroseconds(1000); //min PWM

  Serial.print("<>");           // Synch to Python
}

Python

if ser.isOpen():
	synch_arduino = "<>".encode()
	ser.read_until(synch_arduino)
    print("Connection established!")

I have never used serialEvent for initiating serial receiving.

I just poll inside loop for
if (serial.available() )

Your arduino-code is reading two bytes

if (Serial.available()>=2) 
  {
    byte bytes[2];
    Serial.readBytes(bytes, 2);

are you sure that you python code sends only two bytes?

ser.write(struct.pack('BB', 2, mapped_PWM))

For analysing what is going on I would let print the python-code what the code is sending / receiving

and let the arduino-code print all the bytes that you received to the serial monitor.

hm as you are communicating with your PC with python:
do you have an extra USB-to-TTL-adpater?

If not let your python-code send and print something constant to the python-console to see what bytes get sended

Then stop the python-code. Run the arduino-IDE and send the same bytes manually from the serial monitor
let the arduino-code print to the serial monitor what the arduino-code received.

If you can afford to buy a different microcontroller I would buy one with two hardware UARTS
first UART for the communication between your python-code and the microcontroller
second UART for debugging

for example an Seeeduino XIAO costs around $8

best regards Stefan

the struct module is used to create a binary data structure and then it’s sent by using the ser.write() function. ( struct.pack('BB', 2, mapped_PWM) ➜ the format string 'BB' specifies that two unsigned bytes should be packed and then the values are provided, 2 and mapped_PWM, which are being packed into this structure.)

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.