野火i.MX6ULL Pro开发板mpu6050驱动的调试编写

        本文将详细描述本人编写该驱动遇到的问题已经相关调试经验

一、设备树部分的编写

        这里有第一个坑,如果在野火开发板自带的固件中编写,需要在终端中将/boot/uEnv.txt文件中的一些开机自带的驱动手动关闭,并重启,比如注释掉开机启动的mpu6050驱动,否则驱动挂载出错或不起作用。

         设备树可以直接使用系统自带的,不需要自己改动。上方为mpu6050与芯片连接的引脚配置。下方为i2c1控制器对应的配置,内部有对应的硬件设备,其中68为mpu6050在i2c1总线上的地址,此地址可由mpu6050芯片手册获得。

        为了易于区分,我给设备的compatible属性重新设值。

pinctrl_i2c1: i2c1grp {
    fsl,pins = <
        MX6UL_PAD_UART4_TX_DATA__I2C1_SCL 0x4001b8b0
        MX6UL_PAD_UART4_RX_DATA__I2C1_SDA 0x4001b8b0
    >;
};
/*=====================*/
&i2c1{
	clock-frequency = <100000>;
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_i2c1>;
	status = "okay";
	
	i2c_mpu6050@68 {
	        	/*compatible = "fire,i2c_mpu6050";*/
	        	compatible = "ldysl,i2c_mpu6050_ldysl";
				reg = <0x68>;
	        	status = "okay";
	 };
}; 

 我的板子是mmc的,所以使用的指令如下:

sudo cp /mnt/imx6ull-mmc-npi.dtb /usr/lib/linux-image-4.19.35-imx6/imx6ull-mmc-npi.dtb

修改设备树拷贝到到指定的位置并重启以后。在/sys/bus/devices/0-0068/name是否为你自己修改的compatible值。有的话,说明设备树加载成功。

 二、驱动部分编写

       驱动的编写并不复杂,根据给的例程对照着修改即可。

1、驱动的初始化与注销

        主要用到i2c_add_driver()和i2c_del_driver(),然后围绕这两个函数的参数构建i2c_driver结构体中的几个成员参数。

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

/*驱动注销函数*/
static void __exit mpu6050_driver_exit(void)
{
	pr_info("mpu6050_driver_exit\n");
	i2c_del_driver(&mpu6050_driver);
}

module_init(mpu6050_driver_init);
module_exit(mpu6050_driver_exit);

MODULE_LICENSE("GPL");

2、在i2c_driver结构体中构建probe和remove函数和几个用于匹配的结构体。

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

/*定义设备树匹配表*/
static const struct of_device_id mpu6050_of_match_table[] = {
	{.compatible = "ldysl,i2c_mpu6050_ldysl"},
	{/*预留*/}
};

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

3、在probe函数中注册字符设备并将其与文件操作结构体相关联

static int mpu6050_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
	int ret = -1;
	printk(KERN_EMERG "\t match success !!\n");
	
	//动态分配来获取设备编号, 次设备号为0.可通过cat /proc/devices的方式查看
	//DEV_CNT为1,当前只申请一个设备编号
	ret = alloc_chrdev_region(&mpu6050_devno, 0, DEV_CNT, DEV_NAME);
	if (ret < 0) {
		printk("fail to alloc mpu6050_devno\n");
		goto alloc_err;
	}
	
	//关联字符设备结构体cdev与文件操作结构体
	mpu6050_chr_dev_fops.owner = THIS_MODULE;
	cdev_init(&mpu6050_chr_dev, &mpu6050_chr_dev_fops);

	//将设备添加进cdev_map列表中
	ret = cdev_add(&mpu6050_chr_dev, mpu6050_devno, DEV_CNT);
	if (ret < 0) {
		printk("fail to add cdev\n");
		goto add_err;
	}

	class_mpu6050 = class_create(THIS_MODULE, DEV_NAME);

	device_mpu6050 = device_create(class_mpu6050, NULL, mpu6050_devno, NULL, DEV_NAME);
	mpu6050_client = client;
	return 0;

add_err:
	// 添加设备失败时,需要注销设备号
	unregister_chrdev_region(mpu6050_devno, DEV_CNT);
	printk("\n error! \n");
alloc_err:

	return -1;
}

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;
}

4、构建文件操作结构体

        在mpu6050中主要是构建open和read函数,因为这个设备我们一般只读取数据所有无需构建write。

        野火将mpu6050的初始化放在open函数中,它的初始化就是使用i2c提供的接口来进行寄存器的配置,详细配置参阅手册即可,这里就再叙述。

        read函数也是使用i2c提供的接口获取到相应的参数并组合成一个short型数组,再使用copy_to_user()从内核态映射到用户态,完成用户态数据的获取。

static struct file_operations mpu6050_chr_dev_fops = {
	.owner = THIS_MODULE,
	.open = mpu6050_open,
	.read = mpu6050_read,
	.release = mpu6050_release,
};

static int mpu6050_open(struct inode *inode, struct file *filp)
{
	mpu6050_init();
	return 0;
}

static int mpu6050_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
	char data_H;
	char data_L;
	int error;
	//保存mpu6050转换得到的原始数据
	short mpu6050_result[6] = {0};

	/*读取3轴加速度原始值*/
	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;

	/*读取3轴角速度原始值*/
	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;


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

	/*将读取得到的数据拷贝到用户空间*/
	//error = copy_to_user(buf, mpu6050_result, sizeof(mpu6050_result));
	printk("cnt:%d\n", cnt);
	error = copy_to_user(buf, mpu6050_result, cnt);

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

	return 0;
}

static int mpu6050_release(struct inode *inode, struct file *filp)
{
	printk("\nmpu6050_release \n");
	return 0;
}

static struct file_operations mpu6050_chr_dev_fops = {
	.owner = THIS_MODULE,
	.open = mpu6050_open,
	.read = mpu6050_read,
	.release = mpu6050_release,
};

5、使用i2c接口进行寄存器的读写

        使用i2c进行读写都需要用i2c_transfer()来执行,不同的是需要构建i2c_msg结构体来传入这个函数。

        写寄存器操作时,构建一个i2c_msg结构体,并将需要写入的寄存器地址以及值制作为数组传入i2c_msg.buf中即可。

        读寄存器操作有一些复杂,不过需要构建一个i2c_msg结构体数组。第一个结构数组的i2c_msg[1].buf成员中放入寄存器地址。第二个结构体数组先置i2c_msg[2].flag为I2C_M_RD,并将缓存的指针存入i2c_msg[2].buf即可。

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;
	
	/*发送数据要写入地址reg*/
	send_msg.addr = mpu6050_client->addr;	//mpu6050在i2c总线上的地址
	send_msg.flags = 0;
	send_msg.buf = write_data;				//写入的首地址
	send_msg.len = 2;						//msg长度

	error = i2c_transfer(mpu6050_client->adapter, &send_msg, 1);
	if (error != 1)	{
		printk(KERN_DEBUG "i2c transfer error\n");
		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;
	struct i2c_msg mpu6050_msg[2];

	/*设置读取位置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;						//写入长度

	/*设置读取位置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;
}

三、应用的编写

        应用的编写倒没啥,就是一个open然后read,最后一个close。

需要注意的有两个点:

        第一个,open()函数对应的属性文件是否存在、即是否匹配成功。

        第二个,属性文件的名称是否正确,可能会出现二者对不上导致的open失败。

        第三个,路径是否正确,我一开始调的时候就是有驱动文件但是就是打不开,直到我使用perror发现文件不存在,我去看发现我的路径写成了open("dev/XXX")少了一个‘/’,就很难顶。

int main(int argc, char *argv[])
{
    short resive_data[6];  //保存收到的 mpu6050转换结果数据,依次为 AX(x轴角度), AY, AZ 。GX(x轴加速度), GY ,GZ

    /*打开文件*/
    int fd = open("/dev/I2C1_mpu6050", O_RDWR);
    if(fd < 0)
    {
		printf("open file : %s failed !\n", argv[0]);
        perror("why error:");
		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;
}

四、代码调试 

        ·我觉得敲代码最重要的就是知道如何调试,调试的方法可以让我们更快的锁定问题,解决问题。

1、probe匹配。

        匹配应该没啥问题,只要名称一样就能匹配上,如果匹配失败对照着检查一下代码。

        之前失败是因为我使用cdev_add()函数,将字符设备结构体struct cdev和设备号传入其中时,手抖了,传设备号,传入了指针即&devno,导致设备probe成功,但是应用层获取不到那个设备。

        匹配成功后,在/sys/bus/i2c/drivers/下看到相关设备

2、调用app读取数据

最开始调试时,我不知道的open()路径有误,导致我的觉得我驱动写的有问题,

 但我调试发现路径下有,我直接cat确实报了错。仔细看会发现是copy_to_user的问题,因为我是直接cat所以读取数据到用户态的这个长度出问题导致的出错。只要屏蔽这个,我直接cat就没问题。后面反过来看app,最终找到了问题,驱动也就大功告成。

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值