modbus_tcp_esclave.ino
Opta se convierte en esclavo Modbus/TCP accesible desde cualquier SCADA (Node-RED, autómatas comerciales). El sketch responde a las funciones 1, 3 y 5 con los relés como bobinas, trama MBAP construida a mano, cero librerías.
Hardware
Una Opta PLC de STMicroelectronics (STM32H747 en tiempo real + coprocesador seguro i.MX RT1052) programada con el entorno Arduino PLC, alimentada a 24 V DIN. Opta en la red Ethernet (puerto switch integrado), un maestro Modbus/TCP para probar (modbus-cli, Node-RED, SCADA).
Cableado
Cable RJ45 a un puerto Ethernet de Opta; fijar IP estática coherente con la red industrial (evitar DHCP en taller).
Funcionamiento
El servidor escucha en el puerto 502, decabeza el MBAP y luego la PDU: FC3 lee registros, FC1 bobinas, FC5 manda a los relés. Toda trama inválida devuelve una excepción Modbus.
Ejecución
Instala el gestor de placas Arduino PLC, selecciona «Opta PLC», conecta USB/DFU y vuelca el programa. El sketch corre por el STM32H747 en tiempo real.
Variantes
Añadir FC16 para que el SCADA escriba registros, o exponer la entrada analógica AI1 como entrada discreta (FC2) para teledetección.
modbus_tcp_esclave.ino
// Opta PLC : esclave Modbus/TCP minimal (FC1 bobines, FC3 registres, FC5).
// Trame MBAP+PDU construite à la main : aucune dépendance externe.
#include "PLCContainer.h"
#include <Ethernet.h>
PLCContainer PLC;
EthernetServer serveur(502); // port standard Modbus/TCP
uint16_t registres[4] = { 0, 0, 0, 0 }; // 4 registres holding
bool bobines[2] = { false, false }; // 2 bobines = RO1, RO2
void setup() {
Serial.begin(115200);
PLC.begin();
byte mac[] = { 0x00, 0x80, 0xE1, 0x14, 0x22, 0x33 };
IPAddress ip(192, 168, 1, 60), gw(192, 168, 1, 1), dns(192, 168, 1, 1);
PLC.Eth.begin(mac, ip, dns, gw);
serveur.begin();
Serial.print("Modbus/TCP sur "); Serial.println(ip);
}
void loop() {
EthernetClient c = serveur.available();
if (!c) { delay(5); return; }
uint8_t trame[256];
int ln = c.readBytes((char*)trame, 12); // entete MBAP (12 octets)
if (ln == 12) {
int len = (trame[4] << 8 | trame[5]) - 2; // longueur du PDU
ln = c.readBytes((char*)trame + 12, len);
uint8_t fc = trame[12];
uint16_t ad = trame[13] << 8 | trame[14];
uint16_t qte = trame[15] << 8 | trame[16];
uint8_t rep[256];
int rl = 0;
if (fc == 0x03 && ad + qte <= 4) { // lire holding registers
rep[rl++] = fc; rep[rl++] = qte * 2;
for (int i = 0; i < qte; i++) {
registres[i] = bobines[i] ? 1 : 0; // miroir des sorties
rep[rl++] = registres[ad + i] >> 8; rep[rl++] = registres[ad + i] & 0xFF;
}
} else if (fc == 0x01 && ad + qte <= 2) { // lire bobines
rep[rl++] = fc; rep[rl++] = 1; rep[rl++] = bobines[0] | (bobines[1] << 1);
rl += 0;
} else if (fc == 0x05) { // écrire bobine unique
bobines[ad] = (trame[15] == 0xFF);
if (ad < 2) bobines[ad] ? PLC.Relay[ad].on() : PLC.Relay[ad].off();
memcpy(rep, trame + 12, 4); rl = 4; // écho
} else { fc |= 0x80; rep[rl++] = fc; rep[rl++] = 0x01; }
rep[rl++] = trame[0]; rep[rl++] = trame[1]; // réponse MBAP
rep[rl++] = 0; rep[rl++] = 0; rep[rl++] = 0; rep[rl++] = rl - 4 + 2;
c.write(rep, rl);
}
c.stop();
}