Integración del Protocolo FreeModbus TCP sobre el Stack de Red uIP

Arquitectura del Sistema Embebido

El diseño del sistema se fundamenta en una topología de hardware de bajo consumo y alta eficiencia, ideal para aplicaciones industriales IoT. La plataforma objetivo combina un microcontrolador de alto rendimiento con un controlador Ethernet externo.

Especificaciones de Hardware

  • Unidad de Procesamiento: Microcontrolador STM32F407ZG basado en ARM Cortex-M4, encargado de la lógica de aplicación y el manejo de la capa MAC.
  • Interfaz de Red: Controlador Ethernet ENC28J60 operando a través del bus SPI, soportando velocidades de 10/100 Mbps.
  • Recursos de Memoria: 1 MB de memoria Flash para el almacenamiento del firmware y 192 KB de SRAM para las pilas de protocolos y búferes de datos.
  • Gestión Energética: Regulador LDO de 3.3V optimizado para minimizar la disipación térmica.

Flujo de Capas del Protocolo

La pila de comunicación sigue un modelo estrictamente escalonado: la capa física es gestionada por el ENC28J60, el stack uIP procesa las conexiones TCP/IP, FreeModbus encapsula y decodifica las tramas de aplicación, y finalmente la lógica del usuario responde a las peticiones Modbus TCP en el puerto estándar 502.

Proceso de Adaptación del Stack de Red

Arranque del Subsistema de Red

La inicialización encapsula la configuración del hardware y el levantamiento del stack TCP/IP en una única rutina de bootstrap.

#include "uip.h"
#include "enc28j60_driver.h"

void red_inicializar_sistema(const uint8_t *mac, const uint8_t *ip) {
    // Configuración física y enlace de datos
    eth_driver_init(mac);
    phy_escribir_registro(PHLCON, 0x0476); 
    
    // Inicialización de capas superiores (IP, ARP, TCP)
    uip_ipaddr_t addr;
    uip_ipaddr(&addr, ip[0], ip[1], ip[2], ip[3]);
    uip_sethostaddr(&addr);
    
    // Apertura de puerto para tráfico Modbus
    uip_listen(HTONS(502));
    uip_set_appcall(gestionar_eventos_modbus);
}

Capa de Abstracción de Puerto para FreeModbus

Para integrar FreeModbus, es necesario implementar las funciones de la API del puerto. A continuación, se muestra una implementación optimizada que utiliza variables de estado para el manejo de los búferes.

static uint8_t buffer_peticion_tcp[512];
static uint16_t longitud_trama_actual = 0;
static bool hay_datos_pendientes = false;

BOOL xMBTCPPortInit(USHORT usTCPPort) {
    // Forzar el puerto estándar si se recibe un valor nulo
    uint16_t puerto_objetivo = (usTCPPort == 0) ? 502 : usTCPPort;
    memset(buffer_peticion_tcp, 0, sizeof(buffer_peticion_tcp));
    return TRUE;
}

BOOL xMBTCPPortGetRequest(UCHAR **ppucMBTCPFrame, USHORT *usTCPLength) {
    if (!hay_datos_pendientes) {
        return FALSE;
    }
    *ppucMBTCPFrame = buffer_peticion_tcp;
    *usTCPLength = longitud_trama_actual;
    hay_datos_pendientes = false; // Resetear bandera tras la lectura
    return TRUE;
}

BOOL xMBTCPPortSendResponse(const UCHAR *pucMBTCPFrame, USHORT usTCPLength) {
    // Inyectar directamente la respuesta en el búfer de salida de uIP
    if (usTCPLength <= UIP_APPDATA_SIZE) {
        memcpy(uip_appdata, pucMBTCPFrame, usTCPLength);
        uip_send(uip_appdata, usTCPLength);
        return TRUE;
    }
    return FALSE;
}

Callback de Eventos de Aplicación

El stack uIP notifica los eventos de red a la aplicación a través de un callback. Aquí se mapean dichos eventos al motor de eventos de FreeModbus.

typedef enum {
    TCP_DESCONECTADO,
    TCP_CONECTADO,
    TCP_LISTO
} EstadoTCP;

static EstadoTCP estado_tcp = TCP_DESCONECTADO;

void gestionar_eventos_modbus(void) {
    if (uip_connected()) {
        estado_tcp = TCP_CONECTADO;
    }
    
    if (uip_newdata()) {
        // Copiar payload entrante y notificar al stack Modbus
        longitud_trama_actual = uip_len;
        memcpy(buffer_peticion_tcp, uip_appdata, uip_len);
        hay_datos_pendientes = true;
        xMBPortEventPost(EV_FRAME_RECEIVED);
    }
    
    if (uip_poll() && estado_tcp == TCP_LISTO) {
        // Lógica para mantener conexiones activas o enviar respuestas diferidas
        estado_tcp = TCP_CONECTADO;
    }
    
    if (uip_closed() || uip_aborted() || uip_timedout()) {
        estado_tcp = TCP_DESCONECTADO;
    }
}

Manejo de Tramas y Cabeceras MBAP

El protocolo Modbus TCP utiliza una cabecera MBAP (Modbus Application Protocol) de 7 bytes. En lugar de manipular bytes individuales con desplazamientos de bits, se utiliza una estructura empaquetada para mejorar la legibilidad y el mantenimiento del código.

typedef struct __attribute__((packed)) {
    uint16_t id_transaccion;
    uint16_t id_protocolo;
    uint16_t longitud;
    uint8_t  id_unidad;
} CabeceraMBAP;

void construir_respuesta_mbap(uint8_t *buffer_salida, uint16_t tx_id, uint8_t unidad, uint16_t pdu_len) {
    CabeceraMBAP *cabecera = (CabeceraMBAP *)buffer_salida;
    cabecera->id_transaccion = htons(tx_id);
    cabecera->id_protocolo = htons(0x0000); // 0x0000 identifica a Modbus
    cabecera->longitud = htons(pdu_len + 1); // Longitud = ID Unidad + PDU
    cabecera->id_unidad = unidad;
}

void procesar_ciclo_modbus_tcp(void) {
    static uint8_t trama_interna[512];
    uint16_t tamano_rx = 0;
    uint8_t *puntero_rx = NULL;
    
    if (xMBTCPPortGetRequest(&puntero_rx, &tamano_rx) && tamano_rx >= 7) {
        CabeceraMBAP *cabecera_rx = (CabeceraMBAP *)puntero_rx;
        uint16_t longitud_pdu = ntohs(cabecera_rx->longitud) - 1;
        
        // Procesar la carga útil (PDU) omitiendo los 7 bytes de la cabecera
        eMBErrorCode error = eMBTCPReceive(puntero_rx + 7, longitud_pdu);
        
        if (error == MB_ENOERR) {
            // Preparar cabecera de respuesta reutilizando el ID de transacción
            construir_respuesta_mbap(trama_interna, ntohs(cabecera_rx->id_transaccion), 
                                     cabecera_rx->id_unidad, longitud_pdu);
            
            // Anexar datos de respuesta generados por FreeModbus
            memcpy(trama_interna + 7, buffer_respuesta_pdu, longitud_pdu);
            
            xMBTCPPortSendResponse(trama_interna, 7 + longitud_pdu);
        }
    }
}

Validación y Diagnóstico de Red

Análisis de Tráfico

La validación del protocolo se realiza mediante herramientas de captura de paquetes. Un intercambio exitoso de lectura de registros (Función 0x03) presenta el siguiente flujo:

No. | Tiempo      | Origen          | Destino         | Protocolo | Información
----|-------------|-----------------|-----------------|-----------|-------------------------
 1  | 0.000000000 | 10.0.0.50       | 10.0.0.100      | TCP       | 49152 → 502 [SYN]
 2  | 0.000112300 | 10.0.0.100      | 10.0.0.50       | TCP       | 502 → 49152 [SYN, ACK]
 3  | 0.000210400 | 10.0.0.50       | 10.0.0.100      | TCP       | 49152 → 502 [ACK]
 4  | 0.001050000 | 10.0.0.50       | 10.0.0.100      | Modbus    | Transacción: 1, Unidad: 1, Lectura (0x03)
 5  | 0.001250000 | 10.0.0.100      | 10.0.0.50       | Modbus    | Transacción: 1, Unidad: 1, Respuesta Lectura

Matriz de Resolución de Problemas

Síntoma Acción Correctiva
Timeout en el establecimiento de conexión TCP Verificar la configuración de la subred, puerta de enlace y reglas de firewall en el equipo maestro.
Retransmisiones TCP excesivas Ajustar el tempoirzador de retransmisión (UIP_RTO) en uipopt.h para adaptarse a latencias de red altas.
Latencia inaceptable en respuestas Elevar la prioridad de la interrupción del SPI/MAC y evitar bloqueos en el bucle principal del microcontrolador.
Tramas descartadas por error de formato Asegurar que el campo length de la cabecera MBAP coincida exactamente con el tamaño del PDU + 1 byte.

Características Avanzadas

Enrutamiento Multi-Esclavo

En gateways Modbus, es común que una única conexión TCP represente a múltiples dispositivos físicos. Esto se gestiona mediante tablas de enrutamiento basadas en el ID de unidad.

typedef struct {
    uint8_t id_esclavo;
    uint16_t direccion_base;
    uint16_t limite_registros;
} MapaEsclavo;

static const MapaEsclavo topologia_red[] = {
    { .id_esclavo = 1, .direccion_base = 0x0000, .limite_registros = 100 },
    { .id_esclavo = 2, .direccion_base = 0x0100, .limite_registros = 50  }
};

bool enrutar_peticion_esclavo(uint8_t unidad_solicitada, uint16_t *offset) {
    for (size_t i = 0; i < sizeof(topologia_red) / sizeof(MapaEsclavo); i++) {
        if (topologia_red[i].id_esclavo == unidad_solicitada) {
            *offset = topologia_red[i].direccion_base;
            return true;
        }
    }
    return false; // Esclavo desconocido
}

Implementación de Seguridad con MbedTLS

Para entornos industriales expuestos, se puede encapsular el tráfico Modbus TCP dentro de un túnel TLS (Modbus Security).

#include "mbedtls/ssl.h"

int inicializar_contexto_seguro(mbedtls_ssl_context *ssl, mbedtls_ssl_config *cfg) {
    mbedtls_ssl_init(ssl);
    mbedtls_ssl_config_init(cfg);
    
    int status = mbedtls_ssl_config_defaults(cfg, 
                                             MBEDTLS_SSL_IS_SERVER,
                                             MBEDTLS_SSL_TRANSPORT_STREAM,
                                             MBEDTLS_SSL_PRESET_DEFAULT);
    if (status != 0) {
        return status;
    }
    return mbedtls_ssl_setup(ssl, cfg);
}

void enviar_trama_cifrada(mbedtls_ssl_context *ssl, const uint8_t *datos, size_t longitud) {
    uint8_t buffer_cifrado[1024];
    // En una implementación real, manejar fragmentación y tamaños de buffer
    int bytes_cifrados = mbedtls_ssl_write(ssl, datos, longitud);
    if (bytes_cifrados > 0) {
        uip_send(buffer_cifrado, bytes_cifrados);
    }
}

Optimización del Entorno de Desarrollo

La configuración del compilador y la gestión de memoria son críticas debido a las limitaciones de recursos en microcontroladores.

  • Directivas del Compilador: Utilizar niveles de optimización altos (-O2 o -Os en GCC/IAR) enfocados en tamaño. Definir macros como -DUIP_CONF_BUFFER_SIZE=1024 para limitar el uso de RAM del stack TCP.
  • Gestión de Memoria Estática: Evitar el uso de malloc() dinámico. Pre-asignar pools de memoria estáticos para los búferes de red y las estructuras de control de FreeModbus.
  • Telemetría de Depuración: Implementar macros de registro condicional que puedan ser desactivadas en producción para ahorrar ciclos de CPU y espacio en Flash.
#define HABILITAR_LOG_MODBUS 1

#if HABILITAR_LOG_MODBUS
    #include <stdio.h>
    #define LOG_DEBUG(fmt, ...) printf("[MODBUS] " fmt "\n", ##__VA_ARGS__)
#else
    #define LOG_DEBUG(fmt, ...) ((void)0)
#endif

// Ejemplo de asignación estática
#define TAMANO_POOL_MEMORIA 4096
static uint8_t pool_memoria[TAMANO_POOL_MEMORIA];

void sistema_init_memoria(void) {
    mem_init(pool_memoria, TAMANO_POOL_MEMORIA);
}

Etiquetas: FreeModbus uIP Modbus TCP STM32F4 ENC28J60

Publicado el 6-20 20:50