2. GEM通信协议基础:TCP/IP协议栈、Socket通信原理、GEM消息格式

各位好,我是老张。今天咱们聊GEM通信这块硬骨头。说实话,我刚入行那会儿,觉得协议栈这东西离应用层很远,直到第一次调试GEM联机——设备死活连不上主机,抓包一看,TCP三次握手都没完成。嗯,从那以后我老实了。

GEM通信说白了就三层:TCP/IP协议栈负责把数据从A搬到B,Socket是咱们写代码时直接调用的接口,GEM消息格式则是设备与主机之间约定好的“黑话”。这三层缺一不可,咱们一层层拆开看。

核心认知:GEM通信不是发明新协议,而是在TCP/IP之上定义了一套应用层消息规范。你只要搞懂Socket怎么收发字节流,再套上GEM的消息格式,就能跟任何SEMI标准设备对话。

2.1 TCP/IP协议栈——GEM通信的“高速公路”

GEM为什么选TCP/IP?我个人的理解是:它成熟、稳定、跨平台。你想想看,半导体工厂里设备来自天南海北,Windows、Linux、VxWorks都有,TCP/IP是它们唯一通用的“语言”。

TCP/IP协议栈分四层,GEM主要跟传输层和应用层打交道:

层级 典型协议 GEM中的角色
应用层 GEM/SECS-II 定义消息格式、会话管理、状态机
传输层 TCP 可靠连接、数据分段、流量控制
网络层 IP 寻址、路由(设备IP由工厂IT分配)
链路层 以太网 物理传输(通常走1000BASE-T)

这里有个坑,我提醒一下:GEM要求使用TCP,不是UDP。为什么?因为GEM消息必须保证按序到达、不丢包。UDP虽然快,但丢了包你得自己重传,那代码复杂度就上去了。SEMI标准选TCP,说白了就是“把可靠的事交给协议栈,咱们专心搞业务”。

实战经验:我在项目里见过有人为了“性能”改用UDP+自定义重传,结果调试了两个月,最后还是换回TCP。别重复造轮子,TCP的拥塞控制和重传机制比你自己写的强得多。

2.2 Socket通信原理——代码层面的“电话线”

Socket是什么?你可以把它想象成一根电话线。设备端拨号(connect),主机端接听(accept),然后两边对着话筒说话(send/recv)。挂断(close)时,对方就知道了。

GEM通信中,设备通常作为客户端,主机作为服务器。为什么这么设计?因为设备数量多、IP不固定,主机作为服务端更容易管理连接。我习惯把Socket通信拆成四个步骤:

  1. 创建Socket —— 指定协议族(AF_INET)、类型(SOCK_STREAM)
  2. 建立连接 —— 客户端connect,服务端accept
  3. 收发数据 —— 用send/recv读写字节流
  4. 关闭连接 —— 优雅关闭,避免数据丢失

来看一段典型的GEM设备端Socket初始化代码(C语言风格):

// 设备端:创建TCP Socket并连接到主机
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
    // 嗯,socket创建失败,检查权限或资源
    return -1;
}

struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(5000);  // GEM默认端口5000
inet_pton(AF_INET, "192.168.1.100", &server_addr.sin_addr);

// 连接主机,超时建议设10秒
if (connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
    // 连接失败,可能是主机没启动或网络不通
    close(sock);
    return -1;
}

// 连接成功,开始收发GEM消息
// ...

注意:GEM标准规定,设备必须在启动后主动连接主机。如果连接断开,设备需要自动重连。我曾经遇到一个设备,重连间隔设成了1秒,结果把主机CPU打满了。建议重连间隔至少3秒,并且加上指数退避。

2.3 GEM消息格式——设备与主机的“黑话”

TCP/IP和Socket只是管道,真正的内容是GEM消息。GEM消息格式定义在SEMI E5(SECS-II)标准中,我把它总结成三个核心概念:消息头、数据项、消息类型。

2.3.1 消息头(Message Header)

每条GEM消息都有一个10字节的固定头部,结构如下:

偏移 长度 字段 说明
0 2 Length 整条消息长度(包含头部)
2 1 Device ID 设备ID,通常为0
3 1 Stream 流号(0-127)
4 1 Function 功能号(0-255)
5 1 PType 包类型,GEM固定为0
6 1 SType 会话类型,数据消息为0
7 2 System Bytes 系统字节,用于消息匹配
9 1 Header Byte 9 备用/扩展

这里有个关键点:Stream和Function共同决定消息的语义。比如S1F13是“建立通信请求”,S1F14是“建立通信确认”。我刚开始记这些编号时头都大了,后来发现规律:奇数号是请求(Primary),偶数号是响应(Secondary)。

2.3.2 数据项(Data Item)

消息头后面跟着的就是数据。GEM使用一种叫SML(SECS Message Language)的格式来编码数据。说白了就是:每个数据项前面加一个类型标签和长度。

常见的SML类型:

  • L —— 列表(List),可以嵌套
  • B —— 二进制(Binary)
  • BOOLEAN —— 布尔值
  • A —— ASCII字符串
  • I1/I2/I4/I8 —— 有符号整数(1/2/4/8字节)
  • U1/U2/U4/U8 —— 无符号整数
  • F4/F8 —— 浮点数

举个例子,一条S1F13消息(建立通信请求)的完整报文大概是这样的:

// S1F13 建立通信请求 (十六进制)
00 1A  // Length = 26字节
00     // Device ID = 0
01     // Stream = 1
0D     // Function = 13 (0x0D)
00     // PType = 0
00     // SType = 0
00 01  // System Bytes = 1
00     // Header Byte 9 = 0
// 数据部分:空列表
04 00  // L, 0个元素

个人习惯:调试GEM消息时,我从来不看十六进制。我会用Wireshark的SECS-II解析器,或者写个小工具把报文转成SML文本。你想想看,对着“04 00”猜意思,不如直接看“L[0]”来得痛快。

2.3.3 消息类型与事务

GEM消息分为两类:

  • Primary Message(主消息) —— 发起方发送,奇数Function号
  • Secondary Message(从消息) —— 响应方回复,偶数Function号

一个完整的事务就是“Primary + Secondary”配对。System Bytes字段就是用来匹配请求和响应的。设备发S1F13时带上System Bytes=1,主机回复S1F14时必须把System Bytes也设为1。这样两边都知道“哦,这条回复是对应那条请求的”。

避坑指南:我曾经犯过一个低级错误——重发消息时忘了更新System Bytes。结果主机收到两条相同System Bytes的请求,直接当成重复消息丢弃了。记住:每条新消息的System Bytes必须递增,通常用循环计数器实现。

2.4 知识体系总览

下面这张图把GEM通信的完整链路画出来了,从物理层到应用层,一目了然:

GEM通信协议栈与消息流转 应用层 GEM / SECS-II 消息 传输层 TCP (可靠连接) 网络层 IP (寻址路由) 链路层 以太网 (物理传输) Socket API socket() / connect() / send() / recv() GEM消息格式 10字节头部 + SML数据 Stream/Function/System Bytes 消息示例:S1F13 Stream=1, Function=13 System Bytes=1 数据: L[0] (空列表) 核心流程 设备(客户端) → 创建Socket → 连接主机 → 发送GEM消息 → 接收响应 → 关闭连接

2.5 本章小结

咱们把GEM通信的底牌都翻了一遍:

  • TCP/IP协议栈是基础设施,GEM站在应用层,依赖TCP的可靠性
  • Socket编程是咱们写代码的入口,记住设备主动连主机,重连要加退避
  • GEM消息格式由10字节头部+SML数据组成,Stream/Function决定语义,System Bytes匹配事务