一、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与工控机时钟 |
- PLC通信核心是工业级可靠/实时数据传输,架构分物理层-数据链路层-应用层,与嵌入式/ROS2通信底层原理相通,仅优化方向不同;
- 对接视觉/ROS2机器人时,优先选择MODBUS TCP(通用)、EtherCAT(高实时)、OPC UA(跨平台),C++可通过串口库/socket/开源SDK实现对接,ROS2可封装为节点发布/订阅PLC数据;
- 工业场景需重点关注通信可靠性(超时、重连、校验)和硬件抗干扰(屏蔽、接地、终端电阻),确保视觉-机器人-PLC的稳定交互。