PTENES
MÓDULO 2.4 · PRÁCTICA

🧵 Varias tareas al mismo tiempo

Un robot necesita leer sensores, corregir los motores, escuchar el celular y enviar datos, todo a la vez. Aquí cambias delay() por millis(), organizas el comportamiento en una máquina de estados y usas FreeRTOS de ESP32 para dividir el trabajo entre tareas y núcleos.

6
Temas
Intermedio
Nivel
Práctica
Tipo
ESP32
Hardware (Wokwi sirve)
0%
0 de 0 temas leídos · Sección 1 de 6

Contenido detallado

1

⏳ Por qué delay() bloquea el robot

En la Trilha 1, delay(500) parecía inofensivo: el LED parpadea y listo. Pero, durante esos 500 ms, el procesador no hace nada más. No lee el sensor de distancia, no corrige el motor ni detecta el botón de parada. Un rover a 0,5 m/s avanza 25 cm «con los ojos cerrados» por cada medio segundo de espera. Con un sensor y un LED esto pasa desapercibido; con Wi-Fi, PID y telemetría al mismo tiempo, el robot empieza a chocar, oscilar y perder comandos.

🌱 ¿Eres nuevo aquí?

Una función bloqueante solo devuelve el control al programa cuando termina. delay(), while esperando un botón y pulseIn() sin límite de tiempo son bloqueantes. Una función no bloqueante hace una pequeña parte del trabajo y termina enseguida, dejando que se ejecute el resto del código.

con delay() detenido: no ve nadadetenido: no ve nada con millis() 0 ms500 ms1000 ms

Cómo leer: cada barra azul es una vuelta del programa en la que lee sensores y decide. Arriba, el delay() deja solo dos intervalos por segundo; el resto (en rojo) es tiempo a ciegas. Abajo, el mismo programa sin espera ejecuta un intervalo corto tras otro y percibe el entorno todo el tiempo.

✓ Señales de código saludable

  • ✓O loop() da miles de vueltas por segundo.
  • ✓El botón de parada responde al instante, en cualquier momento.
  • ✓Esperas con límite de tiempo (pulseIn(pino, HIGH, 25000)).

✗ Señales de código bloqueante

  • ✗Al robot le «toma tiempo» reaccionar ante un obstáculo o el celular.
  • ✗while (digitalRead(botao) == HIGH) {} esperando a alguien.
  • ✗Los comandos Wi-Fi llegan con retraso o en ráfagas.

Por qué aprender

El PID del módulo 2.3 solo funciona si se ejecuta a un ritmo fijo, y el seguidor de línea del 2.6 necesita leer cinco sensores cientos de veces por segundo mientras envía telemetría. Si no eliminas los bloqueos, ninguna de estas partes funciona junto con las demás.

Conceptos clave

Bloqueante
retiene el procesador
Tiempo a ciegas
nadie mira el sensor
Latencia
demora hasta reaccionar
Límite de tiempo
espera con plazo
2

⏱️ millis() y tareas sin bloqueo

millis() devuelve cuántos milisegundos han pasado desde que se encendió la placa. En vez de esperar, tú miras el reloj: «¿Ya pasaron 500 ms desde la última vez que parpadeé? Entonces parpadeo y anoto la hora». Cada tarea tiene su propio registro de tiempo y su propio período. El loop() pasa por todas, hace solo lo que toca y sigue adelante.

LED · 500 mssensor · 20 msinforme · 1 s if (agora − ultimaVez ≥ periodo) { faz; ultimaVez = agora; }

Cómo leer: cada línea es una tarea con su propio ritmo. Las marcas muestran cuándo trabaja realmente. Ninguna espera a la otra: la misma comprobación de la última línea, repetida con distintos relojes, las coordina todas en el mismo loop().

Objetivo: ejecutar cuatro tareas en el mismo loop(), sin delay(), y medir cuántas vueltas da el loop por segundo. En Wokwi: ESP32 + LED en GPIO 2 + potenciómetro en GPIO 34.

multitarefa-millis.ino
// Cuatro tareas en el mismo loop, sin delay() (ESP32, paquete esp32 3.x)
const int PINO_LED = 2;      // El LED parpadea cada 500 ms
const int PINO_BOTAO = 0;    // botón BOOT de la placa (LOW = presionado)
const int PINO_POT = 34;     // potenciómetro (pin solo de entrada, ADC1)
unsigned long tLed = 0, tPot = 0, tSerial = 0, voltas = 0;   // vueltas: desde el último informe
bool ledLigado = false;
int leituraPot = 0, botaoAntes = HIGH;
void setup() {
  Serial.begin(115200);
  pinMode(PINO_LED, OUTPUT); pinMode(PINO_BOTAO, INPUT_PULLUP);
  Serial.println("Cuatro tareas sin delay: listo");
}
void loop() {
  unsigned long agora = millis();
  voltas++;
  if (agora - tLed >= 500) {          // tarea 1: parpadear
    tLed = agora; ledLigado = !ledLigado; digitalWrite(PINO_LED, ledLigado);
  }
  if (agora - tPot >= 20) {           // tarea 2: leer el sensor 50 veces por segundo (0 a 4095)
    tPot = agora; leituraPot = analogRead(PINO_POT);
  }
  if (agora - tSerial >= 1000) {      // tarea 3: informe
    tSerial = agora;
    Serial.printf("pot=%d | vueltas del loop en el ultimo segundo=%lu\n", leituraPot, voltas);
    voltas = 0;
  }
  int botao = digitalRead(PINO_BOTAO); // tarea 4: botón, en cualquier momento
  if (botaoAntes == HIGH && botao == LOW) Serial.println("Boton presionado");
  botaoAntes = botao;
}

Cómo verificar

  • ✓El LED parpadea una vez por segundo, siempre al mismo ritmo, mientras giras el potenciómetro.
  • ✓El informe muestra cientos de miles de vueltas por segundo y «Botao apertado» aparece al instante.
  • ✗Agrega un delay(500) al final del loop: las vueltas bajan a 2 y el botón empieza a fallar.

💡 ¿Y cuándo millis() «da la vuelta»?

El contador vuelve a cero después de unos 49 días. Si siempre comparas usando resta (ahora - tLed >= 500) y variables unsigned long, el cálculo sigue siendo correcto incluso cuando da la vuelta. Evita ahora >= tLed + 500: esa forma falla.

Por qué aprender

Es la técnica más sencilla y funciona en cualquier placa, incluidas ESP32-C3 y C6, que tienen un solo núcleo. Para la mayoría de los robots de la Trilha 2, un loop bien organizado con millis() es suficiente.

Conceptos clave

millis()
reloj desde el arranque
Período
cada cuánto
Flanco
el instante del cambio
unsigned long
sobrevive a la vuelta
3

🔀 Máquina de estados real

Cuando el comportamiento crece, las if se mezclan: «si está avanzando y vio una pared, pero no si ya está girando, a menos que...». La máquina de estados pone orden: el robot siempre está en un estado con nombre (ESPERANDO, ANDANDO, RECUANDO, GIRANDO); cada estado sabe qué hacer con los motores y qué eventos lo llevan al siguiente. El tiempo dentro del estado se mide con millis(), así que nada bloquea.

ESPERANDO ANDANDO RETROCEDIENDO GIRANDO botón dist < 20 cm 400 ms 350 ms botón(cualquier estado)

Cómo leer: cada cápsula es un estado y cada flecha azul es una transición con su condición escrita al lado. La flecha roja discontinua se aplica a todos: el botón es la parada que funciona en cualquier estado. Dibuja esto en papel antes de escribir el switch.

Objetivo: el rover de la Trilha 1 con la lógica para esquivar obstáculos reescrita como máquina de estados. Conexiones: TB6612FNG (PWMA 25, AIN1 26, AIN2 27, PWMB 14, BIN1 16, BIN2 17, STBY en 3V3), HC-SR04 (TRIG 13, ECHO 4 mediante divisor de tensión a 3,3 V), botón BOOT.

rover-estados.ino
// Rover con máquina de estados (ESP32 3.x + TB6612FNG + HC-SR04)
enum Estado { ESPERANDO, ANDANDO, RECUANDO, GIRANDO };
const char *NOMES[] = { "ESPERANDO", "ANDANDO", "RECUANDO", "GIRANDO" };
Estado estado = ESPERANDO;
unsigned long inicioEstado = 0, tSom = 0;
float dist = 999;
const int PWMA = 25, AIN1 = 26, AIN2 = 27;   // motor izquierdo
const int PWMB = 14, BIN1 = 16, BIN2 = 17;   // motor derecho
const int TRIG = 13, ECHO = 4, BOTAO = 0, VEL = 170;   // VEL: crucero (0 a 255)

void motor(int pwm, int in1, int in2, int v) {   // v de -255 a 255; ledcWrite recibe el pin (3.x)
  digitalWrite(in1, v > 0); digitalWrite(in2, v < 0); ledcWrite(pwm, abs(v));
}
void andar(int esq, int dir) { motor(PWMA, AIN1, AIN2, esq); motor(PWMB, BIN1, BIN2, dir); }
float distanciaCm() {
  digitalWrite(TRIG, LOW); delayMicroseconds(2); digitalWrite(TRIG, HIGH); delayMicroseconds(10); digitalWrite(TRIG, LOW);
  unsigned long us = pulseIn(ECHO, HIGH, 25000);   // máximo 25 ms de espera
  return us == 0 ? 999 : us / 58.0;                // 0 = nada adelante
}
void mudarPara(Estado novo) {
  estado = novo; inicioEstado = millis();
  Serial.printf("[%lu ms] -> %s\n", inicioEstado, NOMES[novo]);
}
bool botaoApertado() {                             // solo el flanco al presionar
  static int antes = HIGH;
  int agora = digitalRead(BOTAO);
  bool borda = (antes == HIGH && agora == LOW);
  antes = agora; return borda;
}
void setup() {
  Serial.begin(115200);
  pinMode(AIN1, OUTPUT); pinMode(AIN2, OUTPUT); pinMode(BIN1, OUTPUT); pinMode(BIN2, OUTPUT);
  ledcAttach(PWMA, 20000, 8); ledcAttach(PWMB, 20000, 8);   // pin, 20 kHz, 8 bits
  pinMode(TRIG, OUTPUT); pinMode(ECHO, INPUT); pinMode(BOTAO, INPUT_PULLUP);
  mudarPara(ESPERANDO);
}
void loop() {
  unsigned long noEstado = millis() - inicioEstado;
  bool botao = botaoApertado();
  if (millis() - tSom >= 60) { tSom = millis(); dist = distanciaCm(); }   // ping cada 60 ms
  if (botao && estado != ESPERANDO) { andar(0, 0); mudarPara(ESPERANDO); return; }
  switch (estado) {
    case ESPERANDO: andar(0, 0);         if (botao) mudarPara(ANDANDO);           break;
    case ANDANDO:   andar(VEL, VEL);     if (dist < 20) mudarPara(RECUANDO);      break;
    case RECUANDO:  andar(-VEL, -VEL);   if (noEstado > 400) mudarPara(GIRANDO);  break;
    case GIRANDO:   andar(VEL, -VEL);    if (noEstado > 350) mudarPara(ANDANDO);  break;
  }
}

Cómo verificar (primero con las ruedas en el aire)

  • ✓El Serial Monitor muestra cada cambio: [5123 ms] -> ANDANDO, después RECUANDO y GIRANDO al acercar la mano.
  • ✓Presiona BOOT para detener los motores de inmediato, en cualquier estado.
  • ✗¿Las ruedas giran al revés? Cambia los cables de ese motor o invierte el signo de v.

Dos errores comunes: un delay(400) dentro de RECUANDO (vuelve el tiempo ciego) y estados sin nombre (modo = 3), que nadie recuerda qué significan un mes después.

Por qué aprender

Los robots de competencia, las aspiradoras y Nav2 de la Trilha 4 piensan en estados. El registro de cambios se convierte en un diario del robot: cuando algo salga mal, lees en qué estado estaba y qué lo hizo cambiar.

Conceptos clave

Estado
qué está haciendo el robot
Transición
evento que cambia el estado
enum + switch
estados con nombre
Tiempo en el estado
millis() − inicio
4

🧠 FreeRTOS en el ESP32: tareas y núcleos

El ESP32 ya ejecuta un pequeño sistema operativo debajo de Arduino: FreeRTOS. Tu loop() es una tarea suya, en el núcleo 1; el Wi-Fi y el Bluetooth están en el núcleo 0. Puedes crear tus propias tareas, cada una con un for (;;) infinito, prioridad y núcleo. Cuando una tarea llama a vTaskDelay(), se duerme y el planificador le entrega el procesador a otra. Aquí la espera no es tiempo ciego.

🌱 ¿Eres nuevo aquí?

Un RTOS (sistema operativo en tiempo real) garantiza que la tarea más importante se ejecute cuando hace falta. Una tarea es una función que se ejecuta «en paralelo» con las demás. El planificador decide quién usa cada núcleo en cada instante, según la prioridad: un número mayor tiene preferencia.

Núcleo 0 Wi-Fi / BT tele Wi-Fi / BT duerme 500 ms Núcleo 1 ctrlctrlctrlctrl loop()loop()libre 10 ms

Cómo leer: el tiempo avanza de izquierda a derecha. En el núcleo 1, la tarea de control (ctrl) se activa cada 10 ms exactos y loop() usa el tiempo restante. En el núcleo 0, la telemetría (tele) comparte el espacio con el Wi-Fi. Separarlas así evita que un pico de red retrase el control de los motores.

Parámetro de xTaskCreatePinnedToCoreQué es
tarefaControlela función con el for (;;)
«control»nombre, aparece en los mensajes de error
4096pila en bytes en el ESP32 (en el FreeRTOS estándar se expresa en palabras)
NULLparámetro que se entrega a la función
2prioridad (el loop() tiene 1)
&hControle«referencia» para pausar o eliminar la tarea después
1núcleo: 0 o 1 (C3 y C6 solo tienen el 0)

Objetivo: una tarea de control a 100 Hz exactos en el núcleo 1 (lee el potenciómetro y ajusta el brillo del LED) y otra de telemetría en el núcleo 0. El mismo circuito del tema 2.

freertos-duas-tarefas.ino
// Dos tareas FreeRTOS en núcleos diferentes (ESP32 de dos núcleos, paquete esp32 3.x)
const int PINO_LED = 2, PINO_POT = 34;
TaskHandle_t hControle = NULL, hTelemetria = NULL;
volatile int brilho = 0;                  // escrito por el control, leído por la telemetría
volatile unsigned long ciclos = 0;
void tarefaControle(void *param) {
  TickType_t ultimo = xTaskGetTickCount();
  const TickType_t periodo = pdMS_TO_TICKS(10);   // 100 veces por segundo, sin deriva
  for (;;) {
    brilho = analogRead(PINO_POT) / 16;           // 0..4095 se convierte en 0..255
    ledcWrite(PINO_LED, brilho); ciclos++;
    vTaskDelayUntil(&ultimo, periodo);            // duerme hasta el siguiente tic de 10 ms
  }
}
void tarefaTelemetria(void *param) {
  for (;;) {
    Serial.printf("nucleo %d | brilho=%d | ciclos de controle=%lu\n", xPortGetCoreID(), brilho, ciclos);
    vTaskDelay(pdMS_TO_TICKS(500));               // cede el núcleo durante 500 ms
  }
}
void setup() {
  Serial.begin(115200);
  ledcAttach(PINO_LED, 5000, 8);
  // función, nombre, pila (bytes), parámetro, prioridad, referencia, núcleo
  xTaskCreatePinnedToCore(tarefaControle,   "controle",   4096, NULL, 2, &hControle,   1);
  xTaskCreatePinnedToCore(tarefaTelemetria, "telemetria", 4096, NULL, 1, &hTelemetria, 0);
  Serial.printf("setup rodou no nucleo %d\n", xPortGetCoreID());
}
void loop() { vTaskDelay(pdMS_TO_TICKS(1000)); }   // loop() también es una tarea; aquí solo descansa

Cómo verificar

  • ✓«setup rodou no nucleo 1» y, cada medio segundo, «nucleo 0 | brilho=... | ciclos de controle=...»
  • ✓Los ciclos aumentan cerca de 50 entre una línea y otra (100 por segundo).

💡 «Task watchdog got triggered»

Si una tarea entra en un for (;;) que nunca llama a vTaskDelay (ni espera una cola o un semáforo), no libera el núcleo, la tarea inactiva no se ejecuta y el perro guardián reinicia la placa. Toda tarea necesita dormir o esperar en algún punto del bucle.

Por qué aprender

Con Wi-Fi, MQTT y PID juntos, el control necesita un reloj al que la red no afecte. vTaskDelayUntil da exactamente eso: el mismo dt en cada ciclo, que es lo que supone el PID del módulo 2.3.

Conceptos clave

Tarea
función con su propio bucle
Prioridad
la más alta tiene preferencia
vTaskDelayUntil
período fijo, sin deriva
Watchdog
reinicia una tarea bloqueada
5

📬 Colas y semáforos: tareas que se comunican

Dos tareas que modifican la misma variable al mismo tiempo pueden leer un dato a medias. FreeRTOS ofrece dos sistemas seguros de comunicación. La cola (queue) entrega mensajes en orden: la tarea del sensor coloca una lectura y la del motor la retira; si la cola está vacía, la tarea que espera se duerme sin consumir procesador. El mutex es una llave única: solo quien la tiene puede usar el recurso (Serial, la tarjeta SD, el bus I2C) y luego la devuelve.

sensorcada 20 ms actuadorse activa al recibir un dato cola: 10 lugares, salen en el orden en que entraron Serial mutex: 1 llave

Cómo leer: abajo, los datos avanzan de izquierda a derecha por la cola (los cuadrados más claros ya están ocupados). Arriba, las líneas punteadas muestran que ambas tareas quieren usar Serial, pero solo quien tiene la llave escribe; la otra espera su turno.

Objetivo: la tarea del sensor envía lecturas por una cola; la del actuador ajusta el LED y, si pasan 100 ms sin datos, apaga todo (parada segura). Ambas escriben en Serial protegidas por un mutex.

fila-e-mutex.ino
// Cola (sensor -> actuador) y mutex (Serial compartida), FreeRTOS en el ESP32
struct Leitura { uint32_t t_ms; int valor; };
QueueHandle_t fila;
SemaphoreHandle_t travaSerial;
const int PINO_LED = 2, PINO_POT = 34;
void logSeguro(const char *texto) {
  if (xSemaphoreTake(travaSerial, pdMS_TO_TICKS(50)) == pdTRUE) {   // toma la llave
    Serial.println(texto);
    xSemaphoreGive(travaSerial);                                     // devuelve la llave
  }
}
void tarefaSensor(void *p) {
  for (;;) {
    Leitura l = { (uint32_t)millis(), analogRead(PINO_POT) };
    if (xQueueSend(fila, &l, 0) != pdTRUE) logSeguro("fila cheia: leitura descartada");
    vTaskDelay(pdMS_TO_TICKS(20));
  }
}
void tarefaAtuador(void *p) {
  Leitura l; bool alto = false; char msg[64];
  for (;;) {
    if (xQueueReceive(fila, &l, pdMS_TO_TICKS(100)) == pdTRUE) {    // duerme hasta que llegue un dato
      ledcWrite(PINO_LED, l.valor / 16);
      bool agoraAlto = l.valor > 3500;
      if (agoraAlto != alto) {                                       // avisa solo cuando hay un cambio
        alto = agoraAlto; snprintf(msg, sizeof msg, "%lu ms: valor %s (%d)", (unsigned long)l.t_ms, alto ? "alto" : "normal", l.valor);
        logSeguro(msg);
      }
    } else {
      ledcWrite(PINO_LED, 0);                                        // 100 ms sin datos: parada segura
      logSeguro("sem leitura: atuador parado");
    }
  }
}
void setup() {
  Serial.begin(115200);
  ledcAttach(PINO_LED, 5000, 8);
  fila =xQueueCreate(10, sizeof(Leitura));        // caben 10 lecturas
  travaSerial = xSemaphoreCreateMutex();
  xTaskCreatePinnedToCore(tarefaSensor,  "sensor",  3072, NULL, 2, NULL, 1);
  xTaskCreatePinnedToCore(tarefaAtuador, "atuador", 4096, NULL, 2, NULL, 1);
  logSeguro("fila e mutex prontos");
}
void loop() { vTaskDelay(pdMS_TO_TICKS(1000)); }

Cómo verificar

  • ✓El brillo del LED sigue el movimiento del potenciómetro; girarlo hasta el final muestra «valor alto» y volverlo atrás muestra «valor normal».
  • ✓Comenta la línea de xQueueSend: el LED se apaga y aparece «sem leitura: atuador parado». Es la parada segura del módulo 2.1, ahora entre tareas.

✓ Usa una cola cuando

  • ✓Una tarea produce datos y otra los consume (sensor → control, control → registro).
  • ✓Quieres que el consumidor duerma hasta que llegue algo, con un plazo.

✗ Trampas

  • ✗Tomar el mutex y olvidarse de liberarlo: las demás tareas quedan bloqueadas para siempre.
  • ✗Compartir una struct grande sin protección: el dato se lee a medias.

Por qué aprender

Así funcionará la telemetría del módulo 2.5 sin retrasar el control: el ciclo de control solo coloca un paquete en la cola y otra tarea, sin apuro, lo guarda en la tarjeta o lo envía por la red.

Conceptos clave

Cola
mensajes en orden
Mutex
una llave por recurso
Condición de carrera
dos modifican al mismo tiempo
Tiempo de espera
detecta el silencio
6

🗂️ Organizar el código en archivos y clases

Un sketch de 600 líneas en un solo archivo se convierte en un laberinto. Arduino IDE 2.x acepta varias pestañas en la misma carpeta: el .h dice qué puede hacer una pieza, el .cpp dice cómo. Una clase agrupa datos (los pines del motor) y funciones (girar, frenar). Crear dos objetos de la misma clase, uno para cada rueda, elimina la repetición.

Paso a paso en Arduino IDE 2.x

1

Guarda el sketch

Guárdalo como robo-organizado. La IDE crea la carpeta con el mismo nombre y el .ino dentro.

2

Crea las pestañas

En el menú de tres puntos junto a las pestañas, elige "Nueva pestaña" y crea Motor.h y luego Motor.cpp.

3

Pega los tres bloques y súbelos

Cada bloque de abajo va en la pestaña con el mismo nombre. La IDE compila todo junto. Haz la prueba con el robot suspendido, con las ruedas en el aire.

Motor.h
// Motor.h: un motor DC en un canal del puente H TB6612FNG
// (el DRV8833 no tiene un pin PWM separado: adapta velocidad() para PWM en IN1/IN2)
#pragma once
#include <Arduino.h>
class Motor {
public:
  Motor(int pinoPwm, int pinoIn1, int pinoIn2, bool invertido = false);
  void begin();                  // configura los pines y PWM (API 3.x)
  void velocidade(int v);        // de -255 (marcha atrás) a 255 (marcha adelante); 0 = libre
  void freio();                  // bloquea la rueda
  int atual() const { return _v; }
private:
  int _pwm, _in1, _in2, _v = 0;
  bool _inv;
};
Motor.cpp
#include "Motor.h"

Motor::Motor(int pinoPwm, int pinoIn1, int pinoIn2, bool invertido)
  : _pwm(pinoPwm), _in1(pinoIn1), _in2(pinoIn2), _inv(invertido) {}
void Motor::begin() {
  pinMode(_in1, OUTPUT); pinMode(_in2, OUTPUT);
  ledcAttach(_pwm, 20000, 8);    // 20 kHz (inaudible), 8 bits = 0..255
  velocidade(0);
}
void Motor::velocidade(int v) {
  v = constrain(v, -255, 255); _v = v;
  if (_inv) v = -v;              // motor montado en espejo
  digitalWrite(_in1, v > 0); digitalWrite(_in2, v < 0);
  ledcWrite(_pwm, abs(v));
}
void Motor::freio() {            // TB6612FNG: IN1 = IN2 = HIGH bloquea
  digitalWrite(_in1, HIGH); digitalWrite(_in2, HIGH); ledcWrite(_pwm, 255); _v = 0;
}
robo-organizado.ino
// robo-organizado.ino: el sketch principal queda corto y legible
#include "Motor.h"

Motor esquerdo(25, 26, 27);          // PWMA, AIN1, AIN2
Motor direito(14, 16, 17, true);     // PWMB, BIN1, BIN2 (montado en espejo)
const int SEQ[] = { 150, 0, -150, 0 };

void setup() {
  Serial.begin(115200);
  esquerdo.begin();
  direito.begin();
  Serial.println("Motores listos: haz la prueba con las ruedas en el aire");
}

void loop() {
  for (int v : SEQ) {
    esquerdo.velocidade(v);
    direito.velocidade(v);
    Serial.printf("izq=%d der=%d\n", esquerdo.atual(), direito.atual());
    delay(1000);   // aquí está bien: es una prueba de banco, no el robot en movimiento
  }
}

Cómo verificar

  • ✓Las dos ruedas giran hacia adelante, se detienen, giran hacia atrás y se detienen, cada segundo, en la misma dirección.
  • ✗¿Una rueda gira al revés? Cambia el true de lado en el constructor, sin tocar ningún cable.

Por qué aprender

El código organizado te permite volver al robot después de un mes, dividir el trabajo en grupo o cambiar el puente H sin reescribirlo todo. Una pestaña por pieza (Motor, BarraIR, Pid, Telemetria) y el .ino solo conecta las piezas: es el mismo formato de las bibliotecas que instalas desde la IDE. Guarda la carpeta en un repositorio Git y haz un commit por cada ajuste que funcione.

Conceptos clave

.h
qué hace la pieza
.cpp
cómo lo hace
Clase
datos + funciones juntos
Objeto
una rueda, un motor

✅ Resumen del módulo

✓
delay() crea tiempo ciego: mientras espera, el robot no siente ni reacciona.
✓
millis() con resta permite encajar varias tareas en el mismo loop, cada una a su propio ritmo.
✓
Máquina de estados: enum + switch + tiempo en el estado + una parada que siempre se aplica.
✓
FreeRTOS: control en el núcleo 1 con vTaskDelayUntil; red y telemetría en el núcleo 0.
✓
Cola y mutex pasan datos entre tareas sin condiciones de carrera; el tiempo de espera en la cola activa una parada segura.

Para profundizar

Siguiente módulo

2.5 · Telemetría y registro de datos: ver lo que el robot siente y decide, en vivo y en un archivo.