STM32从零到量产开发:四路继电器工业控制模块开发-Modbus RTU 从站协议栈设计说明

STM32从零到量产开发:四路继电器工业控制模块开发-Modbus RTU 从站协议栈设计说明

1. 核心功能概述

该代码实现了一个基于 STM32 的 Modbus RTU 从站协议栈,主要面向四通道继电器控制模块(P02设备)。除了标准的 Modbus 协议支持外,还针对量产需求增加了故障安全、诊断计数和设备信息上报等扩展功能。

1.1 支持的 Modbus 功能码

功能码 (Hex) 名称 描述
0x01 Read Coils 读取继电器线圈状态
0x02 Read Discrete Inputs 读取数字输入 (DI) 状态
0x03 Read Holding Regs 读取保持寄存器(设备配置参数)
0x04 Read Input Regs 读取输入寄存器(运行状态与诊断信息)
0x05 Write Single Coil 控制单个继电器吸合/断开
0x06 Write Single Reg 修改单个保持寄存器参数
0x0F Write Multiple Coils 批量控制多个继电器
0x10 Write Multiple Regs 批量修改多个保持寄存器参数
0x11 Report Slave ID 上报从站标识及运行状态

1.2 异常码支持

异常码 含义 触发场景
0x01 非法功能码 请求了未支持的功能码
0x02 非法数据地址 请求的寄存器/线圈地址越界
0x03 非法数据值 写入的数据值不合法(如继电器控制值非 0xFF00/0x0000)
0x06 设备忙 (预留)设备处于忙碌状态

1.3 扩展功能

  1. 通信故障安全 (#1) :当连续 fail_safe_sec 秒未收到有效 Modbus 帧时,自动将所有继电器切换至安全态(断开),防止上位机死机或通信断线导致设备失控。收到新有效帧后自动恢复。
  2. 诊断计数 (#3):实时统计异常响应次数、各继电器动作次数及寿命告警位图,用于通信质量评估与设备维护。
  3. 设备信息 (#4):通过 FC17 及输入寄存器暴露设备序列号(SN)、固件版本、设备类型码及累计运行小时数。

2. API 接口说明

2.1 核心控制接口

  • void Modbus_Init(void):初始化 Modbus 从站。加载 Flash 参数,配置 RS485 波特率,并重置所有统计计数与状态变量。
  • void Modbus_Poll(void):Modbus 主任务处理。在主循环中高频调用,负责检测 RS485 帧接收、CRC 校验、地址匹配、功能码分发执行、响应发送及参数落盘。
  • void Modbus_CheckFailSafe(void):通信故障安全检测。在主循环中高频调用,监测通信超时并触发安全保护机制。

2.2 状态查询接口

  • uint16_t Modbus_GetFrameCount(void):获取累计有效帧计数。
  • uint16_t Modbus_GetCRCErrorCount(void):获取累计 CRC 校验错误计数。
  • uint16_t Modbus_GetExceptionCount(void):获取异常响应计数(仅统计非广播帧)。
  • uint8_t Modbus_IsFailSafeActive(void):查询当前是否处于故障安全态(1=已触发,0=正常)。

3. 寄存器映射表

3.1 线圈 (Coils) - 地址 0x0000 ~ 0x0003

地址 含义 读写属性
0x0000 RELAY1 (继电器1) 读/写
0x0001 RELAY2 (继电器2) 读/写
0x0002 RELAY3 (继电器3) 读/写
0x0003 RELAY4 (继电器4) 读/写

3.2 离散输入 (Discrete Inputs) - 地址 0x0000 ~ 0x0003

地址 含义 读写属性
0x0000 DI1 (数字输入1) 只读
0x0001 DI2 (数字输入2) 只读
0x0002 DI3 (数字输入3) 只读
0x0003 DI4 (数字输入4) 只读

3.3 输入寄存器 (Input Registers) - 地址 0x0000 ~ 0x000F

地址 宏定义 含义 备注
0x0000 IR_ADDR_UPTIME 运行秒数 16位回绕
0x0001 IR_ADDR_RESETCOUNT 累计复位次数 -
0x0002 IR_ADDR_FRAMECOUNT 有效帧计数 -
0x0003 IR_ADDR_CRCERROR CRC 错误计数 -
0x0004 IR_ADDR_EXCEPCOUNT 异常响应计数 #3 诊断
0x0005 IR_ADDR_RUNHOURS 累计运行小时 #4 设备信息
0x0006 IR_ADDR_RELAY1_ACT 继电器1动作次数 #3 诊断
0x0007 IR_ADDR_RELAY2_ACT 继电器2动作次数 #3 诊断
0x0008 IR_ADDR_RELAY3_ACT 继电器3动作次数 #3 诊断
0x0009 IR_ADDR_RELAY4_ACT 继电器4动作次数 #3 诊断
0x000A IR_ADDR_LIFE_ALARM 寿命告警位图 bit0~3 对应继电器1~4
0x000B IR_ADDR_FW_VER 固件版本 格式: 0xMMmm
0x000C IR_ADDR_DEV_TYPE 设备类型码 0x0002 = P02
0x000D IR_ADDR_SN_LOW 序列号低16位 取自芯片 UID
0x000E IR_ADDR_SN_HIGH 序列号高16位 取自芯片 UID
0x000F IR_ADDR_FAILSAFE 故障安全态位图 bit0=使能, bit8=当前触发

3.4 保持寄存器 (Holding Registers) - 地址 0x0000 ~ 0x0004

地址 宏定义 含义 备注
0x0000 HR_ADDR_SLAVE 从站地址 写入后需重启生效
0x0001 HR_ADDR_BAUD 波特率索引 写入后回复帧结束自动切换
0x0002 HR_ADDR_LINK 链路使能 0=静默模式(不回复), 1=正常
0x0003 HR_ADDR_FAILSAFE_EN 故障安全使能 #1 扩展: 0/1
0x0004 HR_ADDR_FAILSAFE_SEC 故障安全超时(秒) #1 扩展: 0=禁用

4. 关键数据结构与逻辑流程

4.1 关键数据结构

  • param_t (Flash 参数结构) :由 FlashParam_Get() 获取,包含 slave_addrbaud_indexlink_enablefail_safe_enfail_safe_sec 等持久化配置。
  • 静态状态变量
    • s_frame_count / s_crc_err_count / s_ex_resp_count:协议统计计数器。
    • s_last_valid_tick / s_fail_safe_active:故障安全机制的时间戳与状态标志。
    • s_pending_*:参数变更缓存。写入保持寄存器时仅更新 pending 变量,在 Modbus_Poll 末尾统一调用 FlashParam_Save() 落盘,避免频繁擦写 Flash。
  • 收发缓冲区s_tx_buf (响应组帧缓冲) 与 s_rx_frame (接收帧缓存)。

4.2 核心逻辑流程

  1. 初始化阶段 :调用 Modbus_Init(),从 Flash 读取波特率并配置 RS485 硬件,将当前系统时间 (HAL_GetTick()) 设为 s_last_valid_tick 以防止上电瞬间误触发故障安全。
  2. 主循环轮询 (Modbus_Poll)
    • 接收与校验 :检测 RS485 帧就绪,提取数据并进行 CRC16 校验。校验失败则累加 s_crc_err_count 并丢弃。
    • 地址匹配:判断是否为广播地址 (0x00) 或本机地址,不匹配则静默丢弃。
    • 保活与指示 :有效帧刷新 s_last_valid_tick,触发通信 LED 脉冲 (BSP_CommPulse)。
    • 功能码分发 :通过 switch(fc) 路由至对应的 MB_Handle* 处理函数。处理函数内部完成数据解析、BSP 层调用及响应帧组装。
    • 响应发送 :若非广播且链路使能,调用 BSP_RS485_Send 发送响应。
    • 安全态恢复:响应发送完成后,若当前处于故障安全态,则清除该状态并熄灭故障 LED,确保本次响应能如实反映触发状态。
    • 参数落盘与应用 :检查 s_pending_* 变量,若有变更则写入 Flash;若波特率变更,在回复发送完毕后重新配置 RS485 波特率。
  3. 故障安全检测 (Modbus_CheckFailSafe)
    • 检查故障安全是否使能及超时时间是否大于 0。
    • 计算当前时间与 s_last_valid_tick 的差值,若超过设定阈值且尚未处于安全态,则调用 BSP_Relay_SetAll(0) 断开所有继电器,置位 s_fail_safe_active 并点亮故障 LED。
c 复制代码
/**
  ******************************************************************************
  * @file    modbus_rtu.h
  * @brief   Modbus RTU 从站协议栈 - 对应产品规格书第4章寄存器映射
  *
 * 支持功能码 (规格书 4.1~4.4):
 *   0x01 Read Coils           - 读取继电器线圈状态
 *   0x02 Read Discrete Inputs  - 读取数字输入
 *   0x03 Read Holding Regs    - 读取保持寄存器
 *   0x04 Read Input Regs      - 读取输入寄存器
 *   0x05 Write Single Coil    - 写单个继电器
 *   0x06 Write Single Reg     - 写单个保持寄存器
 *   0x0F Write Multiple Coils - 写多个继电器
 *   0x10 Write Multiple Regs  - 写多个保持寄存器
 *   0x11 Report Slave ID      - 上报从站标识/运行状态 (#4 设备信息)
 *
 * 异常码:
 *   01 非法功能码
 *   02 非法数据地址
 *   03 非法数据值
 *   06 设备忙
 *
 * 扩展功能 (量产增强, 见 README 第4章):
 *   #1 通信故障安全  - 连续 fail_safe_sec 秒无有效帧自动切安全态(全断开), 收帧自动恢复
 *   #3 诊断计数      - 异常响应计数 / 继电器寿命(动作次数)计数 / 寿命告警位图
 *   #4 设备信息      - FC17 从站标识 / 输入寄存器暴露 SN·固件版本·运行小时
 *
 * 寄存器映射 (详见 产品说明书.md 第4章 及 README):
 *   线圈 0x0000~0x0003      : RELAY1~4
 *   离散输入 0x0000~0x0003  : DI1~4
 *   输入寄存器 0x0000~0x000F: 见下方 IR_ADDR_* 宏 (Uptime/复位/帧/CRC/异常/运行小时/寿命...)
 *   保持寄存器 0x0000~0x0004: SlaveAddr/BaudIndex/LinkEnable/FailSafeEn/FailSafeSec
 ******************************************************************************
 */
#ifndef __MODBUS_RTU_H
#define __MODBUS_RTU_H

#ifdef __cplusplus
extern "C" {
#endif

#include "main.h"
#include "bsp_led.h"

/* Modbus 功能码 */
#define MB_FC_READ_COILS              0x01u
#define MB_FC_READ_DISCRETE_INPUTS    0x02u
#define MB_FC_READ_HOLDING_REGS       0x03u
#define MB_FC_READ_INPUT_REGS         0x04u
#define MB_FC_WRITE_SINGLE_COIL       0x05u
#define MB_FC_WRITE_SINGLE_REG        0x06u
#define MB_FC_WRITE_MULTIPLE_COILS    0x0Fu
#define MB_FC_WRITE_MULTIPLE_REGS     0x10u
#define MB_FC_REPORT_SLAVE_ID         0x11u   /* #4 上报从站标识 */

/* Modbus 异常码 */
#define MB_EX_ILLEGAL_FUNCTION        0x01u
#define MB_EX_ILLEGAL_DATA_ADDRESS    0x02u
#define MB_EX_ILLEGAL_DATA_VALUE      0x03u
#define MB_EX_SLAVE_DEVICE_BUSY       0x06u

/* 最大 PDU 长度 */
#define MB_MAX_PDU_SIZE               253u

/**
  * @brief  初始化 Modbus 从站 (加载参数, 启动 RS485 接收)
  */
void Modbus_Init(void);

/**
  * @brief  Modbus 主任务处理
  * @note   检测 RS485 帧接收完成 -> 解析 -> 执行 -> 回复
  *         建议在主循环高频调用
  */
void Modbus_Poll(void);

/**
  * @brief  获取有效帧计数 (输入寄存器 0x0002)
  */
uint16_t Modbus_GetFrameCount(void);

/**
  * @brief  获取 CRC 错误计数 (输入寄存器 0x0003)
  */
uint16_t Modbus_GetCRCErrorCount(void);

/**
  * @brief  获取异常响应计数 (#3 诊断)
  * @note   主站下发非法功能码/地址/数值时回复异常帧, 累计次数用于通信质量评估。
  */
uint16_t Modbus_GetExceptionCount(void);

/**
  * @brief  通信故障安全检测 (#1)
  * @note   在主循环高频调用。若连续 fail_safe_sec 秒未收到有效 Modbus 帧,
  *         自动将全部继电器切到安全态(断开), 防止上位机死机导致设备失控。
  *         收到新有效帧后自动恢复(由主站重新下发控制指令)。
 */
void Modbus_CheckFailSafe(void);

/**
  * @brief  查询当前是否处于故障安全态 (#1)
  * @retval 1=已触发安全态(通信超时), 0=正常
  */
uint8_t Modbus_IsFailSafeActive(void);

#ifdef __cplusplus
}
#endif
#endif /* __MODBUS_RTU_H */
c 复制代码
/**
  ******************************************************************************
  * @file    modbus_rtu.c
  * @brief   Modbus RTU 从站协议栈实现
  * @details 实现功能码 01/02/03/04/05/06/0F/10/11,寄存器映射见 modbus_rtu.h 头注释及产品说明书第4章。
  *          硬件链路:USART1(RS485) + PA8_DE,物理收发由 bsp_rs485 完成。
  *          业务关联(参数流向):
  *            - 继电器状态/动作次数/寿命告警 -> bsp_relay.c (BSP_Relay_*)
  *            - 数字输入状态             -> bsp_di.c     (BSP_DI_*)
  *            - 通信/故障 LED 脉冲        -> bsp_led.c    (BSP_LED_*)
  *            - 从机地址/波特率/故障安全   -> flash_param.c (FlashParam_*)
  *            - 运行时间/复位次数/看门狗   -> system_app.c  (SystemApp_*)
  *          协议主入口:Modbus_Init() 初始化 -> Modbus_Poll() 主循环轮询 -> Modbus_CheckFailSafe() 安全检测。
  ******************************************************************************
  */
#include "mid_mb_slave.h"
#include "bsp_rs485.h"
#include "bsp_relay.h"
#include "bsp_di.h"
#include "bsp_led.h"
#include "bsp_storage.h"
#include "app_main.h"
#include <string.h>

/* -------------------------- API 快速导航 --------------------------
 * void     Modbus_Init(void);                        Modbus 从站初始化(UART + DE 引脚)
 * void     Modbus_Poll(void);                        主循环轮询,接收帧解析与应答
 * void     Modbus_CheckFailSafe(void);               故障安全检测(通信超时则全断开)
 * uint16_t Modbus_GetFrameCount(void);               读取有效帧计数
 * uint16_t Modbus_GetCRCErrorCount(void);            读取 CRC 错误计数
 * uint16_t Modbus_GetExceptionCount(void);           读取异常响应计数
 * uint8_t  Modbus_IsFailSafeActive(void);            读取故障安全是否触发
 * ------------------------------------------------------------------ */

/* -------------------------- 寄存器映射定义(物理地址常量) -------------------------- */
/* 线圈 (Coils) 0x0000~0x0003 = RELAY1~4
   数量引用 BSP 层 RELAY_CH_NUM 单一数据源,扩展通道数只改 bsp_relay.h 一处 */
#define COIL_BASE_ADDR        0x0000u
#define COIL_COUNT            RELAY_CH_NUM

/* 离散输入 (Discrete Inputs) 0x0000~0x0003 = DI1~4
   数量引用 BSP 层 DI_CH_NUM 单一数据源,扩展通道数只改 bsp_di.h 一处 */
#define DI_BASE_ADDR          0x0000u
#define DI_COUNT              DI_CH_NUM

/* 输入寄存器 (Input Registers) 扩展映射 (#3/#4 诊断与设备信息) */
#define IR_BASE_ADDR          0x0000u
#define IR_COUNT              16u   /* 扩展到 16 个输入寄存器 */
#define IR_ADDR_UPTIME        0x0000u  /* 运行秒数(16位回绕) */
#define IR_ADDR_RESETCOUNT    0x0001u  /* 累计复位次数(16位) */
#define IR_ADDR_FRAMECOUNT    0x0002u  /* 有效帧计数(16位) */
#define IR_ADDR_CRCERROR      0x0003u  /* CRC 错误计数(16位) */
#define IR_ADDR_EXCEPCOUNT    0x0004u  /* 异常响应计数(16位, #3) */
#define IR_ADDR_RUNHOURS      0x0005u  /* 累计运行小时(16位, #4) */
#define IR_ADDR_RELAY1_ACT    0x0006u  /* 继电器1动作次数(16位, #3) */
#define IR_ADDR_RELAY2_ACT    0x0007u  /* 继电器2动作次数(16位, #3) */
#define IR_ADDR_RELAY3_ACT    0x0008u  /* 继电器3动作次数(16位, #3) */
#define IR_ADDR_RELAY4_ACT    0x0009u  /* 继电器4动作次数(16位, #3) */
#define IR_ADDR_LIFE_ALARM    0x000Au  /* 寿命告警位图(16位, bit0~3=#3) */
#define IR_ADDR_FW_VER        0x000Bu  /* 固件版本 0xMMmm (#4) */
#define IR_ADDR_DEV_TYPE      0x000Cu  /* 设备类型码 (#4, 0x0002=P02) */
#define IR_ADDR_SN_LOW        0x000Du  /* 序列号低16位 (#4, 取自芯片 UID) */
#define IR_ADDR_SN_HIGH       0x000Eu  /* 序列号高16位 (#4) */
#define IR_ADDR_FAILSAFE      0x000Fu  /* 故障安全态: bit0=使能 bit8=当前触发(#1) */

/* 保持寄存器 (Holding Registers) 扩展映射 (#1 故障安全可配) */
#define HR_BASE_ADDR          0x0000u
#define HR_COUNT              5u
#define HR_ADDR_SLAVE         0x0000u
#define HR_ADDR_BAUD          0x0001u
#define HR_ADDR_LINK          0x0002u
#define HR_ADDR_FAILSAFE_EN   0x0003u  /* 故障安全使能 0/1 (#1) */
#define HR_ADDR_FAILSAFE_SEC  0x0004u  /* 故障安全超时(秒) 0=禁用 (#1) */

/* Modbus 广播地址 */
#define MB_BROADCAST_ADDR     0x00u

/* #4 设备信息固定常量 */
#define FW_VERSION            0x0100u  /* 固件版本 v1.0 (高字节主版本, 低字节次版本) */
#define DEVICE_TYPE           0x0002u  /* 设备类型: P02 四通道继电器模块 */
/* STM32F1 96-bit Unique Device ID 基地址 (参考 RM0008 30.2) */
#define DEV_UID_BASE          0x1FFFF7E8u

/* -------------------------- 模块静态状态变量(统计/发送/接收/待应用参数) -------------------------- */
/* 统计计数 */
static uint16_t s_frame_count    = 0u;    /* 有效帧计数 (IR 0x0002) */
static uint16_t s_crc_err_count  = 0u;    /* CRC 错误计数 (IR 0x0003) */
static uint16_t s_ex_resp_count  = 0u;    /* 异常响应计数 (#3 诊断, IR 0x0004) */

/* #1 通信故障安全状态 */
static uint32_t s_last_valid_tick = 0u;   /* 最近一次有效帧时间戳(ms) */
static uint8_t  s_fail_safe_active = 0u;  /* 当前是否处于故障安全态 */

/* 参数变更 pending(0/0xFF 表示未变更,回复后再统一落盘 Flash) */
static uint8_t  s_pending_slave = 0u;     /* 0=不变, 非0=新值 */
static uint8_t  s_pending_baud  = 0xFFu;  /* 0xFF=不变 */
static uint8_t  s_pending_link  = 0xFFu;  /* 0xFF=不变 */
static uint8_t  s_pending_fs_en  = 0xFFu; /* 0xFF=不变 (#1 故障安全使能) */
static uint8_t  s_pending_fs_sec = 0xFFu; /* 0xFF=不变 (#1 故障安全超时秒) */
static uint8_t  s_need_apply_baud = 0u;   /* 需要在回复后切换波特率 */

/* 响应/接收缓冲 */
static uint8_t  s_tx_buf[RS485_TX_BUF_SIZE];   /* Modbus 响应组帧缓冲 */
static uint16_t s_tx_len = 0u;                 /* 当前响应帧长度 */
static uint8_t  s_rx_frame[RS485_RX_BUF_SIZE]; /* Modbus 接收帧缓存(由 Modbus_Poll 读取) */

/* -------------------------- 底层私有协议辅助函数(CRC/组帧/异常/设备信息) -------------------------- */
/**
  * @brief  Modbus CRC16 计算(多项式 0xA001,初始值 0xFFFF)
  * @param  data: 待计算数据指针
  * @param  len:  数据长度
  * @retval 16位 CRC 校验值(低字节在前,Modbus 小端序)
  * @note   被 MB_FinalizeCRC() 与 Modbus_Poll() 帧校验共用,为协议最底层原语
  */
static uint16_t MB_CRC16(const uint8_t *data, uint16_t len)
{
    uint16_t crc = 0xFFFFu;
    uint16_t i, j;
    for (i = 0u; i < len; i++)
    {
        crc ^= (uint16_t)data[i];
        for (j = 0u; j < 8u; j++)
        {
            if (crc & 0x0001u)
            {
                crc = (uint16_t)((crc >> 1) ^ 0xA001u);
            }
            else
            {
                crc >>= 1;
            }
        }
    }
    return crc;
}

/**
  * @brief  向响应缓冲追加一个字节
  * @param  b: 待追加字节
  * @note   受 s_tx_buf 容量保护(预留 2 字节 CRC 空间),越界静默丢弃
  *         调用链:各 FC 处理函数 -> MB_AppendByte/MB_AppendWord -> MB_FinalizeCRC
  */
static void MB_AppendByte(uint8_t b)
{
    if (s_tx_len < (RS485_TX_BUF_SIZE - 2u))
    {
        s_tx_buf[s_tx_len++] = b;
    }
}

/**
  * @brief  向响应缓冲追加一个 16 位字(Modbus 大端序:高字节在前)
  * @param  w: 待追加字
  */
static void MB_AppendWord(uint16_t w)
{
    MB_AppendByte((uint8_t)(w >> 8));
    MB_AppendByte((uint8_t)(w & 0xFFu));
}

/**
  * @brief  在响应缓冲末尾追加 CRC16(低字节在前)
  * @note   所有正常响应/异常响应的最后一步,统一在此收尾
  */
static void MB_FinalizeCRC(void)
{
    uint16_t crc = MB_CRC16(s_tx_buf, s_tx_len);
    /* CRC 低字节在前 (Modbus 小端序) */
    MB_AppendByte((uint8_t)(crc & 0xFFu));
    MB_AppendByte((uint8_t)((crc >> 8) & 0xFFu));
}

/**
  * @brief  组异常响应帧(功能码 | 0x80 + 异常码 + CRC)
  * @param  fc:         原请求功能码
  * @param  slave_addr: 从机地址(用于判断是否为广播帧)
  * @param  ex_code:    异常码(MB_EX_*)
  * @note   【关联指引】仅对"非广播"地址累加异常响应计数 s_ex_resp_count
  *         广播帧本就不回复,不应计入诊断计数;被所有 MB_Handle* 非法分支调用
  */
static void MB_BuildException(uint8_t fc, uint8_t slave_addr, uint8_t ex_code)
{
    /* 仅对"非广播"地址计异常响应 (#3):广播帧本就不回复, 不应计入 */
    if (slave_addr != MB_BROADCAST_ADDR)
    {
        s_ex_resp_count++;
    }
    s_tx_buf[0] = slave_addr;
    s_tx_buf[1] = (uint8_t)(fc | 0x80u);
    s_tx_buf[2] = ex_code;
    s_tx_len = 3u;
    MB_FinalizeCRC();
}

/**
  * @brief  读取 STM32 芯片 96-bit 唯一 ID,组合成 32-bit 序列号 (#4 设备信息)
  * @retval 32位唯一序列号(三段 32-bit 异或)
  * @note   被 MB_HandleReadInputRegs 的 IR_ADDR_SN_LOW/HIGH 分支调用,用于输入寄存器暴露设备 SN
  */
static uint32_t MB_GetDeviceSN(void)
{
    const uint32_t *uid = (const uint32_t *)DEV_UID_BASE;
    /* 三段 32-bit 异或, 得到一颗芯片唯一的 SN (读输入寄存器 0x000D/0x000E) */
    return (uid[0] ^ uid[1] ^ uid[2]);
}

/* -------------------------- 功能码处理(读/写线圈、离散输入、保持/输入寄存器) -------------------------- */
/**
  * @brief  0x01 读线圈(继电器)状态
  * @param  frame:       完整请求帧
  * @param  len:         请求帧长度
  * @param  slave_addr:  从机地址(用于异常帧寻址)
  * @note   线圈映射 BSP_Relay_Get(),bit0 对应起始线圈;越界返回异常 02/03
  */
static void MB_HandleReadCoils(const uint8_t *frame, uint16_t len, uint8_t slave_addr)
{
    uint16_t start, qty, i, byte_count;
    if (len < 8u)
    {
        /* addr+fc+start(2)+qty(2)+crc(2) = 8 */
        MB_BuildException(MB_FC_READ_COILS, slave_addr, MB_EX_ILLEGAL_DATA_VALUE);
        return;
    }
    start = (uint16_t)(((uint16_t)frame[2] << 8) | frame[3]);
    qty   = (uint16_t)(((uint16_t)frame[4] << 8) | frame[5]);

    if (qty < 1u || qty > COIL_COUNT ||
        (start - COIL_BASE_ADDR) + qty > COIL_COUNT)
    {
        MB_BuildException(MB_FC_READ_COILS, slave_addr, MB_EX_ILLEGAL_DATA_ADDRESS);
        return;
    }

    s_tx_buf[0] = slave_addr;
    s_tx_buf[1] = MB_FC_READ_COILS;
    byte_count = (qty + 7u) / 8u;
    s_tx_buf[2] = (uint8_t)byte_count;
    s_tx_len = 3u;
    /* 清零数据区 */
    for (i = 0u; i < byte_count; i++)
    {
        s_tx_buf[3u + i] = 0u;
    }
    /* 填充位 (bit0 = 起始线圈) */
    for (i = 0u; i < qty; i++)
    {
        uint8_t coil_idx = (uint8_t)(start - COIL_BASE_ADDR + i);
        if (BSP_Relay_Get((uint8_t)(coil_idx + 1u)))
        {
            s_tx_buf[3u + (i / 8u)] |= (uint8_t)(1u << (i % 8u));
        }
    }
    s_tx_len = 3u + byte_count;
    MB_FinalizeCRC();
}

/**
  * @brief  0x02 读离散输入(DI)状态
  * @param  frame:       完整请求帧
  * @param  len:         请求帧长度
  * @param  slave_addr:  从机地址
  * @note   映射 BSP_DI_Get(),逻辑同 0x01;越界返回异常 02/03
  */
static void MB_HandleReadDiscreteInputs(const uint8_t *frame, uint16_t len, uint8_t slave_addr)
{
    uint16_t start, qty, i, byte_count;
    if (len < 8u)
    {
        MB_BuildException(MB_FC_READ_DISCRETE_INPUTS, slave_addr, MB_EX_ILLEGAL_DATA_VALUE);
        return;
    }
    start = (uint16_t)(((uint16_t)frame[2] << 8) | frame[3]);
    qty   = (uint16_t)(((uint16_t)frame[4] << 8) | frame[5]);

    if (qty < 1u || qty > DI_COUNT ||
        (start - DI_BASE_ADDR) + qty > DI_COUNT)
    {
        MB_BuildException(MB_FC_READ_DISCRETE_INPUTS, slave_addr, MB_EX_ILLEGAL_DATA_ADDRESS);
        return;
    }

    s_tx_buf[0] = slave_addr;
    s_tx_buf[1] = MB_FC_READ_DISCRETE_INPUTS;
    byte_count = (qty + 7u) / 8u;
    s_tx_buf[2] = (uint8_t)byte_count;
    s_tx_len = 3u;
    for (i = 0u; i < byte_count; i++)
    {
        s_tx_buf[3u + i] = 0u;
    }
    for (i = 0u; i < qty; i++)
    {
        uint8_t di_idx = (uint8_t)(start - DI_BASE_ADDR + i);
        if (BSP_DI_Get((uint8_t)(di_idx + 1u)))
        {
            s_tx_buf[3u + (i / 8u)] |= (uint8_t)(1u << (i % 8u));
        }
    }
    s_tx_len = 3u + byte_count;
    MB_FinalizeCRC();
}

/**
  * @brief  0x03 读保持寄存器(从机地址/波特率/链路/故障安全)
  * @param  frame:       完整请求帧
  * @param  len:         请求帧长度
  * @param  slave_addr:  从机地址
  * @note   【关联指引】数据来源 FlashParam_Get(),寄存器定义见 HR_ADDR_* 宏;
  *         写保持寄存器的落盘在 Modbus_Poll 末尾统一调用 FlashParam_Save()
  */
static void MB_HandleReadHoldingRegs(const uint8_t *frame, uint16_t len, uint8_t slave_addr)
{
    uint16_t start, qty, i;
    const param_t *p;
    if (len < 8u)
    {
        MB_BuildException(MB_FC_READ_HOLDING_REGS, slave_addr, MB_EX_ILLEGAL_DATA_VALUE);
        return;
    }
    start = (uint16_t)(((uint16_t)frame[2] << 8) | frame[3]);
    qty   = (uint16_t)(((uint16_t)frame[4] << 8) | frame[5]);

    if (qty < 1u || qty > HR_COUNT ||
        (start - HR_BASE_ADDR) + qty > HR_COUNT)
    {
        MB_BuildException(MB_FC_READ_HOLDING_REGS, slave_addr, MB_EX_ILLEGAL_DATA_ADDRESS);
        return;
    }

    p = FlashParam_Get();
    s_tx_buf[0] = slave_addr;
    s_tx_buf[1] = MB_FC_READ_HOLDING_REGS;
    s_tx_buf[2] = (uint8_t)(qty * 2u);
    s_tx_len = 3u;
    for (i = 0u; i < qty; i++)
    {
        uint16_t reg_addr = (uint16_t)(start - HR_BASE_ADDR + i);
        uint16_t value = 0u;
        switch (reg_addr)
        {
			case HR_ADDR_SLAVE:       value = p->slave_addr;    break;
			case HR_ADDR_BAUD:        value = p->baud_index;    break;
			case HR_ADDR_LINK:        value = p->link_enable;   break;
			case HR_ADDR_FAILSAFE_EN: value = p->fail_safe_en;  break;  /* #1 */
			case HR_ADDR_FAILSAFE_SEC:value = p->fail_safe_sec; break;  /* #1 */
			default: break;
        }
        MB_AppendWord(value);
    }
    MB_FinalizeCRC();
}

/**
  * @brief  0x04 读输入寄存器(诊断计数/运行信息/设备信息,#3/#4)
  * @param  frame:       完整请求帧
  * @param  len:         请求帧长度
  * @param  slave_addr:  从机地址
  * @note   【关联指引】数据来源 SystemApp_* / BSP_Relay_* / FlashParam_* / MB_GetDeviceSN();
  *         寄存器地址定义见 IR_ADDR_* 宏;运行小时委托 SystemApp_GetRunHours() 计算
  */
static void MB_HandleReadInputRegs(const uint8_t *frame, uint16_t len, uint8_t slave_addr)
{
    uint16_t start, qty, i;
    if (len < 8u)
    {
        MB_BuildException(MB_FC_READ_INPUT_REGS, slave_addr, MB_EX_ILLEGAL_DATA_VALUE);
        return;
    }
    start = (uint16_t)(((uint16_t)frame[2] << 8) | frame[3]);
    qty   = (uint16_t)(((uint16_t)frame[4] << 8) | frame[5]);

    if (qty < 1u || qty > IR_COUNT ||
        (start - IR_BASE_ADDR) + qty > IR_COUNT)
    {
        MB_BuildException(MB_FC_READ_INPUT_REGS, slave_addr, MB_EX_ILLEGAL_DATA_ADDRESS);
        return;
    }

    s_tx_buf[0] = slave_addr;
    s_tx_buf[1] = MB_FC_READ_INPUT_REGS;
    s_tx_buf[2] = (uint8_t)(qty * 2u);
    s_tx_len = 3u;
    for (i = 0u; i < qty; i++)
    {
        uint16_t reg_addr = (uint16_t)(start - IR_BASE_ADDR + i);
        uint16_t value = 0u;
        switch (reg_addr)
        {
        case IR_ADDR_UPTIME:     value = SystemApp_GetUptime();        break;
        case IR_ADDR_RESETCOUNT: value = SystemApp_GetResetCount();    break;
        case IR_ADDR_FRAMECOUNT: value = s_frame_count;                break;
        case IR_ADDR_CRCERROR:   value = s_crc_err_count;              break;
        case IR_ADDR_EXCEPCOUNT: value = s_ex_resp_count;              break;  /* #3 */
        case IR_ADDR_RUNHOURS:   value = (uint16_t)SystemApp_GetRunHours(); break; /* #4 */
        case IR_ADDR_RELAY1_ACT: value = (uint16_t)BSP_Relay_GetActionCount(1u); break; /* #3 */
        case IR_ADDR_RELAY2_ACT: value = (uint16_t)BSP_Relay_GetActionCount(2u); break;
        case IR_ADDR_RELAY3_ACT: value = (uint16_t)BSP_Relay_GetActionCount(3u); break;
        case IR_ADDR_RELAY4_ACT: value = (uint16_t)BSP_Relay_GetActionCount(4u); break;
        case IR_ADDR_LIFE_ALARM: value = (uint16_t)BSP_Relay_GetLifeAlarmMask(); break; /* #3 */
        case IR_ADDR_FW_VER:     value = FW_VERSION;                   break;  /* #4 */
        case IR_ADDR_DEV_TYPE:   value = DEVICE_TYPE;                 break;  /* #4 */
        case IR_ADDR_SN_LOW:     value = (uint16_t)(MB_GetDeviceSN() & 0xFFFFu);        break; /* #4 */
        case IR_ADDR_SN_HIGH:    value = (uint16_t)((MB_GetDeviceSN() >> 16) & 0xFFFFu); break;
        case IR_ADDR_FAILSAFE:   /* #1 状态位图: bit0=使能, bit8=当前触发 */
            value = (uint16_t)((FlashParam_Get()->fail_safe_en ? 1u : 0u) |
                               (s_fail_safe_active ? 0x100u : 0u));
            break;
        default: break;
        }
        MB_AppendWord(value);
    }
    MB_FinalizeCRC();
}

/**
  * @brief  写单个线圈内部逻辑(0x05/0x0F 共用)
  * @param  addr:  线圈地址
  * @param  value: 0xFF00=吸合, 0x0000=断开
  * @retval 0=成功,非0=异常码(MB_EX_*)
  * @note   【关联指引】地址/数值校验后调用 BSP_Relay_Set() 执行,最终动作计数由继电器驱动维护
  */
static uint8_t MB_WriteSingleCoil(uint16_t addr, uint16_t value)
{
    uint8_t ch;
    if ((addr - COIL_BASE_ADDR) >= COIL_COUNT)
    {
        return MB_EX_ILLEGAL_DATA_ADDRESS;
    }
    if (value != 0xFF00u && value != 0x0000u)
    {
        return MB_EX_ILLEGAL_DATA_VALUE;
    }
    ch = (uint8_t)(addr - COIL_BASE_ADDR + 1u);
    BSP_Relay_Set(ch, (value == 0xFF00u) ? RELAY_ON : RELAY_OFF);
    return 0u;
}

/**
  * @brief  0x05 写单个线圈(继电器)
  * @param  frame:       完整请求帧
  * @param  len:         请求帧长度
  * @param  slave_addr:  从机地址
  * @note   正常响应回显请求(addr+fc+addr+value+CRC);非法则异常响应
  */
static void MB_HandleWriteSingleCoil(const uint8_t *frame, uint16_t len, uint8_t slave_addr)
{
    uint16_t addr, value;
    uint8_t  ex;
    if (len < 8u)
    {
        MB_BuildException(MB_FC_WRITE_SINGLE_COIL, slave_addr, MB_EX_ILLEGAL_DATA_VALUE);
        return;
    }
    addr = (uint16_t)(((uint16_t)frame[2] << 8) | frame[3]);
    value = (uint16_t)(((uint16_t)frame[4] << 8) | frame[5]);

    ex = MB_WriteSingleCoil(addr, value);
    if (ex != 0u)
    {
        MB_BuildException(MB_FC_WRITE_SINGLE_COIL, slave_addr, ex);
        return;
    }
    /* 正常响应: 回显请求 (addr+fc+addr(2)+value(2)+crc(2)) */
    s_tx_buf[0] = slave_addr;
    s_tx_buf[1] = MB_FC_WRITE_SINGLE_COIL;
    s_tx_len = 2u;
    MB_AppendWord(addr);
    MB_AppendWord(value);
    MB_FinalizeCRC();
}

/**
  * @brief  写单个保持寄存器内部逻辑(0x06/0x10 共用),仅写入 pending 缓存
  * @param  addr:  保持寄存器地址
  * @param  value: 待写入值
  * @retval 0=成功,非0=异常码(MB_EX_*)
  * @note   【关联指引】合法值暂存到 s_pending_* 全局,不直接落盘;
  *         真正的 FlashParam_Save() 在 Modbus_Poll 解析完成后统一调用,避免重复擦写 Flash
  */
static uint8_t MB_WriteSingleHoldingReg(uint16_t addr, uint16_t value)
{
    if ((addr - HR_BASE_ADDR) >= HR_COUNT)
    {
        return MB_EX_ILLEGAL_DATA_ADDRESS;
    }
    switch (addr)
    {
    case HR_ADDR_SLAVE:
        if (value < SLAVE_ADDR_MIN || value > SLAVE_ADDR_MAX)
        {
            return MB_EX_ILLEGAL_DATA_VALUE;
        }
        s_pending_slave = (uint8_t)value;
        break;
    case HR_ADDR_BAUD:
        if (value > BAUD_INDEX_MAX)
        {
            return MB_EX_ILLEGAL_DATA_VALUE;
        }
        s_pending_baud = (uint8_t)value;
        break;
    case HR_ADDR_LINK:
        if (value > 1u)
        {
            return MB_EX_ILLEGAL_DATA_VALUE;
        }
        s_pending_link = (uint8_t)value;
        break;
    case HR_ADDR_FAILSAFE_EN:   /* #1 故障安全使能 0/1 */
        if (value > FAILSAFE_EN_MAX)
        {
            return MB_EX_ILLEGAL_DATA_VALUE;
        }
        s_pending_fs_en = (uint8_t)value;
        break;
    case HR_ADDR_FAILSAFE_SEC:  /* #1 故障安全超时(秒) 0~255 */
        if (value > 255u)
        {
            return MB_EX_ILLEGAL_DATA_VALUE;
        }
        s_pending_fs_sec = (uint8_t)value;
        break;
    default:
        return MB_EX_ILLEGAL_DATA_ADDRESS;
    }
    return 0u;
}

/**
  * @brief  0x06 写单个保持寄存器
  * @param  frame:       完整请求帧
  * @param  len:         请求帧长度
  * @param  slave_addr:  从机地址
  * @note   正常响应回显;非法转发异常;落盘由 Modbus_Poll 统一处理
  */
static void MB_HandleWriteSingleReg(const uint8_t *frame, uint16_t len, uint8_t slave_addr)
{
    uint16_t addr, value;
    uint8_t  ex;
    if (len < 8u)
    {
        MB_BuildException(MB_FC_WRITE_SINGLE_REG, slave_addr, MB_EX_ILLEGAL_DATA_VALUE);
        return;
    }
    addr = (uint16_t)(((uint16_t)frame[2] << 8) | frame[3]);
    value = (uint16_t)(((uint16_t)frame[4] << 8) | frame[5]);

    ex = MB_WriteSingleHoldingReg(addr, value);
    if (ex != 0u)
    {
        MB_BuildException(MB_FC_WRITE_SINGLE_REG, slave_addr, ex);
        return;
    }
    /* 正常响应: 回显 */
    s_tx_buf[0] = slave_addr;
    s_tx_buf[1] = MB_FC_WRITE_SINGLE_REG;
    s_tx_len = 2u;
    MB_AppendWord(addr);
    MB_AppendWord(value);
    MB_FinalizeCRC();
}

/**
  * @brief  0x0F 写多个线圈(批量继电器控制)
  * @param  frame:       完整请求帧
  * @param  len:         请求帧长度
  * @param  slave_addr:  从机地址
  * @note   地址/字节数/总长度校验后逐位调用 BSP_Relay_Set();响应回显起始地址+数量
  */
static void MB_HandleWriteMultipleCoils(const uint8_t *frame, uint16_t len, uint8_t slave_addr)
{
    uint16_t start, qty, byte_count, i;
    if (len < 9u)
    {
        /* addr+fc+start(2)+qty(2)+bytecnt(1)+crc(2)=9, 最小 */
        MB_BuildException(MB_FC_WRITE_MULTIPLE_COILS, slave_addr, MB_EX_ILLEGAL_DATA_VALUE);
        return;
    }
    start = (uint16_t)(((uint16_t)frame[2] << 8) | frame[3]);
    qty   = (uint16_t)(((uint16_t)frame[4] << 8) | frame[5]);
    byte_count = frame[6];

    if (qty < 1u || qty > COIL_COUNT ||
        (start - COIL_BASE_ADDR) + qty > COIL_COUNT)
    {
        MB_BuildException(MB_FC_WRITE_MULTIPLE_COILS, slave_addr, MB_EX_ILLEGAL_DATA_ADDRESS);
        return;
    }
    if (byte_count != (uint16_t)((qty + 7u) / 8u))
    {
        MB_BuildException(MB_FC_WRITE_MULTIPLE_COILS, slave_addr, MB_EX_ILLEGAL_DATA_VALUE);
        return;
    }
    /* 检查总长度: addr+fc+start(2)+qty(2)+bytecnt(1)+data+crc(2) = 7+byte_count+2 */
    if (len < (uint16_t)(7u + byte_count + 2u))
    {
        MB_BuildException(MB_FC_WRITE_MULTIPLE_COILS, slave_addr, MB_EX_ILLEGAL_DATA_VALUE);
        return;
    }

    /* 执行写入 (地址已校验, 直接写入) */
    for (i = 0u; i < qty; i++)
    {
        uint8_t coil_idx = (uint8_t)(start - COIL_BASE_ADDR + i);
        uint8_t bit_val = (uint8_t)((frame[7u + (i / 8u)] >> (i % 8u)) & 0x01u);
        BSP_Relay_Set((uint8_t)(coil_idx + 1u), bit_val ? RELAY_ON : RELAY_OFF);
    }
    /* 响应: addr+fc+start(2)+qty(2)+crc(2) */
    s_tx_buf[0] = slave_addr;
    s_tx_buf[1] = MB_FC_WRITE_MULTIPLE_COILS;
    s_tx_len = 2u;
    MB_AppendWord(start);
    MB_AppendWord(qty);
    MB_FinalizeCRC();
}

/**
  * @brief  0x10 写多个保持寄存器
  * @param  frame:       完整请求帧
  * @param  len:         请求帧长度
  * @param  slave_addr:  从机地址
  * @note   逐寄存器调用 MB_WriteSingleHoldingReg 写入 pending;任一非法立即异常返回(已写入不回滚)
  */
static void MB_HandleWriteMultipleRegs(const uint8_t *frame, uint16_t len, uint8_t slave_addr)
{
    uint16_t start, qty, byte_count, i;
    uint8_t  ex = 0u;
    if (len < 9u)
    {
        MB_BuildException(MB_FC_WRITE_MULTIPLE_REGS, slave_addr, MB_EX_ILLEGAL_DATA_VALUE);
        return;
    }
    start = (uint16_t)(((uint16_t)frame[2] << 8) | frame[3]);
    qty   = (uint16_t)(((uint16_t)frame[4] << 8) | frame[5]);
    byte_count = frame[6];

    if (qty < 1u || qty > HR_COUNT ||
        (start - HR_BASE_ADDR) + qty > HR_COUNT)
    {
        MB_BuildException(MB_FC_WRITE_MULTIPLE_REGS, slave_addr, MB_EX_ILLEGAL_DATA_ADDRESS);
        return;
    }
    if (byte_count != (uint16_t)(qty * 2u))
    {
        MB_BuildException(MB_FC_WRITE_MULTIPLE_REGS, slave_addr, MB_EX_ILLEGAL_DATA_VALUE);
        return;
    }
    if (len < (uint16_t)(7u + byte_count + 2u))
    {
        MB_BuildException(MB_FC_WRITE_MULTIPLE_REGS, slave_addr, MB_EX_ILLEGAL_DATA_VALUE);
        return;
    }

    /* 逐个写入 (任一非法则返回异常, 已写入的不回滚 - 简化处理) */
    for (i = 0u; i < qty; i++)
    {
        uint16_t reg_addr = (uint16_t)(start - HR_BASE_ADDR + i);
        uint16_t value = (uint16_t)(((uint16_t)frame[7u + i * 2u] << 8) | frame[7u + i * 2u + 1u]);
        ex = MB_WriteSingleHoldingReg(reg_addr, value);
        if (ex != 0u)
        {
            MB_BuildException(MB_FC_WRITE_MULTIPLE_REGS, slave_addr, ex);
            return;
        }
    }
    /* 响应: addr+fc+start(2)+qty(2)+crc(2) */
    s_tx_buf[0] = slave_addr;
    s_tx_buf[1] = MB_FC_WRITE_MULTIPLE_REGS;
    s_tx_len = 2u;
    MB_AppendWord(start);
    MB_AppendWord(qty);
    MB_FinalizeCRC();
}

/**
  * @brief  0x11 Report Slave ID (#4 设备信息)
  * @param  frame:       完整请求帧(本命令无附加数据)
  * @param  len:         请求帧长度
  * @param  slave_addr:  从机地址
  * @note   响应:[addr][0x11][byte_count][slave_id][run_status=0xFF][extra=类型+版本][crc]
  */
static void MB_HandleReportSlaveID(const uint8_t *frame, uint16_t len, uint8_t slave_addr)
{
    uint8_t slave_id   = FlashParam_Get()->slave_addr;
    uint8_t run_status = 0xFFu;   /* 0xFF = 设备正常运行 */
    uint8_t extra[3];
    uint8_t extra_len = 3u;

    (void)frame; (void)len;  /* 该命令无附加请求数据, 仅校验帧完整性 */

    extra[0] = (uint8_t)(DEVICE_TYPE & 0xFFu);          /* 设备类型 */
    extra[1] = (uint8_t)((FW_VERSION >> 8) & 0xFFu);    /* 固件主版本 */
    extra[2] = (uint8_t)(FW_VERSION & 0xFFu);           /* 固件次版本 */

    s_tx_buf[0] = slave_addr;
    s_tx_buf[1] = MB_FC_REPORT_SLAVE_ID;
    s_tx_buf[2] = (uint8_t)(1u + 1u + extra_len);  /* 后续数据总字节数 */
    s_tx_len = 3u;
    MB_AppendByte(slave_id);
    MB_AppendByte(run_status);
    MB_AppendByte(extra[0]);
    MB_AppendByte(extra[1]);
    MB_AppendByte(extra[2]);
    MB_FinalizeCRC();
}

/* -------------------------- 对外公共接口(初始化 → 主轮询 → 状态查询/安全检测) -------------------------- */
/**
  * @brief  Modbus 从站初始化
  * @note   【关联指引】必须在 FlashParam_Init() 之后调用(依赖 FlashParam_Get()->baud_index);
  *         复位全部统计/状态/响应缓冲,并以当前时刻作为"最近有效帧"基准防止上电误触发故障安全
  */
void Modbus_Init(void)
{
    s_frame_count    = 0u;
    s_crc_err_count  = 0u;
    s_ex_resp_count  = 0u;
    s_pending_slave  = 0u;
    s_pending_baud   = 0xFFu;
    s_pending_link   = 0xFFu;
    s_pending_fs_en  = 0xFFu;
    s_pending_fs_sec = 0xFFu;
    s_need_apply_baud = 0u;
    s_fail_safe_active = 0u;
    /* #1 以当前时刻作为"最近有效帧"基准, 防止上电后立即误触发故障安全
     * (上电后 fail_safe_sec 秒内无通信属正常启动阶段) */
    s_last_valid_tick = HAL_GetTick();
    s_tx_len = 0u;

    /* 根据存储的波特率索引配置 RS485 */
    BSP_RS485_SetBaudrate(FlashParam_Get()->baud_index);
}

/**
  * @brief  Modbus 主任务处理(主循环高频调用,非阻塞)
  * @note   数据流:BSP_RS485_IsFrameReady 检测 -> BSP_RS485_GetFrame 取帧 -> CRC 校验
  *         -> 地址匹配 -> 功能码分发(MB_Handle*) -> 发送响应(BSP_RS485_Send)
  *         -> 落盘参数(FlashParam_Save) -> 必要时应用波特率
  *         任意有效帧刷新 s_last_valid_tick 并清除故障安全态(关联 Modbus_CheckFailSafe)
  */
void Modbus_Poll(void)
{
    uint16_t len = 0u;
    uint8_t  slave_addr;
    uint8_t  fc;
    uint8_t  is_broadcast;
    uint8_t  need_reply;
    uint16_t calc_crc, recv_crc;
    uint8_t  link_enable;

    if (!BSP_RS485_IsFrameReady())
    {
        return;
    }

    BSP_RS485_GetFrame(s_rx_frame, &len);

    /* 最小帧长度检查 (addr+fc+crc = 4) */
    if (len < 4u || len > RS485_RX_BUF_SIZE)
    {
        return;
    }

    /* CRC 校验 */
    calc_crc = MB_CRC16(s_rx_frame, (uint16_t)(len - 2u));
    recv_crc = (uint16_t)(((uint16_t)s_rx_frame[len - 1u] << 8) | s_rx_frame[len - 2u]);
    if (calc_crc != recv_crc)
    {
        s_crc_err_count++;
        return;
    }

    /* 地址匹配 */
    slave_addr = s_rx_frame[0];
    is_broadcast = (slave_addr == MB_BROADCAST_ADDR) ? 1u : 0u;
    if (!is_broadcast && slave_addr != FlashParam_Get()->slave_addr)
    {
        return;  /* 非本机 */
    }

    /* 有效帧计数 */
    s_frame_count++;

    /* #LED COMM 灯: 收到发往本机的有效帧即点亮 RS485 活动脉冲 */
    BSP_CommPulse();

    /* #1 通信故障安全: 收到任意有效帧即刷新最近有效帧时间戳(保活)。
     *   注: 故障安全态的"清除"刻意移到响应发送之后(见 BSP_RS485_Send 下方),
     *   以便 FC04@0x000F 等"读状态"响应能如实反映触发态(bit8), 符合协议文档 §9/§10/§11。 */
    s_last_valid_tick = HAL_GetTick();

    /* 通信使能检查: link_enable=0 静默模式 (不回复, 但写操作仍执行) */
    link_enable = FlashParam_Get()->link_enable;
    need_reply = (!is_broadcast && link_enable != 0u) ? 1u : 0u;

    /* 重置 pending */
    s_pending_slave  = 0u;
    s_pending_baud   = 0xFFu;
    s_pending_link   = 0xFFu;
    s_pending_fs_en  = 0xFFu;
    s_pending_fs_sec = 0xFFu;

    fc = s_rx_frame[1];
    s_tx_len = 0u;

    switch (fc)
    {
    case MB_FC_READ_COILS:
        MB_HandleReadCoils(s_rx_frame, len, slave_addr);
        break;
    case MB_FC_READ_DISCRETE_INPUTS:
        MB_HandleReadDiscreteInputs(s_rx_frame, len, slave_addr);
        break;
    case MB_FC_READ_HOLDING_REGS:
        MB_HandleReadHoldingRegs(s_rx_frame, len, slave_addr);
        break;
    case MB_FC_READ_INPUT_REGS:
        MB_HandleReadInputRegs(s_rx_frame, len, slave_addr);
        break;
    case MB_FC_WRITE_SINGLE_COIL:
        MB_HandleWriteSingleCoil(s_rx_frame, len, slave_addr);
        break;
    case MB_FC_WRITE_SINGLE_REG:
        MB_HandleWriteSingleReg(s_rx_frame, len, slave_addr);
        break;
    case MB_FC_WRITE_MULTIPLE_COILS:
        MB_HandleWriteMultipleCoils(s_rx_frame, len, slave_addr);
        break;
    case MB_FC_WRITE_MULTIPLE_REGS:
        MB_HandleWriteMultipleRegs(s_rx_frame, len, slave_addr);
        break;
    case MB_FC_REPORT_SLAVE_ID:
        MB_HandleReportSlaveID(s_rx_frame, len, slave_addr);
        break;
    default:
        /* 不支持的功能码 */
        if (need_reply)
        {
            MB_BuildException(fc, slave_addr, MB_EX_ILLEGAL_FUNCTION);
        }
        break;
    }

    /* 发送响应 (广播不回复) */
    if (need_reply && s_tx_len > 0u)
    {
        BSP_RS485_Send(s_tx_buf, s_tx_len);
    }

    /* #1 通信故障安全: 响应已发出, 此刻再清除故障安全态(恢复)。
     *   保证本次读状态响应(如 FC04@0x000F 的 bit8)如实反映"触发态",
     *   之后才恢复由主站控制(协议 §11)。读出即清, 符合 §9/§10 设计。 */
    if (s_fail_safe_active)
    {
        s_fail_safe_active = 0u;
        BSP_LED_SetFault(0u);  /* 通信恢复: 清除 FAULT 灯 */
    }

    /* 参数保存 (如有保持寄存器被修改, 含 #1 故障安全参数) */
    if (s_pending_slave != 0u || s_pending_baud != 0xFFu || s_pending_link != 0xFFu ||
        s_pending_fs_en != 0xFFu || s_pending_fs_sec != 0xFFu)
    {
        FlashParam_Save(s_pending_slave, s_pending_baud, s_pending_link,
                        s_pending_fs_en, s_pending_fs_sec);
        if (s_pending_baud != 0xFFu)
        {
            s_need_apply_baud = 1u;
        }
        /* #1 故障安全参数立即生效: 下次 Modbus_CheckFailSafe 读取 FlashParam_Get() 即为新值 */
    }

    /* 回复后应用波特率变更 (当前波特率回复已发送完毕) */
    if (s_need_apply_baud)
    {
        BSP_RS485_SetBaudrate(FlashParam_Get()->baud_index);
        s_need_apply_baud = 0u;
    }
}

/**
  * @brief  获取有效帧计数 (输入寄存器 0x0002)
  * @retval 累计有效帧数
  */
uint16_t Modbus_GetFrameCount(void)
{
    return s_frame_count;
}

/**
  * @brief  获取 CRC 错误计数 (输入寄存器 0x0003)
  * @retval 累计 CRC 校验失败帧数
  */
uint16_t Modbus_GetCRCErrorCount(void)
{
    return s_crc_err_count;
}

/**
  * @brief  获取异常响应计数 (#3 诊断)
  * @retval 累计主站非法请求触发的异常帧数
  * @note   来源 s_ex_resp_count,仅非广播帧计入;用于通信质量评估
  */
uint16_t Modbus_GetExceptionCount(void)
{
    return s_ex_resp_count;
}

/**
  * @brief  查询当前是否处于故障安全态 (#1)
  * @retval 1=已触发安全态(通信超时), 0=正常
  */
uint8_t Modbus_IsFailSafeActive(void)
{
    return s_fail_safe_active;
}

/**
  * @brief  #1 通信故障安全检测(主循环高频调用)
  * @note   若连续 fail_safe_sec 秒未收到有效 Modbus 帧, 自动将全部继电器
  *         切到安全态(断开), 防止上位机死机/断线导致设备失控。
  *         收到新有效帧(见 Modbus_Poll)后自动恢复, 由主站重新下发控制指令。
  *         【关联指引】状态变量 s_fail_safe_active / s_last_valid_tick 的写入在 Modbus_Poll 收到有效帧处,
  *         本函数仅读取并可能在超时时置位;置位后由 BSP_Relay_SetAll(0) 切安全态 + BSP_LED_SetFault(1) 告警
  */
void Modbus_CheckFailSafe(void)
{
    const param_t *p = FlashParam_Get();

    /* 故障安全未启用 或 超时时间为 0 -> 不处理 */
    if (p->fail_safe_en == 0u || p->fail_safe_sec == 0u)
    {
        return;
    }

    /* 已处于故障安全态: 持续保持继电器断开, 等待主站恢复通信 (不重复动作) */
    if (s_fail_safe_active)
    {
        return;
    }

    /* 计算距上次有效帧的空闲时长 (ms)。HAL_GetTick 32位回绕对减法安全 */
    uint32_t now       = HAL_GetTick();
    uint32_t elapsed   = now - s_last_valid_tick;
    uint32_t timeout_ms = (uint32_t)p->fail_safe_sec * 1000u;

    if (elapsed >= timeout_ms)
    {
        /* 触发故障安全: 切全部继电器到安全态(断开) + FAULT 灯快闪告警 */
        BSP_Relay_SetAll(0u);
        s_fail_safe_active = 1u;
        BSP_LED_SetFault(1u);
    }
}
相关推荐
choumin6 小时前
C程序能调用extern “C“声明的带默认参数的C++函数吗?
c语言·c++·extern c·默认参数
Funing76 小时前
系列一:STM32 外设初始化
stm32·单片机·嵌入式硬件
LONGZETECH7 小时前
传统实训 vs 仿真实训:从技术架构拆解新能源汽车教学的范式之变
c语言·3d·unity·架构·汽车
白白白小纯7 小时前
每日算法day3—回文链表,链表分割
c语言·数据结构·算法·leetcode
XiangrongZ7 小时前
debian中配置stm32的交叉编译环境
运维·stm32·debian
✎ ﹏梦醒͜ღ҉繁华落℘7 小时前
单片机基础知识--LVGL学习-lv_obj_add_flag()
c语言·单片机·学习
zcmodeltech8 小时前
钢铁冶金沙盘模型多工序协同控制系统设计:基于STM32与Modbus RTU的高炉-连铸-热轧全流程联动方案
stm32·单片机·嵌入式硬件·数据库架构·嵌入式实时数据库·多分类
Nebula嵌入式8 小时前
【C语言】01-从零开始:编译运行与第一个程序
c语言·嵌入式
luj_17689 小时前
随机性在算法与占卜中的共通原理
c语言·开发语言·c++·经验分享·算法
西城微科方案开发9 小时前
充气泵PCBA方案:数字传感器VS模拟传感器
单片机