语言基础/分析和实践 C&C++ 位域结构数据类型

概述

本文重点介绍了 C/C++ 中位域/位段的概念,讲述了位域/位段与结构体的关系,结构体字段定义和字段存储顺序关系,位段结构的字节序和位序问题、位域大小端问题、位段结构的位使用规则、位域结构数据在通信传输(如CAN协议)中的使用注意事项等。

@History
在2017年以前,大多时候只基于C++写集散控制系统软件,在接收终端设备数据过程中,有时候要处理位域结构的数据,算是与位域的概念的初识。正经开搞嵌入式纯C开发,慢慢体会到节省存储空间的必要性,再到后来开始独立去设计CAN通信协议,才算正在理解到位域的作用和重要性。

转载请标明出处,
https://blog.csdn.net/quguanxin/category_6223029.html

位域和结构体的关系

在经典《C和指针》的第10章(结构和联合),10.5节 <段位>(P209),原描述如下:大河qu @ CSDN#
在C语言中,提供了两种类型的聚合数据类型,数组和结构。结构是一些值的集合,这些值称为它的成员,相较于数组,一个结构的成员可能具有不同的类型。 #关于结构,我们最后还必须提到它实现位段的能力。位段的声明和结构类似,但它的成员是一个或多个位的字段。这些不同长度的字段实际上存储于一个或多个整型变量中。#位段的声明和任何普通的结构成员声明相同,但有两个例外。首先,位段成员必须声明为int、signed int 或 unsigned int类型。其次,在成员名的后边是一个冒号和一个整数,这个整数指定该位段所占用的位的数目。(用signed或unsigned整数显式的声明位段是个好主意。如果把位段声明为int类型,它究竟被解释为有符号数还是无符号数是由编译器决定的)。

位域/位段的概念

读完《C和指针》中对于位域的描述后,我对其概念定义产生了些疑问。为此我又去读了读 位域-百科 ,其中的描述是这样的,
C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为"位段"或称"位域( bit field)" 。随着对位段含义的理解深入,最后我认定百科中的这个定义是更加贴近真实的的完美定义。、
我自己对位段这个概念的理解,经历了如下几个阶段,没舍得删,
@20200311
上述两种描述是存在出入的。可能是我太较真,读这么经典的描述,还是觉得有些别扭。描述中,对于“位段”和“位段成员”,感觉两个概念是混乱的。但是如果我们把位段和位段成员两个名词之间画一个等号,那么再去读书中的描述,就会舒坦些。即,位段(或位段成员),与普通结构成员是同一个层次的描述,它们的上层描述词是“结构”,是统一的。这样的话,将位段这一名词理解为特殊的结构成员(将这个特殊的结构成员一个名字叫位段成员),似乎很合理啊(甚至在广义上,还能将位段理解成C的一种语法功能…)。当时我给自己做了这样的约定:使用位域功能时,struct标识的整体叫它位域结构/位段结构,struct内部的成员叫它位域/位段或位域成员/位段成员。
@20210510
这是再后来的故事了,随着对位域的使用逐渐熟络,我已经清楚的知道,所谓位域,是不能脱离结构体而独自定义的。至此,位域和结构体的关系也就明确了:在C/C++中,位域的定义通常与结构体相关联,位域是结构体的成员,是作为结构体的一种字段类型存在的。位域的定义必须在结构体内部,而无法脱离结构体进行定义。
@20230710
有一次我在读Linux SLIST源码的时候,算是正在理解了field这个单词的含义。可以简单的参考如下宏定义,

//这里关注宏参数field,其代表的是结构字段名称
#define	SLIST_NEXT(elm, field)	((elm)->field.sle_next)
//借助SLIST_ENTRY定义链表结构
struct TLucyItem {
    TYourData data;
    SLIST_ENTRY(TLucyItem) linkNode;
};

如上,宏函数形参 field 对应的‘实参’是TLucyItem结构中linkNode这一字段名。有兴趣的可以去读读《语言基础/单向链表的构建和使用(含Linux中SLIST的解析和使用)》中的讲述。

位域定义的语法

位段的声明和任何普通结构(struct)成员的声明相同,但,
位段成员必须是整形或枚举类型(通常是无符号类型),且在成员名的后面是一个冒号和一个整数,整数规定了成员所占用的位数。位段(位域成员)的定义,不能脱离struct范围而单独使用。每个位段(结构体成员)的位数不能超过它的类型的宽度。

不能单独定义位段,
在这里插入图片描述

位段不能超过类型宽度,
在这里插入图片描述

不规范的定义会造成位浪费,
在这里插入图片描述
TBitY 当然是没有编译问题的,它将我们的9bit数据分散到了两个字段中,iba浪费3bit、ibb浪费4bit。同理,TBitZ中的iba也会浪费2bit。这里还有一点要说明,就是你永远定义不出来超过存储字段总位数的位段结构,如上TBitZ,ibb在无法存储到上一个 unsigned int 存储字段中时,编译器会新开辟一个 unsigned int 存储字段,sizeof(TBitZ)==8呢! 后续我们会实践证明,浪费的这些位占据的是各存储字段的较低的有效位,此不赘述。

位域结构的基本类型/存储类型

typedef struct tagBit16FieldA {
    MX_U16 u32FieldA : 2 /*bit*/;
    MX_U16 u32FieldB : 3 /*bit*/;
    MX_U16 u32FieldC : 4 /*bit*/;
    MX_U16 u32FieldD : 4 /*bit*/;
    MX_U16 u32FieldE : 3 /*bit*/;
} TBitFieldA;

如上,多字节类型MX_U16在这个位域结构中被称为位域结构的基本类型或存储类型,我认为后者更加贴切。

位段结构的利弊

除了节省空间外,位段还具有简化源代码的优点(相比使用移位和屏蔽的方法),但位段的使用会带来执行效率的损失和代码弱移植性较问题。先谈弱移植性的问题,在《C和指针》P209提到一个建议,注重可移植性的程序应该避免使用位段。因为位段在不同的系统中可能有不同的结果,具体的,
1、 int位段被当作有符号还是无符号数。
2.、位段中位的最大数目。许多编译器吧位段成员的长度限制在一个整形值的长度之内,所以一个能够运行与32位整数的机器上的位段声明可能在16位整数的机器上无法运行。
3、位段中的成员在内存中是从左向右还是从右向左分配的。
4、 当一个声明中有两个位段,第二个位段比较大,无法容纳与第1个位段剩余的位时,编译器有可能吧第2个位段放在内存的下一个字节,也可能直接放在第一个位段后面,从而在两个内存位置的边界上形成重叠。

弱调试性?
也不知道咋来的印象,会觉得位域结构的代码不好调试。先在VS中测试了下,
在这里插入图片描述
如上,查看位域结构体对象 t1 的各字段运行值是没有问题的,即使使用1字节对齐也不影响。难不成是 Keil 环境中不可以来?

在Keil C51 集成开发环境中,呢,也是可以进行查看调试值的,如下,
在这里插入图片描述
在任何IDE下对位域字段进行打印调试,也是没有问题的,

    printf("%d-%d-%d-%d", t1.ibPartA, t1.ibPartB, t1.ibPartC, t1.ibPartD);

到这,我只能说,是我记错了,位域结构对象,似乎并没有弱调试性的特点。不过,不能使用&对位段做取地址运算,因此不存在位段的指针,编译器通常不支持位段的引用,
在这里插入图片描述
虽然我们不能对位域成员(位段)取地址,但是对位域结构对象取地址是没有问题的,即允许使用位域结构对象的首地址进行内存拷贝操作,像使用普通结构体对象一样。在《语言基础/分析和实践 sizeof 操作符》中我们有讲述到:sizeof算符不能用以位域(这里的位域指的是位域成员),但是C语言是允许将sizeof用以位域结构类型或其对象的。另外,在IDE中编译 volatile 修饰的位段并没有发现什么异常,但我从来没有在STM32的驱动或其他源码中发现过使用_IO修饰的位段,一般都是直接修饰U32的寄存器值。

利弊分析-空间,
基于位域可移植性较弱的特点,它不太适合作为传输数据(不适合定义在协议中)。考虑到,在嵌入式系统设计中,内存和存储空间通常及其有限,嵌入式系统及其常规的上位机系统在内存中的布局和管理并没有太大的差别,权衡之下,位域结构在程序设计和协议设计与实现中都是常用的。
利弊分析-效率,
在内存中,位段通常不会按照字节边界对齐,而是按照位边界对齐,这可能导致结构体的整体对齐问题,使得编译器需要额外的处理来确保结构体内部的位段能够正确地访问。在访问带位段成员的结构体时,可能需要进行位操作,引入额外的复杂性和开销,需要更多的处理器周期来进行位的读取和写入操作,从而降低了访问效率。

个人建议不要构造复杂的位域结构来使用,通常我们只是定义总位数不超过一个基础类型(U8、U16、U32、U64)的位域结构,尽量的让一个位域结构内的全部成员都是一个数据类型。

结构字段的定义和存储顺序

早些年,在 IDE VS 中已经大约地验证过,小端系统下,先出现(或者说先被定义的)的字段,占据的是低地址。也就是说,结构字段的存储顺序与我们对字段的阅读顺序(不是字节阅读顺序哈,是字段的阅读顺序)是一致的。接下来的小节中,就分别去大小端系统中实际看看。

小端系统上的结构字段存储

最简单和最好理解的办法是使用联合结构,如下,

typedef union {   //Win x86
    unsigned long long u64Data;
    struct  TData {
        unsigned short u16A;
        unsigned char  u8B;
        unsigned short u16C; //前补1
        unsigned char  u8D;  //后补1
    } tData;
} Unio64Dt;

在这里插入图片描述
如上所示,最先定义的字段A,对应的是U64的最低2个有效字节0x0708,具体对应如下,
在这里插入图片描述
总之,小端系统中,对于结构体TData,其中先定义出现的字段,安置在内存的低地址上,后出现的字段按照在内存高地址上。因结构体对齐而填充的字节,被安置在对应字段内存位置的高地址上。按照U64的高低有效字节描述法,将结构整体视做一个完整单一数据类型的话,在小端系统中,先定义的字段占据低有效字节,后定义的字段占据高有效字节,对齐填充字节在字段的高有效字节位置。关于对齐和填充的讲述,也可以去具体参考 《存储和传输/探究结构数据在内存中的对齐和填充规则》这篇文章。

大端系统上的结构字段存储

如上文,在小端系统中的测试。假设结构对象tData 存在 MemArea(0x00EFFA80-0x00EFFA88) 的内存区域上,则结构体的第一字段segA在内存 MemArea 低地址(0x00EFFA80-0x00EFFA81)上,最后的字段segD存在MemArea高地址(0x00EFFA87)上。其实上述验证后,我卡顿了很久的时间,因为一直在试图寻找和搭建一个大端环境,具体参见《存储和传输/寻找大端字节序/有哪款MCU或MPU是真支持大端》中讲述的过程。下文使用的大端环境还是 STC89C51 + Keil C51,

#ifdef _WIN32
#pragma pack(1)  //Keil C51 不支持
#endif 
typedef struct {
    unsigned char  segA;
    unsigned short segB;
#ifdef _WIN32
    unsigned int   segC;  //Keil C51 sizeof(int)==2
#else
    unsigned long  segC;
#endif   
} TData;
#ifdef _WIN32
#pragma pack()
#endif

//int main() {
    TData tData;
    void *pvDtAddr = &tData;
    tData.segA = 0x01;
    tData.segB = 0x0203;
    tData.segC = 0x04050607;

在这里插入图片描述
如上,无论在小端还是大端字节序的系统中,字段的定义和字段整体的存储区位置之间的相对顺序是一致的,即它们都是将第一个字段存0x01存在 MemArea 的part1, 0x0203 存在 MemArea 的part2(地址0x0083F921-0x0083F922 / 或 I:0x19- I:0x20), 最后字段segC==0x04050607存在在MemArea 的part3区域上。
将上述结论延伸下:对于一个结构体对象,假设其在小端系统和大端系统上的内存地址是一致的,那么,无论是大小端系统的哪个,该结构对象中,各个结构体字段的地址也应该是一致的,不一样的只是多字节字段内部的字节序。

小结(承上启下)

这里再次强调下字段定义顺序和字节顺序的差别。如上,segA、segB、segC、segD的排列是字段定义顺序,而小端系统中的segA/u16A==0x0708,其在小端系统中存储为 08 07,这是字段A的存储字节序。可以认为,字节序(大小端字节序问题)是单个字段内部要讨论的问题,讨论的是单个字段(单/多字节单一数据类型)的字节排序问题(如果结构体包含结构字段,则平铺展开后再分析)。

另外,前文我们已经讨论过,位域结构就是一种特殊的结构体类型。因此,在结构体数据类型的相关实践中碰到的那些问题,位域结构都会继承下来,甚至是更复杂,需要更深入的讨论,这些派生的问题可能包括:位段结构字节序问题、位段结构bit位序、位端结构的字节对齐和位填充规则、位段数据的存储等等。接下来,我们将逐一讨论上述问题。

位域结构的存储(对齐、填充、跨字节)

再次强调下,上一小节的结论,对于普通非位域结构定义,其在小端还是大端字节序的系统中,字段的定义和字段整体的存储区位置之间的相对顺序是一致的,都是先定义的字段在低地址区域,后定义的字段在高地址区域。而位域,本质上是针对一个单一数据类型的,不太准确的说,位域是针对单个普通结构字段的按位拆分使用,因此关于位域结构的存储的研究方向与普通结构的存储的研究方向还是存在巨大差别的。对普通结构字段存储的研究多个单一数据类型字段之间的,而位域字段存储的研究是单个单一数据类型值内的…
位域结构的字节对齐问题和字节填充、位浪费问题等,已经在 《数据存储与传输/探究普通结构和位域结构体数据的字节对齐规则》中完成了基本讨论。本小节在其基础上,使用不同的示例展开更深入的实践和理解分析。下文定义了位域结构TBitFieldA,我们将分别在小端和大端系统中,对该结构的对象的内存存储进行测试,使用相同的数据结构和结构对象成员赋值。

#ifdef __cplusplus //temp windows
typedef unsigned short int MX_U16;
#else              //stc89c51 /其int为16bit /且不支持long int的位域定义
typedef unsigned int MX_U16;
#endif

typedef struct tagBit16FieldA {
    MX_U16 u32FieldA : 2 /*bit*/;
    MX_U16 u32FieldB : 3 /*bit*/;
    MX_U16 u32FieldC : 4 /*bit*/;
    MX_U16 u32FieldD : 4 /*bit*/;
    MX_U16 u32FieldE : 3 /*bit*/;
} TBitFieldA;

int main() {
    TBitFieldA bitY = { 1/*2bit*/, 2/*3bit*/, 3/*4bit*/, 4/*4bit*/, 5/*3bit*/ };
    void *pAddr = &bitY;
}

上述代码在 Windows Visual Studio x86 编译运行,以及在Keil C51基础集成开发环境下,连接 STC89C51 仿真器进行调试,结果分别如下。红色,小端。绿色,大端。
在这里插入图片描述
如上实践结果,基于MX_U16存储类型的位段结构,其在小端和大端环境下,仅有字节序有反转,而没有位域字段在结构体内的定义顺序的颠倒。其实这个情况通过理论也能推演(哈哈,没有上述实践结构前,我并不敢如此说),我们可以定义 TBitFieldA tBitFieldA 和 MX_16 u16A 的联合体 unionA,在大小端系统中对 unionA.tBitFieldA 各成员赋值相同,等价于对 unionA.u16A 赋值相同,故,无论是在大小端字节序系统,printf u16A 值是一样的,只是u16A在内存存储上,存在字节序差异。

考虑到大端字节序是符合阅读顺序的,接下来先看看大端的情况,
在这里插入图片描述
如上,大端系统中,位域结构体中的第一个字段u32FieldA==1 排在了U16的低有效位上,结构体中的末尾字段u32FieldE ==5 排在了U16的高有效位上。要注意的是,这里的字段不再是普通结构那样整字节的,因此也就没有地址的概念,但还是可以不太负责的延伸下,u32FieldA 这一在位段中首先出现的字段,并不像普通结构中首先出现的字段segA那样,存在较低的地址上,而是占用了较高字节地址的较高bit位。
前文<大端系统上的结构字段存储>小节中已经验证,对于普通结构,在大小端系统中的同一结构的字段级存储顺序是一致的。小端系统下,位段的存储分析,似乎要不大端系统下位段分析复杂一些,
在这里插入图片描述
针对小端系统上位段结构对象的存储,先看上图的第二部分,很容易发现,无论你从左到右还是从右到左的去阅读这个二进制数据,都还原不到用户设置的位段值序列,因此很明显这是一种错误的位段对象内存数据展开和分析方案。即,阅读小端系统下内存中位段结构对象的成员值时,你不能从低地址到高地址展开各个字节为bit位值,这样就乱套了。正确的方法如上图第一部分,你必须要把存储位段的多字节数据的内存倒叙展开,也就是展开成与大端系统上完全相同的样子。
综上得出的一个结论是,
基于单个多字节类型U16/U32/U64构建的位域结构数据,无论是在小端系统还是在大端系统上,末尾位域字段始终对应的是多字节的高位区,第一位域字段始终对应的是多字节的低位区。注意了哈,这里的描述用的是高位区和低位区,而之前章节中对普通结构字段存储规则描述时,使用的是高地址去和低地址区的概念,一定要体会明白哈。
还要注意到一个情况是,FieldC是跨越两个字节的,我曾经很担心这种位段跨字节会带来问题,但我说不上来会有什么问题!但实践上,这里什么问题都不会有。你永远定义不

结合《数据存储与传输/探究普通结构和位域结构体数据的字节对齐规则》的分析,并结合上文相的关分析,最终的关于结构(含普通结果和位域结构)成员的存储规律,有如下总结:
0、大小端问题是(多字节)基础数据类型在内存中字节排序的问题,也即所谓大小端的问题其实与结构体本身关系不大,本质上大小端问题是发生在结构内的成员层级上的,只是sizeof结构体字节大小,从顶层体现了这种本质或者说体现了规则的结果。
1、首先要明白的是,结构体的普通成员和位段成员存储规律的探究层次是不同的。对位段成员研究,针对的是单一数据类型(或者说单一普通结构成员)内部位的阅读、bit位与位段的顺序关系。而对普通成员的研究,其研究对象是多个基础数据类型(或者说多个普通成员)之间的存储顺序、Byte字节与普通字段的顺序关系。
方便起见,在后文中,将普通结果成员称为字段,位域结构成员称为位段。在此基础上,上一小段描述可以简化为,位段是在字段的基础上,将一个字段划分为1-n个位段。
2、相同字段定义顺序的结构类型,其对象在小端和大端系统中,字段的存储顺序是一致的,只是多字节字段内部字节的存储顺序不一致。为了字段对齐而填充的字节,也是无论大小端,均填充在真实字段存储地址之后的更高的地址上。
3、位段没有内存地址的概念,它是对一个字段的拆分定义。无论小端和大端系统,结构中先出现的位段,都占据字段的低位,后出现的位段占据字段的高位。为了符合位段语法(不够容纳新位段时,占用新的字段空间)而浪费的bit位,始终出现在字段的低有效位上(下一节有实践代码)。

位域结构的Bit位序和Byte字节序

去年刚写到这一小节的时候,我因为搞不清字节序和位序之间的关系,卡顿了数月之久,期间想着同步写一篇关于"传输和存储/字节序和位序的区别和关联" 的文章,写了不多我就放弃了,那些探究对我来说,太过纵深了,似乎没有必要。我的目的本来没有那么复杂,我的目的本来只是简单如下,
有时候,我们并没有条件直接通过IDE内存监视器来查看一个位域结构对象的成员值,如release版本的调试过程,而整个位域结构的值,也常常被打印成U32或U16这样的存储值,而不是具体的成员值。这时候,你得有手段有能力通过输出的整数值,反推出位域结构各个字段的运行值。即使你定义了联合结构,也少不了上述分析过程,你始终得把整型值的各bit位,符合真实规则地对应给位域结构的各个成员/字段上,尤其是小端系统中,比大端系统更加复杂些。

什么是位序

首选,什么是位序?在维基百科中,其定义如下,
在这里插入图片描述
如上,位序是在计算机中用来识别一个二进制数位位置的协定(convention)。讨论数据的位序时,常会提到最高有效位(Most Significant Bit,MSb)和最低有效位(Least Significant Bit,Lsb)。MSb指的是在一个多位数据中,权重最高的一位,它在二进制表示中通常位于最左边,代表着最大的数值。在有符号数中,MSb还可以表示符号位,用于表示正负值。LSb 则是在一个多位数据中,权重最低的一位,它在二进制表示中通常位于最右边,代表着最小的数值。其实,在计算机体系下,也有最高有效字节MSB(大写B哦)和最低有效字节LSB(大写B哦)的概念,谈过字节序和大小端的所有问题,这里不再展开。

分析位段存储

下文是我早些年写的一个测例,相关结论在前文已经论证过,这里没舍得删掉,

typedef unsigned int MX_U32;
//win64 vs x86
struct TBitFieldY {
    MX_U32 u32FieldA : 3 /*bit*/;
    MX_U32 u32FieldB : 4 /*bit*/;
    MX_U32 u32FieldC : 5 /*bit*/;
    MX_U32 u32FieldD : 6 /*bit*/;
    MX_U32 u32FieldE : 7 /*bit*/;
    MX_U32 u32FieldF : 7 /*bit*/;
};
//
int main() {
    // 001 /0010 /00011 /000100 /0000101 /0000110
    TBitFieldY bitY = { 1/*3bit*/, 2/*4bit*/, 3/*5bit*/, 4/*6bit*/, 5/*7bit*/, 6/*7bit*/ };
    //for monitor mem
    void *addr = &bitY;
    ...
}

在VS内存监视器中,bitY 位段对象的存储如下,
在这里插入图片描述
如上,我们在VS的内存查看器中查看addr地址上的内存数据,将其直接翻译成二进制数据为,

//我们从首位段到尾位段的赋值 1 2 3 4 5 6
/001 /0010 /00011 /000100 /0000101 /0000110
//直译A-从低地址到高地址直译 (低字节到高字节) 0x91 0x41 0x14 0x0c
/1001 /0001 /0100 /0001 /0001 /0100 /0000 /1100
//直译B-从高地址到低地址直译 (高字节到低字节)0x0c 0x14 0x41 0x91 
/0000 /1100 /0001 /0100 /0100 /0001 /1001 /0001 

我们按照从头到尾的顺序,按照各个位段的位宽分别是 3-4-5-6-7-7 来对上述两种直译的二进制数做解读,发现都不能匹配上原始的位段的赋值啊,别急。先插入一个小段落,IDE中通常不能直接监视位的内存存储,只能监视到字节层级,所以打印二进制是一种手段,如下

//从高bit位到低bit位输出一个无符号整型数的内存
void printBits(unsigned int num) {
    unsigned int mask = 1 << (sizeof(num) * 8 - 1); // Create a mask with 1 in the most significant bit position
    while (mask > 0) {
        if (num & mask) {
            printf("1");
        } else {
            printf("0");
        }
        mask >>= 1; // Shift the mask to the right by one bit
    }
    printf("\n");
}

在这里插入图片描述
如上打印结果,与逆地址序直译的二进制数是一致的,呢,没错呢!眼尖的我们,立马按照位段从尾到头的各字段位宽 7-7-6-5-4-3bit 逆字段顺序,来重新划解,如下,
在这里插入图片描述
如上图,以绿色竖条划分,可读出原始数据 6、5、4、 3、2、 1,这就对啦。如上可得结论,前文已经提过,再说一遍:最后出现的位段,排在位序最高的位置。这里额外点别的,
内存监视器中只能查看字节,并没有位序的概念。如上,你必须要先按照小端存储特点,把内存字节值(逆序)转化为字节数据,才有资格开始谈论位序相关内容。

位段的衔接顺序

回到上一节中提到的直译A,无论按照怎样的位段顺序来解读它,都是错的。我们详细的来看看这种错误,
在这里插入图片描述
如上,橙色的箭头,无论向左和向右,都得不到我们赋值操作后期望/实际打印的位值结果。正确的是,
在这里插入图片描述
不太合适但却比较直观的表述上边这个事情的描述是,在内存监视器中0x91和0x41是相邻的两个字节,但是从更微观的位来看的话,0x91的1与0x41的4,并不是衔接的。正确的衔接位置反而是0x41的1和0x91的9。上述只是表达一个衔接的关系,并没有实际意义哈。我并不清楚在硬件层次上的存储,字节和位之间的真实关系,而且不同硬件可能有不同的实现,这里不过多讨论。

上边的分析过程,好繁琐,有没有一种更简便和易于理解的方法呢?当然是有的,我们可以先把小端的多字节,按照符合值阅读习惯的顺序排列,也即按照值的大端字节序排列,如下,
在这里插入图片描述
如上,蓝色是地址变大的方向,橙色是位方向,都是有序的方向,看起来顺眼多了。如此,也统一了小端和大端系统上,字节值翻译为位段值的方式方法。对于一个数,一个单一类型的数的存储,左侧是高bit位,这是阅读一个数的本质上的唯一顺序协定,至于硬件存储中的顺序和方法,这里不讨论。

我以前曾经这么猜想过:字节序和位序方向是一致的,这有利于处理效率的,如果位序和字节序不一致,则必须逐个字节以8bit为单位来处理,而不方便用32bit为单位一次性加载。尽管截止现在,我还是没想明白,但我想在任何硬件实现上,都已经实现了最高效的实现方案。

位域结构数据的传输问题

@202403 重启整理此文的缘由是,合理定义CAN仲裁段和传输数据的位段结构。接下来我们将聚焦位段数据的传输问题,包括NET和CAN通信。
本专栏下的《CAN总线/CAN应用层协议设计,理解并实践仲裁段位域定义》一文中,也会从STM32 CAN驱动层,寄存器层次上,对位段数据传输做进一步说明。

含位域数据的udp报文

之前写了一个案例在这里的,现在已经感觉它没有用,删掉了。以太网的传输单位是字节,也只认识字节流,跟位域字段没有任何的直接的关系,以太网也不管你是什么大端和小端,也不管你是单字节还是多字节数据。至于传递的是什么,接收的是什么,该如何解析,那是通信双方的约定。还要明确的一点是,网络报文的内容(如,Wireshark抓取的),与发送端的发送数据的内存存储的字节流是完全的一个字节不差的顺序对应的,理解了这点,剩下的就不是问题了。

大小端系统的CanID定义是一致的

针对同一份通信协议,如果通信双方,CardA采用大端字节序系统和CardB采用小端字节序系统,那么在软件代码实现过程中,CardA和CardB上的CanID的结构定义要是一致的吗?总不会要分别定义吧,那也太坑了。接下来我们就按照前文的所有整理,来回答这个问题。
在前文<位域结构的存储>那一小节中,我们验证了,相同的位段结构定义,在小端和大端系统中,字节值是一样的,只是字节序不一样。在<位域结构的Bit位序和Byte字节序>那一小节中,我们了解到,要在小端系统中阅读一个位段对象的存储,要将小端序的字节转换成大端序字节序。综上,对于一个具体的位段结构,按位来看的话,其在大端和小端系统上,木有差别。
因此,大小端系统上的表示仲裁段的位段结构定义是一样的,且仲裁级别最高的信息,放在位段结构最末尾的字段上。

CAN的字节发送顺序

我们都说 "CAN只是定义了从高位到低位的传输顺序,却没限制字节发送顺序”,这句话,好理解吗?你真的理解吗?

typedef struct {
  __IO uint32_t TIR;  /*!< CAN TX mailbox identifier register */
  __IO uint32_t TDTR; /*!< CAN mailbox data length control and time stamp register */
  __IO uint32_t TDLR; /*!< CAN mailbox data low register */
  __IO uint32_t TDHR; /*!< CAN mailbox data high register */
} CAN_TxMailBox_TypeDef;

如上,我们重点关注 TIR 寄存器(CAN 发送邮箱标识符寄存器 (CAN_TIxR) ),它被定义为 U32 类型的字段。其具体位含义如下,
在这里插入图片描述
位 2 IDE : 标识符扩展 (Identifier extension),此位用于定义邮箱中消息的标识符类型。0:标准标识符,1:扩展标识符。以标准帧为例,U32的STDID变量的低11bit,由高到低位,分别对应到CAN_TIxR邮箱表示符寄存区的31bit-21bit。结合CAN协议定义的高位优先发送规则,TIR寄存器的31bit应该是最先被发送的,也就是STDID的第10bit,或者是EXID的第28bit。

结合 <位域结构的Bit位序和Byte字节序>那一小节中的描述,对于上述 uint32_t 类型的 TIR 字段(其本身可视作一个位段对象), 在小端系统中31bit-24bit,这8位是在高地址上的,大端系统中,同样的8bit,是在低内存地址上的。但是、但是,如上8bit数据,无论存在大端的低地址还是存在小端的高内存地址,这8bit二进制数据是一致的,即值是一致的。
因此,无论发送端是小端还是大端,接收端收到的位数据是不变的,即发送端是大小端字节序不影响接收端接收到的位数据的内容和顺序。接下来我们从不同的角度细分析下。

从内存地址角度解读,
如果发送端是小端,则先发送高地址字节数据。如实发送端是大端,则先发送低地址内存数据。
从高低有效字节解读,
无论是大端还是小端,先发送的都是最高有效字节。
站在接收方的角度上,
其先收的bit一定是最高有效位,而不必关注此时的发送方是先发的高地址还是低地址的数据。接收方只需要知道自己是大端还是小端,按照字节的规则吧收到的bit存放到寄存器中,并按照字节的规则从寄存器映射到内存中就可以。收发双方,压根不关心对方是大端还是小端字节序。

对比着以太网大端字节序传输的概念,加个汤。作为以太网通信的物理层设备,PHY芯片(物理层接口芯片)在以太网通信中起着桥梁的作用,负责将数据在物理层和数据链路层之间进行转换和传输。PHY芯片通过将数据转换为符合以太网规范的信号,将其发送到网线上,以实现计算机之间的数据通信。在接收端,PHY芯片接收来自网线的二进制位数据,并将其转换为字节流,以便上层协议栈进行处理和解析。我也没在以太网规范中看到有任何提及位序的情况,不过可能是我眼瞎哈,至少这个没有在明面上限定。
PHY芯片在将二进制数据转换为字节流后,可以通过中断或轮询等方式通知上层协议栈数据的到达,数据的缓存通常是由处理器或NIC来管理。因此PHY帮你屏蔽了网络上位传输和位序的问题,只是保证,发送端先发什么字节,接收端就会先收到什么字节。

洗洗睡吧

这里本来写了<CAN数据段的大小端问题>,后来搬到《CAN总线/CAN应用层协议设计,理解并实践仲裁段位域定义》文章中去了。最后以CAN扩展帧定义来结束本文,

typedef struct{
    MX_U32 ibDDevdNo    :5;   //目标设备标识
    MX_U32 ibDDevType   :4;   //目标设备类型
    MX_U32 ibSDevdNo    :5;   //源设备标识
    MX_U32 ibSDevType   :4;   //源设备类型
    MX_U32 ibMsgF       :5;   //消息标识符
    MX_U32 ibMsgT       :4;   //消息标识符
    MX_U32 ibMsgC       :2;   //消息标识符
    MX_U32 ibReserved   :3;   //预留
} TExtendCanID;
  • 11
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值