1. 程式人生 > >Linux內核調用I2C驅動_以MPU6050為例

Linux內核調用I2C驅動_以MPU6050為例

匹配 獲取 inux error: sdn git alt 說明 時鐘

Linux內核調用I2C驅動_以MPU6050為例

0. 導語

最近一段時間都在惡補數據結構和C++,加上導師的事情比較多,Linux內核驅動的學習進程總是被阻礙、不過,十一假期終於沒有人打擾,有這個奢侈的大塊時間,可以一個人安安靜靜的在教研室看看Linux內核驅動的東西。按照Linux嵌入式學習的進程,SPI驅動搞完了之後就進入到I2C驅動的學習當中,十一還算是比較順利,I2C的Linux驅動完成了。

為了測試I2C是否好用,選擇一個常用的I2C傳感器,手頭有個MPU6050,剛好作為I2C的從器件,那就以MPU6050為例,進行Linux底層的I2C驅動開發。

同樣的使用Linux內核中的GPIO模擬I2C的時序一點難度沒有,I2C的硬件標準時序也是非常的簡單,閉著眼睛都能畫出時序圖吧,如果我們使用Linux內核提供了I2C機制,那麽問題不單單是要解決時序,而重點在於對於整個I2C的機制的把握,

,。剛剛拿到I2C內核機制的時候,我也看的很暈,i2c_client, i2c_master, i2c_driver, i2c_device,這些東西到底有什麽關系呢?到底我該如何讓Linux系統的I2C為我所用,按照我的意願對MPU6050進行讀取?到底我能挑出對我有用的Linux的I2C機制,其他沒用的機制我不啟動,以簡化代碼。

那麽,就真需要從I2C最底層說起。

1. 實驗平臺

平臺 內容
ARM板子 友善之臂Nano-T3 (CortexA53架構,Samsung S5C6818芯片)
ARM板子的Linux系統 Ubuntu 16.04.2 LTS
Linux開發主機 Ubuntu 16.04.3 LTS amd4版本
編譯器COMPILE_CROSS arm-cortexa9-linux-gnueabihf-
從設備 MPU6050模塊(I2C接口)

2. 查看系統I2C的支持

按照SPI驅動的思維,使用spi_driver註冊,然後和spi_device匹配,使之進入probe函數,完成spi_master的獲取,依照這個方法,我的I2C驅動也是按照這個方法,尋求i2c_driver和i2c_device匹配,然而I2C的驅動尤其特殊之處,使得我的i2c_driver怎麽註冊都不成功,不是內核內存炸了,就是總是返回失敗。

後來我才發現,i2c的使用是不需要註冊的,或者嚴格說一點,Linux系統在啟動的時候已經幫你註冊好了,而你再去i2c_driver_register的時候肯定是失敗的。所以到底我們使用I2C驅動的時候到底需不需要註冊,則需要在Linux系統裏面查看當前I2C的註冊狀態。

那麽流程就比較清晰了,如果查看系統註冊了I2C那麽就在驅動中直接使用;如果系統沒有註冊I2C那麽我們先註冊I2C再使用。

2.1 如何查看?

目標板終端輸入:ls /sys/bus/i2c/devices

技術分享圖片

可以看到我這個主機是支持4個I2C外設的(方框圈出),如果是這樣的情況,我們就可以直接使用上面的i2c。這裏的i2c-0,i2c-1....指的是4個i2c_master,而i2c_master可以掛N個i2c_client

其他的數字設備就是我掛載的i2c_master上的i2c_client,舉個例子,畫圈的【0-0069】意思是:掛載到i2c-0上的從地址為0x69的設備,那麽【2-0048】的意思就是:掛載到i2c-2 adapter上的從地址為0x48的設備。

我們開發的MPU6050驅動依托I2C進行傳輸,則需要在這個文件夾創建設備節點才能利用Linux內核提供的I2C方法進行數據的交互。

2.2 弄清楚MPU6050的從地址與Linux I2C從地址的合法性

隨手搜了一下MPU6050的從地址,有的給出了MPU6050的從地址是0x68,有的給出的是0xD0,一開始我也懶查,認定MPU6050的地址在A0引腳為低電平的時候為0x68,加載驅動的時候出現了很尷尬的事情,0-0068這個地址已經被DS1607實時時鐘占用,然後網上有人說是把A0引腳打到高電平地址就是0xD0,可是我試0xD0的時候,被Linux警告,說是從地址不合法,我查看了Linux內核的i2c_core.c文件,裏面有個地址校驗,高於0x7F的7-bit地址,都是不合法的,Linux不可能犯這樣的錯誤,肯定是網友的鍋。果然,我閱讀了手冊,如果A0的電平為高那麽地址是0x69。說從地址是0xD0的人,犯了一個錯誤,他們多半玩的是模擬IO出的I2C波形,他們對I2C協議標準不夠了解,的確0x69 << 1 = 0xD0,I2C在讀寫的時候,預留出7-bit地址前移1位,把最低位作為讀寫標識,但絕對不能說從地址就是0xD0。

不過可以再一次看見Linux內核的嚴謹、嚴肅的態度。也再一次說,不能懶惰,自己查手冊,看最標準的說明。

3 I2C 驅動開發

我這裏給出最簡單的模型,其他的字符驅動註冊什麽的同spi驅動,這裏只說明I2C驅動怎麽使用。

3.1 I2C的註冊

static struct i2c_board_info __initdata sp6818_mpu6050_board_info = {
    I2C_BOARD_INFO("mpu6050-i2c", MPU6050_SLAVE_ADDRESS),,
    .irq    = -1,
};

int xxx_hw_init(){
    struct i2c_client *client;
    struct i2c_adapter *adapter;

    adapter = i2c_get_adapter(0);
    if (!adapter) {
        ret = -ENXIO;
        printk(DRV_NAME "\terror: %d : init i2c adapter failed.\n", ret);
        return ret;
    }
    strlcpy(adapter->name, "nxp_i2c",sizeof(adapter->name));
    client = i2c_new_device(adapter, &sp6818_mpu6050_board_info);
    if (!client) {
        ret = -ENXIO;
        printk(DRV_NAME "\terror: %d : init i2c client failed.\n", ret);
        return ret;
    }
}

你沒有看錯,i2c的使用就是這麽簡單,我有什麽辦法,我之前開發加上i2c_register和字符驅動的初始化什麽的,整init函數整了近100多行,結果不斷的嘗試,發現就這些。

下面就說幾個重點:

3.1.1 adapter的獲取

adapter = i2c_get_adapter(0);定義一個指針,然後使用i2c_get_adapter(0),得到我們上面說的,i2c-0,這個adapter。你疑問了,我為什麽選擇i2c-0這個adapter,為什麽不選擇-i2c-其他。因為這個開發板只把-i2c-0的引腳印出來了。

技術分享圖片

。。。

這樣就獲取到了adapter。

3.1.2 client的創建

接著我們就要創建一個client,這個client就指的是你的mpu6050,我們使用i2c_board_info這個結構體來描述mpu6050,先定義一個這個info:

static struct i2c_board_info __initdata sp6818_mpu6050_board_info = {
    I2C_BOARD_INFO("mpu6050-i2c", MPU6050_SLAVE_ADDRESS),,
    .irq    = -1,
};

第二行的,”mpu6050-i2c“就是註冊到Linux系統裏面的設備名字,可以在如圖所示路徑和cat命令查看。

技術分享圖片

MPU6050_SLAVE_ADDRESS就是MPU6050的地址了,0x69 ,MPU6050的A0接高電平,地址是0x69沒毛病。

然後,就是生成這個client且和之前那個adapter綁定:

client = i2c_new_device(adapter, &sp6818_mpu6050_board_info);

之後client的信息和adapter的信息我們要保存起來,可以定義一個全局指針之類的承接初始化後的client和adapter,因為後面的傳輸數據要用。

到此,I2C完成了,很簡單,可是探索起來好麻煩。

3.2 I2C數據的寫入

static int 
__mpu6050_write_reg(MPU6050* this, char reg_addr, char reg_value)
{
    int ret;
    struct i2c_msg msg;
    char write_buffer[10];
    
    memset(write_buffer, 0, 10);
    write_buffer[0] = (char)reg_addr;
    write_buffer[1] = (char)reg_value;
    msg.addr = (this->hw->i2c_clit->addr);
    msg.flags = 0;
    msg.len = 2;
    msg.buf = &write_buffer[0];
    ret = i2c_transfer(this->hw->i2c_adper, &msg, 1);

    return ret;
}

看一下我的數據寫入函數,提取出有用的信息,mpu6050寫寄存器,需要傳輸兩個字節的信息,一個是寄存器地址,另一個是寄存器的值,按照上面的格式進行,msg.length不包含器件的從地址,就是實在的你想法幾個數據的多少,我們這裏只發兩個,一個是寄存器地址和寄存器的值,所以是2;如果你是要發送則msg.flag一定是0。

我的函數this->hw->i2c_adapter就是上面存儲的adapter的指針,this->hw->i2c_clit就是存儲的上面初始化的client的指針。

3.3 I2C數據的讀

讀相比於寫就費勁多了,但是也沒難到哪裏去,只不過是兩條msg,先寫後讀:

__mpu6050_read_reg(MPU6050* this, char reg_addr)
{

    struct i2c_msg msg[2];
    char write_buffer[10];
    int ret, i;

    memset(write_buffer, 0, 10);
    memset(this->buffer, 0, 10);
    write_buffer[0] = (char)reg_addr;
    msg[0].addr = (this->hw->i2c_clit->addr);
    msg[0].flags = 0;
    msg[0].len = 1;
    msg[0].buf = &write_buffer[0];
    msg[1].addr = (this->hw->i2c_clit->addr);
    msg[1].flags = I2C_M_RD;
    msg[1].len = 1;
    msg[1].buf = &this->buffer[0];
    
    ret = i2c_transfer(this->hw->i2c_adper, &msg, 2);
}

意思很明顯。

到此,i2c的註冊和數據傳輸完成,我們可以在上層建立函數讀取MPU6050的值了。

4 成果

驗證函數:

#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    char buffer[128];
    int in, out;
    int nread;
    unsigned int x_accel, y_accel, z_accel;
    unsigned int x_gyro, y_gyro, z_gyro;

    in = open("/dev/MPU6050", O_RDONLY);
    if (!in) {
        printf("ERROR: %d, Open /dev/MPU6050 nod failed.\n", -1);
        return -1;  
    }   
    nread = read(in, buffer, 12);
    if (nread < 0) {
        printf("ERROR: %d, A read error has occurred\n", nread);
        return -1;  
    }
    close(in);
    x_accel = buffer[1] + (buffer[0] << 8);
    y_accel = buffer[3] + (buffer[2] << 8);
    z_accel = buffer[5] + (buffer[4] << 8); 
    x_gyro = buffer[7] + (buffer[6] << 8);
    y_gyro = buffer[9] + (buffer[8] << 8);
    z_gyro = buffer[11] + (buffer[10] << 8);    
    printf("x accel is: %d \n", x_accel);
    printf("y accel is: %d \n", y_accel);
    printf("z accel is: %d \n", z_accel);   
    printf("x gyro is: %d \n", x_gyro);
    printf("y gyro is: %d \n", y_gyro);
    printf("z gyro is: %d \n", z_gyro);

    exit(0);
}

測試腳本:

# !/bin/bash                                                                            
for((i=1;i<=10000;i++));                                                                
do                                                                                      
./test_mpu6050.o                                                                        
sleep 1                                                                                 
done                                                                                    

技術分享圖片

源代碼:

Github地址:https://github.com/lifimlt/carlosdriver

見 mpu6050.c mpu6050.h 和mpu6050_def.h三個文件

mpu6050_test.c為測試文件

參考文獻:

[1] Linux org, Serial Peripheral Interface (I2C),

[2] choiyoung87, Linux中的I2C(二)——adapter的初始化, 2011年12月01日

[3] liuwanpeng , [《linux設備驅動開發詳解》筆記——15 linux i2c驅動](https://www.cnblogs.com/liuwanpeng/p/7346558.html), 2017年8月23日

Linux內核調用I2C驅動_以MPU6050為例