Files
433_STM32/Core/Src/modbus_rtu_master.c

381 lines
15 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
******************************************************************************
* @file modbus_rtu_master.c
* @brief Modbus RTU Master 模块实现
* @note 本模块通过 RS485 (UART3) 以 Modbus RTU 协议轮询 Noris AMS 从站,
* 读取寄存器 41161 中的报警状态位,并提取为紧凑报警字节。
*
* 工作原理:
* 1. 每 1000ms 发送一次 Read Holding Registers (FC=0x03) 请求
* 2. 等待从站响应,支持帧内字符超时检测和响应总超时
* 3. 解析响应帧并进行 CRC16 校验
* 4. 从寄存器值中提取 Bit4-7 的报警位,映射为紧凑报警字节
* 5. 报警状态由 main.c 负责上报 (变化通知 + 心跳包)
*
* 状态机流转:
* IDLE → WAIT_POLL → WAIT_RESPONSE → PROCESS → IDLE
*
* @version 1.1
******************************************************************************
*/
#include "modbus_rtu_master.h"
#include "usart.h"
#include "main.h"
#include <string.h>
/* ================================================================
* CRC16-Modbus 查找表 (256 项)
* 多项式: 0xA001 (反转形式),初始值: 0xFFFF
* 用于 Modbus RTU 帧的 CRC 校验计算,采用查表法加速运算
* ================================================================ */
static const uint16_t crc16_table[256] = {
0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40,
0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841,
0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40,
0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41,
0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641,
0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040,
0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240,
0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441,
0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41,
0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840,
0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41,
0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40,
0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640,
0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041,
0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240,
0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441,
0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41,
0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840,
0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41,
0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40,
0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640,
0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041,
0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241,
0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440,
0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40,
0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841,
0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40,
0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41,
0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641,
0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040
};
/* ================================================================
* 状态机枚举定义
* Modbus RTU Master 使用四状态状态机驱动轮询过程
* ================================================================ */
/**
* @brief Modbus RTU 轮询状态机状态枚举
* - MB_STATE_IDLE: 空闲状态,等待轮询间隔到达
* - MB_STATE_WAIT_POLL: 准备发送,立即发送请求帧
* - MB_STATE_WAIT_RESPONSE: 已发送请求,等待从站响应
* - MB_STATE_PROCESS: 帧接收完成,解析响应数据
*/
typedef enum {
MB_STATE_IDLE,
MB_STATE_WAIT_POLL,
MB_STATE_WAIT_RESPONSE,
MB_STATE_PROCESS
} mb_state_t;
/* ================================================================
* 模块内部静态变量
* ================================================================ */
/** 当前状态机状态 */
static mb_state_t mb_state = MB_STATE_IDLE;
/** 当前状态的进入时刻 (用于超时判断) */
static uint32_t mb_state_tick = 0;
/** 上一次轮询的时刻 (用于计算轮询间隔) */
static uint32_t mb_last_poll_tick = 0;
/** 回波屏蔽截止时间 (发送后忽略自身回波的时间点) */
static uint32_t ignore_echo_until = 0;
/** 接收缓冲区: 存储从站响应的原始字节 */
static uint8_t mb_rx_buf[MODBUS_RTU_MAX_RX_BUF];
/** 接收缓冲区当前写入索引 */
static uint8_t mb_rx_idx = 0;
/** 最后一次接收到字节的时刻 (用于帧间超时判断) */
static uint32_t mb_rx_last_byte_tick = 0;
/** 当前报警状态字节 (紧凑格式Bit0-3 对应 4 种报警) */
static uint8_t mb_alarm_state = 0;
/** 上一次报警状态 (用于变化检测,初始值 0xFF 表示首次轮询) */
static uint8_t mb_alarm_last = 0xFF;
/** 最近一次成功读取的寄存器原始值 (16-bit大端序解析) */
static uint16_t mb_reg_val = 0;
/* ================================================================
* 内部静态函数实现
* ================================================================ */
/**
* @brief 计算 Modbus CRC16 校验值
* @note 采用查表法,多项式 0xA001初始值 0xFFFF
* CRC16-Modbus 算法: 低字节在前 (Little-Endian)
* @param data: 待计算的数据缓冲区指针
* @param len: 数据长度 (字节)
* @retval CRC16 校验值
*/
static uint16_t modbus_crc16(const uint8_t *data, uint16_t len)
{
uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < len; i++) {
crc = (crc >> 8) ^ crc16_table[(crc ^ data[i]) & 0xFF];
}
return crc;
}
/**
* @brief 从 16 位寄存器值中提取报警位,映射为紧凑报警字节
* @note 映射关系:
* 寄存器 Bit4 → 报警字节 Bit0 (火灾)
* 寄存器 Bit5 → 报警字节 Bit1 (水密门)
* 寄存器 Bit6 → 报警字节 Bit2 (舱底水)
* 寄存器 Bit7 → 报警字节 Bit3 (气体检测)
* @param reg: 16 位寄存器原始值
* @retval 紧凑报警字节 (Bit0-3 有效)
*/
static uint8_t extract_alarm_bits(uint16_t reg)
{
uint8_t alarm = 0;
if (NORIS_FIRE_ALARM(reg)) alarm |= (1 << 0);
if (NORIS_DOOR_ALARM(reg)) alarm |= (1 << 1);
if (NORIS_BILGE_ALARM(reg)) alarm |= (1 << 2);
if (NORIS_GAS_ALARM(reg)) alarm |= (1 << 3);
return alarm;
}
/**
* @brief 构造并发送 Modbus RTU Read Holding Registers 请求帧
* @note 帧格式: [从站地址][FC=0x03][起始地址H][起始地址L][数量H][数量L][CRC_L][CRC_H]
* 发送完成后设置回波屏蔽窗口,并清空接收缓冲区
* @retval 无
*/
static void send_modbus_request(void)
{
uint8_t tx[8];
/* 构造请求帧 PDU */
tx[0] = MODBUS_RTU_SLAVE_ADDR; /* 从站地址 */
tx[1] = 0x03; /* 功能码: Read Holding Registers */
tx[2] = (uint8_t)(MODBUS_RTU_TARGET_REG >> 8); /* 寄存器起始地址高字节 */
tx[3] = (uint8_t)(MODBUS_RTU_TARGET_REG & 0xFF); /* 寄存器起始地址低字节 */
tx[4] = (uint8_t)(MODBUS_RTU_REG_QTY >> 8); /* 读取数量高字节 */
tx[5] = (uint8_t)(MODBUS_RTU_REG_QTY & 0xFF); /* 读取数量低字节 */
/* 计算并附加 CRC16 (低字节在前) */
uint16_t crc = modbus_crc16(tx, 6);
tx[6] = (uint8_t)(crc & 0xFF); /* CRC 低字节 */
tx[7] = (uint8_t)(crc >> 8); /* CRC 高字节 */
/* 设置回波屏蔽窗口: 发送后 MODBUS_RTU_TX_ECHO_MARGIN 毫秒内忽略接收数据 */
ignore_echo_until = HAL_GetTick() + MODBUS_RTU_TX_ECHO_MARGIN;
/* 清空接收缓冲区,准备接收新响应 */
mb_rx_idx = 0;
/* 通过 UART3 (RS485) 阻塞发送请求帧 */
HAL_UART_Transmit(&huart3, tx, 8, HAL_MAX_DELAY);
}
/**
* @brief 解析 Modbus RTU 响应帧
* @note 校验流程:
* 1. 最小帧长度检查 (≥5 字节: 地址 + FC + 字节数 + CRC×2)
* 2. 从站地址匹配检查
* 3. 功能码检查 (FC=0x03 为正常响应FC=0x83 为异常响应,静默丢弃)
* 4. 数据字节数与期望值匹配检查
* 5. 实际帧长度与期望长度匹配检查
* 6. CRC16 校验
* 7. 提取寄存器值 (大端序)
* @retval true: 解析成功mb_reg_val 已更新
* false: 解析失败 (帧不完整/地址不匹配/CRC错误等)
*/
static bool parse_modbus_response(void)
{
/* 检查最小帧长度: 地址(1) + FC(1) + 字节数(1) + CRC(2) = 5 */
if (mb_rx_idx < 5) return false;
/* 检查从站地址是否匹配 */
if (mb_rx_buf[0] != MODBUS_RTU_SLAVE_ADDR) return false;
/* 检查功能码是否为 0x03 (正常响应); 0x83 为异常响应,静默丢弃 */
if (mb_rx_buf[1] != 0x03) return false;
/* 检查数据字节数是否与期望值匹配 (寄存器数量 × 2) */
uint8_t byte_count = mb_rx_buf[2];
if (byte_count != (MODBUS_RTU_REG_QTY * 2)) return false;
/* 计算期望帧总长度: 地址(1) + FC(1) + 字节数(1) + 数据 + CRC(2) */
uint16_t expected_len = 3 + byte_count + 2;
if (mb_rx_idx < expected_len) return false;
/* 提取并验证 CRC16 (低字节在前) */
uint16_t crc_received = (uint16_t)mb_rx_buf[expected_len - 2]
| ((uint16_t)mb_rx_buf[expected_len - 1] << 8);
uint16_t crc_calc = modbus_crc16(mb_rx_buf, expected_len - 2);
if (crc_received != crc_calc) return false;
/* 提取寄存器值 (大端序: 高字节在前) */
mb_reg_val = ((uint16_t)mb_rx_buf[3] << 8) | mb_rx_buf[4];
return true;
}
/* ================================================================
* 公共函数实现
* ================================================================ */
/**
* @brief Modbus RTU Master 模块初始化
* @note 将状态机重置为 IDLE 状态,清空所有缓冲区和状态变量。
* mb_alarm_last 初始化为 0xFF使得首次轮询结果不会触发变化通知
* (main.c 中通过 g_last_alarm_state == 0xFF 判断跳过首次上报)
*/
void ModbusRTU_Master_Init(void)
{
mb_state = MB_STATE_IDLE;
mb_state_tick = HAL_GetTick();
mb_last_poll_tick = HAL_GetTick();
mb_rx_idx = 0;
mb_alarm_state = 0;
mb_alarm_last = 0xFF;
mb_reg_val = 0;
ignore_echo_until = 0;
}
/**
* @brief Modbus RTU Master 主任务函数
* @note 在主循环中周期性调用,驱动四状态轮询状态机:
*
* IDLE: 等待轮询间隔 (MODBUS_RTU_POLL_INTERVAL = 1000ms)
* 间隔到达后切换到 WAIT_POLL
*
* WAIT_POLL: 立即发送 Modbus RTU 请求帧
* 发送完成后切换到 WAIT_RESPONSE
*
* WAIT_RESPONSE: 等待从站响应
* - 帧内字符超时到达 → 切换到 PROCESS 解析
* - 响应总超时到达 → 丢弃本次,回到 IDLE
*
* PROCESS: 解析响应帧
* - 解析成功 → 更新 mb_alarm_state
* - 解析失败 → 静默丢弃
* 处理完成后回到 IDLE
*
* 报警变化上报由 main.c 负责,本模块仅更新 mb_alarm_state
*/
void ModbusRTU_Master_Task(void)
{
switch (mb_state) {
/* ---- 空闲状态: 等待轮询间隔到达 ---- */
case MB_STATE_IDLE:
if ((int32_t)(HAL_GetTick() - mb_last_poll_tick) >= MODBUS_RTU_POLL_INTERVAL) {
mb_last_poll_tick = HAL_GetTick();
mb_state = MB_STATE_WAIT_POLL;
mb_state_tick = HAL_GetTick();
}
break;
/* ---- 准备发送状态: 构造并发送请求帧 ---- */
case MB_STATE_WAIT_POLL:
send_modbus_request();
mb_state = MB_STATE_WAIT_RESPONSE;
mb_state_tick = HAL_GetTick();
break;
/* ---- 等待响应状态: 等待从站响应或超时 ---- */
case MB_STATE_WAIT_RESPONSE:
/* 帧内字符超时: 已接收数据且最后一个字节之后超过间隔时间,
* 认为一帧数据接收完成 */
if (mb_rx_idx > 0 && (int32_t)(HAL_GetTick() - mb_rx_last_byte_tick) > MODBUS_RTU_INTER_CHAR_TIMEOUT) {
mb_state = MB_STATE_PROCESS;
break;
}
/* 响应总超时: 自发送请求后超过 MODBUS_RTU_RESP_TIMEOUT 仍未收到完整帧,
* 丢弃本次轮询,回到 IDLE 等待下次 */
if ((int32_t)(HAL_GetTick() - mb_state_tick) > MODBUS_RTU_RESP_TIMEOUT) {
mb_state = MB_STATE_IDLE;
}
break;
/* ---- 处理状态: 解析响应帧,更新报警状态 ---- */
case MB_STATE_PROCESS:
if (parse_modbus_response()) {
/* 解析成功: 从寄存器值提取报警位 */
mb_alarm_state = extract_alarm_bits(mb_reg_val);
if (mb_alarm_state != mb_alarm_last) {
/* 报警状态发生变化,更新记录值 */
mb_alarm_last = mb_alarm_state;
}
}
/* 无论解析成功与否,回到 IDLE 等待下一次轮询 */
mb_state = MB_STATE_IDLE;
break;
/* ---- 异常兜底: 未知状态强制回到 IDLE ---- */
default:
mb_state = MB_STATE_IDLE;
break;
}
}
/**
* @brief 向 Modbus RTU 接收缓冲区送入一个字节
* @note 在 UART3 接收中断回调中调用 (HAL_UART_RxCpltCallback → huart3 分支)
*
* 数据过滤逻辑:
* 1. 回波屏蔽: 发送请求后 MODBUS_RTU_TX_ECHO_MARGIN 毫秒内的数据全部丢弃,
* 避免将自身发送的请求帧回波误判为从站响应
* 2. 状态过滤: 仅在 WAIT_RESPONSE 状态下接收数据,
* 其他状态收到的数据直接丢弃
* 3. 缓冲区溢出保护: 接收索引超过缓冲区大小时停止写入
*
* @param byte: 从 UART3 接收到的原始字节
* @retval 无
*/
void ModbusRTU_FeedRxByte(uint8_t byte)
{
/* 屏蔽发送回波 */
if (HAL_GetTick() < ignore_echo_until) return;
/* 仅在等待响应状态下接收 */
if (mb_state != MB_STATE_WAIT_RESPONSE) return;
/* 缓冲区溢出保护 */
if (mb_rx_idx >= MODBUS_RTU_MAX_RX_BUF) return;
/* 存入缓冲区并更新时间戳 */
mb_rx_buf[mb_rx_idx++] = byte;
mb_rx_last_byte_tick = HAL_GetTick();
}
/**
* @brief 获取当前 Modbus RTU 报警状态
* @note 供 main.c 中的以下场景调用:
* 1. 变化通知: 与上次状态比较,变化时通过 PROTO_TYPE_IO (0x10) 上报
* 2. 心跳包: 每 30 秒将当前报警状态填入心跳 payload
* @retval 紧凑报警字节:
* Bit0 = 火灾报警 (对应寄存器 Bit4)
* Bit1 = 水密门报警 (对应寄存器 Bit5)
* Bit2 = 舱底水报警 (对应寄存器 Bit6)
* Bit3 = 气体检测报警 (对应寄存器 Bit7)
*/
uint8_t ModbusRTU_GetAlarmState(void)
{
return mb_alarm_state;
}