Bloque 3.3: Programación de Entradas y Salidas

Explora las capacidades avanzadas de interacción con el mundo físico mediante señales analógicas y operaciones de bajo nivel.

Diapositivas del Tema

Consulta la presentación visual de este tema para una mejor comprensión de los conceptos.

Ver Presentación →

Del Digital a lo Analógico (y viceversa)

Mientras que digitalRead/Write manejan estados simples (ON/OFF), las señales analógicas permiten trabajar con una gama continua de valores. Esto es crucial para leer sensores de temperatura, luz o posición y controlar dispositivos con precisión variable como motores o LEDs.

0. El Monitor Serial: Tu Ventana de Depuración

Cuando un Arduino está ejecutando un sketch, no tenemos una pantalla ni un teclado directamente conectados como en una computadora. Entonces, ¿cómo sabemos qué está haciendo o cómo vemos los valores que lee un sensor? La respuesta es el Monitor Serial.

Captura de pantalla del Monitor Serial en el Arduino IDE
Captura del Monitor Serial en el Arduino IDE, mostrando mensajes enviados por Serial.print().

¿Qué es el Monitor Serial?

El Monitor Serial es una herramienta del Arduino IDE que permite la comunicación bidireccional entre tu Arduino y la computadora a través del cable USB. Es como un chat simple: el Arduino puede "escribir" mensajes (valores de sensores, estados, etc.) y la computadora (a través del IDE) puede "leer" esos mensajes. También puedes enviar comandos desde la computadora al Arduino.

¿Cómo se Usa?

  1. Inicializar la comunicación: En la función setup() de tu sketch, debes incluir la línea Serial.begin(velocidad_en_baudios);. El valor más común es 9600.
    void setup() {
      Serial.begin(9600); // Inicia la comunicación serial a 9600 baudios
      // ... resto de tu código de setup
    }
  2. Enviar datos desde Arduino: Usa las funciones Serial.print() y Serial.println() en tu código loop() o en cualquier otra parte.
    • Serial.print(valor); Imprime el valor seguido por el siguiente carácter que se imprima.
    • Serial.println(valor); Imprime el valor y luego añade un "salto de línea" (como presionar Enter), posicionando el cursor al inicio de la siguiente línea.
    int valorSensor = analogRead(A0);
    Serial.print("Valor del sensor: ");
    Serial.println(valorSensor);
  3. Abrir el Monitor Serial: En el Arduino IDE, ve al menú Herramientas > Monitor Serial (o usa el icono de lupa en la esquina superior derecha de la barra de herramientas).
  4. Configurar la Velocidad (Baudios): En el Monitor Serial, asegúrate de que el menú desplegable de la esquina inferior derecha esté configurado con la misma velocidad (por ejemplo, "9600 baud") que usaste en Serial.begin(). Si no coinciden, verás caracteres extraños o ilegibles.

¿Qué son los Baudios?

Los baudios (baud rate) son una medida de la velocidad de transmisión de datos en una línea de comunicación. Un "baudio" representa el número de cambios de señal (bits) por segundo. En el caso del Monitor Serial, indica cuántos bits de datos se envían cada segundo.

  • 9600 baudios: Significa que se transmiten 9600 bits por segundo.
  • ¿Por qué 9600? Es una velocidad estándar y bastante confiable para la comunicación serial entre Arduino y una computadora moderna. Es lo suficientemente rápida para enviar mensajes de texto legibles por humanos, pero lo suficientemente lenta como para que los microcontroladores antiguos y modernos puedan seguirla sin errores significativos.
  • Otras velocidades comunes: 4800, 19200, 38400, 57600, 115200. Puedes usar cualquiera, siempre que Serial.begin(velocidad) en tu Arduino y la configuración del Monitor Serial sean exactamente las mismas.
  • Importancia de la coincidencia: Si el Arduino envía datos a 9600 baudios pero el Monitor Serial los espera a 115200 baudios, los datos se interpretarán incorrectamente, resultando en texto ilegible. Es crucial que ambos lados de la comunicación estén "sintonizados" a la misma frecuencia.
Consejo: Si ves caracteres extraños en el Monitor Serial, lo primero que debes verificar es que la velocidad (baudios) en el Monitor Serial coincida con la usada en Serial.begin().

0.1. Recibiendo Datos desde el Monitor Serial

Ya vimos cómo enviar mensajes del Arduino a la computadora con Serial.print(). Pero la comunicación serial es bidireccional. ¿Cómo hace el Arduino para "escuchar" y responder a comandos enviados desde el teclado del Monitor Serial? Las funciones Serial.available() y Serial.read() son las encargadas de esto.

Funciones Clave

  • Serial.available(): Devuelve el número de bytes (caracteres) disponibles en el buffer de recepción serial. Si devuelve 0, no hay datos nuevos para leer. Si devuelve un número mayor que 0, hay datos esperando.
  • Serial.read(): Lee el siguiente byte (carácter) disponible en el buffer de recepción. Devuelve el valor del byte (0-255) o -1 si no hay datos disponibles.

Ejemplo 0.1.1: Encendiendo un LED desde el Teclado

Este ejemplo permite encender o apagar el LED integrado (pin 13) escribiendo 'H' (de High) o 'L' (de Low) en el Monitor Serial.

Diagrama del LED integrado en el pin 13 de Arduino
El LED integrado en la mayoría de placas Arduino está conectado al pin 13.

Código:

##include <Arduino.h>

const int ledPin = 13; // Pin del LED integrado
int estadoLed = LOW;       // Variable para almacenar el estado del LED

void setup() {
  Serial.begin(9600);
  pinMode(ledPin, OUTPUT);
  
  Serial.println("Escribe 'H' para encender el LED o 'L' para apagarlo.");
}

void loop() {
  // Verificamos si hay datos disponibles para leer
  if (Serial.available() > 0) {
    // Leemos el byte (carácter) entrante
    char comando = Serial.read();

    // Procesamos el comando recibido
    if (comando == 'H' || comando == 'h') {
      estadoLed = HIGH;
      Serial.println("LED Encendido.");
    } 
    else if (comando == 'L' || comando == 'l') {
      estadoLed = LOW;
      Serial.println("LED Apagado.");
    }
    else {
      Serial.println("Comando no reconocido. Escribe 'H' o 'L'.");
    }
    
    // Actualizamos el estado físico del LED
    digitalWrite(ledPin, estadoLed);
  }
}

Instrucciones:

  1. Carga este sketch en tu Arduino.
  2. Abre el Monitor Serial (Herramientas > Monitor Serial).
  3. Asegúrate de que la velocidad esté en "9600 baud".
  4. Escribe una 'H' (mayúscula o minúscula) y presiona Enter. El LED debe encenderse.
  5. Escribe una 'L' (mayúscula o minúscula) y presiona Enter. El LED debe apagarse.

Este ejemplo sencillo introduce el concepto de control interactivo del Arduino desde la computadora, una base para proyectos más complejos de comunicación.


1. Entradas Analógicas: analogRead()

Las entradas digitales solo pueden detectar dos estados: HIGH (5V o 3.3V) o LOW (0V). Sin embargo, muchos sensores del mundo real producen una señal que varía continuamente dentro de un rango, como la luz que cambia suavemente del día a la noche o la temperatura que sube y baja gradualmente. Esta es una señal analógica.

Ejemplo de señal analógica
Ejemplo de una señal analógica variando suavemente con el tiempo.

La función analogRead(pin) permite a un Arduino leer estos valores variables. En placas como el Arduino UNO, esto se hace mediante un Convertidor Analógico-Digital (ADC) integrado en el microcontrolador.

¿Cómo funciona analogRead()?

  • El ADC muestrea la tensión en el pin especificado.
  • Compara esta tensión con una tensión de referencia (por defecto, la tensión de alimentación del Arduino, 5V en UNO).
  • Convierte esta comparación en un número digital. El Arduino UNO tiene un ADC de 10 bits, lo que significa que puede representar 2^10 = 1024 valores distintos.
  • Devuelve un valor entero entre 0 y 1023.
    • 0 corresponde a 0 Voltios en el pin.
    • 1023 corresponde a la tensión de referencia (5V en UNO).
    • Un valor intermedio representa una tensión proporcional. Por ejemplo, 512 es aproximadamente la mitad de la tensión de referencia (2.5V en UNO).

Sintaxis: int valor = analogRead(numero_de_pin);

Pines Analógicos: En el Arduino UNO, los pines dedicados para lectura analógica están etiquetados como A0, A1, A2, ..., A5. Aunque técnicamente puedes usar números (0-5), es más claro usar las etiquetas A0, A1, etc.

Ejemplo 1.1: Leyendo un Potenciómetro

Un potenciómetro es una resistencia variable que actúa como un divisor de tensión. Al girar su eje, cambia la tensión en su pin central, que podemos leer con analogRead().

Esquema de conexión de un potenciómetro a un pin analógico de Arduino
Conexión de un potenciómetro al pin A0 de un Arduino UNO. Los pines laterales van a 5V y GND.

Código:

##include <Arduino.h>

// Definimos el pin analógico donde está conectado el potenciómetro
const int potPin = A0;

// Variable para almacenar el valor leído
int valorPot = 0;

void setup() {
  // Iniciamos la comunicación serial a 9600 baudios
  Serial.begin(9600);
}

void loop() {
  // Leemos el valor analógico del potenciómetro (0 a 1023)
  valorPot = analogRead(potPin);

  // Imprimimos el valor en el Monitor Serial
  Serial.print("Valor del Potenciometro: ");
  Serial.println(valorPot);

  // Esperamos un poco antes de la próxima lectura
  delay(100); // 100ms
}

1.1. Limitaciones y Buenas Prácticas con analogRead()

Aunque analogRead() es una función poderosa, es importante conocer sus limitaciones para evitar problemas en tus proyectos.

Tiempo de Conversión y Rendimiento

Cada llamada a analogRead() no es instantánea. El ADC interno necesita un tiempo para muestrear la tensión y realizar la conversión. En un Arduino UNO, este proceso toma aproximadamente 100 microsegundos (0.1 ms) por lectura. Si haces muchas lecturas seguidas en un bucle sin delays, puedes ralentizar significativamente tu programa.

Ejemplo: Si tu bucle principal necesita ejecutarse 1000 veces por segundo (1 KHz), y haces una sola lectura de analogRead(), estás usando el 10% de tu tiempo de bucle solo en esa lectura (100 lecturas/segundo * 0.1 ms/lectura = 10 ms). Si necesitas leer 6 pines analógicos, estarías usando el 60% del tiempo del bucle solo en lecturas ADC.
Ruido y Fluctuaciones en las Lecturas

Las lecturas de analogRead() pueden fluctuar ligeramente incluso si la señal de entrada es estable. Esto se debe a ruido eléctrico inherente al circuito, interferencias o inestabilidades en la fuente de alimentación. Estas pequeñas fluctuaciones pueden ser problemáticas si tu código toma decisiones críticas basadas en un cambio muy pequeño en el valor leído.

Gráfica mostrando fluctuaciones en lecturas analogicas
Ejemplo de cómo puede verse el ruido en lecturas sucesivas de un sensor estable.
Técnicas de Filtrado: Promedio Móvil Simple

Una forma común de reducir el efecto del ruido es aplicar un filtro digital. El más simple es el promedio móvil: en lugar de usar una sola lectura, tomas varias lecturas en un corto período y usas su promedio.

Ejemplo 1.1.1: Lectura con Promedio Móvil

Este ejemplo lee un sensor varias veces y calcula el promedio para obtener un valor más estable.

// Definimos el pin analógico
const int sensorPin = A0;

// Número de muestras para el promedio
const int numMuestras = 10;
int muestras[10]; // Arreglo para almacenar las muestras
int indiceMuestra = 0; // Índice actual en el arreglo
long sumaTotal = 0; // Suma de todas las muestras
int promedio = 0; // Valor promedio calculado

void setup() {
  Serial.begin(9600);
  // Inicializamos el arreglo de muestras
  for (int i = 0; i < numMuestras; i++) {
    muestras[i] = 0;
  }
}

void loop() {
  // Leemos el valor actual del sensor
  int lecturaActual = analogRead(sensorPin);

  // Restamos la muestra más antigua de la suma total
  sumaTotal = sumaTotal - muestras[indiceMuestra];
  // Guardamos la nueva lectura en la posición actual
  muestras[indiceMuestra] = lecturaActual;
  // Añadimos la nueva lectura a la suma total
  sumaTotal = sumaTotal + lecturaActual;
  // Calculamos el promedio
  promedio = sumaTotal / numMuestras;

  // Imprimimos el valor promedio en el Monitor Serial
  Serial.print("Promedio: ");
  Serial.println(promedio);

  // Avanzamos el índice circularmente
  indiceMuestra = (indiceMuestra + 1) % numMuestras;

  // Pequeña pausa
  delay(50);
}

Explicación:

  • Se define un arreglo muestras[10] para almacenar las últimas 10 lecturas.
  • Se usa una variable indiceMuestra para seguir la posición actual en el arreglo, avanzando circularmente (0, 1, 2, ..., 9, 0, 1, ...).
  • Se mantiene una sumaTotal de las muestras actuales.
  • En cada iteración del loop():
    • Se lee un nuevo valor.
    • Se resta del sumaTotal la muestra que se va a "desplazar" (la más antigua).
    • Se guarda la nueva lectura en la posición actual del arreglo.
    • Se añade la nueva lectura al sumaTotal.
    • Se calcula el promedio dividiendo la suma por el número de muestras.
    • Se actualiza el indiceMuestra para la próxima iteración.

Este método es eficiente y proporciona una lectura más estable. Puedes ajustar el número de muestras (numMuestras) para equilibrar la estabilidad deseada con la velocidad de respuesta del sistema.


2. Salidas Analógicas (PWM): analogWrite()

Los pines digitales solo pueden estar completamente encendidos (HIGH) o apagados (LOW). Pero ¿qué pasa si queremos controlar el brillo de un LED o la velocidad de un motor con más precisión que simplemente ON/OFF? La mayoría de microcontroladores Arduino no pueden generar una verdadera señal analógica de voltaje variable. En su lugar, utilizan una técnica llamada Modulación por Ancho de Pulso (PWM).

Ejemplo de señal PWM
Ejemplo de una señal PWM a diferentes anchos de pulso (Duty Cycle). Arriba 30%, abajo 80%.

¿Qué es PWM?

PWM funciona generando una señal digital (cuadrada) que cambia rápidamente entre HIGH y LOW. El truco está en variar el duty cycle (ciclo de trabajo), que es el porcentaje del tiempo que la señal está en HIGH durante un período completo.

  • Un duty cycle del 0% significa que la señal está siempre en LOW. Esto es equivalente a 0V.
  • Un duty cycle del 50% significa que la señal está la mitad del tiempo en HIGH y la mitad en LOW.
  • Un duty cycle del 100% significa que la señal está siempre en HIGH. Esto es equivalente al voltaje máximo (5V o 3.3V).

Para el ojo humano o un motor, un LED con un duty cycle del 50% en una frecuencia alta (por ejemplo, 490 Hz en Arduino UNO) aparecerá a la mitad de su brillo máximo, o un motor girará a la mitad de su velocidad máxima. Esta es la ilusión que crea la técnica PWM.

La función analogWrite(pin, valor) permite controlar este duty cycle en pines específicos.

¿Cómo funciona analogWrite()?

  • El valor es un número entero entre 0 y 255 (8 bits).
  • 0 corresponde a un duty cycle del 0% (siempre LOW).
  • 255 corresponde a un duty cycle del 100% (siempre HIGH).
  • 127 corresponde aproximadamente a un duty cycle del 50% (mitad HIGH, mitad LOW).

Sintaxis: analogWrite(numero_de_pin, valor);

Pines PWM: No todos los pines digitales pueden generar PWM. En el Arduino UNO, los pines compatibles están marcados con el símbolo ~: ~3, ~5, ~6, ~9, ~10, ~11.

Importante: analogWrite() no requiere un pinMode() previo. El pin se configura automáticamente como salida cuando se usa esta función.

Ejemplo 2.1: Controlando el Brillo de un LED con un Potenciómetro

Combinamos la lectura analógica del potenciómetro con la salida PWM para crear un control de brillo variable.

Esquema de conexión de un LED con PWM y un potenciómetro al Arduino
LED conectado al pin 9 (PWM) con una resistencia de 220Ω. Potenciómetro conectado al pin A0.

Código:

##include <Arduino.h>

// Definimos los pines
const int potPin = A0;  // Pin para el potenciómetro
const int ledPin = 9;   // Pin PWM para el LED

// Variables
int valorPot = 0;      // Valor leído del potenciómetro (0-1023)
int brilloLED = 0;    // Valor de brillo para el LED (0-255)

void setup() {
  // No es necesario usar pinMode para analogWrite en este caso
  // Serial.begin(9600); // Opcional, para depuración
}

void loop() {
  // 1. Leemos el valor del potenciómetro (0-1023)
  valorPot = analogRead(potPin);

  // 2. Mapeamos ese valor al rango de PWM (0-255)
  // La función map(valor, deMin, deMax, aMin, aMax) hace esta conversión
  brilloLED = map(valorPot, 0, 1023, 0, 255);

  // 3. Escribimos el valor de brillo al LED
  analogWrite(ledPin, brilloLED);

  // 4. Pequeña pausa para estabilidad
  delay(10);
}
Función map(): Esta función es muy útil para convertir valores de un rango a otro. En el ejemplo, map(valorPot, 0, 1023, 0, 255) toma un valor entre 0 y 1023 y lo convierte proporcionalmente a un valor entre 0 y 255.

3. Operaciones Bit a Bit (Bitwise Operations)

En programación de sistemas embebidos, a menudo necesitamos manipular bits individuales dentro de un byte o una palabra de datos. Las operaciones bit a bit nos permiten hacerlo de forma eficiente y directa a nivel del hardware. Esto es mucho más rápido y consume menos memoria que otras formas de manipulación.

Operadores Bit a Bit

Operador Nombre Descripción Ejemplo
& AND Devuelve 1 solo si ambos bits son 1. 1010 & 1100 = 1000
| OR Devuelve 1 si al menos uno de los bits es 1. 1010 | 1100 = 1110
^ XOR Devuelve 1 si los bits son diferentes. 1010 ^ 1100 = 0110
~ NOT Invierte todos los bits. ~1010 = 0101 (depende del tamaño del dato)
<< Desplazamiento a la Izquierda Mueve los bits hacia la izquierda un número de posiciones, llenando con ceros a la derecha. 1010 << 2 = 1000 (en un contexto de 4 bits)
>> Desplazamiento a la Derecha Mueve los bits hacia la derecha un número de posiciones. 1010 >> 1 = 0101

Ejemplo 3.1: Comprobando Bits Específicos (Máscaras)

Supongamos que un sensor o un registro interno devuelve un byte donde cada bit representa el estado de algo (por ejemplo, 8 botones). Queremos comprobar si el botón 2 (bit 1, contando desde 0) está presionado.

// Supongamos que leemos un byte de un puerto o sensor
byte estadoPuerto = digitalRead(2) | (digitalRead(3) << 1) | (digitalRead(4) << 2); // Simulación

// Creamos una máscara para el bit 1 (botón 2)
byte mascaraBoton2 = 1 << 1; // Esto es 00000010 en binario

// Comprobamos si el bit 1 está encendido usando AND
if (estadoPuerto & mascaraBoton2) {
  Serial.println("Boton 2 esta presionado!");
} else {
  Serial.println("Boton 2 NO esta presionado.");
}

Explicación:

  • 1 << 1 crea la máscara 00000010.
  • estadoPuerto & mascaraBoton2 realiza una operación AND bit a bit. El resultado solo será distinto de cero si el bit 1 de estadoPuerto también es 1.

Ejemplo 3.2: Encendiendo/Apagando Bits Específicos

Queremos encender el LED conectado al pin 13 (bit 7 del puerto B en UNO) y apagar el del pin 8 (bit 0 del puerto B) sin afectar los demás.

// Encender el LED del pin 13 (bit 7 de PORTB)
// Usamos OR con una máscara que tiene solo el bit 7 encendido
PORTB = PORTB | (1 << 7); 

// Apagar el LED del pin 8 (bit 0 de PORTB)
// Usamos AND con el inverso de una máscara que tiene solo el bit 0 encendido
PORTB = PORTB & ~(1 << 0);

Explicación:

  • PORTB | (1 << 7): Enciende el bit 7 sin tocar los demás.
  • ~(1 << 0) crea una máscara con todos los bits a 1 excepto el bit 0 (ej. 11111110).
  • PORTB & ~(1 << 0): Apaga el bit 0 sin tocar los demás.
Nota: Manipular directamente registros como PORTB es programación de bajo nivel. Es muy eficiente pero requiere conocer la arquitectura del microcontrolador. Para principiantes, digitalWrite() es más seguro y legible.

4. Manejo Eficiente de Variables y Memoria

Los microcontroladores tienen recursos limitados de RAM y Flash. Usar el tipo de dato más apropiado no solo es buena práctica, sino crucial para que tu programa funcione correctamente y de forma eficiente.

4.1. Tipos de Datos Comunes y su Tamaño

Tipo Tamaño (bytes) Rango de Valores Ejemplo de Uso
bool / boolean 1 false (0) o true (1) Estado de un botón (bool botonPresionado = false;)
byte / uint8_t 1 0 a 255 Valor de PWM (byte brillo = 150;), lectura de sensor de 8 bits
char 1 -128 a 127 Carácter (char letra = 'A';)
int / int16_t 2 -32,768 a 32,767 Contador de bucles, cálculos simples
unsigned int / uint16_t 2 0 a 65,535 Valores que no son negativos y necesitan más rango que byte
long / int32_t 4 -2,147,483,648 a 2,147,483,647 Valores de tiempo (long millisInicio = millis();), cálculos grandes
unsigned long / uint32_t 4 0 a 4,294,967,295 Valores de tiempo sin signo, IDs únicos grandes
float 4 ±3.4028235E+38 (6-7 dígitos decimales) Cálculos con decimales (temperatura, voltaje)
Consejo de Memoria: Si sabes que una variable solo necesitará almacenar valores entre 0 y 100, usar byte (1 byte) es mucho más eficiente que usar int (2 bytes) o long (4 bytes). En un programa complejo, estas pequeñas optimizaciones suman.

Ejemplo 4.1: Comparando Uso de Memoria

Este ejemplo no se ejecuta en la placa, sino que ilustra cómo el tipo de dato afecta el uso de memoria. Puedes ver el tamaño de los tipos de datos usando la función sizeof() en el Monitor Serial.

void setup() {
  Serial.begin(9600);
  
  Serial.print("Tamaño de bool: "); Serial.println(sizeof(bool));
  Serial.print("Tamaño de byte: "); Serial.println(sizeof(byte));
  Serial.print("Tamaño de int: "); Serial.println(sizeof(int));
  Serial.print("Tamaño de long: "); Serial.println(sizeof(long));
  Serial.print("Tamaño de float: "); Serial.println(sizeof(float));
}

void loop() {
  // Nada que hacer aquí
}

Este código imprimirá los tamaños en bytes de cada tipo de dato en el Monitor Serial.


4.1. Estructuras de Datos para Organizar tu Proyecto

Cuando trabajas con múltiples sensores, actuadores o estados en un proyecto mecatrónico, manejar cada variable individualmente puede volverse complicado y propenso a errores. Las estructuras de datos nos permiten agrupar y organizar información de forma lógica, haciendo que nuestro código sea más limpio, mantenible y escalable.

Arrays (Arreglos)

Un array es una colección ordenada de elementos del mismo tipo, almacenados en posiciones de memoria contiguas. Se accede a cada elemento mediante un índice numérico (que comienza en 0).

Declaración: tipo nombre[tamaño];

Ejemplo:

// Array de enteros para almacenar lecturas de 5 sensores
int lecturasSensores[5];

// Array de pines para 3 LEDs
const int pinesLEDs[3] = {9, 10, 11};

// Acceder a elementos
lecturasSensores[0] = 512; // Asigna 512 al primer sensor
int brilloLED2 = pinesLEDs[1]; // brilloLED2 obtiene el valor 10

Aplicación: Leyendo múltiples sensores.

// Array de pines de sensores
const int numSensores = 3;
const int pinesSensores[numSensores] = {A0, A1, A2};

// Array para almacenar las lecturas
int lecturas[numSensores];

void setup() {
  Serial.begin(9600);
}

void loop() {
  // Leer todos los sensores usando un bucle
  for (int i = 0; i < numSensores; i++) {
    lecturas[i] = analogRead(pinesSensores[i]);
    Serial.print("Sensor ");
    Serial.print(i);
    Serial.print(": ");
    Serial.print(lecturas[i]);
    if (i < numSensores - 1) Serial.print(", ");
  }
  Serial.println();
  delay(500);
}

Structs (Estructuras)

Un struct permite agrupar variables de diferentes tipos bajo un solo nombre. Es ideal para representar objetos del mundo real con múltiples atributos, como un sensor con su pin, última lectura, estado, etc.

Declaración:

struct NombreDeLaEstructura {
  tipo1 miembro1;
  tipo2 miembro2;
  // ... más miembros
};

Ejemplo:

// Definimos una estructura para un sensor
struct Sensor {
  int pin;
  int ultimaLectura;
  bool activo;
  char nombre[20]; // Array de caracteres para el nombre
};

// Crear una variable de tipo Sensor
Sensor miSensor;

void setup() {
  // Inicializar los miembros del struct
  miSensor.pin = A0;
  miSensor.ultimaLectura = 0;
  miSensor.activo = true;
  strcpy(miSensor.nombre, "Sensor de Luz"); // Copiar string al array
}

void loop() {
  if (miSensor.activo) {
    miSensor.ultimaLectura = analogRead(miSensor.pin);
    Serial.print(miSensor.nombre);
    Serial.print(": ");
    Serial.println(miSensor.ultimaLectura);
  }
  delay(1000);
}

Aplicación: Gestión de múltiples sensores con structs.

// Definimos una estructura para un sensor
struct Sensor {
  int pin;
  int ultimaLectura;
  char nombre[20];
};

// Creamos un array de structs para 2 sensores
Sensor sensores[2];

void setup() {
  Serial.begin(9600);
  
  // Inicializamos el primer sensor
  sensores[0].pin = A0;
  strcpy(sensores[0].nombre, "Luz");
  
  // Inicializamos el segundo sensor
  sensores[1].pin = A1;
  strcpy(sensores[1].nombre, "Temperatura");
}

void loop() {
  // Leer todos los sensores definidos en el array de structs
  for (int i = 0; i < 2; i++) {
    sensores[i].ultimaLectura = analogRead(sensores[i].pin);
    Serial.print(sensores[i].nombre);
    Serial.print(": ");
    Serial.println(sensores[i].ultimaLectura);
  }
  Serial.println("---");
  delay(1000);
}
Nota sobre C strings: Para manejar texto (strings) dentro de structs en C/C++ básico (como el de Arduino), se usa un array de caracteres (char nombre[20]) y funciones como strcpy() para copiar strings. La librería String de Arduino también existe, pero puede consumir más memoria dinámica y causar fragmentación, por lo que usar char[] es a menudo preferido para estructuras simples.

Breve Mención a Otras Estructuras

  • Matrices (Arrays 2D): Útiles para representar tableros, pantallas de LEDs, o mapas de calor. Por ejemplo, int matriz[3][3]; crea una matriz 3x3.
    int pantallaLED[8][8]; // Una pantalla de LED 8x8
    pantallaLED[3][4] = 1; // Enciende el LED en la fila 3, columna 4
  • Enumeraciones (enum): Permiten definir un conjunto de constantes con nombre, mejorando la legibilidad del código.
    enum EstadoLED {APAGADO, ENCENDIDO, PARPADEANDO};
    EstadoLED estadoMiLED = ENCENDIDO;
  • JSON: Es un formato de intercambio de datos muy popular, especialmente para comunicaciones con APIs web o servidores. En Arduino, se puede usar con librerías como ArduinoJson. Es muy útil para proyectos IoT donde el dispositivo necesita enviar/recibir datos estructurados.
    // Ejemplo de un string JSON
    {"sensor":"temperatura", "valor":25.6, "unidad":"Celsius"}

El uso adecuado de estas estructuras de datos es clave para escribir programas Arduino limpios, eficientes y fáciles de mantener, especialmente a medida que tus proyectos crecen en complejidad.

¿Dónde se usan comúnmente?

Entender dónde se aplican estas estructuras en proyectos reales ayuda a apreciar su valor. Haz clic en cada categoría para ver ejemplos específicos.

  • Lectura de múltiples sensores del mismo tipo: Como en el ejemplo anterior, leer 5 sensores de temperatura, 8 celdas de carga en una balanza, o 10 sensores de humedad distribuidos en una invernadero.
  • Control de múltiples actuadores: Gestionar 16 LEDs de una tira, 8 relés en un módulo, o 4 servomotores. Un array de pines y un bucle for simplifican el código de inicialización.
  • Almacenamiento de datos históricos: Guardar las últimas 50 lecturas de un sensor para calcular un promedio o detectar tendencias.

  • Representar dispositivos complejos: Un sensor DHT22 que proporciona temperatura y humedad. Un struct SensorDHT {int pin; float temp; float humedad; unsigned long ultimaLectura;} agrupa todos sus datos.
  • Gestionar tareas en sistemas no bloqueantes: En programación avanzada, un struct puede representar una tarea con su intervalo, tiempo de la última ejecución, estado, etc.
  • Configuración de dispositivos: Almacenar la configuración de una bomba de agua (pin de control, pin del sensor de flujo, umbral mínimo, estado) en un solo lugar estructurado.
  • Modelar entidades del mundo real: Un motor paso a paso con su pin de dirección, pin de paso, posición actual, velocidad objetivo, etc.

  • Pantallas de LEDs o LCD: Una pantalla LED 8x8 se controla naturalmente con una matriz 8x8 donde cada elemento representa el estado (encendido/apagado) de un LED.
  • Mapas o niveles de juego: En proyectos de entretenimiento o robótica educativa, un nivel de un juego simple puede representarse con una matriz donde diferentes números representan paredes, caminos, enemigos, etc.
  • Sensores de matriz: Sensores como una matriz de sensores táctiles o un teclado matricial se manejan eficientemente con arrays 2D.

  • Estados de una máquina de estados: En lugar de números mágicos, usar enum Estado {INICIO, ESPERANDO, PROCESANDO, ERROR, FINALIZADO}; hace que el código sea mucho más legible y mantenible.
  • Tipos de sensores o actuadores: Identificar diferentes tipos de sensores conectados (enum TipoSensor {TEMPERATURA, HUMEDAD, LUX, MOVIMIENTO};).
  • Modos de operación: Un sistema puede tener modos como enum Modo {MANUAL, AUTOMATICO, CALIBRACION};.

  • Comunicación con APIs web: Enviar datos de sensores a un servidor en formato JSON ({"device_id":"ARDUINO_01", "data":[{"sensor":"temp", "value":22.5}]}).
  • Configuración remota: Recibir comandos o nuevas configuraciones desde una aplicación web o móvil en formato JSON.
  • Almacenamiento en archivos (SD, SPIFFS): Guardar la configuración del dispositivo o logs de datos en tarjetas SD o sistemas de archivos internos en formato JSON para fácil lectura y parseo.
Consejo: Cuando empieces un proyecto nuevo, piensa en los datos que vas a manejar. ¿Tienes varios sensores del mismo tipo? Un array puede ser la solución. ¿Tienes un dispositivo complejo con múltiples atributos? Un struct será muy útil. Identificar estas necesidades temprano te ayudará a escribir un código más robusto desde el principio.

5. Galería de Videos Recomendados

Arduino Tutorial: Analog Input and Output (PWM)

Un tutorial visual que explica analogRead y analogWrite con ejemplos prácticos.

Bitwise Operators in C/C++ (for Arduino)

Una explicación detallada de las operaciones bit a bit y su aplicación en microcontroladores.


6. Referencias y Lecturas Recomendadas

  • Referencia del Lenguaje Arduino - Documentación oficial de analogRead, analogWrite, operadores, etc.
  • Schwartz, M. (2017). Arduino Cookbook. O'Reilly Media. (Capítulos sobre entradas/salidas analógicas y manipulación de bits)