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