PLC通信

一、PLC通信的核心认知(与嵌入式/ROS2通信的对比)

1. 基本定义

PLC(可编程逻辑控制器)通信是PLC与外部设备(传感器、视觉系统、机器人、上位机、其他PLC、变频器等)之间的工业级数据交互过程,核心目标是高可靠性、硬实时性、抗干扰性,区别于通用IT通信(如HTTP)和嵌入式/ROS2通信:

  • 嵌入式通信:侧重芯片级(如MCU/UART/I2C)、短距离、低功耗,关注底层硬件驱动和裸机/RTOS适配;
  • ROS2通信:基于DDS协议,侧重分布式、模块化、机器人多节点(如传感器-控制器-执行器)的灵活交互,关注消息订阅/发布、服务调用的易用性;
  • PLC通信:侧重工业现场的强电磁干扰、高低温、长距离场景,关注ms级(甚至μs级)实时性、故障自恢复、数据校验的完整性。

2. 核心价值

对视觉/ROS2机器人开发而言,PLC是工业场景中"机器人-产线-视觉系统"的核心枢纽:

  • 视觉系统:将检测结果(如工件坐标、缺陷状态)传给PLC,由PLC触发产线动作(如夹爪抓取、传送带启停);
  • ROS2机器人:通过PLC获取产线IO状态(如安全门开关、物料到位信号),同时将机器人关节状态、运动指令反馈给PLC做逻辑联锁;
  • 嵌入式硬件:PLC可直接对接传感器/执行器的嵌入式模块,你熟悉的嵌入式通信逻辑(如帧封装、校验)可直接迁移到PLC通信开发中。

3. 通信架构(OSI七层模型的工业简化版)

PLC通信遵循"物理层-数据链路层-应用层"三层架构(工业场景无需复杂的网络层/传输层):

层级 核心作用 典型技术(PLC) 对应技术栈
物理层 电气特性+物理介质 RS485、RJ45、CAN总线、光纤 嵌入式UART/CAN物理层、ROS2以太网物理层
数据链路层 帧封装、差错控制、介质访问 MODBUS RTU帧、PROFINET IRT、EtherCAT帧 ROS2 DDS帧、嵌入式串口协议帧
应用层 业务数据解析、功能定义 MODBUS功能码、OPC UA信息模型、CIP协议 ROS2话题/服务消息、视觉算法数据结构

二、PLC通信的核心协议

1. 通用型协议:MODBUS(覆盖80%中小型项目)

MODBUS是工业通信的"普通话",支持串口/以太网,无需专用硬件,是对接视觉/ROS2控制与PLC的首选。

(1)协议分类与核心结构
类型 传输介质 核心特征 端口/参数
MODBUS RTU RS485/232 二进制帧、紧凑、抗干扰、主从架构 波特率(9600/19200)、奇偶校验(无)、停止位(1)
MODBUS ASCII RS485/232 ASCII码帧、易调试、效率低 同RTU
MODBUS TCP RJ45、以太网 基于TCP/IP、无需串口、跨网络 端口502、TCP可靠连接

核心帧结构(RTU)

复制代码
地址码(1字节) + 功能码(1字节) + 数据域(N字节) + CRC校验(2字节)
  • 地址码:PLC从站地址(0为广播);

  • 功能码(核心,需熟记):

    功能码 作用 适用场景
    01 读线圈(离散输出) 读取PLC输出点(如夹爪控制)
    03 读保持寄存器 读取PLC数值(如视觉坐标)
    05 写单个线圈 控制单个输出(如指示灯)
    06 写单个保持寄存器 写入单个数值(如目标位置)
    16 写多个保持寄存器 批量写入(如机器人关节角度)
  • CRC校验:基于Modbus RTU的16位CRC算法(C++可直接调用开源函数)。

(2)C++对接实现(适配Linux/ROS2环境)
① MODBUS RTU(串口)

依赖库:libserial(Linux)、QtSerialPort(跨平台),以下是读取PLC保持寄存器的核心代码:

cpp 复制代码
#include <libserial/SerialPort.h>
#include <cstdint>
#include <vector>
#include <iostream>

// CRC16校验函数(Modbus RTU专用)
uint16_t modbus_crc16(const uint8_t* data, uint8_t len) {
    uint16_t crc = 0xFFFF;
    for (uint8_t i = 0; i < len; i++) {
        crc ^= data[i];
        for (uint8_t j = 0; j < 8; j++) {
            crc = (crc & 0x0001) ? (crc >> 1) ^ 0xA001 : crc >> 1;
        }
    }
    return crc;
}

// 读取保持寄存器(地址:40001对应寄存器0,偏移需注意)
std::vector<uint16_t> read_holding_registers(SerialPort& serial, uint8_t slave_addr, uint16_t reg_addr, uint16_t reg_num) {
    std::vector<uint8_t> send_frame;
    // 1. 封装请求帧
    send_frame.push_back(slave_addr);       // 从站地址
    send_frame.push_back(0x03);             // 功能码:读保持寄存器
    send_frame.push_back((reg_addr >> 8) & 0xFF); // 寄存器地址高8位
    send_frame.push_back(reg_addr & 0xFF);  // 寄存器地址低8位
    send_frame.push_back((reg_num >> 8) & 0xFF);  // 寄存器数量高8位
    send_frame.push_back(reg_num & 0xFF);   // 寄存器数量低8位
    // 2. 计算CRC并添加到帧尾
    uint16_t crc = modbus_crc16(send_frame.data(), send_frame.size());
    send_frame.push_back(crc & 0xFF);       // CRC低8位
    send_frame.push_back((crc >> 8) & 0xFF); // CRC高8位

    // 3. 发送帧
    serial.Write(send_frame.data(), send_frame.size());

    // 4. 接收响应(响应长度=3+2*reg_num+2:地址1+功能码1+字节数1+数据2*N+CRC2)
    uint8_t resp_len = 3 + 2 * reg_num + 2;
    std::vector<uint8_t> resp_frame(resp_len);
    serial.Read(resp_frame.data(), resp_len);

    // 5. 解析数据(跳过地址、功能码、字节数,取后续2*N字节)
    std::vector<uint16_t> result;
    for (int i = 0; i < reg_num; i++) {
        uint16_t val = (resp_frame[3 + 2*i] << 8) | resp_frame[4 + 2*i];
        result.push_back(val);
    }
    return result;
}

int main() {
    // 初始化串口(适配ROS2环境的Linux串口)
    SerialPort serial("/dev/ttyUSB0"); // 视觉/机器人工控机的串口设备
    serial.Open();
    serial.SetBaudRate(SerialPort::BaudRate::BAUD_19200);
    serial.SetCharacterSize(SerialPort::CharacterSize::CHAR_SIZE_8);
    serial.SetParity(SerialPort::Parity::PARITY_NONE);
    serial.SetStopBits(SerialPort::StopBits::STOP_BITS_1);

    // 读取PLC地址1的40001-40003寄存器(对应reg_addr=0,reg_num=3)
    auto reg_data = read_holding_registers(serial, 1, 0, 3);
    for (auto val : reg_data) {
        std::cout << "寄存器值:" << val << std::endl;
    }

    serial.Close();
    return 0;
}
② MODBUS TCP(以太网,ROS2首选)

无需串口,直接通过TCP/IP通信,端口502,核心是在MODBUS RTU帧前添加7字节MBAP头,C++用socket实现:

cpp 复制代码
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <vector>
#include <iostream>

// 读取MODBUS TCP保持寄存器
std::vector<uint16_t> modbus_tcp_read_reg(uint32_t plc_ip, uint16_t reg_addr, uint16_t reg_num) {
    // 1. 创建TCP socket(ROS2节点中可封装为类成员)
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    sockaddr_in plc_addr{};
    plc_addr.sin_family = AF_INET;
    plc_addr.sin_port = htons(502); // MODBUS TCP固定端口
    plc_addr.sin_addr.s_addr = plc_ip; // PLC的IP(如192.168.1.100)

    // 2. 连接PLC
    connect(sock, (sockaddr*)&plc_addr, sizeof(plc_addr));

    // 3. 封装请求帧(MBAP头 + RTU帧)
    std::vector<uint8_t> send_frame;
    // MBAP头(7字节):事务标识2 + 协议标识2(0) + 长度2(后续字节数) + 单元标识1
    send_frame.push_back(0x00); send_frame.push_back(0x01); // 事务标识
    send_frame.push_back(0x00); send_frame.push_back(0x00); // 协议标识(0=MODBUS)
    send_frame.push_back(0x00); send_frame.push_back(0x06); // 长度(后续6字节:单元标识1+功能码1+地址2+数量2)
    send_frame.push_back(0x01); // 单元标识(对应RTU从站地址)
    // RTU帧部分(功能码+地址+数量)
    send_frame.push_back(0x03); // 读保持寄存器
    send_frame.push_back((reg_addr >> 8) & 0xFF);
    send_frame.push_back(reg_addr & 0xFF);
    send_frame.push_back((reg_num >> 8) & 0xFF);
    send_frame.push_back(reg_num & 0xFF);

    // 4. 发送请求
    send(sock, send_frame.data(), send_frame.size(), 0);

    // 5. 接收响应(长度=7+3+2*reg_num)
    uint8_t resp_len = 7 + 3 + 2 * reg_num;
    std::vector<uint8_t> resp_frame(resp_len);
    recv(sock, resp_frame.data(), resp_len, 0);

    // 6. 解析数据
    std::vector<uint16_t> result;
    for (int i = 0; i < reg_num; i++) {
        uint16_t val = (resp_frame[9 + 2*i] << 8) | resp_frame[10 + 2*i];
        result.push_back(val);
    }

    close(sock);
    return result;
}

int main() {
    // PLC IP:192.168.1.100 → 转换为网络字节序
    uint32_t plc_ip = inet_addr("192.168.1.100");
    // 读取40001-40003(reg_addr=0,reg_num=3)
    auto reg_data = modbus_tcp_read_reg(plc_ip, 0, 3);
    for (auto val : reg_data) {
        std::cout << "TCP寄存器值:" << val << std::endl;
    }
    return 0;
}
(3)ROS2集成MODBUS TCP

将上述代码封装为ROS2节点,将PLC寄存器数据发布为ROS2话题,供机器人节点订阅:

cpp 复制代码
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/int32_multi_array.hpp"
// 引入上述modbus_tcp_read_reg函数

class PLCModbusNode : public rclcpp::Node {
public:
    PLCModbusNode() : Node("plc_modbus_node") {
        // 发布器:PLC寄存器数据话题
        pub_ = this->create_publisher<std_msgs::msg::Int32MultiArray>("plc/holding_registers", 10);
        // 定时器:100ms读取一次PLC(工业场景常用周期)
        timer_ = this->create_wall_timer(
            std::chrono::milliseconds(100),
            std::bind(&PLCModbusNode::read_plc_data, this)
        );
        // PLC IP参数(可通过ROS2参数配置)
        this->declare_parameter("plc_ip", "192.168.1.100");
    }

private:
    void read_plc_data() {
        // 获取PLC IP参数
        std::string plc_ip_str = this->get_parameter("plc_ip").as_string();
        uint32_t plc_ip = inet_addr(plc_ip_str.c_str());
        // 读取寄存器
        auto reg_data = modbus_tcp_read_reg(plc_ip, 0, 3);
        // 封装ROS2消息
        std_msgs::msg::Int32MultiArray msg;
        msg.data.resize(reg_data.size());
        for (int i = 0; i < reg_data.size(); i++) {
            msg.data[i] = reg_data[i];
        }
        // 发布消息
        pub_->publish(msg);
        RCLCPP_INFO(this->get_logger(), "发布PLC寄存器数据:%d, %d, %d", msg.data[0], msg.data[1], msg.data[2]);
    }

    rclcpp::Publisher<std_msgs::msg::Int32MultiArray>::SharedPtr pub_;
    rclcpp::TimerBase::SharedPtr timer_;
};

int main(int argc, char** argv) {
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<PLCModbusNode>());
    rclcpp::shutdown();
    return 0;
}

CMakeLists.txt关键配置(ROS2编译):

cmake 复制代码
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)

add_executable(plc_modbus_node src/plc_modbus_node.cpp)
ament_target_dependencies(plc_modbus_node rclcpp std_msgs)

install(TARGETS plc_modbus_node
  DESTINATION lib/${PROJECT_NAME})

2. 实时工业以太网协议(高实时性场景)

适用于机器人高精度运动控制(如关节同步)、视觉引导的高速抓取,核心是EtherCAT (倍福)、PROFINET IRT(西门子)。

(1)EtherCAT(μs级实时性,ROS2机器人首选)
  • 核心特征:基于标准以太网,周期可达25μs,支持分布式时钟同步,无需专用网卡(开源库SOEM可实现软主站);

  • C++对接:使用开源库SOEM(Simple Open EtherCAT Master),适配Linux/ROS2;

  • ROS2集成:通过ros2_ethercat开源包,将EtherCAT从站(PLC/伺服)的数据映射为ROS2话题,核心逻辑:

    cpp 复制代码
    // 简化示例:SOEM读取EtherCAT数据并发布到ROS2
    #include "rclcpp/rclcpp.hpp"
    #include "sensor_msgs/msg/joint_state.hpp"
    #include <soem/ethercat.h>
    
    class EtherCATNode : public rclcpp::Node {
    public:
        EtherCATNode() : Node("ethercat_node") {
            pub_ = this->create_publisher<sensor_msgs::msg::JointState>("joint_states", 10);
            // 初始化EtherCAT主站
            ec_init("eth0"); // 机器人工控机的以太网口
            // 扫描从站(PLC/伺服)
            if (ec_config_init(FALSE) > 0) {
                RCLCPP_INFO(this->get_logger(), "EtherCAT从站数量:%d", ec_slavecount);
            }
            // 启动EtherCAT循环
            ec_statecheck(0, EC_STATE_OPERATIONAL, 5000);
        }
    
        ~EtherCATNode() { ec_close(); }
    
        void run() {
            while (rclcpp::ok()) {
                // 读取EtherCAT从站输入数据(PLC的关节角度)
                uint16_t joint1 = EC_READ_U16(ec_slave[1].inputs, 0);
                uint16_t joint2 = EC_READ_U16(ec_slave[1].inputs, 2);
                // 封装ROS2关节状态消息
                sensor_msgs::msg::JointState msg;
                msg.header.stamp = this->get_clock()->now();
                msg.name = {"joint1", "joint2"};
                msg.position = {joint1/1000.0, joint2/1000.0}; // 单位转换
                pub_->publish(msg);
                // 发送输出数据(机器人控制指令到PLC)
                EC_WRITE_U16(ec_slave[1].outputs, 0, 1000); // 示例:设置速度
                ec_send_processdata();
                ec_receive_processdata(EC_TIMEOUTRET);
                rclcpp::spin_some(this->get_node_base_interface());
            }
        }
    
    private:
        rclcpp::Publisher<sensor_msgs::msg::JointState>::SharedPtr pub_;
    };
    
    int main(int argc, char** argv) {
        rclcpp::init(argc, argv);
        auto node = std::make_shared<EtherCATNode>();
        node->run();
        rclcpp::shutdown();
        return 0;
    }
(2)PROFINET IRT(西门子生态,ms级实时性)
  • 核心特征:基于TCP/IP,分"非实时(NRT)"和"等时同步实时(IRT)",IRT周期可达1ms;
  • C++对接:使用西门子SIMATIC NET SDK或开源库openPN
  • ROS2集成:西门子官方提供ROS2-PROFINET桥接工具,可直接将PROFINET IO数据映射为ROS2话题。

3. 标准化跨平台协议:OPC UA(工业4.0核心)

适用于多品牌PLC(西门子、罗克韦尔、三菱)、视觉系统、ROS2机器人的跨平台集成,替代传统MODBUS的"寄存器地址依赖"问题。

(1)核心优势
  • 平台无关:支持Windows/Linux/嵌入式,C++/Python/ROS2均可对接;
  • 信息模型:将PLC的IO、寄存器抽象为"节点"(如ns=1;s=::PLC1:DB1.DBD0),无需记忆寄存器地址;
  • 安全性:支持加密、认证,适合工业互联网场景。
(2)C++对接(开源库open62541)
cpp 复制代码
#include <open62541/client_config_default.h>
#include <open62541/client_highlevel.h>
#include <iostream>

int main() {
    // 创建OPC UA客户端
    UA_Client* client = UA_Client_new();
    UA_ClientConfig_setDefault(UA_Client_getConfig(client));
    // 连接PLC的OPC UA服务器(地址如opc.tcp://192.168.1.100:4840)
    UA_StatusCode status = UA_Client_connect(client, "opc.tcp://192.168.1.100:4840");
    if (status != UA_STATUSCODE_GOOD) {
        UA_Client_delete(client);
        return -1;
    }

    // 读取PLC的节点值(如DB1.DBD0,对应节点ID:ns=1;s=DB1.DBD0)
    UA_Variant value;
    UA_Variant_init(&value);
    UA_NodeId nodeId = UA_NODEID_STRING(1, "DB1.DBD0"); // 命名空间1,节点名DB1.DBD0
    status = UA_Client_readValueAttribute(client, nodeId, &value);
    if (status == UA_STATUSCODE_GOOD && UA_Variant_hasScalarType(&value, &UA_TYPES[UA_TYPES_DOUBLE])) {
        double val = *(double*)value.data;
        std::cout << "OPC UA节点值:" << val << std::endl;
    }

    // 写入值到PLC节点
    double write_val = 123.45;
    UA_Variant_setScalar(&value, &write_val, &UA_TYPES[UA_TYPES_DOUBLE]);
    UA_Client_writeValueAttribute(client, nodeId, &value);

    // 清理资源
    UA_Variant_clear(&value);
    UA_Client_disconnect(client);
    UA_Client_delete(client);
    return 0;
}
(3)ROS2集成OPC UA

使用ros2-opcua开源包,将OPC UA节点数据发布为ROS2话题,核心逻辑:

cpp 复制代码
// 简化示例:OPC UA客户端→ROS2发布器
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/float64.hpp"
#include <open62541/client_config_default.h>

class OPCUANode : public rclcpp::Node {
public:
    OPCUANode() : Node("opcua_node") {
        pub_ = this->create_publisher<std_msgs::msg::Float64>("plc/db1_dbd0", 10);
        timer_ = this->create_wall_timer(
            std::chrono::milliseconds(100),
            std::bind(&OPCUANode::read_opcua_data, this)
        );
        // 初始化OPC UA客户端
        client_ = UA_Client_new();
        UA_ClientConfig_setDefault(UA_Client_getConfig(client_));
        UA_Client_connect(client_, "opc.tcp://192.168.1.100:4840");
    }

    ~OPCUANode() {
        UA_Client_disconnect(client_);
        UA_Client_delete(client_);
    }

private:
    void read_opcua_data() {
        UA_Variant value;
        UA_Variant_init(&value);
        UA_NodeId nodeId = UA_NODEID_STRING(1, "DB1.DBD0");
        if (UA_Client_readValueAttribute(client_, nodeId, &value) == UA_STATUSCODE_GOOD) {
            double val = *(double*)value.data;
            std_msgs::msg::Float64 msg;
            msg.data = val;
            pub_->publish(msg);
        }
        UA_Variant_clear(&value);
    }

    rclcpp::Publisher<std_msgs::msg::Float64>::SharedPtr pub_;
    rclcpp::TimerBase::SharedPtr timer_;
    UA_Client* client_;
};

int main(int argc, char** argv) {
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<OPCUANode>());
    rclcpp::shutdown();
    return 0;
}

4. 其他协议

协议 适用场景 C++对接方式
CANopen 嵌入式PLC、机器人关节控制 SocketCAN + canopenpp库
EtherNet/IP 罗克韦尔PLC、汽车产线 开源库opener + ROS2-EtherNet/IP桥接
MQTT PLC上云、远程监控 paho-mqtt库 + ROS2-MQTT桥接

三、PLC通信的硬件接口与可靠性设计

1. 硬件接口

接口类型 特征 注意事项(工业场景)
RS485 差分信号、抗干扰、最多32节点 需接终端电阻(120Ω)、屏蔽线缆接地
RJ45 标准以太网、支持PROFINET/EtherCAT 工业交换机(支持TSN)、屏蔽网线
CAN总线 短距离、高实时性 终端电阻(60Ω)、隔离收发器

2. 可靠性设计(工业场景核心)

作为视觉/机器人开发工程师,需重点关注以下点:

  • 通信超时:设置500ms超时时间,避免ROS2节点阻塞;
  • 断线重连:在C++代码中检测连接状态,断线后自动重连(如MODBUS TCP的socket重连);
  • 数据校验:除协议自带的CRC外,应用层增加范围校验(如机器人关节角度≤180°);
  • 心跳机制:PLC与ROS2节点互发心跳包,异常时触发安全停机;
  • 抗干扰:硬件上使用隔离串口/以太网模块,软件上对数据做防抖(如连续3次读取一致才生效)。

四、PLC通信的调试与故障排查

1. 核心工具(适配Linux/ROS2环境)

  • 串口调试:minicom(Linux)、SSCOM(Windows)→ 验证MODBUS RTU帧;
  • 以太网抓包:Wireshark→ 分析MODBUS TCP/PROFINET帧;
  • 专用工具:西门子Profinet IO Scout、倍福TwinCAT Scope→ 调试实时以太网;
  • ROS2调试:ros2 topic echo→ 验证PLC数据是否正确发布。

2. 常见故障及解决

故障现象 原因 解决方法
通信超时 PLC IP/端口错误、物理连接断开 ping PLC IP、检查线缆/串口参数
数据错误 CRC校验失败、寄存器地址偏移 核对帧格式、确认寄存器地址(如40001对应0)
实时性差 以太网带宽不足、非实时协议 切换EtherCAT/PROFINET IRT、优化ROS2 DDS配置
ROS2与PLC不同步 时钟未同步 使用NTP同步PLC与工控机时钟

  1. PLC通信核心是工业级可靠/实时数据传输,架构分物理层-数据链路层-应用层,与嵌入式/ROS2通信底层原理相通,仅优化方向不同;
  2. 对接视觉/ROS2机器人时,优先选择MODBUS TCP(通用)、EtherCAT(高实时)、OPC UA(跨平台),C++可通过串口库/socket/开源SDK实现对接,ROS2可封装为节点发布/订阅PLC数据;
  3. 工业场景需重点关注通信可靠性(超时、重连、校验)和硬件抗干扰(屏蔽、接地、终端电阻),确保视觉-机器人-PLC的稳定交互。
相关推荐
寻寻觅觅☆8 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
fpcc8 小时前
并行编程实战——CUDA编程的Parallel Task类型
c++·cuda
ceclar1239 小时前
C++使用format
开发语言·c++·算法
鲁邦通物联网10 小时前
架构拆解:如何构建支持室外内外网络切换的机器人梯控中间件?
机器人·机器人梯控·agv梯控·机器人乘梯·机器人自主乘梯·agv机器人梯控
lanhuazui1010 小时前
C++ 中什么时候用::(作用域解析运算符)
c++
charlee4410 小时前
从零实现一个生产级 RAG 语义搜索系统:C++ + ONNX + FAISS 实战
c++·faiss·onnx·rag·语义搜索
xixixi7777710 小时前
太赫兹通信:6G时代的“超高速无线血液”
无线通信·信息与通信·无线·通信·6g·频谱·太赫兹
不做无法实现的梦~10 小时前
ros2实现路径规划---nav2部分
linux·stm32·嵌入式硬件·机器人·自动驾驶
老约家的可汗10 小时前
初识C++
开发语言·c++
crescent_悦10 小时前
C++:Product of Polynomials
开发语言·c++