ymodbus 是平台可移植的 Modbus 协议栈,支持 Modbus RTU 与 Modbus TCP 两种传输方式,同时提供 主站(Master) 与 从站(Slave) 角色。协议栈通过收发回调与底层串口/网络解耦,便于在 RS485、以太网等场景下集成。端口层(ports) 在 ymodbus_port.h 中抽象互斥锁、链表与堆,支持 RT-Thread、FreeRTOS、Linux 以及未定义平台时的裸机风格;日志通过 ymodbus_log.h 按平台选择 RT-Thread ulog、或类 RT-Thread 的 printf 格式(见 3.2 节)。
- 协议:Modbus RTU / Modbus TCP(ADU 格式符合标准,RTU 最小 4 字节、最大 256 字节;TCP 最小 8 字节、最大 260 字节)
- 角色:主站、从站(通过
YMODBUS_USING_MASTER/YMODBUS_USING_SLAVE编译裁剪) - 功能码:0x01~0x06、0x0F、0x10、0x11、0x16、0x17(读线圈/离散输入/保持寄存器/输入寄存器,单/多写,掩码写,写读寄存器,报告从站 ID)
- 资源:支持静态分配(无堆)与动态分配(需
YMODBUS_USING_HEAP,提供ymodbus_create/ymodbus_slave_create/ymodbus_registers_create等) - 线程安全:对
struct ymodbus的访问由端口层提供的ymodbus_mutex保护 - 从站:支持多从站挂在同一
struct ymodbus下,寄存器区通过ymodbus_registers做多段映射(线圈/离散输入/保持/输入寄存器)
- 主站:
#include <ymodbus_master.h>(已包含 ymodbus_core) - 从站:
#include <ymodbus_slave.h>、#include <ymodbus_registers.h> - 协议常量:
ymodbus_rtu.h、ymodbus_tcp.h(ADU 长度等) - 配置:
ymodbus_cfg.h(平台宏、YMODBUS_USING_SLAVE/YMODBUS_USING_MASTER) - 端口:
ymodbus_port.h(由ymodbus_core.h间接包含,提供互斥锁、链表、可选堆接口) - 日志:
ymodbus_log.h(按平台使用 RT-Thread rtdbg/ulog 或 printf 风格,见 3.2 节) - 工具:
ymodbus_utils.h(CRC、非 RT-Thread 下的 slist 实现) - 依赖:根据所选平台(RT-Thread:rt_mutex、rt_slist、可选 rtdbg;FreeRTOS/Linux:见 ports 与 log 实现)
ymodbus/
├── inc/
│ ├── ymodbus_core.h # 核心类型、设备结构、init/detach、收发注册、PDU 结构
│ ├── ymodbus_cfg.h # 平台与功能开关(RTTHREAD/FREERTOS/LINUX、YMODBUS_USING_SLAVE/MASTER)
│ ├── ymodbus_master.h # 主站:序列化请求、解析响应、读写 API
│ ├── ymodbus_slave.h # 从站:slave 结构、request_handle、init/attach
│ ├── ymodbus_registers.h # 从站寄存器区:add/create、remove/delete、cleanup
│ ├── ymodbus_rtu.h # RTU 常量(ADU 长度等)
│ ├── ymodbus_tcp.h # TCP 常量
│ ├── ymodbus_log.h # 平台相关日志(RT-Thread ulog / printf 风格)
│ └── ymodbus_utils.h # CRC、非 RT-Thread 下的 slist
├── ports/
│ └── ymodbus_port.h # 端口层:互斥锁、链表、堆(RTTHREAD/FREERTOS/LINUX/else)
├── src/
│ ├── ymodbus_core.c # 设备管理、RTU/TCP 变体、CRC/头部、request_pdu_length
│ ├── ymodbus_master.c # 主站请求组帧与响应解析、一站式读写
│ ├── ymodbus_slave.c # 从站请求解析与寄存器调度
│ ├── ymodbus_registers.c # 寄存器区读写、默认 read/write 实现
│ └── ymodbus_utils.c # CRC 计算
├── docs/
│ └── README.md # 本文档
├── samples/
│ ├── main_slave.c # 从站演示(本地注入 RTU 请求并回包)
│ ├── main_master.c # 主站演示(主站-从站内存环回仿真)
│ └── Makefile # Linux/WSL 下样例构建(支持 EXAMPLE 参数)
├── SConscript # 构建:YMODBUS_USING / YMODBUS_USING_MASTER / YMODBUS_USING_SLAVE
└── kconfig # 可选 Kconfig 入口(本工程在 board/Kconfig 中配置)
在包含协议栈头文件之前,需定义平台宏和功能宏(可通过编译选项或 ymodbus_cfg.h 统一配置):
- 平台(任选其一):
RTTHREAD— RT-Thread(mutex/slist/堆由 RT 提供,日志走 rtdbg/ulog)FREERTOS— FreeRTOS(mutex/slist/堆见 port,日志为 printf 风格)LINUX— Linux 用户态(pthread/stdlib 堆,日志为 printf 风格)- 均未定义 — 裸机风格(无互斥、无堆,日志为 printf 风格)
- 从站:
YMODBUS_USING_SLAVE— 启用从站及ymodbus_slave_*、ymodbus_registers_* - 主站:
YMODBUS_USING_MASTER— 启用主站及ymodbus_master_*
动态分配(堆)由端口层根据平台自动定义:RT-Thread 下由 RT_USING_HEAP 决定是否定义 YMODBUS_USING_HEAP;FreeRTOS 下由 configSUPPORT_DYNAMIC_ALLOCATION 决定;Linux 下默认 YMODBUS_USING_HEAP;裸机/未定义平台不提供堆。
协议栈内部使用统一宏 LOG_E / LOG_W / LOG_I / LOG_D。各源文件在包含 ymodbus_log.h 前需定义 YMODBUS_LOG_TAG(如 "ymodbus.core"、"ymodbus.master"、"ymodbus.slave")。按平台行为如下:
| 平台宏 | 日志实现 |
|---|---|
| RTTHREAD | 使用 rtdbg.h(ulog),可配合 DBG_TAG/DBG_LVL |
| FREERTOS / LINUX / 其他 | printf 风格,格式为 [E/W/I/D][tag] msg,与 RT-Thread 输出风格一致 |
在非 RT-Thread 平台需自定义输出(如重定向到串口或 syslog)时,可在包含 ymodbus_log.h 前定义:
#define YMODBUS_LOG_PRINTF(fmt, ...) my_printf(fmt, ##__VA_ARGS__)在 board/Kconfig 中:
YMODBUS_USING:总开关,启用 ymodbus 组件YMODBUS_USING_MASTER:启用主站YMODBUS_USING_SLAVE:启用从站
SConscript 根据上述依赖编译 ymodbus_core.c、ymodbus_utils.c,以及按需的 ymodbus_master.c、ymodbus_slave.c、ymodbus_registers.c。头文件路径包含 inc 与 ports。
- 核心(ymodbus_core):
struct ymodbus表示一条 Modbus 通道(RTU 或 TCP),持有缓冲区、收发回调、互斥锁;通过ymodbus_variable区分 RTU(CRC、1 字节从站地址)与 TCP(MBAP 头、事务 ID、无 CRC)。设备创建/初始化后注册到全局链表,供按名查找。 - 主站(ymodbus_master):根据请求 PDU 组帧为 ADU(加头部、CRC),经
send_cb发送;再经rcv_cb收响应,校验 CRC、解析响应 PDU 或异常码,写回resp_buf。一站式 API 内部完成「序列化请求 → 发送 → 接收 → 解析」。 - 从站(ymodbus_slave):同一
struct ymodbus可挂多个ymodbus_slave(不同从站地址)。收到一帧后由应用将长度传入ymodbus_slave_request_handle;内部校验 CRC、按地址找从站、按功能码与地址区间查找寄存器区(ymodbus_registers),执行默认或自定义 read/write,组响应或异常帧并经send_cb回包。 - 寄存器(ymodbus_registers):四类区(线圈、离散输入、保持、输入寄存器),每区可多段;每段有起始/结束地址、数据指针及可选的 read/write 回调,跨段读写由协议栈按地址拼接。
主站使用前需创建一个 struct ymodbus 实例,并为其提供接收与发送回调,使协议栈能从“当前通道”收发包。本工程采用 动态创建 方式(需 YMODBUS_USING_HEAP),也演示了和 RS485 的配合。
#include <ymodbus_master.h>
#include <ymodbus_rtu.h>
struct rs485 *mb_rs485;
struct ymodbus *mb_rtu;
/* 接收:把数据读入 buf,返回读到的字节数,失败返回负值 */
static int mb_rcv_cb(void *data, void *buf, uint16_t bufsz)
{
return rs485_wait_receive(mb_rs485, buf, bufsz, 5000);
}
/* 发送:把 req_buf 发出去,返回发送字节数,失败返回负值 */
static int mb_send_cb(void *data, void *buf, uint16_t bufsz)
{
/* 可选:发前清空接收缓冲,避免收到自己发的内容 */
char cbuf[64];
rs485_wait_receive(mb_rs485, cbuf, sizeof(cbuf), 10);
return rs485_transmit(mb_rs485, buf, bufsz);
}
int main(void)
{
int ret;
/* 创建并连接 RS485 口(本工程使用 uart5 + 方向控制脚) */
mb_rs485 = rs485_create("rs485.5", "uart5", GET_PIN(F, 8), PIN_HIGH);
ret = rs485_connect(mb_rs485);
/* 创建 Modbus RTU 主站实例(内部自动分配缓冲区,需 YMODBUS_USING_HEAP) */
mb_rtu = ymodbus_create("mbmaster",
YMODBUS_TYPE_RTU,
YMODBUS_RTU_ADU_MAX_LENGTH,
mb_rcv_cb,
mb_send_cb,
NULL);
if (!mb_rtu)
{
/* 错误处理 */
}
}若希望完全静态分配(无堆),可使用 ymodbus_init,缓冲区由调用方提供:
struct ymodbus mb_rtu;
uint8_t mb_rtu_buf[YMODBUS_RTU_ADU_MAX_LENGTH];
ymodbus_init("mb_rtu", &mb_rtu, YMODBUS_TYPE_RTU,
mb_rtu_buf, sizeof(mb_rtu_buf),
mb_rcv_cb, mb_send_cb, NULL);主站提供“发请求 + 收响应”的一站式接口,内部先 ymodbus_master_serialize_request 再 ymodbus_master_response_handle。超时与阻塞由 rcv_cb 实现决定(例如上面在 rs485_wait_receive 里等待 5000ms)。
uint8_t slave_addr = 1;
uint16_t regs[10];
int ret;
/* 读输入寄存器 FC 0x04,本工程中用于读取告警条数等 */
ret = ymodbus_master_read_input_registers(slave_addr, mb_rtu, 0, 1, regs);
/* 继续读取告警详细信息(示例) */
ret = ymodbus_master_read_input_registers(slave_addr, mb_rtu, 1, 3, regs);
/* 写多个保持寄存器 FC 0x10,本工程用于清除告警信息 */
ret = ymodbus_master_write_multiple_registers(slave_addr, mb_rtu, 0, 2, regs);返回值:0 表示成功;负数为错误码(如 -EINVAL、-ENODATA、-EBADMSG 等);若返回值大于 0,表示从站返回了 Modbus 异常码(如 0x01 非法功能、0x02 非法数据地址等,见 enum ymodbus_exception)。
其他主站 API:读线圈/离散输入、写单/多线圈、报告从站 ID、掩码写寄存器、写读寄存器等,见 ymodbus_master.h。
若需自己控制“发完请求后”的等待时间或非阻塞轮询,可拆成两步:
- 发请求:
ymodbus_master_serialize_request(slave_addr, mb, &pdu, &req_len);- 在
mb->buf中组好 ADU 并调用send_cb发送。
- 在
- 收响应:在合适时机调用
ymodbus_master_response_handle(mb, &pdu, resp_len);- 若由协议栈内部收包,可传
resp_len=0,内部通过rcv_cb收数据;否则由调用方先收好再传入resp_len。
- 若由协议栈内部收包,可传
仓库 samples/main_master.c 提供了纯软件仿真主站演示:主站通过回调把请求送到本地从站处理,再把响应返回给主站,便于无串口硬件时调试主站流程。
构建与运行:
cd samples
make EXAMPLE=master
./ymodbus_master_sample从站需在打开 Modbus 设备(如 RS485)并收到一帧数据后,将数据放入 mb->buf,再调用 ymodbus_slave_request_handle。协议栈会完成 CRC 校验、解析 PDU、按地址查找从站、调度寄存器读写、组响应或异常帧,最后通过 send_cb 回包。
#include <ymodbus_slave.h>
#include <ymodbus_registers.h>
struct ymodbus *mb_rtu;
struct ymodbus_slave *mb_slave;
/* 复用主站的 mb_rtu:即一个 ymodbus 实例同时挂主站和本地从站 */
mb_rtu = ymodbus_create("mbmaster", YMODBUS_TYPE_RTU,
YMODBUS_RTU_ADU_MAX_LENGTH,
mb_rcv_cb, mb_send_cb, NULL);
mb_slave = ymodbus_slave_create("slave1", mb_rtu, 1); /* 需 YMODBUS_USING_HEAP */
if (!mb_slave)
{
/* 错误处理,例如 ymodbus_detach(mb_rtu); */
}也可使用静态从站:ymodbus_slave_init(&slave, "slave1", mb_rtu, 1),再 ymodbus_slave_attach_mb 等。
从站通过“寄存器区”暴露数据。每个区是一段地址区间,对应一块内存(或自定义 read/write 回调)。
- 线圈(Coils):可读可写,位数据。
- 离散输入(Discrete Inputs):只读,位数据。
- 保持寄存器(Holding Registers):可读可写,16 位。
- 输入寄存器(Input Registers):只读,16 位。
方式一:动态创建(需 YMODBUS_USING_HEAP)
/* 保持寄存器:0~39 分成 4 段,每段 10 个寄存器 */
static uint16_t holding_regs1[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
/* ... holding_regs2 ~ holding_regs4 ... */
/* 输入寄存器、线圈、离散输入示例数据 ... */
ymodbus_registers_create(mb_slave, 0, 10, holding_regs1, YMODBUS_REGISTERS_TYPE_HOLDING_REGISTERS);
/* ... 其他 ymodbus_registers_create ... */
/* 删除某段寄存器(按 regs 指针与 type 匹配删除) */
ymodbus_registers_delete(mb_slave, holding_regs1, YMODBUS_REGISTERS_TYPE_HOLDING_REGISTERS);方式二:静态添加
uint16_t holding_buf[32];
struct ymodbus_registers reg_holding = {
.start_addr = 0,
.end_addr = 31,
.regs = holding_buf,
.read = NULL, /* 使用协议栈默认 read_registers */
.write = NULL, /* 使用协议栈默认 write_registers */
};
ymodbus_registers_add(mb_slave, ®_holding, YMODBUS_REGISTERS_TYPE_HOLDING_REGISTERS);可多次 add/create,实现多段地址;协议栈会按地址区间自动拼接读/写。自定义 read/write 时,在 struct ymodbus_registers 中指定自己的函数指针即可。移除/清理:ymodbus_registers_remove、ymodbus_registers_cleanup。
从站侧,当从 RS485 等收到一帧并写入 mb->buf 后,必须将本帧长度传入 req_len,再调用:
uint16_t resp_len;
int ret = ymodbus_slave_request_handle(mb_rtu, req_len, &resp_len);req_len:本次收到的 ADU 字节数(协议栈不会在从站内部用 rcv_cb 收数,需由应用先读好再传)。ret == 0表示成功处理并已通过send_cb回包;负数为系统错误;从站内部异常会组装成 Modbus 异常响应并发送。
仓库 samples/main_slave.c 提供了从站演示:程序内构造一条 RTU 读保持寄存器请求,注入 mb->buf 后调用 ymodbus_slave_request_handle,并打印响应帧。
构建与运行:
cd samples
make EXAMPLE=slave
./ymodbus_slave_samplesamples/Makefile 通过 EXAMPLE 参数选择编译目标:
EXAMPLE=slave:构建ymodbus_slave_sample(对应main_slave.c)EXAMPLE=master:构建ymodbus_master_sample(对应main_master.c)
常用命令:
cd samples
make EXAMPLE=slave
make EXAMPLE=master
make debug EXAMPLE=slave
make debug EXAMPLE=master
make helpdebug 目标会使用 -O0 -g 重新构建,便于 GDB/VSCode 断点调试。
本工程中,主站通过 RS485 访问现场 Modbus 设备,同时在本机上挂了一个测试从站 mb_slave。下面示例与 applications/main.c 一致。
struct rs485 *mb_rs485;
struct ymodbus *mb_rtu;
static int mb_rcv_cb(void *data, void *buf, uint16_t bufsz)
{
return rs485_wait_receive(mb_rs485, buf, bufsz, 5000);
}
static int mb_send_cb(void *data, void *buf, uint16_t bufsz)
{
char cbuf[64];
rs485_wait_receive(mb_rs485, cbuf, sizeof(cbuf), 10);
return rs485_transmit(mb_rs485, buf, bufsz);
}mb_rs485 = rs485_create("rs485.5", "uart5", GET_PIN(F, 8), PIN_HIGH);
rs485_connect(mb_rs485);
mb_rtu = ymodbus_create("mbmaster", YMODBUS_TYPE_RTU,
YMODBUS_RTU_ADU_MAX_LENGTH,
mb_rcv_cb, mb_send_cb, NULL);
mb_slave = ymodbus_slave_create("slave1", mb_rtu, 1);
/* 通过 ymodbus_registers_create 给 mb_slave 绑定 4 类寄存器区,见 5.2 节 */uint8_t slave_addr = 1;
while (1)
{
alarm_process(slave_addr, phone_numbers);
rt_thread_mdelay(1000);
if (slave_addr < slave_addr_max)
slave_addr++;
else
slave_addr = 1;
}即本工程“主站 + 本地从站 + RS485”的完整链路:应用调用主站 API → 协议栈组帧并 send_cb → RS485 发送 → 现场从站应答 → rcv_cb 收数据 → 协议栈解析并写回应用提供的 resp_buf;本机 mb_slave 也可被外部主站访问,用于联调和自测。
主站一站式 API 返回值大于 0 时,表示从站返回的 Modbus 异常码,对应 enum ymodbus_exception:
| 值 | 宏名 | 含义 |
|---|---|---|
| 0x01 | YMODBUS_EXCEPTION_ILLEGAL_FUNCTION | 非法功能码 |
| 0x02 | YMODBUS_EXCEPTION_ILLEGAL_DATA_ADDRESS | 非法数据地址 |
| 0x03 | YMODBUS_EXCEPTION_ILLEGAL_DATA_VALUE | 非法数据值 |
| 0x04 | YMODBUS_EXCEPTION_SLAVE_OR_SERVER_FAILURE | 从站设备故障 |
| 0x0A | YMODBUS_EXCEPTION_GATEWAY_PATH | 网关路径不可用 |
| 0x0B | YMODBUS_EXCEPTION_GATEWAY_TARGET | 网关目标无响应 |
| 0xFF | YMODBUS_EXCEPTION_UNKNOWN | 未知异常 |
- 平台可移植:端口层(
ymodbus_port.h)支持 RT-Thread、FreeRTOS、Linux 及裸机风格;日志层(ymodbus_log.h)在 RT-Thread 下用 rtdbg/ulog,其他平台用类 RT-Thread 的 printf 格式,可自定义YMODBUS_LOG_PRINTF。 - 主从 + 多从站 + 寄存器抽象完善:从站支持多 slave、多段寄存器映射,跨段读写由协议栈处理,业务只需提供缓冲区或自定义 read/write。
- 支持纯静态资源:无堆时可运行主站/从站(
ymodbus_init、ymodbus_slave_init、ymodbus_registers_add),适合 RAM 紧张的 MCU。 - 协议覆盖常用功能码:满足绝大多数工业设备与网关需求;参数与长度校验严格,符合标准。
- 收发与传输解耦:通过 rcv_cb/send_cb 可接 RS485、TCP、其他串口等,便于复用。
- 错误码统一:使用 errno 风格负值及 Modbus 异常码(主站返回值 > 0),便于上层处理。
- 依赖所选平台:需正确配置
ymodbus_cfg.h与ymodbus_port.h,移植到新 OS 或裸机需实现端口宏(mutex、slist、可选堆)。 - 生态与文档:相比 libmodbus/freemodbus 等,第三方教程较少,需结合源码与本文档使用。
- 超时与重试:协议栈不内置超时与重试,需在 rcv_cb 或两步 API 的应用层实现。
- 扩展功能码:标准功能码以外的自定义功能码需在协议栈内扩展 switch 或预留 hook 机制。
- 头文件:
components/ymodbus/inc/、components/ymodbus/ports/ - 应用示例:本工程
applications/main.c中的 Modbus 主站 + RS485 调用。 - Modbus 规范:Modbus Application Protocol V1.1b3、Modbus over serial line V1.02。
| 日期 | 说明 |
|---|---|
| 2026-03-09 | 同步 samples 用法:新增 main_slave.c/main_master.c 说明,补充 make EXAMPLE=slave/master 与 debug 参数化构建方式。 |
| 2025-02-10 | 与协议栈代码同步:补充平台(RTTHREAD/FREERTOS/LINUX)、目录增加 ymodbus_log.h、新增 3.2 日志与 3.4 协议栈架构;修正端口与优缺点描述。 |