modbus_tcp_esclave.ino

Arduino Opta (micro PLC, STM32H747) · Archivo : modbus_tcp_esclave.ino · Lenguaje : Arduino · Uso : Servir des registres de la PLC en Modbus/TCP

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();
}

← Volver a la ficha