iTOP-STM32MP157开发板采用ST推出的双核cortex-A7+单核cortex-M4异构处理器,既可用Linux、又可以用于STM32单片机开发。开发板采用核心板+底板结构,主频650M、1G内存、8G存储,核心板采用工业级板对板连接器,高可靠,牢固耐用,可满足高速信号环境下使用。共240PIN,CPU功能全部引出:底板扩展接口丰富底板板载4G接口(选配)、千兆以太网、WIFI蓝牙模块HDMI、CAN、RS485、LVDS接口、温湿度传感器(选配)光环境传感器、六轴传感器、2路USB OTG、3路串口,CAMERA接口、ADC电位器、SPDIF、SDIO接口等
第172章 使用C文件编写I2C client代码
首先我们来回顾一下前面讲解的平台总线相关知识,平台总线将驱动分为了platform driver和platform device两个部分,而最终设备树取代了platform device部分,而为了对I2C子系统框架有一个更深刻的认识,在本章节将使用platform device重新编写I2C client代码并与上一章中的I2C driver部分进行匹配。
172.1 I2C硬件资源描述
172.1.1 i2c_get_adapter函数
i2c_get_adapter函数的主要作用是根据给定的I2C适配器编号 nr 从 i2c_adapter_idr 中查找对应的 i2c_adapter 结构体,该函数定义在“drivers/i2c/i2c-core-base.c”文件中,具体内容如下所示:
struct i2c_adapter *i2c_get_adapter(int nr)
{
struct i2c_adapter *adapter;
// 获取 i2c_adapter_idr 中的锁
mutex_lock(&core_lock);
// 在 i2c_adapter_idr 中查找指定编号的适配器
adapter = idr_find(&i2c_adapter_idr, nr);
if (!adapter)
goto exit;
// 尝试获取适配器所属模块的引用计数
if (try_module_get(adapter->owner))
// 增加适配器 device 的引用计数
get_device(&adapter->dev);
else
adapter = NULL;
exit:
// 释放 i2c_adapter_idr 中的锁
mutex_unlock(&core_lock);
return adapter;
}
第6行:获取 core_lock 锁,以防止其他线程同时访问 i2c_adapter_idr。
第9-11行:使用 idr_find() 函数在 i2c_adapter_idr 中查找指定编号的适配器。
第14-18行如果找到了适配器,就尝试获取适配器所属模块的引用计数,以防止模块被卸载。如果成功获取,就增加适配器 device 的引用计数。如果失败,就将 adapter 设置为 NULL。
172.1.2 i2c_put_adapter函数
上一小节的i2c_get_adapter函数用于查找i2c_adapter结构体,当驱动卸载时2c_adapter结构体需要被释放,而当结构体i2c_put_adapter函数用于释放 i2c_adapter 结构体,i2c_put_adapter函数也定义在“drivers/i2c/i2c-core-base.c”文件中,具体内容如下所示:
void i2c_put_adapter(struct i2c_adapter *adap)
{
// 如果 adap 指针为 NULL,直接返回
if (!adap)
return;
put_device(&adap->dev); // 调用 put_device 函数释放 i2c_adapter 设备
module_put(adap->owner); // 减少 i2c_adapter 所属模块的引用计数
}
172.1.3 i2c_new_device函数
i2c_new_device函数用于创建和注册与 I2C 总线上对应的设备。注册完成后,I2C子系统会自动为该设备创建相应的设备节点,供上层应用程序进行访问和控制,该函数同样定义在定义在“drivers/i2c/i2c-core-base.c”文件中,具体内容如下所示:、
struct i2c_client *i2c_new_device(struct i2c_adapter *adap, struct i2c_board_info const *info)
{
struct i2c_client *client;
int status;
// 分配 i2c_client 结构体空间
client = kzalloc(sizeof *client, GFP_KERNEL);
if (!client)
return NULL;
// 设置 i2c_client 的适配器指针
client->adapter = adap;
// 从 i2c_board_info 结构体中拷贝相关信息到 i2c_client
client->dev.platform_data = info->platform_data;
client->flags = info->flags;
client->addr = info->addr;
client->init_irq = info->irq;
if (!client->init_irq)
client->init_irq = i2c_dev_irq_from_resources(info->resources,
info->num_resources);
client->irq = client->init_irq;
strlcpy(client->name, info->type, sizeof(client->name));
// 检查地址是否有效
status = i2c_check_addr_validity(client->addr, client->flags);
if (status) {
dev_err(&adap->dev, "Invalid %d-bit I2C address 0x%02hx\n",
client->flags & I2C_CLIENT_TEN ? 10 : 7, client->addr);
goto out_err_silent;
}
// 检查地址是否已被其他设备占用
status = i2c_check_addr_ex(adap, i2c_encode_flags_to_addr(client));
if (status)
dev_err(&adap->dev,
"%d i2c clients have been registered at 0x%02x",
status, client->addr);
// 设置 i2c_client 的设备信息
client->dev.parent = &client->adapter->dev;
client->dev.bus = &i2c_bus_type;
client->dev.type = &i2c_client_type;
client->dev.of_node = of_node_get(info->of_node);
client->dev.fwnode = info->fwnode;
i2c_dev_set_name(adap, client, info, status);
// 如果有设备属性,添加到设备
if (info->properties) {
status = device_add_properties(&client->dev, info->properties);
if (status) {
dev_err(&adap->dev,
"Failed to add properties to client %s: %d\n",
client->name, status);
goto out_err_put_of_node;
}
}
// 注册设备
status = device_register(&client->dev);
if (status)
goto out_free_props;
dev_dbg(&adap->dev, "client [%s] registered with bus id %s\n",
client->name, dev_name(&client->dev));
return client;
out_free_props:
if (info->properties)
device_remove_properties(&client->dev);
out_err_put_of_node:
of_node_put(info->of_node);
out_err_silent:
kfree(client);
return NULL;
}
第6-9行:动态分配 i2c_client 结构体空间,使用 kzalloc() 动态分配一个 i2c_client 结构体的内存空间,并将其清零。
第12行:设置 i2c_client 的适配器指针,将传入的 i2c_adapter 指针保存到 i2c_client 的 adapter 字段中。
第15-23行:从 i2c_board_info 复制信息到 i2c_client,将输入的 i2c_board_info 结构体中的相关信息,如设备地址(addr)、设备标志(flags)、中断号(irq)等,复制到新创建的 i2c_client 结构体中。
第26-38检查地址合法性:调用 i2c_check_addr_validity() 和 i2c_check_addr_ex() 函数检查设备地址的有效性和是否与其他设备冲突。
第40-46行:设置 i2c_client 的其他信息,初始化 i2c_client 的其他字段,如设备名称(name)、设备节点(of_node)、设备属性(properties)等。
第60-65行:注册 i2c_client 设备,最后,调用 device_register() 函数将新创建的 i2c_client 设备注册到内核设备模型中。
172.1.4 i2c_board_info 结构体
i2c_new_device函数会传入一个i2c_board_info类型的结构体,该结构体描述了I2C设备的静态信息,如设备类型、地址、名称等,该结构体定义在“include/linux/i2c.h”文件中,具体内容如下所示:
struct i2c_board_info {
char type[I2C_NAME_SIZE]; // I2C 设备的类型名称,最大长度为 I2C_NAME_SIZE
unsigned short flags; // I2C 设备的标志位,用于指定设备的特殊属性
unsigned short addr; // I2C 设备的地址
const char *dev_name; // I2C 设备的设备名称
void *platform_data; // I2C 设备的平台数据,可为 NULL
struct device_node *of_node; // I2C 设备节点在设备树中的节点指针
struct fwnode_handle *fwnode; // I2C 设备节点在 ACPI 中的 fwnode 句柄
const struct property_entry *properties; // I2C 设备的属性列表
const struct resource *resources; // I2C 设备使用的资源列表
unsigned int num_resources; // I2C 设备使用的资源数量
int irq;
};
172.2驱动程序的编写
本实验驱动对应的网盘路径为:iTOP-RK3568开发板【底板V1.7版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux驱动配套资料\04_Linux驱动程序\103_ft5x06_02\。
本实验旨在使用platform device编写I2C client部分代码,编写完成的ft5x06_device.c代码如下所示:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/i2c.h>
// 定义一个 i2c_adapter 结构体指针
struct i2c_adapter *i2c_ada;
// 定义 i2c_board_info 结构体数组,用于描述 ft5x06 设备
static struct i2c_board_info ft5x06[] = {
{
.type = "my-ft5x06",
.addr = 0x38, // ft5x06 设备的 I2C 地址
},
};
// 驱动的初始化函数
static int ft5x06_client_init(void)
{
// 获取 I2C 适配器
i2c_ada = i2c_get_adapter(1);
if (!i2c_ada)
{
printk(KERN_ERR "Failed to get I2C adapter\n");
return -ENODEV;
}
// 注册 ft5x06 设备
i2c_new_device(i2c_ada, ft5x06);
return 0;
}
// 驱动的退出函数
static void ft5x06_client_exit(void)
{
// 释放 I2C 适配器
i2c_put_adapter(i2c_ada);
}
// 驱动的初始化和退出入口函数
module_init(ft5x06_client_init);
module_exit(ft5x06_client_exit);
MODULE_LICENSE("GPL");
platform device驱动编写完成之后,因为匹配方式不同,所以并不能直接使用上一章节编写的I2C驱动程序,需要对驱动程序进行简单的修改,修改完成的t5x06_driver.c I2C驱动程序如下所示:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/of_device.h>
// ft5x06设备的初始化函数
int ft5x06_probe(struct i2c_client *client, const struct i2c_device_id *id) {
printk("This is ft5x06 probe\n");
return 0;
}
// ft5x06设备的移除函数
int ft5x06_remove(struct i2c_client *client) {
return 0;
}
// 定义 i2c_device_id 结构体数组,用于标识 ft5x06 设备
static const struct i2c_device_id ft5x06_id[] = {
{ "my-ft5x06", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, ft5x06_id);
// 定义 i2c_driver 结构体,描述 ft5x06 设备驱动
static struct i2c_driver ft5x06_driver = {
.driver = {
.name = "my-ft5x06",
.owner = THIS_MODULE,
},
.probe = ft5x06_probe,
.remove = ft5x06_remove,
.id_table = ft5x06_id,
};
// 驱动初始化函数
static int __init ft5x06_driver_init(void) {
int ret;
// 注册I2C设备驱动
ret = i2c_add_driver(&ft5x06_driver);
if (ret < 0) {
printk("i2c_add_driver is error\n");
return ret;
}
return 0;
}
// 驱动退出函数
static void __exit ft5x06_driver_exit(void) {
// 注销I2C设备驱动
i2c_del_driver(&ft5x06_driver);
}
module_init(ft5x06_driver_init);
module_exit(ft5x06_driver_exit);
MODULE_LICENSE("GPL");
172.3 运行测试
172.3.1 编译驱动程序
首先在上一小节中的ft5x06_device.c代码同一目录下创建 Makefile 文件,Makefile 文件内容如下所示:
export ARCH=arm64#设置平台架构
export CROSS_COMPILE=aarch64-linux-gnu-#交叉编译器前缀
obj-m += ft5x06_device.o #此处要和你的驱动源文件同名
KDIR :=/home/topeet/Linux/linux_sdk/kernel #这里是你的内核目录
PWD ?= $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules #make操作
clean:
make -C $(KDIR) M=$(PWD) clean #make clean操作
对于Makefile的内容注释已在上图添加,保存退出之后,来到存放platform_driver.c和Makefile文件目录下,如下图所示:
然后使用命令“make”进行驱动的编译,编译完成如下图所示:
编译完生成ft5x06_driver.ko目标文件,如下图所示:
然后在上一小节中的ft5x06_driver.c代码同一目录下创建 Makefile 文件,Makefile 文件内容如下所示: 对于Makefile的内容注释已在上图添加,保存退出之后,来到存放platform_driver.c和Makefile文件目录下,如下图所示:
然后使用命令“make”进行驱动的编译,编译完成如下图所示:
编译完生成ft5x06_driver.ko目标文件,如下图所示:
172.3.2 运行测试
首先启动开发板,进入系统之后如下图所示:
然后将上一个小节编译完成的ko文件拷贝到开发板上,拷贝完成如下图所示:
然后使用以下命令加载两个驱动,加载完成如下图所示:
insmod ft5x06_device.ko
insmod ft5x06_driver.ko
可以看到在ft5x06_device.ko驱动加载成功之后,成功在I2C1控制器注册了地址为0x38的设备,然后加载了ft5x06_device.ko驱动,成功打印了在probe函数中的打印,证明platform device和ft5x06_driver驱动匹配成功了,而这里打印了两遍probe函数是因为驱动还会跟设备树进行匹配,一般情况下只使用设备树这一硬件描述方式,本章学习的platform device这一方式大家稍作了解即可。
然后使用以下命令进行驱动模块的卸载,如下图所示:
rmmod ft5x06_driver.ko
rmmod ft5x06_device.ko
由于没有在remove卸载函数中添加打印相关内容,所以使用rmmod命令卸载驱动之后,没有任何打印,至此,platform device I2C驱动实验就完成了。