Asesoría Agenda una llamada de descubrimiento técnico hoy mismo »

· Eduardo Vieira · Protocolos Industriales  · 6 min de lectura

Códigos de Función Modbus: La Guía Definitiva (0x01 a 0x17)

Dominá los códigos de función Modbus con ejemplos reales en C (libmodbus) y Python (pymodbus). Guía completa para leer coils, inputs y registers sin errores.

Dominá los códigos de función Modbus con ejemplos reales en C (libmodbus) y Python (pymodbus). Guía completa para leer coils, inputs y registers sin errores.

Si alguna vez te quedaste mirando un analizador de tramas preguntándote por qué tu PLC te devuelve un 0x83, esta guía es para vos.

Los códigos de función Modbus son el núcleo del protocolo. Definen qué acción debe realizar el servidor (esclavo). Muchos tutoriales solo cubren el 0x03 (Read Holding Registers), pero en la industria real vas a necesitar dominar todo el espectro para interactuar con I/O remotos, variadores de frecuencia y sensores complejos.

En esta guía definitiva, vamos a desglosar todos los códigos de función estándar, con diagramas de trama bit a bit y código de producción en C (libmodbus) y Python (pymodbus).

El Modelo de Datos Modbus: Las 4 Tablas

Antes de tirar código, entendé dónde estás parado. Modbus divide la memoria en 4 áreas distintas. Confundirlas es la causa #1 de errores “Illegal Data Address”.

ÁreaTipo de ObjetoAccesoDirecciónUso Típico
CoilsBitR/W00001 - 09999Salidas digitales (Relés, Contactores)
Discrete InputsBitRead Only10001 - 19999Entradas digitales (Sensores, Pulsadores)
Input Registers16-bit WordRead Only30001 - 39999Mediciones analógicas (ADC, Temperatura)
Holding Registers16-bit WordR/W40001 - 49999Setpoints, Configuración, Valores R/W

Nota Crítica: En el protocolo (el cable), las direcciones son Zero-Based.

  • Holding Register 40001 es la dirección 0.
  • Holding Register 40101 es la dirección 100.

Librerías como pymodbus y libmodbus usan direccionamiento Zero-Based (0-65535).


Estructura de la Trama (PDU)

El PDU (Protocol Data Unit) es la parte del mensaje que es independiente del medio físico (TCP o Serial).

+---------------+------------------------+
| Function Code |          Data          |
+---------------+------------------------+
|    1 Byte     |      N Bytes           |
+---------------+------------------------+
  • Rango: 0x01 a 0x7F (1-127).
  • Excepción: Si el servidor rechaza la solicitud, devuelve el código de función con el MSB en 1 (Ej: 0x03 $\to$ 0x83).

1. Funciones de Lectura (Bit Access)

0x01: Read Coils (Leer Bobinas)

Lee el estado ON/OFF de salidas digitales discreta.

  • Request: [Func Code] [Start Addr HI] [Start Addr LO] [Quantity HI] [Quantity LO]
  • Response: [Func Code] [Byte Count] [Coil Status 1] ... [Coil Status N]

Python (pymodbus)

# Leer salidas digitales 0 a 7
rr = client.read_coils(address=0, count=8, slave=1)
if not rr.isError():
    print(f"Salidas: {rr.bits}")
else:
    print(f"Error: {rr}")

C (libmodbus)

uint8_t bits[8];
int rc = modbus_read_bits(ctx, 0, 8, bits);
if (rc == -1) {
    fprintf(stderr, "%s\n", modbus_strerror(errno));
}

0x02: Read Discrete Inputs (Leer Entradas Discretas)

Igual que el 0x01, pero para leer entradas físicas (sensores, finales de carrera) que no se pueden escribir. A menudo ignorado, pero vital para seguridad.

Python (pymodbus)

# Leer sensores en pines de entrada
rr = client.read_discrete_inputs(address=0, count=16, slave=1)

C (libmodbus)

uint8_t inputs[16];
// Notar la función "input_bits"
int rc = modbus_read_input_bits(ctx, 0, 16, inputs);

2. Funciones de Lectura (Word Access)

0x03: Read Holding Registers

El “navaja suiza” de Modbus. Lee registros de 16 bits de lectura/escritura. Se usa para todo: configuraciones, valores analógicos, contadores.

  • Request: 03 [Addr Hi] [Addr Lo] [Qty Hi] [Qty Lo]
  • Response: 03 [Byte Count] [Val Hi] [Val Lo] ...

Python (pymodbus) - Manejo de Tipos

Modbus solo mueve bits. pymodbus nos ayuda a decodificarlos en tipos reales (float, int32).

# Leer 2 registros (4 bytes) para un Float 32-bit
rr = client.read_holding_registers(address=100, count=2, slave=1)
if not rr.isError():
    # Decodificar float (IEEE 754)
    decoder = BinaryPayloadDecoder.fromRegisters(
        rr.registers,
        byteorder=Endian.Big,
        wordorder=Endian.Big
    )
    temperatura = decoder.decode_32bit_float()

C (libmodbus)

uint16_t tab_reg[2];
int rc = modbus_read_registers(ctx, 100, 2, tab_reg);
if (rc != -1) {
    // Convertir a float manualmente
    float temp = modbus_get_float_abcd(tab_reg);
    printf("Temp: %.2f\n", temp);
}

0x04: Read Input Registers

Para leer datos analógicos de solo lectura (mediciones de ADC, valores de sensores calibrados). Muchos dispositivos mapean todo a Holding Registers, pero los estrictos usan Input Registers para mediciones.

Python (pymodbus)

rr = client.read_input_registers(address=0, count=1, slave=1)

C (libmodbus)

uint16_t val;
modbus_read_input_registers(ctx, 0, 1, &val);

3. Funciones de Escritura (Single)

0x05: Write Single Coil

Fuerza una salida a ON u OFF.

  • Dato Curioso: En el protocolo, ON se envía como 0xFF00 y OFF como 0x0000. Esto previene activaciones accidentales por ruido (rara vez un byte de ruido es exactamente 0xFF).

Python

client.write_coil(address=5, value=True, slave=1)

C (libmodbus)

modbus_write_bit(ctx, 5, TRUE);

0x06: Write Single Register

Escribe un solo registro de 16 bits.

⚠️ PELIGRO: No uses esto para escribir valores de 32 bits (como Floats o Longs). Rompe la atomicidad. Si tu proceso lee el valor entre las dos escrituras 0x06, leerá basura corrupta. Usá 0x10 siempre para datos > 16 bits.

Python

client.write_register(address=200, value=1234, slave=1)

C (libmodbus)

modbus_write_register(ctx, 200, 1234);

4. Funciones de Escritura (Multiple) - ¡Las que debés usar!

0x0F: Write Multiple Coils

Permite setear un banco entero de salidas en una sola transacción atómica. Mucho más eficiente que llamar a write_coil en bucle.

Python

estados = [True, False, True, True, False]
client.write_coils(address=0, values=estados, slave=1)

C (libmodbus)

uint8_t bits[] = {1, 0, 1, 1, 0};
modbus_write_bits(ctx, 0, 5, bits);

0x10: Write Multiple Registers

El estándar industrial para escribir parámetros. Permite escribir bloques enteros de configuración o valores complejos (Floats, Strings) de forma atómica.

Python

# Escribir un Float (2 registros)
builder = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Big)
builder.add_32bit_float(23.5)
payload = builder.to_registers()

client.write_registers(address=400, values=payload, slave=1)

C (libmodbus)

float setpoint = 23.5;
uint16_t raw[2];
modbus_set_float_abcd(setpoint, raw);
modbus_write_registers(ctx, 400, 2, raw);

0x17: Read/Write Multiple Registers

La joya oculta. Permite escribir un bloque y leer otro en una sola transacción. Ideal para sincronización de alta velocidad donde querés actualizar setpoints y recibir el estado actual sin latencia de ida y vuelta.

Python (pymodbus)

# Escribir 2 registros en addr 100, Leer 2 de addr 200
rr = client.read_write_multiple_registers(
    read_address=200, read_count=2,
    write_address=100, write_registers=[10, 20],
    slave=1
)

C (libmodbus)

uint16_t write_cols[] = {10, 20};
uint16_t read_cols[2];
modbus_write_and_read_registers(ctx, 100, 2, write_cols, 200, 2, read_cols);

Códigos de Excepción (Modbus Exceptions)

Cuando algo falla, el esclavo no se queda callado (ni debería). Responde con un código de error. Si tu código no maneja esto, estás programando a ciegas.

CódigoNombreSignificado Real
01Illegal FunctionLe pediste algo que no sabe hacer (ej: escribir en un Input Register).
02Illegal AddressLa dirección no existe en el mapa de memoria del dispositivo.
03Illegal Data ValueEl valor está fuera de rango o la cantidad de registros pedida es inválida.
04Server FailureEl esclavo falló internamente (hardware error, etc.).
06Server Busy”Bancá un toque, estoy ocupado”. Reintentá luego.

Manejo en Python

from pymodbus.pdu import ExceptionResponse

rr = client.read_holding_registers(0, 10, slave=1)
if rr.isError():
    if isinstance(rr, ExceptionResponse):
        print(f"Modbus Exception: {rr.exception_code}")
    else:
        print("Error de comunicación (Timeout/Connection)")

Resumen y Mejores Prácticas

  1. Usá siempre 0x10 (Write Multiple) para valores numéricos complejos (Floats, Int32) para garantizar atomicidad.
  2. Manejá las excepciones, no solo los timeouts. Un Illegal Address requiere corrección de código, un Timeout requiere reintento.
  3. Agrupá lecturas. Es mejor leer 100 registros continuos de una vez (Function 0x03) que hacer 100 peticiones de 1 registro. Modbus es lento por latencia, no por ancho de banda.
  4. Ojo con el Endianness. Si leés valores “raros” pero consistentes, probablemente tenés los bytes o las palabras invertidas (ABCD vs CDAB).

Dominar estos códigos te diferencia de un programador que “toca de oído” a un integrador que entiende lo que pasa por el cable.

Volver al blog

Related Posts

View All Posts »