【嵌入式Linux驅動開發】二十四、Linux I2C 驅動上手嘗試

  人的前程關於眼界、關乎格局。
  志之所趨,無遠弗屆,窮山復海不能限也;
  志之所向,無堅不入,銳兵精甲不能御也。


一、I2C驅動框架簡介

  Linux內核將 I2C 驅動分爲兩部分:

  • ①、 I2C 總線驅動, I2C 總線驅動就是 SOC 的 I2C 控制器驅動,也叫做 I2C 適配器驅動。
  • ②、 I2C 設備驅動, I2C 設備驅動就是針對具體的 I2C 設備而編寫的驅動。

1.2、I2C總線驅動

  platform 是虛擬出來的一條總線,目的是爲了實現總線、設備、驅動框架。對於 I2C 而言,不需要虛擬出一條總線,直接使用 I2C總線即可。I2C 總線驅動重點是 I2C 適配器(也就是 SOC 的 I2C 接口控制器)驅動,這裏要用到兩個重要的數據結構: i2c_adapteri2c_algorithm

  Linux 內核將 SOC 的 I2C 適配器(控制器)抽象成 i2c_adapteri2c_adapter 結構體定義在include/linux/i2c.h 文件中,結構體內容如下:

struct i2c_adapter {
	struct module *owner;
	unsigned int class; /* classes to allow probing for */
	const struct i2c_algorithm *algo; /* 總線訪問算法 */
	void *algo_data;
	
	/* data fields that are valid for all devices */
	struct rt_mutex bus_lock;
	
	int timeout; /* in jiffies */
	int retries;
	struct device dev; /* the adapter device */
	
	int nr;
	char name[48];
	struct completion dev_released;
	
	struct mutex userspace_clients_lock;
	struct list_head userspace_clients;
	
	struct i2c_bus_recovery_info *bus_recovery_info;
	const struct i2c_adapter_quirks *quirks;
};

  對於一個 I2C 適配器,肯定要對外提供讀寫 API 函數,設備驅動程序可以使用這些 API 函數來完成讀寫操作。 i2c_algorithm 就是 I2C 適配器與 IIC 設備進行通信的方法。

  i2c_algorithm 結構體定義在 include/linux/i2c.h 文件中,內容如下:

struct i2c_algorithm {
	......
	int (*master_xfer)(struct i2c_adapter *adap,
	struct i2c_msg *msgs,
	int num);
	int (*smbus_xfer) (struct i2c_adapter *adap, u16 addr,
	unsigned short flags, char read_write,
	u8 command, int size, union i2c_smbus_data *data);
	
	/* To determine what the adapter supports */
	u32 (*functionality) (struct i2c_adapter *);
	......
};

  上述代碼中,master_xfer 是 I2C 適配器的傳輸函數,可以通過此函數來完成與 IIC 設備之間的通信。smbus_xfer 是 SMBUS 總線的傳輸函數。

  綜上所述, I2C 總線驅動,或者說 I2C 適配器驅動的主要工作就是初始化 i2c_adapter 結構體變量,然後設置 i2c_algorithm 中的 master_xfer 函數。完成以後通過 i2c_add_numbered_adapteri2c_add_adapter 這兩個函數向系統註冊設置好的 i2c_adapter,這兩個函數的原型如下:

//adapter 或 adap:要添加到 Linux 內核中的 i2c_adapter,也就是 I2C 適配器。
//返回值: 0,成功;負值,失敗。
int i2c_add_adapter(struct i2c_adapter *adapter)
int i2c_add_numbered_adapter(struct i2c_adapter *adap)

  這兩個函數的區別在於 i2c_add_adapter 使用動態總線號,而 i2c_add_numbered_adapter使用靜態總線號

  如果要刪除 I2C 適配器的話使用 i2c_del_adapter 函數即可,函數原型如下:

//adap:要刪除的 I2C 適配器。
void i2c_del_adapter(struct i2c_adapter * adap)

  關於 I2C 的總線(控制器或適配器)驅動就介紹到這裏,一般 SOC 的 I2C 總線驅動都是由半導體廠商編寫的,比如 I.MX6U 的 I2C 適配器驅動 NXP 已經編寫好了,這個不需要用戶去編寫。因此 I2C 總線驅動對我們這些 SOC 使用者來說是被屏蔽掉的,我們只要專注於 I2C 設備驅動即可。除非你是在半導體公司上班,工作內容就是寫 I2C 適配器驅動。

1.3、I2C設備驅動

  I2C 設備驅動重點關注兩個數據結構: i2c_clienti2c_driver,根據總線、設備和驅動型,I2C 總線上一小節已經講了。還剩下設備和驅動, i2c_client 就是描述設備信息的, i2c_driver 描述驅動內容,類似於 platform_driver

1.3.1、 i2c_client 結構體

  i2c_client 結構體定義在 include/linux/i2c.h 文件中,內容如下:

struct i2c_client {
	unsigned short flags; /* 標誌 */
	unsigned short addr; /* 芯片地址, 7 位,存在低 7 位*/
	......
	char name[I2C_NAME_SIZE]; /* 名字 */
	struct i2c_adapter *adapter; /* 對應的 I2C 適配器 */
	struct device dev; /* 設備結構體 */
	int irq; /* 中斷 */
	struct list_head detected;
	......
};

  一個設備對應一個 i2c_client,每檢測到一個 I2C 設備就會給這個 I2C 設備分配一個i2c_client。

1.3.2、i2c_driver 結構體

  i2c_driver 類似 platform_driver,是我們編寫 I2C 設備驅動重點要處理的內容, i2c_driver 結構體定義在 include/linux/i2c.h 文件中。對於我們 I2C 設備驅動編寫人來說,重點工作就是構建 i2c_driver,構建完成以後需要向Linux 內核註冊這個 i2c_driver。 i2c_driver 註冊函數爲 int i2c_register_driver,此函數原型如下:

//owner: 一般爲 THIS_MODULE。
//driver:要註冊的 i2c_driver。
//返回值: 0,成功;負值,失敗。
int i2c_register_driver(struct module *owner, struct i2c_driver *driver)

  另外 i2c_add_driver 也常常用於註冊 i2c_driver, i2c_add_driver 是一個宏,定義如下:

#define i2c_add_driver(driver) i2c_register_driver(THIS_MODULE, driver)

  i2c_add_driver 就是對 i2c_register_driver 做了一個簡單的封裝,只有一個參數,就是要註冊的 i2c_driver。

  註銷 I2C 設備驅動的時候需要將前面註冊的 i2c_driver 從 Linux 內核中註銷掉,需要用到i2c_del_driver 函數,此函數原型如下:

//driver:要註銷的 i2c_driver。
void i2c_del_driver(struct i2c_driver *driver)

i2c_driver 的註冊示例代碼

/* i2c 驅動的 probe 函數 */
static int xxx_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
	/* 函數具體程序 */
	return 0;
}

/* i2c 驅動的 remove 函數 */
static int ap3216c_remove(struct i2c_client *client)
{
	/* 函數具體程序 */
	return 0;
}

/* 傳統匹配方式 ID 列表 */
static const struct i2c_device_id xxx_id[] = {
	{"xxx", 0},
	{}
};

/* 設備樹匹配列表 */
static const struct of_device_id xxx_of_match[] = {
	{ .compatible = "xxx" },
	{ /* Sentinel */ }
};

/* i2c 驅動結構體 */
static struct i2c_driver xxx_driver = {
	probe = xxx_probe,
	remove = xxx_remove,
	driver = {
		owner = THIS_MODULE,
		name = "xxx",
		of_match_table = xxx_of_match,
	},
	id_table = xxx_id,
};

/* 驅動入口函數 */
static int __init xxx_init(void)
{
	int ret = 0;
	
	ret = i2c_add_driver(&xxx_driver);
	return ret;
}

/* 驅動出口函數 */
static void __exit xxx_exit(void)
{
	i2c_del_driver(&xxx_driver);
}

module_init(xxx_init);
module_exit(xxx_exit);

二、I2C 設備驅動編寫流程

  I2C 適配器驅動 SOC 廠商已經替我們編寫好了,我們需要做的就是編寫具體的設備驅動,本小節我們就來學習一下 I2C 設備驅動的詳細編寫流程。

2.1、 I2C 設備信息描述

2.1.1、未使用設備樹

  首先肯定要描述 I2C 設備節點信息,先來看一下沒有使用設備樹的時候是如何在 BSP 裏面描述 I2C 設備信息的,在未使用設備樹的時候需要在 BSP 裏面使用 i2c_board_info 結構體來描述一個具體的 I2C 設備。 i2c_board_info 結構體如下:

struct i2c_board_info {
	char type[I2C_NAME_SIZE]; /* I2C 設備名字 */
	unsigned short flags; /* 標誌 */
	unsigned short addr; /* I2C 器件地址 */
	void *platform_data;
	struct dev_archdata *archdata;
	struct device_node *of_node;
	struct fwnode_handle *fwnode;
	int irq;
};

  其中,type 和 addr 這兩個成員變量是必須要設置的,一個是 I2C 設備的名字,一個是 I2C 設備的器件地址。

  可以在 Linux 源碼裏面全局搜索 i2c_board_info,會找到大量以 i2c_board_info 定義的I2C 設備信息,這些就是未使用設備樹的時候 I2C 設備的描述方式,當採用了設備樹以後就不會再使用i2c_board_info 來描述 I2C 設備了。

2.1.2、使用設備樹

  使用設備樹的時候 I2C 設備信息通過創建相應的節點就行了,比如 NXP 官方的 EVK 開發板在 I2C1 上接了 mag3110 這個磁力計芯片,因此必須在 i2c1 節點下創建 mag3110 子節點,然後在這個子節點內描述 mag3110 這個芯片的相關信息。打開 imx6ull-14x14-evk.dts 這個設備樹文件,然後找到如下內容:

&i2c1 {
	clock-frequency = <100000>;
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_i2c1>;
	status = "okay";

	mag3110@0e {
		compatible = "fsl,mag3110";
		reg = <0x0e>;
		position = <2>;
	};
	......
};

  第 7~11 行,向 i2c1 添加 mag3110 子節點,第 7 行“ mag3110@0e”是子節點名字,“ @”後面的“ 0e”就是 mag3110 的 I2C 器件地址。第 8 行設置 compatible 屬性值爲“ fsl,mag3110”。第 9 行的 reg 屬性也是設置 mag3110 的器件地址的,因此值爲 0x0e。

  I2C 設備節點的創建重點是 compatible 屬性和 reg 屬性的設置,一個用於匹配驅動,一個用於設置器件地址。

2.2、I2C 設備數據收發處理流程

  前面說過,I2C 設備驅動首先要做的就是初始化 i2c_driver 並向 Linux 內核註冊。當設備和驅動匹配以後 i2c_driver 裏面的 probe 函數就會執行, probe 函數裏面所做的就是字符設備驅動那一套了。

  一般需要在 probe 函數裏面初始化 I2C 設備,要初始化 I2C 設備就必須能夠對 I2C 設備寄存器進行讀寫操作,這裏就要用到 i2c_transfer 函數了i2c_transfer 函數最終會調用 I2C 適配器中i2c_algorithm 裏面的 master_xfer 函數,對於 I.MX6U 而言就是i2c_imx_xfer 這個函數。 i2c_transfer 函數原型如下:

//adap: 所使用的 I2C 適配器, i2c_client 會保存其對應的 i2c_adapter。
//msgs: I2C 要發送的一個或多個消息。
//num: 消息數量,也就是 msgs 的數量。
//返回值: 負值,失敗,其他非負值,發送的 msgs 數量。
int i2c_transfer(struct i2c_adapter *adap,
				struct i2c_msg *msgs,
				int num)

  重點來看一下 msgs 這個參數,這是一個 i2c_msg 類型的指針參數, I2C 進行數據收發。說白了就是消息的傳遞, Linux 內核使用 i2c_msg 結構體來描述一個消息。 i2c_msg 結構體定義在 include/uapi/linux/i2c.h 文件中,結構體內容如下:

struct i2c_msg {
	__u16 addr; /* 從機地址 */
	__u16 flags; /* 標誌 */
	#define I2C_M_TEN 0x0010
	#define I2C_M_RD 0x0001
	#define I2C_M_STOP 0x8000
	#define I2C_M_NOSTART 0x4000
	#define I2C_M_REV_DIR_ADDR 0x2000
	#define I2C_M_IGNORE_NAK 0x1000
	#define I2C_M_NO_RD_ACK 0x0800
	#define I2C_M_RECV_LEN 0x0400
	__u16 len; /* 消息(本 msg)長度 */
	__u8 *buf; /* 消息數據 */
};

  使用 i2c_transfer 函數發送數據之前要先構建好 i2c_msg

使用 i2c_transfer 進行 I2C 數據收發的示例代碼如下:

/* 設備結構體 */
struct xxx_dev {
	......
	void *private_data; /* 私有數據,一般會設置爲 i2c_client */
};

/*
* @description : 讀取 I2C 設備多個寄存器數據
* @param – dev : I2C 設備
* @param – reg : 要讀取的寄存器首地址
* @param – val : 讀取到的數據
* @param – len : 要讀取的數據長度
* @return : 操作結果
*/
static int xxx_read_regs(struct xxx_dev *dev, u8 reg, void *val, int len)
{
	int ret;
	struct i2c_msg msg[2];
	struct i2c_client *client = (struct i2c_client *)
	dev->private_data;
	
	/* msg[0],第一條寫消息,發送要讀取的寄存器首地址 */
	msg[0].addr = client->addr; /* I2C 器件地址 */
	msg[0].flags = 0; /* 標記爲發送數據 */
	msg[0].buf = &reg; /* 讀取的首地址 */
	msg[0].len = 1; /* reg 長度 */
	
	/* msg[1],第二條讀消息,讀取寄存器數據 */
	msg[1].addr = client->addr; /* I2C 器件地址 */
	msg[1].flags = I2C_M_RD; /* 標記爲讀取數據 */
	msg[1].buf = val; /* 讀取數據緩衝區 */
	msg[1].len = len; /* 要讀取的數據長度 */
	
	ret = i2c_transfer(client->adapter, msg, 2);
	if(ret == 2) {
		ret = 0;
	} else {
		ret = -EREMOTEIO;
	}
	return ret;
}

/*
* @description : 向 I2C 設備多個寄存器寫入數據
* @param – dev : 要寫入的設備結構體
* @param – reg : 要寫入的寄存器首地址
* @param – val : 要寫入的數據緩衝區
* @param – len : 要寫入的數據長度
* @return : 操作結果
*/
static s32 xxx_write_regs(struct xxx_dev *dev, u8 reg, u8 *buf, u8 len)
{
	u8 b[256];
	struct i2c_msg msg;
	struct i2c_client *client = (struct i2c_client *)
	dev->private_data;
	
	b[0] = reg; /* 寄存器首地址 */
	memcpy(&b[1],buf,len); /* 將要發送的數據拷貝到數組 b 裏面 */
	
	msg.addr = client->addr; /* I2C 器件地址 */
	msg.flags = 0; /* 標記爲寫數據 */
	
	msg.buf = b; /* 要發送的數據緩衝區 */
	msg.len = len + 1; /* 要發送的數據長度 */
	
	return i2c_transfer(client->adapter, &msg, 1);
}

  上述例程需要說明的是:

  • 第 2~5行,設備結構體,在設備結構體裏面添加一個執行 void的指針成員變量 private_data,此成員變量用於保存設備的私有數據。在 I2C 設備驅動中我們一般將其指向 I2C 設備對應的i2c_client。
  • 第 15~40 行, xxx_read_regs 函數用於讀取 I2C 設備多個寄存器數據。第 18 行定義了一個i2c_msg 數組, 2 個數組元素,因爲 I2C 讀取數據的時候要先發送要讀取的寄存器地址,然後再讀取數據,所以需要準備兩個 i2c_msg。一個用於發送寄存器地址,一個用於讀取寄存器值。對於 msg[0],將 flags 設置爲 0,表示寫數據。 msg[0]的 addr 是 I2C 設備的器件地址, msg[0]的 buf成員變量就是要讀取的寄存器地址。對於 msg[1],將 flags 設置爲 I2C_M_RD,表示讀取數據。msg[1]的 buf 成員變量用於保存讀取到的數據, len 成員變量就是要讀取的數據長度。調用i2c_transfer 函數完成 I2C 數據讀操作。
  • 第 50~66 行, xxx_write_regs 函數用於向 I2C 設備多個寄存器寫數據, I2C 寫操作要比讀操作簡單一點,因此一個 i2c_msg 即可。數組 b 用於存放寄存器首地址和要發送的數據,第 59 行設置 msg 的 addr 爲 I2C 器件地址。第 60 行設置 msg 的 flags 爲 0,也就是寫數據。第 62 行設置要發送的數據,也就是數組 b。第 63 行設置 msg 的 len 爲 len+1,因爲要加上一個字節的寄存器地址。最後通過 i2c_transfer 函數完成向 I2C 設備的寫操作。

  另外還有兩個API函數分別用於I2C數據的收發操作,這兩個函數最終都會調用 i2c_transfer。首先來看一下 I2C 數據發送函數 i2c_master_send,函數原型如下:

//client: I2C 設備對應的 i2c_client。
//buf:要發送的數據。
//count: 要發送的數據字節數,要小於 64KB,因爲 i2c_msg 的 len 成員變量是一個 u16(無符號 16 位)類型的數據。
//返回值: 負值,失敗,其他非負值,發送的字節數
int i2c_master_send(const struct i2c_client *client,
					const char *buf,
					int count)

I2C 數據接收函數爲 i2c_master_recv,函數原型如下:

//client: I2C 設備對應的 i2c_client。
//buf:要接收的數據。
//count: 要接收的數據字節數,要小於 64KB,因爲 i2c_msg 的 len 成員變量是一個 u16(無符號 16 位)類型的數據。
//返回值: 負值,失敗,其他非負值,發送的字節數。
int i2c_master_recv(const struct i2c_client *client,
					char *buf,
					int count)

  關於 Linux 下 I2C 設備驅動的編寫流程就講解到這裏,重點就是 i2c_msg 的構建和i2c_transfer 函數的調用,接下來我們就編寫 AP3216C 這個 I2C 設備的 Linux 驅動。

二、程序編寫

2.1、修改設備樹

2.1.1、IO修改

  首先肯定是要修改 IO, AP3216C 用到了 I2C1 接口, I.MX6U-ALPHA 開發板上的 I2C1 接口使用到了 UART4_TXD 和 UART4_RXD,因此肯定要在設備樹裏面設置這兩個 IO。如果要用到 AP3216C 的中斷功能的話還需要初始化 AP_INT 對應的 GIO1_IO01 這個 IO,本章實驗我們不使用中斷功能。因此只需要設置 UART4_TXD 和 UART4_RXD 這兩個 IO, NXP 其實已經將他這兩個 IO 設置好了,打開 imx6ull-alientek-emmc.dts,然後找到如下內容:

pinctrl_i2c1: i2c1grp {
	fsl,pins = <
		MX6UL_PAD_UART4_TX_DATA__I2C1_SCL 0x4001b8b0
		MX6UL_PAD_UART4_RX_DATA__I2C1_SDA 0x4001b8b0
	>;
};

  pinctrl_i2c1 就是 I2C1 的 IO 節點,這裏將 UART4_TXD 和 UART4_RXD 這兩個 IO 分別複用爲 I2C1_SCL 和 I2C1_SDA,電氣屬性都設置爲 0x4001b8b0。

2.1.2、在 i2c1 節點追加 ap3216c 子節點

  AP3216C 是連接到 I2C1 上的,因此需要在 i2c1 節點下添加 ap3216c 的設備子節點,在 imx6ull-alientek-emmc.dts 文件中找到 i2c1 節點,刪除該節點下默認的mag3110和fxls8471子節點信息,加入ap3216c子節點信息,最終i2c1節點內容如下:

&i2c1 {
	clock-frequency = <100000>;
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_i2c1>;
	status = "okay";

	ap3216c@1e {
		compatible = "alientek,ap3216c";
		reg = <0x1e>;
	};
};

需要說明的:

  • 第 7 行, ap3216c 子節點, @後面的“ 1e”是 ap3216c 的器件地址。
  • 第 8 行,設置 compatible 值爲“ alientek,ap3216c”。
  • 第 9 行, reg 屬性也是設置 ap3216c 器件地址的,因此 reg 設置爲 0x1e。

  設備樹修改完成以後使用“ make dtbs”重新編譯一下,然後使用新的設備樹啓動 Linux 內核。 /sys/bus/i2c/devices 目錄下存放着所有 I2C 設備,如果設備樹修改正確的話,會在/sys/bus/i2c/devices 目錄下看到一個名爲“ 0-001e”的子目錄,進入到該目錄,使用cat name查看該目錄下name文件的屬性,該屬性即設備名字!如下圖所示:

在這裏插入圖片描述

2.2、AP3216C 驅動編寫

三、運行程序

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章