在嵌入式系統開發中,感測器扮演著至關重要的角色。本文將引導開發者如何在 Linux 核心中,利用工業 I/O (IIO) 子系統,開發 Nunchuk 加速度計的驅動程式。此過程涵蓋了從裝置樹的組態、提供者與消費者驅動程式的撰寫,到最終的編譯和測試驗證。此外,本文也將探討 Regmap API 的使用,藉由其提供的抽象化介面,簡化與硬體暫存器的互動,並提升程式碼的可讀性和可維護性,進一步最佳化驅動程式的開發流程。

工業 I/O 子系統章節:Nunchuk 加速度計驅動程式開發與測試

在本文中,我們將探討如何在 Linux 核心中開發和測試 Nunchuk 加速度計的驅動程式。Nunchuk 是 Wii 遊戲機的一個附件,內建加速度計,可以測量三個軸向的加速度。本文將介紹如何使用 Linux 工業 I/O(IIO)子系統來開發 Nunchuk 加速度計的驅動程式,並進行測試。

裝置樹組態

首先,我們需要在裝置樹中新增 Nunchuk 加速度計的節點。裝置樹是用於描述硬體裝置的組態檔案,Linux 核心會根據裝置樹的組態來初始化硬體裝置。

&i2c1 {
    pinctrl-names = "default";
    pinctrl-0 = <&i2c1_pins>;
    clock-frequency = <100000>;
    status = "okay";
    nunchuk_accel: nunchuk_accel@52 {
        #io-channel-cells = <1>;
        compatible = "nunchuk_accel";
        reg = <0x52>;
    };
};

&soc {
    nunchuk_consumer {
        compatible = "nunchuk_consumer";
        io-channels = <&nunchuk_accel 0>, <&nunchuk_accel 1>, <&nunchuk_accel 2>;
        io-channel-names = "accel_x", "accel_y", "accel_z";
    };
};

內容解密:

  1. &i2c1:表示對 i2c1 控制器的組態。
  2. nunchuk_accel: nunchuk_accel@52:定義了一個名為 nunchuk_accel 的節點,地址為 0x52,表示 Nunchuk 加速度計裝置。
  3. #io-channel-cells = <1>:指定 IIO 規格中的單元格數目為 1,表示該裝置有多個 IIO 輸出。
  4. compatible = "nunchuk_accel":指定該裝置的相容性字串,用於與驅動程式進行匹配。
  5. &soc:表示對系統單晶片(SoC)的組態。
  6. nunchuk_consumer:定義了一個名為 nunchuk_consumer 的節點,表示 Nunchuk 加速度計的消費者驅動程式。
  7. io-channelsio-channel-names:分別指定了 IIO 輸入通道和對應的名稱,用於與消費者驅動程式進行匹配。

驅動程式開發

接下來,我們需要開發 Nunchuk 加速度計的驅動程式。驅動程式分為兩個部分:提供者驅動程式(Provider Driver)和消費者驅動程式(Consumer Driver)。

提供者驅動程式

提供者驅動程式負責與 Nunchuk 加速度計硬體進行互動,讀取加速度資料並提供給 IIO 子系統。

// nunchuk_accel.c
#include <linux/i2c.h>
#include <linux/iio/iio.h>

static int nunchuk_accel_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    // 初始化 Nunchuk 加速度計裝置
    // ...
    return 0;
}

static const struct i2c_device_id nunchuk_accel_id[] = {
    { "nunchuk_accel", 0 },
    { }
};

static struct i2c_driver nunchuk_accel_driver = {
    .driver = {
        .name = "nunchuk_accel",
    },
    .probe = nunchuk_accel_probe,
    .id_table = nunchuk_accel_id,
};

module_i2c_driver(nunchuk_accel_driver);

內容解密:

  1. nunchuk_accel_probe:提供者驅動程式的探測函式,用於初始化 Nunchuk 加速度計裝置。
  2. i2c_device_id:定義了提供者驅動程式支援的裝置 ID。
  3. i2c_driver:定義了提供者驅動程式的結構體,包含了驅動程式的名稱、探測函式和裝置 ID 表。

消費者驅動程式

消費者驅動程式負責使用 IIO 子系統提供的介面來讀取 Nunchuk 加速度計的資料。

// nunchuk_consumer.c
#include <linux/iio/consumer.h>
#include <linux/input.h>

static int nunchuk_consumer_probe(struct platform_device *pdev)
{
    // 取得 IIO 通道
    struct iio_channel *chan_x, *chan_y, *chan_z;
    chan_x = iio_channel_get(&pdev->dev, "accel_x");
    chan_y = iio_channel_get(&pdev->dev, "accel_y");
    chan_z = iio_channel_get(&pdev->dev, "accel_z");
    // ...
    return 0;
}

static struct platform_driver nunchuk_consumer_driver = {
    .driver = {
        .name = "nunchuk_consumer",
    },
    .probe = nunchuk_consumer_probe,
};

module_platform_driver(nunchuk_consumer_driver);

內容解密:

  1. nunchuk_consumer_probe:消費者驅動程式的探測函式,用於取得 IIO 通道並初始化輸入裝置。
  2. iio_channel_get:用於取得指定的 IIO 通道。

編譯和測試

編譯和測試驅動程式需要以下步驟:

  1. 編譯提供者和消費者驅動程式。
  2. 將編譯好的驅動程式佈署到 Raspberry Pi 上。
  3. 載入提供者驅動程式並測試 Nunchuk 加速度計的功能。
  4. 載入消費者驅動程式並測試輸入裝置的功能。
# 編譯驅動程式
make

# 佈署驅動程式到 Raspberry Pi
make deploy

# 載入提供者驅動程式
insmod nunchuk_accel.ko

# 測試 Nunchuk 加速度計的功能
cat /sys/bus/iio/devices/iio\:device0/in_accel_x_raw

# 載入消費者驅動程式
insmod nunchuk_consumer.ko

# 測試輸入裝置的功能
evtest

使用 Regmap API 於裝置驅動程式

Linux 具有諸如 I2C 和 SPI 等子系統,用於連線位於這些匯流排上的裝置。這些匯流排具有讀取和寫入裝置暫存器的共同功能,通常導致在具有此暫存器讀寫功能的子系統中存在冗餘程式碼。

Regmap 子系統簡介

為了避免冗餘程式碼並促進驅動程式的維護與開發,Linux 開發者自版本 3.1 開始引入了一種新的核心 API,稱為 regmap。該基礎設施最初存在於 Linux ASoC(ALSA)子系統中,但現在已透過 regmap API(在核心原始碼樹中的 include/linux/regmap.h 定義並在 drivers/base/regmap/ 中實作)提供給整個 Linux 系統。regmap 子系統抽象了對 SPI 和 I2C 裝置暫存器的存取,以及對記憶體對映暫存器(MMIO)的存取。同時,regmap 子系統提供了快取機制,可以減少對裝置的存取次數,並能處理 IRQ 晶片和中斷請求(IRQs)。

Regmap 結構與初始化

regmap 子系統主要包含 struct regmapstruct regmap_busstruct regmap_config 等結構。struct regmap 代表慢速 I/O 裝置的暫存器操作對映,而 struct regmap_bus 代表某類別慢速 I/O 裝置(如 SPI 或 I2C 裝置)的暫存器操作,並與 struct regmap 繫結。struct regmap_config 是每個裝置和匯流排的設定結構,由驅動程式碼定義,並包含與裝置暫存器相關的所有資訊。

使用 Regmap API 的範例

對於像 ADXL345 加速度計這樣的裝置,可以使用 regmap API 開發兩個簡單的驅動程式,一個支援 I2C 匯流排(adxl345-i2c.c),另一個支援 SPI 匯流排(adxl345-spi.c)。這兩個驅動程式使用特定的程式碼組態匯流排,並使用 devm_regmap_init_spi()devm_regmap_init_i2c() 函式初始化 struct regmap

程式碼範例:I2C 與 SPI 初始化

// adxl345-i2c.c 中的 regmap 初始化
static const struct regmap_config adxl345_i2c_regmap_config = {
    .reg_bits = 8,
    .val_bits = 8,
};

static int adxl345_i2c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    struct regmap *regmap;
    regmap = devm_regmap_init_i2c(client, &adxl345_i2c_regmap_config);
    return adxl345_core_probe(&client->dev, regmap, id ? id->name : NULL);
}

// adxl345-spi.c 中的 regmap 初始化
static const struct regmap_config adxl345_spi_regmap_config = {
    .reg_bits = 8,
    .val_bits = 8,
    /* 設定第 7 和第 6 位元以啟用多位元組讀取 */
    .read_flag_mask = BIT(7) | BIT(6),
};

static int adxl345_spi_probe(struct spi_device *spi)
{
    const struct spi_device_id *id = spi_get_device_id(spi);
    struct regmap *regmap;
    regmap = devm_regmap_init_spi(spi, &adxl345_spi_regmap_config);
    return adxl345_core_probe(&spi->dev, regmap, id->name);
}

共用核心驅動程式

完成 regmap 組態和初始化後,應開發一個共用的核心驅動程式(adxl345-accel-core.c),該驅動程式可以使用以下函式與裝置進行通訊:

int regmap_write(struct regmap *map, unsigned int reg, unsigned int val);
int regmap_read(struct regmap *map, unsigned int reg, unsigned int *val);
int regmap_update_bits(struct regmap *map, unsigned int reg, unsigned int mask, unsigned int val);

內容解密:

  • regmap_write() 用於向裝置的指定暫存器寫入資料。
  • regmap_read() 用於從裝置的指定暫存器讀取資料。
  • regmap_update_bits() 用於更新裝置指定暫存器中的某些位元。

這些函式提供了對裝置暫存器的抽象化存取,簡化了驅動程式的開發。

使用 Regmap API 在裝置驅動程式中的關鍵解析

Regmap 基礎組態與重要欄位解析

Regmap 是 Linux 核心中用於簡化裝置暫存器操作的 API,其核心組態結構為 regmap_config。該結構包含了裝置暫存器對映的詳細設定,以下是其關鍵欄位的深度解析:

1. 暫存器位元數 (reg_bits) 與值位元數 (val_bits)

  • reg_bits 定義了裝置暫存器的位元數,例如 8 位元的暫存器應設為 8。
  • val_bits 則表示將寫入暫存器的值的位元數。

2. 寫入與讀取操作的控制機制

  • writeable_regreadable_reg 為回呼函式,用於檢查暫存器是否可寫或可讀。若未提供這些回呼函式,則會檢查 wr_tablerd_table 以確定操作的有效性。
  • reg_readreg_write 為可選的回呼函式,用於處理自定義的讀寫操作,尤其適用於無法以標準匯流排操作(如 SPI 或 I2C)表示讀寫動作的裝置。

3. 快取與非快取操作的管理

  • volatile_reg 回呼函式用於判斷暫存器是否為易失性(volatile)。若為易失性,則直接對硬體進行操作,繞過快取機制。
  • volatile_table 提供了一個範圍檢查,若暫存器地址落在該範圍內,則同樣會繞過快取,直接進行操作。

4. 註冊範圍與旗標遮罩

  • max_register 定義了最大的有效暫存器地址,超出此範圍的操作將被拒絕。
  • read_flag_maskwrite_flag_mask 用於在 SPI 或 I2C 等匯流排操作中區分讀寫操作,通常設定在暫存器地址的高位元組中。

Regmap API 的核心操作:regmap_writeregmap_read

Regmap 提供了一系列 API 以簡化裝置驅動程式的開發,以下是對 regmap_writeregmap_read 的深入分析:

regmap_write 操作流程

  1. 鎖定機制:根據 regmap_config 中的 fast_io 設定,選擇使用自旋鎖(spinlock)或互斥鎖(mutex)。

  2. 檢查暫存器地址:若設定了 max_register,則檢查待寫入的暫存器地址是否在有效範圍內。

  3. writeable_reg 回呼檢查:若設定了該回呼函式,則呼叫它以驗證暫存器的可寫性。若傳回 false,則操作終止並傳回錯誤碼。

  4. wr_table 範圍檢查:若未設定 writeable_reg 但定義了 wr_table,則檢查待寫入的暫存器地址是否在允許範圍內。

程式碼範例與解析

// 定義 regmap_config 結構並初始化
struct regmap_config regmap_cfg = {
    .reg_bits = 8,
    .val_bits = 8,
    .max_register = 0x20,
    .writeable_reg = my_writeable_reg,
};

// 初始化 regmap
regmap = devm_regmap_init_i2c(i2c_client, &regmap_cfg);
if (IS_ERR(regmap)) {
    dev_err(dev, "Failed to initialize regmap\n");
    return PTR_ERR(regmap);
}

// 使用 regmap_write 寫入資料到裝置
ret = regmap_write(regmap, 0x10, 0xFF);
if (ret != 0) {
    dev_err(dev, "Failed to write to register 0x10\n");
    return ret;
}

#### 內容解密:

  • .reg_bits = 8.val_bits = 8 表示該裝置使用 8 位元的暫存器和資料寬度。
  • .max_register = 0x20 設定了最大可存取的暫存器地址為 0x20,超出此範圍的操作將被拒絕。
  • .writeable_reg = my_writeable_reg 指定了一個回呼函式來檢查暫存器是否可寫。
  • 使用 devm_regmap_init_i2c 初始化 regmap,並檢查傳回值以確保初始化成功。
  • regmap_write 用於將值 0xFF 寫入暫存器地址 0x10,並處理可能的錯誤傳回。