1. nanoModbus项目概述与背景
nanoModbus是一个轻量级的开源Modbus协议栈实现,特别适合资源受限的嵌入式系统。我在最近的一个工业自动化项目中采用了这个协议栈,发现它在保持功能完整性的同时,代码量仅为传统Modbus库的1/3左右。本文记录了我基于FT4232H Mini Module硬件平台的完整移植过程。
Modbus作为工业领域最常用的通信协议之一,其RTU模式在RS485/RS232设备中应用广泛。nanoModbus的独特优势在于:
- 单文件实现(仅nanomodbus.c和nanomodbus.h)
- 无动态内存分配
- 支持RTU和TCP双模式
- 代码体积仅约20KB(ARM Cortex-M编译)
我选择的FT4232H开发板具有4个独立UART接口,正好可以同时模拟Modbus主从设备。这种配置在实际项目中很常见,比如PLC与多个传感器节点的通信场景。
2. 工程环境搭建与移植
2.1 基础工程准备
首先从GitHub获取nanoModbus源码(v1.22.0版本),同时下载FTDI官方提供的VCP示例工程作为基础框架。我的开发环境是Windows 10 + Visual Studio 2019。
关键步骤:
- 新建nanoModbus_master目录
- 复制VCP_EX.vcproj重命名为nanoModbus_master.vcproj
- 用文本编辑器全局替换工程文件中的"VCP_EX"为"nanoModbus_master"
- 添加nanomodbus.c/h到工程
特别注意:nanomodbus.c必须设置为"不使用预编译头",否则会导致编译错误。这是因为它采用了纯C编写,与VS的预编译头机制不兼容。
2.2 硬件接口配置
FT4232H的四个UART接口在Windows下会被识别为独立的COM口。在我的测试环境中:
- 接口A → COM84(作为Server)
- 接口B → COM85(作为Client)
硬件连接时需要注意:
- 将接口A的TXD与接口B的RXD交叉连接
- 将接口A的RXD与接口B的TXD交叉连接
- 共地连接(GND引脚)
这种连接方式模拟了典型的RS485双线制通信场景。实际项目中如果使用真正的RS485转换芯片,还需要注意终端电阻和偏置电阻的配置。
3. Modbus协议栈核心移植
3.1 平台适配层实现
nanoModbus通过nmbs_platform_conf结构体实现硬件抽象,需要实现以下关键组件:
c复制typedef struct nmbs_platform_conf {
nmbs_transport transport; // 传输类型(RTU/TCP)
int32_t (*read)(uint8_t* buf, uint16_t count, int32_t byte_timeout_ms, void* arg);
int32_t (*write)(const uint8_t* buf, uint16_t count, int32_t byte_timeout_ms, void* arg);
void* arg; // 用户上下文
} nmbs_platform_conf;
3.1.1 传输类型选择
对于串口通信,设置为NMBS_TRANSPORT_RTU。如果通过以太网通信,则选择NMBS_TRANSPORT_TCP。两种模式的主要区别:
| 特性 | RTU模式 | TCP模式 |
|---|---|---|
| 帧格式 | 二进制 | 在TCP包中封装Modbus |
| 校验方式 | CRC16 | 无(依赖TCP校验) |
| 典型速率 | 1200-115200 bps | 10/100 Mbps |
| 适用场景 | 短距离设备间通信 | 远距离网络通信 |
3.1.2 读写函数实现
读函数示例(基于Windows API):
c复制int32_t ft_transport_read(uint8_t* buf, uint16_t count,
int32_t byte_timeout_ms, void* arg) {
nmbs_user_data_s *dev = (nmbs_user_data_s*)arg;
DWORD dwRead;
// 设置超时参数
COMMTIMEOUTS timeouts = { 0 };
timeouts.ReadIntervalTimeout = MAXDWORD; // 禁用间隔超时
timeouts.ReadTotalTimeoutConstant = byte_timeout_ms;
SetCommTimeouts(dev->hCommPort, &timeouts);
if (!ReadFile(dev->hCommPort, buf, count, &dwRead, NULL)) {
return NMBS_ERROR_TRANSPORT;
}
return dwRead;
}
写函数实现类似,但需要注意:
- 写超时应略大于字节传输时间(如115200bps时每个字节约87μs)
- 清空发送缓冲区后再写入新数据
3.2 串口配置要点
正确的串口参数对Modbus RTU通信至关重要:
c复制DCB dcb = { 0 };
dcb.DCBlength = sizeof(DCB);
GetCommState(hCommPort, &dcb);
// 关键参数设置
dcb.BaudRate = 115200; // 波特率
dcb.ByteSize = 8; // 数据位
dcb.Parity = NOPARITY; // 无校验
dcb.StopBits = ONESTOPBIT;// 停止位
// 必须设置的流控参数
dcb.fOutxCtsFlow = FALSE;
dcb.fRtsControl = RTS_CONTROL_DISABLE;
dcb.fOutX = FALSE;
dcb.fInX = FALSE;
SetCommState(hCommPort, &dcb);
实际项目中遇到过因流控设置不当导致的通信失败案例。有些RS485转换芯片会自动控制方向,此时必须禁用RTS/CTS流控。
4. Server端实现细节
4.1 设备初始化流程
- 查找FTDI设备:
c复制FT_CreateDeviceInfoList(&numDevs);
FT_GetDeviceInfoList(devInfo, &numDevs);
- 打开指定接口:
c复制FT_OpenEx("FT4232H MiniModule A", FT_OPEN_BY_DESCRIPTION, &fthandle);
- 获取COM端口号(注意高编号端口处理):
c复制char COMx[11];
if (COMPORT <= 9)
sprintf(COMx, "COM%d", COMPORT);
else
sprintf(COMx, "\\\\\\\\.\\\\COM%d", COMPORT); // 需要四个反斜杠
4.2 Modbus回调函数配置
nanoModbus通过回调函数实现业务逻辑处理:
c复制nmbs_callbacks callbacks;
nmbs_callbacks_create(&callbacks);
// 寄存器读取回调
callbacks.read_holding_registers = ft_read_holding_registers;
// 寄存器写入回调
callbacks.write_multiple_registers = ft_write_multiple_registers;
示例寄存器读取实现:
c复制nmbs_error ft_read_holding_registers(uint16_t address, uint16_t quantity,
uint16_t* registers_out,
uint8_t unit_id, void* arg) {
// 实际项目这里可以访问硬件寄存器或内存数据库
for (uint16_t i = 0; i < quantity; i++) {
registers_out[i] = address + i; // 示例数据
}
return NMBS_ERROR_NONE;
}
4.3 Server主循环
c复制nmbs_t nmbs;
nmbs_server_create(&nmbs, 1, &platform_conf, &callbacks);
while (1) {
nmbs_error err = nmbs_server_poll(&nmbs);
if (err != NMBS_ERROR_NONE) {
// 错误处理逻辑
}
Sleep(10); // 防止CPU占用过高
}
5. Client端实现与测试
5.1 Client初始化
与Server端的主要区别:
- 使用接口B(COM85)
- 调用nmbs_client_create而非server_create
c复制nmbs_t nmbs;
nmbs_client_create(&nmbs, &platform_conf);
nmbs_set_destination_rtu_address(&nmbs, 0x01); // 目标地址匹配Server
5.2 读写测试代码
c复制uint16_t regs[32];
while (1) {
// 读取保持寄存器
nmbs_error err = nmbs_read_holding_registers(&nmbs, 0, 32, regs);
if (err == NMBS_ERROR_NONE) {
// 处理读取的数据...
}
// 写入多个寄存器
err = nmbs_write_multiple_registers(&nmbs, 0, 32, regs);
Sleep(1000); // 1秒间隔
}
5.3 典型问题排查
-
通信无响应:
- 检查物理连接(TXD/RXD是否交叉)
- 确认波特率等参数一致
- 使用串口调试工具验证基础通信
-
CRC校验错误:
- 确认两端CRC计算方式一致
- 检查字节超时设置是否合理
-
从站无应答:
- 确认从站地址设置正确
- 检查Server是否已正确初始化
6. 性能优化建议
在实际项目应用中,我总结了以下优化经验:
-
超时参数调优:
- 字节超时:典型值5-20ms(115200bps时)
- 帧间超时:至少3.5个字符时间(如115200bps时约300μs)
-
批量读写优化:
- 尽量使用多寄存器读写功能码(如FC23)
- 避免频繁的小数据包传输
-
错误处理增强:
- 添加重试机制(典型3次重试)
- 实现超时后的连接恢复流程
-
内存占用优化:
- 根据实际需求调整NMBS_MAX_MESSAGE_LENGTH
- 静态分配所需缓冲区
这个移植项目完整展示了如何在嵌入式系统中实现Modbus通信。nanoModbus的简洁设计使其非常适合资源受限的环境,而通过合理的参数配置和优化,完全可以满足工业级应用的要求。