目录
- 一、前言
- [二、分析场景:modbus_write_bits 执行流程](#二、分析场景:modbus_write_bits 执行流程)
- [三、第一步:创建 RTU 上下文(modbus_new_rtu)](#三、第一步:创建 RTU 上下文(modbus_new_rtu))
- 四、第二步:设置从设备地址(modbus_set_slave)
- 五、第三步:建立串口连接(modbus_connect)
- 六、第四步:构造写请求(modbus_write_bits)
- [七、第五步:发送请求报文(send_msg 系列函数)](#七、第五步:发送请求报文(send_msg 系列函数))
- [八、RTU 后端核心结构解读](#八、RTU 后端核心结构解读)
- 九、总结
- 十、结尾
一、前言
在上一篇笔记中,我们完成了 libmodbus 库的介绍、源码获取与阅读工具的实操,为深入研读源码打下了基础。libmodbus 的核心价值在于封装了 Modbus 协议的底层细节,而发送请求是 Modbus 主设备的核心操作之一。本次笔记将以modbus_write_bits(写多个线圈)函数为例,拆解 libmodbus 发送请求的完整执行流程,深入解析每一步的源码逻辑与核心结构体作用,帮助你理解 libmodbus 的底层封装思路,为后续的二次开发与裸机移植提供理论支撑。
二、分析场景:modbus_write_bits 执行流程
本次源码分析聚焦于 Modbus 主设备发送写请求的场景,以modbus_write_bits函数为核心线索,逐步拆解从上下文创建到报文发送的完整流程,整个流程分为五大核心步骤,每一步对应 libmodbus 的关键 API 与底层实现。
补充:
modbus_write_bits函数用于向 Modbus 从设备写入多个线圈状态(对应 1 位数字量输出),是工业控制中常用的写操作 API,其底层封装了 Modbus RTU 报文的构造、校验与发送逻辑。
三、第一步:创建 RTU 上下文(modbus_new_rtu)
主设备发送请求的第一步,是调用modbus_new_rtu函数创建 Modbus RTU 模式的上下文,该函数的核心作用是分配内存、初始化核心结构体并设置 RTU 后端属性,核心源码如下:
c
// 分配modbus上下文结构体内存,用于表示整个Modbus总线环境
ctx = (modbus_t *) malloc(sizeof(modbus_t));
if (ctx == NULL) {
return NULL;
}
// 初始化上下文公共属性
_modbus_init_common(ctx);
// 设置RTU模式后端,绑定RTU相关的操作函数集
ctx->backend = &_modbus_rtu_backend;
// 分配RTU模式专属数据结构体内存,用于保存串口相关属性
ctx->backend_data = (modbus_rtu_t *) malloc(sizeof(modbus_rtu_t));
if (ctx->backend_data == NULL) {
modbus_free(ctx);
errno = ENOMEM;
return NULL;
}
ctx_rtu = (modbus_rtu_t *) ctx->backend_data;
/* Device name and \0 */
// 分配内存并保存串口设备名称(如Windows的COM1、Linux的/dev/ttyUSB0)
ctx_rtu->device = (char *) malloc((strlen(device) + 1) * sizeof(char));
if (ctx_rtu->device == NULL) {
modbus_free(ctx);
errno = ENOMEM;
return NULL;
}
#if defined(_WIN32)
// Windows平台下复制串口设备名称
strcpy_s(ctx_rtu->device, strlen(device) + 1, device);
#else
// 类Unix平台下复制串口设备名称
strcpy(ctx_rtu->device, device);
#endif
其中核心的结构体赋值逻辑如下,用于绑定 RTU 后端并初始化串口相关数据:
c
// 绑定Modbus RTU模式的后端操作函数集
ctx->backend = &_modbus_rtu_backend;
// 分配RTU专属数据结构体,保存串口设备名、波特率等属性
ctx->backend_data = (modbus_rtu_t *) malloc(sizeof(modbus_rtu_t));
modbus_new_rtu函数的核心流程可总结为:分配上下文内存→初始化公共属性→设置 RTU 后端→分配并初始化 RTU 专属串口数据,为后续的 Modbus 操作搭建基础环境。
四、第二步:设置从设备地址(modbus_set_slave)
创建完 RTU 上下文后,需要调用modbus_set_slave函数指定目标从设备地址,确保请求报文能够被正确的从机接收,核心代码如下:
首先是主设备调用的设置逻辑:
c
if (use_backend == RTU) {
// 设置目标从设备ID(SERVER_ID为预定义的从机地址)
modbus_set_slave(ctx, SERVER_ID);
}
modbus_set_slave函数的底层实现如下:
c
int modbus_set_slave(modbus_t *ctx, int slave)
{
// 校验上下文是否有效
if (ctx == NULL) {
errno = EINVAL;
return -1;
}
// 调用后端绑定的set_slave函数,设置从设备地址
return ctx->backend->set_slave(ctx, slave);
}
该函数的核心作用是将传入的从设备 ID 赋值给modbus_t结构体的slave字段,完整的modbus_t结构体如下(展示核心字段):
c
struct _modbus {
/* Slave address */
int slave; // 从设备地址,用于报文封装
/* Socket or file descriptor */
int s; // 串口/网络文件描述符
int debug; // 调试模式开关
int error_recovery; // 错误恢复模式
int quirks; // 兼容特性配置
struct timeval response_timeout; // 响应超时时间
struct timeval byte_timeout; // 字节传输超时时间
struct timeval indication_timeout; // 指示超时时间
const modbus_backend_t *backend; // 后端操作函数集指针
void *backend_data; // 后端专属数据(如RTU串口属性)
};
这一步的核心目的是为后续报文封装提供从设备地址,确保生成的 Modbus 报文包含正确的目标从机标识。
五、第三步:建立串口连接(modbus_connect)
设置完从设备地址后,需要调用modbus_connect函数建立与串口的连接,完成串口的打开与参数配置,核心源码如下:
c
int modbus_connect(modbus_t *ctx)
{
// 校验上下文是否有效
if (ctx == NULL) {
errno = EINVAL;
return -1;
}
// 调用后端绑定的connect函数,建立串口连接
return ctx->backend->connect(ctx);
}
modbus_connect函数最终会调用_modbus_rtu_connect函数,该函数内部包含完整的串口打开 与串口参数配置(波特率、数据位、停止位、校验位)逻辑。
补充:对于 Modbus RTU 模式而言,主从设备的串口物理连接(或虚拟串口连接)是通信的基础,而
modbus_connect函数的核心作用是完成软件层面的串口初始化,确保数据能够通过串口正常传输。
需要注意的是,主从设备的串口是否成功建立软件连接,是后续报文能否正常发送与接收的关键前提。
六、第四步:构造写请求(modbus_write_bits)
完成前期准备工作后,进入核心的写请求构造阶段,调用modbus_write_bits函数,该函数的参数包含了写操作的核心信息:起始地址、写入位数、数据缓冲区,核心源码与逻辑如下:
首先是函数的参数定义与局部变量声明:
c
int modbus_write_bits(modbus_t *ctx, int addr, int nb, const uint8_t *src)
{
int rc;
int i;
int byte_count;
int req_length;
int bit_check = 0;
int pos = 0;
参数说明:addr为写入的线圈起始地址,nb为写入的线圈位数,src为待写入的数据缓冲区,这三个参数明确了写操作的核心信息。
1. 构造基础请求报文
通过调用后端的build_request_basis函数,构造 Modbus 报文的基础部分,源码如下:
c
req_length = ctx->backend->build_request_basis(
ctx, MODBUS_FC_WRITE_MULTIPLE_COILS, addr, nb, req);
该函数最终会调用_modbus_rtu_build_request_basis,结合 Modbus 功能码表,其核心作用是构造报文的基础字段:从设备地址、功能码、起始地址(高位)、起始地址(低位)、寄存器数(高位)、寄存器数(低位)。
补充:该步骤构造的基础报文字段,与此前 Modbus 报文解析中的查询报文基础结构完全一致,是 Modbus 协议标准化的体现。
2. 计算数据域字节数
根据写入的线圈位数,计算需要发送的数据域字节数,源码如下:
c
byte_count = (nb / 8) + ((nb % 8) ? 1 : 0);
该代码的逻辑对应 Modbus 协议中位寄存器的数据字节数计算规则:当写入的位数nb为 8 的整数倍时,字节数为nb/8;当nb不为 8 的整数倍时,字节数向上取整(nb/8 + 1),多余的位填充为 0,不影响有效数据的传输。
七、第五步:发送请求报文(send_msg 系列函数)
构造完成请求报文后,通过send_msg函数完成报文的预处理与实际发送,整个过程分为两个核心步骤:报文预处理(计算 CRC 校验)与串口发送。
1. 发送报文核心函数(send_msg)
c
static int send_msg(modbus_t *ctx, uint8_t *msg, int msg_length)
{
int rc;
int i;
// 报文发送前预处理(核心:计算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]);
printf("\n");
}
/* In recovery mode, the write command will be issued until to be
successful! Disabled by default. */
// 错误恢复模式下,重复发送直到成功(默认禁用)
do {
rc = ctx->backend->send(ctx, msg, msg_length);
if (rc == -1) {
_error_print(ctx, NULL);
if (ctx->error_recovery & MODBUS_ERROR_RECOVERY_LINK) {
2. 报文预处理(计算 CRC 校验)
send_msg_pre函数的底层实现为_modbus_rtu_send_msg_pre,核心作用是计算 Modbus RTU 报文的 CRC16 校验码,并追加到报文末尾,源码如下:
c
static int _modbus_rtu_send_msg_pre(uint8_t *req, int req_length)
{
// 计算报文的CRC16校验码
uint16_t crc = crc16(req, req_length);
/* According to the MODBUS specs (p. 14), the low order byte of the CRC comes
* first in the RTU message */
// 按照Modbus协议规范,CRC低位在前,高位在后,追加到报文末尾
req[req_length++] = crc & 0x00FF;
req[req_length++] = crc >> 8;
// 返回追加校验码后的完整报文长度
return req_length;
}
3. 实际串口发送数据
send函数的底层实现为_modbus_rtu_send,核心作用是通过串口将完整的 Modbus 报文发送出去,支持跨平台(Windows / 类 Unix),源码如下:
c
static ssize_t _modbus_rtu_send(modbus_t *ctx, const uint8_t *req, int req_length)
{
#if defined(_WIN32)
// Windows平台下,通过WriteFile函数写入串口
modbus_rtu_t *ctx_rtu = ctx->backend_data;
DWORD n_bytes = 0;
return (WriteFile(ctx_rtu->w_ser.fd, req, req_length, &n_bytes, NULL))
? (ssize_t) n_bytes
: -1;
#else
#if HAVE_DECL_TIOCM_RTS
// 类Unix平台下,支持RTS信号控制
modbus_rtu_t *ctx_rtu = ctx->backend_data;
if (ctx_rtu->rts != MODBUS_RTU_RTS_NONE) {
ssize_t size;
if (ctx->debug) {
fprintf(stderr, "Sending request using RTS signal\n");
}
ctx_rtu->set_rts(ctx, ctx_rtu->rts == MODBUS_RTU_RTS_UP);
usleep(ctx_rtu->rts_delay);
size = write(ctx->s, req, req_length);
usleep(ctx_rtu->onebyte_time * req_length + ctx_rtu->rts_delay);
ctx_rtu->set_rts(ctx, ctx_rtu->rts != MODBUS_RTU_RTS_UP);
return size;
} else {
#endif
// 类Unix平台下,直接通过write函数写入串口
return write(ctx->s, req, req_length);
#if HAVE_DECL_TIOCM_RTS
}
#endif
#endif
}
补充:若需将 libmodbus 改造成裸机环境使用,核心需要修改该函数中的串口发送逻辑,替换为对应 MCU 的串口发送 API(如 STM32 的
HAL_UART_Transmit),其余逻辑可保持不变。
八、RTU 后端核心结构解读
整个 Modbus RTU 发送流程的核心支撑是_modbus_rtu_backend结构体,该结构体定义了 RTU 模式下所有关键操作的函数指针,遵循固定的顺序与格式,源码如下:
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相关资源
};
该结构体是 libmodbus 模块化设计的核心体现,所有 RTU 模式的操作都通过该结构体中的函数指针完成,这也是 libmodbus 能够支持多后端(RTU/TCP)的关键所在。
九、总结
- libmodbus 发送请求核心流程:创建 RTU 上下文→设置从机地址→建立串口连接→构造请求报文→发送报文(含 CRC 校验);
- 核心支撑是
_modbus_rtu_backend结构体,封装了 RTU 模式的所有操作函数,体现模块化设计思想; - 报文预处理的核心是计算 CRC16 校验码,裸机移植需修改串口发送底层 API;
- 源码中的字节数计算、报文结构与 Modbus 协议规范完全一致,实现了协议的标准化封装。
十、结尾
本次我们深入拆解了 libmodbus 发送请求的完整源码流程,清晰看到了 Modbus 协议的标准化封装与模块化设计思想。吃透这些底层逻辑,不仅能熟练使用 libmodbus 进行开发,还能根据实际需求进行二次开发与裸机移植。libmodbus 的源码封装看似复杂,实则遵循清晰的协议逻辑与工程规范,反复研读能够大幅提升嵌入式工程开发能力。感谢各位的阅读,持续关注本系列笔记,一起深耕 Modbus 与开源库开发领域,解锁更多实用技能与实战场景!