[SECS/GEM研究] (六)SML与数据序列化

SECS/GEM 协议栈开发笔记 · 第 6 篇

基于代码库:Libsecs(一个 C++ 实现的 SEMI 协议栈)

说在前面:由于工作的原因,本人早些年开发了一套SEMI标准的协议栈用于公司的SECS标准实现,为了防止经验遗忘,特写下本专辑以免后续忘记。

上篇介绍了下 SECS-II 的 Item 树和 SxFy ,结尾说到SML ,以及"多块拆分"那点事。这一篇把把这两部分内容介绍完。

第五篇末尾说" SECS-II 自己还有一套大消息拆成多块传输的机制,跟 SECS-I 的 254 字节硬砍不是一回事"------这句严格讲不太准,先纠一下:SECS-II 本身其实不定义分块,它只管 Item 怎么长。真正动手"切"的,是下面两层的事。


一、为什么还要有 SML

光有 Item 树和十六进制字节够不够?够,但调试时会很不方便。

抓了一包数据,十六进制长这样:01 00 41 07 52 55 4E 4E 49 4E 47 ...。这其实是 <A "RUNNING"> ,但是一眼是看不出来的。线上跑着几百条消息,得有人能看得懂的方式来展示。

所以行业里约定了一个文本表示法 ,叫 SML(SECS Message Language)。它把那棵 Item 树写成带尖括号的嵌套文本,人眼一扫就懂。日志、抓包工具、模拟器、脚本测试全靠它------当然得说清楚:SML 不是 SEMI 标准强制的(SEMI 只规定了Item的字节长啥样),它是工具链和工程师之间的约定。几乎所有SECS工具都有支持和实现,我在Libsecs中也支持解析SML。


二、SML 语法长啥样

规则其实就一条骨架:每个 Item 写成 <类型名[长度] 值>

  • 类型名:L / A / B / BOOLEAN / I1~I8 / U1~U8 / F4 / F8,跟第五篇那张 format 表一一对应。
  • 长度 (方括号里):这个 Item 有几个元素。List 的元素是子项个数,A 的元素是字符数,整数/浮点是数值个数。
  • :字符串用双引号包;数值之间空格分隔;List 的子项直接写在大尖括号里面,层层嵌套。

拿第五篇那棵"设备状态"树举个完整例子:

text 复制代码
<L[4]
  <A[7] "RUNNING">
  <U1[1] 1>
  <F4[1] 98.6>
  <L[2]
    <U2[1] 500>
    <U2[1] 3>
  >
>

<A[7] "RUNNING"> 是 7 个字符的 ASCII;<U1[1] 1> 是 1 个无符号单字节;<L[2] ... > 里面又嵌了两个 U2。

这里有个小细节很多人会纠结:长度能不能不写 ?能。<A "RUNNING"><A[7] "RUNNING"> 都是合法的,解析器自己会数。一般来说写上会更完整更严谨。

注意:很多人以为 SML 是 SEMI 标准里白纸黑字定的,其实不是😭,它就是个"人话翻译层"。


三、 SML 怎么在代码中实现和落地

我在Libsecs里面的实现思路很简单------每个 Item 类型自己负责"怎么变成 SML 文本"和"从 SML 文本变回来",基类只留纯虚接口。

Item 基类定了两个关键方法:

cpp 复制代码
virtual std::string toSml() const = 0;                       // Item 树 → SML 文本
virtual void fromSml(const std::string& value) = 0;          // SML 文本 → Item 树
static std::shared_ptr<Item> createItem(const std::string& sml); // 静态工厂,一步到位

各自实现长这样:

  • A::toSml() 输出 <A[4] "RUNNING">,字符串带引号;
  • 整数类的 _integet_toSml() 输出 <U1[1] 1,数值空格分开;
  • L::toSml() 就是"写自己头 <L[N] + 对每个子项调它的 toSml() + 收尾 >",递归下去,整棵树压成一段 SML。

反方向,createItem(sml) 走的是"先 tokenize(切成 类型名/长度/体 三段),再按类型递归 createItem(token)"。L::fromSml() 就是把体里一个一个子 Item 抠出来 add 进自己。所以 libsecs 给的是一条完整的双向通道

text 复制代码
         toByteArray / SecsItemParserHelper::parse
Item 树  <-------------------------------------->  线字节(网线上跑的)
         toSml / createItem(fromSml)
Item 树  <-------------------------------------->  SML 文本(人看的)

调试时你既能"代码里建 Item 树 → 序列化发走",也能"从一段 SML 文本直接 createItem 还原成 Item 树"------这在脚本化测试时特别友好,不用真去构造字节。


四、序列化/反序列化:为什么解析端不用知道 schema

第五篇讲了 toByteArray() 怎么把树压成字节。这里补"反方向"------怎么把字节还原成树。

libsecs 里干这活的叫 SecsItemParserHelper::parse()。核心就一个 parser_map,按 format byte 高 6 位把每种类型分派给对应的解析函数:

cpp 复制代码
static ParserMap parser_map({
    { L::FORMAT_CODE,        &SecsItemParserHelper::parseL },
    { A::FORMAT_CODE,        &SecsItemParserHelper::parseA },
    { U1::FORMAT_CODE,       &SecsItemParserHelper::parseItem<U1> },
    // ... 其余 I/U/F/Boolean/B 类似
});

parse() 自己只做三件事:读 format byte → 按低 2 位算出长度字段占几字节 → 读出数据长度 → 拿 format code 去 map 里找解释器。

这套机制是"完全自描述"的------接收端不需要提前知道这条消息的 schema,看一眼字节流就能一棵棵 Item 还原出来。SECS-II 这套设计,到今天看,设计这套机制的专家还是挺聪明的。


五、多块拆分:到底谁在切

当年也以为"多块是 SECS-II 定义的",翻完标准才明白:SECS-II 只定义 Item,分块是下面两层的活

第一层:传输层物理分块(SECS-I)。

一条消息的 Item 字节一旦超过 244 字节,SECS-I 就动手切。

每块 = 10 字节头 + 最多 244 字节数据 = 254 字节 (这就是第五篇说的"254 上限"的真实来源)。最后一块在 block number 上或上 0x8000,告诉接收端"到头了"。这是任何消息都逃不掉的硬限制,跟 GEM 无关,纯物理:串口一个包就这么大。HSMS 通常用 4 字节长度前缀一次发完、不分块,但它头里照样保留了 block number + E-bit 字段------真要发超大消息,HSMS 也能多块,机制同源。

第二层:GEM 应用层 multi-block 授权(E30)。

这层才真正是"大消息流控"。当一条 GEM 业务消息(比如 S6F11 事件报告、S2F23 等)的数据体超过 244 字节,你不能闷头就发------得先问 host:"我这条 X 字节的大消息,你接收得了吗?"host 回"准"你再发。这是 E30 定的规矩,怕超大消息把对方缓冲区冲垮。

比如GEM的实现有这些细节:

  • SINGLE_BLOCK_LIMIT = 244------跟 SECS-I 对齐,同一道门槛;
  • requiresGrant(body):body 字节数 > 244 才需要走授权,小消息直接发;
  • 发大消息时 sendStream6Data() 先发 S6F5 (带 dataId + dataLength 询问),等 S6F6GRANTED 才发真数据;host 回 NO_SPACE / BUSY 就先憋着;
  • 接收方向 还会处理 host 主动来问的 S2F39 ,回 S2F40 授权;并且 S2F23 / S2F33 / S2F35 / S2F45 / S2F49 这几类"可能被塞超大内容"的消息,必须先 consumeGrant 通过才让进;
  • 防御参数也给了默认值:maxInboundBytes 128MB(太大直接拒)、grantTimeout 45 秒(问了 45 秒没回当作废)、maxPending 16(同时排队的授权最多 16 个)。

关键点:传输层"物理切",GEM 层"逻辑问"。≤244 的小消息,直接发、不用问;>244 的大消息,传输层照样切,但 GEM 层还要先取得 host 同意。上篇我说"HSMS 一条消息多大都能一次发完"其实也该补一句:能一次发是传输层的事,要不要"先问后发"是 GEM 层的规矩,两码事。


到这篇为止,协议栈自底向上四层(SECS-I → HSMS → SECS-II → SML/序列化)算是讲完了。我在实现 libsecs时,按照"数据怎么表达、怎么转换、怎么切"跟"怎么运"彻底解耦------Item 树一个字节都不用改,按照这套架构思维实现的secs协议栈,用户用串口还是以太网,切不切,都无所谓。需求变化后完全可以不用修改底层代码。


六、小结

  1. SML 是消息的文本表示,不是 SEMI 强制标准,但是行业通用约定;
  2. 语法骨架 <类型名[长度] 值>,字符串带引号、数值空格分、List 递归嵌套,长度可写可不写;
  3. 分块是两层的活:SECS-I 按 244 字节物理切 (254 封顶,末块打 E-bit),GEM 层对 >244 字节消息先问后发(S6F5/S6F6,或 S2F39/S2F40);
  4. 两个 244 是同一道门槛的两面,小消息直接发、大消息既要切也要问。

下一篇分析下SECS协议中的事务管理------一次"问---答"是怎么被托管的。前面一直在讲单条消息,可真实场景是"S1F1 问出去,得等 S1F2 回来才算完",中间这条消息归谁管、超时了怎么办、同时问一百条怎么不乱,这里涉及到 T3 超时和事务状态机,这是下一篇要分析的。

本人不是 SEMI 专家,上面这些也是当年啃 E5/E30 文档一点点试出来的,哪里理解有偏差,评论区直接拍我。

相关推荐
_小柏_1 小时前
总结下最近面试出现的问题
c++·面试
我是慎独1 小时前
VulkanSceneGraph学习教程(十一)
c++·学习
stolentime2 小时前
AT_agc066_a [AGC066A] Adjacent Difference题解
c++·算法·贪心算法·构造
程序猿编码2 小时前
不用PyTorch,我用C++手写了一个能认手写数字的神经网络
c++·pytorch·神经网络·大模型
不会代码的小猴2 小时前
4. 控件学习2
开发语言·c++·笔记·qt
闻道且行之3 小时前
图片处理助手|C++ 手搓离线 AI 抠图工具,U2Net 原理到落地一次讲透
开发语言·c++·人工智能·神经网络·opencv·计算机视觉
玖釉-3 小时前
nvpro_core2 源码与架构解析:NVIDIA Vulkan 图形开发基础框架
c++·windows·图形渲染
啦啦啦啦啦zzzz4 小时前
高级I/O函数(一)
linux·服务器·网络·c++·网络编程
笨鸟先飞的橘猫4 小时前
c++游戏后端开源框架学习——wukong(十、lua热更新)
c++·游戏·开源