← Índice del curso

Robótica y Automatización · Tema 24

Proyecto: brazo robótico con servomotores

Juntamos lo aprendido desde el tema 15: servos, rampas, perfiles de velocidad, transformaciones y cinemática. El resultado es un brazo de dos servos al que le escribimos «M 10 10» y lleva su extremo a ese punto del plano en línea recta, con límites, validaciones y una parada segura.

01 · Alcance

Qué vamos a construir

El proyecto es un brazo plano de dos articulaciones con servos de posición, controlado por un Arduino UNO R3. Se maneja por el monitor serie con comandos de texto. Su función es llevar el extremo a un punto (x, y) en centímetros, en el marco de la base, recorriendo una recta y deteniéndose con seguridad.

Qué hace el proyecto y qué no.
IncluyeNo incluye
Cinemática inversa con elección de codo por continuidadMedición de la posición real (lazo abierto)
Validación de alcance, límites, apoyo y trayectoriaControl de la orientación de la herramienta
Movimiento en recta con perfil trapezoidalCompensación de carga, holgura o flexión
Calibración con dos puntos por servoParada de emergencia por software
Soltado automático de los servos tras inactividadDetección de atascos

Un proyecto se define por sus requisitos antes que por su código. Por eso el orden de este tema es: requisitos, hardware, arquitectura, calibración, código, simulación y pruebas.

02 · Qué debe cumplir

Requisitos y cómo se verifican

Un requisito útil es verificable: tiene un criterio que se puede medir. Estos son los del proyecto, con valores de partida que ajustás a tu brazo:

Requisitos funcionales (F), de seguridad (S) y de calidad (C).
IdRequisitoVerificación
F1Llevar el extremo a un punto (x, y) de la zona utilizableMedir con regla en tres puntos; error ≤ 5 mm
F2El extremo recorre una recta entre el punto actual y el destinoDibujar con un lápiz o comparar con una escuadra; desvío ≤ 5 mm
F3Velocidad cartesiana limitada a 4 cm/s y aceleración a 8 cm/s²Cronometrar un recorrido de 10 cm: unos 3,0 s
S1Rechazar con motivo todo destino fuera de alcance, límites o apoyoProbar un caso de cada tipo
S2Rechazar una recta cuyo recorrido salga de la zonaProbar un destino que cruza el hueco central
S3Frenar con la aceleración planificada ante el comando SEnviar S durante un movimiento y observar
S4Dejar de generar pulsos tras 10 s sin órdenesEsperar y verificar el mensaje
C1No reiniciarse ni tener caídas de tensión en movimientoObservar el Arduino y la fuente con carga
C2Repetir un punto con ≤ 3 mm de dispersiónIr y volver 10 veces y medir

Los límites de velocidad y aceleración (F3) no son los del servo, sino los que elegimos para que el movimiento sea suave y el torque sostenible. Los límites articulares son θ1 de 0° a 180° y θ2 de −90° a +90°: es lo que da un servo estándar montado con el cero de la articulación del codo en el centro de su recorrido.

Con esos límites y un apoyo en y = 0, la zona utilizable es una franja de unos 12,8 a 18 cm de la base, de unos 278 cm² (tema 23: el límite de θ2 agranda el hueco central). Es una zona modesta, y es un resultado de diseño que conviene ver antes de construir.

03 · Montaje

Hardware, montaje y alimentación

  • Controlador: Arduino UNO R3.
  • Actuadores: dos servos de posición de rotación limitada, del tipo estándar; el de la base soporta todo el peso, así que es el más exigente.
  • Eslabones: livianos y rígidos, con longitudes entre ejes L1 = 10 cm y L2 = 8 cm (medílas).
  • Base: firme, fijada al plano de trabajo. Un brazo que se mueve sobre su base invalida todas las coordenadas.
  • Alimentación de potencia: fuente regulada para servos, en el rango de tensión del modelo y con capacidad holgada de corriente de arranque y carga.
  • Interruptor de potencia para cortar solo la alimentación de los servos, accesible y visible (tema 4).
  • Capacitor de reserva cerca de los servos (del orden de cientos de µF; verificá con mediciones, tema 15).
Conexión del Arduino, la fuente y los dos servos La fuente regulada alimenta a los dos servos a través de un interruptor y un capacitor. El Arduino envía una señal a cada servo desde los pines 6 y 7. Todas las masas están unidas en un punto común. El Arduino se alimenta por USB y no entrega la corriente de los servos. Arduino UNO R3D6 y D7: señalesFuente reguladapara servosInterruptory capacitorServo 1 (base)θ₁Servo 2 (codo)θ₂señal D6señal D7masa común (verde)potencia (negro)

Reglas de conexión (tema 15): la corriente de los servos no sale del pin de 5 V del Arduino; las masas de la fuente y del Arduino se unen; la señal va por un hilo y la potencia por otro, con cables gruesos y cortos. Con Servo en el UNO R3, los pines 9 y 10 pierden el PWM por el uso del Timer1; usamos D6 y D7.

Antes de montar los eslabones, probá cada servo sin carga y con su horn desconectado del brazo. Armá el brazo con los servos centrados, para que el cero geométrico coincida con una referencia mecánica. Dejá los cables de modo que no tiren del mecanismo en ninguna pose.

Torque de diseño. Con eslabones de 0,05 kg y 0,04 kg y una carga de 0,02 kg (por ejemplo, un lápiz con su soporte), el brazo extendido horizontal exige en la base unos 0,115 N·m; con el factor 1,5 del tema 15, 0,17 N·m (1,8 kgf·cm). Elegí un servo cuya ficha declare un valor holgadamente mayor a la tensión que usarás, recordando que el torque de bloqueo no es continuo.

04 · Capas

Arquitectura del software

El firmware se organiza en capas, cada una con una responsabilidad y una interfaz pequeña. Los datos bajan de los comandos a los servos; las validaciones suben:

Capas del firmware del brazo Seis capas apiladas: comandos por serie, planificador de recta con perfil trapezoidal, cinemática inversa con elección de rama, validación de zona, calibración de ángulo a microsegundos y servos. Una flecha de seguridad lateral indica que el soltado por inactividad actúa sobre los servos. Comandos por serie: C, M x y, H, S, D, ?Planificador: recta, perfil trapezoidal, 20 msCinemática inversa y elección de rama (temas 21 y 22)Validación: alcance, límites, apoyo (tema 23)Calibración: grados → µs (temas 15 y 21)Servos (tema 15)Seguridad:límites, velocidadarticular máxima,soltado porinactividad

Esta separación tiene un propósito práctico. La capa de cinemática no sabe de servos: se puede probar en Python con los mismos números. La de calibración no sabe de geometría: si cambiás de servo, solo se tocan sus constantes. El planificador no sabe de pines. Cada parte se prueba por separado, y un error se localiza por capas.

El programa no usa delay() ni bucles que bloqueen: cada vuelta de loop() lee el puerto serie, avanza un paso del movimiento si pasaron 20 ms y revisa el tiempo de inactividad (temas 16 y 19). Un movimiento es un estado, no una función que espera.

05 · Antes de mover nada

Calibración paso a paso

La cinemática supone longitudes y ángulos exactos; la realidad ofrece valores medidos. Este procedimiento reduce la diferencia. Hacelo con los eslabones montados, a baja velocidad y sin carga.

  1. Medí las longitudes L1 y L2 entre los centros de los ejes de giro, con una regla o un calibre. Actualizalas en el código.
  2. Fijá el cero geométrico. Con θ1 = 0° el primer eslabón debe apuntar a lo largo del eje x; con θ2 = 0° los eslabones quedan alineados. Marcá esas poses con una plantilla.
  3. Medí dos puntos por servo. Para el servo de la base, ordená un ancho de pulso y registrá el ángulo geométrico real (transportador o plantilla) en dos poses separadas, por ejemplo 0° y 180°. Repetí con el codo en −90° y +90°. Esos pares son CAL_TH y CAL_US.
  4. Definí la ventana segura US_LIM algo dentro de los extremos medidos, para no empujar contra los topes del servo (tema 15).
  5. Verificá los sentidos. Aumentar θ1 debe girar la base en sentido antihorario; aumentar θ2 debe doblar el codo en sentido antihorario relativo al primer eslabón. Si no, intercambiá el orden de los puntos de calibración (un signo en la pendiente).
  6. Comprobá en tres poses distintas que el ángulo real coincide con el ordenado, y registrá el error residual.

La calibración con dos puntos supone que la relación entre microsegundos y ángulo es lineal entre ellos. Si el error en una pose intermedia es apreciable, agregá un tercer punto o una corrección por tramos.

Cada microsegundo vale poco, pero un error de cero sostenido pesa: 20 µs en el servo de la base equivalen a 2° y mueven el extremo unos 5 mm. El laboratorio 2 lo muestra. Por eso la calibración manda sobre la resolución del servo.

06 · Código

El firmware completo

Los comandos, en una línea terminada en salto de línea: C conecta los servos y los lleva a HOME (un salto desde una posición desconocida: hacelo con el brazo libre); M x y mueve el extremo a ese punto en cm; H vuelve a HOME; S frena con rampa; D deja de generar pulsos; ? informa ángulos y posición ordenados.

#include <Servo.h>

// ---------- Geometría (cm) y límites (grados) ----------
const float L1 = 10.0, L2 = 8.0;                       // medidas entre ejes
const float TH_MIN[2] = {0.0, -90.0};
const float TH_MAX[2] = {180.0, 90.0};
const float Y_MIN = 0.0;                               // apoyo
const float HOME_TH[2] = {90.0, -45.0};

// ---------- Calibración: dos puntos medidos por servo ----------
const float CAL_TH[2][2] = {{0.0, 180.0}, {-90.0, 90.0}};      // grados geométricos
const float CAL_US[2][2] = {{600.0, 2400.0}, {700.0, 2300.0}}; // microsegundos medidos
const int   US_LIM[2][2] = {{550, 2450}, {650, 2350}};         // ventana segura
const byte  PIN_SERVO[2] = {6, 7};

// ---------- Movimiento ----------
const float V_CART = 4.0, A_CART = 8.0, V_MIN = 0.3;   // cm/s, cm/s², cm/s
const float W_MAX = 120.0;                             // °/s por articulación
const unsigned long PERIODO_MS = 20, SIN_ORDEN_MS = 10000;

enum Estado { REPOSO, MOVIENDO };
Servo servo[2];
Estado estado = REPOSO;
bool conectado = false, detener = false;
float th[2] = {90.0, -45.0};                           // ángulos ordenados
float p0x, p0y, ux, uy, largo, recorrido, vel;
unsigned long ultimoCiclo = 0, ultimaOrden = 0;
char linea[32];
byte nLinea = 0;

// ---------- Geometría ----------
void directa(const float a[2], float &x, float &y) {
  const float t1 = a[0] * DEG_TO_RAD, f = (a[0] + a[1]) * DEG_TO_RAD;
  x = L1 * cos(t1) + L2 * cos(f);
  y = L1 * sin(t1) + L2 * sin(f);
}

byte soluciones(float x, float y, float s[2][2]) {
  float d = (x * x + y * y - L1 * L1 - L2 * L2) / (2.0 * L1 * L2);
  if (d > 1.0 + 1e-6 || d < -1.0 - 1e-6) return 0;
  d = constrain(d, -1.0, 1.0);
  const float raiz = sqrt(1.0 - d * d);
  for (byte k = 0; k < 2; k++) {
    const float t2 = atan2(k == 0 ? raiz : -raiz, d);
    float t1 = (atan2(y, x) - atan2(L2 * sin(t2), L1 + L2 * cos(t2))) * RAD_TO_DEG;
    while (t1 > 180.0) t1 -= 360.0;
    while (t1 <= -180.0) t1 += 360.0;
    s[k][0] = t1;
    s[k][1] = t2 * RAD_TO_DEG;
  }
  return 2;
}

bool valida(const float s[2], float y) {
  for (byte i = 0; i < 2; i++) if (s[i] < TH_MIN[i] || s[i] > TH_MAX[i]) return false;
  return y >= Y_MIN && L1 * sin(s[0] * DEG_TO_RAD) >= Y_MIN;
}

// Elige la solución válida más cercana a ref; false si no hay ninguna.
bool elegir(float x, float y, const float ref[2], float out[2]) {
  float s[2][2];
  if (soluciones(x, y, s) == 0) return false;
  float mejor = 1e9;
  bool hay = false;
  for (byte k = 0; k < 2; k++) {
    if (!valida(s[k], y)) continue;
    const float costo = fabs(s[k][0] - ref[0]) + fabs(s[k][1] - ref[1]);
    if (costo < mejor) { mejor = costo; out[0] = s[k][0]; out[1] = s[k][1]; hay = true; }
  }
  return hay;
}

// ---------- Servos ----------
int aMicros(byte i, float grados) {
  const float us = CAL_US[i][0] + (grados - CAL_TH[i][0]) * (CAL_US[i][1] - CAL_US[i][0]) / (CAL_TH[i][1] - CAL_TH[i][0]);
  return constrain((int)(us + 0.5), US_LIM[i][0], US_LIM[i][1]);
}

void escribir() {
  for (byte i = 0; i < 2; i++) servo[i].writeMicroseconds(aMicros(i, th[i]));
}

void conectar() {
  if (conectado) return;
  th[0] = HOME_TH[0]; th[1] = HOME_TH[1];
  for (byte i = 0; i < 2; i++) servo[i].attach(PIN_SERVO[i], US_LIM[i][0], US_LIM[i][1]);
  escribir();
  conectado = true;
  Serial.println("Conectado: salto inicial hasta HOME desde una posicion desconocida");
}

void desconectar() {
  for (byte i = 0; i < 2; i++) { servo[i].detach(); digitalWrite(PIN_SERVO[i], LOW); }
  conectado = false;
  estado = REPOSO;
  Serial.println("Sin pulsos; estado mecanico no confirmado");
}

// ---------- Planificación ----------
const char *motivoDestino(float x, float y) {
  float s[2][2];
  if (soluciones(x, y, s) == 0) return "destino fuera de alcance";
  for (byte k = 0; k < 2; k++) if (valida(s[k], y)) return nullptr;
  return (y < Y_MIN) ? "destino bajo el apoyo" : "destino fuera de limites";
}

void iniciarMovimiento(float x, float y) {
  const char *motivo = motivoDestino(x, y);
  if (motivo) { Serial.print("Rechazado: "); Serial.println(motivo); return; }

  directa(th, p0x, p0y);
  const float dx = x - p0x, dy = y - p0y;
  largo = sqrt(dx * dx + dy * dy);
  if (largo < 0.05) { Serial.println("Ya esta en el punto"); return; }
  ux = dx / largo; uy = dy / largo;

  // Comprobación previa de toda la recta, punto a punto.
  float ref[2] = {th[0], th[1]}, s[2];
  const int n = (int)ceil(largo / 0.5);
  for (int k = 1; k <= n; k++) {
    const float px = p0x + dx * k / n, py = p0y + dy * k / n;
    if (!elegir(px, py, ref, s) || fabs(s[0] - ref[0]) > 20.0 || fabs(s[1] - ref[1]) > 20.0) {
      Serial.print("Rechazado: la trayectoria sale de la zona cerca de (");
      Serial.print(px, 1); Serial.print("; "); Serial.print(py, 1); Serial.println(")");
      return;
    }
    ref[0] = s[0]; ref[1] = s[1];
  }
  recorrido = 0; vel = 0; detener = false;
  estado = MOVIENDO;
  ultimoCiclo = ultimaOrden = millis();
}

void pasoDeMovimiento(unsigned long ahora) {
  const float dt = (ahora - ultimoCiclo) / 1000.0;
  ultimoCiclo = ahora;
  const float restante = largo - recorrido;
  vel = min(min(vel + A_CART * dt, V_CART), sqrt(2.0 * A_CART * restante));
  if (vel < V_MIN) vel = V_MIN;
  recorrido = min(largo, recorrido + vel * dt);

  float nuevo[2];
  if (!elegir(p0x + ux * recorrido, p0y + uy * recorrido, th, nuevo)) {
    estado = REPOSO; Serial.println("Abortado: punto no utilizable"); return;
  }
  const float w = max(fabs(nuevo[0] - th[0]), fabs(nuevo[1] - th[1])) / dt;
  if (w > W_MAX) { estado = REPOSO; Serial.println("Abortado: velocidad articular excedida"); return; }
  th[0] = nuevo[0]; th[1] = nuevo[1];
  escribir();

  if (recorrido >= largo - 1e-4) {
    estado = REPOSO; ultimaOrden = ahora;
    float x, y; directa(th, x, y);
    Serial.print(detener ? "Detenido en (" : "Llegada ordenada a (");
    Serial.print(x, 2); Serial.print("; "); Serial.print(y, 2); Serial.println(") - posicion real no confirmada");
  }
}

// ---------- Comandos ----------
void procesar(char *texto) {
  const char cmd = toupper(texto[0]);
  if (cmd == 'C') conectar();
  else if (cmd == 'D') desconectar();
  else if (cmd == 'S') {
    if (estado == MOVIENDO) {
      largo = min(largo, recorrido + vel * vel / (2.0 * A_CART) + 0.01);
      detener = true;
    }
  } else if (cmd == '?') {
    float x, y; directa(th, x, y);
    Serial.print("th1="); Serial.print(th[0], 1); Serial.print(" th2="); Serial.print(th[1], 1);
    Serial.print(" x="); Serial.print(x, 2); Serial.print(" y="); Serial.println(y, 2);
  } else if (cmd == 'H' || cmd == 'M') {
    if (!conectado) { Serial.println("Primero C"); return; }
    if (estado == MOVIENDO) { Serial.println("Ocupado"); return; }
    float x, y;
    if (cmd == 'H') directa(HOME_TH, x, y);
    else { char *fin; x = strtod(texto + 1, &fin); y = strtod(fin, nullptr); }
    iniciarMovimiento(x, y);
  }
}

void leerSerie() {
  while (Serial.available() > 0) {
    const char c = Serial.read();
    if (c == '\n' || c == '\r') {
      if (nLinea > 0) { linea[nLinea] = 0; procesar(linea); nLinea = 0; }
    } else if (nLinea < sizeof(linea) - 1) {
      linea[nLinea++] = c;
    }
  }
}

void setup() {
  digitalWrite(PIN_SERVO[0], LOW); digitalWrite(PIN_SERVO[1], LOW);
  pinMode(PIN_SERVO[0], OUTPUT); pinMode(PIN_SERVO[1], OUTPUT);
  Serial.begin(115200);
  Serial.println("C: conectar; M x y: ir a (x,y) cm; H: home; S: frenar; D: soltar; ?: estado");
}

void loop() {
  leerSerie();
  const unsigned long ahora = millis();
  if (estado == MOVIENDO && ahora - ultimoCiclo >= PERIODO_MS) pasoDeMovimiento(ahora);
  if (conectado && estado == REPOSO && ahora - ultimaOrden >= SIN_ORDEN_MS) desconectar();
}

Cómo se relaciona con los temas anteriores:

  • aMicros() y escribir(): calibración y servos (temas 15 y 21). La ventana US_LIM se pasa a attach().
  • soluciones() y elegir(): cinemática inversa y elección de rama por continuidad (tema 22).
  • valida() y motivoDestino(): límites, apoyo y motivo del rechazo (tema 23).
  • iniciarMovimiento(): comprobación previa de toda la recta, punto por punto (temas 22 y 23).
  • pasoDeMovimiento(): perfil trapezoidal en la distancia cartesiana (tema 17), dt medido (temas 16 y 18) y límite de velocidad articular (tema 16).
  • leerSerie() y loop(): estado sin bloqueos (temas 16 y 19).

Sesión típica por el monitor serie (a 115200 baudios, con fin de línea): C, M 10 10 (llegada ordenada a (10,00; 10,00) tras unos 2,2 s), M 14 6, M -6 12 (rechazada: la recta cruza el hueco central), M 20 0 (fuera de alcance), M 0 5 (fuera de límites), M 8 -2 (bajo el apoyo), H, D.

Tres decisiones de seguridad están en el código: el primer comando C salta a HOME desde una posición desconocida (tema 15), pero después cada paso respeta la velocidad articular máxima; todo destino y toda la recta se validan antes de mover; y si el brazo queda 10 s sin órdenes, se retira la señal. Retirar la señal no corta la potencia: el comportamiento del servo sin pulsos depende del modelo, y el interruptor de potencia es la medida real de seguridad.

Limitaciones asumidas: lazo abierto (el programa informa posiciones ordenadas), una sola tarea a la vez, y un análisis simple de la trayectoria (puntos cada 0,5 cm). No detecta atascos, colisiones con objetos ni pérdida de calibración. Cuando S frena un movimiento, el brazo queda donde se detuvo, y el siguiente M parte desde esa pose.

07 · Exploración interactiva

Laboratorios: plan de movimiento y calibración

1. Probar un plan antes de mover el brazo

Elegí un punto de partida y un destino. El laboratorio ejecuta la misma lógica del firmware: valida el destino, comprueba la recta punto a punto y simula el movimiento con el perfil trapezoidal. La zona lima es la utilizable; el círculo violeta es HOME.

Veredicto—
Duración estimada—
Ángulos finales—
Velocidad articular máxima—

Elegí inicio y destino.

Con el inicio y el destino por defecto, el movimiento se acepta, dura unos 2,2 s y la articulación más rápida llega a unos 29 °/s. Poné el inicio en (10; 10) y el destino en (−6; 12): ambos puntos están en la zona, pero la recta pasa por el hueco central y se rechaza cerca de (7,6; 10,3). Poné el destino en (20; 0): fuera de alcance. En (0; 5): fuera de límites. En (8; −2): bajo el apoyo. Cada rechazo tiene su causa.

2. Qué cuesta un error de calibración

Un desfase de cero constante en cada servo (en µs) se traduce en un error angular. Servo 1: 1800 µs para 180° (10 µs/°). Servo 2: 1600 µs para 180° (8,9 µs/°). Se calcula cuánto se desplaza el extremo en el punto elegido.

Error angular servo 1 / 2—
Desplazamiento del extremo—
Efecto de 1 µs en el servo 1—

Elegí un punto y errores de cero.

Con 20 µs de error en ambos servos y el punto (10; 10), el extremo se corre unos 7,6 mm: más que la tolerancia de 5 mm del requisito F1. Un solo microsegundo mueve el extremo unos 0,25 mm. Con errores de signos opuestos pueden compensarse en un punto y sumarse en otro, por eso la calibración se verifica en varias poses.

08 · Código ejecutable

Simular el plan en Python

Replicamos la lógica del firmware (validación del destino, comprobación de la recta, perfil trapezoidal y límite de velocidad articular) y la aplicamos a una secuencia de comandos. Sirve para predecir duraciones y rechazos y para comparar con el brazo real.

Probalo en el navegador. Python se descarga con Pyodide la primera vez y requiere conexión. El código es editable y no controla ningún brazo.

import math

L1, L2 = 10.0, 8.0
TH_MIN, TH_MAX = (0.0, -90.0), (180.0, 90.0)
Y_MIN = 0.0
V_CART, A_CART, V_MIN = 4.0, 8.0, 0.3        # cm/s, cm/s2, cm/s
W_MAX, DT = 120.0, 0.02                      # grados/s, s
HOME = (90.0, -45.0)

def directa(th):
    a, f = math.radians(th[0]), math.radians(th[0] + th[1])
    return L1 * math.cos(a) + L2 * math.cos(f), L1 * math.sin(a) + L2 * math.sin(f)

def soluciones(x, y):
    d = (x * x + y * y - L1 ** 2 - L2 ** 2) / (2 * L1 * L2)
    if abs(d) > 1 + 1e-9:
        return []
    d = max(-1.0, min(1.0, d))
    s = math.sqrt(1 - d * d)
    res = []
    for signo in (1, -1):
        t2 = math.atan2(signo * s, d)
        t1 = math.atan2(y, x) - math.atan2(L2 * math.sin(t2), L1 + L2 * math.cos(t2))
        t1 = (math.degrees(t1) + 180) % 360 - 180
        res.append((t1, math.degrees(t2)))
    return res

def valida(s, y):
    en_limites = all(TH_MIN[i] <= s[i] <= TH_MAX[i] for i in range(2))
    return en_limites and y >= Y_MIN and L1 * math.sin(math.radians(s[0])) >= Y_MIN

def elegir(x, y, ref):
    validas = [s for s in soluciones(x, y) if valida(s, y)]
    if not validas:
        return None
    return min(validas, key=lambda s: abs(s[0] - ref[0]) + abs(s[1] - ref[1]))

def planificar(th0, objetivo):
    x0, y0 = directa(th0)
    dx, dy = objetivo[0] - x0, objetivo[1] - y0
    largo = math.hypot(dx, dy)
    if largo < 0.05:
        return "sin movimiento", th0, 0.0, 0.0
    sols = soluciones(*objetivo)
    if not sols:
        return "rechazado: destino fuera de alcance", th0, 0.0, 0.0
    if not any(valida(q, objetivo[1]) for q in sols):
        motivo = "bajo el apoyo" if objetivo[1] < Y_MIN else "fuera de limites"
        return f"rechazado: destino {motivo}", th0, 0.0, 0.0
    n, ref = math.ceil(largo / 0.5), th0
    for k in range(1, n + 1):
        x, y = x0 + dx * k / n, y0 + dy * k / n
        s = elegir(x, y, ref)
        if s is None:
            return f"rechazado: la trayectoria sale de la zona en ({x:.1f}; {y:.1f})", th0, 0.0, 0.0
        if max(abs(s[0] - ref[0]), abs(s[1] - ref[1])) > 20:
            return "rechazado: salto de rama", th0, 0.0, 0.0
        ref = s
    s_rec = v = t = w_max = 0.0
    th = th0
    while s_rec < largo:
        v = max(min(v + A_CART * DT, V_CART, math.sqrt(2 * A_CART * (largo - s_rec))), V_MIN)
        s_rec = min(largo, s_rec + v * DT)
        t += DT
        nuevo = elegir(x0 + dx * s_rec / largo, y0 + dy * s_rec / largo, th)
        w = max(abs(nuevo[0] - th[0]), abs(nuevo[1] - th[1])) / DT
        w_max = max(w_max, w)
        if w > W_MAX:
            return f"abortado: {w:.0f} grados/s", th, t, w_max
        th = nuevo
    return "ok", th, t, w_max

th = HOME
print(f"Inicio en HOME: x={directa(th)[0]:.2f} y={directa(th)[1]:.2f}")
for objetivo in [(10, 10), (-6, 12), (14, 6), (20, 0), (0, 5), (8, -2), (10, 10)]:
    estado, nuevo, t, w = planificar(th, objetivo)
    if estado == "ok":
        th = nuevo
    print(f"M {objetivo}: {estado}; t={t:.2f} s; th1={th[0]:.1f} th2={th[1]:.1f}; w max={w:.1f} grados/s")

Desde HOME (5,66; 15,66): M 10 10 se acepta en 2,22 s con 28,5 °/s como máximo y deja θ1 = 78,4°, θ2 = −77,0°. M -6 12 se rechaza porque la recta sale de la zona en (7,6; 10,3). M 14 6 se acepta en 1,86 s. M 20 0, M 0 5 y M 8 -2 se rechazan por fuera de alcance, fuera de límites y bajo el apoyo. El último M 10 10 vuelve en 1,86 s.

Probá cambiar V_CART a 8: las duraciones bajan, pero la velocidad articular máxima sube y puede superar W_MAX. Probá TH_MAX[1] en 120 y mirá qué destinos nuevos aparecen. Los resultados que la simulación y el brazo deben compartir son los rechazos y la duración aproximada.

09 · Cerrar el proyecto

Pruebas de aceptación

Una prueba de aceptación compara el resultado con el criterio del requisito. Completá esta tabla con valores medidos; un proyecto sin mediciones es un prototipo.

Plan de pruebas.
PruebaProcedimientoCriterio
P1 · Exactitud (F1)Ordenar (10;10), (14;6) y (0;15); medir la posición del extremo con reglaError ≤ 5 mm en cada uno
P2 · Recta (F2)Fijar un lápiz y trazar M 14 6 → M 10 10 sobre papel; medir el desvío con una escuadraDesvío ≤ 5 mm
P3 · Tiempo (F3)Cronometrar 10 recorridos de 10 cm3,0 s ± 15 %
P4 · Rechazos (S1, S2)Enviar los seis casos de la sesión típicaMensaje correcto y brazo inmóvil
P5 · Parada (S3)Enviar S a mitad de un movimiento, a velocidad máximaFrena sin tirón; la distancia coincide con v²/2a
P6 · Inactividad (S4)Esperar 10 s tras llegarMensaje «Sin pulsos» y servos sin señal
P7 · Alimentación (C1)Movimientos repetidos de ida y vuelta con la carga previstaSin reinicios ni caída de tensión apreciable
P8 · Repetibilidad (C2)Ir y volver al mismo punto 10 veces; medir la dispersión≤ 3 mm

Si una prueba falla, no se ajusta el criterio: se encuentra la causa. Lo más frecuente es la calibración (P1, P2), la holgura mecánica (P8) o la alimentación (P7).

Registrá los resultados, la fecha y los valores de calibración usados. Cuando cambies un servo, un eslabón o la carga, repetí las pruebas afectadas.

10 · Cuando no sale

Diagnóstico de problemas

«El brazo llega a un punto distinto del pedido»

Probablemente calibración o longitudes. Medí L1 y L2 de nuevo, verificá el cero de cada articulación y los dos puntos de calibración. Revisá también que θ2 sea relativo al primer eslabón.

«Se mueve en espejo»

Un sentido está invertido: intercambiá el orden de los puntos de calibración del servo afectado.

«Tiembla o zumba al llegar»

Fuente insuficiente, holgura mecánica o carga que supera el torque. Medí la tensión bajo carga, revisá el montaje y reducí la aceleración.

«Se reinicia el Arduino al arrancar un movimiento»

La corriente de los servos hunde la alimentación. Revisá que no salga del pin de 5 V, la capacidad de la fuente, el capacitor y el estado de la masa común.

«Rechaza destinos que parecen válidos»

Verificá los límites, el apoyo y la zona: el hueco central es grande con θ2 a ±90°. Probá el mismo destino en la simulación para ver el motivo.

«El movimiento se aborta por velocidad articular»

La recta pasa cerca de un borde de la zona, donde la sensibilidad crece (tema 22). Reducí V_CART, acortá el recorrido o cambiá el destino.

«Después de un S, el siguiente M parte desde otro lado»

Es lo esperado: parte desde donde frenó. Si hace falta volver a un punto conocido, usá H.

«Con carga llega más abajo»

Flexión, holgura o torque insuficiente. El modelo es rígido y sin carga; compensarlo requiere medición o una mecánica más rígida.

11 · Práctica guiada

Predecí antes de comprobar

1. Calibración de dos puntos

El servo 1 se calibró con (0° → 600 µs) y (180° → 2400 µs). ¿Qué ancho de pulso corresponde a 45° y a 135°? ¿Cuántos µs por grado?

Ver solución

Pendiente: 1800/180 = 10 µs/°. A 45°: 600 + 450 = 1050 µs. A 135°: 600 + 1350 = 1950 µs.

2. Servo del codo

El servo 2 se calibró con (−90° → 700 µs) y (+90° → 2300 µs). ¿Qué pulso corresponde a θ2 = −45°?

Ver solución

Pendiente 1600/180 ≈ 8,89 µs/°. A −45°: 700 + 45·8,89 = 1100 µs.

3. Duración de un recorrido

Con V = 4 cm/s y A = 8 cm/s², ¿cuánto dura un recorrido recto de 10 cm?

Ver solución

v²/a = 2 cm < 10 cm: hay crucero. T = D/v + v/a = 2,5 + 0,5 = 3,0 s.

4. Recorrido corto

¿Y uno de 1,5 cm?

Ver solución

1,5 < 2: triangular. Pico √(8·1,5) ≈ 3,46 cm/s; T = 2·√(1,5/8) ≈ 0,87 s. No se llega a 4 cm/s.

5. Torque de diseño

Con eslabones de 0,05 y 0,04 kg, L1 = 10 cm y L2 = 8 cm, ¿qué torque exige en la base una carga de 0,05 kg con el brazo extendido y horizontal? ¿Con el factor 1,5?

Ver solución

9,81·(0,05·0,05 + 0,04·0,14 + 0,05·0,18) = 9,81·(0,0025 + 0,0056 + 0,009) ≈ 0,168 N·m. Con 1,5: ≈ 0,25 N·m (2,6 kgf·cm). Con una carga de 0,02 kg eran 0,17 N·m: la carga importa.

6. Un rechazo inesperado

El brazo está en (10; 10) y se ordena M −6 12. Ambos puntos son utilizables, pero la orden se rechaza. ¿Por qué?

Ver solución

La recta entre ellos pasa por puntos a menos de 12,8 cm de la base, dentro del hueco central creado por el límite de θ2. La comprobación previa lo detecta cerca de (7,6; 10,3). Un destino válido no garantiza un camino válido.

7. Error de calibración

El servo de la base tiene 30 µs de error de cero. ¿Qué error angular produce y cuánto se desplaza el extremo si está a 15 cm de la base?

Ver solución

30/10 = 3°. Desplazamiento: 15 cm · 3° · π/180 ≈ 0,79 cm = 7,9 mm, perpendicular a la línea base–extremo.

12 · Para seguir

Extensiones

  • Pinza o lápiz: un tercer servo sin cinemática propia, o una herramienta fija. Cambia el torque de diseño y la posición del extremo.
  • Secuencias: una lista de puntos recorridos con pausas, con la máquina de estados del tema 19.
  • Realimentación: servos con salida de posición o un potenciómetro en cada articulación, para pasar de lazo abierto a una verificación (tema 18).
  • Finales de carrera y detección de atascos para cumplir mejor los requisitos de seguridad.
  • Interfaz: un joystick o una pantalla para elegir el punto, en lugar del monitor serie.
  • Mayor zona de trabajo: mecanismos que amplíen el rango del codo, o eslabones de otra proporción.

Para cada extensión, escribí primero el requisito y su verificación; después, el código.

13 · Síntesis

Ideas para recordar

  • Un proyecto parte de requisitos verificables, no del código.
  • La arquitectura por capas (comandos, planificador, cinemática, validación, calibración, servos) permite probar y cambiar cada parte.
  • Toda orden pasa por validación de destino y de la recta completa, y se rechaza con su motivo.
  • El movimiento en recta usa el perfil trapezoidal sobre la distancia cartesiana y un límite de velocidad articular.
  • La calibración manda: 20 µs de error de cero pueden valer varios milímetros en el extremo.
  • La alimentación, la masa común y el interruptor de potencia son parte del diseño de seguridad.
  • Lazo abierto: las posiciones informadas son ordenadas, no medidas.
  • Se cierra con pruebas de aceptación medidas, registradas y repetibles.

¿Listo para el tema 25?

Antes de avanzar, completá la tabla de pruebas de aceptación con tus mediciones y anotá qué requisito no se cumplió y qué harías para cumplirlo. Pensá qué parte del código cambiaría si el robot en lugar de un brazo fuera un vehículo con ruedas.

En el próximo tema cambiamos de familia de robots: configuraciones de robots móviles, con diferencial, triciclo, Ackermann, omnidireccional y orugas.