I2C 是一種廣泛應用於嵌入式系統的雙線通訊協定,而 SMBus 作為其衍生協定,則專注於系統管理功能。在 Linux 核心開發中,撰寫 I2C 客戶端驅動程式需要遵循特定的規範和流程。驅動程式需妥善處理 I2C 與 SMBus 的相容性,並善用 Linux I2C 子系統提供的 API,例如 i2c_msg、i2c_transfer 和 i2c_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_send和i2c_master_recv函式分別用於向客戶端裝置寫入和讀取一些位元組。- 第一個引數是指向
struct i2c_client的指標,其中包含客戶端裝置的I2C地址。 - 第二個引數是指向緩衝區的指標,該緩衝區將儲存從從裝置讀取的資料和將寫入從裝置的資料。
- 第三個引數是要讀取/寫入的位元組數(必須小於緩衝區的長度,也應該小於64k,因為
msg.len是u16型別)。 - 傳回值是實際讀取/寫入的位元組數。
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_byte、i2c_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(裝置位址)、flags、read_write(讀寫方向)、command(命令碼)、size(資料大小)以及data(資料指標)等引數。 - 該函式不應直接被呼叫,而應透過更高層級的封裝函式,如
i2c_smbus_read_byte_data或i2c_smbus_write_word_data。
Linux I2C 子系統的架構
Linux I2C 子系統遵循 Linux 裝置模型,主要由以下幾個部分組成:
I2C 匯流排核心 (
drivers/i2c/i2c-core.c):- 提供 I2C Client Drivers 與 I2C Controller Drivers 之間的介面支援。
- 管理匯流排仲裁、重試處理等協定細節。
I2C 控制器驅動 (
drivers/i2c/busses/):- 為特定 SoC 的 I2C 控制器提供自定義的讀寫函式。
- 需要宣告一個包含
i2c_adapter結構的私有結構,如struct bcm2835_i2c_dev。
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);
}
程式碼解密:
devm_kzalloc():動態分配記憶體給ioexp_dev結構,並將其初始化為零。i2c_set_clientdata():將私有結構ioexp_dev與i2c_client結構關聯,以便在驅動程式的其他函式中存取私有資料結構。misc_register():向核心註冊雜項裝置,使其可供使用者空間存取。
重點整理
- 本實驗展示瞭如何為連線到 I2C 匯流排的 PCF8574 I/O 擴充套件裝置編寫驅動程式。
- 使用裝置樹來宣告 I2C 裝置,並組態引腳作為 I2C 訊號。
- 編寫驅動程式以使用 SMBus 函式讀寫 I2C 裝置。
- 使用雜項裝置介導向使用者空間提供存取介面。
I2C客戶端驅動程式開發
本章節主要介紹如何在Linux系統下開發I2C客戶端驅動程式,以控制連線在I2C匯流排上的裝置。
驅動程式基本架構
在開發I2C客戶端驅動程式之前,需要了解其基本架構。主要包括以下幾個部分:
- 私有裝置結構:定義一個私有結構來儲存與I2C客戶端相關的資訊。
- 檔案操作函式:實作
read和write函式,以處理使用者空間對裝置檔案的操作。 - Probe函式:在驅動程式與裝置匹配後被呼叫,用於初始化裝置和註冊雜項裝置。
- 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)
{
// 登出雜項裝置
}
該函式在驅動程式被移除時被呼叫,用於登出雜項裝置。
實作步驟
- 建立私有裝置結構:定義一個私有結構來儲存與I2C客戶端相關的資訊。
- 實作檔案操作函式:實作
read和write函式,以處理使用者空間對裝置檔案的操作。 - 實作Probe函式:在驅動程式與裝置匹配後被呼叫,用於初始化裝置和註冊雜項裝置。
- 實作Remove函式:在驅動程式被移除時被呼叫,用於登出雜項裝置。
- 註冊驅動程式:使用
module_i2c_driver巨集註冊I2C驅動程式。
內容解密:
上述步驟中,Probe函式的實作是關鍵。它負責初始化私有裝置結構、註冊雜項裝置以及設定檔案操作函式。Remove函式的實作則負責登出雜項裝置。此外,檔案操作函式的實作需要根據具體的裝置需求進行設計。