Caso práctico: Logger cadena de frío Arduino MKR GSM 1400

Caso práctico: Logger cadena de frío Arduino MKR GSM 1400 — hero

Objetivo y caso de uso

Qué construirás: Un registrador de datos de cadena de frío utilizando Arduino MKR GSM 1400, DS3231, MicroSD SPI y DS18B20 para monitoreo de temperatura.

Para qué sirve

  • Monitoreo de temperatura en transporte de productos farmacéuticos.
  • Registro de condiciones ambientales en el almacenamiento de alimentos perecederos.
  • Control de temperatura en laboratorios de investigación.
  • Alertas en tiempo real para condiciones fuera de rango.

Resultado esperado

  • Registro de temperatura cada 5 minutos con precisión de ±0.5°C.
  • Envío de datos a la nube con una frecuencia de 1 paquete cada 10 minutos.
  • Latencia de respuesta de alertas de temperatura superior a 2 segundos.
  • Capacidad de almacenar hasta 1,000 registros en la tarjeta MicroSD.

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

Arquitectura/flujo: Arduino MKR GSM 1400 <-> DS18B20 (sensor) <-> DS3231 (reloj) <-> MicroSD (almacenamiento) <-> MQTT (comunicación)

Nivel: Avanzado

Prerrequisitos

  • Sistemas operativos probados:
  • Windows 11 23H2 (64-bit) o Windows 10 22H2 (64-bit).
  • Ubuntu 22.04 LTS (x86_64).
  • macOS 13.6 Ventura o macOS 14 Sonoma (Apple Silicon o Intel).

  • Toolchain exacta (línea de comandos, sin IDE gráfico):

  • Python 3.11.6
  • PlatformIO Core 6.1.15
  • Plataforma de compilación SAMD para PlatformIO:
    • platform = atmelsam@8.1.0
    • framework = arduino (ArduinoCore-samd 1.8.13)
  • Librerías Arduino (versionadas, a instalar vía PlatformIO):

    • arduino-libraries/MKRGSM@1.5.1
    • adafruit/RTClib@2.1.4
    • paulstoffregen/OneWire@2.3.7
    • milesburton/DallasTemperature@3.11.0
    • arduino-libraries/SD@1.2.4
  • Requisitos de hardware/driver:

  • Arduino MKR GSM 1400 usa USB CDC nativo. No requiere drivers en macOS y Linux modernos.
  • Windows 10/11: el controlador CDC-ACM se instala automáticamente. Si no aparece el puerto, actualizar Windows Update o usar el driver del paquete Arduino SAMD (instalado al usar Arduino IDE, opcional).
  • Permisos en Linux: agregar el usuario al grupo dialout para poder abrir el puerto serie (ver sección de compilación/ejecución).

  • Conectividad:

  • SIM nano con datos activos y APN conocido de tu operador (PIN opcional). Se usará GSM/GPRS para publicar los registros.
  • Cobertura GSM suficiente en el lugar de pruebas (2G/3G dependiendo del módem/operador).

Materiales

  • Arduino MKR GSM 1400 (modelo exacto ABX00018).
  • Módulo RTC DS3231 (compatible 3.3 V, con batería CR2032 para respaldo).
  • MicroSD SPI (soporte de tarjeta microSD a 3.3 V) + nivelador/buffer CD74HC4050.
  • CD74HC4050 como buffer unidireccional para líneas de salida del MKR hacia la MicroSD.
  • Nota: el CD74HC4050 se alimenta a 3.3 V; MISO no pasa por el 4050 (es entrada a la MCU).
  • Tarjeta microSD clase 10 (formateada FAT32).
  • Sensor de temperatura DS18B20 (cápsula TO-92 o sonda impermeable) a 3.3 V.
  • Resistencia 4.7 kΩ (pull-up del bus 1-Wire del DS18B20).
  • Antena para el MKR GSM 1400 (obligatoria para radio).
  • SIM con datos (APN/usuario/contraseña, PIN si aplica).
  • Cables Dupont/M-F/M-M según módulos.
  • Fuente de alimentación:
  • USB 5 V estable para desarrollo.
  • Recomendado: batería LiPo 3.7 V conectada al conector JST del MKR para evitar caídas de tensión durante ráfagas GSM (opcional pero recomendable).
  • Consumibles:
  • Batería CR2032 para el DS3231.
  • Cinta térmica o abrazaderas para fijar la sonda DS18B20 a paquetes fríos.

Preparación y conexión

Consideraciones generales de señal y alimentación

  • El MKR GSM 1400 trabaja a 3.3 V. No aplicar 5 V a sus entradas.
  • El módulo microSD y el DS3231 deben alimentarse a 3.3 V. Muchos módulos de mercado incluyen regulador a 3.3 V; si tu módulo microSD está diseñado para 5 V, es preferible usar uno nativo 3.3 V. En este caso incluimos el CD74HC4050 como buffer/aislador unidireccional en SCK, MOSI y CS.
  • La línea MISO (desde la MicroSD al MKR) no debe atravesar el CD74HC4050 (el 4050 es unidireccional: de entrada a salida); se conecta directamente a la entrada MISO del MKR (tensión 3.3 V compatible).
  • El DS18B20 necesita un pull-up de 4.7 kΩ entre su línea de datos y 3.3 V (si tu cable es largo, podrías necesitar ajustar el valor o la topología para integridad de señal).

Tabla de conexiones

La SPI del MKR GSM 1400 está expuesta en el header SPI (MOSI/MISO/SCK). No uses pines digitales numerados para MOSI/MISO/SCK; usa los pines etiquetados del conector SPI. El pin CS sí puede ser cualquier GPIO (usaremos D4).

Función Componente Pin en MKR GSM 1400 A través del CD74HC4050 Pin del módulo Notas
Alimentación 3.3 V Todos 3V3 N/A VCC 3.3 V a DS3231, MicroSD y CD74HC4050.
Tierra Todos GND N/A GND Masa común para todos los módulos.
SPI MOSI MicroSD MOSI (cabecera SPI) Sí (buffer) DI (MOSI) Conectar MOSI del MKR a entrada del 4050; salida del 4050 al DI.
SPI MISO MicroSD MISO (cabecera SPI) No DO (MISO) Directo MicroSD→MKR (3.3 V).
SPI SCK MicroSD SCK (cabecera SPI) Sí (buffer) SCK MKR SCK→4050→SCK de MicroSD.
SPI CS (GPIO) MicroSD D4 Sí (buffer) CS Selección de chip; define D4 en el firmware.
I2C SDA DS3231 SDA No SDA I2C a 3.3 V.
I2C SCL DS3231 SCL No SCL I2C a 3.3 V.
1-Wire DAT DS18B20 D5 No DQ Añadir pull-up 4.7 kΩ entre D5 y 3.3 V.
Alimentación DS18B20 DS18B20 3V3 N/A VDD Modo alimentación normal (no parasitario).
Tierra DS18B20 DS18B20 GND N/A GND
VCC CD74HC4050 3V3 N/A VCC Alimentar el 4050 a 3.3 V para nivel lógico correcto.
Entradas 4050 Desde MKR (MOSI, SCK, D4) MOSI, SCK, D4 N/A 4050-IN Entradas del buffer desde el MKR.
Salidas 4050 Hacia MicroSD (DI, SCK, CS) N/A N/A 4050-OUT Salidas del buffer hacia el módulo MicroSD.
Antena GSM MKR GSM 1400 Conector u.FL N/A Antena Conecta la antena antes de encender el módem.
SIM MKR GSM 1400 Ranura SIM N/A SIM Inserta la nanoSIM con datos activos.

Preparación previa a energizar

  1. Formatea la microSD a FAT32 (tamaño de clúster por defecto).
  2. Inserta la CR2032 en el DS3231 (asegura polaridad).
  3. Inserta la nanoSIM y conecta la antena GSM al MKR.
  4. Alimenta todo por USB. Para pruebas con GSM se recomienda además conectar una batería LiPo al conector JST del MKR para evitar resets por picos de corriente (el módem puede requerir >1.5 A en ráfagas muy cortas).
  5. Ten a mano los datos del APN de tu operador (APN, usuario, contraseña) y el PIN de la SIM si estuviera activo.

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

A continuación se muestra un firmware con:
– Registro periódico en microSD (CSV).
– Timestamps desde DS3231.
– Lectura de temperatura DS18B20.
– Umbrales configurables de cadena de frío (p. ej. 2 °C a 8 °C).
– Publicación periódica por GSM vía HTTP POST.
– Retardo no bloqueante con millis().

Archivo: platformio.ini (en la raíz del proyecto):

[env:mkrgsm1400]
platform = atmelsam@8.1.0
board = mkrgsm1400
framework = arduino
monitor_speed = 115200
lib_deps =
  arduino-libraries/MKRGSM@1.5.1
  adafruit/RTClib@2.1.4
  paulstoffregen/OneWire@2.3.7
  milesburton/DallasTemperature@3.11.0
  arduino-libraries/SD@1.2.4
build_flags =
  -DARDUINOJSON_USE_LONG_LONG=1

Archivo: src/main.cpp

#include <Arduino.h>
#include <MKRGSM.h>
#include <Wire.h>
#include <RTClib.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <SPI.h>
#include <SD.h>

// =========================
// Configuración del proyecto
// =========================

// SIM/APN
static const char SIM_PIN[] = "";              // PIN de la SIM o "" si no tiene
static const char APN[] = "YOUR_APN";          // Cambiar por el APN de tu operador
static const char APN_USER[] = "";             // Usuario APN si aplica
static const char APN_PASS[] = "";             // Contraseña APN si aplica

// Publicación HTTP
static const char HTTP_HOST[] = "httpbin.org"; // Servidor de pruebas
static const int  HTTP_PORT = 80;
static const char HTTP_PATH[] = "/post";       // Ruta POST

// Umbrales de cadena de frío
static const float TEMP_MIN_C = 2.0f;          // °C
static const float TEMP_MAX_C = 8.0f;          // °C
static const uint32_t BREACH_HOLDOFF_MS = 10000; // Persistencia (10 s) antes de disparar alarma

// Logging y temporización
static const uint32_t SAMPLE_INTERVAL_MS = 60000; // 60 s
static const uint32_t SEND_INTERVAL_MS   = 300000; // 5 min
static const char LOG_FILENAME[] = "/coldchain.csv";

// Pines
static const uint8_t PIN_SD_CS   = 4;          // CS de microSD (D4), vía CD74HC4050
static const uint8_t PIN_1WIRE   = 5;          // D5 para DS18B20 con pull-up 4.7k a 3.3V

// =========================
// Objetos globales
// =========================
RTC_DS3231 rtc;

OneWire oneWire(PIN_1WIRE);
DallasTemperature dallas(&oneWire);

GSM gsmAccess;
GPRS gprs;
GSMClient netClient; // No-SSL. Para HTTPS usar GSMSSLClient (requiere certificados).

File logFile;

DeviceAddress dsAddr;
bool dsFound = false;

bool sdReady  = false;
bool gsmReady = false;

// Timers no bloqueantes
uint32_t t_lastSample = 0;
uint32_t t_lastSend   = 0;

// Estado de alarma
bool inBreach = false;
uint32_t breachSince = 0;

// =========================
// Utilidades
// =========================

String iso8601(const DateTime& dt) {
  char buf[25];
  snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d:%02d:%02dZ",
           dt.year(), dt.month(), dt.day(),
           dt.hour(), dt.minute(), dt.second());
  return String(buf);
}

void ensureRTCInit() {
  if (!rtc.begin()) {
    Serial.println(F("[RTC] Error: no se detecta DS3231 en I2C (0x68)."));
    return;
  }
  if (rtc.lostPower()) {
    Serial.println(F("[RTC] Se detectó pérdida de energía. Ajustando a tiempo de compilación."));
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
  }
  Serial.println(F("[RTC] OK"));
}

void ensureDallasInit() {
  dallas.begin();
  if (dallas.getDeviceCount() < 1) {
    Serial.println(F("[DS18B20] No se detectan sensores. Verifica cableado y pull-up."));
    dsFound = false;
    return;
  }
  dsFound = dallas.getAddress(dsAddr, 0);
  if (!dsFound) {
    Serial.println(F("[DS18B20] No se obtuvo dirección del primer sensor."));
    return;
  }
  dallas.setResolution(dsAddr, 12); // Máxima resolución
  Serial.print(F("[DS18B20] Sensor 0: "));
  for (uint8_t i = 0; i < 8; i++) {
    Serial.print(dsAddr[i], HEX); if (i < 7) Serial.print(":");
  }
  Serial.println();
}

bool ensureSDInit() {
  if (sdReady) return true;
  if (!SD.begin(PIN_SD_CS)) {
    Serial.println(F("[SD] Falló SD.begin(). Verifica CS, 4050 y formato FAT32."));
    sdReady = false;
    return false;
  }
  // Crear encabezado si no existe
  if (!SD.exists(LOG_FILENAME)) {
    File f = SD.open(LOG_FILENAME, FILE_WRITE);
    if (f) {
      f.println(F("timestamp,temp_c,status,breach"));
      f.close();
      Serial.println(F("[SD] Archivo creado con encabezado."));
    } else {
      Serial.println(F("[SD] No se pudo crear el archivo de log."));
      sdReady = false;
      return false;
    }
  }
  sdReady = true;
  Serial.println(F("[SD] OK"));
  return true;
}

float readTemperatureC() {
  if (!dsFound) return NAN;
  dallas.requestTemperatures();
  float c = dallas.getTempC(dsAddr);
  return c;
}

String statusFromTemp(float c) {
  if (isnan(c)) return "NA";
  if (c < TEMP_MIN_C) return "LOW";
  if (c > TEMP_MAX_C) return "HIGH";
  return "OK";
}

void appendLog(const String& line) {
  if (!ensureSDInit()) return;
  File f = SD.open(LOG_FILENAME, FILE_WRITE);
  if (f) {
    f.println(line);
    f.close();
  } else {
    Serial.println(F("[SD] Error al abrir el log para escritura."));
  }
}

bool ensureGSM() {
  if (gsmReady) return true;
  Serial.print(F("[GSM] Inicializando módem... "));
  bool connected = false;
  for (int i = 0; i < 3 && !connected; i++) {
    if (gsmAccess.begin(SIM_PIN) == GSM_READY) {
      connected = true;
      break;
    }
    delay(2000);
  }
  if (!connected) {
    Serial.println(F("Fallo."));
    return false;
  }
  Serial.println(F("OK"));

  Serial.print(F("[GPRS] Adjuntando a APN... "));
  if (gprs.attachGPRS(APN, APN_USER, APN_PASS)) {
    Serial.println(F("OK"));
    gsmReady = true;
    return true;
  } else {
    Serial.println(F("Fallo. Verifica APN/credenciales/cobertura."));
    gsmReady = false;
    return false;
  }
}

bool httpPostJSON(const String& host, int port, const String& path, const String& json) {
  if (!ensureGSM()) return false;

  Serial.print(F("[HTTP] Conectando a ")); Serial.print(host); Serial.print(F(":")); Serial.println(port);
  if (!netClient.connect(host.c_str(), port)) {
    Serial.println(F("[HTTP] No se pudo conectar."));
    return false;
  }

  String req = String("POST ") + path + " HTTP/1.1\r\n" +
               "Host: " + host + "\r\n" +
               "Content-Type: application/json\r\n" +
               "Connection: close\r\n" +
               "Content-Length: " + String(json.length()) + "\r\n\r\n" +
               json;

  netClient.print(req);

  // Leer respuesta simple
  uint32_t start = millis();
  bool ok = false;
  while (millis() - start < 10000) {
    while (netClient.available()) {
      String line = netClient.readStringUntil('\n');
      line.trim();
      if (line.startsWith("HTTP/1.1 200")) ok = true;
      // parar si fin de headers
      if (line.length() == 0) {
        // cuerpo siguiente (omitimos)
        break;
      }
    }
    if (!netClient.connected()) break;
  }
  netClient.stop();
  Serial.println(ok ? F("[HTTP] Respuesta 200 OK.") : F("[HTTP] Respuesta no OK."));
  return ok;
}

void publishLastSample(float c, const String& status, const String& ts, bool breach) {
  // JSON mínimo
  String json = String("{\"device\":\"mkrgsm1400\",\"ts\":\"") + ts +
                "\",\"temp_c\":" + String(c, 3) +
                ",\"status\":\"" + status +
                "\",\"breach\":" + (breach ? "true" : "false") + "}";

  httpPostJSON(HTTP_HOST, HTTP_PORT, HTTP_PATH, json);
}

void printBanner() {
  Serial.println();
  Serial.println(F("== cellular-cold-chain-sd-logger =="));
  Serial.println(F("HW: Arduino MKR GSM 1400 + DS3231 + MicroSD SPI (CD74HC4050) + DS18B20"));
  Serial.println(F("Toolchain: PlatformIO Core 6.1.15, atmelsam@8.1.0, ArduinoCore-samd 1.8.13"));
  Serial.println(F("Log: /coldchain.csv | Intervalo muestreo: 60 s | Envios: 5 min"));
  Serial.println();
}

// =========================
// Setup y loop
// =========================

void setup() {
  Serial.begin(115200);
  while (!Serial && millis() < 4000) { /* esperar CDC */ }
  printBanner();

  ensureRTCInit();
  ensureDallasInit();
  ensureSDInit();

  // Confirmar fecha/hora inicial
  DateTime now = rtc.now();
  Serial.print(F("[RTC] Tiempo actual: ")); Serial.println(iso8601(now));

  // Intentar arrancar GSM/GPRS temprano (no bloquear)
  ensureGSM();

  t_lastSample = millis();
  t_lastSend   = millis();
}

void loop() {
  uint32_t nowMs = millis();

  // Muestreo periódico
  if (nowMs - t_lastSample >= SAMPLE_INTERVAL_MS || t_lastSample == 0) {
    t_lastSample = nowMs;

    // Leer tiempo y temperatura
    DateTime now = rtc.now();
    String ts = iso8601(now);

    float tempC = readTemperatureC();
    String stat = statusFromTemp(tempC);

    // Lógica de brecha
    bool breachEvent = false;
    if (stat == "LOW" || stat == "HIGH") {
      if (!inBreach) {
        // inicia periodo de confirmación
        if (breachSince == 0) breachSince = nowMs;
        if (nowMs - breachSince >= BREACH_HOLDOFF_MS) {
          inBreach = true;
          breachEvent = true;
          Serial.println(F("[ALERTA] Temperatura fuera de rango persistente."));
        }
      }
    } else {
      inBreach = false;
      breachSince = 0;
    }

    // Línea CSV
    String line = ts;
    line += ",";
    if (isnan(tempC)) line += "NaN"; else line += String(tempC, 3);
    line += ",";
    line += stat;
    line += ",";
    line += (inBreach ? "1" : "0");

    Serial.print(F("[LOG] ")); Serial.println(line);
    appendLog(line);

    // Opcional: escribir un archivo de estado rápido
    File f = SD.open("/last.txt", FILE_WRITE);
    if (f) {
      f.seek(0);
      f.print("ts=");    f.println(ts);
      f.print("temp=");  f.println(isnan(tempC) ? String("NaN") : String(tempC, 3));
      f.print("status=");f.println(stat);
      f.print("breach=");f.println(inBreach ? "1" : "0");
      f.close();
    }
  }

  // Envío periódico por GSM
  if (nowMs - t_lastSend >= SEND_INTERVAL_MS || t_lastSend == 0) {
    t_lastSend = nowMs;

    DateTime now = rtc.now();
    String ts = iso8601(now);
    float tempC = readTemperatureC();
    String stat = statusFromTemp(tempC);

    if (!isnan(tempC)) {
      publishLastSample(tempC, stat, ts, inBreach);
    } else {
      Serial.println(F("[GSM] Se omite envío: temperatura NaN."));
    }
  }

  // Trabajo de fondo mínimo
  delay(10);
}

Puntos clave del código:
– Usa DS3231 como fuente de tiempo; si detecta pérdida de energía del RTC, fija el reloj a tiempo de compilación.
– Inicializa la SD y crea el archivo CSV con encabezado si no existe.
– Lee el DS18B20 a 12 bits y registra cada minuto: timestamp ISO8601, temperatura, estado y si hay brecha activa.
– Implementa retardo de confirmación antes de declarar una “brecha” (para evitar falsos positivos por transitorios).
– Publica cada 5 minutos un JSON pequeño al endpoint HTTP (no TLS) usando MKRGSM. Para producción, usa TLS con GSMSSLClient y certificados.

Compilación, flash y ejecución (comandos exactos)

1) Instalar PlatformIO Core 6.1.15:

  • Windows/macOS/Linux (recomendado con pipx para aislar):
python3 -m pip install --user pipx
python3 -m pipx ensurepath
pipx install "platformio==6.1.15"
platformio --version

Salida esperada (similar):
– PlatformIO Core, version 6.1.15

2) Inicializar el proyecto para MKR GSM 1400:

mkdir -p cellular-cold-chain-sd-logger
cd cellular-cold-chain-sd-logger
pio project init --board mkrgsm1400 --project-option "platform=atmelsam@8.1.0" --project-option "framework=arduino"

3) Sustituir platformio.ini con el contenido proporcionado antes y crear src/main.cpp:

mkdir -p src
# Copia y pega el platformio.ini y el src/main.cpp según el tutorial

4) Instalar dependencias (se instalarán automáticamente en el primer build, pero puedes forzarlo):

pio pkg install
pio lib --global install "arduino-libraries/MKRGSM@1.5.1" "adafruit/RTClib@2.1.4" "paulstoffregen/OneWire@2.3.7" "milesburton/DallasTemperature@3.11.0" "arduino-libraries/SD@1.2.4"

5) Compilar:

pio run

6) Conectar el MKR GSM 1400 por USB. En Linux, si no tienes permisos de puerto serie:

sudo usermod -aG dialout $USER
# cierra sesión y vuelve a entrar, o reinicia la sesión

7) Localizar el puerto serie:

pio device list
  • Windows: COMx (p. ej., COM5)
  • macOS: /dev/cu.usbmodemXXXX
  • Linux: /dev/ttyACM0

8) Subir firmware:

# Si PlatformIO detecta el puerto automáticamente:
pio run -t upload

# O especificando puerto:
pio run -t upload --upload-port /dev/ttyACM0

9) Abrir monitor serie a 115200 baudios:

pio device monitor -b 115200

Notas:
– Si el módem necesita varios segundos para adjuntarse a la red, verás reintentos en la consola.
– Conecta primero la antena antes de energizar el MKR.

Validación paso a paso

1) Arranque y diagnóstico inicial
– Abre el monitor serie. Debes ver:
– Banner con descripciones de HW/toolchain.
– [RTC] OK y hora actual en ISO8601. Si aparece “pérdida de energía”, el RTC se ajustó a la hora de compilación; configura el reloj manual si quieres exactitud total (puedes cambiar el ajuste en código a un método propio).
– [SD] OK y/o creación del archivo con encabezado.
– Detección del DS18B20 (imprime la dirección de 8 bytes).

2) Verificación del DS18B20
– Observa líneas [LOG] cada 60 s con el valor temp_c. Coloca el sensor en:
– Ambiente (~20–25 °C) para ver “OK” si los umbrales están 2–8 °C, verás “HIGH”.
– Mezcla de hielo y agua (~0 °C) para ver “LOW”.
– Confirma que el estado (“status”) coincide con tus expectativas.

3) Verificación del DS3231
– Desconecta USB, mantén la CR2032 y espera 2–3 min.
– Reconecta. La hora debe continuar avanzando correctamente (no reiniciarse).
– Si la hora es errónea, reemplaza la CR2032 y vuelve a compilar o añade una rutina para ajustar el RTC (e.g., desde host vía un comando serie).

4) Verificación de escritura en SD
– Tras varios ciclos, extrae la microSD y abre coldchain.csv:
– Debe tener encabezado y filas similares a:
– 2025-01-12T18:25:00Z,5.312,OK,0
– 2025-01-12T18:26:00Z,8.954,HIGH,1
– Alternativamente, deja la SD y revisa /last.txt con un lector si tu módulo lo permite (o quita la SD y léela en PC).

5) Verificación GSM/GPRS y HTTP
– En la consola, busca:
– [GSM] Inicializando módem… OK
– [GPRS] Adjuntando a APN… OK
– [HTTP] Conectando a httpbin.org:80
– [HTTP] Respuesta 200 OK.
– Las publicaciones se realizan cada 5 min (puedes bajar SEND_INTERVAL_MS para testear).
– Si no obtienes 200 OK:
– Verifica APN, cobertura, saldo de datos, y que no haya firewall bloqueando puertos outbound 80.

6) Verificación de detección de brecha
– Coloca el sensor fuera del rango (p. ej., agua tibia ~30 °C).
– Tras el tiempo de persistencia (10 s por BREACH_HOLDOFF_MS), debe mostrarse:
– [ALERTA] Temperatura fuera de rango persistente.
– En el CSV, la columna breach pasa a 1 mientras dure la brecha.

7) Estabilidad de alimentación (opcional pero recomendado)
– Si notas reinicios durante el adjunto GPRS o POST:
– Conecta una LiPo al MKR y repite.
– Observa que el sistema no reinicia durante picos de transmisión.

Troubleshooting (errores típicos y soluciones)

1) SD.begin() falla o el CSV no se crea
– Síntomas: [SD] Falló SD.begin().
– Causas probables:
– CS incorrecto: verifica que el firmware use D4 y que el cable vaya por la salida del 4050 al pin CS del módulo SD.
– Orden del CD74HC4050: recuerda que es unidireccional. Usa 4050 para MOSI, SCK y CS desde MCU→SD. No inserte el 4050 en MISO.
– MicroSD no formateada FAT32 o defectuosa.
– Alimentación inestable a 3.3 V.
– Solución:
– Revisa cableado, pinout y reemplaza la microSD o formatea (FAT32).
– Confirma continuidad de señales con multímetro.

2) No se detecta el DS18B20
– Síntomas: “[DS18B20] No se detectan sensores.”
– Causas:
– Falta resistencia de 4.7 kΩ entre D5 y 3.3 V.
– Sensor alimentado en modo parasitario sin configurar el firmware.
– Cableado invertido (GND/VDD/DQ).
– Solución:
– Añade pull-up de 4.7 kΩ.
– Verifica el pinout del encapsulado o sonda.
– Usa alimentación normal (VDD a 3.3V).

3) El RTC DS3231 marca hora errónea o constante
– Causas:
– Falta CR2032 o agotada.
– Módulo DS3231 a 5 V incompatible con 3.3 V (I2C pull-ups a 5 V).
– Solución:
– Cambia CR2032.
– Asegura que las resistencias de pull-up I2C vayan a 3.3 V (en algunos módulos, hay jumpers o resistencias que debes modificar).

4) GSM no registra o GPRS no adjunta
– Síntomas:
– [GSM] fallo, [GPRS] fallo.
– Causas:
– SIM con PIN no configurado (SIM_PIN vacío).
– APN incorrecto o credenciales faltantes.
– Cobertura insuficiente o antena desconectada.
– Solución:
– Ajusta SIM_PIN, APN, usuario, contraseña.
– Mueve el dispositivo a zona con mejor cobertura.
– Conecta correctamente la antena.

5) Reinicios aleatorios al enviar por GSM
– Causas:
– Picos de corriente del módem (hasta >1 A en burst).
– Solución:
– Usa una LiPo en el conector JST del MKR.
– Añade un condensador de reserva (e.g., 470–1000 µF) en la línea de 3.3/5 V según diseño total.

6) No aparece el puerto serie
– Windows:
– Prueba otro cable USB (datos, no solo carga).
– Reinstala el driver CDC (vía Arduino IDE si es necesario).
– macOS/Linux:
– Comprueba permisos (Linux: grupo dialout).
– Verifica con pio device list.
– Todos:
– Pulsa el botón de reset doble-rapido para forzar el bootloader (el puerto puede cambiar temporalmente).

7) HTTP responde distinto a 200 OK
– Causas:
– Servidor indisponible, cortafuegos operatorio, NAT peculiar.
– Solución:
– Prueba otro host (temporalmente un servidor propio) o un APN diferente.
– Considera activar TLS con GSMSSLClient y un endpoint HTTPS confiable (necesitarás cargar certificados).

8) Archivo CSV corrupto tras apagones
– Causa:
– Extracción de la SD mientras el archivo está abierto.
– Solución:
– El código abre/cierra por escritura atómica por línea. Minimiza riesgo. Aun así, evita desconectar durante escritura y usa alimentación estable.

Mejoras/variantes

  • Seguridad y transporte:
  • Migrar de HTTP a HTTPS con GSMSSLClient. Cargar certificados raíz al módem (proceso específico para MKRGSM; usar herramienta de certificados del core SAMD o scripts AT). Cambiar GSMClient por GSMSSLClient y ajustar puerto 443.
  • Usar MQTT sobre TLS para publicar a un broker (p. ej., AWS IoT Core) con autenticación por certificados.
  • Robustez de almacenamiento:
  • Rotación de logs (daily rolling): coldchain-YYYYMMDD.csv y purga automática según espacio libre.
  • Cálculo de checksum por línea (CRC32) para detección de corrupción.
  • Multi-sensor:
  • Gestionar múltiples DS18B20 en el mismo bus 1-Wire (identificación por dirección y columnas separadas).
  • Gestión de energía:
  • Dormir entre muestras (modo standby del SAMD21, apagar módem entre envíos). Despertar por RTC (alarma DS3231) si se desea.
  • Alertas activas:
  • Enviar SMS cuando se detecte brecha persistente (usar MKRGSM SMS).
  • Añadir buzzer y LED de estado (verde OK, rojo brecha).
  • Geolocalización aproximada:
  • Consultar Cell-ID a la red y adjuntar MCC/MNC/LAC/CI al payload para trazabilidad.
  • Configuración remota:
  • Leer umbrales, intervalos y APN desde un archivo config.ini en SD, o por comandos simples vía serie.
  • Integridad temporal:
  • Sincronización NTP por GPRS al arranque si el RTC perdió energía (UDP NTP en puerto 123) y actualización del DS3231.

Checklist de verificación

  • [ ] He instalado PlatformIO Core 6.1.15 y puedo ejecutar platformio --version.
  • [ ] He inicializado el proyecto con mkrgsm1400 y la plataforma atmelsam@8.1.0.
  • [ ] He copiado platformio.ini y src/main.cpp exactamente como en el tutorial.
  • [ ] He cableado la MicroSD por SPI usando el conector SPI del MKR y el CD74HC4050 en MOSI/SCK/CS, dejando MISO directo.
  • [ ] He conectado el DS3231 a SDA/SCL (3.3 V) y le he colocado una CR2032 funcional.
  • [ ] He cableado el DS18B20 al pin D5 con resistencia de 4.7 kΩ a 3.3 V.
  • [ ] He insertado la antena y la SIM en el MKR GSM 1400.
  • [ ] He configurado APN/usuario/contraseña y PIN en el código.
  • [ ] El monitor serie muestra [RTC] OK, [SD] OK y el DS18B20 detectado.
  • [ ] Veo líneas [LOG] cada minuto con timestamp y temperatura.
  • [ ] Se crea/actualiza coldchain.csv en la microSD con datos válidos.
  • [ ] Cada 5 min obtengo [HTTP] Respuesta 200 OK (o he ajustado APN/host hasta conseguirlo).
  • [ ] He probado condiciones de baja y alta temperatura y el estado cambia a LOW/HIGH; se marca breach cuando persiste.
  • [ ] El sistema no se reinicia durante transmisiones GSM (uso LiPo si fue necesario).

Con este caso práctico has construido un registrador de temperatura para cadena de frío con sello temporal por RTC, almacenamiento en microSD mediante SPI y publicación celular periódica, todo sobre Arduino MKR GSM 1400 y respetando el camino de señal con CD74HC4050. El proyecto es una base sólida para despliegues de campo y escalabilidad hacia comunicaciones seguras, configuración remota y optimización energética.

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 mínima de Windows requerida para el sistema operativo?




Pregunta 2: ¿Qué versión de Python se necesita para la toolchain?




Pregunta 3: ¿Cuál es la librería de Arduino que se debe instalar para el módulo RTC?




Pregunta 4: ¿Qué tipo de SIM se requiere para la conectividad?




Pregunta 5: ¿Cuál es el modelo exacto del Arduino requerido?




Pregunta 6: ¿Qué controlador se instala automáticamente en Windows 10/11?




Pregunta 7: ¿Cuál es la versión de la plataforma de compilación SAMD para PlatformIO?




Pregunta 8: ¿Qué tipo de batería se necesita para el módulo RTC DS3231?




Pregunta 9: ¿Qué permisos se deben configurar en Linux para abrir el puerto serie?




Pregunta 10: ¿Cuál es la versión de la librería DallasTemperature que se debe instalar?




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: Detección de palabras clave

Caso práctico: Detección de palabras clave — hero

Objetivo y caso de uso

Qué construirás: Un detector de palabras clave utilizando un Arduino Nano 33 IoT y un micrófono INMP441 I2S para procesar audio en tiempo real.

Para qué sirve

  • Detección de comandos de voz en sistemas de automatización del hogar.
  • Interacción con dispositivos IoT mediante palabras clave específicas.
  • Implementación en sistemas de asistencia personal que responden a órdenes vocales.

Resultado esperado

  • Latencia de detección de palabras clave inferior a 200 ms.
  • Precisión de detección superior al 85% en condiciones controladas.
  • Capacidad de procesar hasta 10 palabras clave simultáneamente.

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

Arquitectura/flujo: Captura de audio I2S -> Procesamiento de características -> Clasificación en tiempo real -> Respuesta a comandos.

Nivel: Avanzado

Prerrequisitos

Sistema operativo y herramientas (versiones probadas)

  • Sistema operativo
  • Ubuntu 22.04.4 LTS (x86_64). También válido en macOS 13 Ventura y Windows 11, ajustando rutas/comandos.
  • Toolchain para la placa
  • Arduino CLI v0.35.3
  • Core SAMD para Arduino: arduino:samd@1.8.14
  • Librerías Arduino:
    • Arduino_I2S@1.0.3
    • arduinoFFT@1.6.1
  • Toolchain científico (para entrenamiento de un clasificador simple)
  • Python 3.11.6
  • Paquetes:
    • numpy==1.26.4
    • scikit-learn==1.4.2
    • pyserial==3.5

Qué aprenderás y qué harás

  • Capturar audio por I2S a 16 kHz desde el micrófono INMP441 en un Arduino Nano 33 IoT.
  • Extraer características log-mel de ventana corta (MFBE) y aplicar DCT para obtener MFCC.
  • Entrenar un clasificador lineal (logistic regression) en PC con Python, exportando pesos a C++.
  • Embebido del clasificador en el firmware del Nano 33 IoT con inferencia en tiempo real.
  • Validar detecciones y afinar umbrales.

Materiales

  • 1x Arduino Nano 33 IoT (modelo exacto: ABX00032; MCU SAMD21G18A + NINA-W102; alimentación 3.3 V)
  • 1x Micrófono I2S INMP441 (breakout de 3.3 V; pines: VDD, GND, SCK/BCLK, WS/LRCLK, SD, L/R)
  • 1x Cable micro‑USB de datos para el Nano 33 IoT
  • Cables Dupont macho‑hembra para las conexiones
  • Opcional:
  • Protoboard
  • PC con puertos USB y Python 3.11

Nota sobre alimentación: el INMP441 opera a 3.3 V; no uses 5 V. El Nano 33 IoT trabaja íntegramente a 3.3 V, por lo que no necesitas conversores de nivel.

Preparación y conexión

Instalación del toolchain de Arduino CLI

  1. Instala Arduino CLI v0.35.3:
    «`bash
    # En Linux x86_64
    curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh
    sudo mv bin/arduino-cli /usr/local/bin/

# Verifica versión
arduino-cli version
# Debería mostrar: arduino-cli Version: 0.35.3
2. Instala el core SAMD y librerías necesarias:bash
arduino-cli core update-index
arduino-cli core install arduino:samd@1.8.14

# Librerías:
arduino-cli lib install «Arduino_I2S@1.0.3» «arduinoFFT@1.6.1»
3. Conecta el Nano 33 IoT por USB y lista puertos:bash
arduino-cli board list
# Ejemplo de salida en Linux:
# Port Type Board Name FQBN
# /dev/ttyACM0 Serial Port (USB) Arduino Nano 33 IoT arduino:samd:nano_33_iot
«`

Notas de driver:
– Linux/macOS: dispositivo CDC ACM /dev/ttyACM o /dev/cu.usbmodem; no necesitas drivers extra.
– Windows 10/11: Windows Update instala el driver CDC automáticamente.

Conexión eléctrica (I2S)

La interfaz I2S en el Nano 33 IoT está implementada en el MCU SAMD21. Para este caso práctico se utiliza el bus I2S en modo receptor con el micrófono INMP441 (no requiere MCLK). Configuración típica a 16 kHz, 32 bits por muestra (el INMP441 emite 24 bits válidos en 32).

Conecta según la tabla:

Señal INMP441 Señal I2S Pin Arduino Nano 33 IoT Notas
VDD 3V3 3V3 (pin 3.3V) Alimentación 3.3 V
GND GND GND Tierra común
SCK (BCLK) I2S BCLK D2 Reloj de bit
WS (LRCLK) I2S LRCLK D3 Reloj de palabra/canal
SD I2S SD D4 Datos de micrófono (entrada al Nano)
L/R Selección de canal GND (sugerido) GND = canal izquierdo; VDD = derecho

Observaciones:
– El INMP441 no requiere MCLK (Master Clock), lo cual simplifica el cableado.
– Mantén los cables de I2S lo más cortos posible.
– Asegura una masa común robusta entre placa y micrófono.

Verificación de pines I2S por firmware:
– En caso de duda, puedes confirmar en tiempo de compilación usando las macros del core SAMD: PIN_I2S_SCK, PIN_I2S_FS, PIN_I2S_SD. En este caso práctico usaremos D2/D3/D4 como asignación estándar para Nano 33 IoT.

Código completo (firmware Arduino) y explicación

A continuación se presenta un firmware autocontenido que:
– Inicializa I2S a 16 kHz, 32 bits.
– Captura frames de 30 ms (480 muestras) con salto de 10 ms (160 muestras).
– Extrae 20 bandas log-mel y aplica DCT para 13 MFCC por frame.
– Mantiene una ventana de 25 frames (~250 ms) para formar un “feature map” de 25×13 = 325 features.
– Aplica un clasificador logístico (pesos que exportaremos desde Python) para detectar la palabra clave.
– Emite por Serial la probabilidad y disparos de detección.

El clasificador está separado en un archivo de cabecera “weights.h” que generaremos tras el entrenamiento. Para poder compilar desde ya, incluimos unos pesos de ejemplo con bias=0 y todos los pesos a 0 (no detectará nada) y se reemplazan más adelante.

Crea la estructura de proyecto:
– Directorio del sketch: i2s_kws_nano33iot/
– i2s_kws_nano33iot.ino
– weights.h

Contenido:

// File: i2s_kws_nano33iot.ino
#include <Arduino.h>
#include <I2S.h>           // Arduino_I2S
#include <arduinoFFT.h>    // arduinoFFT

#include "weights.h"       // Pesos del clasificador (auto-generado por Python)

// Parámetros de audio
static const uint32_t SAMPLE_RATE = 16000;     // 16 kHz
static const uint16_t BITS_PER_SAMPLE = 32;    // INMP441 -> 24 bits válidos en 32
static const uint16_t FRAME_LEN = 480;         // 30 ms a 16 kHz
static const uint16_t FRAME_HOP = 160;         // 10 ms
static const uint16_t FFT_SIZE = 512;          // Siguiente potencia de 2 >= FRAME_LEN
static const uint8_t  NUM_MEL = 20;            // Nº de bandas mel
static const uint8_t  NUM_MFCC = 13;           // Nº de coeficientes MFCC
static const uint8_t  NUM_FRAMES_STACK = 25;   // ~250 ms de contexto
static const float    PREEMPHASIS = 0.97f;

// Buffers
static int16_t  ringBuffer[FRAME_LEN];         // Ventana actual (16 bits)
static float    frameF32[FRAME_LEN];           // Copia en float
static float    fftReal[FFT_SIZE];
static float    fftImag[FFT_SIZE];
static float    melEnergies[NUM_MEL];
static float    mfcc[NUM_MFCC];
static float    featStack[NUM_FRAMES_STACK * NUM_MFCC]; // 25x13 = 325 features

// FFT
arduinoFFT FFT = arduinoFFT(fftReal, fftImag, FFT_SIZE, SAMPLE_RATE);

// Tabla de filtros mel (precomputada en setup)
static uint16_t melLowerBin[NUM_MEL];
static uint16_t melUpperBin[NUM_MEL];
static float    melWeights[NUM_MEL][FFT_SIZE/2 + 1];

// Prototipos
void computeMelFilterbank();
void frameToMFCC(const int16_t *pcm, float *out_mfcc);
void computeLogMel(const float *magSpec, float *out_mel);
void dct13(const float *in, float *out);
float logistic(const float x);
float dotProduct(const float *a, const float *b, size_t n);
void pushFrameFeatures(const float *mfcc);
void inferAndReport();

// Utilidad: lectura robusta de muestras desde I2S (descarta underflows)
bool readI2SSamples(int16_t *dst, size_t nSamples) {
  size_t count = 0;
  while (count < nSamples) {
    int32_t s = I2S.read();
    if (s == 0) {
      // read() devuelve 0 si no hay dato listo; espera breve
      delayMicroseconds(50);
      continue;
    }
    // INMP441: 24-bit en 32-bit firmado; escalar a 16-bit
    int16_t v = (int16_t)(s >> 14); // Ajuste empírico (de 32 a ~18 bits -> 16 bits)
    dst[count++] = v;
  }
  return true;
}

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

  Serial.println("Init I2S KWS (Nano 33 IoT + INMP441)");

  // Inicializa I2S receptor: modo Philips, 16kHz, 32 bits
  if (!I2S.begin(I2S_PHILIPS_MODE, SAMPLE_RATE, BITS_PER_SAMPLE)) {
    Serial.println("Error: no se pudo iniciar I2S");
    while (1) { delay(1000); }
  }

  // Nota: El I2S del Nano 33 IoT usa pines fijos. Este sketch asume:
  // D2=BCLK, D3=LRCLK, D4=SD. Revisa tu conexionado.

  // Inicializa filtros mel (triángulos sobre espectro de magnitud)
  computeMelFilterbank();

  // Inicializa buffer de características apiladas
  memset(featStack, 0, sizeof(featStack));

  Serial.println("I2S listo. Capturando...");
}

void loop() {
  // Desplazamiento de FRAME_HOP:
  // - Leer FRAME_HOP nuevas muestras
  // - Mantener una ventana actual con tamaño FRAME_LEN para extracción de MFCC
  static int16_t window[FRAME_LEN] = {0};
  static size_t writePos = 0;

  // Lee FRAME_HOP muestras nuevas
  int16_t hopBuf[FRAME_HOP];
  readI2SSamples(hopBuf, FRAME_HOP);

  // Desplaza ventana: elimina FRAME_HOP iniciales, añade FRAME_HOP al final
  memmove(window, window + FRAME_HOP, (FRAME_LEN - FRAME_HOP) * sizeof(int16_t));
  memcpy(window + (FRAME_LEN - FRAME_HOP), hopBuf, FRAME_HOP * sizeof(int16_t));

  // Extrae MFCC de la ventana actual
  frameToMFCC(window, mfcc);

  // Apila y ejecuta inferencia cuando tengamos NUM_FRAMES_STACK
  pushFrameFeatures(mfcc);
  inferAndReport();
}

void computeMelFilterbank() {
  // Definición de los límites de frecuencia
  float fMin = 20.0f;
  float fMax = SAMPLE_RATE / 2.0f;

  auto hzToMel = [](float hz) {
    return 2595.0f * log10f(1.0f + hz / 700.0f);
  };
  auto melToHz = [](float mel) {
    return 700.0f * (powf(10.0f, mel / 2595.0f) - 1.0f);
  };

  float melMin = hzToMel(fMin);
  float melMax = hzToMel(fMax);
  float melStep = (melMax - melMin) / (NUM_MEL + 1);

  // Puntos mel
  float melPts[NUM_MEL + 2];
  for (int i = 0; i < NUM_MEL + 2; ++i) {
    melPts[i] = melMin + i * melStep;
  }

  // Convertir a bins de FFT
  for (int m = 0; m < NUM_MEL + 2; ++m) {
    float hz = melToHz(melPts[m]);
    int bin = (int) floorf((FFT_SIZE + 1) * hz / SAMPLE_RATE);
    if (m > 0 && m < NUM_MEL + 1) {
      // Guardar bordes inferiores/superiores por banda
      melLowerBin[m - 1] = (uint16_t) bin;
      melUpperBin[m - 1] = (uint16_t) bin; // se corrige en el bucle siguiente
    }
  }

  // Construir filtros triangulares
  // Limpia pesos
  for (int i = 0; i < NUM_MEL; i++) {
    for (int k = 0; k <= FFT_SIZE / 2; k++) {
      melWeights[i][k] = 0.0f;
    }
  }

  for (int m = 1; m <= NUM_MEL; ++m) {
    int f_m_minus = (int) floorf((FFT_SIZE + 1) * melToHz(melPts[m - 1]) / SAMPLE_RATE);
    int f_m       = (int) floorf((FFT_SIZE + 1) * melToHz(melPts[m]) / SAMPLE_RATE);
    int f_m_plus  = (int) floorf((FFT_SIZE + 1) * melToHz(melPts[m + 1]) / SAMPLE_RATE);

    for (int k = f_m_minus; k < f_m; ++k) {
      if (k >= 0 && k <= FFT_SIZE / 2) {
        melWeights[m - 1][k] = (float)(k - f_m_minus) / (float)(f_m - f_m_minus + 1e-9f);
      }
    }
    for (int k = f_m; k < f_m_plus; ++k) {
      if (k >= 0 && k <= FFT_SIZE / 2) {
        melWeights[m - 1][k] = (float)(f_m_plus - k) / (float)(f_m_plus - f_m + 1e-9f);
      }
    }
  }
}

void frameToMFCC(const int16_t *pcm, float *out_mfcc) {
  // Pre-énfasis y ventana Hann
  for (int i = 0; i < FRAME_LEN; i++) {
    float x = (float)pcm[i] / 32768.0f;
    if (i > 0) {
      x = x - PREEMPHASIS * ((float)pcm[i - 1] / 32768.0f);
    }
    float w = 0.5f - 0.5f * cosf(2.0f * PI * i / (FRAME_LEN - 1));
    frameF32[i] = x * w;
  }

  // Relleno a FFT_SIZE
  for (int i = 0; i < FFT_SIZE; i++) {
    if (i < FRAME_LEN) {
      fftReal[i] = frameF32[i];
    } else {
      fftReal[i] = 0.0f;
    }
    fftImag[i] = 0.0f;
  }

  // FFT
  FFT.Windowing(FFT_WIN_TYP_RECTANGLE, FFT_FORWARD); // ya aplicamos ventana Hann
  FFT.Compute(FFT_FORWARD);
  FFT.ComplexToMagnitude();

  // Magnitud espectral hasta Nyquist
  // fftReal[k] contiene magnitud; ignorar bin 0 DC en log-mel
  computeLogMel(fftReal, melEnergies);

  // DCT a 13 coeficientes
  dct13(melEnergies, out_mfcc);

  // Normalización simple (opc.): media ~0
  // Aquí se puede aplicar CMVN si se desea; para simplicidad lo omitimos.
}

void computeLogMel(const float *magSpec, float *out_mel) {
  for (int m = 0; m < NUM_MEL; m++) {
    float e = 0.0f;
    for (int k = 0; k <= FFT_SIZE/2; k++) {
      e += magSpec[k] * melWeights[m][k];
    }
    out_mel[m] = logf(e + 1e-6f); // log-amplitud
  }
}

void dct13(const float *in, float *out) {
  // DCT-II ortonormal aproximada para 13 coeficientes sobre NUM_MEL entradas
  for (int n = 0; n < NUM_MFCC; n++) {
    float sum = 0.0f;
    for (int m = 0; m < NUM_MEL; m++) {
      sum += in[m] * cosf(PI * (m + 0.5f) * n / (float)NUM_MEL);
    }
    out[n] = sum; // sin normalización adicional por simplicidad
  }
}

float logistic(const float x) {
  // Evitar overflow
  if (x > 20.0f) return 1.0f;
  if (x < -20.0f) return 0.0f;
  return 1.0f / (1.0f + expf(-x));
}

void pushFrameFeatures(const float *mfcc) {
  // Desplaza hacia la izquierda un bloque de NUM_MFCC y añade al final
  memmove(featStack, featStack + NUM_MFCC, sizeof(float) * NUM_MFCC * (NUM_FRAMES_STACK - 1));
  memcpy(featStack + NUM_MFCC * (NUM_FRAMES_STACK - 1), mfcc, sizeof(float) * NUM_MFCC);
}

void inferAndReport() {
  // Inferencia basada en los features apilados (325 features)
  // Dot product + bias -> sigmoid -> prob.
  float score = dotProduct(featStack, KWS_WEIGHTS, KWS_FEATURES);
  score += KWS_BIAS;
  float prob = logistic(score);

  // Heurística de disparo: histéresis simple
  static bool triggered = false;
  static uint32_t lastTrigger = 0;
  const float TH_ON  = 0.75f;
  const float TH_OFF = 0.65f;
  const uint32_t REFRACTORY_MS = 1000;

  uint32_t now = millis();
  if (!triggered && prob > TH_ON && (now - lastTrigger) > REFRACTORY_MS) {
    triggered = true;
    lastTrigger = now;
    Serial.print("DETECCION: prob=");
    Serial.println(prob, 3);
  } else if (triggered && prob < TH_OFF) {
    triggered = false;
  }

  // Telemetría (opcional): comentar si causa latencias
  Serial.print("p=");
  Serial.println(prob, 3);
}

float dotProduct(const float *a, const float *b, size_t n) {
  float s = 0.0f;
  for (size_t i = 0; i < n; i++) s += a[i] * b[i];
  return s;
}

Archivo de pesos (temporal, será reemplazado luego por entrenamiento):

// File: weights.h
#pragma once
// Nº de características: NUM_FRAMES_STACK * NUM_MFCC = 25 * 13 = 325
#define KWS_FEATURES 325

// Pesos iniciales de marcador de posición (todo a 0.0f); serán generados por Python
static const float KWS_WEIGHTS[KWS_FEATURES] = {
  // Se sobreescribirá con pesos reales; mantener longitud 325
  #define Z0 0.0f
  Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,  Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,
  Z0,Z0,Z0,Z0,Z0,  Z0,Z0,Z0,Z0,Z0,  Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,
  Z0,Z0,Z0,Z0,Z0,  Z0,Z0,Z0,Z0,Z0,  Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,
  Z0,Z0,Z0,Z0,Z0,  Z0,Z0,Z0,Z0,Z0,  Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,
  Z0,Z0,Z0,Z0,Z0,  Z0,Z0,Z0,Z0,Z0,  Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,
  Z0,Z0,Z0,Z0,Z0,  Z0,Z0,Z0,Z0,Z0,  Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,
  Z0,Z0,Z0,Z0,Z0,  Z0,Z0,Z0,Z0,Z0,  Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,
  Z0,Z0,Z0,Z0,Z0,  Z0,Z0,Z0,Z0,Z0,  Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,
  Z0,Z0,Z0,Z0,Z0,  Z0,Z0,Z0,Z0,Z0,  Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,
  Z0,Z0,Z0,Z0,Z0,  Z0,Z0,Z0,Z0,Z0,  Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,
  Z0,Z0,Z0,Z0,Z0,  Z0,Z0,Z0,Z0,Z0,  Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,Z0,
  Z0,Z0,Z0,Z0,Z0,  Z0,Z0,Z0,Z0,Z0,  Z0,Z0,Z0,Z0,Z0
  #undef Z0
};

static const float KWS_BIAS = 0.0f;

Explicación breve de las partes clave:
– Captura I2S: I2S.begin(I2S_PHILIPS_MODE, 16000, 32) configura el bus en modo receptor. INMP441 entrega 24 bits útiles; se reducen a int16 con un shift.
– Ventaneo: FRAME_LEN=480 muestras (30 ms) con hop de 160 muestras (10 ms) para solapidado.
– FFT y mel: Se aplica ventana Hann, FFT 512, se genera un banco de 20 filtros mel triangulares y se calcula log-energía.
– MFCC: DCT tipo II para 13 coeficientes por frame. Se apilan 25 frames (~250 ms).
– Clasificador: producto punto con pesos exportados + función logística; se reporta probabilidad y se aplica histéresis con periodo refractario.

Compilación, flashing y ejecución

Asumiendo que tu sketch está en ~/proyectos/i2s_kws_nano33iot:

  1. Compilar (Nano 33 IoT):
    bash
    arduino-cli compile \
    --fqbn arduino:samd:nano_33_iot \
    ~/proyectos/i2s_kws_nano33iot
  2. Subir (ajusta el puerto al listado por board list):
    bash
    # En Linux suele ser /dev/ttyACM0
    arduino-cli upload \
    -p /dev/ttyACM0 \
    --fqbn arduino:samd:nano_33_iot \
    ~/proyectos/i2s_kws_nano33iot
  3. Abrir monitor serie a 115200 baudios:
    bash
    arduino-cli monitor -p /dev/ttyACM0 -c baudrate=115200

Salida esperada inicial (sin pesos reales):
– Mensaje de inicio “Init I2S KWS…” y líneas “p=0.500” fluctuando cerca de 0.5 (con pesos cero, la sigmoide de 0 produce 0.5).

Entrenamiento del clasificador y exportación de pesos

Para pasar de “esqueleto” a un KWS real, capturaremos ejemplos de la palabra clave (ej.: “hola”) y de fondo/noise, entrenaremos un clasificador logístico y exportaremos sus pesos a weights.h.

Preparación del entorno Python

cd ~/proyectos/i2s_kws_nano33iot
python3 -m venv .venv
source .venv/bin/activate
pip install numpy==1.26.4 scikit-learn==1.4.2 pyserial==3.5

Firmware de captura rápida

Usaremos el propio firmware para stream de MFCC ya calculados, simplificando dataset y entrenamiento (features ya procesadas). Añade en el loop un modo de “dump” controlado por comando serie o, más simple, crea un pequeño script Python que escuche “p=” y MFCC si lo deseas. Aquí proponemos una segunda sketch minimal para streaming de MFCC en lugar de probabilidad. Alternativamente, modifica el actual para imprimir MFCC cuando reciba ‘F’.

Para rapidez, usaremos un script Python que escucha “MFCC:” que enviaremos. Modifica temporalmente inferAndReport así:

  • Sustituye Serial.print(«p=»…) por impresión de MFCC:
// Sustituye inferAndReport por esta versión temporal para recolectar MFCC
void inferAndReport() {
  // Imprime MFCC de la última ventana apilada (25x13)
  Serial.print("MFCC:");
  for (int i = 0; i < NUM_FRAMES_STACK * NUM_MFCC; i++) {
    Serial.print(featStack[i], 6);
    if (i < NUM_FRAMES_STACK * NUM_MFCC - 1) Serial.print(',');
  }
  Serial.println();
}

Compila y sube de nuevo. Abre el monitor para verificar que salen líneas “MFCC:…”.

Script Python de captura etiquetada

Crea capture.py para etiquetar en vivo (presiona ‘k’ cuando digas la palabra, ‘n’ para ruido):

# File: capture.py
import sys, time, serial, threading
from datetime import datetime

PORT = sys.argv[1] if len(sys.argv) > 1 else "/dev/ttyACM0"
BAUD = 115200
OUT = "dataset.csv"

print(f"Abrir {PORT} @ {BAUD}")
ser = serial.Serial(PORT, BAUD, timeout=1)

label = "noise"
running = True
count = {"noise":0, "keyword":0}

def key_reader():
    global label, running
    try:
        while running:
            k = sys.stdin.read(1)
            if k == 'k':
                label = "keyword"
                print("[LABEL] keyword")
            elif k == 'n':
                label = "noise"
                print("[LABEL] noise")
            elif k == 'q':
                running = False
                break
    except Exception as e:
        print("Key thread error:", e)

threading.Thread(target=key_reader, daemon=True).start()

with open(OUT, "w") as f:
    # Cabecera
    cols = [f"f{i}" for i in range(325)]
    f.write("label," + ",".join(cols) + "\n")
    try:
        while running:
            line = ser.readline().decode(errors="ignore").strip()
            if line.startswith("MFCC:"):
                data = line.split("MFCC:")[1].strip()
                parts = data.split(",")
                if len(parts) != 325:
                    continue
                f.write(label + "," + ",".join(parts) + "\n")
                f.flush()
                count[label] += 1
                if (count["noise"] + count["keyword"]) % 20 == 0:
                    print(f"Samples -> noise: {count['noise']}, keyword: {count['keyword']}")
    except KeyboardInterrupt:
        pass

running = False
ser.close()
print("Guardado en", OUT)

Uso:

python capture.py /dev/ttyACM0
# Pulsa 'k' mientras dices "hola" 1 s antes y 1 s después para capturar ejemplos
# Pulsa 'n' para marcar ruido/fondo
# Pulsa 'q' para terminar

Objetivo mínimo de dataset:
– 300 ejemplos “keyword”
– 600 ejemplos “noise”
– Total ~900 filas

Consejo: recoge en diferentes condiciones (distancias, ruidos, voces).

Entrenamiento y exportación de pesos

Crea train_export.py:

# File: train_export.py
import numpy as np
from sklearn.linear_model import LogisticRegression

DATASET = "dataset.csv"
OUT_H = "weights.h"
FEATURES = 325

# Carga CSV
rows = []
labels = []
with open(DATASET, "r") as f:
    header = f.readline()
    for line in f:
        parts = line.strip().split(",")
        label = parts[0]
        feats = np.array([float(x) for x in parts[1:]], dtype=np.float32)
        if feats.shape[0] != FEATURES:
            continue
        rows.append(feats)
        labels.append(1 if label == "keyword" else 0)

X = np.vstack(rows)
y = np.array(labels, dtype=np.int32)

# Normalización simple por-feature (media 0, var 1)
mu = X.mean(axis=0)
sigma = X.std(axis=0) + 1e-6
Xn = (X - mu) / sigma

# Entrena logistic regression (L2, solver liblinear o saga)
clf = LogisticRegression(max_iter=1000, solver="liblinear")
clf.fit(Xn, y)

acc = clf.score(Xn, y)
print("Accuracy (train) =", acc)

w = clf.coef_[0].astype(np.float32)
b = float(clf.intercept_[0])

# Exporta a header C++ con normalización integrada: transformamos pesos a espacio original
# y = sigmoid( (x-mu)/sigma · w + b ) = sigmoid( x · (w/sigma) + (b - mu·(w/sigma)) )
ws = w / sigma
b_adj = b - (mu * ws).sum()

def as_c_array(arr, name):
    s = f"static const float {name}[{len(arr)}] = {{\n"
    line = ""
    for i, v in enumerate(arr):
        line += f"{v:.8e}f,"
        if (i+1) % 10 == 0:
            s += "  " + line + "\n"
            line = ""
    if line:
        s += "  " + line + "\n"
    s += "};\n"
    return s

with open(OUT_H, "w") as f:
    f.write("#pragma once\n")
    f.write(f"#define KWS_FEATURES {FEATURES}\n\n")
    f.write(as_c_array(ws, "KWS_WEIGHTS"))
    f.write(f"\nstatic const float KWS_BIAS = {b_adj:.8e}f;\n")

print(f"Generado {OUT_H}")

Ejecución:

python train_export.py
# Reemplazará weights.h con pesos reales y sesgo ajustado

Restablece el firmware original (inferAndReport con probabilidad y disparos), compila y sube:

arduino-cli compile --fqbn arduino:samd:nano_33_iot ~/proyectos/i2s_kws_nano33iot
arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:samd:nano_33_iot ~/proyectos/i2s_kws_nano33iot
arduino-cli monitor -p /dev/ttyACM0 -c baudrate=115200

Ahora la salida “p=…” variará y, cuando pronuncies “hola”, deberías ver “DETECCION: prob=…”.

Validación paso a paso

  1. Verificación física:
  2. Conexiones:
    • VDD->3.3V, GND->GND
    • INMP441 SCK->D2, WS->D3, SD->D4
    • L/R->GND (canal izquierdo)
  3. Cables cortos y firmes.
  4. Inicialización:
  5. Al abrir el monitor serie a 115200, verás:
    • “Init I2S KWS (Nano 33 IoT + INMP441)”
    • “I2S listo. Capturando…”
  6. Nivel de ruido:
  7. Observa “p=…” en reposo: valores típicos alrededor de 0.1–0.4 si el modelo distingue silencio; si mal entrenado, ~0.5.
  8. Palabra clave:
  9. Pronuncia “hola” a 20–40 cm del micrófono; deberías ver “DETECCION: prob=0.80–0.99”.
  10. Repite varias veces para medir consistencia.
  11. Falsos positivos:
  12. Conversa sin decir “hola” o reproduce ruido; mide cuántas veces dispara por minuto (objetivo < 1/min).
  13. Robustez:
  14. Cambia distancia (10–80 cm), orientación del micrófono y presencia de ruido de fondo moderado.
  15. Latencia:
  16. El pipeline usa ~250 ms de contexto; la detección debería ocurrir en < 400 ms desde el inicio de la palabra.

Métricas sugeridas:
– Tasa de acierto con 50 ensayos deliberados de “hola”.
– Falsos positivos en 10 min de conversación sin la keyword.
– Probabilidad media al decir “hola” vs en silencio.

Troubleshooting

  1. No hay salida en el monitor serie
  2. Asegura el puerto correcto en arduino-cli monitor.
  3. Pulsa el botón RESET doble para entrar en bootloader y vuelve a subir.
  4. Verifica alimentación del USB y cable de datos (no solo carga).
  5. Error “no se pudo iniciar I2S”
  6. Revisa pines D2, D3, D4; evita cortocircuitos y cables sueltos.
  7. Quita otros dispositivos del bus que pudieran interferir.
  8. Reinicia la placa y el PC si el puerto USB quedó en mal estado.
  9. Probabilidades siempre ~0.5 o 0.0/1.0
  10. Verifica que weights.h fue regenerado y que el include apunta al archivo correcto.
  11. Asegura suficientes muestras y balance de clases (≥ 300 keyword, ≥ 600 noise).
  12. Revisa normalización integrada en train_export.py; no edites manualmente.
  13. Distorsión/recortes en audio
  14. Ajusta el shift de conversión (s >> 14). Si saturas, prueba >>15; si el volumen es bajo, prueba >>13.
  15. Asegura que L/R está a GND (o VDD) sólidamente; flotante puede introducir errores de canal.
  16. Ruido elevado o probabilidad inestable
  17. Usa cables más cortos y masa común robusta.
  18. Aísla de corrientes de aire y vibraciones (el INMP441 es sensible).
  19. Incrementa NUM_MEL a 26 y/o aplica media temporal de probabilidades.
  20. Subida falla con “No device found on port”
  21. Verifica que el puerto no cambió (/dev/ttyACM1).
  22. En Linux, añade tu usuario al grupo dialout: sudo usermod -aG dialout $USER y re‑inicia sesión.
  23. Memoria insuficiente o resets
  24. Reduce FFT_SIZE a 256 y ajusta FRAME_LEN a 320.
  25. Reduce NUM_FRAMES_STACK a 20 o NUM_MFCC a 10.
  26. Evita prints muy frecuentes; comenta telemetría en producción.
  27. Dataset inconsistente
  28. Alinea el tiempo: al etiquetar ‘k’, habla la keyword inmediatamente para capturar frames con señal.
  29. Graba en varias sesiones para mejorar generalización.

Mejoras/variantes

  • Sustituir el clasificador logístico por:
  • SVM lineal (exportable como vector de pesos).
  • Red MLP de 1–2 capas pequeñas con activaciones ReLU, entrenada en Python y exportada como arrays; inferencia con CMSIS‑NN.
  • Usar MFBE en vez de MFCC:
  • Eliminar DCT (dct13); directo sobre log-mel a menudo da buenos resultados y reduce cómputo.
  • TFLite Micro:
  • Entrenar un modelo DSCNN pequeño y portarlo con Arduino_TensorFlowLite (asegúrate de tamaños de tensor ajustados a RAM del SAMD21).
  • Filtrado adaptativo:
  • VAD (Voice Activity Detection) por energía/ZCR antes de pasar a MFCC para reducir cargas y falsos positivos.
  • BLE/IoT:
  • Publicar eventos de detección por BLE (NINA-W102) o MQTT via WiFi para integración domótica.
  • Optimización:
  • Usar fixed‑point Q15 y CMSIS‑DSP para FFT/DCT.
  • Bajar SAMPLE_RATE a 8 kHz para voces graves, ajustando filtros mel.
  • Multi‑keyword:
  • One‑vs‑rest con varios clasificadores logísticos o softmax con MLP.

Checklist de verificación

  • [ ] Instalé Arduino CLI v0.35.3 y el core arduino:samd@1.8.14 sin errores.
  • [ ] Instalé las librerías Arduino_I2S@1.0.3 y arduinoFFT@1.6.1.
  • [ ] Conecté el INMP441 a 3.3V y GND del Nano 33 IoT.
  • [ ] Cableé I2S: SCK->D2, WS->D3, SD->D4; L/R->GND.
  • [ ] El firmware compila y sube con FQBN arduino:samd:nano_33_iot.
  • [ ] El monitor serie muestra “I2S listo. Capturando…”.
  • [ ] Puedo capturar MFCC con capture.py y etiquetar con ‘k’/‘n’.
  • [ ] Entrené el clasificador y generé weights.h con train_export.py.
  • [ ] Recompilé y subí el firmware con los nuevos pesos.
  • [ ] Veo “DETECCION” con probabilidad > 0.75 al decir “hola”.
  • [ ] Los falsos positivos están en niveles aceptables; ajusté TH_ON/TH_OFF si fue necesario.

Con esto, tendrás un pipeline completo de i2s‑keyword‑spotting sobre el Arduino Nano 33 IoT con micrófono INMP441, usando una toolchain reproducible y un flujo de trabajo de principio a fin (captura, entrenamiento, despliegue e inferencia en tiempo real).

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 instalar?




Pregunta 3: ¿Cuál es el modelo exacto del Arduino utilizado en el proyecto?




Pregunta 4: ¿Qué micrófono se utiliza para capturar audio?




Pregunta 5: ¿Cuál es la frecuencia de muestreo del audio capturado?




Pregunta 6: ¿Qué paquete de Python se utiliza para la regresión logística?




Pregunta 7: ¿Qué tipo de clasificador se entrena en Python?




Pregunta 8: ¿Qué se debe evitar al alimentar el micrófono INMP441?




Pregunta 9: ¿Qué librería se utiliza para el procesamiento de audio?




Pregunta 10: ¿Qué herramienta se utiliza para la inferencia en tiempo real en el Arduino?




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: Pesaje de colmena Zigbee Arduino, XBee, HX711

Caso práctico: Pesaje de colmena Zigbee Arduino, XBee, HX711 — hero

Objetivo y caso de uso

Qué construirás: Un sistema de pesaje para colmenas utilizando Arduino Uno R3, XBee S2C y HX711 para medir el peso y monitorear el ambiente.

Para qué sirve

  • Monitoreo del peso de colmenas en tiempo real para optimizar la producción de miel.
  • Registro de datos ambientales (temperatura y humedad) usando el sensor SHT31.
  • Transmisión de datos a través de Zigbee para una comunicación eficiente y de bajo consumo.
  • Integración con sistemas de alerta para notificar cambios significativos en el peso.

Resultado esperado

  • Precisión en la medición del peso de ±0.1 kg utilizando HX711.
  • Latencia de transmisión de datos menor a 200 ms a través de Zigbee.
  • Capacidad de enviar datos de temperatura y humedad cada 5 minutos.
  • Recepción de datos en un PC con Python y pyserial, validando al menos 95% de los paquetes.

Público objetivo: Ingenieros y entusiastas de la electrónica; Nivel: Avanzado

Arquitectura/flujo: Sensor de peso (HX711) y ambiental (SHT31) conectados a Arduino Uno R3, datos enviados a través de XBee Zigbee S2C a un coordinador en PC.

Nivel: Avanzado

Prerrequisitos

Sistema operativo y herramientas (probado)

  • Linux:
  • Ubuntu 22.04.4 LTS (64-bit)
  • Python 3.11.6 (para validación PC)
  • Digi XCTU 6.5.10 (configuración XBee)
  • Windows:
  • Windows 11 Pro 23H2
  • Python 3.11.6
  • Digi XCTU 6.5.10

Toolchain exacta para Arduino Uno (AVR)

  • Arduino CLI 0.35.3
  • Core AVR:
  • FQBN: arduino:avr:uno
  • Paquete: arduino:avr@1.8.6
  • Librerías Arduino (Library Manager vía arduino-cli lib install):
  • HX711@0.7.5 (bogde/HX711)
  • Adafruit SHT31 Library@2.2.1
  • Adafruit BusIO@1.14.5
  • Monitor serie de Arduino CLI:
  • arduino-cli monitor (integrado desde 0.34+)

Herramientas adicionales para Zigbee

  • Digi XCTU 6.5.10 para configurar los módulos XBee Zigbee S2C (EM357).
  • Un adaptador USB para XBee (por ejemplo, “XBee USB Explorer”) para el coordinador en el PC.
  • Python en el PC con pyserial para validar la recepción:
  • pyserial 3.5

Materiales

  • Nodo sensor (en colmena):
  • 1x Arduino Uno R3
  • 1x XBee Zigbee S2C (EM357)
  • 1x Shield XBee para Arduino con regulador 3.3V y nivelación de voltaje (cualquier modelo estándar compatible con UNO R3)
  • 1x HX711 (amplificador para célula de carga)
  • 1x Célula de carga (ej. 50 kg o 100 kg, tipo viga)
  • 1x SHT31 (breakout con interfaz I2C, por ejemplo Adafruit SHT31-D)
  • Cableado Dupont, tornillería, base rígida para célula de carga, conectores
  • Fuente de alimentación para el nodo (inicialmente USB, en despliegue real: batería con step-up/step-down y/o panel solar)

  • Coordinador (en PC):

  • 1x XBee Zigbee S2C (EM357) adicional (coordinador)
  • 1x Adaptador USB para XBee (XBee Explorer USB o similar)
  • PC con Ubuntu 22.04.4 LTS o Windows 11 23H2, con XCTU 6.5.10

  • Nota de coherencia:

  • El modelo principal a emplear en el nodo es exactamente: Arduino Uno R3 + XBee Zigbee S2C (EM357) + HX711 + SHT31.
  • El coordinador XBee Zigbee S2C (EM357) adicional se usa para la validación y recepción de datos Zigbee en el PC, no altera el modelo del nodo sensor.

Preparación y conexión

Consideraciones de alimentación y niveles lógicos

  • El XBee Zigbee S2C funciona a 3.3V y puede demandar picos de corriente >200 mA durante transmisión. Por eso, use un Shield XBee con regulador 3.3V y level shifting. Evite alimentar el XBee directamente desde el pin 3.3V del UNO (capacidad limitada).
  • El HX711 funciona a 5V típicamente (verifique su módulo).
  • El SHT31 (breakout) suele ser 3–5V tolerant (si es Adafruit, integra level shifting); confirme el modelo y su rango de alimentación. Aquí alimentaremos a 5V para simplificar.

Mapeo de pines y conexiones

La siguiente tabla detalla las conexiones para el nodo:

Componente Señal/Pin Arduino Uno R3 Notas
XBee S2C DIN (entrada XBee) D3 (TX SoftwareSerial) A través del Shield XBee (nivelado)
XBee S2C DOUT (salida XBee) D2 (RX SoftwareSerial) A través del Shield XBee (nivelado)
XBee S2C VCC 3.3V (del Shield) Regulador del Shield
XBee S2C GND GND Masa común
HX711 VCC 5V Verifique su módulo
HX711 GND GND Masa común
HX711 DT (DOUT) D4 Entrada de datos
HX711 SCK D5 Reloj
Célula carga E+ / E- HX711 E+ / E- Excitación
Célula carga A+ / A- HX711 A+ / A- Señal
SHT31 VIN 5V Si su breakout lo permite
SHT31 GND GND Masa común
SHT31 SDA A4 I2C
SHT31 SCL A5 I2C
  • Consola de depuración: USB del Arduino (Serial a 115200 baudios).
  • Canal Zigbee: XBee en modo transparente (AT) a 9600 baudios, conectado al UNO vía SoftwareSerial en D2 (RX) y D3 (TX).

Preparación de XBee (coordinador y nodo)

  • Use XCTU 6.5.10 para configurar ambos XBee S2C (EM357):
  • Coordinador (en el PC):
  • Función: Zigbee Coordinator AT (XB24C, “Zigbee TH Reg”, AT)
  • BD (Baud Rate): 9600
  • PAN ID (ID): por ejemplo 0x7A7A
  • Canal (CH): opcional o automático
  • AP (API Mode): 0 (modo transparente)
  • CE (Coordinator Enable): 1
  • NI (Node Identifier): COORD
  • Escribir cambios (Write).
  • Anote SH y SL (dirección 64-bit).
  • Nodo (en el UNO):
  • Función: Zigbee Router AT (XB24C, “Zigbee TH Reg”, AT)
  • BD (Baud Rate): 9600
  • PAN ID (ID): 0x7A7A (igual al coordinador)
  • AP: 0 (transparente)
  • CE: 0
  • DH/DL: ponga la dirección 64-bit del coordinador (DH=SH del coordinador, DL=SL del coordinador)
  • JV: 1 (permitir rejoin si el coordinador reinicia)
  • NI: BEE_NODE
  • AI: compruebe 0x00 tras asociar (Association Indication OK)
  • Escribir cambios (Write).

  • Tras configurar el nodo, monte el XBee S2C en el Shield, y el Shield sobre el UNO R3.

Código completo (Arduino C++) con explicación

El siguiente sketch lee peso (HX711) y ambiente (SHT31), filtra, formatea una línea CSV y la envía vía Zigbee (XBee en modo transparente) y en paralelo por el puerto USB para depuración. Incluye comandos por serial para tare, factor de calibración y periodo, y persistencia en EEPROM.

// zigbee_beehive_weight_sensing.ino
// Hardware: Arduino Uno R3 + XBee Zigbee S2C (EM357) + HX711 + SHT31
// Toolchain: Arduino CLI 0.35.3, arduino:avr@1.8.6
// Librerías: HX711@0.7.5, Adafruit SHT31 Library@2.2.1, Adafruit BusIO@1.14.5

#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_SHT31.h>
#include <HX711.h>
#include <SoftwareSerial.h>
#include <EEPROM.h>

// Pines
static const uint8_t PIN_HX711_DOUT = 4;
static const uint8_t PIN_HX711_SCK  = 5;
static const uint8_t PIN_XBEE_RX    = 2; // RX del UNO (desde DOUT del XBee)
static const uint8_t PIN_XBEE_TX    = 3; // TX del UNO (hacia DIN del XBee)

// Objetos
HX711 scale;
Adafruit_SHT31 sht31 = Adafruit_SHT31();
SoftwareSerial xbee(PIN_XBEE_RX, PIN_XBEE_TX);

// Configuración de medidas
volatile unsigned long samplePeriodMs = 5000; // periodo de muestreo
volatile long calibrationFactor = 2280; // factor de ejemplo; calibrar para su célula/puente
volatile int hx711Averages = 10; // promediado interno HX711

// EEPROM layout
// addr 0..3: signature 'BZWS' (Beehive Zigbee Weight Sensing)
// addr 4..7: calibrationFactor (int32)
// addr 8..11: samplePeriodMs (uint32)
const int EEPROM_ADDR_SIGNATURE = 0;
const int EEPROM_ADDR_CAL       = 4;
const int EEPROM_ADDR_PERIOD    = 8;

void eepromWriteLong(int addr, long value) {
  EEPROM.put(addr, value);
}

long eepromReadLong(int addr) {
  long v = 0;
  EEPROM.get(addr, v);
  return v;
}

void eepromWriteULong(int addr, unsigned long value) {
  EEPROM.put(addr, value);
}

unsigned long eepromReadULong(int addr) {
  unsigned long v = 0;
  EEPROM.get(addr, v);
  return v;
}

bool eepromHasSignature() {
  char sig[4];
  for (int i = 0; i < 4; ++i) sig[i] = EEPROM.read(EEPROM_ADDR_SIGNATURE + i);
  return (sig[0]=='B' && sig[1]=='Z' && sig[2]=='W' && sig[3]=='S');
}

void eepromWriteSignature() {
  EEPROM.write(EEPROM_ADDR_SIGNATURE + 0, 'B');
  EEPROM.write(EEPROM_ADDR_SIGNATURE + 1, 'Z');
  EEPROM.write(EEPROM_ADDR_SIGNATURE + 2, 'W');
  EEPROM.write(EEPROM_ADDR_SIGNATURE + 3, 'S');
}

void saveConfigToEEPROM() {
  eepromWriteSignature();
  eepromWriteLong(EEPROM_ADDR_CAL, calibrationFactor);
  eepromWriteULong(EEPROM_ADDR_PERIOD, samplePeriodMs);
}

void loadConfigFromEEPROM() {
  if (eepromHasSignature()) {
    calibrationFactor = eepromReadLong(EEPROM_ADDR_CAL);
    samplePeriodMs = eepromReadULong(EEPROM_ADDR_PERIOD);
  }
}

void printHelp() {
  Serial.println(F("Comandos (USB@115200):"));
  Serial.println(F("  h             : ayuda"));
  Serial.println(F("  t             : tare (poner a cero)"));
  Serial.println(F("  c <factor>    : set calibracion (p.ej. c 2280)"));
  Serial.println(F("  p <ms>        : set periodo muestreo en ms (p.ej. p 5000)"));
  Serial.println(F("  a <n>         : set promediado HX711 (1..20)"));
  Serial.println(F("  s             : muestreo inmediato"));
  Serial.println(F("  w             : guardar en EEPROM"));
  Serial.println(F("  r             : recargar de EEPROM"));
  Serial.println(F("Formato TX: CSV: ts_ms,weight_kg,tempC,humRH,status"));
}

bool setupSHT31() {
  if (!sht31.begin(0x44)) { // 0x44 por defecto; 0x45 si A0 soldado
    Serial.println(F("ERROR: SHT31 no encontrado en 0x44"));
    return false;
  }
  // Opcional: sht31.heater(true/false)
  return true;
}

void setupHX711() {
  scale.begin(PIN_HX711_DOUT, PIN_HX711_SCK);
  scale.set_scale(); // Inicialmente sin factor
  delay(100);
  scale.tare(20);    // Establecer tara inicial con N lecturas
  scale.set_scale((float)calibrationFactor);
}

void sendLine(const char* line) {
  xbee.println(line);   // Hacia Zigbee (modo transparente)
  Serial.println(line); // Depuracion en USB
}

void performSampleAndSend() {
  // Lectura HX711
  float weight = scale.get_units(hx711Averages); // unidades en función del factor
  // Lecturas SHT31
  float t = sht31.readTemperature();
  float h = sht31.readHumidity();
  // Estado
  const char* status = "OK";
  if (isnan(t) || isnan(h)) status = "SHT31_ERR";

  // Línea CSV
  char line[128];
  unsigned long ts = millis();
  // CSV: ts_ms,weight_kg,tempC,humRH,status
  snprintf(line, sizeof(line), "%lu,%.3f,%.2f,%.2f,%s", ts, weight, t, h, status);
  sendLine(line);
}

void handleSerialCommands() {
  if (!Serial.available()) return;
  String cmd = Serial.readStringUntil('\n');
  cmd.trim();
  if (cmd.length() == 0) return;

  if (cmd == "h") {
    printHelp();
  } else if (cmd == "t") {
    Serial.println(F("Tare..."));
    scale.tare(20);
    Serial.println(F("OK"));
  } else if (cmd.startsWith("c ")) {
    long f = cmd.substring(2).toInt();
    calibrationFactor = f;
    scale.set_scale((float)calibrationFactor);
    Serial.print(F("Calibracion=")); Serial.println(calibrationFactor);
  } else if (cmd.startsWith("p ")) {
    unsigned long p = (unsigned long)cmd.substring(2).toInt();
    if (p >= 500 && p <= 60000UL) {
      samplePeriodMs = p;
      Serial.print(F("Periodo(ms)=")); Serial.println(samplePeriodMs);
    } else {
      Serial.println(F("ERROR: periodo fuera de rango (500..60000)"));
    }
  } else if (cmd.startsWith("a ")) {
    int n = cmd.substring(2).toInt();
    if (n >= 1 && n <= 20) {
      hx711Averages = n;
      Serial.print(F("Promediado HX711=")); Serial.println(hx711Averages);
    } else {
      Serial.println(F("ERROR: promediado (1..20)"));
    }
  } else if (cmd == "s") {
    performSampleAndSend();
  } else if (cmd == "w") {
    saveConfigToEEPROM();
    Serial.println(F("EEPROM guardada"));
  } else if (cmd == "r") {
    loadConfigFromEEPROM();
    scale.set_scale((float)calibrationFactor);
    Serial.print(F("EEPROM: cal=")); Serial.print(calibrationFactor);
    Serial.print(F(" period=")); Serial.println(samplePeriodMs);
  } else {
    Serial.println(F("Comando desconocido. 'h' para ayuda."));
  }
}

void setup() {
  Serial.begin(115200);
  xbee.begin(9600);

  Serial.println(F("zigbee-beehive-weight-sensing (UNO R3 + XBee S2C + HX711 + SHT31)"));
  loadConfigFromEEPROM();

  if (!setupSHT31()) {
    Serial.println(F("Continuando sin SHT31 (status=SHT31_ERR)"));
  }

  setupHX711();

  Serial.print(F("Cal=")); Serial.print(calibrationFactor);
  Serial.print(F(" Period(ms)=")); Serial.print(samplePeriodMs);
  Serial.print(F(" Avg=")); Serial.println(hx711Averages);

  printHelp();
}

void loop() {
  static unsigned long last = 0;
  unsigned long now = millis();
  if ((now - last) >= samplePeriodMs) {
    last = now;
    performSampleAndSend();
  }
  handleSerialCommands();
}

Explicación breve de partes clave

  • SoftwareSerial en D2/D3: evita ocupar Serial hardware (USB) del UNO para debug, y nos permite tener el XBee S2C independiente a 9600 baudios en modo transparente.
  • HX711:
  • scale.set_scale(calibrationFactor): transforma la lectura del ADC en “unidades” (kg si calibramos adecuadamente).
  • hx711Averages controla cuántas muestras internas se promedian en la función get_units().
  • SHT31:
  • Se inicializa en la dirección I2C 0x44 (común). Si su breakout usa 0x45, cambie en el código.
  • Persistencia en EEPROM:
  • Guarda factor de calibración y periodo (firma “BZWS”) para conservarlos entre reinicios.
  • Interfaz de comandos:
  • Comandos simples por USB para tare, calibración, periodo, etc., muy útil en campo.

Compilación, “flash” y ejecución

Asegúrese de conectar el Arduino Uno R3 por USB. Identifique el puerto serie (ejemplos: /dev/ttyACM0 en Linux, COM5 en Windows).

Preparación de proyecto

  • Estructura de carpetas (Linux/macOS; en Windows use rutas equivalentes):
mkdir -p ~/projects/zigbee_beehive_weight_sensing
nano ~/projects/zigbee_beehive_weight_sensing/zigbee_beehive_weight_sensing.ino
# Pegue el sketch completo anterior y guarde

Instalación de toolchain y librerías (Arduino CLI)

# 1) Instalar Arduino CLI (si no lo tiene) y asegúrese de usar la 0.35.3
arduino-cli version
# Debería mostrar: arduino-cli Version: 0.35.3

# 2) Actualizar índices de cores y librerías
arduino-cli core update-index

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

# 4) Instalar librerías exactas
arduino-cli lib install "HX711@0.7.5" "Adafruit SHT31 Library@2.2.1" "Adafruit BusIO@1.14.5"

Compilar y subir al UNO R3

  • Linux:
# Lista de placas para encontrar el puerto (opc.)
arduino-cli board list

# Compilar
arduino-cli compile --fqbn arduino:avr:uno ~/projects/zigbee_beehive_weight_sensing

# Subir (ajuste el puerto si es distinto)
arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:avr:uno ~/projects/zigbee_beehive_weight_sensing
  • Windows (PowerShell, ejemplo puerto COM5):
arduino-cli board list

arduino-cli compile --fqbn arduino:avr:uno $env:USERPROFILE\projects\zigbee_beehive_weight_sensing

arduino-cli upload -p COM5 --fqbn arduino:avr:uno $env:USERPROFILE\projects\zigbee_beehive_weight_sensing

Visualizar depuración por USB

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

Finalice con Ctrl+C.

Validación paso a paso

Objetivo: confirmar que el nodo “zigbee-beehive-weight-sensing” transmite peso y ambiente por Zigbee y que el coordinador en PC recibe líneas CSV.

1) Verifique alimentación y enlaces:
– El Shield XBee está firmemente insertado.
– GND común entre todos.
– HX711 cableado correcto: E+/E- al excitador, A+/A- a señal de la célula (polaridad correcta).
– SHT31 conectado a A4/A5.

2) Verifique XBee en XCTU:
– Conecte el XBee coordinador al PC con el adaptador USB.
– En XCTU 6.5.10, agregue el puerto del XBee coordinador, lea parámetros.
– Confirme:
– Función: Zigbee Coordinator AT
– PAN ID = 0x7A7A (o el que definió)
– BD=9600, AP=0
– AI=0x00 (si ya hay nodos; si no, aparecerá 0x21 hasta que el nodo se asocie)
– Inserte/encienda el nodo UNO con su XBee.
– En XCTU, busque dispositivos en red (Network Working Mode) y verifique que aparezca “BEE_NODE”.

3) Monitor USB del UNO:
– Ejecute arduino-cli monitor a 115200.
– Debe ver un banner y ayuda de comandos. Ejemplo:
– “zigbee-beehive-weight-sensing (UNO R3 + XBee S2C + HX711 + SHT31)”
– “Cal=2280 Period(ms)=5000 Avg=10”
– “Comandos (USB@115200): …”
– En ausencia de SHT31, verá “SHT31_ERR” en el campo status.

4) Calibración (imprescindible para peso real):
– Con la colmena/viga descargada, ejecute: “t” y Enter (tare).
– Coloque un peso patrón conocido (p.ej., 5.000 kg) sobre la plataforma.
– Observe la salida CSV en USB: “ts_ms,weight_kg,tempC,humRH,status”. Ajuste “c ” hasta que weight_kg se aproxime al valor real:
– Comando ejemplo: “c 2100” o “c 2450” y Enter, reintente hasta converger.
– Cuando esté satisfecho, ejecute “w” para guardar en EEPROM.

5) Verificación de transmisión Zigbee en el PC:
– Reciba desde el XBee coordinador. Dos opciones:
a) Uso de consola serial de XCTU:
– Abra una consola serial a 9600 baudios sobre el puerto del coordinador.
– Deben llegar líneas CSV periódicas: “12345,12.345,27.15,62.10,OK”
b) Uso de Python (pyserial) para registrar datos:

# Linux/Windows, instale pyserial 3.5
python -m pip install --user pyserial==3.5

Y el script:

# logger_zigbee_beehive.py
# Python 3.11.6 + pyserial 3.5
import sys, time, serial

if len(sys.argv) < 3:
    print("Uso: python logger_zigbee_beehive.py <PORT> <BAUD>")
    print("Ej:  python logger_zigbee_beehive.py COM7 9600   (Windows)")
    print("     python logger_zigbee_beehive.py /dev/ttyUSB0 9600  (Linux)")
    sys.exit(1)

port = sys.argv[1]
baud = int(sys.argv[2])

with serial.Serial(port, baud, timeout=1) as ser:
    print(f"Escuchando {port}@{baud} ... Ctrl+C para salir")
    while True:
        try:
            line = ser.readline().decode('utf-8', errors='ignore').strip()
            if line:
                ts = int(time.time())
                print(f"{ts},{line}")
        except KeyboardInterrupt:
            break
  • Ejecútelo:
    • Windows: python logger_zigbee_beehive.py COM7 9600
    • Linux: python3 logger_zigbee_beehive.py /dev/ttyUSB0 9600
  • Debe ver líneas con timestamp del PC + CSV del nodo.

6) Validación funcional:
– Cambie la carga sobre la célula (añada/retire masa). Verifique que weight_kg cambia coherentemente.
– Soplar ligeramente o tocar el sensor SHT31 debe alterar humedad/temperatura (leve).
– status debe ser “OK”; si SHT31 falla, verá “SHT31_ERR”.

7) Estabilidad:
– Observación de varios minutos: el peso debe fluctuar poco (ruido bajo). Si hay ruido visible:
– Aumente hx711Averages: “a 15”.
– Aumente samplePeriodMs: “p 10000”.

Troubleshooting (errores típicos y solución)

1) XBee no se asocia (AI != 0x00 en XCTU):
– Causa: PAN ID distinto, canal distinto, potencia insuficiente, antena mal, distancia excesiva.
– Solución: Asegure ID idéntico, AP=0 en ambos, BD=9600, CE=1 en el coordinador, JV=1 en el router, reinicie el nodo. Acerque los módulos. Compruebe alimentación estable en el Shield.

2) No llega nada al coordinador, pero el UNO imprime por USB:
– Causa: Baud rate en XBee distinto (p.ej., 115200 vs 9600), o SoftwareSerial en pines diferentes a los del Shield.
– Solución: Fije BD=9600 en XCTU para ambos módulos. Verifique que el Shield enruta DIN/DOUT a D2/D3 (jumpers). Si no, ajuste el sketch a los pines reales del Shield.

3) Reseteos aleatorios del UNO al transmitir:
– Causa: Caídas de tensión por picos de consumo del XBee.
– Solución: Use Shield XBee con regulador de calidad, fuente USB/5V estable. Añada condensador de 100 µF–470 µF cerca del XBee. Evite alimentar XBee desde el pin 3.3V del UNO sin Shield.

4) Lecturas de peso negativas o saturadas:
– Causa: Cables A+/A- invertidos o mala conexión en HX711; factor de calibración no ajustado.
– Solución: Revise polaridad A+/A-, apriete terminales. Tare (“t”) con la plataforma descargada. Ajuste factor con “c ”.

5) SHT31 no responde (status=SHT31_ERR):
– Causa: Dirección I2C distinta (0x45), cableado invertido SDA/SCL, ausencia de alimentación.
– Solución: Compruebe A4=SDA, A5=SCL, GND común, VIN=5V (o 3.3V según breakout). Si su placa usa 0x45, modifique sht31.begin(0x45).

6) La consola USB muestra caracteres, pero el coordinador (XBee) recibe basura:
– Causa: Confusión entre USB (115200) y XBee (9600).
– Solución: Recuerde que USB va a 115200; Zigbee (XBee) va a 9600. Ajuste baud del receptor al del XBee.

7) arduino-cli upload falla con “permission denied” o no encuentra el puerto:
– Causa: En Linux faltan permisos de dialout; puerto incorrecto.
– Solución: Agregue su usuario a “dialout”: sudo usermod -a -G dialout $USER; reabra sesión. Use arduino-cli board list para confirmar /dev/ttyACM0.

8) Ruido excesivo en medida de peso:
– Causa: Montaje mecánico con vibraciones o célula sin pre-carga; interferencias.
– Solución: Aisle vibraciones, fije bien la célula, use promediado HX711 (a 10–15), incremente periodo, añada filtro de media móvil en firmware.

Mejoras y variantes

  • Sincronización temporal real:
  • Añada un RTC (DS3231) por I2C para timestamp con fecha/hora real.
  • Baja potencia:
  • Migrar a alimentación por batería + panel solar. Use modos de sleep del ATmega328P y despierte por watchdog para muestrear cada 1–5 min. El XBee puede configurarse con ciclos de sueño (SP/ST) y rutas “pin sleep” si el diseño lo permite.
  • Fiabilidad de enlace:
  • Cambie a API Mode (AP=1/2) con tramas ZB Transmit; añade ACK, reintentos y direccionamiento explícito. En ese caso, valore usar una librería XBee específica para Arduino o construir frames manualmente.
  • Gestión de eventos:
  • Transmitir solo si variación de peso supera un umbral (delta) o cada N minutos, para ahorrar energía/ancho de banda.
  • Integridad de datos:
  • Añada CRC al final de la línea CSV (p.ej., CRC-8/16) y validación en el receptor.
  • Multisensor:
  • Añada sensor de peso adicional (doble viga) para detectar desplazamientos. Integre sensores de entrada/salida (IR) para conteo de abejas correlacionando con variaciones de peso.
  • Envoltorio y condiciones ambientales:
  • Diseñar caja estanca con desecante y respiradero para el SHT31 (membrana PTFE), y pasacables adecuados para la célula de carga.
  • Backend:
  • En el PC, reenviar datos a un broker MQTT o base de datos (InfluxDB/TimescaleDB), y dashboard (Grafana).

Checklist de verificación (marque cada ítem)

  • [ ] He instalado Arduino CLI 0.35.3 y el core arduino:avr@1.8.6.
  • [ ] He instalado las librerías exactas: HX711@0.7.5, Adafruit SHT31 Library@2.2.1, Adafruit BusIO@1.14.5.
  • [ ] He creado el sketch en ~/projects/zigbee_beehive_weight_sensing/ con el código proporcionado.
  • [ ] He cableado HX711 (DOUT→D4, SCK→D5), SHT31 (SDA→A4, SCL→A5), XBee (DOUT→D2, DIN→D3) mediante Shield.
  • [ ] He configurado los XBee en XCTU 6.5.10: Coordinador AT (CE=1), Router AT (DH/DL=addr del Coordinador), ambos BD=9600, PAN ID igual.
  • [ ] He compilado y subido el sketch al UNO con FQBN arduino:avr:uno.
  • [ ] En el monitor USB (115200) veo ayuda y líneas CSV periódicas.
  • [ ] He realizado tare (“t”) y calibración (“c ”) con un peso patrón, y guardado en EEPROM (“w”).
  • [ ] En el PC, con XCTU o Python (pyserial 3.5), recibo las líneas CSV desde el XBee coordinador.
  • [ ] Las lecturas de peso y SHT31 varían como se espera cuando cambio la carga o altero el entorno.

Apéndice: Notas de diseño para zigbee-beehive-weight-sensing

  • Diseño mecánico:
  • La célula de carga debe trabajar en su eje designado, con pre-carga mínima y rigidez lateral. Cualquier deformación lateral introduce offset y ruido.
  • Escala y calibración:
  • Use al menos dos puntos de calibración (0 kg y peso patrón cercano al rango operativo típico) para estimar linealidad.
  • Inmunidad:
  • Mantenga cables de señal de la célula retorcidos y cortos; separe del módulo XBee para minimizar acoplo RF.
  • Seguridad energética:
  • Si usa batería, añada protección contra sobredescarga y medición de voltaje (divisor resistivo a una entrada analógica, con muestreo ocasional).

Con este caso práctico, ha construido un nodo de sensado de peso para colmena con ENV por Zigbee, empleando exactamente Arduino Uno R3 + XBee Zigbee S2C (EM357) + HX711 + SHT31, compilado con Arduino CLI y verificado extremo a extremo con un coordinador XBee en el PC.

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 Ubuntu recomendada para el sistema operativo?




Pregunta 2: ¿Qué herramienta se usa para la configuración de los módulos XBee?




Pregunta 3: ¿Qué versión de Python se menciona para la validación en PC?




Pregunta 4: ¿Cuál es la librería de Arduino para el sensor de peso?




Pregunta 5: ¿Qué tipo de célula de carga se menciona en el artículo?




Pregunta 6: ¿Qué adaptador se sugiere para el coordinador en el PC?




Pregunta 7: ¿Qué versión de la herramienta Arduino CLI se menciona?




Pregunta 8: ¿Cuál es el FQBN para Arduino Uno?




Pregunta 9: ¿Qué tipo de regulador se menciona para el Shield XBee?




Pregunta 10: ¿Qué método se usa para validar la recepción en Python?




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: FFT vibraciones RS485 con Arduino y ADXL355

Caso práctico: FFT vibraciones RS485 con Arduino y ADXL355 — hero

Objetivo y caso de uso

Qué construirás: Un monitor de vibraciones FFT robusto utilizando Arduino Mega 2560 y ADXL355 para la transmisión de datos en tiempo real a través de RS-485.

Para qué sirve

  • Monitoreo de vibraciones en maquinaria industrial para detectar fallos.
  • Control de calidad en procesos de manufactura mediante análisis de vibraciones.
  • Aplicaciones en mantenimiento predictivo para evitar paradas no programadas.
  • Integración en sistemas de automatización para la supervisión remota de equipos.

Resultado esperado

  • Transmisión de datos de vibración en tiempo real con latencias menores a 100 ms.
  • Frecuencia de muestreo de vibraciones a 1 kHz para análisis FFT.
  • Mensajes de alerta enviados a través de RS-485 al detectar vibraciones anómalas.
  • Capacidad de enviar hasta 10 paquetes de datos por segundo a través de la red.

Público objetivo: Ingenieros y técnicos en automatización; Nivel: Avanzado

Arquitectura/flujo: Arduino Mega 2560 <-> ADXL355 <-> W5500 <-> RS-485

Nivel: Avanzado

Prerrequisitos

Sistema operativo y herramientas

  • Sistema operativo base (elige uno y mantén coherencia):
  • Linux: Ubuntu 22.04 LTS (Jammy) x86_64
  • Toolchain de Arduino (CLI, no GUI):
  • Arduino CLI v0.35.3 (linux-amd64)
  • Core AVR: arduino:avr@1.8.6
  • Librerías Arduino:
    • Ethernet@2.0.2 (para W5500)
    • arduinoFFT@1.6.0
    • SPI (incluida en el core)
  • Python 3.10 (para validación opcional) con:
  • pyserial==3.5
  • Adaptador USB–RS485 (para validación del bus RS485)

Permisos y preparación del entorno (Linux)

  • Añade tu usuario a dialout para acceso serie:
  • sudo usermod -aG dialout «$USER»
  • Cierra sesión y vuelve a entrar.
  • Directorio de trabajo limpio (por ejemplo, $HOME/proyectos/fft-vibration-monitor-rs485).

Red local

  • Red IPv4 básica con rango 192.168.1.0/24 (o adapta IP estática en el código).
  • Sin servidor DHCP estrictamente necesario si usas IP estática.

Materiales

  • Placa principal: Arduino Mega 2560 (ATmega2560).
  • Shield de red: Ethernet Shield W5500 (compatibilidad Arduino oficial, CS en D10).
  • Acelerómetro triaxial: ADXL355 (interfaz SPI, alimentación 3.3 V).
  • Transceptor RS485: MAX485 (modo half-duplex).
  • Nivelador de lógica bidireccional 5 V ↔ 3.3 V para SPI del ADXL355 (p. ej., TXB0104 o módulo BSS138 de 4 canales).
  • Resistencias y pasivos:
  • Terminación RS485: 120 Ω (colocar en el extremo de la línea, cerca del MAX485 si es fin de línea).
  • Resistencias de polarización (bias) RS485 en el bus (si tu red no las tiene): típicamente 680 Ω–1 kΩ entre A–Vcc y B–GND en un único punto.
  • Fuente de alimentación estable 5 V para Arduino (USB o externa) y 3.3 V para el ADXL355 (puede provenir del 3.3 V del Mega o del Shield; verificar capacidad de corriente).
  • Cables Dupont y cable par trenzado para la línea RS485 (A/B).
  • Adaptador USB–RS485 para el PC (para validación).
  • Opcional: base o soporte para el sensor y un pequeño motor o vibrador para generar vibraciones reproducibles.

Nota: El conjunto es exactamente “Arduino Mega 2560 + Ethernet Shield W5500 + ADXL355 + MAX485” y toda la guía asume estos cuatro elementos.

Preparación y conexión

Reglas generales de cableado

  • Mantén GND común entre todos los módulos.
  • El ADXL355 es 3.3 V-only. Nunca apliques 5 V a sus pines de lógica. Usa nivelador para MOSI, SCK y CS. La línea MISO del ADXL355 a 3.3 V suele ser interpretada como HIGH por el Mega, pero es buena práctica encaminarla a través del nivelador si el módulo lo requiere.
  • Todos los dispositivos SPI comparten SCK/MOSI/MISO; cada uno debe tener su propia línea CS (Chip Select). Asegúrate de poner en HIGH los CS de los dispositivos que no estés usando en cada transacción.
  • El Ethernet Shield W5500 usa el bus SPI por el conector ICSP y CS en D10. La SD del shield usa CS en D4 (mantenla en HIGH si no se usa).
  • RS485 (MAX485) es half-duplex: controla las líneas DE/RE con un pin digital para alternar transmisión/recepción.

Mapa de pines y conexiones

Tabla de cableado resumido:

Módulo Señal Arduino Mega 2560 Notas
W5500 (Shield) SPI ICSP (SCK/MISO/MOSI) Se conecta por el header ICSP del Shield
W5500 (Shield) CS D10 Mantener HIGH cuando SPI se use con otros dispositivos
W5500 (Shield) SD CS D4 Mantener HIGH si no se usa la SD
ADXL355 (SPI) VCC 3.3 V Alimentación 3.3 V
ADXL355 (SPI) GND GND Tierra común
ADXL355 (SPI) SCK D52 (SCK) ↔ nivelador SPI compartido, va al nivelador hacia el sensor
ADXL355 (SPI) MOSI D51 (MOSI) ↔ nivelador SPI compartido, 5 V→3.3 V
ADXL355 (SPI) MISO D50 (MISO) (3.3 V) 3.3 V suele ser aceptado; opcional nivelador
ADXL355 (SPI) CS D7 ↔ nivelador CS dedicado para el ADXL355
ADXL355 (INT) DRDY D3 (INT1) Señal de “data ready” (opcional pero recomendable)
MAX485 VCC 5 V Alimentación del transceptor
MAX485 GND GND Tierra común
MAX485 RO (Receiver Out) D19 (RX1) UART1 RX del Mega
MAX485 DI (Driver In) D18 (TX1) UART1 TX del Mega
MAX485 /RE y DE D2 (control) Une /RE y DE, controla con D2
MAX485 A/B Línea RS485 Conectar a bus y poner 120 Ω si eres extremo
Arduino Mega USB PC Para cargar firmware y depurar por Serial

Notas avanzadas:
– Si usas DRDY del ADXL355, podrás muestrear con jitter mínimo y exactitud de ODR (muy recomendable para FFT).
– Mantén los cables SPI cortos y ordenados para reducir EMI.
– Coloca el ADXL355 firmemente sobre la estructura cuyas vibraciones deseas medir (acoplamiento mecánico firme).

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

A continuación, un sketch monolítico que:
– Inicializa W5500 con IP estática.
– Inicializa el ADXL355 en SPI (modo medición, rango ±2 g).
– Toma 256 muestras a 1 kHz del eje Z (opcionalmente por DRDY).
– Calcula FFT con arduinoFFT y obtiene picos dominantes.
– Expone los resultados por RS485 (comandos de texto) y por HTTP (endpoint /status).
– Evita conflictos SPI con selección adecuada de CS.

Características del protocolo RS485:
– Velocidad: 115200 8N1 en Serial1 (pines 18/19).
– Control de dirección (D2): HIGH para transmitir, LOW para recibir.
– Comandos (terminados en ‘
‘):
– ID?
– GET:PEAKS
– GET:RMS
– GET:FFT (devuelve magnitudes de N/2 bins como CSV reducido, opcional)

Bloque 1/2 – Sketch principal:

/*
  fft-vibration-monitor-rs485.ino
  Dispositivo: Arduino Mega 2560 + Ethernet Shield W5500 + ADXL355 + MAX485
  Toolchain: Arduino CLI v0.35.3, core arduino:avr@1.8.6
  Librerías: Ethernet@2.0.2, arduinoFFT@1.6.0, SPI (core)
*/

#include <SPI.h>
#include <Ethernet.h>
#include <arduinoFFT.h>

// ------------------------ Configuración de pines ------------------------
static const uint8_t PIN_CS_W5500   = 10; // CS Ethernet
static const uint8_t PIN_CS_SD      = 4;  // CS SD en el Shield
static const uint8_t PIN_CS_ADXL    = 7;  // CS del ADXL355
static const uint8_t PIN_ADXL_DRDY  = 3;  // DRDY -> INT1 (opcional)
static const uint8_t PIN_RS485_DIR  = 2;  // DE y /RE del MAX485 unidos -> D2

// ------------------------ Red (Ethernet W5500) --------------------------
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x01 };
IPAddress ip(192, 168, 1, 177);
EthernetServer server(80);

// ------------------------ ADXL355 (SPI) ---------------------------------
// Registro y constantes (ver datasheet ADXL355)
#define ADXL355_REG_DEVID_AD   0x00
#define ADXL355_REG_DEVID_MST  0x01
#define ADXL355_REG_PARTID     0x02
#define ADXL355_REG_REVID      0x03
#define ADXL355_REG_STATUS     0x04
#define ADXL355_REG_TEMP2      0x06
#define ADXL355_REG_TEMP1      0x07
#define ADXL355_REG_XDATA3     0x08
#define ADXL355_REG_XDATA2     0x09
#define ADXL355_REG_XDATA1     0x0A
#define ADXL355_REG_YDATA3     0x0B
#define ADXL355_REG_YDATA2     0x0C
#define ADXL355_REG_YDATA1     0x0D
#define ADXL355_REG_ZDATA3     0x0E
#define ADXL355_REG_ZDATA2     0x0F
#define ADXL355_REG_ZDATA1     0x10
#define ADXL355_REG_FILTER     0x28
#define ADXL355_REG_RANGE      0x2C
#define ADXL355_REG_POWER_CTL  0x2D
#define ADXL355_REG_RESET      0x2F

// Modo SPI: CPOL=0, CPHA=0 (Mode 0), MSB first
SPISettings spiADXL(5000000, MSBFIRST, SPI_MODE0); // 5 MHz (ajustable)

// Escala (LSB/g) aproximada del ADXL355 en ±2g (ver datasheet)
static const float ADXL355_LSB_PER_G = 256000.0f;

// ------------------------ FFT y muestreo --------------------------------
static const uint16_t FS_HZ      = 1000;  // Frecuencia de muestreo efectiva
static const uint16_t N_SAMPLES  = 256;   // Longitud de la FFT (potencia de 2)
static const float    INV_FS     = 1.0f / FS_HZ;

double vReal[N_SAMPLES];
double vImag[N_SAMPLES];

arduinoFFT FFT = arduinoFFT(vReal, vImag, N_SAMPLES, FS_HZ);

// Buffer de adquisición
volatile uint16_t sampleIndex = 0;
volatile bool bufferReady = false;
volatile float bufZ[N_SAMPLES]; // Aceleración (g) eje Z

// Métricas
volatile float lastRMS = 0.0f;
volatile float lastPeakFreq = 0.0f;

// Resultados de picos (para respuesta)
static const uint8_t NUM_TOP_PEAKS = 8;
float topFreq[NUM_TOP_PEAKS];
float topMag[NUM_TOP_PEAKS];

// ------------------------ Utilidades SPI/CS -----------------------------
inline void csHighAll() {
  digitalWrite(PIN_CS_W5500, HIGH);
  digitalWrite(PIN_CS_SD, HIGH);
  digitalWrite(PIN_CS_ADXL, HIGH);
}

uint8_t adxl355_read8(uint8_t reg) {
  uint8_t val;
  csHighAll();
  digitalWrite(PIN_CS_ADXL, LOW);
  SPI.beginTransaction(spiADXL);
  // Lectura: bit 7 = 1 indica lectura, dirección en bits 6..0
  SPI.transfer(0x80 | (reg & 0x7F));
  val = SPI.transfer(0x00);
  SPI.endTransaction();
  digitalWrite(PIN_CS_ADXL, HIGH);
  return val;
}

void adxl355_write8(uint8_t reg, uint8_t val) {
  csHighAll();
  digitalWrite(PIN_CS_ADXL, LOW);
  SPI.beginTransaction(spiADXL);
  // Escritura: bit 7 = 0
  SPI.transfer(reg & 0x7F);
  SPI.transfer(val);
  SPI.endTransaction();
  digitalWrite(PIN_CS_ADXL, HIGH);
}

int32_t adxl355_read20(uint8_t regMSB) {
  // Lee 20 bits firmados (en 3 bytes, donde los 4 bits LSB del tercer byte son significativos)
  uint8_t b3, b2, b1;
  int32_t raw = 0;
  csHighAll();
  digitalWrite(PIN_CS_ADXL, LOW);
  SPI.beginTransaction(spiADXL);
  SPI.transfer(0x80 | (regMSB & 0x7F)); // dirección de XDATA3/YDATA3/ZDATA3
  b3 = SPI.transfer(0x00);
  b2 = SPI.transfer(0x00);
  b1 = SPI.transfer(0x00);
  SPI.endTransaction();
  digitalWrite(PIN_CS_ADXL, HIGH);

  raw = ((int32_t)b3 << 12) | ((int32_t)b2 << 4) | ((b1 >> 4) & 0x0F);
  // Extensión de signo de 20 bits
  if (raw & 0x80000) {
    raw |= 0xFFF00000;
  }
  return raw;
}

bool adxl355_init() {
  // Verifica IDs
  uint8_t devid_ad  = adxl355_read8(ADXL355_REG_DEVID_AD);
  uint8_t devid_mst = adxl355_read8(ADXL355_REG_DEVID_MST);
  uint8_t partid    = adxl355_read8(ADXL355_REG_PARTID);
  // Valores típicos esperados: 0xAD, 0x1D, 0xED
  if (devid_ad != 0xAD || devid_mst != 0x1D || partid != 0xED) {
    return false;
  }

  // Reset suave (opcional)
  adxl355_write8(ADXL355_REG_RESET, 0x52); // Key 'R'

  delay(20);

  // Standby para configurar (bit 0 de POWER_CTL = 0)
  // Según datasheet, POWER_CTL[0]=0 -> Standby, [0]=1 -> Measurement
  uint8_t pwr = adxl355_read8(ADXL355_REG_POWER_CTL);
  pwr &= ~0x01; // asegurar Standby
  adxl355_write8(ADXL355_REG_POWER_CTL, pwr);

  // Rango ±2g (ver datasheet: RANGE bits 1:0 seleccionan rango)
  // 0x01: ±2g (según hoja de datos; validar si tu módulo usa diferente mapeo)
  adxl355_write8(ADXL355_REG_RANGE, 0x01);

  // ODR 1000 Hz (aprox.). En ADXL355_REG_FILTER, bits 3:0 seleccionan ODR/LPF.
  // Un valor típico para ~1 kHz es 0x05 (consultar tablas en hoja de datos).
  // Ajusta si necesitas ODR preciso.
  adxl355_write8(ADXL355_REG_FILTER, 0x05);

  // Measurement mode
  pwr = adxl355_read8(ADXL355_REG_POWER_CTL);
  pwr |= 0x01; // bit 0 a 1 -> Measurement
  adxl355_write8(ADXL355_REG_POWER_CTL, pwr);

  delay(10);
  return true;
}

float adxl355_readZ_g() {
  int32_t raw = adxl355_read20(ADXL355_REG_ZDATA3);
  // Conversión a g (aprox.)
  return ((float)raw) / ADXL355_LSB_PER_G;
}

// ------------------------ RS485 (Serial1) -------------------------------
void rs485_setRx() { digitalWrite(PIN_RS485_DIR, LOW);  }
void rs485_setTx() { digitalWrite(PIN_RS485_DIR, HIGH); }
void rs485_println(const String &s) {
  rs485_setTx();
  Serial1.print(s);
  Serial1.print('\n');
  Serial1.flush();
  rs485_setRx();
}

// ------------------------ Temporización de muestreo ---------------------
void setupTimer1_1kHz() {
  // Timer1 CTC a 1 kHz: f_clk = 16 MHz, prescaler 1, OCR1A = 15999
  noInterrupts();
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1  = 0;
  OCR1A = 15999; // 16e6 / 1e3 - 1
  TCCR1B |= (1 << WGM12); // CTC
  TCCR1B |= (1 << CS10);  // prescaler 1
  TIMSK1 |= (1 << OCIE1A);
  interrupts();
}

ISR(TIMER1_COMPA_vect) {
  if (bufferReady) return; // espera a que procesen
  // Lectura directa del eje Z a ~1 kHz
  float z_g = adxl355_readZ_g();
  bufZ[sampleIndex] = z_g;
  sampleIndex++;
  if (sampleIndex >= N_SAMPLES) {
    sampleIndex = 0;
    bufferReady = true;
  }
}

// ------------------------ Procesamiento FFT -----------------------------
void computeFFTAndMetrics() {
  // Copiar el buffer a vReal/vImag y aplicar ventana Hann
  for (uint16_t i = 0; i < N_SAMPLES; i++) {
    double w = 0.5 * (1.0 - cos(2.0 * PI * i / (N_SAMPLES - 1)));
    vReal[i] = (double)bufZ[i] * w;
    vImag[i] = 0.0;
  }

  // RMS (dominio tiempo)
  double sum2 = 0.0;
  for (uint16_t i = 0; i < N_SAMPLES; i++) sum2 += vReal[i] * vReal[i];
  lastRMS = sqrt(sum2 / N_SAMPLES);

  // FFT
  FFT.windowing(vReal, N_SAMPLES, FFT_WIN_TYP_RECTANGLE, FFT_FORWARD); // ya aplicamos Hann, pero dejamos sin ventana aquí
  FFT.compute(vReal, vImag, N_SAMPLES, FFT_FORWARD);
  FFT.complexToMagnitude(vReal, vImag, N_SAMPLES);

  // Encontrar picos en 0..Fs/2
  // Ignora bin 0 (DC)
  uint16_t startBin = 1;
  uint16_t endBin = (N_SAMPLES / 2) - 1;

  // Inicializa arrays de top N picos
  for (uint8_t k = 0; k < NUM_TOP_PEAKS; k++) {
    topFreq[k] = 0.0f;
    topMag[k]  = 0.0f;
  }

  // Búsqueda simple de picos
  double maxMag = 0.0;
  uint16_t maxBin = 0;

  for (uint16_t bin = startBin; bin <= endBin; bin++) {
    double mag = vReal[bin];
    // Peak global
    if (mag > maxMag) {
      maxMag = mag;
      maxBin = bin;
    }
    // Inserción ordenada en top N
    for (uint8_t k = 0; k < NUM_TOP_PEAKS; k++) {
      if (mag > topMag[k]) {
        // Desplaza hacia abajo
        for (int8_t j = NUM_TOP_PEAKS - 1; j > (int8_t)k; j--) {
          topMag[j]  = topMag[j - 1];
          topFreq[j] = topFreq[j - 1];
        }
        topMag[k]  = mag;
        topFreq[k] = (float)bin * ((float)FS_HZ / (float)N_SAMPLES);
        break;
      }
    }
  }

  lastPeakFreq = (float)maxBin * ((float)FS_HZ / (float)N_SAMPLES);
}

// ------------------------ HTTP /status ----------------------------------
void handleHttpClient(EthernetClient &client) {
  // Lectura simple de la primera línea
  String req = client.readStringUntil('\n');
  if (req.indexOf("GET /status") >= 0 || req.indexOf("GET / ") >= 0) {
    // Respuesta JSON simple
    String body = "{";
    body += "\"device\":\"fft-vibration-monitor-rs485\",";
    body += "\"board\":\"Arduino Mega 2560\",";
    body += "\"fs\":" + String(FS_HZ) + ",";
    body += "\"n\":" + String(N_SAMPLES) + ",";
    body += "\"rms_g\":" + String(lastRMS, 6) + ",";
    body += "\"peak_freq_hz\":" + String(lastPeakFreq, 2) + ",";
    body += "\"top_peaks\":[";
    for (uint8_t k = 0; k < NUM_TOP_PEAKS; k++) {
      body += "{\"f\":" + String(topFreq[k], 2) + ",\"a\":" + String(topMag[k], 6) + "}";
      if (k < NUM_TOP_PEAKS - 1) body += ",";
    }
    body += "]";
    body += "}\n";

    client.println("HTTP/1.1 200 OK");
    client.println("Content-Type: application/json");
    client.print("Content-Length: ");
    client.println(body.length());
    client.println("Connection: close");
    client.println();
    client.print(body);
  } else {
    client.println("HTTP/1.1 404 Not Found");
    client.println("Content-Length: 0");
    client.println("Connection: close");
    client.println();
  }
}

// ------------------------ Comandos RS485 --------------------------------
String cmdBuf;
void handleRS485() {
  while (Serial1.available() > 0) {
    char c = (char)Serial1.read();
    if (c == '\r') continue;
    if (c == '\n') {
      String line = cmdBuf;
      cmdBuf = "";
      line.trim();
      if (line == "ID?") {
        rs485_println("ID,ArduinoMega2560,ADXL355,W5500,MAX485");
      } else if (line == "GET:RMS") {
        rs485_println("RMS_G," + String(lastRMS, 6));
      } else if (line == "GET:PEAKS") {
        // Responde pares f,a separados por punto y coma
        String resp = "PEAKS";
        for (uint8_t k = 0; k < NUM_TOP_PEAKS; k++) {
          resp += ",";
          resp += String(topFreq[k], 2);
          resp += ",";
          resp += String(topMag[k], 6);
        }
        rs485_println(resp);
      } else if (line == "GET:FFT") {
        // Envía magnitudes de 0..N/2-1 (corta si quieres ahorrar ancho de banda)
        rs485_println("FFT,Fs=" + String(FS_HZ) + ",N=" + String(N_SAMPLES));
        String row = "";
        for (uint16_t bin = 0; bin < (N_SAMPLES / 2); bin++) {
          row += String(vReal[bin], 6);
          if (bin < (N_SAMPLES / 2) - 1) row += ",";
        }
        rs485_println(row);
      } else {
        rs485_println("ERR,UNKNOWN_CMD");
      }
    } else {
      if (cmdBuf.length() < 128) cmdBuf += c;
    }
  }
}

// ------------------------ Setup / Loop ----------------------------------
void setup() {
  pinMode(PIN_CS_W5500, OUTPUT);
  pinMode(PIN_CS_SD, OUTPUT);
  pinMode(PIN_CS_ADXL, OUTPUT);
  pinMode(PIN_RS485_DIR, OUTPUT);
  pinMode(PIN_ADXL_DRDY, INPUT); // opcional si se conecta DRDY

  csHighAll();
  rs485_setRx();

  Serial.begin(115200);   // Depuración por USB
  Serial1.begin(115200);  // RS485 (MAX485)

  // SPI
  SPI.begin();

  // Ethernet
  Ethernet.init(PIN_CS_W5500);
  Ethernet.begin(mac, ip);
  delay(100);
  server.begin();

  Serial.print("IP: ");
  Serial.println(Ethernet.localIP());

  // Inicializa ADXL355
  if (!adxl355_init()) {
    Serial.println("Error: ADXL355 no detectado (IDs no coinciden).");
  } else {
    Serial.println("ADXL355 OK");
  }

  // Timer de muestreo (1 kHz)
  setupTimer1_1kHz();

  Serial.println("Setup completo.");
}

void loop() {
  // Procesar buffer si listo
  if (bufferReady) {
    noInterrupts();
    bufferReady = false;
    interrupts();
    computeFFTAndMetrics();
  }

  // RS485
  handleRS485();

  // HTTP
  EthernetClient client = server.available();
  if (client) {
    // Esperar datos y atender
    unsigned long t0 = millis();
    while (client.connected() && millis() - t0 < 100) {
      if (client.available()) {
        handleHttpClient(client);
        break;
      }
    }
    delay(1);
    client.stop();
  }
}

Breve explicación de partes clave:
– Selección de CS: csHighAll asegura que solo un dispositivo SPI esté activo a la vez. El W5500 y la SD del shield quedan deseleccionados durante transacciones con el ADXL355.
– adxl355_read20: el ADXL355 entrega 20 bits por eje en 3 bytes; se realiza sign-extend apropiado a 32 bits.
– ODR: se configura en 0x05 para obtener ~1 kHz; si necesitas frecuencias exactas o diferentes, consulta la tabla de ODR/LPF del datasheet (puedes ajustar en ADXL355_REG_FILTER).
– Timer1: genera una IRQ a 1 kHz para muestrear de forma estable sin jitter del loop.
– FFT: se aplica ventana Hann previa al cálculo para reducir leakage; se busca el pico global y se extraen los top N picos.
– RS485: se usa D2 para conmutar el MAX485 entre TX y RX; se adoptan comandos de texto sencillos.
– Ethernet: expone /status con JSON mínimo para supervisión remota.

Bloque 2/2 – Script de validación (Python, PC) para RS485:

# validate_rs485.py
# Requiere: Python 3.10 + pyserial==3.5
# Uso:
#   python3 validate_rs485.py /dev/ttyUSB0 115200
# Conecta el adaptador USB-RS485 al bus A/B junto con el MAX485 del Mega.

import sys
import serial
import time

def send_cmd(ser, cmd):
    ser.write((cmd + "\n").encode("ascii"))
    ser.flush()

def read_line(ser, timeout=2.0):
    ser.timeout = timeout
    line = ser.readline().decode("ascii", errors="ignore").strip()
    return line

if __name__ == "__main__":
    if len(sys.argv) < 3:
        print("Uso: python3 validate_rs485.py <puerto> <baud>")
        sys.exit(1)

    port = sys.argv[1]
    baud = int(sys.argv[2])

    with serial.Serial(port, baudrate=baud, bytesize=8, parity='N', stopbits=1) as ser:
        time.sleep(0.2)
        send_cmd(ser, "ID?")
        print("-> ID?")
        print("<- " + read_line(ser))

        send_cmd(ser, "GET:RMS")
        print("-> GET:RMS")
        print("<- " + read_line(ser))

        send_cmd(ser, "GET:PEAKS")
        print("-> GET:PEAKS")
        print("<- " + read_line(ser))

        send_cmd(ser, "GET:FFT")
        print("-> GET:FFT (cabecera)")
        print("<- " + read_line(ser))
        print("-> GET:FFT (datos)")
        print("<- " + read_line(ser))

Compilación, carga y ejecución (Arduino CLI)

Comandos exactos y ordenados:

1) Instala Arduino CLI v0.35.3 (si no lo tienes)
– Linux (x86_64):
– wget https://downloads.arduino.cc/arduino-cli/arduino-cli_latest_Linux_64bit.tar.gz -O /tmp/arduino-cli.tar.gz
– sudo tar -xzf /tmp/arduino-cli.tar.gz -C /usr/local/bin –strip-components=1 arduino-cli

2) Verifica versión:
– arduino-cli version
– Debe mostrar: arduino-cli Version: 0.35.3

3) Prepara el core AVR:
– arduino-cli core update-index
– arduino-cli core install arduino:avr@1.8.6

4) Prepara un directorio de sketch:
– mkdir -p $HOME/proyectos/fft-vibration-monitor-rs485
– cd $HOME/proyectos/fft-vibration-monitor-rs485
– Crea el archivo fft-vibration-monitor-rs485.ino con el código C++ anterior.

5) Instala librerías exactas:
– arduino-cli lib install «Ethernet@2.0.2»
– arduino-cli lib install «arduinoFFT@1.6.0»

6) Identifica el puerto serie del Mega:
– arduino-cli board list
– Localiza tu Arduino Mega 2560 y anota el puerto (ej.: /dev/ttyACM0)

7) Compila para Arduino Mega 2560 (FQBN: arduino:avr:mega):
– arduino-cli compile –fqbn arduino:avr:mega –warnings all –build-path build .

8) Sube el firmware:
– arduino-cli upload -p /dev/ttyACM0 –fqbn arduino:avr:mega –input-dir build

9) Monitorea logs (opcional, USB a 115200):
– arduino-cli monitor -p /dev/ttyACM0 -c baudrate=115200

10) Validación RS485 desde PC (con adaptador USB–RS485):
– pip3 install pyserial==3.5
– python3 validate_rs485.py /dev/ttyUSB0 115200

11) Validación HTTP:
– curl -s http://192.168.1.177/status | jq .
– Si no tienes jq, usa:
– curl -s http://192.168.1.177/status

Validación paso a paso

1) Verificación de IDs del ADXL355:
– Abre el monitor serie por USB (115200).
– Al iniciar, deberías ver:
– “IP: 192.168.1.177”
– “ADXL355 OK”
– “Setup completo.”
– Si aparece “ADXL355 no detectado”, revisa SPI/CS y niveles lógicos.

2) Muestreo y FFT:
– El dispositivo muestrea 256 puntos a 1 kHz (ventana Hann) y calcula FFT.
– No hay UI visual, pero:
– Enviando GET:RMS por RS485, deberías recibir valores ~0.005–0.05 g si el sensor está quieto (ruido térmico + vibración ambiente).
– Enviando GET:PEAKS, con el sensor quieto, el pico dominante puede estar cerca de DC; con un motor pequeño o golpe seco, verás picos a su frecuencia fundamental y armónicos.

3) Prueba RS485:
– Conecta el adaptador USB–RS485 del PC al bus A/B (A→A, B→B).
– Ejecuta el script Python:
– Debes ver:
– “ID,ArduinoMega2560,ADXL355,W5500,MAX485”
– “RMS_G,0.00xxxx”
– “PEAKS, f1,a1, f2,a2, …” (8 picos)
– El comando GET:FFT devuelve cabecera y una línea larga con magnitudes.

4) Prueba HTTP /status:
– curl http://192.168.1.177/status
– Debe retornar un JSON con: device, fs, n, rms_g, peak_freq_hz y top_peaks.
– Repite la petición mientras haces vibrar el sensor; peak_freq_hz se moverá hacia la frecuencia dominante observada.

5) Validación de integridad del bus RS485:
– Si usas línea larga, instala terminación 120 Ω en ambos extremos.
– Verifica que solo exista un par de resistencias de polarización (bias) en todo el bus (en un único punto).

6) Consistencia de SPI:
– Asegúrate de que D10 y D4 estén en HIGH cuando el ADXL355 sea el dispositivo activo, y que D7 esté en HIGH cuando el W5500 sea activo. El sketch ya gestiona esto con csHighAll().

Troubleshooting

1) ADXL355 no responde (IDs incorrectos):
– Síntomas: “ADXL355 no detectado (IDs no coinciden)”.
– Causas probables:
– CS incorrecto: verifica que el ADXL355 esté en D7 y que D10 y D4 estén HIGH durante la transacción.
– Sin nivelador de lógica: si alimentas con 3.3 V y pines de 5 V sin adaptar, el sensor puede dañarse o no responder.
– Cableado SPI incorrecto (MOSI/MISO invertidos).
– Solución: verifica mapeo, nivelador, continuidad y tensiones.

2) Ethernet deja de funcionar al iniciar muestreo:
– Síntomas: /status no responde tras unos segundos.
– Causas: conflicto SPI por CS mal gestionado o ISR muy pesada.
– Solución: confirma csHighAll() antes de operar con ADXL355; reduce la frecuencia SPI si fuera necesario (p. ej., 2 MHz).

3) FFT inconsistente (picos varían mucho):
– Causas: muestreo no estable, ODR no coincide con FS, vibración insuficiente o aliasing.
– Solución:
– Ajusta ADXL355_REG_FILTER para ODR ~ FS (1 kHz).
– Usa DRDY del ADXL355 con attachInterrupt para muestreo exacto por “data ready”.
– Asegura fijación mecánica rígida del sensor (evita foam o cinta blanda).

4) RS485 responde con errores o no responde:
– Causas: sin control de dirección (DE/RE), baudrate distinto, terminación/bias deficientes.
– Solución:
– Verifica que D2 conmute DE/RE (LOW para Rx, HIGH para Tx).
– Asegura 115200 8N1 en ambos lados.
– Añade 120 Ω en extremos y bias en un único punto.

5) Medidas saturadas (g muy altos):
– Causas: rango inadecuado (±2 g) frente a vibraciones fuertes.
– Solución: cambia el rango del ADXL355 (RANGE) a ±4 g o ±8 g (ver datasheet) y actualiza el factor LSB/g.

6) Datos de FFT “planos” (todo ~0):
– Causas: lectura del eje incorrecta (registro mal, bytes mal ensamblados), CS del sensor bajo permanentemente.
– Solución:
– Verifica adxl355_read20: orden de bytes y extensión de signo.
– Comprueba que el pin CS del ADXL355 esté alto en reposo y solo bajo durante la transacción.

7) HTTP bloquea RS485 o viceversa:
– Causas: uso intensivo del loop sin gestionar tiempos; cliente HTTP no libera conexión.
– Solución:
– Mantén timeouts cortos en HTTP (como en el sketch).
– No hagas prints excesivos en Serial.
– Evita operaciones de bloqueo largas dentro del loop.

8) Ruido excesivo en espectro:
– Causas: acoplamiento mecánico pobre, cables largos, interferencias EMI.
– Solución:
– Usa cable apantallado para el sensor si la distancia lo requiere.
– Asegura masa común.
– Filtra en banda (LPF/HPF del ADXL355 vía FILTER) o aplica más promediado.

Mejoras y variantes

  • Sincronización por DRDY:
  • Conecta DRDY del ADXL355 al pin D3 y usa attachInterrupt para leer muestra justo cuando el sensor la tenga lista. Desactiva el Timer1 o úsalo como watchdog. Mejorará la coherencia temporal y la ubicación de picos en frecuencia.

  • Cambiar ventana de FFT:

  • Prueba Blackman, Hamming o Flat Top (si implementas manualmente) para diferentes compromisos entre resolución y amplitud de pico.

  • Publicación UDP/MQTT por Ethernet:

  • Añade un cliente MQTT (p. ej., PubSubClient) o UDP broadcast con las métricas (RMS, pico principal). Esto facilita integración en SCADA/IIoT.

  • Protocolo Modbus RTU por RS485:

  • Estructura registros para RMS, pico, ODR, estado, etc., y usa un stack Modbus RTU esclavo. Esto estandariza la integración.

  • Promediado espectral:

  • Realiza varios bloques de N_SAMPLES, promedia magnitudes (Welch) y reduce varianza. Aumenta estabilidad de picos.

  • Configuración remota:

  • Implementa comandos por RS485/HTTP para cambiar N_SAMPLES, FS, rango del sensor, IP estática y número de picos a reportar.

  • Ejes múltiples:

  • Procesa X/Y/Z y reporta vector RMS y picos por eje. Aumenta el costo computacional; considera N=128 por eje para mantener tiempos.

Checklist de verificación

  • [ ] Toolchain exacta instalada:
  • [ ] Arduino CLI v0.35.3
  • [ ] Core arduino:avr@1.8.6
  • [ ] Librerías: Ethernet@2.0.2, arduinoFFT@1.6.0

  • [ ] Cableado correcto y coherente:

  • [ ] W5500 en Shield con CS D10 y SD CS D4 (alto si no se usa).
  • [ ] ADXL355 a 3.3 V, SPI con nivelador y CS en D7.
  • [ ] DRDY del ADXL355 a D3 (opcional).
  • [ ] MAX485: RO→D19 (RX1), DI→D18 (TX1), DE/RE→D2, A/B al bus.

  • [ ] RS485 preparado:

  • [ ] Terminación 120 Ω en los extremos del bus.
  • [ ] Bias en un único punto (si el bus lo requiere).
  • [ ] Adaptador USB–RS485 en el PC y polaridad A/B correcta.

  • [ ] Compilación y carga:

  • [ ] arduino-cli core update-index / install realizados.
  • [ ] arduino-cli lib install con versiones exactas.
  • [ ] Compilado con FQBN arduino:avr:mega y subido sin errores.

  • [ ] Arranque correcto:

  • [ ] Monitor USB muestra IP y “ADXL355 OK”.
  • [ ] /status responde por HTTP.

  • [ ] Validación funcional:

  • [ ] GET:RMS devuelve un valor coherente (quieto vs. vibrando).
  • [ ] GET:PEAKS muestra frecuencias lógicas cuando se activa un vibrador/motor.
  • [ ] GET:FFT devuelve cabecera y datos.

  • [ ] Estabilidad espectral:

  • [ ] Picos consistentes al repetir medición.
  • [ ] Sin bloqueos al alternar RS485/HTTP.

Con este caso práctico has construido un monitor de vibraciones FFT robusto sobre RS485 utilizando exactamente el combo “Arduino Mega 2560 + Ethernet Shield W5500 + ADXL355 + MAX485”, compilado y desplegado con Arduino CLI (core arduino:avr@1.8.6), y validado tanto por RS485 como por HTTP. Esta base es extensible hacia protocolos industriales (Modbus RTU/TCP) y a técnicas de análisis espectral más avanzadas.

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 base recomendado para el proyecto?




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




Pregunta 3: ¿Qué librería se utiliza para la comunicación Ethernet en este proyecto?




Pregunta 4: ¿Cuál es el modelo del acelerómetro utilizado?




Pregunta 5: ¿Qué tipo de transceptor RS485 se menciona en los requisitos?




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




Pregunta 7: ¿Qué tipo de conexión se requiere para el ADXL355?




Pregunta 8: ¿Cuál es la resistencia de terminación recomendada para RS485?




Pregunta 9: ¿Qué voltaje de alimentación se requiere para el ADXL355?




Pregunta 10: ¿Cuál es el directorio de trabajo recomendado para 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: Rover autoequilibrado con IMU en Nano 33 BLE

Caso práctico: Rover autoequilibrado con IMU en Nano 33 BLE — hero

Objetivo y caso de uso

Qué construirás: Un rover autoequilibrado utilizando un Arduino Nano 33 BLE Sense, un controlador TB6612FNG y un sensor AS5600 para la detección de posición.

Para qué sirve

  • Control de equilibrio en tiempo real para vehículos robóticos.
  • Aplicaciones en robótica educativa para enseñar principios de control y estabilidad.
  • Integración de sensores para mejorar la navegación autónoma en entornos complejos.
  • Prototipos de vehículos que requieren ajuste dinámico de posición.

Resultado esperado

  • Estabilidad del rover con un tiempo de respuesta de menos de 100 ms.
  • Latencia en la comunicación entre el IMU y el controlador de motor inferior a 50 ms.
  • Capacidad de mantener el equilibrio en superficies inclinadas de hasta 15 grados.
  • Medición de la posición angular con una precisión de ±1 grado utilizando el AS5600.

Público objetivo: Estudiantes y profesionales en robótica; Nivel: Avanzado

Arquitectura/flujo: Sensor IMU (LSM9DS1) -> Procesador (nRF52840) -> Controlador de motor (TB6612FNG) -> Actuadores (motores del rover).

Nivel: Avanzado

Prerrequisitos

Sistema operativo y herramientas (versiones probadas)

  • Windows 11 23H2, macOS 14.5 (Sonoma) o Ubuntu 22.04 LTS.
  • Git 2.44.0 o superior.
  • Python 3.11.x (probado con 3.11.9).
  • PlatformIO Core 6.1.15 (CLI). Nota: Usaremos PlatformIO para la placa Arduino Nano 33 BLE Sense (nRF52840, Mbed OS). No usaremos Arduino IDE/GUI.

Toolchain concreta (con versiones)

  • PlatformIO Core (pio) 6.1.15.
  • Plataforma: nordicnrf52 @ 10.3.1 (pinned).
  • Board ID: nano33ble (compatible con Arduino Nano 33 BLE y BLE Sense, MCU nRF52840).
  • Framework: arduino (Mbed OS para Nano 33 BLE Sense).
  • Paquete del compilador: toolchain-gccarmnoneeabi@1.90301.0 (GCC 9-2019-q4-major) fijado para reproducibilidad.
  • Librerías de proyecto (pinned):
  • Arduino_LSM9DS1@1.1.0 (IMU de Nano 33 BLE Sense Rev1).
  • AS5600@0.4.1 (librería de Rob Tillaart para encoder magnético).
  • ArduinoBLE@1.3.5 (opcional si luego quieres telemetría BLE; aquí la dejaremos como comentada).

Nota importante sobre IMU: Este caso práctico utiliza la IMU LSM9DS1 (Nano 33 BLE Sense Rev1). Si dispones de un Nano 33 BLE Sense Rev2 (LSM6DSOX), sustituye la librería por Arduino_LSM6DSOX y adapta las llamadas. Más adelante se indica la variante.

Materiales

  • 1 x Arduino Nano 33 BLE Sense (modelo con IMU LSM9DS1).
  • 1 x Driver de motor TB6612FNG (placa breakout doble canal A/B).
  • 1 x Sensor AS5600 (encoder magnético absoluto I2C, 12 bits, dirección 0x36).
  • 2 x Motores DC con caja reductora, 6–12 V, con ruedas (igualados).
  • 1 x Batería LiPo 2S (7.4 V) con interruptor.
  • 1 x Imán diametral para el AS5600 (6–8 mm), montado en eje de rueda o eje intermedio.
  • Cables de puente macho-macho y macho-hembra.
  • Porta-baterías o cinta de fijación.
  • Soporte temporal para pruebas (p. ej., varillas o un marco que impida caídas mientras ajustas el control).
  • 1 x Convertidor buck si necesitas derivar 5 V auxiliares (no requerido para la lógica: el Nano 33 BLE Sense funciona a 3.3 V y se alimenta por USB durante el desarrollo).
  • Elementos de seguridad: gafas, superficie acolchada para evitar daños al robot durante el ajuste.

Preparación y conexión

Consideraciones de alimentación

  • TB6612FNG:
  • VM (potencia motores): 6–12 V desde la batería (recomendado 2S LiPo).
  • VCC (lógica): 3.3 V desde el pin 3V3 del Nano 33 BLE Sense.
  • GND común: une GND del driver, GND del Arduino y GND de la batería.

  • Arduino Nano 33 BLE Sense: durante el desarrollo, aliméntalo via USB. En modo autónomo, puedes alimentar por VIN (5–21 V) si la batería lo permite, o usar un regulador DC/DC a 5 V/USB.

  • AS5600: se alimenta con 3.3 V. Asegúrate de que la placa del AS5600 soporte 3.3 V en VCC y que I2C sea 3.3 V.

Tabla de conexiones

La siguiente tabla asigna pines del Nano 33 BLE Sense a TB6612FNG y AS5600. Las líneas PWM deben ser pines con soporte PWM en el Nano 33 BLE.

Función Componente Pin en TB6612FNG / AS5600 Pin en Nano 33 BLE Sense Notas
Alimentación lógica TB6612FNG VCC 3V3 2.7–5.5 V; usamos 3.3 V del Nano
Tierra TB6612FNG GND GND GND común con Arduino y batería
Alimentación motores TB6612FNG VM Batería (+) 6–12 V (p. ej., 7.4 V LiPo)
Standby TB6612FNG STBY D8 Ponlo en HIGH para habilitar drivers
Motor A sentido 1 TB6612FNG AIN1 D4
Motor A sentido 2 TB6612FNG AIN2 D5
Motor A PWM TB6612FNG PWMA D9 PWM
Motor B sentido 1 TB6612FNG BIN1 D6
Motor B sentido 2 TB6612FNG BIN2 D7
Motor B PWM TB6612FNG PWMB D10 PWM
Motor A TB6612FNG AO1/AO2 Motor izquierdo Consolida sentido luego en software
Motor B TB6612FNG BO1/BO2 Motor derecho
I2C SDA AS5600 SDA SDA (A4) 3.3 V I2C
I2C SCL AS5600 SCL SCL (A5) 3.3 V I2C
Alimentación AS5600 VCC 3V3
Tierra AS5600 GND GND

Notas:
– Comprueba el mapping físico de SDA/SCL en tu placa; en Nano 33 BLE Sense suelen estar serigrafiados como SDA/SCL y corresponden a A4/A5.
– Si inviertes los cables del motor, solo tendrás que corregir “signos” en software (o viceversa).
– Montaje AS5600: el imán debe estar centrado con el chip, a ~2 mm de distancia. Fija mecánicamente para evitar variaciones.

Orientación del IMU

Para este proyecto, asume:
– La placa se monta verticalmente formando el “cuerpo” del rover.
– El eje de pitch (inclinación hacia adelante/atrás) es el eje X del IMU.
– USB mirando hacia la izquierda del rover y el conector de pines hacia arriba.
Si tu montaje difiere, ajusta el cálculo de ángulos (mapear ejes o cambiar signos).

Código completo

A continuación, el firmware en C++ (framework Arduino) que:
– Lee IMU (LSM9DS1) y filtra el ángulo de pitch con un filtro complementario.
– Lee ángulo del AS5600 para estimar velocidad de rueda.
– Ejecuta un control PID de equilibrio con término de velocidad de rueda como “damping” adicional.
– Comanda los motores vía TB6612FNG.

Archivo: src/main.cpp

#include <Arduino.h>
#include <Wire.h>
#include <AS5600.h>            // Rob Tillaart AS5600 library
#include <Arduino_LSM9DS1.h>   // IMU Arduino Nano 33 BLE Sense (Rev1)

// -------------------- Configuración de pines TB6612FNG --------------------
constexpr uint8_t PIN_STBY = 8;
constexpr uint8_t PIN_AIN1 = 4;
constexpr uint8_t PIN_AIN2 = 5;
constexpr uint8_t PIN_PWMA = 9;    // PWM
constexpr uint8_t PIN_BIN1 = 6;
constexpr uint8_t PIN_BIN2 = 7;
constexpr uint8_t PIN_PWMB = 10;   // PWM

// -------------------- Parámetros de control --------------------
static float loop_hz = 500.0f;     // Frecuencia de control (Hz)
static float dt = 1.0f / 500.0f;
static const float alpha = 0.98f;  // Filtro complementario

// PID del ángulo (pitch)
volatile float Kp = 24.0f;
volatile float Ki = 2.0f;
volatile float Kd = 0.35f;

// “Damping” adicional con velocidad de rueda estimada (AS5600)
volatile float Kv = 0.05f;

// Límites de salida
static const int16_t PWM_MAX = 255;    // Máx. 8 bits (PlatformIO mapea a 0-255)
static const float ANGLE_LIMIT_DEG = 20.0f; // Apagar si sobrepasa

// -------------------- Estado --------------------
AS5600 as5600;            // I2C addr 0x36 por defecto
float as5600_prev_angle = 0.0f;  // [rad], absoluto 0..2π
unsigned long as5600_prev_ms = 0;

// Offset y escala IMU
float gyro_bias_x = 0.0f, gyro_bias_y = 0.0f, gyro_bias_z = 0.0f; // [dps]
float pitch_deg = 0.0f;                 // Ángulo estimado [deg]
float pitch_rate_dps = 0.0f;            // Velocidad angular [deg/s]

// PID internals
float err_int = 0.0f;
float err_prev = 0.0f;

// Dirección de motores (puede ajustarse según cableado)
int motor_sign_left = +1;
int motor_sign_right = +1;

// -------------------- Utilidades TB6612FNG --------------------
void tb6612_init() {
  pinMode(PIN_STBY, OUTPUT);
  pinMode(PIN_AIN1, OUTPUT);
  pinMode(PIN_AIN2, OUTPUT);
  pinMode(PIN_PWMA, OUTPUT);
  pinMode(PIN_BIN1, OUTPUT);
  pinMode(PIN_BIN2, OUTPUT);
  pinMode(PIN_PWMB, OUTPUT);
  digitalWrite(PIN_STBY, HIGH); // Habilitar
}

void setMotorRaw(bool chA, int16_t pwm) {
  // pwm en rango [-255, 255], signo = sentido
  uint8_t pinIn1 = chA ? PIN_AIN1 : PIN_BIN1;
  uint8_t pinIn2 = chA ? PIN_AIN2 : PIN_BIN2;
  uint8_t pinPWM = chA ? PIN_PWMA : PIN_PWMB;

  int16_t val = constrain(pwm, -PWM_MAX, PWM_MAX);
  if (val > 0) {
    digitalWrite(pinIn1, HIGH);
    digitalWrite(pinIn2, LOW);
    analogWrite(pinPWM, val);
  } else if (val < 0) {
    digitalWrite(pinIn1, LOW);
    digitalWrite(pinIn2, HIGH);
    analogWrite(pinPWM, -val);
  } else {
    // freno libre: IN1=LOW, IN2=LOW (coast) o freno activo IN1=HIGH, IN2=HIGH
    digitalWrite(pinIn1, LOW);
    digitalWrite(pinIn2, LOW);
    analogWrite(pinPWM, 0);
  }
}

void setMotors(int16_t left, int16_t right) {
  setMotorRaw(true,  motor_sign_left * left);
  setMotorRaw(false, motor_sign_right * right);
}

// -------------------- IMU --------------------
bool imu_init_and_calibrate() {
  if (!IMU.begin()) {
    return false;
  }
  delay(50);

  // Intentar fijar tasa de muestreo razonable si está disponible
  // Nota: Arduino_LSM9DS1 no expone fácilmente ODR desde el wrapper Arduino;
  // trabajamos con lectura en bucle fijo y dt estable.
  // Calibración de offset giroscópico:
  const int N = 2000; // ~4 s @500 Hz
  gyro_bias_x = gyro_bias_y = gyro_bias_z = 0.0f;
  int count = 0;

  while (count < N) {
    float gx, gy, gz, ax, ay, az;
    bool ok = IMU.gyroscopeAvailable() && IMU.accelerationAvailable();
    if (ok) {
      IMU.readGyroscope(gx, gy, gz);        // dps
      IMU.readAcceleration(ax, ay, az);     // g
      gyro_bias_x += gx;
      gyro_bias_y += gy;
      gyro_bias_z += gz;
      count++;
    }
    delayMicroseconds(1000); // ~1 kHz loop en calibración
  }
  gyro_bias_x /= N; gyro_bias_y /= N; gyro_bias_z /= N;

  // Inicializa pitch con acelerómetro (suponiendo montaje vertical)
  float ax, ay, az;
  if (IMU.accelerationAvailable()) {
    IMU.readAcceleration(ax, ay, az);
    // Pitch ≈ atan2(-ax, sqrt(ay^2 + az^2)) en grados (depende de orientación)
    float pitch0 = atan2f(-ax, sqrtf(ay*ay + az*az)) * 180.0f / PI;
    pitch_deg = pitch0;
  }
  return true;
}

// Lee sensores y actualiza pitch
void imu_step() {
  float gx, gy, gz, ax, ay, az;
  if (IMU.gyroscopeAvailable()) {
    IMU.readGyroscope(gx, gy, gz);
    gx -= gyro_bias_x;
    gy -= gyro_bias_y;
    gz -= gyro_bias_z;
  } else {
    gx = gy = gz = 0.0f;
  }
  if (IMU.accelerationAvailable()) {
    IMU.readAcceleration(ax, ay, az);
  } else {
    ax = ay = 0.0f; az = 1.0f;
  }

  // Velocidad de pitch (deg/s): asumir eje de pitch = X
  pitch_rate_dps = gx;

  // Ángulo de pitch desde acelerómetro (grados)
  float pitch_acc_deg = atan2f(-ax, sqrtf(ay*ay + az*az)) * 180.0f / PI;

  // Filtro complementario
  pitch_deg = alpha * (pitch_deg + pitch_rate_dps * dt) + (1.0f - alpha) * pitch_acc_deg;
}

// -------------------- AS5600 --------------------
bool as5600_init() {
  Wire.begin();
  // Librería Rob Tillaart: setup básico
  if (!as5600.begin(AS5600_DEFAULT_ADDRESS)) {
    return false;
  }
  // Modo de potencia normal
  as5600.setPowerMode(AS5600_NORMAL);
  // Guardar ángulo inicial como referencia
  as5600_prev_angle = as5600.getRadians();
  as5600_prev_ms = millis();
  return true;
}

float as5600_wheel_speed_rads() {
  // Diferencia de ángulo con “unwrap” simple
  float angle = as5600.getRadians(); // 0..2π
  float da = angle - as5600_prev_angle;
  // unwrapping
  if (da > PI)  da -= 2.0f * PI;
  if (da < -PI) da += 2.0f * PI;
  unsigned long now = millis();
  float dt_s = (now - as5600_prev_ms) / 1000.0f;
  if (dt_s <= 0) dt_s = 1e-3f;

  float omega = da / dt_s; // rad/s

  as5600_prev_angle = angle;
  as5600_prev_ms = now;
  return omega;
}

// -------------------- Control --------------------
void safety_stop() {
  setMotors(0, 0);
  // Mantener STBY en alto, pero sin PWM
}

int16_t control_step(float pitch_setpoint_deg, float wheel_vel_rads) {
  float err = pitch_setpoint_deg - pitch_deg;
  err_int += err * dt;

  // Derivada sobre la medida (para reducir ruido)
  float err_der = -(pitch_rate_dps);

  // PID del ángulo
  float u = Kp * err + Ki * err_int + Kd * err_der;

  // Damping con velocidad de rueda (reduce oscilaciones)
  u -= Kv * wheel_vel_rads;

  // Anti-windup simple: limitar integral si saturamos
  float u_sat = constrain(u, -PWM_MAX, PWM_MAX);
  if (u != u_sat) {
    // retrocálculo proporcional
    err_int -= 0.1f * (u - u_sat) / max(1.0f, Ki);
  }
  return (int16_t)u_sat;
}

// -------------------- Setup y loop --------------------
void setup() {
  Serial.begin(115200);
  while (!Serial && millis() < 2000) { /* wait up to 2s */ }

  tb6612_init();
  setMotors(0, 0);

  if (!as5600_init()) {
    Serial.println("ERROR: AS5600 no encontrado (I2C 0x36). Revisa SDA/SCL/3V3/GND.");
    // seguimos, pero sin damping por rueda
  } else {
    Serial.println("AS5600 OK");
  }

  if (!imu_init_and_calibrate()) {
    Serial.println("ERROR: IMU no inicializada. Revisa Arduino_LSM9DS1 y alimentación.");
    while (true) { delay(1000); }
  }
  Serial.println("IMU calibrada. Iniciando control...");

  // Configurar la frecuencia de control
  dt = 1.0f / loop_hz;
}

void loop() {
  static unsigned long last_us = micros();
  unsigned long now_us = micros();
  float elapsed = (now_us - last_us) / 1000000.0f;
  if (elapsed < dt) {
    // esperar el siguiente tick
    delayMicroseconds(100);
    return;
  }
  last_us = now_us;

  // Leer sensores
  imu_step();
  float wheel_vel = as5600_prev_ms ? as5600_wheel_speed_rads() : 0.0f;

  // Seguridad por ángulo excesivo
  if (fabs(pitch_deg) > ANGLE_LIMIT_DEG) {
    safety_stop();
    // Indicar estado por serial
    static uint32_t t0 = 0;
    if (millis() - t0 > 200) {
      Serial.println("Fuera de rango. Coloca el rover vertical y reinicia.");
      t0 = millis();
    }
    return;
  }

  // Setpoint de pitch
  float pitch_sp = 0.0f; // vertical

  // Control
  int16_t u = control_step(pitch_sp, wheel_vel);

  // Reparto simétrico a ambos motores
  setMotors(u, u);

  // Telemetría básica
  static uint32_t last_print = 0;
  if (millis() - last_print > 50) { // 20 Hz
    Serial.print("pitch=");
    Serial.print(pitch_deg, 2);
    Serial.print(" dps=");
    Serial.print(pitch_rate_dps, 1);
    Serial.print(" wheel_radps=");
    Serial.print(wheel_vel, 2);
    Serial.print(" u=");
    Serial.println(u);
    last_print = millis();
  }
}

Explicación breve de partes clave

  • Filtro complementario (alpha=0.98): combina giroscopio (integración rápida) y acelerómetro (referencia de gravedad) para estimar pitch robusto y con baja deriva.
  • Calibración de giroscopio: promedia lecturas en reposo para eliminar bias; mejora notablemente el control.
  • Control PID: salida en unidades de PWM. El término derivativo usa la velocidad de pitch medida (sobre la medida) para mitigar ruido. El término Ki integra lentamente para corregir pequeñas desviaciones.
  • Damping por velocidad de rueda (Kv): reduce el “péndulo” aprovechando el AS5600; si no hay AS5600 disponible/funcional, Kv puede ponerse a 0.
  • Safety: si |pitch| > 20°, se desactiva PWM para evitar caídas violentas.
  • setMotors: encapsula la lógica de dirección y PWM para TB6612FNG por canal.

Variante para Nano 33 BLE Sense Rev2 (LSM6DSOX)

  • Sustituye en platformio.ini la librería Arduino_LSM9DS1 por Arduino_LSM6DSOX.
  • Cambia include y lectura de IMU:
  • #include <Arduino_LSM6DSOX.h>
  • Reemplaza IMU por IMU (misma API: IMU.begin(), IMU.gyroscopeAvailable(), IMU.accelerationAvailable(), etc.). La API Arduino es casi equivalente.

Compilación, carga y ejecución

Estructura mínima del proyecto

  • platformio.ini
  • src/main.cpp

Archivo: platformio.ini

[env:nano33ble]
platform = nordicnrf52@10.3.1
board = nano33ble
framework = arduino
platform_packages =
  toolchain-gccarmnoneeabi@1.90301.0
lib_deps =
  arduino-libraries/Arduino_LSM9DS1@1.1.0
  robtillaart/AS5600@0.4.1
;  arduino-libraries/ArduinoBLE@1.3.5
monitor_speed = 115200
monitor_filters = time, default
build_flags =
  -DCORE_DEBUG_LEVEL=0

Instalación de PlatformIO Core (si no lo tienes)

  • Windows/macOS/Linux con Python 3.11.x:
  • pipx recomendado:
    • pipx install platformio
  • o con pip:
    • python -m pip install --user platformio

Verifica la versión:

pio --version

Debe mostrar 6.1.15.

Inicializar proyecto y dependencias

  1. Crea directorio:
    mkdir imu-self-balancing-rover && cd imu-self-balancing-rover
  2. Inicializa proyecto:
    pio project init --board nano33ble
  3. Sustituye platformio.ini por el mostrado arriba.
  4. Copia el código a src/main.cpp.
  5. Instala dependencias explícitamente (opcional; PlatformIO las resuelve en el primer build):
    pio pkg install --library "arduino-libraries/Arduino_LSM9DS1@1.1.0"
    pio pkg install --library "robtillaart/AS5600@0.4.1"

Compilación

pio run

Carga (upload)

  • Conecta el Nano 33 BLE Sense por USB. En Windows aparecerá un puerto COM; en macOS/Linux /dev/ttyACM0 o similar.
  • En Linux, si recibes error de permisos en /dev/ttyACM0:
  • sudo usermod -aG dialout $USER
  • Cierra sesión y vuelve a entrar.
  • Carga el firmware:
    pio run -t upload --upload-port /dev/ttyACM0
    Sustituye el puerto si es necesario (Windows: COM7, p. ej.).

Monitor serie

pio device monitor -b 115200 --eol LF

Verás líneas tipo:

AS5600 OK
IMU calibrada. Iniciando control...
[12:34:56.789] pitch=0.45 dps=0.2 wheel_radps=0.01 u=5

Validación paso a paso

1) Validar IMU en reposo

  • Coloca el rover en un soporte vertical, quieto.
  • Conecta el monitor serie.
  • Observa:
  • “IMU calibrada. Iniciando control…”
  • pitch ≈ 0 ± 1.5° en reposo.
  • dps cercano a 0 (|dps| < 0.5).
  • Si no, revisa calibración (repetir reset con el rover completamente quieto).

2) Validar AS5600

  • Gira lentamente la rueda con el imán.
  • En telemetría, wheel_radps debe ser distinto de 0 y cambiar de signo según sentido.
  • Gira continuamente: se esperan valores típicos entre ±5 rad/s para ruedas lentas, más si giras rápido.

3) Validar drivers de motor (sin control)

  • Temporalmente, comenta la lógica de safety y control y fija un PWM bajo para probar sentidos (por ejemplo, en setup tras tb6612_init()):
  • setMotors(+60, +60); delay(1000); setMotors(-60, -60); delay(1000); setMotors(0,0);
  • Comprueba que ambos motores giran y que con el mismo signo giran hacia adelante. Si no, invierte motor_sign_left/right o intercambia cables.

4) Validación del bucle de control en soporte

  • Habilita el código original.
  • Sostén el rover vertical sobre un soporte que permita pequeñas oscilaciones sin caídas.
  • Al soltar suavemente, debe intentar mantenerse, aplicando correcciones (escucharás el zumbido PWM).
  • Observa telemetría: cuando se inclina hacia adelante (pitch positivo, por ejemplo), la salida u debe ir en el sentido que empuja hacia atrás para recuperar.

5) Ajuste inicial de PID

  • Si oscila poco y cae, aumenta Kp en pasos de +2 hasta que “reaccione” con rapidez pero sin excederse.
  • Si ves oscilación sostenida, aumenta Kd de 0.35 a 0.5–0.8 en pequeños pasos.
  • Ki modera el error estático; sube lentamente (2.0 → 3.0 → 4.0). Si ves “deriva” de salida o overshoot lento, reduce Ki.
  • Kv (damping por rueda): si el AS5600 reporta velocidad confiable, prueba subir Kv a 0.1–0.2 para amortiguar.

6) Validación en suelo

  • Usa una superficie lisa y despejada.
  • Enciende con el rover sujetado en vertical; suelta suavemente.
  • Debe mantenerse unos segundos; afina PID para incrementar tiempo de equilibrio.
  • Métrica: tiempo en equilibrio (>10 s), amplitud de oscilación (<±5°), deriva longitudinal aceptable.

7) Pruebas de seguridad

  • Inclina deliberadamente más de 20°. Debe cortar motores (“Fuera de rango…”).
  • Verifica que al volver a vertical y reiniciar, retoma el control.

Troubleshooting

1) No compila por librerías IMU
– Síntoma: error “Arduino_LSM9DS1.h not found”.
– Causa: librería no instalada o tienes un Nano 33 BLE Sense Rev2.
– Solución:
– Instala la librería: pio pkg install --library "arduino-libraries/Arduino_LSM9DS1@1.1.0".
– Si tu placa es Rev2, usa Arduino_LSM6DSOX y ajusta el include y lib_deps.

2) Error de puerto serie en Linux (permiso denegado)
– Síntoma: “Permission denied: /dev/ttyACM0”.
– Solución:
sudo usermod -aG dialout $USER y reinicia sesión.
– Verifica pertenencia al grupo: groups.

3) Sin lectura de AS5600 (siempre 0)
– Síntoma: “ERROR: AS5600 no encontrado”.
– Causas:
– SDA/SCL intercambiados o sin pull-ups (las placas Nano 33 BLE Sense ya llevan pull-ups).
– VCC del AS5600 a 5 V en una placa que no lo soporta.
– Imán mal centrado o demasiado lejos.
– Soluciones:
– Verifica conexiones según la tabla.
– Alimenta a 3.3 V, comparte GND.
– Ajusta la distancia del imán (1.5–3 mm) y céntralo.
– Prueba un escáner I2C para ver 0x36 presente.

4) Motores no giran
– Síntoma: PWM u ≠ 0 en serie, pero motores parados.
– Causas:
– STBY en LOW; VM sin tensión; GND no común.
– PWM en pines sin soporte o soldaduras flojas.
– Soluciones:
– Comprueba D8 = HIGH (STBY).
– Mide VM con multímetro (6–12 V).
– Verifica que D9 y D10 son PWM y tienen continuidad.

5) Robo “tiembla” y cae
– Síntoma: vibración fuerte y pérdida de equilibrio.
– Causas:
– Kp muy alto, Kd muy bajo, fricción o backlash mecánico.
– Soluciones:
– Reduce Kp, aumenta Kd.
– Incrementa Kv si AS5600 está bien montado.
– Revisa holguras mecánicas y aprieta sujecciones.

6) Deriva constante hacia adelante/atrás
– Síntoma: mantiene equilibrio pero se desplaza.
– Causas:
– Bias residual del giroscopio; offset de pitch no centrado; ruedas con radios distintos.
– Soluciones:
– Repite calibración: enciende sin tocar el robot, sobre un soporte estable.
– Añade corrección de offset de pitch (sumar un pequeño bias al setpoint, p. ej., +0.3°).
– Igualar ruedas y presión.

7) Lecturas IMU ruidosas
– Síntoma: pitch “salta” o telemetría errática.
– Causas:
– Vibraciones de motor, cables cercanos a IMU, dt inestable.
– Soluciones:
– Añade espuma antivibración bajo la placa.
– Asegura dt constante (este código ya fuerza un intervalo).
– Baja loop_hz a 250 Hz y prueba alpha=0.97.

8) Se resetea al acelerar
– Síntoma: reinicios cuando la demanda de motor sube.
– Causas:
– Caída de tensión; ruido EMI; masa mal distribuida.
– Soluciones:
– Añade condensadores en VM (100 µF + 1 µF cerámico).
– Cables de potencia trenzados y separados de señales.
– Usa un buck dedicado para VIN del Arduino si alimentas todo de la misma batería.

Mejoras/variantes

  • Segunda rueda con AS5600: añade otro AS5600 para estimar velocidad diferencial y mejorar control de trayectoria.
  • Telemetría BLE: habilita ArduinoBLE para monitorizar pitch, PWM, tuning Kp/Ki/Kd desde una app móvil.
  • Estimación avanzada de actitud: Madgwick/Mahony para mejorar frente a vibraciones, o un Kalman discreto.
  • Control cascada completo:
  • Lazo interno: pitch (rápido).
  • Lazo externo: velocidad/posición (lento) con encoders (AS5600 x2).
  • Modo “arranque asistido”: un algoritmo que detecte verticalidad, aplica rampas suaves de PWM.
  • Limitación de corriente: mide corriente del motor (sensor ACS o shunt + ADC) y modula PWM para proteger.
  • Modo “seguimiento”: añade un sensor de distancia (ToF o ultrasonidos) y controla desplazamiento manteniendo equilibrio.
  • Ahorro energético: reduce PWM cuando el robot está estable; duerme IMU si queda inclinado y sin intento de recuperación.

Checklist de verificación

  • [ ] Sistema operativo y Python 3.11.x instalados.
  • [ ] PlatformIO Core 6.1.15 verificado con pio –version.
  • [ ] Proyecto creado con board nano33ble y platform = nordicnrf52@10.3.1.
  • [ ] platformio.ini con toolchain-gccarmnoneeabi@1.90301.0 y lib_deps fijados.
  • [ ] Cableado conforme a la tabla (STBY→D8, PWMA→D9, PWMB→D10, etc.), GND común.
  • [ ] AS5600 alimentado a 3.3 V, SDA→A4, SCL→A5, imán centrado.
  • [ ] Compila sin errores: pio run.
  • [ ] Carga correcta: pio run -t upload –upload-port .
  • [ ] Monitor serie operativo a 115200 bps.
  • [ ] IMU calibrada con el robot quieto al encender.
  • [ ] Lecturas de pitch estables en reposo (±1.5°).
  • [ ] Motores probados en ambos sentidos con PWM bajo.
  • [ ] Control mantiene el equilibrio en soporte y luego en suelo.
  • [ ] Ajuste Kp/Ki/Kd/Kv documentado y valores anotados finales.
  • [ ] Prueba de seguridad: corte por |pitch| > 20° funciona.

Apéndice: comandos útiles de PlatformIO

  • Limpiar build:
    pio run -t clean
  • Reconocer puertos serie:
    pio device list
  • Monitor con timestamp y reset a DTR desactivado:
    pio device monitor -b 115200 --eol LF --rts=0 --dtr=0

Notas finales de coherencia hardware/software

  • Este caso práctico está diseñado específicamente para:
  • Arduino Nano 33 BLE Sense (nRF52840, Mbed; usamos IMU LSM9DS1).
  • Driver TB6612FNG para dos motores DC.
  • Encoder magnético AS5600 por I2C para estimar velocidad de una rueda.
  • Toda la toolchain, comandos y librerías están alineados con esa combinación.
  • Si migras a Nano 33 BLE Sense Rev2, ajusta únicamente la librería IMU y mantén el resto igual.

Con esto dispones de un rover autoequilibrado funcional, reproducible con versiones fijadas y preparado para iterar en control, sensorización y robustez mecánica.

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 mínima de Git requerida para el proyecto?




Pregunta 2: ¿Qué sistema operativo NO es compatible según los requisitos?




Pregunta 3: ¿Qué herramienta se utilizará para programar la placa Arduino?




Pregunta 4: ¿Cuál es la versión de PlatformIO Core que se utilizará?




Pregunta 5: ¿Qué modelo de Arduino se menciona en el artículo?




Pregunta 6: ¿Qué librería se debe usar para la IMU si se tiene un Nano 33 BLE Sense Rev2?




Pregunta 7: ¿Cuál es la dirección I2C del sensor AS5600?




Pregunta 8: ¿Qué tipo de batería se requiere para el proyecto?




Pregunta 9: ¿Cuál es la función del driver de motor TB6612FNG en el proyecto?




Pregunta 10: ¿Qué tipo de motores se utilizan 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: epaper-air-quality-logger con Arduino Nano

Caso práctico: epaper-air-quality-logger con Arduino Nano — hero

Objetivo y caso de uso

Qué construirás: Un registrador de calidad del aire de bajo consumo utilizando Arduino Nano 33 IoT, un display e-Paper de Waveshare y un sensor BME680.

Para qué sirve

  • Monitoreo continuo de la calidad del aire en entornos urbanos.
  • Visualización de datos de calidad del aire en tiempo real en un display e-Paper.
  • Registro de datos históricos para análisis de tendencias de contaminación.
  • Integración con sistemas de alerta mediante MQTT para notificaciones en tiempo real.

Resultado esperado

  • Datos de calidad del aire actualizados cada 10 segundos con latencia mínima.
  • Visualización de niveles de CO2, temperatura y humedad en el display e-Paper.
  • Envío de datos a un servidor MQTT con una frecuencia de 1 paquete cada 30 segundos.
  • Capacidad de operar con una duración de batería de más de 6 meses en modo de bajo consumo.

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

Arquitectura/flujo: Arduino Nano 33 IoT -> BME680 -> e-Paper -> MQTT.

Nivel: Avanzado

Prerrequisitos

Sistema operativo y herramientas

  • Sistemas operativos soportados:
  • Linux: Ubuntu 22.04 LTS x86_64
  • macOS: 13 Ventura o 14 Sonoma (Intel/Apple Silicon)
  • Windows 11 Pro/Enterprise (x64)

  • Toolchain exacta:

  • Arduino CLI 0.35.3
  • Core de placa: Arduino SAMD Boards 1.8.14
  • Bibliotecas Arduino (vía Library Manager):
  • GxEPD2 1.5.9
  • Adafruit GFX Library 1.11.9
  • Adafruit BME680 Library 2.0.3
  • Adafruit Unified Sensor 1.1.14
  • Adafruit BusIO 1.14.1
  • ArduinoLowPower 1.2.2
  • FlashStorage_SAMD 1.3.2

Notas:
– El Arduino Nano 33 IoT usa USB nativo. No requiere drivers en macOS ni Linux. En Windows 10/11 se instala como “USB Serial Device (COMx)”; no se necesitan drivers externos.
– Se usa Arduino CLI (no el IDE GUI) para todo el flujo: instalación del core, dependencias, compilación y subida.

Verificación del hardware y entorno

  • Confirmar el puerto serie:
  • Linux/macOS: típico /dev/ttyACM0 o /dev/tty.usbmodemXXXX
  • Windows: COM3, COM4, etc. (ver en “Administrador de dispositivos”)
  • Conexión a Internet para descargar cores y bibliotecas.
  • Cable micro‑USB de datos (no solo carga).

Materiales

  • 1x Arduino Nano 33 IoT (modelo exacto)
  • 1x Módulo Waveshare 2.9″ e‑Paper monocromo con controlador SSD1680 (modelo exacto; versión b/w V2 con SSD1680)
  • 1x Sensor ambiental BME680 (I2C)
  • Cables Dupont macho‑hembra
  • Protoboard (opcional, para ordenar cableado)

Observación sobre alimentación y niveles:
– El Nano 33 IoT funciona a 3.3 V lógicos, compatibles con la pantalla e‑Paper SSD1680 y con el BME680. No usar 5 V en señales.

Preparación y conexión

Disposición de pines y cableado

Para la pantalla Waveshare 2.9″ e‑Paper (SSD1680) se usará SPI. El módulo típico expone: VCC, GND, DIN (MOSI), CLK (SCK), CS, DC, RST, BUSY. No se usa MISO en el panel b/w.

Para el BME680 se usará I2C con alimentación a 3.3 V. La mayoría de breakout boards vienen con regulador y pull‑ups integradas; verificar el serigrafiado de su módulo.

Tabla de conexiones (Nano 33 IoT ↔ periféricos):

Función Nano 33 IoT e‑Paper (SSD1680) BME680 (I2C)
Alimentación 3V3 VCC VIN/3V3
Tierra GND GND GND
SPI MOSI D11 (MOSI) DIN
SPI SCK D13 (SCK) CLK
SPI CS panel D10 CS
SPI DC (data/command) D9 DC
Reset panel D8 RST
Busy panel D7 BUSY
I2C SDA SDA SDA
I2C SCL SCL SCL

Indicaciones:
– Conecte el BME680 a los pines etiquetados “SDA” y “SCL” del Nano 33 IoT (no confundir con A4/A5 propios de placas AVR).
– La pantalla e‑Paper debe alimentarse con 3.3 V. No usar 5 V en VCC ni en señales.
– Mantenga cortos los cables SPI de la e‑Paper para minimizar ruido y artefactos de actualización.

Preparación del entorno de compilación

1) Descargar e instalar Arduino CLI 0.35.3:
– Linux:
– curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh
– Verifique versión: arduino-cli version (debe mostrar 0.35.3)
– macOS:
– brew update && brew install arduino-cli
– Verifique: arduino-cli version
– Windows:
– Descargue el binario .exe de Arduino CLI 0.35.3 y añádalo al PATH.
– Verifique: arduino-cli version

2) Instalar el core de la placa SAMD (exacto):
– arduino-cli core update-index
– arduino-cli core install arduino:samd@1.8.14

3) Instalar bibliotecas exactas:
– arduino-cli lib install «GxEPD2@1.5.9»
– arduino-cli lib install «Adafruit GFX Library@1.11.9»
– arduino-cli lib install «Adafruit BME680 Library@2.0.3»
– arduino-cli lib install «Adafruit Unified Sensor@1.1.14»
– arduino-cli lib install «Adafruit BusIO@1.14.1»
– arduino-cli lib install «ArduinoLowPower@1.2.2»
– arduino-cli lib install «FlashStorage_SAMD@1.3.2»

4) Verificar que el FQBN esté disponible:
– arduino-cli board listall | grep -i «Nano 33 IoT»
– Debe listar: arduino:samd:nano_33_iot

Código completo

A continuación se entrega el sketch “epaper-air-quality-logger.ino”. El objetivo:
– Leer cada minuto el BME680 (T, H, P, gas).
– Calibrar un valor de baseline de gas durante los primeros 5 minutos.
– Calcular un índice simple de calidad de aire (IAQ%) basado en gas y humedad.
– Mostrar en e‑Paper: valores actuales y una minigráfica histórica.
– Registrar datos en memoria flash del SAMD21 con un buffer circular persistente.
– Permitir volcado de registros por Serial en CSV cuando se envía el comando “DUMP”.

Notas importantes para el display:
– Para Waveshare 2.9″ b/w V2 (SSD1680) usar la clase GxEPD2_290_T5 (128×296).
– Configuramos pines CS/DC/RST/BUSY según la tabla de conexión.

// epaper-air-quality-logger.ino
// Dispositivo: Arduino Nano 33 IoT + Waveshare 2.9" e-Paper (SSD1680) + BME680
// Toolchain: Arduino CLI 0.35.3, Core SAMD 1.8.14
// Bibliotecas: ver versiones en la sección de prerrequisitos

#include <Arduino.h>
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME680.h>
#include <Adafruit_GFX.h>
#include <GxEPD2_BW.h>
#include <Fonts/FreeSans9pt7b.h>
#include <ArduinoLowPower.h>
#include <FlashStorage_SAMD.h>

// Pines e-Paper
#define EPD_CS   10
#define EPD_DC    9
#define EPD_RST   8
#define EPD_BUSY  7

// Instancia display: Waveshare 2.9" V2 (SSD1680) -> GxEPD2_290_T5 128x296
// Nota: GxEPD2 usa Adafruit_GFX como backend gráfico
#include <GxEPD2_3C.h>   // no se usará color; se incluye por compatibilidad
#include <GxEPD2_290.h>  // headers base
// Para SSD1680 (GDEW029T5 o equivalente):
class GxEPD2_290_T5; // forward decl. (incluido en librería)
GxEPD2_BW<GxEPD2_290_T5, GxEPD2_290_T5::HEIGHT> display(EPD_CS, EPD_DC, EPD_RST, EPD_BUSY);

// BME680 por I2C
Adafruit_BME680 bme(&Wire);

// Configuración de logging y almacenamiento persistente
#define LOG_CAPACITY 288  // 288 muestras ~ 24 h si muestreamos cada 5 min (ajustable)
#define SAMPLE_PERIOD_MS (60UL * 1000UL)  // 60 s
#define CALIBRATION_TIME_MS (5UL * 60UL * 1000UL)  // 5 min de baseline

struct Measurement {
  uint32_t t_ms;   // tiempo desde arranque (ms)
  float temp;      // °C
  float hum;       // %RH
  float pres;      // hPa
  float gas;       // ohmios
  float iaq;       // 0..100 índice simple (no BSEC)
};

struct LogStore {
  uint32_t magic;     // firma para validar
  uint16_t head;      // próxima posición de escritura
  uint16_t count;     // nº de muestras válidas (<= LOG_CAPACITY)
  float gas_baseline; // baseline persistente del gas
  Measurement data[LOG_CAPACITY];
};

FlashStorage(log_store, LogStore);

static const uint32_t MAGIC = 0xA1Q1E0FF;

LogStore store;
uint32_t last_sample_ms = 0;
bool baseline_locked = false;

// Utilidades de mapeo/clamp
static inline float clampf(float v, float lo, float hi) {
  if (v < lo) return lo;
  if (v > hi) return hi;
  return v;
}

// Cálculo de IAQ simple (0-100) no equivalente a BSEC
float compute_iaq_percent(float gas, float gas_baseline, float humidity) {
  if (gas_baseline <= 0) return 0;
  // Puntuación de gas: mayor resistencia -> mejor aire (menos VOC)
  float gas_score = (gas / gas_baseline) * 100.0f;
  gas_score = clampf(gas_score, 0.0f, 100.0f);

  // Humedad ideal ~ 40% (penaliza desviaciones)
  float hum_score = 100.0f - fabsf(humidity - 40.0f) * 2.5f; // ±40% -> 0
  hum_score = clampf(hum_score, 0.0f, 100.0f);

  // Fusión ponderada (más peso a gas)
  float iaq = 0.75f * gas_score + 0.25f * hum_score;
  return clampf(iaq, 0.0f, 100.0f);
}

void drawHeader() {
  display.setFont(&FreeSans9pt7b);
  display.setTextColor(GxEPD_BLACK);
  display.setCursor(4, 16);
  display.print("epaper-air-quality-logger");
  display.setFont(); // volver a font por defecto para cuerpo
}

void drawReadings(const Measurement& m) {
  char line[48];

  snprintf(line, sizeof(line), "T: %.2f C  H: %.1f %%", m.temp, m.hum);
  display.setCursor(4, 36);
  display.print(line);

  snprintf(line, sizeof(line), "P: %.1f hPa  Gas: %.0f ohm", m.pres, m.gas);
  display.setCursor(4, 52);
  display.print(line);

  snprintf(line, sizeof(line), "IAQ*: %.1f /100  (baseline: %.0f)", m.iaq, store.gas_baseline);
  display.setCursor(4, 68);
  display.print(line);

  display.setCursor(4, 84);
  display.print("*Indice simplificado (no BSEC)");
}

void drawSparkline() {
  // Área de la minigráfica: x=4..292, y=90..120 (altura ~30 px)
  const int x0 = 4, y0 = 120, w = 288, h = 28;
  display.drawRect(x0-1, y0-h-1, w+2, h+2, GxEPD_BLACK);

  if (store.count == 0) {
    display.setCursor(x0, y0 - 8);
    display.print("Sin datos suficientes para graficar.");
    return;
  }

  // Graficar IAQ en 0..100 mapeado a altura
  int points = min((int)store.count, w);
  // Recorremos el log desde el más reciente hacia atrás
  int idx = (int)store.head - 1;
  if (idx < 0) idx = LOG_CAPACITY - 1;

  for (int i = 0; i < points; i++) {
    const Measurement& m = store.data[idx];
    float iaq = clampf(m.iaq, 0.0f, 100.0f);
    int y = y0 - (int)((iaq / 100.0f) * (float)h);
    int x = x0 + (w - 1 - i);
    display.drawPixel(x, y, GxEPD_BLACK);
    if (--idx < 0) idx = LOG_CAPACITY - 1;
  }
}

void epaperFullRefresh(const Measurement& last) {
  display.setFullWindow();
  display.firstPage();
  do {
    display.fillScreen(GxEPD_WHITE);
    drawHeader();
    drawReadings(last);
    drawSparkline();
  } while (display.nextPage());
}

void initStorage() {
  store = log_store.read();
  if (store.magic != MAGIC) {
    memset(&store, 0, sizeof(store));
    store.magic = MAGIC;
    store.head = 0;
    store.count = 0;
    store.gas_baseline = 0.0f;
    log_store.write(store);
  }
}

void appendMeasurement(const Measurement& m) {
  store.data[store.head] = m;
  store.head = (store.head + 1) % LOG_CAPACITY;
  if (store.count < LOG_CAPACITY) store.count++;
  // Escribimos bloque completo (flash) de forma conservadora (1/min)
  log_store.write(store);
}

bool setupBME680() {
  if (!bme.begin(0x76)) {         // la mayoría de BME680 usan 0x76
    if (!bme.begin(0x77)) {       // fallback si el jumper de su módulo selecciona 0x77
      return false;
    }
  }
  bme.setTemperatureOversampling(BME680_OS_8X);
  bme.setHumidityOversampling(BME680_OS_2X);
  bme.setPressureOversampling(BME680_OS_4X);
  bme.setIIRFilterSize(BME680_FILTER_SIZE_3);
  bme.setGasHeater(320, 150); // 320 C durante 150 ms
  return true;
}

bool readBME680(Measurement& m) {
  if (!bme.performReading()) return false;
  m.t_ms = millis();
  m.temp = bme.temperature;          // °C
  m.hum  = bme.humidity;             // %RH
  m.pres = bme.pressure / 100.0f;    // Pa -> hPa
  m.gas  = bme.gas_resistance;       // ohmios
  // Base line: primeras muestras (5 min) para calibrar gas
  if (!baseline_locked && m.t_ms < CALIBRATION_TIME_MS) {
    // Promedio incremental simple
    if (store.gas_baseline <= 0.0f) {
      store.gas_baseline = m.gas;
    } else {
      store.gas_baseline = (store.gas_baseline * 0.99f) + (m.gas * 0.01f);
    }
  } else if (!baseline_locked) {
    baseline_locked = true;
    // Persistir baseline tras calibración
    log_store.write(store);
  }

  float base = (store.gas_baseline > 0.0f) ? store.gas_baseline : m.gas;
  m.iaq = compute_iaq_percent(m.gas, base, m.hum);
  return true;
}

void dumpCSV() {
  Serial.println(F("#epaper-air-quality-logger CSV"));
  Serial.println(F("#t_ms,temp_c,hum_pct,pres_hpa,gas_ohm,iaq_pct"));
  int idx = store.head - store.count;
  if (idx < 0) idx += LOG_CAPACITY;
  for (int i = 0; i < store.count; i++) {
    const Measurement& m = store.data[idx];
    Serial.print(m.t_ms); Serial.print(',');
    Serial.print(m.temp, 2); Serial.print(',');
    Serial.print(m.hum, 1); Serial.print(',');
    Serial.print(m.pres, 1); Serial.print(',');
    Serial.print(m.gas, 0); Serial.print(',');
    Serial.println(m.iaq, 1);
    if (++idx >= LOG_CAPACITY) idx = 0;
  }
  Serial.println(F("#END"));
}

void setup() {
  Serial.begin(115200);
  while (!Serial && millis() < 3000) { } // breve ventana para consola

  initStorage();

  Wire.begin();
  if (!setupBME680()) {
    Serial.println(F("Error: BME680 no encontrado en 0x76/0x77"));
    // Seguimos para mostrar error en pantalla
  }

  // Inicialización del e-Paper
  display.init(115200); // SPI a 115200kHz internamente optimiza; se usa para debug
  display.setRotation(1); // apaisado (ancho 296, alto 128)
  // Pantalla inicial
  display.setFullWindow();
  display.firstPage();
  do {
    display.fillScreen(GxEPD_WHITE);
    drawHeader();
    display.setCursor(4, 40);
    display.print("Inicializando sensores...");
  } while (display.nextPage());

  last_sample_ms = 0;
}

void loop() {
  // Comandos por Serial
  if (Serial.available()) {
    int c = Serial.read();
    if (c == 'D' || c == 'd') {
      dumpCSV();
    }
  }

  uint32_t now = millis();
  if (now - last_sample_ms >= SAMPLE_PERIOD_MS || last_sample_ms == 0) {
    last_sample_ms = now;

    Measurement m{};
    if (readBME680(m)) {
      appendMeasurement(m);
      epaperFullRefresh(m);
      Serial.print(F("OK: T=")); Serial.print(m.temp, 2);
      Serial.print(F("C H=")); Serial.print(m.hum, 1);
      Serial.print(F("% P=")); Serial.print(m.pres, 1);
      Serial.print(F("hPa GAS=")); Serial.print(m.gas, 0);
      Serial.print(F("ohm IAQ=")); Serial.print(m.iaq, 1);
      Serial.println(F("%"));
    } else {
      // Reporte de error y mostrar en e-Paper
      Serial.println(F("Error: performReading() BME680"));
      display.setFullWindow();
      display.firstPage();
      do {
        display.fillScreen(GxEPD_WHITE);
        drawHeader();
        display.setCursor(4, 48);
        display.print("Error lectura BME680");
      } while (display.nextPage());
    }
  }

  // Bajo consumo entre muestras
  LowPower.sleep(1000); // Sleep ligero 1s, repite hasta llegar al minuto
}

Explicación breve de partes clave:
– Baseline del gas: se promedia durante 5 minutos al arranque para normalizar la resistencia del sensor (que varía entre unidades y con el entorno). Luego se “congela” y se persiste.
– IAQ simplificado: no usa BSEC (Bosch), pero ofrece una métrica cualitativa de 0 a 100 que combina gas y humedad.
– e‑Paper: se usa un refresco completo por ciclo para simplificar. En mejoras proponemos pasar a parciales.
– Persistencia: usamos FlashStorage_SAMD para almacenar un buffer circular. Es un ejemplo didáctico: escribir flash con frecuencia conlleva desgaste; más abajo damos recomendaciones para mitigar.

Compilación, flasheo y ejecución

Se asume que el sketch está en un directorio llamado “epaper-air-quality-logger”.

Estructura sugerida:
– epaper-air-quality-logger/
– epaper-air-quality-logger.ino

Pasos:

1) Preparar el core y libs (si no lo hizo antes):

arduino-cli core update-index
arduino-cli core install arduino:samd@1.8.14
arduino-cli lib install "GxEPD2@1.5.9"
arduino-cli lib install "Adafruit GFX Library@1.11.9"
arduino-cli lib install "Adafruit BME680 Library@2.0.3"
arduino-cli lib install "Adafruit Unified Sensor@1.1.14"
arduino-cli lib install "Adafruit BusIO@1.14.1"
arduino-cli lib install "ArduinoLowPower@1.2.2"
arduino-cli lib install "FlashStorage_SAMD@1.3.2"

2) Compilar para Nano 33 IoT:

arduino-cli compile --fqbn arduino:samd:nano_33_iot epaper-air-quality-logger

3) Identificar el puerto (ejemplos):
– Linux: ls /dev/ttyACM
– macOS: ls /dev/tty.usbmodem

– Windows: mode | findstr COM (o ver en el Administrador de dispositivos)

4) Subir el binario:
– Linux/macOS:

arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:samd:nano_33_iot epaper-air-quality-logger
  • Windows (ajuste COMx):
arduino-cli upload -p COM4 --fqbn arduino:samd:nano_33_iot epaper-air-quality-logger

5) Abrir monitor serie a 115200 baudios:

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

En Windows:

arduino-cli monitor -p COM4 -c baudrate=115200

Validación paso a paso

1) Validación de arranque:
– La pantalla e‑Paper debe mostrar “epaper-air-quality-logger” y el texto “Inicializando sensores…” durante el primer ciclo.
– En el monitor serie, verá un mensaje de estado o un error del BME680 si no está detectado.

2) Detección del BME680:
– Si todo va bien, al cabo de ~1 minuto, se imprimirá por serie una línea con T, H, P, GAS e IAQ.
– La pantalla hará un refresco completo mostrando:
– Temperatura, humedad, presión
– Resistencia de gas
– IAQ (%) y el valor de baseline
– Un recuadro en la parte inferior para la minigráfica (al principio con pocos puntos)

3) Calibración de baseline:
– Durante los primeros 5 minutos, el baseline de gas se ajusta (valor fluctúa hacia un promedio).
– El campo “baseline” en la pantalla se estabilizará.
– baseline_locked pasa a true internamente y el valor queda persistido.

4) Registro en flash:
– Tras varios minutos, envíe “d” o “D” por el monitor serie.
– Debe aparecer un CSV con encabezado y filas: t_ms,temp_c,hum_pct,pres_hpa,gas_ohm,iaq_pct.
– Reinicie la placa (botón reset) y vuelva a pedir “DUMP”: la data debe persistir (no se pierde tras reset).

5) Minigráfica:
– A partir de ~10 muestras, la línea en el recuadro inferior debe mostrar una tendencia de IAQ (0..100) desplazándose hacia la izquierda con el tiempo.

6) Comprobación visual y numérica:
– Compare temperatura y humedad con otro termohigrómetro para validar orden de magnitud.
– Alterar el ambiente:
– Exponer brevemente a alcohol isopropílico o aliento cerca del sensor (sin tocar): la resistencia de gas bajará y el IAQ tenderá a disminuir (peor “calidad”).
– Volver a aire limpio: el IAQ se recupera gradualmente.

Troubleshooting

1) La pantalla e‑Paper no muestra nada / se queda en blanco
– Causas probables:
– Pines mal conectados (DC/CS/RST/BUSY invertidos).
– Falta de 3.3 V o masa común.
– Clase de panel incorrecta en GxEPD2.
– Solución:
– Verifique la tabla de pines. Asegure EPD_CS=10, EPD_DC=9, EPD_RST=8, EPD_BUSY=7.
– Confirme que su Waveshare es 2.9″ b/w V2 (SSD1680). La clase GxEPD2_290_T5 es la adecuada para SSD1680.
– Pruebe display.setRotation(0/1/2/3) por si el mapeo afecta coordenadas visibles.

2) BME680 no detectado en 0x76/0x77
– Causas:
– Cableado SDA/SCL invertido o en pines incorrectos.
– Alimentación a 5 V en lugar de 3.3 V o GND suelta.
– Dirección I2C configurada por puente a otra distinta.
– Solución:
– Conectar a los pines etiquetados SDA/SCL del Nano 33 IoT (no A4/A5).
– Usar un escáner I2C para confirmar dirección.
– Revisar soldaduras o jumpers en el módulo BME680.

3) Ghosting o artefactos en e‑Paper
– Causas:
– Actualizaciones muy frecuentes sin refresco completo.
– Cables SPI largos o ruidosos.
– Solución:
– Mantener el refresco completo cada cierto número de ciclos.
– Reducir longitud de cables y retorcer MOSI/SCK con GND cercano para minimizar ruido.

4) Subida falla: “No device found on…”
– Causas:
– Puerto incorrecto.
– El bootloader solo está activo unos segundos tras reset.
– Solución:
– Identificar el puerto correcto con arduino-cli board list.
– Pulsar reset doblemente para entrar en “modo bootloader” y reintentar upload.

5) Mensajes “Error lectura BME680”
– Causas:
– Tiempos de calentamiento del gas no satisfechos.
– Interferencias I2C o alimentación inestable.
– Solución:
– Verificar alimentación 3.3 V estable.
– Aumentar delay entre lecturas o revisar setGasHeater(320, 150).

6) Desgaste de flash evidente / errores de escritura
– Causas:
– Frecuencia de escritura muy alta.
– Solución:
– Incrementar intervalo de muestreo (p. ej., 5 minutos).
– Escribir en flash cada N muestras en lugar de cada vez (buffer RAM + commit).

7) e‑Paper parpadea demasiado
– Causa:
– Refresco completo en cada ciclo.
– Solución:
– Migrar a actualizaciones parciales para datos pequeños (ver mejoras).
– Usar full refresh cada X ciclos para “limpiar”.

8) IAQ no parece realista
– Causa:
– El IAQ simplificado no es equivalente a BSEC.
– Solución:
– Aplique BSEC2 de Bosch para IAQ calibrado y métricas como eCO2/VOC Index (ver mejoras).

Mejoras y variantes

  • IAQ profesional con BSEC2:
  • Sustituir el cálculo simple por Bosch BSEC2 para obtener IAQ, eCO2, bVOC con calibración robusta.
  • Asegurarse de la compatibilidad de BSEC2 con SAMD21 y uso de licencias.

  • Reducción de desgaste de flash:

  • Implementar un buffer en RAM para N muestras (p. ej., 12) y escribir en flash en bloques.
  • Reducir el muestreo a cada 5 minutos y LOG_CAPACITY = 288 para ~24 h.

  • Actualización parcial de e‑Paper:

  • Usar “setPartialWindow” y dibujar solo las áreas cambiadas (número IAQ/última barra del sparkline).
  • Hacer un full-refresh cada 10 parciales para evitar ghosting.

  • Exportación de datos:

  • Implementar un comando “DUMPJSON” con ArduinoJson para exportar en JSON.
  • Guardar en archivo CSV en microSD (si se añade un módulo microSD por SPI con su propio CS).

  • Integración IoT:

  • Publicar mediciones por WiFi (Nano 33 IoT) a MQTT/InfluxDB y mantener la e‑Paper como tablero local.
  • Sincronizar hora por NTP y almacenar timestamps UNIX en el log.

  • Energía:

  • Usar “LowPower.deepSleep” con alarma RTC para ciclos de muestreo largos.
  • Apagar periféricos entre muestras (p. ej., desalimentar BME680 con transistor p‑MOS si el diseño lo permite).

  • Visual:

  • Cambiar la tipografía por fuentes GFX más grandes/bold para legibilidad.
  • Añadir iconos según rangos de IAQ con bitmap monocromo.

Checklist de verificación

  • [ ] He instalado Arduino CLI 0.35.3 y el core arduino:samd@1.8.14.
  • [ ] He instalado las bibliotecas exactas: GxEPD2 1.5.9, Adafruit GFX 1.11.9, Adafruit BME680 2.0.3, Adafruit Unified Sensor 1.1.14, Adafruit BusIO 1.14.1, ArduinoLowPower 1.2.2, FlashStorage_SAMD 1.3.2.
  • [ ] He cableado correctamente e‑Paper: CS=10, DC=9, RST=8, BUSY=7; MOSI=D11, SCK=D13; 3V3 y GND comunes.
  • [ ] He cableado correctamente BME680 por I2C: SDA y SCL a los pines SDA/SCL del Nano 33 IoT; 3V3 y GND.
  • [ ] El sketch compila con: arduino-cli compile –fqbn arduino:samd:nano_33_iot epaper-air-quality-logger.
  • [ ] El sketch sube con: arduino-cli upload -p –fqbn arduino:samd:nano_33_iot epaper-air-quality-logger.
  • [ ] Veo en la e‑Paper el título y, tras ~1 min, los valores de T/H/P/GAS/IAQ y la minigráfica.
  • [ ] Tras 5 min, el baseline de gas se estabiliza y el IAQ varía con el ambiente.
  • [ ] Al enviar “D” por el monitor serie, recibo el CSV con registros.
  • [ ] Tras un reset, el log sigue presente (persistencia OK).

Apéndice: comandos de referencia compactos

  • Instalar core SAMD y libs:
arduino-cli core update-index
arduino-cli core install arduino:samd@1.8.14
arduino-cli lib install "GxEPD2@1.5.9" "Adafruit GFX Library@1.11.9" "Adafruit BME680 Library@2.0.3" "Adafruit Unified Sensor@1.1.14" "Adafruit BusIO@1.14.1" "ArduinoLowPower@1.2.2" "FlashStorage_SAMD@1.3.2"
  • Compilar y subir:
arduino-cli compile --fqbn arduino:samd:nano_33_iot epaper-air-quality-logger
arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:samd:nano_33_iot epaper-air-quality-logger
arduino-cli monitor -p /dev/ttyACM0 -c baudrate=115200

Nota final

Este caso práctico está específicamente enfocado al modelo “Arduino Nano 33 IoT + Waveshare 2.9in e‑Paper (SSD1680) + BME680” con la toolchain y versiones indicadas. Todo el cableado, el código y los comandos han sido diseñados para esta combinación concreta a fin de lograr un “epaper-air-quality-logger” reproducible y validable.

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 soportado para el Arduino Nano 33 IoT?




Pregunta 2: ¿Qué herramienta se utiliza para la instalación y subida en lugar del IDE GUI?




Pregunta 3: ¿Cuál es la versión de la biblioteca GxEPD2 mencionada en el artículo?




Pregunta 4: ¿Qué tipo de cable se necesita para la conexión del Arduino Nano 33 IoT?




Pregunta 5: ¿Qué puerto serie es típico en Linux para el Arduino Nano 33 IoT?




Pregunta 6: ¿Qué voltaje lógico utiliza el Arduino Nano 33 IoT?




Pregunta 7: ¿Cuál es el modelo exacto del módulo e-Paper mencionado?




Pregunta 8: ¿Qué tipo de sensor es el BME680?




Pregunta 9: ¿Qué versión de Arduino SAMD Boards se menciona en el artículo?




Pregunta 10: ¿Qué es necesario confirmar antes de comenzar la instalació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: