Ivan Nestorovski
Published © GPL3+

DC Motor Position Control with Potentiometer for Robotic Arm

Replacing servos with DC motors + potentiometers for precise, cost-effective robotic arm control.

IntermediateProtip1 hour749
DC Motor Position Control with Potentiometer for Robotic Arm

Things used in this project

Hardware components

Arduino UNO
Arduino UNO
×1
L293D Motor Driver Shield
×1
N20 Geared DC Motor
×4
B10K Potentiometer
×4
3D Printed Gears and Case
×4

Software apps and online services

Arduino IDE
Arduino IDE
Visual Studio 2017
Microsoft Visual Studio 2017

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
Soldering iron (generic)
Soldering iron (generic)
Solder Wire, Lead Free
Solder Wire, Lead Free
Premium Female/Male Extension Jumper Wires, 40 x 6" (150mm)
Premium Female/Male Extension Jumper Wires, 40 x 6" (150mm)

Story

Read more

Custom parts and enclosures

DC MOTOR POSITION CONTROL WITH POTENTIOMETER AND ARDUINO

Proof of concept design that works in my case.

Schematics

Pin Connection

Code

Robotic Finger Joint Control Interface

Python
The Python script creates a Graphical User Interface (GUI) for controlling robotic finger joints using the Tkinter library for GUI elements and the pyserial library for serial communication. The GUI consists of vertical sliders representing each finger joint's position, labels displaying the current position values, and a canvas to visually represent the finger positions.

Upon running the script, it checks if pyserial is installed and installs it if necessary. The user can then select a serial port from the dropdown menu and click the "Connect" button to establish a serial connection with the selected port at a baud rate of 9600.

As the user adjusts the sliders to change the finger joint positions, the script translates these values into a suitable range for motor control and sends the translated values over the serial connection in a specific format ("v{i}:{translated_value}\n").

The script also updates the GUI elements dynamically, displaying the translated position values on the labels and adjusting the canvas to visually represent the finger positions. Additionally, it handles errors related to serial communication and provides feedback in the console for debugging purposes.
# Made by Ivan Nestorovski
# This code is designed for position control of N20 DC Motors.
# You can adjust the precision and usage according to your project needs.
import sys
import subprocess

# List of required libraries
required_libraries = ['tkinter', 'pyserial']

# Function to check and install missing libraries
def check_install_libraries():
    missing_libraries = []
    for lib in required_libraries:
        try:
            __import__(lib)
        except ImportError:
            missing_libraries.append(lib)

    if missing_libraries:
        print(f"Installing missing libraries: {', '.join(missing_libraries)}")
        for lib in missing_libraries:
            subprocess.check_call([sys.executable, "-m", "pip", "install", lib])
        print("Libraries installed successfully.")
    else:
        print("All required libraries are already installed.")

# Call the function to check and install libraries
check_install_libraries()

# Import libraries after installation
import tkinter as tk
from tkinter import ttk
import serial.tools.list_ports

ser = None  # Initialize ser as None


# Function to translate degree value to 450-850 range
def translate_value(value):
    return int(450 + (value / 90) * (850 - 450))

# Function to update the label, finger position, and send the value over serial
def update_label(value, slider, label, canvas, finger_line):
    translated_value = translate_value(float(value))
    label.config(text=f"v{slider.joint_number}: {translated_value}")
    canvas.coords(finger_line, 20, 100 + slider.joint_number * 60, 20 + translated_value / 4.5, 100 + slider.joint_number * 60)

    # Send the value over serial in the format "v1:450\n"
    ser.write(f"v{slider.joint_number}:{translated_value}\n".encode())

# Function to handle the OK button click event in the dialog box
def on_ok():
    global ser
    selected_port = port_var.get()
    print("Selected port:", selected_port)  # Debugging

    # Close the existing serial connection (if any)
    if ser is not None and ser.is_open:
        ser.close()

    # Open a new serial connection with the selected port
    try:
        ser = serial.Serial(selected_port, 9600, timeout=1)
        port_label.config(text=f"Serial Port: {selected_port}")
    except serial.SerialException as e:
        print(f"Error opening serial port: {e}")

# Create the main window
root = tk.Tk()
root.title("Finger Joint GUI")

# Create a frame for the serial port selection
port_frame = ttk.Frame(root)
port_frame.pack(side='top', padx=10, pady=10)

# Create a label for displaying the selected serial port
port_label = ttk.Label(port_frame, text="Select a serial port")
port_label.grid(row=0, column=0, sticky='w')

# Get a list of available serial ports
ports = serial.tools.list_ports.comports()
port_names = [port.device for port in ports]

# Create a dropdown menu for selecting the serial port
port_var = tk.StringVar(port_frame)

if port_names:  # Check if there are any available ports
    port_var.set(port_names[0])  # Set the default value to the first available port
else:
    print("No available serial ports found.")

dropdown = ttk.OptionMenu(port_frame, port_var, *port_names)
dropdown.grid(row=0, column=1, sticky='w')

# Create a button for opening the dialog box to select the serial port
select_button = ttk.Button(port_frame, text="Connect", command=on_ok)
select_button.grid(row=0, column=2, sticky='w')

# Create a canvas to draw fingers
canvas = tk.Canvas(root, width=200, height=400)
canvas.pack(side='left')

# Set a theme for the ttk widgets
style = ttk.Style(root)
style.theme_use('clam')

# Initialize lists for labels and finger lines
labels = []
finger_lines = []

# Create sliders, labels, and finger representations for each finger joint
for i in range(1, 5):
    # Create a scale for the finger joint using ttk for a better look
    slider = ttk.Scale(root, from_=0, to=90, orient='vertical')
    slider.joint_number = i
    slider.pack(side='left', padx=10)

    # Create a label to display the translated value
    label = ttk.Label(root, text=f"v{i}: 450")
    label.pack(side='left', padx=10)
    labels.append(label)

    # Draw a line on the canvas to represent a finger
    finger_line = canvas.create_line(20, 100 + i * 60, 70, 100 + i * 60, width=10)
    finger_lines.append(finger_line)

    # Update the command of the slider to use the current slider and label
    # Use a default argument to capture the current slider
    slider.config(command=lambda value, s=slider, l=label, c=canvas, f=finger_line: update_label(value, s, l, c, f))

root.mainloop()

DC Motor Position Controller

Arduino
This Arduino-based system is designed to control the position of DC motors with precision. It operates by reading the analog value from potentiometers, which are mapped to a range of 0 to 1023, representing the motor’s current position. Simultaneously, it receives the desired position through the computer’s serial input. The system then calculates the error between the current and desired positions.

Utilizing a proportional control algorithm, characterized by a constant factor known as Kp, the system adjusts the motor’s speed and direction to correct the error. This error is transformed into an output signal, ranging from 0 to 255, which is then sent to the L293D motor driver shield. The shield converts this output signal into a Pulse Width Modulation (PWM) signal, effectively controlling the motor’s movement.

To monitor the system’s performance, the Arduino outputs the current position, the desired position, and the calculated error to the serial monitor. This real-time feedback allows for immediate observation of the motor’s response and adjustment of target positions, facilitating hands-on testing and system tuning.

This system is ideal for applications requiring responsive and accurate motor positioning, such as in robotics or automated systems. It provides a straightforward method for implementing closed-loop control, ensuring that the motors reach and maintain the desired positions with high precision.
// Made by Ivan Nestorovski
// This code is designed for position control of N20 DC Motors.
// You can adjust the precision and usage according to your project needs.

#include <AFMotor.h> // Include the Adafruit Motor Shield library

// Initialize an array of AF_DCMotor objects to control up to 4 motors
AF_DCMotor motors[4] = {AF_DCMotor(1), AF_DCMotor(2), AF_DCMotor(3), AF_DCMotor(4)};
// Define the analog input pins connected to the potentiometers
int potPins[4] = {A0, A1, A2, A3};

// Variables to store timing information for control intervals
unsigned long previousMillis = 0;
const long interval = 20; // Control interval in milliseconds
// Arrays to store target positions and last error values for each motor
int targets[4] = {0, 0, 0, 0};
int lastErrors[4] = {0, 0, 0, 0};
// Tolerance for position error
int tolerance = 10;
// Flags to manage new commands from serial input
bool newCommandAvailable = false;
int newTargetMotorIndex = 0;
int newTargetValue = 0;

void setup() {
 Serial.begin(9600); // Start serial communication at 9600 baud rate
 // Initialize motors and read initial potentiometer values
 for (int i = 0; i < 4; i++) {
  motors[i].setSpeed(255); // Set motor speed to maximum
  int initialPotValue = analogRead(potPins[i]); // Read initial potentiometer value
  // Print initial potentiometer values for each motor to the serial monitor
  Serial.print("Initial potentiometer value for motor ");
  Serial.print(i + 1);
  Serial.print(": ");
  Serial.println(initialPotValue);
 }
}

void loop() {
 unsigned long currentMillis = millis(); // Get the current time

 // Check if there is any serial input available
 if (Serial.available()) {
  char c = Serial.read(); // Read the next character from serial input
  // If the character is a digit, process it as part of a command
  if (c >= '0' && c <= '9') {
   if (!newCommandAvailable) {
    // If a new command is not available, build the motor index
    newTargetMotorIndex = newTargetMotorIndex * 10 + c - '0';
   } else {
    // If a new command is available, build the target value
    newTargetValue = newTargetValue * 10 + c - '0';
   }
  } else if (c == ':') {
   // If the character is a colon, mark that a new command is available
   newCommandAvailable = true;
  } else if (c == '\n') {
   // If the character is a newline, process the completed command
   if (newTargetMotorIndex >= 1 && newTargetMotorIndex <= 4) {
    // Set the target position for the specified motor
    targets[newTargetMotorIndex - 1] = newTargetValue;
   }
   // Reset command processing variables
   newCommandAvailable = false;
   newTargetMotorIndex = 0;
   newTargetValue = 0;
  }
 }

 // Control loop for each motor
 for (int i = 0; i < 4; i++) {
  // Check if it's time to update the motor control
  if (currentMillis - previousMillis >= interval && targets[i] != 0) {
   previousMillis = currentMillis; // Update the previousMillis variable

   int potValue = analogRead(potPins[i]); // Read the current potentiometer value
   int error = targets[i] - potValue; // Calculate the position error

   // If the error is greater than the tolerance, adjust motor direction
   if (abs(error) > tolerance) {
    if (error < 0 && error < lastErrors[i]) {
     motors[i].run(FORWARD); // Run motor forward if error is negative and decreasing
    } else if (error > 0 && error > lastErrors[i]) {
     motors[i].run(BACKWARD); // Run motor backward if error is positive and increasing
    }
   } else {
    // If the error is within tolerance, stop the motor and send an acknowledgement
    motors[i].run(RELEASE);
    Serial.print("ack:"); // Acknowledgement prefix
    Serial.println(i + 1); // Send motor index
    targets[i] = 0; // Reset the target position
   }

   lastErrors[i] = error; // Update the last error value
  }
 }
}

Credits

Ivan Nestorovski

Ivan Nestorovski

2 projects • 3 followers

Comments