Arquitectura del sistema
El sistema se compone de tres capas: el hardware de adquisición (Arduino UNO con sus periféricos), la capa de comunicación (protocolo serial sobre USB), y la interfaz HMI (aplicación web en el navegador). Esta arquitectura es el punto de partida para los trabajos prácticos de la cátedra.
HMI ──→ Comando texto ──→ USB CDC ──→ UART RX ──→ Parser ──→ digitalWrite / PWM
Arduino UNO
ATmega328P a 16 MHz. ADC de 10 bits, UART a 9600–115200 bps, pines PWM y GPIO.
USB Serial
El chip CH340/ATmega16U2 convierte UART a USB CDC. No requiere driver adicional en Linux/macOS.
Web Serial API
API nativa de Chrome/Edge que permite acceso al puerto COM desde JavaScript sin instalación.
HMI Web
HTML + CSS + Canvas/SVG para visualización. Sin frameworks externos requeridos.
Requisitos y herramientas
| Componente | Descripción | Necesario |
|---|---|---|
| Arduino UNO R3 | Microcontrolador ATmega328P con USB integrado | ✅ Sí |
| Arduino IDE | v2.x recomendado — arduino.cc/en/software | ✅ Sí |
| Google Chrome / Edge | 89+ para Web Serial API | ✅ Sí |
| Editor de código | VS Code, Sublime Text u otro | Recomendado |
| Cable USB-A a USB-B | Comunicación y alimentación del Arduino | ✅ Sí |
| Multímetro | Para verificar señales de entrada | Recomendado |
Instalación rápida
- Instalar Arduino IDE desde
arduino.cc/en/softwarey verificar que reconoce el Arduino UNO al conectarlo. - En el IDE, ir a Herramientas → Puerto y seleccionar el puerto COM asignado (ej:
COM3en Windows,/dev/ttyACM0en Linux). - Crear un archivo HTML local (o servidor HTTP simple) para la interfaz HMI. No se requieren dependencias npm.
- Abrir el archivo HTML en Chrome. Al conectar el Arduino, el navegador pedirá permiso para acceder al puerto serial.
Protocolo de comunicación serial
Se utiliza un protocolo de texto simple, legible y fácil de depurar. Cada mensaje es una línea terminada en \n. El diseño prioriza la simplicidad sobre la eficiencia, lo que facilita el aprendizaje y la verificación de señales — criterio clave en el contexto de la cátedra.
Formato de trama
CLAVE:VALOR\n — ejemplo: V0:512\n
CMD:PARAM\n — ejemplo: LED:1\n
DATA:512,301,89\n — separador coma
{"V0":512,"V1":301,"T":25.3}\n
Tabla de comandos estándar
| Comando (HMI→ARD) | Descripción | Ejemplo |
|---|---|---|
START | Inicia envío continuo de datos | START\n |
STOP | Pausa el envío de datos | STOP\n |
RATE:N | Establece intervalo de muestreo en ms | RATE:100\n |
LED:0|1 | Controla LED de salida | LED:1\n |
PWM:N | Ajusta duty cycle PWM (0–255) | PWM:128\n |
MODE:N | Cambia modo de operación del instrumento | MODE:2\n |
PIN?:N | Solicita lectura puntual de pin analógico | PIN?:A0\n |
| Dato (ARD→HMI) | Descripción | Ejemplo |
|---|---|---|
V0:N | Valor ADC canal 0 (0–1023) | V0:512\n |
V1:N | Valor ADC canal 1 | V1:301\n |
TEMP:N.N | Temperatura con decimal | TEMP:25.4\n |
OK | Confirmación de comando recibido | OK\n |
ERR:MSG | Error con descripción | ERR:CMD_UNKNOWN\n |
HELLO:v1.0 | Identificación al conectar | HELLO:v1.0\n |
Firmware Arduino (C++)
La estructura del sketch sigue un patrón no bloqueante basado en temporización con millis(), permitiendo leer sensores, enviar datos y responder comandos de forma concurrente sin usar delay(). Este enfoque es fundamental en instrumentación ya que garantiza la integridad temporal de las mediciones.
Estructura principal — setup() y loop()
// ============================================= // Instrumento Virtual — Firmware base // Arduino UNO + HMI Web via USB Serial // ============================================= const int BAUD_RATE = 115200; const int SAMPLE_PIN = A0; // Pin analógico de entrada const int LED_PIN = 13; // LED integrado unsigned long lastSample = 0; unsigned long sampleRate = 100; // ms entre muestras bool streaming = false; String inputBuffer = ""; // Buffer para comandos entrantes void setup() { Serial.begin(BAUD_RATE); pinMode(LED_PIN, OUTPUT); // Esperar a que el puerto serie esté listo while (!Serial) { delay(10); } // Mensaje de identificación al conectar Serial.println("HELLO:v1.0"); } void loop() { readCommands(); // Procesar comandos entrantes sendSamples(); // Enviar datos si streaming activo }
Envío de datos de sensores
void sendSamples() { if (!streaming) return; unsigned long now = millis(); if (now - lastSample < sampleRate) return; lastSample = now; // Leer canal analógico (0 a 1023) int raw = analogRead(SAMPLE_PIN); // Convertir a tensión (0.0 – 5.0 V) float voltage = raw * (5.0 / 1023.0); // Enviar en formato CLAVE:VALOR Serial.print("V0:"); Serial.println(raw); Serial.print("VV:"); Serial.println(voltage, 3); // Para enviar múltiples canales en una línea: // Serial.print("DATA:"); Serial.print(ch0); Serial.print(","); // Serial.print(ch1); Serial.println(); }
Intérprete de comandos desde la HMI
void readCommands() { while (Serial.available() > 0) { char c = (char) Serial.read(); if (c == '\n') { inputBuffer.trim(); // Eliminar \r y espacios if (inputBuffer.length() > 0) { interpretCommand(inputBuffer); } inputBuffer = ""; // Limpiar buffer } else { inputBuffer += c; // Acumular caracteres } } } void interpretCommand(String cmd) { // Separar CLAVE y PARAM por el delimitador ':' int sep = cmd.indexOf(':'); String key = (sep > 0) ? cmd.substring(0, sep) : cmd; String param = (sep > 0) ? cmd.substring(sep + 1) : ""; if (key == "START") { streaming = true; Serial.println("OK"); } else if (key == "STOP") { streaming = false; Serial.println("OK"); } else if (key == "RATE") { sampleRate = param.toInt(); Serial.println("OK"); } else if (key == "LED") { digitalWrite(LED_PIN, param.toInt() ? HIGH : LOW); Serial.println("OK"); } else if (key == "PWM") { analogWrite(9, param.toInt()); // Pin 9 con PWM Serial.println("OK"); } else { Serial.print("ERR:CMD_UNKNOWN:"); Serial.println(key); } }
delay() bloquea el procesamiento de comandos entrantes. Siempre usar el patrón millis() - lastTime >= interval para temporización no bloqueante.
Interfaz HMI en HTML/JS
Conexión con Web Serial API
La Web Serial API permite al navegador comunicarse con dispositivos serie (incluyendo Arduino) sin instalar drivers ni extensiones. El flujo es: solicitar permiso → abrir puerto → leer/escribir mediante streams. Es la tecnología que utilizaremos en los trabajos prácticos de IM2 para construir los paneles de visualización.
class ArduinoSerial { constructor(onData) { this.port = null; this.reader = null; this.writer = null; this.onData = onData; // Callback intérprete (línea: string) this.buffer = ""; this.running = false; } async connect(baudRate = 115200) { try { // Solicitar puerto al usuario this.port = await navigator.serial.requestPort(); await this.port.open({ baudRate }); // Configurar writer (HMI → Arduino) const textEncoder = new TextEncoderStream(); textEncoder.readable.pipeTo(this.port.writable); this.writer = textEncoder.writable.getWriter(); // Iniciar loop de lectura this.running = true; this._readLoop(); return true; } catch (err) { console.error("Error al conectar:", err); return false; } } async _readLoop() { const decoder = new TextDecoderStream(); this.port.readable.pipeTo(decoder.writable); this.reader = decoder.readable.getReader(); while (this.running) { const { value, done } = await this.reader.read(); if (done) break; this.buffer += value; const lines = this.buffer.split('\n'); this.buffer = lines.pop(); // Retener fragmento incompleto for (const line of lines) { const trimmed = line.trim(); if (trimmed) this.onData(trimmed); // Llamar intérprete } } } async send(message) { if (this.writer) { await this.writer.write(message + '\n'); } } async disconnect() { this.running = false; await this.reader?.cancel(); await this.writer?.close(); await this.port?.close(); } }
Intérprete de datos en la HMI
// Instanciar la clase con un callback de datos const arduino = new ArduinoSerial(interpretarLinea); function interpretarLinea(line) { const [key, val] = line.split(':'); switch (key) { case 'HELLO': updateStatus('Conectado — firmware ' + val); arduino.send('START'); // Iniciar streaming automáticamente break; case 'V0': updateGauge('voltaje', parseInt(val) * 5.0 / 1023); break; case 'TEMP': updateDisplay('temperatura', parseFloat(val)); break; case 'DATA': // Múltiples valores separados por coma const values = val.split(',').map(Number); updateOscilloscope(values); break; case 'ERR': console.warn('Arduino error:', val); break; } }
Renderizado en Canvas — forma de onda básica
const canvas = document.getElementById('scope'); const ctx = canvas.getContext('2d'); const buffer = []; // Historial de muestras const MAX_PTS = 200; function updateOscilloscope(sample) { buffer.push(sample); if (buffer.length > MAX_PTS) buffer.shift(); const W = canvas.width; const H = canvas.height; const step = W / MAX_PTS; // Fondo ctx.fillStyle = '#0d0f14'; ctx.fillRect(0, 0, W, H); // Grilla ctx.strokeStyle = '#1a2a1a'; ctx.lineWidth = 1; for (let i = 0; i <= 4; i++) { const y = (H / 4) * i; ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke(); } // Señal ctx.strokeStyle = '#00e5a0'; ctx.lineWidth = 2; ctx.beginPath(); buffer.forEach((v, i) => { const x = i * step; const y = H - (v / 1023) * H; i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); }); ctx.stroke(); }
Ejemplos de instrumentos completos
Ejemplo 1 — Voltímetro digital
Mide la tensión en el pin A0 (0–5 V), calcula promedio de 16 muestras para reducir ruido, y envía el resultado cada 200 ms.
// Voltímetro con promediado de muestras const int N_SAMPLES = 16; unsigned long lastMs = 0; bool streaming = false; String cmdBuf = ""; void setup() { Serial.begin(115200); while (!Serial); Serial.println("HELLO:voltimetro-v1"); } float readVoltage() { long sum = 0; for (int i = 0; i < N_SAMPLES; i++) { sum += analogRead(A0); delayMicroseconds(200); // Estabilizar ADC } return (sum / N_SAMPLES) * (5.0 / 1023.0); } void loop() { // Leer comandos while (Serial.available()) { char c = Serial.read(); if (c == '\n') { cmdBuf.trim(); if (cmdBuf == "START") { streaming = true; Serial.println("OK"); } if (cmdBuf == "STOP") { streaming = false; Serial.println("OK"); } cmdBuf = ""; } else { cmdBuf += c; } } // Enviar medición if (streaming && millis() - lastMs >= 200) { lastMs = millis(); float v = readVoltage(); Serial.print("VOLT:"); Serial.println(v, 4); } }
Ejemplo 2 — Osciloscopio de baja frecuencia
Captura 50 muestras del pin A0 lo más rápido posible (sin delays) y las envía como paquete DATA: con valores separados por coma.
const int BLOCK = 50; // Muestras por paquete bool run = false; String buf = ""; void setup() { Serial.begin(115200); while (!Serial); Serial.println("HELLO:scope-v1"); } void loop() { while (Serial.available()) { char c = Serial.read(); if (c == '\n') { buf.trim(); if (buf == "START") run = true; if (buf == "STOP") run = false; buf = ""; } else { buf += c; } } if (!run) return; // Capturar bloque de muestras int samples[BLOCK]; for (int i = 0; i < BLOCK; i++) { samples[i] = analogRead(A0); delayMicroseconds(500); // ~2 kHz de muestreo } // Enviar paquete: DATA:val1,val2,...,valN Serial.print("DATA:"); for (int i = 0; i < BLOCK; i++) { Serial.print(samples[i]); if (i < BLOCK - 1) Serial.print(','); } Serial.println(); }
delayMicroseconds(500) se logran aproximadamente 1.6 kHz. Para señales de mayor frecuencia es necesario ajustar el prescaler del ADC.
Debug y resolución de errores
| Síntoma | Causa probable | Solución |
|---|---|---|
| El botón Conectar no aparece o no abre diálogo | Navegador no soporta Web Serial | Usar Chrome 89+ o Edge 89+. Verificar con navigator.serial en consola. |
| La HMI no recibe datos aunque el Arduino envía | Monitor Serie del IDE abierto | Cerrar el Monitor Serie antes de conectar desde el navegador. |
| Datos corruptos o líneas mezcladas | Baud rate incorrecto | Verificar que el baud rate en Serial.begin() y en port.open() sean iguales. |
| Comando enviado desde HMI no responde | Buffer de entrada sin procesar | Asegurarse de que el loop() llama a readCommands() frecuentemente. |
| Lecturas ADC muy ruidosas | Sin promediado o referencia flotante | Promediar N muestras. Conectar AREF a 3.3 V o usar analogReference(INTERNAL). |
| Puerto COM no aparece en Windows | Driver CH340 faltante | Instalar driver CH340 desde el fabricante o usar Arduino UNO original (ATmega16U2). |
Verificación rápida del protocolo
Antes de abrir la HMI, usar el Monitor Serie del Arduino IDE para verificar que el firmware envía los datos correctamente:
← (escribir en Monitor Serie): START → (respuesta Arduino): OK → V0:512 → VV:2.502 → V0:498 → VV:2.434 ← (escribir): STOP → OK