使用 GCC 结构位打包的字节顺序

byte order using GCC struct bit packing

我正在使用 GCC 结构位字段尝试解释 8 字节 CAN 消息数据。我写了一个小程序作为一种可能的消息布局的例子。代码和注释应该描述我的问题。我分配了 8 个字节,以便所有 5 个信号都应等于 1。正如 Intel PC 上的输出所示,情况并非如此。我处理的所有 CAN 数据都是 big endian,而且它们几乎从不打包成 8 位对齐的事实使得 htonl() 和朋友在这种情况下毫无用处。有人知道解决方案吗?

#include <stdio.h>
#include <netinet/in.h>

typedef union
{
    unsigned char data[8];
    struct { 
        unsigned int signal1 : 32;
        unsigned int signal2 :  6;
        unsigned int signal3 : 16;
        unsigned int signal4 :  8;
        unsigned int signal5 :  2;
    } __attribute__((__packed__));
} _message1;

int main()
{
    _message1 message1;
    unsigned char incoming_data[8]; //This is how this message would come in from a CAN bus for all signals == 1

    incoming_data[0] = 0x00;
    incoming_data[1] = 0x00;
    incoming_data[2] = 0x00;
    incoming_data[3] = 0x01; //bit 1 of signal 1
    incoming_data[4] = 0x04; //bit 1 of signal 2
    incoming_data[5] = 0x00;
    incoming_data[6] = 0x04; //bit 1 of signal 3
    incoming_data[7] = 0x05; //bit 1 of signal 4 and signal 5

    for(int i = 0; i < 8; ++i){
        message1.data[i] = incoming_data[i];
    }

    printf("signal1 = %x\n", message1.signal1);
    printf("signal2 = %x\n", message1.signal2);
    printf("signal3 = %x\n", message1.signal3);
    printf("signal4 = %x\n", message1.signal4);
    printf("signal5 = %x\n", message1.signal5);
}

由于编译器和体系结构之间的结构打包顺序不同,因此最好的选择是使用辅助函数 pack/unpack 二进制数据。

例如:

static inline void message1_unpack(uint32_t            *fields,
                                   const unsigned char *buffer)
{
    const uint64_t  data = (((uint64_t)buffer[0]) << 56)
                         | (((uint64_t)buffer[1]) << 48)
                         | (((uint64_t)buffer[2]) << 40)
                         | (((uint64_t)buffer[3]) << 32)
                         | (((uint64_t)buffer[4]) << 24)
                         | (((uint64_t)buffer[5]) << 16)
                         | (((uint64_t)buffer[6]) <<  8)
                         |  ((uint64_t)buffer[7]);
    fields[0] =  data >> 32;           /* Bits 32..63 */
    fields[1] = (data >> 26) & 0x3F;   /* Bits 26..31 */
    fields[2] = (data >> 10) & 0xFFFF; /* Bits 10..25 */
    fields[3] = (data >> 2)  & 0xFF;   /* Bits  2..9  */
    fields[4] =  data        & 0x03;   /* Bits  0..1  */
}

请注意,由于连续的字节被解释为单个无符号整数(以大端字节顺序),因此以上内容将是完全可移植的。

当然,您可以使用结构来代替字段数组;但它根本不需要与在线结构有任何相似之处。但是,如果您有多个不同的结构要解包,那么(最大宽度)字段数组通常会变得更容易和更健壮。

所有理智的编译器都会很好地优化上面的代码。特别是,带有 -O2 的 GCC 做得非常好。

相反,将这些相同的字段打包到缓冲区,非常相似:

static inline void  message1_pack(unsigned char  *buffer,
                                  const uint32_t *fields)
{
    const uint64_t  data = (((uint64_t)(fields[0]          )) << 32)
                         | (((uint64_t)(fields[1] & 0x3F   )) << 26)
                         | (((uint64_t)(fields[2] & 0xFFFF )) << 10)
                         | (((uint64_t)(fields[3] & 0xFF   )) <<  2)
                         | ( (uint64_t)(fields[4] & 0x03   )       );
    buffer[0] = data >> 56;
    buffer[1] = data >> 48;
    buffer[2] = data >> 40;
    buffer[3] = data >> 32;
    buffer[4] = data >> 24;
    buffer[5] = data >> 16;
    buffer[6] = data >>  8;
    buffer[7] = data;
}

请注意,掩码定义了字段长度(0x03 = 0b11(2 位),0x3F = 0b111111(16 位),0xFF = 0b11111111(8 位), 0xFFFF = 0b1111111111111111(16 位));并且移位量取决于每个字段中最低有效位的位位置。

为了验证这些功能是否有效,打包、解包、重新打包和重新打包一个缓冲区,该缓冲区应包含除其中一个字段外全为零的缓冲区,并验证数据在两次往返中保持正确。它通常足以检测典型的错误(错误的位移量、掩码中的拼写错误)。

请注意,文档将是确保代码保持可维护性的关键。我会亲自在上面的每个功能之前添加注释块,类似于

/* message1_unpack(): Unpack 8-byte message to 5 fields:
       field[0]: Foobar. Bits 32..63.
       field[1]: Buzz.   Bits 26..31.
       field[2]: Wahwah. Bits 10..25.
       field[3]: Cheez.  Bits  2..9.
       field[4]: Blop.   Bits  0..1.
*/

字段 "names" 反映了他们在文档中的名字。