1. 程式人生 > >基於i2c子系統的驅動分析-裝置樹

基於i2c子系統的驅動分析-裝置樹

基於i2c子系統的驅動分析

和i2c有關的程式碼都在原始碼drivers/i2c目錄下。核心提供了兩種i2c的實現方法:

  1. 第一種叫i2c_dev,對應drivers/i2c/i2c-dev.c,這種方法僅僅封裝了soc的i2c控制器操作,並嚮應用層提供操作介面。其本質是為應用層提供了一個庫,驅動功能由應用層實現,不是主流的做法
  2. 第二種是驅動層實現所有驅動功能,是比較主流的做法

第二種可以認為是正統的i2c驅動,其本質是:工程師任意選用input子系統、misc框架、普通字元驅動等方式實現i2c驅動,i2c子系統的意義僅僅是為硬體操作提供介面(庫)

1.i2c子系統的結構

如圖
這裡寫圖片描述
可以看出,i2c子系統基本機制和platform很類似,都是裝置和驅動兩者匹配來工作。i2c驅動只需呼叫核心層提供的介面(相當於核心層提供了庫),即可方便地操作i2c

2.i2c匯流排核心分析

i2c匯流排核心提供了裝置驅動和裝置(client)的註冊、登出方法, 還提供了一組不依賴於硬體平臺的介面函式,I2C 匯流排驅動和裝置驅動之間依賴於 I2C 核心作為紐帶

3.i2c介面卡(adapter)驅動分析

所謂的i2c介面卡驅動,就是soc內部的i2c控制器的驅動,由原廠移植核心時提供,一般位於driver/i2c/busses內。而i2c介面卡裝置的註冊,在3.x後的kernel中採用了裝置樹節點的方式,故這裡需要分類討論

老核心下的i2c介面卡

我們這裡用的是i2c-s3c2410.c,該驅動相容三星大部分的soc,包括210。該驅動由platform匯流排實現,該驅動probe函式中主要做了:

  • 填充了一個i2c_adapter結構體,並呼叫介面註冊之,i2c_adapter 對應於SOC上的一個介面卡
  • 從platform_data(自留地)接收硬體資訊,做必要的處理(為暫存器申請虛擬地址對映、申請中斷等)
  • 通過操作暫存器,對soc內的i2c介面卡做初始化,比如把i2c速率設定為預設的100k。這一套設定基本通吃大部分器件,一般情況不用改動的

新核心下的i2c介面卡

在新核心下,i2c介面卡的驅動倒是沒有變化,而i2c介面卡裝置體的註冊,卻採用了裝置樹的方式

  • 下面是imx6qdl.dtsi中對i2c1介面卡裝置的定義和註冊,裡面定義了很多引數,一般來說我們是根本不用去修改這個節點的。假設我們要修改其中的引數(比如頻率),只需在專案的dts中引用該節點,並重寫即可
i2c1: i2c@021a0000 {
    #address-cells = <1>;
    #size-cells = <0>;
    compatible = "fsl,imx6q-i2c", "fsl,imx21-i2c";
    reg = <0x021a0000 0x4000>;
    interrupts = <0 36 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clks IMX6QDL_CLK_I2C1>;
    status = "disabled";
};

4.i2c裝置(client)註冊分析

所謂的i2c裝置(client),就是掛在i2c上的外設(比如各種感測器),這個需要我們自己註冊,在3.x後的kernel中採用了裝置樹節點的方式,故這裡需要分類討論

老核心下的i2c裝置(client)

對於老版本的核心,首先應該進入mach-xxx.c完成i2c裝置(client)的註冊。如何註冊?這方面i2c和platform有較大不同,主要是soc上有多個i2c,所以是分開註冊的

  • 在mach-xxx.c中的xxx_machine_init函式中,發現由i2c_register_board_info來註冊三個i2c上各自的裝置。以i2c_devs0為例,i2c_devs0是一個數組,裡面是i2c0上所有的裝置
    i2c_register_board_info(0, i2c_devs0, ARRAY_SIZE(i2c_devs0));
    i2c_register_board_info(1, i2c_devs1, ARRAY_SIZE(i2c_devs1));
    i2c_register_board_info(2, i2c_devs2, ARRAY_SIZE(i2c_devs2));
  • 檢視i2c_devs0的定義,我們發現該陣列內部都是i2c_board_info結構體,如果要新增裝置到i2c0,只需在該陣列中使用I2C_BOARD_INFO這個巨集即可,第一個引數是名字,第二個引數是裝置在i2c上的地址,此巨集的本質就是填充一個struct i2c_board_info,這一步作用是把wm8580以i2c裝置的身份被註冊,並且繫結i2c0這個介面卡
static struct i2c_board_info i2c_devs0[] __initdata = {
    {
        I2C_BOARD_INFO("wm8580", 0x1b),
        /*假如要新增裝置,就在這裡加*/
    },
};
  • 分析到這,我們可以發現結構體i2c_board_info就代表了i2c裝置,那麼client和這怎麼沒關係啊?其實結構體i2c_board_info是製造client的原料,client將會在i2c匯流排核心初始化時被製造

新核心下的i2c裝置(client)

  • 下面是imx6dl-hummingboard.dts中對於一個pcf8523的註冊,它首先引用了imx6qdl.dtsi中的i2c1介面卡,並重寫了其中的status 屬性為okay,如果裝置對頻率有要求,也可以重寫clocks屬性。在i2c1節點中定義了一個pcf8523節點,只有定義在這,該節點才會以i2c裝置的身份被註冊,並且繫結i2c1這個介面卡
&i2c1 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_hummingboard_i2c1>;
    status = "okay";

    rtc: pcf8523@68 {
        compatible = "nxp,pcf8523";
        reg = <0x68>;
    };
};

5.驅動編寫流程

首先要明白一點,對於驅動工程師,如果手中是移植過的核心,則i2c匯流排核心和i2c介面卡驅動是不需要動的,我們主要關注點在:提供i2c裝置(client)、編寫i2c裝置驅動

  • i2c子系統的本質是:工程師任意選用input子系統、misc框架、普通字元驅動等方式實現i2c驅動,i2c子系統的意義僅僅是為硬體操作提供介面(庫)

這裡寫圖片描述

  • 此外,對於新核心和老核心,裝置樹會導致驅動會有一些細微的不同,主要體現在驅動和裝置match的部分

老核心下的i2c驅動

#include <linux/i2c.h>
#include <linux/module.h>
#include <linux/string.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/delay.h>
#include <asm/uaccess.h>

/*mpu6050內部暫存器地址 */
#define MPU6050_RA_PWR_MGMT_1       0x6B
#define MPU6050_RA_ACCEL_XOUT_H     0x3B

#define MPU6050_CNT 1
#define MPU6050_NAME "mpu6050"

struct i2c_client *mpu6050_client;

/* 
 * 寫mpu6050內部的暫存器。先發暫存器地址再發暫存器的值
 */
static int mpu6050_write_reg(unsigned char addr, unsigned char dat)
{
    int ret = -1;
    struct i2c_msg msgs[2];

    msgs[0].addr  = mpu6050_client -> addr;//MPU6050_ADDR
    msgs[0].buf   = &addr;              
    msgs[0].len   = 1; //長度1 byte
    msgs[0].flags = 0; //表示寫

    msgs[1].addr  = mpu6050_client -> addr;//MPU6050_ADDR
    msgs[1].buf   = &dat;                 
    msgs[1].len   = 1; //長度1 byte
    msgs[1].flags = 0; //表示寫 

    /*連續傳送兩幀資訊*/
    ret = i2c_transfer(mpu6050_client ->adapter, msgs, 2);
    if (ret != 2) {
        printk(KERN_INFO "i2c_transfer(mpu6050 write) error \n");
        return -EIO;
    }
    return 0;
}

/* 
 *讀mpu6050內部的暫存器。先發暫存器地址再讀暫存器的值
 */
static int mpu6050_read_reg(unsigned char addr,  unsigned char buf)
{
    int ret = -1;
    struct i2c_msg msgs[2];

    msgs[0].addr  = mpu6050_client -> addr;//MPU6050_ADDR
    msgs[0].buf   = &addr;              
    msgs[0].len   = 1;  //長度1 byte
    msgs[0].flags = 0;  //表示寫

    msgs[1].addr  = mpu6050_client -> addr;//MPU6050_ADDR
    msgs[1].buf   = &buf;                 
    msgs[1].len   = 1;  //長度1 byte
    msgs[1].flags = I2C_M_RD; //表示讀 

    /*連續傳送兩幀資訊*/
    ret = i2c_transfer(mpu6050_client ->adapter, msgs, 2);
    if (ret != 2) {
        printk(KERN_INFO "i2c_transfer(mpu6050 read) error \n");
        return -EIO;
    }

    return 0;
}

static int mpu6050_open(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "open mpu6050\n");
    msleep(50);
    mpu6050_write_reg(MPU6050_RA_PWR_MGMT_1, 0X80);//復位 
    /*這裡僅僅做個例子,一般在這裡要做初始化*/
    return 0;
}


ssize_t mpu6050_read(struct file *file, char __user *ubuf,
         size_t size, loff_t *opp)
{
    unsigned char buf [6] = {0};

    mpu6050_read_reg(MPU6050_RA_GYRO_XOUT_H, buf[0]);

    /*這裡僅僅是舉個例子,怎麼從外設中讀資料*/

    ret = copy_to_user(ubuf, buf , size);
    if (ret) {
        printk(KERN_INFO "copy_to_user fail\n");
        return -EINVAL;
    }

    return 0;
}

static int mpu6050_release(struct inode *inode, struct file *file)
{
    return 0;
}

static const struct file_operations mpu6050_fops = {
    .owner = THIS_MODULE,
    .open = mpu6050_open,
    .read = mpu6050_read,
    .release = mpu6050_release,
};


static struct cdev *mpu6050_pcdev;
static struct class *mpu6050_pclass;

dev_t mpu6050dev_num = 0;
unsigned int mpu6050dev_major = 0;

int mpu6050_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    int ret = -1;

    mpu6050_client = client;
    /*核心自動分配一個裝置號*/
    ret = alloc_chrdev_region(&mpu6050dev_num, 0, MPU6050_CNT, MPU6050_NAME);
    mpu6050dev_major = MAJOR(mpu6050dev_num);
    if (ret < 0) {
        printk(KERN_INFO "alloc_chrdev_region fail\n");
        goto out_err_0;
    }
    printk(KERN_INFO "MAJOR %d\n", mpu6050dev_major);

    /*例項化一個字元裝置體*/
    mpu6050_pcdev = cdev_alloc();
    /*填充cdev裝置體 。最主要是將file_operations填充進去*/
    cdev_init(mpu6050_pcdev, &mpu6050_fops);

    /* 將裝置體與裝置號繫結並向核心註冊一個字元裝置*/
    ret = cdev_add(mpu6050_pcdev, mpu6050dev_num, MPU6050_CNT);
    if (ret) {
        printk(KERN_INFO "cdev_add fail\n");
        goto out_err_1;
    }

    /*建立類、裝置*/
    mpu6050_pclass = class_create(THIS_MODULE, "mpu6050");
    if (IS_ERR(mpu6050_pclass)) {     //排錯
    printk(KERN_ERR "can't register  class\n");
        goto out_err_2;
    }
    device_create(mpu6050_pclass, NULL, mpu6050dev_num, NULL, "mpu6050");

    return 0;

/* “倒影式”錯誤處理流程*/
out_err_3:
    class_destroy(mpu6050_pclass);

out_err_2:
    cdev_del(mpu6050_pcdev);

out_err_1:
    unregister_chrdev_region(mpu6050dev_num, MPU6050_CNT);

out_err_0:
    return -EINVAL;
}

int mpu6050_remove(struct i2c_client *client)
{
    /*倒影式登出流程*/
    device_destroy(mpu6050_pclass, mpu6050dev_num);
    class_destroy(mpu6050_pclass);
    cdev_del(mpu6050_pcdev);
    unregister_chrdev_region(mpu6050dev_num, MPU6050_CNT);

    return 0;
}

/*
 * i2c裝置驅動結構體內的id_table。用作匹配功能
 */
static struct i2c_device_id mpu6050_id[] = {
    { "mpu6050", 0},
    { }
};
/*
 * 這裡開始定義i2c裝置驅動結構體
 */
static struct i2c_driver mpu6050_driver = {
    .driver = {
    .name = "mpu6050",//i2c匯流排和platform不同這個name僅僅是名字。並不用作匹配功能
       .owner = THIS_MODULE,
    },
    .probe = mpu6050_probe,
    .remove = mpu6050_remove,
    .id_table = mpu6050_id,//i2c匯流排和platform不同。只用id_table來匹配driver和client
};

/*
 * 模組載入函式負責註冊i2c裝置驅動
 */
static int __init mpu6050_init(void)
{
    return i2c_add_driver(&mpu6050_driver);
}

static void __exit mpu6050_exit(void)
{
    i2c_del_driver(&mpu6050_driver);
}

module_init(mpu6050_init);
module_exit(mpu6050_exit);

MODULE_LICENSE("GPL");

整個程式很簡單,關鍵點主要是driver結構體裡要有一個id_table,如果mach-xxx中定義的裝置名字和id_table相同,那麼會probe函式就被觸發
  • 觸發probe後,i2c_client *client將作為引數傳入probe,這個i2c_client裡面就包含了裝置的私有資料(比如裝置的i2c地址、繫結的i2c介面卡等),類似plat_data,我們在probe中將i2c_client *client繫結給全域性變數mpu6050_client,這樣就能在read、write等函式中用mpu6050_client -> addr來得到裝置的i2c地址了,用mpu6050_client ->adapter來得到繫結的i2c介面卡
  • 只要read、write知道了裝置的i2c地址、繫結的i2c介面卡,那麼就能利用kernel提供的介面來進行i2c傳輸。上面程式碼中用的介面是i2c_transfer,這種方法有點老舊;kernel官方強烈推薦smbus族介面來進行i2c收發,smbus是i2c_transfer的子集,很多soc可能不支援i2c_transfer這個介面,這時就只能使用smbus族介面。這兩個介面在內部邏輯上有很大不同,比如我們要寫mpu6050內部的RA_PWR_MGMT_1暫存器,根據上面的程式碼,我們呼叫了兩次i2c_transfer,第一次傳送RA_PWR_MGMT_1暫存器的地址,第二次傳送要寫的值。而對於smbus族介面來說,只需呼叫一次就行了,可以認為smbus族介面進行了更好的封裝,不僅寫操作如此,讀操作也如此,具體介面如下
/*第一個引數是client,第二個引數是i2c裝置內的暫存器地址,第三個引數是要寫入的值*/
i2c_smbus_write_byte_data(mpu6050_client, MPU6050_RA_PWR_MGMT_1, data);

/*第一個引數是client,第二個引數是i2c裝置內的暫存器地址,返回值是讀出來的值*/
read_val = i2c_smbus_read_byte_data(mpu6050_client,  MPU6050_RA_ACCEL_XOUT_H);
  • 如果要以16bit為單位讀寫i2c,那麼可以用下面的介面,使用方法都是類似的
i2c_smbus_read_word_data();
i2c_smbus_write_word_data();

新核心下的i2c驅動

裝置樹對i2c裝置的註冊有比較大的影響,詳見前面的章節,這裡不再贅述;而對於驅動程式,裝置樹帶來的變化極小,主要是驅動和裝置之間的匹配方式變了

  • 老舊的id_table方式不再使用,取而代之的是類似的一種結構:of_match_table
  • 這裡以pcf8523驅動為例,只要驅動中的of_match_table 中的compatible 值和裝置節點中的compatible 相匹配,那麼probe函式就會被觸發。不僅i2c是這樣,platform、spi等都是這個原理
/*定義的of_match_table*/
static const struct of_device_id pcf8523_of_match[] = {
    { .compatible = "nxp,pcf8523" },
    { }
};

/*driver 結構體中的of_match_table*/
static struct i2c_driver pcf8523_driver = {
    .driver = {
        .name = DRIVER_NAME,
        .owner = THIS_MODULE,
        .of_match_table = of_match_ptr(pcf8523_of_match),
    },
    .probe = pcf8523_probe,
    .id_table = pcf8523_id,
};
i2c和spi驅動還支援一種“別名匹配”的機制,就以pcf8523為例,假設某程式設計師在裝置樹中的pcf8523裝置節點中寫了compatible = “pcf8523”;,顯然相對於驅動id_table中的”nxp,pcf8523”,他遺漏了nxp欄位,但是驅動卻仍然可以匹配上,因為別名匹配對compatible中字串裡第二個欄位敏感