本文将详细描述本人编写该驱动遇到的问题已经相关调试经验
一、设备树部分的编写
这里有第一个坑,如果在野火开发板自带的固件中编写,需要在终端中将/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,最终找到了问题,驱动也就大功告成。