假如你现在赖在床上,你可能会完成一个梦。假如你现在起来做事,你可能会完成一个梦想。生命太短,时光太长,活着,并高兴着,才是幸福所向。拥有一份好的心态,活出自己,高兴一天是一天!
----小新
一.字符设备驱动框架
1.设备号
字符设备驱动需要申请唯一的设备号,以便系统能够正确识别和访问设备。设备号可以通过调用 alloc_chrdev_region
或 register_chrdev_region
函数来申请。
struct cdev {
//描述字符设备的一个结构体
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
MAJOR(dev_t dev);
MINOR(dev_t dev);
dev_t 获得主设备号和次设备号:而使用下列宏则可以通过主设备号和次设备号生成 dev_t。
MKDEV(int major, int minor);
2.文件操作函数
字符设备驱动需要实现一系列文件操作函数,例如 open
、read
、write
、release
等,以便应用程序能够访问设备。这些函数需要在驱动的 fops
结构中声明。
open
:打开设备文件时会被调用,可以在此函数中进行设备的初始化操作。read
:读取设备文件数据时会被调用,需要实现从设备中读取数据的操作。write
:向设备文件写入数据时会被调用,需要实现向设备中写入数据的操作。release
:关闭设备文件时会被调用,可以在此函数中进行设备的清理操作。
例子:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "my_chardev"
#define DEVICE_MAJOR 240
#define DEVICE_MINOR 0
static int my_chardev_open(struct inode *inode, struct file *file)
{
// 打开设备文件时的操作,例如进行设备的初始化
return 0;
}
static int my_chardev_release(struct inode *inode, struct file *file)
{
// 关闭设备文件时的操作,例如进行设备的清理
return 0;
}
static ssize_t my_chardev_read(struct file *file, char __user *buf, size_t count, loff_t *offset)
{
// 读取设备文件的操作,从设备中读取数据并返回给用户空间
return 0;
}
static ssize_t my_chardev_write(struct file *file, const char __user *buf, size_t count, loff_t *offset)
{
// 向设备文件写入数据的操作,从用户空间读取数据并写入到设备中
return 0;
}
static const struct file_operations my_chardev_fops = {
.owner = THIS_MODULE,
.open = my_chardev_open,
.read = my_chardev_read,
.write = my_chardev_write,
.release = my_chardev_release,
};
static int __init my_chardev_init(void)
{
int ret;
ret = register_chrdev(DEVICE_MAJOR, DEVICE_NAME, &my_chardev_fops);
if (ret < 0) {
printk(KERN_ERR "Failed to register character device\n");
return ret;
}
return 0;
}
static void __exit my_chardev_exit(void)
{
unregister_chrdev(DEVICE_MAJOR, DEVICE_NAME);
}
3.中断处理
如果字符设备驱动需要处理中断,则需要实现中断处理函数 irq_handler_callback
,并在驱动注册时将中断号和回调函数传递给系统。
- 定义中断号:在驱动注册时,需要将中断号和中断处理函数传递给系统。可以使用
request_irq
函数来申请中断号。 - 中断处理函数:中断处理函数需要快速处理中断事件,并返回处理结果。在字符设备驱动中,中断处理函数通常会通过读取设备寄存器来获取中断事件,并进行相应的处理。
- 中断上下文:中断上下文是指中断处理函数运行时的环境,包括中断号、CPU 状态、栈等信息。在字符设备驱动中,可以通过
irq_handler_ctx
结构来访问中断上下文。 - 中断共享:如果多个设备共享同一个中断号,则需要实现中断共享。可以使用
share_irq
宏来声明中断共享。
static irqreturn_t my_chardev_irq_handler(int irq, void *dev_id)
{
// 中断处理函数,读取设备寄存器获取中断事件,并进行相应的处理
return IRQ_HANDLED;
}
static int __init my_chardev_init(void)
{
int ret;
ret = register_chrdev(DEVICE_MAJOR, DEVICE_NAME, &my_chardev_fops);
if (ret < 0) {
printk(KERN_ERR "Failed to register character device\n");
return ret;
}
ret = request_irq(IRQ_NUM, my_chardev_irq_handler, IRQF_SHARED, DEVICE_NAME, NULL);
if (ret < 0) {
printk(KERN_ERR "Failed to request interrupt\n");
unregister_chrdev(DEVICE_MAJOR, DEVICE_NAME);
return ret;
}
return 0;
}
4.内存映射
如果字符设备驱动需要支持内存映射,则需要实现 mmap
函数。该函数用于将设备内存映射到用户空间,以便应用程序可以直接访问设备内存。
5.电源管理
如果字符设备驱动需要支持电源管理,则需要实现相应的电源管理回调函数,例如 suspend
和 resume
。这些函数用于在系统进入和退出电源管理状态时控制设备的电源状态。
6.调试和测试
编写字符设备驱动时需要注意调试和测试。可以使用 printk 函数打印调试信息,或使用 kgdb 调试器进行调试。同时,需要进行充分的测试以确保驱动的正确性和稳定性。
二.字符设备驱动框架代码
1.驱动代码:
编写chrdev.c文件 :
/*
* @Descripttion: 注册字符设备
* @version:
* @Author: topeet
*/
#include <linux/init.h> //初始化头文件
#include <linux/module.h> //最基本的文件,支持动态添加和卸载模块。
#include <linux/fs.h> //包含了文件操作相关 struct 的定义,例如大名鼎鼎的
struct file_operations
#include <linux/kdev_t.h>
#include <linux/cdev.h> // 对字符设备结构 cdev 以及一系列的操作函数的定义。包含了 cdev 结构及相关函数的定义。
#define DEVICE_NUMBER 1 //定义次设备号的个数
#define DEVICE_SNAME "schrdev" //定义静态注册设备的名称
#define DEVICE_ANAME "achrdev" //定义动态注册设备的名称
#define DEVICE_MINOR_NUMBER 0 //定义次设备号的起始地址
static int major_num, minor_num; //定义主设备号和次设备号
struct cdev cdev; //定义一个 cdev 结构体
module_param(major_num, int, S_IRUSR); //驱动模块传入普通参数 major_num
module_param(minor_num, int, S_IRUSR); //驱动模块传入普通参数 minor_num
int chrdev_open(struct inode *inode, struct file *file)
{
printk("chrdev_open\n");
return 0;
}
// file_operations chrdev_ops
struct file_operations chrdev_ops = {
.owner = THIS_MODULE,
.open = chrdev_open};
static int hello_init(void)
{
dev_t dev_num;
int ret; //函数返回值
if (major_num)
{
/*静态注册设备号*/
printk("major_num = %d\n", major_num); //打印传入进来的主设备号
printk("minor_num = %d\n", minor_num); //打印传入进来的次设备号
dev_num = MKDEV(major_num, minor_num); //MKDEV 将主设备号和次设备号合并为一个设备号
ret = register_chrdev_region(dev_num, DEVICE_NUMBER, DEVICE_SNAME); //注册设备号
if (ret < 0)
{
printk("register_chrdev_region error\n");
}
printk("register_chrdev_region ok\n"); //静态注册设备号成功
}
else
{
/*动态注册设备号*/
ret = alloc_chrdev_region(&dev_num, DEVICE_MINOR_NUMBER, 1, DEVICE_ANAME);
if (ret < 0)
{
printk("alloc_chrdev_region error\n");
}
printk("alloc_chrdev_region ok\n"); //动态注册设备号成功
major_num = MAJOR(dev_num); //将主设备号取出来
minor_num = MINOR(dev_num); //将次设备号取出来
printk("major_num = %d\n", major_num); //打印传入进来的主设备号
printk("minor_num = %d\n", minor_num); //打印传入进来的次设备号
}
cdev.owner = THIS_MODULE;
//cdev_init 函数初始化 cdev 结构体成员变量
cdev_init(&cdev, &chrdev_ops);
//完成字符设备注册到内核
cdev_add(&cdev, dev_num, DEVICE_NUMBER);
return 0;
}
static void hello_exit(void)
{
//注销设备号
unregister_chrdev_region(MKDEV(major_num, minor_num), DEVICE_NUMBER); cd
ev_del(&cdev);
printk("gooodbye! \n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
编写Makefile文件:
obj-m += chrdevr.o #先写生成的中间文件的名字是什么,-m 的意思是把我们的驱动编译成模块
KDIR:= /home/topeet/driver/imx6ull/linux-imx-rel_imx_4.1.15_2.1.0_ga/
PWD?=$(shell pwd) #获取当前目录的变量
all:
make -C $(KDIR) M=$(PWD) modules #make 会进入内核源码的路径,然后把当前路径下的代码编译成
模块
把chrdev.c和Makefile文件放在同一目录下,然后cd进去,执行make命令就会生成chrdev.ko文件。然后安装驱动:insmod chrdev.ko
(1) 结果如下
2.app代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
int fd;
char buf[64] = {0};
fd = open("/dev/test",O_RDWR); //打开设备节点
if(fd < 0)
{
perror("open error \n");
return fd;
}
//read(fd,buf,sizeof(buf)); //从文件中读取数据放入缓冲区中
close(fd);
return 0;
}
(1)结果如下