最近学习字符设备驱动,其大致的框架与流程都基本搞懂了,为了方便以后代码重用,写了一个较为完善的模板,
以后如果需要写如:led、key、beep等的字符设备驱动,就不需要从O开始,可以直接用来修改调试,
代码中有比较清晰的注释,以及错误处理与回收机制,并且经过了初步测试,是通过的
该模板的作用:编译生成.ko文件后,通过insmod *.ko加载模块,会自动在/dev目录下生成相应的设备文件,模板中设备文件名称为firdev,
同样,卸载模块时会自动将其删除,这主要是device与class这两个结构体的功劳,代码中都有注释...
// 驱动模板
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#define CHRDEV_CNT 1
#define CHRDEV_NAME "firdev"
/* 自定义字符设备结构体 */
struct chr_dev {
dev_t devnum; //设备号
struct cdev *pcdev; //cdev
struct class *class; //类
struct device *device; //设备
};
struct chr_dev firdev;
char kernel_buf[512];
/*
* @brief 文件打开函数
* @param inode : 传递给驱动的inode
file : 要打开的设备文件
* @retval 0 成功, 其他 失败
*/
static int chrdev_open(struct inode *inode, struct file *file)
{
filp->private_data = &firdev; //设置私有数据
printk(KERN_INFO "chrdev_open\n");
return 0;
}
/*
* @brief 文件关闭函数
* @param inode : 传递给驱动的inode
file : 要关闭的设备文件
* @retval 0 成功, 其他 失败
*/
static int chrdev_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "chrdev_release\n");
return 0;
}
/*
* @brief 读函数, 将内核中的数据拷贝到应用层
* @param file: 要打开的设备文件
buf : 返回给用户空间的数据缓冲区
cnt : 要读取的数据长度
offt: 相对于文件首地址的偏移
* @retval 读取的字节数, 负值表示读取失败
*/
ssize_t chrdev_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
{
int ret = -1;
printk(KERN_INFO "chrdev_read\n");
//一定要用如下拷贝函数, 不信你试试别的
ret = copy_to_user(ubuf, kernel_buf, count);
if (ret) {
printk(KERN_ERR "copy_to_user fail\n");
return -EINVAL;
}
printk(KERN_INFO "copy_to_user success...:%s\n", kernel_buf);
return 0;
}
/*
* @brief 写函数, 将应用层传递过来的数据复制到内核中
* @param filp: 打开的文件描述符
buf : 要写给设备写入的数据
cnt : 要写入的数据长度
offt: 相对于文件首地址的偏移
* @retval 写入的字节数, 负值表示写入失败
*/
static ssize_t chrdev_write(struct file *file, const char __user *ubuf, size_t count, loff_t *ppos)
{
int ret = -1;
printk(KERN_INFO "chrdev_write\n");
//一定要用如下拷贝函数, 不信你试试别的
ret = copy_from_user(kernel_buf, ubuf, count);
if (ret) {
printk(KERN_ERR "copy_from_user fail\n");
return -EINVAL;
}
printk(KERN_INFO "copy_from_user success...:%s\n", kernel_buf);
return 0;
}
//文件操作结构体, 外部操作此模块的接口, 需要我们填充
//.owner:指向拥有这个结构的模块的指针,用来在它的操作还在被使用时阻止模块被卸载
static struct file_operations chrdev_fops = {
.owner = THIS_MODULE,
//应用层间接调用的就是如下接口
.open = chrdev_open, //打开设备时调用
.release = chrdev_release, //关闭设备时调用
.write = chrdev_write, //写设备操作
.read = chrdev_read, //读设备操作
};
static int __init chrdev_init(void)
{
int ret;
// 1.分配设备号
ret = alloc_chrdev_region(&firdev.devnum, 0, CHRDEV_CNT, CHRDEV_NAME);
if(ret < 0) {
printk(KERN_ERR "alloc_chrdev_region fail\n");
goto tag1; //如果执行到这里, 可以直接返回
}
printk(KERN_INFO "major = %d, minor = %d.\n", MAJOR(firdev.devnum), MINOR(firdev.devnum));
// 2.初始化cdev, 并添加到系统
// cdev绑定file_operations与dev_t, 添加进系统从而产生了联系
firdev.pcdev = cdev_alloc();
//firdev.pcdev->owner = THIS_MODULE;
//firdev.pcdev->ops = &chrdev_fops; //将cdev和file_operations进行绑定
cdev_init(firdev.pcdev, &chrdev_fops);//这条语句可代替上面两条语句
ret = cdev_add(firdev.pcdev, firdev.devnum, CHRDEV_CNT);//将cdev结构体加入到系统中去
if (ret) {
printk(KERN_ERR "Unable to cdev_add\n");
goto tag2;//如果执行到这里, 说明前面设备号分配成功了, 需要释放掉
}
printk(KERN_INFO "cdev_add success\n");
// 3.创建类
// 注册字符设备驱动完成后, 添加设备类的操作, 让内核帮我们发信息
// 给udev,让udev自动创建和删除设备文件
firdev.class = class_create(THIS_MODULE, CHRDEV_NAME);
if (IS_ERR(firdev.class)) {
goto tag3;//如果执行到这里, 说明设备号与cdev都分配成功了, 需要释放掉
}
// 4.创建设备
// 最后1个参数字符串,就是我们将来要在/dev目录下创建的设备文件的名字
// 所以我们这里要的文件名是/dev/firdev
firdev.device = device_create(firdev.class, NULL, firdev.devnum, NULL, CHRDEV_NAME);
if (IS_ERR(firdev.device)) {
goto tag3;
}
return 0;
tag3:
cdev_del(firdev.pcdev);
tag2:
unregister_chrdev_region(firdev.devnum, CHRDEV_CNT);
tag1:
return -EINVAL;
}
//模块卸载函数, 注销要跟创建时倒着来
static void __exit chrdev_exit(void)
{
// 销毁设备,即把创建的设备文件删掉
device_destroy(firdev.class, firdev.devnum);
// 销毁类,释放资源
class_destroy(firdev.class);
// 注销字符设备驱动结构
cdev_del(firdev.pcdev);
// 然后注销申请到的设备号
unregister_chrdev_region(firdev.devnum, CHRDEV_CNT);
}
//模块加载与卸载时会调用如下接口
module_init(chrdev_init);
module_exit(chrdev_exit);
//下面这些都是跟模块相关, 需要加上才能编译
MODULE_LICENSE("GPL"); // 模块许可证
MODULE_AUTHOR("author"); // 模块作者
MODULE_DESCRIPTION("description"); // 模块信息
MODULE_ALIAS("alias"); // 模块别名
// 测试代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#define FILE "/dev/firdev"
int main(void)
{
int fd = -1;
int i = 0;
char buf[64];
fd = open(FILE, O_RDWR);
if (fd < 0) {
printf("open %s fail:%d\n", FILE, fd);
return -1;
}
printf("open %s ok\n", FILE);
while(1) {
memset(buf, 0, sizeof(buf));
printf("input>>");
scanf("%s", buf);
if(strstr(buf, "write")) {
char *p = strstr(buf, ":");
write(fd, p+1, strlen(p));
printf("len:%d\n", strlen(p));
}
else if(!strcmp(buf, "read")) {
memset(buf, 0, sizeof(buf));
read(fd, buf, sizeof(buf));
printf("read str:%s\n", buf);
}
else if(!strcmp(buf, "clear"))
write(fd, 0, 1);
else if(!strcmp(buf, "quit"))
break;
}
}
// 测试结果
将驱动模板编译生成.ko文件,我的是first_drv.ko,拷贝至开发板
然后执行:insmod first_drv.ko
root@ALIENTEK-IMX6U:/mnt/ttt# insmod first_drv.ko
major = 249, minor = 0.
cdev_add success
说明模块加载成功,cat /proc/devices可看到firdev驱动,并且ls /dev可看到firdev目录,这就是自动生成的设备文件
然后测试代码测试,如下: