Un registro de Modbus (Holding Register) guarda un número entero de $16$ bits. Como el bus UART transfiere datos en trozos de $8$ bits, tenemos que enviar ese número partido en dos bytes: una mitad "Alta" y una mitad "Baja".
Aquí reside la trampa industrial que destruye los fines de semana de los ingenieros novatos:
1050 ($0\text{x}041\text{A}$) en RAM de forma inversa: primero guarda 0x1A y en la siguiente dirección guarda 0x04.Si simplemente copias la memoria RAM directo al puerto UART (usando el DMA para transmitir), el Servidor Central recibirá los bytes al revés. Leerá 0x1A04, y en lugar de cobrar $10.50$ pesos, el software creerá que el gas cuesta $66.60$ pesos. Para evitar la catástrofe, **debemos invertir los bytes en software (Swapping)** antes de enviarlos y al momento de recibirlos.
Cuando el Maestro te envía una trama con Función 03, te está pidiendo que le envíes el contenido de uno o más registros (Arreglos de 16-bits). El esclavo debe responder con: El ID, la Función 03, **la cantidad de bytes que va a mandar en total**, y luego los datos de los registros (partidos en dos bytes cada uno, High seguido de Low).
Ejercicio 1: Integraremos la respuesta de la Función 03 (Read Holding Registers) y la Función 06 (Write Single Register) en el analizador (Parser) que armamos en el Día 139. Extraeremos las direcciones usando desplazamientos de bits (Bitwise Shifts) para esquivar la trampa del Endianness. Ensamblaremos la respuesta y la enviaremos físicamente usando el pin DE/RE (Día 138).
/**
* @file modbus_handlers.c
* @brief Manejadores de Funciones Modbus (FC03 y FC06) con Endianness Control
*/
#include "stm32f401xe.h"
#include <stdint.h>
#define MAX_HOLDING_REGS 10
extern uint16_t holding_registers[MAX_HOLDING_REGS]; // De Día 139
extern void rs485_enviar_trama_bloqueante(uint8_t* buffer, uint16_t longitud); // De Día 138
// (Simulamos la función CRC16 que desentrañaremos mañana en el Día 141)
extern uint16_t calcular_crc16(uint8_t *buffer, uint16_t longitud);
/**
* @brief Responde a la petición 0x03 (Leer registros de 16-bits)
* Trama RX: [ID][03][Start Hi][Start Lo][Num Hi][Num Lo][CRC_L][CRC_H]
*/
void procesar_fc03_read_holding(uint8_t* rx_buffer, uint16_t rx_len) {
// 1. Extraer datos esquivando el Endianness (Modbus manda el High Byte primero)
uint16_t start_addr = (rx_buffer[2] << 8) | rx_buffer[3];
uint16_t num_regs = (rx_buffer[4] << 8) | rx_buffer[5];
// 2. Validación de Seguridad (Exception 0x02 - Illegal Data Address)
if ((start_addr + num_regs) > MAX_HOLDING_REGS) {
// Enviar trama de error Modbus 0x83
return;
}
// 3. Ensamblar Respuesta
uint8_t tx_buffer[256];
uint16_t tx_idx = 0;
tx_buffer[tx_idx++] = rx_buffer[0]; // Eco del ID de la máquina (Ej. 0x01)
tx_buffer[tx_idx++] = 0x03; // Eco de la Función
tx_buffer[tx_idx++] = num_regs * 2; // Byte Count (Cantidad de bytes que siguen)
// Inyectar los registros forzando el formato Big-Endian
for (uint16_t i = 0; i < num_regs; i++) {
uint16_t valor = holding_registers[start_addr + i];
tx_buffer[tx_idx++] = (valor >> 8) & 0xFF; // High Byte
tx_buffer[tx_idx++] = valor & 0xFF; // Low Byte
}
// 4. Calcular Firma CRC16 para la respuesta
uint16_t crc = calcular_crc16(tx_buffer, tx_idx);
// Modbus dice que el CRC se envía Little-Endian (Low Byte primero)
// Es una de las raras excepciones del protocolo.
tx_buffer[tx_idx++] = crc & 0xFF; // CRC Low Byte
tx_buffer[tx_idx++] = (crc >> 8) & 0xFF; // CRC High Byte
// 5. Transmitir por RS485 controlando el pin DIR
rs485_enviar_trama_bloqueante(tx_buffer, tx_idx);
}
/**
* @brief Responde a la petición 0x06 (Escribir un solo registro de 16-bits)
* Trama RX: [ID][06][Reg Hi][Reg Lo][Val Hi][Val Lo][CRC_L][CRC_H]
*/
void procesar_fc06_write_single(uint8_t* rx_buffer, uint16_t rx_len) {
uint16_t reg_addr = (rx_buffer[2] << 8) | rx_buffer[3];
uint16_t reg_valor = (rx_buffer[4] << 8) | rx_buffer[5];
if (reg_addr >= MAX_HOLDING_REGS) {
return; // Exception 0x02
}
// ¡LA ESCRITURA SE MATERIALIZA EN EL SISTEMA!
holding_registers[reg_addr] = reg_valor;
// Opcional: Aquí llamarías a la rutina para guardar el cambio en la EEPROM (Día 120)
// para que el nuevo precio del gas sobreviva a un apagón.
// La respuesta de la Función 06 es simplemente un eco exacto de la petición
// Esto le confirma al Servidor Central que el comando fue ejecutado.
rs485_enviar_trama_bloqueante(rx_buffer, rx_len);
}
Objetivo del día: Sincronización de Base de Datos Bi-Direccional.
Una máquina expendedora en 2026 no puede tener precios codificados estáticamente. Si el precio internacional del gas sube, el administrador desde la oficina central abre su software SCADA y cambia el precio a $11.20 pesos en su pantalla. El SCADA envía silenciosamente una trama Modbus FC 06 dirigida al Holding Register `0`.
Tu Despachador intercepta el paquete mediante el DMA sin bloquear la metrología, extrae el número esquivando el Endianness y sobrescribe el arreglo `holding_registers[0]`. Al instante, el Súper Bucle utiliza ese nuevo precio para calcular la siguiente venta, y la pantalla I2C 20x4 se actualiza sola. El hardware devuelve la confirmación RS485. Acabas de lograr una Integración Vertical Completa, desde los microvoltios del sensor analógico hasta el software de contabilidad de la empresa.
memcpy() para copiar directamente números de 16 o 32 bits a la trama de transmisión, pues llegarán al revés.>> y AND &).