当前位置: 首页 > news >正文

libmodbus 源码分析

目录

  • 一、前言
  • 二、libmodbus 的文件地图
  • 三、核心数据结构
  • 四、ST 移植版:modbus-st-rtu.c 全景
  • 五、情景一:主机发送请求
  • 六、情景二:从机接收请求
  • 七、情景三:从机构造回应
  • 八、与 UART_Device 的桥接
  • 九、常见坑
  • 十、结尾

一、前言

大家好,这里是Hello_Embed

上一篇我们分析了 Modbus RTU 协议——帧格式、四种寄存器、功能码。本篇进入源码层:libmodbus 是怎么组织代码的?modbus_tmodbus_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_Devicesend/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 的三个核心情景和数据结构全景:

学习路径回顾:

Note 10: DMA+IDLE (物理层基础) Note 11: RTOS 信号量 (多任务基础) Note 12: UART_Device OOP (抽象层) Note 13: Modbus 协议分析 (协议层) Note 14: libmodbus 源码分析 ← 本篇
http://www.jsqmd.com/news/805335/

相关文章:

  • 从NBA智能分析到智慧城市:数据驱动系统的架构、挑战与落地实践
  • OpenClaw本地运行教程,隐私安全型AI助手搭建指南
  • Nodejs后端服务如何稳定调用Claude并避免封号风险
  • 简单序列帧动画播放器,播放GIF
  • 【python】离线安装库到内网中
  • 开源AI代码生成工具unsaged:本地化部署与高效开发实践
  • vsftpd匿名用户本地用户虚拟用户总结【20260512】001篇
  • 淘宝要接入AI购物助手:以后买东西,可能不是搜索,而是“让AI帮你挑”
  • Midjourney Sumi-e风格合规性预警:2024Q3版权新规下,3类易侵权水墨元素识别清单与安全替代方案(含JIS X 9051标准对照)
  • 终极PS4存档管理指南:Apollo Save Tool完全解析
  • 个人开发者如何利用Taotoken模型广场快速选型并验证创意原型
  • 2026届必备的五大AI辅助写作助手解析与推荐
  • 百元级GEO工具真的能生成高质量内容吗?
  • 终极VLC美化指南:5款VeLoCity专业主题让你的播放器焕然一新
  • 忆阻器争议:从数学定义到产业应用,如何理解下一代存储技术
  • 35岁零基础转行网络安全?值得吗?
  • 怎么解决梨采摘后的果肉黑心问题
  • 基于Dlib与OpenCV的人脸关键点检测实战:从静态图片到实时视频
  • 基于STM32CubeMX与HAL库的MAX30102心率血氧监测系统实战指南
  • 穿透 MQ 专栏 (五):【终局之战】MySQL 和 MQ 的世纪联姻:扒开“分布式事务”的遮羞布
  • 工程师远程高效设计:从工具链到协作心法的实战指南
  • 35岁裸辞转行网络安全!零基础入门的真实励志案例,建议收藏
  • 要以战养兵,不要纸上谈兵
  • 电子仪器CE标志合规:从技术文件到尽职调查的完整指南
  • 别再用暴力搜索了!用C++解鸡兔同笼,这几种算法思路让你面试加分
  • 你的音乐被“囚禁“了?ncmdumpGUI终极解锁指南:让NCM文件重获自由
  • 终极指南:如何在Windows上轻松安装安卓APK应用
  • 别再手动调参了!用Matlab Regression Learner App,5分钟搞定你的第一个回归模型
  • 别瞎转了!零基础拿捏网络安全,看这篇“保姆级”避坑指南就够了
  • Taotoken用量看板如何帮助团队清晰管理大模型支出