Un banco HIL se compone de tres actores fundamentales:
pytest) corriendo en tu computadora. Este script le ordena al Simulador: "Inyecta 4000 pulsos a 100 Hz y dime cuánto tardó el DUT en cerrar la válvula".Ayer vimos que nuestro sistema lee el Caudalímetro mediante un ADC (para detectar si el cable está cortado). El Simulador HIL no puede generar pulsos digitales (0V - 3.3V) cuadrados simplemente. Para engañar al STM32, el Simulador debe inyectar voltajes analógicos (1.65V para Reposo y 3.3V para Sabotaje) usando un DAC (Conversor Digital a Analógico) o un PWM filtrado.
Circuito de Pruebas: Tomaremos un Arduino o ESP32 (el Simulador). Conectaremos su pin DAC (Salida Analógica) al pin ADC del STM32. Conectaremos un pin digital de lectura del Simulador a la salida del Relevador de la Válvula del STM32. Conectaremos el Simulador por USB a nuestra computadora.
Ejercicio 1: El Script de Python (Test Runner). Escribiremos una prueba automatizada usando `pytest` y `pyserial`. Este script exige una prueba extrema: simular el corte del cable (Día 240) y verificar que el código MISRA-C del STM32 apague la válvula en menos de 20 milisegundos.
# =========================================================================
# test_hil_tamper.py (Test Runner corriendo en la PC del Arquitecto)
# =========================================================================
import serial
import time
import pytest
# Conectar al Simulador (Ej. un ESP32 inyector conectado por USB)
# El Simulador recibe comandos de texto y genera señales eléctricas reales.
simulador_hil = serial.Serial('COM3', 115200, timeout=1)
def mandar_comando_simulador(comando):
simulador_hil.write(f"{comando}\n".encode('utf-8'))
respuesta = simulador_hil.readline().decode('utf-8').strip()
return respuesta
def test_tamper_cable_cortado_tiempo_reaccion():
"""
PRUEBA HIL 01: Verificación de Latencia de Cierre ante Sabotaje EOL.
Requisito SIL-3: El sistema debe cerrar la válvula en < 20ms tras el corte.
"""
# 1. Preparar el Escenario (Mundo Físico)
# Ordenamos al Simulador inyectar 1.65V (Estado de Reposo Normal)
mandar_comando_simulador("SET_DAC_VOLTAGE 1.65")
time.sleep(1) # Dejar que el STM32 se estabilice
# Asegurarnos de que la válvula está abierta (Simulando un despacho en curso)
# Le decimos al simulador que engañe al STM32 para abrir la válvula (simular teclado)
mandar_comando_simulador("SIMULATE_BUTTON_START")
time.sleep(0.5)
estado_valvula = mandar_comando_simulador("READ_VALVE_PIN")
assert estado_valvula == "HIGH", "El STM32 no abrió la válvula. Falla inicial."
# 2. INYECTAR EL FALLO FÍSICO (El Sabotaje)
# Ordenamos al simulador subir el voltaje a 3.3V instantáneamente (Corte de cable)
# Al mismo tiempo, le pedimos al simulador que mida cuánto tarda el STM32
# en tirar a "LOW" el pin de la válvula. Todo esto en microsegundos de hardware.
print("\n[HIL] Inyectando Sabotaje (Voltaje > 3.0V). Midiendo latencia de corte...")
respuesta_tiempo = mandar_comando_simulador("INJECT_TAMPER_AND_MEASURE")
# La respuesta del simulador será algo como "VALVE_CLOSED_IN_MS:12"
assert "VALVE_CLOSED_IN_MS" in respuesta_tiempo, "El STM32 jamás cerró la válvula!"
tiempo_reaccion_ms = int(respuesta_tiempo.split(":")[1])
print(f"[HIL] El STM32 cerró la válvula en {tiempo_reaccion_ms} ms.")
# 3. VERIFICACIÓN CRÍTICA (Assert)
# Verificamos que el código MISRA-C y el RTOS cumplieron el presupuesto de latencia
assert tiempo_reaccion_ms < 20, f"Peligro: Latencia de {tiempo_reaccion_ms}ms supera límite SIL."
/**
* @file simulador_hil_firmware.ino (Código que corre en el Arduino/ESP32 Simulador)
* @brief Escucha comandos USB de Python y genera voltajes para engañar al STM32.
*/
// ... (Setup Serial) ...
void loop() {
if (Serial.available()) {
String cmd = Serial.readStringUntil('\n');
if (cmd.startsWith("SET_DAC_VOLTAGE")) {
// ... (Configurar el pin DAC para inyectar 1.65V)
Serial.println("OK");
}
if (cmd.equals("INJECT_TAMPER_AND_MEASURE")) {
// 1. Tomar el tiempo actual
unsigned long t_inicio = millis();
// 2. Inyectar el voltaje de sabotaje (3.3V directo al ADC del STM32)
// Engañamos físicamente al cable.
dacWrite(PIN_DAC_SALIDA, 255);
// 3. Quedarnos bloqueados esperando la reacción del STM32 (Bucle While Rápido)
while(digitalRead(PIN_LECTURA_VALVULA_STM32) == HIGH) {
if (millis() - t_inicio > 1000) {
Serial.println("TIMEOUT_FATAL");
return;
}
}
// 4. El STM32 tiró el pin a LOW. Calculamos la latencia.
unsigned long latencia = millis() - t_inicio;
// 5. Respondemos a Python
Serial.print("VALVE_CLOSED_IN_MS:");
Serial.println(latencia);
}
}
}
Objetivo del día: Certificación Continua (CI/CD Hardware) y Trazabilidad de Requerimientos.
¿Qué pasa si mañana otro ingeniero modifica la Tarea del Caudalímetro en el RTOS y, por error, le pone un osDelay(50) en lugar de 1ms? A simple vista, la máquina seguirá despachando gas y el ingeniero subirá el código (Commit). Pero en la vida real, ese retraso de 50ms podría significar que el gas se escape durante medio segundo más tras un sabotaje. En la industria, no puedes confiar en la vista humana para cazar estos errores lógicos.
Al construir un **Banco HIL (Hardware-In-the-Loop)**, acabas de implementar Integración Continua física. Ahora, cada vez que compilas tu código MISRA-C en el STM32, conectas el banco y ejecutas un script de Python en tu computadora. En 30 segundos, el banco bombardea la máquina con 50 escenarios destructivos: cortes de cable, ráfagas de 1000 litros por minuto (Overrun de interrupciones), caídas de voltaje simuladas y golpes de ariete falsos. Python verifica matemáticamente que el STM32 haya reaccionado exactamente como lo dicta la norma de seguridad. Tu Tesis ya no es un proyecto "que parece que funciona"; es una plataforma certificada por un framework de automatización auditable.
pytest en Python, que orquesta la prueba enviando comandos secuenciales de alto nivel al inyector.