Modelado y Control de Posición de un Servo Simulado (2ªParte)

Modelado y Control de Posición de un Servo Simulado (2ªParte)

Introducción

Esta es la segunda parte del control del servo simulado. En la primera parte modelamos y sintonizamos un control de velocidad. El control de velocidad a excepción de la ausencia de tiempo muerto, no tuvo mayor complicación, nos enfrentamos a un proceso con respuesta autorregulada fácil de identificar y sintonizar. Ahora es el turno de controlar la posición y afrontar una respuesta completamente diferente, ¿no te lo crees? Pues sigue leyendo.

Funcionamiento

Recordemos que disponemos de un servo de 5 voltios que en su eje lleva acoplado un disco graduado entre 0 y 360 grados. El voltaje aplicado sobre el motor hace que éste gire en un sentido de forma continua pudiéndose invertir la polaridad para que el servo gire en sentido contrario. A medida que aplicamos más voltaje, más rápido gira el disco.

Control de Posición

Atendiendo al simulador, el rango de la posición (PV) va de 0 a 359 grados y el voltaje del motor (OP) va de -5 a 5 voltios. El voltaje positivo y negativo hace que el servo pueda girar en ambos sentidos pero como punto de consigna (SP) sólo admite grados positivos.

Identificación

Tras jugar un poco con el simulador enseguida llegamos a la conclusión de que nos enfrentamos a un integrador puro en el que cada nivel de tensión produce una nueva velocidad constante, y por tanto, la posición crece linealmente en el tiempo.

Imagen 1 – StepTest. Imagen creada con el script Identificacion_v2.py

Mediante un script de Python se ha identificado que la posición crece a una velocidad proporcional al voltaje aplicado, y esa proporción es de aproximadamente 5.72 º/s por voltio.

Imagen 2 – Resumen de tramos resultantes identificados mediante Identificacion_v2.py

Una vez obtenida la pendiente estamos en disposición de hallar varias sintonías que en esta ocasión he añadido en dos partes, Lambda y Servo.

LambdaTf=1sTf=2sTf=3sTf=5s
Kc0.170.350.520.87
Ti1235
Td0000
ServoPIPID
Kc0.140.21
Ti1.52
Td00.5

Resultados

Imagen 3 – Prueba de Sintonías

Lambda:

  • Tf = 1: respuesta rápida, pero demasiado agresiva lo que provoca oscilaciones considerables tanto en la posición (PV) como en la salida (OP).
  • Tf = 2 y 3: similar a la sintonía con Tf=1 aunque con pequeñas oscilaciones y tiempos de asentamiento más largos.
  • Tf = 5: respuesta muy lenta, tarda demasiado en alcanzar el punto de consigna haciendo que la señal oscile en exceso.

Servo:

  • PI: consigue un buen equilibrio entre velocidad y estabilidad, sin sobresaltos ni oscilaciones excesivas.
  • PID: mejora el seguimiento del punto de consigna de forma más precisa, con algo más de movimiento de la salida (OP), pero sin inestabilidad.

Código Python

Identificacion_v2.py

Al ejecutar el script aparecerá un cuadro de diálogo preguntando por el archivo .m. Tras la ejecución nos devuelve dos imágenes png y un txt con los resultados.

import re
import numpy as np
import matplotlib.pyplot as plt
import os
from tkinter import Tk, filedialog
from sklearn.linear_model import LinearRegression

def corregir_salto_angular(th):
    th_corr = th.copy()
    for i in range(1, len(th_corr)):
        if th_corr[i] - th_corr[i - 1] < -300:
            th_corr[i:] += 360
    return th_corr

# --- Selección de archivo ---
Tk().withdraw()
ruta_archivo = filedialog.askopenfilename(
    title="Selecciona el archivo .m con datos",
    filetypes=[("Archivos MATLAB", "*.m")]
)

if not ruta_archivo:
    raise FileNotFoundError("No se seleccionó ningún archivo.")

base_name = os.path.splitext(os.path.basename(ruta_archivo))[0]
nombre_txt = f"{base_name}_modelo.txt"

with open(ruta_archivo, "r", encoding="utf-8") as f:
    contenido = f.read()

coincidencias = re.findall(r"data\s*=\s*\[([\s\S]+?)\];", contenido)
if not coincidencias:
    raise ValueError("No se encontró el bloque 'data = [...]' en el archivo.")

filas = coincidencias[0].strip().split("\n")
datos = np.array([[float(val) for val in fila.strip().split()] for fila in filas])

# Asignar columnas
t, th, w, u, ref = datos.T
th = corregir_salto_angular(th)

# --- Análisis de tramos ---
eps = 1e-3
cambios = np.where(np.abs(np.diff(u)) > eps)[0] + 1
segmentos = [(0, cambios[0])] + [(cambios[i], cambios[i + 1]) for i in range(len(cambios) - 1)] + [(cambios[-1], len(u))]
segmentos = segmentos[1:]  # Eliminar arranque

ganancias = []

plt.figure(figsize=(10, 6))
for i, (ini, fin) in enumerate(segmentos, start=1):
    t_seg = t[ini:fin]
    th_seg = th[ini:fin]
    u_val = u[ini]

    if len(t_seg) < 5:
        continue

    model = LinearRegression()
    model.fit(t_seg.reshape(-1, 1), th_seg)
    pendiente = model.coef_[0]
    th_fit = model.predict(t_seg.reshape(-1, 1))

    ganancias.append((i, u_val, pendiente))

    # Graficar
    plt.plot(t_seg, th_seg, label=f'U={u_val:.2f} V')
    plt.plot(t_seg, th_fit, linestyle='--', linewidth=1, color='black')

plt.title("Tramos de posición (th) y regresiones lineales")
plt.xlabel("Tiempo (s)")
plt.ylabel("Ángulo (°)")
plt.legend()
plt.grid()
plt.tight_layout()
plt.savefig(f"{base_name}_tramos.png", dpi=300)
plt.show()

# --- Modelo global ---
if ganancias:
    U_vals, vel = zip(*[(u, p) for _, u, p in ganancias])
    modelo = LinearRegression().fit(np.array(U_vals).reshape(-1, 1), np.array(vel))
    K = modelo.coef_[0]

    with open(nombre_txt, "w", encoding="utf-8") as ftxt:
        ftxt.write("MODELO IDENTIFICADO\n")
        ftxt.write("-------------------\n")
        ftxt.write(f"G(s) = {K:.4f} / s\n\n")

        ftxt.write("GANANCIAS POR TRAMO\n")
        ftxt.write("-------------------\n")
        for tramo, u_val, pend in ganancias:
            ftxt.write(f"Tramo {tramo}: U = {u_val:.2f} V → Velocidad ≈ {pend:.2f} °/s\n")
        
        ftxt.write("\nSINTONÍAS RECOMENDADAS\n")
        ftxt.write("-----------------------\n")

        # Sintonía LAMBDA (PI)
        ftxt.write("Método LAMBDA (PI puro)\n")
        for lam in [1, 2, 3, 5]:
            Kc = lam / K
            Ti = lam
            ftxt.write(f"λ = {lam:.1f} → Kc = {Kc:.4f}, Ti = {Ti:.4f}, Td = 0\n")

        # Sintonía tipo SERVO PI
        Kc_servo_PI = 0.8 / K
        Ti_servo_PI = 1.5
        ftxt.write("\nTipo SERVO (PI)\n")
        ftxt.write(f"Kc = {Kc_servo_PI:.4f}, Ti = {Ti_servo_PI:.4f}, Td = 0\n")

        # Sintonía tipo SERVO PID
        Kc_servo_PID = 1.2 / K
        Ti_servo_PID = 2
        Td_servo_PID = 0.5
        ftxt.write("\nTipo SERVO (PID)\n")
        ftxt.write(f"Kc = {Kc_servo_PID:.4f}, Ti = {Ti_servo_PID:.4f}, Td = {Td_servo_PID:.4f}\n")

    print(f"\nModelo estimado: G(s) = {K:.2f} / s")
    print(f"Archivo generado: {nombre_txt} + gráfico PNG")
else:
    print("No se obtuvieron tramos válidos.")

Enlaces

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Información básica sobre protección de datos
Responsable Garikoitz Martínez Moreno +info...
Finalidad Gestionar y moderar tus comentarios. +info...
Legitimación Consentimiento del interesado. +info...
Destinatarios Automattic Inc., EEUU para filtrar el spam. +info...
Derechos Acceder, rectificar y cancelar los datos, así como otros derechos. +info...
Información adicional Puedes consultar la información adicional y detallada sobre protección de datos en nuestra página de política de privacidad.