关于大端小端在跨字节的定义结构体的不同

关于大端小端在跨字节的定义结构体

目录

编辑信息

作者:chemoontheshy

创建时间:2021/09/17

完成时间:2021/09/17

说明

主要说明本文,主要说明当需要填充跨字节的时候,如何修改,由于最近写到 HEVC NAL 的 FU 分包时,发现 HEVC 的 NAL 头会跨字节的。所以了解一下,记录在此。这里不阐述什么是大端和小端之分,只是阐述关于跨字节的填充结构体的定义。

研究这个一步,主要是为了后续,HEVC NAL 的 FU 分包的时候,需要填写,FUHeader,由于网络传输都是使用大端,所以需要用小端结构体,模拟大端结构。

关于大端小端在跨字节的定义结构体的不同

HEVCNAL unit header 为例


            +---------------+---------------+
            |0|1|2|3|4|5|6|7|0|1|2|3|4|5|6|7|
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |F|   Type    |  LayerId  | TID |
            +-------------+-----------------+

根据上述,可以看出 LayerId 字段跨字段了,这时候填充就会出现问题。

基于惯性思维,或根据 H264 header 的填充,用 uint8_t 来定义结构体,但是由于跨字节,那是不是可以用 uint16_t来定义结构体。 于是有了

struct HEVCHeader
{
#ifdef RTP_BIG_ENDIAN
    uint8_t flag : 1;
    uint8_t type : 6;
    uint8_t layerId : 6;
    uint8_t tid : 3;
#else
    uint8_t tid : 3;
    uint8_t layerId : 6;
    uint8_t type : 6;
    uint8_t flag : 1;
#endif // RTP_BIG_ENDIAN
};

由大端小端是由处理器 CPU 决定的,一般我们常用的电脑都是 x86 架构,属于小端,所以用电脑测试应该是按小端模式来测试,我现在要做的是先把结构体 HEVCHeader填入数据后打印,看是否与我们的预期的一致。

int main()
{
    HEVCHeader hevcHeader;
    hevcHeader.flag = 0;
    hevcHeader.type = 25;
    hevcHeader.layerId = 0;
    hevcHeader.tid = 1;
    //获取hevcHeader的头指针
    //uint8_t* head = (uint8_t*)&hevcHeader;
    uint8_t* head = reinterpret_cast<uint8_t*>(&hevcHeader);
    uint8_t head1 = *head;
    uint8_t head2 = *(head+1);
    printf("head1=0x%x\n", head1);
    printf("head2=0x%x\n", head2);
    /*
    * printf
    * hex : head1=0xc9
    * hex : head2=0xc0
    * bin : head1=11001001
    * bin :    head2=11000000
    * 理论值 :00110010 00000001
    * 实际值 :00000001 00110010
    * 
    */

    return 0;
}

由上述代码可指,和我们预期差了很多,所以我们来探究实际值到底怎样得处理的。

根据实际值,可基本了解到uint16_t是怎样存储的

img

由上图,存储uint16_t,由于是小端,所以是第一个字节的小端开始存储,然后第二个字节的小端开始存储。所以为了达到我们需要的情况,需要重新设计结构体。然后发现,如以uint16_t格式填充,

  • LayerId的最高位 1-bit
  • Type 6-bit
  • F 1-bit
  • TID 3-bit
  • LayerId 5-bit

所以结构体应该设置为:

struct NewHeader
{
#ifdef RTP_BIG_ENDIAN
    uint16_t layerId_2 : 5;
    uint16_t tid : 3;

    uint16_t flag : 1;
    uint16_t Type : 6;
    uint16_t layerId_1 : 1;
#else
    uint16_t layerId_1    : 1;
    uint16_t Type        : 6;
    uint16_t flag        : 1;

    uint16_t tid        : 3;
    uint16_t layerId_2    : 5;
#endif // RTP_BIG_ENDIAN
};

然后根据新的来结构体填充

int main()
{
    HEVCHeader hevcHeader;
    hevcHeader.flag = 0;
    hevcHeader.type = 25;
    hevcHeader.layerId = 0;
    hevcHeader.tid = 1;
    //获取hevcHeader的头指针
    //uint8_t* head = (uint8_t*)&hevcHeader;
    uint8_t* head = reinterpret_cast<uint8_t*>(&hevcHeader);
    uint8_t head1 = *head;
    uint8_t head2 = *(head+1);
    printf("head1=0x%x\n", head1);
    printf("head2=0x%x\n", head2);
    /*
    * printf
    * hex : head1=0x01
    * hex : head2=0x32
    * bin : head1=00000001
    * bin :    head2=00110010
    * 理论值 :00110010 00000001
    * 实际值 :00000001 00110010
    * 
    */

    NewHeader newHeader;
    newHeader.flag = 0;
    newHeader.Type = 25;
    newHeader.layerId_1 = 0;
    newHeader.layerId_2 = 0;
    newHeader.tid = 1;
    uint8_t* newhead = reinterpret_cast<uint8_t*>(&newHeader);
    uint8_t newhead1 = *newhead;
    uint8_t newhead2 = *(newhead + 1);
    printf("newhead1=0x%x\n", newhead1);
    printf("newhead2=0x%x\n", newhead2);
    /*
    * printf
    * hex : head1=0x32
    * hex : head2=0x01
    * bin : head1=00110010
    * bin :    head2=00000001
    * 理论值 :00110010 00000001
    * 实际值 :00110010 00000001
    * 
    */

    return 0;
}

使用新的结构体就可以用小端填写 HEVCHeader,就可以模拟大端了。

同时根据NewHeader的结构可以发现,其实相当于分开了两个字节,故也可以写成以uint8_t的结构体:

struct HEVCHeader_uint8_t
{
#ifdef RTP_BIG_ENDIAN
    uint8_t layerId_2 : 5;
    uint8_t tid : 3;

    uint8_t flag : 1;
    uint8_t Type : 6;
    uint8_t layerId_1 : 1;
#else
    uint8_t layerId_1 : 1;
    uint8_t Type : 6;
    uint8_t flag : 1;

    uint8_t tid : 3;
    uint8_t layerId_2 : 5;
#endif // RTP_BIG_ENDIAN
};

测试后可以达到相同的效果,故基本可以总结,当跨字节的时候,还是以uint8_t为单位进行构造比较合适。

参考:

待写

Edit with Markdown