目录
- 一、前言
- [二、libmodbus 的文件地图](#二、libmodbus 的文件地图)
- 三、核心数据结构
- [四、ST 移植版:modbus-st-rtu.c 全景](#四、ST 移植版:modbus-st-rtu.c 全景)
- 五、情景一:主机发送请求
- 六、情景二:从机接收请求
- 七、情景三:从机构造回应
- [八、与 UART_Device 的桥接](#八、与 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 原本的 POSIX read()/write() 替换成了 pdev->RecvByte() 和 pdev->Send()。
三、核心数据结构
3.1 modbus_t ------ 主上下文
c
// modbus-private.h
struct _modbus {
int slave; // 从站地址
int s; // socket/file descriptor
int debug;
int error_recovery;
struct timeval response_timeout; // 响应超时
struct timeval byte_timeout; // 字节间超时
const modbus_backend_t *backend; // ★ 函数指针表
void *backend_data; // ★ 后端私有数据 (modbus_rtu_t *)
};
backend 指向一张虚函数表 ,所有底层 I/O 操作通过它分发。backend_data 持有 RTU 或 TCP 的私有参数。
3.2 modbus_backend_t ------ 虚函数表
c
// modbus-private.h
typedef struct _modbus_backend {
unsigned int backend_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 私有数据
c
// modbus-rtu-private.h
typedef struct _modbus_rtu {
char *device; // 设备名: "uart4", "usb"
int baud;
uint8_t data_bit;
uint8_t stop_bit;
char parity; // 'N', 'E', 'O'
int confirmation_to_ignore;
struct UART_Device *dev; // ★ OOP 设备指针
} modbus_rtu_t;
3.4 modbus_mapping_t ------ 寄存器映射
c
typedef struct _modbus_mapping_t {
int nb_bits; // 线圈数量
int start_bits; // 线圈起始地址
int nb_input_bits; // 离散输入数量
int start_input_bits;
int nb_input_registers; // 输入寄存器数量
int start_input_registers;
int nb_registers; // 保持寄存器数量
int start_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 用 POSIX read()/write() 操作 /dev/ttyS0。我们要把它改成用 UART_Device 的 send/RecvByte。
4.1 创建 RTU 上下文:modbus_new_st_rtu()
c
modbus_t *modbus_new_st_rtu(const char *device, int baud,
char parity, int data_bit, int stop_bit)
{
// ① 分配 modbus_t
modbus_t *ctx = pvPortMalloc(sizeof(modbus_t));
_modbus_init_common(ctx); // 初始化默认超时、错误恢复等
// ② 绑定虚函数表
ctx->backend = &_modbus_rtu_backend_uart;
// ③ 查找 OOP 设备 (★ 关键桥接)
struct UART_Device *pdev = GetUARTDevice((char *)device);
if (!pdev) { modbus_free(ctx); return NULL; }
// ④ 分配 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;
return ctx;
}
4.2 核心 I/O 函数的 ST 实现
c
// 发送: 调用 OOP 的 send()
static ssize_t _modbus_rtu_send(modbus_t *ctx, const uint8_t *req, int req_length)
{
modbus_rtu_t *ctx_rtu = ctx->backend_data;
struct UART_Device *pdev = ctx_rtu->dev;
return pdev->send(pdev, (uint8_t *)req, req_length, 1000);
}
// 接收一个字节: 调用 OOP 的 RecvByte()
static ssize_t _modbus_rtu_recv(modbus_t *ctx, uint8_t *rsp, int rsp_length, int timeout)
{
modbus_rtu_t *ctx_rtu = ctx->backend_data;
struct UART_Device *pdev = ctx_rtu->dev;
return pdev->RecvByte(pdev, rsp, timeout);
}
// 初始化: 调用 OOP 的 Init()
static int _modbus_rtu_connect(modbus_t *ctx)
{
modbus_rtu_t *ctx_rtu = ctx->backend_data;
struct UART_Device *pdev = ctx_rtu->dev;
pdev->Init(pdev, ctx_rtu->baud, ctx_rtu->parity,
ctx_rtu->data_bit, ctx_rtu->stop_bit);
return 0;
}
五、情景一:主机发送请求
场景: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 (物理层)
换一个后端只改一行:
c
// 用板载 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(静默忽略),不是错误。上层的标准写法:
c
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 源码分析 ← 本篇