目的
前面文章 《嵌入式Linux驱动开发 01:基础开发与使用》 和 《嵌入式Linux驱动开发 02:将驱动程序添加到内核中》 介绍了驱动开发最基础的内容,这篇文章将在前面基础上更进一步,引入平台(platform)总线驱动模型。
这篇文章中内容均在下面的开发板上进行测试:
《新唐NUC980使用记录:自制开发板(基于NUC980DK61YC)》
这篇文章是在下面文章基础上进行的:
《新唐NUC980使用记录(5.10.y内核):在用户应用中使用GPIO》
基础说明
在前面的文章中只是最简单的驱动编写和使用的逻辑,并没有涉及到真正需要驱动的外设等。实际开发中驱动程序是通常是为了操作有些外设的,你可以在前面的基础上在驱动程序中通过寄存器或者厂家提供的库(如果有的话)来操作外设。
通常对于同一类型的外设通常除了寄存器或引脚等位置不同外,功能都是一样的。所以通常会把驱动的功能逻辑和具体的寄存器或引脚等数据分开来,分成 driver
和 device
两部分。 driver
是真正驱动逻辑部分,而 device
用于存放设备寄存器或引脚等信息。这两部分信息通过标签来关联。
这两部分比较低级的组织方式就是全部以 .c
或者 .h
文件的形式来组织,放在内核工程中一起编译,这就是平台( platform
)总线模型。这种方式下如果具体使用的外设的位置和引脚等有变化的话只需要修改 device
部分代码文件即可。
平台总线驱动模型中不管是驱动还是资源都是一个内核模块。作为资源的模块中定义并注册 platform_device
内容,作为驱动的模块中定义并注册 platform_driver
。两者通过一定的字符串进行匹配。一个驱动可以匹配多个资源来创建多个设备节点。
开发准备
本文中演示中涉及目录与文件结构初始组织如下:
进入源码目录:
cd ~/nuc980-sdk/NUC980-linux-5.10.y/
在源码目录下建立相关目录和Kconfig和Makefile参考 《嵌入式Linux驱动开发 02:将驱动程序添加到内核中》 这个篇文章,需要改动的内容如下:
drivers/user/char_dev
目录下的 Makefile
文件,其内容改为如下:
obj-$(CONFIG_USER_CHAR_DEV) += char_dev.o
obj-$(CONFIG_USER_CHAR_DEV) += char_drv.o
新增 char_drv.c
文件:
touch drivers/user/char_dev/char_drv.c
在驱动中获取资源
这里演示一个驱动和一个资源的情况,两者代码分别如下:
platform_device(char_dev.c)
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/platform_device.h>
/* 定义一个资源 */
static struct resource resources[] = {
{
.start = 22,
.end = 33,
.name = "naisu",
.flags = IORESOURCE_BITS, // 这个宏定义在 include/linux/ioport.h 中,还有更多选项可选
// .flags这个字段用于标识资源类型
// 使用platform_get_resource方法获取资源时就会通过该字段进行筛选
.desc = 666,
},
};
// static void char_dev_release(struct device *dev)
// {
// printk("NX modlog: file %s, func %s, line %d.\n", __FILE__, __FUNCTION__, __LINE__);
// }
/* 定义platform_device,绑定资源 */
static struct platform_device char_dev = {
.name = "naisu_char_dev", // 资源名称
.num_resources = ARRAY_SIZE(resources),
.resource = resources,
// .dev = {
// .release = char_dev_release,
// },
};
/* 模块加载操作 */
static int __init char_dev_init(void)
{
int err;
printk("NX modlog: file %s, func %s, line %d.\n", __FILE__, __FUNCTION__, __LINE__);
err = platform_device_register(&char_dev); // 注册platform_device
return err;
}
/* 模块退出操作 */
static void __exit char_dev_exit(void)
{
printk("NX modlog: file %s, func %s, line %d.\n", __FILE__, __FUNCTION__, __LINE__);
platform_device_unregister(&char_dev); // 释放资源
}
module_init(char_dev_init); // 模块入口
module_exit(char_dev_exit); // 模块出口
MODULE_LICENSE("GPL"); // 模块许可
platform_driver(char_drv.c)
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/platform_device.h>
static int major = 0;
static const char *char_drv_name = "char_drv";
static struct class *char_drv_class;
static struct device *char_drv_device;
/* 探测到资源操作 */
static int char_probe(struct platform_device *pdev)
{
struct resource *res;
printk("NX modlog: file %s, func %s, line %d.\n", __FILE__, __FUNCTION__, __LINE__);
res = &pdev->resource[0];
printk("NX modlog: %d %d %s %ld %ld\n", res->start, res->end, res->name, res->flags, res->desc);
return 0;
}
/* 移除资源操作 */
static int char_remove(struct platform_device *pdev)
{
printk("NX modlog: file %s, func %s, line %d.\n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}
/* 定义platform_driver,用于探测和获取资源等 */
static struct platform_driver char_driver = {
.probe = char_probe,
.remove = char_remove,
.driver = {
.name = "naisu_char_dev", // 通过资源名称进行探测匹配
},
};
/* 驱动文件操作接口集合 */
static const struct file_operations char_drv_fops = {
.owner = THIS_MODULE,
};
/* 模块加载操作 */
static int __init char_drv_init(void)
{
int err;
printk("NX modlog: file %s, func %s, line %d.\n", __FILE__, __FUNCTION__, __LINE__);
major = register_chrdev(0, char_drv_name, &char_drv_fops); // 注册字符设备,第一个参数0表示让内核自动分配主设备号
char_drv_class = class_create(THIS_MODULE, "char_drv_class");
if (IS_ERR(char_drv_class))
{
unregister_chrdev(major, char_drv_name);
return -1;
}
char_drv_device = device_create(char_drv_class, NULL, MKDEV(major, 0), NULL, char_drv_name); // 创建设备节点创建设备节点,成功后就会出现/dev/char_drv_name的设备文件
if (IS_ERR(char_drv_device))
{
device_destroy(char_drv_class, MKDEV(major, 0));
unregister_chrdev(major, char_drv_name);
return -1;
}
err = platform_driver_register(&char_driver); // 注册platform_driver
return err;
}
/* 模块退出操作 */
static void __exit char_drv_exit(void)
{
printk("NX modlog: file %s, func %s, line %d.\n", __FILE__, __FUNCTION__, __LINE__);
platform_driver_unregister(&char_driver); // 释放platform_driver
device_destroy(char_drv_class, MKDEV(major, 0)); // 销毁设备节点,销毁后/dev/下设备节点文件就会删除
class_destroy(char_drv_class);
unregister_chrdev(major, char_drv_name); // 注销字符设备
}
module_init(char_drv_init); // 模块入口
module_exit(char_drv_exit); // 模块出口
MODULE_LICENSE("GPL"); // 模块许可
编译测试
在内核配置界面启用自定义的驱动:
make menuconfig
编译然后拷贝到开发板上进行测试:
# 设置编译工具链
# export ARCH=arm; export CROSS_COMPILE=arm-buildroot-linux-gnueabi-
# export PATH=$PATH:/home/nx/nuc980-sdk/buildroot-2023.02/output/host/bin
# 编译生成内核镜像
make uImage
# 可以根据电脑配置使用make -jx等加快编译速度
# 编译完成后拷贝到电脑上再拷贝到SD卡中
# sudo cp arch/arm/boot/uImage /media/sf_common/
# 我这里开发环境和开发板在同一局域网中,所以可以直接通过网络将文件拷贝到开发板上
# 在开发板中挂载boot分区
# mount /dev/mmcblk0p1 /mnt/
# 在ubuntu中使用scp命令拷贝dtb文件到开发板上
# scp arch/arm/boot/uImage root@192.168.31.142:/mnt/
# 拷贝完成后重启开发板即可测试
# reboot
单驱动使用多个资源
拷贝一份资源文件:
cp drivers/user/char_dev/char_dev.c drivers/user/char_dev/char_dev2.c
文件中对资源内容稍作修改:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/platform_device.h>
/* 定义一个资源 */
static struct resource resources[] = {
{
.start = 1234567,
.end = 7654321,
.name = "naisu222",
.flags = IORESOURCE_BITS,
.desc = 777,
},
};
/* 定义platform_device,绑定资源 */
static struct platform_device char_dev = {
.name = "naisu_char_dev2", // 不同的platform_device,该字段不能重复
.num_resources = ARRAY_SIZE(resources),
.resource = resources,
.driver_override = "naisu_char_dev", // 该字段将资源强制与platform_driver.driver.name相同的驱动匹配
};
/* 模块加载操作 */
static int __init char_dev_init(void)
{
int err;
printk("NX modlog: file %s, func %s, line %d.\n", __FILE__, __FUNCTION__, __LINE__);
err = platform_device_register(&char_dev); // 注册platform_device
return err;
}
/* 模块退出操作 */
static void __exit char_dev_exit(void)
{
printk("NX modlog: file %s, func %s, line %d.\n", __FILE__, __FUNCTION__, __LINE__);
platform_device_unregister(&char_dev); // 释放资源
}
module_init(char_dev_init); // 模块入口
module_exit(char_dev_exit); // 模块出口
MODULE_LICENSE("GPL"); // 模块许可
然后修改 drivers/user/char_dev
目录下的 Makefile
文件,其内容改为如下:
obj-$(CONFIG_USER_CHAR_DEV) += char_dev.o
obj-$(CONFIG_USER_CHAR_DEV) += char_dev2.o
obj-$(CONFIG_USER_CHAR_DEV) += char_drv.o
接着修改驱动文件 char_drv.c
:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/platform_device.h>
static int major = 0;
static const char *char_drv_name = "char_drv_";
static struct class *char_drv_class;
static int device_node_minor = 0;
struct device_node {
int minor;
char data[128];
};
static struct device_node device_node_arr[2];
/* 探测到资源操作 */
static int char_probe(struct platform_device *pdev)
{
int minor = device_node_minor;
struct resource *res;
printk("NX modlog: file %s, func %s, line %d.\n", __FILE__, __FUNCTION__, __LINE__);
res = &pdev->resource[0];
printk("NX modlog: %d %d %s %ld %ld\n", res->start, res->end, res->name, res->flags, res->desc);
sprintf(device_node_arr[minor].data, "NX modlog: %d %d %s %ld %ld\n", res->start, res->end, res->name, res->flags, res->desc);
device_create(char_drv_class, NULL, MKDEV(major, minor), NULL, "%s%d", char_drv_name, minor); // 创建设备节点创建设备节点,成功后就会出现/dev/char_drv_name的设备文件
device_node_arr[minor].minor = device_node_minor; // 保存子设备号,供后面卸载使用
platform_set_drvdata(pdev, &device_node_arr[minor]); // 保存节点自定义数据到原始资源上
device_node_minor++;
return 0;
}
/* 移除资源操作 */
static int char_remove(struct platform_device *pdev)
{
struct device_node *node = platform_get_drvdata(pdev); // 获取保存的节点自定义数据
printk("NX modlog: file %s, func %s, line %d.\n", __FILE__, __FUNCTION__, __LINE__);
device_destroy(char_drv_class, MKDEV(major, node->minor)); // 销毁设备节点,销毁后/dev/下设备节点文件就会删除
return 0;
}
/* 定义platform_driver,用于探测和获取资源等 */
static struct platform_driver char_driver = {
.probe = char_probe,
.remove = char_remove,
.driver = {
.name = "naisu_char_dev", // 通过资源名称进行探测匹配
},
};
static int char_drv_open(struct inode *node, struct file *file)
{
return 0;
}
static int char_drv_close(struct inode *node, struct file *file)
{
return 0;
}
static ssize_t char_drv_read(struct file *file, char __user *buf, size_t size, loff_t *offset)
{
int minor = iminor(file_inode(file)); // 获取当前设备节点minor号
// 读取设备数据,可以使用 cat /dev/char_drv_x 来读取
return simple_read_from_buffer(buf, size, offset, device_node_arr[minor].data, strlen(device_node_arr[minor].data));
}
/* 驱动文件操作接口集合 */
static const struct file_operations char_drv_fops = {
.owner = THIS_MODULE,
.open = char_drv_open,
.release = char_drv_close,
.read = char_drv_read,
};
/* 模块加载操作 */
static int __init char_drv_init(void)
{
int err;
printk("NX modlog: file %s, func %s, line %d.\n", __FILE__, __FUNCTION__, __LINE__);
major = register_chrdev(0, char_drv_name, &char_drv_fops); // 注册字符设备,第一个参数0表示让内核自动分配主设备号
char_drv_class = class_create(THIS_MODULE, "char_drv_class");
if (IS_ERR(char_drv_class))
{
unregister_chrdev(major, char_drv_name);
return -1;
}
err = platform_driver_register(&char_driver); // 注册platform_driver
return err;
}
/* 模块退出操作 */
static void __exit char_drv_exit(void)
{
printk("NX modlog: file %s, func %s, line %d.\n", __FILE__, __FUNCTION__, __LINE__);
platform_driver_unregister(&char_driver); // 释放platform_driver
class_destroy(char_drv_class);
unregister_chrdev(major, char_drv_name); // 注销字符设备
}
module_init(char_drv_init); // 模块入口
module_exit(char_drv_exit); // 模块出口
MODULE_LICENSE("GPL"); // 模块许可
修改完成后重新编译内核拷贝测试:
总结
平台总线这种资源和驱动分离的结构是发展到一定阶段必然出现的产物。平台总线本身虽然从代码上面进行了分离,但每次调整资源还是会涉及到内核,所以现在更加流行的是设备树方式。设备树和平台总线内容是相关联的,接下来的文章将在平台总线的基础上介绍基于设备树方式驱动的开发。