利用I2C_bus(I2C总线)为挂接在I2C总线上的设备AP3216C编写驱动程序
前言
关于I2C总线的原理和结构的介绍,请参看我的另一篇博文 https://blog.csdn.net/wenhao_ir/article/details/146405656
在阅读以下内容前,也建议先看一看上面这篇博文。
i2c_driver
的实现
完整代码(ap3216c_drv.c)
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/of_device.h>
#include <linux/slab.h>
#include <linux/delay.h>
#include <linux/i2c.h>
#include <linux/uaccess.h>
#include <linux/fs.h>
static int major = 0;
static struct class *ap3216c_class;
static struct i2c_client *ap3216c_client;
static ssize_t ap3216c_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
int err;
char kernel_buf[6];
int val;
if (size != 6)
return -EINVAL;
/*注意:下面的函数i2c_smbus_read_word_data并不是由i2c-tools工具集提供的,i2c-tools工具集只提供了用户空间的相关函数。
下面的函数i2c_smbus_read_word_data是由Linux内核的I2C子系统提供的,函数定义于Linux内核的i2c-core-smbus.c文件中。
配置内核时启用I2C支持,包括 CONFIG_I2C 和 CONFIG_I2C_CHARDEV,那么相关函数就有了。*/
val = i2c_smbus_read_word_data(ap3216c_client, 0xA); /* read IR */
kernel_buf[0] = val & 0xff;
kernel_buf[1] = (val>>8) & 0xff;
val = i2c_smbus_read_word_data(ap3216c_client, 0xC); /* read 光强 */
kernel_buf[2] = val & 0xff;
kernel_buf[3] = (val>>8) & 0xff;
val = i2c_smbus_read_word_data(ap3216c_client, 0xE); /* read 距离 */
kernel_buf[4] = val & 0xff;
kernel_buf[5] = (val>>8) & 0xff;
err = copy_to_user(buf, kernel_buf, size);
return size;
}
static int ap3216c_open (struct inode *node, struct file *file)
{
/*注意:下面的函数i2c_smbus_write_byte_data并不是由i2c-tools工具集提供的,i2c-tools工具集只提供了用户空间的相关函数。
下面的函数i2c_smbus_write_byte_data是由Linux内核的I2C子系统提供的,函数定义于Linux内核的i2c-core-smbus.c文件中。
配置内核时启用I2C支持,包括 CONFIG_I2C 和 CONFIG_I2C_CHARDEV,那么相关函数就有了。*/
i2c_smbus_write_byte_data(ap3216c_client, 0, 0x4);
/* delay for reset */
mdelay(20);
i2c_smbus_write_byte_data(ap3216c_client, 0, 0x3);
mdelay(250);
return 0;
}
static struct file_operations ap3216c_ops = {
.owner = THIS_MODULE,
.open = ap3216c_open,
.read = ap3216c_read,
};
static const struct of_device_id of_match_ids_ap3216c[] = {
{ .compatible = "lite-on,ap3216c", .data = NULL },
{ /* END OF LIST */ },
};
static const struct i2c_device_id ap3216c_ids[] = {
{ "ap3216c", (kernel_ulong_t)NULL },
{ /* END OF LIST */ }
};
static int ap3216c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
ap3216c_client = client;
/* register_chrdev */
major = register_chrdev(0, "ap3216c", &ap3216c_ops);
ap3216c_class = class_create(THIS_MODULE, "ap3216c_class");
device_create(ap3216c_class, NULL, MKDEV(major, 0), NULL, "ap3216c"); /* /dev/ap3216c */
return 0;
}
static int ap3216c_remove(struct i2c_client *client)
{
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
device_destroy(ap3216c_class, MKDEV(major, 0));
class_destroy(ap3216c_class);
/* unregister_chrdev */
unregister_chrdev(major, "ap3216c");
return 0;
}
static struct i2c_driver i2c_ap3216c_driver = {
.driver = {
.name = "ap3216c",
.of_match_table = of_match_ids_ap3216c,
},
.probe = ap3216c_probe,
.remove = ap3216c_remove,
.id_table = ap3216c_ids,
};
static int __init i2c_driver_ap3216c_init(void)
{
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
return i2c_add_driver(&i2c_ap3216c_driver);
}
module_init(i2c_driver_ap3216c_init);
static void __exit i2c_driver_ap3216c_exit(void)
{
i2c_del_driver(&i2c_ap3216c_driver);
}
module_exit(i2c_driver_ap3216c_exit);
MODULE_AUTHOR("suwenhao");
MODULE_LICENSE("GPL");
代码分析说明
这个代码当你看了下面几篇博文的相关内容后就没啥好说的。
https://blog.csdn.net/wenhao_ir/article/details/146405656【这篇博文全部看】
https://blog.csdn.net/wenhao_ir/article/details/146319007 【搜索“使用读写命令行对I2C设备AP3216C进行读写操作”】
https://blog.csdn.net/wenhao_ir/article/details/146361457 【这篇博文全部看】
值得注意的两个问题
第1个值得注意的地方:
代码中的函数i2c_smbus_read_word_data
和 i2c_smbus_write_byte_data
不是 由 i2c-tools
提供的,而是 Linux 内核 I2C 子系统 的一部分,由 内核的 I2C 核心代码 实现。这两个函数的定义位于 Linux 内核 的 i2c-core-smbus.c
文件中,
第2个值得注意的地方:
i2c_driver
中并没有指定是哪条I2C总线,那么它怎么知道操作哪条I2C总线?
答案很简单,因为在i2c_client
中会含有I2C设备挂接于哪条I2C总线的信息啦。你往这篇博文后面看i2c_client
的实现就知道了。配置内核时启用I2C支持,包括 CONFIG_I2C 和 CONFIG_I2C_CHARDEV,那么相关函数就有了。
Makfile文件的编写
KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88/
all:
make -C $(KERN_DIR) M=`pwd` modules
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
obj-m += ap3216c_drv.o
交叉编译生成ap3216c_drv.ko
将文件ap3216c_drv.c
、Makefile
复制到Ubuntu的相关目录中:
然后执行make,从而生成ap3216c_drv.ko
文件
ap3216c_drv.ko
复制到网络文件目录中
加载ap3216c_drv.ko
打开串口终端→打开开发板→挂载网络文件系统
mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt
把内核打印信息打开:
echo "7 4 1 7" > /proc/sys/kernel/printk
加载模块ap3216c_drv.ko
cd /mnt/i2c_bus_driver/
insmod ap3216c_drv.ko
[ 38.335052] ap3216c_drv: loading out-of-tree module taints kernel.
[ 38.346271] /home/book/mycode/i2c_bus_driver/ap3216c_drv.c i2c_driver_ap3216c_init 116
可见,成功加载了模块,对应的内核打印信息语句如下:
用户空间的测试程序的实现
完整代码(ap3216c_drv_test.c)
这个测试程序就很简单了,代码如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
/*
*/
int main(int argc, char **argv)
{
int fd;
char buf[6];
int len;
/* 2. 打开文件 */
fd = open("/dev/ap3216c", O_RDWR);
if (fd == -1)
{
printf("can not open file /dev/ap3216c\n");
return -1;
}
len = read(fd, buf, 6);
printf("APP read : ");
for (len = 0; len < 6; len++)
printf("%02x ", buf[len]);
printf("\n");
close(fd);
return 0;
}
就不多说了,直接复制到Ubuntu中,用下面的交叉编译命令编译就是了:
交叉编译
arm-buildroot-linux-gnueabihf-gcc -o ap3216c_drv_test ap3216c_drv_test.c
ap3216c_drv_test
复制到网络文件系统目录中
运行测试程序
具体的测试程序的运行见下一个目录``i2c_client的实现
。
i2c_client
的实现和生成
方式一:直接使用echo命令创建
要读懂下面这两条命令,可以先看看我之前写的博文 https://blog.csdn.net/wenhao_ir/article/details/146319007 中对芯片AP3216C的介绍,搜索关键词“使用读写命令行对I2C设备AP3216C进行读写操作”
// 在i2c-0下创建i2c_client
echo ap3216c 0x1e > /sys/bus/i2c/devices/i2c-0/new_device
// 删除i2c_client
0x1e > /sys/bus/i2c/devices/i2c-0/delete_device
命令中,0x1e是AP3216C的地址。
从上面的命令中,我们可以看到,一个i2c_client
需包含三个关键信息,第1个是设备的名字(name)、第2个是I2C设备的地址,第3个是该I2C设备挂在哪条I2C总线上。
我们这里来实际测试一下,按上面i2c_driver
实现中记录的方法加载好模块ap3216c_drv.ko
.
然后我们用echo命令创建i2c_client
,运行下面的命令:
echo ap3216c 0x1e > /sys/bus/i2c/devices/i2c-0/new_device
[ 1043.137190] /home/book/mycode/i2c_bus_driver/ap3216c_drv.c ap3216c_probe 79
[ 1043.167269] i2c i2c-0: new_device: Instantiated device ap3216c at 0x1e
从画红线的内核打印信息来看,i2c_client
生成成功并与i2c_driver
匹配成功了,相关的打印输出语句代码如下:
我们可以用下面的命令看下有没有相关的驱动程序加载于内核中:
cat /proc/devices
有了,如下图所示:
我们还可以看下有没有设备文件生成:
ls /dev/ap3216*
有了,如下图所示:
接下来,我们运行上面已经编译生成好的测试程序,看下能否达到预期效果。
cd /mnt/i2c_bus_test/
./ap3216c_drv_test
然后我们用手电去照射芯片AP3216C,再运行测试程序,看下有没有数据变化:
可见,光强数据有变化了,具体值的解析就不去赘述了,参考 https://blog.csdn.net/wenhao_ir/article/details/146319007 【搜索“第03条命令-读取光照强度数据”】
然后我再用手指去接近芯片AP3216C,再运行测试程序,看下有没有数据变化:
可见,距离数据值也有变化了,具体值的解析就不去赘述了,参考 https://blog.csdn.net/wenhao_ir/article/details/146319007 【搜索“第04条命令-读取距离值”】
接下来,我们删除掉刚才用命令行创建的i2c_client
,用下面的命令:
echo 0x1e > /sys/bus/i2c/devices/i2c-0/delete_device
从运行结果来看,删除i2c_client
的操作触发了ap3216c_drv.c
中的与probe函数相对应的ap3216c_remove
的执行,如下图所示:
此时我们发现相应的驱动程序和设备文件没有了,如下所示:
cat /proc/devices
major号为245的驱动程序不见了。
ls /dev/ap3216*
方式二:利用代码实现i2c_client
完整代码
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/i2c.h>
static struct i2c_client *ap3216c_client;
static int __init i2c_client_ap3216c_init(void)
{
struct i2c_adapter *adapter;
static struct i2c_board_info board_info = {
I2C_BOARD_INFO("ap3216c", 0x1e), // 填写I2C设备的信息,注意I2C_BOARD_INFO是一个宏
};
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
/* register I2C device */
adapter = i2c_get_adapter(0); // 获取0号I2C适配器(控制器)
ap3216c_client = i2c_new_device(adapter, &board_info); // 在 I2C-0 总线上创建一个新的 I2C 设备
i2c_put_adapter(adapter); // 释放掉适配器资源
return 0;
}
static void __exit i2c_client_ap3216c_exit(void)
{
i2c_unregister_device(ap3216c_client);
}
module_init(i2c_client_ap3216c_init);
module_exit(i2c_client_ap3216c_exit);
MODULE_AUTHOR("suwenhao");
MODULE_LICENSE("GPL");
代码分析
代码虽然比较简单,但还是要讲解,由于关键代码都在函数i2c_client_ap3216c_init
里,所以这里就分析函数i2c_client_ap3216c_init
,分析如下:
函数i2c_client_ap3216c_init
的代码如下:
static int __init i2c_client_ap3216c_init(void)
{
struct i2c_adapter *adapter;
static struct i2c_board_info board_info = {
I2C_BOARD_INFO("ap3216c", 0x1e), // 填写I2C设备的信息,注意I2C_BOARD_INFO是一个宏
};
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
/* register I2C device */
adapter = i2c_get_adapter(0); // 获取0号I2C适配器(控制器)
ap3216c_client = i2c_new_device(adapter, &board_info); // 在 I2C-0 总线上创建一个新的 I2C 设备
i2c_put_adapter(adapter); // 释放掉适配器资源
return 0;
}
这段代码的作用是在 Linux 内核的 I2C 框架 下,手动创建一个 i2c_client
设备,并将其注册到 I2C 总线中。它通常用于 没有设备树(Device Tree)或 ACPI 方式描述 I2C 设备的情况下。
1. 定义 i2c_board_info
static struct i2c_board_info board_info = {
I2C_BOARD_INFO("ap3216c", 0x1e),
};
i2c_board_info
结构体用于描述 I2C 设备的信息,它用于手动注册 I2C 设备。I2C_BOARD_INFO("ap3216c", 0x1e)
宏展开后等效于:struct i2c_board_info board_info = { .type = "ap3216c", .addr = 0x1e, };
"ap3216c"
:这个设备的 I2C 驱动名称,必须与i2c_driver
里的.name
一致,才能正确匹配驱动。0x1e
:I2C 设备的地址(7 位地址)。
2. 获取 I2C 适配器
adapter = i2c_get_adapter(0);
i2c_get_adapter(bus_num)
用于 获取 I2C 总线适配器。bus_num = 0
代表获取 I2C-0 总线上的适配器。- 这个适配器代表 I2C 控制器(I2C master)。
3. 创建 i2c_client
设备
ap3216c_client = i2c_new_device(adapter, &board_info);
i2c_new_device(adapter, &board_info)
作用:- 在 I2C-0 总线上创建一个新的 I2C 设备(
i2c_client
)。 - 设备地址是
0x1e
,设备名是"ap3216c"
。 - 这个
i2c_client
结构体会被自动注册,并与i2c_driver
进行匹配:- 如果有
ap3216c
这个i2c_driver
,那么i2c-core
就会调用它的probe()
函数。
- 如果有
- 在 I2C-0 总线上创建一个新的 I2C 设备(
4. 释放 I2C 适配器
i2c_put_adapter(adapter);
i2c_put_adapter(adapter)
释放 之前i2c_get_adapter()
获取的适配器,防止资源泄漏。
Makefile文件的编写
KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88/
all:
make -C $(KERN_DIR) M=`pwd` modules
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
obj-m += ap3216c_drv.o
交叉编译生成ap3216c_client.ko
复制文件ap3216c_client.c、Makefile到Ubuntu中:
然后make生成ap3216c_client.ko
make
ap3216c_client.ko复制到网络文件目录中
加载生成的ap3216c_client.ko
首先确认i2c_driver
的ap3216c_drv.ko
加载好了。
然后加载ap3216c_client.ko
cd /mnt/i2c_bus_client
insmod ap3216c_client.ko
可见加载后触发了i2c_driver
中的probe函数。我们接下来看下major和设备文件被创建没有。
cat /proc/devices
可见 major被创建了。
ls /dev/ap3216*
可见设备文件也有了。
运行用户空间的测试程序
接下来,我们运行上面已经编译生成好的测试程序,看下能否达到预期效果。
cd /mnt/i2c_bus_test/
./ap3216c_drv_test
然后我们用手电去照射芯片AP3216C,再运行测试程序,看下有没有数据变化:
可见,光强数据有变化了,具体值的解析就不去赘述了,参考 https://blog.csdn.net/wenhao_ir/article/details/146319007 【搜索“第03条命令-读取光照强度数据”】
然后我再用手指去接近芯片AP3216C,再运行测试程序,看下有没有数据变化:
可见,距离数据值也有变化了,具体值的解析就不去赘述了,参考 https://blog.csdn.net/wenhao_ir/article/details/146319007 【搜索“第04条命令-读取距离值”】
卸载掉i2c_client
rmmod ap3216c_client
可见,对模块ap3216c_client.ko的卸载会触发i2c_driver
中的remove函数,相关的代码如下:
此时我们发现相应的驱动程序和设备文件没有了,如下所示:
cat /proc/devices
major号为245的驱动程序不见了。
ls /dev/ap3216*
提问:如果某条I2C总线上有多个设备需要扫描怎么办?
比如我的I2C总线上外挂有多种测量光照和距离的芯片作为冗余备份,我可不可以进行扫描然后选择一个可用设备,这是可以的,代码如下:
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/i2c.h>
static struct i2c_client *ap3216c_client;
/* Addresses to scan */
static const unsigned short normal_i2c[] = {
0x1e, I2C_CLIENT_END
};
static int __init i2c_client_ap3216c_init(void)
{
struct i2c_adapter *adapter;
struct i2c_board_info board_info;
memset(&board_info, 0, sizeof(struct i2c_board_info));
strscpy(board_info.type, "ap3216c", sizeof(board_info.type));
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
/* register I2C device */
adapter = i2c_get_adapter(0);
ap3216c_client = i2c_new_probed_device(adapter, &board_info,
normal_i2c, NULL);
i2c_put_adapter(adapter);
return 0;
}
static void __exit i2c_client_ap3216c_exit(void)
{
i2c_unregister_device(ap3216c_client);
}
module_init(i2c_client_ap3216c_init);
module_exit(i2c_client_ap3216c_exit);
MODULE_AUTHOR("suwenhao");
MODULE_LICENSE("GPL");
代码就不去分析测试了,以后要用的时候再来研究。
方式三:利用设备树生成i2c_client
修改设备树文件
我们只需要把I2C设备加到设备树中的I2C控制器节点下面即可。
在博文 https://blog.csdn.net/wenhao_ir/article/details/146393239 中我已经列举出了I2C控制器在Linux的设备树文件中的描述。
现在我们只需要把下面的节点信息:
&i2c1 {
ap3216c@1e {
compatible = "lite-on,ap3216c";
reg = <0x1e>;
};
};
加到设备树文件100ask_imx6ull-14x14.dts
的引用结构&i2c1
中去即可,我们先把内核源码中的设备树文件复制到Windows的VScode工程目录中。
设备树文件100ask_imx6ull-14x14.dts
在Ubuntu中的路径如下:
/home/book/100ask_imx6ull-sdk/Linux-4.9.88/arch/arm/boot/dts/100ask_imx6ull-14x14.dts
然后把Ubuntu中的设备树文件100ask_imx6ull-14x14.dts
重命名为100ask_imx6ull-14x14.dts.bak2_把AP3216C加入第1个IC2控制器的节点下前的备份
,
同时把100ask_imx6ull-14x14.dtb
也重名为100ask_imx6ull-14x14.dtb.bak2_把AP3216C加入第1个IC2控制器的节点下前的备份
。
接着把复制到Windows的VScode工程目录中的设备树文件100ask_imx6ull-14x14.dts
打开,然后搜索“&i2c1”
在它里面加入子节点ap3216c@1e
的信息:
&i2c1 {
ap3216c@1e {
compatible = "lite-on,ap3216c";
reg = <0x1e>;
};
};
加好之下如下图所示:
编译修改好的设备树文件
然后再把修改好的设备树文件100ask_imx6ull-14x14.dts
复制到Ubuntu中:
然后进入内核编译出新的设备树文件:
cd /home/book/100ask_imx6ull-sdk/Linux-4.9.88
make dtbs
更新开发析上的设备树文件
把新生成的dtb文件复制到NFS网络系统目录中:
打开串口终端→打开开发板→挂载网络文件系统:
mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt
然后用下面的命令覆盖掉之前的设备树文件:
cp /mnt/100ask_imx6ull-14x14.dtb /boot/
重启开发板,看下 i2c_client
是否生成
启动之后执行下面的命令查看下有没有i2c_client
生成:
cd /sys/bus/i2c/devices/i2c-0/
ls
可见,有了,目录名字为0-001e
,我们进入目录0-001e
看下里面有什么:
cd 0-001e/
ls
我们看下名字为name
的文件里的内容:
cat name
可见,这个 i2c_client
的名字为ap3216c
。
装载驱动程序
装载驱动程序之前还是把内核打印信息打开:
echo "7 4 1 7" > /proc/sys/kernel/printk
然后挂载网络文件系统:
mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt
然后加载我们之前编译生成的ap3216c_drv.ko
cd /mnt/i2c_bus_driver/
insmod ap3216c_drv.ko
可见,在加载ap3216c_drv.ko
时,由于内核已经存在能够匹配成功的i2c_client
,所以匹配成功,然后调用了probe函数,从而生成了相应的major和设备文件:
cat /proc/devices
ls /dev/ap3216*
接下来我们就可以运行测试程序去测试了。
运行用户空间的测试程序
接下来,我们运行上面已经编译生成好的测试程序,看下能否达到预期效果。
cd /mnt/i2c_bus_test/
./ap3216c_drv_test
然后我们用手电去照射芯片AP3216C,再运行测试程序,看下有没有数据变化:
可见,光强数据有变化了,具体值的解析就不去赘述了,参考 https://blog.csdn.net/wenhao_ir/article/details/146319007 【搜索“第03条命令-读取光照强度数据”】
然后我再用手指去接近芯片AP3216C,再运行测试程序,看下有没有数据变化:
可见,距离数据值也有变化了,具体值的解析就不去赘述了,参考 https://blog.csdn.net/wenhao_ir/article/details/146319007 【搜索“第04条命令-读取距离值”】
看能否正常卸载掉ap3216c_drv.ko
rmmod ap3216c_drv
可见,释放掉i2c_driver结构体是会触发i2c_driver结构体中的成员函数remove
,相关的调用触发关系如下图所示:
此时我们发现相应的驱动程序和设备文件没有了,如下所示:
cat /proc/devices
major号为245的驱动程序不见了。
ls /dev/ap3216*
附相关文件
分别编译好的三个工程文件及源码
如下图所示:
https://pan.baidu.com/s/1_8PmddBf53rtAvLK6rTfVA?pwd=r8sk
没有进行分割的工程源码(包括Makefile文件、设备树源码)
https://pan.baidu.com/s/1BQQOXvvs5XzE5LzvQQbn6g?pwd=hrac
设备树源码(dts)和编译好的二进制文件(dtb)
https://pan.baidu.com/s/1oUyO6bkJt8L8GL-9jgfyhg?pwd=bibk