1. 背景
1.1 西门子 PLC 数据采集的痛点
西门子是工业自动化领域市场占有率最高的 PLC 品牌之一。产线数据采集(OEE、能耗、产量、设备状态)、边缘网关、MES 对接等场景都需要从 S7-300/400/1200/1500 系列 PLC 中读取数据。但长期以来,西门子 PLC 的以太网通信存在三重痛点:
- 协议私有:S7 通信协议是西门子私有协议,基于 ISO-on-TCP(RFC 1006)封装 COTP + S7 PDU,官方不开放完整协议文档,第三方开发者难以自研;
- 官方 SDK 绑定严重:西门子官方提供 Simatic Net、S7 Communication(如 s7-300 FDL/ISO)、OPC 服务器等方案,但多为闭源、商业授权、绑定 Windows,跨平台与嵌入式场景受限;
- 免费开源方案碎片化:早期可用的免费方案(如 libnodave、snap7 前身)文档缺失、维护不稳定、不支持新型号。
1.2 snap7 的定位
snap7(https://snap7.sourceforge.net/)是一个纯 C++ 编写的开源 S7 通信库,由 Davide Nardella 开发并维护,采用 LGPLv3 + 静态链接豁免条款(可静态链接商用)。其核心特性:
- 纯 C++ 实现、零第三方依赖:内部自带 ISO-on-TCP/COTP/S7 PDU 协议栈,仅依赖操作系统 Socket API;
- 跨平台:Windows / Linux / macOS / FreeBSD / OpenBSD,嵌入式(ARM 等)可裁剪编译;
- 三端模型齐全:TS7Client(客户端,采集最常用)、TS7Server(模拟服务器,可仿真 PLC 供测试/教学)、TS7Partner(S7 伙伴对等通信);
- 多语言绑定:官方提供 C API(snap7.h),社区派生 C++ 类封装、C#(Snap7.NET/S7.Net)、Python(python-snap7)、Go(gos7)、Java、Node.js 等绑定,同一套 C 内核 + 各语言薄封装;
- 型号覆盖广:S7-200/300/400/1200/1500,以及兼容 S7 协议的第三方设备(部分国产 PLC、软 PLC 仿真器)。
2. API 说明
snap7 提供三层接口:C++ 类封装(TS7Client / TS7Server / TS7Partner)→ 官方 C API(Cli_* / Srv_* / Par_*)→ 各语言绑定。底层均走同一套 C 内核,因此 API 语义完全一致。
2.1 C++ 客户端 TS7Client 核心 API
| API | 签名 | 作用 |
|---|---|---|
| 连接管理 | int ConnectTo(const char* Address, int Rack, int Slot) | 按 IP + 机架/槽位连接(自动推导 TSAP) |
| 连接管理 | int Connect() | 使用此前 SetConnectionParams 设置的参数连接 |
| 连接管理 | int SetConnectionParams(const char* Address, word LocalTSAP, word RemoteTSAP) | 手动指定 TSAP(1200/1500 与特殊拓扑必用) |
| 连接管理 | int SetConnectionType(word ConnectionType) | 连接类型:CONNTYPE_PG=0x01 / CONNTYPE_OP=0x02 / CONNTYPE_DRIVER=0x03 |
| 断开 | void Disconnect() | 断开连接 |
| 状态 | bool GetConnected() | 是否已连接(物理 TCP 层) |
| 通用读写 | int ReadArea(int Area, int DBNumber, int Start, int Amount, int WordLen, void* pUsrData) | 任意区域读写 |
| 通用读写 | int WriteArea(int Area, int DBNumber, int Start, int Amount, int WordLen, void* pUsrData) | 任意区域写入 |
| 快捷读写 | int DBRead(int DBNumber, int Start, int Size, void* pUsrData) | DB 块字节流读 |
| 快捷读写 | int DBWrite(int DBNumber, int Start, int Size, void* pUsrData) | DB 块字节流写 |
| 快捷读写 | int MBRead(int Start, int Size, void* pUsrData) / int MBWrite(...) | M 标志区读写 |
| 快捷读写 | int EBRead/EBWrite/ABRead/ABWrite(...) | 输入 I / 输出 Q 区读写 |
| 系统信息 | int GetCpuInfo(TS7CpuInfo* pInfo) | CPU 型号/序列号/固件 |
| 系统信息 | int GetCpInfo(TS7CpInfo* pInfo) | 通信处理器信息 |
| 系统信息 | int GetPlcStatus(int* Status) | 运行状态(RUN/STOP) |
| 诊断 | int ReadSZL(int ID, int Index, TS7Szl* pSzl, int SzlSize) | 读系统状态列表(诊断/报文) |
| 保护 | int SetSessionPassword(const char* Password) / int ClearSessionPassword() | 1200/1500 会话密码(保护等级) |
| 异步变体 | int ConnectAsync() / int ReadAreaAsync(...) 等 | 非阻塞版本(内部线程 + 完成事件) |
| 事件 | void SetAsCallBack(pfn_CallBack pCallback, void* UsrPtr) | 异步完成/错误回调 |
2.2 区域与数据长度常量(最高频踩坑点)
Area(读写区域):
| 常量 | 值 | 含义 |
|---|---|---|
| S7AreaPE | 0x81 | 输入区 I(过程映像输入) |
| S7AreaPA | 0x82 | 输出区 Q(过程映像输出) |
| S7AreaMK | 0x83 | 标志位区 M |
| S7AreaDB | 0x84 | 数据块 DB |
| S7AreaCT | 0x1C | 计数器 C |
| S7AreaTM | 0x1D | 定时器 T |
WordLen(数据长度单位):
| 常量 | 值 | 单位 |
|---|---|---|
| S7WLBit | 0x01 | 位 |
| S7WLByte | 0x02 | 字节 |
| S7WLWord | 0x04 | 字(2 字节) |
| S7WLDWord | 0x06 | 双字(4 字节) |
| S7WLReal | 0x08 | 浮点(4 字节) |
| S7WLCounter | 0x1C | 计数器值 |
| S7WLTimer | 0x1D | 定时器值 |
寻址语义 :Start 是字节偏移 (位读写时是位地址 = 字节号 × 8 + 位号);Amount 是数量,单位由 WordLen 决定。例如读 DB1 第 0 字节起 10 个字节:ReadArea(S7AreaDB, 1, 0, 10, S7WLByte, buf);读 M10.3 一个位:ReadArea(S7AreaMK, 0, 10*8+3, 1, S7WLBit, &bit)。
2.3 TS7Server / TS7Partner
- TS7Server(服务器,仿真用) :Start() 启动 S7 服务器监听;RegisterArea(int AreaCode, int Index, void* pUsrData, int Size) 注册内存区域供客户端读写;PickEvent() 取出客户端访问事件;SetArea/UnsetArea 动态改区。可用于无硬件仿真联调:在测试机跑一个 TS7Server 模拟 DB/M 区,客户端代码无需 PLC 即可开发调试。
- TS7Partner(伙伴):S7 协议原生支持 PLC 与 PLC(或 PC 与 PLC)之间的伙伴式通信(如 S7-300 的 BSEND/BRCV 或 PUT/GET 伙伴),Start() 后通过 Read/Write 与对端交换数据。
2.4 官方 C API(snap7.h)
C API 与 C++ 封装一一对应,函数前缀 Cli_(客户端)/ Srv_(服务器)/ Par_(伙伴),返回 int(0=OK,负值=错误码),句柄为 S7Object:
cpp
S7Object client = Cli_Create();
int err = Cli_ConnectTo(client, "192.168.0.1", 0, 2);
if (err != 0) { printf("connect failed: %s\n", Cli_ErrorText(err)); }
uint8_t buf[256];
Cli_DBRead(client, 1, 0, 256, buf);
Cli_Destroy(&client);
错误码可用 Cli_ErrorText(int Error) 转为可读文本,常用错误:errTCPConnectionFailed(-10409)、errNegotiatingPDU(-10415)、errCliFunctionNotAvailable 等(见 snap7.h 枚举)。
2.5 Go 绑定 gos7 对照
gos7(https://github.com/robinson/gos7)是社区最流行的 Go 绑定,封装同一套 S7 协议(纯 Go 实现,非 cgo 调用 C 内核),API 语义与 C++ 对齐:
| C++(TS7Client) | gos7 | 说明 |
|---|---|---|
| TS7Client::ConnectTo(ip, rack, slot) | handler := gos7.NewTCPClientHandler(ip, rack, slot); handler.Connect() | 连接:gos7 拆成 handler(TCP 层)与 client(协议层) |
| SetConnectionType(0x01) | handler.SetConnectionType(1) | 连接类型(1200/1500 必设) |
| DBRead(db, start, size, buf) | client.DBRead(db, start, size, buf) | 字节流读 DB |
| DBWrite(...) | client.DBWrite(...) | 字节流写 DB |
| MBRead/MBWrite | client.MBRead/MBWrite | M 区读写 |
| ReadArea(area, db, start, amount, wordlen, buf) | client.ReadArea(area, db, start, size, wordLen, buf) | 通用读写 |
| GetPlcStatus | client.GetPlcStatus() | 状态查询 |
| --- | helper := gos7.NewHelper() | 字节序解析器:GetReal/GetDWord/GetWord/GetInt/GetDInt/GetByte/GetChar/GetString/SetReal/SetWord/... |
gos7 典型读浮点:
cpp
handler := gos7.NewTCPClientHandler("192.168.0.1", 0, 2)
handler.Timeout = 5 * time.Second
handler.IdleTimeout = 30 * time.Second
handler.SetConnectionType(1) // CONNTYPE_PG,S7-1200/1500 必设
if err := handler.Connect(); err != nil {
log.Fatal(err)
}
defer handler.Close()
client := gos7.NewClient(handler)
helper := gos7.NewHelper()
buf := make([]byte, 4)
if err := client.DBRead(1, 0, 4, buf); err != nil {
log.Fatal(err)
}
temp := helper.GetReal(buf) // S7 大端解码为 float32
3. 详细使用说明
3.1 协议链路(理解 TSAP 的前提)
snap7 的 S7 通信栈从上到下为:
应用层 S7 PDU(Job/Ack_Data 报文,含参数区 + 数据区)
会话层 COTP(TPKT 头 3 字节 + COTP 头 3 字节)
传输层 ISO-on-TCP(RFC 1006,TCP 端口 102)
- TPKT 头:0x03 0x00 <length_hi> <length_lo>,声明 ISO 报文总长;
- COTP 头:0x02 0xF0 0x80(连接请求)/ 0xF0 0x00 0x01(数据 TPDU);
- 建链过程:TCP 连上 102 端口后,先交换 COTP CR/CC 帧(携带本地/远程 TSAP),协商成功后进入 S7 PDU 阶段,再协商 PDU 长度(240/480/960 等),之后才是真正的 S7 Job 读写请求。
TSAP(Transport Service Access Point,传输服务访问点):2 字节,COTP 建链时指定连接"从哪来、到哪去"。这是 snap7 配置中最核心也最易错的概念:
TSAP 高字节 = 连接类型:0x01 本地 / 0x02 OP(HMI) / 0x03 PG(编程器)
TSAP 低字节 = rack * 0x20 + slot
- S7-300/400(RackSlot 模式):CPU 典型 rack=0、slot=2,ConnectTo(ip, 0, 2) 自动推导远程 TSAP = 0x0302(PG 连接);CPU 槽位要按实际硬件填写(S7-300 通常 slot=2,S7-400 根据机架扩展可能变化,如 0/2/3/4);
- S7-1200/1500(Fixed 模式) :snap7 对 1200/1500 自动采用固定 TSAP(本地 0x0100、远程 0x0100),因此 ConnectTo(ip, 0, 1) 即可,rack/slot 不再参与 TSAP 计算(但 PLC 侧仍需在组态中允许 PUT/GET 通信);
- 手动控制:拓扑复杂或直连工控机网卡时用 SetConnectionParams(ip, localTSAP, remoteTSAP),例如 SetConnectionParams("192.168.0.1", 0x0100, 0x0200)。
3.2 PLC 侧组态准备(1200/1500 必做,否则连不上)
- 打开 TIA Portal,选中 CPU → 属性 → 防护与安全 → 连接机制 ,勾选 "允许来自远程对象的 PUT/GET 通信访问";
- 若 DB 需要按偏移寻址,右键 DB 属性取消 "优化的块访问"(1200/1500 默认开启优化块访问,开启后偏移地址不可用,只能符号寻址------snap7 是偏移寻址,必须取消);
- 若启用了访问保护,需在 PLC 中设置密码(snap7 用 SetSessionPassword 配合);
- 确认 CPU 的 PG/HMI 连接资源未被占满(S7-1500 默认约 20 个连接,WinCC/博图在线监视也会占用)。
3.3 最小可运行示例:C++ 读 DB 浮点 + 写 M 字
cpp
#include <iostream>
#include "snap7.h"
int main() {
TS7Client client;
// S7-300/400:rack=0, slot=2;1200/1500 自动固定 TSAP
int err = client.ConnectTo("192.168.0.1", 0, 2);
if (err != 0) {
std::cerr << "connect failed: " << err << std::endl;
return 1;
}
// S7-1200/1500 建议显式设置连接类型
// client.SetConnectionType(CONNTYPE_PG);
// 读 DB1 偏移 0 的 4 字节(float,S7 大端)
uint8_t buf[4];
err = client.DBRead(1, 0, 4, buf);
if (err == 0) {
uint32_t u = (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3];
float v;
memcpy(&v, &u, 4); // 大端字节序,x86 上需重组
std::cout << "DB1.DBD0 = " << v << std::endl;
}
// 写 M100 字(2 字节)
uint16_t mw = 1234;
uint8_t mdata[2] = { uint8_t(mw >> 8), uint8_t(mw & 0xFF) }; // S7 大端
err = client.MBWrite(100, 2, mdata);
client.Disconnect();
return 0;
}
注意:S7 全协议为大端(Big-Endian),x86 上解析 32 位浮点/整型必须手工重组字节,或使用库提供的字节序工具(C++ 可参考 gos7 helper 的思路自封装 s7_to_host 函数)。
3.4 批量读取:位、字节、字、定时器、计数器
cpp
// 读 M10.0 ~ M10.7 八个位(一次 ReadArea 读 8 个位)
uint8_t bits[1];
client.ReadArea(S7AreaMK, 0, 10 * 8, 8, S7WLBit, bits);
// 读 DB2 前 256 字节(批量,推荐一次拉整块)
uint8_t block[256];
client.DBRead(2, 0, 256, block);
// 读计数器 C1(S7 计数器的值以 16 位字返回,高 12 位为 BCD 计数值)
uint16_t ctRaw;
client.ReadArea(S7AreaCT, 0, 1, 1, S7WLCounter, &ctRaw);
int countValue = ((ctRaw >> 12) & 0x0F) * 1000 +
((ctRaw >> 8) & 0x0F) * 100 +
((ctRaw >> 4) & 0x0F) * 10 +
(ctRaw & 0x0F); // BCD → int
// 读定时器 T1(S7 定时器值 = 时基(2位) + BCD 数值(12位))
uint16_t tmRaw;
client.ReadArea(S7AreaTM, 0, 1, 1, S7WLTimer, &tmRaw);
int timeBase[] = { 10, 100, 1000, 10000 }; // 单位 ms
int bcd = tmRaw & 0x0FFF;
int base = (tmRaw >> 12) & 0x03;
int ms = bcd_to_int(bcd) * timeBase[base]; // BCD 数值 × 时基
要点:定时器/计数器返回的是 16 位"压缩格式"而非可直接使用的整数;BCD 解码是常见需求。工程上建议封装成 ReadCounterAsInt / ReadTimerAsMs 工具函数。
3.5 服务器仿真(无硬件联调)
cpp
TS7Server server;
const int kDBSize = 1024;
uint8_t dbArea[kDBSize] = {0};
server.RegisterArea(srvAreaDB, 1, dbArea, kDBSize); // 注册 DB1
if (server.Start() == 0) {
// 客户端用 snap7 连到本机 127.0.0.1:102 即可读写 DB1
std::cout << "S7 server started, waiting for clients..." << std::endl;
getchar();
server.Stop();
}
配合 TS7Client 可在无 PLC 的环境完成协议栈联调、单元测试与 CI 集成。
3.6 工程实践:采集线程 + 批量轮询
cpp
class S7Collector {
public:
bool Start(const std::string& ip, int rack, int slot) {
if (client_.ConnectTo(ip.c_str(), rack, slot) != 0) return false;
running_ = true;
thread_ = std::thread([this] { PollLoop(); });
return true;
}
void Stop() { running_ = false; if (thread_.joinable()) thread_.join(); client_.Disconnect(); }
private:
void PollLoop() {
while (running_) {
uint8_t db1[512], mArea[64];
bool ok = true;
ok &= client_.DBRead(1, 0, sizeof(db1), db1) == 0; // 批量整块
ok &= client_.MBRead(0, sizeof(mArea), mArea) == 0; // M 区整块
if (ok) { /* 解析 db1/mArea → 上抛(Kafka/时序库/共享内存) */ }
std::this_thread::sleep_for(std::chrono::milliseconds(500)); // 500ms 周期
}
}
TS7Client client_;
std::thread thread_;
std::atomic<bool> running_{false};
};
工程要点:单连接单线程轮询 (TS7Client 非线程安全)、整块批量读 、固定周期 、断线指数退避重连。
3.7 gos7 完整采集示例(Go 侧对照)
cpp
func main() {
handler := gos7.NewTCPClientHandler("192.168.0.1", 0, 2)
handler.Timeout = 5 * time.Second
handler.IdleTimeout = 30 * time.Second
handler.SetConnectionType(1)
if err := handler.Connect(); err != nil {
log.Fatalf("connect: %v", err)
}
defer handler.Close()
client := gos7.NewClient(handler)
helper := gos7.NewHelper()
ticker := time.NewTicker(500 * time.Millisecond)
for range ticker.C {
buf := make([]byte, 256)
if err := client.DBRead(1, 0, 256, buf); err != nil {
log.Printf("DBRead failed: %v", err)
continue
}
temp := helper.GetReal(buf[0:4]) // DB1.DBD0 float32
pressure := helper.GetReal(buf[4:8]) // DB1.DBD4 float32
counter := helper.GetUInt(buf[8:10]) // DB1.DBW8 uint16
fmt.Printf("temp=%.2f pressure=%.2f counter=%d\n", temp, pressure, counter)
}
}
4. 常错点(坑)
4.1 高频踩坑 TOP 20
| 坑 | 现象 | 解法 | |
|---|---|---|---|
| 1 | 忘设连接类型 | 连 S7-1200/1500 报 errNegotiatingPDU 或一直失败 | SetConnectionType(CONNTYPE_PG) / gos7 handler.SetConnectionType(1) |
| 2 | TSAP 计算错误 | 300/400 rack/slot 填错、或把 1200/1500 也按 rack/slot 推导 | 300/400 用 rack/slot(0,2 典型);1200/1500 走固定 TSAP;复杂拓扑 SetConnectionParams 手动指定 |
| 3 | PLC 侧未开 PUT/GET | 连接成功但读写超时/拒绝 | TIA 属性→防护与安全→连接机制→允许 PUT/GET 通信访问 |
| 4 | 优化块访问未取消 | 1200/1500 DB 按偏移读回全 0 或报错 | DB 属性取消"优化的块访问",或改用符号寻址方案 |
| 5 | 字节序反了 | 浮点数读出天文数字、整数错位 | S7 固定大端,x86 需重组;用 gos7 helper / 自封装 s7_to_host |
| 6 | Start 单位理解错 | 位读写用字节偏移 | 位地址 = 字节号 × 8 + 位号;字节/字/双字用字节偏移 |
| 7 | Amount 与 WordLen 不匹配 | 读多读少、数据错位 | WordLen=Byte 时 Amount 为字节数;Word 时 Amount 为字数(×2 字节) |
| 8 | DB 编号从 0 或 1 混淆 | 读错块 | S7 DB 编号从 1 开始(DB1, DB2...),DB0 不存在 |
| 9 | 读返回成功但数据不变 | 地址越界被 PLC 截断 | 核对 DB 实际长度与偏移,越界部分返回 0 不报错 |
| 10 | 定时器/计数器当整数读 | 值完全不对 | 需 BCD/时基解码(见 3.4) |
| 11 | 多线程共享一个 TS7Client | 偶发超时、帧错乱、崩溃 | 每个线程独立连接,或外部加互斥串行化 |
| 12 | 轮询频率过高 | CPU 负荷飙升、连接被踢 | 批量读 + 500ms~1s 周期 + 变化率阈值上报 |
| 13 | 逐字节小报文读取 | 网络开销大、周期拉长 | 一次整块读入本地缓冲再解析 |
| 14 | 不检查返回码 | 断线后静默丢数据 | 每次调用检查 err != 0,记录并触发重连 |
| 15 | 无断线重连逻辑 | 采集程序跑几天后停止上报 | 指数退避重连(1s→2s→4s→...→30s 封顶) |
| 16 | ISO-on-TCP 端口 102 被防火墙挡 | 本机通、跨网段不通 | 放行 102/TCP(PC 侧防火墙与 PLC 侧路由) |
| 17 | 连接资源被占满 | 偶发连不上,重启 WinCC 后恢复 | 控制连接数,常驻连接复用,不频繁 Connect/Disconnect |
| 18 | S7-1200/1500 访问保护密码 | 连接后读写返回权限错误 | SetSessionPassword / PLC 属性关闭保护 |
| 19 | CPU 在 STOP 状态 | 读 DB 返回正常但数据冻结 | 先 GetPlcStatus 判断,STOP 时走诊断/报警通道 |
| 20 | gos7 IdleTimeout 未设置 | 空闲后被 PLC 断开,首次重连慢 | handler.IdleTimeout = 30 * time.Second |
4.2 典型报错速查
| 错误码(C API) | 含义 | 处理 |
|---|---|---|
| errTCPConnectionFailed (-10409) | TCP 连不上 102 | 网络/防火墙/IP 可达性 |
| errNegotiatingPDU (-10415) | COTP/PDU 协商失败 | 连接类型、TSAP、PLC 侧 PUT/GET |
| errCliCannotConnect | 无法建立 S7 会话 | 连接资源、保护等级 |
| errCliTimeout | 请求超时 | 网络质量、CPU 忙、轮询过频 |
| errCliInvalidBlockNumber | DB 号非法 | DB 编号从 1 起 |
5. 性能清单
- 整块读取:一个 DB 用一次 DBRead 拉全量,不要按变量逐字节读(报文往返是最大开销);
- 合理轮询周期:一般数据 500ms~1s,高速数据(<50ms)单独通道,避免全量高频;
- 连接复用:常驻单连接,禁止每次采集新建/断开(S7 建链要 COTP CR/CC + PDU 协商 3~5 个往返);
- 多 PLC 并行:每个 PLC 独立连接 + 独立采集线程/goroutine,互不阻塞;
- 解析前置:把 DB 布局映射为固定偏移结构体,读回后一次 memcpy/helper 解析,避免逐字段多次回调;
- 按需分区:热数据与冷数据分 DB 或分偏移段,冷段降低采样率;
- 写入收敛:写操作合并为一次 DBWrite(连续区)或降低写频率,写 M 字比写单个位报文更省;
- 监控资源:GetPlcStatus + 错误计数统计,断线自动退避,防止重连风暴;
- CI 用 TS7Server 仿真:联调/回归不依赖真机,避免现场反复试错;
- 缓存到本地缓冲:采集线程只写环形缓冲,上抛(Kafka/TDengine/MQTT)另线程异步,避免网络抖动拖垮采集周期。
6. FAQ 速查表
| 问题 | 答案 |
|---|---|
| snap7 支持哪些 PLC? | S7-200/300/400/1200/1500 及兼容 S7 协议的第三方设备;部分老型号(S7-200)需注意区域差异 |
| 连 S7-1200/1500 为什么连不上? | 90% 是 PLC 侧未勾选"允许 PUT/GET 通信访问"或连接类型未设 PG/OP |
| rack/slot 怎么填? | S7-300 典型 (0, 2);S7-400 按机架配置(如 0/2/3/4);1200/1500 走固定 TSAP,填 (0, 1) 即可 |
| TSAP 可以手动指定吗? | 可以:SetConnectionParams(ip, localTSAP, remoteTSAP);高字节 0x01=本地/0x02=OP/0x03=PG |
| S7 的字节序? | 大端(Big-Endian),与 x86 小端相反,浮点/整型必须转换 |
| 怎么读单个位? | ReadArea(S7AreaMK, 0, byte*8+bit, 1, S7WLBit, &data) |
| 定时器/计数器怎么读? | ReadArea(..., S7WLTimer/S7WLCounter, ...) 后做 BCD/时基解码 |
| gos7 与 C++ API 差异? | gos7 用 handler(TCP)+ client(协议)两层;NewHelper() 提供字节序解析;语义一一对应 |
| 多线程安全吗? | TS7Client 非线程安全;每线程独立连接或加锁串行 |
| 可以同时连多台 PLC 吗? | 可以,每台一个独立 TS7Client 实例 + 独立轮询线程 |
| 无 PLC 怎么测试? | TS7Server 注册内存区仿真 PLC,客户端连 127.0.0.1:102 |
| 与 OPC UA / Modbus 网关的区别? | snap7 是寄存器/字节级直连(底层传输);OPC UA 是语义化信息模型;Modbus 是通用总线协议;三者在采集栈中常组合使用 |
7. 总结
- snap7 是西门子 S7 PLC 以太网数采的免费开源首选:纯 C++ 零依赖、跨平台、三端模型齐全,覆盖 300/400/1200/1500 全系;
- 核心心智模型:ISO-on-TCP(端口 102)→ COTP(TSAP 建链)→ S7 PDU(Job 读写);TSAP 与 rack/slot 是连接成败的关键;
- API 三句话:ConnectTo(ip, rack, slot) 建连 → DBRead/MBRead/ReadArea 按区域+偏移+数量+字长读取 → 大端解析(gos7 用 NewHelper());
- 最大坑点群:连接类型未设、TSAP 推导错、优化块访问未关、字节序反、位地址算错、多线程共享连接、无重连逻辑;
- 工程落地建议:整块批量读 + 固定周期 + 单连接单线程 + 指数退避重连 + 共享缓冲异步上抛,与 libmodbus(Modbus 通用总线)、open62541(OPC UA 语义化)、三菱 MC 协议(三菱设备)、FANUC FOCAS(FANUC CNC)一起构成完整的「PLC/CNC 直连采集 → 边缘网关 → MQTT/Kafka → 时序库 → 分析/AI」工业数采技术栈。