在嵌入式系統中,感測器是連接物理世界與數位世界的橋樑。ADXL345 作為一款常用的三軸加速度計,其 Linux 驅動程式的開發涉及了核心驅動的幾個關鍵面向。本文將以一個從底層到上層的清晰流程,深入解析 ADXL345 驅動程式的設計與實現,涵蓋 SPI 通訊、Sysfs 介面、中斷處理以及與輸入子系統的整合。
步驟一:驅動程式的骨架與註冊
任何一個 Linux 裝置驅動程式的起點,都是向核心註冊一個代表自己的結構體。對於 SPI 裝置,我們需要定義一個 spi_driver 結構。
// adxl345.c
// 1. 定義裝置樹相容性字串
static const struct of_device_id adxl345_dt_ids[] = {
{ .compatible = "arrow,adxl345" },
{ }
};
MODULE_DEVICE_TABLE(of, adxl345_dt_ids);
// 2. 定義 spi_driver 結構
static struct spi_driver adxl345_driver = {
.driver = {
.name = "adxl345",
.of_match_table = adxl345_dt_ids,
},
.probe = adxl345_spi_probe, // 探測函式
.remove = adxl345_spi_remove, // 移除函式
};
// 3. 使用輔助宏註冊驅動
module_spi_driver(adxl345_driver);
程式碼解說:
of_device_id: 用於裝置樹 (Device Tree) 的匹配。當核心在裝置樹中找到一個compatible屬性為"arrow,adxl345"的節點時,就會呼叫這個驅動的probe函式。spi_driver: 包含了驅動的名稱、匹配表,以及最重要的兩個回呼函式:probe(當裝置被發現時呼叫)和remove(當裝置被移除時呼叫)。module_spi_driver: 這是一個輔助宏,它會自動生成模組的init和exit函式,並在其中呼叫spi_register_driver和spi_unregister_driver。
圖表解說:驅動程式核心元件圖
此圖展示了 ADXL345 SPI 驅動程式與 Linux 核心中其他子系統的依賴關係。
@startuml
!theme _none_
skinparam dpi auto
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam minClassWidth 100
skinparam defaultFontSize 16
title ADXL345 驅動程式元件關係
package "ADXL345 驅動程式" {
[adxl345_driver] as driver
[probe] as probe
[remove] as remove
[of_match_table] as dts
}
[Linux SPI 核心] ..> driver : 註冊/管理
driver -- probe
driver -- remove
driver -- dts
probe ..> [Linux 輸入子系統] : 註冊 input_dev
probe ..> [Sysfs] : 建立裝置屬性
dts ..> [Device Tree] : 匹配裝置
@enduml
步驟二:硬體通訊 - SPI 介面
在 probe 函式被呼叫後,驅動程式需要與硬體進行通訊來初始化裝置。ADXL345 使用 SPI 匯流排,因此我們需要封裝 SPI 的讀寫操作。
// 讀取多個暫存器
static int adxl345_spi_read_block(struct device *dev, unsigned char reg, int count, void *buf) {
struct spi_device *spi = to_spi_device(dev);
ssize_t status;
// ADXL345 的 SPI 協議要求在暫存器位址前加上特定的命令位元
reg = ADXL345_READMB_CMD(reg);
// 使用核心提供的 SPI 同步寫後讀函式
status = spi_write_then_read(spi, ®, 1, buf, count);
return (status < 0) ? status : 0;
}
// 寫入單一暫存器
static int adxl345_spi_write(struct device *dev, unsigned char reg, unsigned char val) {
struct spi_device *spi = to_spi_device(dev);
return spi_w8r8(spi, ADXL345_WRITE_CMD(reg)); // 假設有 WRITE_CMD 宏
}
圖表解說:SPI 區塊讀取時序圖
此循序圖展示了驅動程式透過 SPI 核心從 ADXL345 硬體讀取資料的詳細過程。
@startuml
!theme _none_
skinparam dpi auto
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam minClassWidth 100
skinparam defaultFontSize 16
title SPI 區塊讀取時序
participant "驅動程式" as Driver
participant "SPI 核心" as Core
participant "ADXL345 硬體" as HW
Driver -> Driver : 組合讀取指令 (reg | CMD_BITS)
Driver -> Core : 呼叫 spi_write_then_read()
Core -> HW : 發送讀取指令 (reg)
HW --> Core : 回傳資料 (buf)
Core --> Driver : 回傳讀取狀態與資料
Driver -> Driver : 處理回傳狀態
@enduml
步驟三:核心功能 - 讀取加速度資料
有了底層的通訊能力,我們就可以實現讀取三軸加速度資料的核心功能。
struct axis_triple { s16 x, y, z; };
// 讀取三軸資料
static void adxl345_get_triple(struct adxl345 *ac, struct axis_triple *axis) {
__le16 buf[3]; // 緩衝區,用於存放從硬體讀取的原始資料
// 呼叫我們封裝的 SPI 讀取函式
ac->bops->read_block(ac->dev, DATAX0, sizeof(buf), buf);
// 將小端序 (little-endian) 的原始資料轉換為主機的 CPU 架構格式,
// 並進行符號擴展 (因為 ADXL345 的資料是 13 位元有符號數)
axis->x = sign_extend32(le16_to_cpu(buf[0]), 12);
axis->y = sign_extend32(le16_to_cpu(buf[1]), 12);
axis->z = sign_extend32(le16_to_cpu(buf[2]), 12);
}
步驟四:與使用者空間互動 - Sysfs 介面
為了讓使用者空間的應用程式能夠讀取加速度資料或設定裝置參數,我們透過 Sysfs 導出一些虛擬檔案。
// 讀取位置資訊的回呼函式
static ssize_t adxl345_position_read(struct device *dev, struct device_attribute *attr, char *buf) {
struct adxl345 *ac = dev_get_drvdata(dev);
struct axis_triple axis;
adxl345_get_triple(ac, &axis);
return sprintf(buf, "(%d, %d, %d)\n", axis.x, axis.y, axis.z);
}
// 定義 Sysfs 屬性
static DEVICE_ATTR(position, S_IRUGO, adxl345_position_read, NULL);
// 在 probe 函式中建立 Sysfs 檔案
// sysfs_create_file(&spi->dev.kobj, &dev_attr_position.attr);
使用方式:在驅動載入後,使用者只需執行 cat /sys/bus/spi/devices/.../position 即可讀取目前的加速度值。
步驟五:事件驅動 - 中斷與輸入子系統
ADXL345 能夠在偵測到敲擊 (tap) 事件時產生硬體中斷。我們的驅動程式可以捕捉這個中斷,並將其轉換為一個標準的 Linux 輸入事件。
1. 中斷處理函式 (IRQ Handler)
static irqreturn_t adxl345_irq(int irq, void *handle) {
struct adxl345 *ac = handle;
int int_stat;
// 讀取中斷來源暫存器,確認是何種中斷
int_stat = AC_READ(ac, INT_SOURCE);
// 檢查是否為單次敲擊事件
if (int_stat & SINGLE_TAP) {
// 報告一個標準的按鍵事件
input_report_key(ac->input, BTN_TAP, 1); // 按下
input_sync(ac->input);
input_report_key(ac->input, BTN_TAP, 0); // 釋放
input_sync(ac->input);
}
return IRQ_HANDLED;
}
2. 在 probe 函式中註冊中斷與輸入裝置
// adxl345_spi_probe() 函式內部
// ...
// 1. 配置輸入裝置
ac->input = devm_input_allocate_device(dev);
ac->input->name = "adxl345";
__set_bit(EV_KEY, ac->input->evbit);
__set_bit(BTN_TAP, ac->input->keybit);
// 2. 註冊輸入裝置
err = input_register_device(ac->input);
// 3. 申請中斷
err = devm_request_threaded_irq(dev, ac->irq, NULL, adxl345_irq,
IRQF_TRIGGER_RISING | IRQF_ONESHOT,
dev_name(dev), ac);
// ...
完成以上步驟後,當使用者敲擊裝置時,應用程式就可以像監聽鍵盤按鍵一樣,從 /dev/input/eventX 讀取到一個 BTN_TAP 事件。透過這個完整的流程,我們成功地將一個硬體裝置的功能,層層抽象並整合到 Linux 核心的標準框架中。