libmodbus 源码分析
目录
- 一、前言
- 二、libmodbus 的文件地图
- 三、核心数据结构
- 四、ST 移植版:modbus-st-rtu.c 全景
- 五、情景一:主机发送请求
- 六、情景二:从机接收请求
- 七、情景三:从机构造回应
- 八、与 UART_Device 的桥接
- 九、常见坑
- 十、结尾
一、前言
大家好,这里是Hello_Embed。
上一篇我们分析了 Modbus RTU 协议——帧格式、四种寄存器、功能码。本篇进入源码层:libmodbus 是怎么组织代码的?modbus_t和modbus_mapping_t是什么关系?发送一帧数据的完整调用链是怎样的?
我们从三个经典情景出发——主机发请求、从机收请求、从机构造回应——用代码走读的方式把 libmodbus 核心流程串起来。
二、libmodbus 的文件地图
Middlewares/Third_Party/libmodbus/ ├── modbus.h ← 公共 API + 功能码宏 + modbus_mapping_t ├── modbus.c ← 核心引擎:帧收发、超时、主机/从机逻辑 ├── modbus-private.h ← 内部结构:_modbus_backend vtable, sft_t ├── modbus-rtu.h ← RTU 模式声明 + modbus_new_rtu() ├── modbus-rtu.c ← RTU 后端(POSIX 版,读写串口) ├── modbus-rtu-private.h ← modbus_rtu_t 结构定义 ├── modbus-st-rtu.c ← ★ STM32 移植版:用 UART_Device 替代 POSIX 串口 ├── modbus-data.c ← 寄存器映射的创建/释放 ├── modbus-tcp.h / .c ← TCP 后端(本项目不用) └── errno.h / errno.c ← 错误码重点关注:modbus-st-rtu.c——这是我们移植到 STM32H5 的关键文件。它把 libmodbus 原本的 POSIXread()/write()替换成了pdev->RecvByte()和pdev->Send()。
三、核心数据结构
3.1 modbus_t —— 主上下文
// modbus-private.hstruct_modbus{intslave;// 从站地址ints;// socket/file descriptorintdebug;interror_recovery;structtimevalresponse_timeout;// 响应超时structtimevalbyte_timeout;// 字节间超时constmodbus_backend_t*backend;// ★ 函数指针表void*backend_data;// ★ 后端私有数据 (modbus_rtu_t *)};backend指向一张虚函数表,所有底层 I/O 操作通过它分发。backend_data持有 RTU 或 TCP 的私有参数。
3.2 modbus_backend_t —— 虚函数表
// modbus-private.htypedefstruct_modbus_backend{unsignedintbackend_type;int(*set_slave)(...);int(*build_request_basis)(...);// 构架请求帧头int(*build_response_basis)(...);// 构架响应帧头ssize_t(*send)(...);// ★ 发送ssize_t(*recv)(...);// ★ 接收一个字节int(*receive)(...);// 接收完整帧int(*check_integrity)(...);// CRC 校验int(*connect)(...);// ★ 初始化int(*flush)(...);void(*free)(...);// ... 更多函数指针}modbus_backend_t;ST 移植版定义了_modbus_rtu_backend_uart实例,其中send→_modbus_rtu_send(内部调pdev->send),recv→_modbus_rtu_recv(内部调pdev->RecvByte)。
3.3 modbus_rtu_t —— RTU 私有数据
// modbus-rtu-private.htypedefstruct_modbus_rtu{char*device;// 设备名: "uart4", "usb"intbaud;uint8_tdata_bit;uint8_tstop_bit;charparity;// 'N', 'E', 'O'intconfirmation_to_ignore;structUART_Device*dev;// ★ OOP 设备指针}modbus_rtu_t;3.4 modbus_mapping_t —— 寄存器映射
typedefstruct_modbus_mapping_t{intnb_bits;// 线圈数量intstart_bits;// 线圈起始地址intnb_input_bits;// 离散输入数量intstart_input_bits;intnb_input_registers;// 输入寄存器数量intstart_input_registers;intnb_registers;// 保持寄存器数量intstart_registers;uint8_t*tab_bits;// 线圈位表uint8_t*tab_input_bits;// 离散输入位表uint16_t*tab_input_registers;// 输入寄存器值表uint16_t*tab_registers;// 保持寄存器值表}modbus_mapping_t;这就是从站的"内存镜像"——四张表对应四种 Modbus 寄存器。modbus_reply根据请求的功能码读写对应的表。
3.5 数据结构关系总图
modbus_t (协议栈上下文) ├── backend → _modbus_rtu_backend_uart (虚函数表, 编译期常量) │ ├── send → _modbus_rtu_send → pdev->send(...) │ ├── recv → _modbus_rtu_recv → pdev->RecvByte(...) │ ├── connect → _modbus_rtu_connect → pdev->Init(...) │ └── flush → _modbus_rtu_flush → pdev->Flush(...) │ └── backend_data → modbus_rtu_t ├── device = "uart4" ├── baud = 115200 └── dev → g_uart4_dev (UART_Device *) modbus_mapping_t (寄存器镜像, 独立分配) ├── tab_bits[16] ← 线圈 Coil ├── tab_input_bits[3] ← 离散输入 DI ├── tab_registers[5] ← 保持寄存器 HR └── tab_input_registers[4] ← 输入寄存器 IR四、ST 移植版:modbus-st-rtu.c 全景
这是整个移植的核心。标准 libmodbus 用 POSIXread()/write()操作/dev/ttyS0。我们要把它改成用UART_Device的send/RecvByte。
4.1 创建 RTU 上下文:modbus_new_st_rtu()
modbus_t*modbus_new_st_rtu(constchar*device,intbaud,charparity,intdata_bit,intstop_bit){// ① 分配 modbus_tmodbus_t*ctx=pvPortMalloc(sizeof(modbus_t));_modbus_init_common(ctx);// 初始化默认超时、错误恢复等// ② 绑定虚函数表ctx->backend=&_modbus_rtu_backend_uart;// ③ 查找 OOP 设备 (★ 关键桥接)structUART_Device*pdev=GetUARTDevice((char*)device);if(!pdev){modbus_free(ctx);returnNULL;}// ④ 分配 RTU 私有数据, 存设备指针和串口参数modbus_rtu_t*ctx_rtu=pvPortMalloc(sizeof(modbus_rtu_t));ctx_rtu->dev=pdev;// ★ 把 UART_Device 挂到上下文ctx_rtu->baud=baud;ctx_rtu->parity=parity;// ...ctx->backend_data=ctx_rtu;returnctx;}4.2 核心 I/O 函数的 ST 实现
// 发送: 调用 OOP 的 send()staticssize_t_modbus_rtu_send(modbus_t*ctx,constuint8_t*req,intreq_length){modbus_rtu_t*ctx_rtu=ctx->backend_data;structUART_Device*pdev=ctx_rtu->dev;returnpdev->send(pdev,(uint8_t*)req,req_length,1000);}// 接收一个字节: 调用 OOP 的 RecvByte()staticssize_t_modbus_rtu_recv(modbus_t*ctx,uint8_t*rsp,intrsp_length,inttimeout){modbus_rtu_t*ctx_rtu=ctx->backend_data;structUART_Device*pdev=ctx_rtu->dev;returnpdev->RecvByte(pdev,rsp,timeout);}// 初始化: 调用 OOP 的 Init()staticint_modbus_rtu_connect(modbus_t*ctx){modbus_rtu_t*ctx_rtu=ctx->backend_data;structUART_Device*pdev=ctx_rtu->dev;pdev->Init(pdev,ctx_rtu->baud,ctx_rtu->parity,ctx_rtu->data_bit,ctx_rtu->stop_bit);return0;}五、情景一:主机发送请求
场景:PC 上位机作为主站,通过 modbus_read_registers 读从站。
用户调用: modbus_read_registers(ctx, addr=0, nb=1, dest) │ ▼ modbus.c: send_msg() ← 组装请求帧 │ ① build_request_basis() ← 填地址+功能码+起始地址+数量 │ ② send_msg_pre() ← 追加 CRC │ ③ backend->send() ← ★ 调 _modbus_rtu_send │ → pdev->send(pdev, req, len, 1000) │ → HAL_UART_Transmit_DMA(&huart2, ...) │ → xSemaphoreTake(TX_Semaphore) │ ▼ 发送完成, 等待响应: │ receive_msg(ctx, rsp, MSG_CONFIRMATION) │ while (未收完) { │ backend->recv() ← 逐字节收 │ → pdev->RecvByte(pdev, &byte, timeout) │ → xQueueReceive(RX_Queue, &byte, timeout) │ } │ check_integrity() ← CRC 校验 ▼ 返回给用户: dest = 读到的寄存器值六、情景二:从机接收请求
用户调用 (while 循环中): modbus_receive(ctx, query) │ ▼ receive_msg(ctx, req, MSG_INDICATION) │ ├─ while (未收完一帧) { │ backend->recv() ← _modbus_rtu_recv │ → pdev->RecvByte(pdev, &byte, BYTE_TIMEOUT) │ → xQueueReceive(RX_Queue, &byte, timeout) │ │ if (超时) return -1 │ if (帧间超时 && 已收到一些字节) break ← IDLE 自然触发帧边界 │ } │ └─ check_integrity() ← CRC 校验 if (CRC 不匹配 && 开启了错误恢复) flush() ← 清空缓冲, 准备收下一帧关键细节:从机收帧时,依赖 IDLE 中断的天然帧边界。HAL_UARTEx_ReceiveToIdle_DMA一次收一帧,数据已经在 Queue 里。modbus_receive从 Queue 逐字节取,超时判断帧结束。
七、情景三:从机构造回应
收到请求后, 用户调用: modbus_reply(ctx, query, query_length, mb_mapping) │ ├─ 解析功能码: │ FC_READ_COILS → 读 tab_bits, build 响应 │ FC_READ_HOLDING_REGS → 读 tab_registers, build 响应 │ FC_WRITE_SINGLE_COIL → 写 tab_bits, build 确认 │ ... │ ├─ 构造响应帧: │ build_response_basis() ← 填地址+功能码 │ send_msg_pre() ← 追加 CRC │ └─ 发送: backend->send() ← _modbus_rtu_send → pdev->send(pdev, rsp, rsp_length, 1000)八、与 UART_Device 的桥接
回顾整个调用链,OOP 封装的价值在这里充分体现:
libmodbus (协议层, 不知道用哪个串口) │ ▼ backend->send() / backend->recv() / backend->connect() │ UART_Device (抽象层, 不知道底层是哪种 HAL) │ ▼ pdev->send() / pdev->RecvByte() / pdev->Init() │ HAL + DMA + FreeRTOS (物理层)换一个后端只改一行:
// 用板载 UART4 (接 RS-485)ctx=modbus_new_st_rtu("uart4",115200,'N',8,1);// 用 USB CDC (虚拟串口, 接 PC)ctx=modbus_new_st_rtu("usb",115200,'N',8,1);九、常见坑
9.1 GetUARTDevice 返回 NULL
modbus_new_st_rtu内部会检查GetUARTDevice的返回值。如果设备名不存在于g_uart_devices[]数组中,返回 NULL。检查是否在uart_device.c里注册了对应的g_xxx_dev。
9.2 modbus_receive 返回 0(过滤帧)
从机模式下,modbus_receive对不匹配的从站地址返回 0(静默忽略),不是错误。上层的标准写法:
do{rc=modbus_receive(ctx,query);}while(rc==0);// 过滤不匹配的帧9.3 CRC 校验失败
收到 CRC 错误时,如果error_recovery启用了MODBUS_ERROR_RECOVERY_PROTOCOL,libmodbus 会自动调用flush()清空缓冲。否则需要手动处理。
9.4 malloc → pvPortMalloc
标准 libmodbus 用malloc,ST 移植版全部替换为pvPortMalloc(FreeRTOS 安全分配)。新增代码时注意不要混用。
十、结尾
本篇走完了 libmodbus 的三个核心情景和数据结构全景:
modbus_t+modbus_backend_tvtable +modbus_rtu_t的层次设计modbus-st-rtu.c如何把 POSIX I/O 桥接到UART_Device- 主机发送(③send→) / 从机接收(RecvByte←) / 从机回应(③send→) 三条调用链
学习路径回顾:
Note 10: DMA+IDLE (物理层基础) Note 11: RTOS 信号量 (多任务基础) Note 12: UART_Device OOP (抽象层) Note 13: Modbus 协议分析 (协议层) Note 14: libmodbus 源码分析 ← 本篇