字符设备驱动开发

好的!以下是对字符设备驱动开发的更详细分步指南,包含完整的代码示例、Makefile 配置和测试方法。

---

### **字符设备驱动开发详细步骤**

#### **1. 驱动代码框架(`my_char_driver.c`)**
```c
#include <linux/module.h>
#include <linux/fs.h>          // 文件操作结构体 file_operations
#include <linux/cdev.h>        // 字符设备结构体 cdev
#include <linux/device.h>      // 设备类相关函数
#include <linux/uaccess.h>     // copy_to_user/copy_from_user

#define DEVICE_NAME "my_char_dev"
#define CLASS_NAME "my_char_class"

static int major_num;
static struct class *char_class = NULL;
static struct cdev my_cdev;

// 自定义设备数据结构(示例:包含一个缓冲区)
struct my_device_data {
    char buffer[1024];
    size_t buffer_len;
};
static struct my_device_data dev_data;

//---------------- 文件操作函数实现 -----------------
static int my_open(struct inode *inode, struct file *filp) {
    printk(KERN_INFO "Device opened\n");
    return 0;
}

static int my_release(struct inode *inode, struct file *filp) {
    printk(KERN_INFO "Device closed\n");
    return 0;
}

static ssize_t my_read(struct file *filp, char __user *buf, size_t len, loff_t *off) {
    struct my_device_data *data = &dev_data;

    // 检查是否已经读完
    if (*off >= data->buffer_len) 
        return 0;

    // 计算剩余可读数据长度
    size_t remaining = data->buffer_len - *off;
    len = min(len, remaining);

    // 将内核缓冲区数据复制到用户空间
    if (copy_to_user(buf, data->buffer + *off, len)) 
        return -EFAULT;

    *off += len;  // 更新偏移量
    return len;
}

static ssize_t my_write(struct file *filp, const char __user *buf, size_t len, loff_t *off) {
    struct my_device_data *data = &dev_data;

    // 检查写入是否超出缓冲区大小
    if (*off + len > sizeof(data->buffer)) 
        return -ENOMEM;

    // 将用户空间数据复制到内核缓冲区
    if (copy_from_user(data->buffer + *off, buf, len)) 
        return -EFAULT;

    *off += len;
    data->buffer_len = *off;  // 更新缓冲区长度
    return len;
}

// 定义文件操作结构体
static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = my_open,
    .release = my_release,
    .read = my_read,
    .write = my_write,
};

//---------------- 模块初始化和退出 -----------------
static int __init my_char_init(void) {
    dev_t dev;

    // 1. 动态申请主设备号
    if (alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME) < 0) {
        printk(KERN_ALERT "Failed to allocate device number\n");
        return -1;
    }
    major_num = MAJOR(dev);

    // 2. 初始化并注册字符设备
    cdev_init(&my_cdev, &fops);
    if (cdev_add(&my_cdev, dev, 1) < 0) {
        unregister_chrdev_region(dev, 1);
        printk(KERN_ALERT "Failed to add cdev\n");
        return -1;
    }

    // 3. 创建设备类(用于自动创建设备节点)
    char_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(char_class)) {
        cdev_del(&my_cdev);
        unregister_chrdev_region(dev, 1);
        printk(KERN_ALERT "Failed to create class\n");
        return PTR_ERR(char_class);
    }

    // 4. 创建设备节点(自动生成 /dev/my_char_dev)
    device_create(char_class, NULL, dev, NULL, DEVICE_NAME);
    printk(KERN_INFO "Device registered with major number %d\n", major_num);
    return 0;
}

static void __exit my_char_exit(void) {
    dev_t dev = MKDEV(major_num, 0);

    // 销毁设备节点和类
    device_destroy(char_class, dev);
    class_destroy(char_class);

    // 注销字符设备
    cdev_del(&my_cdev);
    unregister_chrdev_region(dev, 1);
    printk(KERN_INFO "Device unregistered\n");
}

module_init(my_char_init);
module_exit(my_char_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A Simple Character Device Driver");

2. 编写 Makefile

obj-m += my_char_driver.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

3. 编译并加载驱动

# 编译模块
make

# 加载驱动
sudo insmod my_char_driver.ko

# 查看设备号
cat /proc/devices | grep my_char_dev

# 检查设备节点是否自动创建
ls -l /dev/my_char_dev  # 若未自动创建,手动创建:
# sudo mknod /dev/my_char_dev c <major_num> 0
# sudo chmod 666 /dev/my_char_dev

4. 测试驱动

  • 写入数据到设备
    echo "Hello, Linux Driver!" > /dev/my_char_dev
    
  • 从设备读取数据
    cat /dev/my_char_dev  # 输出 "Hello, Linux Driver!"
    
  • 查看内核日志
    dmesg | tail -n 10
    # 应显示 "Device opened", "Device closed" 等日志
    

5. 关键概念详解

  1. 设备号分配

    • alloc_chrdev_region:动态分配主设备号,避免与系统设备冲突。
    • MKDEV(major, minor):生成设备号(主设备号 + 次设备号)。
  2. 文件操作接口

    • open:设备打开时调用(可初始化资源)。
    • release:设备关闭时调用(释放资源)。
    • read/write:必须使用 copy_to_user/copy_from_user 在内核和用户空间传递数据。
  3. 自动创建设备节点

    • class_create:创建设备类(在 /sys/class/ 生成目录)。
    • device_create:在 /dev/ 自动生成设备节点,无需手动 mknod

6. 常见问题解决

  • 权限不足
    sudo chmod 666 /dev/my_char_dev  # 允许普通用户读写
    
  • 模块加载失败
    • 检查 dmesg 输出,常见原因:函数未实现、符号未导出。
  • 写入后读取为空
    • 确保在 write 函数中更新缓冲区长度(如 dev_data.buffer_len)。

7. 扩展功能

  • 添加 ioctl 控制

    #include <linux/ioctl.h>
    #define MY_IOCTL_CMD _IOR('k', 1, int)  // 定义 ioctl 命令
    
    static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
        switch (cmd) {
            case MY_IOCTL_CMD:
                printk(KERN_INFO "ioctl command received\n");
                break;
            default:
                return -ENOTTY;
        }
        return 0;
    }
    
    // 在 file_operations 中添加:
    .unlocked_ioctl = my_ioctl,
    
  • 支持多设备

    • alloc_chrdev_region 中指定次设备号数量(如 alloc_chrdev_region(&dev, 0, 2, DEVICE_NAME))。
    • cdev_add 中注册多个设备。

通过以上代码和步骤,你可以实现一个完整的字符设备驱动,并通过用户空间程序与其交互。建议从简单功能开始,逐步添加复杂逻辑(如并发控制、阻塞IO等)。

ubuntu@ubuntu:~/cstudy/chardevice$ vim my_char_driver.c
ubuntu@ubuntu:~/cstudy/chardevice$ vim Makefile
ubuntu@ubuntu:~/cstudy/chardevice$ make
make -C /lib/modules/5.15.0-138-generic/build M=/home/ubuntu/cstudy/chardevice modules
make[1]: Entering directory '/usr/src/linux-headers-5.15.0-138-generic'
CC [M] /home/ubuntu/cstudy/chardevice/my_char_driver.o
/home/ubuntu/cstudy/chardevice/my_char_driver.c: In function ‘my_read’:
/home/ubuntu/cstudy/chardevice/my_char_driver.c:40:5: warning: ISO C90 forbids mixed declarations and code [-Wdeclaration-after-statement]
40 | size_t remaining = data->buffer_len - *off;
| ^~~~~~
MODPOST /home/ubuntu/cstudy/chardevice/Module.symvers
CC [M] /home/ubuntu/cstudy/chardevice/my_char_driver.mod.o
LD [M] /home/ubuntu/cstudy/chardevice/my_char_driver.ko
BTF [M] /home/ubuntu/cstudy/chardevice/my_char_driver.ko
Skipping BTF generation for /home/ubuntu/cstudy/chardevice/my_char_driver.ko due to unavailability of vmlinux
make[1]: Leaving directory '/usr/src/linux-headers-5.15.0-138-generic'
ubuntu@ubuntu:~/cstudy/chardevice$ sudo insmod my_char_driver.ko
[sudo] password for ubuntu:
ubuntu@ubuntu:~/cstudy/chardevice$ cat /proc/devices | grep my_char_dev
238 my_char_dev
ubuntu@ubuntu:~/cstudy/chardevice$ ls -l /dev/my_char_dev
crw------- 1 root root 238, 0 Apr 26 12:29 /dev/my_char_dev
ubuntu@ubuntu:~/cstudy/chardevice$ echo "Hello, Linux Driver!" > /dev/my_char_dev
-bash: /dev/my_char_dev: Permission denied
ubuntu@ubuntu:~/cstudy/chardevice$ sudo echo "Hello, Linux Driver!" > /dev/my_char_dev
-bash: /dev/my_char_dev: Permission denied
ubuntu@ubuntu:~/cstudy/chardevice$ sudo chmod 666 /dev/my_char_dev
ubuntu@ubuntu:~/cstudy/chardevice$ echo "Hello, Linux Driver!" > /dev/my_char_dev
ubuntu@ubuntu:~/cstudy/chardevice$ cat /dev/my_char_dev
Hello, Linux Driver!
ubuntu@ubuntu:~/cstudy/chardevice$ dmesg | tail -n 10
dmesg: read kernel buffer failed: Operation not permitted
ubuntu@ubuntu:~/cstudy/chardevice$ sudo dmesg | tail -n 10
[ 1529.175921] clocksource: wd-tsc-wd read-back delay of 201000ns, clock-skew test skipped!
[ 1839.997249] clocksource: timekeeping watchdog on CPU3: hpet wd-wd read-back delay of 358000ns
[ 1839.997262] clocksource: wd-tsc-wd read-back delay of 261000ns, clock-skew test skipped!
[ 1954.852763] Device registered with major number 238
[ 2135.640308] clocksource: timekeeping watchdog on CPU0: hpet wd-wd read-back delay of 185000ns
[ 2135.640320] clocksource: wd-tsc-wd read-back delay of 178000ns, clock-skew test skipped!
[ 2217.541106] Device opened
[ 2217.541164] Device closed
[ 2239.696820] Device opened
[ 2239.697123] Device closed

posted @ 2025-04-26 20:37  奔付山河  阅读(47)  评论(0)    收藏  举报