目录
[1. 修改工程](#1. 修改工程)
[2. 移植](#2. 移植)
[2.1 transport](#2.1 transport)
[2.2 arg](#2.2 arg)
[2.3 read](#2.3 read)
[2.4 write](#2.4 write)
[3. Server部分](#3. Server部分)
[3.1 查找设备](#3.1 查找设备)
[3.2 打开设备](#3.2 打开设备)
[3.3 设置串口参数](#3.3 设置串口参数)
[3.4 初始化nanoModbus](#3.4 初始化nanoModbus)
[3.5 Poll处理](#3.5 Poll处理)
[4. Client部分](#4. Client部分)
[4.1 查找设备](#4.1 查找设备)
[4.2 初始化nanoModbus](#4.2 初始化nanoModbus)
[4.3 设置Modbus从站设备的rtu地址](#4.3 设置Modbus从站设备的rtu地址)
[4.4 读写部分](#4.4 读写部分)
nanoModbus下载地址如下,基于v1.22.0github.com
https://github.com/debevv/nanoMODBUS
硬件平台选择FT4232H Mini Module,该平台支持4个UART,将UART1和UART2分别作为Modbus主机和从机。
软件基于官方例程修改:https://ftdichip.com/wp-content/uploads/2020/07/VCP_EX.zip
1. 修改工程
在VCP_EX工程目录内新建nanoModbus_master文件夹,将VCP_EX文件夹中的VCP_EX.vcproj和VCP_EX.cpp拷贝过来,并修改文件名为nanoModbus_master.vcproj和nanoModbus_master.cpp,用文本编辑器修改nanoModbus_master.vcproj,将VCP_EX改为nanoModbus_master。
将其他文件h文件和c文件拷贝到文件夹内。
打开VCP_EX.sln,将nanoModbus_master.vcproj加入工程。将源文件加入工程,工程目录如下:

右键选中stdafx.cpp,属性界面内选择配置属性-->C/C++-->预编译头,在预编译头中选择"创建",而不是原来默认的"使用"(解决无法打开预编译头文件的问题)。
将nanoModbus的源文件(nanomodbus.c和nanomodbus.h)加入工程。和stdafx.cpp类似操作,nanomodbus.c属性在预编译头中选择"不使用预编译头"。

2. 移植
移植的核心是nmbs_platform_conf结构体的配置。
cpp
typedef struct nmbs_platform_conf {
nmbs_transport transport; /*!< Transport type */
int32_t (*read)(uint8_t* buf, uint16_t count, int32_t byte_timeout_ms,
void* arg); /*!< Bytes read transport function pointer */
int32_t (*write)(const uint8_t* buf, uint16_t count, int32_t byte_timeout_ms,
void* arg); /*!< Bytes write transport function pointer */
uint16_t (*crc_calc)(const uint8_t* data, uint32_t length,
void* arg); /*!< CRC calculation function pointer. Optional */
void* arg; /*!< User data, will be passed to functions above */
uint32_t initialized; /*!< Reserved, workaround for older user code not calling nmbs_platform_conf_create() */
} nmbs_platform_conf;
transport - 传输类型,NMBS_TRANSPORT_RTU和NMBS_TRANSPORT_TCP。
Modbus RTU是一种基于串行通信的协议,通常使用RS-232或RS-485接口。其报文结构紧凑,包含从站地址、功能码、数据以及CRC校验码。RTU的通信效率较高,但对传输距离和设备数量有限制,通常适用于小型工业网络。
Modbus TCP运行在TCP/IP网络上,使用以太网进行通信。它将标准Modbus数据帧嵌入到TCP帧中,报文头部包含事务处理标识符、协议标识符、数据长度和单元标识符。由于TCP/IP的可靠性,Modbus TCP不需要额外的CRC校验。它支持更大的网络规模和更高的传输速度,适合复杂的工业自动化场景。
read - 读函数指针,用于向串口或TCP连接读数据。
write - 写函数指针,用于向串口或TCP连接写数据。
crc_calc - 校验函数指针,可选,一般不需要配置,nanoModbus会初始化为内部的CRC校验函数。
arg - 传递用户上下文信息。例如:串口句柄(如HANDLE类型)、套接字描述符(如SOCKET)、自定义配置结构体。
initialized - 解决旧代码未调用初始化函数的问题,新用户只需要调用nmbs_platform_conf_create自动初始化这个变量。
对于一般的应用,初始化transport、read、write和arg即可。当前版本的nanoModbus需要调用nmbs_platform_conf_create先初始化一下配置变量
cpp
nmbs_platform_conf platform_conf;
nmbs_platform_conf_create(&platform_conf);
platform_conf.transport = NMBS_TRANSPORT_RTU;
platform_conf.read = ft_transport_read;
platform_conf.write = ft_transport_write;
platform_conf.arg = (void *)&nmbs_dev_ft;
2.1 transport
如果是串口,transport设置为NMBS_TRANSPORT_RTU,如果是TCP/IP,则设置为NMBS_TRANSPORT_TCP。
cpp
platform_conf.transport = NMBS_TRANSPORT_RTU;
2.2 arg
根据自己平台定义一个用户数据结构体
cpp
typedef struct {
HANDLE hCommPort;
}nmbs_user_data_s;
nmbs_user_data_s nmbs_dev_ft;
这里是记录串口的句柄。
然后把这个变量赋值
cpp
platform_conf.arg = (void *)&nmbs_dev_ft;
2.3 read
这里使用标准串口读函数,正确时返回的是读入字节数,错误则返回负数。
cpp
int32_t ft_transport_read(uint8_t* buf, uint16_t count, int32_t byte_timeout_ms, void* arg)
{
nmbs_user_data_s *dev_ft = (nmbs_user_data_s*)arg;
DWORD dwRead;
BOOL fSuccess;
if (byte_timeout_ms > 0)
{
COMMTIMEOUTS timeouts = { 0 };
timeouts.ReadTotalTimeoutConstant = (DWORD)byte_timeout_ms;
SetCommTimeouts(dev_ft->hCommPort, &timeouts);
}
fSuccess = ReadFile(dev_ft->hCommPort, buf, count, &dwRead, NULL);
if (!fSuccess)
{
printf("Read Failed %d\n", GetLastError());
return NMBS_ERROR_TRANSPORT;
}
return dwRead;
}
2.4 write
和读类似
cpp
int32_t ft_transport_write(const uint8_t* buf, uint16_t count, int32_t byte_timeout_ms, void* arg)
{
nmbs_user_data_s* dev_ft = (nmbs_user_data_s*)arg;
BOOL fSuccess;
DWORD dwWritten;
if (byte_timeout_ms > 0)
{
COMMTIMEOUTS timeouts = { 0 };
timeouts.WriteTotalTimeoutConstant = (DWORD)byte_timeout_ms;
SetCommTimeouts(dev_ft->hCommPort, &timeouts);
}
fSuccess = WriteFile(dev_ft->hCommPort, buf, count, &dwWritten, NULL);
if (!fSuccess)
{
printf("Write Failed %d\n", GetLastError());
return NMBS_ERROR_TRANSPORT;
}
return dwWritten;
}
3. Server部分
3.1 查找设备
FT4232H有4个COM口,需要找到对应的COM信息,和VCP_EX例程一样,通过D2XX的API函数来实现。
通过FT_OpenEx打开FT4232H的第一个COM口。
cpp
/***********************************************************************
//Find the com port that has been assigned to your device.
/***********************************************************************/
FT_STATUS ftStatus;
FT_DEVICE_LIST_INFO_NODE* devInfo;
DWORD numDevs;
// create the device information list
ftStatus = FT_CreateDeviceInfoList(&numDevs);
if (ftStatus == FT_OK) {
printf("Number of devices is %d\n", numDevs);
}
if (numDevs > 0) {
// allocate storage for list based on numDevs
devInfo = (FT_DEVICE_LIST_INFO_NODE*)malloc(sizeof(FT_DEVICE_LIST_INFO_NODE)*numDevs);
// get the device information list
ftStatus = FT_GetDeviceInfoList(devInfo,&numDevs);
if (ftStatus == FT_OK) {
for (int i = 0; i < numDevs; i++) {
printf("Dev %d:\n",i);
printf(" Flags=0x%x\n",devInfo[i].Flags);
printf(" Type=0x%x\n",devInfo[i].Type);
printf(" ID=0x%x\n",devInfo[i].ID);
printf(" LocId=0x%x\n",devInfo[i].LocId);
printf(" SerialNumber=%s\n",devInfo[i].SerialNumber);
printf(" Description=%s\n",devInfo[i].Description);
printf(" ftHandle=0x%x\n",devInfo[i].ftHandle);
}
}
}
res = FT_OpenEx("FT4232H MiniModule A", FT_OPEN_BY_DESCRIPTION, &fthandle);
if (res != FT_OK) {
printf("opening failed! with error %d\n", res);
return 1;
}
res = FT_GetComPortNumber(fthandle, &COMPORT);
if (res != FT_OK) {
printf("get com port failed %d\n", res);
return 1;
}
if (COMPORT == -1) {
printf("no com port installed \n");
}
else {
printf("com port number is %d\n", COMPORT);
}
FT_Close(fthandle);
FT_GetComPortNumber获取的是对应的COM编号。
3.2 打开设备
通过上一步获取的COM编号,打开这个串口设备,注意这里官方目前的代码有个bug,当使用COM10及更高端口时,直接使用"COM%d"格式会失败,当端口号≥10时,必须使用"\\\\.\\COM%d"格式(如"\\\\.\\COM10"),因为Windows对COM1-9有预定义别名,而COM10+需直接访问设备命名空间。
cpp
/********************************************************/
// Open the com port assigned to your device
/********************************************************/
if (COMPORT <= 9)
sprintf(COMx, "COM%d", COMPORT);
else
sprintf(COMx, "\\\\.\\COM%d", COMPORT); // 注意转义字符需写为双反斜杠
hCommPort = CreateFile(
COMx,
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
if (hCommPort == INVALID_HANDLE_VALUE)
{
printf("Help - failed to open\n");
return(1);
}
printf("Open %s OK\n", COMx);
nmbs_dev_ft.hCommPort = hCommPort;
将这个串口的句柄赋值给nanoModbus的配置参数。
注意COMx数组的大小也要改为
cpp
char COMx[11];
memset(COMx, 0, 11);
3.3 设置串口参数
通过GetCommState获取参数,然后修改参数再通过SetCommState设置新的参数。
cpp
/********************************************************/
// Configure the UART interface parameters
/********************************************************/
fSuccess = GetCommState(hCommPort, &dcb);
if (!fSuccess) {
printf("GetCommStateFailed \n", GetLastError());
return (2);
}
//set parameters.
dcb.BaudRate = 115200;
dcb.ByteSize = 8;
dcb.Parity = NOPARITY;
dcb.StopBits = ONESTOPBIT;
fSuccess = SetCommState(hCommPort, &dcb);
if (!fSuccess) {
printf("SetCommStateFailed \n", GetLastError());
return (3);
}
printf("Port configured \n");
3.4 初始化nanoModbus
首先初始化一个callback变量
cpp
nmbs_callbacks nmbs_callback;
nmbs_callbacks_create(&nmbs_callback);
nmbs_callback.arg = (void*)&nmbs_dev_ft;
nmbs_callback.read_holding_registers = ft_read_holding_registers;
nmbs_callback.write_multiple_registers = ft_write_multiple_registers;
同样,将用户数据传入这个变量
cpp
nmbs_callback.arg = (void*)&nmbs_dev_ft;
这个变量定义了一系列的回调函数 ,这些回调函数用于定义 Modbus 从站(服务端)对各类功能码的响应逻辑,这部分是Modbus定义的功能码处理函数,用户根据实际情况选择对应的函数指针赋值。
| 成员类型 | 成员名 | 作用 | ||
|---|---|---|---|---|
| 功能码处理函数指针 | read_coils |
对应Modbus功能码0x01(读取线圈状态),用于从从站设备读取可读写的数字量输出(如继电器状态、LED指示灯等)。coils和discrete_inputs的区别是coils可以通过FC 05或FC 15修改,而discrete_inputs不可以修改。 作用 :功能码01(FC 01),用于读取从站设备中连续的线圈状态(ON/OFF)。线圈是可读写的数字量。 参数: - uint16_t address:起始地址 - uint16_t quantity:读取数量(1-2000) - nmbs_bitfield coils_out:存储结果的位数组 - uint8_t unit_id:请求的从站单元ID(RTU为设备地址,TCP为0) - void *arg:用户自定义参数 **示例:**从GPIO寄存器读取线圈状态 nmbs_error my_read_coils( uint16_t address, uint16_t quantity, nmbs_bitfield coils_out, uint8_t unit_id, void *arg ) { for (uint16_t i = 0; i < quantity; i++) { uint16_t addr = address + i; if (addr < MAX_COILS) { coils_out[i / 8] |= (get_gpio_output_state(addr) << (i % 8)); // 设置对应位 } else { return NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS; } } return NMBS_ERROR_NONE; } |
||
| read_discrete_inputs | 对应Modbus功能码0x02(读取离散输入),用于读取从站设备的只读数字量输入(如传感器状态、按钮信号等)。 作用 :当主站发送FC 02请求时,从站通过该回调函数提供离散输入的实际数据。 参数: - uint16_t address:起始地址 - uint16_t quantity:读取数量(1-2000) - nmbs_bitfield inputs_out:存储结果的位数组 - uint8_t unit_id:请求的从站单元ID(RTU为设备地址,TCP为0) - void *arg:用户自定义参数 **示例:**从GPIO寄存器读取离散输入 nmbs_error my_read_discrete_inputs( uint16_t address, uint16_t quantity, nmbs_bitfield inputs_out, uint8_t unit_id, void *arg ) { for (uint16_t i = 0; i < quantity; i++) { uint16_t addr = address + i; if (addr < MAX_INPUTS) { inputs_out[i / 8] |= (get_gpio_state(addr) << (i % 8)); // 设置对应位 } else { return NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS; } } return NMBS_ERROR_NONE; } |
|||
| read_holding_registers | 对应 Modbus 功能码 03(读取保持寄存器),用于从从站设备读取可读写的 16位寄存器数据(如配置参数、传感器测量值等) 作用 :功能码 03(FC 03),用于读取从站设备中连续的保持寄存器 参数: - uint16_t quantity:读取的寄存器数量(最大125,受协议限制) - uint16_t *registers_out, 输出缓冲区(存储读取的寄存器值) - 其他:略 **示例:**从EEPROM或内存中读取寄存器数据 nmbs_error my_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++) { uint16_t addr = address + i; if (addr < MAX_REGISTERS) { registers_out[i] = read_eeprom_word(addr); // 从EEPROM读取16位数据 } else { return NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS; } } return NMBS_ERROR_NONE; } |
|||
read_input_registers |
对应 Modbus 功能码 04(读取输入寄存器),用于从从站设备读取 只读的16位寄存器数据(如传感器实时测量值)。和holding_registers的区别是,holding_registers可读写,而input_registers是只读的。 作用: 功能码 04(FC 04),用于读取从站设备中连续的输入寄存器(Input Registers)。 **参数:**略(类似) **示例:**从ADC读取模拟量并转换为寄存器值 nmbs_error my_read_input_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++) { uint16_t addr = address + i; if (addr < MAX_INPUT_REGISTERS) { registers_out[i] = read_adc_value(addr); // 从ADC读取16位数据 } else { return NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS; } } return NMBS_ERROR_NONE; } |
|||
| write_single_coil write_single_register write_multiple_coils write_multiple_registers | 这4个函数对应coil或register的写。 | |||
| read_file_record | 用于实现 Modbus 功能码 0x14(FC 20) 的函数,其核心作用是 从从站设备的文件记录中读取数据 作用: 功能码 0x14(FC 20),用于读取从站设备中存储的文件记录(File Record) 参数: - nmbs_t *nmbs, nanoModbus 实例指针 - uint16_t file_number, 文件编号(1-65535) - uint16_t record_number, 记录编号(0000-9999) - uint16_t *registers, 输出缓冲区(存储读取的寄存器值) - uint16_t count, 需读取的寄存器数量 - 其他:略 **示例:**从Flash中读取文件记录 nmbs_error my_read_file_record( nmbs_t *nmbs, uint16_t file_number, uint16_t record_number, uint16_t *registers, uint16_t count, uint8_t unit_id, void *arg ) { uint16_t record_base_addr = calculate_flash_address(file_number, record_number); for (uint16_t i = 0; i < count; i++) { registers[i] = read_flash_word(record_base_addr + i * 2); // 假设每个寄存器占2字节 } return NMBS_ERROR_NONE; } |
|||
| write_file_record | 对应read_file_record理解即可。 | |||
| 设备识别函数 | read_device_identification |
用于实现 Modbus 功能码 0x2B(FC 43) 的函数,其核心作用是 读取从站设备的标识信息,包括厂商名称、产品型号、版本号等。 示例: nmbs_error my_read_device_identification( uint8_t object_id, char *buffer, ) { switch (object_id) { case 0x00: // VendorName strncpy(buffer, "ABC Corp", NMBS_DEVICE_IDENTIFICATION_STRING_LENGTH); break; case 0x01: // ProductCode strncpy(buffer, "MOD-100", NMBS_DEVICE_IDENTIFICATION_STRING_LENGTH); break; default: return NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS; } return NMBS_ERROR_NONE; } | ||
read_device_identification_map |
返回设备标识的映射表(位域数据) | |||
| 用户数据与状态 | arg |
用户自定义数据指针,传递给所有回调函数(如硬件句柄、配置参数) | ||
initialized |
库内部状态标记(用于兼容旧版代码,普通用户无需关注) |
这些回调函数不需要都实现,这里选2个作为示例:
cpp
nmbs_callback.read_holding_registers = ft_read_holding_registers;
nmbs_callback.write_multiple_registers = ft_write_multiple_registers;
这2个函数实现示例如下:
cpp
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[i] = get_sensor_data(address + i); // 自定义数据获取函数
registers_out[i] = i;
}
return NMBS_ERROR_NONE;
}
nmbs_error ft_write_multiple_registers(uint16_t address, uint16_t quantity, const uint16_t* registers, uint8_t unit_id, void* arg)
{
// 将数据写入硬件或内存
printf("write registers:\n");
for (uint16_t i = 0; i < quantity; i++) {
//set_device_state(address + i, registers[i]); // 自定义数据写入函数
printf("[%d]=0x%x ", i, registers[i]);
}
return NMBS_ERROR_NONE;
}
由于这里是验证主机方式,所以通过nmbs_server_create创建,第二个参数是Client的地址,注意必须和Client一致。
cpp
nmbs_t nmbs;
if (nmbs_server_create(&nmbs, 1, &platform_conf, &nmbs_callback) != NMBS_ERROR_NONE) {
DWORD err = GetLastError();
printf("create nmbs server fail. Error code: %d\n", err);
return 4;
}
设置读超时
cpp
nmbs_set_read_timeout(&nmbs, 1000); // 设置1秒超时
3.5 Poll处理
在while(1)主循环中调用nmbs_server_poll,这个函数用于处理客户端请求并管理数据收发
cpp
while (1) {
nmbs_error err = nmbs_server_poll(&nmbs);
if (err != NMBS_ERROR_NONE) {
// 处理超时或错误
}
Sleep(10);
}
4. Client部分
将Server的工程复制一份作为Client的工程,将nanoModbus_server相关信息改为nanoModbus_client,再将这个工程添加到VCP_EX.sln中。
4.1 查找设备
打开设备改为设备B
cpp
res = FT_OpenEx("FT4232H MiniModule B", FT_OPEN_BY_DESCRIPTION, &fthandle);
4.2 初始化nanoModbus
通过nmbs_client_create初始化,比server部分简单点
cpp
nmbs_t nmbs;
if (nmbs_client_create(&nmbs, &platform_conf) != NMBS_ERROR_NONE) {
DWORD err = GetLastError();
printf("create nmbs client fail. Error code: %d\n", err);
return 4;
}
printf("client create ok\n");
nmbs_set_byte_timeout(&nmbs, 100);
nmbs_set_read_timeout(&nmbs, 1000);
4.3 设置Modbus从站设备的rtu地址
通过nmbs_set_destination_rtu_address设置这个地址,要和Server端的设置一致,例如这里设置的rtu地址为1。
cpp
nmbs_set_destination_rtu_address(&nmbs, 0x01);
4.4 读写部分
这部分是和Server端配置的回调函数匹配的,之前设置的是read_holding_registers和write_multiple_registers,所以这里是和具体的应用相关的,需要根据自己的应用修改:
cpp
uint16_t regs_test[32];
while (1) {
nmbs_error status = nmbs_read_holding_registers(&nmbs, 0, 32, regs_test);
if (status != NMBS_ERROR_NONE) {
printf("read registers fail\n");
break;
}
else {
printf("Client read register:\n");
for (int i = 0; i < 32; i++)
{
printf("[%d]=0x%x ", i, regs_test[i]);
}
}
status = nmbs_write_multiple_registers(&nmbs, 0, 32, regs_test);
if (status != NMBS_ERROR_NONE) {
printf("write registers fail\n");
break;
}
else {
printf("Client write register:\n");
for (int i = 0; i < 32; i++)
{
printf("[%d]=0x%x ", i, regs_test[i]);
}
}
Sleep(1000);
}
5. 测试
将FT4232H模块串口1的RXD/TXD和串口2的TXD/RXD短接,分别运行server和client,先运行server,log信息如下:
bash
*************** NanoModbus Server ***************
Number of devices is 4
Dev 0:
Flags=0x2
Type=0x7
ID=0x4036011
LocId=0x1242
SerialNumber=FTDIB2GV2QB
Description=FT4232H MiniModule B
ftHandle=0x0
Dev 1:
Flags=0x2
Type=0x7
ID=0x4036011
LocId=0x1243
SerialNumber=FTDIB2GV2QC
Description=FT4232H MiniModule C
ftHandle=0x0
Dev 2:
Flags=0x2
Type=0x7
ID=0x4036011
LocId=0x1244
SerialNumber=FTDIB2GV2QD
Description=FT4232H MiniModule D
ftHandle=0x0
Dev 3:
Flags=0x2
Type=0x7
ID=0x4036011
LocId=0x1241
SerialNumber=FTDIB2GV2QA
Description=FT4232H MiniModule A
ftHandle=0x0
com port number is 84
Open \\.\COM84 OK
Port configured
server create ok
client端的log信息如下:
bash
*************** NanoModbus Client ***************
Number of devices is 4
Dev 0:
Flags=0x2
Type=0x7
ID=0x4036011
LocId=0x1242
SerialNumber=FTDIB2GV2QB
Description=FT4232H MiniModule B
ftHandle=0x0
Dev 1:
Flags=0x2
Type=0x7
ID=0x4036011
LocId=0x1243
SerialNumber=FTDIB2GV2QC
Description=FT4232H MiniModule C
ftHandle=0x0
Dev 2:
Flags=0x2
Type=0x7
ID=0x4036011
LocId=0x1244
SerialNumber=FTDIB2GV2QD
Description=FT4232H MiniModule D
ftHandle=0x0
Dev 3:
Flags=0x1
Type=0x3
ID=0x0
LocId=0x0
SerialNumber=
Description=
ftHandle=0x0
com port number is 85
Open \\.\COM85 OK
Port configured
client create ok
两边数据交换log:
