目录一、前言二、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-sendrecv→_modbus_rtu_recv内部调pdev-RecvByte。3.3 modbus_rtu_t —— RTU 私有数据// modbus-rtu-private.htypedefstruct_modbus_rtu{char*device;// 设备名: uart4, usbintbaud;uint8_tdata_bit;uint8_tstop_bit;charparity;// N, E, Ointconfirmation_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*ctxpvPortMalloc(sizeof(modbus_t));_modbus_init_common(ctx);// 初始化默认超时、错误恢复等// ② 绑定虚函数表ctx-backend_modbus_rtu_backend_uart;// ③ 查找 OOP 设备 (★ 关键桥接)structUART_Device*pdevGetUARTDevice((char*)device);if(!pdev){modbus_free(ctx);returnNULL;}// ④ 分配 RTU 私有数据, 存设备指针和串口参数modbus_rtu_t*ctx_rtupvPortMalloc(sizeof(modbus_rtu_t));ctx_rtu-devpdev;// ★ 把 UART_Device 挂到上下文ctx_rtu-baudbaud;ctx_rtu-parityparity;// ...ctx-backend_datactx_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_rtuctx-backend_data;structUART_Device*pdevctx_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_rtuctx-backend_data;structUART_Device*pdevctx_rtu-dev;returnpdev-RecvByte(pdev,rsp,timeout);}// 初始化: 调用 OOP 的 Init()staticint_modbus_rtu_connect(modbus_t*ctx){modbus_rtu_t*ctx_rtuctx-backend_data;structUART_Device*pdevctx_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, addr0, nb1, 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)ctxmodbus_new_st_rtu(uart4,115200,N,8,1);// 用 USB CDC (虚拟串口, 接 PC)ctxmodbus_new_st_rtu(usb,115200,N,8,1);九、常见坑9.1 GetUARTDevice 返回 NULLmodbus_new_st_rtu内部会检查GetUARTDevice的返回值。如果设备名不存在于g_uart_devices[]数组中返回 NULL。检查是否在uart_device.c里注册了对应的g_xxx_dev。9.2 modbus_receive 返回 0过滤帧从机模式下modbus_receive对不匹配的从站地址返回 0静默忽略不是错误。上层的标准写法do{rcmodbus_receive(ctx,query);}while(rc0);// 过滤不匹配的帧9.3 CRC 校验失败收到 CRC 错误时如果error_recovery启用了MODBUS_ERROR_RECOVERY_PROTOCOLlibmodbus 会自动调用flush()清空缓冲。否则需要手动处理。9.4 malloc → pvPortMalloc标准 libmodbus 用mallocST 移植版全部替换为pvPortMallocFreeRTOS 安全分配。新增代码时注意不要混用。十、结尾本篇走完了 libmodbus 的三个核心情景和数据结构全景modbus_tmodbus_backend_tvtable modbus_rtu_t的层次设计modbus-st-rtu.c如何把 POSIX I/O 桥接到UART_Device主机发送(③send→) / 从机接收(RecvByte←) / 从机回应(③send→) 三条调用链学习路径回顾Note 10: DMAIDLE (物理层基础) Note 11: RTOS 信号量 (多任务基础) Note 12: UART_Device OOP (抽象层) Note 13: Modbus 协议分析 (协议层) Note 14: libmodbus 源码分析 ← 本篇