22.2Linux的I2C驱动实验(编程)_csdn
我尽量讲的更详细,为了关注我的粉丝!!!
这里我们用到的是stm32mp157的板子,所以我们看一下I2C用到的引脚。
1、硬件原理图分析
可以看到在这块板子上面用的SDA和SCL总线是PA11,PA12。所以要修改设备树和镜像文件!
2、修改镜像
打开 stm32mp15-pinctrl.dtsi文件。
镜像这里不用改!
3、修改设备树
打开 stm32mp157d-atk.dts 文件。
通过节点内容追加的方式,向 i2c5 节点中添加“ap3216c@1e”子节点。
原本在内核源码 arch/arm/boot/dts/stm32mp151.dtsi 设备树文件中就可以找到STM32MP1 的 I2C5控制器节点,但是不够,因为在实验过程中有不同的从设备,所以要在其他地方添加从设备的地址信息。我们在 stm32mp157d-atk.dts 文件添加上面的节点信息!
编译:
make dtbs
复制到开发板中:
sudo cp arch/arm/boot/dts/stm32mp157d-atk.dtb /home/chensir/linux/tftpboot/ -f
“0-001e”就是 ap3216c 的设备目录。
1e”就是 ap3216c 器件地址。进入0-001e 目录,可以看到“name”文件, name 文件保存着此设备名字,在这里就是“ap3216c”。
这里就可以说明系统扫描到了这个从设备,并将它配置在devices目录下。
4、AP3216C 驱动编写
之前的博客也是跟大家按照肌肉记忆来编写程序!一步一步按照思路来编写!
总代码会放在最后。
为了让大家更能明白,可以先对着总代码,进行对我的写代码流程更加详细得当!
先在 ap3216creg.h 中定义好 AP3216C 的寄存器,ap3216creg.h 没什么好讲的,就是一些寄存器宏定义。
输入如下内容:
这个不就跟裸机代码keil一样啦!所以这里没什么好讲的!
接下来就是讲编写驱动代码了!
4.1、头文件
可以看到下面的头文件比以前的多了i2c.h和ap3216creg.h。
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/i2c.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include "ap3216creg.h"
4.2、驱动注册和注销
对于 I2C 驱动而言,没有像 misc
子系统、input子系统那样直接提供完全自动创建设备号、cdev
、device
、class
的机制。
但是我们可以像之前的文章中module_platform_driver类似写module_i2c_driver来实现init和exit一体化。也就是不需要自己编写init和exit了。
这里的ap3216c_driver是定义为i2c_driver的结构体,类似platform_driver。
举例:(这里并不要我们写!)
把i2c_add_driver和i2c_del_driver都封装了!
4.2.1、编写I2C_driver驱动结构体
其中流程是:
27行代码:设备树中的compatible值会与ap3216c_of_match下的compatible相匹配。
如下图ap3216c_of_match的代码:
其中MODULE_DEVICE_TABLE是声明一下而已!
26行代码:会在driver目录下生成ap3216c。这个是驱动开发者自己编写的!
和设备树中的compatible没有关系。
25行代码:一般将其设置为 THIS_MODULE
,这表明该驱动属于当前的内核模块。
29行代码:id_table
是一个设备 ID 表,用于传统的设备匹配方式(不使用设备树时)。ap3216c_id
是一个预先定义好的 ID 表。
22~23行代码:compatible值一旦匹配成功,就会执行probe和remove。(这些其实之前的驱动程序讲解已经讲过很多遍了)
4.2.2、编写probe和remove函数
这里就用于之前的设备注册和注销,它并不能像MISC和INPUT子系统一样自动注册和注销设备号、cdev、class、device。
这里可以很明显发现和platform驱动不同;
static int miscbeep_probe(struct platform_device *pdev)static int miscbeep_remove(struct platform_device *dev)
但是道理都一样,传递进去的数据是设备,区别就是数据分别是I2C设备和platform设备。
4.2.3、注册和注销字符驱动设备
在这之前需要定义相关的设备结构体,为了统一用到的数据。
1、定义设备结构体
这里我们不用:
struct ap3216c_dev ap3216cdev;
因为如果有多个从设备的话,难以处理多个设备实例的情况;会增加代码复杂度。
用上面这个传递会使系统会在栈上创建该结构体的一个副本。这样的拷贝操作会带来额外的性能开销。
==当然如果驱动代码全文没有用struct ap3216c_dev ap3216cdev这种形式的,就一定要为它分配内存!==这种形式已经是全局的 struct ap3216c_dev
结构体实例,指针 dev
直接指向它,无需额外分配内存。
后面可以通过:
例如:
struct ap3216c_dev *dev
通过指针变量来修改设备结构体的子类内容。传递指针(即结构体的地址),只需要传递一个固定大小(通常是 4 字节或 8 字节,取决于系统的架构)的地址值,避免了大量数据的拷贝,提高了程序的运行效率。
这里对于结构体指针不熟练的同学尤为重要的是:
例如struct ap3216c_dev *dev=&my_device;
其中dev存储的是my_device变量的地址,当使用printf展示dev和&my_device时地址是一样的,但是&dev是存储的dev变量自身的地址!
分配内存:
2、申请设备号
同理注销设备号:
3、配置cdev
配置设备结构体:
初始化cdev:
这些都是之前的文章讲遍了的!
THIS_MODULE:让dev在本模块执行。(cdev本身就是执行到dev)
&ap3216c_ops:字符操作集,与用户空间信息交互。
举例:(这里了解一下即可)
对字符操作集进行操作:
添加cdev:
同理注销cdev:
4、配置class和device
配置相关设备结构体:
创建class和device:
这里之前也讲过:作用就是在dev目录下生成ap3216c。
同理注销class和device:
之前的代码。比如按键,led都需要经过获取设备节点、设备树下GPIO的相关属性然后配置GPIO,I2C协议的SCL\SDA总线都需要连接GPIO,为什么驱动I2C就不用获取设备节点?
虽然 I2C 的 SCL 和 SDA 线在物理上连接到 GPIO 引脚,但由于 I2C 控制器和内核 I2C 子系统的存在,使得 I2C 驱动开发中不需要像操作普通 GPIO 那样去获取和配置其属性,而是通过专门的 I2C 接口函数来实现通信功能。
- I2C 控制器的功能:在大多数芯片中,I2C 功能是由专门的 I2C 控制器来实现的。I2C 控制器负责产生时钟信号、控制数据的传输、处理数据的起始和停止条件等复杂的操作。它会将 SCL 和 SDA 引脚配置为特定的 I2C 功能模式,而不是简单地作为普通 GPIO 使用。例如,当 I2C 控制器初始化时,它会自动将对应的 GPIO 引脚设置为开漏输出模式(这是 I2C 通信所要求的电气特性),并配置内部的时钟发生器和数据移位寄存器等硬件模块,以实现 I2C 协议规定的通信功能。
- 内核 I2C 子系统的封装:Linux 内核的 I2C 子系统对底层的 I2C 控制器进行了抽象和封装。驱动开发者通过 I2C 子系统提供的接口函数来与 I2C 设备进行通信,而无需关心底层 SCL 和 SDA 引脚的具体配置细节。这些接口函数会在内部调用硬件相关的底层驱动代码,来完成对 I2C 控制器的操作,进而实现对 SCL 和 SDA 引脚的控制。例如,驱动开发者使用
i2c_transfer
函数来发送和接收 I2C 数据帧时,I2C 子系统会根据具体的硬件平台和 I2C 控制器的特性,自动配置 SCL 和 SDA 引脚的电平变化、数据采样时机等,以确保数据的正确传输。
按键和 LED 驱动
- 内核里有专门的 GPIO 子系统,提供了一系列函数用于配置和操作 GPIO 引脚。按键和 LED 驱动通常会利用这些函数来获取设备树里 GPIO 的属性,然后配置相应的 GPIO 引脚。例如,使用
of_get_named_gpio
函数从设备树中获取 GPIO 编号,再使用gpio_direction_input
或gpio_direction_output
函数配置引脚方向。
I2C 驱动
- 内核有独立的 I2C 子系统,该子系统对 I2C 通信的底层细节进行了封装。I2C 驱动主要关注如何与 I2C 设备进行通信,也就是如何发送和接收 I2C 数据帧。通过 I2C 子系统提供的函数,如
i2c_transfer
,可以方便地实现与 I2C 设备的通信,而无需直接操作 GPIO 引脚。
这样就不用在驱动代码里面写获取设备节点了!除非遇到要开启中断的就要了!
这里是没有用到中断和DMA!
4.3、配置字符操作集
在完成写完字符操作集之前,我们先来编写I2C协议,发送和接收单个字节或多个字节,并且进阶完成ap3216c中3个传感器数据的收集!
因为我们有3个传感器的收集,所以我们要对一个从设备的3个寄存器进行读取和发送数据!
4.4、从ap3216c读取多个寄存器数据
因为之前说了系统扫描一个从设备就会有一个client。client很重要!里面有从设备的相关数据,所以要在设备结构体中配置!其中client也有adapter适配器!可以看到上面的代码也有client。
配置结构体:
写读取寄存器数据代码:
图片中的代码读取的数据是那个val。这个数据是由从设备那边提供的!
其中在 struct ap3216c_dev 中定义 struct i2c_client *client;是为了关联设备与 i2c_client 结构体。而在函数中重新定义 struct i2c_client *client
`一般是作为局部变量,用于在函数作用域内方便地操作 i2c_client 相关功能,避免直接操作结构体成员带来的混淆,同时也便于参数传递、错误处理和临时存储等,增强代码的可读性与可维护性。
4.5、向 ap3216c 多个寄存器写入数据
4.6、读取 ap3216c 指定寄存器值,读取一个寄存器
4.7、向 ap3216c 指定寄存器写入指定的值,写一个寄存器
可以看到上面已经把读取数据和写入数据已经封装好了!
5、编写AP3216C的三路合一传感器数据
我们这里只编写读取数据,观察传感器数据!
我们先配置相关的设备结构体:
5.1、读取 AP3216C 的数据,包括 ALS,PS 和 IR
注意!如果同时打开 ALS,IR+PS 两次数据读取的时间间隔要大于 112.5ms!
因为这里我们要读取6个数据。如上一个文章所说:
这些都是AP3216C上3个寄存器的不同地址。
在之前ap3216creg.h编写时就可以发现:
地址是+1的,所以可以利用数组来读取从设备发过来的数据,读取6个数据!
这样就可以循环读取6次了!目前读取的数据在数组buf内!
根据上面的条件发现,传递过来的数据可能是无效的,所以要加判断!
我们可以一一分析:
1、IR数据低8位中的第7位为1时则无效。其中IR&PS 数据有效的意思是当第7位为0时,则IR和PS传感器都是有效的。而图片中并未提到IR寄存器中低8位的2-6位,显然2-6位都是不重要的,0X0A
寄存器里只有第 1 - 0 位存储的是 IR
的有效数据。所以后面编程时要把无关数据置0。
2、ALS传感器都是有效的,只要将数据进行合并即可。
3、PS数据低8位第7位为1时,物体接近,为0时,物体远离;第6位为1时,PS和IR数据都无效,反之有效;其中PS寄存器低8位里面有效数据就是第0-3位存储的数据。PS数据高8位第7位为1时,物体接近,为0时,物体远离;第6位为1时,PS和IR数据都无效,反之有效;其中PS寄存器低8位里面有效数据就是第0-5位存储的数据。所以后面编程时要把无关数据置0。
原理讲了这么多,现在我们就开始写代码!
代码的解释都在上面写了噢!
6、编写字符操作集
我们现在其实已经完成了关于I2C的读取和写入!但是为了可以通过执行用户程序得到传感器数据,所以得编写字符操作集,完成传感器数据的信息交互。
6.1、编写open函数
驱动开发者通常会将设备的私有数据封装在自定义的设备结构体中,而 struct cdev
是内核可见的公共部分。通过 cdev
来间接访问设备结构体,可以避免在通用的字符设备操作流程中直接暴露设备的私有数据,提高了代码的安全性和可维护性。
这种方式使得驱动的各个部分可以更好地进行模块化设计。例如,内核的字符设备管理模块只需要处理 cdev
相关的操作==,而驱动开发者可以在自定义设备结构体中添加任意的私有数据和状态,两者之间通过 cdev
进行关联,互不干扰==。
在设备驱动的生命周期中,设备结构体可能是动态分配和释放的。内核只需要管理 cdev
的注册和注销,而具体的设备结构体可以由驱动自行管理。通过 cdev
来关联设备结构体,使得设备的动态管理更加灵活。
这里我们希望用户执行用户程序,一执行就初始化ap3216c:
6.2、编写read函数
这里就要将3个传感器数据读取了!
6.2、编写release函数
最后我们会发现我们并没有给client指明结构体实例:
1、在 struct ap3216c_dev
结构体中定义 struct i2c_client *client
只是声明了一个指针类型的成员变量,它的作用是用来保存指向 i2c_client
结构体的指针。不过,这个指针在初始时是未被赋值的(除非有显式初始化),它不指向任何有效的 i2c_client
结构体实例。
2、在 ap3216c_probe
函数里,struct i2c_client *client
作为参数传入,这个 client
指针指向一个有效的 i2c_client
结构体实例,该实例代表了当前连接的 I2C 设备。通过 ap3216cdev->client = client;
这行代码,把传入的 client
指针赋值给 ap3216c_dev
结构体中的 client
成员,这样 ap3216c_dev
结构体就能够保存当前 I2C 设备的相关信息。
缺失赋值语句带来的影响:
如果不进行 ap3216cdev->client = client;
这个赋值操作,ap3216c_dev
结构体中的 client
成员依旧是未初始化的,也就是它可能指向一个无效的内存地址,或者是一个随机值。在后续的驱动函数(如 ap3216c_read_regs
、ap3216c_write_regs
等)里,当尝试通过 ap3216cdev->client
去访问 i2c_client
结构体的成员时,就会出现问题。
也就是:
实际就是关联起来!
当然也可以不在驱动代码里面的设备结构体写client,直接引用i2c_client是另一种可行的方案,但是后果是:
- 若不把 i2c_client
指针保存到 ap3216c_dev
结构体中,那么在每个需要使用 i2c_client
的函数里都得显式传递该指针。这样会让函数的参数列表变长,代码变得冗余。例如,在多个操作函数中都要传递 i2c_client
指针,当驱动代码规模增大时,维护和修改都会变得困难。
所以我们在probe添加赋值操作:
同样我们要在remove编写(这里不是解除关联):
从 i2c_client
结构体中获取之前存储的 ap3216cdev
指针。这样我们就可以通过 ap3216cdev
访问到设备的各种信息,从而正确地释放资源。
补充错误类型:
驱动代码这里就全部讲完了!
下面我们开始写APP.c代码!
7、编写测试 APP
这里其实也简单,就是传递3个数据!
核心代码:
8、效果
正点原子的STM32MP157的板子,ap3216c芯片的位置并没有设置丝印,所以并不好找!
如下图:
当放手指上去时:
当放手电筒时:
9、总代码:
ap3216c.c:
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/i2c.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include "ap3216creg.h"#define AP3216C_CNT 1
#define AP3216C_NAME "ap3216c"/*ap3216c设备结构体*/
struct ap3216c_dev{dev_t devid;int major;int minor;struct cdev cdev;struct class *class; /* 类 */struct device *device; /* 设备 */struct i2c_client *client;//描述设备unsigned short ir, als, ps; /* 16位,三个光传感器数据 */
};/*从ap3216c读取多个寄存器数据*/
static int ap3216c_read_regs(struct ap3216c_dev *dev,u8 reg,void *val, int len)
{int ret;struct i2c_msg msg[2];//真正传输的数据// i2c_client 用于描述 I2C 总线下的设备/*在 struct ap3216c_dev 中定义 struct i2c_client *client; 是为了关联设备与 i2c_client 结构体。而在函数中重新定义 struct i2c_client *client 一般是作为局部变量,用于在函数作用域内方便地操作 i2c_client 相关功能,避免直接操作结构体成员带来的混淆,同时也便于参数传递、错误处理和临时存储等,增强代码的可读性与可维护性。*/struct i2c_client *client = (struct i2c_client *)dev->client;/*这里意味着从设备的地址始终在 i2c_client结构体的addr子类中所以还是要访问 i2c_client结构体*//* msg[0]为发送要读取的首地址 */msg[0].addr = client->addr; /* ap3216c 地址,一般为7位地址 */msg[0].flags = 0; /* 标记为发送数据 ,其实就是0*/msg[0].buf = ® /* 读取的首地址 */msg[0].len = 1; /* reg 长度 *//* msg[1],第二条读消息,读取寄存器数据 */msg[1].addr = client->addr; /* I2C 器件地址,跟上面的地址一样,一个client一个从设备*/msg[1].flags = I2C_M_RD; /* 标记为读取数据,其实就是1*/msg[1].buf = val; /* 读取数据缓冲区 */msg[1].len = len; /* 要读取的数据长度 *//*上面的就是驱动代码读数据的时序:先发送然后读取*/ret = i2c_transfer(client->adapter, msg, 2);//发送这两条数据if(ret == 2) {ret = 0;} else {printk("i2c rd failed=%d reg=%06x len=%d\n",ret, reg, len);ret = -EREMOTEIO;}return ret;
}/*向ap3216c多个寄存器写入数据*/
static s32 ap3216c_write_regs(struct ap3216c_dev *dev, u8 reg,u8 *buf, u8 len)
{u8 b[256];struct i2c_msg msg;//真正传输的数据/*在 struct ap3216c_dev 中定义 struct i2c_client *client; 是为了关联设备与 i2c_client 结构体。而在函数中重新定义 struct i2c_client *client 一般是作为局部变量,用于在函数作用域内方便地操作 i2c_client 相关功能,避免直接操作结构体成员带来的混淆,同时也便于参数传递、错误处理和临时存储等,增强代码的可读性与可维护性。*/struct i2c_client *client = (struct i2c_client *)dev->client;// i2c_client 用于描述 I2C 总线下的设备b[0] = reg; /* 寄存器首地址 ,将寄存器先赋值给b数组*/memcpy(&b[1],buf,len); /*将要写入的数据拷贝到数组b里面,从位置1开始赋值,位置0已经给寄存器了*/msg.addr = client->addr; /*ap3216c地址,一般为7个地址*/msg.flags = 0; /*标记为写数据,其实就是0*/msg.buf = b; /* 要写入的数据缓冲区 */msg.len = len + 1; /*要写入的数据长度,这里加1是因为前面多加了个reg*/return i2c_transfer(client->adapter,&msg,1);//传输的数据
}/*读取ap3216c指定寄存器值读取一个寄存器*/
static unsigned char ap3216c_read_reg(struct ap3216c_dev *dev,u8 reg)
{u8 data = 0;ap3216c_read_regs(dev, reg, &data, 1);return data;
}/*向ap3216c指定寄存器写入指定的值写一个寄存器*/
static void ap3216c_write_reg(struct ap3216c_dev *dev, u8 reg,u8 data)
{u8 buf = 0;buf = data;ap3216c_write_regs(dev, reg, &buf, 1);
}/*读取 AP3216C 的数据,包括 ALS,PS 和 IR, 注意!如果同时
打开 ALS,IR+PS 两次数据读取的时间间隔要大于 112.5ms*/
void ap3216c_readdata(struct ap3216c_dev *dev)
{unsigned char i =0;unsigned char buf[6];//8位/* 循环读取所有传感器数据 */for(i = 0; i < 6; i++) {buf[i] = ap3216c_read_reg(dev, AP3216C_IRDATALOW + i);}/*判断IR寄存器中低8位的第7位是否有效,0:有效;1:无效*/if(buf[0] & 0X80) {/* IR_OF 位为 1,则数据无效 */dev->ir = 0;}else {/* 读取 IR 传感器的数据 *//*只要IR的高8位数据和低8位中的第0-1位数据,无效数据都置0,如低8位的2-7位数据*/dev->ir = ((unsigned short)buf[1] << 2) | (buf[0] & 0X03);}/*ALS寄存器高8位与低8位合并*/dev->als = ((unsigned short)buf[3] << 8) | buf[2];/*判断PS寄存器中低8位的第6位是否有效,0:有效;1:无效*/if(buf[4] & 0x40) {/* IR_OF 位为 1,则数据无效 */dev->ps = 0;}else{/* 读取 PS 传感器的数据 *//*其中也要判断PS寄存器中高8位的第6位是否有效,0:有效;1:无效*//**只要高8位的5:0位和低8位的3:0为数据合并,无效数据都置0*/dev->ps = ((unsigned short)(buf[5] & 0X3F) << 4) | (buf[4] &0X0F);}
}static int ap3216c_open(struct inode *inode, struct file *filp)
{/* 从 file 结构体获取 cdev 指针, 再根据 cdev 获取 ap3216c_dev 首地址 *//**open 函数中,通过 filp->f_path.dentry->d_inode->i_cdev 获取到字符设备的 struct cdev 指针*通过Linux 内核采用分层架构设计,各个子系统和模块之间需要保持一定的独立性和抽象性在字符设备操作函数(如 open、read、write 等)中,内核传递给驱动的参数是通用的,比如 struct inode 和 struct file 指针。这些参数中只包含了与文件系统和字符设备通用管理相关的信息,并不直接包含驱动自定义设备结构体的指针。内核通过 inode->i_cdev 提供了一个统一的接口,让驱动可以获取到字符设备的核心管理结构,然后再由驱动自己去关联到具体的设备数据*内核是分层架构,用 cdev 统一管理字符设备,操作函数参数通用,只能通过 cdev 关联具体设备数据。*/struct cdev *cdev = filp->f_path.dentry->d_inode->i_cdev;/*通过cdev获取ap3216c设备结构体的首地址,保证内核操作的独立性*/struct ap3216c_dev *ap3216cdev = container_of(cdev,struct ap3216c_dev, cdev);/*初始化ap3216c*//*配置寄存器 软复位*/ap3216c_write_reg(ap3216cdev, AP3216C_SYSTEMCONG, 0x04);mdelay(50);//延时50ms/*配置寄存器 使能PS+ALS+IR*/ap3216c_write_reg(ap3216cdev, AP3216C_SYSTEMCONG, 0X03);return 0;
}static ssize_t ap3216c_read(struct file *filp, char __user *buf,size_t cnt, loff_t *off)
{short data[3];//3个16位long err = 0;//用于存储错误码,初始化为 0 表示无错误/* 从 file 结构体获取 cdev 指针, 再根据 cdev 获取 ap3216c_dev 首地址 *//**open 函数中,通过 filp->f_path.dentry->d_inode->i_cdev 获取到字符设备的 struct cdev 指针*通过Linux 内核采用分层架构设计,各个子系统和模块之间需要保持一定的独立性和抽象性在字符设备操作函数(如 open、read、write 等)中,内核传递给驱动的参数是通用的,比如 struct inode 和 struct file 指针。这些参数中只包含了与文件系统和字符设备通用管理相关的信息,并不直接包含驱动自定义设备结构体的指针。内核通过 inode->i_cdev 提供了一个统一的接口,让驱动可以获取到字符设备的核心管理结构,然后再由驱动自己去关联到具体的设备数据*内核是分层架构,用 cdev 统一管理字符设备,操作函数参数通用,只能通过 cdev 关联具体设备数据。*/struct cdev *cdev = filp->f_path.dentry->d_inode->i_cdev;/*通过cdev获取ap3216c设备结构体的首地址,保证内核操作的独立性*/struct ap3216c_dev *dev = container_of(cdev,struct ap3216c_dev, cdev);ap3216c_readdata(dev);data[0] = dev->ir;data[1] = dev->als;data[2] = dev->ps;err = copy_to_user(buf, data, sizeof(data));return 0;
}static int ap3216c_release(struct inode *inode, struct file *filp)
{struct cdev *cdev = filp->f_path.dentry->d_inode->i_cdev;/*通过cdev获取ap3216c设备结构体的首地址,保证内核操作的独立性*/struct ap3216c_dev *ap3216cdev = container_of(cdev,struct ap3216c_dev, cdev);/*配置寄存器 掉电*/ap3216c_write_reg(ap3216cdev, AP3216C_SYSTEMCONG, 0x00);return 0;
}/* AP3216C 操作函数 */
static const struct file_operations ap3216c_ops = {.owner = THIS_MODULE,.open = ap3216c_open,.read = ap3216c_read,.release = ap3216c_release,
};/*i2c 驱动的 probe 函数*/
static int ap3216c_probe(struct i2c_client *client,const struct i2c_device_id *id)
{int ret;/*分配内存*/struct ap3216c_dev *ap3216cdev;ap3216cdev = devm_kzalloc(&client->dev, sizeof(*ap3216cdev),GFP_KERNEL);if(!ap3216cdev){return -ENOMEM;}/* 注册字符设备驱动 *//*1->申请设备号*/ap3216cdev->major=0;if(ap3216cdev->major){//若给定主设备号ap3216cdev->devid=MKDEV(ap3216cdev->major,0);ret=register_chrdev_region(ap3216cdev->devid,AP3216C_CNT,AP3216C_NAME);}else {//若未给定主主设备号ret=alloc_chrdev_region(&ap3216cdev->devid,0,AP3216C_CNT,AP3216C_NAME);ap3216cdev->major=MAJOR(ap3216cdev->devid);ap3216cdev->minor=MINOR(ap3216cdev->devid);}if(ret < 0){//注册字符设备驱动失败goto fail_devid;}printk("major = %d,minor = %d,CNT = %d,NAME = %s\r\n",ap3216cdev->major,ap3216cdev->minor,AP3216C_CNT,AP3216C_NAME);/*2->初始化cdev*/ap3216cdev->cdev.owner=THIS_MODULE;cdev_init(&ap3216cdev->cdev,&ap3216c_ops);/*3->添加cdev*/ret=cdev_add(&ap3216cdev->cdev,ap3216cdev->devid,AP3216C_CNT);if(ret < 0) {goto fail_cdev;}/*4->创建类class*/ap3216cdev->class = class_create(THIS_MODULE,AP3216C_NAME);//创建/dev/ap3216cif(IS_ERR(ap3216cdev->class)){ret = PTR_ERR(ap3216cdev->class);goto fail_class;}/*5->创建设备节点device*/ap3216cdev->device = device_create(ap3216cdev->class,NULL,ap3216cdev->devid,NULL,AP3216C_NAME);//创建/dev/ap3216cif(IS_ERR(ap3216cdev->device)){ret = PTR_ERR(ap3216cdev->device);goto fail_device;}ap3216cdev->client = client;/* 保存 ap3216cdev 结构体 *//*实现了ap3216c_dev设备结构体与 i2c_client 之间的关联*/i2c_set_clientdata(client,ap3216cdev);return 0;
fail_device:class_destroy(ap3216cdev->class);
fail_class:cdev_del(&ap3216cdev->cdev);
fail_cdev:unregister_chrdev_region(ap3216cdev->devid,AP3216C_CNT);
fail_devid:return ret;
}
/*i2c 驱动的 remove 函数*/
static int ap3216c_remove(struct i2c_client *client)
{/*获取与 i2c_client 关联的自定义数据指针*/struct ap3216c_dev *ap3216cdev = i2c_get_clientdata(client);/*注销字符设备驱动*//*注销设备device*/device_destroy(ap3216cdev->class,ap3216cdev->devid);/*注销类class*/class_destroy(ap3216cdev->class);/*注销字符设备对象*/cdev_del(&ap3216cdev->cdev);/*注销设备号*/unregister_chrdev_region(ap3216cdev->devid,AP3216C_CNT);return 0;
}/* 传统匹配方式 ID 列表 */
static const struct i2c_device_id ap3216c_id[] = {{"alientek,ap3216c", 0},{}
};/* 设备树匹配列表 */
static const struct of_device_id ap3216c_of_match[] = {
{ .compatible = "alientek,ap3216c" },
{ /* Sentinel */ }
};
MODULE_DEVICE_TABLE(of,ap3216c_of_match);/* i2c 驱动结构体 */
static struct i2c_driver ap3216c_driver = {.probe = ap3216c_probe,.remove = ap3216c_remove,.driver = {.owner = THIS_MODULE,.name = "ap3216c",.of_match_table = ap3216c_of_match,},.id_table = ap3216c_id,
};/*驱动注册和注销一体化*/
module_i2c_driver(ap3216c_driver);MODULE_LICENSE("GPL");
MODULE_AUTHOR("chensir");
MODULE_INFO(intree, "Y");
ap3216cApp.c:
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "sys/ioctl.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include <poll.h>
#include <sys/select.h>
#include <sys/time.h>
#include <signal.h>
#include <fcntl.h>int main(int argc, char *argv[])
{int fd;char *filename;unsigned short databuf[3];unsigned short ir, als, ps;int ret = 0;if (argc != 2) {printf("Error Usage!\r\n");return -1;}filename = argv[1];fd = open(filename, O_RDWR);if(fd < 0) {printf("can't open file %s\r\n", filename);return -1;}while (1) {ret = read(fd, databuf, sizeof(databuf));if(ret == 0) { /* 数据读取成功 */ir = databuf[0]; /* ir 传感器数据 */als = databuf[1]; /* als 传感器数据 */ps = databuf[2]; /* ps 传感器数据 */printf("ir = %d, als = %d, ps = %d\r\n", ir, als, ps);}usleep(200000); /*100ms */}close(fd); /* 关闭文件 */return 0;
}
makefile:
KERNELDIR := /home/chensir/linux/atk-mp1/linux/my_linux/linux-5.4.31
CURRENT_PATH := $(shell pwd)
obj-m := ap3216c.obuild: kernel_moduleskernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean