从设备接收请求的状态机与超时机制

一. 从设备接收请求的核心框架

1.1 从设备主循环
cs 复制代码
// 从设备工作流程
modbus_new_rtu()              // 创建RTU上下文
modbus_set_slave()            // 设置自身从站地址
modbus_connect()              // 连接串口

modbus_mapping_new()          // 创建数据映射(线圈、寄存器等)

while (1) {
    // 1. 接收请求
    rc = modbus_receive(ctx, request);
    
    // 2. 处理请求并回复
    if (rc > 0) {
        modbus_reply(ctx, request, rc, mapping);
    }
}

// 清理资源
modbus_mapping_free();
modbus_close();
modbus_free();

1.2 接收请求的入口函数

cs 复制代码
int modbus_receive(modbus_t *ctx, uint8_t *req)
{
    if (ctx == NULL) {
        errno = EINVAL;
        return -1;
    }
    return ctx->backend->receive(ctx, req);  // 调用后端接收函数
}

二. RTU接收实现的核心机制

2.1 消息类型定义
cs 复制代码
typedef enum {
    MSG_INDICATION,     // 服务器端(从设备)接收的请求消息
    MSG_CONFIRMATION    // 客户端(主设备)接收的响应消息
} msg_type_t;
2.2 RTU接收函数逻辑
cs 复制代码
static int _modbus_rtu_receive(modbus_t *ctx, uint8_t *req)
{
    modbus_rtu_t *ctx_rtu = ctx->backend_data;
    
    if (ctx_rtu->confirmation_to_ignore) {
        // 忽略确认帧的特殊情况
        _modbus_receive_msg(ctx, req, MSG_CONFIRMATION);
        ctx_rtu->confirmation_to_ignore = FALSE;
        return 0;
    } else {
        // 正常接收指示帧(请求)
        int rc = _modbus_receive_msg(ctx, req, MSG_INDICATION);
        if (rc == 0) {
            ctx_rtu->confirmation_to_ignore = TRUE;
        }
        return rc;
    }
}

三. 接收状态机详解(核心)

3.1 状态机的三个阶段
cs 复制代码
开始接收
    ↓
_STEP_FUNCTION(读取2字节:地址+功能码)
    ↓ 根据功能码确定下一步要读多少字节
_STEP_META(读取元数据,长度不定)
    ↓ 根据元数据确定数据部分长度
_STEP_DATA(读取数据+CRC校验码)
    ↓
完成接收,进行完整性校验
3.2 状态机实现的核心循环
cs 复制代码
int _modbus_receive_msg(modbus_t *ctx, uint8_t *msg, msg_type_t msg_type)
{
    int step = _STEP_FUNCTION;            // 初始状态
    int msg_length = 0;                   // 已接收字节数
    int length_to_read = ctx->backend->header_length;  // 待读取字节数
    
    while (length_to_read != 0) {
        // 使用select等待数据(带超时)
        rc = ctx->backend->select(ctx, &rset, p_tv, length_to_read);
        if (rc == -1) {
            // 超时或错误处理
            return -1;
        }
        
        // 读取数据到缓冲区
        // ...
        
        // 状态机切换
        switch (step) {
            case _STEP_FUNCTION:
                // 处理功能码阶段
                break;
            case _STEP_META:
                // 处理元数据阶段
                break;
            case _STEP_DATA:
                // 处理数据阶段
                break;
        }
        
        // 更新已读取长度
        msg_length += rc_read;
        length_to_read -= rc_read;
    }
    
    // 完整性校验
    return ctx->backend->check_integrity(ctx, msg, msg_length);
}

四. 状态机各阶段详细解析

4.1 第一阶段:_STEP_FUNCTION
cs 复制代码
case _STEP_FUNCTION:
    /* 已经读取了地址和功能码,现在确定元数据长度 */
    length_to_read = compute_meta_length_after_function(
        msg[ctx->backend->header_length],  // 功能码
        msg_type                           // 消息类型
    );
    
    if (length_to_read != 0) {
        step = _STEP_META;      // 还有元数据要读
        break;
    }
    // 否则直接进入下一步

compute_meta_length_after_function() 的作用

  • 根据功能码确定接下来要读取的字节数

  • 不同的功能码有不同的元数据结构

常见功能码的元数据长度

功能码 功能 元数据长度 说明
0x01 读线圈 4字节 起始地址(2) + 数量(2)
0x0F 写多个线圈 5字节 起始地址(2) + 数量(2) + 字节数(1)
0x03 读保持寄存器 4字节 起始地址(2) + 数量(2)
0x10 写多个寄存器 5字节 起始地址(2) + 数量(2) + 字节数(1)
4.2 第二阶段:_STEP_META
cs 复制代码
case _STEP_META:
    /* 已经读取了元数据,现在确定数据部分长度 */
    length_to_read = compute_data_length_after_meta(ctx, msg, msg_type);
    
    /* 检查总长度是否超过限制 */
    if ((msg_length + length_to_read) > ctx->backend->max_adu_length) {
        errno = EMBBADDATA;
        return -1;  // 数据太多
    }
    
    step = _STEP_DATA;  // 进入数据读取阶段
    break;

关键函数:compute_data_length_after_meta()

cs 复制代码
// 以写多个线圈(0x0F)为例:
static int compute_data_length_for_0x0F(uint8_t *msg, int header_length)
{
    // 元数据的第5个字节是数据字节数
    int data_byte_count = msg[header_length + 5];
    // 数据部分 + CRC校验(2字节)
    return data_byte_count + 2;
}
4.3 第三阶段:_STEP_DATA
cs 复制代码
case _STEP_DATA:
    // 在这个阶段,length_to_read已经设置为需要读取的数据长度
    // 包括实际的数据和2字节的CRC
    // 循环会继续读取直到length_to_read变为0
    break;

五. 超时机制详细解析

5.1 三种超时时间
cs 复制代码
struct modbus {
    // ...
    struct timeval response_timeout;   // 响应超时
    struct timeval byte_timeout;       // 字节超时
    struct timeval indication_timeout; // 指示超时(从设备专用)
    // ...
};
5.2 超时选择逻辑
cs 复制代码
// 确定使用哪种超时
if (length_to_read > 0 &&
    (ctx->byte_timeout.tv_sec > 0 || ctx->byte_timeout.tv_usec > 0)) {
    /* 还有数据要读,使用字节超时 */
    tv.tv_sec = ctx->byte_timeout.tv_sec;
    tv.tv_usec = ctx->byte_timeout.tv_usec;
    p_tv = &tv;
} else {
    /* 使用响应超时或指示超时 */
    if (msg_type == MSG_INDICATION) {
        // 从设备等待请求
        tv.tv_sec = ctx->indication_timeout.tv_sec;
        tv.tv_usec = ctx->indication_timeout.tv_usec;
    } else {
        // 主设备等待响应
        tv.tv_sec = ctx->response_timeout.tv_sec;
        tv.tv_usec = ctx->response_timeout.tv_usec;
    }
    p_tv = &tv;
}
5.3 指示超时(indication_timeout)的特殊作用
  • 用途:从设备等待主设备请求的超时时间

  • 设置:如果不设置,默认使用response_timeout

  • 意义:防止从设备无限期等待,可以设置较长的超时时间

单片机中的典型设置

cs 复制代码
// 从设备等待请求,可以设置较长的超时
struct timeval timeout;
timeout.tv_sec = 10;      // 10秒
timeout.tv_usec = 0;
modbus_set_indication_timeout(ctx, &timeout);

六. 完整接收示例:功能码0x0F(写多个线圈)

6.1 报文结构回顾
cs 复制代码
请求报文(11个线圈,起始地址0x0013):
+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+
| 地址   | 功能码 | 起始地址高 | 起始地址低 | 数量高 | 数量低 | 字节数 | 数据1  | 数据2  | CRC低  | CRC高  |
+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+
| 0x05  | 0x0F  | 0x00  | 0x13  | 0x00  | 0x0B  | 0x02  | 0xD1  | 0x05  | 0xXX  | 0xXX  |
+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+
6.2 状态机处理过程

第一步:_STEP_FUNCTION

  • 读取2字节:[0x05, 0x0F]

  • 功能码=0x0F(写多个线圈)

  • 计算元数据长度:5字节(地址2+数量2+字节数1)

  • 切换到_STEP_META状态

第二步:_STEP_META

  • 读取5字节:[0x00, 0x13, 0x00, 0x0B, 0x02]

  • 解析:起始地址=0x0013(19),数量=0x000B(11),字节数=0x02

  • 计算数据部分长度:字节数(2) + CRC(2) = 4字节

  • 切换到_STEP_DATA状态

第三步:_STEP_DATA

  • 读取4字节:[0xD1, 0x05, CRC低, CRC高]

  • 完成接收,总长度=2+5+4=11字节

  • 进行CRC校验

七. 单片机实现要点

7.1 状态机简化实现
cs 复制代码
// 单片机中的接收状态机
enum {
    STATE_IDLE,           // 空闲状态
    STATE_RECEIVING,      // 接收中
    STATE_COMPLETE,       // 接收完成
    STATE_ERROR           // 错误状态
};

struct rtu_receiver {
    uint8_t buffer[256];      // 接收缓冲区
    int buf_index;            // 缓冲区索引
    int expected_length;      // 期望长度
    int state;                // 当前状态
    uint32_t last_receive_time; // 最后接收时间
};

// 串口中断处理
void UART_IRQHandler(void)
{
    if (UART_GetFlagStatus(UART_FLAG_RXNE)) {
        uint8_t data = UART_ReceiveData();
        
        switch (receiver.state) {
            case STATE_IDLE:
                // 等待地址
                if (data == slave_address) {
                    receiver.buffer[0] = data;
                    receiver.buf_index = 1;
                    receiver.state = STATE_RECEIVING;
                    receiver.expected_length = 2;  // 先收地址和功能码
                }
                break;
                
            case STATE_RECEIVING:
                receiver.buffer[receiver.buf_index++] = data;
                
                // 根据已接收的数据确定期望长度
                if (receiver.buf_index == 2) {
                    // 已经收到功能码,确定后续长度
                    receiver.expected_length = compute_expected_length(
                        receiver.buffer[1]  // 功能码
                    );
                }
                
                // 检查是否接收完成
                if (receiver.buf_index >= receiver.expected_length) {
                    receiver.state = STATE_COMPLETE;
                }
                break;
        }
        
        // 重置超时计时器
        receiver.last_receive_time = get_tick_count();
    }
}
5.7.2 超时处理实现
cs 复制代码
// 定时器中断处理超时
void TIMER_IRQHandler(void)
{
    uint32_t current_time = get_tick_count();
    uint32_t elapsed = current_time - receiver.last_receive_time;
    
    // 检查字节超时(如3.5个字符时间)
    if (receiver.state == STATE_RECEIVING && 
        elapsed > byte_timeout_ms) {
        // 超时,帧不完整
        receiver.state = STATE_ERROR;
        reset_receiver();
    }
    
    // 检查指示超时(从设备等待请求)
    if (receiver.state == STATE_IDLE && 
        elapsed > indication_timeout_ms) {
        // 长时间没有收到请求,可以进行超时处理
        handle_indication_timeout();
    }
}

八. 调试技巧与常见问题

8.1 调试状态机
cs 复制代码
// 添加调试输出
void debug_state_machine(int step, int length_to_read, int msg_length)
{
    printf("[状态机] 步骤: ");
    switch (step) {
        case _STEP_FUNCTION: printf("FUNCTION"); break;
        case _STEP_META: printf("META"); break;
        case _STEP_DATA: printf("DATA"); break;
    }
    printf(", 待读: %d, 已读: %d\n", length_to_read, msg_length);
}
8.2 常见问题排查

问题1:接收不完整

cs 复制代码
// 可能原因:
// 1. 字节超时太短
// 2. 波特率不匹配
// 3. 硬件问题

// 解决方法:
// 增加字节超时时间
struct timeval byte_timeout;
byte_timeout.tv_sec = 0;
byte_timeout.tv_usec = 500000;  // 500ms
modbus_set_byte_timeout(ctx, &byte_timeout);

问题2:状态机卡在某个阶段

cs 复制代码
// 可能原因:
// 1. 功能码处理错误
// 2. 长度计算错误
// 3. 缓冲区溢出

// 解决方法:
// 添加详细日志,检查每个阶段的计算

问题3:指示超时不工作

cs 复制代码
// 可能原因:
// 1. 没有正确设置indication_timeout
// 2. 超时值太小

// 解决方法:
// 明确设置指示超时
struct timeval indication_timeout;
indication_timeout.tv_sec = 30;  // 30秒
indication_timeout.tv_usec = 0;
modbus_set_indication_timeout(ctx, &indication_timeout);

九. 本章核心知识点总结

  1. 三层接收架构

    • 应用层:modbus_receive()

    • 后端层:_modbus_rtu_receive()

    • 核心层:_modbus_receive_msg()(状态机)

  2. 状态机三个阶段

    • STEP_FUNCTION:确定功能码,计算元数据长度

    • STEP_META:读取元数据,计算数据长度

    • STEP_DATA:读取数据+CRC,完成接收

  3. 超时机制

    • 字节超时:控制字符间间隔,防止死等

    • 响应超时:整体响应时间限制

    • 指示超时:从设备等待请求的超时

  4. 长度计算逻辑

    • 根据功能码→确定元数据长度

    • 根据元数据→确定数据长度

    • 动态调整期望接收长度

  5. 单片机实现要点

    • 使用中断+状态机实现

    • 硬件定时器处理超时

    • 注意字节顺序和CRC校验

  6. 调试关键

    • 打印状态机转换过程

    • 验证长度计算逻辑

    • 调整超时参数优化性能

相关推荐
dashizhi20151 小时前
如何备份服务器文件、服务器文件机密数据自动备份的方法
运维·服务器
HWL56791 小时前
在网页中实现WebM格式视频自动循环播放
前端·css·html·excel·音视频
捷智算云服务1 小时前
捷智算GPU维修中心构建服务器整机系统级保障体系
运维·服务器
鸡吃丸子1 小时前
前端视角下的埋点:实操指南与避坑要点
前端
前端摸鱼匠1 小时前
Vue 3 的ref在响应式对象中:介绍ref在reactive对象中的自动解包
前端·javascript·vue.js·前端框架·ecmascript
Zach_yuan1 小时前
从零理解 HTTP:协议原理、URL 结构与简易服务器实现
linux·服务器·网络协议·http
HWL56791 小时前
防止移动设备自动全屏播放视频,让视频在页面内嵌位置正常播放
前端·css·音视频
晚风吹长发1 小时前
初步了解Linux中的POSIX信号量及环形队列的CP模型
linux·运维·服务器·数据结构·c++·算法
Polaris_YJH1 小时前
使用Vue3+Vite+Pinia+elementUI搭建初级企业级项目
前端·javascript·elementui·vue