一. 从设备接收请求的核心框架
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);
九. 本章核心知识点总结
-
三层接收架构:
-
应用层:
modbus_receive() -
后端层:
_modbus_rtu_receive() -
核心层:
_modbus_receive_msg()(状态机)
-
-
状态机三个阶段:
-
STEP_FUNCTION:确定功能码,计算元数据长度
-
STEP_META:读取元数据,计算数据长度
-
STEP_DATA:读取数据+CRC,完成接收
-
-
超时机制:
-
字节超时:控制字符间间隔,防止死等
-
响应超时:整体响应时间限制
-
指示超时:从设备等待请求的超时
-
-
长度计算逻辑:
-
根据功能码→确定元数据长度
-
根据元数据→确定数据长度
-
动态调整期望接收长度
-
-
单片机实现要点:
-
使用中断+状态机实现
-
硬件定时器处理超时
-
注意字节顺序和CRC校验
-
-
调试关键:
-
打印状态机转换过程
-
验证长度计算逻辑
-
调整超时参数优化性能
-