Linux内核调用I2C驱动实现MPU6050的数据读取

45次阅读

共计 5558 个字符,预计需要花费 14 分钟才能阅读完成。

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 版本
Linux 内核版本 Linux3.4.y
编译器 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>

short x_accel, y_accel, z_accel;
short x_gyro, y_gyro, z_gyro;

int main()
{char buffer[128];
    short *time;
    int in, out;
    int nread;
    
    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);
    close(in);    
    if (nread < 0) {printf("ERROR: %d, A read error has occurred\n", nread);
        return -1;    
    }

    time = (short*)buffer;
    x_accel = *(time);
    y_accel = *(time + 1);
    z_accel = *(time + 2);
    x_gyro =  *(time + 3);
    y_gyro =  *(time + 4);
    z_gyro =  *(time + 5);
    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/ca…

见 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 驱动](http://www.cnblogs.com/liuwan… 2017 年 8 月 23 日

正文完
 0