当前位置:首页 > 嵌入式 > 嵌入式分享
[导读]Linux驱动开发中,字符设备是最基础也最常用的类型——它像文件一样通过open/read/write/close与用户空间交互。很多嵌入式工程师在写第一个驱动时,往往卡在设备号的申请、cdev的注册以及数据拷贝的细节上。本文以一个虚拟字符设备"mydev"为例,从头到尾走通注册流程和数据交互。


Linux驱动开发中,字符设备是最基础也最常用的类型——它像文件一样通过open/read/write/close与用户空间交互。很多嵌入式工程师在写第一个驱动时,往往卡在设备号的申请、cdev的注册以及数据拷贝的细节上。本文以一个虚拟字符设备"mydev"为例,从头到尾走通注册流程和数据交互。

一、设备号与cdev注册

字符设备通过主设备号和次设备号标识。现代内核推荐动态分配主设备号,避免冲突。

#include <linux/module.h>

#include <linux/fs.h>

#include <linux/cdev.h>

#include <linux/uaccess.h>


#define DEVICE_NAME "mydev"

#define CLASS_NAME  "myclass"


static int major;

static struct class *my_class;

static struct cdev my_cdev;


// 设备结构体(存放私有数据)

struct mydev_data {

   char buffer[256];

   size_t len;

};

static struct mydev_data dev_data;


static int __init my_init(void) {

   dev_t dev_num;


   // 1. 动态分配设备号

   alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);

   major = MAJOR(dev_num);


   // 2. 创建设备类(用于udev自动创建设备节点)

   my_class = class_create(THIS_MODULE, CLASS_NAME);

   device_create(my_class, NULL, dev_num, NULL, DEVICE_NAME);


   // 3. 初始化cdev并添加到内核

   cdev_init(&my_cdev, &my_fops);  // my_fops在后面定义

   my_cdev.owner = THIS_MODULE;

   cdev_add(&my_cdev, dev_num, 1);


   // 初始化设备数据

   memset(dev_data.buffer, 0, sizeof(dev_data.buffer));

   dev_data.len = 0;


   printk(KERN_INFO "mydev: registered with major %d\n", major);

   return 0;

}

class_create和device_create让用户空间的udev在/dev/下自动生成设备节点,省去手动mknod的麻烦。

二、file_operations实现

字符驱动的核心是填充struct file_operations,告诉内核当用户调用open/read/write/release时该做什么。

static int my_open(struct inode *inode, struct file *filp) {

   filp->private_data = &dev_data;  // 将设备数据关联到文件指针

   return 0;

}


static ssize_t my_read(struct file *filp, char __user *buf,

                      size_t count, loff_t *f_pos) {

   struct mydev_data *data = filp->private_data;

   size_t available = data->len - *f_pos;

   size_t to_copy = min(count, available);


   if (to_copy == 0) return 0;  // EOF


   // 从内核空间拷贝到用户空间

   if (copy_to_user(buf, data->buffer + *f_pos, to_copy)) {

       return -EFAULT;

   }


   *f_pos += to_copy;

   return to_copy;

}


static ssize_t my_write(struct file *filp, const char __user *buf,

                       size_t count, loff_t *f_pos) {

   struct mydev_data *data = filp->private_data;

   size_t to_copy = min(count, sizeof(data->buffer));


   // 从用户空间拷贝到内核空间

   if (copy_from_user(data->buffer, buf, to_copy)) {

       return -EFAULT;

   }


   data->len = to_copy;

   *f_pos = 0;  // 简单实现,每次写重置位置

   return to_copy;

}


static int my_release(struct inode *inode, struct file *filp) {

   return 0;

}


static struct file_operations my_fops = {

   .owner   = THIS_MODULE,

   .open    = my_open,

   .read    = my_read,

   .write   = my_write,

   .release = my_release,

};

关键点:

copy_to_user和copy_from_user必须使用,绝不能直接用memcpy——内核空间不能直接访问用户指针,否则会引发缺页异常或安全问题。

filp->private_data是连接文件实例和设备数据的桥梁,每个open都可以有自己的上下文。

返回值表示实际传输的字节数,错误返回负值(如-EFAULT)。

三、模块卸载

卸载时需要逆序清理:

static void __exit my_exit(void) {

   dev_t dev_num = MKDEV(major, 0);

   device_destroy(my_class, dev_num);

   class_destroy(my_class);

   cdev_del(&my_cdev);

   unregister_chrdev_region(dev_num, 1);

   printk(KERN_INFO "mydev: unregistered\n");

}


module_init(my_init);

module_exit(my_exit);

MODULE_LICENSE("GPL");

MODULE_AUTHOR("Your Name");

四、用户空间测试

编译驱动后insmod mydev.ko,在/dev/mydev出现。可以用简单的C程序测试:

int fd = open("/dev/mydev", O_RDWR);

write(fd, "Hello driver!", 13);

char buf[256];

read(fd, buf, sizeof(buf));

printf("Read: %s\n", buf);

close(fd);

五、三个翻车高发点

忘记copy_to_user返回值的检查:返回非零表示拷贝失败,必须返回-EFAULT,否则用户空间读到垃圾数据。

设备节点未自动创建:如果class_create/device_create没写,需要手动mknod /dev/mydev c major 0。推荐用udev自动管理。

open时不设置private_data:后续read/write中filp->private_data为NULL,访问即崩溃。务必在open中赋值。

字符设备驱动是Linux驱动的入门必修课。掌握设备号分配、cdev注册、file_operations实现和用户空间数据拷贝,就能应对大多数简单的硬件交互需求。在此基础上,可以进一步学习阻塞IO、poll机制、ioctl等高级特性。



本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
换一批
延伸阅读

作 者:道哥,10年嵌入式开发老兵,专注于:C/C、嵌入式、Linux。关注下方公众号,回复【书籍】,获取Linux、嵌入式领域经典书籍;回复【PDF】,获取所有原创文章(PDF格式)。目录API函数编写驱动程序编写应用...

关键字: 编写 驱动程序 字符设备驱动

作 者:道哥,10年嵌入式开发老兵,专注于:C/C、嵌入式、Linux。关注下方公众号,回复【书籍】,获取Linux、嵌入式领域经典书籍;回复【PDF】,获取所有原创文章(PDF格式)。目录混乱的API函数旧的API函数...

关键字: 驱动程序 字符设备驱动

从这篇博文开始,我将开始手把手教会大家写Linux设备驱动程序 这是开篇,如何来写第一个字符设备驱动程序。 首先,写一个最简单的字符设备驱动程序需要什么?或者说我们需要了解什么? 1、每一个字符设备至少需要有一个设备号...

关键字: Linux 驱动程序 字符设备驱动

上节,我们讲解了如何写第一个Linux字符设备驱动程序,这节,我们将代码做一下修改。 如下: #include #include #include #include #include #include #in

关键字: Linux 驱动程序 字符设备驱动

前面,我们基本已经学会怎么去编写一个简单的字符设备驱动程序了,这节,我们来看看友善之臂中提供的led驱动。 参考之前写的文章,我们已经知道LED的GPIO口,和一些配置信息: http://blog.csdn.NET/m...

关键字: 字符设备驱动 驱动程序 tiny

    前面我发的那些文章写的LED,按键,蜂鸣器这些驱动统称为字符设备驱动,那么今天我们就来看看字符设备驱动的特点和如何来进行开发: 一、linux系统将设备分为3类:字符设备、块设备、网络设备。使用驱动程序: 1、字...

关键字: 字符设备驱动

Linux驱动:封装对底层硬件的操作,向上层应用提供操作接口 一. 概念介绍 一般用户在应用程序里调用的 open, read, write 函数是 c 库的函数, 这些函数会触发

关键字: linux字符设备 字符设备驱动

设备驱动程序可以使用模块的方式动态加载到内核中去。加载模块的方式与以往的应用程序开发有很大的不同。以往在开发应用程序时都有一个main()函数作为程序的入口点,而在驱动开发时却没有main()函数,模块在调用insmod...

关键字: 基础教程 字符设备驱动 嵌入式linux 操作系统 设备驱动

在linux系统中,很多驱动是字符型驱动,有些是直接编译集成在内核中,另一些是单独编译成“.ko”动态加载的。其实字符驱动只是个外壳,用于内核与应用程序间通信

关键字: 模型 字符设备驱动 CD STRUCT
关闭