微观属性-嵌入式系统的简约二进制数据串行器

Micro Property是一个用于以最小的开销序列化数据的库。它设计用于微控制器和必须在低速通信线路上运行的各种受存储器限制的嵌入式设备。



当然,我知道诸如xml,json,bson,yaml,protobuf,Thrift,ASN.1等格式。我什至发现了一棵奇异的树,它本身就是JSON,XML,YAML和其他类似语言的杀手



那么为什么它们都不适合呢?为什么我被迫写另一个序列化器?



在评论中发表文章之后,他们给出了我错过的CBORUBJSONMessagePack格式的几个链接他们很可能无需编写自行车就能解决我的问题。

遗憾的是我无法早些找到这些规范,因此我将为读者添加此段,并提醒我不要急于编写代码;-)。

Habré格式的评论:CBORUBJSON



图片





初始要求



设想您需要修改一个分布式系统,该分布式系统包含数百种不同类型的设备(十多种类型的设备执行不同的功能)。它们被组合在一起,使用Modbus RTU协议通过串行通信线路相互交换数据。



同样,这些设备中的某些设备连接到一条公共的CAN通信线路,从而在整个系统内提供了整个数据传输。 Modbus通信线上的数据传输速率高达115200 Baud,并且由于CAN总线的长度和严重的工业干扰,其CAN总线的速度被限制为最高50kBaud。



绝大多数设备是在STM32F1x和STM32F2x系列微控制器上开发的。尽管其中一些也可以在STM32F4x上运行。当然,基于Windows / Linux的系统具有x86微处理器作为顶级控制器。



要估计在设备之间处理和传输或存储为设置/操作参数的数据量:一种情况-2个1字节的数字和6个4字节的数字,在另一种情况下-11个1字节和1个4字节数字,以及等等作为参考,标准CAN帧中的数据大小最大为8个字节,而在Modbus帧中,最大数据大小为252个字节。



如果您还没有深入到兔子洞的深度,请在此输入数据中添加以下内容:跟踪不同类型设备的协议版本和固件版本,以及保持与当前现有数据格式的兼容性以及确保联合的要求。随着功能的发展和实现中的障碍,下一代设备的工作也不会停滞不前,并且会不断发展和返工。另外,与外部系统的交互,需求扩展等。



最初,由于资源有限和通信线路速度较慢,使用二进制格式进行数据交换,该格式仅与Modbus寄存器绑定。但是这样的实现没有通过兼容性和可扩展性的第一个测试。



因此,在重新设计架构时,有必要放弃使用标准Modbus寄存器。甚至不是因为除了此协议之外还使用了其他通信线路,而是因为基于16位寄存器的数据结构的组织过于有限。



的确,在将来,随着系统的不可避免的发展,可能需要(实际上已经需要)传输文本字符串或数组。从理论上讲,它们也可以显示在Modbus寄存器图上,但这实际上是油,因为来自抽象而非抽象。



当然,您可以参考协议版本和块类型将数据作为二进制Blob传输。尽管乍一看,这个想法似乎是正确的,因为通过固定对体系结构的某些要求,您可以一劳永逸地定义数据格式,从而大大节省了使用XML或JSON等格式时不可避免的开销成本。



为了更轻松地比较选项,我为自己制作了下表:
:

:



  • . , .


:



  • , .
  • . , .
  • . , , . , .
  • , .


:



:

  • .


:

  • . , .
  • , , .




试想一下,即使有每条消息绑定到协议版本和/或设备类型,也有数百种设备开始彼此交换二进制数据,然后立即需要使用带有命名字段的序列化程序。毕竟,即使是很短的时间,即使是对支持整个解决方案的复杂性进行简单的插值,也会迫使您抓狂。



而且,即使不考虑客户增加功能性的期望,实施中存在强制性门框以及乍看之下的“次要”改进,这无疑也会给他们带来特别的刺激,他们在这种动物园协调良好的工作中寻找反复出现的门框...



图片



有什么选择?



经过这样的推理,您不由自主地得出结论,即从一开始就需要对二进制数据进行通用标识,包括在低速通信线路上交换数据包时。



当我得出一个结论,即没有一个序列化器就无法做到这一点时,我首先查看了已经从最佳方面证明了自己的现有解决方案,并且这些解决方案已在许多项目中使用。



必须立即删除基本格式xml,json,yaml和其他文本变体,它们具有非常方便和简单的形式语法,该语法非常适合处理文档,同时也方便人类阅读和编辑。只是由于它们的方便性和简单性,它们在存储需要处理的二进制数据时具有非常大的开销。



因此,鉴于资源有限和低速通信线路,决定使用二进制数据表示格式。但是即使在可以将数据转换为二进制表示形式的格式(例如协议缓冲区,FlatBuffers,ASN.1或Apache Thrift)的情况下,数据序列化的开销以及使用它们的一般便利性也无法直接实现任何这些库的实现。



具有最小开销的BSON格式最适合参数组合。我认真考虑过使用它。但是结果是,我仍然决定放弃它,因为在所有其他条件都相同的情况下,即使BSON也会产生不可接受的间接费用。

对于某些人来说,您不得不担心一打额外的字节似乎有些奇怪,但是不幸的是,每次发送消息都必须发送这十二个字节在使用低速通信线路的情况下,每个消息甚至另外十个字节也很重要。



换句话说,当您使用十个字节进行操作时,便开始对每个字节进行计数。但是,与数据一起,设备地址,数据包校验和以及每个通信线路和协议特有的其他信息也将传输到网络。

发生了什么



经过深思熟虑和几次实验,获得了具有以下特征的串行器:



  • 固定大小的数据的开销为1个字节(不计算数据字段名称的长度)。
  • , , — 2 ( ). , CAN Modbus, .
  • — 16 .
  • , , .. . , 16 .
  • (, ) — 252 (.. ).
  • — .
  • . .
  • « », , . , , - ( 0xFF).
  • . , . .
  • , . .




  • 8 64 .
  • .
  • ( ).
  • — . , , . ;-)
  • . , .


我想分开记



该实现是使用SFINAE(替代失败不是错误)模板机制在单个头文件中的C ++ x11中完成。



通过正确读取缓冲区(变量)中的数据来支持b大约比存储的数据类型大ng。例如,可以将8位的整数读入8到64位的变量中。我认为可能值得添加大于8位的整数打包,以便可以较小的数字进行传输。



如果不需要复制,可以通过复制到指定的存储区或通过获取原始缓冲区中数据的常规引用来读取序列化的数组。但是应谨慎使用此功能,因为整数数组以网络字节顺序存储,计算机之间可能有所不同。



甚至没有计划对结构或更复杂的对象进行序列化。由于其字段可能对齐,因此以二进制形式传输结构通常很危险。但是,如果仍然以相对简单的方式解决此问题,则仍然存在将包含整数的对象的所有字段转换为网络字节顺序并返回的问题。



此外,在紧急情况下,始终可以将结构保存和恢复为字节数组。自然,在这种情况下,整数转换将需要手动完成。



实作



实现在这里:https : //github.com/rsashka/microprop



如何使用它在具有不同程度细节的示例中编写:



快速使用
#include "microprop.h"

Microprop prop(buffer, sizeof (buffer));//      

prop.FieldExist(string || integer); //      ID
prop.FieldType(string || integer); //    

prop.Append(string || integer, value); //  
prop.Read(string || integer, value); //  




缓慢而周到的使用
#include "microprop.h"

Microprop prop(buffer, sizeof (buffer)); //  

prop.AssignBuffer(buffer, sizeof (buffer)); //  
prop.AssignBuffer((const)buffer, sizeof (buffer)); //  read only 
prop.AssignBuffer(buffer, sizeof (buffer), true); //  read only 

prop.FieldNext(ptr); //     
prop.FieldName(string || integer, size_t *length = nullptr); //   ID 
prop.FieldDataSize(string || integer); //   

//   
prop.Append(string || blob || integer, value || array);
prop.Read(string || blob || integer, value || array);

prop.Append(string || blob || integer, uint8_t *, size_t);
prop.Read(string || blob || integer, uint8_t *, size_t);

prop.AppendAsString(string || blob || integer, string);
const char * ReadAsString(string || blob || integer);




使用枚举作为数据标识符的示例实现
class Property : public Microprop {
public:
    enum ID {
    ID1, ID2, ID3
  };

  template <typename ... Types>
  inline const uint8_t * FieldExist(ID id, Types ... arg) {
    return Microprop::FieldExist((uint8_t) id, arg...);
  }

  template <typename ... Types>
  inline size_t Append(ID id, Types ... arg) {
    return Microprop::Append((uint8_t) id, arg...);
  }

  template <typename T>
  inline size_t Read(ID id, T & val) {
    return Microprop::Read((uint8_t) id, val);
  }

  inline size_t Read(ID id, uint8_t *data, size_t size) {
    return Microprop::Read((uint8_t) id, data, size);
  }

    
  template <typename ... Types>
  inline size_t AppendAsString(ID id, Types ... arg) {
    return Microprop::AppendAsString((uint8_t) id, arg...);
  }

  template <typename ... Types>
  inline const char * ReadAsString(ID id, Types... arg) {
    return Microprop::ReadAsString((uint8_t) id, arg...);
  }
};




该代码是根据MIT许可发布的,因此请使用它以确保健康。



我很高兴收到任何反馈,包括评论和/或建议。



更新:我没有为文章选择图片是错误的;-)



All Articles