当前位置:首页 > 嵌入式 > 嵌入式分享
[导读]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 )。
关闭