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 (
-O2o-Osen GCC/IAR) enfocados en tamaño. Definir macros como-DUIP_CONF_BUFFER_SIZE=1024para 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);
}