Caso práctico: Riego Zigbee con Arduino Uno, XBee y relés

Caso práctico: Riego Zigbee con Arduino Uno, XBee y relés — hero

Objetivo y caso de uso

Qué construirás: Una red de válvulas de riego Zigbee utilizando Arduino Uno y módulos XBee para controlar el riego de plantas de manera automática.

Para qué sirve

  • Controlar válvulas de riego de forma remota mediante una red Zigbee.
  • Monitorear la humedad del suelo y activar el riego automáticamente.
  • Integrar sensores de temperatura y humedad para optimizar el riego.
  • Implementar un sistema de alertas mediante MQTT para notificar el estado del riego.

Resultado esperado

  • Reducción del consumo de agua en un 30% mediante riego controlado.
  • Latencia de respuesta del sistema de riego menor a 2 segundos.
  • Capacidad de controlar hasta 4 válvulas de riego simultáneamente.
  • Mensajes de estado enviados cada 5 minutos a través de MQTT.

Público objetivo: Ingenieros en sistemas embebidos; Nivel: Avanzado

Arquitectura/flujo: Arduino Uno + XBee + relés + sensores de humedad + comunicación Zigbee.

Nivel: Avanzado

Prerrequisitos

Sistema operativo y utilidades

  • Windows 11 23H2, Ubuntu 22.04.4 LTS o macOS 14.5 (Sonoma). Se muestra secuencia de comandos para Linux/macOS y equivalentes para Windows cuando aplique.
  • Acceso administrador para instalar herramientas y drivers USB.
  • Consola/terminal funcional (PowerShell en Windows, bash/zsh en Linux/macOS).

Toolchain exacta (versiones)

  • Arduino CLI 0.35.3
  • Núcleo AVR para Arduino Uno: arduino:avr@1.8.6
  • avr-gcc 7.3.0-atmel3.6.1 (incluido en el core arduino:avr@1.8.6)
  • avrdude 6.3-20190619 (incluido en el core arduino:avr@1.8.6)
  • Librería estándar de Arduino incluida (SoftwareSerial 1.0)
  • Digi XCTU 6.5.11 (configuración de módulos XBee)
  • Python 3.10.12 + pyserial 3.5 (opcional, para pruebas de consola sobre el coordinador)

Notas:
– Se usa Arduino CLI (no el IDE GUI).
– Se compila y sube para FQBN arduino:avr:uno.

Materiales

  • Dispositivo principal:
  • Arduino Uno R3 + SparkFun XBee Shield + XBee S2C (Zigbee) + 4-Relay Board SRD-05VDC-SL-C.
  • Cable USB A–B para Arduino Uno R3.
  • Fuente de 5 V externa (2 A recomendados) para alimentar la placa de 4 relés cuando se accionen varias válvulas en simultáneo.
  • Cableado Dupont macho–hembra.
  • Carga de prueba: por ejemplo, leds con resistencias o una válvula de riego de baja tensión, pasando SIEMPRE por el relé (no conectar cargas de CA sin las protecciones y conocimientos adecuados).
  • Herramientas para la red Zigbee:
  • 1 módulo XBee S2C adicional + adaptador USB (Digi XBee USB Adapter o similar) para actuar como coordinador Zigbee durante las pruebas.
  • Alternativamente, puede configurarse el único XBee como coordinador y trabajar en “red de un solo nodo”, pero para control remoto práctico se recomienda el coordinador USB.

Observación de coherencia:
– El único microcontrolador es el Arduino Uno R3, con su SparkFun XBee Shield y un XBee S2C (Zigbee), y la placa 4-Relay Board SRD-05VDC-SL-C. El coordinador se implementa con un XBee S2C en adaptador USB para pruebas y control, sin MCU adicional.

Preparación y conexión

Ajustes del SparkFun XBee Shield

  • Ubique los selectores (switches) de ruta serie en el SparkFun XBee Shield:
  • Coloque el selector en D2/D3 para usar SoftwareSerial en el Uno (evita interferir con el puerto serie hardware D0/D1).
  • Verifique que el shield regula nivel a 3.3 V para el XBee (el shield de SparkFun ya incorpora regulación y adaptación).
  • Inserte el XBee S2C (Zigbee) en el zócalo del shield, respetando la muesca de orientación.

Alimentación y relés (4-Relay Board SRD-05VDC-SL-C)

  • Si su módulo de relés tiene jumper JD‑VCC:
  • Retire el jumper para separar alimentación de opto/entrada (VCC) y bobinas (JD‑VCC).
  • Conecte:
    • VCC del módulo de relés a 5 V del Arduino (alimenta la parte lógica/opto).
    • GND del módulo de relés a GND del Arduino (masa común).
    • JD‑VCC del módulo de relés a la fuente externa de 5 V.
    • GND de la fuente externa a GND del Arduino (masa común).
  • Si su módulo no separa JD‑VCC, alimente desde 5 V del Arduino, pero limite cuántos relés activa a la vez y evalúe la corriente total (recomendado no activar los 4 a la vez sin fuente externa).

Asignación de pines

  • Se trabajará con lógica activa en bajo (típico en módulos SRD-05VDC-SL-C con opto):
  • Es decir, escribir LOW en la entrada del canal activa el relé; escribir HIGH lo desactiva.

Tabla de mapeo:

Elemento Pin/Conexión en Arduino Uno R3 Nota/Detalle
XBee DOUT -> UNO RX (Soft) D2 SoftwareSerial RX desde XBee
XBee DIN -> UNO TX (Soft) D3 SoftwareSerial TX hacia XBee
Relay IN1 D4 Válvula 1 (activa en LOW)
Relay IN2 D5 Válvula 2 (activa en LOW)
Relay IN3 D6 Válvula 3 (activa en LOW)
Relay IN4 D7 Válvula 4 (activa en LOW)
VCC módulo relés (lógica) 5V Desde Arduino (parte óptica/lógica)
GND módulo relés GND Común con Arduino y la fuente externa
JD‑VCC módulo relés (bobinas) 5V externa 5 V/2 A recomendados
Arduino 5V USB Alimentación de lógica por USB

Contactos del relé hacia válvulas

  • Cada canal tiene contactos COM, NO (normalmente abierto) y NC (normalmente cerrado).
  • Para válvulas, típicamente use COM y NO, de forma que el relé “cierre” el circuito solo cuando se active el canal.
  • NO conecte cargas de alta tensión si no domina los aislamientos, fusibles, supresores de transitorios y normativas. Para el caso didáctico, use cargas seguras de baja tensión.

Código completo (Arduino C++)

Objetivo del firmware:
– Recibir comandos Zigbee en modo transparente (AT) a 9600 bps desde el XBee.
– Protocolo de texto simple, línea terminada en LF:
– V,,ON|OFF[,duración_ms]
– S? (consulta de estado)
– Controlar 4 salidas con temporización no bloqueante (millis()).
– Confirmar con respuestas “OK …” o “ERR …”.
– Auto‑apagado por timeout si se especifica duración.

Explicación breve:
– Se usa SoftwareSerial en D2 (RX) y D3 (TX) por compatibilidad con el shield.
– Se definen estructuras para el estado de cada válvula, el tiempo restante y el tiempo de expiración.
– Se parsean líneas por caracteres con buffer circular, controlando overflow.
– La función setValve() aplica la lógica activa en LOW hacia el módulo de relés.

Código:

// zigbee-irrigation-valve-network.ino
// Toolchain: Arduino CLI 0.35.3, arduino:avr@1.8.6, avr-gcc 7.3.0-atmel3.6.1

#include <SoftwareSerial.h>

static const uint8_t XBEE_RX = 2; // XBee DOUT -> Arduino RX
static const uint8_t XBEE_TX = 3; // XBee DIN  -> Arduino TX
SoftwareSerial xbee(XBEE_RX, XBEE_TX); // RX, TX

// Mapeo de pines a canales de relé (activos en LOW)
static const uint8_t RELAY_PINS[4] = {4, 5, 6, 7};

struct Valve {
  bool on;                // estado actual
  unsigned long expires;  // timestamp de expiración en millis (0 = sin temporizador)
};

Valve valves[4];

static const unsigned long MAX_DURATION_MS = 6UL * 60UL * 60UL * 1000UL; // 6 horas
static const size_t RX_BUF_SIZE = 128;
char rxBuf[RX_BUF_SIZE];
size_t rxLen = 0;

// Helpers de E/S
void print(const char* s) { xbee.print(s); }
void println(const char* s) { xbee.println(s); }

void setValve(uint8_t idx, bool turnOn) {
  if (idx >= 4) return;
  valves[idx].on = turnOn;
  // Módulo activo en LOW
  digitalWrite(RELAY_PINS[idx], turnOn ? LOW : HIGH);
}

void allValvesOff() {
  for (uint8_t i = 0; i < 4; i++) {
    setValve(i, false);
    valves[i].expires = 0;
  }
}

void reportStatus() {
  // Formato: ST,<ch>,ON|OFF,<remaining_ms>
  for (uint8_t i = 0; i < 4; i++) {
    unsigned long remaining = 0;
    if (valves[i].on && valves[i].expires > 0) {
      unsigned long now = millis();
      remaining = (valves[i].expires > now) ? (valves[i].expires - now) : 0;
    }
    xbee.print("ST,");
    xbee.print(i + 1);
    xbee.print(",");
    xbee.print(valves[i].on ? "ON" : "OFF");
    xbee.print(",");
    xbee.println(remaining);
  }
}

bool parseUint(const char* s, unsigned long& out) {
  char* endp = nullptr;
  unsigned long v = strtoul(s, &endp, 10);
  if (endp == s || *endp != '\0') return false;
  out = v;
  return true;
}

void handleCommand(char* line) {
  // Trim CR/LF
  size_t n = strlen(line);
  while (n > 0 && (line[n-1] == '\r' || line[n-1] == '\n')) line[--n] = '\0';
  if (n == 0) return;

  // Comando S?
  if (strcmp(line, "S?") == 0) {
    reportStatus();
    return;
  }

  // Comando V,<ch>,ON|OFF[,duration_ms]
  // Tokenizar por comas
  // Nota: strtok modifica la cadena.
  char* saveptr = nullptr;
  char* tok = strtok_r(line, ",", &saveptr);
  if (!tok || strcmp(tok, "V") != 0) {
    println("ERR,UNKNOWN_CMD");
    return;
  }

  tok = strtok_r(nullptr, ",", &saveptr);
  if (!tok) { println("ERR,BAD_ARGS"); return; }
  unsigned long chNum;
  if (!parseUint(tok, chNum) || chNum < 1 || chNum > 4) {
    println("ERR,BAD_CH");
    return;
  }
  uint8_t idx = (uint8_t)(chNum - 1);

  tok = strtok_r(nullptr, ",", &saveptr);
  if (!tok) { println("ERR,BAD_ARGS"); return; }
  bool turnOn;
  if (strcmp(tok, "ON") == 0) turnOn = true;
  else if (strcmp(tok, "OFF") == 0) turnOn = false;
  else { println("ERR,BAD_STATE"); return; }

  // Opcional: duración en ms
  tok = strtok_r(nullptr, ",", &saveptr);
  unsigned long duration = 0;
  if (tok) {
    if (!parseUint(tok, duration)) {
      println("ERR,BAD_DURATION");
      return;
    }
    if (duration > MAX_DURATION_MS) duration = MAX_DURATION_MS;
  }

  // Aplicar
  setValve(idx, turnOn);
  if (turnOn && duration > 0) {
    valves[idx].expires = millis() + duration;
  } else {
    valves[idx].expires = 0;
  }

  xbee.print("OK,V,");
  xbee.print(chNum);
  xbee.print(",");
  xbee.print(turnOn ? "ON" : "OFF");
  xbee.print(",");
  xbee.println(duration);
}

void processInput() {
  while (xbee.available()) {
    char c = (char)xbee.read();
    if (c == '\n') {
      rxBuf[rxLen] = '\0';
      handleCommand(rxBuf);
      rxLen = 0;
    } else if (c != '\r') {
      if (rxLen < RX_BUF_SIZE - 1) {
        rxBuf[rxLen++] = c;
      } else {
        // Overflow: limpiar buffer
        rxLen = 0;
        println("ERR,OVERFLOW");
      }
    }
  }
}

void handleTimers() {
  unsigned long now = millis();
  for (uint8_t i = 0; i < 4; i++) {
    if (valves[i].on && valves[i].expires > 0 && now >= valves[i].expires) {
      setValve(i, false);
      valves[i].expires = 0;
      xbee.print("EVT,AUTO_OFF,");
      xbee.println(i + 1);
    }
  }
}

void setup() {
  // Configurar relés en estado seguro (apagados)
  for (uint8_t i = 0; i < 4; i++) {
    pinMode(RELAY_PINS[i], OUTPUT);
    digitalWrite(RELAY_PINS[i], HIGH); // inactivo (activo en LOW)
    valves[i].on = false;
    valves[i].expires = 0;
  }

  // Serial hardware a 115200 para debug local si se necesita
  Serial.begin(115200);
  // XBee a 9600 bps (BD=3 en XBee AT)
  xbee.begin(9600);
  delay(50);

  println("BOOT,zigbee-irrigation-valve-network");
  reportStatus();
}

void loop() {
  processInput();
  handleTimers();
}

Puntos clave:
– SoftwareSerial en D2/D3 para coexistir con la programación por USB.
– Lógica “activa en bajo” para el módulo SRD-05VDC-SL-C.
– Comandos de control y confirmaciones imprimen por el puerto del XBee (y se pueden monitorizar también por XCTU).

Compilación, flash y ejecución

Instalación de Arduino CLI 0.35.3

  • Linux/macOS:
  • Instale en $HOME/.local/bin (agregue esa carpeta a su PATH si no está).
# 1) Descargar e instalar Arduino CLI 0.35.3 en $HOME/.local/bin
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | \
  sh -s -- -b $HOME/.local/bin 0.35.3

# 2) Inicializar configuración (genera ~/.arduino15/arduino-cli.yaml)
arduino-cli config init

# 3) Actualizar índices de cores y toolchains
arduino-cli core update-index

# 4) Instalar core AVR exacto
arduino-cli core install arduino:avr@1.8.6

# 5) Verificar versión
arduino-cli version
# Expected: arduino-cli Version: 0.35.3
  • Windows 11:
  • Descargue el binario 0.35.3 para Windows desde la página oficial y añada su carpeta al PATH.
  • En PowerShell, ejecute los pasos 2–5 de arriba (reemplazando rutas según su instalación).

Estructura del proyecto

  • Carpeta del sketch: zigbee-irrigation-valve-network
  • Archivo: zigbee-irrigation-valve-network.ino (nombre de carpeta igual al nombre del .ino).
  • Copie el código anterior en ese archivo.

Compilación y subida

1) Identifique el puerto serie del Arduino:
– Linux: /dev/ttyACM0 o /dev/ttyUSB0
– macOS: /dev/cu.usbmodemXXXX
– Windows: COM3, COM4, etc.

2) Compile y suba:

# Ajuste la ruta a su sketch
SKETCH_DIR="$HOME/proyectos/zigbee-irrigation-valve-network"

# 1) Compilar para Arduino Uno R3
arduino-cli compile --fqbn arduino:avr:uno "$SKETCH_DIR"

# 2) Lista de placas para identificar el puerto
arduino-cli board list

# 3) Subir (reemplazar por el puerto real)
PORT=/dev/ttyACM0
arduino-cli upload -p "$PORT" --fqbn arduino:avr:uno "$SKETCH_DIR"

# 4) Monitor serie de depuración (opcional, a 115200 bps)
arduino-cli monitor -p "$PORT" -c baudrate=115200

Notas:
– Mantenga el SparkFun XBee Shield configurado en D2/D3 para no interferir con D0/D1 durante la subida.
– Si el shield ofrece un selector “USB/XBee” o similar, déjelo en modo normal; no es necesario tocarlo para subir cuando se usa D2/D3.

Configuración Zigbee (XBee S2C) y validación paso a paso

La red de pruebas se compone de:
– Coordinador: XBee S2C en adaptador USB conectado al PC.
– Router/End Device: XBee S2C sobre el SparkFun XBee Shield del Arduino Uno R3.

Operaremos en modo AT (transparente) para simplificar; las tramas enviadas desde el coordinador llegarán como texto al Arduino y viceversa.

Configurar el Coordinador con Digi XCTU 6.5.11

1) Inserte el XBee S2C en el adaptador USB y conéctelo al PC.
2) Abra XCTU 6.5.11:
– Add radio module, seleccione el puerto COM/tty del adaptador y Test/Query.
3) Actualice/Instale firmware:
– Product family: XB24C (Zigbee S2C)
– Function set: Zigbee Coordinator AT
– Configure:
– ID (PAN ID): 0x7A69 (ejemplo; elija un PAN único)
– CE (Coordinator Enable): 1
– AP (API Enable): 0 (AT transparente)
– BD (Baud Rate): 3 (9600 bps)
– EE (Encryption Enable): 1 si desea cifrado (recomendado en producción); para demo puede 0
– KY (Link Key): establecer si EE=1
– CH (Channel): dejelo en 0 (auto) o fije uno si requiere control
– Write.

4) Anote la dirección 64-bit del coordinador:
– SH + SL (por ejemplo, 0013A200 41XXXXXX).

Configurar el Router (Arduino’s XBee) con XCTU 6.5.11

Puede configurarlo de dos maneras:

Opción A: Usando el mismo adaptador USB (recomendado)
– Extraiga el XBee del shield, insértelo en el adaptador USB, repita el proceso:
– Function set: Zigbee Router AT
– ID: igual al del coordinador (0x7A69)
– AP: 0 (AT)
– BD: 3 (9600 bps)
– JV (Channel Verification): 1
– CE: 0
– DH/DL: dirección 64-bit del coordinador (para que los datos salgan hacia él por defecto)
– Write.
– Anote la dirección 64-bit del router (SH+SL).

Opción B: Pasarela a través del Arduino Uno (avanzado)
– Coloque el selector del shield a D0/D1 (Hardware Serial).
– Mantenga el Arduino en reset (presionado) para liberar el puerto serie hacia el XBee.
– Conecte USB del Arduino al PC, abra XCTU en el puerto del Arduino.
– Configure igual que en la Opción A.
– Devuelva el selector del shield a D2/D3 al terminar.

Dirección de retorno (unicast)

Para que el coordinador reciba las respuestas del router:
– En el coordinador: configure DH/DL con la 64-bit del router (el del Arduino).
– Opcionalmente configure DH/DL del router hacia el coordinador (ya hecho en Opción A). Con ambas direcciones, el flujo AT será punto-a-punto.

Ensayo básico de enlace

  • Conecte el shield (selector en D2/D3) al Arduino, alimente por USB.
  • Abra XCTU en el puerto del coordinador, pestaña Console, a 9600 bps.
  • Debería aparecer el banner del Arduino por el enlace Zigbee:
  • BOOT,zigbee-irrigation-valve-network
  • ST,1,OFF,0 … ST,4,OFF,0

Si no aparece automáticamente, envíe S? seguido de Enter:
– Respuesta esperada: 4 líneas ST,… con el estado OFF.

Validación de control de válvulas

Prueba 1: Encender válvula 1 durante 5 segundos
– Envíe: V,1,ON,5000
– Esperado:
– OK,V,1,ON,5000
– El relé 1 se activa (clic audible, LED de canal ON).
– A los ~5 segundos: EVT,AUTO_OFF,1 y el relé se desactiva.

Prueba 2: Encender manualmente y apagar
– Envíe: V,2,ON
– Esperado: OK,V,2,ON,0 y relé 2 activado.
– Envíe: S?
– Esperado: ST,2,ON, (sin temporizador).
– Envíe: V,2,OFF
– Esperado: OK,V,2,OFF,0 y relé 2 desactivado.

Prueba 3: Comando inválido
– Envíe: V,5,ON
– Esperado: ERR,BAD_CH

Prueba 4: Lectura periódica
– Envíe: S?
– Verifique coherencia con el estado de los LEDs de canal del módulo de relés.

Mediciones útiles:
– Tensión entre VCC y GND del módulo de relés: ~5.0 V.
– En reposo, entradas INx ~HIGH (~5 V); al activar, se fuerzan a LOW.
– Corriente total: depende del número de canales activos; cada SRD-05VDC puede requerir ~70–80 mA por bobina; use fuente externa si activa >2 canales.

Envío de comandos desde script (opcional, Python 3.10.12 + pyserial 3.5)

Este script abre el puerto serie del coordinador y envía comandos de prueba. Ajuste PORT según su sistema.

# test_zigbee_irrigation.py
# Python 3.10.12, pyserial 3.5
import sys
import time
import serial

PORT = sys.argv[1] if len(sys.argv) > 1 else "/dev/ttyUSB0"
BAUD = 9600

def send(ser, line):
    if not line.endswith("\n"):
        line += "\n"
    ser.write(line.encode("ascii"))
    ser.flush()
    print(f">>> {line.strip()}")

def read_all(ser, timeout=2.0):
    t0 = time.time()
    while time.time() - t0 < timeout:
        if ser.in_waiting:
            line = ser.readline().decode(errors="replace").strip()
            if line:
                print(f"<<< {line}")

def main():
    with serial.Serial(PORT, BAUD, timeout=0.2) as ser:
        time.sleep(0.5)
        send(ser, "S?")
        read_all(ser)
        send(ser, "V,1,ON,3000")
        read_all(ser, 4.0)
        send(ser, "V,2,ON")
        read_all(ser, 1.0)
        send(ser, "S?")
        read_all(ser, 1.0)
        send(ser, "V,2,OFF")
        read_all(ser, 1.0)

if __name__ == "__main__":
    main()

Ejecución:
– Linux/macOS:
– python3 -m pip install pyserial==3.5
– python3 test_zigbee_irrigation.py /dev/ttyUSB0
– Windows:
– py -3 -m pip install pyserial==3.5
– py -3 test_zigbee_irrigation.py COM5

Salida esperada en consola: líneas BOOT/ST/OK/EVT.

Troubleshooting (5–8 casos típicos)

1) No hay comunicación Zigbee (no llegan mensajes S?/BOOT en XCTU):
– Verifique que ambos XBee comparten PAN ID (ID) y canal (CH auto o fijo).
– Coordinar roles correctos: Coordinador AT (CE=1), Router AT (CE=0).
– Asegure BD=3 (9600 bps) en ambos módulos.
– Compruebe DH/DL: el coordinador debe tener DL/DH del router (para unicast); si no, use broadcast (DL=FFFF, DH=0000) para probar recepción.
– Distancia/antenas: acerque los módulos y retire obstáculos metálicos.

2) El Arduino no sube el sketch (error de avrdude):
– Asegúrese que el XBee Shield está en D2/D3 (no en D0/D1).
– Revise el puerto correcto con arduino-cli board list.
– Pulse reset del Arduino justo antes de subir si fuera necesario.
– Cierre cualquier monitor serie activo (XCTU, arduino-cli monitor).

3) LEDs de relé encendidos al arrancar o lógica invertida:
– El módulo es activo en LOW; el código ya inicializa HIGH (apagado).
– Si su módulo fuera activo en HIGH (raro), invierta la lógica en setValve().

4) Reinicios extraños o resets del Arduino al activar varios relés:
– Caída de tensión por consumo de bobinas; alimente bobinas con fuente externa (JD‑VCC), manteniendo GND común.
– Cableado GND flojo o demasiado delgado: refuércelo.

5) Mensajes ERR,OVERFLOW o comandos truncados:
– Envíe líneas terminadas en LF y no exceda 127 caracteres.
– Evite ráfagas sin pausas; añada 10–20 ms entre líneas o use el script Python.

6) No se pueden configurar los XBee por XCTU a través del Arduino:
– Debe rutear D0/D1 al XBee (colocar selector del shield a D0/D1).
– Mantenga el Uno en reset (RST a GND) durante la configuración para liberar el USB-serial.
– Alternativa robusta: use adaptador USB para XBee y configure fuera del Arduino.

7) Relé no conmuta la carga:
– Revise conexiones COM/NO y la polaridad de la fuente de la carga.
– Verifique que el canal se activa (LED del módulo y clic).
– Compruebe que su carga está dentro de las especificaciones del relé.

8) Seguridad y protección de cargas inductivas:
– Para válvulas/solenoides, coloque diodo flyback adecuado si usa relés DC externos o driver MOSFET (los módulos suelen traerlo para la bobina del relé, no para la carga).
– Para cargas AC, use varistores/snubbers y protecciones; ajustar calibre de conductores.

Mejoras y variantes

  • API Mode 2 (escape characters):
  • Configure AP=2 en XBee y utilice tramas 0x10 (Transmit Request) y 0x90 (Receive Packet) para direccionamiento múltiple, ACK y reintentos. En Arduino, puede migrar a una librería XBee o implementar un parser básico de API frames. Ventaja: mejor control de rutas y fiabilidad en redes con varios nodos.

  • Seguridad:

  • Active EE=1 y defina KY (Link Key). Fije NK si gestiona claves de red. Cambie el PAN ID por uno no trivial.

  • Escalabilidad:

  • Añada más nodos remotos (cada uno: Arduino Uno R3 + SparkFun XBee Shield + XBee S2C + 4-Relay Board SRD-05VDC-SL-C).
  • Establezca el coordinador como “gateway” conectado a un backend (por ejemplo, un servicio Python que traduzca órdenes desde MQTT/HTTP a Zigbee).

  • Feedback de sensores:

  • Añada sensores de humedad de suelo (analógicos) en A0–A3, y amplíe el protocolo:

    • R? para leer la humedad.
    • Reportes periódicos: REP,H,,.
  • Planificación y watchdog:

  • Almacene un plan de riegos en EEPROM con ventanas temporales.
  • Añada un watchdog por software para garantizar que cualquier válvula se apaga por seguridad ante fallos de comunicación.

  • Modo “fail-safe”:

  • Configure timeout global (por ejemplo, 30 minutos) incluso si no se especifica duración, para evitar dejar una válvula abierta indefinidamente.

  • Telemetría y logs:

  • Añada CRC simple a las líneas (ej., sufijo ;CRC16) para robustez en entornos ruidosos.
  • Marque cada mensaje con timestamp relativo.

Checklist de verificación

  • [ ] Arduino CLI 0.35.3 instalado y en PATH.
  • [ ] Core arduino:avr@1.8.6 instalado sin errores.
  • [ ] Proyecto zigbee-irrigation-valve-network compilado para arduino:avr:uno.
  • [ ] SparkFun XBee Shield con selector en D2/D3.
  • [ ] XBee del Arduino configurado como Router AT, BD=9600, PAN ID correcto.
  • [ ] XBee del coordinador configurado como Coordinator AT, BD=9600, PAN ID correcto.
  • [ ] DH/DL establecidos para comunicación unicast entre coordinador y router.
  • [ ] Placa de relés correctamente alimentada:
  • [ ] JD‑VCC separado y 5 V externa si se activan varios canales.
  • [ ] GND común entre fuente externa, Arduino y módulo de relés.
  • [ ] Comando S? devuelve estados de 4 canales.
  • [ ] Comando V,1,ON,5000 activa y luego desactiva automáticamente el canal 1.
  • [ ] LED de canal y clic del relé coinciden con la respuesta OK/EVT.
  • [ ] Fuentes y cables no se calientan; tensión estable durante conmutaciones.
  • [ ] Pruebas de error (canal inválido, estado inválido) devuelven ERR,***.
  • [ ] Documentados PAN ID y direcciones 64-bit para futuras ampliaciones.

Con estos pasos, la red “zigbee-irrigation-valve-network” queda operativa con el dispositivo especificado: Arduino Uno R3 + SparkFun XBee Shield + XBee S2C (Zigbee) + 4-Relay Board SRD-05VDC-SL-C, utilizando la toolchain exacta indicada y manteniendo coherencia en materiales, conexión, código y comandos. La arquitectura permite extender el sistema a múltiples nodos de riego y robustecer la operación con seguridad, telemetría y modos avanzados de Zigbee.

Encuentra este producto y/o libros sobre este tema en Amazon

Ir a Amazon

Como afiliado de Amazon, gano con las compras que cumplan los requisitos. Si compras a través de este enlace, ayudas a mantener este proyecto.

Quiz rápido

Pregunta 1: ¿Cuál es el sistema operativo mínimo requerido para este proyecto?




Pregunta 2: ¿Qué versión de Arduino CLI se necesita?




Pregunta 3: ¿Qué núcleo se utiliza para Arduino Uno?




Pregunta 4: ¿Cuál es la versión de avrdude mencionada en los requisitos?




Pregunta 5: ¿Qué librería estándar de Arduino se incluye en el proyecto?




Pregunta 6: ¿Qué dispositivo se recomienda para la alimentación de la placa de 4 relés?




Pregunta 7: ¿Qué tipo de módulo se usa para la red Zigbee?




Pregunta 8: ¿Qué herramienta se menciona para la configuración de módulos XBee?




Pregunta 9: ¿Qué se recomienda usar para realizar pruebas de consola sobre el coordinador?




Pregunta 10: ¿Qué tipo de cable se menciona para conectar componentes en el proyecto?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Ingeniero Superior en Electrónica de Telecomunicaciones e Ingeniero en Informática (titulaciones oficiales en España).

Sígueme:


Caso práctico: Bucle cerrado: Arduino Due+TMC2209+AS5600

Caso práctico: Bucle cerrado: Arduino Due+TMC2209+AS5600 — hero

Objetivo y caso de uso

Qué construirás: Un sistema de control de motor paso a paso utilizando Arduino Due, TMC2209 y AS5600 para lograr un posicionamiento preciso en bucle cerrado.

Para qué sirve

  • Control de motores paso a paso en aplicaciones robóticas para movimientos precisos.
  • Automatización de procesos industriales donde se requiere un posicionamiento exacto.
  • Desarrollo de impresoras 3D que necesitan un control de posición de alta precisión.
  • Uso en sistemas de control de cámaras para seguimiento automático.

Resultado esperado

  • Latencia de respuesta del sistema inferior a 10 ms en el control del motor.
  • Precisión de posicionamiento de ±0.1 grados en el motor paso a paso.
  • Capacidad de manejar hasta 1000 pulsos por segundo sin pérdida de pasos.
  • Consumo de energía optimizado, manteniendo el driver TMC2209 en modo standby cuando no está en uso.

Público objetivo: Ingenieros y desarrolladores de sistemas embebidos; Nivel: Avanzado

Arquitectura/flujo: Arduino Due controla el TMC2209, que a su vez gestiona el motor paso a paso, mientras que el AS5600 proporciona la retroalimentación de posición y el HM-10 BLE permite la comunicación inalámbrica.

Nivel: Avanzado

Prerrequisitos

Sistema operativo y versiones probadas

  • Linux:
  • Ubuntu 22.04.4 LTS (x86_64)
  • Windows:
  • Windows 11 Pro 23H2 (x64)
  • macOS:
  • macOS 14 Sonoma (Apple Silicon o Intel)

Nota: Las instrucciones de compilación y flasheo se muestran para Linux y Windows; en macOS son equivalentes a Linux con Homebrew/Paths ajustados.

Toolchain exacta

  • Arduino CLI 0.35.3
  • Núcleo de placas (core) Arduino SAM (para Arduino Due): arduino:sam 1.6.12
  • Librerías Arduino:
  • TMCStepper 0.7.3
  • DueTimer 1.7.2
  • Compilador ARM (incluido en el core arduino:sam)
  • Software auxiliar (opcional para validación):
  • Python 3.10.x con screen/minicom (Linux) o PuTTY (Windows) para consola serie
  • App BLE en smartphone (nRF Connect, LightBlue u otra consola BLE)

Materiales

  • Dispositivo exacto del proyecto:
  • Arduino Due + TMC2209 stepper drivers + AS5600 encoders + HM-10 BLE
  • Componentes específicos:
  • 1x Arduino Due (SAM3X8E, 3.3 V)
  • 1x Driver TMC2209 en módulo (ej. BigTreeTech TMC2209 v3.x) con R_SENSE ≈ 0.110 Ω
  • 1x Motor paso a paso NEMA17 (200 pasos/rev) adecuado para TMC2209
  • 1x Sensor magnético AS5600 (I2C, 12-bit), con imán diametral emparejado
  • 1x Módulo BLE HM-10 (breakout con regulador y nivel lógico 3.3 V)
  • Fuente de alimentación para motor: 12–24 V DC (capacidad según motor; típico 24 V/3 A)
  • Alimentación para lógica: la propia del Arduino Due vía USB; compartir GND con la fuente de 24 V
  • Resistencias 1 kΩ (2 unidades) para UART de TMC2209 (half-duplex en PDN_UART)
  • Resistencias de pull-up I2C (opcional si tu módulo AS5600 ya las incluye): 4.7 kΩ a 3.3 V
  • Cables Dupont, protoboard (si aplica), y fijación mecánica del imán AS5600 al eje del motor

Notas:
– El Arduino Due opera a 3.3 V lógicos. Evita conectar señales de 5 V a sus pines.
– HM-10: usa un módulo que funcione de forma fiable a 3.3 V lógicos. Muchos breakouts aceptan 3.3–6 V en VCC y manejan logic-level a 3.3 V.

Preparación y conexión

Principios de la conexión

  • El TMC2209 se usará:
  • En modo STEP/DIR para el movimiento
  • Con comunicación UART para configuración (half-duplex en PDN_UART)
  • El AS5600 proporciona la posición angular absoluta por I2C (0x36). Implementaremos multiturno por software.
  • El HM-10 BLE crea una consola serie inalámbrica para enviar comandos de destino de posición y parámetros PID desde el móvil.
  • El Arduino Due:
  • Serial (USB) para log y depuración
  • Serial1 (pines 18/19) para HM-10
  • Serial2 (pines 16/17) para UART del TMC2209
  • I2C (SDA: 20, SCL: 21) para AS5600
  • Pines digitales para STEP/DIR/EN del TMC2209

Tabla de mapeo de pines y alimentación

Módulo/Señal Arduino Due pin/puerto Módulo externo Notas
STEP (TMC2209) D2 STEP del TMC2209 Pulsos de paso (activo flanco)
DIR (TMC2209) D3 DIR del TMC2209 Sentido de giro
EN (TMC2209) D4 EN del TMC2209 Activo en LOW para habilitar el driver
UART TX (TMC2209) TX2 (D16) PDN_UART del TMC2209 (via 1k) Half-duplex: ver esquema de resistencias abajo
UART RX (TMC2209) RX2 (D17) PDN_UART del TMC2209 (via 1k) Half-duplex: mismo nodo PDN_UART
Motor coils A1 A2 B1 B2 Conecta según datasheet del motor
VM (Driver) +12–24 V Alimentación potencia motor
GND (Driver) GND GND común GND común con Arduino Due
AS5600 VCC 3.3 V VCC del AS5600 3.3 V recomendado
AS5600 GND GND GND del AS5600
AS5600 SDA SDA (D20) SDA del AS5600 I2C 0x36; pull-ups a 3.3 V si el módulo no las tiene
AS5600 SCL SCL (D21) SCL del AS5600
HM-10 VCC 3.3 V VCC del HM-10 Usar módulo compatible 3.3 V
HM-10 GND GND GND del HM-10
HM-10 TXD RX1 (D19) TXD del HM-10 9600 8N1 por defecto
HM-10 RXD TX1 (D18) RXD del HM-10
USB (log) Programming Port USB Para subir firmware y ver logs serie (Serial)

Conexión UART TMC2209 (half-duplex):
– Conecta PDN_UART del TMC2209 a:
– Arduino Due TX2 a través de una resistencia de 1 kΩ
– Arduino Due RX2 a través de otra resistencia de 1 kΩ
– Si el módulo expone pin PDN/CFG, úsalo para UART. Consulta la hoja del módulo específico (algunos traen jumpers para activar UART).

Ajustes TMC2209:
– Microstepping objetivo: 1/16 microsteps (interpolación a 256 interna activa)
– Corriente RMS inicial: 800 mA (ajusta a tu motor térmicamente seguro)
– Modo stealthChop habilitado para suavidad a baja velocidad

Mecánica del AS5600:
– Monta el imán diametral en el eje del motor, centrado y a ~1–3 mm del chip según datasheet. Una desalineación reduce linealidad.

Código completo (Arduino Due, C++)

A continuación, un sketch integral que:
– Configura TMC2209 por UART
– Lee el AS5600 por I2C y genera conteo multiturno
– Implementa un control de posición en bucle cerrado con PID
– Genera STEP/DIR con limitación de velocidad y aceleración
– Expone una interfaz BLE (HM-10) para comandos GOTO, ZERO, KP/KI/KD, STATUS
– Emite logs por USB (Serial)

Bloques clave:
– ISR de control a 1 kHz
– ISR de generación de pasos a 40 kHz (tick 25 µs)
– Conversión ángulo AS5600 -> pasos (con multiturno)
– Parser de comandos BLE

// Proyecto: closed-loop-stepper-positioning con Arduino Due + TMC2209 + AS5600 + HM-10
// Toolchain: Arduino CLI 0.35.3, core arduino:sam 1.6.12
// Librerías: TMCStepper 0.7.3, DueTimer 1.7.2

#include <Arduino.h>
#include <Wire.h>
#include <TMCStepper.h>
#include <DueTimer.h>

// --------------------------- Configuración de pines ---------------------------
#define STEP_PIN        2
#define DIR_PIN         3
#define EN_PIN          4

// UART para TMC2209
HardwareSerial& TMC_UART = Serial2;   // TX2(D16), RX2(D17)

// UART para HM-10 BLE
HardwareSerial& BLE = Serial1;        // TX1(D18), RX1(D19)

// I2C AS5600
#define AS5600_ADDR    0x36
#define AS5600_RAW_ANGLE_H 0x0C
#define AS5600_RAW_ANGLE_L 0x0D

// ---------------------- Configuración eléctrica TMC2209 ----------------------
#define R_SENSE         0.110f
#define DRIVER_ADDRESS  0b00  // Ajustar si usas múltiples drivers
TMC2209Stepper driver(&TMC_UART, R_SENSE, DRIVER_ADDRESS);

// Microstepping
const uint16_t MICROSTEPS = 16;          // 1/16
const uint16_t FULL_STEPS_PER_REV = 200; // motor 1.8°
const uint32_t STEPS_PER_REV = FULL_STEPS_PER_REV * MICROSTEPS; // 3200

// -------------------- Control, cinemática y límites -------------------------
volatile int32_t target_pos_steps = 0;    // Objetivo en micro-pasos
volatile float   cmd_vel_steps_s = 0.0f;  // Velocidad comandada por el controlador [steps/s]
volatile float   max_vel_steps_s = 12000.0f;   // Límite velocidad
volatile float   max_acc_steps_s2 = 40000.0f;  // Límite aceleración

// PID de posición (error = target - measured)
volatile float Kp = 2.0f;
volatile float Ki = 0.0f;
volatile float Kd = 0.02f;
volatile float integ = 0.0f;
volatile float prev_err = 0.0f;

// Medición encoder multiturno
volatile uint16_t last_raw_angle = 0;
volatile int32_t turns = 0;            // conteo de vueltas (multiturno)
volatile int32_t zero_offset_counts = 0; // puesta a cero
const float steps_per_encoder_count = (float)STEPS_PER_REV / 4096.0f;

// Estado de paso
volatile float phase_accum = 0.0f;     // acumulador de fase para emitir pasos
volatile bool step_high = false;       // estado del pulso STEP
volatile int8_t motion_dir = 1;        // +1 fwd, -1 rev

// Telemetría
volatile int32_t measured_pos_steps = 0;  // posición medida [steps]
volatile int32_t commanded_pos_steps = 0; // posición comandada (integración de pasos emitidos)

// Buffers BLE
char ble_buf[96];
size_t ble_idx = 0;

// Timers
// controlTimer: 1 kHz, stepTimer: 40 kHz (25 us)
DueTimer controlTimer = DueTimer(3);
DueTimer stepTimer    = DueTimer(4);

// ---------------------- Utilidades de I2C para AS5600 -----------------------
uint16_t readAS5600RawAngle() {
  Wire.beginTransmission(AS5600_ADDR);
  Wire.write(AS5600_RAW_ANGLE_H);
  if (Wire.endTransmission(false) != 0) {
    return last_raw_angle; // si falla, devuelve último valor
  }
  Wire.requestFrom(AS5600_ADDR, 2u);
  if (Wire.available() < 2) {
    return last_raw_angle;
  }
  uint8_t high = Wire.read();
  uint8_t low  = Wire.read();
  uint16_t angle = ((high & 0x0F) << 8) | low; // 12 bits: [11:8] en high[3:0], [7:0] en low
  return angle;
}

// ---------------------- Inicialización del TMC2209 --------------------------
void initTMC2209() {
  pinMode(EN_PIN, OUTPUT);
  digitalWrite(EN_PIN, HIGH); // deshabilitado inicialmente (EN activo en LOW)

  TMC_UART.begin(115200);
  delay(50);

  driver.begin();              // Carga parámetros por defecto
  driver.toff(5);              // driver enable (sgnificant >0)
  driver.rms_current(800);     // mA RMS (ajusta según motor)
  driver.microsteps(MICROSTEPS);
  driver.pdn_disable(true);    // PDN/UART como UART, no power-down
  driver.I_scale_analog(false);
  driver.en_spreadCycle(false); // usar stealthChop
  driver.pwm_autoscale(true);   // calibración automática
  driver.TCOOLTHRS(0xFFFFF);    // umbral alto para stealthChop
  driver.semin(5); driver.semax(2); driver.sedn(0b01); // CoolStep básico

  digitalWrite(EN_PIN, LOW);   // habilitar driver
}

// ----------------------- ISR del control (1 kHz) -----------------------------
void controlISR() {
  // 1) Leer encoder y construir multiturno
  uint16_t angle = readAS5600RawAngle();
  int16_t delta = (int16_t)angle - (int16_t)last_raw_angle;

  // Detectar cruces (wrap) en ±2048 (la mitad de 4096)
  if (delta > 2048) {
    turns -= 1; // pasó por 0 hacia atrás
  } else if (delta < -2048) {
    turns += 1; // pasó por 0 hacia delante
  }
  last_raw_angle = angle;

  int32_t multi_counts = turns * 4096 + (int32_t)angle - zero_offset_counts;
  int32_t pos_steps = (int32_t)lroundf(multi_counts * steps_per_encoder_count);
  measured_pos_steps = pos_steps;

  // 2) Calcular error de posición
  int32_t err = target_pos_steps - measured_pos_steps;

  // 3) PID
  const float dt = 0.001f; // 1 ms
  float f_err = (float)err;
  float deriv = (f_err - prev_err) / dt;
  integ += f_err * dt;

  // anti wind-up sencillo: clamp integral
  const float integ_limit = max_vel_steps_s * 0.5f;
  if (integ > integ_limit) integ = integ_limit;
  if (integ < -integ_limit) integ = -integ_limit;

  float vel_cmd = Kp * f_err + Ki * integ + Kd * deriv;

  // 4) Acel/vel limitación (rampa)
  // Limitar aceleración incremental por periodo de control
  float dv_max = max_acc_steps_s2 * dt;
  float dv = vel_cmd - cmd_vel_steps_s;
  if (dv > dv_max) dv = dv_max;
  if (dv < -dv_max) dv = -dv_max;
  cmd_vel_steps_s += dv;

  // Limitar velocidad máxima
  if (cmd_vel_steps_s > max_vel_steps_s) cmd_vel_steps_s = max_vel_steps_s;
  if (cmd_vel_steps_s < -max_vel_steps_s) cmd_vel_steps_s = -max_vel_steps_s;

  // Configurar dirección deseada (se usa en generador de pasos)
  motion_dir = (cmd_vel_steps_s >= 0.0f) ? +1 : -1;
}

// ------------------- ISR del generador de pasos (25 us) ----------------------
void stepISR() {
  static bool need_low_after_high = false;

  if (need_low_after_high) {
    digitalWrite(STEP_PIN, LOW);
    need_low_after_high = false;
    // Actualiza posición comandada tras el flanco de bajada
    commanded_pos_steps += motion_dir;
    return;
  }

  // Incrementa el acumulador con |vel|*dt (dt = 25e-6 s)
  float inc = fabsf(cmd_vel_steps_s) * 0.000025f; // steps per tick
  phase_accum += inc;

  if (phase_accum >= 1.0f) {
    // Preparar pulso: fijar DIR primero
    digitalWrite(DIR_PIN, (motion_dir > 0) ? HIGH : LOW);
    // Emitir flanco de subida STEP
    digitalWrite(STEP_PIN, HIGH);
    need_low_after_high = true;
    phase_accum -= 1.0f;
  }
}

// ------------------------- Parser de comandos BLE ----------------------------
// Comandos:
// - GOTO <steps>
// - ZERO
// - KP <val>, KI <val>, KD <val>
// - VMAX <steps_s>, AMAX <steps_s2>
// - STATUS
void handleCommand(const char* line) {
  if (strncmp(line, "GOTO", 4) == 0) {
    long val = atol(line + 4);
    noInterrupts();
    target_pos_steps = (int32_t)val;
    interrupts();
    BLE.println(F("OK GOTO"));
  } else if (strncmp(line, "ZERO", 4) == 0) {
    noInterrupts();
    zero_offset_counts = turns * 4096 + (int32_t)last_raw_angle;
    target_pos_steps = 0;
    commanded_pos_steps = 0;
    interrupts();
    BLE.println(F("OK ZERO"));
  } else if (strncmp(line, "KP ", 3) == 0) {
    float v = atof(line + 3);
    noInterrupts(); Kp = v; interrupts();
    BLE.println(F("OK KP"));
  } else if (strncmp(line, "KI ", 3) == 0) {
    float v = atof(line + 3);
    noInterrupts(); Ki = v; integ = 0; interrupts();
    BLE.println(F("OK KI"));
  } else if (strncmp(line, "KD ", 3) == 0) {
    float v = atof(line + 3);
    noInterrupts(); Kd = v; interrupts();
    BLE.println(F("OK KD"));
  } else if (strncmp(line, "VMAX ", 5) == 0) {
    float v = atof(line + 5);
    noInterrupts(); max_vel_steps_s = v; interrupts();
    BLE.println(F("OK VMAX"));
  } else if (strncmp(line, "AMAX ", 5) == 0) {
    float v = atof(line + 5);
    noInterrupts(); max_acc_steps_s2 = v; interrupts();
    BLE.println(F("OK AMAX"));
  } else if (strncmp(line, "STATUS", 6) == 0) {
    noInterrupts();
    long t = target_pos_steps;
    long m = measured_pos_steps;
    long c = commanded_pos_steps;
    float v = cmd_vel_steps_s;
    float kp = Kp, ki = Ki, kd = Kd;
    interrupts();
    BLE.print(F("T=")); BLE.print(t);
    BLE.print(F(" M=")); BLE.print(m);
    BLE.print(F(" C=")); BLE.print(c);
    BLE.print(F(" V=")); BLE.print(v, 1);
    BLE.print(F(" KP=")); BLE.print(kp, 3);
    BLE.print(F(" KI=")); BLE.print(ki, 3);
    BLE.print(F(" KD=")); BLE.print(kd, 3);
    BLE.println();
  } else {
    BLE.println(F("ERR CMD"));
  }
}

void pollBLE() {
  while (BLE.available()) {
    char ch = BLE.read();
    if (ch == '\r' || ch == '\n') {
      if (ble_idx > 0) {
        ble_buf[ble_idx] = '\0';
        handleCommand(ble_buf);
        ble_idx = 0;
      }
    } else {
      if (ble_idx < sizeof(ble_buf) - 1) {
        ble_buf[ble_idx++] = ch;
      }
    }
  }
}

// ------------------------------- setup/loop ----------------------------------
void setup() {
  pinMode(STEP_PIN, OUTPUT);
  pinMode(DIR_PIN, OUTPUT);
  digitalWrite(STEP_PIN, LOW);
  digitalWrite(DIR_PIN, LOW);

  Serial.begin(115200); // USB (Programming Port)
  while (!Serial) { ; }
  Serial.println(F("Init: Arduino Due closed-loop stepper with TMC2209 + AS5600 + HM-10"));

  BLE.begin(9600);      // HM-10 default
  Wire.begin();         // I2C

  // Inicializar TMC2209
  initTMC2209();

  // Inicializar AS5600 base
  last_raw_angle = readAS5600RawAngle();
  turns = 0;
  zero_offset_counts = 0;

  // Timers
  controlTimer.attachInterrupt(controlISR).setFrequency(1000).start(); // 1 kHz
  stepTimer.attachInterrupt(stepISR).setFrequency(40000).start();      // 40 kHz (25 us)

  Serial.println(F("Timers started. Send BLE commands: GOTO, ZERO, KP/KI/KD, VMAX/AMAX, STATUS"));
}

uint32_t lastLog = 0;

void loop() {
  // Consola BLE
  pollBLE();

  // Log periódico por USB
  uint32_t now = millis();
  if (now - lastLog > 500) {
    noInterrupts();
    long t = target_pos_steps;
    long m = measured_pos_steps;
    long c = commanded_pos_steps;
    float v = cmd_vel_steps_s;
    interrupts();
    Serial.print(F("[INFO] T=")); Serial.print(t);
    Serial.print(F(" M=")); Serial.print(m);
    Serial.print(F(" C=")); Serial.print(c);
    Serial.print(F(" V=")); Serial.print(v, 1);
    Serial.println();
    lastLog = now;
  }
}

Explicación breve de partes clave:
– initTMC2209 configura el driver para 1/16 microstepping, corriente RMS, stealthChop y UART activo en PDN.
– controlISR (1 kHz) ejecuta el PID de posición sobre la medición de AS5600; limita velocidad y aceleración para suavidad.
– stepISR (40 kHz) implementa un generador de pulsos con acumulador de fase: emite un pulso STEP cuando la “energía” acumulada supera 1 micro-paso.
– La multiturn del AS5600 detecta cruces por 0/4095 y ajusta un contador de vueltas; el comando ZERO re-referencia la posición a 0 pasos.
– La interfaz BLE acepta comandos de texto sencillos (por ejemplo: “GOTO 6400” para dos vueltas si 3200 steps/rev).

Compilación, flasheo y ejecución

A continuación, pasos reproducibles con Arduino CLI 0.35.3 usando Arduino Due (Programming Port).

Instalación de Arduino CLI y core arduino:sam

Linux (Ubuntu 22.04):

# 1) Instalar Arduino CLI 0.35.3 en $HOME/bin
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=$HOME/bin sh -s -- v0.35.3
echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

# 2) Verificar versión
arduino-cli version
# Debe mostrar: arduino-cli Version: 0.35.3

# 3) Inicializar configuración
arduino-cli config init

# 4) Actualizar índice de cores y librerías
arduino-cli core update-index

# 5) Instalar el core Arduino SAM (Due)
arduino-cli core install arduino:sam@1.6.12

# 6) Instalar librerías necesarias
arduino-cli lib install "TMCStepper@0.7.3"
arduino-cli lib install "DueTimer@1.7.2"

Windows 11 (PowerShell):
– Descarga binario de Arduino CLI 0.35.3 para Windows desde GitHub Releases, agrega su carpeta al PATH del usuario.
– En PowerShell:

arduino-cli version
arduino-cli core update-index
arduino-cli core install arduino:sam@1.6.12
arduino-cli lib install "TMCStepper@0.7.3"
arduino-cli lib install "DueTimer@1.7.2"

Nota: En Windows, instala los drivers Arduino (incluidos con IDE o CLI) para el “Arduino Due (Programming Port)”. El puerto aparecerá como COMx.

Compilación y subida

1) Conecta el Arduino Due por el Programming Port (no el Native USB Port).
2) Guarda el sketch anterior como carpeta/proyecto, por ejemplo: closed_loop_due/closed_loop_due.ino
3) Identifica el puerto:
– Linux:

arduino-cli board list
# Ejemplo de salida:
# Port         Type              Board Name                 FQBN
# /dev/ttyACM0 Serial Port (USB) Arduino Due (Programming)  arduino:sam:arduino_due_x_dbg
  • Windows:
arduino-cli board list
# Buscar COMx correspondiente al "Arduino Due (Programming Port)"

4) Compila:

arduino-cli compile --fqbn arduino:sam:arduino_due_x_dbg closed_loop_due

5) Sube el firmware:

# Linux: ajusta el puerto si es distinto
arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:sam:arduino_due_x_dbg --verify closed_loop_due

# Windows (PowerShell), ajusta COMx
arduino-cli upload -p COM5 --fqbn arduino:sam:arduino_due_x_dbg --verify closed_loop_due

6) Abre la consola serie para logs (USB):

# Linux
screen /dev/ttyACM0 115200
# o
minicom -D /dev/ttyACM0 -b 115200

En Windows, usa PuTTY/Serial a 115200 baudios en el COMx del Programming Port.

Validación paso a paso

Objetivo: demostrar “closed-loop-stepper-positioning” con el AS5600 como sensor de posición y el TMC2209 como actuador, con comandos BLE y telemetría por USB.

1) Revisión eléctrica de seguridad
– Verifica que EN del TMC2209 esté inicialmente en HIGH (driver deshabilitado) antes de dar alimentación de motor.
– Asegúrate de GND común entre fuente del motor, driver y Arduino Due.
– Alimenta el TMC2209 con 12–24 V, sin motor inicialmente, para comprobar que no hay sobrecorriente.
– Conecta el motor después de la verificación de alimentación.

2) Puesta en marcha del software
– Conecta el Programming Port del Due, sube el firmware (ver comandos).
– Abre el monitor serie a 115200 baudios. Debes ver:
– “Init: Arduino Due closed-loop stepper …”
– “Timers started. Send BLE commands: …”
– Si no ves mensajes, revisa que usas el Programming Port y que el puerto serie es correcto.

3) Prueba del encoder AS5600
– Sin cerrar el lazo, ejecuta comandos BLE para ver STATUS.
– Desde el móvil, con la app BLE:
– Empareja con HM-10 (nombre por defecto “HMSoft” o similar).
– Abre la UART BLE y envía “STATUS”.
– Debes recibir algo como: “T=0 M=0 C=0 V=0.0 KP=2.000 KI=0.000 KD=0.020”
– Gira el eje manualmente: el campo M (medida) debe cambiar; si cambia al revés de lo esperado, luego invertirás DIR en el cableado o en software.

4) Cierre de lazo básico con ZERO y GOTO
– Envía “ZERO”: pondrá el origen en la posición actual y fijará T=0.
– Envía “GOTO 3200”: objetivo 1 vuelta (microstepping 1/16 => 3200 µpasos).
– Observa:
– El motor debe acelerar, moverse y detenerse cerca del objetivo.
– En Serial (USB), verás logs de T, M, C, V. M debe converger a T con error pequeño.

5) Ajuste PID (si es necesario)
– Si hay sobreoscilación (overshoot) o vibración:
– Reduce Kp o aumenta Kd ligeramente. Ejemplo:
– “KD 0.05”
– “KP 1.5”
– Si hay error permanente:
– Introduce una pequeña Ki. Ejemplo: “KI 0.005”
– Verifica el comportamiento repitiendo GOTO a varios objetivos: 0, 1600, 3200, -1600, etc.

6) Verificación de límites VMAX y AMAX
– Envía “VMAX 8000” para bajar velocidad máx si el motor salta pasos.
– Envía “AMAX 20000” si notas golpes al arrancar; valores bajos suavizan el arranque/parada.
– Comprueba estabilidad frente a cambios de objetivo consecutivos.

7) Evaluación de seguimiento
– Compara M (medido) y C (comandado). En un buen ajuste, ambos serán cercanos y convergerán al objetivo T.
– Un desfase sostenido indica desajuste de PID o saturación (VMAX/AMAX demasiado bajos).

8) Comando sostenido y multiturno
– Prueba GOTO 12800 (≈4 vueltas) y regresa a 0.
– Asegura que el multiturno no presenta saltos (M crece/ decrece continuamente).

9) Comprobación BLE frente a USB
– Confirma que los comandos por BLE afectan al comportamiento y se reflejan en el log USB.

10) Prueba de carga
– Si es posible, aplica una ligera carga resistiva al eje y verifica que el lazo corrige el error y sostiene la posición.

Troubleshooting

1) El Due no aparece en la lista de puertos o la subida falla
– Causa: cable USB inadecuado (solo carga), drivers no instalados o puerto incorrecto.
– Solución:
– Usa un cable USB de datos y el Programming Port.
– En Linux: agrega tu usuario al grupo dialout: sudo usermod -a -G dialout $USER y reabre sesión.
– Verifica con arduino-cli board list y ajusta el puerto en upload.

2) El motor vibra o no se mueve al enviar GOTO
– Causa: fase de motor mal cableada (A/B invertidas) o microstepping/dir invertido.
– Solución:
– Intercambia una bobina (A1/A2) o B1/B2 para corregir vibración.
– Invierte el sentido DIR en software (cambia HIGH/LOW) o invierte el cable DIR.

3) El encoder mide al revés del movimiento
– Causa: convención de sentido opuesta entre AS5600 y pasos.
– Solución:
– Cambia el sentido DIR en software o multiplica measured_pos_steps por -1 en el cálculo, o invierte el imán 180°.

4) Pérdida de pasos a alta velocidad
– Causa: VMAX/AMAX excesivos, corriente insuficiente o tensión de alimentación baja.
– Solución:
– Reduce VMAX/AMAX (comandos VMAX/AMAX).
– Aumenta rms_current en TMC (recompila con mayor driver.rms_current dentro de límite térmico).
– Sube VM a 24 V si tu hardware lo permite.

5) TMC2209 no responde por UART (config no se aplica)
– Causa: cableado UART half-duplex incorrecto o PDN no configurado como UART.
– Solución:
– Verifica resistencias de 1 kΩ desde TX2 y RX2 al pin PDN_UART del TMC2209.
– Asegura driver.pdn_disable(true) y velocidad 115200 en TMC_UART.
– Comprueba jumpers del módulo (algunos requieren puentear para UART).

6) Lectura del AS5600 salta o es ruidosa
– Causa: imán descentrado, distancia inadecuada o cable I2C largo sin pull-ups adecuadas.
– Solución:
– Centra mejor el imán, ajusta la distancia a 1–3 mm.
– Añade pull-ups de 4.7 kΩ a 3.3 V en SDA/SCL si el módulo no las trae.
– Filtra con media móvil o un pequeño filtro derivativo (aumenta Kd, reduce Kp).

7) HM-10 no conecta o no recibe comandos
– Causa: módulo BLE a baudios distintos o sin alimentación correcta.
– Solución:
– Asegura BLE.begin(9600) y que tu HM-10 esté a 9600 (de fábrica).
– Verifica VCC 3.3 V y conexión TXD→RX1, RXD→TX1.
– Reinicia el módulo y vuelve a parear.

8) El lazo oscila o tarda demasiado en asentar
– Causa: PID mal ajustado.
– Solución:
– Empieza con Ki = 0, aumenta Kp hasta un ligero overshoot, añade Kd para amortiguar.
– Introduce Ki pequeño para eliminar error de régimen permanente.
– Reajusta VMAX/AMAX para no saturar el actuador.

Mejoras/variantes

  • Estrategia de control
  • Reemplazar PID por control por perfil trapezoidal/cinco segmentos y lazo de seguimiento de velocidad + lazo de posición (cascada).
  • Añadir feedforward de velocidad/acceleración basado en la cinemática planeada.

  • Telemetría avanzada

  • Transmitir por BLE un paquete periódico JSON con {T, M, C, V, err} y visualizarlo en una app móvil o en un dashboard.

  • Gestión de fallos

  • Añadir detección de pérdida de seguimiento (|T-M| > umbral por tiempo) y estrategia de reintento o alarma.
  • Monitorizar sobrecorriente/temperatura del driver con pines DIAG del TMC2209.

  • Multieje

  • Extender a dos ejes con Serial3 para el segundo TMC2209 y un segundo AS5600 en dirección I2C alternativa (usar versiones de AS5600 con ADDR o multiplexor I2C).

  • Autotuning

  • Implementar un procedimiento de sintonía automática: escalón pequeño, medir respuesta, estimar parámetros de planta y derivar Kp, Ki, Kd (método Ziegler-Nichols o IMC).

  • Sin librería de encoder

  • Si deseas robustez total frente a dependencias, ya se usa lectura I2C directa; puedes añadir calibración del AS5600 (offset) escribiendo registros del chip.

Checklist de verificación

  • [ ] Instalada Arduino CLI 0.35.3 y core arduino:sam 1.6.12
  • [ ] Librerías TMCStepper 0.7.3 y DueTimer 1.7.2 instaladas
  • [ ] Conexión eléctrica revisada: GND común, VM 12–24 V, motor conectado correctamente
  • [ ] PDN_UART cableado con dos resistencias de 1 kΩ a TX2 y RX2
  • [ ] AS5600 conectado a 3.3 V, SDA (D20), SCL (D21) y pull-ups si faltan
  • [ ] HM-10 conectado a Serial1 (TX1/RX1), alimentado a 3.3 V, BLE funcional
  • [ ] Firmware compilado con FQBN arduino:sam:arduino_due_x_dbg y subido sin errores
  • [ ] Log USB visible a 115200 baudios con mensajes de inicio y estado
  • [ ] Comando BLE “ZERO” responde “OK ZERO”
  • [ ] Comando BLE “GOTO 3200” mueve 1 vuelta y asienta cerca del objetivo
  • [ ] Ajuste PID realizado (sin oscilaciones, error de régimen pequeño)
  • [ ] VMAX y AMAX adecuados (sin pérdidas de pasos ni golpes)
  • [ ] Multiturno correcto (varias vueltas en ambos sentidos sin saltos)

Apéndice: configuración opcional del HM-10 mediante AT

Si deseas renombrar el HM-10 o fijar su velocidad, temporalmente conéctalo a un adaptador USB-UART a 9600 baudios y envía:

AT            // debe responder OK
AT+NAMECLStepper   // renombra el periférico
AT+BAUD4      // 9600 (por defecto); BAUD8 sería 115200 si lo cambias
AT+RESET

Asegúrate de mantener BLE.begin(9600) si usas 9600 baudios.

Notas finales sobre coherencia del hardware

  • El tutorial está estrictamente alineado con el modelo: Arduino Due + TMC2209 stepper drivers + AS5600 encoders + HM-10 BLE. Todo el cableado, código y comandos se han ajustado a esta combinación.
  • El uso del core arduino:sam 1.6.12 y la FQBN arduino:sam:arduino_due_x_dbg asegura compatibilidad con el Programming Port del Arduino Due en Arduino CLI 0.35.3.

Encuentra este producto y/o libros sobre este tema en Amazon

Ir a Amazon

Como afiliado de Amazon, gano con las compras que cumplan los requisitos. Si compras a través de este enlace, ayudas a mantener este proyecto.

Quiz rápido

Pregunta 1: ¿Cuál es la versión probada de Ubuntu mencionada en el artículo?




Pregunta 2: ¿Qué tipo de controlador se menciona como parte del dispositivo del proyecto?




Pregunta 3: ¿Cuál es la versión del Arduino CLI utilizada en el artículo?




Pregunta 4: ¿Qué tipo de sensor se utiliza en el proyecto?




Pregunta 5: ¿Cuál es el voltaje de operación del Arduino Due?




Pregunta 6: ¿Qué librería se menciona para el manejo de temporizadores?




Pregunta 7: ¿Qué tipo de motor se menciona en el artículo?




Pregunta 8: ¿Cuál es la capacidad típica de la fuente de alimentación para el motor?




Pregunta 9: ¿Qué software auxiliar se menciona como opcional para validación?




Pregunta 10: ¿Qué es R_SENSE en el contexto del controlador TMC2209?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Ingeniero Superior en Electrónica de Telecomunicaciones e Ingeniero en Informática (titulaciones oficiales en España).

Sígueme:


Caso práctico: Contador de personas con HM01B0 y Portenta H7

Caso práctico: Contador de personas con HM01B0 y Portenta H7 — hero

Objetivo y caso de uso

Qué construirás: Un contador de personas en tiempo real utilizando la cámara Himax HM01B0 y el Arduino Portenta H7 para transmitir conteos de manera eficiente.

Para qué sirve

  • Monitoreo de aforo en tiendas para optimizar la experiencia del cliente.
  • Control de acceso en eventos para garantizar la seguridad y el cumplimiento de aforos.
  • Estadísticas de tráfico peatonal en espacios públicos para análisis de comportamiento.

Resultado esperado

  • Conteo de personas con una precisión del 95% en condiciones de luz óptimas.
  • Transmisión de datos en tiempo real con latencias inferiores a 200 ms.
  • Capacidad de procesar hasta 30 FPS (fotogramas por segundo) en la detección de personas.

Público objetivo: Desarrolladores avanzados; Nivel: Avanzado

Arquitectura/flujo: Captura de video desde la cámara HM01B0, procesamiento en el borde con Portenta H7, transmisión de conteos a través de MQTT.

Nivel: Avanzado

Prerrequisitos

En este caso práctico implementarás un pipeline completo de visión embebida para contar personas que cruzan una línea virtual, ejecutándose enteramente en el borde (edge) usando la cámara Himax HM01B0 del Portenta Vision Shield. Trabajaremos en C++ con la toolchain de Arduino CLI y el core mbed específico de Portenta H7.

Sistema operativo (uno de los siguientes)

  • Windows 11 23H2 (x64)
  • Ubuntu 22.04 LTS (x86_64)
  • macOS 13 Ventura o macOS 14 Sonoma (Apple Silicon o Intel)

Toolchain y versiones exactas

  • Arduino CLI 0.35.3
  • Core de placas: arduino:mbed_portenta 4.2.1
  • FQBN (Fully Qualified Board Name): arduino:mbed_portenta:envie_m7 (Portenta H7, núcleo M7)
  • Librerías Arduino:
  • Arduino_HM01B0 1.0.4 (captura desde la cámara Himax HM01B0 del Vision Shield)
  • Arduino_DebugUtils 1.4.0 (opcional, para logs con niveles)
  • Python 3.11.x (solo para un script auxiliar de validación vía puerto serie; opcional)

Nota: Si ya tienes Arduino CLI en otra versión, se recomienda instalar la versión indicada para reproducibilidad exacta.

Requisitos de conocimientos previos

  • C++ para Arduino y conceptos de memoria en sistemas embebidos.
  • Nociones de visión computacional: sustracción de fondo, operaciones morfológicas, componentes conectados y tracking básico por proximidad.
  • Uso de terminal y puertos serie.

Materiales

  • 1 x Arduino Portenta H7
  • 1 x Portenta Vision Shield (Himax HM01B0) — versión con cámara HM01B0 (monocromo, resolución típica 320×320). Se asume la variante estándar; el ejemplo no depende de Ethernet o LoRa, aunque se comentan mejoras con red más adelante.
  • 1 x Cable USB‑C de datos (no solo carga), de buena calidad.
  • 1 x Trípode o soporte para fijar el ángulo de cámara (recomendado para estabilidad del conteo).
  • 1 x Ordenador con uno de los SO indicados y derechos de administrador.
  • Iluminación estable de la zona a monitorear (evitar cambios bruscos de luz que degraden la sustracción de fondo).
  • Opcional:
  • 1 x Regla o cinta para medir posición de la “línea virtual” respecto del suelo.
  • 1 x Superficie con patrón contrastado para pruebas.

Preparación y conexión

Montaje y orientación de la cámara

  • Ensambla el Portenta Vision Shield encima del Portenta H7 alineando los dos conectores de alta densidad. Presiona firmemente y de forma uniforme.
  • Orienta el conjunto de modo que la lente de la HM01B0 apunte a la zona de paso donde quieres contar personas (pasillo, puerta, etc.).
  • Coloca el conjunto en el trípode o soporte, a una altura donde la línea de conteo (virtual) atravesará el torso de una persona promedio (aprox. 1.0–1.2 m de altura si la cámara apunta horizontalmente, o vista cenital si la montas en techo).

Conexión al host

  • Conecta el cable USB‑C del Portenta H7 al ordenador.
  • El dispositivo aparecerá como puerto serie:
  • Windows: COMx (ver en Administrador de dispositivos)
  • Linux: /dev/ttyACM0 (o ACM1 si tienes más dispositivos)
  • macOS: /dev/tty.usbmodem-xxxx

Tabla de puertos y elementos físicos relevantes

Elemento Ubicación Función Observaciones
USB‑C Portenta H7 Borde del módulo Datos y alimentación Requiere cable de datos; para subir firmware y monitor serie
Cámara HM01B0 En Vision Shield Captura de imagen Sensor monocromo, típico 320×320; se accede por I2C y DVP internos
LED integrado Portenta H7 Indicador estado Usado para feedback sencillo (parpadeo en arranque y eventos)
Conectores de alta densidad Entre H7 y Shield Señales internas No conectar cables aquí; ya provee la ruta cámara–MCU

Código completo (C++ para Arduino Portenta H7)

Implementaremos:
– Inicialización de la HM01B0 a 320×320 (grises).
– Captura periódica de frames.
– Sustracción de fondo con actualización exponencial (EMA) para robustez a cambios lentos de iluminación.
– Umbral adaptativo simple con rango de histéresis.
– Limpieza morfológica (apertura/cierre aproximados con un kernel 3×3).
– Etiquetado de componentes conectados (4-conexión) y obtención de bounding boxes/centroides.
– Tracking por proximidad con asignación greedy y poda por tiempo sin ver.
– Conteo de cruces respecto a una línea virtual horizontal en coordenada y_line.
– Telemetría por Serial y una interfaz mínima con comandos: “r” (reset), “b” (re‑calibrar fondo), “t” (toggle logs detallados).

Nota: El código usa la librería Arduino_HM01B0 para la cámara. La API concreta de la versión 1.0.4 expone begin() y readFrame(). Si tu versión varía, ajusta los nombres de métodos (ver Troubleshooting).

/*
  Proyecto: camera-edge-people-counting
  Dispositivo: Arduino Portenta H7 + Portenta Vision Shield (Himax HM01B0)
  Toolchain: Arduino CLI 0.35.3, core arduino:mbed_portenta@4.2.1
  Librerías: Arduino_HM01B0@1.0.4

  Funcionalidad:
  - Captura frames monocromo 320x320 desde HM01B0
  - Sustracción de fondo con EMA
  - Umbral + operaciones morfológicas
  - Componentes conectados y centroides
  - Tracking simple y conteo de cruces sobre una línea horizontal
*/

#include <Arduino.h>
#include <Arduino_HM01B0.h>  // Librería de la cámara Himax HM01B0

// ------------------------- Parámetros de cámara y procesamiento -------------------------
static const int CAM_W = 320;
static const int CAM_H = 320;

// Línea virtual para conteo (en píxeles, 0 = top). Puedes ajustar en tiempo de ejecución con 'L<valor>\n'.
volatile int y_line = CAM_H / 2;

// Parámetros de sustracción de fondo
static const float BG_ALPHA = 0.02f;   // EMA: peso de frame actual
static const uint8_t THRESH_LOW  = 18; // Histéresis inferior
static const uint8_t THRESH_HIGH = 30; // Histéresis superior

// Filtros morfológicos: número de iteraciones
static const int ERODE_ITERS  = 1;
static const int DILATE_ITERS = 2;

// Restricciones de blobs (en píxeles)
static const int MIN_BLOB_AREA = 300;   // Ajustar según distancia a la cámara
static const int MAX_BLOB_AREA = 20000; // Evita falsos positivos gigantes (p. ej. todo el frame)

// Tracking
static const int MAX_TRACKS         = 16;
static const int MAX_MISSES         = 6;  // frames que tolera sin ver
static const int ASSIGN_DIST_THRESH = 40; // distancia máx. en píxeles para asignación

// Buffers
static uint8_t  frame[CAM_W * CAM_H];       // Frame actual (8-bit, gris)
static uint8_t  fgMask[CAM_W * CAM_H];      // Máscara binaria foreground
static uint8_t  bgModel[CAM_W * CAM_H];     // Fondo (8-bit)
static uint8_t  morphBuf[CAM_W * CAM_H];    // Buffer temporal para morfología
static int16_t  labelMap[CAM_W * CAM_H];    // Etiquetas por píxel (-1 = fondo)

// Cámara
HM01B0 himax;

// ------------------------- Utilidades -------------------------
inline int idx(int x, int y) { return y * CAM_W + x; }

void clearMask(uint8_t *buf) {
  memset(buf, 0, CAM_W * CAM_H);
}

void clearLabels() {
  for (int i = 0; i < CAM_W * CAM_H; ++i) labelMap[i] = -1;
}

// Inicializa el modelo de fondo con el primer frame
void initBackground(const uint8_t *src) {
  memcpy(bgModel, src, CAM_W * CAM_H);
}

// EMA del fondo
inline uint8_t ema_bg(uint8_t prev, uint8_t cur, float alpha) {
  float v = (1.0f - alpha) * (float)prev + alpha * (float)cur;
  if (v < 0) v = 0; if (v > 255) v = 255;
  return (uint8_t)(v + 0.5f);
}

void updateBackground(const uint8_t *src) {
  for (int i = 0; i < CAM_W * CAM_H; ++i) {
    bgModel[i] = ema_bg(bgModel[i], src[i], BG_ALPHA);
  }
}

void computeForeground(const uint8_t *src, uint8_t *dst) {
  // Umbral con histéresis simple para robustez a ruido
  for (int i = 0; i < CAM_W * CAM_H; ++i) {
    int d = abs((int)src[i] - (int)bgModel[i]);
    uint8_t prev = dst[i];
    if (prev) {
      // Una vez “on”, usa umbral bajo para permanecer
      dst[i] = (d > THRESH_LOW) ? 255 : 0;
    } else {
      // Para encender, umbral alto
      dst[i] = (d > THRESH_HIGH) ? 255 : 0;
    }
  }
}

// Erosión binaria 3x3
void erode3x3(const uint8_t *src, uint8_t *dst) {
  for (int y = 1; y < CAM_H - 1; ++y) {
    for (int x = 1; x < CAM_W - 1; ++x) {
      bool allOn = true;
      for (int j = -1; j <= 1 && allOn; ++j) {
        for (int i = -1; i <= 1; ++i) {
          if (src[idx(x + i, y + j)] == 0) { allOn = false; break; }
        }
      }
      dst[idx(x, y)] = allOn ? 255 : 0;
    }
  }
  // bordes = 0
  for (int x = 0; x < CAM_W; ++x) { dst[idx(x,0)] = 0; dst[idx(x,CAM_H-1)] = 0; }
  for (int y = 0; y < CAM_H; ++y) { dst[idx(0,y)] = 0; dst[idx(CAM_W-1,y)] = 0; }
}

// Dilatación binaria 3x3
void dilate3x3(const uint8_t *src, uint8_t *dst) {
  for (int y = 1; y < CAM_H - 1; ++y) {
    for (int x = 1; x < CAM_W - 1; ++x) {
      bool anyOn = false;
      for (int j = -1; j <= 1 && !anyOn; ++j) {
        for (int i = -1; i <= 1; ++i) {
          if (src[idx(x + i, y + j)] != 0) { anyOn = true; break; }
        }
      }
      dst[idx(x, y)] = anyOn ? 255 : 0;
    }
  }
  // bordes = 0
  for (int x = 0; x < CAM_W; ++x) { dst[idx(x,0)] = 0; dst[idx(x,CAM_H-1)] = 0; }
  for (int y = 0; y < CAM_H; ++y) { dst[idx(0,y)] = 0; dst[idx(CAM_W-1,y)] = 0; }
}

// Pipeline morfológico: apertura (erode->dilate) + cierre (dilate->erode)
void morphologicalCleanup(uint8_t *mask) {
  erode3x3(mask, morphBuf);
  dilate3x3(morphBuf, mask);
  dilate3x3(mask, morphBuf);
  erode3x3(morphBuf, mask);
}

// ------------------------- Componentes conectados y blobs -------------------------
struct Blob {
  int minx, miny, maxx, maxy;
  int area;
  int cx, cy;
};

static const int MAX_BLOBS = 16;
Blob blobs[MAX_BLOBS];
int numBlobs = 0;

void resetBlobs() {
  numBlobs = 0;
}

void addPixelToBlob(Blob &b, int x, int y) {
  if (x < b.minx) b.minx = x;
  if (y < b.miny) b.miny = y;
  if (x > b.maxx) b.maxx = x;
  if (y > b.maxy) b.maxy = y;
  b.area++;
  b.cx += x;
  b.cy += y;
}

void finalizeBlob(Blob &b) {
  if (b.area > 0) {
    b.cx /= b.area;
    b.cy /= b.area;
  }
}

// BFS para etiquetado simple 4-conectado
struct QueueNode { int x, y; };
static QueueNode q[CAM_W * CAM_H]; // Ojo: memoria grande; Portenta H7 lo soporta.

void connectedComponents(uint8_t *mask) {
  clearLabels();
  resetBlobs();

  int qh = 0, qt = 0;
  int currentLabel = 0;

  for (int y = 1; y < CAM_H - 1; ++y) {
    for (int x = 1; x < CAM_W - 1; ++x) {
      int id = idx(x, y);
      if (mask[id] == 0 || labelMap[id] != -1) continue;

      if (numBlobs >= MAX_BLOBS) {
        // Evita exceso de memoria/tiempo: abortar etiquetado adicional
        continue;
      }

      // Inicializa nuevo blob
      Blob &b = blobs[numBlobs];
      b.minx = b.maxx = x;
      b.miny = b.maxy = y;
      b.area = 0;
      b.cx = b.cy = 0;

      // BFS
      qh = qt = 0;
      q[qt++] = {x, y};
      labelMap[id] = currentLabel;

      while (qh < qt) {
        QueueNode n = q[qh++];
        addPixelToBlob(b, n.x, n.y);

        // Vecinos 4-conectados
        const int nx[4] = { 1, -1, 0, 0 };
        const int ny[4] = { 0, 0, 1, -1 };

        for (int k = 0; k < 4; ++k) {
          int xx = n.x + nx[k];
          int yy = n.y + ny[k];
          int nid = idx(xx, yy);
          if (xx <= 0 || xx >= CAM_W - 1 || yy <= 0 || yy >= CAM_H - 1) continue;
          if (mask[nid] == 0) continue;
          if (labelMap[nid] != -1) continue;
          labelMap[nid] = currentLabel;
          q[qt++] = {xx, yy};
          if (qt >= (CAM_W * CAM_H)) break; // safety
        }
        if (qt >= (CAM_W * CAM_H)) break; // safety
      }

      finalizeBlob(b);

      // Filtros por área
      if (b.area >= MIN_BLOB_AREA && b.area <= MAX_BLOB_AREA) {
        numBlobs++;
        currentLabel++;
      } else {
        // Invalida etiqueta si no cumple área
        // (no añadimos a lista de blobs finales)
        // Nada extra: se descarta al no incrementar numBlobs
      }
    }
  }
}

// ------------------------- Tracking y conteo -------------------------
struct Track {
  bool active;
  int id;
  int x, y;     // posición actual
  int px, py;   // posición previa
  int age;      // frames desde creación
  int missed;   // frames sin asignar
};

Track tracks[MAX_TRACKS];
int nextTrackId = 1;

int countUp = 0;
int countDown = 0;

void resetTracks() {
  for (int i = 0; i < MAX_TRACKS; ++i) tracks[i].active = false;
  nextTrackId = 1;
}

int spawnTrack(int x, int y) {
  for (int i = 0; i < MAX_TRACKS; ++i) {
    if (!tracks[i].active) {
      tracks[i].active = true;
      tracks[i].id = nextTrackId++;
      tracks[i].x = x;
      tracks[i].y = y;
      tracks[i].px = x;
      tracks[i].py = y;
      tracks[i].age = 1;
      tracks[i].missed = 0;
      return i;
    }
  }
  return -1;
}

int dist2(int x1, int y1, int x2, int y2) {
  int dx = x1 - x2;
  int dy = y1 - y2;
  return dx*dx + dy*dy;
}

void stepTracking() {
  // Marcar todos como no asignados
  for (int i = 0; i < MAX_TRACKS; ++i) {
    if (tracks[i].active) {
      tracks[i].missed++;
    }
  }

  // Asignación greedy: para cada blob, asignar el track más cercano
  for (int b = 0; b < numBlobs; ++b) {
    int bx = blobs[b].cx;
    int by = blobs[b].cy;

    int bestIdx = -1;
    int bestD2 = ASSIGN_DIST_THRESH * ASSIGN_DIST_THRESH + 1;

    for (int t = 0; t < MAX_TRACKS; ++t) {
      if (!tracks[t].active) continue;
      int d2 = dist2(bx, by, tracks[t].x, tracks[t].y);
      if (d2 < bestD2) {
        bestD2 = d2;
        bestIdx = t;
      }
    }

    if (bestIdx >= 0) {
      // Actualiza track
      Track &tr = tracks[bestIdx];
      tr.px = tr.x;
      tr.py = tr.y;
      tr.x = bx;
      tr.y = by;
      tr.age++;
      tr.missed = 0;

      // Check cruce de línea
      // Conteo: si cruzó de arriba->abajo (py < y_line y y >= y_line) => Down
      //         si cruzó de abajo->arriba (py >= y_line y y < y_line) => Up
      if (tr.py < y_line && tr.y >= y_line) {
        countDown++;
        // feedback rápido
        digitalWrite(LED_BUILTIN, HIGH);
        delay(5);
        digitalWrite(LED_BUILTIN, LOW);
      } else if (tr.py >= y_line && tr.y < y_line) {
        countUp++;
        digitalWrite(LED_BUILTIN, HIGH);
        delay(5);
        digitalWrite(LED_BUILTIN, LOW);
      }
    } else {
      // No se asignó: crear track nuevo
      spawnTrack(bx, by);
    }
  }

  // Poda de tracks con demasiados misses
  for (int i = 0; i < MAX_TRACKS; ++i) {
    if (tracks[i].active && tracks[i].missed > MAX_MISSES) {
      tracks[i].active = false;
    }
  }
}

// ------------------------- Serie y comandos -------------------------
bool verbose = false;
bool bgInitialized = false;

void printStatus() {
  int total = countUp + countDown;
  Serial.print(F("CNT UP=")); Serial.print(countUp);
  Serial.print(F(" DOWN=")); Serial.print(countDown);
  Serial.print(F(" TOTAL=")); Serial.println(total);
}

void handleSerial() {
  while (Serial.available()) {
    char c = Serial.read();
    if (c == 'r' || c == 'R') {
      countUp = countDown = 0;
      resetTracks();
      Serial.println(F("[OK] Contadores y tracks reseteados."));
    } else if (c == 't' || c == 'T') {
      verbose = !verbose;
      Serial.print(F("[OK] Verbose=")); Serial.println(verbose ? "ON" : "OFF");
    } else if (c == 'b' || c == 'B') {
      // Recalibrar el fondo usando el frame actual
      initBackground(frame);
      bgInitialized = true;
      Serial.println(F("[OK] Fondo re-calibrado."));
    } else if (c == 'L') {
      // Protocolo simple: 'L<numero>\n' para cambiar la línea (y_line)
      String s = Serial.readStringUntil('\n');
      int v = s.toInt();
      if (v > 0 && v < CAM_H) {
        y_line = v;
        Serial.print(F("[OK] y_line=")); Serial.println(y_line);
      } else {
        Serial.println(F("[ERR] Valor de L inválido."));
      }
    }
  }
}

// ------------------------- Setup & Loop -------------------------
void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, LOW);

  Serial.begin(115200);
  while (!Serial) { ; } // Espera conexión en USB CDC

  Serial.println(F("camera-edge-people-counting (Portenta H7 + Vision Shield HM01B0)"));
  Serial.println(F("Inicializando camara..."));

  if (!himax.begin(CAM_W, CAM_H)) {
    Serial.println(F("[ERR] No se pudo inicializar HM01B0. Verifique el shield."));
    while (1) { digitalWrite(LED_BUILTIN, !digitalWrite); delay(200); }
  }

  // Intenta poner la cámara en modo continuo (si la API lo soporta)
  himax.setFrameRate(30); // Si la API es distinta, ajusta; de lo contrario, ignora
  delay(100);

  clearMask(fgMask);
  resetTracks();

  // Parpadeo de arranque
  for (int i = 0; i < 3; ++i) {
    digitalWrite(LED_BUILTIN, HIGH); delay(50);
    digitalWrite(LED_BUILTIN, LOW);  delay(50);
  }

  Serial.println(F("[OK] Setup completado. Comandos: r=reset, b=bg recalib, t=verbose toggle, L<y>\\n para linea."));
}

unsigned long lastReport = 0;

void loop() {
  handleSerial();

  // Captura un frame
  bool ok = himax.readFrame(frame); // API típica: readFrame(dest)
  if (!ok) {
    Serial.println(F("[WARN] Fallo al leer frame"));
    delay(5);
    return;
  }

  // Inicializa/actualiza fondo
  if (!bgInitialized) {
    initBackground(frame);
    bgInitialized = true;
    return; // saltamos primer ciclo
  } else {
    updateBackground(frame);
  }

  // Foreground mask
  computeForeground(frame, fgMask);

  // Morfología
  morphologicalCleanup(fgMask);

  // Componentes conectados
  connectedComponents(fgMask);

  // Tracking y conteo
  stepTracking();

  // Telemetría
  unsigned long now = millis();
  if (now - lastReport > 500) {
    printStatus();
    lastReport = now;

    if (verbose) {
      Serial.print(F("Blobs=")); Serial.println(numBlobs);
      for (int i = 0; i < numBlobs; ++i) {
        Serial.print(F("  B")); Serial.print(i);
        Serial.print(F(": cx=")); Serial.print(blobs[i].cx);
        Serial.print(F(" cy=")); Serial.print(blobs[i].cy);
        Serial.print(F(" area=")); Serial.println(blobs[i].area);
      }
      Serial.print(F("y_line=")); Serial.println(y_line);
    }
  }

  // Control simple de framerate
  delay(5);
}

Notas sobre el código:
– Si tu versión exacta de Arduino_HM01B0 usa método distinto de begin()/readFrame(), adapta esas llamadas. En 1.0.4 suelen estar disponibles begin(ancho, alto) y readFrame(uint8_t*).
– El etiquetado BFS y los buffers son “grandes”, pero la Portenta H7 (núcleo M7) dispone de RAM suficiente para esta carga. Si necesitas bajar uso de RAM, reduce la resolución de trabajo haciendo submuestreo en software o reconfigurando la cámara a 160×160 y ajusta constantes.
– La línea virtual está en y_line y se puede cambiar en tiempo real con el comando serie “L200” seguido de Enter (ajustando 200 al valor deseado).
– La sustracción de fondo se inicializa con el primer frame. Para re‑calibrar el fondo en escena vacía, usa el comando “b”.

Script auxiliar (opcional) para registrar conteos desde el host

Puedes usar este pequeño script en Python 3.11 para registrar las líneas “CNT UP=… DOWN=… TOTAL=…” en un CSV. Ajusta el puerto.

# tools/serial_logger.py
import serial, time, re, csv, sys

PORT = sys.argv[1] if len(sys.argv) > 1 else "/dev/ttyACM0"
BAUD = 115200
PAT  = re.compile(r"CNT UP=(\d+)\s+DOWN=(\d+)\s+TOTAL=(\d+)")

with serial.Serial(PORT, BAUD, timeout=1) as ser, open("counts_log.csv", "w", newline="") as f:
    writer = csv.writer(f)
    writer.writerow(["timestamp_ms", "up", "down", "total"])
    t0 = time.time()
    # vaciar buffer inicial
    time.sleep(0.5)
    ser.reset_input_buffer()
    print("Escuchando en", PORT)
    while True:
        line = ser.readline().decode(errors="ignore").strip()
        if not line:
            continue
        m = PAT.search(line)
        if m:
            now_ms = int((time.time() - t0) * 1000)
            up, down, total = m.groups()
            writer.writerow([now_ms, up, down, total])
            print(now_ms, up, down, total)

Ejecuta:
– Linux/macOS: python3 tools/serial_logger.py /dev/ttyACM0
– Windows: py tools/serial_logger.py COMx

Compilación/flash/ejecución

A continuación los comandos exactos con Arduino CLI 0.35.3 y el core mbed_portenta 4.2.1. Sustituye el puerto por el tuyo.

Instalación de Arduino CLI (si no lo tienes)

  • Windows: usa el instalador MSI desde releases de Arduino CLI 0.35.3, o winget:
  • winget install Arduino.ArduinoCLI –version 0.35.3
  • macOS (Homebrew):
  • brew install arduino-cli@0.35.3
  • Linux (x86_64, tarball oficial):
  • curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=$HOME/.local/bin sh -s 0.35.3
  • Asegura que ~/.local/bin está en tu PATH.

Verifica:
– arduino-cli version
– Debe devolver arduino-cli Version: 0.35.3

Preparar el entorno de Portenta H7

  • arduino-cli core update-index
  • arduino-cli core install arduino:mbed_portenta@4.2.1
  • arduino-cli lib install «Arduino_HM01B0@1.0.4»
  • (opcional) arduino-cli lib install «Arduino_DebugUtils@1.4.0»

Lista de placas conectadas:
– arduino-cli board list

Deberías ver algo como:
– Portenta H7 at /dev/ttyACM0 FQBN: arduino:mbed_portenta:envie_m7 (si no aparece FQBN, no pasa nada, lo especificaremos en compile/upload)

Compilar

Asumiendo que guardaste el sketch como camera-edge-people-counting/camera-edge-people-counting.ino:

  • arduino-cli compile –fqbn arduino:mbed_portenta:envie_m7 camera-edge-people-counting

Para activar optimizaciones extra (opcional):
– arduino-cli compile –fqbn arduino:mbed_portenta:envie_m7 –build-property compiler.cpp.extra_flags=»-O3 -DNDEBUG» camera-edge-people-counting

Subir (flash)

Conecta el Portenta H7 en modo normal. Si tienes problemas para subir, presiona dos veces el botón reset para forzar el bootloader.

  • Linux/macOS:
  • arduino-cli upload -p /dev/ttyACM0 –fqbn arduino:mbed_portenta:envie_m7 camera-edge-people-counting
  • Windows:
  • arduino-cli upload -p COMx –fqbn arduino:mbed_portenta:envie_m7 camera-edge-people-counting

Ejecución y monitor serie

  • arduino-cli monitor -p /dev/ttyACM0 -c baudrate=115200
  • Windows: arduino-cli monitor -p COMx -c baudrate=115200

Comandos disponibles durante la ejecución:
– r — Reset de contadores y tracks.
– b — Recalibrar fondo con el frame actual (hazlo cuando no haya personas en escena).
– t — Alternar logs detallados.
– L — Cambiar la línea virtual, por ejemplo: L180 seguido de Enter.

Validación paso a paso

1) Verificación de arranque:
– Observa parpadeo breve del LED integrado al iniciar.
– En el monitor serie, ver líneas:
– camera-edge-people-counting (Portenta H7 + Vision Shield HM01B0)
– Inicializando camara…
– [OK] Setup completado. Comandos: …

2) Calibración del fondo:
– Asegura que la escena esté vacía (sin personas en movimiento) durante 2–3 segundos.
– Opcional: envía “b” para forzar recalibración.
– Espera 1–2 segundos.

3) Comprobación de flujo de telemetría:
– Debes ver cada ~0.5 s líneas como:
– CNT UP=0 DOWN=0 TOTAL=0
– Si activas “t”, se imprimirán blobs detectados. Con escena estática, Blobs=0.

4) Prueba con una persona cruzando la línea virtual:
– Pide a un compañero que cruce la escena desde arriba hacia abajo respecto a la línea (si la cámara está horizontal, “arriba” es menor y).
– Debes observar:
– Aparición temporal de blobs (verbose ON).
– Un evento de conteo cuando el centroide atraviese y_line:
– CNT UP=0 DOWN=1 TOTAL=1 (si cruzó de arriba a abajo)
– El LED hará un pequeño destello en el evento.

5) Verificación inversa:
– Pide el cruce en dirección contraria (de abajo hacia arriba).
– Debe incrementarse UP.

6) Ajuste de línea virtual:
– Envía “L220” (por ejemplo) y verifica que y_line=220 se imprima.
– Repite la prueba y observa el cambio en sensibilidad según la altura de la línea.

7) Registro con script Python (opcional):
– Ejecuta: python3 tools/serial_logger.py /dev/ttyACM0
– Verifica que se crea counts_log.csv y se van añadiendo filas con up/down/total.

8) Condiciones desafiantes:
– Cambia ligeramente la iluminación (enciende/apaga una lámpara). El EMA debería adaptarse en unos segundos. Si hay falsos positivos, recalibra con “b”.
– Prueba con dos personas separadas por ~50–100 px. Observa si los blobs se mantienen distinguidos. Si se fusionan, reduce DILATE_ITERS o baja THRESH_HIGH para delimitar mejor.

Resultados esperados:
– Conteo robusto con una persona a la vez, y razonable con dos personas si hay separación.
– Falsos positivos bajos en escena estática estable.

Troubleshooting

1) No se detecta el Portenta H7 en la CLI
– Síntomas: arduino-cli board list no muestra el puerto; upload falla.
– Causas/soluciones:
– Cable USB solo carga: usa cable de datos.
– Driver en Windows: permite “Dispositivo USB serial (CDC ACM)” por defecto; si falla, reinstala controladores.
– Forzar bootloader: presiona dos veces reset y reintenta upload.
– Usa otro puerto USB o evita hubs pasivos.

2) Error al instalar core arduino:mbed_portenta@4.2.1
– Síntomas: core install falla.
– Soluciones:
– arduino-cli core update-index
– Verifica conexión a Internet y proxy.
– Prueba otra versión cercana compatible (p. ej. 4.2.0) y ajusta comandos.

3) La librería Arduino_HM01B0 no compila o sus APIs difieren
– Síntomas: errores por métodos faltantes (begin/readFrame).
– Soluciones:
– Confirma versión: arduino-cli lib list | grep HM01B0
– Instala la versión indicada: arduino-cli lib install «Arduino_HM01B0@1.0.4»
– Si tu versión expone otra API (p. ej., begin() sin parámetros y setFrameSize()), adapta:
– himax.begin(); himax.setFrameSize(320,320); himax.grab(frame) o himax.readFrame(frame).

4) Imagen excesivamente ruidosa o muchos falsos positivos
– Síntomas: Blobs aparecen sin personas.
– Soluciones:
– Aumenta THRESH_LOW/HIGH (p. ej. 24/40).
– Reduce DILATE_ITERS o añade otra erosión.
– Usa “b” para recalibrar fondo sin movimiento.
– Evita superficies reflectantes y cambios bruscos de luz.

5) No aparecen blobs aunque haya personas
– Síntomas: CNT no cambia, verbose muestra Blobs=0.
– Soluciones:
– Baja umbrales (THRESH_LOW/HIGH).
– Ajusta MIN_BLOB_AREA (si estás lejos, el área proyectada es pequeña).
– Verifica que la persona pase por la región de la línea virtual o acerca la cámara.

6) Conteos dobles (reconteo del mismo individuo)
– Síntomas: UP o DOWN suben dos veces por un único cruce.
– Soluciones:
– Incrementa ASSIGN_DIST_THRESH (p. ej. 60) para mantener asignación estable.
– Aumenta MAX_MISSES si hay oclusiones cortas.
– Ubica la línea lejos de bordes donde el tracking pierde contexto.

7) Memoria insuficiente al compilar con resoluciones mayores
– Síntomas: “not enough memory” o comportamiento errático.
– Soluciones:
– Reduce resolución de CAM_W/CAM_H y adapta begin() si lo soporta.
– Elimina el buffer labelMap si limitas a un número de blobs con un detector más simple (p. ej., proyección horizontal/vertical).
– Compila con -Os (optimización por tamaño) si fuera necesario.

8) El LED no parpadea aunque el conteo cambia
– Síntomas: CNT cambia, pero LED fijo.
– Solución:
– Verifica que LED_BUILTIN esté definido en el core mbed_portenta (lo está). Si no, reemplaza por PIN_LED o por un digitalWrite a un GPIO expuesto si añadiste un LED externo.

Mejoras/variantes

  • Publicación por red:
  • Si tu Vision Shield es la variante Ethernet, puedes publicar los conteos vía MQTT/HTTP a un broker o servidor. Añade la librería Ethernet del Portenta Vision Shield y envía CNT cada N segundos.
  • Persistencia en microSD:
  • Algunas variantes del Vision Shield incluyen microSD. Registra cada evento de cruce en un CSV en la tarjeta para auditoría offline.
  • Región de interés (ROI) y máscara:
  • Aplica una máscara fija para ignorar áreas con reflejos o ventanas. Genera un array ROI_MASK[CAM_W*CAM_H] y “anula” píxeles fuera de ROI en computeForeground().
  • Downsampling y pirámide:
  • Para mayor rendimiento, haz submuestreo 2× (160×160) antes de morfología y etiquetado. Ajusta constants y y_line.
  • Filtro de velocidad:
  • Rechaza tracks con velocidad no humana (demasiado rápida/lenta) usando derivadas en stepTracking().
  • TinyML (detección semántica):
  • Sustituye sustracción de fondo por un modelo de “person detection” (TinyML, TFLite Micro) que entregue bounding boxes o probabilidad + CAM/heatmap, mejorando robustez ante iluminación. Procura modelos cuantizados int8 y entradas 96×96 para ajustarse a RAM/tiempo.
  • Doble línea y dirección neta:
  • Usa dos líneas (y_line1 < y_line2) y cuenta un cruce solo si se cumple la secuencia (para reducir rebotes de conteo).
  • Sincronización M7/M4:
  • Explora offload de tareas ligeras al M4 (p. ej., I/O) mientras el M7 procesa visión, usando RPC internos (avanzado).

Checklist de verificación

  • [ ] He instalado Arduino CLI 0.35.3 y verifiqué arduino-cli version.
  • [ ] He instalado el core arduino:mbed_portenta@4.2.1 con core update-index + core install.
  • [ ] He instalado la librería Arduino_HM01B0@1.0.4.
  • [ ] He ensamblado correctamente el Portenta H7 con el Portenta Vision Shield (Himax HM01B0) y conectado por USB‑C.
  • [ ] arduino-cli board list muestra el puerto (COMx o /dev/ttyACM0).
  • [ ] El sketch compila con: arduino-cli compile –fqbn arduino:mbed_portenta:envie_m7.
  • [ ] He subido el firmware con arduino-cli upload -p –fqbn arduino:mbed_portenta:envie_m7.
  • [ ] El monitor serie a 115200 baudios muestra CNT y acepta comandos (r, b, t, L).
  • [ ] Con escena vacía, recalibré fondo con “b” y CNT permanece en 0.
  • [ ] Al cruzar la línea desde arriba hacia abajo, aumenta DOWN.
  • [ ] Al cruzar la línea desde abajo hacia arriba, aumenta UP.
  • [ ] He ajustado y_line, umbrales y áreas para mi escena hasta obtener estabilidad.
  • [ ] (Opcional) He registrado datos con el script Python y verificado el CSV.

Con esto cierras un caso práctico completo y reproducible de camera-edge-people-counting con Arduino Portenta H7 + Portenta Vision Shield (Himax HM01B0) usando exclusivamente procesamiento en el borde, sin depender de la nube ni de un PC externo en tiempo de ejecución.

Encuentra este producto y/o libros sobre este tema en Amazon

Ir a Amazon

Como afiliado de Amazon, gano con las compras que cumplan los requisitos. Si compras a través de este enlace, ayudas a mantener este proyecto.

Quiz rápido

Pregunta 1: ¿Qué sistema operativo se recomienda para implementar el pipeline de visión embebida?




Pregunta 2: ¿Cuál es la versión de Arduino CLI indicada para este proyecto?




Pregunta 3: ¿Qué tipo de cámara se utiliza en el Portenta Vision Shield?




Pregunta 4: ¿Qué lenguaje de programación se utiliza para implementar el pipeline?




Pregunta 5: ¿Cuál es la resolución típica de la cámara Himax HM01B0?




Pregunta 6: ¿Qué librería se utiliza para la captura desde la cámara Himax HM01B0?




Pregunta 7: ¿Qué tipo de soporte se recomienda para la cámara?




Pregunta 8: ¿Qué versión del core de placas se debe usar para el Portenta H7?




Pregunta 9: ¿Qué conocimiento previo se requiere para este proyecto?




Pregunta 10: ¿Qué se recomienda hacer si ya tienes Arduino CLI en otra versión?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Ingeniero Superior en Electrónica de Telecomunicaciones e Ingeniero en Informática (titulaciones oficiales en España).

Sígueme:


Caso práctico: Control PID de fermentación RS485 y Ethernet

Caso práctico: Control PID de fermentación RS485 y Ethernet — hero

Objetivo y caso de uso

Qué construirás: Un controlador de temperatura para fermentación utilizando Arduino Mega 2560, RS485 y Ethernet para la comunicación.

Para qué sirve

  • Controlar la temperatura de fermentación en tiempo real mediante un sensor PT100.
  • Transmitir datos de temperatura a través de RS485 para monitoreo a larga distancia.
  • Acceder a los datos de fermentación desde una interfaz web usando Ethernet.
  • Integrar alertas de temperatura fuera de rango mediante MQTT.

Resultado esperado

  • Latencia de respuesta del sistema de control menor a 200 ms.
  • Precisión de temperatura de ±0.5 °C en mediciones.
  • Capacidad de enviar datos a un servidor MQTT a una tasa de 1 paquete cada 5 segundos.
  • Monitoreo de temperatura con un rango de 0 a 100 °C.

Público objetivo: Ingenieros y desarrolladores de sistemas embebidos; Nivel: Avanzado

Arquitectura/flujo: Sensor PT100 → MAX31865 → Arduino Mega → RS485/Ethernet → Interfaz web/MQTT.

Nivel: Avanzado

Prerrequisitos

Sistema operativo y entorno recomendado

  • Linux: Ubuntu 22.04 LTS (jammy) 64-bit
  • Windows (alternativo): Windows 11 Pro 22H2 o superior
  • Red local con DHCP deshabilitado para la IP que asignaremos, o reserva DHCP si prefieres

Toolchain exacta

  • Arduino CLI 0.35.3
  • Núcleo (core) AVR: arduino:avr@1.8.6
  • Librerías (vía Library Manager):
  • Ethernet@2.0.2 (compatible con W5500)
  • Adafruit MAX31865 library@1.5.0
  • Adafruit BusIO@1.14.1
  • PID by Brett Beauregard@1.2.1

Notas:
– Usaremos estrictamente Arduino CLI (no GUI).
– FQBN: arduino:avr:mega (para Arduino Mega 2560).
– Velocidad de monitor serie: 115200 baudios.

Materiales

  • Arduino Mega 2560 R3 (ATmega2560-16AU).
  • W5500 Ethernet Shield (formato Arduino R3; SPI por cabecera ICSP, con CS en D10).
  • Módulo MAX485 RS485 TTL (5 V, con resistencias de polarización y terminación con jumpers).
  • Conversor RTD MAX31865 (placa “MAX31865 RTD” tipo Adafruit o compatible, para PT100 3 hilos).
  • Sonda RTD PT100 de 3 hilos (clase A o B; inmersión para líquidos).
  • Convertidor de nivel lógico bidireccional 4 canales para SPI (5 V ↔ 3.3 V). Importante: el MAX31865 típico trabaja a 3.3 V y no es tolerante a 5 V.
  • Cables Dupont, regleta o protoboard.
  • Fuente para actuador térmico:
  • Opción segura de laboratorio: pad calefactor de silicona 12 V y SSR DC-DC (entrada 3–32 VDC, salida 12 VDC, ≥10 A) o MOSFET DC.
  • Evita cargas de red/mains en prácticas de aula; si las usas, sigue normativa y aislamiento adecuados.
  • Relé de estado sólido (SSR) DC-DC para el pad calefactor (controlado por señal de 5 V).
  • Dongle USB–RS485 para el PC (validación del bus RS485).
  • Cable Ethernet CAT5e/CAT6.
  • Cable USB para el Mega.
  • Resistencias de terminación/bias si el módulo MAX485 no las incorpora (típicamente 120 Ω en extremos del bus; polarización con ~680 Ω–1 kΩ a Vcc y GND, aunque muchos módulos ya las incluyen).

Preparación y conexión

Consideraciones de bus y alimentación

  • El MAX485 funciona a 5 V (lado TTL) y con A/B diferencial al bus RS485. Asegura:
  • Terminación de 120 Ω SOLO en los extremos del bus.
  • Polarización (bias) de reposo en uno de los extremos, si no lo provee tu módulo.
  • El MAX31865, en su mayoría de placas, funciona a 3.3 V y NO tolera 5 V en las líneas SPI:
  • Alimenta el MAX31865 con 3.3 V desde el Mega (consumo típico bajo).
  • Usa conversión de nivel 5 V → 3.3 V para SCK, MOSI, CS y DRDY. MISO (3.3 V) hacia el Mega suele ser reconocido como alto; aun así, puedes pasarlo por el conversor para homogeneizar.

Asignación de pines y cableado

  • El W5500 usa SPI por ICSP en el shield y CS en D10 (no modificar). La SD del shield (si existe) suele usar D4; mantenla deshabilitada (D4 como salida en HIGH).
  • SPI adicional para el MAX31865 compartido (MISO/MOSI/SCK) con CS independiente (D9).

Tabla de pines y puertos

Subsistema Señal Arduino Mega 2560 Módulo Notas
Ethernet (W5500 Shield) SPI (MISO/MOSI/SCK) ICSP 6 pines W5500 El shield hace el ruteo por ICSP
Ethernet (W5500 Shield) CS D10 W5500 Reservado para W5500
SD (si presente en shield) CS D4 SD Poner D4 OUTPUT HIGH para deshabilitar
MAX31865 3.3 V 3.3 V VCC No usar 5 V
MAX31865 GND GND GND Común con Mega
MAX31865 SCK 52 (vía LV Shifter) SCLK 5 V → 3.3 V
MAX31865 MOSI 51 (vía LV Shifter) SDI 5 V → 3.3 V
MAX31865 MISO 50 (opcional LV Shifter) SDO 3.3 V → 5 V (aceptado)
MAX31865 CS D9 (vía LV Shifter) CS 5 V → 3.3 V
MAX31865 DRDY D48 (vía LV Shifter) DRDY 5 V → 3.3 V
RTD PT100 3 hilos A/B/C según manual MAX31865 Configurar jumpers para 3 hilos
RS485 (MAX485) VCC 5 V VCC 5 V
RS485 (MAX485) GND GND GND Común con Mega
RS485 (MAX485) DI D16 (TX2) DI UART2 TX
RS485 (MAX485) RO D17 (RX2) RO UART2 RX
RS485 (MAX485) DE y RE D22 (juntos) DE/RE Alto: transmitir; Bajo: recibir
RS485 (MAX485) A/B Bus A/B Diferencial, cable trenzado
Actuador térmico (SSR DC-DC) IN+ D6 SSR IN+ Control por PWM de periodo-ventana
Actuador térmico (SSR DC-DC) IN- GND SSR IN- Retorno a GND

Puntos clave:
– Asegura que D9 (CS MAX31865) y D10 (CS W5500) estén en HIGH por defecto; solo el dispositivo activo debe tener CS LOW.
– Configura el MAX31865 para 3 hilos (cambia el jumper/cableado según la placa).
– En el bus RS485, habilita terminación solo si estás en extremo; caso contrario, desactívala.

Código completo (Arduino C++ para Mega 2560)

El siguiente sketch implementa:
– Lectura de temperatura PT100 vía MAX31865 (SPI).
– Control PID de una resistencia calefactora mediante “time-proportional output” en ventana de 2 s.
– Servidor HTTP simple sobre W5500 con endpoints / y /status, y cambio de setpoint vía /set?sp=XX.X.
– Interfaz RS485 (Serial2) modo esclavo con protocolo ASCII simple: GET PV|SP|OUT|ALL, SET SP x.y, MODE AUTO|MAN, SET MAN x.

Explicación rápida:
– setup(): inicializa SPI, Ethernet, MAX31865, PID, RS485.
– loop(): lee temperatura, ejecuta PID, conmuta SSR, atiende RS485 y HTTP.
– RS485: línea por línea con ‘
‘; respuesta tras elevar DE/RE.
– Seguridad: si hay fallo del MAX31865, desactiva salida y reporta FAULT.

Crea la carpeta rs485-fermentation-pid-control y dentro el archivo rs485-fermentation-pid-control.ino con este contenido:

/*
  rs485-fermentation-pid-control
  Dispositivo: Arduino Mega 2560 + W5500 Ethernet Shield + MAX485 RS485 + MAX31865 RTD
  Toolchain:
    - Arduino CLI 0.35.3
    - Core arduino:avr@1.8.6
    - Ethernet@2.0.2
    - Adafruit MAX31865 library@1.5.0 (+ Adafruit BusIO@1.14.1)
    - PID@1.2.1
*/

#include <SPI.h>
#include <Ethernet.h>
#include <PID_v1.h>
#include <Adafruit_MAX31865.h>

// ---------------------- Configuración de hardware ----------------------
// W5500: CS en pin 10 (mantenido por el shield)
const uint8_t PIN_CS_ETH = 10;
const uint8_t PIN_CS_SD  = 4;   // si hay SD en el shield

// MAX31865 (SPI compartido)
const uint8_t PIN_CS_MAX31865 = 9;
const uint8_t PIN_DRDY        = 48; // opcional, pero útil

// RS485 con MAX485 en Serial2
const uint8_t PIN_RS485_DE_RE = 22; // DE y RE unidos
#define RS485_SERIAL Serial2

// SSR de calefacción (control por ventana temporal)
const uint8_t PIN_SSR = 6;

// Parámetros RTD
#define RREF      430.0   // resistencia de referencia (ver tu placa)
#define RNOMINAL  100.0   // PT100 nominal a 0 °C

// Ethernet (MAC e IP fijas)
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x01 };
IPAddress ip(192, 168, 1, 77);
IPAddress dns(192, 168, 1, 1);
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 255, 0);
EthernetServer server(80);

// MAX31865 objeto (3 hilos)
Adafruit_MAX31865 max31865 = Adafruit_MAX31865(PIN_CS_MAX31865);

// PID
double PV = 0.0;          // Process Variable (temperatura)
double SP = 20.0;         // Setpoint (°C)
double OUT = 0.0;         // Salida PID (0-100 %)
double MAN = 0.0;         // Mando manual (0-100 %)
PID pid(&PV, &OUT, &SP, 8.0, 0.6, 20.0, DIRECT); // Kp, Ki, Kd iniciales (ajustar en tu planta)

// Control por ventana
const unsigned long WINDOW_MS = 2000UL; // 2 s
unsigned long windowStart = 0;

// Filtros
const float alpha = 0.2f; // EMA para suavizar lectura
bool firstRead = true;

// RS485 buffer
static const size_t RS485_BUFSZ = 128;
char rs485_buf[RS485_BUFSZ];
size_t rs485_len = 0;

// Estado
enum Mode { MODE_AUTO, MODE_MANUAL };
Mode mode = MODE_AUTO;
bool max_fault = false;

// ---------------------- Utilidades ----------------------
void rs485SetTX(bool tx) {
  digitalWrite(PIN_RS485_DE_RE, tx ? HIGH : LOW);
}

void rs485SendLine(const char* s) {
  rs485SetTX(true);
  RS485_SERIAL.print(s);
  RS485_SERIAL.print("\r\n");
  RS485_SERIAL.flush();
  rs485SetTX(false);
}

void httpSendHeaderOK(EthernetClient& client, const char* mime) {
  client.println("HTTP/1.1 200 OK");
  client.print("Content-Type: ");
  client.println(mime);
  client.println("Connection: close");
  client.println();
}

void httpSend404(EthernetClient& client) {
  client.println("HTTP/1.1 404 Not Found");
  client.println("Connection: close");
  client.println();
  client.println("404");
}

String urlDecode(const String& s) {
  String r;
  for (uint16_t i = 0; i < s.length(); i++) {
    char c = s[i];
    if (c == '+') r += ' ';
    else if (c == '%' && i + 2 < s.length()) {
      char h1 = s[i+1], h2 = s[i+2];
      auto hex = [&](char h)->int { if (h>='0'&&h<='9') return h-'0'; if (h>='A'&&h<='F') return h-'A'+10; if (h>='a'&&h<='f') return h-'a'+10; return 0; };
      r += char((hex(h1) << 4) | hex(h2));
      i += 2;
    } else r += c;
  }
  return r;
}

// ---------------------- Inicialización ----------------------
void setup() {
  pinMode(PIN_SSR, OUTPUT);
  digitalWrite(PIN_SSR, LOW);

  pinMode(PIN_RS485_DE_RE, OUTPUT);
  rs485SetTX(false);

  pinMode(PIN_CS_ETH, OUTPUT);
  digitalWrite(PIN_CS_ETH, HIGH);
  pinMode(PIN_CS_SD, OUTPUT);     // deshabilita SD
  digitalWrite(PIN_CS_SD, HIGH);

  pinMode(PIN_CS_MAX31865, OUTPUT);
  digitalWrite(PIN_CS_MAX31865, HIGH);

  pinMode(PIN_DRDY, INPUT_PULLUP);

  Serial.begin(115200);
  RS485_SERIAL.begin(9600); // 8N1 por defecto

  // MAX31865
  if (!max31865.begin(MAX31865_3WIRE)) {
    Serial.println(F("[MAX31865] Error al inicializar (verifica conexiones y 3 hilos)."));
  }
  max31865.enable50Hz(true); // Rechazo 50 Hz (ajusta a tu red)

  // Ethernet
  Ethernet.init(PIN_CS_ETH);
  Ethernet.begin(mac, ip, dns, gateway, subnet);
  delay(1000);
  server.begin();
  Serial.print(F("[ETH] IP: ")); Serial.println(Ethernet.localIP());

  // PID
  pid.SetOutputLimits(0.0, 100.0); // 0-100 %
  pid.SetSampleTime(1000);         // 1 s
  pid.SetMode(AUTOMATIC);

  windowStart = millis();
  Serial.println(F("[BOOT] rs485-fermentation-pid-control listo."));
}

// ---------------------- Lectura de temperatura ----------------------
void readTemperature() {
  // Lectura y chequeo de fallos
  uint8_t fault = max31865.readFault();
  if (fault) {
    max_fault = true;
    Serial.print(F("[MAX31865] FAULT: 0x"));
    Serial.println(fault, HEX);
    max31865.clearFault();
    return;
  } else {
    max_fault = false;
  }

  float t = max31865.temperature(RNOMINAL, RREF);
  if (isnan(t) || t < -50.0 || t > 250.0) {
    max_fault = true;
    return;
  }

  if (firstRead) {
    PV = t;
    firstRead = false;
  } else {
    PV = alpha * t + (1.0f - alpha) * PV; // filtro EMA
  }
}

// ---------------------- Control de salida (SSR) ----------------------
void driveOutput() {
  unsigned long now = millis();
  if (now - windowStart >= WINDOW_MS) {
    windowStart += WINDOW_MS;
  }

  double duty = (mode == MODE_AUTO) ? OUT : MAN;
  if (max_fault) duty = 0.0; // seguridad

  unsigned long onTime = (unsigned long)( (duty / 100.0) * WINDOW_MS );
  if ((now - windowStart) < onTime) {
    digitalWrite(PIN_SSR, HIGH);
  } else {
    digitalWrite(PIN_SSR, LOW);
  }
}

// ---------------------- Protocolo RS485 ASCII ----------------------
void processRS485Line(char* line) {
  // Elimina CR/LF finales
  size_t n = strlen(line);
  while (n > 0 && (line[n-1] == '\r' || line[n-1] == '\n' || line[n-1] == ' ')) { line[--n] = 0; }
  // Convierte a mayúsculas para comparar (mantén valores numéricos)
  for (size_t i = 0; i < n; i++) if (line[i] >= 'a' && line[i] <= 'z') line[i] -= 32;

  if (strncmp(line, "GET ", 4) == 0) {
    const char* what = line + 4;
    if (strcmp(what, "PV") == 0) {
      char out[32]; snprintf(out, sizeof(out), "PV:%.2f", PV);
      rs485SendLine(out);
    } else if (strcmp(what, "SP") == 0) {
      char out[32]; snprintf(out, sizeof(out), "SP:%.2f", SP);
      rs485SendLine(out);
    } else if (strcmp(what, "OUT") == 0) {
      char out[32]; snprintf(out, sizeof(out), "OUT:%.1f MODE:%s FAULT:%d", (mode==MODE_AUTO?OUT:MAN), (mode==MODE_AUTO?"AUTO":"MAN"), max_fault?1:0);
      rs485SendLine(out);
    } else if (strcmp(what, "ALL") == 0) {
      char out[96];
      snprintf(out, sizeof(out), "PV:%.2f SP:%.2f OUT:%.1f MODE:%s FAULT:%d", PV, SP, (mode==MODE_AUTO?OUT:MAN), (mode==MODE_AUTO?"AUTO":"MAN"), max_fault?1:0);
      rs485SendLine(out);
    } else {
      rs485SendLine("ERR:UNKNOWN_GET");
    }
  } else if (strncmp(line, "SET ", 4) == 0) {
    const char* rest = line + 4;
    if (strncmp(rest, "SP ", 3) == 0) {
      double v = atof(rest + 3);
      if (v >= -10.0 && v <= 40.0) { // típico rango de fermentación
        SP = v;
        rs485SendLine("OK");
      } else {
        rs485SendLine("ERR:SP_RANGE");
      }
    } else if (strncmp(rest, "MAN ", 4) == 0) {
      double v = atof(rest + 4);
      if (v >= 0.0 && v <= 100.0) {
        MAN = v;
        rs485SendLine("OK");
      } else {
        rs485SendLine("ERR:MAN_RANGE");
      }
    } else {
      rs485SendLine("ERR:UNKNOWN_SET");
    }
  } else if (strncmp(line, "MODE ", 5) == 0) {
    const char* m = line + 5;
    if (strcmp(m, "AUTO") == 0) {
      mode = MODE_AUTO;
      pid.SetMode(AUTOMATIC);
      rs485SendLine("OK");
    } else if (strcmp(m, "MAN") == 0 || strcmp(m, "MANUAL") == 0) {
      mode = MODE_MANUAL;
      pid.SetMode(MANUAL);
      rs485SendLine("OK");
    } else {
      rs485SendLine("ERR:UNKNOWN_MODE");
    }
  } else {
    rs485SendLine("ERR:UNKNOWN_CMD");
  }
}

void pollRS485() {
  while (RS485_SERIAL.available()) {
    char c = (char)RS485_SERIAL.read();
    if (c == '\n') {
      rs485_buf[rs485_len] = 0;
      processRS485Line(rs485_buf);
      rs485_len = 0;
    } else if (rs485_len + 1 < RS485_BUFSZ) {
      rs485_buf[rs485_len++] = c;
    } else {
      rs485_len = 0; // overflow -> reset
    }
  }
}

// ---------------------- HTTP server ----------------------
void handleHttpClient(EthernetClient& client) {
  // Lee la primera línea del request
  String req = client.readStringUntil('\r');
  client.readStringUntil('\n'); // consume LF
  if (req.length() == 0) return;

  // Muy simple: parsea método y ruta
  int sp1 = req.indexOf(' ');
  int sp2 = req.indexOf(' ', sp1 + 1);
  if (sp1 < 0 || sp2 < 0) { httpSend404(client); return; }
  String method = req.substring(0, sp1);
  String path = req.substring(sp1 + 1, sp2);

  // Lee y descarta cabeceras restantes hasta línea vacía
  while (true) {
    String h = client.readStringUntil('\r');
    client.readStringUntil('\n');
    if (h.length() == 0) break;
  }

  if (method != "GET") {
    httpSend404(client);
    return;
  }

  if (path == "/" || path.startsWith("/index")) {
    httpSendHeaderOK(client, "text/html; charset=utf-8");
    client.println("<!DOCTYPE html><html><head><meta charset='utf-8'><title>Fermentation PID</title></head><body>");
    client.println("<h1>Fermentation PID (RS485 + W5500)</h1>");
    client.print("<p>PV: "); client.print(PV, 2); client.println(" &deg;C</p>");
    client.print("<p>SP: "); client.print(SP, 2); client.println(" &deg;C</p>");
    client.print("<p>OUT: "); client.print((mode==MODE_AUTO?OUT:MAN), 1); client.println(" %</p>");
    client.print("<p>MODE: "); client.print((mode==MODE_AUTO?"AUTO":"MAN")); client.println("</p>");
    client.print("<p>FAULT: "); client.print(max_fault ? "YES" : "NO"); client.println("</p>");
    client.println("<p>Endpoints: <code>/status</code>, <code>/set?sp=XX.X</code>, <code>/mode?m=AUTO|MAN</code>, <code>/man?v=0..100</code></p>");
    client.println("</body></html>");
    return;
  }

  if (path.startsWith("/status")) {
    httpSendHeaderOK(client, "text/plain; charset=utf-8");
    client.print("PV:"); client.println(PV, 2);
    client.print("SP:"); client.println(SP, 2);
    client.print("OUT:"); client.println((mode==MODE_AUTO?OUT:MAN), 1);
    client.print("MODE:"); client.println((mode==MODE_AUTO?"AUTO":"MAN"));
    client.print("FAULT:"); client.println(max_fault ? 1 : 0);
    return;
  }

  if (path.startsWith("/set")) {
    int q = path.indexOf('?');
    if (q >= 0) {
      String qs = path.substring(q + 1);
      if (qs.startsWith("sp=")) {
        String v = urlDecode(qs.substring(3));
        double spv = v.toFloat();
        if (spv >= -10.0 && spv <= 40.0) {
          SP = spv;
          httpSendHeaderOK(client, "text/plain; charset=utf-8");
          client.println("OK");
          return;
        }
      }
    }
    httpSend404(client);
    return;
  }

  if (path.startsWith("/mode")) {
    int q = path.indexOf('?');
    if (q >= 0) {
      String qs = path.substring(q + 1);
      if (qs.startsWith("m=")) {
        String m = urlDecode(qs.substring(2));
        if (m == "AUTO") {
          mode = MODE_AUTO;
          pid.SetMode(AUTOMATIC);
          httpSendHeaderOK(client, "text/plain; charset=utf-8");
          client.println("OK");
          return;
        } else if (m == "MAN") {
          mode = MODE_MANUAL;
          pid.SetMode(MANUAL);
          httpSendHeaderOK(client, "text/plain; charset=utf-8");
          client.println("OK");
          return;
        }
      }
    }
    httpSend404(client);
    return;
  }

  if (path.startsWith("/man")) {
    int q = path.indexOf('?');
    if (q >= 0) {
      String qs = path.substring(q + 1);
      if (qs.startsWith("v=")) {
        String s = urlDecode(qs.substring(2));
        double v = s.toFloat();
        if (v >= 0.0 && v <= 100.0) {
          MAN = v;
          httpSendHeaderOK(client, "text/plain; charset=utf-8");
          client.println("OK");
          return;
        }
      }
    }
    httpSend404(client);
    return;
  }

  httpSend404(client);
}

// ---------------------- Bucle principal ----------------------
void loop() {
  // 1) Medición
  readTemperature();

  // 2) Control
  if (!max_fault && mode == MODE_AUTO) {
    pid.Compute();
  }

  // 3) Actuación
  driveOutput();

  // 4) Comunicación RS485
  pollRS485();

  // 5) HTTP
  EthernetClient client = server.available();
  if (client) {
    handleHttpClient(client);
    delay(1);
    client.stop();
  }

  // 6) Debug periódico
  static unsigned long t0 = 0;
  unsigned long now = millis();
  if (now - t0 >= 5000) {
    t0 = now;
    Serial.print(F("[DBG] PV=")); Serial.print(PV, 2);
    Serial.print(F(" SP=")); Serial.print(SP, 2);
    Serial.print(F(" OUT=")); Serial.print((mode==MODE_AUTO?OUT:MAN), 1);
    Serial.print(F(" MODE=")); Serial.print((mode==MODE_AUTO?"AUTO":"MAN"));
    Serial.print(F(" FAULT=")); Serial.println(max_fault ? 1 : 0);
  }
}

Puntos clave a revisar en el código:
– MAX31865: ajusta RREF a tu placa (430 Ω es común en Adafruit). RNOMINAL=100 (PT100).
– Filtro: suaviza lecturas (EMA).
– PID: coeficientes iniciales para un proceso térmico lento; debes afinarlos en tu sistema.
– Salida: estrategia de ventana fija para controlar SSR DC (recomendado frente a PWM de alta frecuencia).
– RS485: protocolo ASCII minimalista, fácil de probar con un adaptador USB–RS485.
– HTTP: endpoints planos para integrar rápidamente con cURL o scripts.

Compilación, “flash” y ejecución

Instalación de Arduino CLI y core

Linux (Ubuntu 22.04):

# 1) Instalar Arduino CLI 0.35.3 en ~/bin
mkdir -p ~/bin
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh -s -- -b ~/bin 0.35.3
export PATH="$HOME/bin:$PATH"

# 2) Verificar versión
arduino-cli version
# Debería mostrar: arduino-cli Version: 0.35.3

# 3) Inicializar configuración (si es primera vez)
arduino-cli config init

# 4) Actualizar índice e instalar core AVR 1.8.6
arduino-cli core update-index
arduino-cli core install arduino:avr@1.8.6

# 5) Instalar librerías exactas
arduino-cli lib install "Ethernet@2.0.2" "Adafruit MAX31865 library@1.5.0" "Adafruit BusIO@1.14.1" "PID@1.2.1"

Windows (PowerShell) es similar; asegúrate de añadir arduino-cli.exe al PATH y usar las mismas versiones.

Compilación

Coloca el sketch en una carpeta con el mismo nombre del .ino:

# Estructura:
# rs485-fermentation-pid-control/
# └── rs485-fermentation-pid-control.ino

arduino-cli compile --fqbn arduino:avr:mega --warnings all --export-binaries ./rs485-fermentation-pid-control

Salida esperada: ruta a .hex en ./rs485-fermentation-pid-control/build/arduino.avr.mega/…

Subida (flash)

Identifica el puerto serie del Mega (ej.: /dev/ttyACM0 en Linux, COM5 en Windows).

# Linux
arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:avr:mega ./rs485-fermentation-pid-control

# Windows (ejemplo)
arduino-cli upload -p COM5 --fqbn arduino:avr:mega .\rs485-fermentation-pid-control

Ejecución y monitor serie

arduino-cli monitor -p /dev/ttyACM0 -c baudrate=115200

Deberías ver logs de arranque y depuración cada 5 s.

Validación paso a paso

1) Verificación de red (W5500)

  • Conecta el cable Ethernet al switch/router de tu laboratorio.
  • Ping:
  • Linux/Windows: ping 192.168.1.77
  • HTTP con cURL:
  • Estado: curl http://192.168.1.77/status
    • Debe devolver texto con PV, SP, OUT, MODE y FAULT.
  • Página: curl http://192.168.1.77/
    • Debe devolver HTML básico.
  • Cambiar setpoint: curl «http://192.168.1.77/set?sp=21.0» → OK
  • Cambiar modo: curl «http://192.168.1.77/mode?m=MAN» → OK
  • Salida manual: curl «http://192.168.1.77/man?v=25» → OK

Resultados esperados:
– /status: valores numéricos coherentes; FAULT=0; PV estable o lentamente cambiante.

2) Verificación del MAX31865 y RTD

  • A temperatura ambiente, PV ~ 18–25 °C (según sala).
  • Toca con la mano la sonda RTD: la temperatura debe subir algunos grados en ~10–20 s.
  • Si FAULT cambia a 1:
  • Revisa cableado 3 hilos, jumpers de 3-wire, continuidad de la sonda, y nivel lógico.

3) Verificación de RS485

  • Usa un adaptador USB–RS485 en el PC. Conecta A↔A, B↔B, GND común.
  • Configura terminación si tu tramo es corto y solo tienes 2 nodos: termina en ambos extremos (módulo del Mega y el dongle).
  • Abre un terminal serie a 9600 8N1 (por ejemplo, screen/minicom):
  • Linux: screen /dev/ttyUSB0 9600
  • Envía comandos:
  • GET PV
  • GET SP
  • SET SP 19.0
  • MODE MAN
  • SET MAN 30
  • GET ALL

Resultados esperados:
– Respuestas con “OK” o datos, p. ej.: PV:20.35, SP:19.00, OUT:xx.x MODE:MAN FAULT:0.

4) Validación del control PID (sin riesgo)

  • Usa un pad calefactor 12 V con SSR DC-DC y un recipiente aislado con agua (0.5–1 L).
  • Coloca la PT100 sumergida, evitando tocar directamente el pad/calefactor.
  • Modo AUTO:
  • Ajusta SP a 25.0 °C con curl o RS485.
  • Observa /status cada 30–60 s: PV debe aproximarse a SP sin oscilaciones grandes.
  • OUT tenderá a bajar conforme PV se acerque a SP.
  • Modo MAN:
  • Cambia a MAN y fija MAN=0, 20, 50, 100.
  • Observa que el SSR enciende durante una fracción de la ventana (0–100 % del tiempo de 2 s).

Criterios de éxito:
– El sistema responde a SP y mantiene PV en ±0.3–0.8 °C a entorno estable (depende de aislamiento y agitación).
– RS485 y HTTP devuelven valores consistentes con monitor serie.

Troubleshooting (5–8 errores típicos y soluciones)

1) Ethernet no responde (sin ping o /status no carga)
– Causas:
– MAC duplicada en la red.
– IP en subred distinta o conflicto de IP.
– CS de SD (D4) sin deshabilitar, interfiriendo con SPI.
– Soluciones:
– Cambia MAC a una diferente (p. ej., último byte).
– Ajusta IP/gateway/subnet a tu red.
– Asegura pinMode(4, OUTPUT) y digitalWrite(4, HIGH) para deshabilitar SD.

2) Lectura de temperatura errática o FAULT recurrente
– Causas:
– MAX31865 a 3.3 V sin conversión de nivel; SCK/MOSI/CS/DRDY recibiendo 5 V.
– Cableado 3 hilos incorrecto o jumper en modo 2/4 hilos.
– RREF no coincide con tu placa (error en cálculo).
– Soluciones:
– Usa level shifter para todas las líneas SPI y DRDY.
– Verifica configuración 3-wire en el MAX31865.
– Ajusta RREF (comúnmente 430.0 Ω en Adafruit; revisa tu módulo).

3) RS485 sin respuestas o caracteres basura
– Causas:
– A/B invertidos.
– DE/RE no controlados correctamente (siempre en RX o TX).
– Falta de terminación o exceso de terminaciones en tramo corto.
– Soluciones:
– Intercambia A/B y prueba de nuevo.
– Revisa PIN_RS485_DE_RE; debe ir HIGH al transmitir y LOW al recibir.
– Activa solo dos terminaciones (en extremos del bus).

4) El SSR no conmuta como se espera
– Causas:
– SSR incompatible (AC-AC siendo conducido con DC).
– Nivel lógico insuficiente en entrada SSR o polaridad invertida.
– Ventana de tiempo muy corta o SP/OUT en cero por FAULT.
– Soluciones:
– Usa SSR DC-DC para carga DC; verifica especificaciones.
– Comprueba D6→IN+ y GND→IN- y el LED del SSR (si lo tiene).
– Aumenta WINDOW_MS o revisa que max_fault sea 0.

5) Errores de compilación por librerías
– Causas:
– Nombres diferentes en Library Manager o versiones no coinciden.
– Soluciones:
– Reinstala con los nombres exactos: «Ethernet», «Adafruit MAX31865 library», «Adafruit BusIO», «PID».

6) Fallo al subir (upload) el sketch
– Causas:
– Puerto incorrecto o permisos (Linux).
– FQBN erróneo.
– Soluciones:
– Lista puertos: arduino-cli board list.
– Añade tu usuario a dialout: sudo usermod -aG dialout $USER; reinicia sesión.
– Usa –fqbn arduino:avr:mega.

7) Oscilaciones o sobreimpulso térmico excesivo
– Causas:
– Ganancias PID no sintonizadas para tu sistema térmico (inercia alta).
– Soluciones:
– Disminuye Kp, aumenta ligeramente Ki (cuidado con windup), ajusta Kd.
– Incrementa WINDOW_MS si la conmutación es muy rápida para tu carga.

8) Congestión SPI (W5500 y MAX31865)
– Causa:
– CS mal gestionados; dos dispositivos activos simultáneamente.
– Solución:
– Revisa que cada CS esté en HIGH salvo el dispositivo activo.
– Evita acceso concurrente en interrupciones (no usamos aquí).

Mejoras/variantes

  • Modbus RTU sobre RS485:
  • Sustituye protocolo ASCII por Modbus RTU (esclavo) usando una librería Modbus conforme. Expone holding registers: PV (scaled), SP, OUT, MODE, FAULT.
  • Registro de datos y MQTT:
  • Añade cliente MQTT por TCP en W5500 para publicar PV/SP/OUT hacia un broker local.
  • PID autotune:
  • Integra un algoritmo de autotuning (Ziegler-Nichols, Tyreus-Luyben) para obtener parámetros específicos de tu planta térmica.
  • Multipunto RS485:
  • Dirección de nodo y anti-colisión. Soporta múltiples controladores en un bus largo con direccionamiento.
  • Seguridad y enclavamientos:
  • Watchdog, corte por sobretemperatura, validación de sensor dual (RTD + termistor backup).
  • Interfaz web mejorada:
  • Página web con sliders y gráficos en tiempo real (sin depender de ArduinoJson si usas endpoints planos).
  • Redundancia de alimentación:
  • Separación de alimentación lógica y potencia para minimizar ruido en la medición.

Checklist de verificación

  • [ ] Ubuntu 22.04 o Windows 11 instalados y actualizados.
  • [ ] Arduino CLI 0.35.3 en PATH y verificado con arduino-cli version.
  • [ ] Core arduino:avr@1.8.6 instalado.
  • [ ] Librerías instaladas: Ethernet@2.0.2, Adafruit MAX31865 library@1.5.0, Adafruit BusIO@1.14.1, PID@1.2.1.
  • [ ] Cableado del W5500 Shield montado correctamente sobre el Mega (ICSP OK, CS en D10).
  • [ ] MAX31865 alimentado a 3.3 V y con conversión de nivel en SCK/MOSI/CS/DRDY.
  • [ ] RTD PT100 cableado en 3 hilos y jumpers de 3-wire configurados en el MAX31865.
  • [ ] RS485: DI→TX2 (D16), RO→RX2 (D17), DE/RE→D22, A/B al bus, terminación correcta.
  • [ ] SSR DC-DC comandado desde D6; carga de 12 V segura para pruebas.
  • [ ] Compilación exitosa con arduino-cli compile –fqbn arduino:avr:mega.
  • [ ] Subida exitosa a través del puerto correcto.
  • [ ] Respuesta a ping 192.168.1.77 y endpoints /, /status.
  • [ ] RS485 responde a GET/SET/MODE con OK/datos esperados.
  • [ ] Control PID estabiliza PV cerca de SP sin oscilaciones significativas.
  • [ ] FAULT=0 en operación normal; FAULT=1 ante desconexión de RTD y salida desactivada.

Apéndice: Script de validación RS485 (opcional)

Si prefieres automatizar la validación por RS485 desde tu PC con Python 3 y pyserial:

#!/usr/bin/env python3
import serial, time

PORT = "/dev/ttyUSB0"  # ajusta a tu dongle RS485
BAUD = 9600

def cmd(ser, s):
    ser.write((s+"\n").encode())
    ser.flush()
    line = ser.readline().decode().strip()
    print(f"> {s}\n< {line}")
    time.sleep(0.2)

with serial.Serial(PORT, BAUD, timeout=1) as ser:
    cmd(ser, "GET ALL")
    cmd(ser, "SET SP 20.0")
    cmd(ser, "GET SP")
    cmd(ser, "MODE MAN")
    cmd(ser, "SET MAN 30")
    cmd(ser, "GET OUT")
    cmd(ser, "MODE AUTO")
    cmd(ser, "GET ALL")

Con este caso práctico has desplegado un controlador PID de fermentación robusto sobre Arduino Mega 2560, integrando campo RS485 (MAX485) y monitoreo/control por Ethernet (W5500), con medición precisa de temperatura vía RTD PT100 y MAX31865. El flujo de trabajo, la toolchain exacta y el código son reproducibles y extensibles para usos industriales ligeros o de laboratorio avanzado.

Encuentra este producto y/o libros sobre este tema en Amazon

Ir a Amazon

Como afiliado de Amazon, gano con las compras que cumplan los requisitos. Si compras a través de este enlace, ayudas a mantener este proyecto.

Quiz rápido

Pregunta 1: ¿Cuál es el sistema operativo recomendado para este proyecto?




Pregunta 2: ¿Qué versión de Arduino CLI se debe utilizar?




Pregunta 3: ¿Cuál es el FQBN correcto para Arduino Mega 2560?




Pregunta 4: ¿Qué librería es necesaria para el módulo MAX31865?




Pregunta 5: ¿Qué tipo de conexión utiliza el W5500 Ethernet Shield?




Pregunta 6: ¿Cuál es la velocidad de monitor serie recomendada?




Pregunta 7: ¿Qué tipo de sonda RTD se debe utilizar?




Pregunta 8: ¿Qué tipo de convertidor se necesita para la comunicación SPI entre 5 V y 3.3 V?




Pregunta 9: ¿Qué resistencias son necesarias para el módulo MAX485?




Pregunta 10: ¿Qué tipo de relé se recomienda para controlar el pad calefactor?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Ingeniero Superior en Electrónica de Telecomunicaciones e Ingeniero en Informática (titulaciones oficiales en España).

Sígueme:


Caso práctico: Rastreador GNSS/GSM Arduino MKR GSM 1400

Caso práctico: Rastreador GNSS/GSM Arduino MKR GSM 1400 — hero

Objetivo y caso de uso

Qué construirás: Un rastreador de activos GNSS-GSM utilizando Arduino MKR GSM 1400, NEO-M8N y BNO055 para enviar telemetría por GSM.

Para qué sirve

  • Rastreo en tiempo real de vehículos o activos utilizando GNSS y GSM.
  • Monitoreo de condiciones ambientales mediante el sensor BNO055.
  • Envío de datos de ubicación a un broker MQTT para análisis remoto.
  • Integración con aplicaciones móviles para visualización de datos.

Resultado esperado

  • Actualizaciones de ubicación cada 10 segundos con precisión de 2.5 metros.
  • Latencia en la transmisión de datos no superior a 5 segundos.
  • Capacidad de enviar hasta 100 paquetes de datos por hora.
  • Consumo de energía por debajo de 100 mA en modo activo.

Público objetivo: Desarrolladores y entusiastas de IoT; Nivel: Avanzado

Arquitectura/flujo: GNSS (NEO-M8N) -> Procesador (Arduino MKR GSM 1400) -> Transmisión (GSM) -> Broker MQTT.

Nivel: Avanzado

Prerrequisitos

Sistemas operativos soportados

  • Linux: Ubuntu 22.04 LTS (Jammy) actualizado a fecha reciente
  • Windows 11 23H2 con PowerShell 7.4.x
  • macOS 14 (Sonoma)

Para este caso práctico se mostrarán los comandos con prioridad en Linux/macOS (bash/zsh) y notas equivalentes para Windows.

Toolchain exacta y versiones

Usaremos Arduino CLI (no el IDE gráfico) con el core SAMD oficial de Arduino y librerías específicas. Versionado exacto:

  • Arduino CLI: v0.35.6
  • Core de placas: arduino:samd v1.8.13
  • Compilador embebido: arm-none-eabi-gcc provisto por el core arduino:samd v1.8.13 (administrado automáticamente por Arduino CLI)
  • Librerías Arduino:
  • MKRGSM v1.5.0
  • TinyGPSPlus v1.0.3
  • Adafruit BNO055 v1.6.3
  • Adafruit Unified Sensor v1.1.14
  • PubSubClient v2.8.0
  • (Opcional para depuración) ArduinoHttpClient v0.6.0

Requisitos de red/operador

  • SIM funcional con datos 2G/3G (GPRS/UMTS) activos
  • APN, usuario y contraseña del operador
  • Cobertura GSM en la zona de pruebas
  • Broker MQTT de pruebas (usaremos test.mosquitto.org:1883 para validación)

Drivers y permisos

  • Linux:
  • Añade el usuario al grupo dialout: sudo usermod -aG dialout $USER
  • Desconecta y reconecta la placa tras reiniciar sesión
  • Windows:
  • Los MKR SAMD usan USB CDC estándar; el driver lo instala Windows Update automáticamente
  • macOS:
  • No requiere drivers adicionales

Materiales

  • Placa principal:
  • Arduino MKR GSM 1400 (SAMD21 + módem u‑blox SARA‑U201)
  • Sensores:
  • Receptor GNSS u‑blox NEO‑M8N (módulo con interfaz UART y antena activa/patch; alimentación a 3.3 V)
  • IMU/Orientación: Bosch BNO055 (módulo I2C, 3.3 V)
  • Antenas:
  • Antena GSM adecuada para la MKR GSM 1400 (conector u.FL)
  • Antena GNSS para NEO‑M8N (u.FL o SMA según módulo)
  • Alimentación:
  • Batería LiPo 3.7 V con conector JST‑PH (≥ 1200 mAh recomendado para picos del módem)
  • Cable USB‑C/USB‑A a Micro‑USB (la MKR GSM 1400 es micro‑USB)
  • Cables:
  • Jumpers macho‑hembra para UART/I2C
  • Almacenamiento:
  • Tarjeta SIM con datos y PIN conocido (o PIN deshabilitado)

Nota importante: la MKR GSM 1400 trabaja a 3.3 V. Asegúrate de que los módulos NEO‑M8N y BNO055 que utilices admiten 3.3 V lógicos y de alimentación (la mayoría de breakouts modernos lo hacen).

Preparación y conexión

Conexión eléctrica y de señales

  • Alimentación:
  • Conecta la batería LiPo a la MKR GSM 1400 (conector JST). Mantén además el cable USB para programación.
  • Conecta la antena GSM al conector u.FL de la MKR GSM 1400.
  • Conecta la antena GNSS al NEO‑M8N.
  • GNSS (NEO‑M8N) por UART a 9600 bps:
  • Usaremos Serial1 del MKR (UART hardware en pines RX/TX).
  • BNO055 por I2C a 100 kHz (por defecto), dirección 0x28 (ADR a GND).

Tabla de cableado propuesto:

Componente Señal Arduino MKR GSM 1400 NEO‑M8N BNO055 Notas
Alimentación 3V3 3V3 VCC VIN Mantener 3.3 V estable
Alimentación GND GND GND GND Masa común
UART GNSS RX D13 (RX/Ser1) TX NEO TX -> MKR RX
UART GNSS TX D14 (TX/Ser1) RX NEO RX -> MKR TX
I2C SDA SDA SDA Pull‑ups suelen venir en el breakout
I2C SCL SCL SCL
I2C Addr ADR GND Dirección BNO055 = 0x28
Antena GSM u.FL Conector GSM Imprescindible para registro en red
Antena GNSS u.FL/SMA Conector GNSS Imprescindible para captar satélites

Recomendaciones:
– Sitúa la antena GNSS con vista al cielo.
– Evita cables largos en UART/I2C.
– Si el módulo NEO‑M8N incluye LED PPS, útil para ver fix.

Código completo (C++ para Arduino MKR GSM 1400)

Este sketch implementa un “gnss-gsm-asset-tracker” que:
– Lee GNSS (lat/lon/alt/velocidad/satélites) desde NEO‑M8N por Serial1 y lo parsea con TinyGPS++.
– Lee orientación absoluta (yaw/pitch/roll) desde BNO055 por I2C.
– Conecta a la red GSM/GPRS con MKRGSM, obtiene IMEI y publica telemetría JSON por MQTT (TCP/1883).
– Envía paquetes periódicos y reconecta robustamente ante cortes.

Antes de compilar, edita los defines APN, GPRS_USER, GPRS_PASS y, si aplica, PINNUMBER.

Guarda el archivo como gnss_gsm_asset_tracker/gnss_gsm_asset_tracker.ino

/*
  gnss_gsm_asset_tracker.ino
  Dispositivo: Arduino MKR GSM 1400 + NEO-M8N (UART) + BNO055 (I2C)
  Objetivo: Asset tracker GNSS + orientación vía GSM/GPRS (MQTT)
  Toolchain: Arduino CLI v0.35.6 + arduino:samd@1.8.13
  Librerías:
    MKRGSM@1.5.0, TinyGPSPlus@1.0.3, Adafruit BNO055@1.6.3,
    Adafruit Unified Sensor@1.1.14, PubSubClient@2.8.0
*/

#include <MKRGSM.h>
#include <TinyGPSPlus.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BNO055.h>
#include <PubSubClient.h>

// --------- Configuración de red GSM/GPRS ----------
#define APN        "internet"     // <-- Cambia por el APN de tu operador
#define GPRS_USER  ""             // <-- Usuario APN (si aplica)
#define GPRS_PASS  ""             // <-- Contraseña APN (si aplica)
#define PINNUMBER  ""             // <-- PIN SIM ("" si deshabilitado)

// --------- MQTT (broker de pruebas) ---------------
const char* MQTT_SERVER = "test.mosquitto.org";
const uint16_t MQTT_PORT = 1883;
const char* MQTT_BASE_TOPIC = "gnss-gsm-asset-tracker";

// --------- UART GNSS ------------------------------
#define GNSS_BAUD 9600

// --------- Periodos -------------------------------
const unsigned long TELEMETRY_PERIOD_MS = 15000;  // envíos cada 15 s
const unsigned long GNSS_PRINT_PERIOD_MS = 2000;  // logs GNSS cada 2 s

// Objetos globales
GSM gsmAccess;
GPRS gprs;
GSMClient netClient;
GSMModem modem;
PubSubClient mqtt(netClient);

TinyGPSPlus gps;
Adafruit_BNO055 bno = Adafruit_BNO055(55, 0x28);

String deviceImei = "unknown";
char mqttTopic[128];
char payload[384];

unsigned long lastTelemetry = 0;
unsigned long lastGnssPrint = 0;

// ------------- Utilidades -------------------------
static void log(const char* msg) {
  Serial.println(msg);
}

void readGnssStream() {
  while (Serial1.available()) {
    char c = (char)Serial1.read();
    gps.encode(c);
  }
}

bool initBNO055() {
  if (!bno.begin()) {
    log("BNO055: fallo al inicializar. Verifica I2C y alimentación.");
    return false;
  }
  delay(10);
  bno.setExtCrystalUse(true);
  log("BNO055: inicializado en modo NDOF (orientación absoluta).");
  return true;
}

bool connectGSM() {
  log("Módem: inicializando...");
  if (!modem.begin()) {
    log("Módem: no responde (revisa alimentación/antena).");
    return false;
  }

  String modemInfo = modem.getModemInfo();
  Serial.print("Módem info: "); Serial.println(modemInfo);

  deviceImei = modem.getIMEI();
  if (deviceImei.length() == 0) deviceImei = "unknown";
  Serial.print("IMEI: "); Serial.println(deviceImei);

  log("GSM: registrando en red...");
  for (int i = 0; i < 15; i++) {
    if (gsmAccess.begin(PINNUMBER) == GSM_READY) {
      log("GSM: registrado.");
      break;
    }
    delay(1000);
    if (i == 14) {
      log("GSM: no se pudo registrar (cobertura/PIN?).");
      return false;
    }
  }

  log("GPRS: adjuntando...");
  for (int i = 0; i < 10; i++) {
    if (gprs.attachGPRS(APN, GPRS_USER, GPRS_PASS) == GPRS_READY) {
      log("GPRS: adjuntado OK.");
      return true;
    }
    delay(1500);
  }
  log("GPRS: fallo al adjuntar (APN/credenciales?).");
  return false;
}

bool ensureMqttConnected() {
  if (mqtt.connected()) return true;

  mqtt.setServer(MQTT_SERVER, MQTT_PORT);
  // ClientID derivado del IMEI
  char clientId[48];
  snprintf(clientId, sizeof(clientId), "mkrgsm1400-%s", deviceImei.c_str());

  Serial.print("MQTT: conectando a ");
  Serial.print(MQTT_SERVER);
  Serial.print(":");
  Serial.println(MQTT_PORT);

  // Intentos con backoff simple
  for (int attempt = 1; attempt <= 5; attempt++) {
    if (mqtt.connect(clientId)) {
      log("MQTT: conectado.");
      return true;
    }
    Serial.print("MQTT: intento ");
    Serial.print(attempt);
    Serial.println(" fallido.");
    delay(2000 * attempt);
  }
  log("MQTT: no se pudo conectar.");
  return false;
}

void publishTelemetry() {
  // Construir tópico: gnss-gsm-asset-tracker/<IMEI>/telemetry
  snprintf(mqttTopic, sizeof(mqttTopic), "%s/%s/telemetry", MQTT_BASE_TOPIC, deviceImei.c_str());

  // Extraer datos GNSS
  double lat = gps.location.isValid() ? gps.location.lat() : NAN;
  double lon = gps.location.isValid() ? gps.location.lng() : NAN;
  double alt = gps.altitude.isValid() ? gps.altitude.meters() : NAN;
  double spd = gps.speed.isValid() ? gps.speed.kmph() : NAN;
  uint32_t sats = gps.satellites.isValid() ? gps.satellites.value() : 0;
  double hdop = gps.hdop.isValid() ? gps.hdop.hdop() : NAN;
  bool fix = gps.location.isValid() && gps.location.age() < 2000;

  // Extraer orientación BNO055 (Euler: heading/yaw, roll, pitch)
  sensors_event_t orientationData, angVelocityData, linearAccelData, magnetometerData, accelerometerData, gravityData;
  bno.getEvent(&orientationData, Adafruit_BNO055::VECTOR_EULER);
  float yaw   = orientationData.orientation.x; // grados
  float roll  = orientationData.orientation.y;
  float pitch = orientationData.orientation.z;

  // Timestamp aproximado por millis (ideal: RTC/GNSS time)
  unsigned long ts = millis();

  // Construir JSON compacto
  // Nota: evitamos snprintf con %f por AVR, pero en SAMD está soportado; de todos modos convertimos manualmente para control
  char latStr[16], lonStr[16], altStr[16], spdStr[16], hdopStr[16];
  char yawStr[16], pitchStr[16], rollStr[16];

  auto fmt = [](char* buf, size_t sz, double v) {
    if (isnan(v)) { strncpy(buf, "null", sz); }
    else { dtostrf(v, 0, 6, buf); }
  };

  fmt(latStr, sizeof(latStr), lat);
  fmt(lonStr, sizeof(lonStr), lon);
  fmt(altStr, sizeof(altStr), alt);
  fmt(spdStr, sizeof(spdStr), spd);
  fmt(hdopStr, sizeof(hdopStr), hdop);
  fmt(yawStr, sizeof(yawStr), yaw);
  fmt(pitchStr, sizeof(pitchStr), pitch);
  fmt(rollStr, sizeof(rollStr), roll);

  snprintf(payload, sizeof(payload),
    "{"
      "\"imei\":\"%s\","
      "\"ts\":%lu,"
      "\"gnss\":{\"fix\":%s,\"lat\":%s,\"lon\":%s,\"alt\":%s,\"speed_kmh\":%s,\"sats\":%lu,\"hdop\":%s},"
      "\"ori\":{\"yaw_deg\":%s,\"pitch_deg\":%s,\"roll_deg\":%s}"
    "}",
    deviceImei.c_str(), ts,
    fix ? "true" : "false", latStr, lonStr, altStr, spdStr, (unsigned long)sats, hdopStr,
    yawStr, pitchStr, rollStr
  );

  Serial.print("Publicando MQTT -> ");
  Serial.print(mqttTopic);
  Serial.print(" : ");
  Serial.println(payload);

  if (!mqtt.publish(mqttTopic, payload)) {
    log("MQTT: publish fallido (¿desconexión?).");
  }
}

void setup() {
  Serial.begin(115200);
  while (!Serial) { ; }

  log("\n== MKR GSM 1400 GNSS-GSM Asset Tracker ==");
  log("Iniciando GNSS UART...");
  Serial1.begin(GNSS_BAUD);

  log("Iniciando BNO055...");
  bool bnoOk = initBNO055();
  if (!bnoOk) {
    log("Continuando sin BNO055 (se enviarán 'null' en orientación).");
  }

  if (!connectGSM()) {
    log("ERROR: No se pudo conectar GSM/GPRS. Reintentando en bucle...");
  }

  mqtt.setKeepAlive(30);
  mqtt.setSocketTimeout(15);
  // callback MQTT no usado (solo publicación), pero podría implementarse

  lastTelemetry = millis();
  lastGnssPrint = millis();
}

void loop() {
  // Alimentar parser GNSS
  readGnssStream();

  // Mantener MQTT
  if (!mqtt.connected()) {
    ensureMqttConnected();
  } else {
    mqtt.loop();
  }

  unsigned long now = millis();

  // Logging GNSS periódico
  if (now - lastGnssPrint > GNSS_PRINT_PERIOD_MS) {
    lastGnssPrint = now;
    if (gps.location.isValid()) {
      Serial.print("GNSS: ");
      Serial.print(gps.location.lat(), 6);
      Serial.print(", ");
      Serial.print(gps.location.lng(), 6);
      Serial.print(" alt(m)=");
      Serial.print(gps.altitude.meters());
      Serial.print(" sats=");
      Serial.print(gps.satellites.value());
      Serial.print(" hdop=");
      Serial.print(gps.hdop.hdop());
      Serial.print(" spd(kmh)=");
      Serial.println(gps.speed.kmph());
    } else {
      Serial.println("GNSS: buscando fix...");
    }
  }

  // Publicación periódica
  if (now - lastTelemetry > TELEMETRY_PERIOD_MS) {
    lastTelemetry = now;
    if (mqtt.connected()) {
      publishTelemetry();
    }
  }

  // Si GPRS cae, reintenta
  if (gprs.getStatus() != GPRS_READY) {
    log("GPRS: desconectado, reintentando attach...");
    gprs.attachGPRS(APN, GPRS_USER, GPRS_PASS);
  }
}

Explicación breve de partes clave

  • Inicialización de periféricos:
  • Serial1.begin(9600): UART GNSS (NEO‑M8N por defecto a 9600 bps).
  • bno.begin() + setExtCrystalUse(true): BNO055 en modo NDOF con cristal externo para mejor estabilidad.
  • Conexión celular:
  • modem.begin() y getIMEI(): inicializa el SARA‑U201 y lee IMEI para identificar el dispositivo.
  • gsmAccess.begin(PIN): registro en red GSM.
  • gprs.attachGPRS(APN,…): adjunta PDP context para datos.
  • Transporte MQTT:
  • PubSubClient con GSMClient (TCP plano en 1883).
  • ensureMqttConnected(): reconexión robusta con backoff.
  • Fusión de datos:
  • GNSS via TinyGPS++: lat/lon/alt/velocidad/satélites/HDOP.
  • Orientación BNO055: Euler yaw/pitch/roll.
  • Composición JSON: payload compacto sin dependencias adicionales.
  • Publicación:
  • Tópico: gnss-gsm-asset-tracker//telemetry
  • Periodo: 15 s (ajustable).

Compilación, flash y ejecución

Asegúrate de tener Arduino CLI v0.35.6 en PATH. En Linux/macOS:

# 1) Instalar Arduino CLI (si no lo tienes)
# Linux x64:
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh
# Mover a PATH (ajústalo a tu preferencia)
sudo mv bin/arduino-cli /usr/local/bin/
arduino-cli version  # Debe mostrar 0.35.6

# 2) Inicializar configuración y actualizar índices
arduino-cli config init
arduino-cli core update-index

# 3) Instalar el core SAMD exacto
arduino-cli core install arduino:samd@1.8.13

# 4) Instalar librerías con versiones exactas
arduino-cli lib install "MKRGSM@1.5.0"
arduino-cli lib install "TinyGPSPlus@1.0.3"
arduino-cli lib install "Adafruit BNO055@1.6.3"
arduino-cli lib install "Adafruit Unified Sensor@1.1.14"
arduino-cli lib install "PubSubClient@2.8.0"

# 5) Crear la estructura del proyecto
mkdir -p ~/proyectos/gnss-gsm-asset-tracker
cd ~/proyectos/gnss-gsm-asset-tracker
# Crea el archivo gnss_gsm_asset_tracker.ino y pega el código

# 6) Identificar el puerto serie de la MKR GSM 1400
arduino-cli board list
# Ejemplo de salida (Linux): /dev/ttyACM0  Arduino MKR GSM 1400  arduino:samd:mkrgsm1400

# 7) Compilar para la FQBN apropiada
arduino-cli compile --fqbn arduino:samd:mkrgsm1400 .

# 8) Subir el firmware (ajusta el puerto si difiere)
arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:samd:mkrgsm1400 .

# 9) Abrir monitor serie a 115200 baudios
arduino-cli monitor -p /dev/ttyACM0 -c baudrate=115200

Notas:
– En Windows, sustituye el puerto por COMx, por ejemplo:
– arduino-cli upload -p COM6 –fqbn arduino:samd:mkrgsm1400 .
– arduino-cli monitor -p COM6 -c baudrate=115200
– Si el APN requiere credenciales, edita GPRS_USER/GPRS_PASS en el sketch.
– Si tu SIM tiene PIN, pon PINNUMBER acorde.

Validación paso a paso

1) Encendido y logs iniciales
– Conecta antena GSM, antena GNSS y SIM.
– Alimenta con LiPo y USB.
– Abre el monitor serie a 115200.
– Debes ver mensajes similares:
– «== MKR GSM 1400 GNSS-GSM Asset Tracker ==»
– «Iniciando GNSS UART…»
– «BNO055: inicializado…» (o advertencia si no)
– «Módem: inicializando…» seguido de «Módem info: …» y «IMEI: …»
– «GSM: registrado.» y «GPRS: adjuntado OK.»
– «MQTT: conectado.»

2) Fix GNSS
– Coloca la antena GNSS con vista al cielo.
– En el monitor, verás «GNSS: buscando fix…» hasta lograr fix, luego coordenadas:
– GNSS: 40.416775, -3.703790 alt(m)=650.12 sats=10 hdop=0.9 spd(kmh)=0.12

3) Publicación MQTT
– Cada ~15 s, el sketch imprime «Publicando MQTT -> gnss-gsm-asset-tracker//telemetry : {…}».
– Desde otro equipo (o el mismo) suscríbete al tópico para verificar:
– Linux/macOS con mosquitto-clients:
bash
# Instalar (si hace falta)
# Ubuntu: sudo apt-get install -y mosquitto-clients
mosquitto_sub -h test.mosquitto.org -t "gnss-gsm-asset-tracker/+/telemetry" -v

– Debes recibir líneas con el JSON enviado. Ejemplo:
gnss-gsm-asset-tracker/356612345678901/telemetry {"imei":"356612345678901","ts":123456,"gnss":{"fix":true,"lat":40.416775,"lon":-3.703790,"alt":650.120000,"speed_kmh":0.120000,"sats":10,"hdop":0.900000},"ori":{"yaw_deg":123.125000,"pitch_deg":-2.500000,"roll_deg":1.000000}}

4) Validación de orientación
– Mueve físicamente el montaje; observa cambios en yaw/pitch/roll en el JSON y en el monitor serie.

5) Robustez de reconexión
– Desatornilla (temporalmente) la antena GSM o apantalla la señal para simular caída:
– Observa reintentos de GPRS/MQTT. Al recuperar señal, el envío debe reanudarse sin reset manual.

6) Tasa de publicación y latencia
– Confirma el periodo aproximado entre mensajes (≈15 s).
– La latencia típica TCP/2G puede ser de varios segundos; considera esto normal.

Troubleshooting (5–8 problemas típicos y soluciones)

1) No hay registro GSM: “GSM: no se pudo registrar”
– Causas:
– Antena GSM no conectada o defectuosa.
– SIM sin servicio o PIN activo incorrecto.
– Cobertura insuficiente.
– Soluciones:
– Revisa conexión u.FL cuidadosamente.
– Verifica PINNUMBER y/o deshabilita PIN en un teléfono.
– Prueba en otra ubicación / operador que mantenga 2G/3G operativo.

2) “GPRS: fallo al adjuntar (APN/credenciales?)”
– Causas:
– APN incorrecto o credenciales requeridas.
– Servicio de datos no activo.
– Soluciones:
– Confirma APN exacto de tu operador (may/min correctos).
– Rellena GPRS_USER/GPRS_PASS si aplica.
– Contacta al operador para habilitar datos.

3) MQTT no conecta a test.mosquitto.org
– Causas:
– Bloqueo del puerto 1883 en la red del operador.
– DNS no resuelto por el módem.
– Intermitencia 2G.
– Soluciones:
– Prueba con IP directa del broker (ojo: puede cambiar):
– mosquitto.org IP pública en el momento de prueba (resuélvela con nslookup en tu PC).
– Monta un broker propio en VPS/puerto alternativo abierto por el operador.
– Incrementa los timeouts y reintentos en ensureMqttConnected().

4) GNSS sin fix prolongado
– Causas:
– Antena GNSS mal posicionada.
– Tiempo de arranque en frío (TTFF) elevado sin asistencia.
– Soluciones:
– Sitúa la antena a cielo abierto, alejando interferencias.
– Espera 5–15 minutos el primer fix.
– Si tu módulo soporta batería backup, asegúrala para retener efemérides.

5) Lecturas BNO055 nulas o erráticas
– Causas:
– I2C sin alimentación/masa compartida.
– Dirección errónea (ADR no en GND -> 0x29).
– Soluciones:
– Verifica GND común.
– Comprueba que ADR esté a GND para 0x28 o ajusta el constructor a 0x29.
– Reduce vibraciones; BNO055 requiere estabilización para calibraciones.

6) Subida por USB falla (“No device found”)
– Causas:
– Puerto equivocado.
– Regla de permisos en Linux.
– Soluciones:
– Revisa arduino-cli board list.
– Añade tu usuario a dialout y reconecta.
– Pulsa reset dos veces para forzar bootloader (puede cambiar el puerto).

7) Reinicios al transmitir GSM
– Causas:
– Caídas de tensión por picos de corriente del módem (hasta ~2 A pico).
– Soluciones:
– Usa LiPo ≥1200 mAh y cable USB corto/calidad.
– Evita alimentar solo por USB del PC; combina con LiPo.
– Añade condensadores de bulk (p.ej. 470–1000 µF) cerca del módem si tu carrier board lo permite.

8) JSON truncado o publicación falla
– Causas:
– Buffer MQTT/payload insuficiente.
– Soluciones:
– Aumenta el tamaño de payload[] si añadiste campos.
– Verifica que mqtt.publish devuelve true; reintenta al reconectar.

Mejoras/variantes

  • Cifrado TLS:
  • Cambiar a GSMSSL (cliente SSL) y usar un broker MQTT con TLS en 8883.
  • Requiere más RAM y gestión de raíces de confianza; evaluar huellas SHA1 estáticas.
  • Compresión/optimización:
  • Enviar payload binario CBOR/MessagePack para reducir bytes en 2G.
  • Añadir downsampling y delta encoding entre posiciones cercanas.
  • Geofencing local:
  • Implementar lógica de “salida/entrada” de zonas; publicar eventos además de telemetría periódica.
  • Filtrado sensor:
  • Complementar GNSS con acelerómetro/BNO055 para detectar movimiento (start/stop) y despertar el módem solo al moverse.
  • Gestión de energía:
  • Uso del modo de bajo consumo del SAMD21 y encendido controlado del módem para extender batería.
  • Almacenamiento offline:
  • Buffer circular en flash/SD para enviar lotes cuando haya cobertura.
  • Formato/Backends:
  • Enviar a HTTP/REST con ArduinoHttpClient (si el endpoint soporta http sin TLS o se usa SSL).
  • Integración con plataformas IoT (ThingsBoard, AWS IoT, etc.) usando MQTT con topics y certificados.

Checklist de verificación

  • [ ] He instalado Arduino CLI v0.35.6 y el core arduino:samd@1.8.13
  • [ ] He instalado las librerías exactas: MKRGSM@1.5.0, TinyGPSPlus@1.0.3, Adafruit BNO055@1.6.3, Adafruit Unified Sensor@1.1.14, PubSubClient@2.8.0
  • [ ] He conectado correctamente: MKR GSM 1400 + NEO‑M8N (UART D13 RX / D14 TX), BNO055 (I2C SDA/SCL), masas y 3.3 V
  • [ ] Antena GSM a la MKR y antena GNSS al NEO‑M8N están bien acopladas
  • [ ] SIM con datos y APN configurado en el código (PIN si aplica)
  • [ ] Compila sin errores con FQBN arduino:samd:mkrgsm1400
  • [ ] Subida correcta al puerto serie detectado (ACM/COM correspondiente)
  • [ ] Monitor serie muestra registro GSM y adjunto GPRS exitosos
  • [ ] Se recibe fix GNSS (coordenadas válidas) en el monitor
  • [ ] Se observan publicaciones cada ~15 s hacia el tópico MQTT gnss-gsm-asset-tracker//telemetry
  • [ ] Verificado con mosquitto_sub que llegan los JSON con lat/lon/orientación
  • [ ] Probada la reconexión tras simular pérdida de señal

Con todo lo anterior, habrás construido un “gnss-gsm-asset-tracker” consistente y reproducible sobre el modelo exacto Arduino MKR GSM 1400 + NEO‑M8N + BNO055, compilado con Arduino CLI y versiones de toolchain/librerías especificadas, y validado extremo a extremo con un broker MQTT público.

Encuentra este producto y/o libros sobre este tema en Amazon

Ir a Amazon

Como afiliado de Amazon, gano con las compras que cumplan los requisitos. Si compras a través de este enlace, ayudas a mantener este proyecto.

Quiz rápido

Pregunta 1: ¿Cuál es la versión de Arduino CLI utilizada en el artículo?




Pregunta 2: ¿Qué sistema operativo no es mencionado como soportado?




Pregunta 3: ¿Cuál es el core de placas indicado para Arduino?




Pregunta 4: ¿Qué librería es opcional para depuración según el artículo?




Pregunta 5: ¿Qué tipo de cobertura se necesita para las pruebas?




Pregunta 6: ¿Qué comando se debe usar para añadir un usuario al grupo dialout en Linux?




Pregunta 7: ¿Cuál es la versión de la librería Adafruit BNO055 mencionada?




Pregunta 8: ¿Qué tipo de SIM se requiere según los requisitos de red?




Pregunta 9: ¿Qué tipo de antena se requiere para el receptor GNSS NEO-M8N?




Pregunta 10: ¿Qué broker MQTT se sugiere para validación en las pruebas?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Ingeniero Superior en Electrónica de Telecomunicaciones e Ingeniero en Informática (titulaciones oficiales en España).

Sígueme:


Caso práctico: Hexápodo Arduino Mega 2560 + PCA9685 + XBee

Caso práctico: Hexápodo Arduino Mega 2560 + PCA9685 + XBee — hero

Objetivo y caso de uso

Qué construirás: Un robot hexápodo controlado por Zigbee utilizando Arduino Mega 2560, PCA9685 y XBee S2C.

Para qué sirve

  • Control remoto de un hexápodo para exploración en terrenos difíciles.
  • Implementación de evitación de obstáculos utilizando el sensor VL53L0X.
  • Demostraciones de robótica educativa en ferias tecnológicas.
  • Proyectos de investigación en movilidad robótica y comunicación inalámbrica.

Resultado esperado

  • Latencia de control remoto inferior a 100 ms.
  • Capacidad de respuesta a comandos en tiempo real con un mínimo de 10 FPS.
  • Precisión de detección de obstáculos de hasta 30 cm con el sensor ToF.
  • Consumo de energía del sistema menor a 500 mA durante la operación.

Público objetivo: Estudiantes avanzados y entusiastas de la robótica; Nivel: Avanzado

Arquitectura/flujo: Arduino Mega 2560 controla el PCA9685 para manejar los servos, mientras que el XBee S2C se encarga de la comunicación Zigbee y el VL53L0X proporciona datos de distancia para la evitación de obstáculos.

Nivel: Avanzado

Prerrequisitos

  • Sistema operativo recomendado:
  • Ubuntu 22.04.4 LTS (64-bit) o equivalente en Linux con soporte para USB CDC.
  • Alternativa: Windows 10/11 o macOS 12+, con adaptaciones de puerto serie.

  • Toolchain exacta (probada):

  • Arduino CLI 0.35.3
  • Core Arduino AVR Boards arduino:avr@1.8.6
  • avr-gcc 7.3.0-atmel3.6.1
  • avrdude 6.3
  • Bibliotecas Arduino (Arduino Library Manager):
  • Adafruit PWM Servo Driver Library@2.4.1
  • Adafruit BusIO@1.14.5
  • Adafruit Unified Sensor@1.1.14
  • Adafruit VL53L0X@1.3.1
  • Python 3.10.12 (opcional, para script de comandos por ZigBee)
  • minicom 2.7.1 (o picocom) para terminal serie en Linux, o PuTTY en Windows

  • Hardware principal del caso:

  • Arduino Mega 2560
  • PCA9685 (driver 16 canales servo por I2C)
  • XBee S2C (Zigbee) en modo transparente (AP=0)
  • VL53L0X (sensor ToF por I2C)
  • Objetivo del proyecto: servo-hexapod-zigbee (hexápodo de 18 servos con control remoto Zigbee y evitación de obstáculos con ToF).

  • Notas de compatibilidad:

  • Arduino Mega 2560 usa 5 V lógicos; el XBee S2C opera a 3.3 V. Es obligatorio un shield con conversión de nivel o un conversor bidireccional (TX/RX).
  • PCA9685 y VL53L0X comparten el bus I2C (SDA/SCL); asegúrate de que sus direcciones no colisionen (por defecto PCA9685 0x40, VL53L0X 0x29).

Materiales

  • Electrónica y módulos:
  • 1x Arduino Mega 2560 (original o compatible, con USB).
  • 1x PCA9685 16-channel 12-bit PWM Servo Driver (dirección por defecto 0x40).
  • 1x XBee S2C Zigbee (serie S2C, 2.4 GHz).
  • 1x Shield/adaptador XBee para Arduino Mega (con conversión de nivel a 3.3 V) o conversor de nivel bidireccional para UART.
  • 1x VL53L0X (módulo ToF, 940 nm).
  • 18x servos estándar para robótica (ej.: SG90/MG90S para prototipo o servos metálicos 12–20 kg·cm para carga real).
  • 1x Fuente DC 6V–7.4 V con capacidad ≥ 5–10 A (según servos).
  • 1x Portapilas LiPo 2S/3S o fuente conmutada 6 V regulada de alta corriente.
  • Cableado Dupont, regletas, tornillería y bridas.
  • 1x Chasis de hexápodo (6 patas, 3 DOF por pata: coxa, fémur, tibia).
  • 1x Fusible/limitador o protector de corriente para la línea de servos (recomendado).
  • 1x Interruptor general en el bus de servos (recomendado).

  • Opcionales útiles:

  • 1x Módulo UBEC 5 V/6 V de 5–10 A para servos.
  • Pasta térmica/adhesivo para disipación del PCA9685 (no siempre necesario, pero útil).

  • Software:

  • Arduino CLI 0.35.3 (instalar por script oficial).
  • Librerías especificadas (vías Arduino CLI).
  • minicom/picocom o Python para pruebas Zigbee.

Preparación y conexión

Consideraciones eléctricas

  • Los servos NO deben alimentarse desde el pin 5V del Arduino Mega. Usa una fuente externa para la línea V+ del PCA9685 (pines “V+” de potencia).
  • Une siempre GND del Arduino, GND del PCA9685 y GND de la fuente de servos. Masa común es imprescindible para estabilidad y PWM correcto.
  • El XBee S2C debe recibir 3.3 V y señales TX/RX a 3.3 V. Emplea un shield XBee o conversores de nivel. No conectes el XBee directamente a 5 V.

Mapeo de pines/puertos

  • I2C en Arduino Mega 2560:
  • SDA: pin 20
  • SCL: pin 21

  • UART para XBee:

  • Serial1: RX1 pin 19, TX1 pin 18 (recomendado para dejar Serial0/USB para logs).

  • Alimentación:

  • PCA9685: VCC a 5 V del Mega (lógica), GND común; V+ (poder) a 6 V–7.4 V externa para servos.
  • VL53L0X: VCC a 5 V si el módulo tiene regulador/level shifting (común en breakout comerciales), o 3.3 V si es bare.

Tabla de conexiones

Módulo Pin/Señal módulo Conecta a (Mega/otro) Notas
PCA9685 VCC 5V Mega Lógica
PCA9685 GND GND común Común a todo
PCA9685 SDA SDA (20) Mega I2C
PCA9685 SCL SCL (21) Mega I2C
PCA9685 V+ (barral) +6–7.4 V externa Potencia servos
PCA9685 GND (barral) GND fuente servos Común
VL53L0X VCC 5V (si trae regulador) Ver ficha módulo
VL53L0X GND GND común
VL53L0X SDA SDA (20) Mega I2C
VL53L0X SCL SCL (21) Mega I2C
XBee S2C VCC 3.3 V del shield No 5 V
XBee S2C GND GND común
XBee S2C DOUT (TX del XBee) RX1 (19) Mega (via shield/level) UART
XBee S2C DIN (RX del XBee) TX1 (18) Mega (via shield/level) UART
Servos (18) Señal Canales PCA9685 0–17 Ver mapeo abajo
Servos +V V+ PCA9685 6–7.4 V
Servos GND GND PCA9685

Mapeo de servos a canales (hexápodo 6 patas x 3 DOF)

  • Convención de patas: L0, L1, L2 (lado izquierdo front–middle–rear), R0, R1, R2 (lado derecho front–middle–rear).
  • DOF por pata: coxa (rotación horizontal), fémur (levanta/baja), tibia (extiende).
Pata Coxa (canal) Fémur (canal) Tibia (canal)
L0 0 1 2
L1 3 4 5
L2 6 7 8
R0 9 10 11
R1 12 13 14
R2 15 16 17
  • Ajusta los canales si tu cableado físico difiere, pero mantén una estructura coherente para simplificar el control y las gait tables.

Configuración básica del XBee S2C (modo transparente)

  • Conecta el XBee remoto a tu PC por un adaptador USB-XBee (3.3 V).
  • Entra en modo de comandos “AT” a 115200 bps (por defecto puede venir a 9600 bps; ajusta según sea necesario):
  • Abre minicom/picocom: a 9600 o 115200 según módulo.
  • Teclea “+++” y espera “OK”.
  • Configura (ejemplo: PAN en 0x1234, canal C, router/coordinator según topología). Para control remoto simple, haz que el robot sea “Router” (CE=0) y el XBee de la PC “Coordinator” (CE=1):
  • ATRE
  • ATAP0 (modo transparente)
  • ATBD7 (115200 bps)
  • ATCE1 (coordinator en el remoto de PC) o ATCE0 (router en el robot)
  • ATID1234 (PAN ID)
  • ATCH0C (canal 0x0C, opcional)
  • ATWR (guardar)
  • ATCN (salir)
  • En el lado del robot (XBee en el shield del Mega), aplica:
  • ATAP0
  • ATBD7
  • ATCE0
  • ATID1234
  • ATWR
  • Nota: si prefieres API mode (AP=2), modifica el código para usar frames; en este caso práctico usamos transparente para mantener el enfoque en gait + sensórica.

Código completo (Arduino C++)

El siguiente sketch controla:
– PCA9685 a 50 Hz para 18 servos.
– Gait tipo “trípode” con temporización no bloqueante.
– Parser de comandos por Zigbee (Serial1) con texto: FWD, BACK, LEFT, RIGHT, STOP, STEP, SPEED n, CALIB leg dof offset.
– VL53L0X para evitación: detiene y notifica si hay obstáculo < 200 mm.

Guarda el archivo como servo_hexapod_zigbee/servo_hexapod_zigbee.ino.

#include <Wire.h>
#include <Adafruit_PWMServoDriver.h>
#include <Adafruit_VL53L0X.h>
#include <Adafruit_Sensor.h>

// ---------- Configuración PCA9685 ----------
static const uint8_t PCA_ADDR = 0x40;
static const float SERVO_FREQ = 50.0f;     // Hz

Adafruit_PWMServoDriver pca = Adafruit_PWMServoDriver(PCA_ADDR);

// Rango típico de servo en microsegundos:
static const int SERVO_MIN_US = 500;
static const int SERVO_MAX_US = 2500;

// Canales por pata (coxa, femur, tibia)
struct LegMap { uint8_t coxa, femur, tibia; };
LegMap legs[6] = {
  {0, 1, 2},     // L0
  {3, 4, 5},     // L1
  {6, 7, 8},     // L2
  {9, 10, 11},   // R0
  {12, 13, 14},  // R1
  {15, 16, 17}   // R2
};

// Offsets por servo para centrado (us). Ajusta tras calibración.
int16_t servoOffsetUs[18] = {
  0,0,0,  0,0,0,  0,0,0,  0,0,0,  0,0,0,  0,0,0
};

// Inversión por servo (1 normal, -1 invertido)
int8_t servoDir[18] = {
  1, -1, -1,   // L0: coxa normal, femur invertido, tibia invertida
  1, -1, -1,   // L1
  1, -1, -1,   // L2
  -1, -1, -1,  // R0: coxa invertida (simetría), femur invertido, tibia invertida
  -1, -1, -1,  // R1
  -1, -1, -1   // R2
};

// ---------- Cinemática sencilla (3 DOF por pata) ----------
struct Vec3 { float x, y, z; };

// Longitudes (mm) de cada link. Ajusta a tu chasis.
static const float L_COXA = 28.0f;
static const float L_FEMUR = 55.0f;
static const float L_TIBIA = 85.0f;

// Límites de ángulo por DOF (grados)
static const float COXA_MIN = -45.0f, COXA_MAX = 45.0f;
static const float FEMUR_MIN = -20.0f, FEMUR_MAX = 75.0f;
static const float TIBIA_MIN = -70.0f, TIBIA_MAX = 20.0f;

// Conversión de ángulo (grados) a microsegundos
int angleToUs(float angleDeg) {
  // Mapear [0..180] a [SERVO_MIN_US..SERVO_MAX_US]
  // Primero acotamos a 0..180 desde una referencia 90 en neutro
  float a = angleDeg;
  if (a < 0) a = 0;
  if (a > 180) a = 180;
  float us = SERVO_MIN_US + (a / 180.0f) * (SERVO_MAX_US - SERVO_MIN_US);
  return (int)us;
}

// Conversión us -> ticks PCA9685
uint16_t usToTicks(int us) {
  float ticks = (SERVO_FREQ * 4096.0f * (float)us) / 1000000.0f;
  if (ticks < 0) ticks = 0;
  if (ticks > 4095) ticks = 4095;
  return (uint16_t)ticks;
}

// Set de un canal con ángulo absoluto (0..180), más offset e inversión
void setServoAngle(uint8_t ch, float angleDeg) {
  // Inversión por hardware lógico
  float a = angleDeg;
  if (servoDir[ch] < 0) a = 180.0f - a;
  int baseUs = angleToUs(a);
  int us = baseUs + servoOffsetUs[ch];
  uint16_t t = usToTicks(us);
  pca.setPWM(ch, 0, t);
}

// IK de una pata: retorna ángulos en grados (coxa, femur, tibia)
bool ikSolve(const Vec3 &target, float &aCoxa, float &aFemur, float &aTibia, bool isRightSide) {
  // Ajuste horizontal: coxa rota en planta por atan2(y, x)
  float a = atan2f(target.y, target.x); // rad
  aCoxa = a * 180.0f / M_PI; // grados

  // Proyección al plano de fémur-tibia: dist desde el eje de coxa
  float r = sqrtf(target.x*target.x + target.y*target.y) - L_COXA;
  float z = target.z;

  // Distancia al pie en el plano (r,z)
  float d = sqrtf(r*r + z*z);

  // Ley del coseno para aFemur y aTibia
  float cosTibia = (L_FEMUR*L_FEMUR + L_TIBIA*L_TIBIA - d*d) / (2.0f*L_FEMUR*L_TIBIA);
  if (cosTibia < -1.0f || cosTibia > 1.0f) return false;
  float tibiaRad = acosf(cosTibia); // 0..pi

  float cosFemur = (L_FEMUR*L_FEMUR + d*d - L_TIBIA*L_TIBIA) / (2.0f*L_FEMUR*d);
  if (cosFemur < -1.0f || cosFemur > 1.0f) return false;
  float femurRad = acosf(cosFemur);

  // Elevación natural: ángulo de la línea r-z
  float elev = atan2f(z, r);

  // Convenciones (ajusta según montaje mecánico):
  aFemur = (elev + femurRad) * 180.0f / M_PI - 10.0f;
  aTibia = (M_PI - tibiaRad) * 180.0f / M_PI - 10.0f;

  // Ajuste de simetría si es lado derecho
  if (isRightSide) {
    aCoxa = -aCoxa;
  }

  // Limitar a rangos seguros
  if (aCoxa < COXA_MIN) aCoxa = COXA_MIN;
  if (aCoxa > COXA_MAX) aCoxa = COXA_MAX;
  if (aFemur < FEMUR_MIN) aFemur = FEMUR_MIN;
  if (aFemur > FEMUR_MAX) aFemur = FEMUR_MAX;
  if (aTibia < TIBIA_MIN) aTibia = TIBIA_MIN;
  if (aTibia > TIBIA_MAX) aTibia = TIBIA_MAX;

  // Convertir a referencia 0..180 para servo:
  // Suponemos mid (90) como neutro mecánico aproximado.
  aCoxa = 90.0f + aCoxa;
  aFemur = 90.0f + aFemur;
  aTibia = 90.0f + aTibia;

  return true;
}

// ---------- Gait: trípode ----------
// Dos trípodes: {L0, R1, L2} y {R0, L1, R2}
uint8_t tripodA[3] = {0, 4, 2}; // índices de patas L0(0), R1(4), L2(2)
uint8_t tripodB[3] = {3, 1, 5}; // R0(3), L1(1), R2(5)

// Parámetros de movimiento:
float stepHeight = 25.0f;   // mm elevación
float stepLength = 40.0f;   // mm avance
float defaultX = 80.0f;     // mm delantero
float defaultY = 50.0f;     // mm lateral (separación)
float defaultZ = -60.0f;    // mm vertical (negativo hacia abajo)
uint16_t gaitPeriodMs = 550; // velocidad (ms por semiperiodo)

// Estado de control
enum Mode {MODE_STOP, MODE_FWD, MODE_BACK, MODE_LEFT, MODE_RIGHT, MODE_STEP};
Mode mode = MODE_STOP;

// Parser de comandos
String cmdBuffer;

// Sensor VL53L0X
Adafruit_VL53L0X lox;
uint16_t obstacleThresholdMm = 200; // parar si < 200 mm
bool obstacleDetected = false;

// Temporización
uint32_t lastGaitTick = 0;

// Posiciones base de cada pata en marco del cuerpo
Vec3 basePos[6];

void initBasePos() {
  // Asumir simetría: Y positivo lado izquierdo, negativo derecho
  basePos[0] = { defaultX,  defaultY, defaultZ}; // L0
  basePos[1] = { 0.0f,      defaultY, defaultZ}; // L1
  basePos[2] = {-defaultX,  defaultY, defaultZ}; // L2
  basePos[3] = { defaultX, -defaultY, defaultZ}; // R0
  basePos[4] = { 0.0f,     -defaultY, defaultZ}; // R1
  basePos[5] = {-defaultX, -defaultY, defaultZ}; // R2
}

void applyLegAngles(uint8_t legIdx, float aCoxa, float aFemur, float aTibia) {
  const LegMap &lm = legs[legIdx];
  setServoAngle(lm.coxa, aCoxa);
  setServoAngle(lm.femur, aFemur);
  setServoAngle(lm.tibia, aTibia);
}

void moveLegTo(uint8_t legIdx, const Vec3 &p) {
  float aC,aF,aT;
  bool rightSide = (legIdx >= 3);
  if (ikSolve(p, aC, aF, aT, rightSide)) {
    applyLegAngles(legIdx, aC, aF, aT);
  }
}

// Genera objetivo por pata según fase del trípode
void computeTargets(float phase, Vec3 outTargets[6]) {
  // phase: 0..1 (semiperiodo) alterna trípodes A/B
  // movimiento según modo:
  float dx = 0, dy = 0;
  if (mode == MODE_FWD) dx = stepLength;
  else if (mode == MODE_BACK) dx = -stepLength;
  else if (mode == MODE_LEFT) dy = stepLength;
  else if (mode == MODE_RIGHT) dy = -stepLength;

  // Para cada pata, si pertenece al grupo activo en esta mitad, elevar y avanzar, si no, retroceder a ras de suelo
  auto inTripod = [&](uint8_t leg, const uint8_t group[3]) {
    return (leg == group[0] || leg == group[1] || leg == group[2]);
  };

  const uint8_t* activeGroup = ((millis() / gaitPeriodMs) % 2 == 0) ? tripodA : tripodB;

  for (uint8_t i=0; i<6; ++i) {
    Vec3 base = basePos[i];
    Vec3 p = base;
    float t = phase; // 0..1 normalizado

    if (mode == MODE_STOP) {
      // Mantener base
      // Si hay STEP, se maneja externamente
    } else {
      if (inTripod(i, activeGroup)) {
        // Fase de swing (elevar y mover hacia adelante)
        float lift = stepHeight * sinf(t * M_PI); // campana
        p.z = base.z + lift;
        p.x = base.x + dx * (t - 0.5f); // centrado
        p.y = base.y + dy * (t - 0.5f);
      } else {
        // Fase de stance (contacto, mover hacia atrás de forma complementaria)
        p.z = base.z; // suelo
        p.x = base.x - dx * (t - 0.5f);
        p.y = base.y - dy * (t - 0.5f);
      }
    }

    outTargets[i] = p;
  }
}

void allLegsToHome() {
  for (uint8_t i=0; i<6; ++i) {
    moveLegTo(i, basePos[i]);
  }
}

// ---------- Comunicación por Zigbee (Serial1, transparente) ----------
void handleSerial1() {
  while (Serial1.available()) {
    char c = (char)Serial1.read();
    if (c == '\r') continue;
    if (c == '\n') {
      processCommand(cmdBuffer);
      cmdBuffer = "";
    } else {
      if (cmdBuffer.length() < 64)
        cmdBuffer += c;
    }
  }
}

void sendStatus(const String &s) {
  Serial1.print("[HEX] ");
  Serial1.println(s);
  Serial.print("[HEX] ");
  Serial.println(s);
}

void processCommand(const String &cmd) {
  String up = cmd;
  up.trim();
  up.toUpperCase();

  if (up == "FWD") { mode = MODE_FWD; sendStatus("MODE=FWD"); }
  else if (up == "BACK") { mode = MODE_BACK; sendStatus("MODE=BACK"); }
  else if (up == "LEFT") { mode = MODE_LEFT; sendStatus("MODE=LEFT"); }
  else if (up == "RIGHT") { mode = MODE_RIGHT; sendStatus("MODE=RIGHT"); }
  else if (up == "STOP") { mode = MODE_STOP; sendStatus("MODE=STOP"); allLegsToHome(); }
  else if (up.startsWith("SPEED ")) {
    int v = up.substring(6).toInt();
    if (v >= 300 && v <= 1200) {
      gaitPeriodMs = (uint16_t)v;
      sendStatus("SPEED(ms)=" + String(gaitPeriodMs));
    } else {
      sendStatus("ERR SPEED [300..1200]");
    }
  } else if (up.startsWith("STEP")) {
    mode = MODE_STEP;
    sendStatus("MODE=STEP (single)");
  } else if (up.startsWith("CALIB ")) {
    // Formato: CALIB leg dof offset_us
    // leg=0..5, dof=0(coxa)/1(femur)/2(tibia)
    int leg=-1, dof=-1, off=0;
    int n = sscanf(up.c_str(), "CALIB %d %d %d", &leg, &dof, &off);
    if (n==3 && leg>=0 && leg<6 && dof>=0 && dof<3 && off>-500 && off<500) {
      uint8_t ch = (dof==0) ? legs[leg].coxa : (dof==1 ? legs[leg].femur : legs[leg].tibia);
      servoOffsetUs[ch] = off;
      sendStatus("CALIB OK leg="+String(leg)+" dof="+String(dof)+" off="+String(off));
      // Reaplica home con nuevo offset
      moveLegTo(leg, basePos[leg]);
    } else {
      sendStatus("ERR CALIB usage: CALIB leg(0..5) dof(0..2) offset(-499..499)");
    }
  } else {
    sendStatus("UNKNOWN CMD: " + up);
  }
}

// ---------- VL53L0X ----------
bool readObstacle(uint16_t &mm) {
  VL53L0X_RangingMeasurementData_t measure;
  lox.rangingTest(&measure, false); // no debug
  if (measure.RangeStatus == 4) { // out of range
    return false;
  } else {
    mm = measure.RangeMilliMeter;
    return true;
  }
}

// ---------- Setup ----------
void setup() {
  Serial.begin(115200);
  Serial1.begin(115200); // XBee
  delay(50);

  Wire.begin();
  Wire.setClock(400000); // I2C rápido
  pca.begin();
  pca.setPWMFreq(SERVO_FREQ);
  delay(10);

  if (!lox.begin()) {
    Serial.println("VL53L0X no detectado. Verifique conexiones.");
  } else {
    Serial.println("VL53L0X listo.");
  }

  initBasePos();
  allLegsToHome();

  Serial.println("Hexapod listo. Comandos: FWD/BACK/LEFT/RIGHT/STOP/SPEED n/STEP/CALIB leg dof off");
  sendStatus("READY");
}

// ---------- Loop ----------
void loop() {
  handleSerial1();

  // Sensado de obstáculo
  uint16_t dist;
  if (readObstacle(dist)) {
    if (dist < obstacleThresholdMm) {
      if (!obstacleDetected) {
        obstacleDetected = true;
        mode = MODE_STOP;
        sendStatus("OBSTACLE " + String(dist) + "mm -> STOP");
        allLegsToHome();
      }
    } else {
      if (obstacleDetected) {
        obstacleDetected = false;
        sendStatus("PATH CLEAR");
      }
    }
  }

  // Gait update
  uint32_t now = millis();
  if (now - lastGaitTick >= 20) { // 50 Hz update
    lastGaitTick = now;

    // Calcular fase del semiperiodo
    uint32_t t = now % gaitPeriodMs;
    float phase = (float)t / (float)gaitPeriodMs; // 0..1

    Vec3 targets[6];
    computeTargets(phase, targets);

    if (mode == MODE_STEP) {
      // Ejecuta una mitad de ciclo y luego STOP
      static bool stepStarted = false;
      static uint32_t stepStartTime = 0;
      if (!stepStarted) {
        stepStarted = true;
        stepStartTime = now;
      }
      for (uint8_t i=0; i<6; ++i) moveLegTo(i, targets[i]);
      if (now - stepStartTime >= gaitPeriodMs) {
        mode = MODE_STOP; stepStarted = false;
        allLegsToHome();
        sendStatus("STEP DONE");
      }
    } else if (mode == MODE_STOP) {
      // Mantener home
      // no-op para evitar jitter
    } else {
      for (uint8_t i=0; i<6; ++i) moveLegTo(i, targets[i]);
    }
  }
}

Breve explicación de las partes clave:
– Configuración del PCA9685 a 50 Hz (SERVO_FREQ = 50) y conversión de microsegundos a ticks (usToTicks).
– Estructura LegMap para asociar canales a cada DOF de cada pata.
– IK simplificada: separa rotación de coxa (atan2 en planta) y resuelve fémur/tibia con ley del coseno en el plano r-z.
– Gait trípode: alterna grupos A y B; una mitad eleva y avanza, la otra mitad mantiene contacto y “arrastra” en sentido contrario para el ciclo de paso.
– Parser de comandos (Serial1) para XBee en modo transparente a 115200 bps.
– VL53L0X: al detectar un obstáculo por debajo de 200 mm, detiene el movimiento y centra.
– Calibración en caliente por offset por-servo (microsegundos) con comando CALIB.

Compilación/flash/ejecución

Asegúrate de tener conectado el Arduino Mega 2560 por USB. Identifica el puerto (Linux suele ser /dev/ttyACM0 o /dev/ttyUSB0).

1) Instalar Arduino CLI 0.35.3 (Linux):

# Descarga e instalación
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=$HOME/.local/bin sh -s 0.35.3

# Verificar versión
$HOME/.local/bin/arduino-cli version
# arduino-cli Version: 0.35.3

# Añadir al PATH (opcional, si no lo tienes):
echo 'export PATH=$HOME/.local/bin:$PATH' >> ~/.bashrc
source ~/.bashrc

2) Configurar core y bibliotecas exactas:

arduino-cli core update-index
arduino-cli core install arduino:avr@1.8.6

arduino-cli lib install "Adafruit PWM Servo Driver Library@2.4.1"
arduino-cli lib install "Adafruit BusIO@1.14.5"
arduino-cli lib install "Adafruit Unified Sensor@1.1.14"
arduino-cli lib install "Adafruit VL53L0X@1.3.1"

3) Compilar (FQBN para Mega 2560: arduino:avr:mega):

# Posicionarse en el directorio que contiene la carpeta servo_hexapod_zigbee/
arduino-cli compile --fqbn arduino:avr:mega servo_hexapod_zigbee

4) Subir al Mega 2560:

# Sustituye /dev/ttyACM0 por el puerto correcto de tu sistema
arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:avr:mega servo_hexapod_zigbee

5) Monitor serie (USB) para logs de depuración:

arduino-cli monitor -p /dev/ttyACM0 -c baudrate=115200

6) Puente Zigbee (lado PC) con minicom (para enviar comandos al robot):

# Detecta el puerto del adaptador XBee USB, por ejemplo /dev/ttyUSB0
sudo apt-get install -y minicom
minicom -b 115200 -D /dev/ttyUSB0
# Escribe comandos: FWD, BACK, LEFT, RIGHT, STOP, SPEED 600, STEP, CALIB 0 1 20, etc.

Opcional (Python 3.10.12) para transmitir comandos al XBee remoto:

# archivo: send_zigbee.py
import sys, time, serial

port = "/dev/ttyUSB0"  # Cambia según tu sistema
baud = 115200
cmd = "FWD"            # o cualquier comando

if len(sys.argv) > 1:
    cmd = " ".join(sys.argv[1:])

with serial.Serial(port, baud, timeout=1) as ser:
    ser.write((cmd + "\n").encode("ascii"))
    time.sleep(0.1)
    # Leer respuesta del robot (eco por Zigbee)
    while ser.in_waiting:
        print(ser.readline().decode(errors="ignore").rstrip())

Ejecuta:

python3 send_zigbee.py "SPEED 600"
python3 send_zigbee.py "FWD"

Validación paso a paso

1) Verificación de I2C:
– Al abrir el monitor serie (USB), el programa debe imprimir “VL53L0X listo.” si el sensor es detectado. Si no, revisa SDA/SCL y VCC/GND.
– Los servos deberían llevar cada pata a su postura base (home) sin vibraciones notables.

2) Verificación de PCA9685 y servos:
– Debes observar un movimiento suave a posiciones centradas al iniciar.
– Si alguna articulación se mueve en sentido incorrecto, ajusta servoDir[] en el código y vuelve a subir.
– Ajusta offsets con CALIB para centrar mecánicamente:
– Ejemplo: “CALIB 0 1 20” corrige +20 us el fémur de L0.

3) Verificación del XBee (modo transparente):
– Desde minicom en el XBee remoto, envía “STOP”. Debes ver un eco “[HEX] MODE=STOP” tanto en minicom como en el monitor USB del Arduino.
– Envía “SPEED 700” y luego “FWD”. El robot debe iniciar un paso de trípode hacia adelante sin bloqueos.

4) Validación de locomoción:
– Con “FWD” el robot alterna trípodes. Observa que tres patas se elevan mientras las otras tres soportan.
– Cambia a “LEFT” o “RIGHT” y confirma el giro sobre el plano (ajuste lateral con dy).

5) Evitación con VL53L0X:
– Acerca un objeto al sensor frontal. Cuando esté a menos de ~200 mm, el robot debe emitir “[HEX] OBSTACLE xxxmm -> STOP” y volver a home.
– Al retirar el obstáculo, verás “[HEX] PATH CLEAR” y podrás reanudar con “FWD”.

6) Comando “STEP”:
– Ejecuta un semiperiodo de la marcha y vuelve a STOP automáticamente con “STEP DONE”.

7) Estabilidad eléctrica:
– Durante “FWD” sostenido, verifica que la fuente de servos no cae de tensión (ni reinicios del Mega).
– Toca el PCA9685: no debe estar excesivamente caliente. Si lo está, revisa la corriente de los servos y el duty.

8) Revisión de sincronía:
– Modifica “SPEED n” y evalúa la correspondencia entre n y la rapidez del paso. Valores muy bajos (<300 ms) pueden causar movimientos bruscos.

Troubleshooting

1) Servos tiemblan o se resetea el Mega al mover:
– Causa: falta de corriente o caída de tensión en V+ o masa no común.
– Solución: usa una fuente de 6–7.4 V con suficiente amperaje; une GND de fuente y GND del Mega; emplea cables gruesos y cortos.

2) VL53L0X no detectado (“no detectado” al iniciar):
– Causa: SDA/SCL invertidos, falta de VCC, mal módulo (3.3 V-only), nivel I2C incorrecto.
– Solución: en el Mega, SDA=20 y SCL=21; verifica VCC según módulo; prueba a 3.3 V si tu breakout no es 5V tolerant; revisa soldaduras.

3) Un grupo de servos se mueve al revés (pata gira al lado opuesto):
– Causa: inversión lógica no ajustada.
– Solución: cambia el signo en servoDir[ch] correspondiente (1 a -1). Recalibra con CALIB si es necesario.

4) No hay comunicación Zigbee (no respuestas en minicom):
– Causa: PAN ID distinto, baudios no coinciden, no están en misma red (coordinator/router).
– Solución: reconfigura ambos XBee con ATID igual, ATBD7 (115200), uno con ATCE1 (coordinator, lado PC) y el del robot con ATCE0 (router). Guarda con ATWR.

5) Compilación falla por bibliotecas:
– Causa: versiones no instaladas o dependencias faltantes.
– Solución: reinstala exactamente:
– arduino-cli lib install «Adafruit PWM Servo Driver Library@2.4.1»
– arduino-cli lib install «Adafruit BusIO@1.14.5»
– arduino-cli lib install «Adafruit Unified Sensor@1.1.14»
– arduino-cli lib install «Adafruit VL53L0X@1.3.1»

6) Servos no se mueven pero log por USB es correcto:
– Causa: PCA9685 sin V+ de potencia o sin GND común.
– Solución: aplica 6–7.4 V en V+ del PCA9685, conecta GND de la fuente a GND del PCA y del Mega.

7) Movimientos violentos al iniciar:
– Causa: offsets no calibrados o límites de ángulo mal configurados.
– Solución: ajusta servoOffsetUs[] y límites (COXA/FEMUR/TIBIA min/max). Empieza con allLegsToHome() antes de gait.

8) Lecturas erráticas de ToF:
– Causa: superficie brillante, ambiente IR saturado, tiempo de integración corto.
– Solución: promedia lecturas, sube el umbral o filtra por estados. Asegúrate de no tapar el sensor.

Mejoras/variantes

  • Migrar a API mode (AP=2) en XBee:
  • Permite direccionamiento a múltiples nodos, ACK y telemetría estructurada. Usa una librería XBee para frame parsing y añade CRC/timeout.

  • Calibración avanzada:

  • Guardar offsets y parámetros en EEPROM del Mega.
  • Añadir rutina de auto-calibración por guías impresas y un láser de referencia.

  • Gaits adicionales:

  • Ripple gait (estabilidad máxima), wave gait (lento y estable), amble (suavidad).
  • Transiciones suaves entre gaits según terreno.

  • Planificación local:

  • Usar el VL53L0X para mantener distancia objetivo, no solo stop. Integrar dos VL53L0X (frontal y lateral), cambiando dirección I2C de uno con pin XSHUT.

  • Telemetría:

  • Reporte periódico de estado (modo, distancia, velocidad, tensión batería).
  • Mando Zigbee con joystick o GUI en PC.

  • Gestión de energía:

  • Añadir sensor de corriente y voltaje (INA219) para limitar esfuerzos.
  • Modo reposo automático si no hay comando en N segundos.

  • Suavizado de trayectorias:

  • Interpolación spline/coseno para cada DOF, control feedforward de velocidad y aceleración.

  • Seguridad:

  • Watchdog (WDT) en el ATmega2560, kill-switch por comando, y botón físico de emergencia en línea V+.

Checklist de verificación

  • [ ] Arduino Mega 2560 detectado por el sistema (puerto serie visible).
  • [ ] Arduino CLI 0.35.3 instalado y en PATH.
  • [ ] Core arduino:avr@1.8.6 instalado sin errores.
  • [ ] Bibliotecas instaladas con versiones: Adafruit PWM Servo Driver 2.4.1, BusIO 1.14.5, Unified Sensor 1.1.14, VL53L0X 1.3.1.
  • [ ] PCA9685 conectado a SDA(20)/SCL(21), VCC=5 V, GND común; V+ con 6–7.4 V externa.
  • [ ] VL53L0X conectado al mismo bus I2C y detectado (mensaje “VL53L0X listo.”).
  • [ ] XBee S2C en el robot conectado a Serial1 (TX1=18, RX1=19) a 115200 bps vía shield/conversor nivel.
  • [ ] XBee remoto en PC configurado con el mismo PAN ID y baudios; se reciben ecos [HEX] en minicom al enviar comandos.
  • [ ] “STOP” recentra patas; “FWD/BACK/LEFT/RIGHT” mueven con trípode; “SPEED n” ajusta ritmo (300–1200 ms).
  • [ ] Acercar un objeto < 200 mm detiene el robot con mensaje de obstáculo.
  • [ ] Offsets calibrados por pata/DOF con “CALIB leg dof off” para posturas neutras.

Con esta práctica culminada, dispones de un servo-hexápodo controlado por Zigbee con un pipeline de toolchain reproducible (Arduino CLI), sensado ToF VL53L0X e integración estable de PCA9685 para 18 servos en un Arduino Mega 2560. El proyecto es una base sólida para evolucionar hacia control cinemático más avanzado, gaits adaptativos y redes Zigbee multipunto.

Encuentra este producto y/o libros sobre este tema en Amazon

Ir a Amazon

Como afiliado de Amazon, gano con las compras que cumplan los requisitos. Si compras a través de este enlace, ayudas a mantener este proyecto.

Quiz rápido

Pregunta 1: ¿Cuál es el sistema operativo recomendado para el proyecto?




Pregunta 2: ¿Qué versión de Arduino CLI se debe utilizar?




Pregunta 3: ¿Qué hardware se utiliza como driver de 16 canales servo?




Pregunta 4: ¿Cuál es la dirección por defecto del PCA9685?




Pregunta 5: ¿Qué tipo de sensor es el VL53L0X?




Pregunta 6: ¿Qué tipo de conversor se necesita para conectar el XBee S2C al Arduino Mega 2560?




Pregunta 7: ¿Qué biblioteca de Arduino se necesita para controlar el PCA9685?




Pregunta 8: ¿Qué módulo se utiliza para la comunicación Zigbee?




Pregunta 9: ¿Cuál es la opción de terminal serie recomendada para Linux?




Pregunta 10: ¿Cuántos servos se utilizan en el hexápodo del proyecto?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Ingeniero Superior en Electrónica de Telecomunicaciones e Ingeniero en Informática (titulaciones oficiales en España).

Sígueme: