1. 基本概念
本文默認讀者掌握裸機下的I2C操作,該部分只做簡單介紹, 主要內容是對linux-4.12系統下I2C驅動的分析。(上一篇二十一、Linux驅動之IIC驅動(基於linux2.6.22.6內核)對linux-2.6.22.6內核的I2C進行了分析,新內核的I2C有了很大的變化,但是也有部分類似,爲了保證完整性,我會全部從頭分析。linux-4.12的移植和對應之前驅動的移植以後會進行更新)
I2C是由數據線SDA和時鐘SCL構成的串行總線,可發送和接收數據,是一個多主機的半雙工通信方式,每個掛接在總線上的器件都有個唯一的地址。位速在標準模式下可達100kbit/s,在快速模式下可達400kbit/s,在高速模式下可待3.4Mbit/s。
I2C總線僅僅使用SCL、 SDA這兩根信號線就實現了設備之間的數據交互, 極大地簡化了對硬件資源和PCB板佈線空間的佔用。 因此, I2C總線非常廣泛地應用在EEPROM、 實時鐘、 小型LCD等設備與CPU的接口中。
2. I2C結構
2.1 文件結構
在linux-4.12內核的driver/i2c目錄下內有如下文件:
2.1.1 algos文件夾
裏面保存I2C的通信方面的算法(algorithms)。
2.1.2 busses文件夾
裏面保存I2C總線驅動相關的文件,比如i2c-omap.c、 i2c-versatile.c、 i2c-s3c2410.c等。
2.1.3 muxes文件夾
裏面保存I2C切換芯片相關驅動,本節不使用。
2.1.4 i2c-core.c文件
完成I2C總線、設備、驅動模型,對用戶提供sys文件系統訪問支持;爲I2C內部adpter等提供註冊接口。
2.1.5 i2c-dev.c文件
實現了I2C適配器設備文件的功能,每一個I2C適配器都被分配爲一個設備。通過適配器訪問設備是的主設備號都爲89,次設備號爲0~255。應用程序通過“i2c-%d”(i2c-0、i2c-1、i2c-2...)文件名並使用文件操作結構open()、write()、read()、ioctl()和close()等來訪問這個設備。
i2c-dev.c並沒有針對特定的設備而設計,只是提供了通用read()等接口,應用程序可以借用這些接口訪問適配器上的I2C設別的存儲空間或寄存器,並控制I2C設別的工作方式。
2.2 I2C體系架構
I2C體系架構圖如下:
主要分爲3個部分,I2C核心、I2C總線驅動、I2C設備驅動。
2.2.1 I2C核心
是Linux內核用來維護和管理的I2C的核心部分,其中維護了兩個靜態的List,分別記錄系統中的I2C driver結構和I2C adapter結構。I2C core提供接口函數,允許一個I2C adatper、I2C driver和I2C client初始化時在I2C core中進行註冊,以及退出時進行註銷等操作函數。同時還提供了I2C總線讀寫訪問的一般接口。
2.2.2 I2C總線驅動
I2C總線驅動是對I2C硬件體系結構中適配器端的實現,適配器可由CPU控制,甚至可以直接集成在CPU內部。I2C總線驅動主要包含了I2C適配器數據結構i2c_adapter、I2C適配器的算法結構i2c_algorithm和控制I2C適配器產生通信信號的函數。經由I2C總線驅動的代碼,我們可以控制I2C適配器以主控方式產生開始位、停止位、讀寫週期,以及以從設備方式被讀寫、產生ACK等。
2.2.3 I2C設備驅動
I2C設備驅動是對I2C硬件體系結構中設備端的實現,設備一般掛接在受CPU控制的I2C適配器上,通過I2C適配器與CPU交換數據。I2C設備驅動主要包含了數據結構i2c_driver和i2c_client,我們需要根據具體設備實現其中的成員函數,可以爲一個具體的I2C設備開發特定的I2C設備驅動程序,在驅動中完成對特定的數據格式的解釋以及實現一些專用的功能。
2.2.4 各結構之間的關係
1. i2c_adapter與i2c_algorithm
i2c_adapter對應與物理上的一個適配器,而i2c_algorithm對應一套通信方法,一個i2c適配器需要i2c_algorithm中提供的(i2c_algorithm中的又是更下層與硬件相關的代碼提供)通信函數來控制適配器上產生特定的訪問週期。缺少i2c_algorithm的i2c_adapter什麼也做不了,因此i2c_adapter中包含其使用i2c_algorithm的指針。
2. i2c_driver和i2c_client
i2c_driver對應一套驅動方法,i2c_client對應真實的i2c物理設備device,每個i2c設備都需要一個i2c_client來描述i2c_driver與i2c_client的關係是一對多。一個i2c_driver上可以支持多個同等類型的i2c_client。
3. i2c_adapter和i2c_client
i2c_adapter和i2c_client的關係與i2c硬件體系中適配器和設備的關係一致,即i2c_client依附於i2c_adapter,由於一個適配器上可以連接多個i2c設備,所以i2c_adapter中包含依附於它的i2c_client的鏈表。
2.2.5 爲什麼構建這麼複雜的I2C體系架構?
在單片機中使用I2C只需要簡單地配置幾個I2C寄存器,構建包含開始位、停止位、讀寫週期、ACK等簡單概念函數即可,爲什麼在Linux上需要構建這麼複雜的I2C體系架構呢?爲了通用性及爲了符合Linux內核驅動模式。
對於linux系統來說,支持各式各樣的CPU類型,並且還想要滿足各種I2C芯片硬件特性,就必須將一些穩定性好的部分抽離出來,這樣就抽象出了i2c_client(描述具體的I2C設備)、i2c_driver(描述操作I2C設備方法的接口)、i2c_adpter(描述CPU如何控制I2C總線)、i2c_algorithm(I2C通信算法)等。前兩個就是抽象出的I2C設備驅動,後兩個爲I2C總線驅動。
在一個SoC上可能有多條I2C總線,一條總線對應一個I2C總線驅動(I2C適配器),每一條總線上又可以接多個I2C設備。假如一個SoC上有3個I2C適配器,外接3個I2C設備,想要3個適配器分別都能驅動3個I2C設備,我們只需寫3個適配器代碼,3個I2C設備代碼即可。
3. 分析內核(linux-4.12)
下面開始深入內核分析I2C驅動。
3.1 重要數據結構
內核的I2C驅動圍繞着以下幾個數據結構進行盤根交錯的展開。
3.1.1 i2c_adapter(I2C適配器結構)
i2c_adapter是用來描述一個I2C適配器,在SoC中的指的就是內部外設I2C控制器,當向I2C核心層註冊一個I2C適配器時就需要提供這樣的一個結構體變量。
struct i2c_adapter {
struct module *owner; //所有者
/*
class表示該I2C bus支持哪些類型的I2C從設備,只有匹配的I2C從設備才能和適配器綁定。
具體的類型包括:
1. I2C_CLASS_HWMON,硬件監控類,如lm_sensors等;
2. I2C_CLASS_DDC,DDC是數字顯示通道(Digital Display Channel)的意思, 通常用於顯示設備信息的獲取;
3. I2C_CLASS_SPD,存儲類的模組;
4. I2C_CLASS_DEPRECATED,不支持自動探測。
*/
unsigned int class;
const struct i2c_algorithm *algo; //該適配器與從設備的通信算法
void *algo_data;
/* data fields that are valid for all devices */
const struct i2c_lock_operations *lock_ops;
struct rt_mutex bus_lock;
struct rt_mutex mux_lock;
int timeout; //超時時間
int retries; //重試次數
struct device dev; //該適配器設備對應的device
int nr; //該適配器的ID,會體現在sysfs中(/sys/bus/i2c/devices/i2c-n中的‘n’)
char name[48]; //適配器的名字
struct completion dev_released; //用來掛接與適配器匹配成功的從設備i2c_client的一個鏈表頭
struct mutex userspace_clients_lock;
struct list_head userspace_clients; //clients鏈表
struct i2c_bus_recovery_info *bus_recovery_info;
const struct i2c_adapter_quirks *quirks;
struct irq_domain *host_notify_domain;
};
3.1.2 i2c_algorithm(I2C算法結構)
i2c_algorithm結構體代表的是適配器的通信算法,在構建i2c_adapter結構體變量的時候會去填充這個元素。
struct i2c_algorithm {
/*
I2C協議有關的數據傳輸接口,輸入參數是struct i2c_msg類型(可參考2.3小節的介紹)
的數組(大小由num指定)。返回值是成功傳輸的msg的個數,如有錯誤返回負值。
*/
int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs,
int num);
/*SMBUS有關的數據傳輸接口,如果爲NULL,I2C core會嘗試使用master_xfer模擬。*/
int (*smbus_xfer) (struct i2c_adapter *adap, u16 addr,
unsigned short flags, char read_write,
u8 command, int size, union i2c_smbus_data *data);
/*
該I2C adapter支持的功能:
1. I2C_FUNC_I2C,支持傳統的I2C功能;
2. I2C_FUNC_10BIT_ADDR,支持10bit地址;
3. I2C_FUNC_PROTOCOL_MANGLING,支持非標準的協議行爲(具體請參考2.3小節的介紹);
4. I2C_FUNC_NOSTART,支持不需要發送START信號的I2C傳輸(具體請參考2.3小節的介紹);
5. I2C_FUNC_SMBUS_xxx,SMBUS相關的功能,不再詳細介紹。
*/
u32 (*functionality) (struct i2c_adapter *);
#if IS_ENABLED(CONFIG_I2C_SLAVE)
int (*reg_slave)(struct i2c_client *client);
int (*unreg_slave)(struct i2c_client *client);
#endif
};
3.1.3 i2c_client(I2C次設備結構)
i2c_client描述一個I2C次設備。
struct i2c_client {
unsigned short flags; // 標誌
unsigned short addr; // 該I2C設備的7位設備地址
char name[I2C_NAME_SIZE]; // 設備名字
struct i2c_adapter *adapter; // 該I2C設備支持哪個適配器
struct device dev; // 設備結構體
int irq; //設備所使用的結構體
struct list_head detected; //鏈表頭
#if IS_ENABLED(CONFIG_I2C_SLAVE)
i2c_slave_cb_t slave_cb; /* callback for slave mode */
#endif
};
3.1.4 i2c_driver(I2C設備驅動結構)
i2c_driver描述一個I2C設備驅動。
struct i2c_driver {
unsigned int class; //所屬類
/* Notifies the driver that a new bus has appeared. You should avoid
* using this, it will be removed in a near future.
*/
int (*attach_adapter)(struct i2c_adapter *) __deprecated; //匹配適配器函數指針,不使用
/* 標準驅動模型接口 */
int (*probe)(struct i2c_client *, const struct i2c_device_id *);
int (*remove)(struct i2c_client *);
/* 新驅動模型接口 */
int (*probe_new)(struct i2c_client *);
/* driver model interfaces that don't relate to enumeration */
void (*shutdown)(struct i2c_client *);
/* 鬧鐘回調函數 */
void (*alert)(struct i2c_client *, enum i2c_alert_protocol protocol,
unsigned int data);
/* 類似於ioctl的命令接口函數 */
int (*command)(struct i2c_client *client, unsigned int cmd, void *arg);
struct device_driver driver; //該i2c設備驅動所對應的device_driver
const struct i2c_device_id *id_table; //存放該i2c設備驅動id名,用於與i2c_client->name比較匹配
/* 檢測回調函數 */
int (*detect)(struct i2c_client *, struct i2c_board_info *);
const unsigned short *address_list;
struct list_head clients;
bool disable_i2c_core_irq_mapping;
};
3.1.5 i2c_msg(I2C數據包)
i2c_msg描述一個I2C數據包。
struct i2c_msg {
__u16 addr; //I2C從機的設備地址
__u16 flags; //當flags=0表示寫, flags= I2C_M_RD表示讀
#define I2C_M_RD 0x0001 /* read data, from slave to master */
/* I2C_M_RD is guaranteed to be 0x0001! */
#define I2C_M_TEN 0x0010 /* this is a ten bit chip address */
#define I2C_M_RECV_LEN 0x0400 /* length will be first received byte */
#define I2C_M_NO_RD_ACK 0x0800 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_IGNORE_NAK 0x1000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_REV_DIR_ADDR 0x2000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NOSTART 0x4000 /* if I2C_FUNC_NOSTART */
#define I2C_M_STOP 0x8000 /* if I2C_FUNC_PROTOCOL_MANGLING */
__u16 len; //傳輸的數據長度,等於buf數組裏的字節數
__u8 *buf; //存放數據的數組
};
3.2 分析I2C總線驅動
內核是最好的學習工具。想要寫一個linux驅動,最好的方法就是分析內核自帶的驅動程序。對於I2C總線驅動我們可以分析內核源碼drivers/i2c/busses/i2c-s3c2410.c文件。
3.2.1 入口函數
static const struct platform_device_id s3c24xx_driver_ids[] = {
{
.name = "s3c2410-i2c",
.driver_data = 0,
}, {
.name = "s3c2440-i2c",
.driver_data = QUIRK_S3C2440,
}, {
.name = "s3c2440-hdmiphy-i2c",
.driver_data = QUIRK_S3C2440 | QUIRK_HDMIPHY | QUIRK_NO_GPIO,
}, { },
};
... ...
static struct platform_driver s3c24xx_i2c_driver = {
.probe = s3c24xx_i2c_probe,
.remove = s3c24xx_i2c_remove,
.id_table = s3c24xx_driver_ids,
.driver = {
.name = "s3c-i2c",
.pm = S3C24XX_DEV_PM_OPS,
.of_match_table = of_match_ptr(s3c24xx_i2c_match),
},
};
static int __init i2c_adap_s3c_init(void)
{
return platform_driver_register(&s3c24xx_i2c_driver); //以platform模型註冊platform_driver
}
首先在platform總線上註冊一個platform_driver結構,當platform總線上有與id_table->name相同(“s3c2440-i2c”)的platform_device被註冊或已經註冊時調用s3c2440_i2c_driver->probe函數。也就是s3c24xx_i2c_probe()函數。
3.2.2 s3c24xx_i2c_probe()函數
static int s3c24xx_i2c_probe(struct platform_device *pdev)
{
struct s3c24xx_i2c *i2c;
struct s3c2410_platform_i2c *pdata = NULL;
... ...
pdata = dev_get_platdata(&pdev->dev); //得到資源
... ...
/* 分配相關結構體內存 */
i2c = devm_kzalloc(&pdev->dev, sizeof(struct s3c24xx_i2c), GFP_KERNEL);
if (!i2c)
return -ENOMEM;
i2c->pdata = devm_kzalloc(&pdev->dev, sizeof(*pdata), GFP_KERNEL);
if (!i2c->pdata)
return -ENOMEM;
... ...
/* 填充適配器i2c_adapter結構體 */
strlcpy(i2c->adap.name, "s3c2410-i2c", sizeof(i2c->adap.name)); //填充適配器名字
i2c->adap.owner = THIS_MODULE; //所有者
i2c->adap.algo = &s3c24xx_i2c_algorithm; //填充算法
i2c->adap.retries = 2; //重試次數
i2c->adap.class = I2C_CLASS_DEPRECATED; //支持的設備類型,I2C_CLASS_DEPRECATED表示不支持自動檢測
i2c->tx_setup = 50;
init_waitqueue_head(&i2c->wait); //初始化等待隊列頭
...
i2c->clk = devm_clk_get(&pdev->dev, "i2c"); //獲得時鐘
/* 獲得資源並映射 */
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
i2c->regs = devm_ioremap_resource(&pdev->dev, res);
/*設置i2c_adapter適配器結構體, 將i2c結構體設爲adap的私有數據成員*/
i2c->adap.algo_data = i2c;
i2c->adap.dev.parent = &pdev->dev;
/*初始化I2C控制器*/
ret = clk_prepare_enable(i2c->clk); //使能時鐘
if (ret) {
dev_err(&pdev->dev, "I2C clock enable failed\n");
return ret;
}
ret = s3c24xx_i2c_init(i2c); //初始化2440相關寄存器
clk_disable(i2c->clk); //使能時鐘
/*申請中斷*/
ret = devm_request_irq(&pdev->dev, i2c->irq, s3c24xx_i2c_irq,0, dev_name(&pdev->dev), i2c);
... ...
/*設置i2c_adapter適配器*/
i2c->adap.nr = i2c->pdata->bus_num; //設置適配器ID,如果私有數據沒有設置默認爲0
i2c->adap.dev.of_node = pdev->dev.of_node;
... ...
/*註冊i2c_adapter適配器*/
ret = i2c_add_numbered_adapter(&i2c->adap);
... ...
}
該函數主要作用如下:
1. 分配設置i2c_adapter適配器相關結構
2. 硬件相關設置
3. 設置等待隊列
4. 申請中斷
5. 註冊i2c_adapter適配器
最後調用i2c_add_numbered_adapter註冊i2c_adapter適配器。
3.2.3 註冊i2c_adapter適配器
看看該函數是如何實現的:
int i2c_add_numbered_adapter(struct i2c_adapter *adap)
{
if (adap->nr == -1) /* adap->nr=0,不滿足條件 */
return i2c_add_adapter(adap);
return __i2c_add_numbered_adapter(adap);
}
註冊i2c_adapter適配器有兩個函數,i2c_add_adapter會自動分配adapter ID,i2c_add_numbered_adapter則可以指定ID。
在內核啓動時調用(mach-smdk2440.c裏)smdk2440_machine_init()函數,裏面又調用s3c_i2c0_set_platdata()函數。函數代碼如下:
void __init s3c_i2c0_set_platdata(struct s3c2410_platform_i2c *pd)
{
struct s3c2410_platform_i2c *npd;
if (!pd) {
pd = &default_i2c_data;
pd->bus_num = 0;
}
npd = s3c_set_platdata(pd, sizeof(struct s3c2410_platform_i2c),
&s3c_device_i2c0);
if (!npd->cfg_gpio)
npd->cfg_gpio = s3c_i2c0_cfg_gpio;
}
在s3c_i2c0_set_platdata中設置pd->bus_num = 0,而在s3c24xx_i2c_probe()函數中有如下代碼:
可得adap->nr=0,所以i2c_add_numbered_adapter()函數實際調用的是__i2c_add_numbered_adapter函數(i2c_add_adapter自動分配ID後也是調用這個函數)。__i2c_add_numbered_adapter函數又調用了i2c_register_adapter()函數,函數如下:
static int i2c_register_adapter(struct i2c_adapter *adap)
{
... ...
dev_set_name(&adap->dev, "i2c-%d", adap->nr); //設置適配器名字
adap->dev.bus = &i2c_bus_type; //設置適配器成員dev的總線爲i2c_bus_type
adap->dev.type = &i2c_adapter_type; //設置適配器成員dev的類型爲適配器類型
res = device_register(&adap->dev); //註冊適配器成員dev設備
... ...
/* 對dts上所描述的i2c_client設備進行實例化,並創建相應的sys文件:sys/bus/i2c/devices/xxx */
of_i2c_register_devices(adap);
i2c_acpi_register_devices(adap);
i2c_acpi_install_space_handler(adap);
/* 內核啓動時(註冊適配器之前),如果調用i2c_register_board_info()函數註冊
過i2c_board_info結構(描述i2c_client的結構),就調用i2c_scan_static_board_info
對這些i2c_board_info結構裏的i2c_client進行實例化 */
if (adap->nr < __i2c_first_dynamic_bus_num)
i2c_scan_static_board_info(adap);
... ...
/* 遍歷i2c_bus_type匹配i2c_driver與i2c_adapter */
bus_for_each_drv(&i2c_bus_type, NULL, adap, __process_new_adapter);
... ...
}
該函數主要作用如下3大點:
3.2.3.1 設置註冊適配器i2c_adapter
1. 設置適配器名字
2. 設置適配器成員dev的總線爲i2c_bus_type
3. 設置適配器成員dev的類型爲適配器類型
4. 註冊設備(適配器成員dev,以後可以通過這個dev找到該適配器)
3.2.3.2 實例化i2c_client設備(該部分在I2C設備驅動部分詳細講解)
1. 如果使用了設備樹,對dts上所描述的i2c_client設備進行實例化,並創建相應的sys文件:sys/bus/i2c/devices/xxx。
2. 內核啓動時(註冊適配器之前),如果調用i2c_register_board_info()函數註冊過i2c_board_info結構(描述i2c_client的結構),就調用i2c_scan_static_board_info()函數對這些i2c_board_info結構裏的i2c_client進行實例化。
3.2.3.3 遍歷i2c_bus_type匹配i2c_driver與i2c_adapter
匹配過程實際是使用__process_new_adapter()函數來匹配i2c_bus_type上的每一個i2c_drivers,該函數最終又是調用i2c_detect()函數:
static int __process_new_adapter(struct device_driver *d, void *data)
{
return i2c_do_add_adapter(to_i2c_driver(d), data);
}
... ...
static int i2c_do_add_adapter(struct i2c_driver *driver, struct i2c_adapter *adap)
{
i2c_detect(adap, driver);
... ...
}
i2c_detect()函數部分代碼如下:
static int i2c_detect(struct i2c_adapter *adapter, struct i2c_driver *driver)
{
const unsigned short *address_list;
struct i2c_client *temp_client;
int i, err = 0;
int adap_id = i2c_adapter_id(adapter); //得到適配器ID = adap->nr
address_list = driver->address_list; //從i2c_driver得到I2C設備地址列表
... ...
/* 如果適配器dapter->class定義爲不自動檢測類型,函數返回 */
if (adapter->class == I2C_CLASS_DEPRECATED) {
... ...
return 0;
}
/* 如果i2c_driver支持的設備類型與適配器支持的類型不一樣,函數返回 */
if (!(adapter->class & driver->class))
return 0;
/* 分配設置i2c_client */
temp_client = kzalloc(sizeof(struct i2c_client), GFP_KERNEL);
if (!temp_client)
return -ENOMEM;
temp_client->adapter = adapter;
/* 取出每一個address_list裏的設備地址進行檢測 */
for (i = 0; address_list[i] != I2C_CLIENT_END; i += 1) {
dev_dbg(&adapter->dev,
"found normal entry for adapter %d, addr 0x%02x\n",
adap_id, address_list[i]);
temp_client->addr = address_list[i];
err = i2c_detect_address(temp_client, driver); //檢測該設備地址
if (unlikely(err))
break;
}
kfree(temp_client); //釋放i2c_client
return err;
}
該函數主要作用如下:
1. 從i2c_driver得到I2C設備地址列表address_list
2. 判斷適配器i2c_adapter、i2c_driver支持的設備類型class
3. 取出每一個address_list裏的設備地址進行檢測(調用i2c_detect_address()函數)
分析作用2:在前面s3c24xx_i2c_probe()函數中定義了適配器i2c_adapter爲I2C_CLASS_DEPRECATED不自動檢測類型,如下圖:
所以對於i2c-s3c2410.c這個內核(這裏只針對linux-4.12,比如linux-3.4.2 這裏的class設置的是I2C_CLASS_HWMON | I2C_CLASS_SPD)提供的總線驅動來說,在 i2c_detect()函數中什麼也沒做直接返回了。但是對於driver/i2c/busses內核文件內的其他總線驅動,i2c_adapter適配器的class成員設置的是支持自動檢測類型(如設爲I2C_CLASS_HWMON),如果同時滿足adapter->class與driver->class類型相同,就可以繼續往下執行檢測設備地址。
分析作用3:循環取出i2c_driver->address_list裏存放的設備地址進行檢測,即調用i2c_detect_address()函數:
static int i2c_detect_address(struct i2c_client *temp_client, struct i2c_driver *driver)
{
struct i2c_board_info info;
struct i2c_adapter *adapter = temp_client->adapter;
int addr = temp_client->addr;
int err;
/* 確保7位I2C設備地址在0x08~0x77之間 */
err = i2c_check_7bit_addr_validity_strict(addr);
if (err) {
dev_warn(&adapter->dev, "Invalid probe address 0x%02x\n",
addr);
return err;
}
... ...
/* 確保總線上真實掛接有該設備地址的I2C芯片,通過i2c_smbus_xfer發送設備地址,
如果有ACK返回,表示確實有真實的I2C芯片接在該總線上,如果沒有ACK,函數返回 */
if (!i2c_default_probe(adapter, addr))
return 0;
/* 調用i2c_driver->detect檢測函數再次檢測芯片具體是什麼芯片,如通過讀寫芯片特定的寄存器 */
memset(&info, 0, sizeof(struct i2c_board_info));
info.addr = addr;
err = driver->detect(temp_client, &info);
... ...
/*創建設備*/
client = i2c_new_device(adapter, &info);
... ...
return 0;
}
該函數主要作用如下:
1. 確保傳入的7位I2C設備地址在0x08~0x77之間(I2C芯片通常是7位設備地址,範圍在0x08~0x77之間)
2. 確保總線上真實掛接有該設備地址的I2C芯片(在i2c_default_probe()函數中通過i2c_smbus_xfer發送設備地址,如果有ACK返回,表示確實有真實的I2C芯片接在該總線上,如果沒有ACK,函數返回)
3. 調用i2c_driver->detect檢測函數再次檢測芯片具體是什麼芯片(如通過讀寫I2C芯片特定的寄存器)
4. 調用i2c_new_device創建設備(創建I2C設備驅動的i2c_client)
下面先來看看i2c_smbus_xfer()函數是如何檢測設備地址是否真實存在的。
3.2.4 i2c_smbus_xfer()函數
函數部分代碼如下:
s32 i2c_smbus_xfer(struct i2c_adapter *adapter, u16 addr, unsigned short flags,
char read_write, u8 command, int protocol,
union i2c_smbus_data *data)
{
... ...
flags &= I2C_M_TEN | I2C_CLIENT_PEC | I2C_CLIENT_SCCB;
if (adapter->algo->smbus_xfer) { //如果適配器指定了algo->smbus_xfer成員
... ...
}
res = i2c_smbus_xfer_emulated(adapter, addr, flags, read_write,
command, protocol, data); //使用smbus協議發送I2C消息
... ...
}
該函數首先判斷i2c_adapter適配器成員algo->smbus_xfer函數,可以回顧一下i2c-s3c2410.c中的i2c_adapter適配器是如何定義的,s3c24xx_i2c_probe()函數中有如下代碼:
s3c24xx_i2c_algorithm定義如下:
可見適配器並沒有algo->smbus_xfer函數,所以i2c_smbus_xfer()函數實際是使用i2c_smbus_xfer_emulated()函數進行發送數據的,i2c_smbus_xfer_emulated()函數部分代碼如下:
static s32 i2c_smbus_xfer_emulated(struct i2c_adapter *adapter, u16 addr,
unsigned short flags,
char read_write, u8 command, int size,
union i2c_smbus_data *data)
{
unsigned char msgbuf0[I2C_SMBUS_BLOCK_MAX+3]; //屬於 msg[0]的buf成員
unsigned char msgbuf1[I2C_SMBUS_BLOCK_MAX+2]; //屬於 msg[1]的buf成員
int num = read_write == I2C_SMBUS_READ ? 2 : 1; //如果爲讀命令,就等於2,表示要執行兩次數據傳輸
int i;
u8 partial_pec = 0;
int status;
struct i2c_msg msg[2] = {
{
.addr = addr,
.flags = flags,
.len = 1,
.buf = msgbuf0,
}, {
.addr = addr,
.flags = flags | I2C_M_RD,
.len = 0,
.buf = msgbuf1,
},
};
msgbuf0[0] = command; //IIC設備地址最低位爲讀寫命令
switch (size) {
... ...
/*讀操作需要兩個i2c_msg,寫操作需要一個i2c_msg*/
case I2C_SMBUS_BYTE_DATA:
if (read_write == I2C_SMBUS_READ) //如果是讀字節
msg[1].len = 1;
else {
msg[0].len = 2;
msgbuf0[1] = data->byte;
}
break;
... ...
status = i2c_transfer(adapter, msg, num); //將i2c_msg結構體的內容發送給I2C設備
if (status < 0) //返回值不等於0表示沒有收到ACK
return status;
}
可見該函數是通過構造i2c_msg結構體通過i2c_transfer()函數發送給I2C設備,如果I2C設備返回ACK表示設備存在。i2c_transfer()函數實際又調用了__i2c_transfer()函數,部分代碼如下:
int __i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
{
... ...
orig_jiffies = jiffies;
for (ret = 0, try = 0; try <= adap->retries; try++) {
ret = adap->algo->master_xfer(adap, msgs, num);
if (ret != -EAGAIN)
break;
if (time_after(jiffies, orig_jiffies + adap->timeout))
break;
}
... ...
}
最終是調用到了I2C總線驅動裏註冊的adapter適配器的算法函數master_xfer發送i2c_msg。master_xfer()函數調用過程:
1. 傳輸數據時,調用s3c24xx_i2c_algorithm結構體中的數據傳輸函數s3c24xx_i2c_xfer()。
2. s3c24xx_i2c_xfer()中會調用s3c24xx_i2c_doxfer()進行數據的傳輸。
3. s3c24xx_i2c_doxfer()中向總線發送IIC設備地址和開始信號S後,便會調用wati_event_timeout()函數進入等待狀態。
4. 將數據準備好發送時,將產生中斷,並調用實現註冊的中斷處理函數s3c24xx_i2c_irq()。
5. s3c24xx_i2c_irq()調用下一個字節傳輸函數i2c_s3c_irq_nextbyte()來傳輸數據。
6. 當數據傳輸完成後,會調用 s3c24xx_i2c_stop()。
7. 最後調用wake_up()喚醒等待隊列,完成數據的傳輸過程。
其中第5點調用i2s_s3c_irq_nextbyte()函數中有如下代碼:
case STATE_START:
if (iicstat & S3C2410_IICSTAT_LASTBIT &&
!(i2c->msg->flags & I2C_M_IGNORE_NAK)) {
/* 如果沒有收到ACK */
dev_dbg(i2c->dev, "ack was not received\n");
s3c24xx_i2c_stop(i2c, -ENXIO);
goto out_ack;
}
當使用適配器發送數據沒有收到ACK會有如下調用:
s3c24xx_i2c_stop(i2c, -ENXIO);
s3c24xx_i2c_master_complete(i2c, ret); // ret=-ENXIO
if (ret)
i2c->msg_idx = ret; // i2c->msg_idx=-ENXIO
i2c_smbus_xfer()又通過以下調用得到返回值-ENXIO:
i2c_smbus_xfer()
i2c_transfer()
s3c24xx_i2c_xfer()
s3c24xx_i2c_doxfer
ret = i2c->msg_idx; // ret=-ENXIO
return ret;
回顧3.2.3.3中講的i2c_detect_address()函數內容:
1. 確保傳入的7位I2C設備地址在0x08~0x77之間(I2C芯片通常是7位設備地址,範圍在0x08~0x77之間)
2. 確保總線上真實掛接有該設備地址的I2C芯片(在i2c_default_probe()函數中通過i2c_smbus_xfer發送設備地址,如果有ACK返回,表示確實有真實的I2C芯片接在該總線上,如果沒有ACK,函數返回)
3. 調用i2c_driver->detect檢測函數再次檢測芯片具體是什麼芯片(如通過讀寫I2C芯片特定的寄存器)
4. 調用i2c_new_device創建設備(創建I2C設備驅動的i2c_client)
i2c_detect_address()函數中檢驗設備地址i2c_default_probe()函數使用如下:
static int i2c_default_probe(struct i2c_adapter *adap, unsigned short addr)
{
int err;
... ...
err = i2c_smbus_xfer(... ...);
... ...
return err >= 0;
}
... ...
static int i2c_detect_address(struct i2c_client *temp_client,
struct i2c_driver *driver)
{
... ...
if (!i2c_default_probe(adapter, addr))
return 0;
/* 調用i2c_driver->detect檢測函數再次檢測芯片具體是什麼芯片,如通過讀寫芯片特定的寄存器 */
memset(&info, 0, sizeof(struct i2c_board_info));
info.addr = addr;
err = driver->detect(temp_client, &info);
... ...
/*創建設備*/
client = i2c_new_device(adapter, &info);
... ...
return 0;
}
可見,當使用i2c_smbus_xfer()函數(最終是調用到了I2C總線驅動裏註冊的adapter適配器的算法函數master_xfer)發送數據(設備地址)時:
1. 如果沒有收到I2C設備的ACK, i2c_smbus_xfer()返回-ENXIO,所以i2c_default_probe()函數返回0,i2c_detect_address()函數也就會返回0。
2. 如果收到ACK表示I2C總線上確實有該設備地址的芯片存在,就會調用i2c_driver->detect檢測函數再次檢測該設備地址的芯片具體是什麼芯片(如通過讀寫I2C芯片特定的寄存器)。接着便調用i2c_new_device()創建設備(i2c_client),該函數是I2C設備驅動中很重要的組成部分,放到後面在仔細分析。
3.2.5 總結
到目前爲止,i2c-s3c2410.c文件爲我們實現了對2440上I2C控制器的設置、I2C通信協議相關代碼等,組成了一個適配器i2c_adapter(I2C總線驅動),使用這個適配器內核就知道如何與掛在I2C總線上的I2C設備通信了。下面講解I2C設備驅動部分。
3.3 分析I2C設備驅動
其實在以上講解分析I2C總線驅動部分已經涉及到I2C設備驅動的使用了,實例化創建i2c_client、使用i2c_client、i2c_driver等。I2C總線驅動與I2C設備驅動相互盤根交錯,並沒有明顯的界線。如果說I2C總線驅動的核心是向內核註冊適配器(i2c_adapter),那麼I2C設備驅動的核心就是實例化I2C設備(i2c_client)與註冊設備驅動(i2c_driver)。
在Linux內核文檔(/Documentation/i2c/instantiating-devices)中,介紹了實例化I2C設備(註冊i2c_client)的4種方法,實際上這4種方法最終都是通過調用i2c_new_device()函數來實例化I2C設備的,所以先來仔細分析一下i2c_new_device()函數。
3.3.1 i2c_new_device()函數
實例化I2C設備核心就是調用i2c_new_device()函數,該函數部分代碼如下:
struct i2c_client *i2c_new_device(struct i2c_adapter *adap, struct i2c_board_info const *info)
{
struct i2c_client *client;
int status;
client = kzalloc(sizeof *client, GFP_KERNEL); //分配i2c_client結構體內存
... ...
/*設置i2c_client結構體*/
client->adapter = adap; //設置該從設備的適配器
... ...
client->flags = info->flags; //設置該從設備的標誌
client->addr = info->addr; //設置該從設備的設備地址
client->irq = info->irq; //中斷號
... ...
strlcpy(client->name, info->type, sizeof(client->name)); //設置該從設備的名字
... ...
client->dev.parent = &client->adapter->dev; //設置設備父類爲適配器的dev
client->dev.bus = &i2c_bus_type; //設置設備總線類型
client->dev.type = &i2c_client_type; //設置設備類型
client->dev.of_node = info->of_node; //設備節點
client->dev.fwnode = info->fwnode;
i2c_dev_set_name(adap, client);
/* 向設備添加屬性 */
if (info->properties) {
status = device_add_properties(&client->dev, info->properties);
if (status) {
dev_err(&adap->dev,
"Failed to add properties to client %s: %d\n",
client->name, status);
goto out_err;
}
}
/* 註冊設備 */
status = device_register(&client->dev);
... ...
return client;
... ...
}
該函數傳入兩個參數,struct i2c_adapter結構和struct i2c_board_info結構,前者即是I2C總線驅動中的適配器,後者就是一個I2C設備信息表,結構原型如下(include/linux/i2c.h):
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;
const struct property_entry *properties;
const struct resource *resources;
unsigned int num_resources;
int irq;
};
/*設置i2c_board_info結構體type與addr成員的宏 */
#define I2C_BOARD_INFO(dev_type, dev_addr) \
.type = dev_type, .addr = (dev_addr)
我們重點關心type與addr成員,type描述一個I2C設備類型名,addr描述I2C設備地址。在i2c_new_device()函數中會將傳入的i2c_board_info結構體成員拷貝給i2c_client結構,現在再來總結一下i2c_new_device()函數內容:
1. 分配i2c_client結構體
2. 設置i2c_client結構體
2.1 設置適配器
2.2 設置flag
2.3 設置設備地址(i2c_board_info->addr)
2.4 設置irq
2.4 設置設備名字(i2c_board_info->type)
3. 設置i2c_client描述的device設備(通過這個device可以找到i2c_client)
4. 使用device_register()函數註冊device設備
最後是調用device_register註冊這個device結構,由總線設備驅動模型可以猜到,必定有地方調用driver_register註冊一個device_driver結構。內核通過調用i2c_add_driver()函數註冊設備驅動(i2c_driver)裏,就會註冊與i2c_driver相關聯的device_driver結構。
對應的釋放設備函數原型爲:
void i2c_unregister_device(struct i2c_client *client);
3.3.2 i2c_add_driver()函數
在include/linux/i2c.h中有如下宏定義:
//include/linux/i2c.h
#define i2c_add_driver(driver) \
i2c_register_driver(THIS_MODULE, driver)
所以i2c_add_driver()函數實際調用了i2c_register_driver()函數,該函數代碼如下:
int i2c_register_driver(struct module *owner, struct i2c_driver *driver)
{
int res;
... ...
/* 設置i2c_driver的device_driver成員 */
driver->driver.owner = owner; //設置device_driver成員的所有者
driver->driver.bus = &i2c_bus_type; //設置device_driver成員的總線類型
INIT_LIST_HEAD(&driver->clients);
/* 註冊device_driver結構體 */
res = driver_register(&driver->driver);
if (res)
return res;
/* 遍歷i2c_bus_type匹配i2c_driver與i2c_adapter */
i2c_for_each_dev(driver, __process_new_driver);
return 0;
}
該函數作用如下:
1. 設置i2c_driver的device_driver成員
2. 使用driver_register()函數註冊device_driver
3. 遍歷i2c_bus_type匹配i2c_driver與i2c_adapter
3.3.2.1 分析作用2
由總線設備驅動模型可以知道,當向內核註冊device_driver驅動或者註冊device設備,都會調用到device_driver或device的總線結構體裏的.match匹配函數匹配設備鏈表與驅動鏈表,由於他們的總線類型都設爲i2c_bus_type,所以使用i2c_new_device()函數實例化I2C設備(i2c_client)或者使用i2c_add_driver()函數註冊設備驅動(i2c_driver)時,都會調用到i2c_bus_type結構的.match匹配函數,i2c_bus_type結構定義如下:
struct bus_type i2c_bus_type = {
.name = "i2c",
.match = i2c_device_match,
.probe = i2c_device_probe,
.remove = i2c_device_remove,
.shutdown = i2c_device_shutdown,
};
其中i2c_device_match()函數代碼如下:
const struct i2c_device_id *i2c_match_id(const struct i2c_device_id *id,
const struct i2c_client *client)
{
if (!(id && client))
return NULL;
while (id->name[0]) {
if (strcmp(client->name, id->name) == 0) //通過比較.name成員進行匹配
return id;
id++;
}
return NULL;
}
... ...
static int i2c_device_match(struct device *dev, struct device_driver *drv)
{
struct i2c_client *client = i2c_verify_client(dev); //得到i2c_client結構體
struct i2c_driver *driver;
/* 使用設備樹匹配i2c_driver->of_match_table與i2c_client */
if (i2c_of_match_device(drv->of_match_table, client))
return 1;
/* ACPI類型匹配 */
if (acpi_driver_match_device(dev, drv))
return 1;
driver = to_i2c_driver(drv); //得到i2c_driver結構體
/* 匹配i2c_driver->id_table與i2c_client */
if (i2c_match_id(driver->id_table, client))
return 1;
return 0;
}
可見i2c_bus_type->match匹配函數實際是通過判斷i2c_driver->id_table->name與i2c_client->name是否相同,(這裏思考一個問題,在調用i2c_new_device()函數註冊i2c_client時,是通過傳入的i2c_board_info->type成員賦值給i2c_client->name的,如果i2c_board_info不是由我們構建傳入的,調用i2c_new_device()函數之前要在哪裏指定這個i2c_board_info->type呢?)。相同則匹配成功,調用i2c_bus_type->probe函數,該函數部分代碼如下:
static int i2c_device_probe(struct device *dev)
{
struct i2c_client *client = i2c_verify_client(dev); //得到i2c_client結構體
struct i2c_driver *driver;
int status;
... ...
driver = to_i2c_driver(dev->driver); //得到i2c_driver結構體
... ...
/* i2c_driver沒有id_table成員且設備樹匹配失敗則函數返回 */
if (!driver->id_table &&
!i2c_of_match_device(dev->driver->of_match_table, client))
return -ENODEV;
... ...
if (driver->probe_new) //如果i2c_driver結構體有probe_new成員則調用
status = driver->probe_new(client);
else if (driver->probe) //如果i2c_driver結構體有probe成員則調用
status = driver->probe(client,
i2c_match_id(driver->id_table, client));
else
status = -EINVAL;
... ...
return 0;
... ...
}
i2c_bus_type->probe函數最終調用的是i2c_driver結構體的probe_new成員或者probe成員。probe函數裏面做的事情由我們自己決定比如註冊、構建設備節點等。
3.3.2.2 分析作用3
i2c_register_driver()函數的作用3遍歷i2c_bus_type匹配i2c_driver與i2c_adapter就是調用下面代碼:
__process_new_driver()函數內容如下:
static int __process_new_driver(struct device *dev, void *data)
{
if (dev->type != &i2c_adapter_type)
return 0;
return i2c_do_add_adapter(data, to_i2c_adapter(dev));
}
只調用了i2c_do_add_adapter()函數,是不是很熟悉呢?沒錯,在3.2.3中註冊i2c_adapter適配器時最終也調用了該函數,
作用就是遍歷i2c_bus_type匹配i2c_driver與i2c_adapter的,所以調用i2c_add_driver()函數會做與3.2.3.3節完全一樣的工作,這裏就不再重複了。
3.3.3 總結I2C設備驅動
Linux內核中構建了許多總線,但並不都是真實的,比如platform平臺總線就是虛擬的,平臺總線中也有設備鏈表,驅動鏈表。針對I2C總線,也有一個I2C總線的結構即i2c_bus_type結構,此結構裏面也有設備鏈表和也有驅動鏈表。
設備鏈表裏存放i2c_client的結構體,這些結構體是調用i2c_new_device()函數時加入的,不但要加入這些結構體,還會在i2c_bus_type結構的驅動鏈表中一個一個地比較drv裏的i2c_driver來判斷是否有匹配的,如果有將調用i2c_driver裏面的probe函數,匹配函數由總線提供。
驅動鏈表裏存放i2c_driver結構體,這些結構體是調用i2c_add_driver()時加入的,不但要加入這些結構體,還會在i2c_bus_type結構的設備鏈表中一個一個地比較dev裏的i2c_client來判斷是否有匹配的,如果匹配將調用i2c_driver裏面的probe函數。
上述的匹配函數就是i2c_bus_type結構裏面的i2c_device_match函數。i2c_device_match函數通過i2c_driver->id_table->name與i2c_client->name比較(使用設備樹是比較compatible屬性),如果相同,就表示此驅動drv裏存放的i2c_driver能支持這個設備dev裏存放的i2c_client。
總的說來,I2C設備驅動基於bus-dev-drv模型的構建過程如下:
1. 左邊註冊一個設備,設備裏存有i2c_client(調用i2c_new_device()函數)
2. 右邊註冊一個驅動,驅動裏存有i2c_driver(調用i2c_add_driver()函數)
3. 比較i2c_driver->id_table->name與i2c_client->name(使用設備樹是比較compatible屬性),如果相同,則調用i2c_driver的probe函數。
4. probe函數裏面做的事情由用戶決定(比如註冊、構建設備節點等)。
3.3.4 實例化I2C設備
上面介紹了實例化I2C設備的底層概念,接下來分析對於編寫驅動我們應該做什麼。總共有四種方法(如果算上設備樹總共有5種方法,這裏就不介紹設備樹方式了),這4種方法最終都是通過調用i2c_new_device()函數來實例化I2C設備的。
3.3.4.1 實例化I2C設備方法一(需指定適配器,指定設備地址不檢測是否存在)
該方式核心是向內核靜態註冊i2c_board_info結構。
回顧3.2.3 註冊i2c_adapter適配器中,內核啓動後會裝載i2c-s3c2410.c(默認編譯進內核)該驅動,也就是會調用到i2c_add_numbered_adapter()函數然後調用i2c_register_adapter()函數,該函數部分代碼如下:
static int i2c_register_adapter(struct i2c_adapter *adap)
{
... ...
dev_set_name(&adap->dev, "i2c-%d", adap->nr); //設置適配器名字
adap->dev.bus = &i2c_bus_type; //設置適配器成員dev的總線爲i2c_bus_type
adap->dev.type = &i2c_adapter_type; //設置適配器成員dev的類型爲適配器類型
res = device_register(&adap->dev); //註冊適配器成員dev設備
... ...
/* 對dts上所描述的i2c_client設備進行實例化,並創建相應的sys文件:sys/bus/i2c/devices/xxx */
of_i2c_register_devices(adap);
i2c_acpi_register_devices(adap);
i2c_acpi_install_space_handler(adap);
/* 內核啓動時(註冊適配器之前),如果調用i2c_register_board_info()函數註冊
過i2c_board_info結構(描述i2c_client的結構),就調用i2c_scan_static_board_info
對這些i2c_board_info結構裏的i2c_client進行實例化 */
if (adap->nr < __i2c_first_dynamic_bus_num)
i2c_scan_static_board_info(adap);
... ...
/* 遍歷i2c_bus_type匹配i2c_driver與i2c_adapter */
bus_for_each_drv(&i2c_bus_type, NULL, adap, __process_new_adapter);
... ...
}
通過判斷比較adap->nr與 __i2c_first_dynamic_bus_num決定是否執行i2c_scan_static_board_info()函數。假設執行了該函數,看看該函數做了什麼工作:
static void i2c_scan_static_board_info(struct i2c_adapter *adapter)
{
struct i2c_devinfo *devinfo;
down_read(&__i2c_board_lock);
/* 循環取出__i2c_board_list鏈表裏的devinfo->board_info傳入i2c_new_device()函數 */
list_for_each_entry(devinfo, &__i2c_board_list, list) {
if (devinfo->busnum == adapter->nr
&& !i2c_new_device(adapter,
&devinfo->board_info))
dev_err(&adapter->dev,
"Can't create device at 0x%02x\n",
devinfo->board_info.addr);
}
up_read(&__i2c_board_lock);
}
只做了一件事:循環取出__i2c_board_list鏈表裏的devinfo->i2c_board_info傳入i2c_new_device()函數。也就是將__i2c_board_list鏈表裏保存的I2C設備信息實例化了。
下面分析看看什麼情況下會執行i2c_scan_static_board_info()函數。從3.2.3 註冊i2c_adapter適配器中我們得到adap->nr=0,那麼__i2c_first_dynamic_bus_num爲多少呢?搜索內核發現,在driver/i2c/i2c_boardinfo.c文件中有如下內容:
int __i2c_first_dynamic_bus_num; //定義__i2c_first_dynamic_bus_num,默認初始化爲0
EXPORT_SYMBOL_GPL(__i2c_first_dynamic_bus_num);
... ...
int i2c_register_board_info(int busnum, struct i2c_board_info const *info, unsigned len)
{
int status;
... ...
if (busnum >= __i2c_first_dynamic_bus_num)
__i2c_first_dynamic_bus_num = busnum + 1;
/* 爲每一個I2C設備信息分配內存,添加到__i2c_board_list鏈表裏 */
for (status = 0; len; len--, info++) {
struct i2c_devinfo *devinfo;
devinfo = kzalloc(sizeof(*devinfo), GFP_KERNEL);
if (!devinfo) {
pr_debug("i2c-core: can't register boardinfo!\n");
status = -ENOMEM;
break;
}
devinfo->busnum = busnum;
devinfo->board_info = *info;
... ...
list_add_tail(&devinfo->list, &__i2c_board_list); //添加到__i2c_board_list鏈表
}
... ...
return status;
}
在driver/i2c/i2c_boardinfo.c文件中定義__i2c_first_dynamic_bus_num未初始化則默認爲0,該變量只在i2c_register_board_info()註冊單板I2C信息函數裏執行修改操作(還在i2c_init()函數裏修改該變量,但是沒有調用該函數)。
對於2440的單板文件mach-smdk2440.c裏並沒有使用i2c_register_board_info()註冊單板I2C信息函數,所以__i2c_first_dynamic_bus_num一直爲0,也就是說在i2c_register_adapter()函數裏不會執行i2c_scan_static_board_info()掃描單板I2C信息函數。我們參考mach-mini2440.c單板文件如何使用i2c_register_board_info()函數的:
static struct i2c_board_info mini2440_i2c_devs[] __initdata = {
{
I2C_BOARD_INFO("24c08", 0x50), //"24c08"表示設備名,0x50表示設備地址
.platform_data = &at24c08,
},
};
... ...
static void __init mini2440_init(void)
{
... ...
/* 設置platform_device I2C相關信息 */
s3c_i2c0_set_platdata(NULL);
/* 註冊單板I2C相關信息
參數1:I2C總線號,0
參數2:i2c_board_info結構體數組指針
參數3:i2c_board_info結構體數組的成員個數
*/
i2c_register_board_info(0, mini2440_i2c_devs,
ARRAY_SIZE(mini2440_i2c_devs));
... ...
}
MACHINE_START(MINI2440, "MINI2440")
/* Maintainer: Michel Pollet <[email protected]> */
.atag_offset = 0x100,
.map_io = mini2440_map_io,
.init_machine = mini2440_init,
.init_irq = s3c2440_init_irq,
.init_time = mini2440_init_time,
MACHINE_END
當內核啓動時就會調用mini2440_init()函數,裏面調用了i2c_register_board_info()註冊單板I2C信息函數(注意:該函數要在註冊適配器之前調用),參數1爲I2C總線號0(適配器0),參數2爲定義的i2c_board_info結構體數組指針,參數3爲i2c_board_info結構體數組成員個數,i2c_board_info結構體在3.3.1 i2c_new_device()函數的講解中介紹了,這裏不再重複。
這樣調用i2c_register_board_info()函數如下圖,導致__i2c_first_dynamic_bus_num=1。
所以在調用i2c_register_adapter()函數時會調用到i2c_scan_static_board_info()函數。針對JZ2440開發板(單板相關文件對應mach-smdk2440.c)仿照mach-mini2440.c修改內核源碼即可實現實例化I2C設備。(具體實現放在後面的5.1 方法一)
3.3.4.2 實例化I2C設備方法二(需指定適配器,指定設備地址(檢測與不檢測兩種函數))
該方式核心是向內核動態註冊i2c_board_info結構。
與實例化I2C設備方法一類似,該方法也是通過構建i2c_board_info結構體,然後直接調用i2c_new_device()函數,對於該函數第一個參數需要的i2c_adapter結構體,可以通過i2c_get_adapter()函數(傳入參數0表示想要獲得適配器0)得到。與方法一的區別在於可以方便insmod,不需要修改源碼,也就是可以在註冊適配器之後再實例化I2C設備。
還可以通過調用i2c_new_probed_device()函數,該函數部分代碼如下:
struct i2c_client *i2c_new_probed_device(struct i2c_adapter *adap,
struct i2c_board_info *info,
unsigned short const *addr_list,
int (*probe)(struct i2c_adapter *, unsigned short addr))
{
int i;
if (!probe)
probe = i2c_default_probe; //如果參數4沒有指定檢測函數,使用默認檢測函數檢測設備地址是否真實存在
/* 循環判斷addr_list上的設備地址是否真實存在,存在則打破循環 */
for (i = 0; addr_list[i] != I2C_CLIENT_END; i++) {
/* Check address validity */
if (i2c_check_7bit_addr_validity_strict(addr_list[i]) < 0) {
dev_warn(&adap->dev, "Invalid 7-bit address 0x%02x\n",
addr_list[i]);
continue;
}
/* Check address availability (7 bit, no need to encode flags) */
if (i2c_check_addr_busy(adap, addr_list[i])) {
dev_dbg(&adap->dev,
"Address 0x%02x already in use, not probing\n",
addr_list[i]);
continue;
}
/* Test address responsiveness */
if (probe(adap, addr_list[i]))
break;
}
if (addr_list[i] == I2C_CLIENT_END) {
dev_dbg(&adap->dev, "Probing failed, no device found\n");
return NULL;
}
info->addr = addr_list[i]; //將該設備地址存入i2c_board_info
return i2c_new_device(adap, info); //創建設備
}
該函數主要內容如下:
1. 判斷參數4是否指定檢測函數,沒有則使用默認檢測函數檢測設備地址是否真實存在
2. 循環判斷addr_list上的設備地址是否真實存在,存在則打破循環
3. 調用i2c_new_device創建設備
最終還是調用了i2c_new_device()函數,只不過先檢測了設備地址是否真實存在(調用i2c_default_probe()函數檢驗設備地址是否有ACK)。(具體實現放在後面的5.2 方法二)
3.3.4.3 實例化I2C設備方法三(不需要指定適配器,需要指定設備地址並檢測是否存在)
該方式核心是使用i2c_add_driver()函數註冊設備驅動時會自動匹配適配器,自動實例化I2C設備。
回顧3.3.2 i2c_add_driver()函數講的,當調用i2c_add_driver()函數最終會調用到i2c_detect()函數,函數部分代碼如下:
static int i2c_detect(struct i2c_adapter *adapter, struct i2c_driver *driver)
{
const unsigned short *address_list;
struct i2c_client *temp_client;
int i, err = 0;
int adap_id = i2c_adapter_id(adapter); //得到適配器ID = adap->nr
address_list = driver->address_list; //從i2c_driver得到I2C設備地址列表
... ...
/* 如果適配器dapter->class定義爲不自動檢測類型,函數返回 */
if (adapter->class == I2C_CLASS_DEPRECATED) {
... ...
return 0;
}
/* 如果i2c_driver支持的設備類型與適配器支持的類型不一樣,函數返回 */
if (!(adapter->class & driver->class))
return 0;
/* 分配設置i2c_client */
temp_client = kzalloc(sizeof(struct i2c_client), GFP_KERNEL);
if (!temp_client)
return -ENOMEM;
temp_client->adapter = adapter;
/* 取出每一個address_list裏的設備地址進行檢測 */
for (i = 0; address_list[i] != I2C_CLIENT_END; i += 1) {
dev_dbg(&adapter->dev,
"found normal entry for adapter %d, addr 0x%02x\n",
adap_id, address_list[i]);
temp_client->addr = address_list[i];
err = i2c_detect_address(temp_client, driver); //檢測該設備地址
if (unlikely(err))
break;
}
kfree(temp_client); //釋放i2c_client
return err;
}
首先對於mach-smdk2440.c這個適配器來說,之前代碼分析了註冊該適配器時會指定adapter->class = I2C_CLASS_DEPRECATED,我們可以通過修改該內容,讓這個適配器支持自動檢測。然後當調用i2c_add_driver()函數傳入的i2c_driver結構體定義了class成員(驅動支持的類型),並且與某個(2440只有一個)適配器的class(適配器支持的類型)匹配,表示使用自動檢測模式註冊設備驅動,會繼續調用i2c_detect_address()函數,該函數部分代碼如下:
static int i2c_detect_address(struct i2c_client *temp_client, struct i2c_driver *driver)
{
struct i2c_board_info info;
struct i2c_adapter *adapter = temp_client->adapter;
int addr = temp_client->addr;
int err;
/* 確保7位I2C設備地址在0x08~0x77之間 */
err = i2c_check_7bit_addr_validity_strict(addr);
if (err) {
dev_warn(&adapter->dev, "Invalid probe address 0x%02x\n",
addr);
return err;
}
... ...
/* 確保總線上真實掛接有該設備地址的I2C芯片,通過i2c_smbus_xfer發送設備地址,
如果有ACK返回,表示確實有真實的I2C芯片接在該總線上,如果沒有ACK,函數返回 */
if (!i2c_default_probe(adapter, addr))
return 0;
/* 調用i2c_driver->detect檢測函數再次檢測芯片具體是什麼芯片,如通過讀寫芯片特定的寄存器 */
memset(&info, 0, sizeof(struct i2c_board_info));
info.addr = addr;
err = driver->detect(temp_client, &info);
... ...
/*創建設備*/
client = i2c_new_device(adapter, &info);
... ...
return 0;
}
最終也會調用i2c_new_device()函數註冊設備(詳細分析內容看3.2.3.3)。這裏回答3.3.2.1 裏提出的思考問題:調用i2c_new_device()函數註冊i2c_client時,是通過傳入的i2c_board_info->type成員賦值給i2c_client->name的,如果i2c_board_info不是由我們構建傳入的,調用i2c_new_device()函數之前要在哪裏指定這個i2c_board_info->type呢?可以看到調用i2c_new_device()函數之前會調用i2c_driver->detect檢測函數,我們可以在該函數裏賦值i2c_board_info->type成員。
方法二調用i2c_add_driver()函數註冊設備驅動時不指定i2c_driver結構體的class成員,所以這裏直接返回,需要我們自己調用i2c_new_device()函數。
(具體實現放在後面的5.3 方法三)
3.3.4.4 實例化I2C設備方法四(需要指定適配器,需要指定設備地址不檢測是否存在)
該方式核心是從用戶空間直接實例化I2C設備。
在用戶空間執行例如“echo at24c08 0x50 > /sys/class/i2c-adapter/i2c-0/new_device”:
內核會自動創建一個使用適配器0的設備名爲“at24c08”,設備地址爲0x50的I2C設備,相當於方法二中構建i2c_board_info結構體,然後直接調用i2c_new_device()函數。
在用戶空間執行例如“echo 0x50 > /sys/class/i2c-adapter/i2c-0/delete_device”:
內核會調用i2c_unregister_device()函數刪除這個設備地址爲0x50的I2C設備。
(具體實現放在後面的5.4 方法四)
4 分析硬件
講解具體實現I2C設備驅動之前先講解一下硬件信息。由於使用的JZ2440開發板上沒有接I2C設備,所以通過杜邦線外接一個DS3231時鐘芯片。
4.1 DS3231介紹
DS3231是低成本、高精度I2C實時時鐘RTC,RTC保存秒、分、時、星期、日期、月和年信息。少於31天的月份,將自動調整月末的日期,包括閏年的修正。時鐘的工作格式可以是24小時或帶/AM/PM指示的12小時格式。提供兩個可設置的日曆鬧鐘和一個可設置的方波輸出。地址與數據通過I2C雙向總線串行傳輸。詳細芯片資料參考:https://html.alldatasheet.com/html-pdf/112132/DALLAS/DS3231/2425/11/DS3231.html
4.2 原理圖
原理圖如下(DS3231時鐘芯片通過杜邦線連接到開發板):
4.3 相關寄存器
ds3231其中的一組時鐘和日曆相關寄存器如下圖所示:
通過芯片手冊可得以下信息:
1. 通過讀寫00h~06h這7個寄存器地址就能讀取與設置時鐘和日曆了。
2. 從設備地址爲7位ds3231地址1101000。
5. 編寫I2C設備驅動與測試
內核:linux-4.12
編譯器:arm-linux-gcc-4.4.3
環境:ubuntu9.10
5.1 方法一
使用該方法編寫I2C設備驅動特點:
1. 需要修改內核源碼
2. 需要指定使用哪個適配器(S3C2440只有一個)
3. 手動構建i2c_board_info(指定設備名字,指定設備地址)
4. 內核幫我們調用i2c_new_device()函數
5. 不檢測設備地址是否真實存在
下面開始修改內核源碼並測試。
5.1.1 修改arch/arm/mach-s3c24xx/mach-smdk2440.c
在mach-smdk2440.c中添加構建單板I2C設備信息數組,並在smdk2440_machine_init()函數中註冊該單板I2C設備信息,完整修改代碼如下:
/*構建單板I2C設備信息*/
static struct i2c_board_info smdk2440_i2c_devs[] __initdata = {
{
I2C_BOARD_INFO("ds3231", 0x68),
},
};
static void __init smdk2440_machine_init(void)
{
s3c24xx_fb_set_platdata(&smdk2440_fb_info);
s3c_i2c0_set_platdata(NULL);
/*
註冊單板I2C設備信息
參數1:使用適配器0
參數2:i2c_board_info結構體數組
參數3:i2c_board_info結構體數組成員個數
*/
i2c_register_board_info(0, smdk2440_i2c_devs, ARRAY_SIZE(smdk2440_i2c_devs));
platform_add_devices(smdk2440_devices, ARRAY_SIZE(smdk2440_devices));
smdk_machine_init();
}
重新編譯內核。下載啓動。
5.1.2 編寫I2C設備驅動
通過上面的修改,按照3.3.4.1 實例化I2C設備方法一的分析,當內核啓動後將會調用到i2c_new_device()函數註冊i2c_board_info結構體(註冊了i2c_client),所以接下來我們只需編寫註冊i2c_driver部分即可,完整代碼ds3231.c如下:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/i2c.h>
#include <linux/rtc.h>
#include <linux/mutex.h>
#include <linux/workqueue.h>
#include <linux/device.h>
#define BCD2BIN(val) (((val) & 0x0f) + ((val)>>4)*10)
#define BIN2BCD(val) ((((val)/10)<<4) + (val)%10)
#define DS3231_REG_SEC 0x00 //秒
#define DS3231_REG_MIN 0x01 //分
#define DS3231_REG_HOURS 0x02 //時
#define DS3231_REG_DAY 0x03 //星期
#define DS3231_REG_DATE 0x04 //日
#define DS3231_REG_MOUNTH 0x05 //月
#define DS3231_REG_YEAR 0x06 //年
static struct i2c_driver ds3231_driver;
static struct i2c_client *ds3231_client;
int major; //主設備號
static struct cdev ds3231_cdev;
static struct class *ds3231_class;
static ssize_t ds3231_read(struct file *file, char __user *buf, size_t size, loff_t * offset)
{
unsigned char time[7];
int reg = DS3231_REG_YEAR;
int i=sizeof(time);
if (size != sizeof(time))
return -EINVAL;
/* 實際調用了i2c_smbus_xfer函數發送數據
* 循環讀取時鐘與日曆寄存器數據,讀出來的數據是BCD碼,
* 先轉換成16進制並拷貝給應用程序
*/
for (reg = DS3231_REG_YEAR; reg >= DS3231_REG_SEC; reg--){
int tmp;
if ((tmp = i2c_smbus_read_byte_data(ds3231_client, reg)) < 0) {
dev_warn(&ds3231_client->dev,
"can't read from ds3231 chip\n");
return -EIO;
}
time[--i] = BCD2BIN(tmp) & 0xff;
}
return copy_to_user(buf, &time, sizeof(time));
}
static ssize_t ds3231_write(struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
unsigned char time[7];
int reg;
int i=0;
if (size != sizeof(time))
return -EINVAL;
/* 拷貝要寫入的7個16進制時鐘與日曆值 */
if(copy_from_user(time, buf, sizeof(time)))
return -EINVAL;
/* 實際調用了i2c_smbus_xfer函數發送數據
* 循環寫入轉換成BCD碼的時鐘與日曆值
*/
for (reg = DS3231_REG_SEC; reg <= DS3231_REG_YEAR; reg++) {
if (i2c_smbus_write_byte_data(ds3231_client, reg, BIN2BCD(time[i]))< 0) {
dev_warn(&ds3231_client->dev,
"can't write to ds3231 chip\n");
return -EINVAL;
}
i++;
}
return sizeof(time);
}
static struct file_operations ds3231_fops = {
.owner = THIS_MODULE,
.write = ds3231_write,
.read = ds3231_read,
};
static int ds3231_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
int err;
dev_t devid;
ds3231_client = client;
printk("ds3231 probe !\n");
/* 創建字符設備 */
devid = MKDEV(major, 0); //從主設備號major,次設備號0得到dev_t類型
if (major)
{
err=register_chrdev_region(devid, 1, "ds3231"); //註冊字符設備
}
else
{
err=alloc_chrdev_region(&devid, 0, 1, "ds3231"); //註冊字符設備
major = MAJOR(devid); //從dev_t類型得到主設備
}
if(err < 0)
return err;
cdev_init(&ds3231_cdev, &ds3231_fops);
cdev_add(&ds3231_cdev, devid, 1);
ds3231_class = class_create(THIS_MODULE, "ds3231");
device_create(ds3231_class, NULL, MKDEV(major, 0), NULL, "ds3231"); /* /dev/ds3231 */
return err;
}
static int ds3231_remove(struct i2c_client *client)
{
printk("ds3231_remove !\n");
device_destroy(ds3231_class,MKDEV(major, 0));
class_destroy(ds3231_class);
cdev_del(&ds3231_cdev);
unregister_chrdev_region(MKDEV(major, 0), 1);
return 0;
}
static const struct i2c_device_id ds3231_id_table[] = {
{ "ds3231", 0 }, //名字“ds3231”與i2c_board_info的.type一樣
{}
};
static struct i2c_driver ds3231_driver = {
.driver = {
.name = "ds3231_driver",
.owner = THIS_MODULE,
},
.probe = ds3231_probe,
.remove = ds3231_remove,
.id_table = ds3231_id_table,
};
static int ds3231_init(void)
{
return i2c_add_driver(&ds3231_driver);
}
static void ds3231_exit(void)
{
i2c_del_driver(&ds3231_driver);
}
module_init(ds3231_init);
module_exit(ds3231_exit);
MODULE_AUTHOR("LVZHENHAI");
MODULE_LICENSE("GPL");
當裝載該驅動時,由總線設備驅動模型會調用到該驅動的probe函數,我們在該函數裏面創建字符設備,再編寫一個測試程序ds3231_test.c,完整代碼如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
/* ./ds3231_test r //讀時鐘與日曆格式
* ./ds3231_test w Year Mounth Date Day Hour Min Sec //設置時鐘與日曆格式
*/
void print_usage(char *file)
{
printf("%s r\n", file);
printf("%s w Year Mounth Date Day Hour Min Sec\n", file);
}
int main(int argc, char **argv)
{
int fd;
unsigned char buf[7];
int i;
if ((argc != 2) && (argc != 9))
{
print_usage(argv[0]);
return -1;
}
fd = open("/dev/ds3231", O_RDWR);
if (fd < 0)
{
printf("can't open /dev/ds3231\n");
return -1;
}
if (strcmp(argv[1], "r") == 0) //讀取時鐘與日曆
{
read(fd, buf, sizeof(buf));
printf("Now Time= Year:%02d, Mounth:%02d, Date:%02d, Day:%d, Hour:%02d, Min:%02d, Sec:%02d\n", \
buf[6], buf[5],buf[4], buf[3], buf[2],buf[1],buf[0]);
}
else if (strcmp(argv[1], "w") == 0) //寫時鐘與日曆
{
for(i=0;i<7;i++)
{
buf[6-i] = strtoul(argv[i+2], NULL, 0); //將字符轉換成數值
}
printf("Set Time Year:%02d, Mounth:%02d, Date:%02d, Day:%d, Hour:%02d, Min:%02d, Sec:%02d\n", \
buf[6], buf[5],buf[4], buf[3], buf[2],buf[1],buf[0]);
write(fd, buf, sizeof(buf));
}
else
{
print_usage(argv[0]);
return -1;
}
return 0;
}
Makefile代碼如下:
KERN_DIR = /work/tools/linux-4.12
all:
make -C $(KERN_DIR) M=`pwd` modules
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
obj-m += ds3231.o
5.1.3 測試
1. 啓動修改好的內核。
2. 拷貝以上3個文件到網絡文件系統編譯並安裝驅動,進入文件目錄,執行如下命令:
make
arm-linux-gcc -o ds3231_test ds3231_test.c
ls得到如下:
安裝驅動如下:
可以看到調用了ds3231.c驅動的probe函數,使用應用程序測試,執行如下命令:
執行如下命令:
./ds3231_test w 18 12 31 1 23 59 55 (設置當前時間爲18年12月31日星期1 23點59分55秒)
./ds3231_test r (讀取當前時間)
5.2 方法二
使用該方法編寫I2C設備驅動特點:
1. 需要指定使用哪個適配器(S3C2440只有一個)
2. 手動構建i2c_board_info(指定設備名字,指定設備地址)
3. 編寫驅動調用i2c_new_device()函數(不檢測設備地址是否真實存在)或者調用i2c_new_probed_device()函數(檢測設備地址是否真實存在)
按照3.3.4.2 實例化I2C設備方法二的分析編寫I2C設備驅動,在ds3231_dev.c裏註冊i2c_client,在ds3231_drv.c(與方法一的ds3231.c相同,不貼代碼了)裏註冊i2c_driver,ds3231_dev.c的編寫有兩種,主要不同在使用i2c_new_device()函數(不檢測設備地址是否真實存在)或者調用i2c_new_probed_device()函數。
5.2.1 編寫I2C設備驅動(使用i2c_new_device)
使用i2c_new_device()函數的ds3231_dev.c代碼如下:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/i2c.h>
#include <linux/err.h>
#include <linux/regmap.h>
#include <linux/slab.h>
static struct i2c_board_info ds3231_info = {
I2C_BOARD_INFO("ds3231", 0x68),
};
static struct i2c_client *ds3231_client;
static int ds3231_dev_init(void)
{
struct i2c_adapter *i2c_adap;
i2c_adap = i2c_get_adapter(0); /*因爲硬件掛接在I2C總線0上,所以參數是0*/
/*在該適配器下創建了一個新設備,設備信息在i2c_board_info結構體中,
以後就可以使用這個i2c_adapter 的操作函數對該設備發出I2C的信號了*/
ds3231_client = i2c_new_device(i2c_adap, &ds3231_info);
i2c_put_adapter(i2c_adap); /*使用完要釋放*/
return 0;
}
static void ds3231_dev_exit(void)
{
i2c_unregister_device(ds3231_client); /*卸載註冊的設備*/
}
module_init(ds3231_dev_init);
module_exit(ds3231_dev_exit);
MODULE_AUTHOR("LVZHENHAI");
MODULE_LICENSE("GPL");
5.2.2 編寫I2C設備驅動(使用i2c_new_probed_device)
使用i2c_new_probed_device()函數的ds3231_dev.c代碼如下:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/i2c.h>
#include <linux/err.h>
#include <linux/regmap.h>
#include <linux/slab.h>
static struct i2c_client *ds3231_client;
/*short addr_list[]中可以存放多個設備地址,
在 i2c_new_probed_device函數中依次進行匹配,
I2C_CLIENT_END 表示後面沒有參數了*/
static const unsigned short addr_list[] = { 0x60, 0x68, I2C_CLIENT_END };
static int ds3231_dev_init(void)
{
struct i2c_adapter *i2c_adap;
struct i2c_board_info ds3231_info;
memset(&ds3231_info, 0, sizeof(struct i2c_board_info));
strlcpy(ds3231_info.type, "ds3231", I2C_NAME_SIZE);
i2c_adap = i2c_get_adapter(0);
ds3231_client = i2c_new_probed_device(i2c_adap, &ds3231_info, addr_list, NULL);
i2c_put_adapter(i2c_adap);
if (ds3231_client)
return 0;
else
return -ENODEV;
}
static void ds3231_dev_exit(void)
{
i2c_unregister_device(ds3231_client);
}
module_init(ds3231_dev_init);
module_exit(ds3231_dev_exit);
MODULE_AUTHOR("LVZHENHAI");
MODULE_LICENSE("GPL");
測試程序ds3231_test.c與方法一的一樣,ds3231_drv.c與方法一的ds3231.c相同。Makefile代碼如下:
KERN_DIR = /work/tools/linux-4.12
all:
make -C $(KERN_DIR) M=`pwd` modules
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
obj-m += ds3231_dev.o
obj-m += ds3231_drv.o
5.2.3 測試
注意如果使用過方法一修改了mach-smdk2440.c文件要修改回去,否則裝載ds3231_dev.c時提示不能重複裝載相同設備地址的驅動。
拷貝以上4個文件到網絡文件系統編譯並安裝驅動,進入文件目錄,執行如下命令:
make
arm-linux-gcc -o ds3231_test ds3231_test.c
ls得到如下:
安裝驅動如下:
可以看到調用了ds3231_drv.c驅動的probe函數,後面的使用應用程序測試與方法一一樣。
5.3 方法三
使用該方法編寫I2C設備驅動特點:
1. 修改源碼(由於該版本內核不支持i2c-s3c2410.c這個適配器自動檢測,修改讓其支持)
2. 不需要指定使用哪個適配器
3. 不需要構建i2c_board_info,但通過賦值給i2c_board_info->type成員傳遞設備名字,構建一個address_list傳遞地址
4. 內核幫我們調用i2c_new_device()函數
5. 檢測設備地址是否真實存在
5.3.1 修改driver/i2c/busses/i2c-s3c2410.c
按照3.3.4.2 實例化I2C設備方法二的分析,修改適配器文件driver/i2c/busses/i2c-s3c2410.c文件,由於該版本內核不支持這個適配器自動檢測I2C_CLASS_DEPRECATED,修改讓其支持改爲I2C_CLASS_HWMON,如下:
然後重新編譯內核。
5.3.2 編寫I2C設備驅動
按照3.3.4.2 實例化I2C設備方法二的分析編寫I2C設備驅動,我們只需註冊i2c_driver,完整代碼ds3231.c如下:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/i2c.h>
#include <linux/rtc.h>
#include <linux/mutex.h>
#include <linux/workqueue.h>
#include <linux/device.h>
#define BCD2BIN(val) (((val) & 0x0f) + ((val)>>4)*10)
#define BIN2BCD(val) ((((val)/10)<<4) + (val)%10)
#define DS3231_REG_SEC 0x00 //秒
#define DS3231_REG_MIN 0x01 //分
#define DS3231_REG_HOURS 0x02 //時
#define DS3231_REG_DAY 0x03 //星期
#define DS3231_REG_DATE 0x04 //日
#define DS3231_REG_MOUNTH 0x05 //月
#define DS3231_REG_YEAR 0x06 //年
static struct i2c_driver ds3231_driver;
static struct i2c_client *ds3231_client;
int major; //主設備號
static struct cdev ds3231_cdev;
static struct class *ds3231_class;
static ssize_t ds3231_read(struct file *file, char __user *buf, size_t size, loff_t * offset)
{
unsigned char time[7];
int reg = DS3231_REG_YEAR;
int i=sizeof(time);
if (size != sizeof(time))
return -EINVAL;
/* 實際調用了i2c_smbus_xfer函數發送數據
* 循環讀取時鐘與日曆寄存器數據,讀出來的數據是BCD碼,
* 先轉換成16進制並拷貝給應用程序
*/
for (reg = DS3231_REG_YEAR; reg >= DS3231_REG_SEC; reg--){
int tmp;
if ((tmp = i2c_smbus_read_byte_data(ds3231_client, reg)) < 0) {
dev_warn(&ds3231_client->dev,
"can't read from ds3231 chip\n");
return -EIO;
}
time[--i] = BCD2BIN(tmp) & 0xff;
}
return copy_to_user(buf, &time, sizeof(time));
}
static ssize_t ds3231_write(struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
unsigned char time[7];
int reg;
int i=0;
if (size != sizeof(time))
return -EINVAL;
/* 拷貝要寫入的7個16進制時鐘與日曆值 */
if(copy_from_user(time, buf, sizeof(time)))
return -EINVAL;
/* 實際調用了i2c_smbus_xfer函數發送數據
* 循環寫入轉換成BCD碼的時鐘與日曆值
*/
for (reg = DS3231_REG_SEC; reg <= DS3231_REG_YEAR; reg++) {
if (i2c_smbus_write_byte_data(ds3231_client, reg, BIN2BCD(time[i]))< 0) {
dev_warn(&ds3231_client->dev,
"can't write to ds3231 chip\n");
return -EINVAL;
}
i++;
}
return sizeof(time);
}
static struct file_operations ds3231_fops = {
.owner = THIS_MODULE,
.write = ds3231_write,
.read = ds3231_read,
};
static int ds3231_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
int err;
dev_t devid;
ds3231_client = client;
printk("ds3231 probe !\n");
/* 創建字符設備 */
devid = MKDEV(major, 0); //從主設備號major,次設備號0得到dev_t類型
if (major)
{
err=register_chrdev_region(devid, 1, "ds3231"); //註冊字符設備
}
else
{
err=alloc_chrdev_region(&devid, 0, 1, "ds3231"); //註冊字符設備
major = MAJOR(devid); //從dev_t類型得到主設備
}
if(err < 0)
return err;
cdev_init(&ds3231_cdev, &ds3231_fops);
cdev_add(&ds3231_cdev, devid, 1);
ds3231_class = class_create(THIS_MODULE, "ds3231");
device_create(ds3231_class, NULL, MKDEV(major, 0), NULL, "ds3231"); /* /dev/ds3231 */
return err;
}
static int ds3231_remove(struct i2c_client *client)
{
printk("ds3231_remove !\n");
device_destroy(ds3231_class,MKDEV(major, 0));
class_destroy(ds3231_class);
cdev_del(&ds3231_cdev);
unregister_chrdev_region(MKDEV(major, 0), 1);
return 0;
}
static const struct i2c_device_id ds3231_id_table[] = {
{ "ds3231", 0 }, //設備名字
{}
};
static int ds3231_detect(struct i2c_client *client,
struct i2c_board_info *info)
{
/* 能運行到這裏, 表示該addr的設備是存在的
* 但是有些設備單憑地址無法分辨(A芯片的地址是0x68, B芯片的地址也是0x68)
* 還需要進一步讀寫I2C設備來分辨是哪款芯片
* detect就是用來進一步分辨這個芯片是哪一款,並且設置info->type
*/
printk("ds3231_detect : addr = 0x%x\n", client->addr);
/* 在這裏可以進一步判斷是哪一款芯片 */
strlcpy(info->type, "ds3231", I2C_NAME_SIZE); //與i2c_device_id裏的設備名字一樣
return 0;
}
static const unsigned short addr_list[] = { 0x60, 0x68, I2C_CLIENT_END };
static struct i2c_driver ds3231_driver = {
.class = I2C_CLASS_HWMON, /* 表示該驅動是什麼類型,匹配支持相同的類型適配器 */
.driver = {
.name = "ds3231_driver",
.owner = THIS_MODULE,
},
.probe = ds3231_probe,
.remove = ds3231_remove,
.id_table = ds3231_id_table,
.detect = ds3231_detect, /* 用這個函數來檢測設備確實存在 */
.address_list = addr_list, /* 這些設備的地址 */
};
static int ds3231_init(void)
{
return i2c_add_driver(&ds3231_driver);
}
static void ds3231_exit(void)
{
i2c_del_driver(&ds3231_driver);
}
module_init(ds3231_init);
module_exit(ds3231_exit);
MODULE_AUTHOR("LVZHENHAI");
MODULE_LICENSE("GPL");
測試程序ds3231_test.c與方法一一樣,Makefile也一樣。
5.3.3 測試
1. 啓動修改好的內核。
2. 拷貝以上3個文件到網絡文件系統編譯並安裝驅動,進入文件目錄,執行如下命令:
make
arm-linux-gcc -o ds3231_test ds3231_test.c
ls得到如下:
安裝驅動如下:
可以看到先調用了ds3231_drv.c驅動的detect函數,然後執行probe函數,後面的使用應用程序測試與方法一一樣。
5.4 方法四
使用該方法編寫I2C設備驅動特點:
1. 直接在用戶空間創建或刪除I2C設備(通過命令指定設備名字,指定設備地址)
2. 不檢測設備地址是否真實存在(註冊i2c_driver時去匹配上面的設備名字,匹配成功就會執行驅動的probe函數)
啓動內核在開發板上執行如下命令:
ls /sys/class/i2c-adapter/
可以看到只有一個i2c-0,這就是2440唯一一個適配器。下面直接通過命令創建設備或刪除設備。
5.4.1 創建設備
執行如下命令:
echo ds3231 0x68 > /sys/class/i2c-adapter/i2c-0/new_device
內核會自動創建一個使用適配器0的設備名爲“ds3231”,設備地址爲0x68的I2C設備,相當於方法二中構建i2c_board_info結構體,然後內核幫我們調用i2c_new_device()函數。
5.4.2 刪除設備
執行如下命令:
echo 0x68 > /sys/class/i2c-adapter/i2c-0/delete_device
內核會調用i2c_unregister_device()函數刪除這個設備地址爲0x68的I2C設備。
5.4.3 測試
使用5.4.1 創建設備後,再使用方法一的設備驅動程序ds3231.c、測試程序ds3231_test.c與Makefile。測試方法與效果也一樣。
還有一種方法是使用設備樹方式,這裏就先不介紹了。