Linux驱动开发学习——字符设备

发布于:2025-08-12 ⋅ 阅读:(14) ⋅ 点赞:(0)

基础知识最全Linux驱动开发全流程详细解析(持续更新)-CSDN博客

示例代码等相关解释:豆包

虚拟led单实例驱动开发。

在 Linux 系统中,字符设备是最常见的设备类型之一(如键盘、LED、串口等),其数据传输以字节流形式进行,支持按顺序读写。字符设备驱动程序是操作系统内核与硬件设备(或虚拟设备)之间的接口,负责将用户空间的操作(如 open/read/write)转换为对硬件的具体控制。

1.核心模块

每一个字符设备基本包括:

  • 设备编号:每个字符设备都由一个唯一的 设备编号 标识
  • 文件操作接口:file_operation
  • 核态和用户态的交互:copy_to_user copy_from_user
  • 设备节点:驱动加载后,需在 /dev 目录下创建设备节点(如 /dev/vir_led),用户通过操作该节点与设备交互。设备节点由 mknod 命令或内核自动创建(通过 class_create 和 device_create

2.操作步骤

  1. 设备结构体:设备的私有数据,比如状态 硬件寄存器等。
  2. 文件操作函数:在代码文件中需要实现struct file_operations 中的关键函数,包括:
    • open():初始化设备(如分配资源、复位状态),将设备结构体绑定到 file->private_data
    • release():释放资源(如关闭硬件、释放内存)。
    • read():向用户空间返回设备数据(如 LED 状态)。
    • write():接收用户空间的命令并执行(如控制 LED 开关)。
  3. 注册到内核
    • 分配设备号:通过 alloc_chrdev_region(动态分配)或 register_chrdev_region(静态指定)获取主、次设备号。
    • 初始化注册的字符设备:用 cdev_init 关联设备结构体与文件操作接口,再用 cdev_add 将设备注册到内核。
    • 创建设备节点(用户空间可见):通过 class_create 创建设备类(在 /sys/class 下),再用 device_create 在 /dev 下生成设备节点。
  4. 编写模块初始化与退出函数:

    • module_init:完成设备编号分配、字符设备注册、设备节点创建等工作。
    • module_exit:按与初始化相反的顺序释放资源(避免内存泄漏),包括删除设备节点、注销字符设备、释放设备编号。

3.示例代码

#include <linux/init.h>
#include <linux/module.h>  
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/fs.h>
#include <linux/device.h> 

#define DEVICE_NAME "vir_led"
#define CLASS_NAME "led_class"

struct vir_led {
    struct cdev cdev;
    int status;  // 0: 灭, 1: 亮
};

static struct vir_led led_dev;
static dev_t dev_num;
static struct class* led_class = NULL;
static struct device* led_device = NULL;

// 函数声明
static int led_open(struct inode *inode, struct file *file);
static int led_release(struct inode *inode, struct file *file);
static ssize_t led_read(struct file *file, char __user *buf, size_t count, loff_t *ppos);
static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos);

// 这是一个文件操作结构体,是驱动与用户空间进行交互的接口,就是当用户调用open() read() write() close()等函数时,会调用对应的函数
static struct file_operations led_fops = {
    .owner = THIS_MODULE,  
    .open = led_open,
    .release = led_release,
    .read = led_read,
    .write = led_write   
};

static int led_open(struct inode *inode, struct file *file) {
    file->private_data = &led_dev;
    printk(KERN_INFO "led_open\n");
    return 0;  
}

static int led_release(struct inode *inode, struct file *file) {
    printk(KERN_INFO "led_release\n");
    return 0;
}

static ssize_t led_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) {
    struct vir_led *led = file->private_data;
    char status_buf[32];
    int len;
    ssize_t ret;

    len = sprintf(status_buf, "led status: %s\n", led->status ? "on" : "off");

    // 检查是否已读取完毕
    if (*ppos >= len) {
        return 0;  // 返回0表示读取结束
    }

    // 限制读取长度,不超过剩余数据
    if (count > len - *ppos) {
        count = len - *ppos;
    }

    // 从当前位置拷贝数据到用户空间
    ret = copy_to_user(buf, status_buf + *ppos, count);
    if (ret) {
        return -EFAULT;
    }

    // 更新位置指针
    *ppos += count;

    return count;  // 返回实际读取的字节数
}

static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) {
    struct vir_led *led = file->private_data;
    char cmd_buf[32];

    if (count > sizeof(cmd_buf) - 1) {
        count = sizeof(cmd_buf) - 1;
    }

    if (copy_from_user(cmd_buf, buf, count)) {
        return -EFAULT;
    }
    cmd_buf[count] = '\0';  

    if (strncmp(cmd_buf, "on", 2) == 0) {
        led->status = 1;
        printk(KERN_INFO "Virtual LED turned ON\n");
    } else if (strncmp(cmd_buf, "off", 3) == 0) {
        led->status = 0;
        printk(KERN_INFO "Virtual LED turned OFF\n");
    } else {
        printk(KERN_WARNING "Invalid command: %s\n", cmd_buf);
        return -EINVAL;
    }
    
    // 对于写操作,更新位置指针
    *ppos += count;
    return count;
}

// 初始化函数
static int __init led_init(void) {
    int ret;

    // 1. 动态分配设备号
    ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
    if (ret < 0) {
        printk(KERN_ERR "Failed to allocate device number\n");
        return ret;
    }
    printk(KERN_INFO "Device number allocated: %d:%d\n", MAJOR(dev_num), MINOR(dev_num));

    // 2. 初始化字符设备并注册
    cdev_init(&led_dev.cdev, &led_fops);
    led_dev.cdev.owner = THIS_MODULE;
    ret = cdev_add(&led_dev.cdev, dev_num, 1);
    if (ret < 0) {
        printk(KERN_ERR "Failed to add character device\n");
        goto unregister_chrdev;
    }

    // 3. 创建设备类
    led_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(led_class)) {
        printk(KERN_ERR "Failed to create device class\n");
        ret = PTR_ERR(led_class);
        goto cdev_del;
    }

    // 4. 创建设备节点
    led_device = device_create(led_class, NULL, dev_num, NULL, DEVICE_NAME);
    if (IS_ERR(led_device)) {
        printk(KERN_ERR "Failed to create device\n");
        ret = PTR_ERR(led_device);
        goto class_destroy;
    }

    // 5. 初始化设备状态
    led_dev.status = 0;  // 初始状态:灭
    printk(KERN_INFO "Virtual LED driver initialized\n");
    return 0;

class_destroy:
    class_destroy(led_class);
cdev_del:
    cdev_del(&led_dev.cdev);
unregister_chrdev:
    unregister_chrdev_region(dev_num, 1);
    return ret;
}

// 退出函数
static void __exit led_exit(void) {
    device_destroy(led_class, dev_num);
    class_destroy(led_class);
    cdev_del(&led_dev.cdev);
    unregister_chrdev_region(dev_num, 1);
    printk(KERN_INFO "Virtual LED driver exited\n");
}

module_init(led_init);
module_exit(led_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("lili");
MODULE_DESCRIPTION("Virtual LED Character Device Driver");
MODULE_VERSION("1.0");
    

readme.md

<!-- 虚拟led的实现:单个LED -->
ubunut-18.04

## Makefile
```
obj-m += virtual_led.o
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

default:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
```

## 编译
在文件夹下执行
```
make 
```
## 加载测试

```bash
sudo insmod virtual_led.ko  # 加载模块
sudo chmod 666 /dev/vir_led  # 赋予普通用户访问权限
echo "on" > /dev/vir_led    # 点亮LED
cat /dev/vir_led            # 查看状态(应显示"LED status: ON")
echo "off" > /dev/vir_led   # 熄灭LED
sudo rmmod virtual_led          # 卸载模块
dmesg | tail                    # 查看内核打印信息
```
上面的虚拟的led实在软件层面模拟led的开关,实际应该是通过硬件gpio来控制led的开关。
修改的方法为在led_write函数中添加gpio_set_value(LED_GPIO, value);


网站公告

今日签到

点亮在社区的每一天
去签到