I2C 是一種廣泛應用於嵌入式系統的雙線通訊協定,而 SMBus 作為其衍生協定,則專注於系統管理功能。在 Linux 核心開發中,撰寫 I2C 客戶端驅動程式需要遵循特定的規範和流程。驅動程式需妥善處理 I2C 與 SMBus 的相容性,並善用 Linux I2C 子系統提供的 API,例如 i2c_msgi2c_transferi2c_smbus_xfer 等核心函式,以實作高效的資料傳輸。同時,理解 Linux I2C 子系統架構,包含 I2C 核心、控制器驅動程式和裝置驅動程式之間的互動關係,對於開發穩健的驅動程式至關重要。透過裝置樹的正確設定,可以將 I2C 裝置資訊整合到系統中,並簡化驅動程式的開發流程。

I2C客戶端驅動程式

I2C是一種由飛利浦開發的協定,最初用於連線CPU與電視機中的其他電路。它是一種雙線、雙向的序列匯流排,用於低速數位資料傳輸,連線一個或多個從裝置到主裝置(一個或多個匯流排控制器),提供簡單高效的資料傳輸方法。I2C協定廣泛應用於嵌入式系統中。

SMBus與I2C的關係

SMBus(系統管理匯流排)是英特爾根據I2C協定開發的衍生協定。最常見的透過SMBus連線的裝置是使用I2C EEPROM組態的RAM模組和硬體監控晶片,這些晶片監控PC主機板和嵌入式系統中的關鍵引數。由於SMBus大多是通用I2C匯流排的子集,因此可以在許多I2C系統上使用其協定。然而,有些系統不符合SMBus和I2C的電氣約束;其他系統無法實作所有常見的SMBus協定語義或訊息。

編寫I2C裝置驅動程式的最佳實踐

如果您正在為I2C裝置編寫驅動程式,請盡可能嘗試使用SMBus命令(如果裝置僅使用I2C協定的子集)。這使得在SMBus介面卡和I2C介面卡上都可以使用裝置驅動程式(SMBus命令集會自動轉換為I2C在I2C介面卡上,但純I2C命令無法在大多數純SMBus介面卡上處理)。

建立純I2C通訊的功能

以下是用於建立純I2C通訊的功能:

int i2c_master_send(struct i2c_client *client, const char *buf, int count);
int i2c_master_recv(struct i2c_client *client, char *buf, int count);

內容解密:

  • i2c_master_sendi2c_master_recv函式分別用於向客戶端裝置寫入和讀取一些位元組。
  • 第一個引數是指向struct i2c_client的指標,其中包含客戶端裝置的I2C地址。
  • 第二個引數是指向緩衝區的指標,該緩衝區將儲存從從裝置讀取的資料和將寫入從裝置的資料。
  • 第三個引數是要讀取/寫入的位元組數(必須小於緩衝區的長度,也應該小於64k,因為msg.lenu16型別)。
  • 傳回值是實際讀取/寫入的位元組數。
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msg, int num);

內容解密:

  • i2c_transfer函式傳送一系列訊息。每個訊息可以是讀取或寫入,並且可以以任何方式混合。事務是組合的:在事務之間不傳送停止位。
  • 第一個引數是指向struct i2c_adapter的指標,表示I2C介面卡。
  • 第二個引數是指向struct i2c_msg陣列的指標,表示要傳輸的訊息。
  • 第三個引數是訊息的數量。
  • 傳回值是實際傳輸的訊息數量。

I2C Client Drivers 的核心功能與實作

I2C Client Drivers 是 Linux 核心中用於與 I2C 裝置進行通訊的驅動程式。本章將探討 I2C Client Drivers 的核心功能、實作方式以及與其他 I2C 子系統元件的互動關係。

I2C 通訊的基本結構:i2c_msg

在 I2C 通訊中,i2c_msg 結構扮演著至關重要的角色。該結構包含了每條訊息的客戶端位址、訊息位元組數以及訊息資料本身。這種靈活的結構設計使得 I2C 通訊能夠支援多種不同的裝置和傳輸需求。

SMBus 通訊函式

SMBus(System Management Bus)是根據 I2C 的一種通訊協定,用於系統管理相關的任務。Linux 提供了多種 SMBus 通訊函式,例如 i2c_smbus_read_bytei2c_smbus_write_byte_data 等,這些函式均建立在 i2c_smbus_xfer 這個通用函式之上。

s32 i2c_smbus_xfer(struct i2c_adapter *adapter, u16 addr,
                  unsigned short flags, char read_write, u8 command,
                  int size, union i2c_smbus_data *data);

內容解密:

  • i2c_smbus_xfer 函式是實作 SMBus 通訊的核心。
  • 它需要 i2c_adapter 指標、addr(裝置位址)、flagsread_write(讀寫方向)、command(命令碼)、size(資料大小)以及 data(資料指標)等引數。
  • 該函式不應直接被呼叫,而應透過更高層級的封裝函式,如 i2c_smbus_read_byte_datai2c_smbus_write_word_data

Linux I2C 子系統的架構

Linux I2C 子系統遵循 Linux 裝置模型,主要由以下幾個部分組成:

  1. I2C 匯流排核心 (drivers/i2c/i2c-core.c):

    • 提供 I2C Client Drivers 與 I2C Controller Drivers 之間的介面支援。
    • 管理匯流排仲裁、重試處理等協定細節。
  2. I2C 控制器驅動 (drivers/i2c/busses/):

    • 為特定 SoC 的 I2C 控制器提供自定義的讀寫函式。
    • 需要宣告一個包含 i2c_adapter 結構的私有結構,如 struct bcm2835_i2c_dev
  3. I2C 裝置驅動

    • 分散於 drivers/ 目錄下,根據裝置型別不同而有所不同。
    • 使用 I2C 核心 API 與 I2C 裝置進行資料傳輸。

BCM2835 I2C 控制器驅動範例

對於 BCM2837 SoC,其 I2C 控制器驅動位於 drivers/i2c/busses/i2c-bcm2835.c。該驅動宣告了一個 bcm2835_i2c_dev 結構,其中包含了必要的狀態資訊和 i2c_adapter 結構。

struct bcm2835_i2c_dev {
    struct device *dev;
    void __iomem *regs;
    int irq;
    struct i2c_adapter adapter;
    // ...
};

probe() 函式中,i2c_adapter 結構被初始化,並透過 i2c_add_numbered_adapter() 函式註冊到 I2C 匯流排核心。

I2C 資料傳輸流程

當 I2C Client Driver 呼叫 i2c_smbus_write_byte_data() 等函式時,最終會呼叫到 i2c_smbus_xfer(),並進一步呼叫到特定 I2C 控制器的 master_xfer() 方法。以 BCM2837 為例,該方法指向 bcm2835_i2c_xfer() 函式,負責實際的暫存器讀寫操作。

i2c_smbus_read_word_data() -> i2c_smbus_xfer() -> i2c_smbus_xfer_emulated() -> i2c_transfer() -> __i2c_transfer() -> bcm2835_i2c_xfer()

I2C 客戶端驅動程式開發

撰寫 I2C 客戶端驅動程式是嵌入式系統開發中的一個重要環節。在本章中,我們將探討如何開發控制 I/O 擴充套件器、DAC、ADC、加速度計和多顯示 LED 控制器的 I2C 客戶端驅動程式。

I2C 客戶端驅動程式註冊

I2C 子系統定義了一個 i2c_driver 結構體,該結構體繼承自 device_driver 結構體。每個 I2C 裝置驅動程式都必須例項化並向 I2C 匯流排核心註冊。通常,您將實作一個單一的驅動程式結構,並從中例項化所有客戶端。驅動程式結構包含通用的存取例程,應當將其初始化為零,除了您提供的欄位。

以下是一個 I2C 加速度計裝置的 i2c_driver 結構體定義範例:

static struct i2c_driver ioaccel_driver = {
    .driver = {
        .name = "mma8451",
        .owner = THIS_MODULE,
        .of_match_table = ioaccel_dt_ids,
    },
    .probe = ioaccel_probe,
    .remove = ioaccel_remove,
    .id_table = i2c_ids,
};

使用 i2c_add_driver()i2c_del_driver() 函式註冊和登出驅動程式。這些函式包含在 init()exit() 函式中。如果驅動程式在這些函式中不執行其他操作,請使用 module_i2c_driver() 巨集。

內容解密:

  • .name欄位指定了驅動程式的名稱。
  • .owner欄位指定了驅動程式的所有者,通常為THIS_MODULE
  • .of_match_table欄位是一個指向of_device_id結構陣列的指標,用於儲存相容字串。
  • .probe.remove欄位分別指定了驅動程式的探測和移除函式。
  • .id_table欄位是一個指向i2c_device_id結構陣列的指標,用於非根據裝置樹的 I2C 裝置探測。

裝置樹中的 I2C 裝置宣告

在裝置樹中,I2C 控制器通常在描述處理器的 .dtsi 檔案中宣告。以 BCM2837 SoC 為例,I2C 控制器的定義通常宣告為 status = "disabled"。例如,在 bcm283x.dtsi 檔案中,宣告了三個 I2C 控制器,它們將透過 of_platform_populate() 函式註冊到 I2C 匯流排核心。

I2C 控制器註冊流程

@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title Linux I2C 客戶端驅動程式開發

package "Linux Shell 操作" {
    package "檔案操作" {
        component [ls/cd/pwd] as nav
        component [cp/mv/rm] as file
        component [chmod/chown] as perm
    }

    package "文字處理" {
        component [grep] as grep
        component [sed] as sed
        component [awk] as awk
        component [cut/sort/uniq] as text
    }

    package "系統管理" {
        component [ps/top/htop] as process
        component [systemctl] as service
        component [cron] as cron
    }

    package "管線與重導向" {
        component [| 管線] as pipe
        component [> >> 輸出] as redirect
        component [$() 命令替換] as subst
    }
}

nav --> file : 檔案管理
file --> perm : 權限設定
grep --> sed : 過濾處理
sed --> awk : 欄位處理
pipe --> redirect : 串接命令
process --> service : 服務管理

note right of pipe
  命令1 | 命令2
  前者輸出作為後者輸入
end note

@enduml

此圖示展示了 I2C 控制器的註冊流程,從裝置樹中的宣告到 I2C 匯流排核心的註冊。

I2C 客戶端驅動程式

I2C 裝置樹宣告

I2C 裝置的裝置樹宣告是在板級或平台級別(例如 arch/arm/boot/dts/bcm2710-rpi-3-b.dts)作為 I2C 主控制器的子節點來完成的。

  • I2C 控制器裝置被啟用(status = "okay")。
  • I2C 匯流排頻率是透過 clock-frequency 屬性來定義的。
  • I2C 匯流排上的裝置被描述為 I2C 控制器節點的子節點。reg 屬性提供了裝置在匯流排上的 I2C 從屬地址。
  • 在 I2C 裝置節點中,compatible 屬性必須與驅動程式的 of_device_id 相容字串相匹配。

實驗 6.1:I2C I/O 擴充套件裝置模組

在本實驗中,您將實作第一個用於控制 I2C 裝置的驅動程式。該驅動程式將管理連線到 I2C 匯流排的 PCF8574 I/O 擴充套件裝置。

硬體描述

使用 Raspberry Pi 的 GPIO 擴充套件接頭來取得 I2C 訊號。GPIO2 和 GPIO3 引腳將用於取得 SDA1 和 SCL1 訊號。

裝置樹描述

開啟並修改 bcm2710-rpi-3-b.dts 裝置樹檔案,在 i2c1 控制器主節點內新增 ioexp@39 子節點。i2c1 控制器透過將 status 屬性設為 “okay” 來啟用。i2c1 節點的 pinctrl-0 屬性指向 i2c1_pins 引腳組態節點,其中 GPIO2 和 GPIO3 被複用為 I2C 訊號。在子節點裝置的宣告中,reg 屬性提供了連線到 I2C 匯流排的 PCF8574 I/O 擴充套件裝置的 I2C 地址。

&i2c1 {
    pinctrl-names = "default";
    pinctrl-0 = <&i2c1_pins>;
    clock-frequency = <100000>;
    status = "okay";
    [...]
    ioexp@39 {
        compatible = "arrow,ioexp";
        reg = <0x39>;
    };
};

i2c1_pins 引腳組態節點

i2c1_pins: i2c1 {
    brcm,pins = <2 3>; /* GPIO2 和 GPIO3 引腳 */
    brcm,function = <4>; /* ALT0 多路復用功能 */
};

“I2C I/O 擴充套件裝置” 模組的程式碼描述

1. 包含必要的標頭檔

#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/i2c.h>
#include <linux/fs.h>
#include <linux/of.h>
#include <linux/uaccess.h>

2. 定義私有結構

struct ioexp_dev {
    struct i2c_client *client;
    struct miscdevice ioexp_miscdevice;
    char name[8]; /* ioexpXX */
};
  • ioexp_dev 結構的第一個欄位是 i2c_client 結構,用於處理 I2C 裝置。
  • 第二個欄位是 miscdevice 結構,用於自動處理 open() 函式。

3. 定義檔案操作結構

static const struct file_operations ioexp_fops = {
    .owner = THIS_MODULE,
    .read = ioexp_read_file,
    .write = ioexp_write_file,
};

4. probe() 函式

static int ioexp_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    static int counter = 0;
    struct ioexp_dev *ioexp;

    /* 分配私有結構 */
    ioexp = devm_kzalloc(&client->dev, sizeof(struct ioexp_dev), GFP_KERNEL);

    /* 將指標儲存到 bus 裝置上下文中的裝置結構 */
    i2c_set_clientdata(client, ioexp);

    /* 儲存指向 I2C 使用者端的指標 */
    ioexp->client = client;

    /*
     * 初始化雜項裝置,ioexp 在每次 probe 呼叫後遞增
     */
    sprintf(ioexp->name, "ioexp%02d", counter++);
    ioexp->ioexp_miscdevice.name = ioexp->name;
    ioexp->ioexp_miscdevice.minor = MISC_DYNAMIC_MINOR;
    ioexp->ioexp_miscdevice.fops = &ioexp_fops;

    /* 註冊雜項裝置 */
    return misc_register(&ioexp->ioexp_miscdevice);
}

程式碼解密:

  1. devm_kzalloc():動態分配記憶體給 ioexp_dev 結構,並將其初始化為零。
  2. i2c_set_clientdata():將私有結構 ioexp_devi2c_client 結構關聯,以便在驅動程式的其他函式中存取私有資料結構。
  3. misc_register():向核心註冊雜項裝置,使其可供使用者空間存取。

重點整理

  • 本實驗展示瞭如何為連線到 I2C 匯流排的 PCF8574 I/O 擴充套件裝置編寫驅動程式。
  • 使用裝置樹來宣告 I2C 裝置,並組態引腳作為 I2C 訊號。
  • 編寫驅動程式以使用 SMBus 函式讀寫 I2C 裝置。
  • 使用雜項裝置介導向使用者空間提供存取介面。

I2C客戶端驅動程式開發

本章節主要介紹如何在Linux系統下開發I2C客戶端驅動程式,以控制連線在I2C匯流排上的裝置。

驅動程式基本架構

在開發I2C客戶端驅動程式之前,需要了解其基本架構。主要包括以下幾個部分:

  1. 私有裝置結構:定義一個私有結構來儲存與I2C客戶端相關的資訊。
  2. 檔案操作函式:實作readwrite函式,以處理使用者空間對裝置檔案的操作。
  3. Probe函式:在驅動程式與裝置匹配後被呼叫,用於初始化裝置和註冊雜項裝置。
  4. Remove函式:在驅動程式被移除時被呼叫,用於登出雜項裝置。

程式碼解析

私有裝置結構

struct ioexp_dev {
    struct i2c_client *client;
    struct miscdevice ioexp_miscdevice;
    char name[8]; /* ioexpXX */
};

該結構儲存了I2C客戶端的指標、雜項裝置結構以及裝置名稱。

檔案操作函式

static ssize_t ioexp_read_file(struct file *file, char __user *userbuf, size_t count, loff_t *ppos)
{
    // 從I2C裝置讀取資料並傳送到使用者空間
}

static ssize_t ioexp_write_file(struct file *file, const char __user *userbuf, size_t count, loff_t *ppos)
{
    // 從使用者空間接收資料並寫入I2C裝置
}

這兩個函式分別處理使用者空間對裝置檔案的讀寫操作。

Probe函式

static int ioexp_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    // 初始化私有裝置結構並註冊雜項裝置
}

該函式在驅動程式與裝置匹配後被呼叫,用於初始化裝置和註冊雜項裝置。

Remove函式

static int ioexp_remove(struct i2c_client *client)
{
    // 登出雜項裝置
}

該函式在驅動程式被移除時被呼叫,用於登出雜項裝置。

實作步驟

  1. 建立私有裝置結構:定義一個私有結構來儲存與I2C客戶端相關的資訊。
  2. 實作檔案操作函式:實作readwrite函式,以處理使用者空間對裝置檔案的操作。
  3. 實作Probe函式:在驅動程式與裝置匹配後被呼叫,用於初始化裝置和註冊雜項裝置。
  4. 實作Remove函式:在驅動程式被移除時被呼叫,用於登出雜項裝置。
  5. 註冊驅動程式:使用module_i2c_driver巨集註冊I2C驅動程式。

內容解密:

上述步驟中,Probe函式的實作是關鍵。它負責初始化私有裝置結構、註冊雜項裝置以及設定檔案操作函式。Remove函式的實作則負責登出雜項裝置。此外,檔案操作函式的實作需要根據具體的裝置需求進行設計。