C语言如何实现Modbus TCP通信?

99ANYc3cd6
预计阅读时长 52 分钟
位置: 首页 C语言 正文

Modbus TCP 是工业自动化领域最常用的通信协议之一,它将经典的 Modbus 协议封装在 TCP/IP 数据包中,使得设备可以通过以太网进行通信。

c语言 modbus tcp
(图片来源网络,侵删)

本文将分为以下几个部分:

  1. Modbus TCP 核心概念
  2. 开发环境准备
  3. 服务器端实现
  4. 客户端实现
  5. 常用库介绍
  6. 调试与工具

Modbus TCP 核心概念

在开始编码前,必须理解几个关键点:

a. MBAP (Modbus Application Protocol) Header

Modbus TCP 的所有请求和响应报文前面都有一个 7 字节的 MBAP 头部,这是它与串行 Modbus (RTU/ASCII) 最大的区别,MBAP 头部包含以下信息:

字节 字段 描述
0-1 Transaction Identifier (事务标识符) 由客户端发起请求时生成,服务器在响应中必须原样返回,用于客户端匹配请求和响应。
2-3 Protocol Identifier (协议标识符) 对于 Modbus TCP,此值固定为 0x0000
4-5 Length (长度) 表示单元标识符PDU (Protocol Data Unit) 的总长度,即 Unit ID + PDU 的字节数。
6 Unit Identifier (单元标识符) 在 Modbus TCP 中通常被忽略,但为了兼容 Modbus 网关,一般设置为设备地址(如 1)。

完整的 Modbus TCP 报文结构: [MBAP Header (7 bytes)] + [PDU (Function Code + Data)]

c语言 modbus tcp
(图片来源网络,侵删)

b. PDU (Protocol Data Unit)

PDU 是 Modbus 的核心功能部分,由功能码和数据组成。

  • 功能码: 一个字节,定义了要执行的操作。

    • 0x01: 读线圈状态
    • 0x02: 读离散输入
    • 0x03: 读保持寄存器
    • 0x04: 读输入寄存器
    • 0x05: 写单个线圈
    • 0x06: 写单个寄存器
    • 0x0F: 写多个线圈
    • 0x10: 写多个寄存器
  • 数据: 随功能码变化,例如要读取的起始地址、数量等。

c. 通信模型

  • 服务器: 监听特定端口(默认为 502),等待客户端连接,通常是 PLC、仪表、驱动器等设备。
  • 客户端: 主动连接服务器,发送请求并接收响应,通常是 HMI (人机界面)、SCADA 系统、上位机等。

开发环境准备

我们将使用 Linux 和 C 语言进行演示,因为它提供了最底层的 Socket API。

c语言 modbus tcp
(图片来源网络,侵删)

必要工具

  • Linux 操作系统 (如 Ubuntu, CentOS)
  • GCC 编译器
  • 文本编辑器 (如 vim, nano, code)

测试工具

为了方便测试,我们需要一个 Modbus TCP 客户端工具,推荐使用 modbus-climodpoll。 安装 modpoll (来自 libmodbus 工具集):

sudo apt-get update
sudo apt-get install libmodbus-tools

modpoll 的用法:

# 读取 1 号设备,保持寄存器,起始地址 0,读取 2 个
modpoll -p 502 -t 3 -1 -r 0 -c 2 127.0.0.1

服务器端实现

服务器端的核心任务是:

  1. 创建一个 Socket。
  2. 绑定 IP 地址和端口 502。
  3. 监听连接。
  4. 循环接受客户端连接。
  5. 为每个连接创建一个线程或使用 select/poll 来处理请求和响应。

下面是一个完整的示例代码,它实现了最常用的 读保持寄存器 (0x03)写单个寄存器 (0x06) 功能。

modbus_server.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#define PORT 502
#define MAX_CLIENTS 5
#define BUFFER_SIZE 1024
// 模拟的设备寄存器
// 保持寄存器
uint16_t holding_registers[128] = {0};
// 线程处理函数
void *handle_client(void *arg) {
    int client_socket = *(int *)arg;
    free(arg); // 释放参数内存
    uint8_t buffer[BUFFER_SIZE];
    int bytes_read;
    printf("Client connected on socket %d\n", client_socket);
    while ((bytes_read = recv(client_socket, buffer, BUFFER_SIZE, 0)) > 0) {
        // 1. 解析 MBAP 头部
        uint16_t transaction_id = (buffer[0] << 8) | buffer[1];
        uint16_t protocol_id = (buffer[2] << 8) | buffer[3];
        uint16_t length = (buffer[4] << 8) | buffer[5];
        uint8_t unit_id = buffer[6];
        printf("Received - TID: %d, PID: %d, Len: %d, UnitID: %d\n", transaction_id, protocol_id, length, unit_id);
        // 2. 解析 PDU (功能码和数据)
        uint8_t function_code = buffer[7];
        uint8_t response_buffer[BUFFER_SIZE];
        int response_length = 0;
        // 3. 根据功能码处理请求
        switch (function_code) {
            case 0x03: { // 读保持寄存器
                uint16_t start_addr = (buffer[8] << 8) | buffer[9];
                uint16_t quantity = (buffer[10] << 8) | buffer[11];
                printf("Read Holding Registers: Start=%d, Quantity=%d\n", start_addr, quantity);
                // 检查请求是否有效
                if (start_addr + quantity > sizeof(holding_registers) / sizeof(holding_registers[0])) {
                    // 异常响应:非法地址
                    response_buffer[7] = function_code | 0x80; // 设置最高位表示异常
                    response_buffer[8] = 0x02; // 异常码:非法数据地址
                    response_length = 9; // MBAP(7) + PDU(2)
                } else {
                    // 正常响应
                    response_buffer[7] = function_code;
                    response_buffer[8] = quantity * 2; // 返回的字节数
                    response_length = 9;
                    // 将寄存器值复制到响应缓冲区
                    for (int i = 0; i < quantity; i++) {
                        response_buffer[9 + i * 2] = (holding_registers[start_addr + i] >> 8) & 0xFF;
                        response_buffer[9 + i * 2 + 1] = holding_registers[start_addr + i] & 0xFF;
                    }
                    response_length += quantity * 2;
                }
                break;
            }
            case 0x06: { // 写单个寄存器
                uint16_t register_addr = (buffer[8] << 8) | buffer[9];
                uint16_t register_value = (buffer[10] << 8) | buffer[11];
                printf("Write Single Register: Addr=%d, Value=%d\n", register_addr, register_value);
                if (register_addr >= sizeof(holding_registers) / sizeof(holding_registers[0])) {
                    // 异常响应:非法地址
                    response_buffer[7] = function_code | 0x80;
                    response_buffer[8] = 0x02; // 异常码:非法数据地址
                    response_length = 9;
                } else {
                    // 正常响应
                    holding_registers[register_addr] = register_value;
                    response_buffer[7] = function_code;
                    response_buffer[8] = register_addr >> 8;
                    response_buffer[9] = register_addr & 0xFF;
                    response_buffer[10] = register_value >> 8;
                    response_buffer[11] = register_value & 0xFF;
                    response_length = 12;
                }
                break;
            }
            default: {
                // 未知功能码
                response_buffer[7] = function_code | 0x80;
                response_buffer[8] = 0x01; // 异常码:非法功能码
                response_length = 9;
                break;
            }
        }
        // 4. 构建完整的 Modbus TCP 响应报文
        // 复制 MBAP 头部
        response_buffer[0] = (transaction_id >> 8) & 0xFF;
        response_buffer[1] = transaction_id & 0xFF;
        response_buffer[2] = (protocol_id >> 8) & 0xFF; // 通常是 0
        response_buffer[3] = protocol_id & 0xFF;       // 通常是 0
        response_buffer[4] = ((response_length - 6) >> 8) & 0xFF; // PDU长度 = 总长度 - MBAP长度(7) - UnitID(1)
        response_buffer[5] = (response_length - 6) & 0xFF;
        response_buffer[6] = unit_id; // Unit ID
        // 5. 发送响应
        send(client_socket, response_buffer, response_length, 0);
    }
    if (bytes_read == 0) {
        printf("Client disconnected on socket %d\n", client_socket);
    } else {
        perror("recv");
    }
    close(client_socket);
    pthread_exit(NULL);
}
int main() {
    int server_fd, client_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    pthread_t thread_id;
    // 1. 创建 Socket
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
    // 2. 设置 Socket 选项,允许地址重用
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY; // 监听所有网络接口
    address.sin_port = htons(PORT);
    // 3. 绑定地址和端口
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }
    // 4. 开始监听
    if (listen(server_fd, MAX_CLIENTS) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }
    printf("Modbus TCP Server listening on port %d...\n", PORT);
    // 5. 循环接受客户端连接
    while (1) {
        if ((client_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
            perror("accept");
            continue;
        }
        printf("New connection from %s:%d\n", inet_ntoa(address.sin_addr), ntohs(address.sin_port));
        // 为每个客户端创建一个新线程
        int *new_sock = malloc(sizeof(int));
        *new_sock = client_socket;
        if (pthread_create(&thread_id, NULL, handle_client, (void*)new_sock) != 0) {
            perror("could not create thread");
            free(new_sock);
            close(client_socket);
        }
    }
    return 0;
}

编译与运行

gcc modbus_server.c -o modbus_server -lpthread
./modbus_server

服务器现在开始监听 502 端口。


客户端实现

客户端的核心任务是:

  1. 创建一个 Socket。
  2. 连接到服务器的 IP 地址和端口 502。
  3. 构造 Modbus TCP 请求报文。
  4. 发送请求并接收响应。
  5. 解析响应。

modbus_client.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 502
#define BUFFER_SIZE 1024
int main() {
    int sock = 0;
    struct sockaddr_in serv_addr;
    uint8_t buffer[BUFFER_SIZE] = {0};
    uint8_t request[BUFFER_SIZE] = {0};
    // 1. 创建 Socket
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        printf("\n Socket creation error \n");
        return -1;
    }
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(SERVER_PORT);
    // 2. 将 IP 地址从文本转换为网络格式
    if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
        printf("\nInvalid address/ Address not supported \n");
        return -1;
    }
    // 3. 连接服务器
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        printf("\nConnection Failed \n");
        return -1;
    }
    // --- 示例1: 读保持寄存器 (功能码 0x03) ---
    printf("--- Reading Holding Registers (FC 03) ---\n");
    // 构造请求报文
    uint16_t transaction_id = 0x0001;
    uint16_t protocol_id = 0x0000;
    uint8_t unit_id = 1;
    uint16_t start_addr = 0;
    uint16_t quantity = 2;
    // MBAP Header
    request[0] = (transaction_id >> 8) & 0xFF;
    request[1] = transaction_id & 0xFF;
    request[2] = (protocol_id >> 8) & 0xFF;
    request[3] = protocol_id & 0xFF;
    request[4] = 0x00; // Length (PDU长度 = 6)
    request[5] = 0x06;
    request[6] = unit_id;
    // PDU
    request[7] = 0x03; // 功能码
    request[8] = (start_addr >> 8) & 0xFF;
    request[9] = start_addr & 0xFF;
    request[10] = (quantity >> 8) & 0xFF;
    request[11] = quantity & 0xFF;
    send(sock, request, 12, 0);
    int valread = read(sock, buffer, BUFFER_SIZE);
    printf("Server Response (Read): ");
    for (int i = 0; i < valread; i++) {
        printf("%02X ", buffer[i]);
    }
    printf("\n\n");
    // --- 示例2: 写单个寄存器 (功能码 0x06) ---
    printf("--- Writing Single Register (FC 06) ---\n");
    // 构造请求报文
    transaction_id = 0x0002;
    uint16_t register_addr = 0;
    uint16_t register_value = 1234;
    // MBAP Header
    request[0] = (transaction_id >> 8) & 0xFF;
    request[1] = transaction_id & 0xFF;
    request[2] = (protocol_id >> 8) & 0xFF;
    request[3] = protocol_id & 0xFF;
    request[4] = 0x00; // Length (PDU长度 = 6)
    request[5] = 0x06;
    request[6] = unit_id;
    // PDU
    request[7] = 0x06; // 功能码
    request[8] = (register_addr >> 8) & 0xFF;
    request[9] = register_addr & 0xFF;
    request[10] = (register_value >> 8) & 0xFF;
    request[11] = register_value & 0xFF;
    send(sock, request, 12, 0);
    valread = read(sock, buffer, BUFFER_SIZE);
    printf("Server Response (Write): ");
    for (int i = 0; i < valread; i++) {
        printf("%02X ", buffer[i]);
    }
    printf("\n");
    close(sock);
    return 0;
}

编译与运行

gcc modbus_client.c -o modbus_client
./modbus_client

客户端将连接到本地的服务器,执行一次读操作和一次写操作,并打印出收到的十六进制响应。


常用库介绍

虽然直接使用 Socket API 可以让你深入了解协议细节,但在实际项目中,使用成熟的库可以大大提高开发效率和可靠性。

a. libmodbus

这是最流行、功能最全面的 Modbus C/C++ 库。

  • 优点: 跨平台、支持 TCP/RTU/ASCII、API 简洁、功能齐全(所有标准功能码)、处理了字节序、异常响应等细节。
  • 缺点: 底层封装较多,如果你想学习协议细节,直接用 Socket API 更好。

使用 libmodbus 实现服务器端示例:

#include <modbus.h>
#include <stdio.h>
#include <unistd.h>
int main() {
    modbus_t *ctx;
    int server_socket;
    uint16_t holding_registers[128] = {0};
    // 创建 TCP 上下文,监听所有接口的 502 端口
    ctx = modbus_new_tcp(NULL, 502);
    if (ctx == NULL) {
        fprintf(stderr, "Failed to create TCP context: %s\n", modbus_strerror(errno));
        return -1;
    }
    // 设置从站 ID (Unit ID)
    modbus_set_slave(ctx, 1);
    // 启动监听
    server_socket = modbus_tcp_listen(ctx, 1);
    if (server_socket == -1) {
        fprintf(stderr, "Failed to listen: %s\n", modbus_strerror(errno));
        modbus_free(ctx);
        return -1;
    }
    printf("Waiting for client connection...\n");
    modbus_tcp_accept(ctx, &server_socket);
    // 循环处理请求
    while (1) {
        // 接收请求并处理,结果存入 holding_registers
        int rc = modbus_read_registers(ctx, 0, 128, holding_registers);
        if (rc == -1) {
            fprintf(stderr, "%s\n", modbus_strerror(errno));
            break;
        }
        // 可以在这里添加逻辑,比如修改 holding_registers 的值
        holding_registers[0] = 0x1234; // 示例:修改第一个寄存器的值
        // 将 holding_registers 的值写回客户端 (如果客户端有写请求)
        // modbus_write_registers(ctx, 0, 10, holding_registers);
    }
    // 清理
    close(server_socket);
    modbus_close(ctx);
    modbus_free(ctx);
    return 0;
}

编译时需要链接 libmodbus 库:

gcc modbus_server_libmodbus.c -o modbus_server_libmodbus -lmodbus

b. pymodbus

虽然名字是 py,但它是一个纯 Python 的库,在 Python 生态中非常流行,如果你的项目允许使用 Python,这是一个极佳的选择。


调试与工具

调试 Modbus 通信时,抓包是必不可少的步骤。

Wireshark

Wireshark 是一个强大的网络协议分析器。

  1. 安装 Wireshark。
  2. 启动你的服务器和客户端。
  3. 在 Wireshark 中选择你的网络接口(如 eth0lo)。
  4. 在显示过滤器中输入 modbustcp.port == 502
  5. 开始捕获,然后运行你的客户端程序。
  6. 你将能看到所有发送和接收的 Modbus TCP 报文,Wireshark 会自动解析 MBAP 头部和 PDU,非常直观。
  • 从零开始: 直接使用 C 语言的 Socket API,你可以完全控制通信的每一个字节,有助于深入理解 Modbus TCP 协议,适合学习和开发简单的应用。
  • 项目开发: 强烈推荐使用 libmodbus 这样的成熟库,它封装了底层的复杂性,提供了稳定、高效的 API,让你可以专注于业务逻辑而不是协议细节。
  • 调试: Wireshark 是调试 Modbus 通信问题的“神器”,没有它寸步难行。

希望这份详细的指南能帮助你顺利地在 C 语言中实现 Modbus TCP 通信!

-- 展开阅读全文 --
头像
dede pc端如何自动跳转手机端?
« 上一篇 今天
dede5.7 sp1漏洞如何修复?
下一篇 » 今天

相关文章

取消
微信二维码
支付宝二维码

目录[+]