全部例程

Modbus TCP示例

W55MH32

更新于 2026年9月12日

本篇总结

本例程在 W55MH32 上实现 Modbus TCP 服务器模式,响应写单线圈控制 LED 和读线圈状态的功能码。

🔑 KEIL MDK、UART调试、硬件TOE引擎、Modbus TCP、DHCP

硬件平台:本例程基于 W55MH32L-EVB 开发板运行。

采购推荐:点击前往 WIZnet 官方淘宝店铺 选购 W55MH32L-EVB

本示例演示如何在 W55MH32 上通过 Modbus TCP 协议实现工业通信。开发板作为 Modbus TCP 服务器监听 502 端口,PC 上的 Modbus Poll 作为客户端发起读写请求,实现 LED 的远程控制与状态读取。

前置工具准备

开始本例程前,请确认以下工具已安装:

  • WIZ UartTool V1.0:串口调试工具:点击下载
  • Modbus Poll:Modbus 主机模拟工具,用于测试 Modbus TCP 通信:点击下载

Modbus TCP 协议简介

Modbus TCP 是一种基于以太网的通信协议,它是经典 Modbus 协议的扩展。它使用 TCP/IP 协议栈进行通信,运行在 OSI 模型的传输层(TCP层)之上,数据通过 TCP 端口 502 传输,采用主从架构。Modbus 协议最初由 Modicon 公司在 1979 年开发,广泛应用于工业自动化系统,用于实现不同设备之间的数据通信。Modbus TCP 结合了 Modbus 协议的简单性和以太网的高效性,是一种开放、标准化且广泛使用的工业通信协议。

Modbus TCP 报文由以下几个部分组成:

字段长度说明
事务处理标识符2字节用于标识请求与响应之间的配对。
协议标识符2字节通常为 0x0000,表示该报文使用 Modbus 协议。
长度字段2字节表示后续数据的字节数。
单元标识符1字节标识目标从机设备的地址。
功能码1字节定义当前操作的类型(如读写寄存器等)。
数据部分可变长度包含具体的操作数据。

本例程使用的功能码

本例程从机地址设为 0x01。功能码 0x05(写单线圈):数据为 0xff 时点亮 LED,为 0x00 时熄灭 LED;功能码 0x01(读线圈状态):回复当前 LED 状态。Modbus TCP 因其高效性和兼容性,已成为工业物联网(IIoT)和工业 4.0 中不可或缺的一部分。

硬件连接与拓扑

请注意:

测试本例程需要 PC 端和 W55MH32 处于同一网段,PC 和开发板需接入同一路由器或交换机。

  1. Micro USB 线连接 DEBUG USB 口到 PC(供电 + 调试串口)
  2. 网线连接开发板 RJ45 网口 到路由器/交换机 LAN 口,PC 也接入同一网络
┌─────────────────────────────────┐
│ 路由器 / 交换机 │
│ 192.168.1.1 │
└────┬──────────────────┬───────────┘
│ │
┌────┴──────┐ ┌────┴──────┐
│ W55MH32 │ │ PC │
│ (Modbus │ │ (Modbus │
│ Server) │ │ Poll) │
│ :502 │ │ │
└───────────┘ └───────────┘
↑
DEBUG USB ──→ WIZ UartTool
网络拓扑:W55MH32 作为 Modbus TCP 服务器监听 502 端口,PC 运行 Modbus Poll 作为客户端

工程结构

工程位于 SDK 内 1.SDK\ModuleDemo\WZTOE\Modbus_TCP\,核心文件如下:

Modbus_TCP/
├── Project/
│   └── Project.uvprojx       ← Keil 工程文件
└── USER/
    ├── main.c                ← 硬件初始化 + Modbus TCP 主流程
    ├── modbus/
    │   ├── modbus.c          ← Modbus TCP 报文处理(do_Modbus / mbTCPtoEVB)
    │   └── modbus.h
    ├── wiz_interface/
    │   ├── wiz_interface.c   ← PHY 检测 / 网络信息打印
    │   └── wiz_interface.h
    ├── uart/
    │   ├── bsp_uart.c        ← 调试串口 + printf 重定向
    │   └── bsp_uart.h
    └── delay/
        └── delay.c/h          ← SysTick 毫秒延时

本例程相比 TOE 初始化章节的核心新增:LED 回调注册机制和 Modbus TCP 报文状态机处理。

步骤1:初始化并注册 LED 相关函数

文件:USER/main.c — 在程序初始化部分添加 LED 所使用的 GPIO 外设初始化和注册设置、获取 LED 状态的函数,用于在接收到特定的 Modbus TCP 数据时进行状态的显示。

user_led_init();
user_led_control_init(get_user_led_status, set_user_led_status);

user_led_control_init() 为 LED 控制初始化函数,允许用户注册两个回调函数:一个用于获取 LED 状态,另一个用于设置 LED 状态。这些回调函数将在 get_led_status() 和 set_led_status() 函数中被调用。函数如下:

void user_led_control_init(int (*get_fun)(void), void (*set_fun)(uint32_t))
{
    if (get_fun != NULL && set_fun != NULL)
    {
        getUserLED_cb = get_fun;
        setUserLED_cb = set_fun;
    }
}

get_led_status() 和 set_led_status() 为获取和设置 LED 状态的函数,前者调用注册的获取回调函数并返回结果,后者调用注册的设置回调函数并传入新的状态值。函数如下:

int get_led_status(void)
{
    return getUserLED_cb();
}
void set_led_status(int32_t val)
{
    setUserLED_cb(val);
}

回调机制的优势

通过回调函数注册机制,Modbus 协议层与硬件控制层解耦。你可以将 get_user_led_status 和 set_user_led_status 替换为任意硬件控制函数(如继电器、电机等),而无需修改 Modbus 处理逻辑。

步骤2:主循环调用 do_Modbus() 处理通信

文件:USER/main.c — 在循环中不断执行 do_Modbus() 函数,用于处理 Modbus TCP 通信,根据套接字的不同状态执行相应的操作,包括监听连接请求、处理连接建立事件、接收和处理数据以及关闭连接等。

while (1)
{
    do_Modbus(SOCKET_ID);
}

do_Modbus() 首先获取本地 IP 地址和指定 socket 的状态,根据 socket 的状态执行相应操作:SOCK_CLOSED 时重新打开 TCP socket;SOCK_INIT 时调用 listen() 开始监听;SOCK_ESTABLISHED 时处理连接和数据接收;SOCK_CLOSE_WAIT 时断开连接。

void do_Modbus(uint8_t sn)
{
    uint8_t  state = 0;
    uint16_t len;
    getSIPR(lip);
    state = getSn_SR(sn);
    switch (state)
    {
    case SOCK_SYNSENT:
        break;
    case SOCK_INIT:
        listen(sn);
        if (!b_listening_printed)
        {
            b_listening_printed = 1;
            printf("Listening on %d.%d.%d.%d:%d\r\n", lip[0], lip[1], lip[2], lip[3], local_port);
        }
        break;
    case SOCK_LISTEN:
        break;
    case SOCK_ESTABLISHED:
        if (getSn_IR(sn) & Sn_IR_CON)
        {
            setSn_IR(sn, Sn_IR_CON);
            printf("Connected\r\n");
            getSn_DIPR(sn, rip);
            port = getSn_DPORT(sn);
            printf("RemoteIP:%d.%d.%d.%d Port:%d\r\n", rip[0], rip[1], rip[2], rip[3], port);

            if (b_listening_printed)
                b_listening_printed = 0;
        }
        len = getSn_RX_RSR(sn);
        if (len > 0)
        {
            mbTCPtoEVB(sn);
        }
        break;
    case SOCK_CLOSE_WAIT:
        disconnect(sn);
        break;
    case SOCK_CLOSED:
    case SOCK_FIN_WAIT:
        close(sn);
        socket(sn, Sn_MR_TCP, local_port, Sn_MR_ND); // Sn_MR_ND
        break;
    default:
        break;
    }
}

mbTCPtoEVB() 函数用于处理从 Modbus TCP 接收到的数据,根据数据内容执行相应的操作。首先会进行检查、比对:当设备地址为 0x01 且功能码为 0x05(写单个线圈)时,若数据值为 0xFF 则调用 set_led_status(0) 点亮 LED 并打印 "LED ON",若数据值为 0x00 则调用 set_led_status(1) 熄灭 LED 并打印 "LED OFF",再调用 send() 将接收到的数据原样回送给客户端;当功能码为 0x01(读线圈)时,组装响应报文,send_data[9] 填入 ~get_led_status()(取反后 1=ON, 0=OFF)。如果接收到的数据不符合预期,会打印相应的错误信息。

void mbTCPtoEVB(uint8_t sn)
{
    if (MBtcp2evbFrame() != 0)                         // Frame received complete
    {
        if (pucASCIIBufferCur[0] == 0x01)              // Check whether the device address is 0x01
        {
            if ((uint8_t)pucASCIIBufferCur[1] == 0x05) // Write to a single device
            {
                if ((uint8_t)pucASCIIBufferCur[4] == 0xff)
                {
                    set_led_status(0);                 /* ON: 板载LED低电平点亮,set(0)=ResetBits=点亮 */
                    printf("LED ON\r\n");
                }
                else if ((uint8_t)pucASCIIBufferCur[4] == 0x00)
                {
                    set_led_status(1);                 /* OFF: set(1)=SetBits=熄灭 */
                    printf("LED OFF\r\n");
                }
                send(sn, recv_data, recv_len);
            }
            else if ((uint8_t)pucASCIIBufferCur[1] == 0x01) // Read Write to a single device
            {
                if (recv_data[recv_len - 1] != 0x01)
                {
                    printf("len error!%x\r\n", recv_data[recv_len - 1]);
                }
                else
                {
                    printf("Read OK!\r\n");
                    send_data[0] = recv_data[0];
                    send_data[1] = recv_data[1];
                    send_data[2] = recv_data[2];
                    send_data[3] = recv_data[3];
                    send_data[4] = 0x00;
                    send_data[5] = 0x04;
                    send_data[6] = 0x01;
                    send_data[7] = 0x01;
                    send_data[8] = 0x01;
                    send_data[9] = ~get_led_status();   /* 取反:低电平点亮语义 → Modbus 标准语义 */
                    send_len     = 10;
                    send(sn, send_data, send_len);
                    memset(send_data, 0, send_len);
                }
            }
            else
            {
                printf("error code!\r\n");
            }
        }
        else
        {
            printf("address error!\r\n");
        }
    }
}

报文处理逻辑

本例程中,从机地址设为 0x01。功能码 0x05 用于写单线圈(控制 LED 开关),数据为 0xff 时点亮 LED,为 0x00 时熄灭 LED;功能码 0x01 用于读线圈状态,回复当前 LED 状态。设备地址或功能码不匹配时会打印相应的错误信息。读 LED 状态时返回 ~get_led_status(),因为板载 LED 是低电平点亮,get_led_status() 返回的是 GPIO 电平(0=点亮, 1=熄灭),需要按位取反才能转换成 Modbus 标准线圈语义(1=ON, 0=OFF)。

运行验证

打开 WIZ UartTool V1.0,连接 DEBUG USB 对应 COM 口(115200 / 8N1)。烧录例程运行后,首先可以看到进行了 PHY 链路检测,然后打印了设置的网络地址信息,然后开始监听地址和端口号。预期串口输出如下:

W55MH32 Modbus TCP Server example
PHY link
The current Mbtis speed : 100Mbps
The current Duplex Mode : Full-Duplex
DHCP running
DHCP success!
====================================================================================================
 W55MH32 network configuration : DHCP

 MAC         : 00:08:DC:00:0E:9A
 IP          : 192.168.1.6
 Subnet Mask : 255.255.255.0
 Gateway     : 192.168.1.1
 DNS         : 192.168.1.1
====================================================================================================

Listening on 192.168.1.6:502

接着使用 Modbus Poll 工具进行 Modbus 测试:首先按 F3 设置连接服务器参数(Remote IP 填写 W55MH32 的 IP 192.168.1.30,Port 502,Slave ID 1);接着按 F8 设置读取参数(Function 01 Read Coils,Address 0,Quantity 1)并开启读取轮询,可以看到在不断读取 LED 状态;设置 LED 的状态为打开,可以看到 LED 成功打开,并且读取的 LED 状态已经更新。

W55MH32 串口输出 — Modbus TCP 服务器初始化(PHY 链路 + 网络配置 + 监听 502 端口)
Modbus Poll Connection Setup(F3)— Modbus TCP/IP,目标 192.168.1.6:502
WIZ UartTool 串口输出(Read OK!)+ Modbus Poll — TCP 连接成功,读取 LED 线圈地址 0
Modbus Poll Read/Write Definition(F8)— Slave ID=1,Function 01 Read Coils,Address=0,Quantity=1
WIZ UartTool 串口输出(LED ON)+ Modbus Poll Write Single Coil(Function 05,Value=On)— LED 远程控制测试


常见问题

Q: Modbus TCP 默认端口是多少,可以修改吗

Modbus TCP 默认使用 502 端口。在本例程中通过 local_port 变量指定监听端口,可根据需要修改。但使用 Modbus Poll 等标准工具测试时,建议保持 502 以保证兼容性。

Q: 从机地址(单元标识符)有什么作用

单元标识符(Unit Identifier)用于在 Modbus TCP 报文中区分逻辑设备。本例程将从机地址设为 0x01,在 mbTCPtoEVB() 中通过 pucASCIIBufferCur[0] == 0x01 进行校验。如果报文的设备地址与预设值不符,会打印 "address error!"。

Q: do_Modbus() 函数中的 socket 状态机是如何工作的

do_Modbus() 通过 getSn_SR(sn) 获取 socket 状态并执行相应操作:SOCK_CLOSED 时重新打开 TCP socket;SOCK_INIT 时调用 listen() 开始监听;SOCK_ESTABLISHED 时处理连接和数据接收;SOCK_CLOSE_WAIT 时断开连接。该状态机循环运行,保证通信的持续可用。

Q: 为什么读 LED 状态时返回 ~get_led_status()

板载 LED 是低电平点亮,get_led_status() 返回的是 GPIO 电平(0=点亮, 1=熄灭)。而 Modbus 线圈的标准语义是 1=ON, 0=OFF,所以返回时需要按位取反 ~get_led_status(),把低电平点亮的语义转换成标准 Modbus 语义,才能与 Modbus Poll 显示的状态一致。

Q: Modbus TCP 通信出现异常如何排查

首先确保 PC 和 W55MH32 处于同一网段并能 ping 通;检查监听端口是否正确;使用 Modbus Poll 时确认连接的服务器 IP、端口、从机地址与例程一致;通过串口打印观察连接建立和数据收发情况;若报文格式错误会打印 "len error!" 或 "error code!",可根据提示定位问题。

总结

本文实现了 W55MH32 通过 Modbus TCP 协议作为服务器与客户端通信的完整流程:

  • 协议基础:Modbus TCP 基于 TCP/IP,运行在 502 端口,采用主从架构,报文包含事务/协议/长度标识、单元标识符、功能码和数据部分
  • 初始化注册:通过 user_led_control_init() 注册获取和设置 LED 状态的回调函数,实现硬件控制接口的解耦
  • 状态机处理:do_Modbus() 依据 socket 状态(CLOSED→INIT→LISTEN→ESTABLISHED)执行监听、连接和数据收发
  • 报文解析:mbTCPtoEVB() 处理功能码 0x05(写单线圈控制 LED)和 0x01(读线圈状态),并返回响应数据
  • 测试验证:配合 Modbus Poll 工具(F3 设置连接、F8 开启轮询)即可完成 LED 的读写控制验证

下载本章例程

我们提供完整的工程文件以及配套开发板,方便你随时测试,快速完成产品开发:

开发环境: Keil MDK5 配套开发板

Andy

W5500 技术支持专家