Linux字符设备驱动开发

旧模板在2.3小节。

新模版在5.3小节。

应用程序和驱动的交互原理

驱动就是获取外设或者传感器数据,控制外设。数据会提交给应用程序。Linux驱动编写既要编写一个驱动,还要编写一个简单的测试应用程序,APP。

单片机下驱动和应用都是放在一个文件里面,杂糅到一起。Linux下驱动和应用是完全分开的。

用户空间和内核空间

Linux操作系统内核和驱动程序运行在内核空间,应用程序运行在用户空间。

应用程序想要访问内核资源:系统调用、异常、陷入。

应用程序使用open函数打开一个设备文件。

应用程序通过API函数来间接的调用系统调用。每一个系统调用都有一个系统调用号。

一、字符设备驱动简介

字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。

  • Linux里面一切皆文件,驱动设备表现就是一个/dev/下的文件,如/dev/led,应用程序调用open函数打开一个设备的时候,比如led。应用程序通过write函数向/dev/led写数据。关闭设备close。
  • 编写驱动的时候,需要编写驱动的open、close、write函数。字符设备驱动struct file_operations。
  • 写驱动的时候要考虑应用开发的便利性。驱动分框架,要按照驱动框架来编写。

二、字符设备驱动开发步骤

2.1 驱动模块的加载和卸载

Linux驱动运行方式:

  • 编译进Linux内核,内核启动自动运行
  • 编译成模块(.ko),通过modprobe或insmod加载

注:

  • insmod不能解决模块的依赖关系,比如 drv.ko 依赖 first.ko 这个模块,就必须先使用insmod 命令加载 first.ko 这个模块,然后再加载 drv.ko 这个模块。
  • modprobe会自动分析模块的依赖关系,默认回去/lib/modules/<kernel-version>查找模块。

卸载命令:rmmod drv.ko;modeprobe -r drv

在编写驱动的时候需要注册加载和卸载函数:

cpp 复制代码
//字符设备驱动模块加载和卸载函数模板
/* 驱动入口函数 */
static int __init xxx_init(void)
{
    /* 入口函数具体内容 */
    return 0;
}

/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
    /* 出口函数具体内容 */
}

module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数

2.2 字符设备注册与注销

cpp 复制代码
static inline int register_chrdev(
    unsigned int major,//主设备号
    const char *name,//设备名字
    const struct file_operations *fops//指向设备的操作函数集合变量
)

static inline void unregister_chrdev(
    unsigned int major,//主设备号
    const char *name//设备名
)


//加入字符设备注册和注销

static struct file_operations test_fops;

/* 驱动入口函数 */
static int __init xxx_init(void)
{
    /* 入口函数具体内容 */
    int retvalue = 0;

    /* 注册字符设备驱动 */
    retvalue = register_chrdev(200, "chrtest", &test_fops);
    if(retvalue < 0){
        /* 字符设备注册失败,自行处理 */
    }
    return 0;
}

/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
    /* 注销字符设备驱动 */
    unregister_chrdev(200, "chrtest");
}

/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);

2.3实现设备的具体操作函数

旧API模板

cpp 复制代码
//加入设备操作函数
#define XXX_MAJOR		200		/* 主设备号 */
#define XXX_NAME		"xxx" 	/* 设备名字 */

/* 打开设备 */
static int xxx_open(struct inode *inode, struct file *filp)
{
    /* 用户实现具体功能 */
    return 0;
}

/* 从设备读取 */
static ssize_t xxx_read(struct file *filp, char __user *buf,
    size_t cnt, loff_t *offt)
{
    /* 用户实现具体功能 */
    return 0;
}

/* 向设备写数据 */
static ssize_t xxx_write(struct file *filp,
    const char __user *buf,
    size_t cnt, loff_t *offt)
{
    /* 用户实现具体功能 */
    return 0;
}

/* 关闭/释放设备 */
static int xxx_release(struct inode *inode, struct file *filp)
{
    /* 用户实现具体功能 */
    return 0;
}

static struct file_operations xxx_fops = {
    .owner = THIS_MODULE,
    .open = xxx_open,
    .read = xxx_read,
    .write = xxx_write,
    .release = xxx_release,
};

/* 驱动入口函数 */
static int __init xxx_init(void)
{
    /* 入口函数具体内容 */
    int retvalue = 0;

    /* 注册字符设备驱动 */
    retvalue = register_chrdev(XXX_MAJOR, XXX_NAME, &xxx_fops);
    if(retvalue < 0){
        /* 字符设备注册失败,自行处理 */
    }
    return 0;
}

/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
    /* 注销字符设备驱动 */
    unregister_chrdev(XXX_MAJOR, XXX_NAME);
}

/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);

MODULE_LICENSE() //添加模块 LICENSE 信息。"GPL",采用GPL协议
MODULE_AUTHOR() //添加模块作者信息
MODULE_INFO(intree, "Y");

2.4 添加LICENSE和作者信息

cpp 复制代码
MODULE_LICENSE() //添加模块 LICENSE 信息。"GPL",采用GPL协议
MODULE_AUTHOR() //添加模块作者信息

三、Linux设备号

3.1 设备号的组成

cpp 复制代码
typedef u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;

高 12 位为主设备号,低 20 位为次设备号


include/linux/kdev_t.h:

    #define MINORBITS 20
    #define MINORMASK ((1U << MINORBITS) - 1)

    #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
    #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
    #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))

3.2 设备好的分配

  • 静态分配设备号:驱动开发者静态指定的设备号(查看文档 Documentation/devices.txt)
cpp 复制代码
int register_chrdev_region(dev_t from, unsigned count, const char *name)
  • 动态分配设备号:避免设备号冲突。
cpp 复制代码
int alloc_chrdev_region(
    dev_t *dev, //保存申请到的设备号
    unsigned baseminor, //次设备号起始地址
    unsigned count, //要申请的设备号数量
    const char *name //设备名字
)

void unregister_chrdev_region(
    dev_t from, //要释放的设备号
    unsigned count //表示从from开始,要释放的设备号数量
)

四、chrdevbase 字符设备驱动开发实验

4.1 实验程序编写

  1. 新建Linux_Drivers文件夹
  2. 创建01_chrdevbase的VSCode工程
  3. 添加头文件路径(Ctrl+Shift+P,Edit configurations,在includePath添加头文件路径)
  4. 编写实验程序

4.2 编写测试APP

open/close/write/read参见Linux-应用编程学习笔记(二、文件I/O、标准I/O)

通过调用open/close/write/read来控制设备。

捋顺一下思路:驱动文件编译完成之后是.KO文件,通过modeprobe加载到/dev里面,比如设备叫chrdevbase,在APP中open(/dev/chrdevbase),进而调用驱动里面的open函数。

4.3 编译驱动程序和测试APP

编译驱动程序

cpp 复制代码
KERNELDIR := /home/zuozhongkai/linux/RV1126/alientek_sdk/kernel #KERNELDIR 表示开发板所使用的 Linux 内核源码目录,使用绝对路径
CURRENT_PATH := $(shell pwd) #通过运行"pwd"命令来获取当前所处路径
obj-m := chrdevbase.o

build: kernel_modules

kernel_modules:
    $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules # modules 表示编译模块, -C 表示将当前的工作目录切换到指定目录中,也就是 KERNERLDIR 目录。 M 表示模块源码目录,"make modules"命令中加入 M=dir 以后程序会自动到指定的 dir 目录中读取模块的源码并将其编译为.ko 文件。
clean:
    $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean




//Makefile编写好后,编译驱动模块
make ARCH=arm //ARCH=arm 必须指定,否则编译会失败

编译测试APP

cpp 复制代码
/opt/atk-dlrv1126-toolchain/bin/arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp

4.4 运行测试

1、使用 ADB 将驱动模块和测试 APP 发送到开发板

cpp 复制代码
adb push chrdevbase.ko chrdevbaseApp /lib/modules/4.19.111

2、加载驱动模块

cpp 复制代码
modprobe chrdevbase

//报错找不到"modules.dep"文件
depmod 
//没有depmod命令的重新配置busybox,使能此命令,重新编译busybox

3、创建设备节点文件

cpp 复制代码
mknod /dev/chrdevbase c 200 0

4、chrdevbase 设备操作测试

cpp 复制代码
./chrdevbaseApp /dev/chrdevbase 1
./chrdevbaseApp /dev/chrdevbase 2

5、卸载驱动模块

cpp 复制代码
rmmod chrdevbase

五、新字符设备驱动

5.1 分配和释放设备号

cpp 复制代码
如果没有指定设备号的话就使用如下函数来申请设备号:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
如果给定了设备的主设备号和次设备号就使用如下所示函数来注册设备号即可:
int register_chrdev_region(dev_t from, unsigned count, const char *name)

unregister_chrdev_region(devid, 1); /* 注销设备号 */

5.2 新字符设备注册方法

cpp 复制代码
struct cdev {
    struct kobject kobj;
    struct module *owner;
    const struct file_operations *ops;
    struct list_head list;
    dev_t dev;
    unsigned int count;
} __randomize_layout;


void cdev_init(struct cdev *cdev, const struct file_operations *fops)
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
void cdev_del(struct cdev *p)

5.3 自动创建设备节点

为了省去modprobe加载驱动后,手动mknod创建设备节点。

udev机制:udev使用用户程序,开发板启动会启动udev,实现设备节点文件的创建与删除。

创建和删除类

自动创建设备节点的工作是在驱动程序的入口函数 中完成的,一般在 cdev_add 函数后面

加自动创建设备节点相关代码。

cpp 复制代码
struct class * class_create(owner, name);
void class_destroy(struct class *cls);

创建设备

cpp 复制代码
struct device *device_create(
    struct class *class, //设备创建到哪个类
    struct device *parent, //父设备,没有NULL
    dev_t devt, //设备号
    void *drvdata, //数据,NULL
    const char *fmt, //设备名字,xxx->/dev/xxx 
    ...
)

void device_destroy(struct class *cls, dev_t devt)


//创建/删除类/设备参考代码
struct class *class; /* 类 */
struct device *device; /* 设备 */
dev_t devid; /* 设备号 */

/* 驱动入口函数 */
static int __init xxx_init(void)
{
    /* 创建类 */
    class = class_create(THIS_MODULE, "xxx");
    /* 创建设备 */
    device = device_create(class, NULL, devid, NULL, "xxx");
    return 0;
}

/* 驱动出口函数 */
static void __exit led_exit(void)
{
    /* 删除设备 */
    device_destroy(newchrled.class, newchrled.devid);
    /* 删除类 */
    class_destroy(newchrled.class);
}

module_init(led_init);
module_exit(led_exit);

新API模板

cpp 复制代码
#define NEWCHRXXX_CNT			1		  	/* 设备号个数 */
#define NEWCHRXXX_NAME			"newchrxxx"	/* 名字 */

/* newchrled设备结构体 */
struct newchrxxx_dev{
	dev_t devid;			/* 设备号 	 */
	struct cdev cdev;		/* cdev 	*/
	struct class *class;	/* 类 		*/
	struct device *device;	/* 设备 	 */
	int major;				/* 主设备号	  */
	int minor;				/* 次设备号   */
};

struct newchrxxx_dev newchrxxx;	/* xxx设备 */

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int xxx_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &newchrxxx; /* 设置私有数据 */
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t xxx_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    ......
	return 0;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t xxx_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    ......

	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int xxx_release(struct inode *inode, struct file *filp)
{
    ......
	return 0;
}

/* 设备操作函数 */
static struct file_operations newchrxxx_fops = {
	.owner = THIS_MODULE,
	.open = xxx_open,
	.read = xxx_read,
	.write = xxx_write,
	.release = 	xxx_release,
};

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init xxx_init(void)
{
	u32 val = 0;
	int ret;

	/* 注册字符设备驱动 */
	/* 1、创建设备号 */
	if (newchrxxx.major) {		/*  定义了设备号 */
		newchrxxx.devid = MKDEV(newchrxxx.major, 0);
		ret = register_chrdev_region(newchrxxx.devid, NEWCHRXXX_CNT, NEWCHRXXX_NAME);
		if(ret < 0) {
			pr_err("cannot register %s char driver [ret=%d]\n",NEWCHRXXX_NAME, NEWCHRXXX_CNT);
			goto fail_map;
		}
	} else {						/* 没有定义设备号 */
		ret = alloc_chrdev_region(&newchrxxx.devid, 0, NEWCHRXXX_CNT, NEWCHRXXX_NAME);	/* 申请设备号 */
		if(ret < 0) {
			pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n", NEWCHRXXX_NAME, ret);
			goto fail_map;
		}
		newchrxxx.major = MAJOR(newchrxxx.devid);	/* 获取分配号的主设备号 */
		newchrxxx.minor = MINOR(newchrxxx.devid);	/* 获取分配号的次设备号 */
	}
	printk("newchrxxx major=%d,minor=%d\r\n",newchrxxx.major, newchrxxx.minor);	
	
	/* 2、初始化cdev */
	newchrxxx.cdev.owner = THIS_MODULE;
	cdev_init(&newchrxxx.cdev, &newchrxxx_fops);
	
	/* 3、添加一个cdev */
	ret = cdev_add(&newchrxxx.cdev, newchrxxx.devid, NEWCHRXXX_CNT);
	if(ret < 0)
		goto del_unregister;
		
	/* 4、创建类 */
	newchrxxx.class = class_create(THIS_MODULE, NEWCHRXXX_NAME);
	if (IS_ERR(newchrxxx.class)) {
		goto del_cdev;
	}

	/* 5、创建设备 */
	newchrxxx.device = device_create(newchrxxx.class, NULL, newchrxxx.devid, NULL, NEWCHRXXX_NAME);
	if (IS_ERR(newchrxxx.device)) {
		goto destroy_class;
	}
	
	return 0;

destroy_class:
	class_destroy(newchrxxx.class);
del_cdev:
	cdev_del(&newchrxxx.cdev);
del_unregister:
	unregister_chrdev_region(newchrxxx.devid, NEWCHRXXX_CNT);
//fail_map:
	//xxx_unmap();
	//return -EIO;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit led_exit(void)
{
	.....

	/* 注销字符设备驱动 */
	cdev_del(&newchrxxx.cdev);/*  删除cdev */
	unregister_chrdev_region(newchrxxx.devid, NEWCHRXXX_CNT); /* 注销设备号 */

	device_destroy(newchrxxx.class, newchrxxx.devid);
	class_destroy(newchrxxx.class);
}

module_init(xxx_init);
module_exit(xxx_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");
相关推荐
Komorebi.py38 分钟前
【Linux】-学习笔记05
linux·笔记·学习
Mr_Xuhhh44 分钟前
重生之我在学环境变量
linux·运维·服务器·前端·chrome·算法
内核程序员kevin4 小时前
TCP Listen 队列详解与优化指南
linux·网络·tcp/ip
朝九晚五ฺ8 小时前
【Linux探索学习】第十四弹——进程优先级:深入理解操作系统中的进程优先级
linux·运维·学习
自由的dream8 小时前
Linux的桌面
linux
xiaozhiwise9 小时前
Makefile 之 自动化变量
linux
意疏11 小时前
【Linux 篇】Docker 的容器之海与镜像之岛:于 Linux 系统内探索容器化的奇妙航行
linux·docker
BLEACH-heiqiyihu11 小时前
RedHat7—Linux中kickstart自动安装脚本制作
linux·运维·服务器
一只爱撸猫的程序猿11 小时前
一个简单的Linux 服务器性能优化案例
linux·mysql·nginx