Micro Property: serializador de datos binarios minimalista para sistemas integrados

Micro Property es una biblioteca para serializar datos con una sobrecarga mínima. Está diseñado para su uso en microcontroladores y una variedad de dispositivos integrados con limitación de memoria que tienen que operar en líneas de comunicación de baja velocidad.



Por supuesto, conozco formatos como xml, json, bson, yaml, protobuf, Thrift, ASN.1. Incluso encontré un árbol exótico, que en sí mismo es un asesino de JSON, XML, YAML y otros como ellos .



Entonces, ¿por qué no encajaban todos? ¿Por qué me vi obligado a escribir otro serializador?



Después de la publicación del artículo en los comentarios, dieron varios enlaces a los formatos CBOR , UBJSON y MessagePack que me perdí . Y es probable que resuelvan mi problema sin escribir una bicicleta.

Es una pena que no pude encontrar estas especificaciones antes, así que agregaré este párrafo para los lectores y para mi propio recordatorio de no apresurarme a escribir código ;-)

Reseñas de formatos en Habré: CBOR , UBJSON



imagen





Requisitos iniciales



Imagine que necesita modificar un sistema distribuido que consta de varios cientos de dispositivos de diferentes tipos (más de diez tipos de dispositivos que realizan diferentes funciones). Se combinan en grupos que intercambian datos entre sí a través de líneas de comunicación en serie utilizando el protocolo Modbus RTU.



Además, algunos de estos dispositivos están conectados a una línea de comunicación CAN común, que proporciona transferencia de datos dentro de todo el sistema en su conjunto. La velocidad de transferencia de datos en la línea de comunicación Modbus es de hasta 115200 baudios, y la velocidad en el bus CAN está limitada a la velocidad de hasta 50 kbaudios debido a su longitud y a la presencia de interferencias industriales graves.



La inmensa mayoría de los dispositivos se desarrollan en microcontroladores de las series STM32F1x y STM32F2x. Aunque algunos de ellos también funcionan en STM32F4x. Y, por supuesto, sistemas basados ​​en Windows / Linux con microprocesadores x86 como controladores de nivel superior.



Para estimar la cantidad de datos que se procesan y transmiten entre dispositivos o se almacenan como configuraciones / parámetros operativos: En un caso - 2 números de 1 byte y 6 números de 4 bytes, en el otro - 11 números de 1 byte y 1 número de 4 bytes y etc. Como referencia, el tamaño de los datos en una trama CAN estándar es de hasta 8 bytes y en una trama Modbus, hasta 252 bytes de carga útil.



Si aún no ha penetrado en la profundidad del agujero del conejo, agregue a estos datos de entrada: la necesidad de realizar un seguimiento de las versiones de protocolo y las versiones de firmware para diferentes tipos de dispositivos, así como el requisito de mantener la compatibilidad no solo con los formatos de datos existentes actualmente, sino también para garantizar la unión trabajo de dispositivos con generaciones futuras, que tampoco se detienen y están en constante evolución y reelaboración a medida que se desarrolla la funcionalidad y se encuentran jambas en las implementaciones. Además, interacción con sistemas externos, ampliación de requisitos, etc.



Inicialmente, debido a los recursos limitados y las bajas velocidades de las líneas de comunicación, se utilizó un formato binario para el intercambio de datos, que estaba vinculado solo a los registros Modbus. Pero tal implementación no pasó la primera prueba de compatibilidad y extensibilidad.



Por lo tanto, al rediseñar la arquitectura, fue necesario abandonar el uso de registros Modbus estándar. Y ni siquiera porque se utilicen otras líneas de comunicación además de este protocolo, sino por la organización excesivamente limitada de las estructuras de datos basadas en registros de 16 bits.



De hecho, en el futuro, con la evolución inevitable del sistema, puede ser necesario (y de hecho, ya era necesario) transferir cadenas de texto o matrices. En teoría, también se pueden mostrar en el mapa de registros Modbus, pero esto resulta ser aceite, porque viene la abstracción sobre la abstracción.



Por supuesto, puede transferir datos como un blob binario con referencia a la versión del protocolo y el tipo de bloque. Y aunque a primera vista esta idea puede parecer acertada, después de fijar ciertos requisitos para la arquitectura, puede definir formatos de datos de una vez por todas, ahorrando así significativamente los costos generales que serán inevitables al usar formatos como XML o JSON.



Para facilitar la comparación de opciones, me hice la siguiente tabla:
:

:



  • . , .


:



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


:



:

  • .


:

  • . , .
  • , , .




E imagine cómo varios cientos de dispositivos comienzan a intercambiar datos binarios entre sí, incluso con la vinculación de cada mensaje a la versión del protocolo y / o al tipo de dispositivo, entonces la necesidad de usar un serializador con campos con nombre se vuelve inmediatamente obvia. Después de todo, incluso una simple interpolación de la complejidad de respaldar una solución de este tipo en su conjunto, aunque después de muy poco tiempo, lo obliga a agarrar su cabeza.



Y esto, incluso sin tener en cuenta los deseos esperados del cliente de aumentar la funcionalidad, la presencia de jambas obligatorias en la implementación y mejoras "menores", a primera vista, que sin duda traerán consigo un sabor especial de la búsqueda de jambas recurrentes en el trabajo bien coordinado de tal zoológico ...



imagen



¿Cuales son las opciones?



Después de tal razonamiento, involuntariamente llega a la conclusión de que es necesario desde el principio establecer una identificación universal de datos binarios, incluso cuando se intercambian paquetes a través de líneas de comunicación de baja velocidad.



Y cuando llegué a la conclusión de que no se puede prescindir de un serializador, primero miré las soluciones existentes que ya han demostrado ser las mejores y que ya se utilizan en muchos proyectos.



Los formatos básicos xml, json, yaml y otras variantes de texto con una sintaxis formal muy conveniente y simple, que es muy adecuada para procesar documentos y, al mismo tiempo, conveniente para la lectura y edición por humanos, tuvieron que eliminarse de inmediato. Y solo por su conveniencia y simplicidad, tienen una sobrecarga muy grande al almacenar datos binarios, que solo necesitaban ser procesados.



Por lo tanto, en vista de los recursos limitados y las líneas de comunicación de baja velocidad, se decidió utilizar un formato de presentación de datos binarios. Pero incluso en el caso de formatos que pueden convertir datos a una representación binaria, como Protocol Buffers, FlatBuffers, ASN.1 o Apache Thrift, la sobrecarga de serializar datos, así como la facilidad general de uso, no contribuyó a la implementación inmediata de ninguna de estas bibliotecas.



El formato BSON, que tiene una sobrecarga mínima, fue el mejor ajuste para el conjunto de parámetros. Y consideré seriamente usarlo. Pero como resultado, decidió abandonarlo, ya que en igualdad de condiciones, incluso BSON tendrá costos generales inaceptables.

Puede parecer extraño para algunos que tenga que preocuparse por una docena de bytes adicionales, pero desafortunadamente, esta docena de bytes tendrá que transmitirse cada vez que se envíe un mensaje. Y en el caso de trabajar en líneas de comunicación de baja velocidad, incluso diez bytes adicionales en cada paquete son importantes.



En otras palabras, cuando opera con diez bytes, comienza a contar cada uno de ellos. Pero junto con los datos, también se transmiten a la red las direcciones de los dispositivos, las sumas de comprobación de paquetes y otra información específica de cada línea de comunicación y protocolo.

Que pasó



Como resultado de la deliberación y varios experimentos, se obtuvo un serializador con las siguientes características y características:



  • La sobrecarga para datos de tamaño fijo es de 1 byte (sin contar la longitud del nombre del campo de datos).
  • , , — 2 ( ). , CAN Modbus, .
  • — 16 .
  • , , .. . , 16 .
  • (, ) — 252 (.. ).
  • — .
  • . .
  • « », , . , , - ( 0xFF).
  • . , . .
  • , . .




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


Me gustaría anotar por separado



La implementación se realiza en C ++ x11 en un solo archivo de encabezado utilizando el mecanismo de plantillas SFINAE (La falla de sustitución no es un error).



Apoyado por la lectura correcta de los datos en el búfer (variable) b Un tamaño más grande que el tipo de datos almacenados. Por ejemplo, un número entero de 8 bits se puede leer en una variable de 8 a 64 bits. Estoy pensando, tal vez valdría la pena agregar un paquete de enteros, cuyo tamaño exceda los 8 bits, para que puedan transmitirse en un número menor.



Las matrices serializadas se pueden leer tanto copiando en el área de memoria especificada, como obteniendo una referencia normal a los datos en el búfer original, si desea evitar la copia, en los casos en que no sea necesario. Pero esta función debe usarse con precaución, porque las matrices de números enteros se almacenan en orden de bytes de red, que puede diferir entre máquinas.



Ni siquiera se planeó la serialización de estructuras u objetos más complejos. Generalmente es peligroso transferir estructuras en forma binaria debido a la posible alineación de sus campos. Pero si, no obstante, este problema se resuelve de una manera relativamente simple, seguirá existiendo el problema de convertir todos los campos de objetos que contienen números enteros al orden de bytes de la red y viceversa.



Además, en caso de emergencia, las estructuras siempre se pueden guardar y restaurar como una matriz de bytes. Naturalmente, en este caso, la conversión de números enteros deberá realizarse manualmente.



Implementación



La implementación está aquí: https://github.com/rsashka/microprop



Cómo usarlo está escrito en ejemplos con diferentes grados de detalle:



Uso rápido
#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); //  




Uso lento y reflexivo
#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);




Implementación de ejemplo usando enum como identificador de datos
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...);
  }
};




El código está publicado bajo la licencia MIT, así que utilícelo para su salud.



Estaré encantado de recibir comentarios, incluidos comentarios y / o sugerencias.



Actualización: no me equivoqué al elegir una imagen para el artículo ;-)



All Articles