便携式和紧密的钻头包装

Portable and Tight Bit Packing

假设我有三个unsigned ints、{abcd},我想用非标准打包长度,分别为{9,5,7,11}。我希望制作一个网络数据包 (unsigned char pkt[4]),我可以将这些值打包并在另一台机器上使用相同的头文件可靠地解压缩它们,而不管字节序如何。

我读到的关于使用打包结构的所有内容都表明位排序将不可预测,所以这是不可能的。所以这给我留下了位设置和位清除操作,但我不确定如何确保字节顺序不会给我带来问题。以下是否足够,或者我应该 运行 分别解决 ad 的字节序问题?

void pack_pkt(uint16_t a, uint8_t b, uint8_t c, uint16_t d, uint8_t *pkt){
    uint32_t pkt_h = ((uint32_t)a & 0x1FF)      // 9 bits
                 | (((uint32_t)b & 0x1F) << 9)  // 5 bits
                 | (((uint32_t)c & 0x3F) << 14) // 7 bits
                 | (((uint32_t)d & 0x7FF) << 21); //11 bits
    *pkt = htonl(pkt_h);
}

void unpack_pkt(uint16_t *a, uint8_t *b, uint8_t *c, uint16_t *d, uint8_t *pkt){
    uint32_t pkt_h = ntohl(*pkt);
    (*a) = pkt_h & 0x1FF;
    (*b) = (pkt_h >> 9) & 0x1F;
    (*c) = (pkt_h >> 14) & 0x3F;
    (*d) = (pkt_h >> 21) & 0x7FF;
}

如果是这样,我还可以采取哪些其他措施来确保可移植性?

打包打包我建议使用这样的结构

  • 记住结构的大小在其他机器上是不同的,比如 8 位系统和 32 位系统编译相同的结构但大小不同我们称之为结构中的填充所以你可以使用 pack 来确保结构大小在发送器和接收器中是相同的
typedef struct {
    uint8_t A;
    uint8_t B;
    uint8_t C;
    uint8_t D;
} MyPacket;

现在您可以将此结构流式传输到字节流中,例如 SerialPort 或 UART 或其他 在接收器中,您可以将字节打包在一起

查看以下函数

void transmitPacket(MyPacket* packet) {
    int len = sizeof(MyPacket);
    uint8_t* pData = (uint8_t*) packet;
    while (len-- > 0) {
        // send bytes 1 by 1
        transmitByte(*pData++);
    }
}

void receivePacket(MyPacket* packet) {
    int len = sizeof(MyPacket);
    uint8_t* pData = (uint8_t*) packet;
    while (len-- > 0) {
        // receive bytes 1 by 1
        *pData++ = receiveByte();
    }
}

记住字节中的位顺序在每个地方都是相同的,但是你必须检查你的字节顺序以确保数据包不会在接收器中被错过

例如,如果您的数据包大小为 4 个字节,并且您先发送低字节 你必须在接收器中接收低字节

在您的代码中,您在 uint8_t* 指针中获得了数据包,但您的实际数据包大小为 uint32_t 且为 4 个字节

具有位域的结构对于此目的确实基本上没有用,因为它们的字段顺序甚至填充规则都不一致。

shall I run into problems with the endianness of a and d separately?

ad 的字节顺序无关紧要,它们的 byte-order 永远不会被使用。 ad 不会被重新解释为原始字节,只会使用或分配它们的整数值,在这些情况下,字节顺序不会出现。

但是还有一个问题:uint8_t *pkt*pkt = htonl(pkt_h); 结合意味着 只有最低有效字节被保存(不管它是否是由小端或大端机器执行,因为这不是重新解释,而是隐式转换)。 uint8_t *pkt 本身是可以的,但是必须将生成的 4 个字节组复制到它指向的缓冲区中,不能一次性分配给它。 uint32_t *pkt 将使这样的 single-assignment 能够在不丢失数据的情况下工作,但这会使该功能使用起来不太方便。

类似unpack_pkt,目前只使用了一个字节的数据。

当这些问题得到解决后,应该会很好:

void pack_pkt(uint16_t a, uint8_t b, uint8_t c, uint16_t d, uint8_t *buffer){
    uint32_t pkt_h = ((uint32_t)a & 0x1FF)      // 9 bits
                 | (((uint32_t)b & 0x1F) << 9)  // 5 bits
                 | (((uint32_t)c & 0x3F) << 14) // 7 bits
                 | (((uint32_t)d & 0x7FF) << 21); //11 bits
    uint32_t pkt = htonl(pkt_h);
    memcpy(buffer, &pkt, sizeof(uint32_t));
}

void unpack_pkt(uint16_t *a, uint8_t *b, uint8_t *c, uint16_t *d, uint8_t *buffer){
    uint32_t pkt;
    memcpy(&pkt, buffer, sizeof(uint32_t));
    uint32_t pkt_h = ntohl(pkt);
    (*a) = pkt_h & 0x1FF;
    (*b) = (pkt_h >> 9) & 0x1F;
    (*c) = (pkt_h >> 14) & 0x3F;
    (*d) = (pkt_h >> 21) & 0x7FF;
}

一种无需担心字节顺序的替代方法是手动解构 uint32_t(而不是有条件地 byte-swapping 使用 htonl 然后将其重新解释为原始字节),例如:

void pack_pkt(uint16_t a, uint8_t b, uint8_t c, uint16_t d, uint8_t *pkt){
    uint32_t pkt_h = ((uint32_t)a & 0x1FF)      // 9 bits
                 | (((uint32_t)b & 0x1F) << 9)  // 5 bits
                 | (((uint32_t)c & 0x3F) << 14) // 7 bits
                 | (((uint32_t)d & 0x7FF) << 21); //11 bits
    // example serializing the bytes in big endian order, regardless of host endianness
    pkt[0] = pkt_h >> 24;
    pkt[1] = pkt_h >> 16;
    pkt[2] = pkt_h >> 8;
    pkt[3] = pkt_h;
}

原来的做法还不错,这只是一个替代方案,可以考虑一下。由于没有任何东西被重新解释,字节顺序根本不重要,这可能会增加对代码正确性的信心。当然,作为一个缺点,它需要更多的代码来完成同样的事情。顺便说一句,尽管手动解构 uint32_t 并存储 4 个单独的字节看起来工作量很大,但 GCC 可以将 compile it efficiently 转换为 bswap 和 32 位存储。另一方面,Clang 错过了这个机会,其他编译器也可能错过,所以这并非没有缺点。