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.
| Incluye | No incluye |
|---|---|
| Cinemática inversa con elección de codo por continuidad | Medición de la posición real (lazo abierto) |
| Validación de alcance, límites, apoyo y trayectoria | Control de la orientación de la herramienta |
| Movimiento en recta con perfil trapezoidal | Compensación de carga, holgura o flexión |
| Calibración con dos puntos por servo | Parada de emergencia por software |
| Soltado automático de los servos tras inactividad | Detecció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:
| Id | Requisito | Verificación |
|---|---|---|
| F1 | Llevar el extremo a un punto (x, y) de la zona utilizable | Medir con regla en tres puntos; error ≤ 5 mm |
| F2 | El extremo recorre una recta entre el punto actual y el destino | Dibujar con un lápiz o comparar con una escuadra; desvío ≤ 5 mm |
| F3 | Velocidad cartesiana limitada a 4 cm/s y aceleración a 8 cm/s² | Cronometrar un recorrido de 10 cm: unos 3,0 s |
| S1 | Rechazar con motivo todo destino fuera de alcance, límites o apoyo | Probar un caso de cada tipo |
| S2 | Rechazar una recta cuyo recorrido salga de la zona | Probar un destino que cruza el hueco central |
| S3 | Frenar con la aceleración planificada ante el comando S | Enviar S durante un movimiento y observar |
| S4 | Dejar de generar pulsos tras 10 s sin órdenes | Esperar y verificar el mensaje |
| C1 | No reiniciarse ni tener caídas de tensión en movimiento | Observar el Arduino y la fuente con carga |
| C2 | Repetir un punto con ≤ 3 mm de dispersión | Ir 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 cmyL2 = 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).
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:
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.
- Medí las longitudes
L1yL2entre los centros de los ejes de giro, con una regla o un calibre. Actualizalas en el código. - 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. - 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_THyCAL_US. - Definí la ventana segura
US_LIMalgo dentro de los extremos medidos, para no empujar contra los topes del servo (tema 15). - 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).
- 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()yescribir(): calibración y servos (temas 15 y 21). La ventanaUS_LIMse pasa aattach().soluciones()yelegir(): cinemática inversa y elección de rama por continuidad (tema 22).valida()ymotivoDestino(): 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),dtmedido (temas 16 y 18) y límite de velocidad articular (tema 16).leerSerie()yloop(): 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.
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.
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.
| Prueba | Procedimiento | Criterio |
|---|---|---|
| P1 · Exactitud (F1) | Ordenar (10;10), (14;6) y (0;15); medir la posición del extremo con regla | Error ≤ 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 escuadra | Desvío ≤ 5 mm |
| P3 · Tiempo (F3) | Cronometrar 10 recorridos de 10 cm | 3,0 s ± 15 % |
| P4 · Rechazos (S1, S2) | Enviar los seis casos de la sesión típica | Mensaje correcto y brazo inmóvil |
| P5 · Parada (S3) | Enviar S a mitad de un movimiento, a velocidad máxima | Frena sin tirón; la distancia coincide con v²/2a |
| P6 · Inactividad (S4) | Esperar 10 s tras llegar | Mensaje «Sin pulsos» y servos sin señal |
| P7 · Alimentación (C1) | Movimientos repetidos de ida y vuelta con la carga prevista | Sin 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.