J'ai du code sur un microcontrôleur Cortex-M4 et je souhaite communiquer avec un PC en utilisant un protocole binaire. Actuellement, j'utilise des structures compressées à l'aide de l'attribut packed
spécifique à GCC.
Voici un aperçu:
struct Sensor1Telemetry {
int16_t temperature;
uint32_t timestamp;
uint16_t voltageMv;
// etc...
} __attribute__((__packed__));
struct TelemetryPacket {
Sensor1Telemetry tele1;
Sensor2Telemetry tele2;
// etc...
} __attribute__((__packed__));
Ma question est:
TelemetryPacket
sur le MCU et l'application client, le code ci-dessus sera-t-il portable sur plusieurs plates-formes? (Je suis intéressé par x86 et x86_64, et en ai besoin pour fonctionner sur Windows, Linux et OS X.)MODIFIER :
Vous ne devez jamais utiliser de structures sur des domaines de compilation, par rapport à la mémoire (registres matériels, séparer les éléments lus dans un fichier ou passer des données entre des processeurs ou le même logiciel différent de processeur (entre une application et un pilote de noyau)). Vous demandez des ennuis car le compilateur a un peu de libre arbitre pour choisir l'alignement et l'utilisateur en plus peut aggraver la situation en utilisant des modificateurs.
Non, il n'y a aucune raison de supposer que vous pouvez le faire en toute sécurité sur toutes les plateformes, même si vous utilisez la même version du compilateur gcc par exemple contre différentes cibles (différentes versions du compilateur ainsi que les différences de cible).
Pour réduire vos risques d'échec, commencez par les éléments les plus gros en premier (64 bits, puis 32 bits, le 16 bits, puis enfin tous les éléments 8 bits). ainsi que la valeur par défaut peut être modifiée par celui qui construit le compilateur à partir des sources.
Maintenant, si c'est une question de sécurité d'emploi, allez-y, vous pouvez effectuer une maintenance régulière de ce code, vous aurez probablement besoin d'une définition de chaque structure pour chaque cible (donc une copie du code source pour la définition de la structure pour ARM et un autre pour x86, ou en aura besoin si ce n'est pas immédiatement.) Et ensuite, chaque ou quelques versions de produits vous seront appelés pour travailler sur le code ... Belles petites bombes à retardement de maintenance qui partent ...
Si vous souhaitez communiquer en toute sécurité entre des domaines de compilation ou des processeurs la même architecture ou des architectures différentes, utilisez un tableau d'une certaine taille, un flux d'octets, un flux de demi-mots ou un flux de mots. Réduit considérablement vos risques de panne et d'entretien sur la route. N'utilisez pas de structures pour séparer les éléments qui ne font que restaurer le risque et l'échec.
La raison pour laquelle les gens semblent penser que c'est correct en raison de l'utilisation du même compilateur ou de la même famille contre la même cible ou la même famille (ou des compilateurs dérivés d'autres choix de compilateurs), car vous comprenez les règles du langage et où les zones définies par l'implémentation vous sont-elles finira par rencontrer une différence, parfois cela prend des décennies dans votre carrière, parfois cela prend des semaines ... C'est le problème "fonctionne sur ma machine" ...
Compte tenu des plates-formes mentionnées, oui, les structures compressées sont parfaitement adaptées à l'utilisation. x86 et x86_64 ont toujours pris en charge l'accès non aligné, et contrairement à la croyance commune, l'accès non aligné sur ces plateformes a ( presque ) la même vitesse que l'accès aligné pendant une longue période (il n'y a rien de tel qu'un accès non aligné est beaucoup plus lent). Le seul inconvénient est que l'accès peut ne pas être atomique, mais je ne pense pas que ce soit important dans ce cas. Et il y a un accord entre les compilateurs, les structures compressées utiliseront la même disposition.
GCC/clang prend en charge les structures compressées avec la syntaxe que vous avez mentionnée. MSVC a #pragma pack
, qui peut être utilisé comme ceci:
#pragma pack(Push, 1)
struct Sensor1Telemetry {
int16_t temperature;
uint32_t timestamp;
uint16_t voltageMv;
// etc...
};
#pragma pack(pop)
Deux problèmes peuvent survenir:
movaps
ou ldrd
), alors vous pouvez avoir un plantage en utilisant ce pointeur (gcc ne vous en avertit pas, mais clang le fait).Voici le doc de GCC:
L'attribut emballé spécifie qu'un champ de variable ou de structure doit avoir le plus petit alignement possible - un octet pour une variable
GCC garantit donc qu'aucun remplissage ne sera utilisé.
MSVC:
Emballer une classe, c'est placer ses membres directement les uns après les autres en mémoire
MSVC garantit donc qu'aucun remplissage ne sera utilisé.
La seule zone "dangereuse" que j'ai trouvée est l'utilisation des champs de bits. La disposition peut alors différer entre GCC et MSVC. Mais, il y a une option dans GCC, qui les rend compatibles: -mms-bitfields
Astuce: même si cette solution fonctionne maintenant et qu'il est très peu probable qu'elle cesse de fonctionner, je vous recommande de garder une faible dépendance de votre code à l'égard de cette solution.
Remarque: J'ai considéré uniquement GCC, clang et MSVC dans cette réponse. Il y a peut-être des compilateurs pour lesquels ces choses ne sont pas vraies.
Si
alors oui, " les structures emballées " sont portables.
À mon goût, trop de "si", ne faites pas ça. Cela ne vaut pas la peine de se poser.
Vous pouvez le faire ou utiliser une alternative plus fiable.
Pour le noyau dur parmi les fanatiques de sérialisation, il y a CapnProto . Cela vous donne une structure native à gérer et s'engage à ce que, lorsqu'elle est transférée sur un réseau et légèrement travaillée, elle ait toujours du sens à l'autre extrémité. L'appeler une sérialisation est presque inexact; il vise à faire le moins possible la représentation en mémoire d'une structure. Peut être adapté au portage vers un M4
Il y a les tampons de protocole Google, c'est binaire. Plus ballonné, mais assez bon. Il y a le nanopb qui l'accompagne (plus adapté aux microcontrôleurs), mais il ne fait pas tout le GPB (je ne pense pas qu'il le fasse oneof
). Beaucoup de gens l'utilisent cependant avec succès.
Certains des temps d'exécution C asn1 sont suffisamment petits pour être utilisés sur des microcontrôleurs. Je sais celui-ci correspond à M0.
Si vous voulez quelque chose de portable au maximum, vous pouvez déclarer un tampon de uint8_t[TELEM1_SIZE]
Et memcpy()
vers et depuis les décalages en son sein, en effectuant des conversions d'endianité telles que htons()
et htonl()
(ou des équivalents en petits caractères tels que ceux de glib). Vous pouvez envelopper cela dans une classe avec des méthodes getter/setter en C++, ou une structure avec des fonctions getter-setter en C.
En parlant d'alternatives et en tenant compte de votre question conteneur de type tuple pour les données compressées (pour lequel je n'ai pas assez de réputation pour commenter), je suggère de jeter un œil à celui d'Alex Robenko CommsChampion projet:
COMMS est uniquement les en-têtes C++ (11), une bibliothèque indépendante de la plate-forme, ce qui rend la mise en œuvre d'un protocole de communication un processus facile et relativement rapide. Il fournit tous les types et classes nécessaires pour que la définition des messages personnalisés, ainsi que l'encapsulation des champs de données de transport, soient de simples déclarations déclaratives des définitions de type et de classe. Ces déclarations préciseront ce qui doit être mis en œuvre. Les internes de la bibliothèque COMMS gèrent la partie HOW.
Puisque vous travaillez sur un microcontrôleur Cortex-M4, vous pouvez également trouver intéressant:
La bibliothèque COMMS a été spécifiquement développée pour être utilisée dans les systèmes embarqués, y compris ceux en métal nu. Il n'utilise pas d'exceptions et/ou RTTI. Il minimise également l'utilisation de l'allocation dynamique de mémoire et offre la possibilité de l'exclure complètement si nécessaire, ce qui peut être nécessaire lors du développement de systèmes embarqués bare-metal.
Alex fournit un excellent ebook gratuit intitulé Guide to Implementing Communication Protocols in C++ (for Embedded Systems) qui décrit les éléments internes.
Cela dépend fortement de la structure, gardez à l'esprit qu'en C++ struct
est une classe avec visibilité par défaut publique.
Vous pouvez donc hériter et même ajouter du virtuel pour que cela puisse casser les choses pour vous.
S'il s'agit d'une classe de données pure (en termes C++ une classe de disposition standard ), cela devrait fonctionner en combinaison avec packed
.
Gardez également à l'esprit que si vous commencez à le faire, vous risquez d'avoir des problèmes avec les règles d'aliasing strictes de votre compilateur, car vous devrez regarder la représentation en octets de votre mémoire (-fno-strict-aliasing
est votre ami).
Remarque
Cela étant dit, je déconseille fortement d'utiliser cela pour la sérialisation. Si vous utilisez des outils pour cela (par exemple: protobuf, flatbuffers, msgpack ou autres), vous obtenez une tonne de fonctionnalités:
Voici un pseudo-code vers un algorithme qui peut répondre à vos besoins pour garantir l'utilisation avec le système d'exploitation et la plate-forme appropriés.
Si vous utilisez la langue C
, vous ne pourrez pas utiliser classes
, templates
et quelques autres choses, mais vous pouvez utiliser preprocessor directives
Pour créer la version de votre struct(s)
dont vous avez besoin en fonction du OS
, de l'architecte CPU-GPU-Hardware Controller Manufacturer {Intel, AMD, IBM, Apple, etc.}
, platform x86 - x64 bit
, et enfin du endian
de la disposition des octets . Sinon, l'accent serait mis ici sur le C++ et l'utilisation de modèles.
Prenez votre struct(s)
par exemple:
struct Sensor1Telemetry { int16_t temperature; uint32_t timestamp; uint16_t voltageMv; // etc... } __attribute__((__packed__)); struct TelemetryPacket { Sensor1Telemetry tele1; Sensor2Telemetry tele2; // etc... } __attribute__((__packed__));
Vous pouvez mettre en forme ces structures comme telles:
enum OS_Type {
// Flag Bits - Windows First 4bits
WINDOWS = 0x01 // 1
WINDOWS_7 = 0x02 // 2
WINDOWS_8 = 0x04, // 4
WINDOWS_10 = 0x08, // 8
// Flag Bits - Linux Second 4bits
LINUX = 0x10, // 16
LINUX_vA = 0x20, // 32
LINUX_vB = 0x40, // 64
LINUX_vC = 0x80, // 128
// Flag Bits - Linux Third Byte
OS = 0x100, // 256
OS_vA = 0x200, // 512
OS_vB = 0x400, // 1024
OS_vC = 0x800 // 2048
//....
};
enum ArchitectureType {
Android = 0x01
AMD = 0x02,
ASUS = 0x04,
NVIDIA = 0x08,
IBM = 0x10,
INTEL = 0x20,
MOTOROALA = 0x40,
//...
};
enum PlatformType {
X86 = 0x01,
X64 = 0x02,
// Legacy - Deprecated Models
X32 = 0x04,
X16 = 0x08,
// ... etc.
};
enum EndianType {
LITTLE = 0x01,
BIG = 0x02,
MIXED = 0x04,
// ....
};
// Struct to hold the target machines properties & attributes: add this to your existing struct.
struct TargetMachine {
unsigned int os_;
unsigned int architecture_;
unsigned char platform_;
unsigned char endian_;
TargetMachine() :
os_(0), architecture_(0),
platform_(0), endian_(0) {
}
TargetMachine( unsigned int os, unsigned int architecture_,
unsigned char platform_, unsigned char endian_ ) :
os_(os), architecture_(architecture),
platform_(platform), endian_(endian) {
}
};
template<unsigned int OS, unsigned int Architecture, unsigned char Platform, unsigned char Endian>
struct Sensor1Telemetry {
int16_t temperature;
uint32_t timestamp;
uint16_t voltageMv;
// etc...
} __attribute__((__packed__));
template<unsigned int OS, unsigned int Architecture, unsigned char Platform, unsigned char Endian>
struct TelemetryPacket {
TargetMachine targetMachine { OS, Architecture, Platform, Endian };
Sensor1Telemetry tele1;
Sensor2Telemetry tele2;
// etc...
} __attribute__((__packed__));
Avec ces identifiants enum
, vous pouvez alors utiliser class template specialization
Pour configurer ce class
à ses besoins en fonction des combinaisons ci-dessus. Ici, je prendrais tous les cas courants qui sembleraient bien fonctionner avec default
class declaration & definition
Et définirais cela comme la fonctionnalité de la classe principale. Ensuite, pour ces cas particuliers, tels que différents Endian
avec ordre des octets, ou des versions spécifiques du système d'exploitation faisant quelque chose d'une manière différente, ou GCC versus MS
Des compilateurs avec l'utilisation de __attribute__((__packed__))
par rapport à #pragma pack()
peut alors être les quelques spécialisations à prendre en compte. Vous ne devriez pas avoir besoin de spécifier une spécialisation pour chaque combinaison possible; cela serait trop intimidant et prendrait beaucoup de temps, ne devrait faire que les quelques rares cas qui peuvent se produire pour vous assurer d'avoir toujours les instructions de code appropriées pour le public cible. Ce qui rend également le enums
très pratique, c'est que si vous les passez comme argument de fonction, vous pouvez en définir plusieurs à la fois car ils sont conçus comme des indicateurs de bits. Donc, si vous souhaitez créer une fonction qui prend cette structure de modèle comme premier argument, puis les systèmes d'exploitation pris en charge en tant que second, vous pouvez ensuite passer tout le support du système d'exploitation disponible en tant que drapeaux de bits.
Cela peut aider à garantir que cet ensemble de packed structures
Est "compressé" et/ou aligné correctement en fonction de la cible appropriée et qu'il exécutera toujours les mêmes fonctionnalités pour maintenir la portabilité sur différentes plates-formes.
Maintenant, vous devrez peut-être effectuer cette spécialisation deux fois entre les directives du préprocesseur pour différents compilateurs de prise en charge. Tels que si le compilateur actuel est GCC car il définit la structure d'une manière avec ses spécialisations, alors Clang dans une autre, ou MSVC, blocs de code, etc. assurez-vous qu'il est correctement utilisé dans le scénario spécifié ou la combinaison d'attributs de la machine cible.