目录
- 一、前言
- 二、从设备接收请求完整执行流程
- [三、接收核心函数与 RTU 后端解析](#三、接收核心函数与 RTU 后端解析)
- [四、_modbus_receive_msg 接收流程拆解](#四、_modbus_receive_msg 接收流程拆解)
- 五、接收超时机制详解
- 六、总结
- 七、结尾
一、前言
在上一篇笔记中,我们深入拆解了 libmodbus 主设备发送请求的完整源码流程,掌握了 Modbus 报文的构造、校验与发送逻辑。而 Modbus 通信是双向交互的,从设备的核心职责是接收主设备的请求报文、解析并执行对应操作、返回响应结果。本次笔记将聚焦 libmodbus 从设备接收请求的场景,拆解从初始化到报文接收、完整性校验的完整流程,重点解析核心接收函数与超时机制,帮助你完整掌握 libmodbus 的双向通信逻辑,为后续实现完整的 Modbus 主从通信打下基础。
二、从设备接收请求完整执行流程
从设备接收并响应主设备请求的完整代码执行流程如下,其中初始化流程与主设备发送请求完全一致:
modbus_new_rtu→modbus_set_slave→modbus_connect→modbus_mapping_new_start_address→ 循环执行(modbus_receive→modbus_send_raw_request/modbus_reply)→modbus_mapping_free→modbus_close→modbus_free
其中,初始化阶段(modbus_new_rtu、modbus_set_slave、modbus_connect)的逻辑与主设备完全相同,核心是创建 RTU 上下文、设置自身从设备地址、打开并配置串口,为接收报文做好准备。
支撑整个 RTU 模式操作的核心后端结构体依然是_modbus_rtu_backend,完整定义如下:
c
const modbus_backend_t _modbus_rtu_backend = {
_MODBUS_BACKEND_TYPE_RTU, // 后端类型:RTU模式
_MODBUS_RTU_HEADER_LENGTH, // RTU报文头部长度
_MODBUS_RTU_CHECKSUM_LENGTH, // RTU报文校验码长度
MODBUS_RTU_MAX_ADU_LENGTH, // RTU报文最大允许长度
_modbus_set_slave, // 设置从设备地址
_modbus_rtu_build_request_basis, // 构造请求报文基础部分
_modbus_rtu_build_response_basis, // 构造响应报文基础部分
_modbus_rtu_prepare_response_tid, // 准备响应报文事务ID
_modbus_rtu_send_msg_pre, // 发送报文预处理(计算CRC)
_modbus_rtu_send, // 实际发送报文
_modbus_rtu_receive, // 接收报文核心逻辑(从设备入口)
_modbus_rtu_recv, // 底层串口读取数据
_modbus_rtu_check_integrity, // 校验报文完整性
_modbus_rtu_pre_check_confirmation, // 预校验响应报文
_modbus_rtu_connect, // 建立串口连接
_modbus_rtu_is_connected, // 检查串口连接状态
_modbus_rtu_close, // 关闭串口连接
_modbus_rtu_flush, // 刷新串口缓冲区
_modbus_rtu_select, // 串口超时等待选择
_modbus_rtu_free // 释放RTU相关资源
};
补充:从设备与主设备共用同一个 RTU 后端结构体,仅在函数调用的场景与
msg_type标志位上存在差异,这是 libmodbus 模块化设计的高效体现。
三、接收核心函数与 RTU 后端解析
从设备接收请求的入口函数是modbus_receive,其核心作用是调用后端绑定的receive函数,完成报文的接收逻辑,源码如下:
c
int modbus_receive(modbus_t *ctx, uint8_t *req)
{
// 校验Modbus上下文是否有效
if (ctx == NULL) {
errno = EINVAL;
return -1;
}
// 调用RTU后端的receive函数,执行实际接收逻辑
return ctx->backend->receive(ctx, req);
}
该函数最终会调用_modbus_rtu_receive,这是 RTU 模式下接收报文的核心逻辑实现,源码如下:
c
static int _modbus_rtu_receive(modbus_t *ctx, uint8_t *req)
{
int rc;
modbus_rtu_t *ctx_rtu = ctx->backend_data;
// 若存在需要忽略的确认报文,先接收并忽略
if (ctx_rtu->confirmation_to_ignore) {
_modbus_receive_msg(ctx, req, MSG_CONFIRMATION);
/* Ignore errors and reset the flag */
ctx_rtu->confirmation_to_ignore = FALSE;
rc = 0;
if (ctx->debug) {
printf("Confirmation to ignore\n");
}
} else {
// 接收指示报文(主设备发送的请求报文)
rc = _modbus_receive_msg(ctx, req, MSG_INDICATION);
if (rc == 0) {
/* The next expected message is a confirmation to ignore */
ctx_rtu->confirmation_to_ignore = TRUE;
}
}
return rc;
}
从源码中可以发现,从设备与主设备共用同一个_modbus_receive_msg函数完成报文接收:
c
int _modbus_receive_msg(modbus_t *ctx, uint8_t *msg, msg_type_t msg_type)
其中,msg_type_t msg_type是区分主设备与从设备的关键标志位:MSG_INDICATION表示从设备接收主设备的请求报文,MSG_CONFIRMATION表示主设备接收从设备的响应报文,通过该标志位,实现了同一个函数支撑双向通信的接收需求。
四、_modbus_receive_msg 接收流程拆解
_modbus_receive_msg是 libmodbus 接收报文的核心函数,采用状态机模式逐步读取报文数据,完整流程分为四步,具体如下:
第零步:确认初始读取字节数
首先初始化接收状态与需要读取的初始字节数,核心是读取报文头部与功能码,源码如下:
c
/* We need to analyse the message step by step. At the first step, we want
* to reach the function code because all packets contain this
* information. */
step = _STEP_FUNCTION;
// 初始读取长度:头部长度 + 1字节功能码
length_to_read = ctx->backend->header_length + 1;
第一步:等待数据到达
通过循环等待串口数据到达,支持超时配置,源码如下:
c
while (length_to_read != 0) {
// 等待串口数据,超时时间由p_tv指定
rc = ctx->backend->select(ctx, &rset, p_tv, length_to_read);
if (rc == -1) {
_error_print(ctx, "select");
if (ctx->error_recovery & MODBUS_ERROR_RECOVERY_LINK) {
补充:
p_tv是指向超时时间结构体的指针,后续会详细解析其配置逻辑,这是实现接收超时机制的核心。
第二步:读取底层原始数据
当检测到串口有数据后,调用底层recv函数读取原始字节数据,源码如下:
c
// 从串口读取数据,写入msg缓冲区(偏移msg_length确保数据不覆盖)
rc = ctx->backend->recv(ctx, msg + msg_length, length_to_read);
if (rc == 0) {
// 读取失败,设置连接重置错误
errno = ECONNRESET;
rc = -1;
}
只有当返回值rc不为 - 1 时,说明读取到了有效原始数据,才能进入后续的状态机处理流程。
第三步:状态机分阶段读取完整报文(核心)
这一步是接收流程的核心,通过状态机判断当前报文接收阶段,计算后续需要读取的字节数,循环读取直到获取完整报文,源码如下:
c
/* Sums bytes received */
msg_length += rc;
/* Computes remaining bytes */
length_to_read -= rc;
if (length_to_read == 0) {
switch (step) {
case _STEP_FUNCTION:
/* Function code position */
// 根据功能码计算后续需要读取的元数据长度
length_to_read = compute_meta_length_after_function(
msg[ctx->backend->header_length], msg_type);
if (length_to_read != 0) {
step = _STEP_META;
break;
} /* else switches straight to the next step */
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;
_error_print(ctx, "too many data");
return -1;
}
step = _STEP_DATA;
break;
整个状态机分为三个阶段,对应 Modbus 报文的结构:
_STEP_FUNCTION:读取设备地址与功能码,这是所有 Modbus 报文的必备字段;_STEP_META:读取元数据(起始地址、寄存器数量等),根据功能码计算所需长度;_STEP_DATA:读取业务数据与校验码,根据元数据计算所需长度。
第四步:校验报文完整性
当报文读取完成后,调用后端的check_integrity函数校验报文的完整性(核心是校验 CRC16 校验码),源码如下:
c
// 校验报文完整性,返回校验结果
return ctx->backend->check_integrity(ctx, msg, msg_length);
接收流程总结
Modbus 报文接收的核心逻辑可概括为:通过状态机分三阶段读取数据,先读取设备地址与功能码,再根据功能码确定元数据长度,最后根据元数据确定业务数据与校验码长度,最终校验报文完整性。无论是从设备接收请求,还是主设备接收响应,都遵循这一统一流程。
五、接收超时机制详解
libmodbus 的接收流程中设计了两层超时机制,分别对应不同的接收阶段,确保在无数据传输时不会无限阻塞,提升程序的健壮性,核心源码与解析如下:
1. 第一阶段超时:indication_timeout(请求等待超时)
该超时时间是从设备等待主设备发送请求的总超时时间,源码如下:
c
if (msg_type == MSG_INDICATION) {
/* Wait for a message, we don't know when the message will be
* received */
if (ctx->indication_timeout.tv_sec == 0 && ctx->indication_timeout.tv_usec == 0) {
/* By default, the indication timeout isn't set */
p_tv = NULL;
} else {
/* Wait for an indication (name of a received request by a server, see schema)
*/
tv.tv_sec = ctx->indication_timeout.tv_sec;
tv.tv_usec = ctx->indication_timeout.tv_usec;
p_tv = &tv;
}
indication_timeout:从机等待主机请求的超时时间,若为主机调用,则对应等待从机响应的超时时间;- 配置为 0(秒与微秒均为 0):采用查询方式,不阻塞,直接返回是否有数据;
- 配置为非 0 值:采用阻塞方式,等待指定时间,超时后返回错误,该方式在 FreeRTOS 中对其他任务的执行影响较小。
该超时主要作用于第一阶段(设备地址与功能码)的等待,是整个报文接收的总超时控制。
2. 后续阶段超时:byte_timeout(字节传输超时)
当读取到设备地址与功能码后,后续元数据、业务数据的传输采用byte_timeout超时机制,源码如下:
c
tv.tv_sec = ctx->byte_timeout.tv_sec;
tv.tv_usec = ctx->byte_timeout.tv_usec;
p_tv = &tv;
byte_timeout:单个字节传输的超时时间,仅作用于报文内部的后续字节读取;- 该超时时间通常配置得较短,仅等待单个字节的传输耗时,若超时则判定报文传输异常,返回错误。
补充:两层超时机制的设计,既保证了整个请求接收的灵活性(总超时),又保证了报文内部传输的高效性(字节超时),有效提升了 Modbus 通信的健壮性,避免了无效的长时间阻塞。
六、总结
- 从设备接收请求流程:初始化(同主设备)→ 循环接收(
modbus_receive)→ 解析响应 → 释放资源,共用 RTU 后端结构体; - 核心接收函数
_modbus_receive_msg采用状态机分三阶段读取报文,最终校验 CRC 完整性,主从设备通过msg_type区分; - 两层超时机制:
indication_timeout(总请求超时)与byte_timeout(单个字节超时),支持查询与阻塞两种模式,提升通信健壮性。
七、结尾
本次我们完整拆解了 libmodbus 从设备接收请求的源码流程,掌握了状态机报文读取与双层超时机制的核心逻辑,至此 libmodbus 的主从双向通信核心流程已全部解析完毕。这些底层逻辑不仅是使用 libmodbus 开发的基础,更是实现 Modbus 协议二次开发与裸机移植的关键。吃透这些内容,能够大幅提升你在工业通信领域的开发能力与故障排查效率。感谢各位的阅读,持续关注本系列笔记,一起深耕 Modbus 与嵌入式开源库开发,解锁更多实战技能与工业应用场景!