
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.

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.

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.
Lambda | Tf=1s | Tf=2s | Tf=3s | Tf=5s |
Kc | 0.17 | 0.35 | 0.52 | 0.87 |
Ti | 1 | 2 | 3 | 5 |
Td | 0 | 0 | 0 | 0 |
Servo | PI | PID |
Kc | 0.14 | 0.21 |
Ti | 1.5 | 2 |
Td | 0 | 0.5 |
Resultados

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
- Simulador [garikoitz.info]
- Carpeta del Proyecto [garikoitz.info]
- Respuesta integradora [garikoitz.info]
- Laboratorio virtuales [Unilabs] [Simulador]
- Modelado y Control de Velocidad de un servo simulado (1ªparte) [garikoitz.info]