(基於4.14內核版本)
爲了梳理清楚linux內核中的i2c實現框架,從本文開始,博主將分幾個章節分別解析i2c總線在linux內核中的造成過程、匹配過程、以及設備驅動程序源碼實現。node
在介紹linux內核中i2c框架以前,咱們最好是知道怎麼使用它,實現一個相應的i2c設備驅動程序demo,而後從使用去深挖背後的實現原理,先知道怎麼用,而後再知道爲何能夠這麼用。linux
回到本文的重點——I2C,作過裸板開發或者是單片機開發的朋友確定對I2C不陌生,I2C是主從結構,主器件使用從機地址進行尋址,它的拓撲結構是這樣的:
(圖片來自網絡,若有侵權,請聯繫我及時刪除)數組
基本的流程是這樣的:網絡
I2C設備驅動程序框架分爲兩個部分:driver和device。框架
分別將driver和device加載到內存中,i2c bus在程序加載時會自動調用match函數,根據名稱來匹配driver和device,匹配完成時調用probe()dom
在driver中,定義probe()函數,在probe函數中建立設備節點,針對不一樣的設備實現不一樣的功能。函數
在device中,設置設備I2C地址,選擇I2C適配器。測試
I2C適配器:I2C的底層傳輸功能,通常指硬件I2C控制器。ui
直接來看下面的示例代碼:
i2c_bus_driver.c:code
#include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/i2c.h> #include <linux/err.h> #include <linux/slab.h> #include <linux/uaccess.h> /* 結構體數組 結構體第一個參數爲名稱,第二個參數爲private數據*/ static const struct i2c_device_id downey_drv_id_table[] = { {"downey_i2c",0}, {}, }; static int major; static struct class *i2c_test_cls; static struct device *i2c_test_dev; static const char* CLASS_NAME = "I2C_TEST_CLASS"; static const char* DEVICE_NAME = "I2C_TEST_DEVICE"; static struct i2c_client *downey_client; static int i2c_test_open(struct inode *node, struct file *file) { printk(KERN_ALERT "i2c init \n"); return 0; } static ssize_t i2c_test_read(struct file *file,char *buf, size_t len,loff_t *offset) { int cnt = 0; uint8_t reg = 0; uint8_t val = 0; copy_from_user(®,buf,1); /*i2c讀byte,經過這個函數能夠從設備中指定地址讀取數據*/ val = i2c_smbus_read_byte_data(downey_client,reg); cnt = copy_to_user(&buf[1],&val,1); return 1; } static ssize_t i2c_test_write(struct file *file,const char *buf,size_t len,loff_t *offset) { uint8_t recv_msg[255] = {0}; uint8_t reg = 0; int cnt = 0; cnt = copy_from_user(recv_msg,buf,len); reg = recv_msg[0]; printk(KERN_INFO "recv data = %x.%x\n",recv_msg[0],recv_msg[1]); /*i2c寫byte,經過這個函數能夠往設備中指定地址寫數據*/ if(i2c_smbus_write_byte_data(downey_client,reg,recv_msg[1]) < 0){ printk(KERN_ALERT " write failed!!!\n"); return -EIO; } return len; } static int i2c_test_release(struct inode *node,struct file *file) { printk(KERN_INFO "Release!!\n"); return 0; } static struct file_operations file_oprts = { .open = i2c_test_open, .read = i2c_test_read, .write = i2c_test_write, .release = i2c_test_release, }; /*當i2c bus檢測到匹配的device - driver,調用probe()函數,在probe函數中,申請設備號,建立設備節點,綁定相應的file operation結構體。*/ static int downey_drv_probe(struct i2c_client *client, const struct i2c_device_id *id) { /*保存參數client,在i2c讀寫操做時須要用到這個參數,其中保存了適配器、設備地址等信息*/ printk(KERN_ALERT "addr = %x\n",client->addr); downey_client = client; major = register_chrdev(0,DEVICE_NAME,&file_oprts); if(major < 0 ){ printk(KERN_ALERT "Register failed!!\r\n"); return major; } printk(KERN_ALERT "Registe success,major number is %d\r\n",major); /*以CLASS_NAME建立一個class結構,這個動做將會在/sys/class目錄建立一個名爲CLASS_NAME的目錄*/ i2c_test_cls = class_create(THIS_MODULE,CLASS_NAME); if(IS_ERR(i2c_test_cls)) { unregister_chrdev(major,DEVICE_NAME); return PTR_ERR(i2c_test_cls); } /*以DEVICE_NAME爲名,參考/sys/class/CLASS_NAME在/dev目錄下建立一個設備:/dev/DEVICE_NAME*/ i2c_test_dev = device_create(i2c_test_cls,NULL,MKDEV(major,0),NULL,DEVICE_NAME); if(IS_ERR(i2c_test_dev)) { class_destroy(i2c_test_cls); unregister_chrdev(major,DEVICE_NAME); return PTR_ERR(i2c_test_dev); } printk(KERN_ALERT "i2c_test device init success!!\r\n"); return 0; } /*Remove :當匹配關係不存在時(device或是driver被卸載),調用remove函數,remove函數是probe函數的反操做,將probe函數中申請的資源所有釋放。*/ static int downey_drv_remove(struct i2c_client *client) { printk(KERN_ALERT "remove!!!\n"); device_destroy(i2c_test_cls,MKDEV(major,0)); class_unregister(i2c_test_cls); class_destroy(i2c_test_cls); unregister_chrdev(major,DEVICE_NAME); return 0; } static struct i2c_driver downey_drv = { /*.driver中的name元素僅僅是一個標識,並不做爲bus匹配的name識別*/ .driver = { .name = "random", .owner = THIS_MODULE, }, .probe = downey_drv_probe, .remove = downey_drv_remove, /*.id_table中存儲driver名稱,做爲bus匹配時的識別*/ .id_table = downey_drv_id_table, // .address_list = downey_i2c, }; int drv_init(void) { int ret = 0; printk(KERN_ALERT "init!!!\n"); ret = i2c_add_driver(&downey_drv); if(ret){ printk(KERN_ALERT "add driver failed!!!\n"); return -ENODEV; } return 0; } void drv_exit(void) { i2c_del_driver(&downey_drv); return ; } MODULE_LICENSE("GPL"); module_init(drv_init); module_exit(drv_exit);
i2c_bus_driver.c:
#include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/i2c.h> #include <linux/err.h> #include <linux/slab.h> #include <linux/regmap.h> // #include <linux/paltform_device.h> static struct i2c_adapter *adap; static struct i2c_client *client; #define I2C_DEVICE_ADDR 0x68 /**指定i2c device的信息 * downey_i2c 是device中的name元素,當這個模塊被加載時,i2c總線將使用這個名稱匹配相應的drv。 * I2C_DEVICE_ADDR 爲設備的i2c地址 * */ static struct i2c_board_info downey_board = { I2C_BOARD_INFO("downey_i2c",I2C_DEVICE_ADDR), }; int dev_init(void) { /*獲取i2c適配器,適配器通常指板上I2C控制器,實現i2c底層協議的字節收發,特殊狀況下,用普通gpio模擬I2C也可做爲適配器*/ adap = i2c_get_adapter(2); if(IS_ERR(adap)){ printk(KERN_ALERT "I2c_get_adapter failed!!!\n"); return -ENODEV; } /*建立一個I2C device,並註冊到i2c bus的device鏈表中*/ client = i2c_new_device(adap,&downey_board); /*使能相應適配器*/ i2c_put_adapter(adap); if(!client){ printk(KERN_ALERT "Get new device failed!!!\n"); return -ENODEV; } return 0; } void dev_exit(void) { i2c_unregister_device(client); return ; } MODULE_LICENSE("GPL"); module_init(dev_init); module_exit(dev_exit);
driver和device做爲兩個獨立的模塊,須要分別編譯,分別生成i2c_bus_driver.ko和i2c_bus_device.ko(編譯過程我就再也不囉嗦了)。
而後加載driver:
sudo insmod i2c_bus_driver.ko
log信息
Dec 31 07:21:49 beaglebone kernel: [13114.715050] init!!!
加載device:
sudo insmod i2c_bus_device.ko
log信息:
Dec 31 07:21:49 beaglebone kernel: [13114.717420] addr = 68 Dec 31 07:21:49 beaglebone kernel: [13114.726671] Registe success,major number is 241 Dec 31 07:21:49 beaglebone kernel: [13114.739575] i2c_test device init success!!
查看log能夠發現,當加載完i2c_bus_device.ko時,driver中probe函數被調用,打印出設備地址,註冊的設備號,表示註冊成功。接下來就是寫一個用戶程序來測試驅動。
#include <stdio.h> #include <stdlib.h> #include <error.h> #include <fcntl.h> #include <string.h> #include <unistd.h> #include <stdint.h> static char buf[256] = {1}; int main(int argc,char *argv[]) { int ret = 0; uint8_t buf[2] = {0}; char cmd[6] = {0}; int reg_addr = 0; int value = 0; int fd = open("/dev/I2C_TEST_DEVICE",O_RDWR); if(fd < 0) { perror("Open file failed!!!\r\n"); } while(1){ /*for example : write 0x00 0x08*/ /*The val should be 0 when the cmd is read.*/ printf("Enter your cmd:<read/write> <reg_addr> <val> : \n"); scanf("%s",cmd); scanf("%x",®_addr); scanf("%x",&value); printf("%s :%x :%x\n",cmd,reg_addr,value); if(0 == memcmp(cmd,"write",5)){ buf[0] = reg_addr; buf[1] = value; int ret = write(fd,buf,2); if(ret < 0){ perror("Failed to write!!\n"); }else{ printf("Write value %x to reg addr %x success\n",value,reg_addr); } } else if(0 == memcmp(cmd,"read",4)){ buf[0] = reg_addr; ret = read(fd,buf,1); if(ret < 0){ perror("Read failed!!\n"); }else{ printf("Read %x from addr %x\n",buf[1],reg_addr); } } else{ printf("Unsupport cmd\n"); } memset(cmd,0,sizeof(cmd)); } close(fd); return 0; }
用戶程序實現從終端讀取用戶指令,而後讀寫傳感器的寄存器,代碼都通過測試,本身試試吧!
在上述i2c的device建立中,咱們使用了i2c_new_device()接口,值得一提的是,這個接口並不會檢測設備是否存在,只要對應的device-driver存在,就會調用driver中的probe函數。
可是有時候會有這樣的需求:在匹配時須要先檢測設備是否插入,若是沒有i2c設備鏈接在主板上,就拒絕模塊的加載,這樣能夠有效地管理i2c設備的資源,避免無關設備佔用i2c資源。
新的建立方式接口爲:
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))
這個函數添加了在匹配模塊時的檢測功能:
爲了一探究竟,咱們來看看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; /*若是傳入probe爲NULL,則使用默認probe函數*/ if (!probe) probe = i2c_default_probe; /*輪詢傳入的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; } /*檢測到可用地址,將其賦值給board info結構體*/ info->addr = addr_list[i]; return i2c_new_device(adap, info); }
根據源碼中的顯示,i2c_new_probed_device主要是執行了這樣的操做:
看到這裏就一目瞭然了,一切細節在源碼面前都無處可藏。其實就在對相應地址作一次檢測而已,到最後仍是調用i2c_new_device。
不過不知道你有沒有發現,i2c_new_device傳入的參數2的addr部分被忽略了,因此board info中的地址實際上是可有可無的,由於函數會在addr list中查找可用的地址而後賦值給board info的addr元素,而本來的addr被覆蓋。因此,若是你在寫內核代碼時感到疑惑,看源碼就行了!
好了,關於linux驅動中i2c驅動的討論就到此爲止啦,若是朋友們對於這個有什麼疑問或者發現有文章中有什麼錯誤,歡迎留言
原創博客,轉載請註明出處!
祝各位早日實現項目叢中過,bug不沾身.