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