linux驱动i2c子系统之mpu6050

前言:

        其实也没什么新颖的,因为Linux内核贯彻设备和驱动分离的原则,i2c驱动开发的流程和平台总线设备驱动开发差不太多。这个i2c子系统呢同样也是系统帮我们做好了bus,在硬件外设信息方面,我们描述好i2c外设设备然后注册进去或在设备树中描述好i2c外设硬件信息,设备这部分就搞定了,而i2c驱动方面,也和之前学过的一样,写好匹配的符号表,和proce的函数然后注册这个驱动到系统中,如果在系统中找到对应的设备或者在设备树中找到了一致的compatible,就会进入probe函数。

        匹配成功之后同样就可以拿到基于i2c协议外设的硬件信息了(proce函数的一个参数是i2c_client,从这里拿),一般都是外设的地址,我们都知道i2c通信,主机都是要知道i2c的地址的;当然根据需要,可以定义时钟之类的信息。i2c子系统没有帮我们生成设备节点,所以我们要走字符设备驱动的老路子,“入号注节硬操”,才能生成设备节点、响应应用程序的文件操作。

i2c子系统中,有几个比较重要的概念。

1.i2c_adapter,直译是适配器的意思,但是我觉得叫控制器比较好理解,一般一个SOC都有很多个i2c控制器,不过这个不用我们管,linux内核已经写好了,它实际就是驱动soc的i2c控制器的。在后面的代码会体现,我们i2c传输数据的函数,这个成员的主人就是i2c_adapter。

2.i2c_client,直译是客户端的意思,我的理解是它就是adapter的小弟,其实就是用来描述挂载在i2c_adapter下的i2c接口的外设。这个就相当于平台设备驱动模型中的“设备”的角色。我们需要描述硬件信息,如我现在要写的是mpu6050,就要在设备树控制器中,写个mpu6050子节点,给它写上地址等信息。

3.i2c_driver,这个当然就是i2c驱动了,要写作为匹配的符号表、要写probe之后要做什么。

代码:

设备树:

首先我们要确定,我们的开发板有哪些I2C的gpio还可以用,我在开发板底板的原理图找到了摄像头接口里面有I2C_7接口,我在内核配置中没有发现有配置进CAMERA,应该是可以用的。

其实从另一个角度也能看出有没有空闲可用的i2c接口。

从芯片手册的i2c章节,我们可以知道4412这个soc一共有9个i2c控制器,一个给了HDMI,所以还有8个可以用。

通过查看/sys/bus/i2c/devices可以看到有几个i2c控制器已经用了,作为学习就尽量选择没有被用过的。

选定用I2C7作为i2c控制器后,去设备树找i2c,找到i2c7控制器:

这个是SOC厂家写的disi,我们一般不用改动已经写好的部分,但是状态status那我们要改成"okay",当当然,不是在这里改。

来到我们板级的设备树文件exynos4412-itop-elite.dts

编写一个节点:

&i2c_7{
	status = "okay";
	mpu6050@68{
			compatible = "lch-mpu6050";
			reg = <0x68>;

	};
};

这个&i2c_7最终编译的时候会与之前那个内容很多的i2c_7合并,并且有不同的地方,以后编译的dts文件为准进行覆盖,所以status最终会是“okay”即开启的状态。而

    mpu6050@68{
            compatible = "lch-mpu6050";//这个是与驱动进行匹配的
            reg = <0x68>;

    };

这个子节点,只需要写设备地址就够了。这个地址要查mpu6050的芯片手册(或其他i2c外设的芯片手册)。如果是有其他需要的信息也可以再定义。

15th_i2c.c

//一堆头文件
#include <linux/init.h>
#include <linux/module.h>
#include <linux/input.h>
#include <linux/interrupt.h>
#include <linux/slab.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <linux/of_gpio.h>
#include <linux/i2c.h>
									
#include <asm/io.h>
#include <linux/uaccess.h>
//里面定义了要传输的数据的数据结构,这样应用里接收数据时也可以用								
#include "mpu6050.h"
//下面是一堆寄存器的地址,有的是初始化mpu6050要用的,有的是读数据要用的
//反正最终都是用i2c的函数去读/写。								
									
#define SMPLRT_DIV		0x19 //采样频率寄存器-25 典型值:0x07(125Hz)
																		//寄存器集合里的数据根据采样频率更新
#define CONFIG			0x1A	//配置寄存器-26-典型值:0x06(5Hz)
																			//DLPF is disabled(DLPF_CFG=0 or 7)
#define GYRO_CONFIG		0x1B//陀螺仪配置-27,可以配置自检和满量程范围
																		//典型值:0x18(不自检,2000deg/s)
#define ACCEL_CONFIG		0x1C	//加速度配置-28 可以配置自检和满量程范围及高通滤波频率
																			//典型值:0x01(不自检,2G,5Hz)
#define ACCEL_XOUT_H	0x3B //59-65,加速度计测量值 XOUT_H
#define ACCEL_XOUT_L	0x3C  // XOUT_L
#define ACCEL_YOUT_H	0x3D  //YOUT_H
#define ACCEL_YOUT_L	0x3E  //YOUT_L
#define ACCEL_ZOUT_H	0x3F  //ZOUT_H
#define ACCEL_ZOUT_L	0x40 //ZOUT_L---64
#define TEMP_OUT_H		0x41 //温度测量值--65
#define TEMP_OUT_L		0x42
#define GYRO_XOUT_H		0x43 //陀螺仪值--67,采样频率(由寄存器 25 定义)写入到这些寄存器
#define GYRO_XOUT_L		0x44
#define GYRO_YOUT_H		0x45
#define GYRO_YOUT_L		0x46
#define GYRO_ZOUT_H		0x47
#define GYRO_ZOUT_L		0x48 //陀螺仪值--72
#define PWR_MGMT_1		0x6B //电源管理 典型值:0x00(正常启用)

//设计一个全局的设备对象
struct mpu_sensor{
	int dev_major;
	struct device *dev;
	struct class *cls;
	struct i2c_client *client;//记录probe中client
};

struct mpu_sensor *mpu_dev;

//char buf5[2] = {ACCEL_CONFIG, 0x01};
//mpu6050_write_bytes(mpu_dev->client, buf5, 2);
//封装了一个i2c写数据的函数,这样我们只需要传数据给到这个函数就会调用i2c的传输函数去写入了。
int mpu6050_write_bytes(struct i2c_client *client, char *buf, int count)
{

	int ret;
	struct i2c_adapter *adapter = client->adapter;
	struct i2c_msg msg;

	msg.addr = client->addr; //这个不是寄存器地址,而是mpu6050这个设备的地址即0x68,匹配成功后            
                             //addr会自动从设备树中找到
	msg.flags = 0;           //表明这个i2c_transfer操作是读还是写,0是写,1是读
	msg.len = count;         //数据的Byte数
	msg.buf = buf;           //数据
	
	ret = i2c_transfer(adapter, &msg,  1);    //发送数据,返回实际传输了多少B的数据

	return ret==1?count:ret;                   

}
//这个和上面那个类似
int mpu6050_read_bytes(struct i2c_client *client, char *buf, int count)
{
	
		int ret;
		struct i2c_adapter *adapter = client->adapter;    
		struct i2c_msg msg;
	
		msg.addr = client->addr;
		msg.flags = I2C_M_RD;    //这个宏实际就是1,前面说了的,读是1
		msg.len = count;
		msg.buf = buf;
		
		ret = i2c_transfer(adapter, &msg,  1);//0写1读
	
		return ret==1?count:ret;
}

//读取某个特定寄存器的地址,然后返回这个寄存器里的值
//要从i2c读数据是这样的,你要先发给它一个地址,然后进行读操作,就能读到对应寄存器里面的数据了
int mpu6050_read_reg_byte(struct i2c_client *client, char reg)
{
	// 先写寄存器的地址, 然后在读寄存器的值

		int ret;
		struct i2c_adapter *adapter = client->adapter;
		struct i2c_msg msg[2];

		char rxbuf[1];
	
		msg[0].addr = client->addr;
		msg[0].flags = 0;
		msg[0].len = 1;
		msg[0].buf = &reg;

		msg[1].addr = client->addr;
		msg[1].flags = I2C_M_RD;
		msg[1].len = 1;
		msg[1].buf = rxbuf;
		
		ret = i2c_transfer(adapter, msg,  2);
		if(ret < 0)
		{
			printk("i2c_transfer read error\n");
			return ret;
		}

		return rxbuf[0];

}
//当应用程序,打开设备节点时,就对这个外设进行初始化~一堆写寄存器操作
//具体怎么写看芯片手册的init部分,大多都是这个流程
int mpu6050_drv_open(struct inode *inode, struct file *filp)
{
	char buf1[2] = {PWR_MGMT_1, 0x0};//电源管理
	mpu6050_write_bytes(mpu_dev->client, buf1, 2);

	char buf2[2] = {SMPLRT_DIV, 0x07};//采样频率寄存器125HZ
	mpu6050_write_bytes(mpu_dev->client, buf2, 2);

	char buf3[2] = {CONFIG, 0x06};//配置寄存器 典型值
	mpu6050_write_bytes(mpu_dev->client, buf3, 2);

	char buf4[2] ={GYRO_CONFIG, 0x18};//陀螺仪配置
	mpu6050_write_bytes(mpu_dev->client, buf4, 2);

	char buf5[2] = {ACCEL_CONFIG, 0x01};//加速度配置
	mpu6050_write_bytes(mpu_dev->client, buf5, 2);

	return 0;
}
int mpu6050_drv_close(struct inode *inode, struct file *filp)
{
	return 0;
}
//这个就是通过判断应用程序传下来的cmd是什么,然后对应的返回回去上头要的数据
//这个args参数很有意思,可以用指针先把数据弄进&args,然后应用程序再用一个指针接着数据
//就能实现数据的传递了
long mpu6050_drv_ioctl (struct file *filp, unsigned int cmd, unsigned long args)
{
	union mpu6050_data data;

	switch(cmd){
		case IOC_GET_ACCEL:
			//读数据 数据手册说的,传感器一个数据是存在两个8位的寄存器中,所以我们要把它们合起来
            //例如下面这两个合起来就是x欧拉角的数据,其他的类似
			data.accel.x = mpu6050_read_reg_byte(mpu_dev->client, ACCEL_XOUT_L);
			data.accel.x |= mpu6050_read_reg_byte(mpu_dev->client, ACCEL_XOUT_H) << 8;

			data.accel.y = mpu6050_read_reg_byte(mpu_dev->client, ACCEL_YOUT_L);
			data.accel.y |= mpu6050_read_reg_byte(mpu_dev->client, ACCEL_YOUT_H) << 8;

			data.accel.z = mpu6050_read_reg_byte(mpu_dev->client, ACCEL_ZOUT_L);
			data.accel.z |= mpu6050_read_reg_byte(mpu_dev->client, ACCEL_ZOUT_H) << 8;
			break;
		case IOC_GET_GYRO:
			data.gyro.x = mpu6050_read_reg_byte(mpu_dev->client, GYRO_XOUT_L);
			data.gyro.x |= mpu6050_read_reg_byte(mpu_dev->client, GYRO_XOUT_H) << 8;


			data.gyro.y = mpu6050_read_reg_byte(mpu_dev->client, GYRO_YOUT_L);
			data.gyro.y |= mpu6050_read_reg_byte(mpu_dev->client, GYRO_YOUT_H) << 8;

			data.gyro.z= mpu6050_read_reg_byte(mpu_dev->client, GYRO_ZOUT_L);
			data.gyro.z |= mpu6050_read_reg_byte(mpu_dev->client, GYRO_ZOUT_H) << 8;
			break;
		case IOC_GET_TEMP:
			data.temp = mpu6050_read_reg_byte(mpu_dev->client, TEMP_OUT_L);
			data.temp |= mpu6050_read_reg_byte(mpu_dev->client, TEMP_OUT_H) << 8;
			break;
		default:
			printk("invalid cmd\n");
			return -EINVAL;
	}
    //这里为什么要用switch呢?因为存数据的这个数据结构是联合体
	//将所有的数据交给用户
	if(copy_to_user((void __user * )args, &data, sizeof(data)) > 0)
		return -EFAULT;
	return 0;
}
//“操”字诀
const struct file_operations mpu6050_fops = {
	.open = mpu6050_drv_open,
	.release = mpu6050_drv_close,
	.unlocked_ioctl = mpu6050_drv_ioctl,

};

//匹配成功,进行字符设备驱动那一套流程
int mpu6050_drv_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
	printk("-----%s----\n", __FUNCTION__);

	/*
		申请设备号,实现fops
			创建设备文件
			通过i2c的接口去初始化i2c从设备
	*/

	mpu_dev = kzalloc(sizeof(struct mpu_sensor), GFP_KERNEL);
	
	mpu_dev->client = client;

	mpu_dev->dev_major = register_chrdev(0,"mpu_drv", &mpu6050_fops);

	mpu_dev->cls = class_create(THIS_MODULE, "mpu_cls");

	mpu_dev->dev = device_create(mpu_dev->cls, NULL, MKDEV(mpu_dev->dev_major, 0),
				NULL, "mpu_sensor");
	

	
	return 0;

}


int mpu5060_drv_remove(struct i2c_client *client)
{
	printk("-----%s----\n", __FUNCTION__);
	device_destroy(mpu_dev->cls, MKDEV(mpu_dev->dev_major, 0));
	class_destroy(mpu_dev->cls);
	unregister_chrdev(mpu_dev->dev_major, "mpu_drv");
	kfree(mpu_dev);
	return 0;
}

//用于和设备树匹配的
const struct of_device_id  of_mpu6050_id[] = {
		{
			.compatible = "lch-mpu6050",
		},
		{/*northing to be done*/},

};

//用于传统方法的匹配
const struct i2c_device_id mpu_id_table[] = {
		{"mpu6050_drv", 0x1111},
		{/*northing to be done*/},
};
	
struct i2c_driver mpu6050_drv = {
	.probe = mpu6050_drv_probe,
	.remove = mpu5060_drv_remove,
	.driver = {
		.name = "lch-mpu6050",//随便写,/sys/bus/i2c/driver/mpu6050_drv
		.of_match_table = of_match_ptr(of_mpu6050_id),
	},
	
	.id_table = mpu_id_table,//非设备树情况下的匹配,在设备树的模式下不需要使用
};


static int __init mpu6050_drv_init(void)
{
	// 1,构建i2c driver,并注册到i2c总线
	return i2c_add_driver(&mpu6050_drv);

}

static void __exit mpu6050_drv_exit(void)
{
	i2c_del_driver(&mpu6050_drv);

}

module_init(mpu6050_drv_init);
module_exit(mpu6050_drv_exit);
MODULE_LICENSE("GPL");

app_i2c.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>


#include "mpu6050.h"


int main(int argc, char *argv[])
{
	int fd;
	
	union mpu6050_data data;
	
	fd = open("/dev/mpu_sensor", O_RDWR);
	if(fd < 0)
	{
		perror("open");
		exit(1);
	}

	while(1)
	{
		ioctl(fd, IOC_GET_ACCEL, &data);
		printf("accel data :  x = %d, y=%d, z=%d\n", data.accel.x, data.accel.y, data.accel.z);

	
		ioctl(fd, IOC_GET_GYRO, &data);
		printf("gyro data :  x = %d, y=%d, z=%d\n", data.gyro.x, data.gyro.y, data.gyro.z);

		sleep(1);

	}

	close(fd);
	

	return 0;

}

        把驱动insmod进系统,运行应用就会打印出信息了。

思考:

        总是说“驱动是机制,应用是策略”,但是这次编写i2c设备我觉得驱动里“策略”的代码太多了,如果将read和write造成读写i2c寄存器的接口,那么这样暴露出来的i2c设备节点,就可以是只关注i2c协议的驱动,而与具体是什么i2c接口的外设无关,很多关于mpu6050的代码就可以在应用程序中编写了。而且这个设备节点,也可以给其他i2c接口的外设使用。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值