通用位域打包和解包函数¶
问题陈述¶
当与硬件交互时,需要在几种方法之间进行选择。可以将指针内存映射到精心设计的结构体,该结构体覆盖硬件设备的内存区域,并像结构体成员一样访问其字段(可能声明为位域)。但是以这种方式编写代码会降低其可移植性,因为 CPU 和硬件设备之间可能存在字节序不匹配的问题。此外,在将硬件文档中的寄存器定义转换为结构体的位域索引时,必须密切注意。此外,一些硬件(通常是网络设备)倾向于以违反任何合理的字边界(有时甚至是 64 位边界)的方式对其寄存器字段进行分组。这会带来需要在结构体内定义寄存器字段的“高”部分和“低”部分的不便。结构体字段定义的更健壮的替代方法是通过移动适当的位数来提取所需的字段。但这仍然不能防止字节序不匹配,除非所有内存访问都是逐字节执行的。此外,代码很容易变得混乱,并且高层次的思想可能会在许多必需的位移中丢失。许多驱动程序采用位移方法,然后尝试使用定制的宏来减少混乱,但是这些宏通常会采用快捷方式,仍然会阻止代码真正可移植。
解决方案¶
此 API 处理 2 个基本操作
将 CPU 可用的数字打包到内存缓冲区(具有硬件约束/怪癖)中
将内存缓冲区(具有硬件约束/怪癖)解包为 CPU 可用的数字。
该 API 提供了对上述硬件约束和怪癖、CPU 字节序以及因此两者之间可能存在的不匹配的抽象。
这些 API 函数的基本单位是 u64。从 CPU 的角度来看,位 63 始终表示字节 7 的位偏移 7,尽管只是逻辑上的。问题是:我们在内存中的哪里布置这个位?
以下示例涵盖了打包的 u64 字段的内存布局。打包缓冲区中的字节偏移始终隐式为 0、1、... 7。示例显示的是逻辑字节和位所在的位置。
通常(没有怪癖),我们会这样做
63 62 61 60 59 58 57 56 55 54 53 52 51 50 49 48 47 46 45 44 43 42 41 40 39 38 37 36 35 34 33 32
7 6 5 4
31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
3 2 1 0
也就是说,CPU 可用的 u64 的 MSByte (7) 位于内存偏移 0 处,而 u64 的 LSByte (0) 位于内存偏移 7 处。这对应于大多数人认为的“大端”,其中位 i 对应于数字 2^i。在代码注释中,这也被称为“逻辑”表示法。
如果设置了 QUIRK_MSB_ON_THE_RIGHT,我们会这样做
56 57 58 59 60 61 62 63 48 49 50 51 52 53 54 55 40 41 42 43 44 45 46 47 32 33 34 35 36 37 38 39
7 6 5 4
24 25 26 27 28 29 30 31 16 17 18 19 20 21 22 23 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7
3 2 1 0
也就是说,QUIRK_MSB_ON_THE_RIGHT 不会影响字节位置,而是反转字节内的位偏移。
如果设置了 QUIRK_LITTLE_ENDIAN,我们会这样做
39 38 37 36 35 34 33 32 47 46 45 44 43 42 41 40 55 54 53 52 51 50 49 48 63 62 61 60 59 58 57 56
4 5 6 7
7 6 5 4 3 2 1 0 15 14 13 12 11 10 9 8 23 22 21 20 19 18 17 16 31 30 29 28 27 26 25 24
0 1 2 3
因此,QUIRK_LITTLE_ENDIAN 表示在内存区域内,来自每个 4 字节字的每个字节都放置在其相对于该字边界的镜像位置。
如果同时设置了 QUIRK_MSB_ON_THE_RIGHT 和 QUIRK_LITTLE_ENDIAN,我们会这样做
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
4 5 6 7
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
0 1 2 3
如果仅设置了 QUIRK_LSW32_IS_FIRST,我们会这样做
31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
3 2 1 0
63 62 61 60 59 58 57 56 55 54 53 52 51 50 49 48 47 46 45 44 43 42 41 40 39 38 37 36 35 34 33 32
7 6 5 4
在这种情况下,8 字节的内存区域解释如下:前 4 个字节对应于最低有效 4 字节字,后 4 个字节对应于较高有效 4 字节字。
如果设置了 QUIRK_LSW32_IS_FIRST 和 QUIRK_MSB_ON_THE_RIGHT,我们会这样做
24 25 26 27 28 29 30 31 16 17 18 19 20 21 22 23 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7
3 2 1 0
56 57 58 59 60 61 62 63 48 49 50 51 52 53 54 55 40 41 42 43 44 45 46 47 32 33 34 35 36 37 38 39
7 6 5 4
如果设置了 QUIRK_LSW32_IS_FIRST 和 QUIRK_LITTLE_ENDIAN,它看起来像这样
7 6 5 4 3 2 1 0 15 14 13 12 11 10 9 8 23 22 21 20 19 18 17 16 31 30 29 28 27 26 25 24
0 1 2 3
39 38 37 36 35 34 33 32 47 46 45 44 43 42 41 40 55 54 53 52 51 50 49 48 63 62 61 60 59 58 57 56
4 5 6 7
如果设置了 QUIRK_LSW32_IS_FIRST、QUIRK_LITTLE_ENDIAN 和 QUIRK_MSB_ON_THE_RIGHT,它看起来像这样
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
0 1 2 3
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
4 5 6 7
我们始终将偏移视为没有怪癖,并在访问内存区域之前将其翻译。
关于不是 4 的倍数的缓冲区长度的说明¶
为了处理 4 字节组相对于彼此以“小端”布局,但在组内以“大端”布局的内存布局怪癖,4 字节组的概念是打包 API 的固有组成部分(不要与内存访问混淆,内存访问是以字节为单位执行的)。
对于不是 4 的倍数的缓冲区长度,这意味着一个组将是不完整的。根据怪癖,这可能会导致通过缓冲区访问的位域不连续。打包 API 假设不连续不是内存布局的意图,因此它通过有效地将最重要的 4 个八位字节组的逻辑长度缩短为实际可用的八位字节数来避免不连续。
下面给出了一个大小为 31 字节的缓冲区的示例。物理缓冲区偏移是隐式的,并且在一个组内从左到右增加,在一个列内从上到下增加。
无怪癖
31 29 28 | Group 7 (most significant)
27 26 25 24 | Group 6
23 22 21 20 | Group 5
19 18 17 16 | Group 4
15 14 13 12 | Group 3
11 10 9 8 | Group 2
7 6 5 4 | Group 1
3 2 1 0 | Group 0 (least significant)
QUIRK_LSW32_IS_FIRST
3 2 1 0 | Group 0 (least significant)
7 6 5 4 | Group 1
11 10 9 8 | Group 2
15 14 13 12 | Group 3
19 18 17 16 | Group 4
23 22 21 20 | Group 5
27 26 25 24 | Group 6
30 29 28 | Group 7 (most significant)
QUIRK_LITTLE_ENDIAN
30 28 29 | Group 7 (most significant)
24 25 26 27 | Group 6
20 21 22 23 | Group 5
16 17 18 19 | Group 4
12 13 14 15 | Group 3
8 9 10 11 | Group 2
4 5 6 7 | Group 1
0 1 2 3 | Group 0 (least significant)
QUIRK_LITTLE_ENDIAN | QUIRK_LSW32_IS_FIRST
0 1 2 3 | Group 0 (least significant)
4 5 6 7 | Group 1
8 9 10 11 | Group 2
12 13 14 15 | Group 3
16 17 18 19 | Group 4
20 21 22 23 | Group 5
24 25 26 27 | Group 6
28 29 30 | Group 7 (most significant)
预期用途¶
选择使用此 API 的驱动程序首先需要确定上述 3 种怪癖组合(总共 8 种)中的哪一种与硬件文档描述的匹配。然后,他们应该包装 packing() 函数,创建一个新的 xxx_packing(),该函数使用设置的适当的 QUIRK_* one-hot 位来调用它。
packing() 函数返回一个 int 编码的错误代码,该代码可以保护程序员免受不正确的 API 使用。这些错误预计不会在运行时发生,因此 xxx_packing() 返回 void 并简单地吞下这些错误是合理的。或者,它可以转储堆栈或打印错误描述。