SH3001六轴传感器应用(二)(IIC驱动开发)

一、前言

我这边使用的开发板原本已经做好了该sensor的驱动,但是使用过程中发现,原始驱动sensor是通过事件的方式上报的,加速度和陀螺仪数据并不同步,不满足使用要求,只有重新写一个iic的驱动,进行sensor数据的读取了。

二、驱动编写

1、linuxIIC驱动属于字符设备的驱动,所以使用字符设备的固定流程进行注册就行了。首先第一步肯定是设备数的编写如下,这里肯定是对应硬件接口IIC子节点下,进行编写,这里的核心是"compatible "属性,这个一定要和驱动文件的compatible 保持一致,不然无法找到设备节点。

XML 复制代码
&i2c4 {
	status = "okay";
	pinctrl-0 = <&i2c4m1_xfer>;

	sh3001_acc@36 {
		compatible = "sh3001";
		reg = <0x36>;
		type = <SENSOR_TYPE_ACCEL>;
		pinctrl-names = "default";
		pinctrl-0 = <&sh3001_irq_gpio>;
		irq-gpio = <&gpio2 RK_PC4 IRQ_TYPE_LEVEL_HIGH>;
		irq_enable = <0>;
		poll_delay_ms = <30>;
		layout = <2>;
		status = "okay";
	};
}

2、接下来就是设备的init 和exit了,init用于开始进行设备的匹配,exit用于在驱动设备卸载时执行。

cpp 复制代码
static int __init sh3001_init(void)
{
    int ret = 0;

    ret = i2c_add_driver(&sh3001_driver);
    return ret;
}

static void __exit sh3001_exit(void)
{
    i2c_del_driver(&sh3001_driver);
}

module_init(sh3001_init);
module_exit(sh3001_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("TRACY");

3、在执行module_init(sh3001_init)的时候,会调用sh3001_driver结构体,这里将会将会进行设备的匹配,sh3001_of_match的结构体数组就是去匹配设备数相关节点的。

cpp 复制代码
/* 传统匹配方式ID列表 */
static const struct i2c_device_id ap3216c_id[] = {
    {"sh3001", 0},  
    {}
};

/*设备数匹配列表*/
static const struct of_device_id sh3001_of_match[] = {
    {.compatible = "sh3001"},
    {/*Sentinel*/}
};


/*sh3001驱动结构体 */
static struct i2c_driver sh3001_driver = {
    .probe = sh3001_probe,
    .remove =sh3001_remove,
    .driver = {
        .owner = THIS_MODULE,
        .name = "sh3001",
        .of_match_table = sh3001_of_match
    },
    .id_table = ap3216c_id

};

这里在说明一下.id_table = ap3216c_id 和设备树 of_match_table 之间的关系。

.id_table 是用于 传统非设备树匹配(非-DT)平台 上的 I2C 设备名称匹配。它提供了一个 字符串设备名数组(如 "sh3001"),供内核在 手动注册 i2c_client 的平台 中与驱动进行匹配(例如老的板文件、用户空间调用 i2c_new_device())。

和设备树 of_match_table 的区别?

匹配方式 使用场景 依据 需要的字段
.id_table 非设备树平台 i2c_client->name .id_table = ...
.of_match_table 设备树平台(Device Tree) compatible .of_match_table = ...

也就是说:

  • 设备树驱动匹配走的是 of_match_table(即你设备树中写了 compatible = "xxx"

  • 传统方式(不使用设备树)走的是 id_table

  • 内核 会将匹配的 of_device_id 转换为 i2c_device_id(如果 .id_table 存在)传给 probe 的 id 参数。

  • 你用的是设备树,如果 id_table 不写或者为 NULL,内核也可以匹配,但有些框架依赖 id_table 比如 i2c_register_driver() 的自动注册机制,或者 module alias 功能就会缺失。

cpp 复制代码
static const struct i2c_device_id ap3216c_id[] = {
    { "ap3216c", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, ap3216c_id);  // 重要!

static const struct of_device_id ap3216c_of_match[] = {
    { .compatible = "liteon,ap3216c" },
    { }
};
MODULE_DEVICE_TABLE(of, ap3216c_of_match);

这样你的驱动即支持设备树匹配,又支持非设备树注册,同时也能被 modprobeudev 正确识别。

4、设备数成功匹配后,就开始probe()函数的调用,首先得定义定义一个标准的IIC设备的结构体。

接下也算是固定流程:

分配结构体内存---->注册字符设备、创建设备号----->初始化字符设备----->添加字符设备----->创建类----->创建设备。

这里需要注意的就是需要确定这些创建、添加是否成功。

cpp 复制代码
struct sh3001_dev{
    struct i2c_client * client;   /*i2c 设备*/
    dev_t   devid;                  /*设备号 */
    struct cdev  cdev;              /*字符设备核心*/
    struct class * class;            /*类*/
    struct device * device;         /*设备*/
    struct device_node * nd;       /*设备节点*/

};


 /*
  * @description 	: i2c驱动的probe函数,当驱动与
  *                    设备匹配以后此函数就会执行
  * @param -- client	: i2c设备
  * @param - id      	: i2c设备ID
  * @return          	: 0,成功;其他负值,失败
  */
static int sh3001_probe(struct i2c_client * client,const struct i2c_device_id *id)
{
    int ret;
    struct sh3001_dev * sh3001_cdev;
    /*分配结构体内存*/
    sh3001_cdev=devm_kzalloc(&client->dev,sizeof(*sh3001_cdev),GFP_KERNEL);

    if(!sh3001_cdev)
    {
        return -ENOMEM;
    }
    
    /*注册字符设备驱动*/
    /*创建设备号*/
    ret = alloc_chrdev_region(&sh3001_cdev->devid,0,SH3001_CNT,SH3001_NAME);
    if(ret<0)
    {
        pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n", SH3001_NAME, ret);
        return -ENOMEM;
    }
    else
    {
        printk("alloc chrdev sucess\n");
    }

    /*初始化cdev*/
    sh3001_cdev->cdev.owner = THIS_MODULE;
    cdev_init(&sh3001_cdev->cdev,&sh3001_ops);

    /*添加一个cdev*/
    ret = cdev_add(&sh3001_cdev->cdev,sh3001_cdev->devid,SH3001_CNT);
    if(ret < 0)
    {
        goto del_unregister;
    }
    else
    {
         printk("cdev add sucess cdev\n");
    }

    /*创建类 */
    sh3001_cdev->class = class_create(THIS_MODULE,SH3001_NAME);
    if(IS_ERR(sh3001_cdev->class))
        goto del_cdev;
    printk("create class sucess\n");
    /*创建设备*/
    sh3001_cdev->device = device_create(sh3001_cdev->class,NULL,sh3001_cdev->devid,NULL,SH3001_NAME);
    if(IS_ERR(sh3001_cdev->device))
        goto destroy_class;
    
    
    sh3001_cdev->client=client;
    i2c_set_clientdata(client,sh3001_cdev);

    printk("create device sucess:%p  iic_client:%p\n",(void *)&sh3001_cdev->cdev,(void *)sh3001_cdev->client);

    return 0;
    del_cdev:
        cdev_del(&sh3001_cdev->cdev);
    destroy_class:
        device_destroy(sh3001_cdev->class,sh3001_cdev->devid);
    del_unregister:
        unregister_chrdev_region(sh3001_cdev->devid, SH3001_CNT);
        return -EIO;
   
}

5、上面probe()在初始化字符设备时是映射了应用的接口的。read()write()的方式比较常见,这里我们使用ioctl去进行寄存器的控制读取。

cpp 复制代码
 static const struct file_operations sh3001_ops = {
    .owner = THIS_MODULE,
    .open = sh3001_open,
    .unlocked_ioctl = sh3001_ioctl,
	.compat_ioctl = sh3001_ioctl,
    .release = sh3001_release
};
 cdev_init(&sh3001_cdev->cdev,&sh3001_ops);

使用ioctl需要注意的就是命令的注册

cpp 复制代码
#define  SH3001_IOCTL_WRITE    _IOW('s',0x01,struct sh3001_ioctl_data)
#define  SH3001_IOCTL_READ     _IOR('s',0x02,struct sh3001_ioctl_data)
#define  SH3001_IOCTL_START    _IOR('s',0x03,struct sh3001_ioctl_data)

这些宏是 Linux 提供的,用来创建 ioctl 命令编号。其目的是:

唯一标识一个 ioctl 命令,并且告诉内核和驱动:

  • 命令属于哪个设备

  • 命令编号是多少

  • 用户是否传递了参数

  • 参数的方向(从用户传给内核,还是从内核返回给用户)

  • 参数的数据结构是什么

  • 并且这个命令在应用层也有一样使用调用,不然应用层无法传递到对应ioctl的接口。

接下来就是ioctl代码的实现,其核心就是通过文件指针找到iic的驱动地址。

cpp 复制代码
static long sh3001_ioctl (struct file * filp, unsigned int cmd, unsigned long arg_addr)
{

	//printk("enter ioctl\n");
	#if 1
    struct sh3001_ioctl_data data;
	int i;
	short acc_x,acc_y,acc_z,gyr_x,gyr_y,gyr_z;
	
    /* 从file结构体获取cdev指针,再根据cdev获取sh3001_dev首地址 */
    struct cdev * cdev = filp->f_path.dentry->d_inode->i_cdev;
    struct sh3001_dev * sh3001cdev=container_of(cdev,struct sh3001_dev,cdev);
   
	pr_info("sh3001_ioctl called with cmd=0x%x\n", cmd);
    /*返回 0 表示所有 n 字节数据成功拷贝
        返回 >0 的正数 表示拷贝失败的字节数(即实际成功拷贝了 n - ret 字节)*/
    if(copy_from_user(&data,(void __user *)arg_addr,sizeof(data)))
    {
        dev_err(sh3001cdev->device, "copy user data error\n");
        return -EFAULT;
    }
    if(!(sh3001cdev->client))
    {
        dev_err(sh3001cdev->device, "no iic client\n");
        return -ENODEV;
    }

    switch(cmd)
    {
        case SH3001_IOCTL_WRITE:

            break;
        case SH3001_IOCTL_READ:
            sh3001_read_regs(sh3001cdev->client,data.reg,12,data.val);
			printk("ioctl reg:%d  data:\n",data.reg);
			for(i=0;i<6;i++)
			{
				if(i%2==0)
					printk(" ");
				printk("%02x%02x",data.val[2*i+1],data.val[2*i]);
				
			}
            if(copy_to_user((void __user *)arg_addr,&data,sizeof(data)))
            {
                return -EFAULT;
            }
            
            break;
		case SH3001_IOCTL_START:
			for(i=0;i<200;i++)
			{
				sh3001_read_regs(sh3001cdev->client,data.reg,12,data.val);

				acc_x += ((data.val[1] << 8) & 0xFF00) + (data.val[0] & 0xFF);
				acc_y += ((data.val[3] << 8) & 0xFF00) + (data.val[2] & 0xFF);
				acc_z += ((data.val[5] << 8) & 0xFF00) + (data.val[4] & 0xFF);

				gyr_x += ((data.val[1] << 8) & 0xFF00) + (data.val[0] & 0xFF);
				gyr_y += ((data.val[3] << 8) & 0xFF00) + (data.val[2] & 0xFF);
				gyr_z += ((data.val[5] << 8) & 0xFF00) + (data.val[4] & 0xFF);

				msleep(5);
			}

			acc_x = acc_x/200;
			acc_y = acc_y/200;
			acc_z = acc_z/200;
			gyr_x = gyr_x/200;
			gyr_y = gyr_y/200;
			gyr_z = gyr_z/200;

			memcpy(&data.val[0],&acc_x,2);
			memcpy(&data.val[2],&acc_y,2);
			memcpy(&data.val[4],&acc_z,2);
			memcpy(&data.val[6],&gyr_x,2);
			memcpy(&data.val[8],&gyr_y,2);
			memcpy(&data.val[10],&gyr_z,2);

			if(copy_to_user((void __user *)arg_addr,&data,sizeof(data)))
            {
                return -EFAULT;
            }
            
            break;
        default:break;
    }

	#endif
    return 0;
}

三、编译

1、接下来就是将驱动文件.c编译成ko模块,makefile如下:

bash 复制代码
obj-m += sh3001_driver.o

KDIR := /home/tracy/rockchip/linux_sdk/kernel  # kernel source directory
PWD := $(shell pwd)    #current directory

all:
	make -C $(KDIR) M=$(PWD) ARCH=arm64 CROSS_COMPILE=aarch64-buildroot-linux-gnu- modules

clean:
	make -C $(KDIR) M=$(PWD) clean

2、将编译好的驱动文件加载进驱动中可以使用insmod和modprobe命令,推荐使用modprobe命令,insmod 会自动处理依赖和参数传递。

rmmod移出驱动命令。

lsmod列出加载驱动命令。

四、调试技巧

在调试过程可以使用示波器查看接口是否有波形产生,多使用printk()打印调试日志。使用dmesg命令进行查看。