linux实现设备驱动-字符型设备驱动
一、linux驱动分类
(1)字符设备驱动 (Character Device Driver):
- 特点: 数据按字节流 (byte stream) 形式访问,即数据访问有严格的顺序性。
- 数据传输为字节流形式,顺序读写,不带缓存,无固定块大小。
- 通常采用中断驱动或轮询方式处理 I/O 请求,响应实时性较强。
- 设备节点通常位于 /dev 目录下,通过文件描述符进行操作。
- 示例: LED灯、按键、串口(UART)等,输入输出设备(鼠标、键盘、屏幕、摄像头)
- 管理方式: 通过设备号 (Device Number) 管理。
(2)块设备驱动 (Block Device Driver):
- 特点: 数据以固定大小的块 (block)(大小为512k) 为单位进行访问,带缓存,通常用于存储设备。
- 示例: 硬盘、SSD、U盘等。访问时可以随机读取任意位置的数据,不受顺序限制。
- 管理方式: 通过设备号 (Device Number) 管理。
(3)网络设备驱动 (Network Device Driver):
- 特点: 集成复杂的协议栈 (Protocol Stack),如TCP/IP、UDP等。
- 示例: 网卡。
- 管理方式: 按照名字 (Name) 管理,如
ens33。
(1)字符型设备驱动:
1.字符型设备驱动不自动生成设备节点(‘myled)
- 手动创建设备节点 :(在开发板中创建)
①前提条件:开发板已运行 Linux 内核,且需知道目标设备的主设备号和次设备号(可通过内核日志dmesg或驱动代码获取)。
②

③验证与权限设置:

- 自动生成设备节点 class_create() device_create()
在init函数中调用class_create和device_create接口,结合sysfs自动生成设备节点。

2.设备号 (Device Number):设备号是内核用于管理驱动设备的一个32位无符号整数 (u32)。它被划分为两部分:
- 主设备号 (Major Number): 高12位,用于区分设备类型。例如,所有LED灯使用同一个主设备号,所有按键使用另一个主设备号。
- 次设备号 (Minor Number): 低20位,用于区分同一类型设备中的不同个体。例如,4个LED灯分别使用不同的次设备号(0, 1, 2, 3)。
(2)嵌入式 Linux 设备驱动调用流程示意图分析:

1.用户应用层(user app)
- 应用程序通过
open(led)或fopen(led)发起设备操作请求,依赖libc库将请求转换为系统调用。
2. 根文件系统层(rootfs)
- 设备节点(如
led_1、key_2、uart_3)存在于 /dev 目录,是用户层与驱动层的交互入口。
3. 系统调用与驱动管理层
- 系统调用
syscall_open(led)将用户请求传入内核,内核通过设备号匹配对应的驱动(如 LED 驱动匹配设备号 1)。 - 内核驱动管理模块(含
open操作与驱动索引 “1、2、3”)负责分发请求到具体驱动。
4. 设备驱动层
- 包含
LED_DRV、KEY_DRV、UART_DRV三类字符设备驱动,是硬件操作的直接实现层,完成如 LED 点亮、按键检测、串口数据收发等具体功能。
5. 硬件设备层
- 对应实际硬件(LED、KEY、UART),驱动通过硬件寄存器、中断等机制完成物理层操作。
(3)字符型设备驱动开发流程:
将驱动程序写在内核顶层目录/driver/char目录下:

1.Linux 内核模块的 “标准注册接口”:
确保驱动能被内核正确加载和卸载,是字符设备驱动(如示例中的 LED 驱动)模块化管理的关键实现。


2.确定设备号,设备名称:
为驱动程序分配一个唯一的设备号。通常,主设备号从较大的数字开始分配(如255),因为较小的号码已被系统占用。
// 定义主设备号、次设备号和设备名称
#define MAJOR_NUM 248 // 主设备号
#define MINOR_NUM 0 // 次设备号
#define DEV_NAME "led1" // 设备名称
3.定义并实现操作方法 (file_operations结构体)
定义一个 struct file_operations 结构体(驱动层操作函数集合),其中包含指向驱动程序具体操作函数的指针,如 open, read, write, release (close)。
// 声明操作函数原型
static int open(struct inode *node, struct file *file);
static ssize_t read(struct file *file, char __user *buf, size_t len, loff_t *loff);
static ssize_t write(struct file *file, const char __user *buf, size_t len, loff_t *loff);
static int close(struct inode *node, struct file *file);// 定义并初始化 file_operations 结构体
static struct file_operations fops = {.owner = THIS_MODULE, // 指向当前模块,用于防止模块在被引用时被卸载.open = open, // 绑定 open 函数.read = read, // 绑定 read 函数.write = write, // 绑定 write 函数.release = close // 绑定 release (close) 函数
};
4. 定义一个cdev结构体:
描述字符型设备相关的信息(设备号、设备名称...),绑定设备号与操作方法
static dev_t devno; // 设备号变量
static struct cdev cdev; // cdev 结构体实例// 在模块初始化函数中执行绑定
static int __init led1_init(void)
{int ret = 0;// 1. 使用 MKDEV 宏组合主、次设备号devno = MKDEV(MAJOR_NUM, MINOR_NUM);// 2. 初始化 cdev 结构体,关联操作方法cdev_init(&cdev, &fops);// 3. 将 cdev 添加到内核中,指定设备号和数量ret = cdev_add(&cdev, devno, 1);if(ret < 0)goto err_cdev_add; // 错误处理...
}
5.实现对应init函数(向内核注册驱动入口函数 insmod xxx.ko)
①注册字符型设备
- 静态注册:register_chrdev_region(使用预先定义的
MAJOR_NUM和MINOR_NUM向内核申请设备号范围) - 动态注册:alloc_chrdev_region(如果静态省请未成功,系统自动分配一个未被占用的设备号)

②初始化cdev结构体:



6.驱动层实现硬件交互逻辑在read/write等接口中,通过寄存器操作、中断处理或 DMA 完成硬件数据读写。



7.注销与清理:实现对应exit函数(驱动注销函数 rmmod xxx)
①注销字符型设备:unregister_chrdev_region();
②注销cdev结构体 :cdev_del();
③删除类和设备节点 class_destroy() ;device_destroy()
④释放硬件资源 :iounmap();

8.应用程序调用流程
用户程序通过标准库函数(如 open, read, write)发起系统调用,最终由内核调用驱动程序中对应的函数。
// 用户空间应用程序 (led1_app.c)
int main(int argc, const char *argv[])
{int fd = open("/dev/led1", O_RDWR); // 调用 open 系统调用if(fd < 0) { /* 错误处理 */ }write(fd, "ledon", 5); // 调用 write 系统调用write(fd, "ledoff", 6); // 调用 write 系统调用close(fd); // 调用 close 系统调用return 0;
}
编写驱动→配置 Makefile,Kconfig→动态编译生成.ko→加载测试→(可选)集成到内核。
