通用位字段打包和解包函数
问题陈述
在处理硬件时,人们必须在几种接口方式之间进行选择。可以将指针内存映射到硬件设备的内存区域上,并将其字段作为结构体成员(可能声明为位字段)进行访问。但是以这种方式编写代码会使其不太可移植,因为CPU和硬件设备之间可能存在字节序不匹配的问题。此外,在将硬件文档中的寄存器定义转换为结构体的位字段索引时,人们必须特别注意。此外,一些硬件(通常是网络设备)倾向于以违反任何合理字边界的方式对其寄存器字段进行分组(有时甚至是64位的)。这会带来不便,需要在结构体内定义寄存器字段的“高”和“低”部分。相对于结构体字段定义,更健壮的替代方案是通过移位适当数量的位来提取所需的字段。但是,这仍然无法防止字节序不匹配,除非所有内存访问都是逐字节执行的。此外,代码很容易变得混乱,并且高层次的思想可能会在许多所需的位移中丢失。许多驱动程序采用位移方法,然后尝试使用定制的宏来减少混乱,但往往这些宏采用了仍然阻止代码真正可移植的捷径。
解决方案
此API涉及两个基本操作:
- 将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的最高有效字节(7)位于内存偏移0处,u64的最低有效字节(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
我们始终将偏移量视为没有怪癖,并在访问内存区域之前进行转换。
预期用途
选择使用此API的驱动程序首先需要确定上述3种怪癖组合(共8种)中的哪一种与硬件文档描述相匹配。然后,他们应该包装packing()函数,创建一个新的xxx_packing(),使用适当的QUIRK_*
位设置来调用它。
packing()函数返回一个int编码的错误代码,这可以保护程序员免受不正确的API使用。不希望在运行时发生错误,因此xxx_packing()返回void并简单地忽略这些错误是合理的。可选地,它可以转储堆栈或打印错误描述。