搜索
bottom↓
回复: 12

一个较完善的字符设备驱动模板

[复制链接]

出0入4汤圆

发表于 2020-8-14 20:22:59 | 显示全部楼层 |阅读模式
// 背景
最近学习字符设备驱动,其大致的框架与流程都基本搞懂了,为了方便以后代码重用,写了一个较为完善的模板,
以后如果需要写如: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目录,这就是自动生成的设备文件
然后测试代码测试,如下:

至此,字符设备驱动学习告一段落!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

阿莫论坛20周年了!感谢大家的支持与爱护!!

月入3000的是反美的。收入3万是亲美的。收入30万是移民美国的。收入300万是取得绿卡后回国,教唆那些3000来反美的!

出0入0汤圆

发表于 2020-8-15 01:41:46 来自手机 | 显示全部楼层
可以,作为模版,建议增加ioctl,毕竟现实应用中ioctl用的比readwrite用的多,如果要完善点,再增加上中断和workqueue,应该能覆盖80%的应用了

出0入4汤圆

 楼主| 发表于 2020-8-16 13:35:38 | 显示全部楼层
wx-ta 发表于 2020-8-15 01:41
可以,作为模版,建议增加ioctl,毕竟现实应用中ioctl用的比readwrite用的多,如果要完善点,再增加上中断 ...

好的,感谢,等我弄懂了那部分就加上去

出0入4汤圆

 楼主| 发表于 2020-8-20 19:36:24 | 显示全部楼层
//增加ioctl与lseek函数
#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>
#include <linux/ioctl.h>

/* 字符设备数量、名称、以及缓存大小 */
#define CHRDEV_CNT                1
#define CHRDEV_NAME                "chrdev"
#define BUF_SIZE                256

#define CHRDEV_MAGIC        'k'                //指令类型
#define CHRDEV_MAXNR        3                //最大指令序号
/* ioctl 自定义指令 */                       
#define CMD_OPEN                (_IO(CHRDEV_MAGIC, 1))
#define CMD_CLOSE                (_IO(CHRDEV_MAGIC, 2))
#define CMD_SET                        (_IO(CHRDEV_MAGIC, 3))

/* 自定义字符设备结构体 */
struct chr_dev {
        dev_t devnum;                        //设备号
        struct cdev *pcdev;                //cdev
        struct class *class;        //类
        struct device *device;        //设备
        char   kbuf[BUF_SIZE];        //数据存储区
};

struct         chr_dev firdev = {
        .devnum = 0,
};
/*
  * @brief  文件打开函数
  * @param  inode : 传递给驱动的inode
                        file  : 要打开的设备文件       
  * @retval 0 成功, 其他 失败
  */
static int chrdev_open(struct inode *inode, struct file *file)
{
        file->private_data = &firdev;                 //设置私有数据
        firdev.devnum = inode->i_rdev;                //获取设备号
        printk(KERN_INFO "chrdev_open,devnum:%x\n", firdev.devnum);
        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 *filp, char __user *ubuf, size_t size, loff_t *ppos)
{
        int ret = -1;
        unsigned long p =  *ppos;
        unsigned int count = size;
        struct chr_dev *dev = filp->private_data;        //获取私有数据
        //有效长度判断
         if (p >= BUF_SIZE)
                return 0;
        if (count > BUF_SIZE - p)
                count = BUF_SIZE - p;
        //一定要用如下拷贝函数, 从内核空间拷贝至用户
        if(copy_to_user(ubuf, (void*)(dev->kbuf + p), count)) {
                printk(KERN_ERR "copy_to_user fail\n");
                return -EINVAL;
        }
        else {
                *ppos += count;
                ret = count;
                printk(KERN_INFO "to user success...\n");
        }       
        return ret;
}
/*
  * @brief  写函数, 将应用层传递过来的数据复制到内核中
  * @param  filp: 打开的文件描述符
                        buf : 要写给设备写入的数据
                        cnt : 要写入的数据长度
                        offt: 相对于文件首地址的偏移       
  * @retval 写入的字节数, 负值表示写入失败
  */
static ssize_t chrdev_write(struct file *filp, const char __user *ubuf, size_t size, loff_t *ppos)
{
        int ret = -1;
        unsigned long p =  *ppos;
        unsigned int count = size;
        struct chr_dev *dev = filp->private_data;
        //有效长度判断
        if(p >= BUF_SIZE)
                return 0;
        if (count > BUF_SIZE - p)
                count = BUF_SIZE - p;
        //一定要用如下拷贝函数,从用户空间拷贝至内核
        if (copy_from_user(dev->kbuf + p, ubuf, count)) {
                printk(KERN_ERR "copy_from_user fail\n");
                return -EINVAL;
        }
        else {
                *ppos += count;
                ret = count;
                printk(KERN_INFO "from user success...\n");
        }
        return ret;
}
/*
  * @brief  文件定位函数
  * @param  filp: 打开的文件描述符
                        whence: 偏移起始位置
                        offset: 偏移的步数
  * @retval 偏移的字节数, 负值表示偏移失败
  */
static loff_t chrdev_llseek(struct file *filp, loff_t whence, int offset)
{
        unsigned int newpos = 0;

        switch(whence) {
                case SEEK_SET: newpos = offset;                                 break;
                case SEEK_CUR: newpos = filp->f_pos + offset;         break;
                case SEEK_END: newpos = BUF_SIZE -1 + offset;        break;
                default: return -EINVAL;
        }
        if ((newpos < 0) || (newpos > BUF_SIZE))
                return -EINVAL;
       
        filp->f_pos = newpos;
       
        return newpos;
}
/*
  * @brief  IO控制函数
  * @param  filp: 打开的文件描述符
                        cmd: 命令
                        arg: 参数
  * @retval 0表示执行成功, 负值表示失败
  */
long chrdev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
        if (_IOC_TYPE(cmd) != CHRDEV_MAGIC)
                return -EINVAL;
          if (_IOC_NR(cmd) > CHRDEV_MAXNR)
                return -EINVAL;

        switch(cmd) {
                case CMD_OPEN:
                        printk("IO open device!\n");
                        return 0;
                case CMD_CLOSE:
                        printk("IO close device!\n");
                        return 0;
                case CMD_SET:
                        printk("IO setup device, arg:%d\n", arg);
                        return 0;
                default:
                        return -EINVAL;
        }
        return 0;
}
//文件操作结构体, 外部操作此模块的接口, 需要我们填充
//.owner:指向拥有这个结构的模块的指针,用来在它的操作还在被使用时阻止模块被卸载
static struct file_operations chrdev_fops = {
        .owner                 = THIS_MODULE,
        //应用层间接调用的就是如下接口
        .open                = chrdev_open,                //打开设备时调用
        .release        = chrdev_release,       
        .write                 = chrdev_write,
        .read                = chrdev_read,
        .llseek                = chrdev_llseek,
        .unlocked_ioctl = chrdev_ioctl,
};

//模块加载函数
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 chrdev_fail; //如果执行到这里, 可以直接返回
        }
        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 cdev_fail;//如果执行到这里, 说明前面设备号分配成功了, 需要释放掉
        }
        printk(KERN_INFO "cdev_add success\n");
       
        // 3.创建类
        // 注册字符设备驱动完成后, 添加设备类的操作, 让内核帮我们发信息
        // 给udev,让udev自动创建和删除设备文件
        firdev.class = class_create(THIS_MODULE, CHRDEV_NAME);
        if (IS_ERR(firdev.class)) {
                goto class_fail;//如果执行到这里, 说明设备号与cdev都分配成功了, 需要释放掉
        }
       
        // 4.创建设备
        // 最后1个参数字符串,就是我们将来要在/dev目录下创建的设备文件的名字
        // 所以我们这里要的文件名是/dev/firdev
        firdev.device = device_create(firdev.class, NULL, firdev.devnum, NULL, CHRDEV_NAME);
        if (IS_ERR(firdev.device)) {
                goto device_fail;
        }
        return 0;

device_fail:
        device_destroy(firdev.class, firdev.devnum);
class_fail:
        cdev_del(firdev.pcdev);
cdev_fail:
        unregister_chrdev_region(firdev.devnum, CHRDEV_CNT);
chrdev_fail:       
        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");                                // 模块别名

出100入101汤圆

发表于 2020-8-20 19:44:19 来自手机 | 显示全部楼层
高手,学习

出0入4汤圆

 楼主| 发表于 2020-8-20 19:53:31 | 显示全部楼层

惭愧,我是菜鸟里边最菜的一个

出0入8汤圆

发表于 2020-8-20 23:54:39 来自手机 | 显示全部楼层
高手,这是基于什么开发版学的。

出0入0汤圆

发表于 2020-8-21 08:49:09 | 显示全部楼层
正点不是已经在不定期截取段落发帖了嘛?而且楼主这内容他们pdf里都有啊,而且描述更清晰。没别的意思,别多想,祝早日学成!

出0入4汤圆

 楼主| 发表于 2020-8-21 09:14:57 | 显示全部楼层
zhongsandaoren 发表于 2020-8-21 08:49
正点不是已经在不定期截取段落发帖了嘛?而且楼主这内容他们pdf里都有啊,而且描述更清晰。没别的意思,别 ...

我有他们的板子,也看了他们资料,是有参考的,不过看了之后感觉理解的并不够深,也看了其它很多资料,但是只有自己整理出来了,理顺了思路,才是自己的...

出0入4汤圆

 楼主| 发表于 2020-8-21 09:19:26 | 显示全部楼层
justdomyself 发表于 2020-8-20 23:54
高手,这是基于什么开发版学的。

手头有好多板子了,友善的2440、ITOP-4412、原子的iMx6U、瑞芯微RK3399,不过大部分都在吃灰,目前主要用原子的

出0入8汤圆

发表于 2020-8-21 20:01:11 | 显示全部楼层
batou 发表于 2020-8-21 09:19
手头有好多板子了,友善的2440、ITOP-4412、原子的iMx6U、瑞芯微RK3399,不过大部分都在吃灰,目前主要用 ...

你已经入门了,我还在门外观望,感觉不会linux是个硬伤

出0入4汤圆

 楼主| 发表于 2020-8-25 08:41:23 | 显示全部楼层
justdomyself 发表于 2020-8-21 20:01
你已经入门了,我还在门外观望,感觉不会linux是个硬伤

加油,赶紧用起来,里面的代码跟机制写的真的好

出0入8汤圆

发表于 2020-8-25 10:06:34 | 显示全部楼层
batou 发表于 2020-8-25 08:41
加油,赶紧用起来,里面的代码跟机制写的真的好

没有那个工作环境 很难学进去的。
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-4-25 23:59

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

快速回复 返回顶部 返回列表