通用位字段打包和解包函数¶
问题陈述¶
与硬件交互时,人们必须在几种方法中做出选择。一种方法是将精心设计的结构体指针内存映射到硬件设备的内存区域上,并将其字段作为结构体成员(可能声明为位字段)进行访问。但这种编码方式会降低可移植性,因为 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 的最高有效字节 (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
我们总是将偏移量视为没有怪癖的情况,然后在访问内存区域之前对其进行转换。
关于缓冲区长度不是 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 种)中,哪些与硬件文档中描述的相匹配。
有 3 种支持的使用模式,详述如下。
packing()¶
此 API 函数已弃用。
packing() 函数返回一个整数编码的错误代码,这可以保护程序员免受不正确的 API 使用。这些错误不应在运行时发生,因此将 packing() 封装到一个返回 void 并吞噬这些错误的自定义函数中是合理的。可选地,它可以转储堆栈或打印错误描述。
void my_packing(void *buf, u64 *val, int startbit, int endbit,
size_t len, enum packing_op op)
{
int err;
/* Adjust quirks accordingly */
err = packing(buf, val, startbit, endbit, len, op, QUIRK_LSW32_IS_FIRST);
if (likely(!err))
return;
if (err == -EINVAL) {
pr_err("Start bit (%d) expected to be larger than end (%d)\n",
startbit, endbit);
} else if (err == -ERANGE) {
if ((startbit - endbit + 1) > 64)
pr_err("Field %d-%d too large for 64 bits!\n",
startbit, endbit);
else
pr_err("Cannot store %llx inside bits %d-%d (would truncate)\n",
*val, startbit, endbit);
}
dump_stack();
}
pack() 和 unpack()¶
它们是 packing() 的 const-correct 变体,并消除了最后一个“enum packing_op op”参数。
调用 pack(...) 等效于(且更推荐)调用 packing(..., PACK)。
调用 unpack(...) 等效于(且更推荐)调用 packing(..., UNPACK)。
pack_fields() 和 unpack_fields()¶
当缓冲区中表示了许多字段时,该库提供了优化的函数,它鼓励消费者驱动程序避免为每个字段重复调用 pack() 和 unpack(),而是使用 pack_fields() 和 unpack_fields(),这可以减少代码占用。
这些 API 使用 struct packed_field_u8
或 struct packed_field_u16
数组中的字段定义,允许消费者驱动程序根据其自定义需求最小化这些数组的大小。
pack_fields() 和 unpack_fields() API 函数实际上是宏,它们根据传入的字段数组类型在编译时自动选择适当的函数。
与 pack() 和 unpack() 相比,另一个好处是字段定义的健全性检查是在编译时使用 BUILD_BUG_ON
处理的,而不是仅在执行有问题代码时才处理。这些函数返回 void,因此无需对其进行封装以处理意外错误。
建议(但非强制要求)将您的打包缓冲区封装到一个固定大小的结构化类型中。这通常使编译器更容易强制使用正确大小的缓冲区。
以下是使用字段 API 的示例
/* Ordering inside the unpacked structure is flexible and can be different
* from the packed buffer. Here, it is optimized to reduce padding.
*/
struct data {
u64 field3;
u32 field4;
u16 field1;
u8 field2;
};
#define SIZE 13
typdef struct __packed { u8 buf[SIZE]; } packed_buf_t;
static const struct packed_field_u8 fields[] = {
PACKED_FIELD(100, 90, struct data, field1),
PACKED_FIELD(90, 87, struct data, field2),
PACKED_FIELD(86, 30, struct data, field3),
PACKED_FIELD(29, 0, struct data, field4),
};
void unpack_your_data(const packed_buf_t *buf, struct data *unpacked)
{
BUILD_BUG_ON(sizeof(*buf) != SIZE;
unpack_fields(buf, sizeof(*buf), unpacked, fields,
QUIRK_LITTLE_ENDIAN);
}
void pack_your_data(const struct data *unpacked, packed_buf_t *buf)
{
BUILD_BUG_ON(sizeof(*buf) != SIZE;
pack_fields(buf, sizeof(*buf), unpacked, fields,
QUIRK_LITTLE_ENDIAN);
}