I2C子系统–mpu6050驱动实验

一、i2c基本知识,回忆i2c物理总线和基本通信协议

二、 IIC驱动框架

        在编写单片机裸机i2c驱动时我们需要根据i2c协议手动配置i2c控制寄存器使其能够输出起始信号、停止信号、数据信息等等。

        在Linux系统中则采用了总线、设备驱动模型。我们之前讲解的平台设备也是采用了这种模型,只不过平台总线是一个虚拟的总线。

        我们知道一个i2c(例如i2c1)上可以挂在多个i2c设备,例如MPU6050、i2c接口的OLED显示屏、摄像头(摄像头通过i2c接口发送控制信息)等等, 这些设备共用一个i2c,这个i2c的驱动我们称为i2c总线驱动。而对应具体的设备,例如mpu6050的驱动就是i2c设备驱动。 这样我们要使用mpu6050就需要拥有“两个驱动”一个是i2c总线驱动和mpu6050设备驱动。

        i2c总线驱动由芯片厂商提供(驱动复杂,官方提供了经过测试的驱动,我们直接用),

        mpu6050设备驱动可以从mpu6050芯片厂家那里获得(不确定有),也可以我们手动编写。

 

如上图所示,i2c驱动框架包括i2c总线驱动、具体某个设备的驱动。

i2c总线包括i2c设备(i2c_client)和i2c驱动(i2c_driver), 当我们向linux中注册设备或驱动的时候,按照i2c总线匹配规则进行配对,配对成功,则可以通过i2c_driver中.prob函数创建具体的设备驱动。 在现代linux中,i2c设备不再需要手动创建,而是使用设备树机制引入,设备树节点是与paltform总线相配合使用的。 所以需先对i2c总线包装一层paltform总线,当设备树节点转换为平台总线设备时,我们在进一步将其转换为i2c设备,注册到i2c总线中。

设备驱动创建成功,我们还需要实现设备的文件操作接口(file_operations),file_operations中会使用到内核中i2c核心函数(i2c系统已经实现的函数,专门开放给驱动工程师使用)。 使用这些函数会涉及到i2c适配器,也就是i2c控制器。由于ic2控制器有不同的配置,所有linux将每一个i2c控制器抽象成i2c适配器对象。 这个对象中存在一个很重要的成员变量——Algorithm,Algorithm中存在一系列函数指针,这些函数指针指向真正硬件操作代码。

三、mpu6050驱动代码

3.1 添加设备树节点

pinctrl_i2c1: i2c1grp {        //在iomuxc子节点下面添加新的子节点pinctrl_i2c1: i2c1grp
            fsl,pins = <
                    MX6UL_PAD_UART4_TX_DATA__I2C1_SCL 0x4001b8b0
                    MX6UL_PAD_UART4_RX_DATA__I2C1_SDA 0x4001b8b0
            >;//在fsl,pins属性声明引脚组
    };





//是在i2c1控制器下面设置i2c1属性。
    &i2c1{
            clock-frequency = <100000>;
//设置i2c1控制器的时钟频率100k
            pinctrl-names = "default";
// 设置引脚默认状态
            pinctrl-0 = <&pinctrl_i2c1>;
//设置pinctrl_i2c1为具体的引脚组
            status = "okay"; 
// 设置i2c1控制器状态为okay           

//添加MPU6050子节点
            i2c_mpu6050@68 {   
//设置MPU6050子节点属性为fire,i2c_mpu6050,和驱动保持一致即可。         
                                    compatible = "fire,i2c_mpu6050";
/设置reg属性,reg属性只需要指定MPU6050在i2c1总线上的地址,原理图分析可知为0x68。
                                    reg = <0x68>;
                                    status = "okay";
            };
    };

3.2 编写mpu6050驱动编程

3.2.1 驱动入口和出口函数实现

        驱动入口和出口函数仅仅用于注册、注销i2c设备驱动,代码如下:

        mpu6050驱动入口和出口函数实现 (linux_driver/I2c_MPU6050/i2c_mpu6050.c)

/*定义ID 匹配表*/
static const struct i2c_device_id gtp_device_id[] = {
    {"fire,i2c_mpu6050", 0},
    {}};

/*定义设备树匹配表*/
static const struct of_device_id mpu6050_of_match_table[] = {
    {.compatible = "fire,i2c_mpu6050"},
    {/* sentinel */}};

/*定义i2c设备结构体*/
struct i2c_driver mpu6050_driver = {
    .probe = mpu6050_probe,
    .remove = mpu6050_remove,
    .id_table = gtp_device_id,
    .driver = {
            .name = "fire,i2c_mpu6050",
            .owner = THIS_MODULE,
            .of_match_table = mpu6050_of_match_table,
    },
};

/*
*驱动初始化函数
*/
static int __init mpu6050_driver_init(void)
{
    int ret;
    pr_info("mpu6050_driver_init\n");
//添加一个i2c设备驱动
    ret = i2c_add_driver(&mpu6050_driver);
    return ret;
}

/*
*驱动注销函数
*/
static void __exit mpu6050_driver_exit(void)
{
    pr_info("mpu6050_driver_exit\n");
//删除一个i2c设备驱动
    i2c_del_driver(&mpu6050_driver);
}

module_init(mpu6050_driver_init);
module_exit(mpu6050_driver_exit);

MODULE_LICENSE("GPL");

3.2.2  probe函数和.remove函数实现

        通常情况下.probe用于实现一些初始化工作,.remove用于实现退出之前的清理工作。 mpu6050需要初始化的内容很少,我们放到了字符设备的.open函数中实现.probe函数只需要添加、注册一个字符设备即可。

程序源码如下所示:

//.probe函数仅仅注册了一个字符设备
static int mpu6050_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    int ret = -1; //保存错误状态码
    printk(KERN_EMERG "\t  match successed  \n");
    //采用动态分配的方式,获取设备编号,次设备号为0
    ret = alloc_chrdev_region(&mpu6050_devno, 0, DEV_CNT, DEV_NAME);
    if (ret < 0)
    {
            printk("fail to alloc mpu6050_devno\n");
            goto alloc_err;
    }
    ...
}


//.remove函数工作是注销字符设备
static int mpu6050_remove(struct i2c_client *client)
{
    /*删除设备*/
    device_destroy(class_mpu6050, mpu6050_devno);     //清除设备
    class_destroy(class_mpu6050);                                     //清除类
    cdev_del(&mpu6050_chr_dev);                                               //清除设备号
    unregister_chrdev_region(mpu6050_devno, DEV_CNT); //取消注册字符设备
    return 0;
}

3.2.3 实现字符设备操作函数集

        在.probe函数中添加了一个字符设备,mpu6050的初始化以及转换结果的读取都在这个字符设备的操作函数中实现, 其中最主要是.open 和.read函数。下面是这两个函数的实现。

/*字符设备操作函数集,open函数实现*/
//调用了我们自己编写的mpu6050_init函数
static int mpu6050_open(struct inode *inode, struct file *filp)
{
    // printk("\n mpu6050_open \n");
    /*向 mpu6050 发送配置数据,让mpu6050处于正常工作状态*/
    mpu6050_init();
    return 0;
}

/*初始化i2c
 *返回值,成功,返回0。失败,返回 -1
 */
//调用i2c_write_mpu6050函数向mpu6050发送控制参数,控制参数可参考芯片手册,我们重点讲解函数i2c_write_mpu6050实现。
static int mpu6050_init(void)
{
    int error = 0;
    /*配置mpu6050*/
    error += i2c_write_mpu6050(mpu6050_client, PWR_MGMT_1, 0X00);
    error += i2c_write_mpu6050(mpu6050_client, SMPLRT_DIV, 0X07);
    error += i2c_write_mpu6050(mpu6050_client, CONFIG, 0X06);
    error += i2c_write_mpu6050(mpu6050_client, ACCEL_CONFIG, 0X01);

    if (error < 0)
    {
            /*初始化错误*/
            printk(KERN_DEBUG "\n mpu6050_init error \n");
            return -1;
    }
    return 0;
}


/*通过i2c 向mpu6050写入数据
 *mpu6050_client:mpu6050_client是i2c_client类型的结构体。
 *address, 数据要写入的地址,
 *data, 要写入的数据
 *返回值,错误,-1。成功,0
 */
static int i2c_write_mpu6050(struct i2c_client *mpu6050_client, u8 address, u8 data)
{
    int error = 0;
    u8 write_data[2];
    struct i2c_msg send_msg; //要发送的数据结构体,用来装要发送的数据

    /*设置要发送的数据*/
   //保存地址和数据
    write_data[0] = address;
    write_data[1] = data;

    /*发送 iic要写入的地址 reg*/
    send_msg.addr = mpu6050_client->addr; //mpu6050在 iic 总线上的地址
    send_msg.flags = 0;                                       //标记为发送数据
    send_msg.buf = write_data;                        //写入的首地址
    send_msg.len = 2;                                         //reg长度

    /*执行发送*/
    //i2c_transfer是系统提供的i2c设备驱动发送函数
    error = i2c_transfer(mpu6050_client->adapter, &send_msg, 1);
    if (error != 1)
    {
            printk(KERN_DEBUG "\n i2c_transfer error \n");
            return -1;
    }
    return 0;
}

3.2.4 mpu6050_read函数源码

/*字符设备操作函数集,.read函数实现*/
static ssize_t mpu6050_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{

    char data_H;
    char data_L;
    int error;
    short mpu6050_result[6]; //保存mpu6050转换得到的原始数据

    i2c_read_mpu6050(mpu6050_client, ACCEL_XOUT_H, &data_H, 1);
    i2c_read_mpu6050(mpu6050_client, ACCEL_XOUT_L, &data_L, 1);
    mpu6050_result[0] = data_H << 8;
    mpu6050_result[0] += data_L;

    i2c_read_mpu6050(mpu6050_client, ACCEL_YOUT_H, &data_H, 1);
    i2c_read_mpu6050(mpu6050_client, ACCEL_YOUT_L, &data_L, 1);
    mpu6050_result[1] = data_H << 8;
    mpu6050_result[1] += data_L;

    i2c_read_mpu6050(mpu6050_client, ACCEL_ZOUT_H, &data_H, 1);
    i2c_read_mpu6050(mpu6050_client, ACCEL_ZOUT_L, &data_L, 1);
    mpu6050_result[2] = data_H << 8;
    mpu6050_result[2] += data_L;

    i2c_read_mpu6050(mpu6050_client, GYRO_XOUT_H, &data_H, 1);
    i2c_read_mpu6050(mpu6050_client, GYRO_XOUT_L, &data_L, 1);
    mpu6050_result[3] = data_H << 8;
    mpu6050_result[3] += data_L;

    i2c_read_mpu6050(mpu6050_client, GYRO_YOUT_H, &data_H, 1);
    i2c_read_mpu6050(mpu6050_client, GYRO_YOUT_L, &data_L, 1);
    mpu6050_result[4] = data_H << 8;
    mpu6050_result[4] += data_L;

    i2c_read_mpu6050(mpu6050_client, GYRO_ZOUT_H, &data_H, 1);
    i2c_read_mpu6050(mpu6050_client, GYRO_ZOUT_L, &data_L, 1);
    mpu6050_result[5] = data_H << 8;
    mpu6050_result[5] += data_L;

    /*将读取得到的数据拷贝到用户空间*/
    error = copy_to_user(buf, mpu6050_result, cnt);

    if(error != 0)
    {
            printk("copy_to_user error!");
            return -1;
    }
    return 0;
}


static int i2c_read_mpu6050(struct i2c_client *mpu6050_client, u8 address, void *data, u32 length)
{
    int error = 0;
    u8 address_data = address;
//其中i2c_msg结构体,读取工作与写入不同,读取时需要先写入要读取的地址然后再执行读取
    struct i2c_msg mpu6050_msg[2];

    /*设置读取位置i2c_msg*/
    mpu6050_msg[0].addr = mpu6050_client->addr; //mpu6050在 iic 总线上的地址
    mpu6050_msg[0].flags = 0;                                       //标记为发送数据
    mpu6050_msg[0].buf = &address_data;                     //写入的首地址
    mpu6050_msg[0].len = 1;                                         //写入长度

    /*读取i2c_msg*/
    mpu6050_msg[1].addr = mpu6050_client->addr; //mpu6050在 iic 总线上的地址
    mpu6050_msg[1].flags = I2C_M_RD;                        //标记为读取数据
    mpu6050_msg[1].buf = data;                                      //读取得到的数据保存位置
    mpu6050_msg[1].len = length;                            //读取长度,单位字节

    error = i2c_transfer(mpu6050_client->adapter, mpu6050_msg, 2);

    if (error != 2)
    {
            printk(KERN_DEBUG "\n i2c_read_mpu6050 error \n");
            return -1;
    }
    return 0;
}

3.2.5 mpu6050测试应用程序实现

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
//保存收到的 mpu6050转换结果数据,依次为 AX(x轴角度), AY, AZ 。GX(x轴加速度), GY ,GZ
    short resive_data[6];
    printf("led_tiny test\n");

    /*打开文件*/
//打开MPU6050设备文件。
    int fd = open("/dev/I2C1_mpu6050", O_RDWR);
    if(fd < 0)
    {
            printf("open file : %s failed !\n", argv[0]);
            return -1;
    }

    /*读取数据*/读取传感器是并打印
    int error = read(fd,resive_data,12);
    if(error < 0)
    {
        printf("write file error! \n");
        close(fd);
        /*判断是否关闭成功*/
    }

    printf("AX=%d, AY=%d, AZ=%d ",(int)resive_data[0],(int)resive_data[1],(int)resive_data[2]);
    printf("GX=%d, GY=%d, GZ=%d \n \n",(int)resive_data[3],(int)resive_data[4],(int)resive_data[5]);

    /*关闭文件*/
    error = close(fd);
    if(error < 0)
    {
        printf("close file error! \n");
    }

    return 0;
}

4、编译运行

4.1 编译设备树

将 linux_driver/I2c_MPU6050/imx6ull-mmc-npi.dts 拷贝到 内核源码/arch/arm/boot/dts/

输入以下命令编译设备树:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- npi_v7_defconfig
make ARCH=arm -j4 CROSS_COMPILE=arm-linux-gnueabihf- dtbs

编译成功后生成的设备树文件(.dtb)位于源码目录下的 内核源码/arch/arm/boot/dts, 开发板适配的设备树文件名为 imx6ull-mmc-npi.dts。

4.2 编译驱动程序和应用程序

将 linux_driver/I2c_MPU6050/ 拷贝到内核源码同级目录,执行里面的MakeFile,生成i2c_mpu6050.ko和6050_test_app

4.3 加载设备树 

通过SCP或NFS将编译好的设备树拷贝到开发板上。替换掉原来的设备树文件 /usr/lib/linux-image-4.19.35-imx6/imx6ull-mmc-npi.dts 。

除此以外还需要将开发板上其他i2c设备屏蔽,打开/boot/目录下的uEnv.txt文件,屏蔽如下内容。

reboot 重启开发板

4.4 测试结果

加载i2c_mpu6050.ko,拿起开发板,运行6050_test_app即可看到如下效果。

   注意:这里采集的是原始数据,所以波动较大是正常的。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值