47

我在 Cortex-M4 微控制器上有一些代码,想使用二进制协议与 PC 通信。目前,我正在使用使用 GCC 特定packed属性的打包结构。

这是一个粗略的大纲:

struct Sensor1Telemetry {
    int16_t temperature;
    uint32_t timestamp;
    uint16_t voltageMv;
    // etc...
} __attribute__((__packed__));

struct TelemetryPacket {
    Sensor1Telemetry tele1;
    Sensor2Telemetry tele2;
    // etc...
} __attribute__((__packed__));

我的问题是:

  • 假设我对 MCU 和客户端应用程序上的结构使用完全相同的定义,TelemetryPacket上述代码是否可以跨多个平台移植?(我对 x86 和 x86_64 感兴趣,需要它在 Windows、Linux 和 OS X 上运行。)
  • 其他编译器是否支持具有相同内存布局的打包结构?用什么语法?

编辑

  • 是的,我知道打包结构是非标准的,但它们似乎很有用,可以考虑使用它们。
  • 我对 C 和 C++ 都感兴趣,尽管我认为 GCC 不会以不同的方式处理它们。
  • 这些结构不是继承的,也不会继承任何东西。
  • 这些结构仅包含固定大小的整数字段,以及其他类似的打包结构。(我以前被花车烧过……)
4

9 回答 9

37

考虑到上述平台,是的,打包结构完全可以使用。x86 和 x86_64 始终支持非对齐访问,并且与通常的看法相反,这些平台上的非对齐访问在很长一段时间内(几乎)具有与对齐访问相同的速度(不存在未对齐访问慢得多的事情)。唯一的缺点是访问可能不是原子的,但我认为在这种情况下并不重要。并且编译器之间有一个协议,打包的结构将使用相同的布局。

GCC/clang 支持使用您提到的语法的打包结构。MSVC 有#pragma pack,可以这样使用:

#pragma pack(push, 1)
struct Sensor1Telemetry {
    int16_t temperature;
    uint32_t timestamp;
    uint16_t voltageMv;
    // etc...
};
#pragma pack(pop)

可能会出现两个问题:

  1. 跨平台的字节序必须相同(您的 MCU 必须使用 little-endian)
  2. 如果您将指针分配给压缩结构成员,并且您使用的架构不支持未对齐访问(或使用具有对齐要求的指令,例如movapsor ldrd),那么使用该指针可能会导致崩溃(gcc 不'没有警告你这一点,但铿锵声)。

这是来自 GCC 的文档:

打包属性指定变量或结构字段应具有最小可能的对齐方式 - 变量一个字节

所以 GCC保证不会使用任何填充。

MSVC:

打包一个类就是将它的成员直接放在内存中

所以 MSVC保证不会使用任何填充。

我发现的唯一“危险”区域是位域的使用。那么 GCC 和 MSVC 之间的布局可能会有所不同。但是,GCC 中有一个选项可以使它们兼容:-mms-bitfields


提示:即使这个解决方案现在可以工作,而且它不太可能停止工作,我建议你保持你的代码对这个解决方案的依赖性低。

注意:我在这个答案中只考虑了 GCC、clang 和 MSVC。可能有编译器,但这些事情不是真的。

于 2017-07-15T09:39:34.103 回答
16

如果

  • 字节顺序不是问题
  • 两个编译器都正确处理打包
  • 两种 C 实现的类型定义都是准确的(符合标准)。

那么是的,“打包结构”是可移植的。

对于我的口味太多的“如果”,不要这样做。不值得麻烦出现。

于 2017-07-15T08:51:10.533 回答
8

您可以这样做,或者使用更可靠的替代方法。

对于那里的连载狂热者中的核心,有CapnProto。这为您提供了一个本地结构来处理,并保证当它通过网络传输并轻松处理时,它在另一端仍然有意义。称其为序列化几乎是不准确的。它旨在尽可能地对结构的内存表示做一些事情。可能适合移植到 M4

有谷歌协议缓冲区,它是二进制的。更臃肿,但还不错。附带的 nanopb (更适合微控制器),但它并没有完成整个 GPB(我认为它没有oneof)。很多人虽然成功地使用它。

一些 C asn1 运行时足够小,可以在微控制器上使用。我知道这个适合 M0。

于 2017-07-15T09:32:58.933 回答
7

你永远不应该在编译域中使用结构,而不是内存(硬件寄存器,从文件中分离读取的项目或在处理器或同一处理器不同软件之间传递数据(在应用程序和内核驱动程序之间))。你是在自找麻烦,因为编译器有一定的自由意志来选择对齐,然后用户可以通过使用修饰符使情况变得更糟。

不,没有理由假设您可以跨平台安全地执行此操作,即使您使用相同的 gcc 编译器版本,例如针对不同的目标(编译器的不同构建以及目标差异)。

为了降低失败的几率,首先从最大的项目开始(64 位,然后是 32 位,16 位,最后是任何 8 位项目)理想情况下,至少对齐 32 位,也许是 64,这是希望 arm 和 x86 做的,但这总是可以改变为以及从源代码构建编译器的任何人都可以修改默认值。

现在,如果这是一个工作安全问题,请确保继续,您可以对此代码进行定期维护,可能需要为每个目标定义每个结构(因此,ARM 结构定义的源代码副本和另一个对于 x86,或者如果不是立即需要,最终将需要它)。然后每一个或每几个产品发布,你都会被叫来处理代码......漂亮的小维护定时炸弹会爆炸......

如果您想在相同或不同架构的编译域或处理器之间安全地进行通信,请使用某种大小的数组、字节流、半字流或字流。显着降低故障和维护的风险。不要使用结构来分离那些只会恢复风险和失败的项目。

人们似乎认为这没问题的原因是因为您对相同的目标或系列(或从其他编译器选择派生的编译器)使用相同的编译器或系列,因为您了解语言的规则以及实现定义的区域在哪里最终会遇到不同,有时你的职业生涯需要几十年,有时需要几周......这是“在我的机器上工作”的问题......

于 2017-07-16T13:39:47.510 回答
2

如果您想要最大程度地可移植的东西,您可以在其中声明偏移量的缓冲区uint8_t[TELEM1_SIZE]memcpy()执行字节顺序转换,例如htons()htonl()(或小端等效项,例如 glib 中的那些)。您可以将其包装在 C++ 中具有 getter/setter 方法的类中,或者将其包装在 C 中具有 getter-setter 函数的结构中。

于 2017-07-15T19:26:36.127 回答
1

它在很大程度上取决于结构是什么,请记住,在 C++struct中是一个具有默认可见性公共的类。

所以你可以继承甚至添加 virtual ,这样这可能会破坏你的东西。

如果它是一个纯数据类(在 C++ 术语中是标准布局类),它应该与packed.

还要记住,如果你开始这样做,你可能会遇到编译器严格的别名规则的问题,因为你必须查看内存的字节表示(-fno-strict-aliasing是你的朋友)。

笔记

话虽如此,我强烈建议不要将其用于序列化。如果您为此使用工具(即:protobuf、flatbuffers、msgpack 或其他工具),您将获得大量功能:

  • 语言独立
  • rpc(远程过程调用)
  • 数据规范语言
  • 模式/验证
  • 版本控制
于 2017-07-15T11:43:25.320 回答
1

谈到替代方案并考虑您的问题Tuple-like container for packed data(对此我没有足够的声誉发表评论),我建议看看 Alex Robenko 的CommsChampion项目:

COMMS 仅是 C++(11) 标头,独立于平台的库,它使通信协议的实现成为一个简单且相对快速的过程。它提供了所有必要的类型和类,以使自定义消息的定义以及包装传输数据字段成为类型和类定义的简单声明语句。这些语句将指定需要实施的内容。COMMS 库内部处理 HOW 部分。

由于您正在使用 Cortex-M4 微控制器,您可能还会发现有趣的是:

COMMS 库专门开发用于嵌入式系统,包括裸机系统。它不使用异常和/或 RTTI。它还最大限度地减少了动态内存分配的使用,并提供了在需要时完全排除它的能力,这在开发裸机嵌入式系统时可能需要。

Alex 提供了一本出色的免费电子书,名为Guide to Implementing Communication Protocols in C++ (for Embedded Systems),其中描述了内部结构。

于 2017-07-25T06:50:58.573 回答
0

这是针对可能满足您需要的算法的伪代码,以确保与正确的目标操作系统和平台一起使用。

如果使用C您将无法使用的语言classestemplates以及其他一些东西,但您可以使用基于、架构师、以及最后的字节布局preprocessor directives来创建您需要的版本。否则,这里的重点将是 C++ 和模板的使用。struct(s)OSCPU-GPU-Hardware Controller Manufacturer {Intel, AMD, IBM, Apple, etc.}platform x86 - x64 bitendian

以你struct(s)为例:

struct Sensor1Telemetry {
    int16_t temperature;
    uint32_t timestamp;
    uint16_t voltageMv;
    // etc...
} __attribute__((__packed__));

struct TelemetryPacket {
    Sensor1Telemetry tele1;
    Sensor2Telemetry tele2;
    // etc...
} __attribute__((__packed__));

您可以像这样对这些结构进行模板化:

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__));

使用这些enum标识符,您可以根据上述组合使用它class template specialization来设置它以满足其需要。class在这里,我将采用所有似乎可以正常工作的常见情况并将其default class declaration & definition设置为主类的功能。然后对于那些特殊情况,例如Endian字节顺序不同,或者特定的操作系统版本以不同的方式做某事,或者GCC versus MS编译器使用__attribute__((__packed__))vs#pragma pack()然后可以是需要考虑的少数专业。您不需要为每个可能的组合指定专业化;这将太令人生畏和耗时,应该只需要执行少数可能发生的罕见情况,以确保您始终为目标受众提供正确的代码说明。同样enums非常方便的是,如果您将这些作为函数参数传递,您可以一次设置多个,因为它们被设计为位标志。因此,如果您想创建一个将此模板结构作为其第一个参数的函数,然后将支持的操作系统作为其第二个参数,那么您可以将所有可用的操作系统支持作为位标志传递。

这可能有助于确保packed structures根据适当的目标“打包”和/或正确对齐这组,并且它将始终执行相同的功能以保持跨不同平台的可移植性。

现在,您可能必须在不同的支持编译器的预处理器指令之间进行两次这种专门化。这样,如果当前编译器是 GCC,因为它以一种方式及其专业化定义结构,然后在另一种方式中使用 Clang,或 MSVC、代码块等。因此,最初设置它会有一些开销,但它应该,可以高度确保它在目标机器的指定场景或属性组合中正确使用。

于 2017-07-15T10:56:30.180 回答
-2

不总是。当您将数据发送到不同的架构处理器时,您需要考虑字节序、原始数据类型等。最好使用ThriftMessage Pack。如果没有,请创建自己的 Serialize 和 DeSerialize 方法。

于 2017-07-18T10:29:42.960 回答