linux設備驅動程序-i2c(0)-i2c設備驅動源碼實現


(基於4.14內核版本)
為了梳理清楚linux內核中的i2c實現框架,從本文開始,博主將分幾個章節分別解析i2c總線在linux內核中的形成過程、匹配過程、以及設備驅動程序源碼實現。

在介紹linux內核中i2c框架之前,我們最好是知道怎么使用它,實現一個相應的i2c設備驅動程序demo,然后從使用去深挖背后的實現原理,先知道怎么用,然后再知道為什么可以這么用。

I2C的基本知識掃盲

回到本文的重點——I2C,做過裸板開發或者是單片機開發的朋友肯定對I2C不陌生,I2C是主從結構,主器件使用從機地址進行尋址,它的拓撲結構是這樣的:

(圖片來自網絡,如有侵權,請聯系我及時刪除)

基本的流程是這樣的:

  • 主機發送從機地址
  • 從機監聽總線,檢測到接收地址與自身地址地址匹配,回復。
  • 主機啟動收發數據
  • 從機接收數據,響應
  • 數據收發完畢,主機釋放總線。
    完整的I2C操作其實是比較復雜的,這里就不再展開講解,博主將會在隨后的章節中進行詳解。

I2C設備驅動程序框架

I2C設備驅動程序框架分為兩個部分:driver和device。

分別將driver和device加載到內存中,i2c bus在程序加載時會自動調用match函數,根據名稱來匹配driver和device,匹配完成時調用probe()

在driver中,定義probe()函數,在probe函數中創建設備節點,針對不同的設備實現不同的功能。

在device中,設置設備I2C地址,選擇I2C適配器。

I2C適配器:I2C的底層傳輸功能,一般指硬件I2C控制器。

I2C設備驅動程序

driver端示例

直接來看下面的示例代碼:
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/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(&reg,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);

代碼簡述

  • 入口函數drv_init()在i2c bus的driver鏈表中添加一個driver節點,當然這個操作是在加載模塊時完成
  • i2c_add_driver函數以struct i2c_driver類型結構體為參數,開發者需要填充driver信息,probe(),remove(),以及id_table,其中id_table中包含了driver名稱,在i2c bus做匹配時使用。
  • 當driver-device匹配成功時,調用driver中的probe函數,在函數中,創建設備節點等等(之前章節中的字符驅動框架還記得吧)。
  • 當匹配關系不存在時,調用remove函數,執行probe的反操作。
  • 需要注意的是,在這里的示例中,這僅僅是一個demo,所以沒有做任何操作,事實上,如果你要實現一個設備的驅動,需要在read或write函數中實現相應功能,這里僅僅是將在/dev目錄下創建一個用戶文件接口,將用戶寫的數據寫入i2c設備,讀取設備數據到用戶空間。

device端代碼示例

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);

代碼簡述

  • 在入口函數dev_init函數中先獲取適配器,后續使用i2c_put_adapter將適配器放入執行列表,適配器一般指板上I2C控制器,實現i2c底層協議的字節收發,特殊情況下,用普通gpio模擬I2C也可作為適配器。
  • 創建一個新的bus device,並將其鏈入bus 的device鏈表
  • 創建device設備需要struct i2c_board_info類型結構體作為參數,函數包含匹配時使用的name元素,以及設備的i2c地址。

編譯加載運行

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函數被調用,打印出設備地址,注冊的設備號,表示注冊成功。接下來就是寫一個用戶程序來測試驅動。

實驗環境

  • 開發板:beagle bone green開發板
  • 內核版本 :4.14.71-ti-r80
  • i2c設備 :9軸傳感器,i2c地址0x68

用戶代碼

#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",&reg_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;
}

用戶程序實現從終端讀取用戶指令,然后讀寫傳感器的寄存器,代碼都經過測試,自己試試吧!

device的另一種創建

在上述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))  

這個函數添加了在匹配模塊時的檢測功能:

  • 參數1:adapter,適配器
  • 參數2:board info,包含名稱和i2c地址
  • 參數3:設備地址列表,既然參數2中有地址,為什么還要增加一個參數列表呢?咱們下面分解
  • 參數4:probe檢測函數,此probe非彼probe,這個probe函數實現的功能是檢測板上是否已經物理連接了相應的設備,當傳入NULL時,就是用默認的probe函數,建議使用默認probe。

為了一探究竟,我們來看看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主要是執行了這樣的操作:

  • 輪詢傳入的addr_list,檢測指定地址列表中地址是否合法,值得注意的是,在輪詢addr_list時,判斷列表中元素是否等於I2C_CLIENT_END,所以我們在給addr_list賦值時,應該以I2C_CLIENT_END結尾
  • 檢測地址是否被占用
  • 檢測對應地址上設備是否處於運行狀態
  • 將找到的地址賦值給info->addr
  • 調用i2c_new_device

看到這里就一目了然了,一切細節在源碼面前都無處可藏。其實就在對相應地址做一次檢測而已,到最后還是調用i2c_new_device。

不過不知道你有沒有發現,i2c_new_device傳入的參數2的addr部分被忽略了,所以board info中的地址其實是無關緊要的,因為函數會在addr list中查找可用的地址然后賦值給board info的addr元素,而原本的addr被覆蓋。所以,如果你在寫內核代碼時感到疑惑,看源碼就好了!

好了,關於linux驅動中i2c驅動的討論就到此為止啦,如果朋友們對於這個有什么疑問或者發現有文章中有什么錯誤,歡迎留言

原創博客,轉載請注明出處!

祝各位早日實現項目叢中過,bug不沾身.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM