/** * @file modbus_rtu_slave.c * @brief Modbus RTU 从站协议处理模块。 */ #include "modbus_rtu_slave.h" #include /** @brief Modbus RTU 最大 ADU 长度,单位为字节。 */ #define MODBUS_RTU_ADU_SIZE_MAX (256U) /** * @brief 当前通信参数下使用的 RTU 帧间隔。 * * 当前波特率为 115200 bit/s。Modbus 规范建议波特率高于 * 19200 bit/s 时采用固定 1.75 ms 帧间隔,此处向上取整为 2 ms。 */ #define MODBUS_RTU_FRAME_GAP_MS (2U) #define MODBUS_BROADCAST_ADDRESS (0U) #define MODBUS_EX_ILLEGAL_FUNCTION (0x01U) #define MODBUS_EX_ILLEGAL_ADDRESS (0x02U) #define MODBUS_EX_ILLEGAL_VALUE (0x03U) #define MODBUS_READ_COILS_MAX (2000U) #define MODBUS_READ_REGS_MAX (125U) #define MODBUS_WRITE_COILS_MAX (1968U) #define MODBUS_WRITE_REGS_MAX (123U) #define MODBUS_COIL_VALUE_ON (0xFF00U) #define MODBUS_COIL_VALUE_OFF (0x0000U) static UART_HandleTypeDef *ModbusUart; static uint8_t ModbusSlaveAddress; /** * DMA 缓冲区只由 DMA 写入;帧缓冲区在接收回调中完成一次快照, * 随后由任务解析,避免 DMA 重启后覆盖尚未处理的数据。 */ static uint8_t ModbusRxDmaBuffer[MODBUS_RTU_ADU_SIZE_MAX]; static uint8_t ModbusRxFrame[MODBUS_RTU_ADU_SIZE_MAX]; static uint8_t ModbusTxFrame[MODBUS_RTU_ADU_SIZE_MAX]; static volatile uint16_t ModbusRxFrameLength; static volatile uint8_t ModbusRxFrameReady; static volatile uint8_t ModbusTxBusy; static volatile uint32_t ModbusRxEventTick; static volatile uint32_t ModbusLastValidFrameTick; static volatile uint8_t ModbusHasReceivedValidFrame; /** * 10000 个保持寄存器占用 20000 字节;10000 个线圈按位存储, * 占用 1250 字节。 */ static uint16_t ModbusHoldingRegisters[MODBUS_MAP_ITEM_COUNT]; static uint8_t ModbusCoils[(MODBUS_MAP_ITEM_COUNT + 7U) / 8U]; volatile MODBUS_SLAVE_STATS ModbusSlaveStatistics; /** * @brief 计算 Modbus RTU CRC16 校验值。 * @param[in] data 待校验数据。 * @param[in] length 待校验数据长度。 * @return CRC16 校验值。 */ static uint16_t ModbusCrc16(const uint8_t *data, uint16_t length) { uint16_t crc = 0xFFFFU; uint16_t index; uint8_t bit; for (index = 0U; index < length; index++) { crc ^= data[index]; for (bit = 0U; bit < 8U; bit++) { if ((crc & 0x0001U) != 0U) { crc = (uint16_t)((crc >> 1U) ^ 0xA001U); } else { crc >>= 1U; } } } return crc; } /** * @brief 从网络字节序数据中读取一个 16 位无符号整数。 * @param[in] data 至少包含两个字节的数据地址。 * @return 转换后的 16 位无符号整数。 */ static uint16_t ModbusGetU16Be(const uint8_t *data) { return (uint16_t)(((uint16_t)data[0] << 8U) | data[1]); } /** * @brief 检查连续数据地址范围是否合法。 * @param[in] start 起始地址。 * @param[in] quantity 数据项数量。 * @retval 1 地址范围合法。 * @retval 0 地址范围非法。 */ static uint8_t ModbusAddressRangeIsValid(uint16_t start, uint16_t quantity) { if ((quantity == 0U) || (start >= MODBUS_MAP_ITEM_COUNT)) { return 0U; } /* 先做减法再比较,避免 start + quantity 发生 16 位溢出。 */ return (quantity <= (MODBUS_MAP_ITEM_COUNT - start)) ? 1U : 0U; } /** * @brief 读取已经确认地址合法的线圈。 * @param[in] address 线圈地址。 * @return 线圈状态,取值为 0 或 1。 */ static uint8_t ModbusCoilGetUnchecked(uint16_t address) { uint8_t mask = (uint8_t)(1U << (address & 0x0007U)); return ((ModbusCoils[address >> 3U] & mask) != 0U) ? 1U : 0U; } /** * @brief 设置已经确认地址合法的线圈。 * @param[in] address 线圈地址。 * @param[in] state 0 表示复位,非 0 表示置位。 */ static void ModbusCoilSetUnchecked(uint16_t address, uint8_t state) { uint8_t mask = (uint8_t)(1U << (address & 0x0007U)); if (state != 0U) { ModbusCoils[address >> 3U] |= mask; } else { ModbusCoils[address >> 3U] &= (uint8_t)(~mask); } } /** * @brief 启动 USART DMA 空闲接收。 * @retval HAL_OK DMA 接收启动成功。 * @retval HAL_BUSY 串口未配置或发送尚未结束。 * @return 其他 HAL 状态表示 DMA 接收启动失败。 */ static HAL_StatusTypeDef ModbusStartReceive(void) { HAL_StatusTypeDef status; if (ModbusTxBusy != 0U) { return HAL_BUSY; } status = HAL_UARTEx_ReceiveToIdle_DMA(ModbusUart, ModbusRxDmaBuffer, sizeof(ModbusRxDmaBuffer)); if ((status == HAL_OK) && (ModbusUart->hdmarx != NULL)) { /* * 普通 Modbus 帧只应在 IDLE 或缓冲区满时交给应用。 * 关闭 DMA 半传输中断,避免长帧在一半位置被误认为完整帧。 */ __HAL_DMA_DISABLE_IT(ModbusUart->hdmarx, DMA_IT_HT); } return status; } /** * @brief 在 RTU 帧末尾追加 CRC 低字节和高字节。 * @param[in,out] frame 待追加 CRC 的帧缓冲区。 * @param[in] payloadLength 不包含 CRC 的有效载荷长度。 */ static void ModbusAppendCrc(uint8_t *frame, uint16_t payloadLength) { uint16_t crc = ModbusCrc16(frame, payloadLength); /* Modbus RTU 在线路上传输 CRC 低字节在前、高字节在后。 */ frame[payloadLength] = (uint8_t)(crc & 0x00FFU); frame[payloadLength + 1U] = (uint8_t)(crc >> 8U); } /** * @brief 构造 Modbus 异常响应。 * @param[in] function 请求功能码。 * @param[in] exception 异常码。 * @return 异常响应 ADU 长度。 */ static uint16_t ModbusBuildException(uint8_t function, uint8_t exception) { ModbusTxFrame[0] = ModbusSlaveAddress; ModbusTxFrame[1] = (uint8_t)(function | 0x80U); ModbusTxFrame[2] = exception; ModbusAppendCrc(ModbusTxFrame, 3U); return 5U; } /** * @brief 处理读线圈功能码 0x01。 * @param[in] request RTU 请求帧。 * @param[in] requestLength 请求帧长度。 * @return 待发送响应长度,异常请求返回异常响应长度。 */ static uint16_t ModbusProcessReadCoils(const uint8_t *request, uint16_t requestLength) { uint16_t start; uint16_t quantity; uint16_t index; uint8_t byteCount; if (requestLength != 8U) { ModbusSlaveStatistics.illegalValueCount++; return ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE); } start = ModbusGetU16Be(&request[2]); quantity = ModbusGetU16Be(&request[4]); if ((quantity == 0U) || (quantity > MODBUS_READ_COILS_MAX)) { ModbusSlaveStatistics.illegalValueCount++; return ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE); } if (ModbusAddressRangeIsValid(start, quantity) == 0U) { ModbusSlaveStatistics.illegalAddressCount++; return ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_ADDRESS); } byteCount = (uint8_t)((quantity + 7U) / 8U); ModbusTxFrame[0] = ModbusSlaveAddress; ModbusTxFrame[1] = 0X01; ModbusTxFrame[2] = byteCount; (void)memset(&ModbusTxFrame[3], 0, byteCount); /* Modbus 规定第一个线圈放在响应数据首字节的最低位。 */ for (index = 0U; index < quantity; index++) { if (ModbusCoilGetUnchecked((uint16_t)(start + index)) != 0U) { ModbusTxFrame[3U + (index >> 3U)] |= (uint8_t)(1U << (index & 0x0007U)); } } ModbusAppendCrc(ModbusTxFrame, (uint16_t)(3U + byteCount)); return (uint16_t)(5U + byteCount); } /** * @brief 处理读保持寄存器功能码 0x03。 * @param[in] request RTU 请求帧。 * @param[in] requestLength 请求帧长度。 * @return 待发送响应长度,异常请求返回异常响应长度。 */ static uint16_t ModbusProcessReadHolding(const uint8_t *request, uint16_t requestLength) { uint16_t start; uint16_t quantity; uint16_t index; uint16_t value; if (requestLength != 8U) { ModbusSlaveStatistics.illegalValueCount++; return ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE); } start = ModbusGetU16Be(&request[2]); quantity = ModbusGetU16Be(&request[4]); if ((quantity == 0U) || (quantity > MODBUS_READ_REGS_MAX)) { ModbusSlaveStatistics.illegalValueCount++; return ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE); } if (ModbusAddressRangeIsValid(start, quantity) == 0U) { ModbusSlaveStatistics.illegalAddressCount++; return ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_ADDRESS); } ModbusTxFrame[0] = ModbusSlaveAddress; ModbusTxFrame[1] = 0X03; ModbusTxFrame[2] = (uint8_t)(quantity * 2U); for (index = 0U; index < quantity; index++) { value = ModbusHoldingRegisters[start + index]; ModbusTxFrame[3U + index * 2U] = (uint8_t)(value >> 8U); ModbusTxFrame[4U + index * 2U] = (uint8_t)(value & 0x00FFU); } ModbusAppendCrc(ModbusTxFrame, (uint16_t)(3U + quantity * 2U)); return (uint16_t)(5U + quantity * 2U); } /** * @brief 处理写单个保持寄存器功能码 0x06。 * @param[in] request RTU 请求帧。 * @param[in] requestLength 请求帧长度。 * @param[in] isBroadcast 非 0 表示当前请求为广播。 * @return 单播响应长度;广播或无法响应时返回 0。 */ static uint16_t ModbusProcessWriteSingleRegister( const uint8_t *request, uint16_t requestLength, uint8_t isBroadcast) { uint16_t address; uint16_t value; if (requestLength != 8U) { ModbusSlaveStatistics.illegalValueCount++; return (isBroadcast != 0U) ? 0U : ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE); } address = ModbusGetU16Be(&request[2]); value = ModbusGetU16Be(&request[4]); if (address >= MODBUS_MAP_ITEM_COUNT) { ModbusSlaveStatistics.illegalAddressCount++; return (isBroadcast != 0U) ? 0U : ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_ADDRESS); } ModbusHoldingRegisters[address] = value; if (isBroadcast != 0U) { return 0U; } /* 0x06 的正常响应必须原样回显请求的前 6 个字节。 */ (void)memcpy(ModbusTxFrame, request, 6U); ModbusAppendCrc(ModbusTxFrame, 6U); return 8U; } /** * @brief 处理写单个线圈功能码 0x05。 * @param[in] request RTU 请求帧。 * @param[in] requestLength 请求帧长度。 * @param[in] isBroadcast 非 0 表示当前请求为广播。 * @return 单播响应长度;广播或无法响应时返回 0。 */ static uint16_t ModbusProcessWriteSingleCoil( const uint8_t *request, uint16_t requestLength, uint8_t isBroadcast) { uint16_t address; uint16_t value; if (requestLength != 8U) { ModbusSlaveStatistics.illegalValueCount++; return (isBroadcast != 0U) ? 0U : ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE); } address = ModbusGetU16Be(&request[2]); value = ModbusGetU16Be(&request[4]); /* * 0x05 只接受 0xFF00(线圈置位)和 0x0000(线圈复位); * 其他数值属于非法数据值。 */ if ((value != MODBUS_COIL_VALUE_ON) && (value != MODBUS_COIL_VALUE_OFF)) { ModbusSlaveStatistics.illegalValueCount++; return (isBroadcast != 0U) ? 0U : ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE); } if (address >= MODBUS_MAP_ITEM_COUNT) { ModbusSlaveStatistics.illegalAddressCount++; return (isBroadcast != 0U) ? 0U : ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_ADDRESS); } ModbusCoilSetUnchecked( address, (value == MODBUS_COIL_VALUE_ON) ? 1U : 0U); if (isBroadcast != 0U) { return 0U; } /* 0x05 正常应答原样回显请求的前 6 个字节,再追加 CRC。 */ (void)memcpy(ModbusTxFrame, request, 6U); ModbusAppendCrc(ModbusTxFrame, 6U); return 8U; } /** * @brief 处理写多个线圈功能码 0x0F。 * @param[in] request RTU 请求帧。 * @param[in] requestLength 请求帧长度。 * @param[in] isBroadcast 非 0 表示当前请求为广播。 * @return 单播响应长度;广播或无法响应时返回 0。 */ static uint16_t ModbusProcessWriteMultipleCoils( const uint8_t *request, uint16_t requestLength, uint8_t isBroadcast) { uint16_t start; uint16_t quantity; uint16_t index; uint8_t byteCount; uint8_t expectedByteCount; if (requestLength < 9U) { ModbusSlaveStatistics.illegalValueCount++; return (isBroadcast != 0U) ? 0U : ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE); } start = ModbusGetU16Be(&request[2]); quantity = ModbusGetU16Be(&request[4]); byteCount = request[6]; expectedByteCount = (uint8_t)((quantity + 7U) / 8U); if ((quantity == 0U) || (quantity > MODBUS_WRITE_COILS_MAX) || (byteCount != expectedByteCount) || (requestLength != (uint16_t)(9U + byteCount))) { ModbusSlaveStatistics.illegalValueCount++; return (isBroadcast != 0U) ? 0U : ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE); } if (ModbusAddressRangeIsValid(start, quantity) == 0U) { ModbusSlaveStatistics.illegalAddressCount++; return (isBroadcast != 0U) ? 0U : ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_ADDRESS); } for (index = 0U; index < quantity; index++) { uint8_t state; state = (uint8_t)((request[7U + (index >> 3U)] >> (index & 0x0007U)) & 0x01U); ModbusCoilSetUnchecked((uint16_t)(start + index), state); } if (isBroadcast != 0U) { return 0U; } ModbusTxFrame[0] = ModbusSlaveAddress; ModbusTxFrame[1] = 0X0F; (void)memcpy(&ModbusTxFrame[2], &request[2], 4U); ModbusAppendCrc(ModbusTxFrame, 6U); return 8U; } /** * @brief 处理写多个保持寄存器功能码 0x10。 * @param[in] request RTU 请求帧。 * @param[in] requestLength 请求帧长度。 * @param[in] isBroadcast 非 0 表示当前请求为广播。 * @return 单播响应长度;广播或无法响应时返回 0。 */ static uint16_t ModbusProcessWriteMultipleRegisters( const uint8_t *request, uint16_t requestLength, uint8_t isBroadcast) { uint16_t start; uint16_t quantity; uint16_t index; uint8_t byteCount; if (requestLength < 9U) { ModbusSlaveStatistics.illegalValueCount++; return (isBroadcast != 0U) ? 0U : ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE); } start = ModbusGetU16Be(&request[2]); quantity = ModbusGetU16Be(&request[4]); byteCount = request[6]; if ((quantity == 0U) || (quantity > MODBUS_WRITE_REGS_MAX) || (byteCount != (uint8_t)(quantity * 2U)) || (requestLength != (uint16_t)(9U + byteCount))) { ModbusSlaveStatistics.illegalValueCount++; return (isBroadcast != 0U) ? 0U : ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE); } if (ModbusAddressRangeIsValid(start, quantity) == 0U) { ModbusSlaveStatistics.illegalAddressCount++; return (isBroadcast != 0U) ? 0U : ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_ADDRESS); } for (index = 0U; index < quantity; index++) { ModbusHoldingRegisters[start + index] = ModbusGetU16Be(&request[7U + index * 2U]); } if (isBroadcast != 0U) { return 0U; } ModbusTxFrame[0] = ModbusSlaveAddress; ModbusTxFrame[1] = 0X10; (void)memcpy(&ModbusTxFrame[2], &request[2], 4U); ModbusAppendCrc(ModbusTxFrame, 6U); return 8U; } /** * @brief 校验并分发一帧 Modbus RTU 请求。 * @param[in] request RTU 请求帧。 * @param[in] requestLength 请求帧长度。 * @return 待发送响应长度;无需响应时返回 0。 */ static uint16_t ModbusProcessRequest(const uint8_t *request, uint16_t requestLength) { uint16_t calculatedCrc; uint16_t receivedCrc; uint8_t isBroadcast; if (requestLength < 4U) { return 0U; } calculatedCrc = ModbusCrc16(request, (uint16_t)(requestLength - 2U)); receivedCrc = (uint16_t)(request[requestLength - 2U] | ((uint16_t)request[requestLength - 1U] << 8U)); if (calculatedCrc != receivedCrc) { ModbusSlaveStatistics.crcErrorCount++; return 0U; } if ((request[0] != ModbusSlaveAddress) && (request[0] != MODBUS_BROADCAST_ADDRESS)) { ModbusSlaveStatistics.ignoredAddressCount++; return 0U; } isBroadcast = (request[0] == MODBUS_BROADCAST_ADDRESS) ? 1U : 0U; ModbusSlaveStatistics.validFrameCount++; ModbusLastValidFrameTick = HAL_GetTick(); ModbusHasReceivedValidFrame = 1U; switch (request[1]) { case 0X01U://读线圈 // 广播请求不允许读取,从站不作响应。 return (isBroadcast != 0U) ? 0U : ModbusProcessReadCoils(request, requestLength); case 0X03U://读保持寄存器 return (isBroadcast != 0U) ? 0U : ModbusProcessReadHolding(request, requestLength); case 0x05U://写单个线圈 return ModbusProcessWriteSingleCoil(request, requestLength, isBroadcast); case 0x06U://写单个保持寄存器 return ModbusProcessWriteSingleRegister(request, requestLength, isBroadcast); case 0x0FU://写多个线圈 return ModbusProcessWriteMultipleCoils(request, requestLength, isBroadcast); case 0x10U://写多个保持寄存器 return ModbusProcessWriteMultipleRegisters(request, requestLength, isBroadcast); default://未知功能码 ModbusSlaveStatistics.illegalFunctionCount++; return (isBroadcast != 0U) ? 0U : ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_FUNCTION); } } HAL_StatusTypeDef ModbusSlaveInit(UART_HandleTypeDef *huart, uint8_t slaveAddress) { ModbusUart = huart; ModbusSlaveAddress = slaveAddress; ModbusHoldingRegisters[HMI_REG_DEVICE_ID] = 0xF407U; return ModbusStartReceive(); } void ModbusSlavePoll(void) { uint16_t responseLength; if ((ModbusRxFrameReady == 0U) || (ModbusTxBusy != 0U)) { return; } responseLength = ModbusProcessRequest(ModbusRxFrame, ModbusRxFrameLength); if (responseLength > 0U) { ModbusTxBusy = 1U; if (HAL_UART_Transmit_DMA(ModbusUart, ModbusTxFrame, responseLength) == HAL_OK) { ModbusSlaveStatistics.txFrameCount++; } else { ModbusTxBusy = 0U; ModbusSlaveStatistics.uartErrorCount++; (void)ModbusStartReceive();//重启DMA接收 } } else { (void)ModbusStartReceive(); } /* * 当前帧;处理完成后再释放帧槽。 */ ModbusRxFrameReady = 0U; } void ModbusSlaveOnRxEvent(UART_HandleTypeDef *huart, uint16_t size) { ModbusSlaveStatistics.rxEventCount++;//串口接收事件计数 if ((size > 0U) && (size <= MODBUS_RTU_ADU_SIZE_MAX)) { if (ModbusRxFrameReady == 0U) { (void)memcpy(ModbusRxFrame, ModbusRxDmaBuffer, size); ModbusRxFrameLength = size; ModbusRxEventTick = HAL_GetTick(); ModbusRxFrameReady = 1U; } else { //前一帧未处理完毕 ModbusSlaveStatistics.droppedFrameCount++; } } else { //长度异常帧 ModbusSlaveStatistics.droppedFrameCount++; (void)ModbusStartReceive(); } } void ModbusSlaveOnTxComplete(UART_HandleTypeDef *huart) { if ((huart != ModbusUart) || (ModbusUart == NULL)) { return; } ModbusTxBusy = 0U; (void)ModbusStartReceive();//重启DMA接收 } void ModbusSlaveOnUartError(UART_HandleTypeDef *huart) { if ((huart != ModbusUart) || (ModbusUart == NULL)) { return; } ModbusSlaveStatistics.uartErrorCount++; ModbusTxBusy = 0U; (void)HAL_UART_Abort(huart);//立即终止这个串口当前正在进行的发送和接收操作 (void)ModbusStartReceive();//重启DMA接收 } uint8_t ModbusSlaveSetHoldingRegister(uint16_t address, uint16_t value) { if (address >= MODBUS_MAP_ITEM_COUNT) { return 0U; } ModbusHoldingRegisters[address] = value; return 1U; } uint8_t ModbusSlaveGetHoldingRegister(uint16_t address, uint16_t *value) { if ((address >= MODBUS_MAP_ITEM_COUNT) || (value == NULL)) { return 0U; } *value = ModbusHoldingRegisters[address]; return 1U; } uint8_t ModbusSlaveSetCoil(uint16_t address, uint8_t state) { if (address >= MODBUS_MAP_ITEM_COUNT) { return 0U; } ModbusCoilSetUnchecked(address, state); return 1U; } uint8_t ModbusSlaveGetCoil(uint16_t address, uint8_t *state) { if ((address >= MODBUS_MAP_ITEM_COUNT) || (state == NULL)) { return 0U; } *state = ModbusCoilGetUnchecked(address); return 1U; } uint8_t ModbusSlaveIsConnected(uint32_t timeoutMs) { if (ModbusHasReceivedValidFrame == 0U) { return 0U; } return ((HAL_GetTick() - ModbusLastValidFrameTick) <= timeoutMs) ? 1U : 0U; }