Kontrol Motor DC Encoder dengan PID

Diagram alir sistem
DC Motor Speed Control with PID

Pada tutorial kali ini kita akan menggunakan motor DC Encoder tipe JGA25-370.

Apa itu JGA25-370?

Motor JGA25-370 adalah Motor DC + gearbox + encoder dalam satu paket. Motor Jenis ini Digunakan untuk robotika, kontrol kecepatan, sistem tracking, dan automation system.

Secara konsep fisis Motor DC yaitu mengubah energi listrik menjadi putaran. Untuk Jenis motor JGA25 370 menggunakan tegangan 12V DC. Didalam motor terdapat Gearbox yang bertujuan untuk menurunkan kecepatan dan meningkatkan torsi. Misal Motor memiliki perbandingan gear rasio yaitu 1:4 maka putaran asli: 4000 RPM dan output putaran menjadi 1000 RPM.

Pada Motor juga terdapat Encoder (Hall Sensor). Encoder berfungsi untuk mengukur kecepatan dan arah putaran motor. Pin encoder pada motor JGA25-370 yaitu sebagai berikut:

  1. M1 → Motor +
  2. GND → Ground encoder
  3. C1 → Encoder channel A
  4. C2 → Encoder channel B
  5. VCC → Supply encoder (5V / 3.3V)
  6. M2 → Motor –

Cara kerja encoder yaitu menghasilkan Pulse A dan B (quadrature signal). Dari signal tersebut kita bisa hitung RPM dan deteksi arah (maju / mundur). Konsep penting yang perlu dipahami dalam motor encoder yaitu Pulse Per Revolution (PPR). PPR (Pulse Per Revolution) yaitu jumlah pulse dalam 1 putaran. PPR dipengaruhi oleh jumlah slot encoder dan gear rasio.

Tanpa encoder maka kita tidak tahu kecepatan putaran motor dan tidak bisa control dengan PID. Dengan encoder maka kita bisa kontrol presisi, bisa buat closed-loop system, dan bisa stabil di setpoint tertentu.

Contoh aplikasi yaitu Line follower robot, Kamera tracking system, Conveyor automation, Sistem kontrol berbasis PID, dan sebagainya.

Apa itu L298N?

Driver L298N adalah Modul untuk mengontrol motor DC menggunakan mikrokontroler (seperti Arduino). Fungsinya yaitu Mengatur arah putaran, Mengatur kecepatan (PWM), Menggerakkan 2 motor sekaligus. L298N menggunakan konsep H-Bridge circuit, yaitu suatu konsep memungkinkan motor untuk maju, mundur, stop, dan brake.

Pin penting pada driver L298N yaitu:

  • +12V → supply motor
  • GND → ground (HARUS sama dengan Arduino)
  • +5V → output (kalau jumper aktif)

Motor output

  • OUT1 & OUT2 → Motor A
  • OUT3 & OUT4 → Motor B

Control pin

  • IN1 & IN2 → arah motor A
  • IN3 & IN4 → arah motor B
  • ENA → kontrol kecepatan motor A (PWM)
  • ENB → kontrol kecepatan motor B (PWM)

Cara kerja sederhana: misal Arah motor

  • Jika IN1 (HIGH) dan IN2 (LOW) maka motor Maju.
  • Jika IN1 (LOW) dan IN2 (HIGH) maka motor Mundur.
  • Jika IN1 (LOW) dan IN2 (LOW) maka motor Stop.

Mengatur Kecepatan motor dilakukan dengan PWM di PIN Enable A (EN-A). Misal PWM 0 maka mati atau PWM 255 maka motor bergerak full speed.

Arduino UNO

Dalam tutorial ini kita menggunakan arduino uno. Arduino Uno adalah Mikrokontroler yang berfungsi sebagai “otak” sistem kontrol. Digunakan untuk membaca sensor (encoder), menjalankan algoritma (PID), dan mengontrol aktuator (motor via L298N).

Struktur PIN Arduino Uno terdiri dari Digital Pin (D0–D13), Analog Pin (A0–A5), dan Power Pin. Pin PWM Arduino Uno yaitu D3, D5, D6, D9, D10, D11. Biasanya ditandai simbol ~. Pulse Width Modulation (PWM ) yaitu simulasi tegangan analog dari sinyal digital. Cara kerja PWM yaitu HIGH – LOW – HIGH – LOW (cepat sekali). Duty cycle menentukan “daya”. Misal PWM 0 maka efek daya mati, PWM 127 efek daya setengah, atau PWM 255 efek daya maksimum.

Pemilihan pin encoder sangat mempengaruhi kualitas pembacaan sinyal. Menggunakan interrupt pada Arduino memungkinkan sistem membaca pulsa encoder secara lebih akurat dibanding polling biasa.

Oleh karena itu rekomendasi PIN out yang terhubung antara arduino dan driver seperti berikut:

  • Pada driver L298N EN-A menggunakan pin arduino D5 (PWM)
  • Pada driver L298N IN1 menggunakan pin arduino D4
  • Pada driver L298N IN2 menggunakan pin arduino D3
  • Pada motor DC Encoder A menggunakan pin arduino D2 (interrupt)
  • Pada motor DC Encoder B menggunakan pin arduino D7

Implementasi dengan Coding Arduino

Oke langsung saja, pada bab inti yaitu penggunaan motor driver dan motor encoder dengan arduino.

Setelah semua komponen siap selanjutnya kita buat rangkaian seperti gambar berikut ini

// ================= PIN =================
#define PWM 5
#define IN1 3
#define IN2 4

#define ENCA 7
#define ENCB 6

// ================= PID =================
float Kp = 0.06;
float Ki = 0.055;
float Kd = 0.04;

float setpoint = 0;
float rpm = 0;

float error, prev_error = 0;
float integral = 0;
float derivative;
float output;

// ================= ENCODER =================
volatile long encoderCount = 0;
int lastA = 0;

// ================= PARAM =================
int PPR = 200;  // sesuaikan encoder

unsigned long lastTime = 0;
bool motorRun = false;

// ================= SETUP =================
void setup() {
  Serial.begin(9600);

  pinMode(PWM, OUTPUT);
  pinMode(IN1, OUTPUT);
  pinMode(IN2, OUTPUT);

  pinMode(ENCA, INPUT_PULLUP);
  pinMode(ENCB, INPUT_PULLUP);

  lastA = digitalRead(ENCA);

  digitalWrite(IN1, HIGH);
  digitalWrite(IN2, LOW);

  lastTime = millis();
}

// ================= LOOP =================
void loop() {
  readEncoder();
  readSerial();
  computeRPM();
  computePID();
}

// ================= ENCODER =================
void readEncoder() {
  int currentA = digitalRead(ENCA);

  if (currentA != lastA) {
    if (digitalRead(ENCB) == currentA) {
      encoderCount++;
    } else {
      encoderCount--;
    }
  }
  lastA = currentA;
}

// ================= RPM =================
void computeRPM() {
  unsigned long now = millis();

  if (now - lastTime >= 100) {
    long count = encoderCount;
    encoderCount = 0;

    float new_rpm = (count * 600.0) / PPR;

    // smoothing
    rpm = 0.9 * rpm + 0.1 * new_rpm;

    // kirim ke Python GUI
    Serial.print(setpoint);
    Serial.print(",");
    Serial.println(rpm);

    lastTime = now;
  }
}

// ================= PID =================
void computePID() {
  if (!motorRun) {
    analogWrite(PWM, 0);
    return;
  }

  error = setpoint - rpm;
  integral += error * 0.1;
  // BATASI integral
  if (integral > 100) integral = 100;
  if (integral < -100) integral = -100;
  derivative = (error - prev_error) / 0.1;

  output = Kp * error + Ki * integral + Kd * derivative;
  // clamp output
  if (output > 255) output = 255;
  if (output < 0) output = 0;

  prev_error = error;

  int pwm = constrain(output, 0, 255);

  // anti-stall
  if (pwm > 0 && pwm < 70) pwm = 70;

  analogWrite(PWM, pwm);
}

// ================= SERIAL =================
void readSerial() {
  if (Serial.available()) {
    String data = Serial.readStringUntil('\n');
    data.trim();

    // ===== RUN =====
    if (data == "RUN") {
      motorRun = true;
    }

    // ===== STOP =====
    else if (data == "STOP") {
      motorRun = false;
    }

    // ===== PARAM =====
    else {
      int i1 = data.indexOf(',');
      int i2 = data.indexOf(',', i1 + 1);
      int i3 = data.indexOf(',', i2 + 1);
      int i4 = data.indexOf(',', i3 + 1);

      if (i1 > 0 && i2 > 0 && i3 > 0 && i4 > 0) {
        setpoint = data.substring(0, i1).toFloat();
        Kp = data.substring(i1 + 1, i2).toFloat();
        Ki = data.substring(i2 + 1, i3).toFloat();
        Kd = data.substring(i3 + 1, i4).toFloat();
        PPR = data.substring(i4 + 1).toInt();

        // reset PID biar stabil
        integral = 0;
        prev_error = 0;
      }
    }
  }
}

Code diatas diperuntukan untuk terhubung dengan interface aplikasi yang di program dengan python dengan code berikut.

import tkinter as tk
from tkinter import ttk
import serial
import threading
import time
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

# ================= SERIAL =================
ser = None
running = False

# ================= DATA =================
time_data = []
rpm_data = []
setpoint_data = []

start_time = time.time()

# ================= GUI =================
root = tk.Tk()
root.title("PID Motor Control")
root.geometry("900x600")

# ================= FRAME =================
frame_control = tk.Frame(root)
frame_control.pack(side=tk.TOP, fill=tk.X)

frame_plot = tk.Frame(root)
frame_plot.pack(fill=tk.BOTH, expand=True)

# ================= INPUT =================
tk.Label(frame_control, text="Set Speed (RPM)").grid(row=0, column=0)
entry_sp = tk.Entry(frame_control)
entry_sp.insert(0, "4000")
entry_sp.grid(row=1, column=0)

tk.Label(frame_control, text="Kp").grid(row=0, column=1)
entry_kp = tk.Entry(frame_control)
entry_kp.insert(0, "0.06")
entry_kp.grid(row=1, column=1)

tk.Label(frame_control, text="Ki").grid(row=0, column=2)
entry_ki = tk.Entry(frame_control)
entry_ki.insert(0, "0.055")
entry_ki.grid(row=1, column=2)

tk.Label(frame_control, text="Kd").grid(row=0, column=3)
entry_kd = tk.Entry(frame_control)
entry_kd.insert(0, "0.04")
entry_kd.grid(row=1, column=3)

# ================= PRESENT SPEED =================
tk.Label(frame_control, text="Present RPM").grid(row=0, column=4)
label_rpm = tk.Label(frame_control, text="0")
label_rpm.grid(row=1, column=4)

# ================= COM =================
tk.Label(frame_control, text="COM").grid(row=0, column=5)
entry_com = tk.Entry(frame_control)
entry_com.insert(0, "COM3")
entry_com.grid(row=1, column=5)

#INPUT BOX PPR
tk.Label(frame_control, text="PPR").grid(row=0, column=6)
entry_ppr = tk.Entry(frame_control)
entry_ppr.insert(0, "48")
entry_ppr.grid(row=1, column=6)


# ================= BUTTON =================
def connect_serial():
    global ser
    try:
        ser = serial.Serial(entry_com.get(), 9600, timeout=1)
        print("Connected")
    except:
        print("Failed connect")

def send_param():
    if ser:
        msg = f"{entry_sp.get()},{entry_kp.get()},{entry_ki.get()},{entry_kd.get()},{entry_ppr.get()}\n"
        ser.write(msg.encode())

def run_motor():
    global running
    running = True
    if ser:
        ser.write(b"RUN\n")

def stop_motor():
    global running
    running = False
    if ser:
        ser.write(b"STOP\n")

tk.Button(frame_control, text="Connect", command=connect_serial).grid(row=2, column=0)
tk.Button(frame_control, text="Send", command=send_param).grid(row=2, column=1)

tk.Button(frame_control, text="Run Motor", bg="red", command=run_motor).grid(row=2, column=2)
tk.Button(frame_control, text="Stop Motor", bg="green", command=stop_motor).grid(row=2, column=3)

# ================= PLOT =================
fig, ax = plt.subplots()
line_rpm, = ax.plot([], [], label="RPM")
line_sp, = ax.plot([], [], label="Setpoint")

ax.set_xlabel("Time (s)")
ax.set_ylabel("RPM")
ax.legend()

canvas = FigureCanvasTkAgg(fig, master=frame_plot)
canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)

# ================= UPDATE THREAD =================
def read_serial():
    global time_data, rpm_data, setpoint_data

    while True:
        if ser and ser.in_waiting:
            try:
                line = ser.readline().decode().strip()
                sp, rpm = map(float, line.split(","))

                t = time.time() - start_time

                time_data.append(t)
                rpm_data.append(rpm)
                setpoint_data.append(sp)

                if len(time_data) > 100:
                    time_data.pop(0)
                    rpm_data.pop(0)
                    setpoint_data.pop(0)

                label_rpm.config(text=f"{rpm:.2f}")

            except:
                pass

        time.sleep(0.01)

# ================= UPDATE PLOT =================
def update_plot():
    line_rpm.set_data(time_data, rpm_data)
    line_sp.set_data(time_data, setpoint_data)

    ax.relim()
    ax.autoscale_view()

    canvas.draw()
    root.after(100, update_plot)

# ================= THREAD START =================
threading.Thread(target=read_serial, daemon=True).start()
update_plot()

root.mainloop()
Driver L298N
Arduino UNO
Breadboard MB102
Jumper Wire Male-Female
JGA25 370