目录
一、字符设备驱动简介
二、字符设备驱动开发步骤
1.确定设备号
2.定义 file_operations 结构体
3.实现操作函数
4.注册和注销字符设备
5.编译和加载模块
6.用户空间交互:
三、字符设备驱动示例模板
四、字符设备驱动开发总结
一、字符设备驱动简介
字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、 IIC、 SPI,LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。
在详细的学习字符设备驱动架构之前,我们先来简单的了解一下 Linux 下的应用程序是如何调用驱动程序的, Linux 应用程序对驱动程序的调用流程如下图所示:
在 Linux 中一切皆为文件,驱动加载成功以后会在“/dev”目录下生成一个相应的文件,应用程序通过对这个名为“/dev/xxx” (xxx 是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。
比如现在有个叫做/dev/led 的驱动文件,此文件是 led 灯的驱动文件。应用程序使用 open 函数来打开文件/dev/led,使用完成以后使用 close 函数关闭/dev/led 这个文件。 open和 close 就是打开和关闭 led 驱动的函数,如果要点亮或关闭 led,那么就使用 write 函数来操作,也就是向此驱动写入数据,这个数据就是要关闭还是要打开 led 的控制参数。如果要获取led 灯的状态,就用 read 函数从驱动中读取相应的状态。
应用程序运行在用户空间,而 Linux 驱动属于内核的一部分,因此驱动运行于内核空间。当我们在用户空间想要实现对内核的操作,比如使用 open 函数打开/dev/led 这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入” 到内核空间,这样才能实现对底层驱动的操作。
open、 close、 write 和 read 等这些函数是由 C 库提供的,在 Linux 系统中,系统调用作为 C 库的一部分。当我们调用 open 函数的时候流程如下图所示:
其中关于 C 库以及如何通过系统调用“陷入” 到内核空间这个我们不用去管,我们重点关注的是应用程序和具体的驱动,应用程序使用到的函数在具体驱动程序中都有与之对应的函数,比如应用程序中调用了 open 这个函数,那么在驱动程序中也得有一个名为 open 的函数。每一个系统调用,在驱动中都有与之对应的一个驱动函数,在 Linux 内核文件 include/linux/fs.h 中有个叫做 file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合,内容如下所示:
/* file_operations 结构体 */
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iopoll)(struct kiocb *kiocb, struct io_comp_batch *,
unsigned int flags);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
unsigned long mmap_supported_flags;
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
struct file *file_out, loff_t pos_out,
loff_t len, unsigned int remap_flags);
int (*fadvise)(struct file *, loff_t, loff_t, int);
int (*uring_cmd)(struct io_uring_cmd *ioucmd, unsigned int issue_flags);
int (*uring_cmd_iopoll)(struct io_uring_cmd *, struct io_comp_batch *,
unsigned int poll_flags);
}
在字符设备驱动开发中最常用的就是上面这些函数,关于其他的函数大家可以查阅相关文档。我们在字符设备驱动开发中最主要的工作就是实现上面这些函数,不一定全部都要实现,但是像 open、 release、 write、 read 等都是需要实现的,当然了,具体需要实现哪些函数还是要看具体的驱动要求。
二、字符设备驱动开发步骤
下面是开发字符设备驱动的基本步骤:
1.确定设备号
-
如果使用静态分配设备号,可以在代码中指定一个固定的设备号。
-
如果使用动态分配设备号,可以调用 'alloc_chrdev_region' 函数在模块初始化时分配设备号。
-
如果使用 'udev' 等工具进行设备号的动态分配和管理,则需要在驱动程序中声明一个 'dev_t' 类型的变量,并使用 'MKDEV' 宏将主设备号和次设备号合并为一个设备号。
2.定义 file_operations 结构体
-
创建一个结构体,用于定义字符设备驱动程序对外提供的操作接口。常见的函数包括 'open'、'release'、'read'、'write'、'ioctl' 等。
-
在驱动程序的初始化函数中,将这些操作函数与对应的函数指针关联起来。
3.实现操作函数
-
实现字符设备驱动中定义的操作函数,根据设备的需求来进行相应的操作。
-
在 'open' 函数中可以进行设备的初始化工作,例如分配内存、初始化设备状态等。
-
在 'release' 函数中可以进行设备的清理工作,例如释放内存、关闭设备等。
-
在 'read' 函数中可以从设备读取数据,并将数据传递给用户空间。
-
在 'write' 函数中可以接收用户空间传递的数据,并将数据写入设备。
-
在 'ioctl' 函数中可以处理设备的特殊控制命令。
4.注册和注销字符设备
-
在驱动程序的初始化函数中,调用 'alloc_chrdev_region' 函数分配设备号。
-
使用 'cdev_init' 初始化 'cdev' 结构体,并将 file_operations 结构体指针赋值给 'cdev' 的成员。
-
使用 'cdev_add' 将设备添加到内核中,使其可用。
-
在驱动程序的退出函数中,使用 'cdev_del' 和 'unregister_chrdev_region' 函数注销设备。
5.编译和加载模块
-
将驱动程序的源代码编译为内核模块,生成对应的 .ko 文件。
-
使用 'insmod' 命令加载模块到内核中。
6.用户空间交互:
-
用户程序可以通过系统调用来访问字符设备,例如使用 'open'、'read'、'write'、'ioctl' 等函数来打开、读取和写入设备。
-
用户程序可以使用文件描述符来标识打开的设备,通过文件描述符进行读写操作。
以上是字符设备驱动开发的基本步骤。在实际开发中,还需要进行错误处理、设备管理、内存分配和释放等工作,具体的实现细节会根据设备的特性和需求而有所不同。为了开发出高质量的驱动程序,建议仔细阅读相关的文档、示例代码和最佳实践,并进行充分的测试和验证。
三、字符设备驱动示例模板
字符驱动框架是一种在Linux内核中实现设备驱动程序的方法。它允许开发者编写基于字符设备的驱动程序,以便与用户空间中的字符设备进行通信。下面是一个简单的字符驱动框架模板,包括了必要的函数和数据结构。
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "mydevice"
#define BUF_SIZE 1024
static dev_t dev;
static struct cdev cdev;
static char buffer[BUF_SIZE];
static int buffer_len = 0;
static int device_open(struct inode *inode, struct file *filp)
{
// 设备打开时的操作
return 0;
}
static int device_release(struct inode *inode, struct file *filp)
{
// 设备关闭时的操作
return 0;
}
static ssize_t device_read(struct file *filp, char *user_buf, size_t count, loff_t *f_pos)
{
// 从设备读取数据
size_t to_copy = min(count, (size_t)buffer_len);
if (copy_to_user(user_buf, buffer, to_copy) != 0)
return -EFAULT;
return to_copy;
}
static ssize_t device_write(struct file *filp, const char *user_buf, size_t count, loff_t *f_pos)
{
// 向设备写入数据
size_t to_copy = min(count, (size_t)BUF_SIZE);
if (copy_from_user(buffer, user_buf, to_copy) != 0)
return -EFAULT;
buffer_len = to_copy;
return to_copy;
}
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = device_open,
.release = device_release,
.read = device_read,
.write = device_write,
};
static int __init chardev_init(void)
{
// 模块初始化函数
if (alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME) < 0)
{
printk(KERN_ALERT "Failed to allocate character device region\n");
return -1;
}
cdev_init(&cdev, &fops);
if (cdev_add(&cdev, dev, 1) < 0)
{
unregister_chrdev_region(dev, 1);
printk(KERN_ALERT "Failed to add character device\n");
return -1;
}
printk(KERN_INFO "Character device registered: %s\n", DEVICE_NAME);
return 0;
}
static void __exit chardev_exit(void)
{
// 模块退出函数
cdev_del(&cdev);
unregister_chrdev_region(dev, 1);
printk(KERN_INFO "Character device unregistered\n");
}
module_init(chardev_init);
module_exit(chardev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Character Device Driver");
这个模板定义了一个名为 'mydevice' 的字符设备驱动。驱动程序会将用户空间的数据写入到 'buffer' 中,并从 'buffer' 中读取数据返回给用户空间。其中,'device_open' 和 'device_release' 函数在设备打开和关闭时被调用,'device_read' 和 'device_write' 函数用于读取和写入设备数据。
该模板使用 'alloc_chrdev_region' 函数为字符设备分配主次设备号,并使用 'cdev_init' 和 'cdev_add' 函数将设备添加到系统中。在模块退出时,使用 'cdev_del' 和 'unregister_chrdev_region' 函数来注销设备。
请注意,这只是一个简单的字符驱动框架模板,仅用于演示目的。实际的字符驱动可能需要更多的功能和错误处理。在开发字符驱动程序时,请仔细参考Linux内核文档和示例代码,以确保正确性和稳定性。
四、字符设备驱动开发总结
字符驱动是一种在操作系统内核中实现的设备驱动程序,用于与字符设备进行交互。字符设备是一种以字节为单位进行输入和输出的设备,例如终端、串口、打印机等。字符驱动框架提供了一组函数和数据结构,使得开发者可以编写自定义的字符设备驱动程序。
在Linux内核中,字符驱动的实现基于以下几个核心组件:
1.设备号:每个字符设备驱动在注册时都会被分配一个唯一的设备号。设备号包括主设备号和次设备号。主设备号标识设备驱动程序,次设备号标识具体的设备实例。
2.file_operations 结构体:这是一个函数指针结构体,定义了设备驱动程序对外提供的操作接口。常见的函数包括 'open'、'release'、'read'、'write'、'ioctl' 等。开发者需要实现这些函数来处理设备的打开、关闭、读取和写入等操作。
3.cdev 结构体:'cdev' 是字符设备驱动的核心结构体,它代表一个字符设备实例。它包含了对应的 file_operations 结构体指针以及设备号等信息。通过使用 'cdev' 结构体,开发者可以注册和管理字符设备。
4.字符设备注册和注销:在字符驱动的初始化阶段,需要使用 'alloc_chrdev_region' 函数为驱动程序分配设备号。然后使用 'cdev_init' 初始化 'cdev' 结构体,并使用 'cdev_add' 将设备添加到系统中。在驱动程序退出时,需要使用 'cdev_del' 和 'unregister_chrdev_region' 函数来注销设备。
5.用户空间交互:字符驱动允许用户空间程序通过系统调用来访问设备。用户程序可以打开设备、读取和写入设备数据,并通过 'ioctl' 等方式发送控制命令。
关于更多嵌入式C语言、FreeRTOS、RT-Thread、Linux应用编程、linux驱动等相关知识,关注公众号【嵌入式Linux知识共享】,后续精彩内容及时收看了解。