Do Modelo ao Hardware: MIL → SIL → HIL em Três Etapas
· Leitura de 3 minutos

MIL → SIL → HIL é a progressão padrão do modelo-V para controle embarcado: simule tudo primeiro, depois substitua o modelo da planta por hardware real uma camada de cada vez. Com Synapsys, a transição é uma troca de uma linha porque a camada de transporte é totalmente abstraída do algoritmo.
As três etapas
| Etapa | Planta | Controlador | Transporte | Objetivo |
|---|---|---|---|---|
| MIL | Simulada (StateSpace) | Código do algoritmo | Memória compartilhada | Iteração rápida, testes unitários |
| SIL | Simulada | Binário compilado / processo externo | ZeroMQ | Testes de integração, perfil de latência |
| HIL | Dispositivo real | Código do algoritmo ou firmware MCU | HardwareInterface | Testes de aceitação na planta real |
Etapa 1 — MIL: tudo em um processo
from synapsys.api import ss, c2d
from synapsys.agents import PlantAgent, ControllerAgent, SyncEngine, SyncMode
from synapsys.algorithms import PID
from synapsys.transport import SharedMemoryTransport
import numpy as np
planta_d = c2d(ss([[-1]], [[1]], [[1]], [[0]]), dt=0.01)
pid = PID(Kp=4.0, Ki=1.0, dt=0.01)
def lei(y: np.ndarray) -> np.ndarray:
return np.array([pid.compute(setpoint=3.0, measurement=y[0])])
with SharedMemoryTransport("demo", {"y": 1, "u": 1}, create=True) as bus:
bus.write("y", np.zeros(1))
bus.write("u", np.zeros(1))
sync = SyncEngine(SyncMode.LOCK_STEP, dt=0.01)
PlantAgent("planta", planta_d, bus, sync).start(blocking=False)
ControllerAgent("ctrl", lei, bus, sync).start(blocking=True)
Etapa 2 — SIL: dois processos via ZeroMQ
A função lei não muda. Apenas o transporte é trocado:
- processo_planta.py
- processo_controlador.py
from synapsys.api import ss, c2d
from synapsys.agents import PlantAgent, SyncEngine, SyncMode
from synapsys.transport import ZMQTransport
planta_d = c2d(ss([[-1]], [[1]], [[1]], [[0]]), dt=0.01)
pub = ZMQTransport("tcp://*:5555", mode="pub")
sub = ZMQTransport("tcp://localhost:5556", mode="sub")
sync = SyncEngine(SyncMode.WALL_CLOCK, dt=0.01)
PlantAgent("planta", planta_d, pub, sync, sub_transport=sub).start(blocking=True)
from synapsys.agents import ControllerAgent, SyncEngine, SyncMode
from synapsys.algorithms import PID
from synapsys.transport import ZMQTransport
import numpy as np
pid = PID(Kp=4.0, Ki=1.0, dt=0.01)
def lei(y: np.ndarray) -> np.ndarray:
return np.array([pid.compute(setpoint=3.0, measurement=y[0])])
sub = ZMQTransport("tcp://localhost:5555", mode="sub")
pub = ZMQTransport("tcp://*:5556", mode="pub")
sync = SyncEngine(SyncMode.WALL_CLOCK, dt=0.01)
ControllerAgent("ctrl", lei, sub, sync, pub_transport=pub).start(blocking=True)
Etapa 3 — HIL: hardware real
from synapsys.agents import HardwareAgent, SyncEngine, SyncMode
from synapsys.hw import HardwareInterface
from synapsys.transport import ZMQTransport
import numpy as np
class MinhaInterfaceDAQ(HardwareInterface):
def __init__(self):
super().__init__(n_inputs=1, n_outputs=1)
def read_outputs(self, timeout_ms: float = 100.0) -> np.ndarray:
# return np.array([self.daq.read_channel(0)])
return np.array([0.0]) # stub
def write_inputs(self, u: np.ndarray, timeout_ms: float = 100.0) -> None:
# self.daq.write_channel(0, float(u[0]))
pass
sub = ZMQTransport("tcp://localhost:5556", mode="sub")
pub = ZMQTransport("tcp://*:5555", mode="pub")
sync = SyncEngine(SyncMode.WALL_CLOCK, dt=0.01)
HardwareAgent("hw", MinhaInterfaceDAQ(), pub, sync, sub_transport=sub).start(blocking=True)
O processo controlador não muda. A troca é cirúrgica.
Resumo
| Etapa | O que muda |
|---|---|
| MIL → SIL | SharedMemoryTransport → ZMQTransport, dividir em dois processos |
| SIL → HIL | PlantAgent → HardwareAgent(MinhaInterfaceDAQ()) |
| Algoritmo de controle | Não muda |
Veja o exemplo SIL completo em
examples/advanced/02_sil_ai_controller/.
