英語

通用位欄位打包和解包函式

問題陳述

與硬體互動時,人們必須在幾種方法中做出選擇。一種方法是將精心設計的結構體指標記憶體對映到硬體裝置的記憶體區域上,並將其欄位作為結構體成員(可能宣告為位欄位)進行訪問。但這種編碼方式會降低可移植性,因為 CPU 和硬體裝置之間可能存在位元組序不匹配問題。此外,在將硬體文件中的暫存器定義轉換為結構體的位欄位索引時,必須格外注意。此外,一些硬體(通常是網路裝置)傾向於以違反任何合理字邊界(有時甚至是 64 位邊界)的方式對暫存器欄位進行分組。這帶來了不得不在結構體中定義暫存器欄位的“高”和“低”部分的麻煩。比結構體欄位定義更健壯的替代方案是透過適當的位移來提取所需的欄位。但這仍然無法防止位元組序不匹配,除非所有記憶體訪問都是逐位元組進行的。此外,程式碼很容易變得混亂,高階思想可能會在所需的眾多位移操作中迷失。許多驅動程式採用位移方法,然後嘗試透過定製宏來減少混亂,但這些宏往往採取捷徑,從而仍然阻礙程式碼真正可移植。

解決方案

此 API 處理 2 個基本操作

  • 將 CPU 可用的數字打包到記憶體緩衝區中(帶硬體約束/怪癖)

  • 將記憶體緩衝區(具有硬體約束/怪癖)解包為 CPU 可用的數字。

此 API 提供了對上述硬體約束和怪癖、CPU 位元組序以及兩者之間可能存在的任何不匹配的抽象。

這些 API 函式的基本單位是 u64。從 CPU 的角度來看,位 63 總是指位元組 7 的位偏移 7,儘管這僅是邏輯上的。問題是:我們如何將這個位在記憶體中佈局?

以下示例涵蓋了打包的 u64 欄位的記憶體佈局。打包緩衝區中的位元組偏移量總是隱式地為 0, 1, ... 7。這些示例顯示了邏輯位元組和位的位置。

  1. 通常(無怪癖),我們會這樣做

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。在程式碼註釋中,這也被稱為“邏輯”表示法。

  1. 如果設定了 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 不影響位元組定位,但會反轉位元組內部的位偏移量。

  1. 如果設定了 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 位元組字中的每個位元組都相對於該字的邊界放置在其映象位置。

  1. 如果同時設定了 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
  1. 如果僅設定了 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 位元組字。

  1. 如果設定了 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
  1. 如果設定了 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
  1. 如果設定了 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_u8struct 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);
}