第一部分:理论模型回顾
在深入代码之前,我们必须先理解 libmodbus 要实现的理论模型。这两张图是整个库设计的灵魂。
1. ADU 与 PDU:协议的分层 
-
图片内容:此图展示了 Modbus 协议的数据帧结构。
-
核心概念:
-
PDU (协议数据单元) :这是 Modbus 协议的核心,与具体网络无关。它只有两部分:功能码 + 数据。功能码(如0x01读线圈)指定操作,数据区提供具体参数(如地址、数量)。
-
ADU (应用数据单元):这是在实际网络上传输的完整数据包。它在 PDU 的基础上,根据网络类型增加了"包装"。
-
串口 (RTU) :在 PDU 前面加从站地址 ,后面加CRC校验码。
-
以太网 (TCP) :在 PDU 前面加一个更复杂的MBAP头(包含事务ID、协议标识、长度等)。
-
-
软件库的任务:libmodbus 的核心工作之一,就是帮我们自动完成 PDU 到 ADU 的"打包"和"解包",这样我们程序员就不用自己处理这些琐碎的字节拼接了。
-
2. 事务处理模型:通信的流程 
-
图片内容:清晰地展示了 Modbus "请求-响应"的完整流程。
-
核心概念:
-
这是一个典型的客户端/服务器(主站/从站) 模型。
-
主站(客户端):发起"请求"(包含地址、功能码、数据)。
-
从站(服务器):接收请求 -> 执行操作(如读取线圈)-> 返回"响应"(包含地址、功能码、结果数据)。
-
异常处理:如果从站处理出错(比如地址不存在),它会返回一个"异常响应"(功能码最高位置1,并附带错误码)。
-
软件库的任务:libmodbus 为我们封装了建立连接、发送请求、接收并解析响应的整个流程。
-
第二部分:代码框架与初始化
现在,我们进入代码世界。这几张图展示了如何使用 libmodbus 库,以及库内部如何初始化。
3. 库的初始化与连接建立 


-
图片内容 :这几张图都是 C 代码片段,展示了如何创建 Modbus 上下文 (
modbus_new_*)、配置参数,并建立连接 (modbus_connect)。它们非常相似,是不同应用场景的例子。 -
保姆级解析:
-
选择后端 :libmodbus 支持多种通信方式(RTU串口、TCP网络)。你需要先告诉库你想用哪一种。代码中通过
if...else分支来选择。csif (use_backend == TCP) { ctx = modbus_new_tcp("192.168.1.100", 502); // 创建TCP连接上下文 } else { ctx = modbus_new_rtu("/dev/ttyUSB0", 115200, 'N', 8, 1); // 创建RTU串口上下文 }-
modbus_new_tcp:用于网络通信,参数是 IP 地址和端口号(通常是502)。 -
modbus_new_rtu:用于串口通信,参数是串口设备名(如COM1、/dev/ttyS0)、波特率、校验位、数据位、停止位。 -
返回值
ctx:这是一个非常重要的modbus_t类型的指针。你可以把它理解为一个 "Modbus连接句柄"或"遥控器" 。后续所有的操作(读、写、设置)都需要通过这个ctx来进行。
-
-
配置参数:创建上下文后,可以进行一些配置。
-
modbus_set_debug(ctx, TRUE);:打开调试模式。库会把发送和接收的原始字节数据打印出来,调试神器! -
modbus_set_slave(ctx, 1);:(仅RTU模式) 设置从站地址。当你作为主站时,这个函数设置的是你将要访问的从站设备的地址。
-
-
建立连接 :配置完成后,调用
modbus_connect(ctx)。对于TCP,这个函数会去连接目标服务器;对于RTU,它会打开串口设备。如果失败,会返回-1。
-
-
一句话总结 :展示了使用 libmodbus 编程的标准开头------"三板斧":1)
modbus_new_*创建上下文;2)modbus_set_*配置参数;3)modbus_connect建立连接。
第三部分:核心数据结构 

理解了如何创建连接,现在看看 libmodbus 内部是如何组织和管理这些连接信息的。
4. 核心数据结构:struct _modbus
-
图片内容 :这两张图展示了 libmodbus 库中最核心的数据结构
struct _modbus(在代码中通常用modbus_t这个别名)的定义。它就像一个"连接信息档案袋"。 -
保姆级解析:我们拆开这个结构体,看看里面都装了些什么宝贝:
csstruct _modbus { int slave; // 【从站地址】对于主站,它表示要访问的从站ID;对于从站,它表示自己的ID。 int s; // 【套接字/文件描述符】这是操作系统级别的通信句柄。TCP对应网络socket,RTU对应串口文件。 int debug; // 【调试开关】就是我们前面用 `modbus_set_debug` 设置的那个。 const modbus_backend_t *backend; // 【后端指针】这是**灵魂所在**!指向一个结构体,里面全是函数指针。 void *backend_data; // 【后端私有数据】给后端函数自己用的数据,比如TCP的IP信息,RTU的串口参数。 // ... 还有一些超时时间等字段 };-
backend指针是关键! 它指向一个modbus_backend_t结构体。这个结构体里定义了一大堆函数指针,比如:-
send:指向modbus_rtu_send或modbus_tcp_send。 -
receive:指向modbus_rtu_receive或modbus_tcp_receive。 -
build_request_basis:指向构建请求报文的函数(区分TCP和RTU的报文头)。
-
-
为什么这样设计? 这种设计模式叫 "策略模式" 或 "插件架构" 。
struct _modbus是通用框架,而backend是具体的实现插件。当你用modbus_new_rtu创建上下文时,backend就被赋值为指向 RTU 后端的函数集。这样,上层代码(比如modbus_read_bits)只需要调用ctx->backend->send(...),实际执行的就是对应的 RTU 或 TCP 发送函数。这实现了代码的完美解耦和复用。
-
-
一句话总结 :
struct _modbus是 libmodbus 库的心脏,它用backend指针实现了多协议支持,让同一套上层API可以操作不同的底层网络。
第四部分:核心工作流程 (图片5, 6, 9)
现在,我们来看库具体是怎么工作的。这几张图揭示了主站发送请求和从站处理请求的内部循环。
5. 主站:发送请求与接收响应的流程 
-
图片内容 :这张图展示了一个典型的主站"写入单个位"(
modbus_write_bit)函数内部的执行流程。 -
保姆级解析(跟着代码走):
-
构建请求 :
ctx->backend->build_request_basis(ctx, function, addr, value, req);- 调用
backend里的函数,根据是RTU还是TCP,把功能码、地址、数据等参数,组装成正确的 ADU 报文 ,存到req缓冲区里。
- 调用
-
发送报文 :
rc = send_msg(ctx, req, req_length);- 调用
send_msg函数(下图 ),它最终会调用ctx->backend->send,通过socket或串口把数据发出去。
- 调用
-
接收响应 :
rc = modbus_receive_msg(ctx, rsp, MSG_CONFIRMATION);- 等待并接收从站返回的数据,存到
rsp缓冲区。
- 等待并接收从站返回的数据,存到
-
检查确认 :
rc = check_confirmation(ctx, req, rsp, rc);- 校验响应报文:地址对不对?功能码对不对(异常响应?)?数据对不对?CRC校验对不对?
-
返回结果:把成功或失败的结果返回给用户。
-
-
流程总结 :构建 -> 发送 -> 接收 -> 校验 -> 返回 。这是所有读/写函数(
modbus_read_bits,modbus_write_register等)的通用模板。
6. send_msg 函数详解 
-
图片内容 :这是上上图中
send_msg函数的内部实现。 -
保姆级解析:
csstatic int send_msg(modbus_t *ctx, uint8_t *msg, int msg_length) { // 第一步:可能做一些发送前的预处理,比如RTU模式下计算并添加CRC msg_length = ctx->backend->send_msg_pre(msg, msg_length); // 第二步:如果打开了调试,把要发送的字节流打印出来(就是我们抓包看到的样子!) if (ctx->debug) { for (i = 0; i < msg_length; i++) printf("[%.2X]", msg[i]); } // 第三步:调用后端真正的发送函数,可能循环发送直到成功(如果设置了错误恢复) do { rc = ctx->backend->send(ctx, msg, msg_length); } while (rc == -1 && (ctx->error_recovery & MODBUS_ERROR_RECOVERY_LINK)); return rc; }- 这个函数是连接"通用逻辑"和"具体后端实现"的桥梁。它处理了调试输出 和链接错误恢复 等通用逻辑,然后把真正的发送任务派发给
backend->send。
- 这个函数是连接"通用逻辑"和"具体后端实现"的桥梁。它处理了调试输出 和链接错误恢复 等通用逻辑,然后把真正的发送任务派发给
7. 从站:请求处理循环 
-
图片内容 :这张图展示了一个 Modbus 从站服务器的主循环代码。
-
保姆级解析:
csfor (;;) { // 无限循环,一直等待请求 do { rc = modbus_receive(ctx, query); // 1. 接收请求报文 } while (rc == 0); // 可能过滤掉一些非本从站的请求 // ... 这里可能有一些特殊处理,比如模拟错误响应 ... rc = modbus_reply(ctx, query, rc, mb_mapping); // 2. 核心:处理请求并回复 if (rc == -1) { break; } // 处理出错则退出 }-
modbus_receive:从网络或串口读取一个完整的 Modbus 请求 ADU。 -
modbus_reply:这是从站的核心处理函数。它:-
解析请求报文(解包ADU,得到PDU里的功能码和地址)。
-
根据功能码,去一个叫
mb_mapping的内存映射表里(这个表模拟了线圈、寄存器等区域)读取或写入数据。 -
根据操作结果,构建一个正常的响应报文或异常响应报文。
-
调用
backend->send把响应发回去。
-
-
mb_mapping:这是一个重要的数据结构,由用户创建并传递进来。它包含了线圈、离散输入、保持寄存器、输入寄存器这四大区域在内存中的实际数组。从站设备的所有数据都"生活"在这个映射表里。
-
第五部分:应用场景与总结 
8. 实际应用场景
-
图片内容:这是一张硬件拓扑图,展示了工业场景中如何使用 Modbus。
-
核心讲解:
-
上位机 :通常是一台电脑或工控机,运行着用 libmodbus 编写的主站程序。
-
通信链路:
-
RS-485总线 :一种常见的串行通信标准,可以挂接多个设备。上位机通过 USB转485 或 串口卡 连接到总线。
-
以太网:有些高级设备支持 Modbus TCP,可以直接接入网络。
-
-
从站设备 :各种传感器、仪表、PLC等。每个设备都有一个唯一的从站地址(1-247)。
-
非Modbus设备 :需要额外的协议转换网关才能接入Modbus网络。
-
-
Libmodbus 的角色:在上位机的软件中,libmodbus 就是那个负责与下面所有 Modbus 设备"对话"的通信引擎。
最终总结:Libmodbus 框架精要
让我们把所有的知识点串起来,画一张完整的"心智图":
-
设计模式 :采用 "前后端分离" 的插件式架构。
-
前端 (libmodbus API) :提供统一的、友好的函数接口给程序员使用(如
modbus_read_registers)。 -
后端 (Backend):包含 RTU 和 TCP 两种实现,每种实现都提供一组标准的功能函数(send, receive, build_header...)。
-
连接上下文 (
modbus_t) :作为粘合剂,内部有一个backend指针,决定实际调用的后端函数。
-
-
工作流程:
-
主站 (客户端):
cs用户调用 modbus_write_bit() -> 库函数内部调用 backend->build_request_basis() 打包报文 -> 调用 send_msg() (内部调用 backend->send()) 发送 -> 调用 modbus_receive_msg() 接收响应 -> 调用 check_confirmation() 验证响应 -> 返回结果给用户 -
从站 (服务器):
cs无限循环 { 调用 modbus_receive() 等待请求 调用 modbus_reply() { 解析请求 操作 mb_mapping 内存映射表 构建响应 调用 backend->send() 发送响应 } }
-
-
学习意义:
-
理解工业通信:通过源码,你能彻底明白 Modbus 协议如何在字节流层面工作。
-
掌握优秀设计 :
backend指针和modbus_t结构体是抽象与接口编程的典范,这种设计模式在大型软件和驱动开发中极其常见。 -
提升调试能力 :知道了
debug开关和send_msg函数,你就能理解为什么 libmodbus 能打印出原始报文,这对解决现场通信问题至关重要。
-