在嵌入式系統開發中,感測器扮演著至關重要的角色。本文將引導開發者如何在 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";
};
};
內容解密:
&i2c1:表示對 i2c1 控制器的組態。nunchuk_accel: nunchuk_accel@52:定義了一個名為nunchuk_accel的節點,地址為0x52,表示 Nunchuk 加速度計裝置。#io-channel-cells = <1>:指定 IIO 規格中的單元格數目為 1,表示該裝置有多個 IIO 輸出。compatible = "nunchuk_accel":指定該裝置的相容性字串,用於與驅動程式進行匹配。&soc:表示對系統單晶片(SoC)的組態。nunchuk_consumer:定義了一個名為nunchuk_consumer的節點,表示 Nunchuk 加速度計的消費者驅動程式。io-channels和io-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);
內容解密:
nunchuk_accel_probe:提供者驅動程式的探測函式,用於初始化 Nunchuk 加速度計裝置。i2c_device_id:定義了提供者驅動程式支援的裝置 ID。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);
內容解密:
nunchuk_consumer_probe:消費者驅動程式的探測函式,用於取得 IIO 通道並初始化輸入裝置。iio_channel_get:用於取得指定的 IIO 通道。
編譯和測試
編譯和測試驅動程式需要以下步驟:
- 編譯提供者和消費者驅動程式。
- 將編譯好的驅動程式佈署到 Raspberry Pi 上。
- 載入提供者驅動程式並測試 Nunchuk 加速度計的功能。
- 載入消費者驅動程式並測試輸入裝置的功能。
# 編譯驅動程式
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 regmap、struct regmap_bus 和 struct 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_reg和readable_reg為回呼函式,用於檢查暫存器是否可寫或可讀。若未提供這些回呼函式,則會檢查wr_table和rd_table以確定操作的有效性。reg_read和reg_write為可選的回呼函式,用於處理自定義的讀寫操作,尤其適用於無法以標準匯流排操作(如 SPI 或 I2C)表示讀寫動作的裝置。
3. 快取與非快取操作的管理
volatile_reg回呼函式用於判斷暫存器是否為易失性(volatile)。若為易失性,則直接對硬體進行操作,繞過快取機制。volatile_table提供了一個範圍檢查,若暫存器地址落在該範圍內,則同樣會繞過快取,直接進行操作。
4. 註冊範圍與旗標遮罩
max_register定義了最大的有效暫存器地址,超出此範圍的操作將被拒絕。read_flag_mask和write_flag_mask用於在 SPI 或 I2C 等匯流排操作中區分讀寫操作,通常設定在暫存器地址的高位元組中。
Regmap API 的核心操作:regmap_write 與 regmap_read
Regmap 提供了一系列 API 以簡化裝置驅動程式的開發,以下是對 regmap_write 和 regmap_read 的深入分析:
regmap_write 操作流程
鎖定機制:根據
regmap_config中的fast_io設定,選擇使用自旋鎖(spinlock)或互斥鎖(mutex)。檢查暫存器地址:若設定了
max_register,則檢查待寫入的暫存器地址是否在有效範圍內。writeable_reg回呼檢查:若設定了該回呼函式,則呼叫它以驗證暫存器的可寫性。若傳回 false,則操作終止並傳回錯誤碼。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, ®map_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,並處理可能的錯誤傳回。