Modbus TCP协议实现与应用开发¶
学习目标¶
完成本教程后,你将能够:
- 理解Modbus TCP协议的架构和工作原理
- 掌握Modbus TCP与Modbus RTU的区别
- 熟悉MBAP报头和功能码的使用
- 实现Modbus TCP客户端和服务器
- 使用lwIP协议栈开发Modbus TCP应用
- 进行Modbus TCP网络调试和故障排除
- 实现工业设备的远程监控和控制
前置要求¶
在开始本教程之前,你需要:
知识要求: - 熟悉TCP/IP网络协议基础 - 了解Modbus RTU协议原理 - 掌握C语言编程 - 理解客户端-服务器架构
技能要求: - 能够使用STM32或ESP32进行开发 - 会使用lwIP或其他TCP/IP协议栈 - 了解工业自动化基本概念 - 熟悉网络调试工具(Wireshark、Modbus Poll)
准备工作¶
硬件准备¶
| 名称 | 数量 | 说明 | 参考型号 |
|---|---|---|---|
| STM32开发板 | 1 | 带以太网的MCU | STM32F407/F429 |
| ESP32开发板 | 1 | 可选,用于WiFi方案 | ESP32-DevKitC |
| 以太网线 | 1 | Cat5e或更高 | - |
| 路由器/交换机 | 1 | 用于网络连接 | - |
| USB调试器 | 1 | ST-Link或J-Link | - |
软件准备¶
- 开发环境:
- STM32CubeIDE或Arduino IDE
- lwIP协议栈(STM32)或ModbusTCP库(ESP32)
- 测试工具:
- Modbus Poll(主站模拟器)
- Modbus Slave(从站模拟器)
- Wireshark(网络抓包)
- Python + pymodbus(脚本测试)
环境配置¶
安装Modbus测试工具¶
Modbus TCP协议概述¶
什么是Modbus TCP?¶
Modbus TCP是Modbus协议在以太网上的实现,将Modbus应用层协议封装在TCP/IP协议中,实现工业设备的网络通信。
核心特点: - 基于以太网:使用标准TCP/IP协议 - 开放标准:免费使用,广泛支持 - 简单可靠:继承Modbus协议的简单性 - 远程访问:支持跨网络通信 - 高速传输:比串口Modbus更快
Modbus TCP vs Modbus RTU¶
| 特性 | Modbus RTU | Modbus TCP |
|---|---|---|
| 传输介质 | RS232/RS485串口 | 以太网 |
| 物理层 | 串行通信 | TCP/IP |
| 地址范围 | 1-247 | 0-255(单元标识符) |
| 校验方式 | CRC16 | TCP校验和 |
| 传输速度 | 9600-115200 bps | 10/100/1000 Mbps |
| 通信距离 | <1200m | 无限制(通过路由) |
| 成本 | 低 | 中等 |
| 应用场景 | 现场总线 | 工业以太网 |
Modbus TCP协议架构¶
+----------------------------------------------------------+
| 应用层 |
| +--------------------------------------------------+ |
| | Modbus应用协议 | |
| | 功能码 + 数据 | |
| +--------------------------------------------------+ |
+----------------------------------------------------------+
| MBAP报头 |
| +--------------------------------------------------+ |
| | 事务ID | 协议ID | 长度 | 单元ID | |
| | 2字节 | 2字节 | 2字节 | 1字节 | |
| +--------------------------------------------------+ |
+----------------------------------------------------------+
| TCP层 |
| +--------------------------------------------------+ |
| | TCP协议(端口502) | |
| +--------------------------------------------------+ |
+----------------------------------------------------------+
| IP层 |
| +--------------------------------------------------+ |
| | IP协议 | |
| +--------------------------------------------------+ |
+----------------------------------------------------------+
| 以太网层 |
| +--------------------------------------------------+ |
| | 以太网帧 | |
| +--------------------------------------------------+ |
+----------------------------------------------------------+
MBAP报头详解¶
MBAP(Modbus Application Protocol)报头是Modbus TCP特有的,用于在TCP/IP网络上传输Modbus消息。
MBAP报头结构¶
typedef struct {
uint16_t transaction_id; // 事务标识符(0-65535)
uint16_t protocol_id; // 协议标识符(固定为0)
uint16_t length; // 后续字节数(单元ID+PDU长度)
uint8_t unit_id; // 单元标识符(从站地址)
} mbap_header_t;
字段说明:
- 事务标识符(Transaction ID):
- 由客户端生成,用于匹配请求和响应
- 每个请求使用不同的ID
-
服务器响应时原样返回
-
协议标识符(Protocol ID):
- 固定为0x0000
-
用于标识Modbus协议
-
长度(Length):
- 后续字节数 = 单元ID(1字节) + PDU长度
-
不包括MBAP报头本身的6字节
-
单元标识符(Unit ID):
- 类似于Modbus RTU的从站地址
- 0xFF表示广播(不常用)
- 通常设置为1或0
Modbus TCP帧格式示例¶
读取保持寄存器请求¶
请求帧(12字节):
00 01 - 事务ID = 1
00 00 - 协议ID = 0
00 06 - 长度 = 6字节
01 - 单元ID = 1
03 - 功能码 = 03(读保持寄存器)
00 00 - 起始地址 = 0
00 0A - 数量 = 10个寄存器
十六进制:00 01 00 00 00 06 01 03 00 00 00 0A
读取保持寄存器响应¶
响应帧(27字节):
00 01 - 事务ID = 1(与请求相同)
00 00 - 协议ID = 0
00 17 - 长度 = 23字节
01 - 单元ID = 1
03 - 功能码 = 03
14 - 字节数 = 20字节(10个寄存器×2字节)
00 64 - 寄存器0 = 100
00 C8 - 寄存器1 = 200
01 2C - 寄存器2 = 300
... - 其他寄存器数据
十六进制:00 01 00 00 00 17 01 03 14 00 64 00 C8 01 2C ...
常用功能码¶
| 功能码 | 名称 | 说明 | 请求数据 | 响应数据 |
|---|---|---|---|---|
| 01 | Read Coils | 读线圈 | 起始地址+数量 | 字节数+状态 |
| 02 | Read Discrete Inputs | 读离散输入 | 起始地址+数量 | 字节数+状态 |
| 03 | Read Holding Registers | 读保持寄存器 | 起始地址+数量 | 字节数+数据 |
| 04 | Read Input Registers | 读输入寄存器 | 起始地址+数量 | 字节数+数据 |
| 05 | Write Single Coil | 写单个线圈 | 地址+值 | 地址+值 |
| 06 | Write Single Register | 写单个寄存器 | 地址+值 | 地址+值 |
| 15 | Write Multiple Coils | 写多个线圈 | 地址+数量+数据 | 地址+数量 |
| 16 | Write Multiple Registers | 写多个寄存器 | 地址+数量+数据 | 地址+数量 |
异常响应¶
当服务器无法处理请求时,返回异常响应:
异常响应帧:
00 01 - 事务ID
00 00 - 协议ID
00 03 - 长度 = 3字节
01 - 单元ID
83 - 功能码 = 0x03 + 0x80(异常标志)
02 - 异常码 = 02(非法数据地址)
常见异常码:
| 异常码 | 名称 | 说明 |
|---|---|---|
| 01 | Illegal Function | 不支持的功能码 |
| 02 | Illegal Data Address | 非法数据地址 |
| 03 | Illegal Data Value | 非法数据值 |
| 04 | Slave Device Failure | 从站设备故障 |
| 05 | Acknowledge | 确认(长时间操作) |
| 06 | Slave Device Busy | 从站设备忙 |
步骤1:Modbus TCP服务器实现(STM32 + lwIP)¶
1.1 数据结构定义¶
#include "lwip/tcp.h"
#include <stdint.h>
#include <string.h>
// MBAP报头结构
typedef struct __attribute__((packed)) {
uint16_t transaction_id;
uint16_t protocol_id;
uint16_t length;
uint8_t unit_id;
} mbap_header_t;
// Modbus TCP帧结构
typedef struct {
mbap_header_t header;
uint8_t function_code;
uint8_t data[252]; // 最大PDU长度
} modbus_tcp_frame_t;
// 寄存器数据
#define HOLDING_REG_START 0
#define HOLDING_REG_COUNT 100
#define INPUT_REG_START 0
#define INPUT_REG_COUNT 50
#define COIL_START 0
#define COIL_COUNT 64
#define DISCRETE_START 0
#define DISCRETE_COUNT 64
static uint16_t holding_registers[HOLDING_REG_COUNT];
static uint16_t input_registers[INPUT_REG_COUNT];
static uint8_t coils[COIL_COUNT / 8];
static uint8_t discrete_inputs[DISCRETE_COUNT / 8];
// Modbus TCP服务器PCB
static struct tcp_pcb *modbus_tcp_pcb;
1.2 字节序转换¶
Modbus TCP使用大端字节序(网络字节序):
// 主机字节序转网络字节序
static uint16_t htons_modbus(uint16_t hostshort)
{
return ((hostshort & 0xFF) << 8) | ((hostshort >> 8) & 0xFF);
}
// 网络字节序转主机字节序
static uint16_t ntohs_modbus(uint16_t netshort)
{
return ((netshort & 0xFF) << 8) | ((netshort >> 8) & 0xFF);
}
// 或使用lwIP提供的函数
#include "lwip/def.h"
// htons(), ntohs(), htonl(), ntohl()
1.3 功能码处理函数¶
读取保持寄存器(功能码03)¶
static uint16_t modbus_read_holding_registers(uint16_t start_addr,
uint16_t quantity,
uint8_t *response_data)
{
// 检查地址范围
if(start_addr >= HOLDING_REG_COUNT ||
start_addr + quantity > HOLDING_REG_COUNT ||
quantity == 0 || quantity > 125)
{
return 0x02; // 异常码:非法数据地址
}
// 字节数
uint8_t byte_count = quantity * 2;
response_data[0] = byte_count;
// 复制寄存器数据
for(uint16_t i = 0; i < quantity; i++)
{
uint16_t reg_value = holding_registers[start_addr + i];
response_data[1 + i*2] = (reg_value >> 8) & 0xFF;
response_data[2 + i*2] = reg_value & 0xFF;
}
return byte_count + 1; // 返回响应数据长度
}
写单个寄存器(功能码06)¶
static uint16_t modbus_write_single_register(uint16_t reg_addr,
uint16_t reg_value,
uint8_t *response_data)
{
// 检查地址范围
if(reg_addr >= HOLDING_REG_COUNT)
{
return 0x02; // 异常码:非法数据地址
}
// 写入寄存器
holding_registers[reg_addr] = reg_value;
// 构建响应(回显请求)
response_data[0] = (reg_addr >> 8) & 0xFF;
response_data[1] = reg_addr & 0xFF;
response_data[2] = (reg_value >> 8) & 0xFF;
response_data[3] = reg_value & 0xFF;
return 4; // 响应数据长度
}
写多个寄存器(功能码16)¶
static uint16_t modbus_write_multiple_registers(uint16_t start_addr,
uint16_t quantity,
uint8_t byte_count,
uint8_t *reg_data,
uint8_t *response_data)
{
// 检查参数
if(start_addr >= HOLDING_REG_COUNT ||
start_addr + quantity > HOLDING_REG_COUNT ||
quantity == 0 || quantity > 123 ||
byte_count != quantity * 2)
{
return 0x03; // 异常码:非法数据值
}
// 写入寄存器
for(uint16_t i = 0; i < quantity; i++)
{
uint16_t reg_value = (reg_data[i*2] << 8) | reg_data[i*2 + 1];
holding_registers[start_addr + i] = reg_value;
}
// 构建响应
response_data[0] = (start_addr >> 8) & 0xFF;
response_data[1] = start_addr & 0xFF;
response_data[2] = (quantity >> 8) & 0xFF;
response_data[3] = quantity & 0xFF;
return 4;
}
读取线圈(功能码01)¶
static uint16_t modbus_read_coils(uint16_t start_addr,
uint16_t quantity,
uint8_t *response_data)
{
// 检查地址范围
if(start_addr >= COIL_COUNT ||
start_addr + quantity > COIL_COUNT ||
quantity == 0 || quantity > 2000)
{
return 0x02;
}
// 计算字节数
uint8_t byte_count = (quantity + 7) / 8;
response_data[0] = byte_count;
// 清零响应数据
memset(&response_data[1], 0, byte_count);
// 复制线圈状态
for(uint16_t i = 0; i < quantity; i++)
{
uint16_t coil_addr = start_addr + i;
uint8_t byte_index = coil_addr / 8;
uint8_t bit_index = coil_addr % 8;
if(coils[byte_index] & (1 << bit_index))
{
response_data[1 + i/8] |= (1 << (i % 8));
}
}
return byte_count + 1;
}
1.4 Modbus TCP请求处理¶
static void modbus_process_request(struct tcp_pcb *tpcb, struct pbuf *p)
{
if(p->tot_len < 8) // 最小帧长度:MBAP(7) + 功能码(1)
{
pbuf_free(p);
return;
}
// 解析MBAP报头
uint8_t *data = (uint8_t *)p->payload;
mbap_header_t header;
header.transaction_id = (data[0] << 8) | data[1];
header.protocol_id = (data[2] << 8) | data[3];
header.length = (data[4] << 8) | data[5];
header.unit_id = data[6];
// 检查协议ID
if(header.protocol_id != 0)
{
pbuf_free(p);
return;
}
// 获取功能码
uint8_t function_code = data[7];
// 准备响应缓冲区
uint8_t response[260]; // MBAP(7) + 功能码(1) + 数据(最大252)
uint16_t response_len = 0;
uint8_t exception_code = 0;
// 根据功能码处理
switch(function_code)
{
case 0x03: // 读保持寄存器
{
uint16_t start_addr = (data[8] << 8) | data[9];
uint16_t quantity = (data[10] << 8) | data[11];
response[7] = 0x03; // 功能码
uint16_t data_len = modbus_read_holding_registers(start_addr, quantity, &response[8]);
if(data_len == 0x02 || data_len == 0x03)
{
exception_code = data_len;
}
else
{
response_len = 8 + data_len;
}
break;
}
case 0x06: // 写单个寄存器
{
uint16_t reg_addr = (data[8] << 8) | data[9];
uint16_t reg_value = (data[10] << 8) | data[11];
response[7] = 0x06;
uint16_t data_len = modbus_write_single_register(reg_addr, reg_value, &response[8]);
if(data_len == 0x02 || data_len == 0x03)
{
exception_code = data_len;
}
else
{
response_len = 8 + data_len;
}
break;
}
case 0x10: // 写多个寄存器
{
uint16_t start_addr = (data[8] << 8) | data[9];
uint16_t quantity = (data[10] << 8) | data[11];
uint8_t byte_count = data[12];
response[7] = 0x10;
uint16_t data_len = modbus_write_multiple_registers(start_addr, quantity,
byte_count, &data[13], &response[8]);
if(data_len == 0x02 || data_len == 0x03)
{
exception_code = data_len;
}
else
{
response_len = 8 + data_len;
}
break;
}
case 0x01: // 读线圈
{
uint16_t start_addr = (data[8] << 8) | data[9];
uint16_t quantity = (data[10] << 8) | data[11];
response[7] = 0x01;
uint16_t data_len = modbus_read_coils(start_addr, quantity, &response[8]);
if(data_len == 0x02)
{
exception_code = data_len;
}
else
{
response_len = 8 + data_len;
}
break;
}
default:
exception_code = 0x01; // 非法功能码
break;
}
// 构建响应
if(exception_code != 0)
{
// 异常响应
response[0] = (header.transaction_id >> 8) & 0xFF;
response[1] = header.transaction_id & 0xFF;
response[2] = 0x00;
response[3] = 0x00;
response[4] = 0x00;
response[5] = 0x03; // 长度 = 3
response[6] = header.unit_id;
response[7] = function_code | 0x80; // 异常标志
response[8] = exception_code;
response_len = 9;
}
else
{
// 正常响应 - 填充MBAP报头
response[0] = (header.transaction_id >> 8) & 0xFF;
response[1] = header.transaction_id & 0xFF;
response[2] = 0x00;
response[3] = 0x00;
uint16_t length = response_len - 6;
response[4] = (length >> 8) & 0xFF;
response[5] = length & 0xFF;
response[6] = header.unit_id;
}
// 发送响应
tcp_write(tpcb, response, response_len, TCP_WRITE_FLAG_COPY);
tcp_output(tpcb);
pbuf_free(p);
}
1.5 TCP回调函数¶
// 接收回调
static err_t modbus_tcp_recv(void *arg, struct tcp_pcb *tpcb,
struct pbuf *p, err_t err)
{
if(p == NULL)
{
// 连接关闭
tcp_close(tpcb);
return ERR_OK;
}
// 处理Modbus请求
modbus_process_request(tpcb, p);
return ERR_OK;
}
// 接受连接回调
static err_t modbus_tcp_accept(void *arg, struct tcp_pcb *newpcb, err_t err)
{
printf("Modbus TCP client connected: %s\r\n",
ip4addr_ntoa(&newpcb->remote_ip));
// 设置接收回调
tcp_recv(newpcb, modbus_tcp_recv);
return ERR_OK;
}
// 初始化Modbus TCP服务器
void modbus_tcp_server_init(void)
{
// 初始化寄存器数据
for(int i = 0; i < HOLDING_REG_COUNT; i++)
{
holding_registers[i] = i * 10;
}
for(int i = 0; i < INPUT_REG_COUNT; i++)
{
input_registers[i] = i * 5;
}
// 创建TCP PCB
modbus_tcp_pcb = tcp_new();
if(modbus_tcp_pcb == NULL)
{
printf("Failed to create Modbus TCP PCB\r\n");
return;
}
// 绑定到端口502
err_t err = tcp_bind(modbus_tcp_pcb, IP_ADDR_ANY, 502);
if(err != ERR_OK)
{
printf("Failed to bind Modbus TCP port: %d\r\n", err);
tcp_close(modbus_tcp_pcb);
return;
}
// 开始监听
modbus_tcp_pcb = tcp_listen(modbus_tcp_pcb);
// 设置接受回调
tcp_accept(modbus_tcp_pcb, modbus_tcp_accept);
printf("Modbus TCP server started on port 502\r\n");
}
步骤2:Modbus TCP客户端实现¶
2.1 客户端数据结构¶
typedef struct {
struct tcp_pcb *pcb;
ip_addr_t server_ip;
uint16_t server_port;
uint16_t transaction_id;
bool connected;
uint8_t rx_buffer[260];
uint16_t rx_len;
} modbus_tcp_client_t;
static modbus_tcp_client_t modbus_client;
2.2 客户端请求构建¶
// 构建读保持寄存器请求
static uint16_t modbus_build_read_holding_request(uint8_t *buffer,
uint16_t transaction_id,
uint8_t unit_id,
uint16_t start_addr,
uint16_t quantity)
{
// MBAP报头
buffer[0] = (transaction_id >> 8) & 0xFF;
buffer[1] = transaction_id & 0xFF;
buffer[2] = 0x00; // 协议ID
buffer[3] = 0x00;
buffer[4] = 0x00; // 长度
buffer[5] = 0x06;
buffer[6] = unit_id;
// PDU
buffer[7] = 0x03; // 功能码
buffer[8] = (start_addr >> 8) & 0xFF;
buffer[9] = start_addr & 0xFF;
buffer[10] = (quantity >> 8) & 0xFF;
buffer[11] = quantity & 0xFF;
return 12;
}
// 构建写单个寄存器请求
static uint16_t modbus_build_write_single_register_request(uint8_t *buffer,
uint16_t transaction_id,
uint8_t unit_id,
uint16_t reg_addr,
uint16_t reg_value)
{
// MBAP报头
buffer[0] = (transaction_id >> 8) & 0xFF;
buffer[1] = transaction_id & 0xFF;
buffer[2] = 0x00;
buffer[3] = 0x00;
buffer[4] = 0x00;
buffer[5] = 0x06;
buffer[6] = unit_id;
// PDU
buffer[7] = 0x06; // 功能码
buffer[8] = (reg_addr >> 8) & 0xFF;
buffer[9] = reg_addr & 0xFF;
buffer[10] = (reg_value >> 8) & 0xFF;
buffer[11] = reg_value & 0xFF;
return 12;
}
// 构建写多个寄存器请求
static uint16_t modbus_build_write_multiple_registers_request(uint8_t *buffer,
uint16_t transaction_id,
uint8_t unit_id,
uint16_t start_addr,
uint16_t quantity,
uint16_t *reg_values)
{
// MBAP报头
buffer[0] = (transaction_id >> 8) & 0xFF;
buffer[1] = transaction_id & 0xFF;
buffer[2] = 0x00;
buffer[3] = 0x00;
uint16_t length = 7 + quantity * 2;
buffer[4] = (length >> 8) & 0xFF;
buffer[5] = length & 0xFF;
buffer[6] = unit_id;
// PDU
buffer[7] = 0x10; // 功能码
buffer[8] = (start_addr >> 8) & 0xFF;
buffer[9] = start_addr & 0xFF;
buffer[10] = (quantity >> 8) & 0xFF;
buffer[11] = quantity & 0xFF;
buffer[12] = quantity * 2; // 字节数
// 寄存器数据
for(uint16_t i = 0; i < quantity; i++)
{
buffer[13 + i*2] = (reg_values[i] >> 8) & 0xFF;
buffer[14 + i*2] = reg_values[i] & 0xFF;
}
return 13 + quantity * 2;
}
2.3 客户端TCP回调¶
// 接收回调
static err_t modbus_client_recv(void *arg, struct tcp_pcb *tpcb,
struct pbuf *p, err_t err)
{
modbus_tcp_client_t *client = (modbus_tcp_client_t *)arg;
if(p == NULL)
{
// 连接关闭
client->connected = false;
tcp_close(tpcb);
return ERR_OK;
}
// 复制数据到接收缓冲区
if(p->tot_len <= sizeof(client->rx_buffer))
{
pbuf_copy_partial(p, client->rx_buffer, p->tot_len, 0);
client->rx_len = p->tot_len;
// 解析响应
modbus_parse_response(client);
}
pbuf_free(p);
return ERR_OK;
}
// 连接回调
static err_t modbus_client_connected(void *arg, struct tcp_pcb *tpcb, err_t err)
{
modbus_tcp_client_t *client = (modbus_tcp_client_t *)arg;
if(err == ERR_OK)
{
client->connected = true;
printf("Modbus TCP client connected\r\n");
// 设置接收回调
tcp_recv(tpcb, modbus_client_recv);
}
else
{
printf("Modbus TCP connection failed: %d\r\n", err);
}
return ERR_OK;
}
// 初始化客户端
void modbus_tcp_client_init(const char *server_ip, uint16_t server_port)
{
// 解析服务器IP
ip4addr_aton(server_ip, &modbus_client.server_ip);
modbus_client.server_port = server_port;
modbus_client.transaction_id = 1;
modbus_client.connected = false;
// 创建TCP PCB
modbus_client.pcb = tcp_new();
if(modbus_client.pcb == NULL)
{
printf("Failed to create Modbus TCP client PCB\r\n");
return;
}
// 设置回调参数
tcp_arg(modbus_client.pcb, &modbus_client);
// 连接到服务器
err_t err = tcp_connect(modbus_client.pcb, &modbus_client.server_ip,
modbus_client.server_port, modbus_client_connected);
if(err != ERR_OK)
{
printf("Failed to initiate connection: %d\r\n", err);
tcp_close(modbus_client.pcb);
}
}
2.4 客户端API函数¶
// 读取保持寄存器
err_t modbus_read_holding_registers_client(uint16_t start_addr,
uint16_t quantity,
uint16_t *reg_values)
{
if(!modbus_client.connected)
{
return ERR_CONN;
}
uint8_t request[12];
uint16_t len = modbus_build_read_holding_request(request,
modbus_client.transaction_id++,
1, // unit_id
start_addr,
quantity);
// 发送请求
err_t err = tcp_write(modbus_client.pcb, request, len, TCP_WRITE_FLAG_COPY);
if(err != ERR_OK)
{
return err;
}
tcp_output(modbus_client.pcb);
// 等待响应(简化版,实际应使用回调或信号量)
uint32_t timeout = HAL_GetTick() + 1000;
while(modbus_client.rx_len == 0 && HAL_GetTick() < timeout)
{
// 等待接收
}
if(modbus_client.rx_len == 0)
{
return ERR_TIMEOUT;
}
// 解析响应
uint8_t *data = modbus_client.rx_buffer;
uint8_t function_code = data[7];
if(function_code == 0x03)
{
uint8_t byte_count = data[8];
for(uint16_t i = 0; i < quantity; i++)
{
reg_values[i] = (data[9 + i*2] << 8) | data[10 + i*2];
}
modbus_client.rx_len = 0;
return ERR_OK;
}
else if(function_code == 0x83)
{
// 异常响应
uint8_t exception_code = data[8];
printf("Modbus exception: %d\r\n", exception_code);
modbus_client.rx_len = 0;
return ERR_VAL;
}
return ERR_VAL;
}
// 写单个寄存器
err_t modbus_write_single_register_client(uint16_t reg_addr, uint16_t reg_value)
{
if(!modbus_client.connected)
{
return ERR_CONN;
}
uint8_t request[12];
uint16_t len = modbus_build_write_single_register_request(request,
modbus_client.transaction_id++,
1,
reg_addr,
reg_value);
err_t err = tcp_write(modbus_client.pcb, request, len, TCP_WRITE_FLAG_COPY);
if(err != ERR_OK)
{
return err;
}
tcp_output(modbus_client.pcb);
return ERR_OK;
}
步骤3:ESP32 Modbus TCP实现¶
3.1 Arduino + ModbusTCP库¶
安装库¶
服务器示例¶
#include <WiFi.h>
#include <ModbusTCP.h>
const char* ssid = "你的WiFi名称";
const char* password = "你的WiFi密码";
// Modbus TCP服务器
ModbusTCP mb;
// 寄存器定义
#define HREG_START 0
#define HREG_COUNT 100
#define IREG_START 0
#define IREG_COUNT 50
#define COIL_START 0
#define COIL_COUNT 64
void setup() {
Serial.begin(115200);
// 连接WiFi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi连接成功");
Serial.print("IP地址: ");
Serial.println(WiFi.localIP());
// 启动Modbus TCP服务器
mb.server();
// 添加保持寄存器
for(int i = 0; i < HREG_COUNT; i++) {
mb.addHreg(HREG_START + i, i * 10);
}
// 添加输入寄存器
for(int i = 0; i < IREG_COUNT; i++) {
mb.addIreg(IREG_START + i, 0);
}
// 添加线圈
for(int i = 0; i < COIL_COUNT; i++) {
mb.addCoil(COIL_START + i, false);
}
Serial.println("Modbus TCP服务器已启动");
}
void loop() {
// 处理Modbus请求
mb.task();
// 更新传感器数据到输入寄存器
static unsigned long lastUpdate = 0;
if(millis() - lastUpdate > 1000) {
lastUpdate = millis();
// 模拟传感器数据
float temperature = 20.0 + random(0, 100) / 10.0;
float humidity = 50.0 + random(0, 300) / 10.0;
uint16_t pressure = 1000 + random(0, 100);
mb.Ireg(IREG_START + 0, (uint16_t)(temperature * 10));
mb.Ireg(IREG_START + 1, (uint16_t)(humidity * 10));
mb.Ireg(IREG_START + 2, pressure);
mb.Ireg(IREG_START + 3, millis() / 1000); // 运行时间
}
// 根据线圈状态控制LED
if(mb.Coil(COIL_START)) {
digitalWrite(LED_BUILTIN, HIGH);
} else {
digitalWrite(LED_BUILTIN, LOW);
}
delay(10);
}
客户端示例¶
#include <WiFi.h>
#include <ModbusTCP.h>
const char* ssid = "你的WiFi名称";
const char* password = "你的WiFi密码";
// Modbus TCP客户端
ModbusTCP mb;
// 服务器IP地址
IPAddress server(192, 168, 1, 100);
void setup() {
Serial.begin(115200);
// 连接WiFi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi连接成功");
// 初始化Modbus TCP客户端
mb.client();
Serial.println("Modbus TCP客户端已启动");
}
void loop() {
// 连接到服务器
if(!mb.isConnected(server)) {
mb.connect(server);
Serial.println("连接到Modbus TCP服务器...");
delay(1000);
return;
}
// 读取保持寄存器
if(mb.readHreg(server, 0, 10)) {
Serial.println("保持寄存器:");
for(int i = 0; i < 10; i++) {
Serial.printf(" [%d] = %d\n", i, mb.Hreg(i));
}
}
delay(1000);
// 读取输入寄存器
if(mb.readIreg(server, 0, 4)) {
float temp = mb.Ireg(0) / 10.0;
float humi = mb.Ireg(1) / 10.0;
uint16_t pressure = mb.Ireg(2);
uint16_t uptime = mb.Ireg(3);
Serial.printf("温度: %.1f°C, 湿度: %.1f%%, 气压: %d hPa, 运行时间: %ds\n",
temp, humi, pressure, uptime);
}
delay(1000);
// 写入寄存器
static uint16_t counter = 0;
mb.writeHreg(server, 0, counter++);
delay(1000);
// 控制线圈
static bool ledState = false;
ledState = !ledState;
mb.writeCoil(server, 0, ledState);
Serial.printf("LED状态: %s\n", ledState ? "ON" : "OFF");
delay(2000);
}
步骤4:Python测试与调试¶
4.1 使用pymodbus测试¶
安装pymodbus¶
读取寄存器¶
from pymodbus.client import ModbusTcpClient
import time
# 连接到Modbus TCP服务器
client = ModbusTcpClient('192.168.1.100', port=502)
if client.connect():
print("连接成功")
# 读取保持寄存器
result = client.read_holding_registers(address=0, count=10, slave=1)
if not result.isError():
print("保持寄存器:")
for i, value in enumerate(result.registers):
print(f" 寄存器{i}: {value}")
else:
print(f"读取失败: {result}")
# 读取输入寄存器
result = client.read_input_registers(address=0, count=4, slave=1)
if not result.isError():
temp = result.registers[0] / 10.0
humi = result.registers[1] / 10.0
pressure = result.registers[2]
uptime = result.registers[3]
print(f"\n传感器数据:")
print(f" 温度: {temp}°C")
print(f" 湿度: {humi}%")
print(f" 气压: {pressure} hPa")
print(f" 运行时间: {uptime}秒")
# 读取线圈
result = client.read_coils(address=0, count=8, slave=1)
if not result.isError():
print(f"\n线圈状态: {result.bits[:8]}")
client.close()
else:
print("连接失败")
写入寄存器¶
from pymodbus.client import ModbusTcpClient
client = ModbusTcpClient('192.168.1.100', port=502)
if client.connect():
# 写单个保持寄存器
result = client.write_register(address=0, value=1234, slave=1)
if not result.isError():
print("写入成功")
# 写多个保持寄存器
values = [100, 200, 300, 400, 500]
result = client.write_registers(address=0, values=values, slave=1)
if not result.isError():
print(f"写入{len(values)}个寄存器成功")
# 写单个线圈
result = client.write_coil(address=0, value=True, slave=1)
if not result.isError():
print("线圈设置为ON")
# 写多个线圈
coil_values = [True, False, True, False, True, False, True, False]
result = client.write_coils(address=0, values=coil_values, slave=1)
if not result.isError():
print(f"写入{len(coil_values)}个线圈成功")
client.close()
连续监控¶
from pymodbus.client import ModbusTcpClient
import time
def monitor_modbus_device(ip, port=502, interval=1):
"""连续监控Modbus设备"""
client = ModbusTcpClient(ip, port=port)
if not client.connect():
print("连接失败")
return
print(f"开始监控 {ip}:{port}")
print("按Ctrl+C停止\n")
try:
while True:
# 读取输入寄存器
result = client.read_input_registers(address=0, count=4, slave=1)
if not result.isError():
temp = result.registers[0] / 10.0
humi = result.registers[1] / 10.0
pressure = result.registers[2]
uptime = result.registers[3]
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
print(f"[{timestamp}] 温度:{temp:5.1f}°C 湿度:{humi:5.1f}% "
f"气压:{pressure:4d}hPa 运行:{uptime:6d}s")
else:
print(f"读取错误: {result}")
time.sleep(interval)
except KeyboardInterrupt:
print("\n监控停止")
finally:
client.close()
# 使用示例
monitor_modbus_device('192.168.1.100', interval=2)
4.2 使用Modbus Poll测试¶
配置Modbus Poll¶
- 启动Modbus Poll
- 创建新连接:
- Connection -> Connect
- Connection: Modbus TCP/IP
- IP Address: 192.168.1.100
- Port: 502
-
Slave ID: 1
-
配置读取定义:
- Setup -> Read/Write Definition
- Function: 03 (Read Holding Registers)
- Address: 0
- Quantity: 10
-
Scan Rate: 1000ms
-
开始轮询:
- 点击"Poll"按钮
- 观察寄存器值的变化
测试写入功能¶
- **双击寄存器值**进行修改
- 观察设备响应
- 检查写入是否成功
4.3 使用Wireshark抓包分析¶
抓包过滤¶
# 只显示Modbus TCP流量
tcp.port == 502
# 显示特定IP的Modbus流量
ip.addr == 192.168.1.100 && tcp.port == 502
# 显示Modbus请求
modbus.func_code == 3
# 显示Modbus异常
modbus.except_code
分析Modbus TCP帧¶
- 查看MBAP报头:
- Transaction ID
- Protocol ID
- Length
-
Unit ID
-
查看PDU:
- Function Code
-
Data
-
检查异常:
- 异常功能码(原功能码 + 0x80)
- 异常码
导出统计信息¶
Statistics -> Conversations -> TCP
- 查看Modbus TCP连接统计
- 吞吐量、延迟、丢包率
Statistics -> Protocol Hierarchy
- 查看Modbus协议占比
步骤5:故障排除与调试¶
5.1 常见问题诊断¶
问题1:无法连接到服务器¶
症状: - 客户端连接超时 - 无法建立TCP连接
诊断步骤:
// 1. 检查网络连通性
void check_network_connectivity(const char *ip)
{
// Ping测试
printf("Ping %s...\r\n", ip);
// 使用lwIP的ping功能或系统命令
// ping_send_now(&target_ip);
}
// 2. 检查端口是否开放
void check_port_status(void)
{
// 使用netstat或类似工具
// 确认端口502是否监听
printf("Checking port 502...\r\n");
}
// 3. 检查防火墙设置
// 确保防火墙允许端口502通信
可能原因: - IP地址配置错误 - 服务器未启动 - 防火墙阻止连接 - 网络不通
问题2:请求超时¶
症状: - 发送请求后无响应 - 超时错误
诊断代码:
// 添加超时处理
err_t modbus_read_with_timeout(uint16_t start_addr, uint16_t quantity,
uint16_t *reg_values, uint32_t timeout_ms)
{
uint32_t start_time = HAL_GetTick();
// 发送请求
err_t err = modbus_send_request(start_addr, quantity);
if(err != ERR_OK)
{
return err;
}
// 等待响应
while(modbus_client.rx_len == 0)
{
if(HAL_GetTick() - start_time > timeout_ms)
{
printf("Modbus request timeout\r\n");
return ERR_TIMEOUT;
}
// 处理其他任务
osDelay(10);
}
// 解析响应
return modbus_parse_response(reg_values, quantity);
}
可能原因: - 服务器响应慢 - 网络延迟高 - 服务器处理请求失败 - 请求参数错误
问题3:数据错误¶
症状: - 读取的数据不正确 - 字节序错误
诊断代码:
// 验证字节序
void verify_byte_order(void)
{
uint16_t test_value = 0x1234;
uint8_t *bytes = (uint8_t *)&test_value;
printf("System byte order: ");
if(bytes[0] == 0x34)
{
printf("Little Endian\r\n");
}
else
{
printf("Big Endian\r\n");
}
// Modbus使用大端字节序
printf("Modbus requires Big Endian\r\n");
}
// 数据验证
bool validate_modbus_response(uint8_t *data, uint16_t len)
{
if(len < 8)
{
printf("Response too short\r\n");
return false;
}
// 检查协议ID
uint16_t protocol_id = (data[2] << 8) | data[3];
if(protocol_id != 0)
{
printf("Invalid protocol ID: 0x%04X\r\n", protocol_id);
return false;
}
// 检查长度字段
uint16_t length = (data[4] << 8) | data[5];
if(length + 6 != len)
{
printf("Length mismatch: expected %d, got %d\r\n", length + 6, len);
return false;
}
return true;
}
问题4:异常响应¶
症状: - 收到异常码 - 操作失败
异常码处理:
void handle_modbus_exception(uint8_t exception_code)
{
printf("Modbus Exception: ");
switch(exception_code)
{
case 0x01:
printf("Illegal Function (0x01)\r\n");
printf(" - 功能码不支持\r\n");
break;
case 0x02:
printf("Illegal Data Address (0x02)\r\n");
printf(" - 地址超出范围\r\n");
break;
case 0x03:
printf("Illegal Data Value (0x03)\r\n");
printf(" - 数据值非法\r\n");
break;
case 0x04:
printf("Slave Device Failure (0x04)\r\n");
printf(" - 设备故障\r\n");
break;
case 0x05:
printf("Acknowledge (0x05)\r\n");
printf(" - 长时间操作,稍后重试\r\n");
break;
case 0x06:
printf("Slave Device Busy (0x06)\r\n");
printf(" - 设备忙,稍后重试\r\n");
break;
default:
printf("Unknown Exception (0x%02X)\r\n", exception_code);
break;
}
}
5.2 调试工具¶
串口调试输出¶
// 启用详细调试信息
#define MODBUS_DEBUG 1
#if MODBUS_DEBUG
#define MODBUS_LOG(fmt, ...) printf("[MODBUS] " fmt "\r\n", ##__VA_ARGS__)
#else
#define MODBUS_LOG(fmt, ...)
#endif
// 使用示例
void modbus_send_request(uint8_t *data, uint16_t len)
{
MODBUS_LOG("Sending request, length: %d", len);
// 打印十六进制数据
printf("Data: ");
for(uint16_t i = 0; i < len; i++)
{
printf("%02X ", data[i]);
}
printf("\r\n");
// 发送数据
tcp_write(pcb, data, len, TCP_WRITE_FLAG_COPY);
}
数据包记录¶
// 记录所有Modbus通信
typedef struct {
uint32_t timestamp;
bool is_request;
uint8_t data[260];
uint16_t length;
} modbus_log_entry_t;
#define LOG_SIZE 100
static modbus_log_entry_t modbus_log[LOG_SIZE];
static uint16_t log_index = 0;
void modbus_log_packet(bool is_request, uint8_t *data, uint16_t len)
{
modbus_log_entry_t *entry = &modbus_log[log_index];
entry->timestamp = HAL_GetTick();
entry->is_request = is_request;
entry->length = len;
memcpy(entry->data, data, len);
log_index = (log_index + 1) % LOG_SIZE;
}
// 打印日志
void modbus_print_log(void)
{
printf("=== Modbus Communication Log ===\r\n");
for(uint16_t i = 0; i < LOG_SIZE; i++)
{
modbus_log_entry_t *entry = &modbus_log[i];
if(entry->length == 0)
continue;
printf("[%lu] %s: ", entry->timestamp,
entry->is_request ? "REQ" : "RSP");
for(uint16_t j = 0; j < entry->length; j++)
{
printf("%02X ", entry->data[j]);
}
printf("\r\n");
}
}
步骤6:性能优化¶
6.1 连接管理¶
连接池¶
#define MAX_CONNECTIONS 5
typedef struct {
struct tcp_pcb *pcb;
bool in_use;
uint32_t last_activity;
uint8_t client_id;
} modbus_connection_t;
static modbus_connection_t connections[MAX_CONNECTIONS];
// 获取空闲连接
modbus_connection_t* get_free_connection(void)
{
for(int i = 0; i < MAX_CONNECTIONS; i++)
{
if(!connections[i].in_use)
{
connections[i].in_use = true;
connections[i].last_activity = HAL_GetTick();
return &connections[i];
}
}
return NULL; // 无空闲连接
}
// 超时清理
void cleanup_idle_connections(void)
{
uint32_t now = HAL_GetTick();
const uint32_t timeout = 60000; // 60秒超时
for(int i = 0; i < MAX_CONNECTIONS; i++)
{
if(connections[i].in_use)
{
if(now - connections[i].last_activity > timeout)
{
printf("Closing idle connection %d\r\n", i);
tcp_close(connections[i].pcb);
connections[i].in_use = false;
}
}
}
}
批量读写¶
// 批量读取多个寄存器区域
err_t modbus_batch_read(uint16_t *addresses, uint16_t *quantities,
uint16_t count, uint16_t **results)
{
for(uint16_t i = 0; i < count; i++)
{
err_t err = modbus_read_holding_registers_client(addresses[i],
quantities[i],
results[i]);
if(err != ERR_OK)
{
return err;
}
// 短暂延迟避免服务器过载
osDelay(10);
}
return ERR_OK;
}
// 批量写入
err_t modbus_batch_write(uint16_t *addresses, uint16_t *values, uint16_t count)
{
for(uint16_t i = 0; i < count; i++)
{
err_t err = modbus_write_single_register_client(addresses[i], values[i]);
if(err != ERR_OK)
{
return err;
}
osDelay(10);
}
return ERR_OK;
}
6.2 数据缓存¶
// 寄存器缓存
typedef struct {
uint16_t address;
uint16_t value;
uint32_t timestamp;
bool valid;
} register_cache_t;
#define CACHE_SIZE 100
static register_cache_t register_cache[CACHE_SIZE];
// 从缓存读取
bool read_from_cache(uint16_t address, uint16_t *value, uint32_t max_age_ms)
{
for(int i = 0; i < CACHE_SIZE; i++)
{
if(register_cache[i].valid &&
register_cache[i].address == address)
{
uint32_t age = HAL_GetTick() - register_cache[i].timestamp;
if(age < max_age_ms)
{
*value = register_cache[i].value;
return true;
}
}
}
return false;
}
// 更新缓存
void update_cache(uint16_t address, uint16_t value)
{
// 查找现有条目
for(int i = 0; i < CACHE_SIZE; i++)
{
if(register_cache[i].address == address)
{
register_cache[i].value = value;
register_cache[i].timestamp = HAL_GetTick();
register_cache[i].valid = true;
return;
}
}
// 添加新条目(简化版,实际应使用LRU算法)
static int next_index = 0;
register_cache[next_index].address = address;
register_cache[next_index].value = value;
register_cache[next_index].timestamp = HAL_GetTick();
register_cache[next_index].valid = true;
next_index = (next_index + 1) % CACHE_SIZE;
}
// 带缓存的读取
err_t modbus_read_with_cache(uint16_t address, uint16_t *value, uint32_t cache_time_ms)
{
// 先尝试从缓存读取
if(read_from_cache(address, value, cache_time_ms))
{
return ERR_OK;
}
// 缓存未命中,从设备读取
err_t err = modbus_read_holding_registers_client(address, 1, value);
if(err == ERR_OK)
{
// 更新缓存
update_cache(address, *value);
}
return err;
}
6.3 异步处理¶
// 使用RTOS任务异步处理Modbus请求
typedef struct {
uint8_t function_code;
uint16_t address;
uint16_t quantity;
uint16_t *data;
void (*callback)(err_t result, uint16_t *data);
} modbus_request_t;
QueueHandle_t modbus_request_queue;
// Modbus客户端任务
void modbus_client_task(void *arg)
{
modbus_request_t request;
while(1)
{
// 从队列获取请求
if(xQueueReceive(modbus_request_queue, &request, portMAX_DELAY) == pdTRUE)
{
err_t result = ERR_OK;
switch(request.function_code)
{
case 0x03: // 读保持寄存器
result = modbus_read_holding_registers_client(request.address,
request.quantity,
request.data);
break;
case 0x06: // 写单个寄存器
result = modbus_write_single_register_client(request.address,
request.data[0]);
break;
default:
result = ERR_VAL;
break;
}
// 调用回调函数
if(request.callback != NULL)
{
request.callback(result, request.data);
}
}
}
}
// 异步读取
void modbus_read_async(uint16_t address, uint16_t quantity,
void (*callback)(err_t, uint16_t*))
{
modbus_request_t request;
request.function_code = 0x03;
request.address = address;
request.quantity = quantity;
request.data = malloc(quantity * sizeof(uint16_t));
request.callback = callback;
xQueueSend(modbus_request_queue, &request, portMAX_DELAY);
}
// 回调函数示例
void read_complete_callback(err_t result, uint16_t *data)
{
if(result == ERR_OK)
{
printf("Read complete: %d\r\n", data[0]);
}
else
{
printf("Read failed: %d\r\n", result);
}
free(data);
}
步骤7:安全考虑¶
7.1 访问控制¶
IP白名单¶
// IP白名单
typedef struct {
ip4_addr_t ip;
bool allowed;
} ip_whitelist_entry_t;
#define WHITELIST_SIZE 10
static ip_whitelist_entry_t ip_whitelist[WHITELIST_SIZE];
// 初始化白名单
void init_ip_whitelist(void)
{
// 添加允许的IP地址
IP4_ADDR(&ip_whitelist[0].ip, 192, 168, 1, 10);
ip_whitelist[0].allowed = true;
IP4_ADDR(&ip_whitelist[1].ip, 192, 168, 1, 20);
ip_whitelist[1].allowed = true;
}
// 检查IP是否允许
bool is_ip_allowed(ip4_addr_t *ip)
{
for(int i = 0; i < WHITELIST_SIZE; i++)
{
if(ip_whitelist[i].allowed &&
ip4_addr_cmp(ip, &ip_whitelist[i].ip))
{
return true;
}
}
return false;
}
// 在接受连接时检查
err_t modbus_tcp_accept_with_acl(void *arg, struct tcp_pcb *newpcb, err_t err)
{
if(!is_ip_allowed(&newpcb->remote_ip))
{
printf("Connection from %s denied (not in whitelist)\r\n",
ip4addr_ntoa(&newpcb->remote_ip));
tcp_abort(newpcb);
return ERR_ABRT;
}
printf("Connection from %s accepted\r\n",
ip4addr_ntoa(&newpcb->remote_ip));
return modbus_tcp_accept(arg, newpcb, err);
}
寄存器访问权限¶
// 寄存器访问权限
typedef enum {
ACCESS_NONE = 0,
ACCESS_READ = 1,
ACCESS_WRITE = 2,
ACCESS_READ_WRITE = 3
} access_permission_t;
typedef struct {
uint16_t start_addr;
uint16_t end_addr;
access_permission_t permission;
} register_permission_t;
static register_permission_t register_permissions[] = {
{0, 9, ACCESS_READ_WRITE}, // 寄存器0-9:读写
{10, 49, ACCESS_READ}, // 寄存器10-49:只读
{50, 99, ACCESS_NONE}, // 寄存器50-99:禁止访问
};
// 检查访问权限
bool check_register_permission(uint16_t address, bool is_write)
{
for(int i = 0; i < sizeof(register_permissions)/sizeof(register_permissions[0]); i++)
{
if(address >= register_permissions[i].start_addr &&
address <= register_permissions[i].end_addr)
{
access_permission_t perm = register_permissions[i].permission;
if(is_write)
{
return (perm == ACCESS_WRITE || perm == ACCESS_READ_WRITE);
}
else
{
return (perm == ACCESS_READ || perm == ACCESS_READ_WRITE);
}
}
}
return false; // 默认拒绝
}
// 在处理请求时检查权限
uint16_t modbus_read_holding_registers_with_permission(uint16_t start_addr,
uint16_t quantity,
uint8_t *response_data)
{
// 检查每个寄存器的读权限
for(uint16_t i = 0; i < quantity; i++)
{
if(!check_register_permission(start_addr + i, false))
{
return 0x02; // 异常码:非法数据地址
}
}
// 权限检查通过,执行读取
return modbus_read_holding_registers(start_addr, quantity, response_data);
}
7.2 防止DoS攻击¶
连接限制¶
#define MAX_CONNECTIONS_PER_IP 3
#define MAX_REQUESTS_PER_SECOND 10
typedef struct {
ip4_addr_t ip;
uint8_t connection_count;
uint32_t request_count;
uint32_t last_reset_time;
} client_stats_t;
#define MAX_CLIENTS 20
static client_stats_t client_stats[MAX_CLIENTS];
// 检查连接限制
bool check_connection_limit(ip4_addr_t *ip)
{
// 查找或创建客户端统计
client_stats_t *stats = NULL;
for(int i = 0; i < MAX_CLIENTS; i++)
{
if(ip4_addr_cmp(&client_stats[i].ip, ip))
{
stats = &client_stats[i];
break;
}
}
if(stats == NULL)
{
// 新客户端,创建统计
for(int i = 0; i < MAX_CLIENTS; i++)
{
if(client_stats[i].connection_count == 0)
{
stats = &client_stats[i];
ip4_addr_copy(stats->ip, *ip);
break;
}
}
}
if(stats == NULL)
{
return false; // 客户端表已满
}
// 检查连接数限制
if(stats->connection_count >= MAX_CONNECTIONS_PER_IP)
{
printf("Connection limit exceeded for %s\r\n", ip4addr_ntoa(ip));
return false;
}
stats->connection_count++;
return true;
}
// 检查请求速率限制
bool check_request_rate_limit(ip4_addr_t *ip)
{
client_stats_t *stats = NULL;
for(int i = 0; i < MAX_CLIENTS; i++)
{
if(ip4_addr_cmp(&client_stats[i].ip, ip))
{
stats = &client_stats[i];
break;
}
}
if(stats == NULL)
{
return true; // 新客户端,允许
}
uint32_t now = HAL_GetTick();
// 每秒重置计数器
if(now - stats->last_reset_time > 1000)
{
stats->request_count = 0;
stats->last_reset_time = now;
}
stats->request_count++;
if(stats->request_count > MAX_REQUESTS_PER_SECOND)
{
printf("Request rate limit exceeded for %s\r\n", ip4addr_ntoa(ip));
return false;
}
return true;
}
7.3 数据加密(Modbus Security)¶
虽然标准Modbus TCP不支持加密,但可以通过以下方式增强安全性:
使用TLS/SSL¶
// 使用mbedTLS实现Modbus over TLS
#include "mbedtls/ssl.h"
#include "mbedtls/net_sockets.h"
typedef struct {
mbedtls_ssl_context ssl;
mbedtls_ssl_config conf;
mbedtls_net_context server_fd;
} modbus_tls_context_t;
// 初始化TLS
int modbus_tls_init(modbus_tls_context_t *ctx, const char *server_ip, uint16_t port)
{
mbedtls_ssl_init(&ctx->ssl);
mbedtls_ssl_config_init(&ctx->conf);
mbedtls_net_init(&ctx->server_fd);
// 连接到服务器
char port_str[6];
snprintf(port_str, sizeof(port_str), "%d", port);
int ret = mbedtls_net_connect(&ctx->server_fd, server_ip, port_str, MBEDTLS_NET_PROTO_TCP);
if(ret != 0)
{
return ret;
}
// 配置SSL
mbedtls_ssl_config_defaults(&ctx->conf,
MBEDTLS_SSL_IS_CLIENT,
MBEDTLS_SSL_TRANSPORT_STREAM,
MBEDTLS_SSL_PRESET_DEFAULT);
mbedtls_ssl_setup(&ctx->ssl, &ctx->conf);
mbedtls_ssl_set_bio(&ctx->ssl, &ctx->server_fd,
mbedtls_net_send, mbedtls_net_recv, NULL);
// 执行SSL握手
while((ret = mbedtls_ssl_handshake(&ctx->ssl)) != 0)
{
if(ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE)
{
return ret;
}
}
return 0;
}
// 通过TLS发送Modbus请求
int modbus_tls_send(modbus_tls_context_t *ctx, uint8_t *data, uint16_t len)
{
return mbedtls_ssl_write(&ctx->ssl, data, len);
}
// 通过TLS接收Modbus响应
int modbus_tls_recv(modbus_tls_context_t *ctx, uint8_t *buffer, uint16_t max_len)
{
return mbedtls_ssl_read(&ctx->ssl, buffer, max_len);
}
步骤8:实战应用¶
8.1 工业数据采集系统¶
系统架构¶
数据采集代码¶
// 数据采集任务
void data_acquisition_task(void *arg)
{
const uint16_t sensor_addresses[] = {0, 1, 2, 3, 4}; // 传感器寄存器地址
const uint16_t sensor_count = 5;
uint16_t sensor_values[sensor_count];
while(1)
{
// 读取所有传感器数据
err_t err = modbus_read_holding_registers_client(sensor_addresses[0],
sensor_count,
sensor_values);
if(err == ERR_OK)
{
// 解析传感器数据
float temperature = sensor_values[0] / 10.0;
float humidity = sensor_values[1] / 10.0;
uint16_t pressure = sensor_values[2];
uint16_t flow_rate = sensor_values[3];
uint16_t level = sensor_values[4];
// 打印数据
printf("Sensor Data:\r\n");
printf(" Temperature: %.1f°C\r\n", temperature);
printf(" Humidity: %.1f%%\r\n", humidity);
printf(" Pressure: %d kPa\r\n", pressure);
printf(" Flow Rate: %d L/min\r\n", flow_rate);
printf(" Level: %d mm\r\n", level);
// 存储到数据库或发送到云平台
store_sensor_data(temperature, humidity, pressure, flow_rate, level);
// 检查报警条件
check_alarms(temperature, humidity, pressure, flow_rate, level);
}
else
{
printf("Failed to read sensor data: %d\r\n", err);
}
// 每5秒采集一次
osDelay(5000);
}
}
// 报警检查
void check_alarms(float temp, float humi, uint16_t pressure,
uint16_t flow, uint16_t level)
{
// 温度报警
if(temp > 80.0)
{
printf("ALARM: High temperature! %.1f°C\r\n", temp);
trigger_alarm(ALARM_HIGH_TEMP);
}
// 压力报警
if(pressure > 150)
{
printf("ALARM: High pressure! %d kPa\r\n", pressure);
trigger_alarm(ALARM_HIGH_PRESSURE);
}
// 液位报警
if(level < 100)
{
printf("ALARM: Low level! %d mm\r\n", level);
trigger_alarm(ALARM_LOW_LEVEL);
}
}
8.2 远程设备控制¶
控制命令处理¶
// 控制命令结构
typedef struct {
uint8_t device_id;
uint8_t command;
uint16_t parameter;
} control_command_t;
// 命令定义
#define CMD_START_MOTOR 0x01
#define CMD_STOP_MOTOR 0x02
#define CMD_SET_SPEED 0x03
#define CMD_SET_POSITION 0x04
#define CMD_RESET 0x05
// 执行控制命令
err_t execute_control_command(control_command_t *cmd)
{
err_t err = ERR_OK;
switch(cmd->command)
{
case CMD_START_MOTOR:
// 写入启动命令到线圈
err = modbus_write_single_coil_client(0, true);
printf("Starting motor on device %d\r\n", cmd->device_id);
break;
case CMD_STOP_MOTOR:
// 写入停止命令到线圈
err = modbus_write_single_coil_client(0, false);
printf("Stopping motor on device %d\r\n", cmd->device_id);
break;
case CMD_SET_SPEED:
// 写入速度设定值到寄存器
err = modbus_write_single_register_client(10, cmd->parameter);
printf("Setting speed to %d on device %d\r\n",
cmd->parameter, cmd->device_id);
break;
case CMD_SET_POSITION:
// 写入位置设定值到寄存器
err = modbus_write_single_register_client(20, cmd->parameter);
printf("Setting position to %d on device %d\r\n",
cmd->parameter, cmd->device_id);
break;
case CMD_RESET:
// 写入复位命令
err = modbus_write_single_register_client(100, 0xFFFF);
printf("Resetting device %d\r\n", cmd->device_id);
break;
default:
printf("Unknown command: 0x%02X\r\n", cmd->command);
err = ERR_VAL;
break;
}
return err;
}
// 控制任务
void device_control_task(void *arg)
{
QueueHandle_t cmd_queue = (QueueHandle_t)arg;
control_command_t cmd;
while(1)
{
// 从队列接收控制命令
if(xQueueReceive(cmd_queue, &cmd, portMAX_DELAY) == pdTRUE)
{
err_t err = execute_control_command(&cmd);
if(err == ERR_OK)
{
printf("Command executed successfully\r\n");
}
else
{
printf("Command execution failed: %d\r\n", err);
}
}
}
}
8.3 Modbus TCP网关¶
RTU到TCP转换¶
// Modbus RTU到TCP网关
typedef struct {
// RTU侧
UART_HandleTypeDef *uart;
uint8_t rtu_buffer[256];
uint16_t rtu_len;
// TCP侧
struct tcp_pcb *tcp_pcb;
uint8_t tcp_buffer[260];
uint16_t tcp_len;
} modbus_gateway_t;
static modbus_gateway_t gateway;
// RTU帧转TCP帧
void rtu_to_tcp(uint8_t *rtu_frame, uint16_t rtu_len,
uint8_t *tcp_frame, uint16_t *tcp_len)
{
// RTU帧格式:[从站地址][功能码][数据][CRC16]
// TCP帧格式:[MBAP报头][功能码][数据]
// 提取RTU数据(去除CRC)
uint8_t slave_addr = rtu_frame[0];
uint8_t function_code = rtu_frame[1];
uint16_t data_len = rtu_len - 3; // 减去地址、功能码和CRC
// 构建MBAP报头
static uint16_t transaction_id = 1;
tcp_frame[0] = (transaction_id >> 8) & 0xFF;
tcp_frame[1] = transaction_id & 0xFF;
tcp_frame[2] = 0x00; // 协议ID
tcp_frame[3] = 0x00;
uint16_t length = data_len + 2; // 单元ID + 功能码 + 数据
tcp_frame[4] = (length >> 8) & 0xFF;
tcp_frame[5] = length & 0xFF;
tcp_frame[6] = slave_addr; // 单元ID
// 复制功能码和数据
tcp_frame[7] = function_code;
memcpy(&tcp_frame[8], &rtu_frame[2], data_len);
*tcp_len = 8 + data_len;
transaction_id++;
}
// TCP帧转RTU帧
void tcp_to_rtu(uint8_t *tcp_frame, uint16_t tcp_len,
uint8_t *rtu_frame, uint16_t *rtu_len)
{
// 提取TCP数据
uint8_t unit_id = tcp_frame[6];
uint8_t function_code = tcp_frame[7];
uint16_t data_len = tcp_len - 8;
// 构建RTU帧
rtu_frame[0] = unit_id;
rtu_frame[1] = function_code;
memcpy(&rtu_frame[2], &tcp_frame[8], data_len);
// 计算CRC
uint16_t crc = modbus_crc16(rtu_frame, data_len + 2);
rtu_frame[data_len + 2] = crc & 0xFF;
rtu_frame[data_len + 3] = (crc >> 8) & 0xFF;
*rtu_len = data_len + 4;
}
// CRC16计算
uint16_t modbus_crc16(uint8_t *data, uint16_t len)
{
uint16_t crc = 0xFFFF;
for(uint16_t i = 0; i < len; i++)
{
crc ^= data[i];
for(uint8_t j = 0; j < 8; j++)
{
if(crc & 0x0001)
{
crc = (crc >> 1) ^ 0xA001;
}
else
{
crc >>= 1;
}
}
}
return crc;
}
最佳实践¶
9.1 代码组织¶
模块化设计¶
project/
├── modbus/
│ ├── modbus_tcp.c/h # Modbus TCP核心
│ ├── modbus_client.c/h # 客户端实现
│ ├── modbus_server.c/h # 服务器实现
│ └── modbus_config.h # 配置文件
├── network/
│ ├── lwip_init.c/h # lwIP初始化
│ └── ethernetif.c/h # 以太网驱动
└── application/
├── data_acquisition.c/h # 数据采集
├── device_control.c/h # 设备控制
└── gateway.c/h # 网关功能
9.2 错误处理¶
统一错误处理机制¶
typedef enum {
MODBUS_OK = 0,
MODBUS_ERR_TIMEOUT,
MODBUS_ERR_CONN_FAILED,
MODBUS_ERR_INVALID_RESPONSE,
MODBUS_ERR_EXCEPTION,
MODBUS_ERR_NO_MEMORY
} modbus_error_t;
// 错误处理函数
void handle_modbus_error(modbus_error_t error, const char *context)
{
printf("[ERROR] %s: ", context);
switch(error)
{
case MODBUS_ERR_TIMEOUT:
printf("Timeout\r\n");
// 重试逻辑
break;
case MODBUS_ERR_CONN_FAILED:
printf("Connection failed\r\n");
// 重连逻辑
break;
case MODBUS_ERR_EXCEPTION:
printf("Modbus exception\r\n");
// 异常处理
break;
default:
printf("Unknown error %d\r\n", error);
break;
}
}
9.3 日志记录¶
// 日志级别
typedef enum {
LOG_DEBUG,
LOG_INFO,
LOG_WARNING,
LOG_ERROR
} log_level_t;
static log_level_t current_log_level = LOG_INFO;
// 日志函数
void modbus_log(log_level_t level, const char *format, ...)
{
if(level < current_log_level)
{
return;
}
const char *level_str[] = {"DEBUG", "INFO", "WARN", "ERROR"};
printf("[%s] ", level_str[level]);
va_list args;
va_start(args, format);
vprintf(format, args);
va_end(args);
printf("\r\n");
}
// 使用示例
modbus_log(LOG_INFO, "Modbus TCP server started on port %d", 502);
modbus_log(LOG_ERROR, "Failed to read register %d", address);
9.4 性能监控¶
// 性能统计
typedef struct {
uint32_t total_requests;
uint32_t successful_requests;
uint32_t failed_requests;
uint32_t timeout_count;
uint32_t exception_count;
uint32_t total_response_time_ms;
uint32_t min_response_time_ms;
uint32_t max_response_time_ms;
} modbus_stats_t;
static modbus_stats_t modbus_stats;
// 更新统计
void update_modbus_stats(bool success, uint32_t response_time_ms)
{
modbus_stats.total_requests++;
if(success)
{
modbus_stats.successful_requests++;
modbus_stats.total_response_time_ms += response_time_ms;
if(response_time_ms < modbus_stats.min_response_time_ms ||
modbus_stats.min_response_time_ms == 0)
{
modbus_stats.min_response_time_ms = response_time_ms;
}
if(response_time_ms > modbus_stats.max_response_time_ms)
{
modbus_stats.max_response_time_ms = response_time_ms;
}
}
else
{
modbus_stats.failed_requests++;
}
}
// 打印统计信息
void print_modbus_stats(void)
{
printf("=== Modbus Statistics ===\r\n");
printf("Total Requests: %u\r\n", modbus_stats.total_requests);
printf("Successful: %u\r\n", modbus_stats.successful_requests);
printf("Failed: %u\r\n", modbus_stats.failed_requests);
printf("Timeout: %u\r\n", modbus_stats.timeout_count);
printf("Exception: %u\r\n", modbus_stats.exception_count);
if(modbus_stats.successful_requests > 0)
{
uint32_t avg_time = modbus_stats.total_response_time_ms /
modbus_stats.successful_requests;
printf("Avg Response Time: %u ms\r\n", avg_time);
printf("Min Response Time: %u ms\r\n", modbus_stats.min_response_time_ms);
printf("Max Response Time: %u ms\r\n", modbus_stats.max_response_time_ms);
}
float success_rate = (float)modbus_stats.successful_requests /
modbus_stats.total_requests * 100.0;
printf("Success Rate: %.2f%%\r\n", success_rate);
}
总结¶
通过本教程,你学习了:
- ✅ Modbus TCP协议的架构和工作原理
- ✅ MBAP报头和功能码的使用方法
- ✅ STM32平台上的Modbus TCP服务器实现
- ✅ Modbus TCP客户端开发
- ✅ ESP32平台的Modbus TCP应用
- ✅ Python测试和调试技术
- ✅ 性能优化和安全考虑
- ✅ 工业数据采集和设备控制应用
- ✅ Modbus RTU到TCP网关实现
关键要点:
- 协议理解:Modbus TCP在Modbus RTU基础上增加了MBAP报头
- 字节序:Modbus使用大端字节序(网络字节序)
- 端口:标准端口为502
- 异常处理:正确处理异常响应和超时
- 性能优化:使用连接池、缓存、异步处理
- 安全考虑:访问控制、速率限制、数据加密
- 实战应用:数据采集、设备控制、协议转换
进阶挑战¶
尝试以下挑战来巩固学习:
- 挑战1:实现Modbus TCP主从站自动发现功能
- 挑战2:添加Modbus数据记录和历史查询功能
- 挑战3:实现Modbus TCP冗余连接(主备服务器)
- 挑战4:开发Web界面监控Modbus设备状态
- 挑战5:实现Modbus TCP到MQTT的桥接
完整代码¶
完整的项目代码可以在GitHub上找到:
项目包含: - STM32 Modbus TCP服务器完整代码 - Modbus TCP客户端实现 - ESP32 Arduino示例 - Python测试脚本 - Modbus RTU到TCP网关代码
下一步¶
建议继续学习:
- OPC UA协议 - 学习现代工业通信标准
- EtherCAT协议 - 学习实时工业以太网
- PROFINET协议 - 学习西门子工业以太网
- 工业物联网平台 - 学习云端集成
- 时间敏感网络(TSN) - 学习确定性以太网
参考资料¶
- Modbus官方文档
- Modbus Application Protocol V1.1b3
- Modbus Messaging Implementation Guide V1.0b
-
官网:https://modbus.org/
-
技术标准
- IEC 61158:工业通信网络标准
-
IEEE 802.3:以太网标准
-
开源库
- libmodbus:https://libmodbus.org/
- pymodbus:https://github.com/riptideio/pymodbus
-
ModbusTCP-ESP32:https://github.com/emelianov/modbus-esp8266
-
书籍推荐
- 《Modbus工业通信协议详解》
- 《工业以太网技术与应用》
-
《PLC与工业网络通信技术》
-
在线资源
- Modbus Organization:https://modbus.org/
- Industrial Ethernet Book:https://www.iebmedia.com/
- Control Engineering:https://www.controleng.com/
常见问题FAQ¶
Q1: Modbus TCP和Modbus RTU可以互相转换吗?
A: 可以,通过Modbus网关可以实现RTU和TCP之间的协议转换。主要区别是传输层(串口vs以太网)和帧格式(CRC vs MBAP报头)。
Q2: Modbus TCP的最大传输速度是多少?
A: 理论上受限于以太网速度(10/100/1000 Mbps),实际速度取决于: - 网络带宽 - 设备处理能力 - 请求/响应大小 - 网络延迟
Q3: 一个Modbus TCP服务器可以支持多少个客户端?
A: 标准没有限制,实际取决于: - 服务器硬件性能 - 内存大小 - TCP连接数限制 - 应用需求
通常嵌入式设备支持5-20个并发连接。
Q4: Modbus TCP安全吗?
A: 标准Modbus TCP没有内置安全机制,建议: - 使用VPN或专用网络 - 实施访问控制(IP白名单) - 使用TLS/SSL加密 - 部署防火墙
Q5: 如何选择Modbus RTU还是Modbus TCP?
A: 选择依据: - Modbus RTU:现场总线、短距离、成本敏感 - Modbus TCP:工业以太网、远程访问、高速传输
本教程持续更新中,欢迎反馈和建议!