Linux內核調用I2C驅動_以MPU6050為例
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最底層說起。
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的註冊狀態。
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為例