Word swap: el float que venía del revés

Modbus 5 min de lectura

Un analizador de red nuevo, un mapa de registros en la mano, media hora de trabajo. La lectura de potencia activa devolvía 1.7e38. La siguiente, 3.4e-41. El sensor estaba perfectamente: lo que estaba del revés era el float.

Un número de 32 bits en registros de 16

Modbus es un protocolo de registros de 16 bits. Un float IEEE 754 de precisión simple ocupa 32. La consecuencia es inevitable: cada medida viaja partida en dos registros consecutivos, y el estándar no dice en qué orden. Cada fabricante eligió el suyo.

Con dos decisiones independientes —el orden de los bytes dentro de cada registro y el orden de las dos palabras entre sí— salen cuatro combinaciones posibles, y solo una da el número correcto:

Registros leídos:  0x0000  0x43C8

big-endian, MSW primero (ABCD)  ->  4.59e-41   descartado
big-endian, LSW primero (CDAB)  ->  400.0      <-- este
little-endian, MSW (BADC)       ->  1.13e-40   descartado
little-endian, LSW (DCBA)       ->  -3.03e+35  descartado

El caso de arriba es real y es el que usan los analizadores de red de Carlo Gavazzi de la serie WM: bytes en big-endian, palabras con la menos significativa primero. Es lo que las bibliotecas suelen llamar word swap. En Python, sin dependencias:

import struct

def leer_float(registros):
    """Dos registros Modbus -> float IEEE 754.

    Byte order big-endian, word order LSW primero (word swap).
    Cambiar esto rompe TODAS las medidas del equipo, no una.
    """
    bajo, alto = registros           # el primero es la palabra baja
    crudo = struct.pack(">HH", alto, bajo)
    return struct.unpack(">f", crudo)[0]

Cómo se averigua sin adivinar

Probar las cuatro y quedarse con la que «parece bien» funciona, pero solo si se elige la medida adecuada como referencia. Y aquí es donde casi todo el mundo se equivoca de variable.

La referencia buena es la tensión. Está siempre presente, su magnitud se conoce de antemano dentro de un pequeño margen (unos 230 V entre fase y neutro, unos 400 V entre fases) y ninguna de las tres decodificaciones incorrectas produce por casualidad un número en ese entorno. Decodificas las cuatro, una da 400 y las otras dan basura: no hay ambigüedad posible.

La referencia mala —y la tentadora— es la potencia. Si haces la prueba con la planta parada, la potencia vale cero, y cero se decodifica igual de bien en los cuatro órdenes. Todos los bytes son ceros: cualquier permutación de ceros sigue siendo cero. La prueba pasa, te vas a casa, y el fallo aparece la primera vez que la planta arranca.

Regla. El orden de palabras se valida contra una magnitud conocida y distinta de cero. Validar con el equipo parado no valida nada.

El signo es otra decisión, y no es del driver

Superado el orden de palabras, queda la segunda trampa. La potencia activa en el punto de conexión tiene signo, y el criterio es una convención: positivo importando, negativo exportando. Salvo que los transformadores de intensidad estén montados al revés, cosa que ocurre con una frecuencia notable y que no siempre se puede corregir físicamente sin cortar el suministro.

La tentación es arreglarlo en el driver, que es donde está el struct.unpack. Es un error. El driver está validado en otras instalaciones y es el mismo fichero en todas: meterle el matiz de una planta concreta significa que a partir de ese día ningún despliegue es seguro sin acordarse de esta.

El signo invertido es una característica de la instalación, no del equipo, así que vive en la configuración de esa instalación, como un multiplicador explícito que el lazo de control aplica al leer. Una línea en el fichero de configuración, visible, y con el nombre suficientemente feo como para que nadie la confunda con una ganancia.

Y las energías no son floats

Las variables instantáneas —tensión, corriente, potencia, factor de potencia— son floats de dos registros. Los contadores de energía, no: suelen ser enteros sin signo de 64 bits repartidos en cuatro registros, y traen un valor absoluto de fábrica. No arrancan a cero cuando tú empiezas a leerlos; arrancan donde el equipo lleve acumulado desde que se fabricó.

De ahí sale una regla de cálculo que parece obvia escrita y que se incumple constantemente: la energía de un periodo es una diferencia contra una instantánea previa, nunca una lectura directa. El «consumo de hoy» es la lectura de ahora menos la lectura guardada a las 00:00.

Y como esa resta se va a ejecutar todos los días durante años, hay que decidir qué hacer cuando da algo imposible. Se dan dos casos, los dos reales:

  • Delta negativo. El contador se ha puesto a cero: alguien lo reinició, o se cambió el equipo. La diferencia no significa nada. Se descarta el intervalo y se toma la lectura nueva como base.
  • Delta absurdamente grande. Un salto de decenas de miles de kWh en un día en una instalación que hace cientos. Casi siempre es un equipo sustituido por otro con distinto acumulado, y ocasionalmente una lectura corrupta. También se descarta, con un umbral coherente con el tamaño de la instalación.

Sin esos dos filtros, un cambio de contador mete un pico de decenas de MWh en la serie histórica y contamina para siempre cualquier gráfica anual, cualquier comparativa mensual y cualquier informe que se haya enviado ya.

El poso

  • Modbus transporta registros de 16 bits; todo lo demás es un acuerdo entre el fabricante y tú. Ese acuerdo se documenta en el código, junto al unpack.
  • Valida el orden de palabras contra una magnitud conocida y no nula. El cero se lee bien de las cuatro formas: es el mejor falso positivo que existe.
  • Los matices de una instalación van en la configuración de esa instalación. Un driver compartido con excepciones deja de ser compartible.
  • Un contador acumulado no es una medida: es el extremo de una resta. Y toda resta necesita saber qué hacer cuando el resultado es imposible.

¿Te suena de tu planta? Esto sale de operar instalaciones reales todos los días. Si tienes un problema parecido, cuéntanoslo: la primera conversación es con un ingeniero, no con un comercial.

Hablar con un ingeniero Probar el simulador →