MG996R servos freezing when powered from Arduino (two-Arduino setup)

Hello, we were making a project for TÜBİTAK, the project's about assistive tech for people with disabilities.
It's a project where there is 2 servos (MG996R), 2 arduino unos (1 for power only, the other is for controlling) and an ESP32-CAM for vision in control of the servos but here in the prototype stage, we're doing Arduinos. Unfortunately, we couldn't use one arduino for everything because the power wouldn't be enough, after previously damaging boards using barrel jack + USB simultaneously in the past, i told the team something like "Let's use 2 arduinos, one for power and the other for comms", and they agreed.


I connected the servos and the arduinos with a common ground. At first it was great, but then the servos kept freezing randomly. I thought it was something to do with the cables, but they were high quality cables and they weren't hot either.
What could be the issue? Is it the Arduino's regulator?
Here's the code(s) for the arduino:

// THIS IS THE ARDUINO CODE
#include <Servo.h>

Servo xServo;
Servo yServo;

String cmd = "";

void setup() {
  Serial.begin(115200);

  xServo.attach(4);  
  yServo.attach(5);  

  xServo.write(90);
  yServo.write(90);
}

void loop() {
  while (Serial.available()) {
    char c = Serial.read();
    if (c == '\n' || c == '\r') {
      if (cmd.length() > 0) {
        handleCommand(cmd);
        cmd = "";
      }
    } else {
      cmd += c;
    }
  }
}

void handleCommand(String s) {
  char axis = s.charAt(0);
  int value = s.substring(1).toInt();

  value = constrain(value, 0, 180);

  if (axis == 'X' || axis == 'x') {
    xServo.write(value);
  }
  else if (axis == 'Y' || axis == 'y') {
    yServo.write(value);
  }

  Serial.print(axis);
  Serial.println(value);
}


# THIS IS THE PYHTON SCRIPT FOR NOW
import tkinter as tk
import serial
import time
PORT = "COM4"
BAUD = 115200
DELAY = 0.0
ser = serial.Serial(PORT, BAUD, timeout=1)
time.sleep(2)
root = tk.Tk()
root.title("XY Servo Controller")
root.minsize(300, 360)
title = tk.Label(
    root,
    text="XY Servo Controller",
    font=("Segoe UI", 16, "bold")
)
title.pack(pady=(10, 5))
info = tk.Label(
    root,
    text="Drag inside the square to move servos",
    font=("Segoe UI", 10),
    fg="gray"
)
info.pack(pady=(0, 10))
canvas = tk.Canvas(root, bg="#111", highlightthickness=0)
canvas.pack(fill="both", expand=True, padx=15, pady=10)

label = tk.Label(
    root,
    text="X: 90   Y: 90",
    font=("Consolas", 12)
)
label.pack(pady=10)
def send_xy(event):
    w = canvas.winfo_width()
    h = canvas.winfo_height()
    x = max(0, min(event.x, w))
    y = max(0, min(event.y, h))
    # HORIZONTAL REVERSED
    servo_x = int((w - x) / w * 180)
    # VERTICAL NORMAL
    servo_y = int(y / h * 180)
    servo_x = max(0, min(180, servo_x))
    servo_y = max(0, min(180, servo_y))
    ser.write(f"X{servo_x}\n".encode())
    time.sleep(DELAY)
    ser.write(f"Y{servo_y}\n".encode())
    label.config(text=f"X: {servo_x}   Y: {servo_y}")
    r = max(5, min(w, h) // 40)
    canvas.delete("cursor")
    canvas.create_oval(
        x - r, y - r,
        x + r, y + r,
        fill="#ff4444",
        outline="",
        tags="cursor"
    )
canvas.bind("<Button-1>", send_xy)
canvas.bind("<B1-Motion>", send_xy)
def on_close():
    ser.close()
    root.destroy()
root.protocol("WM_DELETE_WINDOW", on_close)
root.mainloop()

I'd like to hear from you guys about a solution.
Thanks,
@the_com3_port

The Uno, indeed any Arduino board is not designed to be used as a power supply for servos or any other high current component, so what you propose is a bad idea. Why not use an alternative power source such as a 5V wall adapter of or 4 AA batteries in series

1 Like

Is the LM2596 or alternatives good enough for it?

The external power supply for the servos should ideally be able to supply at least 4 amps, if not more. This might sound a lot but the stall current of a MG996R can be as high as 2 amps and occurs when it starts moving

1 Like

If you have a USB 5V power supply or phone charger, you could try using one of those.

FOLLOWUP:

I removed the Ardunios and added a single Arduino that is responsible for servos and an esp32cam. here are the codes for anyone wondering:

//ESP32 CODE
#include "esp_camera.h"
#include <WiFi.h>
#include "esp_http_server.h"
#include "soc/soc.h"
#include "soc/rtc_cntl_reg.h"

// ================= GLOBAL SETTINGS =================
const char* ssid = "ESP32-CAM";
#define ARDUINO_TX 14 
#define BAUDRATE 19200
HardwareSerial SerialServo(1); 

// Handle for the server instance
httpd_handle_t camera_httpd = NULL;

// ================= HTML UI =================
const char* index_html = R"rawliteral(
<!DOCTYPE html><html><head><title>ESP32-CAM Turret</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
  body { background:#111; color:white; text-align:center; font-family:Arial; }
  .btn { font-size:24px; margin:5px; width:100px; height:60px; background:#444; color:white; border:1px solid #0f0; border-radius:8px; }
  .btn:active { background:#0f0; color:black; }
  img { width:100%; max-width:600px; border:2px solid #0f0; margin-top:10px; }
  .controls { display: grid; grid-template-columns: 1fr 1fr 1fr; width: 320px; margin: 20px auto; }
</style></head><body>
  <h3>ESP32-CAM TURRET</h3>
  <img src="/stream" id="stream">
  <div class="controls">
    <div></div><button class="btn" onclick="cmd('UP')">UP</button><div></div>
    <button class="btn" onclick="cmd('LEFT')">LEFT</button>
    <div></div>
    <button class="btn" onclick="cmd('RIGHT')">RIGHT</button>
    <div></div><button class="btn" onclick="cmd('DOWN')">DOWN</button><div></div>
  </div>
<script>
  function cmd(d) { fetch(`/move?dir=${d}`); }
</script></body></html>
)rawliteral";

// ================= HANDLERS =================

// Stream Handler (Sends JPGs)
static esp_err_t stream_handler(httpd_req_t *req){
  camera_fb_t * fb = NULL;
  esp_err_t res = ESP_OK;
  char * part_buf[64];

  res = httpd_resp_set_type(req, "multipart/x-mixed-replace; boundary=frame");
  if(res != ESP_OK) return res;

  while(true){
    fb = esp_camera_fb_get();
    if(!fb) { delay(10); continue; }
    
    size_t hlen = snprintf((char *)part_buf, 64, "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n", fb->len);
    res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen);
    if(res == ESP_OK) res = httpd_resp_send_chunk(req, (const char *)fb->buf, fb->len);
    if(res == ESP_OK) res = httpd_resp_send_chunk(req, "\r\n", 2);
    
    esp_camera_fb_return(fb);
    if(res != ESP_OK) break;
  }
  return res;
}

// Fixed Command Handler
static esp_err_t move_handler(httpd_req_t *req){
    char* buf;
    size_t buf_len;
    char   val[32];

    // Read the query string from the URL
    buf_len = httpd_req_get_url_query_len(req) + 1;
    if (buf_len > 1) {
        buf = (char*)malloc(buf_len);
        if (httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) {
            // Now we search the query string for the "dir" key
            if (httpd_query_key_value(buf, "dir", val, sizeof(val)) == ESP_OK) {
                SerialServo.println(val); // Send to Arduino
                Serial.printf("Command Sent: %s\n", val);
            }
        }
        free(buf);
    }
    httpd_resp_send(req, "OK", 2);
    return ESP_OK;
}

static esp_err_t index_handler(httpd_req_t *req){
  httpd_resp_send(req, index_html, strlen(index_html));
  return ESP_OK;
}

// ================= SERVER START =================
void startCameraServer(){
  httpd_config_t config = HTTPD_DEFAULT_CONFIG();
  config.server_port = 80;

  httpd_uri_t index_uri = { .uri="/", .method=HTTP_GET, .handler=index_handler, .user_ctx=NULL };
  httpd_uri_t move_uri = { .uri="/move", .method=HTTP_GET, .handler=move_handler, .user_ctx=NULL };
  httpd_uri_t stream_uri = { .uri="/stream", .method=HTTP_GET, .handler=stream_handler, .user_ctx=NULL };

  if (httpd_start(&camera_httpd, &config) == ESP_OK) {
    httpd_register_uri_handler(camera_httpd, &index_uri);
    httpd_register_uri_handler(camera_httpd, &move_uri);
    httpd_register_uri_handler(camera_httpd, &stream_uri);
    Serial.println("Server started");
  }
}

void setup() {
  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); // Disable brownout
  Serial.begin(115200);
  
  // Serial1: RX=-1 (none), TX=14
  SerialServo.begin(BAUDRATE, SERIAL_8N1, -1, ARDUINO_TX);

  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = 5; config.pin_d1 = 18; config.pin_d2 = 19; config.pin_d3 = 21;
  config.pin_d4 = 36; config.pin_d5 = 39; config.pin_d6 = 34; config.pin_d7 = 35;
  config.pin_xclk = 0; config.pin_pclk = 22; config.pin_vsync = 25; config.pin_href = 23;
  config.pin_sccb_sda = 26; config.pin_sccb_scl = 27; config.pin_pwdn = 32;
  config.pin_reset = -1; config.xclk_freq_hz = 20000000; config.pixel_format = PIXFORMAT_JPEG;
  
  // Using QVGA to ensure smooth video over WiFi AP
  config.frame_size = FRAMESIZE_QVGA; 
  config.jpeg_quality = 12;
  config.fb_count = 1;

  if(esp_camera_init(&config) != ESP_OK) {
    Serial.println("Camera Init Failed");
    return;
  }

  WiFi.softAP(ssid);
  Serial.print("AP IP Address: ");
  Serial.println(WiFi.softAPIP());

  startCameraServer();
}

void loop() { 
  delay(100); 
}
//arduino code
#include <Servo.h>
#include <SoftwareSerial.h>

Servo xServo, yServo;
SoftwareSerial espSerial(10, -1); // RX=10, TX unused

int xPos = 90, yPos = 90;
int xTarget = 90, yTarget = 90;
unsigned long lastMoveTime = 0;
const int stepInterval = 15; // ms between each degree step

void setup() {
  xServo.attach(8);
  yServo.attach(9);
  xServo.write(xPos);
  yServo.write(yPos);
  
  espSerial.begin(19200);
  Serial.begin(115200);
}

void loop() {
  // 1. Read Serial Commands (Non-blocking)
  if (espSerial.available()) {
    String cmd = espSerial.readStringUntil('\n');
    cmd.trim();
    if (cmd == "UP")    yTarget = min(180, yTarget + 15);
    if (cmd == "DOWN")  yTarget = max(0, yTarget - 15);
    if (cmd == "LEFT")  xTarget = max(0, xTarget - 15);
    if (cmd == "RIGHT") xTarget = min(180, xTarget + 15);
  }

  // 2. Smooth Movement (Non-blocking using millis)
  if (millis() - lastMoveTime > stepInterval) {
    if (xPos < xTarget) xPos++;
    if (xPos > xTarget) xPos--;
    if (yPos < yTarget) yPos++;
    if (yPos > yTarget) yPos--;
    
    xServo.write(xPos);
    yServo.write(yPos);
    lastMoveTime = millis();
  }
}

So how are you powering the servos?

With a Yihua 3010D III. I'll switch to a LM2596

It can only do 3A at best and that required a heatsink.

Don't forget that you need a common GND connection for the external supply to the servos and the Arduino

Okay!