Linux学习笔记——设备驱动
设备驱动
- 主设备号和次设备号的用途
- 主设备号(Major Number)
- 次设备号(Minor Number)
- 字符型驱动设备如何创建设备文件
- 手动创建
- 自动创建
- 如何在设备驱动程序中注册字符设备
- 方法一:使用 `cdev` 接口
- 方法二:使用传统注册函数
- /dev 目录下的设备文件如何创建
- 常见三种方式:
- Linux字符设备与块设备的主要区别
- 为什么驱动中操作物理地址前要使用 ioremap?
- insmod/rmmod 加载卸载模块时执行哪些函数?
- 注意事项:
- NAND 驱动的 probe 流程
- Linux 驱动开发中常用的调试方法
- ioremap(映射外设寄存器)
- 函数原型
- 头文件
- 参数说明
- 作用
- 举个简单例子:
- open(打开设备文件)
- 函数原型
- 参数说明
- 作用
- read(从设备或文件读取数据)
- 函数原型
- 参数说明
- 返回值
- 举个例子:
- write(向设备或文件写入数据)
- 函数原型
- 参数说明
- 返回值
- 注意事项
- 举个例子:
- copy_to_user(内核 → 用户)
- 函数原型
- 参数说明
- 返回值
- 用途
- 举个例子:
- copy_from_user(用户 → 内核)
- 函数原型
- 参数说明
- 返回值
- 用途
- 举个例子:
- 小结表
主设备号和次设备号的用途
主设备号(Major Number)
主设备号用于标识设备所对应的驱动程序。内核通过主设备号来找到设备所用的驱动程序。虽然现代Linux内核支持多个驱动共享主设备号,但一般仍遵循“一个主设备号对应一个驱动”的设计。
次设备号(Minor Number)
次设备号由驱动程序使用,用于区分主设备号所表示的多个设备实例。例如,一个硬盘驱动程序可能通过不同的次设备号来区分不同的分区。驱动程序可以根据次设备号获取特定设备的指针或者作为索引。
字符型驱动设备如何创建设备文件
手动创建
使用 mknod
命令创建:
mknod /dev/led c 250 0
/dev/led
:设备文件名c
:字符设备类型250
:主设备号0
:次设备号
自动创建
UDEV/MDEV 是运行在用户空间的设备管理程序,用于动态创建和删除设备节点。通常在系统启动脚本 /etc/init.d/rcS
中执行 mdev -s
实现自动创建设备节点。
如何在设备驱动程序中注册字符设备
方法一:使用 cdev
接口
void cdev_init(struct cdev *cdev, struct file_operations *fops);
cdev
:指向struct cdev
的指针,字符设备结构体。fops
:指向file_operations
结构体的指针,定义设备的操作函数。
方法二:使用传统注册函数
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
major
:主设备号(0表示自动分配)name
:驱动程序名称fops
:设备操作函数集
该函数会为 major
注册 0-255 的次设备号,并为每个设备建立默认的 cdev
结构体。
/dev 目录下的设备文件如何创建
常见三种方式:
-
devfs机制(已淘汰)
- 旧内核中使用,2.6 之前版本为主。
-
udev机制(主流)
- 基于内核通知事件,使用
device_create()
与class_create()
接口配合自动创建设备节点。
- 基于内核通知事件,使用
-
手动创建(mknod)
- 在脚本或调试阶段,开发者常用
mknod
命令手动创建设备文件。
- 在脚本或调试阶段,开发者常用
Linux字符设备与块设备的主要区别
类型 | 特点 | 示例 |
---|---|---|
字符设备 | 按字节顺序读写,不支持随机访问 | 串口、鼠标、键盘、摄像头等 |
块设备 | 支持随机访问,按块进行读写(如512B) | 硬盘、U盘、SD卡等 |
为什么驱动中操作物理地址前要使用 ioremap?
在保护模式下,CPU 不能直接访问物理地址。ioremap
会将设备的物理地址映射到虚拟地址空间,使驱动程序能通过虚拟地址访问硬件寄存器。它是内核提供的访问I/O内存的重要接口。
insmod/rmmod 加载卸载模块时执行哪些函数?
insmod
加载模块时,会执行module_init()
中注册的初始化函数。rmmod
卸载模块时,会执行module_exit()
中注册的退出函数。
注意事项:
init
中申请的资源,如内存、中断、GPIO 等,必须在exit
中进行对称释放,避免资源泄漏。
NAND 驱动的 probe 流程
当 NAND 驱动的 probe 函数执行时,大致步骤如下:
- 与 NAND 芯片通讯,读取 NAND ID。
- 查表获取 NAND 芯片的参数信息:如厂家、页大小、擦除块大小、芯片大小等。
- 调用
nand_scan()
函数自动完成如下任务:- 初始化硬件接口
- 扫描坏块表(BBT)
- 建立 MTD 结构体
Linux 驱动开发中常用的调试方法
- printk() 日志输出:驱动调试首选,输出到
dmesg
。 - 查看OOPS信息:系统崩溃时记录错误现场。
- strace:追踪用户态程序的系统调用。
- 内核 hacking 选项:编译内核时开启 debug 相关配置。
- ioctl 调试:用户程序通过 ioctl 与驱动交互,调试灵活性高。
- /proc 文件系统:通过
/proc
暴露调试信息给用户态。 - kgdb(内核级gdb):进行内核级断点调试。
好的,下面是面向小白整理的《Linux驱动开发常用函数》详细内容介绍,涵盖 ioremap
、open
、read
、write
、copy_to_user
、copy_from_user
函数的基本用法、参数解释和使用举例。
ioremap(映射外设寄存器)
函数原型
void *ioremap(unsigned long phys_addr, unsigned long size);
void *__ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);
头文件
#include <linux/io.h>
参数说明
phys_addr
:要映射的物理地址,即外设寄存器地址。size
:要映射的内存空间大小(单位:字节)。flags
:映射权限/属性(只在__ioremap()
中使用)。
作用
将外设的物理地址(比如网卡寄存器地址)映射到内核虚拟地址空间,这样驱动程序就可以通过虚拟地址来访问硬件寄存器。
举个简单例子:
假设有一块网卡,它的控制寄存器地址是 0xFE000000
,大小为 0x100:
void __iomem *ioaddr;
ioaddr = ioremap(0xFE000000, 0x100);
访问第 2 个寄存器(每个寄存器4字节):
iowrite32(value, ioaddr + 4); // 写入
val = ioread32(ioaddr + 4); // 读取
open(打开设备文件)
函数原型
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
参数说明
pathname
:文件或设备路径,如/dev/mydevice
。flags
:打开模式(如O_RDONLY
、O_WRONLY
、O_RDWR
)。mode
:创建文件时的权限(通常用不到)。
作用
用于打开一个设备文件(或普通文件),返回一个文件描述符 fd
,之后的 read
、write
都是基于这个 fd
。
read(从设备或文件读取数据)
函数原型
ssize_t read(int fd, void *buf, size_t count);
参数说明
fd
:文件描述符(通过open
获取)。buf
:读取数据保存的位置。count
:想读取的字节数。
返回值
- 返回实际读取的字节数;
- 返回 0 表示文件/设备已经没有数据;
- 失败返回 -1。
举个例子:
char buf[100];
int len = read(fd, buf, 100); // 从设备读取 100 字节
write(向设备或文件写入数据)
函数原型
ssize_t write(int fd, const void *buf, size_t count);
参数说明
fd
:文件描述符。buf
:要写入的数据缓冲区。count
:要写入的数据字节数。
返回值
- 返回实际写入的字节数;
- 失败返回 -1。
注意事项
write
不会自动处理缓冲区偏移,程序员需手动处理;write(fd, p1 + len, strlen(p1) - len)
是常见处理方式;- Linux内核中
BUFSIZ
是系统默认缓冲大小,但不是写入上限。
举个例子:
char msg[] = "Hello driver";
int len = write(fd, msg, strlen(msg)); // 写入字符串到设备
copy_to_user(内核 → 用户)
函数原型
unsigned long copy_to_user(void *to, const void *from, unsigned long n);
参数说明
to
:目标地址(用户空间)。from
:源地址(内核空间)。n
:拷贝的字节数。
返回值
- 成功返回 0;
- 失败返回未成功复制的字节数。
用途
在驱动中,把读取到的数据“传递”回用户空间程序。
举个例子:
copy_to_user(user_buf, kernel_buf, len);
copy_from_user(用户 → 内核)
函数原型
unsigned long copy_from_user(void *to, const void *from, unsigned long n);
参数说明
to
:目标地址(内核空间)。from
:源地址(用户空间)。n
:拷贝的字节数。
返回值
- 成功返回 0;
- 失败返回未成功复制的字节数。
用途
在驱动中,把用户空间传来的参数读入到内核空间以供处理。
举个例子:
copy_from_user(kernel_buf, user_buf, len);
小结表
函数名 | 功能简述 | 常见场景 |
---|---|---|
ioremap | 物理地址 → 虚拟地址 | 映射设备寄存器 |
open | 打开设备文件 | 用户程序打开 /dev/xxx |
read | 从设备读数据 | 应用程序读取 |
write | 向设备写数据 | 应用程序写入 |
copy_to_user | 内核 → 用户数据拷贝 | 驱动向用户返回数据 |
copy_from_user | 用户 → 内核数据拷贝 | 用户给驱动传参数 |